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

34
.gitignore vendored Normal file
View File

@@ -0,0 +1,34 @@
# Gradle
.gradle/
build/
!gradle/wrapper/gradle-wrapper.jar
# Local configuration
local.properties
# IDE files
.idea/
*.iml
*.ipr
*.iws
.DS_Store
# Keystore files
*.jks
*.keystore
# Google Services
app/google-services.json
# Captures
captures/
*.apk
*.aab
output.json
# Log files
*.log
# Gemini files
GEMINI.md
tech_spec/

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>

13
build.gradle.kts Normal file
View File

@@ -0,0 +1,13 @@
// [FILE] build.gradle.kts
// [PURPOSE] Root build file for the project, configures plugins for all modules.
plugins {
// [PLUGIN] Android Application plugin
id("com.android.application") version "8.11.0" apply false
// [PLUGIN] Kotlin Android plugin
id("org.jetbrains.kotlin.android") version "1.9.22" apply false
// [PLUGIN] Hilt Android plugin
id("com.google.dagger.hilt.android") version "2.48.1" apply false
}
// [END_FILE_build.gradle.kts]

12
buildSrc/build.gradle.kts Normal file
View File

@@ -0,0 +1,12 @@
// [FILE] buildSrc/build.gradle.kts
// [PURPOSE] Build file for the buildSrc module, enabling access to Gradle APIs.
plugins {
`kotlin-dsl`
}
repositories {
mavenCentral()
}
// [END_FILE_buildSrc/build.gradle.kts]

View File

@@ -0,0 +1,100 @@
// [FILE] Dependencies.kt
// [PURPOSE] Centralized dependency management for the entire project.
object Versions {
// Build
const val compileSdk = 34
const val minSdk = 26
const val targetSdk = 34
const val versionCode = 1
const val versionName = "1.0"
// Kotlin
const val kotlin = "1.9.22"
const val coroutines = "1.7.3"
// Jetpack Compose
const val composeCompiler = "1.5.8"
const val composeBom = "2023.10.01"
const val activityCompose = "1.8.2"
const val navigationCompose = "2.7.6"
const val hiltNavigationCompose = "1.1.0"
// AndroidX
const val coreKtx = "1.12.0"
const val lifecycle = "2.6.2"
const val appcompat = "1.6.1"
// Networking
const val retrofit = "2.9.0"
const val okhttp = "4.12.0"
const val moshi = "1.15.0"
// Database
const val room = "2.6.1"
// DI
const val hilt = "2.48.1"
const val hiltCompiler = "1.1.0"
// Logging
const val timber = "5.0.1"
// Testing
const val junit = "4.13.2"
const val extJunit = "1.1.5"
const val espresso = "3.5.1"
}
object Libs {
// Kotlin
const val coroutinesCore = "org.jetbrains.kotlinx:kotlinx-coroutines-core:${Versions.coroutines}"
const val coroutinesAndroid = "org.jetbrains.kotlinx:kotlinx-coroutines-android:${Versions.coroutines}"
// AndroidX
const val coreKtx = "androidx.core:core-ktx:${Versions.coreKtx}"
const val lifecycleRuntime = "androidx.lifecycle:lifecycle-runtime-ktx:${Versions.lifecycle}"
const val appcompat = "androidx.appcompat:appcompat:${Versions.appcompat}"
// Compose
const val composeBom = "androidx.compose:compose-bom:${Versions.composeBom}"
const val composeUi = "androidx.compose.ui:ui"
const val composeUiGraphics = "androidx.compose.ui:ui-graphics"
const val composeUiToolingPreview = "androidx.compose.ui:ui-tooling-preview"
const val composeMaterial3 = "androidx.compose.material3:material3"
const val activityCompose = "androidx.activity:activity-compose:${Versions.activityCompose}"
const val navigationCompose = "androidx.navigation:navigation-compose:${Versions.navigationCompose}"
const val hiltNavigationCompose = "androidx.hilt:hilt-navigation-compose:${Versions.hiltNavigationCompose}"
// Networking (Retrofit, OkHttp, Moshi)
const val retrofit = "com.squareup.retrofit2:retrofit:${Versions.retrofit}"
const val converterMoshi = "com.squareup.retrofit2:converter-moshi:${Versions.retrofit}"
const val converterGson = "com.squareup.retrofit2:converter-gson:${Versions.retrofit}"
const val okhttp = "com.squareup.okhttp3:okhttp:${Versions.okhttp}"
const val okhttpLoggingInterceptor = "com.squareup.okhttp3:logging-interceptor:${Versions.okhttp}"
const val moshi = "com.squareup.moshi:moshi:${Versions.moshi}"
const val moshiKotlin = "com.squareup.moshi:moshi-kotlin:${Versions.moshi}"
const val moshiCodegen = "com.squareup.moshi:moshi-kotlin-codegen:${Versions.moshi}"
// Database (Room)
const val roomRuntime = "androidx.room:room-runtime:${Versions.room}"
const val roomKtx = "androidx.room:room-ktx:${Versions.room}"
const val roomCompiler = "androidx.room:room-compiler:${Versions.room}"
// Dependency Injection (Hilt)
const val hiltAndroid = "com.google.dagger:hilt-android:${Versions.hilt}"
const val hiltCompiler = "com.google.dagger:hilt-android-compiler:${Versions.hilt}"
// Logging
const val timber = "com.jakewharton.timber:timber:${Versions.timber}"
// Testing
const val junit = "junit:junit:${Versions.junit}"
const val extJunit = "androidx.test.ext:junit:${Versions.extJunit}"
const val espressoCore = "androidx.test.espresso:espresso-core:${Versions.espresso}"
const val composeUiTestJunit4 = "androidx.compose.ui:ui-test-junit4"
const val composeUiTooling = "androidx.compose.ui:ui-tooling"
const val composeUiTestManifest = "androidx.compose.ui:ui-test-manifest"
}
// [END_FILE_Dependencies.kt]

73
data/build.gradle.kts Normal file
View File

@@ -0,0 +1,73 @@
// [FILE] data/build.gradle.kts
// [PURPOSE] Build script for the data module.
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
id("com.google.dagger.hilt.android")
id("kotlin-kapt")
}
android {
namespace = "com.homebox.lens.data"
compileSdk = Versions.compileSdk
defaultConfig {
minSdk = Versions.minSdk
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
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"
}
}
dependencies {
// [MODULE_DEPENDENCY] Domain module
implementation(project(":domain"))
// [DEPENDENCY] AndroidX
implementation(Libs.coreKtx)
// [DEPENDENCY] Coroutines
implementation(Libs.coroutinesCore)
// [DEPENDENCY] Networking (Retrofit, Gson)
implementation(Libs.retrofit)
implementation(Libs.converterGson)
implementation(Libs.okhttp)
implementation(Libs.okhttpLoggingInterceptor)
// [DEPENDENCY] Database (Room)
implementation(Libs.roomRuntime)
implementation(Libs.roomKtx)
kapt(Libs.roomCompiler)
// [DEPENDENCY] DI (Hilt)
implementation(Libs.hiltAndroid)
kapt(Libs.hiltCompiler)
// [DEPENDENCY] Testing
testImplementation(Libs.junit)
androidTestImplementation(Libs.extJunit)
androidTestImplementation(Libs.espressoCore)
}
kapt {
correctErrorTypes = true
}
// [END_FILE_data/build.gradle.kts]

View File

@@ -0,0 +1,63 @@
// [PACKAGE] com.homebox.lens.data.api
// [FILE] HomeboxApiService.kt
package com.homebox.lens.data.api
import com.homebox.lens.data.api.dto.GroupStatisticsDto
import com.homebox.lens.data.api.dto.ItemCreateDto
import com.homebox.lens.data.api.dto.ItemOutDto
import com.homebox.lens.data.api.dto.ItemSummaryDto
import com.homebox.lens.data.api.dto.ItemUpdateDto
import com.homebox.lens.data.api.dto.LabelOutDto
import com.homebox.lens.data.api.dto.LocationOutCountDto
import com.homebox.lens.data.api.dto.PaginationResultDto
import retrofit2.Response
import retrofit2.http.Body
import retrofit2.http.DELETE
import retrofit2.http.GET
import retrofit2.http.POST
import retrofit2.http.PUT
import retrofit2.http.Path
import retrofit2.http.Query
// [CONTRACT]
/**
* [ENTITY: Interface('HomeboxApiService')]
* [PURPOSE] Определяет эндпоинты для взаимодействия с Homebox API, используя DTO.
*/
interface HomeboxApiService {
// [ENDPOINT] Items
@GET("v1/items")
suspend fun getItems(
@Query("q") query: String? = null,
@Query("page") page: Int? = null,
@Query("pageSize") pageSize: Int? = null
): PaginationResultDto<ItemSummaryDto>
@POST("v1/items")
suspend fun createItem(@Body item: ItemCreateDto): ItemSummaryDto
@GET("v1/items/{id}")
suspend fun getItem(@Path("id") itemId: String): ItemOutDto
@PUT("v1/items/{id}")
suspend fun updateItem(@Path("id") itemId: String, @Body item: ItemUpdateDto): ItemOutDto
@DELETE("v1/items/{id}")
suspend fun deleteItem(@Path("id") itemId: String): Response<Unit>
// [ENDPOINT] Locations
@GET("v1/locations")
suspend fun getLocations(): List<LocationOutCountDto>
// [ENDPOINT] Labels
@GET("v1/labels")
suspend fun getLabels(): List<LabelOutDto>
// [ENDPOINT] Statistics
@GET("v1/groups/statistics")
suspend fun getStatistics(): GroupStatisticsDto
}
// [END_FILE_HomeboxApiService.kt]

