From e2ea7711addf8a96974e63dfb0f5713d754fdddf Mon Sep 17 00:00:00 2001 From: Jarne Demeulemeester Date: Sat, 21 Dec 2024 18:15:24 +0100 Subject: [PATCH] feat: initial home screen layout --- app/phone/build.gradle.kts | 1 + .../dev/jdtech/jellyfin/NavigationRoot.kt | 26 ++- .../jellyfin/presentation/film/HomeScreen.kt | 193 ++++++++++++++++++ 3 files changed, 218 insertions(+), 2 deletions(-) create mode 100644 app/phone/src/main/java/dev/jdtech/jellyfin/presentation/film/HomeScreen.kt diff --git a/app/phone/build.gradle.kts b/app/phone/build.gradle.kts index fc780f2bd8..1494a1e1f2 100644 --- a/app/phone/build.gradle.kts +++ b/app/phone/build.gradle.kts @@ -111,6 +111,7 @@ dependencies { implementation(projects.player.core) implementation(projects.player.video) implementation(projects.setup) + implementation(projects.modes.film) implementation(libs.aboutlibraries.core) implementation(libs.aboutlibraries) implementation(libs.androidx.activity) diff --git a/app/phone/src/main/java/dev/jdtech/jellyfin/NavigationRoot.kt b/app/phone/src/main/java/dev/jdtech/jellyfin/NavigationRoot.kt index 06b00c2db8..a51e3006c8 100644 --- a/app/phone/src/main/java/dev/jdtech/jellyfin/NavigationRoot.kt +++ b/app/phone/src/main/java/dev/jdtech/jellyfin/NavigationRoot.kt @@ -11,6 +11,8 @@ import androidx.navigation.NavOptionsBuilder import androidx.navigation.Navigator import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable +import androidx.navigation.navigation +import dev.jdtech.jellyfin.presentation.film.HomeScreen import dev.jdtech.jellyfin.presentation.setup.addserver.AddServerScreen import dev.jdtech.jellyfin.presentation.setup.login.LoginScreen import dev.jdtech.jellyfin.presentation.setup.servers.ServersScreen @@ -33,6 +35,12 @@ data object UsersRoute @Serializable data object LoginRoute +@Serializable +data object FilmGraphRoute + +@Serializable +data object HomeRoute + @Composable fun NavigationRoot( navController: NavHostController, @@ -41,7 +49,7 @@ fun NavigationRoot( hasCurrentUser: Boolean, ) { val startDestination = when { - hasServers && hasCurrentServer && hasCurrentUser -> UsersRoute // TODO: change to MainRoute + hasServers && hasCurrentServer && hasCurrentUser -> FilmGraphRoute hasServers && hasCurrentServer -> UsersRoute hasServers -> ServersRoute else -> WelcomeRoute @@ -112,7 +120,14 @@ fun NavigationRoot( } composable { LoginScreen( - onSuccess = {}, + onSuccess = { + navController.safeNavigate(FilmGraphRoute) { + popUpTo(FilmGraphRoute) { + inclusive = false + } + launchSingleTop = true + } + }, onChangeServerClick = { navController.safeNavigate(ServersRoute) { popUpTo(ServersRoute) { @@ -126,6 +141,13 @@ fun NavigationRoot( }, ) } + navigation( + startDestination = HomeRoute, + ) { + composable { + HomeScreen() + } + } } } diff --git a/app/phone/src/main/java/dev/jdtech/jellyfin/presentation/film/HomeScreen.kt b/app/phone/src/main/java/dev/jdtech/jellyfin/presentation/film/HomeScreen.kt new file mode 100644 index 0000000000..3e85df4403 --- /dev/null +++ b/app/phone/src/main/java/dev/jdtech/jellyfin/presentation/film/HomeScreen.kt @@ -0,0 +1,193 @@ +package dev.jdtech.jellyfin.presentation.film + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.WindowInsets +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.safeDrawing +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SearchBar +import androidx.compose.material3.SearchBarDefaults +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.isTraversalGroup +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.traversalIndex +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewScreenSizes +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import dev.jdtech.jellyfin.core.presentation.dummy.dummyHomeSection +import dev.jdtech.jellyfin.core.presentation.dummy.dummyHomeView +import dev.jdtech.jellyfin.film.presentation.home.HomeState +import dev.jdtech.jellyfin.film.presentation.home.HomeViewModel +import dev.jdtech.jellyfin.presentation.theme.FindroidTheme +import dev.jdtech.jellyfin.presentation.theme.spacings +import dev.jdtech.jellyfin.core.R as CoreR +import dev.jdtech.jellyfin.film.R as FilmR + +@Composable +fun HomeScreen( + viewModel: HomeViewModel = hiltViewModel(), +) { + val state by viewModel.state.collectAsStateWithLifecycle() + + LaunchedEffect(true) { + viewModel.loadData() + } + + HomeScreenLayout( + state = state, + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun HomeScreenLayout( + state: HomeState, +) { + val density = LocalDensity.current + val layoutDirection = LocalLayoutDirection.current + + val contentPadding = PaddingValues( + start = with(density) { WindowInsets.safeDrawing.getLeft(this, layoutDirection).toDp() + MaterialTheme.spacings.default }, + end = with(density) { WindowInsets.safeDrawing.getRight(this, layoutDirection).toDp() + MaterialTheme.spacings.default }, + ) + + var searchQuery by rememberSaveable { mutableStateOf("") } + var expanded by rememberSaveable { mutableStateOf(false) } + + Box( + modifier = Modifier + .fillMaxSize() + .semantics { isTraversalGroup = true } + ) { + SearchBar( + inputField = { + SearchBarDefaults.InputField( + query = searchQuery, + onQueryChange = { searchQuery = it }, + onSearch = { expanded = true }, + expanded = expanded, + onExpandedChange = { expanded = it }, + placeholder = { Text("Search movies and TV shows") }, + leadingIcon = { + Icon( + painter = painterResource(CoreR.drawable.ic_search), + contentDescription = null + ) + } + ) + }, + expanded = expanded, + onExpandedChange = { expanded = it }, + modifier = Modifier + .align(Alignment.TopCenter) + .semantics { traversalIndex = 0f }, + ) { } + LazyColumn( + modifier = Modifier + .fillMaxSize() + .semantics { traversalIndex = 1f }, + contentPadding = PaddingValues( + top = with(density) { WindowInsets.safeDrawing.getTop(this).toDp() + 72.dp }, + bottom = MaterialTheme.spacings.default + ), + verticalArrangement = Arrangement.spacedBy(MaterialTheme.spacings.medium) + ) { + items(state.sections) { section -> + Box( + modifier = Modifier + .fillMaxWidth() + .height(42.dp) + .padding(contentPadding) + ) { + Text( + text = section.homeSection.name.asString(), + modifier = Modifier.align(Alignment.CenterStart) + ) + } + LazyRow( + contentPadding = contentPadding, + horizontalArrangement = Arrangement.spacedBy(MaterialTheme.spacings.small) + ) { + items(section.homeSection.items) { items -> + Box( + modifier = Modifier.size(120.dp, 180.dp).background(MaterialTheme.colorScheme.errorContainer) + ) + } + } + } + items(state.views) { view -> + Box( + modifier = Modifier + .fillMaxWidth() + .height(42.dp) + .padding(contentPadding) + ) { + Text( + text = stringResource(FilmR.string.latest_library, view.view.name), + modifier = Modifier.align(Alignment.CenterStart) + ) + TextButton( + onClick = {}, + modifier = Modifier.align(Alignment.CenterEnd) + ) { + Text(stringResource(CoreR.string.view_all)) + } + } + if (view.view.items != null) { + LazyRow( + contentPadding = contentPadding, + horizontalArrangement = Arrangement.spacedBy(MaterialTheme.spacings.small) + ) { + items(view.view.items!!) { items -> + Box( + modifier = Modifier.size(120.dp, 180.dp).background(MaterialTheme.colorScheme.errorContainer) + ) + } + } + } + } + } + } + +} + +@PreviewScreenSizes +@Preview +@Composable +private fun HomeScrmeenLayoutPreview() { + FindroidTheme { + HomeScreenLayout( + state = HomeState( + sections = listOf(dummyHomeSection), + views = listOf(dummyHomeView), + ) + ) + } +} \ No newline at end of file