From 96d35e8d25ce9894eacca44b97d832c4520bb562 Mon Sep 17 00:00:00 2001 From: DatLag Date: Mon, 6 May 2024 21:08:50 +0200 Subject: [PATCH 01/23] transition to new navigation --- .../aniflow/anilist/AiringTodayRepository.kt | 5 + .../anilist/PopularNextSeasonRepository.kt | 27 +-- .../anilist/PopularSeasonRepository.kt | 12 +- .../aniflow/anilist/TrendingRepository.kt | 32 +-- .../aniflow/anilist/state/CollectionState.kt | 52 +++++ .../aniflow/anilist/state/SeasonState.kt | 33 --- .../datlag/aniflow/module/NetworkModule.kt | 3 +- .../dev/datlag/aniflow/other/StateSaver.kt | 8 +- .../aniflow/ui/navigation/RootComponent.kt | 3 +- .../home => }/component/CollapsingToolbar.kt | 104 ++++----- .../screen/component/HidingNavigationBar.kt | 88 +++++++ .../navigation/screen/home/HomeComponent.kt | 24 ++ .../ui/navigation/screen/home/HomeScreen.kt | 129 ++++++++++ .../screen/home/HomeScreenComponent.kt | 120 ++++++++++ .../screen/home/component/AllLoadingView.kt | 28 +++ .../screen/home/component/DefaultOverview.kt | 101 ++++++++ .../screen/home/component/ScheduleOverview.kt | 110 +++++++++ .../component/airing}/Airing.kt | 3 +- .../component/airing}/AiringCard.kt | 13 +- .../component/airing}/Episode.kt | 2 +- .../component/default}/GenreChip.kt | 2 +- .../component/default}/MediumCard.kt | 7 +- .../component/default}/Rating.kt | 3 +- .../screen/initial/InitialScreenComponent.kt | 16 +- .../screen/initial/component/CompactScreen.kt | 6 +- .../initial/component/ExpandedScreen.kt | 2 +- .../screen/initial/component/MediumScreen.kt | 2 +- .../screen/initial/home/HomeComponent.kt | 33 --- .../screen/initial/home/HomeScreen.kt | 220 ------------------ .../initial/home/HomeScreenComponent.kt | 88 ------- .../initial/home/component/AiringOverview.kt | 104 --------- .../home/component/PopularSeasonOverview.kt | 117 ---------- .../home/component/TrendingOverview.kt | 100 -------- .../screen/medium/MediumScreenComponent.kt | 3 +- .../screen/medium/component/GenreSection.kt | 3 +- 35 files changed, 752 insertions(+), 851 deletions(-) create mode 100644 anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/state/CollectionState.kt delete mode 100644 anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/state/SeasonState.kt rename composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/{initial/home => }/component/CollapsingToolbar.kt (65%) create mode 100644 composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/component/HidingNavigationBar.kt create mode 100644 composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeComponent.kt create mode 100644 composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeScreen.kt create mode 100644 composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeScreenComponent.kt create mode 100644 composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/component/AllLoadingView.kt create mode 100644 composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/component/DefaultOverview.kt create mode 100644 composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/component/ScheduleOverview.kt rename composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/{initial/home/component => home/component/airing}/Airing.kt (93%) rename composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/{initial/home/component => home/component/airing}/AiringCard.kt (84%) rename composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/{initial/home/component => home/component/airing}/Episode.kt (94%) rename composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/{initial/home/component => home/component/default}/GenreChip.kt (95%) rename composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/{initial/home/component => home/component/default}/MediumCard.kt (92%) rename composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/{initial/home/component => home/component/default}/Rating.kt (92%) delete mode 100644 composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/HomeComponent.kt delete mode 100644 composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/HomeScreen.kt delete mode 100644 composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/HomeScreenComponent.kt delete mode 100644 composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/component/AiringOverview.kt delete mode 100644 composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/component/PopularSeasonOverview.kt delete mode 100644 composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/component/TrendingOverview.kt diff --git a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/AiringTodayRepository.kt b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/AiringTodayRepository.kt index 6265668..5be5087 100644 --- a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/AiringTodayRepository.kt +++ b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/AiringTodayRepository.kt @@ -6,6 +6,7 @@ import dev.datlag.aniflow.anilist.model.Medium import dev.datlag.aniflow.anilist.type.AiringSort import kotlinx.coroutines.flow.* import kotlinx.datetime.Clock +import kotlinx.serialization.Serializable import kotlin.time.Duration.Companion.hours class AiringTodayRepository( @@ -85,10 +86,14 @@ class AiringTodayRepository( } sealed interface State { + @Serializable + data object None : State + data class Success( val collection: Collection ) : State + @Serializable data object Error : State companion object { diff --git a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/PopularNextSeasonRepository.kt b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/PopularNextSeasonRepository.kt index b8a666b..9149513 100644 --- a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/PopularNextSeasonRepository.kt +++ b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/PopularNextSeasonRepository.kt @@ -3,7 +3,7 @@ package dev.datlag.aniflow.anilist import com.apollographql.apollo3.ApolloClient import com.apollographql.apollo3.api.Optional import dev.datlag.aniflow.anilist.common.nextSeason -import dev.datlag.aniflow.anilist.state.SeasonState +import dev.datlag.aniflow.anilist.state.CollectionState import dev.datlag.aniflow.anilist.type.MediaSeason import dev.datlag.aniflow.anilist.type.MediaSort import dev.datlag.aniflow.anilist.type.MediaType @@ -14,24 +14,14 @@ class PopularNextSeasonRepository( private val apolloClient: ApolloClient, private val fallbackClient: ApolloClient, private val nsfw: Flow = flowOf(false), - private val viewManga: Flow = flowOf(false), ) { private val page = MutableStateFlow(0) - private val type = viewManga.distinctUntilChanged().map { - page.update { 0 } - if (it) { - MediaType.MANGA - } else { - MediaType.ANIME - } - } - private val query = combine(page, type, nsfw.distinctUntilChanged()) { p, t, n -> + private val query = combine(page, nsfw.distinctUntilChanged()) { p, n -> val (season, year) = Clock.System.now().nextSeason Query( page = p, - type = t, nsfw = n, season = season, year = year @@ -43,12 +33,12 @@ class PopularNextSeasonRepository( val data = it.data if (data == null) { if (it.hasErrors()) { - SeasonState.fromGraphQL(data) + CollectionState.fromSeasonGraphQL(data) } else { null } } else { - SeasonState.fromGraphQL(data) + CollectionState.fromSeasonGraphQL(data) } } @@ -58,15 +48,15 @@ class PopularNextSeasonRepository( val data = it.data if (data == null) { if (it.hasErrors()) { - SeasonState.fromGraphQL(data) + CollectionState.fromSeasonGraphQL(data) } else { null } } else { - SeasonState.fromGraphQL(data) + CollectionState.fromSeasonGraphQL(data) } }.transform { - return@transform if (it is SeasonState.Error) { + return@transform if (it.isError) { emitAll(fallbackQuery) } else { emit(it) @@ -83,7 +73,6 @@ class PopularNextSeasonRepository( private data class Query( val page: Int, - val type: MediaType, val nsfw: Boolean, val season: MediaSeason, val year: Int @@ -96,7 +85,7 @@ class PopularNextSeasonRepository( } else { Optional.present(nsfw) }, - type = Optional.present(type), + type = Optional.present(MediaType.ANIME), sort = Optional.present(listOf(MediaSort.POPULARITY_DESC)), preventGenres = if (nsfw) { Optional.absent() diff --git a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/PopularSeasonRepository.kt b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/PopularSeasonRepository.kt index b305ee2..7ad69b7 100644 --- a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/PopularSeasonRepository.kt +++ b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/PopularSeasonRepository.kt @@ -2,7 +2,7 @@ package dev.datlag.aniflow.anilist import com.apollographql.apollo3.ApolloClient import com.apollographql.apollo3.api.Optional -import dev.datlag.aniflow.anilist.state.SeasonState +import dev.datlag.aniflow.anilist.state.CollectionState import dev.datlag.aniflow.anilist.type.MediaSort import dev.datlag.aniflow.anilist.type.MediaType import kotlinx.coroutines.flow.* @@ -36,12 +36,12 @@ class PopularSeasonRepository( val data = it.data if (data == null) { if (it.hasErrors()) { - SeasonState.fromGraphQL(data) + CollectionState.fromSeasonGraphQL(data) } else { null } } else { - SeasonState.fromGraphQL(data) + CollectionState.fromSeasonGraphQL(data) } } @@ -51,15 +51,15 @@ class PopularSeasonRepository( val data = it.data if (data == null) { if (it.hasErrors()) { - SeasonState.fromGraphQL(data) + CollectionState.fromSeasonGraphQL(data) } else { null } } else { - SeasonState.fromGraphQL(data) + CollectionState.fromSeasonGraphQL(data) } }.transform { - return@transform if (it is SeasonState.Error) { + return@transform if (it.isError) { emitAll(fallbackQuery) } else { emit(it) diff --git a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/TrendingRepository.kt b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/TrendingRepository.kt index 049dfaf..a9a54d9 100644 --- a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/TrendingRepository.kt +++ b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/TrendingRepository.kt @@ -3,9 +3,11 @@ package dev.datlag.aniflow.anilist import com.apollographql.apollo3.ApolloClient import com.apollographql.apollo3.api.Optional import dev.datlag.aniflow.anilist.model.Medium +import dev.datlag.aniflow.anilist.state.CollectionState import dev.datlag.aniflow.anilist.type.MediaSort import dev.datlag.aniflow.anilist.type.MediaType import kotlinx.coroutines.flow.* +import kotlinx.serialization.Serializable class TrendingRepository( private val apolloClient: ApolloClient, @@ -36,12 +38,12 @@ class TrendingRepository( val data = it.data if (data == null) { if (it.hasErrors()) { - State.fromGraphQL(data) + CollectionState.fromTrendingGraphQL(data) } else { null } } else { - State.fromGraphQL(data) + CollectionState.fromTrendingGraphQL(data) } } @@ -51,15 +53,15 @@ class TrendingRepository( val data = it.data if (data == null) { if (it.hasErrors()) { - State.fromGraphQL(data) + CollectionState.fromTrendingGraphQL(data) } else { null } } else { - State.fromGraphQL(data) + CollectionState.fromTrendingGraphQL(data) } }.transform { - return@transform if (it is State.Error) { + return@transform if (it.isError) { emitAll(fallbackQuery) } else { emit(it) @@ -98,24 +100,4 @@ class TrendingRepository( html = Optional.present(true) ) } - - sealed interface State { - data class Success( - val collection: Collection - ) : State - - data object Error : State - - companion object { - fun fromGraphQL(data: TrendingQuery.Data?): State { - val mediaList = data?.Page?.mediaFilterNotNull() - - if (mediaList.isNullOrEmpty()) { - return Error - } - - return Success(mediaList.map { Medium(it) }) - } - } - } } \ No newline at end of file diff --git a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/state/CollectionState.kt b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/state/CollectionState.kt new file mode 100644 index 0000000..7653629 --- /dev/null +++ b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/state/CollectionState.kt @@ -0,0 +1,52 @@ +package dev.datlag.aniflow.anilist.state + +import dev.datlag.aniflow.anilist.SeasonQuery +import dev.datlag.aniflow.anilist.TrendingQuery +import dev.datlag.aniflow.anilist.model.Medium +import kotlinx.serialization.Serializable + +@Serializable +sealed interface CollectionState { + + val isSuccess: Boolean + get() = this is Success + + val isError: Boolean + get() = this is Error + + @Serializable + data object None : CollectionState + + @Serializable + data class Success( + val collection: Collection + ) : CollectionState + + @Serializable + data object Error : CollectionState + + companion object { + fun fromTrendingGraphQL(data: TrendingQuery.Data?): CollectionState { + val mediaList = data?.Page?.mediaFilterNotNull() + + if (mediaList.isNullOrEmpty()) { + return Error + } + + return Success(mediaList.map { Medium(it) }) + } + + fun fromSeasonGraphQL(data: SeasonQuery.Data?): CollectionState { + val mediaList = data?.Page?.mediaFilterNotNull() + + if (mediaList.isNullOrEmpty()) { + return Error + } + + return Success(mediaList.map { Medium(it) }) + } + } +} + +val CollectionState?.isLoading: Boolean + get() = this == null || this is CollectionState.None \ No newline at end of file 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 deleted file mode 100644 index 9e588af..0000000 --- a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/state/SeasonState.kt +++ /dev/null @@ -1,33 +0,0 @@ -package dev.datlag.aniflow.anilist.state - -import com.apollographql.apollo3.api.Optional -import dev.datlag.aniflow.anilist.AdultContent -import dev.datlag.aniflow.anilist.SeasonQuery -import dev.datlag.aniflow.anilist.common.season -import dev.datlag.aniflow.anilist.common.year -import dev.datlag.aniflow.anilist.model.Medium -import dev.datlag.aniflow.anilist.type.MediaSeason -import dev.datlag.aniflow.anilist.type.MediaSort -import dev.datlag.aniflow.anilist.type.MediaType -import kotlinx.datetime.Clock -import kotlinx.datetime.Instant - -sealed interface SeasonState { - data class Success( - val collection: Collection - ) : SeasonState - - data object Error : SeasonState - - companion object { - fun fromGraphQL(data: SeasonQuery.Data?): SeasonState { - val mediaList = data?.Page?.mediaFilterNotNull() - - if (mediaList.isNullOrEmpty()) { - return Error - } - - return Success(mediaList.map { Medium(it) }) - } - } -} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/module/NetworkModule.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/module/NetworkModule.kt index f74faa3..9ad4636 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/module/NetworkModule.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/module/NetworkModule.kt @@ -154,8 +154,7 @@ data object NetworkModule { PopularNextSeasonRepository( apolloClient = instance(Constants.AniList.APOLLO_CLIENT), fallbackClient = instance(Constants.AniList.FALLBACK_APOLLO_CLIENT), - nsfw = appSettings.adultContent, - viewManga = appSettings.viewManga + nsfw = appSettings.adultContent ) } bindSingleton { diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/other/StateSaver.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/other/StateSaver.kt index f5ee1b7..1c41456 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/other/StateSaver.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/other/StateSaver.kt @@ -4,7 +4,7 @@ import androidx.compose.ui.graphics.Color import com.mayakapps.kache.InMemoryKache import com.mayakapps.kache.KacheStrategy import dev.datlag.aniflow.anilist.* -import dev.datlag.aniflow.anilist.state.SeasonState +import dev.datlag.aniflow.anilist.state.CollectionState import dev.datlag.aniflow.settings.model.AppSettings import dev.datlag.tooling.async.scopeCatching import dev.datlag.tooling.async.suspendCatching @@ -76,17 +76,17 @@ data object StateSaver { return state } - fun updateTrending(state: TrendingRepository.State): TrendingRepository.State { + fun updateTrending(state: CollectionState): CollectionState { trendingLoading.update { false } return state } - fun updatePopularCurrent(state: SeasonState): SeasonState { + fun updatePopularCurrent(state: CollectionState): CollectionState { popularCurrentLoading.update { false } return state } - fun updatePopularNext(state: SeasonState): SeasonState { + fun updatePopularNext(state: CollectionState): CollectionState { popularNextLoading.update { false } return state } 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 c72aa54..9f176e1 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 @@ -16,6 +16,7 @@ import com.arkivanov.decompose.router.stack.* import dev.datlag.aniflow.common.onRender import dev.datlag.aniflow.model.ifValueOrNull import dev.datlag.aniflow.other.UserHelper +import dev.datlag.aniflow.ui.navigation.screen.home.HomeScreenComponent import dev.datlag.aniflow.ui.navigation.screen.initial.InitialScreenComponent import dev.datlag.aniflow.ui.navigation.screen.medium.MediumScreenComponent import dev.datlag.aniflow.ui.navigation.screen.settings.SettingsScreen @@ -43,7 +44,7 @@ class RootComponent( componentContext: ComponentContext ): Component { return when (rootConfig) { - is RootConfig.Home -> InitialScreenComponent( + is RootConfig.Home -> HomeScreenComponent( componentContext = componentContext, di = di, onMediumDetails = { diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/component/CollapsingToolbar.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/component/CollapsingToolbar.kt similarity index 65% rename from composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/component/CollapsingToolbar.kt rename to composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/component/CollapsingToolbar.kt index b060205..679dbe2 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/component/CollapsingToolbar.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/component/CollapsingToolbar.kt @@ -1,5 +1,6 @@ -package dev.datlag.aniflow.ui.navigation.screen.initial.home.component +package dev.datlag.aniflow.ui.navigation.screen.component +import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.* @@ -25,7 +26,6 @@ import dev.datlag.aniflow.LocalHaze import dev.datlag.aniflow.SharedRes import dev.datlag.aniflow.anilist.type.MediaType import dev.datlag.tooling.compose.ifFalse -import dev.datlag.tooling.compose.ifTrue import dev.datlag.tooling.decompose.lifecycle.collectAsStateWithLifecycle import dev.icerock.moko.resources.compose.painterResource import kotlinx.coroutines.flow.Flow @@ -84,7 +84,55 @@ fun CollapsingToolbar( ) } }, - title = { }, + title = { + AnimatedVisibility( + visible = isCollapsed + ) { + Text(text = "AniFlow") + } + }, + actions = { + Row( + modifier = Modifier.ifFalse(isCollapsed) { + background(MaterialTheme.colorScheme.surface.copy(alpha = 0.75F), CircleShape) + }, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.End, + ) { + IconButton( + onClick = { + onAnimeClick() + }, + enabled = isManga + ) { + Icon( + imageVector = Icons.Filled.PlayCircleFilled, + contentDescription = null, + tint = if (isManga) { + LocalContentColor.current + } else { + MaterialTheme.colorScheme.primary + } + ) + } + IconButton( + onClick = { + onMangaClick() + }, + enabled = !isManga + ) { + Icon( + imageVector = Icons.AutoMirrored.Default.MenuBook, + contentDescription = null, + tint = if (isManga) { + MaterialTheme.colorScheme.primary + } else { + LocalContentColor.current + } + ) + } + } + }, scrollBehavior = scrollBehavior, colors = TopAppBarDefaults.largeTopAppBarColors( containerColor = Color.Transparent, @@ -97,55 +145,5 @@ fun CollapsingToolbar( ) ).fillMaxWidth() ) - - Row( - modifier = Modifier.padding( - bottom = 6.dp, - end = 4.dp - ).align(Alignment.BottomEnd), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.End - ) { - Row( - modifier = Modifier.ifFalse(isCollapsed) { - background(MaterialTheme.colorScheme.surface.copy(alpha = 0.75F), CircleShape) - }, - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.End, - ) { - IconButton( - onClick = { - onAnimeClick() - }, - enabled = isManga - ) { - Icon( - imageVector = Icons.Filled.PlayCircleFilled, - contentDescription = null, - tint = if (isManga) { - LocalContentColor.current - } else { - MaterialTheme.colorScheme.primary - } - ) - } - IconButton( - onClick = { - onMangaClick() - }, - enabled = !isManga - ) { - Icon( - imageVector = Icons.AutoMirrored.Default.MenuBook, - contentDescription = null, - tint = if (isManga) { - MaterialTheme.colorScheme.primary - } else { - LocalContentColor.current - } - ) - } - } - } } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/component/HidingNavigationBar.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/component/HidingNavigationBar.kt new file mode 100644 index 0000000..8c2daf2 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/component/HidingNavigationBar.kt @@ -0,0 +1,88 @@ +package dev.datlag.aniflow.ui.navigation.screen.component + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.LinearOutSlowInEasing +import androidx.compose.animation.core.tween +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.FavoriteBorder +import androidx.compose.material.icons.filled.Home +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.dp +import dev.chrisbanes.haze.hazeChild +import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi +import dev.chrisbanes.haze.materials.HazeMaterials +import dev.datlag.aniflow.LocalHaze +import dev.datlag.aniflow.SharedRes +import dev.datlag.aniflow.common.isScrollingUp +import dev.icerock.moko.resources.compose.stringResource + +@OptIn(ExperimentalHazeMaterialsApi::class) +@Composable +fun HidingNavigationBar( + visible: Boolean, +) { + val density = LocalDensity.current + + AnimatedVisibility( + visible = visible, + enter = slideInVertically( + initialOffsetY = { + with(density) { it.dp.roundToPx() } + }, + animationSpec = tween( + easing = LinearOutSlowInEasing, + durationMillis = 500 + ) + ), + exit = slideOutVertically( + targetOffsetY = { it }, + animationSpec = tween( + easing = LinearOutSlowInEasing, + durationMillis = 500 + ) + ) + ) { + NavigationBar( + modifier = Modifier.hazeChild( + state = LocalHaze.current, + style = HazeMaterials.thin(NavigationBarDefaults.containerColor) + ).fillMaxWidth(), + containerColor = Color.Transparent, + contentColor = MaterialTheme.colorScheme.contentColorFor(NavigationBarDefaults.containerColor) + ) { + NavigationBarItem( + onClick = { }, + selected = true, + icon = { + Icon( + imageVector = Icons.Filled.Home, + contentDescription = null + ) + }, + label = { + Text(text = stringResource(SharedRes.strings.home)) + } + ) + NavigationBarItem( + onClick = { }, + selected = false, + icon = { + Icon( + imageVector = Icons.Default.FavoriteBorder, + contentDescription = null + ) + }, + label = { + Text(text = stringResource(SharedRes.strings.favorites)) + } + ) + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeComponent.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeComponent.kt new file mode 100644 index 0000000..d3911ee --- /dev/null +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeComponent.kt @@ -0,0 +1,24 @@ +package dev.datlag.aniflow.ui.navigation.screen.home + +import dev.datlag.aniflow.anilist.AiringTodayRepository +import dev.datlag.aniflow.anilist.TrendingRepository +import dev.datlag.aniflow.anilist.model.Medium +import dev.datlag.aniflow.anilist.state.CollectionState +import dev.datlag.aniflow.anilist.type.MediaType +import dev.datlag.aniflow.ui.navigation.Component +import kotlinx.coroutines.flow.Flow + +interface HomeComponent : Component { + val viewing: Flow + + val airing: Flow + val trending: Flow + val popularNow: Flow + val popularNext: Flow + + fun viewProfile() + fun viewAnime() + fun viewManga() + + fun details(medium: Medium) +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeScreen.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeScreen.kt new file mode 100644 index 0000000..d5402a7 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeScreen.kt @@ -0,0 +1,129 @@ +package dev.datlag.aniflow.ui.navigation.screen.home + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.LinearOutSlowInEasing +import androidx.compose.animation.core.tween +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.dp +import dev.chrisbanes.haze.haze +import dev.chrisbanes.haze.hazeChild +import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi +import dev.chrisbanes.haze.materials.HazeMaterials +import dev.datlag.aniflow.LocalHaze +import dev.datlag.aniflow.LocalPaddingValues +import dev.datlag.aniflow.anilist.type.MediaType +import dev.datlag.aniflow.common.LocalPadding +import dev.datlag.aniflow.common.isScrollingUp +import dev.datlag.aniflow.other.StateSaver +import dev.datlag.aniflow.ui.navigation.screen.component.CollapsingToolbar +import dev.datlag.aniflow.ui.navigation.screen.component.HidingNavigationBar +import dev.datlag.aniflow.ui.navigation.screen.home.component.AllLoadingView +import dev.datlag.aniflow.ui.navigation.screen.home.component.DefaultOverview +import dev.datlag.aniflow.ui.navigation.screen.home.component.ScheduleOverview +import dev.datlag.tooling.decompose.lifecycle.collectAsStateWithLifecycle + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun HomeScreen(component: HomeComponent) { + val appBarState = rememberTopAppBarState() + val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior( + state = appBarState + ) + val listState = rememberLazyListState() + + Scaffold( + modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + topBar = { + CollapsingToolbar( + state = appBarState, + scrollBehavior = scrollBehavior, + viewTypeFlow = component.viewing, + onProfileClick = component::viewProfile, + onAnimeClick = component::viewAnime, + onMangaClick = component::viewManga + ) + }, + floatingActionButton = { + + }, + bottomBar = { + HidingNavigationBar( + visible = listState.isScrollingUp() + ) + } + ) { + CompositionLocalProvider( + LocalPaddingValues provides it + ) { + AllLoadingView( + loadingContent = { + + } + ) { + val viewType by component.viewing.collectAsStateWithLifecycle(MediaType.UNKNOWN__) + val isManga = remember(viewType) { + viewType == MediaType.MANGA + } + + LazyColumn( + state = listState, + modifier = Modifier.fillMaxSize().haze(state = LocalHaze.current), + verticalArrangement = Arrangement.spacedBy(32.dp), + contentPadding = LocalPadding(top = 16.dp) + ) { + if (!isManga) { + item { + ScheduleOverview( + flow = component.airing, + onMoreClick = { }, + onMediumClick = component::details + ) + } + } + item { + DefaultOverview( + title = "Trending", + flow = component.trending, + onMoreClick = { }, + onMediumClick = component::details + ) + } + item { + DefaultOverview( + title = "Popular", + flow = component.popularNow, + onMoreClick = { }, + onMediumClick = component::details + ) + } + if (!isManga) { + item { + DefaultOverview( + title = "Popular Next", + flow = component.popularNext, + onMoreClick = { }, + onMediumClick = component::details + ) + } + } + } + } + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeScreenComponent.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeScreenComponent.kt new file mode 100644 index 0000000..b418f15 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeScreenComponent.kt @@ -0,0 +1,120 @@ +package dev.datlag.aniflow.ui.navigation.screen.home + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.remember +import com.arkivanov.decompose.ComponentContext +import dev.chrisbanes.haze.HazeState +import dev.datlag.aniflow.LocalHaze +import dev.datlag.aniflow.anilist.AiringTodayRepository +import dev.datlag.aniflow.anilist.PopularNextSeasonRepository +import dev.datlag.aniflow.anilist.PopularSeasonRepository +import dev.datlag.aniflow.anilist.TrendingRepository +import dev.datlag.aniflow.anilist.model.Medium +import dev.datlag.aniflow.anilist.state.CollectionState +import dev.datlag.aniflow.anilist.type.MediaType +import dev.datlag.aniflow.common.onRender +import dev.datlag.aniflow.model.coroutines.Executor +import dev.datlag.aniflow.other.StateSaver +import dev.datlag.aniflow.settings.Settings +import dev.datlag.tooling.decompose.ioScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import org.kodein.di.DI +import org.kodein.di.instance + +class HomeScreenComponent( + componentContext: ComponentContext, + override val di: DI, + private val onMediumDetails: (Medium) -> Unit, + private val onProfile: () -> Unit +) : HomeComponent, ComponentContext by componentContext { + + private val appSettings by instance() + override val viewing = appSettings.viewManga.map { + if (it) { + MediaType.MANGA + } else { + MediaType.ANIME + } + } + private val viewTypeExecutor = Executor() + private val stateScope = ioScope() + + private val airingTodayRepository by instance() + override val airing: Flow = airingTodayRepository.airing.map { + StateSaver.Home.updateAiring(it) + }.stateIn( + scope = stateScope, + started = SharingStarted.WhileSubscribed(), + initialValue = AiringTodayRepository.State.None + ) + + private val trendingRepository by instance() + override val trending: Flow = trendingRepository.trending.map { + StateSaver.Home.updateTrending(it) + }.stateIn( + scope = stateScope, + started = SharingStarted.WhileSubscribed(), + initialValue = CollectionState.None + ) + + private val popularSeasonRepository by instance() + override val popularNow: Flow = popularSeasonRepository.popularThisSeason.map { + StateSaver.Home.updatePopularCurrent(it) + }.stateIn( + scope = stateScope, + started = SharingStarted.WhileSubscribed(), + initialValue = CollectionState.None + ) + + private val popularNextSeasonRepository by instance() + override val popularNext: Flow = popularNextSeasonRepository.popularNextSeason.map { + StateSaver.Home.updatePopularNext(it) + }.stateIn( + scope = stateScope, + started = SharingStarted.WhileSubscribed(), + initialValue = CollectionState.None + ) + + @Composable + override fun render() { + val haze = remember { HazeState() } + + CompositionLocalProvider( + LocalHaze provides haze, + ) { + onRender { + HomeScreen(this) + } + } + } + + override fun viewProfile() { + onProfile() + } + + override fun viewAnime() { + StateSaver.Home.updateAllLoading() + launchIO { + viewTypeExecutor.enqueue { + appSettings.setViewManga(false) + } + } + } + + override fun viewManga() { + StateSaver.Home.updateAllLoading() + launchIO { + viewTypeExecutor.enqueue { + appSettings.setViewManga(true) + } + } + } + + override fun details(medium: Medium) { + onMediumDetails(medium) + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/component/AllLoadingView.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/component/AllLoadingView.kt new file mode 100644 index 0000000..315b630 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/component/AllLoadingView.kt @@ -0,0 +1,28 @@ +package dev.datlag.aniflow.ui.navigation.screen.home.component + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import dev.datlag.aniflow.other.StateSaver +import dev.datlag.tooling.decompose.lifecycle.collectAsStateWithLifecycle + +@Composable +fun AllLoadingView( + modifier: Modifier = Modifier.fillMaxSize(), + loadingContent: @Composable BoxScope.() -> Unit, + content: @Composable BoxScope.() -> Unit +) { + Box( + modifier = modifier + ) { + val allLoading by StateSaver.Home.isAllLoading.collectAsStateWithLifecycle(StateSaver.Home.currentAllLoading) + + content() + if (allLoading) { + loadingContent() + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/component/DefaultOverview.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/component/DefaultOverview.kt new file mode 100644 index 0000000..2cdf9dd --- /dev/null +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/component/DefaultOverview.kt @@ -0,0 +1,101 @@ +package dev.datlag.aniflow.ui.navigation.screen.home.component + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowForwardIos +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import dev.datlag.aniflow.anilist.model.Medium +import dev.datlag.aniflow.anilist.state.CollectionState +import dev.datlag.aniflow.ui.navigation.screen.home.component.default.MediumCard +import dev.datlag.tooling.decompose.lifecycle.collectAsStateWithLifecycle +import kotlinx.coroutines.flow.Flow + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun DefaultOverview( + title: String, + flow: Flow, + onMoreClick: () -> Unit, + onMediumClick: (Medium) -> Unit, +) { + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + val state by flow.collectAsStateWithLifecycle(CollectionState.None) + + Row( + modifier = Modifier.padding(start = 16.dp, end = 4.dp).fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = title, + style = MaterialTheme.typography.headlineMedium, + fontWeight = FontWeight.Bold + ) + Spacer(modifier = Modifier.weight(1f)) + IconButton( + onClick = onMoreClick, + enabled = state.isSuccess + ) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowForwardIos, + contentDescription = null + ) + } + } + + when (val current = state) { + is CollectionState.None -> { + Loading() + } + is CollectionState.Success -> { + LazyRow( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(16.dp), + contentPadding = PaddingValues(horizontal = 16.dp) + ) { + items(current.collection.toList(), key = { it.id }) { + MediumCard( + medium = it, + titleLanguage = null, + modifier = Modifier + .width(200.dp) + .height(280.dp) + .animateItemPlacement(), + onClick = onMediumClick, + ) + } + } + } + is CollectionState.Error -> { + Text("Could not load $title") + } + } + } +} + +@Composable +private fun Loading() { + Box( + modifier = Modifier.fillMaxWidth().height(280.dp), + contentAlignment = Alignment.Center + ) { + LinearProgressIndicator( + modifier = Modifier.fillMaxWidth(fraction = 0.2F).clip(CircleShape) + ) + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/component/ScheduleOverview.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/component/ScheduleOverview.kt new file mode 100644 index 0000000..2228344 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/component/ScheduleOverview.kt @@ -0,0 +1,110 @@ +package dev.datlag.aniflow.ui.navigation.screen.home.component + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowForwardIos +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import dev.datlag.aniflow.anilist.AiringTodayRepository +import dev.datlag.aniflow.anilist.model.Medium +import dev.datlag.aniflow.other.StateSaver +import dev.datlag.aniflow.ui.navigation.screen.home.component.airing.AiringCard +import dev.datlag.tooling.decompose.lifecycle.collectAsStateWithLifecycle +import kotlinx.coroutines.flow.Flow + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun ScheduleOverview( + flow: Flow, + onMoreClick: () -> Unit, + onMediumClick: (Medium) -> Unit +) { + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + val state by flow.collectAsStateWithLifecycle(AiringTodayRepository.State.None) + + Row( + modifier = Modifier.padding(start = 16.dp, end = 4.dp).fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Schedule", + style = MaterialTheme.typography.headlineMedium, + fontWeight = FontWeight.Bold + ) + Spacer(modifier = Modifier.weight(1f)) + IconButton( + onClick = onMoreClick, + enabled = state is AiringTodayRepository.State.Success + ) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowForwardIos, + contentDescription = null + ) + } + } + + when (val current = state) { + is AiringTodayRepository.State.None -> { + Loading() + } + is AiringTodayRepository.State.Success -> { + val listState = rememberLazyListState( + initialFirstVisibleItemIndex = StateSaver.List.Home.airingOverview, + initialFirstVisibleItemScrollOffset = StateSaver.List.Home.airingOverviewOffset + ) + + LazyRow( + state = listState, + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + flingBehavior = rememberSnapFlingBehavior(listState), + horizontalArrangement = Arrangement.spacedBy(16.dp), + contentPadding = PaddingValues(horizontal = 16.dp) + ) { + items(current.collection.toList()) { + AiringCard( + airing = it, + titleLanguage = null, + modifier = Modifier + .height(150.dp) + .fillParentMaxWidth(fraction = 0.9F) + .animateItemPlacement(), + onClick = onMediumClick + ) + } + } + } + is AiringTodayRepository.State.Error -> { + Text("Could not load Schedule") + } + } + } +} + +@Composable +private fun Loading() { + Box( + modifier = Modifier.fillMaxWidth().height(150.dp), + contentAlignment = Alignment.Center + ) { + LinearProgressIndicator( + modifier = Modifier.fillMaxWidth(fraction = 0.2F).clip(CircleShape) + ) + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/component/Airing.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/component/airing/Airing.kt similarity index 93% rename from composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/component/Airing.kt rename to composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/component/airing/Airing.kt index ef4c57f..26ac5f2 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/component/Airing.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/component/airing/Airing.kt @@ -1,4 +1,4 @@ -package dev.datlag.aniflow.ui.navigation.screen.initial.home.component +package dev.datlag.aniflow.ui.navigation.screen.home.component.airing import androidx.compose.foundation.layout.* import androidx.compose.material.icons.Icons @@ -18,7 +18,6 @@ import dev.datlag.aniflow.common.formatNext import kotlinx.datetime.Instant import kotlinx.datetime.TimeZone import kotlinx.datetime.toLocalDateTime -import kotlin.time.Duration.Companion.seconds @Composable fun Airing(airingAt: Int, color: Color = LocalContentColor.current) { diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/component/AiringCard.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/component/airing/AiringCard.kt similarity index 84% rename from composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/component/AiringCard.kt rename to composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/component/airing/AiringCard.kt index dd290ac..fab58e0 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/component/AiringCard.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/component/airing/AiringCard.kt @@ -1,12 +1,10 @@ -package dev.datlag.aniflow.ui.navigation.screen.initial.home.component +package dev.datlag.aniflow.ui.navigation.screen.home.component.airing import androidx.compose.foundation.layout.* import androidx.compose.material3.Card import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.layout.ContentScale @@ -15,19 +13,10 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import coil3.compose.AsyncImage import coil3.compose.rememberAsyncImagePainter -import dev.datlag.aniflow.LocalDI import dev.datlag.aniflow.anilist.AiringQuery import dev.datlag.aniflow.anilist.model.Medium import dev.datlag.aniflow.common.preferred -import dev.datlag.aniflow.settings.Settings -import dev.datlag.aniflow.settings.model.AppSettings import dev.datlag.aniflow.ui.theme.SchemeTheme -import dev.datlag.aniflow.ui.theme.rememberSchemeThemeDominantColorState -import dev.datlag.tooling.decompose.lifecycle.collectAsStateWithLifecycle -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.launch -import org.kodein.di.instance -import org.kodein.di.instanceOrNull import dev.datlag.aniflow.settings.model.TitleLanguage as SettingsTitle @Composable diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/component/Episode.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/component/airing/Episode.kt similarity index 94% rename from composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/component/Episode.kt rename to composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/component/airing/Episode.kt index 65a47ab..bcc046c 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/component/Episode.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/component/airing/Episode.kt @@ -1,4 +1,4 @@ -package dev.datlag.aniflow.ui.navigation.screen.initial.home.component +package dev.datlag.aniflow.ui.navigation.screen.home.component.airing import androidx.compose.foundation.layout.* import androidx.compose.material.icons.Icons diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/component/GenreChip.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/component/default/GenreChip.kt similarity index 95% rename from composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/component/GenreChip.kt rename to composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/component/default/GenreChip.kt index 296f4ff..8fad0ab 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/component/GenreChip.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/component/default/GenreChip.kt @@ -1,4 +1,4 @@ -package dev.datlag.aniflow.ui.navigation.screen.initial.home.component +package dev.datlag.aniflow.ui.navigation.screen.home.component.default import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.wrapContentHeight diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/component/MediumCard.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/component/default/MediumCard.kt similarity index 92% rename from composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/component/MediumCard.kt rename to composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/component/default/MediumCard.kt index dbddefa..2e61f0f 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/component/MediumCard.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/component/default/MediumCard.kt @@ -1,10 +1,9 @@ -package dev.datlag.aniflow.ui.navigation.screen.initial.home.component +package dev.datlag.aniflow.ui.navigation.screen.home.component.default import androidx.compose.foundation.layout.* import androidx.compose.material3.Card import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text -import androidx.compose.material3.contentColorFor import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -14,14 +13,10 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import coil3.compose.AsyncImage import coil3.compose.rememberAsyncImagePainter -import com.mikepenz.markdown.compose.extendedspans.internal.deserializeToColor import dev.datlag.aniflow.anilist.model.Medium import dev.datlag.aniflow.common.* -import dev.datlag.aniflow.settings.model.AppSettings import dev.datlag.aniflow.ui.theme.SchemeTheme import dev.datlag.aniflow.ui.theme.rememberSchemeThemeDominantColorState -import dev.datlag.tooling.decompose.lifecycle.collectAsStateWithLifecycle -import kotlinx.coroutines.flow.Flow import dev.datlag.aniflow.settings.model.TitleLanguage as SettingsTitle @OptIn(ExperimentalStdlibApi::class) diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/component/Rating.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/component/default/Rating.kt similarity index 92% rename from composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/component/Rating.kt rename to composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/component/default/Rating.kt index c14d7dd..19b9cfe 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/component/Rating.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/component/default/Rating.kt @@ -1,4 +1,4 @@ -package dev.datlag.aniflow.ui.navigation.screen.initial.home.component +package dev.datlag.aniflow.ui.navigation.screen.home.component.default import androidx.compose.foundation.layout.* import androidx.compose.material.icons.Icons @@ -13,7 +13,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import dev.datlag.aniflow.anilist.TrendingQuery import dev.datlag.aniflow.anilist.model.Medium import dev.datlag.aniflow.model.round diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/InitialScreenComponent.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/InitialScreenComponent.kt index 0df6215..74c8091 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/InitialScreenComponent.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/InitialScreenComponent.kt @@ -21,7 +21,6 @@ import dev.datlag.aniflow.settings.Settings import dev.datlag.aniflow.ui.navigation.Component import dev.datlag.aniflow.ui.navigation.ContentHolderComponent import dev.datlag.aniflow.ui.navigation.screen.initial.favorites.FavoritesScreenComponent -import dev.datlag.aniflow.ui.navigation.screen.initial.home.HomeScreenComponent import dev.datlag.aniflow.ui.navigation.screen.settings.SettingsScreenComponent import kotlinx.coroutines.flow.map import org.kodein.di.DI @@ -90,17 +89,10 @@ class InitialScreenComponent( view: View, componentContext: ComponentContext ): Component { - return when (view) { - is View.Home -> HomeScreenComponent( - componentContext = componentContext, - di = di, - onMediumDetails = onMediumDetails - ) - is View.Favorites -> FavoritesScreenComponent( - componentContext = componentContext, - di = di - ) - } + return FavoritesScreenComponent( + componentContext = componentContext, + di = di + ) } @OptIn(ExperimentalDecomposeApi::class) diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/component/CompactScreen.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/component/CompactScreen.kt index 9f52d1b..a30a800 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/component/CompactScreen.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/component/CompactScreen.kt @@ -4,13 +4,9 @@ import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.* import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.MenuBook -import androidx.compose.material.icons.filled.AccountCircle import androidx.compose.material.icons.filled.CameraEnhance -import androidx.compose.material.icons.filled.PlayCircleFilled import androidx.compose.material3.* import androidx.compose.runtime.* -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.nestedscroll.nestedScroll @@ -24,7 +20,7 @@ import dev.datlag.aniflow.LocalHaze import dev.datlag.aniflow.LocalPaddingValues import dev.datlag.aniflow.common.isScrollingUp import dev.datlag.aniflow.ui.navigation.screen.initial.InitialComponent -import dev.datlag.aniflow.ui.navigation.screen.initial.home.component.CollapsingToolbar +import dev.datlag.aniflow.ui.navigation.screen.component.CollapsingToolbar import dev.datlag.aniflow.ui.navigation.screen.initial.model.FABConfig import dev.icerock.moko.resources.compose.stringResource diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/component/ExpandedScreen.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/component/ExpandedScreen.kt index 92201eb..621683c 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/component/ExpandedScreen.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/component/ExpandedScreen.kt @@ -15,7 +15,7 @@ import com.arkivanov.decompose.extensions.compose.subscribeAsState import dev.datlag.aniflow.common.isScrollingUp import dev.datlag.aniflow.ui.custom.ExpandedPages import dev.datlag.aniflow.ui.navigation.screen.initial.InitialComponent -import dev.datlag.aniflow.ui.navigation.screen.initial.home.component.CollapsingToolbar +import dev.datlag.aniflow.ui.navigation.screen.component.CollapsingToolbar import dev.datlag.aniflow.ui.navigation.screen.initial.model.FABConfig import dev.datlag.tooling.compose.EndCornerShape import dev.icerock.moko.resources.compose.stringResource diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/component/MediumScreen.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/component/MediumScreen.kt index 394bf0f..db72d58 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/component/MediumScreen.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/component/MediumScreen.kt @@ -15,7 +15,7 @@ import com.arkivanov.decompose.extensions.compose.subscribeAsState import dev.datlag.aniflow.common.isScrollingUp import dev.datlag.aniflow.ui.custom.ExpandedPages import dev.datlag.aniflow.ui.navigation.screen.initial.InitialComponent -import dev.datlag.aniflow.ui.navigation.screen.initial.home.component.CollapsingToolbar +import dev.datlag.aniflow.ui.navigation.screen.component.CollapsingToolbar import dev.datlag.aniflow.ui.navigation.screen.initial.model.FABConfig import dev.icerock.moko.resources.compose.stringResource diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/HomeComponent.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/HomeComponent.kt deleted file mode 100644 index bc678bd..0000000 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/HomeComponent.kt +++ /dev/null @@ -1,33 +0,0 @@ -package dev.datlag.aniflow.ui.navigation.screen.initial.home - -import com.arkivanov.decompose.router.slot.ChildSlot -import com.arkivanov.decompose.value.Value -import dev.datlag.aniflow.anilist.AiringTodayRepository -import dev.datlag.aniflow.anilist.PopularSeasonRepository -import dev.datlag.aniflow.anilist.TrendingRepository -import dev.datlag.aniflow.anilist.model.Medium -import dev.datlag.aniflow.anilist.state.SeasonState -import dev.datlag.aniflow.anilist.type.MediaType -import dev.datlag.aniflow.settings.model.AppSettings -import dev.datlag.aniflow.trace.TraceStateMachine -import dev.datlag.aniflow.ui.navigation.Component -import dev.datlag.aniflow.ui.navigation.ContentHolderComponent -import io.ktor.utils.io.* -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.StateFlow -import dev.datlag.aniflow.settings.model.TitleLanguage as SettingsTitle - -interface HomeComponent : ContentHolderComponent { - val titleLanguage: Flow - - val airingState: Flow - val trendingState: Flow - val popularSeasonState: Flow - val popularNextSeasonState: Flow - - val traceState: Flow - - fun details(medium: Medium) - fun trace(channel: ByteArray) -} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/HomeScreen.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/HomeScreen.kt deleted file mode 100644 index c502384..0000000 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/HomeScreen.kt +++ /dev/null @@ -1,220 +0,0 @@ -package dev.datlag.aniflow.ui.navigation.screen.initial.home - -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.MenuBook -import androidx.compose.material.icons.filled.Camera -import androidx.compose.material.icons.filled.CameraEnhance -import androidx.compose.material.icons.filled.MenuBook -import androidx.compose.material.icons.filled.PlayCircleFilled -import androidx.compose.material3.* -import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi -import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass -import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp -import com.arkivanov.decompose.extensions.compose.subscribeAsState -import com.maxkeppeker.sheets.core.models.base.rememberUseCaseState -import com.maxkeppeler.sheets.option.OptionDialog -import com.maxkeppeler.sheets.option.models.DisplayMode -import com.maxkeppeler.sheets.option.models.Option -import com.maxkeppeler.sheets.option.models.OptionConfig -import com.maxkeppeler.sheets.option.models.OptionDetails -import com.maxkeppeler.sheets.option.models.OptionSelection -import dev.chrisbanes.haze.haze -import dev.datlag.aniflow.LocalHaze -import dev.datlag.aniflow.LocalPaddingValues -import dev.datlag.aniflow.SharedRes -import dev.datlag.aniflow.anilist.type.MediaType -import dev.datlag.aniflow.common.asMedium -import dev.datlag.aniflow.common.isScrollingUp -import dev.datlag.aniflow.common.plus -import dev.datlag.aniflow.common.preferred -import dev.datlag.aniflow.other.StateSaver -import dev.datlag.aniflow.other.rememberImagePickerState -import dev.datlag.aniflow.trace.TraceStateMachine -import dev.datlag.aniflow.trace.model.SearchResponse -import dev.datlag.aniflow.ui.navigation.screen.initial.home.component.AiringOverview -import dev.datlag.aniflow.ui.navigation.screen.initial.home.component.PopularSeasonOverview -import dev.datlag.aniflow.ui.navigation.screen.initial.home.component.TrendingOverview -import dev.datlag.aniflow.ui.navigation.screen.initial.model.FABConfig -import dev.datlag.tooling.decompose.lifecycle.collectAsStateWithLifecycle -import dev.icerock.moko.resources.compose.stringResource - -@Composable -fun HomeScreen(component: HomeComponent) { - val isAllLoading by StateSaver.Home.isAllLoading.collectAsStateWithLifecycle(StateSaver.Home.currentAllLoading) - - Box( - modifier = Modifier.fillMaxSize() - ) { - MainView(component, Modifier.fillMaxWidth()) - - if (isAllLoading) { - Box( - modifier = Modifier.fillMaxSize().background(MaterialTheme.colorScheme.background), - contentAlignment = Alignment.Center - ) { - LinearProgressIndicator( - modifier = Modifier.fillMaxWidth(fraction = 0.2F).clip(CircleShape) - ) - } - } - } -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun MainView(component: HomeComponent, modifier: Modifier = Modifier) { - val padding = PaddingValues(vertical = 16.dp) - val listState = rememberLazyListState( - initialFirstVisibleItemIndex = StateSaver.List.homeOverview, - initialFirstVisibleItemScrollOffset = StateSaver.List.homeOverviewOffset - ) - val imagePicker = rememberImagePickerState { - it?.let(component::trace) - } - val traceState by component.traceState.collectAsStateWithLifecycle(TraceStateMachine.State.Waiting) - val titleLanguage by component.titleLanguage.collectAsStateWithLifecycle(null) - - LaunchedEffect(listState, traceState) { - FABConfig.state.value = FABConfig.Scan( - listState = listState, - loading = traceState.isLoading, - onClick = { - imagePicker.launch() - } - ) - } - - when (val current = traceState) { - is TraceStateMachine.State.Success -> { - val results = remember(traceState) { - current.response.combinedResults.sortedWith( - compareByDescending { - it.maxSimilarity - }.thenByDescending { - it.avgSimilarity - } - ) - } - val useCase = rememberUseCaseState(visible = results.isNotEmpty()) - - OptionDialog( - state = useCase, - selection = OptionSelection.Single( - options = results.map { - Option( - titleText = it.aniList.asMedium().title.preferred(titleLanguage), - details = OptionDetails( - title = stringResource(SharedRes.strings.similarity_title), - body = if (it.isSingle) { - stringResource(SharedRes.strings.similarity_text_single, it.avgPercentage) - } else { - stringResource(SharedRes.strings.similarity_text_max_avg, it.maxPercentage, it.avgPercentage) - } - ) - ) - }, - onSelectOption = { index, _ -> - component.details(results[index].aniList.asMedium()) - } - ), - config = OptionConfig( - mode = DisplayMode.LIST - ) - ) - } - else -> { } - } - - LazyColumn( - state = listState, - modifier = modifier.haze(state = LocalHaze.current), - contentPadding = LocalPaddingValues.current?.plus(padding) ?: padding, - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - item { - Text( - modifier = Modifier.padding(horizontal = 16.dp), - text = "Schedule", - style = MaterialTheme.typography.headlineMedium, - fontWeight = FontWeight.Bold - ) - } - item { - AiringOverview( - state = component.airingState, - titleLanguage = titleLanguage, - onClick = component::details - ) - } - item { - Text( - text = "Trending", - style = MaterialTheme.typography.headlineMedium, - fontWeight = FontWeight.Bold, - modifier = Modifier.padding(top = 16.dp, start = 16.dp, end = 16.dp) - ) - } - item { - TrendingOverview( - state = component.trendingState, - titleLanguage = titleLanguage, - onClick = component::details - ) - } - item { - Text( - text = "Popular This Season", - style = MaterialTheme.typography.headlineMedium, - fontWeight = FontWeight.Bold, - modifier = Modifier.padding(top = 16.dp, start = 16.dp, end = 16.dp) - ) - } - item { - PopularSeasonOverview( - state = component.popularSeasonState, - current = true, - titleLanguage = titleLanguage, - onClick = component::details, - ) - } - item { - Text( - text = "Upcoming Next Season", - style = MaterialTheme.typography.headlineMedium, - fontWeight = FontWeight.Bold, - modifier = Modifier.padding(top = 16.dp, start = 16.dp, end = 16.dp) - ) - } - item { - PopularSeasonOverview( - state = component.popularNextSeasonState, - current = false, - titleLanguage = titleLanguage, - onClick = component::details - ) - } - } - - DisposableEffect(listState) { - onDispose { - StateSaver.List.homeOverview = listState.firstVisibleItemIndex - StateSaver.List.homeOverviewOffset = listState.firstVisibleItemScrollOffset - FABConfig.state.value = null - } - } -} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/HomeScreenComponent.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/HomeScreenComponent.kt deleted file mode 100644 index e0e0f5c..0000000 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/HomeScreenComponent.kt +++ /dev/null @@ -1,88 +0,0 @@ -package dev.datlag.aniflow.ui.navigation.screen.initial.home - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import com.arkivanov.decompose.ComponentContext -import com.arkivanov.decompose.router.slot.* -import com.arkivanov.decompose.value.MutableValue -import com.arkivanov.decompose.value.Value -import com.arkivanov.decompose.value.update -import dev.datlag.aniflow.LocalDI -import dev.datlag.aniflow.anilist.* -import dev.datlag.aniflow.anilist.model.Medium -import dev.datlag.aniflow.anilist.state.SeasonState -import dev.datlag.aniflow.anilist.type.MediaType -import dev.datlag.aniflow.common.onRender -import dev.datlag.aniflow.model.coroutines.Executor -import dev.datlag.aniflow.other.StateSaver -import dev.datlag.aniflow.settings.Settings -import dev.datlag.aniflow.settings.model.AppSettings -import dev.datlag.aniflow.trace.TraceStateMachine -import dev.datlag.aniflow.ui.navigation.Component -import dev.datlag.aniflow.ui.navigation.screen.medium.MediumScreenComponent -import dev.datlag.tooling.compose.ioDispatcher -import dev.datlag.tooling.decompose.ioScope -import dev.datlag.tooling.decompose.lifecycle.collectAsStateWithLifecycle -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.* -import org.kodein.di.DI -import org.kodein.di.instance -import kotlin.time.Duration.Companion.seconds -import dev.datlag.aniflow.settings.model.TitleLanguage as SettingsTitle - -class HomeScreenComponent( - componentContext: ComponentContext, - override val di: DI, - private val onMediumDetails: (Medium) -> Unit -) : HomeComponent, ComponentContext by componentContext { - - private val appSettings by di.instance() - override val titleLanguage: Flow = appSettings.titleLanguage.flowOn(ioDispatcher()) - - private val airingTodayRepository by di.instance() - override val airingState: Flow = airingTodayRepository.airing.map { - StateSaver.Home.updateAiring(it) - } - - private val trendingRepository by di.instance() - override val trendingState: Flow = trendingRepository.trending.map { - StateSaver.Home.updateTrending(it) - } - - private val popularSeasonRepository by di.instance() - override val popularSeasonState: Flow = popularSeasonRepository.popularThisSeason.map { - StateSaver.Home.updatePopularCurrent(it) - } - - private val popularNextSeasonRepository by di.instance() - override val popularNextSeasonState: Flow = popularNextSeasonRepository.popularNextSeason.map { - StateSaver.Home.updatePopularNext(it) - } - - private val traceStateMachine by di.instance() - override val traceState: Flow = traceStateMachine.state.flowOn( - context = ioDispatcher() - ) - - @Composable - override fun render() { - onRender { - HomeScreen(this) - } - } - - override fun dismissContent() { - - } - - override fun details(medium: Medium) { - onMediumDetails(medium) - } - - override fun trace(channel: ByteArray) { - launchIO { - traceStateMachine.dispatch(TraceStateMachine.Action.Load(channel)) - } - } -} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/component/AiringOverview.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/component/AiringOverview.kt deleted file mode 100644 index 2c34261..0000000 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/component/AiringOverview.kt +++ /dev/null @@ -1,104 +0,0 @@ -package dev.datlag.aniflow.ui.navigation.screen.initial.home.component - -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.gestures.FlingBehavior -import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyRow -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material3.CardDefaults -import androidx.compose.material3.LinearProgressIndicator -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.getValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.unit.dp -import dev.datlag.aniflow.anilist.AiringQuery -import dev.datlag.aniflow.anilist.AiringTodayRepository -import dev.datlag.aniflow.anilist.model.Medium -import dev.datlag.aniflow.other.StateSaver -import dev.datlag.aniflow.settings.model.AppSettings -import dev.datlag.tooling.decompose.lifecycle.collectAsStateWithLifecycle -import dev.datlag.tooling.safeSubList -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.StateFlow -import dev.datlag.aniflow.settings.model.TitleLanguage as SettingsTitle - -@Composable -fun AiringOverview( - state: Flow, - titleLanguage: SettingsTitle?, - onClick: (Medium) -> Unit -) { - val loadingState by state.collectAsStateWithLifecycle(null) - - when (val reachedState = loadingState) { - null -> Loading() - is AiringTodayRepository.State.Success -> { - SuccessContent( - data = reachedState.collection.toList(), - titleLanguage = titleLanguage, - onClick = onClick - ) - } - else -> { - - } - } -} - -@Composable -private fun Loading() { - Box( - modifier = Modifier.fillMaxWidth().height(150.dp), - contentAlignment = Alignment.Center - ) { - LinearProgressIndicator( - modifier = Modifier.fillMaxWidth(fraction = 0.2F).clip(CircleShape) - ) - } -} - -@OptIn(ExperimentalFoundationApi::class) -@Composable -private fun SuccessContent( - data: List, - titleLanguage: SettingsTitle?, - onClick: (Medium) -> Unit -) { - val state = rememberLazyListState( - initialFirstVisibleItemIndex = StateSaver.List.Home.airingOverview, - initialFirstVisibleItemScrollOffset = StateSaver.List.Home.airingOverviewOffset - ) - - LazyRow( - state = state, - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(8.dp), - flingBehavior = rememberSnapFlingBehavior(state), - contentPadding = PaddingValues(horizontal = 16.dp) - ) { - items(data.safeSubList(0, 10), key = { it.episode to it.media?.id }) { media -> - AiringCard( - airing = media, - titleLanguage = titleLanguage, - modifier = Modifier - .height(150.dp) - .fillParentMaxWidth(fraction = 0.9F) - .animateItemPlacement(), - onClick = onClick - ) - } - } - - DisposableEffect(state) { - onDispose { - StateSaver.List.Home.airingOverview = state.firstVisibleItemIndex - StateSaver.List.Home.airingOverviewOffset = state.firstVisibleItemScrollOffset - } - } -} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/component/PopularSeasonOverview.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/component/PopularSeasonOverview.kt deleted file mode 100644 index 869457e..0000000 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/component/PopularSeasonOverview.kt +++ /dev/null @@ -1,117 +0,0 @@ -package dev.datlag.aniflow.ui.navigation.screen.initial.home.component - -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyRow -import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material3.CardDefaults -import androidx.compose.material3.LinearProgressIndicator -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.unit.dp -import dev.datlag.aniflow.anilist.SeasonQuery -import dev.datlag.aniflow.anilist.TrendingQuery -import dev.datlag.aniflow.anilist.model.Medium -import dev.datlag.aniflow.anilist.state.SeasonState -import dev.datlag.aniflow.other.StateSaver -import dev.datlag.aniflow.settings.model.AppSettings -import dev.datlag.tooling.decompose.lifecycle.collectAsStateWithLifecycle -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.distinctUntilChanged -import dev.datlag.aniflow.settings.model.TitleLanguage as SettingsTitle - -@Composable -fun PopularSeasonOverview( - state: Flow, - current: Boolean, - titleLanguage: SettingsTitle?, - onClick: (Medium) -> Unit, -) { - val loadingState by state.collectAsStateWithLifecycle(null) - - when (val reachedState = loadingState) { - null -> Loading() - is SeasonState.Success -> { - SuccessContent( - data = reachedState.collection.toList(), - current = current, - titleLanguage = titleLanguage, - onClick = onClick - ) - } - else -> { - - } - } -} - -@Composable -private fun Loading() { - Box( - modifier = Modifier.fillMaxWidth().height(280.dp), - contentAlignment = Alignment.Center - ) { - LinearProgressIndicator( - modifier = Modifier.fillMaxWidth(fraction = 0.2F).clip(CircleShape) - ) - } -} - -@OptIn(ExperimentalFoundationApi::class) -@Composable -private fun SuccessContent( - data: List, - current: Boolean, - titleLanguage: SettingsTitle?, - onClick: (Medium) -> Unit -) { - val listState = rememberLazyListState( - initialFirstVisibleItemIndex = if (current) { - StateSaver.List.Home.popularOverview - } else { - StateSaver.List.Home.popularNextOverview - }, - initialFirstVisibleItemScrollOffset = if (current) { - StateSaver.List.Home.popularOverviewOffset - } else { - StateSaver.List.Home.popularNextOverviewOffset - }, - ) - - LazyRow( - state = listState, - modifier = Modifier.fillMaxSize(), - horizontalArrangement = Arrangement.spacedBy(16.dp), - contentPadding = PaddingValues(horizontal = 16.dp) - ) { - itemsIndexed(data, key = { _, it -> it.id }) { _, medium -> - MediumCard( - medium = medium, - titleLanguage = titleLanguage, - modifier = Modifier - .width(200.dp) - .height(280.dp) - .animateItemPlacement(), - onClick = onClick - ) - } - } - - DisposableEffect(listState) { - onDispose { - if (current) { - StateSaver.List.Home.popularOverview = listState.firstVisibleItemIndex - StateSaver.List.Home.popularOverviewOffset = listState.firstVisibleItemScrollOffset - } else { - StateSaver.List.Home.popularNextOverview = listState.firstVisibleItemIndex - StateSaver.List.Home.popularNextOverviewOffset = listState.firstVisibleItemScrollOffset - } - } - } -} diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/component/TrendingOverview.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/component/TrendingOverview.kt deleted file mode 100644 index 0cab00c..0000000 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/component/TrendingOverview.kt +++ /dev/null @@ -1,100 +0,0 @@ -package dev.datlag.aniflow.ui.navigation.screen.initial.home.component - -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyRow -import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material3.CardDefaults -import androidx.compose.material3.LinearProgressIndicator -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.unit.dp -import dev.datlag.aniflow.anilist.TrendingRepository -import dev.datlag.aniflow.anilist.model.Medium -import dev.datlag.aniflow.other.StateSaver -import dev.datlag.aniflow.settings.model.AppSettings -import dev.datlag.tooling.decompose.lifecycle.collectAsStateWithLifecycle -import dev.datlag.tooling.safeSubList -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.distinctUntilChanged -import dev.datlag.aniflow.settings.model.TitleLanguage as SettingsTitle - -@Composable -fun TrendingOverview( - state: Flow, - titleLanguage: SettingsTitle?, - onClick: (Medium) -> Unit, -) { - val loadingState by state.collectAsStateWithLifecycle(null) - - when (val reachedState = loadingState) { - null -> Loading() - is TrendingRepository.State.Success -> { - SuccessContent( - data = reachedState.collection.toList(), - titleLanguage = titleLanguage, - onClick = onClick - ) - } - else -> { - - } - } -} - -@Composable -private fun Loading() { - Box( - modifier = Modifier.fillMaxWidth().height(280.dp), - contentAlignment = Alignment.Center - ) { - LinearProgressIndicator( - modifier = Modifier.fillMaxWidth(fraction = 0.2F).clip(CircleShape) - ) - } -} - -@OptIn(ExperimentalFoundationApi::class) -@Composable -private fun SuccessContent( - data: List, - titleLanguage: SettingsTitle?, - onClick: (Medium) -> Unit -) { - val listState = rememberLazyListState( - initialFirstVisibleItemIndex = StateSaver.List.Home.trendingOverview, - initialFirstVisibleItemScrollOffset = StateSaver.List.Home.trendingOverviewOffset - ) - - LazyRow( - state = listState, - modifier = Modifier.fillMaxSize(), - horizontalArrangement = Arrangement.spacedBy(16.dp), - contentPadding = PaddingValues(horizontal = 16.dp) - ) { - itemsIndexed(data.safeSubList(0, 10), key = { _, it -> it.id }) { _, medium -> - MediumCard( - medium = medium, - titleLanguage = titleLanguage, - modifier = Modifier - .width(200.dp) - .height(280.dp) - .animateItemPlacement(), - onClick = onClick - ) - } - } - - DisposableEffect(listState) { - onDispose { - StateSaver.List.Home.trendingOverview = listState.firstVisibleItemIndex - StateSaver.List.Home.trendingOverviewOffset = listState.firstVisibleItemScrollOffset - } - } -} \ No newline at end of file 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 8fb315f..9e888c3 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 @@ -2,6 +2,7 @@ package dev.datlag.aniflow.ui.navigation.screen.medium import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.remember import com.apollographql.apollo3.ApolloClient import com.apollographql.apollo3.api.Optional import com.arkivanov.decompose.ComponentContext @@ -187,7 +188,7 @@ class MediumScreenComponent( @Composable override fun render() { - val state = HazeState() + val state = remember { HazeState() } CompositionLocalProvider( LocalHaze provides state 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 b785e37..bf097ec 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 @@ -10,10 +10,9 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import dev.datlag.aniflow.anilist.model.Medium -import dev.datlag.aniflow.ui.navigation.screen.initial.home.component.GenreChip +import dev.datlag.aniflow.ui.navigation.screen.home.component.default.GenreChip import dev.datlag.tooling.decompose.lifecycle.collectAsStateWithLifecycle import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.StateFlow @Composable fun GenreSection( From 9a478a6520e841654866a5e4c32c3db3308a5404 Mon Sep 17 00:00:00 2001 From: DatLag Date: Mon, 6 May 2024 21:20:26 +0200 Subject: [PATCH 02/23] fix some visual bugs --- .../kotlin/dev/datlag/aniflow/App.kt | 15 ++++++--------- .../datlag/aniflow/common/ExtendComponent.kt | 12 ------------ .../ui/navigation/screen/home/HomeScreen.kt | 4 ++-- .../screen/medium/MediumScreenComponent.kt | 7 ++----- .../dev/datlag/aniflow/ui/theme/SchemeTheme.kt | 18 ------------------ 5 files changed, 10 insertions(+), 46 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/App.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/App.kt index da67b2e..942bcb6 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/App.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/App.kt @@ -20,7 +20,6 @@ import dev.datlag.aniflow.other.StateSaver import dev.datlag.aniflow.settings.Settings import dev.datlag.aniflow.settings.model.AppSettings import dev.datlag.aniflow.ui.theme.Colors -import dev.datlag.aniflow.ui.theme.CommonSchemeTheme import dev.datlag.aniflow.ui.theme.DynamicMaterialTheme import dev.datlag.tooling.compose.toTypography import dev.datlag.tooling.decompose.lifecycle.collectAsStateWithLifecycle @@ -62,14 +61,12 @@ internal fun App( seedColor = tempColor?.toComposeColor() ?: seedColor, animate = !allLoading ) { - CommonSchemeTheme(animate = !allLoading) { - Surface( - modifier = Modifier.fillMaxSize(), - color = MaterialTheme.colorScheme.background, - contentColor = MaterialTheme.colorScheme.onBackground - ) { - content() - } + Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.background, + contentColor = MaterialTheme.colorScheme.onBackground + ) { + content() } } } diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/common/ExtendComponent.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/common/ExtendComponent.kt index 03cc05a..0cb01d9 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/common/ExtendComponent.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/common/ExtendComponent.kt @@ -41,18 +41,6 @@ fun Component.onRenderWithScheme(key: Any?, content: @Composable (SchemeTheme.Up } } -@Composable -fun Component.onRenderApplyCommonScheme(key: Any?, content: @Composable (SchemeTheme.Updater?) -> Unit) { - onRenderWithScheme(key, content) - - SchemeTheme.setCommon(key) - DisposableEffect(key) { - onDispose { - SchemeTheme.setCommon(null) - } - } -} - @Composable fun StateFlow.mapCollect(transform: (value: T) -> R): State { return remember(this) { diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeScreen.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeScreen.kt index d5402a7..d1ce91e 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeScreen.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeScreen.kt @@ -64,7 +64,7 @@ fun HomeScreen(component: HomeComponent) { }, bottomBar = { HidingNavigationBar( - visible = listState.isScrollingUp() + visible = listState.isScrollingUp() && listState.canScrollForward ) } ) { @@ -85,7 +85,7 @@ fun HomeScreen(component: HomeComponent) { state = listState, modifier = Modifier.fillMaxSize().haze(state = LocalHaze.current), verticalArrangement = Arrangement.spacedBy(32.dp), - contentPadding = LocalPadding(top = 16.dp) + contentPadding = LocalPadding(vertical = 16.dp) ) { if (!isManga) { item { 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 9e888c3..0ee9887 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 @@ -16,10 +16,7 @@ import dev.datlag.aniflow.anilist.model.Medium import dev.datlag.aniflow.anilist.type.MediaFormat import dev.datlag.aniflow.anilist.type.MediaStatus import dev.datlag.aniflow.anilist.type.MediaType -import dev.datlag.aniflow.common.nullableFirebaseInstance -import dev.datlag.aniflow.common.onRenderApplyCommonScheme -import dev.datlag.aniflow.common.popular -import dev.datlag.aniflow.common.rated +import dev.datlag.aniflow.common.* import dev.datlag.aniflow.model.* import dev.datlag.aniflow.other.BurningSeriesResolver import dev.datlag.aniflow.other.Constants @@ -193,7 +190,7 @@ class MediumScreenComponent( CompositionLocalProvider( LocalHaze provides state ) { - onRenderApplyCommonScheme(initialMedium.id) { + onRenderWithScheme(initialMedium.id) { MediumScreen(this) } } diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/theme/SchemeTheme.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/theme/SchemeTheme.kt index 8d137a1..8620e55 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/theme/SchemeTheme.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/theme/SchemeTheme.kt @@ -30,7 +30,6 @@ import kotlin.coroutines.CoroutineContext data object SchemeTheme { internal val executor = Executor() - internal val commonSchemeKey = MutableStateFlow(null) private val kache = InMemoryKache>( maxSize = 25L * 1024 * 1024 ) { @@ -49,10 +48,6 @@ data object SchemeTheme { kache.getOrPut(key) { fallback } }.getOrNull() - fun setCommon(key: Any?) { - commonSchemeKey.update { key } - } - @Composable fun create(key: Any?): Updater? { if (key == null) { @@ -199,19 +194,6 @@ fun SchemeTheme( } } -@Composable -fun CommonSchemeTheme(animate: Boolean = false, content: @Composable (SchemeTheme.Updater?) -> Unit) { - val key by SchemeTheme.commonSchemeKey.collectAsStateWithLifecycle() - - SchemeTheme( - key = key, - animate = animate, - defaultColor = null, - defaultOnColor = null, - content = content - ) -} - private fun Color.contrastAgainst(background: Color): Float { val fg = if (alpha < 1f) compositeOver(background) else this From 075c60a3ab0df8eba53a8d8034de039b27d45399 Mon Sep 17 00:00:00 2001 From: DatLag Date: Mon, 6 May 2024 21:58:44 +0200 Subject: [PATCH 03/23] prepare new trace repository --- .../datlag/aniflow/module/NetworkModule.kt | 13 ++- .../navigation/screen/home/HomeComponent.kt | 5 ++ .../ui/navigation/screen/home/HomeScreen.kt | 24 +++++ .../screen/home/HomeScreenComponent.kt | 16 ++++ .../datlag/aniflow/trace/TraceRepository.kt | 53 +++++++++++ .../datlag/aniflow/trace/TraceStateMachine.kt | 89 ------------------- 6 files changed, 104 insertions(+), 96 deletions(-) create mode 100644 trace/src/commonMain/kotlin/dev/datlag/aniflow/trace/TraceRepository.kt delete mode 100644 trace/src/commonMain/kotlin/dev/datlag/aniflow/trace/TraceStateMachine.kt diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/module/NetworkModule.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/module/NetworkModule.kt index 9ad4636..62efb3c 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/module/NetworkModule.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/module/NetworkModule.kt @@ -32,7 +32,7 @@ import dev.datlag.aniflow.model.safeFirstOrNull import dev.datlag.aniflow.other.UserHelper import dev.datlag.aniflow.settings.Settings import dev.datlag.aniflow.trace.Trace -import dev.datlag.aniflow.trace.TraceStateMachine +import dev.datlag.aniflow.trace.TraceRepository import dev.datlag.tooling.async.suspendCatching import io.github.aakira.napier.Napier import kotlinx.coroutines.flow.map @@ -113,12 +113,6 @@ data object NetworkModule { baseUrl("https://api.trace.moe/") }.create() } - bindProvider { - TraceStateMachine( - trace = instance(), - crashlytics = nullableFirebaseInstance()?.crashlytics - ) - } bindSingleton { val appSettings = instance() @@ -169,5 +163,10 @@ data object NetworkModule { fallbackClient = instance(Constants.AniList.FALLBACK_APOLLO_CLIENT) ) } + bindSingleton { + TraceRepository( + trace = instance(), + ) + } } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeComponent.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeComponent.kt index d3911ee..df11915 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeComponent.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeComponent.kt @@ -5,6 +5,7 @@ import dev.datlag.aniflow.anilist.TrendingRepository import dev.datlag.aniflow.anilist.model.Medium import dev.datlag.aniflow.anilist.state.CollectionState import dev.datlag.aniflow.anilist.type.MediaType +import dev.datlag.aniflow.trace.TraceRepository import dev.datlag.aniflow.ui.navigation.Component import kotlinx.coroutines.flow.Flow @@ -16,9 +17,13 @@ interface HomeComponent : Component { val popularNow: Flow val popularNext: Flow + val traceState: Flow + fun viewProfile() fun viewAnime() fun viewManga() fun details(medium: Medium) + fun trace(byteArray: ByteArray) + fun clearTrace() } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeScreen.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeScreen.kt index d1ce91e..2ef4cc1 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeScreen.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeScreen.kt @@ -1,5 +1,6 @@ package dev.datlag.aniflow.ui.navigation.screen.home +import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.LinearOutSlowInEasing import androidx.compose.animation.core.tween @@ -10,6 +11,8 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.CameraEnhance import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider @@ -31,6 +34,8 @@ import dev.datlag.aniflow.anilist.type.MediaType import dev.datlag.aniflow.common.LocalPadding import dev.datlag.aniflow.common.isScrollingUp import dev.datlag.aniflow.other.StateSaver +import dev.datlag.aniflow.other.rememberImagePickerState +import dev.datlag.aniflow.trace.TraceRepository import dev.datlag.aniflow.ui.navigation.screen.component.CollapsingToolbar import dev.datlag.aniflow.ui.navigation.screen.component.HidingNavigationBar import dev.datlag.aniflow.ui.navigation.screen.home.component.AllLoadingView @@ -46,6 +51,9 @@ fun HomeScreen(component: HomeComponent) { state = appBarState ) val listState = rememberLazyListState() + val imagePicker = rememberImagePickerState { + it?.let(component::trace) + } Scaffold( modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), @@ -60,7 +68,23 @@ fun HomeScreen(component: HomeComponent) { ) }, floatingActionButton = { + val traceState by component.traceState.collectAsStateWithLifecycle(TraceRepository.State.None) + ExtendedFloatingActionButton( + onClick = { + imagePicker.launch() + }, + expanded = listState.isScrollingUp(), + icon = { + Icon( + imageVector = Icons.Filled.CameraEnhance, + contentDescription = null + ) + }, + text = { + Text(text = "Scan") + } + ) }, bottomBar = { HidingNavigationBar( diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeScreenComponent.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeScreenComponent.kt index b418f15..52b3121 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeScreenComponent.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeScreenComponent.kt @@ -17,6 +17,7 @@ import dev.datlag.aniflow.common.onRender import dev.datlag.aniflow.model.coroutines.Executor import dev.datlag.aniflow.other.StateSaver import dev.datlag.aniflow.settings.Settings +import dev.datlag.aniflow.trace.TraceRepository import dev.datlag.tooling.decompose.ioScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted @@ -79,6 +80,13 @@ class HomeScreenComponent( initialValue = CollectionState.None ) + private val traceRepository by instance() + override val traceState: Flow = traceRepository.response + + init { + traceRepository.clear() + } + @Composable override fun render() { val haze = remember { HazeState() } @@ -117,4 +125,12 @@ class HomeScreenComponent( override fun details(medium: Medium) { onMediumDetails(medium) } + + override fun trace(byteArray: ByteArray) { + traceRepository.search(byteArray) + } + + override fun clearTrace() { + traceRepository.clear() + } } \ No newline at end of file diff --git a/trace/src/commonMain/kotlin/dev/datlag/aniflow/trace/TraceRepository.kt b/trace/src/commonMain/kotlin/dev/datlag/aniflow/trace/TraceRepository.kt new file mode 100644 index 0000000..5ff5893 --- /dev/null +++ b/trace/src/commonMain/kotlin/dev/datlag/aniflow/trace/TraceRepository.kt @@ -0,0 +1,53 @@ +package dev.datlag.aniflow.trace + +import dev.datlag.aniflow.model.CatchResult +import dev.datlag.aniflow.trace.model.SearchResponse +import kotlinx.coroutines.flow.* +import kotlinx.serialization.Serializable + +class TraceRepository( + private val trace: Trace +) { + + private val byteArray = MutableStateFlow(null) + val response: Flow = byteArray.transform { + return@transform if (it == null || it.isEmpty()) { + emit(State.None) + } else { + emit( + State.fromResponse( + CatchResult.repeat(2) { + trace.search(it) + }.asNullableSuccess() + ) + ) + } + } + + fun clear() = byteArray.update { null } + fun search(array: ByteArray) = byteArray.update { array } + + @Serializable + sealed interface State { + @Serializable + data object None : State + + @Serializable + data class Success( + val response: SearchResponse, + ) : State + + @Serializable + data object Error : State + + companion object { + fun fromResponse(response: SearchResponse?): State { + return if (response == null || response.isError) { + Error + } else { + Success(response) + } + } + } + } +} \ No newline at end of file diff --git a/trace/src/commonMain/kotlin/dev/datlag/aniflow/trace/TraceStateMachine.kt b/trace/src/commonMain/kotlin/dev/datlag/aniflow/trace/TraceStateMachine.kt deleted file mode 100644 index 09e8c5b..0000000 --- a/trace/src/commonMain/kotlin/dev/datlag/aniflow/trace/TraceStateMachine.kt +++ /dev/null @@ -1,89 +0,0 @@ -package dev.datlag.aniflow.trace - -import com.freeletics.flowredux.dsl.FlowReduxStateMachine -import dev.datlag.aniflow.firebase.FirebaseFactory -import dev.datlag.aniflow.model.CatchResult -import dev.datlag.aniflow.trace.model.SearchResponse -import io.ktor.client.request.forms.* -import io.ktor.utils.io.* -import kotlinx.coroutines.ExperimentalCoroutinesApi - -@OptIn(ExperimentalCoroutinesApi::class) -class TraceStateMachine( - private val trace: Trace, - private val crashlytics: FirebaseFactory.Crashlytics? -) : FlowReduxStateMachine( - initialState = State.Waiting -) { - - init { - spec { - inState { - on { action, state -> - state.override { State.Loading(action.image) } - } - } - inState { - onEnter { state -> - val response = CatchResult.repeat(2) { - val result = trace.search(state.snapshot.image) - - if (result.isError) { - throw IllegalStateException("Result is Error") - } else { - result - } - }.mapSuccess { - State.Success(it) - } - - state.override { - response.asSuccess { - crashlytics?.log(it) - - State.Error - } - } - } - } - inState { - on { action, state -> - state.override { State.Loading(action.image) } - } - } - inState { - on { action, state -> - state.override { State.Loading(action.image) } - } - } - } - } - - sealed interface State { - - val isWaiting: Boolean - get() = this is Waiting - - val isLoading: Boolean - get() = this is Loading - - val isSuccess: Boolean - get() = this is Success - - data object Waiting : State - - class Loading( - internal val image: ByteArray - ) : State - - data class Success( - val response: SearchResponse - ) : State - - data object Error : State - } - - sealed interface Action { - class Load(internal val image: ByteArray) : Action - } -} \ No newline at end of file From 6a72fa17b5ae903629909f33dba9ad47c217d194 Mon Sep 17 00:00:00 2001 From: DatLag Date: Tue, 7 May 2024 20:52:36 +0200 Subject: [PATCH 04/23] prepare wallpaper screen --- .../kotlin/dev/datlag/aniflow/App.kt | 9 + .../aniflow/ui/navigation/RootComponent.kt | 65 +++-- .../aniflow/ui/navigation/RootConfig.kt | 6 + .../screen/component/HidingNavigationBar.kt | 103 ++++++- .../screen/favorites/FavoritesComponent.kt | 9 + .../screen/favorites/FavoritesScreen.kt | 23 ++ .../favorites/FavoritesScreenComponent.kt | 39 +++ .../navigation/screen/home/HomeComponent.kt | 2 + .../ui/navigation/screen/home/HomeScreen.kt | 71 ++++- .../screen/home/HomeScreenComponent.kt | 12 +- .../screen/initial/InitialComponent.kt | 29 -- .../screen/initial/InitialScreen.kt | 41 --- .../screen/initial/InitialScreenComponent.kt | 128 --------- .../ui/navigation/screen/initial/View.kt | 12 - .../screen/initial/component/CompactScreen.kt | 140 ---------- .../initial/component/ExpandedScreen.kt | 110 -------- .../screen/initial/component/MediumScreen.kt | 107 -------- .../screen/initial/component/NavIcon.kt | 20 -- .../initial/favorites/FavoritesComponent.kt | 6 - .../initial/favorites/FavoritesScreen.kt | 8 - .../favorites/FavoritesScreenComponent.kt | 19 -- .../screen/initial/model/FABConfig.kt | 17 -- .../screen/settings/SettingsScreen.kt | 253 +++++++++--------- .../screen/wallpaper/WallpaperComponent.kt | 9 + .../screen/wallpaper/WallpaperScreen.kt | 23 ++ .../wallpaper/WallpaperScreenComponent.kt | 39 +++ 26 files changed, 486 insertions(+), 814 deletions(-) create mode 100644 composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/favorites/FavoritesComponent.kt create mode 100644 composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/favorites/FavoritesScreen.kt create mode 100644 composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/favorites/FavoritesScreenComponent.kt delete mode 100644 composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/InitialComponent.kt delete mode 100644 composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/InitialScreen.kt delete mode 100644 composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/InitialScreenComponent.kt delete mode 100644 composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/View.kt delete mode 100644 composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/component/CompactScreen.kt delete mode 100644 composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/component/ExpandedScreen.kt delete mode 100644 composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/component/MediumScreen.kt delete mode 100644 composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/component/NavIcon.kt delete mode 100644 composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/favorites/FavoritesComponent.kt delete mode 100644 composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/favorites/FavoritesScreen.kt delete mode 100644 composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/favorites/FavoritesScreenComponent.kt delete mode 100644 composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/model/FABConfig.kt create mode 100644 composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/wallpaper/WallpaperComponent.kt create mode 100644 composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/wallpaper/WallpaperScreen.kt create mode 100644 composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/wallpaper/WallpaperScreenComponent.kt diff --git a/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/App.kt b/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/App.kt index 21a429b..378a1cc 100644 --- a/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/App.kt +++ b/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/App.kt @@ -1,6 +1,7 @@ package dev.datlag.aniflow import android.content.Context +import android.os.StrictMode import androidx.multidex.MultiDexApplication import coil3.ImageLoader import coil3.SingletonImageLoader @@ -27,6 +28,14 @@ class App : MultiDexApplication(), DIAware { super.onCreate() if (BuildConfig.DEBUG) { + StrictMode.setThreadPolicy( + StrictMode.ThreadPolicy.Builder() + .detectAll() + .permitDiskReads() + .penaltyLog() + .penaltyDialog() + .build() + ) Napier.base(DebugAntilog()) } StateSaver.sekretLibraryLoaded = NativeLoader.loadLibrary("sekret") 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 9f176e1..e3f5566 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 @@ -8,6 +8,7 @@ import androidx.compose.ui.layout.layout 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.fade 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 @@ -16,11 +17,12 @@ import com.arkivanov.decompose.router.stack.* import dev.datlag.aniflow.common.onRender import dev.datlag.aniflow.model.ifValueOrNull import dev.datlag.aniflow.other.UserHelper +import dev.datlag.aniflow.ui.navigation.screen.favorites.FavoritesScreenComponent import dev.datlag.aniflow.ui.navigation.screen.home.HomeScreenComponent -import dev.datlag.aniflow.ui.navigation.screen.initial.InitialScreenComponent import dev.datlag.aniflow.ui.navigation.screen.medium.MediumScreenComponent import dev.datlag.aniflow.ui.navigation.screen.settings.SettingsScreen import dev.datlag.aniflow.ui.navigation.screen.settings.SettingsScreenComponent +import dev.datlag.aniflow.ui.navigation.screen.wallpaper.WallpaperScreenComponent import io.github.aakira.napier.Napier import org.kodein.di.DI import org.kodein.di.instance @@ -52,6 +54,12 @@ class RootComponent( }, onProfile = { navigation.push(RootConfig.Settings) + }, + onWallpaper = { + navigation.replaceCurrent(RootConfig.Wallpaper) + }, + onFavorites = { + navigation.replaceCurrent(RootConfig.Favorites) } ) is RootConfig.Details -> MediumScreenComponent( @@ -64,6 +72,26 @@ class RootComponent( componentContext = componentContext, di = di ) + is RootConfig.Favorites -> FavoritesScreenComponent( + componentContext = componentContext, + di = di, + onWallpaper = { + navigation.replaceCurrent(RootConfig.Wallpaper) + }, + onHome = { + navigation.replaceCurrent(RootConfig.Home) + } + ) + is RootConfig.Wallpaper -> WallpaperScreenComponent( + componentContext = componentContext, + di = di, + onHome = { + navigation.replaceCurrent(RootConfig.Home) + }, + onFavorites = { + navigation.replaceCurrent(RootConfig.Favorites) + } + ) } } @@ -75,40 +103,7 @@ class RootComponent( stack = stack, animation = predictiveBackAnimation( backHandler = this.backHandler, - fallbackAnimation = stackAnimation { child -> - when (child.configuration) { - is RootConfig.Settings -> stackAnimator(tween()) { factor, _, content -> - content( - Modifier.layout { measurable, constraints -> - val placeable = measurable.measure(constraints) - - layout(placeable.width, placeable.height) { - placeable.placeRelative(y = -(placeable.height.toFloat() * factor).toInt(), x = 0) - } - } - ) - } - is RootConfig.Home -> { - val current = stack.value.active - - when (current.configuration) { - is RootConfig.Settings -> stackAnimator(tween()) { factor, _, content -> - content( - Modifier.layout { measurable, constraints -> - val placeable = measurable.measure(constraints) - - layout(placeable.width, placeable.height) { - placeable.placeRelative(y = -(placeable.height.toFloat() * factor).toInt(), x = 0) - } - } - ) - } - else -> slide() - } - } - else -> slide() - } - }, + fallbackAnimation = stackAnimation(fade()), onBack = { navigation.pop() } diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/RootConfig.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/RootConfig.kt index 78858a3..38a9df0 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/RootConfig.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/RootConfig.kt @@ -16,4 +16,10 @@ sealed class RootConfig { @Serializable data object Settings : RootConfig() + + @Serializable + data object Favorites : RootConfig() + + @Serializable + data object Wallpaper : RootConfig() } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/component/HidingNavigationBar.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/component/HidingNavigationBar.kt index 8c2daf2..764d728 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/component/HidingNavigationBar.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/component/HidingNavigationBar.kt @@ -9,10 +9,17 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.FavoriteBorder import androidx.compose.material.icons.filled.Home +import androidx.compose.material.icons.filled.Wallpaper +import androidx.compose.material.icons.outlined.Home +import androidx.compose.material.icons.rounded.Favorite +import androidx.compose.material.icons.rounded.FavoriteBorder +import androidx.compose.material.icons.rounded.Home +import androidx.compose.material.icons.rounded.Wallpaper import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.dp import dev.chrisbanes.haze.hazeChild @@ -21,12 +28,19 @@ import dev.chrisbanes.haze.materials.HazeMaterials import dev.datlag.aniflow.LocalHaze import dev.datlag.aniflow.SharedRes import dev.datlag.aniflow.common.isScrollingUp +import dev.datlag.aniflow.ui.navigation.RootConfig import dev.icerock.moko.resources.compose.stringResource +import kotlinx.serialization.Serializable +import kotlinx.serialization.Transient @OptIn(ExperimentalHazeMaterialsApi::class) @Composable fun HidingNavigationBar( visible: Boolean, + selected: NavigationBarState, + onWallpaper: () -> Unit, + onHome: () -> Unit, + onFavorites: () -> Unit ) { val density = LocalDensity.current @@ -58,11 +72,32 @@ fun HidingNavigationBar( contentColor = MaterialTheme.colorScheme.contentColorFor(NavigationBarDefaults.containerColor) ) { NavigationBarItem( - onClick = { }, - selected = true, + onClick = { + if (selected !is NavigationBarState.Wallpaper) { + onWallpaper() + } + }, + selected = selected is NavigationBarState.Wallpaper, + icon = { + Icon( + imageVector = selected.wallpaperIcon, + contentDescription = null + ) + }, + label = { + Text(text = "Wallpapers") + } + ) + NavigationBarItem( + onClick = { + if (selected !is NavigationBarState.Home) { + onHome() + } + }, + selected = selected is NavigationBarState.Home, icon = { Icon( - imageVector = Icons.Filled.Home, + imageVector = selected.homeIcon, contentDescription = null ) }, @@ -71,11 +106,15 @@ fun HidingNavigationBar( } ) NavigationBarItem( - onClick = { }, - selected = false, + onClick = { + if (selected !is NavigationBarState.Favorite) { + onFavorites() + } + }, + selected = selected is NavigationBarState.Favorite, icon = { Icon( - imageVector = Icons.Default.FavoriteBorder, + imageVector = selected.favoriteIcon, contentDescription = null ) }, @@ -85,4 +124,56 @@ fun HidingNavigationBar( ) } } +} + +@Serializable +sealed interface NavigationBarState { + @Transient + val unselectedIcon: ImageVector + + @Transient + val selectedIcon: ImageVector + get() = unselectedIcon + + val wallpaperIcon: ImageVector + get() = when (this) { + is Wallpaper -> selectedIcon + else -> Wallpaper.unselectedIcon + } + + val homeIcon: ImageVector + get() = when (this) { + is Home -> selectedIcon + else -> Home.unselectedIcon + } + + val favoriteIcon: ImageVector + get() = when (this) { + is Favorite -> selectedIcon + else -> Favorite.unselectedIcon + } + + @Serializable + data object Wallpaper : NavigationBarState { + override val unselectedIcon: ImageVector + get() = Icons.Rounded.Wallpaper + } + + @Serializable + data object Home : NavigationBarState { + override val unselectedIcon: ImageVector + get() = Icons.Outlined.Home + + override val selectedIcon: ImageVector + get() = Icons.Rounded.Home + } + + @Serializable + data object Favorite : NavigationBarState { + override val unselectedIcon: ImageVector + get() = Icons.Rounded.FavoriteBorder + + override val selectedIcon: ImageVector + get() = Icons.Rounded.Favorite + } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/favorites/FavoritesComponent.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/favorites/FavoritesComponent.kt new file mode 100644 index 0000000..82f11f0 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/favorites/FavoritesComponent.kt @@ -0,0 +1,9 @@ +package dev.datlag.aniflow.ui.navigation.screen.favorites + +import dev.datlag.aniflow.ui.navigation.Component + +interface FavoritesComponent : Component { + + fun viewWallpaper() + fun viewHome() +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/favorites/FavoritesScreen.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/favorites/FavoritesScreen.kt new file mode 100644 index 0000000..302de11 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/favorites/FavoritesScreen.kt @@ -0,0 +1,23 @@ +package dev.datlag.aniflow.ui.navigation.screen.favorites + +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import dev.datlag.aniflow.ui.navigation.screen.component.HidingNavigationBar +import dev.datlag.aniflow.ui.navigation.screen.component.NavigationBarState + +@Composable +fun FavoritesScreen(component: FavoritesComponent) { + Scaffold( + bottomBar = { + HidingNavigationBar( + visible = true, + selected = NavigationBarState.Favorite, + onWallpaper = component::viewWallpaper, + onHome = component::viewHome, + onFavorites = { } + ) + } + ) { + + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/favorites/FavoritesScreenComponent.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/favorites/FavoritesScreenComponent.kt new file mode 100644 index 0000000..253b917 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/favorites/FavoritesScreenComponent.kt @@ -0,0 +1,39 @@ +package dev.datlag.aniflow.ui.navigation.screen.favorites + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.remember +import com.arkivanov.decompose.ComponentContext +import dev.chrisbanes.haze.HazeState +import dev.datlag.aniflow.LocalHaze +import dev.datlag.aniflow.common.onRender +import org.kodein.di.DI + +class FavoritesScreenComponent( + componentContext: ComponentContext, + override val di: DI, + private val onWallpaper: () -> Unit, + private val onHome: () -> Unit, +) : FavoritesComponent, ComponentContext by componentContext { + + @Composable + override fun render() { + val haze = remember { HazeState() } + + CompositionLocalProvider( + LocalHaze provides haze + ) { + onRender { + FavoritesScreen(this) + } + } + } + + override fun viewWallpaper() { + onWallpaper() + } + + override fun viewHome() { + onHome() + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeComponent.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeComponent.kt index df11915..0069afc 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeComponent.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeComponent.kt @@ -22,6 +22,8 @@ interface HomeComponent : Component { fun viewProfile() fun viewAnime() fun viewManga() + fun viewWallpaper() + fun viewFavorites() fun details(medium: Medium) fun trace(byteArray: ByteArray) diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeScreen.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeScreen.kt index 2ef4cc1..b504b1c 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeScreen.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeScreen.kt @@ -6,24 +6,28 @@ import androidx.compose.animation.core.LinearOutSlowInEasing import androidx.compose.animation.core.tween import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideOutVertically -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.CameraEnhance import androidx.compose.material3.* -import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import com.maxkeppeker.sheets.core.models.base.Header +import com.maxkeppeker.sheets.core.models.base.rememberUseCaseState +import com.maxkeppeler.sheets.option.OptionDialog +import com.maxkeppeler.sheets.option.models.DisplayMode +import com.maxkeppeler.sheets.option.models.Option +import com.maxkeppeler.sheets.option.models.OptionConfig +import com.maxkeppeler.sheets.option.models.OptionSelection import dev.chrisbanes.haze.haze import dev.chrisbanes.haze.hazeChild import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi @@ -31,17 +35,18 @@ import dev.chrisbanes.haze.materials.HazeMaterials import dev.datlag.aniflow.LocalHaze import dev.datlag.aniflow.LocalPaddingValues import dev.datlag.aniflow.anilist.type.MediaType -import dev.datlag.aniflow.common.LocalPadding -import dev.datlag.aniflow.common.isScrollingUp +import dev.datlag.aniflow.common.* import dev.datlag.aniflow.other.StateSaver import dev.datlag.aniflow.other.rememberImagePickerState import dev.datlag.aniflow.trace.TraceRepository import dev.datlag.aniflow.ui.navigation.screen.component.CollapsingToolbar import dev.datlag.aniflow.ui.navigation.screen.component.HidingNavigationBar +import dev.datlag.aniflow.ui.navigation.screen.component.NavigationBarState import dev.datlag.aniflow.ui.navigation.screen.home.component.AllLoadingView import dev.datlag.aniflow.ui.navigation.screen.home.component.DefaultOverview import dev.datlag.aniflow.ui.navigation.screen.home.component.ScheduleOverview import dev.datlag.tooling.decompose.lifecycle.collectAsStateWithLifecycle +import io.github.aakira.napier.Napier @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -69,6 +74,48 @@ fun HomeScreen(component: HomeComponent) { }, floatingActionButton = { val traceState by component.traceState.collectAsStateWithLifecycle(TraceRepository.State.None) + val results = remember(traceState) { + (traceState as? TraceRepository.State.Success)?.response?.combinedResults.orEmpty().toList() + } + + val dialogState = rememberUseCaseState( + visible = results.isNotEmpty(), + onCloseRequest = { component.clearTrace() }, + onDismissRequest = { component.clearTrace() }, + onFinishedRequest = { component.clearTrace() } + ) + + LaunchedEffect(results) { + if (results.isNotEmpty()) { + dialogState.show() + } + } + + OptionDialog( + state = dialogState, + config = OptionConfig( + mode = DisplayMode.LIST + ), + header = Header.Custom { padding -> + Text( + modifier = Modifier.padding(padding.merge(PaddingValues(16.dp))).fillMaxWidth(), + text = "Matching Anime", + textAlign = TextAlign.Center, + fontWeight = FontWeight.SemiBold, + style = MaterialTheme.typography.titleLarge + ) + }, + selection = OptionSelection.Single( + options = results.map { + Option( + titleText = it.aniList.asMedium().preferred(null) + ) + }, + onSelectOption = { option, _ -> + component.details(results[option].aniList.asMedium()) + } + ) + ) ExtendedFloatingActionButton( onClick = { @@ -88,7 +135,11 @@ fun HomeScreen(component: HomeComponent) { }, bottomBar = { HidingNavigationBar( - visible = listState.isScrollingUp() && listState.canScrollForward + visible = listState.isScrollingUp() && listState.canScrollForward, + selected = NavigationBarState.Home, + onWallpaper = component::viewWallpaper, + onHome = { }, + onFavorites = component::viewFavorites ) } ) { diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeScreenComponent.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeScreenComponent.kt index 52b3121..74886e3 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeScreenComponent.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeScreenComponent.kt @@ -30,7 +30,9 @@ class HomeScreenComponent( componentContext: ComponentContext, override val di: DI, private val onMediumDetails: (Medium) -> Unit, - private val onProfile: () -> Unit + private val onProfile: () -> Unit, + private val onWallpaper: () -> Unit, + private val onFavorites: () -> Unit ) : HomeComponent, ComponentContext by componentContext { private val appSettings by instance() @@ -122,6 +124,14 @@ class HomeScreenComponent( } } + override fun viewWallpaper() { + onWallpaper() + } + + override fun viewFavorites() { + onFavorites() + } + override fun details(medium: Medium) { onMediumDetails(medium) } diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/InitialComponent.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/InitialComponent.kt deleted file mode 100644 index 62a22bd..0000000 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/InitialComponent.kt +++ /dev/null @@ -1,29 +0,0 @@ -package dev.datlag.aniflow.ui.navigation.screen.initial - -import androidx.compose.ui.graphics.vector.ImageVector -import com.arkivanov.decompose.ExperimentalDecomposeApi -import com.arkivanov.decompose.router.pages.ChildPages -import com.arkivanov.decompose.value.Value -import dev.datlag.aniflow.anilist.type.MediaType -import dev.datlag.aniflow.ui.navigation.Component -import dev.icerock.moko.resources.StringResource -import kotlinx.coroutines.flow.Flow - -interface InitialComponent : Component { - val viewing: Flow - val pagerItems: List - val selectedPage: Value - - @OptIn(ExperimentalDecomposeApi::class) - val pages: Value> - - fun selectPage(index: Int) - fun viewProfile() - fun viewAnime() - fun viewManga() - - data class PagerItem( - val label: StringResource, - val icon: ImageVector - ) -} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/InitialScreen.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/InitialScreen.kt deleted file mode 100644 index 35ce9fa..0000000 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/InitialScreen.kt +++ /dev/null @@ -1,41 +0,0 @@ -package dev.datlag.aniflow.ui.navigation.screen.initial - -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi -import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass -import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass -import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import com.arkivanov.decompose.ExperimentalDecomposeApi -import com.arkivanov.decompose.extensions.compose.pages.Pages -import dev.chrisbanes.haze.HazeState -import dev.datlag.aniflow.LocalHaze -import dev.datlag.aniflow.ui.navigation.screen.initial.component.CompactScreen -import dev.datlag.aniflow.ui.navigation.screen.initial.component.ExpandedScreen -import dev.datlag.aniflow.ui.navigation.screen.initial.component.MediumScreen - -@OptIn(ExperimentalFoundationApi::class, ExperimentalDecomposeApi::class, - ExperimentalMaterial3WindowSizeClassApi::class -) -@Composable -fun InitialScreen(component: InitialComponent) { - val haze = remember { HazeState() } - - CompositionLocalProvider( - LocalHaze provides haze - ) { - Box( - modifier = Modifier.fillMaxSize() - ) { - when (calculateWindowSizeClass().widthSizeClass) { - WindowWidthSizeClass.Compact -> CompactScreen(component) - WindowWidthSizeClass.Medium -> MediumScreen(component) - WindowWidthSizeClass.Expanded -> ExpandedScreen(component) - } - } - } -} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/InitialScreenComponent.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/InitialScreenComponent.kt deleted file mode 100644 index 74c8091..0000000 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/InitialScreenComponent.kt +++ /dev/null @@ -1,128 +0,0 @@ -package dev.datlag.aniflow.ui.navigation.screen.initial - -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.AccountCircle -import androidx.compose.material.icons.filled.Favorite -import androidx.compose.material.icons.filled.Home -import androidx.compose.material.icons.filled.Settings -import androidx.compose.runtime.Composable -import com.arkivanov.decompose.ComponentContext -import com.arkivanov.decompose.ExperimentalDecomposeApi -import com.arkivanov.decompose.router.pages.* -import com.arkivanov.decompose.value.Value -import com.arkivanov.decompose.value.operator.map -import dev.datlag.aniflow.SharedRes -import dev.datlag.aniflow.anilist.model.Medium -import dev.datlag.aniflow.anilist.type.MediaType -import dev.datlag.aniflow.common.onRender -import dev.datlag.aniflow.model.coroutines.Executor -import dev.datlag.aniflow.other.StateSaver -import dev.datlag.aniflow.settings.Settings -import dev.datlag.aniflow.ui.navigation.Component -import dev.datlag.aniflow.ui.navigation.ContentHolderComponent -import dev.datlag.aniflow.ui.navigation.screen.initial.favorites.FavoritesScreenComponent -import dev.datlag.aniflow.ui.navigation.screen.settings.SettingsScreenComponent -import kotlinx.coroutines.flow.map -import org.kodein.di.DI -import org.kodein.di.instance - -class InitialScreenComponent( - componentContext: ComponentContext, - override val di: DI, - private val onMediumDetails: (Medium) -> Unit, - private val onProfile: () -> Unit -) : InitialComponent, ComponentContext by componentContext { - - private val appSettings by di.instance() - - override val pagerItems: List = listOf( - InitialComponent.PagerItem( - label = SharedRes.strings.home, - icon = Icons.Default.Home - ), - InitialComponent.PagerItem( - label = SharedRes.strings.favorites, - icon = Icons.Filled.Favorite - ) - ) - - @OptIn(ExperimentalDecomposeApi::class) - private val pagesNavigation = PagesNavigation() - - @OptIn(ExperimentalDecomposeApi::class) - override val pages: Value> = childPages( - source = pagesNavigation, - serializer = View.serializer(), - initialPages = { - Pages( - items = listOf( - View.Home, - View.Favorites - ), - selectedIndex = 0 - ) - }, - childFactory = ::createChild - ) - - @OptIn(ExperimentalDecomposeApi::class) - override val selectedPage: Value = pages.map { it.selectedIndex } - - private val viewTypeExecutor = Executor() - - override val viewing = appSettings.viewManga.map { - if (it) { - MediaType.MANGA - } else { - MediaType.ANIME - } - } - - @Composable - override fun render() { - onRender { - InitialScreen(this) - } - } - - private fun createChild( - view: View, - componentContext: ComponentContext - ): Component { - return FavoritesScreenComponent( - componentContext = componentContext, - di = di - ) - } - - @OptIn(ExperimentalDecomposeApi::class) - override fun selectPage(index: Int) { - pagesNavigation.select(index = index) { new, old -> - if (new.items[new.selectedIndex] == old.items[old.selectedIndex]) { - (pages.value.items[pages.value.selectedIndex].instance as? ContentHolderComponent)?.dismissContent() - } - } - } - - override fun viewProfile() { - onProfile() - } - - override fun viewAnime() { - StateSaver.Home.updateAllLoading() - launchIO { - viewTypeExecutor.enqueue { - appSettings.setViewManga(false) - } - } - } - - override fun viewManga() { - StateSaver.Home.updateAllLoading() - launchIO { - viewTypeExecutor.enqueue { - appSettings.setViewManga(true) - } - } - } -} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/View.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/View.kt deleted file mode 100644 index 4ac5437..0000000 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/View.kt +++ /dev/null @@ -1,12 +0,0 @@ -package dev.datlag.aniflow.ui.navigation.screen.initial - -import kotlinx.serialization.Serializable - -@Serializable -sealed class View { - @Serializable - data object Home : View() - - @Serializable - data object Favorites : View() -} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/component/CompactScreen.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/component/CompactScreen.kt deleted file mode 100644 index a30a800..0000000 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/component/CompactScreen.kt +++ /dev/null @@ -1,140 +0,0 @@ -package dev.datlag.aniflow.ui.navigation.screen.initial.component - -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.pager.HorizontalPager -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.CameraEnhance -import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.input.nestedscroll.nestedScroll -import com.arkivanov.decompose.ExperimentalDecomposeApi -import com.arkivanov.decompose.extensions.compose.pages.Pages -import com.arkivanov.decompose.extensions.compose.subscribeAsState -import dev.chrisbanes.haze.hazeChild -import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi -import dev.chrisbanes.haze.materials.HazeMaterials -import dev.datlag.aniflow.LocalHaze -import dev.datlag.aniflow.LocalPaddingValues -import dev.datlag.aniflow.common.isScrollingUp -import dev.datlag.aniflow.ui.navigation.screen.initial.InitialComponent -import dev.datlag.aniflow.ui.navigation.screen.component.CollapsingToolbar -import dev.datlag.aniflow.ui.navigation.screen.initial.model.FABConfig -import dev.icerock.moko.resources.compose.stringResource - -@OptIn(ExperimentalFoundationApi::class, ExperimentalDecomposeApi::class, ExperimentalHazeMaterialsApi::class, - ExperimentalMaterial3Api::class -) -@Composable -fun CompactScreen(component: InitialComponent) { - val appBarState = rememberTopAppBarState() - val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior( - state = appBarState - ) - - Scaffold( - modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), - topBar = { - CollapsingToolbar( - state = appBarState, - scrollBehavior = scrollBehavior, - viewTypeFlow = component.viewing, - onProfileClick = { - component.viewProfile() - }, - onAnimeClick = { - component.viewAnime() - }, - onMangaClick = { - component.viewManga() - } - ) - }, - bottomBar = { - val selectedPage by component.selectedPage.subscribeAsState() - - NavigationBar( - modifier = Modifier.hazeChild( - state = LocalHaze.current, - style = HazeMaterials.thin(NavigationBarDefaults.containerColor) - ).fillMaxWidth(), - containerColor = Color.Transparent, - contentColor = MaterialTheme.colorScheme.contentColorFor(NavigationBarDefaults.containerColor) - ) { - component.pagerItems.forEachIndexed { index, pagerItem -> - NavigationBarItem( - selected = selectedPage == index, - icon = { - NavIcon(pagerItem) - }, - onClick = { - component.selectPage(index) - }, - label = { - Text(text = stringResource(pagerItem.label)) - }, - alwaysShowLabel = true - ) - } - } - }, - floatingActionButton = { - val state by FABConfig.state - - when (val current = state) { - is FABConfig.Scan -> { - if (!current.loading) { - ExtendedFloatingActionButton( - onClick = current.onClick, - icon = { - Icon( - imageVector = Icons.Filled.CameraEnhance, - contentDescription = null - ) - }, - text = { - Text( - text = "Scan" - ) - }, - expanded = current.listState.isScrollingUp(), - ) - } - } - else -> { } - } - } - ) { - CompositionLocalProvider( - LocalPaddingValues provides it - ) { - Box( - modifier = Modifier.fillMaxSize() - ) { - val selectedPage by component.selectedPage.subscribeAsState() - - Pages( - pages = component.pages, - onPageSelected = { index -> - if (selectedPage != index) { - component.selectPage(index) - } - }, - pager = { modifier, pagerState, key, pageContent -> - HorizontalPager( - modifier = modifier, - state = pagerState, - key = key, - pageContent = pageContent, - userScrollEnabled = false - ) - } - ) { _, page -> - page.render() - } - } - } - } -} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/component/ExpandedScreen.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/component/ExpandedScreen.kt deleted file mode 100644 index 621683c..0000000 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/component/ExpandedScreen.kt +++ /dev/null @@ -1,110 +0,0 @@ -package dev.datlag.aniflow.ui.navigation.screen.initial.component - -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.padding -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.CameraEnhance -import androidx.compose.material3.* -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.unit.dp -import com.arkivanov.decompose.ExperimentalDecomposeApi -import com.arkivanov.decompose.extensions.compose.subscribeAsState -import dev.datlag.aniflow.common.isScrollingUp -import dev.datlag.aniflow.ui.custom.ExpandedPages -import dev.datlag.aniflow.ui.navigation.screen.initial.InitialComponent -import dev.datlag.aniflow.ui.navigation.screen.component.CollapsingToolbar -import dev.datlag.aniflow.ui.navigation.screen.initial.model.FABConfig -import dev.datlag.tooling.compose.EndCornerShape -import dev.icerock.moko.resources.compose.stringResource - -@OptIn(ExperimentalDecomposeApi::class, ExperimentalMaterial3Api::class) -@Composable -fun ExpandedScreen(component: InitialComponent) { - val appBarState = rememberTopAppBarState() - val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior( - state = appBarState - ) - - Scaffold( - modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), - topBar = { - CollapsingToolbar( - state = appBarState, - scrollBehavior = scrollBehavior, - viewTypeFlow = component.viewing, - onProfileClick = { - component.viewProfile() - }, - onAnimeClick = { - component.viewAnime() - }, - onMangaClick = { - component.viewManga() - } - ) - }, - floatingActionButton = { - val state by FABConfig.state - - when (val current = state) { - is FABConfig.Scan -> { - if (!current.loading) { - ExtendedFloatingActionButton( - onClick = current.onClick, - icon = { - Icon( - imageVector = Icons.Filled.CameraEnhance, - contentDescription = null - ) - }, - text = { - Text( - text = "Scan" - ) - }, - expanded = current.listState.isScrollingUp(), - ) - } - } - else -> { } - } - } - ) { - PermanentNavigationDrawer( - modifier = Modifier.padding(it), - drawerContent = { - PermanentDrawerSheet( - drawerShape = EndCornerShape(otherCorner = 0.dp) - ) { - val selectedPage by component.selectedPage.subscribeAsState() - - Spacer(modifier = Modifier.weight(1F)) - component.pagerItems.forEachIndexed { index, pagerItem -> - NavigationDrawerItem( - selected = selectedPage == index, - icon = { - NavIcon(pagerItem) - }, - onClick = { - component.selectPage(index) - }, - label = { - Text(text = stringResource(pagerItem.label)) - } - ) - } - Spacer(modifier = Modifier.weight(1F)) - } - } - ) { - ExpandedPages( - pages = component.pages - ) { _, page -> - page.render() - } - } - } -} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/component/MediumScreen.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/component/MediumScreen.kt deleted file mode 100644 index db72d58..0000000 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/component/MediumScreen.kt +++ /dev/null @@ -1,107 +0,0 @@ -package dev.datlag.aniflow.ui.navigation.screen.initial.component - -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.padding -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.CameraEnhance -import androidx.compose.material3.* -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.input.nestedscroll.nestedScroll -import com.arkivanov.decompose.ExperimentalDecomposeApi -import com.arkivanov.decompose.extensions.compose.subscribeAsState -import dev.datlag.aniflow.common.isScrollingUp -import dev.datlag.aniflow.ui.custom.ExpandedPages -import dev.datlag.aniflow.ui.navigation.screen.initial.InitialComponent -import dev.datlag.aniflow.ui.navigation.screen.component.CollapsingToolbar -import dev.datlag.aniflow.ui.navigation.screen.initial.model.FABConfig -import dev.icerock.moko.resources.compose.stringResource - -@OptIn(ExperimentalDecomposeApi::class, ExperimentalMaterial3Api::class) -@Composable -fun MediumScreen(component: InitialComponent) { - val appBarState = rememberTopAppBarState() - val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior( - state = appBarState - ) - - Scaffold( - modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), - topBar = { - CollapsingToolbar( - state = appBarState, - scrollBehavior = scrollBehavior, - viewTypeFlow = component.viewing, - onProfileClick = { - component.viewProfile() - }, - onAnimeClick = { - component.viewAnime() - }, - onMangaClick = { - component.viewManga() - } - ) - }, - floatingActionButton = { - val state by FABConfig.state - - when (val current = state) { - is FABConfig.Scan -> { - if (!current.loading) { - ExtendedFloatingActionButton( - onClick = current.onClick, - icon = { - Icon( - imageVector = Icons.Filled.CameraEnhance, - contentDescription = null - ) - }, - text = { - Text( - text = "Scan" - ) - }, - expanded = current.listState.isScrollingUp(), - ) - } - } - else -> { } - } - } - ) { - Row( - modifier = Modifier.padding(it) - ) { - NavigationRail { - val selectedPage by component.selectedPage.subscribeAsState() - - Spacer(modifier = Modifier.weight(1F)) - component.pagerItems.forEachIndexed { index, pagerItem -> - NavigationRailItem( - selected = selectedPage == index, - icon = { - NavIcon(pagerItem) - }, - onClick = { - component.selectPage(index) - }, - label = { - Text(text = stringResource(pagerItem.label)) - }, - alwaysShowLabel = true - ) - } - Spacer(modifier = Modifier.weight(1F)) - } - - ExpandedPages( - pages = component.pages - ) { _, page -> - page.render() - } - } - } -} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/component/NavIcon.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/component/NavIcon.kt deleted file mode 100644 index 7395bff..0000000 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/component/NavIcon.kt +++ /dev/null @@ -1,20 +0,0 @@ -package dev.datlag.aniflow.ui.navigation.screen.initial.component - -import androidx.compose.foundation.layout.size -import androidx.compose.material3.Icon -import androidx.compose.material3.LocalContentColor -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import dev.datlag.aniflow.ui.navigation.screen.initial.InitialComponent -import dev.icerock.moko.resources.compose.stringResource - -@Composable -fun NavIcon(item: InitialComponent.PagerItem) { - Icon( - imageVector = item.icon, - contentDescription = stringResource(item.label), - modifier = Modifier.size(24.dp), - tint = LocalContentColor.current - ) -} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/favorites/FavoritesComponent.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/favorites/FavoritesComponent.kt deleted file mode 100644 index 7a9d4d5..0000000 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/favorites/FavoritesComponent.kt +++ /dev/null @@ -1,6 +0,0 @@ -package dev.datlag.aniflow.ui.navigation.screen.initial.favorites - -import dev.datlag.aniflow.ui.navigation.Component - -interface FavoritesComponent : Component { -} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/favorites/FavoritesScreen.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/favorites/FavoritesScreen.kt deleted file mode 100644 index d4857ad..0000000 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/favorites/FavoritesScreen.kt +++ /dev/null @@ -1,8 +0,0 @@ -package dev.datlag.aniflow.ui.navigation.screen.initial.favorites - -import androidx.compose.runtime.Composable - -@Composable -fun FavoritesScreen(component: FavoritesComponent) { - -} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/favorites/FavoritesScreenComponent.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/favorites/FavoritesScreenComponent.kt deleted file mode 100644 index 4ed6fcc..0000000 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/favorites/FavoritesScreenComponent.kt +++ /dev/null @@ -1,19 +0,0 @@ -package dev.datlag.aniflow.ui.navigation.screen.initial.favorites - -import androidx.compose.runtime.Composable -import com.arkivanov.decompose.ComponentContext -import dev.datlag.aniflow.common.onRender -import org.kodein.di.DI - -class FavoritesScreenComponent( - componentContext: ComponentContext, - override val di: DI -) : FavoritesComponent, ComponentContext by componentContext { - - @Composable - override fun render() { - onRender { - FavoritesScreen(this) - } - } -} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/model/FABConfig.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/model/FABConfig.kt deleted file mode 100644 index 7f79510..0000000 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/model/FABConfig.kt +++ /dev/null @@ -1,17 +0,0 @@ -package dev.datlag.aniflow.ui.navigation.screen.initial.model - -import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.runtime.mutableStateOf - -sealed interface FABConfig { - - data class Scan( - val listState: LazyListState, - val loading: Boolean = false, - val onClick: () -> Unit - ) : FABConfig - - companion object { - val state = mutableStateOf(null) - } -} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/settings/SettingsScreen.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/settings/SettingsScreen.kt index 6c6278d..6c844ec 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/settings/SettingsScreen.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/settings/SettingsScreen.kt @@ -20,6 +20,7 @@ import dev.chrisbanes.haze.haze import dev.datlag.aniflow.LocalHaze import dev.datlag.aniflow.LocalPaddingValues import dev.datlag.aniflow.SharedRes +import dev.datlag.aniflow.common.merge import dev.datlag.aniflow.common.plus import dev.datlag.aniflow.other.Constants import dev.datlag.aniflow.other.StateSaver @@ -30,143 +31,145 @@ import dev.icerock.moko.resources.compose.painterResource @Composable fun SettingsScreen(component: SettingsComponent) { - val padding = PaddingValues(16.dp) - val listState = rememberLazyListState( - initialFirstVisibleItemIndex = StateSaver.List.settingsOverview, - initialFirstVisibleItemScrollOffset = StateSaver.List.settingsOverviewOffset - ) + Scaffold { + val padding = it.merge(PaddingValues(16.dp)) + val listState = rememberLazyListState( + initialFirstVisibleItemIndex = StateSaver.List.settingsOverview, + initialFirstVisibleItemScrollOffset = StateSaver.List.settingsOverviewOffset + ) - LazyColumn( - state = listState, - modifier = Modifier.fillMaxWidth(), - contentPadding = LocalPaddingValues.current?.plus(padding) ?: padding, - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - item { - UserSection( - userFlow = component.user, - loginUri = component.loginUri, - modifier = Modifier.fillParentMaxWidth() - ) - } - item { - ColorSection( - selectedColorFlow = component.selectedColor, - modifier = Modifier.fillParentMaxWidth(), - onChange = component::changeProfileColor - ) - } - item { - TitleSection( - titleFlow = component.selectedTitleLanguage, - modifier = Modifier.fillParentMaxWidth(), - onChange = component::changeTitleLanguage - ) - } - item { - CharacterSection( - characterFlow = component.selectedCharLanguage, - modifier = Modifier.fillParentMaxWidth(), - onChanged = component::changeCharLanguage - ) - } - item { - AdultSection( - adultFlow = component.adultContent, - modifier = Modifier.fillParentMaxWidth(), - onChange = component::changeAdultContent - ) - } - item { - DomainSection( - modifier = Modifier.fillParentMaxWidth() - ) - } - item { - val uriHandler = LocalUriHandler.current - val isLoggedIn by component.isLoggedIn.collectAsStateWithLifecycle(false) + LazyColumn( + state = listState, + modifier = Modifier.fillMaxWidth(), + contentPadding = padding, + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + item { + UserSection( + userFlow = component.user, + loginUri = component.loginUri, + modifier = Modifier.fillParentMaxWidth() + ) + } + item { + ColorSection( + selectedColorFlow = component.selectedColor, + modifier = Modifier.fillParentMaxWidth(), + onChange = component::changeProfileColor + ) + } + item { + TitleSection( + titleFlow = component.selectedTitleLanguage, + modifier = Modifier.fillParentMaxWidth(), + onChange = component::changeTitleLanguage + ) + } + item { + CharacterSection( + characterFlow = component.selectedCharLanguage, + modifier = Modifier.fillParentMaxWidth(), + onChanged = component::changeCharLanguage + ) + } + item { + AdultSection( + adultFlow = component.adultContent, + modifier = Modifier.fillParentMaxWidth(), + onChange = component::changeAdultContent + ) + } + item { + DomainSection( + modifier = Modifier.fillParentMaxWidth() + ) + } + item { + val uriHandler = LocalUriHandler.current + val isLoggedIn by component.isLoggedIn.collectAsStateWithLifecycle(false) - Row( - modifier = Modifier - .fillParentMaxWidth() - .defaultMinSize(minHeight = ButtonDefaults.MinHeight) - .clip(MaterialTheme.shapes.small) - .onClick { - if (isLoggedIn) { - component.logout() - } else { - uriHandler.openUri(component.loginUri) - } - }, - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - if (isLoggedIn) { - Icon( - imageVector = Icons.Default.NotInterested, - contentDescription = null, - ) - Text(text = "Logout") - } else { + Row( + modifier = Modifier + .fillParentMaxWidth() + .defaultMinSize(minHeight = ButtonDefaults.MinHeight) + .clip(MaterialTheme.shapes.small) + .onClick { + if (isLoggedIn) { + component.logout() + } else { + uriHandler.openUri(component.loginUri) + } + }, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + if (isLoggedIn) { + Icon( + imageVector = Icons.Default.NotInterested, + contentDescription = null, + ) + Text(text = "Logout") + } else { + Image( + modifier = Modifier.size(24.dp).clip(CircleShape), + painter = painterResource(SharedRes.images.anilist), + contentDescription = null, + ) + Text(text = "Login") + } + } + } + item { + val uriHandler = LocalUriHandler.current + + Row( + modifier = Modifier + .fillParentMaxWidth() + .defaultMinSize(minHeight = ButtonDefaults.MinHeight) + .clip(MaterialTheme.shapes.small) + .onClick { + uriHandler.openUri(Constants.GITHUB_REPO) + }, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { Image( - modifier = Modifier.size(24.dp).clip(CircleShape), - painter = painterResource(SharedRes.images.anilist), + modifier = Modifier.size(24.dp), + painter = painterResource(SharedRes.images.github), contentDescription = null, + colorFilter = ColorFilter.tint(LocalContentColor.current) ) - Text(text = "Login") + Text(text = "GitHub Repository") } } - } - item { - val uriHandler = LocalUriHandler.current + item { + val uriHandler = LocalUriHandler.current - Row( - modifier = Modifier - .fillParentMaxWidth() - .defaultMinSize(minHeight = ButtonDefaults.MinHeight) - .clip(MaterialTheme.shapes.small) - .onClick { - uriHandler.openUri(Constants.GITHUB_REPO) - }, - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - Image( - modifier = Modifier.size(24.dp), - painter = painterResource(SharedRes.images.github), - contentDescription = null, - colorFilter = ColorFilter.tint(LocalContentColor.current) - ) - Text(text = "GitHub Repository") + Row( + modifier = Modifier + .fillParentMaxWidth() + .defaultMinSize(minHeight = ButtonDefaults.MinHeight) + .clip(MaterialTheme.shapes.medium) + .onClick { + uriHandler.openUri(Constants.GITHUB_OWNER) + }, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Icon( + imageVector = Icons.Default.Code, + contentDescription = null, + ) + Text(text = "Developed by DatLag") + } } } - item { - val uriHandler = LocalUriHandler.current - Row( - modifier = Modifier - .fillParentMaxWidth() - .defaultMinSize(minHeight = ButtonDefaults.MinHeight) - .clip(MaterialTheme.shapes.medium) - .onClick { - uriHandler.openUri(Constants.GITHUB_OWNER) - }, - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - Icon( - imageVector = Icons.Default.Code, - contentDescription = null, - ) - Text(text = "Developed by DatLag") + DisposableEffect(listState) { + onDispose { + StateSaver.List.settingsOverview = listState.firstVisibleItemIndex + StateSaver.List.settingsOverviewOffset = listState.firstVisibleItemScrollOffset } } } - - DisposableEffect(listState) { - onDispose { - StateSaver.List.settingsOverview = listState.firstVisibleItemIndex - StateSaver.List.settingsOverviewOffset = listState.firstVisibleItemScrollOffset - } - } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/wallpaper/WallpaperComponent.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/wallpaper/WallpaperComponent.kt new file mode 100644 index 0000000..02759b7 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/wallpaper/WallpaperComponent.kt @@ -0,0 +1,9 @@ +package dev.datlag.aniflow.ui.navigation.screen.wallpaper + +import dev.datlag.aniflow.ui.navigation.Component + +interface WallpaperComponent : Component { + + fun viewHome() + fun viewFavorites() +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/wallpaper/WallpaperScreen.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/wallpaper/WallpaperScreen.kt new file mode 100644 index 0000000..860bf45 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/wallpaper/WallpaperScreen.kt @@ -0,0 +1,23 @@ +package dev.datlag.aniflow.ui.navigation.screen.wallpaper + +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import dev.datlag.aniflow.ui.navigation.screen.component.HidingNavigationBar +import dev.datlag.aniflow.ui.navigation.screen.component.NavigationBarState + +@Composable +fun WallpaperScreen(component: WallpaperComponent) { + Scaffold( + bottomBar = { + HidingNavigationBar( + visible = true, + selected = NavigationBarState.Wallpaper, + onWallpaper = { }, + onHome = component::viewHome, + onFavorites = component::viewFavorites + ) + } + ) { + + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/wallpaper/WallpaperScreenComponent.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/wallpaper/WallpaperScreenComponent.kt new file mode 100644 index 0000000..98ea7bc --- /dev/null +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/wallpaper/WallpaperScreenComponent.kt @@ -0,0 +1,39 @@ +package dev.datlag.aniflow.ui.navigation.screen.wallpaper + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.remember +import com.arkivanov.decompose.ComponentContext +import dev.chrisbanes.haze.HazeState +import dev.datlag.aniflow.LocalHaze +import dev.datlag.aniflow.common.onRender +import org.kodein.di.DI + +class WallpaperScreenComponent( + componentContext: ComponentContext, + override val di: DI, + private val onHome: () -> Unit, + private val onFavorites: () -> Unit, +) : WallpaperComponent, ComponentContext by componentContext { + + @Composable + override fun render() { + val haze = remember { HazeState() } + + CompositionLocalProvider( + LocalHaze provides haze + ) { + onRender { + WallpaperScreen(this) + } + } + } + + override fun viewHome() { + onHome() + } + + override fun viewFavorites() { + onFavorites() + } +} \ No newline at end of file From 154c09800a8b14aeefc7d5623c63a216968bbe32 Mon Sep 17 00:00:00 2001 From: DatLag Date: Tue, 7 May 2024 23:01:41 +0200 Subject: [PATCH 05/23] added nekos api --- composeApp/build.gradle.kts | 1 + .../datlag/aniflow/common/ExtendCompose.kt | 46 +++++++ .../datlag/aniflow/module/NetworkModule.kt | 16 +++ .../screen/component/HidingNavigationBar.kt | 51 +++++-- .../screen/wallpaper/WallpaperComponent.kt | 11 ++ .../screen/wallpaper/WallpaperScreen.kt | 128 +++++++++++++++++- .../wallpaper/WallpaperScreenComponent.kt | 17 +++ .../moko-resources/images/cat_filled.svg | 10 ++ .../moko-resources/images/cat_rounded.svg | 4 + nekos/build.gradle.kts | 53 ++++++++ .../dev/datlag/aniflow/nekos/AdultContent.kt | 34 +++++ .../kotlin/dev/datlag/aniflow/nekos/Nekos.kt | 14 ++ .../datlag/aniflow/nekos/NekosRepository.kt | 85 ++++++++++++ .../aniflow/nekos/model/ImagesResponse.kt | 44 ++++++ .../dev/datlag/aniflow/nekos/model/Rating.kt | 28 ++++ settings.gradle.kts | 1 + 16 files changed, 526 insertions(+), 17 deletions(-) create mode 100644 composeApp/src/commonMain/moko-resources/images/cat_filled.svg create mode 100644 composeApp/src/commonMain/moko-resources/images/cat_rounded.svg create mode 100644 nekos/build.gradle.kts create mode 100644 nekos/src/commonMain/kotlin/dev/datlag/aniflow/nekos/AdultContent.kt create mode 100644 nekos/src/commonMain/kotlin/dev/datlag/aniflow/nekos/Nekos.kt create mode 100644 nekos/src/commonMain/kotlin/dev/datlag/aniflow/nekos/NekosRepository.kt create mode 100644 nekos/src/commonMain/kotlin/dev/datlag/aniflow/nekos/model/ImagesResponse.kt create mode 100644 nekos/src/commonMain/kotlin/dev/datlag/aniflow/nekos/model/Rating.kt diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index 7dfe004..5d46d9a 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -119,6 +119,7 @@ kotlin { implementation(project(":firebase")) implementation(project(":anilist")) implementation(project(":model")) + implementation(project(":nekos")) implementation(project(":settings")) implementation(project(":trace")) } diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/common/ExtendCompose.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/common/ExtendCompose.kt index f56c95f..35ecb95 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/common/ExtendCompose.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/common/ExtendCompose.kt @@ -6,6 +6,8 @@ import androidx.compose.foundation.layout.calculateEndPadding import androidx.compose.foundation.layout.calculateStartPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.grid.LazyGridState +import androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridState import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.SheetState @@ -128,6 +130,50 @@ fun LazyListState.isScrollingUp(): Boolean { }.value } +@Composable +fun LazyGridState.isScrollingUp(): Boolean { + var previousIndex by remember(this) { + mutableStateOf(firstVisibleItemIndex) + } + var previousScrollOffset by remember(this) { + mutableStateOf(firstVisibleItemScrollOffset) + } + return remember(this) { + derivedStateOf { + if (previousIndex != firstVisibleItemIndex) { + previousIndex > firstVisibleItemIndex + } else { + previousScrollOffset >= firstVisibleItemScrollOffset + }.also { + previousIndex = firstVisibleItemIndex + previousScrollOffset = firstVisibleItemScrollOffset + } + } + }.value +} + +@Composable +fun LazyStaggeredGridState.isScrollingUp(): Boolean { + var previousIndex by remember(this) { + mutableStateOf(firstVisibleItemIndex) + } + var previousScrollOffset by remember(this) { + mutableStateOf(firstVisibleItemScrollOffset) + } + return remember(this) { + derivedStateOf { + if (previousIndex != firstVisibleItemIndex) { + previousIndex > firstVisibleItemIndex + } else { + previousScrollOffset >= firstVisibleItemScrollOffset + }.also { + previousIndex = firstVisibleItemIndex + previousScrollOffset = firstVisibleItemScrollOffset + } + } + }.value +} + /** * Checks if the modal is currently expanded or a swipe action is in progress to be expanded. */ diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/module/NetworkModule.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/module/NetworkModule.kt index 62efb3c..7f05638 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/module/NetworkModule.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/module/NetworkModule.kt @@ -29,6 +29,8 @@ import org.kodein.di.bindSingleton import org.kodein.di.instance import dev.datlag.aniflow.common.nullableFirebaseInstance import dev.datlag.aniflow.model.safeFirstOrNull +import dev.datlag.aniflow.nekos.Nekos +import dev.datlag.aniflow.nekos.NekosRepository import dev.datlag.aniflow.other.UserHelper import dev.datlag.aniflow.settings.Settings import dev.datlag.aniflow.trace.Trace @@ -113,6 +115,12 @@ data object NetworkModule { baseUrl("https://api.trace.moe/") }.create() } + bindSingleton { + val builder = instance() + builder.build { + baseUrl("https://api.nekosapi.com/v3/") + }.create() + } bindSingleton { val appSettings = instance() @@ -168,5 +176,13 @@ data object NetworkModule { trace = instance(), ) } + bindSingleton { + val appSettings = instance() + + NekosRepository( + nekos = instance(), + nsfw = appSettings.adultContent + ) + } } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/component/HidingNavigationBar.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/component/HidingNavigationBar.kt index 764d728..7d0b9b6 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/component/HidingNavigationBar.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/component/HidingNavigationBar.kt @@ -5,20 +5,21 @@ import androidx.compose.animation.core.LinearOutSlowInEasing import androidx.compose.animation.core.tween import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideOutVertically +import androidx.compose.foundation.Image import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.FavoriteBorder import androidx.compose.material.icons.filled.Home import androidx.compose.material.icons.filled.Wallpaper import androidx.compose.material.icons.outlined.Home -import androidx.compose.material.icons.rounded.Favorite -import androidx.compose.material.icons.rounded.FavoriteBorder -import androidx.compose.material.icons.rounded.Home -import androidx.compose.material.icons.rounded.Wallpaper +import androidx.compose.material.icons.rounded.* import androidx.compose.material3.* import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.dp @@ -29,6 +30,7 @@ import dev.datlag.aniflow.LocalHaze import dev.datlag.aniflow.SharedRes import dev.datlag.aniflow.common.isScrollingUp import dev.datlag.aniflow.ui.navigation.RootConfig +import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import kotlinx.serialization.Serializable import kotlinx.serialization.Transient @@ -71,30 +73,48 @@ fun HidingNavigationBar( containerColor = Color.Transparent, contentColor = MaterialTheme.colorScheme.contentColorFor(NavigationBarDefaults.containerColor) ) { + val isWallpaper = remember(selected) { + selected is NavigationBarState.Wallpaper + } + val isHome = remember(selected) { + selected is NavigationBarState.Home + } + val isFavorites = remember(selected) { + selected is NavigationBarState.Favorite + } + NavigationBarItem( onClick = { - if (selected !is NavigationBarState.Wallpaper) { + if (!isWallpaper) { onWallpaper() } }, - selected = selected is NavigationBarState.Wallpaper, + selected = isWallpaper, icon = { - Icon( - imageVector = selected.wallpaperIcon, - contentDescription = null + Image( + modifier = Modifier.size(24.dp), + painter = painterResource( + if (isWallpaper) { + SharedRes.images.cat_filled + } else { + SharedRes.images.cat_rounded + } + ), + contentDescription = null, + colorFilter = ColorFilter.tint(LocalContentColor.current) ) }, label = { - Text(text = "Wallpapers") + Text(text = "Nekos") } ) NavigationBarItem( onClick = { - if (selected !is NavigationBarState.Home) { + if (!isHome) { onHome() } }, - selected = selected is NavigationBarState.Home, + selected = isHome, icon = { Icon( imageVector = selected.homeIcon, @@ -107,11 +127,11 @@ fun HidingNavigationBar( ) NavigationBarItem( onClick = { - if (selected !is NavigationBarState.Favorite) { + if (!isFavorites) { onFavorites() } }, - selected = selected is NavigationBarState.Favorite, + selected = isFavorites, icon = { Icon( imageVector = selected.favoriteIcon, @@ -157,6 +177,9 @@ sealed interface NavigationBarState { data object Wallpaper : NavigationBarState { override val unselectedIcon: ImageVector get() = Icons.Rounded.Wallpaper + + override val selectedIcon: ImageVector + get() = Icons.Rounded.Image } @Serializable diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/wallpaper/WallpaperComponent.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/wallpaper/WallpaperComponent.kt index 02759b7..13520bc 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/wallpaper/WallpaperComponent.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/wallpaper/WallpaperComponent.kt @@ -1,9 +1,20 @@ package dev.datlag.aniflow.ui.navigation.screen.wallpaper +import dev.datlag.aniflow.nekos.NekosRepository +import dev.datlag.aniflow.nekos.model.Rating import dev.datlag.aniflow.ui.navigation.Component +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow interface WallpaperComponent : Component { + val adultContent: Flow + val rating: StateFlow + + val state: Flow + fun viewHome() fun viewFavorites() + + fun filter(rating: Rating) } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/wallpaper/WallpaperScreen.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/wallpaper/WallpaperScreen.kt index 860bf45..6ee9150 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/wallpaper/WallpaperScreen.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/wallpaper/WallpaperScreen.kt @@ -1,23 +1,145 @@ package dev.datlag.aniflow.ui.navigation.screen.wallpaper -import androidx.compose.material3.Scaffold +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid +import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells +import androidx.compose.foundation.lazy.staggeredgrid.items +import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.FilterList +import androidx.compose.material3.* import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.unit.dp +import coil3.compose.AsyncImage +import com.maxkeppeker.sheets.core.models.base.rememberUseCaseState +import com.maxkeppeler.sheets.option.OptionDialog +import com.maxkeppeler.sheets.option.models.DisplayMode +import com.maxkeppeler.sheets.option.models.Option +import com.maxkeppeler.sheets.option.models.OptionConfig +import com.maxkeppeler.sheets.option.models.OptionSelection +import dev.chrisbanes.haze.haze +import dev.datlag.aniflow.LocalHaze +import dev.datlag.aniflow.common.isScrollingUp +import dev.datlag.aniflow.common.merge +import dev.datlag.aniflow.nekos.NekosRepository +import dev.datlag.aniflow.nekos.model.Rating import dev.datlag.aniflow.ui.navigation.screen.component.HidingNavigationBar import dev.datlag.aniflow.ui.navigation.screen.component.NavigationBarState +import dev.datlag.tooling.decompose.lifecycle.collectAsStateWithLifecycle +import io.github.aakira.napier.Napier +@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class) @Composable fun WallpaperScreen(component: WallpaperComponent) { + val listState = rememberLazyStaggeredGridState() + Scaffold( + floatingActionButton = { + val dialogState = rememberUseCaseState( + visible = false + ) + val adultContent by component.adultContent.collectAsStateWithLifecycle(false) + val rating by component.rating.collectAsStateWithLifecycle() + + OptionDialog( + state = dialogState, + config = OptionConfig( + mode = DisplayMode.LIST + ), + selection = OptionSelection.Single( + options = listOf( + Option( + titleText = "Safe", + selected = rating is Rating.Safe, + ), + Option( + titleText = "Suggestive", + selected = rating is Rating.Suggestive, + ), + Option( + titleText = "Borderline", + selected = rating is Rating.Borderline, + disabled = !adultContent + ), + Option( + titleText = "Explicit", + selected = rating is Rating.Explicit, + disabled = !adultContent + ) + ), + onSelectOption = { option, _ -> + val filterRating = when (option) { + 1 -> Rating.Suggestive + 2 -> Rating.Borderline + 3 -> Rating.Explicit + else -> Rating.Safe + } + component.filter(filterRating) + } + ) + ) + + ExtendedFloatingActionButton( + onClick = { dialogState.show() }, + expanded = listState.isScrollingUp(), + icon = { + Icon( + imageVector = Icons.Rounded.FilterList, + contentDescription = null + ) + }, + text = { + Text(text = "Filter") + } + ) + }, bottomBar = { HidingNavigationBar( - visible = true, + visible = listState.isScrollingUp(), selected = NavigationBarState.Wallpaper, onWallpaper = { }, onHome = component::viewHome, onFavorites = component::viewFavorites ) } - ) { + ) { padding -> + val state by component.state.collectAsStateWithLifecycle(NekosRepository.State.None) + when (val current = state) { + is NekosRepository.State.None -> Text(text = "Loading") + is NekosRepository.State.Error -> Text(text = "Error") + is NekosRepository.State.Success -> { + LazyVerticalStaggeredGrid( + state = listState, + modifier = Modifier.haze(state = LocalHaze.current), + columns = StaggeredGridCells.Adaptive(120.dp), + contentPadding = padding.merge(PaddingValues(16.dp)), + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalItemSpacing = 16.dp + ) { + items(current.response.items, key = { it.id }) { + Card( + modifier = Modifier.animateItemPlacement(), + onClick = { + Napier.e(it.toString()) + } + ) { + AsyncImage( + modifier = Modifier.fillMaxSize(), + model = it.imageUrl ?: it.sampleUrl, + contentDescription = null, + contentScale = ContentScale.Crop + ) + } + } + } + } + } } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/wallpaper/WallpaperScreenComponent.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/wallpaper/WallpaperScreenComponent.kt index 98ea7bc..e8a5088 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/wallpaper/WallpaperScreenComponent.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/wallpaper/WallpaperScreenComponent.kt @@ -7,7 +7,13 @@ import com.arkivanov.decompose.ComponentContext import dev.chrisbanes.haze.HazeState import dev.datlag.aniflow.LocalHaze import dev.datlag.aniflow.common.onRender +import dev.datlag.aniflow.nekos.NekosRepository +import dev.datlag.aniflow.nekos.model.Rating +import dev.datlag.aniflow.settings.Settings +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow import org.kodein.di.DI +import org.kodein.di.instance class WallpaperScreenComponent( componentContext: ComponentContext, @@ -16,6 +22,13 @@ class WallpaperScreenComponent( private val onFavorites: () -> Unit, ) : WallpaperComponent, ComponentContext by componentContext { + private val appSettings by instance() + override val adultContent: Flow = appSettings.adultContent + + private val nekosRepository by instance() + override val rating: StateFlow = nekosRepository.rating + override val state: Flow = nekosRepository.response + @Composable override fun render() { val haze = remember { HazeState() } @@ -36,4 +49,8 @@ class WallpaperScreenComponent( override fun viewFavorites() { onFavorites() } + + override fun filter(rating: Rating) { + nekosRepository.rating(rating) + } } \ No newline at end of file diff --git a/composeApp/src/commonMain/moko-resources/images/cat_filled.svg b/composeApp/src/commonMain/moko-resources/images/cat_filled.svg new file mode 100644 index 0000000..a91f29a --- /dev/null +++ b/composeApp/src/commonMain/moko-resources/images/cat_filled.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/composeApp/src/commonMain/moko-resources/images/cat_rounded.svg b/composeApp/src/commonMain/moko-resources/images/cat_rounded.svg new file mode 100644 index 0000000..a5b058c --- /dev/null +++ b/composeApp/src/commonMain/moko-resources/images/cat_rounded.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/nekos/build.gradle.kts b/nekos/build.gradle.kts new file mode 100644 index 0000000..a6a8af3 --- /dev/null +++ b/nekos/build.gradle.kts @@ -0,0 +1,53 @@ +plugins { + alias(libs.plugins.multiplatform) + alias(libs.plugins.android.library) + alias(libs.plugins.serialization) + alias(libs.plugins.ksp) +} + +val artifact = VersionCatalog.artifactName("trace") + +kotlin { + androidTarget() + jvm() + + iosX64() + iosArm64() + iosSimulatorArm64() + + applyDefaultHierarchyTemplate() + + sourceSets { + commonMain.dependencies { + api(libs.coroutines) + implementation(libs.tooling) + api(libs.ktorfit) + implementation(libs.serialization) + + api(project(":model")) + implementation(project(":firebase")) + } + } +} + +dependencies { + add("kspCommonMainMetadata", libs.ktorfit.ksp) + add("kspAndroid", libs.ktorfit.ksp) + add("kspJvm", libs.ktorfit.ksp) + add("kspIosX64", libs.ktorfit.ksp) + add("kspIosArm64", libs.ktorfit.ksp) + add("kspIosSimulatorArm64", libs.ktorfit.ksp) +} + +android { + compileSdk = Configuration.compileSdk + namespace = artifact + + defaultConfig { + minSdk = Configuration.minSdk + } + compileOptions { + sourceCompatibility = CompileOptions.sourceCompatibility + targetCompatibility = CompileOptions.targetCompatibility + } +} diff --git a/nekos/src/commonMain/kotlin/dev/datlag/aniflow/nekos/AdultContent.kt b/nekos/src/commonMain/kotlin/dev/datlag/aniflow/nekos/AdultContent.kt new file mode 100644 index 0000000..203d63e --- /dev/null +++ b/nekos/src/commonMain/kotlin/dev/datlag/aniflow/nekos/AdultContent.kt @@ -0,0 +1,34 @@ +package dev.datlag.aniflow.nekos + +internal data object AdultContent { + + sealed interface Tag : CharSequence { + val tag: String + + override val length: Int + get() = tag.length + + override operator fun get(index: Int): Char { + return tag[index] + } + + override fun subSequence(startIndex: Int, endIndex: Int): CharSequence { + return tag.subSequence(startIndex, endIndex) + } + + data object Bikini : Tag { + override val tag: String = "Bikini" + } + + companion object { + val all = listOf(Bikini) + val allTags = all.map { it.tag } + + fun exists(tag: String): Boolean { + return all.any { t -> + t.tag.equals(tag, ignoreCase = true) + } + } + } + } +} \ No newline at end of file diff --git a/nekos/src/commonMain/kotlin/dev/datlag/aniflow/nekos/Nekos.kt b/nekos/src/commonMain/kotlin/dev/datlag/aniflow/nekos/Nekos.kt new file mode 100644 index 0000000..f9df2ce --- /dev/null +++ b/nekos/src/commonMain/kotlin/dev/datlag/aniflow/nekos/Nekos.kt @@ -0,0 +1,14 @@ +package dev.datlag.aniflow.nekos + +import de.jensklingenberg.ktorfit.http.GET +import de.jensklingenberg.ktorfit.http.Query +import dev.datlag.aniflow.nekos.model.ImagesResponse + +interface Nekos { + + @GET("images") + suspend fun images( + @Query("rating") rating: String, + @Query("offset") offset: Int? = null, + ): ImagesResponse +} \ No newline at end of file diff --git a/nekos/src/commonMain/kotlin/dev/datlag/aniflow/nekos/NekosRepository.kt b/nekos/src/commonMain/kotlin/dev/datlag/aniflow/nekos/NekosRepository.kt new file mode 100644 index 0000000..f1e4d84 --- /dev/null +++ b/nekos/src/commonMain/kotlin/dev/datlag/aniflow/nekos/NekosRepository.kt @@ -0,0 +1,85 @@ +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.coroutines.flow.* +import kotlinx.serialization.Serializable + +class NekosRepository( + private val nekos: Nekos, + private val nsfw: Flow = flowOf(false), +) { + + private val offset = MutableStateFlow(null) + val rating = MutableStateFlow(Rating.Safe) + + private val result = combine(offset, rating) { o, r -> + CatchResult.repeat(2) { + nekos.images( + rating = r.query, + offset = o, + ) + } + }.map { + State.fromResponse(it.asNullableSuccess()) + } + val response = combine(nsfw.distinctUntilChanged(), result, rating) { n, q, r -> + if (n) { + q + } else { + if (r is Rating.Explicit || r is Rating.Borderline) { + rating(Rating.Safe) + } + + when (q) { + is State.Success -> { + State.Success( + ImagesResponse( + items = q.response.items.filterNot { it.hasAdultTag }, + count = q.response.count + ) + ) + } + else -> q + } + } + } + + fun offset(value: Int) { + offset.update { value } + } + + fun rating(value: Rating) { + rating.update { + if (it != value) { + offset.update { null } + } + value + } + } + + @Serializable + sealed interface State { + @Serializable + data object None : State + + @Serializable + data class Success( + val response: ImagesResponse + ) : State + + @Serializable + data object Error : State + + companion object { + fun fromResponse(response: ImagesResponse?): State { + return if (response == null || response.isError) { + Error + } else { + Success(response) + } + } + } + } +} \ No newline at end of file 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 new file mode 100644 index 0000000..1fc4745 --- /dev/null +++ b/nekos/src/commonMain/kotlin/dev/datlag/aniflow/nekos/model/ImagesResponse.kt @@ -0,0 +1,44 @@ +package dev.datlag.aniflow.nekos.model + +import dev.datlag.aniflow.nekos.AdultContent +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.Transient + +@Serializable +data class ImagesResponse( + @SerialName("items") val items: List, + @SerialName("count") val count: Int = items.size, +) { + @Transient + val isError: Boolean = items.isEmpty() + + @Serializable + data class Item( + @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(), + ) { + @Transient + val hasAdultTag: Boolean = tags.any { + it.isNsfw || AdultContent.Tag.exists(it.name) + } + + @Serializable + data class Character( + @SerialName("id") val id: Int, + @SerialName("name") val name: String, + @SerialName("description") val description: String? = null, + @SerialName("gender") val gender: String? = null, + ) + + @Serializable + data class Tag( + @SerialName("id") val id: Int, + @SerialName("name") val name: String, + @SerialName("is_nsfw") val isNsfw: Boolean = false, + ) + } +} diff --git a/nekos/src/commonMain/kotlin/dev/datlag/aniflow/nekos/model/Rating.kt b/nekos/src/commonMain/kotlin/dev/datlag/aniflow/nekos/model/Rating.kt new file mode 100644 index 0000000..9847979 --- /dev/null +++ b/nekos/src/commonMain/kotlin/dev/datlag/aniflow/nekos/model/Rating.kt @@ -0,0 +1,28 @@ +package dev.datlag.aniflow.nekos.model + +import kotlinx.serialization.Serializable + +@Serializable +sealed interface Rating { + val query: String + + @Serializable + data object Safe : Rating { + override val query: String = "safe" + } + + @Serializable + data object Suggestive : Rating { + override val query: String = "suggestive" + } + + @Serializable + data object Borderline : Rating { + override val query: String = "borderline" + } + + @Serializable + data object Explicit : Rating { + override val query: String = "explicit" + } +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index bfd2268..62a5eed 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -4,6 +4,7 @@ include(":composeApp", ":composeApp:sekret") include(":firebase") include(":anilist") include(":model") +include(":nekos") include(":settings") include(":trace") From 8d06debe3a9b9e1441858579378f721bab63499c Mon Sep 17 00:00:00 2001 From: DatLag Date: Thu, 9 May 2024 15:35:31 +0200 Subject: [PATCH 06/23] refactor nekos location --- .../datlag/aniflow/common/ExtendCompose.kt | 25 ++++---- .../aniflow/ui/navigation/RootComponent.kt | 33 ++++------- .../aniflow/ui/navigation/RootConfig.kt | 2 +- .../screen/component/HidingNavigationBar.kt | 41 +++++-------- .../screen/favorites/FavoritesComponent.kt | 2 +- .../screen/favorites/FavoritesScreen.kt | 2 +- .../favorites/FavoritesScreenComponent.kt | 6 +- .../navigation/screen/home/HomeComponent.kt | 2 +- .../ui/navigation/screen/home/HomeScreen.kt | 2 +- .../screen/home/HomeScreenComponent.kt | 6 +- .../NekosComponent.kt} | 8 +-- .../NekosScreen.kt} | 59 +++++++++++++++---- .../NekosScreenComponent.kt} | 19 +++--- .../screen/settings/SettingsComponent.kt | 1 + .../screen/settings/SettingsScreen.kt | 36 +++++++++++ .../settings/SettingsScreenComponent.kt | 7 ++- .../moko-resources/base/strings.xml | 1 + .../kotlin/dev/datlag/aniflow/nekos/Nekos.kt | 2 +- 18 files changed, 153 insertions(+), 101 deletions(-) rename composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/{wallpaper/WallpaperComponent.kt => nekos/NekosComponent.kt} (71%) rename composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/{wallpaper/WallpaperScreen.kt => nekos/NekosScreen.kt} (74%) rename composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/{wallpaper/WallpaperScreenComponent.kt => nekos/NekosScreenComponent.kt} (77%) diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/common/ExtendCompose.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/common/ExtendCompose.kt index 35ecb95..f22469a 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/common/ExtendCompose.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/common/ExtendCompose.kt @@ -64,16 +64,19 @@ fun PaddingValues.merge(other: PaddingValues): PaddingValues { ) } -fun Modifier.localPadding(additional: PaddingValues = PaddingValues(0.dp)) = composed { - this.padding(LocalPaddingValues.current?.plus(additional) ?: additional) +@Composable +fun Modifier.localPadding(additional: PaddingValues = PaddingValues(0.dp)): Modifier { + return this.padding(LocalPaddingValues.current?.plus(additional) ?: additional) } -fun Modifier.localPadding(all: Dp) = composed { - this.localPadding(PaddingValues(all)) +@Composable +fun Modifier.localPadding(all: Dp): Modifier { + return this.localPadding(PaddingValues(all)) } -fun Modifier.localPadding(horizontal: Dp, vertical: Dp = 0.dp) = composed { - this.localPadding(PaddingValues(horizontal = horizontal, vertical = vertical)) +@Composable +fun Modifier.localPadding(horizontal: Dp, vertical: Dp = 0.dp): Modifier { + return this.localPadding(PaddingValues(horizontal = horizontal, vertical = vertical)) } @Composable @@ -100,12 +103,14 @@ fun LocalPadding(top: Dp = 0.dp, start: Dp = 0.dp, bottom: Dp = 0.dp, end: Dp = ) ?: PaddingValues(top = top, start = start, bottom = bottom, end = end) } -fun Modifier.mergedLocalPadding(other: PaddingValues, additional: PaddingValues = PaddingValues(0.dp)) = composed { - this.padding((LocalPaddingValues.current?.merge(other) ?: other).plus(additional)) +@Composable +fun Modifier.mergedLocalPadding(other: PaddingValues, additional: PaddingValues = PaddingValues(0.dp)): Modifier { + return this.padding((LocalPaddingValues.current?.merge(other) ?: other).plus(additional)) } -fun Modifier.mergedLocalPadding(other: PaddingValues, additional: Dp) = composed { - this.mergedLocalPadding(other, PaddingValues(additional)) +@Composable +fun Modifier.mergedLocalPadding(other: PaddingValues, additional: Dp): Modifier { + return this.mergedLocalPadding(other, PaddingValues(additional)) } @Composable 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 e3f5566..157b761 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 @@ -1,29 +1,20 @@ package dev.datlag.aniflow.ui.navigation -import androidx.compose.animation.core.tween -import androidx.compose.foundation.gestures.Orientation import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.layout.layout 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.fade 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.extensions.compose.stack.animation.stackAnimator import com.arkivanov.decompose.router.stack.* import dev.datlag.aniflow.common.onRender -import dev.datlag.aniflow.model.ifValueOrNull import dev.datlag.aniflow.other.UserHelper import dev.datlag.aniflow.ui.navigation.screen.favorites.FavoritesScreenComponent import dev.datlag.aniflow.ui.navigation.screen.home.HomeScreenComponent import dev.datlag.aniflow.ui.navigation.screen.medium.MediumScreenComponent -import dev.datlag.aniflow.ui.navigation.screen.settings.SettingsScreen import dev.datlag.aniflow.ui.navigation.screen.settings.SettingsScreenComponent -import dev.datlag.aniflow.ui.navigation.screen.wallpaper.WallpaperScreenComponent -import io.github.aakira.napier.Napier +import dev.datlag.aniflow.ui.navigation.screen.nekos.NekosScreenComponent import org.kodein.di.DI import org.kodein.di.instance @@ -55,8 +46,8 @@ class RootComponent( onProfile = { navigation.push(RootConfig.Settings) }, - onWallpaper = { - navigation.replaceCurrent(RootConfig.Wallpaper) + onDiscover = { + // navigation.replaceCurrent(RootConfig.Wallpaper) }, onFavorites = { navigation.replaceCurrent(RootConfig.Favorites) @@ -70,27 +61,25 @@ class RootComponent( ) is RootConfig.Settings -> SettingsScreenComponent( componentContext = componentContext, - di = di + di = di, + onNekos = { + navigation.bringToFront(RootConfig.Nekos) + } ) is RootConfig.Favorites -> FavoritesScreenComponent( componentContext = componentContext, di = di, - onWallpaper = { - navigation.replaceCurrent(RootConfig.Wallpaper) + onDiscover = { + // navigation.replaceCurrent(RootConfig.Wallpaper) }, onHome = { navigation.replaceCurrent(RootConfig.Home) } ) - is RootConfig.Wallpaper -> WallpaperScreenComponent( + is RootConfig.Nekos -> NekosScreenComponent( componentContext = componentContext, di = di, - onHome = { - navigation.replaceCurrent(RootConfig.Home) - }, - onFavorites = { - navigation.replaceCurrent(RootConfig.Favorites) - } + onBack = navigation::pop ) } } diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/RootConfig.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/RootConfig.kt index 38a9df0..6ab07c1 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/RootConfig.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/RootConfig.kt @@ -21,5 +21,5 @@ sealed class RootConfig { data object Favorites : RootConfig() @Serializable - data object Wallpaper : RootConfig() + data object Nekos : RootConfig() } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/component/HidingNavigationBar.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/component/HidingNavigationBar.kt index 7d0b9b6..65d92fd 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/component/HidingNavigationBar.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/component/HidingNavigationBar.kt @@ -40,7 +40,7 @@ import kotlinx.serialization.Transient fun HidingNavigationBar( visible: Boolean, selected: NavigationBarState, - onWallpaper: () -> Unit, + onDiscover: () -> Unit, onHome: () -> Unit, onFavorites: () -> Unit ) { @@ -73,8 +73,8 @@ fun HidingNavigationBar( containerColor = Color.Transparent, contentColor = MaterialTheme.colorScheme.contentColorFor(NavigationBarDefaults.containerColor) ) { - val isWallpaper = remember(selected) { - selected is NavigationBarState.Wallpaper + val isDiscover = remember(selected) { + selected is NavigationBarState.Discover } val isHome = remember(selected) { selected is NavigationBarState.Home @@ -85,27 +85,19 @@ fun HidingNavigationBar( NavigationBarItem( onClick = { - if (!isWallpaper) { - onWallpaper() + if (!isDiscover) { + onDiscover() } }, - selected = isWallpaper, + selected = isDiscover, icon = { - Image( - modifier = Modifier.size(24.dp), - painter = painterResource( - if (isWallpaper) { - SharedRes.images.cat_filled - } else { - SharedRes.images.cat_rounded - } - ), - contentDescription = null, - colorFilter = ColorFilter.tint(LocalContentColor.current) + Icon( + imageVector = selected.discoverIcon, + contentDescription = null ) }, label = { - Text(text = "Nekos") + Text(text = "Discover") } ) NavigationBarItem( @@ -155,10 +147,10 @@ sealed interface NavigationBarState { val selectedIcon: ImageVector get() = unselectedIcon - val wallpaperIcon: ImageVector + val discoverIcon: ImageVector get() = when (this) { - is Wallpaper -> selectedIcon - else -> Wallpaper.unselectedIcon + is Discover -> selectedIcon + else -> Discover.unselectedIcon } val homeIcon: ImageVector @@ -174,12 +166,9 @@ sealed interface NavigationBarState { } @Serializable - data object Wallpaper : NavigationBarState { + data object Discover : NavigationBarState { override val unselectedIcon: ImageVector - get() = Icons.Rounded.Wallpaper - - override val selectedIcon: ImageVector - get() = Icons.Rounded.Image + get() = Icons.Rounded.Search } @Serializable diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/favorites/FavoritesComponent.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/favorites/FavoritesComponent.kt index 82f11f0..b577263 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/favorites/FavoritesComponent.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/favorites/FavoritesComponent.kt @@ -4,6 +4,6 @@ import dev.datlag.aniflow.ui.navigation.Component interface FavoritesComponent : Component { - fun viewWallpaper() + fun viewDiscover() fun viewHome() } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/favorites/FavoritesScreen.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/favorites/FavoritesScreen.kt index 302de11..d4831e9 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/favorites/FavoritesScreen.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/favorites/FavoritesScreen.kt @@ -12,7 +12,7 @@ fun FavoritesScreen(component: FavoritesComponent) { HidingNavigationBar( visible = true, selected = NavigationBarState.Favorite, - onWallpaper = component::viewWallpaper, + onDiscover = component::viewDiscover, onHome = component::viewHome, onFavorites = { } ) diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/favorites/FavoritesScreenComponent.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/favorites/FavoritesScreenComponent.kt index 253b917..72cde08 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/favorites/FavoritesScreenComponent.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/favorites/FavoritesScreenComponent.kt @@ -12,7 +12,7 @@ import org.kodein.di.DI class FavoritesScreenComponent( componentContext: ComponentContext, override val di: DI, - private val onWallpaper: () -> Unit, + private val onDiscover: () -> Unit, private val onHome: () -> Unit, ) : FavoritesComponent, ComponentContext by componentContext { @@ -29,8 +29,8 @@ class FavoritesScreenComponent( } } - override fun viewWallpaper() { - onWallpaper() + override fun viewDiscover() { + onDiscover() } override fun viewHome() { diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeComponent.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeComponent.kt index 0069afc..b51b1b7 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeComponent.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeComponent.kt @@ -22,7 +22,7 @@ interface HomeComponent : Component { fun viewProfile() fun viewAnime() fun viewManga() - fun viewWallpaper() + fun viewDiscover() fun viewFavorites() fun details(medium: Medium) diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeScreen.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeScreen.kt index b504b1c..4f9841d 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeScreen.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeScreen.kt @@ -137,7 +137,7 @@ fun HomeScreen(component: HomeComponent) { HidingNavigationBar( visible = listState.isScrollingUp() && listState.canScrollForward, selected = NavigationBarState.Home, - onWallpaper = component::viewWallpaper, + onDiscover = component::viewDiscover, onHome = { }, onFavorites = component::viewFavorites ) diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeScreenComponent.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeScreenComponent.kt index 74886e3..151c9f4 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeScreenComponent.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeScreenComponent.kt @@ -31,7 +31,7 @@ class HomeScreenComponent( override val di: DI, private val onMediumDetails: (Medium) -> Unit, private val onProfile: () -> Unit, - private val onWallpaper: () -> Unit, + private val onDiscover: () -> Unit, private val onFavorites: () -> Unit ) : HomeComponent, ComponentContext by componentContext { @@ -124,8 +124,8 @@ class HomeScreenComponent( } } - override fun viewWallpaper() { - onWallpaper() + override fun viewDiscover() { + onDiscover() } override fun viewFavorites() { diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/wallpaper/WallpaperComponent.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/nekos/NekosComponent.kt similarity index 71% rename from composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/wallpaper/WallpaperComponent.kt rename to composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/nekos/NekosComponent.kt index 13520bc..5e4b149 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/wallpaper/WallpaperComponent.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/nekos/NekosComponent.kt @@ -1,4 +1,4 @@ -package dev.datlag.aniflow.ui.navigation.screen.wallpaper +package dev.datlag.aniflow.ui.navigation.screen.nekos import dev.datlag.aniflow.nekos.NekosRepository import dev.datlag.aniflow.nekos.model.Rating @@ -6,15 +6,13 @@ import dev.datlag.aniflow.ui.navigation.Component import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow -interface WallpaperComponent : Component { +interface NekosComponent : Component { val adultContent: Flow val rating: StateFlow val state: Flow - fun viewHome() - fun viewFavorites() - + fun back() fun filter(rating: Rating) } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/wallpaper/WallpaperScreen.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/nekos/NekosScreen.kt similarity index 74% rename from composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/wallpaper/WallpaperScreen.kt rename to composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/nekos/NekosScreen.kt index 6ee9150..db33622 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/wallpaper/WallpaperScreen.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/nekos/NekosScreen.kt @@ -1,20 +1,25 @@ -package dev.datlag.aniflow.ui.navigation.screen.wallpaper +package dev.datlag.aniflow.ui.navigation.screen.nekos import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells import androidx.compose.foundation.lazy.staggeredgrid.items import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridState import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.rounded.ArrowBackIos +import androidx.compose.material.icons.rounded.ArrowBackIos import androidx.compose.material.icons.rounded.FilterList import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color 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.rememberUseCaseState @@ -24,7 +29,11 @@ import com.maxkeppeler.sheets.option.models.Option import com.maxkeppeler.sheets.option.models.OptionConfig import com.maxkeppeler.sheets.option.models.OptionSelection import dev.chrisbanes.haze.haze +import dev.chrisbanes.haze.hazeChild +import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi +import dev.chrisbanes.haze.materials.HazeMaterials import dev.datlag.aniflow.LocalHaze +import dev.datlag.aniflow.SharedRes import dev.datlag.aniflow.common.isScrollingUp import dev.datlag.aniflow.common.merge import dev.datlag.aniflow.nekos.NekosRepository @@ -32,14 +41,44 @@ import dev.datlag.aniflow.nekos.model.Rating import dev.datlag.aniflow.ui.navigation.screen.component.HidingNavigationBar import dev.datlag.aniflow.ui.navigation.screen.component.NavigationBarState import dev.datlag.tooling.decompose.lifecycle.collectAsStateWithLifecycle +import dev.icerock.moko.resources.compose.stringResource import io.github.aakira.napier.Napier -@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class) +@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class, ExperimentalHazeMaterialsApi::class) @Composable -fun WallpaperScreen(component: WallpaperComponent) { +fun NekosScreen(component: NekosComponent) { val listState = rememberLazyStaggeredGridState() Scaffold( + topBar = { + TopAppBar( + navigationIcon = { + IconButton( + onClick = { + component.back() + } + ) { + Icon( + imageVector = Icons.AutoMirrored.Rounded.ArrowBackIos, + contentDescription = null + ) + } + }, + title = { + Text(text = stringResource(SharedRes.strings.nekos_api)) + }, + colors = TopAppBarDefaults.largeTopAppBarColors( + containerColor = Color.Transparent, + scrolledContainerColor = Color.Transparent, + ), + modifier = Modifier.hazeChild( + state = LocalHaze.current, + style = HazeMaterials.thin( + containerColor = MaterialTheme.colorScheme.surface, + ) + ).fillMaxWidth() + ) + }, floatingActionButton = { val dialogState = rememberUseCaseState( visible = false @@ -98,15 +137,6 @@ fun WallpaperScreen(component: WallpaperComponent) { Text(text = "Filter") } ) - }, - bottomBar = { - HidingNavigationBar( - visible = listState.isScrollingUp(), - selected = NavigationBarState.Wallpaper, - onWallpaper = { }, - onHome = component::viewHome, - onFavorites = component::viewFavorites - ) } ) { padding -> val state by component.state.collectAsStateWithLifecycle(NekosRepository.State.None) @@ -115,6 +145,8 @@ fun WallpaperScreen(component: WallpaperComponent) { is NekosRepository.State.None -> Text(text = "Loading") is NekosRepository.State.Error -> Text(text = "Error") is NekosRepository.State.Success -> { + val uriHandler = LocalUriHandler.current + LazyVerticalStaggeredGrid( state = listState, modifier = Modifier.haze(state = LocalHaze.current), @@ -128,7 +160,8 @@ fun WallpaperScreen(component: WallpaperComponent) { modifier = Modifier.animateItemPlacement(), onClick = { Napier.e(it.toString()) - } + uriHandler.openUri(it.imageUrl ?: it.sampleUrl ?: "") + }, ) { AsyncImage( modifier = Modifier.fillMaxSize(), diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/wallpaper/WallpaperScreenComponent.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/nekos/NekosScreenComponent.kt similarity index 77% rename from composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/wallpaper/WallpaperScreenComponent.kt rename to composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/nekos/NekosScreenComponent.kt index e8a5088..dce20df 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/wallpaper/WallpaperScreenComponent.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/nekos/NekosScreenComponent.kt @@ -1,4 +1,4 @@ -package dev.datlag.aniflow.ui.navigation.screen.wallpaper +package dev.datlag.aniflow.ui.navigation.screen.nekos import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider @@ -15,12 +15,11 @@ import kotlinx.coroutines.flow.StateFlow import org.kodein.di.DI import org.kodein.di.instance -class WallpaperScreenComponent( +class NekosScreenComponent( componentContext: ComponentContext, override val di: DI, - private val onHome: () -> Unit, - private val onFavorites: () -> Unit, -) : WallpaperComponent, ComponentContext by componentContext { + private val onBack: () -> Unit, +) : NekosComponent, ComponentContext by componentContext { private val appSettings by instance() override val adultContent: Flow = appSettings.adultContent @@ -37,17 +36,13 @@ class WallpaperScreenComponent( LocalHaze provides haze ) { onRender { - WallpaperScreen(this) + NekosScreen(this) } } } - override fun viewHome() { - onHome() - } - - override fun viewFavorites() { - onFavorites() + override fun back() { + onBack() } override fun filter(rating: Rating) { diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/settings/SettingsComponent.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/settings/SettingsComponent.kt index 7973392..d0a93e7 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/settings/SettingsComponent.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/settings/SettingsComponent.kt @@ -22,4 +22,5 @@ interface SettingsComponent : Component { fun changeTitleLanguage(value: SettingsTitle?) fun changeCharLanguage(value: SettingsChar?) fun logout() + fun nekos() } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/settings/SettingsScreen.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/settings/SettingsScreen.kt index 6c844ec..b028fd8 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/settings/SettingsScreen.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/settings/SettingsScreen.kt @@ -1,5 +1,6 @@ package dev.datlag.aniflow.ui.navigation.screen.settings +import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.Image import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn @@ -28,6 +29,7 @@ import dev.datlag.aniflow.ui.navigation.screen.settings.component.* import dev.datlag.tooling.compose.onClick import dev.datlag.tooling.decompose.lifecycle.collectAsStateWithLifecycle import dev.icerock.moko.resources.compose.painterResource +import dev.icerock.moko.resources.compose.stringResource @Composable fun SettingsScreen(component: SettingsComponent) { @@ -163,6 +165,40 @@ fun SettingsScreen(component: SettingsComponent) { Text(text = "Developed by DatLag") } } + item { + var clicked by remember { mutableStateOf(0) } + + Row( + modifier = Modifier + .fillParentMaxWidth() + .defaultMinSize(minHeight = ButtonDefaults.MinHeight) + .clip(MaterialTheme.shapes.medium) + .onClick { + if (clicked >= 99) { + component.nekos() + } else { + clicked++ + } + }, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Image( + modifier = Modifier.size(24.dp), + painter = painterResource(SharedRes.images.cat_filled), + contentDescription = null, + colorFilter = ColorFilter.tint(LocalContentColor.current) + ) + Text(text = stringResource(SharedRes.strings.nekos_api)) + AnimatedVisibility( + visible = clicked >= 1, + ) { + Badge { + Text(text = "$clicked") + } + } + } + } } DisposableEffect(listState) { diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/settings/SettingsScreenComponent.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/settings/SettingsScreenComponent.kt index b8a474b..748f01e 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/settings/SettingsScreenComponent.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/settings/SettingsScreenComponent.kt @@ -17,7 +17,8 @@ import dev.datlag.aniflow.settings.model.CharLanguage as SettingsChar class SettingsScreenComponent( componentContext: ComponentContext, - override val di: DI + override val di: DI, + private val onNekos: () -> Unit ) : SettingsComponent, ComponentContext by componentContext { private val appSettings by di.instance() @@ -68,4 +69,8 @@ class SettingsScreenComponent( userHelper.logout() } } + + override fun nekos() { + onNekos() + } } \ No newline at end of file diff --git a/composeApp/src/commonMain/moko-resources/base/strings.xml b/composeApp/src/commonMain/moko-resources/base/strings.xml index 396f293..a94486b 100644 --- a/composeApp/src/commonMain/moko-resources/base/strings.xml +++ b/composeApp/src/commonMain/moko-resources/base/strings.xml @@ -57,4 +57,5 @@ Explicit Profile Favorites + Nekos API diff --git a/nekos/src/commonMain/kotlin/dev/datlag/aniflow/nekos/Nekos.kt b/nekos/src/commonMain/kotlin/dev/datlag/aniflow/nekos/Nekos.kt index f9df2ce..376fac5 100644 --- a/nekos/src/commonMain/kotlin/dev/datlag/aniflow/nekos/Nekos.kt +++ b/nekos/src/commonMain/kotlin/dev/datlag/aniflow/nekos/Nekos.kt @@ -6,7 +6,7 @@ import dev.datlag.aniflow.nekos.model.ImagesResponse interface Nekos { - @GET("images") + @GET("images/random") suspend fun images( @Query("rating") rating: String, @Query("offset") offset: Int? = null, From 94de758680940736b62595fc9353ca1bba04521d Mon Sep 17 00:00:00 2001 From: DatLag Date: Thu, 9 May 2024 16:08:49 +0200 Subject: [PATCH 07/23] change toolbar font and profile icon --- .../screen/component/CollapsingToolbar.kt | 52 +++++++++++++++--- .../navigation/screen/home/HomeComponent.kt | 2 + .../ui/navigation/screen/home/HomeScreen.kt | 1 + .../screen/home/HomeScreenComponent.kt | 7 ++- .../fonts/MarckScript-regular.ttf | Bin 0 -> 81816 bytes 5 files changed, 53 insertions(+), 9 deletions(-) create mode 100644 composeApp/src/commonMain/moko-resources/fonts/MarckScript-regular.ttf diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/component/CollapsingToolbar.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/component/CollapsingToolbar.kt index 679dbe2..f8acff0 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/component/CollapsingToolbar.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/component/CollapsingToolbar.kt @@ -10,24 +10,28 @@ import androidx.compose.material.icons.automirrored.filled.MenuBook import androidx.compose.material.icons.filled.AccountCircle import androidx.compose.material.icons.filled.PlayCircleFilled import androidx.compose.material3.* -import androidx.compose.runtime.Composable -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.unit.dp +import coil3.compose.AsyncImage +import coil3.compose.rememberAsyncImagePainter import dev.chrisbanes.haze.hazeChild import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi import dev.chrisbanes.haze.materials.HazeMaterials import dev.datlag.aniflow.LocalHaze import dev.datlag.aniflow.SharedRes +import dev.datlag.aniflow.anilist.model.User import dev.datlag.aniflow.anilist.type.MediaType import dev.datlag.tooling.compose.ifFalse import dev.datlag.tooling.decompose.lifecycle.collectAsStateWithLifecycle +import dev.icerock.moko.resources.compose.fontFamilyResource import dev.icerock.moko.resources.compose.painterResource +import dev.icerock.moko.resources.compose.stringResource import kotlinx.coroutines.flow.Flow import kotlin.math.max import kotlin.math.min @@ -37,6 +41,7 @@ import kotlin.math.min fun CollapsingToolbar( state: TopAppBarState, scrollBehavior: TopAppBarScrollBehavior, + userFlow: Flow, viewTypeFlow: Flow, onProfileClick: () -> Unit, onAnimeClick: () -> Unit, @@ -78,9 +83,37 @@ fun CollapsingToolbar( onProfileClick() } ) { - Icon( - imageVector = Icons.Filled.AccountCircle, - contentDescription = null + val user by userFlow.collectAsStateWithLifecycle(null) + var colorFilter by remember(user) { mutableStateOf(null) } + val tintColor = LocalContentColor.current + + AsyncImage( + model = user?.avatar?.large, + contentDescription = null, + contentScale = ContentScale.Crop, + colorFilter = colorFilter, + placeholder = rememberVectorPainter(Icons.Filled.AccountCircle), + error = rememberAsyncImagePainter( + model = user?.avatar?.medium, + contentScale = ContentScale.Crop, + error = rememberVectorPainter(Icons.Filled.AccountCircle), + placeholder = rememberVectorPainter(Icons.Filled.AccountCircle), + onError = { + colorFilter = ColorFilter.tint(tintColor) + }, + onSuccess = { + colorFilter = null + }, + onLoading = { + colorFilter = ColorFilter.tint(tintColor) + } + ), + onSuccess = { + colorFilter = null + }, + onLoading = { + colorFilter = ColorFilter.tint(tintColor) + } ) } }, @@ -88,7 +121,10 @@ fun CollapsingToolbar( AnimatedVisibility( visible = isCollapsed ) { - Text(text = "AniFlow") + Text( + text = stringResource(SharedRes.strings.app_name), + fontFamily = fontFamilyResource(SharedRes.fonts.marckscript_regular) + ) } }, actions = { diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeComponent.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeComponent.kt index b51b1b7..c44f942 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeComponent.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeComponent.kt @@ -3,6 +3,7 @@ package dev.datlag.aniflow.ui.navigation.screen.home import dev.datlag.aniflow.anilist.AiringTodayRepository import dev.datlag.aniflow.anilist.TrendingRepository import dev.datlag.aniflow.anilist.model.Medium +import dev.datlag.aniflow.anilist.model.User import dev.datlag.aniflow.anilist.state.CollectionState import dev.datlag.aniflow.anilist.type.MediaType import dev.datlag.aniflow.trace.TraceRepository @@ -11,6 +12,7 @@ import kotlinx.coroutines.flow.Flow interface HomeComponent : Component { val viewing: Flow + val user: Flow val airing: Flow val trending: Flow diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeScreen.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeScreen.kt index 4f9841d..33d61ae 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeScreen.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeScreen.kt @@ -66,6 +66,7 @@ fun HomeScreen(component: HomeComponent) { CollapsingToolbar( state = appBarState, scrollBehavior = scrollBehavior, + userFlow = component.user, viewTypeFlow = component.viewing, onProfileClick = component::viewProfile, onAnimeClick = component::viewAnime, diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeScreenComponent.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeScreenComponent.kt index 151c9f4..f903ce6 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeScreenComponent.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeScreenComponent.kt @@ -11,11 +11,13 @@ import dev.datlag.aniflow.anilist.PopularNextSeasonRepository import dev.datlag.aniflow.anilist.PopularSeasonRepository import dev.datlag.aniflow.anilist.TrendingRepository import dev.datlag.aniflow.anilist.model.Medium +import dev.datlag.aniflow.anilist.model.User import dev.datlag.aniflow.anilist.state.CollectionState import dev.datlag.aniflow.anilist.type.MediaType import dev.datlag.aniflow.common.onRender import dev.datlag.aniflow.model.coroutines.Executor import dev.datlag.aniflow.other.StateSaver +import dev.datlag.aniflow.other.UserHelper import dev.datlag.aniflow.settings.Settings import dev.datlag.aniflow.trace.TraceRepository import dev.datlag.tooling.decompose.ioScope @@ -44,8 +46,11 @@ class HomeScreenComponent( } } private val viewTypeExecutor = Executor() - private val stateScope = ioScope() + private val userHelper by instance() + override val user: Flow = userHelper.user + + private val stateScope = ioScope() private val airingTodayRepository by instance() override val airing: Flow = airingTodayRepository.airing.map { StateSaver.Home.updateAiring(it) diff --git a/composeApp/src/commonMain/moko-resources/fonts/MarckScript-regular.ttf b/composeApp/src/commonMain/moko-resources/fonts/MarckScript-regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..2a8f6bab92349d206092c18bed64cc10c5fd1f55 GIT binary patch literal 81816 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 Date: Thu, 9 May 2024 16:21:58 +0200 Subject: [PATCH 08/23] fix missing initial tint --- .../aniflow/ui/navigation/screen/component/CollapsingToolbar.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/component/CollapsingToolbar.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/component/CollapsingToolbar.kt index f8acff0..6210273 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/component/CollapsingToolbar.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/component/CollapsingToolbar.kt @@ -84,8 +84,8 @@ fun CollapsingToolbar( } ) { val user by userFlow.collectAsStateWithLifecycle(null) - var colorFilter by remember(user) { mutableStateOf(null) } val tintColor = LocalContentColor.current + var colorFilter by remember(user) { mutableStateOf(ColorFilter.tint(tintColor)) } AsyncImage( model = user?.avatar?.large, From 358722eff12bb5bf6bc56c56a6eb8d1b96861061 Mon Sep 17 00:00:00 2001 From: DatLag Date: Thu, 9 May 2024 18:57:51 +0200 Subject: [PATCH 09/23] update dependencies --- gradle/libs.versions.toml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8be70d5..6beed23 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] app = "1.1.1" -aboutlibraries = "11.1.3" +aboutlibraries = "11.1.4" activity = "1.9.0" android = "8.2.2" android-core = "1.13.1" @@ -24,7 +24,7 @@ firebase-android-auth = "22.3.1" firebase-android-crashlytics = "18.6.4" flowredux = "1.2.1" google-identity = "1.1.0" -haze = "0.7.0" +haze = "0.7.1" html-converter = "0.9.5" kache = "2.1.0" kasechange = "1.4.1" @@ -40,7 +40,6 @@ markdown-renderer = "0.16.0" moko-resources = "0.24.0-beta-3" multidex = "2.0.1" napier = "2.7.1" -oidc = "0.9.2" sekret = "2.0.0-alpha-04" serialization = "1.6.3" splashscreen = "1.0.1" @@ -103,7 +102,6 @@ moko-resources-compose = { group = "dev.icerock.moko", name = "resources-compose moko-resources-generator = { group = "dev.icerock.moko", name = "resources-generator", version.ref = "moko-resources" } multidex = { group = "androidx.multidex", name = "multidex", version.ref = "multidex" } napier = { group = "io.github.aakira", name = "napier", version.ref = "napier" } -oidc = { group = "io.github.kalinjul.kotlin.multiplatform", name = "oidc-appsupport", version.ref = "oidc" } sekret = { group = "dev.datlag.sekret", name = "annotations", version.ref = "sekret" } serialization = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-core", version.ref = "serialization" } serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "serialization" } From 9f9f2e69d873efc3015cc441be1bef279d2d89bb Mon Sep 17 00:00:00 2001 From: DatLag Date: Fri, 10 May 2024 13:23:27 +0200 Subject: [PATCH 10/23] fix user settings not changing --- .../datlag/aniflow/module/NetworkModule.kt | 4 +++ .../dev/datlag/aniflow/other/UserHelper.kt | 33 ++++++++++--------- 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/module/NetworkModule.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/module/NetworkModule.kt index 7f05638..666abd2 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/module/NetworkModule.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/module/NetworkModule.kt @@ -10,8 +10,10 @@ import coil3.svg.SvgDecoder import com.apollographql.apollo3.ApolloClient import com.apollographql.apollo3.api.http.HttpRequest import com.apollographql.apollo3.api.http.HttpResponse +import com.apollographql.apollo3.cache.normalized.FetchPolicy import com.apollographql.apollo3.cache.normalized.api.MemoryCacheFactory import com.apollographql.apollo3.cache.normalized.api.NormalizedCacheFactory +import com.apollographql.apollo3.cache.normalized.fetchPolicy import com.apollographql.apollo3.cache.normalized.normalizedCache import com.apollographql.apollo3.network.http.HttpInterceptor import com.apollographql.apollo3.network.http.HttpInterceptorChain @@ -87,6 +89,7 @@ data object NetworkModule { } }) .normalizedCache(instance(Constants.AniList.CACHE_FACTORY)) + .fetchPolicy(FetchPolicy.CacheAndNetwork) .build() } bindSingleton(Constants.AniList.FALLBACK_APOLLO_CLIENT) { @@ -94,6 +97,7 @@ data object NetworkModule { .dispatcher(ioDispatcher()) .serverUrl(Constants.AniList.SERVER_URL) .normalizedCache(instance(Constants.AniList.CACHE_FACTORY)) + .fetchPolicy(FetchPolicy.CacheAndNetwork) .build() } bindSingleton { diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/other/UserHelper.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/other/UserHelper.kt index 87561b0..3bd28fc 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/other/UserHelper.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/other/UserHelper.kt @@ -6,6 +6,8 @@ import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.platform.UriHandler import com.apollographql.apollo3.ApolloClient import com.apollographql.apollo3.api.Optional +import com.apollographql.apollo3.cache.normalized.FetchPolicy +import com.apollographql.apollo3.cache.normalized.fetchPolicy import dev.datlag.aniflow.anilist.ViewerMutation import dev.datlag.aniflow.anilist.ViewerQuery import dev.datlag.aniflow.anilist.model.User @@ -19,6 +21,7 @@ import dev.datlag.tooling.async.suspendCatching import dev.datlag.tooling.compose.ioDispatcher import dev.datlag.tooling.compose.withIOContext import dev.datlag.tooling.compose.withMainContext +import io.github.aakira.napier.Napier import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.flow.* @@ -44,7 +47,7 @@ class UserHelper( private val updatableUser = isLoggedIn.transform { loggedIn -> if (loggedIn) { emitAll( - client.query(ViewerQuery()).toFlow().map { + client.query(ViewerQuery()).fetchPolicy(FetchPolicy.NetworkFirst).toFlow().map { it.data?.Viewer?.let(::User) } ) @@ -56,17 +59,15 @@ class UserHelper( initialValue = null ) - val user = updatableUser.transform { user -> - emit( - user?.also { - appSettings.setData( - adultContent = it.displayAdultContent, - color = SettingsColor.fromString(it.profileColor), - titleLanguage = it.titleLanguage.toSettings(), - charLanguage = it.charLanguage.toSettings() - ) - } - ) + val user = updatableUser.map { user -> + user?.also { + appSettings.setData( + adultContent = it.displayAdultContent, + color = SettingsColor.fromString(it.profileColor), + titleLanguage = it.titleLanguage.toSettings(), + charLanguage = it.charLanguage.toSettings() + ) + } }.flowOn(ioDispatcher()) suspend fun updateAdultSetting(value: Boolean) { @@ -76,7 +77,7 @@ class UserHelper( ViewerMutation( adult = Optional.present(value) ) - ).execute().data?.UpdateUser?.let(::User) + ).fetchPolicy(FetchPolicy.NetworkFirst).execute().data?.UpdateUser?.let(::User) ) } @@ -89,7 +90,7 @@ class UserHelper( ViewerMutation( color = Optional.present(value.label) ) - ).execute().data?.UpdateUser?.let(::User) + ).fetchPolicy(FetchPolicy.NetworkFirst).execute().data?.UpdateUser?.let(::User) ) } } @@ -103,7 +104,7 @@ class UserHelper( ViewerMutation( title = Optional.presentIfNotNull(value.toMutation()) ) - ).execute().data?.UpdateUser?.let(::User) + ).fetchPolicy(FetchPolicy.NetworkFirst).execute().data?.UpdateUser?.let(::User) ) } } @@ -117,7 +118,7 @@ class UserHelper( ViewerMutation( char = Optional.presentIfNotNull(value.toMutation()) ) - ).execute().data?.UpdateUser?.let(::User) + ).fetchPolicy(FetchPolicy.NetworkFirst).execute().data?.UpdateUser?.let(::User) ) } } From de0e7758c9e9374dcd6cff1a7cb2d34f90fd268a Mon Sep 17 00:00:00 2001 From: DatLag Date: Fri, 10 May 2024 13:41:11 +0200 Subject: [PATCH 11/23] improve flow performance --- .../aniflow/anilist/AiringTodayRepository.kt | 18 ++++++---- .../anilist/PopularNextSeasonRepository.kt | 17 ++++++---- .../anilist/PopularSeasonRepository.kt | 21 ++++++++---- .../aniflow/anilist/TrendingRepository.kt | 21 ++++++++---- .../dev/datlag/aniflow/other/UserHelper.kt | 8 +++-- .../screen/component/CollapsingToolbar.kt | 3 +- .../ui/navigation/screen/nekos/NekosScreen.kt | 34 +++++++++++++++---- .../datlag/aniflow/nekos/NekosRepository.kt | 21 ++++-------- .../datlag/aniflow/trace/TraceRepository.kt | 7 ++-- 9 files changed, 97 insertions(+), 53 deletions(-) diff --git a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/AiringTodayRepository.kt b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/AiringTodayRepository.kt index 5be5087..426c5a9 100644 --- a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/AiringTodayRepository.kt +++ b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/AiringTodayRepository.kt @@ -4,6 +4,7 @@ import com.apollographql.apollo3.ApolloClient import com.apollographql.apollo3.api.Optional import dev.datlag.aniflow.anilist.model.Medium import dev.datlag.aniflow.anilist.type.AiringSort +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.* import kotlinx.datetime.Clock import kotlinx.serialization.Serializable @@ -23,12 +24,16 @@ class AiringTodayRepository( ) }.distinctUntilChanged() - private val airingPreFilter = query.transform { - return@transform emitAll(apolloClient.query(it.toGraphQL()).toFlow()) + @OptIn(ExperimentalCoroutinesApi::class) + private val airingPreFilter = query.transformLatest { + return@transformLatest emitAll(apolloClient.query(it.toGraphQL()).toFlow()) } - private val fallbackPreFilter = query.transform { - return@transform emitAll(fallbackClient.query(it.toGraphQL()).toFlow()) + + @OptIn(ExperimentalCoroutinesApi::class) + private val fallbackPreFilter = query.transformLatest { + return@transformLatest emitAll(fallbackClient.query(it.toGraphQL()).toFlow()) } + private val fallbackQuery = combine(fallbackPreFilter, nsfw.distinctUntilChanged()) { q, n -> val data = q.data if (data == null) { @@ -42,6 +47,7 @@ class AiringTodayRepository( } }.filterNotNull() + @OptIn(ExperimentalCoroutinesApi::class) val airing = combine(airingPreFilter, nsfw.distinctUntilChanged()) { q, n -> val data = q.data if (data == null) { @@ -53,8 +59,8 @@ class AiringTodayRepository( } else { State.fromGraphQL(data, n) } - }.filterNotNull().transform { - return@transform if (it is State.Error) { + }.filterNotNull().transformLatest { + return@transformLatest if (it is State.Error) { emitAll(fallbackQuery) } else { emit(it) diff --git a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/PopularNextSeasonRepository.kt b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/PopularNextSeasonRepository.kt index 9149513..3a291c6 100644 --- a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/PopularNextSeasonRepository.kt +++ b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/PopularNextSeasonRepository.kt @@ -7,6 +7,7 @@ import dev.datlag.aniflow.anilist.state.CollectionState import dev.datlag.aniflow.anilist.type.MediaSeason import dev.datlag.aniflow.anilist.type.MediaSort import dev.datlag.aniflow.anilist.type.MediaType +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.* import kotlinx.datetime.Clock @@ -17,6 +18,7 @@ class PopularNextSeasonRepository( ) { private val page = MutableStateFlow(0) + private val query = combine(page, nsfw.distinctUntilChanged()) { p, n -> val (season, year) = Clock.System.now().nextSeason @@ -27,8 +29,10 @@ class PopularNextSeasonRepository( year = year ) }.distinctUntilChanged() - private val fallbackQuery = query.transform { - return@transform emitAll(fallbackClient.query(it.toGraphQL()).toFlow()) + + @OptIn(ExperimentalCoroutinesApi::class) + private val fallbackQuery = query.transformLatest { + return@transformLatest emitAll(fallbackClient.query(it.toGraphQL()).toFlow()) }.mapNotNull { val data = it.data if (data == null) { @@ -42,8 +46,9 @@ class PopularNextSeasonRepository( } } - val popularNextSeason = query.transform { - return@transform emitAll(apolloClient.query(it.toGraphQL()).toFlow()) + @OptIn(ExperimentalCoroutinesApi::class) + val popularNextSeason = query.transformLatest { + return@transformLatest emitAll(apolloClient.query(it.toGraphQL()).toFlow()) }.mapNotNull { val data = it.data if (data == null) { @@ -55,8 +60,8 @@ class PopularNextSeasonRepository( } else { CollectionState.fromSeasonGraphQL(data) } - }.transform { - return@transform if (it.isError) { + }.transformLatest { + return@transformLatest if (it.isError) { emitAll(fallbackQuery) } else { emit(it) diff --git a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/PopularSeasonRepository.kt b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/PopularSeasonRepository.kt index 7ad69b7..4d4750c 100644 --- a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/PopularSeasonRepository.kt +++ b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/PopularSeasonRepository.kt @@ -5,6 +5,7 @@ import com.apollographql.apollo3.api.Optional import dev.datlag.aniflow.anilist.state.CollectionState import dev.datlag.aniflow.anilist.type.MediaSort import dev.datlag.aniflow.anilist.type.MediaType +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.* class PopularSeasonRepository( @@ -15,7 +16,9 @@ class PopularSeasonRepository( ) { private val page = MutableStateFlow(0) - private val type = viewManga.distinctUntilChanged().map { + + @OptIn(ExperimentalCoroutinesApi::class) + private val type = viewManga.distinctUntilChanged().mapLatest { page.update { 0 } if (it) { MediaType.MANGA @@ -23,6 +26,7 @@ class PopularSeasonRepository( MediaType.ANIME } } + private val query = combine(page, type, nsfw.distinctUntilChanged()) { p, t, n -> Query( page = p, @@ -30,8 +34,10 @@ class PopularSeasonRepository( nsfw = n ) }.distinctUntilChanged() - private val fallbackQuery = query.transform { - return@transform emitAll(fallbackClient.query(it.toGraphQL()).toFlow()) + + @OptIn(ExperimentalCoroutinesApi::class) + private val fallbackQuery = query.transformLatest { + return@transformLatest emitAll(fallbackClient.query(it.toGraphQL()).toFlow()) }.mapNotNull { val data = it.data if (data == null) { @@ -45,8 +51,9 @@ class PopularSeasonRepository( } } - val popularThisSeason = query.transform { - return@transform emitAll(apolloClient.query(it.toGraphQL()).toFlow()) + @OptIn(ExperimentalCoroutinesApi::class) + val popularThisSeason = query.transformLatest { + return@transformLatest emitAll(apolloClient.query(it.toGraphQL()).toFlow()) }.mapNotNull { val data = it.data if (data == null) { @@ -58,8 +65,8 @@ class PopularSeasonRepository( } else { CollectionState.fromSeasonGraphQL(data) } - }.transform { - return@transform if (it.isError) { + }.transformLatest { + return@transformLatest if (it.isError) { emitAll(fallbackQuery) } else { emit(it) diff --git a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/TrendingRepository.kt b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/TrendingRepository.kt index a9a54d9..28f2b03 100644 --- a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/TrendingRepository.kt +++ b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/TrendingRepository.kt @@ -6,6 +6,7 @@ import dev.datlag.aniflow.anilist.model.Medium import dev.datlag.aniflow.anilist.state.CollectionState import dev.datlag.aniflow.anilist.type.MediaSort import dev.datlag.aniflow.anilist.type.MediaType +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.* import kotlinx.serialization.Serializable @@ -17,7 +18,9 @@ class TrendingRepository( ) { private val page = MutableStateFlow(0) - private val type = viewManga.distinctUntilChanged().map { + + @OptIn(ExperimentalCoroutinesApi::class) + private val type = viewManga.distinctUntilChanged().mapLatest { page.update { 0 } if (it) { MediaType.MANGA @@ -25,6 +28,7 @@ class TrendingRepository( MediaType.ANIME } } + private val query = combine(page, type, nsfw.distinctUntilChanged()) { p, t, n -> Query( page = p, @@ -32,8 +36,10 @@ class TrendingRepository( nsfw = n ) }.distinctUntilChanged() - private val fallbackQuery = query.transform { - return@transform emitAll(fallbackClient.query(it.toGraphQL()).toFlow()) + + @OptIn(ExperimentalCoroutinesApi::class) + private val fallbackQuery = query.transformLatest { + return@transformLatest emitAll(fallbackClient.query(it.toGraphQL()).toFlow()) }.mapNotNull { val data = it.data if (data == null) { @@ -47,8 +53,9 @@ class TrendingRepository( } } - val trending = query.transform { - return@transform emitAll(apolloClient.query(it.toGraphQL()).toFlow()) + @OptIn(ExperimentalCoroutinesApi::class) + val trending = query.transformLatest { + return@transformLatest emitAll(apolloClient.query(it.toGraphQL()).toFlow()) }.mapNotNull { val data = it.data if (data == null) { @@ -60,8 +67,8 @@ class TrendingRepository( } else { CollectionState.fromTrendingGraphQL(data) } - }.transform { - return@transform if (it.isError) { + }.transformLatest { + return@transformLatest if (it.isError) { emitAll(fallbackQuery) } else { emit(it) diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/other/UserHelper.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/other/UserHelper.kt index 3bd28fc..1292d8c 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/other/UserHelper.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/other/UserHelper.kt @@ -23,6 +23,7 @@ import dev.datlag.tooling.compose.withIOContext import dev.datlag.tooling.compose.withMainContext import io.github.aakira.napier.Napier import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.flow.* import kotlinx.coroutines.runBlocking @@ -43,8 +44,8 @@ class UserHelper( val isLoggedIn: Flow = userSettings.isAniListLoggedIn.flowOn(ioDispatcher()).distinctUntilChanged() val loginUrl: String = "https://anilist.co/api/v2/oauth/authorize?client_id=$clientId&response_type=token" - @OptIn(DelicateCoroutinesApi::class) - private val updatableUser = isLoggedIn.transform { loggedIn -> + @OptIn(DelicateCoroutinesApi::class, ExperimentalCoroutinesApi::class) + private val updatableUser = isLoggedIn.transformLatest { loggedIn -> if (loggedIn) { emitAll( client.query(ViewerQuery()).fetchPolicy(FetchPolicy.NetworkFirst).toFlow().map { @@ -59,7 +60,8 @@ class UserHelper( initialValue = null ) - val user = updatableUser.map { user -> + @OptIn(ExperimentalCoroutinesApi::class) + val user = updatableUser.mapLatest { user -> user?.also { appSettings.setData( adultContent = it.displayAdultContent, diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/component/CollapsingToolbar.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/component/CollapsingToolbar.kt index 6210273..ac6de54 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/component/CollapsingToolbar.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/component/CollapsingToolbar.kt @@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.MenuBook +import androidx.compose.material.icons.automirrored.rounded.MenuBook import androidx.compose.material.icons.filled.AccountCircle import androidx.compose.material.icons.filled.PlayCircleFilled import androidx.compose.material3.* @@ -158,7 +159,7 @@ fun CollapsingToolbar( enabled = !isManga ) { Icon( - imageVector = Icons.AutoMirrored.Default.MenuBook, + imageVector = Icons.AutoMirrored.Rounded.MenuBook, contentDescription = null, tint = if (isManga) { MaterialTheme.colorScheme.primary 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 db33622..c828041 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 @@ -1,14 +1,12 @@ package dev.datlag.aniflow.ui.navigation.screen.nekos import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells import androidx.compose.foundation.lazy.staggeredgrid.items import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridState +import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.rounded.ArrowBackIos import androidx.compose.material.icons.rounded.ArrowBackIos @@ -16,7 +14,9 @@ import androidx.compose.material.icons.rounded.FilterList import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalUriHandler @@ -142,8 +142,30 @@ fun NekosScreen(component: NekosComponent) { val state by component.state.collectAsStateWithLifecycle(NekosRepository.State.None) when (val current = state) { - is NekosRepository.State.None -> Text(text = "Loading") - is NekosRepository.State.Error -> Text(text = "Error") + is NekosRepository.State.None -> { + Box( + modifier = Modifier.fillMaxSize().padding(padding), + contentAlignment = Alignment.Center + ) { + LinearProgressIndicator( + modifier = Modifier.fillMaxWidth(0.2F).clip(CircleShape) + ) + } + } + is NekosRepository.State.Error -> { + Box( + modifier = Modifier.fillMaxSize().padding(padding), + contentAlignment = Alignment.Center + ) { + Button( + onClick = { + component.back() + } + ) { + Text(text = stringResource(SharedRes.strings.back)) + } + } + } is NekosRepository.State.Success -> { val uriHandler = LocalUriHandler.current 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 f1e4d84..98fc69f 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.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.* import kotlinx.serialization.Serializable @@ -11,17 +12,16 @@ class NekosRepository( private val nsfw: Flow = flowOf(false), ) { - private val offset = MutableStateFlow(null) val rating = MutableStateFlow(Rating.Safe) - private val result = combine(offset, rating) { o, r -> + @OptIn(ExperimentalCoroutinesApi::class) + private val result = rating.mapLatest { CatchResult.repeat(2) { nekos.images( - rating = r.query, - offset = o, + rating = it.query ) } - }.map { + }.mapLatest { State.fromResponse(it.asNullableSuccess()) } val response = combine(nsfw.distinctUntilChanged(), result, rating) { n, q, r -> @@ -46,17 +46,8 @@ class NekosRepository( } } - fun offset(value: Int) { - offset.update { value } - } - fun rating(value: Rating) { - rating.update { - if (it != value) { - offset.update { null } - } - value - } + rating.update { value } } @Serializable diff --git a/trace/src/commonMain/kotlin/dev/datlag/aniflow/trace/TraceRepository.kt b/trace/src/commonMain/kotlin/dev/datlag/aniflow/trace/TraceRepository.kt index 5ff5893..81244a9 100644 --- a/trace/src/commonMain/kotlin/dev/datlag/aniflow/trace/TraceRepository.kt +++ b/trace/src/commonMain/kotlin/dev/datlag/aniflow/trace/TraceRepository.kt @@ -2,6 +2,7 @@ package dev.datlag.aniflow.trace import dev.datlag.aniflow.model.CatchResult import dev.datlag.aniflow.trace.model.SearchResponse +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.* import kotlinx.serialization.Serializable @@ -10,8 +11,10 @@ class TraceRepository( ) { private val byteArray = MutableStateFlow(null) - val response: Flow = byteArray.transform { - return@transform if (it == null || it.isEmpty()) { + + @OptIn(ExperimentalCoroutinesApi::class) + val response: Flow = byteArray.transformLatest { + return@transformLatest if (it == null || it.isEmpty()) { emit(State.None) } else { emit( From 9a1d42fc28cbc1668e95fc975ea16dd90f1593ee Mon Sep 17 00:00:00 2001 From: DatLag Date: Fri, 10 May 2024 14:32:21 +0200 Subject: [PATCH 12/23] prepare bs connection --- .../kotlin/dev/datlag/aniflow/App.kt | 1 + .../other/BurningSeriesResolver.android.kt | 30 +++++--- .../aniflow/other/BurningSeriesResolver.kt | 13 ++-- .../dev/datlag/aniflow/ui/custom/EditFAB.kt | 73 +++++++++---------- .../screen/home/component/ScheduleOverview.kt | 10 --- .../screen/medium/MediumComponent.kt | 2 + .../navigation/screen/medium/MediumScreen.kt | 35 ++++++++- .../screen/medium/MediumScreenComponent.kt | 60 ++++++++++----- .../ui/navigation/screen/nekos/NekosScreen.kt | 11 ++- .../screen/settings/SettingsScreen.kt | 4 +- .../moko-resources/base/strings.xml | 5 ++ .../other/BurningSeriesResolver.ios.kt | 4 +- 12 files changed, 156 insertions(+), 92 deletions(-) diff --git a/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/App.kt b/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/App.kt index 378a1cc..54e2690 100644 --- a/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/App.kt +++ b/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/App.kt @@ -32,6 +32,7 @@ class App : MultiDexApplication(), DIAware { StrictMode.ThreadPolicy.Builder() .detectAll() .permitDiskReads() + .permitDiskWrites() .penaltyLog() .penaltyDialog() .build() 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 68b46a8..3757ec7 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 @@ -6,6 +6,7 @@ import android.content.Context import android.content.pm.PackageManager import android.net.Uri import android.os.Build +import androidx.core.database.getStringOrNull import dev.datlag.tooling.scopeCatching import io.github.aakira.napier.Napier @@ -69,8 +70,9 @@ actual class BurningSeriesResolver( progress = progress, length = length, number = number, - series = Episode.Series( - title = seriesHref + series = Series( + title = seriesHref, + href = seriesHref ) ) ) @@ -83,12 +85,12 @@ actual class BurningSeriesResolver( return episodes } - actual fun resolveByName(english: String?, romaji: String?) { + actual fun resolveByName(english: String?, romaji: String?): Set { val englishTrimmed = english?.trim()?.ifBlank { null }?.replace("'", "") val romajiTrimmed = romaji?.trim()?.ifBlank { null }?.replace("'", "") if (seriesClient == null || (englishTrimmed == null && romajiTrimmed == null)) { - return + return emptySet() } val selection = if (englishTrimmed != null && romajiTrimmed != null) { @@ -104,26 +106,36 @@ actual class BurningSeriesResolver( selection, null, null - ) ?: return + ) ?: return emptySet() + + val series = mutableSetOf() if (seriesCursor.moveToFirst()) { while (!seriesCursor.isAfterLast) { val titleIndex = seriesCursor.getColumnIndex("title") + val hrefIndex = seriesCursor.getColumnIndex("hrefPrimary") - if (titleIndex == -1) { + if (hrefIndex == -1) { seriesCursor.moveToNext() continue } - val title = seriesCursor.getString(titleIndex) - Napier.e("Series matching name: $title") + val title = seriesCursor.getStringOrNull(titleIndex) + val href = seriesCursor.getString(hrefIndex) + + series.add( + Series( + title = title ?: href, + href = href + ) + ) seriesCursor.moveToNext() } } seriesCursor.close() - return + return series } 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 024fd96..609fcba 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/other/BurningSeriesResolver.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/other/BurningSeriesResolver.kt @@ -3,7 +3,7 @@ package dev.datlag.aniflow.other expect class BurningSeriesResolver { val isAvailable: Boolean fun resolveWatchedEpisodes(): Set - fun resolveByName(english: String?, romaji: String?) + fun resolveByName(english: String?, romaji: String?): Set fun close() } @@ -13,8 +13,9 @@ data class Episode( val length: Long, val number: String, val series: Series -) { - data class Series( - val title: String - ) -} \ No newline at end of file +) + +data class Series( + val title: String, + val href: String +) \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/custom/EditFAB.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/custom/EditFAB.kt index 210a93a..dd4aaa0 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/custom/EditFAB.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/custom/EditFAB.kt @@ -19,8 +19,11 @@ import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.TransformOrigin import androidx.compose.ui.unit.dp import dev.datlag.aniflow.SharedRes +import dev.datlag.tooling.compose.withDefaultContext +import dev.datlag.tooling.compose.withIOContext import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource +import kotlinx.coroutines.delay @Composable fun EditFAB( @@ -35,25 +38,24 @@ fun EditFAB( horizontalAlignment = Alignment.End, verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterVertically) ) { - var showOtherFABs by remember { mutableStateOf(false) } + var showOtherFABs by remember(expanded) { mutableStateOf(expanded) } AnimatedVisibility( visible = showOtherFABs, - enter = scaleIn( + enter = slideInVertically( animationSpec = bouncySpring(), - transformOrigin = TransformOrigin(1F, 0.5F) + initialOffsetY = { -(-it / 2) } ) + fadeIn( animationSpec = bouncySpring() ), - exit = scaleOut( - transformOrigin = TransformOrigin(1F, 0.5F) + exit = slideOutVertically( + animationSpec = bouncySpring(), + targetOffsetY = { -(-it / 2) } ) + fadeOut( animationSpec = bouncySpring() ) ) { LabelFAB( - label = "Progress", - expanded = expanded, onClick = { showOtherFABs = false onProgress() @@ -67,21 +69,20 @@ fun EditFAB( } AnimatedVisibility( visible = showOtherFABs, - enter = scaleIn( + enter = slideInVertically( animationSpec = bouncySpring(), - transformOrigin = TransformOrigin(1F, 0.5F) + initialOffsetY = { -(-it / 2) } ) + fadeIn( animationSpec = bouncySpring() ), - exit = scaleOut( - transformOrigin = TransformOrigin(1F, 0.5F) + exit = slideOutVertically( + animationSpec = bouncySpring(), + targetOffsetY = { -(-it / 2) } ) + fadeOut( animationSpec = bouncySpring() ) ) { LabelFAB( - label = "Rating", - expanded = expanded, onClick = { showOtherFABs = false onRate() @@ -95,21 +96,20 @@ fun EditFAB( } AnimatedVisibility( visible = showOtherFABs && bsAvailable, - enter = scaleIn( + enter = slideInVertically( animationSpec = bouncySpring(), - transformOrigin = TransformOrigin(1F, 0.5F) + initialOffsetY = { -(-it / 2) } ) + fadeIn( animationSpec = bouncySpring() ), - exit = scaleOut( - transformOrigin = TransformOrigin(1F, 0.5F) + exit = slideOutVertically( + animationSpec = bouncySpring(), + targetOffsetY = { -(-it / 2) } ) + fadeOut( animationSpec = bouncySpring() ) ) { LabelFAB( - label = stringResource(SharedRes.strings.bs), - expanded = expanded, onClick = { showOtherFABs = false onBS() @@ -156,34 +156,21 @@ fun EditFAB( @Composable private fun LabelFAB( - label: String, - expanded: Boolean, onClick: () -> Unit, icon: @Composable () -> Unit ) { + var showLabel by remember { mutableStateOf(false) } + + LaunchedEffect(showLabel) { + if (!showLabel) { + showLabel = true + } + } + Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp) ) { - AnimatedVisibility( - visible = expanded, - enter = fadeIn(), - exit = fadeOut() - ) { - Surface( - onClick = onClick, - tonalElevation = 8.dp, - shadowElevation = 4.dp, - shape = RoundedCornerShape(4.dp) - ) { - Text( - modifier = Modifier.padding(4.dp), - text = label, - maxLines = 1 - ) - } - } - SmallFloatingActionButton( modifier = Modifier.padding(end = 4.dp), onClick = onClick @@ -191,6 +178,12 @@ private fun LabelFAB( icon() } } + + DisposableEffect(showLabel) { + onDispose { + showLabel = false + } + } } private fun bouncySpring() = spring( diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/component/ScheduleOverview.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/component/ScheduleOverview.kt index 2228344..af0b934 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/component/ScheduleOverview.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/component/ScheduleOverview.kt @@ -47,16 +47,6 @@ fun ScheduleOverview( style = MaterialTheme.typography.headlineMedium, fontWeight = FontWeight.Bold ) - Spacer(modifier = Modifier.weight(1f)) - IconButton( - onClick = onMoreClick, - enabled = state is AiringTodayRepository.State.Success - ) { - Icon( - imageVector = Icons.AutoMirrored.Filled.ArrowForwardIos, - contentDescription = null - ) - } } when (val current = state) { 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 4317ca7..daea17a 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 @@ -7,6 +7,7 @@ import dev.datlag.aniflow.anilist.model.Character import dev.datlag.aniflow.anilist.model.Medium import dev.datlag.aniflow.anilist.type.MediaFormat import dev.datlag.aniflow.anilist.type.MediaStatus +import dev.datlag.aniflow.other.Series import dev.datlag.aniflow.settings.model.AppSettings import dev.datlag.aniflow.ui.navigation.Component import dev.datlag.aniflow.ui.navigation.ContentHolderComponent @@ -50,6 +51,7 @@ interface MediumComponent : ContentHolderComponent { val siteUrl: Flow val bsAvailable: Boolean + val bsOptions: Flow> val dialog: Value> 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 5c7a2e6..9ac14af 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 @@ -21,6 +21,11 @@ import com.arkivanov.decompose.extensions.compose.subscribeAsState 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 +import com.maxkeppeler.sheets.option.models.Option +import com.maxkeppeler.sheets.option.models.OptionConfig +import com.maxkeppeler.sheets.option.models.OptionSelection import com.maxkeppeler.sheets.rating.RatingDialog import com.maxkeppeler.sheets.rating.models.RatingBody import com.maxkeppeler.sheets.rating.models.RatingConfig @@ -40,6 +45,8 @@ import dev.datlag.aniflow.other.UserHelper import dev.datlag.aniflow.ui.custom.EditFAB import dev.datlag.aniflow.ui.navigation.screen.medium.component.* import dev.datlag.tooling.decompose.lifecycle.collectAsStateWithLifecycle +import dev.icerock.moko.resources.compose.painterResource +import io.github.aakira.napier.Napier import kotlinx.coroutines.launch import org.kodein.di.instance @@ -87,6 +94,9 @@ fun MediumScreen(component: MediumComponent) { floatingActionButton = { val userRating by component.rating.collectAsStateWithLifecycle(-1) val ratingState = rememberUseCaseState() + val bsState = rememberUseCaseState() + + val bsOptions by component.bsOptions.collectAsStateWithLifecycle(emptySet()) val alreadyAdded by component.alreadyAdded.collectAsStateWithLifecycle( component.initialMedium.entry != null @@ -116,6 +126,29 @@ fun MediumScreen(component: MediumComponent) { ) ) + OptionDialog( + state = bsState, + selection = OptionSelection.Single( + options = bsOptions.map { + Option( + titleText = it.title + ) + }, + onSelectOption = { option, _ -> + Napier.e("Selected: ${bsOptions.toList()[option]}") + } + ), + header = Header.Default( + icon = IconSource( + painter = painterResource(SharedRes.images.bs) + ), + title = "Connect with BS" + ), + config = OptionConfig( + mode = DisplayMode.LIST + ) + ) + if (!notReleased) { val uriHandler = LocalUriHandler.current val userHelper by LocalDI.current.instance() @@ -125,7 +158,7 @@ fun MediumScreen(component: MediumComponent) { bsAvailable = component.bsAvailable, expanded = listState.isScrollingUp(), onBS = { - + bsState.show() }, onRate = { uriHandler.openUri(userHelper.loginUrl) 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 0ee9887..8fb607b 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 @@ -32,6 +32,7 @@ import dev.datlag.tooling.compose.ioDispatcher import dev.datlag.tooling.compose.withMainContext import dev.datlag.tooling.decompose.ioScope import dev.datlag.tooling.safeCast +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.* import kotlinx.datetime.Clock import kotlinx.datetime.Instant @@ -61,51 +62,62 @@ class MediumScreenComponent( it.safeCast() } - override val isAdult: Flow = mediumSuccessState.map { + @OptIn(ExperimentalCoroutinesApi::class) + override val isAdult: Flow = mediumSuccessState.mapLatest { it.medium.isAdult } override val isAdultAllowed: Flow = appSettings.adultContent - private val type: Flow = mediumSuccessState.map { + @OptIn(ExperimentalCoroutinesApi::class) + private val type: Flow = mediumSuccessState.mapLatest { it.medium.type } - override val bannerImage: Flow = mediumSuccessState.map { + @OptIn(ExperimentalCoroutinesApi::class) + override val bannerImage: Flow = mediumSuccessState.mapLatest { it.medium.bannerImage } - override val coverImage: Flow = mediumSuccessState.map { + @OptIn(ExperimentalCoroutinesApi::class) + override val coverImage: Flow = mediumSuccessState.mapLatest { it.medium.coverImage } - override val title: Flow = mediumSuccessState.map { + @OptIn(ExperimentalCoroutinesApi::class) + override val title: Flow = mediumSuccessState.mapLatest { it.medium.title } - override val description: Flow = mediumSuccessState.map { + @OptIn(ExperimentalCoroutinesApi::class) + override val description: Flow = mediumSuccessState.mapLatest { it.medium.description?.ifBlank { null } } override val translatedDescription: MutableStateFlow = MutableStateFlow(null) - override val genres: Flow> = mediumSuccessState.map { + @OptIn(ExperimentalCoroutinesApi::class) + override val genres: Flow> = mediumSuccessState.mapLatest { it.medium.genres }.mapNotEmpty() - override val format: Flow = mediumSuccessState.map { + @OptIn(ExperimentalCoroutinesApi::class) + override val format: Flow = mediumSuccessState.mapLatest { it.medium.format } - override val episodes: Flow = mediumSuccessState.map { + @OptIn(ExperimentalCoroutinesApi::class) + override val episodes: Flow = mediumSuccessState.mapLatest { it.medium.episodes }.distinctUntilChanged() - override val duration: Flow = mediumSuccessState.map { + @OptIn(ExperimentalCoroutinesApi::class) + override val duration: Flow = mediumSuccessState.mapLatest { it.medium.avgEpisodeDurationInMin }.distinctUntilChanged() - override val status: Flow = mediumSuccessState.map { + @OptIn(ExperimentalCoroutinesApi::class) + override val status: Flow = mediumSuccessState.mapLatest { it.medium.status }.distinctUntilChanged() @@ -121,13 +133,15 @@ class MediumScreenComponent( it.medium.averageScore.asNullIf(-1) }.distinctUntilChanged() - override val characters: Flow> = mediumSuccessState.map { + @OptIn(ExperimentalCoroutinesApi::class) + override val characters: Flow> = mediumSuccessState.mapLatest { it.medium.characters }.mapNotEmpty() private val changedRating: MutableStateFlow = MutableStateFlow(initialMedium.entry?.score?.toInt() ?: -1) + @OptIn(ExperimentalCoroutinesApi::class) override val rating: Flow = combine( - mediumSuccessState.map { + mediumSuccessState.mapLatest { it.medium.entry?.score?.toInt() }, changedRating @@ -139,23 +153,28 @@ class MediumScreenComponent( } } - override val trailer: Flow = mediumSuccessState.map { + @OptIn(ExperimentalCoroutinesApi::class) + override val trailer: Flow = mediumSuccessState.mapLatest { it.medium.trailer } - override val alreadyAdded: Flow = mediumSuccessState.map { + @OptIn(ExperimentalCoroutinesApi::class) + override val alreadyAdded: Flow = mediumSuccessState.mapLatest { it.medium.entry != null } - override val isFavorite: Flow = mediumSuccessState.map { + @OptIn(ExperimentalCoroutinesApi::class) + override val isFavorite: Flow = mediumSuccessState.mapLatest { it.medium.isFavorite } - override val isFavoriteBlocked: Flow = mediumSuccessState.map { + @OptIn(ExperimentalCoroutinesApi::class) + override val isFavoriteBlocked: Flow = mediumSuccessState.mapLatest { it.medium.isFavoriteBlocked } - override val siteUrl: Flow = mediumSuccessState.map { + @OptIn(ExperimentalCoroutinesApi::class) + override val siteUrl: Flow = mediumSuccessState.mapLatest { it.medium.siteUrl } @@ -164,6 +183,11 @@ class MediumScreenComponent( override val bsAvailable: Boolean get() = burningSeriesResolver.isAvailable + @OptIn(ExperimentalCoroutinesApi::class) + override val bsOptions = title.mapLatest { + burningSeriesResolver.resolveByName(it.english, it.romaji) + } + private val dialogNavigation = SlotNavigation() override val dialog: Value> = childSlot( source = dialogNavigation, 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 c828041..1a3ad50 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 @@ -94,20 +94,20 @@ fun NekosScreen(component: NekosComponent) { selection = OptionSelection.Single( options = listOf( Option( - titleText = "Safe", + titleText = stringResource(SharedRes.strings.safe), selected = rating is Rating.Safe, ), Option( - titleText = "Suggestive", + titleText = stringResource(SharedRes.strings.suggestive), selected = rating is Rating.Suggestive, ), Option( - titleText = "Borderline", + titleText = stringResource(SharedRes.strings.borderline), selected = rating is Rating.Borderline, disabled = !adultContent ), Option( - titleText = "Explicit", + titleText = stringResource(SharedRes.strings.explicit), selected = rating is Rating.Explicit, disabled = !adultContent ) @@ -134,7 +134,7 @@ fun NekosScreen(component: NekosComponent) { ) }, text = { - Text(text = "Filter") + Text(text = stringResource(SharedRes.strings.filter)) } ) } @@ -181,7 +181,6 @@ fun NekosScreen(component: NekosComponent) { Card( modifier = Modifier.animateItemPlacement(), onClick = { - Napier.e(it.toString()) uriHandler.openUri(it.imageUrl ?: it.sampleUrl ?: "") }, ) { diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/settings/SettingsScreen.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/settings/SettingsScreen.kt index b028fd8..7417e41 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/settings/SettingsScreen.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/settings/SettingsScreen.kt @@ -193,7 +193,9 @@ fun SettingsScreen(component: SettingsComponent) { AnimatedVisibility( visible = clicked >= 1, ) { - Badge { + Badge( + containerColor = MaterialTheme.colorScheme.tertiaryContainer + ) { Text(text = "$clicked") } } diff --git a/composeApp/src/commonMain/moko-resources/base/strings.xml b/composeApp/src/commonMain/moko-resources/base/strings.xml index a94486b..5de154c 100644 --- a/composeApp/src/commonMain/moko-resources/base/strings.xml +++ b/composeApp/src/commonMain/moko-resources/base/strings.xml @@ -58,4 +58,9 @@ Profile Favorites Nekos API + Filter + Safe + Suggestive + Borderline + Explicit 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 c4ff0bc..4d7205e 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 @@ -10,7 +10,9 @@ actual class BurningSeriesResolver { return emptySet() } - actual fun resolveByName(english: String?, romaji: String?) { } + actual fun resolveByName(english: String?, romaji: String?): Set { + return emptySet() + } actual fun close() { } } \ No newline at end of file From e97509f608e7ed3196e712fba3ef31ae0094c40e Mon Sep 17 00:00:00 2001 From: DatLag Date: Fri, 10 May 2024 16:14:06 +0200 Subject: [PATCH 13/23] commonize home section list size --- .../aniflow/anilist/PopularNextSeasonRepository.kt | 2 +- .../datlag/aniflow/anilist/PopularSeasonRepository.kt | 2 +- .../aniflow/ui/navigation/screen/home/HomeScreen.kt | 6 +----- .../screen/home/component/DefaultOverview.kt | 11 ----------- .../screen/home/component/ScheduleOverview.kt | 1 - 5 files changed, 3 insertions(+), 19 deletions(-) diff --git a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/PopularNextSeasonRepository.kt b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/PopularNextSeasonRepository.kt index 3a291c6..35ba539 100644 --- a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/PopularNextSeasonRepository.kt +++ b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/PopularNextSeasonRepository.kt @@ -84,7 +84,7 @@ class PopularNextSeasonRepository( ) { fun toGraphQL() = SeasonQuery( page = Optional.present(page), - perPage = Optional.present(10), + perPage = Optional.present(20), adultContent = if (nsfw) { Optional.absent() } else { diff --git a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/PopularSeasonRepository.kt b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/PopularSeasonRepository.kt index 4d4750c..120f9f9 100644 --- a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/PopularSeasonRepository.kt +++ b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/PopularSeasonRepository.kt @@ -88,7 +88,7 @@ class PopularSeasonRepository( ) { fun toGraphQL() = SeasonQuery( page = Optional.present(page), - perPage = Optional.present(10), + perPage = Optional.present(20), adultContent = if (nsfw) { Optional.absent() } else { diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeScreen.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeScreen.kt index 33d61ae..2a4ba5b 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeScreen.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeScreen.kt @@ -122,7 +122,7 @@ fun HomeScreen(component: HomeComponent) { onClick = { imagePicker.launch() }, - expanded = listState.isScrollingUp(), + expanded = listState.isScrollingUp() && listState.canScrollForward, icon = { Icon( imageVector = Icons.Filled.CameraEnhance, @@ -167,7 +167,6 @@ fun HomeScreen(component: HomeComponent) { item { ScheduleOverview( flow = component.airing, - onMoreClick = { }, onMediumClick = component::details ) } @@ -176,7 +175,6 @@ fun HomeScreen(component: HomeComponent) { DefaultOverview( title = "Trending", flow = component.trending, - onMoreClick = { }, onMediumClick = component::details ) } @@ -184,7 +182,6 @@ fun HomeScreen(component: HomeComponent) { DefaultOverview( title = "Popular", flow = component.popularNow, - onMoreClick = { }, onMediumClick = component::details ) } @@ -193,7 +190,6 @@ fun HomeScreen(component: HomeComponent) { DefaultOverview( title = "Popular Next", flow = component.popularNext, - onMoreClick = { }, onMediumClick = component::details ) } diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/component/DefaultOverview.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/component/DefaultOverview.kt index 2cdf9dd..23c6a22 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/component/DefaultOverview.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/component/DefaultOverview.kt @@ -26,7 +26,6 @@ import kotlinx.coroutines.flow.Flow fun DefaultOverview( title: String, flow: Flow, - onMoreClick: () -> Unit, onMediumClick: (Medium) -> Unit, ) { Column( @@ -45,16 +44,6 @@ fun DefaultOverview( style = MaterialTheme.typography.headlineMedium, fontWeight = FontWeight.Bold ) - Spacer(modifier = Modifier.weight(1f)) - IconButton( - onClick = onMoreClick, - enabled = state.isSuccess - ) { - Icon( - imageVector = Icons.AutoMirrored.Filled.ArrowForwardIos, - contentDescription = null - ) - } } when (val current = state) { diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/component/ScheduleOverview.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/component/ScheduleOverview.kt index af0b934..1310ec0 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/component/ScheduleOverview.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/component/ScheduleOverview.kt @@ -28,7 +28,6 @@ import kotlinx.coroutines.flow.Flow @Composable fun ScheduleOverview( flow: Flow, - onMoreClick: () -> Unit, onMediumClick: (Medium) -> Unit ) { Column( From 070e271b2b46d1567aa2840b2df41539e478d988 Mon Sep 17 00:00:00 2001 From: DatLag Date: Fri, 10 May 2024 17:23:13 +0200 Subject: [PATCH 14/23] prepare edit dialog --- .../commonMain/graphql/AiringQuery.graphql | 3 +- .../graphql/MediaListEntryQuery.graphql | 3 +- .../commonMain/graphql/MediumQuery.graphql | 3 +- .../commonMain/graphql/SeasonQuery.graphql | 3 +- .../commonMain/graphql/TrendingQuery.graphql | 3 +- .../datlag/aniflow/anilist/model/Medium.kt | 20 ++--- .../dev/datlag/aniflow/common/ExtendMedium.kt | 30 ++++++- .../datlag/aniflow/module/NetworkModule.kt | 8 +- .../navigation/screen/medium/DialogConfig.kt | 3 + .../screen/medium/MediumComponent.kt | 7 +- .../navigation/screen/medium/MediumScreen.kt | 79 +++---------------- .../screen/medium/MediumScreenComponent.kt | 23 ++++-- .../medium/dialog/edit/EditComponent.kt | 11 +++ .../screen/medium/dialog/edit/EditDialog.kt | 30 +++++++ .../medium/dialog/edit/EditDialogComponent.kt | 41 ++++++++++ .../moko-resources/base/strings.xml | 7 ++ 16 files changed, 176 insertions(+), 98 deletions(-) create mode 100644 composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/dialog/edit/EditComponent.kt create mode 100644 composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/dialog/edit/EditDialog.kt create mode 100644 composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/dialog/edit/EditDialogComponent.kt diff --git a/anilist/src/commonMain/graphql/AiringQuery.graphql b/anilist/src/commonMain/graphql/AiringQuery.graphql index 0f9b0c6..4897c1f 100644 --- a/anilist/src/commonMain/graphql/AiringQuery.graphql +++ b/anilist/src/commonMain/graphql/AiringQuery.graphql @@ -79,7 +79,8 @@ query AiringQuery( } }, mediaListEntry { - score(format: POINT_5) + score(format: POINT_5), + status }, trailer { id, diff --git a/anilist/src/commonMain/graphql/MediaListEntryQuery.graphql b/anilist/src/commonMain/graphql/MediaListEntryQuery.graphql index d3dd588..893e7d6 100644 --- a/anilist/src/commonMain/graphql/MediaListEntryQuery.graphql +++ b/anilist/src/commonMain/graphql/MediaListEntryQuery.graphql @@ -1,5 +1,6 @@ query MediaListEntryQuery($id: Int) { MediaList(mediaId: $id) { - score(format: POINT_5) + score(format: POINT_5), + status } } \ No newline at end of file diff --git a/anilist/src/commonMain/graphql/MediumQuery.graphql b/anilist/src/commonMain/graphql/MediumQuery.graphql index 1c15f19..a4f8062 100644 --- a/anilist/src/commonMain/graphql/MediumQuery.graphql +++ b/anilist/src/commonMain/graphql/MediumQuery.graphql @@ -68,7 +68,8 @@ query MediumQuery($id: Int, $statusVersion: Int, $html: Boolean) { } }, mediaListEntry { - score(format: POINT_5) + score(format: POINT_5), + status }, trailer { id, diff --git a/anilist/src/commonMain/graphql/SeasonQuery.graphql b/anilist/src/commonMain/graphql/SeasonQuery.graphql index 7209818..aac6dbb 100644 --- a/anilist/src/commonMain/graphql/SeasonQuery.graphql +++ b/anilist/src/commonMain/graphql/SeasonQuery.graphql @@ -80,7 +80,8 @@ query SeasonQuery( } }, mediaListEntry { - score(format: POINT_5) + score(format: POINT_5), + status }, trailer { id, diff --git a/anilist/src/commonMain/graphql/TrendingQuery.graphql b/anilist/src/commonMain/graphql/TrendingQuery.graphql index df37538..401b475 100644 --- a/anilist/src/commonMain/graphql/TrendingQuery.graphql +++ b/anilist/src/commonMain/graphql/TrendingQuery.graphql @@ -83,7 +83,8 @@ query TrendingQuery( } }, mediaListEntry { - score(format: POINT_5) + score(format: POINT_5), + status }, trailer { id, 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 53db389..d1e38b4 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 @@ -3,10 +3,7 @@ package dev.datlag.aniflow.anilist.model import dev.datlag.aniflow.anilist.* import dev.datlag.aniflow.anilist.AdultContent 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.MediaType +import dev.datlag.aniflow.anilist.type.* import dev.datlag.aniflow.model.ifValue import dev.datlag.aniflow.model.toInt import kotlinx.datetime.Clock @@ -379,22 +376,27 @@ data class Medium( @Serializable data class Entry( - val score: Double? + val score: Double?, + val status: MediaListStatus ) { constructor(entry: MediumQuery.MediaListEntry) : this( - score = entry.score + score = entry.score, + status = entry.status ?: MediaListStatus.UNKNOWN__ ) constructor(entry: TrendingQuery.MediaListEntry) : this( - score = entry.score + score = entry.score, + status = entry.status ?: MediaListStatus.UNKNOWN__ ) constructor(entry: AiringQuery.MediaListEntry) : this( - score = entry.score + score = entry.score, + status = entry.status ?: MediaListStatus.UNKNOWN__ ) constructor(entry: SeasonQuery.MediaListEntry) : this( - score = entry.score + score = entry.score, + status = entry.status ?: MediaListStatus.UNKNOWN__ ) } 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 1713b87..d6144ea 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/common/ExtendMedium.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/common/ExtendMedium.kt @@ -1,14 +1,14 @@ package dev.datlag.aniflow.common +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.* import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import dev.datlag.aniflow.LocalDI import dev.datlag.aniflow.SharedRes import dev.datlag.aniflow.anilist.model.Character import dev.datlag.aniflow.anilist.model.Medium -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.* import dev.datlag.aniflow.settings.Settings import dev.datlag.aniflow.settings.model.AppSettings import dev.datlag.aniflow.trace.model.SearchResponse @@ -149,4 +149,26 @@ fun SearchResponse.Result.AniList.asMedium(): Medium { _isAdult = this.isAdult, title = this.title.asMediumTitle() ) -} \ No newline at end of file +} + +fun MediaListStatus.icon() = when (this) { + MediaListStatus.CURRENT -> Icons.Rounded.Edit + MediaListStatus.COMPLETED -> Icons.Rounded.Check + MediaListStatus.PAUSED -> Icons.Rounded.Pause + MediaListStatus.DROPPED -> Icons.Rounded.Close + MediaListStatus.PLANNING -> Icons.Rounded.WatchLater + MediaListStatus.REPEATING -> Icons.Rounded.Replay + else -> Icons.Rounded.Add +} + +fun MediaListStatus.stringRes(isManga: Boolean) = when (this) { + MediaListStatus.CURRENT -> if (isManga) SharedRes.strings.reading else SharedRes.strings.watching + MediaListStatus.COMPLETED -> SharedRes.strings.completed + MediaListStatus.PAUSED -> SharedRes.strings.paused + MediaListStatus.DROPPED -> SharedRes.strings.dropped + MediaListStatus.PLANNING -> SharedRes.strings.planning + MediaListStatus.REPEATING -> SharedRes.strings.repeating + else -> SharedRes.strings.add +} + +fun MediaListStatus.stringRes(type: MediaType) = this.stringRes(type == MediaType.MANGA) \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/module/NetworkModule.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/module/NetworkModule.kt index 666abd2..9746c24 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/module/NetworkModule.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/module/NetworkModule.kt @@ -165,14 +165,14 @@ data object NetworkModule { } bindSingleton { CharacterRepository( - client = instance(Constants.AniList.APOLLO_CLIENT), - fallbackClient = instance(Constants.AniList.FALLBACK_APOLLO_CLIENT), + client = instance(Constants.AniList.APOLLO_CLIENT).newBuilder().fetchPolicy(FetchPolicy.NetworkFirst).build(), + fallbackClient = instance(Constants.AniList.FALLBACK_APOLLO_CLIENT).newBuilder().fetchPolicy(FetchPolicy.NetworkFirst).build(), ) } bindSingleton { MediumRepository( - client = instance(Constants.AniList.APOLLO_CLIENT), - fallbackClient = instance(Constants.AniList.FALLBACK_APOLLO_CLIENT) + client = instance(Constants.AniList.APOLLO_CLIENT).newBuilder().fetchPolicy(FetchPolicy.NetworkFirst).build(), + fallbackClient = instance(Constants.AniList.FALLBACK_APOLLO_CLIENT).newBuilder().fetchPolicy(FetchPolicy.NetworkFirst).build() ) } bindSingleton { diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/DialogConfig.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/DialogConfig.kt index 70f41f9..1cb21b7 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/DialogConfig.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/DialogConfig.kt @@ -10,4 +10,7 @@ sealed class DialogConfig { data class Character( val initial: Char ) : DialogConfig() + + @Serializable + data object Edit : DialogConfig() } \ No newline at end of file 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 daea17a..d91afa3 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 @@ -6,7 +6,9 @@ import dev.datlag.aniflow.anilist.MediumRepository import dev.datlag.aniflow.anilist.model.Character import dev.datlag.aniflow.anilist.model.Medium import dev.datlag.aniflow.anilist.type.MediaFormat +import dev.datlag.aniflow.anilist.type.MediaListStatus import dev.datlag.aniflow.anilist.type.MediaStatus +import dev.datlag.aniflow.anilist.type.MediaType import dev.datlag.aniflow.other.Series import dev.datlag.aniflow.settings.model.AppSettings import dev.datlag.aniflow.ui.navigation.Component @@ -50,8 +52,8 @@ interface MediumComponent : ContentHolderComponent { val isFavoriteBlocked: Flow val siteUrl: Flow - val bsAvailable: Boolean - val bsOptions: Flow> + val type: Flow + val listStatus: Flow val dialog: Value> @@ -64,4 +66,5 @@ interface MediumComponent : ContentHolderComponent { fun descriptionTranslation(text: String?) fun showCharacter(character: Character) fun toggleFavorite() + fun edit() } \ No newline at end of file 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 9ac14af..8a9c9ba 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 @@ -38,6 +38,7 @@ import dev.datlag.aniflow.LocalDI import dev.datlag.aniflow.LocalHaze import dev.datlag.aniflow.LocalPaddingValues import dev.datlag.aniflow.SharedRes +import dev.datlag.aniflow.anilist.type.MediaListStatus import dev.datlag.aniflow.anilist.type.MediaStatus import dev.datlag.aniflow.common.* import dev.datlag.aniflow.other.StateSaver @@ -46,6 +47,7 @@ import dev.datlag.aniflow.ui.custom.EditFAB import dev.datlag.aniflow.ui.navigation.screen.medium.component.* import dev.datlag.tooling.decompose.lifecycle.collectAsStateWithLifecycle import dev.icerock.moko.resources.compose.painterResource +import dev.icerock.moko.resources.compose.stringResource import io.github.aakira.napier.Napier import kotlinx.coroutines.launch import org.kodein.di.instance @@ -92,79 +94,24 @@ fun MediumScreen(component: MediumComponent) { ) }, floatingActionButton = { - val userRating by component.rating.collectAsStateWithLifecycle(-1) - val ratingState = rememberUseCaseState() - val bsState = rememberUseCaseState() - - val bsOptions by component.bsOptions.collectAsStateWithLifecycle(emptySet()) - - val alreadyAdded by component.alreadyAdded.collectAsStateWithLifecycle( - component.initialMedium.entry != null - ) val notReleased by component.status.mapCollect(component.initialMedium.status) { it == MediaStatus.UNKNOWN__ || it == MediaStatus.NOT_YET_RELEASED } - RatingDialog( - state = ratingState, - selection = RatingSelection( - onSelectRating = { rating, _ -> - component.rate(rating) - } - ), - header = Header.Default( - title = "Rate this Anime", - icon = IconSource(Icons.Filled.Star) - ), - body = RatingBody.Default( - bodyText = "" - ), - config = RatingConfig( - ratingOptionsCount = 5, - ratingOptionsSelected = userRating.takeIf { it > 0 }, - ratingZeroValid = true - ) - ) - - OptionDialog( - state = bsState, - selection = OptionSelection.Single( - options = bsOptions.map { - Option( - titleText = it.title - ) - }, - onSelectOption = { option, _ -> - Napier.e("Selected: ${bsOptions.toList()[option]}") - } - ), - header = Header.Default( - icon = IconSource( - painter = painterResource(SharedRes.images.bs) - ), - title = "Connect with BS" - ), - config = OptionConfig( - mode = DisplayMode.LIST - ) - ) - if (!notReleased) { - val uriHandler = LocalUriHandler.current - val userHelper by LocalDI.current.instance() + val status by component.listStatus.collectAsStateWithLifecycle(component.initialMedium.entry?.status ?: MediaListStatus.UNKNOWN__) + val type by component.type.collectAsStateWithLifecycle(component.initialMedium.type) - EditFAB( - displayAdd = !alreadyAdded, - bsAvailable = component.bsAvailable, - expanded = listState.isScrollingUp(), - onBS = { - bsState.show() - }, - onRate = { - uriHandler.openUri(userHelper.loginUrl) + ExtendedFloatingActionButton( + onClick = { component.edit() }, + icon = { + Icon( + imageVector = status.icon(), + contentDescription = null, + ) }, - onProgress = { - // ratingState.show() + text = { + Text(text = stringResource(status.stringRes(type))) } ) } 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 8fb607b..020e20e 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 @@ -14,6 +14,7 @@ import dev.datlag.aniflow.anilist.* import dev.datlag.aniflow.anilist.model.Character import dev.datlag.aniflow.anilist.model.Medium import dev.datlag.aniflow.anilist.type.MediaFormat +import dev.datlag.aniflow.anilist.type.MediaListStatus import dev.datlag.aniflow.anilist.type.MediaStatus import dev.datlag.aniflow.anilist.type.MediaType import dev.datlag.aniflow.common.* @@ -26,6 +27,7 @@ import dev.datlag.aniflow.settings.model.AppSettings import dev.datlag.aniflow.settings.model.CharLanguage import dev.datlag.aniflow.ui.navigation.DialogComponent import dev.datlag.aniflow.ui.navigation.screen.medium.dialog.character.CharacterDialogComponent +import dev.datlag.aniflow.ui.navigation.screen.medium.dialog.edit.EditDialogComponent import dev.datlag.tooling.alsoTrue import dev.datlag.tooling.async.suspendCatching import dev.datlag.tooling.compose.ioDispatcher @@ -70,7 +72,7 @@ class MediumScreenComponent( override val isAdultAllowed: Flow = appSettings.adultContent @OptIn(ExperimentalCoroutinesApi::class) - private val type: Flow = mediumSuccessState.mapLatest { + override val type: Flow = mediumSuccessState.mapLatest { it.medium.type } @@ -178,14 +180,9 @@ class MediumScreenComponent( it.medium.siteUrl } - private val burningSeriesResolver by di.instance() - - override val bsAvailable: Boolean - get() = burningSeriesResolver.isAvailable - @OptIn(ExperimentalCoroutinesApi::class) - override val bsOptions = title.mapLatest { - burningSeriesResolver.resolveByName(it.english, it.romaji) + override val listStatus: Flow = mediumSuccessState.mapLatest { + it.medium.entry?.status ?: MediaListStatus.UNKNOWN__ } private val dialogNavigation = SlotNavigation() @@ -200,6 +197,12 @@ class MediumScreenComponent( initialChar = config.initial, onDismiss = dialogNavigation::dismiss ) + is DialogConfig.Edit -> EditDialogComponent( + componentContext = context, + di = di, + titleFlow = title, + onDismiss = dialogNavigation::dismiss + ) } } @@ -294,4 +297,8 @@ class MediumScreenComponent( aniListClient.mutation(mutation).execute() } } + + override fun edit() { + dialogNavigation.activate(DialogConfig.Edit) + } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/dialog/edit/EditComponent.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/dialog/edit/EditComponent.kt new file mode 100644 index 0000000..0381a4c --- /dev/null +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/dialog/edit/EditComponent.kt @@ -0,0 +1,11 @@ +package dev.datlag.aniflow.ui.navigation.screen.medium.dialog.edit + +import dev.datlag.aniflow.other.Series +import dev.datlag.aniflow.ui.navigation.DialogComponent +import kotlinx.coroutines.flow.Flow + +interface EditComponent : DialogComponent { + + val bsAvailable: Boolean + val bsOptions: Flow> +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/dialog/edit/EditDialog.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/dialog/edit/EditDialog.kt new file mode 100644 index 0000000..dbaf710 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/dialog/edit/EditDialog.kt @@ -0,0 +1,30 @@ +package dev.datlag.aniflow.ui.navigation.screen.medium.dialog.edit + +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import dev.datlag.aniflow.LocalEdgeToEdge + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun EditDialog(component: EditComponent) { + val sheetState = rememberModalBottomSheetState() + val (insets, bottomPadding) = if (LocalEdgeToEdge.current) { + WindowInsets( + left = 0, + top = 0, + right = 0, + bottom = 0 + ) to BottomSheetDefaults.windowInsets.only(WindowInsetsSides.Bottom).asPaddingValues() + } else { + BottomSheetDefaults.windowInsets to PaddingValues() + } + + ModalBottomSheet( + onDismissRequest = component::dismiss, + windowInsets = insets, + sheetState = sheetState + ) { + Text(text = "Edit Dialog") + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/dialog/edit/EditDialogComponent.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/dialog/edit/EditDialogComponent.kt new file mode 100644 index 0000000..4e04c0f --- /dev/null +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/dialog/edit/EditDialogComponent.kt @@ -0,0 +1,41 @@ +package dev.datlag.aniflow.ui.navigation.screen.medium.dialog.edit + +import androidx.compose.runtime.Composable +import com.arkivanov.decompose.ComponentContext +import dev.datlag.aniflow.anilist.model.Medium +import dev.datlag.aniflow.common.onRender +import dev.datlag.aniflow.other.BurningSeriesResolver +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.mapLatest +import org.kodein.di.DI +import org.kodein.di.instance + +class EditDialogComponent( + componentContext: ComponentContext, + override val di: DI, + private val titleFlow: Flow, + private val onDismiss: () -> Unit +) : EditComponent, ComponentContext by componentContext { + + private val burningSeriesResolver by instance() + + override val bsAvailable: Boolean + get() = burningSeriesResolver.isAvailable + + @OptIn(ExperimentalCoroutinesApi::class) + override val bsOptions = titleFlow.mapLatest { + burningSeriesResolver.resolveByName(it.english, it.romaji) + } + + @Composable + override fun render() { + onRender { + EditDialog(this) + } + } + + override fun dismiss() { + onDismiss() + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/moko-resources/base/strings.xml b/composeApp/src/commonMain/moko-resources/base/strings.xml index 5de154c..acc80f7 100644 --- a/composeApp/src/commonMain/moko-resources/base/strings.xml +++ b/composeApp/src/commonMain/moko-resources/base/strings.xml @@ -63,4 +63,11 @@ Suggestive Borderline Explicit + Watching + Reading + Planning + Completed + Dropped + Paused + Repeating From dd8a366b8badbd5fd6bd4d7ce7f5ac05bc408a41 Mon Sep 17 00:00:00 2001 From: DatLag Date: Fri, 10 May 2024 17:33:56 +0200 Subject: [PATCH 15/23] map icons by media type/format --- .../dev/datlag/aniflow/common/ExtendMedium.kt | 8 ++++++++ .../screen/medium/component/CoverSection.kt | 15 ++++++++++----- 2 files changed, 18 insertions(+), 5 deletions(-) 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 d6144ea..d3c25b6 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/common/ExtendMedium.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/common/ExtendMedium.kt @@ -1,9 +1,11 @@ package dev.datlag.aniflow.common import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.rounded.MenuBook import androidx.compose.material.icons.rounded.* import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.ui.graphics.vector.ImageVector import dev.datlag.aniflow.LocalDI import dev.datlag.aniflow.SharedRes import dev.datlag.aniflow.anilist.model.Character @@ -62,6 +64,12 @@ fun Medium.Title.notPreferred(setting: SettingsTitle?): String? { } } +fun MediaFormat.icon(): ImageVector = when (this) { + MediaFormat.MANGA, MediaFormat.NOVEL, MediaFormat.ONE_SHOT -> Icons.AutoMirrored.Rounded.MenuBook + MediaFormat.MUSIC -> Icons.Rounded.MusicNote + else -> Icons.Rounded.OndemandVideo +} + fun MediaFormat.text(): StringResource { return when (this) { MediaFormat.TV -> SharedRes.strings.media_format_tv diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/component/CoverSection.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/component/CoverSection.kt index a3b3fbf..87e4ef1 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/component/CoverSection.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/component/CoverSection.kt @@ -3,10 +3,14 @@ package dev.datlag.aniflow.ui.navigation.screen.medium.component import androidx.compose.foundation.layout.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.List +import androidx.compose.material.icons.automirrored.rounded.List import androidx.compose.material.icons.filled.NoAdultContent import androidx.compose.material.icons.filled.OndemandVideo import androidx.compose.material.icons.filled.RssFeed import androidx.compose.material.icons.filled.Timelapse +import androidx.compose.material.icons.rounded.NoAdultContent +import androidx.compose.material.icons.rounded.RssFeed +import androidx.compose.material.icons.rounded.Timelapse import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -22,6 +26,7 @@ import dev.datlag.aniflow.SharedRes import dev.datlag.aniflow.anilist.model.Medium import dev.datlag.aniflow.anilist.type.MediaFormat import dev.datlag.aniflow.anilist.type.MediaStatus +import dev.datlag.aniflow.common.icon import dev.datlag.aniflow.common.text import dev.datlag.tooling.compose.ifTrue import dev.datlag.tooling.decompose.lifecycle.collectAsStateWithLifecycle @@ -95,7 +100,7 @@ fun CoverSection( horizontalArrangement = Arrangement.spacedBy(8.dp) ) { Icon( - imageVector = Icons.Default.OndemandVideo, + imageVector = format.icon(), contentDescription = null ) Text(text = stringResource(format.text())) @@ -107,7 +112,7 @@ fun CoverSection( horizontalArrangement = Arrangement.spacedBy(8.dp) ) { Icon( - imageVector = Icons.Default.NoAdultContent, + imageVector = Icons.Rounded.NoAdultContent, contentDescription = null ) Text(text = stringResource(SharedRes.strings.explicit)) @@ -120,7 +125,7 @@ fun CoverSection( horizontalArrangement = Arrangement.spacedBy(8.dp) ) { Icon( - imageVector = Icons.AutoMirrored.Filled.List, + imageVector = Icons.AutoMirrored.Rounded.List, contentDescription = null ) Text(text = "$episodes Episodes") @@ -133,7 +138,7 @@ fun CoverSection( horizontalArrangement = Arrangement.spacedBy(8.dp) ) { Icon( - imageVector = Icons.Default.Timelapse, + imageVector = Icons.Rounded.Timelapse, contentDescription = null ) Text(text = "${duration}min / Episode") @@ -145,7 +150,7 @@ fun CoverSection( horizontalArrangement = Arrangement.spacedBy(8.dp) ) { Icon( - imageVector = Icons.Default.RssFeed, + imageVector = Icons.Rounded.RssFeed, contentDescription = null ) Text(text = stringResource(status.text())) From 679bf7e001246a8598110e51c901396e934e2529 Mon Sep 17 00:00:00 2001 From: DatLag Date: Fri, 10 May 2024 19:06:39 +0200 Subject: [PATCH 16/23] move settings to bottom sheet --- .../component/DomainSection.android.kt | 2 +- .../aniflow/ui/navigation/RootComponent.kt | 15 ++--- .../aniflow/ui/navigation/RootConfig.kt | 3 - .../ui/navigation/screen/home/DialogConfig.kt | 10 +++ .../navigation/screen/home/HomeComponent.kt | 5 ++ .../ui/navigation/screen/home/HomeScreen.kt | 5 ++ .../screen/home/HomeScreenComponent.kt | 25 ++++++- .../dialog}/settings/SettingsComponent.kt | 5 +- .../dialog/settings/SettingsDialog.kt} | 43 ++++++++---- .../settings/SettingsDialogComponent.kt} | 12 ++-- .../settings/component/AdultSection.kt | 2 +- .../settings/component/CharacterSection.kt | 2 +- .../settings/component/ColorSection.kt | 2 +- .../settings/component/DomainSection.kt | 2 +- .../settings/component/TitleSection.kt | 2 +- .../dialog}/settings/component/UserSection.kt | 65 +++++++++++++------ .../dialog/character/CharacterDialog.kt | 3 +- .../settings/component/DomainSection.ios.kt | 2 +- 18 files changed, 143 insertions(+), 62 deletions(-) rename composeApp/src/androidMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/{ => home/dialog}/settings/component/DomainSection.android.kt (96%) create mode 100644 composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/DialogConfig.kt rename composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/{ => home/dialog}/settings/SettingsComponent.kt (82%) rename composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/{settings/SettingsScreen.kt => home/dialog/settings/SettingsDialog.kt} (85%) rename composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/{settings/SettingsScreenComponent.kt => home/dialog/settings/SettingsDialogComponent.kt} (91%) rename composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/{ => home/dialog}/settings/component/AdultSection.kt (96%) rename composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/{ => home/dialog}/settings/component/CharacterSection.kt (97%) rename composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/{ => home/dialog}/settings/component/ColorSection.kt (97%) rename composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/{ => home/dialog}/settings/component/DomainSection.kt (66%) rename composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/{ => home/dialog}/settings/component/TitleSection.kt (97%) rename composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/{ => home/dialog}/settings/component/UserSection.kt (51%) rename composeApp/src/iosMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/{ => home/dialog}/settings/component/DomainSection.ios.kt (64%) diff --git a/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/settings/component/DomainSection.android.kt b/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/dialog/settings/component/DomainSection.android.kt similarity index 96% rename from composeApp/src/androidMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/settings/component/DomainSection.android.kt rename to composeApp/src/androidMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/dialog/settings/component/DomainSection.android.kt index 2add183..891b37c 100644 --- a/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/settings/component/DomainSection.android.kt +++ b/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/dialog/settings/component/DomainSection.android.kt @@ -1,4 +1,4 @@ -package dev.datlag.aniflow.ui.navigation.screen.settings.component +package dev.datlag.aniflow.ui.navigation.screen.home.dialog.settings.component import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row 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 157b761..f9ba5e9 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 @@ -13,7 +13,7 @@ import dev.datlag.aniflow.other.UserHelper import dev.datlag.aniflow.ui.navigation.screen.favorites.FavoritesScreenComponent import dev.datlag.aniflow.ui.navigation.screen.home.HomeScreenComponent import dev.datlag.aniflow.ui.navigation.screen.medium.MediumScreenComponent -import dev.datlag.aniflow.ui.navigation.screen.settings.SettingsScreenComponent +import dev.datlag.aniflow.ui.navigation.screen.home.dialog.settings.SettingsDialogComponent import dev.datlag.aniflow.ui.navigation.screen.nekos.NekosScreenComponent import org.kodein.di.DI import org.kodein.di.instance @@ -43,14 +43,14 @@ class RootComponent( onMediumDetails = { navigation.push(RootConfig.Details(it)) }, - onProfile = { - navigation.push(RootConfig.Settings) - }, onDiscover = { // navigation.replaceCurrent(RootConfig.Wallpaper) }, onFavorites = { navigation.replaceCurrent(RootConfig.Favorites) + }, + onNekos = { + navigation.bringToFront(RootConfig.Nekos) } ) is RootConfig.Details -> MediumScreenComponent( @@ -59,13 +59,6 @@ class RootComponent( initialMedium = rootConfig.medium, onBack = navigation::pop ) - is RootConfig.Settings -> SettingsScreenComponent( - componentContext = componentContext, - di = di, - onNekos = { - navigation.bringToFront(RootConfig.Nekos) - } - ) is RootConfig.Favorites -> FavoritesScreenComponent( componentContext = componentContext, di = di, diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/RootConfig.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/RootConfig.kt index 6ab07c1..dd55b08 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/RootConfig.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/RootConfig.kt @@ -14,9 +14,6 @@ sealed class RootConfig { constructor(id: Int) : this(Medium(id)) } - @Serializable - data object Settings : RootConfig() - @Serializable data object Favorites : RootConfig() diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/DialogConfig.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/DialogConfig.kt new file mode 100644 index 0000000..d75fdd5 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/DialogConfig.kt @@ -0,0 +1,10 @@ +package dev.datlag.aniflow.ui.navigation.screen.home + +import kotlinx.serialization.Serializable + +@Serializable +sealed class DialogConfig { + + @Serializable + data object Settings : DialogConfig() +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeComponent.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeComponent.kt index c44f942..1fed965 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeComponent.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeComponent.kt @@ -1,5 +1,7 @@ package dev.datlag.aniflow.ui.navigation.screen.home +import com.arkivanov.decompose.router.slot.ChildSlot +import com.arkivanov.decompose.value.Value import dev.datlag.aniflow.anilist.AiringTodayRepository import dev.datlag.aniflow.anilist.TrendingRepository import dev.datlag.aniflow.anilist.model.Medium @@ -8,6 +10,7 @@ import dev.datlag.aniflow.anilist.state.CollectionState import dev.datlag.aniflow.anilist.type.MediaType import dev.datlag.aniflow.trace.TraceRepository import dev.datlag.aniflow.ui.navigation.Component +import dev.datlag.aniflow.ui.navigation.DialogComponent import kotlinx.coroutines.flow.Flow interface HomeComponent : Component { @@ -21,6 +24,8 @@ interface HomeComponent : Component { val traceState: Flow + val dialog: Value> + fun viewProfile() fun viewAnime() fun viewManga() diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeScreen.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeScreen.kt index 2a4ba5b..53243c8 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeScreen.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeScreen.kt @@ -21,6 +21,7 @@ import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import com.arkivanov.decompose.extensions.compose.subscribeAsState import com.maxkeppeker.sheets.core.models.base.Header import com.maxkeppeker.sheets.core.models.base.rememberUseCaseState import com.maxkeppeler.sheets.option.OptionDialog @@ -60,6 +61,10 @@ fun HomeScreen(component: HomeComponent) { it?.let(component::trace) } + val dialogState by component.dialog.subscribeAsState() + + dialogState.child?.instance?.render() + Scaffold( modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), topBar = { diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeScreenComponent.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeScreenComponent.kt index f903ce6..9a15745 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeScreenComponent.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeScreenComponent.kt @@ -4,6 +4,8 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.remember import com.arkivanov.decompose.ComponentContext +import com.arkivanov.decompose.router.slot.* +import com.arkivanov.decompose.value.Value import dev.chrisbanes.haze.HazeState import dev.datlag.aniflow.LocalHaze import dev.datlag.aniflow.anilist.AiringTodayRepository @@ -20,6 +22,8 @@ import dev.datlag.aniflow.other.StateSaver import dev.datlag.aniflow.other.UserHelper import dev.datlag.aniflow.settings.Settings import dev.datlag.aniflow.trace.TraceRepository +import dev.datlag.aniflow.ui.navigation.DialogComponent +import dev.datlag.aniflow.ui.navigation.screen.home.dialog.settings.SettingsDialogComponent import dev.datlag.tooling.decompose.ioScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted @@ -32,9 +36,9 @@ class HomeScreenComponent( componentContext: ComponentContext, override val di: DI, private val onMediumDetails: (Medium) -> Unit, - private val onProfile: () -> Unit, private val onDiscover: () -> Unit, - private val onFavorites: () -> Unit + private val onFavorites: () -> Unit, + private val onNekos: () -> Unit ) : HomeComponent, ComponentContext by componentContext { private val appSettings by instance() @@ -90,6 +94,21 @@ class HomeScreenComponent( private val traceRepository by instance() override val traceState: Flow = traceRepository.response + private val dialogNavigation = SlotNavigation() + override val dialog: Value> = childSlot( + source = dialogNavigation, + serializer = DialogConfig.serializer() + ) { config, context -> + when (config) { + is DialogConfig.Settings -> SettingsDialogComponent( + componentContext = context, + di = di, + onNekos = onNekos, + onDismiss = dialogNavigation::dismiss + ) + } + } + init { traceRepository.clear() } @@ -108,7 +127,7 @@ class HomeScreenComponent( } override fun viewProfile() { - onProfile() + dialogNavigation.activate(DialogConfig.Settings) } override fun viewAnime() { diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/settings/SettingsComponent.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/dialog/settings/SettingsComponent.kt similarity index 82% rename from composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/settings/SettingsComponent.kt rename to composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/dialog/settings/SettingsComponent.kt index d0a93e7..c748239 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/settings/SettingsComponent.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/dialog/settings/SettingsComponent.kt @@ -1,13 +1,14 @@ -package dev.datlag.aniflow.ui.navigation.screen.settings +package dev.datlag.aniflow.ui.navigation.screen.home.dialog.settings import dev.datlag.aniflow.anilist.model.User import dev.datlag.aniflow.ui.navigation.Component +import dev.datlag.aniflow.ui.navigation.DialogComponent import kotlinx.coroutines.flow.Flow import dev.datlag.aniflow.settings.model.Color as SettingsColor import dev.datlag.aniflow.settings.model.TitleLanguage as SettingsTitle import dev.datlag.aniflow.settings.model.CharLanguage as SettingsChar -interface SettingsComponent : Component { +interface SettingsComponent : DialogComponent { val user: Flow val isLoggedIn: Flow val loginUri: String diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/settings/SettingsScreen.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/dialog/settings/SettingsDialog.kt similarity index 85% rename from composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/settings/SettingsScreen.kt rename to composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/dialog/settings/SettingsDialog.kt index 7417e41..8dab1a0 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/settings/SettingsScreen.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/dialog/settings/SettingsDialog.kt @@ -1,6 +1,8 @@ -package dev.datlag.aniflow.ui.navigation.screen.settings +package dev.datlag.aniflow.ui.navigation.screen.home.dialog.settings import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut import androidx.compose.foundation.Image import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn @@ -8,8 +10,9 @@ import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.* +import androidx.compose.material.icons.rounded.ArrowBackIosNew +import androidx.compose.material.icons.rounded.Code import androidx.compose.material3.* -import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -17,24 +20,38 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.unit.dp -import dev.chrisbanes.haze.haze -import dev.datlag.aniflow.LocalHaze -import dev.datlag.aniflow.LocalPaddingValues +import dev.datlag.aniflow.LocalEdgeToEdge import dev.datlag.aniflow.SharedRes +import dev.datlag.aniflow.common.isFullyExpandedOrTargeted import dev.datlag.aniflow.common.merge -import dev.datlag.aniflow.common.plus import dev.datlag.aniflow.other.Constants import dev.datlag.aniflow.other.StateSaver -import dev.datlag.aniflow.ui.navigation.screen.settings.component.* +import dev.datlag.aniflow.ui.navigation.screen.home.dialog.settings.component.* import dev.datlag.tooling.compose.onClick import dev.datlag.tooling.decompose.lifecycle.collectAsStateWithLifecycle import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource +@OptIn(ExperimentalMaterial3Api::class) @Composable fun SettingsScreen(component: SettingsComponent) { - Scaffold { - val padding = it.merge(PaddingValues(16.dp)) + val sheetState = rememberModalBottomSheetState() + val (insets, bottomPadding) = if (LocalEdgeToEdge.current) { + WindowInsets( + left = 0, + top = 0, + right = 0, + bottom = 0 + ) to BottomSheetDefaults.windowInsets.only(WindowInsetsSides.Bottom).asPaddingValues() + } else { + BottomSheetDefaults.windowInsets to PaddingValues() + } + + ModalBottomSheet( + onDismissRequest = component::dismiss, + windowInsets = insets, + sheetState = sheetState + ) { val listState = rememberLazyListState( initialFirstVisibleItemIndex = StateSaver.List.settingsOverview, initialFirstVisibleItemScrollOffset = StateSaver.List.settingsOverviewOffset @@ -43,14 +60,16 @@ fun SettingsScreen(component: SettingsComponent) { LazyColumn( state = listState, modifier = Modifier.fillMaxWidth(), - contentPadding = padding, + contentPadding = bottomPadding.merge(PaddingValues(16.dp)), verticalArrangement = Arrangement.spacedBy(8.dp) ) { item { UserSection( userFlow = component.user, loginUri = component.loginUri, - modifier = Modifier.fillParentMaxWidth() + dismissVisible = sheetState.isFullyExpandedOrTargeted(forceFullExpand = true), + modifier = Modifier.fillParentMaxWidth(), + onDismiss = component::dismiss ) } item { @@ -159,7 +178,7 @@ fun SettingsScreen(component: SettingsComponent) { horizontalArrangement = Arrangement.spacedBy(8.dp) ) { Icon( - imageVector = Icons.Default.Code, + imageVector = Icons.Rounded.Code, contentDescription = null, ) Text(text = "Developed by DatLag") diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/settings/SettingsScreenComponent.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/dialog/settings/SettingsDialogComponent.kt similarity index 91% rename from composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/settings/SettingsScreenComponent.kt rename to composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/dialog/settings/SettingsDialogComponent.kt index 748f01e..a441d22 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/settings/SettingsScreenComponent.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/dialog/settings/SettingsDialogComponent.kt @@ -1,4 +1,4 @@ -package dev.datlag.aniflow.ui.navigation.screen.settings +package dev.datlag.aniflow.ui.navigation.screen.home.dialog.settings import androidx.compose.runtime.Composable import com.arkivanov.decompose.ComponentContext @@ -7,7 +7,6 @@ import dev.datlag.aniflow.common.onRender import dev.datlag.aniflow.other.UserHelper import dev.datlag.aniflow.settings.Settings import dev.datlag.tooling.compose.ioDispatcher -import dev.datlag.tooling.decompose.ioScope import kotlinx.coroutines.flow.* import org.kodein.di.DI import org.kodein.di.instance @@ -15,10 +14,11 @@ import dev.datlag.aniflow.settings.model.Color as SettingsColor import dev.datlag.aniflow.settings.model.TitleLanguage as SettingsTitle import dev.datlag.aniflow.settings.model.CharLanguage as SettingsChar -class SettingsScreenComponent( +class SettingsDialogComponent( componentContext: ComponentContext, override val di: DI, - private val onNekos: () -> Unit + private val onNekos: () -> Unit, + private val onDismiss: () -> Unit ) : SettingsComponent, ComponentContext by componentContext { private val appSettings by di.instance() @@ -73,4 +73,8 @@ class SettingsScreenComponent( override fun nekos() { onNekos() } + + override fun dismiss() { + onDismiss() + } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/settings/component/AdultSection.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/dialog/settings/component/AdultSection.kt similarity index 96% rename from composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/settings/component/AdultSection.kt rename to composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/dialog/settings/component/AdultSection.kt index 90c7c2f..b78002a 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/settings/component/AdultSection.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/dialog/settings/component/AdultSection.kt @@ -1,4 +1,4 @@ -package dev.datlag.aniflow.ui.navigation.screen.settings.component +package dev.datlag.aniflow.ui.navigation.screen.home.dialog.settings.component import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/settings/component/CharacterSection.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/dialog/settings/component/CharacterSection.kt similarity index 97% rename from composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/settings/component/CharacterSection.kt rename to composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/dialog/settings/component/CharacterSection.kt index bfc81f0..3c89c14 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/settings/component/CharacterSection.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/dialog/settings/component/CharacterSection.kt @@ -1,4 +1,4 @@ -package dev.datlag.aniflow.ui.navigation.screen.settings.component +package dev.datlag.aniflow.ui.navigation.screen.home.dialog.settings.component import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/settings/component/ColorSection.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/dialog/settings/component/ColorSection.kt similarity index 97% rename from composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/settings/component/ColorSection.kt rename to composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/dialog/settings/component/ColorSection.kt index 7eb3a2b..c14a620 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/settings/component/ColorSection.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/dialog/settings/component/ColorSection.kt @@ -1,4 +1,4 @@ -package dev.datlag.aniflow.ui.navigation.screen.settings.component +package dev.datlag.aniflow.ui.navigation.screen.home.dialog.settings.component import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/settings/component/DomainSection.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/dialog/settings/component/DomainSection.kt similarity index 66% rename from composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/settings/component/DomainSection.kt rename to composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/dialog/settings/component/DomainSection.kt index 965d9c3..7e6a960 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/settings/component/DomainSection.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/dialog/settings/component/DomainSection.kt @@ -1,4 +1,4 @@ -package dev.datlag.aniflow.ui.navigation.screen.settings.component +package dev.datlag.aniflow.ui.navigation.screen.home.dialog.settings.component import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/settings/component/TitleSection.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/dialog/settings/component/TitleSection.kt similarity index 97% rename from composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/settings/component/TitleSection.kt rename to composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/dialog/settings/component/TitleSection.kt index 017f437..7607813 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/settings/component/TitleSection.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/dialog/settings/component/TitleSection.kt @@ -1,4 +1,4 @@ -package dev.datlag.aniflow.ui.navigation.screen.settings.component +package dev.datlag.aniflow.ui.navigation.screen.home.dialog.settings.component import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/settings/component/UserSection.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/dialog/settings/component/UserSection.kt similarity index 51% rename from composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/settings/component/UserSection.kt rename to composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/dialog/settings/component/UserSection.kt index 5bc3a1c..3a4efe1 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/settings/component/UserSection.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/dialog/settings/component/UserSection.kt @@ -1,10 +1,14 @@ -package dev.datlag.aniflow.ui.navigation.screen.settings.component +package dev.datlag.aniflow.ui.navigation.screen.home.dialog.settings.component -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.ArrowBackIosNew +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -34,7 +38,9 @@ import org.kodein.di.instance fun UserSection( userFlow: Flow, loginUri: String, + dismissVisible: Boolean, modifier: Modifier = Modifier, + onDismiss: () -> Unit, ) { val user by userFlow.collectAsStateWithLifecycle(null) @@ -45,22 +51,43 @@ fun UserSection( ) { val uriHandler = LocalUriHandler.current - AsyncImage( - modifier = Modifier.size(96.dp).clip(CircleShape).onClick( - enabled = user == null, + Box( + modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp), + contentAlignment = Alignment.Center + ) { + this@Column.AnimatedVisibility( + modifier = Modifier.align(Alignment.CenterStart), + visible = dismissVisible, + enter = fadeIn(), + exit = fadeOut() ) { - uriHandler.openUri(loginUri) - }, - model = user?.avatar?.large, - contentDescription = null, - error = rememberAsyncImagePainter( - model = user?.avatar?.medium, + IconButton( + onClick = onDismiss + ) { + Icon( + imageVector = Icons.Rounded.ArrowBackIosNew, + contentDescription = stringResource(SharedRes.strings.close) + ) + } + } + + AsyncImage( + modifier = Modifier.size(96.dp).clip(CircleShape).onClick( + enabled = user == null, + ) { + uriHandler.openUri(loginUri) + }, + model = user?.avatar?.large, + contentDescription = null, + error = rememberAsyncImagePainter( + model = user?.avatar?.medium, + contentScale = ContentScale.Crop, + error = painterResource(SharedRes.images.anilist) + ), contentScale = ContentScale.Crop, - error = painterResource(SharedRes.images.anilist) - ), - contentScale = ContentScale.Crop, - alignment = Alignment.Center - ) + alignment = Alignment.Center + ) + } Text( text = user?.name ?: stringResource(SharedRes.strings.settings), style = MaterialTheme.typography.headlineMedium, diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/dialog/character/CharacterDialog.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/dialog/character/CharacterDialog.kt index 4fcc449..d5160ee 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/dialog/character/CharacterDialog.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/dialog/character/CharacterDialog.kt @@ -10,6 +10,7 @@ import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.* +import androidx.compose.material.icons.rounded.ArrowBackIosNew import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment @@ -72,7 +73,7 @@ fun CharacterDialog(component: CharacterComponent) { onClick = component::dismiss ) { Icon( - imageVector = Icons.Default.ArrowBackIosNew, + imageVector = Icons.Rounded.ArrowBackIosNew, contentDescription = stringResource(SharedRes.strings.close) ) } diff --git a/composeApp/src/iosMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/settings/component/DomainSection.ios.kt b/composeApp/src/iosMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/dialog/settings/component/DomainSection.ios.kt similarity index 64% rename from composeApp/src/iosMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/settings/component/DomainSection.ios.kt rename to composeApp/src/iosMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/dialog/settings/component/DomainSection.ios.kt index 8fe2b8e..1cb2e89 100644 --- a/composeApp/src/iosMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/settings/component/DomainSection.ios.kt +++ b/composeApp/src/iosMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/dialog/settings/component/DomainSection.ios.kt @@ -1,4 +1,4 @@ -package dev.datlag.aniflow.ui.navigation.screen.settings.component +package dev.datlag.aniflow.ui.navigation.screen.home.dialog.settings.component import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier From 6fc42fa8ba1e6f33a42c20bf888e0e62175e6d3a Mon Sep 17 00:00:00 2001 From: DatLag Date: Sat, 11 May 2024 13:48:57 +0200 Subject: [PATCH 17/23] support instant apps --- composeApp/build.gradle.kts | 1 + .../src/androidMain/AndroidManifest.xml | 7 +- .../aniflow/other/InstantAppHelper.android.kt | 27 +++ .../androidMain/res/xml/network_security.xml | 19 ++ .../datlag/aniflow/other/InstantAppHelper.kt | 12 ++ .../dev/datlag/aniflow/other/StateSaver.kt | 3 - .../dev/datlag/aniflow/ui/custom/EditFAB.kt | 192 ------------------ .../aniflow/ui/custom/InstantAppContent.kt | 19 ++ .../screen/component/CollapsingToolbar.kt | 11 +- .../ui/navigation/screen/home/HomeScreen.kt | 124 ++++++----- .../home/dialog/settings/SettingsDialog.kt | 13 -- .../navigation/screen/medium/MediumScreen.kt | 58 ++++-- .../medium/component/CollapsingToolbar.kt | 4 +- .../dialog/character/CharacterDialog.kt | 4 +- .../moko-resources/base/strings.xml | 1 + .../aniflow/other/InstantAppHelper.ios.kt | 17 ++ gradle/libs.versions.toml | 2 + 17 files changed, 232 insertions(+), 282 deletions(-) create mode 100644 composeApp/src/androidMain/kotlin/dev/datlag/aniflow/other/InstantAppHelper.android.kt create mode 100644 composeApp/src/androidMain/res/xml/network_security.xml create mode 100644 composeApp/src/commonMain/kotlin/dev/datlag/aniflow/other/InstantAppHelper.kt delete mode 100644 composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/custom/EditFAB.kt create mode 100644 composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/custom/InstantAppContent.kt create mode 100644 composeApp/src/iosMain/kotlin/dev/datlag/aniflow/other/InstantAppHelper.ios.kt diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index 5d46d9a..01c3edd 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -147,6 +147,7 @@ kotlin { implementation(libs.android.credentials.play.services) implementation(libs.translate) + implementation(libs.instantapps) } } } diff --git a/composeApp/src/androidMain/AndroidManifest.xml b/composeApp/src/androidMain/AndroidManifest.xml index 915517e..e19f2a3 100644 --- a/composeApp/src/androidMain/AndroidManifest.xml +++ b/composeApp/src/androidMain/AndroidManifest.xml @@ -1,6 +1,10 @@ + xmlns:tools="http://schemas.android.com/tools" + xmlns:dist="http://schemas.android.com/apk/distribution" + android:targetSandboxVersion="2"> + + @@ -29,6 +33,7 @@ android:icon="@android:drawable/ic_menu_compass" android:label="@string/app_name" android:usesCleartextTraffic="false" + android:networkSecurityConfig="@xml/network_security" android:hardwareAccelerated="true" android:enableOnBackInvokedCallback="true" android:appCategory="news" diff --git a/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/other/InstantAppHelper.android.kt b/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/other/InstantAppHelper.android.kt new file mode 100644 index 0000000..be329ae --- /dev/null +++ b/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/other/InstantAppHelper.android.kt @@ -0,0 +1,27 @@ +package dev.datlag.aniflow.other + +import android.content.Context +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalContext +import com.google.android.gms.instantapps.InstantApps +import dev.datlag.aniflow.findActivity + +actual class InstantAppHelper(private val context: Context) { + + actual val isInstantApp: Boolean + get() = InstantApps.getPackageManagerCompat(context).isInstantApp + + actual fun showInstallPrompt() { + context.findActivity()?.let { + InstantApps.showInstallPrompt(it, null, 1337, null) + } + } +} + +@Composable +actual fun rememberInstantAppHelper(): InstantAppHelper { + val context = LocalContext.current + + return remember(context) { InstantAppHelper(context) } +} \ No newline at end of file diff --git a/composeApp/src/androidMain/res/xml/network_security.xml b/composeApp/src/androidMain/res/xml/network_security.xml new file mode 100644 index 0000000..7944f5b --- /dev/null +++ b/composeApp/src/androidMain/res/xml/network_security.xml @@ -0,0 +1,19 @@ + + + + + + + + + anilist.co + googleapis.com + trace.moe + nekosapi.com + + + + + + + \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/other/InstantAppHelper.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/other/InstantAppHelper.kt new file mode 100644 index 0000000..5e2ce7c --- /dev/null +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/other/InstantAppHelper.kt @@ -0,0 +1,12 @@ +package dev.datlag.aniflow.other + +import androidx.compose.runtime.Composable + +expect class InstantAppHelper { + val isInstantApp: Boolean + + fun showInstallPrompt() +} + +@Composable +expect fun rememberInstantAppHelper(): InstantAppHelper \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/other/StateSaver.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/other/StateSaver.kt index 1c41456..345637d 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/other/StateSaver.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/other/StateSaver.kt @@ -32,9 +32,6 @@ data object StateSaver { mediumOverview[id] = index to offset } - var settingsOverview: Int = 0 - var settingsOverviewOffset: Int = 0 - data object Home { var airingOverview: Int = 0 var airingOverviewOffset: Int = 0 diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/custom/EditFAB.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/custom/EditFAB.kt deleted file mode 100644 index dd4aaa0..0000000 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/custom/EditFAB.kt +++ /dev/null @@ -1,192 +0,0 @@ -package dev.datlag.aniflow.ui.custom - -import androidx.compose.animation.* -import androidx.compose.animation.core.Spring -import androidx.compose.animation.core.spring -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Add -import androidx.compose.material.icons.filled.Edit -import androidx.compose.material.icons.filled.Star -import androidx.compose.material.icons.filled.Visibility -import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.graphics.TransformOrigin -import androidx.compose.ui.unit.dp -import dev.datlag.aniflow.SharedRes -import dev.datlag.tooling.compose.withDefaultContext -import dev.datlag.tooling.compose.withIOContext -import dev.icerock.moko.resources.compose.painterResource -import dev.icerock.moko.resources.compose.stringResource -import kotlinx.coroutines.delay - -@Composable -fun EditFAB( - displayAdd: Boolean = false, - bsAvailable: Boolean = false, - expanded: Boolean = false, - onBS: () -> Unit, - onRate: () -> Unit, - onProgress: () -> Unit -) { - Column( - horizontalAlignment = Alignment.End, - verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterVertically) - ) { - var showOtherFABs by remember(expanded) { mutableStateOf(expanded) } - - AnimatedVisibility( - visible = showOtherFABs, - enter = slideInVertically( - animationSpec = bouncySpring(), - initialOffsetY = { -(-it / 2) } - ) + fadeIn( - animationSpec = bouncySpring() - ), - exit = slideOutVertically( - animationSpec = bouncySpring(), - targetOffsetY = { -(-it / 2) } - ) + fadeOut( - animationSpec = bouncySpring() - ) - ) { - LabelFAB( - onClick = { - showOtherFABs = false - onProgress() - } - ) { - Icon( - imageVector = Icons.Default.Visibility, - contentDescription = null - ) - } - } - AnimatedVisibility( - visible = showOtherFABs, - enter = slideInVertically( - animationSpec = bouncySpring(), - initialOffsetY = { -(-it / 2) } - ) + fadeIn( - animationSpec = bouncySpring() - ), - exit = slideOutVertically( - animationSpec = bouncySpring(), - targetOffsetY = { -(-it / 2) } - ) + fadeOut( - animationSpec = bouncySpring() - ) - ) { - LabelFAB( - onClick = { - showOtherFABs = false - onRate() - } - ) { - Icon( - imageVector = Icons.Default.Star, - contentDescription = null - ) - } - } - AnimatedVisibility( - visible = showOtherFABs && bsAvailable, - enter = slideInVertically( - animationSpec = bouncySpring(), - initialOffsetY = { -(-it / 2) } - ) + fadeIn( - animationSpec = bouncySpring() - ), - exit = slideOutVertically( - animationSpec = bouncySpring(), - targetOffsetY = { -(-it / 2) } - ) + fadeOut( - animationSpec = bouncySpring() - ) - ) { - LabelFAB( - onClick = { - showOtherFABs = false - onBS() - } - ) { - Image( - modifier = Modifier.size(24.dp), - painter = painterResource(SharedRes.images.bs), - contentDescription = stringResource(SharedRes.strings.bs), - colorFilter = ColorFilter.tint(LocalContentColor.current) - ) - } - } - - ExtendedFloatingActionButton( - onClick = { - showOtherFABs = !showOtherFABs - }, - expanded = expanded, - icon = { - val icon = if (displayAdd) { - Icons.Default.Add - } else { - Icons.Default.Edit - } - - Icon( - imageVector = icon, - contentDescription = null - ) - }, - text = { - val text = if (displayAdd) { - SharedRes.strings.add - } else { - SharedRes.strings.edit - } - - Text(text = stringResource(text)) - } - ) - } -} - -@Composable -private fun LabelFAB( - onClick: () -> Unit, - icon: @Composable () -> Unit -) { - var showLabel by remember { mutableStateOf(false) } - - LaunchedEffect(showLabel) { - if (!showLabel) { - showLabel = true - } - } - - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - SmallFloatingActionButton( - modifier = Modifier.padding(end = 4.dp), - onClick = onClick - ) { - icon() - } - } - - DisposableEffect(showLabel) { - onDispose { - showLabel = false - } - } -} - -private fun bouncySpring() = spring( - dampingRatio = Spring.DampingRatioMediumBouncy, - stiffness = Spring.StiffnessMedium -) \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/custom/InstantAppContent.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/custom/InstantAppContent.kt new file mode 100644 index 0000000..3016866 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/custom/InstantAppContent.kt @@ -0,0 +1,19 @@ +package dev.datlag.aniflow.ui.custom + +import androidx.compose.runtime.Composable +import dev.datlag.aniflow.other.InstantAppHelper +import dev.datlag.aniflow.other.rememberInstantAppHelper + +@Composable +fun InstantAppContent( + onInstantApp: @Composable (InstantAppHelper) -> Unit = {}, + content: @Composable () -> Unit +) { + val helper = rememberInstantAppHelper() + + if (helper.isInstantApp) { + onInstantApp(helper) + } else { + content() + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/component/CollapsingToolbar.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/component/CollapsingToolbar.kt index ac6de54..98e0c75 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/component/CollapsingToolbar.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/component/CollapsingToolbar.kt @@ -28,6 +28,7 @@ import dev.datlag.aniflow.LocalHaze import dev.datlag.aniflow.SharedRes import dev.datlag.aniflow.anilist.model.User import dev.datlag.aniflow.anilist.type.MediaType +import dev.datlag.aniflow.other.rememberInstantAppHelper import dev.datlag.tooling.compose.ifFalse import dev.datlag.tooling.decompose.lifecycle.collectAsStateWithLifecycle import dev.icerock.moko.resources.compose.fontFamilyResource @@ -76,17 +77,23 @@ fun CollapsingToolbar( LargeTopAppBar( navigationIcon = { + val helper = rememberInstantAppHelper() + IconButton( modifier = Modifier.ifFalse(isCollapsed) { background(MaterialTheme.colorScheme.surface.copy(alpha = 0.75F), CircleShape) }, onClick = { - onProfileClick() + if (helper.isInstantApp) { + helper.showInstallPrompt() + } else { + onProfileClick() + } } ) { val user by userFlow.collectAsStateWithLifecycle(null) val tintColor = LocalContentColor.current - var colorFilter by remember(user) { mutableStateOf(ColorFilter.tint(tintColor)) } + var colorFilter by remember(user) { mutableStateOf(null) } AsyncImage( model = user?.avatar?.large, diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeScreen.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeScreen.kt index 53243c8..bbfcaf5 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeScreen.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeScreen.kt @@ -11,6 +11,7 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.CameraEnhance +import androidx.compose.material.icons.rounded.GetApp import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment @@ -35,11 +36,13 @@ import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi import dev.chrisbanes.haze.materials.HazeMaterials import dev.datlag.aniflow.LocalHaze import dev.datlag.aniflow.LocalPaddingValues +import dev.datlag.aniflow.SharedRes import dev.datlag.aniflow.anilist.type.MediaType import dev.datlag.aniflow.common.* import dev.datlag.aniflow.other.StateSaver import dev.datlag.aniflow.other.rememberImagePickerState import dev.datlag.aniflow.trace.TraceRepository +import dev.datlag.aniflow.ui.custom.InstantAppContent import dev.datlag.aniflow.ui.navigation.screen.component.CollapsingToolbar import dev.datlag.aniflow.ui.navigation.screen.component.HidingNavigationBar import dev.datlag.aniflow.ui.navigation.screen.component.NavigationBarState @@ -47,6 +50,7 @@ import dev.datlag.aniflow.ui.navigation.screen.home.component.AllLoadingView import dev.datlag.aniflow.ui.navigation.screen.home.component.DefaultOverview import dev.datlag.aniflow.ui.navigation.screen.home.component.ScheduleOverview import dev.datlag.tooling.decompose.lifecycle.collectAsStateWithLifecycle +import dev.icerock.moko.resources.compose.stringResource import io.github.aakira.napier.Napier @OptIn(ExperimentalMaterial3Api::class) @@ -79,65 +83,85 @@ fun HomeScreen(component: HomeComponent) { ) }, floatingActionButton = { - val traceState by component.traceState.collectAsStateWithLifecycle(TraceRepository.State.None) - val results = remember(traceState) { - (traceState as? TraceRepository.State.Success)?.response?.combinedResults.orEmpty().toList() - } + InstantAppContent( + onInstantApp = { helper -> + ExtendedFloatingActionButton( + onClick = { + helper.showInstallPrompt() + }, + expanded = listState.isScrollingUp() && listState.canScrollForward, + icon = { + Icon( + imageVector = Icons.Rounded.GetApp, + contentDescription = null + ) + }, + text = { + Text(text = stringResource(SharedRes.strings.install)) + } + ) + } + ) { + val traceState by component.traceState.collectAsStateWithLifecycle(TraceRepository.State.None) + val results = remember(traceState) { + (traceState as? TraceRepository.State.Success)?.response?.combinedResults.orEmpty().toList() + } - val dialogState = rememberUseCaseState( - visible = results.isNotEmpty(), - onCloseRequest = { component.clearTrace() }, - onDismissRequest = { component.clearTrace() }, - onFinishedRequest = { component.clearTrace() } - ) + val optionState = rememberUseCaseState( + visible = results.isNotEmpty(), + onCloseRequest = { component.clearTrace() }, + onDismissRequest = { component.clearTrace() }, + onFinishedRequest = { component.clearTrace() } + ) - LaunchedEffect(results) { - if (results.isNotEmpty()) { - dialogState.show() + LaunchedEffect(results) { + if (results.isNotEmpty()) { + optionState.show() + } } - } - OptionDialog( - state = dialogState, - config = OptionConfig( - mode = DisplayMode.LIST - ), - header = Header.Custom { padding -> - Text( - modifier = Modifier.padding(padding.merge(PaddingValues(16.dp))).fillMaxWidth(), - text = "Matching Anime", - textAlign = TextAlign.Center, - fontWeight = FontWeight.SemiBold, - style = MaterialTheme.typography.titleLarge + OptionDialog( + state = optionState, + config = OptionConfig( + mode = DisplayMode.LIST + ), + header = Header.Custom { padding -> + Text( + modifier = Modifier.padding(padding.merge(PaddingValues(16.dp))).fillMaxWidth(), + text = "Matching Anime", + textAlign = TextAlign.Center, + fontWeight = FontWeight.SemiBold, + style = MaterialTheme.typography.titleLarge + ) + }, + selection = OptionSelection.Single( + options = results.map { + Option( + titleText = it.aniList.asMedium().preferred(null) + ) + }, + onSelectOption = { option, _ -> + component.details(results[option].aniList.asMedium()) + } ) - }, - selection = OptionSelection.Single( - options = results.map { - Option( - titleText = it.aniList.asMedium().preferred(null) + ) + + ExtendedFloatingActionButton( + onClick = { + imagePicker.launch() + }, + expanded = listState.isScrollingUp() && listState.canScrollForward, + icon = { + Icon( + imageVector = Icons.Filled.CameraEnhance, + contentDescription = null ) }, - onSelectOption = { option, _ -> - component.details(results[option].aniList.asMedium()) + text = { + Text(text = "Scan") } ) - ) - - ExtendedFloatingActionButton( - onClick = { - imagePicker.launch() - }, - expanded = listState.isScrollingUp() && listState.canScrollForward, - icon = { - Icon( - imageVector = Icons.Filled.CameraEnhance, - contentDescription = null - ) - }, - text = { - Text(text = "Scan") - } - ) + } }, bottomBar = { HidingNavigationBar( diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/dialog/settings/SettingsDialog.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/dialog/settings/SettingsDialog.kt index 8dab1a0..56eaedd 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/dialog/settings/SettingsDialog.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/dialog/settings/SettingsDialog.kt @@ -52,13 +52,7 @@ fun SettingsScreen(component: SettingsComponent) { windowInsets = insets, sheetState = sheetState ) { - val listState = rememberLazyListState( - initialFirstVisibleItemIndex = StateSaver.List.settingsOverview, - initialFirstVisibleItemScrollOffset = StateSaver.List.settingsOverviewOffset - ) - LazyColumn( - state = listState, modifier = Modifier.fillMaxWidth(), contentPadding = bottomPadding.merge(PaddingValues(16.dp)), verticalArrangement = Arrangement.spacedBy(8.dp) @@ -221,12 +215,5 @@ fun SettingsScreen(component: SettingsComponent) { } } } - - DisposableEffect(listState) { - onDispose { - StateSaver.List.settingsOverview = listState.firstVisibleItemIndex - StateSaver.List.settingsOverviewOffset = listState.firstVisibleItemScrollOffset - } - } } } \ No newline at end of file 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 8a9c9ba..82fced4 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 @@ -8,6 +8,7 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.* +import androidx.compose.material.icons.rounded.GetApp import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment @@ -43,7 +44,7 @@ import dev.datlag.aniflow.anilist.type.MediaStatus import dev.datlag.aniflow.common.* import dev.datlag.aniflow.other.StateSaver import dev.datlag.aniflow.other.UserHelper -import dev.datlag.aniflow.ui.custom.EditFAB +import dev.datlag.aniflow.ui.custom.InstantAppContent import dev.datlag.aniflow.ui.navigation.screen.medium.component.* import dev.datlag.tooling.decompose.lifecycle.collectAsStateWithLifecycle import dev.icerock.moko.resources.compose.painterResource @@ -94,26 +95,45 @@ fun MediumScreen(component: MediumComponent) { ) }, floatingActionButton = { - val notReleased by component.status.mapCollect(component.initialMedium.status) { - it == MediaStatus.UNKNOWN__ || it == MediaStatus.NOT_YET_RELEASED - } + InstantAppContent( + onInstantApp = { helper -> + ExtendedFloatingActionButton( + onClick = { helper.showInstallPrompt() }, + expanded = listState.isScrollingUp() && listState.canScrollForward, + icon = { + Icon( + imageVector = Icons.Rounded.GetApp, + contentDescription = null, + ) + }, + text = { + Text(text = stringResource(SharedRes.strings.install)) + } + ) + } + ) { + val notReleased by component.status.mapCollect(component.initialMedium.status) { + it == MediaStatus.UNKNOWN__ || it == MediaStatus.NOT_YET_RELEASED + } - if (!notReleased) { - val status by component.listStatus.collectAsStateWithLifecycle(component.initialMedium.entry?.status ?: MediaListStatus.UNKNOWN__) - val type by component.type.collectAsStateWithLifecycle(component.initialMedium.type) + if (!notReleased) { + val status by component.listStatus.collectAsStateWithLifecycle(component.initialMedium.entry?.status ?: MediaListStatus.UNKNOWN__) + val type by component.type.collectAsStateWithLifecycle(component.initialMedium.type) - ExtendedFloatingActionButton( - onClick = { component.edit() }, - icon = { - Icon( - imageVector = status.icon(), - contentDescription = null, - ) - }, - text = { - Text(text = stringResource(status.stringRes(type))) - } - ) + ExtendedFloatingActionButton( + onClick = { component.edit() }, + expanded = listState.isScrollingUp() && listState.canScrollForward, + icon = { + Icon( + imageVector = status.icon(), + contentDescription = null, + ) + }, + text = { + Text(text = stringResource(status.stringRes(type))) + } + ) + } } } ) { diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/component/CollapsingToolbar.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/component/CollapsingToolbar.kt index e0d96ec..3693506 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/component/CollapsingToolbar.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/component/CollapsingToolbar.kt @@ -33,6 +33,7 @@ import dev.datlag.aniflow.anilist.MediumRepository import dev.datlag.aniflow.anilist.model.Medium import dev.datlag.aniflow.common.notPreferred import dev.datlag.aniflow.common.preferred +import dev.datlag.aniflow.other.rememberInstantAppHelper import dev.datlag.aniflow.settings.model.AppSettings import dev.datlag.aniflow.ui.custom.shareHandler import dev.datlag.tooling.compose.ifFalse @@ -169,9 +170,10 @@ fun CollapsingToolbar( val mediumState by mediumFlow.collectAsStateWithLifecycle(null) val siteUrl by siteUrlFlow.collectAsStateWithLifecycle(initialMedium.siteUrl) val shareHandler = shareHandler() + val instantAppHelper = rememberInstantAppHelper() AnimatedVisibility( - visible = mediumState is MediumRepository.State.Success, + visible = mediumState is MediumRepository.State.Success && !instantAppHelper.isInstantApp, enter = fadeIn(), exit = fadeOut() ) { diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/dialog/character/CharacterDialog.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/dialog/character/CharacterDialog.kt index d5160ee..9a94460 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/dialog/character/CharacterDialog.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/dialog/character/CharacterDialog.kt @@ -27,6 +27,7 @@ import dev.datlag.aniflow.LocalEdgeToEdge import dev.datlag.aniflow.SharedRes import dev.datlag.aniflow.anilist.CharacterRepository import dev.datlag.aniflow.common.* +import dev.datlag.aniflow.other.rememberInstantAppHelper import dev.datlag.aniflow.ui.navigation.screen.medium.component.TranslateButton import dev.datlag.tooling.compose.ifFalse import dev.datlag.tooling.compose.ifTrue @@ -62,6 +63,7 @@ fun CharacterDialog(component: CharacterComponent) { ) { val image by component.image.collectAsStateWithLifecycle(component.initialChar.image) val state by component.state.collectAsStateWithLifecycle(null) + val instantAppHelper = rememberInstantAppHelper() this@ModalBottomSheet.AnimatedVisibility( modifier = Modifier.align(Alignment.CenterStart), @@ -106,7 +108,7 @@ fun CharacterDialog(component: CharacterComponent) { this@ModalBottomSheet.AnimatedVisibility( modifier = Modifier.align(Alignment.CenterEnd), - visible = state is CharacterRepository.State.Success, + visible = state is CharacterRepository.State.Success && !instantAppHelper.isInstantApp, enter = fadeIn(), exit = fadeOut() ) { diff --git a/composeApp/src/commonMain/moko-resources/base/strings.xml b/composeApp/src/commonMain/moko-resources/base/strings.xml index acc80f7..e79ddf3 100644 --- a/composeApp/src/commonMain/moko-resources/base/strings.xml +++ b/composeApp/src/commonMain/moko-resources/base/strings.xml @@ -70,4 +70,5 @@ Dropped Paused Repeating + Install diff --git a/composeApp/src/iosMain/kotlin/dev/datlag/aniflow/other/InstantAppHelper.ios.kt b/composeApp/src/iosMain/kotlin/dev/datlag/aniflow/other/InstantAppHelper.ios.kt new file mode 100644 index 0000000..4077ae5 --- /dev/null +++ b/composeApp/src/iosMain/kotlin/dev/datlag/aniflow/other/InstantAppHelper.ios.kt @@ -0,0 +1,17 @@ +package dev.datlag.aniflow.other + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember + +actual data object InstantAppHelper { + actual val isInstantApp: Boolean + get() = false + + actual fun showInstallPrompt() { } + +} + +@Composable +actual fun rememberInstantAppHelper(): InstantAppHelper { + return remember { InstantAppHelper } +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6beed23..446ef67 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -26,6 +26,7 @@ flowredux = "1.2.1" google-identity = "1.1.0" haze = "0.7.1" html-converter = "0.9.5" +instantapps = "18.0.1" kache = "2.1.0" kasechange = "1.4.1" kmpalette = "3.1.0" @@ -83,6 +84,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" } +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" } kmpalette = { group = "com.kmpalette", name = "kmpalette-core", version.ref = "kmpalette" } From 3aaf365fe46e21209d3f980d1ee28a2c106c494f Mon Sep 17 00:00:00 2001 From: DatLag Date: Sat, 11 May 2024 14:18:45 +0200 Subject: [PATCH 18/23] support app update --- composeApp/build.gradle.kts | 2 + .../kotlin/dev/datlag/aniflow/MainActivity.kt | 11 ++++++ .../dev/datlag/aniflow/other/UpdateManager.kt | 37 +++++++++++++++++++ gradle/libs.versions.toml | 3 ++ 4 files changed, 53 insertions(+) create mode 100644 composeApp/src/androidMain/kotlin/dev/datlag/aniflow/other/UpdateManager.kt diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index 01c3edd..908b62d 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -148,6 +148,8 @@ kotlin { implementation(libs.translate) implementation(libs.instantapps) + implementation(libs.appupdate) + implementation(libs.appupdate.kotlin) } } } diff --git a/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/MainActivity.kt b/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/MainActivity.kt index 1caf4a6..0dd4fc9 100644 --- a/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/MainActivity.kt +++ b/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/MainActivity.kt @@ -19,7 +19,12 @@ import com.arkivanov.essenty.lifecycle.Lifecycle import com.arkivanov.essenty.lifecycle.LifecycleOwner import com.arkivanov.essenty.lifecycle.essentyLifecycle import com.arkivanov.essenty.statekeeper.stateKeeper +import com.google.android.play.core.appupdate.AppUpdateManagerFactory +import com.google.android.play.core.appupdate.AppUpdateOptions +import com.google.android.play.core.install.model.AppUpdateType +import com.google.android.play.core.ktx.requestAppUpdateInfo import dev.datlag.aniflow.other.DomainVerifier +import dev.datlag.aniflow.other.UpdateManager import dev.datlag.aniflow.other.UserHelper import dev.datlag.aniflow.ui.navigation.RootComponent import dev.datlag.tooling.compose.launchIO @@ -54,6 +59,9 @@ class MainActivity : AppCompatActivity() { ) DomainVerifier.verify(this) + UpdateManager.checkForUpdates(this) { manager, info, type -> + manager.startUpdateFlow(info, this, AppUpdateOptions.defaultOptions(type)) + } setContent { CompositionLocalProvider( @@ -107,6 +115,9 @@ class MainActivity : AppCompatActivity() { super.onResume() DomainVerifier.verify(this) + UpdateManager.checkResume(this) { manager, info -> + manager.startUpdateFlow(info, this, AppUpdateOptions.defaultOptions(AppUpdateType.IMMEDIATE)) + } } override fun onPause() { diff --git a/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/other/UpdateManager.kt b/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/other/UpdateManager.kt new file mode 100644 index 0000000..d7274c7 --- /dev/null +++ b/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/other/UpdateManager.kt @@ -0,0 +1,37 @@ +package dev.datlag.aniflow.other + +import android.content.Context +import com.google.android.play.core.appupdate.AppUpdateInfo +import com.google.android.play.core.appupdate.AppUpdateManager +import com.google.android.play.core.appupdate.AppUpdateManagerFactory +import com.google.android.play.core.install.model.AppUpdateType +import com.google.android.play.core.install.model.UpdateAvailability + +data object UpdateManager { + + fun checkForUpdates(context: Context, onUpdateAvailable: (AppUpdateManager, AppUpdateInfo, updateType: Int) -> Unit) { + val manager = AppUpdateManagerFactory.create(context) + + manager.appUpdateInfo.addOnSuccessListener { info -> + val updateAvailable = info.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE + + if (updateAvailable) { + val immediate = info.isUpdateTypeAllowed(AppUpdateType.IMMEDIATE) + + if (immediate) { + onUpdateAvailable(manager, info, AppUpdateType.IMMEDIATE) + } + } + } + } + + fun checkResume(context: Context, onUpdateResume: (AppUpdateManager, AppUpdateInfo) -> Unit) { + val manager = AppUpdateManagerFactory.create(context) + + manager.appUpdateInfo.addOnSuccessListener { info -> + if (info.updateAvailability() == UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS) { + onUpdateResume(manager, info) + } + } + } +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 446ef67..02542a2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,6 +7,7 @@ android-core = "1.13.1" android-credentials = "1.3.0-alpha02" apollo = "4.0.0-beta.6" appcompat = "1.6.1" +appupdate = "2.1.0" atomicfu = "0.24.0" coil = "3.0.0-alpha06" compose = "1.6.2" @@ -60,6 +61,8 @@ apollo = { group = "com.apollographql.apollo3", name = "apollo-runtime", version apollo-cache = { group = "com.apollographql.apollo3", name = "apollo-normalized-cache", version.ref = "apollo" } apollo-cache-sql = { group = "com.apollographql.apollo3", name = "apollo-normalized-cache-sqlite", version.ref = "apollo" } appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } +appupdate = { group = "com.google.android.play", name = "app-update", version.ref = "appupdate" } +appupdate-kotlin = { group = "com.google.android.play", name = "app-update-ktx", version.ref = "appupdate" } atomicfu = { group = "org.jetbrains.kotlinx", name = "atomicfu-gradle-plugin", version.ref = "atomicfu" } coil = { group = "io.coil-kt.coil3", name = "coil", version.ref = "coil" } coil-network = { group = "io.coil-kt.coil3", name = "coil-network-ktor", version.ref = "coil" } From 41cdcf59188a65eb8f44f00e302de5ff1da57029 Mon Sep 17 00:00:00 2001 From: DatLag Date: Sat, 11 May 2024 14:58:24 +0200 Subject: [PATCH 19/23] login on actions --- .../kotlin/dev/datlag/aniflow/MainActivity.kt | 2 +- .../screen/medium/MediumComponent.kt | 2 ++ .../navigation/screen/medium/MediumScreen.kt | 12 +++++++++- .../screen/medium/MediumScreenComponent.kt | 3 +++ .../medium/component/CollapsingToolbar.kt | 23 ++++++++++++++----- .../dialog/character/CharacterComponent.kt | 2 ++ .../dialog/character/CharacterDialog.kt | 19 +++++++++++---- .../character/CharacterDialogComponent.kt | 11 ++++++--- 8 files changed, 58 insertions(+), 16 deletions(-) diff --git a/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/MainActivity.kt b/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/MainActivity.kt index 0dd4fc9..b899e96 100644 --- a/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/MainActivity.kt +++ b/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/MainActivity.kt @@ -89,7 +89,7 @@ class MainActivity : AppCompatActivity() { } val accessToken = uri.getFragmentOrQueryParameter("access_token") - if (accessToken.isNullOrBlank()) { + if (accessToken.isNullOrBlank() || !::root.isInitialized) { return } 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 d91afa3..0c6b8bf 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 @@ -23,6 +23,8 @@ interface MediumComponent : ContentHolderComponent { val initialMedium: Medium val titleLanguage: Flow val charLanguage: Flow + val isLoggedIn: Flow + val loginUri: String val mediumState: Flow val isAdult: Flow 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 82fced4..a6c12a6 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 @@ -86,6 +86,8 @@ fun MediumScreen(component: MediumComponent) { bannerImageFlow = component.bannerImage, coverImage = coverImage, titleFlow = component.title, + isLoggedIn = component.isLoggedIn, + loginUri = component.loginUri, isFavoriteFlow = component.isFavorite, isFavoriteBlockedFlow = component.isFavoriteBlocked, siteUrlFlow = component.siteUrl, @@ -117,11 +119,19 @@ fun MediumScreen(component: MediumComponent) { } if (!notReleased) { + val loggedIn by component.isLoggedIn.collectAsStateWithLifecycle(false) val status by component.listStatus.collectAsStateWithLifecycle(component.initialMedium.entry?.status ?: MediaListStatus.UNKNOWN__) val type by component.type.collectAsStateWithLifecycle(component.initialMedium.type) + val uriHandler = LocalUriHandler.current ExtendedFloatingActionButton( - onClick = { component.edit() }, + onClick = { + if (!loggedIn) { + uriHandler.openUri(component.loginUri) + } else { + component.edit() + } + }, expanded = listState.isScrollingUp() && listState.canScrollForward, icon = { Icon( 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 020e20e..60e2386 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 @@ -52,7 +52,10 @@ class MediumScreenComponent( private val aniListClient by di.instance(Constants.AniList.APOLLO_CLIENT) private val appSettings by di.instance() + private val userHelper by di.instance() + override val isLoggedIn: Flow = userHelper.isLoggedIn + override val loginUri: String = userHelper.loginUrl override val titleLanguage: Flow = appSettings.titleLanguage.flowOn(ioDispatcher()) override val charLanguage: Flow = appSettings.charLanguage.flowOn(ioDispatcher()) diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/component/CollapsingToolbar.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/component/CollapsingToolbar.kt index 3693506..0f7d85e 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/component/CollapsingToolbar.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/component/CollapsingToolbar.kt @@ -12,6 +12,9 @@ import androidx.compose.material.icons.filled.ArrowBackIosNew import androidx.compose.material.icons.filled.Favorite import androidx.compose.material.icons.filled.FavoriteBorder import androidx.compose.material.icons.filled.Share +import androidx.compose.material.icons.rounded.ArrowBackIosNew +import androidx.compose.material.icons.rounded.Favorite +import androidx.compose.material.icons.rounded.FavoriteBorder import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment @@ -56,6 +59,8 @@ fun CollapsingToolbar( bannerImageFlow: Flow, coverImage: Medium.CoverImage, titleFlow: Flow, + isLoggedIn: Flow, + loginUri: String, isFavoriteFlow: Flow, isFavoriteBlockedFlow: Flow, siteUrlFlow: Flow, @@ -106,7 +111,7 @@ fun CollapsingToolbar( } ) { Icon( - imageVector = Icons.Default.ArrowBackIosNew, + imageVector = Icons.Rounded.ArrowBackIosNew, contentDescription = null ) } @@ -177,22 +182,28 @@ fun CollapsingToolbar( enter = fadeIn(), exit = fadeOut() ) { + val loggedIn by isLoggedIn.collectAsStateWithLifecycle(false) val isFavoriteBlocked by isFavoriteBlockedFlow.collectAsStateWithLifecycle(initialMedium.isFavoriteBlocked) val isFavorite by isFavoriteFlow.collectAsStateWithLifecycle(initialMedium.isFavorite) var favoriteChanged by remember(isFavorite) { mutableStateOf(null) } + val uriHandler = LocalUriHandler.current IconButton( onClick = { - favoriteChanged = !(favoriteChanged ?: isFavorite) - onToggleFavorite() + if (!loggedIn) { + uriHandler.openUri(loginUri) + } else { + favoriteChanged = !(favoriteChanged ?: isFavorite) + onToggleFavorite() + } }, - enabled = !isFavoriteBlocked + enabled = !loggedIn || !isFavoriteBlocked ) { Icon( imageVector = if (favoriteChanged ?: isFavorite) { - Icons.Default.Favorite + Icons.Rounded.Favorite } else { - Icons.Default.FavoriteBorder + Icons.Rounded.FavoriteBorder }, contentDescription = null ) diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/dialog/character/CharacterComponent.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/dialog/character/CharacterComponent.kt index 3059b41..c957aec 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/dialog/character/CharacterComponent.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/dialog/character/CharacterComponent.kt @@ -9,6 +9,8 @@ import kotlinx.coroutines.flow.StateFlow interface CharacterComponent : DialogComponent { val initialChar: Character + val isLoggedIn: Flow + val loginUri: String val state: Flow val charLanguage: Flow diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/dialog/character/CharacterDialog.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/dialog/character/CharacterDialog.kt index 9a94460..f005460 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/dialog/character/CharacterDialog.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/dialog/character/CharacterDialog.kt @@ -11,6 +11,8 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.* import androidx.compose.material.icons.rounded.ArrowBackIosNew +import androidx.compose.material.icons.rounded.Favorite +import androidx.compose.material.icons.rounded.FavoriteBorder import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment @@ -18,6 +20,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp @@ -112,22 +115,28 @@ fun CharacterDialog(component: CharacterComponent) { enter = fadeIn(), exit = fadeOut() ) { + val loggedIn by component.isLoggedIn.collectAsStateWithLifecycle(false) val isFavoriteBlocked by component.isFavoriteBlocked.collectAsStateWithLifecycle(component.initialChar.isFavoriteBlocked) val isFavorite by component.isFavorite.collectAsStateWithLifecycle(component.initialChar.isFavorite) var favoriteChanged by remember(isFavorite) { mutableStateOf(null) } + val uriHandler = LocalUriHandler.current IconButton( onClick = { - favoriteChanged = !(favoriteChanged ?: isFavorite) - component.toggleFavorite() + if (!loggedIn) { + uriHandler.openUri(component.loginUri) + } else { + favoriteChanged = !(favoriteChanged ?: isFavorite) + component.toggleFavorite() + } }, - enabled = !isFavoriteBlocked + enabled = !loggedIn || !isFavoriteBlocked ) { Icon( imageVector = if (favoriteChanged ?: isFavorite) { - Icons.Default.Favorite + Icons.Rounded.Favorite } else { - Icons.Default.FavoriteBorder + Icons.Rounded.FavoriteBorder }, contentDescription = null, ) diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/dialog/character/CharacterDialogComponent.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/dialog/character/CharacterDialogComponent.kt index 26125fe..76d741b 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/dialog/character/CharacterDialogComponent.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/dialog/character/CharacterDialogComponent.kt @@ -11,6 +11,7 @@ import dev.datlag.aniflow.common.nullableFirebaseInstance import dev.datlag.aniflow.common.onRender import dev.datlag.aniflow.model.safeFirstOrNull import dev.datlag.aniflow.other.Constants +import dev.datlag.aniflow.other.UserHelper import dev.datlag.aniflow.settings.Settings import dev.datlag.aniflow.settings.model.CharLanguage import dev.datlag.aniflow.settings.model.TitleLanguage @@ -28,12 +29,16 @@ class CharacterDialogComponent( private val onDismiss: () -> Unit ) : CharacterComponent, ComponentContext by componentContext { - private val aniListClient by di.instance(Constants.AniList.APOLLO_CLIENT) - private val characterRepository by di.instance() + private val aniListClient by instance(Constants.AniList.APOLLO_CLIENT) + private val characterRepository by instance() - private val appSettings by di.instance() + private val appSettings by instance() override val charLanguage: Flow = appSettings.charLanguage.flowOn(ioDispatcher()) + private val userHelper by instance() + override val isLoggedIn: Flow = userHelper.isLoggedIn + override val loginUri: String = userHelper.loginUrl + override val state = characterRepository.character private val characterSuccessState = state.mapNotNull { it.safeCast() From ac2ed4e833da2b4f11c4e209a8556db03ec1f79e Mon Sep 17 00:00:00 2001 From: DatLag Date: Sat, 11 May 2024 15:24:37 +0200 Subject: [PATCH 20/23] switch title language in home screen --- .../aniflow/ui/navigation/screen/home/HomeComponent.kt | 2 ++ .../datlag/aniflow/ui/navigation/screen/home/HomeScreen.kt | 5 +++++ .../aniflow/ui/navigation/screen/home/HomeScreenComponent.kt | 3 +++ .../ui/navigation/screen/home/component/DefaultOverview.kt | 4 +++- .../ui/navigation/screen/home/component/ScheduleOverview.kt | 4 +++- 5 files changed, 16 insertions(+), 2 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeComponent.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeComponent.kt index 1fed965..8acf1f3 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeComponent.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeComponent.kt @@ -8,6 +8,7 @@ import dev.datlag.aniflow.anilist.model.Medium import dev.datlag.aniflow.anilist.model.User import dev.datlag.aniflow.anilist.state.CollectionState import dev.datlag.aniflow.anilist.type.MediaType +import dev.datlag.aniflow.settings.model.TitleLanguage import dev.datlag.aniflow.trace.TraceRepository import dev.datlag.aniflow.ui.navigation.Component import dev.datlag.aniflow.ui.navigation.DialogComponent @@ -16,6 +17,7 @@ import kotlinx.coroutines.flow.Flow interface HomeComponent : Component { val viewing: Flow val user: Flow + val titleLanguage: Flow val airing: Flow val trending: Flow diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeScreen.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeScreen.kt index bbfcaf5..659dbeb 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeScreen.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeScreen.kt @@ -185,6 +185,7 @@ fun HomeScreen(component: HomeComponent) { val isManga = remember(viewType) { viewType == MediaType.MANGA } + val titleLanguage by component.titleLanguage.collectAsStateWithLifecycle(null) LazyColumn( state = listState, @@ -196,6 +197,7 @@ fun HomeScreen(component: HomeComponent) { item { ScheduleOverview( flow = component.airing, + titleLanguage = titleLanguage, onMediumClick = component::details ) } @@ -204,6 +206,7 @@ fun HomeScreen(component: HomeComponent) { DefaultOverview( title = "Trending", flow = component.trending, + titleLanguage = titleLanguage, onMediumClick = component::details ) } @@ -211,6 +214,7 @@ fun HomeScreen(component: HomeComponent) { DefaultOverview( title = "Popular", flow = component.popularNow, + titleLanguage = titleLanguage, onMediumClick = component::details ) } @@ -219,6 +223,7 @@ fun HomeScreen(component: HomeComponent) { DefaultOverview( title = "Popular Next", flow = component.popularNext, + titleLanguage = titleLanguage, onMediumClick = component::details ) } diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeScreenComponent.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeScreenComponent.kt index 9a15745..796936b 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeScreenComponent.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeScreenComponent.kt @@ -21,6 +21,7 @@ import dev.datlag.aniflow.model.coroutines.Executor import dev.datlag.aniflow.other.StateSaver import dev.datlag.aniflow.other.UserHelper import dev.datlag.aniflow.settings.Settings +import dev.datlag.aniflow.settings.model.TitleLanguage import dev.datlag.aniflow.trace.TraceRepository import dev.datlag.aniflow.ui.navigation.DialogComponent import dev.datlag.aniflow.ui.navigation.screen.home.dialog.settings.SettingsDialogComponent @@ -49,6 +50,8 @@ class HomeScreenComponent( MediaType.ANIME } } + override val titleLanguage: Flow = appSettings.titleLanguage + private val viewTypeExecutor = Executor() private val userHelper by instance() diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/component/DefaultOverview.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/component/DefaultOverview.kt index 23c6a22..5cd690c 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/component/DefaultOverview.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/component/DefaultOverview.kt @@ -17,6 +17,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import dev.datlag.aniflow.anilist.model.Medium import dev.datlag.aniflow.anilist.state.CollectionState +import dev.datlag.aniflow.settings.model.TitleLanguage import dev.datlag.aniflow.ui.navigation.screen.home.component.default.MediumCard import dev.datlag.tooling.decompose.lifecycle.collectAsStateWithLifecycle import kotlinx.coroutines.flow.Flow @@ -26,6 +27,7 @@ import kotlinx.coroutines.flow.Flow fun DefaultOverview( title: String, flow: Flow, + titleLanguage: TitleLanguage?, onMediumClick: (Medium) -> Unit, ) { Column( @@ -60,7 +62,7 @@ fun DefaultOverview( items(current.collection.toList(), key = { it.id }) { MediumCard( medium = it, - titleLanguage = null, + titleLanguage = titleLanguage, modifier = Modifier .width(200.dp) .height(280.dp) diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/component/ScheduleOverview.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/component/ScheduleOverview.kt index 1310ec0..795c2ad 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/component/ScheduleOverview.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/component/ScheduleOverview.kt @@ -20,6 +20,7 @@ import androidx.compose.ui.unit.dp import dev.datlag.aniflow.anilist.AiringTodayRepository import dev.datlag.aniflow.anilist.model.Medium import dev.datlag.aniflow.other.StateSaver +import dev.datlag.aniflow.settings.model.TitleLanguage import dev.datlag.aniflow.ui.navigation.screen.home.component.airing.AiringCard import dev.datlag.tooling.decompose.lifecycle.collectAsStateWithLifecycle import kotlinx.coroutines.flow.Flow @@ -28,6 +29,7 @@ import kotlinx.coroutines.flow.Flow @Composable fun ScheduleOverview( flow: Flow, + titleLanguage: TitleLanguage?, onMediumClick: (Medium) -> Unit ) { Column( @@ -69,7 +71,7 @@ fun ScheduleOverview( items(current.collection.toList()) { AiringCard( airing = it, - titleLanguage = null, + titleLanguage = titleLanguage, modifier = Modifier .height(150.dp) .fillParentMaxWidth(fraction = 0.9F) From 39af1ee6efcea3f29ea078464a4427a3247ca33a Mon Sep 17 00:00:00 2001 From: DatLag Date: Sat, 11 May 2024 16:01:15 +0200 Subject: [PATCH 21/23] added manga info --- .../commonMain/graphql/AiringQuery.graphql | 4 +- .../commonMain/graphql/MediumQuery.graphql | 4 +- .../commonMain/graphql/SeasonQuery.graphql | 4 +- .../commonMain/graphql/TrendingQuery.graphql | 4 +- .../datlag/aniflow/anilist/model/Medium.kt | 20 +++++-- .../kotlin/dev/datlag/aniflow/MainActivity.kt | 2 +- .../screen/medium/MediumComponent.kt | 2 + .../navigation/screen/medium/MediumScreen.kt | 20 +------ .../screen/medium/MediumScreenComponent.kt | 10 ++++ .../medium/component/CollapsingToolbar.kt | 36 +++++------- .../screen/medium/component/CoverSection.kt | 57 +++++++++++++------ .../moko-resources/base/plurals.xml | 15 +++++ .../moko-resources/base/strings.xml | 1 + 13 files changed, 112 insertions(+), 67 deletions(-) create mode 100644 composeApp/src/commonMain/moko-resources/base/plurals.xml diff --git a/anilist/src/commonMain/graphql/AiringQuery.graphql b/anilist/src/commonMain/graphql/AiringQuery.graphql index 4897c1f..95489b1 100644 --- a/anilist/src/commonMain/graphql/AiringQuery.graphql +++ b/anilist/src/commonMain/graphql/AiringQuery.graphql @@ -87,7 +87,9 @@ query AiringQuery( site, thumbnail }, - siteUrl + siteUrl, + chapters, + volumes } } } diff --git a/anilist/src/commonMain/graphql/MediumQuery.graphql b/anilist/src/commonMain/graphql/MediumQuery.graphql index a4f8062..17b0d17 100644 --- a/anilist/src/commonMain/graphql/MediumQuery.graphql +++ b/anilist/src/commonMain/graphql/MediumQuery.graphql @@ -76,6 +76,8 @@ query MediumQuery($id: Int, $statusVersion: Int, $html: Boolean) { site, thumbnail }, - siteUrl + siteUrl, + chapters, + volumes } } \ No newline at end of file diff --git a/anilist/src/commonMain/graphql/SeasonQuery.graphql b/anilist/src/commonMain/graphql/SeasonQuery.graphql index aac6dbb..133ae5c 100644 --- a/anilist/src/commonMain/graphql/SeasonQuery.graphql +++ b/anilist/src/commonMain/graphql/SeasonQuery.graphql @@ -88,7 +88,9 @@ query SeasonQuery( site, thumbnail }, - siteUrl + siteUrl, + chapters, + volumes } } } \ No newline at end of file diff --git a/anilist/src/commonMain/graphql/TrendingQuery.graphql b/anilist/src/commonMain/graphql/TrendingQuery.graphql index 401b475..5fe393a 100644 --- a/anilist/src/commonMain/graphql/TrendingQuery.graphql +++ b/anilist/src/commonMain/graphql/TrendingQuery.graphql @@ -91,7 +91,9 @@ query TrendingQuery( site, thumbnail }, - siteUrl + siteUrl, + chapters, + volumes } } } \ 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 d1e38b4..dfeec35 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 @@ -46,7 +46,9 @@ data class Medium( val trailer: Trailer? = null, val isFavorite: Boolean = false, private val _isFavoriteBlocked: Boolean = true, - val siteUrl: String = "$SITE_URL$id" + val siteUrl: String = "$SITE_URL$id", + val chapters: Int = -1, + val volumes: Int = -1, ) { constructor(trending: TrendingQuery.Medium) : this( id = trending.id, @@ -94,7 +96,9 @@ data class Medium( }, isFavorite = trending.isFavourite, _isFavoriteBlocked = trending.isFavouriteBlocked, - siteUrl = trending.siteUrl?.ifBlank { null } ?: "$SITE_URL${trending.id}" + siteUrl = trending.siteUrl?.ifBlank { null } ?: "$SITE_URL${trending.id}", + chapters = trending.chapters ?: -1, + volumes = trending.volumes ?: -1 ) constructor(airing: AiringQuery.Media) : this( @@ -143,7 +147,9 @@ data class Medium( }, isFavorite = airing.isFavourite, _isFavoriteBlocked = airing.isFavouriteBlocked, - siteUrl = airing.siteUrl?.ifBlank { null } ?: "$SITE_URL${airing.id}" + siteUrl = airing.siteUrl?.ifBlank { null } ?: "$SITE_URL${airing.id}", + chapters = airing.chapters ?: -1, + volumes = airing.volumes ?: -1 ) constructor(season: SeasonQuery.Medium) : this( @@ -192,7 +198,9 @@ data class Medium( }, isFavorite = season.isFavourite, _isFavoriteBlocked = season.isFavouriteBlocked, - siteUrl = season.siteUrl?.ifBlank { null } ?: "$SITE_URL${season.id}" + siteUrl = season.siteUrl?.ifBlank { null } ?: "$SITE_URL${season.id}", + chapters = season.chapters ?: -1, + volumes = season.volumes ?: -1 ) constructor(query: MediumQuery.Media) : this( @@ -241,7 +249,9 @@ data class Medium( }, isFavorite = query.isFavourite, _isFavoriteBlocked = query.isFavouriteBlocked, - siteUrl = query.siteUrl?.ifBlank { null } ?: "$SITE_URL${query.id}" + siteUrl = query.siteUrl?.ifBlank { null } ?: "$SITE_URL${query.id}", + chapters = query.chapters ?: -1, + volumes = query.volumes ?: -1 ) @Transient diff --git a/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/MainActivity.kt b/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/MainActivity.kt index b899e96..0dd4fc9 100644 --- a/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/MainActivity.kt +++ b/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/MainActivity.kt @@ -89,7 +89,7 @@ class MainActivity : AppCompatActivity() { } val accessToken = uri.getFragmentOrQueryParameter("access_token") - if (accessToken.isNullOrBlank() || !::root.isInitialized) { + if (accessToken.isNullOrBlank()) { return } 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 0c6b8bf..832916b 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 @@ -41,6 +41,8 @@ interface MediumComponent : ContentHolderComponent { val episodes: Flow val duration: Flow val status: Flow + val chapters: Flow + val volumes: Flow val rated: Flow val popular: Flow 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 a6c12a6..b482533 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 @@ -80,20 +80,9 @@ fun MediumScreen(component: MediumComponent) { CollapsingToolbar( state = appBarState, scrollBehavior = scrollState, - initialMedium = component.initialMedium, - titleLanguageFlow = component.titleLanguage, - mediumFlow = component.mediumState, - bannerImageFlow = component.bannerImage, coverImage = coverImage, - titleFlow = component.title, - isLoggedIn = component.isLoggedIn, - loginUri = component.loginUri, - isFavoriteFlow = component.isFavorite, - isFavoriteBlockedFlow = component.isFavoriteBlocked, - siteUrlFlow = component.siteUrl, showShare = listState.isScrollingUp(), - onBack = { component.back() }, - onToggleFavorite = { component.toggleFavorite() } + component = component ) }, floatingActionButton = { @@ -159,12 +148,7 @@ fun MediumScreen(component: MediumComponent) { item { CoverSection( coverImage = coverImage, - initialMedium = component.initialMedium, - formatFlow = component.format, - episodesFlow = component.episodes, - durationFlow = component.duration, - statusFlow = component.status, - isAdultFlow = component.isAdult, + component = component, modifier = Modifier.fillParentMaxWidth().padding(horizontal = 16.dp) ) } 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 60e2386..d4609b7 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 @@ -188,6 +188,16 @@ class MediumScreenComponent( it.medium.entry?.status ?: MediaListStatus.UNKNOWN__ } + @OptIn(ExperimentalCoroutinesApi::class) + override val chapters: Flow = mediumSuccessState.mapLatest { + it.medium.chapters + } + + @OptIn(ExperimentalCoroutinesApi::class) + override val volumes: Flow = mediumSuccessState.mapLatest { + it.medium.volumes + } + private val dialogNavigation = SlotNavigation() override val dialog: Value> = childSlot( source = dialogNavigation, diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/component/CollapsingToolbar.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/component/CollapsingToolbar.kt index 0f7d85e..18e4e1e 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/component/CollapsingToolbar.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/component/CollapsingToolbar.kt @@ -39,6 +39,7 @@ import dev.datlag.aniflow.common.preferred import dev.datlag.aniflow.other.rememberInstantAppHelper import dev.datlag.aniflow.settings.model.AppSettings import dev.datlag.aniflow.ui.custom.shareHandler +import dev.datlag.aniflow.ui.navigation.screen.medium.MediumComponent import dev.datlag.tooling.compose.ifFalse import dev.datlag.tooling.compose.ifTrue import dev.datlag.tooling.decompose.lifecycle.collectAsStateWithLifecycle @@ -53,25 +54,14 @@ import dev.datlag.aniflow.settings.model.TitleLanguage as SettingsTitle fun CollapsingToolbar( state: TopAppBarState, scrollBehavior: TopAppBarScrollBehavior, - initialMedium: Medium, - titleLanguageFlow: Flow, - mediumFlow: Flow, - bannerImageFlow: Flow, coverImage: Medium.CoverImage, - titleFlow: Flow, - isLoggedIn: Flow, - loginUri: String, - isFavoriteFlow: Flow, - isFavoriteBlockedFlow: Flow, - siteUrlFlow: Flow, showShare: Boolean, - onBack: () -> Unit, - onToggleFavorite: () -> Unit + component: MediumComponent ) { Box( modifier = Modifier.fillMaxWidth() ) { - val bannerImage by bannerImageFlow.collectAsStateWithLifecycle(initialMedium.bannerImage) + val bannerImage by component.bannerImage.collectAsStateWithLifecycle(component.initialMedium.bannerImage) val isCollapsed by remember(state) { derivedStateOf { state.collapsedFraction >= 0.99F } } @@ -107,7 +97,7 @@ fun CollapsingToolbar( Modifier.background(MaterialTheme.colorScheme.surface.copy(alpha = 0.75F), CircleShape) }, onClick = { - onBack() + component.back() } ) { Icon( @@ -121,8 +111,8 @@ fun CollapsingToolbar( modifier = Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterVertically) ) { - val title by titleFlow.collectAsStateWithLifecycle(initialMedium.title) - val titleLanguage by titleLanguageFlow.collectAsStateWithLifecycle(null) + val title by component.title.collectAsStateWithLifecycle(component.initialMedium.title) + val titleLanguage by component.titleLanguage.collectAsStateWithLifecycle(null) Text( text = title.preferred(titleLanguage), @@ -172,8 +162,8 @@ fun CollapsingToolbar( horizontalArrangement = Arrangement.End, verticalAlignment = Alignment.CenterVertically, ) { - val mediumState by mediumFlow.collectAsStateWithLifecycle(null) - val siteUrl by siteUrlFlow.collectAsStateWithLifecycle(initialMedium.siteUrl) + val mediumState by component.mediumState.collectAsStateWithLifecycle(null) + val siteUrl by component.siteUrl.collectAsStateWithLifecycle(component.initialMedium.siteUrl) val shareHandler = shareHandler() val instantAppHelper = rememberInstantAppHelper() @@ -182,19 +172,19 @@ fun CollapsingToolbar( enter = fadeIn(), exit = fadeOut() ) { - val loggedIn by isLoggedIn.collectAsStateWithLifecycle(false) - val isFavoriteBlocked by isFavoriteBlockedFlow.collectAsStateWithLifecycle(initialMedium.isFavoriteBlocked) - val isFavorite by isFavoriteFlow.collectAsStateWithLifecycle(initialMedium.isFavorite) + val loggedIn by component.isLoggedIn.collectAsStateWithLifecycle(false) + val isFavoriteBlocked by component.isFavoriteBlocked.collectAsStateWithLifecycle(component.initialMedium.isFavoriteBlocked) + val isFavorite by component.isFavorite.collectAsStateWithLifecycle(component.initialMedium.isFavorite) var favoriteChanged by remember(isFavorite) { mutableStateOf(null) } val uriHandler = LocalUriHandler.current IconButton( onClick = { if (!loggedIn) { - uriHandler.openUri(loginUri) + uriHandler.openUri(component.loginUri) } else { favoriteChanged = !(favoriteChanged ?: isFavorite) - onToggleFavorite() + component.toggleFavorite() } }, enabled = !loggedIn || !isFavoriteBlocked diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/component/CoverSection.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/component/CoverSection.kt index 87e4ef1..4c9b6ad 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/component/CoverSection.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/component/CoverSection.kt @@ -8,9 +8,7 @@ import androidx.compose.material.icons.filled.NoAdultContent import androidx.compose.material.icons.filled.OndemandVideo import androidx.compose.material.icons.filled.RssFeed import androidx.compose.material.icons.filled.Timelapse -import androidx.compose.material.icons.rounded.NoAdultContent -import androidx.compose.material.icons.rounded.RssFeed -import androidx.compose.material.icons.rounded.Timelapse +import androidx.compose.material.icons.rounded.* import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -28,8 +26,10 @@ import dev.datlag.aniflow.anilist.type.MediaFormat import dev.datlag.aniflow.anilist.type.MediaStatus import dev.datlag.aniflow.common.icon import dev.datlag.aniflow.common.text +import dev.datlag.aniflow.ui.navigation.screen.medium.MediumComponent import dev.datlag.tooling.compose.ifTrue import dev.datlag.tooling.decompose.lifecycle.collectAsStateWithLifecycle +import dev.icerock.moko.resources.compose.pluralStringResource import dev.icerock.moko.resources.compose.stringResource import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow @@ -37,12 +37,7 @@ import kotlinx.coroutines.flow.StateFlow @Composable fun CoverSection( coverImage: Medium.CoverImage, - initialMedium: Medium, - formatFlow: Flow, - episodesFlow: Flow, - durationFlow: Flow, - statusFlow: Flow, - isAdultFlow: Flow, + component: MediumComponent, modifier: Modifier = Modifier ) { Row( @@ -88,11 +83,13 @@ fun CoverSection( modifier = Modifier.weight(1F).fillMaxHeight(), verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterVertically) ) { - val format by formatFlow.collectAsStateWithLifecycle(initialMedium.format) - val episodes by episodesFlow.collectAsStateWithLifecycle(initialMedium.episodes) - val duration by durationFlow.collectAsStateWithLifecycle(initialMedium.avgEpisodeDurationInMin) - val status by statusFlow.collectAsStateWithLifecycle(initialMedium.status) - val isAdult by isAdultFlow.collectAsStateWithLifecycle(initialMedium.isAdult) + val format by component.format.collectAsStateWithLifecycle(component.initialMedium.format) + val episodes by component.episodes.collectAsStateWithLifecycle(component.initialMedium.episodes) + val duration by component.duration.collectAsStateWithLifecycle(component.initialMedium.avgEpisodeDurationInMin) + val status by component.status.collectAsStateWithLifecycle(component.initialMedium.status) + val isAdult by component.isAdult.collectAsStateWithLifecycle(component.initialMedium.isAdult) + val chapters by component.chapters.collectAsStateWithLifecycle(component.initialMedium.chapters) + val volumes by component.volumes.collectAsStateWithLifecycle(component.initialMedium.volumes) Row( modifier = Modifier.fillMaxWidth(), @@ -128,7 +125,21 @@ fun CoverSection( imageVector = Icons.AutoMirrored.Rounded.List, contentDescription = null ) - Text(text = "$episodes Episodes") + Text(text = pluralStringResource(SharedRes.plurals.episodes, episodes, episodes)) + } + } else { + if (chapters > -1) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Icon( + imageVector = Icons.Rounded.AutoStories, + contentDescription = null + ) + Text(text = pluralStringResource(SharedRes.plurals.chapters, chapters, chapters)) + } } } if (duration > -1) { @@ -141,7 +152,21 @@ fun CoverSection( imageVector = Icons.Rounded.Timelapse, contentDescription = null ) - Text(text = "${duration}min / Episode") + Text(text = stringResource(SharedRes.strings.duration_per_episode, duration)) + } + } else { + if (volumes > -1) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Icon( + imageVector = Icons.Rounded.Book, + contentDescription = null + ) + Text(text = pluralStringResource(SharedRes.plurals.volumes, volumes, volumes)) + } } } Row( diff --git a/composeApp/src/commonMain/moko-resources/base/plurals.xml b/composeApp/src/commonMain/moko-resources/base/plurals.xml new file mode 100644 index 0000000..f9b4587 --- /dev/null +++ b/composeApp/src/commonMain/moko-resources/base/plurals.xml @@ -0,0 +1,15 @@ + + + + %d Episode + %d Episodes + + + %d Chapter + %d Chapters + + + %d Volume + %d Volumes + + \ No newline at end of file diff --git a/composeApp/src/commonMain/moko-resources/base/strings.xml b/composeApp/src/commonMain/moko-resources/base/strings.xml index e79ddf3..e1ab238 100644 --- a/composeApp/src/commonMain/moko-resources/base/strings.xml +++ b/composeApp/src/commonMain/moko-resources/base/strings.xml @@ -71,4 +71,5 @@ Paused Repeating Install + %dmin / Episode From 1329c563846a151c7229b51e4bfd75795a7e3751 Mon Sep 17 00:00:00 2001 From: DatLag Date: Sat, 11 May 2024 16:16:50 +0200 Subject: [PATCH 22/23] commonize design --- .../component/DomainSection.android.kt | 10 ++++-- .../component/TranslateButton.android.kt | 3 +- .../screen/home/component/airing/Airing.kt | 3 +- .../screen/home/component/airing/Episode.kt | 7 ++-- .../home/dialog/settings/SettingsDialog.kt | 7 ++-- .../dialog/settings/component/AdultSection.kt | 6 ++-- .../settings/component/CharacterSection.kt | 6 ++-- .../dialog/settings/component/ColorSection.kt | 3 +- .../dialog/settings/component/TitleSection.kt | 6 ++-- .../screen/medium/component/AdultSection.kt | 3 +- .../medium/component/CollapsingToolbar.kt | 3 +- .../medium/component/DescriptionSection.kt | 33 ++++++++++--------- .../dialog/character/CharacterDialog.kt | 10 +++--- .../moko-resources/base/strings.xml | 6 ++++ 14 files changed, 66 insertions(+), 40 deletions(-) diff --git a/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/dialog/settings/component/DomainSection.android.kt b/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/dialog/settings/component/DomainSection.android.kt index 891b37c..53f37c1 100644 --- a/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/dialog/settings/component/DomainSection.android.kt +++ b/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/dialog/settings/component/DomainSection.android.kt @@ -7,6 +7,8 @@ import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.Share +import androidx.compose.material.icons.rounded.Check +import androidx.compose.material.icons.rounded.Share import androidx.compose.material3.Icon import androidx.compose.material3.Switch import androidx.compose.material3.SwitchDefaults @@ -18,8 +20,10 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp +import dev.datlag.aniflow.SharedRes import dev.datlag.aniflow.other.DomainVerifier import dev.datlag.tooling.decompose.lifecycle.collectAsStateWithLifecycle +import dev.icerock.moko.resources.compose.stringResource @Composable actual fun DomainSection(modifier: Modifier) { @@ -37,11 +41,11 @@ actual fun DomainSection(modifier: Modifier) { horizontalArrangement = Arrangement.spacedBy(8.dp) ) { Icon( - imageVector = Icons.Filled.Share, + imageVector = Icons.Rounded.Share, contentDescription = null, ) Text( - text = "Open Links" + text = stringResource(SharedRes.strings.open_links) ) Spacer(modifier = Modifier.weight(1F)) Switch( @@ -54,7 +58,7 @@ actual fun DomainSection(modifier: Modifier) { if (verified) { Icon( modifier = Modifier.size(SwitchDefaults.IconSize), - imageVector = Icons.Default.Check, + imageVector = Icons.Rounded.Check, contentDescription = null ) } diff --git a/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/component/TranslateButton.android.kt b/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/component/TranslateButton.android.kt index 589b819..7a6f5cb 100644 --- a/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/component/TranslateButton.android.kt +++ b/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/component/TranslateButton.android.kt @@ -4,6 +4,7 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Translate +import androidx.compose.material.icons.rounded.Translate import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Modifier @@ -102,7 +103,7 @@ actual fun TranslateButton( } else { Icon( modifier = Modifier.size(ButtonDefaults.IconSize), - imageVector = Icons.Default.Translate, + imageVector = Icons.Rounded.Translate, contentDescription = null ) } diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/component/airing/Airing.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/component/airing/Airing.kt index 26ac5f2..69ceb47 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/component/airing/Airing.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/component/airing/Airing.kt @@ -3,6 +3,7 @@ package dev.datlag.aniflow.ui.navigation.screen.home.component.airing import androidx.compose.foundation.layout.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Schedule +import androidx.compose.material.icons.rounded.Schedule import androidx.compose.material3.Icon import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme @@ -28,7 +29,7 @@ fun Airing(airingAt: Int, color: Color = LocalContentColor.current) { verticalAlignment = Alignment.CenterVertically ) { Icon( - imageVector = Icons.Default.Schedule, + imageVector = Icons.Rounded.Schedule, contentDescription = null, modifier = Modifier.size(24.dp), tint = color diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/component/airing/Episode.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/component/airing/Episode.kt index bcc046c..4ab96f0 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/component/airing/Episode.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/component/airing/Episode.kt @@ -3,6 +3,7 @@ package dev.datlag.aniflow.ui.navigation.screen.home.component.airing import androidx.compose.foundation.layout.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Slideshow +import androidx.compose.material.icons.rounded.Slideshow import androidx.compose.material3.Icon import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme @@ -13,6 +14,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import dev.datlag.aniflow.SharedRes +import dev.icerock.moko.resources.compose.stringResource @Composable fun Episode(value: Int, color: Color = LocalContentColor.current) { @@ -23,13 +26,13 @@ fun Episode(value: Int, color: Color = LocalContentColor.current) { verticalAlignment = Alignment.CenterVertically ) { Icon( - imageVector = Icons.Default.Slideshow, + imageVector = Icons.Rounded.Slideshow, contentDescription = null, modifier = Modifier.size(24.dp), tint = color ) Text( - text = "Episode $value", + text = stringResource(SharedRes.strings.episode_number, value), style = MaterialTheme.typography.titleMedium, textAlign = TextAlign.Center, color = color diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/dialog/settings/SettingsDialog.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/dialog/settings/SettingsDialog.kt index 56eaedd..b1bb025 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/dialog/settings/SettingsDialog.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/dialog/settings/SettingsDialog.kt @@ -12,6 +12,7 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.* import androidx.compose.material.icons.rounded.ArrowBackIosNew import androidx.compose.material.icons.rounded.Code +import androidx.compose.material.icons.rounded.NotInterested import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment @@ -120,17 +121,17 @@ fun SettingsScreen(component: SettingsComponent) { ) { if (isLoggedIn) { Icon( - imageVector = Icons.Default.NotInterested, + imageVector = Icons.Rounded.NotInterested, contentDescription = null, ) - Text(text = "Logout") + Text(text = stringResource(SharedRes.strings.logout)) } else { Image( modifier = Modifier.size(24.dp).clip(CircleShape), painter = painterResource(SharedRes.images.anilist), contentDescription = null, ) - Text(text = "Login") + Text(text = stringResource(SharedRes.strings.login)) } } } diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/dialog/settings/component/AdultSection.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/dialog/settings/component/AdultSection.kt index b78002a..4338ed9 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/dialog/settings/component/AdultSection.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/dialog/settings/component/AdultSection.kt @@ -7,6 +7,8 @@ import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.NoAdultContent +import androidx.compose.material.icons.rounded.Check +import androidx.compose.material.icons.rounded.NoAdultContent import androidx.compose.material3.Icon import androidx.compose.material3.Switch import androidx.compose.material3.Text @@ -35,7 +37,7 @@ fun AdultSection( horizontalArrangement = Arrangement.spacedBy(8.dp) ) { Icon( - imageVector = Icons.Default.NoAdultContent, + imageVector = Icons.Rounded.NoAdultContent, contentDescription = null, ) Text( @@ -49,7 +51,7 @@ fun AdultSection( if (adultContent) { Icon( modifier = Modifier.size(SwitchDefaults.IconSize), - imageVector = Icons.Default.Check, + imageVector = Icons.Rounded.Check, contentDescription = null ) } diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/dialog/settings/component/CharacterSection.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/dialog/settings/component/CharacterSection.kt index 3c89c14..e50f70e 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/dialog/settings/component/CharacterSection.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/dialog/settings/component/CharacterSection.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ExpandMore import androidx.compose.material.icons.filled.PersonPin +import androidx.compose.material.icons.rounded.ExpandMore import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -22,6 +23,7 @@ import com.maxkeppeler.sheets.option.models.DisplayMode import com.maxkeppeler.sheets.option.models.Option import com.maxkeppeler.sheets.option.models.OptionConfig import com.maxkeppeler.sheets.option.models.OptionSelection +import dev.datlag.aniflow.SharedRes import dev.datlag.aniflow.common.toComposeString import dev.datlag.tooling.decompose.lifecycle.collectAsStateWithLifecycle import dev.datlag.aniflow.settings.model.CharLanguage as SettingsChar @@ -67,14 +69,14 @@ fun CharacterSection( contentDescription = null ) Text( - text = "Character Language" + text = stringResource(SharedRes.strings.char_language) ) Spacer(modifier = Modifier.weight(1F)) IconButton( onClick = { useCase.show() } ) { Icon( - imageVector = Icons.Default.ExpandMore, + imageVector = Icons.Rounded.ExpandMore, contentDescription = null ) } diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/dialog/settings/component/ColorSection.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/dialog/settings/component/ColorSection.kt index c14a620..3fe90be 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/dialog/settings/component/ColorSection.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/dialog/settings/component/ColorSection.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Circle import androidx.compose.material.icons.filled.Palette +import androidx.compose.material.icons.rounded.Palette import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -86,7 +87,7 @@ fun ColorSection( horizontalArrangement = Arrangement.spacedBy(8.dp) ) { Icon( - imageVector = Icons.Default.Palette, + imageVector = Icons.Rounded.Palette, contentDescription = null, ) Text( diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/dialog/settings/component/TitleSection.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/dialog/settings/component/TitleSection.kt index 7607813..f92012f 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/dialog/settings/component/TitleSection.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/dialog/settings/component/TitleSection.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ExpandMore +import androidx.compose.material.icons.rounded.ExpandMore import androidx.compose.material.icons.rounded.Title import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon @@ -22,6 +23,7 @@ import com.maxkeppeler.sheets.option.models.DisplayMode import com.maxkeppeler.sheets.option.models.Option import com.maxkeppeler.sheets.option.models.OptionConfig import com.maxkeppeler.sheets.option.models.OptionSelection +import dev.datlag.aniflow.SharedRes import dev.datlag.aniflow.common.toComposeString import dev.datlag.aniflow.settings.model.TitleLanguage as SettingsTitle import dev.datlag.tooling.decompose.lifecycle.collectAsStateWithLifecycle @@ -67,14 +69,14 @@ fun TitleSection( contentDescription = null ) Text( - text = "Title Language" + text = stringResource(SharedRes.strings.title_language) ) Spacer(modifier = Modifier.weight(1F)) IconButton( onClick = { useCase.show() } ) { Icon( - imageVector = Icons.Default.ExpandMore, + imageVector = Icons.Rounded.ExpandMore, contentDescription = null ) } diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/component/AdultSection.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/component/AdultSection.kt index ad9a44f..a8461a4 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/component/AdultSection.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/component/AdultSection.kt @@ -4,6 +4,7 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBackIosNew +import androidx.compose.material.icons.rounded.ArrowBackIosNew import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Icon @@ -60,7 +61,7 @@ fun AdultSection( ) { Icon( modifier = Modifier.size(ButtonDefaults.IconSize), - imageVector = Icons.Default.ArrowBackIosNew, + imageVector = Icons.Rounded.ArrowBackIosNew, contentDescription = null ) Spacer(modifier = Modifier.size(ButtonDefaults.IconSpacing)) diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/component/CollapsingToolbar.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/component/CollapsingToolbar.kt index 18e4e1e..d41d2ce 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/component/CollapsingToolbar.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/component/CollapsingToolbar.kt @@ -15,6 +15,7 @@ import androidx.compose.material.icons.filled.Share import androidx.compose.material.icons.rounded.ArrowBackIosNew import androidx.compose.material.icons.rounded.Favorite import androidx.compose.material.icons.rounded.FavoriteBorder +import androidx.compose.material.icons.rounded.Share import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment @@ -210,7 +211,7 @@ fun CollapsingToolbar( } ) { Icon( - imageVector = Icons.Default.Share, + imageVector = Icons.Rounded.Share, contentDescription = null ) } diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/component/DescriptionSection.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/component/DescriptionSection.kt index c531649..fd54e0f 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/component/DescriptionSection.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/component/DescriptionSection.kt @@ -3,9 +3,12 @@ package dev.datlag.aniflow.ui.navigation.screen.medium.component import androidx.compose.animation.core.animateIntAsState import androidx.compose.animation.core.tween import androidx.compose.foundation.layout.* +import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ExpandLess import androidx.compose.material.icons.filled.ExpandMore +import androidx.compose.material.icons.rounded.ExpandLess +import androidx.compose.material.icons.rounded.ExpandMore import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme @@ -68,20 +71,20 @@ fun DescriptionSection( animationSpec = tween() ) - Text( - modifier = Modifier.padding(horizontal = 16.dp).onClick { - descriptionExpanded = !descriptionExpanded - }, - text = (translatedDescription ?: description)!!.htmlToAnnotatedString(), - maxLines = max(animatedLines, 1), - softWrap = true, - overflow = TextOverflow.Ellipsis, - onTextLayout = { result -> - if (!descriptionExpanded) { - descriptionExpandable = result.hasVisualOverflow + SelectionContainer { + Text( + modifier = Modifier.padding(horizontal = 16.dp), + text = (translatedDescription ?: description)!!.htmlToAnnotatedString(), + maxLines = max(animatedLines, 1), + softWrap = true, + overflow = TextOverflow.Ellipsis, + onTextLayout = { result -> + if (!descriptionExpanded) { + descriptionExpandable = result.hasVisualOverflow + } } - } - ) + ) + } if (descriptionExpandable) { IconButton( modifier = Modifier.fillMaxWidth(), @@ -90,9 +93,9 @@ fun DescriptionSection( } ) { val icon = if (descriptionExpanded) { - Icons.Default.ExpandLess + Icons.Rounded.ExpandLess } else { - Icons.Default.ExpandMore + Icons.Rounded.ExpandMore } Icon( diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/dialog/character/CharacterDialog.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/dialog/character/CharacterDialog.kt index f005460..7c8b93b 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/dialog/character/CharacterDialog.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/dialog/character/CharacterDialog.kt @@ -10,9 +10,7 @@ import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.* -import androidx.compose.material.icons.rounded.ArrowBackIosNew -import androidx.compose.material.icons.rounded.Favorite -import androidx.compose.material.icons.rounded.FavoriteBorder +import androidx.compose.material.icons.rounded.* import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment @@ -185,7 +183,7 @@ fun CharacterDialog(component: CharacterComponent) { horizontalAlignment = Alignment.CenterHorizontally, ) { Icon( - imageVector = Icons.Filled.Bloodtype, + imageVector = Icons.Rounded.Bloodtype, contentDescription = null ) Text( @@ -206,7 +204,7 @@ fun CharacterDialog(component: CharacterComponent) { horizontalAlignment = Alignment.CenterHorizontally, ) { Icon( - imageVector = Icons.Filled.Man4, + imageVector = Icons.Rounded.Man4, contentDescription = null ) Text( @@ -227,7 +225,7 @@ fun CharacterDialog(component: CharacterComponent) { horizontalAlignment = Alignment.CenterHorizontally, ) { Icon( - imageVector = Icons.Filled.Cake, + imageVector = Icons.Rounded.Cake, contentDescription = null ) Text( diff --git a/composeApp/src/commonMain/moko-resources/base/strings.xml b/composeApp/src/commonMain/moko-resources/base/strings.xml index e1ab238..9651c3a 100644 --- a/composeApp/src/commonMain/moko-resources/base/strings.xml +++ b/composeApp/src/commonMain/moko-resources/base/strings.xml @@ -72,4 +72,10 @@ Repeating Install %dmin / Episode + Episode %d + Title Language + Character Language + Login + Logout + Open Links From 89a397094ee30442ab21e86b28887ae9677e3fac Mon Sep 17 00:00:00 2001 From: DatLag Date: Sat, 11 May 2024 16:35:23 +0200 Subject: [PATCH 23/23] remove unused strings --- composeApp/src/commonMain/moko-resources/base/strings.xml | 2 -- 1 file changed, 2 deletions(-) diff --git a/composeApp/src/commonMain/moko-resources/base/strings.xml b/composeApp/src/commonMain/moko-resources/base/strings.xml index 9651c3a..908f4ce 100644 --- a/composeApp/src/commonMain/moko-resources/base/strings.xml +++ b/composeApp/src/commonMain/moko-resources/base/strings.xml @@ -46,8 +46,6 @@ Gray Custom Similarity - Maximum: %s\nAverage: %s - Average: %s Romaji English Native