View File

@@ -0,0 +1,30 @@
// [PACKAGE] com.homebox.lens.data.api.dto
// [FILE] CustomFieldDto.kt
// [SEMANTICS] data_transfer_object, custom_field
// [IMPORTS]
import com.google.gson.annotations.SerializedName
import com.homebox.lens.domain.model.CustomField
// [CORE-LOGIC]
/**
* [CONTRACT]
* DTO для кастомного поля.
*/
data class CustomFieldDto(
@SerializedName("name") val name: String,
@SerializedName("value") val value: String,
@SerializedName("type") val type: String
)
/**
* [CONTRACT]
* Маппер из CustomFieldDto в доменную модель CustomField.
*/
fun CustomFieldDto.toDomain(): CustomField {
return CustomField(
name = this.name,
value = this.value,
type = this.type
)
}

View File

@@ -0,0 +1,32 @@
// [PACKAGE] com.homebox.lens.data.api.dto
// [FILE] GroupStatisticsDto.kt
// [SEMANTICS] data_transfer_object, statistics
// [IMPORTS]
import com.google.gson.annotations.SerializedName
import com.homebox.lens.domain.model.GroupStatistics
// [CORE-LOGIC]
/**
* [CONTRACT]
* DTO для статистики.
*/
data class GroupStatisticsDto(
@SerializedName("items") val items: Int,
@SerializedName("labels") val labels: Int,
@SerializedName("locations") val locations: Int,
@SerializedName("totalValue") val totalValue: Double
)
/**
* [CONTRACT]
* Маппер из GroupStatisticsDto в доменную модель GroupStatistics.
*/
fun GroupStatisticsDto.toDomain(): GroupStatistics {
return GroupStatistics(
items = this.items,
labels = this.labels,
locations = this.locations,
totalValue = this.totalValue
)
}

View File

@@ -0,0 +1,33 @@
// [PACKAGE] com.homebox.lens.data.api.dto
// [FILE] ImageDto.kt
// [SEMANTICS] data_transfer_object, image
// [IMPORTS]
import com.google.gson.annotations.SerializedName
import com.homebox.lens.domain.model.Image
// [CORE-LOGIC]
/**
* [CONTRACT]
* DTO для изображения.
* @property id Уникальный идентификатор.
* @property path Путь к файлу.
* @property isPrimary Является ли основным.
*/
data class ImageDto(
@SerializedName("id") val id: String,
@SerializedName("path") val path: String,
@SerializedName("isPrimary") val isPrimary: Boolean
)
/**
* [CONTRACT]
* Маппер из ImageDto в доменную модель Image.
*/
fun ImageDto.toDomain(): Image {
return Image(
id = this.id,
path = this.path,
isPrimary = this.isPrimary
)
}

View File

@@ -0,0 +1,36 @@
// [PACKAGE] com.homebox.lens.data.api.dto
// [FILE] ItemAttachmentDto.kt
// [SEMANTICS] data_transfer_object, attachment
// [IMPORTS]
import com.google.gson.annotations.SerializedName
import com.homebox.lens.domain.model.ItemAttachment
// [CORE-LOGIC]
/**
* [CONTRACT]
* DTO для вложения.
*/
data class ItemAttachmentDto(
@SerializedName("id") val id: String,
@SerializedName("name") val name: String,
@SerializedName("path") val path: String,
@SerializedName("type") val type: String,
@SerializedName("createdAt") val createdAt: String,
@SerializedName("updatedAt") val updatedAt: String
)
/**
* [CONTRACT]
* Маппер из ItemAttachmentDto в доменную модель ItemAttachment.
*/
fun ItemAttachmentDto.toDomain(): ItemAttachment {
return ItemAttachment(
id = this.id,
name = this.name,
path = this.path,
type = this.type,
createdAt = this.createdAt,
updatedAt = this.updatedAt
)
}

View File

@@ -0,0 +1,50 @@
// [PACKAGE] com.homebox.lens.data.api.dto
// [FILE] ItemCreateDto.kt
// [SEMANTICS] data_transfer_object, item_creation
// [IMPORTS]
import com.google.gson.annotations.SerializedName
import com.homebox.lens.domain.model.ItemCreate
// [CORE-LOGIC]
/**
* [CONTRACT]
* DTO для создания вещи.
*/
data class ItemCreateDto(
@SerializedName("name") val name: String,
@SerializedName("assetId") val assetId: String?,
@SerializedName("description") val description: String?,
@SerializedName("notes") val notes: String?,
@SerializedName("serialNumber") val serialNumber: String?,
@SerializedName("quantity") val quantity: Int?,
@SerializedName("value") val value: Double?,
@SerializedName("purchasePrice") val purchasePrice: Double?,
@SerializedName("purchaseDate") val purchaseDate: String?,
@SerializedName("warrantyUntil") val warrantyUntil: String?,
@SerializedName("locationId") val locationId: String?,
@SerializedName("parentId") val parentId: String?,
@SerializedName("labelIds") val labelIds: List<String>?
)
/**
* [CONTRACT]
* Маппер из доменной модели ItemCreate в ItemCreateDto.
*/
fun ItemCreate.toDto(): ItemCreateDto {
return ItemCreateDto(
name = this.name,
assetId = this.assetId,
description = this.description,
notes = this.notes,
serialNumber = this.serialNumber,
quantity = this.quantity,
value = this.value,
purchasePrice = this.purchasePrice,
purchaseDate = this.purchaseDate,
warrantyUntil = this.warrantyUntil,
locationId = this.locationId,
parentId = this.parentId,
labelIds = this.labelIds
)
}

View File

@@ -0,0 +1,66 @@
// [PACKAGE] com.homebox.lens.data.api.dto
// [FILE] ItemDto.kt
package com.homebox.lens.data.api.dto
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import java.math.BigDecimal
// [CONTRACT]
/**
* [ENTITY: DataClass('ItemOut')]
* [PURPOSE] DTO для полной информации о вещи (GET /v1/items/{id}).
*/
@JsonClass(generateAdapter = true)
data class ItemOut(
@Json(name = "id") val id: String,
@Json(name = "name") val name: String,
@Json(name = "description") val description: String?,
@Json(name = "image") val image: String?,
@Json(name = "location") val location: LocationOut?,
@Json(name = "labels") val labels: List<LabelOut>,
@Json(name = "value") val value: BigDecimal?,
@Json(name = "createdAt") val createdAt: String?
)
/**
* [ENTITY: DataClass('ItemSummary')]
* [PURPOSE] DTO для краткой информации о вещи в списках (GET /v1/items).
*/
@JsonClass(generateAdapter = true)
data class ItemSummary(
@Json(name = "id") val id: String,
@Json(name = "name") val name: String,
@Json(name = "image") val image: String?,
@Json(name = "location") val location: LocationOut?,
@Json(name = "createdAt") val createdAt: String?
)
/**
* [ENTITY: DataClass('ItemCreate')]
* [PURPOSE] DTO для создания новой вещи (POST /v1/items).
*/
@JsonClass(generateAdapter = true)
data class ItemCreate(
@Json(name = "name") val name: String,
@Json(name = "description") val description: String?,
@Json(name = "locationId") val locationId: String?,
@Json(name = "labelIds") val labelIds: List<String>?,
@Json(name = "value") val value: BigDecimal?
)
/**
* [ENTITY: DataClass('ItemUpdate')]
* [PURPOSE] DTO для обновления вещи (PUT /v1/items/{id}).
*/
@JsonClass(generateAdapter = true)
data class ItemUpdate(
@Json(name = "name") val name: String,
@Json(name = "description") val description: String?,
@Json(name = "locationId") val locationId: String?,
@Json(name = "labelIds") val labelIds: List<String>?,
@Json(name = "value") val value: BigDecimal?
)
// [END_FILE_ItemDto.kt]

View File

@@ -0,0 +1,68 @@
// [PACKAGE] com.homebox.lens.data.api.dto
// [FILE] ItemOutDto.kt
// [SEMANTICS] data_transfer_object, item_detailed
// [IMPORTS]
import com.google.gson.annotations.SerializedName
import com.homebox.lens.domain.model.ItemOut
// [CORE-LOGIC]
/**
* [CONTRACT]
* DTO для полной модели вещи.
*/
data class ItemOutDto(
@SerializedName("id") val id: String,
@SerializedName("name") val name: String,
@SerializedName("assetId") val assetId: String?,
@SerializedName("description") val description: String?,
@SerializedName("notes") val notes: String?,
@SerializedName("serialNumber") val serialNumber: String?,
@SerializedName("quantity") val quantity: Int,
@SerializedName("isArchived") val isArchived: Boolean,
@SerializedName("value") val value: Double,
@SerializedName("purchasePrice") val purchasePrice: Double?,
@SerializedName("purchaseDate") val purchaseDate: String?,
@SerializedName("warrantyUntil") val warrantyUntil: String?,
@SerializedName("location") val location: LocationOutDto?,
@SerializedName("parent") val parent: ItemSummaryDto?,
@SerializedName("children") val children: List<ItemSummaryDto>,
@SerializedName("labels") val labels: List<LabelOutDto>,
@SerializedName("attachments") val attachments: List<ItemAttachmentDto>,
@SerializedName("images") val images: List<ImageDto>,
@SerializedName("fields") val fields: List<CustomFieldDto>,
@SerializedName("maintenance") val maintenance: List<MaintenanceEntryDto>,
@SerializedName("createdAt") val createdAt: String,
@SerializedName("updatedAt") val updatedAt: String
)
/**
* [CONTRACT]
* Маппер из ItemOutDto в доменную модель ItemOut.
*/
fun ItemOutDto.toDomain(): ItemOut {
return ItemOut(
id = this.id,
name = this.name,
assetId = this.assetId,
description = this.description,
notes = this.notes,
serialNumber = this.serialNumber,
quantity = this.quantity,
isArchived = this.isArchived,
value = this.value,
purchasePrice = this.purchasePrice,
purchaseDate = this.purchaseDate,
warrantyUntil = this.warrantyUntil,
location = this.location?.toDomain(),
parent = this.parent?.toDomain(),
children = this.children.map { it.toDomain() },
labels = this.labels.map { it.toDomain() },
attachments = this.attachments.map { it.toDomain() },
images = this.images.map { it.toDomain() },
fields = this.fields.map { it.toDomain() },
maintenance = this.maintenance.map { it.toDomain() },
createdAt = this.createdAt,
updatedAt = this.updatedAt
)
}

