feat: Implement setup screen and login logic

- Add SetupScreen with UI for server URL, username, and password input.
- Make SetupScreen the initial screen in the navigation graph.
- Implement secure credential storage using EncryptedSharedPreferences.
- Create CredentialsRepository and AuthRepository to manage credentials and auth tokens.
- Add LoginUseCase to handle the business logic for logging in.
- Implement a temporary Retrofit client in ItemRepository to handle login against a user-provided URL.
- Integrate login logic into SetupViewModel.
- Update all relevant project documentation and DI modules.
This commit is contained in:
2025-08-08 20:17:50 +03:00
parent 01e9b7bb00
commit 2853b5a47e
23 changed files with 602 additions and 23 deletions

View File

@@ -82,6 +82,9 @@ dependencies {
// [DEPENDENCY] Logging
implementation(Libs.timber)
// [DEPENDENCY] Security
implementation(Libs.securityCrypto)
// [DEPENDENCY] Testing
testImplementation(Libs.junit)
androidTestImplementation(Libs.extJunit)

View File

@@ -8,6 +8,13 @@ import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.homebox.lens.ui.screen.dashboard.DashboardScreen
import com.homebox.lens.ui.screen.inventorylist.InventoryListScreen
import com.homebox.lens.ui.screen.itemdetails.ItemDetailsScreen
import com.homebox.lens.ui.screen.itemedit.ItemEditScreen
import com.homebox.lens.ui.screen.labelslist.LabelsListScreen
import com.homebox.lens.ui.screen.locationslist.LocationsListScreen
import com.homebox.lens.ui.screen.search.SearchScreen
import com.homebox.lens.ui.screen.setup.SetupScreen
// [CORE-LOGIC]
/**
@@ -19,12 +26,36 @@ fun NavGraph() {
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = Screen.Dashboard.route
startDestination = Screen.Setup.route
) {
composable(route = Screen.Setup.route) {
SetupScreen(onSetupComplete = {
navController.navigate(Screen.Dashboard.route) {
popUpTo(Screen.Setup.route) { inclusive = true }
}
})
}
composable(route = Screen.Dashboard.route) {
DashboardScreen()
}
// TODO: Добавить остальные экраны в граф навигации
composable(route = Screen.InventoryList.route) {
InventoryListScreen()
}
composable(route = Screen.ItemDetails.route) {
ItemDetailsScreen()
}
composable(route = Screen.ItemEdit.route) {
ItemEditScreen()
}
composable(route = Screen.LabelsList.route) {
LabelsListScreen()
}
composable(route = Screen.LocationsList.route) {
LocationsListScreen()
}
composable(route = Screen.Search.route) {
SearchScreen()
}
}
}
// [END_FILE_NavGraph.kt]

View File

@@ -11,14 +11,17 @@ package com.homebox.lens.navigation
* @property route Строковый идентификатор маршрута.
*/
sealed class Screen(val route: String) {
/**
* [CONTRACT]
* Представляет экран "Дэшборд".
*/
data object Setup : Screen("setup_screen")
data object Dashboard : Screen("dashboard_screen")
// TODO: Добавить объекты для остальных экранов:
// data object ItemDetails : Screen("item_details_screen")
// data object Search : Screen("search_screen")
data object InventoryList : Screen("inventory_list_screen")
data object ItemDetails : Screen("item_details_screen/{itemId}") {
fun createRoute(itemId: String) = "item_details_screen/$itemId"
}
data object ItemEdit : Screen("item_edit_screen/{itemId}") {
fun createRoute(itemId: String) = "item_edit_screen/$itemId"
}
data object LabelsList : Screen("labels_list_screen")
data object LocationsList : Screen("locations_list_screen")
data object Search : Screen("search_screen")
}
// [END_FILE_Screen.kt]

View File

@@ -0,0 +1,113 @@
// [PACKAGE] com.homebox.lens.ui.screen.setup
// [FILE] SetupScreen.kt
package com.homebox.lens.ui.screen.setup
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
// [ENTRYPOINT]
@Composable
fun SetupScreen(
viewModel: SetupViewModel = hiltViewModel(),
onSetupComplete: () -> Unit
) {
val uiState by viewModel.uiState.collectAsState()
if (uiState.isSetupComplete) {
onSetupComplete()
}
SetupScreenContent(
uiState = uiState,
onServerUrlChange = viewModel::onServerUrlChange,
onUsernameChange = viewModel::onUsernameChange,
onPasswordChange = viewModel::onPasswordChange,
onConnectClick = viewModel::connect
)
}
// [CONTENT]
@Composable
private fun SetupScreenContent(
uiState: SetupUiState,
onServerUrlChange: (String) -> Unit,
onUsernameChange: (String) -> Unit,
onPasswordChange: (String) -> Unit,
onConnectClick: () -> Unit
) {
Scaffold(
topBar = {
TopAppBar(title = { Text("Server Setup") })
}
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.padding(16.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
OutlinedTextField(
value = uiState.serverUrl,
onValueChange = onServerUrlChange,
label = { Text("Server URL") },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
value = uiState.username,
onValueChange = onUsernameChange,
label = { Text("Username") },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
value = uiState.password,
onValueChange = onPasswordChange,
label = { Text("Password") },
visualTransformation = PasswordVisualTransformation(),
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(16.dp))
Button(
onClick = onConnectClick,
enabled = !uiState.isLoading,
modifier = Modifier.fillMaxWidth()
) {
if (uiState.isLoading) {
CircularProgressIndicator(modifier = Modifier.size(24.dp))
} else {
Text("Connect")
}
}
uiState.error?.let {
Spacer(modifier = Modifier.height(8.dp))
Text(text = it, color = MaterialTheme.colorScheme.error)
}
}
}
}
@Preview(showBackground = true)
@Composable
fun SetupScreenPreview() {
SetupScreenContent(
uiState = SetupUiState(error = "Failed to connect"),
onServerUrlChange = {},
onUsernameChange = {},
onPasswordChange = {},
onConnectClick = {}
)
}
// [END_FILE_SetupScreen.kt]

View File

@@ -0,0 +1,15 @@
// [PACKAGE] com.homebox.lens.ui.screen.setup
// [FILE] SetupUiState.kt
package com.homebox.lens.ui.screen.setup
// [STATE]
data class SetupUiState(
val serverUrl: String = "",
val username: String = "",
val password: String = "",
val isLoading: Boolean = false,
val error: String? = null,
val isSetupComplete: Boolean = false
)
// [END_FILE_SetupUiState.kt]

View File

@@ -0,0 +1,88 @@
// [PACKAGE] com.homebox.lens.ui.screen.setup
// [FILE] SetupViewModel.kt
package com.homebox.lens.ui.screen.setup
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.homebox.lens.domain.model.Credentials
import com.homebox.lens.domain.model.Result
import com.homebox.lens.domain.repository.CredentialsRepository
import com.homebox.lens.domain.usecase.LoginUseCase
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import javax.inject.Inject
// [VIEWMODEL]
@HiltViewModel
class SetupViewModel @Inject constructor(
private val credentialsRepository: CredentialsRepository,
private val loginUseCase: LoginUseCase
) : ViewModel() {
// [STATE]
private val _uiState = MutableStateFlow(SetupUiState())
val uiState = _uiState.asStateFlow()
init {
loadCredentials()
}
private fun loadCredentials() {
viewModelScope.launch {
credentialsRepository.getCredentials().collect { credentials ->
if (credentials != null) {
_uiState.update {
it.copy(
serverUrl = credentials.serverUrl,
username = credentials.username,
password = credentials.password
)
}
}
}
}
}
// [ACTION]
fun onServerUrlChange(newUrl: String) {
_uiState.update { it.copy(serverUrl = newUrl) }
}
// [ACTION]
fun onUsernameChange(newUsername: String) {
_uiState.update { it.copy(username = newUsername) }
}
// [ACTION]
fun onPasswordChange(newPassword: String) {
_uiState.update { it.copy(password = newPassword) }
}
// [ACTION]
fun connect() {
viewModelScope.launch {
_uiState.update { it.copy(isLoading = true, error = null) }
val credentials = Credentials(
serverUrl = _uiState.value.serverUrl.trim(),
username = _uiState.value.username.trim(),
password = _uiState.value.password
)
credentialsRepository.saveCredentials(credentials)
when (val result = loginUseCase(credentials)) {
is Result.Success -> {
_uiState.update { it.copy(isLoading = false, isSetupComplete = true) }
}
is Result.Error -> {
_uiState.update { it.copy(isLoading = false, error = result.exception.message ?: "Login failed") }
}
}
}
}
}
// [END_FILE_SetupViewModel.kt]

View File

@@ -94,6 +94,9 @@ object Libs {
const val composeUiTestJunit4 = "androidx.compose.ui:ui-test-junit4"
const val composeUiTooling = "androidx.compose.ui:ui-tooling"
const val composeUiTestManifest = "androidx.compose.ui:ui-test-manifest"
// Security
const val securityCrypto = "androidx.security:security-crypto:${Versions.securityCrypto}"
}
// [END_FILE_Dependencies.kt]

View File

@@ -10,7 +10,9 @@ import com.homebox.lens.data.api.dto.ItemSummaryDto
import com.homebox.lens.data.api.dto.ItemUpdateDto
import com.homebox.lens.data.api.dto.LabelOutDto
import com.homebox.lens.data.api.dto.LocationOutCountDto
import com.homebox.lens.data.api.dto.LoginFormDto
import com.homebox.lens.data.api.dto.PaginationResultDto
import com.homebox.lens.data.api.dto.TokenResponseDto
import retrofit2.Response
import retrofit2.http.Body
import retrofit2.http.DELETE
@@ -27,6 +29,10 @@ import retrofit2.http.Query
*/
interface HomeboxApiService {
// [ENDPOINT] Auth
@POST("v1/users/login")
suspend fun login(@Body loginForm: LoginFormDto): TokenResponseDto
// [ENDPOINT] Items
@GET("v1/items")
suspend fun getItems(

View File

@@ -0,0 +1,15 @@
// [PACKAGE] com.homebox.lens.data.api.dto
// [FILE] LoginFormDto.kt
package com.homebox.lens.data.api.dto
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class LoginFormDto(
@Json(name = "username") val username: String,
@Json(name = "password") val password: String,
@Json(name = "stayLoggedIn") val stayLoggedIn: Boolean = true
)
// [END_FILE_LoginFormDto.kt]

View File

@@ -0,0 +1,15 @@
// [PACKAGE] com.homebox.lens.data.api.dto
// [FILE] TokenResponseDto.kt
package com.homebox.lens.data.api.dto
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class TokenResponseDto(
@Json(name = "token") val token: String,
@Json(name = "attachmentToken") val attachmentToken: String,
@Json(name = "expiresAt") val expiresAt: String
)
// [END_FILE_TokenResponseDto.kt]

View File

@@ -56,12 +56,19 @@ object ApiModule {
// [PROVIDER]
@Provides
@Singleton
fun provideRetrofit(okHttpClient: OkHttpClient, moshi: Moshi): Retrofit {
fun provideMoshiConverterFactory(moshi: Moshi): MoshiConverterFactory {
return MoshiConverterFactory.create(moshi)
}
// [PROVIDER]
@Provides
@Singleton
fun provideRetrofit(okHttpClient: OkHttpClient, moshiConverterFactory: MoshiConverterFactory): Retrofit {
// [ACTION] Build Retrofit instance
return Retrofit.Builder()
.baseUrl(BASE_URL)
.client(okHttpClient)
.addConverterFactory(MoshiConverterFactory.create(moshi))
.addConverterFactory(moshiConverterFactory)
.build()
}

View File

@@ -3,11 +3,13 @@
package com.homebox.lens.data.di
import com.homebox.lens.data.api.HomeboxApiService
import com.homebox.lens.data.repository.ItemRepositoryImpl
import com.homebox.lens.data.repository.AuthRepositoryImpl
import com.homebox.lens.data.repository.CredentialsRepositoryImpl
import com.homebox.lens.domain.repository.AuthRepository
import com.homebox.lens.domain.repository.CredentialsRepository
import com.homebox.lens.domain.repository.ItemRepository
import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@@ -15,18 +17,23 @@ import javax.inject.Singleton
// [CONTRACT]
/**
* [MODULE: DaggerHilt('RepositoryModule')]
* [PURPOSE] Предоставляет реализацию для интерфейса ItemRepository.
* [PURPOSE] Предоставляет реализации для интерфейсов репозиториев.
*/
@Module
@InstallIn(SingletonComponent::class)
object RepositoryModule {
abstract class RepositoryModule {
// [PROVIDER]
@Provides
@Binds
@Singleton
fun provideItemRepository(apiService: HomeboxApiService): ItemRepository {
return ItemRepositoryImpl(apiService)
}
abstract fun bindItemRepository(itemRepositoryImpl: com.homebox.lens.data.repository.ItemRepositoryImpl): ItemRepository
@Binds
@Singleton
abstract fun bindCredentialsRepository(credentialsRepositoryImpl: CredentialsRepositoryImpl): CredentialsRepository
@Binds
@Singleton
abstract fun bindAuthRepository(authRepositoryImpl: AuthRepositoryImpl): AuthRepository
}
// [END_FILE_RepositoryModule.kt]

View File

@@ -0,0 +1,37 @@
// [PACKAGE] com.homebox.lens.data.di
// [FILE] StorageModule.kt
package com.homebox.lens.data.di
import android.content.Context
import android.content.SharedPreferences
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey
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)
object StorageModule {
@Provides
@Singleton
fun provideEncryptedSharedPreferences(@ApplicationContext context: Context): SharedPreferences {
val masterKey = MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
return EncryptedSharedPreferences.create(
context,
"secret_shared_prefs",
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
}
}
// [END_FILE_StorageModule.kt]

View File

@@ -0,0 +1,30 @@
// [PACKAGE] com.homebox.lens.data.repository
// [FILE] AuthRepositoryImpl.kt
package com.homebox.lens.data.repository
import android.content.SharedPreferences
import com.homebox.lens.domain.repository.AuthRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import javax.inject.Inject
class AuthRepositoryImpl @Inject constructor(
private val encryptedPrefs: SharedPreferences
) : AuthRepository {
companion object {
private const val KEY_AUTH_TOKEN = "key_auth_token"
}
override suspend fun saveToken(token: String) {
encryptedPrefs.edit().putString(KEY_AUTH_TOKEN, token).apply()
}
override fun getToken(): Flow<String?> = flow {
emit(encryptedPrefs.getString(KEY_AUTH_TOKEN, null))
}.flowOn(Dispatchers.IO)
}
// [END_FILE_AuthRepositoryImpl.kt]

View File

@@ -0,0 +1,46 @@
// [PACKAGE] com.homebox.lens.data.repository
// [FILE] CredentialsRepositoryImpl.kt
package com.homebox.lens.data.repository
import android.content.SharedPreferences
import com.homebox.lens.domain.model.Credentials
import com.homebox.lens.domain.repository.CredentialsRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import javax.inject.Inject
// [REPOSITORY_IMPL]
class CredentialsRepositoryImpl @Inject constructor(
private val encryptedPrefs: SharedPreferences
) : CredentialsRepository {
companion object {
private const val KEY_SERVER_URL = "key_server_url"
private const val KEY_USERNAME = "key_username"
private const val KEY_PASSWORD = "key_password"
}
override suspend fun saveCredentials(credentials: Credentials) {
encryptedPrefs.edit()
.putString(KEY_SERVER_URL, credentials.serverUrl)
.putString(KEY_USERNAME, credentials.username)
.putString(KEY_PASSWORD, credentials.password)
.apply()
}
override fun getCredentials(): Flow<Credentials?> = flow {
val serverUrl = encryptedPrefs.getString(KEY_SERVER_URL, null)
val username = encryptedPrefs.getString(KEY_USERNAME, null)
val password = encryptedPrefs.getString(KEY_PASSWORD, null)
if (serverUrl != null && username != null && password != null) {
emit(Credentials(serverUrl, username, password))
} else {
emit(null)
}
}.flowOn(Dispatchers.IO)
}
// [END_FILE_CredentialsRepositoryImpl.kt]

View File

@@ -4,10 +4,15 @@
package com.homebox.lens.data.repository
// [IMPORTS]
import com.homebox.lens.data.api.HomeboxApiService
import com.homebox.lens.data.api.dto.LoginFormDto
import com.homebox.lens.data.api.dto.toDomain
import com.homebox.lens.data.api.dto.toDto
import com.homebox.lens.domain.model.*
import com.homebox.lens.domain.repository.AuthRepository
import com.homebox.lens.domain.repository.ItemRepository
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import javax.inject.Inject
import javax.inject.Singleton
@@ -19,9 +24,29 @@ import javax.inject.Singleton
*/
@Singleton
class ItemRepositoryImpl @Inject constructor(
private val apiService: HomeboxApiService
private val apiService: HomeboxApiService,
private val authRepository: AuthRepository,
private val okHttpClient: OkHttpClient,
private val moshiConverterFactory: MoshiConverterFactory
) : ItemRepository {
override suspend fun login(credentials: Credentials): Result<Unit> {
return try {
val tempApiService = Retrofit.Builder()
.baseUrl(credentials.serverUrl)
.client(okHttpClient)
.addConverterFactory(moshiConverterFactory)
.build()
.create(HomeboxApiService::class.java)
val loginForm = LoginFormDto(credentials.username, credentials.password)
val tokenResponse = tempApiService.login(loginForm)
authRepository.saveToken(tokenResponse.token)
Result.Success(Unit)
} catch (e: Exception) {
Result.Error(e)
}
}
/**
* [CONTRACT] @see ItemRepository.createItem
*/

View File

@@ -0,0 +1,18 @@
// [PACKAGE] com.homebox.lens.domain.model
// [FILE] Credentials.kt
package com.homebox.lens.domain.model
/**
* [CONTRACT]
* Data class to hold server credentials.
* @property serverUrl The URL of the Homebox server.
* @property username The username for authentication.
* @property password The password for authentication.
*/
data class Credentials(
val serverUrl: String,
val username: String,
val password: String
)
// [END_FILE_Credentials.kt]

View File

@@ -0,0 +1,27 @@
// [PACKAGE] com.homebox.lens.domain.repository
// [FILE] AuthRepository.kt
package com.homebox.lens.domain.repository
import kotlinx.coroutines.flow.Flow
/**
* [CONTRACT]
* Repository for managing authentication tokens.
*/
interface AuthRepository {
/**
* [CONTRACT]
* Saves the authentication token.
* @param token The token to save.
*/
suspend fun saveToken(token: String)
/**
* [CONTRACT]
* Retrieves the authentication token.
* @return A Flow emitting the token, or null if not found.
*/
fun getToken(): Flow<String?>
}
// [END_FILE_AuthRepository.kt]

View File

@@ -0,0 +1,28 @@
// [PACKAGE] com.homebox.lens.domain.repository
// [FILE] CredentialsRepository.kt
package com.homebox.lens.domain.repository
import com.homebox.lens.domain.model.Credentials
import kotlinx.coroutines.flow.Flow
/**
* [CONTRACT]
* Repository for managing user credentials.
*/
interface CredentialsRepository {
/**
* [CONTRACT]
* Saves the user credentials securely.
* @param credentials The credentials to save.
*/
suspend fun saveCredentials(credentials: Credentials)
/**
* [CONTRACT]
* Retrieves the saved user credentials.
* @return A Flow emitting the saved [Credentials], or null if none are saved.
*/
fun getCredentials(): Flow<Credentials?>
}
// [END_FILE_CredentialsRepository.kt]

View File

@@ -12,6 +12,7 @@ import com.homebox.lens.domain.model.*
* Определяет контракт, которому должен следовать слой данных.
*/
interface ItemRepository {
suspend fun login(credentials: Credentials): Result<Unit>
suspend fun createItem(newItemData: ItemCreate): ItemSummary
suspend fun getItemDetails(itemId: String): ItemOut
suspend fun updateItem(itemId: String, item: ItemUpdate): ItemOut

View File

@@ -0,0 +1,29 @@
// [PACKAGE] com.homebox.lens.domain.usecase
// [FILE] LoginUseCase.kt
package com.homebox.lens.domain.usecase
import com.homebox.lens.domain.model.Credentials
import com.homebox.lens.domain.repository.ItemRepository
import javax.inject.Inject
import com.homebox.lens.domain.model.Result
/**
* [CONTRACT]
* Use case for user login.
* @param itemRepository The repository to handle item and auth operations.
*/
class LoginUseCase @Inject constructor(
private val itemRepository: ItemRepository
) {
/**
* [CONTRACT]
* Executes the login process.
* @param credentials The user's credentials.
* @return A [Result] object indicating success or failure.
*/
suspend operator fun invoke(credentials: Credentials): Result<Unit> {
return itemRepository.login(credentials)
}
}
// [END_FILE_LoginUseCase.kt]

View File

@@ -59,6 +59,15 @@
<file name="app/src/main/java/com/homebox/lens/ui/screen/search/SearchViewModel.kt" status="stub" spec_ref_id="screen_search">
<purpose_summary>ViewModel for the Search screen.</purpose_summary>
</file>
<file name="app/src/main/java/com/homebox/lens/ui/screen/setup/SetupScreen.kt" status="stub" spec_ref_id="screen_setup">
<purpose_summary>UI for the Setup screen.</purpose_summary>
</file>
<file name="app/src/main/java/com/homebox/lens/ui/screen/setup/SetupViewModel.kt" status="stub" spec_ref_id="screen_setup">
<purpose_summary>ViewModel for the Setup screen.</purpose_summary>
</file>
<file name="app/src/main/java/com/homebox/lens/ui/screen/setup/SetupUiState.kt" status="implemented" spec_ref_id="screen_setup">
<purpose_summary>UI state for the Setup screen.</purpose_summary>
</file>
</module>
<module name="data" type="android_library">
<purpose_summary>Data layer, responsible for data sources (network, local DB) and repository implementations.</purpose_summary>
@@ -80,12 +89,33 @@
<file name="data/src/main/java/com/homebox/lens/data/di/RepositoryModule.kt" status="implemented" ref_id="di_repo">
<purpose_summary>Hilt module for binding repository interfaces to their implementations.</purpose_summary>
</file>
<file name="data/src/main/java/com/homebox/lens/data/di/StorageModule.kt" status="implemented" ref_id="di_storage">
<purpose_summary>Hilt module for providing storage-related dependencies (EncryptedSharedPreferences).</purpose_summary>
</file>
<file name="data/src/main/java/com/homebox/lens/data/repository/CredentialsRepositoryImpl.kt" status="implemented" ref_id="repo_credentials_impl">
<purpose_summary>Implementation of the CredentialsRepository.</purpose_summary>
</file>
<file name="data/src/main/java/com/homebox/lens/data/repository/AuthRepositoryImpl.kt" status="implemented" ref_id="repo_auth_impl">
<purpose_summary>Implementation of the AuthRepository.</purpose_summary>
</file>
</module>
<module name="domain" type="kotlin_jvm_library">
<purpose_summary>Domain layer, contains business logic, use cases, and repository interfaces. Pure Kotlin module.</purpose_summary>
<file name="domain/src/main/java/com/homebox/lens/domain/model/Credentials.kt" status="implemented" ref_id="model_credentials">
<purpose_summary>Data class for holding user credentials.</purpose_summary>
</file>
<file name="domain/src/main/java/com/homebox/lens/domain/repository/AuthRepository.kt" status="implemented" ref_id="repo_auth_interface">
<purpose_summary>Interface for the auth repository.</purpose_summary>
</file>
<file name="domain/src/main/java/com/homebox/lens/domain/repository/CredentialsRepository.kt" status="implemented" ref_id="repo_credentials_interface">
<purpose_summary>Interface for the credentials repository.</purpose_summary>
</file>
<file name="domain/src/main/java/com/homebox/lens/domain/repository/ItemRepository.kt" status="implemented" ref_id="repo_interface">
<purpose_summary>Interface defining the contract for data operations related to items.</purpose_summary>
</file>
<file name="domain/src/main/java/com/homebox/lens/domain/usecase/LoginUseCase.kt" status="implemented" spec_ref_id="uc_login">
<purpose_summary>Use case for user login.</purpose_summary>
</file>
<file name="domain/src/main/java/com/homebox/lens/domain/usecase/CreateItemUseCase.kt" status="implemented" spec_ref_id="uc_create_item">
<purpose_summary>Use case for creating a new item.</purpose_summary>
</file>

View File

@@ -117,6 +117,7 @@
<USE_CASE id="uc_delete_item" file_ref="domain/src/main/java/com/homebox/lens/domain/usecase/DeleteItemUseCase.kt" />
<USE_CASE id="uc_get_all_labels" file_ref="domain/src/main/java/com/homebox/lens/domain/usecase/GetAllLabelsUseCase.kt" />
<USE_CASE id="uc_get_all_locations" file_ref="domain/src/main/java/com/homebox/lens/domain/usecase/GetAllLocationsUseCase.kt" />
<USE_CASE id="uc_login" file_ref="domain/src/main/java/com/homebox/lens/domain/usecase/LoginUseCase.kt" />
<!-- UI Screens -->
<UI_SCREEN id="screen_dashboard" file_ref="app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardScreen.kt" />
@@ -126,5 +127,6 @@
<UI_SCREEN id="screen_labels_list" file_ref="app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListScreen.kt" />
<UI_SCREEN id="screen_locations_list" file_ref="app/src/main/java/com/homebox/lens/ui/screen/locationslist/LocationsListScreen.kt" />
<UI_SCREEN id="screen_search" file_ref="app/src/main/java/com/homebox/lens/ui/screen/search/SearchScreen.kt" />
<UI_SCREEN id="screen_setup" file_ref="app/src/main/java/com/homebox/lens/ui/screen/setup/SetupScreen.kt" />
</IMPLEMENTATION_MAP>
</PROJECT_SPECIFICATION>