Labels + Location list

This commit is contained in:
2025-08-09 11:53:33 +03:00
parent 8db12a7599
commit c69f255fff
10 changed files with 581 additions and 28 deletions

View File

@@ -57,10 +57,28 @@ fun NavGraph() {
ItemEditScreen() ItemEditScreen()
} }
composable(route = Screen.LabelsList.route) { composable(route = Screen.LabelsList.route) {
LabelsListScreen() LabelsListScreen(
onNavigateBack = { navController.popBackStack() },
onLabelClick = { labelId ->
// TODO: Navigate to a pre-filtered inventory list screen
navController.navigate(Screen.InventoryList.route)
},
onAddNewLabelClick = {
// TODO: Navigate to a screen for creating a new label
}
)
} }
composable(route = Screen.LocationsList.route) { composable(route = Screen.LocationsList.route) {
LocationsListScreen() LocationsListScreen(
onNavigateBack = { navController.popBackStack() },
onLocationClick = { locationId ->
// TODO: Navigate to a pre-filtered inventory list screen
navController.navigate(Screen.InventoryList.route)
},
onAddNewLocationClick = {
// TODO: Navigate to a screen for creating a new location
}
)
} }
composable(route = Screen.Search.route) { composable(route = Screen.Search.route) {
SearchScreen() SearchScreen()

View File

@@ -1,22 +1,172 @@
// [PACKAGE] com.homebox.lens.ui.screen.labelslist // [PACKAGE] com.homebox.lens.ui.screen.labelslist
// [FILE] LabelsListScreen.kt // [FILE] LabelsListScreen.kt
// [SEMANTICS] ui, screen, labels_list, compose
package com.homebox.lens.ui.screen.labelslist package com.homebox.lens.ui.screen.labelslist
import androidx.compose.material3.Text import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
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.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.filled.Add
import androidx.compose.material3.*
import androidx.compose.runtime.Composable 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.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.homebox.lens.R
import com.homebox.lens.domain.model.LabelOut
import com.homebox.lens.ui.theme.HomeboxLensTheme
// [ENTRYPOINT] // [ENTRYPOINT]
/**
* [CONTRACT]
* @summary Главная Composable-функция для экрана списка меток.
* @param onNavigateBack Функция для навигации на предыдущий экран.
* @param onLabelClick Функция, вызываемая при нажатии на метку.
* @param onAddNewLabelClick Функция, вызываемая при нажатии на FAB для добавления новой метки.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun LabelsListScreen() { fun LabelsListScreen(
// [ACTION] viewModel: LabelsListViewModel = hiltViewModel(),
Text(text = "Labels List Screen") onNavigateBack: () -> Unit,
onLabelClick: (String) -> Unit,
onAddNewLabelClick: () -> Unit
) {
// [STATE]
val uiState by viewModel.uiState.collectAsState()
// [UI]
Scaffold(
topBar = {
TopAppBar(
title = { Text(stringResource(id = R.string.nav_labels)) },
navigationIcon = {
IconButton(onClick = onNavigateBack) {
Icon(
imageVector = Icons.Filled.ArrowBack,
contentDescription = stringResource(id = R.string.cd_navigate_back)
)
}
}
)
},
floatingActionButton = {
FloatingActionButton(onClick = onAddNewLabelClick) {
Icon(
imageVector = Icons.Default.Add,
contentDescription = stringResource(id = R.string.cd_add_new_label)
)
}
}
) { paddingValues ->
// [ANCHOR] Основной контент в зависимости от состояния
Box(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
) {
when (val state = uiState) {
is LabelsListUiState.Loading -> {
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
}
is LabelsListUiState.Error -> {
Text(
text = state.message,
color = MaterialTheme.colorScheme.error,
textAlign = TextAlign.Center,
modifier = Modifier.align(Alignment.Center).padding(16.dp)
)
}
is LabelsListUiState.Success -> {
LabelsList(
labels = state.labels,
onLabelClick = onLabelClick
)
}
}
}
}
} }
@Preview(showBackground = true) // [HELPER]
/**
* [CONTRACT]
* @summary Отображает список меток.
*/
@Composable @Composable
fun LabelsListScreenPreview() { private fun LabelsList(
LabelsListScreen() labels: List<LabelOut>,
onLabelClick: (String) -> Unit,
modifier: Modifier = Modifier
) {
LazyColumn(
modifier = modifier.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
items(labels) { label ->
LabelListItem(
label = label,
onClick = { onLabelClick(label.id) }
)
}
}
} }
// [END_FILE_LabelsListScreen.kt]
// [HELPER]
/**
* [CONTRACT]
* @summary Отображает один элемент списка меток.
*/
@Composable
private fun LabelListItem(
label: LabelOut,
onClick: () -> Unit
) {
ListItem(
headlineContent = { Text(label.name) },
leadingContent = {
Icon(
imageVector = Icons.Filled.PlayArrow,
contentDescription = null
)
},
modifier = Modifier.clickable(onClick = onClick)
)
}
// [PREVIEW]
@Preview(showBackground = true, name = "Labels List Success")
@Composable
private fun LabelsListScreenSuccessPreview() {
val labels = listOf(
LabelOut("1", "Electronics", "#FF0000", false, "", ""),
LabelOut("2", "Books", "#00FF00", false, "", ""),
LabelOut("3", "Winter Clothes", "#0000FF", false, "", "")
)
HomeboxLensTheme {
LabelsList(labels = labels, onLabelClick = {})
}
}
// [PREVIEW]
@Preview(showBackground = true, name = "Labels List Empty")
@Composable
private fun LabelsListScreenEmptyPreview() {
HomeboxLensTheme {
LabelsList(labels = emptyList(), onLabelClick = {})
}
}
// [END_FILE_LabelsListScreen.kt]

View File

@@ -0,0 +1,36 @@
// [PACKAGE] com.homebox.lens.ui.screen.labelslist
// [FILE] LabelsListUiState.kt
// [SEMANTICS] ui, state, labels_list
package com.homebox.lens.ui.screen.labelslist
import com.homebox.lens.domain.model.LabelOut
// [CORE-LOGIC]
// [ENTITY: SealedInterface('LabelsListUiState')]
/**
* [CONTRACT]
* Определяет все возможные состояния для экрана "Список меток".
* @invariant В любой момент времени экран может находиться только в одном из этих состояний.
*/
sealed interface LabelsListUiState {
/**
* [CONTRACT]
* Состояние успешной загрузки данных.
* @property labels Список меток.
*/
data class Success(val labels: List<LabelOut>) : LabelsListUiState
/**
* [CONTRACT]
* Состояние ошибки во время загрузки данных.
* @property message Человекочитаемое сообщение об ошибке.
*/
data class Error(val message: String) : LabelsListUiState
/**
* [CONTRACT]
* Состояние, когда данные для экрана загружаются.
*/
data object Loading : LabelsListUiState
}
// [END_FILE_LabelsListUiState.kt]

View File

@@ -1,16 +1,73 @@
// [PACKAGE] com.homebox.lens.ui.screen.labelslist // [PACKAGE] com.homebox.lens.ui.screen.labelslist
// [FILE] LabelsListViewModel.kt // [FILE] LabelsListViewModel.kt
// [SEMANTICS] ui_logic, labels_list, state_management
package com.homebox.lens.ui.screen.labelslist package com.homebox.lens.ui.screen.labelslist
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.homebox.lens.domain.usecase.GetAllLabelsUseCase
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
// [VIEWMODEL] // [VIEWMODEL]
// [ENTITY: ViewModel('LabelsListViewModel')]
/**
* [CONTRACT]
* @summary ViewModel для экрана со списком меток.
* @description Управляет состоянием экрана, загружает список меток и обрабатывает ошибки.
* @invariant `uiState` всегда является одним из состояний, определенных в `LabelsListUiState`.
*/
@HiltViewModel @HiltViewModel
class LabelsListViewModel @Inject constructor() : ViewModel() { class LabelsListViewModel @Inject constructor(
private val getAllLabelsUseCase: GetAllLabelsUseCase
) : ViewModel() {
// [STATE] // [STATE]
// TODO: Implement UI state private val _uiState = MutableStateFlow<LabelsListUiState>(LabelsListUiState.Loading)
val uiState = _uiState.asStateFlow()
// [LIFECYCLE_HANDLER]
init {
loadLabels()
}
/**
* [CONTRACT]
* @summary Загружает список меток.
* @description Выполняет `GetAllLabelsUseCase` и обновляет UI, переключая его
* между состояниями `Loading`, `Success` и `Error`.
* @sideeffect Асинхронно обновляет `_uiState`.
*/
fun loadLabels() {
// [ENTRYPOINT]
viewModelScope.launch {
_uiState.value = LabelsListUiState.Loading
Timber.i("[ACTION] Starting labels list load. State -> Loading.")
// [CORE-LOGIC]
val result = runCatching {
getAllLabelsUseCase()
}
// [RESULT_HANDLER]
result.fold(
onSuccess = { labels ->
Timber.i("[SUCCESS] Labels loaded successfully. Count: ${labels.size}. State -> Success.")
_uiState.value = LabelsListUiState.Success(labels)
},
onFailure = { exception ->
Timber.e(exception, "[ERROR] Failed to load labels. State -> Error.")
_uiState.value = LabelsListUiState.Error(
message = exception.message ?: "Could not load labels."
)
}
)
}
}
// [END_CLASS_LabelsListViewModel]
} }
// [END_FILE_LabelsListViewModel.kt] // [END_FILE_LabelsListViewModel.kt]

View File

@@ -1,22 +1,175 @@
// [PACKAGE] com.homebox.lens.ui.screen.locationslist // [PACKAGE] com.homebox.lens.ui.screen.locationslist
// [FILE] LocationsListScreen.kt // [FILE] LocationsListScreen.kt
// [SEMANTICS] ui, screen, locations_list, compose
package com.homebox.lens.ui.screen.locationslist package com.homebox.lens.ui.screen.locationslist
import androidx.compose.material3.Text import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
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.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Place
import androidx.compose.material3.*
import androidx.compose.runtime.Composable 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.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.homebox.lens.R
import com.homebox.lens.domain.model.LocationOutCount
import com.homebox.lens.ui.theme.HomeboxLensTheme
// [ENTRYPOINT] // [ENTRYPOINT]
/**
* [CONTRACT]
* @summary Главная Composable-функция для экрана списка локаций.
* @param onNavigateBack Функция для навигации на предыдущий экран.
* @param onLocationClick Функция, вызываемая при нажатии на локацию.
* @param onAddNewLocationClick Функция, вызываемая при нажатии на FAB для добавления новой локации.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun LocationsListScreen() { fun LocationsListScreen(
// [ACTION] viewModel: LocationsListViewModel = hiltViewModel(),
Text(text = "Locations List Screen") onNavigateBack: () -> Unit,
onLocationClick: (String) -> Unit,
onAddNewLocationClick: () -> Unit
) {
// [STATE]
val uiState by viewModel.uiState.collectAsState()
// [UI]
Scaffold(
topBar = {
TopAppBar(
title = { Text(stringResource(id = R.string.nav_locations)) },
navigationIcon = {
IconButton(onClick = onNavigateBack) {
Icon(
imageVector = Icons.Filled.ArrowBack,
contentDescription = stringResource(id = R.string.cd_navigate_back)
)
}
}
)
},
floatingActionButton = {
FloatingActionButton(onClick = onAddNewLocationClick) {
Icon(
imageVector = Icons.Default.Add,
contentDescription = stringResource(id = R.string.cd_add_new_location)
)
}
}
) { paddingValues ->
// [ANCHOR] Основной контент в зависимости от состояния
Box(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
) {
when (val state = uiState) {
is LocationsListUiState.Loading -> {
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
}
is LocationsListUiState.Error -> {
Text(
text = state.message,
color = MaterialTheme.colorScheme.error,
textAlign = TextAlign.Center,
modifier = Modifier.align(Alignment.Center).padding(16.dp)
)
}
is LocationsListUiState.Success -> {
LocationsList(
locations = state.locations,
onLocationClick = onLocationClick
)
}
}
}
}
} }
@Preview(showBackground = true) // [HELPER]
/**
* [CONTRACT]
* @summary Отображает список локаций.
*/
@Composable @Composable
fun LocationsListScreenPreview() { private fun LocationsList(
LocationsListScreen() locations: List<LocationOutCount>,
onLocationClick: (String) -> Unit,
modifier: Modifier = Modifier
) {
LazyColumn(
modifier = modifier.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
items(locations) { location ->
LocationListItem(
location = location,
onClick = { onLocationClick(location.id) }
)
}
}
} }
// [END_FILE_LocationsListScreen.kt]
// [HELPER]
/**
* [CONTRACT]
* @summary Отображает один элемент списка локаций.
*/
@Composable
private fun LocationListItem(
location: LocationOutCount,
onClick: () -> Unit
) {
ListItem(
headlineContent = { Text(location.name) },
leadingContent = {
Icon(
imageVector = Icons.Default.Place,
contentDescription = null
)
},
trailingContent = {
Text(text = location.itemCount.toString())
},
modifier = Modifier.clickable(onClick = onClick)
)
}
// [PREVIEW]
@Preview(showBackground = true, name = "Locations List Success")
@Composable
private fun LocationsListScreenSuccessPreview() {
val locations = listOf(
LocationOutCount("1", "Garage", "#FF0000", false, 12, "", ""),
LocationOutCount("2", "Kitchen", "#00FF00", false, 3, "", ""),
LocationOutCount("3", "Office", "#0000FF", false, 25, "", "")
)
HomeboxLensTheme {
LocationsList(locations = locations, onLocationClick = {})
}
}
// [PREVIEW]
@Preview(showBackground = true, name = "Locations List Empty")
@Composable
private fun LocationsListScreenEmptyPreview() {
HomeboxLensTheme {
LocationsList(locations = emptyList(), onLocationClick = {})
}
}
// [END_FILE_LocationsListScreen.kt]

View File

@@ -0,0 +1,36 @@
// [PACKAGE] com.homebox.lens.ui.screen.locationslist
// [FILE] LocationsListUiState.kt
// [SEMANTICS] ui, state, locations_list
package com.homebox.lens.ui.screen.locationslist
import com.homebox.lens.domain.model.LocationOutCount
// [CORE-LOGIC]
// [ENTITY: SealedInterface('LocationsListUiState')]
/**
* [CONTRACT]
* Определяет все возможные состояния для экрана "Список локаций".
* @invariant В любой момент времени экран может находиться только в одном из этих состояний.
*/
sealed interface LocationsListUiState {
/**
* [CONTRACT]
* Состояние успешной загрузки данных.
* @property locations Список локаций со счетчиками.
*/
data class Success(val locations: List<LocationOutCount>) : LocationsListUiState
/**
* [CONTRACT]
* Состояние ошибки во время загрузки данных.
* @property message Человекочитаемое сообщение об ошибке.
*/
data class Error(val message: String) : LocationsListUiState
/**
* [CONTRACT]
* Состояние, когда данные для экрана загружаются.
*/
data object Loading : LocationsListUiState
}
// [END_FILE_LocationsListUiState.kt]

View File

@@ -1,16 +1,73 @@
// [PACKAGE] com.homebox.lens.ui.screen.locationslist // [PACKAGE] com.homebox.lens.ui.screen.locationslist
// [FILE] LocationsListViewModel.kt // [FILE] LocationsListViewModel.kt
// [SEMANTICS] ui_logic, locations_list, state_management
package com.homebox.lens.ui.screen.locationslist package com.homebox.lens.ui.screen.locationslist
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.homebox.lens.domain.usecase.GetAllLocationsUseCase
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
// [VIEWMODEL] // [VIEWMODEL]
// [ENTITY: ViewModel('LocationsListViewModel')]
/**
* [CONTRACT]
* @summary ViewModel для экрана со списком локаций.
* @description Управляет состоянием экрана, загружает список локаций и обрабатывает ошибки.
* @invariant `uiState` всегда является одним из состояний, определенных в `LocationsListUiState`.
*/
@HiltViewModel @HiltViewModel
class LocationsListViewModel @Inject constructor() : ViewModel() { class LocationsListViewModel @Inject constructor(
private val getAllLocationsUseCase: GetAllLocationsUseCase
) : ViewModel() {
// [STATE] // [STATE]
// TODO: Implement UI state private val _uiState = MutableStateFlow<LocationsListUiState>(LocationsListUiState.Loading)
val uiState = _uiState.asStateFlow()
// [LIFECYCLE_HANDLER]
init {
loadLocations()
}
/**
* [CONTRACT]
* @summary Загружает список локаций.
* @description Выполняет `GetAllLocationsUseCase` и обновляет UI, переключая его
* между состояниями `Loading`, `Success` и `Error`.
* @sideeffect Асинхронно обновляет `_uiState`.
*/
fun loadLocations() {
// [ENTRYPOINT]
viewModelScope.launch {
_uiState.value = LocationsListUiState.Loading
Timber.i("[ACTION] Starting locations list load. State -> Loading.")
// [CORE-LOGIC]
val result = runCatching {
getAllLocationsUseCase()
}
// [RESULT_HANDLER]
result.fold(
onSuccess = { locations ->
Timber.i("[SUCCESS] Locations loaded successfully. Count: ${locations.size}. State -> Success.")
_uiState.value = LocationsListUiState.Success(locations)
},
onFailure = { exception ->
Timber.e(exception, "[ERROR] Failed to load locations. State -> Error.")
_uiState.value = LocationsListUiState.Error(
message = exception.message ?: "Could not load locations."
)
}
)
}
}
// [END_CLASS_LocationsListViewModel]
} }
// [END_FILE_LocationsListViewModel.kt] // [END_FILE_LocationsListViewModel.kt]

View File

@@ -12,6 +12,9 @@
<!-- Content Descriptions --> <!-- Content Descriptions -->
<string name="cd_open_navigation_drawer">Open navigation drawer</string> <string name="cd_open_navigation_drawer">Open navigation drawer</string>
<string name="cd_scan_qr_code">Scan QR code</string> <string name="cd_scan_qr_code">Scan QR code</string>
<string name="cd_navigate_back">Navigate back</string>
<string name="cd_add_new_location">Add new location</string>
<string name="cd_add_new_label">Add new label</string>
<!-- Dashboard Screen --> <!-- Dashboard Screen -->
<string name="dashboard_title">Dashboard</string> <string name="dashboard_title">Dashboard</string>
@@ -28,5 +31,6 @@
<!-- Navigation --> <!-- Navigation -->
<string name="nav_locations">Locations</string> <string name="nav_locations">Locations</string>
<string name="nav_labels">Labels</string>
</resources> </resources>

View File

@@ -12,6 +12,9 @@
<!-- Content Descriptions --> <!-- Content Descriptions -->
<string name="cd_open_navigation_drawer">Открыть боковое меню</string> <string name="cd_open_navigation_drawer">Открыть боковое меню</string>
<string name="cd_scan_qr_code">Сканировать QR-код</string> <string name="cd_scan_qr_code">Сканировать QR-код</string>
<string name="cd_navigate_back">Вернуться назад</string>
<string name="cd_add_new_location">Добавить новую локацию</string>
<string name="cd_add_new_label">Добавить новую метку</string>
<!-- Dashboard Screen --> <!-- Dashboard Screen -->
<string name="dashboard_title">Главная</string> <string name="dashboard_title">Главная</string>
@@ -28,5 +31,6 @@
<!-- Navigation --> <!-- Navigation -->
<string name="nav_locations">Локации</string> <string name="nav_locations">Локации</string>
<string name="nav_labels">Метки</string>
</resources> </resources>

View File

@@ -231,6 +231,44 @@
</USER_INTERACTIONS> </USER_INTERACTIONS>
</SCREEN> </SCREEN>
<!-- [ЯКОРЬ] Конец спецификации UI для экрана Локаций. --> <!-- [ЯКОРЬ] Конец спецификации UI для экрана Локаций. -->
<!-- [ЯКОРЬ] Начало спецификации UI для экрана Меток. -->
<SCREEN id="screen_labels_list" status="defined">
<summary>Экран "Метки"</summary>
<description>
Отображает вертикальный список всех доступных меток. Экран должен быть интегрирован в общую структуру навигации приложения.
</description>
<LAYOUT>
<COMPONENT type="TopAppBar">
<description>Общая верхняя панель приложения с заголовком "Метки" и кнопкой "назад".</description>
</COMPONENT>
<COMPONENT type="MainContent" orientation="vertical">
<description>Основная область контента, занимающая все доступное пространство под TopAppBar.</description>
<SUB_COMPONENT type="List" name="LabelsList">
<description>Вертикальный, прокручиваемый список (LazyColumn) всех меток.</description>
<ELEMENT type="ListItem">
<description>Элемент списка, представляющий одну метку. Состоит из иконки (например, 'label') и названия метки. Весь элемент является кликабельным и ведет на экран со списком предметов с данной меткой.</description>
</ELEMENT>
</SUB_COMPONENT>
</COMPONENT>
<COMPONENT type="FloatingActionButton" icon="add">
<description>
Плавающая кнопка действия, расположенная в правом нижнем углу. Позволяет пользователю добавить новую метку.
</description>
</COMPONENT>
</LAYOUT>
<USER_INTERACTIONS>
<INTERACTION>
<action>Нажатие на элемент списка меток</action>
<reaction>Осуществляется навигация на экран списка инвентаря, отфильтрованного по выбранной метке.</reaction>
</INTERACTION>
<INTERACTION>
<action>Нажатие на FloatingActionButton</action>
<reaction>Открывается диалоговое окно или новый экран для создания новой метки.</reaction>
</INTERACTION>
</USER_INTERACTIONS>
</SCREEN>
<!-- [ЯКОРЬ] Конец спецификации UI для экрана Меток. -->
</UI_SPECIFICATIONS> </UI_SPECIFICATIONS>