diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 14be7c6..bf927f4 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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) -} \ No newline at end of file + ksp(libs.hilt.compiler) +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ab5ac18..27c7a88 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,8 +1,8 @@ - + diff --git a/app/src/main/java/hu/bbara/purefin/MainActivity.kt b/app/src/main/java/hu/bbara/purefin/MainActivity.kt deleted file mode 100644 index 139141b..0000000 --- a/app/src/main/java/hu/bbara/purefin/MainActivity.kt +++ /dev/null @@ -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") - } -} \ No newline at end of file diff --git a/app/src/main/java/hu/bbara/purefin/PurefinActivity.kt b/app/src/main/java/hu/bbara/purefin/PurefinActivity.kt new file mode 100644 index 0000000..7f9f850 --- /dev/null +++ b/app/src/main/java/hu/bbara/purefin/PurefinActivity.kt @@ -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) + } + } +} diff --git a/app/src/main/java/hu/bbara/purefin/PurefinApplication.kt b/app/src/main/java/hu/bbara/purefin/PurefinApplication.kt new file mode 100644 index 0000000..cd74a4e --- /dev/null +++ b/app/src/main/java/hu/bbara/purefin/PurefinApplication.kt @@ -0,0 +1,9 @@ +package hu.bbara.purefin + +import android.app.Application +import dagger.hilt.android.HiltAndroidApp + +@HiltAndroidApp +class PurefinApplication : Application() { + +} \ No newline at end of file diff --git a/app/src/main/java/hu/bbara/purefin/client/JellyfinApiClient.kt b/app/src/main/java/hu/bbara/purefin/client/JellyfinApiClient.kt new file mode 100644 index 0000000..e6537da --- /dev/null +++ b/app/src/main/java/hu/bbara/purefin/client/JellyfinApiClient.kt @@ -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 + } + +} \ No newline at end of file diff --git a/app/src/main/java/hu/bbara/purefin/common/ui/PurefinComplexTextField.kt b/app/src/main/java/hu/bbara/purefin/common/ui/PurefinComplexTextField.kt new file mode 100644 index 0000000..5140784 --- /dev/null +++ b/app/src/main/java/hu/bbara/purefin/common/ui/PurefinComplexTextField.kt @@ -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 + )) + } +} \ No newline at end of file diff --git a/app/src/main/java/hu/bbara/purefin/common/ui/PurefinPasswordField.kt b/app/src/main/java/hu/bbara/purefin/common/ui/PurefinPasswordField.kt new file mode 100644 index 0000000..784390f --- /dev/null +++ b/app/src/main/java/hu/bbara/purefin/common/ui/PurefinPasswordField.kt @@ -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 + ) + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/hu/bbara/purefin/common/ui/PurefinTextButton.kt b/app/src/main/java/hu/bbara/purefin/common/ui/PurefinTextButton.kt new file mode 100644 index 0000000..fa0725c --- /dev/null +++ b/app/src/main/java/hu/bbara/purefin/common/ui/PurefinTextButton.kt @@ -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 + ) +} \ No newline at end of file diff --git a/app/src/main/java/hu/bbara/purefin/login/ui/LoginScreen.kt b/app/src/main/java/hu/bbara/purefin/login/ui/LoginScreen.kt new file mode 100644 index 0000000..faab80e --- /dev/null +++ b/app/src/main/java/hu/bbara/purefin/login/ui/LoginScreen.kt @@ -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)) + + } +} diff --git a/app/src/main/java/hu/bbara/purefin/login/viewmodel/LoginViewModel.kt b/app/src/main/java/hu/bbara/purefin/login/viewmodel/LoginViewModel.kt new file mode 100644 index 0000000..5485963 --- /dev/null +++ b/app/src/main/java/hu/bbara/purefin/login/viewmodel/LoginViewModel.kt @@ -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 = _username.asStateFlow() + private val _password = MutableStateFlow("") + val password: StateFlow = _password.asStateFlow() + + val url: Flow = 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) + } + +} diff --git a/app/src/main/java/hu/bbara/purefin/session/UserSession.kt b/app/src/main/java/hu/bbara/purefin/session/UserSession.kt new file mode 100644 index 0000000..77ddb40 --- /dev/null +++ b/app/src/main/java/hu/bbara/purefin/session/UserSession.kt @@ -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 +) diff --git a/app/src/main/java/hu/bbara/purefin/session/UserSessionModule.kt b/app/src/main/java/hu/bbara/purefin/session/UserSessionModule.kt new file mode 100644 index 0000000..6e13047 --- /dev/null +++ b/app/src/main/java/hu/bbara/purefin/session/UserSessionModule.kt @@ -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 { + return DataStoreFactory.create( + serializer = UserSessionSerializer, + produceFile = { context.dataStoreFile("user_session.json") }, + corruptionHandler = ReplaceFileCorruptionHandler( + produceNewData = { UserSessionSerializer.defaultValue } + ) + ) + } + + @Provides + @Singleton + fun provideUserSessionRepository( + userSessionDataStore: DataStore + ): UserSessionRepository { + return UserSessionRepository(userSessionDataStore) + } +} \ No newline at end of file diff --git a/app/src/main/java/hu/bbara/purefin/session/UserSessionRepository.kt b/app/src/main/java/hu/bbara/purefin/session/UserSessionRepository.kt new file mode 100644 index 0000000..ffd34c6 --- /dev/null +++ b/app/src/main/java/hu/bbara/purefin/session/UserSessionRepository.kt @@ -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 +) { + val session: Flow = userSessionDataStore.data + + val serverUrl: Flow = session + .map { it.url } + .distinctUntilChanged() + + suspend fun getUrl(): String = serverUrl.first() + + val accessToken: Flow = session + .map { it.accessToken } + .distinctUntilChanged() + + suspend fun updateAccessToken(accessToken: String) { + userSessionDataStore.updateData { + it.copy(accessToken = accessToken) + } + } + + val isLoggedIn: Flow = 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) + } + } +} diff --git a/app/src/main/java/hu/bbara/purefin/session/UserSessionSerializer.kt b/app/src/main/java/hu/bbara/purefin/session/UserSessionSerializer.kt new file mode 100644 index 0000000..0cf8b71 --- /dev/null +++ b/app/src/main/java/hu/bbara/purefin/session/UserSessionSerializer.kt @@ -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 { + override val defaultValue: UserSession + get() = UserSession(accessToken = "", url = "", loggedIn = false) + + override suspend fun readFrom(input: InputStream): UserSession { + try { + return Json.decodeFromString( + input.readBytes().decodeToString() + ) + } catch (serialization: SerializationException) { + throw CorruptionException("proto", serialization) + } + } + + override suspend fun writeTo(t: UserSession, output: OutputStream) { + output.write( + Json.encodeToString(t) + .encodeToByteArray() + ) + } + + +} \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 18318be..1b4e189 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -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 } \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8ddeeac..f880f1c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" }