diff --git a/.idea/gradle.xml b/.idea/gradle.xml index c19de999..7a345ede 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -9,14 +9,14 @@ diff --git a/app/src/main/java/luci/sixsixsix/powerampache2/common/Extensions.kt b/app/src/main/java/luci/sixsixsix/powerampache2/common/Extensions.kt index e76ba6dd..2aa5e282 100644 --- a/app/src/main/java/luci/sixsixsix/powerampache2/common/Extensions.kt +++ b/app/src/main/java/luci/sixsixsix/powerampache2/common/Extensions.kt @@ -62,7 +62,6 @@ import luci.sixsixsix.powerampache2.R import luci.sixsixsix.powerampache2.common.Constants.PLAY_STORE_URL import luci.sixsixsix.powerampache2.domain.common.Constants.PLUGIN_CHROMECAST_ACTIVITY_ID import luci.sixsixsix.powerampache2.domain.common.Constants.PLUGIN_CHROMECAST_ID -import luci.sixsixsix.powerampache2.domain.models.Song import java.io.File import java.io.FileInputStream import java.io.FileOutputStream @@ -102,7 +101,7 @@ fun Context.openLinkInBrowser(link: String) = } ) -fun Context.exportSong(song: Song, offlineUri: String) { +fun Context.exportSong(mimeType: String?, offlineUri: String) { val fileWithinAppDir = File(offlineUri) val fileUri = FileProvider.getUriForFile(this, getString(R.string.sharing_provider_authority), @@ -113,7 +112,7 @@ fun Context.exportSong(song: Song, offlineUri: String) { startActivity( Intent.createChooser( Intent(Intent.ACTION_SEND).apply { - type = song.mime + type = mimeType setDataAndType(fileUri, contentResolver.getType(fileUri)) putExtra(Intent.EXTRA_STREAM, fileUri) addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) diff --git a/app/src/main/java/luci/sixsixsix/powerampache2/common/Mappers.kt b/app/src/main/java/luci/sixsixsix/powerampache2/common/Mappers.kt index a987a981..2f6089da 100644 --- a/app/src/main/java/luci/sixsixsix/powerampache2/common/Mappers.kt +++ b/app/src/main/java/luci/sixsixsix/powerampache2/common/Mappers.kt @@ -4,9 +4,9 @@ import android.net.Uri import androidx.media3.common.MediaItem import androidx.media3.common.MediaMetadata import androidx.media3.common.StarRating -import luci.sixsixsix.powerampache2.domain.models.Song +import luci.sixsixsix.powerampache2.presentation.models.SongUI -fun Song.toMediaItem(songUri: String) = MediaItem.Builder() +fun SongUI.toMediaItem(songUri: String) = MediaItem.Builder() .setMediaId(mediaId) .setUri(songUri) .setMimeType(mime) @@ -25,8 +25,12 @@ fun Song.toMediaItem(songUri: String) = MediaItem.Builder() .setGenre(if (genre.isNotEmpty()) { genre[0].name } else null) .setComposer(composer) .setAlbumArtist(albumArtist.name) - .setOverallRating(StarRating(5, if (averageRating in 0f..5f) averageRating.toFloat() else 0f)) + .setOverallRating(StarRating( + 5, + if (averageRating in 0f..5f) averageRating else 0f)) .setReleaseYear(year) - .setUserRating(StarRating(5, if (rating in 0f..5f) rating.toFloat() else 0f)) - .build() + .setUserRating(StarRating( + 5, + if (rating in 0f..5f) rating else 0f) + ).build() ).build() \ No newline at end of file diff --git a/app/src/main/java/luci/sixsixsix/powerampache2/common/ShareManagerImpl.kt b/app/src/main/java/luci/sixsixsix/powerampache2/common/ShareManagerImpl.kt index 818b317c..47a4a9ee 100644 --- a/app/src/main/java/luci/sixsixsix/powerampache2/common/ShareManagerImpl.kt +++ b/app/src/main/java/luci/sixsixsix/powerampache2/common/ShareManagerImpl.kt @@ -46,6 +46,7 @@ class ShareManagerImpl @Inject constructor( .append("/share") .append("/song") .append("/${song.id}") + // TODO: possibly blocking call in non-blocking context warnings .append("/${URLEncoder.encode(song.title, "UTF-8")}") .append("/${URLEncoder.encode(song.album.name,"UTF-8")}") .append("/${URLEncoder.encode(song.artist.name,"UTF-8")}") @@ -77,8 +78,8 @@ class ShareManagerImpl @Inject constructor( id: String, title: String, artist: String, - songCallback: (song: Song) -> Unit, - songsCallback: (songs: List) -> Unit, + songCallback: suspend (song: Song) -> Unit, + songsCallback: suspend (songs: List) -> Unit, errorCallback: () -> Unit ) { val song = getSongFromIdUseCase(id) diff --git a/app/src/main/java/luci/sixsixsix/powerampache2/common/delegates/FetchArtistSongsHandler.kt b/app/src/main/java/luci/sixsixsix/powerampache2/common/delegates/FetchArtistSongsHandler.kt index 77521ad1..19772d85 100644 --- a/app/src/main/java/luci/sixsixsix/powerampache2/common/delegates/FetchArtistSongsHandler.kt +++ b/app/src/main/java/luci/sixsixsix/powerampache2/common/delegates/FetchArtistSongsHandler.kt @@ -1,13 +1,13 @@ package luci.sixsixsix.powerampache2.common.delegates -import luci.sixsixsix.powerampache2.domain.models.Song +import luci.sixsixsix.powerampache2.presentation.models.SongUI interface FetchArtistSongsHandler { suspend fun getSongsFromArtist( artistId: String, isOfflineMode: Boolean, fetchRemote: Boolean = true, - songsCallback: (List) -> Unit, + songsCallback: (List) -> Unit, loadingCallback: (Boolean) -> Unit = { }, errorCallback: (Throwable?) -> Unit = { } ) diff --git a/app/src/main/java/luci/sixsixsix/powerampache2/common/delegates/FetchArtistSongsHandlerImpl.kt b/app/src/main/java/luci/sixsixsix/powerampache2/common/delegates/FetchArtistSongsHandlerImpl.kt index d0877554..53559a5f 100644 --- a/app/src/main/java/luci/sixsixsix/powerampache2/common/delegates/FetchArtistSongsHandlerImpl.kt +++ b/app/src/main/java/luci/sixsixsix/powerampache2/common/delegates/FetchArtistSongsHandlerImpl.kt @@ -1,17 +1,20 @@ package luci.sixsixsix.powerampache2.common.delegates import luci.sixsixsix.powerampache2.common.Resource -import luci.sixsixsix.powerampache2.domain.models.Song import luci.sixsixsix.powerampache2.domain.usecase.artists.SongsFromArtistUseCase +import luci.sixsixsix.powerampache2.domain.usecase.songs.IsSongAvailableOfflineUseCase +import luci.sixsixsix.powerampache2.presentation.models.SongUI +import luci.sixsixsix.powerampache2.presentation.models.toSongUI class FetchArtistSongsHandlerImpl( private val songsFromArtistUseCase: SongsFromArtistUseCase, + private val isSongAvailableOfflineUseCase: IsSongAvailableOfflineUseCase, ): FetchArtistSongsHandler { override suspend fun getSongsFromArtist( artistId: String, isOfflineMode: Boolean, fetchRemote: Boolean, - songsCallback: (List) -> Unit, + songsCallback: (List) -> Unit, loadingCallback: (Boolean) -> Unit, errorCallback: (Throwable?) -> Unit ) { @@ -24,7 +27,9 @@ class FetchArtistSongsHandlerImpl( // check against network data but use db data. // OR if in offline mode result.data?.let { songs -> - songsCallback(songs) + songsCallback(songs.toSongUI { + isSongAvailableOfflineUseCase(it) + }) } } } diff --git a/app/src/main/java/luci/sixsixsix/powerampache2/di/AppModule.kt b/app/src/main/java/luci/sixsixsix/powerampache2/di/AppModule.kt index e0e7b41b..43b1015f 100644 --- a/app/src/main/java/luci/sixsixsix/powerampache2/di/AppModule.kt +++ b/app/src/main/java/luci/sixsixsix/powerampache2/di/AppModule.kt @@ -21,7 +21,6 @@ */ package luci.sixsixsix.powerampache2.di -import android.app.Application import android.content.Context import android.util.Log import androidx.annotation.OptIn @@ -38,12 +37,9 @@ import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent -import kotlinx.coroutines.CoroutineScope import luci.sixsixsix.powerampache2.R import luci.sixsixsix.powerampache2.common.ConfigProviderImpl import luci.sixsixsix.powerampache2.common.DataStringsProviderImpl -import luci.sixsixsix.powerampache2.alarm.PingScheduler -import luci.sixsixsix.powerampache2.domain.utils.AlarmScheduler import luci.sixsixsix.powerampache2.domain.utils.ConfigProvider import luci.sixsixsix.powerampache2.domain.utils.DataStringsProvider import luci.sixsixsix.powerampache2.domain.utils.SharedPreferencesManager diff --git a/app/src/main/java/luci/sixsixsix/powerampache2/di/BindModule.kt b/app/src/main/java/luci/sixsixsix/powerampache2/di/BindModule.kt index 92deae01..1d7d78f5 100644 --- a/app/src/main/java/luci/sixsixsix/powerampache2/di/BindModule.kt +++ b/app/src/main/java/luci/sixsixsix/powerampache2/di/BindModule.kt @@ -21,19 +21,15 @@ */ package luci.sixsixsix.powerampache2.di -import android.app.Application import dagger.Binds import dagger.Module -import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent -import kotlinx.coroutines.CoroutineScope import luci.sixsixsix.powerampache2.alarm.PingScheduler import luci.sixsixsix.powerampache2.common.ShareManagerImpl import luci.sixsixsix.powerampache2.common.WorkerHelperImpl import luci.sixsixsix.powerampache2.domain.utils.AlarmScheduler import luci.sixsixsix.powerampache2.domain.utils.ShareManager -import luci.sixsixsix.powerampache2.domain.utils.SharedPreferencesManager import luci.sixsixsix.powerampache2.domain.utils.WorkerHelper import javax.inject.Singleton diff --git a/app/src/main/java/luci/sixsixsix/powerampache2/player/MusicController.kt b/app/src/main/java/luci/sixsixsix/powerampache2/player/MusicController.kt index 0b53462c..7516b736 100644 --- a/app/src/main/java/luci/sixsixsix/powerampache2/player/MusicController.kt +++ b/app/src/main/java/luci/sixsixsix/powerampache2/player/MusicController.kt @@ -78,7 +78,7 @@ class MusicController @Inject constructor( initController(context) } - // There are 2 ways to stop the music using the timer, if the setting"sleepTimerWaitSongEnd" + // There are 2 ways to stop the music using the timer, if the setting "sleepTimerWaitSongEnd" // is not enabled, stop the music right away, to do so, listen to the alarm event sent // through sleepTimerEventBus.sleepTimerEvents. // If the setting"sleepTimerWaitSongEnd" is enabled, ignore the callback from the alarm @@ -100,6 +100,7 @@ class MusicController @Inject constructor( // Callback triggered every time a new song is being played applicationCoroutineScope.launch { + // TODO: unused named lambda parameter, check if needed playlistManager.currentSongState.filterNotNull().collectLatest { newSong -> val sleepTimerEndTimestamp = sleepTimerEndTimestampFlow().value if ( diff --git a/app/src/main/java/luci/sixsixsix/powerampache2/player/MusicPlaylistManager.kt b/app/src/main/java/luci/sixsixsix/powerampache2/player/MusicPlaylistManager.kt index e93134bc..1f9e3791 100644 --- a/app/src/main/java/luci/sixsixsix/powerampache2/player/MusicPlaylistManager.kt +++ b/app/src/main/java/luci/sixsixsix/powerampache2/player/MusicPlaylistManager.kt @@ -24,26 +24,27 @@ package luci.sixsixsix.powerampache2.player import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import luci.sixsixsix.mrlog.L -import luci.sixsixsix.powerampache2.domain.common.reduceList -import luci.sixsixsix.powerampache2.domain.models.Song +import luci.sixsixsix.powerampache2.presentation.models.reduceList +import luci.sixsixsix.powerampache2.presentation.models.SongUI import javax.inject.Inject import javax.inject.Singleton @Singleton class MusicPlaylistManager @Inject constructor() { - private val _currentSongState = MutableStateFlow(null) - val currentSongState: StateFlow = _currentSongState //val currentSong = _currentSong.asStateFlow() + private val _currentSongState = MutableStateFlow(null) + val currentSongState: StateFlow = _currentSongState //val currentSong = _currentSong.asStateFlow() private val _currentSearchQuery = MutableStateFlow("") val currentSearchQuery: StateFlow = _currentSearchQuery - private val _currentQueueState = MutableStateFlow(listOf()) - val currentQueueState: StateFlow> = _currentQueueState + private val _currentQueueState = MutableStateFlow(listOf()) + val currentQueueState: StateFlow> = _currentQueueState - private val _downloadedSongFlow = MutableStateFlow(null) - val downloadedSongFlow: StateFlow = _downloadedSongFlow + private val _downloadedSongFlow = MutableStateFlow(null) + // TODO: is this needed? + val downloadedSongFlow: StateFlow = _downloadedSongFlow - fun updateDownloadedSong(song: Song?) { + fun updateDownloadedSong(song: SongUI?) { _downloadedSongFlow.value = song } @@ -57,7 +58,7 @@ class MusicPlaylistManager @Inject constructor() { * one that is currently playing. Add a list of song to the queue state,if no song is currently * set as state, automatically set the first song of the queue */ - fun addToCurrentQueueUpdateTopSong(newSong: Song, newQueue: List) { + fun addToCurrentQueueUpdateTopSong(newSong: SongUI, newQueue: List) { // add the current song on top of the queue val updatedQueue = ArrayList(_currentQueueState.value).apply { remove(newSong) @@ -77,7 +78,7 @@ class MusicPlaylistManager @Inject constructor() { /** * used in the callback when music player goes to the next song in the playlist */ - fun updateCurrentSong(newSong: Song?) { + fun updateCurrentSong(newSong: SongUI?) { L( "MusicPlaylistManager updateCurrentSong", newSong) _currentSongState.value = newSong } @@ -86,19 +87,19 @@ class MusicPlaylistManager @Inject constructor() { * same as updateCurrentSong but also provides current queue * TODO unused function */ - fun moveToSongInQueue(newSong: Song?, queue: List) = newSong?.let { - L( "MusicPlaylistManager moveToSongInQueue", newSong) - _currentSongState.value = newSong - } + //fun moveToSongInQueue(newSong: SongUI?, queue: List) = newSong?.let { + // L( "MusicPlaylistManager moveToSongInQueue", newSong) + // _currentSongState.value = newSong + //} - fun replaceCurrentQueue(newQueue: List) { + fun replaceCurrentQueue(newQueue: List) { L( "MusicPlaylistManager replaceCurrentQueue", newQueue.size) - _currentQueueState.value = newQueue.filterNotNull().reduceList() + _currentQueueState.value = newQueue.reduceList() checkCurrentSong() } - fun replaceQueuePlaySong(newQueue: List, songToPlay: Song) { - _currentQueueState.value = newQueue.filterNotNull().reduceList() + fun replaceQueuePlaySong(newQueue: List, songToPlay: SongUI) { + _currentQueueState.value = newQueue.reduceList() _currentSongState.value = songToPlay } @@ -106,7 +107,7 @@ class MusicPlaylistManager @Inject constructor() { * add a list of song to the queue state * if no song is currently set as state, automatically set the first song of the queue */ - fun addToCurrentQueue(newQueue: List) { + fun addToCurrentQueue(newQueue: List) { L( "MusicPlaylistManager addToCurrentQueue", newQueue.size) _currentQueueState.value = LinkedHashSet(_currentQueueState.value) .apply { addAll(newQueue) } @@ -118,7 +119,7 @@ class MusicPlaylistManager @Inject constructor() { /** * adds the song to the current queue if the song is not null */ - fun addToCurrentQueue(newSong: Song?) = newSong?.let { + fun addToCurrentQueue(newSong: SongUI?) = newSong?.let { L( "MusicPlaylistManager addToCurrentQueue", newSong) addToCurrentQueue(listOf(newSong)) } @@ -126,9 +127,9 @@ class MusicPlaylistManager @Inject constructor() { /** * removes a list of songs from the current queue */ - fun removeFromCurrentQueue(songsToRemove: List) { + fun removeFromCurrentQueue(songsToRemove: List) { _currentQueueState.value = LinkedHashSet(_currentQueueState.value) - .apply { removeAll(songsToRemove.filterNotNull().toSet()) } + .apply { removeAll(songsToRemove.toSet()) } .toList() // if the queue is empty after this operation also remove the current song if (_currentQueueState.value.isEmpty()) { @@ -140,13 +141,13 @@ class MusicPlaylistManager @Inject constructor() { /** * remove a single song from queue */ - fun removeFromCurrentQueue(songToRemove: Song) = + fun removeFromCurrentQueue(songToRemove: SongUI) = removeFromCurrentQueue(listOf(songToRemove)) /** * add items to the current queue as next in queue */ - fun addToCurrentQueueNext(list: List) { + fun addToCurrentQueueNext(list: List) { L( "MusicPlaylistManager addToCurrentQueueNext", list.size) val queue = ArrayList(_currentQueueState.value) .apply { @@ -167,9 +168,9 @@ class MusicPlaylistManager @Inject constructor() { replaceCurrentQueue(queue) } - fun addToCurrentQueueTop(list: List) { + fun addToCurrentQueueTop(list: List) { L( "MusicPlaylistManager addToCurrentQueueTop", list.size) - val queue = ArrayList(currentQueueState.value).apply { + val queue = ArrayList(currentQueueState.value).apply { addAll(0, list) } replaceCurrentQueue(queue) @@ -187,7 +188,7 @@ class MusicPlaylistManager @Inject constructor() { /** * assign the new song state, remove the song from the queue if exists and re-add it on top */ - fun updateTopSong(newSong: Song) { + fun updateTopSong(newSong: SongUI) { L("MusicPlaylistManager updateTopSong", newSong) _currentSongState.value = newSong // add the current song on top of the queue @@ -197,7 +198,7 @@ class MusicPlaylistManager @Inject constructor() { } } - fun addToCurrentQueueNext(song: Song?) = song?.let { + fun addToCurrentQueueNext(song: SongUI?) = song?.let { L( "MusicPlaylistManager addToCurrentQueueNext", song) addToCurrentQueueNext(listOf(song)) } diff --git a/app/src/main/java/luci/sixsixsix/powerampache2/player/PlayerManager.kt b/app/src/main/java/luci/sixsixsix/powerampache2/player/PlayerManager.kt index af2b528a..d6c31f15 100644 --- a/app/src/main/java/luci/sixsixsix/powerampache2/player/PlayerManager.kt +++ b/app/src/main/java/luci/sixsixsix/powerampache2/player/PlayerManager.kt @@ -1,11 +1,8 @@ package luci.sixsixsix.powerampache2.player import android.content.Context -import android.os.Handler -import android.os.Looper import androidx.annotation.OptIn import androidx.media3.common.AudioAttributes -import androidx.media3.common.Player import androidx.media3.common.util.UnstableApi import androidx.media3.datasource.DefaultDataSource import androidx.media3.datasource.cache.CacheDataSource @@ -16,13 +13,8 @@ import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.source.DefaultMediaSourceFactory import androidx.media3.exoplayer.trackselection.DefaultTrackSelector import dagger.hilt.android.qualifiers.ApplicationContext -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.Job -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.launch -import luci.sixsixsix.powerampache2.BuildConfig import luci.sixsixsix.powerampache2.domain.utils.SharedPreferencesManager import javax.inject.Inject import javax.inject.Singleton @@ -112,5 +104,6 @@ class PlayerManager @OptIn(UnstableApi::class) _playerState.value = null } + // TODO: unused? fun isPlayerInitialized(): Boolean = _player != null } \ No newline at end of file diff --git a/app/src/main/java/luci/sixsixsix/powerampache2/player/SimpleMediaServiceHandler.kt b/app/src/main/java/luci/sixsixsix/powerampache2/player/SimpleMediaServiceHandler.kt index f2d725c8..f5601019 100644 --- a/app/src/main/java/luci/sixsixsix/powerampache2/player/SimpleMediaServiceHandler.kt +++ b/app/src/main/java/luci/sixsixsix/powerampache2/player/SimpleMediaServiceHandler.kt @@ -22,10 +22,8 @@ package luci.sixsixsix.powerampache2.player import android.content.Context -import android.content.Intent import android.media.session.PlaybackState import androidx.annotation.OptIn -import androidx.lifecycle.viewModelScope import androidx.media3.common.MediaItem import androidx.media3.common.PlaybackException import androidx.media3.common.PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED @@ -58,6 +56,8 @@ import luci.sixsixsix.powerampache2.domain.errors.UserNotEnabledException import javax.inject.Inject import javax.inject.Singleton +// TODO: there's a bunch of unused stuff in here, worth investigation and cleaning + @Singleton class SimpleMediaServiceHandler @Inject constructor( private val playerManager: PlayerManager, @@ -106,7 +106,7 @@ class SimpleMediaServiceHandler @Inject constructor( fun getMediaItemCount() = player().mediaItemCount fun addMediaItemList(mediaItems: List) { - if(mediaItems.isNullOrEmpty() && player().mediaItemCount == 0) return + if(mediaItems.isEmpty() && player().mediaItemCount == 0) return if (player().mediaItemCount > 0 && playlistManager.currentSongState.value?.mediaId == player().currentMediaItem?.mediaId) { // if the current song of the playlist (if playlist is not empty) corresponds to the current diff --git a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/TestScreen.kt b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/TestScreen.kt index 59c06444..7ada2e0d 100644 --- a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/TestScreen.kt +++ b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/TestScreen.kt @@ -34,7 +34,6 @@ 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.toArgb import androidx.compose.ui.unit.dp import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.navigation.DestinationsNavigator diff --git a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/common/AmpacheListItem.kt b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/common/AmpacheListItem.kt index d7323293..d33f72e1 100644 --- a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/common/AmpacheListItem.kt +++ b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/common/AmpacheListItem.kt @@ -85,6 +85,8 @@ import luci.sixsixsix.powerampache2.domain.models.isOwnerAdmin import luci.sixsixsix.powerampache2.domain.models.isOwnerSystem import luci.sixsixsix.powerampache2.domain.models.isSmartPlaylist import luci.sixsixsix.powerampache2.presentation.common.songitem.SongItemEvent +import luci.sixsixsix.powerampache2.presentation.models.SongUI +import luci.sixsixsix.powerampache2.presentation.models.isAvailableOffline data class InfoViewItemText( val firstRowText: String, @@ -97,7 +99,6 @@ fun AmpacheListItem( item: T, songItemEventListener: (songItemEvent: SongItemEvent) -> Unit, modifier: Modifier = Modifier, - isSongDownloaded: Boolean = false, showDownloadedSongMarker: Boolean = false, enableSwipeToRemove: Boolean = false, onRemove: (AmpacheModel) -> Unit = {}, @@ -111,7 +112,7 @@ fun AmpacheListItem( var showPublicBadge = false when(item) { - is Song -> { + is SongUI -> { imageUrl = item.imageUrl infoViewItemText = InfoViewItemText( item.title, @@ -141,9 +142,9 @@ fun AmpacheListItem( } else stringResource(R.string.item_title_playlist) } ?: stringResource(R.string.item_title_playlist) - val items = item.items?.let { + val items = item.items.let { if (it > 0) stringResource(id = R.string.playlistItem_songCount, it) else " " - } ?: run { " " } + } infoViewItemText = InfoViewItemText(item.name, items, ownerText) isFavourite = item.flag == 1 rating = item.rating @@ -162,9 +163,9 @@ fun AmpacheListItem( imageUrl = imageUrl, textInfo = infoViewItemText, songItemEventListener = songItemEventListener, - isSongDownloaded = isSongDownloaded, + isSongDownloaded = if (item is SongUI) { item.isAvailableOffline() } else false, showDownloadedSongMarker = showDownloadedSongMarker, - hideSongMenu = item !is Song, + hideSongMenu = item !is SongUI, isFavourite = isFavourite, rating = rating, showAmpacheBadge = showAmpacheBadge, @@ -292,7 +293,7 @@ fun AmpacheListItemMain( SongDropDownMenu( isContextMenuVisible = isContextMenuVisible, pressOffset = pressOffset, - isSongDownloaded = isSongDownloaded, + isSongAvailableOffline = isSongDownloaded, songItemEventListener = { isContextMenuVisible = false songItemEventListener(it) diff --git a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/common/SongDropDownMenu.kt b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/common/SongDropDownMenu.kt index 1075f1f6..d663d405 100644 --- a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/common/SongDropDownMenu.kt +++ b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/common/SongDropDownMenu.kt @@ -59,7 +59,7 @@ fun SongDropDownMenu( modifier: Modifier = Modifier, isContextMenuVisible: Boolean, pressOffset: DpOffset, - isSongDownloaded: Boolean, + isSongAvailableOffline: Boolean, songItemEventListener: (songItemEvent: SongItemEvent) -> Unit, onDismissRequest:() -> Unit ) { @@ -114,7 +114,7 @@ fun SongDropDownMenu( ) { songItemEventListener(SongItemEvent.SHOW_SONG_INFO) } - if (isSongDownloaded) { + if (isSongAvailableOffline) { SongDropDownMenuItem( text = R.string.dropdownMenu_item_export, iconImageVector = Icons.Default.SaveAlt diff --git a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/common/songitem/InfoTextSectionSongItem.kt b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/common/songitem/InfoTextSectionSongItem.kt index d20f0c62..69df5a21 100644 --- a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/common/songitem/InfoTextSectionSongItem.kt +++ b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/common/songitem/InfoTextSectionSongItem.kt @@ -33,14 +33,14 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import luci.sixsixsix.powerampache2.R import luci.sixsixsix.powerampache2.common.fontDimensionResource -import luci.sixsixsix.powerampache2.domain.models.Song -import luci.sixsixsix.powerampache2.domain.models.totalTime +import luci.sixsixsix.powerampache2.presentation.models.SongUI +import luci.sixsixsix.powerampache2.presentation.models.totalTime @Composable fun InfoTextSectionSongItem( modifier: Modifier, - song: Song, + song: SongUI, subtitleString: SubtitleString, songInfoThirdRow: SongInfoThirdRow = SongInfoThirdRow.AlbumTitle ) { diff --git a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/common/songitem/SongItem.kt b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/common/songitem/SongItem.kt index 75ab3201..7076c530 100644 --- a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/common/songitem/SongItem.kt +++ b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/common/songitem/SongItem.kt @@ -61,7 +61,8 @@ import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp import coil.compose.AsyncImage import luci.sixsixsix.powerampache2.R -import luci.sixsixsix.powerampache2.domain.models.Song +import luci.sixsixsix.powerampache2.presentation.models.SongUI +import luci.sixsixsix.powerampache2.presentation.models.isAvailableOffline import luci.sixsixsix.powerampache2.presentation.common.SongDropDownMenu import luci.sixsixsix.powerampache2.presentation.common.SwipeToDismissItem @@ -71,23 +72,22 @@ enum class SubtitleString { NOTHING, ARTIST, ALBUM } @Composable fun SongItem( - song: Song, + song: SongUI, songItemEventListener: (songItemEvent: SongItemEvent) -> Unit, modifier: Modifier = Modifier, isEditMode: Boolean = false, isEditEnabled: Boolean = true, isLandscape: Boolean = false, - isSongDownloaded: Boolean = false, showDownloadedSongMarker: Boolean = true, subtitleString: SubtitleString = SubtitleString.ARTIST, songInfoThirdRow: SongInfoThirdRow = SongInfoThirdRow.AlbumTitle, enableSwipeToRemove: Boolean = false, isEditSongSelected: Boolean = false, - onRemove: (Song) -> Unit = {}, - onRightToLeftSwipe: (Song) -> Unit = {}, - onEditMoveUp: (Song) -> Unit = { _ -> }, - onEditMoveDown: (Song) -> Unit = { _ -> }, - onEditSelected: (Boolean, Song) -> Unit = { _, _ -> } + onRemove: (SongUI) -> Unit = {}, + onRightToLeftSwipe: (SongUI) -> Unit = {}, + onEditMoveUp: (SongUI) -> Unit = { _ -> }, + onEditMoveDown: (SongUI) -> Unit = { _ -> }, + onEditSelected: (Boolean, SongUI) -> Unit = { _, _ -> } ) { SwipeToDismissItem( item = song, @@ -98,7 +98,6 @@ fun SongItem( songItemEventListener = songItemEventListener, modifier = modifier, isLandscape = isLandscape, - isSongDownloaded = isSongDownloaded, showDownloadedSongMarker = showDownloadedSongMarker, subtitleString = subtitleString, songInfoThirdRow = songInfoThirdRow @@ -107,9 +106,8 @@ fun SongItem( SongItemForegroundEdit( song = song, modifier = modifier, - isSongDownloaded = isSongDownloaded, isEditEnabled = isEditEnabled, - showDownloadedSongMarker = showDownloadedSongMarker, + //showDownloadedSongMarker = showDownloadedSongMarker, subtitleString = subtitleString, songInfoThirdRow = songInfoThirdRow, checked = isEditSongSelected, @@ -128,11 +126,10 @@ fun SongItem( @Composable fun SongItemMain( - song: Song, + song: SongUI, songItemEventListener: (songItemEvent: SongItemEvent) -> Unit, modifier: Modifier = Modifier, isLandscape: Boolean, - isSongDownloaded: Boolean, showDownloadedSongMarker: Boolean, subtitleString: SubtitleString = SubtitleString.ARTIST, songInfoThirdRow: SongInfoThirdRow = SongInfoThirdRow.AlbumTitle @@ -157,7 +154,6 @@ fun SongItemMain( .background(Color.Transparent) .align(Alignment.CenterVertically), song = song, - isSongDownloaded = isSongDownloaded, showDownloadedSongMarker = showDownloadedSongMarker ) } @@ -204,7 +200,7 @@ fun SongItemMain( SongDropDownMenu( isContextMenuVisible = isContextMenuVisible, pressOffset = pressOffset, - isSongDownloaded = isSongDownloaded, + isSongAvailableOffline = song.isAvailableOffline(), songItemEventListener = { isContextMenuVisible = false songItemEventListener(it) @@ -218,8 +214,7 @@ fun SongItemMain( @Composable private fun SongAlbumCover( modifier: Modifier, - song: Song, - isSongDownloaded: Boolean, + song: SongUI, showDownloadedSongMarker: Boolean ) { Card( @@ -242,7 +237,7 @@ private fun SongAlbumCover( error = painterResource(id = R.drawable.placeholder_album), contentDescription = song.title, ) - if(isSongDownloaded && showDownloadedSongMarker) { + if(song.isAvailableOffline() && showDownloadedSongMarker) { Card(modifier = Modifier.size(20.dp)) { Box( modifier = Modifier @@ -264,9 +259,8 @@ private fun SongAlbumCover( @Composable fun SongItemPreview() { SongItem( - song = Song.mockSong, + song = SongUI.mockSongUI, songItemEventListener = {}, subtitleString = SubtitleString.NOTHING, - isSongDownloaded = true ) } diff --git a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/common/songitem/SongItemForegroundEdit.kt b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/common/songitem/SongItemForegroundEdit.kt index a6693cfb..13167025 100644 --- a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/common/songitem/SongItemForegroundEdit.kt +++ b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/common/songitem/SongItemForegroundEdit.kt @@ -43,19 +43,18 @@ import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import luci.sixsixsix.powerampache2.R -import luci.sixsixsix.powerampache2.domain.models.Song +import luci.sixsixsix.powerampache2.presentation.models.SongUI @Composable fun SongItemForegroundEdit( - song: Song, + song: SongUI, modifier: Modifier = Modifier, - isSongDownloaded: Boolean, isEditEnabled: Boolean, - showDownloadedSongMarker: Boolean, + //showDownloadedSongMarker: Boolean, subtitleString: SubtitleString = SubtitleString.ARTIST, songInfoThirdRow: SongInfoThirdRow = SongInfoThirdRow.AlbumTitle, checked: Boolean, - onCheckedChange: (Boolean, Song) -> Unit, + onCheckedChange: (Boolean, SongUI) -> Unit, onMoveUp: () -> Unit, onMoveDown: () -> Unit, ) { @@ -148,9 +147,8 @@ fun SongItemForegroundEdit( @Preview fun SongItemForegroundEditPreview() { SongItemForegroundEdit( - song = Song.mockSong, - isSongDownloaded = true, - showDownloadedSongMarker = true, + song = SongUI.mockSongUI, + //showDownloadedSongMarker = true, checked = true, onMoveDown = {}, isEditEnabled = true, diff --git a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/common/songitem/SongWrapper.kt b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/common/songitem/SongWrapper.kt deleted file mode 100644 index 069c0fc6..00000000 --- a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/common/songitem/SongWrapper.kt +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Copyright (C) 2024 Antonio Tari - * - * This file is a part of Power Ampache 2 - * Ampache Android client application - * @author Antonio Tari - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - */ -package luci.sixsixsix.powerampache2.presentation.common.songitem - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize -import luci.sixsixsix.powerampache2.domain.models.Song - -@Parcelize -data class SongWrapper( - val song: Song, - val isOffline: Boolean -): Parcelable diff --git a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/dialogs/AddToPlaylistOrQueueDialog.kt b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/dialogs/AddToPlaylistOrQueueDialog.kt index bd2f26b4..8b694a44 100644 --- a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/dialogs/AddToPlaylistOrQueueDialog.kt +++ b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/dialogs/AddToPlaylistOrQueueDialog.kt @@ -64,6 +64,7 @@ import luci.sixsixsix.powerampache2.common.RandomThemeBackgroundColour import luci.sixsixsix.powerampache2.domain.models.Playlist import luci.sixsixsix.powerampache2.domain.models.PlaylistType import luci.sixsixsix.powerampache2.domain.models.Song +import luci.sixsixsix.powerampache2.presentation.models.SongUI import luci.sixsixsix.powerampache2.presentation.screens.main.viewmodel.MainEvent import luci.sixsixsix.powerampache2.presentation.screens.main.viewmodel.MainViewModel @@ -74,7 +75,7 @@ val textPaddingVertical = 10.dp data class AddToPlaylistOrQueueDialogOpen( val isOpen: Boolean, - val songs: List = listOf() + val songs: List = listOf() ) /** @@ -82,7 +83,7 @@ data class AddToPlaylistOrQueueDialogOpen( */ @Composable fun AddToPlaylistOrQueueDialog( - songs: List, + songs: List, onDismissRequest: () -> Unit, onCreatePlaylistRequest: (success: Boolean) -> Unit = {}, mainViewModel: MainViewModel, @@ -194,7 +195,7 @@ fun AddToPlaylistOrQueueDialog( private fun addToPlaylist( context: Context, viewModel: AddToPlaylistOrQueueDialogViewModel, - songs: List, + songs: List, playlist: Playlist ) { when (songs.size) { @@ -222,7 +223,7 @@ private fun addToPlaylist( private fun addToQueue( mainViewModel: MainViewModel, viewModel: AddToPlaylistOrQueueDialogViewModel, - songs: List + songs: List ) { when (songs.size) { 1 -> mainViewModel.onEvent(MainEvent.OnAddSongToQueue(songs[0])) @@ -236,7 +237,7 @@ private fun addToQueue( private fun CreateAddToPlaylist( context: Context, viewModel: AddToPlaylistOrQueueDialogViewModel, - songs: List, + songs: List, onConfirm: (playlistName: String, playlistType: PlaylistType) -> Unit, onCancel: () -> Unit ) { diff --git a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/dialogs/AddToPlaylistOrQueueDialogViewModel.kt b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/dialogs/AddToPlaylistOrQueueDialogViewModel.kt index 0ee63b3f..dbe99be5 100644 --- a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/dialogs/AddToPlaylistOrQueueDialogViewModel.kt +++ b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/dialogs/AddToPlaylistOrQueueDialogViewModel.kt @@ -40,13 +40,14 @@ import luci.sixsixsix.powerampache2.domain.PlaylistsRepository import luci.sixsixsix.powerampache2.domain.errors.ErrorHandler import luci.sixsixsix.powerampache2.domain.models.Playlist import luci.sixsixsix.powerampache2.domain.models.PlaylistType -import luci.sixsixsix.powerampache2.domain.models.Song import luci.sixsixsix.powerampache2.domain.models.USER_SYSTEM import luci.sixsixsix.powerampache2.domain.models.User import luci.sixsixsix.powerampache2.domain.usecase.UserFlowUseCase import luci.sixsixsix.powerampache2.domain.usecase.playlists.PlaylistsFlow import luci.sixsixsix.powerampache2.domain.usecase.playlists.PlaylistsUseCase +import luci.sixsixsix.powerampache2.presentation.models.SongUI import luci.sixsixsix.powerampache2.player.MusicPlaylistManager +import luci.sixsixsix.powerampache2.presentation.models.toSong import java.util.UUID import javax.inject.Inject @@ -151,10 +152,14 @@ class AddToPlaylistOrQueueDialogViewModel @Inject constructor( private fun createPlaylistAndAddSongs( playlistName: String, playlistType: PlaylistType, - songsToAdd: List + songsToAdd: List ) = viewModelScope.launch { playlistsRepository - .createNewPlaylistAddSongs(playlistName, playlistType, songsToAdd).collect { result -> + .createNewPlaylistAddSongs( + name = playlistName, + playlistType = playlistType, + songsToAdd = songsToAdd.map { it.toSong() }, + ).collect { result -> when (result) { is Resource.Success -> { result.data?.let { @@ -188,10 +193,10 @@ class AddToPlaylistOrQueueDialogViewModel @Inject constructor( } } - private fun addSongsToPlaylist(playlist: Playlist, songs: List) = viewModelScope.launch { + private fun addSongsToPlaylist(playlist: Playlist, songs: List) = viewModelScope.launch { playlistsRepository.addSongsToPlaylist( playlist = playlist, - songsToAdd = songs + songsToAdd = songs.map { it.toSong() }, ).collect { result -> when (result) { is Resource.Success -> { @@ -215,9 +220,17 @@ data class AddToPlaylistOrQueueDialogState ( ) sealed class AddToPlaylistOrQueueDialogEvent { - data class OnAddAlbumToQueue(val songs: List): AddToPlaylistOrQueueDialogEvent() - data class AddSongsToPlaylist(val songs: List, val playlist: Playlist): AddToPlaylistOrQueueDialogEvent() - data class CreatePlaylistAndAddSongs(val songs: List, val playlistName: String, val playlistType: PlaylistType): AddToPlaylistOrQueueDialogEvent() - data class AddSongToPlaylist(val song: Song, val playlistId: String): AddToPlaylistOrQueueDialogEvent() - data class CreatePlaylistAndAddSong(val song: Song, val playlistName: String, val playlistType: PlaylistType): AddToPlaylistOrQueueDialogEvent() + data class OnAddAlbumToQueue(val songs: List): AddToPlaylistOrQueueDialogEvent() + data class AddSongsToPlaylist( + val songs: List, val playlist: Playlist + ): AddToPlaylistOrQueueDialogEvent() + data class CreatePlaylistAndAddSongs( + val songs: List, val playlistName: String, val playlistType: PlaylistType + ): AddToPlaylistOrQueueDialogEvent() + data class AddSongToPlaylist( + val song: SongUI, val playlistId: String + ): AddToPlaylistOrQueueDialogEvent() + data class CreatePlaylistAndAddSong( + val song: SongUI, val playlistName: String, val playlistType: PlaylistType + ): AddToPlaylistOrQueueDialogEvent() } diff --git a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/dialogs/EraseConfirmDialog.kt b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/dialogs/EraseConfirmDialog.kt index 0a06bdc9..e13637c2 100644 --- a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/dialogs/EraseConfirmDialog.kt +++ b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/dialogs/EraseConfirmDialog.kt @@ -44,10 +44,11 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import luci.sixsixsix.powerampache2.domain.models.Song import luci.sixsixsix.powerampache2.presentation.common.RoundedCornerButton +import luci.sixsixsix.powerampache2.presentation.models.SongUI data class ShowEraseConfirmDialog( val isOpen: Boolean, - val song: Song? = null, + val song: SongUI? = null, ) @Composable diff --git a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/dialogs/info/InfoDialogAlbum.kt b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/dialogs/info/InfoDialogAlbum.kt index 7426b944..16bd1f10 100644 --- a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/dialogs/info/InfoDialogAlbum.kt +++ b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/dialogs/info/InfoDialogAlbum.kt @@ -44,6 +44,7 @@ import luci.sixsixsix.powerampache2.R import luci.sixsixsix.powerampache2.common.capitalizeWords import luci.sixsixsix.powerampache2.domain.models.Album import luci.sixsixsix.powerampache2.domain.models.totalTime +import luci.sixsixsix.powerampache2.presentation.models.totalTime import luci.sixsixsix.powerampache2.domain.plugin.info.PluginAlbumData import luci.sixsixsix.powerampache2.domain.plugin.info.totalTime import luci.sixsixsix.powerampache2.presentation.common.MusicChips diff --git a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/dialogs/info/InfoDialogSong.kt b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/dialogs/info/InfoDialogSong.kt index 8031a200..f0de7b1b 100644 --- a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/dialogs/info/InfoDialogSong.kt +++ b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/dialogs/info/InfoDialogSong.kt @@ -43,23 +43,23 @@ import coil.compose.AsyncImage import luci.sixsixsix.powerampache2.R import luci.sixsixsix.powerampache2.common.capitalizeWords import luci.sixsixsix.powerampache2.domain.common.toDebugMap -import luci.sixsixsix.powerampache2.domain.models.Song -import luci.sixsixsix.powerampache2.domain.models.totalTime +import luci.sixsixsix.powerampache2.presentation.models.totalTime import luci.sixsixsix.powerampache2.domain.plugin.info.PluginSongData import luci.sixsixsix.powerampache2.domain.plugin.info.totalTime import luci.sixsixsix.powerampache2.presentation.common.MusicChips import luci.sixsixsix.powerampache2.presentation.dialogs.info.components.InfoDialogText import luci.sixsixsix.powerampache2.presentation.dialogs.info.components.InfoDialogTextHorizontal import luci.sixsixsix.powerampache2.presentation.dialogs.info.components.InfoDialogTitleText +import luci.sixsixsix.powerampache2.presentation.models.SongUI data class ShowSongInfoDialogOpen( val isOpen: Boolean, - val song: Song? = null, + val song: SongUI? = null, val songPlugin: PluginSongData? = null, ) @Composable -fun InfoDialogSong(song: Song, songPlugin: PluginSongData?, onDismissRequest: () -> Unit) { +fun InfoDialogSong(song: SongUI, songPlugin: PluginSongData?, onDismissRequest: () -> Unit) { InfoDialogBase(onDismissRequest) { Card( //border = BorderStroke((0.5).dp, MaterialTheme.colorScheme.background), @@ -103,7 +103,6 @@ fun InfoDialogSong(song: Song, songPlugin: PluginSongData?, onDismissRequest: () if (dur > 0) songPlugin.totalTime() else song.totalTime() } ?: song.totalTime() - val year = try { (songPlugin?.year ?: "0").toInt() } catch (e: Exception) { 0 } InfoDialogTextHorizontal(name, "") @@ -112,7 +111,7 @@ fun InfoDialogSong(song: Song, songPlugin: PluginSongData?, onDismissRequest: () if (artistName.isNotBlank()) InfoDialogTextHorizontal("Artist", artistName) if (playCount > 0) InfoDialogTextHorizontal("Play Count", playCount.toString()) if (listeners > 0) InfoDialogTextHorizontal("Listeners", listeners.toString()) - InfoDialogTextHorizontal("Duration", duration.toString()) + InfoDialogTextHorizontal("Duration", duration) if (year > 0) InfoDialogTextHorizontal("Year", year.toString()) if (songPlugin?.language?.isNotBlank() == true) InfoDialogText("Language", songPlugin.language) if (artistAlbum.isNotBlank()) InfoDialogTextHorizontal("Album Artist", artistAlbum) diff --git a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/models/SongUI.kt b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/models/SongUI.kt new file mode 100644 index 00000000..d86eb3a5 --- /dev/null +++ b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/models/SongUI.kt @@ -0,0 +1,221 @@ +package luci.sixsixsix.powerampache2.presentation.models + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize +import luci.sixsixsix.powerampache2.domain.common.Constants +import luci.sixsixsix.powerampache2.domain.models.AmpacheModel +import luci.sixsixsix.powerampache2.domain.models.MusicAttribute +import luci.sixsixsix.powerampache2.domain.models.Song + +@Parcelize +data class SongUI( + val mediaId: String, + override val id: String = mediaId, + val title: String, + val album: MusicAttribute, + val artist: MusicAttribute, + val albumArtist: MusicAttribute, + val songUrl: String, + val imageUrl: String, + val bitrate: Int, + val streamBitrate: Int, + val catalog: Int, + val channels: Int, + val composer: String, + val filename: String, + val genre: List, + val mime: String?, + val playCount: Int, + val playlistTrackNumber: Int, + val rateHz: Int, + val size: Int, + val time: Int, + val trackNumber: Int, + val year: Int, + val name: String, + val mode: String?, + val artists: List, + val flag: Int, + val streamFormat: String?, + val format: String?, + val streamMime: String?, + val publisher: String?, + val replayGainTrackGain: Float?, + val replayGainTrackPeak: Float?, + val disk: Int, + val diskSubtitle: String, + val mbId: String, + val comment: String, + val language: String, + val lyrics: String, + val albumMbId: String, + val artistMbId: String, + val albumArtistMbId: String, + val averageRating: Float, + val preciseRating: Float, + val rating: Float, + val isDownloaded: Boolean, +): Comparable, Parcelable, AmpacheModel { + override fun compareTo(other: SongUI): Int = mediaId.compareTo(other.mediaId) + + companion object { + val mockSongUI = Song( + mediaId = "12345", + title = "Stabwound", + artist = MusicAttribute("666", "Necrophagist"), + album = MusicAttribute("2004", "Epitaph"), + albumArtist = MusicAttribute.emptyInstance(), + genre = listOf(MusicAttribute.randomInstance(), + MusicAttribute.randomInstance(), + MusicAttribute.randomInstance() + ), + songUrl = "http://192.168.200.200/ampache/public/play/index.php?ssid=bd15d8f22785f5176aa2f783f88616f3&type=song&oid=12345&uid=1&player=api&name=Necrophagist%20-%20Stabwound.mp3", + imageUrl = "http://192.168.200.200/ampache/public/image.php?object_id=1986&object_type=album&auth=bd15d8f22785f5176aa2f783f88616f3&name=art.jpg", + averageRating = Constants.ERROR_FLOAT, + preciseRating = Constants.ERROR_FLOAT, + rating = Constants.ERROR_FLOAT, + ).toSongUI(true) + + fun mapSongs(songs: List) = LinkedHashMap().apply { + songs.forEach { + put(it.mediaId, it) + } + } + } +} + +fun SongUI.isAvailableOffline() = isDownloaded + +fun SongUI.hasLyrics() = lyrics.isNotBlank() + +fun SongUI.isFavourite() = flag != 0 + +fun SongUI.totalTime(): String { + val minutes = time / 60 + val seconds = time % 60 + return "$minutes:${if (seconds < 10) { "0" } else { "" } }${seconds}" +} + +fun List.reduceList() = if (size > Constants.config.queueSizeLimit) { + subList(0, Constants.config.queueSizeLimit) } else this + +/** Returns a "presentation" level SongUI model of the Song**/ +fun Song.toSongUI(isDownloaded: Boolean = false) = SongUI( + mediaId = mediaId, + title = title, + artist = artist, + album = album, + albumArtist = albumArtist, + songUrl = songUrl, + imageUrl = imageUrl, + bitrate = bitrate, + streamBitrate = streamBitrate, + catalog = catalog, + channels = channels, + composer = composer, + filename = filename, + genre = genre, + mime = mime, + name = name, + playCount = playCount, + playlistTrackNumber = playlistTrackNumber, + rateHz = rateHz, + size = size, + time = time, + trackNumber = trackNumber, + year = year, + mode = mode, + artists = artists, + flag = flag, + streamFormat = streamFormat, + format = format, + streamMime = streamMime, + publisher = publisher, + replayGainTrackGain = replayGainTrackGain, + replayGainTrackPeak = replayGainTrackPeak, + lyrics = lyrics, + comment = comment, + language = language, + disk = disk, + diskSubtitle = diskSubtitle, + mbId = mbId, + albumMbId = albumMbId, + artistMbId = artistMbId, + albumArtistMbId = albumArtistMbId, + rating = rating, + preciseRating = preciseRating, + averageRating = averageRating, + isDownloaded = isDownloaded, +) + +/** Returns a domain level Song model version of the SongUI **/ +fun SongUI.toSong() = Song( + mediaId = mediaId, + title = title, + artist = artist, + album = album, + albumArtist = albumArtist, + songUrl = songUrl, + imageUrl = imageUrl, + bitrate = bitrate, + streamBitrate = streamBitrate, + catalog = catalog, + channels = channels, + composer = composer, + filename = filename, + genre = genre, + mime = mime, + name = name, + playCount = playCount, + playlistTrackNumber = playlistTrackNumber, + rateHz = rateHz, + size = size, + time = time, + trackNumber = trackNumber, + year = year, + mode = mode, + artists = artists, + flag = flag, + streamFormat = streamFormat, + format = format, + streamMime = streamMime, + publisher = publisher, + replayGainTrackGain = replayGainTrackGain, + replayGainTrackPeak = replayGainTrackPeak, + lyrics = lyrics, + comment = comment, + language = language, + disk = disk, + diskSubtitle = diskSubtitle, + mbId = mbId, + albumMbId = albumMbId, + artistMbId = artistMbId, + albumArtistMbId = albumArtistMbId, + rating = rating, + preciseRating = preciseRating, + averageRating = averageRating, +) + +/** Returns a list of "presentation" level Song models, mapped from the list of Song **/ +suspend fun List.toSongUI(isAvailableOfflineCallback: suspend (Song) -> Boolean): List { + val songUiList = mutableListOf() + this.forEach { + songUiList.add( + it.toSongUI( + isAvailableOfflineCallback(it) + ) + ) + } + return songUiList +} + +/** Returns a list of domain level Song models, mapped from the list of SongUI **/ +fun List.toSong(): List { + val songList = mutableListOf() + this.forEach { + songList.add( + it.toSong() + ) + } + return songList +} \ No newline at end of file diff --git a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/artists/ArtistsScreen.kt b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/artists/ArtistsScreen.kt index dacb44f7..6483aa07 100644 --- a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/artists/ArtistsScreen.kt +++ b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/artists/ArtistsScreen.kt @@ -59,10 +59,10 @@ const val GRID_ITEMS_ROW_LAND = 6 @Destination @Composable fun ArtistsScreen( + modifier: Modifier = Modifier, navigator: DestinationsNavigator, gridPerRow: Int = GRID_ITEMS_ROW, viewModel: ArtistsViewModel = hiltViewModel(), - modifier: Modifier = Modifier ) { ArtistsScreenContent( artists = viewModel.state.artists, @@ -78,14 +78,14 @@ fun ArtistsScreen( @Destination @Composable fun ArtistsScreenContent( + modifier: Modifier = Modifier, artists: List, isLoading: Boolean, isRefreshing: Boolean, gridPerRow: Int = GRID_ITEMS_ROW, - modifier: Modifier = Modifier, swipeToRefreshEnabled: Boolean = true, onEvent: (ArtistEvent) -> Unit, - navigateToArtist: (artistId: String, artist: Artist) -> Unit + navigateToArtist: (artistId: String, artist: Artist) -> Unit, ) { val swipeRefreshState = rememberSwipeRefreshState(isRefreshing = isRefreshing) var orientation by remember { mutableIntStateOf(Configuration.ORIENTATION_PORTRAIT) } diff --git a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/artists/ArtistsViewModel.kt b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/artists/ArtistsViewModel.kt index cc30cefa..cc82bc26 100644 --- a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/artists/ArtistsViewModel.kt +++ b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/artists/ArtistsViewModel.kt @@ -62,6 +62,7 @@ class ArtistsViewModel @Inject constructor( getArtists(fetchRemote = true) } is ArtistEvent.OnSearchQueryChange -> { + // TODO: invert check to remove if empty body if (event.query.isBlank() && state.searchQuery.isBlank()) { } else { diff --git a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/home/HomeScreen.kt b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/home/HomeScreen.kt index fd41ff06..664e6d46 100644 --- a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/home/HomeScreen.kt +++ b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/home/HomeScreen.kt @@ -67,7 +67,7 @@ sealed class HomeScreenRowItems(@StringRes val title: Int, val items: List, randomAlbums: List ) = - frequentAlbums.isNullOrEmpty() && - recentAlbums.isNullOrEmpty() && - randomAlbums.isNullOrEmpty() && - state.newestAlbums.isNullOrEmpty() && - playlists.isNullOrEmpty() + frequentAlbums.isEmpty() && + recentAlbums.isEmpty() && + randomAlbums.isEmpty() && + state.newestAlbums.isEmpty() && + playlists.isEmpty() diff --git a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/home/HomeScreenViewModel.kt b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/home/HomeScreenViewModel.kt index 68cc27c1..bd2eccb0 100644 --- a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/home/HomeScreenViewModel.kt +++ b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/home/HomeScreenViewModel.kt @@ -38,7 +38,6 @@ import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch @@ -47,7 +46,6 @@ import luci.sixsixsix.powerampache2.common.Resource import luci.sixsixsix.powerampache2.common.delegates.FetchArtistSongsHandler import luci.sixsixsix.powerampache2.common.delegates.FetchArtistSongsHandlerImpl import luci.sixsixsix.powerampache2.domain.AlbumsRepository -import luci.sixsixsix.powerampache2.domain.PlaylistsRepository import luci.sixsixsix.powerampache2.domain.models.Album import luci.sixsixsix.powerampache2.domain.models.AmpacheModel import luci.sixsixsix.powerampache2.domain.models.Artist @@ -56,7 +54,6 @@ import luci.sixsixsix.powerampache2.domain.models.FrequentPlaylist import luci.sixsixsix.powerampache2.domain.models.HighestPlaylist import luci.sixsixsix.powerampache2.domain.models.Playlist import luci.sixsixsix.powerampache2.domain.models.RecentPlaylist -import luci.sixsixsix.powerampache2.domain.models.Song import luci.sixsixsix.powerampache2.domain.usecase.albums.RecommendedAlbumsFlow import luci.sixsixsix.powerampache2.domain.usecase.artists.MostPlayedArtistsUseCase import luci.sixsixsix.powerampache2.domain.usecase.artists.RecommendedArtistsFlow @@ -64,6 +61,8 @@ import luci.sixsixsix.powerampache2.domain.usecase.artists.SongsFromArtistUseCas import luci.sixsixsix.powerampache2.domain.usecase.playlists.PlaylistsFlow import luci.sixsixsix.powerampache2.domain.usecase.playlists.PlaylistsUseCase import luci.sixsixsix.powerampache2.domain.usecase.settings.OfflineModeFlowUseCase +import luci.sixsixsix.powerampache2.domain.usecase.songs.IsSongAvailableOfflineUseCase +import luci.sixsixsix.powerampache2.presentation.models.SongUI import javax.inject.Inject @HiltViewModel @@ -75,8 +74,12 @@ class HomeScreenViewModel @Inject constructor( recommendedArtistsFlow: RecommendedArtistsFlow, recommendedAlbumsFlow: RecommendedAlbumsFlow, offlineModeFlowUseCase: OfflineModeFlowUseCase, - songsFromArtistUseCase: SongsFromArtistUseCase -) : ViewModel(), FetchArtistSongsHandler by FetchArtistSongsHandlerImpl(songsFromArtistUseCase) { + songsFromArtistUseCase: SongsFromArtistUseCase, + isSongAvailableOfflineUseCase: IsSongAvailableOfflineUseCase, +) : ViewModel(), FetchArtistSongsHandler by FetchArtistSongsHandlerImpl( + songsFromArtistUseCase, + isSongAvailableOfflineUseCase, + ) { var state by mutableStateOf(HomeScreenState()) private var _recentNetwork: MutableStateFlow> = MutableStateFlow(listOf()) @@ -186,7 +189,7 @@ class HomeScreenViewModel @Inject constructor( init { viewModelScope.launch { // playlists can change or be edited, make sure to always listen to the latest version - offlineModeStateFlow.collectLatest { isOfflineMode -> + offlineModeStateFlow.collectLatest { fetchAllAsync() } } @@ -314,6 +317,7 @@ class HomeScreenViewModel @Inject constructor( } // ---- NEWEST + // TODO: fetchRemote isn't used? private suspend fun getNewest(fetchRemote: Boolean = true) { albumsRepository .getNewestAlbums() @@ -398,6 +402,7 @@ class HomeScreenViewModel @Inject constructor( // ---- RANDOM private suspend fun getRandom(fetchRemote: Boolean = true) { + // TODO: empty callback? getRandom(fetchRemote = fetchRemote, injectArtists = true) { albums -> //state = state.copy(randomAlbums = albums) } @@ -436,7 +441,7 @@ class HomeScreenViewModel @Inject constructor( fetchRemote: Boolean = false, callback: (albums: List) -> Unit ) { - if (albums.isNullOrEmpty()) { + if (albums.isEmpty()) { getRandom(fetchRemote = fetchRemote, injectArtists = true) { albums -> callback(albums) } @@ -505,7 +510,7 @@ class HomeScreenViewModel @Inject constructor( fun fetchSongsFromArtist( artist: Artist, fetchRemote: Boolean = true, - songsCallback: (List) -> Unit + songsCallback: (List) -> Unit ) { fetchSongsArtistJob?.cancel() fetchSongsArtistJob = viewModelScope.launch { @@ -515,7 +520,7 @@ class HomeScreenViewModel @Inject constructor( isOfflineMode = offlineModeStateFlow.value, songsCallback = songsCallback, loadingCallback = { state = state.copy(currentArtistPlayLoading = if (it) artist else null) }, - errorCallback = { state.copy(currentArtistPlayLoading = null) } + errorCallback = { state = state.copy(currentArtistPlayLoading = null) } ) } } diff --git a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/home/components/ArtistItemSquare.kt b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/home/components/ArtistItemSquare.kt index cfd50e76..ef03a381 100644 --- a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/home/components/ArtistItemSquare.kt +++ b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/home/components/ArtistItemSquare.kt @@ -31,7 +31,6 @@ import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.material.icons.Icons import androidx.compose.material.icons.sharp.PlayArrow -import androidx.compose.material.icons.sharp.PlayCircleOutline import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.IconButtonDefaults diff --git a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/main/screens/LoggedInScreen.kt b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/main/screens/LoggedInScreen.kt index 6f8d9401..d9ddbfa8 100644 --- a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/main/screens/LoggedInScreen.kt +++ b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/main/screens/LoggedInScreen.kt @@ -53,9 +53,9 @@ import com.ramcosta.composedestinations.DestinationsNavHost import com.ramcosta.composedestinations.navigation.dependency import luci.sixsixsix.mrlog.L import luci.sixsixsix.powerampache2.R -import luci.sixsixsix.powerampache2.domain.models.Song import luci.sixsixsix.powerampache2.presentation.NavGraphs import luci.sixsixsix.powerampache2.presentation.dialogs.IntroDialog +import luci.sixsixsix.powerampache2.presentation.models.SongUI import luci.sixsixsix.powerampache2.presentation.screens.main.AuthViewModel import luci.sixsixsix.powerampache2.presentation.screens.main.screens.components.CheckCustomStoragePermissionDialog import luci.sixsixsix.powerampache2.presentation.screens.main.screens.components.SheetDragHandle @@ -166,5 +166,5 @@ fun LoggedInScreen( } @Composable -fun getPeakHeight(song: Song?): Dp = //TODO find a way to animate this (low-priority) +fun getPeakHeight(song: SongUI?): Dp = //TODO find a way to animate this (low-priority) if (song == null) { 0.dp } else { dimensionResource(id = R.dimen.miniPlayer_height) } diff --git a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/main/screens/LoginScreen.kt b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/main/screens/LoginScreen.kt index 3a2e3343..c2690870 100644 --- a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/main/screens/LoginScreen.kt +++ b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/main/screens/LoginScreen.kt @@ -182,7 +182,7 @@ fun LoginScreenContent( contentDescription = "Power Ampache Title" ) - if (!error.isNullOrBlank()) { + if (error.isNotBlank()) { ErrorView( errorString = error, modifier = Modifier diff --git a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/main/screens/MainContentScreen.kt b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/main/screens/MainContentScreen.kt index 1efb4754..7080cbe6 100644 --- a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/main/screens/MainContentScreen.kt +++ b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/main/screens/MainContentScreen.kt @@ -21,7 +21,6 @@ */ package luci.sixsixsix.powerampache2.presentation.screens.main.screens -import android.content.Intent import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.Spring import androidx.compose.animation.core.spring @@ -74,7 +73,6 @@ import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.painterResource diff --git a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/main/screens/components/CheckCustomStoragePermissionDialog.kt b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/main/screens/components/CheckCustomStoragePermissionDialog.kt index 987b619f..6426a30e 100644 --- a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/main/screens/components/CheckCustomStoragePermissionDialog.kt +++ b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/main/screens/components/CheckCustomStoragePermissionDialog.kt @@ -33,7 +33,8 @@ import luci.sixsixsix.powerampache2.presentation.screens.settings.SettingsViewMo true -> { val rootUri = settingsViewModel.playerSettingsStateFlow .collectAsStateWithLifecycle().value.customDownloadLocation - checkCustomStoragePermission(rootUri) { + + CheckCustomStoragePermission(rootUri) { settingsViewModel.onEvent(SettingsEvent.OnChooseCustomDirDownloads(it)) } } @@ -44,7 +45,7 @@ import luci.sixsixsix.powerampache2.presentation.screens.settings.SettingsViewMo } @Composable -private fun checkCustomStoragePermission(rootUri: Uri?, onSelectCustomDir: (Uri) -> Unit) { +private fun CheckCustomStoragePermission(rootUri: Uri?, onSelectCustomDir: (Uri) -> Unit) { rootUri?.let { uri -> val context = LocalContext.current if(!context.hasPersistedWritePermission(uri)) { diff --git a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/main/screens/components/Drawer.kt b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/main/screens/components/Drawer.kt index a8ccaea6..9c1bbd84 100644 --- a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/main/screens/components/Drawer.kt +++ b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/main/screens/components/Drawer.kt @@ -96,13 +96,13 @@ val drawerItems = listOf( @Composable fun MainDrawer( + modifier: Modifier = Modifier, user: User, versionInfo: String, hideDonationButtons: Boolean, currentItem: MainContentMenuItem, items: List = drawerItems, onItemClick: (MainContentMenuItem) -> Unit, - modifier: Modifier = Modifier, donateButton: @Composable () -> Unit = { DonateButton( isTransparent = true, modifier = Modifier.padding(horizontal = 16.dp) diff --git a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/main/screens/components/LoadingShimmerScreen.kt b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/main/screens/components/LoadingShimmerScreen.kt index 643864bd..c2ae7ca0 100644 --- a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/main/screens/components/LoadingShimmerScreen.kt +++ b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/main/screens/components/LoadingShimmerScreen.kt @@ -77,7 +77,7 @@ fun LoadingShimmerScreen() { @Composable fun LoadingShimmerScreenSection() { - Column() { + Column { Text( modifier = Modifier .width(200.dp) diff --git a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/main/screens/components/LoginSheet.kt b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/main/screens/components/LoginSheet.kt index 1cf6559f..4cb2d778 100644 --- a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/main/screens/components/LoginSheet.kt +++ b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/main/screens/components/LoginSheet.kt @@ -46,7 +46,6 @@ import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview diff --git a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/main/screens/components/LoginTextField.kt b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/main/screens/components/LoginTextField.kt index 479a7c63..b68b1933 100644 --- a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/main/screens/components/LoginTextField.kt +++ b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/main/screens/components/LoginTextField.kt @@ -25,7 +25,6 @@ import androidx.annotation.StringRes import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.material3.TextFieldDefaults diff --git a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/main/screens/components/LoginTextFields.kt b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/main/screens/components/LoginTextFields.kt index 0ff2bf18..a9fa1fc7 100644 --- a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/main/screens/components/LoginTextFields.kt +++ b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/main/screens/components/LoginTextFields.kt @@ -38,7 +38,6 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight diff --git a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/main/screens/components/MainContentTopAppBar.kt b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/main/screens/components/MainContentTopAppBar.kt index bd61a364..9fb70bde 100644 --- a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/main/screens/components/MainContentTopAppBar.kt +++ b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/main/screens/components/MainContentTopAppBar.kt @@ -38,7 +38,6 @@ import androidx.compose.material.icons.automirrored.filled.PlaylistAdd import androidx.compose.material.icons.filled.Cast import androidx.compose.material.icons.filled.Menu import androidx.compose.material.icons.filled.NotificationImportant -import androidx.compose.material.icons.filled.NotificationsNone import androidx.compose.material.icons.filled.PlayCircle import androidx.compose.material.icons.filled.QueueMusic import androidx.compose.material.icons.filled.Search @@ -64,7 +63,6 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import luci.sixsixsix.powerampache2.R import luci.sixsixsix.powerampache2.presentation.common.CircleBackButton -import luci.sixsixsix.powerampache2.presentation.dialogs.AddToPlaylistOrQueueDialogOpen @OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class) @Composable diff --git a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/main/screens/components/SignupSheet.kt b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/main/screens/components/SignupSheet.kt index 615d77bf..7bab432b 100644 --- a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/main/screens/components/SignupSheet.kt +++ b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/main/screens/components/SignupSheet.kt @@ -52,7 +52,6 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.KeyboardType diff --git a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/main/viewmodel/MainEvent.kt b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/main/viewmodel/MainEvent.kt index 8e9652c2..c80a31fe 100644 --- a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/main/viewmodel/MainEvent.kt +++ b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/main/viewmodel/MainEvent.kt @@ -21,18 +21,18 @@ */ package luci.sixsixsix.powerampache2.presentation.screens.main.viewmodel -import luci.sixsixsix.powerampache2.domain.models.Song +import luci.sixsixsix.powerampache2.presentation.models.SongUI sealed class MainEvent { data class OnSearchQueryChange(val query: String): MainEvent() data object OnDismissUserMessage: MainEvent() data object OnEnableOfflineMode: MainEvent() data object OnLogout: MainEvent() // TODO move this to AuthViewModel - data class AddSongsToQueueAndPlay(val song: Song, val songList: List): MainEvent() - data class AddSongsToQueueAndPlayShuffled(val songList: List): MainEvent() - data class PlaySongAddToQueueTop(val song: Song, val songList: List): MainEvent() - data class PlaySongReplacePlaylist(val song: Song, val songList: List): MainEvent() - data class PlaySong(val song: Song): MainEvent() + data class AddSongsToQueueAndPlay(val song: SongUI, val songList: List): MainEvent() + data class AddSongsToQueueAndPlayShuffled(val songList: List): MainEvent() + data class PlaySongAddToQueueTop(val song: SongUI, val songList: List): MainEvent() + data class PlaySongReplacePlaylist(val song: SongUI, val songList: List): MainEvent() + data class PlaySong(val song: SongUI): MainEvent() data object PlayPauseCurrent: MainEvent() data object SkipNext: MainEvent() data object SkipPrevious: MainEvent() @@ -43,18 +43,18 @@ sealed class MainEvent { data object Reset: MainEvent() data object FavouriteSong: MainEvent() data class UpdateProgress(val newProgress: Float): MainEvent() - data class OnAddSongToQueue(val song: Song): MainEvent() - data class OnAddSongToPlaylist(val song: Song): MainEvent() - data class OnAddSongToQueueNext(val song: Song): MainEvent() - data class OnShareSong(val song: Song): MainEvent() - data class OnShareSongWebUrl(val song: Song): MainEvent() - data class OnRateSong(val song: Song, val rate: Int): MainEvent() - data class OnDownloadSong(val song: Song): MainEvent() - data class OnDownloadSongs(val songs: List): MainEvent() + data class OnAddSongToQueue(val song: SongUI): MainEvent() + data class OnAddSongToPlaylist(val song: SongUI): MainEvent() + data class OnAddSongToQueueNext(val song: SongUI): MainEvent() + data class OnShareSong(val song: SongUI): MainEvent() + data class OnShareSongWebUrl(val song: SongUI): MainEvent() + data class OnRateSong(val song: SongUI, val rate: Int): MainEvent() + data class OnDownloadSong(val song: SongUI): MainEvent() + data class OnDownloadSongs(val songs: List): MainEvent() data object OnStopDownloadSongs: MainEvent() - data class OnDownloadedSongDelete(val song: Song): MainEvent() - data class OnDownloadedSongListDelete(val songs: List): MainEvent() - data class OnExportDownloadedSong(val song: Song): MainEvent() + data class OnDownloadedSongDelete(val song: SongUI): MainEvent() + data class OnDownloadedSongListDelete(val songs: List): MainEvent() + data class OnExportDownloadedSong(val song: SongUI): MainEvent() data object OnFabPress: MainEvent() data object OnCastPress: MainEvent() } diff --git a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/main/viewmodel/MainEventHandlerExt.kt b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/main/viewmodel/MainEventHandlerExt.kt index 8c75fe00..78c3db55 100644 --- a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/main/viewmodel/MainEventHandlerExt.kt +++ b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/main/viewmodel/MainEventHandlerExt.kt @@ -35,9 +35,9 @@ import luci.sixsixsix.powerampache2.common.exportSong import luci.sixsixsix.powerampache2.common.startCastPluginActivity import luci.sixsixsix.powerampache2.common.toMediaItem import luci.sixsixsix.powerampache2.worker.SongDownloadWorker -import luci.sixsixsix.powerampache2.domain.models.Song -import luci.sixsixsix.powerampache2.player.PlayerEvent import luci.sixsixsix.powerampache2.player.PlayerEvent.* +import luci.sixsixsix.powerampache2.presentation.models.SongUI +import luci.sixsixsix.powerampache2.presentation.models.toSong /** * UI ACTIONS AND EVENTS (play, stop, skip, like, download, etc ...) @@ -93,10 +93,10 @@ fun MainViewModel.handleEvent(event: MainEvent, context: Context) { is MainEvent.OnDownloadSong -> downloadSong(event.song) is MainEvent.OnShareSong -> viewModelScope.launch { - shareManager.shareSongDeepLink(context, event.song) + shareManager.shareSongDeepLink(context, event.song.toSong()) } is MainEvent.OnShareSongWebUrl -> viewModelScope.launch { - shareManager.shareSongWeb(context, event.song) + shareManager.shareSongWeb(context, event.song.toSong()) } is MainEvent.Repeat -> viewModelScope.launch { val nextRepeatMode = nextRepeatMode() @@ -108,20 +108,20 @@ fun MainViewModel.handleEvent(event: MainEvent, context: Context) { shuffleOn = event.shuffleOn } is MainEvent.SkipNext -> viewModelScope.launch { - simpleMediaServiceHandler.onPlayerEvent(PlayerEvent.SkipForward) + simpleMediaServiceHandler.onPlayerEvent(SkipForward) } is MainEvent.SkipPrevious -> viewModelScope.launch { - simpleMediaServiceHandler.onPlayerEvent(PlayerEvent.SkipBack) + simpleMediaServiceHandler.onPlayerEvent(SkipBack) } is MainEvent.UpdateProgress -> viewModelScope.launch { progress = event.newProgress simpleMediaServiceHandler.onPlayerEvent(Progress(event.newProgress)) } MainEvent.Backwards -> viewModelScope.launch { - simpleMediaServiceHandler.onPlayerEvent(PlayerEvent.Backward) + simpleMediaServiceHandler.onPlayerEvent(Backward) } MainEvent.Forward -> viewModelScope.launch { - simpleMediaServiceHandler.onPlayerEvent(PlayerEvent.Forward) + simpleMediaServiceHandler.onPlayerEvent(Forward) } MainEvent.FavouriteSong -> currentSong()?.let { favouriteSong(it) @@ -144,7 +144,10 @@ fun MainViewModel.handleEvent(event: MainEvent, context: Context) { } is MainEvent.OnExportDownloadedSong -> viewModelScope.launch { try { - context.exportSong(event.song, songsRepository.getSongUri(event.song)) + context.exportSong( + mimeType = event.song.mime, + offlineUri = songsRepository.getSongUri(event.song.toSong()), + ) } catch (e: Exception) { errorHandler.updateErrorLogMessage(e.stackTraceToString()) } @@ -169,17 +172,18 @@ fun MainViewModel.handleEvent(event: MainEvent, context: Context) { // send queue to cast plugin if (isChromecastPluginInstalled()) { viewModelScope.launch { - sendQueueToChromecastUseCase(currentQueue().value).also { isSuccess -> - if (!isSuccess) { - // this is just a safety net, the error should never happen because - // the queue has been reduced before sending it to the Cast plugin. - Toast.makeText(context, - context.getString(R.string.plugin_cast_queueTooLarge_error), - Toast.LENGTH_LONG - ).show() - // TODO: showing toast from view model, violating Clean Architecture? + sendQueueToChromecastUseCase(currentQueue().value.toSong()) + .also { isSuccess -> + if (!isSuccess) { + // this is just a safety net, the error should never happen because + // the queue has been reduced before sending it to the Cast plugin. + Toast.makeText(context, + context.getString(R.string.plugin_cast_queueTooLarge_error), + Toast.LENGTH_LONG + ).show() + // TODO: showing toast from view model, violating Clean Architecture? + } } - } } } context.startCastPluginActivity() @@ -192,7 +196,7 @@ fun MainViewModel.handleEvent(event: MainEvent, context: Context) { * to play albums and playlists */ @UnstableApi -fun MainViewModel.addSongsToQueueAndPlay(song: Song, songList: List) { +fun MainViewModel.addSongsToQueueAndPlay(song: SongUI, songList: List) { startPlayLoading() playlistManager.updateCurrentSong(song) playlistManager.addToCurrentQueueTop(songList) @@ -203,7 +207,7 @@ fun MainViewModel.addSongsToQueueAndPlay(song: Song, songList: List) { * select a single song, play, and put it on the top of the queue * the song list is just for verification (TODO: should that be optional?) */ -private fun MainViewModel.playSongAddToQueueTop(song: Song, songList: List) { +private fun MainViewModel.playSongAddToQueueTop(song: SongUI, songList: List) { startPlayLoading() playlistManager.addToCurrentQueueUpdateTopSong(song, songList) play(song) @@ -214,7 +218,7 @@ private fun MainViewModel.playSongAddToQueueTop(song: Song, songList: List * the song list is just for verification (TODO: should that be optional?) */ @OptIn(UnstableApi::class) -private fun MainViewModel.playSongReplacePlaylist(song: Song, songList: List) { +private fun MainViewModel.playSongReplacePlaylist(song: SongUI, songList: List) { startPlayLoading() playlistManager.replaceQueuePlaySong(songList, song) play(song) @@ -223,13 +227,13 @@ private fun MainViewModel.playSongReplacePlaylist(song: Song, songList: List) { +private fun MainViewModel.addSongsToQueueAndPlayShuffled(songList: List) { startPlayLoading() val shuffled = songList.shuffled() playlistManager.replaceCurrentQueue(shuffled) @@ -246,7 +250,7 @@ private fun MainViewModel.addSongsToQueueAndPlayShuffled(songList: List) * * call stopPlayLoading() in case of errors */ -private fun MainViewModel.play(song: Song) { +private fun MainViewModel.play(song: SongUI) { startPlayLoading() if (loadSongDataJob?.isActive == true) { loadSongDataJob?.invokeOnCompletion { @@ -263,17 +267,19 @@ private fun MainViewModel.play(song: Song) { } } -private fun MainViewModel.playSongForce(song: Song) = viewModelScope.launch { +private fun MainViewModel.playSongForce(song: SongUI) = viewModelScope.launch { L( "MainEvent.Play", "playing song") try { simpleMediaServiceHandler.onPlayerEvent( - PlayerEvent.ForcePlay( - song.toMediaItem(songsRepository.getSongUri(song))) + ForcePlay( + song.toMediaItem(songsRepository.getSongUri(song.toSong())) + ) ) } catch (e: Exception) { logToErrorLogs("fun MainViewModel.playSongForce EXCEPTION, loading song data now") logToErrorLogs(e.stackTraceToString()) } + stopPlayLoading() L( "MainEvent.Play", "aaaa play song launched. After") } @@ -283,7 +289,7 @@ private fun MainViewModel.playPauseSong() = viewModelScope.launch { startMusicServiceIfNecessary() L( "MainEvent.Play", "playing song") try { - simpleMediaServiceHandler.onPlayerEvent(PlayerEvent.PlayPause) + simpleMediaServiceHandler.onPlayerEvent(PlayPause) } catch (e: Exception) { stopPlayLoading() logToErrorLogs(e.stackTraceToString()) diff --git a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/main/viewmodel/MainViewModel.kt b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/main/viewmodel/MainViewModel.kt index 193beed0..f1812cc2 100644 --- a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/main/viewmodel/MainViewModel.kt +++ b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/main/viewmodel/MainViewModel.kt @@ -58,8 +58,7 @@ import luci.sixsixsix.powerampache2.domain.SongsRepository import luci.sixsixsix.powerampache2.domain.common.Constants import luci.sixsixsix.powerampache2.domain.common.WeakContext import luci.sixsixsix.powerampache2.domain.errors.ErrorHandler -import luci.sixsixsix.powerampache2.domain.models.Song -import luci.sixsixsix.powerampache2.domain.models.isFavourite +import luci.sixsixsix.powerampache2.presentation.models.isFavourite import luci.sixsixsix.powerampache2.domain.usecase.DownloadSongUseCase import luci.sixsixsix.powerampache2.domain.usecase.SessionFlowUseCase import luci.sixsixsix.powerampache2.domain.usecase.plugin.IsChromecastPluginInstalled @@ -75,6 +74,9 @@ import luci.sixsixsix.powerampache2.player.MusicPlaylistManager import luci.sixsixsix.powerampache2.player.PlayerEvent import luci.sixsixsix.powerampache2.player.RepeatMode import luci.sixsixsix.powerampache2.player.SimpleMediaServiceHandler +import luci.sixsixsix.powerampache2.presentation.models.SongUI +import luci.sixsixsix.powerampache2.presentation.models.toSong +import luci.sixsixsix.powerampache2.presentation.models.toSongUI import javax.inject.Inject import kotlin.math.abs @@ -130,8 +132,8 @@ class MainViewModel @Inject constructor( var emittedDownloads by savedStateHandle.saveable { mutableStateOf(listOf()) } // TODO: there is no queue to restore! because the queue is in MusicPlaylistManager - var restoredSong: Song? = null - var restoredQueue = listOf() + var restoredSong: SongUI? = null + var restoredQueue = listOf() val mainLock = Any() @@ -147,6 +149,7 @@ class MainViewModel @Inject constructor( delay(6000) if (Constants.config.featureString.isNotBlank() && application.isFeatureAvailable(Constants.config.featureString)) { + // TODO: investigate if we should use a kotlin function here instead System.exit(0) } } @@ -161,10 +164,6 @@ class MainViewModel @Inject constructor( fun onEvent(event: MainEvent) = weakContext.get()?.applicationContext?.let { handleEvent(event, it) } - fun isOfflineSong(song: Song, callback: (Boolean) -> Unit) = viewModelScope.launch { - callback(isSongAvailableOfflineUseCase(song)) - } - /** * set isPlayLoading to true, the play button is listening to this variable */ @@ -194,7 +193,9 @@ class MainViewModel @Inject constructor( songsRepository.getSongsForQuickPlay().collect { result -> when (result) { is Resource.Success -> { - result.data?.let { songs -> + result.data?.toSongUI { + isSongAvailableOfflineUseCase(it) + }?.let { songs -> if (songs.isNotEmpty()) { addSongsToQueueAndPlay(songs[0], songs) } @@ -207,7 +208,7 @@ class MainViewModel @Inject constructor( } } - fun favouriteSong(song: Song) = viewModelScope.launch { + fun favouriteSong(song: SongUI) = viewModelScope.launch { songsRepository.likeSong(song.mediaId, (song.flag != 1)).collect { result -> when (result) { is Resource.Success -> result.data?.let { @@ -221,7 +222,7 @@ class MainViewModel @Inject constructor( } } - fun rateSong(song: Song, rate: Int) = viewModelScope.launch { + fun rateSong(song: SongUI, rate: Int) = viewModelScope.launch { songsRepository.rateSong(song.mediaId, rate).collect { result -> when (result) { is Resource.Success -> result.data?.let { @@ -265,11 +266,18 @@ class MainViewModel @Inject constructor( private suspend fun playDeepLinkedSong(id: String, title: String, artist: String, webLink: String) { shareManager.fetchDeepLinkedSong(id, title, artist, songCallback = { - onEvent(MainEvent.PlaySongAddToQueueTop(it, currentQueue().value)) - }, + onEvent(MainEvent.PlaySongAddToQueueTop( + song = it.toSongUI( + isSongAvailableOfflineUseCase(it) + ), + songList = currentQueue().value) + )}, songsCallback = { - onEvent(MainEvent.AddSongsToQueueAndPlayShuffled(it)) - }, + onEvent(MainEvent.AddSongsToQueueAndPlayShuffled( + songList = it.toSongUI { song -> + isSongAvailableOfflineUseCase(song) + } + ))}, errorCallback = { weakContext.get()?.let { context -> if (webLink.isNotBlank()) { @@ -282,8 +290,8 @@ class MainViewModel @Inject constructor( ) } - fun downloadSong(song: Song) = viewModelScope.launch { - songsRepository.downloadSong(song).collect { result -> + fun downloadSong(song: SongUI) = viewModelScope.launch { + songsRepository.downloadSong(song.toSong()).collect { result -> when (result) { is Resource.Success -> { result.data?.let { @@ -297,12 +305,12 @@ class MainViewModel @Inject constructor( } } - fun downloadSongs(songs: List) { - viewModelScope.launch { songsRepository.downloadSongs(songs) } + fun downloadSongs(songs: List) { + viewModelScope.launch { songsRepository.downloadSongs(songs.toSong()) } } - fun deleteDownloadedSong(song: Song) = viewModelScope.launch { - songsRepository.deleteDownloadedSong(song).collect { result -> + fun deleteDownloadedSong(song: SongUI) = viewModelScope.launch { + songsRepository.deleteDownloadedSong(song.toSong()).collect { result -> when (result) { is Resource.Success -> { result.data?.let { @@ -317,10 +325,10 @@ class MainViewModel @Inject constructor( } } - fun deleteDownloadedSongs(songs: List) = viewModelScope.launch { + fun deleteDownloadedSongs(songs: List) = viewModelScope.launch { var count = 0 songs.forEach { song -> - songsRepository.deleteDownloadedSong(song).collect { result -> + songsRepository.deleteDownloadedSong(song.toSong()).collect { result -> when (result) { is Resource.Success -> { result.data?.let { ++count } @@ -373,9 +381,9 @@ class MainViewModel @Inject constructor( logToErrorLogs("Load song data START") val mediaItemList = mutableListOf() - for (song: Song? in playlistManager.currentQueueState.value) { + for (song: SongUI? in playlistManager.currentQueueState.value) { song?.let { - mediaItemList.add(it.toMediaItem(songsRepository.getSongUri(it))) + mediaItemList.add(it.toMediaItem(songsRepository.getSongUri(it.toSong()))) } } @@ -409,14 +417,14 @@ class MainViewModel @Inject constructor( RepeatMode.ALL -> RepeatMode.OFF } - fun scrobble(song: Song) { + fun scrobble(song: SongUI) { scrobbleJob?.cancel() scrobbleJob = viewModelScope.launch { delay(LOCAL_SCROBBLE_TIMEOUT_MS) // add song to history after 30s - songsRepository.addToHistory(song) + songsRepository.addToHistory(song.toSong()) // send scrobble to backend - songsRepository.scrobble(song).collect { response -> + songsRepository.scrobble(song.toSong()).collect { response -> when (response) { is Resource.Error -> { } is Resource.Loading -> { } @@ -426,7 +434,7 @@ class MainViewModel @Inject constructor( } } - fun downloadAfterPlayback(song: Song) { + fun downloadAfterPlayback(song: SongUI) { // do not cancel, let the previous finish download // downloadAfterPlaybackJob?.cancel() downloadAfterPlaybackJob = viewModelScope.launch { // start downloading half way diff --git a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/main/viewmodel/ObserveDownloadsExt.kt b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/main/viewmodel/ObserveDownloadsExt.kt index f3bceac4..d06eb52e 100644 --- a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/main/viewmodel/ObserveDownloadsExt.kt +++ b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/main/viewmodel/ObserveDownloadsExt.kt @@ -22,7 +22,6 @@ package luci.sixsixsix.powerampache2.presentation.screens.main.viewmodel import android.content.Context -import androidx.compose.ui.res.stringResource import androidx.lifecycle.viewModelScope import androidx.work.WorkInfo import androidx.work.WorkManager @@ -30,6 +29,7 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.launch import luci.sixsixsix.mrlog.L import luci.sixsixsix.powerampache2.R +import luci.sixsixsix.powerampache2.presentation.models.toSongUI import luci.sixsixsix.powerampache2.worker.SongDownloadWorker internal fun MainViewModel.observeDownloads(application: Context) { @@ -71,12 +71,12 @@ internal fun MainViewModel.observeDownloads(application: Context) { } if (workInfo.state == WorkInfo.State.SUCCEEDED) { - workInfo?.outputData?.getString(SongDownloadWorker.KEY_RESULT_SONG)?.let { songId -> + workInfo.outputData.getString(SongDownloadWorker.KEY_RESULT_SONG)?.let { songId -> viewModelScope.launch { if (!emittedDownloads.contains(songId)) { emittedDownloads = emittedDownloads.toMutableList().apply { add(songId) } songsRepository.getDownloadedSongById(songId)?.let { finishedSong -> - playlistManager.updateDownloadedSong(finishedSong) + playlistManager.updateDownloadedSong(finishedSong.toSongUI()) errorHandler.updateUserMessage( application.getString(R.string.downloaded_snackbar_title, finishedSong.name) ) //"${finishedSong.name} downloaded") diff --git a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/main/viewmodel/ObservePlaylistManagerExt.kt b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/main/viewmodel/ObservePlaylistManagerExt.kt index 6c4b8f2d..1b393fde 100644 --- a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/main/viewmodel/ObservePlaylistManagerExt.kt +++ b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/main/viewmodel/ObservePlaylistManagerExt.kt @@ -64,11 +64,10 @@ fun MainViewModel.observePlaylistManager() { // listen to queue changes viewModelScope.launch { - playlistManager.currentQueueState.collectLatest { q -> - val queue = q.filterNotNull() - if (!queue.isNullOrEmpty()) { + playlistManager.currentQueueState.collectLatest { queue -> + if (queue.isNotEmpty()) { startMusicServiceIfNecessary() - } else if (queue.isNullOrEmpty() && currentSong() == null) { + } else if (currentSong() == null) { stopMusicService() } L("**** observing playlist change queue (before Load song data) :", queue.size) diff --git a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/main/viewmodel/ObserveSessionExt.kt b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/main/viewmodel/ObserveSessionExt.kt index 70f06d18..6ad903d8 100644 --- a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/main/viewmodel/ObserveSessionExt.kt +++ b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/main/viewmodel/ObserveSessionExt.kt @@ -32,7 +32,7 @@ fun MainViewModel.observeSession() { synchronized(mainLock) { val oldToken = authToken authToken = it?.auth ?: "" - logToErrorLogs(" old toke $oldToken, new one: $authToken") + logToErrorLogs(" old token $oldToken, new one: $authToken") if (authToken.isNotBlank()) { // refresh the playlist with new urls with the new token // only if a queue exists diff --git a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/notifications/NotificationsViewModel.kt b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/notifications/NotificationsViewModel.kt index 43a14430..580177c8 100644 --- a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/notifications/NotificationsViewModel.kt +++ b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/notifications/NotificationsViewModel.kt @@ -24,7 +24,6 @@ package luci.sixsixsix.powerampache2.presentation.screens.notifications import androidx.lifecycle.ViewModel import dagger.hilt.android.lifecycle.HiltViewModel import luci.sixsixsix.powerampache2.domain.errors.ErrorHandler -import luci.sixsixsix.powerampache2.player.MusicPlaylistManager import javax.inject.Inject @HiltViewModel diff --git a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/offline/OfflineSongsEvent.kt b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/offline/OfflineSongsEvent.kt index 8e04c222..daf3d5af 100644 --- a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/offline/OfflineSongsEvent.kt +++ b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/offline/OfflineSongsEvent.kt @@ -1,7 +1,7 @@ package luci.sixsixsix.powerampache2.presentation.screens.offline -import luci.sixsixsix.powerampache2.domain.models.Song +import luci.sixsixsix.powerampache2.presentation.models.SongUI sealed class OfflineSongsEvent { - data class OnSongSelected(val song: Song): OfflineSongsEvent() + data class OnSongSelected(val song: SongUI): OfflineSongsEvent() } \ No newline at end of file diff --git a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/offline/OfflineSongsScreen.kt b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/offline/OfflineSongsScreen.kt index b16a0edc..39ffdcff 100644 --- a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/offline/OfflineSongsScreen.kt +++ b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/offline/OfflineSongsScreen.kt @@ -80,6 +80,7 @@ import luci.sixsixsix.powerampache2.presentation.dialogs.EraseConfirmDialog import luci.sixsixsix.powerampache2.presentation.dialogs.ShareDialog import luci.sixsixsix.powerampache2.presentation.dialogs.info.InfoDialogSong import luci.sixsixsix.powerampache2.presentation.dialogs.info.ShowSongInfoDialogOpen +import luci.sixsixsix.powerampache2.presentation.models.SongUI import luci.sixsixsix.powerampache2.presentation.navigation.Ampache2NavGraphs import luci.sixsixsix.powerampache2.presentation.screens.main.viewmodel.MainEvent import luci.sixsixsix.powerampache2.presentation.screens.main.viewmodel.MainViewModel @@ -98,7 +99,7 @@ fun OfflineSongsScreen( // This variable can be passed into subscreen which will handle the creation of the playlist. // For example the offline screen will add offline songs to a playlist, the queue screen will - // add the queue, etc.. + // add the queue, etc. // The button that sets this variable is the add-to-playlist button on the top bar val isPlaylistAddDialogOpen = remember { mutableStateOf(false) } val titleOfflineSongs = stringResource(R.string.menu_drawer_offline) @@ -168,10 +169,10 @@ fun OfflineSongsScreen( .padding(top = dimensionResource(id = R.dimen.albumDetailScreen_top_padding)), ) { OfflineSongsMainContent( + modifier = modifier, navigator = navigator, mainViewModel = mainViewModel, viewModel = viewModel, - modifier = modifier, playlistOrQueueDialogOpen = isPlaylistAddDialogOpen, offlineScreenBarTitle = { title -> barTitle = title @@ -184,9 +185,9 @@ fun OfflineSongsScreen( @Composable @Destination(start = false) fun OfflineSongsMainContent( + modifier: Modifier = Modifier, navigator: DestinationsNavigator? = Ampache2NavGraphs.navigator, mainViewModel: MainViewModel, - modifier: Modifier = Modifier, playlistOrQueueDialogOpen: MutableState, viewModel: OfflineSongsViewModel = hiltViewModel(), addToPlaylistOrQueueDialogViewModel: AddToPlaylistOrQueueDialogViewModel = hiltViewModel(), @@ -226,7 +227,7 @@ fun OfflineSongsMainContent( } } - var showDeleteSongDialog by remember { mutableStateOf(null) } + var showDeleteSongDialog by remember { mutableStateOf(null) } showDeleteSongDialog?.let { songToRemove -> EraseConfirmDialog( onDismissRequest = { @@ -242,7 +243,7 @@ fun OfflineSongsMainContent( ) } - var showDeleteFromDownloadsDialog by remember { mutableStateOf(null) } + var showDeleteFromDownloadsDialog by remember { mutableStateOf(null) } showDeleteFromDownloadsDialog?.let { songToRemove -> EraseConfirmDialog( onDismissRequest = { @@ -266,7 +267,7 @@ fun OfflineSongsMainContent( } } - var songToShare: Song? by remember { mutableStateOf(null) } + var songToShare: SongUI? by remember { mutableStateOf(null) } AnimatedVisibility(songToShare != null) { songToShare?.let { songS -> ShareDialog( @@ -297,7 +298,6 @@ fun OfflineSongsMainContent( items(state.songs) { song -> SongItem( song = song, - isSongDownloaded = true, showDownloadedSongMarker = false, songItemEventListener = { event -> when(event) { diff --git a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/offline/OfflineSongsState.kt b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/offline/OfflineSongsState.kt index 461831f5..25f221e1 100644 --- a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/offline/OfflineSongsState.kt +++ b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/offline/OfflineSongsState.kt @@ -1,8 +1,8 @@ package luci.sixsixsix.powerampache2.presentation.screens.offline -import luci.sixsixsix.powerampache2.domain.models.Song +import luci.sixsixsix.powerampache2.presentation.models.SongUI data class OfflineSongsState( - val songs: List = emptyList(), + val songs: List = emptyList(), val isLoading: Boolean = false, ) diff --git a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/offline/OfflineSongsViewModel.kt b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/offline/OfflineSongsViewModel.kt index 89a361b7..7f56fd9d 100644 --- a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/offline/OfflineSongsViewModel.kt +++ b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/offline/OfflineSongsViewModel.kt @@ -29,6 +29,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import luci.sixsixsix.powerampache2.domain.usecase.songs.OfflineSongsFlow +import luci.sixsixsix.powerampache2.presentation.models.toSongUI import javax.inject.Inject @HiltViewModel @@ -40,7 +41,9 @@ class OfflineSongsViewModel @Inject constructor( // TODO check consistency of downloaded songs and database entries every time, // delete data accordingly. Do this in data layer OfflineSongsState( - songs = songs, + songs = songs.map { + it.toSongUI(true) + }, isLoading = false ) } diff --git a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/playlists/PlaylistsViewModel.kt b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/playlists/PlaylistsViewModel.kt index 5e07caf5..4af43020 100644 --- a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/playlists/PlaylistsViewModel.kt +++ b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/playlists/PlaylistsViewModel.kt @@ -40,6 +40,7 @@ import luci.sixsixsix.powerampache2.common.Resource import luci.sixsixsix.powerampache2.domain.PlaylistsRepository import luci.sixsixsix.powerampache2.domain.common.Constants.ALWAYS_FETCH_ALL_PLAYLISTS import luci.sixsixsix.powerampache2.domain.models.Playlist +import luci.sixsixsix.powerampache2.domain.models.isOwnerAdmin import luci.sixsixsix.powerampache2.domain.usecase.UserFlowUseCase import luci.sixsixsix.powerampache2.domain.usecase.playlists.PlaylistsFlow import luci.sixsixsix.powerampache2.domain.usecase.playlists.PlaylistsUseCase @@ -75,6 +76,7 @@ class PlaylistsViewModel @Inject constructor( when (event) { is PlaylistEvent.Refresh -> getPlaylists(fetchRemote = true) + // TODO: inverse the check to get rid of if empty body is PlaylistEvent.OnSearchQueryChange -> if (event.query.isBlank() && state.searchQuery.isBlank()) { } else { state = state.copy(searchQuery = event.query) @@ -109,10 +111,11 @@ class PlaylistsViewModel @Inject constructor( .collect { result -> when (result) { is Resource.Success -> { + // TODO: why is it commented out only partially? Check if needed at all result.data?.let { playlists -> - // playlist updated automatically through live data from database -// state = state.copy(playlists = playlists) -// L("viewmodel.getPlaylists size", state.playlists.size) + // // playlist updated automatically through live data from database + // state = state.copy(playlists = playlists) + // L("viewmodel.getPlaylists size", state.playlists.size) } isEndOfDataReached = (result.networkData?.isEmpty() == true && offset > 0) diff --git a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/queue/QueueEvent.kt b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/queue/QueueEvent.kt index bdc04ea1..c665f788 100644 --- a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/queue/QueueEvent.kt +++ b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/queue/QueueEvent.kt @@ -1,10 +1,10 @@ package luci.sixsixsix.powerampache2.presentation.screens.queue -import luci.sixsixsix.powerampache2.domain.models.Song +import luci.sixsixsix.powerampache2.presentation.models.SongUI sealed class QueueEvent { - data class OnSongSelected(val song: Song): QueueEvent() - data class OnSongRemove(val song: Song): QueueEvent() + data class OnSongSelected(val song: SongUI): QueueEvent() + data class OnSongRemove(val song: SongUI): QueueEvent() data object OnPlayQueue: QueueEvent() data object OnClearQueue: QueueEvent() } diff --git a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/queue/QueueViewModel.kt b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/queue/QueueViewModel.kt index ad84a2e2..ada75b84 100644 --- a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/queue/QueueViewModel.kt +++ b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/queue/QueueViewModel.kt @@ -24,7 +24,6 @@ package luci.sixsixsix.powerampache2.presentation.screens.queue import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.onEach import luci.sixsixsix.powerampache2.player.MusicPlaylistManager import luci.sixsixsix.powerampache2.player.SimpleMediaServiceHandler import javax.inject.Inject diff --git a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/queue/components/QueueScreenContent.kt b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/queue/components/QueueScreenContent.kt index 70b5acc0..a1a63b11 100644 --- a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/queue/components/QueueScreenContent.kt +++ b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/queue/components/QueueScreenContent.kt @@ -40,7 +40,6 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import com.ramcosta.composedestinations.navigation.DestinationsNavigator import luci.sixsixsix.powerampache2.R -import luci.sixsixsix.powerampache2.domain.models.Song import luci.sixsixsix.powerampache2.presentation.common.songitem.SongItem import luci.sixsixsix.powerampache2.presentation.common.songitem.SongItemEvent import luci.sixsixsix.powerampache2.presentation.common.songitem.SubtitleString @@ -52,6 +51,7 @@ import luci.sixsixsix.powerampache2.presentation.dialogs.EraseConfirmDialog import luci.sixsixsix.powerampache2.presentation.dialogs.ShareDialog import luci.sixsixsix.powerampache2.presentation.dialogs.info.InfoDialogSong import luci.sixsixsix.powerampache2.presentation.dialogs.info.ShowSongInfoDialogOpen +import luci.sixsixsix.powerampache2.presentation.models.SongUI import luci.sixsixsix.powerampache2.presentation.navigation.Ampache2NavGraphs import luci.sixsixsix.powerampache2.presentation.screens.main.viewmodel.MainEvent import luci.sixsixsix.powerampache2.presentation.screens.main.viewmodel.MainViewModel @@ -86,7 +86,7 @@ fun QueueScreenContent( } } - var showRemoveFromQueueDialog by remember { mutableStateOf(null) } + var showRemoveFromQueueDialog by remember { mutableStateOf(null) } showRemoveFromQueueDialog?.let { songToRemove -> EraseConfirmDialog( onDismissRequest = { @@ -101,7 +101,7 @@ fun QueueScreenContent( ) } - var showDeleteFromDownloadsDialog by remember { mutableStateOf(null) } + var showDeleteFromDownloadsDialog by remember { mutableStateOf(null) } showDeleteFromDownloadsDialog?.let { songToRemove -> EraseConfirmDialog( onDismissRequest = { @@ -125,7 +125,7 @@ fun QueueScreenContent( } } - var songToShare: Song? by remember { mutableStateOf(null) } + var songToShare: SongUI? by remember { mutableStateOf(null) } AnimatedVisibility(songToShare != null) { songToShare?.let { songS -> ShareDialog( diff --git a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/search/SearchResultsScreen.kt b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/search/SearchResultsScreen.kt index 8853b991..84ddf1b2 100644 --- a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/search/SearchResultsScreen.kt +++ b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/search/SearchResultsScreen.kt @@ -132,7 +132,7 @@ fun SearchResultsScreen( } } else if (searchState.isNoResults) { // show no results screen (search query present but no results) - showHideEmptyResultsView(searchState.isLoading, searchState.isFetchingMore, searchState.isNoResults) + ShowHideEmptyResultsView(searchState.isLoading, searchState.isFetchingMore, searchState.isNoResults) } else { // show search results ResultsListView( @@ -172,7 +172,7 @@ fun SearchResultsScreen( } @Composable -private fun showHideEmptyResultsView(isLoading: Boolean, isRefreshing: Boolean, isNoResults: Boolean) { +private fun ShowHideEmptyResultsView(isLoading: Boolean, isRefreshing: Boolean, isNoResults: Boolean) { if (!isLoading && !isRefreshing && isNoResults){ Card(modifier = Modifier.fillMaxSize()) { Column( diff --git a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/search/SearchScreenState.kt b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/search/SearchScreenState.kt index 7859d7bc..769a6cff 100644 --- a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/search/SearchScreenState.kt +++ b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/search/SearchScreenState.kt @@ -28,13 +28,13 @@ import luci.sixsixsix.powerampache2.domain.models.Album import luci.sixsixsix.powerampache2.domain.models.Artist import luci.sixsixsix.powerampache2.domain.models.Genre import luci.sixsixsix.powerampache2.domain.models.Playlist -import luci.sixsixsix.powerampache2.domain.models.Song +import luci.sixsixsix.powerampache2.presentation.models.SongUI @Parcelize data class SearchScreenState ( val selectedGenre: Genre? = null, val genres: List = emptyList(), - val songs: List = emptyList(), + val songs: List = emptyList(), val albums: List = emptyList(), val artists: List = emptyList(), val playlists: List = emptyList(), diff --git a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/search/SearchViewModel.kt b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/search/SearchViewModel.kt index fc448d48..e07b3265 100644 --- a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/search/SearchViewModel.kt +++ b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/search/SearchViewModel.kt @@ -47,10 +47,12 @@ import luci.sixsixsix.powerampache2.domain.usecase.artists.ArtistsByGenreUseCase import luci.sixsixsix.powerampache2.domain.usecase.artists.ArtistsUseCase import luci.sixsixsix.powerampache2.domain.usecase.playlists.PlaylistsUseCase import luci.sixsixsix.powerampache2.domain.usecase.settings.LocalSettingsFlowUseCase +import luci.sixsixsix.powerampache2.domain.usecase.songs.IsSongAvailableOfflineUseCase import luci.sixsixsix.powerampache2.domain.usecase.songs.GetSongsUseCase import luci.sixsixsix.powerampache2.domain.usecase.songs.OfflineSongsFlow import luci.sixsixsix.powerampache2.domain.usecase.songs.SongsByGenreUseCase import luci.sixsixsix.powerampache2.player.MusicPlaylistManager +import luci.sixsixsix.powerampache2.presentation.models.toSongUI import javax.inject.Inject // minimum allowed size for a search query to trigger a search @@ -58,6 +60,7 @@ private const val MIN_QUERY_SIZE = 3 @HiltViewModel class SearchViewModel @Inject constructor( + private val isSongAvailableOfflineUseCase: IsSongAvailableOfflineUseCase, private val genresUseCase: GenresUseCase, private val getSongsUseCase: GetSongsUseCase, private val songsByGenreUseCase: SongsByGenreUseCase, @@ -147,7 +150,9 @@ class SearchViewModel @Inject constructor( private suspend fun fetchGenresOffline() = getSongsUseCase().collect { result -> when (result) { is Resource.Success -> - result.data?.let { songs -> + result.data?.toSongUI { + isSongAvailableOfflineUseCase(it) + }?.let { songs -> val genres: List = HashSet().apply { songs.map { it.genre }.forEach { attributes -> addAll(attributes.map { Genre( @@ -184,7 +189,9 @@ class SearchViewModel @Inject constructor( songsByGenreUseCase(genre).collect { result -> when (result) { is Resource.Success -> - result.data?.let { songs -> + result.data?.toSongUI { + isSongAvailableOfflineUseCase(it) + }?.let { songs -> state = state.copy(songs = songs) } is Resource.Error -> @@ -198,11 +205,12 @@ class SearchViewModel @Inject constructor( getSongsUseCase().collect { result -> when (result) { is Resource.Success -> - result.data?.let { songs -> - val mapped = songs.filter { + result.data?.toSongUI { + isSongAvailableOfflineUseCase(it) + }?.let { songs -> + state = state.copy(songs = songs.filter { it.genre.joinToString(", ").contains(genre.name) - } - state = state.copy(songs = mapped) + }) } is Resource.Error -> state = state.copy(isLoading = false) @@ -252,7 +260,9 @@ class SearchViewModel @Inject constructor( if (state.searchQuery.isNotBlank()) { // only display the list if there is a search term present. // Avoids race conditions when quickly deleting and re-typing search terms. - result.data?.let { songs -> + result.data?.toSongUI { + isSongAvailableOfflineUseCase(it) + }?.let { songs -> state = state.copy(songs = songs) } } diff --git a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/search/screens/ResultsListView.kt b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/search/screens/ResultsListView.kt index 79ce4b8a..b28852f0 100644 --- a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/search/screens/ResultsListView.kt +++ b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/search/screens/ResultsListView.kt @@ -38,16 +38,16 @@ import luci.sixsixsix.powerampache2.domain.models.Album import luci.sixsixsix.powerampache2.domain.models.AmpacheModel import luci.sixsixsix.powerampache2.domain.models.Artist import luci.sixsixsix.powerampache2.domain.models.Playlist -import luci.sixsixsix.powerampache2.domain.models.Song import luci.sixsixsix.powerampache2.presentation.common.AmpacheListItem import luci.sixsixsix.powerampache2.presentation.common.songitem.SongItemEvent +import luci.sixsixsix.powerampache2.presentation.models.SongUI import luci.sixsixsix.powerampache2.presentation.screens.main.viewmodel.MainEvent import luci.sixsixsix.powerampache2.presentation.screens.search.SearchViewEvent @Composable @Destination fun ResultsListView( - songs: List, + songs: List, albums: List, artists: List, playlists: List, @@ -56,14 +56,14 @@ fun ResultsListView( isRefreshing: Boolean, modifier: Modifier = Modifier, onEvent: (SearchViewEvent) -> Unit, - onSongSelected: (Song) -> Unit, + onSongSelected: (SongUI) -> Unit, onAlbumSelected: (albumId: String, album: Album?) -> Unit, onArtistSelected: (artistId: String, artist: Artist?) -> Unit, onPlaylistSelected: (Playlist) -> Unit, onSongEvent: (MainEvent) -> Unit, - onOpenPlaylistDialog: (List) -> Unit, - onShowDeleteFromDownloadsDialog: (Song) -> Unit, - onShowSongInfoDialog: (Song) -> Unit + onOpenPlaylistDialog: (List) -> Unit, + onShowDeleteFromDownloadsDialog: (SongUI) -> Unit, + onShowSongInfoDialog: (SongUI) -> Unit ) { val swipeRefreshState = rememberSwipeRefreshState(isRefreshing = isRefreshing) @@ -88,10 +88,9 @@ fun ResultsListView( items(megaList) { item -> AmpacheListItem( item = item, - isSongDownloaded = false, songItemEventListener = { onSongItemEvent( - song = (item as Song), + song = (item as SongUI), event = it, onSongEvent, onAlbumSelected, @@ -105,7 +104,7 @@ fun ResultsListView( .fillMaxWidth() .clickable { when (item) { - is Song -> onSongSelected(item) + is SongUI -> onSongSelected(item) is Album -> onAlbumSelected(item.id, item) is Artist -> onArtistSelected(item.id, item) is Playlist -> onPlaylistSelected(item) @@ -121,14 +120,14 @@ fun ResultsListView( } private fun onSongItemEvent( - song: Song, + song: SongUI, event: SongItemEvent, onSongEvent: (MainEvent) -> Unit, onAlbumSelected: (albumId: String, album: Album?) -> Unit, onArtistSelected: (artistId: String, artist: Artist?) -> Unit, - onOpenPlaylistDialog: (List) -> Unit, - onShowDeleteFromDownloadsDialog: (Song) -> Unit, - onShowSongInfoDialog: (Song) -> Unit + onOpenPlaylistDialog: (List) -> Unit, + onShowDeleteFromDownloadsDialog: (SongUI) -> Unit, + onShowSongInfoDialog: (SongUI) -> Unit ) { when(event) { SongItemEvent.PLAY_NEXT -> diff --git a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/songs/SongsListScreen.kt b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/songs/SongsListScreen.kt index beaf6588..46dabf98 100644 --- a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/songs/SongsListScreen.kt +++ b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/songs/SongsListScreen.kt @@ -29,15 +29,15 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.material3.CircularProgressIndicator +//import androidx.compose.material3.CircularProgressIndicator import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment +//import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha +//import androidx.compose.ui.draw.alpha import androidx.compose.ui.res.stringResource import androidx.hilt.navigation.compose.hiltViewModel import androidx.media3.common.util.UnstableApi @@ -46,7 +46,7 @@ import com.google.accompanist.swiperefresh.rememberSwipeRefreshState import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.navigation.DestinationsNavigator import luci.sixsixsix.powerampache2.R -import luci.sixsixsix.powerampache2.domain.models.Song +import luci.sixsixsix.powerampache2.presentation.models.SongUI import luci.sixsixsix.powerampache2.presentation.common.LoadingScreen import luci.sixsixsix.powerampache2.presentation.common.songitem.SongItem import luci.sixsixsix.powerampache2.presentation.common.songitem.SongItemEvent @@ -92,7 +92,7 @@ fun SongsListScreen( } } - var showDeleteFromDownloadsDialog by remember { mutableStateOf(null) } + var showDeleteFromDownloadsDialog by remember { mutableStateOf(null) } showDeleteFromDownloadsDialog?.let { songToRemove -> EraseConfirmDialog( onDismissRequest = { @@ -116,7 +116,7 @@ fun SongsListScreen( } } - var songToShare: Song? by remember { mutableStateOf(null) } + var songToShare: SongUI? by remember { mutableStateOf(null) } AnimatedVisibility(songToShare != null) { songToShare?.let { songS -> ShareDialog( @@ -149,8 +149,7 @@ fun SongsListScreen( state.songs.size, //key = { i -> state.songs[i].mediaId } ) { i -> - val song = state.songs[i].song - val isOffline = state.songs[i].isOffline + val song = state.songs[i] SongItem( song = song, songItemEventListener = { event -> @@ -179,7 +178,6 @@ fun SongsListScreen( } }, subtitleString = SubtitleString.ARTIST, - isSongDownloaded = isOffline, modifier = Modifier .fillMaxWidth() .clickable { @@ -192,7 +190,7 @@ fun SongsListScreen( } ) // TODO decide to include or not this - // footer(i = i, state = state) + // Footer(i = i, state = state) } } } @@ -200,25 +198,25 @@ fun SongsListScreen( } } -@Composable -fun footer(i: Int, state: SongsState) { - if (i < state.songs.size - 1) { - // if not last item add a divider - // TODO: do I want a divider? Divider(modifier = Modifier.padding(horizontal = 16.dp)) - } else if (i == state.songs.size - 1) { - // TODO should this screen be allowed to load more ? - Column(modifier = Modifier.fillMaxWidth()) { - CircularProgressIndicator( - modifier = Modifier - .align(Alignment.CenterHorizontally) - .alpha( - if (state.isFetchingMore) { - 1.0f - } else { - 0.0f - } - ) - ) - } - } -} +//@Composable +//fun Footer(i: Int, state: SongsState) { +// if (i < state.songs.size - 1) { +// // if not last item add a divider +// // TODO: do I want a divider? Divider(modifier = Modifier.padding(horizontal = 16.dp)) +// } else if (i == state.songs.size - 1) { +// // TODO should this screen be allowed to load more ? +// Column(modifier = Modifier.fillMaxWidth()) { +// CircularProgressIndicator( +// modifier = Modifier +// .align(Alignment.CenterHorizontally) +// .alpha( +// if (state.isFetchingMore) { +// 1.0f +// } else { +// 0.0f +// } +// ) +// ) +// } +// } +//} diff --git a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/songs/SongsState.kt b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/songs/SongsState.kt index 7c05bc3e..9954de8e 100644 --- a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/songs/SongsState.kt +++ b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/songs/SongsState.kt @@ -1,14 +1,13 @@ package luci.sixsixsix.powerampache2.presentation.screens.songs -import luci.sixsixsix.powerampache2.domain.models.Song -import luci.sixsixsix.powerampache2.presentation.common.songitem.SongWrapper +import luci.sixsixsix.powerampache2.presentation.models.SongUI data class SongsState( - val songs: List = emptyList(), + val songs: List = emptyList(), val isLoading: Boolean = false, val isRefreshing: Boolean = false, val searchQuery: String = "", val isFetchingMore: Boolean = false ) { - fun getSongList(): List = songs.map { it.song } + fun getSongList(): List = songs } diff --git a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/songs/SongsViewModel.kt b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/songs/SongsViewModel.kt index 7a82e1c9..fe5a4f5f 100644 --- a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/songs/SongsViewModel.kt +++ b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens/songs/SongsViewModel.kt @@ -12,12 +12,10 @@ import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import luci.sixsixsix.mrlog.L import luci.sixsixsix.powerampache2.common.Resource -import luci.sixsixsix.powerampache2.domain.SettingsRepository -import luci.sixsixsix.powerampache2.domain.SongsRepository import luci.sixsixsix.powerampache2.domain.usecase.settings.OfflineModeFlowUseCase import luci.sixsixsix.powerampache2.domain.usecase.songs.GetSongsUseCase import luci.sixsixsix.powerampache2.domain.usecase.songs.IsSongAvailableOfflineUseCase -import luci.sixsixsix.powerampache2.presentation.common.songitem.SongWrapper +import luci.sixsixsix.powerampache2.presentation.models.toSongUI import javax.inject.Inject @HiltViewModel @@ -51,11 +49,11 @@ class SongsViewModel @Inject constructor( L("SongsEvent.OnSearchQueryChange") // force refresh when deleting the search query // start a search only if the new query is different from the previous + // TODO: invert the check to avoid empty if body if (event.query.isBlank() && state.searchQuery.isBlank()) { - } else { state = state.copy(searchQuery = event.query) - getSongs(refresh = event.query.isNullOrEmpty()) + getSongs(refresh = event.query.isEmpty()) } } @@ -78,17 +76,11 @@ class SongsViewModel @Inject constructor( when(result) { is Resource.Success -> { result.data?.let { songs -> - val songWrapperList = mutableListOf() - songs.forEach { song -> - songWrapperList.add( - SongWrapper( - song = song, - isOffline = isSongAvailableOfflineUseCase(song) - ) - ) - } - state = state.copy(songs = songWrapperList) - L("viewmodel.getSongs SONGS size at the end", state.songs.size) + state = state.copy(songs = songs.toSongUI { + isSongAvailableOfflineUseCase(it) + }) + L("viewmodel.getSongs SONGS size at the end", + state.songs.size) } } diff --git a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens_detail/album_detail/AlbumDetailScreen.kt b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens_detail/album_detail/AlbumDetailScreen.kt index 09f0935e..6787c2c9 100644 --- a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens_detail/album_detail/AlbumDetailScreen.kt +++ b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens_detail/album_detail/AlbumDetailScreen.kt @@ -66,7 +66,6 @@ import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.navigation.DestinationsNavigator import luci.sixsixsix.powerampache2.R import luci.sixsixsix.powerampache2.domain.models.Album -import luci.sixsixsix.powerampache2.domain.models.Song import luci.sixsixsix.powerampache2.presentation.common.LoadingScreen import luci.sixsixsix.powerampache2.presentation.common.songitem.SongInfoThirdRow import luci.sixsixsix.powerampache2.presentation.common.songitem.SongItem @@ -80,6 +79,7 @@ import luci.sixsixsix.powerampache2.presentation.dialogs.info.InfoDialogAlbum import luci.sixsixsix.powerampache2.presentation.dialogs.ShareDialog import luci.sixsixsix.powerampache2.presentation.dialogs.info.InfoDialogSong import luci.sixsixsix.powerampache2.presentation.dialogs.info.ShowSongInfoDialogOpen +import luci.sixsixsix.powerampache2.presentation.models.SongUI import luci.sixsixsix.powerampache2.presentation.navigation.Ampache2NavGraphs import luci.sixsixsix.powerampache2.presentation.screens.main.viewmodel.MainEvent import luci.sixsixsix.powerampache2.presentation.screens.main.viewmodel.MainViewModel @@ -91,10 +91,10 @@ import luci.sixsixsix.powerampache2.presentation.screens_detail.album_detail.com @Composable @Destination fun AlbumDetailScreen( + modifier: Modifier = Modifier, navigator: DestinationsNavigator, albumId: String, album: Album? = null, - modifier: Modifier = Modifier, viewModel: AlbumDetailViewModel = hiltViewModel(), mainViewModel: MainViewModel, addToPlaylistOrQueueDialogViewModel: AddToPlaylistOrQueueDialogViewModel = hiltViewModel() @@ -167,7 +167,7 @@ fun AlbumDetailScreen( ) } - var showDeleteFromDownloadsDialog by remember { mutableStateOf(null) } + var showDeleteFromDownloadsDialog by remember { mutableStateOf(null) } showDeleteFromDownloadsDialog?.let { songToRemove -> EraseConfirmDialog( onDismissRequest = { @@ -191,7 +191,7 @@ fun AlbumDetailScreen( } } - var songToShare: Song? by remember { mutableStateOf(null) } + var songToShare: SongUI? by remember { mutableStateOf(null) } AnimatedVisibility(songToShare != null) { songToShare?.let { songS -> ShareDialog( @@ -214,6 +214,7 @@ fun AlbumDetailScreen( var imageUrl: String? by remember { mutableStateOf(null) } var imageError by remember { mutableStateOf(false) } + // TODO: investigate the warning about suspicious cascading if expression (also isNotBlank warning) val artUrl = if (isOffline != null && isOffline == true && songs.isNotEmpty()) { // in offline mode, grab url from a song songs[0].imageUrl @@ -321,7 +322,7 @@ fun AlbumDetailScreen( when(event) { AlbumInfoViewEvents.PLAY_ALBUM -> { - if (state.isLoading || viewModel.state.songs.isNullOrEmpty()) return@AlbumInfoSection + if (state.isLoading || viewModel.state.songs.isEmpty()) return@AlbumInfoSection if (isPlayingAlbum) { // will pause if playing @@ -368,8 +369,7 @@ fun AlbumDetailScreen( .fillMaxSize() ) { items(state.songs.size) { i -> - val song = state.songs[i].song - val isOffline = state.songs[i].isOffline + val song = state.songs[i] SongItem( song = song, isLandscape = isLandscape, @@ -412,7 +412,6 @@ fun AlbumDetailScreen( }, subtitleString = SubtitleString.NOTHING, songInfoThirdRow = SongInfoThirdRow.Time, - isSongDownloaded = isOffline ) } } diff --git a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens_detail/album_detail/AlbumDetailState.kt b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens_detail/album_detail/AlbumDetailState.kt index 0aed1108..74b1316f 100644 --- a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens_detail/album_detail/AlbumDetailState.kt +++ b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens_detail/album_detail/AlbumDetailState.kt @@ -24,11 +24,11 @@ package luci.sixsixsix.powerampache2.presentation.screens_detail.album_detail import android.os.Parcelable import kotlinx.parcelize.Parcelize import luci.sixsixsix.powerampache2.domain.models.Artist -import luci.sixsixsix.powerampache2.presentation.common.songitem.SongWrapper +import luci.sixsixsix.powerampache2.presentation.models.SongUI @Parcelize data class AlbumDetailState ( - val songs: List = emptyList(), + val songs: List = emptyList(), val recommendedArtists: List = emptyList(), val isLoading: Boolean = false, val isAlbumDownloaded: Boolean = false, @@ -37,5 +37,5 @@ data class AlbumDetailState ( val searchQuery: String = "", val isFetchingMore: Boolean = false, ): Parcelable { - fun getSongList() = songs.map { it.song } + fun getSongList() = songs } diff --git a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens_detail/album_detail/AlbumDetailViewModel.kt b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens_detail/album_detail/AlbumDetailViewModel.kt index 314eb726..03fd4356 100644 --- a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens_detail/album_detail/AlbumDetailViewModel.kt +++ b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens_detail/album_detail/AlbumDetailViewModel.kt @@ -60,14 +60,16 @@ import luci.sixsixsix.powerampache2.domain.usecase.settings.OfflineModeFlowUseCa import luci.sixsixsix.powerampache2.domain.usecase.settings.ToggleGlobalShuffleUseCase import luci.sixsixsix.powerampache2.domain.usecase.songs.IsSongAvailableOfflineUseCase import luci.sixsixsix.powerampache2.domain.usecase.songs.OfflineSongsFlow -import luci.sixsixsix.powerampache2.presentation.common.songitem.SongWrapper +import luci.sixsixsix.powerampache2.presentation.models.SongUI +import luci.sixsixsix.powerampache2.presentation.models.isAvailableOffline +import luci.sixsixsix.powerampache2.presentation.models.toSongUI import javax.inject.Inject @HiltViewModel class AlbumDetailViewModel @Inject constructor( @ApplicationContext private val application: Context, - private val savedStateHandle: SavedStateHandle, // a way to get access to navigation arguments - // in the view model directly without passing them from the UI or the previos view model, we + savedStateHandle: SavedStateHandle, // a way to get access to navigation arguments + // in the view model directly without passing them from the UI or the previous view model, we // need this because we're passing the symbol around offlineModeFlowUseCase: OfflineModeFlowUseCase, localSettingsFlowUseCase: LocalSettingsFlowUseCase, @@ -165,7 +167,7 @@ class AlbumDetailViewModel @Inject constructor( } private fun refreshFromCache() { - if (!albumStateFlow.value.id.isNullOrBlank()) { + if (albumStateFlow.value.id.isNotBlank()) { L("AlbumDetailEvent.RefreshFromCache", albumStateFlow.value.id) getSongsFromAlbum(albumId = albumStateFlow.value.id, fetchRemote = false) } @@ -204,9 +206,9 @@ class AlbumDetailViewModel @Inject constructor( } } - private fun isAlbumDownloaded(songs: List): Boolean { + private fun isAlbumDownloaded(songs: List): Boolean { songs.forEach { - if (!it.isOffline) return false + if (!it.isAvailableOffline()) return false } return true } @@ -219,16 +221,13 @@ class AlbumDetailViewModel @Inject constructor( when (result) { is Resource.Success -> { result.data?.let { songs -> - val songWrapperList = mutableListOf() - songs.forEach { song -> - songWrapperList.add( - SongWrapper( - song = song, - isOffline = isSongAvailableOfflineUseCase(song) - ) - ) + val songUIList = songs.toSongUI { + isSongAvailableOfflineUseCase(it) } - state = state.copy(songs = songWrapperList, isAlbumDownloaded = isAlbumDownloaded(songWrapperList)) + state = state.copy( + songs = songUIList, + isAlbumDownloaded = isAlbumDownloaded(songUIList) + ) L("AlbumDetailViewModel.getSongsFromAlbum size", result.data?.size, "network", result.networkData?.size) } } @@ -240,6 +239,7 @@ class AlbumDetailViewModel @Inject constructor( } } + // TODO: this is never used private fun getRecommendedArtists(artistId: String, fetchRemote: Boolean = true) { viewModelScope.launch { recommendedArtistsUseCase(baseArtistId = artistId, fetchRemote = fetchRemote) diff --git a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens_detail/album_detail/components/AlbumInfoButtonsRow.kt b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens_detail/album_detail/components/AlbumInfoButtonsRow.kt index 63ebb0c7..474c7a54 100644 --- a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens_detail/album_detail/components/AlbumInfoButtonsRow.kt +++ b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens_detail/album_detail/components/AlbumInfoButtonsRow.kt @@ -45,6 +45,7 @@ import luci.sixsixsix.powerampache2.presentation.common.ShuffleToggleButton @Composable fun AlbumInfoButtonsRow( + // TODO: this is never used? album: Album, isPlayingAlbum: Boolean, isAlbumDownloaded: Boolean, diff --git a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens_detail/artist_detail/ArtistDetailScreen.kt b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens_detail/artist_detail/ArtistDetailScreen.kt index 95a0ab25..a12f75f6 100644 --- a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens_detail/artist_detail/ArtistDetailScreen.kt +++ b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens_detail/artist_detail/ArtistDetailScreen.kt @@ -22,7 +22,6 @@ package luci.sixsixsix.powerampache2.presentation.screens_detail.artist_detail import android.content.res.Configuration -import android.widget.Toast import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box @@ -58,9 +57,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalConfiguration -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.dimensionResource -import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.media3.common.util.UnstableApi @@ -77,7 +74,6 @@ import luci.sixsixsix.powerampache2.presentation.destinations.AlbumDetailScreenD import luci.sixsixsix.powerampache2.presentation.dialogs.AddToPlaylistOrQueueDialog import luci.sixsixsix.powerampache2.presentation.dialogs.AddToPlaylistOrQueueDialogOpen import luci.sixsixsix.powerampache2.presentation.dialogs.AddToPlaylistOrQueueDialogViewModel -import luci.sixsixsix.powerampache2.presentation.dialogs.info.InfoDialogAlbum import luci.sixsixsix.powerampache2.presentation.dialogs.info.InfoDialogArtist import luci.sixsixsix.powerampache2.presentation.screens.albums.components.AlbumItem import luci.sixsixsix.powerampache2.presentation.screens.main.viewmodel.MainEvent @@ -94,10 +90,10 @@ private const val GRID_ITEMS_ROW_LAND = 5 @Composable @Destination fun ArtistDetailScreen( + modifier: Modifier = Modifier, navigator: DestinationsNavigator, artistId: String, artist: Artist? = null, - modifier: Modifier = Modifier, viewModel: ArtistDetailViewModel = hiltViewModel(), mainViewModel: MainViewModel, addToPlaylistOrQueueDialogViewModel: AddToPlaylistOrQueueDialogViewModel = hiltViewModel() @@ -121,6 +117,7 @@ fun ArtistDetailScreen( snapshotFlow { configuration.orientation } .collect { orientation = it } } + // TODO: unused? val isLandscape = when (orientation) { Configuration.ORIENTATION_LANDSCAPE -> { cardsPerRow = GRID_ITEMS_ROW_LAND @@ -155,6 +152,8 @@ fun ArtistDetailScreen( var artUrlTop = generateArtistArtUrl(state.artist, state.albums).ifBlank { infoPluginArtistState?.imageUrl } + + // TODO: should be val? var artUrlBottom = generateArtistArtUrl(state.artist, state.albums).ifBlank { infoPluginArtistState?.imageUrl } @@ -331,7 +330,7 @@ fun ArtistDetailScreen( } } -fun generateArtistArtUrl(artist: Artist, albums: List) = if(artist.artUrl.isNullOrBlank()) { +fun generateArtistArtUrl(artist: Artist, albums: List) = if(artist.artUrl.isBlank()) { if (albums.isNotEmpty()) { albums[albums.indices.random()].artUrl } else "" @@ -374,10 +373,10 @@ private val screenBackgroundGradient @Destination @Composable fun ArtistDetailScreen2( + modifier: Modifier = Modifier, navigator: DestinationsNavigator, artistId: String, viewModel: ArtistDetailViewModel = hiltViewModel(), - modifier: Modifier = Modifier ) { val swipeRefreshState = rememberSwipeRefreshState(isRefreshing = viewModel.state.isRefreshing) val state = viewModel.state diff --git a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens_detail/artist_detail/ArtistDetailViewModel.kt b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens_detail/artist_detail/ArtistDetailViewModel.kt index 8bf1b041..66aa22aa 100644 --- a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens_detail/artist_detail/ArtistDetailViewModel.kt +++ b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens_detail/artist_detail/ArtistDetailViewModel.kt @@ -40,7 +40,6 @@ import luci.sixsixsix.powerampache2.common.delegates.FetchArtistSongsHandler import luci.sixsixsix.powerampache2.common.delegates.FetchArtistSongsHandlerImpl import luci.sixsixsix.powerampache2.domain.errors.ErrorHandler import luci.sixsixsix.powerampache2.domain.models.Artist -import luci.sixsixsix.powerampache2.domain.models.Song import luci.sixsixsix.powerampache2.domain.models.settings.LocalSettings import luci.sixsixsix.powerampache2.domain.usecase.albums.AlbumsFromArtistUseCase import luci.sixsixsix.powerampache2.domain.usecase.artists.ArtistUseCase @@ -51,6 +50,8 @@ import luci.sixsixsix.powerampache2.domain.usecase.plugin.IsInfoPluginInstalled import luci.sixsixsix.powerampache2.domain.usecase.settings.LocalSettingsFlowUseCase import luci.sixsixsix.powerampache2.domain.usecase.settings.OfflineModeFlowUseCase import luci.sixsixsix.powerampache2.domain.usecase.settings.ToggleGlobalShuffleUseCase +import luci.sixsixsix.powerampache2.domain.usecase.songs.IsSongAvailableOfflineUseCase +import luci.sixsixsix.powerampache2.presentation.models.SongUI import javax.inject.Inject import kotlin.math.abs @@ -66,8 +67,12 @@ class ArtistDetailViewModel @Inject constructor( private val toggleGlobalShuffle: ToggleGlobalShuffleUseCase, private val isInfoPluginInstalled: IsInfoPluginInstalled, private val artistDataFromPluginUseCase: ArtistDataFromPluginUseCase, - private val errorHandler: ErrorHandler -) : ViewModel(), FetchArtistSongsHandler by FetchArtistSongsHandlerImpl(songsFromArtistUseCase) { + private val errorHandler: ErrorHandler, + private val isSongAvailableOfflineUseCase: IsSongAvailableOfflineUseCase, +) : ViewModel(), FetchArtistSongsHandler by FetchArtistSongsHandlerImpl( + songsFromArtistUseCase, + isSongAvailableOfflineUseCase, +) { var state by mutableStateOf(ArtistDetailState()) // private val isOfflineModeState = offlineModeFlowUseCase() @@ -83,7 +88,7 @@ class ArtistDetailViewModel @Inject constructor( savedStateHandle.get("artist")?.let { artist -> // if artist provided, first check if there's an entry in the db, if not use the provided - // as fallback. This is important, because there is not certainty that the album is + // as fallback. This is important, because there is no certainty that the album is // in the internal db state = state.copy(artist = artist) getArtist(id, fetchRemote = false) @@ -147,14 +152,14 @@ class ArtistDetailViewModel @Inject constructor( fun fetchSongsFromArtist( artistId: String = state.artist.id, fetchRemote: Boolean = true, - songsCallback: (List) -> Unit + songsCallback: (List) -> Unit ) = viewModelScope.launch { getSongsFromArtist( artistId = artistId, fetchRemote = fetchRemote, isOfflineMode = offlineModeFlowUseCase().first(), songsCallback = songsCallback, loadingCallback = { state = state.copy(isLoading = it) }, - errorCallback = { state.copy(isLoading = false) } + errorCallback = { state = state.copy(isLoading = false) } ) } diff --git a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens_detail/artist_detail/components/ArtistInfoSection.kt b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens_detail/artist_detail/components/ArtistInfoSection.kt index 2eedb731..2656a448 100644 --- a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens_detail/artist_detail/components/ArtistInfoSection.kt +++ b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens_detail/artist_detail/components/ArtistInfoSection.kt @@ -46,7 +46,6 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.core.text.HtmlCompat import luci.sixsixsix.powerampache2.R import luci.sixsixsix.powerampache2.domain.models.Artist import luci.sixsixsix.powerampache2.domain.plugin.info.PluginArtistData diff --git a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens_detail/playlist_detail/PlaylistDetailEvent.kt b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens_detail/playlist_detail/PlaylistDetailEvent.kt index ea54b1b9..3ad3d87a 100644 --- a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens_detail/playlist_detail/PlaylistDetailEvent.kt +++ b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens_detail/playlist_detail/PlaylistDetailEvent.kt @@ -22,12 +22,12 @@ package luci.sixsixsix.powerampache2.presentation.screens_detail.playlist_detail import luci.sixsixsix.powerampache2.domain.models.Playlist -import luci.sixsixsix.powerampache2.domain.models.Song +import luci.sixsixsix.powerampache2.presentation.models.SongUI sealed class PlaylistDetailEvent { data object Refresh: PlaylistDetailEvent() data class Fetch(val playlist: Playlist): PlaylistDetailEvent() - data class OnSongSelected(val song: Song): PlaylistDetailEvent() + data class OnSongSelected(val song: SongUI): PlaylistDetailEvent() data object OnLikePlaylist: PlaylistDetailEvent() data object OnPlayPlaylist: PlaylistDetailEvent() data object OnSharePlaylist: PlaylistDetailEvent() diff --git a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens_detail/playlist_detail/PlaylistDetailScreen.kt b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens_detail/playlist_detail/PlaylistDetailScreen.kt index e86e95c0..86bd7bd5 100644 --- a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens_detail/playlist_detail/PlaylistDetailScreen.kt +++ b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens_detail/playlist_detail/PlaylistDetailScreen.kt @@ -33,7 +33,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.items import androidx.compose.material3.Card import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme @@ -76,7 +76,6 @@ import luci.sixsixsix.powerampache2.domain.models.FrequentPlaylist import luci.sixsixsix.powerampache2.domain.models.HighestPlaylist import luci.sixsixsix.powerampache2.domain.models.Playlist import luci.sixsixsix.powerampache2.domain.models.RecentPlaylist -import luci.sixsixsix.powerampache2.domain.models.Song import luci.sixsixsix.powerampache2.presentation.common.LoadingScreen import luci.sixsixsix.powerampache2.presentation.common.songitem.SongInfoThirdRow import luci.sixsixsix.powerampache2.presentation.common.songitem.SongItem @@ -90,7 +89,7 @@ import luci.sixsixsix.powerampache2.presentation.dialogs.EraseConfirmDialog import luci.sixsixsix.powerampache2.presentation.dialogs.ShareDialog import luci.sixsixsix.powerampache2.presentation.dialogs.info.InfoDialogSong import luci.sixsixsix.powerampache2.presentation.dialogs.info.ShowSongInfoDialogOpen -import luci.sixsixsix.powerampache2.presentation.navigation.Ampache2NavGraphs +import luci.sixsixsix.powerampache2.presentation.models.SongUI import luci.sixsixsix.powerampache2.presentation.navigation.Ampache2NavGraphs.navigateToArtist import luci.sixsixsix.powerampache2.presentation.screens.main.viewmodel.MainEvent import luci.sixsixsix.powerampache2.presentation.screens.main.viewmodel.MainViewModel @@ -152,7 +151,7 @@ fun PlaylistDetailScreen( } } - var showDeleteFromPlaylistDialog by remember { mutableStateOf(null) } + var showDeleteFromPlaylistDialog by remember { mutableStateOf(null) } showDeleteFromPlaylistDialog?.let { songToRemove -> EraseConfirmDialog( onDismissRequest = { @@ -185,7 +184,7 @@ fun PlaylistDetailScreen( } } - var showDeleteFromDownloadsDialog by remember { mutableStateOf(null) } + var showDeleteFromDownloadsDialog by remember { mutableStateOf(null) } showDeleteFromDownloadsDialog?.let { songToRemove -> EraseConfirmDialog( onDismissRequest = { @@ -209,7 +208,7 @@ fun PlaylistDetailScreen( } } - var songToShare: Song? by remember { mutableStateOf(null) } + var songToShare: SongUI? by remember { mutableStateOf(null) } AnimatedVisibility(songToShare != null) { songToShare?.let { songS -> ShareDialog( @@ -322,17 +321,17 @@ fun PlaylistDetailScreen( isDownloading = mainViewModel.state.isDownloading, isGlobalShuffleOn = state.isGlobalShuffleOn, isPlayLoading = mainViewModel.isPlayLoading(), - enabled = !state.songs.isNullOrEmpty(), + enabled = state.songs.isNotEmpty(), songs = viewModel.state.getSongList(), artistClickListener = { - artistId -> Ampache2NavGraphs.navigateToArtist(navigator, artistId) + artistId -> navigateToArtist(navigator, artistId) }, isPlaylistEditLoading = addToPlaylistOrQueueDialogViewModel.state.isPlaylistEditLoading, eventListener = { event -> when(event) { PlaylistInfoViewEvents.PLAY_PLAYLIST -> { - if (viewModel.state.songs.isNullOrEmpty()) return@PlaylistInfoSection + if (viewModel.state.songs.isEmpty()) return@PlaylistInfoSection if (isPlayingPlaylist){ // will pause if playing @@ -340,7 +339,7 @@ fun PlaylistDetailScreen( } else if (state.songs.isNotEmpty()) { if (!state.isGlobalShuffleOn) { mainViewModel.onEvent( - MainEvent.AddSongsToQueueAndPlay(state.songs[0].song, state.getSongList()) + MainEvent.AddSongsToQueueAndPlay(state.songs[0], state.getSongList()) ) } else { mainViewModel.onEvent( @@ -386,7 +385,7 @@ fun PlaylistDetailScreen( } ) - showHideEmptyPlaylistView(playlist = currentPlaylistState, state = state) + ShowHideEmptyPlaylistView(playlist = currentPlaylistState, state = state) SwipeRefresh( state = swipeRefreshState, @@ -396,17 +395,13 @@ fun PlaylistDetailScreen( modifier = Modifier .fillMaxSize() ) { - itemsIndexed( + items( items = state.songs, - //key = { _, item -> item } - ) { _, songWrapped -> - val song = songWrapped.song - val isOffline = songWrapped.isOffline + ) { song -> SongItem( song = song, isLandscape = isLandscape, isEditMode = isEditMode, - isSongDownloaded = isOffline, songItemEventListener = { event -> when(event) { SongItemEvent.PLAY_NEXT -> @@ -477,13 +472,14 @@ fun PlaylistDetailScreen( } @Composable -private fun showHideEmptyPlaylistView(playlist: Playlist, state: PlaylistDetailState) { +private fun ShowHideEmptyPlaylistView(playlist: Playlist, state: PlaylistDetailState) { if ( + // TODO: check if the commented out stuff is still relevant in any way or should be removed /*(playlist is RecentPlaylist || playlist is FrequentPlaylist || playlist is HighestPlaylist || playlist is FlaggedPlaylist) &&*/ - !state.isLoading && !state.isRefreshing && state.songs.isNullOrEmpty() + !state.isLoading && !state.isRefreshing && state.songs.isEmpty() ){ Card(modifier = Modifier.fillMaxSize()) { Column( diff --git a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens_detail/playlist_detail/PlaylistDetailState.kt b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens_detail/playlist_detail/PlaylistDetailState.kt index 3da857ec..fbcc2871 100644 --- a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens_detail/playlist_detail/PlaylistDetailState.kt +++ b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens_detail/playlist_detail/PlaylistDetailState.kt @@ -29,18 +29,17 @@ import luci.sixsixsix.powerampache2.domain.models.HighestPlaylist import luci.sixsixsix.powerampache2.domain.models.settings.LocalSettings import luci.sixsixsix.powerampache2.domain.models.Playlist import luci.sixsixsix.powerampache2.domain.models.RecentPlaylist -import luci.sixsixsix.powerampache2.domain.models.Song import luci.sixsixsix.powerampache2.domain.models.settings.SortMode import luci.sixsixsix.powerampache2.domain.models.settings.defaultPlaylistSort import luci.sixsixsix.powerampache2.domain.models.isSmartPlaylist -import luci.sixsixsix.powerampache2.presentation.common.songitem.SongWrapper +import luci.sixsixsix.powerampache2.presentation.models.SongUI @Parcelize data class PlaylistDetailState ( //val playlist: Playlist = Playlist("", ""), val isNotStatPlaylist: Boolean = false, val isGeneratedOrSmartPlaylist: Boolean = false, - val songs: List = emptyList(), + val songs: List = emptyList(), val isLoading: Boolean = false, val isLikeLoading: Boolean = false, val isRefreshing: Boolean = false, @@ -51,7 +50,7 @@ data class PlaylistDetailState ( val isUserOwner: Boolean = false, val isGlobalShuffleOn: Boolean = LocalSettings.SETTINGS_DEFAULTS_GLOBAL_SHUFFLE ): Parcelable { - fun getSongList(): List = songs.map { it.song } + fun getSongList(): List = songs companion object { fun isGeneratedOrSmartPlaylist(playlist: Playlist) = when (playlist) { diff --git a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens_detail/playlist_detail/PlaylistDetailViewModel.kt b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens_detail/playlist_detail/PlaylistDetailViewModel.kt index 506fe628..3c212013 100644 --- a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens_detail/playlist_detail/PlaylistDetailViewModel.kt +++ b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens_detail/playlist_detail/PlaylistDetailViewModel.kt @@ -58,7 +58,6 @@ import luci.sixsixsix.powerampache2.domain.models.HighestPlaylist import luci.sixsixsix.powerampache2.domain.models.Playlist import luci.sixsixsix.powerampache2.domain.models.PlaylistType import luci.sixsixsix.powerampache2.domain.models.RecentPlaylist -import luci.sixsixsix.powerampache2.domain.models.Song import luci.sixsixsix.powerampache2.domain.models.isSmartPlaylist import luci.sixsixsix.powerampache2.domain.models.settings.SortMode import luci.sixsixsix.powerampache2.domain.usecase.UserFlowUseCase @@ -69,13 +68,15 @@ import luci.sixsixsix.powerampache2.domain.usecase.settings.LocalSettingsFlowUse import luci.sixsixsix.powerampache2.domain.usecase.settings.ToggleGlobalShuffleUseCase import luci.sixsixsix.powerampache2.domain.usecase.songs.IsSongAvailableOfflineUseCase import luci.sixsixsix.powerampache2.domain.usecase.songs.OfflineSongsFlow -import luci.sixsixsix.powerampache2.presentation.common.songitem.SongWrapper +import luci.sixsixsix.powerampache2.presentation.models.SongUI +import luci.sixsixsix.powerampache2.presentation.models.toSong +import luci.sixsixsix.powerampache2.presentation.models.toSongUI import javax.inject.Inject @HiltViewModel class PlaylistDetailViewModel @Inject constructor( @ApplicationContext private val application: Context, - private val savedStateHandle: SavedStateHandle, + savedStateHandle: SavedStateHandle, private val toggleGlobalShuffle: ToggleGlobalShuffleUseCase, localSettingsFlowUseCase: LocalSettingsFlowUseCase, playlistFlow: PlaylistFlow, @@ -241,7 +242,7 @@ class PlaylistDetailViewModel @Inject constructor( } } - fun isEditSongSelected(song: Song): Boolean = editState.selectedSongs.contains(song) + fun isEditSongSelected(song: SongUI): Boolean = editState.selectedSongs.contains(song) private fun ratePlaylist(playlist: Playlist, rate: Int) = viewModelScope.launch { playlistsRepository.ratePlaylist(playlist.id, rate).collect { result -> @@ -292,19 +293,13 @@ class PlaylistDetailViewModel @Inject constructor( .collect { result -> when(result) { is Resource.Success -> { - result.data?.let { songs -> - val songWrapperList = mutableListOf() - songs.forEach { song -> - songWrapperList.add( - SongWrapper( - song = song, - isOffline = isSongAvailableOfflineUseCase(song) - ) - ) - } + result.data?.toSongUI { + isSongAvailableOfflineUseCase(it) + }?.toMutableList()?.let { songs -> state = state.copy( - songs = songWrapperList.apply { - if (state.sortMode == SortMode.DESC) { reverse() } } + songs = songs.apply { + if (state.sortMode == SortMode.DESC) { reverse() } + } ) } } @@ -319,13 +314,13 @@ class PlaylistDetailViewModel @Inject constructor( private fun editPlaylist( playlist: Playlist = playlistStateFlow.value, - newList: List = state.getSongList() + newList: List = state.getSongList() ) = viewModelScope.launch { playlistsRepository .editPlaylist( playlistId = playlist.id, playlistName = playlist.name, - items = newList, + items = newList.toSong(), owner = playlist.owner, playlistType = playlist.type ?: PlaylistType.private ).collect { result -> @@ -361,19 +356,15 @@ class PlaylistDetailViewModel @Inject constructor( } } + // TODO: fetchRemote isn't used? private fun getRecentSongs(fetchRemote: Boolean = true) = viewModelScope.launch { songsRepository.getRecentSongs().collect { result -> when(result) { is Resource.Success -> { - result.data?.let { songs -> - val songWrapperList = mutableListOf() - songs.forEach { song -> - songWrapperList.add( - SongWrapper(song = song, - isOffline = isSongAvailableOfflineUseCase(song)) - ) - } - state = state.copy(songs = songWrapperList) + result.data?.toSongUI { + isSongAvailableOfflineUseCase(it) + }?.let { songs -> + state = state.copy(songs = songs) L("PlaylistDetailViewModel.getRecentSongs size ${state.songs.size}") } } @@ -389,6 +380,7 @@ class PlaylistDetailViewModel @Inject constructor( } + // TODO: fetchRemote isn't used? private fun getFlaggedSongs(fetchRemote: Boolean = true) { viewModelScope.launch { songsRepository @@ -396,17 +388,10 @@ class PlaylistDetailViewModel @Inject constructor( .collect { result -> when(result) { is Resource.Success -> { - result.data?.let { songs -> - val songWrapperList = mutableListOf() - songs.forEach { song -> - songWrapperList.add( - SongWrapper( - song = song, - isOffline = isSongAvailableOfflineUseCase(song) - ) - ) - } - state = state.copy(songs = songWrapperList) + result.data?.toSongUI { + isSongAvailableOfflineUseCase(it) + }?.let { songs -> + state = state.copy(songs = songs) L("PlaylistDetailViewModel.getFlaggedSongs size ${state.songs.size}") } L( "PlaylistDetailViewModel.getFlaggedSongs size of network array ${result.networkData?.size}") @@ -423,24 +408,18 @@ class PlaylistDetailViewModel @Inject constructor( } } - private fun getFrequentSongs(fetchRemote: Boolean = true, ) { + // TODO: fetchRemote isn't used? + private fun getFrequentSongs(fetchRemote: Boolean = true) { viewModelScope.launch { songsRepository .getFrequentSongs() .collect { result -> when(result) { is Resource.Success -> { - result.data?.let { songs -> - val songWrapperList = mutableListOf() - songs.forEach { song -> - songWrapperList.add( - SongWrapper( - song = song, - isOffline = isSongAvailableOfflineUseCase(song = song) - ) - ) - } - state = state.copy(songs = songWrapperList) + result.data?.toSongUI { + isSongAvailableOfflineUseCase(it) + }?.let { songs -> + state = state.copy(songs = songs) L("PlaylistDetailViewModel.getFrequentSongs size ${state.songs.size}") } L( "PlaylistDetailViewModel.getFrequentSongs size of network array ${result.networkData?.size}") @@ -457,24 +436,18 @@ class PlaylistDetailViewModel @Inject constructor( } } - private fun getHighestSongs(fetchRemote: Boolean = true, ) { + // TODO: fetchRemote isn't used? + private fun getHighestSongs(fetchRemote: Boolean = true) { viewModelScope.launch { songsRepository .getHighestSongs() .collect { result -> when(result) { is Resource.Success -> { - result.data?.let { songs -> - val songWrapperList = mutableListOf() - songs.forEach { song -> - songWrapperList.add( - SongWrapper( - song = song, - isOffline = isSongAvailableOfflineUseCase(song) - ) - ) - } - state = state.copy(songs = songWrapperList) + result.data?.toSongUI { + isSongAvailableOfflineUseCase(it) + }?.let { songs -> + state = state.copy(songs = songs) L("PlaylistDetailViewModel.getHighestSongs size ${state.songs.size}") } L( "PlaylistDetailViewModel.getHighestSongs size of network array ${result.networkData?.size}") diff --git a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens_detail/playlist_detail/PlaylistDetailsEditEvent.kt b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens_detail/playlist_detail/PlaylistDetailsEditEvent.kt index ae83d6bb..004b5d63 100644 --- a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens_detail/playlist_detail/PlaylistDetailsEditEvent.kt +++ b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens_detail/playlist_detail/PlaylistDetailsEditEvent.kt @@ -21,13 +21,13 @@ */ package luci.sixsixsix.powerampache2.presentation.screens_detail.playlist_detail -import luci.sixsixsix.powerampache2.domain.models.Song +import luci.sixsixsix.powerampache2.presentation.models.SongUI sealed class PlaylistDetailsEditEvent { - data class OnSongSelected(val isSelected: Boolean, val song: Song): PlaylistDetailsEditEvent() - data class OnRemoveSong(val song: Song): PlaylistDetailsEditEvent() - data class OnMoveUpSong(val song: Song): PlaylistDetailsEditEvent() - data class OnMoveDownSong(val song: Song): PlaylistDetailsEditEvent() + data class OnSongSelected(val isSelected: Boolean, val song: SongUI): PlaylistDetailsEditEvent() + data class OnRemoveSong(val song: SongUI): PlaylistDetailsEditEvent() + data class OnMoveUpSong(val song: SongUI): PlaylistDetailsEditEvent() + data class OnMoveDownSong(val song: SongUI): PlaylistDetailsEditEvent() data object OnRemoveSongDismiss: PlaylistDetailsEditEvent() data object OnDeleteSelectedSongs: PlaylistDetailsEditEvent() data object OnConfirmEdit: PlaylistDetailsEditEvent() diff --git a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens_detail/playlist_detail/PlaylistEditState.kt b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens_detail/playlist_detail/PlaylistEditState.kt index db8a2558..74ab5d5f 100644 --- a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens_detail/playlist_detail/PlaylistEditState.kt +++ b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens_detail/playlist_detail/PlaylistEditState.kt @@ -21,8 +21,8 @@ */ package luci.sixsixsix.powerampache2.presentation.screens_detail.playlist_detail -import luci.sixsixsix.powerampache2.domain.models.Song +import luci.sixsixsix.powerampache2.presentation.models.SongUI data class PlaylistEditState( - val selectedSongs: List + val selectedSongs: List ) \ No newline at end of file diff --git a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens_detail/playlist_detail/components/PlaylistInfoSection.kt b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens_detail/playlist_detail/components/PlaylistInfoSection.kt index 0b96a055..9348cb14 100644 --- a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens_detail/playlist_detail/components/PlaylistInfoSection.kt +++ b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens_detail/playlist_detail/components/PlaylistInfoSection.kt @@ -44,9 +44,9 @@ import luci.sixsixsix.powerampache2.domain.models.ArtistId import luci.sixsixsix.powerampache2.domain.models.MusicAttribute import luci.sixsixsix.powerampache2.domain.models.Playlist import luci.sixsixsix.powerampache2.domain.models.PlaylistType -import luci.sixsixsix.powerampache2.domain.models.Song import luci.sixsixsix.powerampache2.presentation.screens_detail.album_detail.components.AttributeText import luci.sixsixsix.powerampache2.presentation.common.MusicAttributeChips +import luci.sixsixsix.powerampache2.presentation.models.SongUI enum class PlaylistInfoViewEvents { PLAY_PLAYLIST, @@ -72,7 +72,7 @@ fun PlaylistInfoSection( isLikeLoading: Boolean, isPlayLoading: Boolean, enabled: Boolean, - songs: List, + songs: List, eventListener: (playlistInfoViewEvents: PlaylistInfoViewEvents) -> Unit, artistClickListener: (ArtistId) -> Unit ) { @@ -172,7 +172,10 @@ fun PlaylistInfoSectionPreview() { isPlayingPlaylist = true, isDownloading = false, isGlobalShuffleOn = true, - songs = listOf(Song.mockSong, Song.mockSong, Song.mockSong, Song.mockSong, Song.mockSong), + songs = listOf( + SongUI.mockSongUI, SongUI.mockSongUI, SongUI.mockSongUI, + SongUI.mockSongUI, SongUI.mockSongUI + ), isPlaylistEditLoading = true, isLikeLoading = false, isLikeAvailable = true, diff --git a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens_detail/song_detail/SongDetailScreen.kt b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens_detail/song_detail/SongDetailScreen.kt index a196570f..bdb9ac62 100644 --- a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens_detail/song_detail/SongDetailScreen.kt +++ b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens_detail/song_detail/SongDetailScreen.kt @@ -29,7 +29,6 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.SheetValue import androidx.compose.material3.rememberBottomSheetScaffoldState import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf @@ -43,12 +42,13 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.media3.common.util.UnstableApi import luci.sixsixsix.powerampache2.R -import luci.sixsixsix.powerampache2.domain.models.Song import luci.sixsixsix.powerampache2.presentation.dialogs.AddToPlaylistOrQueueDialogViewModel +import luci.sixsixsix.powerampache2.presentation.models.SongUI import luci.sixsixsix.powerampache2.presentation.screens.main.viewmodel.MainViewModel import luci.sixsixsix.powerampache2.presentation.screens_detail.song_detail.components.SongDetailContent import luci.sixsixsix.powerampache2.presentation.screens_detail.song_detail.components.SongDetailQueueDragHandle import luci.sixsixsix.powerampache2.presentation.screens_detail.song_detail.components.TabbedSongDetailView +import androidx.compose.runtime.collectAsState @androidx.annotation.OptIn(UnstableApi::class) @OptIn(ExperimentalMaterial3Api::class) @@ -86,7 +86,8 @@ fun SongDetailScreen( val selectedTabIndex = remember { mutableIntStateOf(0) } val queuePosStr = getQueuePositionStr( - currentQueue = viewModel.currentQueue().value, + // TODO: the line below was missing collectAsState(), which was an error. Change needs testing/confirmation + currentQueue = viewModel.currentQueue().collectAsState().value, // currentQueuePosition is USER FACING: start from 1, not zero currentQueuePosition = viewModel.currentQueuePosition() + 1, isScreenOpen = scaffoldState.bottomSheetState.currentValue == SheetValue.Expanded @@ -128,7 +129,7 @@ fun SongDetailScreen( } } -private fun getQueuePositionStr(currentQueue: List, currentQueuePosition: Int, isScreenOpen: Boolean) = +private fun getQueuePositionStr(currentQueue: List, currentQueuePosition: Int, isScreenOpen: Boolean) = if ( currentQueuePosition > 0 && currentQueue.isNotEmpty() diff --git a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens_detail/song_detail/SongDetailViewModel.kt b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens_detail/song_detail/SongDetailViewModel.kt index 7e24cbc8..da2d27e3 100644 --- a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens_detail/song_detail/SongDetailViewModel.kt +++ b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens_detail/song_detail/SongDetailViewModel.kt @@ -32,7 +32,6 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import luci.sixsixsix.powerampache2.common.Resource import luci.sixsixsix.powerampache2.domain.models.Artist -import luci.sixsixsix.powerampache2.domain.models.Song import luci.sixsixsix.powerampache2.domain.plugin.info.PluginSongData import luci.sixsixsix.powerampache2.domain.plugin.lyrics.getAvailableLyrics import luci.sixsixsix.powerampache2.domain.usecase.ServerInfoStateFlowUseCase @@ -43,6 +42,8 @@ import luci.sixsixsix.powerampache2.domain.usecase.plugin.IsLyricsPluginInstalle import luci.sixsixsix.powerampache2.domain.usecase.plugin.LyricsFromPluginUseCase import luci.sixsixsix.powerampache2.domain.usecase.plugin.SongDataFromPluginUseCase import luci.sixsixsix.powerampache2.domain.usecase.songs.SongFromIdUseCase +import luci.sixsixsix.powerampache2.presentation.models.SongUI +import luci.sixsixsix.powerampache2.presentation.models.toSong import javax.inject.Inject @HiltViewModel @@ -57,6 +58,7 @@ class SongDetailViewModel @Inject constructor( private val isChromecastPluginInstalled: IsChromecastPluginInstalled, ) : ViewModel() { private val _recommendedArtistsStateFlow = MutableStateFlow>(listOf()) + // TODO: is this needed? val recommendedArtistsStateFlow = _recommendedArtistsStateFlow.asStateFlow() private val _pluginInfo = MutableStateFlow(null) @@ -75,7 +77,7 @@ class SongDetailViewModel @Inject constructor( private var lyricsJob: Job? = null private var songInfoJob: Job? = null - private suspend fun getRecommendedArtists(song: Song) { + private suspend fun getRecommendedArtists(song: SongUI) { recommendedArtistsUseCase(song.artist.id).collectLatest { result -> when(result) { is Resource.Error -> {} @@ -90,7 +92,7 @@ class SongDetailViewModel @Inject constructor( } } - fun onNewSong(song: Song) { + fun onNewSong(song: SongUI) { lyricsJob?.cancel() lyricsJob = viewModelScope.launch { getSongLyrics(song) @@ -108,7 +110,7 @@ class SongDetailViewModel @Inject constructor( _isChromecastPluginInstalled.value = isChromecastPluginInstalled() } - private suspend fun getSongLyricsFromPlugin(song: Song) { + private suspend fun getSongLyricsFromPlugin(song: SongUI) { _pluginLyrics.value = "" // only fetch if no lyrics already present if (lyrics.value.isBlank() && isLyricsPluginInstalledUseCase()) { @@ -121,7 +123,7 @@ class SongDetailViewModel @Inject constructor( } - private suspend fun getSongLyrics(song: Song) { + private suspend fun getSongLyrics(song: SongUI) { // if we already have lyrics, for a song with the same id there is no need to fetch again if (lyrics.value.isNotBlank() && song.id == songId) return songId = song.id @@ -136,10 +138,10 @@ class SongDetailViewModel @Inject constructor( } } - private suspend fun getSongInfoFromPlugin(song: Song) { + private suspend fun getSongInfoFromPlugin(song: SongUI) { // only fetch if no lyrics already present if (isInfoPluginInstalled()) { - _pluginInfo.value = getSongInfoPluginUseCase(song) + _pluginInfo.value = getSongInfoPluginUseCase(song.toSong()) } } } diff --git a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens_detail/song_detail/components/MiniPlayer.kt b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens_detail/song_detail/components/MiniPlayer.kt index 7c3e1fd3..764088fc 100644 --- a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens_detail/song_detail/components/MiniPlayer.kt +++ b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens_detail/song_detail/components/MiniPlayer.kt @@ -69,9 +69,9 @@ import androidx.media3.common.util.UnstableApi import coil.compose.AsyncImage import luci.sixsixsix.powerampache2.R import luci.sixsixsix.powerampache2.domain.common.WeakContext -import luci.sixsixsix.powerampache2.domain.models.Song import luci.sixsixsix.powerampache2.player.RepeatMode import luci.sixsixsix.powerampache2.presentation.common.PlayButton +import luci.sixsixsix.powerampache2.presentation.models.SongUI import luci.sixsixsix.powerampache2.presentation.screens.main.viewmodel.MainEvent import luci.sixsixsix.powerampache2.presentation.screens.main.viewmodel.MainViewModel @@ -103,10 +103,11 @@ fun MiniPlayer( @OptIn(ExperimentalFoundationApi::class) @Composable fun MiniPlayerContent( - song: Song, + song: SongUI, isPlaying: Boolean, isPlayLoading: Boolean, isBuffering: Boolean, + // TODO: unused parameters shuffleOn: Boolean, repeatMode: RepeatMode, modifier: Modifier = Modifier, @@ -257,9 +258,9 @@ fun MiniPlayerContent( @Composable @Preview -fun previewMiniPlayer() { +fun PreviewMiniPlayer() { MiniPlayerContent( - song = Song.mockSong, + song = SongUI.mockSongUI, modifier = Modifier .width(400.dp) .height(50.dp), diff --git a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens_detail/song_detail/components/SongDetailButtonRow.kt b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens_detail/song_detail/components/SongDetailButtonRow.kt index 7c7c81af..8abfe009 100644 --- a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens_detail/song_detail/components/SongDetailButtonRow.kt +++ b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens_detail/song_detail/components/SongDetailButtonRow.kt @@ -93,17 +93,18 @@ fun SongDetailButtonRow( } PlayerButton( text = R.string.player_buttonText_download, - icon = if (!isOffline) - Icons.Outlined.DownloadForOffline - else - Icons.Outlined.OfflinePin, + icon = + if (isOffline) + Icons.Outlined.OfflinePin + else + Icons.Outlined.DownloadForOffline, tint = tint, modifier = btnModifier ) { - if (!isOffline) { - eventListener(SongDetailButtonEvents.DOWNLOAD_SONG) - } else { + if (isOffline) { eventListener(SongDetailButtonEvents.DELETE_DOWNLOADED_SONG) + } else { + eventListener(SongDetailButtonEvents.DOWNLOAD_SONG) } } PlayerButton( diff --git a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens_detail/song_detail/components/SongDetailContent.kt b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens_detail/song_detail/components/SongDetailContent.kt index 38a1d488..04a087d1 100644 --- a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens_detail/song_detail/components/SongDetailContent.kt +++ b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens_detail/song_detail/components/SongDetailContent.kt @@ -42,7 +42,6 @@ import androidx.compose.material3.BottomSheetScaffoldState import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton -import androidx.compose.material3.IconButtonColors import androidx.compose.material3.IconButtonDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -69,8 +68,7 @@ import coil.compose.AsyncImage import kotlinx.coroutines.launch import luci.sixsixsix.powerampache2.R import luci.sixsixsix.powerampache2.common.fontDimensionResource -import luci.sixsixsix.powerampache2.domain.models.Song -import luci.sixsixsix.powerampache2.domain.models.totalTime +import luci.sixsixsix.powerampache2.presentation.models.totalTime import luci.sixsixsix.powerampache2.domain.plugin.info.PluginSongData import luci.sixsixsix.powerampache2.presentation.common.LikeButton import luci.sixsixsix.powerampache2.presentation.dialogs.AddToPlaylistOrQueueDialog @@ -78,6 +76,8 @@ import luci.sixsixsix.powerampache2.presentation.dialogs.AddToPlaylistOrQueueDia import luci.sixsixsix.powerampache2.presentation.dialogs.AddToPlaylistOrQueueDialogViewModel import luci.sixsixsix.powerampache2.presentation.dialogs.ShareDialog import luci.sixsixsix.powerampache2.presentation.dialogs.info.InfoDialogSong +import luci.sixsixsix.powerampache2.presentation.models.SongUI +import luci.sixsixsix.powerampache2.presentation.models.isAvailableOffline import luci.sixsixsix.powerampache2.presentation.navigation.Ampache2NavGraphs import luci.sixsixsix.powerampache2.presentation.screens.main.viewmodel.MainEvent import luci.sixsixsix.powerampache2.presentation.screens.main.viewmodel.MainViewModel @@ -121,7 +121,7 @@ fun SongDetailContent( var isOffline by remember { mutableStateOf(false) } - var songToShare: Song? by remember { mutableStateOf(null) } + var songToShare: SongUI? by remember { mutableStateOf(null) } AnimatedVisibility(songToShare != null) { songToShare?.let { songS -> ShareDialog( @@ -227,9 +227,7 @@ fun SongDetailContent( Spacer(modifier = Modifier.height(16.dp)) currentSongState?.let { song -> - mainViewModel.isOfflineSong(song) { - isOffline = it - } + isOffline = song.isAvailableOffline() SongDetailButtonRow( modifier = Modifier .fillMaxWidth() diff --git a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens_detail/song_detail/components/SongDetailQueueDragHandle.kt b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens_detail/song_detail/components/SongDetailQueueDragHandle.kt index 9ba0e5cd..c59ca90c 100644 --- a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens_detail/song_detail/components/SongDetailQueueDragHandle.kt +++ b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens_detail/song_detail/components/SongDetailQueueDragHandle.kt @@ -72,7 +72,7 @@ import androidx.compose.ui.viewinterop.AndroidView import androidx.core.text.HtmlCompat import kotlinx.coroutines.launch import luci.sixsixsix.powerampache2.R -import luci.sixsixsix.powerampache2.domain.models.Song +import luci.sixsixsix.powerampache2.presentation.models.SongUI import luci.sixsixsix.powerampache2.presentation.screens.main.viewmodel.MainViewModel import luci.sixsixsix.powerampache2.ui.theme.additionalColours import java.net.MalformedURLException @@ -81,7 +81,7 @@ import java.net.URL @Composable @OptIn(ExperimentalMaterial3Api::class) fun SongDetailQueueDragHandle( - song: Song?, + song: SongUI?, lyrics: String, scaffoldState: BottomSheetScaffoldState, selectedTabIndex: MutableIntState, @@ -136,7 +136,7 @@ fun SongDetailQueueDragHandle( @OptIn(ExperimentalMaterial3Api::class) @Composable fun SongDetailQueueTopBar( - song: Song?, + song: SongUI?, lyrics: String, modifier: Modifier = Modifier, pagerState: PagerState, @@ -259,7 +259,7 @@ fun WebPageView(url: String, modifier: Modifier = Modifier) { } }, update = { - if (currentUrl.isNotBlank() && it.url?.toString() != currentUrl) { + if (currentUrl.isNotBlank() && it.url != currentUrl) { it.loadUrl(currentUrl) } } @@ -270,19 +270,20 @@ fun WebPageView(url: String, modifier: Modifier = Modifier) { @Composable fun SongHandleTabRow( modifier: Modifier = Modifier, - song: Song?, + // TODO: isn't used? + song: SongUI?, lyrics: String, scaffoldState: BottomSheetScaffoldState, pagerState: PagerState, upNextText: String = stringResource(id = R.string.player_queue_upNext), selectedTabIndex: MutableIntState ) { - LaunchedEffect(selectedTabIndex.value) { - pagerState.animateScrollToPage(selectedTabIndex.value) + LaunchedEffect(selectedTabIndex.intValue) { + pagerState.animateScrollToPage(selectedTabIndex.intValue) } LaunchedEffect(pagerState.currentPage, pagerState.isScrollInProgress) { if (!pagerState.isScrollInProgress) { - selectedTabIndex.value = pagerState.currentPage + selectedTabIndex.intValue = pagerState.currentPage } } @@ -293,23 +294,23 @@ fun SongHandleTabRow( }, modifier = modifier, - selectedTabIndex = selectedTabIndex.value, + selectedTabIndex = selectedTabIndex.intValue, contentColor = textColour, containerColor = Color.Transparent ) { Tab( unselectedContentColor = textColour.copy(alpha = 0.66f), - selected = selectedTabIndex.value == 0, + selected = selectedTabIndex.intValue == 0, onClick = { scope.launch { // if we're in the tab that is selected just close the drawer if (scaffoldState.bottomSheetState.currentValue == SheetValue.Expanded && - selectedTabIndex.value == 0) { + selectedTabIndex.intValue == 0) { scaffoldState.bottomSheetState.partialExpand() } else { scaffoldState.bottomSheetState.expand() } - selectedTabIndex.value = 0 + selectedTabIndex.intValue = 0 } }, text = { @@ -326,18 +327,18 @@ fun SongHandleTabRow( if (lyrics != "") { Tab( unselectedContentColor = textColour.copy(alpha = 0.66f), - selected = selectedTabIndex.value == 1, + selected = selectedTabIndex.intValue == 1, onClick = { scope.launch { // if we're in the tab that is selected just close the drawer if (scaffoldState.bottomSheetState.currentValue == SheetValue.Expanded && - selectedTabIndex.value == 1 + selectedTabIndex.intValue == 1 ) { scaffoldState.bottomSheetState.partialExpand() } else { scaffoldState.bottomSheetState.expand() } - selectedTabIndex.value = 1 + selectedTabIndex.intValue = 1 } }, text = { diff --git a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens_detail/song_detail/components/SongDetailQueueScreenContent.kt b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens_detail/song_detail/components/SongDetailQueueScreenContent.kt index 33da925b..ec90ff06 100644 --- a/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens_detail/song_detail/components/SongDetailQueueScreenContent.kt +++ b/app/src/main/java/luci/sixsixsix/powerampache2/presentation/screens_detail/song_detail/components/SongDetailQueueScreenContent.kt @@ -44,7 +44,6 @@ import androidx.compose.ui.res.stringResource import androidx.hilt.navigation.compose.hiltViewModel import kotlinx.coroutines.launch import luci.sixsixsix.powerampache2.R -import luci.sixsixsix.powerampache2.domain.models.Song import luci.sixsixsix.powerampache2.presentation.navigation.Ampache2NavGraphs import luci.sixsixsix.powerampache2.presentation.screens.queue.QueueEvent import luci.sixsixsix.powerampache2.presentation.screens.queue.QueueViewModel @@ -58,6 +57,7 @@ import luci.sixsixsix.powerampache2.presentation.dialogs.EraseConfirmDialog import luci.sixsixsix.powerampache2.presentation.dialogs.ShareDialog import luci.sixsixsix.powerampache2.presentation.dialogs.info.InfoDialogSong import luci.sixsixsix.powerampache2.presentation.dialogs.info.ShowSongInfoDialogOpen +import luci.sixsixsix.powerampache2.presentation.models.SongUI import luci.sixsixsix.powerampache2.presentation.screens.main.viewmodel.MainEvent import luci.sixsixsix.powerampache2.presentation.screens.main.viewmodel.MainViewModel @@ -91,7 +91,7 @@ fun SongDetailQueueScreenContent( } } - var showRemoveFromQueueDialog by remember { mutableStateOf(null) } + var showRemoveFromQueueDialog by remember { mutableStateOf(null) } showRemoveFromQueueDialog?.let { songToRemove -> EraseConfirmDialog( onDismissRequest = { @@ -106,7 +106,7 @@ fun SongDetailQueueScreenContent( ) } - var showDeleteFromDownloadsDialog by remember { mutableStateOf(null) } + var showDeleteFromDownloadsDialog by remember { mutableStateOf(null) } showDeleteFromDownloadsDialog?.let { songToRemove -> EraseConfirmDialog( onDismissRequest = { @@ -130,7 +130,7 @@ fun SongDetailQueueScreenContent( } } - var songToShare: Song? by remember { mutableStateOf(null) } + var songToShare: SongUI? by remember { mutableStateOf(null) } AnimatedVisibility(songToShare != null) { songToShare?.let { songS -> ShareDialog( diff --git a/domain/src/main/java/luci/sixsixsix/powerampache2/domain/utils/ShareManager.kt b/domain/src/main/java/luci/sixsixsix/powerampache2/domain/utils/ShareManager.kt index a06394c9..51df6f5d 100644 --- a/domain/src/main/java/luci/sixsixsix/powerampache2/domain/utils/ShareManager.kt +++ b/domain/src/main/java/luci/sixsixsix/powerampache2/domain/utils/ShareManager.kt @@ -31,8 +31,8 @@ interface ShareManager { suspend fun shareSongWeb(context: Context, song: Song) suspend fun fetchDeepLinkedSong(id: String, title: String, artist: String, - songCallback: (song: Song) -> Unit, - songsCallback: (songs: List) -> Unit, + songCallback: suspend (song: Song) -> Unit, + songsCallback: suspend (songs: List) -> Unit, errorCallback: () -> Unit) companion object {