mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-11 07:29:14 +01:00
add star/unstar and long-press item menus to the library lists
Star is a new write path: MusicSource.setStar posts star.view/unstar.view and LibraryRepository updates the local row on success (toast on failure). Long-press an album, artist, song or playlist in the library to open a bottom sheet with play/shuffle, star/unstar and view-album/view-artist.
This commit is contained in:
@@ -62,6 +62,27 @@ interface LibraryDao {
|
||||
@Upsert
|
||||
suspend fun upsertPlaylistSongs(items: List<PlaylistSong>)
|
||||
|
||||
@Query("UPDATE artists SET starred = :starred WHERE sourceId = :sourceId AND id = :id")
|
||||
suspend fun setArtistStar(
|
||||
sourceId: Long,
|
||||
id: String,
|
||||
starred: Long?,
|
||||
)
|
||||
|
||||
@Query("UPDATE albums SET starred = :starred WHERE sourceId = :sourceId AND id = :id")
|
||||
suspend fun setAlbumStar(
|
||||
sourceId: Long,
|
||||
id: String,
|
||||
starred: Long?,
|
||||
)
|
||||
|
||||
@Query("UPDATE songs SET starred = :starred WHERE sourceId = :sourceId AND id = :id")
|
||||
suspend fun setSongStar(
|
||||
sourceId: Long,
|
||||
id: String,
|
||||
starred: Long?,
|
||||
)
|
||||
|
||||
@Query("SELECT id FROM artists WHERE sourceId = :sourceId")
|
||||
suspend fun artistIds(sourceId: Long): List<String>
|
||||
|
||||
|
||||
@@ -16,11 +16,13 @@ import com.subtracks.data.prefs.ArtistSort
|
||||
import com.subtracks.data.prefs.PlaylistSort
|
||||
import com.subtracks.data.prefs.SongSort
|
||||
import com.subtracks.data.prefs.StarredFilter
|
||||
import com.subtracks.data.source.StarType
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
class LibraryRepository(
|
||||
private val db: SubtracksDatabase,
|
||||
private val sourceRepository: SourceRepository,
|
||||
private val showMessage: (String) -> Unit = {},
|
||||
) {
|
||||
val activeSourceId: Flow<Long?> = sourceRepository.activeSourceId()
|
||||
|
||||
@@ -234,6 +236,33 @@ class LibraryRepository(
|
||||
playlistId: String,
|
||||
): Flow<PagingData<SongListItem>> = pager(60) { db.libraryDao().playlistSongs(sourceId, playlistId) }
|
||||
|
||||
suspend fun setStar(
|
||||
type: StarType,
|
||||
id: String,
|
||||
starred: Boolean,
|
||||
): Result<Unit> {
|
||||
val sourceId = sourceRepository.activeSourceIdOnce()
|
||||
val source = sourceRepository.activeMusicSource()
|
||||
if (sourceId == null || source == null) return Result.failure(IllegalStateException("No active server"))
|
||||
return runCatching { source.setStar(type, id, starred) }
|
||||
.onSuccess { updateStarred(sourceId, type, id, if (starred) System.currentTimeMillis() else null) }
|
||||
.onFailure { showMessage("Could not update star") }
|
||||
}
|
||||
|
||||
private suspend fun updateStarred(
|
||||
sourceId: Long,
|
||||
type: StarType,
|
||||
id: String,
|
||||
starred: Long?,
|
||||
) {
|
||||
val dao = db.libraryDao()
|
||||
when (type) {
|
||||
StarType.Song -> dao.setSongStar(sourceId, id, starred)
|
||||
StarType.Album -> dao.setAlbumStar(sourceId, id, starred)
|
||||
StarType.Artist -> dao.setArtistStar(sourceId, id, starred)
|
||||
}
|
||||
}
|
||||
|
||||
private fun <T : Any> pager(
|
||||
pageSize: Int,
|
||||
source: () -> PagingSource<Int, T>,
|
||||
|
||||
@@ -12,6 +12,12 @@ interface MusicSource {
|
||||
|
||||
suspend fun ping()
|
||||
|
||||
suspend fun setStar(
|
||||
type: StarType,
|
||||
id: String,
|
||||
starred: Boolean,
|
||||
)
|
||||
|
||||
fun artists(): Flow<List<Artist>>
|
||||
|
||||
fun albums(): Flow<List<Album>>
|
||||
@@ -22,3 +28,9 @@ interface MusicSource {
|
||||
|
||||
fun playlistSongs(playlistIds: List<String>): Flow<List<PlaylistSong>>
|
||||
}
|
||||
|
||||
enum class StarType {
|
||||
Song,
|
||||
Album,
|
||||
Artist,
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.subtracks.data.model.Playlist
|
||||
import com.subtracks.data.model.PlaylistSong
|
||||
import com.subtracks.data.model.Song
|
||||
import com.subtracks.data.source.MusicSource
|
||||
import com.subtracks.data.source.StarType
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
@@ -40,6 +41,22 @@ class SubsonicSource(
|
||||
withContext(Dispatchers.IO) { client.check("ping") }
|
||||
}
|
||||
|
||||
override suspend fun setStar(
|
||||
type: StarType,
|
||||
id: String,
|
||||
starred: Boolean,
|
||||
) {
|
||||
val param =
|
||||
when (type) {
|
||||
StarType.Song -> "id"
|
||||
StarType.Album -> "albumId"
|
||||
StarType.Artist -> "artistId"
|
||||
}
|
||||
withContext(Dispatchers.IO) {
|
||||
client.check(if (starred) "star" else "unstar", mapOf(param to id))
|
||||
}
|
||||
}
|
||||
|
||||
override fun artists(): Flow<List<Artist>> =
|
||||
entityBatches(
|
||||
method = "getArtists",
|
||||
|
||||
@@ -32,20 +32,14 @@ fun appModule(
|
||||
http: OkHttpClient,
|
||||
) = module {
|
||||
val mainHandler = Handler(Looper.getMainLooper())
|
||||
val toast: (String) -> Unit = { message ->
|
||||
mainHandler.post { Toast.makeText(context, message, Toast.LENGTH_SHORT).show() }
|
||||
}
|
||||
single { createAndroidDatabase(context) }
|
||||
single { http }
|
||||
single { createUserPreferences(context) }
|
||||
single {
|
||||
SourceRepository(
|
||||
get(),
|
||||
get(),
|
||||
get(),
|
||||
showMessage = { message ->
|
||||
mainHandler.post { Toast.makeText(context, message, Toast.LENGTH_SHORT).show() }
|
||||
},
|
||||
)
|
||||
}
|
||||
single { LibraryRepository(get(), get()) }
|
||||
single { SourceRepository(get(), get(), get(), showMessage = toast) }
|
||||
single { LibraryRepository(get(), get(), toast) }
|
||||
single<ArtworkSeedStore> { ArtworkSeedRepository(get()) }
|
||||
single { SyncManager(get(), get(), get()) }
|
||||
single { QueueRepository(get()) }
|
||||
|
||||
@@ -220,6 +220,8 @@ private fun MainNavigation() {
|
||||
onArtistClick = { artist -> navController.navigate(Routes.artist(artist.id, artist.coverArt)) },
|
||||
onPlaylistClick = { playlist -> navController.navigate(Routes.playlist(playlist.id)) },
|
||||
onOpenSettings = { navController.navigate(Routes.SETTINGS) },
|
||||
onViewAlbum = { albumId -> navController.navigate(Routes.album(albumId)) },
|
||||
onViewArtist = { artistId -> navController.navigate(Routes.artist(artistId)) },
|
||||
bottomInset = bottomInset,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
package com.subtracks.ui.components
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.Album
|
||||
import androidx.compose.material.icons.rounded.Person
|
||||
import androidx.compose.material.icons.rounded.PlayArrow
|
||||
import androidx.compose.material.icons.rounded.Shuffle
|
||||
import androidx.compose.material.icons.rounded.Star
|
||||
import androidx.compose.material.icons.rounded.StarBorder
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.ListItemDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import com.subtracks.data.source.StarType
|
||||
import com.subtracks.data.model.Album as AlbumModel
|
||||
import com.subtracks.data.model.Artist as ArtistModel
|
||||
import com.subtracks.data.model.Playlist as PlaylistModel
|
||||
import com.subtracks.data.model.Song as SongModel
|
||||
|
||||
sealed interface MenuTarget {
|
||||
val title: String
|
||||
val subtitle: String?
|
||||
|
||||
data class Song(
|
||||
val song: SongModel,
|
||||
) : MenuTarget {
|
||||
override val title: String get() = song.title
|
||||
|
||||
override val subtitle: String? get() = song.artist
|
||||
}
|
||||
|
||||
data class Album(
|
||||
val album: AlbumModel,
|
||||
) : MenuTarget {
|
||||
override val title: String get() = album.name
|
||||
|
||||
override val subtitle: String? get() = album.albumArtist
|
||||
}
|
||||
|
||||
data class Artist(
|
||||
val artist: ArtistModel,
|
||||
) : MenuTarget {
|
||||
override val title: String get() = artist.name
|
||||
|
||||
override val subtitle: String? get() = null
|
||||
}
|
||||
|
||||
data class Playlist(
|
||||
val playlist: PlaylistModel,
|
||||
) : MenuTarget {
|
||||
override val title: String get() = playlist.name
|
||||
|
||||
override val subtitle: String? get() = null
|
||||
}
|
||||
}
|
||||
|
||||
class ItemActions(
|
||||
val playSong: (SongModel) -> Unit = {},
|
||||
val playAlbum: (AlbumModel) -> Unit = {},
|
||||
val shuffleAlbum: (AlbumModel) -> Unit = {},
|
||||
val playPlaylist: (PlaylistModel) -> Unit = {},
|
||||
val shufflePlaylist: (PlaylistModel) -> Unit = {},
|
||||
val setStar: (StarType, String, Boolean) -> Unit = { _, _, _ -> },
|
||||
val viewAlbum: (String) -> Unit = {},
|
||||
val viewArtist: (String) -> Unit = {},
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ItemContextMenu(
|
||||
target: MenuTarget,
|
||||
actions: ItemActions,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
ModalBottomSheet(onDismissRequest = onDismiss) {
|
||||
val dismissThen: (() -> Unit) -> Unit = { action ->
|
||||
onDismiss()
|
||||
action()
|
||||
}
|
||||
MenuHeader(target)
|
||||
when (target) {
|
||||
is MenuTarget.Song -> {
|
||||
MenuItem(Icons.Rounded.PlayArrow, "Play") { dismissThen { actions.playSong(target.song) } }
|
||||
MenuItem(starIcon(target.song.starred), starLabel(target.song.starred)) {
|
||||
dismissThen { actions.setStar(StarType.Song, target.song.id, target.song.starred == null) }
|
||||
}
|
||||
target.song.albumId?.let { albumId ->
|
||||
MenuItem(Icons.Rounded.Album, "View album") { dismissThen { actions.viewAlbum(albumId) } }
|
||||
}
|
||||
target.song.artistId?.let { artistId ->
|
||||
MenuItem(Icons.Rounded.Person, "View artist") { dismissThen { actions.viewArtist(artistId) } }
|
||||
}
|
||||
}
|
||||
|
||||
is MenuTarget.Album -> {
|
||||
MenuItem(Icons.Rounded.PlayArrow, "Play") { dismissThen { actions.playAlbum(target.album) } }
|
||||
MenuItem(Icons.Rounded.Shuffle, "Shuffle") { dismissThen { actions.shuffleAlbum(target.album) } }
|
||||
MenuItem(starIcon(target.album.starred), starLabel(target.album.starred)) {
|
||||
dismissThen { actions.setStar(StarType.Album, target.album.id, target.album.starred == null) }
|
||||
}
|
||||
target.album.artistId?.let { artistId ->
|
||||
MenuItem(Icons.Rounded.Person, "View artist") { dismissThen { actions.viewArtist(artistId) } }
|
||||
}
|
||||
}
|
||||
|
||||
is MenuTarget.Artist -> {
|
||||
MenuItem(starIcon(target.artist.starred), starLabel(target.artist.starred)) {
|
||||
dismissThen { actions.setStar(StarType.Artist, target.artist.id, target.artist.starred == null) }
|
||||
}
|
||||
}
|
||||
|
||||
is MenuTarget.Playlist -> {
|
||||
MenuItem(Icons.Rounded.PlayArrow, "Play") { dismissThen { actions.playPlaylist(target.playlist) } }
|
||||
MenuItem(Icons.Rounded.Shuffle, "Shuffle") { dismissThen { actions.shufflePlaylist(target.playlist) } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MenuHeader(target: MenuTarget) {
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Text(
|
||||
text = target.title,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
},
|
||||
supportingContent =
|
||||
target.subtitle?.takeIf { it.isNotBlank() }?.let { subtitle ->
|
||||
{
|
||||
Text(
|
||||
text = subtitle,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
},
|
||||
colors = ListItemDefaults.colors(containerColor = Color.Transparent),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MenuItem(
|
||||
icon: ImageVector,
|
||||
label: String,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
ListItem(
|
||||
headlineContent = { Text(label) },
|
||||
leadingContent = { Icon(icon, contentDescription = null) },
|
||||
colors = ListItemDefaults.colors(containerColor = Color.Transparent),
|
||||
modifier = Modifier.clickable(onClick = onClick),
|
||||
)
|
||||
}
|
||||
|
||||
private fun starIcon(starred: Long?): ImageVector = if (starred == null) Icons.Rounded.StarBorder else Icons.Rounded.Star
|
||||
|
||||
private fun starLabel(starred: Long?): String = if (starred == null) "Star" else "Unstar"
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.subtracks.ui.library
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
@@ -28,6 +28,7 @@ import com.subtracks.ui.components.CoverArt
|
||||
import com.subtracks.ui.components.EmptyState
|
||||
import com.subtracks.ui.components.FilteredEmptyState
|
||||
import com.subtracks.ui.components.LoadingState
|
||||
import com.subtracks.ui.components.MenuTarget
|
||||
import com.subtracks.ui.components.ResetScrollOnChange
|
||||
import com.subtracks.ui.components.rememberViewportFill
|
||||
import com.subtracks.ui.theme.PrefetchArtworkSeeds
|
||||
@@ -40,6 +41,7 @@ fun AlbumsContent(
|
||||
coverArt: (String?, Boolean) -> CoverArtRef?,
|
||||
bottomInset: Dp,
|
||||
onAlbumClick: (Album) -> Unit,
|
||||
onLongClick: (MenuTarget) -> Unit = {},
|
||||
filtered: Boolean = false,
|
||||
onClearFilters: () -> Unit = {},
|
||||
resetKey: Any? = null,
|
||||
@@ -90,8 +92,10 @@ fun AlbumsContent(
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(1f)
|
||||
.clip(RoundedCornerShape(2.dp))
|
||||
.clickable { onAlbumClick(album) }
|
||||
.testTag(ALBUM_COVER_TAG),
|
||||
.combinedClickable(
|
||||
onClick = { onAlbumClick(album) },
|
||||
onLongClick = { onLongClick(MenuTarget.Album(album)) },
|
||||
).testTag(ALBUM_COVER_TAG),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.subtracks.ui.library
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
@@ -28,6 +28,7 @@ import com.subtracks.ui.components.CoverArt
|
||||
import com.subtracks.ui.components.EmptyState
|
||||
import com.subtracks.ui.components.FilteredEmptyState
|
||||
import com.subtracks.ui.components.LoadingState
|
||||
import com.subtracks.ui.components.MenuTarget
|
||||
import com.subtracks.ui.components.ResetScrollOnChange
|
||||
import com.subtracks.ui.components.rememberViewportFill
|
||||
import com.subtracks.ui.theme.PrefetchArtworkSeeds
|
||||
@@ -38,6 +39,7 @@ fun ArtistsContent(
|
||||
coverArt: (String?, Boolean) -> CoverArtRef?,
|
||||
bottomInset: Dp,
|
||||
onArtistClick: (Artist) -> Unit,
|
||||
onLongClick: (MenuTarget) -> Unit = {},
|
||||
filtered: Boolean = false,
|
||||
onClearFilters: () -> Unit = {},
|
||||
resetKey: Any? = null,
|
||||
@@ -93,7 +95,11 @@ fun ArtistsContent(
|
||||
modifier = Modifier.size(48.dp).clip(CircleShape),
|
||||
)
|
||||
},
|
||||
modifier = Modifier.clickable { onArtistClick(artist) },
|
||||
modifier =
|
||||
Modifier.combinedClickable(
|
||||
onClick = { onArtistClick(artist) },
|
||||
onLongClick = { onLongClick(MenuTarget.Artist(artist)) },
|
||||
),
|
||||
colors = ListItemDefaults.colors(containerColor = Color.Transparent),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -114,6 +114,9 @@ import com.subtracks.data.prefs.PlaylistSort
|
||||
import com.subtracks.data.prefs.SongSort
|
||||
import com.subtracks.data.prefs.StarredFilter
|
||||
import com.subtracks.playback.PlaybackController
|
||||
import com.subtracks.ui.components.ItemActions
|
||||
import com.subtracks.ui.components.ItemContextMenu
|
||||
import com.subtracks.ui.components.MenuTarget
|
||||
import com.subtracks.ui.components.statusBarScrim
|
||||
import com.subtracks.ui.theme.ArtworkColors
|
||||
import com.subtracks.ui.theme.librarySurfaceColor
|
||||
@@ -189,12 +192,15 @@ fun LibraryRoute(
|
||||
onArtistClick: (Artist) -> Unit,
|
||||
onPlaylistClick: (Playlist) -> Unit,
|
||||
onOpenSettings: () -> Unit,
|
||||
onViewAlbum: (String) -> Unit,
|
||||
onViewArtist: (String) -> Unit,
|
||||
bottomInset: Dp,
|
||||
viewModel: LibraryViewModel = koinViewModel(),
|
||||
playbackController: PlaybackController = koinInject(),
|
||||
) {
|
||||
var selectedTab by rememberSaveable { mutableStateOf(LibraryTab.Albums) }
|
||||
var previousTab by rememberSaveable { mutableStateOf(LibraryTab.Albums) }
|
||||
var menuTarget by remember { mutableStateOf<MenuTarget?>(null) }
|
||||
val syncing by viewModel.syncing.collectAsStateWithLifecycle()
|
||||
val playingSongId by viewModel.playingSongId.collectAsStateWithLifecycle()
|
||||
val listTab = selectedTab.listTab()
|
||||
@@ -231,6 +237,7 @@ fun LibraryRoute(
|
||||
onSongClick = viewModel::playSong,
|
||||
onSync = viewModel::sync,
|
||||
onOpenSettings = onOpenSettings,
|
||||
onItemLongClick = { menuTarget = it },
|
||||
playingSongId = playingSongId,
|
||||
listQuery = listQuery,
|
||||
resetKeys = resetKeys,
|
||||
@@ -254,6 +261,23 @@ fun LibraryRoute(
|
||||
search = search,
|
||||
onSearchChange = { viewModel.setSearch(listTab, it) },
|
||||
)
|
||||
menuTarget?.let { target ->
|
||||
ItemContextMenu(
|
||||
target = target,
|
||||
actions =
|
||||
ItemActions(
|
||||
playSong = { viewModel.playSong(it.id) },
|
||||
playAlbum = { viewModel.playAlbum(it.id) },
|
||||
shuffleAlbum = { viewModel.shuffleAlbum(it.id) },
|
||||
playPlaylist = { viewModel.playPlaylist(it.id) },
|
||||
shufflePlaylist = { viewModel.shufflePlaylist(it.id) },
|
||||
setStar = viewModel::setStar,
|
||||
viewAlbum = onViewAlbum,
|
||||
viewArtist = onViewArtist,
|
||||
),
|
||||
onDismiss = { menuTarget = null },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@@ -272,6 +296,7 @@ fun LibraryScreen(
|
||||
onSongClick: (String) -> Unit,
|
||||
onSync: () -> Unit,
|
||||
onOpenSettings: () -> Unit,
|
||||
onItemLongClick: (MenuTarget) -> Unit = {},
|
||||
syncing: Boolean = false,
|
||||
playingSongId: String? = null,
|
||||
bottomInset: Dp = 0.dp,
|
||||
@@ -364,6 +389,7 @@ fun LibraryScreen(
|
||||
dismissSearch()
|
||||
onAlbumClick(it)
|
||||
},
|
||||
onLongClick = onItemLongClick,
|
||||
filtered = filtersActive,
|
||||
onClearFilters = onClearFilters,
|
||||
resetKey = resetKey,
|
||||
@@ -381,6 +407,7 @@ fun LibraryScreen(
|
||||
dismissSearch()
|
||||
onArtistClick(it)
|
||||
},
|
||||
onLongClick = onItemLongClick,
|
||||
filtered = filtersActive,
|
||||
onClearFilters = onClearFilters,
|
||||
resetKey = resetKey,
|
||||
@@ -398,6 +425,7 @@ fun LibraryScreen(
|
||||
dismissSearch()
|
||||
onSongClick(it)
|
||||
},
|
||||
onLongClick = onItemLongClick,
|
||||
playingSongId,
|
||||
filtered = filtersActive,
|
||||
onClearFilters = onClearFilters,
|
||||
@@ -416,6 +444,7 @@ fun LibraryScreen(
|
||||
dismissSearch()
|
||||
onPlaylistClick(it)
|
||||
},
|
||||
onLongClick = onItemLongClick,
|
||||
filtered = filtersActive,
|
||||
onClearFilters = onClearFilters,
|
||||
resetKey = resetKey,
|
||||
|
||||
@@ -20,6 +20,7 @@ import com.subtracks.data.prefs.UserPreferences
|
||||
import com.subtracks.data.repo.LibraryRepository
|
||||
import com.subtracks.data.repo.QueueSongQuery
|
||||
import com.subtracks.data.repo.SourceRepository
|
||||
import com.subtracks.data.source.StarType
|
||||
import com.subtracks.data.sync.SyncManager
|
||||
import com.subtracks.data.sync.SyncStatus
|
||||
import com.subtracks.playback.PlaybackController
|
||||
@@ -138,6 +139,38 @@ class LibraryViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
fun playAlbum(albumId: String) {
|
||||
viewModelScope.launch {
|
||||
sourceRepository.activeSourceIdOnce()?.let { playbackController.playAlbum(it, albumId, 0) }
|
||||
}
|
||||
}
|
||||
|
||||
fun shuffleAlbum(albumId: String) {
|
||||
viewModelScope.launch {
|
||||
sourceRepository.activeSourceIdOnce()?.let { playbackController.shuffleAlbum(it, albumId) }
|
||||
}
|
||||
}
|
||||
|
||||
fun playPlaylist(playlistId: String) {
|
||||
viewModelScope.launch {
|
||||
sourceRepository.activeSourceIdOnce()?.let { playbackController.playPlaylist(it, playlistId, 0) }
|
||||
}
|
||||
}
|
||||
|
||||
fun shufflePlaylist(playlistId: String) {
|
||||
viewModelScope.launch {
|
||||
sourceRepository.activeSourceIdOnce()?.let { playbackController.shufflePlaylist(it, playlistId) }
|
||||
}
|
||||
}
|
||||
|
||||
fun setStar(
|
||||
type: StarType,
|
||||
id: String,
|
||||
starred: Boolean,
|
||||
) {
|
||||
viewModelScope.launch { libraryRepository.setStar(type, id, starred) }
|
||||
}
|
||||
|
||||
fun sync() = syncManager.requestSync()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.subtracks.ui.library
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
@@ -28,6 +28,7 @@ import com.subtracks.ui.components.CoverArt
|
||||
import com.subtracks.ui.components.EmptyState
|
||||
import com.subtracks.ui.components.FilteredEmptyState
|
||||
import com.subtracks.ui.components.LoadingState
|
||||
import com.subtracks.ui.components.MenuTarget
|
||||
import com.subtracks.ui.components.ResetScrollOnChange
|
||||
import com.subtracks.ui.components.rememberViewportFill
|
||||
|
||||
@@ -37,6 +38,7 @@ fun PlaylistsContent(
|
||||
coverArt: (String?, Boolean) -> CoverArtRef?,
|
||||
bottomInset: Dp,
|
||||
onPlaylistClick: (Playlist) -> Unit,
|
||||
onLongClick: (MenuTarget) -> Unit = {},
|
||||
filtered: Boolean = false,
|
||||
onClearFilters: () -> Unit = {},
|
||||
resetKey: Any? = null,
|
||||
@@ -91,7 +93,11 @@ fun PlaylistsContent(
|
||||
modifier = Modifier.size(48.dp).clip(RoundedCornerShape(8.dp)),
|
||||
)
|
||||
},
|
||||
modifier = Modifier.clickable { onPlaylistClick(playlist) },
|
||||
modifier =
|
||||
Modifier.combinedClickable(
|
||||
onClick = { onPlaylistClick(playlist) },
|
||||
onLongClick = { onLongClick(MenuTarget.Playlist(playlist)) },
|
||||
),
|
||||
colors = ListItemDefaults.colors(containerColor = Color.Transparent),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.subtracks.ui.library
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
@@ -39,6 +39,7 @@ import com.subtracks.ui.components.CoverArt
|
||||
import com.subtracks.ui.components.EmptyState
|
||||
import com.subtracks.ui.components.FilteredEmptyState
|
||||
import com.subtracks.ui.components.LoadingState
|
||||
import com.subtracks.ui.components.MenuTarget
|
||||
import com.subtracks.ui.components.ResetScrollOnChange
|
||||
import com.subtracks.ui.components.rememberViewportFill
|
||||
|
||||
@@ -48,6 +49,7 @@ fun SongsContent(
|
||||
coverArt: (String?, Boolean) -> CoverArtRef?,
|
||||
bottomInset: Dp,
|
||||
onSongClick: (String) -> Unit,
|
||||
onLongClick: (MenuTarget) -> Unit = {},
|
||||
playingSongId: String? = null,
|
||||
filtered: Boolean = false,
|
||||
onClearFilters: () -> Unit = {},
|
||||
@@ -86,7 +88,11 @@ fun SongsContent(
|
||||
coverArtId = item.coverArt,
|
||||
coverArt = coverArt,
|
||||
isPlaying = item.song.id == playingSongId,
|
||||
modifier = Modifier.clickable { onSongClick(item.song.id) },
|
||||
modifier =
|
||||
Modifier.combinedClickable(
|
||||
onClick = { onSongClick(item.song.id) },
|
||||
onLongClick = { onLongClick(MenuTarget.Song(item.song)) },
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user