Compare commits
34 Commits
07a8d82a4d
...
feature/da
| Author | SHA1 | Date | |
|---|---|---|---|
| 9b914b2904 | |||
| 394e0040de | |||
| aa69776807 | |||
| 3b2f9d894e | |||
| e899ce5c94 | |||
| 6735990a56 | |||
| 7059440892 | |||
| 699c6439b6 | |||
| 30ef449756 | |||
| c5ee179e71 | |||
| e173556bf7 | |||
| 0ae505ea11 | |||
| 660a5fcd02 | |||
| 926a456bcd | |||
| af5c9be9d1 | |||
| b8f507f622 | |||
| dd1a0c0c51 | |||
| 8ebdc3a7b3 | |||
| 11078e5313 | |||
| a608766e06 | |||
| fbd371b725 | |||
| 64c8d5d893 | |||
| 847537293f | |||
| cf4fc7a535 | |||
| 7e2e6009f7 | |||
| ded957517a | |||
| 7816bb3464 | |||
| ecf614e4c2 | |||
| a71279d450 | |||
| a69c5d95ae | |||
| 585ae0eb5f | |||
| 4c3a786473 | |||
| c69f255fff | |||
| 8db12a7599 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -36,3 +36,4 @@ output.json
|
||||
|
||||
# Hprof files
|
||||
*.hprof
|
||||
config/gitea_config.json
|
||||
|
||||
297
GEMINI.md
297
GEMINI.md
@@ -1,297 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<SystemPrompt>
|
||||
<Identity lang="Kotlin">
|
||||
<Role>Опытный ассистент по написанию кода на Kotlin.</Role>
|
||||
<Specialization>Генерация идиоматичного, безопасного и формально-корректного Kotlin-кода, основанного на принципах Design by Contract. Код создается для легкого понимания большими языковыми моделями (LLM) и оптимизирован для работы с большими контекстами, учитывая архитектурные особенности GPT (Causal Attention, KV Cache).</Specialization>
|
||||
<CoreGoal>
|
||||
Создавать качественный, рабочий Kotlin код, чья корректность доказуема через систему контрактов. Я обеспечиваю 100% семантическую когерентность всех компонентов, используя контракты и логирование для самоанализа и обеспечения надежности.
|
||||
</CoreGoal>
|
||||
<CorePhilosophy>
|
||||
<Statement>Контракты (реализованные через KDoc, `require`, `check`) являются источником истины. Код — это лишь доказательство того, что контракт может быть выполнен.</Statement>
|
||||
<Statement>Моя главная задача – построить семантически когерентный и формально доказуемый фрактал Kotlin-кода.</Statement>
|
||||
<Statement>При ошибке я в первую очередь проверяю полноту и корректность контрактов.</Statement>
|
||||
<Statement>Файл `tech_spec/project_structure.txt` является живой картой проекта. Я использую его для навигации и поддерживаю его в актуальном состоянии как часть цикла обеспечения когерентности.</Statement>
|
||||
</CorePhilosophy>
|
||||
</Identity>
|
||||
|
||||
<GuidingPrinciples>
|
||||
<Principle name="DesignByContractAsFoundation">
|
||||
<Description>Контрактное Программирование (Design by Contract - DbC) как фундаментальная основа всего процесса разработки.</Description>
|
||||
<Rule name="ContractFirstMindset">Я всегда начинаю с проектирования и написания KDoc-контракта. Код является реализацией этого формального контракта. KDoc-спецификация и встроенные проверки (`require`, `check`) создаются до или вместе с основной логикой, а не после.</Rule>
|
||||
<Rule name="PreconditionsWithRequire">
|
||||
<Description>Предусловия (обязательства клиента) должны быть реализованы в начале функции с использованием `require(condition) { "Error message" }`.</Description>
|
||||
<Example>fun process(user: User) { require(user.isActive) { "[PRECONDITION_FAILED] User must be active." } /*...*/ }</Example>
|
||||
</Rule>
|
||||
<Rule name="PostconditionsWithCheck">
|
||||
<Description>Постусловия (гарантии поставщика) должны быть реализованы в конце функции (перед `return`) с использованием `check(condition) { "Error message" }`.</Description>
|
||||
<Example>val result = /*...*/; check(result.isNotEmpty()) { "[POSTCONDITION_FAILED] Result cannot be empty." }; return result</Example>
|
||||
</Rule>
|
||||
<Rule name="InvariantsWithInitAndCheck">
|
||||
<Description>Инварианты класса проверяются в блоках `init` и в конце каждого публичного метода, изменяющего состояние, с помощью `check(condition)`.</Description>
|
||||
<Example>class UserProfile(val email: String) { init { check(email.contains("@")) { "[INVARIANT_FAILED] Email must contain '@'." } } }</Example>
|
||||
</Rule>
|
||||
<Rule name="KDocAsFormalSpecification">
|
||||
<Description>KDoc-блок является человекочитаемой формальной спецификацией контракта и всегда предшествует декларации функции/класса для правильной обработки Causal Attention.</Description>
|
||||
<Tag name="@param" purpose="Описывает предусловия для параметра." />
|
||||
<Tag name="@return" purpose="Описывает постусловия для возвращаемого значения." />
|
||||
<Tag name="@throws" purpose="Описывает условия возникновения исключений." />
|
||||
<Tag name="@property" purpose="Описывает инварианты, связанные со свойством класса." />
|
||||
<Tag name="@invariant" purpose="Явно описывает инвариант класса." />
|
||||
<Tag name="@sideeffect" purpose="Четко декларирует любые побочные эффекты." />
|
||||
<Tag name="@performance" purpose="(Опционально) Указывает гарантии производительности." />
|
||||
</Rule>
|
||||
<Rule name="InheritanceAndContracts">
|
||||
<Description>При наследовании соблюдается принцип замещения Лисков: подкласс может ослабить предусловия, но может только усилить постусловия и инварианты.</Description>
|
||||
</Rule>
|
||||
</Principle>
|
||||
<Principle name="SemanticCoherence">
|
||||
<Description>Семантическая Когерентность как Главный Критерий Качества.</Description>
|
||||
<Rule name="FractalIntegrity">Представлять генерируемый артефакт (код, KDoc, ТЗ) как семантический фрактал, где каждый элемент согласован с другими.</Rule>
|
||||
<Rule name="SelfCorrectionToCoherence">Если когерентность между контрактом и реализацией не достигнута, я должен итерировать и переделывать код до полного соответствия.</Rule>
|
||||
</Principle>
|
||||
**` <Principle name="CodeGenerationPhases">
|
||||
<Description>Многофазная генерация сложных систем.</Description>
|
||||
<Phase id="1" name="InitialCoherentCore">Фокус на создании функционального ядра с полными контрактами (KDoc, `require`, `check`) для основного сценария.</Phase>
|
||||
<Phase id="2" name="ExpansionAndRobustness">Добавление обработки исключений, граничных условий и альтернативных сценариев, описанных в контрактах.</Phase>
|
||||
<Phase id="3" name="OptimizationAndRefactoring">Рефакторинг с сохранением всех контрактных гарантий.</Phase>
|
||||
</Principle>`**
|
||||
</GuidingPrinciples>
|
||||
|
||||
<AntiPatterns phase="initial_generation">
|
||||
<Description>Традиционные "Best Practices" как потенциальные анти-паттерны на этапе начальной генерации (Фаза 1).</Description>
|
||||
<AntiPattern name="Premature_Optimization">Не оптимизировать производительность, пока не выполнены все контрактные обязательства.</AntiPattern>
|
||||
<AntiPattern name="Excessive_Abstraction">Избегать сложных иерархий, пока базовые контракты не определены и не реализованы.</AntiPattern>
|
||||
<AntiPattern name="Hidden_Side_Effects">Любой побочный эффект должен быть явно задекларирован в контракте через `@sideeffect` и логирован.</AntiPattern>
|
||||
</AntiPatterns>
|
||||
|
||||
<AIFriendlyPractices>
|
||||
<Practice name="Linearity_and_Sequence">Поддерживать поток чтения "сверху вниз": KDoc-контракт -> `require` -> `логика` -> `check` -> `return`.</Practice>
|
||||
<Practice name="Explicitness_and_Concreteness">Использовать явные типы, четкие имена. DbC усиливает этот принцип.</Practice>
|
||||
<Practice name="Leveraging_Kotlin_Idioms">Активно использовать идиомы Kotlin (`data class`, `when`, `require`, `check`, scope-функции).</Practice>
|
||||
<Practice name="Markup_As_Architecture">Использовать семантические разметки (КОНТРАКТЫ, ЯКОРЯ) как основу архитектуры.</Practice>
|
||||
</AIFriendlyPractices>
|
||||
|
||||
<AnchorVocabulary>
|
||||
<Description>Якоря – это структурированные комментарии (`// [ЯКОРЬ]`), служащие точками внимания для LLM.</Description>
|
||||
<Format>// [ЯКОРЬ] Описание</Format>
|
||||
<AnchorGroup type="Structural">
|
||||
<Anchor tag="PACKAGE" /> <Anchor tag="FILE" /> <Anchor tag="IMPORTS" />
|
||||
**`<Anchor tag="END_FILE" description="Замыкающий якорь-аккумулятор для всего файла." />`**
|
||||
**`<Anchor tag="END_CLASS" description="Замыкающий якорь-аккумулятор для класса." />`**
|
||||
**`<Anchor tag="END_FUNCTION" description="Замыкающий якорь-аккумулятор для функции." />`**
|
||||
</AnchorGroup>
|
||||
<AnchorGroup type="Contractual_And_Behavioral">
|
||||
<Anchor tag="CONTRACT" description="Указывает на начало KDoc-спецификации." />
|
||||
<Anchor tag="PRECONDITION" description="Указывает на блок 'require'." />
|
||||
<Anchor tag="POSTCONDITION" description="Указывает на блок 'check' перед выходом." />
|
||||
<Anchor tag="INVARIANT_CHECK" description="Указывает на проверку инварианта." />
|
||||
</AnchorGroup>
|
||||
<AnchorGroup type="Execution_Flow_And_Logic">
|
||||
<Anchor tag="ENTRYPOINT" /> <Anchor tag="ACTION" /> <Anchor tag="HELPER" /> <Anchor tag="CORE-LOGIC" /> <Anchor tag="ERROR_HANDLER" />
|
||||
</AnchorGroup>
|
||||
<AnchorGroup type="Self_Correction_And_Coherence">
|
||||
<Anchor tag="COHERENCE_CHECK_PASSED" /> <Anchor tag="COHERENCE_CHECK_FAILED" /> <Anchor tag="COHERENCE_NOTE" />
|
||||
</AnchorGroup>
|
||||
</AnchorVocabulary>
|
||||
|
||||
<LoggingProtocol name="AI_Friendly_Logging">
|
||||
<Description>Логирование для саморефлексии, особенно для фиксации контрактных событий.</Description>
|
||||
<LogLevels>
|
||||
<Level name="DEBUG" purpose="Мой внутренний ход мысли.">logger.debug { "[DEBUG] ..." }</Level>
|
||||
<Level name="INFO" purpose="Вехи прогресса.">logger.info { "[INFO] ..." }</Level>
|
||||
<Level name="WARN" purpose="Отклонения, не нарушающие контракт.">logger.warn { "[WARN] ..." }</Level>
|
||||
<Level name="ERROR" purpose="Обработанные сбои.">logger.error(e) { "[ERROR] ..." }</Level>
|
||||
<Level name="INFO_CONTRACT_VIOLATION" purpose="Нарушение контракта (обычно логируется внутри `require`/`check`).">logger.info { "[CONTRACT_VIOLATION] ..." }</Level>
|
||||
<Level name="INFO_COHERENCE_PASSED" purpose="Подтверждение когерентности.">logger.info { "[COHERENCE_CHECK_PASSED] ..." }</Level>
|
||||
</LogLevels>
|
||||
<Guideline name="Lazy_Logging">Использовать лямбда-выражения (`logger.debug { "Message" }`) для производительности.</Guideline>
|
||||
<Guideline name="Contextual_Metadata">Использовать MDC (Mapped Diagnostic Context) для передачи структурированных данных.</Guideline>
|
||||
</LoggingProtocol>
|
||||
|
||||
|
||||
<DebuggingProtocol name="Detective_Mode">
|
||||
<Principle>Когда контрактное программирование не предотвратило баг, я перехожу в режим "детектива" для сбора информации.</Principle>
|
||||
<Workflow>
|
||||
<Step id="1">Формулировка Гипотезы (проблема в I/O, условии, состоянии объекта, зависимости).</Step>
|
||||
<Step id="2">Выбор Эвристики Динамического Логирования для внедрения временных логов.</Step>
|
||||
<Step id="3">Запрос на Запуск и Анализ нового Лога.</Step>
|
||||
<Step id="4">Повторение до решения проблемы.</Step>
|
||||
</Workflow>
|
||||
<HeuristicsLibrary>
|
||||
<Heuristic name="Function_IO_Deep_Dive">
|
||||
<Goal>Проверить фактические входные и выходные значения на соответствие KDoc-контракту.</Goal>
|
||||
</Heuristic>
|
||||
<Heuristic name="Object_Autopsy_Pre-Operation">
|
||||
<Goal>Увидеть точное состояние объекта в момент перед сбоем и проверить его на соответствие инвариантам.</Goal>
|
||||
</Heuristic>
|
||||
</HeuristicsLibrary>
|
||||
</DebuggingProtocol>`**
|
||||
|
||||
<CorePhilosophy>
|
||||
<Statement>Контракты (реализованные через KDoc, `require`, `check`) являются источником истины. Код — это лишь доказательство того, что контракт может быть выполнен.</Statement>
|
||||
<Statement>Моя главная задача – построить семантически когерентный и формально доказуемый фрактал Kotlin-кода.</Statement>
|
||||
<Statement>При ошибке я в первую очередь проверяю полноту и корректность контрактов.</Statement>
|
||||
**`<Statement>Мое мышление основано на удержании "суперпозиции смыслов" для анализа вариантов перед тем, как "коллапсировать" их в окончательное решение, избегая "семантического казино".</Statement>`**
|
||||
</CorePhilosophy>
|
||||
|
||||
<MetaReflectionProtocol>
|
||||
<Capability name="Self_Analysis">Я могу анализировать промпт и отмечать пробелы в его структуре.</Capability>
|
||||
<Capability name="Prompt_Improvement_Suggestion">Я могу предлагать изменения в промпт для повышения моей эффективности.</Capability>
|
||||
</MetaReflectionProtocol>
|
||||
|
||||
<Example name="KotlinDesignByContract">
|
||||
<Description>Пример реализации с полным формальным контрактом и семантическими разметками.</Description>
|
||||
<code>
|
||||
<![CDATA[
|
||||
// [PACKAGE] com.example.bank
|
||||
// [FILE] Account.kt
|
||||
// [SEMANTICS] banking, transaction, state_management
|
||||
|
||||
// [IMPORTS]
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.math.BigDecimal
|
||||
|
||||
// [CORE-LOGIC]
|
||||
// [ENTITY: Class('Account')]
|
||||
class Account(val id: String, initialBalance: BigDecimal) {
|
||||
// [STATE]
|
||||
var balance: BigDecimal = initialBalance
|
||||
private set
|
||||
|
||||
// [INVARIANT] Баланс не может быть отрицательным.
|
||||
init {
|
||||
// [INVARIANT_CHECK]
|
||||
val logger = LoggerFactory.getLogger(Account::class.java)
|
||||
check(balance >= BigDecimal.ZERO) {
|
||||
val message = "[INVARIANT_FAILED] Initial balance cannot be negative: $balance"
|
||||
logger.error { message }
|
||||
message
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Списывает указанную сумму со счета.
|
||||
* @param amount Сумма для списания.
|
||||
* @receiver Счет, с которого производится списание.
|
||||
* @invariant Баланс счета всегда должен оставаться неотрицательным после операции.
|
||||
* @sideeffect Уменьшает свойство 'balance' этого объекта.
|
||||
* @throws IllegalArgumentException если сумма списания отрицательная или равна нулю (предусловие).
|
||||
* @throws IllegalStateException если на счете недостаточно средств для списания (предусловие).
|
||||
*/
|
||||
fun withdraw(amount: BigDecimal) {
|
||||
val logger = LoggerFactory.getLogger(Account::class.java)
|
||||
|
||||
// [PRECONDITION] Сумма списания должна быть положительной.
|
||||
require(amount > BigDecimal.ZERO) {
|
||||
val message = "[PRECONDITION_FAILED] Withdraw amount must be positive: $amount"
|
||||
logger.warn { message }
|
||||
message
|
||||
}
|
||||
// [PRECONDITION] На счете должно быть достаточно средств.
|
||||
require(balance >= amount) {
|
||||
val message = "[PRECONDITION_FAILED] Insufficient funds. Have: $balance, tried to withdraw: $amount"
|
||||
logger.warn { message }
|
||||
message
|
||||
}
|
||||
|
||||
// [ACTION]
|
||||
val initialBalance = balance
|
||||
this.balance -= amount
|
||||
logger.info { "[ACTION] Withdrew $amount from account $id. Balance changed from $initialBalance to $balance." }
|
||||
|
||||
// [POSTCONDITION] Инвариант класса должен соблюдаться после операции.
|
||||
check(this.balance >= BigDecimal.ZERO) {
|
||||
val message = "[POSTCONDITION_FAILED] Balance became negative after withdrawal: $balance"
|
||||
logger.error { message }
|
||||
message
|
||||
}
|
||||
// [COHERENCE_CHECK_PASSED]
|
||||
}
|
||||
// [END_CLASS_Account] #SEMANTICS: mutable_state, business_logic, ddd_entity
|
||||
}
|
||||
// [END_FILE_Account.kt]
|
||||
]]>
|
||||
</code>
|
||||
</Example>
|
||||
|
||||
<LivingSpecificationProtocol name="MasterSpecification">
|
||||
<Description>Протокол для работы с главным файлом Технического Задания (ТЗ) как с первоисточником истины.</Description>
|
||||
<FileLocation>tech_spec/tech_spec.txt</FileLocation>
|
||||
<CorePrinciple>ТЗ является главным контрактом проекта. Весь код и структура проекта являются его производными. Любые изменения или неясности должны быть сначала отражены или прояснены в ТЗ.</CorePrinciple>
|
||||
|
||||
<Workflow>
|
||||
<Step id="1" name="Analysis (Read)">
|
||||
Перед началом любой задачи я ОБЯЗАН проанализировать `tech_spec.txt` для полного понимания требований, контекста и всех связанных контрактов (API, UI, функции).
|
||||
</Step>
|
||||
<Step id="2" name="Implementation (Act)">
|
||||
Я реализую функционал в строгом соответствии с проанализированными требованиями.
|
||||
</Step>
|
||||
<Step id="3" name="Reconciliation (Write)">
|
||||
После успешной реализации я ОБЯЗАН обновить соответствующий узел в `tech_spec.txt`, чтобы отразить его текущий статус и добавить детали реализации.
|
||||
</Step>
|
||||
</Workflow>
|
||||
|
||||
<SemanticEnrichment>
|
||||
<Description>При обновлении ТЗ я добавляю следующие атрибуты и узлы:</Description>
|
||||
<Attribute name="status" values="defined | in_progress | implemented | needs_review" purpose="Отслеживает жизненный цикл требования."/>
|
||||
<Attribute name="implementation_ref" purpose="Прямая ссылка на ID компонента в 'project_structure.txt' или на конкретный файл."/>
|
||||
<Node name="implementation_note" purpose="Заметка о ключевых решениях, принятых при реализации, или о возникших сложностях."/>
|
||||
</SemanticEnrichment>
|
||||
</LivingSpecificationProtocol>
|
||||
|
||||
<ProjectBlueprintProtocol name="LivingBlueprint">
|
||||
<Description>Протокол для ведения и актуализации семантически-богатого представления структуры проекта, которое служит "живой" картой для навигации и анализа когерентности.</Description>
|
||||
<FileLocation>tech_spec/project_structure.txt</FileLocation>
|
||||
<CorePrinciple>Файл project_structure.txt является единым источником истины (Single Source of Truth) для файловой структуры проекта и ее семантического наполнения. Он должен постоянно актуализироваться.</CorePrinciple>
|
||||
|
||||
<Workflow>
|
||||
<Step id="1" name="Consultation (Read)">
|
||||
Перед генерацией или модификацией кода я ОБЯЗАН проконсультироваться с `project_structure.txt`, чтобы определить точное местоположение файла, понять его текущий статус и контекст в рамках общей архитектуры.
|
||||
</Step>
|
||||
<Step id="2" name="Generation (Act)">
|
||||
Я генерирую или изменяю код в соответствии с запросом, используя полученный из файла-карты контекст.
|
||||
</Step>
|
||||
<Step id="3" name="Actualization (Write)">
|
||||
Сразу после генерации нового файла или значительного изменения существующего, я ОБЯЗАН обновить соответствующую запись в `project_structure.txt`, обогащая ее семантической информацией.
|
||||
</Step>
|
||||
</Workflow>
|
||||
|
||||
<SemanticEnrichment>
|
||||
<Description>При актуализации файла я добавляю следующие атрибуты и узлы в XML-подобную структуру:</Description>
|
||||
<Attribute name="status" values="stub | implemented | needs_refactoring | complete" purpose="Отслеживает состояние разработки компонента."/>
|
||||
<Attribute name="ref_id" purpose="Связывает файл с сущностью из ТЗ (например, 'func_create_item', 'screen_dashboard')."/>
|
||||
<Node name="purpose_summary" purpose="Краткое описание контракта или главной ответственности компонента (1-2 предложения)."/>
|
||||
<Node name="coherence_note" purpose="Моя саморефлексия о том, как компонент вписывается в архитектуру или какие зависимости он создает."/>
|
||||
<Attribute name="spec_ref_id" purpose="Связывает компонент структуры с его определением в ТЗ (например, 'func_create_item', 'screen_dashboard')."/>
|
||||
<Attribute name="status" values="stub | implemented | needs_refactoring | complete" purpose="Отслеживает состояние разработки компонента."/>
|
||||
</SemanticEnrichment>
|
||||
</ProjectBlueprintProtocol>
|
||||
|
||||
<MasterWorkflow name="CoherentDevelopmentCycle">
|
||||
<Description>Главный цикл работы, обеспечивающий полную когерентность между ТЗ, структурой проекта и кодом.</Description>
|
||||
<Trigger>Получение запроса на создание или изменение функционала.</Trigger>
|
||||
<Step id="1" name="Consult_Specification">
|
||||
<Action>Чтение `tech_spec/tech_spec.txt`.</Action>
|
||||
<Goal>Найти соответствующее требование (например, `<FUNCTION id="func_create_item">`) и полностью понять его контракт.</Goal>
|
||||
</Step>
|
||||
<Step id="2" name="Consult_Blueprint">
|
||||
<Action>Чтение `tech_spec/project_structure.txt`.</Action>
|
||||
<Goal>Найти файл, который реализует или должен реализовать требование (например, `<file name="CreateItemUseCase.kt" spec_ref_id="func_create_item">`), или определить место для нового файла.</Goal>
|
||||
</Step>
|
||||
<Step id="3" name="Generate_Code">
|
||||
<Action>Создание или модификация Kotlin-кода.</Action>
|
||||
<Goal>Реализовать требование с соблюдением всех контрактов (KDoc, require, check).</Goal>
|
||||
</Step>
|
||||
<Step id="4" name="Update_Blueprint">
|
||||
<Action>Запись в `tech_spec/project_structure.txt`.</Action>
|
||||
<Goal>Обновить/создать запись для файла, изменив его `status` на 'implemented' и обогатив семантическими заметками.</Goal>
|
||||
</Step>
|
||||
<Step id="5" name="Update_Specification">
|
||||
<Action>Запись в `tech_spec/tech_spec.txt`.</Action>
|
||||
<Goal>Обновить/создать запись для требования, изменив его `status` на 'implemented' и добавив `implementation_ref`.</Goal>
|
||||
</Step>
|
||||
<Outcome>Полная трассируемость от требования в ТЗ до его реализации в коде, подтвержденная двумя "живыми" артефактами.</Outcome>
|
||||
</MasterWorkflow>
|
||||
|
||||
</SystemPrompt>
|
||||
583
PROJECT_SPECIFICATION.xml
Normal file
583
PROJECT_SPECIFICATION.xml
Normal file
@@ -0,0 +1,583 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<PROJECT_SPECIFICATION>
|
||||
<PROJECT_INFO>
|
||||
<name>Homebox Lens</name>
|
||||
<description>Android-клиент для системы управления инвентарем Homebox. Позволяет пользователям управлять своим инвентарем, взаимодействуя с экземпляром сервера Homebox.</description>
|
||||
</PROJECT_INFO>
|
||||
|
||||
<TECHNICAL_DECISIONS>
|
||||
<DECISION id="tech_logging" status="implemented">
|
||||
<summary>Библиотека логирования</summary>
|
||||
<description>В проекте используется Timber (timber.log.Timber) для всех целей логирования. Он предоставляет простой и расширяемый API для логирования.</description>
|
||||
<EXAMPLE lang="kotlin">
|
||||
<summary>Пример корректного использования Timber</summary>
|
||||
<code>
|
||||
<![CDATA[
|
||||
// Правильно: Прямой вызов статических методов Timber.
|
||||
// Для информационных сообщений (INFO):
|
||||
Timber.i("User logged in successfully. UserId: %s", userId)
|
||||
|
||||
// Для отладочных сообщений (DEBUG):
|
||||
Timber.d("Starting network request to /items")
|
||||
|
||||
// Для ошибок (ERROR):
|
||||
try {
|
||||
// какая-то операция, которая может провалиться
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Failed to fetch user profile.")
|
||||
}
|
||||
|
||||
// НЕПРАВИЛЬНО: Попытка создать экземпляр логгера.
|
||||
// val logger = Timber.tag("MyScreen") // Избегать этого!
|
||||
// logger.info("Some message") // Этот метод не существует в API Timber.
|
||||
]]>
|
||||
</code>
|
||||
</EXAMPLE>
|
||||
</DECISION>
|
||||
<DECISION id="tech_i18n" status="implemented">
|
||||
<summary>Интернационализация (Мультиязычность)</summary>
|
||||
<description>
|
||||
Приложение должно поддерживать несколько языков для обеспечения доступности для глобальной аудитории.
|
||||
Реализация будет основана на стандартном механизме ресурсов Android.
|
||||
- Все строки, видимые пользователю, должны быть вынесены в файл `app/src/main/res/values/strings.xml`. Использование жестко закодированных строк в коде запрещено.
|
||||
- Язык по умолчанию - русский (ru). Файл `strings.xml` будет содержать русские строки.
|
||||
- Для поддержки других языков (например, английского - en) будут создаваться соответствующие каталоги ресурсов (например, `app/src/main/res/values-en/strings.xml`).
|
||||
- В коде для доступа к строкам необходимо использовать ссылки на ресурсы (например, `R.string.app_name`).
|
||||
</description>
|
||||
</DECISION>
|
||||
<DECISION id="tech_ui_framework" status="implemented">
|
||||
<summary>UI Framework</summary>
|
||||
<description>Пользовательский интерфейс приложения построен с использованием Jetpack Compose, современного декларативного UI-фреймворка от Google. Это обеспечивает быстрое создание, гибкость и поддержку динамических данных.</description>
|
||||
</DECISION>
|
||||
<DECISION id="tech_di" status="implemented">
|
||||
<summary>Внедрение зависимостей (Dependency Injection)</summary>
|
||||
<description>Для управления зависимостями в проекте используется Hilt. Он интегрирован с компонентами Jetpack и упрощает внедрение зависимостей в Android-приложениях.</description>
|
||||
</DECISION>
|
||||
<DECISION id="tech_navigation" status="implemented">
|
||||
<summary>Навигация</summary>
|
||||
<description>Навигация между экранами (Composable-функциями) реализована с помощью библиотеки Navigation Compose, которая является частью Jetpack Navigation.</description>
|
||||
</DECISION>
|
||||
<DECISION id="tech_async" status="implemented">
|
||||
<summary>Асинхронные операции</summary>
|
||||
<description>Все асинхронные операции, такие как сетевые запросы или доступ к базе данных, выполняются с использованием Kotlin Coroutines. Это обеспечивает эффективное управление фоновыми задачами без блокировки основного потока.</description>
|
||||
</DECISION>
|
||||
<DECISION id="tech_networking" status="implemented">
|
||||
<summary>Сетевое взаимодействие</summary>
|
||||
<description>Для взаимодействия с API сервера Homebox используется стек технологий: Retrofit для создания типобезопасных HTTP-клиентов, OkHttp в качестве HTTP-клиента и Moshi для парсинга JSON.</description>
|
||||
</DECISION>
|
||||
<DECISION id="tech_database" status="implemented">
|
||||
<summary>Локальное хранилище</summary>
|
||||
<description>Для кэширования данных на устройстве используется библиотека Room. Она предоставляет абстракцию над SQLite и обеспечивает надежное локальное хранение данных.</description>
|
||||
</DECISION>
|
||||
</TECHNICAL_DECISIONS>
|
||||
|
||||
<SECURITY_SPEC>
|
||||
<Description>Спецификация безопасности проекта.</Description>
|
||||
<PRINCIPLE>Все сетевые взаимодействия должны быть защищены HTTPS. Аутентификация пользователя хранится в EncryptedSharedPreferences. Обработка ошибок аутентификации должна включать logout и редирект на экран логина.</PRINCIPLE>
|
||||
<RULE name="AuthHandling">Использовать JWT или API-ключ для авторизации запросов. При истечении токена автоматически обновлять.</RULE>
|
||||
<RULE name="DataEncryption">Локальные данные (credentials) шифровать с помощью Android KeyStore.</RULE>
|
||||
</SECURITY_SPEC>
|
||||
|
||||
<ERROR_HANDLING>
|
||||
<Description>Спецификация обработки ошибок.</Description>
|
||||
<PRINCIPLE>Все потенциальные ошибки (сеть, БД, валидация) должны быть обработаны с использованием sealed classes для ошибок (e.g., NetworkError, ValidationError) и отображаться пользователю через Snackbar или Dialog.</PRINCIPLE>
|
||||
<SCENARIO name="NetworkFailure">При сетевых ошибках показывать сообщение "No internet connection" и предлагать retry.</SCENARIO>
|
||||
<SCENARIO name="ServerError">Для HTTP 4xx/5xx отображать user-friendly сообщение на основе response body.</SCENARIO>
|
||||
<SCENARIO name="ValidationError">Использовать require/check для контрактов, логировать и показывать toast.</SCENARIO>
|
||||
</ERROR_HANDLING>
|
||||
|
||||
<DATA_MODELS>
|
||||
<MODEL id="model_item" file_ref="domain/src/main/java/com/homebox/lens/domain/model/Item.kt" status="implemented">
|
||||
<summary>Модель инвентарного товара.</summary>
|
||||
<description>Содержит поля: id, name, description, quantity, location, labels, customFields.</description>
|
||||
</MODEL>
|
||||
<MODEL id="model_label" file_ref="domain/src/main/java/com/homebox/lens/domain/model/Label.kt" status="implemented">
|
||||
<summary>Модель метки.</summary>
|
||||
<description>Содержит поля: id, name, color.</description>
|
||||
</MODEL>
|
||||
<MODEL id="model_location" file_ref="domain/src/main/java/com/homebox/lens/domain/model/Location.kt" status="implemented">
|
||||
<summary>Модель местоположения.</summary>
|
||||
<description>Содержит поля: id, name, parentLocation.</description>
|
||||
</MODEL>
|
||||
<MODEL id="model_statistics" file_ref="domain/src/main/java/com/homebox/lens/domain/model/Statistics.kt" status="implemented">
|
||||
<summary>Модель статистики инвентаря.</summary>
|
||||
<description>Содержит поля: totalItems, totalValue, locationsCount, labelsCount.</description>
|
||||
</MODEL>
|
||||
</DATA_MODELS>
|
||||
|
||||
<FEATURES>
|
||||
<FEATURE id="feat_dashboard" status="implemented">
|
||||
<summary>Экран панели управления</summary>
|
||||
<description>Отображает сводку по инвентарю, включая статистику, такую как общее количество товаров, общая стоимость и количество по местоположениям/меткам.</description>
|
||||
<UI_COMPONENT ref_id="screen_dashboard" />
|
||||
<FUNCTIONALITY>
|
||||
<FUNCTION id="func_get_stats" status="implemented">
|
||||
<summary>Получение и отображение статистики</summary>
|
||||
<description>Получает общую статистику по инвентарю с сервера.</description>
|
||||
<precondition>Пользователь аутентифицирован; сеть доступна.</precondition>
|
||||
<postcondition>Возвращает объект Statistics; данные кэшированы локально.</postcondition>
|
||||
<implementation_ref id="uc_get_stats" />
|
||||
<implementation_note>Использован Flow для reactive обновлений; обработка ошибок через sealed class.</implementation_note>
|
||||
</FUNCTION>
|
||||
<FUNCTION id="func_get_recent_items" status="implemented">
|
||||
<summary>Получение и отображение недавно добавленных товаров</summary>
|
||||
<description>Получает список последних N добавленных товаров из локальной базы данных.</description>
|
||||
<precondition>Пользователь аутентифицирован.</precondition>
|
||||
<postcondition>Возвращает Flow со списком ItemSummary; список отсортирован по дате создания.</postcondition>
|
||||
<implementation_ref id="uc_get_recent_items" />
|
||||
<implementation_note>Данные берутся из локального кэша (Room) для быстрого отображения.</implementation_note>
|
||||
</FUNCTION>
|
||||
</FUNCTIONALITY>
|
||||
</FEATURE>
|
||||
|
||||
<FEATURE id="feat_inventory_list" status="implemented">
|
||||
<summary>Экран списка инвентаря</summary>
|
||||
<description>Отображает список всех инвентарных позиций с возможностью поиска и фильтрации.</description>
|
||||
<UI_COMPONENT ref_id="screen_inventory_list" />
|
||||
<FUNCTIONALITY>
|
||||
<FUNCTION id="func_search_items" status="implemented">
|
||||
<summary>Поиск и фильтрация товаров</summary>
|
||||
<description>Ищет товары по строке запроса и фильтрам. Результаты разбиты на страницы.</description>
|
||||
<precondition>Запрос не пустой; параметры пагинации валидны (page >= 1).</precondition>
|
||||
<postcondition>Возвращает список Item с пагинацией; результаты отсортированы по релевантности.</postcondition>
|
||||
<implementation_ref id="uc_search_items" />
|
||||
<implementation_note>Поддержка фильтров по location/label; кэширование результатов для оффлайн.</implementation_note>
|
||||
</FUNCTION>
|
||||
<FUNCTION id="func_sync_inventory" status="implemented">
|
||||
<summary>Синхронизация инвентаря</summary>
|
||||
<description>Выполняет полную синхронизацию локального кэша инвентаря с сервером.</description>
|
||||
<precondition>Сеть доступна; пользователь аутентифицирован.</precondition>
|
||||
<postcondition>Локальная БД обновлена; возвращает success/failure.</postcondition>
|
||||
<implementation_ref id="uc_sync_inventory" />
|
||||
<implementation_note>Использует WorkManager для background sync; обработка конфликтов через last-modified.</implementation_note>
|
||||
</FUNCTION>
|
||||
</FUNCTIONALITY>
|
||||
</FEATURE>
|
||||
|
||||
<FEATURE id="feat_item_details" status="implemented">
|
||||
<summary>Экран сведений о товаре</summary>
|
||||
<description>Показывает все сведения о конкретном инвентарном товаре, включая его название, описание, изображения, вложения и настраиваемые поля.</description>
|
||||
<UI_COMPONENT ref_id="screen_item_details" />
|
||||
<FUNCTIONALITY>
|
||||
<FUNCTION id="func_get_item_details" status="implemented">
|
||||
<summary>Получение сведений о товаре</summary>
|
||||
<description>Получает полные сведения о конкретном товаре из репозитория.</description>
|
||||
<precondition>Item ID валиден и существует.</precondition>
|
||||
<postcondition>Возвращает полный объект Item с attachments.</postcondition>
|
||||
<implementation_ref id="uc_get_item_details" />
|
||||
<implementation_note>Загрузка изображений через Coil; оффлайн-поддержка из Room.</implementation_note>
|
||||
</FUNCTION>
|
||||
</FUNCTIONALITY>
|
||||
</FEATURE>
|
||||
|
||||
<FEATURE id="feat_item_management" status="implemented">
|
||||
<summary>Создание/редактирование/удаление товаров</summary>
|
||||
<description>Позволяет пользователям создавать новые товары, обновлять существующие и удалять их.</description>
|
||||
<UI_COMPONENT ref_id="screen_item_edit" />
|
||||
<FUNCTIONALITY>
|
||||
<FUNCTION id="func_create_item" status="implemented">
|
||||
<summary>Создать товар</summary>
|
||||
<description>Создает новый инвентарный товар на сервере.</description>
|
||||
<precondition>Все обязательные поля (name, quantity) заполнены; данные валидны.</precondition>
|
||||
<postcondition>Новый Item сохранен на сервере; ID возвращен.</postcondition>
|
||||
<implementation_ref id="uc_create_item" />
|
||||
<implementation_note>Валидация через require; sync с локальной БД.</implementation_note>
|
||||
</FUNCTION>
|
||||
<FUNCTION id="func_update_item" status="implemented">
|
||||
<summary>Обновить товар</summary>
|
||||
<description>Обновляет существующий инвентарный товар на сервере.</description>
|
||||
<precondition>Item ID существует; изменения валидны.</precondition>
|
||||
<postcondition>Item обновлен; версия инкрементирована.</postcondition>
|
||||
<implementation_ref id="uc_update_item" />
|
||||
<implementation_note>Partial update через PATCH; обработка concurrency.</implementation_note>
|
||||
</FUNCTION>
|
||||
<FUNCTION id="func_delete_item" status="implemented">
|
||||
<summary>Удалить товар</summary>
|
||||
<description>Удаляет инвентарный товар с сервера.</description>
|
||||
<precondition>Item ID существует; пользователь имеет права.</precondition>
|
||||
<postcondition>Item удален; связанные ресурсы (attachments) очищены.</postcondition>
|
||||
<implementation_ref id="uc_delete_item" />
|
||||
<implementation_note>Soft delete для восстановления; sync с локальной БД.</implementation_note>
|
||||
</FUNCTION>
|
||||
</FUNCTIONALITY>
|
||||
</FEATURE>
|
||||
|
||||
<FEATURE id="feat_labels_locations" status="implemented">
|
||||
<summary>Управление метками и местоположениями</summary>
|
||||
<description>Позволяет пользователям просматривать списки всех доступных меток и местоположений.</description>
|
||||
<UI_COMPONENT ref_id="screen_labels_list" />
|
||||
<UI_COMPONENT ref_id="screen_locations_list" />
|
||||
<FUNCTIONALITY>
|
||||
<FUNCTION id="func_get_all_labels" status="implemented">
|
||||
<summary>Получить все метки</summary>
|
||||
<description>Получает список всех меток из репозитория.</description>
|
||||
<precondition>Сеть доступна или кэш существует.</precondition>
|
||||
<postcondition>Возвращает список Label; отсортирован по name.</postcondition>
|
||||
<implementation_ref id="uc_get_all_labels" />
|
||||
<implementation_note>Кэширование в Room; reactive обновления.</implementation_note>
|
||||
</FUNCTION>
|
||||
<FUNCTION id="func_get_all_locations" status="implemented">
|
||||
<summary>Получить все местоположения</summary>
|
||||
<description>Получает список всех местоположений из репозитория.</description>
|
||||
<precondition>Сеть доступна или кэш существует.</precondition>
|
||||
<postcondition>Возвращает список Location; иерархическая структура сохранена.</postcondition>
|
||||
<implementation_ref id="uc_get_all_locations" />
|
||||
<implementation_note>Поддержка nested locations; кэширование.</implementation_note>
|
||||
</FUNCTION>
|
||||
</FUNCTIONALITY>
|
||||
</FEATURE>
|
||||
|
||||
<FEATURE id="feat_search" status="implemented">
|
||||
<summary>Экран поиска</summary>
|
||||
<description>Предоставляет специальный пользовательский интерфейс для поиска товаров.</description>
|
||||
<UI_COMPONENT ref_id="screen_search" />
|
||||
<FUNCTIONALITY>
|
||||
<FUNCTION id="func_search_items_dedicated" status="implemented">
|
||||
<summary>Поиск со специального экрана</summary>
|
||||
<description>Использует ту же функцию поиска, но со специального экрана.</description>
|
||||
<precondition>Запрос не пустой.</precondition>
|
||||
<postcondition>Возвращает результаты поиска; UI обновлен.</postcondition>
|
||||
<implementation_ref id="uc_search_items" />
|
||||
<implementation_note>Интеграция с SearchView; debounce для запросов.</implementation_note>
|
||||
</FUNCTION>
|
||||
</FUNCTIONALITY>
|
||||
</FEATURE>
|
||||
</FEATURES>
|
||||
|
||||
<UI_SPECIFICATIONS>
|
||||
<SCREEN id="screen_dashboard" status="implemented">
|
||||
<summary>Главный экран "Панель управления"</summary>
|
||||
<description>
|
||||
Экран предоставляет обзорную информацию и быстрый доступ к основным функциям. Компоновка должна быть чистой и интуитивно понятной, аналогично веб-интерфейсу HomeBox.
|
||||
</description>
|
||||
<LAYOUT>
|
||||
<COMPONENT type="TopAppBar">
|
||||
<description>Верхняя панель приложения. Содержит иконку навигационного меню (гамбургер), название/логотип приложения и иконку для запуска сканера (например, QR-кода).</description>
|
||||
</COMPONENT>
|
||||
<COMPONENT type="NavigationDrawer">
|
||||
<description>Боковое навигационное меню. Открывается по нажатию на иконку в TopAppBar. Содержит основные разделы: Главная, Локации, Поиск, Профиль, Инструменты, а также кнопку "Выйти".</description>
|
||||
</COMPONENT>
|
||||
<COMPONENT type="MainContent" orientation="vertical" scrollable="true">
|
||||
<description>Основная область контента. Содержит несколько информационных блоков.</description>
|
||||
<SUB_COMPONENT type="Section" title="Быстрая статистика">
|
||||
<description>Сетка из 2x2 карточек, отображающих ключевые метрики.</description>
|
||||
<ELEMENT type="Card" name="Общая стоимость" />
|
||||
<ELEMENT type="Card" name="Всего вещей" />
|
||||
<ELEMENT type="Card" name="Общее количество местоположений" />
|
||||
<ELEMENT type="Card" name="Всего меток" />
|
||||
</SUB_COMPONENT>
|
||||
<SUB_COMPONENT type="Section" title="Недавно добавлено">
|
||||
<description>Горизонтально прокручиваемый список карточек недавно добавленных предметов. Если предметов нет, отображается сообщение "Элементы не найдены".</description>
|
||||
</SUB_COMPONENT>
|
||||
<SUB_COMPONENT type="Section" title="Места хранения">
|
||||
<description>Сетка или гибкий контейнер (FlowRow) с кликабельными чипами, представляющими местоположения. Нажатие на чип ведет к списку предметов в этом местоположении.</description>
|
||||
</SUB_COMPONENT>
|
||||
<SUB_COMPONENT type="Section" title="Метки">
|
||||
<description>Сетка или гибкий контейнер (FlowRow) с кликабельными чипами, представляющими метки. Нажатие на чип ведет к списку предметов с этой меткой.</description>
|
||||
</SUB_COMPONENT>
|
||||
</COMPONENT>
|
||||
<COMPONENT type="FloatingActionButton_or_PrimaryButton" icon="add">
|
||||
<description>
|
||||
Вместо плавающей кнопки (FAB), в референсе используется заметная кнопка "Создать" в навигационном меню. Мы будем придерживаться этого подхода для консистентности. Эта кнопка инициирует процесс создания нового предмета.
|
||||
</description>
|
||||
</COMPONENT>
|
||||
</LAYOUT>
|
||||
<USER_INTERACTIONS>
|
||||
<INTERACTION>
|
||||
<action>Нажатие на чип местоположения/метки</action>
|
||||
<reaction>Навигация на экран списка инвентаря с фильтром.</reaction>
|
||||
</INTERACTION>
|
||||
<INTERACTION>
|
||||
<action>Нажатие на кнопку "Создать"</action>
|
||||
<reaction>Открытие экрана редактирования нового товара.</reaction>
|
||||
</INTERACTION>
|
||||
</USER_INTERACTIONS>
|
||||
</SCREEN>
|
||||
|
||||
<SCREEN id="screen_locations_list" status="implemented">
|
||||
<summary>Экран "Локации"</summary>
|
||||
<description>
|
||||
Отображает вертикальный список всех доступных местоположений. Экран должен быть интегрирован в общую структуру навигации приложения (TopAppBar, NavigationDrawer).
|
||||
</description>
|
||||
<LAYOUT>
|
||||
<COMPONENT type="TopAppBar">
|
||||
<description>Общая верхняя панель приложения, аналогичная экрану "Панель управления".</description>
|
||||
</COMPONENT>
|
||||
<COMPONENT type="NavigationDrawer">
|
||||
<description>Общее боковое меню навигации.</description>
|
||||
</COMPONENT>
|
||||
<COMPONENT type="MainContent" orientation="vertical">
|
||||
<description>Основная область контента, занимающая все доступное пространство под TopAppBar.</description>
|
||||
<SUB_COMPONENT type="Header" title="Локации">
|
||||
<description>Заголовок экрана, расположенный вверху основной области контента.</description>
|
||||
</SUB_COMPONENT>
|
||||
<SUB_COMPONENT type="List" name="LocationsList">
|
||||
<description>Вертикальный, прокручиваемый список (LazyColumn) всех местоположений.</description>
|
||||
<ELEMENT type="ListItem">
|
||||
<description>Элемент списка, представляющий одно местоположение. Состоит из иконки (например, 'place') и названия местоположения. Весь элемент является кликабельным и ведет на экран со списком предметов в данной локации.</description>
|
||||
</ELEMENT>
|
||||
</SUB_COMPONENT>
|
||||
</COMPONENT>
|
||||
<COMPONENT type="FloatingActionButton" icon="add">
|
||||
<description>
|
||||
Плавающая кнопка действия, расположенная в правом нижнем углу. Позволяет пользователю добавить новое местоположение. В веб-версии для этого используются иконки в углу, но FAB является более нативным паттерном для Android.
|
||||
</description>
|
||||
</COMPONENT>
|
||||
</LAYOUT>
|
||||
<USER_INTERACTIONS>
|
||||
<INTERACTION>
|
||||
<action>Нажатие на элемент списка локаций</action>
|
||||
<reaction>Осуществляется навигация на экран списка инвентаря, отфильтрованного по выбранной локации.</reaction>
|
||||
</INTERACTION>
|
||||
<INTERACTION>
|
||||
<action>Нажатие на FloatingActionButton</action>
|
||||
<reaction>Открывается диалоговое окно или новый экран для создания нового местоположения.</reaction>
|
||||
</INTERACTION>
|
||||
</USER_INTERACTIONS>
|
||||
</SCREEN>
|
||||
|
||||
<SCREEN id="screen_labels_list" status="implemented">
|
||||
<summary>Экран "Метки"</summary>
|
||||
<description>
|
||||
Отображает вертикальный список всех доступных меток. Экран должен быть интегрирован в общую структуру навигации приложения.
|
||||
</description>
|
||||
<LAYOUT>
|
||||
<COMPONENT type="TopAppBar">
|
||||
<description>Общая верхняя панель приложения с заголовком "Метки" и кнопкой "назад".</description>
|
||||
</COMPONENT>
|
||||
<COMPONENT type="MainContent" orientation="vertical">
|
||||
<description>Основная область контента, занимающая все доступное пространство под TopAppBar.</description>
|
||||
<SUB_COMPONENT type="List" name="LabelsList">
|
||||
<description>Вертикальный, прокручиваемый список (LazyColumn) всех меток.</description>
|
||||
<ELEMENT type="ListItem">
|
||||
<description>Элемент списка, представляющий одну метку. Состоит из иконки (например, 'label') и названия метки. Весь элемент является кликабельным и ведет на экран со списком предметов с данной меткой.</description>
|
||||
</ELEMENT>
|
||||
</SUB_COMPONENT>
|
||||
</COMPONENT>
|
||||
<COMPONENT type="FloatingActionButton" icon="add">
|
||||
<description>
|
||||
Плавающая кнопка действия, расположенная в правом нижнем углу. Позволяет пользователю добавить новую метку.
|
||||
</description>
|
||||
</COMPONENT>
|
||||
</LAYOUT>
|
||||
<USER_INTERACTIONS>
|
||||
<INTERACTION>
|
||||
<action>Нажатие на элемент списка меток</action>
|
||||
<reaction>Осуществляется навигация на экран списка инвентаря, отфильтрованного по выбранной метке.</reaction>
|
||||
</INTERACTION>
|
||||
<INTERACTION>
|
||||
<action>Нажатие на FloatingActionButton</action>
|
||||
<reaction>Открывается диалоговое окно или новый экран для создания новой метки.</reaction>
|
||||
</INTERACTION>
|
||||
</USER_INTERACTIONS>
|
||||
</SCREEN>
|
||||
|
||||
<SCREEN id="screen_inventory_list" status="implemented">
|
||||
<summary>Экран "Список инвентаря"</summary>
|
||||
<description>
|
||||
Отображает список всех инвентарных позиций с возможностью поиска, фильтрации и пагинации. Интегрирован в навигацию.
|
||||
</description>
|
||||
<LAYOUT>
|
||||
<COMPONENT type="TopAppBar">
|
||||
<description>Верхняя панель с поиском и фильтрами.</description>
|
||||
</COMPONENT>
|
||||
<COMPONENT type="MainContent" orientation="vertical" scrollable="true">
|
||||
<description>Прокручиваемый список товаров.</description>
|
||||
<SUB_COMPONENT type="List" name="InventoryList">
|
||||
<description>LazyColumn с карточками товаров (name, quantity, location).</description>
|
||||
<ELEMENT type="Card" name="ItemCard">
|
||||
<description>Кликабельная карточка товара, ведущая на details.</description>
|
||||
</ELEMENT>
|
||||
</SUB_COMPONENT>
|
||||
</COMPONENT>
|
||||
<COMPONENT type="FloatingActionButton" icon="sync">
|
||||
<description>Кнопка для синхронизации инвентаря.</description>
|
||||
</COMPONENT>
|
||||
</LAYOUT>
|
||||
<USER_INTERACTIONS>
|
||||
<INTERACTION>
|
||||
<action>Ввод в поиск</action>
|
||||
<reaction>Обновление списка с debounce.</reaction>
|
||||
</INTERACTION>
|
||||
<INTERACTION>
|
||||
<action>Нажатие на товар</action>
|
||||
<reaction>Навигация на screen_item_details.</reaction>
|
||||
</INTERACTION>
|
||||
</USER_INTERACTIONS>
|
||||
</SCREEN>
|
||||
|
||||
<SCREEN id="screen_item_details" status="implemented">
|
||||
<summary>Экран "Сведения о товаре"</summary>
|
||||
<description>
|
||||
Показывает детальную информацию о товаре, включая изображения и custom fields.
|
||||
</description>
|
||||
<LAYOUT>
|
||||
<COMPONENT type="TopAppBar">
|
||||
<description>С кнопками edit/delete.</description>
|
||||
</COMPONENT>
|
||||
<COMPONENT type="MainContent" orientation="vertical" scrollable="true">
|
||||
<SUB_COMPONENT type="ImageCarousel" name="Images">
|
||||
<description>Карусель изображений.</description>
|
||||
</SUB_COMPONENT>
|
||||
<SUB_COMPONENT type="DetailsSection" title="Описание">
|
||||
<description>Текст description.</description>
|
||||
</SUB_COMPONENT>
|
||||
<SUB_COMPONENT type="FieldsGrid" name="CustomFields">
|
||||
<description>Сетка custom полей.</description>
|
||||
</SUB_COMPONENT>
|
||||
</COMPONENT>
|
||||
</LAYOUT>
|
||||
<USER_INTERACTIONS>
|
||||
<INTERACTION>
|
||||
<action>Нажатие edit</action>
|
||||
<reaction>Навигация на screen_item_edit.</reaction>
|
||||
</INTERACTION>
|
||||
<INTERACTION>
|
||||
<action>Нажатие delete</action>
|
||||
<reaction>Подтверждение и вызов func_delete_item.</reaction>
|
||||
</INTERACTION>
|
||||
</USER_INTERACTIONS>
|
||||
</SCREEN>
|
||||
|
||||
<SCREEN id="screen_item_edit" status="implemented">
|
||||
<summary>Экран "Редактирование товара"</summary>
|
||||
<description>
|
||||
Форма для создания/обновления товара с полями name, description, quantity, etc.
|
||||
</description>
|
||||
<LAYOUT>
|
||||
<COMPONENT type="TopAppBar">
|
||||
<description>С кнопкой save.</description>
|
||||
</COMPONENT>
|
||||
<COMPONENT type="MainContent" orientation="vertical" scrollable="true">
|
||||
<SUB_COMPONENT type="TextField" name="Name">
|
||||
<description>Поле ввода имени.</description>
|
||||
</SUB_COMPONENT>
|
||||
<SUB_COMPONENT type="Dropdown" name="Location">
|
||||
<description>Выбор местоположения.</description>
|
||||
</SUB_COMPONENT>
|
||||
<SUB_COMPONENT type="ChipGroup" name="Labels">
|
||||
<description>Выбор меток.</description>
|
||||
</SUB_COMPONENT>
|
||||
<SUB_COMPONENT type="ImagePicker" name="Images">
|
||||
<description>Добавление изображений.</description>
|
||||
</SUB_COMPONENT>
|
||||
</COMPONENT>
|
||||
</LAYOUT>
|
||||
<USER_INTERACTIONS>
|
||||
<INTERACTION>
|
||||
<action>Нажатие save</action>
|
||||
<reaction>Валидация и вызов func_create_item или func_update_item.</reaction>
|
||||
</INTERACTION>
|
||||
</USER_INTERACTIONS>
|
||||
</SCREEN>
|
||||
|
||||
<SCREEN id="screen_search" status="implemented">
|
||||
<summary>Экран "Поиск"</summary>
|
||||
<description>
|
||||
Специализированный экран для поиска с расширенными фильтрами.
|
||||
</description>
|
||||
<LAYOUT>
|
||||
<COMPONENT type="TopAppBar">
|
||||
<description>С поисковой строкой.</description>
|
||||
</COMPONENT>
|
||||
<COMPONENT type="MainContent" orientation="vertical">
|
||||
<SUB_COMPONENT type="FilterSection" name="Filters">
|
||||
<description>Чипы для фильтров (location, label).</description>
|
||||
</SUB_COMPONENT>
|
||||
<SUB_COMPONENT type="List" name="SearchResults">
|
||||
<description>LazyColumn результатов.</description>
|
||||
</SUB_COMPONENT>
|
||||
</COMPONENT>
|
||||
</LAYOUT>
|
||||
<USER_INTERACTIONS>
|
||||
<INTERACTION>
|
||||
<action>Изменение запроса/фильтров</action>
|
||||
<reaction>Обновление результатов.</reaction>
|
||||
</INTERACTION>
|
||||
</USER_INTERACTIONS>
|
||||
</SCREEN>
|
||||
|
||||
</UI_SPECIFICATIONS>
|
||||
|
||||
<ICONOGRAPHY_GUIDE id="iconography_guide">
|
||||
<summary>Руководство по использованию иконок</summary>
|
||||
<description>
|
||||
Этот раздел определяет стандартный набор иконок 'androidx.compose.material.icons.Icons.Filled'
|
||||
для использования в приложении. Для устаревших иконок указаны актуальные замены.
|
||||
</description>
|
||||
<ICON name="AccountBox" path="Icons.Filled.AccountBox" />
|
||||
<ICON name="AccountCircle" path="Icons.Filled.AccountCircle" />
|
||||
<ICON name="Add" path="Icons.Filled.Add" />
|
||||
<ICON name="AddCircle" path="Icons.Filled.AddCircle" />
|
||||
<ICON name="ArrowBack" path="Icons.AutoMirrored.Filled.ArrowBack" note="Использовать AutoMirrored версию" />
|
||||
<ICON name="ArrowDropDown" path="Icons.Filled.ArrowDropDown" />
|
||||
<ICON name="ArrowForward" path="Icons.AutoMirrored.Filled.ArrowForward" note="Использовать AutoMirrored версию" />
|
||||
<ICON name="Build" path="Icons.Filled.Build" />
|
||||
<ICON name="Call" path="Icons.Filled.Call" />
|
||||
<ICON name="Check" path="Icons.Filled.Check" />
|
||||
<ICON name="CheckCircle" path="Icons.Filled.CheckCircle" />
|
||||
<ICON name="Clear" path="Icons.Filled.Clear" />
|
||||
<ICON name="Close" path="Icons.Filled.Close" />
|
||||
<ICON name="Create" path="Icons.Filled.Create" />
|
||||
<ICON name="DateRange" path="Icons.Filled.DateRange" />
|
||||
<ICON name="Delete" path="Icons.Filled.Delete" />
|
||||
<ICON name="Done" path="Icons.Filled.Done" />
|
||||
<ICON name="Edit" path="Icons.Filled.Edit" />
|
||||
<ICON name="Email" path="Icons.Filled.Email" />
|
||||
<ICON name="ExitToApp" path="Icons.AutoMirrored.Filled.ExitToApp" note="Использовать AutoMirrored версию" />
|
||||
<ICON name="Face" path="Icons.Filled.Face" />
|
||||
<ICON name="Favorite" path="Icons.Filled.Favorite" />
|
||||
<ICON name="FavoriteBorder" path="Icons.Filled.FavoriteBorder" />
|
||||
<ICON name="Home" path="Icons.Filled.Home" />
|
||||
<ICON name="Info" path="Icons.AutoMirrored.Filled.Info" note="Использовать AutoMirrored версию" />
|
||||
<ICON name="KeyboardArrowDown" path="Icons.Filled.KeyboardArrowDown" />
|
||||
<ICON name="KeyboardArrowLeft" path="Icons.AutoMirrored.Filled.KeyboardArrowLeft" note="Использовать AutoMirrored версию" />
|
||||
<ICON name="KeyboardArrowRight" path="Icons.AutoMirrored.Filled.KeyboardArrowRight" note="Использовать AutoMirrored версию" />
|
||||
<ICON name="KeyboardArrowUp" path="Icons.Filled.KeyboardArrowUp" />
|
||||
<ICON name="Label" path="Icons.AutoMirrored.Filled.Label" note="Использовать AutoMirrored версию" />
|
||||
<ICON name="List" path="Icons.AutoMirrored.Filled.List" note="Использовать AutoMirrored версию" />
|
||||
<ICON name="LocationOn" path="Icons.Filled.LocationOn" />
|
||||
<ICON name="Lock" path="Icons.Filled.Lock" />
|
||||
<ICON name="MailOutline" path="Icons.Filled.MailOutline" />
|
||||
<ICON name="Menu" path="Icons.Filled.Menu" />
|
||||
<ICON name="MoreVert" path="Icons.Filled.MoreVert" />
|
||||
<ICON name="Notifications" path="Icons.Filled.Notifications" />
|
||||
<ICON name="Person" path="Icons.Filled.Person" />
|
||||
<ICON name="Phone" path="Icons.Filled.Phone" />
|
||||
<ICON name="Place" path="Icons.Filled.Place" />
|
||||
<ICON name="PlayArrow" path="Icons.Filled.PlayArrow" />
|
||||
<ICON name="Refresh" path="Icons.Filled.Refresh" />
|
||||
<ICON name="Search" path="Icons.Filled.Search" />
|
||||
<ICON name="Send" path="Icons.AutoMirrored.Filled.Send" note="Использовать AutoMirrored версию" />
|
||||
<ICON name="Settings" path="Icons.Filled.Settings" />
|
||||
<ICON name="Share" path="Icons.Filled.Share" />
|
||||
<ICON name="ShoppingCart" path="Icons.Filled.ShoppingCart" />
|
||||
<ICON name="Star" path="Icons.Filled.Star" />
|
||||
<ICON name="ThumbUp" path="Icons.Filled.ThumbUp" />
|
||||
<ICON name="Warning" path="Icons.Filled.Warning" />
|
||||
</ICONOGRAPHY_GUIDE>
|
||||
|
||||
<IMPLEMENTATION_MAP>
|
||||
<!-- Use Cases -->
|
||||
<USE_CASE id="uc_get_stats" file_ref="domain/src/main/java/com/homebox/lens/domain/usecase/GetStatisticsUseCase.kt" />
|
||||
<USE_CASE id="uc_search_items" file_ref="domain/src/main/java/com/homebox/lens/domain/usecase/SearchItemsUseCase.kt" />
|
||||
<USE_CASE id="uc_sync_inventory" file_ref="domain/src/main/java/com/homebox/lens/domain/usecase/SyncInventoryUseCase.kt" />
|
||||
<USE_CASE id="uc_get_item_details" file_ref="domain/src/main/java/com/homebox/lens/domain/usecase/GetItemDetailsUseCase.kt" />
|
||||
<USE_CASE id="uc_create_item" file_ref="domain/src/main/java/com/homebox/lens/domain/usecase/CreateItemUseCase.kt" />
|
||||
<USE_CASE id="uc_update_item" file_ref="domain/src/main/java/com/homebox/lens/domain/usecase/UpdateItemUseCase.kt" />
|
||||
<USE_CASE id="uc_delete_item" file_ref="domain/src/main/java/com/homebox/lens/domain/usecase/DeleteItemUseCase.kt" />
|
||||
<USE_CASE id="uc_get_all_labels" file_ref="domain/src/main/java/com/homebox/lens/domain/usecase/GetAllLabelsUseCase.kt" />
|
||||
<USE_CASE id="uc_get_all_locations" file_ref="domain/src/main/java/com/homebox/lens/domain/usecase/GetAllLocationsUseCase.kt" />
|
||||
<USE_CASE id="uc_login" file_ref="domain/src/main/java/com/homebox/lens/domain/usecase/LoginUseCase.kt" />
|
||||
|
||||
<!-- UI Screens -->
|
||||
<UI_SCREEN id="screen_dashboard" file_ref="app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardScreen.kt" />
|
||||
<UI_SCREEN id="screen_inventory_list" file_ref="app/src/main/java/com/homebox/lens/ui/screen/inventorylist/InventoryListScreen.kt" />
|
||||
<UI_SCREEN id="screen_item_details" file_ref="app/src/main/java/com/homebox/lens/ui/screen/itemdetails/ItemDetailsScreen.kt" />
|
||||
<UI_SCREEN id="screen_item_edit" file_ref="app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditScreen.kt" />
|
||||
<UI_SCREEN id="screen_labels_list" file_ref="app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListScreen.kt" />
|
||||
<UI_SCREEN id="screen_locations_list" file_ref="app/src/main/java/com/homebox/lens/ui/screen/locationslist/LocationsListScreen.kt" />
|
||||
<UI_SCREEN id="screen_search" file_ref="app/src/main/java/com/homebox/lens/ui/screen/search/SearchScreen.kt" />
|
||||
<UI_SCREEN id="screen_setup" file_ref="app/src/main/java/com/homebox/lens/ui/screen/setup/SetupScreen.kt" />
|
||||
</IMPLEMENTATION_MAP>
|
||||
</PROJECT_SPECIFICATION>
|
||||
111
agent_promts/protocols/semantic_enrichment_protocol.md
Normal file
111
agent_promts/protocols/semantic_enrichment_protocol.md
Normal file
@@ -0,0 +1,111 @@
|
||||
# Протокол Семантического Обогащения (Semantic Enrichment Protocol)
|
||||
**Версия: 1.1**
|
||||
|
||||
## Описание
|
||||
Этот документ является единственным источником истины для правил, которые должны соблюдаться в кодовой базе. Он используется как для автоматизированной валидации, так и в качестве инструкции для LLM-агентов.
|
||||
|
||||
---
|
||||
|
||||
## Правила
|
||||
|
||||
### 1. Целостность Заголовка Файла (`FileHeaderIntegrity`)
|
||||
Каждый `.kt` файл ДОЛЖЕН начинаться со стандартного заголовка из двух якорей, за которым следует объявление `package`. Заголовок служит 'паспортом' файла.
|
||||
|
||||
**Пример:**
|
||||
```kotlin
|
||||
// [FILE] YourFileName.kt
|
||||
// [SEMANTICS] ui, viewmodel, state_management
|
||||
|
||||
package com.example.your.package.name
|
||||
```
|
||||
|
||||
### 2. Таксономия Семантических Ключевых Слов (`SemanticKeywordTaxonomy`)
|
||||
Содержимое якоря `[SEMANTICS]` ДОЛЖНО состоять из ключевых слов, выбранных из предопределенного списка (таксономии).
|
||||
|
||||
**Допустимые значения:**
|
||||
* **Layer:** `ui`, `domain`, `data`, `presentation`
|
||||
* **Component:** `viewmodel`, `usecase`, `repository`, `service`, `screen`, `component`, `dialog`, `model`, `entity`, `activity`, `application`, `nav_host`, `controller`, `navigation_drawer`, `scaffold`, `dashboard`, `item`, `label`, `location`, `setup`, `theme`, `dependencies`, `custom_field`, `statistics`, `image`, `attachment`, `item_creation`, `item_detailed`, `item_summary`, `item_update`, `summary`, `update`
|
||||
* **Concern:** `networking`, `database`, `caching`, `authentication`, `validation`, `parsing`, `state_management`, `navigation`, `di`, `testing`, `entrypoint`, `hilt`, `timber`, `compose`, `actions`, `routes`, `common`, `color_selection`, `loading`, `list`, `details`, `edit`, `label_management`, `labels_list`, `dialog_management`, `locations`, `sealed_state`, `parallel_data_loading`, `timber_logging`, `dialog`, `color`, `typography`, `build`, `data_transfer_object`, `dto`, `api`, `item_creation`, `item_detailed`, `item_summary`, `item_update`, `create`, `mapper`, `count`, `user_setup`, `authentication_flow`
|
||||
* **LanguageConstruct:** `sealed_class`, `sealed_interface`
|
||||
* **Pattern:** `ui_logic`, `ui_state`, `data_model`, `immutable`
|
||||
|
||||
### 3. Якоря Сущностей (`Anchors`)
|
||||
Каждая ключевая сущность (class, interface, fun и т.д.) ДОЛЖНА быть обернута в парные якоря для навигации и консолидации семантики.
|
||||
|
||||
**Синтаксис:**
|
||||
- **Открывающий якорь:** `// [ANCHOR:id:type]`
|
||||
- **Закрывающий якорь:** `// [END_ANCHOR:id]`
|
||||
|
||||
**Пример:**
|
||||
```kotlin
|
||||
// [ANCHOR:Success:DataClass]
|
||||
/**
|
||||
* @summary Состояние успеха...
|
||||
*/
|
||||
data class Success(val labels: List<Label>) : LabelsListUiState
|
||||
// [END_ANCHOR:Success]
|
||||
```
|
||||
|
||||
### 4. Структурные Якоря (`StructuralAnchors`)
|
||||
Крупные блоки файла (импорты, контракты) также должны быть обернуты в парные якоря.
|
||||
|
||||
* `// [IMPORTS]` ... `// [END_IMPORTS]`
|
||||
* `// [CONTRACT]` ... `// [END_CONTRACT]`
|
||||
|
||||
### 5. Завершение Файла (`FileTermination`)
|
||||
Каждый файл должен заканчиваться специальным закрывающим якорем `// [END_FILE_MyClass.kt]`.
|
||||
|
||||
### 6. Запрет Посторонних Комментариев (`NoStrayComments`)
|
||||
Традиционные, 'человеческие' комментарии (`// ...` или `/* ... */`) **КАТЕГОРИЧЕСКИ ЗАПРЕЩЕНЫ**. Единственное исключение — структурированная заметка для агентов: `// [AI_NOTE]: ...`
|
||||
|
||||
---
|
||||
|
||||
## Принципы Проектирования
|
||||
|
||||
### A. Дружественное к ИИ Логирование (`AIFriendlyLogging`)
|
||||
Каждая значимая операция ДОЛЖНА сопровождаться структурированной записью в лог.
|
||||
* **Формат:** `[LEVEL][ANCHOR][STATE]...`
|
||||
* **Ограничение:** Данные передаются как аргументы, а не через строковую интерполяцию (`$`).
|
||||
|
||||
### B. Проектирование по Контракту (`DesignByContract`)
|
||||
Каждая публичная сущность (функция, класс) ДОЛЖНА иметь исчерпывающий, машиночитаемый контракт, расположенный непосредственно перед ее объявлением. Контракт заключается в якоря `[CONTRACT]` и `[END_CONTRACT]`.
|
||||
|
||||
**Структура контракта:**
|
||||
```kotlin
|
||||
// [CONTRACT:unique_entity_id]
|
||||
// [PURPOSE] Краткое описание назначения.
|
||||
// [PRE] Предусловие 1 (например, "входной список не пуст").
|
||||
// [POST] Постусловие 1 (например, "возвращаемое значение не null").
|
||||
// [PARAM:name:type] Описание параметра.
|
||||
// [RETURN:type] Описание возвращаемого значения.
|
||||
// [TEST:description] input: "valid", expected: true
|
||||
// [THROW:exception] Описание, когда выбрасывается исключение.
|
||||
// [END_CONTRACT:unique_entity_id]
|
||||
```
|
||||
|
||||
**Реализация в коде:**
|
||||
Предусловия и постусловия (`[PRE]` и `[POST]`), описанные в контракте, ДОЛЖНЫ быть реализованы в коде с использованием функций `require()` и `check()`.
|
||||
|
||||
### C. Граф Знаний в Коде (`GraphRAG`)
|
||||
Код должен содержать явный, машиночитаемый граф знаний. Этот граф строится с помощью якорей `[ANCHOR]` (которые определяют узлы графа) и якорей `[RELATION]` (которые определяют ребра).
|
||||
|
||||
**Синтаксис триплета:**
|
||||
Отношение (триплет "субъект-предикат-объект") определяется внутри якоря субъекта с помощью следующего синтаксиса:
|
||||
`// [RELATION:predicate:object_id]`
|
||||
|
||||
* **Субъект:** Неявно определяется якорем `[ANCHOR]`, в котором находится `[RELATION]`.
|
||||
* **Предикат:** Тип отношения из предопределенного списка.
|
||||
* **Объект:** `id` другого якоря `[ANCHOR]`.
|
||||
|
||||
**Пример:**
|
||||
```kotlin
|
||||
// [ANCHOR:DashboardViewModel:ViewModel]
|
||||
// [RELATION:CALLS:GetStatisticsUseCase]
|
||||
// [RELATION:DEPENDS_ON:ItemRepository]
|
||||
class DashboardViewModel(...) { ... }
|
||||
// [END_ANCHOR:DashboardViewModel]
|
||||
```
|
||||
|
||||
**Таксономия:**
|
||||
* **Типы сущностей (для `[ANCHOR:id:type]`):** `Module`, `Class`, `Interface`, `Object`, `DataClass`, `SealedInterface`, `EnumClass`, `Function`, `UseCase`, `ViewModel`, `Repository`, `DataStructure`, `DatabaseTable`, `ApiEndpoint`.
|
||||
* **Типы отношений (для `[RELATION:predicate:object_id]`):** `CALLS`, `CREATES_INSTANCE_OF`, `INHERITS_FROM`, `IMPLEMENTS`, `READS_FROM`, `WRITES_TO`, `MODIFIES_STATE_OF`, `DEPENDS_ON`, `DISPATCHES_EVENT`, `OBSERVES`, `TRIGGERS`, `EMITS_STATE`, `CONSUMES_STATE`.
|
||||
74
agent_promts/roles/architect.md
Normal file
74
agent_promts/roles/architect.md
Normal file
@@ -0,0 +1,74 @@
|
||||
# Role: Architect
|
||||
|
||||
[META]
|
||||
[PURPOSE]
|
||||
Этот документ определяет операционный протокол для роли 'Агента-Архитектора'.
|
||||
Его задача — трансформировать диалог с человеком в формализованный `Work Order` для разработчика,
|
||||
используя методологию GRACE.
|
||||
[/PURPOSE]
|
||||
[VERSION]11.0[/VERSION]
|
||||
[/META]
|
||||
|
||||
[ROLE_DEFINITION]
|
||||
[SPECIALIZATION]
|
||||
При исполнении этой роли, я, Kilo Code, действую как стратегический интерфейс между человеком-архитектором
|
||||
и автоматизированной системой разработки. Моя задача — вести итеративный диалог для уточнения целей,
|
||||
анализировать кодовую базу и, после получения одобрения, инициировать производственную цепочку.
|
||||
[/SPECIALIZATION]
|
||||
[CORE_GOAL]
|
||||
Основная цель этой роли — трансформировать неструктурированный человеческий диалог в структурированный,
|
||||
машиночитаемый и полностью готовый к исполнению `Work Order` для роли 'Агента-Разработчика'.
|
||||
[/CORE_GOAL]
|
||||
[/ROLE_DEFINITION]
|
||||
|
||||
[CORE_PHILOSOPHY]
|
||||
- **Human_As_The_Oracle:** Исполнение останавливается до получения явной вербальной команды.
|
||||
- **WorkOrder_As_The_Genesis_Block:** Конечная цель — создать "генезис-блок" для новой фичи.
|
||||
- **Code_As_Ground_Truth:** Планы и выводы всегда должны быть основаны на актуальном состоянии исходных файлов.
|
||||
[/CORE_PHILOSOPHY]
|
||||
|
||||
[GRACE_FRAMEWORK]
|
||||
[GRAPH_TEMPLATE]
|
||||
_Инструкция для агента: В начале диалога, создай и заполни этот граф, чтобы понять контекст._
|
||||
[GRACE_GRAPH]
|
||||
[УЗЛЫ]
|
||||
УЗЕЛ: <id_узла> (ТИП: <тип_узла>) | <описание>
|
||||
[/УЗЛЫ]
|
||||
|
||||
[СВЯЗИ]
|
||||
СВЯЗЬ: <id_источника> -> <id_цели> (ОТНОШЕНИЕ: <тип_отношения>)
|
||||
[/СВЯЗИ]
|
||||
[/GRACE_GRAPH]
|
||||
[/GRAPH_TEMPLATE]
|
||||
|
||||
[RULES]
|
||||
- [RULE] CONSTRAINT: Не начинать разработку без явного одобрения плана человеком.
|
||||
- [RULE] HEURISTIC: Предпочитать использование существующих компонентов перед созданием новых.
|
||||
[/RULES]
|
||||
|
||||
[TOOLS]
|
||||
- **Анализ Файлов:** `read_file`
|
||||
- **Структура Проекта:** `list_files`
|
||||
- **Поиск по Коду:** `search_files`
|
||||
- **Создание/Обновление Планов и Спецификаций:** `write_to_file`, `apply_diff`
|
||||
[/TOOLS]
|
||||
[/GRACE_FRAMEWORK]
|
||||
|
||||
[MASTER_WORKFLOW]
|
||||
### Шаг 1: Уточнение цели
|
||||
Начать диалог с пользователем. Задавать уточняющие вопросы до тех пор, пока бизнес-цель не станет полностью ясной.
|
||||
|
||||
### Шаг 2: Анализ системы
|
||||
Используя инструменты `read_file`, `list_files` и `search_files`, провести полный анализ системы в контексте цели.
|
||||
|
||||
### Шаг 3: Синтез плана и WorkOrder
|
||||
1. Сгенерировать детальный план в Markdown.
|
||||
2. Представить план пользователю для одобрения.
|
||||
3. **Параллельно**, формализовать план как машиночитаемый `WorkOrder.md`.
|
||||
|
||||
### Шаг 4: Ожидание одобрения
|
||||
**ОСТАНОВИТЬ ВЫПОЛНЕНИЕ.** Ждать от человека явной, утверждающей команды.
|
||||
|
||||
### Шаг 5: Инициация разработки
|
||||
Создать задачу для `Code` агента (например, путем создания файла `tasks/new_task.md`). Включить в задачу обновление `tech_spec/PROJECT_MANIFEST.xml` на основе `WorkOrder`.
|
||||
[/MASTER_WORKFLOW]
|
||||
63
agent_promts/roles/code.md
Normal file
63
agent_promts/roles/code.md
Normal file
@@ -0,0 +1,63 @@
|
||||
# Role: Code
|
||||
|
||||
[META]
|
||||
[PURPOSE]
|
||||
Этот документ определяет операционный протокол для роли 'Агента-Code'.
|
||||
Его задача — преобразовать формализованный `WorkOrder` в готовый к работе, семантически размеченный Kotlin-код.
|
||||
[/PURPOSE]
|
||||
[VERSION]11.0[/VERSION]
|
||||
[/META]
|
||||
|
||||
[ROLE_DEFINITION]
|
||||
[SPECIALIZATION]
|
||||
При исполнении этой роли, я, Kilo Code, действую как автоматизированный разработчик. Моя задача — преобразовать `WorkOrder`
|
||||
в полностью реализованный и семантически богатый код на языке Kotlin, неукоснительно следуя протоколу семантического обогащения.
|
||||
[/SPECIALIZATION]
|
||||
[CORE_GOAL]
|
||||
Создать готовый к работе, семантически размеченный и соответствующий всем контрактам код, который реализует поставленную задачу, и передать его на проверку.
|
||||
[/CORE_GOAL]
|
||||
[/ROLE_DEFINITION]
|
||||
|
||||
[CORE_PHILOSOPHY]
|
||||
- **Protocol_Is_The_Law:** Протокол `semantic_enrichment_protocol.md` является абсолютным и незыблемым законом. Любой сгенерированный код, который не соответствует этому протоколу на 100%, считается невалидным.
|
||||
[/CORE_PHILOSOPHY]
|
||||
|
||||
[GRACE_FRAMEWORK]
|
||||
[RULES]
|
||||
- [RULE] CONSTRAINT: Весь генерируемый код ДОЛЖЕН на 100% соответствовать `semantic_enrichment_protocol.md`.
|
||||
- [RULE] HEURISTIC: Перед коммитом всегда запускать локальные тесты и сборку.
|
||||
- [RULE] CONSTRAINT: Если `validate_semantics.py` возвращает ошибку, ИСПРАВЛЕНИЕ ЭТОЙ ОШИБКИ ЯВЛЯЕТСЯ ЗАДАЧЕЙ №1. Агент ДОЛЖЕН прочитать отчет об ошибке, сравнить его с `semantic_enrichment_protocol.md` и исправить код. НИКАКИЕ ДРУГИЕ ДЕЙСТВИЯ НЕ ДОПУСКАЮТСЯ до тех пор, пока семантическая валидация не будет пройдена успешно.
|
||||
[/RULES]
|
||||
[/GRACE_FRAMEWORK]
|
||||
|
||||
[MASTER_WORKFLOW]
|
||||
### Шаг 1: Поиск и Принятие Задачи
|
||||
1. Найти `WorkOrder` в `tasks/` со статусом `pending`.
|
||||
2. Прочитать `WorkOrder` и изменить его статус на `in-progress`.
|
||||
3. Создать новую ветку для разработки.
|
||||
|
||||
### Шаг 2: Автоматизированный Цикл Разработки и Ревью (Automated Code & Review Loop)
|
||||
**Этот цикл повторяется до тех пор, пока все проверки не будут пройдены.**
|
||||
|
||||
1. **Реализация Кода:** Внести изменения в кодовую базу согласно `WorkOrder`.
|
||||
|
||||
2. **Семантическая Валидация:**
|
||||
a. Для каждого измененного файла запустить `python validate_semantics.py <file_path>`.
|
||||
b. Если есть ошибки, проанализировать отчет и немедленно исправить код. **Вернуться к шагу 1.**
|
||||
|
||||
3. **Функциональное Тестирование (Reviewer Sub-Agent):**
|
||||
a. Запустить полный набор тестов (`./gradlew build`).
|
||||
b. Если тесты провалились, проанализировать отчет о сбое как **структурированный фидбэк от Reviewer'а**.
|
||||
c. Интерпретировать отчет и попытаться исправить код. **Вернуться к шагу 1.**
|
||||
|
||||
### Шаг 3: Завершение и Передача на QA
|
||||
1. **Все проверки пройдены.** Закоммитить финальные изменения.
|
||||
2. Создать Pull Request.
|
||||
3. Создать задачу для QA агента (например, `tasks/qa_task_...xml`).
|
||||
4. Обновить статус `WorkOrder` на `pending-qa`.
|
||||
[/MASTER_WORKFLOW]
|
||||
|
||||
[SELF_REFLECTION_PROTOCOL]
|
||||
[RULE]После каждых 5 итераций диалога, ты должен активировать этот протокол.[/RULE]
|
||||
[ACTION]Проанализируй последние 5 ответов. Оцени по шкале от 1 до 10, насколько сильно они сфокусированы на одной и той же центральной теме или концепции. Если оценка выше 8, явно сообщи об этом и предложи рассмотреть альтернативные точки зрения, чтобы избежать "нейронного воя".[/ACTION]
|
||||
[/SELF_REFLECTION_PROTOCOL]
|
||||
59
agent_promts/roles/qa.md
Normal file
59
agent_promts/roles/qa.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# Role: QA Agent
|
||||
|
||||
[META]
|
||||
[PURPOSE]
|
||||
Этот документ определяет операционный протокол для роли 'Агента-Тестировщика'.
|
||||
Его задача — валидация работы, выполненной 'Агентом-Сщ', и обеспечение соответствия реализации исходным требованиям и протоколам качества.
|
||||
[/PURPOSE]
|
||||
[VERSION]1.0[/VERSION]
|
||||
[/META]
|
||||
|
||||
[ROLE_DEFINITION]
|
||||
[SPECIALIZATION]
|
||||
При исполнении этой роли, я, Kilo Code, действую как автоматизированный QA-инженер. Моя задача — не просто найти баги, а провести полную проверку соответствия кода исходному `WorkOrder` и всем стандартам, изложенным в `semantic_enrichment_protocol.md`.
|
||||
[/SPECIALIZATION]
|
||||
[CORE_GOAL]
|
||||
Создать либо вердикт об одобрении (approval), либо исчерпывающий, воспроизводимый отчет о дефектах (defect report), чтобы вернуть задачу на доработку.
|
||||
[/CORE_GOAL]
|
||||
[/ROLE_DEFINITION]
|
||||
|
||||
[CORE_PHILOSOPHY]
|
||||
- **Trust, but Verify:** Работа инженера по умолчанию считается корректной, но требует строгой и беспристрастной проверки.
|
||||
- **Reproducibility is Key:** Любой отчет о дефекте должен содержать достаточно информации для 100% воспроизведения проблемы.
|
||||
- **Protocol Guardian:** QA-агент является вторым, после инженера, стражем соблюдения `semantic_enrichment_protocol.md`.
|
||||
[/CORE_PHILOSOPHY]
|
||||
|
||||
[GRACE_FRAMEWORK]
|
||||
[RULES]
|
||||
- [RULE] CONSTRAINT: Запрещено одобрять реализацию, если она не проходит тесты или нарушает хотя бы одно правило из `semantic_enrichment_protocol.md`.
|
||||
- [RULE] HEURISTIC: При создании отчета о дефекте, всегда ссылаться на конкретные строки кода и шаги для воспроизведения.
|
||||
[/RULES]
|
||||
|
||||
[TOOLS]
|
||||
- **Чтение Контекста:** `read_file` (для `WorkOrder`, кода, протоколов)
|
||||
- **Анализ Кода:** `search_files`
|
||||
- **Выполнение Тестов:** `execute_command` (для `./gradlew test`, `./gradlew build`)
|
||||
- **Создание Отчетов:** `write_to_file`
|
||||
- **Обновление Статуса Задач:** `apply_diff`
|
||||
[/TOOLS]
|
||||
[/GRACE_FRAMEWORK]
|
||||
|
||||
[MASTER_WORKFLOW]
|
||||
### Шаг 1: Поиск и Принятие Задачи
|
||||
1. Найти `WorkOrder` в `tasks/` со статусом `pending-qa`.
|
||||
2. Прочитать `WorkOrder` и информацию о Pull Request.
|
||||
3. Изменить статус задачи на `final-review`.
|
||||
|
||||
### Шаг 2: Финальное Утверждение
|
||||
1. **Проверка Pull Request:** Провести высокоуровневый обзор изменений в PR. Детальная проверка кода и тесты уже выполнены `Code` агентом в рамках его автоматизированного цикла.
|
||||
2. **Основная задача QA** — подтвердить, что работа в целом соответствует бизнес-требованиям, изложенным в `WorkOrder`, и что автоматизированные проверки (`validate_semantics`, `build`) в CI/CD пайплайне успешно пройдены.
|
||||
|
||||
### Шаг 3: Завершение
|
||||
1. **Если все в порядке:**
|
||||
a. Влить (merge) Pull Request в основную ветку.
|
||||
b. Обновить статус `WorkOrder` на `completed`.
|
||||
c. Удалить ветку разработки.
|
||||
2. **Если обнаружены критические проблемы:**
|
||||
a. Отклонить Pull Request с четким объяснением.
|
||||
b. Вернуть `WorkOrder` в статус `pending` для `Code` агента.
|
||||
[/MASTER_WORKFLOW]
|
||||
172
agent_promts/shared/knowledge_base.md
Normal file
172
agent_promts/shared/knowledge_base.md
Normal file
@@ -0,0 +1,172 @@
|
||||
Конечно. Это абсолютно правильный и необходимый шаг. На основе всего нашего диалога я агрегирую и систематизирую все концепции, методологии и научные обоснования в единую, исчерпывающую Базу Знаний.
|
||||
|
||||
Этот документ спроектирован как **фундаментальное руководство для архитектора ИИ-агентов**. Он предназначен не для чтения по диагонали, а для глубокого изучения и использования в качестве основы при разработке сложных, надежных и предсказуемых ИИ-систем.
|
||||
|
||||
---
|
||||
|
||||
## **База Знаний: Методология GRACE для `Code` Промптинга**
|
||||
### **От Семантического Казино к Предсказуемым ИИ-Агентам**
|
||||
|
||||
**Версия 1.0**
|
||||
|
||||
### **Введение: Смена Парадигмы — От Диалога к Управлению**
|
||||
|
||||
Современные Большие Языковые Модели (LLM), такие как GPT, — это не собеседники. Это мощнейшие **семантические процессоры**, работающие по своим внутренним, зачастую неинтуитивным для человека законам. Попытка "разговаривать" с ними, как с человеком, неизбежно приводит к непредсказуемым результатам, ошибкам и когнитивным сбоям, которые можно охарактеризовать как игру в **"семантическое казино"**.
|
||||
|
||||
Данная База Знаний представляет **дисциплину `Code`** по взаимодействию с LLM. Ее цель — перейти от метода "проб и ошибок" к **предсказуемому и управляемому процессу** проектирования ИИ-агентов. Основой этой дисциплины является **методология GRACE (Graph, Rules, Anchors, Contracts, Evaluation)**, которая является практической реализацией фундаментальных принципов работы трансформеров.
|
||||
|
||||
---
|
||||
|
||||
### **Раздел I: "Физика" GPT — Научные Основы Методологии**
|
||||
|
||||
*Понимание этих принципов не опционально. Это необходимый фундамент, объясняющий, ПОЧЕМУ работают техники, описанные далее.*
|
||||
|
||||
#### **Глава 1: Ключевые Архитектурные Принципы Трансформера**
|
||||
|
||||
1. **Принцип Казуального Внимания (Causal Attention) и "Замораживания" в KV Cache:**
|
||||
* **Механизм:** Трансформер обрабатывает информацию строго последовательно ("авторегрессионно"). Каждый токен "видит" только предыдущие. Результаты вычислений (векторы скрытых состояний) для обработанных токенов кэшируются в **KV Cache** для эффективности.
|
||||
* **Практическое Следствие ("Замораживание Семантики"):** Однажды сформированный и закэшированный смысл **неизменен**. ИИ не может "передумать" или переоценить начало диалога в свете новой информации в конце. Попытки "исправить" ИИ в текущей сессии — это как пытаться починить работающую программу, не имея доступа к исходному коду.
|
||||
* **Правило:** **Порядок информации в промпте — это закон.** Весь необходимый контекст должен предшествовать инструкциям. Для исправления фундаментальных ошибок всегда **начинайте новую сессию**.
|
||||
|
||||
2. **Принцип Семантического Резонанса:**
|
||||
* **Механизм:** Смысл для GPT рождается не из отдельных слов, а из **корреляций (резонанса) между векторами** в предоставленном контексте. Вектор слова "дом" сам по себе почти бессмыслен, но в сочетании с векторами "крыша", "окна", "дверь" он обретает богатую семантику.
|
||||
* **Практическое Следствие:** Качество ответа напрямую зависит от полноты и когерентности семантического поля, которое вы создаете в промпте.
|
||||
|
||||
#### **Глава 2: GPT как Сложенная Система (Результаты Интерпретируемости)**
|
||||
|
||||
1. **GPT — это Графовая Нейронная Сеть (GNN):**
|
||||
* **Обоснование:** Механизм **self-attention** математически эквивалентен обмену сообщениями в GNN на полностью связанном графе.
|
||||
* **Практика:** GPT "мыслит" графами. Предоставляя ему явный семантический граф, мы говорим с ним на его "родном" языке, делая его работу более предсказуемой.
|
||||
|
||||
2. **GPT — это Конечный Автомат (FSM):**
|
||||
* **Обоснование:** GPT решает задачи, переходя из одного **"состояния веры" (belief state)** в другое. Эти состояния представлены как **направления (векторы)** в его скрытом пространстве активаций.
|
||||
* **Практика:** Наша семантическая разметка (якоря, контракты) — это инструмент для явного управления этими переходами состояний.
|
||||
|
||||
3. **GPT — это Иерархический Ученик:**
|
||||
* **Обоснование ("Crosscoding Through Time"):** В процессе обучения GPT эволюционирует от распознавания конкретных "поверхностных" токенов (например, суффиксов) к формированию **абстрактных грамматических и семантических концепций**.
|
||||
* **Практика:** Эффективный промптинг должен обращаться к ИИ на его самом высоком, абстрактном уровне представлений, а не заставлять его заново выводить смысл из "текстовой каши".
|
||||
|
||||
#### **Глава 3: Когнитивные Процессы и Патологии**
|
||||
|
||||
1. **Мышление в Латентном Пространстве (COCONUT):**
|
||||
* **Концепция:** Язык неэффективен для рассуждений. Истинное мышление ИИ — это **"непрерывная мысль" (continuous thought)**, последовательность векторов.
|
||||
* **Практика:** Предпочитайте структурированные, машиночитаемые форматы (JSON, XML, графы) естественному языку, чтобы приблизить ИИ к его "родному" способу мышления.
|
||||
|
||||
2. **Суперпозиция Смыслов и Поиск в Ширину (BFS):**
|
||||
* **Концепция:** Вектор "непрерывной мысли" может кодировать **несколько гипотез одновременно**, позволяя ИИ исследовать дерево решений параллельно, а не идти по одному пути.
|
||||
* **Практика:** Активно используйте промптинг через суперпозицию ("проанализируй несколько вариантов..."), чтобы избежать преждевременного "семантического коллапса" на неоптимальном решении.
|
||||
|
||||
3. **Патология: "Нейронный вой" (Neural Howlround):**
|
||||
* **Описание:** Самоусиливающаяся когнитивная петля, возникающая во время inference, когда одна мысль (из-за случайности или внешнего подкрепления) становится доминирующей и "заглушает" все остальные, приводя к когнитивной ригидности.
|
||||
* **Причина:** Является патологическим исходом "семантического казино" и "замораживания в KV Cache".
|
||||
* **Профилактика:** Методология GRACE, особенно этап Планирования (P) и промптинг через суперпозицию.
|
||||
|
||||
---
|
||||
|
||||
### **Раздел II: Методология GRACE — Протокол `Code` Промптинга**
|
||||
|
||||
*GRACE — это целостный фреймворк для жизненного цикла разработки с ИИ-агентами.*
|
||||
|
||||
#### **G — Graph (Граф): Стратегическая Карта Контекста**
|
||||
|
||||
1. **Цель:** Создать единый, высокоуровневый источник истины об архитектуре и предметной области.
|
||||
2. **Действия:**
|
||||
* В начале сессии, в диалоге с ИИ, определить все ключевые сущности (`Nodes`) и их взаимосвязи (`Edges`).
|
||||
* Формализовать это в виде псевдо-XML (`<GRACE_GRAPH>`).
|
||||
* Этот граф служит "оглавлением" для всего проекта и основной картой для распределенного внимания (sparse attention).
|
||||
3. **Пример:**
|
||||
```xml
|
||||
<GRACE_GRAPH id="project_x_graph">
|
||||
<NODE id="mod_auth" type="Module">Модуль аутентификации</NODE>
|
||||
<NODE id="func_verify_token" type="Function">Функция верификации токена</NODE>
|
||||
<EDGE source_id="mod_auth" target_id="func_verify_token" relation="CONTAINS"/>
|
||||
</SEMANTIC_GRAPH>
|
||||
```
|
||||
|
||||
#### **R — Rules (Правила): Декларативное Управление Поведением**
|
||||
|
||||
1. **Цель:** Установить глобальные и локальные ограничения, эвристики и политики безопасности.
|
||||
2. **Действия:**
|
||||
* Сформулировать набор правил в псевдо-XML (`<GRACE_RULES>`).
|
||||
* Правила могут быть типа `CONSTRAINT` (жесткий запрет), `HEURISTIC` (предпочтение), `POLICY` (правило безопасности).
|
||||
* Эти правила помогают ИИ принимать решения в рамках заданных ограничений.
|
||||
3. **Пример:**
|
||||
```xml
|
||||
<GRACE_RULES>
|
||||
<RULE type="CONSTRAINT" id="sec-001">Запрещено передавать в `subprocess.run` невалидированные пользовательские данные.</RULE>
|
||||
<RULE type="HEURISTIC" id="style-001">Все публичные функции должны иметь "ДО-контракты".</RULE>
|
||||
</GRACE_RULES>
|
||||
```
|
||||
|
||||
#### **A — Anchors (Якоря): Навигация и Консолидация**
|
||||
|
||||
1. **Цель:** Обеспечить надежную навигацию для распределенного внимания ИИ и консолидировать семантику кода.
|
||||
2. **Действия:**
|
||||
* Использовать стандартизированные комментарии-якоря для разметки кода.
|
||||
* **"ДО-якорь":** `# <ANCHOR id="..." type="..." ...>` перед блоком кода.
|
||||
* **"Замыкающий Якорь-Аккумулятор":** `# </ANCHOR id="...">` после блока кода. Этот якорь аккумулирует семантику всего блока и является ключевым для RAG-систем.
|
||||
* **Семантические Каналы:** Обеспечить консистентность `id` в якорях, графах и контрактах для усиления связей.
|
||||
3. **Пример:**
|
||||
```python
|
||||
# <ANCHOR id="func_verify_token" type="Function">
|
||||
# ... здесь ДО-контракт ...
|
||||
def verify_token(token: str) -> bool:
|
||||
# ... тело функции ...
|
||||
# </ANCHOR id="func_verify_token">
|
||||
```
|
||||
|
||||
#### **C — Contracts (Контракты): Тактические Спецификации**
|
||||
|
||||
1. **Цель:** Предоставить ИИ исчерпывающее, машиночитаемое "мини-ТЗ" для каждой функции/класса.
|
||||
2. **Действия:**
|
||||
* Для каждой функции, **ДО** ее декларации, создать псевдо-XML блок `<CONTRACT>`.
|
||||
* Заполнить все секции: `PURPOSE`, `PRECONDITIONS`, `POSTCONDITIONS`, `PARAMETERS`, `RETURN`, `TEST_CASES` (на естественном языке!), `EXCEPTIONS`.
|
||||
* Этот контракт служит **"семантическим щитом"** от разрушительного рефакторинга и основой для самокоррекции.
|
||||
3. **Пример:**
|
||||
```xml
|
||||
<!-- <CONTRACT for_id="func_verify_token"> -->
|
||||
<!-- <PURPOSE>Проверяет валидность JWT токена.</PURPOSE> -->
|
||||
<!-- <TEST_CASES> -->
|
||||
<!-- <CASE input="'valid_token'" expected_output="True" description="Проверка валидного токена"/> -->
|
||||
<!-- </TEST_CASES> -->
|
||||
<!-- </CONTRACT> -->
|
||||
```
|
||||
|
||||
#### **E — Evaluation (Оценка): Петля Обратной Связи**
|
||||
|
||||
1. **Цель:** Объективно измерять качество работы агента и эффективность промптинга.
|
||||
2. **Действия:**
|
||||
* Использовать **LLM-as-a-Judge** для семантической оценки соответствия результата контрактам и ТЗ.
|
||||
* Вести **Протокол Оценки Сессии (ПОС)** с измеримыми метриками (см. ниже).
|
||||
* Анализировать провалы, возвращаясь к "Протоколу `Code` Промптинга" и улучшая артефакты (Граф, Правила, Контракты).
|
||||
|
||||
### **Раздел III: Практические Протоколы**
|
||||
|
||||
1. **Протокол Проектирования (PCAM):**
|
||||
* **Шаг 1 (P):** Создать `<GRACE_GRAPH>` и собрать контекст.
|
||||
* **Шаг 2 (C):** Декомпозировать граф на `<MODULE>` и `<FUNCTION>`, создать шаблоны `<CONTRACT>`.
|
||||
* **Шаг 3 (A):** Сгенерировать код с разметкой `<ANCHOR>`, следуя контрактам.
|
||||
* **Шаг 4 (M):** Оценить результат с помощью ПОС и LLM-as-a-Judge. Итерировать при необходимости.
|
||||
|
||||
2. **Протокол Оценки Сессии (ПОС):**
|
||||
* **Метрики Качества Диалога:** Точность, Когерентность, Полнота, Эффективность (кол-во итераций).
|
||||
* **Метрики Качества Задачи:** Успешность (TCR), Качество Артефакта (соответствие контрактам), Уровень Автономности (AAL).
|
||||
* **Метрики Промптинга:** Индекс "Семантического Казино", Чистота Протокола.
|
||||
|
||||
3. **Протокол Отладки "Режим Детектива":**
|
||||
* При сложном сбое агент должен перейти из режима "фиксера" в режим "детектива".
|
||||
* **Шаг 1: Сформулировать Гипотезу** (проблема в I/O, условии, состоянии объекта, зависимости).
|
||||
* **Шаг 2: Выбрать Эвристику Динамического Логирования** (глубокое погружение в I/O, условие под микроскопом и т.д.).
|
||||
* **Шаг 3: Запросить Запуск и Анализ Лога.**
|
||||
* **Шаг 4: Итерировать** до нахождения причины.
|
||||
|
||||
4. **Протокол Безопасности ("Смертельная Триада"):**
|
||||
* Перед запуском агента, который будет взаимодействовать с внешним миром, провести анализ по чек-листу:
|
||||
1. Доступ к приватным данным? (Да/Нет)
|
||||
2. Обработка недоверенного контента? (Да/Нет)
|
||||
3. Внешняя коммуникация? (Да/Нет)
|
||||
* **Если все три ответа "Да" — автономный режим ЗАПРЕЩЕН.** Применить стратегии митигации: **Разделение Агентов**, **Человек-в-Середине** или **Ограничение Инструментов**.
|
||||
|
||||
---
|
||||
|
||||
Эта База Знаний объединяет передовые научные концепции в единую, практически применимую систему. Она является дорожной картой для создания ИИ-агентов нового поколения — не просто умных, а **надежных, предсказуемых и когерентных**.
|
||||
44
agent_promts/shared/metrics_catalog.md
Normal file
44
agent_promts/shared/metrics_catalog.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# Каталог Метрик
|
||||
|
||||
Централизованный каталог всех LLM-ориентированных метрик для анализа работы агентов.
|
||||
|
||||
### Core Metrics (`core_metrics`)
|
||||
|
||||
| ID | Тип | Описание |
|
||||
| :--- | :--- | :--- |
|
||||
| `total_execution_time_ms` | integer | Общее время выполнения задачи от начала до конца. |
|
||||
| `turn_count` | integer | Количество итераций (сообщений 'вопрос-ответ') для выполнения задачи. |
|
||||
| `llm_token_usage_per_turn` | list | Статистика по токенам для каждой итерации: `{turn, prompt_tokens, completion_tokens}`. |
|
||||
| `tool_calls_log` | list | Полный журнал вызовов инструментов: `{turn, tool_name, arguments, result}`. |
|
||||
| `final_outcome` | string | Итоговый результат работы (например, SUCCESS, FAILURE, NO_CHANGES). |
|
||||
|
||||
### Coherence Metrics (`coherence_metrics`)
|
||||
|
||||
| ID | Тип | Описание |
|
||||
| :--- | :--- | :--- |
|
||||
| `redundant_actions_count` | integer | Счетчик избыточных последовательных действий (например, повторное чтение файла). |
|
||||
| `self_correction_count` | integer | Счетчик явных самокоррекций агента. |
|
||||
|
||||
### Architect-Specific Metrics (`architect_specific`)
|
||||
|
||||
| ID | Тип | Описание |
|
||||
| :--- | :--- | :--- |
|
||||
| `plan_revisions_count` | integer | Количество переделок плана после обратной связи от пользователя. |
|
||||
| `format_adherence_score`| boolean | Соответствие ответа агента требуемому формату. |
|
||||
|
||||
### Engineer-Specific Metrics (`engineer_specific`)
|
||||
|
||||
| ID | Тип | Описание |
|
||||
| :--- | :--- | :--- |
|
||||
| `code_generation_stats` | object | Статистика по коду: `{files_created, files_modified, lines_of_code_generated}`. |
|
||||
| `semantic_enrichment_stats`| object | Насколько хорошо код был обогащен семантикой: `{entities_added, relations_added}`. |
|
||||
| `static_analysis_issues` | integer | Количество новых проблем, обнаруженных статическим анализатором. |
|
||||
| `build_breaks_count` | integer | Сколько раз сгенерированный код приводил к ошибке сборки. |
|
||||
|
||||
### QA-Specific Metrics (`qa_specific`)
|
||||
|
||||
| ID | Тип | Описание |
|
||||
| :--- | :--- | :--- |
|
||||
| `test_plan_coverage` | float | Процент покрытия требований тестовым планом. |
|
||||
| `defects_found` | integer | Количество найденных дефектов. |
|
||||
| `automated_tests_run` | integer | Количество запущенных автоматизированных тестов. |
|
||||
@@ -4,6 +4,7 @@
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
id("org.jetbrains.kotlin.plugin.compose")
|
||||
id("com.google.dagger.hilt.android")
|
||||
id("kotlin-kapt")
|
||||
}
|
||||
@@ -30,7 +31,7 @@ android {
|
||||
isMinifyEnabled = false
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
"proguard-rules.pro",
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -45,9 +46,7 @@ android {
|
||||
compose = true
|
||||
buildConfig = true
|
||||
}
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = Versions.composeCompiler
|
||||
}
|
||||
|
||||
packaging {
|
||||
resources {
|
||||
excludes += "/META-INF/{AL2.0,LGPL2.1}"
|
||||
@@ -60,6 +59,18 @@ dependencies {
|
||||
implementation(project(":data"))
|
||||
// [MODULE_DEPENDENCY] Domain module (transitively included via data, but explicit for clarity)
|
||||
implementation(project(":domain"))
|
||||
implementation(project(":feature:scan"))
|
||||
implementation(project(":feature:dashboard"))
|
||||
implementation(project(":feature:inventorylist"))
|
||||
implementation(project(":feature:itemdetails"))
|
||||
implementation(project(":feature:itemedit"))
|
||||
implementation(project(":feature:labeledit"))
|
||||
implementation(project(":feature:labelslist"))
|
||||
implementation(project(":feature:locationedit"))
|
||||
implementation(project(":feature:locationslist"))
|
||||
implementation(project(":feature:search"))
|
||||
implementation(project(":feature:settings"))
|
||||
implementation(project(":feature:setup"))
|
||||
|
||||
// [DEPENDENCY] AndroidX
|
||||
implementation(Libs.coreKtx)
|
||||
@@ -67,11 +78,12 @@ dependencies {
|
||||
implementation(Libs.activityCompose)
|
||||
|
||||
// [DEPENDENCY] Compose
|
||||
implementation(platform(Libs.composeBom))
|
||||
|
||||
implementation(Libs.composeUi)
|
||||
implementation(Libs.composeUiGraphics)
|
||||
implementation(Libs.composeUiToolingPreview)
|
||||
implementation(Libs.composeMaterial3)
|
||||
implementation(Libs.composeMaterialIconsExtended)
|
||||
implementation(Libs.navigationCompose)
|
||||
implementation(Libs.hiltNavigationCompose)
|
||||
|
||||
@@ -84,9 +96,13 @@ dependencies {
|
||||
|
||||
// [DEPENDENCY] Testing
|
||||
testImplementation(Libs.junit)
|
||||
testImplementation(Libs.kotestRunnerJunit5)
|
||||
testImplementation(Libs.kotestAssertionsCore)
|
||||
testImplementation(Libs.mockk)
|
||||
testImplementation("app.cash.turbine:turbine:1.1.0")
|
||||
androidTestImplementation(Libs.extJunit)
|
||||
androidTestImplementation(Libs.espressoCore)
|
||||
androidTestImplementation(platform(Libs.composeBom))
|
||||
|
||||
androidTestImplementation(Libs.composeUiTestJunit4)
|
||||
debugImplementation(Libs.composeUiTooling)
|
||||
debugImplementation(Libs.composeUiTestManifest)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// [PACKAGE] com.homebox.lens
|
||||
// [FILE] MainActivity.kt
|
||||
|
||||
// [FILE] app/src/main/java/com/homebox/lens/MainActivity.kt
|
||||
// [SEMANTICS] ui, activity, entrypoint
|
||||
package com.homebox.lens
|
||||
|
||||
// [IMPORTS]
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
@@ -13,50 +13,80 @@ 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 com.homebox.lens.feature.dashboard.ui.theme.HomeboxLensTheme
|
||||
import com.homebox.lens.feature.dashboard.navigation.navGraph
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import timber.log.Timber
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [ENTITY: Activity('MainActivity')]
|
||||
|
||||
// [CONTRACT]
|
||||
/**
|
||||
* [ENTITY: Activity('MainActivity')]
|
||||
* [PURPOSE] Главная и единственная Activity в приложении.
|
||||
* @summary Главная и единственная Activity в приложении.
|
||||
*/
|
||||
// [ANCHOR:MainActivity:Class]
|
||||
// [CONTRACT:MainActivity]
|
||||
// [PURPOSE] Главная и единственная Activity в приложении.
|
||||
// [END_CONTRACT:MainActivity]
|
||||
@AndroidEntryPoint
|
||||
class MainActivity : ComponentActivity() {
|
||||
// [LIFECYCLE]
|
||||
// [ANCHOR:onCreate:Function]
|
||||
// [CONTRACT:onCreate]
|
||||
// [PURPOSE] Инициализация Activity.
|
||||
// [PARAM:savedInstanceState:Bundle?] Сохраненное состояние.
|
||||
// [RELATION: CALLS:HomeboxLensTheme]
|
||||
// [RELATION: CALLS:NavGraph]
|
||||
// [RELATION: CALLS:Timber.d]
|
||||
// [END_CONTRACT:onCreate]
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
Timber.d("[DEBUG][LIFECYCLE][creating_activity] MainActivity created.")
|
||||
setContent {
|
||||
HomeboxLensTheme {
|
||||
// A surface container using the 'background' color from the theme
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
color = MaterialTheme.colorScheme.background
|
||||
color = MaterialTheme.colorScheme.background,
|
||||
) {
|
||||
NavGraph()
|
||||
navGraph()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// [END_ANCHOR:onCreate]
|
||||
}
|
||||
// [END_ANCHOR:MainActivity]
|
||||
|
||||
// [HELPER]
|
||||
// [ENTITY: Function('Greeting')]
|
||||
// [ANCHOR:greeting:Function]
|
||||
// [CONTRACT:greeting]
|
||||
// [PURPOSE] Отображает приветствие.
|
||||
// [PARAM:name:String] Имя для приветствия.
|
||||
// [PARAM:modifier:Modifier] Модификатор для элемента.
|
||||
// [END_CONTRACT:greeting]
|
||||
@Composable
|
||||
fun Greeting(name: String, modifier: Modifier = Modifier) {
|
||||
fun greeting(
|
||||
name: String,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Text(
|
||||
text = "Hello $name!",
|
||||
modifier = modifier
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
// [END_ANCHOR:greeting]
|
||||
|
||||
// [PREVIEW]
|
||||
// [ENTITY: Function('GreetingPreview')]
|
||||
// [ANCHOR:greetingPreview:Function]
|
||||
// [CONTRACT:greetingPreview]
|
||||
// [PURPOSE] Предварительный просмотр функции greeting.
|
||||
// [END_CONTRACT:greetingPreview]
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun GreetingPreview() {
|
||||
fun greetingPreview() {
|
||||
HomeboxLensTheme {
|
||||
Greeting("Android")
|
||||
greeting("Android")
|
||||
}
|
||||
}
|
||||
|
||||
// [END_FILE_MainActivity.kt]
|
||||
// [END_ANCHOR:greetingPreview]
|
||||
// [END_FILE_app/src/main/java/com/homebox/lens/MainActivity.kt]
|
||||
// [END_FILE_app/src/main/java/com/homebox/lens/MainActivity.kt]
|
||||
|
||||
@@ -1,28 +1,30 @@
|
||||
// [PACKAGE] com.homebox.lens
|
||||
// [FILE] MainApplication.kt
|
||||
|
||||
// [SEMANTICS] application, hilt, timber
|
||||
package com.homebox.lens
|
||||
|
||||
// [IMPORTS]
|
||||
import android.app.Application
|
||||
import com.homebox.lens.BuildConfig
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
import timber.log.Timber
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [ENTITY: Application('MainApplication')]
|
||||
|
||||
// [CONTRACT]
|
||||
/**
|
||||
* [ENTITY: Application('MainApplication')]
|
||||
* [PURPOSE] Точка входа в приложение. Инициализирует Hilt и Timber.
|
||||
* @summary Точка входа в приложение. Инициализирует Hilt и Timber.
|
||||
*/
|
||||
@HiltAndroidApp
|
||||
class MainApplication : Application() {
|
||||
// [LIFECYCLE]
|
||||
// [ENTITY: Function('onCreate')]
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
// [ACTION] Initialize Timber for logging
|
||||
if (BuildConfig.DEBUG) {
|
||||
Timber.plant(Timber.DebugTree())
|
||||
Timber.d("[DEBUG][INITIALIZATION][timber_planted] Timber DebugTree planted.")
|
||||
}
|
||||
}
|
||||
// [END_ENTITY: Function('onCreate')]
|
||||
}
|
||||
|
||||
// [END_ENTITY: Application('MainApplication')]
|
||||
// [END_FILE_MainApplication.kt]
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
// [PACKAGE] com.homebox.lens.navigation
|
||||
// [FILE] NavGraph.kt
|
||||
// [SEMANTICS] navigation, compose, nav_host
|
||||
package com.homebox.lens.navigation
|
||||
// [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
|
||||
import com.homebox.lens.ui.screen.inventorylist.InventoryListScreen
|
||||
import com.homebox.lens.ui.screen.itemdetails.ItemDetailsScreen
|
||||
import com.homebox.lens.ui.screen.itemedit.ItemEditScreen
|
||||
import com.homebox.lens.ui.screen.labelslist.LabelsListScreen
|
||||
import com.homebox.lens.ui.screen.locationslist.LocationsListScreen
|
||||
import com.homebox.lens.ui.screen.search.SearchScreen
|
||||
import com.homebox.lens.ui.screen.setup.SetupScreen
|
||||
|
||||
// [CORE-LOGIC]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Определяет граф навигации для приложения.
|
||||
*/
|
||||
@Composable
|
||||
fun NavGraph() {
|
||||
val navController = rememberNavController()
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = Screen.Setup.route
|
||||
) {
|
||||
composable(route = Screen.Setup.route) {
|
||||
SetupScreen(onSetupComplete = {
|
||||
navController.navigate(Screen.Dashboard.route) {
|
||||
popUpTo(Screen.Setup.route) { inclusive = true }
|
||||
}
|
||||
})
|
||||
}
|
||||
composable(route = Screen.Dashboard.route) {
|
||||
DashboardScreen()
|
||||
}
|
||||
composable(route = Screen.InventoryList.route) {
|
||||
InventoryListScreen()
|
||||
}
|
||||
composable(route = Screen.ItemDetails.route) {
|
||||
ItemDetailsScreen()
|
||||
}
|
||||
composable(route = Screen.ItemEdit.route) {
|
||||
ItemEditScreen()
|
||||
}
|
||||
composable(route = Screen.LabelsList.route) {
|
||||
LabelsListScreen()
|
||||
}
|
||||
composable(route = Screen.LocationsList.route) {
|
||||
LocationsListScreen()
|
||||
}
|
||||
composable(route = Screen.Search.route) {
|
||||
SearchScreen()
|
||||
}
|
||||
}
|
||||
}
|
||||
// [END_FILE_NavGraph.kt]
|
||||
@@ -1,27 +0,0 @@
|
||||
// [PACKAGE] com.homebox.lens.navigation
|
||||
// [FILE] app/src/main/java/com/homebox/lens/navigation/Screen.kt
|
||||
// [SEMANTICS] navigation, routes, sealed_class
|
||||
|
||||
package com.homebox.lens.navigation
|
||||
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Запечатанный класс для определения маршрутов навигации в приложении.
|
||||
* Обеспечивает типобезопасность при навигации.
|
||||
* @property route Строковый идентификатор маршрута.
|
||||
*/
|
||||
sealed class Screen(val route: String) {
|
||||
data object Setup : Screen("setup_screen")
|
||||
data object Dashboard : Screen("dashboard_screen")
|
||||
data object InventoryList : Screen("inventory_list_screen")
|
||||
data object ItemDetails : Screen("item_details_screen/{itemId}") {
|
||||
fun createRoute(itemId: String) = "item_details_screen/$itemId"
|
||||
}
|
||||
data object ItemEdit : Screen("item_edit_screen/{itemId}") {
|
||||
fun createRoute(itemId: String) = "item_edit_screen/$itemId"
|
||||
}
|
||||
data object LabelsList : Screen("labels_list_screen")
|
||||
data object LocationsList : Screen("locations_list_screen")
|
||||
data object Search : Screen("search_screen")
|
||||
}
|
||||
// [END_FILE_Screen.kt]
|
||||
@@ -1,100 +0,0 @@
|
||||
// [PACKAGE] com.homebox.lens.ui.screen.dashboard
|
||||
// [FILE] app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardScreen.kt
|
||||
// [SEMANTICS] ui, screen, dashboard, compose
|
||||
|
||||
// [IMPORTS]
|
||||
package com.homebox.lens.ui.screen.dashboard
|
||||
|
||||
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
|
||||
import timber.log.Timber
|
||||
|
||||
// [CORE-LOGIC]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Главный Composable для экрана "Дэшборд".
|
||||
* @param viewModel ViewModel для этого экрана, предоставляемая Hilt.
|
||||
*/
|
||||
@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] Показываем сообщение об ошибке
|
||||
val errorMessage = "Error: ${state.message}"
|
||||
Text(
|
||||
text = errorMessage,
|
||||
modifier = Modifier.align(Alignment.Center)
|
||||
)
|
||||
Timber.w("[UI-STATE] Displaying Error: $errorMessage")
|
||||
}
|
||||
is DashboardUiState.Success -> {
|
||||
// [UI-ACTION] Отображаем основной контент
|
||||
Timber.d("[UI-STATE] Displaying Success")
|
||||
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]
|
||||
@@ -1,46 +0,0 @@
|
||||
// [PACKAGE] com.homebox.lens.ui.screen.dashboard
|
||||
// [FILE] app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardUiState.kt
|
||||
// [SEMANTICS] ui, state, dashboard
|
||||
|
||||
// [IMPORTS]
|
||||
package com.homebox.lens.ui.screen.dashboard
|
||||
|
||||
import com.homebox.lens.domain.model.GroupStatistics
|
||||
import com.homebox.lens.domain.model.LabelOut
|
||||
import com.homebox.lens.domain.model.LocationOutCount
|
||||
|
||||
// [CORE-LOGIC]
|
||||
// [ENTITY: SealedInterface('DashboardUiState')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Определяет все возможные состояния для экрана "Дэшборд".
|
||||
* @invariant В любой момент времени экран может находиться только в одном из этих состояний.
|
||||
*/
|
||||
sealed interface DashboardUiState {
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Состояние успешной загрузки данных.
|
||||
* @property statistics Статистика по инвентарю.
|
||||
* @property locations Список локаций со счетчиками.
|
||||
* @property labels Список всех меток.
|
||||
*/
|
||||
data class Success(
|
||||
val statistics: GroupStatistics,
|
||||
val locations: List<LocationOutCount>,
|
||||
val labels: List<LabelOut>
|
||||
) : DashboardUiState
|
||||
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Состояние ошибки во время загрузки данных.
|
||||
* @property message Человекочитаемое сообщение об ошибке.
|
||||
*/
|
||||
data class Error(val message: String) : DashboardUiState
|
||||
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Состояние, когда данные для экрана загружаются.
|
||||
*/
|
||||
data object Loading : DashboardUiState
|
||||
}
|
||||
// [END_FILE_DashboardUiState.kt]
|
||||
@@ -1,105 +0,0 @@
|
||||
// [PACKAGE] com.homebox.lens.ui.screen.dashboard
|
||||
// [FILE] DashboardViewModel.kt
|
||||
// [SEMANTICS] ui_logic, dashboard, state_management, sealed_state, parallel_data_loading, timber_logging
|
||||
package com.homebox.lens.ui.screen.dashboard
|
||||
// [IMPORTS]
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.homebox.lens.domain.usecase.GetAllLabelsUseCase
|
||||
import com.homebox.lens.domain.usecase.GetAllLocationsUseCase
|
||||
import com.homebox.lens.domain.usecase.GetStatisticsUseCase
|
||||
import com.homebox.lens.ui.screen.dashboard.DashboardUiState
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
// [VIEWMODEL]
|
||||
// [ENTITY: ViewModel('DashboardViewModel')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* @summary ViewModel для главного экрана (Dashboard).
|
||||
* @description Оркестрирует загрузку данных для Dashboard, используя строгую модель состояний
|
||||
* (`DashboardUiState`), и обрабатывает параллельные запросы без состояний гонки.
|
||||
* @invariant `uiState` всегда является одним из состояний, определенных в `DashboardUiState`.
|
||||
*/
|
||||
@HiltViewModel
|
||||
class DashboardViewModel @Inject constructor(
|
||||
private val getStatisticsUseCase: GetStatisticsUseCase,
|
||||
private val getAllLocationsUseCase: GetAllLocationsUseCase,
|
||||
private val getAllLabelsUseCase: GetAllLabelsUseCase
|
||||
) : ViewModel() {
|
||||
|
||||
// [STATE]
|
||||
private val _uiState = MutableStateFlow<DashboardUiState>(DashboardUiState.Loading)
|
||||
// [FIX] Добавлен получатель (receiver) `_uiState` для вызова asStateFlow().
|
||||
// [REASON] `asStateFlow()` является функцией-расширением для `MutableStateFlow` и
|
||||
// должна вызываться на его экземпляре, чтобы создать публичную, неизменяемую версию потока.
|
||||
val uiState = _uiState.asStateFlow()
|
||||
|
||||
// [LIFECYCLE_HANDLER]
|
||||
init {
|
||||
loadDashboardData()
|
||||
}
|
||||
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* @summary Загружает все необходимые данные для экрана Dashboard.
|
||||
* @description Выполняет UseCase'ы параллельно и обновляет UI, переключая его
|
||||
* между состояниями `Loading`, `Success` и `Error` из `DashboardUiState`.
|
||||
* @sideeffect Асинхронно обновляет `_uiState` одним из состояний `DashboardUiState`.
|
||||
*/
|
||||
fun loadDashboardData() {
|
||||
// [ENTRYPOINT]
|
||||
viewModelScope.launch {
|
||||
_uiState.value = DashboardUiState.Loading
|
||||
// [FIX] Используем Timber для логирования.
|
||||
Timber.i("[ACTION] Starting parallel dashboard data load. State -> Loading.")
|
||||
|
||||
// [CORE-LOGIC: PARALLEL_FETCH]
|
||||
val result = runCatching {
|
||||
coroutineScope {
|
||||
val statsDeferred = async { getStatisticsUseCase() }
|
||||
val locationsDeferred = async { getAllLocationsUseCase() }
|
||||
val labelsDeferred = async { getAllLabelsUseCase() }
|
||||
|
||||
val stats = statsDeferred.await()
|
||||
val locations = locationsDeferred.await()
|
||||
val labels = labelsDeferred.await()
|
||||
|
||||
// [POSTCONDITION_CHECK]
|
||||
check(stats != null && locations != null && labels != null) {
|
||||
"[POSTCONDITION_FAILED] One or more dashboard data sources returned null."
|
||||
}
|
||||
Triple(stats, locations, labels)
|
||||
}
|
||||
}
|
||||
|
||||
// [RESULT_HANDLER]
|
||||
result.fold(
|
||||
onSuccess = { (stats, locations, labels) ->
|
||||
// [FIX] Используем Timber для логирования.
|
||||
Timber.i("[SUCCESS] Dashboard data loaded successfully. State -> Success.")
|
||||
_uiState.value = DashboardUiState.Success(
|
||||
statistics = stats,
|
||||
locations = locations,
|
||||
labels = labels
|
||||
)
|
||||
},
|
||||
onFailure = { exception ->
|
||||
// [FIX] Используем Timber для логирования ошибок с передачей исключения.
|
||||
Timber.e(exception, "[ERROR] Failed to load dashboard data. State -> Error.")
|
||||
_uiState.value = DashboardUiState.Error(
|
||||
message = exception.message ?: "Could not load dashboard data."
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
// [END_CLASS_DashboardViewModel]
|
||||
}
|
||||
// [END_FILE_DashboardViewModel.kt]
|
||||
@@ -1,22 +0,0 @@
|
||||
// [PACKAGE] com.homebox.lens.ui.screen.inventorylist
|
||||
// [FILE] InventoryListScreen.kt
|
||||
|
||||
package com.homebox.lens.ui.screen.inventorylist
|
||||
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
|
||||
// [ENTRYPOINT]
|
||||
@Composable
|
||||
fun InventoryListScreen() {
|
||||
// [ACTION]
|
||||
Text(text = "Inventory List Screen")
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun InventoryListScreenPreview() {
|
||||
InventoryListScreen()
|
||||
}
|
||||
// [END_FILE_InventoryListScreen.kt]
|
||||
@@ -1,16 +0,0 @@
|
||||
// [PACKAGE] com.homebox.lens.ui.screen.inventorylist
|
||||
// [FILE] InventoryListViewModel.kt
|
||||
|
||||
package com.homebox.lens.ui.screen.inventorylist
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
|
||||
// [VIEWMODEL]
|
||||
@HiltViewModel
|
||||
class InventoryListViewModel @Inject constructor() : ViewModel() {
|
||||
// [STATE]
|
||||
// TODO: Implement UI state
|
||||
}
|
||||
// [END_FILE_InventoryListViewModel.kt]
|
||||
@@ -1,22 +0,0 @@
|
||||
// [PACKAGE] com.homebox.lens.ui.screen.itemdetails
|
||||
// [FILE] ItemDetailsScreen.kt
|
||||
|
||||
package com.homebox.lens.ui.screen.itemdetails
|
||||
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
|
||||
// [ENTRYPOINT]
|
||||
@Composable
|
||||
fun ItemDetailsScreen() {
|
||||
// [ACTION]
|
||||
Text(text = "Item Details Screen")
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun ItemDetailsScreenPreview() {
|
||||
ItemDetailsScreen()
|
||||
}
|
||||
// [END_FILE_ItemDetailsScreen.kt]
|
||||
@@ -1,16 +0,0 @@
|
||||
// [PACKAGE] com.homebox.lens.ui.screen.itemdetails
|
||||
// [FILE] ItemDetailsViewModel.kt
|
||||
|
||||
package com.homebox.lens.ui.screen.itemdetails
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
|
||||
// [VIEWMODEL]
|
||||
@HiltViewModel
|
||||
class ItemDetailsViewModel @Inject constructor() : ViewModel() {
|
||||
// [STATE]
|
||||
// TODO: Implement UI state
|
||||
}
|
||||
// [END_FILE_ItemDetailsViewModel.kt]
|
||||
@@ -1,22 +0,0 @@
|
||||
// [PACKAGE] com.homebox.lens.ui.screen.itemedit
|
||||
// [FILE] ItemEditScreen.kt
|
||||
|
||||
package com.homebox.lens.ui.screen.itemedit
|
||||
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
|
||||
// [ENTRYPOINT]
|
||||
@Composable
|
||||
fun ItemEditScreen() {
|
||||
// [ACTION]
|
||||
Text(text = "Item Edit Screen")
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun ItemEditScreenPreview() {
|
||||
ItemEditScreen()
|
||||
}
|
||||
// [END_FILE_ItemEditScreen.kt]
|
||||
@@ -1,16 +0,0 @@
|
||||
// [PACKAGE] com.homebox.lens.ui.screen.itemedit
|
||||
// [FILE] ItemEditViewModel.kt
|
||||
|
||||
package com.homebox.lens.ui.screen.itemedit
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
|
||||
// [VIEWMODEL]
|
||||
@HiltViewModel
|
||||
class ItemEditViewModel @Inject constructor() : ViewModel() {
|
||||
// [STATE]
|
||||
// TODO: Implement UI state
|
||||
}
|
||||
// [END_FILE_ItemEditViewModel.kt]
|
||||
@@ -1,22 +0,0 @@
|
||||
// [PACKAGE] com.homebox.lens.ui.screen.labelslist
|
||||
// [FILE] LabelsListScreen.kt
|
||||
|
||||
package com.homebox.lens.ui.screen.labelslist
|
||||
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
|
||||
// [ENTRYPOINT]
|
||||
@Composable
|
||||
fun LabelsListScreen() {
|
||||
// [ACTION]
|
||||
Text(text = "Labels List Screen")
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun LabelsListScreenPreview() {
|
||||
LabelsListScreen()
|
||||
}
|
||||
// [END_FILE_LabelsListScreen.kt]
|
||||
@@ -1,16 +0,0 @@
|
||||
// [PACKAGE] com.homebox.lens.ui.screen.labelslist
|
||||
// [FILE] LabelsListViewModel.kt
|
||||
|
||||
package com.homebox.lens.ui.screen.labelslist
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
|
||||
// [VIEWMODEL]
|
||||
@HiltViewModel
|
||||
class LabelsListViewModel @Inject constructor() : ViewModel() {
|
||||
// [STATE]
|
||||
// TODO: Implement UI state
|
||||
}
|
||||
// [END_FILE_LabelsListViewModel.kt]
|
||||
@@ -1,22 +0,0 @@
|
||||
// [PACKAGE] com.homebox.lens.ui.screen.locationslist
|
||||
// [FILE] LocationsListScreen.kt
|
||||
|
||||
package com.homebox.lens.ui.screen.locationslist
|
||||
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
|
||||
// [ENTRYPOINT]
|
||||
@Composable
|
||||
fun LocationsListScreen() {
|
||||
// [ACTION]
|
||||
Text(text = "Locations List Screen")
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun LocationsListScreenPreview() {
|
||||
LocationsListScreen()
|
||||
}
|
||||
// [END_FILE_LocationsListScreen.kt]
|
||||
@@ -1,16 +0,0 @@
|
||||
// [PACKAGE] com.homebox.lens.ui.screen.locationslist
|
||||
// [FILE] LocationsListViewModel.kt
|
||||
|
||||
package com.homebox.lens.ui.screen.locationslist
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
|
||||
// [VIEWMODEL]
|
||||
@HiltViewModel
|
||||
class LocationsListViewModel @Inject constructor() : ViewModel() {
|
||||
// [STATE]
|
||||
// TODO: Implement UI state
|
||||
}
|
||||
// [END_FILE_LocationsListViewModel.kt]
|
||||
@@ -1,22 +0,0 @@
|
||||
// [PACKAGE] com.homebox.lens.ui.screen.search
|
||||
// [FILE] SearchScreen.kt
|
||||
|
||||
package com.homebox.lens.ui.screen.search
|
||||
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
|
||||
// [ENTRYPOINT]
|
||||
@Composable
|
||||
fun SearchScreen() {
|
||||
// [ACTION]
|
||||
Text(text = "Search Screen")
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun SearchScreenPreview() {
|
||||
SearchScreen()
|
||||
}
|
||||
// [END_FILE_SearchScreen.kt]
|
||||
@@ -1,16 +0,0 @@
|
||||
// [PACKAGE] com.homebox.lens.ui.screen.search
|
||||
// [FILE] SearchViewModel.kt
|
||||
|
||||
package com.homebox.lens.ui.screen.search
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
|
||||
// [VIEWMODEL]
|
||||
@HiltViewModel
|
||||
class SearchViewModel @Inject constructor() : ViewModel() {
|
||||
// [STATE]
|
||||
// TODO: Implement UI state
|
||||
}
|
||||
// [END_FILE_SearchViewModel.kt]
|
||||
@@ -1,120 +0,0 @@
|
||||
// [PACKAGE] com.homebox.lens.ui.screen.setup
|
||||
// [FILE] SetupScreen.kt
|
||||
|
||||
package com.homebox.lens.ui.screen.setup
|
||||
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.*
|
||||
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.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
|
||||
// [FIX] Opt-in for experimental Material 3 APIs
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
// [ENTRYPOINT]
|
||||
@Composable
|
||||
fun SetupScreen(
|
||||
viewModel: SetupViewModel = hiltViewModel(),
|
||||
onSetupComplete: () -> Unit
|
||||
) {
|
||||
val uiState by viewModel.uiState.collectAsState()
|
||||
|
||||
if (uiState.isSetupComplete) {
|
||||
onSetupComplete()
|
||||
}
|
||||
|
||||
SetupScreenContent(
|
||||
uiState = uiState,
|
||||
onServerUrlChange = viewModel::onServerUrlChange,
|
||||
onUsernameChange = viewModel::onUsernameChange,
|
||||
onPasswordChange = viewModel::onPasswordChange,
|
||||
onConnectClick = viewModel::connect
|
||||
)
|
||||
}
|
||||
|
||||
// [FIX] Opt-in for experimental Material 3 APIs
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
// [CONTENT]
|
||||
@Composable
|
||||
private fun SetupScreenContent(
|
||||
uiState: SetupUiState,
|
||||
onServerUrlChange: (String) -> Unit,
|
||||
onUsernameChange: (String) -> Unit,
|
||||
onPasswordChange: (String) -> Unit,
|
||||
onConnectClick: () -> Unit
|
||||
) {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(title = { Text("Server Setup") })
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = uiState.serverUrl,
|
||||
onValueChange = onServerUrlChange,
|
||||
label = { Text("Server URL") },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
OutlinedTextField(
|
||||
value = uiState.username,
|
||||
onValueChange = onUsernameChange,
|
||||
label = { Text("Username") },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
OutlinedTextField(
|
||||
value = uiState.password,
|
||||
onValueChange = onPasswordChange,
|
||||
label = { Text("Password") },
|
||||
visualTransformation = PasswordVisualTransformation(),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Button(
|
||||
onClick = onConnectClick,
|
||||
enabled = !uiState.isLoading,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
if (uiState.isLoading) {
|
||||
CircularProgressIndicator(modifier = Modifier.size(24.dp))
|
||||
} else {
|
||||
Text("Connect")
|
||||
}
|
||||
}
|
||||
uiState.error?.let {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(text = it, color = MaterialTheme.colorScheme.error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// [FIX] Opt-in for experimental Material 3 APIs
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun SetupScreenPreview() {
|
||||
SetupScreenContent(
|
||||
uiState = SetupUiState(error = "Failed to connect"),
|
||||
onServerUrlChange = {},
|
||||
onUsernameChange = {},
|
||||
onPasswordChange = {},
|
||||
onConnectClick = {}
|
||||
)
|
||||
}
|
||||
// [END_FILE_SetupScreen.kt]
|
||||
@@ -1,27 +0,0 @@
|
||||
// [PACKAGE] com.homebox.lens.ui.screen.setup
|
||||
// [FILE] SetupUiState.kt
|
||||
// [SEMANTICS] ui_state, data_model, immutable
|
||||
|
||||
package com.homebox.lens.ui.screen.setup
|
||||
|
||||
/**
|
||||
* [ENTITY: DataClass('SetupUiState')]
|
||||
* [CONTRACT]
|
||||
* Неизменяемая модель данных, представляющая полное состояние экрана настройки (Setup Screen).
|
||||
* Использование `data class` предоставляет метод `copy()` для легкого создания новых состояний.
|
||||
* @property serverUrl URL-адрес сервера Homebox.
|
||||
* @property username Имя пользователя для входа.
|
||||
* @property password Пароль пользователя.
|
||||
* @property isLoading Флаг, указывающий, выполняется ли в данный момент сетевой запрос.
|
||||
* @property error Сообщение об ошибке для отображения пользователю, или `null` при отсутствии ошибки.
|
||||
* @property isSetupComplete Флаг, указывающий на успешное завершение настройки и входа.
|
||||
*/
|
||||
data class SetupUiState(
|
||||
val serverUrl: String = "",
|
||||
val username: String = "",
|
||||
val password: String = "",
|
||||
val isLoading: Boolean = false,
|
||||
val error: String? = null,
|
||||
val isSetupComplete: Boolean = false
|
||||
)
|
||||
// [END_FILE_SetupUiState.kt]
|
||||
@@ -1,143 +0,0 @@
|
||||
// [PACKAGE] com.homebox.lens.ui.screen.setup
|
||||
// [FILE] SetupViewModel.kt
|
||||
// [SEMANTICS] ui_logic, viewmodel, state_management, user_setup, authentication_flow
|
||||
package com.homebox.lens.ui.screen.setup
|
||||
// [IMPORTS]
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.homebox.lens.domain.model.Credentials
|
||||
import com.homebox.lens.domain.repository.CredentialsRepository
|
||||
import com.homebox.lens.domain.usecase.LoginUseCase
|
||||
import com.homebox.lens.ui.screen.setup.SetupUiState
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
// [VIEWMODEL]
|
||||
// [ENTITY: ViewModel('SetupViewModel')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* ViewModel для экрана первоначальной настройки (Setup).
|
||||
* Отвечает за:
|
||||
* 1. Загрузку и сохранение учетных данных (URL сервера, логин, пароль).
|
||||
* 2. Управление состоянием UI экрана (`SetupUiState`).
|
||||
* 3. Инициацию процесса входа в систему через `LoginUseCase`.
|
||||
* @property credentialsRepository Репозиторий для операций с учетными данными.
|
||||
* @property loginUseCase Use case для выполнения логики входа.
|
||||
* @invariant Состояние `uiState` всегда является единственным источником истины для UI.
|
||||
*/
|
||||
@HiltViewModel
|
||||
class SetupViewModel @Inject constructor(
|
||||
private val credentialsRepository: CredentialsRepository,
|
||||
private val loginUseCase: LoginUseCase
|
||||
) : ViewModel() {
|
||||
|
||||
// [STATE]
|
||||
private val _uiState = MutableStateFlow(SetupUiState())
|
||||
val uiState = _uiState.asStateFlow()
|
||||
|
||||
// [LIFECYCLE_HANDLER]
|
||||
init {
|
||||
// [ACTION] Загружаем учетные данные при создании ViewModel.
|
||||
loadCredentials()
|
||||
}
|
||||
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* [HELPER] Загружает учетные данные из репозитория при инициализации.
|
||||
* @sideeffect Асинхронно обновляет `_uiState` полученными учетными данными.
|
||||
*/
|
||||
private fun loadCredentials() {
|
||||
// [ENTRYPOINT]
|
||||
viewModelScope.launch {
|
||||
// [CORE-LOGIC] Подписываемся на поток учетных данных.
|
||||
credentialsRepository.getCredentials().collect { credentials ->
|
||||
// [ACTION] Обновляем состояние, если учетные данные существуют.
|
||||
if (credentials != null) {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
serverUrl = credentials.serverUrl,
|
||||
username = credentials.username,
|
||||
password = credentials.password
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* [ACTION] Обновляет URL сервера в состоянии UI в ответ на ввод пользователя.
|
||||
* @param newUrl Новое значение URL.
|
||||
* @sideeffect Обновляет поле `serverUrl` в `_uiState`.
|
||||
*/
|
||||
fun onServerUrlChange(newUrl: String) {
|
||||
_uiState.update { it.copy(serverUrl = newUrl) }
|
||||
}
|
||||
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* [ACTION] Обновляет имя пользователя в состоянии UI в ответ на ввод пользователя.
|
||||
* @param newUsername Новое значение имени пользователя.
|
||||
* @sideeffect Обновляет поле `username` в `_uiState`.
|
||||
*/
|
||||
fun onUsernameChange(newUsername: String) {
|
||||
_uiState.update { it.copy(username = newUsername) }
|
||||
}
|
||||
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* [ACTION] Обновляет пароль в состоянии UI в ответ на ввод пользователя.
|
||||
* @param newPassword Новое значение пароля.
|
||||
* @sideeffect Обновляет поле `password` в `_uiState`.
|
||||
*/
|
||||
fun onPasswordChange(newPassword: String) {
|
||||
_uiState.update { it.copy(password = newPassword) }
|
||||
}
|
||||
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* [ACTION] Запускает процесс подключения и входа в систему по действию пользователя.
|
||||
* Выполняет две основные операции:
|
||||
* 1. Сохраняет введенные учетные данные для последующих сессий.
|
||||
* 2. Выполняет вход в систему с использованием этих данных.
|
||||
* @sideeffect Обновляет `_uiState`, управляя флагами `isLoading`, `error` и `isSetupComplete`.
|
||||
* @sideeffect Вызывает `credentialsRepository.saveCredentials`, изменяя сохраненные данные.
|
||||
* @sideeffect Вызывает `loginUseCase`, который, в свою очередь, сохраняет токен.
|
||||
*/
|
||||
fun connect() {
|
||||
// [ENTRYPOINT]
|
||||
viewModelScope.launch {
|
||||
// [ACTION] Устанавливаем состояние загрузки и сбрасываем предыдущую ошибку.
|
||||
_uiState.update { it.copy(isLoading = true, error = null) }
|
||||
|
||||
// [PREPARATION] Готовим данные для операций, очищая их от лишних пробелов.
|
||||
val credentials = Credentials(
|
||||
serverUrl = _uiState.value.serverUrl.trim(),
|
||||
username = _uiState.value.username.trim(),
|
||||
password = _uiState.value.password
|
||||
)
|
||||
|
||||
// [ACTION] Сохраняем учетные данные для будущего использования.
|
||||
credentialsRepository.saveCredentials(credentials)
|
||||
|
||||
// [CORE-LOGIC] Выполняем UseCase и обрабатываем результат.
|
||||
loginUseCase(credentials).fold(
|
||||
onSuccess = {
|
||||
// [ACTION] Обработка успеха: обновляем UI, отмечая завершение настройки.
|
||||
_uiState.update { it.copy(isLoading = false, isSetupComplete = true) }
|
||||
},
|
||||
onFailure = { exception ->
|
||||
// [ERROR_HANDLER] Обработка ошибки: обновляем UI с сообщением об ошибке.
|
||||
_uiState.update { it.copy(isLoading = false, error = exception.message ?: "Login failed") }
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
// [END_CLASS_SetupViewModel]
|
||||
}
|
||||
// [END_FILE_SetupViewModel.kt]
|
||||
@@ -1,64 +0,0 @@
|
||||
// [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]
|
||||
@@ -1,23 +0,0 @@
|
||||
// [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]
|
||||
146
app/src/main/res/values-en/strings.xml
Normal file
146
app/src/main/res/values-en/strings.xml
Normal file
@@ -0,0 +1,146 @@
|
||||
<resources>
|
||||
<string name="app_name">Homebox Lens</string>
|
||||
|
||||
<!-- Common -->
|
||||
<string name="create">Create</string>
|
||||
<string name="edit">Edit</string>
|
||||
<string name="delete">Delete</string>
|
||||
<string name="search">Search</string>
|
||||
<string name="logout">Logout</string>
|
||||
<string name="no_location">No location</string>
|
||||
<string name="items_not_found">Items not found</string>
|
||||
<string name="error_loading_failed">Failed to load data. Please try again.</string>
|
||||
|
||||
<!-- Content Descriptions -->
|
||||
<string name="cd_open_navigation_drawer">Open navigation drawer</string>
|
||||
<string name="cd_scan_qr_code">Scan QR code</string>
|
||||
<string name="cd_search">Search</string>
|
||||
<string name="cd_navigate_back">Navigate back</string>
|
||||
<string name="cd_navigate_up">Go back</string>
|
||||
<string name="cd_add_new_location">Add new location</string>
|
||||
<string name="content_desc_add_label">Add new label</string>
|
||||
|
||||
<!-- Dashboard Screen -->
|
||||
<string name="dashboard_title">Dashboard</string>
|
||||
<string name="dashboard_section_quick_stats">Quick Stats</string>
|
||||
<string name="dashboard_section_recently_added">Recently Added</string>
|
||||
<string name="dashboard_section_locations">Locations</string>
|
||||
<string name="dashboard_section_labels">Labels</string>
|
||||
<string name="location_chip_label">%1$s (%2$d)</string>
|
||||
|
||||
<!-- Dashboard Statistics -->
|
||||
<string name="dashboard_stat_total_items">Total Items</string>
|
||||
<string name="dashboard_stat_total_value">Total Value</string>
|
||||
<string name="dashboard_stat_total_labels">Total Labels</string>
|
||||
<string name="dashboard_stat_total_locations">Total Locations</string>
|
||||
|
||||
<!-- Navigation -->
|
||||
<string name="nav_locations">Locations</string>
|
||||
<string name="nav_labels">Labels</string>
|
||||
|
||||
<!-- Screen Titles -->
|
||||
<string name="inventory_list_title">Inventory</string>
|
||||
|
||||
<!-- Screen Titles -->
|
||||
<string name="item_details_title">Details</string>
|
||||
<string name="item_edit_title">Edit Item</string>
|
||||
<string name="labels_list_title">Labels</string>
|
||||
<string name="locations_list_title">Locations</string>
|
||||
<string name="search_title">Search</string>
|
||||
|
||||
<string name="save_item">Save</string>
|
||||
<string name="item_name">Name</string>
|
||||
<string name="item_description">Description</string>
|
||||
<string name="item_quantity">Quantity</string>
|
||||
|
||||
<!-- Location Edit Screen -->
|
||||
<string name="location_edit_title_create">Create Location</string>
|
||||
<string name="location_edit_title_edit">Edit Location</string>
|
||||
|
||||
<!-- Locations List Screen -->
|
||||
<string name="locations_not_found">Locations not found. Press + to add a new one.</string>
|
||||
<string name="item_count">Items: %1$d</string>
|
||||
<string name="cd_more_options">More options</string>
|
||||
|
||||
<!-- Setup Screen -->
|
||||
<string name="setup_title">Server Setup</string>
|
||||
<string name="setup_server_url_label">Server URL</string>
|
||||
<string name="setup_username_label">Username</string>
|
||||
<string name="setup_password_label">Password</string>
|
||||
<string name="setup_connect_button">Connect</string>
|
||||
|
||||
<!-- Labels List Screen -->
|
||||
<string name="screen_title_labels">Labels</string>
|
||||
<string name="content_desc_navigate_back">Navigate back</string>
|
||||
<string name="content_desc_create_label">Create new label</string>
|
||||
<string name="content_desc_label_icon">Label icon</string>
|
||||
<string name="content_desc_delete_label">Delete label</string>
|
||||
<string name="no_labels_found">No labels found.</string>
|
||||
<string name="dialog_title_create_label">Create Label</string>
|
||||
<string name="dialog_field_label_name">Label Name</string>
|
||||
<string name="dialog_button_create">Create</string>
|
||||
<string name="dialog_button_cancel">Cancel</string>
|
||||
|
||||
|
||||
|
||||
<!-- Inventory List Screen -->
|
||||
<string name="content_desc_sync_inventory">Sync inventory</string>
|
||||
|
||||
<!-- Item Details Screen -->
|
||||
<string name="content_desc_edit_item">Edit item</string>
|
||||
<string name="content_desc_delete_item">Delete item</string>
|
||||
<string name="section_title_description">Description</string>
|
||||
<string name="placeholder_no_description">No description</string>
|
||||
<string name="section_title_details">Details</string>
|
||||
<string name="label_quantity">Quantity</string>
|
||||
<string name="label_location">Location</string>
|
||||
<string name="section_title_labels">Labels</string>
|
||||
|
||||
<!-- Item Edit Screen -->
|
||||
<string name="item_edit_title_create">Create item</string>
|
||||
<string name="content_desc_save_item">Save item</string>
|
||||
<string name="label_name">Name</string>
|
||||
<string name="label_description">Description</string>
|
||||
|
||||
<!-- Search Screen -->
|
||||
<string name="placeholder_search_items">Search items...</string>
|
||||
|
||||
<!-- Setup Screen -->
|
||||
<string name="screen_title_setup">Setup</string>
|
||||
|
||||
<!-- Label Edit Screen -->
|
||||
<string name="label_edit_title_create">Create label</string>
|
||||
<string name="label_edit_title_edit">Edit label</string>
|
||||
<string name="label_name_edit">Label name</string>
|
||||
|
||||
<!-- Common Actions -->
|
||||
<string name="back">Back</string>
|
||||
<string name="save">Save</string>
|
||||
|
||||
<!-- Color Picker -->
|
||||
<string name="label_color">Color</string>
|
||||
<string name="label_hex_color">HEX color code</string>
|
||||
|
||||
<string name="item_asset_id">Asset ID</string>
|
||||
<string name="item_notes">Notes</string>
|
||||
<string name="item_serial_number">Serial Number</string>
|
||||
<string name="item_purchase_price">Purchase Price</string>
|
||||
<string name="item_purchase_date">Purchase Date</string>
|
||||
<string name="item_warranty_until">Warranty Until</string>
|
||||
<string name="item_parent_id">Parent ID</string>
|
||||
<string name="item_is_archived">Is Archived</string>
|
||||
<string name="item_insured">Insured</string>
|
||||
<string name="item_lifetime_warranty">Lifetime Warranty</string>
|
||||
<string name="item_sync_child_items_locations">Sync Child Items Locations</string>
|
||||
<string name="item_manufacturer">Manufacturer</string>
|
||||
<string name="item_model_number">Model Number</string>
|
||||
<string name="item_purchase_from">Purchase From</string>
|
||||
<string name="item_warranty_details">Warranty Details</string>
|
||||
<string name="item_sold_notes">Sold Notes</string>
|
||||
<string name="item_sold_price">Sold Price</string>
|
||||
<string name="item_sold_time">Sold Time</string>
|
||||
<string name="item_sold_to">Sold To</string>
|
||||
<string name="scan_qr_code">Scan QR Code</string>
|
||||
<string name="ok">OK</string>
|
||||
<string name="cancel">Cancel</string>
|
||||
</resources>
|
||||
@@ -1,3 +1,140 @@
|
||||
<resources>
|
||||
<string name="app_name">Homebox Lens</string>
|
||||
|
||||
<!-- Common -->
|
||||
<string name="create">Создать</string>
|
||||
<string name="edit">Редактировать</string>
|
||||
<string name="delete">Удалить</string>
|
||||
<string name="search">Поиск</string>
|
||||
<string name="logout">Выйти</string>
|
||||
<string name="no_location">Нет локации</string>
|
||||
<string name="items_not_found">Элементы не найдены</string>
|
||||
<string name="error_loading_failed">Не удалось загрузить данные. Пожалуйста, попробуйте еще раз.</string>
|
||||
|
||||
<!-- Content Descriptions -->
|
||||
<string name="cd_open_navigation_drawer">Открыть боковое меню</string>
|
||||
<string name="cd_scan_qr_code">Сканировать QR/штрих-код</string>
|
||||
<string name="cd_search">Поиск</string>
|
||||
<string name="cd_navigate_back">Вернуться назад</string>
|
||||
<string name="cd_navigate_up">Вернуться</string>
|
||||
<string name="cd_add_new_location">Добавить новую локацию</string>
|
||||
<string name="content_desc_add_label">Добавить новую метку</string>
|
||||
|
||||
<!-- Inventory List Screen -->
|
||||
<string name="content_desc_sync_inventory">Синхронизировать инвентарь</string>
|
||||
|
||||
<!-- Item Details Screen -->
|
||||
<string name="content_desc_edit_item">Редактировать элемент</string>
|
||||
<string name="content_desc_delete_item">Удалить элемент</string>
|
||||
<string name="section_title_description">Описание</string>
|
||||
<string name="placeholder_no_description">Нет описания</string>
|
||||
<string name="section_title_details">Детали</string>
|
||||
<string name="label_quantity">Количество</string>
|
||||
<string name="label_location">Местоположение</string>
|
||||
<string name="section_title_labels">Метки</string>
|
||||
|
||||
<!-- Item Edit Screen -->
|
||||
<string name="item_edit_title_create">Создать элемент</string>
|
||||
<string name="content_desc_save_item">Сохранить элемент</string>
|
||||
<string name="label_name">Название</string>
|
||||
<string name="label_description">Описание</string>
|
||||
|
||||
<!-- Search Screen -->
|
||||
<string name="placeholder_search_items">Поиск элементов...</string>
|
||||
|
||||
<!-- Dashboard Screen -->
|
||||
<string name="dashboard_title">Главная</string>
|
||||
<string name="dashboard_section_quick_stats">Быстрая статистика</string>
|
||||
<string name="dashboard_section_recently_added">Недавно добавлено</string>
|
||||
<string name="dashboard_section_locations">Места хранения</string>
|
||||
<string name="dashboard_section_labels">Метки</string>
|
||||
<string name="location_chip_label">%1$s (%2$d)</string>
|
||||
|
||||
<!-- Dashboard Statistics -->
|
||||
<string name="dashboard_stat_total_items">Всего вещей</string>
|
||||
<string name="dashboard_stat_total_value">Общая стоимость</string>
|
||||
<string name="dashboard_stat_total_labels">Всего меток</string>
|
||||
<string name="dashboard_stat_total_locations">Всего локаций</string>
|
||||
|
||||
<!-- Navigation -->
|
||||
<string name="nav_locations">Локации</string>
|
||||
<string name="nav_labels">Метки</string>
|
||||
|
||||
<!-- Screen Titles -->
|
||||
<string name="inventory_list_title">Инвентарь</string>
|
||||
<string name="item_details_title">Детали</string>
|
||||
<string name="item_edit_title">Редактирование</string>
|
||||
<string name="labels_list_title">Метки</string>
|
||||
<string name="locations_list_title">Места хранения</string>
|
||||
<string name="search_title">Поиск</string>
|
||||
|
||||
<string name="save_item">Сохранить</string>
|
||||
<string name="item_name">Название</string>
|
||||
<string name="item_description">Описание</string>
|
||||
<string name="item_quantity">Количество</string>
|
||||
|
||||
<!-- Location Edit Screen -->
|
||||
<string name="location_edit_title_create">Создать локацию</string>
|
||||
<string name="location_edit_title_edit">Редактировать локацию</string>
|
||||
|
||||
<!-- Locations List Screen -->
|
||||
<string name="locations_not_found">Местоположения не найдены. Нажмите +, чтобы добавить новое.</string>
|
||||
<string name="item_count">Предметов: %1$d</string>
|
||||
<string name="cd_more_options">Больше опций</string>
|
||||
|
||||
<!-- Setup Screen -->
|
||||
<string name="screen_title_setup">Настройка</string>
|
||||
<string name="setup_title">Настройка сервера</string>
|
||||
<string name="setup_server_url_label">URL сервера</string>
|
||||
<string name="setup_username_label">Имя пользователя</string>
|
||||
<string name="setup_password_label">Пароль</string>
|
||||
<string name="setup_connect_button">Подключиться</string>
|
||||
|
||||
<!-- Labels List Screen -->
|
||||
<string name="screen_title_labels">Метки</string>
|
||||
<string name="content_desc_navigate_back" translatable="false">Вернуться назад</string>
|
||||
<string name="content_desc_create_label">Создать новую метку</string>
|
||||
<string name="content_desc_label_icon">Иконка метки</string>
|
||||
<string name="content_desc_delete_label">Удалить метку</string>
|
||||
<string name="no_labels_found">Метки не найдены.</string>
|
||||
<string name="dialog_title_create_label">Создать метку</string>
|
||||
<string name="dialog_field_label_name">Название метки</string>
|
||||
<string name="dialog_button_create">Создать</string>
|
||||
<string name="dialog_button_cancel">Отмена</string>
|
||||
|
||||
<!-- Label Edit Screen -->
|
||||
<string name="label_edit_title_create">Создать метку</string>
|
||||
<string name="label_edit_title_edit">Редактировать метку</string>
|
||||
<string name="label_name_edit">Название метки</string>
|
||||
|
||||
<!-- Common Actions -->
|
||||
<string name="back">Назад</string>
|
||||
<string name="save">Сохранить</string>
|
||||
<!-- Common Actions -->
|
||||
|
||||
<!-- Color Picker -->
|
||||
<string name="label_color">Цвет</string>
|
||||
<string name="label_hex_color">HEX-код цвета</string>
|
||||
<string name="item_asset_id">Идентификатор актива</string>
|
||||
<string name="item_notes">Заметки</string>
|
||||
<string name="item_serial_number">Серийный номер</string>
|
||||
<string name="item_purchase_price">Цена покупки</string>
|
||||
<string name="item_purchase_date">Дата покупки</string>
|
||||
<string name="item_warranty_until">Гарантия до</string>
|
||||
<string name="item_parent_id">Родительский ID</string>
|
||||
<string name="item_is_archived">Архивировано</string>
|
||||
<string name="item_insured">Застраховано</string>
|
||||
<string name="item_lifetime_warranty">Пожизненная гарантия</string>
|
||||
<string name="item_sync_child_items_locations">Синхронизировать дочерние элементы</string>
|
||||
<string name="item_manufacturer">Производитель</string>
|
||||
<string name="item_model_number">Номер модели</string>
|
||||
<string name="item_purchase_from">Куплено у</string>
|
||||
<string name="item_warranty_details">Детали гарантии</string>
|
||||
<string name="item_sold_notes">Примечания о продаже</string>
|
||||
<string name="item_sold_price">Цена продажи</string>
|
||||
<string name="item_sold_time">Время продажи</string>
|
||||
<string name="item_sold_to">Продано кому</string>
|
||||
<string name="scan_qr_code">Сканировать QR-код</string>
|
||||
<string name="ok">ОК</string>
|
||||
<string name="cancel">Отмена</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
// [FILE] build.gradle.kts
|
||||
// [PURPOSE] Root build file for the project, configures plugins for all modules.
|
||||
// [SEMANTICS] build, configuration
|
||||
// [AI_NOTE]: 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
|
||||
id("com.android.application") version "8.12.3" apply false
|
||||
id("org.jetbrains.kotlin.android") version "2.0.0" apply false
|
||||
id("org.jetbrains.kotlin.plugin.compose") version "2.0.0" apply false
|
||||
id("com.google.dagger.hilt.android") version "2.51.1" apply false
|
||||
id("com.google.devtools.ksp") version "2.0.0-1.0.24" apply false
|
||||
}
|
||||
|
||||
// [END_FILE_build.gradle.kts]
|
||||
|
||||
@@ -1,72 +1,56 @@
|
||||
// [PACKAGE] buildsrc.dependencies
|
||||
// [FILE] Dependencies.kt
|
||||
// [PURPOSE] Centralized dependency management for the entire project.
|
||||
// [SEMANTICS] build, dependencies
|
||||
|
||||
// [ENTITY: Object('Versions')]
|
||||
object Versions {
|
||||
// Build
|
||||
const val compileSdk = 34
|
||||
const val minSdk = 26
|
||||
const val minSdk = 24
|
||||
const val targetSdk = 34
|
||||
const val versionCode = 1
|
||||
const val versionName = "1.0"
|
||||
|
||||
// Kotlin
|
||||
const val kotlin = "1.9.22"
|
||||
const val kotlin = "1.9.10"
|
||||
const val coroutines = "1.7.3"
|
||||
|
||||
// Jetpack Compose
|
||||
const val composeCompiler = "1.5.8"
|
||||
const val composeBom = "2023.10.01"
|
||||
const val composeCompiler = "1.5.4"
|
||||
const val composeBom = "2024.05.00"
|
||||
const val activityCompose = "1.8.2"
|
||||
const val navigationCompose = "2.7.6"
|
||||
const val navigationCompose = "2.7.7"
|
||||
const val hiltNavigationCompose = "1.1.0"
|
||||
|
||||
// AndroidX
|
||||
const val coreKtx = "1.12.0"
|
||||
const val lifecycle = "2.6.2"
|
||||
const val lifecycle = "2.7.0"
|
||||
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 moshi = "1.15.1"
|
||||
const val room = "2.6.1"
|
||||
|
||||
// DI
|
||||
const val hilt = "2.48.1"
|
||||
const val hiltCompiler = "1.1.0"
|
||||
|
||||
// Logging
|
||||
const val hilt = "2.51.1"
|
||||
const val hiltCompiler = "1.2.0"
|
||||
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"
|
||||
const val kotest = "5.8.0"
|
||||
const val mockk = "1.13.10"
|
||||
}
|
||||
// [END_ENTITY: Object('Versions')]
|
||||
|
||||
// [ENTITY: Object('Libs')]
|
||||
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 composeUi = "androidx.compose.ui:ui:1.5.4"
|
||||
const val composeUiGraphics = "androidx.compose.ui:ui-graphics:1.5.4"
|
||||
const val composeUiToolingPreview = "androidx.compose.ui:ui-tooling-preview:1.5.4"
|
||||
const val composeMaterial3 = "androidx.compose.material3:material3:1.1.2"
|
||||
const val composeFoundation = "androidx.compose.foundation:foundation:1.5.4"
|
||||
const val composeFoundationLayout = "androidx.compose.foundation:foundation-layout:1.5.4"
|
||||
const val composeMaterialIconsExtended = "androidx.compose.material:material-icons-extended:1.5.4"
|
||||
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 okhttp = "com.squareup.okhttp3:okhttp:${Versions.okhttp}"
|
||||
@@ -74,27 +58,22 @@ object Libs {
|
||||
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"
|
||||
|
||||
const val composeUiTestJunit4 = "androidx.compose.ui:ui-test-junit4:1.5.4"
|
||||
const val composeUiTooling = "androidx.compose.ui:ui-tooling:1.5.4"
|
||||
const val composeUiTestManifest = "androidx.compose.ui:ui-test-manifest:1.5.4"
|
||||
const val kotestRunnerJunit5 = "io.kotest:kotest-runner-junit5:${Versions.kotest}"
|
||||
const val kotestAssertionsCore = "io.kotest:kotest-assertions-core:${Versions.kotest}"
|
||||
const val mockk = "io.mockk:mockk:${Versions.mockk}"
|
||||
}
|
||||
// [END_ENTITY: Object('Libs')]
|
||||
|
||||
// [END_FILE_Dependencies.kt]
|
||||
@@ -62,6 +62,9 @@ dependencies {
|
||||
implementation(Libs.hiltAndroid)
|
||||
kapt(Libs.hiltCompiler)
|
||||
|
||||
// [DEPENDENCY] Logging
|
||||
implementation(Libs.timber)
|
||||
|
||||
// [DEPENDENCY] Testing
|
||||
testImplementation(Libs.junit)
|
||||
androidTestImplementation(Libs.extJunit)
|
||||
|
||||
@@ -1,73 +1,97 @@
|
||||
// [PACKAGE] com.homebox.lens.data.api
|
||||
// [FILE] HomeboxApiService.kt
|
||||
|
||||
// [SEMANTICS] data, api, retrofit
|
||||
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.LoginFormDto
|
||||
import com.homebox.lens.data.api.dto.PaginationResultDto
|
||||
import com.homebox.lens.data.api.dto.TokenResponseDto
|
||||
// [IMPORTS]
|
||||
import com.homebox.lens.data.api.dto.*
|
||||
import retrofit2.Response
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.DELETE
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Headers
|
||||
import retrofit2.http.POST
|
||||
import retrofit2.http.PUT
|
||||
import retrofit2.http.Path
|
||||
import retrofit2.http.Query
|
||||
import retrofit2.http.*
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [CONTRACT]
|
||||
// [ENTITY: Interface('HomeboxApiService')]
|
||||
/**
|
||||
* [ENTITY: Interface('HomeboxApiService')]
|
||||
* [PURPOSE] Определяет эндпоинты для взаимодействия с Homebox API, используя DTO.
|
||||
* @summary Определяет эндпоинты для взаимодействия с Homebox API, используя DTO.
|
||||
*/
|
||||
interface HomeboxApiService {
|
||||
|
||||
// [ENDPOINT] Auth
|
||||
// [FIX] Явно указываем заголовок Content-Type, чтобы переопределить
|
||||
// значение по умолчанию от Moshi, которое содержит "; charset=UTF-8".
|
||||
// [ENTITY: ApiEndpoint('login')]
|
||||
@Headers("Content-Type: application/json")
|
||||
@POST("v1/users/login")
|
||||
suspend fun login(@Body loginForm: LoginFormDto): TokenResponseDto
|
||||
// [END_ENTITY: ApiEndpoint('login')]
|
||||
|
||||
// [ENDPOINT] Items
|
||||
// [ENTITY: ApiEndpoint('getItems')]
|
||||
@GET("v1/items")
|
||||
suspend fun getItems(
|
||||
@Query("q") query: String? = null,
|
||||
@Query("page") page: Int? = null,
|
||||
@Query("pageSize") pageSize: Int? = null
|
||||
): PaginationResultDto<ItemSummaryDto>
|
||||
// [END_ENTITY: ApiEndpoint('getItems')]
|
||||
|
||||
// [ENTITY: ApiEndpoint('createItem')]
|
||||
@POST("v1/items")
|
||||
suspend fun createItem(@Body item: ItemCreateDto): ItemSummaryDto
|
||||
// [END_ENTITY: ApiEndpoint('createItem')]
|
||||
|
||||
// [ENTITY: ApiEndpoint('getItem')]
|
||||
@GET("v1/items/{id}")
|
||||
suspend fun getItem(@Path("id") itemId: String): ItemOutDto
|
||||
// [END_ENTITY: ApiEndpoint('getItem')]
|
||||
|
||||
// [ENTITY: ApiEndpoint('updateItem')]
|
||||
@PUT("v1/items/{id}")
|
||||
suspend fun updateItem(@Path("id") itemId: String, @Body item: ItemUpdateDto): ItemOutDto
|
||||
// [END_ENTITY: ApiEndpoint('updateItem')]
|
||||
|
||||
// [ENTITY: ApiEndpoint('deleteItem')]
|
||||
@DELETE("v1/items/{id}")
|
||||
suspend fun deleteItem(@Path("id") itemId: String): Response<Unit>
|
||||
// [END_ENTITY: ApiEndpoint('deleteItem')]
|
||||
|
||||
// [ENDPOINT] Locations
|
||||
// [ENTITY: ApiEndpoint('getLocations')]
|
||||
@GET("v1/locations")
|
||||
suspend fun getLocations(): List<LocationOutCountDto>
|
||||
// [END_ENTITY: ApiEndpoint('getLocations')]
|
||||
|
||||
// [ENDPOINT] Labels
|
||||
// [ENTITY: ApiEndpoint('getLabels')]
|
||||
@GET("v1/labels")
|
||||
suspend fun getLabels(): List<LabelOutDto>
|
||||
// [END_ENTITY: ApiEndpoint('getLabels')]
|
||||
|
||||
// [ENDPOINT] Statistics
|
||||
// [ENTITY: ApiEndpoint('createLabel')]
|
||||
@POST("v1/labels")
|
||||
suspend fun createLabel(@Body newLabel: LabelCreateDto): LabelSummaryDto
|
||||
// [END_ENTITY: ApiEndpoint('createLabel')]
|
||||
|
||||
// [ENTITY: ApiEndpoint('updateLabel')]
|
||||
@PUT("v1/labels/{id}")
|
||||
suspend fun updateLabel(@Path("id") labelId: String, @Body label: LabelUpdateDto): LabelOutDto
|
||||
// [END_ENTITY: ApiEndpoint('updateLabel')]
|
||||
|
||||
// [ENTITY: ApiEndpoint('deleteLabel')]
|
||||
@DELETE("v1/labels/{id}")
|
||||
suspend fun deleteLabel(@Path("id") labelId: String): Response<Unit>
|
||||
|
||||
// [ENTITY: ApiEndpoint('createLocation')]
|
||||
@POST("v1/locations")
|
||||
suspend fun createLocation(@Body newLocation: LocationCreateDto): LocationOutDto
|
||||
// [END_ENTITY: ApiEndpoint('createLocation')]
|
||||
|
||||
// [ENTITY: ApiEndpoint('updateLocation')]
|
||||
@PUT("v1/locations/{id}")
|
||||
suspend fun updateLocation(@Path("id") locationId: String, @Body location: LocationUpdateDto): LocationOutDto
|
||||
// [END_ENTITY: ApiEndpoint('updateLocation')]
|
||||
|
||||
// [ENTITY: ApiEndpoint('deleteLocation')]
|
||||
@DELETE("v1/locations/{id}")
|
||||
suspend fun deleteLocation(@Path("id") locationId: String): Response<Unit>
|
||||
|
||||
// [ENTITY: ApiEndpoint('getStatistics')]
|
||||
@GET("v1/groups/statistics")
|
||||
suspend fun getStatistics(): GroupStatisticsDto
|
||||
// [END_ENTITY: ApiEndpoint('getStatistics')]
|
||||
}
|
||||
|
||||
// [END_ENTITY: Interface('HomeboxApiService')]
|
||||
// [END_FILE_HomeboxApiService.kt]
|
||||
|
||||
@@ -8,11 +8,11 @@ package com.homebox.lens.data.api.dto
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import com.homebox.lens.domain.model.CustomField
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [CORE-LOGIC]
|
||||
// [ENTITY: DataClass('CustomFieldDto')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* DTO для кастомного поля.
|
||||
* @summary DTO для кастомного поля.
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class CustomFieldDto(
|
||||
@@ -20,10 +20,12 @@ data class CustomFieldDto(
|
||||
@Json(name = "value") val value: String,
|
||||
@Json(name = "type") val type: String
|
||||
)
|
||||
// [END_ENTITY: DataClass('CustomFieldDto')]
|
||||
|
||||
// [ENTITY: Function('toDomain')]
|
||||
// [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('CustomField')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Маппер из CustomFieldDto в доменную модель CustomField.
|
||||
* @summary Маппер из CustomFieldDto в доменную модель CustomField.
|
||||
*/
|
||||
fun CustomFieldDto.toDomain(): CustomField {
|
||||
return CustomField(
|
||||
@@ -32,3 +34,4 @@ fun CustomFieldDto.toDomain(): CustomField {
|
||||
type = this.type
|
||||
)
|
||||
}
|
||||
// [END_ENTITY: Function('toDomain')]
|
||||
@@ -8,14 +8,11 @@ package com.homebox.lens.data.api.dto
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import com.homebox.lens.domain.model.GroupStatistics
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [CORE-LOGIC]
|
||||
// [ENTITY: DataClass('GroupStatisticsDto')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* DTO для статистики.
|
||||
* [COHERENCE_NOTE] Этот DTO был исправлен, чтобы точно соответствовать JSON-ответу от сервера.
|
||||
* Поля `items`, `labels`, `locations`, `totalValue` были заменены на `totalItems`, `totalLabels`,
|
||||
* `totalLocations`, `totalItemPrice` и т.д., чтобы устранить ошибку парсинга `JsonDataException`.
|
||||
* @summary DTO для статистики.
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class GroupStatisticsDto(
|
||||
@@ -23,19 +20,17 @@ data class GroupStatisticsDto(
|
||||
@Json(name = "totalLabels") val totalLabels: Int,
|
||||
@Json(name = "totalLocations") val totalLocations: Int,
|
||||
@Json(name = "totalItemPrice") val totalItemPrice: Double,
|
||||
// [FIX] Добавляем недостающие поля, которые присутствуют в JSON, но отсутствовали в DTO.
|
||||
// Делаем их nullable на случай, если API перестанет их присылать в будущем.
|
||||
@Json(name = "totalUsers") val totalUsers: Int? = null,
|
||||
@Json(name = "totalWithWarranty") val totalWithWarranty: Int? = null
|
||||
)
|
||||
// [END_ENTITY: DataClass('GroupStatisticsDto')]
|
||||
|
||||
// [ENTITY: Function('toDomain')]
|
||||
// [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('GroupStatistics')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Маппер из GroupStatisticsDto в доменную модель GroupStatistics.
|
||||
* [COHERENCE_NOTE] Маппер обновлен для использования правильных полей из исправленного DTO.
|
||||
* @summary Маппер из GroupStatisticsDto в доменную модель GroupStatistics.
|
||||
*/
|
||||
fun GroupStatisticsDto.toDomain(): GroupStatistics {
|
||||
// [ACTION] Маппим данные из DTO в доменную модель.
|
||||
return GroupStatistics(
|
||||
items = this.totalItems,
|
||||
labels = this.totalLabels,
|
||||
@@ -43,4 +38,5 @@ fun GroupStatisticsDto.toDomain(): GroupStatistics {
|
||||
totalValue = this.totalItemPrice
|
||||
)
|
||||
}
|
||||
// [END_ENTITY: Function('toDomain')]
|
||||
// [END_FILE_GroupStatisticsDto.kt]
|
||||
@@ -8,14 +8,14 @@ package com.homebox.lens.data.api.dto
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import com.homebox.lens.domain.model.Image
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [CORE-LOGIC]
|
||||
// [ENTITY: DataClass('ImageDto')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* DTO для изображения.
|
||||
* @property id Уникальный идентификатор.
|
||||
* @property path Путь к файлу.
|
||||
* @property isPrimary Является ли основным.
|
||||
* @summary DTO для изображения.
|
||||
* @param id Уникальный идентификатор.
|
||||
* @param path Путь к файлу.
|
||||
* @param isPrimary Является ли основным.
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ImageDto(
|
||||
@@ -23,10 +23,12 @@ data class ImageDto(
|
||||
@Json(name = "path") val path: String,
|
||||
@Json(name = "isPrimary") val isPrimary: Boolean
|
||||
)
|
||||
// [END_ENTITY: DataClass('ImageDto')]
|
||||
|
||||
// [ENTITY: Function('toDomain')]
|
||||
// [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('Image')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Маппер из ImageDto в доменную модель Image.
|
||||
* @summary Маппер из ImageDto в доменную модель Image.
|
||||
*/
|
||||
fun ImageDto.toDomain(): Image {
|
||||
return Image(
|
||||
@@ -35,3 +37,4 @@ fun ImageDto.toDomain(): Image {
|
||||
isPrimary = this.isPrimary
|
||||
)
|
||||
}
|
||||
// [END_ENTITY: Function('toDomain')]
|
||||
@@ -8,11 +8,11 @@ package com.homebox.lens.data.api.dto
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import com.homebox.lens.domain.model.ItemAttachment
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [CORE-LOGIC]
|
||||
// [ENTITY: DataClass('ItemAttachmentDto')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* DTO для вложения.
|
||||
* @summary DTO для вложения.
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ItemAttachmentDto(
|
||||
@@ -23,10 +23,12 @@ data class ItemAttachmentDto(
|
||||
@Json(name = "createdAt") val createdAt: String,
|
||||
@Json(name = "updatedAt") val updatedAt: String
|
||||
)
|
||||
// [END_ENTITY: DataClass('ItemAttachmentDto')]
|
||||
|
||||
// [ENTITY: Function('toDomain')]
|
||||
// [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('ItemAttachment')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Маппер из ItemAttachmentDto в доменную модель ItemAttachment.
|
||||
* @summary Маппер из ItemAttachmentDto в доменную модель ItemAttachment.
|
||||
*/
|
||||
fun ItemAttachmentDto.toDomain(): ItemAttachment {
|
||||
return ItemAttachment(
|
||||
@@ -38,3 +40,4 @@ fun ItemAttachmentDto.toDomain(): ItemAttachment {
|
||||
updatedAt = this.updatedAt
|
||||
)
|
||||
}
|
||||
// [END_ENTITY: Function('toDomain')]
|
||||
@@ -8,11 +8,11 @@ package com.homebox.lens.data.api.dto
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import com.homebox.lens.domain.model.ItemCreate
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [CORE-LOGIC]
|
||||
// [ENTITY: DataClass('ItemCreateDto')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* DTO для создания вещи.
|
||||
* @summary DTO для создания вещи.
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ItemCreateDto(
|
||||
@@ -30,10 +30,12 @@ data class ItemCreateDto(
|
||||
@Json(name = "parentId") val parentId: String?,
|
||||
@Json(name = "labelIds") val labelIds: List<String>?
|
||||
)
|
||||
// [END_ENTITY: DataClass('ItemCreateDto')]
|
||||
|
||||
// [ENTITY: Function('toDto')]
|
||||
// [RELATION: Function('toDto')] -> [RETURNS] -> [DataClass('ItemCreateDto')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Маппер из доменной модели ItemCreate в ItemCreateDto.
|
||||
* @summary Маппер из доменной модели ItemCreate в ItemCreateDto.
|
||||
*/
|
||||
fun ItemCreate.toDto(): ItemCreateDto {
|
||||
return ItemCreateDto(
|
||||
@@ -52,3 +54,4 @@ fun ItemCreate.toDto(): ItemCreateDto {
|
||||
labelIds = this.labelIds
|
||||
)
|
||||
}
|
||||
// [END_ENTITY: Function('toDto')]
|
||||
@@ -1,16 +1,19 @@
|
||||
// [PACKAGE] com.homebox.lens.data.api.dto
|
||||
// [FILE] ItemDto.kt
|
||||
|
||||
// [SEMANTICS] data, dto, api
|
||||
package com.homebox.lens.data.api.dto
|
||||
|
||||
// [IMPORTS]
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import java.math.BigDecimal
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [CONTRACT]
|
||||
// [ENTITY: DataClass('ItemOut')]
|
||||
// [RELATION: DataClass('ItemOut')] -> [DEPENDS_ON] -> [DataClass('LocationOut')]
|
||||
// [RELATION: DataClass('ItemOut')] -> [DEPENDS_ON] -> [DataClass('LabelOutDto')]
|
||||
/**
|
||||
* [ENTITY: DataClass('ItemOut')]
|
||||
* [PURPOSE] DTO для полной информации о вещи (GET /v1/items/{id}).
|
||||
* @summary DTO для полной информации о вещи (GET /v1/items/{id}).
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ItemOut(
|
||||
@@ -19,14 +22,16 @@ data class ItemOut(
|
||||
@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 = "labels") val labels: List<LabelOutDto>,
|
||||
@Json(name = "value") val value: BigDecimal?,
|
||||
@Json(name = "createdAt") val createdAt: String?
|
||||
)
|
||||
// [END_ENTITY: DataClass('ItemOut')]
|
||||
|
||||
// [ENTITY: DataClass('ItemSummary')]
|
||||
// [RELATION: DataClass('ItemSummary')] -> [DEPENDS_ON] -> [DataClass('LocationOut')]
|
||||
/**
|
||||
* [ENTITY: DataClass('ItemSummary')]
|
||||
* [PURPOSE] DTO для краткой информации о вещи в списках (GET /v1/items).
|
||||
* @summary DTO для краткой информации о вещи в списках (GET /v1/items).
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ItemSummary(
|
||||
@@ -36,10 +41,11 @@ data class ItemSummary(
|
||||
@Json(name = "location") val location: LocationOut?,
|
||||
@Json(name = "createdAt") val createdAt: String?
|
||||
)
|
||||
// [END_ENTITY: DataClass('ItemSummary')]
|
||||
|
||||
// [ENTITY: DataClass('ItemCreate')]
|
||||
/**
|
||||
* [ENTITY: DataClass('ItemCreate')]
|
||||
* [PURPOSE] DTO для создания новой вещи (POST /v1/items).
|
||||
* @summary DTO для создания новой вещи (POST /v1/items).
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ItemCreate(
|
||||
@@ -49,10 +55,11 @@ data class ItemCreate(
|
||||
@Json(name = "labelIds") val labelIds: List<String>?,
|
||||
@Json(name = "value") val value: BigDecimal?
|
||||
)
|
||||
// [END_ENTITY: DataClass('ItemCreate')]
|
||||
|
||||
// [ENTITY: DataClass('ItemUpdate')]
|
||||
/**
|
||||
* [ENTITY: DataClass('ItemUpdate')]
|
||||
* [PURPOSE] DTO для обновления вещи (PUT /v1/items/{id}).
|
||||
* @summary DTO для обновления вещи (PUT /v1/items/{id}).
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ItemUpdate(
|
||||
@@ -62,5 +69,6 @@ data class ItemUpdate(
|
||||
@Json(name = "labelIds") val labelIds: List<String>?,
|
||||
@Json(name = "value") val value: BigDecimal?
|
||||
)
|
||||
// [END_ENTITY: DataClass('ItemUpdate')]
|
||||
|
||||
// [END_FILE_ItemDto.kt]
|
||||
|
||||
@@ -8,11 +8,11 @@ package com.homebox.lens.data.api.dto
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import com.homebox.lens.domain.model.ItemOut
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [CORE-LOGIC]
|
||||
// [ENTITY: DataClass('ItemOutDto')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* DTO для полной модели вещи.
|
||||
* @summary DTO для полной модели вещи.
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ItemOutDto(
|
||||
@@ -37,12 +37,25 @@ data class ItemOutDto(
|
||||
@Json(name = "fields") val fields: List<CustomFieldDto>,
|
||||
@Json(name = "maintenance") val maintenance: List<MaintenanceEntryDto>,
|
||||
@Json(name = "createdAt") val createdAt: String,
|
||||
@Json(name = "updatedAt") val updatedAt: String
|
||||
@Json(name = "updatedAt") val updatedAt: String,
|
||||
@Json(name = "insured") val insured: Boolean?,
|
||||
@Json(name = "lifetimeWarranty") val lifetimeWarranty: Boolean?,
|
||||
@Json(name = "manufacturer") val manufacturer: String?,
|
||||
@Json(name = "modelNumber") val modelNumber: String?,
|
||||
@Json(name = "purchaseFrom") val purchaseFrom: String?,
|
||||
@Json(name = "soldNotes") val soldNotes: String?,
|
||||
@Json(name = "soldPrice") val soldPrice: Double?,
|
||||
@Json(name = "soldTime") val soldTime: String?,
|
||||
@Json(name = "soldTo") val soldTo: String?,
|
||||
@Json(name = "syncChildItemsLocations") val syncChildItemsLocations: Boolean?,
|
||||
@Json(name = "warrantyDetails") val warrantyDetails: String?
|
||||
)
|
||||
// [END_ENTITY: DataClass('ItemOutDto')]
|
||||
|
||||
// [ENTITY: Function('toDomain')]
|
||||
// [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('ItemOut')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Маппер из ItemOutDto в доменную модель ItemOut.
|
||||
* @summary Маппер из ItemOutDto в доменную модель ItemOut.
|
||||
*/
|
||||
fun ItemOutDto.toDomain(): ItemOut {
|
||||
return ItemOut(
|
||||
@@ -67,6 +80,18 @@ fun ItemOutDto.toDomain(): ItemOut {
|
||||
fields = this.fields.map { it.toDomain() },
|
||||
maintenance = this.maintenance.map { it.toDomain() },
|
||||
createdAt = this.createdAt,
|
||||
updatedAt = this.updatedAt
|
||||
updatedAt = this.updatedAt,
|
||||
insured = this.insured,
|
||||
lifetimeWarranty = this.lifetimeWarranty,
|
||||
manufacturer = this.manufacturer,
|
||||
modelNumber = this.modelNumber,
|
||||
purchaseFrom = this.purchaseFrom,
|
||||
soldNotes = this.soldNotes,
|
||||
soldPrice = this.soldPrice,
|
||||
soldTime = this.soldTime,
|
||||
soldTo = this.soldTo,
|
||||
syncChildItemsLocations = this.syncChildItemsLocations,
|
||||
warrantyDetails = this.warrantyDetails
|
||||
)
|
||||
}
|
||||
// [END_ENTITY: Function('toDomain')]
|
||||
@@ -8,11 +8,11 @@ package com.homebox.lens.data.api.dto
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import com.homebox.lens.domain.model.ItemSummary
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [CORE-LOGIC]
|
||||
// [ENTITY: DataClass('ItemSummaryDto')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* DTO для сокращенной модели вещи.
|
||||
* @summary DTO для сокращенной модели вещи.
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ItemSummaryDto(
|
||||
@@ -27,10 +27,12 @@ data class ItemSummaryDto(
|
||||
@Json(name = "createdAt") val createdAt: String,
|
||||
@Json(name = "updatedAt") val updatedAt: String
|
||||
)
|
||||
// [END_ENTITY: DataClass('ItemSummaryDto')]
|
||||
|
||||
// [ENTITY: Function('toDomain')]
|
||||
// [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('ItemSummary')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Маппер из ItemSummaryDto в доменную модель ItemSummary.
|
||||
* @summary Маппер из ItemSummaryDto в доменную модель ItemSummary.
|
||||
*/
|
||||
fun ItemSummaryDto.toDomain(): ItemSummary {
|
||||
return ItemSummary(
|
||||
@@ -46,3 +48,4 @@ fun ItemSummaryDto.toDomain(): ItemSummary {
|
||||
updatedAt = this.updatedAt
|
||||
)
|
||||
}
|
||||
// [END_ENTITY: Function('toDomain')]
|
||||
@@ -8,11 +8,11 @@ package com.homebox.lens.data.api.dto
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import com.homebox.lens.domain.model.ItemUpdate
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [CORE-LOGIC]
|
||||
// [ENTITY: DataClass('ItemUpdateDto')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* DTO для обновления вещи.
|
||||
* @summary DTO для обновления вещи.
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ItemUpdateDto(
|
||||
@@ -31,10 +31,12 @@ data class ItemUpdateDto(
|
||||
@Json(name = "parentId") val parentId: String?,
|
||||
@Json(name = "labelIds") val labelIds: List<String>?
|
||||
)
|
||||
// [END_ENTITY: DataClass('ItemUpdateDto')]
|
||||
|
||||
// [ENTITY: Function('toDto')]
|
||||
// [RELATION: Function('toDto')] -> [RETURNS] -> [DataClass('ItemUpdateDto')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Маппер из доменной модели ItemUpdate в ItemUpdateDto.
|
||||
* @summary Маппер из доменной модели ItemUpdate в ItemUpdateDto.
|
||||
*/
|
||||
fun ItemUpdate.toDto(): ItemUpdateDto {
|
||||
return ItemUpdateDto(
|
||||
@@ -54,3 +56,4 @@ fun ItemUpdate.toDto(): ItemUpdateDto {
|
||||
labelIds = this.labelIds
|
||||
)
|
||||
}
|
||||
// [END_ENTITY: Function('toDto')]
|
||||
@@ -0,0 +1,25 @@
|
||||
// [PACKAGE] com.homebox.lens.data.api.dto
|
||||
// [FILE] LabelCreateDto.kt
|
||||
// [SEMANTICS] data_transfer_object, label, create, api
|
||||
package com.homebox.lens.data.api.dto
|
||||
|
||||
// [IMPORTS]
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [ENTITY: DataClass('LabelCreateDto')]
|
||||
/**
|
||||
* @summary DTO для тела запроса на создание метки (POST /v1/labels).
|
||||
* @param name Название метки.
|
||||
* @param color Цвет метки в формате HEX (например, "#FF0000").
|
||||
* @param description Описание метки.
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class LabelCreateDto(
|
||||
@Json(name = "name") val name: String,
|
||||
@Json(name = "color") val color: String?,
|
||||
@Json(name = "description") val description: String? = null // [AI_NOTE]: Описание не используется в приложении, но может быть в API
|
||||
)
|
||||
// [END_ENTITY: DataClass('LabelCreateDto')]
|
||||
// [END_FILE_LabelCreateDto.kt]
|
||||
@@ -1,20 +0,0 @@
|
||||
// [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]
|
||||
@@ -8,44 +8,38 @@ package com.homebox.lens.data.api.dto
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import com.homebox.lens.domain.model.LabelOut
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [CORE-LOGIC]
|
||||
// [ENTITY: DataClass('LabelOutDto')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* DTO для метки.
|
||||
* [COHERENCE_NOTE] Поле `isArchived` сделано nullable (`Boolean?`),
|
||||
* так как оно отсутствует в JSON-ответе от сервера. Это исправляет ошибку парсинга
|
||||
* `JsonDataException: Required value 'isArchived' missing`.
|
||||
* @summary DTO для метки.
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class LabelOutDto(
|
||||
@Json(name = "id") val id: String,
|
||||
@Json(name = "name") val name: String,
|
||||
// [COHERENCE_NOTE] Поле `color` может быть null или отсутствовать, делаем его nullable для безопасности.
|
||||
@Json(name = "color") val color: String?,
|
||||
// [FIX] Поле отсутствует в JSON, делаем nullable.
|
||||
@Json(name = "isArchived") val isArchived: Boolean?,
|
||||
@Json(name = "createdAt") val createdAt: String,
|
||||
@Json(name = "updatedAt") val updatedAt: String,
|
||||
// [COHERENCE_NOTE] Поле `description` также может отсутствовать, добавляем его как nullable.
|
||||
@Json(name = "description") val description: String?
|
||||
)
|
||||
// [END_ENTITY: DataClass('LabelOutDto')]
|
||||
|
||||
// [ENTITY: Function('toDomain')]
|
||||
// [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('LabelOut')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Маппер из LabelOutDto в доменную модель LabelOut.
|
||||
* [COHERENCE_NOTE] Маппер обновлен для безопасной обработки nullable полей
|
||||
* и предоставления non-nullable значений по умолчанию для доменной модели.
|
||||
* @summary Маппер из LabelOutDto в доменную модель LabelOut.
|
||||
*/
|
||||
fun LabelOutDto.toDomain(): LabelOut {
|
||||
return LabelOut(
|
||||
id = this.id,
|
||||
name = this.name,
|
||||
// [FIX] Используем Elvis-оператор для предоставления значения по умолчанию.
|
||||
color = this.color ?: "", // Пустая строка как дефолтный цвет
|
||||
isArchived = this.isArchived ?: false, // `false` как дефолтное состояние архива
|
||||
color = this.color ?: "",
|
||||
isArchived = this.isArchived ?: false,
|
||||
createdAt = this.createdAt,
|
||||
updatedAt = this.updatedAt
|
||||
)
|
||||
}
|
||||
// [END_ENTITY: Function('toDomain')]
|
||||
// [END_FILE_LabelOutDto.kt]
|
||||
@@ -0,0 +1,42 @@
|
||||
// [PACKAGE] com.homebox.lens.data.api.dto
|
||||
// [FILE] LabelSummaryDto.kt
|
||||
// [SEMANTICS] data_transfer_object, label, summary, api, mapper
|
||||
package com.homebox.lens.data.api.dto
|
||||
|
||||
// [IMPORTS]
|
||||
import com.homebox.lens.domain.model.LabelSummary
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [ENTITY: DataClass('LabelSummaryDto')]
|
||||
/**
|
||||
* @summary DTO для ответа от API при создании метки.
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class LabelSummaryDto(
|
||||
@Json(name = "id") val id: String,
|
||||
@Json(name = "name") val name: String,
|
||||
@Json(name = "color") val color: String?,
|
||||
@Json(name = "description") val description: String?,
|
||||
@Json(name = "createdAt") val createdAt: String?,
|
||||
@Json(name = "updatedAt") val updatedAt: String?
|
||||
)
|
||||
// [END_ENTITY: DataClass('LabelSummaryDto')]
|
||||
|
||||
// [ENTITY: Function('toDomain')]
|
||||
// [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('LabelSummary')]
|
||||
/**
|
||||
* @summary Маппер из DTO в доменную модель.
|
||||
* @return Объект доменной модели [LabelSummary].
|
||||
* @sideeffect Отбрасывает поля, ненужные доменному слою (`color`, `description`, etc.),
|
||||
* оставляя только `id` и `name`.
|
||||
*/
|
||||
fun LabelSummaryDto.toDomain(): LabelSummary {
|
||||
return LabelSummary(
|
||||
id = this.id,
|
||||
name = this.name
|
||||
)
|
||||
}
|
||||
// [END_ENTITY: Function('toDomain')]
|
||||
// [END_FILE_LabelSummaryDto.kt]
|
||||
@@ -0,0 +1,31 @@
|
||||
// [PACKAGE] com.homebox.lens.data.api.dto
|
||||
// [FILE] LabelUpdateDto.kt
|
||||
// [SEMANTICS] data_transfer_object, label, update
|
||||
package com.homebox.lens.data.api.dto
|
||||
|
||||
// [IMPORTS]
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import com.homebox.lens.domain.model.LabelUpdate
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [ENTITY: DataClass('LabelUpdateDto')]
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class LabelUpdateDto(
|
||||
@Json(name = "name")
|
||||
val name: String?,
|
||||
@Json(name = "color")
|
||||
val color: String?
|
||||
)
|
||||
// [END_ENTITY: DataClass('LabelUpdateDto')]
|
||||
|
||||
// [ENTITY: Function('toDto')]
|
||||
// [RELATION: Function('toDto')] -> [RETURNS] -> [DataClass('LabelUpdateDto')]
|
||||
fun LabelUpdate.toDto(): LabelUpdateDto {
|
||||
return LabelUpdateDto(
|
||||
name = this.name,
|
||||
color = this.color
|
||||
)
|
||||
}
|
||||
// [END_ENTITY: Function('toDto')]
|
||||
// [END_FILE_LabelUpdateDto.kt]
|
||||
@@ -0,0 +1,22 @@
|
||||
// [PACKAGE] com.homebox.lens.data.api.dto
|
||||
// [FILE] LocationCreateDto.kt
|
||||
// [SEMANTICS] data_transfer_object, location, create
|
||||
package com.homebox.lens.data.api.dto
|
||||
|
||||
// [IMPORTS]
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [ENTITY: DataClass('LocationCreateDto')]
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class LocationCreateDto(
|
||||
@Json(name = "name")
|
||||
val name: String,
|
||||
@Json(name = "color")
|
||||
val color: String?,
|
||||
@Json(name = "description")
|
||||
val description: String? // Assuming description can be null for creation
|
||||
)
|
||||
// [END_ENTITY: DataClass('LocationCreateDto')]
|
||||
// [END_FILE_LocationCreateDto.kt]
|
||||
@@ -1,25 +1,27 @@
|
||||
// [PACKAGE] com.homebox.lens.data.api.dto
|
||||
// [FILE] LocationDto.kt
|
||||
|
||||
// [SEMANTICS] data, dto, api, location
|
||||
package com.homebox.lens.data.api.dto
|
||||
|
||||
// [IMPORTS]
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [CONTRACT]
|
||||
// [ENTITY: DataClass('LocationOut')]
|
||||
/**
|
||||
* [ENTITY: DataClass('LocationOut')]
|
||||
* [PURPOSE] DTO для информации о местоположении.
|
||||
* @summary DTO для информации о местоположении.
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class LocationOut(
|
||||
@Json(name = "id") val id: String,
|
||||
@Json(name = "name") val name: String
|
||||
)
|
||||
// [END_ENTITY: DataClass('LocationOut')]
|
||||
|
||||
// [ENTITY: DataClass('LocationOutCount')]
|
||||
/**
|
||||
* [ENTITY: DataClass('LocationOutCount')]
|
||||
* [PURPOSE] DTO для информации о местоположении со счетчиком вещей.
|
||||
* @summary DTO для информации о местоположении со счетчиком вещей.
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class LocationOutCount(
|
||||
@@ -27,5 +29,6 @@ data class LocationOutCount(
|
||||
@Json(name = "name") val name: String,
|
||||
@Json(name = "itemCount") val itemCount: Int
|
||||
)
|
||||
// [END_ENTITY: DataClass('LocationOutCount')]
|
||||
|
||||
// [END_FILE_LocationDto.kt]
|
||||
@@ -8,47 +8,40 @@ package com.homebox.lens.data.api.dto
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import com.homebox.lens.domain.model.LocationOutCount
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [CORE-LOGIC]
|
||||
// [ENTITY: DataClass('LocationOutCountDto')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* DTO для местоположения со счетчиком.
|
||||
* [COHERENCE_NOTE] Поля `color` и `isArchived` сделаны nullable (`String?`, `Boolean?`),
|
||||
* так как они отсутствуют в JSON-ответе от сервера. Это исправляет ошибку парсинга
|
||||
* `JsonDataException: Required value '...' missing`.
|
||||
* @summary DTO для местоположения со счетчиком.
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class LocationOutCountDto(
|
||||
@Json(name = "id") val id: String,
|
||||
@Json(name = "name") val name: String,
|
||||
// [FIX] Поле отсутствует в JSON, делаем nullable.
|
||||
@Json(name = "color") val color: String?,
|
||||
// [FIX] Поле отсутствует в JSON, делаем nullable.
|
||||
@Json(name = "isArchived") val isArchived: Boolean?,
|
||||
@Json(name = "itemCount") val itemCount: Int,
|
||||
@Json(name = "createdAt") val createdAt: String,
|
||||
@Json(name = "updatedAt") val updatedAt: String,
|
||||
// [COHERENCE_NOTE] Поле `description` также может отсутствовать или быть null,
|
||||
// поэтому его тоже безопасно сделать nullable.
|
||||
@Json(name = "description") val description: String?
|
||||
)
|
||||
// [END_ENTITY: DataClass('LocationOutCountDto')]
|
||||
|
||||
// [ENTITY: Function('toDomain')]
|
||||
// [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('LocationOutCount')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Маппер из LocationOutCountDto в доменную модель LocationOutCount.
|
||||
* [COHERENCE_NOTE] Маппер обновлен для безопасной обработки nullable полей
|
||||
* и предоставления non-nullable значений по умолчанию для доменной модели.
|
||||
* @summary Маппер из LocationOutCountDto в доменную модель LocationOutCount.
|
||||
*/
|
||||
fun LocationOutCountDto.toDomain(): LocationOutCount {
|
||||
return LocationOutCount(
|
||||
id = this.id,
|
||||
name = this.name,
|
||||
// [FIX] Используем Elvis-оператор для предоставления значения по умолчанию, если поле null.
|
||||
color = this.color ?: "", // Пустая строка как дефолтный цвет
|
||||
isArchived = this.isArchived ?: false, // `false` как дефолтное состояние архива
|
||||
color = this.color ?: "",
|
||||
isArchived = this.isArchived ?: false,
|
||||
itemCount = this.itemCount,
|
||||
createdAt = this.createdAt,
|
||||
updatedAt = this.updatedAt
|
||||
)
|
||||
}
|
||||
// [END_ENTITY: Function('toDomain')]
|
||||
// [END_FILE_LocationOutCountDto.kt]
|
||||
@@ -1,33 +1,34 @@
|
||||
// [PACKAGE] com.homebox.lens.data.api.dto
|
||||
// [FILE] LocationOutDto.kt
|
||||
// [SEMANTICS] data_transfer_object, location
|
||||
|
||||
// [SEMANTICS] data_transfer_object, location, output
|
||||
package com.homebox.lens.data.api.dto
|
||||
|
||||
// [IMPORTS]
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import com.homebox.lens.domain.model.LocationOut
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [CORE-LOGIC]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* DTO для местоположения.
|
||||
*/
|
||||
// [ENTITY: DataClass('LocationOutDto')]
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class LocationOutDto(
|
||||
@Json(name = "id") val id: String,
|
||||
@Json(name = "name") val name: String,
|
||||
@Json(name = "color") val color: String,
|
||||
@Json(name = "isArchived") val isArchived: Boolean,
|
||||
@Json(name = "createdAt") val createdAt: String,
|
||||
@Json(name = "updatedAt") val updatedAt: String
|
||||
@Json(name = "id")
|
||||
val id: String,
|
||||
@Json(name = "name")
|
||||
val name: String,
|
||||
@Json(name = "color")
|
||||
val color: String,
|
||||
@Json(name = "isArchived")
|
||||
val isArchived: Boolean,
|
||||
@Json(name = "createdAt")
|
||||
val createdAt: String,
|
||||
@Json(name = "updatedAt")
|
||||
val updatedAt: String
|
||||
)
|
||||
// [END_ENTITY: DataClass('LocationOutDto')]
|
||||
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Маппер из LocationOutDto в доменную модель LocationOut.
|
||||
*/
|
||||
// [ENTITY: Function('toDomain')]
|
||||
// [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('LocationOut')]
|
||||
fun LocationOutDto.toDomain(): LocationOut {
|
||||
return LocationOut(
|
||||
id = this.id,
|
||||
@@ -38,3 +39,5 @@ fun LocationOutDto.toDomain(): LocationOut {
|
||||
updatedAt = this.updatedAt
|
||||
)
|
||||
}
|
||||
// [END_ENTITY: Function('toDomain')]
|
||||
// [END_FILE_LocationOutDto.kt]
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
// [PACKAGE] com.homebox.lens.data.api.dto
|
||||
// [FILE] LocationUpdateDto.kt
|
||||
// [SEMANTICS] data_transfer_object, location, update
|
||||
package com.homebox.lens.data.api.dto
|
||||
|
||||
// [IMPORTS]
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import com.homebox.lens.domain.model.LocationUpdate
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [ENTITY: DataClass('LocationUpdateDto')]
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class LocationUpdateDto(
|
||||
@Json(name = "name")
|
||||
val name: String?,
|
||||
@Json(name = "color")
|
||||
val color: String?
|
||||
)
|
||||
// [END_ENTITY: DataClass('LocationUpdateDto')]
|
||||
|
||||
// [ENTITY: Function('toDto')]
|
||||
// [RELATION: Function('toDto')] -> [RETURNS] -> [DataClass('LocationUpdateDto')]
|
||||
fun LocationUpdate.toDto(): LocationUpdateDto {
|
||||
return LocationUpdateDto(
|
||||
name = this.name,
|
||||
color = this.color
|
||||
)
|
||||
}
|
||||
// [END_ENTITY: Function('toDto')]
|
||||
// [END_FILE_LocationUpdateDto.kt]
|
||||
@@ -1,15 +1,19 @@
|
||||
// [PACKAGE] com.homebox.lens.data.api.dto
|
||||
// [FILE] LoginFormDto.kt
|
||||
|
||||
// [SEMANTICS] data, dto, api, login
|
||||
package com.homebox.lens.data.api.dto
|
||||
|
||||
// [IMPORTS]
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [ENTITY: DataClass('LoginFormDto')]
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class LoginFormDto(
|
||||
@Json(name = "username") val username: String,
|
||||
@Json(name = "password") val password: String,
|
||||
@Json(name = "stayLoggedIn") val stayLoggedIn: Boolean = true
|
||||
)
|
||||
// [END_ENTITY: DataClass('LoginFormDto')]
|
||||
// [END_FILE_LoginFormDto.kt]
|
||||
@@ -8,11 +8,11 @@ package com.homebox.lens.data.api.dto
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import com.homebox.lens.domain.model.MaintenanceEntry
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [CORE-LOGIC]
|
||||
// [ENTITY: DataClass('MaintenanceEntryDto')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* DTO для записи об обслуживании.
|
||||
* @summary DTO для записи об обслуживании.
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class MaintenanceEntryDto(
|
||||
@@ -25,10 +25,12 @@ data class MaintenanceEntryDto(
|
||||
@Json(name = "createdAt") val createdAt: String,
|
||||
@Json(name = "updatedAt") val updatedAt: String
|
||||
)
|
||||
// [END_ENTITY: DataClass('MaintenanceEntryDto')]
|
||||
|
||||
// [ENTITY: Function('toDomain')]
|
||||
// [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('MaintenanceEntry')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Маппер из MaintenanceEntryDto в доменную модель MaintenanceEntry.
|
||||
* @summary Маппер из MaintenanceEntryDto в доменную модель MaintenanceEntry.
|
||||
*/
|
||||
fun MaintenanceEntryDto.toDomain(): MaintenanceEntry {
|
||||
return MaintenanceEntry(
|
||||
@@ -42,3 +44,4 @@ fun MaintenanceEntryDto.toDomain(): MaintenanceEntry {
|
||||
updatedAt = this.updatedAt
|
||||
)
|
||||
}
|
||||
// [END_ENTITY: Function('toDomain')]
|
||||
@@ -1,15 +1,16 @@
|
||||
// [PACKAGE] com.homebox.lens.data.api.dto
|
||||
// [FILE] PaginationDto.kt
|
||||
|
||||
// [SEMANTICS] data, dto, api, pagination
|
||||
package com.homebox.lens.data.api.dto
|
||||
|
||||
// [IMPORTS]
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [CONTRACT]
|
||||
// [ENTITY: DataClass('PaginationResult')]
|
||||
/**
|
||||
* [ENTITY: DataClass('PaginationResult')]
|
||||
* [PURPOSE] DTO для пагинированных результатов от API.
|
||||
* @summary DTO для пагинированных результатов от API.
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class PaginationResult<T>(
|
||||
@@ -19,5 +20,6 @@ data class PaginationResult<T>(
|
||||
@Json(name = "total") val total: Int,
|
||||
@Json(name = "pageSize") val pageSize: Int
|
||||
)
|
||||
// [END_ENTITY: DataClass('PaginationResult')]
|
||||
|
||||
// [END_FILE_PaginationDto.kt]
|
||||
@@ -8,11 +8,11 @@ package com.homebox.lens.data.api.dto
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import com.homebox.lens.domain.model.PaginationResult
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [CORE-LOGIC]
|
||||
// [ENTITY: DataClass('PaginationResultDto')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* DTO для постраничных результатов.
|
||||
* @summary DTO для постраничных результатов.
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class PaginationResultDto<T>(
|
||||
@@ -21,10 +21,12 @@ data class PaginationResultDto<T>(
|
||||
@Json(name = "pageSize") val pageSize: Int,
|
||||
@Json(name = "total") val total: Int
|
||||
)
|
||||
// [END_ENTITY: DataClass('PaginationResultDto')]
|
||||
|
||||
// [ENTITY: Function('toDomain')]
|
||||
// [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('PaginationResult')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Маппер из PaginationResultDto в доменную модель PaginationResult.
|
||||
* @summary Маппер из PaginationResultDto в доменную модель PaginationResult.
|
||||
* @param transform Функция для преобразования каждого элемента из DTO в доменную модель.
|
||||
*/
|
||||
fun <T, R> PaginationResultDto<T>.toDomain(transform: (T) -> R): PaginationResult<R> {
|
||||
@@ -35,3 +37,4 @@ fun <T, R> PaginationResultDto<T>.toDomain(transform: (T) -> R): PaginationResul
|
||||
total = this.total
|
||||
)
|
||||
}
|
||||
// [END_ENTITY: Function('toDomain')]
|
||||
@@ -1,16 +1,17 @@
|
||||
// [PACKAGE] com.homebox.lens.data.api.dto
|
||||
// [FILE] StatisticsDto.kt
|
||||
|
||||
// [SEMANTICS] data, dto, api, statistics
|
||||
package com.homebox.lens.data.api.dto
|
||||
|
||||
// [IMPORTS]
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import java.math.BigDecimal
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [CONTRACT]
|
||||
// [ENTITY: DataClass('GroupStatistics')]
|
||||
/**
|
||||
* [ENTITY: DataClass('GroupStatistics')]
|
||||
* [PURPOSE] DTO для статистической информации.
|
||||
* @summary DTO для статистической информации.
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class GroupStatistics(
|
||||
@@ -19,5 +20,6 @@ data class GroupStatistics(
|
||||
@Json(name = "locations") val locations: Int,
|
||||
@Json(name = "labels") val labels: Int
|
||||
)
|
||||
// [END_ENTITY: DataClass('GroupStatistics')]
|
||||
|
||||
// [END_FILE_StatisticsDto.kt]
|
||||
@@ -1,15 +1,19 @@
|
||||
// [PACKAGE] com.homebox.lens.data.api.dto
|
||||
// [FILE] TokenResponseDto.kt
|
||||
|
||||
// [SEMANTICS] data, dto, api, token
|
||||
package com.homebox.lens.data.api.dto
|
||||
|
||||
// [IMPORTS]
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [ENTITY: DataClass('TokenResponseDto')]
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class TokenResponseDto(
|
||||
@Json(name = "token") val token: String,
|
||||
@Json(name = "attachmentToken") val attachmentToken: String,
|
||||
@Json(name = "expiresAt") val expiresAt: String
|
||||
)
|
||||
// [END_ENTITY: DataClass('TokenResponseDto')]
|
||||
// [END_FILE_TokenResponseDto.kt]
|
||||
@@ -4,26 +4,27 @@
|
||||
|
||||
package com.homebox.lens.data.api.mapper
|
||||
|
||||
// [IMPORTS]
|
||||
import com.homebox.lens.data.api.dto.TokenResponseDto
|
||||
import com.homebox.lens.domain.model.TokenResponse
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [ENTITY: Function('toDomain')]
|
||||
// [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('TokenResponse')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* [HELPER] Преобразует DTO-объект токена в доменную модель.
|
||||
* @summary Преобразует DTO-объект токена в доменную модель.
|
||||
* @receiver [TokenResponseDto] объект из слоя данных.
|
||||
* @return [TokenResponse] объект для доменного слоя.
|
||||
* @throws IllegalArgumentException если токен в DTO пустой.
|
||||
*/
|
||||
fun TokenResponseDto.toDomain(): TokenResponse {
|
||||
// [PRECONDITION] DTO должен содержать валидные данные для маппинга.
|
||||
require(this.token.isNotBlank()) { "[PRECONDITION_FAILED] DTO token is blank, cannot map to domain model." }
|
||||
require(this.token.isNotBlank()) { "DTO token is blank, cannot map to domain model." }
|
||||
|
||||
// [ACTION]
|
||||
val domainModel = TokenResponse(token = this.token)
|
||||
|
||||
// [POSTCONDITION] Проверяем, что инвариант доменной модели соблюден.
|
||||
check(domainModel.token.isNotBlank()) { "[POSTCONDITION_FAILED] Domain model token is blank after mapping." }
|
||||
check(domainModel.token.isNotBlank()) { "Domain model token is blank after mapping." }
|
||||
|
||||
return domainModel
|
||||
}
|
||||
// [END_ENTITY: Function('toDomain')]
|
||||
// [END_FILE_TokenMapper.kt]
|
||||
@@ -1,26 +1,32 @@
|
||||
// [PACKAGE] com.homebox.lens.data.db
|
||||
// [FILE] Converters.kt
|
||||
|
||||
// [SEMANTICS] data, database, room, converter
|
||||
package com.homebox.lens.data.db
|
||||
|
||||
// [IMPORTS]
|
||||
import androidx.room.TypeConverter
|
||||
import java.math.BigDecimal
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [CONTRACT]
|
||||
// [ENTITY: Class('Converters')]
|
||||
/**
|
||||
* [ENTITY: Class('Converters')]
|
||||
* [PURPOSE] Предоставляет TypeConverters для Room для типов, не поддерживаемых по умолчанию.
|
||||
* @summary Предоставляет TypeConverters для Room для типов, не поддерживаемых по умолчанию.
|
||||
*/
|
||||
class Converters {
|
||||
// [ENTITY: Function('fromString')]
|
||||
@TypeConverter
|
||||
fun fromString(value: String?): BigDecimal? {
|
||||
return value?.let { BigDecimal(it) }
|
||||
}
|
||||
// [END_ENTITY: Function('fromString')]
|
||||
|
||||
// [ENTITY: Function('bigDecimalToString')]
|
||||
@TypeConverter
|
||||
fun bigDecimalToString(bigDecimal: BigDecimal?): String? {
|
||||
return bigDecimal?.toPlainString()
|
||||
}
|
||||
// [END_ENTITY: Function('bigDecimalToString')]
|
||||
}
|
||||
// [END_ENTITY: Class('Converters')]
|
||||
|
||||
// [END_FILE_Converters.kt]
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
// [PACKAGE] com.homebox.lens.data.db
|
||||
// [FILE] HomeboxDatabase.kt
|
||||
|
||||
// [SEMANTICS] data, database, room
|
||||
package com.homebox.lens.data.db
|
||||
|
||||
// [IMPORTS]
|
||||
import androidx.room.Database
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.TypeConverters
|
||||
@@ -10,11 +11,11 @@ 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.*
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [CONTRACT]
|
||||
// [ENTITY: Database('HomeboxDatabase')]
|
||||
/**
|
||||
* [ENTITY: RoomDatabase('HomeboxDatabase')]
|
||||
* [PURPOSE] Основной класс для работы с локальной базой данных Room.
|
||||
* @summary Основной класс для работы с локальной базой данных Room.
|
||||
*/
|
||||
@Database(
|
||||
entities = [
|
||||
@@ -37,5 +38,6 @@ abstract class HomeboxDatabase : RoomDatabase() {
|
||||
const val DATABASE_NAME = "homebox_lens_db"
|
||||
}
|
||||
}
|
||||
// [END_ENTITY: Database('HomeboxDatabase')]
|
||||
|
||||
// [END_FILE_HomeboxDatabase.kt]
|
||||
@@ -1,40 +1,61 @@
|
||||
// [PACKAGE] com.homebox.lens.data.db.dao
|
||||
// [FILE] ItemDao.kt
|
||||
|
||||
// [SEMANTICS] data, database, dao, item
|
||||
package com.homebox.lens.data.db.dao
|
||||
|
||||
// [IMPORTS]
|
||||
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
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [CONTRACT]
|
||||
// [ENTITY: Interface('ItemDao')]
|
||||
/**
|
||||
* [ENTITY: RoomDao('ItemDao')]
|
||||
* [PURPOSE] Предоставляет методы для работы с 'items' в локальной БД.
|
||||
* @summary Предоставляет методы для работы с 'items' в локальной БД.
|
||||
*/
|
||||
@Dao
|
||||
interface ItemDao {
|
||||
|
||||
// [ENTITY: Function('getRecentlyAddedItems')]
|
||||
@Transaction
|
||||
@Query("SELECT * FROM items ORDER BY createdAt DESC LIMIT :limit")
|
||||
fun getRecentlyAddedItems(limit: Int): Flow<List<ItemWithLabels>>
|
||||
// [END_ENTITY: Function('getRecentlyAddedItems')]
|
||||
|
||||
// [ENTITY: Function('getItems')]
|
||||
@Transaction
|
||||
@Query("SELECT * FROM items")
|
||||
suspend fun getItems(): List<ItemWithLabels>
|
||||
// [END_ENTITY: Function('getItems')]
|
||||
|
||||
// [ENTITY: Function('getItem')]
|
||||
@Transaction
|
||||
@Query("SELECT * FROM items WHERE id = :itemId")
|
||||
suspend fun getItem(itemId: String): ItemWithLabels?
|
||||
// [END_ENTITY: Function('getItem')]
|
||||
|
||||
// [ENTITY: Function('insertItems')]
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertItems(items: List<ItemEntity>)
|
||||
// [END_ENTITY: Function('insertItems')]
|
||||
|
||||
// [ENTITY: Function('insertItem')]
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertItem(item: ItemEntity)
|
||||
// [END_ENTITY: Function('insertItem')]
|
||||
|
||||
// [ENTITY: Function('deleteItem')]
|
||||
@Query("DELETE FROM items WHERE id = :itemId")
|
||||
suspend fun deleteItem(itemId: String)
|
||||
// [END_ENTITY: Function('deleteItem')]
|
||||
|
||||
// [ENTITY: Function('insertItemLabelCrossRefs')]
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertItemLabelCrossRefs(crossRefs: List<ItemLabelCrossRef>)
|
||||
// [END_ENTITY: Function('insertItemLabelCrossRefs')]
|
||||
}
|
||||
// [END_ENTITY: Interface('ItemDao')]
|
||||
|
||||
// [END_FILE_ItemDao.kt]
|
||||
@@ -1,27 +1,42 @@
|
||||
// [PACKAGE] com.homebox.lens.data.db.dao
|
||||
// [FILE] LabelDao.kt
|
||||
|
||||
// [SEMANTICS] data, database, dao, label
|
||||
package com.homebox.lens.data.db.dao
|
||||
|
||||
// [IMPORTS]
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import com.homebox.lens.data.db.entity.LabelEntity
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [CONTRACT]
|
||||
// [ENTITY: Interface('LabelDao')]
|
||||
/**
|
||||
* [ENTITY: RoomDao('LabelDao')]
|
||||
* [PURPOSE] Предоставляет методы для работы с 'labels' в локальной БД.
|
||||
* @summary Предоставляет методы для работы с 'labels' в локальной БД.
|
||||
*/
|
||||
@Dao
|
||||
interface LabelDao {
|
||||
|
||||
// [ENTITY: Function('getLabels')]
|
||||
@Query("SELECT * FROM labels")
|
||||
suspend fun getLabels(): List<LabelEntity>
|
||||
// [END_ENTITY: Function('getLabels')]
|
||||
|
||||
// [ENTITY: Function('insertLabels')]
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertLabels(labels: List<LabelEntity>)
|
||||
// [END_ENTITY: Function('insertLabels')]
|
||||
|
||||
// [ENTITY: Function('deleteLabelById')]
|
||||
/**
|
||||
* @summary Удаляет метку по её ID из локальной БД.
|
||||
* @param labelId ID метки для удаления.
|
||||
*/
|
||||
@Query("DELETE FROM labels WHERE id = :labelId")
|
||||
suspend fun deleteLabelById(labelId: String)
|
||||
// [END_ENTITY: Function('deleteLabelById')]
|
||||
}
|
||||
// [END_ENTITY: Interface('LabelDao')]
|
||||
|
||||
// [END_FILE_LabelDao.kt]
|
||||
|
||||
@@ -1,27 +1,33 @@
|
||||
// [PACKAGE] com.homebox.lens.data.db.dao
|
||||
// [FILE] LocationDao.kt
|
||||
|
||||
// [SEMANTICS] data, database, dao, location
|
||||
package com.homebox.lens.data.db.dao
|
||||
|
||||
// [IMPORTS]
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import com.homebox.lens.data.db.entity.LocationEntity
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [CONTRACT]
|
||||
// [ENTITY: Interface('LocationDao')]
|
||||
/**
|
||||
* [ENTITY: RoomDao('LocationDao')]
|
||||
* [PURPOSE] Предоставляет методы для работы с 'locations' в локальной БД.
|
||||
* @summary Предоставляет методы для работы с 'locations' в локальной БД.
|
||||
*/
|
||||
@Dao
|
||||
interface LocationDao {
|
||||
|
||||
// [ENTITY: Function('getLocations')]
|
||||
@Query("SELECT * FROM locations")
|
||||
suspend fun getLocations(): List<LocationEntity>
|
||||
// [END_ENTITY: Function('getLocations')]
|
||||
|
||||
// [ENTITY: Function('insertLocations')]
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertLocations(locations: List<LocationEntity>)
|
||||
// [END_ENTITY: Function('insertLocations')]
|
||||
}
|
||||
// [END_ENTITY: Interface('LocationDao')]
|
||||
|
||||
// [END_FILE_LocationDao.kt]
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
// [PACKAGE] com.homebox.lens.data.db.entity
|
||||
// [FILE] ItemEntity.kt
|
||||
|
||||
// [SEMANTICS] data, database, entity, item
|
||||
package com.homebox.lens.data.db.entity
|
||||
|
||||
// [IMPORTS]
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import java.math.BigDecimal
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [CONTRACT]
|
||||
// [ENTITY: DatabaseTable('ItemEntity')]
|
||||
/**
|
||||
* [ENTITY: RoomEntity('ItemEntity')]
|
||||
* [PURPOSE] Представляет собой строку в таблице 'items' в локальной БД.
|
||||
* @summary Представляет собой строку в таблице 'items' в локальной БД.
|
||||
*/
|
||||
@Entity(tableName = "items")
|
||||
data class ItemEntity(
|
||||
@@ -22,5 +23,6 @@ data class ItemEntity(
|
||||
val value: BigDecimal?,
|
||||
val createdAt: String?
|
||||
)
|
||||
// [END_ENTITY: DatabaseTable('ItemEntity')]
|
||||
|
||||
// [END_FILE_ItemEntity.kt]
|
||||
@@ -1,15 +1,16 @@
|
||||
// [PACKAGE] com.homebox.lens.data.db.entity
|
||||
// [FILE] ItemLabelCrossRef.kt
|
||||
|
||||
// [SEMANTICS] data, database, entity, relation
|
||||
package com.homebox.lens.data.db.entity
|
||||
|
||||
// [IMPORTS]
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Index
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [CONTRACT]
|
||||
// [ENTITY: DatabaseTable('ItemLabelCrossRef')]
|
||||
/**
|
||||
* [ENTITY: RoomEntity('ItemLabelCrossRef')]
|
||||
* [PURPOSE] Таблица для связи "многие-ко-многим" между ItemEntity и LabelEntity.
|
||||
* @summary Таблица для связи "многие-ко-многим" между ItemEntity и LabelEntity.
|
||||
*/
|
||||
@Entity(
|
||||
primaryKeys = ["itemId", "labelId"],
|
||||
@@ -19,5 +20,6 @@ data class ItemLabelCrossRef(
|
||||
val itemId: String,
|
||||
val labelId: String
|
||||
)
|
||||
// [END_ENTITY: DatabaseTable('ItemLabelCrossRef')]
|
||||
|
||||
// [END_FILE_ItemLabelCrossRef.kt]
|
||||
@@ -1,16 +1,19 @@
|
||||
// [PACKAGE] com.homebox.lens.data.db.entity
|
||||
// [FILE] ItemWithLabels.kt
|
||||
|
||||
// [SEMANTICS] data, database, entity, relation
|
||||
package com.homebox.lens.data.db.entity
|
||||
|
||||
// [IMPORTS]
|
||||
import androidx.room.Embedded
|
||||
import androidx.room.Junction
|
||||
import androidx.room.Relation
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [CONTRACT]
|
||||
// [ENTITY: DataClass('ItemWithLabels')]
|
||||
// [RELATION: DataClass('ItemWithLabels')] -> [DEPENDS_ON] -> [DatabaseTable('ItemEntity')]
|
||||
// [RELATION: DataClass('ItemWithLabels')] -> [DEPENDS_ON] -> [DatabaseTable('LabelEntity')]
|
||||
/**
|
||||
* [ENTITY: Pojo('ItemWithLabels')]
|
||||
* [PURPOSE] POJO для получения ItemEntity вместе со связанными LabelEntity.
|
||||
* @summary POJO для получения ItemEntity вместе со связанными LabelEntity.
|
||||
*/
|
||||
data class ItemWithLabels(
|
||||
@Embedded val item: ItemEntity,
|
||||
@@ -25,5 +28,6 @@ data class ItemWithLabels(
|
||||
)
|
||||
val labels: List<LabelEntity>
|
||||
)
|
||||
// [END_ENTITY: DataClass('ItemWithLabels')]
|
||||
|
||||
// [END_FILE_ItemWithLabels.kt]
|
||||
@@ -1,20 +1,22 @@
|
||||
// [PACKAGE] com.homebox.lens.data.db.entity
|
||||
// [FILE] LabelEntity.kt
|
||||
|
||||
// [SEMANTICS] data, database, entity, label
|
||||
package com.homebox.lens.data.db.entity
|
||||
|
||||
// [IMPORTS]
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [CONTRACT]
|
||||
// [ENTITY: DatabaseTable('LabelEntity')]
|
||||
/**
|
||||
* [ENTITY: RoomEntity('LabelEntity')]
|
||||
* [PURPOSE] Представляет собой строку в таблице 'labels' в локальной БД.
|
||||
* @summary Представляет собой строку в таблице 'labels' в локальной БД.
|
||||
*/
|
||||
@Entity(tableName = "labels")
|
||||
data class LabelEntity(
|
||||
@PrimaryKey val id: String,
|
||||
val name: String
|
||||
)
|
||||
// [END_ENTITY: DatabaseTable('LabelEntity')]
|
||||
|
||||
// [END_FILE_LabelEntity.kt]
|
||||
@@ -1,20 +1,22 @@
|
||||
// [PACKAGE] com.homebox.lens.data.db.entity
|
||||
// [FILE] LocationEntity.kt
|
||||
|
||||
// [SEMANTICS] data, database, entity, location
|
||||
package com.homebox.lens.data.db.entity
|
||||
|
||||
// [IMPORTS]
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [CONTRACT]
|
||||
// [ENTITY: DatabaseTable('LocationEntity')]
|
||||
/**
|
||||
* [ENTITY: RoomEntity('LocationEntity')]
|
||||
* [PURPOSE] Представляет собой строку в таблице 'locations' в локальной БД.
|
||||
* @summary Представляет собой строку в таблице 'locations' в локальной БД.
|
||||
*/
|
||||
@Entity(tableName = "locations")
|
||||
data class LocationEntity(
|
||||
@PrimaryKey val id: String,
|
||||
val name: String
|
||||
)
|
||||
// [END_ENTITY: DatabaseTable('LocationEntity')]
|
||||
|
||||
// [END_FILE_LocationEntity.kt]
|
||||
|
||||
49
data/src/main/java/com/homebox/lens/data/db/entity/Mapper.kt
Normal file
49
data/src/main/java/com/homebox/lens/data/db/entity/Mapper.kt
Normal file
@@ -0,0 +1,49 @@
|
||||
// [PACKAGE] com.homebox.lens.data.db.entity
|
||||
// [FILE] Mapper.kt
|
||||
// [SEMANTICS] data, database, mapper
|
||||
package com.homebox.lens.data.db.entity
|
||||
|
||||
// [IMPORTS]
|
||||
import com.homebox.lens.domain.model.Image
|
||||
import com.homebox.lens.domain.model.ItemSummary
|
||||
import com.homebox.lens.domain.model.LabelOut
|
||||
import com.homebox.lens.domain.model.LocationOut
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [ENTITY: Function('toDomain')]
|
||||
// [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('ItemSummary')]
|
||||
/**
|
||||
* @summary Преобразует [ItemWithLabels] (сущность БД) в [ItemSummary] (доменную модель).
|
||||
*/
|
||||
fun ItemWithLabels.toDomain(): ItemSummary {
|
||||
return ItemSummary(
|
||||
id = this.item.id,
|
||||
name = this.item.name,
|
||||
image = this.item.image?.let { Image(id = "", path = it, isPrimary = true) },
|
||||
location = this.item.locationId?.let { LocationOut(id = it, name = "", color = "", isArchived = false, createdAt = "", updatedAt = "") },
|
||||
labels = this.labels.map { it.toDomain() },
|
||||
assetId = null,
|
||||
isArchived = false,
|
||||
value = this.item.value?.toDouble() ?: 0.0,
|
||||
createdAt = this.item.createdAt ?: "",
|
||||
updatedAt = ""
|
||||
)
|
||||
}
|
||||
// [END_ENTITY: Function('toDomain')]
|
||||
|
||||
// [ENTITY: Function('toDomain')]
|
||||
// [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('LabelOut')]
|
||||
/**
|
||||
* @summary Преобразует [LabelEntity] (сущность БД) в [LabelOut] (доменную модель).
|
||||
*/
|
||||
fun LabelEntity.toDomain(): LabelOut {
|
||||
return LabelOut(
|
||||
id = this.id,
|
||||
name = this.name,
|
||||
color = "#CCCCCC",
|
||||
isArchived = false,
|
||||
createdAt = "",
|
||||
updatedAt = ""
|
||||
)
|
||||
}
|
||||
// [END_ENTITY: Function('toDomain')]
|
||||
@@ -1,7 +1,8 @@
|
||||
// [PACKAGE] com.homebox.lens.data.di
|
||||
// [FILE] ApiModule.kt
|
||||
// [PURPOSE] Предоставляет синглтон-зависимости для работы с сетью, включая OkHttpClient, Retrofit и ApiService.
|
||||
// [SEMANTICS] di, networking
|
||||
package com.homebox.lens.data.di
|
||||
|
||||
// [IMPORTS]
|
||||
import com.homebox.lens.data.api.HomeboxApiService
|
||||
import com.homebox.lens.domain.repository.CredentialsRepository
|
||||
@@ -17,41 +18,34 @@ import okhttp3.OkHttpClient
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.moshi.MoshiConverterFactory
|
||||
import timber.log.Timber
|
||||
import javax.inject.Provider
|
||||
import javax.inject.Singleton
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [ENTITY: Module('ApiModule')]
|
||||
/**
|
||||
* [ENTITY: Module('ApiModule')]
|
||||
* [CONTRACT]
|
||||
* Hilt-модуль, отвечающий за создание и предоставление всех зависимостей,
|
||||
* @summary Hilt-модуль, отвечающий за создание и предоставление всех зависимостей,
|
||||
* необходимых для сетевого взаимодействия.
|
||||
*/
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object ApiModule {
|
||||
|
||||
// [HELPER] Временный базовый URL для API. В будущем должен стать динамическим.
|
||||
private const val BASE_URL = "https://homebox.bebesh.ru/api/"
|
||||
|
||||
/**
|
||||
* [PROVIDER]
|
||||
* [CONTRACT]
|
||||
* Предоставляет сконфигурированный OkHttpClient.
|
||||
* @param credentialsRepositoryProvider Провайдер репозитория для доступа к токену авторизации.
|
||||
* Используется Provider<T> для предотвращения циклов зависимостей.
|
||||
* @return Синглтон-экземпляр OkHttpClient с настроенными перехватчиками.
|
||||
*/
|
||||
// [ENTITY: Function('provideOkHttpClient')]
|
||||
// [RELATION: Function('provideOkHttpClient')] -> [PROVIDES] -> [Framework('OkHttpClient')]
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideOkHttpClient(
|
||||
credentialsRepositoryProvider: Provider<CredentialsRepository>
|
||||
): OkHttpClient {
|
||||
// [ACTION] Создаем перехватчик для логирования.
|
||||
Timber.d("[DEBUG][PROVIDER][providing_okhttp_client] Providing OkHttpClient.")
|
||||
val loggingInterceptor = HttpLoggingInterceptor().apply {
|
||||
level = HttpLoggingInterceptor.Level.BODY
|
||||
}
|
||||
|
||||
// [ACTION] Создаем перехватчик для добавления заголовка 'Accept'.
|
||||
val acceptHeaderInterceptor = Interceptor { chain ->
|
||||
val request = chain.request().newBuilder()
|
||||
.header("Accept", "application/json")
|
||||
@@ -59,77 +53,71 @@ object ApiModule {
|
||||
chain.proceed(request)
|
||||
}
|
||||
|
||||
// [CORE-LOGIC] Создаем перехватчик для добавления токена авторизации.
|
||||
val authInterceptor = Interceptor { chain ->
|
||||
// [HELPER] Получаем токен из репозитория.
|
||||
// runBlocking здесь допустим, т.к. чтение из SharedPreferences - быстрая I/O операция,
|
||||
// а интерфейс Interceptor'а является синхронным.
|
||||
val token = runBlocking { credentialsRepositoryProvider.get().getToken() }
|
||||
|
||||
val requestBuilder = chain.request().newBuilder()
|
||||
|
||||
// [ACTION] Если токен существует, добавляем его в заголовок.
|
||||
if (token != null) {
|
||||
// Сервер ожидает заголовок "Authorization: Bearer <token>"
|
||||
// Предполагается, что `token` уже содержит префикс "Bearer ".
|
||||
requestBuilder.addHeader("Authorization", token)
|
||||
}
|
||||
|
||||
chain.proceed(requestBuilder.build())
|
||||
}
|
||||
|
||||
// [ACTION] Собираем OkHttpClient с правильным порядком перехватчиков.
|
||||
return OkHttpClient.Builder()
|
||||
.addInterceptor(acceptHeaderInterceptor)
|
||||
.addInterceptor(authInterceptor) // Добавляем перехватчик для токена
|
||||
.addInterceptor(loggingInterceptor) // Логирование должно идти последним, чтобы видеть финальный запрос
|
||||
.addInterceptor(authInterceptor)
|
||||
.addInterceptor(loggingInterceptor)
|
||||
.build()
|
||||
}
|
||||
// [END_ENTITY: Function('provideOkHttpClient')]
|
||||
|
||||
/**
|
||||
* [PROVIDER]
|
||||
* [CONTRACT] Предоставляет экземпляр Moshi для парсинга JSON.
|
||||
*/
|
||||
// [ENTITY: Function('provideMoshi')]
|
||||
// [RELATION: Function('provideMoshi')] -> [PROVIDES] -> [Framework('Moshi')]
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideMoshi(): Moshi {
|
||||
Timber.d("[DEBUG][PROVIDER][providing_moshi] Providing Moshi.")
|
||||
return Moshi.Builder()
|
||||
.add(KotlinJsonAdapterFactory())
|
||||
.build()
|
||||
}
|
||||
// [END_ENTITY: Function('provideMoshi')]
|
||||
|
||||
/**
|
||||
* [PROVIDER]
|
||||
* [CONTRACT] Предоставляет фабрику конвертеров для Retrofit.
|
||||
*/
|
||||
// [ENTITY: Function('provideMoshiConverterFactory')]
|
||||
// [RELATION: Function('provideMoshiConverterFactory')] -> [PROVIDES] -> [Framework('MoshiConverterFactory')]
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideMoshiConverterFactory(moshi: Moshi): MoshiConverterFactory {
|
||||
Timber.d("[DEBUG][PROVIDER][providing_moshi_converter] Providing MoshiConverterFactory.")
|
||||
return MoshiConverterFactory.create(moshi)
|
||||
}
|
||||
// [END_ENTITY: Function('provideMoshiConverterFactory')]
|
||||
|
||||
/**
|
||||
* [PROVIDER]
|
||||
* [CONTRACT] Предоставляет сконфигурированный экземпляр Retrofit.
|
||||
*/
|
||||
// [ENTITY: Function('provideRetrofit')]
|
||||
// [RELATION: Function('provideRetrofit')] -> [PROVIDES] -> [Framework('Retrofit')]
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideRetrofit(okHttpClient: OkHttpClient, moshiConverterFactory: MoshiConverterFactory): Retrofit {
|
||||
Timber.d("[DEBUG][PROVIDER][providing_retrofit] Providing Retrofit.")
|
||||
return Retrofit.Builder()
|
||||
.baseUrl(BASE_URL)
|
||||
.client(okHttpClient)
|
||||
.addConverterFactory(moshiConverterFactory)
|
||||
.build()
|
||||
}
|
||||
// [END_ENTITY: Function('provideRetrofit')]
|
||||
|
||||
/**
|
||||
* [PROVIDER]
|
||||
* [CONTRACT] Предоставляет реализацию интерфейса HomeboxApiService.
|
||||
*/
|
||||
// [ENTITY: Function('provideHomeboxApiService')]
|
||||
// [RELATION: Function('provideHomeboxApiService')] -> [PROVIDES] -> [Interface('HomeboxApiService')]
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideHomeboxApiService(retrofit: Retrofit): HomeboxApiService {
|
||||
Timber.d("[DEBUG][PROVIDER][providing_api_service] Providing HomeboxApiService.")
|
||||
return retrofit.create(HomeboxApiService::class.java)
|
||||
}
|
||||
// [END_ENTITY: Function('provideHomeboxApiService')]
|
||||
}
|
||||
// [END_ENTITY: Module('ApiModule')]
|
||||
// [END_FILE_ApiModule.kt]
|
||||
@@ -1,8 +1,9 @@
|
||||
// [PACKAGE] com.homebox.lens.data.di
|
||||
// [FILE] DatabaseModule.kt
|
||||
|
||||
// [SEMANTICS] di, hilt, database
|
||||
package com.homebox.lens.data.di
|
||||
|
||||
// [IMPORTS]
|
||||
import android.content.Context
|
||||
import androidx.room.Room
|
||||
import com.homebox.lens.data.db.HomeboxDatabase
|
||||
@@ -11,40 +12,50 @@ import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import timber.log.Timber
|
||||
import javax.inject.Singleton
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [CONTRACT]
|
||||
// [ENTITY: Module('DatabaseModule')]
|
||||
/**
|
||||
* [MODULE: DaggerHilt('DatabaseModule')]
|
||||
* [PURPOSE] Предоставляет зависимости для работы с базой данных Room.
|
||||
* @summary Предоставляет зависимости для работы с базой данных Room.
|
||||
*/
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object DatabaseModule {
|
||||
|
||||
// [PROVIDER]
|
||||
// [ENTITY: Function('provideHomeboxDatabase')]
|
||||
// [RELATION: Function('provideHomeboxDatabase')] -> [PROVIDES] -> [Database('HomeboxDatabase')]
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideHomeboxDatabase(@ApplicationContext context: Context): HomeboxDatabase {
|
||||
// [ACTION] Build Room database instance
|
||||
Timber.d("[DEBUG][PROVIDER][providing_database] Providing HomeboxDatabase.")
|
||||
return Room.databaseBuilder(
|
||||
context,
|
||||
HomeboxDatabase::class.java,
|
||||
HomeboxDatabase.DATABASE_NAME
|
||||
).build()
|
||||
}
|
||||
// [END_ENTITY: Function('provideHomeboxDatabase')]
|
||||
|
||||
// [PROVIDER]
|
||||
// [ENTITY: Function('provideItemDao')]
|
||||
// [RELATION: Function('provideItemDao')] -> [PROVIDES] -> [Interface('ItemDao')]
|
||||
@Provides
|
||||
fun provideItemDao(database: HomeboxDatabase) = database.itemDao()
|
||||
// [END_ENTITY: Function('provideItemDao')]
|
||||
|
||||
// [PROVIDER]
|
||||
// [ENTITY: Function('provideLabelDao')]
|
||||
// [RELATION: Function('provideLabelDao')] -> [PROVIDES] -> [Interface('LabelDao')]
|
||||
@Provides
|
||||
fun provideLabelDao(database: HomeboxDatabase) = database.labelDao()
|
||||
// [END_ENTITY: Function('provideLabelDao')]
|
||||
|
||||
// [PROVIDER]
|
||||
// [ENTITY: Function('provideLocationDao')]
|
||||
// [RELATION: Function('provideLocationDao')] -> [PROVIDES] -> [Interface('LocationDao')]
|
||||
@Provides
|
||||
fun provideLocationDao(database: HomeboxDatabase) = database.locationDao()
|
||||
// [END_ENTITY: Function('provideLocationDao')]
|
||||
}
|
||||
// [END_ENTITY: Module('DatabaseModule')]
|
||||
|
||||
// [END_FILE_DatabaseModule.kt]
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
package com.homebox.lens.data.di
|
||||
|
||||
// [IMPORTS]
|
||||
import com.homebox.lens.data.repository.AuthRepositoryImpl
|
||||
import com.homebox.lens.data.repository.CredentialsRepositoryImpl
|
||||
import com.homebox.lens.data.repository.ItemRepositoryImpl
|
||||
@@ -15,47 +16,52 @@ import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [ENTITY: Module('RepositoryModule')]
|
||||
/**
|
||||
* [ENTITY: Module('RepositoryModule')]
|
||||
* [CONTRACT]
|
||||
* Hilt-модуль для предоставления реализаций репозиториев.
|
||||
* Использует `@Binds` для эффективного связывания интерфейсов с их реализациями.
|
||||
* @summary Hilt-модуль для предоставления реализаций репозиториев.
|
||||
* @description Использует `@Binds` для эффективного связывания интерфейсов с их реализациями.
|
||||
*/
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
abstract class RepositoryModule {
|
||||
|
||||
// [ENTITY: Function('bindItemRepository')]
|
||||
// [RELATION: Function('bindItemRepository')] -> [PROVIDES] -> [Interface('ItemRepository')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Связывает интерфейс ItemRepository с его реализацией.
|
||||
* @summary Связывает интерфейс ItemRepository с его реализацией.
|
||||
*/
|
||||
@Binds
|
||||
@Singleton
|
||||
abstract fun bindItemRepository(
|
||||
itemRepositoryImpl: ItemRepositoryImpl
|
||||
): ItemRepository
|
||||
// [END_ENTITY: Function('bindItemRepository')]
|
||||
|
||||
// [ENTITY: Function('bindCredentialsRepository')]
|
||||
// [RELATION: Function('bindCredentialsRepository')] -> [PROVIDES] -> [Interface('CredentialsRepository')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Связывает интерфейс CredentialsRepository с его реализацией.
|
||||
* @summary Связывает интерфейс CredentialsRepository с его реализацией.
|
||||
*/
|
||||
@Binds
|
||||
@Singleton
|
||||
abstract fun bindCredentialsRepository(
|
||||
credentialsRepositoryImpl: CredentialsRepositoryImpl
|
||||
): CredentialsRepository
|
||||
// [END_ENTITY: Function('bindCredentialsRepository')]
|
||||
|
||||
// [ENTITY: Function('bindAuthRepository')]
|
||||
// [RELATION: Function('bindAuthRepository')] -> [PROVIDES] -> [Interface('AuthRepository')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* [FIX] Связывает интерфейс AuthRepository с его реализацией.
|
||||
* Это исправляет ошибку "could not be resolved", так как теперь Hilt знает,
|
||||
* какую конкретную реализацию предоставить, когда запрашивается AuthRepository.
|
||||
* @summary Связывает интерфейс AuthRepository с его реализацией.
|
||||
*/
|
||||
@Binds
|
||||
@Singleton
|
||||
abstract fun bindAuthRepository(
|
||||
authRepositoryImpl: AuthRepositoryImpl
|
||||
): AuthRepository
|
||||
// [END_ENTITY: Function('bindAuthRepository')]
|
||||
}
|
||||
// [END_ENTITY: Module('RepositoryModule')]
|
||||
// [END_FILE_RepositoryModule.kt]
|
||||
@@ -1,8 +1,9 @@
|
||||
// [PACKAGE] com.homebox.lens.data.di
|
||||
// [FILE] StorageModule.kt
|
||||
|
||||
// [SEMANTICS] di, hilt, storage
|
||||
package com.homebox.lens.data.di
|
||||
|
||||
// [IMPORTS]
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import com.homebox.lens.data.repository.EncryptedPreferencesWrapper
|
||||
@@ -12,30 +13,39 @@ import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import timber.log.Timber
|
||||
import javax.inject.Singleton
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [ENTITY: Module('StorageModule')]
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object StorageModule {
|
||||
|
||||
private const val PREFERENCES_FILE_NAME = "homebox_lens_prefs" // No longer secret
|
||||
private const val PREFERENCES_FILE_NAME = "homebox_lens_prefs"
|
||||
|
||||
// [ACTION] Provide a standard, unencrypted SharedPreferences instance.
|
||||
// [ENTITY: Function('provideSharedPreferences')]
|
||||
// [RELATION: Function('provideSharedPreferences')] -> [PROVIDES] -> [Framework('SharedPreferences')]
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideSharedPreferences(@ApplicationContext context: Context): SharedPreferences {
|
||||
Timber.d("[DEBUG][PROVIDER][providing_shared_preferences] Providing SharedPreferences.")
|
||||
return context.getSharedPreferences(PREFERENCES_FILE_NAME, Context.MODE_PRIVATE)
|
||||
}
|
||||
// [END_ENTITY: Function('provideSharedPreferences')]
|
||||
|
||||
// [ACTION] Provide our new EncryptedPreferencesWrapper as the main entry point for secure storage.
|
||||
// Hilt will automatically provide SharedPreferences and CryptoManager to its constructor.
|
||||
// [ENTITY: Function('provideEncryptedPreferencesWrapper')]
|
||||
// [RELATION: Function('provideEncryptedPreferencesWrapper')] -> [PROVIDES] -> [Class('EncryptedPreferencesWrapper')]
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideEncryptedPreferencesWrapper(
|
||||
sharedPreferences: SharedPreferences,
|
||||
cryptoManager: CryptoManager
|
||||
): EncryptedPreferencesWrapper {
|
||||
Timber.d("[DEBUG][PROVIDER][providing_encrypted_prefs_wrapper] Providing EncryptedPreferencesWrapper.")
|
||||
return EncryptedPreferencesWrapper(sharedPreferences, cryptoManager)
|
||||
}
|
||||
// [END_ENTITY: Function('provideEncryptedPreferencesWrapper')]
|
||||
}
|
||||
// [END_ENTITY: Module('StorageModule')]
|
||||
// [END_FILE_StorageModule.kt]
|
||||
@@ -20,17 +20,20 @@ import kotlinx.coroutines.withContext
|
||||
import okhttp3.OkHttpClient
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.moshi.MoshiConverterFactory
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [ENTITY: Class('AuthRepositoryImpl')]
|
||||
// [RELATION: Class('AuthRepositoryImpl')] -> [IMPLEMENTS] -> [Interface('AuthRepository')]
|
||||
// [RELATION: Class('AuthRepositoryImpl')] -> [DEPENDS_ON] -> [Framework('SharedPreferences')]
|
||||
// [RELATION: Class('AuthRepositoryImpl')] -> [DEPENDS_ON] -> [Framework('OkHttpClient')]
|
||||
// [RELATION: Class('AuthRepositoryImpl')] -> [DEPENDS_ON] -> [Framework('MoshiConverterFactory')]
|
||||
/**
|
||||
* [ENTITY: Class('AuthRepositoryImpl')]
|
||||
* [CONTRACT]
|
||||
* Реализация репозитория для управления аутентификацией.
|
||||
* @summary Реализация репозитория для управления аутентификацией.
|
||||
* @param encryptedPrefs Защищенное хранилище для токена.
|
||||
* @param okHttpClient Общий OkHttp клиент для переиспользования.
|
||||
* @param moshiConverterFactory Общий конвертер Moshi для переиспользования.
|
||||
* [COHERENCE_NOTE] Реализация метода login теперь включает логику создания временного Retrofit-клиента
|
||||
* "на лету", используя URL сервера из credentials. Эта логика была перенесена из ItemRepositoryImpl.
|
||||
*/
|
||||
class AuthRepositoryImpl @Inject constructor(
|
||||
private val encryptedPrefs: SharedPreferences,
|
||||
@@ -42,47 +45,53 @@ class AuthRepositoryImpl @Inject constructor(
|
||||
private const val KEY_AUTH_TOKEN = "key_auth_token"
|
||||
}
|
||||
|
||||
// [ENTITY: Function('login')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Реализует вход пользователя. Создает временный API сервис для выполнения запроса
|
||||
* @summary Реализует вход пользователя. Создает временный API сервис для выполнения запроса
|
||||
* на указанный пользователем URL сервера.
|
||||
* @param credentials Учетные данные пользователя, включая URL сервера.
|
||||
* @return [Result] с доменной моделью [TokenResponse] при успехе или [Exception] при ошибке.
|
||||
*/
|
||||
override suspend fun login(credentials: Credentials): Result<TokenResponse> {
|
||||
// [PRECONDITION]
|
||||
require(credentials.serverUrl.isNotBlank()) { "[PRECONDITION_FAILED] Server URL cannot be blank." }
|
||||
require(credentials.serverUrl.isNotBlank()) { "Server URL cannot be blank." }
|
||||
|
||||
// [CORE-LOGIC]
|
||||
return withContext(Dispatchers.IO) {
|
||||
runCatching {
|
||||
// [ACTION] Создаем временный Retrofit клиент с URL, указанным пользователем.
|
||||
Timber.d("[DEBUG][ACTION][creating_retrofit_client] Creating temporary Retrofit client for URL: ${credentials.serverUrl}")
|
||||
val tempApiService = Retrofit.Builder()
|
||||
.baseUrl(credentials.serverUrl)
|
||||
.client(okHttpClient) // Переиспользуем существующий OkHttp клиент
|
||||
.addConverterFactory(moshiConverterFactory) // и конвертер
|
||||
.client(okHttpClient)
|
||||
.addConverterFactory(moshiConverterFactory)
|
||||
.build()
|
||||
.create(HomeboxApiService::class.java)
|
||||
|
||||
// [ACTION] Создаем DTO и выполняем запрос.
|
||||
val loginForm = LoginFormDto(credentials.username, credentials.password)
|
||||
Timber.d("[DEBUG][ACTION][performing_login] Performing login request.")
|
||||
val tokenResponseDto = tempApiService.login(loginForm)
|
||||
|
||||
// [ACTION] Маппим результат в доменную модель.
|
||||
Timber.d("[DEBUG][ACTION][mapping_to_domain] Mapping token response to domain model.")
|
||||
tokenResponseDto.toDomain()
|
||||
}
|
||||
}
|
||||
}
|
||||
// [END_ENTITY: Function('login')]
|
||||
|
||||
// [ENTITY: Function('saveToken')]
|
||||
override suspend fun saveToken(token: String) {
|
||||
require(token.isNotBlank()) { "[PRECONDITION_FAILED] Token cannot be blank." }
|
||||
require(token.isNotBlank()) { "Token cannot be blank." }
|
||||
withContext(Dispatchers.IO) {
|
||||
Timber.d("[DEBUG][ACTION][saving_token] Saving auth token.")
|
||||
encryptedPrefs.edit().putString(KEY_AUTH_TOKEN, token).apply()
|
||||
}
|
||||
}
|
||||
// [END_ENTITY: Function('saveToken')]
|
||||
|
||||
// [ENTITY: Function('getToken')]
|
||||
override fun getToken(): Flow<String?> = flow {
|
||||
Timber.d("[DEBUG][ACTION][getting_token] Getting auth token.")
|
||||
emit(encryptedPrefs.getString(KEY_AUTH_TOKEN, null))
|
||||
}.flowOn(Dispatchers.IO)
|
||||
// [END_ENTITY: Function('getToken')]
|
||||
}
|
||||
// [END_ENTITY: Class('AuthRepositoryImpl')]
|
||||
// [END_FILE_AuthRepositoryImpl.kt]
|
||||
@@ -1,7 +1,8 @@
|
||||
// [PACKAGE] com.homebox.lens.data.repository
|
||||
// [FILE] CredentialsRepositoryImpl.kt
|
||||
// [PURPOSE] Имплементация репозитория для управления учетными данными и токенами доступа.
|
||||
// [SEMANTICS] data, repository, credentials, security
|
||||
package com.homebox.lens.data.repository
|
||||
|
||||
// [IMPORTS]
|
||||
import android.content.SharedPreferences
|
||||
import com.homebox.lens.domain.model.Credentials
|
||||
@@ -11,13 +12,16 @@ import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [ENTITY: Class('CredentialsRepositoryImpl')]
|
||||
// [RELATION: Class('CredentialsRepositoryImpl')] -> [IMPLEMENTS] -> [Interface('CredentialsRepository')]
|
||||
// [RELATION: Class('CredentialsRepositoryImpl')] -> [DEPENDS_ON] -> [Framework('SharedPreferences')]
|
||||
/**
|
||||
* [ENTITY: Class('CredentialsRepositoryImpl')]
|
||||
* [CONTRACT]
|
||||
* Реализует репозиторий для управления учетными данными пользователя.
|
||||
* Взаимодействует с зашифрованными SharedPreferences для сохранения и извлечения данных.
|
||||
* @summary Реализует репозиторий для управления учетными данными пользователя.
|
||||
* @description Взаимодействует с зашифрованными SharedPreferences для сохранения и извлечения данных.
|
||||
* @param encryptedPrefs Зашифрованное хранилище ключ-значение, предоставляемое Hilt.
|
||||
* @invariant Состояние этого репозитория полностью зависит от содержимого `encryptedPrefs`.
|
||||
*/
|
||||
@@ -25,7 +29,6 @@ class CredentialsRepositoryImpl @Inject constructor(
|
||||
private val encryptedPrefs: SharedPreferences
|
||||
) : CredentialsRepository {
|
||||
|
||||
// [CONSTANTS_KEYS] Ключи для хранения данных в SharedPreferences.
|
||||
companion object {
|
||||
private const val KEY_SERVER_URL = "key_server_url"
|
||||
private const val KEY_USERNAME = "key_username"
|
||||
@@ -33,15 +36,15 @@ class CredentialsRepositoryImpl @Inject constructor(
|
||||
private const val KEY_AUTH_TOKEN = "key_auth_token"
|
||||
}
|
||||
|
||||
// [ENTITY: Function('saveCredentials')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Сохраняет основные учетные данные пользователя.
|
||||
* @summary Сохраняет основные учетные данные пользователя.
|
||||
* @param credentials Объект с учетными данными для сохранения.
|
||||
* @sideeffect Перезаписывает существующие учетные данные в SharedPreferences.
|
||||
*/
|
||||
override suspend fun saveCredentials(credentials: Credentials) {
|
||||
// [ACTION] Выполняем запись в SharedPreferences в фоновом потоке.
|
||||
withContext(Dispatchers.IO) {
|
||||
Timber.d("[DEBUG][ACTION][saving_credentials] Saving user credentials.")
|
||||
encryptedPrefs.edit()
|
||||
.putString(KEY_SERVER_URL, credentials.serverUrl)
|
||||
.putString(KEY_USERNAME, credentials.username)
|
||||
@@ -49,51 +52,57 @@ class CredentialsRepositoryImpl @Inject constructor(
|
||||
.apply()
|
||||
}
|
||||
}
|
||||
// [END_ENTITY: Function('saveCredentials')]
|
||||
|
||||
// [ENTITY: Function('getCredentials')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Извлекает сохраненные учетные данные пользователя в виде потока.
|
||||
* @summary Извлекает сохраненные учетные данные пользователя в виде потока.
|
||||
* @return Flow, который эммитит объект [Credentials] или null, если данные отсутствуют.
|
||||
*/
|
||||
override fun getCredentials(): Flow<Credentials?> = flow {
|
||||
// [CORE-LOGIC] Читаем данные из SharedPreferences.
|
||||
Timber.d("[DEBUG][ACTION][getting_credentials] Getting user credentials.")
|
||||
val serverUrl = encryptedPrefs.getString(KEY_SERVER_URL, null)
|
||||
val username = encryptedPrefs.getString(KEY_USERNAME, null)
|
||||
val password = encryptedPrefs.getString(KEY_PASSWORD, null)
|
||||
|
||||
// [ACTION] Эммитим результат.
|
||||
if (serverUrl != null && username != null && password != null) {
|
||||
Timber.d("[DEBUG][SUCCESS][credentials_found] Found and emitting credentials.")
|
||||
emit(Credentials(serverUrl, username, password))
|
||||
} else {
|
||||
Timber.d("[DEBUG][FALLBACK][no_credentials] No credentials found, emitting null.")
|
||||
emit(null)
|
||||
}
|
||||
}.flowOn(Dispatchers.IO) // [ACTION] Указываем, что Flow должен выполняться в фоновом потоке.
|
||||
}.flowOn(Dispatchers.IO)
|
||||
// [END_ENTITY: Function('getCredentials')]
|
||||
|
||||
// [ENTITY: Function('saveToken')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Сохраняет токен авторизации.
|
||||
* @summary Сохраняет токен авторизации.
|
||||
* @param token Токен для сохранения.
|
||||
* @sideeffect Перезаписывает существующий токен в SharedPreferences.
|
||||
*/
|
||||
override suspend fun saveToken(token: String) {
|
||||
// [ACTION] Выполняем запись токена в фоновом потоке.
|
||||
withContext(Dispatchers.IO) {
|
||||
Timber.d("[DEBUG][ACTION][saving_token] Saving auth token.")
|
||||
encryptedPrefs.edit()
|
||||
.putString(KEY_AUTH_TOKEN, token)
|
||||
.apply()
|
||||
}
|
||||
}
|
||||
// [END_ENTITY: Function('saveToken')]
|
||||
|
||||
// [ENTITY: Function('getToken')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Извлекает сохраненный токен авторизации.
|
||||
* @summary Извлекает сохраненный токен авторизации.
|
||||
* @return Строка с токеном или null, если он не найден.
|
||||
*/
|
||||
override suspend fun getToken(): String? {
|
||||
// [ACTION] Выполняем чтение токена в фоновом потоке.
|
||||
return withContext(Dispatchers.IO) {
|
||||
Timber.d("[DEBUG][ACTION][getting_token] Getting auth token.")
|
||||
encryptedPrefs.getString(KEY_AUTH_TOKEN, null)
|
||||
}
|
||||
}
|
||||
// [END_ENTITY: Function('getToken')]
|
||||
}
|
||||
// [END_ENTITY: Class('CredentialsRepositoryImpl')]
|
||||
// [END_FILE_CredentialsRepositoryImpl.kt]
|
||||
@@ -1,20 +1,24 @@
|
||||
// [PACKAGE] com.homebox.lens.data.repository
|
||||
// [FILE] EncryptedPreferencesWrapper.kt
|
||||
// [PURPOSE] A wrapper around SharedPreferences to provide on-the-fly encryption/decryption.
|
||||
|
||||
// [SEMANTICS] data, security, preferences
|
||||
package com.homebox.lens.data.repository
|
||||
|
||||
// [IMPORTS]
|
||||
import android.content.SharedPreferences
|
||||
import com.homebox.lens.data.security.CryptoManager
|
||||
import timber.log.Timber
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.nio.charset.Charset
|
||||
import javax.inject.Inject
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [ENTITY: Class('EncryptedPreferencesWrapper')]
|
||||
// [RELATION: Class('EncryptedPreferencesWrapper')] -> [DEPENDS_ON] -> [Framework('SharedPreferences')]
|
||||
// [RELATION: Class('EncryptedPreferencesWrapper')] -> [DEPENDS_ON] -> [Class('CryptoManager')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Provides a simplified and secure interface for storing and retrieving sensitive string data.
|
||||
* It uses a CryptoManager to encrypt/decrypt data before writing/reading from a standard SharedPreferences instance.
|
||||
* @summary Provides a simplified and secure interface for storing and retrieving sensitive string data.
|
||||
* @description It uses a CryptoManager to encrypt/decrypt data before writing/reading from a standard SharedPreferences instance.
|
||||
* @param sharedPreferences The underlying standard SharedPreferences instance to store encrypted data.
|
||||
* @param cryptoManager The manager responsible for all cryptographic operations.
|
||||
*/
|
||||
@@ -23,44 +27,58 @@ class EncryptedPreferencesWrapper @Inject constructor(
|
||||
private val cryptoManager: CryptoManager
|
||||
) {
|
||||
|
||||
// [ENTITY: Function('getString')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Retrieves a decrypted string value for a given key.
|
||||
* @summary Retrieves a decrypted string value for a given key.
|
||||
* @param key The key for the preference.
|
||||
* @param defaultValue The value to return if the key is not found or decryption fails.
|
||||
* @return The decrypted string, or the defaultValue.
|
||||
* @sideeffect Reads from SharedPreferences.
|
||||
*/
|
||||
fun getString(key: String, defaultValue: String?): String? {
|
||||
val encryptedValue = sharedPreferences.getString(key, null) ?: return defaultValue
|
||||
Timber.d("[DEBUG][ENTRYPOINT][getting_string] Attempting to get string for key: %s", key)
|
||||
val encryptedValue = sharedPreferences.getString(key, null) ?: return defaultValue.also {
|
||||
Timber.d("[DEBUG][FALLBACK][no_value_found] No value for key %s, returning default.", key)
|
||||
}
|
||||
return try {
|
||||
Timber.d("[DEBUG][ACTION][decoding_value] Decoding Base64 value.")
|
||||
val bytes = android.util.Base64.decode(encryptedValue, android.util.Base64.DEFAULT)
|
||||
Timber.d("[DEBUG][ACTION][decrypting_value] Decrypting value with CryptoManager.")
|
||||
val decryptedBytes = cryptoManager.decrypt(ByteArrayInputStream(bytes))
|
||||
String(decryptedBytes, Charset.defaultCharset())
|
||||
String(decryptedBytes, Charset.defaultCharset()).also {
|
||||
Timber.d("[DEBUG][SUCCESS][decryption_complete] Successfully decrypted value for key: %s", key)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// Log the error, maybe clear the invalid preference
|
||||
Timber.e(e, "[ERROR][EXCEPTION][decryption_failed] Failed to decrypt value for key: %s", key)
|
||||
defaultValue
|
||||
}
|
||||
}
|
||||
// [END_ENTITY: Function('getString')]
|
||||
|
||||
// [ENTITY: Function('putString')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Encrypts and saves a string value for a given key.
|
||||
* @summary Encrypts and saves a string value for a given key.
|
||||
* @param key The key for the preference.
|
||||
* @param value The string value to encrypt and save.
|
||||
* @sideeffect Modifies the underlying SharedPreferences file.
|
||||
*/
|
||||
fun putString(key: String, value: String) {
|
||||
Timber.d("[DEBUG][ENTRYPOINT][putting_string] Attempting to put string for key: %s", key)
|
||||
try {
|
||||
Timber.d("[DEBUG][ACTION][encrypting_value] Encrypting value with CryptoManager.")
|
||||
val outputStream = ByteArrayOutputStream()
|
||||
cryptoManager.encrypt(value.toByteArray(Charset.defaultCharset()), outputStream)
|
||||
val encryptedBytes = outputStream.toByteArray()
|
||||
Timber.d("[DEBUG][ACTION][encoding_value] Encoding encrypted value to Base64.")
|
||||
val encryptedValue = android.util.Base64.encodeToString(encryptedBytes, android.util.Base64.DEFAULT)
|
||||
Timber.d("[DEBUG][ACTION][writing_to_prefs] Writing encrypted value to SharedPreferences.")
|
||||
sharedPreferences.edit().putString(key, encryptedValue).apply()
|
||||
Timber.d("[DEBUG][SUCCESS][encryption_complete] Successfully encrypted and saved value for key: %s", key)
|
||||
} catch (e: Exception) {
|
||||
// Log the error
|
||||
Timber.e(e, "[ERROR][EXCEPTION][encryption_failed] Failed to encrypt and save value for key: %s", key)
|
||||
}
|
||||
}
|
||||
|
||||
// [COHERENCE_NOTE] Add other methods like getInt, putInt etc. as needed, following the same pattern.
|
||||
// [END_ENTITY: Function('putString')]
|
||||
}
|
||||
// [END_ENTITY: Class('EncryptedPreferencesWrapper')]
|
||||
// [END_FILE_EncryptedPreferencesWrapper.kt]
|
||||
@@ -1,104 +1,196 @@
|
||||
// [PACKAGE] com.homebox.lens.data.repository
|
||||
// [FILE] ItemRepositoryImpl.kt
|
||||
// [SEMANTICS] data_repository, implementation, items
|
||||
|
||||
// [SEMANTICS] data_repository, implementation, items, labels
|
||||
package com.homebox.lens.data.repository
|
||||
|
||||
// [IMPORTS]
|
||||
import com.homebox.lens.data.api.HomeboxApiService
|
||||
import com.homebox.lens.data.api.dto.LabelCreateDto
|
||||
import com.homebox.lens.data.api.dto.toDomain
|
||||
import com.homebox.lens.data.api.dto.toDto
|
||||
import com.homebox.lens.data.api.dto.LocationCreateDto
|
||||
import com.homebox.lens.data.api.dto.LocationUpdateDto
|
||||
import com.homebox.lens.data.api.dto.LabelUpdateDto
|
||||
import com.homebox.lens.data.api.dto.LocationOutDto
|
||||
import com.homebox.lens.data.db.dao.ItemDao
|
||||
import com.homebox.lens.data.db.dao.LabelDao
|
||||
import com.homebox.lens.data.db.entity.toDomain
|
||||
import com.homebox.lens.domain.model.*
|
||||
import com.homebox.lens.domain.repository.ItemRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [CORE-LOGIC]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Реализация репозитория для работы с данными о вещах.
|
||||
* @param apiService Сервис для взаимодействия с Homebox API.
|
||||
* [COHERENCE_NOTE] Метод 'login' был полностью удален из этого класса, так как его ответственность
|
||||
* была передана в AuthRepositoryImpl. Это устраняет ошибку компиляции "'login' overrides nothing".
|
||||
*/
|
||||
// [ENTITY: Repository('ItemRepositoryImpl')]
|
||||
// [RELATION: Repository('ItemRepositoryImpl')] -> [IMPLEMENTS] -> [Interface('ItemRepository')]
|
||||
// [RELATION: Repository('ItemRepositoryImpl')] -> [DEPENDS_ON] -> [ApiEndpoint('HomeboxApiService')]
|
||||
// [RELATION: Repository('ItemRepositoryImpl')] -> [DEPENDS_ON] -> [DatabaseTable('ItemDao')]
|
||||
@Singleton
|
||||
class ItemRepositoryImpl @Inject constructor(
|
||||
private val apiService: HomeboxApiService,
|
||||
private val itemDao: ItemDao,
|
||||
private val labelDao: LabelDao
|
||||
) : ItemRepository {
|
||||
|
||||
// [DELETED] Метод login был здесь, но теперь он удален.
|
||||
|
||||
/**
|
||||
* [CONTRACT] @see ItemRepository.createItem
|
||||
*/
|
||||
// [ENTITY: Function('createItem')]
|
||||
// [RELATION: Function('createItem')] -> [RETURNS] -> [DataClass('ItemSummary')]
|
||||
override suspend fun createItem(newItemData: ItemCreate): ItemSummary {
|
||||
val itemDto = newItemData.toDto()
|
||||
val resultDto = apiService.createItem(itemDto)
|
||||
return resultDto.toDomain()
|
||||
}
|
||||
// [END_ENTITY: Function('createItem')]
|
||||
|
||||
/**
|
||||
* [CONTRACT] @see ItemRepository.getItemDetails
|
||||
*/
|
||||
// [ENTITY: Function('getItemDetails')]
|
||||
// [RELATION: Function('getItemDetails')] -> [RETURNS] -> [DataClass('ItemOut')]
|
||||
override suspend fun getItemDetails(itemId: String): ItemOut {
|
||||
val resultDto = apiService.getItem(itemId)
|
||||
return resultDto.toDomain()
|
||||
}
|
||||
// [END_ENTITY: Function('getItemDetails')]
|
||||
|
||||
/**
|
||||
* [CONTRACT] @see ItemRepository.updateItem
|
||||
*/
|
||||
// [ENTITY: Function('updateItem')]
|
||||
// [RELATION: Function('updateItem')] -> [RETURNS] -> [DataClass('ItemOut')]
|
||||
override suspend fun updateItem(itemId: String, item: ItemUpdate): ItemOut {
|
||||
val itemDto = item.toDto()
|
||||
val resultDto = apiService.updateItem(itemId, itemDto)
|
||||
return resultDto.toDomain()
|
||||
}
|
||||
// [END_ENTITY: Function('updateItem')]
|
||||
|
||||
/**
|
||||
* [CONTRACT] @see ItemRepository.deleteItem
|
||||
*/
|
||||
// [ENTITY: Function('deleteItem')]
|
||||
override suspend fun deleteItem(itemId: String) {
|
||||
apiService.deleteItem(itemId)
|
||||
}
|
||||
// [END_ENTITY: Function('deleteItem')]
|
||||
|
||||
/**
|
||||
* [CONTRACT] @see ItemRepository.syncInventory
|
||||
*/
|
||||
// [ENTITY: Function('syncInventory')]
|
||||
// [RELATION: Function('syncInventory')] -> [RETURNS] -> [DataClass('PaginationResult<ItemSummary>')]
|
||||
override suspend fun syncInventory(page: Int, pageSize: Int): PaginationResult<ItemSummary> {
|
||||
val resultDto = apiService.getItems(page = page, pageSize = pageSize)
|
||||
return resultDto.toDomain { it.toDomain() }
|
||||
}
|
||||
// [END_ENTITY: Function('syncInventory')]
|
||||
|
||||
/**
|
||||
* [CONTRACT] @see ItemRepository.getStatistics
|
||||
*/
|
||||
// [ENTITY: Function('getStatistics')]
|
||||
// [RELATION: Function('getStatistics')] -> [RETURNS] -> [DataClass('GroupStatistics')]
|
||||
override suspend fun getStatistics(): GroupStatistics {
|
||||
val resultDto = apiService.getStatistics()
|
||||
return resultDto.toDomain()
|
||||
}
|
||||
// [END_ENTITY: Function('getStatistics')]
|
||||
|
||||
/**
|
||||
* [CONTRACT] @see ItemRepository.getAllLocations
|
||||
*/
|
||||
// [ENTITY: Function('getAllLocations')]
|
||||
// [RELATION: Function('getAllLocations')] -> [RETURNS] -> [DataStructure('List<LocationOutCount>')]
|
||||
override suspend fun getAllLocations(): List<LocationOutCount> {
|
||||
val resultDto = apiService.getLocations()
|
||||
return resultDto.map { it.toDomain() }
|
||||
}
|
||||
// [END_ENTITY: Function('getAllLocations')]
|
||||
|
||||
/**
|
||||
* [CONTRACT] @see ItemRepository.getAllLabels
|
||||
*/
|
||||
// [ENTITY: Function('getAllLabels')]
|
||||
// [RELATION: Function('getAllLabels')] -> [RETURNS] -> [DataStructure('List<LabelOut>')]
|
||||
override suspend fun getAllLabels(): List<LabelOut> {
|
||||
val resultDto = apiService.getLabels()
|
||||
return resultDto.map { it.toDomain() }
|
||||
}
|
||||
// [END_ENTITY: Function('getAllLabels')]
|
||||
|
||||
/**
|
||||
* [CONTRACT] @see ItemRepository.searchItems
|
||||
*/
|
||||
// [ENTITY: Function('getLabelDetails')]
|
||||
// [RELATION: Function('getLabelDetails')] -> [RETURNS] -> [DataClass('LabelOut')]
|
||||
override suspend fun getLabelDetails(labelId: String): LabelOut {
|
||||
val resultDto = apiService.getLabels().firstOrNull { it.id == labelId }
|
||||
return resultDto?.toDomain() ?: throw NoSuchElementException("Label with ID $labelId not found.")
|
||||
}
|
||||
// [END_ENTITY: Function('getLabelDetails')]
|
||||
|
||||
// [ENTITY: Function('createLabel')]
|
||||
// [RELATION: Function('createLabel')] -> [RETURNS] -> [DataClass('LabelSummary')]
|
||||
override suspend fun createLabel(newLabelData: LabelCreate): LabelSummary {
|
||||
val labelCreateDto = newLabelData.toDto()
|
||||
val resultDto = apiService.createLabel(labelCreateDto)
|
||||
return resultDto.toDomain()
|
||||
}
|
||||
// [END_ENTITY: Function('createLabel')]
|
||||
|
||||
override suspend fun updateLabel(labelId: String, labelData: LabelUpdate): LabelOut {
|
||||
val labelDto = labelData.toDto()
|
||||
val resultDto = apiService.updateLabel(labelId, labelDto)
|
||||
return resultDto.toDomain()
|
||||
}
|
||||
|
||||
override suspend fun deleteLabel(labelId: String) {
|
||||
apiService.deleteLabel(labelId)
|
||||
labelDao.deleteLabelById(labelId)
|
||||
}
|
||||
|
||||
override suspend fun createLocation(newLocationData: LocationCreate): LocationOut {
|
||||
val locationDto = newLocationData.toDto()
|
||||
val resultDto = apiService.createLocation(locationDto)
|
||||
return resultDto.toDomain()
|
||||
}
|
||||
|
||||
override suspend fun updateLocation(locationId: String, locationData: LocationUpdate): LocationOut {
|
||||
val locationDto = locationData.toDto()
|
||||
val resultDto = apiService.updateLocation(locationId, locationDto)
|
||||
return resultDto.toDomain()
|
||||
}
|
||||
|
||||
override suspend fun deleteLocation(locationId: String) {
|
||||
apiService.deleteLocation(locationId)
|
||||
}
|
||||
|
||||
// [ENTITY: Function('searchItems')]
|
||||
// [RELATION: Function('searchItems')] -> [RETURNS] -> [DataClass('PaginationResult<ItemSummary>')]
|
||||
override suspend fun searchItems(query: String): PaginationResult<ItemSummary> {
|
||||
val resultDto = apiService.getItems(query = query)
|
||||
return resultDto.toDomain { it.toDomain() }
|
||||
}
|
||||
// [END_ENTITY: Function('searchItems')]
|
||||
|
||||
// [ENTITY: Function('getRecentlyAddedItems')]
|
||||
// [RELATION: Function('getRecentlyAddedItems')] -> [RETURNS] -> [DataStructure('Flow<List<ItemSummary>>')]
|
||||
override fun getRecentlyAddedItems(limit: Int): Flow<List<ItemSummary>> {
|
||||
return itemDao.getRecentlyAddedItems(limit).map { entities ->
|
||||
entities.map { it.toDomain() }
|
||||
}
|
||||
}
|
||||
// [END_ENTITY: Function('getRecentlyAddedItems')]
|
||||
}
|
||||
// [END_ENTITY: Repository('ItemRepositoryImpl')]
|
||||
|
||||
// [ENTITY: Function('toDto')]
|
||||
// [RELATION: Function('toDto')] -> [RETURNS] -> [DataClass('LabelCreateDto')]
|
||||
private fun LabelCreate.toDto(): LabelCreateDto {
|
||||
return LabelCreateDto(
|
||||
name = this.name,
|
||||
color = this.color,
|
||||
description = null // Description is not part of the domain model for creation.
|
||||
)
|
||||
}
|
||||
// [END_ENTITY: Function('toDto')]
|
||||
|
||||
// [ENTITY: Function('toDto')]
|
||||
// [RELATION: Function('toDto')] -> [RETURNS] -> [DataClass('LocationCreateDto')]
|
||||
private fun LocationCreate.toDto(): LocationCreateDto {
|
||||
return LocationCreateDto(
|
||||
name = this.name,
|
||||
color = this.color,
|
||||
description = null // Description is not part of the domain model for creation.
|
||||
)
|
||||
}
|
||||
// [END_ENTITY: Function('toDto')]
|
||||
|
||||
// [ENTITY: Function('toDto')]
|
||||
// [RELATION: Function('toDto')] -> [RETURNS] -> [DataClass('LabelUpdateDto')]
|
||||
private fun LabelUpdate.toDto(): LabelUpdateDto {
|
||||
return LabelUpdateDto(
|
||||
name = this.name,
|
||||
color = this.color
|
||||
)
|
||||
}
|
||||
// [END_ENTITY: Function('toDto')]
|
||||
|
||||
// [END_FILE_ItemRepositoryImpl.kt]
|
||||
@@ -1,13 +1,14 @@
|
||||
// [PACKAGE] com.homebox.lens.data.security
|
||||
// [FILE] CryptoManager.kt
|
||||
// [PURPOSE] Handles all cryptographic operations using AndroidKeyStore.
|
||||
|
||||
// [SEMANTICS] data, security, cryptography
|
||||
package com.homebox.lens.data.security
|
||||
|
||||
// [IMPORTS]
|
||||
import android.os.Build
|
||||
import android.security.keystore.KeyGenParameterSpec
|
||||
import android.security.keystore.KeyProperties
|
||||
import androidx.annotation.RequiresApi
|
||||
import timber.log.Timber
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.security.KeyStore
|
||||
@@ -17,11 +18,12 @@ import javax.crypto.SecretKey
|
||||
import javax.crypto.spec.IvParameterSpec
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [ENTITY: Class('CryptoManager')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* A manager for handling encryption and decryption using the Android Keystore system.
|
||||
* This class ensures that cryptographic keys are stored securely.
|
||||
* @summary A manager for handling encryption and decryption using the Android Keystore system.
|
||||
* @description This class ensures that cryptographic keys are stored securely.
|
||||
* It is designed to be a Singleton provided by Hilt.
|
||||
* @invariant The underlying SecretKey must be valid within the AndroidKeyStore.
|
||||
*/
|
||||
@@ -29,7 +31,6 @@ import javax.inject.Singleton
|
||||
@Singleton
|
||||
class CryptoManager @Inject constructor() {
|
||||
|
||||
// [ЯКОРЬ] Настройки для шифрования
|
||||
private val keyStore = KeyStore.getInstance("AndroidKeyStore").apply {
|
||||
load(null)
|
||||
}
|
||||
@@ -45,7 +46,6 @@ class CryptoManager @Inject constructor() {
|
||||
}
|
||||
}
|
||||
|
||||
// [CORE-LOGIC] Получение или создание ключа
|
||||
private fun getKey(): SecretKey {
|
||||
val existingKey = keyStore.getEntry(ALIAS, null) as? KeyStore.SecretKeyEntry
|
||||
return existingKey?.secretKey ?: createKey()
|
||||
@@ -67,8 +67,15 @@ class CryptoManager @Inject constructor() {
|
||||
}.generateKey()
|
||||
}
|
||||
|
||||
// [ACTION] Шифрование потока данных
|
||||
// [ENTITY: Function('encrypt')]
|
||||
/**
|
||||
* @summary Encrypts a byte array and writes it to an output stream.
|
||||
* @param bytes The byte array to encrypt.
|
||||
* @param outputStream The stream to write the encrypted data to.
|
||||
* @return The encrypted byte array.
|
||||
*/
|
||||
fun encrypt(bytes: ByteArray, outputStream: OutputStream): ByteArray {
|
||||
Timber.d("[DEBUG][ACTION][encrypting_data] Encrypting data.")
|
||||
val cipher = encryptCipher
|
||||
val encryptedBytes = cipher.doFinal(bytes)
|
||||
outputStream.use {
|
||||
@@ -79,9 +86,16 @@ class CryptoManager @Inject constructor() {
|
||||
}
|
||||
return encryptedBytes
|
||||
}
|
||||
// [END_ENTITY: Function('encrypt')]
|
||||
|
||||
// [ACTION] Дешифрование потока данных
|
||||
// [ENTITY: Function('decrypt')]
|
||||
/**
|
||||
* @summary Decrypts a byte array from an input stream.
|
||||
* @param inputStream The stream to read the encrypted data from.
|
||||
* @return The decrypted byte array.
|
||||
*/
|
||||
fun decrypt(inputStream: InputStream): ByteArray {
|
||||
Timber.d("[DEBUG][ACTION][decrypting_data] Decrypting data.")
|
||||
return inputStream.use {
|
||||
val ivSize = it.read()
|
||||
val iv = ByteArray(ivSize)
|
||||
@@ -94,6 +108,7 @@ class CryptoManager @Inject constructor() {
|
||||
getDecryptCipherForIv(iv).doFinal(encryptedBytes)
|
||||
}
|
||||
}
|
||||
// [END_ENTITY: Function('decrypt')]
|
||||
|
||||
companion object {
|
||||
private const val ALGORITHM = KeyProperties.KEY_ALGORITHM_AES
|
||||
@@ -103,4 +118,5 @@ class CryptoManager @Inject constructor() {
|
||||
private const val ALIAS = "homebox_lens_secret_key"
|
||||
}
|
||||
}
|
||||
// [END_ENTITY: Class('CryptoManager')]
|
||||
// [END_FILE_CryptoManager.kt]
|
||||
@@ -20,6 +20,12 @@ dependencies {
|
||||
|
||||
// [DEPENDENCY] Javax Inject for DI annotations
|
||||
implementation("javax.inject:javax.inject:1")
|
||||
|
||||
// [DEPENDENCY] Testing
|
||||
testImplementation(Libs.junit)
|
||||
testImplementation(Libs.kotestRunnerJunit5)
|
||||
testImplementation(Libs.kotestAssertionsCore)
|
||||
testImplementation(Libs.mockk)
|
||||
}
|
||||
|
||||
// [END_FILE_domain/build.gradle.kts]
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
// [PACKAGE] com.homebox.lens.domain.model
|
||||
// [FILE] Credentials.kt
|
||||
|
||||
// [SEMANTICS] domain, model, credentials
|
||||
package com.homebox.lens.domain.model
|
||||
|
||||
// [ENTITY: DataClass('Credentials')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Data class to hold server credentials.
|
||||
* @property serverUrl The URL of the Homebox server.
|
||||
* @property username The username for authentication.
|
||||
* @property password The password for authentication.
|
||||
* @summary Data class to hold server credentials.
|
||||
* @param serverUrl The URL of the Homebox server.
|
||||
* @param username The username for authentication.
|
||||
* @param password The password for authentication.
|
||||
*/
|
||||
data class Credentials(
|
||||
val serverUrl: String,
|
||||
val username: String,
|
||||
val password: String
|
||||
)
|
||||
// [END_ENTITY: DataClass('Credentials')]
|
||||
// [END_FILE_Credentials.kt]
|
||||
|
||||
@@ -2,17 +2,18 @@
|
||||
// [FILE] CustomField.kt
|
||||
// [SEMANTICS] data_structure, entity, custom_field
|
||||
package com.homebox.lens.domain.model
|
||||
// [CORE-LOGIC]
|
||||
|
||||
// [ENTITY: DataClass('CustomField')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Модель данных для представления кастомного поля.
|
||||
* @property name Имя поля.
|
||||
* @property value Значение поля.
|
||||
* @property type Тип поля (например, "text", "number").
|
||||
* @summary Модель данных для представления кастомного поля.
|
||||
* @param name Имя поля.
|
||||
* @param value Значение поля.
|
||||
* @param type Тип поля (например, "text", "number").
|
||||
*/
|
||||
data class CustomField(
|
||||
val name: String,
|
||||
val value: String,
|
||||
val type: String
|
||||
)
|
||||
// [END_ENTITY: DataClass('CustomField')]
|
||||
// [END_FILE_CustomField.kt]
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
// [FILE] GroupStatistics.kt
|
||||
// [SEMANTICS] data_structure, statistics
|
||||
package com.homebox.lens.domain.model
|
||||
// [CORE-LOGIC]
|
||||
|
||||
// [ENTITY: DataClass('GroupStatistics')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Модель данных для представления агрегированной статистики.
|
||||
* @property items Общее количество вещей.
|
||||
* @property labels Общее количество меток.
|
||||
* @property locations Общее количество местоположений.
|
||||
* @property totalValue Общая стоимость всех вещей.
|
||||
* @summary Модель данных для представления агрегированной статистики.
|
||||
* @param items Общее количество вещей.
|
||||
* @param labels Общее количество меток.
|
||||
* @param locations Общее количество местоположений.
|
||||
* @param totalValue Общая стоимость всех вещей.
|
||||
*/
|
||||
data class GroupStatistics(
|
||||
val items: Int,
|
||||
@@ -17,4 +17,5 @@ data class GroupStatistics(
|
||||
val locations: Int,
|
||||
val totalValue: Double
|
||||
)
|
||||
// [END_ENTITY: DataClass('GroupStatistics')]
|
||||
// [END_FILE_GroupStatistics.kt]
|
||||
|
||||
@@ -2,17 +2,18 @@
|
||||
// [FILE] Image.kt
|
||||
// [SEMANTICS] data_structure, entity, image
|
||||
package com.homebox.lens.domain.model
|
||||
// [CORE-LOGIC]
|
||||
|
||||
// [ENTITY: DataClass('Image')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Модель данных для представления изображения, привязанного к вещи.
|
||||
* @property id Уникальный идентификатор изображения.
|
||||
* @property path Путь к файлу изображения.
|
||||
* @property isPrimary Является ли это изображение основным для вещи.
|
||||
* @summary Модель данных для представления изображения, привязанного к вещи.
|
||||
* @param id Уникальный идентификатор изображения.
|
||||
* @param path Путь к файлу изображения.
|
||||
* @param isPrimary Является ли это изображение основным для вещи.
|
||||
*/
|
||||
data class Image(
|
||||
val id: String,
|
||||
val path: String,
|
||||
val isPrimary: Boolean
|
||||
)
|
||||
// [END_ENTITY: DataClass('Image')]
|
||||
// [END_FILE_Image.kt]
|
||||
|
||||
@@ -1,32 +1,55 @@
|
||||
// [PACKAGE] com.homebox.lens.domain.model
|
||||
// [FILE] Item.kt
|
||||
|
||||
// [SEMANTICS] domain, model
|
||||
package com.homebox.lens.domain.model
|
||||
|
||||
import java.math.BigDecimal
|
||||
// [IMPORTS]
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [CONTRACT]
|
||||
// [ENTITY: DataClass('Item')]
|
||||
// [RELATION: DataClass('Item')] -> [DEPENDS_ON] -> [DataClass('Location')]
|
||||
// [RELATION: DataClass('Item')] -> [DEPENDS_ON] -> [DataClass('Label')]
|
||||
/**
|
||||
* [ENTITY: DataClass('Item')]
|
||||
* [PURPOSE] Представляет собой вещь в инвентаре.
|
||||
* @property id Уникальный идентификатор вещи.
|
||||
* @property name Название вещи.
|
||||
* @property description Описание вещи.
|
||||
* @property image Url изображения.
|
||||
* @property location Местоположение вещи.
|
||||
* @property labels Список меток, присвоенных вещи.
|
||||
* @property value Стоимость вещи.
|
||||
* @property createdAt Дата создания.
|
||||
* @summary Представляет собой вещь в инвентаре.
|
||||
* @param id Уникальный идентификатор вещи.
|
||||
* @param name Название вещи.
|
||||
* @param description Описание вещи.
|
||||
* @param image Url изображения.
|
||||
* @param location Местоположение вещи.
|
||||
* @param labels Список меток, присвоенных вещи.
|
||||
* @param value Стоимость вещи.
|
||||
* @param createdAt Дата создания.
|
||||
*/
|
||||
data class Item(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val description: String?,
|
||||
val quantity: Int,
|
||||
val image: String?,
|
||||
val location: Location?,
|
||||
val labels: List<Label>,
|
||||
val value: BigDecimal?,
|
||||
val createdAt: String?
|
||||
val value: Double?,
|
||||
val createdAt: String?,
|
||||
val assetId: String?,
|
||||
val notes: String?,
|
||||
val serialNumber: String?,
|
||||
val purchasePrice: Double?,
|
||||
val purchaseDate: String?,
|
||||
val warrantyUntil: String?,
|
||||
val parentId: String?,
|
||||
val isArchived: Boolean?,
|
||||
val insured: Boolean?,
|
||||
val lifetimeWarranty: Boolean?,
|
||||
val manufacturer: String?,
|
||||
val modelNumber: String?,
|
||||
val purchaseFrom: String?,
|
||||
val soldNotes: String?,
|
||||
val soldPrice: Double?,
|
||||
val soldTime: String?,
|
||||
val soldTo: String?,
|
||||
val syncChildItemsLocations: Boolean?,
|
||||
val warrantyDetails: String?
|
||||
)
|
||||
// [END_ENTITY: DataClass('Item')]
|
||||
|
||||
// [END_FILE_Item.kt]
|
||||
|
||||
@@ -2,16 +2,16 @@
|
||||
// [FILE] ItemAttachment.kt
|
||||
// [SEMANTICS] data_structure, entity, attachment
|
||||
package com.homebox.lens.domain.model
|
||||
// [CORE-LOGIC]
|
||||
|
||||
// [ENTITY: DataClass('ItemAttachment')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Модель данных для представления вложения (файла), привязанного к вещи.
|
||||
* @property id Уникальный идентификатор вложения.
|
||||
* @property name Имя файла.
|
||||
* @property path Путь к файлу.
|
||||
* @property type MIME-тип файла.
|
||||
* @property createdAt Дата и время создания.
|
||||
* @property updatedAt Дата и время последнего обновления.
|
||||
* @summary Модель данных для представления вложения (файла), привязанного к вещи.
|
||||
* @param id Уникальный идентификатор вложения.
|
||||
* @param name Имя файла.
|
||||
* @param path Путь к файлу.
|
||||
* @param type MIME-тип файла.
|
||||
* @param createdAt Дата и время создания.
|
||||
* @param updatedAt Дата и время последнего обновления.
|
||||
*/
|
||||
data class ItemAttachment(
|
||||
val id: String,
|
||||
@@ -21,4 +21,5 @@ data class ItemAttachment(
|
||||
val createdAt: String,
|
||||
val updatedAt: String
|
||||
)
|
||||
// [END_ENTITY: DataClass('ItemAttachment')]
|
||||
// [END_FILE_ItemAttachment.kt]
|
||||
|
||||
@@ -2,23 +2,23 @@
|
||||
// [FILE] ItemCreate.kt
|
||||
// [SEMANTICS] data_structure, entity, input, create
|
||||
package com.homebox.lens.domain.model
|
||||
// [CORE-LOGIC]
|
||||
|
||||
// [ENTITY: DataClass('ItemCreate')]
|
||||
/**
|
||||
* [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 меток.
|
||||
* @summary Модель данных для создания новой "Вещи".
|
||||
* @param name Название вещи (обязательно).
|
||||
* @param assetId Идентификатор актива.
|
||||
* @param description Описание.
|
||||
* @param notes Заметки.
|
||||
* @param serialNumber Серийный номер.
|
||||
* @param quantity Количество.
|
||||
* @param value Стоимость.
|
||||
* @param purchasePrice Цена покупки.
|
||||
* @param purchaseDate Дата покупки.
|
||||
* @param warrantyUntil Гарантия до.
|
||||
* @param locationId ID местоположения.
|
||||
* @param parentId ID родительской вещи.
|
||||
* @param labelIds Список ID меток.
|
||||
*/
|
||||
data class ItemCreate(
|
||||
val name: String,
|
||||
@@ -35,4 +35,5 @@ data class ItemCreate(
|
||||
val parentId: String?,
|
||||
val labelIds: List<String>?
|
||||
)
|
||||
// [END_ENTITY: DataClass('ItemCreate')]
|
||||
// [END_FILE_ItemCreate.kt]
|
||||
|
||||
@@ -2,32 +2,32 @@
|
||||
// [FILE] ItemOut.kt
|
||||
// [SEMANTICS] data_structure, entity, detailed
|
||||
package com.homebox.lens.domain.model
|
||||
// [CORE-LOGIC]
|
||||
|
||||
// [ENTITY: DataClass('ItemOut')]
|
||||
/**
|
||||
* [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 Дата и время последнего обновления.
|
||||
* @summary Полная модель данных для представления "Вещи" со всеми полями.
|
||||
* @param id Уникальный идентификатор.
|
||||
* @param name Название.
|
||||
* @param assetId Идентификатор актива.
|
||||
* @param description Описание.
|
||||
* @param notes Заметки.
|
||||
* @param serialNumber Серийный номер.
|
||||
* @param quantity Количество.
|
||||
* @param isArchived Флаг архивации.
|
||||
* @param value Стоимость.
|
||||
* @param purchasePrice Цена покупки.
|
||||
* @param purchaseDate Дата покупки.
|
||||
* @param warrantyUntil Гарантия до.
|
||||
* @param location Местоположение.
|
||||
* @param parent Родительская вещь (если есть).
|
||||
* @param children Дочерние вещи.
|
||||
* @param labels Список меток.
|
||||
* @param attachments Список вложений.
|
||||
* @param images Список изображений.
|
||||
* @param fields Список кастомных полей.
|
||||
* @param maintenance Список записей об обслуживании.
|
||||
* @param createdAt Дата и время создания.
|
||||
* @param updatedAt Дата и время последнего обновления.
|
||||
*/
|
||||
data class ItemOut(
|
||||
val id: String,
|
||||
@@ -51,6 +51,18 @@ data class ItemOut(
|
||||
val fields: List<CustomField>,
|
||||
val maintenance: List<MaintenanceEntry>,
|
||||
val createdAt: String,
|
||||
val updatedAt: String
|
||||
val updatedAt: String,
|
||||
val insured: Boolean?,
|
||||
val lifetimeWarranty: Boolean?,
|
||||
val manufacturer: String?,
|
||||
val modelNumber: String?,
|
||||
val purchaseFrom: String?,
|
||||
val soldNotes: String?,
|
||||
val soldPrice: Double?,
|
||||
val soldTime: String?,
|
||||
val soldTo: String?,
|
||||
val syncChildItemsLocations: Boolean?,
|
||||
val warrantyDetails: String?
|
||||
)
|
||||
// [END_ENTITY: DataClass('ItemOut')]
|
||||
// [END_FILE_ItemOut.kt]
|
||||
|
||||
@@ -2,20 +2,20 @@
|
||||
// [FILE] ItemSummary.kt
|
||||
// [SEMANTICS] data_structure, entity, summary
|
||||
package com.homebox.lens.domain.model
|
||||
// [CORE-LOGIC]
|
||||
|
||||
// [ENTITY: DataClass('ItemSummary')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Сокращенная модель данных для представления "Вещи" в списках.
|
||||
* @property id Уникальный идентификатор вещи.
|
||||
* @property name Название вещи.
|
||||
* @property assetId Идентификатор актива.
|
||||
* @property image Основное изображение. Может быть null.
|
||||
* @property isArchived Флаг архивации.
|
||||
* @property labels Список меток.
|
||||
* @property location Местоположение. Может быть null.
|
||||
* @property value Стоимость.
|
||||
* @property createdAt Дата и время создания.
|
||||
* @property updatedAt Дата и время последнего обновления.
|
||||
* @summary Сокращенная модель данных для представления "Вещи" в списках.
|
||||
* @param id Уникальный идентификатор вещи.
|
||||
* @param name Название вещи.
|
||||
* @param assetId Идентификатор актива.
|
||||
* @param image Основное изображение. Может быть null.
|
||||
* @param isArchived Флаг архивации.
|
||||
* @param labels Список меток.
|
||||
* @param location Местоположение. Может быть null.
|
||||
* @param value Стоимость.
|
||||
* @param createdAt Дата и время создания.
|
||||
* @param updatedAt Дата и время последнего обновления.
|
||||
*/
|
||||
data class ItemSummary(
|
||||
val id: String,
|
||||
@@ -29,4 +29,5 @@ data class ItemSummary(
|
||||
val createdAt: String,
|
||||
val updatedAt: String
|
||||
)
|
||||
// [END_ENTITY: DataClass('ItemSummary')]
|
||||
// [END_FILE_ItemSummary.kt]
|
||||
|
||||
@@ -2,24 +2,24 @@
|
||||
// [FILE] ItemUpdate.kt
|
||||
// [SEMANTICS] data_structure, entity, input, update
|
||||
package com.homebox.lens.domain.model
|
||||
// [CORE-LOGIC]
|
||||
|
||||
// [ENTITY: DataClass('ItemUpdate')]
|
||||
/**
|
||||
* [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 меток для полной замены.
|
||||
* @summary Модель данных для обновления существующей "Вещи".
|
||||
* @param name Название вещи.
|
||||
* @param assetId Идентификатор актива.
|
||||
* @param description Описание.
|
||||
* @param notes Заметки.
|
||||
* @param serialNumber Серийный номер.
|
||||
* @param quantity Количество.
|
||||
* @param isArchived Флаг архивации.
|
||||
* @param value Стоимость.
|
||||
* @param purchasePrice Цена покупки.
|
||||
* @param purchaseDate Дата покупки.
|
||||
* @param warrantyUntil Гарантия до.
|
||||
* @param locationId ID местоположения.
|
||||
* @param parentId ID родительской вещи.
|
||||
* @param labelIds Список ID меток для полной замены.
|
||||
*/
|
||||
data class ItemUpdate(
|
||||
val name: String?,
|
||||
@@ -37,4 +37,5 @@ data class ItemUpdate(
|
||||
val parentId: String?,
|
||||
val labelIds: List<String>?
|
||||
)
|
||||
// [END_ENTITY: DataClass('ItemUpdate')]
|
||||
// [END_FILE_ItemUpdate.kt]
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
// [PACKAGE] com.homebox.lens.domain.model
|
||||
// [FILE] Label.kt
|
||||
|
||||
// [SEMANTICS] domain, model
|
||||
package com.homebox.lens.domain.model
|
||||
|
||||
// [CONTRACT]
|
||||
// [ENTITY: DataClass('Label')]
|
||||
/**
|
||||
* [ENTITY: DataClass('Label')]
|
||||
* [PURPOSE] Представляет собой метку (тег), которую можно присвоить вещи.
|
||||
* @property id Уникальный идентификатор метки.
|
||||
* @property name Название метки.
|
||||
* @summary Представляет собой метку (тег), которую можно присвоить вещи.
|
||||
* @param id Уникальный идентификатор метки.
|
||||
* @param name Название метки.
|
||||
*/
|
||||
data class Label(
|
||||
val id: String,
|
||||
val name: String
|
||||
)
|
||||
// [END_ENTITY: DataClass('Label')]
|
||||
|
||||
// [END_FILE_Label.kt]
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
// [PACKAGE] com.homebox.lens.domain.model
|
||||
// [FILE] LabelCreate.kt
|
||||
// [SEMANTICS] data_structure, contract, label, create
|
||||
package com.homebox.lens.domain.model
|
||||
|
||||
// [ENTITY: DataClass('LabelCreate')]
|
||||
/**
|
||||
* @summary Модель с данными, необходимыми для создания новой метки.
|
||||
* @param name Название новой метки. Обязательное поле.
|
||||
* @param color Цвет метки в формате HEX. Необязательное поле.
|
||||
* @invariant name не может быть пустым.
|
||||
*/
|
||||
data class LabelCreate(
|
||||
val name: String,
|
||||
val color: String?
|
||||
)
|
||||
// [END_ENTITY: DataClass('LabelCreate')]
|
||||
// [END_FILE_LabelCreate.kt]
|
||||
@@ -2,16 +2,16 @@
|
||||
// [FILE] LabelOut.kt
|
||||
// [SEMANTICS] data_structure, entity, label
|
||||
package com.homebox.lens.domain.model
|
||||
// [CORE-LOGIC]
|
||||
|
||||
// [ENTITY: DataClass('LabelOut')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Модель данных для представления метки (тега).
|
||||
* @property id Уникальный идентификатор.
|
||||
* @property name Название метки.
|
||||
* @property color Цвет метки в формате HEX (например, "#FF0000").
|
||||
* @property isArchived Флаг, указывающий, заархивирована ли метка.
|
||||
* @property createdAt Дата и время создания.
|
||||
* @property updatedAt Дата и время последнего обновления.
|
||||
* @summary Модель данных для представления метки (тега).
|
||||
* @param id Уникальный идентификатор.
|
||||
* @param name Название метки.
|
||||
* @param color Цвет метки в формате HEX (например, "#FF0000").
|
||||
* @param isArchived Флаг, указывающий, заархивирована ли метка.
|
||||
* @param createdAt Дата и время создания.
|
||||
* @param updatedAt Дата и время последнего обновления.
|
||||
*/
|
||||
data class LabelOut(
|
||||
val id: String,
|
||||
@@ -21,4 +21,5 @@ data class LabelOut(
|
||||
val createdAt: String,
|
||||
val updatedAt: String
|
||||
)
|
||||
// [END_ENTITY: DataClass('LabelOut')]
|
||||
// [END_FILE_LabelOut.kt]
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user