Move Downloads out of Settings into a dynamic Library collection

Downloads is no longer a Settings sub-screen - it's now a pinned entry
at the top of Library > Playlists (Settings keeps only the Wi-Fi-only
and default-quality preference toggles, which govern how downloads
behave rather than being downloaded content itself).

The Downloads screen it opens is no longer a flat track list. It now
splits into two sections, both fully reactive to what's actually on
disk:

- Playlists: any playlist with at least one downloaded track. Tapping
  one opens the real PlaylistDetailScreen, not a special-cased view -
  this is a live filter over existing playlists, not a copy of their
  data.
- Tracks: downloaded tracks that aren't a member of any playlist. A
  track belonging to a playlist is never listed twice - that playlist
  already qualifies for the Playlists section by virtue of this same
  track being downloaded, so once it's shown there this list skips it.

New PlaylistDao.observeAllMemberships() (and
PlaylistRepository.observeTrackIdsByPlaylist()) exposes the full
playlist-to-track-id membership map in one reactive query, so working
out which playlists have a downloaded track doesn't require loading
every playlist's full track list. DownloadsViewModel combines that
with the existing downloaded-tracks and playlists flows to derive both
sections - no new database tables, this is a computed view over the
same Room state Phase 5 and Phase 6 already maintain.

Verified on-device: Settings no longer shows a Downloads row; the
Library tab shows the new pinned "Downloads" entry; added a
downloaded track to a test playlist and confirmed it moved from the
flat Tracks list into a Playlists entry with no duplication; confirmed
tapping that entry opens the actual playlist detail screen; deleted
the playlist and confirmed the track reactively reverted to the flat
Tracks list with no manual refresh.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
christopher
2026-09-17 19:23:03 -04:00
co-authored by Claude Sonnet 5
parent 53d4a73309
commit 49f941b8b9
10 changed files with 141 additions and 49 deletions
@@ -13,6 +13,10 @@ data class PlaylistWithCount(
val trackCount: Int, val trackCount: Int,
) )
/** One (playlist, track) membership row - the raw shape the Downloads collection needs to work
* out which playlists contain a downloaded track, without loading every playlist's full track list. */
data class PlaylistTrackMembership(val playlistLocalId: String, val trackId: String)
@Dao @Dao
interface PlaylistDao { interface PlaylistDao {
@Query( @Query(
@@ -51,6 +55,9 @@ interface PlaylistDao {
@Query("SELECT * FROM playlist_tracks WHERE playlistLocalId = :localId ORDER BY position ASC") @Query("SELECT * FROM playlist_tracks WHERE playlistLocalId = :localId ORDER BY position ASC")
fun observeTracks(localId: String): Flow<List<PlaylistTrackEntity>> fun observeTracks(localId: String): Flow<List<PlaylistTrackEntity>>
@Query("SELECT playlistLocalId, trackId FROM playlist_tracks")
fun observeAllMemberships(): Flow<List<PlaylistTrackMembership>>
@Query("SELECT * FROM playlist_tracks WHERE playlistLocalId = :localId ORDER BY position ASC") @Query("SELECT * FROM playlist_tracks WHERE playlistLocalId = :localId ORDER BY position ASC")
suspend fun getTracks(localId: String): List<PlaylistTrackEntity> suspend fun getTracks(localId: String): List<PlaylistTrackEntity>
@@ -62,6 +62,12 @@ class PlaylistRepository @Inject constructor(
fun observeTracks(localId: String): Flow<List<Song>> = fun observeTracks(localId: String): Flow<List<Song>> =
playlistDao.observeTracks(localId).map { list -> list.map { it.toDomain() } } playlistDao.observeTracks(localId).map { list -> list.map { it.toDomain() } }
/** playlistId -> the set of track ids it contains, for the Downloads collection to work out
* which playlists have a downloaded track without loading every playlist's full track list. */
fun observeTrackIdsByPlaylist(): Flow<Map<String, Set<String>>> =
playlistDao.observeAllMemberships()
.map { rows -> rows.groupBy({ it.playlistLocalId }, { it.trackId }).mapValues { it.value.toSet() } }
suspend fun createPlaylist(name: String, initialTracks: List<Song> = emptyList()): String { suspend fun createPlaylist(name: String, initialTracks: List<Song> = emptyList()): String {
val localId = UUID.randomUUID().toString() val localId = UUID.randomUUID().toString()
val now = System.currentTimeMillis() val now = System.currentTimeMillis()
@@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowBack
@@ -12,6 +13,7 @@ import androidx.compose.material.icons.filled.Delete
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
@@ -21,6 +23,7 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
@@ -28,6 +31,7 @@ import androidx.media3.common.util.UnstableApi
import com.InfernalAquatics.deepwave.R import com.InfernalAquatics.deepwave.R
import com.InfernalAquatics.deepwave.data.local.download.DownloadStatus import com.InfernalAquatics.deepwave.data.local.download.DownloadStatus
import com.InfernalAquatics.deepwave.data.local.download.DownloadedTrackEntity import com.InfernalAquatics.deepwave.data.local.download.DownloadedTrackEntity
import com.InfernalAquatics.deepwave.data.model.Playlist
import com.InfernalAquatics.deepwave.data.network.coverArtUrl import com.InfernalAquatics.deepwave.data.network.coverArtUrl
import com.InfernalAquatics.deepwave.ui.components.TrackRow import com.InfernalAquatics.deepwave.ui.components.TrackRow
import com.InfernalAquatics.deepwave.ui.theme.DeepwaveTheme import com.InfernalAquatics.deepwave.ui.theme.DeepwaveTheme
@@ -36,12 +40,14 @@ import com.InfernalAquatics.deepwave.ui.theme.DeepwaveTheme
@Composable @Composable
fun DownloadsScreen( fun DownloadsScreen(
onBack: () -> Unit, onBack: () -> Unit,
onPlaylistClick: (String) -> Unit,
viewModel: DownloadsViewModel = hiltViewModel(), viewModel: DownloadsViewModel = hiltViewModel(),
) { ) {
val uiState by viewModel.uiState.collectAsState() val uiState by viewModel.uiState.collectAsState()
DownloadsContent( DownloadsContent(
uiState = uiState, uiState = uiState,
onBack = onBack, onBack = onBack,
onPlaylistClick = onPlaylistClick,
onTrackClick = viewModel::playTrack, onTrackClick = viewModel::playTrack,
onRemove = viewModel::remove, onRemove = viewModel::remove,
) )
@@ -52,6 +58,7 @@ fun DownloadsScreen(
private fun DownloadsContent( private fun DownloadsContent(
uiState: DownloadsUiState, uiState: DownloadsUiState,
onBack: () -> Unit, onBack: () -> Unit,
onPlaylistClick: (String) -> Unit,
onTrackClick: (Int) -> Unit, onTrackClick: (Int) -> Unit,
onRemove: (String) -> Unit, onRemove: (String) -> Unit,
) { ) {
@@ -67,22 +74,29 @@ private fun DownloadsContent(
) )
}, },
) { innerPadding -> ) { innerPadding ->
if (uiState.tracks.isEmpty()) { if (uiState.downloadedPlaylists.isEmpty() && uiState.looseTracks.isEmpty()) {
Box( Box(
modifier = Modifier modifier = Modifier.fillMaxSize().padding(innerPadding),
.fillMaxSize()
.padding(innerPadding),
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,
) { ) {
Text(text = stringResource(R.string.downloads_empty), style = MaterialTheme.typography.bodyLarge) Text(text = stringResource(R.string.downloads_empty), style = MaterialTheme.typography.bodyLarge)
} }
} else { } else {
LazyColumn( LazyColumn(modifier = Modifier.fillMaxSize().padding(innerPadding)) {
modifier = Modifier if (uiState.downloadedPlaylists.isNotEmpty()) {
.fillMaxSize() item { SectionHeader(stringResource(R.string.downloads_section_playlists)) }
.padding(innerPadding), items(uiState.downloadedPlaylists, key = { "playlist:${it.id}" }) { playlist ->
) { TrackRow(
itemsIndexed(uiState.tracks, key = { _, track -> track.trackId }) { index, track -> title = playlist.name,
subtitle = pluralStringResource(R.plurals.playlist_track_count, playlist.trackCount, playlist.trackCount),
coverArtUrl = coverArtUrl(playlist.coverArtId),
onClick = { onPlaylistClick(playlist.id) },
)
}
}
if (uiState.looseTracks.isNotEmpty()) {
item { SectionHeader(stringResource(R.string.downloads_section_tracks)) }
itemsIndexed(uiState.looseTracks, key = { _, track -> "track:${track.trackId}" }) { index, track ->
TrackRow( TrackRow(
title = track.title, title = track.title,
subtitle = track.artistName.orEmpty(), subtitle = track.artistName.orEmpty(),
@@ -99,8 +113,17 @@ private fun DownloadsContent(
} }
} }
} }
}
private val sampleTracks = List(3) { index -> @Composable
private fun SectionHeader(title: String) {
ListItem(headlineContent = { Text(title, style = MaterialTheme.typography.titleMedium) })
}
private val samplePlaylists = listOf(
Playlist(id = "1", name = "Riddim Favs", coverArtId = null, trackCount = 12, isSynced = true),
)
private val sampleTracks = List(2) { index ->
DownloadedTrackEntity( DownloadedTrackEntity(
trackId = "sample-$index", trackId = "sample-$index",
title = "Sample Track ${index + 1}", title = "Sample Track ${index + 1}",
@@ -120,7 +143,13 @@ private val sampleTracks = List(3) { index ->
@Composable @Composable
private fun DownloadsContentPreview() { private fun DownloadsContentPreview() {
DeepwaveTheme { DeepwaveTheme {
DownloadsContent(uiState = DownloadsUiState(tracks = sampleTracks), onBack = {}, onTrackClick = {}, onRemove = {}) DownloadsContent(
uiState = DownloadsUiState(downloadedPlaylists = samplePlaylists, looseTracks = sampleTracks),
onBack = {},
onPlaylistClick = {},
onTrackClick = {},
onRemove = {},
)
} }
} }
@@ -128,6 +157,6 @@ private fun DownloadsContentPreview() {
@Composable @Composable
private fun DownloadsContentEmptyPreview() { private fun DownloadsContentEmptyPreview() {
DeepwaveTheme { DeepwaveTheme {
DownloadsContent(uiState = DownloadsUiState(), onBack = {}, onTrackClick = {}, onRemove = {}) DownloadsContent(uiState = DownloadsUiState(), onBack = {}, onPlaylistClick = {}, onTrackClick = {}, onRemove = {})
} }
} }
@@ -6,31 +6,56 @@ import androidx.media3.common.util.UnstableApi
import com.InfernalAquatics.deepwave.data.download.DownloadRepository import com.InfernalAquatics.deepwave.data.download.DownloadRepository
import com.InfernalAquatics.deepwave.data.local.download.DownloadStatus import com.InfernalAquatics.deepwave.data.local.download.DownloadStatus
import com.InfernalAquatics.deepwave.data.local.download.DownloadedTrackEntity import com.InfernalAquatics.deepwave.data.local.download.DownloadedTrackEntity
import com.InfernalAquatics.deepwave.data.model.Playlist
import com.InfernalAquatics.deepwave.data.playlist.PlaylistRepository
import com.InfernalAquatics.deepwave.media.PlaybackController import com.InfernalAquatics.deepwave.media.PlaybackController
import com.InfernalAquatics.deepwave.media.toMediaItem import com.InfernalAquatics.deepwave.media.toMediaItem
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
data class DownloadsUiState(val tracks: List<DownloadedTrackEntity> = emptyList()) /**
* [downloadedPlaylists]: any playlist with at least one downloaded track. [looseTracks]:
* downloaded tracks that aren't a member of any playlist - a track belonging to a playlist is
* never listed twice, since that playlist already qualifies for [downloadedPlaylists] by virtue
* of this same track being downloaded.
*/
data class DownloadsUiState(
val downloadedPlaylists: List<Playlist> = emptyList(),
val looseTracks: List<DownloadedTrackEntity> = emptyList(),
)
@UnstableApi @UnstableApi
@HiltViewModel @HiltViewModel
class DownloadsViewModel @Inject constructor( class DownloadsViewModel @Inject constructor(
private val downloadRepository: DownloadRepository, private val downloadRepository: DownloadRepository,
private val playlistRepository: PlaylistRepository,
private val playbackController: PlaybackController, private val playbackController: PlaybackController,
) : ViewModel() { ) : ViewModel() {
val uiState: StateFlow<DownloadsUiState> = downloadRepository.downloadedTracks() val uiState: StateFlow<DownloadsUiState> = combine(
.map { tracks -> DownloadsUiState(tracks.filter { it.status == DownloadStatus.COMPLETE }) } downloadRepository.downloadedTracks(),
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), DownloadsUiState()) playlistRepository.observePlaylists(),
playlistRepository.observeTrackIdsByPlaylist(),
) { tracks, playlists, trackIdsByPlaylist ->
val downloaded = tracks.filter { it.status == DownloadStatus.COMPLETE }
val downloadedIds = downloaded.map { it.trackId }.toSet()
val downloadedPlaylists = playlists.filter { playlist ->
trackIdsByPlaylist[playlist.id].orEmpty().any { it in downloadedIds }
}
val trackIdsInAnyPlaylist = trackIdsByPlaylist.values.flatten().toSet()
val looseTracks = downloaded.filter { it.trackId !in trackIdsInAnyPlaylist }
DownloadsUiState(downloadedPlaylists, looseTracks)
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), DownloadsUiState())
fun playTrack(index: Int) { fun playTrack(index: Int) {
val tracks = uiState.value.tracks val tracks = uiState.value.looseTracks
if (index !in tracks.indices) return if (index !in tracks.indices) return
viewModelScope.launch { viewModelScope.launch {
playbackController.ensureConnected() playbackController.ensureConnected()
@@ -31,6 +31,7 @@ private enum class LibrarySection(val labelRes: Int) {
fun LibraryScreen( fun LibraryScreen(
onArtistClick: (String) -> Unit, onArtistClick: (String) -> Unit,
onPlaylistClick: (String) -> Unit, onPlaylistClick: (String) -> Unit,
onOpenDownloads: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
var section by remember { mutableStateOf(LibrarySection.Playlists) } var section by remember { mutableStateOf(LibrarySection.Playlists) }
@@ -47,7 +48,11 @@ fun LibraryScreen(
} }
} }
when (section) { when (section) {
LibrarySection.Playlists -> PlaylistsScreen(onPlaylistClick = onPlaylistClick, modifier = Modifier.weight(1f)) LibrarySection.Playlists -> PlaylistsScreen(
onPlaylistClick = onPlaylistClick,
onOpenDownloads = onOpenDownloads,
modifier = Modifier.weight(1f),
)
LibrarySection.Artists -> ArtistsScreen(onArtistClick = onArtistClick, modifier = Modifier.weight(1f)) LibrarySection.Artists -> ArtistsScreen(onArtistClick = onArtistClick, modifier = Modifier.weight(1f))
} }
} }
@@ -57,6 +62,6 @@ fun LibraryScreen(
@Composable @Composable
private fun LibraryScreenPreview() { private fun LibraryScreenPreview() {
DeepwaveTheme { DeepwaveTheme {
LibraryScreen(onArtistClick = {}, onPlaylistClick = {}) LibraryScreen(onArtistClick = {}, onPlaylistClick = {}, onOpenDownloads = {})
} }
} }
@@ -95,6 +95,7 @@ fun DeepwaveNavHost(
onOpenArtist = { artistId -> navController.navigate(Route.ArtistDetail(artistId)) }, onOpenArtist = { artistId -> navController.navigate(Route.ArtistDetail(artistId)) },
onOpenAlbum = { albumId -> navController.navigate(Route.AlbumDetail(albumId)) }, onOpenAlbum = { albumId -> navController.navigate(Route.AlbumDetail(albumId)) },
onOpenPlaylist = { playlistId -> navController.navigate(Route.PlaylistDetail(playlistId)) }, onOpenPlaylist = { playlistId -> navController.navigate(Route.PlaylistDetail(playlistId)) },
onOpenDownloads = { navController.navigate(Route.Downloads) },
) )
} }
composable<Route.Settings> { composable<Route.Settings> {
@@ -105,11 +106,13 @@ fun DeepwaveNavHost(
popUpTo<Route.Main> { inclusive = true } popUpTo<Route.Main> { inclusive = true }
} }
}, },
onOpenDownloads = { navController.navigate(Route.Downloads) },
) )
} }
composable<Route.Downloads> { composable<Route.Downloads> {
DownloadsScreen(onBack = { navController.popBackStack() }) DownloadsScreen(
onBack = { navController.popBackStack() },
onPlaylistClick = { playlistId -> navController.navigate(Route.PlaylistDetail(playlistId)) },
)
} }
composable<Route.ArtistDetail> { composable<Route.ArtistDetail> {
ArtistDetailScreen( ArtistDetailScreen(
@@ -41,6 +41,7 @@ fun MainScreen(
onOpenArtist: (String) -> Unit, onOpenArtist: (String) -> Unit,
onOpenAlbum: (String) -> Unit, onOpenAlbum: (String) -> Unit,
onOpenPlaylist: (String) -> Unit, onOpenPlaylist: (String) -> Unit,
onOpenDownloads: () -> Unit,
) { ) {
var selectedTab by remember { mutableStateOf(BottomTab.Home) } var selectedTab by remember { mutableStateOf(BottomTab.Home) }
@@ -67,7 +68,11 @@ fun MainScreen(
when (selectedTab) { when (selectedTab) {
BottomTab.Home -> HomeScreen(onAlbumClick = onOpenAlbum) BottomTab.Home -> HomeScreen(onAlbumClick = onOpenAlbum)
BottomTab.Search -> SearchScreen(onArtistClick = onOpenArtist, onAlbumClick = onOpenAlbum) BottomTab.Search -> SearchScreen(onArtistClick = onOpenArtist, onAlbumClick = onOpenAlbum)
BottomTab.Library -> LibraryScreen(onArtistClick = onOpenArtist, onPlaylistClick = onOpenPlaylist) BottomTab.Library -> LibraryScreen(
onArtistClick = onOpenArtist,
onPlaylistClick = onOpenPlaylist,
onOpenDownloads = onOpenDownloads,
)
} }
} }
} }
@@ -78,6 +83,6 @@ fun MainScreen(
@Composable @Composable
private fun MainScreenPreview() { private fun MainScreenPreview() {
DeepwaveTheme { DeepwaveTheme {
MainScreen(onOpenSettings = {}, onOpenArtist = {}, onOpenAlbum = {}, onOpenPlaylist = {}) MainScreen(onOpenSettings = {}, onOpenArtist = {}, onOpenAlbum = {}, onOpenPlaylist = {}, onOpenDownloads = {})
} }
} }
@@ -7,6 +7,7 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Download
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem import androidx.compose.material3.ListItem
import androidx.compose.material3.Text import androidx.compose.material3.Text
@@ -27,6 +28,7 @@ import com.InfernalAquatics.deepwave.ui.theme.DeepwaveTheme
@Composable @Composable
fun PlaylistsScreen( fun PlaylistsScreen(
onPlaylistClick: (String) -> Unit, onPlaylistClick: (String) -> Unit,
onOpenDownloads: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
viewModel: PlaylistsViewModel = hiltViewModel(), viewModel: PlaylistsViewModel = hiltViewModel(),
) { ) {
@@ -34,6 +36,7 @@ fun PlaylistsScreen(
PlaylistsContent( PlaylistsContent(
uiState = uiState, uiState = uiState,
onPlaylistClick = onPlaylistClick, onPlaylistClick = onPlaylistClick,
onOpenDownloads = onOpenDownloads,
onNewPlaylist = viewModel::openCreate, onNewPlaylist = viewModel::openCreate,
modifier = modifier, modifier = modifier,
) )
@@ -47,6 +50,7 @@ fun PlaylistsScreen(
private fun PlaylistsContent( private fun PlaylistsContent(
uiState: PlaylistsUiState, uiState: PlaylistsUiState,
onPlaylistClick: (String) -> Unit, onPlaylistClick: (String) -> Unit,
onOpenDownloads: () -> Unit,
onNewPlaylist: () -> Unit, onNewPlaylist: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
@@ -58,6 +62,17 @@ private fun PlaylistsContent(
modifier = Modifier.clickable(onClick = onNewPlaylist), modifier = Modifier.clickable(onClick = onNewPlaylist),
) )
} }
item {
// Pinned, not a real Room playlist: its membership is computed live from what's
// actually on disk (see DownloadsViewModel), so it never drifts from reality and
// never syncs to the server - downloads are inherently per-device.
ListItem(
headlineContent = { Text(stringResource(R.string.downloads_title)) },
supportingContent = { Text(stringResource(R.string.downloads_subtitle)) },
leadingContent = { Icon(Icons.Filled.Download, contentDescription = null) },
modifier = Modifier.clickable(onClick = onOpenDownloads),
)
}
if (uiState.playlists.isEmpty()) { if (uiState.playlists.isEmpty()) {
item { item {
ListItem(headlineContent = { Text(stringResource(R.string.playlist_empty)) }) ListItem(headlineContent = { Text(stringResource(R.string.playlist_empty)) })
@@ -84,7 +99,12 @@ private val samplePlaylists = listOf(
@Composable @Composable
private fun PlaylistsContentPreview() { private fun PlaylistsContentPreview() {
DeepwaveTheme { DeepwaveTheme {
PlaylistsContent(uiState = PlaylistsUiState(playlists = samplePlaylists), onPlaylistClick = {}, onNewPlaylist = {}) PlaylistsContent(
uiState = PlaylistsUiState(playlists = samplePlaylists),
onPlaylistClick = {},
onOpenDownloads = {},
onNewPlaylist = {},
)
} }
} }
@@ -92,6 +112,6 @@ private fun PlaylistsContentPreview() {
@Composable @Composable
private fun PlaylistsContentEmptyPreview() { private fun PlaylistsContentEmptyPreview() {
DeepwaveTheme { DeepwaveTheme {
PlaylistsContent(uiState = PlaylistsUiState(), onPlaylistClick = {}, onNewPlaylist = {}) PlaylistsContent(uiState = PlaylistsUiState(), onPlaylistClick = {}, onOpenDownloads = {}, onNewPlaylist = {})
} }
} }
@@ -34,7 +34,6 @@ import com.InfernalAquatics.deepwave.ui.theme.DeepwaveTheme
fun SettingsScreen( fun SettingsScreen(
onBack: () -> Unit, onBack: () -> Unit,
onLoggedOut: () -> Unit, onLoggedOut: () -> Unit,
onOpenDownloads: () -> Unit,
viewModel: SettingsViewModel = hiltViewModel(), viewModel: SettingsViewModel = hiltViewModel(),
) { ) {
val uiState by viewModel.uiState.collectAsState() val uiState by viewModel.uiState.collectAsState()
@@ -46,7 +45,6 @@ fun SettingsScreen(
defaultBitrate = defaultBitrate, defaultBitrate = defaultBitrate,
onBack = onBack, onBack = onBack,
onLogOutClick = { viewModel.logout(onLoggedOut) }, onLogOutClick = { viewModel.logout(onLoggedOut) },
onOpenDownloads = onOpenDownloads,
onWifiOnlyChange = viewModel::setWifiOnly, onWifiOnlyChange = viewModel::setWifiOnly,
onOpenDefaultQualityPicker = viewModel::openDefaultQualityPicker, onOpenDefaultQualityPicker = viewModel::openDefaultQualityPicker,
onDismissDefaultQualityPicker = viewModel::dismissDefaultQualityPicker, onDismissDefaultQualityPicker = viewModel::dismissDefaultQualityPicker,
@@ -62,7 +60,6 @@ private fun SettingsContent(
defaultBitrate: BitrateOption, defaultBitrate: BitrateOption,
onBack: () -> Unit, onBack: () -> Unit,
onLogOutClick: () -> Unit, onLogOutClick: () -> Unit,
onOpenDownloads: () -> Unit,
onWifiOnlyChange: (Boolean) -> Unit, onWifiOnlyChange: (Boolean) -> Unit,
onOpenDefaultQualityPicker: () -> Unit, onOpenDefaultQualityPicker: () -> Unit,
onDismissDefaultQualityPicker: () -> Unit, onDismissDefaultQualityPicker: () -> Unit,
@@ -81,12 +78,6 @@ private fun SettingsContent(
}, },
) { innerPadding -> ) { innerPadding ->
Column(modifier = Modifier.fillMaxSize().padding(innerPadding)) { Column(modifier = Modifier.fillMaxSize().padding(innerPadding)) {
ListItem(
headlineContent = { Text(stringResource(R.string.settings_downloads)) },
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onOpenDownloads),
)
ListItem( ListItem(
headlineContent = { Text(stringResource(R.string.settings_wifi_only_downloads)) }, headlineContent = { Text(stringResource(R.string.settings_wifi_only_downloads)) },
trailingContent = { Switch(checked = wifiOnly, onCheckedChange = onWifiOnlyChange) }, trailingContent = { Switch(checked = wifiOnly, onCheckedChange = onWifiOnlyChange) },
@@ -124,7 +115,6 @@ private fun SettingsContentPreview() {
defaultBitrate = BitrateOption.Normal, defaultBitrate = BitrateOption.Normal,
onBack = {}, onBack = {},
onLogOutClick = {}, onLogOutClick = {},
onOpenDownloads = {},
onWifiOnlyChange = {}, onWifiOnlyChange = {},
onOpenDefaultQualityPicker = {}, onOpenDefaultQualityPicker = {},
onDismissDefaultQualityPicker = {}, onDismissDefaultQualityPicker = {},
+3 -1
View File
@@ -17,7 +17,6 @@
<string name="settings_title">Settings</string> <string name="settings_title">Settings</string>
<string name="settings_log_out">Log out</string> <string name="settings_log_out">Log out</string>
<string name="settings_downloads">Downloads</string>
<string name="settings_wifi_only_downloads">Download over Wi-Fi only</string> <string name="settings_wifi_only_downloads">Download over Wi-Fi only</string>
<string name="settings_default_quality">Default download quality</string> <string name="settings_default_quality">Default download quality</string>
@@ -33,6 +32,9 @@
<string name="downloads_title">Downloads</string> <string name="downloads_title">Downloads</string>
<string name="downloads_empty">No downloads yet</string> <string name="downloads_empty">No downloads yet</string>
<string name="downloads_subtitle">Available offline</string>
<string name="downloads_section_playlists">Playlists</string>
<string name="downloads_section_tracks">Tracks</string>
<string name="player_nothing_playing">Nothing playing</string> <string name="player_nothing_playing">Nothing playing</string>
<string name="player_play">Play</string> <string name="player_play">Play</string>