View File

@@ -0,0 +1,44 @@
// [PACKAGE] com.homebox.lens.data.api.dto
// [FILE] ItemSummaryDto.kt
// [SEMANTICS] data_transfer_object, item_summary
// [IMPORTS]
import com.google.gson.annotations.SerializedName
import com.homebox.lens.domain.model.ItemSummary
// [CORE-LOGIC]
/**
* [CONTRACT]
* DTO для сокращенной модели вещи.
*/
data class ItemSummaryDto(
@SerializedName("id") val id: String,
@SerializedName("name") val name: String,
@SerializedName("assetId") val assetId: String?,
@SerializedName("image") val image: ImageDto?,
@SerializedName("isArchived") val isArchived: Boolean,
@SerializedName("labels") val labels: List<LabelOutDto>,
@SerializedName("location") val location: LocationOutDto?,
@SerializedName("value") val value: Double,
@SerializedName("createdAt") val createdAt: String,
@SerializedName("updatedAt") val updatedAt: String
)
/**
* [CONTRACT]
* Маппер из ItemSummaryDto в доменную модель ItemSummary.
*/
fun ItemSummaryDto.toDomain(): ItemSummary {
return ItemSummary(
id = this.id,
name = this.name,
assetId = this.assetId,
image = this.image?.toDomain(),
isArchived = this.isArchived,
labels = this.labels.map { it.toDomain() },
location = this.location?.toDomain(),
value = this.value,
createdAt = this.createdAt,
updatedAt = this.updatedAt
)
}

View File

@@ -0,0 +1,52 @@
// [PACKAGE] com.homebox.lens.data.api.dto
// [FILE] ItemUpdateDto.kt
// [SEMANTICS] data_transfer_object, item_update
// [IMPORTS]
import com.google.gson.annotations.SerializedName
import com.homebox.lens.domain.model.ItemUpdate
// [CORE-LOGIC]
/**
* [CONTRACT]
* DTO для обновления вещи.
*/
data class ItemUpdateDto(
@SerializedName("name") val name: String?,
@SerializedName("assetId") val assetId: String?,
@SerializedName("description") val description: String?,
@SerializedName("notes") val notes: String?,
@SerializedName("serialNumber") val serialNumber: String?,
@SerializedName("quantity") val quantity: Int?,
@SerializedName("isArchived") val isArchived: Boolean?,
@SerializedName("value") val value: Double?,
@SerializedName("purchasePrice") val purchasePrice: Double?,
@SerializedName("purchaseDate") val purchaseDate: String?,
@SerializedName("warrantyUntil") val warrantyUntil: String?,
@SerializedName("locationId") val locationId: String?,
@SerializedName("parentId") val parentId: String?,
@SerializedName("labelIds") val labelIds: List<String>?
)
/**
* [CONTRACT]
* Маппер из доменной модели ItemUpdate в ItemUpdateDto.
*/
fun ItemUpdate.toDto(): ItemUpdateDto {
return ItemUpdateDto(
name = this.name,
assetId = this.assetId,
description = this.description,
notes = this.notes,
serialNumber = this.serialNumber,
quantity = this.quantity,
isArchived = this.isArchived,
value = this.value,
purchasePrice = this.purchasePrice,
purchaseDate = this.purchaseDate,
warrantyUntil = this.warrantyUntil,
locationId = this.locationId,
parentId = this.parentId,
labelIds = this.labelIds
)
}

View File

@@ -0,0 +1,20 @@
// [PACKAGE] com.homebox.lens.data.api.dto
// [FILE] LabelDto.kt
package com.homebox.lens.data.api.dto
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
// [CONTRACT]
/**
* [ENTITY: DataClass('LabelOut')]
* [PURPOSE] DTO для информации о метке.
*/
@JsonClass(generateAdapter = true)
data class LabelOut(
@Json(name = "id") val id: String,
@Json(name = "name") val name: String
)
// [END_FILE_LabelDto.kt]

View File

@@ -0,0 +1,36 @@
// [PACKAGE] com.homebox.lens.data.api.dto
// [FILE] LabelOutDto.kt
// [SEMANTICS] data_transfer_object, label
// [IMPORTS]
import com.google.gson.annotations.SerializedName
import com.homebox.lens.domain.model.LabelOut
// [CORE-LOGIC]
/**
* [CONTRACT]
* DTO для метки.
*/
data class LabelOutDto(
@SerializedName("id") val id: String,
@SerializedName("name") val name: String,
@SerializedName("color") val color: String,
@SerializedName("isArchived") val isArchived: Boolean,
@SerializedName("createdAt") val createdAt: String,
@SerializedName("updatedAt") val updatedAt: String
)
/**
* [CONTRACT]
* Маппер из LabelOutDto в доменную модель LabelOut.
*/
fun LabelOutDto.toDomain(): LabelOut {
return LabelOut(
id = this.id,
name = this.name,
color = this.color,
isArchived = this.isArchived,
createdAt = this.createdAt,
updatedAt = this.updatedAt
)
}

View File

@@ -0,0 +1,31 @@
// [PACKAGE] com.homebox.lens.data.api.dto
// [FILE] LocationDto.kt
package com.homebox.lens.data.api.dto
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
// [CONTRACT]
/**
* [ENTITY: DataClass('LocationOut')]
* [PURPOSE] DTO для информации о местоположении.
*/
@JsonClass(generateAdapter = true)
data class LocationOut(
@Json(name = "id") val id: String,
@Json(name = "name") val name: String
)
/**
* [ENTITY: DataClass('LocationOutCount')]
* [PURPOSE] DTO для информации о местоположении со счетчиком вещей.
*/
@JsonClass(generateAdapter = true)
data class LocationOutCount(
@Json(name = "id") val id: String,
@Json(name = "name") val name: String,
@Json(name = "itemCount") val itemCount: Int
)
// [END_FILE_LocationDto.kt]

View File

@@ -0,0 +1,38 @@
// [PACKAGE] com.homebox.lens.data.api.dto
// [FILE] LocationOutCountDto.kt
// [SEMANTICS] data_transfer_object, location, count
// [IMPORTS]
import com.google.gson.annotations.SerializedName
import com.homebox.lens.domain.model.LocationOutCount
// [CORE-LOGIC]
/**
* [CONTRACT]
* DTO для местоположения со счетчиком.
*/
data class LocationOutCountDto(
@SerializedName("id") val id: String,
@SerializedName("name") val name: String,
@SerializedName("color") val color: String,
@SerializedName("isArchived") val isArchived: Boolean,
@SerializedName("itemCount") val itemCount: Int,
@SerializedName("createdAt") val createdAt: String,
@SerializedName("updatedAt") val updatedAt: String
)
/**
* [CONTRACT]
* Маппер из LocationOutCountDto в доменную модель LocationOutCount.
*/
fun LocationOutCountDto.toDomain(): LocationOutCount {
return LocationOutCount(
id = this.id,
name = this.name,
color = this.color,
isArchived = this.isArchived,
itemCount = this.itemCount,
createdAt = this.createdAt,
updatedAt = this.updatedAt
)
}

View File

@@ -0,0 +1,36 @@
// [PACKAGE] com.homebox.lens.data.api.dto
// [FILE] LocationOutDto.kt
// [SEMANTICS] data_transfer_object, location
// [IMPORTS]
import com.google.gson.annotations.SerializedName
import com.homebox.lens.domain.model.LocationOut
// [CORE-LOGIC]
/**
* [CONTRACT]
* DTO для местоположения.
*/
data class LocationOutDto(
@SerializedName("id") val id: String,
@SerializedName("name") val name: String,
@SerializedName("color") val color: String,
@SerializedName("isArchived") val isArchived: Boolean,
@SerializedName("createdAt") val createdAt: String,
@SerializedName("updatedAt") val updatedAt: String
)
/**
* [CONTRACT]
* Маппер из LocationOutDto в доменную модель LocationOut.
*/
fun LocationOutDto.toDomain(): LocationOut {
return LocationOut(
id = this.id,
name = this.name,
color = this.color,
isArchived = this.isArchived,
createdAt = this.createdAt,
updatedAt = this.updatedAt
)
}

View File

