diff --git a/app/src/main/java/com/devdunnapps/amplify/data/PlexAPI.kt b/app/src/main/java/com/devdunnapps/amplify/data/PlexAPI.kt index ac0d191..ca343a1 100644 --- a/app/src/main/java/com/devdunnapps/amplify/data/PlexAPI.kt +++ b/app/src/main/java/com/devdunnapps/amplify/data/PlexAPI.kt @@ -110,4 +110,14 @@ interface PlexAPI { @Query("title") title: String? = null, @Query("summary") summary: String? = null ): NetworkResponse + + @GET("library/sections/{section}/all?viewCount=1&type=8&sort=lastViewedAt:desc&limit=10") + suspend fun getRecentlyPlayedArtists( + @Path("section") section: String + ): NetworkResponse + + @GET("library/sections/{section}/all?type=9&sort=addedAt:desc&limit=10") + suspend fun getRecentlyAddedAlbums( + @Path("section") section: String + ): NetworkResponse } diff --git a/app/src/main/java/com/devdunnapps/amplify/data/PlexRepositoryImpl.kt b/app/src/main/java/com/devdunnapps/amplify/data/PlexRepositoryImpl.kt index bfe8d9e..1836673 100644 --- a/app/src/main/java/com/devdunnapps/amplify/data/PlexRepositoryImpl.kt +++ b/app/src/main/java/com/devdunnapps/amplify/data/PlexRepositoryImpl.kt @@ -199,4 +199,14 @@ class PlexRepositoryImpl @Inject constructor( summary: String? ): NetworkResponse = api.editPlaylistMetadata(playlistId = playlistId, title = title, summary = summary) + + override suspend fun getRecentlyPlayedArtists(): NetworkResponse> = + api.getRecentlyPlayedArtists(section).map { + it.mediaContainer.metadata?.map { artist -> artist.toArtist() }.orEmpty() + } + + override suspend fun getRecentlyAddedAlbums(): NetworkResponse> = + api.getRecentlyAddedAlbums(section).map { + it.mediaContainer.metadata?.map { album -> album.toAlbum() }.orEmpty() + } } diff --git a/app/src/main/java/com/devdunnapps/amplify/domain/repository/PlexRepository.kt b/app/src/main/java/com/devdunnapps/amplify/domain/repository/PlexRepository.kt index 12a4ef3..c2b027b 100644 --- a/app/src/main/java/com/devdunnapps/amplify/domain/repository/PlexRepository.kt +++ b/app/src/main/java/com/devdunnapps/amplify/domain/repository/PlexRepository.kt @@ -59,4 +59,8 @@ interface PlexRepository { suspend fun markSongAsListened(songId: String): NetworkResponse suspend fun editPlaylistMetadata(playlistId: String, title: String?, summary: String?): NetworkResponse + + suspend fun getRecentlyPlayedArtists(): NetworkResponse> + + suspend fun getRecentlyAddedAlbums(): NetworkResponse> } diff --git a/app/src/main/java/com/devdunnapps/amplify/ui/albums/AlbumsFragment.kt b/app/src/main/java/com/devdunnapps/amplify/ui/albums/AlbumsFragment.kt index 7bdb506..ea32292 100644 --- a/app/src/main/java/com/devdunnapps/amplify/ui/albums/AlbumsFragment.kt +++ b/app/src/main/java/com/devdunnapps/amplify/ui/albums/AlbumsFragment.kt @@ -30,6 +30,8 @@ import com.devdunnapps.amplify.ui.components.LoadingScreen import com.devdunnapps.amplify.ui.utils.FragmentRootDestinationScaffold import dagger.hilt.android.AndroidEntryPoint +private const val ARTWORK_SIZE = 100 + @AndroidEntryPoint class AlbumsFragment : Fragment() { @@ -74,7 +76,7 @@ private fun AlbumsScreen( LoadingScreen(modifier = modifier) LazyVerticalGrid( - columns = GridCells.Adaptive(100.dp), + columns = GridCells.Adaptive(ARTWORK_SIZE.dp), contentPadding = PaddingValues(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp), horizontalArrangement = Arrangement.spacedBy(16.dp), @@ -84,7 +86,8 @@ private fun AlbumsScreen( albums[index]?.let { AlbumCard( onClick = { onAlbumClick(it.id) }, - album = it + album = it, + artworkSize = ARTWORK_SIZE.dp ) } } diff --git a/app/src/main/java/com/devdunnapps/amplify/ui/artist/albums/ArtistAllAlbumsFragment.kt b/app/src/main/java/com/devdunnapps/amplify/ui/artist/albums/ArtistAllAlbumsFragment.kt index 21626b6..282d673 100644 --- a/app/src/main/java/com/devdunnapps/amplify/ui/artist/albums/ArtistAllAlbumsFragment.kt +++ b/app/src/main/java/com/devdunnapps/amplify/ui/artist/albums/ArtistAllAlbumsFragment.kt @@ -31,6 +31,8 @@ import com.devdunnapps.amplify.ui.utils.FragmentSubDestinationScaffold import com.devdunnapps.amplify.utils.Resource import dagger.hilt.android.AndroidEntryPoint +private const val ARTWORK_SIZE = 150 + @AndroidEntryPoint class ArtistAllAlbumsFragment : Fragment() { @@ -85,7 +87,7 @@ private fun ArtistAllAlbumsScreen( @Composable private fun ArtistAllAlbumsList(albums: List, onAlbumClick: (String) -> Unit, modifier: Modifier = Modifier) { LazyVerticalGrid( - columns = GridCells.Adaptive(150.dp), + columns = GridCells.Adaptive(ARTWORK_SIZE.dp), contentPadding = PaddingValues(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp), horizontalArrangement = Arrangement.spacedBy(16.dp), @@ -94,6 +96,7 @@ private fun ArtistAllAlbumsList(albums: List, onAlbumClick: (String) -> U items(albums) { AlbumCard( onClick = { onAlbumClick(it.id) }, + artworkSize = ARTWORK_SIZE.dp, album = it ) } diff --git a/app/src/main/java/com/devdunnapps/amplify/ui/artists/ArtistsFragment.kt b/app/src/main/java/com/devdunnapps/amplify/ui/artists/ArtistsFragment.kt index 8d14059..cd08524 100644 --- a/app/src/main/java/com/devdunnapps/amplify/ui/artists/ArtistsFragment.kt +++ b/app/src/main/java/com/devdunnapps/amplify/ui/artists/ArtistsFragment.kt @@ -30,6 +30,8 @@ import com.devdunnapps.amplify.ui.components.LoadingScreen import com.devdunnapps.amplify.ui.utils.FragmentRootDestinationScaffold import dagger.hilt.android.AndroidEntryPoint +private const val ARTWORK_SIZE = 100 + @AndroidEntryPoint class ArtistsFragment : Fragment() { @@ -74,7 +76,7 @@ private fun ArtistsScreen( LoadingScreen(modifier = modifier) LazyVerticalGrid( - columns = GridCells.Adaptive(100.dp), + columns = GridCells.Adaptive(ARTWORK_SIZE.dp), contentPadding = PaddingValues(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp), horizontalArrangement = Arrangement.spacedBy(16.dp), @@ -84,7 +86,8 @@ private fun ArtistsScreen( artists[index]?.let { ArtistCard( onClick = { onArtistClick(it.id) }, - artist = it + artist = it, + artworkSize = ARTWORK_SIZE.dp ) } } diff --git a/app/src/main/java/com/devdunnapps/amplify/ui/components/AlbumCard.kt b/app/src/main/java/com/devdunnapps/amplify/ui/components/AlbumCard.kt index 85fb223..c99ca2a 100644 --- a/app/src/main/java/com/devdunnapps/amplify/ui/components/AlbumCard.kt +++ b/app/src/main/java/com/devdunnapps/amplify/ui/components/AlbumCard.kt @@ -3,7 +3,11 @@ package com.devdunnapps.amplify.ui.components import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -16,6 +20,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import coil.compose.AsyncImage import com.devdunnapps.amplify.R @@ -24,15 +29,23 @@ import com.devdunnapps.amplify.utils.PlexUtils import com.google.accompanist.themeadapter.material3.Mdc3Theme @Composable -fun AlbumCard(onClick: () -> Unit, album: Album, modifier: Modifier = Modifier) { +fun AlbumCard( + onClick: () -> Unit, + album: Album, + artworkSize: Dp, + modifier: Modifier = Modifier +) { Column( - modifier = modifier.clickable { onClick() }, + modifier = modifier + .width(IntrinsicSize.Min) + .clickable { onClick() }, verticalArrangement = Arrangement.spacedBy(4.dp) ) { val context = LocalContext.current val imageUrl = remember { PlexUtils.getInstance(context).getSizedImage(album.thumb, 300, 300) } AsyncImage( modifier = Modifier + .size(artworkSize) .aspectRatio(1f) .clip(RoundedCornerShape(6.dp)), model = imageUrl, @@ -42,7 +55,7 @@ fun AlbumCard(onClick: () -> Unit, album: Album, modifier: Modifier = Modifier) contentScale = ContentScale.Crop ) - Column { + Column(modifier = Modifier.fillMaxWidth()) { Text( text = album.title, style = MaterialTheme.typography.bodyMedium, @@ -77,7 +90,8 @@ fun AlbumCardPreview() { year = "", artistName = "", studio = "" - ) + ), + artworkSize = 100.dp ) } } diff --git a/app/src/main/java/com/devdunnapps/amplify/ui/components/ArtistCard.kt b/app/src/main/java/com/devdunnapps/amplify/ui/components/ArtistCard.kt index b699403..21ce18d 100644 --- a/app/src/main/java/com/devdunnapps/amplify/ui/components/ArtistCard.kt +++ b/app/src/main/java/com/devdunnapps/amplify/ui/components/ArtistCard.kt @@ -3,7 +3,11 @@ package com.devdunnapps.amplify.ui.components import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.AccountCircle @@ -21,6 +25,7 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import coil.compose.AsyncImage import com.devdunnapps.amplify.R @@ -29,9 +34,16 @@ import com.devdunnapps.amplify.utils.PlexUtils import com.google.accompanist.themeadapter.material3.Mdc3Theme @Composable -fun ArtistCard(onClick: () -> Unit, artist: Artist, modifier: Modifier = Modifier) { +fun ArtistCard( + artist: Artist, + artworkSize: Dp, + onClick: () -> Unit, + modifier: Modifier = Modifier +) { Column( - modifier = modifier.clickable { onClick() }, + modifier = modifier + .width(IntrinsicSize.Min) + .clickable { onClick() }, verticalArrangement = Arrangement.spacedBy(4.dp), horizontalAlignment = Alignment.CenterHorizontally ) { @@ -39,6 +51,7 @@ fun ArtistCard(onClick: () -> Unit, artist: Artist, modifier: Modifier = Modifie val imageUrl = remember { PlexUtils.getInstance(context).getSizedImage(artist.thumb, 500, 500) } AsyncImage( modifier = Modifier + .size(artworkSize) .aspectRatio(1f) .clip(CircleShape), model = imageUrl, @@ -53,7 +66,8 @@ fun ArtistCard(onClick: () -> Unit, artist: Artist, modifier: Modifier = Modifie textAlign = TextAlign.Center, style = MaterialTheme.typography.bodySmall, maxLines = 1, - overflow = TextOverflow.Ellipsis + overflow = TextOverflow.Ellipsis, + modifier = Modifier.fillMaxWidth() ) } } @@ -70,7 +84,8 @@ fun ArtistCardPreview() { thumb = "/library/metadata/45209/thumb/1641184622", art = null, bio = "" - ) + ), + artworkSize = 100.dp ) } } diff --git a/app/src/main/java/com/devdunnapps/amplify/ui/home/HomeFragment.kt b/app/src/main/java/com/devdunnapps/amplify/ui/home/HomeFragment.kt new file mode 100644 index 0000000..d076a38 --- /dev/null +++ b/app/src/main/java/com/devdunnapps/amplify/ui/home/HomeFragment.kt @@ -0,0 +1,148 @@ +package com.devdunnapps.amplify.ui.home + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.navigation.fragment.findNavController +import com.devdunnapps.amplify.MobileNavigationDirections +import com.devdunnapps.amplify.R +import com.devdunnapps.amplify.ui.components.AlbumCard +import com.devdunnapps.amplify.ui.components.ArtistCard +import com.devdunnapps.amplify.ui.components.Carousel +import com.devdunnapps.amplify.ui.components.ErrorScreen +import com.devdunnapps.amplify.ui.components.LoadingScreen +import com.devdunnapps.amplify.ui.utils.FragmentRootDestinationScaffold +import com.devdunnapps.amplify.utils.Resource +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class HomeFragment : Fragment() { + + private val viewModel: HomeViewModel by viewModels() + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View = ComposeView(requireContext()).apply { + setContent { + FragmentRootDestinationScaffold( + screenTitle = stringResource(R.string.home) + ) { paddingValues -> + HomeRoute( + viewModel = viewModel, + modifier = Modifier.padding(top = paddingValues.calculateTopPadding()), + navigateToArtist = { artistId -> + val directions = + MobileNavigationDirections.actionGlobalNavigationArtist(artistId) + findNavController().navigate(directions) + }, + navigateToAlbum = { albumId -> + val directions = + MobileNavigationDirections.actionGlobalNavigationAlbum(albumId) + findNavController().navigate(directions) + } + ) + } + } + } +} + +@Composable +private fun HomeRoute( + modifier: Modifier = Modifier, + viewModel: HomeViewModel = hiltViewModel(), + navigateToArtist: (String) -> Unit, + navigateToAlbum: (String) -> Unit +) { + val homeState = viewModel.uiState.collectAsState().value + HomeScreen( + homeState = homeState, + modifier = modifier, + navigateToArtist = navigateToArtist, + navigateToAlbum = navigateToAlbum + ) +} + +@Composable +private fun HomeScreen( + homeState: Resource, + modifier: Modifier = Modifier, + navigateToArtist: (String) -> Unit, + navigateToAlbum: (String) -> Unit +) { + when (homeState) { + Resource.Loading -> LoadingScreen(modifier) + is Resource.Success -> HomeContent( + uiModel = homeState.data, + navigateToArtist = navigateToArtist, + navigateToAlbum = navigateToAlbum, + modifier = modifier + ) + is Resource.Error -> ErrorScreen(modifier) + } +} + +@Composable +private fun HomeContent( + uiModel: HomeUIModel, + navigateToArtist: (String) -> Unit, + navigateToAlbum: (String) -> Unit, + modifier: Modifier = Modifier +) { + LazyColumn( + contentPadding = PaddingValues(vertical = 16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + modifier = modifier + ) { + item { + Carousel(title = stringResource(R.string.home_recently_played_carousel_header_title)) { + LazyRow( + contentPadding = PaddingValues(horizontal = 16.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + items(uiModel.recentlyPlayed) { artist -> + ArtistCard( + artist = artist, + artworkSize = 125.dp, + onClick = { navigateToArtist(artist.id) } + ) + } + } + } + } + + item { + Carousel(title = stringResource(R.string.home_recently_added_carousel_header_title)) { + LazyRow( + contentPadding = PaddingValues(horizontal = 16.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + items(uiModel.recentlyAdded) { album -> + AlbumCard( + album = album, + artworkSize = 125.dp, + onClick = { navigateToAlbum(album.id) } + ) + } + } + } + } + } +} diff --git a/app/src/main/java/com/devdunnapps/amplify/ui/home/HomeViewModel.kt b/app/src/main/java/com/devdunnapps/amplify/ui/home/HomeViewModel.kt new file mode 100644 index 0000000..9eba63c --- /dev/null +++ b/app/src/main/java/com/devdunnapps/amplify/ui/home/HomeViewModel.kt @@ -0,0 +1,49 @@ +package com.devdunnapps.amplify.ui.home + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.devdunnapps.amplify.data.networking.NetworkResponse +import com.devdunnapps.amplify.domain.models.Album +import com.devdunnapps.amplify.domain.models.Artist +import com.devdunnapps.amplify.domain.repository.PlexRepository +import com.devdunnapps.amplify.utils.Resource +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.async +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class HomeViewModel @Inject constructor( + private val plexRepository: PlexRepository +) : ViewModel() { + private val _uiState: MutableStateFlow> = + MutableStateFlow(Resource.Loading) + val uiState = _uiState.asStateFlow() + + init { + viewModelScope.launch { + val recentArtistsDeferred = async { plexRepository.getRecentlyPlayedArtists() } + val recentlyAddedAlbumsDeferred = async { plexRepository.getRecentlyAddedAlbums() } + + val recentArtists = recentArtistsDeferred.await() + val recentlyAddedAlbums = recentlyAddedAlbumsDeferred.await() + + if ( + recentArtists is NetworkResponse.Success && + recentlyAddedAlbums is NetworkResponse.Success + ) { + val uiModel = HomeUIModel(recentArtists.data, recentlyAddedAlbums.data) + _uiState.emit(Resource.Success(uiModel)) + } else { + _uiState.emit(Resource.Error()) + } + } + } +} + +data class HomeUIModel( + val recentlyPlayed: List, + val recentlyAdded: List +) diff --git a/app/src/main/java/com/devdunnapps/amplify/ui/search/SearchFragment.kt b/app/src/main/java/com/devdunnapps/amplify/ui/search/SearchFragment.kt index 4b4a072..02efa8c 100644 --- a/app/src/main/java/com/devdunnapps/amplify/ui/search/SearchFragment.kt +++ b/app/src/main/java/com/devdunnapps/amplify/ui/search/SearchFragment.kt @@ -10,7 +10,6 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items @@ -238,11 +237,11 @@ private fun AlbumsSearchResults(albums: List) { items(albums) { album -> AlbumCard( album = album, + artworkSize = 100.dp, onClick = { val action = MobileNavigationDirections.actionGlobalNavigationAlbum(album.id) localView.findNavController().navigate(action) - }, - modifier = Modifier.width(100.dp) + } ) } } @@ -261,11 +260,11 @@ private fun ArtistsSearchResults(artists: List) { items(artists) { artist -> ArtistCard( artist = artist, + artworkSize = 100.dp, onClick = { val action = MobileNavigationDirections.actionGlobalNavigationArtist(artist.id) localView.findNavController().navigate(action) - }, - modifier = Modifier.width(100.dp) + } ) } } diff --git a/app/src/main/res/drawable/ic_bottom_nav_home.xml b/app/src/main/res/drawable/ic_bottom_nav_home.xml new file mode 100644 index 0000000..02b1e6a --- /dev/null +++ b/app/src/main/res/drawable/ic_bottom_nav_home.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_home.xml b/app/src/main/res/drawable/ic_home.xml new file mode 100644 index 0000000..ad899ed --- /dev/null +++ b/app/src/main/res/drawable/ic_home.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_home_outlined.xml b/app/src/main/res/drawable/ic_home_outlined.xml new file mode 100644 index 0000000..f0bd430 --- /dev/null +++ b/app/src/main/res/drawable/ic_home_outlined.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/menu/menu_bottom_nav.xml b/app/src/main/res/menu/menu_bottom_nav.xml index c17fd48..4b12ab0 100644 --- a/app/src/main/res/menu/menu_bottom_nav.xml +++ b/app/src/main/res/menu/menu_bottom_nav.xml @@ -1,6 +1,11 @@ + + + app:startDestination="@+id/navigation_home"> + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras quis elit egestas, vulputate tellus vitae, ultricies nisi. Suspendisse quis sollicitudin erat. Vivamus eu maximus purus. Vivamus quis lacus sem. Praesent rhoncus porttitor lacus nec fringilla. Proin euismod semper odio, et lobortis purus volutpat maximus. Nulla a feugiat massa. Suspendisse vel felis egestas, consectetur mauris sit amet, semper lacus. Aliquam ut metus mi. Morbi a rutrum tellus. Donec vitae luctus nisi. Etiam aliquam elementum felis ut venenatis. Fusce tempor porta purus ut commodo. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Page %1$d + Home + Recently Played Music + Recently Added Music + Artists Shuffle