From 6e1daa4115b196419a6e064fbc4d2876a276ea14 Mon Sep 17 00:00:00 2001 From: Pasta Date: Thu, 16 Jul 2026 13:52:38 +0300 Subject: [PATCH] Revert "feat(tv): add native Android TV support" --- app/src/main/AndroidManifest.xml | 17 -- .../java/io/github/aedev/flow/MainActivity.kt | 127 +++++------- .../java/io/github/aedev/flow/TvActivity.kt | 18 -- .../flow/data/local/AppUiModePreferences.kt | 31 --- .../github/aedev/flow/platform/AppUiMode.kt | 25 --- .../flow/platform/DeviceFormFactorDetector.kt | 22 -- .../screens/settings/InterfaceModeDialog.kt | 71 ------- .../ui/screens/settings/SettingsScreen.kt | 23 --- .../io/github/aedev/flow/ui/tv/FlowTvApp.kt | 116 ----------- .../flow/ui/tv/components/TvFocusableCard.kt | 64 ------ .../flow/ui/tv/components/TvNavigationRail.kt | 78 ------- .../flow/ui/tv/components/TvScreenStates.kt | 51 ----- .../flow/ui/tv/components/TvVideoCard.kt | 114 ----------- .../flow/ui/tv/input/TvPlayerKeyMapper.kt | 26 --- .../flow/ui/tv/navigation/TvDestination.kt | 31 --- .../aedev/flow/ui/tv/screens/TvHomeScreen.kt | 141 ------------- .../flow/ui/tv/screens/TvLibraryScreen.kt | 140 ------------- .../flow/ui/tv/screens/TvPlayerScreen.kt | 193 ------------------ .../flow/ui/tv/screens/TvSearchScreen.kt | 121 ----------- .../flow/ui/tv/screens/TvSettingsScreen.kt | 89 -------- .../ui/tv/screens/TvSubscriptionsScreen.kt | 119 ----------- app/src/main/res/drawable/tv_banner.xml | 22 -- app/src/main/res/values/tv_strings.xml | 35 ---- .../aedev/flow/platform/AppUiModeTest.kt | 28 --- .../flow/ui/tv/input/TvPlayerKeyMapperTest.kt | 25 --- .../ui/tv/navigation/TvDestinationTest.kt | 24 --- 26 files changed, 50 insertions(+), 1701 deletions(-) delete mode 100644 app/src/main/java/io/github/aedev/flow/TvActivity.kt delete mode 100644 app/src/main/java/io/github/aedev/flow/data/local/AppUiModePreferences.kt delete mode 100644 app/src/main/java/io/github/aedev/flow/platform/AppUiMode.kt delete mode 100644 app/src/main/java/io/github/aedev/flow/platform/DeviceFormFactorDetector.kt delete mode 100644 app/src/main/java/io/github/aedev/flow/ui/screens/settings/InterfaceModeDialog.kt delete mode 100644 app/src/main/java/io/github/aedev/flow/ui/tv/FlowTvApp.kt delete mode 100644 app/src/main/java/io/github/aedev/flow/ui/tv/components/TvFocusableCard.kt delete mode 100644 app/src/main/java/io/github/aedev/flow/ui/tv/components/TvNavigationRail.kt delete mode 100644 app/src/main/java/io/github/aedev/flow/ui/tv/components/TvScreenStates.kt delete mode 100644 app/src/main/java/io/github/aedev/flow/ui/tv/components/TvVideoCard.kt delete mode 100644 app/src/main/java/io/github/aedev/flow/ui/tv/input/TvPlayerKeyMapper.kt delete mode 100644 app/src/main/java/io/github/aedev/flow/ui/tv/navigation/TvDestination.kt delete mode 100644 app/src/main/java/io/github/aedev/flow/ui/tv/screens/TvHomeScreen.kt delete mode 100644 app/src/main/java/io/github/aedev/flow/ui/tv/screens/TvLibraryScreen.kt delete mode 100644 app/src/main/java/io/github/aedev/flow/ui/tv/screens/TvPlayerScreen.kt delete mode 100644 app/src/main/java/io/github/aedev/flow/ui/tv/screens/TvSearchScreen.kt delete mode 100644 app/src/main/java/io/github/aedev/flow/ui/tv/screens/TvSettingsScreen.kt delete mode 100644 app/src/main/java/io/github/aedev/flow/ui/tv/screens/TvSubscriptionsScreen.kt delete mode 100644 app/src/main/res/drawable/tv_banner.xml delete mode 100644 app/src/main/res/values/tv_strings.xml delete mode 100644 app/src/test/java/io/github/aedev/flow/platform/AppUiModeTest.kt delete mode 100644 app/src/test/java/io/github/aedev/flow/ui/tv/input/TvPlayerKeyMapperTest.kt delete mode 100644 app/src/test/java/io/github/aedev/flow/ui/tv/navigation/TvDestinationTest.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e97ca2d9..ebc2d062 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,9 +1,6 @@ - - - @@ -55,7 +52,6 @@ - - - - - - - "IDLE" @@ -192,14 +185,6 @@ class MainActivity : ComponentActivity() { var showSplash by remember { mutableStateOf(true) } val context = LocalContext.current - val configuration = LocalConfiguration.current - val uiPreferences = remember { AppUiModePreferences(applicationContext) } - val appUiMode by uiPreferences.mode.collectAsState(initial = AppUiMode.AUTOMATIC) - val deviceFormFactor = remember(configuration.uiMode, context) { - DeviceFormFactorDetector.detect(context) - } - val appUiRoot = appUiMode.resolve(deviceFormFactor) - SideEffect { cachedAppUiRoot = appUiRoot } // Check for a crash that happened last session. // If found, show the CrashReporterScreen instead of the normal UI. @@ -346,64 +331,56 @@ class MainActivity : ComponentActivity() { val isDeeplinkShort by this@MainActivity.isDeeplinkShort val openMusicPlayerRequest by this@MainActivity.openMusicPlayerRequest - if (appUiRoot == AppUiRoot.TV) { - FlowTvApp( - deeplinkVideoId = deeplinkVideoId, - isShort = isDeeplinkShort, - onDeeplinkConsumed = { consumeDeeplink() }, - ) - } else { - FlowApp( - currentTheme = themeMode, - themeVariant = themeVariant, - customThemePalettes = customThemePalettes, - systemLightThemeMode = systemLightThemeMode, - systemDarkThemeMode = systemDarkThemeMode, - systemDarkThemeVariant = systemDarkThemeVariant, - onThemeChange = { newTheme -> - themeMode = newTheme - scope.launch { - dataManager.setThemeMode(newTheme) - } - }, - onThemeVariantChange = { variant -> - themeVariant = variant - scope.launch { - dataManager.setThemeVariant(variant) - } - }, - onCustomThemePalettesChange = { palettes -> - customThemePalettes = palettes - scope.launch { - dataManager.setCustomThemePalettes(palettes) - } - }, - onSystemLightThemeChange = { newTheme -> - systemLightThemeMode = newTheme - scope.launch { - dataManager.setSystemLightThemeMode(newTheme) - } - }, - onSystemDarkThemeChange = { newTheme -> - systemDarkThemeMode = newTheme - scope.launch { - dataManager.setSystemDarkThemeMode(newTheme) - } - }, - onSystemDarkThemeVariantChange = { variant -> - systemDarkThemeVariant = variant - scope.launch { - dataManager.setSystemDarkThemeVariant(variant) - } - }, - deeplinkVideoId = deeplinkVideoId, - isShort = isDeeplinkShort, - openMusicPlayerRequest = openMusicPlayerRequest, - onDeeplinkConsumed = { - consumeDeeplink() + FlowApp( + currentTheme = themeMode, + themeVariant = themeVariant, + customThemePalettes = customThemePalettes, + systemLightThemeMode = systemLightThemeMode, + systemDarkThemeMode = systemDarkThemeMode, + systemDarkThemeVariant = systemDarkThemeVariant, + onThemeChange = { newTheme -> + themeMode = newTheme + scope.launch { + dataManager.setThemeMode(newTheme) } - ) - } + }, + onThemeVariantChange = { variant -> + themeVariant = variant + scope.launch { + dataManager.setThemeVariant(variant) + } + }, + onCustomThemePalettesChange = { palettes -> + customThemePalettes = palettes + scope.launch { + dataManager.setCustomThemePalettes(palettes) + } + }, + onSystemLightThemeChange = { newTheme -> + systemLightThemeMode = newTheme + scope.launch { + dataManager.setSystemLightThemeMode(newTheme) + } + }, + onSystemDarkThemeChange = { newTheme -> + systemDarkThemeMode = newTheme + scope.launch { + dataManager.setSystemDarkThemeMode(newTheme) + } + }, + onSystemDarkThemeVariantChange = { variant -> + systemDarkThemeVariant = variant + scope.launch { + dataManager.setSystemDarkThemeVariant(variant) + } + }, + deeplinkVideoId = deeplinkVideoId, + isShort = isDeeplinkShort, + openMusicPlayerRequest = openMusicPlayerRequest, + onDeeplinkConsumed = { + consumeDeeplink() + } + ) // 2. THE SPLASH SCREEN (Z-Index Top) if (showSplash) { @@ -613,9 +590,7 @@ class MainActivity : ComponentActivity() { ) videoLifecycleLog("onStop") if (!isInPictureInPictureMode && !PictureInPictureHelper.isPopupActive) { - if (cachedAppUiRoot == AppUiRoot.MOBILE) { - requestedOrientation = android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT - } + requestedOrientation = android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT if (!cachedShortsBackgroundPlay) { io.github.aedev.flow.player.shorts.ShortsPlayerPool.getInstance().pauseAll() } @@ -640,7 +615,6 @@ class MainActivity : ComponentActivity() { override fun onUserLeaveHint() { super.onUserLeaveHint() - if (cachedAppUiRoot == AppUiRoot.TV) return FlowCrashHandler.recordPhase("activity", "onUserLeaveHint autoPip=$cachedAutoPipEnabled") videoLifecycleLog("onUserLeaveHint") // Only enter PiP mode if video is playing and has progressed @@ -679,7 +653,6 @@ class MainActivity : ComponentActivity() { isPlaying: Boolean = true, openSettingsOnDenied: Boolean = false ): Boolean { - if (cachedAppUiRoot == AppUiRoot.TV) return false if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return false if (!PictureInPictureHelper.isPipAllowed(this)) { if (openSettingsOnDenied) { diff --git a/app/src/main/java/io/github/aedev/flow/TvActivity.kt b/app/src/main/java/io/github/aedev/flow/TvActivity.kt deleted file mode 100644 index e2351b38..00000000 --- a/app/src/main/java/io/github/aedev/flow/TvActivity.kt +++ /dev/null @@ -1,18 +0,0 @@ -package io.github.aedev.flow - -import android.content.Intent -import android.os.Bundle -import androidx.activity.ComponentActivity - -/** Leanback launcher bridge that keeps [MainActivity] as Flow's single runtime host. */ -class TvActivity : ComponentActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - val forwardedIntent = Intent(intent).apply { - setClass(this@TvActivity, MainActivity::class.java) - addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP) - } - startActivity(forwardedIntent) - finish() - } -} diff --git a/app/src/main/java/io/github/aedev/flow/data/local/AppUiModePreferences.kt b/app/src/main/java/io/github/aedev/flow/data/local/AppUiModePreferences.kt deleted file mode 100644 index eaa6500f..00000000 --- a/app/src/main/java/io/github/aedev/flow/data/local/AppUiModePreferences.kt +++ /dev/null @@ -1,31 +0,0 @@ -package io.github.aedev.flow.data.local - -import android.content.Context -import androidx.datastore.core.DataStore -import androidx.datastore.preferences.core.Preferences -import androidx.datastore.preferences.core.edit -import androidx.datastore.preferences.core.stringPreferencesKey -import io.github.aedev.flow.platform.AppUiMode -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map - -private val Context.appUiModeDataStore: DataStore by safePreferencesDataStore(name = "app_ui_mode") - -/** Stores the interface override independently from playback preferences. */ -class AppUiModePreferences(context: Context) { - private val appContext = context.applicationContext - - val mode: Flow = appContext.appUiModeDataStore.data.map { preferences -> - AppUiMode.fromStorage(preferences[MODE]) - } - - suspend fun setMode(mode: AppUiMode) { - appContext.appUiModeDataStore.edit { preferences -> - preferences[MODE] = mode.name - } - } - - private companion object { - val MODE = stringPreferencesKey("mode") - } -} diff --git a/app/src/main/java/io/github/aedev/flow/platform/AppUiMode.kt b/app/src/main/java/io/github/aedev/flow/platform/AppUiMode.kt deleted file mode 100644 index 0b0337d5..00000000 --- a/app/src/main/java/io/github/aedev/flow/platform/AppUiMode.kt +++ /dev/null @@ -1,25 +0,0 @@ -package io.github.aedev.flow.platform - -/** User-selected interface mode for the shared Flow APK. */ -enum class AppUiMode { - AUTOMATIC, - MOBILE, - TV; - - fun resolve(deviceFormFactor: DeviceFormFactor): AppUiRoot = when (this) { - AUTOMATIC -> if (deviceFormFactor == DeviceFormFactor.TV) AppUiRoot.TV else AppUiRoot.MOBILE - MOBILE -> AppUiRoot.MOBILE - TV -> AppUiRoot.TV - } - - companion object { - fun fromStorage(value: String?): AppUiMode = - entries.firstOrNull { it.name == value } ?: AUTOMATIC - } -} - -/** The UI root rendered by [io.github.aedev.flow.MainActivity]. */ -enum class AppUiRoot { - MOBILE, - TV, -} diff --git a/app/src/main/java/io/github/aedev/flow/platform/DeviceFormFactorDetector.kt b/app/src/main/java/io/github/aedev/flow/platform/DeviceFormFactorDetector.kt deleted file mode 100644 index 9532a3b2..00000000 --- a/app/src/main/java/io/github/aedev/flow/platform/DeviceFormFactorDetector.kt +++ /dev/null @@ -1,22 +0,0 @@ -package io.github.aedev.flow.platform - -import android.app.UiModeManager -import android.content.Context -import android.content.pm.PackageManager -import android.content.res.Configuration - -/** Runtime device categories relevant to Flow's interface selection. */ -enum class DeviceFormFactor { - MOBILE, - TV, -} - -/** Detects television devices without making touchscreen availability mandatory. */ -object DeviceFormFactorDetector { - fun detect(context: Context): DeviceFormFactor { - val uiModeManager = context.getSystemService(Context.UI_MODE_SERVICE) as? UiModeManager - val isTelevisionMode = uiModeManager?.currentModeType == Configuration.UI_MODE_TYPE_TELEVISION - val hasLeanback = context.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK) - return if (isTelevisionMode || hasLeanback) DeviceFormFactor.TV else DeviceFormFactor.MOBILE - } -} diff --git a/app/src/main/java/io/github/aedev/flow/ui/screens/settings/InterfaceModeDialog.kt b/app/src/main/java/io/github/aedev/flow/ui/screens/settings/InterfaceModeDialog.kt deleted file mode 100644 index 14cccc92..00000000 --- a/app/src/main/java/io/github/aedev/flow/ui/screens/settings/InterfaceModeDialog.kt +++ /dev/null @@ -1,71 +0,0 @@ -package io.github.aedev.flow.ui.screens.settings - -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.RadioButton -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import io.github.aedev.flow.R -import io.github.aedev.flow.platform.AppUiMode - -@Composable -fun InterfaceModeDialog( - selected: AppUiMode, - onSelected: (AppUiMode) -> Unit, - onDismiss: () -> Unit, -) { - AlertDialog( - onDismissRequest = onDismiss, - title = { Text(stringResource(R.string.interface_mode_title)) }, - text = { - Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { - AppUiMode.entries.forEach { mode -> - val title = when (mode) { - AppUiMode.AUTOMATIC -> stringResource(R.string.interface_mode_automatic) - AppUiMode.MOBILE -> stringResource(R.string.interface_mode_mobile) - AppUiMode.TV -> stringResource(R.string.interface_mode_tv) - } - val summary = when (mode) { - AppUiMode.AUTOMATIC -> stringResource(R.string.interface_mode_automatic_summary) - AppUiMode.MOBILE -> stringResource(R.string.interface_mode_mobile_summary) - AppUiMode.TV -> stringResource(R.string.interface_mode_tv_summary) - } - Row( - modifier = Modifier - .fillMaxWidth() - .clickable { onSelected(mode) } - .padding(vertical = 10.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(12.dp), - ) { - RadioButton(selected = selected == mode, onClick = null) - Column { - Text(title, style = MaterialTheme.typography.titleMedium) - Text( - text = summary, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } - } - } - } - }, - confirmButton = { - TextButton(onClick = onDismiss) { - Text(stringResource(R.string.close)) - } - }, - ) -} diff --git a/app/src/main/java/io/github/aedev/flow/ui/screens/settings/SettingsScreen.kt b/app/src/main/java/io/github/aedev/flow/ui/screens/settings/SettingsScreen.kt index c2fa9d84..8b9e7eb9 100644 --- a/app/src/main/java/io/github/aedev/flow/ui/screens/settings/SettingsScreen.kt +++ b/app/src/main/java/io/github/aedev/flow/ui/screens/settings/SettingsScreen.kt @@ -44,8 +44,6 @@ import io.github.aedev.flow.player.DeepFlowManager import io.github.aedev.flow.ui.theme.ThemeMode import io.github.aedev.flow.ui.theme.extendedColors import io.github.aedev.flow.data.local.PlayerPreferences -import io.github.aedev.flow.data.local.AppUiModePreferences -import io.github.aedev.flow.platform.AppUiMode import io.github.aedev.flow.utils.AppLanguageManager import com.google.gson.JsonParser import kotlinx.coroutines.Dispatchers @@ -100,9 +98,6 @@ fun SettingsScreen( val context = LocalContext.current val coroutineScope = rememberCoroutineScope() val playerPreferences = remember { PlayerPreferences(context) } - val appUiModePreferences = remember { AppUiModePreferences(context) } - val appUiMode by appUiModePreferences.mode.collectAsStateWithLifecycle(initialValue = AppUiMode.AUTOMATIC) - var showInterfaceModeDialog by remember { mutableStateOf(false) } val backupRepo = remember { io.github.aedev.flow.data.local.BackupRepository(context) } // Brain State @@ -127,16 +122,6 @@ fun SettingsScreen( val discordSettingsState by DiscordPresenceRuntime.settingsState.collectAsStateWithLifecycle() val discordSettingsSummary = discordSettingsSummaryText(discordSettingsState) - if (showInterfaceModeDialog) { - InterfaceModeDialog( - selected = appUiMode, - onSelected = { mode -> - coroutineScope.launch { appUiModePreferences.setMode(mode) } - }, - onDismiss = { showInterfaceModeDialog = false }, - ) - } - // Deep Flow state val deepFlowActive by playerPreferences.deepFlowActive.collectAsState(initial = false) val deepFlowActivatedAt by playerPreferences.deepFlowActivatedAt.collectAsState(initial = 0L) @@ -248,7 +233,6 @@ fun SettingsScreen( val allSettingsEntries = listOf( SettingSearchEntry(Icons.Outlined.Psychology, androidx.compose.ui.res.stringResource(io.github.aedev.flow.R.string.flow_control_center), androidx.compose.ui.res.stringResource(io.github.aedev.flow.R.string.neural_interest_map_subtitle), secFlowEngine, onNavigateToPersonality), SettingSearchEntry(Icons.Outlined.Palette, androidx.compose.ui.res.stringResource(io.github.aedev.flow.R.string.settings_item_theme), "", secAppearance, onNavigateToAppearance), - SettingSearchEntry(Icons.Outlined.Tv, androidx.compose.ui.res.stringResource(io.github.aedev.flow.R.string.settings_item_interface_mode), androidx.compose.ui.res.stringResource(io.github.aedev.flow.R.string.settings_item_interface_mode_subtitle), secAppearance) { showInterfaceModeDialog = true }, SettingSearchEntry(Icons.Outlined.Language, androidx.compose.ui.res.stringResource(io.github.aedev.flow.R.string.settings_item_app_language), currentAppLanguageLabel, secAppearance) { showAppLanguageDialog = true }, SettingSearchEntry(Icons.Outlined.AppShortcut, androidx.compose.ui.res.stringResource(io.github.aedev.flow.R.string.settings_item_app_icon), androidx.compose.ui.res.stringResource(io.github.aedev.flow.R.string.settings_item_app_icon_subtitle), secAppearance, onNavigateToAppIconPicker), SettingSearchEntry(Icons.Outlined.Tune, androidx.compose.ui.res.stringResource(io.github.aedev.flow.R.string.settings_item_player_appearance), androidx.compose.ui.res.stringResource(io.github.aedev.flow.R.string.settings_item_player_appearance_subtitle), secAppearance, onNavigateToPlayerAppearance), @@ -734,13 +718,6 @@ item { onClick = onNavigateToAppearance ) HorizontalDivider(Modifier.padding(start = 56.dp), color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)) - SettingsItem( - icon = Icons.Outlined.Tv, - title = androidx.compose.ui.res.stringResource(io.github.aedev.flow.R.string.settings_item_interface_mode), - subtitle = androidx.compose.ui.res.stringResource(io.github.aedev.flow.R.string.settings_item_interface_mode_subtitle), - onClick = { showInterfaceModeDialog = true } - ) - HorizontalDivider(Modifier.padding(start = 56.dp), color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)) SettingsItem( icon = Icons.Outlined.Language, title = androidx.compose.ui.res.stringResource(io.github.aedev.flow.R.string.settings_item_app_language), diff --git a/app/src/main/java/io/github/aedev/flow/ui/tv/FlowTvApp.kt b/app/src/main/java/io/github/aedev/flow/ui/tv/FlowTvApp.kt deleted file mode 100644 index 261dd718..00000000 --- a/app/src/main/java/io/github/aedev/flow/ui/tv/FlowTvApp.kt +++ /dev/null @@ -1,116 +0,0 @@ -package io.github.aedev.flow.ui.tv - -import androidx.activity.ComponentActivity -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxSize -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.platform.LocalContext -import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.lifecycle.viewmodel.compose.viewModel -import androidx.media3.common.util.UnstableApi -import io.github.aedev.flow.data.model.Video -import io.github.aedev.flow.player.GlobalPlayerState -import io.github.aedev.flow.ui.screens.home.HomeViewModel -import io.github.aedev.flow.ui.screens.player.VideoPlayerViewModel -import io.github.aedev.flow.ui.screens.search.SearchViewModel -import io.github.aedev.flow.ui.screens.subscriptions.SubscriptionsViewModel -import io.github.aedev.flow.ui.tv.components.TvNavigationRail -import io.github.aedev.flow.ui.tv.navigation.TvDestination -import io.github.aedev.flow.ui.tv.screens.TvHomeScreen -import io.github.aedev.flow.ui.tv.screens.TvLibraryScreen -import io.github.aedev.flow.ui.tv.screens.TvPlayerScreen -import io.github.aedev.flow.ui.tv.screens.TvSearchScreen -import io.github.aedev.flow.ui.tv.screens.TvSettingsScreen -import io.github.aedev.flow.ui.tv.screens.TvSubscriptionsScreen - -@androidx.annotation.OptIn(UnstableApi::class) -@Composable -fun FlowTvApp( - deeplinkVideoId: String? = null, - isShort: Boolean = false, - onDeeplinkConsumed: () -> Unit = {}, -) { - val context = LocalContext.current - val activity = context as ComponentActivity - val homeViewModel: HomeViewModel = hiltViewModel(activity) - val playerViewModel: VideoPlayerViewModel = hiltViewModel(activity) - val subscriptionsViewModel: SubscriptionsViewModel = viewModel(viewModelStoreOwner = activity) - val searchViewModel: SearchViewModel = viewModel(viewModelStoreOwner = activity) - var destination by remember { mutableStateOf(TvDestination.HOME) } - val activeVideo by GlobalPlayerState.currentVideo.collectAsStateWithLifecycle() - - fun play(video: Video) { - GlobalPlayerState.setCurrentVideo(video) - playerViewModel.playVideo(video) - } - - LaunchedEffect(deeplinkVideoId) { - val videoId = deeplinkVideoId ?: return@LaunchedEffect - play( - Video( - id = videoId, - title = "", - channelName = "", - channelId = "", - thumbnailUrl = "", - duration = 0, - viewCount = 0L, - uploadDate = "", - isShort = isShort, - ) - ) - onDeeplinkConsumed() - } - - Box(modifier = Modifier.fillMaxSize()) { - Row(modifier = Modifier.fillMaxSize()) { - TvNavigationRail( - selected = destination, - onSelected = { destination = it }, - ) - when (destination) { - TvDestination.HOME -> TvHomeScreen( - viewModel = homeViewModel, - onVideoClick = ::play, - modifier = Modifier.weight(1f), - ) - TvDestination.SUBSCRIPTIONS -> TvSubscriptionsScreen( - viewModel = subscriptionsViewModel, - onVideoClick = ::play, - modifier = Modifier.weight(1f), - ) - TvDestination.SEARCH -> TvSearchScreen( - viewModel = searchViewModel, - onVideoClick = ::play, - modifier = Modifier.weight(1f), - ) - TvDestination.LIBRARY -> TvLibraryScreen( - onVideoClick = ::play, - modifier = Modifier.weight(1f), - ) - TvDestination.SETTINGS -> TvSettingsScreen( - modifier = Modifier.weight(1f), - ) - } - } - - activeVideo?.let { video -> - TvPlayerScreen( - video = video, - viewModel = playerViewModel, - onClose = { - playerViewModel.clearVideo() - GlobalPlayerState.setCurrentVideo(null) - }, - ) - } - } -} diff --git a/app/src/main/java/io/github/aedev/flow/ui/tv/components/TvFocusableCard.kt b/app/src/main/java/io/github/aedev/flow/ui/tv/components/TvFocusableCard.kt deleted file mode 100644 index 18708bbe..00000000 --- a/app/src/main/java/io/github/aedev/flow/ui/tv/components/TvFocusableCard.kt +++ /dev/null @@ -1,64 +0,0 @@ -package io.github.aedev.flow.ui.tv.components - -import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.BoxScope -import androidx.compose.material3.Card -import androidx.compose.material3.CardDefaults -import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.Composable -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.focus.onFocusChanged -import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.unit.dp - -/** Material 3 card with a visible ten-foot focus state. */ -@Composable -fun TvFocusableCard( - onClick: () -> Unit, - modifier: Modifier = Modifier, - content: @Composable BoxScope.() -> Unit, -) { - var focused by remember { mutableStateOf(false) } - val scale by animateFloatAsState( - targetValue = if (focused) 1.05f else 1f, - label = "tvCardScale", - ) - - Card( - onClick = onClick, - modifier = modifier - .graphicsLayer { - scaleX = scale - scaleY = scale - } - .onFocusChanged { focused = it.isFocused }, - colors = CardDefaults.cardColors( - containerColor = if (focused) { - MaterialTheme.colorScheme.secondaryContainer - } else { - MaterialTheme.colorScheme.surfaceContainer - }, - contentColor = if (focused) { - MaterialTheme.colorScheme.onSecondaryContainer - } else { - MaterialTheme.colorScheme.onSurface - }, - ), - border = if (focused) { - BorderStroke(2.dp, MaterialTheme.colorScheme.outline) - } else { - null - }, - elevation = CardDefaults.cardElevation( - defaultElevation = if (focused) 8.dp else 1.dp, - ), - ) { - Box(content = content) - } -} diff --git a/app/src/main/java/io/github/aedev/flow/ui/tv/components/TvNavigationRail.kt b/app/src/main/java/io/github/aedev/flow/ui/tv/components/TvNavigationRail.kt deleted file mode 100644 index b84da6a3..00000000 --- a/app/src/main/java/io/github/aedev/flow/ui/tv/components/TvNavigationRail.kt +++ /dev/null @@ -1,78 +0,0 @@ -package io.github.aedev.flow.ui.tv.components - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp -import io.github.aedev.flow.ui.tv.navigation.TvDestination - -@Composable -fun TvNavigationRail( - selected: TvDestination, - onSelected: (TvDestination) -> Unit, - modifier: Modifier = Modifier, -) { - Surface( - modifier = modifier - .width(236.dp) - .fillMaxHeight(), - color = MaterialTheme.colorScheme.surfaceContainerLow, - ) { - Column( - modifier = Modifier.padding(horizontal = 20.dp, vertical = 32.dp), - verticalArrangement = Arrangement.spacedBy(12.dp), - ) { - Text( - text = stringResource(io.github.aedev.flow.R.string.app_name), - style = MaterialTheme.typography.headlineMedium, - fontWeight = FontWeight.Bold, - modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp), - ) - Spacer(modifier = Modifier.size(8.dp)) - TvDestination.primary.forEach { destination -> - TvFocusableCard( - onClick = { onSelected(destination) }, - modifier = Modifier.fillMaxWidth(), - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 14.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Icon( - imageVector = destination.icon, - contentDescription = null, - tint = if (destination == selected) { - MaterialTheme.colorScheme.primary - } else { - MaterialTheme.colorScheme.onSurfaceVariant - }, - ) - Spacer(modifier = Modifier.width(14.dp)) - Text( - text = stringResource(destination.labelRes), - style = MaterialTheme.typography.titleMedium, - fontWeight = if (destination == selected) FontWeight.Bold else FontWeight.Medium, - ) - } - } - } - } - } -} diff --git a/app/src/main/java/io/github/aedev/flow/ui/tv/components/TvScreenStates.kt b/app/src/main/java/io/github/aedev/flow/ui/tv/components/TvScreenStates.kt deleted file mode 100644 index 869cafad..00000000 --- a/app/src/main/java/io/github/aedev/flow/ui/tv/components/TvScreenStates.kt +++ /dev/null @@ -1,51 +0,0 @@ -package io.github.aedev.flow.ui.tv.components - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp - -@Composable -fun TvLoadingState(modifier: Modifier = Modifier) { - Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - CircularProgressIndicator() - } -} - -@Composable -fun TvMessageState( - title: String, - message: String? = null, - modifier: Modifier = Modifier, -) { - Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - Column( - modifier = Modifier.padding(32.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(8.dp), - ) { - Text( - text = title, - style = MaterialTheme.typography.headlineSmall, - textAlign = TextAlign.Center, - ) - if (!message.isNullOrBlank()) { - Text( - text = message, - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurfaceVariant, - textAlign = TextAlign.Center, - ) - } - } - } -} diff --git a/app/src/main/java/io/github/aedev/flow/ui/tv/components/TvVideoCard.kt b/app/src/main/java/io/github/aedev/flow/ui/tv/components/TvVideoCard.kt deleted file mode 100644 index ad7d9f5e..00000000 --- a/app/src/main/java/io/github/aedev/flow/ui/tv/components/TvVideoCard.kt +++ /dev/null @@ -1,114 +0,0 @@ -package io.github.aedev.flow.ui.tv.components - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.aspectRatio -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.lazy.LazyRow -import androidx.compose.foundation.lazy.items -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import coil.compose.AsyncImage -import io.github.aedev.flow.data.model.Video - -@Composable -fun TvVideoCard( - video: Video, - onClick: () -> Unit, - modifier: Modifier = Modifier, -) { - TvFocusableCard( - onClick = onClick, - modifier = modifier.width(280.dp), - ) { - Column(modifier = Modifier.fillMaxWidth()) { - Box( - modifier = Modifier - .fillMaxWidth() - .aspectRatio(16f / 9f) - .background(MaterialTheme.colorScheme.surfaceVariant), - ) { - AsyncImage( - model = video.thumbnailUrl, - contentDescription = video.title, - modifier = Modifier.fillMaxSize(), - contentScale = ContentScale.Crop, - ) - if (video.duration > 0) { - Text( - text = formatTvDuration(video.duration), - style = MaterialTheme.typography.labelMedium, - color = MaterialTheme.colorScheme.inverseOnSurface, - modifier = Modifier - .align(Alignment.BottomEnd) - .padding(8.dp) - .background( - color = MaterialTheme.colorScheme.inverseSurface, - shape = MaterialTheme.shapes.extraSmall, - ) - .padding(horizontal = 6.dp, vertical = 2.dp), - ) - } - } - Column( - modifier = Modifier.padding(horizontal = 12.dp, vertical = 10.dp), - verticalArrangement = Arrangement.spacedBy(4.dp), - ) { - Text( - text = video.title, - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.SemiBold, - maxLines = 2, - overflow = TextOverflow.Ellipsis, - ) - Text( - text = video.channelName, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - } - } - } -} - -@Composable -fun TvVideoRow( - videos: List