Skip to content

Commit

Permalink
Implemented favorite toggle button on player
Browse files Browse the repository at this point in the history
  • Loading branch information
aidewoode committed Mar 8, 2024
1 parent cfca458 commit f5af4d7
Show file tree
Hide file tree
Showing 11 changed files with 192 additions and 56 deletions.
18 changes: 18 additions & 0 deletions app/src/main/java/org/blackcandy/android/api/BlackCandyService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ interface BlackCandyService {
suspend fun destroyAuthentication()

suspend fun getSongsFromCurrentPlaylist(): List<Song>

suspend fun addSongToFavorite(songId: Int): Song

suspend fun deleteSongFromFavorite(songId: Int): Song
}

class BlackCandyServiceImpl(
Expand Down Expand Up @@ -81,4 +85,18 @@ class BlackCandyServiceImpl(
override suspend fun getSongsFromCurrentPlaylist(): List<Song> {
return client.get("current_playlist/songs").body()
}

override suspend fun addSongToFavorite(songId: Int): Song {
return client.submitForm(
url = "favorite_playlist/songs",
formParameters =
parameters {
append("song_id", songId.toString())
},
).body()
}

override suspend fun deleteSongFromFavorite(songId: Int): Song {
return client.delete("favorite_playlist/songs/$songId").body()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
Expand All @@ -28,7 +27,7 @@ import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import org.blackcandy.android.R
import org.blackcandy.android.models.AlertMessage
import org.blackcandy.android.utils.SnackbarUtil.Companion.ShowSnackbar
import org.blackcandy.android.viewmodels.LoginRoute
import org.blackcandy.android.viewmodels.LoginViewModel
import org.koin.androidx.compose.koinViewModel
Expand Down Expand Up @@ -97,15 +96,8 @@ fun LoginScreen(
}

uiState.alertMessage?.let { alertMessage ->
val snackbarText =
when (alertMessage) {
is AlertMessage.String -> alertMessage.value
is AlertMessage.StringResource -> stringResource(alertMessage.value)
}

LaunchedEffect(snackbarHostState) {
snackbarHostState.showSnackbar(snackbarText)
viewModel.snackbarMessageShown()
ShowSnackbar(alertMessage, snackbarHostState) {
viewModel.alertMessageShown()
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.FilledIconToggleButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconToggleButton
import androidx.compose.material3.IconButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import org.blackcandy.android.R
Expand All @@ -17,7 +18,9 @@ import org.blackcandy.android.models.PlaybackMode
fun PlayerActions(
modifier: Modifier = Modifier,
playbackMode: PlaybackMode,
isFavorited: Boolean,
onModeSwitchButtonClicked: () -> Unit,
onFavoriteButtonClicked: () -> Unit,
) {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
Expand All @@ -35,14 +38,21 @@ fun PlayerActions(
)
}

IconToggleButton(
checked = false,
onCheckedChange = {},
IconButton(
onClick = onFavoriteButtonClicked,
) {
Icon(
painter = painterResource(R.drawable.baseline_favorite_border_24),
contentDescription = stringResource(R.string.unfavorited),
)
if (isFavorited) {
Icon(
painter = painterResource(R.drawable.baseline_favorite_24),
contentDescription = stringResource(R.string.favorited),
tint = Color.Red,
)
} else {
Icon(
painter = painterResource(R.drawable.baseline_favorite_border_24),
contentDescription = stringResource(R.string.unfavorited),
)
}
}

FilledIconToggleButton(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,58 +3,80 @@ package org.blackcandy.android.compose.player
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.dimensionResource
import org.blackcandy.android.R
import org.blackcandy.android.utils.SnackbarUtil.Companion.ShowSnackbar
import org.blackcandy.android.viewmodels.PlayerViewModel
import org.koin.androidx.compose.koinViewModel

@Composable
fun PlayerScreen(viewModel: PlayerViewModel = koinViewModel()) {
fun PlayerScreen(
snackbarHostState: SnackbarHostState = remember { SnackbarHostState() },
viewModel: PlayerViewModel = koinViewModel(),
) {
val uiState by viewModel.uiState.collectAsState()

Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier =
Modifier
.padding(horizontal = dimensionResource(R.dimen.padding_medium))
.padding(bottom = dimensionResource(R.dimen.padding_small)),
Scaffold(
snackbarHost = { SnackbarHost(snackbarHostState) },
containerColor = Color.Transparent,
) {
Spacer(modifier = Modifier.weight(1f))

Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier =
Modifier
.padding(it)
.padding(horizontal = dimensionResource(R.dimen.padding_medium)),
) {
PlayerInfo(currentSong = uiState.musicState.currentSong)
PlayerControl(
Spacer(modifier = Modifier.weight(1f))

Column(
horizontalAlignment = Alignment.CenterHorizontally,
) {
PlayerInfo(currentSong = uiState.musicState.currentSong)
PlayerControl(
modifier =
Modifier
.padding(top = dimensionResource(R.dimen.padding_medium)),
isPlaying = uiState.musicState.isPlaying,
currentPosition = uiState.currentPosition,
duration = uiState.musicState.currentSong?.duration ?: 0.0,
enabled = uiState.musicState.hasCurrentSong,
onPreviousButtonClicked = { viewModel.previous() },
onNextButtonClicked = { viewModel.next() },
onPlayButtonClicked = { viewModel.play() },
onPauseButtonClicked = { viewModel.pause() },
onSeek = { viewModel.seekTo(it) },
)
}

Spacer(modifier = Modifier.weight(1f))

PlayerActions(
modifier =
Modifier
.padding(top = dimensionResource(R.dimen.padding_medium)),
isPlaying = uiState.musicState.isPlaying,
currentPosition = uiState.currentPosition,
duration = uiState.musicState.currentSong?.duration ?: 0.0,
enabled = uiState.musicState.hasCurrentSong,
onPreviousButtonClicked = { viewModel.previous() },
onNextButtonClicked = { viewModel.next() },
onPlayButtonClicked = { viewModel.play() },
onPauseButtonClicked = { viewModel.pause() },
onSeek = { viewModel.seekTo(it) },
.padding(top = dimensionResource(R.dimen.padding_medium))
.padding(horizontal = dimensionResource(R.dimen.padding_small)),
playbackMode = uiState.musicState.playbackMode,
isFavorited = uiState.musicState.currentSong?.isFavorited ?: false,
onModeSwitchButtonClicked = { viewModel.nextMode() },
onFavoriteButtonClicked = { viewModel.toggleFavorite() },
)
}

Spacer(modifier = Modifier.weight(1f))

PlayerActions(
modifier =
Modifier
.padding(top = dimensionResource(R.dimen.padding_medium))
.padding(horizontal = dimensionResource(R.dimen.padding_small)),
playbackMode = uiState.musicState.playbackMode,
onModeSwitchButtonClicked = { viewModel.nextMode() },
)
uiState.alertMessage?.let { alertMessage ->
ShowSnackbar(alertMessage, snackbarHostState) {
viewModel.alertMessageShown()
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package org.blackcandy.android.data

import org.blackcandy.android.api.BlackCandyService
import org.blackcandy.android.models.Song

class FavoritePlaylistRepository(
private val service: BlackCandyService,
) {
suspend fun addSong(songId: Int): Song {
return service.addSongToFavorite(songId)
}

suspend fun deleteSong(songId: Int): Song {
return service.deleteSongFromFavorite(songId)
}
}
4 changes: 3 additions & 1 deletion app/src/main/java/org/blackcandy/android/di/AppModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import org.blackcandy.android.api.BlackCandyService
import org.blackcandy.android.api.BlackCandyServiceImpl
import org.blackcandy.android.data.CurrentPlaylistRepository
import org.blackcandy.android.data.EncryptedPreferencesDataSource
import org.blackcandy.android.data.FavoritePlaylistRepository
import org.blackcandy.android.data.PreferencesDataSource
import org.blackcandy.android.data.ServerAddressRepository
import org.blackcandy.android.data.SystemInfoRepository
Expand Down Expand Up @@ -80,14 +81,15 @@ val appModule =
single { SystemInfoRepository(get()) }
single { UserRepository(get(), get(), get(named("UserDataStore")), get(), get()) }
single { CurrentPlaylistRepository(get()) }
single { FavoritePlaylistRepository(get()) }

viewModel { LoginViewModel(get(), get(), get()) }
viewModel { MainViewModel(get(), get(), get()) }
viewModel { AccountSheetViewModel(get(), get()) }
viewModel { NavHostViewModel(get()) }
viewModel { HomeViewModel(get()) }
viewModel { MiniPlayerViewModel(get()) }
viewModel { PlayerViewModel(get()) }
viewModel { PlayerViewModel(get(), get()) }
}

private const val DATASTORE_PREFERENCES_NAME = "user_preferences"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,7 @@ class MusicServiceController(
}

if (events.contains(Player.EVENT_MEDIA_ITEM_TRANSITION)) {
val currentMediaId = player.currentMediaItem?.mediaId
val currentSong = _musicState.value.playlist.find { it.id.toString() == currentMediaId }

_musicState.update { it.copy(currentSong = currentSong) }
updateCurrentSong()
}

if (events.contains(Player.EVENT_IS_PLAYING_CHANGED)) {
Expand Down Expand Up @@ -125,6 +122,8 @@ class MusicServiceController(
)

_musicState.update { it.copy(playlist = songs) }

updateCurrentSong()
}

fun play() {
Expand Down Expand Up @@ -186,4 +185,11 @@ class MusicServiceController(

_musicState.update { it.copy(playbackMode = playbackMode) }
}

private fun updateCurrentSong() {
val currentMediaId = controller?.currentMediaItem?.mediaId
val currentSong = _musicState.value.playlist.find { it.id.toString() == currentMediaId }

_musicState.update { it.copy(currentSong = currentSong) }
}
}
29 changes: 29 additions & 0 deletions app/src/main/java/org/blackcandy/android/utils/SnackbarUtil.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package org.blackcandy.android.utils

import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.res.stringResource
import org.blackcandy.android.models.AlertMessage

class SnackbarUtil {
companion object {
@Composable
fun ShowSnackbar(
message: AlertMessage,
state: SnackbarHostState,
onShowed: () -> Unit,
) {
val snackbarText =
when (message) {
is AlertMessage.String -> message.value
is AlertMessage.StringResource -> stringResource(message.value)
}

LaunchedEffect(state) {
state.showSnackbar(snackbarText)
onShowed()
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ class LoginViewModel(
}
}

fun snackbarMessageShown() {
fun alertMessageShown() {
_uiState.update { it.copy(alertMessage = null) }
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,22 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import org.blackcandy.android.data.FavoritePlaylistRepository
import org.blackcandy.android.media.MusicServiceController
import org.blackcandy.android.models.AlertMessage
import org.blackcandy.android.models.MusicState

data class PlayerUiState(
val musicState: MusicState = MusicState(),
val currentPosition: Double = 0.0,
val alertMessage: AlertMessage? = null,
)

class PlayerViewModel(
private val musicServiceController: MusicServiceController,
private val favoritePlaylistRepository: FavoritePlaylistRepository,
) : ViewModel() {
private val _uiState = MutableStateFlow(PlayerUiState())

Expand Down Expand Up @@ -58,4 +64,34 @@ class PlayerViewModel(
fun nextMode() {
musicServiceController.setPlaybackMode(uiState.value.musicState.playbackMode.next)
}

fun toggleFavorite() {
val currentSong = uiState.value.musicState.currentSong ?: return

viewModelScope.launch {
try {
val toggledSong =
if (currentSong.isFavorited) {
favoritePlaylistRepository.deleteSong(currentSong.id)
} else {
favoritePlaylistRepository.addSong(currentSong.id)
}

val updatedPlaylist =
uiState.value.musicState.playlist.map { song ->
if (song.id == toggledSong.id) toggledSong else song
}

musicServiceController.updatePlaylist(updatedPlaylist)
} catch (exception: Exception) {
exception.message?.let { message ->
_uiState.update { it.copy(alertMessage = AlertMessage.String(message)) }
}
}
}
}

fun alertMessageShown() {
_uiState.update { it.copy(alertMessage = null) }
}
}
Loading

0 comments on commit f5af4d7

Please sign in to comment.