initial commit

This commit is contained in:
2025-08-06 11:28:28 +03:00
commit b63eca8440
88 changed files with 3392 additions and 0 deletions

99
app/build.gradle.kts Normal file
View File

@@ -0,0 +1,99 @@
// [FILE] app/build.gradle.kts
// [PURPOSE] Build script for the app module.
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("com.google.dagger.hilt.android")
id("kotlin-kapt")
}
android {
namespace = "com.homebox.lens"
compileSdk = Versions.compileSdk
defaultConfig {
applicationId = "com.homebox.lens"
minSdk = Versions.minSdk
targetSdk = Versions.targetSdk
versionCode = Versions.versionCode
versionName = Versions.versionName
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
useSupportLibrary = true
}
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
buildFeatures {
compose = true
buildConfig = true
}
composeOptions {
kotlinCompilerExtensionVersion = Versions.composeCompiler
}
packaging {
resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
}
}
}
dependencies {
// [MODULE_DEPENDENCY] Data module
implementation(project(":data"))
// [MODULE_DEPENDENCY] Domain module (transitively included via data, but explicit for clarity)
implementation(project(":domain"))
// [DEPENDENCY] AndroidX
implementation(Libs.coreKtx)
implementation(Libs.lifecycleRuntime)
implementation(Libs.activityCompose)
// [DEPENDENCY] Compose
implementation(platform(Libs.composeBom))
implementation(Libs.composeUi)
implementation(Libs.composeUiGraphics)
implementation(Libs.composeUiToolingPreview)
implementation(Libs.composeMaterial3)
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(platform(Libs.composeBom))
androidTestImplementation(Libs.composeUiTestJunit4)
debugImplementation(Libs.composeUiTooling)
debugImplementation(Libs.composeUiTestManifest)
}
kapt {
correctErrorTypes = true
}
// [END_FILE_app/build.gradle.kts]

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.homebox.lens">
<application
android:allowBackup="true"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/Theme.Homeboxlens">
<activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.Homeboxlens">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@@ -0,0 +1,62 @@
// [PACKAGE] com.homebox.lens
// [FILE] MainActivity.kt
package com.homebox.lens
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.homebox.lens.navigation.NavGraph
import com.homebox.lens.ui.theme.HomeboxLensTheme
import dagger.hilt.android.AndroidEntryPoint
// [CONTRACT]
/**
* [ENTITY: Activity('MainActivity')]
* [PURPOSE] Главная и единственная Activity в приложении.
*/
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
// [LIFECYCLE]
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
HomeboxLensTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
NavGraph()
}
}
}
}
}
// [HELPER]
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
Text(
text = "Hello $name!",
modifier = modifier
)
}
// [PREVIEW]
@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
HomeboxLensTheme {
Greeting("Android")
}
}
// [END_FILE_MainActivity.kt]

View File

@@ -0,0 +1,28 @@
// [PACKAGE] com.homebox.lens
// [FILE] MainApplication.kt
package com.homebox.lens
import android.app.Application
import com.homebox.lens.BuildConfig
import dagger.hilt.android.HiltAndroidApp
import timber.log.Timber
// [CONTRACT]
/**
* [ENTITY: Application('MainApplication')]
* [PURPOSE] Точка входа в приложение. Инициализирует Hilt и Timber.
*/
@HiltAndroidApp
class MainApplication : Application() {
// [LIFECYCLE]
override fun onCreate() {
super.onCreate()
// [ACTION] Initialize Timber for logging
if (BuildConfig.DEBUG) {
Timber.plant(Timber.DebugTree())
}
}
}
// [END_FILE_MainApplication.kt]

View File

