mirror of
https://github.com/KRTirtho/spotube.git
synced 2026-09-20 14:44:00 +00:00
refactor: make sure screens use viewmodels instead of having logic in composables
This commit is contained in:
parent
16e2a743a5
commit
f4eac7dbd9
@ -131,6 +131,8 @@ val sharedModules = module {
|
|||||||
playbackHelper = get(),
|
playbackHelper = get(),
|
||||||
audioPlayerQueue = get(),
|
audioPlayerQueue = get(),
|
||||||
blacklistRepository = get(),
|
blacklistRepository = get(),
|
||||||
|
shareService = get(),
|
||||||
|
downloadManager = get(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -142,6 +144,9 @@ val sharedModules = module {
|
|||||||
playbackHelper = get(),
|
playbackHelper = get(),
|
||||||
audioPlayerQueue = get(),
|
audioPlayerQueue = get(),
|
||||||
blacklistRepository = get(),
|
blacklistRepository = get(),
|
||||||
|
libraryRepository = get(),
|
||||||
|
shareService = get(),
|
||||||
|
downloadManager = get(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -155,6 +160,8 @@ val sharedModules = module {
|
|||||||
savedTracksRepository = get(),
|
savedTracksRepository = get(),
|
||||||
audioPlayerQueue = get(),
|
audioPlayerQueue = get(),
|
||||||
blacklistRepository = get(),
|
blacklistRepository = get(),
|
||||||
|
shareService = get(),
|
||||||
|
downloadManager = get(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -173,6 +180,8 @@ val sharedModules = module {
|
|||||||
libraryRepository = get(),
|
libraryRepository = get(),
|
||||||
audioPlayerQueue = get(),
|
audioPlayerQueue = get(),
|
||||||
blacklistRepository = get(),
|
blacklistRepository = get(),
|
||||||
|
shareService = get(),
|
||||||
|
downloadManager = get(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -109,9 +109,6 @@ val navigationModule = module {
|
|||||||
),
|
),
|
||||||
audioPlayerQueue = get(),
|
audioPlayerQueue = get(),
|
||||||
audioPlayer = get(),
|
audioPlayer = get(),
|
||||||
shareService = get(),
|
|
||||||
downloadsViewModel = get(),
|
|
||||||
libraryRepository = get(),
|
|
||||||
navigationCommands = get(),
|
navigationCommands = get(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -123,9 +120,6 @@ val navigationModule = module {
|
|||||||
),
|
),
|
||||||
audioPlayerQueue = get(),
|
audioPlayerQueue = get(),
|
||||||
audioPlayer = get(),
|
audioPlayer = get(),
|
||||||
shareService = get(),
|
|
||||||
downloadsViewModel = get(),
|
|
||||||
libraryRepository = get(),
|
|
||||||
navigationCommands = get(),
|
navigationCommands = get(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -138,9 +132,6 @@ val navigationModule = module {
|
|||||||
),
|
),
|
||||||
audioPlayerQueue = get(),
|
audioPlayerQueue = get(),
|
||||||
audioPlayer = get(),
|
audioPlayer = get(),
|
||||||
shareService = get(),
|
|
||||||
downloadsViewModel = get(),
|
|
||||||
libraryRepository = get(),
|
|
||||||
navigationCommands = get(),
|
navigationCommands = get(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -155,9 +146,6 @@ val navigationModule = module {
|
|||||||
),
|
),
|
||||||
audioPlayerQueue = get(),
|
audioPlayerQueue = get(),
|
||||||
audioPlayer = get(),
|
audioPlayer = get(),
|
||||||
shareService = get(),
|
|
||||||
downloadsViewModel = get(),
|
|
||||||
libraryRepository = get(),
|
|
||||||
navigationCommands = get(),
|
navigationCommands = get(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -74,6 +74,23 @@ data class TrackOptionsState(
|
|||||||
val isBlacklisted: Boolean = false,
|
val isBlacklisted: Boolean = false,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
data class TrackOptionsContext(
|
||||||
|
val currentTrackId: String? = null,
|
||||||
|
val queueTrackIds: Set<String> = emptySet(),
|
||||||
|
val savedTrackIds: Set<String> = emptySet(),
|
||||||
|
val blacklistedTrackIds: Set<String> = emptySet(),
|
||||||
|
val blacklistedArtistIds: Set<String> = emptySet(),
|
||||||
|
val forceFavorite: Boolean = false,
|
||||||
|
) {
|
||||||
|
fun stateFor(track: MetadataTrack): TrackOptionsState = TrackOptionsState(
|
||||||
|
isInQueue = track.id in queueTrackIds,
|
||||||
|
isCurrentlyPlaying = track.id == currentTrackId,
|
||||||
|
isFavorite = forceFavorite || track.id in savedTrackIds,
|
||||||
|
isBlacklisted = track.id in blacklistedTrackIds ||
|
||||||
|
track.artists.any { it.id in blacklistedArtistIds },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TrackOptions(
|
fun TrackOptions(
|
||||||
track: MetadataTrack,
|
track: MetadataTrack,
|
||||||
|
|||||||
@ -18,33 +18,15 @@
|
|||||||
package dev.krtirtho.spotube.modules.album
|
package dev.krtirtho.spotube.modules.album
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.track.MetadataTrack
|
|
||||||
import dev.krtirtho.spotube.core.audioplayer.AudioPlayer
|
|
||||||
import dev.krtirtho.spotube.core.audioplayer.AudioPlayerInterface
|
import dev.krtirtho.spotube.core.audioplayer.AudioPlayerInterface
|
||||||
import dev.krtirtho.spotube.core.audioplayer.AudioPlayerQueue
|
import dev.krtirtho.spotube.core.audioplayer.AudioPlayerQueue
|
||||||
import dev.krtirtho.spotube.core.audioplayer.PlayerState
|
import dev.krtirtho.spotube.core.audioplayer.PlayerState
|
||||||
import dev.krtirtho.spotube.core.audioplayer.QueueEntry
|
|
||||||
import dev.krtirtho.spotube.core.navigation.NavigationCommands
|
import dev.krtirtho.spotube.core.navigation.NavigationCommands
|
||||||
import dev.krtirtho.spotube.core.navigation.Routes
|
import dev.krtirtho.spotube.core.navigation.Routes
|
||||||
import dev.krtirtho.spotube.core.share.ShareService
|
|
||||||
import dev.krtirtho.spotube.core.ui.component.CollectionView
|
import dev.krtirtho.spotube.core.ui.component.CollectionView
|
||||||
import dev.krtirtho.spotube.core.ui.component.TrackOptionsAction
|
|
||||||
import dev.krtirtho.spotube.core.ui.component.TrackOptionsState
|
|
||||||
import dev.krtirtho.spotube.modules.artist.ArtistViewModel
|
|
||||||
import dev.krtirtho.spotube.modules.downloads.DownloadsViewModel
|
|
||||||
import dev.krtirtho.spotube.modules.library.LibraryRepository
|
|
||||||
import dev.krtirtho.spotube.modules.library.playlist.AddToPlaylistPicker
|
import dev.krtirtho.spotube.modules.library.playlist.AddToPlaylistPicker
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import org.koin.compose.koinInject
|
|
||||||
import org.koin.compose.viewmodel.koinViewModel
|
|
||||||
import org.koin.core.parameter.parametersOf
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AlbumScreen(
|
fun AlbumScreen(
|
||||||
@ -52,59 +34,15 @@ fun AlbumScreen(
|
|||||||
viewModel: AlbumViewModel,
|
viewModel: AlbumViewModel,
|
||||||
audioPlayerQueue: AudioPlayerQueue,
|
audioPlayerQueue: AudioPlayerQueue,
|
||||||
audioPlayer: AudioPlayerInterface,
|
audioPlayer: AudioPlayerInterface,
|
||||||
shareService: ShareService,
|
|
||||||
downloadsViewModel: DownloadsViewModel,
|
|
||||||
libraryRepository: LibraryRepository,
|
|
||||||
navigationCommands: NavigationCommands
|
navigationCommands: NavigationCommands
|
||||||
) {
|
) {
|
||||||
|
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
val queue by audioPlayerQueue.queueFlow.collectAsStateWithLifecycle()
|
|
||||||
val currentQueueEntry by audioPlayerQueue.currentQueueEntryFlow.collectAsStateWithLifecycle()
|
|
||||||
val currentCollectionEntry by audioPlayerQueue.currentCollectionEntryFlow.collectAsStateWithLifecycle()
|
val currentCollectionEntry by audioPlayerQueue.currentCollectionEntryFlow.collectAsStateWithLifecycle()
|
||||||
val playerState by audioPlayer.playerStateFlow.collectAsStateWithLifecycle()
|
val playerState by audioPlayer.playerStateFlow.collectAsStateWithLifecycle()
|
||||||
val savedTrackIds by viewModel.savedTrackIds.collectAsStateWithLifecycle()
|
|
||||||
val savedAlbumIds by viewModel.savedAlbumIds.collectAsStateWithLifecycle()
|
val savedAlbumIds by viewModel.savedAlbumIds.collectAsStateWithLifecycle()
|
||||||
val blacklistedTrackIds by viewModel.blacklistedTrackIds.collectAsStateWithLifecycle()
|
val currentUserId by viewModel.currentUserId.collectAsStateWithLifecycle()
|
||||||
val blacklistedArtistIds by viewModel.blacklistedArtistIds.collectAsStateWithLifecycle()
|
val trackOptionsContext by viewModel.trackOptionsContext.collectAsStateWithLifecycle()
|
||||||
var showAddToPlaylistPicker by remember { mutableStateOf(false) }
|
val showAddToPlaylistPicker by viewModel.showAddToPlaylistPicker.collectAsStateWithLifecycle()
|
||||||
var tracksToAddToPlaylist by remember { mutableStateOf<List<MetadataTrack>>(emptyList()) }
|
|
||||||
var currentUserId by remember { mutableStateOf<String?>(null) }
|
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
currentUserId = libraryRepository.currentUser()?.id
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getTrackOptionsState(track: MetadataTrack): TrackOptionsState {
|
|
||||||
val currentTrackId = (currentQueueEntry as? QueueEntry.StreamingTrack)?.track?.id
|
|
||||||
val queueTrackIds = queue.mapNotNull { entry ->
|
|
||||||
(entry as? QueueEntry.StreamingTrack)?.track?.id
|
|
||||||
}.toSet()
|
|
||||||
return TrackOptionsState(
|
|
||||||
isInQueue = queueTrackIds.contains(track.id),
|
|
||||||
isCurrentlyPlaying = track.id == currentTrackId,
|
|
||||||
isFavorite = savedTrackIds.contains(track.id),
|
|
||||||
isBlacklisted = track.id in blacklistedTrackIds || track.artists.any { it.id in blacklistedArtistIds },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun handleTrackOptionsAction(track: MetadataTrack, action: TrackOptionsAction) {
|
|
||||||
viewModel.handleTrackOptionsAction(track, action)
|
|
||||||
if (action is TrackOptionsAction.Share) {
|
|
||||||
val uri = track.externalUri?.takeIf { it.isNotBlank() }
|
|
||||||
if (uri != null) {
|
|
||||||
shareService.share(uri, track.title)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (action is TrackOptionsAction.Download) {
|
|
||||||
downloadsViewModel.downloadTrack(track)
|
|
||||||
}
|
|
||||||
if (action is TrackOptionsAction.AddToPlaylist) {
|
|
||||||
tracksToAddToPlaylist = listOf(track)
|
|
||||||
showAddToPlaylistPicker = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val dataState = state as? AlbumScreenState.Data
|
val dataState = state as? AlbumScreenState.Data
|
||||||
val album = dataState?.album
|
val album = dataState?.album
|
||||||
@ -142,40 +80,24 @@ fun AlbumScreen(
|
|||||||
tracks = dataState?.tracks ?: emptyList(),
|
tracks = dataState?.tracks ?: emptyList(),
|
||||||
hasMore = dataState?.nextPagination != null,
|
hasMore = dataState?.nextPagination != null,
|
||||||
isLoadingNextPage = state is AlbumScreenState.Data.LoadingMore,
|
isLoadingNextPage = state is AlbumScreenState.Data.LoadingMore,
|
||||||
currentTrackId = (currentQueueEntry as? QueueEntry.StreamingTrack)?.track?.id,
|
currentTrackId = trackOptionsContext.currentTrackId,
|
||||||
isCurrentTrackPlaying = playerState == PlayerState.PLAYING,
|
isCurrentTrackPlaying = playerState == PlayerState.PLAYING,
|
||||||
onTrackClick = viewModel::playAlbumFromTrack,
|
onTrackClick = viewModel::playAlbumFromTrack,
|
||||||
onLoadNextPage = viewModel::loadNextTracksPage,
|
onLoadNextPage = viewModel::loadNextTracksPage,
|
||||||
onArtistClick = { navigationCommands.navigateTo(Routes.Artist(it.id)) },
|
onArtistClick = { navigationCommands.navigateTo(Routes.Artist(it.id)) },
|
||||||
onAlbumClick = { navigationCommands.navigateTo(Routes.Album(it.id)) },
|
onAlbumClick = { navigationCommands.navigateTo(Routes.Album(it.id)) },
|
||||||
onTrackOptionsAction = ::handleTrackOptionsAction,
|
onTrackOptionsAction = viewModel::handleTrackOptionsAction,
|
||||||
trackOptionsState = ::getTrackOptionsState,
|
trackOptionsState = trackOptionsContext::stateFor,
|
||||||
onBulkDownload = { tracks ->
|
onBulkDownload = viewModel::downloadTracks,
|
||||||
downloadsViewModel.downloadTracks(tracks)
|
onBulkAddToQueue = viewModel::addTracksToQueue,
|
||||||
},
|
onBulkPlayNext = viewModel::playTracksNext,
|
||||||
onBulkAddToQueue = { tracks ->
|
onBulkAddToPlaylist = viewModel::showAddToPlaylistPicker,
|
||||||
viewModel.addTracksToQueue(tracks)
|
|
||||||
},
|
|
||||||
onBulkPlayNext = { tracks ->
|
|
||||||
viewModel.playTracksNext(tracks)
|
|
||||||
},
|
|
||||||
onBulkAddToPlaylist = { tracks ->
|
|
||||||
tracksToAddToPlaylist = tracks
|
|
||||||
showAddToPlaylistPicker = true
|
|
||||||
},
|
|
||||||
trailingContent = {
|
trailingContent = {
|
||||||
AddToPlaylistPicker(
|
AddToPlaylistPicker(
|
||||||
visible = showAddToPlaylistPicker,
|
visible = showAddToPlaylistPicker,
|
||||||
currentUserId = currentUserId,
|
currentUserId = currentUserId,
|
||||||
onDismiss = { showAddToPlaylistPicker = false },
|
onDismiss = viewModel::dismissAddToPlaylistPicker,
|
||||||
onPlaylistSelected = { playlistId ->
|
onPlaylistSelected = viewModel::addTracksToPlaylist,
|
||||||
scope.launch {
|
|
||||||
libraryRepository.addTracksToPlaylist(
|
|
||||||
playlistId,
|
|
||||||
tracksToAddToPlaylist.map { it.id }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@ -26,19 +26,25 @@ import dev.krtirtho.spotube.core.audioplayer.AudioPlayerQueue
|
|||||||
import dev.krtirtho.spotube.core.audioplayer.QueueEntry
|
import dev.krtirtho.spotube.core.audioplayer.QueueEntry
|
||||||
import dev.krtirtho.spotube.core.di.injectLogger
|
import dev.krtirtho.spotube.core.di.injectLogger
|
||||||
import dev.krtirtho.spotube.core.playback.CollectionPlaybackHelper
|
import dev.krtirtho.spotube.core.playback.CollectionPlaybackHelper
|
||||||
|
import dev.krtirtho.spotube.core.share.ShareService
|
||||||
import dev.krtirtho.spotube.core.ui.component.TrackOptionsAction
|
import dev.krtirtho.spotube.core.ui.component.TrackOptionsAction
|
||||||
|
import dev.krtirtho.spotube.core.ui.component.TrackOptionsContext
|
||||||
import dev.krtirtho.spotube.modules.blacklist.BlacklistRepository
|
import dev.krtirtho.spotube.modules.blacklist.BlacklistRepository
|
||||||
|
import dev.krtirtho.spotube.modules.downloads.DownloadManager
|
||||||
import dev.krtirtho.spotube.modules.library.LibraryRepository
|
import dev.krtirtho.spotube.modules.library.LibraryRepository
|
||||||
import dev.krtirtho.spotube.modules.saved_tracks.SavedTracksRepository
|
import dev.krtirtho.spotube.modules.saved_tracks.SavedTracksRepository
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.filterNotNull
|
import kotlinx.coroutines.flow.filterNotNull
|
||||||
import kotlinx.coroutines.flow.flatMapLatest
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
|
|
||||||
@ -89,6 +95,8 @@ class AlbumViewModel(
|
|||||||
private val playbackHelper: CollectionPlaybackHelper,
|
private val playbackHelper: CollectionPlaybackHelper,
|
||||||
private val audioPlayerQueue: AudioPlayerQueue,
|
private val audioPlayerQueue: AudioPlayerQueue,
|
||||||
private val blacklistRepository: BlacklistRepository,
|
private val blacklistRepository: BlacklistRepository,
|
||||||
|
private val shareService: ShareService,
|
||||||
|
private val downloadManager: DownloadManager,
|
||||||
) : ViewModel(), KoinComponent {
|
) : ViewModel(), KoinComponent {
|
||||||
private val logger by injectLogger<AlbumViewModel>()
|
private val logger by injectLogger<AlbumViewModel>()
|
||||||
|
|
||||||
@ -97,12 +105,37 @@ class AlbumViewModel(
|
|||||||
val savedAlbumIds
|
val savedAlbumIds
|
||||||
get() = libraryRepository.savedAlbumIdsFlow
|
get() = libraryRepository.savedAlbumIdsFlow
|
||||||
|
|
||||||
|
private val _currentUserId = MutableStateFlow<String?>(null)
|
||||||
|
val currentUserId: StateFlow<String?> = _currentUserId.asStateFlow()
|
||||||
|
|
||||||
private val _blacklistedTrackIds = MutableStateFlow<Set<String>>(emptySet())
|
private val _blacklistedTrackIds = MutableStateFlow<Set<String>>(emptySet())
|
||||||
val blacklistedTrackIds: StateFlow<Set<String>> = _blacklistedTrackIds.asStateFlow()
|
val blacklistedTrackIds: StateFlow<Set<String>> = _blacklistedTrackIds.asStateFlow()
|
||||||
|
|
||||||
private val _blacklistedArtistIds = MutableStateFlow<Set<String>>(emptySet())
|
private val _blacklistedArtistIds = MutableStateFlow<Set<String>>(emptySet())
|
||||||
val blacklistedArtistIds: StateFlow<Set<String>> = _blacklistedArtistIds.asStateFlow()
|
val blacklistedArtistIds: StateFlow<Set<String>> = _blacklistedArtistIds.asStateFlow()
|
||||||
|
|
||||||
|
private val _tracksToAddToPlaylist = MutableStateFlow<List<MetadataTrack>>(emptyList())
|
||||||
|
private val _showAddToPlaylistPicker = MutableStateFlow(false)
|
||||||
|
val showAddToPlaylistPicker: StateFlow<Boolean> = _showAddToPlaylistPicker.asStateFlow()
|
||||||
|
|
||||||
|
val trackOptionsContext: StateFlow<TrackOptionsContext> = combine(
|
||||||
|
audioPlayerQueue.queueFlow,
|
||||||
|
audioPlayerQueue.currentQueueEntryFlow,
|
||||||
|
savedTracksRepository.savedTracksIdsFlow,
|
||||||
|
_blacklistedTrackIds,
|
||||||
|
_blacklistedArtistIds,
|
||||||
|
) { queue, currentEntry, savedIds, blacklistedTracks, blacklistedArtists ->
|
||||||
|
TrackOptionsContext(
|
||||||
|
currentTrackId = (currentEntry as? QueueEntry.StreamingTrack)?.track?.id,
|
||||||
|
queueTrackIds = queue.mapNotNull { entry ->
|
||||||
|
(entry as? QueueEntry.StreamingTrack)?.track?.id
|
||||||
|
}.toSet(),
|
||||||
|
savedTrackIds = savedIds,
|
||||||
|
blacklistedTrackIds = blacklistedTracks,
|
||||||
|
blacklistedArtistIds = blacklistedArtists,
|
||||||
|
)
|
||||||
|
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), TrackOptionsContext())
|
||||||
|
|
||||||
init {
|
init {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
repository.pluginManager.selectedMetadataPlugin
|
repository.pluginManager.selectedMetadataPlugin
|
||||||
@ -111,6 +144,7 @@ class AlbumViewModel(
|
|||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
.collect {
|
.collect {
|
||||||
loadInitialData()
|
loadInitialData()
|
||||||
|
loadCurrentUser()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
blacklistRepository.blacklistedTracks
|
blacklistRepository.blacklistedTracks
|
||||||
@ -121,6 +155,14 @@ class AlbumViewModel(
|
|||||||
.launchIn(viewModelScope)
|
.launchIn(viewModelScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun loadCurrentUser() {
|
||||||
|
runCatching {
|
||||||
|
_currentUserId.value = libraryRepository.currentUser()?.id
|
||||||
|
}.onFailure { e ->
|
||||||
|
logger.e(e) { "Failed to load current user" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun loadInitialData() = runCatching {
|
private suspend fun loadInitialData() = runCatching {
|
||||||
_state.value = AlbumScreenState.Loading
|
_state.value = AlbumScreenState.Loading
|
||||||
val albumInfo = repository.getAlbumInfo(albumId)
|
val albumInfo = repository.getAlbumInfo(albumId)
|
||||||
@ -235,12 +277,48 @@ class AlbumViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is TrackOptionsAction.Download -> {}
|
is TrackOptionsAction.Download -> downloadManager.enqueue(track)
|
||||||
is TrackOptionsAction.ToggleBlacklist -> toggleTrackBlacklist(track)
|
is TrackOptionsAction.ToggleBlacklist -> toggleTrackBlacklist(track)
|
||||||
is TrackOptionsAction.Share -> {}
|
is TrackOptionsAction.Share -> {
|
||||||
is TrackOptionsAction.AddToPlaylist -> {}
|
val uri = track.externalUri?.takeIf { it.isNotBlank() }
|
||||||
|
if (uri != null) {
|
||||||
|
shareService.share(uri, track.title)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
is TrackOptionsAction.AddToPlaylist -> {
|
||||||
|
_tracksToAddToPlaylist.value = listOf(track)
|
||||||
|
_showAddToPlaylistPicker.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addTracksToPlaylist(playlistId: String) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
runCatching {
|
||||||
|
libraryRepository.addTracksToPlaylist(
|
||||||
|
playlistId,
|
||||||
|
_tracksToAddToPlaylist.value.map { it.id },
|
||||||
|
)
|
||||||
|
}.onFailure { e ->
|
||||||
|
logger.e(e) { "Failed to add tracks to playlist" }
|
||||||
|
}.onSuccess {
|
||||||
|
_showAddToPlaylistPicker.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showAddToPlaylistPicker(tracks: List<MetadataTrack>) {
|
||||||
|
_tracksToAddToPlaylist.value = tracks
|
||||||
|
_showAddToPlaylistPicker.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun dismissAddToPlaylistPicker() {
|
||||||
|
_showAddToPlaylistPicker.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun downloadTracks(tracks: List<MetadataTrack>) {
|
||||||
|
tracks.forEach { track -> downloadManager.enqueue(track) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addTracksToQueue(tracks: List<MetadataTrack>) {
|
fun addTracksToQueue(tracks: List<MetadataTrack>) {
|
||||||
|
|||||||
@ -45,10 +45,6 @@ import androidx.compose.material3.Text
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.runtime.snapshotFlow
|
import androidx.compose.runtime.snapshotFlow
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
@ -63,14 +59,12 @@ import coil3.compose.AsyncImage
|
|||||||
import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.album.MetadataAlbum
|
import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.album.MetadataAlbum
|
||||||
import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.artist.MetadataArtist
|
import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.artist.MetadataArtist
|
||||||
import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.playlist.MetadataPlaylist
|
import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.playlist.MetadataPlaylist
|
||||||
import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.track.MetadataTrack
|
|
||||||
import dev.krtirtho.spotube.core.audioplayer.AudioPlayerInterface
|
import dev.krtirtho.spotube.core.audioplayer.AudioPlayerInterface
|
||||||
import dev.krtirtho.spotube.core.audioplayer.AudioPlayerQueue
|
import dev.krtirtho.spotube.core.audioplayer.AudioPlayerQueue
|
||||||
import dev.krtirtho.spotube.core.audioplayer.PlayerState
|
import dev.krtirtho.spotube.core.audioplayer.PlayerState
|
||||||
import dev.krtirtho.spotube.core.audioplayer.QueueEntry
|
import dev.krtirtho.spotube.core.audioplayer.QueueEntry
|
||||||
import dev.krtirtho.spotube.core.navigation.NavigationCommands
|
import dev.krtirtho.spotube.core.navigation.NavigationCommands
|
||||||
import dev.krtirtho.spotube.core.navigation.Routes
|
import dev.krtirtho.spotube.core.navigation.Routes
|
||||||
import dev.krtirtho.spotube.core.share.ShareService
|
|
||||||
import dev.krtirtho.spotube.core.ui.base.PrimaryButton
|
import dev.krtirtho.spotube.core.ui.base.PrimaryButton
|
||||||
import dev.krtirtho.spotube.core.ui.base.PrimaryIconButton
|
import dev.krtirtho.spotube.core.ui.base.PrimaryIconButton
|
||||||
import dev.krtirtho.spotube.core.ui.base.SecondaryButton
|
import dev.krtirtho.spotube.core.ui.base.SecondaryButton
|
||||||
@ -80,13 +74,9 @@ import dev.krtirtho.spotube.core.ui.component.ApplicationMainBar
|
|||||||
import dev.krtirtho.spotube.core.ui.component.ArtistCard
|
import dev.krtirtho.spotube.core.ui.component.ArtistCard
|
||||||
import dev.krtirtho.spotube.core.ui.component.PlaylistCard
|
import dev.krtirtho.spotube.core.ui.component.PlaylistCard
|
||||||
import dev.krtirtho.spotube.core.ui.component.TrackList
|
import dev.krtirtho.spotube.core.ui.component.TrackList
|
||||||
import dev.krtirtho.spotube.core.ui.component.TrackOptionsAction
|
|
||||||
import dev.krtirtho.spotube.core.ui.component.TrackOptionsState
|
|
||||||
import dev.krtirtho.spotube.core.ui.component.cards.PlayableCard
|
import dev.krtirtho.spotube.core.ui.component.cards.PlayableCard
|
||||||
import dev.krtirtho.spotube.core.ui.component.dragScrollable
|
import dev.krtirtho.spotube.core.ui.component.dragScrollable
|
||||||
import dev.krtirtho.spotube.core.ui.misc.SkeletonTree
|
import dev.krtirtho.spotube.core.ui.misc.SkeletonTree
|
||||||
import dev.krtirtho.spotube.modules.downloads.DownloadsViewModel
|
|
||||||
import dev.krtirtho.spotube.modules.library.LibraryRepository
|
|
||||||
import dev.krtirtho.spotube.modules.library.playlist.AddToPlaylistPicker
|
import dev.krtirtho.spotube.modules.library.playlist.AddToPlaylistPicker
|
||||||
import dev.krtirtho.spotube.resources.iconsax.Iconsax
|
import dev.krtirtho.spotube.resources.iconsax.Iconsax
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxAddSquare
|
import dev.krtirtho.spotube.resources.iconsax.IconsaxAddSquare
|
||||||
@ -95,7 +85,6 @@ import dev.krtirtho.spotube.resources.iconsax.IconsaxUserRemove
|
|||||||
import dev.krtirtho.spotube.resources.iconsax.User
|
import dev.krtirtho.spotube.resources.iconsax.User
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@ -103,58 +92,16 @@ fun ArtistScreen(
|
|||||||
viewModel: ArtistViewModel,
|
viewModel: ArtistViewModel,
|
||||||
audioPlayerQueue: AudioPlayerQueue,
|
audioPlayerQueue: AudioPlayerQueue,
|
||||||
audioPlayer: AudioPlayerInterface,
|
audioPlayer: AudioPlayerInterface,
|
||||||
shareService: ShareService,
|
|
||||||
downloadsViewModel: DownloadsViewModel,
|
|
||||||
libraryRepository: LibraryRepository,
|
|
||||||
navigationCommands: NavigationCommands
|
navigationCommands: NavigationCommands
|
||||||
) {
|
) {
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
|
|
||||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||||
val queue by audioPlayerQueue.queueFlow.collectAsStateWithLifecycle()
|
|
||||||
val currentQueueEntry by audioPlayerQueue.currentQueueEntryFlow.collectAsStateWithLifecycle()
|
val currentQueueEntry by audioPlayerQueue.currentQueueEntryFlow.collectAsStateWithLifecycle()
|
||||||
val playerState by audioPlayer.playerStateFlow.collectAsStateWithLifecycle()
|
val playerState by audioPlayer.playerStateFlow.collectAsStateWithLifecycle()
|
||||||
val savedTrackIds by viewModel.savedTrackIds.collectAsStateWithLifecycle()
|
|
||||||
val savedArtistIds by viewModel.savedArtistIds.collectAsStateWithLifecycle()
|
val savedArtistIds by viewModel.savedArtistIds.collectAsStateWithLifecycle()
|
||||||
val blacklistedTrackIds by viewModel.blacklistedTrackIds.collectAsStateWithLifecycle()
|
|
||||||
val blacklistedArtistIds by viewModel.blacklistedArtistIds.collectAsStateWithLifecycle()
|
val blacklistedArtistIds by viewModel.blacklistedArtistIds.collectAsStateWithLifecycle()
|
||||||
var showAddToPlaylistPicker by remember { mutableStateOf(false) }
|
val currentUserId by viewModel.currentUserId.collectAsStateWithLifecycle()
|
||||||
var tracksToAddToPlaylist by remember { mutableStateOf<List<MetadataTrack>>(emptyList()) }
|
val trackOptionsContext by viewModel.trackOptionsContext.collectAsStateWithLifecycle()
|
||||||
var currentUserId by remember { mutableStateOf<String?>(null) }
|
val showAddToPlaylistPicker by viewModel.showAddToPlaylistPicker.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
currentUserId = libraryRepository.currentUser()?.id
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getTrackOptionsState(track: MetadataTrack): TrackOptionsState {
|
|
||||||
val currentTrackId = (currentQueueEntry as? QueueEntry.StreamingTrack)?.track?.id
|
|
||||||
val queueTrackIds = queue.mapNotNull { entry ->
|
|
||||||
(entry as? QueueEntry.StreamingTrack)?.track?.id
|
|
||||||
}.toSet()
|
|
||||||
return TrackOptionsState(
|
|
||||||
isInQueue = queueTrackIds.contains(track.id),
|
|
||||||
isCurrentlyPlaying = track.id == currentTrackId,
|
|
||||||
isFavorite = savedTrackIds.contains(track.id),
|
|
||||||
isBlacklisted = track.id in blacklistedTrackIds || track.artists.any { it.id in blacklistedArtistIds },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun handleTrackOptionsAction(track: MetadataTrack, action: TrackOptionsAction) {
|
|
||||||
viewModel.handleTrackOptionsAction(track, action)
|
|
||||||
if (action is TrackOptionsAction.Share) {
|
|
||||||
val uri = track.externalUri?.takeIf { it.isNotBlank() }
|
|
||||||
if (uri != null) {
|
|
||||||
shareService.share(uri, track.title)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (action is TrackOptionsAction.Download) {
|
|
||||||
downloadsViewModel.downloadTrack(track)
|
|
||||||
}
|
|
||||||
if (action is TrackOptionsAction.AddToPlaylist) {
|
|
||||||
tracksToAddToPlaylist = listOf(track)
|
|
||||||
showAddToPlaylistPicker = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = { ApplicationMainBar() }
|
topBar = { ApplicationMainBar() }
|
||||||
@ -209,8 +156,8 @@ fun ArtistScreen(
|
|||||||
currentTrackId = (currentQueueEntry as? QueueEntry.StreamingTrack)?.track?.id,
|
currentTrackId = (currentQueueEntry as? QueueEntry.StreamingTrack)?.track?.id,
|
||||||
isCurrentTrackPlaying = playerState == PlayerState.PLAYING,
|
isCurrentTrackPlaying = playerState == PlayerState.PLAYING,
|
||||||
onTrackClick = viewModel::playTopTracksFromTrack,
|
onTrackClick = viewModel::playTopTracksFromTrack,
|
||||||
onTrackOptionsAction = ::handleTrackOptionsAction,
|
onTrackOptionsAction = viewModel::handleTrackOptionsAction,
|
||||||
trackOptionsState = ::getTrackOptionsState,
|
trackOptionsState = trackOptionsContext::stateFor,
|
||||||
onArtistClick = { trackArtist ->
|
onArtistClick = { trackArtist ->
|
||||||
navigationCommands.navigateTo(
|
navigationCommands.navigateTo(
|
||||||
Routes.Artist(
|
Routes.Artist(
|
||||||
@ -228,13 +175,10 @@ fun ArtistScreen(
|
|||||||
onLoadNextPage = {},
|
onLoadNextPage = {},
|
||||||
simplified = true,
|
simplified = true,
|
||||||
scrollable = false,
|
scrollable = false,
|
||||||
onBulkDownload = { tracks -> downloadsViewModel.downloadTracks(tracks) },
|
onBulkDownload = viewModel::downloadTracks,
|
||||||
onBulkAddToQueue = { tracks -> viewModel.addTracksToQueue(tracks) },
|
onBulkAddToQueue = viewModel::addTracksToQueue,
|
||||||
onBulkPlayNext = { tracks -> viewModel.playTracksNext(tracks) },
|
onBulkPlayNext = viewModel::playTracksNext,
|
||||||
onBulkAddToPlaylist = { tracks ->
|
onBulkAddToPlaylist = viewModel::showAddToPlaylistPicker,
|
||||||
tracksToAddToPlaylist = tracks
|
|
||||||
showAddToPlaylistPicker = true
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -292,15 +236,8 @@ fun ArtistScreen(
|
|||||||
AddToPlaylistPicker(
|
AddToPlaylistPicker(
|
||||||
visible = showAddToPlaylistPicker,
|
visible = showAddToPlaylistPicker,
|
||||||
currentUserId = currentUserId,
|
currentUserId = currentUserId,
|
||||||
onDismiss = { showAddToPlaylistPicker = false },
|
onDismiss = viewModel::dismissAddToPlaylistPicker,
|
||||||
onPlaylistSelected = { playlistId ->
|
onPlaylistSelected = viewModel::addTracksToPlaylist,
|
||||||
scope.launch {
|
|
||||||
libraryRepository.addTracksToPlaylist(
|
|
||||||
playlistId,
|
|
||||||
tracksToAddToPlaylist.map { it.id }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -27,15 +27,21 @@ import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.track.MetadataTrack
|
|||||||
import dev.krtirtho.spotube.core.audioplayer.AudioPlayerQueue
|
import dev.krtirtho.spotube.core.audioplayer.AudioPlayerQueue
|
||||||
import dev.krtirtho.spotube.core.audioplayer.QueueEntry
|
import dev.krtirtho.spotube.core.audioplayer.QueueEntry
|
||||||
import dev.krtirtho.spotube.core.di.injectLogger
|
import dev.krtirtho.spotube.core.di.injectLogger
|
||||||
|
import dev.krtirtho.spotube.core.share.ShareService
|
||||||
import dev.krtirtho.spotube.core.ui.component.TrackOptionsAction
|
import dev.krtirtho.spotube.core.ui.component.TrackOptionsAction
|
||||||
|
import dev.krtirtho.spotube.core.ui.component.TrackOptionsContext
|
||||||
import dev.krtirtho.spotube.modules.blacklist.BlacklistRepository
|
import dev.krtirtho.spotube.modules.blacklist.BlacklistRepository
|
||||||
|
import dev.krtirtho.spotube.modules.downloads.DownloadManager
|
||||||
import dev.krtirtho.spotube.modules.library.LibraryRepository
|
import dev.krtirtho.spotube.modules.library.LibraryRepository
|
||||||
import dev.krtirtho.spotube.modules.saved_tracks.SavedTracksRepository
|
import dev.krtirtho.spotube.modules.saved_tracks.SavedTracksRepository
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
|
|
||||||
@ -65,6 +71,8 @@ class ArtistViewModel(
|
|||||||
private val savedTracksRepository: SavedTracksRepository,
|
private val savedTracksRepository: SavedTracksRepository,
|
||||||
private val audioPlayerQueue: AudioPlayerQueue,
|
private val audioPlayerQueue: AudioPlayerQueue,
|
||||||
private val blacklistRepository: BlacklistRepository,
|
private val blacklistRepository: BlacklistRepository,
|
||||||
|
private val shareService: ShareService,
|
||||||
|
private val downloadManager: DownloadManager,
|
||||||
) : ViewModel(), KoinComponent {
|
) : ViewModel(), KoinComponent {
|
||||||
private val logger by injectLogger<ArtistViewModel>()
|
private val logger by injectLogger<ArtistViewModel>()
|
||||||
|
|
||||||
@ -77,12 +85,37 @@ class ArtistViewModel(
|
|||||||
val savedTrackIds
|
val savedTrackIds
|
||||||
get() = savedTracksRepository.savedTracksIdsFlow
|
get() = savedTracksRepository.savedTracksIdsFlow
|
||||||
|
|
||||||
|
private val _currentUserId = MutableStateFlow<String?>(null)
|
||||||
|
val currentUserId: StateFlow<String?> = _currentUserId.asStateFlow()
|
||||||
|
|
||||||
private val _blacklistedTrackIds = MutableStateFlow<Set<String>>(emptySet())
|
private val _blacklistedTrackIds = MutableStateFlow<Set<String>>(emptySet())
|
||||||
val blacklistedTrackIds: StateFlow<Set<String>> = _blacklistedTrackIds.asStateFlow()
|
val blacklistedTrackIds: StateFlow<Set<String>> = _blacklistedTrackIds.asStateFlow()
|
||||||
|
|
||||||
private val _blacklistedArtistIds = MutableStateFlow<Set<String>>(emptySet())
|
private val _blacklistedArtistIds = MutableStateFlow<Set<String>>(emptySet())
|
||||||
val blacklistedArtistIds: StateFlow<Set<String>> = _blacklistedArtistIds.asStateFlow()
|
val blacklistedArtistIds: StateFlow<Set<String>> = _blacklistedArtistIds.asStateFlow()
|
||||||
|
|
||||||
|
private val _tracksToAddToPlaylist = MutableStateFlow<List<MetadataTrack>>(emptyList())
|
||||||
|
private val _showAddToPlaylistPicker = MutableStateFlow(false)
|
||||||
|
val showAddToPlaylistPicker: StateFlow<Boolean> = _showAddToPlaylistPicker.asStateFlow()
|
||||||
|
|
||||||
|
val trackOptionsContext: StateFlow<TrackOptionsContext> = combine(
|
||||||
|
audioPlayerQueue.queueFlow,
|
||||||
|
audioPlayerQueue.currentQueueEntryFlow,
|
||||||
|
savedTracksRepository.savedTracksIdsFlow,
|
||||||
|
_blacklistedTrackIds,
|
||||||
|
_blacklistedArtistIds,
|
||||||
|
) { queue, currentEntry, savedIds, blacklistedTracks, blacklistedArtists ->
|
||||||
|
TrackOptionsContext(
|
||||||
|
currentTrackId = (currentEntry as? QueueEntry.StreamingTrack)?.track?.id,
|
||||||
|
queueTrackIds = queue.mapNotNull { entry ->
|
||||||
|
(entry as? QueueEntry.StreamingTrack)?.track?.id
|
||||||
|
}.toSet(),
|
||||||
|
savedTrackIds = savedIds,
|
||||||
|
blacklistedTrackIds = blacklistedTracks,
|
||||||
|
blacklistedArtistIds = blacklistedArtists,
|
||||||
|
)
|
||||||
|
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), TrackOptionsContext())
|
||||||
|
|
||||||
init {
|
init {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
libraryRepository.savedArtistIdsFlow.collect {
|
libraryRepository.savedArtistIdsFlow.collect {
|
||||||
@ -98,9 +131,20 @@ class ArtistViewModel(
|
|||||||
blacklistRepository.blacklistedArtists
|
blacklistRepository.blacklistedArtists
|
||||||
.onEach { artists -> _blacklistedArtistIds.value = artists.map { it.id }.toSet() }
|
.onEach { artists -> _blacklistedArtistIds.value = artists.map { it.id }.toSet() }
|
||||||
.launchIn(viewModelScope)
|
.launchIn(viewModelScope)
|
||||||
|
loadCurrentUser()
|
||||||
loadOverview()
|
loadOverview()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun loadCurrentUser() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
runCatching {
|
||||||
|
_currentUserId.value = libraryRepository.currentUser()?.id
|
||||||
|
}.onFailure { e ->
|
||||||
|
logger.e(e) { "Failed to load current user" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun refresh() {
|
fun refresh() {
|
||||||
repository.invalidateCaches()
|
repository.invalidateCaches()
|
||||||
loadOverview()
|
loadOverview()
|
||||||
@ -319,12 +363,48 @@ class ArtistViewModel(
|
|||||||
savedTracksRepository.saveTracks(listOf(track.id))
|
savedTracksRepository.saveTracks(listOf(track.id))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is TrackOptionsAction.Download -> {}
|
is TrackOptionsAction.Download -> downloadManager.enqueue(track)
|
||||||
is TrackOptionsAction.ToggleBlacklist -> toggleTrackBlacklist(track)
|
is TrackOptionsAction.ToggleBlacklist -> toggleTrackBlacklist(track)
|
||||||
is TrackOptionsAction.Share -> {}
|
is TrackOptionsAction.Share -> {
|
||||||
is TrackOptionsAction.AddToPlaylist -> {}
|
val uri = track.externalUri?.takeIf { it.isNotBlank() }
|
||||||
|
if (uri != null) {
|
||||||
|
shareService.share(uri, track.title)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
is TrackOptionsAction.AddToPlaylist -> {
|
||||||
|
_tracksToAddToPlaylist.value = listOf(track)
|
||||||
|
_showAddToPlaylistPicker.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addTracksToPlaylist(playlistId: String) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
runCatching {
|
||||||
|
libraryRepository.addTracksToPlaylist(
|
||||||
|
playlistId,
|
||||||
|
_tracksToAddToPlaylist.value.map { it.id },
|
||||||
|
)
|
||||||
|
}.onFailure { e ->
|
||||||
|
logger.e(e) { "Failed to add tracks to playlist" }
|
||||||
|
}.onSuccess {
|
||||||
|
_showAddToPlaylistPicker.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showAddToPlaylistPicker(tracks: List<MetadataTrack>) {
|
||||||
|
_tracksToAddToPlaylist.value = tracks
|
||||||
|
_showAddToPlaylistPicker.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun dismissAddToPlaylistPicker() {
|
||||||
|
_showAddToPlaylistPicker.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun downloadTracks(tracks: List<MetadataTrack>) {
|
||||||
|
tracks.forEach { track -> downloadManager.enqueue(track) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadOverview() {
|
private fun loadOverview() {
|
||||||
|
|||||||
@ -27,35 +27,22 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.track.MetadataTrack
|
|
||||||
import dev.krtirtho.spotube.core.audioplayer.AudioPlayer
|
|
||||||
import dev.krtirtho.spotube.core.audioplayer.AudioPlayerInterface
|
import dev.krtirtho.spotube.core.audioplayer.AudioPlayerInterface
|
||||||
import dev.krtirtho.spotube.core.audioplayer.AudioPlayerQueue
|
import dev.krtirtho.spotube.core.audioplayer.AudioPlayerQueue
|
||||||
import dev.krtirtho.spotube.core.audioplayer.PlayerState
|
import dev.krtirtho.spotube.core.audioplayer.PlayerState
|
||||||
import dev.krtirtho.spotube.core.audioplayer.QueueEntry
|
|
||||||
import dev.krtirtho.spotube.core.navigation.NavigationCommands
|
import dev.krtirtho.spotube.core.navigation.NavigationCommands
|
||||||
import dev.krtirtho.spotube.core.navigation.Routes
|
import dev.krtirtho.spotube.core.navigation.Routes
|
||||||
import dev.krtirtho.spotube.core.share.ShareService
|
|
||||||
import dev.krtirtho.spotube.core.ui.base.OutlineButton
|
import dev.krtirtho.spotube.core.ui.base.OutlineButton
|
||||||
import dev.krtirtho.spotube.core.ui.component.CollectionView
|
import dev.krtirtho.spotube.core.ui.component.CollectionView
|
||||||
import dev.krtirtho.spotube.core.ui.component.TrackOptionsAction
|
|
||||||
import dev.krtirtho.spotube.core.ui.component.TrackOptionsState
|
|
||||||
import dev.krtirtho.spotube.modules.downloads.DownloadsViewModel
|
|
||||||
import dev.krtirtho.spotube.modules.library.LibraryRepository
|
|
||||||
import dev.krtirtho.spotube.modules.library.playlist.AddToPlaylistPicker
|
import dev.krtirtho.spotube.modules.library.playlist.AddToPlaylistPicker
|
||||||
import dev.krtirtho.spotube.modules.library.playlist.PlaylistFormData
|
import dev.krtirtho.spotube.modules.library.playlist.PlaylistFormData
|
||||||
import dev.krtirtho.spotube.modules.library.playlist.PlaylistFormSheet
|
import dev.krtirtho.spotube.modules.library.playlist.PlaylistFormSheet
|
||||||
import dev.krtirtho.spotube.resources.iconsax.Iconsax
|
import dev.krtirtho.spotube.resources.iconsax.Iconsax
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxAddSquare
|
import dev.krtirtho.spotube.resources.iconsax.IconsaxAddSquare
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import org.koin.compose.koinInject
|
|
||||||
import org.koin.compose.viewmodel.koinViewModel
|
|
||||||
import org.koin.core.parameter.parametersOf
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PlaylistScreen(
|
fun PlaylistScreen(
|
||||||
@ -63,40 +50,17 @@ fun PlaylistScreen(
|
|||||||
viewModel: PlaylistViewModel,
|
viewModel: PlaylistViewModel,
|
||||||
audioPlayerQueue: AudioPlayerQueue,
|
audioPlayerQueue: AudioPlayerQueue,
|
||||||
audioPlayer: AudioPlayerInterface,
|
audioPlayer: AudioPlayerInterface,
|
||||||
shareService: ShareService,
|
|
||||||
downloadsViewModel: DownloadsViewModel,
|
|
||||||
libraryRepository: LibraryRepository,
|
|
||||||
navigationCommands: NavigationCommands
|
navigationCommands: NavigationCommands
|
||||||
) {
|
) {
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
val queue by audioPlayerQueue.queueFlow.collectAsStateWithLifecycle()
|
|
||||||
val currentQueueEntry by audioPlayerQueue.currentQueueEntryFlow.collectAsStateWithLifecycle()
|
|
||||||
val currentCollectionEntry by audioPlayerQueue.currentCollectionEntryFlow.collectAsStateWithLifecycle()
|
val currentCollectionEntry by audioPlayerQueue.currentCollectionEntryFlow.collectAsStateWithLifecycle()
|
||||||
val playerState by audioPlayer.playerStateFlow.collectAsStateWithLifecycle()
|
val playerState by audioPlayer.playerStateFlow.collectAsStateWithLifecycle()
|
||||||
val savedPlaylistIds by viewModel.savedPlaylistIds.collectAsStateWithLifecycle()
|
val savedPlaylistIds by viewModel.savedPlaylistIds.collectAsStateWithLifecycle()
|
||||||
val currentUserId by viewModel.currentUserId.collectAsStateWithLifecycle()
|
val currentUserId by viewModel.currentUserId.collectAsStateWithLifecycle()
|
||||||
|
val trackOptionsContext by viewModel.trackOptionsContext.collectAsStateWithLifecycle()
|
||||||
|
val showAddToPlaylistPicker by viewModel.showAddToPlaylistPicker.collectAsStateWithLifecycle()
|
||||||
var showEditPlaylist by remember { mutableStateOf(false) }
|
var showEditPlaylist by remember { mutableStateOf(false) }
|
||||||
var showAddToPlaylistPicker by remember { mutableStateOf(false) }
|
|
||||||
var showAddTracksDialog by remember { mutableStateOf(false) }
|
var showAddTracksDialog by remember { mutableStateOf(false) }
|
||||||
var tracksToAddToPlaylist by remember { mutableStateOf<List<MetadataTrack>>(emptyList()) }
|
|
||||||
|
|
||||||
fun handleTrackOptionsAction(track: MetadataTrack, action: TrackOptionsAction) {
|
|
||||||
viewModel.handleTrackOptionsAction(track, action)
|
|
||||||
if (action is TrackOptionsAction.Share) {
|
|
||||||
val uri = track.externalUri?.takeIf { it.isNotBlank() }
|
|
||||||
if (uri != null) {
|
|
||||||
shareService.share(uri, track.title)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (action is TrackOptionsAction.Download) {
|
|
||||||
downloadsViewModel.downloadTrack(track)
|
|
||||||
}
|
|
||||||
if (action is TrackOptionsAction.AddToPlaylist) {
|
|
||||||
tracksToAddToPlaylist = listOf(track)
|
|
||||||
showAddToPlaylistPicker = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val dataState = state as? PlaylistScreenState.Data
|
val dataState = state as? PlaylistScreenState.Data
|
||||||
val playlist = dataState?.playlist
|
val playlist = dataState?.playlist
|
||||||
@ -108,23 +72,6 @@ fun PlaylistScreen(
|
|||||||
val isPlaying = currentCollectionEntry?.id == playlistId &&
|
val isPlaying = currentCollectionEntry?.id == playlistId &&
|
||||||
playerState == PlayerState.PLAYING
|
playerState == PlayerState.PLAYING
|
||||||
|
|
||||||
val savedTrackIds by viewModel.savedTrackIds.collectAsStateWithLifecycle()
|
|
||||||
val blacklistedTrackIds by viewModel.blacklistedTrackIds.collectAsStateWithLifecycle()
|
|
||||||
val blacklistedArtistIds by viewModel.blacklistedArtistIds.collectAsStateWithLifecycle()
|
|
||||||
|
|
||||||
fun getTrackOptionsState(track: MetadataTrack): TrackOptionsState {
|
|
||||||
val currentTrackId = (currentQueueEntry as? QueueEntry.StreamingTrack)?.track?.id
|
|
||||||
val queueTrackIds = queue.mapNotNull { entry ->
|
|
||||||
(entry as? QueueEntry.StreamingTrack)?.track?.id
|
|
||||||
}.toSet()
|
|
||||||
return TrackOptionsState(
|
|
||||||
isInQueue = queueTrackIds.contains(track.id),
|
|
||||||
isCurrentlyPlaying = track.id == currentTrackId,
|
|
||||||
isFavorite = savedTrackIds.contains(track.id),
|
|
||||||
isBlacklisted = track.id in blacklistedTrackIds || track.artists.any { it.id in blacklistedArtistIds },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val errorMessage = (state as? PlaylistScreenState.Error)?.message
|
val errorMessage = (state as? PlaylistScreenState.Error)?.message
|
||||||
val isLoading = state is PlaylistScreenState.Loading && (dataState == null || dataState.tracks.isEmpty())
|
val isLoading = state is PlaylistScreenState.Loading && (dataState == null || dataState.tracks.isEmpty())
|
||||||
|
|
||||||
@ -168,27 +115,18 @@ fun PlaylistScreen(
|
|||||||
tracks = dataState?.tracks ?: emptyList(),
|
tracks = dataState?.tracks ?: emptyList(),
|
||||||
hasMore = dataState?.nextPagination != null,
|
hasMore = dataState?.nextPagination != null,
|
||||||
isLoadingNextPage = state is PlaylistScreenState.Data.LoadingMore,
|
isLoadingNextPage = state is PlaylistScreenState.Data.LoadingMore,
|
||||||
currentTrackId = (currentQueueEntry as? QueueEntry.StreamingTrack)?.track?.id,
|
currentTrackId = trackOptionsContext.currentTrackId,
|
||||||
isCurrentTrackPlaying = playerState == PlayerState.PLAYING,
|
isCurrentTrackPlaying = playerState == PlayerState.PLAYING,
|
||||||
onTrackClick = viewModel::playPlaylistFromTrack,
|
onTrackClick = viewModel::playPlaylistFromTrack,
|
||||||
onLoadNextPage = viewModel::loadNextTracksPage,
|
onLoadNextPage = viewModel::loadNextTracksPage,
|
||||||
onArtistClick = { navigationCommands.navigateTo(Routes.Artist(it.id)) },
|
onArtistClick = { navigationCommands.navigateTo(Routes.Artist(it.id)) },
|
||||||
onAlbumClick = { navigationCommands.navigateTo(Routes.Album(it.id)) },
|
onAlbumClick = { navigationCommands.navigateTo(Routes.Album(it.id)) },
|
||||||
onTrackOptionsAction = ::handleTrackOptionsAction,
|
onTrackOptionsAction = viewModel::handleTrackOptionsAction,
|
||||||
trackOptionsState = ::getTrackOptionsState,
|
trackOptionsState = trackOptionsContext::stateFor,
|
||||||
onBulkDownload = { tracks ->
|
onBulkDownload = viewModel::downloadTracks,
|
||||||
downloadsViewModel.downloadTracks(tracks)
|
onBulkAddToQueue = viewModel::addTracksToQueue,
|
||||||
},
|
onBulkPlayNext = viewModel::playTracksNext,
|
||||||
onBulkAddToQueue = { tracks ->
|
onBulkAddToPlaylist = viewModel::showAddToPlaylistPicker,
|
||||||
viewModel.addTracksToQueue(tracks)
|
|
||||||
},
|
|
||||||
onBulkPlayNext = { tracks ->
|
|
||||||
viewModel.playTracksNext(tracks)
|
|
||||||
},
|
|
||||||
onBulkAddToPlaylist = { tracks ->
|
|
||||||
tracksToAddToPlaylist = tracks
|
|
||||||
showAddToPlaylistPicker = true
|
|
||||||
},
|
|
||||||
footerContent = footerContent,
|
footerContent = footerContent,
|
||||||
trailingContent = {
|
trailingContent = {
|
||||||
val loadedPlaylist = (dataState as? PlaylistScreenState.Data.Loaded)?.playlist
|
val loadedPlaylist = (dataState as? PlaylistScreenState.Data.Loaded)?.playlist
|
||||||
@ -219,15 +157,8 @@ fun PlaylistScreen(
|
|||||||
AddToPlaylistPicker(
|
AddToPlaylistPicker(
|
||||||
visible = showAddToPlaylistPicker,
|
visible = showAddToPlaylistPicker,
|
||||||
currentUserId = currentUserId,
|
currentUserId = currentUserId,
|
||||||
onDismiss = { showAddToPlaylistPicker = false },
|
onDismiss = viewModel::dismissAddToPlaylistPicker,
|
||||||
onPlaylistSelected = { selectedPlaylistId ->
|
onPlaylistSelected = viewModel::addTracksToPlaylist,
|
||||||
scope.launch {
|
|
||||||
libraryRepository.addTracksToPlaylist(
|
|
||||||
selectedPlaylistId,
|
|
||||||
tracksToAddToPlaylist.map { it.id }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
AddTracksToPlaylistDialog(
|
AddTracksToPlaylistDialog(
|
||||||
|
|||||||
@ -26,20 +26,26 @@ import dev.krtirtho.spotube.core.audioplayer.AudioPlayerQueue
|
|||||||
import dev.krtirtho.spotube.core.audioplayer.QueueEntry
|
import dev.krtirtho.spotube.core.audioplayer.QueueEntry
|
||||||
import dev.krtirtho.spotube.core.di.injectLogger
|
import dev.krtirtho.spotube.core.di.injectLogger
|
||||||
import dev.krtirtho.spotube.core.playback.CollectionPlaybackHelper
|
import dev.krtirtho.spotube.core.playback.CollectionPlaybackHelper
|
||||||
|
import dev.krtirtho.spotube.core.share.ShareService
|
||||||
import dev.krtirtho.spotube.core.ui.component.TrackOptionsAction
|
import dev.krtirtho.spotube.core.ui.component.TrackOptionsAction
|
||||||
|
import dev.krtirtho.spotube.core.ui.component.TrackOptionsContext
|
||||||
import dev.krtirtho.spotube.core.ui.component.TrackOptionsState
|
import dev.krtirtho.spotube.core.ui.component.TrackOptionsState
|
||||||
import dev.krtirtho.spotube.modules.blacklist.BlacklistRepository
|
import dev.krtirtho.spotube.modules.blacklist.BlacklistRepository
|
||||||
|
import dev.krtirtho.spotube.modules.downloads.DownloadManager
|
||||||
import dev.krtirtho.spotube.modules.library.LibraryRepository
|
import dev.krtirtho.spotube.modules.library.LibraryRepository
|
||||||
import dev.krtirtho.spotube.modules.saved_tracks.SavedTracksRepository
|
import dev.krtirtho.spotube.modules.saved_tracks.SavedTracksRepository
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.filterNotNull
|
import kotlinx.coroutines.flow.filterNotNull
|
||||||
import kotlinx.coroutines.flow.flatMapLatest
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
|
|
||||||
@ -90,6 +96,8 @@ class PlaylistViewModel(
|
|||||||
private val playbackHelper: CollectionPlaybackHelper,
|
private val playbackHelper: CollectionPlaybackHelper,
|
||||||
private val audioPlayerQueue: AudioPlayerQueue,
|
private val audioPlayerQueue: AudioPlayerQueue,
|
||||||
private val blacklistRepository: BlacklistRepository,
|
private val blacklistRepository: BlacklistRepository,
|
||||||
|
private val shareService: ShareService,
|
||||||
|
private val downloadManager: DownloadManager,
|
||||||
) : ViewModel(), KoinComponent {
|
) : ViewModel(), KoinComponent {
|
||||||
private val logger by injectLogger<PlaylistViewModel>()
|
private val logger by injectLogger<PlaylistViewModel>()
|
||||||
|
|
||||||
@ -107,6 +115,28 @@ class PlaylistViewModel(
|
|||||||
private val _blacklistedArtistIds = MutableStateFlow<Set<String>>(emptySet())
|
private val _blacklistedArtistIds = MutableStateFlow<Set<String>>(emptySet())
|
||||||
val blacklistedArtistIds: StateFlow<Set<String>> = _blacklistedArtistIds.asStateFlow()
|
val blacklistedArtistIds: StateFlow<Set<String>> = _blacklistedArtistIds.asStateFlow()
|
||||||
|
|
||||||
|
private val _tracksToAddToPlaylist = MutableStateFlow<List<MetadataTrack>>(emptyList())
|
||||||
|
private val _showAddToPlaylistPicker = MutableStateFlow(false)
|
||||||
|
val showAddToPlaylistPicker: StateFlow<Boolean> = _showAddToPlaylistPicker.asStateFlow()
|
||||||
|
|
||||||
|
val trackOptionsContext: StateFlow<TrackOptionsContext> = combine(
|
||||||
|
audioPlayerQueue.queueFlow,
|
||||||
|
audioPlayerQueue.currentQueueEntryFlow,
|
||||||
|
savedTracksRepository.savedTracksIdsFlow,
|
||||||
|
_blacklistedTrackIds,
|
||||||
|
_blacklistedArtistIds,
|
||||||
|
) { queue, currentEntry, savedIds, blacklistedTracks, blacklistedArtists ->
|
||||||
|
TrackOptionsContext(
|
||||||
|
currentTrackId = (currentEntry as? QueueEntry.StreamingTrack)?.track?.id,
|
||||||
|
queueTrackIds = queue.mapNotNull { entry ->
|
||||||
|
(entry as? QueueEntry.StreamingTrack)?.track?.id
|
||||||
|
}.toSet(),
|
||||||
|
savedTrackIds = savedIds,
|
||||||
|
blacklistedTrackIds = blacklistedTracks,
|
||||||
|
blacklistedArtistIds = blacklistedArtists,
|
||||||
|
)
|
||||||
|
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), TrackOptionsContext())
|
||||||
|
|
||||||
init {
|
init {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
repository.pluginManager.selectedMetadataPlugin
|
repository.pluginManager.selectedMetadataPlugin
|
||||||
@ -280,12 +310,48 @@ class PlaylistViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is TrackOptionsAction.Download -> {}
|
is TrackOptionsAction.Download -> downloadManager.enqueue(track)
|
||||||
is TrackOptionsAction.ToggleBlacklist -> toggleTrackBlacklist(track)
|
is TrackOptionsAction.ToggleBlacklist -> toggleTrackBlacklist(track)
|
||||||
is TrackOptionsAction.Share -> {}
|
is TrackOptionsAction.Share -> {
|
||||||
is TrackOptionsAction.AddToPlaylist -> {}
|
val uri = track.externalUri?.takeIf { it.isNotBlank() }
|
||||||
|
if (uri != null) {
|
||||||
|
shareService.share(uri, track.title)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
is TrackOptionsAction.AddToPlaylist -> {
|
||||||
|
_tracksToAddToPlaylist.value = listOf(track)
|
||||||
|
_showAddToPlaylistPicker.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addTracksToPlaylist(playlistId: String) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
runCatching {
|
||||||
|
libraryRepository.addTracksToPlaylist(
|
||||||
|
playlistId,
|
||||||
|
_tracksToAddToPlaylist.value.map { it.id },
|
||||||
|
)
|
||||||
|
}.onFailure { e ->
|
||||||
|
logger.e(e) { "Failed to add tracks to playlist" }
|
||||||
|
}.onSuccess {
|
||||||
|
_showAddToPlaylistPicker.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showAddToPlaylistPicker(tracks: List<MetadataTrack>) {
|
||||||
|
_tracksToAddToPlaylist.value = tracks
|
||||||
|
_showAddToPlaylistPicker.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun dismissAddToPlaylistPicker() {
|
||||||
|
_showAddToPlaylistPicker.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun downloadTracks(tracks: List<MetadataTrack>) {
|
||||||
|
tracks.forEach { track -> downloadManager.enqueue(track) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isTrackBlacklisted(track: MetadataTrack): Boolean {
|
fun isTrackBlacklisted(track: MetadataTrack): Boolean {
|
||||||
|
|||||||
@ -18,34 +18,16 @@
|
|||||||
package dev.krtirtho.spotube.modules.saved_tracks
|
package dev.krtirtho.spotube.modules.saved_tracks
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.track.MetadataTrack
|
|
||||||
import dev.krtirtho.spotube.core.audioplayer.AudioPlayer
|
|
||||||
import dev.krtirtho.spotube.core.audioplayer.AudioPlayerInterface
|
import dev.krtirtho.spotube.core.audioplayer.AudioPlayerInterface
|
||||||
import dev.krtirtho.spotube.core.audioplayer.AudioPlayerQueue
|
import dev.krtirtho.spotube.core.audioplayer.AudioPlayerQueue
|
||||||
import dev.krtirtho.spotube.core.audioplayer.PlayerState
|
import dev.krtirtho.spotube.core.audioplayer.PlayerState
|
||||||
import dev.krtirtho.spotube.core.audioplayer.QueueCollectionEntry
|
import dev.krtirtho.spotube.core.audioplayer.QueueCollectionEntry
|
||||||
import dev.krtirtho.spotube.core.audioplayer.QueueEntry
|
|
||||||
import dev.krtirtho.spotube.core.navigation.NavigationCommands
|
import dev.krtirtho.spotube.core.navigation.NavigationCommands
|
||||||
import dev.krtirtho.spotube.core.navigation.Routes
|
import dev.krtirtho.spotube.core.navigation.Routes
|
||||||
import dev.krtirtho.spotube.core.share.ShareService
|
|
||||||
import dev.krtirtho.spotube.core.ui.component.CollectionView
|
import dev.krtirtho.spotube.core.ui.component.CollectionView
|
||||||
import dev.krtirtho.spotube.core.ui.component.TrackOptionsAction
|
|
||||||
import dev.krtirtho.spotube.core.ui.component.TrackOptionsState
|
|
||||||
import dev.krtirtho.spotube.modules.downloads.DownloadsViewModel
|
|
||||||
import dev.krtirtho.spotube.modules.library.LibraryRepository
|
|
||||||
import dev.krtirtho.spotube.modules.library.playlist.AddToPlaylistPicker
|
import dev.krtirtho.spotube.modules.library.playlist.AddToPlaylistPicker
|
||||||
import dev.krtirtho.spotube.modules.playlist.PlaylistViewModel
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import org.koin.compose.koinInject
|
|
||||||
import org.koin.compose.viewmodel.koinViewModel
|
|
||||||
import org.koin.core.parameter.parametersOf
|
|
||||||
import spotube.composeapp.generated.resources.Res
|
import spotube.composeapp.generated.resources.Res
|
||||||
import spotube.composeapp.generated.resources.liked_tracks
|
import spotube.composeapp.generated.resources.liked_tracks
|
||||||
|
|
||||||
@ -54,56 +36,14 @@ fun SavedTracksScreen(
|
|||||||
viewModel: SavedTracksViewModel,
|
viewModel: SavedTracksViewModel,
|
||||||
audioPlayerQueue: AudioPlayerQueue,
|
audioPlayerQueue: AudioPlayerQueue,
|
||||||
audioPlayer: AudioPlayerInterface,
|
audioPlayer: AudioPlayerInterface,
|
||||||
shareService: ShareService,
|
|
||||||
downloadsViewModel: DownloadsViewModel,
|
|
||||||
libraryRepository: LibraryRepository,
|
|
||||||
navigationCommands: NavigationCommands
|
navigationCommands: NavigationCommands
|
||||||
) {
|
) {
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
val queue by audioPlayerQueue.queueFlow.collectAsStateWithLifecycle()
|
|
||||||
val currentQueueEntry by audioPlayerQueue.currentQueueEntryFlow.collectAsStateWithLifecycle()
|
|
||||||
val currentCollectionEntry by audioPlayerQueue.currentCollectionEntryFlow.collectAsStateWithLifecycle()
|
val currentCollectionEntry by audioPlayerQueue.currentCollectionEntryFlow.collectAsStateWithLifecycle()
|
||||||
val playerState by audioPlayer.playerStateFlow.collectAsStateWithLifecycle()
|
val playerState by audioPlayer.playerStateFlow.collectAsStateWithLifecycle()
|
||||||
val blacklistedTrackIds by viewModel.blacklistedTrackIds.collectAsStateWithLifecycle()
|
val currentUserId by viewModel.currentUserId.collectAsStateWithLifecycle()
|
||||||
val blacklistedArtistIds by viewModel.blacklistedArtistIds.collectAsStateWithLifecycle()
|
val trackOptionsContext by viewModel.trackOptionsContext.collectAsStateWithLifecycle()
|
||||||
var showAddToPlaylistPicker by remember { mutableStateOf(false) }
|
val showAddToPlaylistPicker by viewModel.showAddToPlaylistPicker.collectAsStateWithLifecycle()
|
||||||
var tracksToAddToPlaylist by remember { mutableStateOf<List<MetadataTrack>>(emptyList()) }
|
|
||||||
var currentUserId by remember { mutableStateOf<String?>(null) }
|
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
currentUserId = libraryRepository.currentUser()?.id
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getTrackOptionsState(track: MetadataTrack): TrackOptionsState {
|
|
||||||
val currentTrackId = (currentQueueEntry as? QueueEntry.StreamingTrack)?.track?.id
|
|
||||||
val queueTrackIds = queue.mapNotNull { entry ->
|
|
||||||
(entry as? QueueEntry.StreamingTrack)?.track?.id
|
|
||||||
}.toSet()
|
|
||||||
return TrackOptionsState(
|
|
||||||
isInQueue = queueTrackIds.contains(track.id),
|
|
||||||
isCurrentlyPlaying = track.id == currentTrackId,
|
|
||||||
isFavorite = true,
|
|
||||||
isBlacklisted = track.id in blacklistedTrackIds || track.artists.any { it.id in blacklistedArtistIds },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun handleTrackOptionsAction(track: MetadataTrack, action: TrackOptionsAction) {
|
|
||||||
viewModel.handleTrackOptionsAction(track, action)
|
|
||||||
if (action is TrackOptionsAction.Share) {
|
|
||||||
val uri = track.externalUri?.takeIf { it.isNotBlank() }
|
|
||||||
if (uri != null) {
|
|
||||||
shareService.share(uri, track.title)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (action is TrackOptionsAction.Download) {
|
|
||||||
downloadsViewModel.downloadTrack(track)
|
|
||||||
}
|
|
||||||
if (action is TrackOptionsAction.AddToPlaylist) {
|
|
||||||
tracksToAddToPlaylist = listOf(track)
|
|
||||||
showAddToPlaylistPicker = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val dataState = state as? SavedTracksScreenState.Data
|
val dataState = state as? SavedTracksScreenState.Data
|
||||||
val isPlaying = currentCollectionEntry is QueueCollectionEntry.SavedTracks &&
|
val isPlaying = currentCollectionEntry is QueueCollectionEntry.SavedTracks &&
|
||||||
@ -133,40 +73,24 @@ fun SavedTracksScreen(
|
|||||||
tracks = dataState?.tracks ?: emptyList(),
|
tracks = dataState?.tracks ?: emptyList(),
|
||||||
hasMore = dataState?.nextPagination != null,
|
hasMore = dataState?.nextPagination != null,
|
||||||
isLoadingNextPage = state is SavedTracksScreenState.Data.LoadingMore,
|
isLoadingNextPage = state is SavedTracksScreenState.Data.LoadingMore,
|
||||||
currentTrackId = (currentQueueEntry as? QueueEntry.StreamingTrack)?.track?.id,
|
currentTrackId = trackOptionsContext.currentTrackId,
|
||||||
isCurrentTrackPlaying = playerState == PlayerState.PLAYING,
|
isCurrentTrackPlaying = playerState == PlayerState.PLAYING,
|
||||||
onTrackClick = viewModel::playSavedTracksFromTrack,
|
onTrackClick = viewModel::playSavedTracksFromTrack,
|
||||||
onLoadNextPage = viewModel::loadNextTracksPage,
|
onLoadNextPage = viewModel::loadNextTracksPage,
|
||||||
onArtistClick = { navigationCommands.navigateTo(Routes.Artist(it.id)) },
|
onArtistClick = { navigationCommands.navigateTo(Routes.Artist(it.id)) },
|
||||||
onAlbumClick = { navigationCommands.navigateTo(Routes.Album(it.id)) },
|
onAlbumClick = { navigationCommands.navigateTo(Routes.Album(it.id)) },
|
||||||
onTrackOptionsAction = ::handleTrackOptionsAction,
|
onTrackOptionsAction = viewModel::handleTrackOptionsAction,
|
||||||
trackOptionsState = ::getTrackOptionsState,
|
trackOptionsState = trackOptionsContext::stateFor,
|
||||||
onBulkDownload = { tracks ->
|
onBulkDownload = viewModel::downloadTracks,
|
||||||
downloadsViewModel.downloadTracks(tracks)
|
onBulkAddToQueue = viewModel::addTracksToQueue,
|
||||||
},
|
onBulkPlayNext = viewModel::playTracksNext,
|
||||||
onBulkAddToQueue = { tracks ->
|
onBulkAddToPlaylist = viewModel::showAddToPlaylistPicker,
|
||||||
viewModel.addTracksToQueue(tracks)
|
|
||||||
},
|
|
||||||
onBulkPlayNext = { tracks ->
|
|
||||||
viewModel.playTracksNext(tracks)
|
|
||||||
},
|
|
||||||
onBulkAddToPlaylist = { tracks ->
|
|
||||||
tracksToAddToPlaylist = tracks
|
|
||||||
showAddToPlaylistPicker = true
|
|
||||||
},
|
|
||||||
trailingContent = {
|
trailingContent = {
|
||||||
AddToPlaylistPicker(
|
AddToPlaylistPicker(
|
||||||
visible = showAddToPlaylistPicker,
|
visible = showAddToPlaylistPicker,
|
||||||
currentUserId = currentUserId,
|
currentUserId = currentUserId,
|
||||||
onDismiss = { showAddToPlaylistPicker = false },
|
onDismiss = viewModel::dismissAddToPlaylistPicker,
|
||||||
onPlaylistSelected = { playlistId ->
|
onPlaylistSelected = viewModel::addTracksToPlaylist,
|
||||||
scope.launch {
|
|
||||||
libraryRepository.addTracksToPlaylist(
|
|
||||||
playlistId,
|
|
||||||
tracksToAddToPlaylist.map { it.id }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@ -34,10 +34,15 @@ import dev.krtirtho.spotube.core.audioplayer.AudioPlayerQueue
|
|||||||
import dev.krtirtho.spotube.core.audioplayer.QueueEntry
|
import dev.krtirtho.spotube.core.audioplayer.QueueEntry
|
||||||
import dev.krtirtho.spotube.core.di.injectLogger
|
import dev.krtirtho.spotube.core.di.injectLogger
|
||||||
import dev.krtirtho.spotube.core.playback.CollectionPlaybackHelper
|
import dev.krtirtho.spotube.core.playback.CollectionPlaybackHelper
|
||||||
|
import dev.krtirtho.spotube.core.share.ShareService
|
||||||
import dev.krtirtho.spotube.core.ui.component.TrackOptionsAction
|
import dev.krtirtho.spotube.core.ui.component.TrackOptionsAction
|
||||||
|
import dev.krtirtho.spotube.core.ui.component.TrackOptionsContext
|
||||||
import dev.krtirtho.spotube.modules.blacklist.BlacklistRepository
|
import dev.krtirtho.spotube.modules.blacklist.BlacklistRepository
|
||||||
|
import dev.krtirtho.spotube.modules.downloads.DownloadManager
|
||||||
|
import dev.krtirtho.spotube.modules.library.LibraryRepository
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
@ -46,6 +51,7 @@ import kotlinx.coroutines.flow.filterNotNull
|
|||||||
import kotlinx.coroutines.flow.flatMapLatest
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.koin.compose.koinInject
|
import org.koin.compose.koinInject
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
@ -88,6 +94,9 @@ class SavedTracksViewModel(
|
|||||||
private val playbackHelper: CollectionPlaybackHelper,
|
private val playbackHelper: CollectionPlaybackHelper,
|
||||||
private val audioPlayerQueue: AudioPlayerQueue,
|
private val audioPlayerQueue: AudioPlayerQueue,
|
||||||
private val blacklistRepository: BlacklistRepository,
|
private val blacklistRepository: BlacklistRepository,
|
||||||
|
private val libraryRepository: LibraryRepository,
|
||||||
|
private val shareService: ShareService,
|
||||||
|
private val downloadManager: DownloadManager,
|
||||||
) : ViewModel(), KoinComponent {
|
) : ViewModel(), KoinComponent {
|
||||||
private val logger by injectLogger<SavedTracksViewModel>()
|
private val logger by injectLogger<SavedTracksViewModel>()
|
||||||
|
|
||||||
@ -96,12 +105,36 @@ class SavedTracksViewModel(
|
|||||||
val savedTrackIdsFlow: StateFlow<Set<String>>
|
val savedTrackIdsFlow: StateFlow<Set<String>>
|
||||||
get() = repository.savedTracksIdsFlow
|
get() = repository.savedTracksIdsFlow
|
||||||
|
|
||||||
|
private val _currentUserId = MutableStateFlow<String?>(null)
|
||||||
|
val currentUserId: StateFlow<String?> = _currentUserId.asStateFlow()
|
||||||
|
|
||||||
private val _blacklistedTrackIds = MutableStateFlow<Set<String>>(emptySet())
|
private val _blacklistedTrackIds = MutableStateFlow<Set<String>>(emptySet())
|
||||||
val blacklistedTrackIds: StateFlow<Set<String>> = _blacklistedTrackIds.asStateFlow()
|
val blacklistedTrackIds: StateFlow<Set<String>> = _blacklistedTrackIds.asStateFlow()
|
||||||
|
|
||||||
private val _blacklistedArtistIds = MutableStateFlow<Set<String>>(emptySet())
|
private val _blacklistedArtistIds = MutableStateFlow<Set<String>>(emptySet())
|
||||||
val blacklistedArtistIds: StateFlow<Set<String>> = _blacklistedArtistIds.asStateFlow()
|
val blacklistedArtistIds: StateFlow<Set<String>> = _blacklistedArtistIds.asStateFlow()
|
||||||
|
|
||||||
|
private val _tracksToAddToPlaylist = MutableStateFlow<List<MetadataTrack>>(emptyList())
|
||||||
|
private val _showAddToPlaylistPicker = MutableStateFlow(false)
|
||||||
|
val showAddToPlaylistPicker: StateFlow<Boolean> = _showAddToPlaylistPicker.asStateFlow()
|
||||||
|
|
||||||
|
val trackOptionsContext: StateFlow<TrackOptionsContext> = combine(
|
||||||
|
audioPlayerQueue.queueFlow,
|
||||||
|
audioPlayerQueue.currentQueueEntryFlow,
|
||||||
|
_blacklistedTrackIds,
|
||||||
|
_blacklistedArtistIds,
|
||||||
|
) { queue, currentEntry, blacklistedTracks, blacklistedArtists ->
|
||||||
|
TrackOptionsContext(
|
||||||
|
currentTrackId = (currentEntry as? QueueEntry.StreamingTrack)?.track?.id,
|
||||||
|
queueTrackIds = queue.mapNotNull { entry ->
|
||||||
|
(entry as? QueueEntry.StreamingTrack)?.track?.id
|
||||||
|
}.toSet(),
|
||||||
|
blacklistedTrackIds = blacklistedTracks,
|
||||||
|
blacklistedArtistIds = blacklistedArtists,
|
||||||
|
forceFavorite = true,
|
||||||
|
)
|
||||||
|
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), TrackOptionsContext(forceFavorite = true))
|
||||||
|
|
||||||
init {
|
init {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
combine(
|
combine(
|
||||||
@ -112,6 +145,7 @@ class SavedTracksViewModel(
|
|||||||
repository.savedTracksIdsFlow
|
repository.savedTracksIdsFlow
|
||||||
) { _, _ -> }.collect {
|
) { _, _ -> }.collect {
|
||||||
loadInitialData()
|
loadInitialData()
|
||||||
|
loadCurrentUser()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
blacklistRepository.blacklistedTracks
|
blacklistRepository.blacklistedTracks
|
||||||
@ -122,6 +156,14 @@ class SavedTracksViewModel(
|
|||||||
.launchIn(viewModelScope)
|
.launchIn(viewModelScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun loadCurrentUser() {
|
||||||
|
runCatching {
|
||||||
|
_currentUserId.value = libraryRepository.currentUser()?.id
|
||||||
|
}.onFailure { e ->
|
||||||
|
logger.e(e) { "Failed to load current user" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun loadInitialData() {
|
private suspend fun loadInitialData() {
|
||||||
runCatching {
|
runCatching {
|
||||||
val tracksResult = repository.getSavedTracks()
|
val tracksResult = repository.getSavedTracks()
|
||||||
@ -210,12 +252,48 @@ class SavedTracksViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
is TrackOptionsAction.ToggleFavorite -> {}
|
is TrackOptionsAction.ToggleFavorite -> {}
|
||||||
is TrackOptionsAction.Download -> {}
|
is TrackOptionsAction.Download -> downloadManager.enqueue(track)
|
||||||
is TrackOptionsAction.ToggleBlacklist -> toggleTrackBlacklist(track)
|
is TrackOptionsAction.ToggleBlacklist -> toggleTrackBlacklist(track)
|
||||||
is TrackOptionsAction.Share -> {}
|
is TrackOptionsAction.Share -> {
|
||||||
is TrackOptionsAction.AddToPlaylist -> {}
|
val uri = track.externalUri?.takeIf { it.isNotBlank() }
|
||||||
|
if (uri != null) {
|
||||||
|
shareService.share(uri, track.title)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
is TrackOptionsAction.AddToPlaylist -> {
|
||||||
|
_tracksToAddToPlaylist.value = listOf(track)
|
||||||
|
_showAddToPlaylistPicker.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addTracksToPlaylist(playlistId: String) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
runCatching {
|
||||||
|
libraryRepository.addTracksToPlaylist(
|
||||||
|
playlistId,
|
||||||
|
_tracksToAddToPlaylist.value.map { it.id },
|
||||||
|
)
|
||||||
|
}.onFailure { e ->
|
||||||
|
logger.e(e) { "Failed to add tracks to playlist" }
|
||||||
|
}.onSuccess {
|
||||||
|
_showAddToPlaylistPicker.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showAddToPlaylistPicker(tracks: List<MetadataTrack>) {
|
||||||
|
_tracksToAddToPlaylist.value = tracks
|
||||||
|
_showAddToPlaylistPicker.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun dismissAddToPlaylistPicker() {
|
||||||
|
_showAddToPlaylistPicker.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun downloadTracks(tracks: List<MetadataTrack>) {
|
||||||
|
tracks.forEach { track -> downloadManager.enqueue(track) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isTrackBlacklisted(track: MetadataTrack): Boolean {
|
fun isTrackBlacklisted(track: MetadataTrack): Boolean {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user