mirror of
https://github.com/bbara04/Purefin.git
synced 2026-03-31 17:10:08 +02:00
implement Navigation 3 and Movie Details screen
- Update Android Gradle Plugin to 8.9.1 and target SDK to 36. - Integrate `androidx.navigation3` for Compose-based navigation management. - Implement a centralized `NavigationManager` and `Route` system (supporting `Home` and `Movie` routes). - Add a comprehensive Movie Details screen featuring hero images, synopsis, metadata chips, and cast lists. - Refactor `HomePage` and related components into a dedicated `hu.bbara.purefin.app.home` package. - Enhance `JellyfinApiClient` with methods to fetch item details and "next up" episodes. - Update `PurefinActivity` to use `NavDisplay` for handling app navigation and backstack. - Setup Hilt modules for providing navigation-related dependencies.
This commit is contained in:
@@ -11,12 +11,12 @@ plugins {
|
|||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "hu.bbara.purefin"
|
namespace = "hu.bbara.purefin"
|
||||||
compileSdk = 35
|
compileSdk = 36
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "hu.bbara.purefin"
|
applicationId = "hu.bbara.purefin"
|
||||||
minSdk = 29
|
minSdk = 29
|
||||||
targetSdk = 35
|
targetSdk = 36
|
||||||
versionCode = 1
|
versionCode = 1
|
||||||
versionName = "1.0"
|
versionName = "1.0"
|
||||||
|
|
||||||
@@ -50,6 +50,7 @@ dependencies {
|
|||||||
implementation(libs.androidx.core.ktx)
|
implementation(libs.androidx.core.ktx)
|
||||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||||
implementation(libs.androidx.lifecycle.viewmodel.compose)
|
implementation(libs.androidx.lifecycle.viewmodel.compose)
|
||||||
|
implementation(libs.androidx.lifecycle.viewmodel.navigation3)
|
||||||
implementation(libs.androidx.activity.compose)
|
implementation(libs.androidx.activity.compose)
|
||||||
implementation(platform(libs.androidx.compose.bom))
|
implementation(platform(libs.androidx.compose.bom))
|
||||||
implementation(libs.androidx.compose.ui)
|
implementation(libs.androidx.compose.ui)
|
||||||
@@ -70,6 +71,8 @@ dependencies {
|
|||||||
implementation(libs.medi3.ui)
|
implementation(libs.medi3.ui)
|
||||||
implementation(libs.medi3.exoplayer)
|
implementation(libs.medi3.exoplayer)
|
||||||
implementation(libs.medi3.ui.compose)
|
implementation(libs.medi3.ui.compose)
|
||||||
|
implementation(libs.androidx.navigation3.runtime)
|
||||||
|
implementation(libs.androidx.navigation3.ui)
|
||||||
testImplementation(libs.junit)
|
testImplementation(libs.junit)
|
||||||
androidTestImplementation(libs.androidx.junit)
|
androidTestImplementation(libs.androidx.junit)
|
||||||
androidTestImplementation(libs.androidx.espresso.core)
|
androidTestImplementation(libs.androidx.espresso.core)
|
||||||
|
|||||||
@@ -5,9 +5,21 @@ import android.os.Bundle
|
|||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
|
||||||
|
import androidx.navigation3.runtime.EntryProviderScope
|
||||||
|
import androidx.navigation3.runtime.NavBackStack
|
||||||
|
import androidx.navigation3.runtime.entryProvider
|
||||||
|
import androidx.navigation3.runtime.rememberNavBackStack
|
||||||
|
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
|
||||||
|
import androidx.navigation3.ui.NavDisplay
|
||||||
import coil3.ImageLoader
|
import coil3.ImageLoader
|
||||||
import coil3.SingletonImageLoader
|
import coil3.SingletonImageLoader
|
||||||
import coil3.disk.DiskCache
|
import coil3.disk.DiskCache
|
||||||
@@ -16,10 +28,13 @@ import coil3.network.okhttp.OkHttpNetworkFetcherFactory
|
|||||||
import coil3.request.crossfade
|
import coil3.request.crossfade
|
||||||
import coil3.util.DebugLogger
|
import coil3.util.DebugLogger
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import hu.bbara.purefin.app.HomePage
|
|
||||||
import hu.bbara.purefin.client.JellyfinApiClient
|
import hu.bbara.purefin.client.JellyfinApiClient
|
||||||
import hu.bbara.purefin.client.JellyfinAuthInterceptor
|
import hu.bbara.purefin.client.JellyfinAuthInterceptor
|
||||||
import hu.bbara.purefin.login.ui.LoginScreen
|
import hu.bbara.purefin.login.ui.LoginScreen
|
||||||
|
import hu.bbara.purefin.navigation.LocalNavigationManager
|
||||||
|
import hu.bbara.purefin.navigation.NavigationCommand
|
||||||
|
import hu.bbara.purefin.navigation.NavigationManager
|
||||||
|
import hu.bbara.purefin.navigation.Route
|
||||||
import hu.bbara.purefin.session.UserSessionRepository
|
import hu.bbara.purefin.session.UserSessionRepository
|
||||||
import hu.bbara.purefin.ui.theme.PurefinTheme
|
import hu.bbara.purefin.ui.theme.PurefinTheme
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -30,9 +45,14 @@ import javax.inject.Inject
|
|||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class PurefinActivity : ComponentActivity() {
|
class PurefinActivity : ComponentActivity() {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var entryBuilders: Set<@JvmSuppressWildcards EntryProviderScope<Route>.() -> Unit>
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var userSessionRepository: UserSessionRepository
|
lateinit var userSessionRepository: UserSessionRepository
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var navigationManager: NavigationManager
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var jellyfinApiClient: JellyfinApiClient
|
lateinit var jellyfinApiClient: JellyfinApiClient
|
||||||
|
|
||||||
@@ -47,18 +67,16 @@ class PurefinActivity : ComponentActivity() {
|
|||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
lifecycleScope.launch { init() }
|
lifecycleScope.launch { init() }
|
||||||
configureImageLoader()
|
configureImageLoader()
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
setContent {
|
setContent {
|
||||||
PurefinTheme {
|
PurefinTheme() {
|
||||||
val isLoggedIn by userSessionRepository.isLoggedIn.collectAsState(initial = false)
|
MainApp(
|
||||||
if (isLoggedIn) {
|
userSessionRepository = userSessionRepository,
|
||||||
HomePage()
|
entryBuilders = entryBuilders,
|
||||||
} else {
|
navigationManager = navigationManager
|
||||||
LoginScreen()
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -101,4 +119,50 @@ class PurefinActivity : ComponentActivity() {
|
|||||||
private fun isDebuggable(): Boolean {
|
private fun isDebuggable(): Boolean {
|
||||||
return (applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE) != 0
|
return (applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE) != 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MainApp(
|
||||||
|
userSessionRepository: UserSessionRepository,
|
||||||
|
entryBuilders: Set<@JvmSuppressWildcards EntryProviderScope<Route>.() -> Unit>,
|
||||||
|
navigationManager: NavigationManager
|
||||||
|
) {
|
||||||
|
|
||||||
|
val isLoggedIn by userSessionRepository.isLoggedIn.collectAsState(initial = false)
|
||||||
|
if (isLoggedIn) {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
val backStack = rememberNavBackStack(Route.Home) as NavBackStack<Route>
|
||||||
|
val appEntryProvider =
|
||||||
|
entryProvider {
|
||||||
|
entryBuilders.forEach { builder -> builder() }
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(navigationManager, backStack) {
|
||||||
|
navigationManager.commands.collect { command ->
|
||||||
|
when (command) {
|
||||||
|
NavigationCommand.Pop -> backStack.removeLastOrNull()
|
||||||
|
is NavigationCommand.Navigate -> backStack.add(command.route)
|
||||||
|
is NavigationCommand.ReplaceAll -> {
|
||||||
|
backStack.clear()
|
||||||
|
backStack.add(command.route)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CompositionLocalProvider(LocalNavigationManager provides navigationManager) {
|
||||||
|
NavDisplay(
|
||||||
|
backStack = backStack,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
entryDecorators =
|
||||||
|
listOf(
|
||||||
|
rememberSaveableStateHolderNavEntryDecorator(),
|
||||||
|
rememberViewModelStoreNavEntryDecorator(),
|
||||||
|
),
|
||||||
|
entryProvider = appEntryProvider
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LoginScreen()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,4 @@ import dagger.hilt.android.HiltAndroidApp
|
|||||||
@HiltAndroidApp
|
@HiltAndroidApp
|
||||||
class PurefinApplication : Application() {
|
class PurefinApplication : Application() {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
package hu.bbara.purefin.app.content.movie
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.offset
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import org.jellyfin.sdk.model.UUID
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MovieCard(
|
||||||
|
movieId: String,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
viewModel: MovieScreenViewModel = hiltViewModel()
|
||||||
|
) {
|
||||||
|
|
||||||
|
LaunchedEffect(movieId) {
|
||||||
|
viewModel.selectMovie(UUID.fromString(movieId))
|
||||||
|
}
|
||||||
|
|
||||||
|
val movieItem = viewModel.movie.collectAsState()
|
||||||
|
|
||||||
|
if (movieItem.value != null) {
|
||||||
|
BoxWithConstraints(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(MovieBackgroundDark)
|
||||||
|
) {
|
||||||
|
val isWide = maxWidth >= 900.dp
|
||||||
|
val contentPadding = if (isWide) 32.dp else 20.dp
|
||||||
|
|
||||||
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
|
if (isWide) {
|
||||||
|
Row(modifier = Modifier.fillMaxSize()) {
|
||||||
|
MovieHero(
|
||||||
|
movie = movieItem.value!!,
|
||||||
|
height = 300.dp,
|
||||||
|
isWide = true,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxHeight()
|
||||||
|
.weight(0.5f)
|
||||||
|
)
|
||||||
|
MovieDetails(
|
||||||
|
movie = movieItem.value!!,
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(0.5f)
|
||||||
|
.fillMaxHeight()
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
.padding(start = contentPadding, end = contentPadding, top = 96.dp, bottom = 32.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
) {
|
||||||
|
MovieHero(
|
||||||
|
movie = movieItem.value!!,
|
||||||
|
height = 400.dp,
|
||||||
|
isWide = false,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
MovieDetails(
|
||||||
|
movie = movieItem.value!!,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = contentPadding)
|
||||||
|
.offset(y = (-48).dp)
|
||||||
|
.padding(bottom = 96.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MovieTopBar(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 16.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!isWide) {
|
||||||
|
FloatingPlayButton(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.BottomEnd)
|
||||||
|
.padding(20.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(MovieBackgroundDark),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Loading...",
|
||||||
|
color = Color.White
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package hu.bbara.purefin.app.content.movie
|
||||||
|
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
|
||||||
|
internal val MoviePrimary = Color(0xFFDDA73C)
|
||||||
|
internal val MovieBackgroundDark = Color(0xFF141414)
|
||||||
|
internal val MovieSurfaceDark = Color(0xFF1F1F1F)
|
||||||
|
internal val MovieSurfaceBorder = Color(0x1AFFFFFF)
|
||||||
|
internal val MovieMuted = Color(0x99FFFFFF)
|
||||||
|
internal val MovieMutedStrong = Color(0x66FFFFFF)
|
||||||
@@ -0,0 +1,452 @@
|
|||||||
|
package hu.bbara.purefin.app.content.movie
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
|
import androidx.compose.foundation.layout.FlowRow
|
||||||
|
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.layout.width
|
||||||
|
import androidx.compose.foundation.layout.wrapContentHeight
|
||||||
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.PlayArrow
|
||||||
|
import androidx.compose.material.icons.outlined.Add
|
||||||
|
import androidx.compose.material.icons.outlined.ArrowBack
|
||||||
|
import androidx.compose.material.icons.outlined.Cast
|
||||||
|
import androidx.compose.material.icons.outlined.ClosedCaption
|
||||||
|
import androidx.compose.material.icons.outlined.Download
|
||||||
|
import androidx.compose.material.icons.outlined.ExpandMore
|
||||||
|
import androidx.compose.material.icons.outlined.MoreVert
|
||||||
|
import androidx.compose.material.icons.outlined.Person
|
||||||
|
import androidx.compose.material.icons.outlined.Tune
|
||||||
|
import androidx.compose.material.icons.outlined.VolumeUp
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.draw.shadow
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import coil3.compose.AsyncImage
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun MovieTopBar(modifier: Modifier = Modifier) {
|
||||||
|
Row(
|
||||||
|
modifier = modifier,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
GhostIconButton(icon = Icons.Outlined.ArrowBack, contentDescription = "Back")
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||||
|
GhostIconButton(icon = Icons.Outlined.Cast, contentDescription = "Cast")
|
||||||
|
GhostIconButton(icon = Icons.Outlined.MoreVert, contentDescription = "More")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun GhostIconButton(
|
||||||
|
icon: ImageVector,
|
||||||
|
contentDescription: String,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.size(40.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(MovieBackgroundDark.copy(alpha = 0.4f))
|
||||||
|
.clickable { },
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = contentDescription,
|
||||||
|
tint = Color.White
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun MovieHero(
|
||||||
|
movie: MovieUiModel,
|
||||||
|
height: Dp,
|
||||||
|
isWide: Boolean,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.height(height)
|
||||||
|
.background(MovieBackgroundDark)
|
||||||
|
) {
|
||||||
|
AsyncImage(
|
||||||
|
model = movie.heroImageUrl,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentScale = ContentScale.Crop
|
||||||
|
)
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.matchParentSize()
|
||||||
|
.background(
|
||||||
|
Brush.verticalGradient(
|
||||||
|
colors = listOf(
|
||||||
|
Color.Transparent,
|
||||||
|
MovieBackgroundDark.copy(alpha = 0.4f),
|
||||||
|
MovieBackgroundDark
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if (isWide) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.matchParentSize()
|
||||||
|
.background(
|
||||||
|
Brush.horizontalGradient(
|
||||||
|
colors = listOf(
|
||||||
|
Color.Transparent,
|
||||||
|
MovieBackgroundDark.copy(alpha = 0.8f)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
PlayButton(size = if (isWide) 96.dp else 80.dp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
|
@Composable
|
||||||
|
internal fun MovieDetails(
|
||||||
|
movie: MovieUiModel,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Column(modifier = modifier) {
|
||||||
|
Text(
|
||||||
|
text = movie.title,
|
||||||
|
color = Color.White,
|
||||||
|
fontSize = 32.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
lineHeight = 38.sp
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
FlowRow(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
MetaChip(text = movie.year)
|
||||||
|
MetaChip(text = movie.rating)
|
||||||
|
MetaChip(text = movie.runtime)
|
||||||
|
MetaChip(
|
||||||
|
text = movie.format,
|
||||||
|
background = MoviePrimary.copy(alpha = 0.2f),
|
||||||
|
border = MoviePrimary.copy(alpha = 0.3f),
|
||||||
|
textColor = MoviePrimary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
PlaybackSettings(movie = movie)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
Text(
|
||||||
|
text = "Synopsis",
|
||||||
|
color = Color.White,
|
||||||
|
fontSize = 18.sp,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Text(
|
||||||
|
text = movie.synopsis,
|
||||||
|
color = MovieMuted,
|
||||||
|
fontSize = 15.sp,
|
||||||
|
lineHeight = 22.sp
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
ActionButtons()
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(28.dp))
|
||||||
|
Text(
|
||||||
|
text = "Cast",
|
||||||
|
color = Color.White,
|
||||||
|
fontSize = 18.sp,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
CastRow(cast = movie.cast)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun MetaChip(
|
||||||
|
text: String,
|
||||||
|
background: Color = Color.White.copy(alpha = 0.1f),
|
||||||
|
border: Color = Color.Transparent,
|
||||||
|
textColor: Color = Color.White
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.height(28.dp)
|
||||||
|
.wrapContentHeight(Alignment.CenterVertically)
|
||||||
|
.clip(RoundedCornerShape(6.dp))
|
||||||
|
.background(background)
|
||||||
|
.border(width = 1.dp, color = border, shape = RoundedCornerShape(6.dp))
|
||||||
|
.padding(horizontal = 12.dp),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
color = textColor,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun PlaybackSettings(movie: MovieUiModel) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clip(RoundedCornerShape(16.dp))
|
||||||
|
.background(MovieSurfaceDark)
|
||||||
|
.border(1.dp, MovieSurfaceBorder, RoundedCornerShape(16.dp))
|
||||||
|
.padding(20.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.Tune,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MoviePrimary
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Text(
|
||||||
|
text = "Playback Settings",
|
||||||
|
color = MovieMuted,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
letterSpacing = 2.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||||
|
SettingDropdown(
|
||||||
|
label = "Audio Track",
|
||||||
|
icon = Icons.Outlined.VolumeUp,
|
||||||
|
value = movie.audioTrack
|
||||||
|
)
|
||||||
|
SettingDropdown(
|
||||||
|
label = "Subtitles",
|
||||||
|
icon = Icons.Outlined.ClosedCaption,
|
||||||
|
value = movie.subtitles
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SettingDropdown(
|
||||||
|
label: String,
|
||||||
|
icon: ImageVector,
|
||||||
|
value: String
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
text = label,
|
||||||
|
color = MovieMutedStrong,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
modifier = Modifier.padding(start = 4.dp, bottom = 6.dp)
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(48.dp)
|
||||||
|
.clip(RoundedCornerShape(12.dp))
|
||||||
|
.background(MovieBackgroundDark)
|
||||||
|
.border(1.dp, MovieSurfaceBorder, RoundedCornerShape(12.dp))
|
||||||
|
.padding(horizontal = 16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Icon(imageVector = icon, contentDescription = null, tint = MovieMutedStrong)
|
||||||
|
Spacer(modifier = Modifier.width(10.dp))
|
||||||
|
Text(text = value, color = Color.White, fontSize = 14.sp)
|
||||||
|
}
|
||||||
|
Icon(imageVector = Icons.Outlined.ExpandMore, contentDescription = null, tint = MovieMutedStrong)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ActionButtons() {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(top = 4.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
ActionButton(
|
||||||
|
text = "Watchlist",
|
||||||
|
icon = Icons.Outlined.Add,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
ActionButton(
|
||||||
|
text = "Download",
|
||||||
|
icon = Icons.Outlined.Download,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ActionButton(
|
||||||
|
text: String,
|
||||||
|
icon: ImageVector,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = modifier
|
||||||
|
.height(48.dp)
|
||||||
|
.clip(RoundedCornerShape(12.dp))
|
||||||
|
.background(Color.White.copy(alpha = 0.05f))
|
||||||
|
.border(1.dp, MovieSurfaceBorder, RoundedCornerShape(12.dp))
|
||||||
|
.clickable { },
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
Icon(imageVector = icon, contentDescription = null, tint = Color.White)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Text(text = text, color = Color.White, fontSize = 14.sp, fontWeight = FontWeight.Bold)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun CastRow(cast: List<CastMember>) {
|
||||||
|
LazyRow(
|
||||||
|
contentPadding = PaddingValues(horizontal = 4.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
items(cast) { member ->
|
||||||
|
Column(modifier = Modifier.width(96.dp)) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.aspectRatio(4f / 5f)
|
||||||
|
.clip(RoundedCornerShape(12.dp))
|
||||||
|
.background(MovieSurfaceDark)
|
||||||
|
) {
|
||||||
|
if (member.imageUrl == null) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(Color.White.copy(alpha = 0.05f)),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.Person,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MovieMutedStrong
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
AsyncImage(
|
||||||
|
model = member.imageUrl,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentScale = ContentScale.Crop
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(6.dp))
|
||||||
|
Text(
|
||||||
|
text = member.name,
|
||||||
|
color = Color.White,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = member.role,
|
||||||
|
color = MovieMutedStrong,
|
||||||
|
fontSize = 10.sp,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun PlayButton(
|
||||||
|
size: Dp,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.size(size)
|
||||||
|
.shadow(24.dp, CircleShape)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(MoviePrimary),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.PlayArrow,
|
||||||
|
contentDescription = "Play",
|
||||||
|
tint = MovieBackgroundDark,
|
||||||
|
modifier = Modifier.size(42.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun FloatingPlayButton(modifier: Modifier = Modifier) {
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.size(56.dp)
|
||||||
|
.shadow(20.dp, CircleShape)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(MoviePrimary),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.PlayArrow,
|
||||||
|
contentDescription = "Play",
|
||||||
|
tint = MovieBackgroundDark
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package hu.bbara.purefin.app.content.movie
|
||||||
|
|
||||||
|
data class CastMember(
|
||||||
|
val name: String,
|
||||||
|
val role: String,
|
||||||
|
val imageUrl: String?
|
||||||
|
)
|
||||||
|
|
||||||
|
data class MovieUiModel(
|
||||||
|
val title: String,
|
||||||
|
val year: String,
|
||||||
|
val rating: String,
|
||||||
|
val runtime: String,
|
||||||
|
val format: String,
|
||||||
|
val synopsis: String,
|
||||||
|
val heroImageUrl: String,
|
||||||
|
val audioTrack: String,
|
||||||
|
val subtitles: String,
|
||||||
|
val cast: List<CastMember>
|
||||||
|
)
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package hu.bbara.purefin.app.content.movie
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MovieScreen(
|
||||||
|
movieId: String,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
MovieCard(
|
||||||
|
movieId = movieId,
|
||||||
|
modifier = modifier
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
private fun MovieScreenPreview() {
|
||||||
|
MovieScreen(movieId = "test")
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package hu.bbara.purefin.app.content.movie
|
||||||
|
|
||||||
|
import androidx.navigation3.runtime.EntryProviderScope
|
||||||
|
import hu.bbara.purefin.navigation.Route
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigation 3 entry definition for the Home section.
|
||||||
|
*/
|
||||||
|
fun EntryProviderScope<Route>.homeSection() {
|
||||||
|
entry<Route.Movie> {
|
||||||
|
MovieScreen(movieId = it.movieId)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
package hu.bbara.purefin.app.content.movie
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import hu.bbara.purefin.client.JellyfinApiClient
|
||||||
|
import hu.bbara.purefin.image.JellyfinImageHelper
|
||||||
|
import hu.bbara.purefin.session.UserSessionRepository
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.jellyfin.sdk.model.UUID
|
||||||
|
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||||
|
import org.jellyfin.sdk.model.api.BaseItemPerson
|
||||||
|
import org.jellyfin.sdk.model.api.ImageType
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class MovieScreenViewModel @Inject constructor(
|
||||||
|
private val jellyfinApiClient: JellyfinApiClient,
|
||||||
|
private val userSessionRepository: UserSessionRepository
|
||||||
|
): ViewModel() {
|
||||||
|
|
||||||
|
private val _movie = MutableStateFlow<MovieUiModel?>(null)
|
||||||
|
val movie = _movie.asStateFlow()
|
||||||
|
|
||||||
|
fun selectMovie(movieId: UUID) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val movieInfo = jellyfinApiClient.getItemInfo(movieId)
|
||||||
|
if (movieInfo == null) {
|
||||||
|
_movie.value = null
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
val serverUrl = userSessionRepository.serverUrl.first().trim().ifBlank {
|
||||||
|
"https://jellyfin.bbara.hu"
|
||||||
|
}
|
||||||
|
_movie.value = movieInfo.toUiModel(serverUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun BaseItemDto.toUiModel(serverUrl: String): MovieUiModel {
|
||||||
|
val year = productionYear?.toString() ?: premiereDate?.year?.toString().orEmpty()
|
||||||
|
val rating = officialRating ?: "NR"
|
||||||
|
val runtime = formatRuntime(runTimeTicks)
|
||||||
|
val format = container?.uppercase() ?: "VIDEO"
|
||||||
|
val synopsis = overview ?: "No synopsis available."
|
||||||
|
val heroImageUrl = id?.let { itemId ->
|
||||||
|
JellyfinImageHelper.toImageUrl(
|
||||||
|
url = serverUrl,
|
||||||
|
itemId = itemId,
|
||||||
|
type = ImageType.BACKDROP
|
||||||
|
)
|
||||||
|
} ?: ""
|
||||||
|
val cast = people.orEmpty().map { it.toCastMember() }
|
||||||
|
return MovieUiModel(
|
||||||
|
title = name ?: "Unknown title",
|
||||||
|
year = year,
|
||||||
|
rating = rating,
|
||||||
|
runtime = runtime,
|
||||||
|
format = format,
|
||||||
|
synopsis = synopsis,
|
||||||
|
heroImageUrl = heroImageUrl,
|
||||||
|
audioTrack = "Default",
|
||||||
|
subtitles = "Unknown",
|
||||||
|
cast = cast
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun BaseItemPerson.toCastMember(): CastMember {
|
||||||
|
return CastMember(
|
||||||
|
name = name ?: "Unknown",
|
||||||
|
role = role ?: "",
|
||||||
|
imageUrl = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun formatRuntime(ticks: Long?): String {
|
||||||
|
if (ticks == null || ticks <= 0) return "—"
|
||||||
|
val totalSeconds = ticks / 10_000_000
|
||||||
|
val hours = TimeUnit.SECONDS.toHours(totalSeconds)
|
||||||
|
val minutes = TimeUnit.SECONDS.toMinutes(totalSeconds) % 60
|
||||||
|
return if (hours > 0) {
|
||||||
|
"${hours}h ${minutes}m"
|
||||||
|
} else {
|
||||||
|
"${minutes}m"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package hu.bbara.purefin.app.home
|
||||||
|
|
||||||
|
import androidx.navigation3.runtime.EntryProviderScope
|
||||||
|
import hu.bbara.purefin.navigation.Route
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigation 3 entry definition for the Home section.
|
||||||
|
*/
|
||||||
|
fun EntryProviderScope<Route>.homeSection() {
|
||||||
|
entry<Route.Home> {
|
||||||
|
HomePage()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package hu.bbara.purefin.app
|
package hu.bbara.purefin.app.home
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
@@ -14,11 +14,11 @@ import androidx.compose.runtime.rememberCoroutineScope
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import hu.bbara.purefin.app.home.HomeContent
|
import hu.bbara.purefin.app.home.ui.HomeContent
|
||||||
import hu.bbara.purefin.app.home.HomeDrawerContent
|
import hu.bbara.purefin.app.home.ui.HomeDrawerContent
|
||||||
import hu.bbara.purefin.app.home.HomeMockData
|
import hu.bbara.purefin.app.home.ui.HomeMockData
|
||||||
import hu.bbara.purefin.app.home.HomeTopBar
|
import hu.bbara.purefin.app.home.ui.HomeTopBar
|
||||||
import hu.bbara.purefin.app.home.rememberHomeColors
|
import hu.bbara.purefin.app.home.ui.rememberHomeColors
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -1,13 +1,15 @@
|
|||||||
package hu.bbara.purefin.app
|
package hu.bbara.purefin.app.home
|
||||||
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import hu.bbara.purefin.app.home.ContinueWatchingItem
|
import hu.bbara.purefin.app.home.ui.ContinueWatchingItem
|
||||||
import hu.bbara.purefin.app.home.LibraryItem
|
import hu.bbara.purefin.app.home.ui.LibraryItem
|
||||||
import hu.bbara.purefin.app.home.PosterItem
|
import hu.bbara.purefin.app.home.ui.PosterItem
|
||||||
import hu.bbara.purefin.client.JellyfinApiClient
|
import hu.bbara.purefin.client.JellyfinApiClient
|
||||||
|
import hu.bbara.purefin.navigation.NavigationManager
|
||||||
|
import hu.bbara.purefin.navigation.Route
|
||||||
import hu.bbara.purefin.session.UserSessionRepository
|
import hu.bbara.purefin.session.UserSessionRepository
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
@@ -23,6 +25,7 @@ import javax.inject.Inject
|
|||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class HomePageViewModel @Inject constructor(
|
class HomePageViewModel @Inject constructor(
|
||||||
private val userSessionRepository: UserSessionRepository,
|
private val userSessionRepository: UserSessionRepository,
|
||||||
|
private val navigationManager: NavigationManager,
|
||||||
private val jellyfinApiClient: JellyfinApiClient
|
private val jellyfinApiClient: JellyfinApiClient
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
@@ -42,6 +45,19 @@ class HomePageViewModel @Inject constructor(
|
|||||||
loadHomePageData()
|
loadHomePageData()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun onMovieSelected(movieId: String) {
|
||||||
|
navigationManager.navigate(Route.Movie(movieId))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onBack() {
|
||||||
|
navigationManager.pop()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onGoHome() {
|
||||||
|
navigationManager.replaceAll(Route.Home)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
fun loadContinueWatching() {
|
fun loadContinueWatching() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val continueWatching: List<BaseItemDto> = jellyfinApiClient.getContinueWatching()
|
val continueWatching: List<BaseItemDto> = jellyfinApiClient.getContinueWatching()
|
||||||
@@ -103,7 +119,8 @@ class HomePageViewModel @Inject constructor(
|
|||||||
val libraryPosterItems = libraryItems.map {
|
val libraryPosterItems = libraryItems.map {
|
||||||
PosterItem(
|
PosterItem(
|
||||||
id = it.id,
|
id = it.id,
|
||||||
title = it.name ?: "Unknown"
|
title = it.name ?: "Unknown",
|
||||||
|
type = it.type
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
_libraryItems.update { currentMap ->
|
_libraryItems.update { currentMap ->
|
||||||
@@ -132,16 +149,19 @@ class HomePageViewModel @Inject constructor(
|
|||||||
when (it.type) {
|
when (it.type) {
|
||||||
BaseItemKind.MOVIE -> PosterItem(
|
BaseItemKind.MOVIE -> PosterItem(
|
||||||
id = it.id,
|
id = it.id,
|
||||||
title = it.name ?: "Unknown"
|
title = it.name ?: "Unknown",
|
||||||
|
type = it.type
|
||||||
)
|
)
|
||||||
BaseItemKind.EPISODE -> PosterItem(
|
BaseItemKind.EPISODE -> PosterItem(
|
||||||
id = it.seriesId!!,
|
id = it.seriesId!!,
|
||||||
title = it.seriesName ?: "Unknown"
|
title = it.seriesName ?: "Unknown",
|
||||||
|
type = it.type
|
||||||
)
|
)
|
||||||
|
|
||||||
BaseItemKind.SEASON -> PosterItem(
|
BaseItemKind.SEASON -> PosterItem(
|
||||||
id = it.seriesId!!,
|
id = it.seriesId!!,
|
||||||
title = it.seriesName ?: "Unknown"
|
title = it.seriesName ?: "Unknown",
|
||||||
|
type = it.type
|
||||||
)
|
)
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package hu.bbara.purefin.app.home
|
package hu.bbara.purefin.app.home.ui
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.border
|
import androidx.compose.foundation.border
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package hu.bbara.purefin.app.home
|
package hu.bbara.purefin.app.home.ui
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
@@ -12,7 +12,7 @@ import androidx.compose.runtime.getValue
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import hu.bbara.purefin.app.HomePageViewModel
|
import hu.bbara.purefin.app.home.HomePageViewModel
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun HomeContent(
|
fun HomeContent(
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package hu.bbara.purefin.app.home
|
package hu.bbara.purefin.app.home.ui
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
@@ -26,7 +26,7 @@ import androidx.compose.ui.text.style.TextOverflow
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import hu.bbara.purefin.app.HomePageViewModel
|
import hu.bbara.purefin.app.home.HomePageViewModel
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun HomeDrawerContent(
|
fun HomeDrawerContent(
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package hu.bbara.purefin.app.home
|
package hu.bbara.purefin.app.home.ui
|
||||||
|
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.outlined.Home
|
import androidx.compose.material.icons.outlined.Home
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
package hu.bbara.purefin.app.home
|
package hu.bbara.purefin.app.home.ui
|
||||||
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import org.jellyfin.sdk.model.UUID
|
import org.jellyfin.sdk.model.UUID
|
||||||
|
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||||
|
|
||||||
data class ContinueWatchingItem(
|
data class ContinueWatchingItem(
|
||||||
val id: UUID,
|
val id: UUID,
|
||||||
@@ -20,7 +21,8 @@ data class LibraryItem(
|
|||||||
|
|
||||||
data class PosterItem(
|
data class PosterItem(
|
||||||
val id: UUID,
|
val id: UUID,
|
||||||
val title: String
|
val title: String,
|
||||||
|
val type: BaseItemKind
|
||||||
)
|
)
|
||||||
|
|
||||||
data class HomeNavItem(
|
data class HomeNavItem(
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package hu.bbara.purefin.app.home
|
package hu.bbara.purefin.app.home.ui
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
@@ -36,9 +36,12 @@ import androidx.compose.ui.text.font.FontWeight
|
|||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import coil3.compose.AsyncImage
|
import coil3.compose.AsyncImage
|
||||||
|
import hu.bbara.purefin.app.home.HomePageViewModel
|
||||||
import hu.bbara.purefin.image.JellyfinImageHelper
|
import hu.bbara.purefin.image.JellyfinImageHelper
|
||||||
import hu.bbara.purefin.player.PlayerActivity
|
import hu.bbara.purefin.player.PlayerActivity
|
||||||
|
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||||
import org.jellyfin.sdk.model.api.ImageType
|
import org.jellyfin.sdk.model.api.ImageType
|
||||||
import kotlin.math.nextUp
|
import kotlin.math.nextUp
|
||||||
|
|
||||||
@@ -122,9 +125,9 @@ fun ContinueWatchingCard(
|
|||||||
Button(
|
Button(
|
||||||
modifier = Modifier.align(Alignment.BottomEnd),
|
modifier = Modifier.align(Alignment.BottomEnd),
|
||||||
onClick = {
|
onClick = {
|
||||||
val intent = Intent(context, PlayerActivity::class.java)
|
val intent = Intent(context, PlayerActivity::class.java)
|
||||||
intent.putExtra("MEDIA_ID", item.id.toString())
|
intent.putExtra("MEDIA_ID", item.id.toString())
|
||||||
context.startActivity(intent)
|
context.startActivity(intent)
|
||||||
}) {
|
}) {
|
||||||
Icon(imageVector = Icons.Outlined.PlayArrow, contentDescription = "Play")
|
Icon(imageVector = Icons.Outlined.PlayArrow, contentDescription = "Play")
|
||||||
}
|
}
|
||||||
@@ -173,7 +176,7 @@ fun LibraryPosterSection(
|
|||||||
) { item ->
|
) { item ->
|
||||||
PosterCard(
|
PosterCard(
|
||||||
item = item,
|
item = item,
|
||||||
colors = colors
|
colors = colors,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -183,8 +186,18 @@ fun LibraryPosterSection(
|
|||||||
fun PosterCard(
|
fun PosterCard(
|
||||||
item: PosterItem,
|
item: PosterItem,
|
||||||
colors: HomeColors,
|
colors: HomeColors,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier,
|
||||||
|
viewModel: HomePageViewModel = hiltViewModel()
|
||||||
) {
|
) {
|
||||||
|
fun openItem(posterItem: PosterItem)
|
||||||
|
{
|
||||||
|
when (posterItem.type) {
|
||||||
|
BaseItemKind.MOVIE -> viewModel.onMovieSelected(posterItem.id.toString())
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.width(144.dp)
|
.width(144.dp)
|
||||||
@@ -192,6 +205,7 @@ fun PosterCard(
|
|||||||
.shadow(10.dp, RoundedCornerShape(14.dp))
|
.shadow(10.dp, RoundedCornerShape(14.dp))
|
||||||
.clip(RoundedCornerShape(14.dp))
|
.clip(RoundedCornerShape(14.dp))
|
||||||
.background(colors.card)
|
.background(colors.card)
|
||||||
|
.clickable(onClick = { openItem(item) })
|
||||||
) {
|
) {
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
model = JellyfinImageHelper.toImageUrl(url = "https://jellyfin.bbara.hu", itemId = item.id, type = ImageType.PRIMARY),
|
model = JellyfinImageHelper.toImageUrl(url = "https://jellyfin.bbara.hu", itemId = item.id, type = ImageType.PRIMARY),
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package hu.bbara.purefin.app.home
|
package hu.bbara.purefin.app.home.ui
|
||||||
|
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package hu.bbara.purefin.app.home
|
package hu.bbara.purefin.app.home.ui
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
@@ -25,7 +25,7 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.compose.ui.zIndex
|
import androidx.compose.ui.zIndex
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import hu.bbara.purefin.app.HomePageViewModel
|
import hu.bbara.purefin.app.home.HomePageViewModel
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun HomeTopBar(
|
fun HomeTopBar(
|
||||||
@@ -9,6 +9,7 @@ import org.jellyfin.sdk.api.client.Response
|
|||||||
import org.jellyfin.sdk.api.client.extensions.authenticateUserByName
|
import org.jellyfin.sdk.api.client.extensions.authenticateUserByName
|
||||||
import org.jellyfin.sdk.api.client.extensions.itemsApi
|
import org.jellyfin.sdk.api.client.extensions.itemsApi
|
||||||
import org.jellyfin.sdk.api.client.extensions.mediaInfoApi
|
import org.jellyfin.sdk.api.client.extensions.mediaInfoApi
|
||||||
|
import org.jellyfin.sdk.api.client.extensions.tvShowsApi
|
||||||
import org.jellyfin.sdk.api.client.extensions.userApi
|
import org.jellyfin.sdk.api.client.extensions.userApi
|
||||||
import org.jellyfin.sdk.api.client.extensions.userLibraryApi
|
import org.jellyfin.sdk.api.client.extensions.userLibraryApi
|
||||||
import org.jellyfin.sdk.api.client.extensions.userViewsApi
|
import org.jellyfin.sdk.api.client.extensions.userViewsApi
|
||||||
@@ -24,6 +25,7 @@ import org.jellyfin.sdk.model.api.PlaybackInfoDto
|
|||||||
import org.jellyfin.sdk.model.api.SubtitleDeliveryMethod
|
import org.jellyfin.sdk.model.api.SubtitleDeliveryMethod
|
||||||
import org.jellyfin.sdk.model.api.SubtitleProfile
|
import org.jellyfin.sdk.model.api.SubtitleProfile
|
||||||
import org.jellyfin.sdk.model.api.request.GetItemsRequest
|
import org.jellyfin.sdk.model.api.request.GetItemsRequest
|
||||||
|
import org.jellyfin.sdk.model.api.request.GetNextUpRequest
|
||||||
import org.jellyfin.sdk.model.api.request.GetResumeItemsRequest
|
import org.jellyfin.sdk.model.api.request.GetResumeItemsRequest
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@@ -146,6 +148,31 @@ class JellyfinApiClient @Inject constructor(
|
|||||||
return response.content
|
return response.content
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun getItemInfo(mediaId: UUID): BaseItemDto? {
|
||||||
|
if (!ensureConfigured()) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
val result = api.userLibraryApi.getItem(
|
||||||
|
itemId = mediaId,
|
||||||
|
userId = getUserId()
|
||||||
|
)
|
||||||
|
Log.d("getItemInfo response: {}", result.content.toString())
|
||||||
|
return result.content
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getNextUpEpisode(mediaId: UUID): BaseItemDto {
|
||||||
|
if (!ensureConfigured()) {
|
||||||
|
throw IllegalStateException("Not configured")
|
||||||
|
}
|
||||||
|
val getNextUpRequest = GetNextUpRequest(
|
||||||
|
userId = getUserId(),
|
||||||
|
seriesId = mediaId,
|
||||||
|
)
|
||||||
|
val result = api.tvShowsApi.getNextUp(getNextUpRequest)
|
||||||
|
Log.d("getNextUpEpisode response: {}", result.content.toString())
|
||||||
|
return result.content.items.first()
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun getMediaSources(mediaId: UUID): List<MediaSourceInfo> {
|
suspend fun getMediaSources(mediaId: UUID): List<MediaSourceInfo> {
|
||||||
val result = api.mediaInfoApi
|
val result = api.mediaInfoApi
|
||||||
.getPostedPlaybackInfo(
|
.getPostedPlaybackInfo(
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package hu.bbara.purefin.navigation
|
||||||
|
|
||||||
|
import androidx.compose.runtime.ProvidableCompositionLocal
|
||||||
|
import androidx.compose.runtime.staticCompositionLocalOf
|
||||||
|
import kotlinx.coroutines.channels.BufferOverflow
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
|
|
||||||
|
sealed interface NavigationCommand {
|
||||||
|
data class Navigate(val route: Route) : NavigationCommand
|
||||||
|
data class ReplaceAll(val route: Route) : NavigationCommand
|
||||||
|
data object Pop : NavigationCommand
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NavigationManager {
|
||||||
|
val commands: SharedFlow<NavigationCommand>
|
||||||
|
fun navigate(route: Route)
|
||||||
|
fun replaceAll(route: Route)
|
||||||
|
fun pop()
|
||||||
|
}
|
||||||
|
|
||||||
|
class DefaultNavigationManager : NavigationManager {
|
||||||
|
private val _commands =
|
||||||
|
MutableSharedFlow<NavigationCommand>(
|
||||||
|
extraBufferCapacity = 64,
|
||||||
|
onBufferOverflow = BufferOverflow.DROP_OLDEST
|
||||||
|
)
|
||||||
|
override val commands: SharedFlow<NavigationCommand> = _commands.asSharedFlow()
|
||||||
|
|
||||||
|
override fun navigate(route: Route) {
|
||||||
|
_commands.tryEmit(NavigationCommand.Navigate(route))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun replaceAll(route: Route) {
|
||||||
|
_commands.tryEmit(NavigationCommand.ReplaceAll(route))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun pop() {
|
||||||
|
_commands.tryEmit(NavigationCommand.Pop)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val LocalNavigationManager: ProvidableCompositionLocal<NavigationManager> =
|
||||||
|
staticCompositionLocalOf { error("NavigationManager not provided") }
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package hu.bbara.purefin.navigation
|
||||||
|
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.Provides
|
||||||
|
import dagger.hilt.InstallIn
|
||||||
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Module
|
||||||
|
@InstallIn(SingletonComponent::class)
|
||||||
|
object NavigationManagerModule {
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideNavigationManager(): NavigationManager = DefaultNavigationManager()
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package hu.bbara.purefin.navigation
|
||||||
|
|
||||||
|
import androidx.navigation3.runtime.EntryProviderScope
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.Provides
|
||||||
|
import dagger.hilt.InstallIn
|
||||||
|
import dagger.hilt.android.components.ActivityRetainedComponent
|
||||||
|
import dagger.multibindings.IntoSet
|
||||||
|
|
||||||
|
@Module
|
||||||
|
@InstallIn(ActivityRetainedComponent::class)
|
||||||
|
object NavigationModule {
|
||||||
|
|
||||||
|
@IntoSet
|
||||||
|
@Provides
|
||||||
|
fun provideAppEntryBuilder(): EntryProviderScope<Route>.() -> Unit = {
|
||||||
|
appRouteEntryBuilder()
|
||||||
|
}
|
||||||
|
}
|
||||||
12
app/src/main/java/hu/bbara/purefin/navigation/Route.kt
Normal file
12
app/src/main/java/hu/bbara/purefin/navigation/Route.kt
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
package hu.bbara.purefin.navigation
|
||||||
|
|
||||||
|
import androidx.navigation3.runtime.NavKey
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
sealed interface Route : NavKey {
|
||||||
|
@Serializable
|
||||||
|
data object Home: Route
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Movie(val movieId: String) : Route
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package hu.bbara.purefin.navigation
|
||||||
|
|
||||||
|
import androidx.navigation3.runtime.EntryProviderScope
|
||||||
|
import hu.bbara.purefin.app.content.movie.MovieScreen
|
||||||
|
import hu.bbara.purefin.app.home.HomePage
|
||||||
|
|
||||||
|
fun EntryProviderScope<Route>.appRouteEntryBuilder() {
|
||||||
|
entry<Route.Home> {
|
||||||
|
HomePage()
|
||||||
|
}
|
||||||
|
entry<Route.Movie> {
|
||||||
|
MovieScreen(movieId = it.movieId)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
[versions]
|
[versions]
|
||||||
agp = "8.7.3"
|
agp = "8.9.1"
|
||||||
coreKtx = "1.15.0"
|
coreKtx = "1.15.0"
|
||||||
junit = "4.13.2"
|
junit = "4.13.2"
|
||||||
junitVersion = "1.2.1"
|
junitVersion = "1.2.1"
|
||||||
@@ -18,6 +18,7 @@ okhttp = "4.12.0"
|
|||||||
foundation = "1.10.1"
|
foundation = "1.10.1"
|
||||||
coil = "3.3.0"
|
coil = "3.3.0"
|
||||||
media3 = "1.9.0"
|
media3 = "1.9.0"
|
||||||
|
nav3Core = "1.0.0"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||||
@@ -26,6 +27,7 @@ androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "j
|
|||||||
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
|
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
|
||||||
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
|
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
|
||||||
androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycleRuntimeKtx" }
|
androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycleRuntimeKtx" }
|
||||||
|
androidx-lifecycle-viewmodel-navigation3 = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-navigation3", version.ref = "lifecycleRuntimeKtx" }
|
||||||
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
|
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
|
||||||
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
|
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
|
||||||
androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" }
|
androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" }
|
||||||
@@ -50,6 +52,8 @@ coil-network-okhttp = { group = "io.coil-kt.coil3", name = "coil-network-okhttp"
|
|||||||
medi3-exoplayer = { group = "androidx.media3", name = "media3-exoplayer", version.ref = "media3"}
|
medi3-exoplayer = { group = "androidx.media3", name = "media3-exoplayer", version.ref = "media3"}
|
||||||
medi3-ui = { group = "androidx.media3", name = "media3-ui", version.ref = "media3"}
|
medi3-ui = { group = "androidx.media3", name = "media3-ui", version.ref = "media3"}
|
||||||
medi3-ui-compose = { group = "androidx.media3", name = "media3-ui-compose-material3", version.ref = "media3"}
|
medi3-ui-compose = { group = "androidx.media3", name = "media3-ui-compose-material3", version.ref = "media3"}
|
||||||
|
androidx-navigation3-runtime = { module = "androidx.navigation3:navigation3-runtime", version.ref = "nav3Core" }
|
||||||
|
androidx-navigation3-ui = { module = "androidx.navigation3:navigation3-ui", version.ref = "nav3Core" }
|
||||||
|
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
|
|||||||
Reference in New Issue
Block a user