REFACTOR END

This commit is contained in:
2025-09-28 10:10:01 +03:00
parent 394e0040de
commit 9b914b2904
117 changed files with 3070 additions and 5447 deletions

View File

@@ -0,0 +1,79 @@
// [FILE] ui/common/build.gradle.kts
// [SEMANTICS] build, dependencies, common, ui, hilt, compose
// [PURPOSE] Build script for the shared UI common module.
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
id("org.jetbrains.kotlin.plugin.compose")
id("com.google.dagger.hilt.android")
id("kotlin-kapt")
}
android {
namespace = "com.homebox.lens.ui.common"
compileSdk = Versions.compileSdk
defaultConfig {
minSdk = Versions.minSdk
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
buildFeatures {
compose = true
}
packaging {
resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
}
}
}
dependencies {
// [MODULE_DEPENDENCY] Core modules
implementation(project(":data"))
implementation(project(":domain"))
// [DEPENDENCY] AndroidX
implementation(Libs.coreKtx)
implementation(Libs.lifecycleRuntime)
implementation(Libs.activityCompose)
// [DEPENDENCY] Compose
implementation(Libs.composeUi)
implementation(Libs.composeUiGraphics)
implementation(Libs.composeFoundation)
implementation(Libs.composeUiToolingPreview)
implementation(Libs.composeMaterial3)
implementation(Libs.composeMaterialIconsExtended)
implementation(Libs.navigationCompose)
implementation(Libs.hiltNavigationCompose)
// [DEPENDENCY] DI (Hilt)
implementation(Libs.hiltAndroid)
kapt(Libs.hiltCompiler)
// [DEPENDENCY] Logging
implementation(Libs.timber)
// [DEPENDENCY] Testing
testImplementation(Libs.junit)
androidTestImplementation(Libs.extJunit)
androidTestImplementation(Libs.espressoCore)
androidTestImplementation(Libs.composeUiTestJunit4)
debugImplementation(Libs.composeUiTooling)
debugImplementation(Libs.composeUiTestManifest)
}
kapt {
correctErrorTypes = true
}
// [END_FILE_ui/common/build.gradle.kts]

View File

@@ -0,0 +1,127 @@
// [FILE] AppDrawer.kt
// [SEMANTICS] ui, common, navigation_drawer
package com.homebox.lens.ui.common
// [IMPORTS]
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.Button
import androidx.compose.material3.Divider
import androidx.compose.material3.Icon
import androidx.compose.material3.ModalDrawerSheet
import androidx.compose.material3.NavigationDrawerItem
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.homebox.lens.ui.common.NavigationActions
import timber.log.Timber
// [END_IMPORTS]
// [ANCHOR:AppDrawerContent:Function]
// [RELATION:DEPENDS_ON:NavigationActions]
// [CONTRACT:AppDrawerContent]
// [PURPOSE] Контент для бокового навигационного меню (Drawer).
// [PRE] currentRoute is not null or empty.
// [POST] Drawer content is rendered with navigation items.
// [PARAM:currentRoute:String?] Текущий маршрут для подсветки активного элемента.
// [PARAM:navigationActions:NavigationActions] Объект с навигационными действиями.
// [PARAM:onCloseDrawer:() -> Unit] Лямбда для закрытия бокового меню.
// [END_CONTRACT:AppDrawerContent]
@Composable
internal fun AppDrawerContent(
currentRoute: String?,
navigationActions: NavigationActions,
onCloseDrawer: () -> Unit,
) {
check(currentRoute != null) { "currentRoute must not be null" }
ModalDrawerSheet {
Column {
Spacer(Modifier.height(12.dp))
Button(
onClick = {
Timber.i("[INFO][ACTION][create_item_button_click]", "Create Item button clicked")
navigationActions.navigateToCreateItem()
onCloseDrawer()
},
modifier =
Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
) {
Icon(Icons.Default.Add, contentDescription = null, modifier = Modifier.size(18.dp))
Spacer(Modifier.width(8.dp))
Text("Create")
}
Spacer(Modifier.height(12.dp))
Divider()
NavigationDrawerItem(
label = { Text("Dashboard") },
selected = currentRoute == "dashboard",
onClick = {
Timber.i("[INFO][ACTION][navigate_to_dashboard_from_drawer]", "Dashboard item clicked")
navigationActions.navigateToDashboard()
onCloseDrawer()
},
)
NavigationDrawerItem(
label = { Text("Locations") },
selected = currentRoute == "locations",
onClick = {
Timber.i("[INFO][ACTION][navigate_to_locations_from_drawer]", "Locations item clicked")
navigationActions.navigateToLocations()
onCloseDrawer()
},
)
NavigationDrawerItem(
label = { Text("Labels") },
selected = currentRoute == "labels",
onClick = {
Timber.i("[INFO][ACTION][navigate_to_labels_from_drawer]", "Labels item clicked")
navigationActions.navigateToLabels()
onCloseDrawer()
},
)
NavigationDrawerItem(
label = { Text("Search") },
selected = currentRoute == "search",
onClick = {
Timber.i("[INFO][ACTION][navigate_to_search_from_drawer]", "Search item clicked")
navigationActions.navigateToSearch()
onCloseDrawer()
},
)
NavigationDrawerItem(
icon = { Icon(Icons.Default.Settings, contentDescription = null) },
label = { Text("Settings") },
selected = false,
onClick = {
Timber.i("[INFO][ACTION][navigate_to_settings_from_drawer]", "Settings item clicked")
navigationActions.navigateToSettings()
onCloseDrawer()
},
)
Divider()
NavigationDrawerItem(
label = { Text("Logout") },
selected = false,
onClick = {
Timber.i("[INFO][ACTION][logout_from_drawer]", "Logout item clicked")
navigationActions.navigateToLogout()
onCloseDrawer()
},
)
}
}
}
// [END_ANCHOR:AppDrawerContent]
// [END_FILE_AppDrawer.kt]

