diff --git a/android/app/src/main/kotlin/nl/jknaapen/fladder/VideoPlayerActivity.kt b/android/app/src/main/kotlin/nl/jknaapen/fladder/VideoPlayerActivity.kt index 90ca7c2..08e20a1 100644 --- a/android/app/src/main/kotlin/nl/jknaapen/fladder/VideoPlayerActivity.kt +++ b/android/app/src/main/kotlin/nl/jknaapen/fladder/VideoPlayerActivity.kt @@ -47,7 +47,7 @@ fun VideoPlayerScreen( ) { val leanBackEnabled = leanBackEnabled(LocalContext.current) ExoPlayer { player -> - ScaledContent(if (leanBackEnabled) 0.75f else 1f) { + ScaledContent(if (leanBackEnabled) 0.7f else 1f) { CustomVideoControls(player) } } diff --git a/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/controls/CustomIconButton.kt b/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/controls/CustomIconButton.kt index 59264aa..1ef4d40 100644 --- a/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/controls/CustomIconButton.kt +++ b/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/controls/CustomIconButton.kt @@ -71,7 +71,7 @@ internal fun CustomIconButton( contentAlignment = Alignment.Center ) { CompositionLocalProvider(LocalContentColor provides currentContentColor) { - Box(modifier = Modifier.padding(8.dp)) { + Box(modifier = Modifier.padding(12.dp)) { icon() } } diff --git a/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/controls/ItemHeader.kt b/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/controls/ItemHeader.kt index 59b20cb..d55053a 100644 --- a/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/controls/ItemHeader.kt +++ b/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/controls/ItemHeader.kt @@ -4,7 +4,6 @@ import PlayableData import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -13,19 +12,20 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp import coil3.compose.AsyncImage @Composable -fun ItemHeader(state: PlayableData?) { +fun ItemHeader( + modifier: Modifier = Modifier, + state: PlayableData? +) { val title = state?.title val logoUrl = state?.logoUrl Box( - modifier = Modifier + modifier = modifier .fillMaxWidth() - .statusBarsPadding() - .padding(16.dp), + .statusBarsPadding(), contentAlignment = Alignment.CenterStart ) { if (!logoUrl.isNullOrBlank()) { diff --git a/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/controls/ProgressBar.kt b/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/controls/ProgressBar.kt index dcee415..8289ee0 100644 --- a/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/controls/ProgressBar.kt +++ b/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/controls/ProgressBar.kt @@ -27,6 +27,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue @@ -51,7 +52,6 @@ import androidx.compose.ui.input.key.Key.Companion.DirectionRight import androidx.compose.ui.input.key.Key.Companion.Enter import androidx.compose.ui.input.key.Key.Companion.Escape import androidx.compose.ui.input.key.Key.Companion.Spacebar -import androidx.compose.ui.input.key.KeyEvent import androidx.compose.ui.input.key.KeyEventType import androidx.compose.ui.input.key.key import androidx.compose.ui.input.key.onKeyEvent @@ -63,12 +63,14 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastCoerceIn import androidx.media3.exoplayer.ExoPlayer +import kotlinx.coroutines.delay import nl.jknaapen.fladder.objects.VideoPlayerObject import nl.jknaapen.fladder.utility.formatTime import kotlin.math.max import kotlin.math.min import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.seconds import kotlin.time.DurationUnit import kotlin.time.toDuration @@ -259,7 +261,7 @@ internal fun RowScope.SimpleProgressBar( modifier = Modifier .focusable(enabled = false) .fillMaxWidth() - .height(9.dp) + .height(8.dp) .background( color = Color.White.copy( alpha = 0.15f @@ -313,10 +315,10 @@ internal fun RowScope.SimpleProgressBar( .focusable(enabled = false) .graphicsLayer { translationX = startPx - translationY = 13.dp.toPx() + translationY = 14.dp.toPx() } .width(segDp) - .height(7.dp) + .height(6.dp) .background( color = segment.color.copy(alpha = 0.75f), shape = RoundedCornerShape(8.dp) @@ -367,6 +369,30 @@ internal fun RowScope.SimpleProgressBar( ) + var direction by remember { mutableIntStateOf(0) } + var speed by remember { mutableLongStateOf(1L) } + val scrubSpeedDivider = 15L + + val lastInteraction = remember { mutableLongStateOf(System.currentTimeMillis()) } + + // Restart the multiplier + LaunchedEffect(lastInteraction.longValue) { + delay(500.milliseconds) + speed = 1L + } + + fun updateLastInteraction() { + lastInteraction.longValue = System.currentTimeMillis() + } + + val scrubSpeed = playbackData?.trickPlayModel?.interval ?: 5.seconds.inWholeMilliseconds + + fun scrubSpeedResult(): Long { + return (scrubSpeed * (speed / scrubSpeedDivider).coerceIn( + 1L..60.seconds.inWholeMilliseconds + )) + } + //Thumb Box( modifier = Modifier @@ -379,7 +405,7 @@ internal fun RowScope.SimpleProgressBar( } } .focusable(enabled = true) - .onKeyEvent { keyEvent: KeyEvent -> + .onKeyEvent { keyEvent -> if (keyEvent.type != KeyEventType.KeyDown) return@onKeyEvent false onUserInteraction() @@ -392,25 +418,42 @@ internal fun RowScope.SimpleProgressBar( } DirectionLeft -> { + if (direction != -1) { + direction = -1 + speed = 1L + } else { + speed++ + } if (!scrubbingTimeLine) { onTempPosChanged(position) onScrubbingChanged(true) player.pause() } - val newPos = max(0L, tempPosition - 3000L) + val newPos = max( + 0L, + tempPosition - scrubSpeedResult() + ) onTempPosChanged(newPos) + updateLastInteraction() true } DirectionRight -> { + if (direction != 1) { + direction = 1 + speed = 1L + } else { + speed++ + } if (!scrubbingTimeLine) { onTempPosChanged(position) onScrubbingChanged(true) player.pause() } val newPos = min(player.duration.takeIf { it > 0 } ?: 1L, - tempPosition + 3000L) + tempPosition + scrubSpeedResult()) onTempPosChanged(newPos) + updateLastInteraction() true } @@ -448,6 +491,7 @@ internal fun RowScope.SimpleProgressBar( } } + val MediaSegment.color: Color get() = when (this.type) { MediaSegmentType.COMMERCIAL -> Color.Magenta diff --git a/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/controls/SkipOverlay.kt b/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/controls/SkipOverlay.kt index 135d547..9631a78 100644 --- a/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/controls/SkipOverlay.kt +++ b/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/controls/SkipOverlay.kt @@ -88,15 +88,16 @@ internal fun BoxScope.SegmentSkipOverlay( AnimatedVisibility( activeSegment != null && skip == SegmentSkip.ASK, modifier = Modifier - .fillMaxSize() + .align(alignment = Alignment.CenterEnd) .padding(16.dp) .safeContentPadding() ) { CustomIconButton( modifier = modifier - .align(alignment = Alignment.CenterEnd) .focusRequester(focusRequester) .defaultSelected(true), + backgroundColor = Color.Black.copy(alpha = 0.5f), + enableScaledFocus = true, onClick = { activeSegment?.let { player.seekTo(it.end) diff --git a/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/controls/VideoPlayerControls.kt b/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/controls/VideoPlayerControls.kt index 281c1bb..c9ec56c 100644 --- a/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/controls/VideoPlayerControls.kt +++ b/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/controls/VideoPlayerControls.kt @@ -48,8 +48,11 @@ import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.input.key.Key.Companion.DirectionLeft +import androidx.compose.ui.input.key.Key.Companion.DirectionRight import androidx.compose.ui.input.key.KeyEvent import androidx.compose.ui.input.key.KeyEventType +import androidx.compose.ui.input.key.key import androidx.compose.ui.input.key.onKeyEvent import androidx.compose.ui.input.key.type import androidx.compose.ui.platform.LocalContext @@ -76,6 +79,7 @@ import nl.jknaapen.fladder.utility.ImmersiveSystemBars import nl.jknaapen.fladder.utility.defaultSelected import nl.jknaapen.fladder.utility.leanBackEnabled import nl.jknaapen.fladder.utility.visible +import kotlin.math.absoluteValue import kotlin.time.Duration.Companion.seconds @@ -98,7 +102,7 @@ fun CustomVideoControls( val buffering by VideoPlayerObject.buffering.collectAsState(true) val playing by VideoPlayerObject.playing.collectAsState(false) - val controlsPadding = 16.dp + val controlsPadding = 32.dp ImmersiveSystemBars(isImmersive = !showControls) @@ -128,6 +132,26 @@ fun CustomVideoControls( lastInteraction.longValue = System.currentTimeMillis() } + val forwardSpeed by PlayerSettingsObject.forwardSpeed.collectAsState(30.seconds) + val backwardSpeed by PlayerSettingsObject.backwardSpeed.collectAsState(15.seconds) + + val position by VideoPlayerObject.position.collectAsState(0L) + val player = VideoPlayerObject.implementation.player + val lastSeekInteraction = remember { mutableLongStateOf(System.currentTimeMillis()) } + + var currentSkipTime by remember { mutableLongStateOf(0L) } + + // Restart the multiplier + LaunchedEffect(lastSeekInteraction.longValue) { + delay(2.seconds) + player?.seekTo(position + currentSkipTime) + currentSkipTime = 0L + } + + fun updateSeekInteraction() { + lastSeekInteraction.longValue = System.currentTimeMillis() + } + Box( modifier = Modifier .fillMaxSize() @@ -139,7 +163,27 @@ fun CustomVideoControls( } .onKeyEvent { keyEvent: KeyEvent -> if (keyEvent.type != KeyEventType.KeyDown) return@onKeyEvent false + if (!showControls) { + when (keyEvent.key) { + DirectionLeft -> { + if (currentSkipTime == 0L) { + player?.seekTo(position - backwardSpeed.inWholeMilliseconds) + } + currentSkipTime -= backwardSpeed.inWholeMilliseconds + updateSeekInteraction() + return@onKeyEvent true + } + + DirectionRight -> { + if (currentSkipTime.absoluteValue == 0L) { + player?.seekTo(position + forwardSpeed.inWholeMilliseconds) + } + currentSkipTime += forwardSpeed.inWholeMilliseconds + updateSeekInteraction() + return@onKeyEvent true + } + } bottomControlFocusRequester.requestFocus() updateLastInteraction() return@onKeyEvent true @@ -193,7 +237,10 @@ fun CustomVideoControls( null ) state?.let { - ItemHeader(it) + ItemHeader( + modifier = Modifier.padding(controlsPadding), + it + ) } } if (!leanBackEnabled(LocalContext.current)) { @@ -217,7 +264,6 @@ fun CustomVideoControls( // Progress Bar Column( modifier = Modifier - .padding(horizontal = controlsPadding) .background( brush = Brush.linearGradient( colors = listOf( @@ -228,6 +274,7 @@ fun CustomVideoControls( end = Offset(0f, Float.POSITIVE_INFINITY) ), ) + .padding(horizontal = controlsPadding) .displayCutoutPadding() .padding(top = 8.dp, bottom = controlsPadding) ) { @@ -250,10 +297,10 @@ fun CustomVideoControls( RightButtons(showAudioDialog, showSubDialog) } } - } } SegmentSkipOverlay() + SeekOverlay(value = currentSkipTime) if (buffering && !playing) { CircularProgressIndicator( modifier = Modifier diff --git a/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/dialogs/AudioSelection.kt b/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/dialogs/AudioSelection.kt index 9873af7..8b7403a 100644 --- a/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/dialogs/AudioSelection.kt +++ b/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/dialogs/AudioSelection.kt @@ -10,13 +10,16 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.unit.dp import androidx.media3.common.util.UnstableApi import androidx.media3.exoplayer.ExoPlayer import nl.jknaapen.fladder.objects.VideoPlayerObject import nl.jknaapen.fladder.utility.clearAudioTrack -import nl.jknaapen.fladder.utility.defaultSelected +import nl.jknaapen.fladder.utility.conditional import nl.jknaapen.fladder.utility.setInternalAudioTrack @OptIn(UnstableApi::class) @@ -29,6 +32,8 @@ fun AudioPicker( val audioTracks by VideoPlayerObject.audioTracks.collectAsState(listOf()) val internalAudioTracks by VideoPlayerObject.exoAudioTracks + val focusRequester = remember { FocusRequester() } + val listState = rememberLazyListState() LaunchedEffect(selectedIndex) { @@ -53,7 +58,9 @@ fun AudioPicker( TrackButton( modifier = Modifier .fillMaxWidth() - .defaultSelected(selectedOff), + .conditional(selectedOff) { + focusRequester(focusRequester) + }, onClick = { VideoPlayerObject.setAudioTrackIndex(-1) player.clearAudioTrack() @@ -72,7 +79,9 @@ fun AudioPicker( TrackButton( modifier = Modifier .fillMaxWidth() - .defaultSelected(selected), + .conditional(selected) { + focusRequester(focusRequester) + }, onClick = { serverTrack?.index?.let { VideoPlayerObject.setAudioTrackIndex(it.toInt()) diff --git a/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/dialogs/ChapterSelectionSheet.kt b/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/dialogs/ChapterSelectionSheet.kt index 017df1b..268d51f 100644 --- a/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/dialogs/ChapterSelectionSheet.kt +++ b/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/dialogs/ChapterSelectionSheet.kt @@ -27,12 +27,14 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import coil3.compose.AsyncImage import nl.jknaapen.fladder.objects.VideoPlayerObject -import nl.jknaapen.fladder.utility.defaultSelected +import nl.jknaapen.fladder.utility.conditional @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -44,6 +46,8 @@ internal fun ChapterSelectionSheet( val chapters = playbackData?.chapters ?: listOf() val currentPosition by VideoPlayerObject.position.collectAsState(0L) + val focusRequester = remember { FocusRequester() } + if (chapters.isEmpty()) return var currentChapter: Chapter? by remember { @@ -56,10 +60,13 @@ internal fun ChapterSelectionSheet( val lazyListState = rememberLazyListState() - LaunchedEffect(chapters) { + LaunchedEffect(chapters, currentPosition) { + val chapter = chapters.indexOfCurrent(currentPosition) lazyListState.animateScrollToItem( - chapters.indexOfCurrent(currentPosition) + chapter ) + currentChapter = chapters[chapter] + focusRequester.requestFocus() } CustomModalBottomSheet( @@ -68,7 +75,7 @@ internal fun ChapterSelectionSheet( Column( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp) + .padding(horizontal = 16.dp, vertical = 16.dp) .wrapContentHeight(), verticalArrangement = Arrangement.spacedBy(8.dp) ) { @@ -88,15 +95,19 @@ internal fun ChapterSelectionSheet( Column( modifier = Modifier .background( - color = Color.Black.copy(alpha = 0.75f), + color = if (selectedChapter) Color.White.copy(alpha = 0.25f) else Color.Black.copy( + alpha = 0.75f + ), shape = RoundedCornerShape(8.dp) ) .border( width = 2.dp, - color = Color.White.copy(alpha = if (selectedChapter) 1f else 0f), + color = Color.White.copy(alpha = if (selectedChapter) 0.45f else 0f), shape = RoundedCornerShape(8.dp) ) - .defaultSelected(index == 0) + .conditional(selectedChapter) { + focusRequester(focusRequester) + } .onFocusChanged { if (it.isFocused) { currentChapter = chapter diff --git a/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/dialogs/SubtitlePicker.kt b/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/dialogs/SubtitlePicker.kt index 28213f5..eb24fb4 100644 --- a/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/dialogs/SubtitlePicker.kt +++ b/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/dialogs/SubtitlePicker.kt @@ -11,13 +11,16 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.unit.dp import androidx.media3.common.util.UnstableApi import androidx.media3.exoplayer.ExoPlayer import nl.jknaapen.fladder.objects.VideoPlayerObject import nl.jknaapen.fladder.utility.clearSubtitleTrack -import nl.jknaapen.fladder.utility.defaultSelected +import nl.jknaapen.fladder.utility.conditional import nl.jknaapen.fladder.utility.setInternalSubtitleTrack @OptIn(UnstableApi::class) @@ -30,13 +33,16 @@ fun SubtitlePicker( val subTitles by VideoPlayerObject.subtitleTracks.collectAsState(listOf()) val internalSubTracks by VideoPlayerObject.exoSubTracks + val focusRequester = remember { FocusRequester() } + val listState = rememberLazyListState() - LaunchedEffect(selectedIndex) { + LaunchedEffect(selectedIndex, subTitles) { if (selectedIndex == -1) return@LaunchedEffect listState.scrollToItem( subTitles.indexOfFirst { it.index == selectedIndex.toLong() } ) + focusRequester.requestFocus() } CustomModalBottomSheet( @@ -54,7 +60,9 @@ fun SubtitlePicker( TrackButton( modifier = Modifier .fillMaxWidth() - .defaultSelected(selectedOff), + .conditional(selectedOff) { + focusRequester(focusRequester) + }, onClick = { VideoPlayerObject.setSubtitleTrackIndex(-1) player.clearSubtitleTrack() @@ -73,7 +81,9 @@ fun SubtitlePicker( TrackButton( modifier = Modifier .fillMaxWidth() - .defaultSelected(selected), + .conditional(selected) { + focusRequester(focusRequester) + }, onClick = { serverSub?.index?.let { VideoPlayerObject.setSubtitleTrackIndex(it.toInt()) diff --git a/android/app/src/main/kotlin/nl/jknaapen/fladder/utility/ScaledContent.kt b/android/app/src/main/kotlin/nl/jknaapen/fladder/utility/ScaledContent.kt index fb5e07b..e039300 100644 --- a/android/app/src/main/kotlin/nl/jknaapen/fladder/utility/ScaledContent.kt +++ b/android/app/src/main/kotlin/nl/jknaapen/fladder/utility/ScaledContent.kt @@ -11,9 +11,11 @@ fun ScaledContent( content: @Composable () -> Unit ) { val density = LocalDensity.current + val fontScale = 1f / scale CompositionLocalProvider( LocalDensity provides Density( density = density.density * scale, + fontScale = fontScale ) ) { content() diff --git a/lib/screens/settings/widgets/settings_update_information.dart b/lib/screens/settings/widgets/settings_update_information.dart index 4db8175..c186b95 100644 --- a/lib/screens/settings/widgets/settings_update_information.dart +++ b/lib/screens/settings/widgets/settings_update_information.dart @@ -1,11 +1,17 @@ +import 'dart:developer'; +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:http/http.dart' as http; import 'package:markdown_widget/widget/markdown.dart'; +import 'package:path_provider/path_provider.dart'; import 'package:fladder/providers/settings/client_settings_provider.dart'; import 'package:fladder/providers/update_provider.dart'; import 'package:fladder/screens/settings/settings_list_tile.dart'; +import 'package:fladder/screens/shared/fladder_snackbar.dart'; import 'package:fladder/screens/shared/media/external_urls.dart'; import 'package:fladder/util/list_padding.dart'; import 'package:fladder/util/localization_helper.dart'; @@ -87,6 +93,8 @@ class UpdateInformation extends StatelessWidget { @override Widget build(BuildContext context) { + final apkDownload = + releaseInfo.preferredDownloads.entries.where((entry) => entry.value.toLowerCase().endsWith('.apk')).firstOrNull; return ExpansionTile( backgroundColor: releaseInfo.isNewerThanCurrent ? context.colors.primaryContainer : context.colors.surfaceContainer, @@ -107,6 +115,32 @@ class UpdateInformation extends StatelessWidget { ), ), ), + if (apkDownload != null) + FilledButton( + onPressed: () async { + try { + final response = await http.get(Uri.parse(apkDownload.value)); + + final tempDir = await getTemporaryDirectory(); + final apkPath = '${tempDir.path}/update.apk'; + + if (response.statusCode == 200) { + final file = File(apkPath); + await file.writeAsBytes(response.bodyBytes); + launchUrl(context, file.path); + } else { + throw Exception('Failed to download APK: ${response.statusCode}'); + } + } catch (e) { + if (context.mounted) { + fladderSnackbar(context, title: 'Failed to download update: $e'); + } + } + }, + child: const Text( + "Install", + ), + ), ...releaseInfo.preferredDownloads.entries.map( (entry) { return FilledButton( diff --git a/lib/screens/shared/media/detailed_banner.dart b/lib/screens/shared/media/detailed_banner.dart index dc494eb..8356e95 100644 --- a/lib/screens/shared/media/detailed_banner.dart +++ b/lib/screens/shared/media/detailed_banner.dart @@ -26,7 +26,7 @@ class DetailedBanner extends ConsumerStatefulWidget { } class _DetailedBannerState extends ConsumerState { - late ItemBaseModel selectedPoster = widget.posters.first; + late ValueNotifier selectedPoster = ValueNotifier(widget.posters.first); @override Widget build(BuildContext context) { @@ -45,8 +45,11 @@ class _DetailedBannerState extends ConsumerState { child: AspectRatio( aspectRatio: 1.8, child: CustomShaderMask( - child: FladderImage( - image: selectedPoster.images?.primary, + child: ValueListenableBuilder( + valueListenable: selectedPoster, + builder: (context, value, child) => FladderImage( + image: value.images?.primary, + ), ), ), ), @@ -68,20 +71,23 @@ class _DetailedBannerState extends ConsumerState { padding: const EdgeInsets.symmetric(horizontal: 16).copyWith(bottom: 4), child: FractionallySizedBox( widthFactor: AdaptiveLayout.viewSizeOf(context) <= ViewSize.phone ? 1.0 : 0.55, - child: OverviewHeader( - name: selectedPoster.parentBaseModel.name, - subTitle: selectedPoster.label(context), - image: selectedPoster.getPosters, - logoAlignment: AdaptiveLayout.viewSizeOf(context) <= ViewSize.phone - ? Alignment.center - : Alignment.centerLeft, - summary: selectedPoster.overview.summary, - productionYear: selectedPoster.overview.productionYear, - runTime: selectedPoster.overview.runTime, - genres: selectedPoster.overview.genreItems, - studios: selectedPoster.overview.studios, - officialRating: selectedPoster.overview.parentalRating, - communityRating: selectedPoster.overview.communityRating, + child: ValueListenableBuilder( + valueListenable: selectedPoster, + builder: (context, value, child) => OverviewHeader( + name: value.parentBaseModel.name, + subTitle: value.label(context), + image: value.getPosters, + logoAlignment: AdaptiveLayout.viewSizeOf(context) <= ViewSize.phone + ? Alignment.center + : Alignment.centerLeft, + summary: value.overview.summary, + productionYear: value.overview.productionYear, + runTime: value.overview.runTime, + genres: value.overview.genreItems, + studios: value.overview.studios, + officialRating: value.overview.parentalRating, + communityRating: value.overview.communityRating, + ), ), ), ), @@ -97,9 +103,7 @@ class _DetailedBannerState extends ConsumerState { context.ensureVisible( alignment: 1.0, ); - setState(() { - selectedPoster = poster; - }); + selectedPoster.value = poster; widget.onSelect(poster); }, ),