From 06bea317289bc1c47b03398cc5d0ee70239c2800 Mon Sep 17 00:00:00 2001 From: Priveetee Date: Fri, 21 Aug 2026 13:56:43 +0200 Subject: [PATCH 01/49] fix: leave search before top-level navigation --- .../typetype/android/AppShellAdaptiveTest.kt | 47 +++++++++++++++++++ .../java/dev/typetype/android/AppShell.kt | 3 ++ 2 files changed, 50 insertions(+) diff --git a/app/src/androidTest/java/dev/typetype/android/AppShellAdaptiveTest.kt b/app/src/androidTest/java/dev/typetype/android/AppShellAdaptiveTest.kt index d076f190..b2e1a632 100644 --- a/app/src/androidTest/java/dev/typetype/android/AppShellAdaptiveTest.kt +++ b/app/src/androidTest/java/dev/typetype/android/AppShellAdaptiveTest.kt @@ -17,8 +17,10 @@ import androidx.compose.ui.semantics.SemanticsActions import androidx.compose.ui.test.junit4.v2.createAndroidComposeRule import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.assertIsFocused +import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onAllNodesWithTag import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performKeyInput import androidx.compose.ui.test.performSemanticsAction import androidx.compose.ui.input.key.Key @@ -31,6 +33,9 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import dev.typetype.android.core.ui.navigation.HomeRoute +import dev.typetype.android.core.ui.navigation.LibraryRoute +import dev.typetype.android.core.ui.navigation.SearchRoute +import dev.typetype.android.core.ui.navigation.SubscriptionsRoute import dev.typetype.android.domain.playback.PlaybackQueueController import dev.typetype.android.domain.playback.PlaybackQueueEntry import dev.typetype.android.domain.playback.PlaybackQueueSnapshot @@ -117,6 +122,48 @@ class AppShellAdaptiveTest { composeRule.onNodeWithText("Shorts").assertIsFocused() } + @Test + fun topLevelNavigationDoesNotRestoreSearchOverItsOpeningTab() { + composeRule.setContent { + val navController = rememberNavController() + AppShell( + navController = navController, + playerHostController = PlayerHostController(FakePlaybackQueueController()), + onOpenSearch = { navController.navigate(SearchRoute) }, + onOpenSettings = {}, + onPlayVideo = {}, + onOpenChannel = {}, + onOpenAccounts = {}, + onClosePlayback = {}, + ) { contentModifier -> + NavHost( + navController = navController, + startDestination = HomeRoute, + modifier = contentModifier, + ) { + composable { androidx.compose.material3.Text("Home content") } + composable { + androidx.compose.material3.Text("Subscriptions content") + } + composable { androidx.compose.material3.Text("Library content") } + composable { androidx.compose.material3.Text("Search content") } + } + } + } + + composeRule.onNodeWithContentDescription("Search").performClick() + composeRule.onNodeWithText("Search content").assertIsDisplayed() + composeRule.onNodeWithText("Subscriptions").performClick() + composeRule.onNodeWithText("Subscriptions content").assertIsDisplayed() + + composeRule.onNodeWithContentDescription("Search").performClick() + composeRule.onNodeWithText("Search content").assertIsDisplayed() + composeRule.onNodeWithText("Home").performClick() + + composeRule.onNodeWithText("Home content").assertIsDisplayed() + composeRule.onNodeWithText("Search content").assertDoesNotExist() + } + private fun setShellWidth(width: Dp) { setShellSize(width = width, height = 800.dp) } diff --git a/app/src/main/java/dev/typetype/android/AppShell.kt b/app/src/main/java/dev/typetype/android/AppShell.kt index 2ccb191a..1882316e 100644 --- a/app/src/main/java/dev/typetype/android/AppShell.kt +++ b/app/src/main/java/dev/typetype/android/AppShell.kt @@ -175,6 +175,9 @@ internal fun isAppChromeVisible( ): Boolean = playerTarget != PlayerHostTarget.Expanded && !isPlayerFullscreen private fun NavHostController.navigateTopLevel(route: Any) { + if (currentDestination?.hasRoute() == true) { + popBackStack() + } navigate(route) { popUpTo(graph.findStartDestination().id) { saveState = true } launchSingleTop = true From c89eb8d1706a61e8768da72efd2ccd681c5b63e4 Mon Sep 17 00:00:00 2001 From: Priveetee Date: Fri, 21 Aug 2026 13:56:50 +0200 Subject: [PATCH 02/49] feat: add continuous player motion model --- .../player/host/PlayerHostMotionLayout.kt | 108 ++++++++++++++++++ .../player/host/PlayerHostTransition.kt | 34 ++++++ 2 files changed, 142 insertions(+) create mode 100644 app/src/main/java/dev/typetype/android/feature/player/host/PlayerHostMotionLayout.kt create mode 100644 app/src/main/java/dev/typetype/android/feature/player/host/PlayerHostTransition.kt diff --git a/app/src/main/java/dev/typetype/android/feature/player/host/PlayerHostMotionLayout.kt b/app/src/main/java/dev/typetype/android/feature/player/host/PlayerHostMotionLayout.kt new file mode 100644 index 00000000..ff9888e4 --- /dev/null +++ b/app/src/main/java/dev/typetype/android/feature/player/host/PlayerHostMotionLayout.kt @@ -0,0 +1,108 @@ +package dev.typetype.android.feature.player.host + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.AnchoredDraggableState +import androidx.compose.foundation.gestures.DraggableAnchors +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.animateTo +import androidx.compose.foundation.gestures.anchoredDraggable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.SideEffect +import androidx.compose.runtime.remember +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.unit.IntOffset +import kotlinx.coroutines.flow.distinctUntilChanged + +@OptIn(ExperimentalFoundationApi::class) +@Composable +internal fun PlayerHostMotionLayout( + target: PlayerHostTarget, + requestStamp: Long, + miniAnchorPx: Float, + containerHeightPx: Float, + miniHeightPx: Float, + dragEnabled: Boolean, + miniContentEnabled: Boolean, + onTargetSettled: (PlayerHostTarget) -> Unit, + onProgressChange: (Float) -> Unit, + miniContent: @Composable () -> Unit, + expandedContent: @Composable (Modifier) -> Unit, +) { + val density = LocalDensity.current + val anchors = remember(miniAnchorPx) { + DraggableAnchors { + PlayerHostTarget.Expanded at 0f + PlayerHostTarget.Mini at miniAnchorPx + } + } + val anchoredState = remember { + AnchoredDraggableState(initialValue = target.draggableTarget()) + } + LaunchedEffect(anchors, requestStamp) { + val requestedTarget = target.draggableTarget() + anchoredState.updateAnchors(anchors, requestedTarget) + if (anchoredState.currentValue != requestedTarget) { + anchoredState.animateTo(requestedTarget) + } + } + LaunchedEffect(anchoredState) { + snapshotFlow { anchoredState.settledValue } + .distinctUntilChanged() + .collect(onTargetSettled) + } + + val transition = playerHostTransition( + offsetPx = anchoredState.offset, + miniAnchorPx = miniAnchorPx, + containerHeightPx = containerHeightPx, + miniHeightPx = miniHeightPx, + isAnimationRunning = anchoredState.isAnimationRunning, + ) + val isMini = miniContentEnabled && transition.isSettledMini + val height = with(density) { transition.heightPx.toDp() } + SideEffect { onProgressChange(transition.progress) } + + Box( + modifier = Modifier + .fillMaxWidth() + .height(height) + .testTag(PLAYER_HOST_OVERLAY_TAG) + .offset { IntOffset(0, transition.offsetPx) } + .anchoredDraggable( + state = anchoredState, + orientation = Orientation.Vertical, + enabled = dragEnabled, + ) + .background( + if (isMini) Color.Transparent + else Color.Black.copy(alpha = 1f - transition.progress), + ), + ) { + if (isMini) { + miniContent() + } else { + expandedContent( + Modifier.graphicsLayer { + alpha = transition.expandedContentAlpha + clip = true + }, + ) + } + } +} + +private fun PlayerHostTarget.draggableTarget(): PlayerHostTarget = when (this) { + PlayerHostTarget.Embedded, PlayerHostTarget.Hidden -> PlayerHostTarget.Expanded + else -> this +} diff --git a/app/src/main/java/dev/typetype/android/feature/player/host/PlayerHostTransition.kt b/app/src/main/java/dev/typetype/android/feature/player/host/PlayerHostTransition.kt new file mode 100644 index 00000000..8313e338 --- /dev/null +++ b/app/src/main/java/dev/typetype/android/feature/player/host/PlayerHostTransition.kt @@ -0,0 +1,34 @@ +package dev.typetype.android.feature.player.host + +import kotlin.math.roundToInt + +internal data class PlayerHostTransition( + val progress: Float, + val heightPx: Int, + val offsetPx: Int, + val expandedContentAlpha: Float, + val isSettledMini: Boolean, +) + +internal fun playerHostTransition( + offsetPx: Float, + miniAnchorPx: Float, + containerHeightPx: Float, + miniHeightPx: Float, + isAnimationRunning: Boolean, +): PlayerHostTransition { + val safeOffset = offsetPx.takeIf(Float::isFinite) ?: 0f + val progress = if (miniAnchorPx > 0f) { + (safeOffset / miniAnchorPx).coerceIn(0f, 1f) + } else { + 0f + } + val height = containerHeightPx + (miniHeightPx - containerHeightPx) * progress + return PlayerHostTransition( + progress = progress, + heightPx = height.roundToInt().coerceAtLeast(1), + offsetPx = safeOffset.roundToInt(), + expandedContentAlpha = (1f - progress * 0.35f).coerceIn(0f, 1f), + isSettledMini = progress >= 0.999f && !isAnimationRunning, + ) +} From ed2c35215f819f28ee1a696bd17154bd4b110c9d Mon Sep 17 00:00:00 2001 From: Priveetee Date: Fri, 21 Aug 2026 13:57:04 +0200 Subject: [PATCH 03/49] fix: align player gestures with window state --- .../dev/typetype/android/AppNavigation.kt | 9 +- .../java/dev/typetype/android/AppShell.kt | 45 +++++- .../feature/player/PlayerFullscreenEffect.kt | 11 +- .../android/feature/player/PlayerScreen.kt | 4 + .../player/components/MiniPlayerBar.kt | 5 - .../player/host/FullscreenOrientationState.kt | 68 +++++++++ .../android/feature/player/host/PlayerHost.kt | 131 +++++++++--------- 7 files changed, 191 insertions(+), 82 deletions(-) create mode 100644 app/src/main/java/dev/typetype/android/feature/player/host/FullscreenOrientationState.kt diff --git a/app/src/main/java/dev/typetype/android/AppNavigation.kt b/app/src/main/java/dev/typetype/android/AppNavigation.kt index 52cb2e03..e603ff88 100644 --- a/app/src/main/java/dev/typetype/android/AppNavigation.kt +++ b/app/src/main/java/dev/typetype/android/AppNavigation.kt @@ -78,8 +78,9 @@ internal fun AppTopBar( unreadNotificationsCount: Int, avatarUrl: String?, avatarFallbackLetter: String?, + modifier: Modifier = Modifier, ) { - androidx.compose.foundation.layout.Column { + androidx.compose.foundation.layout.Column(modifier = modifier) { TopAppBar( title = { Row(verticalAlignment = Alignment.CenterVertically) { @@ -146,8 +147,9 @@ internal fun AppBottomBar( fallbackTabRouteQualifiedName: String?, onTabClick: (Any) -> Unit, tabs: List = topLevelTabs, + modifier: Modifier = Modifier, ) { - androidx.compose.foundation.layout.Column { + androidx.compose.foundation.layout.Column(modifier = modifier) { HorizontalDivider(color = MaterialTheme.colorScheme.outline) NavigationBar( modifier = Modifier.testTag(APP_BOTTOM_NAVIGATION_TAG), @@ -172,9 +174,10 @@ internal fun AppNavigationRail( fallbackTabRouteQualifiedName: String?, onTabClick: (Any) -> Unit, tabs: List = topLevelTabs, + modifier: Modifier = Modifier, ) { NavigationRail( - modifier = Modifier + modifier = modifier .fillMaxHeight() .windowInsetsPadding(WindowInsets.systemBars) .testTag(APP_NAVIGATION_RAIL_TAG), diff --git a/app/src/main/java/dev/typetype/android/AppShell.kt b/app/src/main/java/dev/typetype/android/AppShell.kt index 1882316e..bce81b18 100644 --- a/app/src/main/java/dev/typetype/android/AppShell.kt +++ b/app/src/main/java/dev/typetype/android/AppShell.kt @@ -19,6 +19,8 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.semantics.clearAndSetSemantics import androidx.compose.ui.unit.dp import androidx.navigation.NavDestination.Companion.hasRoute import androidx.navigation.NavHostController @@ -74,8 +76,15 @@ fun AppShell( currentDestination?.hasRoute() == true var activeTabRoute by rememberSaveable { mutableStateOf(null) } var isPlayerFullscreen by remember { mutableStateOf(false) } + var playerTransitionProgress by remember { mutableStateOf(0f) } val playerHostState by playerHostController.state.collectAsStateWithLifecycle() val appChromeVisible = isAppChromeVisible(playerHostState.target, isPlayerFullscreen) + val phoneChromeAlpha = playerPhoneChromeAlpha( + hasVideo = playerHostState.videoUrl != null, + playerTarget = playerHostState.target, + isPlayerFullscreen = isPlayerFullscreen, + transitionProgress = playerTransitionProgress, + ) LaunchedEffect(currentDestination) { topLevelTabs.firstOrNull { currentDestination.matchesRoute(it.route) }?.let { activeTabRoute = it.route::class.qualifiedName @@ -108,7 +117,19 @@ fun AppShell( WindowInsets.systemBars }, topBar = { - if (isTopLevel && appChromeVisible) { + if (isTopLevel && !usesNavigationRail) { + AppTopBar( + onOpenSearch = onOpenSearch, + onOpenNotifications = onOpenNotifications, + onOpenSettings = onOpenSettings, + onOpenProfile = onOpenProfile, + notificationsAvailable = notificationsAvailable, + unreadNotificationsCount = unreadNotificationsCount, + avatarUrl = avatarUrl, + avatarFallbackLetter = avatarFallbackLetter, + modifier = Modifier.playerChrome(phoneChromeAlpha), + ) + } else if (isTopLevel && appChromeVisible) { AppTopBar( onOpenSearch = onOpenSearch, onOpenNotifications = onOpenNotifications, @@ -122,12 +143,13 @@ fun AppShell( } }, bottomBar = { - if (!usesNavigationRail && showsNavigation && appChromeVisible) { + if (!usesNavigationRail && showsNavigation) { AppBottomBar( currentDestination = currentDestination, fallbackTabRouteQualifiedName = activeTabRoute, onTabClick = navController::navigateTopLevel, tabs = navigationTabs, + modifier = Modifier.playerChrome(phoneChromeAlpha), ) } }, @@ -149,7 +171,7 @@ fun AppShell( PlayerHost( controller = playerHostController, bottomBarHeightDp = if ( - !usesNavigationRail && showsNavigation && appChromeVisible + !usesNavigationRail && showsNavigation ) { NAV_BAR_HEIGHT_DP } else { @@ -161,6 +183,7 @@ fun AppShell( onOpenChannel = onOpenChannel, onOpenAccounts = onOpenAccounts, onClosePlayback = onClosePlayback, + onTransitionProgressChange = { playerTransitionProgress = it }, content = {}, ) } @@ -169,11 +192,27 @@ fun AppShell( } } +private fun Modifier.playerChrome(alpha: Float): Modifier = + graphicsLayer { this.alpha = alpha.coerceIn(0f, 1f) } + .then(if (alpha <= 0f) Modifier.clearAndSetSemantics { } else Modifier) + internal fun isAppChromeVisible( playerTarget: PlayerHostTarget, isPlayerFullscreen: Boolean, ): Boolean = playerTarget != PlayerHostTarget.Expanded && !isPlayerFullscreen +internal fun playerPhoneChromeAlpha( + hasVideo: Boolean, + playerTarget: PlayerHostTarget, + isPlayerFullscreen: Boolean, + transitionProgress: Float, +): Float = when { + isPlayerFullscreen -> 0f + hasVideo && playerTarget != PlayerHostTarget.Embedded -> transitionProgress.coerceIn(0f, 1f) + isAppChromeVisible(playerTarget, false) -> 1f + else -> 0f +} + private fun NavHostController.navigateTopLevel(route: Any) { if (currentDestination?.hasRoute() == true) { popBackStack() diff --git a/app/src/main/java/dev/typetype/android/feature/player/PlayerFullscreenEffect.kt b/app/src/main/java/dev/typetype/android/feature/player/PlayerFullscreenEffect.kt index 3c85ef05..3b62518a 100644 --- a/app/src/main/java/dev/typetype/android/feature/player/PlayerFullscreenEffect.kt +++ b/app/src/main/java/dev/typetype/android/feature/player/PlayerFullscreenEffect.kt @@ -11,12 +11,16 @@ import dev.typetype.android.core.ui.util.WindowHelper internal fun PlayerFullscreenEffect( activity: Activity?, isFullscreen: Boolean, - onFullscreenChange: (Boolean) -> Unit, + locksLandscape: Boolean, ) { - LaunchedEffect(isFullscreen) { + LaunchedEffect(isFullscreen, locksLandscape) { val window = activity?.window ?: return@LaunchedEffect if (isFullscreen) { - activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE + activity.requestedOrientation = if (locksLandscape) { + ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE + } else { + ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED + } WindowHelper.toggleFullscreen(window, isFullscreen = true) } else { activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED @@ -29,7 +33,6 @@ internal fun PlayerFullscreenEffect( val window = activity?.window ?: return@onDispose activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED WindowHelper.toggleFullscreen(window, isFullscreen = false) - onFullscreenChange(false) } } } diff --git a/app/src/main/java/dev/typetype/android/feature/player/PlayerScreen.kt b/app/src/main/java/dev/typetype/android/feature/player/PlayerScreen.kt index 70555625..c45ce9b0 100644 --- a/app/src/main/java/dev/typetype/android/feature/player/PlayerScreen.kt +++ b/app/src/main/java/dev/typetype/android/feature/player/PlayerScreen.kt @@ -33,6 +33,7 @@ fun PlayerRoute( onOpenAccounts: () -> Unit, onPlayVideo: (videoUrl: String) -> Unit, onOpenChannel: (channelUrl: String) -> Unit = {}, + modifier: Modifier = Modifier, viewModel: PlayerViewModel = hiltViewModel(), channelActionsViewModel: PlayerChannelActionsViewModel = hiltViewModel(), danmakuViewModel: PlayerDanmakuViewModel = hiltViewModel(), @@ -57,6 +58,7 @@ fun PlayerRoute( onOpenAccounts = onOpenAccounts, onPlayVideo = onPlayVideo, onOpenChannel = onOpenChannel, + modifier = modifier, isSubscribed = channelState.isSubscribed(channelUrl), subscriptionInFlight = channelState.isUpdating(channelUrl), onToggleSubscription = { @@ -88,6 +90,7 @@ fun PlayerScreen( danmakuState: PlayerDanmakuState = PlayerDanmakuState(), onDanmakuAction: (PlayerDanmakuAction) -> Unit = {}, onAction: (PlayerAction) -> Unit = {}, + modifier: Modifier = Modifier, ) { val snackbarHostState = remember { SnackbarHostState() } val favoriteAdded = stringResource(R.string.player_snackbar_favorite_added) @@ -133,6 +136,7 @@ fun PlayerScreen( } } Scaffold( + modifier = modifier, snackbarHost = { SnackbarHost(snackbarHostState) }, containerColor = MaterialTheme.colorScheme.background, contentWindowInsets = androidx.compose.foundation.layout.WindowInsets(0), diff --git a/app/src/main/java/dev/typetype/android/feature/player/components/MiniPlayerBar.kt b/app/src/main/java/dev/typetype/android/feature/player/components/MiniPlayerBar.kt index cace7e2b..e36b6a9d 100644 --- a/app/src/main/java/dev/typetype/android/feature/player/components/MiniPlayerBar.kt +++ b/app/src/main/java/dev/typetype/android/feature/player/components/MiniPlayerBar.kt @@ -77,11 +77,6 @@ fun MiniPlayerBar( if (!change.pressed || handled) break val deltaY = change.position.y - startY when { - deltaY <= -swipeThresholdPx -> { - change.consume() - handled = true - onExpand() - } deltaY >= swipeThresholdPx -> { change.consume() handled = true diff --git a/app/src/main/java/dev/typetype/android/feature/player/host/FullscreenOrientationState.kt b/app/src/main/java/dev/typetype/android/feature/player/host/FullscreenOrientationState.kt new file mode 100644 index 00000000..91d4e5c2 --- /dev/null +++ b/app/src/main/java/dev/typetype/android/feature/player/host/FullscreenOrientationState.kt @@ -0,0 +1,68 @@ +package dev.typetype.android.feature.player.host + +internal enum class DeviceOrientation { Portrait, Landscape, Other } + +internal data class FullscreenOrientationState( + val locksLandscape: Boolean = false, + val suppressesLandscapeEntry: Boolean = false, +) { + fun onUserRequest( + fullscreen: Boolean, + orientation: DeviceOrientation, + ): FullscreenOrientationTransition = if (fullscreen) { + FullscreenOrientationTransition( + state = copy( + locksLandscape = true, + suppressesLandscapeEntry = false, + ), + fullscreenRequest = true, + ) + } else { + FullscreenOrientationTransition( + state = copy( + locksLandscape = false, + suppressesLandscapeEntry = orientation == DeviceOrientation.Landscape, + ), + fullscreenRequest = false, + ) + } + + fun onEnvironmentChanged( + orientation: DeviceOrientation, + allowsRotationFullscreen: Boolean, + isFullscreen: Boolean, + ): FullscreenOrientationTransition { + if (!allowsRotationFullscreen) { + return FullscreenOrientationTransition( + state = copy(locksLandscape = locksLandscape && isFullscreen), + fullscreenRequest = false.takeIf { isFullscreen && !locksLandscape }, + ) + } + + return when (orientation) { + DeviceOrientation.Portrait -> FullscreenOrientationTransition( + state = copy( + locksLandscape = locksLandscape && isFullscreen, + suppressesLandscapeEntry = false, + ), + fullscreenRequest = false.takeIf { isFullscreen && !locksLandscape }, + ) + + DeviceOrientation.Landscape -> FullscreenOrientationTransition( + state = copy(locksLandscape = locksLandscape && isFullscreen), + fullscreenRequest = true.takeIf { + !isFullscreen && !suppressesLandscapeEntry + }, + ) + + DeviceOrientation.Other -> FullscreenOrientationTransition( + state = copy(locksLandscape = locksLandscape && isFullscreen), + ) + } + } +} + +internal data class FullscreenOrientationTransition( + val state: FullscreenOrientationState, + val fullscreenRequest: Boolean? = null, +) diff --git a/app/src/main/java/dev/typetype/android/feature/player/host/PlayerHost.kt b/app/src/main/java/dev/typetype/android/feature/player/host/PlayerHost.kt index f823175a..0036bf7b 100644 --- a/app/src/main/java/dev/typetype/android/feature/player/host/PlayerHost.kt +++ b/app/src/main/java/dev/typetype/android/feature/player/host/PlayerHost.kt @@ -1,30 +1,22 @@ package dev.typetype.android.feature.player.host +import android.content.res.Configuration import androidx.activity.compose.BackHandler import androidx.activity.compose.LocalActivity -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.background -import androidx.compose.foundation.gestures.AnchoredDraggableState -import androidx.compose.foundation.gestures.DraggableAnchors -import androidx.compose.foundation.gestures.animateTo -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBars -import androidx.compose.foundation.layout.offset import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.media3.session.MediaController @@ -35,7 +27,6 @@ import dev.typetype.android.feature.player.components.rememberIsInPipMode private val MINI_PLAYER_HEIGHT = 64.dp internal const val PLAYER_HOST_OVERLAY_TAG = "player_host_overlay" -@OptIn(ExperimentalFoundationApi::class) @Composable fun PlayerHost( controller: PlayerHostController, @@ -46,14 +37,20 @@ fun PlayerHost( onOpenChannel: (channelUrl: String) -> Unit, onOpenAccounts: () -> Unit, onClosePlayback: () -> Unit, + onTransitionProgressChange: (Float) -> Unit = {}, content: @Composable () -> Unit, ) { val state by controller.state.collectAsStateWithLifecycle() val density = LocalDensity.current val isInPip by rememberIsInPipMode() val activity = LocalActivity.current - - PlayerFullscreenEffect(activity, isFullscreen, onFullscreenChange) + val configuration = LocalConfiguration.current + val orientation = when (configuration.orientation) { + Configuration.ORIENTATION_PORTRAIT -> DeviceOrientation.Portrait + Configuration.ORIENTATION_LANDSCAPE -> DeviceOrientation.Landscape + else -> DeviceOrientation.Other + } + var fullscreenOrientationState by remember { mutableStateOf(FullscreenOrientationState()) } val navigationBarsBottom = WindowInsets.navigationBars.asPaddingValues() .calculateBottomPadding() @@ -65,27 +62,32 @@ fun PlayerHost( val miniAnchorPx = ( containerHeightPx - miniHeightPx - bottomBarPx - gestureBarPx ).coerceAtLeast(0f) - - val anchors = remember(containerHeightPx, miniAnchorPx) { - DraggableAnchors { - PlayerHostTarget.Expanded at 0f - PlayerHostTarget.Mini at miniAnchorPx - PlayerHostTarget.Hidden at containerHeightPx - } + val allowsRotationFullscreen = state.videoUrl != null && + state.target == PlayerHostTarget.Expanded && + !isInPip && + minOf(maxWidth, maxHeight) < 600.dp + val requestFullscreen: (Boolean) -> Unit = { requested -> + val transition = fullscreenOrientationState.onUserRequest(requested, orientation) + fullscreenOrientationState = transition.state + transition.fullscreenRequest?.let(onFullscreenChange) } - val anchoredState = remember { - AnchoredDraggableState( - initialValue = state.target.draggableTarget(), + PlayerFullscreenEffect( + activity = activity, + isFullscreen = isFullscreen, + locksLandscape = fullscreenOrientationState.locksLandscape, + ) + + LaunchedEffect(orientation, allowsRotationFullscreen, isFullscreen) { + val transition = fullscreenOrientationState.onEnvironmentChanged( + orientation = orientation, + allowsRotationFullscreen = allowsRotationFullscreen, + isFullscreen = isFullscreen, ) + fullscreenOrientationState = transition.state + transition.fullscreenRequest?.let(onFullscreenChange) } - LaunchedEffect(anchors, state.requestStamp) { - val target = state.target.draggableTarget() - anchoredState.updateAnchors(anchors, target) - if (anchoredState.currentValue != target) { - anchoredState.animateTo(target) - } - } + LaunchedEffect(state.playbackClearRequestStamp, mediaController) { val requestStamp = state.playbackClearRequestStamp ?: return@LaunchedEffect val player = mediaController ?: return@LaunchedEffect @@ -95,12 +97,12 @@ fun PlayerHost( } LaunchedEffect(state.target, isFullscreen) { if (state.target != PlayerHostTarget.Expanded && isFullscreen) { - onFullscreenChange(false) + requestFullscreen(false) } } LaunchedEffect(isInPip, isFullscreen) { if (isInPip && isFullscreen) { - onFullscreenChange(false) + requestFullscreen(false) } } @@ -109,41 +111,38 @@ fun PlayerHost( val hasVideo = state.videoUrl != null && state.target != PlayerHostTarget.Embedded if (hasVideo) { - val isMini = !isInPip && ( - anchoredState.currentValue == PlayerHostTarget.Mini && - anchoredState.targetValue == PlayerHostTarget.Mini - ) - - val hostHeightDp = with(density) { - if (isMini) miniHeightPx.toDp() else containerHeightPx.toDp() - } - - Box( - modifier = Modifier - .fillMaxWidth() - .height(hostHeightDp) - .testTag(PLAYER_HOST_OVERLAY_TAG) - .offset { - val offset = if (anchoredState.anchors.size > 0) { - anchoredState.requireOffset() - } else { - containerHeightPx + PlayerHostMotionLayout( + target = state.target, + requestStamp = state.requestStamp, + miniAnchorPx = miniAnchorPx, + containerHeightPx = containerHeightPx, + miniHeightPx = miniHeightPx, + dragEnabled = !isFullscreen && !isInPip, + miniContentEnabled = !isInPip, + onTargetSettled = { target -> + when (target) { + PlayerHostTarget.Expanded -> { + if (controller.state.value.target != target) controller.expand() + } + PlayerHostTarget.Mini -> { + if (controller.state.value.target != target) controller.minimize() } - IntOffset(0, offset.toInt()) + else -> Unit } - .background(if (isMini) Color.Transparent else Color.Black), - ) { - if (isMini) { + }, + onProgressChange = onTransitionProgressChange, + miniContent = { MiniPlayerRuntime( controller = mediaController, onExpand = { controller.expand() }, onSendToBackground = { activity?.moveTaskToBack(false) }, onClose = onClosePlayback, ) - } else { + }, + expandedContent = { transitionModifier -> PlayerRouteScreen( isFullscreen = isFullscreen, - onFullscreenChange = onFullscreenChange, + onFullscreenChange = requestFullscreen, onNavigateBack = { controller.minimize() }, onOpenAccounts = { controller.minimize() @@ -154,22 +153,20 @@ fun PlayerHost( controller.minimize() onOpenChannel(url) }, + modifier = transitionModifier, ) - } - } + }, + ) BackHandler(enabled = isFullscreen || state.target == PlayerHostTarget.Expanded) { if (isFullscreen) { - onFullscreenChange(false) + requestFullscreen(false) } else { controller.minimize() } } + } else { + LaunchedEffect(Unit) { onTransitionProgressChange(0f) } } } } - -private fun PlayerHostTarget.draggableTarget(): PlayerHostTarget = when (this) { - PlayerHostTarget.Embedded -> PlayerHostTarget.Hidden - else -> this -} From a66bb8ce51442afce0f5c0f6b61fc5ffd62fc9f4 Mon Sep 17 00:00:00 2001 From: Priveetee Date: Fri, 21 Aug 2026 13:57:15 +0200 Subject: [PATCH 04/49] test: cover player motion and rotation --- .../player/PlayerFullscreenEffectTest.kt | 4 +- .../player/host/PlayerHostMotionLayoutTest.kt | 107 ++++++++++++++++++ .../android/AppChromeVisibilityTest.kt | 25 ++++ .../host/FullscreenOrientationStateTest.kt | 104 +++++++++++++++++ .../player/host/PlayerHostTransitionTest.kt | 43 +++++++ 5 files changed, 280 insertions(+), 3 deletions(-) create mode 100644 app/src/androidTest/java/dev/typetype/android/feature/player/host/PlayerHostMotionLayoutTest.kt create mode 100644 app/src/test/java/dev/typetype/android/feature/player/host/FullscreenOrientationStateTest.kt create mode 100644 app/src/test/java/dev/typetype/android/feature/player/host/PlayerHostTransitionTest.kt diff --git a/app/src/androidTest/java/dev/typetype/android/feature/player/PlayerFullscreenEffectTest.kt b/app/src/androidTest/java/dev/typetype/android/feature/player/PlayerFullscreenEffectTest.kt index 3731f254..aec49c61 100644 --- a/app/src/androidTest/java/dev/typetype/android/feature/player/PlayerFullscreenEffectTest.kt +++ b/app/src/androidTest/java/dev/typetype/android/feature/player/PlayerFullscreenEffectTest.kt @@ -25,7 +25,6 @@ class PlayerFullscreenEffectTest { val composeRule = createAndroidComposeRule() private val fullscreen = mutableStateOf(false) - private var disposedFullscreen = true @Before fun showEffectInPortrait() { @@ -41,7 +40,7 @@ class PlayerFullscreenEffectTest { PlayerFullscreenEffect( activity = composeRule.activity, isFullscreen = fullscreen.value, - onFullscreenChange = { disposedFullscreen = it }, + locksLandscape = true, ) } composeRule.waitForIdle() @@ -102,7 +101,6 @@ class PlayerFullscreenEffectTest { composeRule.activity.window.attributes.layoutInDisplayCutoutMode, ) } - assertTrue(disposedFullscreen) } composeRule.waitUntil(SYSTEM_BARS_TIMEOUT_MILLIS) { systemBarsVisible() } } diff --git a/app/src/androidTest/java/dev/typetype/android/feature/player/host/PlayerHostMotionLayoutTest.kt b/app/src/androidTest/java/dev/typetype/android/feature/player/host/PlayerHostMotionLayoutTest.kt new file mode 100644 index 00000000..bf341583 --- /dev/null +++ b/app/src/androidTest/java/dev/typetype/android/feature/player/host/PlayerHostMotionLayoutTest.kt @@ -0,0 +1,107 @@ +package dev.typetype.android.feature.player.host + +import androidx.activity.ComponentActivity +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.requiredHeight +import androidx.compose.foundation.layout.requiredWidth +import androidx.compose.material3.Text +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.v2.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performTouchInput +import androidx.compose.ui.test.swipe +import androidx.compose.ui.unit.dp +import org.junit.Assert.assertTrue +import org.junit.Rule +import org.junit.Test + +class PlayerHostMotionLayoutTest { + @get:Rule + val composeRule = createAndroidComposeRule() + + @Test + fun playerFollowsDownwardDragIntoMiniPlayer() { + val observedProgress = mutableStateListOf() + var settledTarget by mutableStateOf(PlayerHostTarget.Expanded) + var requestStamp by mutableIntStateOf(0) + var dragDistancePx = 0f + composeRule.setContent { + val density = LocalDensity.current + dragDistancePx = with(density) { 500.dp.toPx() } + Box(Modifier.requiredWidth(400.dp).requiredHeight(600.dp)) { + PlayerHostMotionLayout( + target = settledTarget, + requestStamp = requestStamp.toLong(), + miniAnchorPx = dragDistancePx, + containerHeightPx = with(density) { 600.dp.toPx() }, + miniHeightPx = with(density) { 64.dp.toPx() }, + dragEnabled = true, + miniContentEnabled = true, + onTargetSettled = { + settledTarget = it + requestStamp += 1 + }, + onProgressChange = observedProgress::add, + miniContent = { Text("Mini content") }, + expandedContent = { Text("Expanded content", modifier = it) }, + ) + } + } + + composeRule.onNodeWithTag(PLAYER_HOST_OVERLAY_TAG).performTouchInput { + swipe(center, Offset(center.x, bottom - 1f), 600L) + } + composeRule.waitUntil(5_000) { settledTarget == PlayerHostTarget.Mini } + composeRule.onNodeWithText("Mini content").assertIsDisplayed() + assertTrue(observedProgress.any { it in 0.1f..0.9f }) + } + + @Test + fun miniPlayerFollowsUpwardDragIntoExpandedPlayer() { + val observedProgress = mutableStateListOf() + var settledTarget by mutableStateOf(PlayerHostTarget.Mini) + var requestStamp by mutableIntStateOf(0) + composeRule.setContent { + val density = LocalDensity.current + Box(Modifier.requiredWidth(400.dp).requiredHeight(600.dp)) { + PlayerHostMotionLayout( + target = settledTarget, + requestStamp = requestStamp.toLong(), + miniAnchorPx = with(density) { 500.dp.toPx() }, + containerHeightPx = with(density) { 600.dp.toPx() }, + miniHeightPx = with(density) { 64.dp.toPx() }, + dragEnabled = true, + miniContentEnabled = true, + onTargetSettled = { + settledTarget = it + requestStamp += 1 + }, + onProgressChange = observedProgress::add, + miniContent = { Text("Mini content") }, + expandedContent = { Text("Expanded content", modifier = it) }, + ) + } + } + + composeRule.onNodeWithText("Mini content").assertIsDisplayed() + observedProgress.clear() + composeRule.onNodeWithText("Mini content").performTouchInput { + swipe(center, Offset(center.x, center.y - 300f), 100L) + } + assertTrue( + "Upward drag did not move the player: $observedProgress", + observedProgress.any { it < 0.99f }, + ) + composeRule.waitUntil(5_000) { settledTarget == PlayerHostTarget.Expanded } + composeRule.onNodeWithText("Expanded content").assertIsDisplayed() + } +} diff --git a/app/src/test/java/dev/typetype/android/AppChromeVisibilityTest.kt b/app/src/test/java/dev/typetype/android/AppChromeVisibilityTest.kt index ec55e374..bf97be6f 100644 --- a/app/src/test/java/dev/typetype/android/AppChromeVisibilityTest.kt +++ b/app/src/test/java/dev/typetype/android/AppChromeVisibilityTest.kt @@ -1,6 +1,7 @@ package dev.typetype.android import dev.typetype.android.feature.player.host.PlayerHostTarget +import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Test @@ -19,4 +20,28 @@ class AppChromeVisibilityTest { assertFalse(isAppChromeVisible(PlayerHostTarget.Expanded, true)) assertFalse(isAppChromeVisible(PlayerHostTarget.Mini, true)) } + + @Test + fun phoneChromeFollowsPlayerMotionInBothDirections() { + assertEquals( + 0.35f, + playerPhoneChromeAlpha(true, PlayerHostTarget.Expanded, false, 0.35f), + ) + assertEquals( + 0.65f, + playerPhoneChromeAlpha(true, PlayerHostTarget.Mini, false, 0.65f), + ) + } + + @Test + fun phoneChromeRemainsVisibleWithoutAnOverlayPlayer() { + assertEquals( + 1f, + playerPhoneChromeAlpha(false, PlayerHostTarget.Hidden, false, 0f), + ) + assertEquals( + 1f, + playerPhoneChromeAlpha(true, PlayerHostTarget.Embedded, false, 0f), + ) + } } diff --git a/app/src/test/java/dev/typetype/android/feature/player/host/FullscreenOrientationStateTest.kt b/app/src/test/java/dev/typetype/android/feature/player/host/FullscreenOrientationStateTest.kt new file mode 100644 index 00000000..3f8b18e4 --- /dev/null +++ b/app/src/test/java/dev/typetype/android/feature/player/host/FullscreenOrientationStateTest.kt @@ -0,0 +1,104 @@ +package dev.typetype.android.feature.player.host + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Test + +class FullscreenOrientationStateTest { + @Test + fun landscapeRotationEntersFullscreenWithoutLockingOrientation() { + val transition = FullscreenOrientationState().onEnvironmentChanged( + orientation = DeviceOrientation.Landscape, + allowsRotationFullscreen = true, + isFullscreen = false, + ) + + assertEquals(true, transition.fullscreenRequest) + assertFalse(transition.state.locksLandscape) + } + + @Test + fun portraitRotationExitsRotationDrivenFullscreen() { + val transition = FullscreenOrientationState().onEnvironmentChanged( + orientation = DeviceOrientation.Portrait, + allowsRotationFullscreen = true, + isFullscreen = true, + ) + + assertEquals(false, transition.fullscreenRequest) + } + + @Test + fun fullscreenButtonLocksLandscapeUntilUserExits() { + val entered = FullscreenOrientationState().onUserRequest( + fullscreen = true, + orientation = DeviceOrientation.Portrait, + ) + val rotated = entered.state.onEnvironmentChanged( + orientation = DeviceOrientation.Landscape, + allowsRotationFullscreen = true, + isFullscreen = true, + ) + + assertTrue(rotated.state.locksLandscape) + assertNull(rotated.fullscreenRequest) + } + + @Test + fun buttonExitDoesNotImmediatelyReenterInLandscape() { + val exited = FullscreenOrientationState(locksLandscape = true).onUserRequest( + fullscreen = false, + orientation = DeviceOrientation.Landscape, + ) + val environment = exited.state.onEnvironmentChanged( + orientation = DeviceOrientation.Landscape, + allowsRotationFullscreen = true, + isFullscreen = false, + ) + + assertTrue(environment.state.suppressesLandscapeEntry) + assertNull(environment.fullscreenRequest) + } + + @Test + fun portraitClearsLandscapeEntrySuppression() { + val transition = FullscreenOrientationState( + suppressesLandscapeEntry = true, + ).onEnvironmentChanged( + orientation = DeviceOrientation.Portrait, + allowsRotationFullscreen = true, + isFullscreen = false, + ) + + assertFalse(transition.state.suppressesLandscapeEntry) + assertNull(transition.fullscreenRequest) + } + + @Test + fun miniPlayerAndTabletsCannotAutoEnterFullscreen() { + val transition = FullscreenOrientationState().onEnvironmentChanged( + orientation = DeviceOrientation.Landscape, + allowsRotationFullscreen = false, + isFullscreen = false, + ) + + assertNull(transition.fullscreenRequest) + assertEquals(FullscreenOrientationState(), transition.state) + } + + @Test + fun manualFullscreenRemainsAvailableOnTablets() { + val transition = FullscreenOrientationState( + locksLandscape = true, + ).onEnvironmentChanged( + orientation = DeviceOrientation.Landscape, + allowsRotationFullscreen = false, + isFullscreen = true, + ) + + assertTrue(transition.state.locksLandscape) + assertNull(transition.fullscreenRequest) + } +} diff --git a/app/src/test/java/dev/typetype/android/feature/player/host/PlayerHostTransitionTest.kt b/app/src/test/java/dev/typetype/android/feature/player/host/PlayerHostTransitionTest.kt new file mode 100644 index 00000000..171f6a92 --- /dev/null +++ b/app/src/test/java/dev/typetype/android/feature/player/host/PlayerHostTransitionTest.kt @@ -0,0 +1,43 @@ +package dev.typetype.android.feature.player.host + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +class PlayerHostTransitionTest { + @Test + fun playerSizeAndPositionFollowTheDrag() { + val transition = playerHostTransition( + offsetPx = 350f, + miniAnchorPx = 700f, + containerHeightPx = 800f, + miniHeightPx = 64f, + isAnimationRunning = false, + ) + + assertEquals(0.5f, transition.progress) + assertEquals(432, transition.heightPx) + assertEquals(350, transition.offsetPx) + assertFalse(transition.isSettledMini) + } + + @Test + fun miniPresentationWaitsForTheMotionToSettle() { + val moving = playerHostTransition(700f, 700f, 800f, 64f, true) + val settled = playerHostTransition(700f, 700f, 800f, 64f, false) + + assertFalse(moving.isSettledMini) + assertTrue(settled.isSettledMini) + assertEquals(64, settled.heightPx) + } + + @Test + fun invalidOffsetUsesTheExpandedPresentation() { + val transition = playerHostTransition(Float.NaN, 700f, 800f, 64f, false) + + assertEquals(0f, transition.progress) + assertEquals(800, transition.heightPx) + assertEquals(0, transition.offsetPx) + } +} From 1b9914196b93d4ec60fe732a1fcfde95aacfed10 Mon Sep 17 00:00:00 2001 From: Priveetee Date: Fri, 21 Aug 2026 13:57:25 +0200 Subject: [PATCH 05/49] fix: expose subscription feed diagnostics --- .../diagnostics/LocalDiagnosticsRepository.kt | 17 +++++- .../data/feed/HomeFeedRepositoryImpl.kt | 35 +++++++---- .../data/feed/SubscriptionFeedDiagnostics.kt | 47 +++++++++++++++ .../feed/SubscriptionFeedDiagnosticsTest.kt | 43 ++++++++++++++ .../feed/SubscriptionFeedLiveContractTest.kt | 59 +++++++++++++++++++ 5 files changed, 187 insertions(+), 14 deletions(-) create mode 100644 app/src/main/java/dev/typetype/android/data/feed/SubscriptionFeedDiagnostics.kt create mode 100644 app/src/test/java/dev/typetype/android/data/feed/SubscriptionFeedDiagnosticsTest.kt create mode 100644 app/src/test/java/dev/typetype/android/data/feed/SubscriptionFeedLiveContractTest.kt diff --git a/app/src/main/java/dev/typetype/android/data/diagnostics/LocalDiagnosticsRepository.kt b/app/src/main/java/dev/typetype/android/data/diagnostics/LocalDiagnosticsRepository.kt index f4cb329a..65c0d68f 100644 --- a/app/src/main/java/dev/typetype/android/data/diagnostics/LocalDiagnosticsRepository.kt +++ b/app/src/main/java/dev/typetype/android/data/diagnostics/LocalDiagnosticsRepository.kt @@ -95,16 +95,21 @@ class LocalDiagnosticsRepository @Inject constructor( } } - internal fun recordLocalEvent(route: String, timestampEpochMillis: Long = System.currentTimeMillis()) { + internal fun recordLocalEvent( + route: String, + timestampEpochMillis: Long = System.currentTimeMillis(), + statusCode: Int? = null, + requestId: String? = null, + ) { if (route !in LOCAL_EVENT_ROUTES) return val scope = currentScope()?.copy(route = route) ?: return val entry = DiagnosticEntry( timestampEpochMillis = timestampEpochMillis, method = LOCAL_METHOD, route = route, - statusCode = null, + statusCode = statusCode?.takeIf { it in 100..599 }, durationMillis = 0, - requestId = null, + requestId = requestId?.takeIf(REQUEST_ID_PATTERN::matches), ) synchronized(lock) { directory.mkdirs() @@ -218,6 +223,12 @@ class LocalDiagnosticsRepository @Inject constructor( "/network/available", "/network/changed", "/network/lost", + "/subscriptions/feed/contract", + "/subscriptions/feed/decode", + "/subscriptions/feed/pagination", + "/subscriptions/feed/persistence", + "/subscriptions/feed/ready", + "/subscriptions/feed/server", ) } } diff --git a/app/src/main/java/dev/typetype/android/data/feed/HomeFeedRepositoryImpl.kt b/app/src/main/java/dev/typetype/android/data/feed/HomeFeedRepositoryImpl.kt index eb05f9ea..7871e13c 100644 --- a/app/src/main/java/dev/typetype/android/data/feed/HomeFeedRepositoryImpl.kt +++ b/app/src/main/java/dev/typetype/android/data/feed/HomeFeedRepositoryImpl.kt @@ -21,6 +21,7 @@ class HomeFeedRepositoryImpl @Inject constructor( private val apiHolder: TypeTypeApiHolder, private val activeAccountScope: ActiveAccountScope, private val feedVideoDao: FeedVideoDao, + private val subscriptionFeedDiagnostics: SubscriptionFeedDiagnostics, ) : HomeFeedRepository { private val subscriptionFeedClient = SubscriptionFeedClient() @@ -34,7 +35,14 @@ class HomeFeedRepositoryImpl @Inject constructor( } override suspend fun cacheSubscriptionsFeed(videos: List