@@ -0,0 +1,40 @@
// [PACKAGE] com.homebox.lens.data.api.dto
// [FILE] MaintenanceEntryDto.kt
// [SEMANTICS] data_transfer_object, maintenance
// [IMPORTS]
import com.google.gson.annotations.SerializedName
import com.homebox.lens.domain.model.MaintenanceEntry
// [CORE-LOGIC]
/**
* [CONTRACT]
* DTO для записи об обслуживании.
*/
data class MaintenanceEntryDto(
@SerializedName("id") val id: String,
@SerializedName("itemId") val itemId: String,
@SerializedName("title") val title: String,
@SerializedName("details") val details: String?,
@SerializedName("dueAt") val dueAt: String?,
@SerializedName("completedAt") val completedAt: String?,
@SerializedName("createdAt") val createdAt: String,
@SerializedName("updatedAt") val updatedAt: String
)
/**
* [CONTRACT]
* Маппер из MaintenanceEntryDto в доменную модель MaintenanceEntry.
*/
fun MaintenanceEntryDto.toDomain(): MaintenanceEntry {
return MaintenanceEntry(
id = this.id,
itemId = this.itemId,
title = this.title,
details = this.details,
dueAt = this.dueAt,
completedAt = this.completedAt,
createdAt = this.createdAt,
updatedAt = this.updatedAt
)
}

View File

@@ -0,0 +1,23 @@
// [PACKAGE] com.homebox.lens.data.api.dto
// [FILE] PaginationDto.kt
package com.homebox.lens.data.api.dto
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
// [CONTRACT]
/**
* [ENTITY: DataClass('PaginationResult')]
* [PURPOSE] DTO для пагинированных результатов от API.
*/
@JsonClass(generateAdapter = true)
data class PaginationResult<T>(
@Json(name = "items") val items: List<T>,
@Json(name = "page") val page: Int,
@Json(name = "pages") val pages: Int,
@Json(name = "total") val total: Int,
@Json(name = "pageSize") val pageSize: Int
)
// [END_FILE_PaginationDto.kt]

View File

@@ -0,0 +1,33 @@
// [PACKAGE] com.homebox.lens.data.api.dto
// [FILE] PaginationResultDto.kt
// [SEMANTICS] data_transfer_object, pagination
// [IMPORTS]
import com.google.gson.annotations.SerializedName
import com.homebox.lens.domain.model.PaginationResult
// [CORE-LOGIC]
/**
* [CONTRACT]
* DTO для постраничных результатов.
*/
data class PaginationResultDto<T>(
@SerializedName("items") val items: List<T>,
@SerializedName("page") val page: Int,
@SerializedName("pageSize") val pageSize: Int,
@SerializedName("total") val total: Int
)
/**
* [CONTRACT]
* Маппер из PaginationResultDto в доменную модель PaginationResult.
* @param transform Функция для преобразования каждого элемента из DTO в доменную модель.
*/
fun <T, R> PaginationResultDto<T>.toDomain(transform: (T) -> R): PaginationResult<R> {
return PaginationResult(
items = this.items.map(transform),
page = this.page,
pageSize = this.pageSize,
total = this.total
)
}

View File

@@ -0,0 +1,23 @@
// [PACKAGE] com.homebox.lens.data.api.dto
// [FILE] StatisticsDto.kt
package com.homebox.lens.data.api.dto
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import java.math.BigDecimal
// [CONTRACT]
/**
* [ENTITY: DataClass('GroupStatistics')]
* [PURPOSE] DTO для статистической информации.
*/
@JsonClass(generateAdapter = true)
data class GroupStatistics(
@Json(name = "totalValue") val totalValue: BigDecimal,
@Json(name = "totalItems") val totalItems: Int,
@Json(name = "locations") val locations: Int,
@Json(name = "labels") val labels: Int
)
// [END_FILE_StatisticsDto.kt]

View File

@@ -0,0 +1,26 @@
// [PACKAGE] com.homebox.lens.data.db
// [FILE] Converters.kt
package com.homebox.lens.data.db
import androidx.room.TypeConverter
import java.math.BigDecimal
// [CONTRACT]
/**
* [ENTITY: Class('Converters')]
* [PURPOSE] Предоставляет TypeConverters для Room для типов, не поддерживаемых по умолчанию.
*/
class Converters {
@TypeConverter
fun fromString(value: String?): BigDecimal? {
return value?.let { BigDecimal(it) }
}
@TypeConverter
fun bigDecimalToString(bigDecimal: BigDecimal?): String? {
return bigDecimal?.toPlainString()
}
}
// [END_FILE_Converters.kt]

View File

@@ -0,0 +1,41 @@
// [PACKAGE] com.homebox.lens.data.db
// [FILE] HomeboxDatabase.kt
package com.homebox.lens.data.db
import androidx.room.Database
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import com.homebox.lens.data.db.dao.ItemDao
import com.homebox.lens.data.db.dao.LabelDao
import com.homebox.lens.data.db.dao.LocationDao
import com.homebox.lens.data.db.entity.*
// [CONTRACT]
/**
* [ENTITY: RoomDatabase('HomeboxDatabase')]
* [PURPOSE] Основной класс для работы с локальной базой данных Room.
*/
@Database(
entities = [
ItemEntity::class,
LabelEntity::class,
LocationEntity::class,
ItemLabelCrossRef::class
],
version = 1,
exportSchema = false
)
@TypeConverters(Converters::class)
abstract class HomeboxDatabase : RoomDatabase() {
abstract fun itemDao(): ItemDao
abstract fun labelDao(): LabelDao
abstract fun locationDao(): LocationDao
companion object {
const val DATABASE_NAME = "homebox_lens_db"
}
}
// [END_FILE_HomeboxDatabase.kt]

View File

@@ -0,0 +1,40 @@
// [PACKAGE] com.homebox.lens.data.db.dao
// [FILE] ItemDao.kt
package com.homebox.lens.data.db.dao
import androidx.room.*
import com.homebox.lens.data.db.entity.ItemEntity
import com.homebox.lens.data.db.entity.ItemLabelCrossRef
import com.homebox.lens.data.db.entity.ItemWithLabels
// [CONTRACT]
/**
* [ENTITY: RoomDao('ItemDao')]
* [PURPOSE] Предоставляет методы для работы с 'items' в локальной БД.
*/
@Dao
interface ItemDao {
@Transaction
@Query("SELECT * FROM items")
suspend fun getItems(): List<ItemWithLabels>
@Transaction
@Query("SELECT * FROM items WHERE id = :itemId")
suspend fun getItem(itemId: String): ItemWithLabels?
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertItems(items: List<ItemEntity>)
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertItem(item: ItemEntity)
@Query("DELETE FROM items WHERE id = :itemId")
suspend fun deleteItem(itemId: String)
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertItemLabelCrossRefs(crossRefs: List<ItemLabelCrossRef>)
}
// [END_FILE_ItemDao.kt]

View File

@@ -0,0 +1,27 @@
// [PACKAGE] com.homebox.lens.data.db.dao
// [FILE] LabelDao.kt
package com.homebox.lens.data.db.dao
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.homebox.lens.data.db.entity.LabelEntity
// [CONTRACT]
/**
* [ENTITY: RoomDao('LabelDao')]
* [PURPOSE] Предоставляет методы для работы с 'labels' в локальной БД.
*/
@Dao
interface LabelDao {
@Query("SELECT * FROM labels")
suspend fun getLabels(): List<LabelEntity>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertLabels(labels: List<LabelEntity>)
}
// [END_FILE_LabelDao.kt]

View File

@@ -0,0 +1,27 @@
// [PACKAGE] com.homebox.lens.data.db.dao
// [FILE] LocationDao.kt
package com.homebox.lens.data.db.dao
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.homebox.lens.data.db.entity.LocationEntity
// [CONTRACT]
/**
* [ENTITY: RoomDao('LocationDao')]
* [PURPOSE] Предоставляет методы для работы с 'locations' в локальной БД.
*/
@Dao
interface LocationDao {
@Query("SELECT * FROM locations")
suspend fun getLocations(): List<LocationEntity>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertLocations(locations: List<LocationEntity>)
}
// [END_FILE_LocationDao.kt]

View File

@@ -0,0 +1,26 @@
// [PACKAGE] com.homebox.lens.data.db.entity
// [FILE] ItemEntity.kt
package com.homebox.lens.data.db.entity
import androidx.room.Entity
import androidx.room.PrimaryKey
import java.math.BigDecimal
// [CONTRACT]
/**
* [ENTITY: RoomEntity('ItemEntity')]
* [PURPOSE] Представляет собой строку в таблице 'items' в локальной БД.
*/
@Entity(tableName = "items")
data class ItemEntity(
@PrimaryKey val id: String,
val name: String,
val description: String?,
val image: String?,
val locationId: String?,
val value: BigDecimal?,
val createdAt: String?
)
// [END_FILE_ItemEntity.kt]

View File

@@ -0,0 +1,19 @@
// [PACKAGE] com.homebox.lens.data.db.entity
// [FILE] ItemLabelCrossRef.kt
package com.homebox.lens.data.db.entity
import androidx.room.Entity
// [CONTRACT]
/**
* [ENTITY: RoomEntity('ItemLabelCrossRef')]
* [PURPOSE] Таблица для связи "многие-ко-многим" между ItemEntity и LabelEntity.
*/
@Entity(primaryKeys = ["itemId", "labelId"])
data class ItemLabelCrossRef(
val itemId: String,
val labelId: String
)
// [END_FILE_ItemLabelCrossRef.kt]

View File

