diff --git a/anilist/src/commonMain/graphql/AiringQuery.graphql b/anilist/src/commonMain/graphql/AiringQuery.graphql index 53d7869..71b5cf1 100644 --- a/anilist/src/commonMain/graphql/AiringQuery.graphql +++ b/anilist/src/commonMain/graphql/AiringQuery.graphql @@ -1,6 +1,8 @@ query AiringQuery( $page: Int, $perPage: Int, + $statusVersion: Int, + $html: Boolean, $sort: [AiringSort], $airingAtGreater: Int ) { @@ -12,9 +14,24 @@ query AiringQuery( media { id, idMal, - isAdult, - genres, + status(version: $statusVersion), + description(asHtml: $html), + episodes, + duration, + chapters, countryOfOrigin, + popularity, + isFavourite, + isFavouriteBlocked, + isAdult, + format, + bannerImage, + coverImage { + extraLarge, + large, + medium, + color + }, averageScore, title { english, @@ -22,12 +39,42 @@ query AiringQuery( romaji, userPreferred }, - bannerImage, - coverImage { - color, - large, - extraLarge, - medium + nextAiringEpisode { + episode, + airingAt + }, + rankings { + rank, + allTime, + year, + season, + type + }, + genres, + characters(sort: [FAVOURITES_DESC,RELEVANCE]) { + nodes { + id, + name { + first, + middle + last, + full, + native, + userPreferred + }, + image { + large, + medium + } + } + }, + mediaListEntry { + score(format: POINT_5) + }, + trailer { + id, + site, + thumbnail } } } diff --git a/anilist/src/commonMain/graphql/SeasonQuery.graphql b/anilist/src/commonMain/graphql/SeasonQuery.graphql index 19935dd..f6f8a07 100644 --- a/anilist/src/commonMain/graphql/SeasonQuery.graphql +++ b/anilist/src/commonMain/graphql/SeasonQuery.graphql @@ -6,15 +6,32 @@ query SeasonQuery( $season: MediaSeason, $preventGenres: [String], $type: MediaType, - $adultContent: Boolean + $adultContent: Boolean, + $statusVersion: Int, + $html: Boolean, ) { Page(page: $page, perPage: $perPage) { media(sort: $sort, seasonYear: $year, season: $season, genre_not_in: $preventGenres, type: $type, isAdult: $adultContent) { id, idMal, - isAdult, - genres, + status(version: $statusVersion), + description(asHtml: $html), + episodes, + duration, + chapters, countryOfOrigin, + popularity, + isFavourite, + isFavouriteBlocked, + isAdult, + format, + bannerImage, + coverImage { + extraLarge, + large, + medium, + color + }, averageScore, title { english, @@ -22,12 +39,42 @@ query SeasonQuery( romaji, userPreferred }, - bannerImage, - coverImage { - color, - large, - medium, - extraLarge + nextAiringEpisode { + episode, + airingAt + }, + rankings { + rank, + allTime, + year, + season, + type + }, + genres, + characters(sort: [FAVOURITES_DESC,RELEVANCE]) { + nodes { + id, + name { + first, + middle + last, + full, + native, + userPreferred + }, + image { + large, + medium + } + } + }, + mediaListEntry { + score(format: POINT_5) + }, + trailer { + id, + site, + thumbnail } } } diff --git a/anilist/src/commonMain/graphql/TrendingQuery.graphql b/anilist/src/commonMain/graphql/TrendingQuery.graphql index 3b98339..a21c83b 100644 --- a/anilist/src/commonMain/graphql/TrendingQuery.graphql +++ b/anilist/src/commonMain/graphql/TrendingQuery.graphql @@ -2,6 +2,8 @@ query TrendingQuery( $page: Int, $perPage: Int, $type: MediaType, + $statusVersion: Int, + $html: Boolean, $sort: [MediaSort], $adultContent: Boolean, $preventGenres: [String] @@ -15,9 +17,24 @@ query TrendingQuery( ) { id, idMal, - isAdult, - genres, + status(version: $statusVersion), + description(asHtml: $html), + episodes, + duration, + chapters, countryOfOrigin, + popularity, + isFavourite, + isFavouriteBlocked, + isAdult, + format, + bannerImage, + coverImage { + extraLarge, + large, + medium, + color + }, averageScore, title { english, @@ -25,12 +42,42 @@ query TrendingQuery( romaji, userPreferred }, - bannerImage, - coverImage { - color, - large, - extraLarge, - medium + nextAiringEpisode { + episode, + airingAt + }, + rankings { + rank, + allTime, + year, + season, + type + }, + genres, + characters(sort: [FAVOURITES_DESC,RELEVANCE]) { + nodes { + id, + name { + first, + middle + last, + full, + native, + userPreferred + }, + image { + large, + medium + } + } + }, + mediaListEntry { + score(format: POINT_5) + }, + trailer { + id, + site, + thumbnail } } } diff --git a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/AiringTodayStateMachine.kt b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/AiringTodayStateMachine.kt index 75584e7..ff66358 100644 --- a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/AiringTodayStateMachine.kt +++ b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/AiringTodayStateMachine.kt @@ -119,7 +119,9 @@ class AiringTodayStateMachine( sort = Optional.present(listOf(AiringSort.TIME)), airingAtGreater = Optional.present( Clock.System.now().minus(1.hours).epochSeconds.toInt() - ) + ), + statusVersion = Optional.present(2), + html = Optional.present(true) ), adultContent = adultContent ) diff --git a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/Cache.kt b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/Cache.kt index 71d543d..9647101 100644 --- a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/Cache.kt +++ b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/Cache.kt @@ -29,7 +29,7 @@ internal object Cache { expireAfterWriteDuration = 2.hours } - private val medium = InMemoryKache( + private val medium = InMemoryKache( maxSize = 10L * 1024 * 1024 ) { strategy = KacheStrategy.LRU @@ -79,13 +79,13 @@ internal object Cache { }.getOrNull() ?: data } - suspend fun getMedium(key: MediumQuery): Medium.Full? { + suspend fun getMedium(key: MediumQuery): Medium? { return suspendCatching { medium.get(key) }.getOrNull() } - suspend fun setMedium(key: MediumQuery, data: Medium.Full): Medium.Full { + suspend fun setMedium(key: MediumQuery, data: Medium): Medium { return suspendCatching { medium.put(key, data) }.getOrNull() ?: data diff --git a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/MediumStateMachine.kt b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/MediumStateMachine.kt index de03cd1..9211cf5 100644 --- a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/MediumStateMachine.kt +++ b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/MediumStateMachine.kt @@ -42,7 +42,7 @@ class MediumStateMachine( query.execute().data ?: query.toFlow().saveFirstOrNull()?.data }.mapSuccess { it.Media?.let { data -> - State.Success(state.snapshot.query, Medium.Full(data)) + State.Success(state.snapshot.query, Medium(data)) } } @@ -88,14 +88,14 @@ class MediumStateMachine( MediumQuery( id = Optional.present(id), statusVersion = Optional.present(2), - html = Optional.present(false) + html = Optional.present(true) ) ) } data class Success( internal val query: MediumQuery, - val data: Medium.Full + val data: Medium ) : State data class Error( diff --git a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/TrendingAnimeStateMachine.kt b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/TrendingAnimeStateMachine.kt index b51e10e..f023f72 100644 --- a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/TrendingAnimeStateMachine.kt +++ b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/TrendingAnimeStateMachine.kt @@ -99,7 +99,9 @@ class TrendingAnimeStateMachine( Optional.present(AdultContent.Genre.allTags) } else { Optional.absent() - } + }, + statusVersion = Optional.present(2), + html = Optional.present(true) ) ) } diff --git a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/model/Character.kt b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/model/Character.kt index 0c6a063..872f100 100644 --- a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/model/Character.kt +++ b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/model/Character.kt @@ -1,7 +1,6 @@ package dev.datlag.aniflow.anilist.model -import dev.datlag.aniflow.anilist.CharacterQuery -import dev.datlag.aniflow.anilist.MediumQuery +import dev.datlag.aniflow.anilist.* import kotlinx.serialization.Serializable @Serializable @@ -93,6 +92,33 @@ data class Character( native = name.native?.ifBlank { null }, userPreferred = name.userPreferred?.ifBlank { null } ) + + constructor(name: TrendingQuery.Name) : this( + first = name.first?.ifBlank { null }, + middle = name.middle?.ifBlank { null }, + last = name.last?.ifBlank { null }, + full = name.full?.ifBlank { null }, + native = name.native?.ifBlank { null }, + userPreferred = name.userPreferred?.ifBlank { null } + ) + + constructor(name: AiringQuery.Name) : this( + first = name.first?.ifBlank { null }, + middle = name.middle?.ifBlank { null }, + last = name.last?.ifBlank { null }, + full = name.full?.ifBlank { null }, + native = name.native?.ifBlank { null }, + userPreferred = name.userPreferred?.ifBlank { null } + ) + + constructor(name: SeasonQuery.Name) : this( + first = name.first?.ifBlank { null }, + middle = name.middle?.ifBlank { null }, + last = name.last?.ifBlank { null }, + full = name.full?.ifBlank { null }, + native = name.native?.ifBlank { null }, + userPreferred = name.userPreferred?.ifBlank { null } + ) } @Serializable @@ -109,6 +135,21 @@ data class Character( large = image.large?.ifBlank { null }, medium = image.medium?.ifBlank { null }, ) + + constructor(image: TrendingQuery.Image) : this( + large = image.large?.ifBlank { null }, + medium = image.medium?.ifBlank { null }, + ) + + constructor(image: AiringQuery.Image) : this( + large = image.large?.ifBlank { null }, + medium = image.medium?.ifBlank { null } + ) + + constructor(image: SeasonQuery.Image) : this( + large = image.large?.ifBlank { null }, + medium = image.medium?.ifBlank { null } + ) } @Serializable @@ -186,6 +227,21 @@ data class Character( ) } + operator fun invoke(character: TrendingQuery.Node) : Character? { + val name = character.name?.let(::Name) ?: return null + val image = character.image?.let(::Image) ?: return null + + return Character( + id = character.id, + name = name, + image = image, + gender = null, + bloodType = null, + birthDate = null, + description = null + ) + } + operator fun invoke(character: CharacterQuery.Character) : Character? { val name = character.name?.let(::Name) ?: return null val image = character.image?.let(::Image) ?: return null @@ -200,5 +256,35 @@ data class Character( description = character.description?.ifBlank { null } ) } + + operator fun invoke(character: AiringQuery.Node) : Character? { + val name = character.name?.let(::Name) ?: return null + val image = character.image?.let(::Image) ?: return null + + return Character( + id = character.id, + name = name, + image = image, + gender = null, + bloodType = null, + birthDate = null, + description = null + ) + } + + operator fun invoke(character: SeasonQuery.Node) : Character? { + val name = character.name?.let(::Name) ?: return null + val image = character.image?.let(::Image) ?: return null + + return Character( + id = character.id, + name = name, + image = image, + gender = null, + bloodType = null, + birthDate = null, + description = null + ) + } } } \ No newline at end of file diff --git a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/model/Medium.kt b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/model/Medium.kt index 787f76d..1953978 100644 --- a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/model/Medium.kt +++ b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/model/Medium.kt @@ -6,28 +6,41 @@ import dev.datlag.aniflow.anilist.common.lastMonth import dev.datlag.aniflow.anilist.type.MediaFormat import dev.datlag.aniflow.anilist.type.MediaRankType import dev.datlag.aniflow.anilist.type.MediaStatus -import dev.datlag.aniflow.anilist.type.MediaTrailer import kotlinx.datetime.Month import kotlinx.serialization.Serializable +import kotlinx.serialization.Transient @Serializable -open class Medium( - open val id: Int, - open val idMal: Int?, - open val isAdult: Boolean, - open val genres: Set, - open val countryOfOrigin: String?, - open val averageScore: Int, - open val title: Title, - open val bannerImage: String?, - open val coverImage: CoverImage +data class Medium( + val id: Int, + val idMal: Int? = null, + val status: MediaStatus = MediaStatus.UNKNOWN__, + val description: String? = null, + val episodes: Int = -1, + val avgEpisodeDurationInMin: Int = -1, + val format: MediaFormat = MediaFormat.UNKNOWN__, + private val _isAdult: Boolean = false, + val genres: Set = emptySet(), + val countryOfOrigin: String? = null, + val averageScore: Int = -1, + val title: Title, + val bannerImage: String? = null, + val coverImage: CoverImage, + val nextAiringEpisode: NextAiring? = null, + val ranking: Set = emptySet(), + private val _characters: Set = emptySet(), + val entry: Entry? = null, + val trailer: Trailer? = null ) { constructor(trending: TrendingQuery.Medium) : this( id = trending.id, idMal = trending.idMal, - isAdult = trending.isAdult ?: trending.genresFilterNotNull()?.any { - AdultContent.Genre.exists(it) - } ?: false, + status = trending.status ?: MediaStatus.UNKNOWN__, + description = trending.description?.ifBlank { null }, + episodes = trending.episodes ?: -1, + avgEpisodeDurationInMin = trending.duration ?: -1, + format = trending.format ?: MediaFormat.UNKNOWN__, + _isAdult = trending.isAdult ?: false, genres = trending.genresFilterNotNull()?.toSet() ?: emptySet(), countryOfOrigin = trending.countryOfOrigin?.toString()?.ifBlank { null }, averageScore = trending.averageScore ?: -1, @@ -43,15 +56,36 @@ open class Medium( medium = trending.coverImage?.medium?.ifBlank { null }, large = trending.coverImage?.large?.ifBlank { null }, extraLarge = trending.coverImage?.extraLarge?.ifBlank { null } - ) + ), + nextAiringEpisode = trending.nextAiringEpisode?.let(::NextAiring), + ranking = trending.rankingsFilterNotNull()?.map(::Ranking)?.toSet() ?: emptySet(), + _characters = trending.characters?.nodesFilterNotNull()?.mapNotNull(Character::invoke)?.toSet() ?: emptySet(), + entry = trending.mediaListEntry?.let(::Entry), + trailer = trending.trailer?.let { + val site = it.site?.ifBlank { null } + val thumbnail = it.thumbnail?.ifBlank { null } + + if (site == null || thumbnail == null) { + null + } else { + Trailer( + id = it.id?.ifBlank { null }, + site = site, + thumbnail = thumbnail, + ) + } + } ) constructor(airing: AiringQuery.Media) : this( id = airing.id, idMal = airing.idMal, - isAdult = airing.isAdult ?: airing.genresFilterNotNull()?.any { - AdultContent.Genre.exists(it) - } ?: false, + status = airing.status ?: MediaStatus.UNKNOWN__, + description = airing.description?.ifBlank { null }, + episodes = airing.episodes ?: -1, + avgEpisodeDurationInMin = airing.duration ?: -1, + format = airing.format ?: MediaFormat.UNKNOWN__, + _isAdult = airing.isAdult ?: false, genres = airing.genresFilterNotNull()?.toSet() ?: emptySet(), countryOfOrigin = airing.countryOfOrigin?.toString()?.ifBlank { null }, averageScore = airing.averageScore ?: -1, @@ -67,15 +101,36 @@ open class Medium( medium = airing.coverImage?.medium?.ifBlank { null }, large = airing.coverImage?.large?.ifBlank { null }, extraLarge = airing.coverImage?.extraLarge?.ifBlank { null } - ) + ), + nextAiringEpisode = airing.nextAiringEpisode?.let(::NextAiring), + ranking = airing.rankingsFilterNotNull()?.map(::Ranking)?.toSet() ?: emptySet(), + _characters = airing.characters?.nodesFilterNotNull()?.mapNotNull(Character::invoke)?.toSet() ?: emptySet(), + entry = airing.mediaListEntry?.let(::Entry), + trailer = airing.trailer?.let { + val site = it.site?.ifBlank { null } + val thumbnail = it.thumbnail?.ifBlank { null } + + if (site == null || thumbnail == null) { + null + } else { + Trailer( + id = it.id?.ifBlank { null }, + site = site, + thumbnail = thumbnail, + ) + } + } ) constructor(season: SeasonQuery.Medium) : this( id = season.id, idMal = season.idMal, - isAdult = season.isAdult ?: season.genresFilterNotNull()?.any { - AdultContent.Genre.exists(it) - } ?: false, + status = season.status ?: MediaStatus.UNKNOWN__, + description = season.description?.ifBlank { null }, + episodes = season.episodes ?: -1, + avgEpisodeDurationInMin = season.duration ?: -1, + format = season.format ?: MediaFormat.UNKNOWN__, + _isAdult = season.isAdult ?: false, genres = season.genresFilterNotNull()?.toSet() ?: emptySet(), countryOfOrigin = season.countryOfOrigin?.toString()?.ifBlank { null }, averageScore = season.averageScore ?: -1, @@ -91,15 +146,36 @@ open class Medium( medium = season.coverImage?.medium?.ifBlank { null }, large = season.coverImage?.large?.ifBlank { null }, extraLarge = season.coverImage?.extraLarge?.ifBlank { null } - ) + ), + nextAiringEpisode = season.nextAiringEpisode?.let(::NextAiring), + ranking = season.rankingsFilterNotNull()?.map(::Ranking)?.toSet() ?: emptySet(), + _characters = season.characters?.nodesFilterNotNull()?.mapNotNull(Character::invoke)?.toSet() ?: emptySet(), + entry = season.mediaListEntry?.let(::Entry), + trailer = season.trailer?.let { + val site = it.site?.ifBlank { null } + val thumbnail = it.thumbnail?.ifBlank { null } + + if (site == null || thumbnail == null) { + null + } else { + Trailer( + id = it.id?.ifBlank { null }, + site = site, + thumbnail = thumbnail, + ) + } + } ) constructor(query: MediumQuery.Media) : this( id = query.id, idMal = query.idMal, - isAdult = query.isAdult ?: query.genresFilterNotNull()?.any { - AdultContent.Genre.exists(it) - } ?: false, + status = query.status ?: MediaStatus.UNKNOWN__, + description = query.description?.ifBlank { null }, + episodes = query.episodes ?: -1, + avgEpisodeDurationInMin = query.duration ?: -1, + format = query.format ?: MediaFormat.UNKNOWN__, + _isAdult = query.isAdult ?: false, genres = query.genresFilterNotNull()?.toSet() ?: emptySet(), countryOfOrigin = query.countryOfOrigin?.toString()?.ifBlank { null }, averageScore = query.averageScore ?: -1, @@ -115,9 +191,35 @@ open class Medium( medium = query.coverImage?.medium?.ifBlank { null }, large = query.coverImage?.large?.ifBlank { null }, extraLarge = query.coverImage?.extraLarge?.ifBlank { null } - ) + ), + nextAiringEpisode = query.nextAiringEpisode?.let(::NextAiring), + ranking = query.rankingsFilterNotNull()?.map(::Ranking)?.toSet() ?: emptySet(), + _characters = query.characters?.nodesFilterNotNull()?.mapNotNull(Character::invoke)?.toSet() ?: emptySet(), + entry = query.mediaListEntry?.let(::Entry), + trailer = query.trailer?.let { + val site = it.site?.ifBlank { null } + val thumbnail = it.thumbnail?.ifBlank { null } + + if (site == null || thumbnail == null) { + null + } else { + Trailer( + id = it.id?.ifBlank { null }, + site = site, + thumbnail = thumbnail + ) + } + } ) + @Transient + val isAdult: Boolean = _isAdult || genres.any { + AdultContent.Genre.exists(it) + } + + @Transient + val characters: Set = _characters.filterNot { it.id == 36309 }.toSet() + @Serializable data class Title( /** @@ -200,138 +302,135 @@ open class Medium( season = ranking.season?.lastMonth(), type = ranking.type ) + + constructor(ranking: TrendingQuery.Ranking) : this( + rank = ranking.rank, + allTime = ranking.allTime ?: (ranking.season?.lastMonth() == null && ranking.year == null), + year = ranking.year ?: -1, + season = ranking.season?.lastMonth(), + type = ranking.type + ) + + constructor(ranking: AiringQuery.Ranking) : this( + rank = ranking.rank, + allTime = ranking.allTime ?: (ranking.season?.lastMonth() == null && ranking.year == null), + year = ranking.year ?: -1, + season = ranking.season?.lastMonth(), + type = ranking.type + ) + + constructor(ranking: SeasonQuery.Ranking) : this( + rank = ranking.rank, + allTime = ranking.allTime ?: (ranking.season?.lastMonth() == null && ranking.year == null), + year = ranking.year ?: -1, + season = ranking.season?.lastMonth(), + type = ranking.type + ) } - data class Full( - override val id: Int, - override val idMal: Int?, - val status: MediaStatus, - val description: String?, - val episodes: Int, - val avgEpisodeDurationInMin: Int?, - val format: MediaFormat, - override val isAdult: Boolean, - override val genres: Set, - override val countryOfOrigin: String?, - override val averageScore: Int, - override val title: Title, - override val bannerImage: String?, - override val coverImage: CoverImage, - val nextAiringEpisode: MediumQuery.NextAiringEpisode?, - val ranking: Set, - val characters: Set, - val entry: Entry?, - val trailer: Trailer? - ) : Medium( - id = id, - idMal = idMal, - isAdult = isAdult, - genres = genres, - countryOfOrigin = countryOfOrigin, - averageScore = averageScore, - title = title, - bannerImage = bannerImage, - coverImage = coverImage + @Serializable + data class Entry( + val score: Double? ) { - constructor(medium: Medium, mediumQuery: MediumQuery.Media) : this( - id = medium.id, - idMal = medium.idMal, - status = mediumQuery.status ?: MediaStatus.UNKNOWN__, - description = mediumQuery.description?.ifBlank { null }, - episodes = mediumQuery.episodes ?: -1, - avgEpisodeDurationInMin = mediumQuery.duration ?: -1, - format = mediumQuery.format ?: MediaFormat.UNKNOWN__, - isAdult = medium.isAdult, - genres = medium.genres, - countryOfOrigin = medium.countryOfOrigin?.ifBlank { null }, - averageScore = medium.averageScore, - title = medium.title, - bannerImage = medium.bannerImage?.ifBlank { null }, - coverImage = medium.coverImage, - nextAiringEpisode = mediumQuery.nextAiringEpisode, - ranking = mediumQuery.rankingsFilterNotNull()?.map(::Ranking)?.toSet() ?: emptySet(), - characters = mediumQuery.characters?.nodesFilterNotNull()?.mapNotNull(Character::invoke)?.filterNot { - it.id == 36309 // Narrator - }?.toSet() ?: emptySet(), - entry = mediumQuery.mediaListEntry?.let(::Entry), - trailer = mediumQuery.trailer?.let { - val site = it.site?.ifBlank { null } - val thumbnail = it.thumbnail?.ifBlank { null } - - if (site == null || thumbnail == null) { - null - } else { - Trailer( - id = it.id?.ifBlank { null }, - site = site, - thumbnail = thumbnail - ) - } - } + constructor(entry: MediumQuery.MediaListEntry) : this( + score = entry.score ) - constructor(mediumQuery: MediumQuery.Media) : this( - medium = Medium(mediumQuery), - mediumQuery = mediumQuery + constructor(entry: TrendingQuery.MediaListEntry) : this( + score = entry.score ) - data class Entry( - val score: Double? - ) { - constructor(entry: MediumQuery.MediaListEntry) : this( - score = entry.score - ) - } + constructor(entry: AiringQuery.MediaListEntry) : this( + score = entry.score + ) + + constructor(entry: SeasonQuery.MediaListEntry) : this( + score = entry.score + ) + } - data class Trailer( - val id: String?, - val site: String, - val thumbnail: String - ) { - val website: String = run { - val prefix = if (site.startsWith("https://", ignoreCase = true) || site.startsWith("http://", ignoreCase = true)) { - "" - } else { - "https://" - } - val suffix = if (site.substringAfterLast('.', missingDelimiterValue = "").isBlank()) { - ".com" - } else { - "" - } - "$prefix$site$suffix" + @Serializable + data class Trailer( + val id: String?, + val site: String, + val thumbnail: String + ) { + val website: String = run { + val prefix = if (site.startsWith("https://", ignoreCase = true) || site.startsWith("http://", ignoreCase = true)) { + "" + } else { + "https://" + } + val suffix = if (site.substringAfterLast('.', missingDelimiterValue = "").isBlank()) { + ".com" + } else { + "" } + "$prefix$site$suffix" + } - val isYoutube: Boolean = site.contains("youtu.be", ignoreCase = true) - || site.contains("youtube", ignoreCase = true) + val isYoutube: Boolean = site.contains("youtu.be", ignoreCase = true) + || site.contains("youtube", ignoreCase = true) - val isDailymotion: Boolean = site.contains("dailymotion", ignoreCase = true) + val isDailymotion: Boolean = site.contains("dailymotion", ignoreCase = true) - private val youtubeVideoId: String? = run { - val afterVi = thumbnail.substringAfter( - delimiter = "vi/", - missingDelimiterValue = thumbnail.substringAfter( - delimiter = "vi_webp/", - missingDelimiterValue = "" - ) - ).ifBlank { null } ?: return@run null + private val youtubeVideoId: String? = run { + val afterVi = thumbnail.substringAfter( + delimiter = "vi/", + missingDelimiterValue = thumbnail.substringAfter( + delimiter = "vi_webp/", + missingDelimiterValue = "" + ) + ).ifBlank { null } ?: return@run null - afterVi.substringBefore('/', missingDelimiterValue = "").ifBlank { null } - } + afterVi.substringBefore('/', missingDelimiterValue = "").ifBlank { null } + } - private val youtubeVideo = (id ?: youtubeVideoId)?.let { - "https://youtube.com/watch?v=$it" - } + private val youtubeVideo = (id ?: youtubeVideoId)?.let { + "https://youtube.com/watch?v=$it" + } - private val dailymotionVideo = id?.let { - "https://dailymotion.com/video/$it" - } + private val dailymotionVideo = id?.let { + "https://dailymotion.com/video/$it" + } - val videoUrl = when { - isYoutube -> youtubeVideo - isDailymotion -> dailymotionVideo - else -> null - } + val videoUrl = when { + isYoutube -> youtubeVideo + isDailymotion -> dailymotionVideo + else -> null } } + + @Serializable + data class NextAiring( + /** + * The airing episode number + */ + val episodes: Int, + + /** + * The time the episode airs at + */ + val airingAt: Int + ) { + constructor(nextAiringEpisode: TrendingQuery.NextAiringEpisode) : this( + episodes = nextAiringEpisode.episode, + airingAt = nextAiringEpisode.airingAt + ) + + constructor(nextAiringEpisode: MediumQuery.NextAiringEpisode) : this( + episodes = nextAiringEpisode.episode, + airingAt = nextAiringEpisode.airingAt + ) + + constructor(nextAiringEpisode: AiringQuery.NextAiringEpisode) : this( + episodes = nextAiringEpisode.episode, + airingAt = nextAiringEpisode.airingAt + ) + + constructor(nextAiringEpisode: SeasonQuery.NextAiringEpisode) : this( + episodes = nextAiringEpisode.episode, + airingAt = nextAiringEpisode.airingAt + ) + } } diff --git a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/state/SeasonState.kt b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/state/SeasonState.kt index 25b7a8b..06da836 100644 --- a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/state/SeasonState.kt +++ b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/state/SeasonState.kt @@ -43,7 +43,9 @@ sealed interface SeasonState { Optional.absent() } else { Optional.present(season) - } + }, + statusVersion = Optional.present(2), + html = Optional.present(true) ) ) diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/common/ExtendMedium.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/common/ExtendMedium.kt index d42f45d..e04402a 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/common/ExtendMedium.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/common/ExtendMedium.kt @@ -70,7 +70,7 @@ fun MediaFormat.text(): StringResource { } } -fun Medium.Full.formatText(): StringResource { +fun Medium.formatText(): StringResource { return this.format.text() } @@ -85,7 +85,7 @@ fun MediaStatus.text(): StringResource { } } -fun Medium.Full.statusText(): StringResource { +fun Medium.statusText(): StringResource { return this.status.text() } @@ -103,7 +103,7 @@ fun Collection.rated(): Medium.Ranking? { ).firstOrNull() } -fun Medium.Full.rated(): Medium.Ranking? = this.ranking.rated() +fun Medium.rated(): Medium.Ranking? = this.ranking.rated() fun Collection.popular(): Medium.Ranking? { val filtered = this.filter { it.type == MediaRankType.POPULAR } @@ -119,7 +119,7 @@ fun Collection.popular(): Medium.Ranking? { ).firstOrNull() } -fun Medium.Full.popular(): Medium.Ranking? = this.ranking.popular() +fun Medium.popular(): Medium.Ranking? = this.ranking.popular() expect fun Character.Name.preferred(): String @@ -138,17 +138,13 @@ fun SearchResponse.Result.AniList.asMedium(): Medium { return Medium( id = this.id, idMal = this.idMal, - isAdult = this.isAdult, - genres = emptySet(), - bannerImage = null, + _isAdult = this.isAdult, coverImage = Medium.CoverImage( color = null, medium = null, large = null, extraLarge = null ), - countryOfOrigin = null, - averageScore = -1, title = this.title.asMediumTitle() ) } diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/RootComponent.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/RootComponent.kt index 815ab1d..a7182fe 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/RootComponent.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/RootComponent.kt @@ -5,6 +5,8 @@ import com.arkivanov.decompose.ComponentContext import com.arkivanov.decompose.ExperimentalDecomposeApi import com.arkivanov.decompose.extensions.compose.stack.Children import com.arkivanov.decompose.extensions.compose.stack.animation.predictiveback.predictiveBackAnimation +import com.arkivanov.decompose.extensions.compose.stack.animation.slide +import com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation import com.arkivanov.decompose.router.stack.StackNavigation import com.arkivanov.decompose.router.stack.childStack import com.arkivanov.decompose.router.stack.pop @@ -57,6 +59,9 @@ class RootComponent( stack = stack, animation = predictiveBackAnimation( backHandler = this.backHandler, + fallbackAnimation = stackAnimation( + animator = slide() + ), onBack = { navigation.pop() } diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/MediumComponent.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/MediumComponent.kt index 4b05f52..5982d90 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/MediumComponent.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/MediumComponent.kt @@ -33,7 +33,7 @@ interface MediumComponent : ContentHolderComponent { val characters: StateFlow> val rating: StateFlow val alreadyAdded: StateFlow - val trailer: StateFlow + val trailer: StateFlow val bsAvailable: Boolean diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/MediumScreen.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/MediumScreen.kt index f5e511e..755c5ec 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/MediumScreen.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/MediumScreen.kt @@ -244,6 +244,7 @@ fun MediumScreen(component: MediumComponent) { var descriptionExpandable by remember(description) { mutableStateOf(false) } var descriptionExpanded by remember(description) { mutableStateOf(false) } val characters by component.characters.collectAsStateWithLifecycle() + val trailer by component.trailer.collectAsStateWithLifecycle() LazyColumn( state = listState, @@ -518,40 +519,49 @@ fun MediumScreen(component: MediumComponent) { } } } - item { - val trailer by component.trailer.collectAsStateWithLifecycle() - val uriHandler = LocalUriHandler.current - trailer?.let { t -> - Card( - modifier = Modifier.fillParentMaxWidth().height(200.dp).padding(16.dp), - onClick = { - uriHandler.openUri(t.videoUrl ?: t.website) - } - ) { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center + trailer?.let { t -> + item { + Text( + modifier = Modifier.padding(top = 16.dp).padding(horizontal = 16.dp), + text = stringResource(SharedRes.strings.trailer), + style = MaterialTheme.typography.headlineSmall + ) + } + item { + val uriHandler = LocalUriHandler.current + + trailer?.let { t -> + Card( + modifier = Modifier.fillParentMaxWidth().height(200.dp).padding(16.dp), + onClick = { + uriHandler.openUri(t.videoUrl ?: t.website) + } ) { - AsyncImage( + Box( modifier = Modifier.fillMaxSize(), - model = t.thumbnail, - contentDescription = null, - contentScale = ContentScale.Crop - ) - if (t.isYoutube) { - Image( - modifier = Modifier.size(48.dp), - painter = painterResource(SharedRes.images.youtube), + contentAlignment = Alignment.Center + ) { + AsyncImage( + modifier = Modifier.fillMaxSize(), + model = t.thumbnail, contentDescription = null, - colorFilter = ColorFilter.tint(LocalContentColor.current) - ) - } else { - Icon( - modifier = Modifier.size(48.dp), - imageVector = Icons.Filled.PlayCircleFilled, - contentDescription = null + contentScale = ContentScale.Crop ) + if (t.isYoutube) { + Image( + modifier = Modifier.size(48.dp), + painter = painterResource(SharedRes.images.youtube), + contentDescription = null, + colorFilter = ColorFilter.tint(LocalContentColor.current) + ) + } else { + Icon( + modifier = Modifier.size(48.dp), + imageVector = Icons.Filled.PlayCircleFilled, + contentDescription = null + ) + } } } } diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/MediumScreenComponent.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/MediumScreenComponent.kt index bb864f1..f87cdab 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/MediumScreenComponent.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/MediumScreenComponent.kt @@ -113,7 +113,7 @@ class MediumScreenComponent( ).stateIn( scope = ioScope(), started = SharingStarted.WhileSubscribed(), - initialValue = null + initialValue = initialMedium.description ) override val translatedDescription: MutableStateFlow = MutableStateFlow(null) @@ -135,17 +135,17 @@ class MediumScreenComponent( ).stateIn( scope = ioScope(), started = SharingStarted.WhileSubscribed(), - initialValue = MediaFormat.UNKNOWN__ + initialValue = initialMedium.format ) - private val nextAiringEpisode: StateFlow = mediumSuccessState.mapNotNull { + private val nextAiringEpisode: StateFlow = mediumSuccessState.mapNotNull { it?.data?.nextAiringEpisode }.flowOn( context = ioDispatcher() ).stateIn( scope = ioScope(), started = SharingStarted.WhileSubscribed(), - initialValue = null + initialValue = initialMedium.nextAiringEpisode ) override val episodes: StateFlow = combine( @@ -157,9 +157,9 @@ class MediumScreenComponent( episodes.ifValueOrNull(-1) { if (airing != null) { if (Instant.fromEpochSeconds(airing.airingAt.toLong()) <= Clock.System.now()) { - airing.episode + airing.episodes } else { - airing.episode - 1 + airing.episodes - 1 } } else { -1 @@ -174,9 +174,9 @@ class MediumScreenComponent( val airing = nextAiringEpisode.value ?: return@run -1 if (Instant.fromEpochSeconds(airing.airingAt.toLong()) <= Clock.System.now()) { - airing.episode + airing.episodes } else { - airing.episode - 1 + airing.episodes - 1 } } ) @@ -188,7 +188,7 @@ class MediumScreenComponent( ).stateIn( scope = ioScope(), started = SharingStarted.WhileSubscribed(), - initialValue = -1 + initialValue = initialMedium.avgEpisodeDurationInMin ) override val status: StateFlow = mediumSuccessState.mapNotNull { @@ -198,7 +198,7 @@ class MediumScreenComponent( ).stateIn( scope = ioScope(), started = SharingStarted.WhileSubscribed(), - initialValue = MediaStatus.UNKNOWN__ + initialValue = initialMedium.status ) override val rated: StateFlow = mediumSuccessState.mapNotNull { @@ -208,7 +208,7 @@ class MediumScreenComponent( ).stateIn( scope = ioScope(), started = SharingStarted.WhileSubscribed(), - initialValue = null + initialValue = initialMedium.rated() ) override val popular: StateFlow = mediumSuccessState.mapNotNull { @@ -218,7 +218,7 @@ class MediumScreenComponent( ).stateIn( scope = ioScope(), started = SharingStarted.WhileSubscribed(), - initialValue = null + initialValue = initialMedium.popular() ) override val score: StateFlow = mediumSuccessState.mapNotNull { @@ -264,7 +264,7 @@ class MediumScreenComponent( initialValue = initialMedium.id ) - private val changedRating: MutableStateFlow = MutableStateFlow(-1) + private val changedRating: MutableStateFlow = MutableStateFlow(initialMedium.entry?.score?.toInt() ?: -1) override val rating: StateFlow = combine( mediumSuccessState.map { it?.data?.entry?.score?.toInt() @@ -284,14 +284,14 @@ class MediumScreenComponent( initialValue = changedRating.value ) - override val trailer: StateFlow = mediumSuccessState.mapNotNull { + override val trailer: StateFlow = mediumSuccessState.mapNotNull { it?.data?.trailer }.flowOn( context = ioDispatcher() ).stateIn( scope = ioScope(), started = SharingStarted.WhileSubscribed(), - initialValue = null + initialValue = initialMedium.trailer ) override val alreadyAdded: StateFlow = mediumSuccessState.mapNotNull { @@ -301,7 +301,7 @@ class MediumScreenComponent( ).stateIn( scope = ioScope(), started = SharingStarted.WhileSubscribed(), - initialValue = false + initialValue = initialMedium.entry != null ) private val userSettings by di.instance() diff --git a/composeApp/src/commonMain/moko-resources/base/strings.xml b/composeApp/src/commonMain/moko-resources/base/strings.xml index 1b119cf..db294d1 100644 --- a/composeApp/src/commonMain/moko-resources/base/strings.xml +++ b/composeApp/src/commonMain/moko-resources/base/strings.xml @@ -32,4 +32,5 @@ Birthdate Add Edit + Trailer