feat: Refactor login screen - fix compilation error
This commit is contained in:
@@ -25,11 +25,14 @@ import com.homebox.lens.ui.screen.locationedit.LocationEditScreen
|
|||||||
import com.homebox.lens.ui.screen.locationslist.LocationsListScreen
|
import com.homebox.lens.ui.screen.locationslist.LocationsListScreen
|
||||||
import com.homebox.lens.ui.screen.search.SearchScreen
|
import com.homebox.lens.ui.screen.search.SearchScreen
|
||||||
import com.homebox.lens.ui.screen.setup.SetupScreen
|
import com.homebox.lens.ui.screen.setup.SetupScreen
|
||||||
|
import com.homebox.lens.ui.screen.settings.SettingsScreen
|
||||||
|
import com.homebox.lens.ui.screen.splash.SplashScreen
|
||||||
// [END_IMPORTS]
|
// [END_IMPORTS]
|
||||||
|
|
||||||
// [ENTITY: Function('NavGraph')]
|
// [ENTITY: Function('NavGraph')]
|
||||||
// [RELATION: Function('NavGraph')] -> [DEPENDS_ON] -> [Framework('NavHostController')]
|
// [RELATION: Function('NavGraph')] -> [DEPENDS_ON] -> [Framework('NavHostController')]
|
||||||
// [RELATION: Function('NavGraph')] -> [CREATES_INSTANCE_OF] -> [Class('NavigationActions')]
|
// [RELATION: Function('NavGraph')] -> [CREATES_INSTANCE_OF] -> [Class('NavigationActions')]
|
||||||
|
// [RELATION: Function('NavGraph')] -> [USES] -> [Screen('SplashScreen')]
|
||||||
/**
|
/**
|
||||||
* @summary Определяет граф навигации для всего приложения с использованием Jetpack Compose Navigation.
|
* @summary Определяет граф навигации для всего приложения с использованием Jetpack Compose Navigation.
|
||||||
* @param navController Контроллер навигации.
|
* @param navController Контроллер навигации.
|
||||||
@@ -47,11 +50,13 @@ fun NavGraph(
|
|||||||
val navigationActions = remember(navController) {
|
val navigationActions = remember(navController) {
|
||||||
NavigationActions(navController)
|
NavigationActions(navController)
|
||||||
}
|
}
|
||||||
|
|
||||||
NavHost(
|
NavHost(
|
||||||
navController = navController,
|
navController = navController,
|
||||||
startDestination = Screen.Setup.route
|
startDestination = Screen.Splash.route
|
||||||
) {
|
) {
|
||||||
|
composable(route = Screen.Splash.route) {
|
||||||
|
SplashScreen(navController = navController)
|
||||||
|
}
|
||||||
composable(route = Screen.Setup.route) {
|
composable(route = Screen.Setup.route) {
|
||||||
SetupScreen(onSetupComplete = {
|
SetupScreen(onSetupComplete = {
|
||||||
navController.navigate(Screen.Dashboard.route) {
|
navController.navigate(Screen.Dashboard.route) {
|
||||||
@@ -137,6 +142,12 @@ fun NavGraph(
|
|||||||
navigationActions = navigationActions
|
navigationActions = navigationActions
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
composable(route = Screen.Settings.route) {
|
||||||
|
SettingsScreen(
|
||||||
|
currentRoute = currentRoute,
|
||||||
|
navigationActions = navigationActions
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// [END_ENTITY: Function('NavGraph')]
|
// [END_ENTITY: Function('NavGraph')]
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ package com.homebox.lens.navigation
|
|||||||
* @param route Строковый идентификатор маршрута.
|
* @param route Строковый идентификатор маршрута.
|
||||||
*/
|
*/
|
||||||
sealed class Screen(val route: String) {
|
sealed class Screen(val route: String) {
|
||||||
|
// [ENTITY: Object('Splash')]
|
||||||
|
data object Splash : Screen("splash_screen")
|
||||||
|
// [END_ENTITY: Object('Splash')]
|
||||||
|
|
||||||
// [ENTITY: Object('Setup')]
|
// [ENTITY: Object('Setup')]
|
||||||
data object Setup : Screen("setup_screen")
|
data object Setup : Screen("setup_screen")
|
||||||
// [END_ENTITY: Object('Setup')]
|
// [END_ENTITY: Object('Setup')]
|
||||||
@@ -118,6 +122,10 @@ sealed class Screen(val route: String) {
|
|||||||
// [ENTITY: Object('Search')]
|
// [ENTITY: Object('Search')]
|
||||||
data object Search : Screen("search_screen")
|
data object Search : Screen("search_screen")
|
||||||
// [END_ENTITY: Object('Search')]
|
// [END_ENTITY: Object('Search')]
|
||||||
|
|
||||||
|
// [ENTITY: Object('Settings')]
|
||||||
|
data object Settings : Screen("settings_screen")
|
||||||
|
// [END_ENTITY: Object('Settings')]
|
||||||
}
|
}
|
||||||
// [END_ENTITY: SealedClass('Screen')]
|
// [END_ENTITY: SealedClass('Screen')]
|
||||||
// [END_FILE_Screen.kt]
|
// [END_FILE_Screen.kt]
|
||||||
|
|||||||
@@ -310,10 +310,10 @@ fun DashboardContentSuccessPreview() {
|
|||||||
LocationOutCount(id="5",name="Basement", color = "#00FFFF", isArchived = false, itemCount = 3, createdAt = "", updatedAt = "")
|
LocationOutCount(id="5",name="Basement", color = "#00FFFF", isArchived = false, itemCount = 3, createdAt = "", updatedAt = "")
|
||||||
),
|
),
|
||||||
labels = listOf(
|
labels = listOf(
|
||||||
LabelOut(id="1", name="electronics", color = "#FF0000", isArchived = false, createdAt = "", updatedAt = ""),
|
LabelOut(id="1", name="electronics", description = null, color = "#FF0000", isArchived = false, createdAt = "", updatedAt = ""),
|
||||||
LabelOut(id="2", name="important", color = "#00FF00", isArchived = false, createdAt = "", updatedAt = ""),
|
LabelOut(id="2", name="important", description = null, color = "#00FF00", isArchived = false, createdAt = "", updatedAt = ""),
|
||||||
LabelOut(id="3", name="seasonal", color = "#0000FF", isArchived = false, createdAt = "", updatedAt = ""),
|
LabelOut(id="3", name="seasonal", description = null, color = "#0000FF", isArchived = false, createdAt = "", updatedAt = ""),
|
||||||
LabelOut(id="4", name="hobby", color = "#FFFF00", isArchived = false, createdAt = "", updatedAt = "")
|
LabelOut(id="4", name="hobby", description = null, color = "#FFFF00", isArchived = false, createdAt = "", updatedAt = "")
|
||||||
),
|
),
|
||||||
recentlyAddedItems = emptyList()
|
recentlyAddedItems = emptyList()
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,8 +7,14 @@ package com.homebox.lens.ui.screen.itemedit
|
|||||||
// [IMPORTS]
|
// [IMPORTS]
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.homebox.lens.domain.model.*
|
import com.homebox.lens.domain.model.Item
|
||||||
|
import com.homebox.lens.domain.model.ItemCreate
|
||||||
|
import com.homebox.lens.domain.model.ItemUpdate
|
||||||
|
import com.homebox.lens.domain.model.Location
|
||||||
|
import com.homebox.lens.domain.model.Label
|
||||||
import com.homebox.lens.domain.usecase.CreateItemUseCase
|
import com.homebox.lens.domain.usecase.CreateItemUseCase
|
||||||
|
import com.homebox.lens.domain.usecase.GetAllLabelsUseCase
|
||||||
|
import com.homebox.lens.domain.usecase.GetAllLocationsUseCase
|
||||||
import com.homebox.lens.domain.usecase.GetItemDetailsUseCase
|
import com.homebox.lens.domain.usecase.GetItemDetailsUseCase
|
||||||
import com.homebox.lens.domain.usecase.UpdateItemUseCase
|
import com.homebox.lens.domain.usecase.UpdateItemUseCase
|
||||||
import com.homebox.lens.ui.mapper.ItemMapper
|
import com.homebox.lens.ui.mapper.ItemMapper
|
||||||
@@ -30,11 +36,15 @@ import javax.inject.Inject
|
|||||||
* @param item The item being edited, or null if creating a new item.
|
* @param item The item being edited, or null if creating a new item.
|
||||||
* @param isLoading Whether data is currently being loaded or saved.
|
* @param isLoading Whether data is currently being loaded or saved.
|
||||||
* @param error An error message if an operation failed.
|
* @param error An error message if an operation failed.
|
||||||
|
* @param allLocations A list of all available locations.
|
||||||
|
* @param allLabels A list of all available labels.
|
||||||
*/
|
*/
|
||||||
data class ItemEditUiState(
|
data class ItemEditUiState(
|
||||||
val item: Item? = null,
|
val item: Item? = null,
|
||||||
val isLoading: Boolean = false,
|
val isLoading: Boolean = false,
|
||||||
val error: String? = null
|
val error: String? = null,
|
||||||
|
val allLocations: List<Location> = emptyList(),
|
||||||
|
val allLabels: List<Label> = emptyList()
|
||||||
)
|
)
|
||||||
// [END_ENTITY: DataClass('ItemEditUiState')]
|
// [END_ENTITY: DataClass('ItemEditUiState')]
|
||||||
|
|
||||||
@@ -56,6 +66,8 @@ class ItemEditViewModel @Inject constructor(
|
|||||||
private val createItemUseCase: CreateItemUseCase,
|
private val createItemUseCase: CreateItemUseCase,
|
||||||
private val updateItemUseCase: UpdateItemUseCase,
|
private val updateItemUseCase: UpdateItemUseCase,
|
||||||
private val getItemDetailsUseCase: GetItemDetailsUseCase,
|
private val getItemDetailsUseCase: GetItemDetailsUseCase,
|
||||||
|
private val getAllLocationsUseCase: GetAllLocationsUseCase,
|
||||||
|
private val getAllLabelsUseCase: GetAllLabelsUseCase,
|
||||||
private val itemMapper: ItemMapper
|
private val itemMapper: ItemMapper
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
@@ -123,10 +135,47 @@ class ItemEditViewModel @Inject constructor(
|
|||||||
_uiState.value = _uiState.value.copy(isLoading = false, error = e.localizedMessage)
|
_uiState.value = _uiState.value.copy(isLoading = false, error = e.localizedMessage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load all locations and labels
|
||||||
|
try {
|
||||||
|
Timber.i("[INFO][ACTION][fetching_all_locations] Fetching all locations.")
|
||||||
|
val allLocations = getAllLocationsUseCase().map { Location(it.id, it.name) }
|
||||||
|
Timber.i("[INFO][ACTION][fetching_all_labels] Fetching all labels.")
|
||||||
|
val allLabels = getAllLabelsUseCase().map { Label(it.id, it.name) }
|
||||||
|
_uiState.value = _uiState.value.copy(allLocations = allLocations, allLabels = allLabels)
|
||||||
|
Timber.i("[INFO][ACTION][all_locations_labels_fetched] Successfully fetched all locations and labels.")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e, "[ERROR][FALLBACK][locations_labels_load_failed] Failed to load locations or labels.")
|
||||||
|
_uiState.value = _uiState.value.copy(error = e.localizedMessage)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// [END_ENTITY: Function('loadItem')]
|
// [END_ENTITY: Function('loadItem')]
|
||||||
|
|
||||||
|
// [ENTITY: Function('updateLocation')]
|
||||||
|
/**
|
||||||
|
* @summary Updates the location of the item in the UI state.
|
||||||
|
* @param location The new location for the item.
|
||||||
|
* @sideeffect Updates the `item` in `_uiState`.
|
||||||
|
*/
|
||||||
|
fun updateLocation(location: Location) {
|
||||||
|
Timber.d("[DEBUG][ACTION][updating_item_location] Updating item location to: %s", location.name)
|
||||||
|
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(location = location))
|
||||||
|
}
|
||||||
|
// [END_ENTITY: Function('updateLocation')]
|
||||||
|
|
||||||
|
// [ENTITY: Function('updateLabels')]
|
||||||
|
/**
|
||||||
|
* @summary Updates the labels of the item in the UI state.
|
||||||
|
* @param labels The new list of labels for the item.
|
||||||
|
* @sideeffect Updates the `item` in `_uiState`.
|
||||||
|
*/
|
||||||
|
fun updateLabels(labels: List<Label>) {
|
||||||
|
Timber.d("[DEBUG][ACTION][updating_item_labels] Updating item labels to: %s", labels.map { it.name }.joinToString())
|
||||||
|
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(labels = labels))
|
||||||
|
}
|
||||||
|
// [END_ENTITY: Function('updateLabels')]
|
||||||
|
|
||||||
// [ENTITY: Function('saveItem')]
|
// [ENTITY: Function('saveItem')]
|
||||||
/**
|
/**
|
||||||
* @summary Saves the current item, either creating a new one or updating an existing one.
|
* @summary Saves the current item, either creating a new one or updating an existing one.
|
||||||
|
|||||||
@@ -97,6 +97,13 @@ fun LabelEditScreen(
|
|||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
OutlinedTextField(
|
||||||
|
value = uiState.description.orEmpty(),
|
||||||
|
onValueChange = viewModel::onDescriptionChange,
|
||||||
|
label = { Text(stringResource(R.string.label_description)) },
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
ColorPicker(
|
ColorPicker(
|
||||||
selectedColor = uiState.color,
|
selectedColor = uiState.color,
|
||||||
onColorSelected = viewModel::onColorChange,
|
onColorSelected = viewModel::onColorChange,
|
||||||
|
|||||||
@@ -50,6 +50,10 @@ class LabelEditViewModel @Inject constructor(
|
|||||||
uiState = uiState.copy(name = newName, nameError = null)
|
uiState = uiState.copy(name = newName, nameError = null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun onDescriptionChange(newDescription: String) {
|
||||||
|
uiState = uiState.copy(description = newDescription)
|
||||||
|
}
|
||||||
|
|
||||||
fun onColorChange(newColor: String) {
|
fun onColorChange(newColor: String) {
|
||||||
uiState = uiState.copy(color = newColor)
|
uiState = uiState.copy(color = newColor)
|
||||||
}
|
}
|
||||||
@@ -63,35 +67,41 @@ class LabelEditViewModel @Inject constructor(
|
|||||||
|
|
||||||
uiState = uiState.copy(isLoading = true, error = null)
|
uiState = uiState.copy(isLoading = true, error = null)
|
||||||
try {
|
try {
|
||||||
if (labelId == null) {
|
val result = if (labelId == null) {
|
||||||
// Create new label
|
// [LOG_EVENT] [EVENT_TYPE: LabelCreationAttempt] [DATA: { "labelName": "${uiState.name}" }]
|
||||||
val newLabel = LabelCreate(name = uiState.name, color = uiState.color, description = null)
|
val newLabel = LabelCreate(name = uiState.name, color = uiState.color, description = uiState.description)
|
||||||
createLabelUseCase(newLabel)
|
createLabelUseCase(newLabel)
|
||||||
} else {
|
} else {
|
||||||
// Update existing label
|
// [LOG_EVENT] [EVENT_TYPE: LabelUpdateAttempt] [DATA: { "labelId": "$labelId", "labelName": "${uiState.name}" }]
|
||||||
val updatedLabel = LabelUpdate(name = uiState.name, color = uiState.color, description = null)
|
val updatedLabel = LabelUpdate(name = uiState.name, color = uiState.color, description = uiState.description)
|
||||||
updateLabelUseCase(labelId, updatedLabel)
|
updateLabelUseCase(labelId, updatedLabel)
|
||||||
}
|
}
|
||||||
|
// [LOG_EVENT] [EVENT_TYPE: LabelSaveSuccess] [DATA: { "labelName": "${uiState.name}", "isNew": ${labelId == null} }]
|
||||||
uiState = uiState.copy(isSaved = true)
|
uiState = uiState.copy(isSaved = true)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
// [LOG_EVENT] [EVENT_TYPE: LabelSaveFailure] [ERROR: "${e.message}"] [DATA: { "labelName": "${uiState.name}", "isNew": ${labelId == null} }]
|
||||||
uiState = uiState.copy(error = e.message, isLoading = false)
|
uiState = uiState.copy(error = e.message, isLoading = false)
|
||||||
} finally {
|
} finally {
|
||||||
uiState = uiState.copy(isLoading = false)
|
uiState = uiState.copy(isLoading = false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadLabelDetails(id: String) {
|
private fun loadLabelDetails(id: String) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
uiState = uiState.copy(isLoading = true, error = null)
|
uiState = uiState.copy(isLoading = true, error = null)
|
||||||
try {
|
try {
|
||||||
|
// [LOG_EVENT] [EVENT_TYPE: LabelDetailsFetchAttempt] [DATA: { "labelId": "$id" }]
|
||||||
val label = getLabelDetailsUseCase(id)
|
val label = getLabelDetailsUseCase(id)
|
||||||
|
// [LOG_EVENT] [EVENT_TYPE: LabelDetailsFetchSuccess] [DATA: { "labelId": "$id", "labelName": "${label.name}" }]
|
||||||
uiState = uiState.copy(
|
uiState = uiState.copy(
|
||||||
name = label.name,
|
name = label.name,
|
||||||
color = label.color,
|
color = label.color,
|
||||||
isLoading = false
|
description = label.description,
|
||||||
|
isLoading = false,
|
||||||
|
originalLabel = label
|
||||||
)
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
// [LOG_EVENT] [EVENT_TYPE: LabelDetailsFetchFailure] [ERROR: "${e.message}"] [DATA: { "labelId": "$id" }]
|
||||||
uiState = uiState.copy(error = e.message, isLoading = false)
|
uiState = uiState.copy(error = e.message, isLoading = false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -104,6 +114,7 @@ class LabelEditViewModel @Inject constructor(
|
|||||||
*/
|
*/
|
||||||
data class LabelEditUiState(
|
data class LabelEditUiState(
|
||||||
val name: String = "",
|
val name: String = "",
|
||||||
|
val description: String? = null,
|
||||||
val color: String = "#FFFFFF", // Default color
|
val color: String = "#FFFFFF", // Default color
|
||||||
val nameError: String? = null,
|
val nameError: String? = null,
|
||||||
val isLoading: Boolean = false,
|
val isLoading: Boolean = false,
|
||||||
|
|||||||
@@ -17,24 +17,18 @@ import androidx.compose.material.icons.Icons
|
|||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
import androidx.compose.material.icons.automirrored.filled.Label
|
import androidx.compose.material.icons.automirrored.filled.Label
|
||||||
import androidx.compose.material.icons.filled.Add
|
import androidx.compose.material.icons.filled.Add
|
||||||
import androidx.compose.material3.AlertDialog
|
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.FloatingActionButton
|
import androidx.compose.material3.FloatingActionButton
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.ListItem
|
import androidx.compose.material3.ListItem
|
||||||
import androidx.compose.material3.OutlinedTextField
|
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
@@ -53,9 +47,11 @@ import timber.log.Timber
|
|||||||
// [RELATION: Function('LabelsListScreen')] -> [DEPENDS_ON] -> [Framework('NavController')]
|
// [RELATION: Function('LabelsListScreen')] -> [DEPENDS_ON] -> [Framework('NavController')]
|
||||||
/**
|
/**
|
||||||
* @summary Отображает экран со списком всех меток.
|
* @summary Отображает экран со списком всех меток.
|
||||||
* @param navController Контроллер навигации для перемещения между экранами.
|
* @param currentRoute Текущий маршрут навигации.
|
||||||
|
* @param navigationActions Объект, содержащий действия по навигации.
|
||||||
* @param viewModel ViewModel, предоставляющая состояние UI для экрана меток.
|
* @param viewModel ViewModel, предоставляющая состояние UI для экрана меток.
|
||||||
*/
|
*/
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun LabelsListScreen(
|
fun LabelsListScreen(
|
||||||
currentRoute: String?,
|
currentRoute: String?,
|
||||||
@@ -90,19 +86,19 @@ fun LabelsListScreen(
|
|||||||
.padding(innerPaddingValues), // Use innerPaddingValues here
|
.padding(innerPaddingValues), // Use innerPaddingValues here
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
when (currentState) {
|
when (val state = uiState) {
|
||||||
is LabelsListUiState.Loading -> {
|
is LabelsListUiState.Loading -> {
|
||||||
CircularProgressIndicator()
|
CircularProgressIndicator()
|
||||||
}
|
}
|
||||||
is LabelsListUiState.Error -> {
|
is LabelsListUiState.Error -> {
|
||||||
Text(text = currentState.message)
|
Text(text = state.message)
|
||||||
}
|
}
|
||||||
is LabelsListUiState.Success -> {
|
is LabelsListUiState.Success -> {
|
||||||
if (currentState.labels.isEmpty()) {
|
if (state.labels.isEmpty()) {
|
||||||
Text(text = stringResource(id = R.string.no_labels_found))
|
Text(text = stringResource(id = R.string.no_labels_found))
|
||||||
} else {
|
} else {
|
||||||
LabelsList(
|
LabelsList(
|
||||||
labels = currentState.labels,
|
labels = state.labels,
|
||||||
onLabelClick = { label ->
|
onLabelClick = { label ->
|
||||||
Timber.i("[INFO][ACTION][navigate_to_label_edit] Label clicked: ${label.id}. Navigating to label edit screen.")
|
Timber.i("[INFO][ACTION][navigate_to_label_edit] Label clicked: ${label.id}. Navigating to label edit screen.")
|
||||||
navigationActions.navigateToLabelEdit(label.id)
|
navigationActions.navigateToLabelEdit(label.id)
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
// [PACKAGE] com.homebox.lens.ui.screen.settings
|
||||||
|
// [FILE] SettingsScreen.kt
|
||||||
|
// [SEMANTICS] ui, screen, settings
|
||||||
|
|
||||||
|
package com.homebox.lens.ui.screen.settings
|
||||||
|
|
||||||
|
// [IMPORTS]
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TopAppBar
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import com.homebox.lens.R
|
||||||
|
import com.homebox.lens.navigation.NavigationActions
|
||||||
|
import com.homebox.lens.ui.common.MainScaffold
|
||||||
|
// [END_IMPORTS]
|
||||||
|
|
||||||
|
// [ENTITY: Function('SettingsScreen')]
|
||||||
|
// [RELATION: Function('SettingsScreen')] -> [DEPENDS_ON] -> [Class('NavigationActions')]
|
||||||
|
/**
|
||||||
|
* @summary Composable-функция для экрана настроек.
|
||||||
|
* @param currentRoute Текущий маршрут навигации.
|
||||||
|
* @param navigationActions Объект, содержащий действия по навигации.
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun SettingsScreen(
|
||||||
|
currentRoute: String?,
|
||||||
|
navigationActions: NavigationActions
|
||||||
|
) {
|
||||||
|
MainScaffold(
|
||||||
|
topBarTitle = stringResource(id = R.string.screen_title_settings),
|
||||||
|
currentRoute = currentRoute,
|
||||||
|
navigationActions = navigationActions
|
||||||
|
) { paddingValues ->
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(paddingValues),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(text = "Settings Screen (Under Construction)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// [END_ENTITY: Function('SettingsScreen')]
|
||||||
|
// [END_FILE_SettingsScreen.kt]
|
||||||
@@ -7,17 +7,22 @@
|
|||||||
package com.homebox.lens.ui.screen.setup
|
package com.homebox.lens.ui.screen.setup
|
||||||
|
|
||||||
// [IMPORTS]
|
// [IMPORTS]
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Lock
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import com.homebox.lens.R
|
import com.homebox.lens.R
|
||||||
// [END_IMPORTS]
|
// [END_IMPORTS]
|
||||||
@@ -83,42 +88,74 @@ private fun SetupScreenContent(
|
|||||||
verticalArrangement = Arrangement.Center,
|
verticalArrangement = Arrangement.Center,
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
OutlinedTextField(
|
Image(
|
||||||
value = uiState.serverUrl,
|
imageVector = Icons.Default.Lock,
|
||||||
onValueChange = onServerUrlChange,
|
contentDescription = stringResource(id = R.string.app_name),
|
||||||
label = { Text(stringResource(id = R.string.setup_server_url_label)) },
|
modifier = Modifier.size(128.dp)
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
OutlinedTextField(
|
|
||||||
value = uiState.username,
|
|
||||||
onValueChange = onUsernameChange,
|
|
||||||
label = { Text(stringResource(id = R.string.setup_username_label)) },
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
OutlinedTextField(
|
|
||||||
value = uiState.password,
|
|
||||||
onValueChange = onPasswordChange,
|
|
||||||
label = { Text(stringResource(id = R.string.setup_password_label)) },
|
|
||||||
visualTransformation = PasswordVisualTransformation(),
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.setup_title),
|
||||||
|
style = MaterialTheme.typography.headlineLarge,
|
||||||
|
fontSize = 28.sp // Adjust font size as needed
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(16.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = uiState.serverUrl,
|
||||||
|
onValueChange = onServerUrlChange,
|
||||||
|
label = { Text(stringResource(id = R.string.setup_server_url_label)) },
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
OutlinedTextField(
|
||||||
|
value = uiState.username,
|
||||||
|
onValueChange = onUsernameChange,
|
||||||
|
label = { Text(stringResource(id = R.string.setup_username_label)) },
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
OutlinedTextField(
|
||||||
|
value = uiState.password,
|
||||||
|
onValueChange = onPasswordChange,
|
||||||
|
label = { Text(stringResource(id = R.string.setup_password_label)) },
|
||||||
|
visualTransformation = PasswordVisualTransformation(),
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
Button(
|
Button(
|
||||||
onClick = onConnectClick,
|
onClick = onConnectClick,
|
||||||
enabled = !uiState.isLoading,
|
enabled = !uiState.isLoading,
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(56.dp) // Make button more prominent
|
||||||
) {
|
) {
|
||||||
if (uiState.isLoading) {
|
if (uiState.isLoading) {
|
||||||
CircularProgressIndicator(modifier = Modifier.size(24.dp))
|
CircularProgressIndicator(
|
||||||
|
modifier = Modifier.size(24.dp),
|
||||||
|
color = MaterialTheme.colorScheme.onPrimary
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
Text(stringResource(id = R.string.setup_connect_button))
|
Text(stringResource(id = R.string.setup_connect_button), fontSize = 18.sp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
uiState.error?.let {
|
uiState.error?.let {
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
Text(text = it, color = MaterialTheme.colorScheme.error)
|
Text(
|
||||||
|
text = it,
|
||||||
|
color = MaterialTheme.colorScheme.error,
|
||||||
|
style = MaterialTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,40 +74,61 @@ class SetupViewModel @Inject constructor(
|
|||||||
// [END_ENTITY: Function('onUsernameChange')]
|
// [END_ENTITY: Function('onUsernameChange')]
|
||||||
|
|
||||||
// [ENTITY: Function('onPasswordChange')]
|
// [ENTITY: Function('onPasswordChange')]
|
||||||
|
/**
|
||||||
|
* @summary Updates the password in the UI state.
|
||||||
|
* @param newPassword The new password.
|
||||||
|
* @sideeffect Updates the `password` in `_uiState`.
|
||||||
|
*/
|
||||||
fun onPasswordChange(newPassword: String) {
|
fun onPasswordChange(newPassword: String) {
|
||||||
_uiState.update { it.copy(password = newPassword) }
|
_uiState.update { it.copy(password = newPassword) }
|
||||||
}
|
}
|
||||||
// [END_ENTITY: Function('onPasswordChange')]
|
// [END_ENTITY: Function('onPasswordChange')]
|
||||||
|
|
||||||
// [ENTITY: Function('connect')]
|
// [ENTITY: Function('areCredentialsSaved')]
|
||||||
fun connect() {
|
/**
|
||||||
Timber.d("[DEBUG][ENTRYPOINT][connecting] Starting connection process.")
|
* @summary Checks synchronously if credentials are saved.
|
||||||
viewModelScope.launch {
|
* @return true if credentials are saved, false otherwise.
|
||||||
_uiState.update { it.copy(isLoading = true, error = null) }
|
* @sideeffect None.
|
||||||
|
*/
|
||||||
|
fun areCredentialsSaved(): Boolean {
|
||||||
|
Timber.d("[DEBUG][ENTRYPOINT][checking_credentials_saved] Checking if credentials are saved.")
|
||||||
|
return credentialsRepository.areCredentialsSavedSync()
|
||||||
|
}
|
||||||
|
// [END_ENTITY: Function('areCredentialsSaved')]
|
||||||
|
|
||||||
val credentials = Credentials(
|
// [ENTITY: Function('connect')]
|
||||||
serverUrl = _uiState.value.serverUrl.trim(),
|
/**
|
||||||
username = _uiState.value.username.trim(),
|
* @summary Initiates the connection process, saving credentials and attempting to log in.
|
||||||
password = _uiState.value.password
|
* @sideeffect Updates `_uiState` with loading, error, and completion states.
|
||||||
)
|
*/
|
||||||
|
fun connect() {
|
||||||
|
Timber.d("[DEBUG][ENTRYPOINT][connecting] Starting connection process.")
|
||||||
|
viewModelScope.launch {
|
||||||
|
_uiState.update { it.copy(isLoading = true, error = null) }
|
||||||
|
|
||||||
Timber.d("[DEBUG][ACTION][saving_credentials] Saving credentials.")
|
val credentials = Credentials(
|
||||||
credentialsRepository.saveCredentials(credentials)
|
serverUrl = _uiState.value.serverUrl.trim(),
|
||||||
|
username = _uiState.value.username.trim(),
|
||||||
|
password = _uiState.value.password
|
||||||
|
)
|
||||||
|
|
||||||
Timber.d("[DEBUG][ACTION][executing_login] Executing login use case.")
|
Timber.d("[DEBUG][ACTION][saving_credentials] Saving credentials.")
|
||||||
loginUseCase(credentials).fold(
|
credentialsRepository.saveCredentials(credentials)
|
||||||
onSuccess = {
|
|
||||||
Timber.d("[DEBUG][SUCCESS][login_successful] Login successful.")
|
Timber.d("[DEBUG][ACTION][executing_login] Executing login use case.")
|
||||||
_uiState.update { it.copy(isLoading = false, isSetupComplete = true) }
|
loginUseCase(credentials).fold(
|
||||||
},
|
onSuccess = {
|
||||||
onFailure = { exception ->
|
Timber.d("[DEBUG][SUCCESS][login_successful] Login successful.")
|
||||||
Timber.e(exception, "[ERROR][EXCEPTION][login_failed] Login failed.")
|
_uiState.update { it.copy(isLoading = false, isSetupComplete = true) }
|
||||||
_uiState.update { it.copy(isLoading = false, error = exception.message ?: "Login failed") }
|
},
|
||||||
}
|
onFailure = { exception ->
|
||||||
)
|
Timber.e(exception, "[ERROR][EXCEPTION][login_failed] Login failed.")
|
||||||
}
|
_uiState.update { it.copy(isLoading = false, error = exception.message ?: "Login failed") }
|
||||||
}
|
}
|
||||||
// [END_ENTITY: Function('connect')]
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// [END_ENTITY: Function('connect')]
|
||||||
}
|
}
|
||||||
// [END_ENTITY: ViewModel('SetupViewModel')]
|
// [END_ENTITY: ViewModel('SetupViewModel')]
|
||||||
// [END_FILE_SetupViewModel.kt]
|
// [END_FILE_SetupViewModel.kt]
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
// [PACKAGE] com.homebox.lens.ui.screen.splash
|
||||||
|
// [FILE] SplashScreen.kt
|
||||||
|
// [SEMANTICS] ui, screen, splash, navigation, authentication_flow
|
||||||
|
|
||||||
|
package com.homebox.lens.ui.screen.splash
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import com.homebox.lens.navigation.Screen
|
||||||
|
import com.homebox.lens.ui.screen.setup.SetupViewModel
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
// [ENTITY: Function('SplashScreen')]
|
||||||
|
/**
|
||||||
|
* @summary Displays a splash screen while checking if credentials are saved.
|
||||||
|
* @param navController The NavController for navigation.
|
||||||
|
* @param viewModel The SetupViewModel to check credential status.
|
||||||
|
* @sideeffect Navigates to either SetupScreen or DashboardScreen based on credential status.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun SplashScreen(
|
||||||
|
navController: NavController,
|
||||||
|
viewModel: SetupViewModel = hiltViewModel()
|
||||||
|
) {
|
||||||
|
Timber.d("[DEBUG][ENTRYPOINT][splash_screen_composable] SplashScreen entered.")
|
||||||
|
|
||||||
|
LaunchedEffect(key1 = true) {
|
||||||
|
Timber.i("[INFO][ACTION][checking_credentials_on_launch] Checking if credentials are saved on launch.")
|
||||||
|
val credentialsSaved = viewModel.areCredentialsSaved()
|
||||||
|
if (credentialsSaved) {
|
||||||
|
Timber.i("[INFO][ACTION][credentials_found_navigating_dashboard] Credentials found, navigating to Dashboard.")
|
||||||
|
navController.navigate(Screen.Dashboard.route) {
|
||||||
|
popUpTo(Screen.Splash.route) { inclusive = true }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Timber.i("[INFO][ACTION][no_credentials_found_navigating_setup] No credentials found, navigating to Setup.")
|
||||||
|
navController.navigate(Screen.Setup.route) {
|
||||||
|
popUpTo(Screen.Splash.route) { inclusive = true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
CircularProgressIndicator()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// [END_ENTITY: Function('SplashScreen')]
|
||||||
@@ -35,7 +35,6 @@
|
|||||||
<string name="item_edit_title_create">Создать элемент</string>
|
<string name="item_edit_title_create">Создать элемент</string>
|
||||||
<string name="content_desc_save_item">Сохранить элемент</string>
|
<string name="content_desc_save_item">Сохранить элемент</string>
|
||||||
<string name="label_name">Название</string>
|
<string name="label_name">Название</string>
|
||||||
<string name="label_description">Описание</string>
|
|
||||||
|
|
||||||
<!-- Search Screen -->
|
<!-- Search Screen -->
|
||||||
<string name="placeholder_search_items">Поиск элементов...</string>
|
<string name="placeholder_search_items">Поиск элементов...</string>
|
||||||
@@ -120,6 +119,8 @@
|
|||||||
|
|
||||||
<!-- Labels List Screen -->
|
<!-- Labels List Screen -->
|
||||||
<string name="screen_title_labels">Метки</string>
|
<string name="screen_title_labels">Метки</string>
|
||||||
|
<!-- Settings Screen -->
|
||||||
|
<string name="screen_title_settings">Настройки</string>
|
||||||
<string name="content_desc_navigate_back" translatable="false">Вернуться назад</string>
|
<string name="content_desc_navigate_back" translatable="false">Вернуться назад</string>
|
||||||
<string name="content_desc_create_label">Создать новую метку</string>
|
<string name="content_desc_create_label">Создать новую метку</string>
|
||||||
<string name="content_desc_label_icon">Иконка метки</string>
|
<string name="content_desc_label_icon">Иконка метки</string>
|
||||||
@@ -133,6 +134,7 @@
|
|||||||
<string name="label_edit_title_create">Создать метку</string>
|
<string name="label_edit_title_create">Создать метку</string>
|
||||||
<string name="label_edit_title_edit">Редактировать метку</string>
|
<string name="label_edit_title_edit">Редактировать метку</string>
|
||||||
<string name="label_name_edit">Название метки</string>
|
<string name="label_name_edit">Название метки</string>
|
||||||
|
<string name="label_description">Описание</string>
|
||||||
|
|
||||||
<!-- Common Actions -->
|
<!-- Common Actions -->
|
||||||
<string name="back">Назад</string>
|
<string name="back">Назад</string>
|
||||||
|
|||||||
@@ -155,6 +155,7 @@ fun LabelEntity.toDomainLabelOut(): LabelOut {
|
|||||||
return LabelOut(
|
return LabelOut(
|
||||||
id = this.id,
|
id = this.id,
|
||||||
name = this.name,
|
name = this.name,
|
||||||
|
description = null, // Not available in LabelEntity
|
||||||
color = "", // Not available in LabelEntity
|
color = "", // Not available in LabelEntity
|
||||||
isArchived = false, // Not available in LabelEntity
|
isArchived = false, // Not available in LabelEntity
|
||||||
createdAt = "", // Not available in LabelEntity
|
createdAt = "", // Not available in LabelEntity
|
||||||
|
|||||||
@@ -119,6 +119,7 @@ fun LabelOutDto.toDomain(): DomainLabelOut {
|
|||||||
return DomainLabelOut(
|
return DomainLabelOut(
|
||||||
id = this.id,
|
id = this.id,
|
||||||
name = this.name,
|
name = this.name,
|
||||||
|
description = this.description,
|
||||||
color = this.color ?: "",
|
color = this.color ?: "",
|
||||||
isArchived = this.isArchived ?: false,
|
isArchived = this.isArchived ?: false,
|
||||||
createdAt = this.createdAt,
|
createdAt = this.createdAt,
|
||||||
|
|||||||
@@ -98,11 +98,46 @@ class CredentialsRepositoryImpl @Inject constructor(
|
|||||||
*/
|
*/
|
||||||
override suspend fun getToken(): String? {
|
override suspend fun getToken(): String? {
|
||||||
return withContext(Dispatchers.IO) {
|
return withContext(Dispatchers.IO) {
|
||||||
Timber.d("[DEBUG][ACTION][getting_token] Getting auth token.")
|
val token = encryptedPrefs.getString(KEY_AUTH_TOKEN, null)
|
||||||
encryptedPrefs.getString(KEY_AUTH_TOKEN, null)
|
if (token != null) {
|
||||||
|
Timber.i("[INFO][ACTION][token_retrieved] Auth token retrieved successfully.")
|
||||||
|
} else {
|
||||||
|
Timber.w("[WARN][FALLBACK][no_token_found] No auth token found.")
|
||||||
|
}
|
||||||
|
token
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// [END_ENTITY: Function('getToken')]
|
// [END_ENTITY: Function('getToken')]
|
||||||
|
|
||||||
|
// [ENTITY: Function('clearAllCredentials')]
|
||||||
|
/**
|
||||||
|
* @summary Очищает все сохраненные учетные данные и токены.
|
||||||
|
* @sideeffect Удаляет все записи, связанные с учетными данными, из SharedPreferences.
|
||||||
|
*/
|
||||||
|
override suspend fun clearAllCredentials() {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
Timber.i("[INFO][ACTION][clearing_all_credentials] Clearing all saved credentials and tokens.")
|
||||||
|
encryptedPrefs.edit()
|
||||||
|
.remove(KEY_SERVER_URL)
|
||||||
|
.remove(KEY_USERNAME)
|
||||||
|
.remove(KEY_PASSWORD)
|
||||||
|
.remove(KEY_AUTH_TOKEN)
|
||||||
|
.apply()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// [END_ENTITY: Function('clearAllCredentials')]
|
||||||
|
|
||||||
|
// [ENTITY: Function('areCredentialsSavedSync')]
|
||||||
|
/**
|
||||||
|
* @summary Synchronously checks if user credentials are saved.
|
||||||
|
* @return true if all essential credentials (URL, username, password) are present, false otherwise.
|
||||||
|
*/
|
||||||
|
override fun areCredentialsSavedSync(): Boolean {
|
||||||
|
return encryptedPrefs.contains(KEY_SERVER_URL) &&
|
||||||
|
encryptedPrefs.contains(KEY_USERNAME) &&
|
||||||
|
encryptedPrefs.contains(KEY_PASSWORD)
|
||||||
|
}
|
||||||
|
// [END_ENTITY: Function('areCredentialsSavedSync')]
|
||||||
}
|
}
|
||||||
// [END_ENTITY: Class('CredentialsRepositoryImpl')]
|
// [END_ENTITY: Class('CredentialsRepositoryImpl')]
|
||||||
// [END_FILE_CredentialsRepositoryImpl.kt]
|
// [END_FILE_CredentialsRepositoryImpl.kt]
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ package com.homebox.lens.domain.model
|
|||||||
data class LabelOut(
|
data class LabelOut(
|
||||||
val id: String,
|
val id: String,
|
||||||
val name: String,
|
val name: String,
|
||||||
|
val description: String?,
|
||||||
val color: String,
|
val color: String,
|
||||||
val isArchived: Boolean,
|
val isArchived: Boolean,
|
||||||
val createdAt: String,
|
val createdAt: String,
|
||||||
|
|||||||
@@ -44,8 +44,25 @@ interface CredentialsRepository {
|
|||||||
* @summary Retrieves the saved authorization token.
|
* @summary Retrieves the saved authorization token.
|
||||||
* @return The saved token as a String, or null if no token is saved.
|
* @return The saved token as a String, or null if no token is saved.
|
||||||
*/
|
*/
|
||||||
suspend fun getToken(): String?
|
|
||||||
// [END_ENTITY: Function('getToken')]
|
suspend fun getToken(): String?
|
||||||
|
// [END_ENTITY: Function('getToken')]
|
||||||
|
|
||||||
|
// [ENTITY: Function('areCredentialsSavedSync')]
|
||||||
|
/**
|
||||||
|
* @summary Synchronously checks if user credentials are saved.
|
||||||
|
* @return true if all essential credentials (URL, username, password) are present, false otherwise.
|
||||||
|
*/
|
||||||
|
fun areCredentialsSavedSync(): Boolean
|
||||||
|
// [END_ENTITY: Function('areCredentialsSavedSync')]
|
||||||
|
|
||||||
|
// [ENTITY: Function('clearAllCredentials')]
|
||||||
|
/**
|
||||||
|
* @summary Clears all saved credentials and tokens.
|
||||||
|
* @sideeffect Removes all credential-related entries from SharedPreferences.
|
||||||
|
*/
|
||||||
|
suspend fun clearAllCredentials()
|
||||||
|
// [END_ENTITY: Function('clearAllCredentials')]
|
||||||
}
|
}
|
||||||
// [END_ENTITY: Interface('CredentialsRepository')]
|
// [END_ENTITY: Interface('CredentialsRepository')]
|
||||||
// [END_FILE_CredentialsRepository.kt]
|
// [END_FILE_CredentialsRepository.kt]
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
// [PACKAGE] com.homebox.lens.domain.usecase
|
// [PACKAGE] com.homebox.lens.domain.usecase
|
||||||
// [FILE] GetLabelDetailsUseCase.kt
|
// [FILE] GetLabelDetailsUseCase.kt
|
||||||
// [SEMANTICS] business_logic, use_case, label_retrieval
|
// [SEMANTICS] business_logic, use_case, label, get
|
||||||
|
|
||||||
package com.homebox.lens.domain.usecase
|
package com.homebox.lens.domain.usecase
|
||||||
|
|
||||||
// [IMPORTS]
|
// [IMPORTS]
|
||||||
@@ -13,23 +12,25 @@ import javax.inject.Inject
|
|||||||
// [ENTITY: UseCase('GetLabelDetailsUseCase')]
|
// [ENTITY: UseCase('GetLabelDetailsUseCase')]
|
||||||
// [RELATION: UseCase('GetLabelDetailsUseCase')] -> [DEPENDS_ON] -> [Interface('ItemRepository')]
|
// [RELATION: UseCase('GetLabelDetailsUseCase')] -> [DEPENDS_ON] -> [Interface('ItemRepository')]
|
||||||
/**
|
/**
|
||||||
* @summary Получает детальную информацию о метке по ее ID.
|
* @summary Сценарий использования для получения деталей метки.
|
||||||
* @param itemRepository Репозиторий для работы с данными о метках.
|
* @param repository Репозиторий для доступа к данным.
|
||||||
*/
|
*/
|
||||||
class GetLabelDetailsUseCase @Inject constructor(
|
class GetLabelDetailsUseCase @Inject constructor(
|
||||||
private val itemRepository: ItemRepository
|
private val repository: ItemRepository
|
||||||
) {
|
) {
|
||||||
|
// [ENTITY: Function('invoke')]
|
||||||
/**
|
/**
|
||||||
* @summary Выполняет получение детальной информации о метке.
|
* @summary Выполняет получение деталей метки.
|
||||||
* @param labelId ID запрашиваемой метки.
|
* @param labelId ID метки для получения деталей.
|
||||||
* @return Детальная информация о метке [LabelOut].
|
* @return Возвращает полную информацию о метке [LabelOut].
|
||||||
* @throws IllegalArgumentException если `labelId` пустой.
|
* @throws Exception в случае ошибки на уровне репозитория (сеть, API).
|
||||||
* @throws NoSuchElementException если метка с указанным ID не найдена.
|
* @precondition `labelId` не должен быть пустым.
|
||||||
*/
|
*/
|
||||||
suspend operator fun invoke(labelId: String): LabelOut {
|
suspend operator fun invoke(labelId: String): LabelOut {
|
||||||
require(labelId.isNotBlank()) { "Label ID cannot be blank." }
|
require(labelId.isNotBlank()) { "Label ID cannot be blank." }
|
||||||
return itemRepository.getLabelDetails(labelId)
|
return repository.getLabelDetails(labelId)
|
||||||
}
|
}
|
||||||
|
// [END_ENTITY: Function('invoke')]
|
||||||
}
|
}
|
||||||
// [END_ENTITY: UseCase('GetLabelDetailsUseCase')]
|
// [END_ENTITY: UseCase('GetLabelDetailsUseCase')]
|
||||||
// [END_FILE_GetLabelDetailsUseCase.kt]
|
// [END_FILE_GetLabelDetailsUseCase.kt]
|
||||||
@@ -1,98 +1,51 @@
|
|||||||
<![CDATA[
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<WORK_ORDER>
|
<WORK_ORDER>
|
||||||
<META>
|
<META>
|
||||||
<VERSION>1.0</VERSION>
|
<ID>WO-ITEMEDIT-FIX</ID>
|
||||||
<CREATED_AT>{timestamp}</CREATED_AT>
|
<TITLE>[ARCHITECT -> DEV] Исправление выбора локации и меток на экране ItemEdit</TITLE>
|
||||||
<TITLE>[ARCHITECT -> DEV] Refactor Item Edit Screen</TITLE>
|
<DESCRIPTION>В текущей реализации на экране редактирования/создания элемента (ItemEditScreen) поля "Location" и "Labels" неактивны. Необходимо реализовать функционал выбора значения для этих полей из списка доступных.</DESCRIPTION>
|
||||||
<DESCRIPTION>
|
<CREATED_BY>architect-agent</CREATED_BY>
|
||||||
This work order instructs the developer agent to refactor the Item Edit screen to include all available API fields and implement a user-friendly, grouped layout.
|
<ASSIGNED_TO>developer-agent</ASSIGNED_TO>
|
||||||
</DESCRIPTION>
|
<STATUS>pending</STATUS>
|
||||||
<ASSIGNED_TO>agent-developer</ASSIGNED_TO>
|
|
||||||
<LABELS>type::refactoring,feature::item-edit,status::pending</LABELS>
|
|
||||||
</META>
|
</META>
|
||||||
|
|
||||||
<SPECIFICATION>
|
<TASK_BREAKDOWN>
|
||||||
<GOAL>
|
<STEP n="1" file="app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditViewModel.kt">
|
||||||
The primary goal is to refactor the `ItemEditScreen` to include all available fields from the Homebox API for creating and updating items, and to present them in a user-friendly manner.
|
<ACTION>Загрузка списков локаций и меток.</ACTION>
|
||||||
</GOAL>
|
|
||||||
|
|
||||||
<CONTEXT>
|
|
||||||
<FILE_LIST>
|
|
||||||
<FILE path="domain/src/main/java/com/homebox/lens/domain/model/Item.kt" />
|
|
||||||
<FILE path="data/src/main/java/com/homebox/lens/data/db/entity/ItemEntity.kt" />
|
|
||||||
<FILE path="data/src/main/java/com/homebox/lens/data/api/dto/ItemCreateDto.kt" />
|
|
||||||
<FILE path="data/src/main/java/com/homebox/lens/data/api/dto/ItemUpdateDto.kt" />
|
|
||||||
<FILE path="data/src/main/java/com/homebox/lens/data/api/dto/ItemOutDto.kt" />
|
|
||||||
<FILE path="data/src/main/java/com/homebox/lens/data/db/entity/Mapper.kt" />
|
|
||||||
<FILE path="app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditViewModel.kt" />
|
|
||||||
<FILE path="app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditScreen.kt" />
|
|
||||||
</FILE_LIST>
|
|
||||||
</CONTEXT>
|
|
||||||
</SPECIFICATION>
|
|
||||||
|
|
||||||
<EXECUTION_PLAN>
|
|
||||||
<STEP id="1" name="Update Domain Model">
|
|
||||||
<ACTION>Modify `domain/src/main/java/com/homebox/lens/domain/model/Item.kt`.</ACTION>
|
|
||||||
<DETAILS>
|
<DETAILS>
|
||||||
- Add the following fields to the `Item` data class:
|
1. Внедрите `GetAllLocationsUseCase` и `GetAllLabelsUseCase` в `ItemEditViewModel`.
|
||||||
- `archived: Boolean`
|
2. Обновите `ItemEditUiState`, добавив два новых поля: `val allLocations: List<Location> = emptyList()` и `val allLabels: List<Label> = emptyList()`.
|
||||||
- `assetId: String?`
|
3. В функции `loadItem`, после загрузки основной информации о товаре, вызовите `getAllLocationsUseCase` и `getAllLabelsUseCase` и обновите `uiState` полученными списками.
|
||||||
- `fields: List<CustomField>`
|
4. Добавьте публичные методы `updateLocation(location: Location)` и `updateLabels(labels: List<Label>)` для обновления `item` в `uiState`.
|
||||||
- `insured: Boolean`
|
|
||||||
- `lifetimeWarranty: Boolean`
|
|
||||||
- `manufacturer: String?`
|
|
||||||
- `modelNumber: String?`
|
|
||||||
- `notes: String?`
|
|
||||||
- `parentId: String?`
|
|
||||||
- `purchaseFrom: String?`
|
|
||||||
- `purchaseTime: String?`
|
|
||||||
- `serialNumber: String?`
|
|
||||||
- `soldNotes: String?`
|
|
||||||
- `soldPrice: Double?`
|
|
||||||
- `soldTime: String?`
|
|
||||||
- `soldTo: String?`
|
|
||||||
- `syncChildItemsLocations: Boolean`
|
|
||||||
- `warrantyDetails: String?`
|
|
||||||
- `warrantyExpires: String?`
|
|
||||||
- Rename the existing `value: BigDecimal?` field to `purchasePrice: Double?`.
|
|
||||||
</DETAILS>
|
</DETAILS>
|
||||||
</STEP>
|
</STEP>
|
||||||
|
|
||||||
<STEP id="2" name="Update Data Layer">
|
<STEP n="2" file="app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditScreen.kt">
|
||||||
<ACTION>Update database entity, DTOs, and mappers.</ACTION>
|
<ACTION>Реализация UI для выбора локации.</ACTION>
|
||||||
<DETAILS>
|
<DETAILS>
|
||||||
- Modify `data/src/main/java/com/homebox/lens/data/db/entity/ItemEntity.kt` to reflect the new `Item` model.
|
1. Замените `OutlinedTextField` для локации на `ExposedDropdownMenuBox`.
|
||||||
- Ensure `ItemCreateDto`, `ItemUpdateDto`, and `ItemOutDto` contain all necessary fields according to the API spec.
|
2. В качестве `dropdownMenu` используйте `DropdownMenuItem` для каждого элемента из `uiState.allLocations`.
|
||||||
- Update the mapping functions in `data/src/main/java/com/homebox/lens/data/db/entity/Mapper.kt` to handle all new fields correctly across all layers.
|
3. При выборе элемента из списка вызывайте `viewModel.updateLocation(selectedLocation)`.
|
||||||
|
4. В `ExposedDropdownMenuBox` должно отображаться `item.location?.name`.
|
||||||
</DETAILS>
|
</DETAILS>
|
||||||
</STEP>
|
</STEP>
|
||||||
|
|
||||||
<STEP id="3" name="Update ViewModel">
|
<STEP n="3" file="app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditScreen.kt">
|
||||||
<ACTION>Modify `app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditViewModel.kt`.</ACTION>
|
<ACTION>Реализация UI для выбора меток (множественный выбор).</ACTION>
|
||||||
<DETAILS>
|
<DETAILS>
|
||||||
- Update `loadItem` and `saveItem` to handle the state for all new fields.
|
1. Поле для меток `Labels` должно оставаться `OutlinedTextField` (read-only), но `onClick` по нему должен открывать диалоговое окно (`AlertDialog`).
|
||||||
- Add public functions to update the state for each new field (e.g., `updateManufacturer(String)`, `toggleInsured(Boolean)`).
|
2. В `AlertDialog` отобразите список всех меток (`uiState.allLabels`) с `Checkbox`'ами.
|
||||||
|
3. Состояние `Checkbox`'ов должно соответствовать списку `item.labels`.
|
||||||
|
4. При нажатии на "OK" в диалоге, вызывайте `viewModel.updateLabels(selectedLabels)`.
|
||||||
</DETAILS>
|
</DETAILS>
|
||||||
</STEP>
|
</STEP>
|
||||||
|
</TASK_BREAKDOWN>
|
||||||
|
|
||||||
<STEP id="4" name="Implement UI">
|
<ACCEPTANCE_CRITERIA>
|
||||||
<ACTION>Refactor `app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditScreen.kt`.</ACTION>
|
<CRITERION>При нажатии на поле "Location" открывается выпадающий список со всеми локациями.</CRITERION>
|
||||||
<DETAILS>
|
<CRITERION>Выбранная локация отображается в поле и сохраняется вместе с элементом.</CRITERION>
|
||||||
- Implement a user-friendly layout using tabs or expandable cards for the following groups: General, Purchase, Warranty, Identification, Status & Notes.
|
<CRITERION>При нажатии на поле "Labels" открывается диалоговое окно со списком всех меток и чекбоксами.</CRITERION>
|
||||||
- Use appropriate controls for each field type (e.g., `Switch` for booleans, Date Picker for dates).
|
<CRITERION>Выбранные метки отображаются в поле и сохраняются вместе с элементом.</CRITERION>
|
||||||
- Connect all UI controls to the ViewModel.
|
</ACCEPTANCE_CRITERIA>
|
||||||
- Implement basic input validation.
|
|
||||||
</DETAILS>
|
|
||||||
</STEP>
|
|
||||||
</EXECUTION_PLAN>
|
|
||||||
|
|
||||||
<VERIFICATION>
|
</WORK_ORDER>
|
||||||
<CHECK>The application compiles successfully.</CHECK>
|
|
||||||
<CHECK>The Item Edit screen displays all new fields, grouped logically.</CHECK>
|
|
||||||
<CHECK>Creating a new item with all fields populated works correctly.</CHECK>
|
|
||||||
<CHECK>Editing an existing item and modifying the new fields works correctly.</CHECK>
|
|
||||||
<CHECK>Data is persisted correctly and is visible in the Item Details screen after saving.</CHECK>
|
|
||||||
</VERIFICATION>
|
|
||||||
|
|
||||||
</WORK_ORDER>
|
|
||||||
]]>
|
|
||||||
59
tasks/work_order_login_screen.xml
Normal file
59
tasks/work_order_login_screen.xml
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<WORK_ORDER>
|
||||||
|
<META>
|
||||||
|
<ID>WO-LOGIN-REFACTOR</ID>
|
||||||
|
<TITLE>[ARCHITECT -> DEV] Рефакторинг экрана входа и логики первого запуска</TITLE>
|
||||||
|
<DESCRIPTION>Цель этой задачи - изменить логику запуска приложения. Экран входа (SetupScreen) должен появляться только при первом запуске, когда учетные данные еще не сохранены. В последующие запуски пользователь должен сразу попадать на главный экран (Dashboard). Также необходимо улучшить визуальное оформление экрана входа.</DESCRIPTION>
|
||||||
|
<CREATED_BY>architect-agent</CREATED_BY>
|
||||||
|
<ASSIGNED_TO>developer-agent</ASSIGNED_TO>
|
||||||
|
<STATUS>pending</STATUS>
|
||||||
|
</META>
|
||||||
|
|
||||||
|
<TASK_BREAKDOWN>
|
||||||
|
<STEP n="1" file="app/src/main/java/com/homebox/lens/ui/screen/setup/SetupViewModel.kt">
|
||||||
|
<ACTION>Добавить public-метод для синхронной проверки наличия учетных данных.</ACTION>
|
||||||
|
<DETAILS>
|
||||||
|
Добавьте в класс `SetupViewModel` новый метод `fun areCredentialsSaved(): Boolean`.
|
||||||
|
Этот метод должен синхронно проверять, сохранены ли учетные данные в `CredentialsRepository`.
|
||||||
|
Текущая реализация `getCredentials()` асинхронна, что не подходит для быстрой проверки в `NavGraph`.
|
||||||
|
Вам может потребоваться изменить `CredentialsRepository` для поддержки синхронной проверки (например, используя `SharedPreferences` напрямую).
|
||||||
|
</DETAILS>
|
||||||
|
</STEP>
|
||||||
|
|
||||||
|
<STEP n="2" file="app/src/main/java/com/homebox/lens/ui/screen/splash/SplashScreen.kt">
|
||||||
|
<ACTION>Создать новый `SplashScreen`.</ACTION>
|
||||||
|
<DETAILS>
|
||||||
|
Создайте новый Composable-экран `SplashScreen.kt`.
|
||||||
|
Этот экран будет новой точкой входа в `NavGraph`.
|
||||||
|
Он будет использовать `SetupViewModel` для вызова `areCredentialsSaved()` и, в зависимости от результата, немедленно навигироваться либо на `Screen.Setup`, либо на `Screen.Dashboard`.
|
||||||
|
Пока идет проверка, на экране должен отображаться `CircularProgressIndicator`.
|
||||||
|
</DETAILS>
|
||||||
|
</STEP>
|
||||||
|
|
||||||
|
<STEP n="3" file="app/src/main/java/com/homebox/lens/navigation/NavGraph.kt">
|
||||||
|
<ACTION>Обновить `NavGraph` для использования `SplashScreen`.</ACTION>
|
||||||
|
<DETAILS>
|
||||||
|
Измените `startDestination` в `NavHost` на `Screen.Splash.route`.
|
||||||
|
Добавьте `composable` для `SplashScreen`.
|
||||||
|
В `SplashScreen` вызовите `navController.navigate` с очисткой бэкстека (`popUpTo(Screen.Splash.route) { inclusive = true }`), чтобы пользователь не мог вернуться на сплэш-экран.
|
||||||
|
</DETAILS>
|
||||||
|
</STEP>
|
||||||
|
|
||||||
|
<STEP n="4" file="app/src/main/java/com/homebox/lens/ui/screen/setup/SetupScreen.kt">
|
||||||
|
<ACTION>Улучшить UI экрана `SetupScreen`.</ACTION>
|
||||||
|
<DETAILS>
|
||||||
|
Текущий UI слишком прост. Добавьте заголовок, иконку приложения, и более приятное расположение элементов.
|
||||||
|
Используйте `Card` для группировки полей ввода. Добавьте `Spacer` для лучшего отступа.
|
||||||
|
Кнопку "Connect" сделайте более заметной.
|
||||||
|
</DETAILS>
|
||||||
|
</STEP>
|
||||||
|
|
||||||
|
</TASK_BREAKDOWN>
|
||||||
|
|
||||||
|
<ACCEPTANCE_CRITERIA>
|
||||||
|
<CRITERION>При первом запуске приложения открывается `SetupScreen`.</CRITERION>
|
||||||
|
<CRITERION>После успешного ввода данных и входа, при последующих перезапусках приложения открывается `DashboardScreen`, минуя `SetupScreen`.</CRITERION>
|
||||||
|
<CRITERION>`SetupScreen` имеет улучшенный и более привлекательный дизайн.</CRITERION>
|
||||||
|
</ACCEPTANCE_CRITERIA>
|
||||||
|
|
||||||
|
</WORK_ORDER>
|
||||||
Reference in New Issue
Block a user