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.MusicNote import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.material.icons.filled.PlaylistAdd import androidx.compose.material.icons.filled.QueueMusic import androidx.compose.material.icons.filled.Refresh import androidx.compose.material.icons.filled.Search import androidx.compose.material.icons.filled.Shuffle 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.Tab import androidx.compose.material3.TabRow import androidx.compose.material3.TabRowDefaults import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf 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.MediaTypeFilter 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 MusicLibraryScreen( viewModel: MainViewModel, onPlayMedia: (MediaItemData) -> Unit, onAddToPlaylist: (MediaItemData) -> Unit, onPickMediaFile: () -> Unit, modifier: Modifier = Modifier ) { val musicItems by viewModel.musicItems.collectAsState() val videoItems by viewModel.videoItems.collectAsState() val libraryItems by viewModel.libraryItems.collectAsState() val libraryFilter by viewModel.libraryFilter.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() val activeItems = when (libraryFilter) { MediaTypeFilter.ALL -> libraryItems MediaTypeFilter.MUSIC -> musicItems MediaTypeFilter.VIDEOS -> videoItems } var selectedTab by remember { mutableIntStateOf(0) } val tabs = listOf("Tracks", "Artists", "Albums") var showSortMenu by remember { mutableStateOf(false) } Column( modifier = modifier .fillMaxSize() .background(AmoledBlack) .statusBarsPadding() .testTag("music_library_screen") ) { // Header & Actions Row( modifier = Modifier .fillMaxWidth() .padding(horizontal = 16.dp, vertical = 8.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween ) { Text( text = if (libraryFilter == MediaTypeFilter.ALL) "Media Library" else if (libraryFilter == MediaTypeFilter.VIDEOS) "Video Library" else "Music Library", color = TextPrimary, style = MaterialTheme.typography.headlineMedium, fontWeight = FontWeight.Bold ) Row(verticalAlignment = Alignment.CenterVertically) { // Grid / List Toggle IconButton( onClick = { viewModel.setIsGridLayout(!isGrid) }, modifier = Modifier.testTag("music_layout_toggle") ) { Icon( imageVector = if (isGrid) Icons.Default.ViewList else Icons.Default.GridView, contentDescription = "Toggle Grid/List", tint = TextSecondary ) } // Sort Options Box { IconButton( onClick = { showSortMenu = true }, modifier = Modifier.testTag("music_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 } ) } } } // Rescan Button IconButton( onClick = { viewModel.scanMedia() }, modifier = Modifier.testTag("music_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 Search Bar at the top with live title/artist filtering and type chips LibrarySearchBar( searchQuery = searchQuery, onQueryChange = { viewModel.setSearchQuery(it) }, selectedFilter = libraryFilter, onFilterChange = { viewModel.setLibraryFilter(it) }, matchCount = activeItems.size, placeholderText = "Filter audio & video by title or artist...", testTag = "music_search_bar" ) // Category Tabs: only when in MUSIC mode and not in a global search if (libraryFilter == MediaTypeFilter.MUSIC) { TabRow( selectedTabIndex = selectedTab, containerColor = AmoledBlack, contentColor = AccentViolet, indicator = { tabPositions -> TabRowDefaults.SecondaryIndicator( modifier = Modifier.tabIndicatorOffset(tabPositions[selectedTab]), color = AccentViolet ) }, divider = { Box(modifier = Modifier.fillMaxWidth().height(0.5.dp).background(DividerSubtle)) } ) { tabs.forEachIndexed { index, title -> Tab( selected = selectedTab == index, onClick = { selectedTab = index }, text = { Text( text = title, color = if (selectedTab == index) AccentViolet else TextSecondary, fontWeight = if (selectedTab == index) FontWeight.Bold else FontWeight.Normal ) } ) } } } // Quick Play All & Shuffle Buttons if (activeItems.isNotEmpty() && (libraryFilter != MediaTypeFilter.MUSIC || selectedTab == 0)) { Row( modifier = Modifier .fillMaxWidth() .padding(horizontal = 16.dp, vertical = 8.dp), horizontalArrangement = Arrangement.spacedBy(12.dp) ) { Surface( onClick = { if (activeItems.isNotEmpty()) { PlaybackManager.setQueue(activeItems, 0, autoPlay = true) } }, shape = RoundedCornerShape(10.dp), color = AccentViolet, modifier = Modifier .weight(1f) .testTag("music_play_all_btn") ) { Row( modifier = Modifier.padding(vertical = 10.dp), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically ) { Icon(imageVector = Icons.Default.PlayArrow, contentDescription = null, tint = AmoledBlack) Spacer(modifier = Modifier.width(6.dp)) Text("Play All (${activeItems.size})", color = AmoledBlack, fontWeight = FontWeight.Bold, fontSize = 13.sp) } } Surface( onClick = { if (activeItems.isNotEmpty()) { PlaybackManager.toggleShuffle() PlaybackManager.setQueue(activeItems.shuffled(), 0, autoPlay = true) } }, shape = RoundedCornerShape(10.dp), color = SurfaceElevated, modifier = Modifier .weight(1f) .testTag("music_shuffle_all_btn") ) { Row( modifier = Modifier.padding(vertical = 10.dp), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically ) { Icon(imageVector = Icons.Default.Shuffle, contentDescription = null, tint = AccentViolet) Spacer(modifier = Modifier.width(6.dp)) Text("Shuffle", color = TextPrimary, fontWeight = FontWeight.Bold, fontSize = 13.sp) } } } } // Content based on tab or active media filter if (libraryFilter != MediaTypeFilter.MUSIC) { TracksTabContent( items = activeItems, isGrid = isGrid, searchQuery = searchQuery, onClearSearch = { viewModel.setSearchQuery("") }, onPlayMedia = onPlayMedia, onAddToPlaylist = onAddToPlaylist, onDetails = { viewModel.showMediaDetails(it) } ) } else { when (selectedTab) { 0 -> TracksTabContent( items = musicItems, isGrid = isGrid, searchQuery = searchQuery, onClearSearch = { viewModel.setSearchQuery("") }, onPlayMedia = onPlayMedia, onAddToPlaylist = onAddToPlaylist, onDetails = { viewModel.showMediaDetails(it) } ) 1 -> GroupedMediaTabContent( groups = musicItems.groupBy { it.artist }, groupLabel = "Artists", onPlayGroup = { items -> PlaybackManager.setQueue(items, 0, autoPlay = true) } ) 2 -> GroupedMediaTabContent( groups = musicItems.groupBy { it.album }, groupLabel = "Albums", onPlayGroup = { items -> PlaybackManager.setQueue(items, 0, autoPlay = true) } ) } } } } @OptIn(UnstableApi::class) @Composable private fun TracksTabContent( items: List, isGrid: Boolean, searchQuery: String = "", onClearSearch: () -> Unit = {}, onPlayMedia: (MediaItemData) -> Unit, onAddToPlaylist: (MediaItemData) -> Unit, onDetails: (MediaItemData) -> Unit ) { if (items.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.MusicNote, contentDescription = null, tint = AccentViolet, modifier = Modifier.size(54.dp) ) Spacer(modifier = Modifier.height(14.dp)) Text( text = if (searchQuery.isNotBlank()) { "No files matching \"$searchQuery\" found.\nCheck the title or artist spelling." } else { "No media items found.\nTap Rescan or Open File to add media." }, color = TextSecondary, textAlign = androidx.compose.ui.text.style.TextAlign.Center, style = MaterialTheme.typography.bodyMedium ) if (searchQuery.isNotBlank()) { Spacer(modifier = Modifier.height(16.dp)) Surface( onClick = onClearSearch, shape = RoundedCornerShape(10.dp), color = SurfaceElevated ) { Text( text = "Clear Search Filter", color = AccentViolet, fontWeight = FontWeight.Bold, modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp) ) } } } } } else if (isGrid) { LazyVerticalGrid( columns = GridCells.Adaptive(150.dp), modifier = Modifier .fillMaxSize() .padding(horizontal = 12.dp), verticalArrangement = Arrangement.spacedBy(10.dp), horizontalArrangement = Arrangement.spacedBy(10.dp) ) { items(items, key = { it.id }) { item -> MusicGridCard( item = item, onClick = { onPlayMedia(item) }, onAddToPlaylist = { onAddToPlaylist(item) }, onDetails = { onDetails(item) } ) } } } else { LazyColumn(modifier = Modifier.fillMaxSize()) { items(items, key = { it.id }) { item -> MusicListItem( item = item, onClick = { onPlayMedia(item) }, onAddToPlaylist = { onAddToPlaylist(item) }, onDetails = { onDetails(item) } ) } } } } @OptIn(UnstableApi::class) @Composable fun MusicListItem( item: MediaItemData, onClick: () -> Unit, onAddToPlaylist: () -> Unit, onDetails: () -> Unit, modifier: Modifier = Modifier ) { var showMenu by remember { mutableStateOf(false) } Row( modifier = modifier .fillMaxWidth() .clickable { onClick() } .padding(horizontal = 16.dp, vertical = 8.dp), verticalAlignment = Alignment.CenterVertically ) { Box( modifier = Modifier .size(48.dp) .clip(RoundedCornerShape(8.dp)) .background(SurfaceElevated), contentAlignment = Alignment.Center ) { if (!item.albumArtUri.isNullOrBlank()) { AsyncImage( model = Uri.parse(item.albumArtUri), contentDescription = null, contentScale = ContentScale.Crop, modifier = Modifier.fillMaxSize() ) } else { Icon( imageVector = if (item.isVideo) Icons.Default.Videocam else Icons.Default.MusicNote, contentDescription = null, tint = AccentViolet ) } } Spacer(modifier = Modifier.width(12.dp)) Column(modifier = Modifier.weight(1f)) { Row(verticalAlignment = Alignment.CenterVertically) { if (item.isVideo) { Surface( shape = RoundedCornerShape(4.dp), color = AccentViolet.copy(alpha = 0.2f), modifier = Modifier.padding(end = 6.dp) ) { Text( text = "VIDEO", color = AccentViolet, fontSize = 9.sp, fontWeight = FontWeight.Bold, modifier = Modifier.padding(horizontal = 4.dp, vertical = 1.dp) ) } } Text( text = item.title, color = TextPrimary, fontSize = 14.sp, fontWeight = FontWeight.Medium, maxLines = 1, overflow = TextOverflow.Ellipsis ) } Spacer(modifier = Modifier.height(2.dp)) Text( text = "${item.artist} • ${item.formattedDuration}", 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(item) showMenu = false } ) DropdownMenuItem( text = { Text("Add to Queue", color = TextPrimary) }, onClick = { PlaybackManager.addToQueue(item) showMenu = false } ) DropdownMenuItem( text = { Text("Add to Playlist", color = TextPrimary) }, onClick = { onAddToPlaylist() showMenu = false } ) DropdownMenuItem( text = { Text("Details", color = TextPrimary) }, onClick = { onDetails() showMenu = false } ) } } } } @OptIn(UnstableApi::class) @Composable fun MusicGridCard( item: MediaItemData, onClick: () -> Unit, onAddToPlaylist: () -> Unit, onDetails: () -> Unit ) { var showMenu by remember { mutableStateOf(false) } Surface( modifier = Modifier .fillMaxWidth() .clip(RoundedCornerShape(12.dp)) .clickable { onClick() }, color = SurfaceSecondary ) { Column(modifier = Modifier.padding(8.dp)) { Box( modifier = Modifier .fillMaxWidth() .height(130.dp) .clip(RoundedCornerShape(10.dp)) .background(SurfaceElevated), contentAlignment = Alignment.Center ) { if (!item.albumArtUri.isNullOrBlank()) { AsyncImage( model = Uri.parse(item.albumArtUri), contentDescription = null, contentScale = ContentScale.Crop, modifier = Modifier.fillMaxSize() ) } else { Icon(imageVector = Icons.Default.MusicNote, contentDescription = null, tint = AccentViolet, modifier = Modifier.size(48.dp)) } Box( modifier = Modifier .align(Alignment.TopEnd) .padding(4.dp) ) { IconButton(onClick = { showMenu = true }, modifier = Modifier.size(32.dp)) { Icon(imageVector = Icons.Default.MoreVert, contentDescription = null, tint = TextPrimary) } DropdownMenu(expanded = showMenu, onDismissRequest = { showMenu = false }, modifier = Modifier.background(SurfaceElevated)) { DropdownMenuItem(text = { Text("Add to Queue", color = TextPrimary) }, onClick = { PlaybackManager.addToQueue(item); showMenu = false }) DropdownMenuItem(text = { Text("Add to Playlist", color = TextPrimary) }, onClick = { onAddToPlaylist(); showMenu = false }) DropdownMenuItem(text = { Text("Details", color = TextPrimary) }, onClick = { onDetails(); showMenu = false }) } } } Spacer(modifier = Modifier.height(6.dp)) Text( text = item.title, color = TextPrimary, fontSize = 13.sp, fontWeight = FontWeight.SemiBold, maxLines = 1, overflow = TextOverflow.Ellipsis ) Text( text = item.artist, color = TextSecondary, fontSize = 11.sp, maxLines = 1, overflow = TextOverflow.Ellipsis ) } } } @Composable private fun GroupedMediaTabContent( groups: Map>, groupLabel: String, onPlayGroup: (List) -> Unit ) { if (groups.isEmpty()) { Box(modifier = Modifier.fillMaxSize().padding(32.dp), contentAlignment = Alignment.Center) { Text(text = "No $groupLabel found.", color = TextSecondary) } } else { LazyColumn(modifier = Modifier.fillMaxSize()) { groups.forEach { (name, tracks) -> item(key = name) { Row( modifier = Modifier .fillMaxWidth() .clickable { onPlayGroup(tracks) } .padding(horizontal = 16.dp, vertical = 12.dp), verticalAlignment = Alignment.CenterVertically ) { Box( modifier = Modifier .size(48.dp) .clip(RoundedCornerShape(8.dp)) .background(SurfaceElevated), contentAlignment = Alignment.Center ) { Icon(imageVector = Icons.Default.MusicNote, contentDescription = null, tint = AccentViolet) } Spacer(modifier = Modifier.width(14.dp)) Column(modifier = Modifier.weight(1f)) { Text(text = name, color = TextPrimary, fontSize = 15.sp, fontWeight = FontWeight.SemiBold) Text(text = "${tracks.size} tracks", color = TextSecondary, fontSize = 12.sp) } IconButton(onClick = { onPlayGroup(tracks) }) { Icon(imageVector = Icons.Default.PlayArrow, contentDescription = "Play All", tint = AccentViolet) } } } } } } }