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.androidx.hilt.navigation.compose)
|
||||
implementation(libs.datastore)
|
||||
implementation(libs.okhttp)
|
||||
implementation(libs.logging.interceptor)
|
||||
implementation(libs.androidx.compose.foundation)
|
||||
testImplementation(libs.junit)
|
||||
androidTestImplementation(libs.androidx.junit)
|
||||
androidTestImplementation(libs.androidx.espresso.core)
|
||||
|
||||
@@ -4,24 +4,12 @@ import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
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.getValue
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
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.session.UserSessionRepository
|
||||
import hu.bbara.purefin.ui.theme.PurefinTheme
|
||||
@@ -33,58 +21,26 @@ class PurefinActivity : ComponentActivity() {
|
||||
|
||||
@Inject
|
||||
lateinit var userSessionRepository: UserSessionRepository
|
||||
@Inject
|
||||
lateinit var jellyfinApiClient: JellyfinApiClient
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
lifecycleScope.launch { init() }
|
||||
enableEdgeToEdge()
|
||||
setContent {
|
||||
PurefinTheme {
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val isLoggedIn by userSessionRepository.isLoggedIn.collectAsState(initial = false)
|
||||
|
||||
if (isLoggedIn) {
|
||||
HomeScreen(logout = { scope.launch { userSessionRepository.setLoggedIn(false) } })
|
||||
HomePage()
|
||||
} else {
|
||||
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
|
||||
LoginScreen(modifier = Modifier.padding(innerPadding))
|
||||
}
|
||||
LoginScreen()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun HomeScreen(modifier: Modifier = Modifier,
|
||||
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)
|
||||
}
|
||||
private suspend fun init() {
|
||||
jellyfinApiClient.updateApiClient()
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
import android.content.Context
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import android.util.Log
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
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.itemsApi
|
||||
import org.jellyfin.sdk.api.client.extensions.libraryApi
|
||||
import org.jellyfin.sdk.api.client.extensions.userApi
|
||||
import org.jellyfin.sdk.createJellyfin
|
||||
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
|
||||
@InstallIn(SingletonComponent::class)
|
||||
@Singleton
|
||||
class JellyfinApiClient @Inject constructor(
|
||||
@ApplicationContext private val applicationContext: Context,
|
||||
private val userSessionRepository: UserSessionRepository,
|
||||
) {
|
||||
val jellyfin = createJellyfin {
|
||||
private val jellyfin = createJellyfin {
|
||||
context = applicationContext
|
||||
clientInfo = ClientInfo(name = "Purefin", version = "0.0.1")
|
||||
}
|
||||
|
||||
suspend fun login(username: String, password: String): Boolean {
|
||||
val api = jellyfin.createApi(baseUrl = userSessionRepository.getUrl())
|
||||
private val api = jellyfin.createApi()
|
||||
|
||||
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 authResult = response.content
|
||||
|
||||
//TODO set loggedIn 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)
|
||||
api.update(accessToken = token)
|
||||
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
|
||||
fun LoginScreen(
|
||||
loginViewModel: LoginViewModel = hiltViewModel(),
|
||||
viewModel: LoginViewModel = hiltViewModel(),
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val JellyfinOrange = Color(0xFFBD542E)
|
||||
@@ -51,9 +51,9 @@ fun LoginScreen(
|
||||
val TextSecondary = Color(0xFF9EA3A8)
|
||||
|
||||
// Observe ViewModel state
|
||||
val serverUrl by loginViewModel.url.collectAsState(initial = "")
|
||||
val username by loginViewModel.username.collectAsState()
|
||||
val password by loginViewModel.password.collectAsState()
|
||||
val serverUrl by viewModel.url.collectAsState("")
|
||||
val username by viewModel.username.collectAsState()
|
||||
val password by viewModel.password.collectAsState()
|
||||
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
@@ -118,7 +118,7 @@ fun LoginScreen(
|
||||
PurefinComplexTextField(
|
||||
label = "Server URL",
|
||||
value = serverUrl,
|
||||
onValueChange = { coroutineScope.launch { loginViewModel.setUrl(it) } },
|
||||
onValueChange = { coroutineScope.launch { viewModel.setUrl(it) } },
|
||||
placeholder = "http://192.168.1.100:8096",
|
||||
leadingIcon = Icons.Default.Storage
|
||||
)
|
||||
@@ -128,7 +128,7 @@ fun LoginScreen(
|
||||
PurefinComplexTextField(
|
||||
label = "Username",
|
||||
value = username,
|
||||
onValueChange = { loginViewModel.setUsername(it) },
|
||||
onValueChange = { viewModel.setUsername(it) },
|
||||
placeholder = "Enter your username",
|
||||
leadingIcon = Icons.Default.Person
|
||||
)
|
||||
@@ -138,7 +138,7 @@ fun LoginScreen(
|
||||
PurefinPasswordField(
|
||||
label = "Password",
|
||||
value = password,
|
||||
onValueChange = { loginViewModel.setPassword(it) },
|
||||
onValueChange = { viewModel.setPassword(it) },
|
||||
placeholder = "••••••••",
|
||||
leadingIcon = Icons.Default.Lock,
|
||||
)
|
||||
@@ -149,7 +149,7 @@ fun LoginScreen(
|
||||
content = { Text("Connect") },
|
||||
onClick = {
|
||||
coroutineScope.launch {
|
||||
loginViewModel.login()
|
||||
viewModel.login()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1,35 +1,40 @@
|
||||
package hu.bbara.purefin.login.viewmodel
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import hu.bbara.purefin.client.JellyfinApiClient
|
||||
import hu.bbara.purefin.session.UserSessionRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class LoginViewModel @Inject constructor(
|
||||
private val userSessionRepository: UserSessionRepository,
|
||||
private val jellyfinApiClient: JellyfinApiClient,
|
||||
@ApplicationContext private val currentContext: Context
|
||||
) : ViewModel() {
|
||||
private val _username = MutableStateFlow("")
|
||||
val username: StateFlow<String> = _username.asStateFlow()
|
||||
private val _password = MutableStateFlow("")
|
||||
val password: StateFlow<String> = _password.asStateFlow()
|
||||
|
||||
val url: Flow<String> = userSessionRepository.session.map {
|
||||
val url: StateFlow<String> = userSessionRepository.session.map {
|
||||
it.url
|
||||
}
|
||||
}.onStart { }
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(5000),
|
||||
initialValue = ""
|
||||
)
|
||||
|
||||
suspend fun setUrl(url: String) {
|
||||
userSessionRepository.updateServerUrl(url)
|
||||
userSessionRepository.setServerUrl(url)
|
||||
}
|
||||
|
||||
fun setUsername(username: String) {
|
||||
@@ -41,13 +46,13 @@ class LoginViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
suspend fun clearFields() {
|
||||
userSessionRepository.updateServerUrl("");
|
||||
userSessionRepository.setServerUrl("");
|
||||
_username.value = ""
|
||||
_password.value = ""
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.jellyfin.sdk.model.serializer.UUIDSerializer
|
||||
import java.util.UUID
|
||||
|
||||
@Serializable
|
||||
data class UserSession(
|
||||
val accessToken: String,
|
||||
val url: String,
|
||||
@Serializable(with = UUIDSerializer::class)
|
||||
val userId: UUID?,
|
||||
val loggedIn: Boolean
|
||||
)
|
||||
|
||||
@@ -5,6 +5,7 @@ import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
|
||||
class UserSessionRepository @Inject constructor(
|
||||
@@ -14,20 +15,33 @@ class UserSessionRepository @Inject constructor(
|
||||
|
||||
val serverUrl: Flow<String> = session
|
||||
.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
|
||||
.map { it.accessToken }
|
||||
.distinctUntilChanged()
|
||||
|
||||
suspend fun updateAccessToken(accessToken: String) {
|
||||
suspend fun setAccessToken(accessToken: String) {
|
||||
userSessionDataStore.updateData {
|
||||
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()
|
||||
|
||||
suspend fun setLoggedIn(isLoggedIn: Boolean) {
|
||||
@@ -35,10 +49,4 @@ class UserSessionRepository @Inject constructor(
|
||||
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> {
|
||||
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 {
|
||||
try {
|
||||
|
||||
@@ -14,6 +14,8 @@ hiltNavigationCompose = "1.2.0"
|
||||
ksp = "2.1.0-1.0.29"
|
||||
datastore = "1.1.1"
|
||||
kotlinxSerializationJson = "1.7.3"
|
||||
okhttp = "4.12.0"
|
||||
foundation = "1.10.1"
|
||||
|
||||
[libraries]
|
||||
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" }
|
||||
datastore = { group = "androidx.datastore", name = "datastore", version.ref = "datastore" }
|
||||
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]
|
||||
|
||||
Reference in New Issue
Block a user