package com.example.ui.screens import androidx.annotation.OptIn import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource 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.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.layout.width import androidx.compose.foundation.pager.VerticalPager import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.CropFree import androidx.compose.material.icons.filled.Favorite import androidx.compose.material.icons.filled.FavoriteBorder import androidx.compose.material.icons.filled.FilterList import androidx.compose.material.icons.filled.FitScreen import androidx.compose.material.icons.filled.Info import androidx.compose.material.icons.filled.MusicNote import androidx.compose.material.icons.filled.Pause import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.material.icons.filled.PlaylistAdd import androidx.compose.material.icons.filled.Refresh import androidx.compose.material.icons.filled.Search import androidx.compose.material.icons.filled.Videocam import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.FilterChip import androidx.compose.material3.FilterChipDefaults import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.testTag import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.media3.common.util.UnstableApi import coil.compose.AsyncImage import com.example.data.model.MediaItemData import com.example.data.model.MediaTypeFilter import com.example.data.model.VideoScaleMode import com.example.playback.PlaybackManager import com.example.ui.components.VideoPlayerView import com.example.ui.theme.AccentViolet import com.example.ui.theme.AccentVioletDim import com.example.ui.theme.AmoledBlack import com.example.ui.theme.DividerSubtle import com.example.ui.theme.SurfaceElevated import com.example.ui.theme.TextPrimary import com.example.ui.theme.TextSecondary import com.example.ui.theme.TextTertiary import com.example.ui.viewmodel.MainViewModel import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.launch @OptIn(UnstableApi::class) @Composable fun SwipeFeedScreen( viewModel: MainViewModel, onOpenPlaylistDialog: (MediaItemData) -> Unit, onPickMediaFile: () -> Unit, modifier: Modifier = Modifier ) { val feedItems by viewModel.feedMediaItems.collectAsState() val feedFilter by viewModel.feedFilter.collectAsState() val isScanning by viewModel.isScanning.collectAsState() val scaleMode by PlaybackManager.videoScaleMode.collectAsState() val favorites by viewModel.favorites.collectAsState() val scope = rememberCoroutineScope() Box( modifier = modifier .fillMaxSize() .background(AmoledBlack) .testTag("swipe_feed_screen") ) { if (feedItems.isEmpty()) { // Empty feed state Column( modifier = Modifier .fillMaxSize() .padding(32.dp), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { if (isScanning) { CircularProgressIndicator(color = AccentViolet) Spacer(modifier = Modifier.height(16.dp)) Text("Scanning media files...", color = TextSecondary) } else { Icon( imageVector = Icons.Default.MusicNote, contentDescription = null, tint = AccentViolet, modifier = Modifier.size(64.dp) ) Spacer(modifier = Modifier.height(16.dp)) Text( text = "No ${feedFilter.label} Found", color = TextPrimary, style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold ) Spacer(modifier = Modifier.height(8.dp)) Text( text = "Rescan your device or import an audio/video file using the system file picker.", color = TextSecondary, textAlign = TextAlign.Center, style = MaterialTheme.typography.bodyMedium ) Spacer(modifier = Modifier.height(24.dp)) Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { Button( onClick = { viewModel.scanMedia() }, colors = ButtonDefaults.buttonColors(containerColor = SurfaceElevated), modifier = Modifier.testTag("feed_rescan_btn") ) { Icon(imageVector = Icons.Default.Refresh, contentDescription = null, tint = AccentViolet) Spacer(modifier = Modifier.width(6.dp)) Text("Rescan", color = TextPrimary) } Button( onClick = onPickMediaFile, colors = ButtonDefaults.buttonColors(containerColor = AccentViolet), modifier = Modifier.testTag("feed_import_file_btn") ) { Text("Open File", color = Color.Black, fontWeight = FontWeight.Bold) } } } } } else { val initialIndex by viewModel.initialFeedIndex.collectAsState() val pagerState = rememberPagerState( initialPage = initialIndex.coerceIn(0, (feedItems.size - 1).coerceAtLeast(0)), pageCount = { feedItems.size } ) var hasRestoredInitialPage by remember { mutableStateOf(false) } LaunchedEffect(initialIndex, feedItems.size) { if (!hasRestoredInitialPage && initialIndex in feedItems.indices && feedItems.isNotEmpty()) { if (pagerState.currentPage != initialIndex) { pagerState.scrollToPage(initialIndex) } hasRestoredInitialPage = true } } // Maintain single-item playback when swiping between pages LaunchedEffect(pagerState, feedItems) { snapshotFlow { pagerState.currentPage } .distinctUntilChanged() .collect { page -> if (page in feedItems.indices) { val targetItem = feedItems[page] val currentLoaded = PlaybackManager.currentMediaItem.value if (currentLoaded?.id != targetItem.id) { PlaybackManager.setQueue(feedItems, page, autoPlay = true) } } } } VerticalPager( state = pagerState, modifier = Modifier.fillMaxSize() ) { page -> val item = feedItems[page] val isFav = favorites.any { it.id == item.id } FeedItemPage( mediaItem = item, isCurrentPage = pagerState.currentPage == page, isFavorite = isFav, onToggleFavorite = { viewModel.toggleFavorite(item) }, onInfoClick = { viewModel.showMediaDetails(item) }, onPlaylistClick = { onOpenPlaylistDialog(item) } ) } } // Top Filter Bar Overlay Row( modifier = Modifier .fillMaxWidth() .align(Alignment.TopCenter) .background( Brush.verticalGradient( colors = listOf(Color.Black.copy(alpha = 0.85f), Color.Transparent) ) ) .statusBarsPadding() .padding(horizontal = 16.dp, vertical = 8.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween ) { Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { MediaTypeFilter.values().forEach { filter -> FilterChip( selected = feedFilter == filter, onClick = { viewModel.setFeedFilter(filter) }, label = { Text( text = filter.label, fontSize = 12.sp, fontWeight = if (feedFilter == filter) FontWeight.Bold else FontWeight.Normal ) }, colors = FilterChipDefaults.filterChipColors( selectedContainerColor = AccentViolet, selectedLabelColor = Color.Black, containerColor = SurfaceElevated.copy(alpha = 0.85f), labelColor = TextSecondary ), shape = RoundedCornerShape(20.dp), border = null, modifier = Modifier.testTag("feed_filter_${filter.name.lowercase()}") ) } } // Quick FIT / FILL Mode Toggle in Header Surface( onClick = { viewModel.toggleVideoScaleMode() }, shape = RoundedCornerShape(12.dp), color = SurfaceElevated.copy(alpha = 0.85f), modifier = Modifier.testTag("feed_scale_mode_btn") ) { Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp) ) { Icon( imageVector = if (scaleMode == VideoScaleMode.FIT) Icons.Default.FitScreen else Icons.Default.CropFree, contentDescription = "Video Fit Mode", tint = AccentViolet, modifier = Modifier.size(16.dp) ) Spacer(modifier = Modifier.width(4.dp)) Text( text = if (scaleMode == VideoScaleMode.FIT) "FIT" else "FILL", color = TextPrimary, fontSize = 11.sp, fontWeight = FontWeight.Bold ) } } } } } @OptIn(UnstableApi::class) @Composable private fun FeedItemPage( mediaItem: MediaItemData, isCurrentPage: Boolean, isFavorite: Boolean, onToggleFavorite: () -> Unit, onInfoClick: () -> Unit, onPlaylistClick: () -> Unit ) { val isPlaying by PlaybackManager.isPlaying.collectAsState() Box( modifier = Modifier .fillMaxSize() .background(AmoledBlack) ) { if (mediaItem.isVideo) { // Native Video Player in Feed VideoPlayerView( mediaItem = mediaItem, modifier = Modifier.fillMaxSize(), onInfoClick = onInfoClick ) } else { // Audio Card in Feed AudioFeedCard( mediaItem = mediaItem, isPlaying = isPlaying && isCurrentPage, modifier = Modifier.fillMaxSize() ) } // Right-side floating action column (Like TikTok / Reels vertical UI) Column( modifier = Modifier .align(Alignment.BottomEnd) .navigationBarsPadding() .padding(end = 16.dp, bottom = 80.dp), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(16.dp) ) { // Play/Pause Action FloatingFeedAction( icon = if (isPlaying && isCurrentPage) Icons.Default.Pause else Icons.Default.PlayArrow, tint = if (isPlaying && isCurrentPage) AccentViolet else TextPrimary, label = if (isPlaying && isCurrentPage) "Pause" else "Play", onClick = { PlaybackManager.togglePlayPause() }, testTag = "feed_play_pause_btn" ) // Favorite Button FloatingFeedAction( icon = if (isFavorite) Icons.Default.Favorite else Icons.Default.FavoriteBorder, tint = if (isFavorite) AccentViolet else TextPrimary, label = if (isFavorite) "Liked" else "Like", onClick = onToggleFavorite, testTag = "feed_fav_btn" ) // Add to Playlist Button FloatingFeedAction( icon = Icons.Default.PlaylistAdd, tint = TextPrimary, label = "Playlist", onClick = onPlaylistClick, testTag = "feed_add_playlist_btn" ) // Media Details FloatingFeedAction( icon = Icons.Default.Info, tint = TextPrimary, label = "Details", onClick = onInfoClick, testTag = "feed_info_btn" ) } // Bottom Metadata Badge (Overlay) Column( modifier = Modifier .align(Alignment.BottomStart) .fillMaxWidth(0.78f) .background( Brush.verticalGradient( colors = listOf(Color.Transparent, Color.Black.copy(alpha = 0.8f)) ) ) .navigationBarsPadding() .padding(start = 16.dp, bottom = 80.dp) ) { Row(verticalAlignment = Alignment.CenterVertically) { Surface( shape = RoundedCornerShape(6.dp), color = AccentVioletDim, modifier = Modifier.padding(bottom = 6.dp) ) { Text( text = if (mediaItem.isVideo) "VIDEO" else "MUSIC", color = AccentViolet, fontSize = 10.sp, fontWeight = FontWeight.Bold, modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp) ) } if (mediaItem.isVideo && mediaItem.resolutionString.isNotBlank()) { Spacer(modifier = Modifier.width(6.dp)) Surface( shape = RoundedCornerShape(6.dp), color = SurfaceElevated, modifier = Modifier.padding(bottom = 6.dp) ) { Text( text = mediaItem.resolutionString, color = TextSecondary, fontSize = 10.sp, modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp) ) } } } Text( text = mediaItem.title, color = TextPrimary, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold, maxLines = 2, overflow = TextOverflow.Ellipsis ) Spacer(modifier = Modifier.height(2.dp)) Text( text = if (mediaItem.isVideo) "${mediaItem.folderName} • ${mediaItem.formattedDuration}" else "${mediaItem.artist} • ${mediaItem.album}", color = TextSecondary, style = MaterialTheme.typography.bodySmall, maxLines = 1, overflow = TextOverflow.Ellipsis ) } } } @OptIn(UnstableApi::class) @Composable private fun AudioFeedCard( mediaItem: MediaItemData, isPlaying: Boolean, modifier: Modifier = Modifier ) { Box( modifier = modifier .fillMaxSize() .background(AmoledBlack) .clickable( interactionSource = remember { MutableInteractionSource() }, indication = null ) { PlaybackManager.togglePlayPause() }, contentAlignment = Alignment.Center ) { // Large Album Art with Tap to play/pause Column( horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.padding(horizontal = 32.dp) ) { Box( modifier = Modifier .size(260.dp) .clip(RoundedCornerShape(24.dp)) .background(SurfaceElevated), contentAlignment = Alignment.Center ) { if (!mediaItem.albumArtUri.isNullOrBlank()) { AsyncImage( model = mediaItem.albumArtUri, contentDescription = "Album Art", contentScale = ContentScale.Crop, modifier = Modifier.fillMaxSize() ) } else { Icon( imageVector = Icons.Default.MusicNote, contentDescription = null, tint = AccentViolet, modifier = Modifier.size(90.dp) ) } // Always-accessible prominent Play/Pause button on artwork Box( modifier = Modifier .fillMaxSize() .background(Color.Black.copy(alpha = if (isPlaying) 0.25f else 0.45f)), contentAlignment = Alignment.Center ) { IconButton( onClick = { PlaybackManager.togglePlayPause() }, modifier = Modifier .size(68.dp) .clip(CircleShape) .background(if (isPlaying) Color.Black.copy(alpha = 0.65f) else AccentViolet) .testTag("audio_feed_play_pause_btn") ) { Icon( imageVector = if (isPlaying) Icons.Default.Pause else Icons.Default.PlayArrow, contentDescription = if (isPlaying) "Pause" else "Play", tint = if (isPlaying) AccentViolet else AmoledBlack, modifier = Modifier.size(38.dp) ) } } } Spacer(modifier = Modifier.height(16.dp)) Text( text = if (isPlaying) "Tap artwork to pause" else "Tap artwork to play", color = TextTertiary, fontSize = 12.sp ) } } } @Composable private fun FloatingFeedAction( icon: androidx.compose.ui.graphics.vector.ImageVector, tint: Color, label: String, onClick: () -> Unit, testTag: String ) { Column( horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.clickable { onClick() } ) { Box( modifier = Modifier .size(48.dp) .clip(CircleShape) .background(Color.Black.copy(alpha = 0.55f)) .testTag(testTag), contentAlignment = Alignment.Center ) { Icon( imageVector = icon, contentDescription = label, tint = tint, modifier = Modifier.size(26.dp) ) } Spacer(modifier = Modifier.height(4.dp)) Text( text = label, color = TextPrimary, fontSize = 11.sp, fontWeight = FontWeight.Medium ) } }