@@ -0,0 +1,29 @@
// [PACKAGE] com.homebox.lens.data.db.entity
// [FILE] ItemWithLabels.kt
package com.homebox.lens.data.db.entity
import androidx.room.Embedded
import androidx.room.Junction
import androidx.room.Relation
// [CONTRACT]
/**
* [ENTITY: Pojo('ItemWithLabels')]
* [PURPOSE] POJO для получения ItemEntity вместе со связанными LabelEntity.
*/
data class ItemWithLabels(
@Embedded val item: ItemEntity,
@Relation(
parentColumn = "id",
entityColumn = "id",
associateBy = Junction(
value = ItemLabelCrossRef::class,
parentColumn = "itemId",
entityColumn = "labelId"
)
)
val labels: List<LabelEntity>
)
// [END_FILE_ItemWithLabels.kt]

View File

@@ -0,0 +1,20 @@
// [PACKAGE] com.homebox.lens.data.db.entity
// [FILE] LabelEntity.kt
package com.homebox.lens.data.db.entity
import androidx.room.Entity
import androidx.room.PrimaryKey
// [CONTRACT]
/**
* [ENTITY: RoomEntity('LabelEntity')]
* [PURPOSE] Представляет собой строку в таблице 'labels' в локальной БД.
*/
@Entity(tableName = "labels")
data class LabelEntity(
@PrimaryKey val id: String,
val name: String
)
// [END_FILE_LabelEntity.kt]

View File

@@ -0,0 +1,20 @@
// [PACKAGE] com.homebox.lens.data.db.entity
// [FILE] LocationEntity.kt
package com.homebox.lens.data.db.entity
import androidx.room.Entity
import androidx.room.PrimaryKey
// [CONTRACT]
/**
* [ENTITY: RoomEntity('LocationEntity')]
* [PURPOSE] Представляет собой строку в таблице 'locations' в локальной БД.
*/
@Entity(tableName = "locations")
data class LocationEntity(
@PrimaryKey val id: String,
val name: String
)
// [END_FILE_LocationEntity.kt]

View File

@@ -0,0 +1,77 @@
// [PACKAGE] com.homebox.lens.data.di
// [FILE] ApiModule.kt
package com.homebox.lens.data.di
import com.homebox.lens.data.api.HomeboxApiService
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import javax.inject.Singleton
// [CONTRACT]
/**
* [MODULE: DaggerHilt('ApiModule')]
* [PURPOSE] Предоставляет зависимости для работы с сетью (Retrofit, OkHttp, Moshi).
*/
@Module
@InstallIn(SingletonComponent::class)
object ApiModule {
// [HELPER]
private const val BASE_URL = "https://api.homebox.app/"
// [PROVIDER]
@Provides
@Singleton
fun provideOkHttpClient(): OkHttpClient {
// [ACTION] Create logging interceptor
val logging = HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
}
// [ACTION] Build OkHttpClient
return OkHttpClient.Builder()
.addInterceptor(logging)
// [TODO] Add AuthInterceptor for Bearer token
.build()
}
// [PROVIDER]
@Provides
@Singleton
fun provideMoshi(): Moshi {
// [ACTION] Build Moshi with Kotlin adapter
return Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
}
// [PROVIDER]
@Provides
@Singleton
fun provideRetrofit(okHttpClient: OkHttpClient, moshi: Moshi): Retrofit {
// [ACTION] Build Retrofit instance
return Retrofit.Builder()
.baseUrl(BASE_URL)
.client(okHttpClient)
.addConverterFactory(MoshiConverterFactory.create(moshi))
.build()
}
// [PROVIDER]
@Provides
@Singleton
fun provideHomeboxApiService(retrofit: Retrofit): HomeboxApiService {
// [ACTION] Create ApiService from Retrofit instance
return retrofit.create(HomeboxApiService::class.java)
}
}
// [END_FILE_ApiModule.kt]

View File

@@ -0,0 +1,50 @@
// [PACKAGE] com.homebox.lens.data.di
// [FILE] DatabaseModule.kt
package com.homebox.lens.data.di
import android.content.Context
import androidx.room.Room
import com.homebox.lens.data.db.HomeboxDatabase
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
// [CONTRACT]
/**
* [MODULE: DaggerHilt('DatabaseModule')]
* [PURPOSE] Предоставляет зависимости для работы с базой данных Room.
*/
@Module
@InstallIn(SingletonComponent::class)
object DatabaseModule {
// [PROVIDER]
@Provides
@Singleton
fun provideHomeboxDatabase(@ApplicationContext context: Context): HomeboxDatabase {
// [ACTION] Build Room database instance
return Room.databaseBuilder(
context,
HomeboxDatabase::class.java,
HomeboxDatabase.DATABASE_NAME
).build()
}
// [PROVIDER]
@Provides
fun provideItemDao(database: HomeboxDatabase) = database.itemDao()
// [PROVIDER]
@Provides
fun provideLabelDao(database: HomeboxDatabase) = database.labelDao()
// [PROVIDER]
@Provides
fun provideLocationDao(database: HomeboxDatabase) = database.locationDao()
}
// [END_FILE_DatabaseModule.kt]

View File

@@ -0,0 +1,31 @@
// [PACKAGE] com.homebox.lens.data.di
// [FILE] RepositoryModule.kt
package com.homebox.lens.data.di
import com.homebox.lens.data.repository.ItemRepositoryImpl
import com.homebox.lens.domain.repository.ItemRepository
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
// [CONTRACT]
/**
* [MODULE: DaggerHilt('RepositoryModule')]
* [PURPOSE] Предоставляет реализацию для интерфейса ItemRepository.
*/
@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {
// [PROVIDER]
@Binds
@Singleton
abstract fun bindItemRepository(
itemRepositoryImpl: ItemRepositoryImpl
): ItemRepository
}
// [END_FILE_RepositoryModule.kt]

View File

@@ -0,0 +1,105 @@
// [PACKAGE] com.homebox.lens.data.repository
// [FILE] ItemRepositoryImpl.kt
// [SEMANTICS] data_repository, implementation, network
// [IMPORTS]
import com.homebox.lens.data.api.HomeboxApiService
import com.homebox.lens.data.api.dto.toDomain
import com.homebox.lens.data.api.dto.toDto
import com.homebox.lens.domain.model.*
import com.homebox.lens.domain.repository.ItemRepository
import javax.inject.Inject
// [CORE-LOGIC]
/**
* [CONTRACT]
* Реализация репозитория для работы с данными о вещах.
* @param apiService Сервис для взаимодействия с Homebox API.
*/
class ItemRepositoryImpl @Inject constructor(
private val apiService: HomeboxApiService
) : ItemRepository {
/**
* [CONTRACT] @see ItemRepository.createItem
*/
override suspend fun createItem(newItemData: ItemCreate): ItemSummary {
// [ACTION]
val itemDto = newItemData.toDto()
val resultDto = apiService.createItem(itemDto)
return resultDto.toDomain()
}
/**
* [CONTRACT] @see ItemRepository.getItemDetails
*/
override suspend fun getItemDetails(itemId: String): ItemOut {
// [ACTION]
val resultDto = apiService.getItem(itemId)
return resultDto.toDomain()
}
/**
* [CONTRACT] @see ItemRepository.updateItem
*/
override suspend fun updateItem(itemId: String, item: ItemUpdate): ItemOut {
// [ACTION]
val itemDto = item.toDto()
val resultDto = apiService.updateItem(itemId, itemDto)
return resultDto.toDomain()
}
/**
* [CONTRACT] @see ItemRepository.deleteItem
*/
override suspend fun deleteItem(itemId: String) {
// [ACTION]
apiService.deleteItem(itemId)
}
/**
* [CONTRACT] @see ItemRepository.syncInventory
*/
override suspend fun syncInventory(page: Int, pageSize: Int): PaginationResult<ItemSummary> {
// [ACTION]
val resultDto = apiService.getItems(page = page, pageSize = pageSize)
return resultDto.toDomain { it.toDomain() }
}
/**
* [CONTRACT] @see ItemRepository.getStatistics
*/
override suspend fun getStatistics(): GroupStatistics {
// [ACTION]
val resultDto = apiService.getStatistics()
return resultDto.toDomain()
}
/**
* [CONTRACT] @see ItemRepository.getAllLocations
*/
override suspend fun getAllLocations(): List<LocationOutCount> {
// [ACTION]
val resultDto = apiService.getLocations()
return resultDto.map { it.toDomain() }
}
/**
* [CONTRACT] @see ItemRepository.getAllLabels
*/
override suspend fun getAllLabels(): List<LabelOut> {
// [ACTION]
val resultDto = apiService.getLabels()
return resultDto.map { it.toDomain() }
}
/**
* [CONTRACT] @see ItemRepository.searchItems
*/
override suspend fun searchItems(query: String): PaginationResult<ItemSummary> {
// [ACTION]
val resultDto = apiService.getItems(query = query)
return resultDto.toDomain { it.toDomain() }
}
}
// [END_FILE_ItemRepositoryImpl.kt]

25
domain/build.gradle.kts Normal file
View File

@@ -0,0 +1,25 @@
// [FILE] domain/build.gradle.kts
// [PURPOSE] Build script for the domain module.
plugins {
id("org.jetbrains.kotlin.jvm")
}
java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
kotlinOptions.jvmTarget = "1.8"
}
dependencies {
// [DEPENDENCY] KotlinX Coroutines for asynchronous operations
implementation(Libs.coroutinesCore)
// [DEPENDENCY] Javax Inject for DI annotations
implementation("javax.inject:javax.inject:1")
}
// [END_FILE_domain/build.gradle.kts]

View File

