diff --git a/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/shared/ui/screen/initial/series/SeriesComponent.kt b/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/shared/ui/screen/initial/series/SeriesComponent.kt index 94e662cd..d944ec89 100644 --- a/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/shared/ui/screen/initial/series/SeriesComponent.kt +++ b/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/shared/ui/screen/initial/series/SeriesComponent.kt @@ -28,6 +28,8 @@ interface SeriesComponent : Component { val dbEpisodes: StateFlow> val nextEpisodeToWatch: Flow + val nextSeasonToWatch: Flow + fun retryLoadingSeries(): Any? fun goBack() @@ -37,4 +39,6 @@ interface SeriesComponent : Component { fun toggleFavorite(): Any? fun itemClicked(episode: Series.Episode): Any? fun itemLongClicked(episode: Series.Episode) + fun watchToggle(series: Series, episode: Series.Episode, watched: Boolean): Any? + fun switchToSeason(season: Series.Season): Any? } \ No newline at end of file diff --git a/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/shared/ui/screen/initial/series/SeriesScreen.kt b/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/shared/ui/screen/initial/series/SeriesScreen.kt index e2206e17..97e4d233 100644 --- a/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/shared/ui/screen/initial/series/SeriesScreen.kt +++ b/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/shared/ui/screen/initial/series/SeriesScreen.kt @@ -8,10 +8,7 @@ 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.filled.ArrowBackIosNew -import androidx.compose.material.icons.filled.Favorite -import androidx.compose.material.icons.filled.FavoriteBorder -import androidx.compose.material.icons.filled.PlayArrow +import androidx.compose.material.icons.filled.* import androidx.compose.material3.* import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass @@ -53,6 +50,7 @@ fun SeriesScreen(component: SeriesComponent) { val dialogState by component.dialog.subscribeAsState() val childState by component.child.subscribeAsState() val nextEpisode by component.nextEpisodeToWatch.collectAsStateWithLifecycle(initialValue = null) + val nextSeason by component.nextSeasonToWatch.collectAsStateWithLifecycle(initialValue = null) LaunchedEffect(href) { SchemeTheme.setCommon(href) @@ -79,11 +77,35 @@ fun SeriesScreen(component: SeriesComponent) { ) { Icon( imageVector = Icons.Default.PlayArrow, - contentDescription = next.episodeTitle + contentDescription = next.episodeTitle, + modifier = Modifier.size(ButtonDefaults.IconSize) ) + Spacer(modifier = Modifier.size(ButtonDefaults.IconSpacing)) Text(text = next.episodeTitle) } } + nextSeason?.let { next -> + ExtendedFloatingActionButton( + onClick = { + component.switchToSeason(next) + }, + modifier = Modifier.align(Alignment.BottomEnd).padding(16.dp) + ) { + val seasonText = if (next.title.toIntOrNull() != null) { + stringResource(SharedRes.strings.season_placeholder, next.title) + } else { + next.title + } + + Icon( + imageVector = Icons.Default.Redo, + contentDescription = next.title, + modifier = Modifier.size(ButtonDefaults.IconSize) + ) + Spacer(modifier = Modifier.size(ButtonDefaults.IconSpacing)) + Text(text = seasonText) + } + } } DisposableEffect(Unit) { @@ -284,6 +306,9 @@ private fun CompactScreen(component: SeriesComponent) { }, onEpisodeLongClick = { component.itemLongClicked(it) + }, + onWatchToggle = { episode, watched -> + component.watchToggle(current.series, episode, watched) } ) } @@ -423,6 +448,9 @@ private fun DefaultScreen(component: SeriesComponent) { }, onEpisodeLongClick = { component.itemLongClicked(it) + }, + onWatchToggle = { episode, watched -> + component.watchToggle(current.series, episode, watched) } ) } @@ -444,7 +472,8 @@ private fun LazyListScope.SeriesContent( dbEpisodes: List, loadingEpisode: String?, onEpisodeClick: (Series.Episode) -> Unit, - onEpisodeLongClick: (Series.Episode) -> Unit + onEpisodeLongClick: (Series.Episode) -> Unit, + onWatchToggle: (episode: Series.Episode, Boolean) -> Unit ) { items(content.episodes, key = { it.href }) { episode -> val dbEpisode = remember(dbEpisodes, episode.href) { @@ -460,6 +489,9 @@ private fun LazyListScope.SeriesContent( }, onLongClick = { onEpisodeLongClick(episode) + }, + onWatchToggle = { watched -> + onWatchToggle(episode, watched) } ) } diff --git a/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/shared/ui/screen/initial/series/SeriesScreenComponent.kt b/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/shared/ui/screen/initial/series/SeriesScreenComponent.kt index 03bfe30d..a894b9f3 100644 --- a/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/shared/ui/screen/initial/series/SeriesScreenComponent.kt +++ b/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/shared/ui/screen/initial/series/SeriesScreenComponent.kt @@ -15,6 +15,7 @@ import dev.datlag.burningseries.model.BSUtil import dev.datlag.burningseries.model.Series import dev.datlag.burningseries.model.common.collectSafe import dev.datlag.burningseries.model.common.safeCast +import dev.datlag.burningseries.model.common.suspendCatching import dev.datlag.burningseries.model.state.EpisodeAction import dev.datlag.burningseries.model.state.EpisodeState import dev.datlag.burningseries.model.state.SeriesAction @@ -149,7 +150,8 @@ class SeriesScreenComponent( seriesEpisodes.firstOrNull { it.number.equals(wantedNumber, true) } ?: seriesEpisodes.firstOrNull { - it.number.toIntOrNull() == (wantedNumber.toIntOrNull() ?: return@firstOrNull false) + val compareNumber = wantedNumber.toIntOrNull() ?: return@firstOrNull false + it.number.toIntOrNull() == compareNumber } } else { null @@ -158,6 +160,20 @@ class SeriesScreenComponent( } }.flowOn(ioDispatcher()) + override val nextSeasonToWatch = combine( + currentSeries.map { it?.seasons }, + currentSeries.map { it?.currentSeason }, + nextEpisodeToWatch + ) { seasons, current, episode -> + if (episode != null) { + null + } else { + seasons?.firstOrNull { + it.value == current?.value?.plus(1) + } + } + }.flowOn(ioDispatcher()) + private val navigation = SlotNavigation() override val child: Value> = childSlot( source = navigation, @@ -345,6 +361,33 @@ class SeriesScreenComponent( } } + override fun watchToggle(series: Series, episode: Series.Episode, watched: Boolean): Any? = ioScope().launchIO { + val maxLength = suspendCatching { + database.burningSeriesQueries.selectEpisodeByHref(episode.href).executeAsOneOrNull()?.length + }.getOrNull() ?: 0L + + val progress = if (watched) { + 0L + } else { + if (maxLength == 0L) { + Long.MIN_VALUE + } else { + maxLength + } + } + + database.burningSeriesQueries.updateEpisodeProgress( + progress = progress, + href = episode.href, + number = episode.number, + title = episode.title, + length = maxLength, + seriesHref = BSUtil.commonSeriesHref(series.href) + ) + } + + override fun switchToSeason(season: Series.Season): Any? = loadNewSeason(season) + private fun loadNewSeason(season: Series.Season) = ioScope().launchIO { (currentSeries.value ?: currentSeries.firstOrNull())?.let { series -> seriesStateMachine.dispatch(SeriesAction.Load(series.hrefBuilder(season.value))) diff --git a/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/shared/ui/screen/initial/series/component/EpisodeItem.kt b/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/shared/ui/screen/initial/series/component/EpisodeItem.kt index e6bc7e30..491209bd 100644 --- a/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/shared/ui/screen/initial/series/component/EpisodeItem.kt +++ b/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/shared/ui/screen/initial/series/component/EpisodeItem.kt @@ -10,7 +10,10 @@ import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha @@ -34,22 +37,29 @@ fun EpisodeItem( dbEpisode: Episode?, isLoading: Boolean, onClick: () -> Unit, - onLongClick: () -> Unit + onLongClick: () -> Unit, + onWatchToggle: (Boolean) -> Unit ) { val blurHash = remember(content.href) { BlurHash.random() } val enabled = content.hosters.isNotEmpty() - val length = remember(dbEpisode) { + val length = remember(dbEpisode?.length) { max(dbEpisode?.length ?: 0L, 0L) } - val progress = remember(dbEpisode) { - max(dbEpisode?.progress ?: 0L, 0L) + val progress = remember(dbEpisode?.progress) { + val value = dbEpisode?.progress ?: 0L + + if (value == Long.MIN_VALUE) { + Long.MIN_VALUE + } else { + max(value, 0L) + } } val isFinished = remember(length, progress) { if (length > 0L && progress > 0L) { (progress.toDouble() / length.toDouble() * 100.0).toFloat() >= 85F } else { - false + progress == Long.MIN_VALUE } } @@ -62,6 +72,9 @@ fun EpisodeItem( .clip(MaterialTheme.shapes.medium) .onClick( enabled = enabled, + onDoubleClick = { + onWatchToggle(isFinished) + }, onLongClick = onLongClick, onClick = onClick ).ifTrue(enabled) { bounceClick(0.95F) }.ifFalse(enabled) { alpha(0.5F) }, @@ -127,9 +140,9 @@ fun EpisodeItem( maxLines = 3 ) } - if (length != 0L && progress != 0L) { + if (length != 0L && max(progress, 0L) != 0L) { Text( - text = stringResource(SharedRes.strings.episode_progress, progress.toDuration(), length.toDuration()), + text = stringResource(SharedRes.strings.episode_progress, max(progress, 0L).toDuration(), length.toDuration()), style = MaterialTheme.typography.labelSmall ) }