From bf096cdd3c2795951f51db10af593b60cc302765 Mon Sep 17 00:00:00 2001 From: DatLag Date: Sat, 25 May 2024 19:05:01 +0200 Subject: [PATCH] use immutable collection to have stable classes --- anilist/build.gradle.kts | 1 + .../aniflow/anilist/ListStateMachine.kt | 3 +- .../dev/datlag/aniflow/anilist/StateSaver.kt | 3 +- .../aniflow/anilist/common/ExtendApollo.kt | 3 +- .../datlag/aniflow/anilist/model/Medium.kt | 37 +++++++++++-------- .../aniflow/anilist/model/PageMediaQuery.kt | 16 ++++---- .../aniflow/anilist/state/DiscoverState.kt | 13 ++++--- .../aniflow/anilist/state/HomeAiringState.kt | 6 ++- .../aniflow/anilist/state/HomeDefaultState.kt | 6 ++- .../datlag/aniflow/anilist/state/ListState.kt | 16 ++++---- .../aniflow/anilist/state/SearchState.kt | 6 ++- .../other/BurningSeriesResolver.android.kt | 27 ++++++++------ .../aniflow/other/BurningSeriesResolver.kt | 8 ++-- .../ui/custom/speeddial/SubSpeedDialFABs.kt | 3 +- .../screen/medium/MediumComponent.kt | 8 ++-- .../screen/medium/MediumScreenComponent.kt | 12 ++++-- .../screen/medium/component/BSDialog.kt | 3 +- .../medium/component/CharacterSection.kt | 3 +- .../screen/medium/component/FABContent.kt | 6 ++- .../screen/medium/component/GenreSection.kt | 3 +- .../ui/navigation/screen/nekos/NekosScreen.kt | 6 +++ .../other/BurningSeriesResolver.ios.kt | 15 +++++--- gradle.properties | 8 +++- gradle/libs.versions.toml | 2 + model/build.gradle.kts | 2 + .../serializer/ImmutableListSerializer.kt | 32 ++++++++++++++++ .../serializer/ImmutableSetSerializer.kt | 32 ++++++++++++++++ nekos/build.gradle.kts | 1 + .../datlag/aniflow/nekos/NekosRepository.kt | 3 +- .../aniflow/nekos/model/ImagesResponse.kt | 9 +++-- settings/build.gradle.kts | 1 + .../aniflow/settings/model/CharLanguage.kt | 4 +- .../datlag/aniflow/settings/model/Color.kt | 4 +- .../aniflow/settings/model/TitleLanguage.kt | 4 +- trace/build.gradle.kts | 1 + .../aniflow/trace/model/SearchResponse.kt | 14 +++++-- 36 files changed, 230 insertions(+), 91 deletions(-) create mode 100644 model/src/commonMain/kotlin/dev/datlag/aniflow/model/serializer/ImmutableListSerializer.kt create mode 100644 model/src/commonMain/kotlin/dev/datlag/aniflow/model/serializer/ImmutableSetSerializer.kt diff --git a/anilist/build.gradle.kts b/anilist/build.gradle.kts index 97ad102..7e5db14 100644 --- a/anilist/build.gradle.kts +++ b/anilist/build.gradle.kts @@ -35,6 +35,7 @@ kotlin { implementation(libs.datetime) implementation(libs.serialization) implementation(libs.tooling) + api(libs.immutable) implementation(project(":model")) implementation(project(":firebase")) diff --git a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/ListStateMachine.kt b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/ListStateMachine.kt index 350fb3f..36befb0 100644 --- a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/ListStateMachine.kt +++ b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/ListStateMachine.kt @@ -8,6 +8,7 @@ import dev.datlag.aniflow.anilist.state.ListState import dev.datlag.aniflow.anilist.type.MediaListStatus import dev.datlag.aniflow.anilist.type.MediaType import dev.datlag.aniflow.firebase.FirebaseFactory +import kotlinx.collections.immutable.persistentSetOf import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -92,7 +93,7 @@ class ListStateMachine( p to r }.runningFold(initial = currentState) { accumulator, (p, r) -> return@runningFold (if (p <= 0) { - ListState.fromResponse(emptyList(), r) + ListState.fromResponse(persistentSetOf(), r) } else { ListState.fromResponse(accumulator, r) }).also { state -> diff --git a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/StateSaver.kt b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/StateSaver.kt index 6ae52c1..0511c7d 100644 --- a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/StateSaver.kt +++ b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/StateSaver.kt @@ -5,6 +5,7 @@ import dev.datlag.aniflow.anilist.state.HomeAiringState import dev.datlag.aniflow.anilist.state.HomeDefaultState import dev.datlag.aniflow.anilist.state.ListState import dev.datlag.aniflow.anilist.state.SearchState +import kotlinx.collections.immutable.persistentSetOf internal object StateSaver { var airingState: HomeAiringState = HomeAiringState.Loading @@ -16,7 +17,7 @@ internal object StateSaver { var searchState: SearchState = SearchState.None var searchQuery: String? = null - var listState: ListState = ListState.Loading(emptyList()) + var listState: ListState = ListState.Loading(persistentSetOf()) var discoverState: DiscoverState = DiscoverState.Recommended.Loading.WatchList } \ No newline at end of file diff --git a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/common/ExtendApollo.kt b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/common/ExtendApollo.kt index 639effc..053eb06 100644 --- a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/common/ExtendApollo.kt +++ b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/common/ExtendApollo.kt @@ -9,6 +9,7 @@ import dev.datlag.aniflow.anilist.type.MediaListSort import dev.datlag.aniflow.anilist.type.MediaListStatus import dev.datlag.aniflow.anilist.type.MediaSeason import dev.datlag.aniflow.anilist.type.MediaType +import kotlinx.collections.immutable.persistentListOf fun ApolloResponse<*>.hasNonCacheError(): Boolean { return when (exception) { @@ -62,4 +63,4 @@ fun Optional.Companion.presentIfNot(predicate: Boolean, value: V) = if (pred present(value) } -fun Optional.Companion.presentAsList(vararg value: V) = present(listOf(*value)) \ No newline at end of file +fun Optional.Companion.presentAsList(vararg value: V) = present(persistentListOf(*value)) \ 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 03b9461..a67d773 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 @@ -8,6 +8,11 @@ import dev.datlag.aniflow.anilist.common.toLocalDate import dev.datlag.aniflow.anilist.type.* import dev.datlag.aniflow.model.ifValue import dev.datlag.aniflow.model.toInt +import kotlinx.collections.immutable.ImmutableCollection +import kotlinx.collections.immutable.ImmutableSet +import kotlinx.collections.immutable.persistentSetOf +import kotlinx.collections.immutable.toImmutableList +import kotlinx.collections.immutable.toImmutableSet import kotlinx.datetime.Clock import kotlinx.datetime.Instant import kotlinx.datetime.LocalDate @@ -26,7 +31,7 @@ data class Medium( val avgEpisodeDurationInMin: Int = -1, val format: MediaFormat = MediaFormat.UNKNOWN__, private val _isAdult: Boolean = false, - val genres: Set = emptySet(), + val genres: ImmutableSet = persistentSetOf(), val countryOfOrigin: String? = null, val averageScore: Int = -1, val title: Title = Title( @@ -43,8 +48,8 @@ data class Medium( color = null ), val nextAiringEpisode: NextAiring? = null, - val ranking: Set = emptySet(), - private val _characters: Set = emptySet(), + val ranking: ImmutableCollection = persistentSetOf(), + private val _characters: ImmutableCollection = persistentSetOf(), val entry: Entry? = null, val trailer: Trailer? = null, val isFavorite: Boolean = false, @@ -64,7 +69,7 @@ data class Medium( avgEpisodeDurationInMin = trending.duration ?: -1, format = trending.format ?: MediaFormat.UNKNOWN__, _isAdult = trending.isAdult ?: false, - genres = trending.genresFilterNotNull()?.toSet() ?: emptySet(), + genres = trending.genresFilterNotNull()?.toImmutableSet() ?: persistentSetOf(), countryOfOrigin = trending.countryOfOrigin?.toString()?.ifBlank { null }, averageScore = trending.averageScore ?: -1, title = Title( @@ -81,8 +86,8 @@ data class Medium( 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(), + ranking = trending.rankingsFilterNotNull()?.map(::Ranking)?.toImmutableSet() ?: persistentSetOf(), + _characters = trending.characters?.nodesFilterNotNull()?.mapNotNull(Character::invoke)?.toImmutableSet() ?: persistentSetOf(), entry = trending.mediaListEntry?.let(::Entry), trailer = trending.trailer?.let { val site = it.site?.ifBlank { null } @@ -116,7 +121,7 @@ data class Medium( avgEpisodeDurationInMin = airing.duration ?: -1, format = airing.format ?: MediaFormat.UNKNOWN__, _isAdult = airing.isAdult ?: false, - genres = airing.genresFilterNotNull()?.toSet() ?: emptySet(), + genres = airing.genresFilterNotNull()?.toImmutableSet() ?: persistentSetOf(), countryOfOrigin = airing.countryOfOrigin?.toString()?.ifBlank { null }, averageScore = airing.averageScore ?: -1, title = Title( @@ -133,8 +138,8 @@ data class Medium( 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(), + ranking = airing.rankingsFilterNotNull()?.map(::Ranking)?.toImmutableSet() ?: persistentSetOf(), + _characters = airing.characters?.nodesFilterNotNull()?.mapNotNull(Character::invoke)?.toImmutableSet() ?: persistentSetOf(), entry = airing.mediaListEntry?.let(::Entry), trailer = airing.trailer?.let { val site = it.site?.ifBlank { null } @@ -168,7 +173,7 @@ data class Medium( avgEpisodeDurationInMin = query.duration ?: -1, format = query.format ?: MediaFormat.UNKNOWN__, _isAdult = query.isAdult ?: false, - genres = query.genresFilterNotNull()?.toSet() ?: emptySet(), + genres = query.genresFilterNotNull()?.toImmutableSet() ?: persistentSetOf(), countryOfOrigin = query.countryOfOrigin?.toString()?.ifBlank { null }, averageScore = query.averageScore ?: -1, title = Title( @@ -185,8 +190,8 @@ data class Medium( 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(), + ranking = query.rankingsFilterNotNull()?.map(::Ranking)?.toImmutableSet() ?: persistentSetOf(), + _characters = query.characters?.nodesFilterNotNull()?.mapNotNull(Character::invoke)?.toImmutableSet() ?: persistentSetOf(), entry = query.mediaListEntry?.let(::Entry), trailer = query.trailer?.let { val site = it.site?.ifBlank { null } @@ -220,7 +225,7 @@ data class Medium( avgEpisodeDurationInMin = media.duration ?: -1, format = media.format ?: MediaFormat.UNKNOWN__, _isAdult = media.isAdult ?: false, - genres = media.genresFilterNotNull()?.toSet() ?: emptySet(), + genres = media.genresFilterNotNull()?.toImmutableSet() ?: persistentSetOf(), countryOfOrigin = media.countryOfOrigin?.toString()?.ifBlank { null }, averageScore = media.averageScore ?: -1, title = Title( @@ -237,8 +242,8 @@ data class Medium( extraLarge = media.coverImage?.extraLarge?.ifBlank { null } ), nextAiringEpisode = media.nextAiringEpisode?.let(::NextAiring), - ranking = media.rankingsFilterNotNull()?.map(::Ranking)?.toSet() ?: emptySet(), - _characters = media.characters?.nodesFilterNotNull()?.mapNotNull(Character::invoke)?.toSet() ?: emptySet(), + ranking = media.rankingsFilterNotNull()?.map(::Ranking)?.toImmutableSet() ?: persistentSetOf(), + _characters = media.characters?.nodesFilterNotNull()?.mapNotNull(Character::invoke)?.toImmutableSet() ?: persistentSetOf(), entry = list?.let(::Entry), trailer = media.trailer?.let { val site = it.site?.ifBlank { null } @@ -268,7 +273,7 @@ data class Medium( } @Transient - val characters: Set = _characters.filterNot { it.id == 36309 }.toSet() + val characters: ImmutableSet = _characters.filterNot { it.id == 36309 }.toImmutableSet() @Transient val isFavoriteBlocked: Boolean = _isFavoriteBlocked || type == MediaType.UNKNOWN__ diff --git a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/model/PageMediaQuery.kt b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/model/PageMediaQuery.kt index 63f55fa..bb05aea 100644 --- a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/model/PageMediaQuery.kt +++ b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/model/PageMediaQuery.kt @@ -12,6 +12,8 @@ import dev.datlag.aniflow.anilist.type.MediaSort import dev.datlag.aniflow.anilist.type.MediaType import dev.datlag.tooling.safeSubList import dev.datlag.tooling.safeSubSet +import kotlinx.collections.immutable.ImmutableCollection +import kotlinx.collections.immutable.toImmutableSet import kotlinx.datetime.Clock import dev.datlag.aniflow.anilist.PageMediaQuery as PageMediaGraphQL @@ -98,15 +100,15 @@ sealed interface PageMediaQuery { } data class Recommendation( - val wantedGenres: Collection, - val preventIds: Collection, + val wantedGenres: ImmutableCollection, + val preventIds: ImmutableCollection, val type: MediaType, val nsfw: Boolean ) : PageMediaQuery { constructor( nsfw: Boolean, - collection: Collection, + collection: ImmutableCollection, type: MediaType = collection.let { c -> val allTypes = c.map { it.type @@ -140,8 +142,8 @@ sealed interface PageMediaQuery { allGenres.groupingBy { g -> g }.eachCount().toList().sortedByDescending { p -> p.second }.safeSubSet(0, 5).toMap().keys.safeSubSet(0, 5) - }, - preventIds = collection.map { it.id }, + }.toImmutableSet(), + preventIds = collection.map { it.id }.toImmutableSet(), type = type, nsfw = nsfw ) @@ -151,8 +153,8 @@ sealed interface PageMediaQuery { type = Optional.presentMediaType(type), sort = Optional.presentAsList(MediaSort.TRENDING_DESC), preventGenres = Optional.presentIfNot(nsfw, AdultContent.Genre.allTags), - wantedGenres = Optional.present(wantedGenres.toList()), - preventIds = Optional.present(preventIds.toList()), + wantedGenres = Optional.presentAsList(*wantedGenres.toTypedArray()), + preventIds = Optional.presentAsList(*preventIds.toTypedArray()), onList = Optional.present(false), statusVersion = 2, html = true diff --git a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/state/DiscoverState.kt b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/state/DiscoverState.kt index bc4370d..8616244 100644 --- a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/state/DiscoverState.kt +++ b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/state/DiscoverState.kt @@ -7,6 +7,9 @@ import dev.datlag.aniflow.anilist.model.Medium import dev.datlag.aniflow.anilist.model.PageListQuery import dev.datlag.aniflow.anilist.model.PageMediaQuery import dev.datlag.aniflow.anilist.type.MediaSeason +import kotlinx.collections.immutable.ImmutableCollection +import kotlinx.collections.immutable.persistentSetOf +import kotlinx.collections.immutable.toImmutableList import dev.datlag.aniflow.anilist.PageMediaQuery as PageMediaGraphQL sealed interface DiscoverState { @@ -47,7 +50,7 @@ sealed interface DiscoverState { Matching( query = PageMediaQuery.Recommendation( nsfw = nsfw, - collection = mediumList + collection = mediumList.toImmutableList() ) ) } @@ -70,7 +73,7 @@ sealed interface DiscoverState { Failure(response.exception) } else { Success( - collection = mediumList.map(::Medium).distinctBy { it.id } + collection = mediumList.map(::Medium).distinctBy { it.id }.toImmutableList() ) } } @@ -100,7 +103,7 @@ sealed interface DiscoverState { Failure(throwable = response.exception) } else { Success( - collection = mediumList.map(::Medium).distinctBy { it.id } + collection = mediumList.map(::Medium).distinctBy { it.id }.toImmutableList() ) } } @@ -111,7 +114,7 @@ sealed interface DiscoverState { private sealed interface PostLoading : DiscoverState data class Success( - val collection: Collection + val collection: ImmutableCollection ) : PostLoading data class Failure( @@ -154,7 +157,7 @@ sealed interface DiscoverListType { } companion object { - val entries = setOf( + val entries = persistentSetOf( DiscoverListType.Recommendation, DiscoverListType.Spring, DiscoverListType.Summer, diff --git a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/state/HomeAiringState.kt b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/state/HomeAiringState.kt index 9718094..d2b9290 100644 --- a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/state/HomeAiringState.kt +++ b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/state/HomeAiringState.kt @@ -6,6 +6,8 @@ import dev.datlag.aniflow.anilist.AdultContent import dev.datlag.aniflow.anilist.AiringQuery import dev.datlag.aniflow.anilist.common.hasNonCacheError import dev.datlag.aniflow.anilist.model.PageAiringQuery +import kotlinx.collections.immutable.ImmutableCollection +import kotlinx.collections.immutable.toImmutableList import dev.datlag.aniflow.anilist.AiringQuery as PageAiringGraphQL @@ -22,7 +24,7 @@ sealed interface HomeAiringState { private sealed interface PostLoading : HomeAiringState data class Success( - val collection: Collection + val collection: ImmutableCollection ) : PostLoading data class Failure( @@ -55,7 +57,7 @@ sealed interface HomeAiringState { if (airingList.isNullOrEmpty()) { Failure(throwable = response.exception) } else { - Success(airingList) + Success(airingList.toImmutableList()) } } } diff --git a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/state/HomeDefaultState.kt b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/state/HomeDefaultState.kt index c5959cb..02a8426 100644 --- a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/state/HomeDefaultState.kt +++ b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/state/HomeDefaultState.kt @@ -4,6 +4,8 @@ import com.apollographql.apollo3.api.ApolloResponse import dev.datlag.aniflow.anilist.PageMediaQuery import dev.datlag.aniflow.anilist.common.hasNonCacheError import dev.datlag.aniflow.anilist.model.Medium +import kotlinx.collections.immutable.ImmutableCollection +import kotlinx.collections.immutable.toImmutableList sealed interface HomeDefaultState { @@ -17,7 +19,7 @@ sealed interface HomeDefaultState { private sealed interface PostLoading : HomeDefaultState - data class Success(val collection: Collection) : PostLoading + data class Success(val collection: ImmutableCollection) : PostLoading data class Failure( internal val throwable: Throwable? @@ -39,7 +41,7 @@ sealed interface HomeDefaultState { if (mediumList.isNullOrEmpty()) { Failure(response.exception) } else { - Success(mediumList.map(::Medium)) + Success(mediumList.map(::Medium).toImmutableList()) } } } diff --git a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/state/ListState.kt b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/state/ListState.kt index 72fa2ba..a0abc56 100644 --- a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/state/ListState.kt +++ b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/state/ListState.kt @@ -4,16 +4,16 @@ import com.apollographql.apollo3.api.ApolloResponse import dev.datlag.aniflow.anilist.ListQuery import dev.datlag.aniflow.anilist.common.hasNonCacheError import dev.datlag.aniflow.anilist.model.Medium -import dev.datlag.aniflow.anilist.model.PageListQuery -import dev.datlag.aniflow.anilist.type.MediaListStatus +import kotlinx.collections.immutable.ImmutableCollection +import kotlinx.collections.immutable.toImmutableList sealed interface ListState { val hasNextPage: Boolean - val collection: Collection + val collection: ImmutableCollection data class Loading( - override val collection: Collection + override val collection: ImmutableCollection ) : ListState { override val hasNextPage: Boolean = false } @@ -22,12 +22,12 @@ sealed interface ListState { data class Success( override val hasNextPage: Boolean, - override val collection: Collection + override val collection: ImmutableCollection ) : PostLoading data class Failure( internal val throwable: Throwable?, - override val collection: Collection + override val collection: ImmutableCollection ) : PostLoading { override val hasNextPage: Boolean = false } @@ -43,7 +43,7 @@ sealed interface ListState { ) fun fromResponse( - previousCollection: Collection, + previousCollection: ImmutableCollection, response: ApolloResponse ): ListState { val data = response.data @@ -73,7 +73,7 @@ sealed interface ListState { media = it.media ?: return@mapNotNull null, list = it ) - }).distinctBy { it.id } + }).distinctBy { it.id }.toImmutableList() ) } } diff --git a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/state/SearchState.kt b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/state/SearchState.kt index f757ec0..11740c1 100644 --- a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/state/SearchState.kt +++ b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/state/SearchState.kt @@ -4,6 +4,8 @@ import com.apollographql.apollo3.api.ApolloResponse import dev.datlag.aniflow.anilist.PageMediaQuery import dev.datlag.aniflow.anilist.common.hasNonCacheError import dev.datlag.aniflow.anilist.model.Medium +import kotlinx.collections.immutable.ImmutableCollection +import kotlinx.collections.immutable.toImmutableList sealed interface SearchState { @@ -20,7 +22,7 @@ sealed interface SearchState { private sealed interface PostLoading : SearchState data class Success( - val collection: Collection + val collection: ImmutableCollection ) : PostLoading data class Failure( @@ -46,7 +48,7 @@ sealed interface SearchState { if (mediumList.isNullOrEmpty()) { Failure(response.exception) } else { - Success(mediumList.map(::Medium)) + Success(mediumList.map(::Medium).distinctBy { it.id }.toImmutableList()) } } } diff --git a/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/other/BurningSeriesResolver.android.kt b/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/other/BurningSeriesResolver.android.kt index fc89c8c..f1471e3 100644 --- a/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/other/BurningSeriesResolver.android.kt +++ b/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/other/BurningSeriesResolver.android.kt @@ -9,6 +9,9 @@ import android.os.Build import androidx.core.database.getStringOrNull import dev.datlag.tooling.scopeCatching import io.github.aakira.napier.Napier +import kotlinx.collections.immutable.ImmutableSet +import kotlinx.collections.immutable.persistentSetOf +import kotlinx.collections.immutable.toImmutableSet actual class BurningSeriesResolver( private val packageManager: PackageManager, @@ -33,9 +36,9 @@ actual class BurningSeriesResolver( true }.getOrNull() ?: false - actual fun resolveWatchedEpisodes(): Set { + actual fun resolveWatchedEpisodes(): ImmutableSet { if (episodeClient == null) { - return emptySet() + return persistentSetOf() } val episodeCursor = episodeClient.query( @@ -44,7 +47,7 @@ actual class BurningSeriesResolver( "progress > 0 AND length > 0", null, null - ) ?: return emptySet() + ) ?: return persistentSetOf() val episodes = mutableSetOf() @@ -82,15 +85,15 @@ actual class BurningSeriesResolver( } episodeCursor.close() - return episodes + return episodes.toImmutableSet() } - actual fun resolveByName(english: String?, romaji: String?): Set { + actual fun resolveByName(english: String?, romaji: String?): ImmutableSet { val englishTrimmed = english?.trim()?.ifBlank { null }?.replace("'", "")?.trim() val romajiTrimmed = romaji?.trim()?.ifBlank { null }?.replace("'", "")?.trim() if (seriesClient == null || (englishTrimmed == null && romajiTrimmed == null)) { - return emptySet() + return persistentSetOf() } val selection = if (englishTrimmed != null && romajiTrimmed != null) { @@ -104,19 +107,19 @@ actual class BurningSeriesResolver( return seriesBySelection(selection) } - actual fun resolveByName(value: String): Set { + actual fun resolveByName(value: String): ImmutableSet { val trimmed = value.trim().replace("'", "").trim() return if (trimmed.length >= 3) { seriesBySelection("title LIKE '%$trimmed%'") } else { - emptySet() + persistentSetOf() } } - private fun seriesBySelection(selection: String): Set { + private fun seriesBySelection(selection: String): ImmutableSet { if (seriesClient == null) { - return emptySet() + return persistentSetOf() } val seriesCursor = seriesClient.query( @@ -125,7 +128,7 @@ actual class BurningSeriesResolver( selection, null, null - ) ?: return emptySet() + ) ?: return persistentSetOf() val series = mutableSetOf() @@ -154,7 +157,7 @@ actual class BurningSeriesResolver( } seriesCursor.close() - return series + return series.toImmutableSet() } actual fun close() { diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/other/BurningSeriesResolver.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/other/BurningSeriesResolver.kt index b69def4..1d0fae6 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/other/BurningSeriesResolver.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/other/BurningSeriesResolver.kt @@ -1,10 +1,12 @@ package dev.datlag.aniflow.other +import kotlinx.collections.immutable.ImmutableSet + expect class BurningSeriesResolver { val isAvailable: Boolean - fun resolveWatchedEpisodes(): Set - fun resolveByName(english: String?, romaji: String?): Set - fun resolveByName(value: String): Set + fun resolveWatchedEpisodes(): ImmutableSet + fun resolveByName(english: String?, romaji: String?): ImmutableSet + fun resolveByName(value: String): ImmutableSet fun close() } diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/custom/speeddial/SubSpeedDialFABs.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/custom/speeddial/SubSpeedDialFABs.kt index 8be6adb..990d3a5 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/custom/speeddial/SubSpeedDialFABs.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/custom/speeddial/SubSpeedDialFABs.kt @@ -10,11 +10,12 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.unit.dp +import kotlinx.collections.immutable.ImmutableList @Composable fun SubSpeedDialFABs( state: SpeedDialFABState, - items: List, + items: ImmutableList, showLabels: Boolean = true, verticalArrangement: Arrangement.Vertical = Arrangement.Top, labelContent: @Composable (T) -> Unit = { 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 218b7c5..b86f223 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 @@ -12,6 +12,8 @@ import dev.datlag.aniflow.anilist.type.MediaType import dev.datlag.aniflow.other.Series import dev.datlag.aniflow.ui.navigation.ContentHolderComponent import dev.datlag.aniflow.ui.navigation.DialogComponent +import kotlinx.collections.immutable.ImmutableCollection +import kotlinx.collections.immutable.ImmutableSet import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow import dev.datlag.aniflow.settings.model.TitleLanguage as SettingsTitle @@ -33,7 +35,7 @@ interface MediumComponent : ContentHolderComponent { val title: Flow val description: Flow val translatedDescription: StateFlow - val genres: Flow> + val genres: Flow> val format: Flow val episodesOrChapters: Flow @@ -45,7 +47,7 @@ interface MediumComponent : ContentHolderComponent { val popular: Flow val score: Flow - val characters: Flow> + val characters: Flow> val rating: Flow val alreadyAdded: Flow val trailer: Flow @@ -59,7 +61,7 @@ interface MediumComponent : ContentHolderComponent { val dialog: Value> val bsAvailable: Boolean - val bsOptions: Flow> + val bsOptions: Flow> fun back() override fun dismissContent() { 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 538573b..a7e4d38 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 @@ -33,6 +33,10 @@ import dev.datlag.aniflow.ui.navigation.screen.medium.dialog.edit.EditDialogComp import dev.datlag.tooling.compose.ioDispatcher import dev.datlag.tooling.decompose.ioScope import dev.datlag.tooling.safeCast +import kotlinx.collections.immutable.ImmutableCollection +import kotlinx.collections.immutable.ImmutableSet +import kotlinx.collections.immutable.toImmutableList +import kotlinx.collections.immutable.toImmutableSet import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.* import org.kodein.di.DI @@ -104,7 +108,7 @@ class MediumScreenComponent( override val translatedDescription: MutableStateFlow = MutableStateFlow(null) @OptIn(ExperimentalCoroutinesApi::class) - override val genres: Flow> = mediumSuccessState.mapLatest { + override val genres: Flow> = mediumSuccessState.mapLatest { it.medium.genres }.mapNotEmpty() @@ -141,7 +145,7 @@ class MediumScreenComponent( }.distinctUntilChanged() @OptIn(ExperimentalCoroutinesApi::class) - override val characters: Flow> = mediumSuccessState.mapLatest { + override val characters: Flow> = mediumSuccessState.mapLatest { it.medium.characters }.mapNotEmpty() @@ -212,11 +216,11 @@ class MediumScreenComponent( it?.ifBlank { null }?.let(burningSeriesResolver::resolveByName).orEmpty() }.flowOn(ioDispatcher()) - override val bsOptions: Flow> = combine( + override val bsOptions: Flow> = combine( bsSearchOptions, bsDefaultOptions ) { search, default -> - search.ifEmpty { default } + search.ifEmpty { default }.toImmutableSet() } @OptIn(ExperimentalCoroutinesApi::class) diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/component/BSDialog.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/component/BSDialog.kt index 8242ae6..e4b6bcb 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/component/BSDialog.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/component/BSDialog.kt @@ -19,13 +19,14 @@ import dev.datlag.aniflow.other.Series import dev.datlag.tooling.decompose.lifecycle.collectAsStateWithLifecycle import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource +import kotlinx.collections.immutable.ImmutableCollection import kotlinx.coroutines.flow.StateFlow @OptIn(ExperimentalMaterial3Api::class) @Composable fun BSDialog( state: UseCaseState, - bsOptions: Collection, + bsOptions: ImmutableCollection, onSearch: suspend (String) -> Unit ) { OptionDialog( diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/component/CharacterSection.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/component/CharacterSection.kt index 18c5d7f..0cc935d 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/component/CharacterSection.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/component/CharacterSection.kt @@ -16,13 +16,14 @@ import dev.datlag.aniflow.anilist.model.Medium import dev.datlag.aniflow.settings.model.CharLanguage import dev.datlag.tooling.decompose.lifecycle.collectAsStateWithLifecycle import dev.icerock.moko.resources.compose.stringResource +import kotlinx.collections.immutable.ImmutableCollection import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow @Composable fun CharacterSection( initialMedium: Medium, - characterFlow: Flow>, + characterFlow: Flow>, charLanguage: Flow, modifier: Modifier = Modifier, onClick: (Character) -> Unit diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/component/FABContent.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/component/FABContent.kt index abf9a58..b280a3d 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/component/FABContent.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/component/FABContent.kt @@ -32,6 +32,8 @@ import dev.datlag.aniflow.ui.navigation.screen.medium.MediumComponent import dev.datlag.tooling.decompose.lifecycle.collectAsStateWithLifecycle import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource +import kotlinx.collections.immutable.persistentSetOf +import kotlinx.collections.immutable.toImmutableList @Composable fun FABContent( @@ -71,7 +73,7 @@ fun FABContent( verticalArrangement = Arrangement.spacedBy(4.dp) ) { val bsAvailable = component.bsAvailable - val bsOptions by component.bsOptions.collectAsStateWithLifecycle(emptySet()) + val bsOptions by component.bsOptions.collectAsStateWithLifecycle(persistentSetOf()) val bsState = rememberUseCaseState() val rating by component.rating.collectAsStateWithLifecycle(-1) @@ -124,7 +126,7 @@ fun FABContent( } else { null } - ), + ).toImmutableList(), showLabels = expanded ) SpeedDialFAB( diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/component/GenreSection.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/component/GenreSection.kt index bf097ec..4681e9f 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/component/GenreSection.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/component/GenreSection.kt @@ -12,12 +12,13 @@ import androidx.compose.ui.unit.dp import dev.datlag.aniflow.anilist.model.Medium import dev.datlag.aniflow.ui.navigation.screen.home.component.default.GenreChip import dev.datlag.tooling.decompose.lifecycle.collectAsStateWithLifecycle +import kotlinx.collections.immutable.ImmutableCollection import kotlinx.coroutines.flow.Flow @Composable fun GenreSection( initialMedium: Medium, - genreFlow: Flow>, + genreFlow: Flow>, modifier: Modifier = Modifier, ) { val genres by genreFlow.collectAsStateWithLifecycle(initialMedium.genres) diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/nekos/NekosScreen.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/nekos/NekosScreen.kt index 99209ba..08e56fe 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/nekos/NekosScreen.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/nekos/NekosScreen.kt @@ -22,6 +22,8 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.unit.dp import coil3.compose.AsyncImage +import com.maxkeppeker.sheets.core.models.base.Header +import com.maxkeppeker.sheets.core.models.base.IconSource import com.maxkeppeker.sheets.core.models.base.rememberUseCaseState import com.maxkeppeler.sheets.option.OptionDialog import com.maxkeppeler.sheets.option.models.DisplayMode @@ -121,6 +123,10 @@ fun NekosScreen(component: NekosComponent) { } component.filter(filterRating) } + ), + header = Header.Default( + icon = IconSource(Icons.Rounded.FilterList), + title = stringResource(SharedRes.strings.filter) ) ) diff --git a/composeApp/src/iosMain/kotlin/dev/datlag/aniflow/other/BurningSeriesResolver.ios.kt b/composeApp/src/iosMain/kotlin/dev/datlag/aniflow/other/BurningSeriesResolver.ios.kt index c91c84e..6c20c33 100644 --- a/composeApp/src/iosMain/kotlin/dev/datlag/aniflow/other/BurningSeriesResolver.ios.kt +++ b/composeApp/src/iosMain/kotlin/dev/datlag/aniflow/other/BurningSeriesResolver.ios.kt @@ -1,21 +1,24 @@ package dev.datlag.aniflow.other +import kotlinx.collections.immutable.ImmutableSet +import kotlinx.collections.immutable.persistentSetOf + actual class BurningSeriesResolver { actual val isAvailable: Boolean get() = false - actual fun resolveWatchedEpisodes(): Set { + actual fun resolveWatchedEpisodes(): ImmutableSet { // ToDo("Check if something like content provider exists") - return emptySet() + return persistentSetOf() } - actual fun resolveByName(english: String?, romaji: String?): Set { - return emptySet() + actual fun resolveByName(english: String?, romaji: String?): ImmutableSet { + return persistentSetOf() } - actual fun resolveByName(value: String): Set { - return emptySet() + actual fun resolveByName(value: String): ImmutableSet { + return persistentSetOf() } actual fun close() { } diff --git a/gradle.properties b/gradle.properties index af6adab..de17d5e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,8 @@ #Gradle org.gradle.jvmargs=-Xmx4096M -Dfile.encoding=UTF-8 -Dkotlin.daemon.jvm.options\="-Xmx4096M" org.gradle.caching=true +org.gradle.daemon=true +org.gradle.configureondemand=true # org.gradle.configuration-cache=true #Kotlin @@ -18,4 +20,8 @@ org.jetbrains.compose.experimental.wasm.enabled=true #MPP kotlin.mpp.androidSourceSetLayoutVersion=2 -kotlin.mpp.enableCInteropCommonization=true \ No newline at end of file +kotlin.mpp.enableCInteropCommonization=true + +systemProp.javax.xml.parsers.SAXParserFactory=com.sun.org.apache.xerces.internal.jaxp.SAXParserFactoryImpl +systemProp.javax.xml.transform.TransformerFactory=com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl +systemProp.javax.xml.parsers.DocumentBuilderFactory=com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 02542a2..bf3deff 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -27,6 +27,7 @@ flowredux = "1.2.1" google-identity = "1.1.0" haze = "0.7.1" html-converter = "0.9.5" +immutable = "0.3.7" instantapps = "18.0.1" kache = "2.1.0" kasechange = "1.4.1" @@ -87,6 +88,7 @@ google-identity = { group = "com.google.android.libraries.identity.googleid", na haze = { group = "dev.chrisbanes.haze", name = "haze", version.ref = "haze" } haze-materials = { group = "dev.chrisbanes.haze", name = "haze-materials", version.ref = "haze" } html-converter = { group = "be.digitalia.compose.htmlconverter", name = "htmlconverter", version.ref = "html-converter" } +immutable = { group = "org.jetbrains.kotlinx", name = "kotlinx-collections-immutable", version.ref = "immutable" } instantapps = { group = "com.google.android.gms", name = "play-services-instantapps", version.ref = "instantapps" } kache = { group = "com.mayakapps.kache", name = "kache", version.ref = "kache" } kasechange = { group = "net.pearx.kasechange", name = "kasechange", version.ref = "kasechange" } diff --git a/model/build.gradle.kts b/model/build.gradle.kts index 7f688e5..4aa2834 100644 --- a/model/build.gradle.kts +++ b/model/build.gradle.kts @@ -30,6 +30,8 @@ kotlin { commonMain.dependencies { implementation(libs.coroutines) implementation(libs.tooling) + api(libs.immutable) + api(libs.serialization) } } } \ No newline at end of file diff --git a/model/src/commonMain/kotlin/dev/datlag/aniflow/model/serializer/ImmutableListSerializer.kt b/model/src/commonMain/kotlin/dev/datlag/aniflow/model/serializer/ImmutableListSerializer.kt new file mode 100644 index 0000000..9b9bfc7 --- /dev/null +++ b/model/src/commonMain/kotlin/dev/datlag/aniflow/model/serializer/ImmutableListSerializer.kt @@ -0,0 +1,32 @@ +package dev.datlag.aniflow.model.serializer + +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toPersistentList +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.Serializer +import kotlinx.serialization.builtins.ListSerializer +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.serialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder + +typealias SerializableImmutableList = @Serializable(ImmutableListSerializer::class) ImmutableList + +@Serializer(forClass = ImmutableList::class) +class ImmutableListSerializer(private val dataSerializer: KSerializer) : KSerializer> { + private class PersistentListDescriptor : SerialDescriptor by serialDescriptor>() { + @ExperimentalSerializationApi + override val serialName: String = "kotlinx.serialization.immutable.ImmutableList" + } + + override val descriptor: SerialDescriptor = PersistentListDescriptor() + override fun serialize(encoder: Encoder, value: ImmutableList) { + return ListSerializer(dataSerializer).serialize(encoder, value) + } + + override fun deserialize(decoder: Decoder): ImmutableList { + return ListSerializer(dataSerializer).deserialize(decoder).toPersistentList() + } +} \ No newline at end of file diff --git a/model/src/commonMain/kotlin/dev/datlag/aniflow/model/serializer/ImmutableSetSerializer.kt b/model/src/commonMain/kotlin/dev/datlag/aniflow/model/serializer/ImmutableSetSerializer.kt new file mode 100644 index 0000000..676a68d --- /dev/null +++ b/model/src/commonMain/kotlin/dev/datlag/aniflow/model/serializer/ImmutableSetSerializer.kt @@ -0,0 +1,32 @@ +package dev.datlag.aniflow.model.serializer + +import kotlinx.collections.immutable.ImmutableSet +import kotlinx.collections.immutable.toPersistentSet +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.Serializer +import kotlinx.serialization.builtins.SetSerializer +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.serialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder + +typealias SerializableImmutableSet = @Serializable(ImmutableSetSerializer::class) ImmutableSet + +@Serializer(forClass = ImmutableSet::class) +class ImmutableSetSerializer(private val dataSerializer: KSerializer) : KSerializer> { + private class PersistentListDescriptor : SerialDescriptor by serialDescriptor>() { + @ExperimentalSerializationApi + override val serialName: String = "kotlinx.serialization.immutable.ImmutableSet" + } + + override val descriptor: SerialDescriptor = PersistentListDescriptor() + override fun serialize(encoder: Encoder, value: ImmutableSet) { + return SetSerializer(dataSerializer).serialize(encoder, value) + } + + override fun deserialize(decoder: Decoder): ImmutableSet { + return SetSerializer(dataSerializer).deserialize(decoder).toPersistentSet() + } +} \ No newline at end of file diff --git a/nekos/build.gradle.kts b/nekos/build.gradle.kts index a6a8af3..5ddd78e 100644 --- a/nekos/build.gradle.kts +++ b/nekos/build.gradle.kts @@ -23,6 +23,7 @@ kotlin { implementation(libs.tooling) api(libs.ktorfit) implementation(libs.serialization) + api(libs.immutable) api(project(":model")) implementation(project(":firebase")) diff --git a/nekos/src/commonMain/kotlin/dev/datlag/aniflow/nekos/NekosRepository.kt b/nekos/src/commonMain/kotlin/dev/datlag/aniflow/nekos/NekosRepository.kt index 98fc69f..49b8baa 100644 --- a/nekos/src/commonMain/kotlin/dev/datlag/aniflow/nekos/NekosRepository.kt +++ b/nekos/src/commonMain/kotlin/dev/datlag/aniflow/nekos/NekosRepository.kt @@ -3,6 +3,7 @@ package dev.datlag.aniflow.nekos import dev.datlag.aniflow.model.CatchResult import dev.datlag.aniflow.nekos.model.ImagesResponse import dev.datlag.aniflow.nekos.model.Rating +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.* import kotlinx.serialization.Serializable @@ -36,7 +37,7 @@ class NekosRepository( is State.Success -> { State.Success( ImagesResponse( - items = q.response.items.filterNot { it.hasAdultTag }, + items = q.response.items.filterNot { it.hasAdultTag }.toImmutableList(), count = q.response.count ) ) diff --git a/nekos/src/commonMain/kotlin/dev/datlag/aniflow/nekos/model/ImagesResponse.kt b/nekos/src/commonMain/kotlin/dev/datlag/aniflow/nekos/model/ImagesResponse.kt index 1fc4745..74edf31 100644 --- a/nekos/src/commonMain/kotlin/dev/datlag/aniflow/nekos/model/ImagesResponse.kt +++ b/nekos/src/commonMain/kotlin/dev/datlag/aniflow/nekos/model/ImagesResponse.kt @@ -1,13 +1,16 @@ package dev.datlag.aniflow.nekos.model +import dev.datlag.aniflow.model.serializer.SerializableImmutableList import dev.datlag.aniflow.nekos.AdultContent +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.Transient @Serializable data class ImagesResponse( - @SerialName("items") val items: List, + @SerialName("items") val items: SerializableImmutableList, @SerialName("count") val count: Int = items.size, ) { @Transient @@ -18,8 +21,8 @@ data class ImagesResponse( @SerialName("id") val id: Int, @SerialName("image_url") val imageUrl: String? = null, @SerialName("sample_url") val sampleUrl: String? = null, - @SerialName("characters") val characters: List = emptyList(), - @SerialName("tags") val tags: List = emptyList(), + @SerialName("characters") val characters: SerializableImmutableList = persistentListOf(), + @SerialName("tags") val tags: SerializableImmutableList = persistentListOf(), ) { @Transient val hasAdultTag: Boolean = tags.any { diff --git a/settings/build.gradle.kts b/settings/build.gradle.kts index 1c3b872..d0c0c62 100644 --- a/settings/build.gradle.kts +++ b/settings/build.gradle.kts @@ -19,6 +19,7 @@ kotlin { commonMain.dependencies { api(libs.coroutines) api(libs.datastore) + api(libs.immutable) implementation(libs.serialization.protobuf) implementation(libs.tooling) diff --git a/settings/src/commonMain/kotlin/dev/datlag/aniflow/settings/model/CharLanguage.kt b/settings/src/commonMain/kotlin/dev/datlag/aniflow/settings/model/CharLanguage.kt index 0ceb92d..e4e437a 100644 --- a/settings/src/commonMain/kotlin/dev/datlag/aniflow/settings/model/CharLanguage.kt +++ b/settings/src/commonMain/kotlin/dev/datlag/aniflow/settings/model/CharLanguage.kt @@ -1,5 +1,7 @@ package dev.datlag.aniflow.settings.model +import kotlinx.collections.immutable.ImmutableSet +import kotlinx.collections.immutable.persistentSetOf import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable @@ -29,7 +31,7 @@ sealed interface CharLanguage { } companion object CharSerializer : KSerializer { - val all: Set = setOf( + val all: ImmutableSet = persistentSetOf( RomajiWestern, Romaji, Native diff --git a/settings/src/commonMain/kotlin/dev/datlag/aniflow/settings/model/Color.kt b/settings/src/commonMain/kotlin/dev/datlag/aniflow/settings/model/Color.kt index 80cb784..66fd0f9 100644 --- a/settings/src/commonMain/kotlin/dev/datlag/aniflow/settings/model/Color.kt +++ b/settings/src/commonMain/kotlin/dev/datlag/aniflow/settings/model/Color.kt @@ -1,5 +1,7 @@ package dev.datlag.aniflow.settings.model +import kotlinx.collections.immutable.ImmutableSet +import kotlinx.collections.immutable.persistentSetOf import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable @@ -63,7 +65,7 @@ sealed interface Color { ) : Color companion object ColorSerializer : KSerializer { - val all: Set = setOf( + val all: ImmutableSet = persistentSetOf( Blue, Purple, Pink, diff --git a/settings/src/commonMain/kotlin/dev/datlag/aniflow/settings/model/TitleLanguage.kt b/settings/src/commonMain/kotlin/dev/datlag/aniflow/settings/model/TitleLanguage.kt index 65b9aab..57a257f 100644 --- a/settings/src/commonMain/kotlin/dev/datlag/aniflow/settings/model/TitleLanguage.kt +++ b/settings/src/commonMain/kotlin/dev/datlag/aniflow/settings/model/TitleLanguage.kt @@ -1,5 +1,7 @@ package dev.datlag.aniflow.settings.model +import kotlinx.collections.immutable.ImmutableSet +import kotlinx.collections.immutable.persistentSetOf import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable @@ -29,7 +31,7 @@ sealed interface TitleLanguage { } companion object TitleSerializer : KSerializer { - val all: Set = setOf( + val all: ImmutableSet = persistentSetOf( Romaji, English, Native diff --git a/trace/build.gradle.kts b/trace/build.gradle.kts index b1b11b3..0c3c23e 100644 --- a/trace/build.gradle.kts +++ b/trace/build.gradle.kts @@ -24,6 +24,7 @@ kotlin { api(libs.ktorfit) implementation(libs.serialization) api(libs.flowredux) + api(libs.immutable) api(project(":model")) implementation(project(":firebase")) diff --git a/trace/src/commonMain/kotlin/dev/datlag/aniflow/trace/model/SearchResponse.kt b/trace/src/commonMain/kotlin/dev/datlag/aniflow/trace/model/SearchResponse.kt index 5645386..4b800a4 100644 --- a/trace/src/commonMain/kotlin/dev/datlag/aniflow/trace/model/SearchResponse.kt +++ b/trace/src/commonMain/kotlin/dev/datlag/aniflow/trace/model/SearchResponse.kt @@ -1,18 +1,24 @@ package dev.datlag.aniflow.trace.model +import dev.datlag.aniflow.model.serializer.SerializableImmutableList +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.ImmutableSet +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList +import kotlinx.collections.immutable.toImmutableSet import kotlinx.serialization.* @Serializable data class SearchResponse( @SerialName("error") val error: String? = null, - @SerialName("result") val result: List = emptyList() + @SerialName("result") val result: SerializableImmutableList = persistentListOf() ) { @Transient val isError = !error.isNullOrBlank() || result.isEmpty() @Transient - val combinedResults: Set = result.groupBy { it.aniList.id }.mapValues { entry -> + val combinedResults: ImmutableSet = result.groupBy { it.aniList.id }.mapValues { entry -> CombinedResult( aniList = Result.AniList( id = entry.key, @@ -23,7 +29,7 @@ data class SearchResponse( maxSimilarity = entry.value.maxOf { it.similarity }, avgSimilarity = entry.value.map { it.similarity }.average().toFloat() ) - }.values.toSet() + }.values.toImmutableSet() fun nsfwAware(allowed: Boolean): SearchResponse { return if (allowed) { @@ -31,7 +37,7 @@ data class SearchResponse( } else { SearchResponse( error = error, - result = result.filterNot { it.aniList.isAdult } + result = result.filterNot { it.aniList.isAdult }.toImmutableList() ) } }