feat: add tab rename and pin/unpin via long-press context menu

Made-with: Cursor
This commit is contained in:
cottongin
2026-03-11 16:27:13 -04:00
parent 973bb9e7fe
commit 2d3b0cea7a
3 changed files with 114 additions and 13 deletions

View File

@@ -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") }
}
)
}

View File

@@ -68,7 +68,7 @@ import xyz.cottongin.radio247.service.StreamResolver
import org.json.JSONArray import org.json.JSONArray
import xyz.cottongin.radio247.ui.components.MiniPlayer import xyz.cottongin.radio247.ui.components.MiniPlayer
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
@Composable @Composable
fun StationListScreen( fun StationListScreen(
onNavigateToNowPlaying: () -> Unit, onNavigateToNowPlaying: () -> Unit,
@@ -90,6 +90,7 @@ fun StationListScreen(
var stationForQuality by remember { mutableStateOf<Station?>(null) } var stationForQuality by remember { mutableStateOf<Station?>(null) }
var qualityStreams by remember { mutableStateOf<List<StationStream>>(emptyList()) } var qualityStreams by remember { mutableStateOf<List<StationStream>>(emptyList()) }
var qualityCurrentOrder by remember { mutableStateOf<List<String>>(emptyList()) } var qualityCurrentOrder by remember { mutableStateOf<List<String>>(emptyList()) }
var tabToRename by remember { mutableStateOf<TabInfo?>(null) }
val importLauncher = rememberLauncherForActivityResult( val importLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.OpenDocument(), contract = ActivityResultContracts.OpenDocument(),
@@ -167,17 +168,47 @@ fun StationListScreen(
edgePadding = 16.dp edgePadding = 16.dp
) { ) {
viewState.tabs.forEachIndexed { index, tab -> viewState.tabs.forEachIndexed { index, tab ->
Tab( var showTabMenu by remember { mutableStateOf(false) }
selected = viewState.selectedTabIndex == index, Box {
onClick = { viewModel.selectTab(index) }, Tab(
text = { selected = viewState.selectedTabIndex == index,
Text( onClick = { viewModel.selectTab(index) },
text = tab.label, text = {
maxLines = 1, Text(
overflow = TextOverflow.Ellipsis 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) } 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 @Composable

View File

@@ -136,10 +136,27 @@ class StationListViewModel(application: Application) : AndroidViewModel(applicat
private fun buildTabs(playlists: List<Playlist>): List<TabInfo> { private fun buildTabs(playlists: List<Playlist>): List<TabInfo> {
val myStations = TabInfo(playlist = null, label = "My Stations", isBuiltIn = false) val myStations = TabInfo(playlist = null, label = "My Stations", isBuiltIn = false)
val playlistTabs = playlists val pinned = playlists
.sortedWith(compareByDescending<Playlist> { it.isBuiltIn }.thenBy { it.sortOrder }) .filter { it.pinned }
.sortedBy { it.sortOrder }
.map { TabInfo(playlist = it, label = it.name, isBuiltIn = it.isBuiltIn) } .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( private fun applySortMode(