package com.example.ui.screens import android.net.Uri import androidx.annotation.OptIn import androidx.compose.foundation.background import androidx.compose.foundation.clickable 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.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowDownward import androidx.compose.material.icons.filled.ArrowUpward import androidx.compose.material.icons.filled.GridView import androidx.compose.material.icons.filled.MoreVert 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.Sort import androidx.compose.material.icons.filled.Videocam import androidx.compose.material.icons.filled.ViewList import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextFieldDefaults import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.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.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.SortOption import com.example.data.model.SortOrder import com.example.playback.PlaybackManager import com.example.ui.components.LibrarySearchBar import com.example.ui.theme.AccentViolet import com.example.ui.theme.AmoledBlack import com.example.ui.theme.DividerSubtle import com.example.ui.theme.SurfaceElevated import com.example.ui.theme.SurfaceSecondary import com.example.ui.theme.TextPrimary import com.example.ui.theme.TextSecondary import com.example.ui.viewmodel.MainViewModel @OptIn(UnstableApi::class) @Composable fun VideoLibraryScreen( viewModel: MainViewModel, onPlayVideo: (MediaItemData) -> Unit, onAddToPlaylist: (MediaItemData) -> Unit, onPickMediaFile: () -> Unit, modifier: Modifier = Modifier ) { val videoItems by viewModel.videoItems.collectAsState() val searchQuery by viewModel.searchQuery.collectAsState() val isScanning by viewModel.isScanning.collectAsState() val isGrid by viewModel.isGridLayout.collectAsState() val currentSortOption by viewModel.musicSortOption.collectAsState() val currentSortOrder by viewModel.musicSortOrder.collectAsState() var showSortMenu by remember { mutableStateOf(false) } Column( modifier = modifier .fillMaxSize() .background(AmoledBlack) .statusBarsPadding() .testTag("video_library_screen") ) { // Top Bar Row( modifier = Modifier .fillMaxWidth() .padding(horizontal = 16.dp, vertical = 8.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween ) { Text( text = "Video Library", color = TextPrimary, style = MaterialTheme.typography.headlineMedium, fontWeight = FontWeight.Bold ) Row(verticalAlignment = Alignment.CenterVertically) { IconButton( onClick = { viewModel.setIsGridLayout(!isGrid) }, modifier = Modifier.testTag("video_layout_toggle") ) { Icon( imageVector = if (isGrid) Icons.Default.ViewList else Icons.Default.GridView, contentDescription = "Toggle Grid/List", tint = TextSecondary ) } Box { IconButton( onClick = { showSortMenu = true }, modifier = Modifier.testTag("video_sort_button") ) { Icon(imageVector = Icons.Default.Sort, contentDescription = "Sort", tint = AccentViolet) } DropdownMenu( expanded = showSortMenu, onDismissRequest = { showSortMenu = false }, modifier = Modifier.background(SurfaceElevated) ) { SortOption.values().forEach { option -> DropdownMenuItem( text = { Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { Text(option.label, color = TextPrimary) if (currentSortOption == option) { Icon( imageVector = if (currentSortOrder == SortOrder.ASCENDING) Icons.Default.ArrowUpward else Icons.Default.ArrowDownward, contentDescription = null, tint = AccentViolet, modifier = Modifier.size(16.dp) ) } } }, onClick = { val newOrder = if (currentSortOption == option && currentSortOrder == SortOrder.ASCENDING) { SortOrder.DESCENDING } else { SortOrder.ASCENDING } viewModel.setMusicSort(option, newOrder) showSortMenu = false } ) } } } IconButton( onClick = { viewModel.scanMedia() }, modifier = Modifier.testTag("video_rescan_button") ) { if (isScanning) { CircularProgressIndicator(modifier = Modifier.size(20.dp), color = AccentViolet, strokeWidth = 2.dp) } else { Icon(imageVector = Icons.Default.Refresh, contentDescription = "Rescan", tint = TextSecondary) } } } } // Prominent Library Search Bar at the top LibrarySearchBar( searchQuery = searchQuery, onQueryChange = { viewModel.setSearchQuery(it) }, matchCount = videoItems.size, placeholderText = "Filter videos by title or artist...", testTag = "video_search_bar" ) Spacer(modifier = Modifier.height(4.dp)) if (videoItems.isEmpty()) { Box( modifier = Modifier .fillMaxSize() .padding(32.dp), contentAlignment = Alignment.Center ) { Column(horizontalAlignment = Alignment.CenterHorizontally) { Icon( imageVector = if (searchQuery.isNotBlank()) Icons.Default.Search else Icons.Default.Videocam, contentDescription = null, tint = AccentViolet, modifier = Modifier.size(56.dp) ) Spacer(modifier = Modifier.height(14.dp)) Text( text = if (searchQuery.isNotBlank()) { "No videos matching \"$searchQuery\" found.\nCheck the video title or artist name." } else { "No videos discovered on device." }, color = TextSecondary, textAlign = androidx.compose.ui.text.style.TextAlign.Center, style = MaterialTheme.typography.bodyMedium ) Spacer(modifier = Modifier.height(16.dp)) if (searchQuery.isNotBlank()) { Surface( onClick = { viewModel.setSearchQuery("") }, shape = RoundedCornerShape(10.dp), color = SurfaceElevated ) { Text( text = "Clear Search Filter", color = AccentViolet, fontWeight = FontWeight.Bold, modifier = Modifier.padding(horizontal = 16.dp, vertical = 10.dp) ) } } else { Surface( onClick = onPickMediaFile, shape = RoundedCornerShape(10.dp), color = AccentViolet ) { Text( text = "Open Video File", color = Color.Black, fontWeight = FontWeight.Bold, modifier = Modifier.padding(horizontal = 16.dp, vertical = 10.dp) ) } } } } } else if (isGrid) { LazyVerticalGrid( columns = GridCells.Adaptive(160.dp), modifier = Modifier .fillMaxSize() .padding(horizontal = 12.dp), verticalArrangement = Arrangement.spacedBy(12.dp), horizontalArrangement = Arrangement.spacedBy(12.dp) ) { items(videoItems, key = { it.id }) { video -> VideoGridItem( video = video, onClick = { onPlayVideo(video) }, onAddToPlaylist = { onAddToPlaylist(video) }, onDetails = { viewModel.showMediaDetails(video) } ) } } } else { LazyColumn(modifier = Modifier.fillMaxSize()) { items(videoItems, key = { it.id }) { video -> VideoListItem( video = video, onClick = { onPlayVideo(video) }, onAddToPlaylist = { onAddToPlaylist(video) }, onDetails = { viewModel.showMediaDetails(video) } ) } } } } } @Composable fun VideoGridItem( video: MediaItemData, onClick: () -> Unit, onAddToPlaylist: () -> Unit, onDetails: () -> Unit ) { var showMenu by remember { mutableStateOf(false) } Surface( modifier = Modifier .fillMaxWidth() .clip(RoundedCornerShape(12.dp)) .clickable { onClick() } .testTag("video_grid_item_${video.id}"), color = SurfaceSecondary ) { Column { Box( modifier = Modifier .fillMaxWidth() .height(110.dp) .background(SurfaceElevated), contentAlignment = Alignment.Center ) { if (!video.albumArtUri.isNullOrBlank()) { AsyncImage( model = Uri.parse(video.albumArtUri), contentDescription = null, contentScale = ContentScale.Crop, modifier = Modifier.fillMaxSize() ) } else { Icon(imageVector = Icons.Default.Videocam, contentDescription = null, tint = AccentViolet, modifier = Modifier.size(40.dp)) } // Duration Pill Surface( shape = RoundedCornerShape(4.dp), color = Color.Black.copy(alpha = 0.8f), modifier = Modifier .align(Alignment.BottomEnd) .padding(6.dp) ) { Text( text = video.formattedDuration, color = Color.White, fontSize = 10.sp, fontWeight = FontWeight.Bold, modifier = Modifier.padding(horizontal = 5.dp, vertical = 2.dp) ) } // Resolution Pill if (video.resolutionString.isNotBlank()) { Surface( shape = RoundedCornerShape(4.dp), color = Color.Black.copy(alpha = 0.8f), modifier = Modifier .align(Alignment.BottomStart) .padding(6.dp) ) { Text( text = video.resolutionString, color = AccentViolet, fontSize = 9.sp, fontWeight = FontWeight.Bold, modifier = Modifier.padding(horizontal = 4.dp, vertical = 2.dp) ) } } } Column(modifier = Modifier.padding(8.dp)) { Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { Text( text = video.title, color = TextPrimary, fontSize = 13.sp, fontWeight = FontWeight.SemiBold, maxLines = 1, overflow = TextOverflow.Ellipsis, modifier = Modifier.weight(1f) ) Box { IconButton(onClick = { showMenu = true }, modifier = Modifier.size(24.dp)) { Icon(imageVector = Icons.Default.MoreVert, contentDescription = null, tint = TextSecondary, modifier = Modifier.size(16.dp)) } DropdownMenu(expanded = showMenu, onDismissRequest = { showMenu = false }, modifier = Modifier.background(SurfaceElevated)) { DropdownMenuItem(text = { Text("Play in Queue", color = TextPrimary) }, onClick = { PlaybackManager.addToQueue(video); showMenu = false }) DropdownMenuItem(text = { Text("Add to Playlist", color = TextPrimary) }, onClick = { onAddToPlaylist(); showMenu = false }) DropdownMenuItem(text = { Text("Details", color = TextPrimary) }, onClick = { onDetails(); showMenu = false }) } } } Text( text = "${video.folderName} • ${video.formattedSize}", color = TextSecondary, fontSize = 11.sp, maxLines = 1, overflow = TextOverflow.Ellipsis ) } } } } @Composable fun VideoListItem( video: MediaItemData, onClick: () -> Unit, onAddToPlaylist: () -> Unit, onDetails: () -> Unit ) { var showMenu by remember { mutableStateOf(false) } Row( modifier = Modifier .fillMaxWidth() .clickable { onClick() } .padding(horizontal = 16.dp, vertical = 8.dp) .testTag("video_list_item_${video.id}"), verticalAlignment = Alignment.CenterVertically ) { Box( modifier = Modifier .size(width = 80.dp, height = 52.dp) .clip(RoundedCornerShape(8.dp)) .background(SurfaceElevated), contentAlignment = Alignment.Center ) { if (!video.albumArtUri.isNullOrBlank()) { AsyncImage( model = Uri.parse(video.albumArtUri), contentDescription = null, contentScale = ContentScale.Crop, modifier = Modifier.fillMaxSize() ) } else { Icon(imageVector = Icons.Default.Videocam, contentDescription = null, tint = AccentViolet) } Surface( shape = RoundedCornerShape(3.dp), color = Color.Black.copy(alpha = 0.8f), modifier = Modifier .align(Alignment.BottomEnd) .padding(3.dp) ) { Text( text = video.formattedDuration, color = Color.White, fontSize = 9.sp, fontWeight = FontWeight.Bold, modifier = Modifier.padding(horizontal = 3.dp, vertical = 1.dp) ) } } Spacer(modifier = Modifier.width(12.dp)) Column(modifier = Modifier.weight(1f)) { Text( text = video.title, color = TextPrimary, fontSize = 14.sp, fontWeight = FontWeight.Medium, maxLines = 1, overflow = TextOverflow.Ellipsis ) Spacer(modifier = Modifier.height(2.dp)) Text( text = "${video.folderName} • ${if (video.resolutionString.isNotBlank()) "${video.resolutionString} • " else ""}${video.formattedSize}", color = TextSecondary, fontSize = 12.sp, maxLines = 1, overflow = TextOverflow.Ellipsis ) } Box { IconButton(onClick = { showMenu = true }) { Icon(imageVector = Icons.Default.MoreVert, contentDescription = "Options", tint = TextSecondary) } DropdownMenu(expanded = showMenu, onDismissRequest = { showMenu = false }, modifier = Modifier.background(SurfaceElevated)) { DropdownMenuItem(text = { Text("Play Next", color = TextPrimary) }, onClick = { PlaybackManager.playNextInQueue(video); showMenu = false }) DropdownMenuItem(text = { Text("Add to Queue", color = TextPrimary) }, onClick = { PlaybackManager.addToQueue(video); showMenu = false }) DropdownMenuItem(text = { Text("Add to Playlist", color = TextPrimary) }, onClick = { onAddToPlaylist(); showMenu = false }) DropdownMenuItem(text = { Text("Details", color = TextPrimary) }, onClick = { onDetails(); showMenu = false }) } } } }