ux: home 'Import music' opens import dialog directly instead of navigating to Library

This commit is contained in:
dev 2026-09-07 11:26:36 -05:00
commit 153a132656
288 changed files with 136 additions and 56 deletions

View file

@ -70,6 +70,9 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.material3.TextButton
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@ -108,7 +111,44 @@ fun PlayerScreen(
) {
val uiState by viewModel.uiState.collectAsState()
val playlists by viewModel.playlists.collectAsState()
val importMessage by viewModel.importMessage.collectAsState()
val snackbarHostState = remember { SnackbarHostState() }
var showImportDialog by remember { mutableStateOf(false) }
val importSongsPicker = rememberLauncherForActivityResult(
ActivityResultContracts.OpenMultipleDocuments()
) { uris -> if (uris.isNotEmpty()) viewModel.importSongs(uris) }
val importFolderPicker = rememberLauncherForActivityResult(
ActivityResultContracts.OpenDocumentTree()
) { treeUri -> if (treeUri != null) viewModel.importSongs(listOf(treeUri)) }
LaunchedEffect(importMessage) {
importMessage?.let {
snackbarHostState.showSnackbar(it)
viewModel.clearImportMessage()
}
}
if (showImportDialog) {
AlertDialog(
onDismissRequest = { showImportDialog = false },
title = { Text("Import music") },
text = { Text("Add songs from your device to your library.") },
confirmButton = {
TextButton(onClick = {
showImportDialog = false
importSongsPicker.launch(arrayOf("audio/*"))
}) { Text("Choose songs…") }
},
dismissButton = {
TextButton(onClick = {
showImportDialog = false
importFolderPicker.launch(null)
}) { Text("Choose folder…") }
},
)
}
// Sync state when screen becomes visible (e.g. returning from Stations)
LaunchedEffect(Unit) {
@ -213,7 +253,7 @@ fun PlayerScreen(
} else {
EmptyStateView(
onBrowseStations = onNavigateToStations,
onImportMusic = onNavigateToLibrary,
onImportMusic = { showImportDialog = true },
modifier = Modifier.padding(padding),
)
}

View file

@ -4,6 +4,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.rada.data.local.entity.PlaylistEntity
import com.rada.data.repository.PlaylistRepository
import com.rada.data.repository.SongImporter
import com.rada.data.repository.StationRepository
import com.rada.domain.model.Station
import com.rada.player.PlaybackManager
@ -24,11 +25,36 @@ class PlayerViewModel @Inject constructor(
private val playbackManager: PlaybackManager,
private val stationRepository: StationRepository,
private val playlistRepository: PlaylistRepository,
private val songImporter: SongImporter,
) : ViewModel() {
private val _uiState = MutableStateFlow(PlayerUiState())
val uiState: StateFlow<PlayerUiState> = _uiState.asStateFlow()
private val _importMessage = MutableStateFlow<String?>(null)
val importMessage: StateFlow<String?> = _importMessage.asStateFlow()
/** Manual import from the player's empty state: copies picked files/folders in. */
fun importSongs(uris: List<android.net.Uri>) {
if (uris.isEmpty()) return
viewModelScope.launch {
val result = songImporter.import(uris)
_importMessage.value = buildString {
append("Imported ${result.imported} song")
if (result.imported != 1) append("s")
if (result.errors.isNotEmpty()) {
append(" (${result.errors.size} failed: ${result.errors.first()}")
if (result.errors.size > 1) append(", +${result.errors.size - 1} more")
append(")")
}
}
}
}
fun clearImportMessage() {
_importMessage.value = null
}
val playlists: StateFlow<List<PlaylistEntity>> = playlistRepository.getAllPlaylists()
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())