@@ -0,0 +1,18 @@
// [PACKAGE] com.homebox.lens.domain.model
// [FILE] CustomField.kt
// [SEMANTICS] data_structure, entity, custom_field
// [CORE-LOGIC]
/**
* [CONTRACT]
* Модель данных для представления кастомного поля.
* @property name Имя поля.
* @property value Значение поля.
* @property type Тип поля (например, "text", "number").
*/
data class CustomField(
val name: String,
val value: String,
val type: String
)
// [END_FILE_CustomField.kt]

View File

@@ -0,0 +1,20 @@
// [PACKAGE] com.homebox.lens.domain.model
// [FILE] GroupStatistics.kt
// [SEMANTICS] data_structure, statistics
// [CORE-LOGIC]
/**
* [CONTRACT]
* Модель данных для представления агрегированной статистики.
* @property items Общее количество вещей.
* @property labels Общее количество меток.
* @property locations Общее количество местоположений.
* @property totalValue Общая стоимость всех вещей.
*/
data class GroupStatistics(
val items: Int,
val labels: Int,
val locations: Int,
val totalValue: Double
)
// [END_FILE_GroupStatistics.kt]

View File

@@ -0,0 +1,18 @@
// [PACKAGE] com.homebox.lens.domain.model
// [FILE] Image.kt
// [SEMANTICS] data_structure, entity, image
// [CORE-LOGIC]
/**
* [CONTRACT]
* Модель данных для представления изображения, привязанного к вещи.
* @property id Уникальный идентификатор изображения.
* @property path Путь к файлу изображения.
* @property isPrimary Является ли это изображение основным для вещи.
*/
data class Image(
val id: String,
val path: String,
val isPrimary: Boolean
)
// [END_FILE_Image.kt]

View File

@@ -0,0 +1,32 @@
// [PACKAGE] com.homebox.lens.domain.model
// [FILE] Item.kt
package com.homebox.lens.domain.model
import java.math.BigDecimal
// [CONTRACT]
/**
* [ENTITY: DataClass('Item')]
* [PURPOSE] Представляет собой вещь в инвентаре.
* @property id Уникальный идентификатор вещи.
* @property name Название вещи.
* @property description Описание вещи.
* @property image Url изображения.
* @property location Местоположение вещи.
* @property labels Список меток, присвоенных вещи.
* @property value Стоимость вещи.
* @property createdAt Дата создания.
*/
data class Item(
val id: String,
val name: String,
val description: String?,
val image: String?,
val location: Location?,
val labels: List<Label>,
val value: BigDecimal?,
val createdAt: String?
)
// [END_FILE_Item.kt]

View File

@@ -0,0 +1,24 @@
// [PACKAGE] com.homebox.lens.domain.model
// [FILE] ItemAttachment.kt
// [SEMANTICS] data_structure, entity, attachment
// [CORE-LOGIC]
/**
* [CONTRACT]
* Модель данных для представления вложения (файла), привязанного к вещи.
* @property id Уникальный идентификатор вложения.
* @property name Имя файла.
* @property path Путь к файлу.
* @property type MIME-тип файла.
* @property createdAt Дата и время создания.
* @property updatedAt Дата и время последнего обновления.
*/
data class ItemAttachment(
val id: String,
val name: String,
val path: String,
val type: String,
val createdAt: String,
val updatedAt: String
)
// [END_FILE_ItemAttachment.kt]

View File

@@ -0,0 +1,38 @@
// [PACKAGE] com.homebox.lens.domain.model
// [FILE] ItemCreate.kt
// [SEMANTICS] data_structure, entity, input, create
// [CORE-LOGIC]
/**
* [CONTRACT]
* Модель данных для создания новой "Вещи".
* @property name Название вещи (обязательно).
* @property assetId Идентификатор актива.
* @property description Описание.
* @property notes Заметки.
* @property serialNumber Серийный номер.
* @property quantity Количество.
* @property value Стоимость.
* @property purchasePrice Цена покупки.
* @property purchaseDate Дата покупки.
* @property warrantyUntil Гарантия до.
* @property locationId ID местоположения.
* @property parentId ID родительской вещи.
* @property labelIds Список ID меток.
*/
data class ItemCreate(
val name: String,
val assetId: String?,
val description: String?,
val notes: String?,
val serialNumber: String?,
val quantity: Int?,
val value: Double?,
val purchasePrice: Double?,
val purchaseDate: String?,
val warrantyUntil: String?,
val locationId: String?,
val parentId: String?,
val labelIds: List<String>?
)
// [END_FILE_ItemCreate.kt]

View File

@@ -0,0 +1,56 @@
// [PACKAGE] com.homebox.lens.domain.model
// [FILE] ItemOut.kt
// [SEMANTICS] data_structure, entity, detailed
// [CORE-LOGIC]
/**
* [CONTRACT]
* Полная модель данных для представления "Вещи" со всеми полями.
* @property id Уникальный идентификатор.
* @property name Название.
* @property assetId Идентификатор актива.
* @property description Описание.
* @property notes Заметки.
* @property serialNumber Серийный номер.
* @property quantity Количество.
* @property isArchived Флаг архивации.
* @property value Стоимость.
* @property purchasePrice Цена покупки.
* @property purchaseDate Дата покупки.
* @property warrantyUntil Гарантия до.
* @property location Местоположение.
* @property parent Родительская вещь (если есть).
* @property children Дочерние вещи.
* @property labels Список меток.
* @property attachments Список вложений.
* @property images Список изображений.
* @property fields Список кастомных полей.
* @property maintenance Список записей об обслуживании.
* @property createdAt Дата и время создания.
* @property updatedAt Дата и время последнего обновления.
*/
data class ItemOut(
val id: String,
val name: String,
val assetId: String?,
val description: String?,
val notes: String?,
val serialNumber: String?,
val quantity: Int,
val isArchived: Boolean,
val value: Double,
val purchasePrice: Double?,
val purchaseDate: String?,
val warrantyUntil: String?,
val location: LocationOut?,
val parent: ItemSummary?,
val children: List<ItemSummary>,
val labels: List<LabelOut>,
val attachments: List<ItemAttachment>,
val images: List<Image>,
val fields: List<CustomField>,
val maintenance: List<MaintenanceEntry>,
val createdAt: String,
val updatedAt: String
)
// [END_FILE_ItemOut.kt]

View File

@@ -0,0 +1,32 @@
// [PACKAGE] com.homebox.lens.domain.model
// [FILE] ItemSummary.kt
// [SEMANTICS] data_structure, entity, summary
// [CORE-LOGIC]
/**
* [CONTRACT]
* Сокращенная модель данных для представления "Вещи" в списках.
* @property id Уникальный идентификатор вещи.
* @property name Название вещи.
* @property assetId Идентификатор актива.
* @property image Основное изображение. Может быть null.
* @property isArchived Флаг архивации.
* @property labels Список меток.
* @property location Местоположение. Может быть null.
* @property value Стоимость.
* @property createdAt Дата и время создания.
* @property updatedAt Дата и время последнего обновления.
*/
data class ItemSummary(
val id: String,
val name: String,
val assetId: String?,
val image: Image?,
val isArchived: Boolean,
val labels: List<LabelOut>,
val location: LocationOut?,
val value: Double,
val createdAt: String,
val updatedAt: String
)
// [END_FILE_ItemSummary.kt]

View File

@@ -0,0 +1,40 @@
// [PACKAGE] com.homebox.lens.domain.model
// [FILE] ItemUpdate.kt
// [SEMANTICS] data_structure, entity, input, update
// [CORE-LOGIC]
/**
* [CONTRACT]
* Модель данных для обновления существующей "Вещи".
* @property name Название вещи.
* @property assetId Идентификатор актива.
* @property description Описание.
* @property notes Заметки.
* @property serialNumber Серийный номер.
* @property quantity Количество.
* @property isArchived Флаг архивации.
* @property value Стоимость.
* @property purchasePrice Цена покупки.
* @property purchaseDate Дата покупки.
* @property warrantyUntil Гарантия до.
* @property locationId ID местоположения.
* @property parentId ID родительской вещи.
* @property labelIds Список ID меток для полной замены.
*/
data class ItemUpdate(
val name: String?,
val assetId: String?,
val description: String?,
val notes: String?,
val serialNumber: String?,
val quantity: Int?,
val isArchived: Boolean?,
val value: Double?,
val purchasePrice: Double?,
val purchaseDate: String?,
val warrantyUntil: String?,
val locationId: String?,
val parentId: String?,
val labelIds: List<String>?
)
// [END_FILE_ItemUpdate.kt]

View File

@@ -0,0 +1,18 @@
// [PACKAGE] com.homebox.lens.domain.model
// [FILE] Label.kt
package com.homebox.lens.domain.model
// [CONTRACT]
/**
* [ENTITY: DataClass('Label')]
* [PURPOSE] Представляет собой метку (тег), которую можно присвоить вещи.
* @property id Уникальный идентификатор метки.
* @property name Название метки.
*/
data class Label(
val id: String,
val name: String
)
// [END_FILE_Label.kt]

View File

@@ -0,0 +1,24 @@
// [PACKAGE] com.homebox.lens.domain.model
// [FILE] LabelOut.kt
// [SEMANTICS] data_structure, entity, label
// [CORE-LOGIC]
/**
* [CONTRACT]
* Модель данных для представления метки (тега).
* @property id Уникальный идентификатор.
* @property name Название метки.
* @property color Цвет метки в формате HEX (например, "#FF0000").
* @property isArchived Флаг, указывающий, заархивирована ли метка.
* @property createdAt Дата и время создания.
* @property updatedAt Дата и время последнего обновления.
*/
data class LabelOut(
val id: String,
val name: String,
val color: String,
val isArchived: Boolean,
val createdAt: String,
val updatedAt: String
)
// [END_FILE_LabelOut.kt]

