mirror of
https://github.com/bbara04/Purefin.git
synced 2026-03-31 17:10:08 +02:00
implement Hilt, DataStore, and Jellyfin SDK
- Setup Hilt for dependency injection and KSP for annotation processing. - Add DataStore for user session management with JSON serialization. - Integrate Jellyfin SDK and create a basic `JellyfinApiClient`. - Replace `MainActivity` with `PurefinActivity` featuring a login flow and a placeholder home screen. - Implement custom UI components: `PurefinComplexTextField`, `PurefinPasswordField`, and `PurefinTextButton`. - Update version catalog with new dependencies and downgrade SDK versions to 35.
This commit is contained in:
@@ -1,18 +1,22 @@
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
alias(libs.plugins.kotlin.compose)
|
||||
alias(libs.plugins.kotlin.serialization)
|
||||
alias(libs.plugins.hilt)
|
||||
alias(libs.plugins.ksp)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "hu.bbara.purefin"
|
||||
compileSdk {
|
||||
version = release(36)
|
||||
}
|
||||
compileSdk = 35
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "hu.bbara.purefin"
|
||||
minSdk = 29
|
||||
targetSdk = 36
|
||||
targetSdk = 35
|
||||
versionCode = 1
|
||||
versionName = "1.0"
|
||||
|
||||
@@ -23,8 +27,7 @@ android {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -37,15 +40,28 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
jvmTarget.set(JvmTarget.JVM_11)
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||
implementation(libs.androidx.lifecycle.viewmodel.compose)
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation(platform(libs.androidx.compose.bom))
|
||||
implementation(libs.androidx.compose.ui)
|
||||
implementation(libs.androidx.compose.ui.graphics)
|
||||
implementation(libs.androidx.compose.ui.tooling.preview)
|
||||
implementation(libs.androidx.compose.material3)
|
||||
implementation(libs.androidx.compose.material.icons.extended)
|
||||
implementation(libs.jellyfin.core)
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
implementation(libs.hilt)
|
||||
implementation(libs.androidx.hilt.navigation.compose)
|
||||
implementation(libs.datastore)
|
||||
testImplementation(libs.junit)
|
||||
androidTestImplementation(libs.androidx.junit)
|
||||
androidTestImplementation(libs.androidx.espresso.core)
|
||||
@@ -53,4 +69,5 @@ dependencies {
|
||||
androidTestImplementation(libs.androidx.compose.ui.test.junit4)
|
||||
debugImplementation(libs.androidx.compose.ui.tooling)
|
||||
debugImplementation(libs.androidx.compose.ui.test.manifest)
|
||||
}
|
||||
ksp(libs.hilt.compiler)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<application
|
||||
android:name=".PurefinApplication"
|
||||
android:allowBackup="true"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
@@ -12,7 +12,7 @@
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.Purefin">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:name=".PurefinActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/Theme.Purefin">
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
package hu.bbara.purefin
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import hu.bbara.purefin.ui.theme.PurefinTheme
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
enableEdgeToEdge()
|
||||
setContent {
|
||||
PurefinTheme {
|
||||
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
|
||||
Greeting(
|
||||
name = "Android",
|
||||
modifier = Modifier.padding(innerPadding)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Greeting(name: String, modifier: Modifier = Modifier) {
|
||||
Text(
|
||||
text = "Hello $name!",
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun GreetingPreview() {
|
||||
PurefinTheme {
|
||||
Greeting("Android")
|
||||
}
|
||||
}
|
||||
90
app/src/main/java/hu/bbara/purefin/PurefinActivity.kt
Normal file
90
app/src/main/java/hu/bbara/purefin/PurefinActivity.kt
Normal file
@@ -0,0 +1,90 @@
|
||||
package hu.bbara.purefin
|
||||
|
||||
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 dagger.hilt.android.AndroidEntryPoint
|
||||
import hu.bbara.purefin.login.ui.LoginScreen
|
||||
import hu.bbara.purefin.session.UserSessionRepository
|
||||
import hu.bbara.purefin.ui.theme.PurefinTheme
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class PurefinActivity : ComponentActivity() {
|
||||
|
||||
@Inject
|
||||
lateinit var userSessionRepository: UserSessionRepository
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
enableEdgeToEdge()
|
||||
setContent {
|
||||
PurefinTheme {
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val isLoggedIn by userSessionRepository.isLoggedIn.collectAsState(initial = false)
|
||||
|
||||
if (isLoggedIn) {
|
||||
HomeScreen(logout = { scope.launch { userSessionRepository.setLoggedIn(false) } })
|
||||
} else {
|
||||
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
|
||||
LoginScreen(modifier = Modifier.padding(innerPadding))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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)
|
||||
}
|
||||
}
|
||||
}
|
||||
9
app/src/main/java/hu/bbara/purefin/PurefinApplication.kt
Normal file
9
app/src/main/java/hu/bbara/purefin/PurefinApplication.kt
Normal file
@@ -0,0 +1,9 @@
|
||||
package hu.bbara.purefin
|
||||
|
||||
import android.app.Application
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
|
||||
@HiltAndroidApp
|
||||
class PurefinApplication : Application() {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package hu.bbara.purefin.client
|
||||
|
||||
import android.content.Context
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import hu.bbara.purefin.session.UserSessionRepository
|
||||
import jakarta.inject.Inject
|
||||
import org.jellyfin.sdk.api.client.extensions.authenticateUserByName
|
||||
import org.jellyfin.sdk.api.client.extensions.userApi
|
||||
import org.jellyfin.sdk.createJellyfin
|
||||
import org.jellyfin.sdk.model.ClientInfo
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
class JellyfinApiClient @Inject constructor(
|
||||
@ApplicationContext private val applicationContext: Context,
|
||||
private val userSessionRepository: UserSessionRepository,
|
||||
) {
|
||||
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())
|
||||
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)
|
||||
userSessionRepository.setLoggedIn(true)
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package hu.bbara.purefin.common.ui
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Visibility
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.material3.TextFieldDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
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.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
@Composable
|
||||
fun PurefinComplexTextField(
|
||||
label: String,
|
||||
value: String,
|
||||
onValueChange: (String) -> Unit,
|
||||
placeholder: String,
|
||||
leadingIcon: ImageVector? = null,
|
||||
trailingIcon: ImageVector? = null,
|
||||
visualTransformation: VisualTransformation = VisualTransformation.None
|
||||
) {
|
||||
val JellyfinOrange = Color(0xFFBD542E)
|
||||
val JellyfinBg = Color(0xFF141517)
|
||||
val JellyfinSurface = Color(0xFF1E2124)
|
||||
val TextSecondary = Color(0xFF9EA3A8)
|
||||
|
||||
Column(modifier = Modifier.fillMaxWidth()) {
|
||||
Text(
|
||||
text = label,
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 14.sp,
|
||||
modifier = Modifier.padding(bottom = 8.dp)
|
||||
)
|
||||
TextField(
|
||||
value = value,
|
||||
onValueChange = onValueChange,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(12.dp)),
|
||||
placeholder = { Text(placeholder, color = TextSecondary) },
|
||||
leadingIcon = if (leadingIcon != null) {
|
||||
{ Icon(leadingIcon, contentDescription = null, tint = TextSecondary) }
|
||||
} else null,
|
||||
trailingIcon = if (trailingIcon != null) {
|
||||
{ Icon(Icons.Default.Visibility, contentDescription = null, tint = TextSecondary) }
|
||||
} else null,
|
||||
visualTransformation = visualTransformation,
|
||||
colors = TextFieldDefaults.colors(
|
||||
focusedContainerColor = JellyfinSurface,
|
||||
unfocusedContainerColor = JellyfinSurface,
|
||||
focusedIndicatorColor = Color.Transparent,
|
||||
unfocusedIndicatorColor = Color.Transparent,
|
||||
cursorColor = JellyfinOrange,
|
||||
focusedTextColor = Color.White,
|
||||
unfocusedTextColor = Color.White
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package hu.bbara.purefin.common.ui
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Visibility
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.material3.TextFieldDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
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.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
@Composable
|
||||
fun PurefinPasswordField(
|
||||
label: String,
|
||||
value: String,
|
||||
onValueChange: (String) -> Unit,
|
||||
placeholder: String,
|
||||
leadingIcon: ImageVector,
|
||||
) {
|
||||
val JellyfinOrange = Color(0xFFBD542E)
|
||||
val JellyfinBg = Color(0xFF141517)
|
||||
val JellyfinSurface = Color(0xFF1E2124)
|
||||
val TextSecondary = Color(0xFF9EA3A8)
|
||||
|
||||
val showField = remember { mutableStateOf(false) }
|
||||
|
||||
Column(modifier = Modifier.fillMaxWidth()) {
|
||||
Text(
|
||||
text = label,
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 14.sp,
|
||||
modifier = Modifier.padding(bottom = 8.dp)
|
||||
)
|
||||
TextField(
|
||||
value = value,
|
||||
onValueChange = onValueChange,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(12.dp)),
|
||||
placeholder = { Text(placeholder, color = TextSecondary) },
|
||||
leadingIcon = { Icon(leadingIcon, contentDescription = null, tint = TextSecondary) },
|
||||
trailingIcon =
|
||||
{
|
||||
IconButton(
|
||||
onClick = { showField.value = !showField.value },
|
||||
) {
|
||||
Icon(Icons.Default.Visibility, contentDescription = null, tint = TextSecondary)
|
||||
}
|
||||
},
|
||||
visualTransformation = if (showField.value) VisualTransformation.None else PasswordVisualTransformation(),
|
||||
colors = TextFieldDefaults.colors(
|
||||
focusedContainerColor = JellyfinSurface,
|
||||
unfocusedContainerColor = JellyfinSurface,
|
||||
focusedIndicatorColor = Color.Transparent,
|
||||
unfocusedIndicatorColor = Color.Transparent,
|
||||
cursorColor = JellyfinOrange,
|
||||
focusedTextColor = Color.White,
|
||||
unfocusedTextColor = Color.White
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package hu.bbara.purefin.common.ui
|
||||
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
|
||||
@Composable
|
||||
fun PurefinTextButton(
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
content: @Composable RowScope.() -> Unit // Slot API
|
||||
) {
|
||||
Button(
|
||||
onClick = onClick,
|
||||
modifier = modifier,
|
||||
enabled = enabled,
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.primary
|
||||
),
|
||||
content = content
|
||||
)
|
||||
}
|
||||
178
app/src/main/java/hu/bbara/purefin/login/ui/LoginScreen.kt
Normal file
178
app/src/main/java/hu/bbara/purefin/login/ui/LoginScreen.kt
Normal file
@@ -0,0 +1,178 @@
|
||||
package hu.bbara.purefin.login.ui
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
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.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Lock
|
||||
import androidx.compose.material.icons.filled.Movie
|
||||
import androidx.compose.material.icons.filled.Person
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material.icons.filled.Storage
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
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.hilt.navigation.compose.hiltViewModel
|
||||
import hu.bbara.purefin.common.ui.PurefinComplexTextField
|
||||
import hu.bbara.purefin.common.ui.PurefinPasswordField
|
||||
import hu.bbara.purefin.common.ui.PurefinTextButton
|
||||
import hu.bbara.purefin.login.viewmodel.LoginViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun LoginScreen(
|
||||
loginViewModel: LoginViewModel = hiltViewModel(),
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val JellyfinOrange = Color(0xFFBD542E)
|
||||
val JellyfinBg = Color(0xFF141517)
|
||||
val JellyfinSurface = Color(0xFF1E2124)
|
||||
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 coroutineScope = rememberCoroutineScope()
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.background(JellyfinBg)
|
||||
.padding(24.dp)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Spacer(modifier = Modifier.weight(0.5f))
|
||||
|
||||
// Logo Section
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(100.dp)
|
||||
.background(JellyfinOrange, RoundedCornerShape(24.dp)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Movie, // Replace with actual logo resource
|
||||
contentDescription = "Logo",
|
||||
tint = Color.White,
|
||||
modifier = Modifier.size(60.dp)
|
||||
)
|
||||
}
|
||||
|
||||
Text(
|
||||
text = "Jellyfin",
|
||||
color = Color.White,
|
||||
fontSize = 32.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.padding(top = 16.dp)
|
||||
)
|
||||
Text(
|
||||
text = "PERSONAL MEDIA SYSTEM",
|
||||
color = TextSecondary,
|
||||
fontSize = 12.sp,
|
||||
letterSpacing = 2.sp
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(48.dp))
|
||||
|
||||
// Form Section
|
||||
Text(
|
||||
text = "Connect to Server",
|
||||
color = Color.White,
|
||||
fontSize = 22.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.align(Alignment.Start)
|
||||
)
|
||||
Text(
|
||||
text = "Enter your details to access your library",
|
||||
color = TextSecondary,
|
||||
fontSize = 14.sp,
|
||||
modifier = Modifier
|
||||
.align(Alignment.Start)
|
||||
.padding(bottom = 24.dp)
|
||||
)
|
||||
|
||||
PurefinComplexTextField(
|
||||
label = "Server URL",
|
||||
value = serverUrl,
|
||||
onValueChange = { coroutineScope.launch { loginViewModel.setUrl(it) } },
|
||||
placeholder = "http://192.168.1.100:8096",
|
||||
leadingIcon = Icons.Default.Storage
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
PurefinComplexTextField(
|
||||
label = "Username",
|
||||
value = username,
|
||||
onValueChange = { loginViewModel.setUsername(it) },
|
||||
placeholder = "Enter your username",
|
||||
leadingIcon = Icons.Default.Person
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
PurefinPasswordField(
|
||||
label = "Password",
|
||||
value = password,
|
||||
onValueChange = { loginViewModel.setPassword(it) },
|
||||
placeholder = "••••••••",
|
||||
leadingIcon = Icons.Default.Lock,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
|
||||
PurefinTextButton(
|
||||
content = { Text("Connect") },
|
||||
onClick = {
|
||||
coroutineScope.launch {
|
||||
loginViewModel.login()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.weight(0.5f))
|
||||
|
||||
// Footer Links
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
TextButton(onClick = {}) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(Icons.Default.Search, contentDescription = null, tint = TextSecondary, modifier = Modifier.size(18.dp))
|
||||
Text(" Discover Servers", color = TextSecondary)
|
||||
}
|
||||
}
|
||||
TextButton(onClick = {}) {
|
||||
Text("Need Help?", color = TextSecondary)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package hu.bbara.purefin.login.viewmodel
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.ViewModel
|
||||
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.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
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 {
|
||||
it.url
|
||||
}
|
||||
|
||||
suspend fun setUrl(url: String) {
|
||||
userSessionRepository.updateServerUrl(url)
|
||||
}
|
||||
|
||||
fun setUsername(username: String) {
|
||||
_username.value = username
|
||||
}
|
||||
|
||||
fun setPassword(password: String) {
|
||||
_password.value = password
|
||||
}
|
||||
|
||||
suspend fun clearFields() {
|
||||
userSessionRepository.updateServerUrl("");
|
||||
_username.value = ""
|
||||
_password.value = ""
|
||||
}
|
||||
|
||||
suspend fun login(): Boolean {
|
||||
return jellyfinApiClient.login(username.value, password.value)
|
||||
}
|
||||
|
||||
}
|
||||
10
app/src/main/java/hu/bbara/purefin/session/UserSession.kt
Normal file
10
app/src/main/java/hu/bbara/purefin/session/UserSession.kt
Normal file
@@ -0,0 +1,10 @@
|
||||
package hu.bbara.purefin.session
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class UserSession(
|
||||
val accessToken: String,
|
||||
val url: String,
|
||||
val loggedIn: Boolean
|
||||
)
|
||||
@@ -0,0 +1,40 @@
|
||||
package hu.bbara.purefin.session
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.core.DataStoreFactory
|
||||
import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler
|
||||
import androidx.datastore.dataStoreFile
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
class UserSessionModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideUserProfileDataStore(
|
||||
@ApplicationContext context: Context
|
||||
): DataStore<UserSession> {
|
||||
return DataStoreFactory.create(
|
||||
serializer = UserSessionSerializer,
|
||||
produceFile = { context.dataStoreFile("user_session.json") },
|
||||
corruptionHandler = ReplaceFileCorruptionHandler(
|
||||
produceNewData = { UserSessionSerializer.defaultValue }
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideUserSessionRepository(
|
||||
userSessionDataStore: DataStore<UserSession>
|
||||
): UserSessionRepository {
|
||||
return UserSessionRepository(userSessionDataStore)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package hu.bbara.purefin.session
|
||||
|
||||
import androidx.datastore.core.DataStore
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import javax.inject.Inject
|
||||
|
||||
class UserSessionRepository @Inject constructor(
|
||||
private val userSessionDataStore: DataStore<UserSession>
|
||||
) {
|
||||
val session: Flow<UserSession> = userSessionDataStore.data
|
||||
|
||||
val serverUrl: Flow<String> = session
|
||||
.map { it.url }
|
||||
.distinctUntilChanged()
|
||||
|
||||
suspend fun getUrl(): String = serverUrl.first()
|
||||
|
||||
val accessToken: Flow<String> = session
|
||||
.map { it.accessToken }
|
||||
.distinctUntilChanged()
|
||||
|
||||
suspend fun updateAccessToken(accessToken: String) {
|
||||
userSessionDataStore.updateData {
|
||||
it.copy(accessToken = accessToken)
|
||||
}
|
||||
}
|
||||
|
||||
val isLoggedIn: Flow<Boolean> = session.map { it.loggedIn }.distinctUntilChanged()
|
||||
|
||||
suspend fun setLoggedIn(isLoggedIn: Boolean) {
|
||||
userSessionDataStore.updateData {
|
||||
it.copy(loggedIn = isLoggedIn)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun updateServerUrl(serverUrl: String) {
|
||||
userSessionDataStore.updateData {
|
||||
it.copy(url = serverUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package hu.bbara.purefin.session
|
||||
|
||||
import androidx.datastore.core.CorruptionException
|
||||
import androidx.datastore.core.Serializer
|
||||
import kotlinx.serialization.SerializationException
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
|
||||
object UserSessionSerializer : Serializer<UserSession> {
|
||||
override val defaultValue: UserSession
|
||||
get() = UserSession(accessToken = "", url = "", loggedIn = false)
|
||||
|
||||
override suspend fun readFrom(input: InputStream): UserSession {
|
||||
try {
|
||||
return Json.decodeFromString<UserSession>(
|
||||
input.readBytes().decodeToString()
|
||||
)
|
||||
} catch (serialization: SerializationException) {
|
||||
throw CorruptionException("proto", serialization)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun writeTo(t: UserSession, output: OutputStream) {
|
||||
output.write(
|
||||
Json.encodeToString(t)
|
||||
.encodeToByteArray()
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
plugins {
|
||||
alias(libs.plugins.android.application) apply false
|
||||
alias(libs.plugins.kotlin.android) apply false
|
||||
alias(libs.plugins.kotlin.compose) apply false
|
||||
alias(libs.plugins.ksp) apply false
|
||||
alias(libs.plugins.hilt) apply false
|
||||
}
|
||||
@@ -1,13 +1,19 @@
|
||||
[versions]
|
||||
agp = "9.0.0"
|
||||
coreKtx = "1.10.1"
|
||||
agp = "8.7.3"
|
||||
coreKtx = "1.15.0"
|
||||
junit = "4.13.2"
|
||||
junitVersion = "1.1.5"
|
||||
espressoCore = "3.5.1"
|
||||
lifecycleRuntimeKtx = "2.6.1"
|
||||
activityCompose = "1.8.0"
|
||||
kotlin = "2.0.21"
|
||||
composeBom = "2024.09.00"
|
||||
junitVersion = "1.2.1"
|
||||
espressoCore = "3.6.1"
|
||||
lifecycleRuntimeKtx = "2.8.7"
|
||||
activityCompose = "1.9.3"
|
||||
kotlin = "2.1.0"
|
||||
composeBom = "2024.12.01"
|
||||
jellyfin-core = "1.8.5"
|
||||
hilt = "2.54"
|
||||
hiltNavigationCompose = "1.2.0"
|
||||
ksp = "2.1.0-1.0.29"
|
||||
datastore = "1.1.1"
|
||||
kotlinxSerializationJson = "1.7.3"
|
||||
|
||||
[libraries]
|
||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||
@@ -15,6 +21,7 @@ junit = { group = "junit", name = "junit", version.ref = "junit" }
|
||||
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
|
||||
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
|
||||
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
|
||||
androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycleRuntimeKtx" }
|
||||
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
|
||||
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
|
||||
androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" }
|
||||
@@ -24,8 +31,19 @@ androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "u
|
||||
androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
|
||||
androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
|
||||
androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" }
|
||||
androidx-compose-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended" }
|
||||
jellyfin-core = { group = "org.jellyfin.sdk", name = "jellyfin-core", version.ref = "jellyfin-core" }
|
||||
hilt = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" }
|
||||
hilt-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" }
|
||||
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" }
|
||||
|
||||
|
||||
[plugins]
|
||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
||||
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
||||
|
||||
hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
|
||||
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
|
||||
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
|
||||
|
||||
Reference in New Issue
Block a user