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