diff --git a/.gitignore b/.gitignore
index aa724b7..86f1fbc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,3 +13,4 @@
.externalNativeBuild
.cxx
local.properties
+/app/src/main/java/com/example/listifyjetapp/utils/constants/Constants.kt
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
new file mode 100644
index 0000000..f0c6ad0
--- /dev/null
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
index 74dd639..b2c751a 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -1,4 +1,3 @@
-
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 1689416..f0bebce 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -5,6 +5,9 @@ plugins {
alias(libs.plugins.hilt)
alias(libs.plugins.ksp)
+
+ // Kotlin serialization plugin for type safe routes and navigation arguments
+ kotlin("plugin.serialization") version "2.0.21"
}
android {
@@ -105,4 +108,26 @@ dependencies {
// google font
implementation(libs.androidx.ui.text.google.fonts)
+ // material icons
+ implementation (libs.androidx.material.icons.extended)
+
+ // ================= Kotlin serialization =======================
+ // Jetpack Compose integration
+ implementation(libs.androidx.navigation.compose)
+
+ // Views/Fragments integration
+ implementation(libs.androidx.navigation.fragment)
+ implementation(libs.androidx.navigation.ui)
+
+ // Feature module support for Fragments
+ implementation(libs.androidx.navigation.dynamic.features.fragment)
+
+ // Testing Navigation
+ androidTestImplementation(libs.androidx.navigation.testing)
+
+ // JSON serialization library, works with the Kotlin serialization plugin
+ implementation(libs.kotlinx.serialization.json)
+
+ // Typed DataStore (Typed API surface, such as Proto)
+ implementation(libs.androidx.datastore.preferences)
}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/listifyjetapp/components/authButtons/AuthButtons.kt b/app/src/main/java/com/example/listifyjetapp/components/authButtons/AuthButtons.kt
new file mode 100644
index 0000000..01c8b0f
--- /dev/null
+++ b/app/src/main/java/com/example/listifyjetapp/components/authButtons/AuthButtons.kt
@@ -0,0 +1,100 @@
+package com.example.listifyjetapp.components.authButtons
+
+import androidx.compose.foundation.BorderStroke
+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.layout.size
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ExitToApp
+import androidx.compose.material.icons.filled.MailOutline
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.Icon
+import androidx.compose.material3.OutlinedButton
+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.navigation.NavController
+import com.example.listifyjetapp.ui.navigation.ListifyScreens
+import com.example.listifyjetapp.ui.theme.ListifyColor
+import com.example.listifyjetapp.widgets.FilledButton
+
+@Composable
+fun AuthButtons(navController: NavController) {
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(16.dp),
+ verticalArrangement = Arrangement.Bottom,
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ OutlinedButton(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 6.dp),
+ shape = RoundedCornerShape(10.dp),
+ border = BorderStroke(2.dp, color = Color.White),
+
+ onClick = { }
+ ) {
+ Icon(
+ imageVector = Icons.Default.MailOutline,
+ modifier = Modifier.size(20.dp),
+ contentDescription = "Sign In Email",
+ )
+ Text(
+ text = "SIGN UP WITH EMAIL",
+ modifier = Modifier.padding(8.dp),
+ fontWeight = FontWeight.Bold,
+ fontSize = 18.sp,
+
+ )
+ }
+
+ FilledButton(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 6.dp),
+ shape = RoundedCornerShape(10.dp),
+ containerColor = Color.White,
+ contentColor = ListifyColor.TextDark,
+ text = "LOG IN",
+ buttonIcon = Icons.AutoMirrored.Filled.ExitToApp,
+ iconDescription = "Log In",
+ onClick = { navController.navigate(ListifyScreens.LoginScreen) }
+ )
+// Button(
+// modifier = Modifier.fillMaxWidth().padding(vertical = 6.dp),
+// shape = RoundedCornerShape(10.dp),
+// colors = ButtonDefaults.buttonColors(
+// containerColor = Color.White,
+// contentColor = ListifyColor.TextDark
+// ),
+// onClick = {
+// navController.navigate(ListifyScreens.LoginScreen.route)
+// }
+// ) {
+// Icon(
+// imageVector = Icons.AutoMirrored.Filled.ExitToApp,
+// modifier = Modifier.size(20.dp),
+// contentDescription = "Log In"
+// )
+//
+// Text(
+// text = "LOG IN",
+// fontWeight = FontWeight.ExtraBold,
+// fontSize = 18.sp,
+// modifier = Modifier.padding(8.dp)
+// )
+// }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/listifyjetapp/components/inputFields/PasswordTextField.kt b/app/src/main/java/com/example/listifyjetapp/components/inputFields/PasswordTextField.kt
new file mode 100644
index 0000000..ec5a4ac
--- /dev/null
+++ b/app/src/main/java/com/example/listifyjetapp/components/inputFields/PasswordTextField.kt
@@ -0,0 +1,70 @@
+package com.example.listifyjetapp.components.inputFields
+
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.text.KeyboardActions
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Visibility
+import androidx.compose.material.icons.filled.VisibilityOff
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.text.input.PasswordVisualTransformation
+import androidx.compose.ui.text.input.VisualTransformation
+import androidx.compose.ui.unit.dp
+
+@Composable
+fun PasswordTextField(
+ password: String,
+ onPasswordChange: (String) -> Unit
+) {
+ var isShowPassword by remember { mutableStateOf(false) }
+
+ OutlinedTextField(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp),
+ value = password,
+ onValueChange = onPasswordChange,
+ label = { Text("Password") },
+ visualTransformation = if (isShowPassword) {
+ VisualTransformation.None
+ } else {
+ PasswordVisualTransformation()
+ },
+ keyboardOptions = KeyboardOptions(
+ keyboardType = KeyboardType.Password,
+ imeAction = ImeAction.Done,
+ ),
+ keyboardActions = KeyboardActions(onDone = {
+ //TODO: Login request - submit login form
+ }),
+ trailingIcon = {
+ if (isShowPassword) {
+ IconButton(onClick = { isShowPassword = false }) {
+ Icon(
+ imageVector = Icons.Filled.Visibility,
+ contentDescription = "show password"
+ )
+ }
+ } else {
+ IconButton(onClick = { isShowPassword = true }) {
+ Icon(
+ imageVector = Icons.Filled.VisibilityOff,
+ contentDescription = "hide password"
+ )
+ }
+ }
+ }
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/listifyjetapp/components/inputFields/ValidatingInputTextField.kt b/app/src/main/java/com/example/listifyjetapp/components/inputFields/ValidatingInputTextField.kt
new file mode 100644
index 0000000..26cfa9f
--- /dev/null
+++ b/app/src/main/java/com/example/listifyjetapp/components/inputFields/ValidatingInputTextField.kt
@@ -0,0 +1,37 @@
+package com.example.listifyjetapp.components.inputFields
+
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.unit.dp
+
+@Composable
+fun ValidatingInputTextField(
+ email: String,
+ onValueChange: (String) -> Unit,
+ validatorHasError: Boolean
+) {
+ OutlinedTextField(
+ modifier = Modifier.fillMaxWidth()
+ .padding(start = 16.dp, end=16.dp, top = 16.dp),
+ value = email,
+ onValueChange = onValueChange,
+ label = { Text("Email") },
+ isError = validatorHasError,
+ supportingText = {
+ if (validatorHasError) {
+ Text("Incorrect email format.")
+ }
+ },
+ keyboardOptions = KeyboardOptions(
+ imeAction = ImeAction.Next, // ** Go to next **
+ keyboardType = KeyboardType.Email
+ ),
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/listifyjetapp/data/ListifyStorage.kt b/app/src/main/java/com/example/listifyjetapp/data/ListifyStorage.kt
new file mode 100644
index 0000000..bac3dbe
--- /dev/null
+++ b/app/src/main/java/com/example/listifyjetapp/data/ListifyStorage.kt
@@ -0,0 +1,77 @@
+package com.example.listifyjetapp.data
+
+import android.content.Context
+import androidx.datastore.core.DataStore
+import androidx.datastore.preferences.core.Preferences
+import androidx.datastore.preferences.core.booleanPreferencesKey
+import androidx.datastore.preferences.core.edit
+import androidx.datastore.preferences.core.intPreferencesKey
+import androidx.datastore.preferences.core.stringPreferencesKey
+import androidx.datastore.preferences.preferencesDataStore
+import com.example.listifyjetapp.model.UserDataStore
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+
+// Create a Preferences DataStore
+val USER_DATASTORE = "user_data"
+val Context.dataStore: DataStore by preferencesDataStore(name = USER_DATASTORE)
+
+class ListifyStorageManager(val context: Context) {
+
+ private companion object {
+ val USER_ID = intPreferencesKey("userId")
+ val EMAIL = stringPreferencesKey("email")
+ val ACCESS_TOKEN = stringPreferencesKey("accessToken")
+ val REFRESH_TOKEN = stringPreferencesKey("refreshToken")
+ val IS_LOGIN = booleanPreferencesKey("isLogin")
+ }
+
+ // write a preference dataStorage - after login
+ suspend fun saveToDataStore(userDataStore: UserDataStore) {
+ context.dataStore.edit {
+ it[USER_ID] = userDataStore.userId
+ it[EMAIL] = userDataStore.email
+ it[ACCESS_TOKEN] = userDataStore.accessToken
+ it[REFRESH_TOKEN] = userDataStore.refreshToken
+ it[IS_LOGIN] = userDataStore.isLogin
+ }
+ }
+
+ // retrieve data from dataStore
+ fun getUser(): Flow = context.dataStore.data.map {
+ UserDataStore(
+ userId= it[USER_ID] ?: 0,
+ email = it[EMAIL] ?: "",
+ accessToken =it[ACCESS_TOKEN] ?: "",
+ refreshToken =it[REFRESH_TOKEN] ?: "",
+ isLogin = it[IS_LOGIN] ?: false,
+ )
+ }
+
+ val userIdFlow: Flow = context.dataStore.data.map { it[USER_ID] ?: 0 }
+ val emailFlow: Flow = context.dataStore.data.map { it[EMAIL] ?: "" }
+ val isLoggedInFlow: Flow = context.dataStore.data.map { it[IS_LOGIN] ?: false }
+
+ val accessTokenFlow: Flow = context.dataStore.data.map { it[ACCESS_TOKEN] ?: "" }
+ val refreshTokenFlow: Flow = context.dataStore.data.map { it[REFRESH_TOKEN] ?: "" }
+
+ suspend fun updateIsLoggedIn(isLoggedIn: Boolean) {
+ context.dataStore.edit { it[IS_LOGIN] = isLoggedIn }
+ }
+
+ suspend fun updateEmail(email: String) {
+ context.dataStore.edit { it[EMAIL] = email }
+ }
+
+ suspend fun updateTokens(accessToken: String, refreshToken: String) {
+ context.dataStore.edit {
+ it[ACCESS_TOKEN] = accessToken
+ it[REFRESH_TOKEN] = refreshToken
+ }
+ }
+
+ // clear data from dataStore - logout
+ suspend fun clearDataStore() = context.dataStore.edit {
+ it.clear()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/listifyjetapp/data/LoginState.kt b/app/src/main/java/com/example/listifyjetapp/data/LoginState.kt
new file mode 100644
index 0000000..5ff6e08
--- /dev/null
+++ b/app/src/main/java/com/example/listifyjetapp/data/LoginState.kt
@@ -0,0 +1,10 @@
+package com.example.listifyjetapp.data
+
+import com.example.listifyjetapp.model.LoginSuccess
+
+sealed class LoginState {
+ object Idle : LoginState()
+ object Loading : LoginState()
+ data class Success(val data: LoginSuccess) : LoginState()
+ data class Error(val message: String) : LoginState()
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/listifyjetapp/di/AppModule.kt b/app/src/main/java/com/example/listifyjetapp/di/AppModule.kt
index 569c61b..54d643f 100644
--- a/app/src/main/java/com/example/listifyjetapp/di/AppModule.kt
+++ b/app/src/main/java/com/example/listifyjetapp/di/AppModule.kt
@@ -1,11 +1,20 @@
package com.example.listifyjetapp.di
+import android.content.Context
+import com.example.listifyjetapp.data.ListifyStorageManager
import com.example.listifyjetapp.network.ListifyAPI
import com.example.listifyjetapp.utils.constants.Constants
+import dagger.Lazy
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
+import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.runBlocking
+import okhttp3.Authenticator
+import okhttp3.Interceptor
+import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import javax.inject.Singleton
@@ -14,14 +23,92 @@ import javax.inject.Singleton
@InstallIn(SingletonComponent::class)
class AppModule {
- // TODO: create a provider to build a Retrofit instance
+ // TODO: OkHttp with:
+ // - Interceptor: adds Authorization: Bearer
+ // - Authenticator: on 401 -> POST /auth/refresh with x-Refresh-Token -> save new access -> retry
@Provides
@Singleton
- fun provideListifyAPI():ListifyAPI {
+ fun provideOkHttpClient(
+ storageManager: ListifyStorageManager,
+ apiLazy: Lazy
+ ): OkHttpClient {
+ val authInterceptor = Interceptor {chain ->
+ // Interceptor is not suspend; grab the latest value synchronously:
+ val accessToken = runBlocking { storageManager.accessTokenFlow.first() }
+ val request = chain.request().newBuilder().apply {
+ if (accessToken.isNotBlank()) {
+ addHeader("Authorization", "Bearer $accessToken")
+ }
+ }.build()
+ chain.proceed(request)
+ }
+
+ val authenticator = Authenticator { route, response ->
+ // prevent infinite loops
+ if (response.request.header("Authenticator") == null) return@Authenticator null
+ // Do not try to refresh while calling refresh
+ if (response.request.url.encodedPath.endsWith("/refresh")) return@Authenticator null
+
+ try {
+ val refreshToken = runBlocking { storageManager.refreshTokenFlow.first() }
+ if (refreshToken.isBlank()) return@Authenticator null
+
+ //call refresh synchronously
+ val login = runBlocking { apiLazy.get().refresh(refreshToken) }
+
+ // save new token
+ runBlocking { storageManager.updateTokens(login.accessToken, login.refreshToken) }
+
+ // rebuild the original request with new access token
+ val newAccessToken = login.accessToken
+ return@Authenticator response.request.newBuilder()
+ .header("Authorization", "Bearer $newAccessToken")
+ .build()
+
+ } catch (_: Exception) {
+ // refresh failed -> no retry
+ null
+ }
+ }
+
+ return OkHttpClient
+ .Builder()
+ .addInterceptor(authInterceptor)
+ .authenticator(authenticator)
+ .build()
+ }
+
+
+ // TODO: Retrofit with OkHttpClient
+ @Provides
+ @Singleton
+ fun provideRetrofit(client: OkHttpClient): Retrofit {
return Retrofit.Builder()
.baseUrl(Constants.BASE_URL)
+ .client(client)
.addConverterFactory(GsonConverterFactory.create())
.build()
- .create(ListifyAPI::class.java)
}
+
+ // TODO: dataStore to read / write tokens
+ @Provides
+ @Singleton
+ fun provideDataStorageAPI(@ApplicationContext context: Context
+ ): ListifyStorageManager = ListifyStorageManager(context)
+
+ @Provides
+ @Singleton
+ fun provideListifyAPI(retrofit: Retrofit): ListifyAPI =
+ retrofit.create(ListifyAPI::class.java)
+
+// // TODO: create a provider to build a Retrofit instance
+// @Provides
+// @Singleton
+// fun provideListifyAPI():ListifyAPI {
+// return Retrofit.Builder()
+// .baseUrl(Constants.BASE_URL)
+// .addConverterFactory(GsonConverterFactory.create())
+// .build()
+// .create(ListifyAPI::class.java)
+// }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/listifyjetapp/model/AuthUser.kt b/app/src/main/java/com/example/listifyjetapp/model/AuthUser.kt
new file mode 100644
index 0000000..95c39c0
--- /dev/null
+++ b/app/src/main/java/com/example/listifyjetapp/model/AuthUser.kt
@@ -0,0 +1,31 @@
+package com.example.listifyjetapp.model
+
+import com.google.gson.annotations.SerializedName
+
+data class UserToken(
+ val id: Int,
+ @SerializedName("user_id") val userId: Int,
+ val token: String,
+ @SerializedName("expires_at") val expiresAt: String,
+ @SerializedName("created_at") val createdAt: String
+)
+
+data class UserDataStore(
+ val userId: Int,
+ val email: String,
+ val accessToken: String,
+ val refreshToken: String,
+ val isLogin: Boolean = false,
+)
+
+data class LoginInfo(
+ val email: String,
+ val password: String,
+)
+
+data class LoginSuccess(
+ val message: String,
+ val accessToken: String,
+ val refreshToken: String,
+ val user: UserWithoutPassword
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/example/listifyjetapp/model/ListModel.kt b/app/src/main/java/com/example/listifyjetapp/model/ListModel.kt
index 09016c6..d539814 100644
--- a/app/src/main/java/com/example/listifyjetapp/model/ListModel.kt
+++ b/app/src/main/java/com/example/listifyjetapp/model/ListModel.kt
@@ -1,11 +1,27 @@
package com.example.listifyjetapp.model
-import java.time.LocalDateTime
+import com.google.gson.annotations.SerializedName
+
+data class BaseList(
+ val id: Int,
+ val name: String,
+ val share: Boolean,
+ //val sharedCode: String, // can be UUID or String
+ //val createdAt: LocalDateTime
+ @SerializedName("shared_code") val sharedCode: String,
+ @SerializedName("created_at") val createdAt: String,
+)
+
+data class ListName (
+ val name: String
+)
data class ListModel(
val id: Int,
val name: String,
val share: Boolean,
- val shareCode: String, // can be UUID or String
- val createdAt: LocalDateTime
+ @SerializedName("shared_code") val sharedCode: String,
+ @SerializedName("created_at") val createdAt: String,
+ @SerializedName("item_count") val itemCount: Int,
+ @SerializedName("shared_with") val sharedWith: List
)
diff --git a/app/src/main/java/com/example/listifyjetapp/model/User.kt b/app/src/main/java/com/example/listifyjetapp/model/User.kt
new file mode 100644
index 0000000..369769e
--- /dev/null
+++ b/app/src/main/java/com/example/listifyjetapp/model/User.kt
@@ -0,0 +1,24 @@
+package com.example.listifyjetapp.model
+
+import com.google.gson.annotations.SerializedName
+
+data class User(
+ val id: Int,
+ val username: String,
+ val email: String,
+ val password: String,
+ @SerializedName("created_at") val createdAt: String
+)
+
+data class UserWithoutPassword(
+ val id: Int,
+ val username: String,
+ val email: String,
+ @SerializedName("created_at") val createdAt: String
+)
+
+data class SharedUsers(
+ @SerializedName("user_id") val userId: Int,
+ val username: String,
+)
+
diff --git a/app/src/main/java/com/example/listifyjetapp/network/ListsAPI.kt b/app/src/main/java/com/example/listifyjetapp/network/ListsAPI.kt
index c408725..b8b18af 100644
--- a/app/src/main/java/com/example/listifyjetapp/network/ListsAPI.kt
+++ b/app/src/main/java/com/example/listifyjetapp/network/ListsAPI.kt
@@ -1,36 +1,50 @@
package com.example.listifyjetapp.network
import com.example.listifyjetapp.model.ListModel
+import com.example.listifyjetapp.model.ListName
+import com.example.listifyjetapp.model.LoginInfo
+import com.example.listifyjetapp.model.LoginSuccess
+import com.example.listifyjetapp.model.User
+import retrofit2.http.Body
import retrofit2.http.DELETE
import retrofit2.http.GET
+import retrofit2.http.Header
+import retrofit2.http.PATCH
import retrofit2.http.POST
import retrofit2.http.Path
import javax.inject.Singleton
@Singleton
interface ListifyAPI {
+ // =============================================== Auth ========================================
+ @POST("auth/login")
+ suspend fun login(@Body request: LoginInfo): LoginSuccess
+
+ @POST("refresh")
+ suspend fun refresh(@Header("x-refresh-token") refreshToken: String): LoginSuccess
+
// =============================================== Users =======================================
-// @PATCH("users/{user_id}")
-// suspend fun patchUserById(@Path("user_id") userId: Int): User
-//
-// @POST("users")
-// suspend fun createUser(): User
-//
-// @GET("users/check_username/{username}")
-// suspend fun getUserByUsername(@Path("/username") username: String ): User
-//
-// @GET("users/email/{email}")
-// suspend fun getUserByUserEmail(@Path("/email") email: String ): User
-//
-// @GET("users/users")
-// suspend fun getAllUsers(): List
+ @PATCH("users/{user_id}")
+ suspend fun patchUserById(@Path("user_id") userId: Int): User
+
+ @POST("users")
+ suspend fun createUser(): User
+
+ @GET("users/check_username/{username}")
+ suspend fun getUserByUsername(@Path("/username") username: String ): User
+
+ @GET("users/email/{email}")
+ suspend fun getUserByUserEmail(@Path("/email") email: String ): User
+
+ @GET("users/users")
+ suspend fun getAllUsers(): List
// =============================================== User Lists ==================================
-// @GET("ul/shared-user/{list_id}/{user_id}")
-// suspend fun getSharedUsers(
-// @Path("user_id") userId: Int,
-// @Path("list_id") listId: Int
-// ): List
+ @GET("ul/shared-user/{list_id}/{user_id}")
+ suspend fun getSharedUsers(
+ @Path("user_id") userId: Int,
+ @Path("list_id") listId: Int
+ ): List
@GET("ul/{user_id}")
suspend fun getListsByUser(@Path("user_id") userId: Int): List
@@ -56,7 +70,10 @@ interface ListifyAPI {
// Post new list
@POST("lists/{user_id}")
- suspend fun createList(@Path("user_id") userId: Int) {}
+ suspend fun insertList(
+ @Path("user_id") userId: Int,
+ @Body request: ListName
+ ): ListModel
// Get items by list id
@GET("lists/{list_id}")
diff --git a/app/src/main/java/com/example/listifyjetapp/repository/AuthUserRepository.kt b/app/src/main/java/com/example/listifyjetapp/repository/AuthUserRepository.kt
new file mode 100644
index 0000000..64741a7
--- /dev/null
+++ b/app/src/main/java/com/example/listifyjetapp/repository/AuthUserRepository.kt
@@ -0,0 +1,18 @@
+package com.example.listifyjetapp.repository
+
+import com.example.listifyjetapp.data.ListifyResult
+import com.example.listifyjetapp.model.LoginInfo
+import com.example.listifyjetapp.model.LoginSuccess
+import com.example.listifyjetapp.network.ListifyAPI
+import javax.inject.Inject
+
+class AuthUserRepository @Inject constructor(private val api: ListifyAPI){
+ suspend fun login(loginInfo: LoginInfo): ListifyResult {
+ try {
+ val response = api.login(loginInfo)
+ return ListifyResult.Success(data = response)
+ } catch (e: Exception) {
+ return ListifyResult.Failure(e.message ?: "Error logging in")
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/listifyjetapp/repository/ListsRepository.kt b/app/src/main/java/com/example/listifyjetapp/repository/ListsRepository.kt
index 0130db4..f54e1e2 100644
--- a/app/src/main/java/com/example/listifyjetapp/repository/ListsRepository.kt
+++ b/app/src/main/java/com/example/listifyjetapp/repository/ListsRepository.kt
@@ -2,6 +2,7 @@ package com.example.listifyjetapp.repository
import com.example.listifyjetapp.data.ListifyResult
import com.example.listifyjetapp.model.ListModel
+import com.example.listifyjetapp.model.ListName
import com.example.listifyjetapp.network.ListifyAPI
import javax.inject.Inject
@@ -15,4 +16,22 @@ class ListsRepository @Inject constructor(private val api: ListifyAPI) {
return ListifyResult.Failure(e.message ?: "Error fetching lists")
}
}
+
+ suspend fun insertListByUser(userId: Int, newListData: ListName): ListifyResult {
+ try {
+ val response = api.insertList(userId, newListData)
+ val formatedResponse = ListModel (
+ id = response.id,
+ name = response.name,
+ share = response.share,
+ sharedCode = response.sharedCode,
+ createdAt = response.createdAt,
+ itemCount = 0,
+ sharedWith= emptyList()
+ )
+ return ListifyResult.Success(data = formatedResponse)
+ } catch (e:Exception) {
+ return ListifyResult.Failure(e.message ?: "Error creating new list")
+ }
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/listifyjetapp/ui/navigation/ListifyNavigation.kt b/app/src/main/java/com/example/listifyjetapp/ui/navigation/ListifyNavigation.kt
index 039c9fa..801d87d 100644
--- a/app/src/main/java/com/example/listifyjetapp/ui/navigation/ListifyNavigation.kt
+++ b/app/src/main/java/com/example/listifyjetapp/ui/navigation/ListifyNavigation.kt
@@ -4,7 +4,9 @@ import androidx.compose.runtime.Composable
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
+import com.example.listifyjetapp.ui.screens.auth.ListifyLoginScreen
import com.example.listifyjetapp.ui.screens.lists.ListifyListsScreen
+import com.example.listifyjetapp.ui.screens.newList.ListifyNewListScreen
import com.example.listifyjetapp.ui.screens.splash.ListifySplashScreen
@@ -16,36 +18,41 @@ fun ListifyNavigation() {
// Build the navigation graph
NavHost(
navController = navController,
- startDestination = ListifyScreens.SplashScreen.route
+ startDestination = ListifyScreens.SplashScreen
) {
// TODO: Define a navigation route for SplashScreen
- composable(ListifyScreens.SplashScreen.route) {
+ composable() {
ListifySplashScreen(navController = navController)
}
// TODO: Define a navigation route for ListsScreen
- composable(ListifyScreens.ListsScreen.route) {
- ListifyListsScreen()
+ composable() {
+ ListifyListsScreen(navController = navController)
+ }
+
+ // TODO: Define a navigation route for NewListScreen
+ composable() {
+ ListifyNewListScreen(navController = navController)
}
// TODO: Define a navigation route for DetailScreen
- composable(ListifyScreens.DetailScreen.route) {
+ composable() {
//ListifyDetailScreen(navController = navController)
}
// TODO: Define a navigation route for ProfileScreen
- composable(ListifyScreens.ProfileScreen.route) {
+ composable() {
//ListifyProfileScreen(navController = navController)
}
// TODO: Define a navigation route for LoginScreen
- composable(ListifyScreens.LoginScreen.route) {
- //ListifyLoginScreen(navController = navController)
+ composable() {
+ ListifyLoginScreen(navController = navController)
}
// TODO: Define a navigation route for SignupScreen
- composable(ListifyScreens.SignupScreen.route) {
+ composable() {
//ListifySignupScreen(navController = navController)
}
}
diff --git a/app/src/main/java/com/example/listifyjetapp/ui/navigation/ListifyScreens.kt b/app/src/main/java/com/example/listifyjetapp/ui/navigation/ListifyScreens.kt
index b1914cb..700dda2 100644
--- a/app/src/main/java/com/example/listifyjetapp/ui/navigation/ListifyScreens.kt
+++ b/app/src/main/java/com/example/listifyjetapp/ui/navigation/ListifyScreens.kt
@@ -1,12 +1,14 @@
package com.example.listifyjetapp.ui.navigation
-enum class ListifyScreens(val route: String) {
- SplashScreen("splash"),
- ListsScreen("lists"),
- DetailScreen("detail"),
- ProfileScreen("profile"),
- LoginScreen("login"),
- SignupScreen("signup")
-}
+import kotlinx.serialization.Serializable
-// navController.navigate(ListifyScreens.LoginScreen.route)
\ No newline at end of file
+
+object ListifyScreens {
+ @Serializable object SplashScreen
+ @Serializable data class ListsScreen(val userId: Int)
+ @Serializable object DetailScreen
+ @Serializable object ProfileScreen
+ @Serializable object LoginScreen
+ @Serializable object SignupScreen
+ @Serializable object NewListScreen
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/listifyjetapp/ui/screens/auth/EmailViewModel.kt b/app/src/main/java/com/example/listifyjetapp/ui/screens/auth/EmailViewModel.kt
new file mode 100644
index 0000000..f5c1fe7
--- /dev/null
+++ b/app/src/main/java/com/example/listifyjetapp/ui/screens/auth/EmailViewModel.kt
@@ -0,0 +1,25 @@
+package com.example.listifyjetapp.ui.screens.auth
+
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.lifecycle.ViewModel
+
+class EmailViewModel: ViewModel() {
+ var email by mutableStateOf("")
+ private set // private set ensures that the email can only be modified via updateEmail()
+
+ val emailHasErrors by derivedStateOf {
+ if (email.isNotEmpty()) {
+ // Email is considered erroneous until it completely matches EMAIL_ADDRESS.
+ !android.util.Patterns.EMAIL_ADDRESS.matcher(email).matches()
+ } else {
+ false
+ }
+ }
+
+ fun updateEmail(input: String) {
+ email = input
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/listifyjetapp/ui/screens/auth/ListifyLoginScreen.kt b/app/src/main/java/com/example/listifyjetapp/ui/screens/auth/ListifyLoginScreen.kt
new file mode 100644
index 0000000..9a982c3
--- /dev/null
+++ b/app/src/main/java/com/example/listifyjetapp/ui/screens/auth/ListifyLoginScreen.kt
@@ -0,0 +1,105 @@
+package com.example.listifyjetapp.ui.screens.auth
+
+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.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ExitToApp
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.navigation.NavHostController
+import com.example.listifyjetapp.components.inputFields.PasswordTextField
+import com.example.listifyjetapp.widgets.ListifyTopBar
+import com.example.listifyjetapp.components.inputFields.ValidatingInputTextField
+import com.example.listifyjetapp.data.LoginState
+import com.example.listifyjetapp.ui.navigation.ListifyScreens
+import com.example.listifyjetapp.ui.theme.ListifyColor
+import com.example.listifyjetapp.widgets.FilledButton
+
+@Composable
+fun ListifyLoginScreen(
+ navController: NavHostController,
+ loginViewModel: LoginViewModel = hiltViewModel()
+) {
+
+ val loginState = loginViewModel.loginState
+
+ fun onLoginClick() {
+ if (loginViewModel.email.isNotBlank() && loginViewModel.password.isNotBlank()) {
+ loginViewModel.login()
+ }
+ }
+
+ Scaffold(
+ modifier = Modifier.fillMaxSize(),
+ topBar = { ListifyTopBar(
+ title = "LOG IN",
+ isListsScreen = false,
+ onGoBackButtonClicked = {navController.popBackStack()},
+ leftText = "CANCEL",
+ ) }
+ ) { innerPadding ->
+ Surface(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(innerPadding),
+ ) {
+
+ Column(
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ ValidatingInputTextField(
+ email = loginViewModel.email,
+ onValueChange = { input -> loginViewModel.updateEmail(input) },
+ validatorHasError = loginViewModel.emailHasErrors
+ )
+
+ PasswordTextField(
+ password = loginViewModel.password,
+ onPasswordChange = loginViewModel::updatePassword
+ )
+ FilledButton(
+ modifier=Modifier.fillMaxWidth().padding(16.dp),
+ shape=RoundedCornerShape(3.dp),
+ containerColor=ListifyColor.SplashYellow,
+ contentColor = ListifyColor.TextDark,
+ text="LOG IN ",
+ buttonIcon=Icons.AutoMirrored.Filled.ExitToApp,
+ iconDescription="Log In",
+ onClick={ onLoginClick() }
+ )
+
+ // handle login state
+ when (loginState) {
+ is LoginState.Loading -> { CircularProgressIndicator() }
+ is LoginState.Success -> {
+ LaunchedEffect(Unit) {
+ navController.navigate(
+ ListifyScreens.ListsScreen(userId = loginState.data.user.id)
+ )
+ }
+ }
+ is LoginState.Error -> {
+ Text(
+ text = loginState.message,
+ color = Color.Red,
+ modifier = Modifier.padding(16.dp)
+ )
+ }
+ LoginState.Idle -> {} // Explicit idle state
+ }
+ }
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/listifyjetapp/ui/screens/auth/LoginViewModel.kt b/app/src/main/java/com/example/listifyjetapp/ui/screens/auth/LoginViewModel.kt
new file mode 100644
index 0000000..53a0699
--- /dev/null
+++ b/app/src/main/java/com/example/listifyjetapp/ui/screens/auth/LoginViewModel.kt
@@ -0,0 +1,71 @@
+package com.example.listifyjetapp.ui.screens.auth
+
+import android.util.Patterns
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.example.listifyjetapp.data.ListifyResult
+import com.example.listifyjetapp.data.ListifyStorageManager
+import com.example.listifyjetapp.data.LoginState
+import com.example.listifyjetapp.model.LoginInfo
+import com.example.listifyjetapp.model.UserDataStore
+import com.example.listifyjetapp.repository.AuthUserRepository
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+@HiltViewModel
+class LoginViewModel @Inject constructor(
+ private val repository: AuthUserRepository,
+ private val storageManager: ListifyStorageManager
+): ViewModel() {
+
+ var email by mutableStateOf("")
+ private set
+ var password by mutableStateOf("")
+ private set
+
+ var loginState by mutableStateOf(LoginState.Idle)
+
+
+ val emailHasErrors by derivedStateOf {
+ if (email.isNotEmpty()) {
+ // Email is considered erroneous until it completely matches EMAIL_ADDRESS.
+ !Patterns.EMAIL_ADDRESS.matcher(email).matches()
+ } else {
+ false
+ }
+ }
+
+ fun updateEmail(newEmail:String) { email = newEmail }
+ fun updatePassword(newPassword: String) { password = newPassword }
+
+ fun login()
+ = viewModelScope.launch {
+ loginState = LoginState.Loading
+ val result = repository.login( LoginInfo(email = email.trim(), password = password) )
+
+ loginState = when (result) {
+ is ListifyResult.Success -> {
+ storageManager.saveToDataStore(
+ UserDataStore(
+ userId = result.data.user.id,
+ email = result.data.user.email,
+ accessToken = result.data.accessToken,
+ refreshToken = result.data.refreshToken,
+ isLogin = true,
+ )
+ )
+ // Save token securely
+ LoginState.Success(result.data)
+ }
+
+ is ListifyResult.Failure -> {
+ LoginState.Error(result.errorMessage)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/listifyjetapp/ui/screens/lists/ListRow.kt b/app/src/main/java/com/example/listifyjetapp/ui/screens/lists/ListRow.kt
new file mode 100644
index 0000000..9fd6113
--- /dev/null
+++ b/app/src/main/java/com/example/listifyjetapp/ui/screens/lists/ListRow.kt
@@ -0,0 +1,102 @@
+package com.example.listifyjetapp.ui.screens.lists
+
+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.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
+import androidx.compose.material.icons.filled.Share
+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.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import com.example.listifyjetapp.model.ListModel
+import com.example.listifyjetapp.ui.theme.ListifyColor
+
+@Composable
+fun ListRow(
+ list: ListModel,
+ //viewModel: User
+) {
+ Row(
+ modifier = Modifier
+ .clickable { }
+ .padding(vertical = 16.dp)
+ .fillMaxWidth()
+ .background(Color.Transparent)
+ ) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ // List info
+ Column(
+ modifier = Modifier.padding(horizontal = 8.dp),
+
+ ) {
+ Text(
+ text = list.name,
+ color = ListifyColor.TextBlack,
+ fontSize = 20.sp,
+ //fontWeight = FontWeight.SemiBold
+ )
+
+ if (list.share) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Icon(
+ modifier = Modifier.size(16.dp),
+ tint = ListifyColor.IconGreen,
+ imageVector = Icons.Default.Share,
+ contentDescription = "Shared with"
+ )
+ Text(
+ text = list.sharedWith.take(3).joinToString(", ") { it.username },
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ color = ListifyColor.TextGrey,
+ fontSize = 16.sp
+ )
+ }
+ }
+
+ Text(
+ text = list.createdAt,
+ color = ListifyColor.TextGrey,
+ fontSize = 16.sp
+ )
+ }
+ // List setting menu
+
+ Row() {
+ Text(
+ text = list.itemCount.toString(),
+ color = ListifyColor.TextGrey,
+ fontSize = 16.sp
+ )
+
+ Icon(
+ imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight,
+ contentDescription = "Forward icon"
+ )
+
+ }
+
+ }
+
+ }
+ HorizontalDivider()
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/listifyjetapp/ui/screens/lists/ListifyListsScreen.kt b/app/src/main/java/com/example/listifyjetapp/ui/screens/lists/ListifyListsScreen.kt
index 0bff383..5c5a2bb 100644
--- a/app/src/main/java/com/example/listifyjetapp/ui/screens/lists/ListifyListsScreen.kt
+++ b/app/src/main/java/com/example/listifyjetapp/ui/screens/lists/ListifyListsScreen.kt
@@ -1,41 +1,109 @@
package com.example.listifyjetapp.ui.screens.lists
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.text.KeyboardActions
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Add
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.navigation.NavController
import com.example.listifyjetapp.R
+import com.example.listifyjetapp.ui.navigation.ListifyScreens
+import com.example.listifyjetapp.utils.filterListItems
+import com.example.listifyjetapp.widgets.ListifySearchBar
+import com.example.listifyjetapp.widgets.ListifyTopBar
@Composable
fun ListifyListsScreen(
- viewModel: ListsViewModel = hiltViewModel()
+ navController: NavController,
+ viewModel: ListsViewModel = hiltViewModel(),
) {
- LaunchedEffect(Unit) { viewModel.getUserLists(4) }
+ LaunchedEffect(Unit) { viewModel.getUserLists() }
+
+ Scaffold(
+ modifier = Modifier.fillMaxSize(),
+ topBar = { ListifyTopBar(
+ title = "Lists",
+ isListsScreen = true,
+ rightIcon = Icons.Default.Add,
+ onRightButtonClick = {
+ navController.navigate(ListifyScreens.NewListScreen)
+ }
+ ) }
+ ) { innerPadding ->
- Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Surface(
- modifier = Modifier
- .fillMaxSize()
- .padding(innerPadding)
+ modifier = Modifier.fillMaxSize().padding(innerPadding)
) {
- if (viewModel.lists.isEmpty()) {
- Text(text = stringResource(R.string.no_lists))
- } else {
- LazyColumn {
- items(viewModel.lists) { list ->
- Text(text = list.name)
+ Column(
+ modifier = Modifier.padding(horizontal = 16.dp),
+ verticalArrangement = Arrangement.Top,
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+
+ val searchTextState = remember { mutableStateOf("") }
+ val keyboardController = LocalSoftwareKeyboardController.current
+
+ ListifySearchBar(
+ searchTextValue = searchTextState,
+ onValueChange = {searchTextState.value = it},
+ keyboardAction = KeyboardActions{
+ // Trigger search logic or hide keyboard
+ searchTextState.value.trim() // perform the search
+ keyboardController?.hide() // hide keyboard
+ }
+ )
+
+ viewModel.errorMessage?.let { msg ->
+ Text(msg,
+ Modifier.padding(16.dp),
+ color = MaterialTheme.colorScheme.error
+ )
+ }
+
+ if (viewModel.isLoading) {
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center
+ ) {
+ CircularProgressIndicator()
+ }
+ } else if (viewModel.lists.isEmpty()) {
+ Text(text = stringResource(R.string.no_lists))
+ } else {
+ LazyColumn(modifier = Modifier.padding(
+ vertical = 16.dp,
+ horizontal = 8.dp
+ )){
+ // Filter lists by search input
+ val results = filterListItems(searchTextState.value, viewModel.lists)
+
+ items(results) {list ->
+ ListRow(list)
+ }
}
}
}
+
}
}
-
}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/listifyjetapp/ui/screens/lists/ListsViewModel.kt b/app/src/main/java/com/example/listifyjetapp/ui/screens/lists/ListsViewModel.kt
index 894316b..44c84ce 100644
--- a/app/src/main/java/com/example/listifyjetapp/ui/screens/lists/ListsViewModel.kt
+++ b/app/src/main/java/com/example/listifyjetapp/ui/screens/lists/ListsViewModel.kt
@@ -1,29 +1,75 @@
package com.example.listifyjetapp.ui.screens.lists
+import android.util.Log
+import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.listifyjetapp.data.ListifyResult
+import com.example.listifyjetapp.data.ListifyStorageManager
import com.example.listifyjetapp.model.ListModel
+import com.example.listifyjetapp.model.ListName
import com.example.listifyjetapp.repository.ListsRepository
import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
-class ListsViewModel @Inject constructor(private val repository: ListsRepository): ViewModel() {
+class ListsViewModel @Inject constructor(
+ private val repository: ListsRepository,
+ private val storageManager: ListifyStorageManager
+): ViewModel() {
val lists = mutableStateListOf()
+ var isLoading by mutableStateOf(false)
+ var errorMessage by mutableStateOf(null)
- fun getUserLists(userId: Int) {
+ private val _navigateBack = MutableStateFlow(false)
+ val navigateBack = _navigateBack.asStateFlow()
+
+ fun getUserLists() {
viewModelScope.launch {
- val result = repository.getUserLists(userId = userId)
- when(result) {
- is ListifyResult.Success -> {
- lists.clear()
- lists.addAll(result.data)
+ val userId = storageManager.getUser().first().userId
+ isLoading = true
+ errorMessage = null
+
+ try {
+ val result = repository.getUserLists(userId = userId)
+ when (result) {
+ is ListifyResult.Success -> {
+ lists.clear()
+ lists.addAll(result.data)
+ }
+
+ is ListifyResult.Failure -> {
+ errorMessage = result.errorMessage
+ Log.d("Fail to fetch lists by user id", result.toString())
+ }
}
- is ListifyResult.Failure -> Unit
+ } finally {
+ isLoading = false
}
}
}
+
+ fun insertListByUser(userId: Int, newListName: ListName)
+ = viewModelScope.launch {
+ isLoading = true
+ val result = repository.insertListByUser(userId, newListName)
+ when (result) {
+ is ListifyResult.Success -> {
+ _navigateBack.value = true
+ }
+ is ListifyResult.Failure -> Unit
+ }
+ isLoading = false
+ }
+
+ fun navigationComplete() {
+ _navigateBack.value = false
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/listifyjetapp/ui/screens/newList/ListifyNewListScreen.kt b/app/src/main/java/com/example/listifyjetapp/ui/screens/newList/ListifyNewListScreen.kt
new file mode 100644
index 0000000..d71b47b
--- /dev/null
+++ b/app/src/main/java/com/example/listifyjetapp/ui/screens/newList/ListifyNewListScreen.kt
@@ -0,0 +1,84 @@
+package com.example.listifyjetapp.ui.screens.newList
+
+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.material3.Scaffold
+import androidx.compose.material3.Surface
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.navigation.NavController
+import com.example.listifyjetapp.model.ListName
+import com.example.listifyjetapp.ui.screens.lists.ListsViewModel
+import com.example.listifyjetapp.widgets.FormInputField
+import com.example.listifyjetapp.widgets.ListifyTopBar
+
+@Composable
+fun ListifyNewListScreen(
+ navController: NavController,
+ viewModel: ListsViewModel = hiltViewModel()
+) {
+
+ val formTextState = remember { mutableStateOf("") }
+
+ LaunchedEffect(viewModel.navigateBack) {
+ viewModel.navigateBack.collect { navigateBack ->
+ if (navigateBack) {
+ navController.popBackStack()
+ viewModel.navigationComplete()
+ }
+ }
+ }
+
+ fun onSaveClick() {
+ val listName = ListName(name = formTextState.value)
+ viewModel.insertListByUser(4, listName)
+ }
+
+ Scaffold(
+ modifier = Modifier.fillMaxSize(),
+ topBar = { ListifyTopBar(
+ title = "New List",
+ isListsScreen = false,
+ //goBackIcon = Icons.AutoMirrored.Filled.ArrowBack,
+ onGoBackButtonClicked = {navController.popBackStack()},
+ leftText = "Cancel",
+ rightText = "Save",
+ onRightButtonClick = {
+ // TODO: save new list
+ onSaveClick()
+ }
+ ) }
+ ) { innerPadding ->
+
+ Surface(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(innerPadding)
+ ) {
+
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp),
+ verticalArrangement = Arrangement.spacedBy(25.dp)
+ ) {
+
+ Column() {
+ FormInputField(
+ textState=formTextState,
+ onValueChange={ formTextState.value = it }
+ )
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/listifyjetapp/ui/screens/splash/ListifySplashScreen.kt b/app/src/main/java/com/example/listifyjetapp/ui/screens/splash/ListifySplashScreen.kt
index 928bbf2..7d4f377 100644
--- a/app/src/main/java/com/example/listifyjetapp/ui/screens/splash/ListifySplashScreen.kt
+++ b/app/src/main/java/com/example/listifyjetapp/ui/screens/splash/ListifySplashScreen.kt
@@ -10,6 +10,7 @@ import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -17,17 +18,23 @@ import androidx.compose.ui.draw.scale
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
+import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController
import com.example.listifyjetapp.R
+import com.example.listifyjetapp.components.authButtons.AuthButtons
import com.example.listifyjetapp.ui.navigation.ListifyScreens
import com.example.listifyjetapp.ui.theme.ListifyColor
import com.example.listifyjetapp.ui.theme.barriecitoFont
import kotlinx.coroutines.delay
@Composable
-fun ListifySplashScreen(navController: NavHostController) {
+fun ListifySplashScreen(
+ navController: NavHostController,
+ splashViewModel: SplashViewModel = hiltViewModel()
+) {
// TODO: Create an Animated object that holds a Float value starting at 0f
val scale = remember { Animatable(initialValue = 0f) }
+ val isShowButtons = remember { mutableStateOf(false) }
LaunchedEffect(key1 = true, block = { // key1 = true ensure it runs once only
scale.animateTo(
@@ -40,8 +47,19 @@ fun ListifySplashScreen(navController: NavHostController) {
// when the animation is over, delay 2s before going to next screen
delay(2000L)
- // TODO: Navigate to MainScreen
- navController.navigate(ListifyScreens.ListsScreen.route)
+
+ val isLoggedIn = splashViewModel.isLoggedIn()
+ if (isLoggedIn && splashViewModel.checkAccessToken()) {
+ // TODO: Navigate to MainScreen
+ navController.navigate(ListifyScreens.ListsScreen(splashViewModel.getUserId())) {
+ // Remove Splash from the back stack when go to Lists Screen
+ popUpTo(ListifyScreens.SplashScreen) { inclusive = true }
+ launchSingleTop = true
+ }
+ } else {
+ // TODO: Show login + SignIn button at the bottom
+ isShowButtons.value = true
+ }
})
Surface(
@@ -65,5 +83,9 @@ fun ListifySplashScreen(navController: NavHostController) {
color = ListifyColor.TextGrey
)
}
+
+ if (isShowButtons.value) {
+ AuthButtons(navController)
+ }
}
}
diff --git a/app/src/main/java/com/example/listifyjetapp/ui/screens/splash/SplashViewModel.kt b/app/src/main/java/com/example/listifyjetapp/ui/screens/splash/SplashViewModel.kt
new file mode 100644
index 0000000..80fbbcf
--- /dev/null
+++ b/app/src/main/java/com/example/listifyjetapp/ui/screens/splash/SplashViewModel.kt
@@ -0,0 +1,56 @@
+package com.example.listifyjetapp.ui.screens.splash
+
+import androidx.lifecycle.ViewModel
+import com.example.listifyjetapp.data.ListifyStorageManager
+import com.example.listifyjetapp.network.ListifyAPI
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.flow.first
+import javax.inject.Inject
+
+@HiltViewModel
+class SplashViewModel @Inject constructor(
+ private val storageManager: ListifyStorageManager,
+ private val api: ListifyAPI
+) : ViewModel() {
+
+ suspend fun isLoggedIn(): Boolean = storageManager.isLoggedInFlow.first()
+ suspend fun getUserId(): Int = storageManager.userIdFlow.first()
+
+ // Optional: decode JWT exp; if you don't want to parse JWT, just try refresh and ignore errors.
+ private fun isExpired(jwt: String): Boolean {
+ return try {
+ val token = jwt.split(".")
+ if (token.size < 2) return true // treat as expired/invalid
+ val payload = android.util.Base64.decode(
+ token[1],
+ android.util.Base64.URL_SAFE or android.util.Base64.NO_WRAP
+ )
+ val json = org.json.JSONObject(String(payload))
+ val exp = json.optLong("exp", 0L)
+ val nowSec = System.currentTimeMillis() / 1000
+ exp != 0L && exp <= nowSec
+ } catch (_: Exception) { false }
+ }
+
+ suspend fun accessToken(): String {
+ return storageManager.accessTokenFlow.first()
+ }
+ // Make sure access toke nis valid before navigating
+ // returns true if we are good to proceed, false if we must show login
+ suspend fun checkAccessToken(): Boolean {
+ val accessToken = storageManager.accessTokenFlow.first()
+ if (accessToken.isNotBlank() && !isExpired(accessToken)) return true
+
+ // try refresh
+ return try {
+ val refreshed = api.refresh(storageManager.refreshTokenFlow.first())
+ storageManager.updateTokens(
+ refreshed.accessToken, refreshed.refreshToken
+ )
+ true
+ } catch (_: Exception) {
+ storageManager.clearDataStore()
+ false
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/listifyjetapp/ui/theme/Color.kt b/app/src/main/java/com/example/listifyjetapp/ui/theme/Color.kt
index 6b65d4a..d525909 100644
--- a/app/src/main/java/com/example/listifyjetapp/ui/theme/Color.kt
+++ b/app/src/main/java/com/example/listifyjetapp/ui/theme/Color.kt
@@ -12,6 +12,8 @@ val Pink40 = Color(0xFF7D5260)
object ListifyColor {
val SplashYellow = Color(0xFFFFCA3A)
+ val TextBlack = Color(0xff000000)
val TextDark = Color(0xff3e4e50)
val TextGrey = Color(0xff858585)
+ val IconGreen = Color(0xff0cc25f)
}
diff --git a/app/src/main/java/com/example/listifyjetapp/utils/filterListItems.kt b/app/src/main/java/com/example/listifyjetapp/utils/filterListItems.kt
new file mode 100644
index 0000000..e1c4f94
--- /dev/null
+++ b/app/src/main/java/com/example/listifyjetapp/utils/filterListItems.kt
@@ -0,0 +1,14 @@
+package com.example.listifyjetapp.utils
+
+import android.util.Log
+import com.example.listifyjetapp.model.ListModel
+
+fun filterListItems(input: String, listItems: List): List {
+
+ return if (input.isEmpty()) {
+ listItems
+ } else {
+ listItems.filter { it.name.contains(input, ignoreCase = true) }
+ }
+
+}
diff --git a/app/src/main/java/com/example/listifyjetapp/widgets/FilledButton.kt b/app/src/main/java/com/example/listifyjetapp/widgets/FilledButton.kt
new file mode 100644
index 0000000..ee2af3e
--- /dev/null
+++ b/app/src/main/java/com/example/listifyjetapp/widgets/FilledButton.kt
@@ -0,0 +1,53 @@
+package com.example.listifyjetapp.widgets
+
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+
+@Composable
+fun FilledButton(
+ modifier: Modifier,
+ shape: Shape,
+ containerColor: Color,
+ contentColor: Color,
+ text: String,
+ buttonIcon: ImageVector? = null,
+ iconDescription: String?,
+ onClick: () -> Unit,
+) {
+ Button(
+ modifier = modifier,
+ shape = shape,
+ colors = ButtonDefaults.buttonColors(
+ containerColor = containerColor,
+ contentColor = contentColor
+ ),
+ onClick = { onClick() }
+ ) {
+ if (buttonIcon != null) {
+ Icon(
+ imageVector = buttonIcon,
+ modifier = Modifier.size(20.dp),
+ contentDescription = iconDescription
+ )
+ }
+
+ Text(
+ text = text,
+ fontWeight = FontWeight.ExtraBold,
+ fontSize = 18.sp,
+ modifier = Modifier.padding(8.dp)
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/listifyjetapp/widgets/FormInputField.kt b/app/src/main/java/com/example/listifyjetapp/widgets/FormInputField.kt
new file mode 100644
index 0000000..d74a32d
--- /dev/null
+++ b/app/src/main/java/com/example/listifyjetapp/widgets/FormInputField.kt
@@ -0,0 +1,46 @@
+package com.example.listifyjetapp.widgets
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextField
+import androidx.compose.material3.TextFieldDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.MutableState
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.unit.dp
+import com.example.listifyjetapp.ui.theme.ListifyColor
+
+@Composable
+fun FormInputField(
+ textState: MutableState,
+ onValueChange: (String) -> Unit,
+) {
+
+ TextField(
+ modifier = Modifier.fillMaxWidth(),
+
+ shape = RoundedCornerShape(12.dp),
+ colors = TextFieldDefaults.colors(
+ unfocusedIndicatorColor = Color.White,
+ focusedIndicatorColor = Color.White,
+ disabledIndicatorColor = Color.Transparent,
+
+ unfocusedLabelColor = ListifyColor.TextDark,
+ unfocusedContainerColor = ListifyColor.TextDark.copy(0.1f),
+ focusedContainerColor = ListifyColor.TextDark.copy(0.1f),
+ cursorColor = ListifyColor.TextDark,
+ ),
+ singleLine = true,
+ maxLines = 1,
+
+ value = textState.value,
+ onValueChange = onValueChange,
+ placeholder = { Text(text="e.g., grocery list") },
+ keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text), // Sets the keyboard to normal text input.
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/listifyjetapp/widgets/ListifySearchBar.kt b/app/src/main/java/com/example/listifyjetapp/widgets/ListifySearchBar.kt
new file mode 100644
index 0000000..f10d513
--- /dev/null
+++ b/app/src/main/java/com/example/listifyjetapp/widgets/ListifySearchBar.kt
@@ -0,0 +1,62 @@
+package com.example.listifyjetapp.widgets
+
+
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.text.KeyboardActions
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.foundation.text.input.InputTransformation.Companion.keyboardOptions
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Search
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextField
+import androidx.compose.material3.TextFieldDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.MutableState
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.unit.dp
+import com.example.listifyjetapp.ui.theme.ListifyColor
+
+
+@Composable
+fun ListifySearchBar(
+ searchTextValue: MutableState,
+ onValueChange: (String) -> Unit,
+ imeAction: ImeAction = ImeAction.Next, // what happen when you press "Next"
+ keyboardAction: KeyboardActions = KeyboardActions.Default // what to do when an action is triggered
+) {
+ TextField(
+ modifier = Modifier.fillMaxWidth().padding(top = 16.dp),
+ shape = RoundedCornerShape(12.dp),
+ colors = TextFieldDefaults.colors(
+ unfocusedIndicatorColor = Color.Transparent,
+ focusedIndicatorColor = Color.Transparent,
+ disabledIndicatorColor = Color.Transparent,
+
+ unfocusedLabelColor = ListifyColor.TextDark.copy(0.1f),
+ focusedContainerColor = ListifyColor.TextDark.copy(0.1f),
+ cursorColor = ListifyColor.TextDark,
+ ),
+ singleLine = true,
+ maxLines = 1,
+ leadingIcon = {
+ Icon(
+ imageVector = Icons.Default.Search,
+ contentDescription = "Search icon",
+ )
+ },
+
+ value = searchTextValue.value,
+ onValueChange = onValueChange,
+ placeholder = { Text(text="Search") },
+ keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text), // Sets the keyboard to normal text input.
+ keyboardActions = keyboardAction,
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/listifyjetapp/widgets/ListifyTopBar.kt b/app/src/main/java/com/example/listifyjetapp/widgets/ListifyTopBar.kt
new file mode 100644
index 0000000..ae71ebe
--- /dev/null
+++ b/app/src/main/java/com/example/listifyjetapp/widgets/ListifyTopBar.kt
@@ -0,0 +1,87 @@
+package com.example.listifyjetapp.widgets
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Add
+import androidx.compose.material3.CenterAlignedTopAppBar
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBarDefaults.topAppBarColors
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import com.example.listifyjetapp.ui.theme.ListifyColor
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Preview
+@Composable
+fun ListifyTopBar(
+ title: String = "Screen Tittle",
+ isListsScreen: Boolean = true,
+ goBackIcon: ImageVector? = null,
+ leftText: String? = "",
+ rightIcon: ImageVector? = null,
+ rightText: String? = "",
+ onGoBackButtonClicked: () -> Unit = {},
+ onRightButtonClick: () -> Unit = {}
+) {
+
+ CenterAlignedTopAppBar(
+ //modifier = Modifier.shadow(elevation = 5.dp),
+ colors = topAppBarColors(containerColor = ListifyColor.SplashYellow),
+ title = {
+ Text(text = title, fontWeight = FontWeight.ExtraBold, fontSize = 24.sp)
+ },
+
+ actions = {
+ if (rightIcon != null) {
+ IconButton(onClick = { onRightButtonClick() }) {
+ Icon(
+ modifier = Modifier.size(24.dp),
+ imageVector = rightIcon, //Icons.Default.Add,
+ contentDescription = "Add icon"
+ )
+ }
+ }
+
+ if (rightText.toString().isNotEmpty() && rightText != null) {
+ Text(
+ text = rightText,
+ modifier = Modifier.padding(horizontal = 16.dp).clickable { onRightButtonClick() },
+ fontSize = 20.sp,
+ color = ListifyColor.TextDark
+ )
+ }
+ },
+
+ navigationIcon = {
+ if (goBackIcon != null) {
+ Icon(
+ imageVector = goBackIcon,
+ contentDescription = "GO back icon",
+ modifier = Modifier.size(24.dp).clickable { onGoBackButtonClicked.invoke() }
+ )
+ }
+ if (leftText.toString().isNotEmpty() && leftText != null) {
+ Text(
+ text = leftText,
+ modifier = Modifier
+ .padding(horizontal = 16.dp)
+ .clickable { onGoBackButtonClicked.invoke() },
+ fontSize = 20.sp,
+ color = ListifyColor.TextDark
+ )
+ }
+
+ }
+ )
+}
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 96b3b79..5561f9b 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,5 +1,6 @@
[versions]
agp = "8.10.1"
+datastore = "1.1.7"
kotlin = "2.1.20"
coreKtx = "1.16.0"
junit = "4.13.2"
@@ -7,10 +8,12 @@ junitVersion = "1.2.1"
espressoCore = "3.6.1"
lifecycleRuntimeKtx = "2.9.1"
activityCompose = "1.10.1"
-composeBom = "2024.09.00"
+composeBom = "2025.07.00"
coilCompose = "1.4.0"
converterGson = "2.9.0"
+materialIconsExtended = "1.7.8"
+navigationCompose = "2.9.3"
roomKtx = "2.7.2"
roomRuntime = "2.7.2"
hiltCompiler = "2.56.2"
@@ -23,10 +26,13 @@ kotlinxCoroutinesCore = "1.10.1"
kotlinxCoroutinesPlayServices = "1.10.1"
lifecycleRuntimeKtxVersion = "2.9.1"
okhttp = "5.0.0-alpha.2"
+kotlinxSerializationJson = "1.7.3"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
+androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastore" }
+androidx-material-icons-extended = { module = "androidx.compose.material:material-icons-extended", version.ref = "materialIconsExtended" }
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" }
@@ -59,6 +65,13 @@ kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-c
kotlinx-coroutines-play-services = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-play-services", version.ref = "kotlinxCoroutinesPlayServices" }
retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
+kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" }
+androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationCompose" }
+androidx-navigation-dynamic-features-fragment = { module = "androidx.navigation:navigation-dynamic-features-fragment", version.ref = "navigationCompose" }
+androidx-navigation-fragment = { module = "androidx.navigation:navigation-fragment", version.ref = "navigationCompose" }
+androidx-navigation-testing = { module = "androidx.navigation:navigation-testing", version.ref = "navigationCompose" }
+androidx-navigation-ui = { module = "androidx.navigation:navigation-ui", version.ref = "navigationCompose" }
+
#font
androidx-ui-text-google-fonts = { group = "androidx.compose.ui", name = "ui-text-google-fonts", version = "1.8.3" } # or match your Compose BOM