View File

@@ -0,0 +1,18 @@
// [PACKAGE] com.homebox.lens.domain.model
// [FILE] Location.kt
package com.homebox.lens.domain.model
// [CONTRACT]
/**
* [ENTITY: DataClass('Location')]
* [PURPOSE] Представляет собой местоположение, где может находиться вещь.
* @property id Уникальный идентификатор местоположения.
* @property name Название местоположения.
*/
data class Location(
val id: String,
val name: String
)
// [END_FILE_Location.kt]

View File

@@ -0,0 +1,24 @@
// [PACKAGE] com.homebox.lens.domain.model
// [FILE] LocationOut.kt
// [SEMANTICS] data_structure, entity, location
// [CORE-LOGIC]
/**
* [CONTRACT]
* Модель данных для представления местоположения (без счетчика).
* @property id Уникальный идентификатор.
* @property name Название местоположения.
* @property color Цвет в формате HEX.
* @property isArchived Флаг архивации.
* @property createdAt Дата и время создания.
* @property updatedAt Дата и время последнего обновления.
*/
data class LocationOut(
val id: String,
val name: String,
val color: String,
val isArchived: Boolean,
val createdAt: String,
val updatedAt: String
)
// [END_FILE_LocationOut.kt]

View File

@@ -0,0 +1,26 @@
// [PACKAGE] com.homebox.lens.domain.model
// [FILE] LocationOutCount.kt
// [SEMANTICS] data_structure, entity, location
// [CORE-LOGIC]
/**
* [CONTRACT]
* Модель данных для представления местоположения со счетчиком вещей.
* @property id Уникальный идентификатор.
* @property name Название местоположения.
* @property color Цвет в формате HEX.
* @property isArchived Флаг архивации.
* @property itemCount Количество вещей в данном местоположении.
* @property createdAt Дата и время создания.
* @property updatedAt Дата и время последнего обновления.
*/
data class LocationOutCount(
val id: String,
val name: String,
val color: String,
val isArchived: Boolean,
val itemCount: Int,
val createdAt: String,
val updatedAt: String
)
// [END_FILE_LocationOutCount.kt]

View File

@@ -0,0 +1,28 @@
// [PACKAGE] com.homebox.lens.domain.model
// [FILE] MaintenanceEntry.kt
// [SEMANTICS] data_structure, entity, maintenance
// [CORE-LOGIC]
/**
* [CONTRACT]
* Модель данных для записи о техническом обслуживании.
* @property id Уникальный идентификатор записи.
* @property itemId ID связанной вещи.
* @property title Заголовок.
* @property details Детальное описание.
* @property dueAt Дата, до которой нужно выполнить.
* @property completedAt Дата выполнения.
* @property createdAt Дата и время создания.
* @property updatedAt Дата и время последнего обновления.
*/
data class MaintenanceEntry(
val id: String,
val itemId: String,
val title: String,
val details: String?,
val dueAt: String?,
val completedAt: String?,
val createdAt: String,
val updatedAt: String
)
// [END_FILE_MaintenanceEntry.kt]

View File

@@ -0,0 +1,21 @@
// [PACKAGE] com.homebox.lens.domain.model
// [FILE] PaginationResult.kt
// [SEMANTICS] data_structure, generic, pagination
// [CORE-LOGIC]
/**
* [CONTRACT]
* Генерик-класс для представления постраничных результатов от API.
* @param T Тип элементов в списке.
* @property items Список элементов на текущей странице.
* @property page Номер текущей страницы.
* @property pageSize Количество элементов на странице.
* @property total Общее количество элементов.
*/
data class PaginationResult<T>(
val items: List<T>,
val page: Int,
val pageSize: Int,
val total: Int
)
// [END_FILE_PaginationResult.kt]

View File

@@ -0,0 +1,28 @@
// [PACKAGE] com.homebox.lens.domain.model
// [FILE] Result.kt
package com.homebox.lens.domain.model
// [CONTRACT]
/**
* [ENTITY: SealedClass('Result')]
* [PURPOSE] Представляет собой результат операции, который может быть либо успешным, либо неуспешным.
* @param T Тип данных в случае успеха.
*/
sealed class Result<out T> {
/**
* [ENTITY: DataClass('Success')]
* [PURPOSE] Представляет собой успешный результат операции.
* @param data Данные, полученные в результате операции.
*/
data class Success<out T>(val data: T) : Result<T>()
/**
* [ENTITY: DataClass('Error')]
* [PURPOSE] Представляет собой неуспешный результат операции.
* @param exception Исключение, которое произошло во время операции.
*/
data class Error(val exception: Exception) : Result<Nothing>()
}
// [END_FILE_Result.kt]

View File

@@ -0,0 +1,24 @@
// [PACKAGE] com.homebox.lens.domain.model
// [FILE] Statistics.kt
package com.homebox.lens.domain.model
import java.math.BigDecimal
// [CONTRACT]
/**
* [ENTITY: DataClass('Statistics')]
* [PURPOSE] Представляет собой статистику по инвентарю.
* @property totalValue Общая стоимость всех вещей.
* @property totalItems Общее количество вещей.
* @property locations Общее количество местоположений.
* @property labels Общее количество меток.
*/
data class Statistics(
val totalValue: BigDecimal,
val totalItems: Int,
val locations: Int,
val labels: Int
)
// [END_FILE_Statistics.kt]

View File

@@ -0,0 +1,25 @@
// [PACKAGE] com.homebox.lens.domain.repository
// [FILE] ItemRepository.kt
// [SEMANTICS] data_access, abstraction, repository
// [IMPORTS]
import com.homebox.lens.domain.model.*
// [CORE-LOGIC]
/**
* [CONTRACT]
* Абстракция репозитория для работы с "Вещами".
* Определяет контракт, которому должен следовать слой данных.
*/
interface ItemRepository {
suspend fun createItem(newItemData: ItemCreate): ItemSummary
suspend fun getItemDetails(itemId: String): ItemOut
suspend fun updateItem(itemId: String, item: ItemUpdate): ItemOut
suspend fun deleteItem(itemId: String)
suspend fun syncInventory(page: Int, pageSize: Int): PaginationResult<ItemSummary>
suspend fun getStatistics(): GroupStatistics
suspend fun getAllLocations(): List<LocationOutCount>
suspend fun getAllLabels(): List<LabelOut>
suspend fun searchItems(query: String): PaginationResult<ItemSummary>
}
// [END_FILE_ItemRepository.kt]

View File

@@ -0,0 +1,44 @@
// [PACKAGE] com.homebox.lens.domain.usecase
// [FILE] CreateItemUseCase.kt
// [SEMANTICS] business_logic, use_case, item_creation
// [IMPORTS]
import com.homebox.lens.domain.model.ItemCreate
import com.homebox.lens.domain.model.ItemSummary
import com.homebox.lens.domain.repository.ItemRepository
import javax.inject.Inject
// [CORE-LOGIC]
/**
* [CONTRACT]
* Use case для создания новой вещи.
* @param itemRepository Репозиторий для работы с данными о вещах.
*/
class CreateItemUseCase @Inject constructor(
private val itemRepository: ItemRepository
) {
/**
* [CONTRACT]
* Выполняет операцию создания вещи.
* @param itemCreate Данные для создания новой вещи.
* @return Возвращает сокращенную модель созданной вещи.
* @throws IllegalArgumentException если название вещи пустое.
*/
suspend operator fun invoke(itemCreate: ItemCreate): ItemSummary {
// [PRECONDITION] Название вещи не может быть пустым.
require(itemCreate.name.isNotBlank()) {
"[PRECONDITION_FAILED] Item name cannot be blank."
}
// [ACTION]
val result = itemRepository.createItem(itemCreate)
// [POSTCONDITION] Убеждаемся, что репозиторий вернул результат.
check(result != null) {
"[POSTCONDITION_FAILED] Repository returned null after creating an item."
}
return result
}
}
// [END_FILE_CreateItemUseCase.kt]

View File

@@ -0,0 +1,34 @@
// [PACKAGE] com.homebox.lens.domain.usecase
// [FILE] DeleteItemUseCase.kt
// [SEMANTICS] business_logic, use_case, item_deletion
// [IMPORTS]
import com.homebox.lens.domain.repository.ItemRepository
import javax.inject.Inject
// [CORE-LOGIC]
/**
* [CONTRACT]
* Use case для удаления вещи.
* @param itemRepository Репозиторий для работы с данными о вещах.
*/
class DeleteItemUseCase @Inject constructor(
private val itemRepository: ItemRepository
) {
/**
* [CONTRACT]
* Выполняет операцию удаления вещи.
* @param itemId ID удаляемой вещи.
* @throws IllegalArgumentException если ID вещи пустое.
*/
suspend operator fun invoke(itemId: String) {
// [PRECONDITION] ID не может быть пустым.
require(itemId.isNotBlank()) {
"[PRECONDITION_FAILED] Item ID cannot be blank."
}
// [ACTION]
itemRepository.deleteItem(itemId)
}
}
// [END_FILE_DeleteItemUseCase.kt]

View File

