From 5d89986b42a834c09e66449ccd91c947fb0f318b Mon Sep 17 00:00:00 2001 From: Blake Dunn Date: Wed, 20 Dec 2023 23:51:37 -0600 Subject: [PATCH] add home screen --- .../com/devdunnapps/amplify/data/PlexAPI.kt | 15 ++ .../amplify/data/PlexRepositoryImpl.kt | 15 ++ .../com/devdunnapps/amplify/data/PlexTVAPI.kt | 9 +- .../amplify/data/PlexTVRepositoryImpl.kt | 12 +- .../models/{SigninDTO.kt => SignInDTO.kt} | 2 +- .../amplify/data/models/SignInResponseDTO.kt | 12 + .../amplify/data/models/UserDTO.kt | 11 +- .../amplify/domain/models/SignInModel.kt | 5 + .../devdunnapps/amplify/domain/models/User.kt | 6 +- .../domain/repository/PlexRepository.kt | 6 + .../domain/repository/PlexTVRepository.kt | 6 +- .../amplify/ui/albums/AlbumsFragment.kt | 7 +- .../artist/albums/ArtistAllAlbumsFragment.kt | 5 +- .../amplify/ui/artists/ArtistsFragment.kt | 7 +- .../amplify/ui/components/AlbumCard.kt | 22 +- .../amplify/ui/components/ArtistCard.kt | 25 +- .../amplify/ui/home/HomeFragment.kt | 231 ++++++++++++++++++ .../amplify/ui/home/HomeViewModel.kt | 76 ++++++ .../ui/onboarding/LoginFlowViewModel.kt | 4 +- .../amplify/ui/onboarding/LoginScreen.kt | 23 +- .../amplify/ui/search/SearchFragment.kt | 9 +- .../devdunnapps/amplify/ui/utils/Greeting.kt | 14 ++ .../main/res/drawable/ic_bottom_nav_home.xml | 5 + app/src/main/res/drawable/ic_home.xml | 10 + .../main/res/drawable/ic_home_outlined.xml | 10 + app/src/main/res/menu/menu_bottom_nav.xml | 5 + .../main/res/navigation/main_navigation.xml | 7 +- app/src/main/res/values/strings.xml | 9 + 28 files changed, 528 insertions(+), 40 deletions(-) rename app/src/main/java/com/devdunnapps/amplify/data/models/{SigninDTO.kt => SignInDTO.kt} (85%) create mode 100644 app/src/main/java/com/devdunnapps/amplify/data/models/SignInResponseDTO.kt create mode 100644 app/src/main/java/com/devdunnapps/amplify/domain/models/SignInModel.kt create mode 100644 app/src/main/java/com/devdunnapps/amplify/ui/home/HomeFragment.kt create mode 100644 app/src/main/java/com/devdunnapps/amplify/ui/home/HomeViewModel.kt create mode 100644 app/src/main/java/com/devdunnapps/amplify/ui/utils/Greeting.kt create mode 100644 app/src/main/res/drawable/ic_bottom_nav_home.xml create mode 100644 app/src/main/res/drawable/ic_home.xml create mode 100644 app/src/main/res/drawable/ic_home_outlined.xml 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..a3ec375 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,19 @@ 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 + + @GET("library/sections/{section}/all?viewCount>=1&type=10&sort=lastViewedAt:desc&limit=3") + suspend fun getRecentlyPlayedSongs( + @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..b5993a6 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,19 @@ 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() + } + + override suspend fun getRecentlyPlayedSongs(): NetworkResponse> = + api.getRecentlyPlayedSongs(section).map { + it.mediaContainer.metadata?.map { song -> song.toSong() }.orEmpty() + } } diff --git a/app/src/main/java/com/devdunnapps/amplify/data/PlexTVAPI.kt b/app/src/main/java/com/devdunnapps/amplify/data/PlexTVAPI.kt index 888d2c9..c47b6a1 100644 --- a/app/src/main/java/com/devdunnapps/amplify/data/PlexTVAPI.kt +++ b/app/src/main/java/com/devdunnapps/amplify/data/PlexTVAPI.kt @@ -1,8 +1,10 @@ package com.devdunnapps.amplify.data import com.devdunnapps.amplify.data.models.ResourceDTO -import com.devdunnapps.amplify.data.models.SigninDTO +import com.devdunnapps.amplify.data.models.SignInDTO +import com.devdunnapps.amplify.data.models.SignInResponseDTO import com.devdunnapps.amplify.data.models.UserDTO +import com.devdunnapps.amplify.data.networking.NetworkResponse import retrofit2.Response import retrofit2.http.* @@ -10,8 +12,11 @@ interface PlexTVAPI { @POST("api/v2/users/signin") @Headers("X-Amplify-Ignore-Auth-Errors: 1") - suspend fun signInUser(@Body user: SigninDTO): Response + suspend fun signInUser(@Body user: SignInDTO): Response @GET("api/v2/resources?includeHttps=1&includeRelay=1") suspend fun getServers(): List + + @GET("api/v2/user") + suspend fun getUser(): NetworkResponse } diff --git a/app/src/main/java/com/devdunnapps/amplify/data/PlexTVRepositoryImpl.kt b/app/src/main/java/com/devdunnapps/amplify/data/PlexTVRepositoryImpl.kt index 4281f7d..388ea41 100644 --- a/app/src/main/java/com/devdunnapps/amplify/data/PlexTVRepositoryImpl.kt +++ b/app/src/main/java/com/devdunnapps/amplify/data/PlexTVRepositoryImpl.kt @@ -1,8 +1,11 @@ package com.devdunnapps.amplify.data import com.devdunnapps.amplify.data.models.ErrorsDTO -import com.devdunnapps.amplify.data.models.SigninDTO +import com.devdunnapps.amplify.data.models.SignInDTO +import com.devdunnapps.amplify.data.networking.NetworkResponse +import com.devdunnapps.amplify.data.networking.map import com.devdunnapps.amplify.domain.models.Server +import com.devdunnapps.amplify.domain.models.SignInModel import com.devdunnapps.amplify.domain.models.User import com.devdunnapps.amplify.domain.repository.PlexTVRepository import com.devdunnapps.amplify.utils.Resource @@ -17,11 +20,11 @@ class PlexTVRepositoryImpl @Inject constructor( private val plexTVClient: PlexTVAPI ): PlexTVRepository { - override fun signInUser(username: String, password: String, authToken: String?): Flow> = flow { + override fun signInUser(username: String, password: String, authToken: String?): Flow> = flow { emit(Resource.Loading) try { - val userCredentials = SigninDTO(username, password, authToken) + val userCredentials = SignInDTO(username, password, authToken) val response = plexTVClient.signInUser(userCredentials) if (response.code() == HttpURLConnection.HTTP_CREATED) { val user = response.body()?.toUser() ?: run { @@ -58,4 +61,7 @@ class PlexTVRepositoryImpl @Inject constructor( } emit(Resource.Success(servers.toList())) } + + override suspend fun getUser(): NetworkResponse = + plexTVClient.getUser().map { it.toUser() } } diff --git a/app/src/main/java/com/devdunnapps/amplify/data/models/SigninDTO.kt b/app/src/main/java/com/devdunnapps/amplify/data/models/SignInDTO.kt similarity index 85% rename from app/src/main/java/com/devdunnapps/amplify/data/models/SigninDTO.kt rename to app/src/main/java/com/devdunnapps/amplify/data/models/SignInDTO.kt index 39777a2..a98ef6d 100644 --- a/app/src/main/java/com/devdunnapps/amplify/data/models/SigninDTO.kt +++ b/app/src/main/java/com/devdunnapps/amplify/data/models/SignInDTO.kt @@ -1,6 +1,6 @@ package com.devdunnapps.amplify.data.models -data class SigninDTO( +data class SignInDTO( val login: String, val password: String, val verificationCode: String? diff --git a/app/src/main/java/com/devdunnapps/amplify/data/models/SignInResponseDTO.kt b/app/src/main/java/com/devdunnapps/amplify/data/models/SignInResponseDTO.kt new file mode 100644 index 0000000..2affe54 --- /dev/null +++ b/app/src/main/java/com/devdunnapps/amplify/data/models/SignInResponseDTO.kt @@ -0,0 +1,12 @@ +package com.devdunnapps.amplify.data.models + +import com.devdunnapps.amplify.domain.models.SignInModel + +data class SignInResponseDTO ( + val authToken: String +) { + + fun toUser() = SignInModel( + authToken = authToken + ) +} diff --git a/app/src/main/java/com/devdunnapps/amplify/data/models/UserDTO.kt b/app/src/main/java/com/devdunnapps/amplify/data/models/UserDTO.kt index 55cec0e..38dc438 100644 --- a/app/src/main/java/com/devdunnapps/amplify/data/models/UserDTO.kt +++ b/app/src/main/java/com/devdunnapps/amplify/data/models/UserDTO.kt @@ -2,11 +2,14 @@ package com.devdunnapps.amplify.data.models import com.devdunnapps.amplify.domain.models.User -data class UserDTO ( - val authToken: String +data class UserDTO( + val username: String, + val friendlyName: String?, + val thumb: String ) { - fun toUser() = User( - authToken = authToken + username = username, + displayName = friendlyName, + avatar = thumb ) } diff --git a/app/src/main/java/com/devdunnapps/amplify/domain/models/SignInModel.kt b/app/src/main/java/com/devdunnapps/amplify/domain/models/SignInModel.kt new file mode 100644 index 0000000..9aba7b9 --- /dev/null +++ b/app/src/main/java/com/devdunnapps/amplify/domain/models/SignInModel.kt @@ -0,0 +1,5 @@ +package com.devdunnapps.amplify.domain.models + +data class SignInModel( + val authToken: String +) diff --git a/app/src/main/java/com/devdunnapps/amplify/domain/models/User.kt b/app/src/main/java/com/devdunnapps/amplify/domain/models/User.kt index 06ca1dd..454fa86 100644 --- a/app/src/main/java/com/devdunnapps/amplify/domain/models/User.kt +++ b/app/src/main/java/com/devdunnapps/amplify/domain/models/User.kt @@ -1,5 +1,7 @@ package com.devdunnapps.amplify.domain.models -data class User ( - val authToken: String +data class User( + val username: String, + val displayName: String?, + val avatar: String ) 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..16d7f23 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,10 @@ 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> + + suspend fun getRecentlyPlayedSongs() : NetworkResponse> } diff --git a/app/src/main/java/com/devdunnapps/amplify/domain/repository/PlexTVRepository.kt b/app/src/main/java/com/devdunnapps/amplify/domain/repository/PlexTVRepository.kt index 5aaa1b0..c5f4ddb 100644 --- a/app/src/main/java/com/devdunnapps/amplify/domain/repository/PlexTVRepository.kt +++ b/app/src/main/java/com/devdunnapps/amplify/domain/repository/PlexTVRepository.kt @@ -1,13 +1,17 @@ package com.devdunnapps.amplify.domain.repository +import com.devdunnapps.amplify.data.networking.NetworkResponse import com.devdunnapps.amplify.domain.models.Server +import com.devdunnapps.amplify.domain.models.SignInModel import com.devdunnapps.amplify.domain.models.User import com.devdunnapps.amplify.utils.Resource import kotlinx.coroutines.flow.Flow interface PlexTVRepository { - fun signInUser(username: String, password: String, authToken: String?): Flow> + fun signInUser(username: String, password: String, authToken: String?): Flow> fun getUserServers(): Flow>> + + suspend fun getUser(): 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..8f43163 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 @@ -17,21 +21,27 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext -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 import com.devdunnapps.amplify.domain.models.Artist 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 +49,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 +64,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 +82,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..ffee513 --- /dev/null +++ b/app/src/main/java/com/devdunnapps/amplify/ui/home/HomeFragment.kt @@ -0,0 +1,231 @@ +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.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +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.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +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 coil.compose.AsyncImage +import com.devdunnapps.amplify.MobileNavigationDirections +import com.devdunnapps.amplify.R +import com.devdunnapps.amplify.domain.models.Song +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.components.SongItem +import com.devdunnapps.amplify.ui.utils.FragmentRootDestinationScaffold +import com.devdunnapps.amplify.ui.utils.Greeting +import com.devdunnapps.amplify.ui.utils.getCurrentSizeClass +import com.devdunnapps.amplify.ui.utils.whenTrue +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) + }, + onSongMenuClick = { songId -> + val action = MobileNavigationDirections.actionGlobalNavigationSongBottomSheet(songId) + findNavController().navigate(action) + } + ) + } + } + } +} + +@Composable +private fun HomeRoute( + modifier: Modifier = Modifier, + viewModel: HomeViewModel = hiltViewModel(), + navigateToArtist: (String) -> Unit, + navigateToAlbum: (String) -> Unit, + onSongMenuClick: (String) -> Unit +) { + val homeState = viewModel.uiState.collectAsState().value + HomeScreen( + homeState = homeState, + modifier = modifier, + navigateToArtist = navigateToArtist, + navigateToAlbum = navigateToAlbum, + playSong = viewModel::playSong, + onSongMenuClick = onSongMenuClick + ) +} + +@Composable +private fun HomeScreen( + homeState: Resource, + modifier: Modifier = Modifier, + navigateToArtist: (String) -> Unit, + navigateToAlbum: (String) -> Unit, + playSong: (Song) -> Unit, + onSongMenuClick: (String) -> Unit +) { + when (homeState) { + Resource.Loading -> LoadingScreen(modifier) + is Resource.Success -> HomeContent( + uiModel = homeState.data, + navigateToArtist = navigateToArtist, + navigateToAlbum = navigateToAlbum, + playSong = playSong, + onSongMenuClick = onSongMenuClick, + modifier = modifier + ) + is Resource.Error -> ErrorScreen(modifier) + } +} + +@Composable +private fun HomeContent( + uiModel: HomeUIModel, + navigateToArtist: (String) -> Unit, + navigateToAlbum: (String) -> Unit, + playSong: (Song) -> Unit, + onSongMenuClick: (String) -> Unit, + modifier: Modifier = Modifier +) { + LazyColumn( + contentPadding = PaddingValues(vertical = 16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + modifier = modifier + ) { + item { + Header(model = uiModel) + } + + 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.recentArtists) { 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) } + ) + } + } + } + } + + item { + Carousel(title = stringResource(R.string.home_recent_songs_header_title)) { + Column { + uiModel.recentSongs.forEach { song -> + SongItem( + song = song, + onClick = { playSong(song) }, + onItemMenuClick = onSongMenuClick + ) + } + } + } + } + } +} + +@Composable +private fun Header(model: HomeUIModel) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier + .whenTrue(getCurrentSizeClass() == WindowWidthSizeClass.Compact) { + fillMaxWidth() + } + .whenTrue(getCurrentSizeClass() != WindowWidthSizeClass.Compact) { + width(400.dp) + } + .padding(horizontal = 16.dp) + .background( + shape = MaterialTheme.shapes.medium, + color = MaterialTheme.colorScheme.surfaceVariant + ) + .padding(8.dp) + ) { + AsyncImage( + model = model.userAvatar, + contentDescription = null, + modifier = Modifier + .size(75.dp) + .clip(shape = CircleShape) + ) + + Text( + text = stringResource(Greeting.create(), model.userTitle), + style = MaterialTheme.typography.headlineSmall + ) + } +} 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..79cefdf --- /dev/null +++ b/app/src/main/java/com/devdunnapps/amplify/ui/home/HomeViewModel.kt @@ -0,0 +1,76 @@ +package com.devdunnapps.amplify.ui.home + +import android.os.Bundle +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.models.Song +import com.devdunnapps.amplify.domain.repository.PlexRepository +import com.devdunnapps.amplify.domain.repository.PlexTVRepository +import com.devdunnapps.amplify.utils.MusicServiceConnection +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, + private val plexTVRepository: PlexTVRepository, + private val musicServiceConnection: MusicServiceConnection +) : ViewModel() { + private val _uiState: MutableStateFlow> = + MutableStateFlow(Resource.Loading) + val uiState = _uiState.asStateFlow() + + init { + viewModelScope.launch { + val userDeferred = async { plexTVRepository.getUser() } + val recentArtistsDeferred = async { plexRepository.getRecentlyPlayedArtists() } + val recentlyAddedAlbumsDeferred = async { plexRepository.getRecentlyAddedAlbums() } + val recentSongsDeferred = async { plexRepository.getRecentlyPlayedSongs() } + + val user = userDeferred.await() + val recentArtists = recentArtistsDeferred.await() + val recentlyAddedAlbums = recentlyAddedAlbumsDeferred.await() + val recentSongs = recentSongsDeferred.await() + + if ( + user is NetworkResponse.Success && + recentArtists is NetworkResponse.Success && + recentlyAddedAlbums is NetworkResponse.Success && + recentSongs is NetworkResponse.Success + ) { + val uiModel = HomeUIModel( + user.data.avatar, + user.data.displayName ?: user.data.username, + recentArtists.data, + recentlyAddedAlbums.data, + recentSongs.data + ) + _uiState.emit(Resource.Success(uiModel)) + } else { + _uiState.emit(Resource.Error()) + } + } + } + + fun playSong(song: Song) { + val bundle = Bundle() + bundle.putSerializable("song", song) + musicServiceConnection.transportControls.sendCustomAction("play_song", bundle) + } +} + +data class HomeUIModel( + val userAvatar: String, + val userTitle: String, + val recentArtists: List, + val recentlyAdded: List, + val recentSongs: List +) diff --git a/app/src/main/java/com/devdunnapps/amplify/ui/onboarding/LoginFlowViewModel.kt b/app/src/main/java/com/devdunnapps/amplify/ui/onboarding/LoginFlowViewModel.kt index 98d6b2e..8771bb5 100644 --- a/app/src/main/java/com/devdunnapps/amplify/ui/onboarding/LoginFlowViewModel.kt +++ b/app/src/main/java/com/devdunnapps/amplify/ui/onboarding/LoginFlowViewModel.kt @@ -6,7 +6,7 @@ import androidx.lifecycle.viewModelScope import com.devdunnapps.amplify.data.networking.NetworkResponse import com.devdunnapps.amplify.domain.models.LibrarySection import com.devdunnapps.amplify.domain.models.Server -import com.devdunnapps.amplify.domain.models.User +import com.devdunnapps.amplify.domain.models.SignInModel import com.devdunnapps.amplify.domain.usecases.GetLibrarySectionsUseCase import com.devdunnapps.amplify.domain.usecases.GetUsersServersUseCase import com.devdunnapps.amplify.domain.usecases.SignInUserUseCase @@ -26,7 +26,7 @@ class LoginFlowViewModel @Inject constructor( private val app: Application ): AndroidViewModel(app) { - private val _user = MutableStateFlow>(Resource.Loading) + private val _user = MutableStateFlow>(Resource.Loading) val user = _user.asStateFlow() private val _twoFactorAuthRequired = MutableStateFlow(false) diff --git a/app/src/main/java/com/devdunnapps/amplify/ui/onboarding/LoginScreen.kt b/app/src/main/java/com/devdunnapps/amplify/ui/onboarding/LoginScreen.kt index 2e33ce6..bdf5b7f 100644 --- a/app/src/main/java/com/devdunnapps/amplify/ui/onboarding/LoginScreen.kt +++ b/app/src/main/java/com/devdunnapps/amplify/ui/onboarding/LoginScreen.kt @@ -1,14 +1,30 @@ package com.devdunnapps.amplify.ui.onboarding -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Visibility import androidx.compose.material.icons.filled.VisibilityOff -import androidx.compose.material3.* -import androidx.compose.runtime.* +import androidx.compose.material3.Button +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester @@ -23,7 +39,6 @@ import androidx.compose.ui.unit.dp import com.devdunnapps.amplify.R import com.devdunnapps.amplify.utils.Resource -@OptIn(ExperimentalMaterial3Api::class) @Composable fun LoginScreen(viewModel: LoginFlowViewModel, onNavigateToServerSelection: () -> Unit) { val twoFactorAuthRequired by viewModel.twoFactorAuthRequired.collectAsState() 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/java/com/devdunnapps/amplify/ui/utils/Greeting.kt b/app/src/main/java/com/devdunnapps/amplify/ui/utils/Greeting.kt new file mode 100644 index 0000000..5a9ab05 --- /dev/null +++ b/app/src/main/java/com/devdunnapps/amplify/ui/utils/Greeting.kt @@ -0,0 +1,14 @@ +package com.devdunnapps.amplify.ui.utils + +import androidx.annotation.StringRes +import com.devdunnapps.amplify.R +import java.util.Calendar + +object Greeting { + @StringRes fun create(): Int = when (Calendar.getInstance().get(Calendar.HOUR_OF_DAY)) { + in 2..11 -> R.string.greeting_morning + in 12..16 -> R.string.greeting_afternoon + in 17..20 -> R.string.greeting_evening + else -> R.string.greeting_night + } +} 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 Artists + Recently Added Music + Recently Played Songs + Good morning,\n%s + Good afternoon,\n%s + Good evening,\n%s + Good night,\n%s + Artists Shuffle