View File

@@ -0,0 +1,115 @@
// [FILE] MainScaffold.kt
// [SEMANTICS] ui, common, scaffold, navigation_drawer
package com.homebox.lens.ui.common
// [IMPORTS]
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Menu
import androidx.compose.material3.DrawerValue
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.ModalNavigationDrawer
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.rememberDrawerState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import kotlinx.coroutines.launch
import timber.log.Timber
// [END_IMPORTS]
// [ANCHOR:MainScaffold:Function]
// [RELATION:DEPENDS_ON:NavigationActions]
// [RELATION:CALLS:AppDrawerContent]
// [CONTRACT:MainScaffold]
// [PURPOSE] Общая обертка для экранов, включающая Scaffold и Navigation Drawer.
// [PRE] topBarTitle is not null.
// [POST] Scaffold is rendered with TopAppBar, NavigationDrawer, and provided content.
// [PARAM:topBarTitle:String] Заголовок для TopAppBar.
// [PARAM:currentRoute:String?] Текущий маршрут для подсветки активного элемента в Drawer.
// [PARAM:navigationActions:NavigationActions] Объект с навигационными действиями.
// [PARAM:onNavigateUp:(() -> Unit)?] Лямбда для навигации вверх, если есть.
// [PARAM:topBarActions:(@Composable () -> Unit)] Composable-функция для отображения действий (иконок) в TopAppBar.
// [PARAM:snackbarHost:(@Composable () -> Unit)] Composable-функция для отображения Snackbar.
// [PARAM:floatingActionButton:(@Composable () -> Unit)] Composable-функция для отображения FloatingActionButton.
// [PARAM:content:(@Composable (PaddingValues) -> Unit)] Основное содержимое экрана, которое будет отображено внутри Scaffold.
// [SIDE_EFFECT] Управляет состоянием (открыто/закрыто) бокового меню (ModalNavigationDrawer).
// [INVARIANT] TopAppBar всегда отображается с иконкой меню.
// [END_CONTRACT:MainScaffold]
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun mainScaffold(
topBarTitle: String,
currentRoute: String?,
navigationActions: NavigationActions,
onNavigateUp: (() -> Unit)? = null,
topBarActions: @Composable () -> Unit = {},
snackbarHost: @Composable () -> Unit = {},
floatingActionButton: @Composable () -> Unit = {},
content: @Composable (PaddingValues) -> Unit,
) {
check(topBarTitle.isNotBlank()) { "topBarTitle must not be blank" }
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
val scope = rememberCoroutineScope()
ModalNavigationDrawer(
drawerState = drawerState,
drawerContent = {
AppDrawerContent(
currentRoute = currentRoute,
navigationActions = navigationActions,
onCloseDrawer = {
scope.launch {
Timber.i("[INFO][ACTION][close_drawer]", "Navigation drawer closed")
drawerState.close()
}
},
)
},
) {
Scaffold(
topBar = {
TopAppBar(
title = { Text(topBarTitle) },
navigationIcon = {
if (onNavigateUp != null) {
IconButton(onClick = {
Timber.i("[INFO][ACTION][navigate_up]", "Navigate up button clicked")
onNavigateUp()
}) {
Icon(
Icons.Filled.ArrowBack,
contentDescription = "Вернуться", // Hardcoded string for now
)
}
} else {
IconButton(onClick = {
scope.launch {
Timber.i("[INFO][ACTION][open_drawer]", "Open navigation drawer button clicked")
drawerState.open()
}
}) {
Icon(
Icons.Default.Menu,
contentDescription = "Открыть боковое меню", // Hardcoded string for now
)
}
}
},
actions = { topBarActions() },
)
},
snackbarHost = snackbarHost,
floatingActionButton = floatingActionButton,
) { paddingValues ->
content(paddingValues)
}
}
}
// [END_ANCHOR:MainScaffold]
// [END_FILE_MainScaffold.kt]

