diff --git a/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/navigation/DialogComponent.kt b/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/navigation/DialogComponent.kt new file mode 100644 index 00000000..3ee2140a --- /dev/null +++ b/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/navigation/DialogComponent.kt @@ -0,0 +1,5 @@ +package dev.datlag.burningseries.ui.navigation + +interface DialogComponent : Component { + fun dismiss() +} \ No newline at end of file diff --git a/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/DialogConfig.kt b/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/DialogConfig.kt new file mode 100644 index 00000000..1886d496 --- /dev/null +++ b/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/DialogConfig.kt @@ -0,0 +1,15 @@ +package dev.datlag.burningseries.ui.screen.initial.series + +import com.arkivanov.essenty.parcelable.Parcelable +import com.arkivanov.essenty.parcelable.Parcelize +import dev.datlag.burningseries.model.Series + +@Parcelize +sealed class DialogConfig : Parcelable { + + @Parcelize + data class Season( + val selected: Series.Season, + val seasons: List + ) : DialogConfig() +} \ No newline at end of file diff --git a/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/SeriesComponent.kt b/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/SeriesComponent.kt index b9d18976..3d38343d 100644 --- a/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/SeriesComponent.kt +++ b/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/SeriesComponent.kt @@ -1,12 +1,16 @@ package dev.datlag.burningseries.ui.screen.initial.series +import com.arkivanov.decompose.router.slot.ChildSlot +import com.arkivanov.decompose.value.Value import dev.datlag.burningseries.model.state.SeriesState import dev.datlag.burningseries.ui.navigation.Component +import dev.datlag.burningseries.ui.navigation.DialogComponent import kotlinx.coroutines.flow.StateFlow interface SeriesComponent : Component { val seriesState: StateFlow + val dialog: Value> val title: StateFlow val href: StateFlow @@ -15,4 +19,6 @@ interface SeriesComponent : Component { fun retryLoadingSeries(): Any? fun goBack() + + fun showDialog(config: DialogConfig) } \ No newline at end of file diff --git a/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/SeriesScreen.kt b/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/SeriesScreen.kt index 944bc770..44b1e04e 100644 --- a/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/SeriesScreen.kt +++ b/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/SeriesScreen.kt @@ -29,6 +29,7 @@ import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import com.arkivanov.decompose.extensions.compose.jetbrains.subscribeAsState import dev.datlag.burningseries.common.diagonalShape import dev.datlag.burningseries.common.lifecycle.collectAsStateWithLifecycle import dev.datlag.burningseries.common.onClick @@ -64,6 +65,7 @@ import kotlin.math.abs @Composable fun SeriesScreen(component: SeriesComponent) { val href by component.href.collectAsStateWithLifecycle() + val dialogState by component.dialog.subscribeAsState() SchemeTheme.setCommon(href) when (calculateWindowSizeClass().widthSizeClass) { @@ -77,6 +79,8 @@ fun SeriesScreen(component: SeriesComponent) { SchemeTheme.setCommon(null, scope) } } + + dialogState.child?.instance?.render() } @Composable @@ -303,7 +307,11 @@ private fun DefaultScreen(component: SeriesComponent) { selectedLanguage = current.series.currentLanguage, seasons = current.series.seasons, languages = current.series.languages, - onSeasonClick = { }, + onSeasonClick = { season -> + season?.let { + component.showDialog(DialogConfig.Season(it, current.series.seasons)) + } + }, onLanguageClick = { } ) } diff --git a/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/SeriesScreenComponent.kt b/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/SeriesScreenComponent.kt index 0dec3355..dca8bdd8 100644 --- a/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/SeriesScreenComponent.kt +++ b/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/SeriesScreenComponent.kt @@ -2,15 +2,21 @@ package dev.datlag.burningseries.ui.screen.initial.series import androidx.compose.runtime.Composable import com.arkivanov.decompose.ComponentContext +import com.arkivanov.decompose.router.slot.* +import com.arkivanov.decompose.value.Value import com.arkivanov.essenty.backhandler.BackCallback import com.arkivanov.essenty.backhandler.BackHandler +import dev.datlag.burningseries.common.defaultScope import dev.datlag.burningseries.common.ioDispatcher import dev.datlag.burningseries.common.ioScope import dev.datlag.burningseries.common.launchIO import dev.datlag.burningseries.model.BSUtil +import dev.datlag.burningseries.model.Series import dev.datlag.burningseries.model.state.SeriesAction import dev.datlag.burningseries.model.state.SeriesState import dev.datlag.burningseries.network.state.SeriesStateMachine +import dev.datlag.burningseries.ui.navigation.DialogComponent +import dev.datlag.burningseries.ui.screen.initial.series.dialog.season.SeasonDialogComponent import io.ktor.client.* import kotlinx.coroutines.flow.* import org.kodein.di.DI @@ -29,9 +35,29 @@ class SeriesScreenComponent( private val seriesStateMachine = SeriesStateMachine(httpClient, initialHref) override val seriesState: StateFlow = seriesStateMachine.state.flowOn(ioDispatcher()).stateIn(ioScope(), SharingStarted.Lazily, SeriesState.Loading(initialHref)) - override val title: StateFlow = seriesState.mapNotNull { it as? SeriesState.Success }.map { it.series.title }.stateIn(ioScope(), SharingStarted.Lazily, initialTitle) - override val href: StateFlow = seriesState.mapNotNull { it as? SeriesState.Success }.map { it.series.href }.stateIn(ioScope(), SharingStarted.Lazily, BSUtil.fixSeriesHref(initialHref)) - override val coverHref: StateFlow = seriesState.mapNotNull { it as? SeriesState.Success }.mapNotNull { it.series.coverHref }.stateIn(ioScope(), SharingStarted.Lazily, initialCoverHref) + private val currentSeries = seriesState.mapNotNull { it as? SeriesState.Success }.map { it.series }.stateIn(ioScope(), SharingStarted.Lazily, null) + override val title: StateFlow = currentSeries.mapNotNull { it?.title }.stateIn(ioScope(), SharingStarted.Lazily, initialTitle) + override val href: StateFlow = currentSeries.mapNotNull { it?.href }.stateIn(ioScope(), SharingStarted.Lazily, BSUtil.fixSeriesHref(initialHref)) + override val coverHref: StateFlow = currentSeries.mapNotNull { it?.coverHref }.stateIn(ioScope(), SharingStarted.Lazily, initialCoverHref) + + private val dialogNavigation = SlotNavigation() + private val _dialog = childSlot( + source = dialogNavigation + ) { config, slotContext -> + when (config) { + is DialogConfig.Season -> SeasonDialogComponent( + componentContext = slotContext, + di = di, + defaultSeason = config.selected, + seasons = config.seasons, + onDismissed = dialogNavigation::dismiss, + onSelected = { + loadNewSeason(it) + } + ) + } + } + override val dialog: Value> = _dialog private val backCallback = BackCallback(priority = Int.MAX_VALUE) { onGoBack() @@ -53,4 +79,14 @@ class SeriesScreenComponent( override fun goBack() { onGoBack() } + + override fun showDialog(config: DialogConfig) { + dialogNavigation.activate(config) + } + + private fun loadNewSeason(season: Series.Season) = ioScope().launchIO { + (currentSeries.value ?: currentSeries.firstOrNull())?.let { series -> + seriesStateMachine.dispatch(SeriesAction.Load(series.hrefBuilder(season.value))) + } + } } \ No newline at end of file diff --git a/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/component/SeasonAndLanguageButtons.kt b/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/component/SeasonAndLanguageButtons.kt index 5c314f9a..0dcc6d84 100644 --- a/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/component/SeasonAndLanguageButtons.kt +++ b/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/component/SeasonAndLanguageButtons.kt @@ -22,8 +22,8 @@ fun SeasonAndLanguageButtons( selectedLanguage: Series.Language?, seasons: List, languages: List, - onSeasonClick: () -> Unit, - onLanguageClick: () -> Unit, + onSeasonClick: (Series.Season?) -> Unit, + onLanguageClick: (Series.Language?) -> Unit, modifier: Modifier = Modifier ) { FlowRow( @@ -34,7 +34,7 @@ fun SeasonAndLanguageButtons( if (selectedSeason != null) { Button( onClick = { - onSeasonClick() + onSeasonClick(selectedSeason) }, enabled = seasons.size > 1, modifier = Modifier.weight(1F) @@ -50,7 +50,7 @@ fun SeasonAndLanguageButtons( if (selectedLanguage != null) { Button( onClick = { - onLanguageClick() + onLanguageClick(selectedLanguage) }, enabled = languages.size > 1, modifier = Modifier.weight(1F) diff --git a/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/dialog/season/SeasonComponent.kt b/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/dialog/season/SeasonComponent.kt new file mode 100644 index 00000000..4659ba48 --- /dev/null +++ b/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/dialog/season/SeasonComponent.kt @@ -0,0 +1,12 @@ +package dev.datlag.burningseries.ui.screen.initial.series.dialog.season + +import dev.datlag.burningseries.model.Series +import dev.datlag.burningseries.ui.navigation.DialogComponent + +interface SeasonComponent : DialogComponent { + + val defaultSeason: Series.Season + val seasons: List + + fun onConfirm(season: Series.Season) +} \ No newline at end of file diff --git a/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/dialog/season/SeasonDialog.kt b/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/dialog/season/SeasonDialog.kt new file mode 100644 index 00000000..b1a72f2e --- /dev/null +++ b/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/dialog/season/SeasonDialog.kt @@ -0,0 +1,111 @@ +package dev.datlag.burningseries.ui.screen.initial.series.dialog.season + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.selection.selectable +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.filled.Clear +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import dev.datlag.burningseries.shared.SharedRes +import dev.icerock.moko.resources.compose.stringResource + +@Composable +fun SeasonDialog(component: SeasonComponent) { + var selectedItem by remember { mutableStateOf(component.defaultSeason) } + + AlertDialog( + onDismissRequest = { + component.dismiss() + }, + title = { + Text( + text = stringResource(SharedRes.strings.select_season), + style = MaterialTheme.typography.headlineMedium, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + softWrap = true + ) + }, + text = { + Column( + modifier = Modifier.fillMaxWidth().verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + component.seasons.forEach { + val selected = selectedItem == it + Row( + modifier = Modifier.selectable( + selected = selected, + role = Role.RadioButton, + onClick = { selectedItem = it } + ).fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + RadioButton( + selected = selected, + onClick = null + ) + val seasonText = if (it.title.toIntOrNull() != null) { + stringResource(SharedRes.strings.season_placeholder, it.title) + } else { + it.title + } + Text( + text = seasonText, + overflow = TextOverflow.Ellipsis, + softWrap = true + ) + } + } + } + }, + confirmButton = { + Button( + onClick = { + if (component.defaultSeason != selectedItem) { + component.onConfirm(selectedItem) + } else { + component.dismiss() + } + } + ) { + Icon( + imageVector = Icons.Default.Check, + contentDescription = stringResource(SharedRes.strings.confirm), + modifier = Modifier.size(ButtonDefaults.IconSize) + ) + Spacer(modifier = Modifier.size(ButtonDefaults.IconSpacing)) + Text(text = stringResource(SharedRes.strings.confirm)) + } + }, + dismissButton = { + Button( + onClick = { + component.dismiss() + }, + modifier = Modifier.padding(bottom = 8.dp), + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.errorContainer, + contentColor = MaterialTheme.colorScheme.onErrorContainer + ) + ) { + Icon( + imageVector = Icons.Default.Clear, + contentDescription = stringResource(SharedRes.strings.close), + modifier = Modifier.size(ButtonDefaults.IconSize) + ) + Spacer(modifier = Modifier.size(ButtonDefaults.IconSpacing)) + Text(text = stringResource(SharedRes.strings.close)) + } + } + ) +} \ No newline at end of file diff --git a/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/dialog/season/SeasonDialogComponent.kt b/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/dialog/season/SeasonDialogComponent.kt new file mode 100644 index 00000000..b5526dbf --- /dev/null +++ b/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/dialog/season/SeasonDialogComponent.kt @@ -0,0 +1,30 @@ +package dev.datlag.burningseries.ui.screen.initial.series.dialog.season + +import androidx.compose.runtime.Composable +import com.arkivanov.decompose.ComponentContext +import dev.datlag.burningseries.model.Series +import org.kodein.di.DI + +class SeasonDialogComponent( + componentContext: ComponentContext, + override val di: DI, + override val defaultSeason: Series.Season, + override val seasons: List, + private val onDismissed: () -> Unit, + private val onSelected: (Series.Season) -> Unit +) : SeasonComponent, ComponentContext by componentContext { + + @Composable + override fun render() { + SeasonDialog(this) + } + + override fun dismiss() { + onDismissed() + } + + override fun onConfirm(season: Series.Season) { + onSelected(season) + onDismissed() + } +} \ No newline at end of file diff --git a/app/shared/src/commonMain/resources/MR/base/strings.xml b/app/shared/src/commonMain/resources/MR/base/strings.xml index 7d04bb36..92d44ff9 100644 --- a/app/shared/src/commonMain/resources/MR/base/strings.xml +++ b/app/shared/src/commonMain/resources/MR/base/strings.xml @@ -22,4 +22,7 @@ Downloading required packages for your platform, please wait Loading search information, please wait Error while loading search information + Select Season + Confirm + Close \ No newline at end of file diff --git a/model/src/commonMain/kotlin/dev/datlag/burningseries/model/BSUtil.kt b/model/src/commonMain/kotlin/dev/datlag/burningseries/model/BSUtil.kt index 606bc783..b9d1f84d 100644 --- a/model/src/commonMain/kotlin/dev/datlag/burningseries/model/BSUtil.kt +++ b/model/src/commonMain/kotlin/dev/datlag/burningseries/model/BSUtil.kt @@ -31,7 +31,7 @@ data object BSUtil { return rebuildHrefFromData(hrefDataFromHref(normalizeHref(href))) } - private fun hrefDataFromHref(href: String): Triple { + fun hrefDataFromHref(href: String): Triple { fun getTitle(): String { val newHref = if (href.startsWith("series/")) { href.substringAfter("series/") @@ -85,7 +85,7 @@ data object BSUtil { ) } - private fun rebuildHrefFromData(hrefData: Triple): String { + fun rebuildHrefFromData(hrefData: Triple): String { return if (hrefData.second != null && hrefData.third != null) { "serie/${hrefData.first}/${hrefData.second}/${hrefData.third}" } else if (hrefData.second != null) { diff --git a/model/src/commonMain/kotlin/dev/datlag/burningseries/model/Series.kt b/model/src/commonMain/kotlin/dev/datlag/burningseries/model/Series.kt index 20191a03..d3cd3e34 100644 --- a/model/src/commonMain/kotlin/dev/datlag/burningseries/model/Series.kt +++ b/model/src/commonMain/kotlin/dev/datlag/burningseries/model/Series.kt @@ -1,5 +1,7 @@ package dev.datlag.burningseries.model +import com.arkivanov.essenty.parcelable.Parcelable +import com.arkivanov.essenty.parcelable.Parcelize import dev.datlag.burningseries.model.common.getDigitsOrNull import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -36,11 +38,28 @@ data class Series( } } + fun hrefBuilder(season: Int? = currentSeason?.value, language: String = currentLanguage?.value ?: selectedLanguage): String { + val hrefData = BSUtil.hrefDataFromHref( + BSUtil.normalizeHref(href) + ) + + return BSUtil.fixSeriesHref( + BSUtil.rebuildHrefFromData( + Triple( + first = hrefData.first, + second = season?.toString() ?: hrefData.second, + third = language + ) + ) + ) + } + + @Parcelize @Serializable data class Season( @SerialName("value") val value: Int, @SerialName("title") val title: String - ) + ) : Parcelable @Serializable data class Language( diff --git a/model/src/commonMain/kotlin/dev/datlag/burningseries/model/state/SeriesState.kt b/model/src/commonMain/kotlin/dev/datlag/burningseries/model/state/SeriesState.kt index 0b2d9c6a..c959655f 100644 --- a/model/src/commonMain/kotlin/dev/datlag/burningseries/model/state/SeriesState.kt +++ b/model/src/commonMain/kotlin/dev/datlag/burningseries/model/state/SeriesState.kt @@ -10,4 +10,5 @@ sealed interface SeriesState { sealed interface SeriesAction { data object Retry : SeriesAction + data class Load(val href: String) : SeriesAction } \ No newline at end of file diff --git a/network/src/commonMain/kotlin/dev/datlag/burningseries/network/state/SeriesStateMachine.kt b/network/src/commonMain/kotlin/dev/datlag/burningseries/network/state/SeriesStateMachine.kt index db275b22..63a04d83 100644 --- a/network/src/commonMain/kotlin/dev/datlag/burningseries/network/state/SeriesStateMachine.kt +++ b/network/src/commonMain/kotlin/dev/datlag/burningseries/network/state/SeriesStateMachine.kt @@ -25,12 +25,26 @@ class SeriesStateMachine( state.override { SeriesState.Error(e.message ?: String()) } } } + on { action, state -> + state.mutate { + this.copy(href = action.href) + } + } } inState { on { _, state -> state.override { SeriesState.Loading(href) } } + on { action, state -> + state.override { SeriesState.Loading(action.href) } + } + } + + inState { + on { action, state -> + state.override { SeriesState.Loading(action.href) } + } } } }