diff --git a/composeApp/src/androidMain/kotlin/it/fast4x/rimusic/Database.kt b/composeApp/src/androidMain/kotlin/it/fast4x/rimusic/Database.kt index 320b431f2a..59acbd908b 100644 --- a/composeApp/src/androidMain/kotlin/it/fast4x/rimusic/Database.kt +++ b/composeApp/src/androidMain/kotlin/it/fast4x/rimusic/Database.kt @@ -1341,6 +1341,9 @@ interface Database { @Query("SELECT likedAt FROM Song WHERE id = :songId") fun likedAt(songId: String): Flow + @Query("SELECT likedAt FROM Song WHERE id = :songId") + fun getLikedAt(songId: String): Long? + @Query("UPDATE Album SET bookmarkedAt = :bookmarkedAt WHERE id = :id") fun bookmarkAlbum(id: String, bookmarkedAt: Long?): Int @@ -1988,6 +1991,18 @@ interface Database { """) fun sortSongsFromPlaylistByDuration( id: Long ): Flow> + @Query(""" + SELECT DISTINCT S.*, Album.title as albumTitle, Format.contentLength as contentLength + FROM Song S + INNER JOIN songplaylistmap SP ON S.id = SP.songId + LEFT JOIN SongAlbumMap ON SongAlbumMap.songId = S.id + LEFT JOIN Album ON Album.id = SongAlbumMap.albumId + LEFT JOIN Format ON Format.songId = S.id + WHERE SP.playlistId = :id + ORDER BY S.thumbnailUrl + """) + fun sortSongsFromPlaylistByUrl( id: Long ): Flow> + @Query(""" SELECT DISTINCT S.*, A.title as albumTitle, Format.contentLength as contentLength FROM Song S @@ -2050,6 +2065,7 @@ interface Database { PlaylistSongSortBy.Duration -> sortSongsFromPlaylistByDuration( id ) PlaylistSongSortBy.DateLiked -> sortSongsFromPlaylistByLikedAt( id ) PlaylistSongSortBy.DateAdded -> sortSongsFromPlaylistByRowId( id ) + PlaylistSongSortBy.UnmatchedSongs -> sortSongsFromPlaylistByUrl(id) }.map { it.run { if( sortOrder == SortOrder.Descending ) @@ -2067,6 +2083,10 @@ interface Database { @Query("SELECT SP.position FROM Song S INNER JOIN songplaylistmap SP ON S.id=SP.songId WHERE SP.playlistId=:id AND S.id NOT LIKE '$LOCAL_KEY_PREFIX%' ORDER BY SP.position") fun songsPlaylistMap(id: Long): Flow> + @Transaction + @Query("SELECT id FROM SONG WHERE likedAt IS NOT NULL AND likedAt < 0") + fun dislikedSongsById(): Flow> + @SuppressWarnings(RoomWarnings.QUERY_MISMATCH) @Transaction @Query("SELECT id, name, browseId, (SELECT COUNT(*) FROM SongPlaylistMap WHERE playlistId = id) as songCount FROM Playlist WHERE id=:id") diff --git a/composeApp/src/androidMain/kotlin/it/fast4x/rimusic/enums/PlaylistSongSortBy.kt b/composeApp/src/androidMain/kotlin/it/fast4x/rimusic/enums/PlaylistSongSortBy.kt index 0287bab3a9..733721221e 100644 --- a/composeApp/src/androidMain/kotlin/it/fast4x/rimusic/enums/PlaylistSongSortBy.kt +++ b/composeApp/src/androidMain/kotlin/it/fast4x/rimusic/enums/PlaylistSongSortBy.kt @@ -34,7 +34,9 @@ enum class PlaylistSongSortBy( DateLiked( R.string.sort_date_liked, R.drawable.heart ), - DateAdded( R.string.sort_date_added, R.drawable.time ); + DateAdded( R.string.sort_date_added, R.drawable.time ), + + UnmatchedSongs( R.string.unmatched, R.drawable.alert ); override val titleId: Int get() { diff --git a/composeApp/src/androidMain/kotlin/it/fast4x/rimusic/ui/components/SwipeableContent.kt b/composeApp/src/androidMain/kotlin/it/fast4x/rimusic/ui/components/SwipeableContent.kt index b3357ad0fa..acea535e99 100644 --- a/composeApp/src/androidMain/kotlin/it/fast4x/rimusic/ui/components/SwipeableContent.kt +++ b/composeApp/src/androidMain/kotlin/it/fast4x/rimusic/ui/components/SwipeableContent.kt @@ -176,10 +176,11 @@ fun SwipeableQueueItem( val onFavourite: () -> Unit = { mediaItemToggleLike(mediaItem) val message: String - if( likedAt != null ) { - val mTitle: String = mediaItem.mediaMetadata.title?.toString() ?: "" - val mArtist: String = mediaItem.mediaMetadata.artist?.toString() ?: "" - + val mTitle: String = mediaItem.mediaMetadata.title?.toString() ?: "" + val mArtist: String = mediaItem.mediaMetadata.artist?.toString() ?: "" + if(likedAt == -1L) { + message = "\"$mTitle - $mArtist\" ${context.resources.getString(R.string.removed_from_disliked)}" + } else if( likedAt != null ) { message = "\"$mTitle - $mArtist\" ${context.resources.getString(R.string.removed_from_favorites)}" } else message = context.resources.getString(R.string.added_to_favorites) @@ -251,10 +252,11 @@ fun SwipeablePlaylistItem( val onFavourite: () -> Unit = { mediaItemToggleLike(mediaItem) val message: String - if( likedAt != null ) { - val mTitle: String = mediaItem.mediaMetadata.title?.toString() ?: "" - val mArtist: String = mediaItem.mediaMetadata.artist?.toString() ?: "" - + val mTitle: String = mediaItem.mediaMetadata.title?.toString() ?: "" + val mArtist: String = mediaItem.mediaMetadata.artist?.toString() ?: "" + if(likedAt == -1L) { + message = "\"$mTitle - $mArtist\" ${context.resources.getString(R.string.removed_from_disliked)}" + } else if ( likedAt != null ) { message = "\"$mTitle - $mArtist\" ${context.resources.getString(R.string.removed_from_favorites)}" } else message = context.resources.getString(R.string.added_to_favorites) diff --git a/composeApp/src/androidMain/kotlin/it/fast4x/rimusic/ui/components/tab/ImportSongsFromCSV.kt b/composeApp/src/androidMain/kotlin/it/fast4x/rimusic/ui/components/tab/ImportSongsFromCSV.kt index d0d812a237..d2c8f49a43 100644 --- a/composeApp/src/androidMain/kotlin/it/fast4x/rimusic/ui/components/tab/ImportSongsFromCSV.kt +++ b/composeApp/src/androidMain/kotlin/it/fast4x/rimusic/ui/components/tab/ImportSongsFromCSV.kt @@ -16,6 +16,7 @@ import it.fast4x.rimusic.ui.components.themed.SmartMessage import it.fast4x.rimusic.appContext import it.fast4x.rimusic.ui.components.tab.toolbar.Descriptive import it.fast4x.rimusic.ui.components.tab.toolbar.MenuIcon +import it.fast4x.rimusic.utils.formatAsDuration class ImportSongsFromCSV private constructor( private val launcher: ManagedActivityResultLauncher, Uri?> @@ -38,18 +39,19 @@ class ImportSongsFromCSV private constructor( Database.asyncTransaction { beforeTransaction( index, row ) /**/ - val mediaId = row["MediaId"] - val title = row["Title"] + val explicitPrefix = if (row["Explicit"] == "true") "e:" else "" + val pseudoMediaId = (row["Track Name"]+row["Artist Name(s)"]).filter { it.isLetterOrDigit() } + val title = row["Title"] ?: row["Track Name"] ?: return@asyncTransaction + val mediaId = row["MediaId"] ?: pseudoMediaId + val artistsText = row["Artists"] ?: row["Artist Name(s)"] ?: "" + val durationText = row["Duration"] ?: formatAsDuration(row["Track Duration (ms)"]?.toLong() ?: 0L) - if( mediaId == null || title == null) - return@asyncTransaction - - val song = Song ( + val song = Song( id = mediaId, - title = title, - artistsText = row["Artists"], - durationText = row["Duration"], - thumbnailUrl = row["ThumbnailUrl"], + title = explicitPrefix+title, + artistsText = artistsText, + durationText = durationText, + thumbnailUrl = row["ThumbnailUrl"] ?: "", totalPlayTimeMs = 1L ) afterTransaction( index, song ) diff --git a/composeApp/src/androidMain/kotlin/it/fast4x/rimusic/ui/components/themed/Dialog.kt b/composeApp/src/androidMain/kotlin/it/fast4x/rimusic/ui/components/themed/Dialog.kt index 7d26b8c77e..092694be39 100644 --- a/composeApp/src/androidMain/kotlin/it/fast4x/rimusic/ui/components/themed/Dialog.kt +++ b/composeApp/src/androidMain/kotlin/it/fast4x/rimusic/ui/components/themed/Dialog.kt @@ -9,6 +9,7 @@ import androidx.compose.animation.fadeOut import androidx.compose.foundation.Canvas import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.basicMarquee import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.isSystemInDarkTheme @@ -18,7 +19,9 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.absoluteOffset import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -26,6 +29,9 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.requiredHeight import androidx.compose.foundation.layout.requiredSize import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.PageSize import androidx.compose.foundation.pager.rememberPagerState @@ -44,6 +50,7 @@ import androidx.compose.material.Text import androidx.compose.material.TextField import androidx.compose.material.TextFieldDefaults import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ripple import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -79,6 +86,7 @@ import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp @@ -90,6 +98,7 @@ import androidx.media3.common.util.UnstableApi import coil.compose.AsyncImage import coil.request.ImageRequest import it.fast4x.compose.persist.persist +import it.fast4x.innertube.Innertube import it.fast4x.rimusic.Database import it.fast4x.rimusic.LocalPlayerServiceBinder import it.fast4x.rimusic.R @@ -126,14 +135,25 @@ import it.fast4x.rimusic.utils.semiBold import it.fast4x.rimusic.utils.setDeviceVolume import it.fast4x.rimusic.utils.showCoverThumbnailAnimationKey import it.fast4x.rimusic.utils.thumbnailFadeKey -import it.fast4x.rimusic.utils.thumbnailOffsetKey import it.fast4x.rimusic.utils.thumbnailRoundnessKey import it.fast4x.rimusic.utils.thumbnailSpacingKey import kotlinx.coroutines.delay import it.fast4x.rimusic.colorPalette +import it.fast4x.rimusic.models.Song +import it.fast4x.rimusic.models.SongPlaylistMap import it.fast4x.rimusic.typography +import it.fast4x.rimusic.ui.styling.Dimensions +import it.fast4x.rimusic.ui.styling.onOverlay +import it.fast4x.rimusic.ui.styling.px +import it.fast4x.rimusic.utils.asMediaItem +import it.fast4x.rimusic.utils.asSong +import it.fast4x.rimusic.utils.getLikeState +import it.fast4x.rimusic.utils.isExplicit +import it.fast4x.rimusic.utils.left import it.fast4x.rimusic.utils.lyricsSizeKey import it.fast4x.rimusic.utils.lyricsSizeLKey +import it.fast4x.rimusic.utils.right +import it.fast4x.rimusic.utils.thumbnail import it.fast4x.rimusic.utils.thumbnailFadeExKey import it.fast4x.rimusic.utils.thumbnailSpacingLKey @@ -1740,6 +1760,267 @@ fun LyricsSizeDialog( } } +@Composable +fun InProgressDialog( + total : Int, + done : Int, + text : String, + onDismiss: (() -> Unit)? = null, +) { + DefaultDialog( + onDismiss = {if (onDismiss != null) {onDismiss()}}, + modifier = Modifier + .fillMaxWidth(if (isLandscape) 0.3f else 0.8f) + ) { + BasicText( + text = text, + style = TextStyle( + textAlign = TextAlign.Center, + fontSize = typography().l.bold.fontSize, + fontWeight = typography().l.bold.fontWeight, + color = colorPalette().text + ), + overflow = TextOverflow.Ellipsis, + ) + Spacer(modifier = Modifier + .height(10.dp) + ) + BasicText( + text = "$done / $total", + style = TextStyle( + textAlign = TextAlign.Center, + fontStyle = typography().xs.semiBold.fontStyle, + color = colorPalette().text + ), + overflow = TextOverflow.Ellipsis, + ) + } +} + +@Composable +fun SongMatchingDialog( + songsList : List?, + songToRematch : Song, + playlistId : Long, + position : Int, + onDismiss: (() -> Unit)? = null, +) { + Dialog( + onDismissRequest = {if (onDismiss != null) {onDismiss()}}, + properties = DialogProperties(usePlatformDefaultWidth = false) + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .fillMaxWidth(if (isLandscape) 0.5f else 0.9f) + .fillMaxHeight(if (isLandscape) 0.9f else 0.7f) + .background(color = colorPalette().background1,shape = RoundedCornerShape(8.dp)) + ) { + Row( + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .border(1.dp, colorPalette().text, shape = RoundedCornerShape(8.dp)) + .padding(horizontal = 5.dp) + .padding(vertical = 10.dp) + ) { + Box { + AsyncImage( + model = songToRematch.asMediaItem.mediaMetadata.artworkUri.thumbnail( + Dimensions.thumbnails.song.px / 2 + ), + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier + .padding(end = 5.dp) + .clip(RoundedCornerShape(5.dp)) + .size(40.dp) + ) + if (songToRematch.likedAt != null) { + HeaderIconButton( + onClick = {}, + icon = getLikeState(songToRematch.asMediaItem.mediaId), + color = colorPalette().favoritesIcon, + iconSize = 12.dp, + modifier = Modifier + .align(Alignment.BottomStart) + .absoluteOffset((-8).dp, 0.dp) + ) + } + } + Column( + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.Center + ) { + Row( + modifier = Modifier + .basicMarquee(iterations = Int.MAX_VALUE) + ) { + if (songToRematch.asMediaItem.isExplicit) { + IconButton( + icon = R.drawable.explicit, + color = colorPalette().text, + enabled = true, + onClick = {}, + modifier = Modifier + .size(18.dp) + ) + Spacer( + modifier = Modifier + .width(5.dp) + ) + } + BasicText( + text = cleanPrefix(songToRematch.title), + style = typography().xs.semiBold, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + ) { + BasicText( + text = songToRematch.artistsText ?: "", + style = typography().s.semiBold.secondary, + maxLines = 1, + overflow = TextOverflow.Clip, + modifier = Modifier + .weight(1f) + .basicMarquee(iterations = Int.MAX_VALUE) + ) + BasicText( + text = songToRematch.durationText ?: "", + style = typography().xs.secondary.medium, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .padding(end = 5.dp) + ) + } + } + } + + if (songsList?.isNotEmpty() == true) { + LazyColumn { + itemsIndexed(songsList) { _, song -> + Row(horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 10.dp) + .padding(vertical = 10.dp) + .clickable(onClick = { + Database.asyncTransaction { + deleteSongFromPlaylist(songToRematch.id, playlistId) + Database.insert(song.asSong) + insert( + SongPlaylistMap( + songId = song.asMediaItem.mediaId, + playlistId = playlistId, + position = position + ) + ) + } + if (onDismiss != null) { + onDismiss() + } + } + ) + ) { + Box { + AsyncImage( + model = song.asMediaItem.mediaMetadata.artworkUri.thumbnail( + Dimensions.thumbnails.song.px / 2 + ), + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier + .padding(end = 5.dp) + .clip(RoundedCornerShape(5.dp)) + .size(30.dp) + ) + if (song.asSong.likedAt != null) { + HeaderIconButton( + onClick = {}, + icon = getLikeState(song.asMediaItem.mediaId), + color = colorPalette().favoritesIcon, + iconSize = 9.dp, + modifier = Modifier + .align(Alignment.BottomStart) + .absoluteOffset((-6.75).dp, 0.dp) + ) + } + } + Column { + Row( + modifier = Modifier + .basicMarquee(iterations = Int.MAX_VALUE) + ) { + if (song.asMediaItem.isExplicit) { + IconButton( + icon = R.drawable.explicit, + color = colorPalette().text, + enabled = true, + onClick = {}, + modifier = Modifier + .size(18.dp) + ) + Spacer( + modifier = Modifier + .width(5.dp) + ) + } + BasicText( + text = cleanPrefix(song.title ?: ""), + style = typography().xs.semiBold, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + ) { + BasicText( + text = song.asSong.artistsText ?: "", + style = typography().xs.semiBold.secondary, + maxLines = 1, + overflow = TextOverflow.Clip, + modifier = Modifier + .weight(1f) + .basicMarquee(iterations = Int.MAX_VALUE) + ) + BasicText( + text = song.durationText ?: "", + style = typography().xxs.secondary.medium, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .padding(end = 5.dp) + ) + } + } + } + } + } + } else { + BasicText( + text = stringResource(R.string.songsnotfound), + style = typography().xl.semiBold, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } + } +} + /*if (isShowingLyrics && !showlyricsthumbnail) DefaultDialog( onDismiss = { diff --git a/composeApp/src/androidMain/kotlin/it/fast4x/rimusic/ui/components/themed/MediaItemMenu.kt b/composeApp/src/androidMain/kotlin/it/fast4x/rimusic/ui/components/themed/MediaItemMenu.kt index c69e818273..ba5f2bb9c5 100644 --- a/composeApp/src/androidMain/kotlin/it/fast4x/rimusic/ui/components/themed/MediaItemMenu.kt +++ b/composeApp/src/androidMain/kotlin/it/fast4x/rimusic/ui/components/themed/MediaItemMenu.kt @@ -165,6 +165,7 @@ fun InPlaylistMediaItemMenu( playlistId: Long, positionInPlaylist: Int, song: Song, + onMatchingSong: (() -> Unit)? = null, modifier: Modifier = Modifier, disableScrollingText: Boolean ) { @@ -206,6 +207,8 @@ fun InPlaylistMediaItemMenu( } MyDownloadHelper.autoDownloadWhenLiked(context(),song.asMediaItem) }, + onMatchingSong = { if (onMatchingSong != null) {onMatchingSong()} + onDismiss() }, modifier = modifier, disableScrollingText = disableScrollingText ) @@ -223,6 +226,7 @@ fun NonQueuedMediaItemMenuLibrary( onRemoveFromPlaylist: (() -> Unit)? = null, onRemoveFromQuickPicks: (() -> Unit)? = null, onDownload: (() -> Unit)? = null, + onMatchingSong: (() -> Unit)? = null, disableScrollingText: Boolean ) { val binder = LocalPlayerServiceBinder.current @@ -319,6 +323,7 @@ fun NonQueuedMediaItemMenuLibrary( } MyDownloadHelper.autoDownloadWhenLiked(context,mediaItem) }, + onMatchingSong = onMatchingSong, modifier = modifier, disableScrollingText = disableScrollingText ) @@ -340,6 +345,7 @@ fun NonQueuedMediaItemMenu( onRemoveFromQuickPicks: (() -> Unit)? = null, onDownload: (() -> Unit)? = null, onAddToPreferites: (() -> Unit)? = null, + onMatchingSong: (() -> Unit)? = null, disableScrollingText: Boolean ) { val binder = LocalPlayerServiceBinder.current @@ -402,6 +408,7 @@ fun NonQueuedMediaItemMenu( onDeleteFromDatabase = onDeleteFromDatabase, onRemoveFromQuickPicks = onRemoveFromQuickPicks, onAddToPreferites = onAddToPreferites, + onMatchingSong = onMatchingSong, modifier = modifier, disableScrollingText = disableScrollingText ) @@ -416,6 +423,7 @@ fun QueuedMediaItemMenu( navController: NavController, onDismiss: () -> Unit, onDownload: (() -> Unit)?, + onMatchingSong: (() -> Unit)? = null, mediaItem: MediaItem, indexInQueue: Int?, modifier: Modifier = Modifier, @@ -497,6 +505,7 @@ fun QueuedMediaItemMenu( } MyDownloadHelper.autoDownloadWhenLiked(context,mediaItem) }, + onMatchingSong = onMatchingSong, disableScrollingText = disableScrollingText ) } @@ -525,6 +534,7 @@ fun BaseMediaItemMenu( onClosePlayer: (() -> Unit)? = null, onGoToPlaylist: ((Long) -> Unit)? = null, onAddToPreferites: (() -> Unit)?, + onMatchingSong: (() -> Unit)?, disableScrollingText: Boolean ) { val context = LocalContext.current @@ -546,6 +556,7 @@ fun BaseMediaItemMenu( onEnqueue = onEnqueue, onDownload = onDownload, onAddToPreferites = onAddToPreferites, + onMatchingSong = onMatchingSong, onAddToPlaylist = { playlist, position -> Database.asyncTransaction { insert(mediaItem) @@ -761,6 +772,7 @@ fun MediaItemMenu( onRemoveFromQuickPicks: (() -> Unit)? = null, onShare: () -> Unit, onGoToPlaylist: ((Long) -> Unit)? = null, + onMatchingSong: (() -> Unit)? = null, disableScrollingText: Boolean ) { val density = LocalDensity.current @@ -1479,6 +1491,13 @@ fun MediaItemMenu( onClick = onAddToPreferites ) + if (onMatchingSong != null) + MenuEntry( + icon = R.drawable.random, + text = stringResource(R.string.match_song), + onClick = { onMatchingSong() } + ) + if (onAddToPlaylist != null) { MenuEntry( icon = R.drawable.add_in_playlist, diff --git a/composeApp/src/androidMain/kotlin/it/fast4x/rimusic/ui/components/themed/PlayerMenu.kt b/composeApp/src/androidMain/kotlin/it/fast4x/rimusic/ui/components/themed/PlayerMenu.kt index 428f7d4fd6..c7760c7143 100644 --- a/composeApp/src/androidMain/kotlin/it/fast4x/rimusic/ui/components/themed/PlayerMenu.kt +++ b/composeApp/src/androidMain/kotlin/it/fast4x/rimusic/ui/components/themed/PlayerMenu.kt @@ -37,6 +37,7 @@ fun PlayerMenu( mediaItem: MediaItem, onDismiss: () -> Unit, onClosePlayer: () -> Unit, + onMatchingSong: (() -> Unit)? = null, disableScrollingText: Boolean ) { @@ -124,6 +125,7 @@ fun PlayerMenu( } }, onClosePlayer = onClosePlayer, + onMatchingSong = onMatchingSong, disableScrollingText = disableScrollingText ) } diff --git a/composeApp/src/androidMain/kotlin/it/fast4x/rimusic/ui/components/themed/PlaylistsItemGridMenu.kt b/composeApp/src/androidMain/kotlin/it/fast4x/rimusic/ui/components/themed/PlaylistsItemGridMenu.kt index c7f39769e2..570aba1e51 100644 --- a/composeApp/src/androidMain/kotlin/it/fast4x/rimusic/ui/components/themed/PlaylistsItemGridMenu.kt +++ b/composeApp/src/androidMain/kotlin/it/fast4x/rimusic/ui/components/themed/PlaylistsItemGridMenu.kt @@ -75,6 +75,7 @@ fun PlaylistsItemGridMenu( playlist: PlaylistPreview? = null, modifier: Modifier = Modifier, onPlayNext: (() -> Unit)? = null, + onDeleteSongsNotInLibrary: (() -> Unit)? = null, onEnqueue: (() -> Unit)? = null, onImportOnlinePlaylist: (() -> Unit)? = null, onAddToPreferites: (() -> Unit)? = null, @@ -353,6 +354,19 @@ fun PlaylistsItemGridMenu( ) } + onDeleteSongsNotInLibrary?.let { onDeleteSongsNotInLibrary -> + GridMenuItem( + icon = R.drawable.trash, + title = R.string.delete_songs_not_in_library, + colorIcon = colorPalette.text, + colorText = colorPalette.text, + onClick = { + onDismiss() + onDeleteSongsNotInLibrary() + } + ) + } + onEnqueue?.let { onEnqueue -> GridMenuItem( icon = R.drawable.enqueue, diff --git a/composeApp/src/androidMain/kotlin/it/fast4x/rimusic/ui/components/themed/PlaylistsItemMenu.kt b/composeApp/src/androidMain/kotlin/it/fast4x/rimusic/ui/components/themed/PlaylistsItemMenu.kt index 224cbd260b..5a4c61739e 100644 --- a/composeApp/src/androidMain/kotlin/it/fast4x/rimusic/ui/components/themed/PlaylistsItemMenu.kt +++ b/composeApp/src/androidMain/kotlin/it/fast4x/rimusic/ui/components/themed/PlaylistsItemMenu.kt @@ -75,6 +75,7 @@ fun PlaylistsItemMenu( playlist: PlaylistPreview? = null, modifier: Modifier = Modifier, onPlayNext: (() -> Unit)? = null, + onDeleteSongsNotInLibrary: (() -> Unit)? = null, onEnqueue: (() -> Unit)? = null, onImportOnlinePlaylist: (() -> Unit)? = null, onAddToPlaylist: ((PlaylistPreview) -> Unit)? = null, @@ -115,6 +116,7 @@ fun PlaylistsItemMenu( playlist = playlist, onSelectUnselect = onSelectUnselect, onPlayNext = onPlayNext, + onDeleteSongsNotInLibrary = onDeleteSongsNotInLibrary, onEnqueue = onEnqueue, onImportOnlinePlaylist = onImportOnlinePlaylist, onAddToPlaylist = onAddToPlaylist, @@ -440,6 +442,16 @@ fun PlaylistsItemMenu( } ) } + onDeleteSongsNotInLibrary?.let { onDeleteSongsNotInLibrary -> + MenuEntry( + icon = R.drawable.trash, + text = stringResource(R.string.delete_songs_not_in_library), + onClick = { + onDismiss() + onDeleteSongsNotInLibrary() + } + ) + } onEnqueue?.let { onEnqueue -> MenuEntry( diff --git a/composeApp/src/androidMain/kotlin/it/fast4x/rimusic/ui/components/themed/SortMenu.kt b/composeApp/src/androidMain/kotlin/it/fast4x/rimusic/ui/components/themed/SortMenu.kt index 7ee64ee143..5909c61ea2 100644 --- a/composeApp/src/androidMain/kotlin/it/fast4x/rimusic/ui/components/themed/SortMenu.kt +++ b/composeApp/src/androidMain/kotlin/it/fast4x/rimusic/ui/components/themed/SortMenu.kt @@ -40,6 +40,7 @@ fun SortMenu ( onDateAdded: (() -> Unit)? = null, onDateLiked: (() -> Unit)? = null, onDuration: (() -> Unit)? = null, + onUnmatchedSong: (() -> Unit)? = null, modifier: Modifier = Modifier, ) { var height by remember { @@ -225,5 +226,16 @@ fun SortMenu ( ) } + onUnmatchedSong?.let { + MenuEntry( + icon = R.drawable.alert, + text = stringResource(R.string.unmatched), + onClick = { + onDismiss() + onUnmatchedSong() + } + ) + } + } } \ No newline at end of file diff --git a/composeApp/src/androidMain/kotlin/it/fast4x/rimusic/ui/screens/album/AlbumDetails.kt b/composeApp/src/androidMain/kotlin/it/fast4x/rimusic/ui/screens/album/AlbumDetails.kt index c2f570074b..f7c78812d3 100644 --- a/composeApp/src/androidMain/kotlin/it/fast4x/rimusic/ui/screens/album/AlbumDetails.kt +++ b/composeApp/src/androidMain/kotlin/it/fast4x/rimusic/ui/screens/album/AlbumDetails.kt @@ -142,6 +142,7 @@ import kotlinx.coroutines.withContext import me.bush.translator.Language import me.bush.translator.Translator import it.fast4x.rimusic.colorPalette +import it.fast4x.rimusic.enums.PopupType import it.fast4x.rimusic.models.SongAlbumMap import it.fast4x.rimusic.service.MyDownloadHelper import it.fast4x.rimusic.typography @@ -456,11 +457,11 @@ fun AlbumDetails( showConfirmDownloadAllDialog = false downloadState = Download.STATE_DOWNLOADING if (listMediaItems.isEmpty()) { - if (songs.isNotEmpty() == true) - songs.forEach { + if (songs.filter { it.likedAt != -1L }.isNotEmpty()){ + songs.filter { it.likedAt != -1L }.forEach { binder?.cache?.removeResource(it.asMediaItem.mediaId) CoroutineScope(Dispatchers.IO).launch { - Database.deleteFormat( it.asMediaItem.mediaId ) + Database.deleteFormat(it.asMediaItem.mediaId) } manageDownload( context = context, @@ -468,6 +469,7 @@ fun AlbumDetails( downloadState = false ) } + } } else { runCatching { listMediaItems.forEach { @@ -716,14 +718,17 @@ fun AlbumDetails( ) HeaderIconButton( icon = R.drawable.downloaded, - color = colorPalette() -.text, + color = if (songs.any { it.likedAt != -1L }) colorPalette().text else colorPalette().textDisabled, onClick = {}, modifier = Modifier .padding(horizontal = 5.dp) .combinedClickable( onClick = { - showConfirmDownloadAllDialog = true + if (songs.any { it.likedAt != -1L }) { + showConfirmDownloadAllDialog = true + } else { + SmartMessage(context.resources.getString(R.string.disliked_this_collection),type = PopupType.Error, context = context) + } }, onLongClick = { SmartMessage( @@ -778,22 +783,22 @@ fun AlbumDetails( HeaderIconButton( icon = R.drawable.shuffle, - enabled = songs.isNotEmpty(), - color = if (songs.isNotEmpty()) colorPalette() -.text else colorPalette() -.textDisabled, + enabled = songs.any { it.likedAt != -1L }, + color = if (songs.any { it.likedAt != -1L }) colorPalette().text else colorPalette().textDisabled, onClick = {}, modifier = Modifier .padding(horizontal = 5.dp) .combinedClickable( onClick = { - if (songs.isNotEmpty()) { + if (songs.any { it.likedAt != -1L }) { binder?.stopRadio() binder?.player?.forcePlayFromBeginning( - songs + songs.filter { it.likedAt != -1L } .shuffled() .map(Song::asMediaItem) ) + } else { + SmartMessage(context.resources.getString(R.string.disliked_this_collection),type = PopupType.Error, context = context) } }, onLongClick = { @@ -808,18 +813,19 @@ fun AlbumDetails( HeaderIconButton( icon = R.drawable.radio, enabled = true, - color = colorPalette() -.text, + color = if (songs.any { it.likedAt != -1L }) colorPalette().text else colorPalette().textDisabled, onClick = {}, modifier = Modifier .padding(horizontal = 5.dp) .combinedClickable( onClick = { - binder?.stopRadio() - binder?.player?.forcePlayFromBeginning( - songs.map(Song::asMediaItem) - ) - binder?.setupRadio(NavigationEndpoint.Endpoint.Watch(videoId = songs.first().id)) + if (songs.any { it.likedAt != -1L }) { + binder?.stopRadio() + binder?.player?.forcePlayFromBeginning(songs.filter { it.likedAt != -1L }.map(Song::asMediaItem)) + binder?.setupRadio(NavigationEndpoint.Endpoint.Watch(videoId = songs.first { it.likedAt != -1L }.id)) + } else { + SmartMessage(context.resources.getString(R.string.disliked_this_collection),type = PopupType.Error, context = context) + } }, onLongClick = { SmartMessage( @@ -921,10 +927,15 @@ fun AlbumDetails( }, onPlayNext = { if (listMediaItems.isEmpty()) { - binder?.player?.addNext( - songs.map(Song::asMediaItem), - context - ) + if (songs.any { it.likedAt != -1L }) { + binder?.player?.addNext( + songs.filter { it.likedAt != -1L } + .map(Song::asMediaItem), + context + ) + } else { + SmartMessage(context.resources.getString(R.string.disliked_this_collection),type = PopupType.Error, context = context) + } } else { binder?.player?.addNext(listMediaItems, context) listMediaItems.clear() @@ -933,10 +944,14 @@ fun AlbumDetails( }, onEnqueue = { if (listMediaItems.isEmpty()) { - binder?.player?.enqueue( - songs.map(Song::asMediaItem), - context - ) + if (songs.any { it.likedAt != -1L }) { + binder?.player?.enqueue( + songs.filter { it.likedAt != -1L } + .map(Song::asMediaItem),context + ) + } else { + SmartMessage(context.resources.getString(R.string.disliked_this_collection),type = PopupType.Error, context = context) + } } else { binder?.player?.enqueue(listMediaItems, context) listMediaItems.clear() @@ -1115,11 +1130,17 @@ fun AlbumDetails( }, onClick = { if (!selectItems) { - binder?.stopRadio() - binder?.player?.forcePlayAtIndex( - songs.map(Song::asMediaItem), - index - ) + if (song.likedAt != -1L) { + binder?.stopRadio() + binder?.player?.forcePlayAtIndex( + songs.filter { it.likedAt != -1L }.map(Song::asMediaItem), + songs.filter { it.likedAt != -1L }.map(Song::asMediaItem).indexOf(song.asMediaItem) + ) + } else { + CoroutineScope(Dispatchers.Main).launch { + SmartMessage(context.resources.getString(R.string.disliked_this_song),type = PopupType.Error, context = context) + } + } } else checkedState.value = !checkedState.value } ), @@ -1349,11 +1370,15 @@ fun AlbumDetails( MultiFloatingActionsContainer( iconId = R.drawable.shuffle, onClick = { - if (songs.isNotEmpty()) { + if (songs.any { it.likedAt != -1L }) { binder?.stopRadio() binder?.player?.forcePlayFromBeginning( - songs.shuffled().map(Song::asMediaItem) + songs.filter { it.likedAt != -1L } + .shuffled() + .map(Song::asMediaItem) ) + } else { + SmartMessage(context.resources.getString(R.string.disliked_this_collection),type = PopupType.Error, context = context) } }, onClickSettings = onSettingsClick, diff --git a/composeApp/src/androidMain/kotlin/it/fast4x/rimusic/ui/screens/home/HomeLibrary.kt b/composeApp/src/androidMain/kotlin/it/fast4x/rimusic/ui/screens/home/HomeLibrary.kt index fbc40ba88d..0f6c572d5f 100644 --- a/composeApp/src/androidMain/kotlin/it/fast4x/rimusic/ui/screens/home/HomeLibrary.kt +++ b/composeApp/src/androidMain/kotlin/it/fast4x/rimusic/ui/screens/home/HomeLibrary.kt @@ -89,6 +89,8 @@ import it.fast4x.rimusic.utils.Preference.HOME_LIBRARY_ITEM_SIZE import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter @ExperimentalMaterial3Api @@ -197,17 +199,23 @@ fun HomeLibrary( } } + val currentDateTime = LocalDateTime.now() + val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd-HH:mm:ss") + var time by remember {mutableStateOf("")} + val formattedDate = currentDateTime.format(formatter) // val importPlaylistDialog = ImportSongsFromCSV.init( beforeTransaction = { _, row -> - plistId = row["PlaylistName"]?.let { + time = formattedDate + val playlistName = row["PlaylistName"] ?: "New Playlist $time" + plistId = playlistName.let { Database.playlistExistByName( it ) - } ?: 0L + } if (plistId == 0L) - plistId = row["PlaylistName"]?.let { + plistId = playlistName.let { Database.insert( Playlist( plistId, it, row["PlaylistBrowseId"] ) ) - }!! + } }, afterTransaction = { index, song -> if (song.id.isBlank()) return@init diff --git a/composeApp/src/androidMain/kotlin/it/fast4x/rimusic/ui/screens/home/HomeSongsModern.kt b/composeApp/src/androidMain/kotlin/it/fast4x/rimusic/ui/screens/home/HomeSongsModern.kt index 1a689febd1..4bbe989406 100644 --- a/composeApp/src/androidMain/kotlin/it/fast4x/rimusic/ui/screens/home/HomeSongsModern.kt +++ b/composeApp/src/androidMain/kotlin/it/fast4x/rimusic/ui/screens/home/HomeSongsModern.kt @@ -192,9 +192,12 @@ import kotlin.math.min import kotlin.time.Duration import it.fast4x.rimusic.ui.components.SwipeablePlaylistItem import it.fast4x.rimusic.ui.components.themed.CacheSpaceIndicator +import it.fast4x.rimusic.ui.components.themed.InProgressDialog import it.fast4x.rimusic.ui.screens.settings.isYouTubeSyncEnabled import it.fast4x.rimusic.utils.isDownloadedSong import it.fast4x.rimusic.utils.isNowPlaying +import kotlinx.coroutines.withContext +import kotlin.system.exitProcess @OptIn(ExperimentalMaterial3Api::class) @@ -220,6 +223,7 @@ fun HomeSongsModern( val parentalControlEnabled by rememberPreference(parentalControlEnabledKey, false) var items by persistList("home/songs") + var itemsAll by persistList("") //var songsWithAlbum by persistList("home/songsWithAlbum") @@ -250,6 +254,10 @@ fun HomeSongsModern( mutableStateOf(0) } + LaunchedEffect(Unit) { + Database.listAllSongsAsFlow().collect { itemsAll = it } + } + var includeLocalSongs by rememberPreference(includeLocalSongsKey, true) var autoShuffle by rememberPreference(autoShuffleKey, false) @@ -306,6 +314,14 @@ fun HomeSongsModern( mutableStateOf(defaultFolder) } + var deleteProgressDialog by remember { + mutableStateOf(false) + } + + var totalSongsToDelete by remember { mutableIntStateOf(0) } + + var songsDeleted by remember { mutableIntStateOf(0) } + val importLauncher = rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { uri -> if (uri == null) return@rememberLauncherForActivityResult @@ -1016,13 +1032,17 @@ fun HomeSongsModern( if (builtInPlaylist == BuiltInPlaylist.Favorites) { HeaderIconButton( icon = R.drawable.downloaded, - enabled = songs.isNotEmpty(), - color = colorPalette().text, + enabled = (items.any { it.song.likedAt != -1L }), + color = if (items.any { it.song.likedAt != -1L }) colorPalette().text else colorPalette().text, onClick = {}, modifier = Modifier .combinedClickable( onClick = { - showConfirmDownloadAllDialog = true + if (items.any { it.song.likedAt != -1L }) { + showConfirmDownloadAllDialog = true + } else { + SmartMessage(context.resources.getString(R.string.disliked_this_collection),type = PopupType.Error, context = context) + } }, onLongClick = { SmartMessage( @@ -1043,8 +1063,8 @@ fun HomeSongsModern( //isRecommendationEnabled = false downloadState = Download.STATE_DOWNLOADING if (listMediaItems.isEmpty()) { - if (items.isNotEmpty() == true) - items.forEach { + if (items.any { it.song.likedAt != -1L }) { + items.filter { it.song.likedAt != -1L }.forEach { binder?.cache?.removeResource(it.song.asMediaItem.mediaId) manageDownload( context = context, @@ -1052,6 +1072,7 @@ fun HomeSongsModern( downloadState = false ) } + } } else { listMediaItems.forEach { binder?.cache?.removeResource(it.mediaId) @@ -1068,6 +1089,19 @@ fun HomeSongsModern( ) } + if (deleteProgressDialog){ + InProgressDialog(total = totalSongsToDelete, done = songsDeleted, text = stringResource(R.string.delete_in_process)) + } + + Database.asyncTransaction { + totalSongsToDelete = (itemsAll.filter { + !it.song.id.startsWith(LOCAL_KEY_PREFIX) + && it.song.likedAt == null + && songUsedInPlaylists(it.song.id) == 0 + && albumBookmarked(songAlbumInfo(it.song.id)?.id ?: "") == 0 + }).size + } + if (builtInPlaylist == BuiltInPlaylist.Favorites || builtInPlaylist == BuiltInPlaylist.Downloaded) { HeaderIconButton( icon = R.drawable.download, @@ -1143,8 +1177,8 @@ fun HomeSongsModern( HeaderIconButton( icon = R.drawable.shuffle, - enabled = items.isNotEmpty(), - color = colorPalette().text, + enabled = items.any { it.song.likedAt != -1L }, + color = if (items.any { it.song.likedAt != -1L }) colorPalette().text else colorPalette().textDisabled, onClick = {}, modifier = Modifier .padding(horizontal = 2.dp) @@ -1152,17 +1186,19 @@ fun HomeSongsModern( onClick = { if (builtInPlaylist == BuiltInPlaylist.OnDevice) items = filteredSongs - if (items.isNotEmpty()) { + if (items.filter { it.song.likedAt != -1L }.isNotEmpty()) { val itemsLimited = - if (items.size > maxSongsInQueue.number) items + if (items.filter { it.song.likedAt != -1L }.size > maxSongsInQueue.number) items.filter { it.song.likedAt != -1L } .shuffled() - .take(maxSongsInQueue.number.toInt()) else items + .take(maxSongsInQueue.number.toInt()) else items.filter { it.song.likedAt != -1L } binder?.stopRadio() binder?.player?.forcePlayFromBeginning( itemsLimited .shuffled() .map(SongEntity::asMediaItem) ) + } else { + SmartMessage(context.resources.getString(R.string.disliked_this_collection),type = PopupType.Error, context = context) } }, onLongClick = { @@ -1242,10 +1278,15 @@ fun HomeSongsModern( if (builtInPlaylist == BuiltInPlaylist.OnDevice) items = filteredSongs if (listMediaItems.isEmpty()) { - binder?.player?.addNext( - items.map(SongEntity::asMediaItem), - context - ) + if (items.any { it.song.likedAt != -1L }) { + binder?.player?.addNext( + items.filter { it.song.likedAt != -1L } + .map(SongEntity::asMediaItem), + context + ) + } else { + SmartMessage(context.resources.getString(R.string.disliked_this_collection),type = PopupType.Error, context = context) + } } else { binder?.player?.addNext(listMediaItems, context) listMediaItems.clear() @@ -1256,10 +1297,15 @@ fun HomeSongsModern( if (builtInPlaylist == BuiltInPlaylist.OnDevice) items = filteredSongs if (listMediaItems.isEmpty()) { - binder?.player?.enqueue( - items.map(SongEntity::asMediaItem), - context - ) + if (items.any { it.song.likedAt != -1L }) { + binder?.player?.enqueue( + items.filter { it.song.likedAt != -1L } + .map(SongEntity::asMediaItem), + context + ) + } else { + SmartMessage(context.resources.getString(R.string.disliked_this_collection),type = PopupType.Error, context = context) + } } else { binder?.player?.enqueue(listMediaItems, context) listMediaItems.clear() @@ -1323,6 +1369,31 @@ fun HomeSongsModern( ) } }, + onDeleteSongsNotInLibrary = { + if (totalSongsToDelete == 0) { + SmartMessage( + context.resources.getString(R.string.nothing_to_delete), + type = PopupType.Info, context = context + ) + } else { + songsDeleted = 0 + deleteProgressDialog = true + itemsAll.filter {!it.song.id.startsWith(LOCAL_KEY_PREFIX)}.forEach { song -> + Database.asyncTransaction { + if ((song.song.likedAt == null) && (Database.songUsedInPlaylists(song.song.id) == 0) && (Database.albumBookmarked(Database.songAlbumInfo(song.song.id)?.id?: "") == 0)) { + binder?.cache?.removeResource(song.song.id) + binder?.downloadCache?.removeResource(song.song.id) + Database.delete(song.song) + songsDeleted++ + if (songsDeleted == totalSongsToDelete) { + deleteProgressDialog = false + exitProcess(0) + } + } + } + } + } + }, onExport = { isExporting = true }, @@ -1874,64 +1945,78 @@ fun HomeSongsModern( hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) }, onClick = { - searching = false - filter = null - - val maxSongs = maxSongsInQueue.number.toInt() - val itemsRange: IntRange - val playIndex: Int - if (items.size < maxSongsInQueue.number) { - itemsRange = items.indices - playIndex = index - } else { - when (queueLimit) { - QueueSelection.START_OF_QUEUE -> { - // tries to guarantee maxSongs many songs - // window starting from index with maxSongs songs (if possible) - itemsRange = index.. { - // tries to guarantee >= maxSongs/2 many songs - // window with +- maxSongs/2 songs (if possible) around index - val minIndex = max(0, index - maxSongs/2) - val maxIndex = min(index + maxSongs/2, items.size) - itemsRange = minIndex.. { - // tries to guarantee maxSongs many songs - // window with maxSongs songs (if possible) ending at index - val minIndex = max(0, index - maxSongs + 1) - val maxIndex = min(index, items.size) - itemsRange = minIndex..maxIndex - - // index is located at end - playIndex = index - minIndex - } - QueueSelection.END_OF_QUEUE_WINDOWED -> { - // tries to guarantee maxSongs many songs, - // similar to original implementation in it's valid range - // window with maxSongs songs (if possible) before index - val minIndex = max(0, index - maxSongs + 1) - val maxIndex = min(minIndex+maxSongs, items.size) - itemsRange = minIndex.. { + // tries to guarantee maxSongs many songs + // window starting from index with maxSongs songs (if possible) + itemsRange = index.. { + // tries to guarantee >= maxSongs/2 many songs + // window with +- maxSongs/2 songs (if possible) around index + val minIndex = max(0, index - maxSongs / 2) + val maxIndex = + min(index + maxSongs / 2, items.size) + itemsRange = minIndex.. { + // tries to guarantee maxSongs many songs + // window with maxSongs songs (if possible) ending at index + val minIndex = max(0, index - maxSongs + 1) + val maxIndex = min(index, items.size) + itemsRange = minIndex..maxIndex + + // index is located at end + playIndex = index - minIndex + } + + QueueSelection.END_OF_QUEUE_WINDOWED -> { + // tries to guarantee maxSongs many songs, + // similar to original implementation in it's valid range + // window with maxSongs songs (if possible) before index + val minIndex = max(0, index - maxSongs + 1) + val maxIndex = + min(minIndex + maxSongs, items.size) + itemsRange = minIndex..("localPlaylist/$playlistId/songs") + var playlistSongsSortByPosition by persistList("localPlaylist/$playlistId/songs") var playlistPreview by persist("localPlaylist/playlist") val thumbnailUrl = remember { mutableStateOf("") } @@ -230,6 +242,11 @@ fun LocalPlaylistSongsModern( .collect { playlistSongs = it } } + LaunchedEffect(Unit) { + Database.songsPlaylist(playlistId, PlaylistSongSortBy.Position, SortOrder.Ascending).filterNotNull() + .collect { playlistSongsSortByPosition = it } + } + LaunchedEffect(Unit) { Database.singlePlaylistPreview(playlistId).collect { playlistPreview = it } } @@ -252,6 +269,15 @@ fun LocalPlaylistSongsModern( var songBaseRecommendation by persist("home/songBaseRecommendation") var positionsRecommendationList = arrayListOf() var autosync by rememberPreference(autosyncKey, false) + var songMatchingDialogEnable by remember { mutableStateOf(false) } + var matchingSong by remember { mutableStateOf(Song( + id = "", + title = "", + durationText = null, + thumbnailUrl = null + ) + ) + } if (isRecommendationEnabled) { LaunchedEffect(Unit, isRecommendationEnabled) { @@ -340,6 +366,43 @@ fun LocalPlaylistSongsModern( val isPipedEnabled by rememberPreference(isPipedEnabledKey, false) val coroutineScope = rememberCoroutineScope() val pipedSession = getPipedSession() + var searchedSongs: List? + fun filteredText(text : String): String{ + val filteredText = text + .lowercase() + .replace("(", " ") + .replace(")", " ") + .replace("-", " ") + .replace("lyrics", "") + .replace("vevo", "") + .replace(" hd", "") + .replace("official video", "") + .replace(Regex("\\s+"), " ") + .filter {it.isLetterOrDigit() || it.isWhitespace() || it == '\'' || it == ',' } + return filteredText + } + + if (songMatchingDialogEnable){ + val explicit = if (matchingSong.asMediaItem.isExplicit) " explicit" else "" + runBlocking(Dispatchers.IO) { + val searchQuery = Innertube.searchPage( + body = SearchBody( + query = filteredText("${cleanPrefix(matchingSong.title)} ${matchingSong.artistsText}$explicit"), + params = Innertube.SearchFilter.Song.value + ), + fromMusicShelfRendererContent = Innertube.SongItem.Companion::from + ) + + searchedSongs = searchQuery?.getOrNull()?.items + } + SongMatchingDialog( + songsList = searchedSongs, + songToRematch = matchingSong, + playlistId = playlistId, + position = playlistSongsSortByPosition.indexOf(SongEntity(song = matchingSong)), + onDismiss = {songMatchingDialogEnable = false} + ) + } if (isDeleting) { ConfirmationDialog( @@ -694,6 +757,7 @@ fun LocalPlaylistSongsModern( val playlistNotPipedType = playlistPreview?.playlist?.name?.startsWith(PIPED_PREFIX, 0, true) == false val hapticFeedback = LocalHapticFeedback.current + var unmatchedSongsCount = playlistSongs.filter { it.song.thumbnailUrl == "" }.size val editThumbnailLauncher = rememberLauncherForActivityResult( ActivityResultContracts.GetContent() @@ -727,6 +791,94 @@ fun LocalPlaylistSongsModern( } } + var getAlbumVersion by remember { mutableStateOf(false) } + var showGetAlbumVersionDialogue by remember { mutableStateOf(false) } + var showGetAlbumVersionDialogueExt by remember { mutableStateOf(false) } + var totalSongsToMatch by remember { mutableIntStateOf(0) } + var songsMatched by remember { mutableIntStateOf(0) } + + if (showGetAlbumVersionDialogue){ + InProgressDialog( + total = totalSongsToMatch, + done = songsMatched, + text = stringResource(R.string.matching_songs) + ) + } + + if (showGetAlbumVersionDialogueExt){ + InProgressDialog( + total = totalSongsToMatch, + done = songsMatched, + text = stringResource(R.string.matching_songs), + onDismiss = {showGetAlbumVersionDialogueExt = false} + ) + } + + + if (playlistSongsSortByPosition.any{songEntity -> songEntity.song.id == (cleanPrefix(songEntity.song.title)+songEntity.song.artistsText).filter{it.isLetterOrDigit()}}){ + showGetAlbumVersionDialogueExt = true + LaunchedEffect(Unit) { + withContext(Dispatchers.IO) { + totalSongsToMatch = playlistSongsSortByPosition + .filter{songEntity -> songEntity.song.id == (cleanPrefix(songEntity.song.title)+songEntity.song.artistsText).filter{it.isLetterOrDigit()}}.size + songsMatched = 0 + + val jobs = mutableListOf() + playlistSongsSortByPosition.forEachIndexed { index, video -> + if (video.song.id == (cleanPrefix(video.song.title)+video.song.artistsText).filter{it.isLetterOrDigit()}){ + jobs.add(coroutineScope.launch(Dispatchers.IO) { + getAlbumVersionFromVideo( + song = video.song, + playlistId = playlistId, + position = index + ) + } + ) + } + } + while(jobs.isNotEmpty()){ + val oldSize = jobs.size + jobs.removeIf{it.isCompleted} + songsMatched += oldSize - jobs.size + delay(10) + } + showGetAlbumVersionDialogueExt = false + getAlbumVersion = false + } + } + } + + LaunchedEffect(getAlbumVersion) { + withContext(Dispatchers.IO) { + totalSongsToMatch = playlistSongsSortByPosition + .filter {(it.song.thumbnailUrl?.startsWith("https://lh3.googleusercontent.com") == false) && !(it.song.id.startsWith(LOCAL_KEY_PREFIX))}.size + songsMatched = 0 + + val jobs = mutableListOf() + playlistSongsSortByPosition.forEachIndexed { index, video -> + if ((video.song.thumbnailUrl?.startsWith("https://lh3.googleusercontent.com") == false) && !(video.song.id.startsWith(LOCAL_KEY_PREFIX))) { + jobs.add(coroutineScope.launch(Dispatchers.IO) { + getAlbumVersionFromVideo( + song = video.song, + playlistId = playlistId, + position = index + ) + } + ) + } + } + while(jobs.isNotEmpty()){ + val oldSize = jobs.size + jobs.removeIf{it.isCompleted} + songsMatched += oldSize - jobs.size + delay(10) + } + + showGetAlbumVersionDialogue = false + getAlbumVersion = false + } + } + Box( modifier = Modifier .background(colorPalette.background0) @@ -810,7 +962,7 @@ fun LocalPlaylistSongsModern( ) { Spacer(modifier = Modifier.height(10.dp)) IconInfo( - title = playlistSongs.size.toString(), + title = playlistSongs.size.toString()+if (unmatchedSongsCount != 0){"($unmatchedSongsCount)"} else "", icon = painterResource(R.drawable.musical_notes) ) Spacer(modifier = Modifier.height(5.dp)) @@ -850,22 +1002,28 @@ fun LocalPlaylistSongsModern( Spacer(modifier = Modifier.height(10.dp)) HeaderIconButton( icon = R.drawable.shuffle, - enabled = playlistSongs.isNotEmpty() == true, - color = if (playlistSongs.isNotEmpty() == true) colorPalette.text else colorPalette.textDisabled, + enabled = playlistSongs.any { it.song.likedAt != -1L }, + color = if (playlistSongs.any { it.song.likedAt != -1L }) colorPalette.text else colorPalette.textDisabled, onClick = {}, modifier = Modifier .combinedClickable( onClick = { - playlistSongs.let { songs -> - if (songs.isNotEmpty()) { - val itemsLimited = - if (songs.size > maxSongsInQueue.number) songs.shuffled() - .take(maxSongsInQueue.number.toInt()) else songs - binder?.stopRadio() - binder?.player?.forcePlayFromBeginning( - itemsLimited.shuffled().map(SongEntity::asMediaItem) - ) - } + if (playlistSongs.any { it.song.likedAt != -1L }) { + playlistSongs.filter { it.song.likedAt != -1L } + .let { songs -> + if (songs.isNotEmpty()) { + val itemsLimited = + if (songs.size > maxSongsInQueue.number) songs.shuffled() + .take(maxSongsInQueue.number.toInt()) else songs + binder?.stopRadio() + binder?.player?.forcePlayFromBeginning( + itemsLimited.shuffled() + .map(SongEntity::asMediaItem) + ) + } + } + } else { + SmartMessage(context.resources.getString(R.string.disliked_this_collection),type = PopupType.Error, context = context) } }, onLongClick = { @@ -896,39 +1054,26 @@ fun LocalPlaylistSongsModern( .fillMaxWidth() ) { - if (playlistNotMonthlyType && - playlistPreview?.playlist?.browseId?.isEmpty() == true - ) - HeaderIconButton( - icon = R.drawable.pin, - enabled = playlistSongs.isNotEmpty(), - color = if (playlistPreview?.playlist?.name?.startsWith( - PINNED_PREFIX, - 0, - true - ) == true - ) - colorPalette.text else colorPalette.textDisabled, - onClick = {}, - modifier = Modifier - .combinedClickable( - onClick = { - Database.asyncTransaction { - if (playlistPreview?.playlist?.name?.startsWith( - PINNED_PREFIX, - 0, - true - ) == true - ) - Database.unPinPlaylist(playlistId) else - Database.pinPlaylist(playlistId) - } - }, - onLongClick = { - SmartMessage(context.resources.getString(R.string.info_pin_unpin_playlist), context = context) + HeaderIconButton( + icon = R.drawable.pin, + enabled = playlistSongs.isNotEmpty(), + color = if (playlistPreview?.playlist?.name?.startsWith(PINNED_PREFIX,0,true) == true) + colorPalette.text else colorPalette.textDisabled, + onClick = {}, + modifier = Modifier + .combinedClickable( + onClick = { + Database.asyncTransaction { + if (playlistPreview?.playlist?.name?.startsWith(PINNED_PREFIX,0,true) == true) + Database.unPinPlaylist(playlistId) else + Database.pinPlaylist(playlistId) } - ) - ) + }, + onLongClick = { + SmartMessage(context.resources.getString(R.string.info_pin_unpin_playlist), context = context) + } + ) + ) if (sortBy == PlaylistSongSortBy.Position && sortOrder == SortOrder.Ascending) HeaderIconButton( @@ -956,13 +1101,17 @@ fun LocalPlaylistSongsModern( HeaderIconButton( icon = R.drawable.downloaded, - enabled = playlistSongs.isNotEmpty(), - color = if (playlistSongs.isNotEmpty()) colorPalette.text else colorPalette.textDisabled, + enabled = playlistSongs.any { it.song.likedAt != -1L }, + color = if (playlistSongs.any { it.song.likedAt != -1L }) colorPalette.text else colorPalette.textDisabled, onClick = {}, modifier = Modifier .combinedClickable( onClick = { - showConfirmDownloadAllDialog = true + if (playlistSongs.any { it.song.likedAt != -1L }) { + showConfirmDownloadAllDialog = true + } else { + SmartMessage(context.resources.getString(R.string.disliked_this_collection),type = PopupType.Error, context = context) + } }, onLongClick = { SmartMessage(context.resources.getString(R.string.info_download_all_songs), context = context) @@ -980,8 +1129,8 @@ fun LocalPlaylistSongsModern( isRecommendationEnabled = false downloadState = Download.STATE_DOWNLOADING if (listMediaItems.isEmpty()) { - if (playlistSongs.isNotEmpty() == true) - playlistSongs.forEach { + if (playlistSongs.any { it.song.likedAt != -1L }) { + playlistSongs.filter { it.song.likedAt != -1L }.forEach { binder?.cache?.removeResource(it.asMediaItem.mediaId) Database.asyncTransaction { Database.insert( @@ -1000,6 +1149,7 @@ fun LocalPlaylistSongsModern( downloadState = false ) } + } } else { listMediaItems.forEach { binder?.cache?.removeResource(it.mediaId) @@ -1030,6 +1180,26 @@ fun LocalPlaylistSongsModern( } ) ) + HeaderIconButton( + icon = R.drawable.random, + enabled = playlistSongs.any {(it.song.thumbnailUrl?.startsWith("https://lh3.googleusercontent.com") == false) && !(it.song.id.startsWith(LOCAL_KEY_PREFIX))}, + color = if (playlistSongs.any {(it.song.thumbnailUrl?.startsWith("https://lh3.googleusercontent.com") == false) && !(it.song.id.startsWith(LOCAL_KEY_PREFIX))}) colorPalette.text else colorPalette.textDisabled, + onClick = {}, + modifier = Modifier + .combinedClickable( + onClick = { + if (playlistSongs.any {(it.song.thumbnailUrl?.startsWith("https://lh3.googleusercontent.com") == false) && !(it.song.id.startsWith(LOCAL_KEY_PREFIX))}) { + getAlbumVersion = true + showGetAlbumVersionDialogue = true + } else { + SmartMessage(context.resources.getString(R.string.no_videos_found), context = context) + } + }, + onLongClick = { + SmartMessage(context.resources.getString(R.string.get_album_version), context = context) + } + ) + ) if (showConfirmDeleteDownloadDialog) { ConfirmationDialog( @@ -1132,10 +1302,11 @@ fun LocalPlaylistSongsModern( playlist = playlistPreview, onEnqueue = { if (listMediaItems.isEmpty()) { - binder?.player?.enqueue( - playlistSongs.map(SongEntity::asMediaItem), - context - ) + if (playlistSongs.any { it.song.likedAt != -1L }) { + binder?.player?.enqueue(playlistSongs.filter { it.song.likedAt != -1L }.map(SongEntity::asMediaItem),context) + } else { + SmartMessage(context.resources.getString(R.string.disliked_this_collection),type = PopupType.Error, context = context) + } } else { binder?.player?.enqueue(listMediaItems, context) listMediaItems.clear() @@ -1144,10 +1315,11 @@ fun LocalPlaylistSongsModern( }, onPlayNext = { if (listMediaItems.isEmpty()) { - binder?.player?.addNext( - playlistSongs.map(SongEntity::asMediaItem), - context - ) + if (playlistSongs.any { it.song.likedAt != -1L }) { + binder?.player?.addNext(playlistSongs.filter { it.song.likedAt != -1L }.map(SongEntity::asMediaItem),context) + } else { + SmartMessage(context.resources.getString(R.string.disliked_this_collection),type = PopupType.Error, context = context) + } } else { binder?.player?.addNext(listMediaItems, context) listMediaItems.clear() @@ -1391,6 +1563,7 @@ fun LocalPlaylistSongsModern( PlaylistSongSortBy.Duration -> stringResource(R.string.sort_duration) PlaylistSongSortBy.DateAdded -> stringResource(R.string.sort_date_added) PlaylistSongSortBy.RelativePlayTime -> stringResource(R.string.sort_relative_listening_time) + PlaylistSongSortBy.UnmatchedSongs -> stringResource(R.string.unmatched) }, style = typography.xs.semiBold, maxLines = 1, @@ -1416,7 +1589,8 @@ fun LocalPlaylistSongsModern( sortBy = PlaylistSongSortBy.RelativePlayTime }, onDuration = { sortBy = PlaylistSongSortBy.Duration }, - onDateAdded = { sortBy = PlaylistSongSortBy.DateAdded } + onDateAdded = { sortBy = PlaylistSongSortBy.DateAdded }, + onUnmatchedSong = {sortBy = PlaylistSongSortBy.UnmatchedSongs} ) } @@ -1812,6 +1986,10 @@ fun LocalPlaylistSongsModern( onLongClick = { menuState.display { InPlaylistMediaItemMenu( + onMatchingSong = { + songMatchingDialogEnable = true + matchingSong = song.song + }, navController = navController, playlist = playlistPreview, playlistId = playlistId, @@ -1825,17 +2003,23 @@ fun LocalPlaylistSongsModern( }, onClick = { if (!selectItems) { - searching = false - filter = null - playlistSongs - .map(SongEntity::asMediaItem) - .let { mediaItems -> - binder?.stopRadio() - binder?.player?.forcePlayAtIndex( - mediaItems, - index - ) + if (song.song.likedAt != -1L) { + searching = false + filter = null + playlistSongs.filter { it.song.likedAt != -1L } + .map(SongEntity::asMediaItem) + .let { mediaItems -> + binder?.stopRadio() + binder?.player?.forcePlayAtIndex( + mediaItems, + mediaItems.indexOf(song.asMediaItem) + ) + } + } else { + CoroutineScope(Dispatchers.Main).launch { + SmartMessage(context.resources.getString(R.string.disliked_this_song),type = PopupType.Error, context = context) } + } } else checkedState.value = !checkedState.value } ) @@ -1866,13 +2050,17 @@ fun LocalPlaylistSongsModern( iconId = R.drawable.shuffle, visible = !reorderingState.isDragging, onClick = { - playlistSongs.let { songs -> - if (songs.isNotEmpty()) { - binder?.stopRadio() - binder?.player?.forcePlayFromBeginning( - songs.shuffled().map(SongEntity::asMediaItem) - ) + if (playlistSongs.any { it.song.likedAt != -1L }) { + playlistSongs.filter { it.song.likedAt != -1L }.let { songs -> + if (songs.isNotEmpty()) { + binder?.stopRadio() + binder?.player?.forcePlayFromBeginning( + songs.shuffled().map(SongEntity::asMediaItem) + ) + } } + } else { + SmartMessage(context.resources.getString(R.string.disliked_this_collection),type = PopupType.Error, context = context) } } ) diff --git a/composeApp/src/androidMain/kotlin/it/fast4x/rimusic/ui/screens/player/Lyrics.kt b/composeApp/src/androidMain/kotlin/it/fast4x/rimusic/ui/screens/player/Lyrics.kt index 71e2f7827e..a8e35d1b41 100644 --- a/composeApp/src/androidMain/kotlin/it/fast4x/rimusic/ui/screens/player/Lyrics.kt +++ b/composeApp/src/androidMain/kotlin/it/fast4x/rimusic/ui/screens/player/Lyrics.kt @@ -58,8 +58,10 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.rotate import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.LinearGradientShader @@ -160,10 +162,16 @@ import it.fast4x.rimusic.thumbnailShape import it.fast4x.rimusic.typography import it.fast4x.rimusic.ui.components.themed.LyricsSizeDialog import it.fast4x.rimusic.utils.conditional +import it.fast4x.rimusic.utils.effectRotationKey +import it.fast4x.rimusic.utils.jumpPreviousKey +import it.fast4x.rimusic.utils.landscapeControlsKey import it.fast4x.rimusic.utils.lyricsSizeAnimateKey import it.fast4x.rimusic.utils.lyricsSizeKey import it.fast4x.rimusic.utils.lyricsSizeLKey +import it.fast4x.rimusic.utils.playNext +import it.fast4x.rimusic.utils.playPrevious import timber.log.Timber +import kotlin.Float.Companion.POSITIVE_INFINITY import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.seconds @@ -350,6 +358,14 @@ fun Lyrics( mutableStateOf(false) } val lightTheme = colorPaletteMode == ColorPaletteMode.Light || (colorPaletteMode == ColorPaletteMode.System && (!isSystemInDarkTheme())) + val effectRotationEnabled by rememberPreference(effectRotationKey, true) + var landscapeControls by rememberPreference(landscapeControlsKey, true) + var jumpPrevious by rememberPreference(jumpPreviousKey,"3") + var isRotated by rememberSaveable { mutableStateOf(false) } + val rotationAngle by animateFloatAsState( + targetValue = if (isRotated) 360F else 0f, + animationSpec = tween(durationMillis = 200), label = "" + ) if (showLyricsSizeDialog) { LyricsSizeDialog( @@ -1764,6 +1780,95 @@ fun Lyrics( .size(24.dp) ) } + if (!showlyricsthumbnail && isDisplayed && isLandscape && landscapeControls) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, + modifier = Modifier + .fillMaxWidth() + .background( + brush = Brush.verticalGradient( + colors = listOf(Color.Transparent,if (lightTheme) Color.White.copy(0.5f) else Color.Black.copy(0.5f)), + startY = 0f, + endY = POSITIVE_INFINITY + ), + ) + .align(Alignment.BottomCenter) + .padding(bottom = 10.dp) + ){ + Image( + painter = painterResource(R.drawable.play_skip_back), + contentDescription = null, + colorFilter = ColorFilter.tint(colorPalette().text), + modifier = Modifier + .clickable( + indication = ripple(bounded = false), + interactionSource = remember { MutableInteractionSource() }, + onClick = { + if (jumpPrevious == "") jumpPrevious = "0" + if(binder?.player?.hasPreviousMediaItem() == false || (jumpPrevious != "0" && (binder?.player?.currentPosition ?: 0) > jumpPrevious.toInt() * 1000) + ){ + binder?.player?.seekTo(0) + } + else binder?.player?.playPrevious() + if (effectRotationEnabled) isRotated = !isRotated + } + ) + .rotate(rotationAngle) + .padding(horizontal = 15.dp) + .size(30.dp) + + ) + Box { + Box(modifier = Modifier + .align(Alignment.Center) + .size(45.dp) + .background(colorPalette().accent, RoundedCornerShape(15.dp)) + ){} + Image( + painter = painterResource(if (binder?.player?.isPlaying == true) R.drawable.pause else R.drawable.play), + contentDescription = null, + colorFilter = ColorFilter.tint(colorPalette().text), + modifier = Modifier + .clickable( + indication = ripple(bounded = false), + interactionSource = remember { MutableInteractionSource() }, + onClick = { + if (binder?.player?.isPlaying == true) { + binder.callPause({}) + } else { + binder?.player?.play() + } + }, + ) + .align(Alignment.Center) + .rotate(rotationAngle) + .padding(horizontal = 15.dp) + .size(36.dp) + + ) + } + Image( + painter = painterResource(R.drawable.play_skip_forward), + contentDescription = null, + colorFilter = ColorFilter.tint(colorPalette().text), + modifier = Modifier + .clickable( + indication = ripple(bounded = false), + interactionSource = remember { MutableInteractionSource() }, + onClick = { + binder?.player?.playNext() + if (effectRotationEnabled) isRotated = !isRotated + } + ) + .rotate(rotationAngle) + .padding(horizontal = 15.dp) + .size(30.dp) + + ) + } + + } Box( modifier = Modifier .align(Alignment.BottomEnd) @@ -1800,6 +1905,17 @@ fun Lyrics( onClick = { menuState.display { Menu { + if (isLandscape && !showlyricsthumbnail){ + MenuEntry( + icon = if (landscapeControls) R.drawable.checkmark else R.drawable.play, + text = stringResource(R.string.toggle_controls_landscape), + enabled = true, + onClick = { + menuState.hide() + landscapeControls = !landscapeControls + } + ) + } MenuEntry( icon = R.drawable.text, enabled = true, diff --git a/composeApp/src/androidMain/kotlin/it/fast4x/rimusic/ui/screens/player/Player.kt b/composeApp/src/androidMain/kotlin/it/fast4x/rimusic/ui/screens/player/Player.kt index fd487bf747..42846f17a1 100644 --- a/composeApp/src/androidMain/kotlin/it/fast4x/rimusic/ui/screens/player/Player.kt +++ b/composeApp/src/androidMain/kotlin/it/fast4x/rimusic/ui/screens/player/Player.kt @@ -228,6 +228,7 @@ import it.fast4x.rimusic.utils.horizontalFadingEdge import it.fast4x.rimusic.utils.isDownloadedSong import it.fast4x.rimusic.utils.isExplicit import it.fast4x.rimusic.utils.isLandscape +import it.fast4x.rimusic.utils.jumpPreviousKey import it.fast4x.rimusic.utils.manageDownload import it.fast4x.rimusic.utils.mediaItems import it.fast4x.rimusic.utils.miniQueueExpandedKey @@ -954,6 +955,15 @@ fun Player( var tempGradient by remember{ mutableStateOf(AnimatedGradient.Linear) } var albumCoverRotation by rememberPreference(albumCoverRotationKey, false) + @Composable + fun Modifier.conditional(condition : Boolean, modifier : @Composable Modifier.() -> Modifier) : Modifier { + return if (condition) { + then(modifier(Modifier)) + } else { + this + } + } + if (animatedGradient == AnimatedGradient.Random){ LaunchedEffect(mediaItem.mediaId){ valueGrad = (2..13).random() @@ -1086,20 +1096,22 @@ fun Player( .onSizeChanged { sizeShader = Size(it.width.toFloat(), it.height.toFloat()) } - .shaderBackground( - MeshGradient( - arrayOf( - saturate(vibrant).darkenBy(), - saturate(lightVibrant).darkenBy(), - saturate(darkVibrant).darkenBy(), - saturate(muted).darkenBy(), - saturate(lightMuted).darkenBy(), - saturate(darkMuted).darkenBy(), - saturate(dominant).darkenBy() - ), - scale = 1f + .conditional(!appRunningInBackground) { + shaderBackground( + MeshGradient( + arrayOf( + saturate(vibrant).darkenBy(), + saturate(lightVibrant).darkenBy(), + saturate(darkVibrant).darkenBy(), + saturate(muted).darkenBy(), + saturate(lightMuted).darkenBy(), + saturate(darkMuted).darkenBy(), + saturate(dominant).darkenBy() + ), + scale = 1f + ) ) - ) + } } else if ((animatedGradient == AnimatedGradient.Random && tempGradient == gradients[4]) || animatedGradient == AnimatedGradient.MesmerizingLens){ containerModifier = containerModifier @@ -1319,14 +1331,6 @@ fun Player( } val textoutline by rememberPreference(textoutlineKey, false) - fun Modifier.conditional(condition : Boolean, modifier : Modifier.() -> Modifier) : Modifier { - return if (condition) { - then(modifier(Modifier)) - } else { - this - } - } - var songPlaylist by remember { mutableStateOf(0) } @@ -2166,29 +2170,8 @@ fun Player( modifier = Modifier .weight(1f) .navigationBarsPadding() - .pointerInput(Unit) { - detectHorizontalDragGestures( - onHorizontalDrag = { change, dragAmount -> - deltaX = dragAmount - }, - onDragStart = { - }, - onDragEnd = { - if (!disablePlayerHorizontalSwipe) { - if (deltaX > 5) { - binder.player.playPrevious() - } else if (deltaX < -5) { - binder.player.playNext() - } - - } - - } - - ) - } ){ - if (!showlyricsthumbnail) + if (!showlyricsthumbnail) { Lyrics( mediaId = mediaItem.mediaId, isDisplayed = isShowingLyrics, @@ -2201,7 +2184,30 @@ fun Player( durationProvider = player::getDuration, isLandscape = isLandscape, clickLyricsText = clickLyricsText, + modifier = Modifier + .pointerInput(Unit) { + detectHorizontalDragGestures( + onHorizontalDrag = { change, dragAmount -> + deltaX = dragAmount + }, + onDragStart = { + }, + onDragEnd = { + if (!disablePlayerHorizontalSwipe) { + if (deltaX > 5) { + binder.player.playPrevious() + } else if (deltaX < -5) { + binder.player.playNext() + } + + } + + } + + ) + } ) + } } } Column ( @@ -2220,7 +2226,7 @@ fun Player( if (showthumbnail) { if (!isShowingVisualizer) { val fling = PagerDefaults.flingBehavior(state = pagerState,snapPositionalThreshold = 0.25f) - val pageSpacing = thumbnailSpacing.toInt()*0.01*(screenWidth) - (2.5*playerThumbnailSize.size.dp) + val pageSpacing = thumbnailSpacingL.toInt()*0.01*(screenWidth) - (2.5*playerThumbnailSizeL.size.dp) LaunchedEffect(pagerState, binder.player.currentMediaItemIndex) { if (appRunningInBackground || isShowingLyrics) { @@ -2246,6 +2252,7 @@ fun Player( contentPadding = PaddingValues(start = ((maxWidth - maxHeight)/2).coerceAtLeast(0.dp), end = ((maxWidth - maxHeight)/2 + if (pageSpacing < 0.dp) (-(pageSpacing)) else 0.dp).coerceAtLeast(0.dp)), beyondViewportPageCount = 3, flingBehavior = fling, + userScrollEnabled = !disablePlayerHorizontalSwipe, modifier = Modifier .padding( all = (if (thumbnailType == ThumbnailType.Modern) -(10.dp) else 0.dp).coerceAtLeast( @@ -2832,6 +2839,7 @@ fun Player( pageSpacing = if (expandedplayer) (thumbnailSpacing.toInt()*0.01*(screenHeight) - if (carousel) (3*carouselSize.size.dp) else (2*carouselSize.size.dp)) else 10.dp, beyondViewportPageCount = 2, flingBehavior = fling, + userScrollEnabled = expandedplayer || !disablePlayerHorizontalSwipe, modifier = modifier .padding( all = (if (expandedplayer) 0.dp else if (thumbnailType == ThumbnailType.Modern) -(10.dp) else 0.dp).coerceAtLeast( diff --git a/composeApp/src/androidMain/kotlin/it/fast4x/rimusic/ui/screens/player/components/controls/Essential.kt b/composeApp/src/androidMain/kotlin/it/fast4x/rimusic/ui/screens/player/components/controls/Essential.kt index 5a099186da..785e0e6abc 100644 --- a/composeApp/src/androidMain/kotlin/it/fast4x/rimusic/ui/screens/player/components/controls/Essential.kt +++ b/composeApp/src/androidMain/kotlin/it/fast4x/rimusic/ui/screens/player/components/controls/Essential.kt @@ -2,6 +2,7 @@ package it.fast4x.rimusic.ui.screens.player.components.controls import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.animateDp +import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.tween import androidx.compose.animation.core.updateTransition import androidx.compose.foundation.ExperimentalFoundationApi @@ -392,7 +393,6 @@ fun ControlsEssential( likedAt: Long?, mediaId: String, playerPlayButtonType: PlayerPlayButtonType, - rotationAngle: Float, isGradientBackgroundEnabled: Boolean, onShowSpeedPlayerDialog: () -> Unit, ) { @@ -400,6 +400,10 @@ fun ControlsEssential( val colorPaletteMode by rememberPreference(colorPaletteModeKey, ColorPaletteMode.Dark) var effectRotationEnabled by rememberPreference(effectRotationKey, true) var isRotated by rememberSaveable { mutableStateOf(false) } + val rotationAngle by animateFloatAsState( + targetValue = if (isRotated) 360F else 0f, + animationSpec = tween(durationMillis = 200), label = "" + ) val shouldBePlayingTransition = updateTransition(shouldBePlaying, label = "shouldBePlaying") val playPauseRoundness by shouldBePlayingTransition.animateDp( transitionSpec = { tween(durationMillis = 100, easing = LinearEasing) }, diff --git a/composeApp/src/androidMain/kotlin/it/fast4x/rimusic/ui/screens/player/components/controls/Modern.kt b/composeApp/src/androidMain/kotlin/it/fast4x/rimusic/ui/screens/player/components/controls/Modern.kt index f806605906..6e5457dd6f 100644 --- a/composeApp/src/androidMain/kotlin/it/fast4x/rimusic/ui/screens/player/components/controls/Modern.kt +++ b/composeApp/src/androidMain/kotlin/it/fast4x/rimusic/ui/screens/player/components/controls/Modern.kt @@ -1,6 +1,8 @@ package it.fast4x.rimusic.ui.screens.player.components.controls import android.os.Build +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image import androidx.compose.foundation.basicMarquee @@ -390,12 +392,15 @@ fun ControlsModern( playbackSpeed: Float, shouldBePlaying: Boolean, playerPlayButtonType: PlayerPlayButtonType, - rotationAngle: Float, isGradientBackgroundEnabled: Boolean, onShowSpeedPlayerDialog: () -> Unit, ) { var effectRotationEnabled by rememberPreference(effectRotationKey, true) var isRotated by rememberSaveable { mutableStateOf(false) } + val rotationAngle by animateFloatAsState( + targetValue = if (isRotated) 360F else 0f, + animationSpec = tween(durationMillis = 200), label = "" + ) var jumpPrevious by rememberPreference(jumpPreviousKey, "3") if (playerPlayButtonType != PlayerPlayButtonType.Disabled) { @@ -464,7 +469,8 @@ fun ControlsModern( modifier = Modifier .offset(x = (0).dp, y = (0).dp) .blur(7.dp) - .size(115.dp), + .size(115.dp) + .rotate(rotationAngle), tint = Color.Black.copy(0.75f) ) } @@ -627,7 +633,8 @@ fun ControlsModern( modifier = Modifier .offset(x = (8).dp, y = (8).dp) .blur(4.dp) - .size(38.dp), + .size(38.dp) + .rotate(rotationAngle), tint = Color.Black ) Image( @@ -664,7 +671,8 @@ fun ControlsModern( modifier = Modifier .offset(x = (0).dp, y = (0).dp) .blur(7.dp) - .size(54.dp), + .size(54.dp) + .rotate(rotationAngle), tint = Color.Black ) Image( @@ -708,7 +716,8 @@ fun ControlsModern( modifier = Modifier .offset(x = (8).dp, y = (8).dp) .blur(4.dp) - .size(38.dp), + .size(38.dp) + .rotate(rotationAngle), tint = Color.Black ) Image( diff --git a/composeApp/src/androidMain/kotlin/it/fast4x/rimusic/ui/screens/playlist/PlaylistSongList.kt b/composeApp/src/androidMain/kotlin/it/fast4x/rimusic/ui/screens/playlist/PlaylistSongList.kt index c8d549b5ab..42a0676ade 100644 --- a/composeApp/src/androidMain/kotlin/it/fast4x/rimusic/ui/screens/playlist/PlaylistSongList.kt +++ b/composeApp/src/androidMain/kotlin/it/fast4x/rimusic/ui/screens/playlist/PlaylistSongList.kt @@ -141,6 +141,7 @@ import it.fast4x.rimusic.models.Song import it.fast4x.rimusic.typography import it.fast4x.rimusic.ui.screens.settings.isYouTubeSyncEnabled import it.fast4x.rimusic.utils.setLikeState +import kotlinx.coroutines.flow.filterNotNull import timber.log.Timber @@ -240,6 +241,13 @@ fun PlaylistSongList( durationTextToMillis(it1) }?.toLong() ?: 0 } + var dislikedSongs by persistList("") + + LaunchedEffect(Unit) { + Database.dislikedSongsById().filterNotNull() + .collect { dislikedSongs = it } + } + if (isImportingPlaylist) { InputTextDialog( onDismiss = { isImportingPlaylist = false }, @@ -427,20 +435,24 @@ fun PlaylistSongList( .padding(horizontal = 5.dp) .combinedClickable( onClick = { - downloadState = Download.STATE_DOWNLOADING - if (playlistPage?.songs?.isNotEmpty() == true) - playlistPage?.songs?.forEach { - binder?.cache?.removeResource(it.asMediaItem.mediaId) - CoroutineScope(Dispatchers.IO).launch { - Database.deleteFormat( it.asMediaItem.mediaId ) - } - manageDownload( - context = context, - mediaItem = it.asMediaItem, - downloadState = false - ) - } - }, + if (playlistPage?.songs?.any { it.asMediaItem.mediaId !in dislikedSongs } == true) { + downloadState = Download.STATE_DOWNLOADING + if (playlistPage?.songs?.any { it.asMediaItem.mediaId !in dislikedSongs } == true) + playlistPage?.songs?.filter{ it.asMediaItem.mediaId !in dislikedSongs }?.forEach { + binder?.cache?.removeResource(it.asMediaItem.mediaId) + CoroutineScope(Dispatchers.IO).launch { + Database.deleteFormat(it.asMediaItem.mediaId) + } + manageDownload( + context = context, + mediaItem = it.asMediaItem, + downloadState = false + ) + } else { + SmartMessage(context.resources.getString(R.string.disliked_this_collection),type = PopupType.Error, context = context) + } + } + }, onLongClick = { SmartMessage(context.resources.getString(R.string.info_download_all_songs), context = context) } @@ -455,19 +467,27 @@ fun PlaylistSongList( .padding(horizontal = 5.dp) .combinedClickable( onClick = { - downloadState = Download.STATE_DOWNLOADING - if (playlistPage?.songs?.isNotEmpty() == true) - playlistPage?.songs?.forEach { - binder?.cache?.removeResource(it.asMediaItem.mediaId) - CoroutineScope(Dispatchers.IO).launch { - Database.deleteFormat( it.asMediaItem.mediaId ) - } - manageDownload( - context = context, - mediaItem = it.asMediaItem, - downloadState = true + if (playlistPage?.songs?.any { it.asMediaItem.mediaId !in dislikedSongs } == true) { + downloadState = Download.STATE_DOWNLOADING + if (playlistPage?.songs?.isNotEmpty() == true) + playlistPage?.songs?.forEach { + binder?.cache?.removeResource(it.asMediaItem.mediaId) + CoroutineScope(Dispatchers.IO).launch { + Database.deleteFormat(it.asMediaItem.mediaId) + } + manageDownload( + context = context, + mediaItem = it.asMediaItem, + downloadState = true + ) + } else { + SmartMessage( + context.resources.getString(R.string.disliked_this_collection), + type = PopupType.Error, + context = context ) } + } }, onLongClick = { SmartMessage(context.resources.getString(R.string.info_remove_all_downloaded_songs), context = context) @@ -479,15 +499,21 @@ fun PlaylistSongList( HeaderIconButton( icon = R.drawable.enqueue, - enabled = playlistPage?.songs?.isNotEmpty() == true, - color = if (playlistPage?.songs?.isNotEmpty() == true) colorPalette().text else colorPalette().textDisabled, + enabled = playlistPage?.songs?.any { it.asMediaItem.mediaId !in dislikedSongs } == true, + color = if (playlistPage?.songs?.any { it.asMediaItem.mediaId !in dislikedSongs } == true) colorPalette().text else colorPalette().textDisabled, onClick = {}, modifier = Modifier .padding(horizontal = 5.dp) .combinedClickable( onClick = { - playlistPage?.songs?.map(Innertube.SongItem::asMediaItem)?.let { mediaItems -> - binder?.player?.enqueue(mediaItems, context) + if (playlistPage?.songs?.any { it.asMediaItem.mediaId !in dislikedSongs } == true) { + playlistPage?.songs?.filter { it.asMediaItem.mediaId !in dislikedSongs } + ?.map(Innertube.SongItem::asMediaItem) + ?.let { mediaItems -> + binder?.player?.enqueue(mediaItems, context) + } + } else { + SmartMessage(context.resources.getString(R.string.disliked_this_collection),type = PopupType.Error, context = context) } }, onLongClick = { @@ -498,21 +524,23 @@ fun PlaylistSongList( HeaderIconButton( icon = R.drawable.shuffle, - enabled = playlistPage?.songs?.isNotEmpty() == true, - color = if (playlistPage?.songs?.isNotEmpty() ==true) colorPalette().text else colorPalette().textDisabled, + enabled = playlistPage?.songs?.any { it.asMediaItem.mediaId !in dislikedSongs } == true, + color = if (playlistPage?.songs?.any { it.asMediaItem.mediaId !in dislikedSongs } == true) colorPalette().text else colorPalette().textDisabled, onClick = {}, modifier = Modifier .padding(horizontal = 5.dp) .combinedClickable( onClick = { - if (playlistPage?.songs?.isNotEmpty() == true) { + if (playlistPage?.songs?.any { it.asMediaItem.mediaId !in dislikedSongs } == true) { binder?.stopRadio() - playlistPage?.songs?.shuffled()?.map(Innertube.SongItem::asMediaItem) + playlistPage?.songs?.filter{ it.asMediaItem.mediaId !in dislikedSongs }?.shuffled()?.map(Innertube.SongItem::asMediaItem) ?.let { binder?.player?.forcePlayFromBeginning( it ) } + } else { + SmartMessage(context.resources.getString(R.string.disliked_this_collection),type = PopupType.Error, context = context) } }, onLongClick = { @@ -523,24 +551,25 @@ fun PlaylistSongList( HeaderIconButton( icon = R.drawable.radio, - enabled = playlistPage?.songs?.isNotEmpty() == true, - color = colorPalette().text, + enabled = playlistPage?.songs?.any { it.asMediaItem.mediaId !in dislikedSongs } == true, + color = if (playlistPage?.songs?.any { it.asMediaItem.mediaId !in dislikedSongs } == true) colorPalette().text else colorPalette().textDisabled, onClick = {}, modifier = Modifier .padding(horizontal = 5.dp) .combinedClickable( onClick = { if (binder != null) { - binder.stopRadio() - binder.playRadio( - NavigationEndpoint.Endpoint.Watch( videoId = - if (binder.player.currentMediaItem?.mediaId != null) - binder.player.currentMediaItem?.mediaId - else playlistPage?.songs?.first()?.asMediaItem?.mediaId - ) - ) + if (playlistPage?.songs?.any { it.asMediaItem.mediaId !in dislikedSongs } == true) { + binder.stopRadio() + binder.playRadio( + NavigationEndpoint.Endpoint.Watch( videoId = + if (binder.player.currentMediaItem?.mediaId != null) + binder.player.currentMediaItem?.mediaId + else playlistPage?.songs?.first { it.asMediaItem.mediaId !in dislikedSongs }?.asMediaItem?.mediaId)) + } else { + SmartMessage(context.resources.getString(R.string.disliked_this_collection),type = PopupType.Error, context = context) + } } - }, onLongClick = { SmartMessage(context.resources.getString(R.string.info_start_radio), context = context) @@ -571,9 +600,9 @@ fun PlaylistSongList( playlistPreview.songCount.minus(1) ?: 0 if (position > 0) position++ else position = 0 - playlistPage!!.songs?.forEachIndexed { index, song -> + playlistPage!!.songs.forEachIndexed { index, song -> runCatching { - coroutineScope.launch(Dispatchers.IO) { + coroutineScope.launch(Dispatchers.IO) { Database.insert(song.asSong) Database.insert( SongPlaylistMap( @@ -812,13 +841,19 @@ fun PlaylistSongList( hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) }, onClick = { - searching = false - filter = null - playlistPage?.songs?.map(Innertube.SongItem::asMediaItem) - ?.let { mediaItems -> - binder?.stopRadio() - binder?.player?.forcePlayAtIndex(mediaItems, index) - } + if (song.asMediaItem.mediaId !in dislikedSongs) { + searching = false + filter = null + playlistPage?.songs?.filter { it.asMediaItem.mediaId !in dislikedSongs } + ?.map(Innertube.SongItem::asMediaItem) + ?.let { mediaItems -> + binder?.stopRadio() + binder?.player?.forcePlayAtIndex( + mediaItems, + mediaItems.indexOf(song.asMediaItem) + ) + } + } else {SmartMessage(context.resources.getString(R.string.disliked_this_song),type = PopupType.Error, context = context)} } ), disableScrollingText = disableScrollingText, @@ -855,13 +890,16 @@ fun PlaylistSongList( lazyListState = lazyListState, iconId = R.drawable.shuffle, onClick = { - playlistPage?.songs?.let { songs -> - if (songs.isNotEmpty()) { - binder?.stopRadio() - binder?.player?.forcePlayFromBeginning( - songs.shuffled().map(Innertube.SongItem::asMediaItem) - ) - } + if (playlistPage?.songs?.any { it.asMediaItem.mediaId !in dislikedSongs } == true) { + binder?.stopRadio() + playlistPage?.songs?.filter{ it.asMediaItem.mediaId !in dislikedSongs }?.shuffled()?.map(Innertube.SongItem::asMediaItem) + ?.let { + binder?.player?.forcePlayFromBeginning( + it + ) + } + } else { + SmartMessage(context.resources.getString(R.string.disliked_this_collection),type = PopupType.Error, context = context) } } ) diff --git a/composeApp/src/androidMain/kotlin/it/fast4x/rimusic/ui/screens/settings/AppearanceSettings.kt b/composeApp/src/androidMain/kotlin/it/fast4x/rimusic/ui/screens/settings/AppearanceSettings.kt index aae26893bb..ae07a5fb3b 100644 --- a/composeApp/src/androidMain/kotlin/it/fast4x/rimusic/ui/screens/settings/AppearanceSettings.kt +++ b/composeApp/src/androidMain/kotlin/it/fast4x/rimusic/ui/screens/settings/AppearanceSettings.kt @@ -1591,19 +1591,18 @@ fun AppearanceSettings( isChecked = disableScrollingText, onCheckedChange = { disableScrollingText = it } ) - if (playerType == PlayerType.Essential) { - if (search.input.isBlank() || stringResource(R.string.disable_horizontal_swipe).contains( - search.input, - true - ) + + if (search.input.isBlank() || stringResource(if (playerType == PlayerType.Modern && !isLandscape) R.string.disable_horizontal_swipe else R.string.disable_vertical_swipe).contains( + search.input, + true + ) + ) + SwitchSettingEntry( + title = stringResource(if (playerType == PlayerType.Modern && !isLandscape) R.string.disable_vertical_swipe else R.string.disable_horizontal_swipe), + text = stringResource(if (playerType == PlayerType.Modern && !isLandscape) R.string.disable_vertical_swipe_secondary else R.string.disable_song_switching_via_swipe), + isChecked = disablePlayerHorizontalSwipe, + onCheckedChange = { disablePlayerHorizontalSwipe = it } ) - SwitchSettingEntry( - title = stringResource(R.string.disable_horizontal_swipe), - text = stringResource(R.string.disable_song_switching_via_swipe), - isChecked = disablePlayerHorizontalSwipe, - onCheckedChange = { disablePlayerHorizontalSwipe = it } - ) - } if (search.input.isBlank() || stringResource(R.string.player_rotating_buttons).contains( search.input, diff --git a/composeApp/src/androidMain/kotlin/it/fast4x/rimusic/utils/GetControlsType.kt b/composeApp/src/androidMain/kotlin/it/fast4x/rimusic/utils/GetControlsType.kt index 15d21119e8..d8fbf766f8 100644 --- a/composeApp/src/androidMain/kotlin/it/fast4x/rimusic/utils/GetControlsType.kt +++ b/composeApp/src/androidMain/kotlin/it/fast4x/rimusic/utils/GetControlsType.kt @@ -95,7 +95,6 @@ fun GetControls( likedAt = likedAt, mediaId = mediaId, playerPlayButtonType = playerPlayButtonType, - rotationAngle = rotationAngle, isGradientBackgroundEnabled = isGradientBackgroundEnabled, onShowSpeedPlayerDialog = { showSpeedPlayerDialog = true } ) @@ -107,7 +106,6 @@ fun GetControls( playbackSpeed = playbackSpeed, shouldBePlaying = shouldBePlaying, playerPlayButtonType = playerPlayButtonType, - rotationAngle = rotationAngle, isGradientBackgroundEnabled = isGradientBackgroundEnabled, onShowSpeedPlayerDialog = { showSpeedPlayerDialog = true } ) diff --git a/composeApp/src/androidMain/kotlin/it/fast4x/rimusic/utils/Preferences.kt b/composeApp/src/androidMain/kotlin/it/fast4x/rimusic/utils/Preferences.kt index ce8d2bf186..fad11df386 100644 --- a/composeApp/src/androidMain/kotlin/it/fast4x/rimusic/utils/Preferences.kt +++ b/composeApp/src/androidMain/kotlin/it/fast4x/rimusic/utils/Preferences.kt @@ -347,6 +347,7 @@ const val ytAccountNameKey = "ytAccountName" const val ytAccountEmailKey = "ytAccountEmail" const val albumCoverRotationKey = "albumCoverRotation" const val isConnectionMeteredEnabledKey = "isConnectionMeteredEnabled" +const val landscapeControlsKey = "landscapeControls" /* @PublishedApi diff --git a/composeApp/src/androidMain/kotlin/it/fast4x/rimusic/utils/Utils.kt b/composeApp/src/androidMain/kotlin/it/fast4x/rimusic/utils/Utils.kt index 964dff58d0..65a58f8951 100644 --- a/composeApp/src/androidMain/kotlin/it/fast4x/rimusic/utils/Utils.kt +++ b/composeApp/src/androidMain/kotlin/it/fast4x/rimusic/utils/Utils.kt @@ -24,8 +24,11 @@ import io.ktor.client.HttpClient import io.ktor.client.plugins.UserAgent import it.fast4x.innertube.Innertube import it.fast4x.innertube.models.bodies.ContinuationBody +import it.fast4x.innertube.models.bodies.SearchBody import it.fast4x.innertube.requests.playlistPage +import it.fast4x.innertube.requests.searchPage import it.fast4x.innertube.utils.ProxyPreferences +import it.fast4x.innertube.utils.from import it.fast4x.innertube.utils.getProxy import it.fast4x.rimusic.Database import it.fast4x.rimusic.EXPLICIT_PREFIX @@ -33,6 +36,7 @@ import it.fast4x.rimusic.cleanPrefix import it.fast4x.rimusic.models.Album import it.fast4x.rimusic.models.Song import it.fast4x.rimusic.models.SongEntity +import it.fast4x.rimusic.models.SongPlaylistMap import it.fast4x.rimusic.service.LOCAL_KEY_PREFIX import it.fast4x.rimusic.service.isLocal import it.fast4x.rimusic.ui.components.themed.NewVersionDialog @@ -42,6 +46,7 @@ import java.time.Duration import java.util.Calendar import java.util.Date import java.util.GregorianCalendar +import kotlin.math.absoluteValue import kotlin.time.Duration.Companion.minutes const val EXPLICIT_BUNDLE_TAG = "is_explicit" @@ -623,3 +628,73 @@ fun Modifier.conditional(condition : Boolean, modifier : Modifier.() -> Modifier } } +suspend fun getAlbumVersionFromVideo(song: Song,playlistId : Long, position : Int){ + val explicit = if (song.asMediaItem.isExplicit) " explicit" else "" + val isExtPlaylist = (song.thumbnailUrl == "") && (song.durationText != "") + var songNotFound: Song + fun filteredText(text : String): String{ + val filteredText = text + .lowercase() + .replace("(", " ") + .replace(")", " ") + .replace("-", " ") + .replace("lyrics", "") + .replace("vevo", "") + .replace(" hd", "") + .replace("official video", "") + .replace(Regex("\\s+"), " ") + .filter {it.isLetterOrDigit() || it.isWhitespace() || it == '\'' || it == ',' } + return filteredText + } + + val searchQuery = Innertube.searchPage( + body = SearchBody( + query = filteredText("${cleanPrefix(song.title)} ${song.artistsText}$explicit"), + params = Innertube.SearchFilter.Song.value + ), + fromMusicShelfRendererContent = Innertube.SongItem.Companion::from + ) + + val searchResults = searchQuery?.getOrNull()?.items + val requiredSong = searchResults?.getOrNull(0) + + val sourceSongWords = filteredText(cleanPrefix(song.title)) + .split(" ").filter { it.isNotEmpty() } + val requiredSongWords = filteredText(cleanPrefix(requiredSong?.title ?: "")) + .split(" ").filter { it.isNotEmpty() } + + val songMatched = (requiredSong != null) && (requiredSongWords.any { it in sourceSongWords }) && + if (isExtPlaylist) { + (durationTextToMillis(requiredSong.durationText ?: "") - durationTextToMillis(song.durationText ?: "")).absoluteValue <= 2000 + } else {true} + + Database.asyncTransaction { + if (songMatched) { + deleteSongFromPlaylist(song.id, playlistId) + if (requiredSong != null) { + Database.insert(requiredSong.asSong) + } + if (requiredSong != null) { + insert( + SongPlaylistMap( + songId = requiredSong.asMediaItem.mediaId, + playlistId = playlistId, + position = position + ) + ) + } + } else if (isExtPlaylist && (song.id == ((cleanPrefix(song.title)+song.artistsText).filter {it.isLetterOrDigit()}))){ + songNotFound = song.copy(id = (song.artistsText+cleanPrefix(song.title)).filter{it.isLetterOrDigit()}) + Database.delete(song) + Database.insert(songNotFound) + Database.insert( + SongPlaylistMap( + songId = songNotFound.id, + playlistId = playlistId, + position = position + ) + ) + } + } +} + diff --git a/composeApp/src/androidMain/kotlin/it/fast4x/rimusic/utils/YoutubeRadio.kt b/composeApp/src/androidMain/kotlin/it/fast4x/rimusic/utils/YoutubeRadio.kt index 4413048699..ea50328169 100644 --- a/composeApp/src/androidMain/kotlin/it/fast4x/rimusic/utils/YoutubeRadio.kt +++ b/composeApp/src/androidMain/kotlin/it/fast4x/rimusic/utils/YoutubeRadio.kt @@ -67,7 +67,7 @@ data class YouTubeRadio @OptIn(UnstableApi::class) constructor var mediaIdFound = false runBlocking { withContext(Dispatchers.Main) { - for (i in 0 until (binder?.player?.mediaItemCount ?: 0)) { + for (i in 0 until (binder?.player?.mediaItemCount ?: 0) - 1) { if (mediaId == binder?.player?.getMediaItemAt(i)?.mediaId) { mediaIdFound = true return@withContext @@ -87,9 +87,9 @@ data class YouTubeRadio @OptIn(UnstableApi::class) constructor withContext(Dispatchers.IO) { mediaItems?.forEach { val songInPlaylist = Database.songUsedInPlaylists(it.mediaId) - val songIsLiked = Database.songliked(it.mediaId) + val songIsLiked = (Database.getLikedAt(it.mediaId) !in listOf(-1L,null)) val sIQ = songsInQueue(it.mediaId) - if (songInPlaylist == 0 && songIsLiked == 0 && (it.mediaId != sIQ)) { + if (songInPlaylist == 0 && !songIsLiked && (it.mediaId != sIQ)) { listMediaItems.add(it) } } @@ -104,6 +104,12 @@ data class YouTubeRadio @OptIn(UnstableApi::class) constructor mediaItems = listMediaItems } + withContext(Dispatchers.IO) { + mediaItems = mediaItems?.filter { + (Database.getLikedAt(it.mediaId) != -1L) + }?.distinct() + } + return mediaItems ?: emptyList() } } diff --git a/composeApp/src/androidMain/res/values/strings.xml b/composeApp/src/androidMain/res/values/strings.xml index a7859ecdb1..50ff4db5cd 100644 --- a/composeApp/src/androidMain/res/values/strings.xml +++ b/composeApp/src/androidMain/res/values/strings.xml @@ -224,6 +224,8 @@ Listen on Piped Listen on Invidious Disable horizontal swipe + Disable song switching via Vertical swipe + It affects only when the Expanded Player is off Disable song switching via swipe Interface in use User interface @@ -870,4 +872,17 @@ Data saving decreases quality, use only if you have a connection with limited data Use YTM login only for browse To be used if the songs are not played when logged in + Cannot Perform this Action with the Disliked Songs + Removed from Dislike + Cannot Perform this Action when all the Songs are Disliked + Toggle On-screen Controls + Delete Songs not in the Library + Deleting the Songs which are not in your Library. The App will Close After the Process is Over + Nothing to Delete + Match the Album/Audio Version of the Songs + No Songs Found to Match + Matching Songs. Please Wait. Do not Minimize or Exit the App + Unmatched Songs + Rematch this Song + Songs Not Found