View File

@@ -0,0 +1,168 @@
// [FILE] NavigationActions.kt
// [SEMANTICS] navigation, controller, actions
package com.homebox.lens.ui.common
// [IMPORTS]
import androidx.navigation.NavHostController
import timber.log.Timber
// [END_IMPORTS]
// [ANCHOR:NavigationActions:Class]
// [RELATION:DEPENDS_ON:NavHostController]
// [CONTRACT:NavigationActions]
// [PURPOSE] Класс-обертка над NavHostController для предоставления типизированных навигационных действий.
// [PRE] navController is not null.
// [POST] All navigation actions use the provided navController.
// [PARAM:navController:NavHostController] Контроллер Jetpack Navigation.
// [END_CONTRACT:NavigationActions]
class NavigationActions(val navController: NavHostController) {
// [AI_NOTE]: Added check for navController not null, as per contract.
init {
check(navController != null) { "navController must not be null" }
}
// [ANCHOR:navigateToDashboard:Function]
// [CONTRACT:navigateToDashboard]
// [PURPOSE] Навигация на главный экран.
// [SIDE_EFFECT] Clears back stack to dashboard to avoid cycles.
// [END_CONTRACT:navigateToDashboard]
fun navigateToDashboard() {
Timber.i("[INFO][ACTION][navigate_to_dashboard]", "Navigating to Dashboard")
navController.navigate("dashboard") {
popUpTo(navController.graph.startDestinationId)
launchSingleTop = true
}
}
// [END_ANCHOR:navigateToDashboard]
// [ANCHOR:navigateToLocations:Function]
// [CONTRACT:navigateToLocations]
// [PURPOSE] Навигация на экран локаций.
// [END_CONTRACT:navigateToLocations]
fun navigateToLocations() {
Timber.i("[INFO][ACTION][navigate_to_locations]", "Navigating to Locations")
navController.navigate("locations") {
launchSingleTop = true
}
}
// [END_ANCHOR:navigateToLocations]
// [ANCHOR:navigateToLabels:Function]
// [CONTRACT:navigateToLabels]
// [PURPOSE] Навигация на экран меток.
// [END_CONTRACT:navigateToLabels]
fun navigateToLabels() {
Timber.i("[INFO][ACTION][navigate_to_labels]", "Navigating to Labels")
navController.navigate("labels") {
launchSingleTop = true
}
}
// [END_ANCHOR:navigateToLabels]
// [ANCHOR:navigateToLabelEdit:Function]
// [CONTRACT:navigateToLabelEdit]
// [PURPOSE] Навигация на редактирование метки.
// [PARAM:labelId:String?] ID метки, optional.
// [END_CONTRACT:navigateToLabelEdit]
fun navigateToLabelEdit(labelId: String? = null) {
Timber.i("[INFO][ACTION][navigate_to_label_edit]", "Navigating to Label Edit", "labelId", labelId)
navController.navigate("label_edit/$labelId")
}
// [END_ANCHOR:navigateToLabelEdit]
// [ANCHOR:navigateToSearch:Function]
// [CONTRACT:navigateToSearch]
// [PURPOSE] Навигация на экран поиска.
// [END_CONTRACT:navigateToSearch]
fun navigateToSearch() {
Timber.i("[INFO][ACTION][navigate_to_search]", "Navigating to Search")
navController.navigate("search") {
launchSingleTop = true
}
}
// [END_ANCHOR:navigateToSearch]
// [ANCHOR:navigateToScan:Function]
// [CONTRACT:navigateToScan]
// [PURPOSE] Навигация на экран сканирования QR/штрих-кодов.
// [END_CONTRACT:navigateToScan]
fun navigateToScan() {
Timber.i("[INFO][ACTION][navigate_to_scan]", "Navigating to Scan screen")
navController.navigate("scan") {
launchSingleTop = true
}
}
// [END_ANCHOR:navigateToScan]
// [ANCHOR:navigateToSettings:Function]
// [CONTRACT:navigateToSettings]
// [PURPOSE] Навигация на экран настроек.
// [END_CONTRACT:navigateToSettings]
fun navigateToSettings() {
Timber.i("[INFO][ACTION][navigate_to_settings]", "Navigating to Settings")
navController.navigate("settings") {
launchSingleTop = true
}
}
// [END_ANCHOR:navigateToSettings]
// [ANCHOR:navigateToInventoryListWithLabel:Function]
// [CONTRACT:navigateToInventoryListWithLabel]
// [PURPOSE] Навигация на список инвентаря с фильтром по метке.
// [PARAM:labelId:String] ID метки для фильтрации.
// [END_CONTRACT:navigateToInventoryListWithLabel]
fun navigateToInventoryListWithLabel(labelId: String) {
Timber.i("[INFO][ACTION][navigate_to_inventory_with_label]", "Navigating to Inventory with label", "labelId", labelId)
val route = "inventory?filter=label:$labelId"
navController.navigate(route)
}
// [END_ANCHOR:navigateToInventoryListWithLabel]
// [ANCHOR:navigateToInventoryListWithLocation:Function]
// [CONTRACT:navigateToInventoryListWithLocation]
// [PURPOSE] Навигация на список инвентаря с фильтром по локации.
// [PARAM:locationId:String] ID локации для фильтрации.
// [END_CONTRACT:navigateToInventoryListWithLocation]
fun navigateToInventoryListWithLocation(locationId: String) {
Timber.i("[INFO][ACTION][navigate_to_inventory_with_location]", "Navigating to Inventory with location", "locationId", locationId)
val route = "inventory?filter=location:$locationId"
navController.navigate(route)
}
// [END_ANCHOR:navigateToInventoryListWithLocation]
// [ANCHOR:navigateToCreateItem:Function]
// [CONTRACT:navigateToCreateItem]
// [PURPOSE] Навигация на экран создания элемента.
// [END_CONTRACT:navigateToCreateItem]
fun navigateToCreateItem() {
Timber.i("[INFO][ACTION][navigate_to_create_item]", "Navigating to Create Item")
navController.navigate("item_edit")
}
// [END_ANCHOR:navigateToCreateItem]
// [ANCHOR:navigateToLogout:Function]
// [CONTRACT:navigateToLogout]
// [PURPOSE] Навигация на экран выхода из системы.
// [SIDE_EFFECT] Clears back stack to dashboard.
// [END_CONTRACT:navigateToLogout]
fun navigateToLogout() {
Timber.i("[INFO][ACTION][navigate_to_logout]", "Navigating to Logout")
navController.navigate("setup") {
popUpTo("dashboard") { inclusive = true }
}
}
// [END_ANCHOR:navigateToLogout]
// [ANCHOR:navigateBack:Function]
// [CONTRACT:navigateBack]
// [PURPOSE] Возврат на предыдущий экран.
// [END_CONTRACT:navigateBack]
fun navigateBack() {
Timber.i("[INFO][ACTION][navigate_back]", "Navigating back")
navController.popBackStack()
}
// [END_ANCHOR:navigateBack]
}
// [END_ANCHOR:NavigationActions]
// [END_FILE_NavigationActions.kt]