@@ -0,0 +1,63 @@
// [PACKAGE] com.homebox.lens.di
// [FILE] AppModule.kt
// [SEMANTICS] dependency_injection, hilt, configuration
// [IMPORTS]
import com.homebox.lens.data.api.HomeboxApiService
import com.homebox.lens.data.repository.ItemRepositoryImpl
import com.homebox.lens.domain.repository.ItemRepository
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import javax.inject.Singleton
// [CORE-LOGIC]
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
private const val BASE_URL = "https://homebox.fly.dev/api/" // Заглушка, заменить на реальный URL
/**
* [CONTRACT]
* Предоставляет синглтон-экземпляр Retrofit.
*/
@Provides
@Singleton
fun provideRetrofit(): Retrofit {
// [PRECONDITION] BASE_URL должен быть валидным URL.
require(BASE_URL.startsWith("https://") || BASE_URL.startsWith("http://")) {
"[PRECONDITION_FAILED] BASE_URL must be a valid URL."
}
return Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
/**
* [CONTRACT]
* Предоставляет синглтон-экземпляр HomeboxApiService.
* @param retrofit Экземпляр Retrofit.
*/
@Provides
@Singleton
fun provideHomeboxApiService(retrofit: Retrofit): HomeboxApiService {
return retrofit.create(HomeboxApiService::class.java)
}
/**
* [CONTRACT]
* Предоставляет реализацию ItemRepository.
* @param apiService Экземпляр HomeboxApiService.
*/
@Provides
@Singleton
fun provideItemRepository(apiService: HomeboxApiService): ItemRepository {
return ItemRepositoryImpl(apiService)
}
}
// [END_FILE_AppModule.kt]

View File

@@ -0,0 +1,30 @@
// [PACKAGE] com.homebox.lens.navigation
// [FILE] NavGraph.kt
// [SEMANTICS] navigation, compose, nav_host
// [IMPORTS]
import androidx.compose.runtime.Composable
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.homebox.lens.ui.screen.dashboard.DashboardScreen
// [CORE-LOGIC]
/**
* [CONTRACT]
* Определяет граф навигации для приложения.
*/
@Composable
fun NavGraph() {
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = Screen.Dashboard.route
) {
composable(route = Screen.Dashboard.route) {
DashboardScreen()
}
// TODO: Добавить остальные экраны в граф навигации
}
}
// [END_FILE_NavGraph.kt]

View File

@@ -0,0 +1,15 @@
// [PACKAGE] com.homebox.lens.navigation
// [FILE] Screen.kt
// [SEMANTICS] navigation, routes, constants
// [CORE-LOGIC]
/**
* [CONTRACT]
* Запечатанный класс для определения навигационных маршрутов в приложении.
* @property route Строковый идентификатор маршрута.
*/
sealed class Screen(val route: String) {
object Dashboard : Screen("dashboard_screen")
// TODO: Добавить остальные экраны по мере их создания
}
// [END_FILE_Screen.kt]

View File

@@ -0,0 +1,94 @@
// [PACKAGE] com.homebox.lens.ui.screen.dashboard
// [FILE] DashboardScreen.kt
// [SEMANTICS] ui, screen, dashboard, compose
// [IMPORTS]
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
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.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
// [CORE-LOGIC]
/**
* [CONTRACT]
* Главный Composable для экрана "Дэшборд".
* @param viewModel ViewModel для этого экрана.
*/
@Composable
fun DashboardScreen(
viewModel: DashboardViewModel = hiltViewModel()
) {
val uiState by viewModel.uiState.collectAsState()
Scaffold { paddingValues ->
Box(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
) {
when (val state = uiState) {
is DashboardUiState.Loading -> {
// [UI-ACTION] Показываем индикатор загрузки
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
}
is DashboardUiState.Error -> {
// [UI-ACTION] Показываем сообщение об ошибке
Text(
text = "Error: ${state.message}",
modifier = Modifier.align(Alignment.Center)
)
}
is DashboardUiState.Success -> {
// [UI-ACTION] Отображаем основной контент
DashboardContent(state)
}
}
}
}
}
/**
* [CONTRACT]
* Composable для отображения успешного состояния дэшборда.
* @param state Состояние UI с данными.
*/
@Composable
fun DashboardContent(state: DashboardUiState.Success) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
// [UI-COMPONENT] Статистика
Text(text = "Statistics:")
Text(text = " Items: ${state.statistics.items}")
Text(text = " Locations: ${state.statistics.locations}")
Text(text = " Labels: ${state.statistics.labels}")
Text(text = " Total Value: ${state.statistics.totalValue}")
// [UI-COMPONENT] Локации
Text(text = "Locations:")
state.locations.forEach { location ->
Text(text = " - ${location.name} (${location.itemCount})")
}
// [UI-COMPONENT] Метки
Text(text = "Labels:")
state.labels.forEach { label ->
Text(text = " - ${label.name}")
}
}
}
// [END_FILE_DashboardScreen.kt]

View File

