From 2d3b0cea7ac013672090d401374b8161b98cf13d Mon Sep 17 00:00:00 2001 From: cottongin Date: Wed, 11 Mar 2026 16:27:13 -0400 Subject: [PATCH] feat: add tab rename and pin/unpin via long-press context menu Made-with: Cursor --- .../stationlist/RenamePlaylistDialog.kt | 42 +++++++++++++ .../screens/stationlist/StationListScreen.kt | 62 ++++++++++++++++--- .../stationlist/StationListViewModel.kt | 23 ++++++- 3 files changed, 114 insertions(+), 13 deletions(-) create mode 100644 app/src/main/java/xyz/cottongin/radio247/ui/screens/stationlist/RenamePlaylistDialog.kt diff --git a/app/src/main/java/xyz/cottongin/radio247/ui/screens/stationlist/RenamePlaylistDialog.kt b/app/src/main/java/xyz/cottongin/radio247/ui/screens/stationlist/RenamePlaylistDialog.kt new file mode 100644 index 0000000..07d2bf0 --- /dev/null +++ b/app/src/main/java/xyz/cottongin/radio247/ui/screens/stationlist/RenamePlaylistDialog.kt @@ -0,0 +1,42 @@ +package xyz.cottongin.radio247.ui.screens.stationlist + +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +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 + +@Composable +fun RenamePlaylistDialog( + currentName: String, + onDismiss: () -> Unit, + onConfirm: (String) -> Unit +) { + var name by remember { mutableStateOf(currentName) } + + AlertDialog( + onDismissRequest = onDismiss, + title = { Text("Rename playlist") }, + text = { + OutlinedTextField( + value = name, + onValueChange = { name = it }, + label = { Text("Playlist name") }, + singleLine = true + ) + }, + confirmButton = { + TextButton( + onClick = { onConfirm(name) }, + enabled = name.isNotBlank() + ) { Text("Rename") } + }, + dismissButton = { + TextButton(onClick = onDismiss) { Text("Cancel") } + } + ) +} diff --git a/app/src/main/java/xyz/cottongin/radio247/ui/screens/stationlist/StationListScreen.kt b/app/src/main/java/xyz/cottongin/radio247/ui/screens/stationlist/StationListScreen.kt index c1af2f8..3d5fe48 100644 --- a/app/src/main/java/xyz/cottongin/radio247/ui/screens/stationlist/StationListScreen.kt +++ b/app/src/main/java/xyz/cottongin/radio247/ui/screens/stationlist/StationListScreen.kt @@ -68,7 +68,7 @@ import xyz.cottongin.radio247.service.StreamResolver import org.json.JSONArray import xyz.cottongin.radio247.ui.components.MiniPlayer -@OptIn(ExperimentalMaterial3Api::class) +@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) @Composable fun StationListScreen( onNavigateToNowPlaying: () -> Unit, @@ -90,6 +90,7 @@ fun StationListScreen( var stationForQuality by remember { mutableStateOf(null) } var qualityStreams by remember { mutableStateOf>(emptyList()) } var qualityCurrentOrder by remember { mutableStateOf>(emptyList()) } + var tabToRename by remember { mutableStateOf(null) } val importLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.OpenDocument(), @@ -167,17 +168,47 @@ fun StationListScreen( edgePadding = 16.dp ) { viewState.tabs.forEachIndexed { index, tab -> - Tab( - selected = viewState.selectedTabIndex == index, - onClick = { viewModel.selectTab(index) }, - text = { - Text( - text = tab.label, - maxLines = 1, - overflow = TextOverflow.Ellipsis + var showTabMenu by remember { mutableStateOf(false) } + Box { + Tab( + selected = viewState.selectedTabIndex == index, + onClick = { viewModel.selectTab(index) }, + text = { + Text( + text = tab.label, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + }, + modifier = Modifier.combinedClickable( + onClick = { viewModel.selectTab(index) }, + onLongClick = { if (tab.playlist != null) showTabMenu = true } ) + ) + DropdownMenu( + expanded = showTabMenu, + onDismissRequest = { showTabMenu = false } + ) { + if (tab.playlist != null && !tab.isBuiltIn) { + DropdownMenuItem( + text = { Text("Rename") }, + onClick = { + showTabMenu = false + tabToRename = tab + } + ) + } + if (tab.playlist != null) { + DropdownMenuItem( + text = { Text(if (tab.playlist.pinned) "Unpin" else "Pin") }, + onClick = { + showTabMenu = false + viewModel.togglePinned(tab.playlist) + } + ) + } } - ) + } } } } @@ -331,6 +362,17 @@ fun StationListScreen( onConfirm = { name -> viewModel.confirmImport(name) } ) } + + tabToRename?.let { tab -> + RenamePlaylistDialog( + currentName = tab.label, + onDismiss = { tabToRename = null }, + onConfirm = { newName -> + tab.playlist?.let { viewModel.renamePlaylist(it, newName) } + tabToRename = null + } + ) + } } @Composable diff --git a/app/src/main/java/xyz/cottongin/radio247/ui/screens/stationlist/StationListViewModel.kt b/app/src/main/java/xyz/cottongin/radio247/ui/screens/stationlist/StationListViewModel.kt index 4d2854c..10b0759 100644 --- a/app/src/main/java/xyz/cottongin/radio247/ui/screens/stationlist/StationListViewModel.kt +++ b/app/src/main/java/xyz/cottongin/radio247/ui/screens/stationlist/StationListViewModel.kt @@ -136,10 +136,27 @@ class StationListViewModel(application: Application) : AndroidViewModel(applicat private fun buildTabs(playlists: List): List { val myStations = TabInfo(playlist = null, label = "My Stations", isBuiltIn = false) - val playlistTabs = playlists - .sortedWith(compareByDescending { it.isBuiltIn }.thenBy { it.sortOrder }) + val pinned = playlists + .filter { it.pinned } + .sortedBy { it.sortOrder } .map { TabInfo(playlist = it, label = it.name, isBuiltIn = it.isBuiltIn) } - return listOf(myStations) + playlistTabs + val unpinned = playlists + .filter { !it.pinned } + .sortedBy { it.sortOrder } + .map { TabInfo(playlist = it, label = it.name, isBuiltIn = it.isBuiltIn) } + return pinned + listOf(myStations) + unpinned + } + + fun renamePlaylist(playlist: Playlist, newName: String) { + viewModelScope.launch { + playlistDao.rename(playlist.id, newName) + } + } + + fun togglePinned(playlist: Playlist) { + viewModelScope.launch { + playlistDao.updatePinned(playlist.id, !playlist.pinned) + } } private fun applySortMode(