feat: add tab rename and pin/unpin via long-press context menu
Made-with: Cursor
This commit is contained in:
@@ -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") }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
Reference in New Issue
Block a user