REFACTOR END
This commit is contained in:
79
ui/common/build.gradle.kts
Normal file
79
ui/common/build.gradle.kts
Normal 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]
|
||||
127
ui/common/src/main/java/com/homebox/lens/ui/common/AppDrawer.kt
Normal file
127
ui/common/src/main/java/com/homebox/lens/ui/common/AppDrawer.kt
Normal 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]
|
||||
@@ -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]
|
||||
@@ -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]
|
||||
Reference in New Issue
Block a user