diff --git a/.idea/.name b/.idea/.name deleted file mode 100644 index 9a4ea733..00000000 --- a/.idea/.name +++ /dev/null @@ -1 +0,0 @@ -Spowlo \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index fdbf5b9c..887e29f4 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -233,11 +233,8 @@ dependencies { ksp(libs.room.compiler) //spotDL library -// implementation(libs.spotdl.android.library) -// implementation(libs.spotdl.android.ffmpeg) - implementation("com.github.BobbyESP.spotdl_android:library:3000") - implementation("com.github.BobbyESP.spotdl_android:ffmpeg:3000") - implementation("com.github.BobbyESP.spotdl_android:common:0.3.0-alpha.f51af0d") + implementation(libs.spotdl.android.library) + implementation(libs.spotdl.android.ffmpeg) implementation(libs.spotify.api.android) diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/InitialEntry.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/InitialEntry.kt index 55ca54ad..aaa2bf85 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/InitialEntry.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/InitialEntry.kt @@ -56,7 +56,6 @@ import androidx.navigation.navArgument import androidx.navigation.navDeepLink import androidx.navigation.navOptions import androidx.navigation.navigation -import com.bobbyesp.library.SpotDL import com.bobbyesp.library.domain.UpdateStatus import com.bobbyesp.spowlo.App import com.bobbyesp.spowlo.MainActivity @@ -77,7 +76,7 @@ import com.bobbyesp.spowlo.ui.pages.download_tasks.FullscreenConsoleOutput import com.bobbyesp.spowlo.ui.pages.downloader.DownloaderPage import com.bobbyesp.spowlo.ui.pages.downloader.DownloaderViewModel import com.bobbyesp.spowlo.ui.pages.history.DownloadsHistoryPage -import com.bobbyesp.spowlo.ui.pages.metadata_viewer.playlists.PlaylistPage +import com.bobbyesp.spowlo.ui.pages.metadata_viewer.playlists.SpotifyItemPage import com.bobbyesp.spowlo.ui.pages.metadata_viewer.playlists.PlaylistPageViewModel import com.bobbyesp.spowlo.ui.pages.mod_downloader.ModsDownloaderPage import com.bobbyesp.spowlo.ui.pages.mod_downloader.ModsDownloaderViewModel @@ -458,7 +457,7 @@ fun InitialEntry( val type = backStackEntry.arguments?.getString("type") ?: "SOMETHING WENT WRONG" - PlaylistPage( + SpotifyItemPage( onBackPressed, id = id, type = type, diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/binders/SpotifyPageBinder.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/binders/SpotifyPageBinder.kt deleted file mode 100644 index c9707c17..00000000 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/binders/SpotifyPageBinder.kt +++ /dev/null @@ -1,65 +0,0 @@ -package com.bobbyesp.spowlo.ui.pages.metadata_viewer.binders - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import com.adamratzman.spotify.models.Album -import com.adamratzman.spotify.models.Artist -import com.adamratzman.spotify.models.Playlist -import com.adamratzman.spotify.models.Track -import com.bobbyesp.spowlo.features.spotify_api.model.SpotifyDataType -import com.bobbyesp.spowlo.ui.pages.metadata_viewer.pages.AlbumPage -import com.bobbyesp.spowlo.ui.pages.metadata_viewer.pages.ArtistPage -import com.bobbyesp.spowlo.ui.pages.metadata_viewer.pages.PlaylistViewPage -import com.bobbyesp.spowlo.ui.pages.metadata_viewer.pages.TrackPage - -@Composable -fun SpotifyPageBinder( - data: Any, - type: SpotifyDataType, - modifier: Modifier = Modifier, - trackDownloadCallback: (String, String) -> Unit, -) { - - LazyColumn(modifier = modifier, verticalArrangement = Arrangement.Top) { - when (type) { - SpotifyDataType.ALBUM -> { - val album = data as? Album - item { - album?.let { - AlbumPage(album, modifier, trackDownloadCallback) - } - } - } - - SpotifyDataType.ARTIST -> { - val artist = data as? Artist - item { - artist?.let { - ArtistPage(artist, modifier, trackDownloadCallback) - } - } - } - - SpotifyDataType.PLAYLIST -> { - val playlist = data as? Playlist - item { - playlist?.let { - PlaylistViewPage(playlist, modifier, trackDownloadCallback) - } - } - - } - - SpotifyDataType.TRACK -> { - val track = data as? Track - item { - track?.let { - TrackPage(track, modifier, trackDownloadCallback) - } - } - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/AlbumPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/AlbumPage.kt index 15b94aea..931d1006 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/AlbumPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/AlbumPage.kt @@ -3,6 +3,7 @@ package com.bobbyesp.spowlo.ui.pages.metadata_viewer.pages import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.aspectRatio @@ -11,6 +12,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Download @@ -44,66 +46,72 @@ fun AlbumPage( ) { val localConfig = LocalConfiguration.current - Column( + LazyColumn( modifier = modifier.fillMaxSize(), + contentPadding = PaddingValues(vertical = 8.dp, horizontal = 12.dp), verticalArrangement = Arrangement.Top ) { - Box( - modifier = Modifier - .clip(MaterialTheme.shapes.extraSmall) - .fillMaxWidth() - .padding(bottom = 6.dp), - contentAlignment = Alignment.Center - ) { - //calculate the image size based on the screen size and the aspect ratio as 1:1 (square) based on the height - val size = (localConfig.screenHeightDp / 3) - AsyncImageImpl( + item { + Box( modifier = Modifier - .size(size.dp) - .aspectRatio( - 1f, matchHeightConstraintsFirst = true - ) - .clip(MaterialTheme.shapes.small), - model = data.images[0].url, - contentDescription = stringResource(id = R.string.track_artwork), - contentScale = ContentScale.Crop, - ) - } - - Column( - modifier = Modifier - .fillMaxWidth() - .padding(start = 16.dp, end = 16.dp, top = 8.dp) - ) { - SelectionContainer { - MarqueeText( - text = data.name, - fontWeight = FontWeight.Bold, - style = MaterialTheme.typography.headlineMedium - ) - } - Spacer(modifier = Modifier.height(6.dp)) - SelectionContainer { - Text( - text = data.artists.joinToString(", ") { it.name }, - style = MaterialTheme.typography.bodyMedium.copy( - fontWeight = FontWeight.Bold - ), - modifier = Modifier.alpha(alpha = 0.8f) + .clip(MaterialTheme.shapes.extraSmall) + .fillMaxWidth() + .padding(bottom = 6.dp), + contentAlignment = Alignment.Center + ) { + //calculate the image size based on the screen size and the aspect ratio as 1:1 (square) based on the height + val size = (localConfig.screenHeightDp / 3) + AsyncImageImpl( + modifier = Modifier + .size(size.dp) + .aspectRatio( + 1f, matchHeightConstraintsFirst = true + ) + .clip(MaterialTheme.shapes.small), + model = data.images[0].url, + contentDescription = stringResource(id = R.string.track_artwork), + contentScale = ContentScale.Crop, ) } - Spacer(modifier = Modifier.height(6.dp)) - SelectionContainer { - Text( - text = dataStringToString( - data = data.type, additional = data.releaseDate.year.toString(), - ), - style = MaterialTheme.typography.bodySmall, - modifier = Modifier.alpha(alpha = 0.8f) - ) + } + item { + Column( + modifier = Modifier.padding(horizontal = 12.dp).padding(top = 6.dp), + ) { + SelectionContainer { + MarqueeText( + text = data.name, + fontWeight = FontWeight.Bold, + style = MaterialTheme.typography.headlineMedium + ) + } + Spacer(modifier = Modifier.height(6.dp)) + SelectionContainer { + Text( + text = data.artists.joinToString(", ") { it.name }, + style = MaterialTheme.typography.bodyMedium.copy( + fontWeight = FontWeight.Bold + ), + modifier = Modifier.alpha(alpha = 0.8f) + ) + } + Spacer(modifier = Modifier.height(6.dp)) + SelectionContainer { + Text( + text = dataStringToString( + data = data.type, additional = data.releaseDate.year.toString(), + ), + style = MaterialTheme.typography.bodySmall, + modifier = Modifier.alpha(alpha = 0.8f) + ) + } } + } + item { Spacer(modifier = Modifier.height(6.dp)) - if (data.externalUrls.spotify != null) { + } + if (data.externalUrls.spotify != null) { + item { Row( modifier = Modifier .fillMaxWidth() @@ -127,30 +135,32 @@ fun AlbumPage( } } + HorizontalDivider( + modifier = Modifier.padding( + horizontal = 16.dp, + vertical = 12.dp + ) + ) } - HorizontalDivider(modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp)) - if (data.tracks.size > 0) { - Column( - modifier = Modifier.fillMaxWidth() - ) { - data.tracks.items.forEach { track -> - val taskName = StringBuilder().append(track.name).append(" - ").append( - track?.artists?.joinToString(", ") { it.name }).toString() - TrackComponent( - contentModifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp), - songName = track.name, - artists = track.artists.joinToString(", ") { it.name }, - spotifyUrl = track.externalUrls.spotify ?: "", - isExplicit = track.explicit, - onClick = { - trackDownloadCallback( - track.externalUrls.spotify!!, - taskName - ) - } + } + if (data.tracks.size > 0) { + items(data.tracks.items.size) { index -> + val track = data.tracks.items[index] + val taskName = StringBuilder().append(track.name).append(" - ").append( + track.artists.joinToString(", ") { it.name }).toString() + TrackComponent( + contentModifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp), + songName = track.name, + artists = track.artists.joinToString(", ") { it.name }, + spotifyUrl = track.externalUrls.spotify ?: "", + isExplicit = track.explicit, + onClick = { + trackDownloadCallback( + track.externalUrls.spotify!!, + taskName ) } - } + ) } } } diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/ArtistPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/ArtistPage.kt index b373b52d..bf59333d 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/ArtistPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/ArtistPage.kt @@ -1,9 +1,9 @@ package com.bobbyesp.spowlo.ui.pages.metadata_viewer.pages -import android.util.Log import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize @@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.FilledTonalButton @@ -53,7 +54,6 @@ fun ArtistPage( modifier: Modifier, trackDownloadCallback: (String, String) -> Unit ) { - // NotImplementedPage() val localConfig = LocalConfiguration.current val topTracks = remember { mutableStateOf?>(null) } @@ -69,37 +69,35 @@ fun ArtistPage( } } - Column( + LazyColumn( modifier = modifier.fillMaxSize(), - verticalArrangement = Arrangement.Top + contentPadding = PaddingValues(vertical = 8.dp, horizontal = 12.dp), ) { - data.images.getOrNull(0)?.url?.let { imageUrl -> - Box( - modifier = Modifier - .clip(MaterialTheme.shapes.extraSmall) - .fillMaxWidth() - .padding(bottom = 6.dp), - contentAlignment = Alignment.Center - ) { - //calculate the image size based on the screen size and the aspect ratio as 1:1 (square) based on the height - val size = (localConfig.screenHeightDp / 3) - AsyncImageImpl( + item { + data.images.getOrNull(0)?.url?.let { imageUrl -> + Box( modifier = Modifier - .size(size.dp) - .aspectRatio(1f, matchHeightConstraintsFirst = true) - .clip(MaterialTheme.shapes.small), - model = imageUrl, - contentDescription = stringResource(id = R.string.track_artwork), - contentScale = ContentScale.Crop, - ) + .clip(MaterialTheme.shapes.extraSmall) + .fillMaxWidth() + .padding(bottom = 6.dp), + contentAlignment = Alignment.Center + ) { + //calculate the image size based on the screen size and the aspect ratio as 1:1 (square) based on the height + val size = (localConfig.screenHeightDp / 3) + AsyncImageImpl( + modifier = Modifier + .size(size.dp) + .aspectRatio(1f, matchHeightConstraintsFirst = true) + .clip(MaterialTheme.shapes.small), + model = imageUrl, + contentDescription = stringResource(id = R.string.track_artwork), + contentScale = ContentScale.Crop, + ) + } } } - Column( - modifier = Modifier - .fillMaxWidth() - .padding(start = 16.dp, end = 16.dp, top = 8.dp) - ) { + item { Column( verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally, @@ -134,16 +132,6 @@ fun ArtistPage( modifier = Modifier.alpha(alpha = 0.8f) ) } - /*Spacer(modifier = Modifier.height(6.dp)) - SelectionContainer { - Text( - text = "${stringResource(id = R.string.song_genres)}: ${data.genres.joinToString(", ")}", - style = MaterialTheme.typography.bodyMedium.copy( - fontWeight = FontWeight.Bold - ), - modifier = Modifier.alpha(alpha = 0.8f) - ) - }*/ FilledTonalButton( modifier = Modifier.padding(start = 8.dp), onClick = { ChromeCustomTabsUtil.openUrl(data.externalUrls.spotify!!) }, @@ -163,54 +151,35 @@ fun ArtistPage( ) } } - Spacer(modifier = Modifier.height(8.dp)) - SelectionContainer { - Text( - text = stringResource(id = R.string.artist_top_tracks), - style = MaterialTheme.typography.bodyLarge.copy( - fontWeight = FontWeight.Bold - ), - modifier = Modifier.alpha(alpha = 0.8f) - ) - } - HorizontalDivider(modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp)) - if (topTracks.value != null) { - Column( - modifier = Modifier.fillMaxWidth() - ) { - topTracks.value!!.forEach { track -> - val taskName = - "${track.name} - ${track.artists.joinToString(", ") { it.name }}" - TrackComponent( - contentModifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp), - songName = track.name, - artists = track.artists.joinToString(", ") { it.name }, - spotifyUrl = track.externalUrls.spotify ?: "", - isExplicit = track.explicit, - onClick = { - trackDownloadCallback(track.externalUrls.spotify ?: "", taskName) - } - ) - } - } - } } - } -} -suspend fun geArtistTopTracks(id: String): List { - try { - val tracksDeferred = withContext(Dispatchers.IO) { - async { - Log.d("SpotifyApiRequests", "providesGetArtistTopTracks($id)") - SpotifyApiRequests.providesGetArtistTopTracks(id) - } + item { + Text( + text = stringResource(id = R.string.artist_top_tracks), + style = MaterialTheme.typography.bodyLarge.copy( + fontWeight = FontWeight.Bold + ), + modifier = Modifier.alpha(alpha = 0.8f) + ) + HorizontalDivider(modifier = Modifier.padding(horizontal = 4.dp, vertical = 2.dp)) } - val tracks = tracksDeferred.await() ?: emptyList() - return tracks - } catch (e: Exception) { - Log.e("geArtistTopTracks", "Error fetching top tracks: $e") - return emptyList() + if (topTracks.value != null) { + items(topTracks.value!!.size) { index -> + val track = topTracks.value!![index] + val taskName = + "${track.name} - ${track.artists.joinToString(", ") { it.name }}" + TrackComponent( + contentModifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp), + songName = track.name, + artists = track.artists.joinToString(", ") { it.name }, + spotifyUrl = track.externalUrls.spotify ?: "", + isExplicit = track.explicit, + onClick = { + trackDownloadCallback(track.externalUrls.spotify ?: "", taskName) + } + ) + } + } } } \ No newline at end of file diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/PlaylistViewPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/PlaylistViewPage.kt index abf4fa8f..ed52710c 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/PlaylistViewPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/PlaylistViewPage.kt @@ -3,14 +3,14 @@ package com.bobbyesp.spowlo.ui.pages.metadata_viewer.pages import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Download @@ -26,7 +26,11 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.TextLinkStyles import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.fromHtml import androidx.compose.ui.unit.dp import com.adamratzman.spotify.models.Playlist import com.bobbyesp.spowlo.App @@ -45,74 +49,89 @@ fun PlaylistViewPage( ) { val localConfig = LocalConfiguration.current - Column( + LazyColumn( modifier = modifier.fillMaxSize(), - verticalArrangement = Arrangement.Top + contentPadding = PaddingValues(vertical = 8.dp, horizontal = 12.dp), ) { - Box( - modifier = Modifier - .clip(MaterialTheme.shapes.extraSmall) - .fillMaxWidth() - .padding(bottom = 6.dp), - contentAlignment = Alignment.Center - ) { - //calculate the image size based on the screen size and the aspect ratio as 1:1 (square) based on the height - val size = (localConfig.screenHeightDp / 3) - AsyncImageImpl( + item { + Box( modifier = Modifier - .size(size.dp) - .aspectRatio( - 1f, matchHeightConstraintsFirst = true - ) - .clip(MaterialTheme.shapes.small), - model = data.images[0].url, - contentDescription = stringResource(id = R.string.track_artwork), - contentScale = ContentScale.Crop, - ) - } - Column( - modifier = Modifier - .fillMaxWidth() - .padding(start = 16.dp, end = 16.dp, top = 8.dp) - ) { - SelectionContainer { - MarqueeText( - text = data.name, - fontWeight = FontWeight.Bold, - style = MaterialTheme.typography.headlineMedium - ) - } - Spacer(modifier = Modifier.height(6.dp)) - SelectionContainer { - Text( - text = data.owner.displayName ?: data.owner.id, - style = MaterialTheme.typography.bodyMedium.copy( - fontWeight = FontWeight.Bold - ), - modifier = Modifier.alpha(alpha = 0.8f) - ) - } - Spacer(modifier = Modifier.height(6.dp)) - SelectionContainer { - Text( - text = dataStringToString( - data = data.type, - additional = data.followers.total.toString() + " " + App.context.getString(R.string.followers) - .lowercase() - ), - style = MaterialTheme.typography.bodySmall, - modifier = Modifier.alpha(alpha = 0.8f) + .clip(MaterialTheme.shapes.extraSmall) + .fillMaxWidth() + .padding(bottom = 6.dp), + contentAlignment = Alignment.Center + ) { + //calculate the image size based on the screen size and the aspect ratio as 1:1 (square) based on the height + val size = (localConfig.screenHeightDp / 3) + AsyncImageImpl( + modifier = Modifier + .size(size.dp) + .aspectRatio( + 1f, matchHeightConstraintsFirst = true + ) + .clip(MaterialTheme.shapes.small), + model = data.images[0].url, + contentDescription = stringResource(id = R.string.track_artwork), + contentScale = ContentScale.Crop, ) } - Spacer(modifier = Modifier.height(6.dp)) - SelectionContainer { - Text( - text = data.description ?: "", - style = MaterialTheme.typography.bodySmall, - modifier = Modifier.alpha(alpha = 0.8f) - ) + } + item { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 12.dp) + .padding(top = 6.dp), + verticalArrangement = Arrangement.spacedBy(6.dp) + ) { + SelectionContainer { + MarqueeText( + text = data.name, + fontWeight = FontWeight.Bold, + style = MaterialTheme.typography.headlineMedium + ) + } + SelectionContainer { + Text( + text = data.owner.displayName ?: data.owner.id, + style = MaterialTheme.typography.bodyMedium.copy( + fontWeight = FontWeight.Bold + ), + modifier = Modifier.alpha(alpha = 0.8f) + ) + } + SelectionContainer { + Text( + text = dataStringToString( + data = data.type, + additional = data.followers.total.toString() + " " + App.context.getString( + R.string.followers + ) + .lowercase() + ), + style = MaterialTheme.typography.bodySmall, + modifier = Modifier.alpha(alpha = 0.8f) + ) + } + SelectionContainer { + val annotatedString = AnnotatedString.fromHtml( + data.description ?: "", + linkStyles = TextLinkStyles( + style = SpanStyle( + color = MaterialTheme.colorScheme.primary, + ) + ) + ) + Text( + text = annotatedString, + style = MaterialTheme.typography.bodySmall, + modifier = Modifier.alpha(alpha = 0.8f) + ) + } } - if (data.externalUrls.spotify != null) { + } + if (data.externalUrls.spotify != null) { + item { Row( modifier = Modifier .fillMaxWidth() @@ -136,48 +155,42 @@ fun PlaylistViewPage( } } + HorizontalDivider(modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp)) } - HorizontalDivider(modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp)) - if (data.tracks.size > 0) { - Column( - modifier = Modifier.fillMaxWidth() - ) { - //for every track in the playlist, show the track name and the artist name - data.tracks.items.forEach { track -> - val actualTrack = track.track?.asTrack - val taskName = StringBuilder().append(actualTrack?.name).append(" - ") - .append(actualTrack?.artists?.joinToString(", ") { it.name }).toString() - TrackComponent( - contentModifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp), - songName = actualTrack?.name ?: App.context.getString(R.string.unknown), - artists = actualTrack?.artists?.joinToString(", ") { it.name } ?: "", - spotifyUrl = actualTrack?.externalUrls?.spotify ?: "", - isExplicit = actualTrack?.explicit ?: false, - isPlaylist = true, - imageUrl = actualTrack?.album?.images?.getOrNull(0)?.url ?: "", - onClick = { - if (actualTrack != null) { - trackDownloadCallback( - actualTrack.externalUrls.spotify!!, - taskName - ) - } - } - ) + } + if (data.tracks.size > 0) { + items(data.tracks.items.size) { index -> + val track = data.tracks.items[index] + val actualTrack = track.track?.asTrack + val taskName = StringBuilder().append(actualTrack?.name).append(" - ") + .append(actualTrack?.artists?.joinToString(", ") { it.name }).toString() + TrackComponent( + contentModifier = Modifier, + songName = actualTrack?.name ?: App.context.getString(R.string.unknown), + artists = actualTrack?.artists?.joinToString(", ") { it.name } ?: "", + spotifyUrl = actualTrack?.externalUrls?.spotify ?: "", + isExplicit = actualTrack?.explicit ?: false, + isPlaylist = true, + imageUrl = actualTrack?.album?.images?.getOrNull(0)?.url ?: "", + onClick = { + if (actualTrack != null) { + trackDownloadCallback( + actualTrack.externalUrls.spotify!!, + taskName + ) + } } - } + ) } } - } -} -/*@ExperimentalSerializationApi -object PlaylistSaver : Saver { - override fun restore(value: String): Playlist? { - return Json.decodeFromString(value) - } - - override fun SaverScope.save(value: Playlist?): String { - return Json.encodeToString(value) + item { + androidx.compose.material3.HorizontalDivider() + Text( + text = stringResource(id = R.string.uncomplete_playlist), + style = MaterialTheme.typography.bodySmall, + modifier = Modifier.padding(12.dp) + ) + } } -}*/ \ No newline at end of file +} \ No newline at end of file diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/TrackPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/TrackPage.kt index 0cde7e98..94b0ba9c 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/TrackPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/TrackPage.kt @@ -3,6 +3,7 @@ package com.bobbyesp.spowlo.ui.pages.metadata_viewer.pages import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.aspectRatio @@ -12,6 +13,7 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -79,114 +81,122 @@ fun TrackPage( } } - Column( - modifier = modifier.fillMaxSize() + LazyColumn( + modifier = modifier.fillMaxSize(), + contentPadding = PaddingValues(vertical = 8.dp, horizontal = 12.dp), ) { - Box( - modifier = Modifier - .clip(MaterialTheme.shapes.extraSmall) - .fillMaxWidth() - .padding(bottom = 6.dp), contentAlignment = Alignment.Center - ) { - //calculate the image size based on the screen size and the aspect ratio as 1:1 (square) based on the height - val size = (localConfig.screenHeightDp / 3) - AsyncImageImpl( + item { + Box( modifier = Modifier - .size(size.dp) - .aspectRatio( - 1f, matchHeightConstraintsFirst = true - ) - .clip(MaterialTheme.shapes.small), - model = trackData.album.images[0].url, - contentDescription = stringResource(id = R.string.track_artwork), - contentScale = ContentScale.Crop, - ) - } - Column( - modifier = Modifier - .fillMaxWidth() - .padding(start = 16.dp, end = 16.dp, top = 8.dp) - ) { - SelectionContainer { - MarqueeText( - text = trackData.name, - fontWeight = FontWeight.Bold, - style = MaterialTheme.typography.headlineMedium - ) - } - Spacer(modifier = Modifier.height(6.dp)) - SelectionContainer { - Text( - text = trackData.artists.joinToString(", ") { it.name }, - style = MaterialTheme.typography.bodyMedium.copy( - fontWeight = FontWeight.Bold - ), - modifier = Modifier.alpha(alpha = 0.8f) - ) - } - Spacer(modifier = Modifier.height(6.dp)) - SelectionContainer { - Text( - text = dataStringToString( - data = trackData.type, - additional = trackData.album.releaseDate?.year.toString() - ), - style = MaterialTheme.typography.bodySmall, - modifier = Modifier.alpha(alpha = 0.8f) + .clip(MaterialTheme.shapes.extraSmall) + .fillMaxWidth() + .padding(bottom = 6.dp), contentAlignment = Alignment.Center + ) { + //calculate the image size based on the screen size and the aspect ratio as 1:1 (square) based on the height + val size = (localConfig.screenHeightDp / 3) + AsyncImageImpl( + modifier = Modifier + .size(size.dp) + .aspectRatio( + 1f, matchHeightConstraintsFirst = true + ) + .clip(MaterialTheme.shapes.small), + model = trackData.album.images[0].url, + contentDescription = stringResource(id = R.string.track_artwork), + contentScale = ContentScale.Crop, ) } } - HorizontalDivider(modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp)) - - Column( - modifier = Modifier.fillMaxWidth() - ) { - val taskName = StringBuilder().append(trackData.name).append(" - ") - .append(trackData.artists.joinToString(", ") { it.name }).toString() - TrackComponent(contentModifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp), - songName = trackData.name, - artists = trackData.artists.joinToString(", ") { it.name }, - spotifyUrl = trackData.externalUrls.spotify!!, - isExplicit = trackData.explicit, - onClick = { trackDownloadCallback(trackData.externalUrls.spotify!!, taskName) }) - } - Spacer(modifier = Modifier.padding(vertical = 8.dp)) - Column(modifier = Modifier.fillMaxWidth()) { - Row( + item { + Column( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp) - ) { - ExtraInfoCard( - headlineText = stringResource(id = R.string.track_popularity), - bodyText = trackData.popularity.toString(), - modifier = Modifier.weight(1f) - ) - Spacer(modifier = Modifier.width(16.dp)) - ExtraInfoCard( - headlineText = stringResource(id = R.string.track_duration), - bodyText = GeneralTextUtils.convertDuration(trackData.durationMs.toDouble()), - modifier = Modifier.weight(1f) - ) + .padding(horizontal = 8.dp), + ) { + SelectionContainer { + MarqueeText( + text = trackData.name, + fontWeight = FontWeight.Bold, + style = MaterialTheme.typography.headlineMedium + ) + } + Spacer(modifier = Modifier.height(6.dp)) + SelectionContainer { + Text( + text = trackData.artists.joinToString(", ") { it.name }, + style = MaterialTheme.typography.bodyMedium.copy( + fontWeight = FontWeight.Bold + ), + modifier = Modifier.alpha(alpha = 0.8f) + ) + } + Spacer(modifier = Modifier.height(6.dp)) + SelectionContainer { + Text( + text = dataStringToString( + data = trackData.type, + additional = trackData.album.releaseDate?.year.toString() + ), + style = MaterialTheme.typography.bodySmall, + modifier = Modifier.alpha(alpha = 0.8f) + ) + } } - AnimatedVisibility(visible = audioFeatures != null) { + HorizontalDivider(modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp)) + } + + item { + val songData = trackData + val taskName = StringBuilder().append(songData.name).append(" - ") + .append(songData.artists.joinToString(", ") { it.name }).toString() + + TrackComponent( + contentModifier = Modifier, + songName = songData.name, + artists = songData.artists.joinToString(", ") { it.name }, + spotifyUrl = songData.externalUrls.spotify!!, + isExplicit = songData.explicit, + onClick = { trackDownloadCallback(songData.externalUrls.spotify!!, taskName) }) + } + + item { + Column(modifier = Modifier.fillMaxWidth()) { Row( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 8.dp) + .padding(horizontal = 16.dp) ) { ExtraInfoCard( - headlineText = stringResource(id = R.string.loudness), - bodyText = audioFeatures!!.loudness.toString(), + headlineText = stringResource(id = R.string.track_popularity), + bodyText = trackData.popularity.toString(), modifier = Modifier.weight(1f) ) Spacer(modifier = Modifier.width(16.dp)) ExtraInfoCard( - headlineText = stringResource(id = R.string.tempo), - bodyText = audioFeatures!!.tempo.toString() + " BPM", + headlineText = stringResource(id = R.string.track_duration), + bodyText = GeneralTextUtils.convertDuration(trackData.durationMs.toDouble()), modifier = Modifier.weight(1f) ) } + AnimatedVisibility(visible = audioFeatures != null) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp) + ) { + ExtraInfoCard( + headlineText = stringResource(id = R.string.loudness), + bodyText = audioFeatures!!.loudness.toString(), + modifier = Modifier.weight(1f) + ) + Spacer(modifier = Modifier.width(16.dp)) + ExtraInfoCard( + headlineText = stringResource(id = R.string.tempo), + bodyText = audioFeatures!!.tempo.toString() + " BPM", + modifier = Modifier.weight(1f) + ) + } + } } } } diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/playlists/PlaylistPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/playlists/PlaylistPage.kt deleted file mode 100644 index 8cb0618d..00000000 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/playlists/PlaylistPage.kt +++ /dev/null @@ -1,92 +0,0 @@ -package com.bobbyesp.spowlo.ui.pages.metadata_viewer.playlists - -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.sp -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.lifecycle.viewmodel.compose.viewModel -import com.bobbyesp.spowlo.R -import com.bobbyesp.spowlo.ui.components.BackButton -import com.bobbyesp.spowlo.ui.pages.common_pages.LoadingPage -import com.bobbyesp.spowlo.ui.pages.metadata_viewer.binders.SpotifyPageBinder -import com.bobbyesp.spowlo.ui.pages.metadata_viewer.binders.typeOfSpotifyDataType - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun PlaylistPage( - onBackPressed: () -> Unit, - playlistPageViewModel: PlaylistPageViewModel = viewModel(), - id: String, - type: String, -) { - val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior() - - val viewState by playlistPageViewModel.viewStateFlow.collectAsStateWithLifecycle() - - LaunchedEffect(id) { - playlistPageViewModel.loadData(id, typeOfSpotifyDataType(type)) - } - - with(viewState) { - when (this.state) { - is PlaylistDataState.Loading -> { - LoadingPage() - } - - is PlaylistDataState.Error -> { - Text(text = this.state.error.message.toString()) - } - - is PlaylistDataState.Loaded -> { - Scaffold(modifier = Modifier - .fillMaxSize() - .nestedScroll(scrollBehavior.nestedScrollConnection), topBar = { - TopAppBar(title = { - Text( - text = stringResource(id = R.string.metadata_viewer), - style = MaterialTheme.typography.titleMedium.copy(fontSize = 18.sp) - ) - }, navigationIcon = { - BackButton { onBackPressed() } - }, actions = {}, scrollBehavior = scrollBehavior - ) - }) { paddings -> - val stateData by remember { - mutableStateOf(this.state.data) - } - SpotifyPageBinder( - data = stateData, - type = typeOfSpotifyDataType(type), - modifier = Modifier - .fillMaxSize() - .padding(top = paddings.calculateTopPadding()), - trackDownloadCallback = { url, name -> - playlistPageViewModel.downloadTrack(url, name) - }, - ) - - } - } - } - } -} - -sealed class PlaylistDataState { - object Loading : PlaylistDataState() - class Error(val error: Exception) : PlaylistDataState() - class Loaded(val data: Any) : PlaylistDataState() -} \ No newline at end of file diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/playlists/SpotifyItemPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/playlists/SpotifyItemPage.kt new file mode 100644 index 00000000..a7d2242c --- /dev/null +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/playlists/SpotifyItemPage.kt @@ -0,0 +1,132 @@ +package com.bobbyesp.spowlo.ui.pages.metadata_viewer.playlists + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.res.stringResource +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.viewmodel.compose.viewModel +import com.adamratzman.spotify.models.Album +import com.adamratzman.spotify.models.Artist +import com.adamratzman.spotify.models.Playlist +import com.adamratzman.spotify.models.Track +import com.bobbyesp.spowlo.R +import com.bobbyesp.spowlo.features.spotify_api.model.SpotifyDataType +import com.bobbyesp.spowlo.ui.components.BackButton +import com.bobbyesp.spowlo.ui.pages.common_pages.LoadingPage +import com.bobbyesp.spowlo.ui.pages.metadata_viewer.binders.typeOfSpotifyDataType +import com.bobbyesp.spowlo.ui.pages.metadata_viewer.pages.AlbumPage +import com.bobbyesp.spowlo.ui.pages.metadata_viewer.pages.ArtistPage +import com.bobbyesp.spowlo.ui.pages.metadata_viewer.pages.PlaylistViewPage +import com.bobbyesp.spowlo.ui.pages.metadata_viewer.pages.TrackPage + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SpotifyItemPage( + onBackPressed: () -> Unit, + playlistPageViewModel: PlaylistPageViewModel = viewModel(), + id: String, + type: String, +) { + val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior() + + val viewState by playlistPageViewModel.viewStateFlow.collectAsStateWithLifecycle() + + LaunchedEffect(id) { + playlistPageViewModel.loadData(id, typeOfSpotifyDataType(type)) + } + + with(viewState) { + when (this.state) { + is PlaylistDataState.Loading -> { + LoadingPage() + } + + is PlaylistDataState.Error -> { + Text(text = this.state.error.message.toString()) + } + + is PlaylistDataState.Loaded -> { + val stateData by remember { + mutableStateOf(this.state.data) + } + + Scaffold( + modifier = Modifier + .fillMaxSize() + .nestedScroll(scrollBehavior.nestedScrollConnection), + topBar = { + TopAppBar( + title = { + Text( + text = stringResource(id = R.string.metadata_viewer), + style = MaterialTheme.typography.titleMedium + ) + }, navigationIcon = { + BackButton { onBackPressed() } + }, actions = {}, scrollBehavior = scrollBehavior + ) + } + ) { paddingValues -> + val pageModifier = Modifier + .fillMaxSize() + .padding(paddingValues) + + val trackDownloadCallback: (url: String, name: String) -> Unit = { url, name -> + playlistPageViewModel.downloadTrack(url, name) + } + + with(stateData) { + when (typeOfSpotifyDataType(type)) { + SpotifyDataType.ALBUM -> { + val album = this as? Album + album?.let { + AlbumPage(album, pageModifier, trackDownloadCallback) + } + } + + SpotifyDataType.ARTIST -> { + val artist = this as? Artist + artist?.let { + ArtistPage(artist, pageModifier, trackDownloadCallback) + } + } + + SpotifyDataType.PLAYLIST -> { + val playlist = this as? Playlist + playlist?.let { + PlaylistViewPage(playlist, pageModifier, trackDownloadCallback) + } + } + + SpotifyDataType.TRACK -> { + val track = this as? Track + track?.let { + TrackPage(track, pageModifier, trackDownloadCallback) + } + } + } + } + } + } + } + } +} + +sealed class PlaylistDataState { + data object Loading : PlaylistDataState() + class Error(val error: Exception) : PlaylistDataState() + class Loaded(val data: Any) : PlaylistDataState() +} \ No newline at end of file diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/searcher/SearcherPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/searcher/SearcherPage.kt index f03e42f5..79ab6c11 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/searcher/SearcherPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/searcher/SearcherPage.kt @@ -320,7 +320,7 @@ fun ResultsList( val item = paginatedItems[index] as T SearchingResult( modifier = Modifier.fillMaxWidth(), - insideModifier = Modifier.padding(vertical = 8.dp), + insideModifier = Modifier.padding(vertical = 4.dp), name = itemName(item), artists = itemArtists(item), artworkUrl = itemArtworkUrl(item), diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 00950ac9..b4d86ca1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -369,4 +369,5 @@ Popularity Top Tracks Top Albums + Right now Spowlo may not show all songs due to the lack of pagination, but when trying to download the full playlist, all the songs will be downloaded. This is already fixed in the upcoming Spowlo 2.0.0 \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7fa03eab..468dc6d7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -22,7 +22,7 @@ androidxHiltNavigationCompose = "1.2.0" androidxTestExt = "1.2.1" -spotdlAndroidVersion = "0.2.1" +spotdlAndroidVersion = "0.3.0-alpha.03b1b47" spotifyApiKotlinVersion = "4.0.2" crashHandlerVersion = "2.0.2" @@ -110,8 +110,8 @@ kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx- kotlinx-datetime = { group = "org.jetbrains.kotlinx", name = "kotlinx-datetime", version.ref = "kotlinxDatetime" } kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } -spotdl-android-library = { group = "com.github.bobbyesp.spotdl-android", name = "spotdl-android-library", version.ref = "spotdlAndroidVersion" } -spotdl-android-ffmpeg = { group = "com.github.bobbyesp.spotdl-android", name = "spotdl-android-ffmpeg", version.ref = "spotdlAndroidVersion" } +spotdl-android-library = { group = "com.github.BobbyESP.spotdl_android", name = "library", version.ref = "spotdlAndroidVersion" } +spotdl-android-ffmpeg = { group = "com.github.BobbyESP.spotdl_android", name = "ffmpeg", version.ref = "spotdlAndroidVersion" } spotify-api-android = { group = "com.adamratzman", name = "spotify-api-kotlin-core", version.ref = "spotifyApiKotlinVersion" }