@@ -0,0 +1,29 @@
// [PACKAGE] com.homebox.lens.domain.usecase
// [FILE] GetAllLabelsUseCase.kt
// [SEMANTICS] business_logic, use_case, label_retrieval
// [IMPORTS]
import com.homebox.lens.domain.model.LabelOut
import com.homebox.lens.domain.repository.ItemRepository
import javax.inject.Inject
// [CORE-LOGIC]
/**
* [CONTRACT]
* Use case для получения всех меток.
* @param itemRepository Репозиторий для работы с данными.
*/
class GetAllLabelsUseCase @Inject constructor(
private val itemRepository: ItemRepository
) {
/**
* [CONTRACT]
* Выполняет операцию получения всех меток.
* @return Возвращает список меток.
*/
suspend operator fun invoke(): List<LabelOut> {
// [ACTION]
return itemRepository.getAllLabels()
}
}
// [END_FILE_GetAllLabelsUseCase.kt]

View File

@@ -0,0 +1,29 @@
// [PACKAGE] com.homebox.lens.domain.usecase
// [FILE] GetAllLocationsUseCase.kt
// [SEMANTICS] business_logic, use_case, location_retrieval
// [IMPORTS]
import com.homebox.lens.domain.model.LocationOutCount
import com.homebox.lens.domain.repository.ItemRepository
import javax.inject.Inject
// [CORE-LOGIC]
/**
* [CONTRACT]
* Use case для получения всех местоположений.
* @param itemRepository Репозиторий для работы с данными.
*/
class GetAllLocationsUseCase @Inject constructor(
private val itemRepository: ItemRepository
) {
/**
* [CONTRACT]
* Выполняет операцию получения всех местоположений.
* @return Возвращает список местоположений со счетчиками.
*/
suspend operator fun invoke(): List<LocationOutCount> {
// [ACTION]
return itemRepository.getAllLocations()
}
}
// [END_FILE_GetAllLocationsUseCase.kt]

View File

@@ -0,0 +1,43 @@
// [PACKAGE] com.homebox.lens.domain.usecase
// [FILE] GetItemDetailsUseCase.kt
// [SEMANTICS] business_logic, use_case, item_retrieval
// [IMPORTS]
import com.homebox.lens.domain.model.ItemOut
import com.homebox.lens.domain.repository.ItemRepository
import javax.inject.Inject
// [CORE-LOGIC]
/**
* [CONTRACT]
* Use case для получения детальной информации о вещи.
* @param itemRepository Репозиторий для работы с данными о вещах.
*/
class GetItemDetailsUseCase @Inject constructor(
private val itemRepository: ItemRepository
) {
/**
* [CONTRACT]
* Выполняет операцию получения детальной информации о вещи.
* @param itemId ID запрашиваемой вещи.
* @return Возвращает полную модель вещи.
* @throws IllegalArgumentException если ID вещи пустое.
*/
suspend operator fun invoke(itemId: String): ItemOut {
// [PRECONDITION] ID не может быть пустым.
require(itemId.isNotBlank()) {
"[PRECONDITION_FAILED] Item ID cannot be blank."
}
// [ACTION]
val result = itemRepository.getItemDetails(itemId)
// [POSTCONDITION] Убеждаемся, что репозиторий вернул результат.
check(result != null) {
"[POSTCONDITION_FAILED] Repository returned null for item ID: $itemId"
}
return result
}
}
// [END_FILE_GetItemDetailsUseCase.kt]

View File

@@ -0,0 +1,29 @@
// [PACKAGE] com.homebox.lens.domain.usecase
// [FILE] GetStatisticsUseCase.kt
// [SEMANTICS] business_logic, use_case, statistics
// [IMPORTS]
import com.homebox.lens.domain.model.GroupStatistics
import com.homebox.lens.domain.repository.ItemRepository
import javax.inject.Inject
// [CORE-LOGIC]
/**
* [CONTRACT]
* Use case для получения статистики.
* @param itemRepository Репозиторий для работы с данными.
*/
class GetStatisticsUseCase @Inject constructor(
private val itemRepository: ItemRepository
) {
/**
* [CONTRACT]
* Выполняет операцию получения статистики.
* @return Возвращает объект со статистикой.
*/
suspend operator fun invoke(): GroupStatistics {
// [ACTION]
return itemRepository.getStatistics()
}
}
// [END_FILE_GetStatisticsUseCase.kt]

View File

@@ -0,0 +1,32 @@
// [PACKAGE] com.homebox.lens.domain.usecase
// [FILE] SearchItemsUseCase.kt
// [SEMANTICS] business_logic, use_case, search
// [IMPORTS]
import com.homebox.lens.domain.model.ItemSummary
import com.homebox.lens.domain.model.PaginationResult
import com.homebox.lens.domain.repository.ItemRepository
import javax.inject.Inject
// [CORE-LOGIC]
/**
* [CONTRACT]
* Use case для поиска вещей по текстовому запросу.
* @param itemRepository Репозиторий для работы с данными.
*/
class SearchItemsUseCase @Inject constructor(
private val itemRepository: ItemRepository
) {
/**
* [CONTRACT]
* Выполняет операцию поиска.
* @param query Поисковый запрос.
* @return Возвращает объект с результатами пагинации.
*/
suspend operator fun invoke(query: String): PaginationResult<ItemSummary> {
// [ACTION]
// Поисковый запрос может быть пустым, сервер обработает это как запрос всех элементов.
return itemRepository.searchItems(query)
}
}
// [END_FILE_SearchItemsUseCase.kt]

View File

@@ -0,0 +1,38 @@
// [PACKAGE] com.homebox.lens.domain.usecase
// [FILE] SyncInventoryUseCase.kt
// [SEMANTICS] business_logic, use_case, data_sync
// [IMPORTS]
import com.homebox.lens.domain.model.ItemSummary
import com.homebox.lens.domain.model.PaginationResult
import com.homebox.lens.domain.repository.ItemRepository
import javax.inject.Inject
// [CORE-LOGIC]
/**
* [CONTRACT]
* Use case для синхронизации (получения) списка вещей.
* @param itemRepository Репозиторий для работы с данными о вещах.
*/
class SyncInventoryUseCase @Inject constructor(
private val itemRepository: ItemRepository
) {
/**
* [CONTRACT]
* Выполняет операцию получения страницы со списком вещей.
* @param page Номер страницы.
* @param pageSize Размер страницы.
* @return Возвращает объект с результатами пагинации.
* @throws IllegalArgumentException если параметры пагинации некорректны.
*/
suspend operator fun invoke(page: Int, pageSize: Int): PaginationResult<ItemSummary> {
// [PRECONDITION] Параметры пагинации должны быть положительными.
require(page > 0 && pageSize > 0) {
"[PRECONDITION_FAILED] Page and pageSize must be positive."
}
// [ACTION]
return itemRepository.syncInventory(page, pageSize)
}
}
// [END_FILE_SyncInventoryUseCase.kt]

View File

@@ -0,0 +1,45 @@
// [PACKAGE] com.homebox.lens.domain.usecase
// [FILE] UpdateItemUseCase.kt
// [SEMANTICS] business_logic, use_case, item_update
// [IMPORTS]
import com.homebox.lens.domain.model.ItemOut
import com.homebox.lens.domain.model.ItemUpdate
import com.homebox.lens.domain.repository.ItemRepository
import javax.inject.Inject
// [CORE-LOGIC]
/**
* [CONTRACT]
* Use case для обновления существующей вещи.
* @param itemRepository Репозиторий для работы с данными о вещах.
*/
class UpdateItemUseCase @Inject constructor(
private val itemRepository: ItemRepository
) {
/**
* [CONTRACT]
* Выполняет операцию обновления вещи.
* @param itemId ID обновляемой вещи.
* @param itemUpdate Данные для обновления.
* @return Возвращает обновленную полную модель вещи.
* @throws IllegalArgumentException если ID вещи пустое.
*/
suspend operator fun invoke(itemId: String, itemUpdate: ItemUpdate): ItemOut {
// [PRECONDITION] ID не может быть пустым.
require(itemId.isNotBlank()) {
"[PRECONDITION_FAILED] Item ID cannot be blank."
}
// [ACTION]
val result = itemRepository.updateItem(itemId, itemUpdate)
// [POSTCONDITION] Убеждаемся, что репозиторий вернул результат.
check(result != null) {
"[POSTCONDITION_FAILED] Repository returned null after updating item ID: $itemId"
}
return result
}
}
// [END_FILE_UpdateItemUseCase.kt]

17
gradle.properties Normal file
View File

@@ -0,0 +1,17 @@
## For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
#
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
# Default value: -Xmx1024m -XX:MaxPermSize=256m
# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
#
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. For more details, visit
# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
# org.gradle.parallel=true
#Tue Aug 05 16:05:40 MSK 2025
android.enableJetifier=true
android.useAndroidX=true
org.gradle.java.installations.auto-download=true
org.gradle.java.installations.repositories=foojay

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View File

@@ -0,0 +1,6 @@
#Tue Aug 05 15:58:20 MSK 2025
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

251
gradlew vendored Executable file
View File

@@ -0,0 +1,251 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH="\\\"\\\""
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

94
gradlew.bat vendored Normal file
View File

@@ -0,0 +1,94 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
set CLASSPATH=
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

25
settings.gradle.kts Normal file
View File

@@ -0,0 +1,25 @@
// [FILE] settings.gradle.kts
// [PURPOSE] Defines the project structure and included modules for Gradle.
pluginManagement {
repositories {
maven { url = uri("https://mvn-mirror.gitverse.ru") }
google()
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
maven { url = uri("https://mvn-mirror.gitverse.ru") }
google()
mavenCentral()
}
}
rootProject.name = "HomeboxLens"
include(":app")
include(":data")
include(":domain")
// [END_FILE_settings.gradle.kts]