mirror of
https://github.com/bbara04/Purefin.git
synced 2026-03-31 17:10:08 +02:00
Added HomePage Loading without Image loading
This commit is contained in:
@@ -62,6 +62,9 @@ dependencies {
|
|||||||
implementation(libs.hilt)
|
implementation(libs.hilt)
|
||||||
implementation(libs.androidx.hilt.navigation.compose)
|
implementation(libs.androidx.hilt.navigation.compose)
|
||||||
implementation(libs.datastore)
|
implementation(libs.datastore)
|
||||||
|
implementation(libs.okhttp)
|
||||||
|
implementation(libs.logging.interceptor)
|
||||||
|
implementation(libs.androidx.compose.foundation)
|
||||||
testImplementation(libs.junit)
|
testImplementation(libs.junit)
|
||||||
androidTestImplementation(libs.androidx.junit)
|
androidTestImplementation(libs.androidx.junit)
|
||||||
androidTestImplementation(libs.androidx.espresso.core)
|
androidTestImplementation(libs.androidx.espresso.core)
|
||||||
|
|||||||
@@ -4,24 +4,12 @@ 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.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
|
||||||
import androidx.compose.material3.Button
|
|
||||||
import androidx.compose.material3.Card
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Scaffold
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
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.login.ui.LoginScreen
|
import hu.bbara.purefin.login.ui.LoginScreen
|
||||||
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
|
||||||
@@ -33,58 +21,26 @@ class PurefinActivity : ComponentActivity() {
|
|||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var userSessionRepository: UserSessionRepository
|
lateinit var userSessionRepository: UserSessionRepository
|
||||||
|
@Inject
|
||||||
|
lateinit var jellyfinApiClient: JellyfinApiClient
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
lifecycleScope.launch { init() }
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
setContent {
|
setContent {
|
||||||
PurefinTheme {
|
PurefinTheme {
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
|
|
||||||
val isLoggedIn by userSessionRepository.isLoggedIn.collectAsState(initial = false)
|
val isLoggedIn by userSessionRepository.isLoggedIn.collectAsState(initial = false)
|
||||||
|
|
||||||
if (isLoggedIn) {
|
if (isLoggedIn) {
|
||||||
HomeScreen(logout = { scope.launch { userSessionRepository.setLoggedIn(false) } })
|
HomePage()
|
||||||
} else {
|
} else {
|
||||||
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
|
LoginScreen()
|
||||||
LoginScreen(modifier = Modifier.padding(innerPadding))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
private suspend fun init() {
|
||||||
fun HomeScreen(modifier: Modifier = Modifier,
|
jellyfinApiClient.updateApiClient()
|
||||||
logout: () -> Unit) {
|
|
||||||
Scaffold(modifier = modifier.fillMaxSize()) { innerPadding ->
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.padding(innerPadding)
|
|
||||||
.padding(16.dp),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
|
||||||
) {
|
|
||||||
Text("Welcome to PureFin", style = MaterialTheme.typography.headlineLarge)
|
|
||||||
LazyColumn(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
|
||||||
items(5) { index ->
|
|
||||||
StreamingCard("Show ${index + 1}", "Description for show ${index + 1}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Button(onClick = logout) {
|
|
||||||
Text("Logout")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun StreamingCard(title: String, description: String) {
|
|
||||||
Card(modifier = Modifier.fillMaxWidth()) {
|
|
||||||
Column(modifier = Modifier.padding(16.dp)) {
|
|
||||||
Text(title, style = MaterialTheme.typography.titleMedium)
|
|
||||||
Text(description, style = MaterialTheme.typography.bodySmall)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
75
app/src/main/java/hu/bbara/purefin/app/HomePage.kt
Normal file
75
app/src/main/java/hu/bbara/purefin/app/HomePage.kt
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
package hu.bbara.purefin.app
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.material3.DrawerValue
|
||||||
|
import androidx.compose.material3.ModalDrawerSheet
|
||||||
|
import androidx.compose.material3.ModalNavigationDrawer
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.rememberDrawerState
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import hu.bbara.purefin.app.home.HomeContent
|
||||||
|
import hu.bbara.purefin.app.home.HomeDrawerContent
|
||||||
|
import hu.bbara.purefin.app.home.HomeMockData
|
||||||
|
import hu.bbara.purefin.app.home.HomeTopBar
|
||||||
|
import hu.bbara.purefin.app.home.rememberHomeColors
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun HomePage(
|
||||||
|
viewModel: HomePageViewModel = hiltViewModel(),
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val colors = rememberHomeColors()
|
||||||
|
val drawerState = rememberDrawerState(DrawerValue.Closed)
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
val continueWatching = viewModel.continueWatching.collectAsState()
|
||||||
|
|
||||||
|
ModalNavigationDrawer(
|
||||||
|
drawerState = drawerState,
|
||||||
|
drawerContent = {
|
||||||
|
ModalDrawerSheet(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(280.dp)
|
||||||
|
.fillMaxSize(),
|
||||||
|
drawerContainerColor = colors.drawerBackground,
|
||||||
|
drawerContentColor = colors.textPrimary
|
||||||
|
) {
|
||||||
|
HomeDrawerContent(
|
||||||
|
title = "Jellyfin",
|
||||||
|
subtitle = "Library Dashboard",
|
||||||
|
colors = colors,
|
||||||
|
primaryNavItems = HomeMockData.primaryNavItems,
|
||||||
|
secondaryNavItems = HomeMockData.secondaryNavItems,
|
||||||
|
user = HomeMockData.user
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Scaffold(
|
||||||
|
modifier = modifier.fillMaxSize(),
|
||||||
|
containerColor = colors.background,
|
||||||
|
contentColor = colors.textPrimary,
|
||||||
|
topBar = {
|
||||||
|
HomeTopBar(
|
||||||
|
title = "Home",
|
||||||
|
colors = colors,
|
||||||
|
onMenuClick = { coroutineScope.launch { drawerState.open() } }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { innerPadding ->
|
||||||
|
HomeContent(
|
||||||
|
colors = colors,
|
||||||
|
continueWatching = continueWatching.value,
|
||||||
|
modifier = Modifier.padding(innerPadding)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
111
app/src/main/java/hu/bbara/purefin/app/HomePageViewModel.kt
Normal file
111
app/src/main/java/hu/bbara/purefin/app/HomePageViewModel.kt
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
package hu.bbara.purefin.app
|
||||||
|
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import hu.bbara.purefin.app.home.ContinueWatchingItem
|
||||||
|
import hu.bbara.purefin.app.home.LibraryItem
|
||||||
|
import hu.bbara.purefin.app.home.PosterItem
|
||||||
|
import hu.bbara.purefin.client.JellyfinApiClient
|
||||||
|
import hu.bbara.purefin.session.UserSessionRepository
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.jellyfin.sdk.model.UUID
|
||||||
|
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||||
|
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
|
import java.time.format.FormatStyle
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class HomePageViewModel @Inject constructor(
|
||||||
|
private val userSessionRepository: UserSessionRepository,
|
||||||
|
private val jellyfinApiClient: JellyfinApiClient
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
|
init {
|
||||||
|
loadHomePageData()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val _continueWatching = MutableStateFlow<List<ContinueWatchingItem>>(emptyList())
|
||||||
|
val continueWatching = _continueWatching.asStateFlow()
|
||||||
|
|
||||||
|
private val _libraries = MutableStateFlow<List<LibraryItem>>(emptyList())
|
||||||
|
val libraries = _libraries.asStateFlow()
|
||||||
|
|
||||||
|
private val _libraryContent = MutableStateFlow<Map<UUID, List<PosterItem>>>(emptyMap())
|
||||||
|
val libraryContent = _libraryContent.asStateFlow()
|
||||||
|
|
||||||
|
|
||||||
|
fun loadContinueWatching() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val continueWatching: List<BaseItemDto> = jellyfinApiClient.getContinueWatching()
|
||||||
|
_continueWatching.value = continueWatching.map {
|
||||||
|
if (it.type == BaseItemKind.EPISODE) {
|
||||||
|
ContinueWatchingItem(
|
||||||
|
primaryText = it.seriesName!!,
|
||||||
|
secondaryText = it.name!!,
|
||||||
|
progress = it.userData!!.playedPercentage!!.toFloat(),
|
||||||
|
colors = listOf(Color.Red, Color.Green)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
ContinueWatchingItem(
|
||||||
|
primaryText = it.name!!,
|
||||||
|
secondaryText = it.premiereDate!!.format(DateTimeFormatter.ofLocalizedDate(
|
||||||
|
FormatStyle.MEDIUM)),
|
||||||
|
progress = it.userData!!.playedPercentage!!.toFloat(),
|
||||||
|
colors = listOf(Color.Red, Color.Green)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadLibraries() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val libraries: List<BaseItemDto> = jellyfinApiClient.getLibraries()
|
||||||
|
val mappedLibraries = libraries.map {
|
||||||
|
LibraryItem(
|
||||||
|
name = it.name!!,
|
||||||
|
id = it.id
|
||||||
|
)
|
||||||
|
}
|
||||||
|
_libraries.value = mappedLibraries
|
||||||
|
mappedLibraries.forEach { library ->
|
||||||
|
loadLibrary(library.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadLibrary(libraryId: UUID) {
|
||||||
|
if (_libraryContent.value.containsKey(libraryId)) return
|
||||||
|
viewModelScope.launch {
|
||||||
|
val libraryItems = jellyfinApiClient.getLibrary(libraryId)
|
||||||
|
val posterItems = libraryItems.map {
|
||||||
|
PosterItem(
|
||||||
|
title = it.name ?: "Unknown",
|
||||||
|
colors = listOf(Color.Blue, Color.Cyan),
|
||||||
|
isLatest = false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
_libraryContent.update { currentMap ->
|
||||||
|
currentMap + (libraryId to posterItems)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadHomePageData() {
|
||||||
|
loadContinueWatching()
|
||||||
|
loadLibraries()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun logout() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
userSessionRepository.setLoggedIn(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
41
app/src/main/java/hu/bbara/purefin/app/home/HomeAvatar.kt
Normal file
41
app/src/main/java/hu/bbara/purefin/app/home/HomeAvatar.kt
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
package hu.bbara.purefin.app.home
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
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.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun HomeAvatar(
|
||||||
|
size: Dp,
|
||||||
|
borderWidth: Dp,
|
||||||
|
borderColor: Color,
|
||||||
|
backgroundColor: Color,
|
||||||
|
icon: ImageVector,
|
||||||
|
iconTint: Color,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.size(size)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.border(borderWidth, borderColor, CircleShape)
|
||||||
|
.background(backgroundColor),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = iconTint
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
54
app/src/main/java/hu/bbara/purefin/app/home/HomeContent.kt
Normal file
54
app/src/main/java/hu/bbara/purefin/app/home/HomeContent.kt
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
package hu.bbara.purefin.app.home
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import hu.bbara.purefin.app.HomePageViewModel
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun HomeContent(
|
||||||
|
viewModel: HomePageViewModel = hiltViewModel(),
|
||||||
|
colors: HomeColors,
|
||||||
|
continueWatching: List<ContinueWatchingItem>,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
|
||||||
|
val libraries by viewModel.libraries.collectAsState()
|
||||||
|
val libraryContent by viewModel.libraryContent.collectAsState()
|
||||||
|
|
||||||
|
LazyColumn(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(colors.background)
|
||||||
|
) {
|
||||||
|
item {
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
ContinueWatchingSection(
|
||||||
|
items = continueWatching,
|
||||||
|
colors = colors
|
||||||
|
)
|
||||||
|
}
|
||||||
|
items(libraries) { item ->
|
||||||
|
LibraryPosterSection(
|
||||||
|
title = item.name,
|
||||||
|
items = libraryContent[item.id] ?: emptyList(),
|
||||||
|
action = "See All",
|
||||||
|
colors = colors
|
||||||
|
)
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
Spacer(modifier = Modifier.height(32.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
206
app/src/main/java/hu/bbara/purefin/app/home/HomeDrawer.kt
Normal file
206
app/src/main/java/hu/bbara/purefin/app/home/HomeDrawer.kt
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
package hu.bbara.purefin.app.home
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
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.Person
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
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.graphics.Color
|
||||||
|
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.sp
|
||||||
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import hu.bbara.purefin.app.HomePageViewModel
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun HomeDrawerContent(
|
||||||
|
title: String,
|
||||||
|
subtitle: String,
|
||||||
|
colors: HomeColors,
|
||||||
|
primaryNavItems: List<HomeNavItem>,
|
||||||
|
secondaryNavItems: List<HomeNavItem>,
|
||||||
|
user: HomeUser,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onNavItemClick: (HomeNavItem) -> Unit = {}
|
||||||
|
) {
|
||||||
|
Column(modifier = modifier.fillMaxSize()) {
|
||||||
|
HomeDrawerHeader(
|
||||||
|
title = title,
|
||||||
|
subtitle = subtitle,
|
||||||
|
colors = colors
|
||||||
|
)
|
||||||
|
HomeDrawerNav(
|
||||||
|
primaryItems = primaryNavItems,
|
||||||
|
secondaryItems = secondaryNavItems,
|
||||||
|
colors = colors,
|
||||||
|
onNavItemClick = onNavItemClick
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
HomeDrawerFooter(user = user, colors = colors)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun HomeDrawerHeader(
|
||||||
|
title: String,
|
||||||
|
subtitle: String,
|
||||||
|
colors: HomeColors,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(start = 24.dp, end = 16.dp, top = 24.dp, bottom = 20.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(40.dp)
|
||||||
|
.background(colors.primary, RoundedCornerShape(12.dp)),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.PlayArrow,
|
||||||
|
contentDescription = "Play",
|
||||||
|
tint = colors.onPrimary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Column(modifier = Modifier.padding(start = 12.dp)) {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
color = colors.textPrimary,
|
||||||
|
fontSize = 20.sp,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = subtitle,
|
||||||
|
color = colors.textSecondary,
|
||||||
|
fontSize = 12.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
HorizontalDivider(color = colors.textSecondary.copy(alpha = 0.2f))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun HomeDrawerNav(
|
||||||
|
primaryItems: List<HomeNavItem>,
|
||||||
|
secondaryItems: List<HomeNavItem>,
|
||||||
|
colors: HomeColors,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onNavItemClick: (HomeNavItem) -> Unit = {}
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 16.dp)
|
||||||
|
) {
|
||||||
|
primaryItems.forEach { item ->
|
||||||
|
HomeDrawerNavItem(item = item, colors = colors) { onNavItemClick(item) }
|
||||||
|
}
|
||||||
|
if (secondaryItems.isNotEmpty()) {
|
||||||
|
HorizontalDivider(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 20.dp, vertical = 12.dp),
|
||||||
|
color = colors.divider
|
||||||
|
)
|
||||||
|
secondaryItems.forEach { item ->
|
||||||
|
HomeDrawerNavItem(item = item, colors = colors) { onNavItemClick(item) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun HomeDrawerNavItem(
|
||||||
|
item: HomeNavItem,
|
||||||
|
colors: HomeColors,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onClick: () -> Unit = {}
|
||||||
|
) {
|
||||||
|
val background = if (item.selected) colors.primary.copy(alpha = 0.12f) else Color.Transparent
|
||||||
|
val tint = if (item.selected) colors.primary else colors.textSecondary
|
||||||
|
Row(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 4.dp)
|
||||||
|
.background(background, RoundedCornerShape(12.dp))
|
||||||
|
.clickable { onClick() }
|
||||||
|
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = item.icon,
|
||||||
|
contentDescription = item.label,
|
||||||
|
tint = tint
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = item.label,
|
||||||
|
color = if (item.selected) colors.primary else colors.textPrimary,
|
||||||
|
fontSize = 15.sp,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
modifier = Modifier.padding(start = 12.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun HomeDrawerFooter (
|
||||||
|
viewModel: HomePageViewModel = hiltViewModel(),
|
||||||
|
user: HomeUser,
|
||||||
|
colors: HomeColors,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp)
|
||||||
|
.background(colors.drawerFooterBackground, RoundedCornerShape(12.dp))
|
||||||
|
.padding(12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
HomeAvatar(
|
||||||
|
size = 32.dp,
|
||||||
|
borderWidth = 1.dp,
|
||||||
|
borderColor = Color.White.copy(alpha = 0.1f),
|
||||||
|
backgroundColor = colors.avatarBackground,
|
||||||
|
icon = Icons.Outlined.Person,
|
||||||
|
iconTint = Color.White
|
||||||
|
)
|
||||||
|
Column(modifier = Modifier.padding(start = 12.dp)
|
||||||
|
.clickable {viewModel.logout()}) {
|
||||||
|
Text(
|
||||||
|
text = user.name,
|
||||||
|
color = colors.textPrimary,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = user.plan,
|
||||||
|
color = colors.textSecondary,
|
||||||
|
fontSize = 11.sp,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
24
app/src/main/java/hu/bbara/purefin/app/home/HomeMockData.kt
Normal file
24
app/src/main/java/hu/bbara/purefin/app/home/HomeMockData.kt
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
package hu.bbara.purefin.app.home
|
||||||
|
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.Home
|
||||||
|
import androidx.compose.material.icons.outlined.Movie
|
||||||
|
import androidx.compose.material.icons.outlined.Search
|
||||||
|
import androidx.compose.material.icons.outlined.Settings
|
||||||
|
import androidx.compose.material.icons.outlined.Tv
|
||||||
|
|
||||||
|
object HomeMockData {
|
||||||
|
val user = HomeUser(name = "Alex User", plan = "Premium Account")
|
||||||
|
|
||||||
|
val primaryNavItems = listOf(
|
||||||
|
HomeNavItem(label = "Home", icon = Icons.Outlined.Home, selected = true),
|
||||||
|
HomeNavItem(label = "Movies", icon = Icons.Outlined.Movie),
|
||||||
|
HomeNavItem(label = "TV Shows", icon = Icons.Outlined.Tv),
|
||||||
|
HomeNavItem(label = "Search", icon = Icons.Outlined.Search)
|
||||||
|
)
|
||||||
|
|
||||||
|
val secondaryNavItems = listOf(
|
||||||
|
HomeNavItem(label = "Settings", icon = Icons.Outlined.Settings)
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
34
app/src/main/java/hu/bbara/purefin/app/home/HomeModels.kt
Normal file
34
app/src/main/java/hu/bbara/purefin/app/home/HomeModels.kt
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
package hu.bbara.purefin.app.home
|
||||||
|
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import org.jellyfin.sdk.model.UUID
|
||||||
|
|
||||||
|
data class ContinueWatchingItem(
|
||||||
|
val primaryText: String,
|
||||||
|
val secondaryText: String,
|
||||||
|
val progress: Float,
|
||||||
|
val colors: List<Color>
|
||||||
|
)
|
||||||
|
|
||||||
|
data class LibraryItem(
|
||||||
|
val name: String,
|
||||||
|
val id: UUID
|
||||||
|
)
|
||||||
|
|
||||||
|
data class PosterItem(
|
||||||
|
val title: String,
|
||||||
|
val isLatest: Boolean,
|
||||||
|
val colors: List<Color>
|
||||||
|
)
|
||||||
|
|
||||||
|
data class HomeNavItem(
|
||||||
|
val label: String,
|
||||||
|
val icon: ImageVector,
|
||||||
|
val selected: Boolean = false
|
||||||
|
)
|
||||||
|
|
||||||
|
data class HomeUser(
|
||||||
|
val name: String,
|
||||||
|
val plan: String
|
||||||
|
)
|
||||||
238
app/src/main/java/hu/bbara/purefin/app/home/HomeSections.kt
Normal file
238
app/src/main/java/hu/bbara/purefin/app/home/HomeSections.kt
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
package hu.bbara.purefin.app.home
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
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.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
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.RoundedCornerShape
|
||||||
|
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.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ContinueWatchingSection(
|
||||||
|
items: List<ContinueWatchingItem>,
|
||||||
|
colors: HomeColors,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
SectionHeader(
|
||||||
|
title = "Continue Watching",
|
||||||
|
action = null,
|
||||||
|
colors = colors
|
||||||
|
)
|
||||||
|
LazyRow(
|
||||||
|
modifier = modifier.fillMaxWidth(),
|
||||||
|
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
items(items) { item ->
|
||||||
|
ContinueWatchingCard(
|
||||||
|
item = item,
|
||||||
|
colors = colors
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ContinueWatchingCard(
|
||||||
|
item: ContinueWatchingItem,
|
||||||
|
colors: HomeColors,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = modifier
|
||||||
|
.width(280.dp)
|
||||||
|
.wrapContentHeight()
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.aspectRatio(16f / 9f)
|
||||||
|
.shadow(12.dp, RoundedCornerShape(16.dp))
|
||||||
|
.clip(RoundedCornerShape(16.dp))
|
||||||
|
.background(colors.card)
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.matchParentSize()
|
||||||
|
.background(Brush.linearGradient(item.colors))
|
||||||
|
)
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.matchParentSize()
|
||||||
|
.background(Color.Black.copy(alpha = 0.2f))
|
||||||
|
)
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.BottomStart)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(4.dp)
|
||||||
|
.background(Color.White.copy(alpha = 0.2f))
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxHeight()
|
||||||
|
.fillMaxWidth(item.progress)
|
||||||
|
.background(colors.primary)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Column(modifier = Modifier.padding(top = 12.dp)) {
|
||||||
|
Text(
|
||||||
|
text = item.primaryText,
|
||||||
|
color = colors.textPrimary,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = item.secondaryText,
|
||||||
|
color = colors.textSecondary,
|
||||||
|
fontSize = 13.sp,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LibraryPosterSection(
|
||||||
|
title: String,
|
||||||
|
items: List<PosterItem>,
|
||||||
|
action: String?,
|
||||||
|
colors: HomeColors,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
SectionHeader(
|
||||||
|
title = title,
|
||||||
|
action = action,
|
||||||
|
colors = colors
|
||||||
|
)
|
||||||
|
LazyRow(
|
||||||
|
modifier = modifier.fillMaxWidth(),
|
||||||
|
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
items(items) { item ->
|
||||||
|
PosterCard(
|
||||||
|
item = item,
|
||||||
|
colors = colors
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PosterCard(
|
||||||
|
item: PosterItem,
|
||||||
|
colors: HomeColors,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.width(144.dp)
|
||||||
|
.aspectRatio(2f / 3f)
|
||||||
|
.shadow(10.dp, RoundedCornerShape(14.dp))
|
||||||
|
.clip(RoundedCornerShape(14.dp))
|
||||||
|
.background(colors.card)
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.matchParentSize()
|
||||||
|
.background(Brush.linearGradient(item.colors))
|
||||||
|
)
|
||||||
|
if (item.isLatest) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.TopEnd)
|
||||||
|
.padding(8.dp)
|
||||||
|
.clip(RoundedCornerShape(6.dp))
|
||||||
|
.background(colors.primary)
|
||||||
|
.padding(horizontal = 8.dp, vertical = 2.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "LATEST",
|
||||||
|
color = colors.onPrimary,
|
||||||
|
fontSize = 10.sp,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.matchParentSize()
|
||||||
|
.background(
|
||||||
|
Brush.verticalGradient(
|
||||||
|
listOf(Color.Transparent, Color.Black.copy(alpha = 0.85f))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = item.title,
|
||||||
|
color = colors.textPrimary,
|
||||||
|
fontSize = 13.sp,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.BottomStart)
|
||||||
|
.padding(10.dp),
|
||||||
|
maxLines = 2,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SectionHeader(
|
||||||
|
title: String,
|
||||||
|
action: String?,
|
||||||
|
colors: HomeColors,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onActionClick: () -> Unit = {}
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
color = colors.textPrimary,
|
||||||
|
fontSize = 20.sp,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
if (action != null) {
|
||||||
|
Text(
|
||||||
|
text = action,
|
||||||
|
color = colors.primary,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
modifier = Modifier.clickable { onActionClick() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
40
app/src/main/java/hu/bbara/purefin/app/home/HomeTokens.kt
Normal file
40
app/src/main/java/hu/bbara/purefin/app/home/HomeTokens.kt
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
package hu.bbara.purefin.app.home
|
||||||
|
|
||||||
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
|
||||||
|
data class HomeColors(
|
||||||
|
val primary: Color,
|
||||||
|
val onPrimary: Color,
|
||||||
|
val background: Color,
|
||||||
|
val drawerBackground: Color,
|
||||||
|
val card: Color,
|
||||||
|
val textPrimary: Color,
|
||||||
|
val textSecondary: Color,
|
||||||
|
val divider: Color,
|
||||||
|
val avatarBackground: Color,
|
||||||
|
val avatarBorder: Color,
|
||||||
|
val drawerFooterBackground: Color
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun rememberHomeColors(isDark: Boolean = isSystemInDarkTheme()): HomeColors {
|
||||||
|
val primary = Color(0xFFDDA73C)
|
||||||
|
return remember(isDark) {
|
||||||
|
HomeColors(
|
||||||
|
primary = primary,
|
||||||
|
onPrimary = Color(0xFF17171C),
|
||||||
|
background = if (isDark) Color(0xFF17171C) else Color(0xFFF8F7F6),
|
||||||
|
drawerBackground = if (isDark) Color(0xFF1E1E24) else Color(0xFFF8F7F6),
|
||||||
|
card = Color(0xFF24242B),
|
||||||
|
textPrimary = if (isDark) Color.White else Color(0xFF141517),
|
||||||
|
textSecondary = if (isDark) Color(0xFF9AA0A6) else Color(0xFF6B7280),
|
||||||
|
divider = if (isDark) Color.White.copy(alpha = 0.08f) else Color.Black.copy(alpha = 0.08f),
|
||||||
|
avatarBackground = Color(0xFF3A3A46),
|
||||||
|
avatarBorder = primary.copy(alpha = 0.3f),
|
||||||
|
drawerFooterBackground = if (isDark) Color.Black.copy(alpha = 0.2f) else Color.Black.copy(alpha = 0.05f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
87
app/src/main/java/hu/bbara/purefin/app/home/HomeTopBar.kt
Normal file
87
app/src/main/java/hu/bbara/purefin/app/home/HomeTopBar.kt
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
package hu.bbara.purefin.app.home
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.RowScope
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.statusBarsPadding
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.Menu
|
||||||
|
import androidx.compose.material.icons.outlined.Person
|
||||||
|
import androidx.compose.material.icons.outlined.Refresh
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.compose.ui.zIndex
|
||||||
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import hu.bbara.purefin.app.HomePageViewModel
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun HomeTopBar(
|
||||||
|
viewModel: HomePageViewModel = hiltViewModel(),
|
||||||
|
title: String,
|
||||||
|
colors: HomeColors,
|
||||||
|
onMenuClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
actions: @Composable RowScope.() -> Unit = {
|
||||||
|
HomeAvatar(
|
||||||
|
size = 36.dp,
|
||||||
|
borderWidth = 2.dp,
|
||||||
|
borderColor = colors.avatarBorder,
|
||||||
|
backgroundColor = colors.avatarBackground,
|
||||||
|
icon = Icons.Outlined.Person,
|
||||||
|
iconTint = Color.White
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(colors.background.copy(alpha = 0.95f))
|
||||||
|
.zIndex(1f)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.statusBarsPadding()
|
||||||
|
.padding(horizontal = 12.dp, vertical = 12.dp)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
IconButton(onClick = onMenuClick) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.Menu,
|
||||||
|
contentDescription = "Menu",
|
||||||
|
tint = colors.textPrimary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
color = colors.textPrimary,
|
||||||
|
fontSize = 20.sp,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
) {
|
||||||
|
Button(onClick = { viewModel.loadHomePageData() }) {
|
||||||
|
Icon(imageVector = Icons.Outlined.Refresh, contentDescription = "Refresh")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,38 +1,97 @@
|
|||||||
package hu.bbara.purefin.client
|
package hu.bbara.purefin.client
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import dagger.Module
|
import android.util.Log
|
||||||
import dagger.hilt.InstallIn
|
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import dagger.hilt.components.SingletonComponent
|
|
||||||
import hu.bbara.purefin.session.UserSessionRepository
|
import hu.bbara.purefin.session.UserSessionRepository
|
||||||
import jakarta.inject.Inject
|
import kotlinx.coroutines.flow.first
|
||||||
|
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.libraryApi
|
||||||
import org.jellyfin.sdk.api.client.extensions.userApi
|
import org.jellyfin.sdk.api.client.extensions.userApi
|
||||||
import org.jellyfin.sdk.createJellyfin
|
import org.jellyfin.sdk.createJellyfin
|
||||||
import org.jellyfin.sdk.model.ClientInfo
|
import org.jellyfin.sdk.model.ClientInfo
|
||||||
|
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||||
|
import org.jellyfin.sdk.model.api.BaseItemDtoQueryResult
|
||||||
|
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||||
|
import org.jellyfin.sdk.model.api.request.GetItemsRequest
|
||||||
|
import org.jellyfin.sdk.model.api.request.GetResumeItemsRequest
|
||||||
|
import java.util.UUID
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@Module
|
@Singleton
|
||||||
@InstallIn(SingletonComponent::class)
|
|
||||||
class JellyfinApiClient @Inject constructor(
|
class JellyfinApiClient @Inject constructor(
|
||||||
@ApplicationContext private val applicationContext: Context,
|
@ApplicationContext private val applicationContext: Context,
|
||||||
private val userSessionRepository: UserSessionRepository,
|
private val userSessionRepository: UserSessionRepository,
|
||||||
) {
|
) {
|
||||||
val jellyfin = createJellyfin {
|
private val jellyfin = createJellyfin {
|
||||||
context = applicationContext
|
context = applicationContext
|
||||||
clientInfo = ClientInfo(name = "Purefin", version = "0.0.1")
|
clientInfo = ClientInfo(name = "Purefin", version = "0.0.1")
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun login(username: String, password: String): Boolean {
|
private val api = jellyfin.createApi()
|
||||||
val api = jellyfin.createApi(baseUrl = userSessionRepository.getUrl())
|
|
||||||
|
private suspend fun getUserId(): UUID? = userSessionRepository.userId.first()
|
||||||
|
|
||||||
|
suspend fun login(url: String, username: String, password: String): Boolean {
|
||||||
|
api.update(baseUrl = url)
|
||||||
val response = api.userApi.authenticateUserByName(username = username, password = password)
|
val response = api.userApi.authenticateUserByName(username = username, password = password)
|
||||||
val authResult = response.content
|
val authResult = response.content
|
||||||
|
|
||||||
//TODO set loggedIn false?
|
|
||||||
val token = authResult.accessToken ?: return false
|
val token = authResult.accessToken ?: return false
|
||||||
userSessionRepository.updateAccessToken(accessToken = token)
|
val userId = authResult.user?.id ?: return false
|
||||||
|
userSessionRepository.setAccessToken(accessToken = token)
|
||||||
|
userSessionRepository.setUserId(userId)
|
||||||
userSessionRepository.setLoggedIn(true)
|
userSessionRepository.setLoggedIn(true)
|
||||||
|
api.update(accessToken = token)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
suspend fun updateApiClient() {
|
||||||
|
val serverUrl = userSessionRepository.serverUrl.first()
|
||||||
|
val accessToken = userSessionRepository.accessToken.first()
|
||||||
|
api.update(baseUrl = serverUrl, accessToken = accessToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getContinueWatching(): List<BaseItemDto> {
|
||||||
|
val userId = getUserId()
|
||||||
|
if (userId == null) {
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
val getResumeItemsRequest = GetResumeItemsRequest(
|
||||||
|
userId = userId,
|
||||||
|
startIndex = 0,
|
||||||
|
//TODO remove this limit if needed
|
||||||
|
limit = 10,
|
||||||
|
enableImages = false,
|
||||||
|
)
|
||||||
|
val response: Response<BaseItemDtoQueryResult> = api.itemsApi.getResumeItems(getResumeItemsRequest)
|
||||||
|
Log.d("getContinueWatching response: {}", response.content.toString())
|
||||||
|
return response.content.items
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getLibraries(): List<BaseItemDto> {
|
||||||
|
val response = api.libraryApi.getMediaFolders(isHidden = false)
|
||||||
|
Log.d("getLibraries response: {}", response.content.toString())
|
||||||
|
return response.content.items
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getLibrary(libraryId: UUID): List<BaseItemDto> {
|
||||||
|
val getItemsRequest = GetItemsRequest(
|
||||||
|
userId = getUserId(),
|
||||||
|
enableImages = false,
|
||||||
|
parentId = libraryId,
|
||||||
|
enableUserData = false,
|
||||||
|
includeItemTypes = listOf(BaseItemKind.MOVIE, BaseItemKind.SERIES),
|
||||||
|
recursive = true,
|
||||||
|
// TODO remove this limit
|
||||||
|
limit = 10
|
||||||
|
)
|
||||||
|
val response = api.itemsApi.getItems(getItemsRequest)
|
||||||
|
Log.d("getLibrary response: {}", response.content.toString())
|
||||||
|
return response.content.items
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ import kotlinx.coroutines.launch
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun LoginScreen(
|
fun LoginScreen(
|
||||||
loginViewModel: LoginViewModel = hiltViewModel(),
|
viewModel: LoginViewModel = hiltViewModel(),
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val JellyfinOrange = Color(0xFFBD542E)
|
val JellyfinOrange = Color(0xFFBD542E)
|
||||||
@@ -51,9 +51,9 @@ fun LoginScreen(
|
|||||||
val TextSecondary = Color(0xFF9EA3A8)
|
val TextSecondary = Color(0xFF9EA3A8)
|
||||||
|
|
||||||
// Observe ViewModel state
|
// Observe ViewModel state
|
||||||
val serverUrl by loginViewModel.url.collectAsState(initial = "")
|
val serverUrl by viewModel.url.collectAsState("")
|
||||||
val username by loginViewModel.username.collectAsState()
|
val username by viewModel.username.collectAsState()
|
||||||
val password by loginViewModel.password.collectAsState()
|
val password by viewModel.password.collectAsState()
|
||||||
|
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
@@ -118,7 +118,7 @@ fun LoginScreen(
|
|||||||
PurefinComplexTextField(
|
PurefinComplexTextField(
|
||||||
label = "Server URL",
|
label = "Server URL",
|
||||||
value = serverUrl,
|
value = serverUrl,
|
||||||
onValueChange = { coroutineScope.launch { loginViewModel.setUrl(it) } },
|
onValueChange = { coroutineScope.launch { viewModel.setUrl(it) } },
|
||||||
placeholder = "http://192.168.1.100:8096",
|
placeholder = "http://192.168.1.100:8096",
|
||||||
leadingIcon = Icons.Default.Storage
|
leadingIcon = Icons.Default.Storage
|
||||||
)
|
)
|
||||||
@@ -128,7 +128,7 @@ fun LoginScreen(
|
|||||||
PurefinComplexTextField(
|
PurefinComplexTextField(
|
||||||
label = "Username",
|
label = "Username",
|
||||||
value = username,
|
value = username,
|
||||||
onValueChange = { loginViewModel.setUsername(it) },
|
onValueChange = { viewModel.setUsername(it) },
|
||||||
placeholder = "Enter your username",
|
placeholder = "Enter your username",
|
||||||
leadingIcon = Icons.Default.Person
|
leadingIcon = Icons.Default.Person
|
||||||
)
|
)
|
||||||
@@ -138,7 +138,7 @@ fun LoginScreen(
|
|||||||
PurefinPasswordField(
|
PurefinPasswordField(
|
||||||
label = "Password",
|
label = "Password",
|
||||||
value = password,
|
value = password,
|
||||||
onValueChange = { loginViewModel.setPassword(it) },
|
onValueChange = { viewModel.setPassword(it) },
|
||||||
placeholder = "••••••••",
|
placeholder = "••••••••",
|
||||||
leadingIcon = Icons.Default.Lock,
|
leadingIcon = Icons.Default.Lock,
|
||||||
)
|
)
|
||||||
@@ -149,7 +149,7 @@ fun LoginScreen(
|
|||||||
content = { Text("Connect") },
|
content = { Text("Connect") },
|
||||||
onClick = {
|
onClick = {
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
loginViewModel.login()
|
viewModel.login()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,35 +1,40 @@
|
|||||||
package hu.bbara.purefin.login.viewmodel
|
package hu.bbara.purefin.login.viewmodel
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
|
||||||
import hu.bbara.purefin.client.JellyfinApiClient
|
import hu.bbara.purefin.client.JellyfinApiClient
|
||||||
import hu.bbara.purefin.session.UserSessionRepository
|
import hu.bbara.purefin.session.UserSessionRepository
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.onStart
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class LoginViewModel @Inject constructor(
|
class LoginViewModel @Inject constructor(
|
||||||
private val userSessionRepository: UserSessionRepository,
|
private val userSessionRepository: UserSessionRepository,
|
||||||
private val jellyfinApiClient: JellyfinApiClient,
|
private val jellyfinApiClient: JellyfinApiClient,
|
||||||
@ApplicationContext private val currentContext: Context
|
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
private val _username = MutableStateFlow("")
|
private val _username = MutableStateFlow("")
|
||||||
val username: StateFlow<String> = _username.asStateFlow()
|
val username: StateFlow<String> = _username.asStateFlow()
|
||||||
private val _password = MutableStateFlow("")
|
private val _password = MutableStateFlow("")
|
||||||
val password: StateFlow<String> = _password.asStateFlow()
|
val password: StateFlow<String> = _password.asStateFlow()
|
||||||
|
|
||||||
val url: Flow<String> = userSessionRepository.session.map {
|
val url: StateFlow<String> = userSessionRepository.session.map {
|
||||||
it.url
|
it.url
|
||||||
}
|
}.onStart { }
|
||||||
|
.stateIn(
|
||||||
|
scope = viewModelScope,
|
||||||
|
started = SharingStarted.WhileSubscribed(5000),
|
||||||
|
initialValue = ""
|
||||||
|
)
|
||||||
|
|
||||||
suspend fun setUrl(url: String) {
|
suspend fun setUrl(url: String) {
|
||||||
userSessionRepository.updateServerUrl(url)
|
userSessionRepository.setServerUrl(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setUsername(username: String) {
|
fun setUsername(username: String) {
|
||||||
@@ -41,13 +46,13 @@ class LoginViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun clearFields() {
|
suspend fun clearFields() {
|
||||||
userSessionRepository.updateServerUrl("");
|
userSessionRepository.setServerUrl("");
|
||||||
_username.value = ""
|
_username.value = ""
|
||||||
_password.value = ""
|
_password.value = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun login(): Boolean {
|
suspend fun login(): Boolean {
|
||||||
return jellyfinApiClient.login(username.value, password.value)
|
return jellyfinApiClient.login(url.value, username.value, password.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
package hu.bbara.purefin.session
|
package hu.bbara.purefin.session
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
import org.jellyfin.sdk.model.serializer.UUIDSerializer
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class UserSession(
|
data class UserSession(
|
||||||
val accessToken: String,
|
val accessToken: String,
|
||||||
val url: String,
|
val url: String,
|
||||||
|
@Serializable(with = UUIDSerializer::class)
|
||||||
|
val userId: UUID?,
|
||||||
val loggedIn: Boolean
|
val loggedIn: Boolean
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import kotlinx.coroutines.flow.Flow
|
|||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
import java.util.UUID
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class UserSessionRepository @Inject constructor(
|
class UserSessionRepository @Inject constructor(
|
||||||
@@ -14,20 +15,33 @@ class UserSessionRepository @Inject constructor(
|
|||||||
|
|
||||||
val serverUrl: Flow<String> = session
|
val serverUrl: Flow<String> = session
|
||||||
.map { it.url }
|
.map { it.url }
|
||||||
.distinctUntilChanged()
|
|
||||||
|
|
||||||
suspend fun getUrl(): String = serverUrl.first()
|
suspend fun setServerUrl(serverUrl: String) {
|
||||||
|
userSessionDataStore.updateData {
|
||||||
|
it.copy(url = serverUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val accessToken: Flow<String> = session
|
val accessToken: Flow<String> = session
|
||||||
.map { it.accessToken }
|
.map { it.accessToken }
|
||||||
.distinctUntilChanged()
|
|
||||||
|
|
||||||
suspend fun updateAccessToken(accessToken: String) {
|
suspend fun setAccessToken(accessToken: String) {
|
||||||
userSessionDataStore.updateData {
|
userSessionDataStore.updateData {
|
||||||
it.copy(accessToken = accessToken)
|
it.copy(accessToken = accessToken)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val userId: Flow<UUID?> = session
|
||||||
|
.map { it.userId }
|
||||||
|
|
||||||
|
suspend fun setUserId(userId: UUID?) {
|
||||||
|
userSessionDataStore.updateData {
|
||||||
|
it.copy(userId = userId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getUserId(): UUID? = userId.first()
|
||||||
|
|
||||||
val isLoggedIn: Flow<Boolean> = session.map { it.loggedIn }.distinctUntilChanged()
|
val isLoggedIn: Flow<Boolean> = session.map { it.loggedIn }.distinctUntilChanged()
|
||||||
|
|
||||||
suspend fun setLoggedIn(isLoggedIn: Boolean) {
|
suspend fun setLoggedIn(isLoggedIn: Boolean) {
|
||||||
@@ -35,10 +49,4 @@ class UserSessionRepository @Inject constructor(
|
|||||||
it.copy(loggedIn = isLoggedIn)
|
it.copy(loggedIn = isLoggedIn)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun updateServerUrl(serverUrl: String) {
|
|
||||||
userSessionDataStore.updateData {
|
|
||||||
it.copy(url = serverUrl)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import java.io.OutputStream
|
|||||||
|
|
||||||
object UserSessionSerializer : Serializer<UserSession> {
|
object UserSessionSerializer : Serializer<UserSession> {
|
||||||
override val defaultValue: UserSession
|
override val defaultValue: UserSession
|
||||||
get() = UserSession(accessToken = "", url = "", loggedIn = false)
|
get() = UserSession(accessToken = "", url = "", loggedIn = false, userId = null)
|
||||||
|
|
||||||
override suspend fun readFrom(input: InputStream): UserSession {
|
override suspend fun readFrom(input: InputStream): UserSession {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ hiltNavigationCompose = "1.2.0"
|
|||||||
ksp = "2.1.0-1.0.29"
|
ksp = "2.1.0-1.0.29"
|
||||||
datastore = "1.1.1"
|
datastore = "1.1.1"
|
||||||
kotlinxSerializationJson = "1.7.3"
|
kotlinxSerializationJson = "1.7.3"
|
||||||
|
okhttp = "4.12.0"
|
||||||
|
foundation = "1.10.1"
|
||||||
|
|
||||||
[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" }
|
||||||
@@ -38,6 +40,9 @@ hilt-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", v
|
|||||||
androidx-hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "hiltNavigationCompose" }
|
androidx-hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "hiltNavigationCompose" }
|
||||||
datastore = { group = "androidx.datastore", name = "datastore", version.ref = "datastore" }
|
datastore = { group = "androidx.datastore", name = "datastore", version.ref = "datastore" }
|
||||||
kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" }
|
kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" }
|
||||||
|
okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" }
|
||||||
|
logging-interceptor = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttp" }
|
||||||
|
androidx-compose-foundation = { group = "androidx.compose.foundation", name = "foundation", version.ref = "foundation" }
|
||||||
|
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
|
|||||||
Reference in New Issue
Block a user