@@ -0,0 +1,83 @@
// [PACKAGE] com.homebox.lens.ui.screen.dashboard
// [FILE] DashboardViewModel.kt
// [SEMANTICS] view_model, dashboard, state_management
// [IMPORTS]
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.homebox.lens.domain.model.GroupStatistics
import com.homebox.lens.domain.model.LabelOut
import com.homebox.lens.domain.model.LocationOutCount
import com.homebox.lens.domain.usecase.GetAllLabelsUseCase
import com.homebox.lens.domain.usecase.GetAllLocationsUseCase
import com.homebox.lens.domain.usecase.GetStatisticsUseCase
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
// [CORE-LOGIC]
/**
* [CONTRACT]
* ViewModel для экрана "Дэшборд".
* @param getStatisticsUseCase Use case для получения статистики.
* @param getAllLocationsUseCase Use case для получения местоположений.
* @param getAllLabelsUseCase Use case для получения меток.
*/
@HiltViewModel
class DashboardViewModel @Inject constructor(
private val getStatisticsUseCase: GetStatisticsUseCase,
private val getAllLocationsUseCase: GetAllLocationsUseCase,
private val getAllLabelsUseCase: GetAllLabelsUseCase
) : ViewModel() {
// [STATE] UI State
private val _uiState = MutableStateFlow<DashboardUiState>(DashboardUiState.Loading)
val uiState: StateFlow<DashboardUiState> = _uiState.asStateFlow()
init {
// [ACTION] Загрузка всех данных при инициализации.
loadDashboardData()
}
/**
* [CONTRACT]
* Загружает все необходимые для экрана данные.
* @sideeffect Обновляет _uiState.
*/
fun loadDashboardData() {
viewModelScope.launch {
_uiState.value = DashboardUiState.Loading
try {
val statistics = getStatisticsUseCase()
val locations = getAllLocationsUseCase()
val labels = getAllLabelsUseCase()
_uiState.value = DashboardUiState.Success(
statistics = statistics,
locations = locations,
labels = labels
)
} catch (e: Exception) {
_uiState.value = DashboardUiState.Error(e.message ?: "Unknown error")
}
}
}
}
/**
* [CONTRACT]
* Запечатанный интерфейс для представления состояний UI дэшборда.
*/
sealed interface DashboardUiState {
data class Success(
val statistics: GroupStatistics,
val locations: List<LocationOutCount>,
val labels: List<LabelOut>
) : DashboardUiState
data class Error(val message: String) : DashboardUiState
object Loading : DashboardUiState
}
// [END_FILE_DashboardViewModel.kt]

View File

@@ -0,0 +1,16 @@
// [PACKAGE] com.homebox.lens.ui.theme
// [FILE] Color.kt
package com.homebox.lens.ui.theme
import androidx.compose.ui.graphics.Color
val Purple80 = Color(0xFFD0BCFF)
val PurpleGrey80 = Color(0xFFCCC2DC)
val Pink80 = Color(0xFFEFB8C8)
val Purple40 = Color(0xFF6650a4)
val PurpleGrey40 = Color(0xFF625b71)
val Pink40 = Color(0xFF7D5260)
// [END_FILE_Color.kt]

View File

@@ -0,0 +1,64 @@
// [PACKAGE] com.homebox.lens.ui.theme
// [FILE] Theme.kt
package com.homebox.lens.ui.theme
import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.core.view.WindowCompat
private val DarkColorScheme = darkColorScheme(
primary = Purple80,
secondary = PurpleGrey80,
tertiary = Pink80
)
private val LightColorScheme = lightColorScheme(
primary = Purple40,
secondary = PurpleGrey40,
tertiary = Pink40
)
@Composable
fun HomeboxLensTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
val view = LocalView.current
if (!view.isInEditMode) {
SideEffect {
val window = (view.context as Activity).window
window.statusBarColor = colorScheme.primary.toArgb()
WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme
}
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
}
// [END_FILE_Theme.kt]

View File

@@ -0,0 +1,23 @@
// [PACKAGE] com.homebox.lens.ui.theme
// [FILE] Typography.kt
package com.homebox.lens.ui.theme
import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
// Set of Material typography styles to start with
val Typography = Typography(
bodyLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
)
)
// [END_FILE_Typography.kt]

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_700">#FF3700B3</color>
</resources>

View File

@@ -0,0 +1,3 @@
<resources>
<string name="app_name">Homebox Lens</string>
</resources>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.Homeboxlens" parent="android:Theme.Material.Light.NoActionBar">
<item name="android:statusBarColor">@color/purple_700</item>
</style>
</resources>