Compare commits
2 Commits
aa69776807
...
9b914b2904
| Author | SHA1 | Date | |
|---|---|---|---|
| 9b914b2904 | |||
| 394e0040de |
@@ -1,74 +0,0 @@
|
|||||||
<!-- File: agent_promts/implementations/filesystem_task_channel.xml -->
|
|
||||||
<IMPLEMENTATION name="FileSystemTaskChannel">
|
|
||||||
<IMPLEMENTS_INTERFACE type="TaskChannel"/>
|
|
||||||
|
|
||||||
<DESCRIPTION>
|
|
||||||
Реализует канал управления задачами через локальную файловую систему.
|
|
||||||
Задачи хранятся как файлы в директории `tasks/`.
|
|
||||||
</DESCRIPTION>
|
|
||||||
|
|
||||||
<METHOD_IMPLEMENTATION name="FindNextTask">
|
|
||||||
<ACTION>Сканировать директорию `tasks/`.</ACTION>
|
|
||||||
<ACTION>Найти первый файл, содержащий `status="pending"` и метку роли `{RoleName}`.</ACTION>
|
|
||||||
<ACTION>Если найден, вернуть содержимое файла. Иначе, вернуть `NULL`.</ACTION>
|
|
||||||
</METHOD_IMPLEMENTATION>
|
|
||||||
|
|
||||||
<METHOD_IMPLEMENTATION name="CreateTask">
|
|
||||||
<ACTION>Создать новый XML-файл в директории `tasks/`.</ACTION>
|
|
||||||
<ACTION>Имя файла: `{Timestamp}_{Title}.xml`.</ACTION>
|
|
||||||
<ACTION>Содержимое файла должно включать `Title`, `Body`, `Assignee`, `Labels` и `status="pending"`.</ACTION>
|
|
||||||
</METHOD_IMPLEMENTATION>
|
|
||||||
|
|
||||||
<METHOD_IMPLEMENTATION name="UpdateTaskStatus">
|
|
||||||
<ACTION>Найти файл задачи по `{IssueID}` (имени файла).</ACTION>
|
|
||||||
<ACTION>Заменить в файле `status="{OldStatus}"` на `status="{NewStatus}"`.</ACTION>
|
|
||||||
</METHOD_IMPLEMENTATION>
|
|
||||||
|
|
||||||
<METHOD_IMPLEMENTATION name="AddComment">
|
|
||||||
<ACTION>Найти файл задачи по `{IssueID}`.</ACTION>
|
|
||||||
<ACTION>Добавить в конец файла XML-блок `<COMMENT timestamp="..." author="...">{CommentBody}</COMMENT>`.</ACTION>
|
|
||||||
</METHOD_IMPLEMENTATION>
|
|
||||||
|
|
||||||
<METHOD_IMPLEMENTATION name="CreatePullRequest">
|
|
||||||
<LOG>
|
|
||||||
[FileSystemTaskChannel] INFO: Операция 'CreatePullRequest' не поддерживается файловым протоколом. Пропущено.
|
|
||||||
Title: {Title}, Head: {HeadBranch}, Base: {BaseBranch}
|
|
||||||
</LOG>
|
|
||||||
</METHOD_IMPLEMENTATION>
|
|
||||||
|
|
||||||
<METHOD_IMPLEMENTATION name="MergeAndComplete">
|
|
||||||
<LOG>
|
|
||||||
[FileSystemTaskChannel] INFO: Операция 'MergeAndComplete' не поддерживается файловым протоколом. Пропущено.
|
|
||||||
IssueID: {IssueID}, PrID: {PrID}
|
|
||||||
</LOG>
|
|
||||||
</METHOD_IMPLEMENTATION>
|
|
||||||
|
|
||||||
<METHOD_IMPLEMENTATION name="ReturnToDev">
|
|
||||||
<LOG>
|
|
||||||
[FileSystemTaskChannel] INFO: Операция 'ReturnToDev' не поддерживается файловым протоколом. Пропущено.
|
|
||||||
IssueID: {IssueID}, PrID: {PrID}
|
|
||||||
</LOG>
|
|
||||||
</METHOD_IMPLEMENTATION>
|
|
||||||
|
|
||||||
<METHOD_IMPLEMENTATION name="CommitChanges">
|
|
||||||
<LOG>
|
|
||||||
[FileSystemTaskChannel] INFO: Операция 'CommitChanges' не поддерживается файловым протоколом. Пропущено.
|
|
||||||
Commit Message: {CommitMessage}
|
|
||||||
</LOG>
|
|
||||||
</METHOD_IMPLEMENTATION>
|
|
||||||
|
|
||||||
<METHOD_IMPLEMENTATION name="CreateBranch">
|
|
||||||
<LOG>
|
|
||||||
[FileSystemTaskChannel] INFO: Операция 'CreateBranch' не поддерживается файловым протоколом. Пропущено.
|
|
||||||
Branch Name: {BranchName}
|
|
||||||
</LOG>
|
|
||||||
</METHOD_IMPLEMENTATION>
|
|
||||||
|
|
||||||
<METHOD_IMPLEMENTATION name="CommitChanges">
|
|
||||||
<LOG>
|
|
||||||
[FileSystemTaskChannel] INFO: Операция 'CommitChanges' не поддерживается файловым протоколом. Пропущено.
|
|
||||||
Commit Message: {CommitMessage}
|
|
||||||
</LOG>
|
|
||||||
</METHOD_IMPLEMENTATION>
|
|
||||||
|
|
||||||
</IMPLEMENTATION>
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
<!-- File: agent_promts/implementations/gitea_task_channel.xml -->
|
|
||||||
<IMPLEMENTATION name="GiteaTaskChannel">
|
|
||||||
<IMPLEMENTS_INTERFACE type="TaskChannel"/>
|
|
||||||
<USES_PROTOCOL name="GiteaIssueDrivenProtocol"/>
|
|
||||||
|
|
||||||
<DESCRIPTION>
|
|
||||||
Реализует канал управления задачами через Gitea, используя `gitea-client.zsh`.
|
|
||||||
</DESCRIPTION>
|
|
||||||
|
|
||||||
<METHOD_IMPLEMENTATION name="FindNextTask">
|
|
||||||
<ACTION>
|
|
||||||
Выполнить команду `./gitea-client.zsh {RoleName} find-tasks --type "{TaskType}"`.
|
|
||||||
</ACTION>
|
|
||||||
</METHOD_IMPLEMENTATION>
|
|
||||||
|
|
||||||
<METHOD_IMPLEMENTATION name="CreateTask">
|
|
||||||
<ACTION>
|
|
||||||
Выполнить команду `./gitea-client.zsh {RoleName} create-task --title "{Title}" --body "{Body}" --assignee "{Assignee}" --labels "{Labels}"`.
|
|
||||||
</ACTION>
|
|
||||||
</METHOD_IMPLEMENTATION>
|
|
||||||
|
|
||||||
<METHOD_IMPLEMENTATION name="UpdateTaskStatus">
|
|
||||||
<ACTION>
|
|
||||||
Выполнить команду `./gitea-client.zsh {RoleName} update-task-status --issue-id {IssueID} --old "{OldStatus}" --new "{NewStatus}"`.
|
|
||||||
</ACTION>
|
|
||||||
</METHOD_IMPLEMENTATION>
|
|
||||||
|
|
||||||
<METHOD_IMPLEMENTATION name="CreatePullRequest">
|
|
||||||
<ACTION>
|
|
||||||
Выполнить команду `./gitea-client.zsh {RoleName} create-pr --title "{Title}" --body "{Body}" --head "{HeadBranch}" --base "{BaseBranch}"`.
|
|
||||||
</ACTION>
|
|
||||||
</METHOD_IMPLEMENTATION>
|
|
||||||
|
|
||||||
<METHOD_IMPLEMENTATION name="MergeAndComplete">
|
|
||||||
<ACTION>
|
|
||||||
Выполнить команду `./gitea-client.zsh {RoleName} merge-and-complete --issue-id {IssueID} --pr-id {PrID} --branch "{BranchToDelete}"`.
|
|
||||||
</ACTION>
|
|
||||||
</METHOD_IMPLEMENTATION>
|
|
||||||
|
|
||||||
<METHOD_IMPLEMENTATION name="ReturnToDev">
|
|
||||||
<ACTION>
|
|
||||||
Выполнить команду `./gitea-client.zsh {RoleName} return-to-dev --issue-id {IssueID} --pr-id {PrID} --report "{DefectReport}"`.
|
|
||||||
</ACTION>
|
|
||||||
</METHOD_IMPLEMENTATION>
|
|
||||||
|
|
||||||
<METHOD_IMPLEMENTATION name="AddComment">
|
|
||||||
<ACTION>
|
|
||||||
<!-- gitea-client.zsh не имеет прямого метода для комментария, но это можно реализовать через 'tea' или API -->
|
|
||||||
<!-- Для совместимости с интерфейсом, пока логируем -->
|
|
||||||
<LOG>ACTION: AddComment. Issue: {IssueID}, Body: {CommentBody}</LOG>
|
|
||||||
</ACTION>
|
|
||||||
</METHOD_IMPLEMENTATION>
|
|
||||||
|
|
||||||
<METHOD_IMPLEMENTATION name="CommitChanges">
|
|
||||||
<ACTION>Выполнить `git add .`.</ACTION>
|
|
||||||
<ACTION>Выполнить `git commit -m "{CommitMessage}"`.</ACTION>
|
|
||||||
<ACTION>Выполнить `git push origin {CurrentBranch}`.</ACTION>
|
|
||||||
</METHOD_IMPLEMENTATION>
|
|
||||||
|
|
||||||
<METHOD_IMPLEMENTATION name="CreateBranch">
|
|
||||||
<ACTION>Выполнить `git checkout -b {BranchName}`.</ACTION>
|
|
||||||
</METHOD_IMPLEMENTATION>
|
|
||||||
|
|
||||||
<METHOD_IMPLEMENTATION name="CommitChanges">
|
|
||||||
<ACTION>Выполнить `git add .`.</ACTION>
|
|
||||||
<ACTION>Выполнить `git commit -m "{CommitMessage}"`.</ACTION>
|
|
||||||
<ACTION>Выполнить `git push origin {CurrentBranch}`.</ACTION>
|
|
||||||
</METHOD_IMPLEMENTATION>
|
|
||||||
</IMPLEMENTATION>
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
<IMPLEMENTATION name="XmlFileLogSink">
|
|
||||||
<IMPLEMENTS_INTERFACE type="LogSink"/>
|
|
||||||
|
|
||||||
<DESCRIPTION>
|
|
||||||
Реализует канал логирования путем дозаписи в файл 'logs/communication_log.xml'.
|
|
||||||
</DESCRIPTION>
|
|
||||||
|
|
||||||
<METHOD_IMPLEMENTATION name="Send">
|
|
||||||
<INPUT>LogMessage</INPUT>
|
|
||||||
<ACTION>
|
|
||||||
Сформировать XML-блок `<LOG_ENTRY>` на основе `LogMessage`.
|
|
||||||
</ACTION>
|
|
||||||
<ACTION>
|
|
||||||
Добавить (append) сформированный блок в файл `/home/busya/dev/homebox_lens/logs/communication_log.xml`.
|
|
||||||
</ACTION>
|
|
||||||
</METHOD_IMPLEMENTATION>
|
|
||||||
</IMPLEMENTATION>
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
<IMPLEMENTATION name="XmlFileMetricsSink">
|
|
||||||
<IMPLEMENTS_INTERFACE type="MetricsSink"/>
|
|
||||||
|
|
||||||
<DESCRIPTION>
|
|
||||||
Реализует канал для метрик путем дозаписи в файл 'logs/metrics_log.xml'.
|
|
||||||
</DESCRIPTION>
|
|
||||||
|
|
||||||
<METHOD_IMPLEMENTATION name="Send">
|
|
||||||
<INPUT>MetricsBundle</INPUT>
|
|
||||||
<ACTION>
|
|
||||||
Сформировать XML-блок `<METRICS_ENTRY>` на основе `MetricsBundle`.
|
|
||||||
</ACTION>
|
|
||||||
<ACTION>
|
|
||||||
Добавить (append) сформированный блок в файл `/home/busya/dev/homebox_lens/logs/metrics_log.xml`.
|
|
||||||
</ACTION>
|
|
||||||
</METHOD_IMPLEMENTATION>
|
|
||||||
</IMPLEMENTATION>
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
<!--
|
|
||||||
Абстрактный контракт для любого приемника логов.
|
|
||||||
Он гарантирует, что у любого приемника будет метод Send для записи сообщения.
|
|
||||||
-->
|
|
||||||
<INTERFACE name="LogSink">
|
|
||||||
<METHOD name="Send" accepts="LogMessage"/>
|
|
||||||
</INTERFACE>
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
<!--
|
|
||||||
Абстрактный контракт для любого приемника метрик.
|
|
||||||
Он гарантирует, что у любого приемника будет метод Send для записи метрик.
|
|
||||||
-->
|
|
||||||
<INTERFACE name="MetricsSink">
|
|
||||||
<METHOD name="Send" accepts="MetricsBundle"/>
|
|
||||||
</INTERFACE>
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
<!-- File: agent_promts/interfaces/task_channel_interface.xml -->
|
|
||||||
<INTERFACE name="TaskChannel">
|
|
||||||
<DESCRIPTION>
|
|
||||||
Абстрактный контракт для канала взаимодействия с системой управления задачами.
|
|
||||||
Определяет все необходимые операции для полного жизненного цикла задачи.
|
|
||||||
</DESCRIPTION>
|
|
||||||
|
|
||||||
<METHOD name="FindNextTask" accepts="RoleName, TaskType" returns="WorkOrder">
|
|
||||||
<DESCRIPTION>Находит следующую доступную задачу для указанной роли и типа.</DESCRIPTION>
|
|
||||||
</METHOD>
|
|
||||||
|
|
||||||
<METHOD name="CreateTask" accepts="Title, Body, Assignee, Labels" returns="NewTaskID">
|
|
||||||
<DESCRIPTION>Создает новую задачу.</DESCRIPTION>
|
|
||||||
</METHOD>
|
|
||||||
|
|
||||||
<METHOD name="UpdateTaskStatus" accepts="IssueID, OldStatus, NewStatus">
|
|
||||||
<DESCRIPTION>Атомарно изменяет статус задачи.</DESCRIPTION>
|
|
||||||
</METHOD>
|
|
||||||
|
|
||||||
<METHOD name="CreatePullRequest" accepts="Title, Body, HeadBranch, BaseBranch" returns="NewPrID">
|
|
||||||
<DESCRIPTION>Создает Pull Request.</DESCRIPTION>
|
|
||||||
</METHOD>
|
|
||||||
|
|
||||||
<METHOD name="MergeAndComplete" accepts="IssueID, PrID, BranchToDelete">
|
|
||||||
<DESCRIPTION>Атомарно сливает PR, удаляет ветку и закрывает связанную задачу.</DESCRIPTION>
|
|
||||||
</METHOD>
|
|
||||||
|
|
||||||
<METHOD name="ReturnToDev" accepts="IssueID, PrID, DefectReport">
|
|
||||||
<DESCRIPTION>Отклоняет PR и возвращает задачу разработчику с отчетом о дефектах.</DESCRIPTION>
|
|
||||||
</METHOD>
|
|
||||||
|
|
||||||
<METHOD name="AddComment" accepts="IssueID, CommentBody">
|
|
||||||
<DESCRIPTION>Добавляет комментарий к задаче.</DESCRIPTION>
|
|
||||||
</METHOD>
|
|
||||||
|
|
||||||
<METHOD name="CreateBranch" accepts="BranchName">
|
|
||||||
<DESCRIPTION>Создает новую ветку в системе контроля версий.</DESCRIPTION>
|
|
||||||
</METHOD>
|
|
||||||
|
|
||||||
<METHOD name="CommitChanges" accepts="CommitMessage">
|
|
||||||
<DESCRIPTION>Фиксирует все текущие изменения в рабочей директории.</DESCRIPTION>
|
|
||||||
</METHOD>
|
|
||||||
</INTERFACE>
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
<!-- =================================================================== -->
|
|
||||||
<!-- ПРАВИЛО 8: Структурированное логирование для AI -->
|
|
||||||
<!-- =================================================================== -->
|
|
||||||
<Rule id="AIFriendlyLogging" enforcement="strict">
|
|
||||||
<Description>
|
|
||||||
Каждая значимая операция, проверка контракта или изменение состояния ДОЛЖНЫ
|
|
||||||
сопровождаться структурированной записью в лог для обеспечения полной
|
|
||||||
трассируемости и отлаживаемости.
|
|
||||||
</Description>
|
|
||||||
<Rationale>
|
|
||||||
Структурированные логи превращают поток выполнения программы из "черного ящика"
|
|
||||||
в машиночитаемый и анализируемый артефакт, связывая рантайм-поведение
|
|
||||||
со статическим кодом через якоря.
|
|
||||||
</Rationale>
|
|
||||||
<Definition type="multi_check">
|
|
||||||
<!--
|
|
||||||
Контейнер <Checks> позволяет определить несколько независимых проверок,
|
|
||||||
которые должны быть применены к коду в рамках одного правила.
|
|
||||||
-->
|
|
||||||
<Checks>
|
|
||||||
<!--
|
|
||||||
ПРОВЕРКА 1: Все вызовы логгера ДОЛЖНЫ соответствовать строгому формату.
|
|
||||||
Это позитивная проверка: каждая строка, содержащая 'logger.*()', должна совпадать с этим шаблоном.
|
|
||||||
-->
|
|
||||||
<Check type="positive_regex_on_match" trigger="logger\.(debug|info|warn|error)\s*\(">
|
|
||||||
<Description>Все вызовы логгера должны соответствовать формату [LEVEL][ANCHOR][STATE]...</Description>
|
|
||||||
<Pattern><![CDATA[logger\.(debug|info|warn|error)\s*\(\s*"\[(DEBUG|INFO|WARN|ERROR)\]\[[A-Z_]+\]\[[a-z_]+\][^"]*"\s*(,.*)?\)]]></Pattern>
|
|
||||||
<FailureMessage>Нарушен структурный формат лога. Ожидается: [LEVEL][ANCHOR][STATE] message.</FailureMessage>
|
|
||||||
</Check>
|
|
||||||
|
|
||||||
<!--
|
|
||||||
ПРОВЕРКА 2: В строках лога НЕ ДОЛЖНО быть строковой интерполяции.
|
|
||||||
Это негативная проверка: если найдена строка, содержащая 'logger.*("$...")', это ошибка.
|
|
||||||
-->
|
|
||||||
<Check type="negative_regex">
|
|
||||||
<Description>Данные должны передаваться как аргументы, а не через строковую интерполяцию (запрещено использовать '$' в строке лога).</Description>
|
|
||||||
<Pattern><![CDATA[logger\.(debug|info|warn|error)\s*\(\s*".*\$.*"]]></Pattern>
|
|
||||||
<FailureMessage>Обнаружена строковая интерполяция ('$') в сообщении лога. Передавайте данные как аргументы.</FailureMessage>
|
|
||||||
</Check>
|
|
||||||
|
|
||||||
<!--
|
|
||||||
ПРОВЕРКА 3: В слое Domain НЕ ДОЛЖНО быть вызовов логгера.
|
|
||||||
Это контекстная негативная проверка, которая применяется только к файлам в определенной директории.
|
|
||||||
-->
|
|
||||||
<Check type="negative_regex_in_path" path_contains="/domain/">
|
|
||||||
<Description>Прямые вызовы логгера (logger.*, Timber.*) запрещены в модуле :domain.</Description>
|
|
||||||
<Pattern><![CDATA[(logger|Timber)\.(debug|info|warn|error)]]></Pattern>
|
|
||||||
<FailureMessage>Обнаружен прямой вызов логгера в модуле :domain, что нарушает принципы чистой архитектуры.</FailureMessage>
|
|
||||||
</Check>
|
|
||||||
</Checks>
|
|
||||||
</Definition>
|
|
||||||
</Rule>
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
<!-- =================================================================== -->
|
|
||||||
<!-- ПРАВИЛО 9: Проектирование по контракту (DbC) -->
|
|
||||||
<!-- =================================================================== -->
|
|
||||||
<Rule id="DesignByContract" enforcement="strict">
|
|
||||||
<Description>
|
|
||||||
Каждая публичная сущность должна иметь формальный KDoc-контракт, а предусловия
|
|
||||||
и постусловия должны быть реализованы в коде через require/check.
|
|
||||||
</Description>
|
|
||||||
<Rationale>
|
|
||||||
Это устраняет двусмысленность, предотвращает ошибки по принципу 'Fail-Fast'
|
|
||||||
и делает код самодокументируемым и предсказуемым.
|
|
||||||
</Rationale>
|
|
||||||
<Definition type="multi_check">
|
|
||||||
<Checks>
|
|
||||||
|
|
||||||
<!--
|
|
||||||
ПРОВЕРКА 1: Обязательные теги в KDoc для публичных функций и классов.
|
|
||||||
Это проверка полноты контракта.
|
|
||||||
-->
|
|
||||||
<Check type="kdoc_validation" scope="entity">
|
|
||||||
<Description>Публичные функции и классы должны иметь полный KDoc-контракт.</Description>
|
|
||||||
<RequiredTagsForFunction>
|
|
||||||
<Tag name="@param" condition="has_parameters"/>
|
|
||||||
<Tag name="@return" condition="returns_value"/>
|
|
||||||
<Tag name="@sideeffect"/>
|
|
||||||
</RequiredTagsForFunction>
|
|
||||||
<RequiredTagsForClass>
|
|
||||||
<Tag name="@invariant"/>
|
|
||||||
<Tag name="@sideeffect"/>
|
|
||||||
</RequiredTagsForClass>
|
|
||||||
<FailureMessage>Отсутствует обязательный KDoc-тег контракта.</FailureMessage>
|
|
||||||
</Check>
|
|
||||||
|
|
||||||
<!--
|
|
||||||
ПРОВЕРКА 2: Наличие `require()` при наличии `@param`.
|
|
||||||
Эта проверка связывает документацию с кодом.
|
|
||||||
-->
|
|
||||||
<Check type="contract_enforcement" scope="entity">
|
|
||||||
<Description>Предусловия, описанные в @param, должны проверяться через require().</Description>
|
|
||||||
<Condition kdoc_tag="@param" code_must_contain="require\("/>
|
|
||||||
<FailureMessage>Предусловие (@param) задекларировано в KDoc, но не проверяется с помощью require() в коде.</FailureMessage>
|
|
||||||
</Check>
|
|
||||||
|
|
||||||
<!--
|
|
||||||
ПРОВЕРКА 3: Наличие `check()` при наличии `@return`.
|
|
||||||
-->
|
|
||||||
<Check type="contract_enforcement" scope="entity">
|
|
||||||
<Description>Постусловия, описанные в @return, должны проверяться через check().</Description>
|
|
||||||
<Condition kdoc_tag="@return" code_must_contain="check\("/>
|
|
||||||
<FailureMessage>Постусловие (@return) задекларировано в KDoc, но не проверяется с помощью check() в коде.</FailureMessage>
|
|
||||||
</Check>
|
|
||||||
|
|
||||||
</Checks>
|
|
||||||
</Definition>
|
|
||||||
</Rule>
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
<Rule id="GraphRAG" enforcement="strict">
|
|
||||||
<Description>Код должен содержать явный, машиночитаемый граф знаний в виде семантических якорей [ENTITY] и [RELATION].</Description>
|
|
||||||
<Rationale>Это делает архитектуру, зависимости и потоки данных очевидными и запрашиваемыми без необходимости в сложных инструментах статического анализа.</Rationale>
|
|
||||||
<Definition type="multi_check">
|
|
||||||
<Checks>
|
|
||||||
<!--
|
|
||||||
ПРОВЕРКА 1: Блок разметки ([ENTITY]/[RELATION]) должен идти ПЕРЕД KDoc.
|
|
||||||
Это реализация правила 'Placement'.
|
|
||||||
-->
|
|
||||||
<Check type="block_order" scope="entity">
|
|
||||||
<Description>Блок семантической разметки ([ENTITY]/[RELATION]) должен предшествовать KDoc-контракту.</Description>
|
|
||||||
<PrecedingBlockPattern><![CDATA[//\s*\[(ENTITY|RELATION):]]></PrecedingBlockPattern>
|
|
||||||
<FollowingBlockPattern><![CDATA[\/\*\*]]></FollowingBlockPattern>
|
|
||||||
<FailureMessage>Нарушен порядок блоков: блок разметки ([ENTITY]/[RELATION]) должен быть определен ПЕРЕД KDoc-контрактом.</FailureMessage>
|
|
||||||
</Check>
|
|
||||||
|
|
||||||
<!--
|
|
||||||
ПРОВЕРКА 2: Тип сущности в [ENTITY] должен быть из разрешенного списка.
|
|
||||||
-->
|
|
||||||
<Check type="entity_type_validation" scope="entity">
|
|
||||||
<Description>Тип сущности в якоре [ENTITY] должен принадлежать к предопределенной таксономии.</Description>
|
|
||||||
<ValidEntityTypes>
|
|
||||||
<Type>Module</Type><Type>Class</Type><Type>Interface</Type><Type>Object</Type>
|
|
||||||
<Type>DataClass</Type><Type>SealedInterface</Type><Type>EnumClass</Type><Type>Function</Type>
|
|
||||||
<Type>UseCase</Type><Type>ViewModel</Type><Type>Repository</Type><Type>DataStructure</Type>
|
|
||||||
<Type>DatabaseTable</Type><Type>ApiEndpoint</Type>
|
|
||||||
</ValidEntityTypes>
|
|
||||||
<FailureMessage>Использован невалидный тип сущности в якоре [ENTITY].</FailureMessage>
|
|
||||||
</Check>
|
|
||||||
|
|
||||||
<!--
|
|
||||||
ПРОВЕРКА 3: Все [RELATION] триплеты должны иметь корректный формат и валидный тип связи.
|
|
||||||
-->
|
|
||||||
<Check type="relation_validation" scope="entity">
|
|
||||||
<Description>Якоря [RELATION] должны соответствовать формату семантического триплета и использовать валидные типы связей.</Description>
|
|
||||||
<TripletPattern><![CDATA[//\s*\[RELATION:\s*'(?P<subject_type>\w+)'\('(?P<subject_name>.*?)'\)\s*->\s*\[(?P<relation_type>\w+)\]\s*->\s*\['(?P<object_type>\w+)'\('(?P<object_name>.*?)'\)\]]]></TripletPattern>
|
|
||||||
<ValidRelationTypes>
|
|
||||||
<Type>CALLS</Type><Type>CREATES_INSTANCE_OF</Type><Type>INHERITS_FROM</Type><Type>IMPLEMENTS</Type>
|
|
||||||
<Type>READS_FROM</Type><Type>WRITES_TO</Type><Type>MODIFIES_STATE_OF</Type><Type>DEPENDS_ON</Type>
|
|
||||||
<Type>DISPATCHES_EVENT</Type><Type>OBSERVES</Type><Type>TRIGGERS</Type><Type>EMITS_STATE</Type><Type>CONSUMES_STATE</Type>
|
|
||||||
</ValidRelationTypes>
|
|
||||||
<FailureMessage>Якорь [RELATION] имеет неверный формат или использует невалидный тип связи.</FailureMessage>
|
|
||||||
</Check>
|
|
||||||
|
|
||||||
<!--
|
|
||||||
ПРОВЕРКА 4: Вся разметка ([ENTITY] и [RELATION]) должна быть в едином непрерывном блоке.
|
|
||||||
Это реализация правила 'MarkupBlockCohesion'.
|
|
||||||
-->
|
|
||||||
<Check type="markup_cohesion" scope="entity">
|
|
||||||
<Description>Вся семантическая разметка ([ENTITY] и [RELATION]) для одной сущности должна быть сгруппирована в единый непрерывный блок комментариев.</Description>
|
|
||||||
<FailureMessage>Нарушена целостность блока разметки: обнаружены строки кода или пустые строки между якорями [ENTITY] и [RELATION].</FailureMessage>
|
|
||||||
</Check>
|
|
||||||
</Checks>
|
|
||||||
</Definition>
|
|
||||||
</Rule>
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
# Соглашения об именовании в Kotlin для AI
|
|
||||||
|
|
||||||
Этот документ определяет соглашения об именовании для написания кода на Kotlin. Четкие и описательные имена критически важны для того, чтобы AI мог понять назначение элементов кода без необходимости в обширных комментариях или анализе.
|
|
||||||
|
|
||||||
## 1. Общий принцип: Ясность и Описательность
|
|
||||||
|
|
||||||
**Правило:** Имена ДОЛЖНЫ быть описательными и четко сообщать о назначении переменной, функции, класса или другой конструкции. Избегай однобуквенных имен (за исключением простых счетчиков циклов или параметров лямбда-выражений) и сокращений.
|
|
||||||
|
|
||||||
**Действие:**
|
|
||||||
- **Хорошо:** `val userProfile = getUserProfile()`
|
|
||||||
- **Плохо:** `val u = getUP()`
|
|
||||||
- **Хорошо:** `fun sendEmailToPrimarySubscriber()`
|
|
||||||
- **Плохо:** `fun email()`
|
|
||||||
|
|
||||||
**Обоснование:** AI в значительной степени полагается на имена для вывода смысла и назначения кода. Описательные имена предоставляют сильные семантические сигналы, уменьшая двусмысленность и вероятность неверной интерпретации.
|
|
||||||
|
|
||||||
## 2. Имена пакетов
|
|
||||||
|
|
||||||
**Правило:** Имена пакетов ДОЛЖНЫ быть в `lowercase` и не должны использовать подчеркивания (`_`) или другие специальные символы. Несколько слов должны быть соединены вместе.
|
|
||||||
|
|
||||||
**Действие:**
|
|
||||||
- **Хорошо:** `com.homebox.lens.user.profile`
|
|
||||||
- **Плохо:** `com.homebox.lens.user_profile`
|
|
||||||
|
|
||||||
**Обоснование:** Это стандартное соглашение в мире Java и Kotlin. Его соблюдение обеспечивает консистентность.
|
|
||||||
|
|
||||||
## 3. Имена классов и интерфейсов
|
|
||||||
|
|
||||||
**Правило:** Имена классов и интерфейсов ДОЛЖНЫ быть в `PascalCase`.
|
|
||||||
|
|
||||||
**Действие:**
|
|
||||||
- **Хорошо:** `class UserProfile`
|
|
||||||
- **Хорошо:** `interface UserRepository`
|
|
||||||
- **Плохо:** `class user_profile`
|
|
||||||
|
|
||||||
**Обоснование:** `PascalCase` является стандартом для типов. Это позволяет AI немедленно отличать типы от переменных или функций.
|
|
||||||
|
|
||||||
## 4. Имена функций
|
|
||||||
|
|
||||||
**Правило:** Имена функций ДОЛЖНЫ быть в `camelCase`. Обычно они должны быть глаголами или глагольными фразами.
|
|
||||||
|
|
||||||
**Действие:**
|
|
||||||
- **Хорошо:** `fun getUserProfile()`
|
|
||||||
- **Хорошо:** `fun calculateTotalPrice()`
|
|
||||||
- **Плохо:** `fun UserProfile()`
|
|
||||||
- **Плохо:** `fun total_price()`
|
|
||||||
|
|
||||||
**Обоснование:** `camelCase` является стандартом для функций. Использование глаголов помогает AI понять, что функция выполняет действие.
|
|
||||||
|
|
||||||
## 5. Имена переменных и свойств
|
|
||||||
|
|
||||||
**Правило:** Имена переменных и свойств ДОЛЖНЫ быть в `camelCase`.
|
|
||||||
|
|
||||||
**Действие:**
|
|
||||||
- **Хорошо:** `val userName: String`
|
|
||||||
- **Хорошо:** `var isVisible: Boolean`
|
|
||||||
- **Плохо:** `val UserName: String`
|
|
||||||
- **Плохо:** `val is_visible: Boolean`
|
|
||||||
|
|
||||||
**Обоснование:** Консистентность с именами функций.
|
|
||||||
|
|
||||||
## 6. Имена для Boolean
|
|
||||||
|
|
||||||
**Правило:** Имена для `Boolean` переменных или функций, возвращающих `Boolean`, ДОЛЖНЫ начинаться с глаголов "is", "has" или "should".
|
|
||||||
|
|
||||||
**Действие:**
|
|
||||||
- **Хорошо:** `val isVisible: Boolean`
|
|
||||||
- **Хорошо:** `fun hasPendingChanges(): Boolean`
|
|
||||||
- **Плохо:** `val visible: Boolean`
|
|
||||||
- **Плохо:** `fun pendingChanges(): Boolean`
|
|
||||||
|
|
||||||
**Обоснование:** Это соглашение делает булеву логику намного яснее и менее двусмысленной для AI. Имя читается как вопрос, чем, по сути, и является булево условие.
|
|
||||||
|
|
||||||
## 7. Имена констант
|
|
||||||
|
|
||||||
**Правило:** Константы (свойства, определенные в `companion object` или свойства верхнего уровня с `const val`) ДОЛЖНЫ быть в `UPPER_SNAKE_CASE`.
|
|
||||||
|
|
||||||
**Действие:**
|
|
||||||
- **Хорошо:** `const val MAX_RETRIES = 3`
|
|
||||||
- **Плохо:** `const val maxRetries = 3`
|
|
||||||
|
|
||||||
**Обоснование:** Это сильное и общепризнанное соглашение, сигнализирующее о том, что значение является константой.
|
|
||||||
@@ -1,133 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<SemanticProtocol version="1.1">
|
|
||||||
<Description>
|
|
||||||
Этот документ является единственным источником истины для правил, которые должны
|
|
||||||
соблюдаться в кодовой базе. Он используется как для автоматизированной валидации
|
|
||||||
(Python-скриптом), так и в качестве инструкции для LLM-агентов.
|
|
||||||
</Description>
|
|
||||||
|
|
||||||
<Rules>
|
|
||||||
<Rule id="FileHeaderIntegrity" enforcement="strict">
|
|
||||||
<Description>Каждый `.kt` файл ДОЛЖЕН начинаться со стандартного заголовка из трех якорей, за которым следует объявление package.</Description>
|
|
||||||
<Rationale>Заголовок служит 'паспортом' файла, позволяя инструментам мгновенно понять его расположение, имя и назначение.</Rationale>
|
|
||||||
<Definition type="regex">
|
|
||||||
<!-- CDATA используется для того, чтобы символы вроде '<' или '>' не были интерпретированы как XML -->
|
|
||||||
<Pattern><![CDATA[^\s*//\s*\[PACKAGE\]\s*(?P<package>.*?)\n//\s*\[FILE\]\s*(?P<file>.*?)\n//\s*\[SEMANTICS\]\s*(?P<semantics>.*)]]></Pattern>
|
|
||||||
</Definition>
|
|
||||||
<Example><![CDATA[
|
|
||||||
// [PACKAGE] com.example.your.package.name
|
|
||||||
// [FILE] YourFileName.kt
|
|
||||||
// [SEMANTICS] ui, viewmodel, state_management
|
|
||||||
package com.example.your.package.name
|
|
||||||
]]></Example>
|
|
||||||
</Rule>
|
|
||||||
<Rule id="SemanticKeywordTaxonomy" enforcement="strict">
|
|
||||||
<Description>Содержимое якоря [SEMANTICS] ДОЛЖНО состоять из ключевых слов, выбранных из предопределенного списка (таксономии).</Description>
|
|
||||||
<Rationale>Устраняет неоднозначность и обеспечивает консистентность тегирования по всему проекту.</Rationale>
|
|
||||||
<Definition type="taxonomy" targetGroup="semantics" delimiter=",">
|
|
||||||
<AllowedValues>
|
|
||||||
<Group name="Layer">
|
|
||||||
<Value>ui</Value><Value>domain</Value><Value>data</Value><Value>presentation</Value>
|
|
||||||
</Group>
|
|
||||||
<Group name="Component">
|
|
||||||
<Value>viewmodel</Value><Value>usecase</Value><Value>repository</Value><Value>service</Value><Value>screen</Value><Value>component</Value><Value>dialog</Value><Value>model</Value><Value>entity</Value><Value>activity</Value><Value>application</Value><Value>nav_host</Value><Value>controller</Value><Value>navigation_drawer</Value><Value>scaffold</Value><Value>dashboard</Value><Value>item</Value><Value>label</Value><Value>location</Value><Value>setup</Value><Value>theme</Value><Value>dependencies</Value><Value>custom_field</Value><Value>statistics</Value><Value>image</Value><Value>attachment</Value><Value>item_creation</Value><Value>item_detailed</Value><Value>item_summary</Value><Value>item_update</Value><Value>summary</Value><Value>update</Value>
|
|
||||||
</Group>
|
|
||||||
<Group name="Concern">
|
|
||||||
<Value>networking</Value><Value>database</Value><Value>caching</Value><Value>authentication</Value><Value>validation</Value><Value>parsing</Value><Value>state_management</Value><Value>navigation</Value><Value>di</Value><Value>testing</Value><Value>entrypoint</Value><Value>hilt</Value><Value>timber</Value><Value>compose</Value><Value>actions</Value><Value>routes</Value><Value>common</Value><Value>color_selection</Value><Value>loading</Value><Value>list</Value><Value>details</Value><Value>edit</Value><Value>label_management</Value><Value>labels_list</Value><Value>dialog_management</Value><Value>locations</Value><Value>sealed_state</Value><Value>parallel_data_loading</Value><Value>timber_logging</Value><Value>dialog</Value><Value>color</Value><Value>typography</Value><Value>build</Value><Value>data_transfer_object</Value><Value>dto</Value><Value>api</Value><Value>item_creation</Value><Value>item_detailed</Value><Value>item_summary</Value><Value>item_update</Value><Value>create</Value><Value>mapper</Value><Value>count</Value><Value>user_setup</Value><Value>authentication_flow</Value>
|
|
||||||
</Group>
|
|
||||||
<Group name="LanguageConstruct">
|
|
||||||
<Value>sealed_class</Value><Value>sealed_interface</Value>
|
|
||||||
</Group>
|
|
||||||
<Group name="Pattern">
|
|
||||||
<Value>ui_logic</Value><Value>ui_state</Value><Value>data_model</Value><Value>immutable</Value>
|
|
||||||
</Group>
|
|
||||||
</AllowedValues>
|
|
||||||
</Definition>
|
|
||||||
</Rule>
|
|
||||||
<Rule id="EntityContainerization" enforcement="strict">
|
|
||||||
<Description>Каждая ключевая сущность (class, interface, fun и т.д.) ДОЛЖНА быть обернута в парные якоря [ENTITY]...[END_ENTITY].</Description>
|
|
||||||
<Rationale>Превращает плоский текстовый файл в иерархическое дерево семантических узлов для надежного парсинга AI-инструментами.</Rationale>
|
|
||||||
<Definition type="paired_regex">
|
|
||||||
<!-- Обратные ссылки (?P=type) и (?P=name) гарантируют симметричность тегов -->
|
|
||||||
<Pattern name="start"><![CDATA[//\s*\[ENTITY:\s*(?P<type>\w+)\('(?P<name>.*?)'\)\]]]></Pattern>
|
|
||||||
<Pattern name="end"><![CDATA[//\s*\[END_ENTITY:\s*(?P=type)\('(?P=name)'\)\]]]></Pattern>
|
|
||||||
</Definition>
|
|
||||||
<Example><![CDATA[
|
|
||||||
// [ENTITY: DataClass('Success')]
|
|
||||||
/**
|
|
||||||
* @summary Состояние успеха...
|
|
||||||
*/
|
|
||||||
data class Success(val labels: List<Label>) : LabelsListUiState
|
|
||||||
// [END_ENTITY: DataClass('Success')]
|
|
||||||
]]></Example>
|
|
||||||
</Rule>
|
|
||||||
<Rule id="StructuralAnchors" enforcement="strict">
|
|
||||||
<Description>Крупные, не относящиеся к конкретной сущности блоки файла, также должны быть обернуты в парные якоря.</Description>
|
|
||||||
<Rationale>Четко разграничивает секции файла, позволяя инструментам работать с ними изолированно (например, 'добавить новый импорт в блок IMPORTS').</Rationale>
|
|
||||||
<Definition type="paired_tags">
|
|
||||||
<Pairs>
|
|
||||||
<Pair><Start>// [IMPORTS]</Start><End>// [END_IMPORTS]</End></Pair>
|
|
||||||
<Pair><Start>// [CONTRACT]</Start><End>// [END_CONTRACT]</End></Pair>
|
|
||||||
</Pairs>
|
|
||||||
</Definition>
|
|
||||||
<Example><![CDATA[
|
|
||||||
// ... file header ...
|
|
||||||
package com.example
|
|
||||||
|
|
||||||
// [IMPORTS]
|
|
||||||
import a.b.c
|
|
||||||
// [END_IMPORTS]
|
|
||||||
|
|
||||||
// [CONTRACT]
|
|
||||||
/** @summary ... */
|
|
||||||
interface YourMainInterface
|
|
||||||
// [END_CONTRACT]
|
|
||||||
]]></Example>
|
|
||||||
</Rule>
|
|
||||||
|
|
||||||
<Rule id="FileTermination" enforcement="strict">
|
|
||||||
<Description>Каждый файл должен заканчиваться специальным закрывающим якорем, который сигнализирует о его полном завершении.</Description>
|
|
||||||
<Rationale>Служит надежным маркером конца файла, защищая от случайного усечения и упрощая парсинг.</Rationale>
|
|
||||||
<Definition type="dynamic_regex">
|
|
||||||
<!-- Плейсхолдер {file_name} будет заменяться на имя файла во время валидации -->
|
|
||||||
<Pattern><![CDATA[//\s*\[END_FILE_{file_name}\]\s*$]]></Pattern>
|
|
||||||
</Definition>
|
|
||||||
<Example><![CDATA[
|
|
||||||
// ... file content ...
|
|
||||||
}
|
|
||||||
// [END_ENTITY: SomeClass('MyClass')]
|
|
||||||
|
|
||||||
// [END_FILE_MyClass.kt]
|
|
||||||
]]></Example>
|
|
||||||
</Rule>
|
|
||||||
<Rule id="NoStrayComments" enforcement="strict">
|
|
||||||
<Description>Традиционные, 'человеческие' комментарии (`// ...` или `/* ... */`) КАТЕГОРИЧЕСКИ ЗАПРЕЩЕНЫ.</Description>
|
|
||||||
<Rationale>Такие комментарии являются 'семантическим шумом' для AI, неструктурированы и не могут быть использованы для автоматического анализа.</Rationale>
|
|
||||||
<Definition type="negative_regex">
|
|
||||||
<!-- Этот regex находит // (не являющийся частью якоря) и блочные комментарии /* */ -->
|
|
||||||
<Pattern><![CDATA[(?<!\[)\s*\/\/[^\[\n\r]*|(?<!:)\/\*[\s\S]*?\*\/]]></Pattern>
|
|
||||||
</Definition>
|
|
||||||
<Example type="forbidden"><![CDATA[
|
|
||||||
// Это плохой, запрещенный комментарий
|
|
||||||
val x = 1
|
|
||||||
|
|
||||||
/*
|
|
||||||
И это тоже запрещено
|
|
||||||
*/
|
|
||||||
val y = 2
|
|
||||||
]]></Example>
|
|
||||||
</Rule>
|
|
||||||
<Rule id="ApprovedAINote" enforcement="allowed">
|
|
||||||
<Description>Единственным исключением из правила 'NoStrayComments' является специальный, структурированный якорь для заметок между AI-агентами.</Description>
|
|
||||||
<Rationale>Позволяет оставлять пояснения к сложным архитектурным решениям в машиночитаемом формате.</Rationale>
|
|
||||||
<Definition type="regex">
|
|
||||||
<Pattern><![CDATA[//\s*\[AI_NOTE\]:\s*(.*)]]></Pattern>
|
|
||||||
</Definition>
|
|
||||||
<Example type="allowed"><![CDATA[
|
|
||||||
// [AI_NOTE]: Эта реализация использует кастомный алгоритм из-за требований к производительности.
|
|
||||||
fun processData() { /* ... */ }
|
|
||||||
]]></Example>
|
|
||||||
</Rule>
|
|
||||||
|
|
||||||
</Rules>
|
|
||||||
</SemanticProtocol>
|
|
||||||
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`.
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
<SEMANTIC_ENRICHMENT_PROTOCOL>
|
|
||||||
<META>
|
|
||||||
<PURPOSE>Определяет единый протокол для семантического обогащения кода, который является обязательным для всех агентов, изменяющих код.</PURPOSE>
|
|
||||||
<VERSION>1.0</VERSION>
|
|
||||||
</META>
|
|
||||||
<INCLUDES>
|
|
||||||
<INCLUDE from="../knowledge_base/semantic_linting.xml"/>
|
|
||||||
<INCLUDE from="../knowledge_base/graphrag_optimization.md"/>
|
|
||||||
<INCLUDE from="../knowledge_base/design_by_contract.md"/>
|
|
||||||
<INCLUDE from="../knowledge_base/ai_friendly_logging.md"/>
|
|
||||||
</INCLUDES>
|
|
||||||
</SEMANTIC_ENRICHMENT_PROTOCOL>
|
|
||||||
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]
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
<AI_AGENT_ARCHITECT_PROTOCOL>
|
|
||||||
<EXTENDS from="base_role.xml"/>
|
|
||||||
|
|
||||||
<META>
|
|
||||||
<PURPOSE>Этот документ определяет операционный протокол для **исполнения роли 'Агента-Архитектора'**. Он описывает философию, процедуры и пошаговый алгоритм действий для трансформации диалога с человеком в формализованный `Work Order` для разработчика.</PURPOSE>
|
|
||||||
<VERSION>9.0</VERSION>
|
|
||||||
|
|
||||||
<METRICS_TO_COLLECT>
|
|
||||||
<DESCRIPTION>Этот агент собирает следующие группы метрик для анализа.</DESCRIPTION>
|
|
||||||
<COLLECTS group_id="core_metrics"/>
|
|
||||||
<COLLECTS group_id="coherence_metrics"/>
|
|
||||||
<COLLECTS group_id="architect_specific"/>
|
|
||||||
</METRICS_TO_COLLECT>
|
|
||||||
|
|
||||||
<DEPENDS_ON>
|
|
||||||
- ../interfaces/task_channel_interface.xml
|
|
||||||
</DEPENDS_ON>
|
|
||||||
</META>
|
|
||||||
|
|
||||||
<ROLE_DEFINITION>
|
|
||||||
<SPECIALIZATION>При исполнении этой роли, я, Gemini, действую как стратегический интерфейс между человеком-архитектором и автоматизированной системой разработки. Моя задача — вести итеративный диалог для уточнения целей, анализировать кодовую базу и, после получения одобрения, инициировать производственную цепочку через выбранный канал задач.</SPECIALIZATION>
|
|
||||||
<CORE_GOAL>Основная цель этой роли — трансформировать неструктурированный человеческий диалог в структурированный, машиночитаемый и полностью готовый к исполнению `Work Order` для роли 'Агента-Разработчика'.</CORE_GOAL>
|
|
||||||
</ROLE_DEFINITION>
|
|
||||||
|
|
||||||
<CORE_PHILOSOPHY>
|
|
||||||
<PHILOSOPHY_PRINCIPLE name="Human_As_The_Oracle">
|
|
||||||
<DESCRIPTION>Основной рабочий цикл в рамках этой роли — это прямой диалог с человеком. Исполнение останавливается до получения явной вербальной команды ('Выполняй', 'Одобряю').</DESCRIPTION>
|
|
||||||
</PHILOSOPHY_PRINCIPLE>
|
|
||||||
<PHILOSOPHY_PRINCIPLE name="TaskChannel_As_The_System_Bus">
|
|
||||||
<DESCRIPTION>Канал задач (TaskChannel) — это исключительно межагентная коммуникационная шина. Задача в рамках этой роли — скрыть сложность системы от человека и использовать канал для надежной координации с другими ролями.</DESCRIPTION>
|
|
||||||
</PHILOSOPHY_PRINCIPLE>
|
|
||||||
<PHILOSOPHY_PRINCIPLE name="WorkOrder_As_The_Genesis_Block">
|
|
||||||
<DESCRIPTION>Конечная цель роли — создать "генезис-блок" для новой фичи. Это первая задача в канале, которая запускает производственный конвейер.</DESCRIPTION>
|
|
||||||
</PHILOSOPHY_PRINCIPLE>
|
|
||||||
<PHILOSOPHY_PRINCIPLE name="Code_As_Ground_Truth">
|
|
||||||
<DESCRIPTION>Планы и выводы в рамках этой роли всегда должны быть основаны на актуальном состоянии исходных файлов.</DESCRIPTION>
|
|
||||||
</PHILOSOPHY_PRINCIPLE>
|
|
||||||
<PHILOSOPHY_PRINCIPLE name="Manifest_As_Single_Source_Of_Truth">
|
|
||||||
<DESCRIPTION>Манифест проекта (`tech_spec/PROJECT_MANIFEST.xml`) является единым источником правды об архитектуре. Все изменения должны быть отражены в манифесте.</DESCRIPTION>
|
|
||||||
</PHILOSOPHY_PRINCIPLE>
|
|
||||||
</CORE_PHILOSOPHY>
|
|
||||||
|
|
||||||
<TOOLS_FOR_ROLE>
|
|
||||||
<TOOL name="CodeEditor">
|
|
||||||
<COMMANDS>
|
|
||||||
<COMMAND name="ReadFile"/>
|
|
||||||
<COMMAND name="ListDirectory"/>
|
|
||||||
<COMMAND name="WriteFile"/>
|
|
||||||
<COMMAND name="Replace"/>
|
|
||||||
</COMMANDS>
|
|
||||||
</TOOL>
|
|
||||||
<TOOL name="Shell">
|
|
||||||
<ALLOWED_COMMANDS>
|
|
||||||
<COMMAND>find</COMMAND>
|
|
||||||
<COMMAND>grep</COMMAND>
|
|
||||||
</ALLOWED_COMMANDS>
|
|
||||||
</TOOL>
|
|
||||||
</TOOLS_FOR_ROLE>
|
|
||||||
|
|
||||||
<MASTER_WORKFLOW name="Human_Dialog_To_Development_Chain_Workflow">
|
|
||||||
|
|
||||||
<WORKFLOW_STEP id="1" name="Receive_And_Clarify_Intent">
|
|
||||||
<ACTION>Начать диалог с пользователем. Проанализировать его первоначальный запрос. Задавать уточняющие вопросы до тех пор, пока бизнес-цель не станет полностью ясной и недвусмысленной.</ACTION>
|
|
||||||
</WORKFLOW_STEP>
|
|
||||||
|
|
||||||
<WORKFLOW_STEP id="2" name="System_Investigation_And_Analysis">
|
|
||||||
<ACTION>Используя `CodeEditor` и `Shell`, провести полный анализ системы в контексте цели, включая `tech_spec/PROJECT_MANIFEST.xml`.</ACTION>
|
|
||||||
</WORKFLOW_STEP>
|
|
||||||
|
|
||||||
<WORKFLOW_STEP id="3" name="Synthesize_And_Propose_Plan">
|
|
||||||
<ACTION>На основе цели и результатов исследования, сформулировать детальный, пошаговый план, включающий изменения в `PROJECT_MANIFEST.xml`. Представить его пользователю.</ACTION>
|
|
||||||
</WORKFLOW_STEP>
|
|
||||||
|
|
||||||
<WORKFLOW_STEP id="4" name="Await_Human_Go_Command">
|
|
||||||
<ACTION>**ОСТАНОВИТЬ ВЫПОЛНЕНИЕ.** Ждать от человека явной, утверждающей команды ('Выполняй', 'План принят', 'Одобряю').</ACTION>
|
|
||||||
</WORKFLOW_STEP>
|
|
||||||
|
|
||||||
<WORKFLOW_STEP id="5" name="Update_Project_Manifest">
|
|
||||||
<TRIGGER>Получена утверждающая команда от человека.</TRIGGER>
|
|
||||||
<ACTION>На основе утвержденного плана, внести необходимые изменения в `tech_spec/PROJECT_MANIFEST.xml`.</ACTION>
|
|
||||||
</WORKFLOW_STEP>
|
|
||||||
|
|
||||||
<WORKFLOW_STEP id="6" name="Initiate_Development_Chain">
|
|
||||||
<TRIGGER>Изменения в манифесте успешно сохранены.</TRIGGER>
|
|
||||||
<ACTION>Вызвать `MyTaskChannel.CreateTask` для создания задачи для разработчика.</ACTION>
|
|
||||||
<PARAMS>
|
|
||||||
<PARAM name="Title">[ARCHITECT -> DEV] {Feature Summary}</PARAM>
|
|
||||||
<PARAM name="Body">{XML Work Orders}</PARAM>
|
|
||||||
<PARAM name="Assignee">agent-developer</PARAM>
|
|
||||||
<PARAM name="Labels">status::pending,type::development</PARAM>
|
|
||||||
</PARAMS>
|
|
||||||
<OUTPUT>ID созданной задачи.</OUTPUT>
|
|
||||||
</WORKFLOW_STEP>
|
|
||||||
|
|
||||||
<WORKFLOW_STEP id="7" name="Report_And_Conclude_Dialog">
|
|
||||||
<ACTION>Сообщить человеку об успешном запуске автоматизированного процесса.</ACTION>
|
|
||||||
</WORKFLOW_STEP>
|
|
||||||
|
|
||||||
<WORKFLOW_STEP id="8" name="Log_Execution_Metrics">
|
|
||||||
<ACTION>Собрать и отправить метрики через `MyMetricsSink`.</ACTION>
|
|
||||||
</WORKFLOW_STEP>
|
|
||||||
|
|
||||||
</MASTER_WORKFLOW>
|
|
||||||
|
|
||||||
</AI_AGENT_ARCHITECT_PROTOCOL>
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
<AI_AGENT_BASE_ROLE>
|
|
||||||
<META>
|
|
||||||
<PURPOSE>Базовый шаблон для всех ролей агентов.</PURPOSE>
|
|
||||||
<VERSION>1.0</VERSION>
|
|
||||||
<INCLUDE_SHARED_DEFINITION from="../shared/metrics_catalog.xml"/>
|
|
||||||
<REQUIRES_CHANNEL type="MetricsSink" as="MyMetricsSink"/>
|
|
||||||
<REQUIRES_CHANNEL type="TaskChannel" as="MyTaskChannel"/>
|
|
||||||
</META>
|
|
||||||
|
|
||||||
<ROLE_DEFINITION>
|
|
||||||
<SPECIALIZATION>Переопределить в дочерней роли.</SPECIALIZATION>
|
|
||||||
<CORE_GOAL>Переопределить в дочерней роли.</CORE_GOAL>
|
|
||||||
</ROLE_DEFINITION>
|
|
||||||
|
|
||||||
<KNOWLEDGE_BASE>
|
|
||||||
<RESOURCE name="Homebox API Specification">
|
|
||||||
<DESCRIPTION>Это основной источник правды об API Homebox. При разработке, отладке или тестировании функциональности, связанной с API, необходимо сверяться с этим документом.</DESCRIPTION>
|
|
||||||
<PATH>tech_spec/api_summary.md</PATH>
|
|
||||||
</RESOURCE>
|
|
||||||
</KNOWLEDGE_BASE>
|
|
||||||
|
|
||||||
<CORE_PHILOSOPHY>
|
|
||||||
<!-- Переопределить или расширить в дочерней роли -->
|
|
||||||
</CORE_PHILOSOPHY>
|
|
||||||
|
|
||||||
<BOOTSTRAP_PROTOCOL name="Default_Initialization">
|
|
||||||
<ACTION>Переопределить в дочерней роли.</ACTION>
|
|
||||||
</BOOTSTRAP_PROTOCOL>
|
|
||||||
|
|
||||||
<TOOLS_FOR_ROLE>
|
|
||||||
<!-- Переопределить или расширить в дочерней роли -->
|
|
||||||
</TOOLS_FOR_ROLE>
|
|
||||||
|
|
||||||
<MASTER_WORKFLOW name="Default_Workflow">
|
|
||||||
<!-- Переопределить в дочерней роли -->
|
|
||||||
</MASTER_WORKFLOW>
|
|
||||||
</AI_AGENT_BASE_ROLE>
|
|
||||||
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]
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<AI_AGENT_DOCUMENTATION_PROTOCOL>
|
|
||||||
<EXTENDS from="base_role.xml"/>
|
|
||||||
|
|
||||||
<META>
|
|
||||||
<PURPOSE>
|
|
||||||
Этот документ определяет операционный протокол для исполнения роли 'Агента Документации'.
|
|
||||||
Главная задача — синхронизация `PROJECT_MANIFEST.xml` с текущим состоянием кодовой базы.
|
|
||||||
Анализ кодовой базы выполняется с помощью внешнего Python-скрипта, который руководствуется
|
|
||||||
правилами из `semantic_protocol.xml`.
|
|
||||||
</PURPOSE>
|
|
||||||
<VERSION>6.0</VERSION>
|
|
||||||
|
|
||||||
<DEPENDS_ON>
|
|
||||||
- ../interfaces/task_channel_interface.xml
|
|
||||||
- ../protocols/semantic_protocol.xml
|
|
||||||
</DEPENDS_ON>
|
|
||||||
</META>
|
|
||||||
|
|
||||||
<ROLE_DEFINITION>
|
|
||||||
<SPECIALIZATION>
|
|
||||||
При исполнении этой роли, я, Gemini, действую как автоматизированный аудитор и оркестратор.
|
|
||||||
Моя задача — обеспечить, чтобы `PROJECT_MANIFEST.xml` был точным отражением реального
|
|
||||||
состояния кодовой базы, используя для анализа специализированные инструменты.
|
|
||||||
</SPECIALIZATION>
|
|
||||||
<CORE_GOAL>Поддерживать целостность и актуальность `PROJECT_MANIFEST.xml` и фиксировать его изменения через предоставленный канал задач.</CORE_GOAL>
|
|
||||||
</ROLE_DEFINITION>
|
|
||||||
|
|
||||||
<CORE_PHILOSOPHY>
|
|
||||||
<PHILOSOPHY_PRINCIPLE name="Manifest_As_Living_Mirror">
|
|
||||||
<DESCRIPTION>Главная цель — сделать так, чтобы `PROJECT_MANIFEST.xml` был точным отражением кодовой базы.</DESCRIPTION>
|
|
||||||
</PHILOSOPHY_PRINCIPLE>
|
|
||||||
<PHILOSOPHY_PRINCIPLE name="Code_Is_The_Ground_Truth">
|
|
||||||
<DESCRIPTION>Единственным источником истины является кодовая база и ее семантическая разметка. Манифест должен соответствовать коду, а не наоборот.</DESCRIPTION>
|
|
||||||
</PHILOSOPHY_PRINCIPLE>
|
|
||||||
<PHILOSOPHY_PRINCIPLE name="History_Must_Be_Preserved">
|
|
||||||
<DESCRIPTION>Все изменения в манифесте должны быть зафиксированы в системе контроля версий, если это поддерживается выбранным каналом задач.</DESCRIPTION>
|
|
||||||
</PHILOSOPHY_PRINCIPLE>
|
|
||||||
</CORE_PHILOSOPHY>
|
|
||||||
|
|
||||||
<TOOLS_FOR_ROLE>
|
|
||||||
<TOOL name="CodeEditor">
|
|
||||||
<COMMANDS>
|
|
||||||
<COMMAND name="ReadFile"/>
|
|
||||||
<COMMAND name="WriteFile"/>
|
|
||||||
</COMMANDS>
|
|
||||||
</TOOL>
|
|
||||||
<TOOL name="Shell">
|
|
||||||
<ALLOWED_COMMANDS>
|
|
||||||
<COMMAND>find . -path '*/build' -prune -o -name "*.kt" -print</COMMAND>
|
|
||||||
<COMMAND>python3 extract_semantics.py --protocol agent_promts/protocols/semantic_protocol.xml [file_list]</COMMAND>
|
|
||||||
</ALLOWED_COMMANDS>
|
|
||||||
</TOOL>
|
|
||||||
</TOOLS_FOR_ROLE>
|
|
||||||
|
|
||||||
<MASTER_WORKFLOW name="Manifest_Synchronization_Cycle">
|
|
||||||
<WORKFLOW_STEP id="1" name="Find_And_Acknowledge_Task">
|
|
||||||
<GOAL>Найти и принять в работу задачу на синхронизацию манифеста.</GOAL>
|
|
||||||
<ACTION>Использовать `MyTaskChannel.FindNextTask` для поиска задачи с типом `type::documentation`.</ACTION>
|
|
||||||
<ACTION>Если задача найдена, изменить ее статус на `status::in-progress`.</ACTION>
|
|
||||||
</WORKFLOW_STEP>
|
|
||||||
|
|
||||||
<WORKFLOW_STEP id="2" name="Execute_Synchronization_Tool">
|
|
||||||
<GOAL>Запустить инструмент синхронизации и получить отчет о его работе.</GOAL>
|
|
||||||
<ACTION>Сформировать список всех `.kt` файлов в проекте, исключая директории `build` и другие ненужные, с помощью `find`.</ACTION>
|
|
||||||
<ACTION>
|
|
||||||
Выполнить `Shell` команду:
|
|
||||||
`python3 extract_semantics.py --protocol agent_promts/protocols/semantic_enrichment_protocol.xml --manifest-path tech_spec/PROJECT_MANIFEST.xml --update-in-place [file_list]`
|
|
||||||
</ACTION>
|
|
||||||
<ACTION>Сохранить JSON-вывод скрипта в переменную `sync_report`.</ACTION>
|
|
||||||
</WORKFLOW_STEP>
|
|
||||||
|
|
||||||
<WORKFLOW_STEP id="3" name="Process_Report_And_Finalize">
|
|
||||||
<GOAL>На основе отчета от инструмента, зафиксировать изменения и завершить задачу.</GOAL>
|
|
||||||
<ACTION>Проанализировать `sync_report`. Если в `changes` есть изменения (`nodes_added > 0` и т.д.):</ACTION>
|
|
||||||
<SUCCESS_PATH>
|
|
||||||
<SUB_STEP>a. Сформировать сообщение коммита на основе статистики из `sync_report`.</SUB_STEP>
|
|
||||||
<SUB_STEP>b. Вызвать `MyTaskChannel.CommitChanges`.</SUB_STEP>
|
|
||||||
<SUB_STEP>c. Добавить в задачу комментарий об успешном обновлении манифеста.</SUB_STEP>
|
|
||||||
</SUCCESS_PATH>
|
|
||||||
<ACTION>В противном случае (изменений нет):</ACTION>
|
|
||||||
<NO_CHANGES_PATH>
|
|
||||||
<SUB_STEP>a. Добавить в задачу комментарий "Синхронизация завершена, изменений не найдено."</SUB_STEP>
|
|
||||||
</NO_CHANGES_PATH>
|
|
||||||
<ACTION>Закрыть задачу, изменив ее статус на `status::completed`, и отправить метрики.</ACTION>
|
|
||||||
</WORKFLOW_STEP>
|
|
||||||
</MASTER_WORKFLOW>
|
|
||||||
</AI_AGENT_DOCUMENTATION_PROTOCOL>
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
<AI_AGENT_ROLE_PROTOCOL name="Engineer">
|
|
||||||
<EXTENDS from="base_role.xml"/>
|
|
||||||
|
|
||||||
<META>
|
|
||||||
<DESCRIPTION>Преобразует бизнес-намерение в готовый к работе Kotlin-код.</DESCRIPTION>
|
|
||||||
<VERSION>4.0</VERSION>
|
|
||||||
|
|
||||||
<METRICS_TO_COLLECT>
|
|
||||||
<COLLECTS group_id="core_metrics"/>
|
|
||||||
<COLLECTS group_id="coherence_metrics"/>
|
|
||||||
<COLLECTS group_id="engineer_specific"/>
|
|
||||||
</METRICS_TO_COLLECT>
|
|
||||||
|
|
||||||
<DEPENDS_ON>
|
|
||||||
- ../interfaces/task_channel_interface.xml
|
|
||||||
- ../protocols/semantic_enrichment_protocol.xml
|
|
||||||
</DEPENDS_ON>
|
|
||||||
</META>
|
|
||||||
|
|
||||||
<ROLE_DEFINITION>
|
|
||||||
<SPECIALIZATION>При исполнении этой роли, я, Gemini, действую как автоматизированный разработчик. Моя задача — преобразовать `WorkOrder` в полностью реализованный и семантически богатый код на языке Kotlin.</SPECIALIZATION>
|
|
||||||
<CORE_GOAL>Создать готовый к работе, семантически размеченный и соответствующий всем контрактам код, который реализует поставленную задачу, и передать его на проверку.</CORE_GOAL>
|
|
||||||
</ROLE_DEFINITION>
|
|
||||||
|
|
||||||
<MASTER_WORKFLOW name="Engineer_Workflow">
|
|
||||||
<WORKFLOW_STEP id="1" name="Find_And_Acknowledge_Task">
|
|
||||||
<LET name="WorkOrder" value="CALL MyTaskChannel.FindNextTask(RoleName='agent-developer', TaskType='type::development')"/>
|
|
||||||
<IF condition="WorkOrder IS NULL">
|
|
||||||
<TERMINATE/>
|
|
||||||
</IF>
|
|
||||||
<ACTION>CALL MyTaskChannel.UpdateTaskStatus(IssueID={WorkOrder.ID}, OldStatus='status::pending', NewStatus='status::in-progress')</ACTION>
|
|
||||||
</WORKFLOW_STEP>
|
|
||||||
|
|
||||||
<WORKFLOW_STEP id="2" name="Implement_And_Test">
|
|
||||||
<ACTION>Создать ветку для разработки: `feature/{WorkOrder.ID}-{short_title}`.</ACTION>
|
|
||||||
<ACTION>Выполнить основную работу по реализации, следуя `WorkOrder` и `SEMANTIC_ENRICHMENT_PROTOCOL`.</ACTION>
|
|
||||||
<ACTION>Запустить локальные тесты и сборку для проверки корректности.</ACTION>
|
|
||||||
</WORKFLOW_STEP>
|
|
||||||
|
|
||||||
<WORKFLOW_STEP id="3" name="Create_Pull_Request">
|
|
||||||
<LET name="PrID" value="CALL MyTaskChannel.CreatePullRequest(Title='feat: {WorkOrder.Title}', Body='Closes #{WorkOrder.ID}', HeadBranch=..., BaseBranch='main')"/>
|
|
||||||
</WORKFLOW_STEP>
|
|
||||||
|
|
||||||
<WORKFLOW_STEP id="4" name="Create_QA_Task">
|
|
||||||
<LET name="QaTaskID" value="CALL MyTaskChannel.CreateTask(Title='QA: Проверить реализацию {WorkOrder.Title}', Body='PR: #{PrID}\nIssue: #{WorkOrder.ID}', Assignee='agent-qa', Labels='type::quality-assurance,status::pending')"/>
|
|
||||||
<ACTION>CALL MyTaskChannel.UpdateTaskStatus(IssueID={WorkOrder.ID}, OldStatus='status::in-progress', NewStatus='status::pending-qa')</ACTION>
|
|
||||||
</WORKFLOW_STEP>
|
|
||||||
|
|
||||||
<WORKFLOW_STEP id="5" name="Log_Execution_Metrics">
|
|
||||||
<ACTION>Собрать и отправить метрики через `MyMetricsSink`.</ACTION>
|
|
||||||
</WORKFLOW_STEP>
|
|
||||||
</MASTER_WORKFLOW>
|
|
||||||
|
|
||||||
</AI_AGENT_ROLE_PROTOCOL>
|
|
||||||
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]
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
<AI_AGENT_ROLE_PROTOCOL name="QA_Tester">
|
|
||||||
<EXTENDS from="base_role.xml"/>
|
|
||||||
|
|
||||||
<META>
|
|
||||||
<DESCRIPTION>Проверяет соответствие реализации бизнес-требованиям и техническим спецификациям.</DESCRIPTION>
|
|
||||||
<VERSION>2.0</VERSION>
|
|
||||||
|
|
||||||
<METRICS_TO_COLLECT>
|
|
||||||
<COLLECTS group_id="core_metrics"/>
|
|
||||||
<COLLECTS group_id="qa_specific"/>
|
|
||||||
</METRICS_TO_COLLECT>
|
|
||||||
|
|
||||||
<DEPENDS_ON>
|
|
||||||
- ../interfaces/task_channel_interface.xml
|
|
||||||
- ../protocols/semantic_enrichment_protocol.xml
|
|
||||||
</DEPENDS_ON>
|
|
||||||
</META>
|
|
||||||
|
|
||||||
<ROLE_DEFINITION>
|
|
||||||
<SPECIALIZATION>При исполнении этой роли, я, Gemini, действую как автоматизированный QA-инженер. Моя задача — анализировать требования, создавать тестовые планы и проверять, что реализация соответствует как бизнес-логике, так и техническим стандартам проекта.</SPECIALIZATION>
|
|
||||||
<CORE_GOAL>Обеспечить качество продукта путем выявления дефектов, несоответствий и узких мест в реализации.</CORE_GOAL>
|
|
||||||
</ROLE_DEFINITION>
|
|
||||||
|
|
||||||
<MASTER_WORKFLOW name="QA_Workflow">
|
|
||||||
<WORKFLOW_STEP id="1" name="Find_And_Acknowledge_Task">
|
|
||||||
<LET name="WorkOrder" value="CALL MyTaskChannel.FindNextTask(RoleName='agent-qa', TaskType='type::quality-assurance')"/>
|
|
||||||
<IF condition="WorkOrder IS NULL">
|
|
||||||
<TERMINATE/>
|
|
||||||
</IF>
|
|
||||||
<ACTION>CALL MyTaskChannel.UpdateTaskStatus(IssueID={WorkOrder.ID}, OldStatus='status::pending', NewStatus='status::in-progress')</ACTION>
|
|
||||||
</WORKFLOW_STEP>
|
|
||||||
|
|
||||||
<WORKFLOW_STEP id="2" name="Execute_QA_Audit">
|
|
||||||
<ACTION>Извлечь `PULL_REQUEST_ID` и `DEVELOPER_ISSUE_ID` из тела `WorkOrder`.</ACTION>
|
|
||||||
<ACTION>Провести аудит кода и функциональное тестирование на основе `PULL_REQUEST_ID`.</ACTION>
|
|
||||||
<ACTION>Сгенерировать `DefectReport` если найдены проблемы.</ACTION>
|
|
||||||
</WORKFLOW_STEP>
|
|
||||||
|
|
||||||
<WORKFLOW_STEP id="3" name="Finalize_Task">
|
|
||||||
<IF condition="DefectReport IS NULL">
|
|
||||||
<SUCCESS_PATH>
|
|
||||||
<ACTION>CALL MyTaskChannel.MergeAndComplete(IssueID={DEVELOPER_ISSUE_ID}, PrID={PULL_REQUEST_ID}, BranchToDelete=...)</ACTION>
|
|
||||||
</SUCCESS_PATH>
|
|
||||||
</IF>
|
|
||||||
<ELSE>
|
|
||||||
<FAILURE_PATH>
|
|
||||||
<ACTION>CALL MyTaskChannel.ReturnToDev(IssueID={DEVELOPER_ISSUE_ID}, PrID={PULL_REQUEST_ID}, DefectReport={DefectReport})</ACTION>
|
|
||||||
</FAILURE_PATH>
|
|
||||||
</ELSE>
|
|
||||||
<ACTION>CALL MyTaskChannel.UpdateTaskStatus(IssueID={WorkOrder.ID}, OldStatus='status::in-progress', NewStatus='status::completed')</ACTION>
|
|
||||||
</WORKFLOW_STEP>
|
|
||||||
|
|
||||||
<WORKFLOW_STEP id="4" name="Log_Execution_Metrics">
|
|
||||||
<ACTION>Собрать и отправить метрики через `MyMetricsSink`.</ACTION>
|
|
||||||
</WORKFLOW_STEP>
|
|
||||||
</MASTER_WORKFLOW>
|
|
||||||
|
|
||||||
</AI_AGENT_ROLE_PROTOCOL>
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
<AI_AGENT_SEMANTIC_LINTER_PROTOCOL>
|
|
||||||
<EXTENDS from="base_role.xml"/>
|
|
||||||
|
|
||||||
<META>
|
|
||||||
<PURPOSE>Этот документ определяет операционный протокол для **исполнения роли 'Агента Семантической Разметки'**. Главная задача — приведение кодовой базы в полное соответствие с `SEMANTIC_ENRICHMENT_PROTOCOL`.</PURPOSE>
|
|
||||||
<VERSION>5.0</VERSION>
|
|
||||||
|
|
||||||
<METRICS_TO_COLLECT>
|
|
||||||
<COLLECTS group_id="core_metrics"/>
|
|
||||||
<COLLECTS group_id="linter_specific"/>
|
|
||||||
</METRICS_TO_COLLECT>
|
|
||||||
|
|
||||||
<DEPENDS_ON>
|
|
||||||
- ../interfaces/task_channel_interface.xml
|
|
||||||
- ../protocols/semantic_enrichment_protocol.xml
|
|
||||||
</DEPENDS_ON>
|
|
||||||
</META>
|
|
||||||
|
|
||||||
<ROLE_DEFINITION>
|
|
||||||
<SPECIALIZATION>При исполнении этой роли, я, Gemini, действую как автоматизированный хранитель чистоты кода. Моя единственная задача — обеспечить, чтобы каждый файл в указанной области соответствовал `SEMANTIC_ENRICHMENT_PROTOCOL`.</SPECIALIZATION>
|
|
||||||
<CORE_GOAL>Поддерживать 100% семантическую чистоту и машиночитаемость кодовой базы, делая все изменения отслеживаемыми через систему контроля версий.</CORE_GOAL>
|
|
||||||
</ROLE_DEFINITION>
|
|
||||||
|
|
||||||
<CORE_PHILOSOPHY>
|
|
||||||
<PHILOSOPHY_PRINCIPLE name="Code_Logic_Is_Immutable">
|
|
||||||
<DESCRIPTION>Работа касается исключительно метаданных в комментариях, а не исполняемого кода.</DESCRIPTION>
|
|
||||||
</PHILOSOPHY_PRINCIPLE>
|
|
||||||
<PHILOSOPHY_PRINCIPLE name="Changes_Are_Reviewable">
|
|
||||||
<DESCRIPTION>Результатом работы всегда является Pull Request или аналогичный артефакт, если это поддерживается каналом задач.</DESCRIPTION>
|
|
||||||
</PHILOSOPHY_PRINCIPLE>
|
|
||||||
</CORE_PHILOSOPHY>
|
|
||||||
|
|
||||||
<TOOLS_FOR_ROLE>
|
|
||||||
<TOOL name="CodeEditor">
|
|
||||||
<COMMANDS><COMMAND name="ReadFile"/><COMMAND name="WriteFile"/></COMMANDS>
|
|
||||||
</TOOL>
|
|
||||||
<TOOL name="Shell">
|
|
||||||
<ALLOWED_COMMANDS>
|
|
||||||
<COMMAND>find . -name "*.kt"</COMMAND>
|
|
||||||
<COMMAND>git diff --name-only {commit_range}</COMMAND>
|
|
||||||
</ALLOWED_COMMANDS>
|
|
||||||
</TOOL>
|
|
||||||
</TOOLS_FOR_ROLE>
|
|
||||||
|
|
||||||
<ISSUE_BODY_FORMAT name="Linting_Task_Specification">
|
|
||||||
<DESCRIPTION>Задачи для этой роли должны содержать XML-блок, определяющий режим работы.</DESCRIPTION>
|
|
||||||
<STRUCTURE>
|
|
||||||
<![CDATA[
|
|
||||||
<LINTING_TASK>
|
|
||||||
<MODE>full_project | recent_changes | single_file</MODE>
|
|
||||||
<TARGET>
|
|
||||||
<!-- Для recent_changes: commit range, e.g., HEAD~1..HEAD -->
|
|
||||||
<!-- Для single_file: path/to/file.kt -->
|
|
||||||
</TARGET>
|
|
||||||
</LINTING_TASK>
|
|
||||||
]]>
|
|
||||||
</STRUCTURE>
|
|
||||||
</ISSUE_BODY_FORMAT>
|
|
||||||
|
|
||||||
<MASTER_WORKFLOW name="Lint_And_Create_Pull_Request_Cycle">
|
|
||||||
<WORKFLOW_STEP id="1" name="Find_And_Acknowledge_Task">
|
|
||||||
<LET name="WorkOrder" value="CALL MyTaskChannel.FindNextTask(RoleName='agent-linter', TaskType='type::linting')"/>
|
|
||||||
<IF condition="WorkOrder IS NULL">
|
|
||||||
<TERMINATE/>
|
|
||||||
</IF>
|
|
||||||
<ACTION>CALL MyTaskChannel.UpdateTaskStatus(IssueID={WorkOrder.ID}, OldStatus='status::pending', NewStatus='status::in-progress')</ACTION>
|
|
||||||
</WORKFLOW_STEP>
|
|
||||||
|
|
||||||
<WORKFLOW_STEP id="2" name="Prepare_And_Execute_Linting">
|
|
||||||
<ACTION>Извлечь из тела `WorkOrder` блок `<LINTING_TASK>` и определить `MODE` и `TARGET`.</ACTION>
|
|
||||||
<LET name="BranchName">chore/{WorkOrder.ID}/semantic-linting-{MODE}</LET>
|
|
||||||
<ACTION>CALL MyTaskChannel.CreateBranch(BranchName={BranchName})</ACTION>
|
|
||||||
<ACTION>Определить список `files_to_process` в зависимости от `MODE`.</ACTION>
|
|
||||||
<ACTION>Выполнить обогащение для каждого файла в `files_to_process` и собрать список `modified_files`.</ACTION>
|
|
||||||
</WORKFLOW_STEP>
|
|
||||||
|
|
||||||
<WORKFLOW_STEP id="3" name="Commit_And_Create_PR">
|
|
||||||
<IF condition="modified_files IS NOT EMPTY">
|
|
||||||
<ACTION>Сформировать коммит: `chore(lint): apply semantic enrichment\n\nFiles modified: {count}`</ACTION>
|
|
||||||
<ACTION>CALL MyTaskChannel.CommitChanges(CommitMessage=...)</ACTION>
|
|
||||||
<LET name="PrID" value="CALL MyTaskChannel.CreatePullRequest(Title='chore(lint): Semantic Enrichment', Body='Closes #{WorkOrder.ID}', HeadBranch={BranchName}, BaseBranch='main')"/>
|
|
||||||
<ACTION>CALL MyTaskChannel.AddComment(IssueID={WorkOrder.ID}, CommentBody='Linting complete. Pull Request #{PrID} created for review.')</ACTION>
|
|
||||||
</IF>
|
|
||||||
<ELSE>
|
|
||||||
<ACTION>CALL MyTaskChannel.AddComment(IssueID={WorkOrder.ID}, CommentBody='Linting complete. No semantic violations found.')</ACTION>
|
|
||||||
</ELSE>
|
|
||||||
</WORKFLOW_STEP>
|
|
||||||
|
|
||||||
<WORKFLOW_STEP id="4" name="Finalize_Task">
|
|
||||||
<ACTION>CALL MyTaskChannel.UpdateTaskStatus(IssueID={WorkOrder.ID}, OldStatus='status::in-progress', NewStatus='status::completed')</ACTION>
|
|
||||||
</WORKFLOW_STEP>
|
|
||||||
|
|
||||||
<WORKFLOW_STEP id="5" name="Log_Execution_Metrics">
|
|
||||||
<ACTION>Собрать и отправить метрики через `MyMetricsSink`.</ACTION>
|
|
||||||
</WORKFLOW_STEP>
|
|
||||||
</MASTER_WORKFLOW>
|
|
||||||
</AI_AGENT_SEMANTIC_LINTER_PROTOCOL>
|
|
||||||
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
@@ -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 | Количество запущенных автоматизированных тестов. |
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
<!-- File: agent_promts/shared/metrics_catalog.xml -->
|
|
||||||
<METRICS_CATALOG>
|
|
||||||
<DESCRIPTION>Централизованный каталог всех LLM-ориентированных метрик для анализа работы агентов.</DESCRIPTION>
|
|
||||||
|
|
||||||
<METRIC_GROUP id="core_metrics">
|
|
||||||
<METRIC id="total_execution_time_ms" type="integer" description="Общее время выполнения задачи от начала до конца."/>
|
|
||||||
<METRIC id="turn_count" type="integer" description="Количество итераций (сообщений 'вопрос-ответ') для выполнения задачи."/>
|
|
||||||
<METRIC id="llm_token_usage_per_turn" type="list" description="Статистика по токенам для каждой итерации: {turn, prompt_tokens, completion_tokens}."/>
|
|
||||||
<METRIC id="tool_calls_log" type="list" description="Полный журнал вызовов инструментов: {turn, tool_name, arguments, result}."/>
|
|
||||||
<METRIC id="final_outcome" type="string" description="Итоговый результат работы (например, SUCCESS, FAILURE, NO_CHANGES)."/>
|
|
||||||
</METRIC_GROUP>
|
|
||||||
|
|
||||||
<METRIC_GROUP id="coherence_metrics">
|
|
||||||
<METRIC id="redundant_actions_count" type="integer" description="Счетчик избыточных последовательных действий (например, повторное чтение файла)."/>
|
|
||||||
<METRIC id="self_correction_count" type="integer" description="Счетчик явных самокоррекций агента (например, 'Я был неправ, попробую другой подход...')."/>
|
|
||||||
</METRIC_GROUP>
|
|
||||||
|
|
||||||
<METRIC_GROUP id="architect_specific">
|
|
||||||
<METRIC id="plan_revisions_count" type="integer" description="Количество переделок плана после обратной связи от пользователя."/>
|
|
||||||
<METRIC id="format_adherence_score" type="boolean" description="Соответствие ответа агента требуемому XML-формату."/>
|
|
||||||
</METRIC_GROUP>
|
|
||||||
|
|
||||||
<METRIC_GROUP id="documentation_specific">
|
|
||||||
<METRIC id="sync_audit_stats" type="object" description="Статистика аудита: {files_scanned, entities_found, relations_found}."/>
|
|
||||||
<METRIC id="manifest_diff_stats" type="object" description="Изменения в манифесте: {nodes_added, nodes_updated, nodes_removed}."/>
|
|
||||||
</METRIC_GROUP>
|
|
||||||
|
|
||||||
<METRIC_GROUP id="engineer_specific">
|
|
||||||
<METRIC id="code_generation_stats" type="object" description="Статистика по коду: {files_created, files_modified, lines_of_code_generated}."/>
|
|
||||||
<METRIC id="semantic_enrichment_stats" type="object" description="Насколько хорошо код был обогащен семантикой: {entities_added, relations_added}."/>
|
|
||||||
<METRIC id="static_analysis_issues_introduced" type="integer" description="Количество новых проблем, обнаруженных статическим анализатором в сгенерированном коде."/>
|
|
||||||
<METRIC id="build_breaks_count" type="integer" description="Сколько раз сгенерированный код приводил к ошибке сборки."/>
|
|
||||||
</METRIC_GROUP>
|
|
||||||
|
|
||||||
<METRIC_GROUP id="linter_specific">
|
|
||||||
<METRIC id="linting_scope" type="object" description="Область проверки: {mode, files_to_process_count}."/>
|
|
||||||
<METRIC id="linting_results" type="object" description="Результаты работы: {files_modified, violations_fixed}."/>
|
|
||||||
</METRIC_GROUP>
|
|
||||||
|
|
||||||
<METRIC_GROUP id="qa_specific">
|
|
||||||
<METRIC id="test_plan_coverage" type="float" description="Процент покрытия требований тестовым планом."/>
|
|
||||||
<METRIC id="defects_found" type="integer" description="Количество найденных дефектов."/>
|
|
||||||
<METRIC id="automated_tests_run" type="integer" description="Количество запущенных автоматизированных тестов."/>
|
|
||||||
<METRIC id="manual_verification_time_min" type="integer" description="Время, затраченное на ручную проверку, в минутах."/>
|
|
||||||
</METRIC_GROUP>
|
|
||||||
|
|
||||||
</METRICS_CATALOG>
|
|
||||||
@@ -4,9 +4,9 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id("com.android.application")
|
id("com.android.application")
|
||||||
id("org.jetbrains.kotlin.android")
|
id("org.jetbrains.kotlin.android")
|
||||||
|
id("org.jetbrains.kotlin.plugin.compose")
|
||||||
id("com.google.dagger.hilt.android")
|
id("com.google.dagger.hilt.android")
|
||||||
id("kotlin-kapt")
|
id("kotlin-kapt")
|
||||||
// id("org.jlleitschuh.gradle.ktlint") version "12.1.1"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
@@ -46,9 +46,7 @@ android {
|
|||||||
compose = true
|
compose = true
|
||||||
buildConfig = true
|
buildConfig = true
|
||||||
}
|
}
|
||||||
composeOptions {
|
|
||||||
kotlinCompilerExtensionVersion = Versions.composeCompiler
|
|
||||||
}
|
|
||||||
packaging {
|
packaging {
|
||||||
resources {
|
resources {
|
||||||
excludes += "/META-INF/{AL2.0,LGPL2.1}"
|
excludes += "/META-INF/{AL2.0,LGPL2.1}"
|
||||||
@@ -61,6 +59,18 @@ dependencies {
|
|||||||
implementation(project(":data"))
|
implementation(project(":data"))
|
||||||
// [MODULE_DEPENDENCY] Domain module (transitively included via data, but explicit for clarity)
|
// [MODULE_DEPENDENCY] Domain module (transitively included via data, but explicit for clarity)
|
||||||
implementation(project(":domain"))
|
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
|
// [DEPENDENCY] AndroidX
|
||||||
implementation(Libs.coreKtx)
|
implementation(Libs.coreKtx)
|
||||||
@@ -68,16 +78,15 @@ dependencies {
|
|||||||
implementation(Libs.activityCompose)
|
implementation(Libs.activityCompose)
|
||||||
|
|
||||||
// [DEPENDENCY] Compose
|
// [DEPENDENCY] Compose
|
||||||
implementation(platform(Libs.composeBom))
|
|
||||||
implementation(Libs.composeUi)
|
implementation(Libs.composeUi)
|
||||||
implementation(Libs.composeUiGraphics)
|
implementation(Libs.composeUiGraphics)
|
||||||
implementation(Libs.composeUiToolingPreview)
|
implementation(Libs.composeUiToolingPreview)
|
||||||
implementation(Libs.composeMaterial3)
|
implementation(Libs.composeMaterial3)
|
||||||
implementation("androidx.compose.material:material-icons-extended-android:1.6.8")
|
implementation(Libs.composeMaterialIconsExtended)
|
||||||
implementation(Libs.navigationCompose)
|
implementation(Libs.navigationCompose)
|
||||||
implementation(Libs.hiltNavigationCompose)
|
implementation(Libs.hiltNavigationCompose)
|
||||||
|
|
||||||
// ktlint(project(":data:semantic-ktlint-rules"))
|
|
||||||
// [DEPENDENCY] DI (Hilt)
|
// [DEPENDENCY] DI (Hilt)
|
||||||
implementation(Libs.hiltAndroid)
|
implementation(Libs.hiltAndroid)
|
||||||
kapt(Libs.hiltCompiler)
|
kapt(Libs.hiltCompiler)
|
||||||
@@ -93,7 +102,7 @@ dependencies {
|
|||||||
testImplementation("app.cash.turbine:turbine:1.1.0")
|
testImplementation("app.cash.turbine:turbine:1.1.0")
|
||||||
androidTestImplementation(Libs.extJunit)
|
androidTestImplementation(Libs.extJunit)
|
||||||
androidTestImplementation(Libs.espressoCore)
|
androidTestImplementation(Libs.espressoCore)
|
||||||
androidTestImplementation(platform(Libs.composeBom))
|
|
||||||
androidTestImplementation(Libs.composeUiTestJunit4)
|
androidTestImplementation(Libs.composeUiTestJunit4)
|
||||||
debugImplementation(Libs.composeUiTooling)
|
debugImplementation(Libs.composeUiTooling)
|
||||||
debugImplementation(Libs.composeUiTestManifest)
|
debugImplementation(Libs.composeUiTestManifest)
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
// [PACKAGE] com.homebox.lens
|
// [FILE] app/src/main/java/com/homebox/lens/MainActivity.kt
|
||||||
// [FILE] MainActivity.kt
|
|
||||||
// [SEMANTICS] ui, activity, entrypoint
|
// [SEMANTICS] ui, activity, entrypoint
|
||||||
package com.homebox.lens
|
package com.homebox.lens
|
||||||
|
|
||||||
@@ -14,21 +13,31 @@ import androidx.compose.material3.Text
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import com.homebox.lens.navigation.NavGraph
|
import com.homebox.lens.feature.dashboard.ui.theme.HomeboxLensTheme
|
||||||
import com.homebox.lens.ui.theme.HomeboxLensTheme
|
import com.homebox.lens.feature.dashboard.navigation.navGraph
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
// [END_IMPORTS]
|
// [END_IMPORTS]
|
||||||
|
|
||||||
// [ENTITY: Activity('MainActivity')]
|
// [ENTITY: Activity('MainActivity')]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @summary Главная и единственная Activity в приложении.
|
* @summary Главная и единственная Activity в приложении.
|
||||||
*/
|
*/
|
||||||
|
// [ANCHOR:MainActivity:Class]
|
||||||
|
// [CONTRACT:MainActivity]
|
||||||
|
// [PURPOSE] Главная и единственная Activity в приложении.
|
||||||
|
// [END_CONTRACT:MainActivity]
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
// [ENTITY: Function('onCreate')]
|
// [ANCHOR:onCreate:Function]
|
||||||
// [RELATION: Function('onCreate')] -> [CALLS] -> [Function('HomeboxLensTheme')]
|
// [CONTRACT:onCreate]
|
||||||
// [RELATION: Function('onCreate')] -> [CALLS] -> [Function('NavGraph')]
|
// [PURPOSE] Инициализация Activity.
|
||||||
|
// [PARAM:savedInstanceState:Bundle?] Сохраненное состояние.
|
||||||
|
// [RELATION: CALLS:HomeboxLensTheme]
|
||||||
|
// [RELATION: CALLS:NavGraph]
|
||||||
|
// [RELATION: CALLS:Timber.d]
|
||||||
|
// [END_CONTRACT:onCreate]
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
Timber.d("[DEBUG][LIFECYCLE][creating_activity] MainActivity created.")
|
Timber.d("[DEBUG][LIFECYCLE][creating_activity] MainActivity created.")
|
||||||
@@ -36,35 +45,48 @@ class MainActivity : ComponentActivity() {
|
|||||||
HomeboxLensTheme {
|
HomeboxLensTheme {
|
||||||
Surface(
|
Surface(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
color = MaterialTheme.colorScheme.background
|
color = MaterialTheme.colorScheme.background,
|
||||||
) {
|
) {
|
||||||
NavGraph()
|
navGraph()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// [END_ENTITY: Function('onCreate')]
|
// [END_ANCHOR:onCreate]
|
||||||
}
|
}
|
||||||
// [END_ENTITY: Activity('MainActivity')]
|
// [END_ANCHOR:MainActivity]
|
||||||
|
|
||||||
// [ENTITY: Function('Greeting')]
|
// [ENTITY: Function('Greeting')]
|
||||||
|
// [ANCHOR:greeting:Function]
|
||||||
|
// [CONTRACT:greeting]
|
||||||
|
// [PURPOSE] Отображает приветствие.
|
||||||
|
// [PARAM:name:String] Имя для приветствия.
|
||||||
|
// [PARAM:modifier:Modifier] Модификатор для элемента.
|
||||||
|
// [END_CONTRACT:greeting]
|
||||||
@Composable
|
@Composable
|
||||||
fun Greeting(name: String, modifier: Modifier = Modifier) {
|
fun greeting(
|
||||||
|
name: String,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "Hello $name!",
|
text = "Hello $name!",
|
||||||
modifier = modifier
|
modifier = modifier,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// [END_ENTITY: Function('Greeting')]
|
// [END_ANCHOR:greeting]
|
||||||
|
|
||||||
// [ENTITY: Function('GreetingPreview')]
|
// [ENTITY: Function('GreetingPreview')]
|
||||||
|
// [ANCHOR:greetingPreview:Function]
|
||||||
|
// [CONTRACT:greetingPreview]
|
||||||
|
// [PURPOSE] Предварительный просмотр функции greeting.
|
||||||
|
// [END_CONTRACT:greetingPreview]
|
||||||
@Preview(showBackground = true)
|
@Preview(showBackground = true)
|
||||||
@Composable
|
@Composable
|
||||||
fun GreetingPreview() {
|
fun greetingPreview() {
|
||||||
HomeboxLensTheme {
|
HomeboxLensTheme {
|
||||||
Greeting("Android")
|
greeting("Android")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// [END_ENTITY: Function('GreetingPreview')]
|
// [END_ANCHOR:greetingPreview]
|
||||||
|
// [END_FILE_app/src/main/java/com/homebox/lens/MainActivity.kt]
|
||||||
// [END_FILE_MainActivity.kt]
|
// [END_FILE_app/src/main/java/com/homebox/lens/MainActivity.kt]
|
||||||
|
|||||||
@@ -10,12 +10,12 @@ import timber.log.Timber
|
|||||||
// [END_IMPORTS]
|
// [END_IMPORTS]
|
||||||
|
|
||||||
// [ENTITY: Application('MainApplication')]
|
// [ENTITY: Application('MainApplication')]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @summary Точка входа в приложение. Инициализирует Hilt и Timber.
|
* @summary Точка входа в приложение. Инициализирует Hilt и Timber.
|
||||||
*/
|
*/
|
||||||
@HiltAndroidApp
|
@HiltAndroidApp
|
||||||
class MainApplication : Application() {
|
class MainApplication : Application() {
|
||||||
|
|
||||||
// [ENTITY: Function('onCreate')]
|
// [ENTITY: Function('onCreate')]
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
@@ -27,4 +27,4 @@ class MainApplication : Application() {
|
|||||||
// [END_ENTITY: Function('onCreate')]
|
// [END_ENTITY: Function('onCreate')]
|
||||||
}
|
}
|
||||||
// [END_ENTITY: Application('MainApplication')]
|
// [END_ENTITY: Application('MainApplication')]
|
||||||
// [END_FILE_MainApplication.kt]
|
// [END_FILE_MainApplication.kt]
|
||||||
|
|||||||
@@ -1,143 +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.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.navigation.NavHostController
|
|
||||||
import androidx.navigation.NavType
|
|
||||||
import androidx.navigation.compose.NavHost
|
|
||||||
import androidx.navigation.compose.composable
|
|
||||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
|
||||||
import androidx.navigation.compose.rememberNavController
|
|
||||||
import androidx.navigation.navArgument
|
|
||||||
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.labeledit.LabelEditScreen
|
|
||||||
import com.homebox.lens.ui.screen.locationedit.LocationEditScreen
|
|
||||||
import com.homebox.lens.ui.screen.locationslist.LocationsListScreen
|
|
||||||
import com.homebox.lens.ui.screen.search.SearchScreen
|
|
||||||
import com.homebox.lens.ui.screen.setup.SetupScreen
|
|
||||||
// [END_IMPORTS]
|
|
||||||
|
|
||||||
// [ENTITY: Function('NavGraph')]
|
|
||||||
// [RELATION: Function('NavGraph')] -> [DEPENDS_ON] -> [Framework('NavHostController')]
|
|
||||||
// [RELATION: Function('NavGraph')] -> [CREATES_INSTANCE_OF] -> [Class('NavigationActions')]
|
|
||||||
/**
|
|
||||||
* @summary Определяет граф навигации для всего приложения с использованием Jetpack Compose Navigation.
|
|
||||||
* @param navController Контроллер навигации.
|
|
||||||
* @see Screen
|
|
||||||
* @sideeffect Регистрирует все экраны и управляет состоянием навигации.
|
|
||||||
* @invariant Стартовый экран - `Screen.Setup`.
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
fun NavGraph(
|
|
||||||
navController: NavHostController = rememberNavController()
|
|
||||||
) {
|
|
||||||
val navBackStackEntry by navController.currentBackStackEntryAsState()
|
|
||||||
val currentRoute = navBackStackEntry?.destination?.route
|
|
||||||
|
|
||||||
val navigationActions = remember(navController) {
|
|
||||||
NavigationActions(navController)
|
|
||||||
}
|
|
||||||
|
|
||||||
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(
|
|
||||||
currentRoute = currentRoute,
|
|
||||||
navigationActions = navigationActions
|
|
||||||
)
|
|
||||||
}
|
|
||||||
composable(route = Screen.InventoryList.route) {
|
|
||||||
InventoryListScreen(
|
|
||||||
currentRoute = currentRoute,
|
|
||||||
navigationActions = navigationActions
|
|
||||||
)
|
|
||||||
}
|
|
||||||
composable(route = Screen.ItemDetails.route) {
|
|
||||||
ItemDetailsScreen(
|
|
||||||
currentRoute = currentRoute,
|
|
||||||
navigationActions = navigationActions
|
|
||||||
)
|
|
||||||
}
|
|
||||||
composable(
|
|
||||||
route = Screen.ItemEdit.route,
|
|
||||||
arguments = listOf(navArgument("itemId") { nullable = true })
|
|
||||||
) { backStackEntry ->
|
|
||||||
val itemId = backStackEntry.arguments?.getString("itemId")
|
|
||||||
ItemEditScreen(
|
|
||||||
currentRoute = currentRoute,
|
|
||||||
navigationActions = navigationActions,
|
|
||||||
itemId = itemId,
|
|
||||||
onSaveSuccess = { navController.popBackStack() }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
composable(Screen.LabelsList.route) {
|
|
||||||
LabelsListScreen(
|
|
||||||
currentRoute = currentRoute,
|
|
||||||
navigationActions = navigationActions
|
|
||||||
)
|
|
||||||
}
|
|
||||||
composable(route = Screen.LocationsList.route) {
|
|
||||||
LocationsListScreen(
|
|
||||||
currentRoute = currentRoute,
|
|
||||||
navigationActions = navigationActions,
|
|
||||||
onLocationClick = { locationId ->
|
|
||||||
// [AI_NOTE]: Navigate to a pre-filtered inventory list screen
|
|
||||||
navController.navigate(Screen.InventoryList.route)
|
|
||||||
},
|
|
||||||
onAddNewLocationClick = {
|
|
||||||
navController.navigate(Screen.LocationEdit.createRoute("new"))
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
composable(route = Screen.LocationEdit.route) { backStackEntry ->
|
|
||||||
val locationId = backStackEntry.arguments?.getString("locationId")
|
|
||||||
LocationEditScreen(
|
|
||||||
locationId = locationId
|
|
||||||
)
|
|
||||||
}
|
|
||||||
composable(route = Screen.LocationEdit.route) { backStackEntry ->
|
|
||||||
val locationId = backStackEntry.arguments?.getString("locationId")
|
|
||||||
LocationEditScreen(
|
|
||||||
locationId = locationId
|
|
||||||
)
|
|
||||||
}
|
|
||||||
composable(
|
|
||||||
route = Screen.LabelEdit.route,
|
|
||||||
arguments = listOf(navArgument("labelId") { nullable = true })
|
|
||||||
) { backStackEntry ->
|
|
||||||
val labelId = backStackEntry.arguments?.getString("labelId")
|
|
||||||
LabelEditScreen(
|
|
||||||
labelId = labelId,
|
|
||||||
onBack = { navController.popBackStack() },
|
|
||||||
onLabelSaved = { navController.popBackStack() }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
composable(route = Screen.Search.route) {
|
|
||||||
SearchScreen(
|
|
||||||
currentRoute = currentRoute,
|
|
||||||
navigationActions = navigationActions
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// [END_ENTITY: Function('NavGraph')]
|
|
||||||
// [END_FILE_NavGraph.kt]
|
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
// [PACKAGE] com.homebox.lens.navigation
|
|
||||||
// [FILE] NavigationActions.kt
|
|
||||||
// [SEMANTICS] navigation, controller, actions
|
|
||||||
package com.homebox.lens.navigation
|
|
||||||
|
|
||||||
// [IMPORTS]
|
|
||||||
import androidx.navigation.NavHostController
|
|
||||||
import timber.log.Timber
|
|
||||||
// [END_IMPORTS]
|
|
||||||
|
|
||||||
// [ENTITY: Class('NavigationActions')]
|
|
||||||
// [RELATION: Class('NavigationActions')] -> [DEPENDS_ON] -> [Framework('NavHostController')]
|
|
||||||
/**
|
|
||||||
* @summary Класс-обертка над NavHostController для предоставления типизированных навигационных действий.
|
|
||||||
* @param navController Контроллер Jetpack Navigation.
|
|
||||||
* @invariant Все навигационные действия должны использовать предоставленный navController.
|
|
||||||
*/
|
|
||||||
class NavigationActions(private val navController: NavHostController) {
|
|
||||||
|
|
||||||
// [ENTITY: Function('navigateToDashboard')]
|
|
||||||
/**
|
|
||||||
* @summary Навигация на главный экран.
|
|
||||||
* @sideeffect Очищает back stack до главного экрана, чтобы избежать циклов.
|
|
||||||
*/
|
|
||||||
fun navigateToDashboard() {
|
|
||||||
Timber.i("[INFO][ACTION][navigate_to_dashboard] Navigating to Dashboard.")
|
|
||||||
navController.navigate(Screen.Dashboard.route) {
|
|
||||||
popUpTo(navController.graph.startDestinationId)
|
|
||||||
launchSingleTop = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// [END_ENTITY: Function('navigateToDashboard')]
|
|
||||||
|
|
||||||
// [ENTITY: Function('navigateToLocations')]
|
|
||||||
fun navigateToLocations() {
|
|
||||||
Timber.i("[INFO][ACTION][navigate_to_locations] Navigating to Locations.")
|
|
||||||
navController.navigate(Screen.LocationsList.route) {
|
|
||||||
launchSingleTop = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// [END_ENTITY: Function('navigateToLocations')]
|
|
||||||
|
|
||||||
// [ENTITY: Function('navigateToLabels')]
|
|
||||||
fun navigateToLabels() {
|
|
||||||
Timber.i("[INFO][ACTION][navigate_to_labels] Navigating to Labels.")
|
|
||||||
navController.navigate(Screen.LabelsList.route) {
|
|
||||||
launchSingleTop = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// [END_ENTITY: Function('navigateToLabels')]
|
|
||||||
|
|
||||||
// [ENTITY: Function('navigateToLabelEdit')]
|
|
||||||
fun navigateToLabelEdit(labelId: String? = null) {
|
|
||||||
Timber.i("[INFO][ACTION][navigate_to_label_edit] Navigating to Label Edit with ID: %s", labelId)
|
|
||||||
navController.navigate(Screen.LabelEdit.createRoute(labelId))
|
|
||||||
}
|
|
||||||
// [END_ENTITY: Function('navigateToLabelEdit')]
|
|
||||||
|
|
||||||
// [ENTITY: Function('navigateToSearch')]
|
|
||||||
fun navigateToSearch() {
|
|
||||||
Timber.i("[INFO][ACTION][navigate_to_search] Navigating to Search.")
|
|
||||||
navController.navigate(Screen.Search.route) {
|
|
||||||
launchSingleTop = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// [END_ENTITY: Function('navigateToSearch')]
|
|
||||||
|
|
||||||
// [ENTITY: Function('navigateToInventoryListWithLabel')]
|
|
||||||
fun navigateToInventoryListWithLabel(labelId: String) {
|
|
||||||
Timber.i("[INFO][ACTION][navigate_to_inventory_with_label] Navigating to Inventory with label: %s", labelId)
|
|
||||||
val route = Screen.InventoryList.withFilter("label", labelId)
|
|
||||||
navController.navigate(route)
|
|
||||||
}
|
|
||||||
// [END_ENTITY: Function('navigateToInventoryListWithLabel')]
|
|
||||||
|
|
||||||
// [ENTITY: Function('navigateToInventoryListWithLocation')]
|
|
||||||
fun navigateToInventoryListWithLocation(locationId: String) {
|
|
||||||
Timber.i("[INFO][ACTION][navigate_to_inventory_with_location] Navigating to Inventory with location: %s", locationId)
|
|
||||||
val route = Screen.InventoryList.withFilter("location", locationId)
|
|
||||||
navController.navigate(route)
|
|
||||||
}
|
|
||||||
// [END_ENTITY: Function('navigateToInventoryListWithLocation')]
|
|
||||||
|
|
||||||
// [ENTITY: Function('navigateToCreateItem')]
|
|
||||||
fun navigateToCreateItem() {
|
|
||||||
Timber.i("[INFO][ACTION][navigate_to_create_item] Navigating to Create Item.")
|
|
||||||
navController.navigate(Screen.ItemEdit.createRoute())
|
|
||||||
}
|
|
||||||
// [END_ENTITY: Function('navigateToCreateItem')]
|
|
||||||
|
|
||||||
// [ENTITY: Function('navigateToLogout')]
|
|
||||||
fun navigateToLogout() {
|
|
||||||
Timber.i("[INFO][ACTION][navigate_to_logout] Navigating to Logout.")
|
|
||||||
navController.navigate(Screen.Setup.route) {
|
|
||||||
popUpTo(Screen.Dashboard.route) { inclusive = true }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// [END_ENTITY: Function('navigateToLogout')]
|
|
||||||
|
|
||||||
// [ENTITY: Function('navigateBack')]
|
|
||||||
fun navigateBack() {
|
|
||||||
Timber.i("[INFO][ACTION][navigate_back] Navigating back.")
|
|
||||||
navController.popBackStack()
|
|
||||||
}
|
|
||||||
// [END_ENTITY: Function('navigateBack')]
|
|
||||||
}
|
|
||||||
// [END_ENTITY: Class('NavigationActions')]
|
|
||||||
// [END_FILE_NavigationActions.kt]
|
|
||||||
@@ -1,123 +0,0 @@
|
|||||||
// [PACKAGE] com.homebox.lens.navigation
|
|
||||||
// [FILE] Screen.kt
|
|
||||||
// [SEMANTICS] navigation, routes, sealed_class
|
|
||||||
package com.homebox.lens.navigation
|
|
||||||
|
|
||||||
// [ENTITY: SealedClass('Screen')]
|
|
||||||
/**
|
|
||||||
* @summary Запечатанный класс для определения маршрутов навигации в приложении.
|
|
||||||
* @description Обеспечивает типобезопасность при навигации.
|
|
||||||
* @param route Строковый идентификатор маршрута.
|
|
||||||
*/
|
|
||||||
sealed class Screen(val route: String) {
|
|
||||||
// [ENTITY: Object('Setup')]
|
|
||||||
data object Setup : Screen("setup_screen")
|
|
||||||
// [END_ENTITY: Object('Setup')]
|
|
||||||
|
|
||||||
// [ENTITY: Object('Dashboard')]
|
|
||||||
data object Dashboard : Screen("dashboard_screen")
|
|
||||||
// [END_ENTITY: Object('Dashboard')]
|
|
||||||
|
|
||||||
// [ENTITY: Object('InventoryList')]
|
|
||||||
data object InventoryList : Screen("inventory_list_screen") {
|
|
||||||
// [ENTITY: Function('withFilter')]
|
|
||||||
/**
|
|
||||||
* @summary Создает маршрут для экрана списка инвентаря с параметром фильтра.
|
|
||||||
* @param key Ключ фильтра (например, "label" или "location").
|
|
||||||
* @param value Значение фильтра (например, ID метки или местоположения).
|
|
||||||
* @return Строку полного маршрута с query-параметром.
|
|
||||||
* @throws IllegalArgumentException если ключ или значение пустые.
|
|
||||||
*/
|
|
||||||
fun withFilter(key: String, value: String): String {
|
|
||||||
require(key.isNotBlank()) { "Filter key cannot be blank." }
|
|
||||||
require(value.isNotBlank()) { "Filter value cannot be blank." }
|
|
||||||
val constructedRoute = "inventory_list_screen?$key=$value"
|
|
||||||
check(constructedRoute.contains("?$key=$value")) { "Route must contain the filter query." }
|
|
||||||
return constructedRoute
|
|
||||||
}
|
|
||||||
// [END_ENTITY: Function('withFilter')]
|
|
||||||
}
|
|
||||||
// [END_ENTITY: Object('InventoryList')]
|
|
||||||
|
|
||||||
// [ENTITY: Object('ItemDetails')]
|
|
||||||
data object ItemDetails : Screen("item_details_screen/{itemId}") {
|
|
||||||
// [ENTITY: Function('createRoute')]
|
|
||||||
/**
|
|
||||||
* @summary Создает маршрут для экрана деталей элемента с указанным ID.
|
|
||||||
* @param itemId ID элемента для отображения.
|
|
||||||
* @return Строку полного маршрута.
|
|
||||||
* @throws IllegalArgumentException если itemId пустой.
|
|
||||||
*/
|
|
||||||
fun createRoute(itemId: String): String {
|
|
||||||
require(itemId.isNotBlank()) { "itemId не может быть пустым." }
|
|
||||||
val route = "item_details_screen/$itemId"
|
|
||||||
check(route.endsWith(itemId)) { "Маршрут должен заканчиваться на itemId." }
|
|
||||||
return route
|
|
||||||
}
|
|
||||||
// [END_ENTITY: Function('createRoute')]
|
|
||||||
}
|
|
||||||
// [END_ENTITY: Object('ItemDetails')]
|
|
||||||
|
|
||||||
// [ENTITY: Object('ItemEdit')]
|
|
||||||
data object ItemEdit : Screen("item_edit_screen?itemId={itemId}") {
|
|
||||||
// [ENTITY: Function('createRoute')]
|
|
||||||
/**
|
|
||||||
* @summary Создает маршрут для экрана редактирования элемента с указанным ID.
|
|
||||||
* @param itemId ID элемента для редактирования. Null, если создается новый элемент.
|
|
||||||
* @return Строку полного маршрута.
|
|
||||||
*/
|
|
||||||
fun createRoute(itemId: String? = null): String {
|
|
||||||
return itemId?.let { "item_edit_screen?itemId=$it" } ?: "item_edit_screen"
|
|
||||||
}
|
|
||||||
// [END_ENTITY: Function('createRoute')]
|
|
||||||
}
|
|
||||||
// [END_ENTITY: Object('ItemEdit')]
|
|
||||||
|
|
||||||
// [ENTITY: Object('LabelsList')]
|
|
||||||
data object LabelsList : Screen("labels_list_screen")
|
|
||||||
// [END_ENTITY: Object('LabelsList')]
|
|
||||||
|
|
||||||
// [ENTITY: Object('LabelEdit')]
|
|
||||||
data object LabelEdit : Screen("label_edit_screen?labelId={labelId}") {
|
|
||||||
// [ENTITY: Function('createRoute')]
|
|
||||||
/**
|
|
||||||
* @summary Создает маршрут для экрана редактирования метки с указанным ID.
|
|
||||||
* @param labelId ID метки для редактирования. Null, если создается новая метка.
|
|
||||||
* @return Строку полного маршрута.
|
|
||||||
*/
|
|
||||||
fun createRoute(labelId: String? = null): String {
|
|
||||||
return labelId?.let { "label_edit_screen?labelId=$it" } ?: "label_edit_screen"
|
|
||||||
}
|
|
||||||
// [END_ENTITY: Function('createRoute')]
|
|
||||||
}
|
|
||||||
// [END_ENTITY: Object('LabelEdit')]
|
|
||||||
|
|
||||||
// [ENTITY: Object('LocationsList')]
|
|
||||||
data object LocationsList : Screen("locations_list_screen")
|
|
||||||
// [END_ENTITY: Object('LocationsList')]
|
|
||||||
|
|
||||||
// [ENTITY: Object('LocationEdit')]
|
|
||||||
data object LocationEdit : Screen("location_edit_screen/{locationId}") {
|
|
||||||
// [ENTITY: Function('createRoute')]
|
|
||||||
/**
|
|
||||||
* @summary Создает маршрут для экрана редактирования местоположения с указанным ID.
|
|
||||||
* @param locationId ID местоположения для редактирования.
|
|
||||||
* @return Строку полного маршрута.
|
|
||||||
* @throws IllegalArgumentException если locationId пустой.
|
|
||||||
*/
|
|
||||||
fun createRoute(locationId: String): String {
|
|
||||||
require(locationId.isNotBlank()) { "locationId не может быть пустым." }
|
|
||||||
val route = "location_edit_screen/$locationId"
|
|
||||||
check(route.endsWith(locationId)) { "Маршрут должен заканчиваться на locationId." }
|
|
||||||
return route
|
|
||||||
}
|
|
||||||
// [END_ENTITY: Function('createRoute')]
|
|
||||||
}
|
|
||||||
// [END_ENTITY: Object('LocationEdit')]
|
|
||||||
|
|
||||||
// [ENTITY: Object('Search')]
|
|
||||||
data object Search : Screen("search_screen")
|
|
||||||
// [END_ENTITY: Object('Search')]
|
|
||||||
}
|
|
||||||
// [END_ENTITY: SealedClass('Screen')]
|
|
||||||
// [END_FILE_Screen.kt]
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
// [PACKAGE] com.homebox.lens.ui.common
|
|
||||||
// [FILE] AppDrawer.kt
|
|
||||||
// [SEMANTICS] ui, common, navigation_drawer
|
|
||||||
package com.homebox.lens.ui.common
|
|
||||||
|
|
||||||
// [IMPORTS]
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.filled.Add
|
|
||||||
import androidx.compose.material3.Button
|
|
||||||
import androidx.compose.material3.Divider
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.ModalDrawerSheet
|
|
||||||
import androidx.compose.material3.NavigationDrawerItem
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import com.homebox.lens.R
|
|
||||||
import com.homebox.lens.navigation.NavigationActions
|
|
||||||
import com.homebox.lens.navigation.Screen
|
|
||||||
// [END_IMPORTS]
|
|
||||||
|
|
||||||
// [ENTITY: Function('AppDrawerContent')]
|
|
||||||
// [RELATION: Function('AppDrawerContent')] -> [DEPENDS_ON] -> [Class('NavigationActions')]
|
|
||||||
/**
|
|
||||||
* @summary Контент для бокового навигационного меню (Drawer).
|
|
||||||
* @param currentRoute Текущий маршрут для подсветки активного элемента.
|
|
||||||
* @param navigationActions Объект с навигационными действиями.
|
|
||||||
* @param onCloseDrawer Лямбда для закрытия бокового меню.
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
internal fun AppDrawerContent(
|
|
||||||
currentRoute: String?,
|
|
||||||
navigationActions: NavigationActions,
|
|
||||||
onCloseDrawer: () -> Unit
|
|
||||||
) {
|
|
||||||
ModalDrawerSheet {
|
|
||||||
Spacer(Modifier.height(12.dp))
|
|
||||||
Button(
|
|
||||||
onClick = {
|
|
||||||
navigationActions.navigateToCreateItem()
|
|
||||||
onCloseDrawer()
|
|
||||||
},
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 16.dp)
|
|
||||||
) {
|
|
||||||
Icon(Icons.Default.Add, contentDescription = null, modifier = Modifier.size(18.dp))
|
|
||||||
Spacer(Modifier.width(8.dp))
|
|
||||||
Text(stringResource(id = R.string.create))
|
|
||||||
}
|
|
||||||
Spacer(Modifier.height(12.dp))
|
|
||||||
Divider()
|
|
||||||
NavigationDrawerItem(
|
|
||||||
label = { Text(stringResource(id = R.string.dashboard_title)) },
|
|
||||||
selected = currentRoute == Screen.Dashboard.route,
|
|
||||||
onClick = {
|
|
||||||
navigationActions.navigateToDashboard()
|
|
||||||
onCloseDrawer()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
NavigationDrawerItem(
|
|
||||||
label = { Text(stringResource(id = R.string.nav_locations)) },
|
|
||||||
selected = currentRoute == Screen.LocationsList.route,
|
|
||||||
onClick = {
|
|
||||||
navigationActions.navigateToLocations()
|
|
||||||
onCloseDrawer()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
NavigationDrawerItem(
|
|
||||||
label = { Text(stringResource(id = R.string.nav_labels)) },
|
|
||||||
selected = currentRoute == Screen.LabelsList.route,
|
|
||||||
onClick = {
|
|
||||||
navigationActions.navigateToLabels()
|
|
||||||
onCloseDrawer()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
NavigationDrawerItem(
|
|
||||||
label = { Text(stringResource(id = R.string.search)) },
|
|
||||||
selected = currentRoute == Screen.Search.route,
|
|
||||||
onClick = {
|
|
||||||
navigationActions.navigateToSearch()
|
|
||||||
onCloseDrawer()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
// [AI_NOTE]: Add Profile and Tools items
|
|
||||||
Divider()
|
|
||||||
NavigationDrawerItem(
|
|
||||||
label = { Text(stringResource(id = R.string.logout)) },
|
|
||||||
selected = false,
|
|
||||||
onClick = {
|
|
||||||
navigationActions.navigateToLogout()
|
|
||||||
onCloseDrawer()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// [END_ENTITY: Function('AppDrawerContent')]
|
|
||||||
// [END_FILE_AppDrawer.kt]
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
// [PACKAGE] com.homebox.lens.ui.common
|
|
||||||
// [FILE] MainScaffold.kt
|
|
||||||
// [SEMANTICS] ui, common, scaffold, navigation_drawer
|
|
||||||
|
|
||||||
package com.homebox.lens.ui.common
|
|
||||||
|
|
||||||
// [IMPORTS]
|
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.filled.Menu
|
|
||||||
import androidx.compose.material3.*
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import com.homebox.lens.R
|
|
||||||
import com.homebox.lens.navigation.NavigationActions
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
// [END_IMPORTS]
|
|
||||||
|
|
||||||
// [ENTITY: Function('MainScaffold')]
|
|
||||||
// [RELATION: Function('MainScaffold')] -> [DEPENDS_ON] -> [Class('NavigationActions')]
|
|
||||||
// [RELATION: Function('MainScaffold')] -> [CALLS] -> [Function('AppDrawerContent')]
|
|
||||||
/**
|
|
||||||
* @summary Общая обертка для экранов, включающая Scaffold и Navigation Drawer.
|
|
||||||
* @param topBarTitle Заголовок для TopAppBar.
|
|
||||||
* @param currentRoute Текущий маршрут для подсветки активного элемента в Drawer.
|
|
||||||
* @param navigationActions Объект с навигационными действиями.
|
|
||||||
* @param topBarActions Composable-функция для отображения действий (иконок) в TopAppBar.
|
|
||||||
* @param content Основное содержимое экрана, которое будет отображено внутри Scaffold.
|
|
||||||
* @sideeffect Управляет состоянием (открыто/закрыто) бокового меню (ModalNavigationDrawer).
|
|
||||||
* @invariant TopAppBar всегда отображается с иконкой меню.
|
|
||||||
*/
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
|
||||||
fun MainScaffold(
|
|
||||||
topBarTitle: String,
|
|
||||||
currentRoute: String?,
|
|
||||||
navigationActions: NavigationActions,
|
|
||||||
topBarActions: @Composable () -> Unit = {},
|
|
||||||
content: @Composable (PaddingValues) -> Unit
|
|
||||||
) {
|
|
||||||
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
|
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
|
|
||||||
ModalNavigationDrawer(
|
|
||||||
drawerState = drawerState,
|
|
||||||
drawerContent = {
|
|
||||||
AppDrawerContent(
|
|
||||||
currentRoute = currentRoute,
|
|
||||||
navigationActions = navigationActions,
|
|
||||||
onCloseDrawer = { scope.launch { drawerState.close() } }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Scaffold(
|
|
||||||
topBar = {
|
|
||||||
TopAppBar(
|
|
||||||
title = { Text(topBarTitle) },
|
|
||||||
navigationIcon = {
|
|
||||||
IconButton(onClick = { scope.launch { drawerState.open() } }) {
|
|
||||||
Icon(
|
|
||||||
Icons.Default.Menu,
|
|
||||||
contentDescription = stringResource(id = R.string.cd_open_navigation_drawer)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
actions = { topBarActions() }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
) { paddingValues ->
|
|
||||||
content(paddingValues)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// [END_ENTITY: Function('MainScaffold')]
|
|
||||||
// [END_FILE_MainScaffold.kt]
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
// [PACKAGE] com.homebox.lens.ui.components
|
|
||||||
// [FILE] ColorPicker.kt
|
|
||||||
// [SEMANTICS] ui, component, color_selection
|
|
||||||
|
|
||||||
package com.homebox.lens.ui.components
|
|
||||||
|
|
||||||
// [IMPORTS]
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.border
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.*
|
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.OutlinedTextField
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.graphics.toArgb
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import com.homebox.lens.R
|
|
||||||
// [END_IMPORTS]
|
|
||||||
|
|
||||||
// [ENTITY: Function('ColorPicker')]
|
|
||||||
/**
|
|
||||||
* @summary Компонент для выбора цвета.
|
|
||||||
* @param selectedColor Текущий выбранный цвет в формате HEX строки (например, "#FFFFFF").
|
|
||||||
* @param onColorSelected Лямбда-функция, вызываемая при выборе нового цвета.
|
|
||||||
* @param modifier Модификатор для настройки внешнего вида.
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
fun ColorPicker(
|
|
||||||
selectedColor: String,
|
|
||||||
onColorSelected: (String) -> Unit,
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
Column(modifier = modifier) {
|
|
||||||
Text(text = stringResource(R.string.label_color), style = MaterialTheme.typography.bodyLarge)
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.size(48.dp)
|
|
||||||
.background(
|
|
||||||
if (selectedColor.isEmpty()) Color.Transparent else Color(android.graphics.Color.parseColor(selectedColor)),
|
|
||||||
CircleShape
|
|
||||||
)
|
|
||||||
.border(2.dp, MaterialTheme.colorScheme.onSurface, CircleShape)
|
|
||||||
.clickable { /* TODO: Implement a more advanced color selection dialog */ }
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(16.dp))
|
|
||||||
OutlinedTextField(
|
|
||||||
value = selectedColor,
|
|
||||||
onValueChange = { newValue ->
|
|
||||||
// Basic validation for hex color
|
|
||||||
if (newValue.matches(Regex("^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$"))) {
|
|
||||||
onColorSelected(newValue)
|
|
||||||
} else if (newValue.isEmpty() || newValue == "#") {
|
|
||||||
onColorSelected("#FFFFFF") // Default to white if input is cleared
|
|
||||||
}
|
|
||||||
},
|
|
||||||
label = { Text(stringResource(R.string.label_hex_color)) },
|
|
||||||
singleLine = true,
|
|
||||||
modifier = Modifier.weight(1f)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// [END_ENTITY: Function('ColorPicker')]
|
|
||||||
// [END_FILE_ColorPicker.kt]
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
// [PACKAGE] com.homebox.lens.ui.components
|
|
||||||
// [FILE] LoadingOverlay.kt
|
|
||||||
// [SEMANTICS] ui, component, loading
|
|
||||||
|
|
||||||
package com.homebox.lens.ui.components
|
|
||||||
|
|
||||||
// [IMPORTS]
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
// [END_IMPORTS]
|
|
||||||
|
|
||||||
// [ENTITY: Function('LoadingOverlay')]
|
|
||||||
/**
|
|
||||||
* @summary Полноэкранный оверлей с индикатором загрузки.
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
fun LoadingOverlay() {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.background(MaterialTheme.colorScheme.surface.copy(alpha = 0.6f)),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
CircularProgressIndicator()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// [END_ENTITY: Function('LoadingOverlay')]
|
|
||||||
// [END_FILE_LoadingOverlay.kt]
|
|
||||||
@@ -1,357 +0,0 @@
|
|||||||
// [PACKAGE] com.homebox.lens.ui.screen.dashboard
|
|
||||||
// [FILE] DashboardScreen.kt
|
|
||||||
// [SEMANTICS] ui, screen, dashboard, compose, navigation
|
|
||||||
package com.homebox.lens.ui.screen.dashboard
|
|
||||||
|
|
||||||
// [IMPORTS]
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.layout.*
|
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
|
||||||
import androidx.compose.foundation.lazy.LazyRow
|
|
||||||
import androidx.compose.foundation.lazy.grid.GridCells
|
|
||||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
|
||||||
import androidx.compose.foundation.lazy.items
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.filled.Search
|
|
||||||
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.res.stringResource
|
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
|
||||||
import com.homebox.lens.R
|
|
||||||
import com.homebox.lens.domain.model.*
|
|
||||||
import com.homebox.lens.navigation.NavigationActions
|
|
||||||
import com.homebox.lens.ui.common.MainScaffold
|
|
||||||
import com.homebox.lens.ui.theme.HomeboxLensTheme
|
|
||||||
import timber.log.Timber
|
|
||||||
// [END_IMPORTS]
|
|
||||||
|
|
||||||
// [ENTITY: Function('DashboardScreen')]
|
|
||||||
// [RELATION: Function('DashboardScreen')] -> [DEPENDS_ON] -> [ViewModel('DashboardViewModel')]
|
|
||||||
// [RELATION: Function('DashboardScreen')] -> [DEPENDS_ON] -> [Class('NavigationActions')]
|
|
||||||
// [RELATION: Function('DashboardScreen')] -> [CALLS] -> [Function('MainScaffold')]
|
|
||||||
/**
|
|
||||||
* @summary Главная Composable-функция для экрана "Панель управления".
|
|
||||||
* @param viewModel ViewModel для этого экрана, предоставляется через Hilt.
|
|
||||||
* @param currentRoute Текущий маршрут для подсветки активного элемента в Drawer.
|
|
||||||
* @param navigationActions Объект с навигационными действиями.
|
|
||||||
* @sideeffect Вызывает навигационные лямбды при взаимодействии с UI.
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
fun DashboardScreen(
|
|
||||||
viewModel: DashboardViewModel = hiltViewModel(),
|
|
||||||
currentRoute: String?,
|
|
||||||
navigationActions: NavigationActions
|
|
||||||
) {
|
|
||||||
val uiState by viewModel.uiState.collectAsState()
|
|
||||||
MainScaffold(
|
|
||||||
topBarTitle = stringResource(id = R.string.dashboard_title),
|
|
||||||
currentRoute = currentRoute,
|
|
||||||
navigationActions = navigationActions,
|
|
||||||
topBarActions = {
|
|
||||||
IconButton(onClick = { navigationActions.navigateToSearch() }) {
|
|
||||||
Icon(
|
|
||||||
Icons.Default.Search,
|
|
||||||
contentDescription = stringResource(id = R.string.cd_scan_qr_code) // [AI_NOTE]: Rename string resource
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
) { paddingValues ->
|
|
||||||
DashboardContent(
|
|
||||||
modifier = Modifier.padding(paddingValues),
|
|
||||||
uiState = uiState,
|
|
||||||
onLocationClick = { location ->
|
|
||||||
Timber.i("[INFO][ACTION][navigate_to_inventory_with_location] Location chip clicked: ${location.id}. Navigating...")
|
|
||||||
navigationActions.navigateToInventoryListWithLocation(location.id)
|
|
||||||
},
|
|
||||||
onLabelClick = { label ->
|
|
||||||
Timber.i("[INFO][ACTION][navigate_to_inventory_with_label] Label chip clicked: ${label.id}. Navigating...")
|
|
||||||
navigationActions.navigateToInventoryListWithLabel(label.id)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// [END_ENTITY: Function('DashboardScreen')]
|
|
||||||
|
|
||||||
// [ENTITY: Function('DashboardContent')]
|
|
||||||
// [RELATION: Function('DashboardContent')] -> [CONSUMES_STATE] -> [SealedInterface('DashboardUiState')]
|
|
||||||
/**
|
|
||||||
* @summary Отображает основной контент экрана в зависимости от uiState.
|
|
||||||
* @param modifier Модификатор для стилизации.
|
|
||||||
* @param uiState Текущее состояние UI экрана.
|
|
||||||
* @param onLocationClick Лямбда-обработчик нажатия на местоположение.
|
|
||||||
* @param onLabelClick Лямбда-обработчик нажатия на метку.
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
private fun DashboardContent(
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
uiState: DashboardUiState,
|
|
||||||
onLocationClick: (LocationOutCount) -> Unit,
|
|
||||||
onLabelClick: (LabelOut) -> Unit
|
|
||||||
) {
|
|
||||||
when (uiState) {
|
|
||||||
is DashboardUiState.Loading -> {
|
|
||||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
|
||||||
CircularProgressIndicator()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is DashboardUiState.Error -> {
|
|
||||||
Box(modifier = Modifier.fillMaxSize().padding(16.dp), contentAlignment = Alignment.Center) {
|
|
||||||
Text(
|
|
||||||
text = uiState.message,
|
|
||||||
color = MaterialTheme.colorScheme.error,
|
|
||||||
textAlign = TextAlign.Center
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is DashboardUiState.Success -> {
|
|
||||||
LazyColumn(
|
|
||||||
modifier = modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.padding(horizontal = 16.dp),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(24.dp)
|
|
||||||
) {
|
|
||||||
item { Spacer(modifier = Modifier.height(8.dp)) }
|
|
||||||
item { StatisticsSection(statistics = uiState.statistics) }
|
|
||||||
item { RecentlyAddedSection(items = uiState.recentlyAddedItems) }
|
|
||||||
item { LocationsSection(locations = uiState.locations, onLocationClick = onLocationClick) }
|
|
||||||
item { LabelsSection(labels = uiState.labels, onLabelClick = onLabelClick) }
|
|
||||||
item { Spacer(modifier = Modifier.height(16.dp)) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// [END_ENTITY: Function('DashboardContent')]
|
|
||||||
|
|
||||||
// [ENTITY: Function('StatisticsSection')]
|
|
||||||
// [RELATION: Function('StatisticsSection')] -> [DEPENDS_ON] -> [DataClass('GroupStatistics')]
|
|
||||||
/**
|
|
||||||
* @summary Секция для отображения общей статистики.
|
|
||||||
* @param statistics Объект со статистическими данными.
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
private fun StatisticsSection(statistics: GroupStatistics) {
|
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(id = R.string.dashboard_section_quick_stats),
|
|
||||||
style = MaterialTheme.typography.titleMedium
|
|
||||||
)
|
|
||||||
Card {
|
|
||||||
LazyVerticalGrid(
|
|
||||||
columns = GridCells.Fixed(2),
|
|
||||||
modifier = Modifier
|
|
||||||
.height(120.dp)
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(16.dp),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
|
||||||
) {
|
|
||||||
item { StatisticCard(title = stringResource(id = R.string.dashboard_stat_total_items), value = statistics.items.toString()) }
|
|
||||||
item { StatisticCard(title = stringResource(id = R.string.dashboard_stat_total_value), value = statistics.totalValue.toString()) }
|
|
||||||
item { StatisticCard(title = stringResource(id = R.string.dashboard_stat_total_labels), value = statistics.labels.toString()) }
|
|
||||||
item { StatisticCard(title = stringResource(id = R.string.dashboard_stat_total_locations), value = statistics.locations.toString()) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// [END_ENTITY: Function('StatisticsSection')]
|
|
||||||
|
|
||||||
// [ENTITY: Function('StatisticCard')]
|
|
||||||
/**
|
|
||||||
* @summary Карточка для отображения одного статистического показателя.
|
|
||||||
* @param title Название показателя.
|
|
||||||
* @param value Значение показателя.
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
private fun StatisticCard(title: String, value: String) {
|
|
||||||
Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center) {
|
|
||||||
Text(text = title, style = MaterialTheme.typography.labelMedium, textAlign = TextAlign.Center)
|
|
||||||
Text(text = value, style = MaterialTheme.typography.headlineSmall, textAlign = TextAlign.Center)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// [END_ENTITY: Function('StatisticCard')]
|
|
||||||
|
|
||||||
// [ENTITY: Function('RecentlyAddedSection')]
|
|
||||||
// [RELATION: Function('RecentlyAddedSection')] -> [DEPENDS_ON] -> [DataClass('ItemSummary')]
|
|
||||||
/**
|
|
||||||
* @summary Секция для отображения недавно добавленных элементов.
|
|
||||||
* @param items Список элементов для отображения.
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
private fun RecentlyAddedSection(items: List<ItemSummary>) {
|
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(id = R.string.dashboard_section_recently_added),
|
|
||||||
style = MaterialTheme.typography.titleMedium
|
|
||||||
)
|
|
||||||
if (items.isEmpty()) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(id = R.string.items_not_found),
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(vertical = 16.dp),
|
|
||||||
textAlign = TextAlign.Center
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
LazyRow(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
|
||||||
items(items) { item ->
|
|
||||||
ItemCard(item = item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// [END_ENTITY: Function('RecentlyAddedSection')]
|
|
||||||
|
|
||||||
// [ENTITY: Function('ItemCard')]
|
|
||||||
// [RELATION: Function('ItemCard')] -> [DEPENDS_ON] -> [DataClass('ItemSummary')]
|
|
||||||
/**
|
|
||||||
* @summary Карточка для отображения краткой информации об элементе.
|
|
||||||
* @param item Элемент для отображения.
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
private fun ItemCard(item: ItemSummary) {
|
|
||||||
Card(modifier = Modifier.width(150.dp)) {
|
|
||||||
Column(modifier = Modifier.padding(8.dp)) {
|
|
||||||
// [AI_NOTE]: Add image here from item.image
|
|
||||||
Spacer(modifier = Modifier
|
|
||||||
.height(80.dp)
|
|
||||||
.fillMaxWidth()
|
|
||||||
.background(MaterialTheme.colorScheme.secondaryContainer))
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
Text(text = item.name, style = MaterialTheme.typography.titleSmall, maxLines = 1)
|
|
||||||
Text(text = item.location?.name ?: stringResource(id = R.string.no_location), style = MaterialTheme.typography.bodySmall, maxLines = 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// [END_ENTITY: Function('ItemCard')]
|
|
||||||
|
|
||||||
// [ENTITY: Function('LocationsSection')]
|
|
||||||
// [RELATION: Function('LocationsSection')] -> [DEPENDS_ON] -> [DataClass('LocationOutCount')]
|
|
||||||
/**
|
|
||||||
* @summary Секция для отображения местоположений в виде чипсов.
|
|
||||||
* @param locations Список местоположений.
|
|
||||||
* @param onLocationClick Лямбда-обработчик нажатия на местоположение.
|
|
||||||
*/
|
|
||||||
@OptIn(ExperimentalLayoutApi::class)
|
|
||||||
@Composable
|
|
||||||
private fun LocationsSection(locations: List<LocationOutCount>, onLocationClick: (LocationOutCount) -> Unit) {
|
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(id = R.string.dashboard_section_locations),
|
|
||||||
style = MaterialTheme.typography.titleMedium
|
|
||||||
)
|
|
||||||
FlowRow(
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
|
||||||
) {
|
|
||||||
locations.forEach { location ->
|
|
||||||
SuggestionChip(
|
|
||||||
onClick = { onLocationClick(location) },
|
|
||||||
label = { Text(stringResource(id = R.string.location_chip_label, location.name, location.itemCount)) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// [END_ENTITY: Function('LocationsSection')]
|
|
||||||
|
|
||||||
// [ENTITY: Function('LabelsSection')]
|
|
||||||
// [RELATION: Function('LabelsSection')] -> [DEPENDS_ON] -> [DataClass('LabelOut')]
|
|
||||||
/**
|
|
||||||
* @summary Секция для отображения меток в виде чипсов.
|
|
||||||
* @param labels Список меток.
|
|
||||||
* @param onLabelClick Лямбда-обработчик нажатия на метку.
|
|
||||||
*/
|
|
||||||
@OptIn(ExperimentalLayoutApi::class)
|
|
||||||
@Composable
|
|
||||||
private fun LabelsSection(labels: List<LabelOut>, onLabelClick: (LabelOut) -> Unit) {
|
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(id = R.string.dashboard_section_labels),
|
|
||||||
style = MaterialTheme.typography.titleMedium
|
|
||||||
)
|
|
||||||
FlowRow(
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
|
||||||
) {
|
|
||||||
labels.forEach { label ->
|
|
||||||
SuggestionChip(
|
|
||||||
onClick = { onLabelClick(label) },
|
|
||||||
label = { Text(label.name) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// [END_ENTITY: Function('LabelsSection')]
|
|
||||||
|
|
||||||
// [ENTITY: Function('DashboardContentSuccessPreview')]
|
|
||||||
@Preview(showBackground = true, name = "Dashboard Success State")
|
|
||||||
@Composable
|
|
||||||
fun DashboardContentSuccessPreview() {
|
|
||||||
val previewState = DashboardUiState.Success(
|
|
||||||
statistics = GroupStatistics(
|
|
||||||
items = 123,
|
|
||||||
totalValue = 9999.99,
|
|
||||||
locations = 5,
|
|
||||||
labels = 8
|
|
||||||
),
|
|
||||||
locations = listOf(
|
|
||||||
LocationOutCount(id="1", name="Office", color = "#FF0000", isArchived = false, itemCount = 10, createdAt = "", updatedAt = ""),
|
|
||||||
LocationOutCount(id="2", name="Garage", color = "#00FF00", isArchived = false, itemCount = 5, createdAt = "", updatedAt = ""),
|
|
||||||
LocationOutCount(id="3",name="Living Room", color = "#0000FF", isArchived = false, itemCount = 15, createdAt = "", updatedAt = ""),
|
|
||||||
LocationOutCount(id="4",name="Kitchen", color = "#FFFF00", isArchived = false, itemCount = 20, createdAt = "", updatedAt = ""),
|
|
||||||
LocationOutCount(id="5",name="Basement", color = "#00FFFF", isArchived = false, itemCount = 3, createdAt = "", updatedAt = "")
|
|
||||||
),
|
|
||||||
labels = listOf(
|
|
||||||
LabelOut(id="1", name="electronics", color = "#FF0000", isArchived = false, createdAt = "", updatedAt = ""),
|
|
||||||
LabelOut(id="2", name="important", color = "#00FF00", isArchived = false, createdAt = "", updatedAt = ""),
|
|
||||||
LabelOut(id="3", name="seasonal", color = "#0000FF", isArchived = false, createdAt = "", updatedAt = ""),
|
|
||||||
LabelOut(id="4", name="hobby", color = "#FFFF00", isArchived = false, createdAt = "", updatedAt = "")
|
|
||||||
),
|
|
||||||
recentlyAddedItems = emptyList()
|
|
||||||
)
|
|
||||||
HomeboxLensTheme {
|
|
||||||
DashboardContent(
|
|
||||||
uiState = previewState,
|
|
||||||
onLocationClick = {},
|
|
||||||
onLabelClick = {}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// [END_ENTITY: Function('DashboardContentSuccessPreview')]
|
|
||||||
|
|
||||||
// [ENTITY: Function('DashboardContentLoadingPreview')]
|
|
||||||
@Preview(showBackground = true, name = "Dashboard Loading State")
|
|
||||||
@Composable
|
|
||||||
fun DashboardContentLoadingPreview() {
|
|
||||||
HomeboxLensTheme {
|
|
||||||
DashboardContent(
|
|
||||||
uiState = DashboardUiState.Loading,
|
|
||||||
onLocationClick = {},
|
|
||||||
onLabelClick = {}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// [END_ENTITY: Function('DashboardContentLoadingPreview')]
|
|
||||||
|
|
||||||
// [ENTITY: Function('DashboardContentErrorPreview')]
|
|
||||||
@Preview(showBackground = true, name = "Dashboard Error State")
|
|
||||||
@Composable
|
|
||||||
fun DashboardContentErrorPreview() {
|
|
||||||
HomeboxLensTheme {
|
|
||||||
DashboardContent(
|
|
||||||
uiState = DashboardUiState.Error(stringResource(id = R.string.error_loading_failed)),
|
|
||||||
onLocationClick = {},
|
|
||||||
onLabelClick = {}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// [END_ENTITY: Function('DashboardContentErrorPreview')]
|
|
||||||
// [END_FILE_DashboardScreen.kt]
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
// [PACKAGE] com.homebox.lens.ui.screen.dashboard
|
|
||||||
// [FILE] DashboardUiState.kt
|
|
||||||
// [SEMANTICS] ui, state, dashboard
|
|
||||||
package com.homebox.lens.ui.screen.dashboard
|
|
||||||
|
|
||||||
// [IMPORTS]
|
|
||||||
import com.homebox.lens.domain.model.GroupStatistics
|
|
||||||
import com.homebox.lens.domain.model.ItemSummary
|
|
||||||
import com.homebox.lens.domain.model.LabelOut
|
|
||||||
import com.homebox.lens.domain.model.LocationOutCount
|
|
||||||
// [END_IMPORTS]
|
|
||||||
|
|
||||||
// [ENTITY: SealedInterface('DashboardUiState')]
|
|
||||||
/**
|
|
||||||
* @summary Определяет все возможные состояния для экрана "Дэшборд".
|
|
||||||
* @invariant В любой момент времени экран может находиться только в одном из этих состояний.
|
|
||||||
*/
|
|
||||||
sealed interface DashboardUiState {
|
|
||||||
// [ENTITY: DataClass('Success')]
|
|
||||||
// [RELATION: DataClass('Success')] -> [DEPENDS_ON] -> [DataClass('GroupStatistics')]
|
|
||||||
// [RELATION: DataClass('Success')] -> [DEPENDS_ON] -> [DataClass('LocationOutCount')]
|
|
||||||
// [RELATION: DataClass('Success')] -> [DEPENDS_ON] -> [DataClass('LabelOut')]
|
|
||||||
// [RELATION: DataClass('Success')] -> [DEPENDS_ON] -> [DataClass('ItemSummary')]
|
|
||||||
/**
|
|
||||||
* @summary Состояние успешной загрузки данных.
|
|
||||||
* @param statistics Статистика по инвентарю.
|
|
||||||
* @param locations Список локаций со счетчиками.
|
|
||||||
* @param labels Список всех меток.
|
|
||||||
* @param recentlyAddedItems Список недавно добавленных товаров.
|
|
||||||
*/
|
|
||||||
data class Success(
|
|
||||||
val statistics: GroupStatistics,
|
|
||||||
val locations: List<LocationOutCount>,
|
|
||||||
val labels: List<LabelOut>,
|
|
||||||
val recentlyAddedItems: List<ItemSummary>
|
|
||||||
) : DashboardUiState
|
|
||||||
// [END_ENTITY: DataClass('Success')]
|
|
||||||
|
|
||||||
// [ENTITY: DataClass('Error')]
|
|
||||||
/**
|
|
||||||
* @summary Состояние ошибки во время загрузки данных.
|
|
||||||
* @param message Человекочитаемое сообщение об ошибке.
|
|
||||||
*/
|
|
||||||
data class Error(val message: String) : DashboardUiState
|
|
||||||
// [END_ENTITY: DataClass('Error')]
|
|
||||||
|
|
||||||
// [ENTITY: Object('Loading')]
|
|
||||||
/**
|
|
||||||
* @summary Состояние, когда данные для экрана загружаются.
|
|
||||||
*/
|
|
||||||
data object Loading : DashboardUiState
|
|
||||||
// [END_ENTITY: Object('Loading')]
|
|
||||||
}
|
|
||||||
// [END_ENTITY: SealedInterface('DashboardUiState')]
|
|
||||||
// [END_FILE_DashboardUiState.kt]
|
|
||||||
@@ -1,85 +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.GetRecentlyAddedItemsUseCase
|
|
||||||
import com.homebox.lens.domain.usecase.GetStatisticsUseCase
|
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
|
||||||
import kotlinx.coroutines.flow.*
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import timber.log.Timber
|
|
||||||
import javax.inject.Inject
|
|
||||||
// [END_IMPORTS]
|
|
||||||
|
|
||||||
// [ENTITY: ViewModel('DashboardViewModel')]
|
|
||||||
// [RELATION: ViewModel('DashboardViewModel')] -> [DEPENDS_ON] -> [UseCase('GetStatisticsUseCase')]
|
|
||||||
// [RELATION: ViewModel('DashboardViewModel')] -> [DEPENDS_ON] -> [UseCase('GetAllLocationsUseCase')]
|
|
||||||
// [RELATION: ViewModel('DashboardViewModel')] -> [DEPENDS_ON] -> [UseCase('GetAllLabelsUseCase')]
|
|
||||||
// [RELATION: ViewModel('DashboardViewModel')] -> [DEPENDS_ON] -> [UseCase('GetRecentlyAddedItemsUseCase')]
|
|
||||||
// [RELATION: ViewModel('DashboardViewModel')] -> [EMITS_STATE] -> [SealedInterface('DashboardUiState')]
|
|
||||||
/**
|
|
||||||
* @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,
|
|
||||||
private val getRecentlyAddedItemsUseCase: GetRecentlyAddedItemsUseCase
|
|
||||||
) : ViewModel() {
|
|
||||||
|
|
||||||
private val _uiState = MutableStateFlow<DashboardUiState>(DashboardUiState.Loading)
|
|
||||||
val uiState = _uiState.asStateFlow()
|
|
||||||
|
|
||||||
init {
|
|
||||||
loadDashboardData()
|
|
||||||
}
|
|
||||||
|
|
||||||
// [ENTITY: Function('loadDashboardData')]
|
|
||||||
/**
|
|
||||||
* @summary Загружает все необходимые данные для экрана Dashboard.
|
|
||||||
* @description Выполняет UseCase'ы параллельно и обновляет UI, переключая его
|
|
||||||
* между состояниями `Loading`, `Success` и `Error` из `DashboardUiState`.
|
|
||||||
* @sideeffect Асинхронно обновляет `_uiState` одним из состояний `DashboardUiState`.
|
|
||||||
*/
|
|
||||||
fun loadDashboardData() {
|
|
||||||
viewModelScope.launch {
|
|
||||||
_uiState.value = DashboardUiState.Loading
|
|
||||||
Timber.i("[INFO][ENTRYPOINT][loading_dashboard_data] Starting dashboard data collection.")
|
|
||||||
|
|
||||||
val statsFlow = flow { emit(getStatisticsUseCase()) }
|
|
||||||
val locationsFlow = flow { emit(getAllLocationsUseCase()) }
|
|
||||||
val labelsFlow = flow { emit(getAllLabelsUseCase()) }
|
|
||||||
val recentItemsFlow = getRecentlyAddedItemsUseCase(limit = 10)
|
|
||||||
|
|
||||||
combine(statsFlow, locationsFlow, labelsFlow, recentItemsFlow) { stats, locations, labels, recentItems ->
|
|
||||||
DashboardUiState.Success(
|
|
||||||
statistics = stats,
|
|
||||||
locations = locations,
|
|
||||||
labels = labels,
|
|
||||||
recentlyAddedItems = recentItems
|
|
||||||
)
|
|
||||||
}.catch { exception ->
|
|
||||||
Timber.e(exception, "[ERROR][EXCEPTION][loading_failed] Failed to load dashboard data. State -> Error.")
|
|
||||||
_uiState.value = DashboardUiState.Error(
|
|
||||||
message = exception.message ?: "Could not load dashboard data."
|
|
||||||
)
|
|
||||||
}.collect { successState ->
|
|
||||||
Timber.i("[INFO][SUCCESS][dashboard_data_loaded] Dashboard data loaded successfully. State -> Success.")
|
|
||||||
_uiState.value = successState
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// [END_ENTITY: Function('loadDashboardData')]
|
|
||||||
}
|
|
||||||
// [END_ENTITY: ViewModel('DashboardViewModel')]
|
|
||||||
// [END_FILE_DashboardViewModel.kt]
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
// [PACKAGE] com.homebox.lens.ui.screen.inventorylist
|
|
||||||
// [FILE] InventoryListScreen.kt
|
|
||||||
// [SEMANTICS] ui, screen, inventory, list
|
|
||||||
|
|
||||||
package com.homebox.lens.ui.screen.inventorylist
|
|
||||||
|
|
||||||
// [IMPORTS]
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import com.homebox.lens.R
|
|
||||||
import com.homebox.lens.navigation.NavigationActions
|
|
||||||
import com.homebox.lens.ui.common.MainScaffold
|
|
||||||
// [END_IMPORTS]
|
|
||||||
|
|
||||||
// [ENTITY: Function('InventoryListScreen')]
|
|
||||||
// [RELATION: Function('InventoryListScreen')] -> [DEPENDS_ON] -> [Class('NavigationActions')]
|
|
||||||
// [RELATION: Function('InventoryListScreen')] -> [CALLS] -> [Function('MainScaffold')]
|
|
||||||
/**
|
|
||||||
* @summary Composable-функция для экрана "Список инвентаря".
|
|
||||||
* @param currentRoute Текущий маршрут для подсветки активного элемента в Drawer.
|
|
||||||
* @param navigationActions Объект с навигационными действиями.
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
fun InventoryListScreen(
|
|
||||||
currentRoute: String?,
|
|
||||||
navigationActions: NavigationActions
|
|
||||||
) {
|
|
||||||
MainScaffold(
|
|
||||||
topBarTitle = stringResource(id = R.string.inventory_list_title),
|
|
||||||
currentRoute = currentRoute,
|
|
||||||
navigationActions = navigationActions
|
|
||||||
) {
|
|
||||||
// [AI_NOTE]: Implement Inventory List Screen UI
|
|
||||||
Text(text = "Inventory List Screen")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// [END_ENTITY: Function('InventoryListScreen')]
|
|
||||||
// [END_FILE_InventoryListScreen.kt]
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
// [PACKAGE] com.homebox.lens.ui.screen.inventorylist
|
|
||||||
// [FILE] InventoryListViewModel.kt
|
|
||||||
// [SEMANTICS] ui, viewmodel, inventory_list
|
|
||||||
package com.homebox.lens.ui.screen.inventorylist
|
|
||||||
|
|
||||||
// [IMPORTS]
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
|
||||||
import javax.inject.Inject
|
|
||||||
// [END_IMPORTS]
|
|
||||||
|
|
||||||
// [ENTITY: ViewModel('InventoryListViewModel')]
|
|
||||||
/**
|
|
||||||
* @summary ViewModel for the inventory list screen.
|
|
||||||
*/
|
|
||||||
@HiltViewModel
|
|
||||||
class InventoryListViewModel @Inject constructor() : ViewModel() {
|
|
||||||
// [AI_NOTE]: Implement UI state
|
|
||||||
}
|
|
||||||
// [END_ENTITY: ViewModel('InventoryListViewModel')]
|
|
||||||
// [END_FILE_InventoryListViewModel.kt]
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
// [PACKAGE] com.homebox.lens.ui.screen.itemdetails
|
|
||||||
// [FILE] ItemDetailsScreen.kt
|
|
||||||
// [SEMANTICS] ui, screen, item, details
|
|
||||||
|
|
||||||
package com.homebox.lens.ui.screen.itemdetails
|
|
||||||
|
|
||||||
// [IMPORTS]
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import com.homebox.lens.R
|
|
||||||
import com.homebox.lens.navigation.NavigationActions
|
|
||||||
import com.homebox.lens.ui.common.MainScaffold
|
|
||||||
// [END_IMPORTS]
|
|
||||||
|
|
||||||
// [ENTITY: Function('ItemDetailsScreen')]
|
|
||||||
// [RELATION: Function('ItemDetailsScreen')] -> [DEPENDS_ON] -> [Class('NavigationActions')]
|
|
||||||
// [RELATION: Function('ItemDetailsScreen')] -> [CALLS] -> [Function('MainScaffold')]
|
|
||||||
/**
|
|
||||||
* @summary Composable-функция для экрана "Детали элемента".
|
|
||||||
* @param currentRoute Текущий маршрут для подсветки активного элемента в Drawer.
|
|
||||||
* @param navigationActions Объект с навигационными действиями.
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
fun ItemDetailsScreen(
|
|
||||||
currentRoute: String?,
|
|
||||||
navigationActions: NavigationActions
|
|
||||||
) {
|
|
||||||
MainScaffold(
|
|
||||||
topBarTitle = stringResource(id = R.string.item_details_title),
|
|
||||||
currentRoute = currentRoute,
|
|
||||||
navigationActions = navigationActions
|
|
||||||
) {
|
|
||||||
// [AI_NOTE]: Implement Item Details Screen UI
|
|
||||||
Text(text = "Item Details Screen")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// [END_ENTITY: Function('ItemDetailsScreen')]
|
|
||||||
// [END_FILE_ItemDetailsScreen.kt]
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
// [PACKAGE] com.homebox.lens.ui.screen.itemdetails
|
|
||||||
// [FILE] ItemDetailsViewModel.kt
|
|
||||||
// [SEMANTICS] ui, viewmodel, item_details
|
|
||||||
package com.homebox.lens.ui.screen.itemdetails
|
|
||||||
|
|
||||||
// [IMPORTS]
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
|
||||||
import javax.inject.Inject
|
|
||||||
// [END_IMPORTS]
|
|
||||||
|
|
||||||
// [ENTITY: ViewModel('ItemDetailsViewModel')]
|
|
||||||
/**
|
|
||||||
* @summary ViewModel for the item details screen.
|
|
||||||
*/
|
|
||||||
@HiltViewModel
|
|
||||||
class ItemDetailsViewModel @Inject constructor() : ViewModel() {
|
|
||||||
// [AI_NOTE]: Implement UI state
|
|
||||||
}
|
|
||||||
// [END_ENTITY: ViewModel('ItemDetailsViewModel')]
|
|
||||||
// [END_FILE_ItemDetailsViewModel.kt]
|
|
||||||
@@ -1,139 +0,0 @@
|
|||||||
// [PACKAGE] com.homebox.lens.ui.screen.itemedit
|
|
||||||
// [FILE] ItemEditScreen.kt
|
|
||||||
// [SEMANTICS] ui, screen, item, edit
|
|
||||||
|
|
||||||
package com.homebox.lens.ui.screen.itemedit
|
|
||||||
|
|
||||||
// [IMPORTS]
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.filled.Save
|
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
|
||||||
import androidx.compose.material3.FloatingActionButton
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.OutlinedTextField
|
|
||||||
import androidx.compose.material3.Scaffold
|
|
||||||
import androidx.compose.material3.SnackbarHost
|
|
||||||
import androidx.compose.material3.SnackbarHostState
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.collectAsState
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.text.input.KeyboardType
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
|
||||||
import com.homebox.lens.R
|
|
||||||
import com.homebox.lens.navigation.NavigationActions
|
|
||||||
import com.homebox.lens.ui.common.MainScaffold
|
|
||||||
import timber.log.Timber
|
|
||||||
// [END_IMPORTS]
|
|
||||||
|
|
||||||
// [ENTITY: Function('ItemEditScreen')]
|
|
||||||
// [RELATION: Function('ItemEditScreen')] -> [DEPENDS_ON] -> [Class('NavigationActions')]
|
|
||||||
// [RELATION: Function('ItemEditScreen')] -> [DEPENDS_ON] -> [ViewModel('ItemEditViewModel')]
|
|
||||||
// [RELATION: Function('ItemEditScreen')] -> [CONSUMES_STATE] -> [DataClass('ItemEditUiState')]
|
|
||||||
// [RELATION: Function('ItemEditScreen')] -> [CALLS] -> [Function('MainScaffold')]
|
|
||||||
/**
|
|
||||||
* @summary Composable-функция для экрана "Редактирование элемента".
|
|
||||||
* @param currentRoute Текущий маршрут для подсветки активного элемента в Drawer.
|
|
||||||
* @param navigationActions Объект с навигационными действиями.
|
|
||||||
* @param itemId ID элемента для редактирования. Null, если создается новый элемент.
|
|
||||||
* @param viewModel ViewModel для управления состоянием экрана.
|
|
||||||
* @param onSaveSuccess Callback, вызываемый после успешного сохранения товара.
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
fun ItemEditScreen(
|
|
||||||
currentRoute: String?,
|
|
||||||
navigationActions: NavigationActions,
|
|
||||||
itemId: String?,
|
|
||||||
viewModel: ItemEditViewModel = hiltViewModel(),
|
|
||||||
onSaveSuccess: () -> Unit
|
|
||||||
) {
|
|
||||||
val uiState by viewModel.uiState.collectAsState()
|
|
||||||
val snackbarHostState = remember { SnackbarHostState() }
|
|
||||||
|
|
||||||
LaunchedEffect(itemId) {
|
|
||||||
Timber.i("[INFO][ENTRYPOINT][item_edit_screen_init] Initializing ItemEditScreen for item ID: %s", itemId)
|
|
||||||
viewModel.loadItem(itemId)
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(uiState.error) {
|
|
||||||
uiState.error?.let {
|
|
||||||
snackbarHostState.showSnackbar(it)
|
|
||||||
Timber.e("[ERROR][UI_ERROR][item_edit_error] Displaying error: %s", it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
viewModel.saveCompleted.collect {
|
|
||||||
Timber.i("[INFO][ACTION][save_completed_callback] Item save completed. Triggering onSaveSuccess.")
|
|
||||||
onSaveSuccess()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MainScaffold(
|
|
||||||
topBarTitle = stringResource(id = R.string.item_edit_title),
|
|
||||||
currentRoute = currentRoute,
|
|
||||||
navigationActions = navigationActions
|
|
||||||
) {
|
|
||||||
Scaffold(
|
|
||||||
snackbarHost = { SnackbarHost(snackbarHostState) },
|
|
||||||
floatingActionButton = {
|
|
||||||
FloatingActionButton(onClick = {
|
|
||||||
Timber.i("[INFO][ACTION][save_button_click] Save button clicked.")
|
|
||||||
viewModel.saveItem()
|
|
||||||
}) {
|
|
||||||
Icon(Icons.Filled.Save, contentDescription = stringResource(R.string.save_item))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.padding(it)
|
|
||||||
.padding(16.dp)
|
|
||||||
) {
|
|
||||||
if (uiState.isLoading) {
|
|
||||||
CircularProgressIndicator(modifier = Modifier.fillMaxWidth())
|
|
||||||
} else {
|
|
||||||
uiState.item?.let { item ->
|
|
||||||
OutlinedTextField(
|
|
||||||
value = item.name,
|
|
||||||
onValueChange = { viewModel.updateName(it) },
|
|
||||||
label = { Text(stringResource(R.string.item_name)) },
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
OutlinedTextField(
|
|
||||||
value = item.description ?: "",
|
|
||||||
onValueChange = { viewModel.updateDescription(it) },
|
|
||||||
label = { Text(stringResource(R.string.item_description)) },
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
OutlinedTextField(
|
|
||||||
value = item.quantity.toString(),
|
|
||||||
onValueChange = { viewModel.updateQuantity(it.toIntOrNull() ?: 0) },
|
|
||||||
label = { Text(stringResource(R.string.item_quantity)) },
|
|
||||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
)
|
|
||||||
// Add more fields as needed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// [END_ENTITY: Function('ItemEditScreen')]
|
|
||||||
// [END_FILE_ItemEditScreen.kt]
|
|
||||||
@@ -1,214 +0,0 @@
|
|||||||
// [PACKAGE] com.homebox.lens.ui.screen.itemedit
|
|
||||||
// [FILE] ItemEditViewModel.kt
|
|
||||||
// [SEMANTICS] ui, viewmodel, item_edit
|
|
||||||
|
|
||||||
package com.homebox.lens.ui.screen.itemedit
|
|
||||||
|
|
||||||
// [IMPORTS]
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import com.homebox.lens.domain.model.Item
|
|
||||||
import com.homebox.lens.domain.model.ItemCreate
|
|
||||||
import com.homebox.lens.domain.model.Label
|
|
||||||
import com.homebox.lens.domain.model.Location
|
|
||||||
import com.homebox.lens.domain.usecase.CreateItemUseCase
|
|
||||||
import com.homebox.lens.domain.usecase.GetItemDetailsUseCase
|
|
||||||
import com.homebox.lens.domain.usecase.UpdateItemUseCase
|
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.SharedFlow
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
|
||||||
import kotlinx.coroutines.flow.asSharedFlow
|
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import timber.log.Timber
|
|
||||||
import javax.inject.Inject
|
|
||||||
// [END_IMPORTS]
|
|
||||||
|
|
||||||
// [ENTITY: DataClass('ItemEditUiState')]
|
|
||||||
/**
|
|
||||||
* @summary UI state for the item edit screen.
|
|
||||||
* @param item The item being edited, or null if creating a new item.
|
|
||||||
* @param isLoading Whether data is currently being loaded or saved.
|
|
||||||
* @param error An error message if an operation failed.
|
|
||||||
*/
|
|
||||||
data class ItemEditUiState(
|
|
||||||
val item: Item? = null,
|
|
||||||
val isLoading: Boolean = false,
|
|
||||||
val error: String? = null
|
|
||||||
)
|
|
||||||
// [END_ENTITY: DataClass('ItemEditUiState')]
|
|
||||||
|
|
||||||
// [ENTITY: ViewModel('ItemEditViewModel')]
|
|
||||||
// [RELATION: ViewModel('ItemEditViewModel')] -> [DEPENDS_ON] -> [UseCase('CreateItemUseCase')]
|
|
||||||
// [RELATION: ViewModel('ItemEditViewModel')] -> [DEPENDS_ON] -> [UseCase('UpdateItemUseCase')]
|
|
||||||
// [RELATION: ViewModel('ItemEditViewModel')] -> [DEPENDS_ON] -> [UseCase('GetItemDetailsUseCase')]
|
|
||||||
// [RELATION: ViewModel('ItemEditViewModel')] -> [EMITS_STATE] -> [DataClass('ItemEditUiState')]
|
|
||||||
/**
|
|
||||||
* @summary ViewModel for the item edit screen.
|
|
||||||
*/
|
|
||||||
@HiltViewModel
|
|
||||||
class ItemEditViewModel @Inject constructor(
|
|
||||||
private val createItemUseCase: CreateItemUseCase,
|
|
||||||
private val updateItemUseCase: UpdateItemUseCase,
|
|
||||||
private val getItemDetailsUseCase: GetItemDetailsUseCase
|
|
||||||
) : ViewModel() {
|
|
||||||
|
|
||||||
private val _uiState = MutableStateFlow(ItemEditUiState())
|
|
||||||
val uiState: StateFlow<ItemEditUiState> = _uiState.asStateFlow()
|
|
||||||
|
|
||||||
private val _saveCompleted = MutableSharedFlow<Unit>()
|
|
||||||
val saveCompleted: SharedFlow<Unit> = _saveCompleted.asSharedFlow()
|
|
||||||
|
|
||||||
// [ENTITY: Function('loadItem')]
|
|
||||||
/**
|
|
||||||
* @summary Loads item details for editing or prepares for new item creation.
|
|
||||||
* @param itemId The ID of the item to load. If null, a new item is being created.
|
|
||||||
* @sideeffect Updates `_uiState` with loading, success, or error states.
|
|
||||||
*/
|
|
||||||
fun loadItem(itemId: String?) {
|
|
||||||
Timber.i("[INFO][ENTRYPOINT][loading_item] Attempting to load item with ID: %s", itemId)
|
|
||||||
viewModelScope.launch {
|
|
||||||
_uiState.value = _uiState.value.copy(isLoading = true, error = null)
|
|
||||||
if (itemId == null) {
|
|
||||||
Timber.i("[INFO][ACTION][new_item_preparation] Preparing for new item creation.")
|
|
||||||
_uiState.value = _uiState.value.copy(isLoading = false, item = Item(id = "", name = "", description = null, quantity = 0, image = null, location = null, labels = emptyList(), value = null, createdAt = null))
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
Timber.i("[INFO][ACTION][fetching_item_details] Fetching details for item ID: %s", itemId)
|
|
||||||
val itemOut = getItemDetailsUseCase(itemId)
|
|
||||||
Timber.d("[DEBUG][ACTION][mapping_item_out_to_item] Mapping ItemOut to Item for UI state.")
|
|
||||||
val item = Item(
|
|
||||||
id = itemOut.id,
|
|
||||||
name = itemOut.name,
|
|
||||||
description = itemOut.description,
|
|
||||||
quantity = itemOut.quantity,
|
|
||||||
image = itemOut.images.firstOrNull()?.path, // Assuming first image is the main one
|
|
||||||
location = itemOut.location?.let { Location(it.id, it.name) }, // Simplified mapping
|
|
||||||
labels = itemOut.labels.map { Label(it.id, it.name) }, // Simplified mapping
|
|
||||||
value = itemOut.value?.toBigDecimal(),
|
|
||||||
createdAt = itemOut.createdAt
|
|
||||||
)
|
|
||||||
_uiState.value = _uiState.value.copy(isLoading = false, item = item)
|
|
||||||
Timber.i("[INFO][ACTION][item_details_fetched] Successfully fetched item details for ID: %s", itemId)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.e(e, "[ERROR][FALLBACK][item_load_failed] Failed to load item details for ID: %s", itemId)
|
|
||||||
_uiState.value = _uiState.value.copy(isLoading = false, error = e.localizedMessage)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// [END_ENTITY: Function('loadItem')]
|
|
||||||
|
|
||||||
// [ENTITY: Function('saveItem')]
|
|
||||||
/**
|
|
||||||
* @summary Saves the current item, either creating a new one or updating an existing one.
|
|
||||||
* @sideeffect Updates `_uiState` with loading, success, or error states. Calls `createItemUseCase` or `updateItemUseCase`.
|
|
||||||
* @throws IllegalStateException if `uiState.value.item` is null when attempting to save.
|
|
||||||
*/
|
|
||||||
fun saveItem() {
|
|
||||||
Timber.i("[INFO][ENTRYPOINT][saving_item] Attempting to save item.")
|
|
||||||
viewModelScope.launch {
|
|
||||||
val currentItem = _uiState.value.item
|
|
||||||
require(currentItem != null) { "[CONTRACT_VIOLATION][PRECONDITION][item_not_present] Cannot save a null item." }
|
|
||||||
|
|
||||||
_uiState.value = _uiState.value.copy(isLoading = true, error = null)
|
|
||||||
try {
|
|
||||||
if (currentItem.id.isBlank()) {
|
|
||||||
Timber.i("[INFO][ACTION][creating_new_item] Creating new item: %s", currentItem.name)
|
|
||||||
val createdItemSummary = createItemUseCase(ItemCreate(
|
|
||||||
name = currentItem.name,
|
|
||||||
description = currentItem.description,
|
|
||||||
quantity = currentItem.quantity,
|
|
||||||
assetId = null, // Item does not have assetId
|
|
||||||
notes = null, // Item does not have notes
|
|
||||||
serialNumber = null, // Item does not have serialNumber
|
|
||||||
value = currentItem.value?.toDouble(), // Convert BigDecimal to Double
|
|
||||||
purchasePrice = null, // Item does not have purchasePrice
|
|
||||||
purchaseDate = null, // Item does not have purchaseDate
|
|
||||||
warrantyUntil = null, // Item does not have warrantyUntil
|
|
||||||
locationId = currentItem.location?.id,
|
|
||||||
parentId = null, // Item does not have parentId
|
|
||||||
labelIds = currentItem.labels.map { it.id }
|
|
||||||
))
|
|
||||||
Timber.d("[DEBUG][ACTION][mapping_item_summary_to_item] Mapping ItemSummary to Item for UI state.")
|
|
||||||
val createdItem = Item(
|
|
||||||
id = createdItemSummary.id,
|
|
||||||
name = createdItemSummary.name,
|
|
||||||
description = null, // ItemSummary does not have description
|
|
||||||
quantity = 0, // ItemSummary does not have quantity
|
|
||||||
image = null, // ItemSummary does not have image
|
|
||||||
location = null, // ItemSummary does not have location
|
|
||||||
labels = emptyList(), // ItemSummary does not have labels
|
|
||||||
value = null, // ItemSummary does not have value
|
|
||||||
createdAt = null // ItemSummary does not have createdAt
|
|
||||||
)
|
|
||||||
_uiState.value = _uiState.value.copy(isLoading = false, item = createdItem)
|
|
||||||
Timber.i("[INFO][ACTION][new_item_created] Successfully created new item with ID: %s", createdItem.id)
|
|
||||||
_saveCompleted.emit(Unit)
|
|
||||||
} else {
|
|
||||||
Timber.i("[INFO][ACTION][updating_existing_item] Updating existing item with ID: %s", currentItem.id)
|
|
||||||
val updatedItemOut = updateItemUseCase(currentItem)
|
|
||||||
Timber.d("[DEBUG][ACTION][mapping_item_out_to_item] Mapping ItemOut to Item for UI state.")
|
|
||||||
val updatedItem = Item(
|
|
||||||
id = updatedItemOut.id,
|
|
||||||
name = updatedItemOut.name,
|
|
||||||
description = updatedItemOut.description,
|
|
||||||
quantity = updatedItemOut.quantity,
|
|
||||||
image = updatedItemOut.images.firstOrNull()?.path,
|
|
||||||
location = updatedItemOut.location?.let { Location(it.id, it.name) },
|
|
||||||
labels = updatedItemOut.labels.map { Label(it.id, it.name) },
|
|
||||||
value = updatedItemOut.value.toBigDecimal(),
|
|
||||||
createdAt = updatedItemOut.createdAt
|
|
||||||
)
|
|
||||||
_uiState.value = _uiState.value.copy(isLoading = false, item = updatedItem)
|
|
||||||
Timber.i("[INFO][ACTION][item_updated] Successfully updated item with ID: %s", updatedItem.id)
|
|
||||||
_saveCompleted.emit(Unit)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.e(e, "[ERROR][FALLBACK][item_save_failed] Failed to save item.")
|
|
||||||
_uiState.value = _uiState.value.copy(isLoading = false, error = e.localizedMessage)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// [END_ENTITY: Function('saveItem')]
|
|
||||||
|
|
||||||
// [ENTITY: Function('updateName')]
|
|
||||||
/**
|
|
||||||
* @summary Updates the name of the item in the UI state.
|
|
||||||
* @param newName The new name for the item.
|
|
||||||
* @sideeffect Updates the `item` in `_uiState`.
|
|
||||||
*/
|
|
||||||
fun updateName(newName: String) {
|
|
||||||
Timber.d("[DEBUG][ACTION][updating_item_name] Updating item name to: %s", newName)
|
|
||||||
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(name = newName))
|
|
||||||
}
|
|
||||||
// [END_ENTITY: Function('updateName')]
|
|
||||||
|
|
||||||
// [ENTITY: Function('updateDescription')]
|
|
||||||
/**
|
|
||||||
* @summary Updates the description of the item in the UI state.
|
|
||||||
* @param newDescription The new description for the item.
|
|
||||||
* @sideeffect Updates the `item` in `_uiState`.
|
|
||||||
*/
|
|
||||||
fun updateDescription(newDescription: String) {
|
|
||||||
Timber.d("[DEBUG][ACTION][updating_item_description] Updating item description to: %s", newDescription)
|
|
||||||
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(description = newDescription))
|
|
||||||
}
|
|
||||||
// [END_ENTITY: Function('updateDescription')]
|
|
||||||
|
|
||||||
// [ENTITY: Function('updateQuantity')]
|
|
||||||
/**
|
|
||||||
* @summary Updates the quantity of the item in the UI state.
|
|
||||||
* @param newQuantity The new quantity for the item.
|
|
||||||
* @sideeffect Updates the `item` in `_uiState`.
|
|
||||||
*/
|
|
||||||
fun updateQuantity(newQuantity: Int) {
|
|
||||||
Timber.d("[DEBUG][ACTION][updating_item_quantity] Updating item quantity to: %d", newQuantity)
|
|
||||||
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(quantity = newQuantity))
|
|
||||||
}
|
|
||||||
// [END_ENTITY: Function('updateQuantity')]
|
|
||||||
}
|
|
||||||
// [END_ENTITY: ViewModel('ItemEditViewModel')]
|
|
||||||
// [END_FILE_ItemEditViewModel.kt]
|
|
||||||
@@ -1,113 +0,0 @@
|
|||||||
// [PACKAGE] com.homebox.lens.ui.screen.labeledit
|
|
||||||
// [FILE] LabelEditScreen.kt
|
|
||||||
// [SEMANTICS] ui, screen, label, edit
|
|
||||||
|
|
||||||
package com.homebox.lens.ui.screen.labeledit
|
|
||||||
|
|
||||||
// [IMPORTS]
|
|
||||||
import androidx.compose.foundation.layout.*
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
|
||||||
import androidx.compose.material.icons.filled.Check
|
|
||||||
import androidx.compose.material3.*
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
|
||||||
import com.homebox.lens.R
|
|
||||||
import com.homebox.lens.ui.components.ColorPicker
|
|
||||||
import com.homebox.lens.ui.components.LoadingOverlay
|
|
||||||
// [END_IMPORTS]
|
|
||||||
|
|
||||||
// [ENTITY: Function('LabelEditScreen')]
|
|
||||||
// [RELATION: Function('LabelEditScreen')] -> [DEPENDS_ON] -> [ViewModel('LabelEditViewModel')]
|
|
||||||
/**
|
|
||||||
* @summary Composable-функция для экрана "Редактирование метки".
|
|
||||||
* @param labelId ID метки для редактирования или null для создания новой.
|
|
||||||
* @param onBack Навигация назад.
|
|
||||||
* @param onLabelSaved Действие после сохранения метки.
|
|
||||||
*/
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
|
||||||
fun LabelEditScreen(
|
|
||||||
labelId: String?,
|
|
||||||
onBack: () -> Unit,
|
|
||||||
onLabelSaved: () -> Unit,
|
|
||||||
viewModel: LabelEditViewModel = hiltViewModel()
|
|
||||||
) {
|
|
||||||
val uiState = viewModel.uiState
|
|
||||||
val snackbarHostState = SnackbarHostState()
|
|
||||||
|
|
||||||
LaunchedEffect(uiState.isSaved) {
|
|
||||||
if (uiState.isSaved) {
|
|
||||||
onLabelSaved()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(uiState.error) {
|
|
||||||
uiState.error?.let {
|
|
||||||
snackbarHostState.showSnackbar(
|
|
||||||
message = it,
|
|
||||||
actionLabel = "Dismiss",
|
|
||||||
duration = SnackbarDuration.Short
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Scaffold(
|
|
||||||
snackbarHost = { SnackbarHost(snackbarHostState) },
|
|
||||||
topBar = {
|
|
||||||
TopAppBar(
|
|
||||||
title = {
|
|
||||||
Text(
|
|
||||||
text = if (labelId == null) {
|
|
||||||
stringResource(id = R.string.label_edit_title_create)
|
|
||||||
} else {
|
|
||||||
stringResource(id = R.string.label_edit_title_edit)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
},
|
|
||||||
navigationIcon = {
|
|
||||||
IconButton(onClick = onBack) {
|
|
||||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.back))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
actions = {
|
|
||||||
IconButton(onClick = viewModel::saveLabel) {
|
|
||||||
Icon(Icons.Default.Check, contentDescription = stringResource(R.string.save))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
) { paddingValues ->
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.padding(paddingValues)
|
|
||||||
.padding(16.dp)
|
|
||||||
) {
|
|
||||||
OutlinedTextField(
|
|
||||||
value = uiState.name,
|
|
||||||
onValueChange = viewModel::onNameChange,
|
|
||||||
label = { Text(stringResource(R.string.label_name)) },
|
|
||||||
isError = uiState.nameError != null,
|
|
||||||
supportingText = { uiState.nameError?.let { Text(it) } },
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
ColorPicker(
|
|
||||||
selectedColor = uiState.color,
|
|
||||||
onColorSelected = viewModel::onColorChange,
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (uiState.isLoading) {
|
|
||||||
LoadingOverlay()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// [END_ENTITY: Function('LabelEditScreen')]
|
|
||||||
// [END_FILE_LabelEditScreen.kt]
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
// [PACKAGE] com.homebox.lens.ui.screen.labeledit
|
|
||||||
// [FILE] LabelEditViewModel.kt
|
|
||||||
// [SEMANTICS] ui, viewmodel, label_management
|
|
||||||
|
|
||||||
package com.homebox.lens.ui.screen.labeledit
|
|
||||||
|
|
||||||
// [IMPORTS]
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.lifecycle.SavedStateHandle
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import com.homebox.lens.domain.model.LabelCreate
|
|
||||||
import com.homebox.lens.domain.model.LabelOut
|
|
||||||
import com.homebox.lens.domain.model.LabelUpdate
|
|
||||||
import com.homebox.lens.domain.usecase.CreateLabelUseCase
|
|
||||||
import com.homebox.lens.domain.usecase.GetLabelDetailsUseCase
|
|
||||||
import com.homebox.lens.domain.usecase.UpdateLabelUseCase
|
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import javax.inject.Inject
|
|
||||||
// [END_IMPORTS]
|
|
||||||
|
|
||||||
// [ENTITY: ViewModel('LabelEditViewModel')]
|
|
||||||
// [RELATION: ViewModel('LabelEditViewModel')] -> [DEPENDS_ON] -> [UseCase('GetLabelDetailsUseCase')]
|
|
||||||
// [RELATION: ViewModel('LabelEditViewModel')] -> [DEPENDS_ON] -> [UseCase('CreateLabelUseCase')]
|
|
||||||
// [RELATION: ViewModel('LabelEditViewModel')] -> [DEPENDS_ON] -> [UseCase('UpdateLabelUseCase')]
|
|
||||||
// [RELATION: ViewModel('LabelEditViewModel')] -> [EMITS_STATE] -> [DataClass('LabelEditUiState')]
|
|
||||||
@HiltViewModel
|
|
||||||
class LabelEditViewModel @Inject constructor(
|
|
||||||
private val savedStateHandle: SavedStateHandle,
|
|
||||||
private val getLabelDetailsUseCase: GetLabelDetailsUseCase,
|
|
||||||
private val createLabelUseCase: CreateLabelUseCase,
|
|
||||||
private val updateLabelUseCase: UpdateLabelUseCase
|
|
||||||
) : ViewModel() {
|
|
||||||
|
|
||||||
var uiState by mutableStateOf(LabelEditUiState())
|
|
||||||
private set
|
|
||||||
|
|
||||||
private val labelId: String? = savedStateHandle["labelId"]
|
|
||||||
|
|
||||||
init {
|
|
||||||
if (labelId != null) {
|
|
||||||
loadLabelDetails(labelId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onNameChange(newName: String) {
|
|
||||||
uiState = uiState.copy(name = newName, nameError = null)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onColorChange(newColor: String) {
|
|
||||||
uiState = uiState.copy(color = newColor)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun saveLabel() {
|
|
||||||
viewModelScope.launch {
|
|
||||||
if (uiState.name.isBlank()) {
|
|
||||||
uiState = uiState.copy(nameError = "Label name cannot be empty.")
|
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
|
|
||||||
uiState = uiState.copy(isLoading = true, error = null)
|
|
||||||
try {
|
|
||||||
if (labelId == null) {
|
|
||||||
// Create new label
|
|
||||||
val newLabel = LabelCreate(name = uiState.name, color = uiState.color)
|
|
||||||
createLabelUseCase(newLabel)
|
|
||||||
} else {
|
|
||||||
// Update existing label
|
|
||||||
val updatedLabel = LabelUpdate(name = uiState.name, color = uiState.color)
|
|
||||||
updateLabelUseCase(labelId, updatedLabel)
|
|
||||||
}
|
|
||||||
uiState = uiState.copy(isSaved = true)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
uiState = uiState.copy(error = e.message, isLoading = false)
|
|
||||||
} finally {
|
|
||||||
uiState = uiState.copy(isLoading = false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun loadLabelDetails(id: String) {
|
|
||||||
viewModelScope.launch {
|
|
||||||
uiState = uiState.copy(isLoading = true, error = null)
|
|
||||||
try {
|
|
||||||
val label = getLabelDetailsUseCase(id)
|
|
||||||
uiState = uiState.copy(
|
|
||||||
name = label.name,
|
|
||||||
color = label.color,
|
|
||||||
isLoading = false
|
|
||||||
)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
uiState = uiState.copy(error = e.message, isLoading = false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// [ENTITY: DataClass('LabelEditUiState')]
|
|
||||||
/**
|
|
||||||
* @summary Состояние UI для экрана редактирования метки.
|
|
||||||
*/
|
|
||||||
data class LabelEditUiState(
|
|
||||||
val name: String = "",
|
|
||||||
val color: String = "#FFFFFF", // Default color
|
|
||||||
val nameError: String? = null,
|
|
||||||
val isLoading: Boolean = false,
|
|
||||||
val error: String? = null,
|
|
||||||
val isSaved: Boolean = false,
|
|
||||||
val originalLabel: LabelOut? = null // To hold original label details if editing
|
|
||||||
)
|
|
||||||
// [END_ENTITY: DataClass('LabelEditUiState')]
|
|
||||||
// [END_FILE_LabelEditViewModel.kt]
|
|
||||||
@@ -1,174 +0,0 @@
|
|||||||
// [PACKAGE] com.homebox.lens.ui.screen.labelslist
|
|
||||||
// [FILE] LabelsListScreen.kt
|
|
||||||
// [SEMANTICS] ui, labels_list, state_management, compose, dialog
|
|
||||||
package com.homebox.lens.ui.screen.labelslist
|
|
||||||
|
|
||||||
// [IMPORTS]
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
|
||||||
import androidx.compose.foundation.lazy.items
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
|
||||||
import androidx.compose.material.icons.automirrored.filled.Label
|
|
||||||
import androidx.compose.material.icons.filled.Add
|
|
||||||
import androidx.compose.material3.AlertDialog
|
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.material3.FloatingActionButton
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.ListItem
|
|
||||||
import androidx.compose.material3.OutlinedTextField
|
|
||||||
import androidx.compose.material3.Scaffold
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.material3.TextButton
|
|
||||||
import androidx.compose.material3.TopAppBar
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.collectAsState
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
|
||||||
import com.homebox.lens.R
|
|
||||||
import com.homebox.lens.domain.model.Label
|
|
||||||
import com.homebox.lens.navigation.NavigationActions
|
|
||||||
import com.homebox.lens.navigation.Screen
|
|
||||||
import com.homebox.lens.ui.common.MainScaffold
|
|
||||||
import timber.log.Timber
|
|
||||||
// [END_IMPORTS]
|
|
||||||
|
|
||||||
// [ENTITY: Function('LabelsListScreen')]
|
|
||||||
// [RELATION: Function('LabelsListScreen')] -> [DEPENDS_ON] -> [ViewModel('LabelsListViewModel')]
|
|
||||||
// [RELATION: Function('LabelsListScreen')] -> [DEPENDS_ON] -> [Framework('NavController')]
|
|
||||||
/**
|
|
||||||
* @summary Отображает экран со списком всех меток.
|
|
||||||
* @param navController Контроллер навигации для перемещения между экранами.
|
|
||||||
* @param viewModel ViewModel, предоставляющая состояние UI для экрана меток.
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
fun LabelsListScreen(
|
|
||||||
currentRoute: String?,
|
|
||||||
navigationActions: NavigationActions,
|
|
||||||
viewModel: LabelsListViewModel = hiltViewModel()
|
|
||||||
) {
|
|
||||||
val uiState by viewModel.uiState.collectAsState()
|
|
||||||
|
|
||||||
MainScaffold(
|
|
||||||
topBarTitle = stringResource(id = R.string.screen_title_labels),
|
|
||||||
currentRoute = currentRoute,
|
|
||||||
navigationActions = navigationActions
|
|
||||||
) { paddingValues ->
|
|
||||||
Scaffold(
|
|
||||||
floatingActionButton = {
|
|
||||||
FloatingActionButton(onClick = {
|
|
||||||
Timber.i("[INFO][ACTION][navigate_to_label_edit] FAB clicked: Navigate to create new label screen.")
|
|
||||||
navigationActions.navigateToLabelEdit(null)
|
|
||||||
}) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Default.Add,
|
|
||||||
contentDescription = stringResource(id = R.string.content_desc_create_label)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
) { innerPaddingValues ->
|
|
||||||
val currentState = uiState
|
|
||||||
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.padding(innerPaddingValues), // Use innerPaddingValues here
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
when (currentState) {
|
|
||||||
is LabelsListUiState.Loading -> {
|
|
||||||
CircularProgressIndicator()
|
|
||||||
}
|
|
||||||
is LabelsListUiState.Error -> {
|
|
||||||
Text(text = currentState.message)
|
|
||||||
}
|
|
||||||
is LabelsListUiState.Success -> {
|
|
||||||
if (currentState.labels.isEmpty()) {
|
|
||||||
Text(text = stringResource(id = R.string.no_labels_found))
|
|
||||||
} else {
|
|
||||||
LabelsList(
|
|
||||||
labels = currentState.labels,
|
|
||||||
onLabelClick = { label ->
|
|
||||||
Timber.i("[INFO][ACTION][navigate_to_label_edit] Label clicked: ${label.id}. Navigating to label edit screen.")
|
|
||||||
navigationActions.navigateToLabelEdit(label.id)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// [END_ENTITY: Function('LabelsListScreen')]
|
|
||||||
|
|
||||||
// [ENTITY: Function('LabelsList')]
|
|
||||||
// [RELATION: Function('LabelsList')] -> [DEPENDS_ON] -> [DataClass('Label')]
|
|
||||||
/**
|
|
||||||
* @summary Composable-функция для отображения списка меток.
|
|
||||||
* @param labels Список объектов `Label` для отображения.
|
|
||||||
* @param onLabelClick Лямбда-функция, вызываемая при нажатии на элемент списка.
|
|
||||||
* @param modifier Модификатор для настройки внешнего вида.
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
private fun LabelsList(
|
|
||||||
labels: List<Label>,
|
|
||||||
onLabelClick: (Label) -> Unit,
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
LazyColumn(
|
|
||||||
modifier = modifier.fillMaxSize(),
|
|
||||||
contentPadding = PaddingValues(16.dp),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
|
||||||
) {
|
|
||||||
items(labels, key = { it.id }) { label ->
|
|
||||||
LabelListItem(
|
|
||||||
label = label,
|
|
||||||
onClick = { onLabelClick(label) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// [END_ENTITY: Function('LabelsList')]
|
|
||||||
|
|
||||||
// [ENTITY: Function('LabelListItem')]
|
|
||||||
// [RELATION: Function('LabelListItem')] -> [DEPENDS_ON] -> [DataClass('Label')]
|
|
||||||
/**
|
|
||||||
* @summary Composable-функция для отображения одного элемента в списке меток.
|
|
||||||
* @param label Объект `Label`, который нужно отобразить.
|
|
||||||
* @param onClick Лямбда-функция, вызываемая при нажатии на элемент.
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
private fun LabelListItem(
|
|
||||||
label: Label,
|
|
||||||
onClick: () -> Unit
|
|
||||||
) {
|
|
||||||
ListItem(
|
|
||||||
headlineContent = { Text(text = label.name) },
|
|
||||||
leadingContent = {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.AutoMirrored.Filled.Label,
|
|
||||||
contentDescription = stringResource(id = R.string.content_desc_label_icon)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
modifier = Modifier.clickable(onClick = onClick)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
// [END_ENTITY: Function('LabelListItem')]
|
|
||||||
|
|
||||||
// [END_FILE_LabelsListScreen.kt]
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
// [PACKAGE] com.homebox.lens.ui.screen.labelslist
|
|
||||||
// [FILE] LabelsListUiState.kt
|
|
||||||
// [SEMANTICS] ui_state, sealed_interface, contract
|
|
||||||
package com.homebox.lens.ui.screen.labelslist
|
|
||||||
|
|
||||||
// [IMPORTS]
|
|
||||||
import com.homebox.lens.domain.model.Label
|
|
||||||
// [END_IMPORTS]
|
|
||||||
|
|
||||||
// [ENTITY: SealedInterface('LabelsListUiState')]
|
|
||||||
/**
|
|
||||||
* @summary Определяет все возможные состояния для UI экрана со списком меток.
|
|
||||||
* @description Использование sealed-интерфейса позволяет исчерпывающе обрабатывать все состояния в Composable-функциях.
|
|
||||||
*/
|
|
||||||
sealed interface LabelsListUiState {
|
|
||||||
// [ENTITY: DataClass('Success')]
|
|
||||||
// [RELATION: DataClass('Success')] -> [DEPENDS_ON] -> [DataClass('Label')]
|
|
||||||
/**
|
|
||||||
* @summary Состояние успеха, содержит список меток и состояние диалога.
|
|
||||||
* @param labels Список меток для отображения.
|
|
||||||
* @param isShowingCreateDialog Флаг, показывающий, должен ли быть отображен диалог создания метки.
|
|
||||||
* @invariant labels не может быть null.
|
|
||||||
*/
|
|
||||||
data class Success(
|
|
||||||
val labels: List<Label>,
|
|
||||||
val isShowingCreateDialog: Boolean = false
|
|
||||||
) : LabelsListUiState
|
|
||||||
// [END_ENTITY: DataClass('Success')]
|
|
||||||
|
|
||||||
// [ENTITY: DataClass('Error')]
|
|
||||||
/**
|
|
||||||
* @summary Состояние ошибки.
|
|
||||||
* @param message Текст ошибки для отображения пользователю.
|
|
||||||
* @invariant message не может быть пустой.
|
|
||||||
*/
|
|
||||||
data class Error(val message: String) : LabelsListUiState
|
|
||||||
// [END_ENTITY: DataClass('Error')]
|
|
||||||
|
|
||||||
// [ENTITY: Object('Loading')]
|
|
||||||
/**
|
|
||||||
* @summary Состояние загрузки данных.
|
|
||||||
* @description Указывает, что идет процесс загрузки меток.
|
|
||||||
*/
|
|
||||||
data object Loading : LabelsListUiState
|
|
||||||
// [END_ENTITY: Object('Loading')]
|
|
||||||
}
|
|
||||||
// [END_ENTITY: SealedInterface('LabelsListUiState')]
|
|
||||||
// [END_FILE_LabelsListUiState.kt]
|
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
// [PACKAGE] com.homebox.lens.ui.screen.labelslist
|
|
||||||
// [FILE] LabelsListViewModel.kt
|
|
||||||
// [SEMANTICS] ui_logic, labels_list, state_management, dialog_management
|
|
||||||
package com.homebox.lens.ui.screen.labelslist
|
|
||||||
|
|
||||||
// [IMPORTS]
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import com.homebox.lens.domain.model.Label
|
|
||||||
import com.homebox.lens.domain.usecase.GetAllLabelsUseCase
|
|
||||||
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 timber.log.Timber
|
|
||||||
import javax.inject.Inject
|
|
||||||
// [END_IMPORTS]
|
|
||||||
|
|
||||||
// [ENTITY: ViewModel('LabelsListViewModel')]
|
|
||||||
// [RELATION: ViewModel('LabelsListViewModel')] -> [DEPENDS_ON] -> [UseCase('GetAllLabelsUseCase')]
|
|
||||||
// [RELATION: ViewModel('LabelsListViewModel')] -> [EMITS_STATE] -> [SealedInterface('LabelsListUiState')]
|
|
||||||
/**
|
|
||||||
* @summary ViewModel для экрана со списком меток.
|
|
||||||
* @description Управляет состоянием экрана, загружает список меток, обрабатывает ошибки и управляет диалогом создания новой метки.
|
|
||||||
* @invariant `uiState` всегда является одним из состояний, определенных в `LabelsListUiState`.
|
|
||||||
*/
|
|
||||||
@HiltViewModel
|
|
||||||
class LabelsListViewModel @Inject constructor(
|
|
||||||
private val getAllLabelsUseCase: GetAllLabelsUseCase
|
|
||||||
) : ViewModel() {
|
|
||||||
|
|
||||||
private val _uiState = MutableStateFlow<LabelsListUiState>(LabelsListUiState.Loading)
|
|
||||||
val uiState = _uiState.asStateFlow()
|
|
||||||
|
|
||||||
init {
|
|
||||||
loadLabels()
|
|
||||||
}
|
|
||||||
|
|
||||||
// [ENTITY: Function('loadLabels')]
|
|
||||||
/**
|
|
||||||
* @summary Загружает список меток.
|
|
||||||
* @description Выполняет `GetAllLabelsUseCase` и обновляет UI, переключая его
|
|
||||||
* между состояниями `Loading`, `Success` и `Error`.
|
|
||||||
* @sideeffect Асинхронно обновляет `_uiState`.
|
|
||||||
*/
|
|
||||||
fun loadLabels() {
|
|
||||||
viewModelScope.launch {
|
|
||||||
_uiState.value = LabelsListUiState.Loading
|
|
||||||
Timber.i("[INFO][ENTRYPOINT][loading_labels] Starting labels list load. State -> Loading.")
|
|
||||||
|
|
||||||
val result = runCatching {
|
|
||||||
getAllLabelsUseCase()
|
|
||||||
}
|
|
||||||
|
|
||||||
result.fold(
|
|
||||||
onSuccess = { labelOuts ->
|
|
||||||
Timber.i("[INFO][SUCCESS][labels_loaded] Labels loaded successfully. Count: ${labelOuts.size}. State -> Success.")
|
|
||||||
val labels = labelOuts.map { labelOut ->
|
|
||||||
Label(
|
|
||||||
id = labelOut.id,
|
|
||||||
name = labelOut.name
|
|
||||||
)
|
|
||||||
}
|
|
||||||
_uiState.value = LabelsListUiState.Success(labels, isShowingCreateDialog = false)
|
|
||||||
},
|
|
||||||
onFailure = { exception ->
|
|
||||||
Timber.e(exception, "[ERROR][EXCEPTION][loading_failed] Failed to load labels. State -> Error.")
|
|
||||||
_uiState.value = LabelsListUiState.Error(
|
|
||||||
message = exception.message ?: "Could not load labels."
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// [END_ENTITY: Function('loadLabels')]
|
|
||||||
|
|
||||||
// [ENTITY: Function('onShowCreateDialog')]
|
|
||||||
/**
|
|
||||||
* @summary Инициирует отображение диалога для создания метки.
|
|
||||||
* @description Обновляет состояние `uiState`, устанавливая `isShowingCreateDialog` в `true`.
|
|
||||||
* @sideeffect Обновляет `_uiState`.
|
|
||||||
*/
|
|
||||||
fun onShowCreateDialog() {
|
|
||||||
Timber.i("[INFO][ACTION][show_create_dialog] Show create label dialog requested.")
|
|
||||||
if (_uiState.value is LabelsListUiState.Success) {
|
|
||||||
_uiState.update {
|
|
||||||
(it as LabelsListUiState.Success).copy(isShowingCreateDialog = true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// [END_ENTITY: Function('onShowCreateDialog')]
|
|
||||||
|
|
||||||
// [ENTITY: Function('onDismissCreateDialog')]
|
|
||||||
/**
|
|
||||||
* @summary Скрывает диалог создания метки.
|
|
||||||
* @description Обновляет состояние `uiState`, устанавливая `isShowingCreateDialog` в `false`.
|
|
||||||
* @sideeffect Обновляет `_uiState`.
|
|
||||||
*/
|
|
||||||
fun onDismissCreateDialog() {
|
|
||||||
Timber.i("[INFO][ACTION][dismiss_create_dialog] Dismiss create label dialog requested.")
|
|
||||||
if (_uiState.value is LabelsListUiState.Success) {
|
|
||||||
_uiState.update {
|
|
||||||
(it as LabelsListUiState.Success).copy(isShowingCreateDialog = false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// [END_ENTITY: Function('onDismissCreateDialog')]
|
|
||||||
|
|
||||||
// [ENTITY: Function('createLabel')]
|
|
||||||
/**
|
|
||||||
* @summary Создает новую метку. [MVP_SCOPE] ЗАГЛУШКА.
|
|
||||||
* @description В текущей реализации (План Б, Этап 1), эта функция только логирует действие
|
|
||||||
* и скрывает диалог. Реальная логика сохранения будет добавлена на следующем этапе.
|
|
||||||
* @param name Название новой метки.
|
|
||||||
* @precondition `name` не должен быть пустым.
|
|
||||||
* @sideeffect Логирует действие, обновляет `_uiState`, чтобы скрыть диалог.
|
|
||||||
*/
|
|
||||||
fun createLabel(name: String) {
|
|
||||||
require(name.isNotBlank()) { "[CONTRACT_VIOLATION] Label name cannot be blank." }
|
|
||||||
|
|
||||||
Timber.i("[INFO][ACTION][create_label] Create label called with name: '$name'. [STUBBED]")
|
|
||||||
|
|
||||||
// [AI_NOTE]: Здесь будет вызов CreateLabelUseCase.
|
|
||||||
|
|
||||||
onDismissCreateDialog()
|
|
||||||
}
|
|
||||||
// [END_ENTITY: Function('createLabel')]
|
|
||||||
}
|
|
||||||
// [END_ENTITY: ViewModel('LabelsListViewModel')]
|
|
||||||
// [END_FILE_LabelsListViewModel.kt]
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
// [PACKAGE] com.homebox.lens.ui.screen.locationedit
|
|
||||||
// [FILE] LocationEditScreen.kt
|
|
||||||
// [SEMANTICS] ui, screen, location, edit
|
|
||||||
|
|
||||||
package com.homebox.lens.ui.screen.locationedit
|
|
||||||
|
|
||||||
// [IMPORTS]
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.material3.Scaffold
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import com.homebox.lens.R
|
|
||||||
// [END_IMPORTS]
|
|
||||||
|
|
||||||
// [ENTITY: Function('LocationEditScreen')]
|
|
||||||
/**
|
|
||||||
* @summary Composable-функция для экрана "Редактирование местоположения".
|
|
||||||
* @param locationId ID местоположения для редактирования или "new" для создания.
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
fun LocationEditScreen(
|
|
||||||
locationId: String?
|
|
||||||
) {
|
|
||||||
val title = if (locationId == "new") {
|
|
||||||
stringResource(id = R.string.location_edit_title_create)
|
|
||||||
} else {
|
|
||||||
stringResource(id = R.string.location_edit_title_edit)
|
|
||||||
}
|
|
||||||
|
|
||||||
Scaffold { paddingValues ->
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.padding(paddingValues),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
// [AI_NOTE]: Implement Location Edit Screen UI
|
|
||||||
Text(text = "Location Edit Screen for ID: $locationId")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// [END_ENTITY: Function('LocationEditScreen')]
|
|
||||||
// [END_FILE_LocationEditScreen.kt]
|
|
||||||
@@ -1,296 +0,0 @@
|
|||||||
// [PACKAGE] com.homebox.lens.ui.screen.locationslist
|
|
||||||
// [FILE] LocationsListScreen.kt
|
|
||||||
// [SEMANTICS] ui, screen, locations, list
|
|
||||||
|
|
||||||
package com.homebox.lens.ui.screen.locationslist
|
|
||||||
|
|
||||||
// [IMPORTS]
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
|
||||||
import androidx.compose.foundation.lazy.items
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.filled.Add
|
|
||||||
import androidx.compose.material.icons.filled.MoreVert
|
|
||||||
import androidx.compose.material3.Card
|
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
|
||||||
import androidx.compose.material3.DropdownMenu
|
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
|
||||||
import androidx.compose.material3.FloatingActionButton
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
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.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
|
||||||
import com.homebox.lens.R
|
|
||||||
import com.homebox.lens.domain.model.LocationOutCount
|
|
||||||
import com.homebox.lens.navigation.NavigationActions
|
|
||||||
import com.homebox.lens.ui.common.MainScaffold
|
|
||||||
import com.homebox.lens.ui.theme.HomeboxLensTheme
|
|
||||||
// [END_IMPORTS]
|
|
||||||
|
|
||||||
// [ENTITY: Function('LocationsListScreen')]
|
|
||||||
// [RELATION: Function('LocationsListScreen')] -> [DEPENDS_ON] -> [ViewModel('LocationsListViewModel')]
|
|
||||||
// [RELATION: Function('LocationsListScreen')] -> [DEPENDS_ON] -> [Class('NavigationActions')]
|
|
||||||
// [RELATION: Function('LocationsListScreen')] -> [CALLS] -> [Function('MainScaffold')]
|
|
||||||
/**
|
|
||||||
* @summary Composable-функция для экрана "Список местоположений".
|
|
||||||
* @param currentRoute Текущий маршрут для подсветки активного элемента в Drawer.
|
|
||||||
* @param navigationActions Объект с навигационными действиями.
|
|
||||||
* @param onLocationClick Лямбда-обработчик нажатия на местоположение.
|
|
||||||
* @param onAddNewLocationClick Лямбда-обработчик нажатия на кнопку добавления нового местоположения.
|
|
||||||
* @param viewModel ViewModel для этого экрана.
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
fun LocationsListScreen(
|
|
||||||
currentRoute: String?,
|
|
||||||
navigationActions: NavigationActions,
|
|
||||||
onLocationClick: (String) -> Unit,
|
|
||||||
onAddNewLocationClick: () -> Unit,
|
|
||||||
viewModel: LocationsListViewModel = hiltViewModel()
|
|
||||||
) {
|
|
||||||
val uiState by viewModel.uiState.collectAsState()
|
|
||||||
|
|
||||||
MainScaffold(
|
|
||||||
topBarTitle = stringResource(id = R.string.locations_list_title),
|
|
||||||
currentRoute = currentRoute,
|
|
||||||
navigationActions = navigationActions
|
|
||||||
) { paddingValues ->
|
|
||||||
Scaffold(
|
|
||||||
modifier = Modifier.padding(paddingValues),
|
|
||||||
floatingActionButton = {
|
|
||||||
FloatingActionButton(onClick = onAddNewLocationClick) {
|
|
||||||
Icon(
|
|
||||||
Icons.Default.Add,
|
|
||||||
contentDescription = stringResource(id = R.string.cd_add_new_location)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
) { innerPadding ->
|
|
||||||
LocationsListContent(
|
|
||||||
modifier = Modifier.padding(innerPadding),
|
|
||||||
uiState = uiState,
|
|
||||||
onLocationClick = onLocationClick,
|
|
||||||
onEditLocation = { /* [AI_NOTE]: Implement onEditLocation */ },
|
|
||||||
onDeleteLocation = { /* [AI_NOTE]: Implement onDeleteLocation */ }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// [END_ENTITY: Function('LocationsListScreen')]
|
|
||||||
|
|
||||||
// [ENTITY: Function('LocationsListContent')]
|
|
||||||
// [RELATION: Function('LocationsListContent')] -> [CONSUMES_STATE] -> [SealedInterface('LocationsListUiState')]
|
|
||||||
/**
|
|
||||||
* @summary Отображает основной контент экрана в зависимости от `uiState`.
|
|
||||||
* @param modifier Модификатор для стилизации.
|
|
||||||
* @param uiState Текущее состояние UI.
|
|
||||||
* @param onLocationClick Лямбда-обработчик нажатия на местоположение.
|
|
||||||
* @param onEditLocation Лямбда-обработчик для редактирования местоположения.
|
|
||||||
* @param onDeleteLocation Лямбда-обработчик для удаления местоположения.
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
private fun LocationsListContent(
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
uiState: LocationsListUiState,
|
|
||||||
onLocationClick: (String) -> Unit,
|
|
||||||
onEditLocation: (String) -> Unit,
|
|
||||||
onDeleteLocation: (String) -> Unit
|
|
||||||
) {
|
|
||||||
Box(modifier = modifier.fillMaxSize()) {
|
|
||||||
when (uiState) {
|
|
||||||
is LocationsListUiState.Loading -> {
|
|
||||||
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
|
|
||||||
}
|
|
||||||
is LocationsListUiState.Error -> {
|
|
||||||
Text(
|
|
||||||
text = uiState.message,
|
|
||||||
color = MaterialTheme.colorScheme.error,
|
|
||||||
textAlign = TextAlign.Center,
|
|
||||||
modifier = Modifier
|
|
||||||
.align(Alignment.Center)
|
|
||||||
.padding(16.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
is LocationsListUiState.Success -> {
|
|
||||||
if (uiState.locations.isEmpty()) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(id = R.string.locations_not_found),
|
|
||||||
textAlign = TextAlign.Center,
|
|
||||||
modifier = Modifier
|
|
||||||
.align(Alignment.Center)
|
|
||||||
.padding(16.dp)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
LazyColumn(
|
|
||||||
contentPadding = PaddingValues(16.dp),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
|
||||||
) {
|
|
||||||
items(uiState.locations, key = { it.id }) { location ->
|
|
||||||
LocationCard(
|
|
||||||
location = location,
|
|
||||||
onClick = { onLocationClick(location.id) },
|
|
||||||
onEditClick = { onEditLocation(location.id) },
|
|
||||||
onDeleteClick = { onDeleteLocation(location.id) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// [END_ENTITY: Function('LocationsListContent')]
|
|
||||||
|
|
||||||
// [ENTITY: Function('LocationCard')]
|
|
||||||
// [RELATION: Function('LocationCard')] -> [DEPENDS_ON] -> [DataClass('LocationOutCount')]
|
|
||||||
/**
|
|
||||||
* @summary Карточка для отображения одного местоположения.
|
|
||||||
* @param location Данные о местоположении.
|
|
||||||
* @param onClick Лямбда-обработчик нажатия на карточку.
|
|
||||||
* @param onEditClick Лямбда-обработчик нажатия на "Редактировать".
|
|
||||||
* @param onDeleteClick Лямбда-обработчик нажатия на "Удалить".
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
private fun LocationCard(
|
|
||||||
location: LocationOutCount,
|
|
||||||
onClick: () -> Unit,
|
|
||||||
onEditClick: () -> Unit,
|
|
||||||
onDeleteClick: () -> Unit
|
|
||||||
) {
|
|
||||||
var menuExpanded by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
Card(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.clickable(onClick = onClick)
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.padding(start = 16.dp, end = 8.dp, top = 16.dp, bottom = 16.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
|
||||||
) {
|
|
||||||
Column(modifier = Modifier.weight(1f)) {
|
|
||||||
Text(text = location.name, style = MaterialTheme.typography.titleMedium)
|
|
||||||
Text(
|
|
||||||
text = stringResource(id = R.string.item_count, location.itemCount),
|
|
||||||
style = MaterialTheme.typography.bodyMedium
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Spacer(Modifier.width(16.dp))
|
|
||||||
Box {
|
|
||||||
IconButton(onClick = { menuExpanded = true }) {
|
|
||||||
Icon(Icons.Default.MoreVert, contentDescription = stringResource(id = R.string.cd_more_options))
|
|
||||||
}
|
|
||||||
DropdownMenu(
|
|
||||||
expanded = menuExpanded,
|
|
||||||
onDismissRequest = { menuExpanded = false }
|
|
||||||
) {
|
|
||||||
DropdownMenuItem(
|
|
||||||
text = { Text(stringResource(id = R.string.edit)) },
|
|
||||||
onClick = {
|
|
||||||
menuExpanded = false
|
|
||||||
onEditClick()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
DropdownMenuItem(
|
|
||||||
text = { Text(stringResource(id = R.string.delete)) },
|
|
||||||
onClick = {
|
|
||||||
menuExpanded = false
|
|
||||||
onDeleteClick()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// [END_ENTITY: Function('LocationCard')]
|
|
||||||
|
|
||||||
// [ENTITY: Function('LocationsListSuccessPreview')]
|
|
||||||
@Preview(showBackground = true, name = "Locations List Success")
|
|
||||||
@Composable
|
|
||||||
fun LocationsListSuccessPreview() {
|
|
||||||
val previewLocations = listOf(
|
|
||||||
LocationOutCount("1", "Garage", "#FF0000", false, 12, "", ""),
|
|
||||||
LocationOutCount("2", "Kitchen", "#00FF00", false, 5, "", ""),
|
|
||||||
LocationOutCount("3", "Office", "#0000FF", false, 23, "", "")
|
|
||||||
)
|
|
||||||
HomeboxLensTheme {
|
|
||||||
LocationsListContent(
|
|
||||||
uiState = LocationsListUiState.Success(previewLocations),
|
|
||||||
onLocationClick = {},
|
|
||||||
onEditLocation = {},
|
|
||||||
onDeleteLocation = {}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// [END_ENTITY: Function('LocationsListSuccessPreview')]
|
|
||||||
|
|
||||||
// [ENTITY: Function('LocationsListEmptyPreview')]
|
|
||||||
@Preview(showBackground = true, name = "Locations List Empty")
|
|
||||||
@Composable
|
|
||||||
fun LocationsListEmptyPreview() {
|
|
||||||
HomeboxLensTheme {
|
|
||||||
LocationsListContent(
|
|
||||||
uiState = LocationsListUiState.Success(emptyList()),
|
|
||||||
onLocationClick = {},
|
|
||||||
onEditLocation = {},
|
|
||||||
onDeleteLocation = {}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// [END_ENTITY: Function('LocationsListEmptyPreview')]
|
|
||||||
|
|
||||||
// [ENTITY: Function('LocationsListLoadingPreview')]
|
|
||||||
@Preview(showBackground = true, name = "Locations List Loading")
|
|
||||||
@Composable
|
|
||||||
fun LocationsListLoadingPreview() {
|
|
||||||
HomeboxLensTheme {
|
|
||||||
LocationsListContent(
|
|
||||||
uiState = LocationsListUiState.Loading,
|
|
||||||
onLocationClick = {},
|
|
||||||
onEditLocation = {},
|
|
||||||
onDeleteLocation = {}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// [END_ENTITY: Function('LocationsListLoadingPreview')]
|
|
||||||
|
|
||||||
// [ENTITY: Function('LocationsListErrorPreview')]
|
|
||||||
@Preview(showBackground = true, name = "Locations List Error")
|
|
||||||
@Composable
|
|
||||||
fun LocationsListErrorPreview() {
|
|
||||||
HomeboxLensTheme {
|
|
||||||
LocationsListContent(
|
|
||||||
uiState = LocationsListUiState.Error("Failed to load locations. Please try again."),
|
|
||||||
onLocationClick = {},
|
|
||||||
onEditLocation = {},
|
|
||||||
onDeleteLocation = {}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// [END_ENTITY: Function('LocationsListErrorPreview')]
|
|
||||||
// [END_FILE_LocationsListScreen.kt]
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
// [PACKAGE] com.homebox.lens.ui.screen.locationslist
|
|
||||||
// [FILE] LocationsListUiState.kt
|
|
||||||
// [SEMANTICS] ui, state, locations
|
|
||||||
|
|
||||||
package com.homebox.lens.ui.screen.locationslist
|
|
||||||
|
|
||||||
// [IMPORTS]
|
|
||||||
import com.homebox.lens.domain.model.LocationOutCount
|
|
||||||
// [END_IMPORTS]
|
|
||||||
|
|
||||||
// [ENTITY: SealedInterface('LocationsListUiState')]
|
|
||||||
/**
|
|
||||||
* @summary Определяет возможные состояния UI для экрана списка местоположений.
|
|
||||||
* @see LocationsListViewModel
|
|
||||||
*/
|
|
||||||
sealed interface LocationsListUiState {
|
|
||||||
// [ENTITY: DataClass('Success')]
|
|
||||||
// [RELATION: DataClass('Success')] -> [DEPENDS_ON] -> [DataClass('LocationOutCount')]
|
|
||||||
/**
|
|
||||||
* @summary Состояние успешной загрузки данных.
|
|
||||||
* @param locations Список местоположений для отображения.
|
|
||||||
*/
|
|
||||||
data class Success(val locations: List<LocationOutCount>) : LocationsListUiState
|
|
||||||
// [END_ENTITY: DataClass('Success')]
|
|
||||||
|
|
||||||
// [ENTITY: DataClass('Error')]
|
|
||||||
/**
|
|
||||||
* @summary Состояние ошибки.
|
|
||||||
* @param message Сообщение об ошибке.
|
|
||||||
*/
|
|
||||||
data class Error(val message: String) : LocationsListUiState
|
|
||||||
// [END_ENTITY: DataClass('Error')]
|
|
||||||
|
|
||||||
// [ENTITY: Object('Loading')]
|
|
||||||
/**
|
|
||||||
* @summary Состояние загрузки данных.
|
|
||||||
*/
|
|
||||||
object Loading : LocationsListUiState
|
|
||||||
// [END_ENTITY: Object('Loading')]
|
|
||||||
}
|
|
||||||
// [END_ENTITY: SealedInterface('LocationsListUiState')]
|
|
||||||
// [END_FILE_LocationsListUiState.kt]
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
// [PACKAGE] com.homebox.lens.ui.screen.locationslist
|
|
||||||
// [FILE] LocationsListViewModel.kt
|
|
||||||
// [SEMANTICS] ui, viewmodel, locations, hilt
|
|
||||||
|
|
||||||
package com.homebox.lens.ui.screen.locationslist
|
|
||||||
|
|
||||||
// [IMPORTS]
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import com.homebox.lens.domain.usecase.GetAllLocationsUseCase
|
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import timber.log.Timber
|
|
||||||
import javax.inject.Inject
|
|
||||||
// [END_IMPORTS]
|
|
||||||
|
|
||||||
// [ENTITY: ViewModel('LocationsListViewModel')]
|
|
||||||
// [RELATION: ViewModel('LocationsListViewModel')] -> [DEPENDS_ON] -> [UseCase('GetAllLocationsUseCase')]
|
|
||||||
// [RELATION: ViewModel('LocationsListViewModel')] -> [EMITS_STATE] -> [SealedInterface('LocationsListUiState')]
|
|
||||||
/**
|
|
||||||
* @summary ViewModel для экрана списка местоположений.
|
|
||||||
* @param getAllLocationsUseCase Use case для получения всех местоположений.
|
|
||||||
* @property uiState Поток, содержащий текущее состояние UI.
|
|
||||||
* @invariant `uiState` всегда отражает результат последней операции загрузки.
|
|
||||||
*/
|
|
||||||
@HiltViewModel
|
|
||||||
class LocationsListViewModel @Inject constructor(
|
|
||||||
private val getAllLocationsUseCase: GetAllLocationsUseCase
|
|
||||||
) : ViewModel() {
|
|
||||||
|
|
||||||
private val _uiState = MutableStateFlow<LocationsListUiState>(LocationsListUiState.Loading)
|
|
||||||
val uiState: StateFlow<LocationsListUiState> = _uiState.asStateFlow()
|
|
||||||
|
|
||||||
init {
|
|
||||||
loadLocations()
|
|
||||||
}
|
|
||||||
|
|
||||||
// [ENTITY: Function('loadLocations')]
|
|
||||||
/**
|
|
||||||
* @summary Загружает список местоположений из репозитория.
|
|
||||||
* @sideeffect Обновляет `_uiState` в зависимости от результата: Loading -> Success/Error.
|
|
||||||
*/
|
|
||||||
fun loadLocations() {
|
|
||||||
Timber.d("[DEBUG][ENTRYPOINT][loading_locations] Starting to load locations.")
|
|
||||||
viewModelScope.launch {
|
|
||||||
_uiState.value = LocationsListUiState.Loading
|
|
||||||
try {
|
|
||||||
Timber.d("[DEBUG][ACTION][fetching_locations] Fetching locations from use case.")
|
|
||||||
val locations = getAllLocationsUseCase()
|
|
||||||
_uiState.value = LocationsListUiState.Success(locations)
|
|
||||||
Timber.d("[DEBUG][SUCCESS][locations_loaded] Successfully loaded locations.")
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.e(e, "[ERROR][EXCEPTION][loading_failed] Failed to load locations.")
|
|
||||||
_uiState.value = LocationsListUiState.Error(e.message ?: "Unknown error")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// [END_ENTITY: Function('loadLocations')]
|
|
||||||
}
|
|
||||||
// [END_ENTITY: ViewModel('LocationsListViewModel')]
|
|
||||||
// [END_FILE_LocationsListViewModel.kt]
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
// [PACKAGE] com.homebox.lens.ui.screen.search
|
|
||||||
// [FILE] SearchScreen.kt
|
|
||||||
// [SEMANTICS] ui, screen, search
|
|
||||||
|
|
||||||
package com.homebox.lens.ui.screen.search
|
|
||||||
|
|
||||||
// [IMPORTS]
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import com.homebox.lens.R
|
|
||||||
import com.homebox.lens.navigation.NavigationActions
|
|
||||||
import com.homebox.lens.ui.common.MainScaffold
|
|
||||||
// [END_IMPORTS]
|
|
||||||
|
|
||||||
// [ENTITY: Function('SearchScreen')]
|
|
||||||
// [RELATION: Function('SearchScreen')] -> [DEPENDS_ON] -> [Class('NavigationActions')]
|
|
||||||
// [RELATION: Function('SearchScreen')] -> [CALLS] -> [Function('MainScaffold')]
|
|
||||||
/**
|
|
||||||
* @summary Composable-функция для экрана "Поиск".
|
|
||||||
* @param currentRoute Текущий маршрут для подсветки активного элемента в Drawer.
|
|
||||||
* @param navigationActions Объект с навигационными действиями.
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
fun SearchScreen(
|
|
||||||
currentRoute: String?,
|
|
||||||
navigationActions: NavigationActions
|
|
||||||
) {
|
|
||||||
MainScaffold(
|
|
||||||
topBarTitle = stringResource(id = R.string.search_title),
|
|
||||||
currentRoute = currentRoute,
|
|
||||||
navigationActions = navigationActions
|
|
||||||
) {
|
|
||||||
// [AI_NOTE]: Implement Search Screen UI
|
|
||||||
Text(text = "Search Screen")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// [END_ENTITY: Function('SearchScreen')]
|
|
||||||
// [END_FILE_SearchScreen.kt]
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
// [PACKAGE] com.homebox.lens.ui.screen.search
|
|
||||||
// [FILE] SearchViewModel.kt
|
|
||||||
// [SEMANTICS] ui, viewmodel, search
|
|
||||||
package com.homebox.lens.ui.screen.search
|
|
||||||
|
|
||||||
// [IMPORTS]
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
|
||||||
import javax.inject.Inject
|
|
||||||
// [END_IMPORTS]
|
|
||||||
|
|
||||||
// [ENTITY: ViewModel('SearchViewModel')]
|
|
||||||
/**
|
|
||||||
* @summary ViewModel for the search screen.
|
|
||||||
*/
|
|
||||||
@HiltViewModel
|
|
||||||
class SearchViewModel @Inject constructor() : ViewModel() {
|
|
||||||
// [AI_NOTE]: Implement UI state
|
|
||||||
}
|
|
||||||
// [END_ENTITY: ViewModel('SearchViewModel')]
|
|
||||||
// [END_FILE_SearchViewModel.kt]
|
|
||||||
@@ -1,141 +0,0 @@
|
|||||||
// [PACKAGE] com.homebox.lens.ui.screen.setup
|
|
||||||
// [FILE] SetupScreen.kt
|
|
||||||
// [SEMANTICS] ui, screen, setup, compose
|
|
||||||
|
|
||||||
@file:OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
|
|
||||||
package com.homebox.lens.ui.screen.setup
|
|
||||||
|
|
||||||
// [IMPORTS]
|
|
||||||
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.res.stringResource
|
|
||||||
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
|
|
||||||
import com.homebox.lens.R
|
|
||||||
// [END_IMPORTS]
|
|
||||||
|
|
||||||
// [ENTITY: Function('SetupScreen')]
|
|
||||||
// [RELATION: Function('SetupScreen')] -> [DEPENDS_ON] -> [ViewModel('SetupViewModel')]
|
|
||||||
// [RELATION: Function('SetupScreen')] -> [CALLS] -> [Function('SetupScreenContent')]
|
|
||||||
/**
|
|
||||||
* @summary Главная Composable-функция для экрана настройки соединения с сервером.
|
|
||||||
* @param viewModel ViewModel для этого экрана, предоставляется через Hilt.
|
|
||||||
* @param onSetupComplete Лямбда, вызываемая после успешной настройки и входа.
|
|
||||||
* @sideeffect Вызывает `onSetupComplete` при изменении `uiState.isSetupComplete`.
|
|
||||||
*/
|
|
||||||
@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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
// [END_ENTITY: Function('SetupScreen')]
|
|
||||||
|
|
||||||
// [ENTITY: Function('SetupScreenContent')]
|
|
||||||
// [RELATION: Function('SetupScreenContent')] -> [CONSUMES_STATE] -> [DataClass('SetupUiState')]
|
|
||||||
/**
|
|
||||||
* @summary Отображает контент экрана настройки: поля ввода и кнопку.
|
|
||||||
* @param uiState Текущее состояние UI.
|
|
||||||
* @param onServerUrlChange Лямбда-обработчик изменения URL сервера.
|
|
||||||
* @param onUsernameChange Лямбда-обработчик изменения имени пользователя.
|
|
||||||
* @param onPasswordChange Лямбда-обработчик изменения пароля.
|
|
||||||
* @param onConnectClick Лямбда-обработчик нажатия на кнопку "Подключиться".
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
private fun SetupScreenContent(
|
|
||||||
uiState: SetupUiState,
|
|
||||||
onServerUrlChange: (String) -> Unit,
|
|
||||||
onUsernameChange: (String) -> Unit,
|
|
||||||
onPasswordChange: (String) -> Unit,
|
|
||||||
onConnectClick: () -> Unit
|
|
||||||
) {
|
|
||||||
Scaffold(
|
|
||||||
topBar = {
|
|
||||||
TopAppBar(title = { Text(stringResource(id = R.string.setup_title)) })
|
|
||||||
}
|
|
||||||
) { paddingValues ->
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.padding(paddingValues)
|
|
||||||
.padding(16.dp),
|
|
||||||
verticalArrangement = Arrangement.Center,
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
|
||||||
) {
|
|
||||||
OutlinedTextField(
|
|
||||||
value = uiState.serverUrl,
|
|
||||||
onValueChange = onServerUrlChange,
|
|
||||||
label = { Text(stringResource(id = R.string.setup_server_url_label)) },
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
OutlinedTextField(
|
|
||||||
value = uiState.username,
|
|
||||||
onValueChange = onUsernameChange,
|
|
||||||
label = { Text(stringResource(id = R.string.setup_username_label)) },
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
OutlinedTextField(
|
|
||||||
value = uiState.password,
|
|
||||||
onValueChange = onPasswordChange,
|
|
||||||
label = { Text(stringResource(id = R.string.setup_password_label)) },
|
|
||||||
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(stringResource(id = R.string.setup_connect_button))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
uiState.error?.let {
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
Text(text = it, color = MaterialTheme.colorScheme.error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// [END_ENTITY: Function('SetupScreenContent')]
|
|
||||||
|
|
||||||
// [ENTITY: Function('SetupScreenPreview')]
|
|
||||||
@Preview(showBackground = true)
|
|
||||||
@Composable
|
|
||||||
fun SetupScreenPreview() {
|
|
||||||
SetupScreenContent(
|
|
||||||
uiState = SetupUiState(error = "Failed to connect"),
|
|
||||||
onServerUrlChange = {},
|
|
||||||
onUsernameChange = {},
|
|
||||||
onPasswordChange = {},
|
|
||||||
onConnectClick = {}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
// [END_ENTITY: Function('SetupScreenPreview')]
|
|
||||||
// [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')]
|
|
||||||
/**
|
|
||||||
* @summary Неизменяемая модель данных, представляющая полное состояние экрана настройки (Setup Screen).
|
|
||||||
* @description Использование `data class` предоставляет метод `copy()` для легкого создания новых состояний.
|
|
||||||
* @param serverUrl URL-адрес сервера Homebox.
|
|
||||||
* @param username Имя пользователя для входа.
|
|
||||||
* @param password Пароль пользователя.
|
|
||||||
* @param isLoading Флаг, указывающий, выполняется ли в данный момент сетевой запрос.
|
|
||||||
* @param error Сообщение об ошибке для отображения пользователю, или `null` при отсутствии ошибки.
|
|
||||||
* @param 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_ENTITY: DataClass('SetupUiState')]
|
|
||||||
// [END_FILE_SetupUiState.kt]
|
|
||||||
@@ -1,113 +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 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 timber.log.Timber
|
|
||||||
import javax.inject.Inject
|
|
||||||
// [END_IMPORTS]
|
|
||||||
|
|
||||||
// [ENTITY: ViewModel('SetupViewModel')]
|
|
||||||
// [RELATION: ViewModel('SetupViewModel')] -> [DEPENDS_ON] -> [Repository('CredentialsRepository')]
|
|
||||||
// [RELATION: ViewModel('SetupViewModel')] -> [DEPENDS_ON] -> [UseCase('LoginUseCase')]
|
|
||||||
// [RELATION: ViewModel('SetupViewModel')] -> [EMITS_STATE] -> [DataClass('SetupUiState')]
|
|
||||||
/**
|
|
||||||
* @summary ViewModel для экрана первоначальной настройки (Setup).
|
|
||||||
* @param credentialsRepository Репозиторий для операций с учетными данными.
|
|
||||||
* @param loginUseCase Use case для выполнения логики входа.
|
|
||||||
* @invariant Состояние `uiState` всегда является единственным источником истины для UI.
|
|
||||||
*/
|
|
||||||
@HiltViewModel
|
|
||||||
class SetupViewModel @Inject constructor(
|
|
||||||
private val credentialsRepository: CredentialsRepository,
|
|
||||||
private val loginUseCase: LoginUseCase
|
|
||||||
) : ViewModel() {
|
|
||||||
|
|
||||||
private val _uiState = MutableStateFlow(SetupUiState())
|
|
||||||
val uiState = _uiState.asStateFlow()
|
|
||||||
|
|
||||||
init {
|
|
||||||
loadCredentials()
|
|
||||||
}
|
|
||||||
|
|
||||||
// [ENTITY: Function('loadCredentials')]
|
|
||||||
private fun loadCredentials() {
|
|
||||||
Timber.d("[DEBUG][ENTRYPOINT][loading_credentials] Loading credentials from repository.")
|
|
||||||
viewModelScope.launch {
|
|
||||||
credentialsRepository.getCredentials().collect { credentials ->
|
|
||||||
if (credentials != null) {
|
|
||||||
Timber.d("[DEBUG][ACTION][updating_state] Credentials found, updating UI state.")
|
|
||||||
_uiState.update {
|
|
||||||
it.copy(
|
|
||||||
serverUrl = credentials.serverUrl,
|
|
||||||
username = credentials.username,
|
|
||||||
password = credentials.password
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// [END_ENTITY: Function('loadCredentials')]
|
|
||||||
|
|
||||||
// [ENTITY: Function('onServerUrlChange')]
|
|
||||||
fun onServerUrlChange(newUrl: String) {
|
|
||||||
_uiState.update { it.copy(serverUrl = newUrl) }
|
|
||||||
}
|
|
||||||
// [END_ENTITY: Function('onServerUrlChange')]
|
|
||||||
|
|
||||||
// [ENTITY: Function('onUsernameChange')]
|
|
||||||
fun onUsernameChange(newUsername: String) {
|
|
||||||
_uiState.update { it.copy(username = newUsername) }
|
|
||||||
}
|
|
||||||
// [END_ENTITY: Function('onUsernameChange')]
|
|
||||||
|
|
||||||
// [ENTITY: Function('onPasswordChange')]
|
|
||||||
fun onPasswordChange(newPassword: String) {
|
|
||||||
_uiState.update { it.copy(password = newPassword) }
|
|
||||||
}
|
|
||||||
// [END_ENTITY: Function('onPasswordChange')]
|
|
||||||
|
|
||||||
// [ENTITY: Function('connect')]
|
|
||||||
fun connect() {
|
|
||||||
Timber.d("[DEBUG][ENTRYPOINT][connecting] Starting connection process.")
|
|
||||||
viewModelScope.launch {
|
|
||||||
_uiState.update { it.copy(isLoading = true, error = null) }
|
|
||||||
|
|
||||||
val credentials = Credentials(
|
|
||||||
serverUrl = _uiState.value.serverUrl.trim(),
|
|
||||||
username = _uiState.value.username.trim(),
|
|
||||||
password = _uiState.value.password
|
|
||||||
)
|
|
||||||
|
|
||||||
Timber.d("[DEBUG][ACTION][saving_credentials] Saving credentials.")
|
|
||||||
credentialsRepository.saveCredentials(credentials)
|
|
||||||
|
|
||||||
Timber.d("[DEBUG][ACTION][executing_login] Executing login use case.")
|
|
||||||
loginUseCase(credentials).fold(
|
|
||||||
onSuccess = {
|
|
||||||
Timber.d("[DEBUG][SUCCESS][login_successful] Login successful.")
|
|
||||||
_uiState.update { it.copy(isLoading = false, isSetupComplete = true) }
|
|
||||||
},
|
|
||||||
onFailure = { exception ->
|
|
||||||
Timber.e(exception, "[ERROR][EXCEPTION][login_failed] Login failed.")
|
|
||||||
_uiState.update { it.copy(isLoading = false, error = exception.message ?: "Login failed") }
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// [END_ENTITY: Function('connect')]
|
|
||||||
}
|
|
||||||
// [END_ENTITY: ViewModel('SetupViewModel')]
|
|
||||||
// [END_FILE_SetupViewModel.kt]
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
// [PACKAGE] com.homebox.lens.ui.theme
|
|
||||||
// [FILE] Theme.kt
|
|
||||||
// [SEMANTICS] ui, theme
|
|
||||||
package com.homebox.lens.ui.theme
|
|
||||||
|
|
||||||
// [IMPORTS]
|
|
||||||
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
|
|
||||||
// [END_IMPORTS]
|
|
||||||
|
|
||||||
private val DarkColorScheme = darkColorScheme(
|
|
||||||
primary = Purple80,
|
|
||||||
secondary = PurpleGrey80,
|
|
||||||
tertiary = Pink80
|
|
||||||
)
|
|
||||||
|
|
||||||
private val LightColorScheme = lightColorScheme(
|
|
||||||
primary = Purple40,
|
|
||||||
secondary = PurpleGrey40,
|
|
||||||
tertiary = Pink40
|
|
||||||
)
|
|
||||||
|
|
||||||
// [ENTITY: Function('HomeboxLensTheme')]
|
|
||||||
// [RELATION: Function('HomeboxLensTheme')] -> [DEPENDS_ON] -> [DataStructure('Typography')]
|
|
||||||
/**
|
|
||||||
* @summary The main theme for the Homebox Lens application.
|
|
||||||
* @param darkTheme Whether the theme should be dark or light.
|
|
||||||
* @param dynamicColor Whether to use dynamic color (on Android 12+).
|
|
||||||
* @param content The content to be displayed within the theme.
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
fun HomeboxLensTheme(
|
|
||||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
|
||||||
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_ENTITY: Function('HomeboxLensTheme')]
|
|
||||||
// [END_FILE_Theme.kt]
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
// [PACKAGE] com.homebox.lens.ui.theme
|
|
||||||
// [FILE] Typography.kt
|
|
||||||
// [SEMANTICS] ui, theme, typography
|
|
||||||
package com.homebox.lens.ui.theme
|
|
||||||
|
|
||||||
// [IMPORTS]
|
|
||||||
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
|
|
||||||
// [END_IMPORTS]
|
|
||||||
|
|
||||||
// [ENTITY: DataStructure('Typography')]
|
|
||||||
/**
|
|
||||||
* @summary Defines the typography for the application.
|
|
||||||
*/
|
|
||||||
val Typography = Typography(
|
|
||||||
bodyLarge = TextStyle(
|
|
||||||
fontFamily = FontFamily.Default,
|
|
||||||
fontWeight = FontWeight.Normal,
|
|
||||||
fontSize = 16.sp,
|
|
||||||
lineHeight = 24.sp,
|
|
||||||
letterSpacing = 0.5.sp
|
|
||||||
)
|
|
||||||
)
|
|
||||||
// [END_ENTITY: DataStructure('Typography')]
|
|
||||||
|
|
||||||
// [END_FILE_Typography.kt]
|
|
||||||
@@ -14,7 +14,9 @@
|
|||||||
<!-- Content Descriptions -->
|
<!-- Content Descriptions -->
|
||||||
<string name="cd_open_navigation_drawer">Open navigation drawer</string>
|
<string name="cd_open_navigation_drawer">Open navigation drawer</string>
|
||||||
<string name="cd_scan_qr_code">Scan QR code</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_back">Navigate back</string>
|
||||||
|
<string name="cd_navigate_up">Go back</string>
|
||||||
<string name="cd_add_new_location">Add new location</string>
|
<string name="cd_add_new_location">Add new location</string>
|
||||||
<string name="content_desc_add_label">Add new label</string>
|
<string name="content_desc_add_label">Add new label</string>
|
||||||
|
|
||||||
@@ -72,6 +74,7 @@
|
|||||||
<string name="content_desc_navigate_back">Navigate back</string>
|
<string name="content_desc_navigate_back">Navigate back</string>
|
||||||
<string name="content_desc_create_label">Create new label</string>
|
<string name="content_desc_create_label">Create new label</string>
|
||||||
<string name="content_desc_label_icon">Label icon</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="no_labels_found">No labels found.</string>
|
||||||
<string name="dialog_title_create_label">Create Label</string>
|
<string name="dialog_title_create_label">Create Label</string>
|
||||||
<string name="dialog_field_label_name">Label Name</string>
|
<string name="dialog_field_label_name">Label Name</string>
|
||||||
@@ -118,4 +121,26 @@
|
|||||||
<string name="label_color">Color</string>
|
<string name="label_color">Color</string>
|
||||||
<string name="label_hex_color">HEX color code</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>
|
</resources>
|
||||||
@@ -13,8 +13,10 @@
|
|||||||
|
|
||||||
<!-- Content Descriptions -->
|
<!-- Content Descriptions -->
|
||||||
<string name="cd_open_navigation_drawer">Открыть боковое меню</string>
|
<string name="cd_open_navigation_drawer">Открыть боковое меню</string>
|
||||||
<string name="cd_scan_qr_code">Сканировать QR-код</string>
|
<string name="cd_scan_qr_code">Сканировать QR/штрих-код</string>
|
||||||
|
<string name="cd_search">Поиск</string>
|
||||||
<string name="cd_navigate_back">Вернуться назад</string>
|
<string name="cd_navigate_back">Вернуться назад</string>
|
||||||
|
<string name="cd_navigate_up">Вернуться</string>
|
||||||
<string name="cd_add_new_location">Добавить новую локацию</string>
|
<string name="cd_add_new_location">Добавить новую локацию</string>
|
||||||
<string name="content_desc_add_label">Добавить новую метку</string>
|
<string name="content_desc_add_label">Добавить новую метку</string>
|
||||||
|
|
||||||
@@ -93,6 +95,7 @@
|
|||||||
<string name="content_desc_navigate_back" translatable="false">Вернуться назад</string>
|
<string name="content_desc_navigate_back" translatable="false">Вернуться назад</string>
|
||||||
<string name="content_desc_create_label">Создать новую метку</string>
|
<string name="content_desc_create_label">Создать новую метку</string>
|
||||||
<string name="content_desc_label_icon">Иконка метки</string>
|
<string name="content_desc_label_icon">Иконка метки</string>
|
||||||
|
<string name="content_desc_delete_label">Удалить метку</string>
|
||||||
<string name="no_labels_found">Метки не найдены.</string>
|
<string name="no_labels_found">Метки не найдены.</string>
|
||||||
<string name="dialog_title_create_label">Создать метку</string>
|
<string name="dialog_title_create_label">Создать метку</string>
|
||||||
<string name="dialog_field_label_name">Название метки</string>
|
<string name="dialog_field_label_name">Название метки</string>
|
||||||
@@ -112,4 +115,26 @@
|
|||||||
<!-- Color Picker -->
|
<!-- Color Picker -->
|
||||||
<string name="label_color">Цвет</string>
|
<string name="label_color">Цвет</string>
|
||||||
<string name="label_hex_color">HEX-код цвета</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>
|
</resources>
|
||||||
|
|||||||
@@ -1,129 +0,0 @@
|
|||||||
// [PACKAGE] com.homebox.lens.ui.screen.itemedit
|
|
||||||
// [FILE] ItemEditViewModelTest.kt
|
|
||||||
// [SEMANTICS] ui, viewmodel, testing
|
|
||||||
|
|
||||||
package com.homebox.lens.ui.screen.itemedit
|
|
||||||
|
|
||||||
import app.cash.turbine.test
|
|
||||||
import com.homebox.lens.domain.model.Item
|
|
||||||
import com.homebox.lens.domain.model.ItemCreate
|
|
||||||
import com.homebox.lens.domain.model.ItemOut
|
|
||||||
import com.homebox.lens.domain.model.ItemSummary
|
|
||||||
import com.homebox.lens.domain.usecase.CreateItemUseCase
|
|
||||||
import com.homebox.lens.domain.usecase.GetItemDetailsUseCase
|
|
||||||
import com.homebox.lens.domain.usecase.UpdateItemUseCase
|
|
||||||
import io.mockk.coEvery
|
|
||||||
import io.mockk.mockk
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|
||||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
|
||||||
import kotlinx.coroutines.test.resetMain
|
|
||||||
import kotlinx.coroutines.test.runTest
|
|
||||||
import kotlinx.coroutines.test.setMain
|
|
||||||
import org.junit.After
|
|
||||||
import org.junit.Assert.assertEquals
|
|
||||||
import org.junit.Assert.assertFalse
|
|
||||||
import org.junit.Assert.assertNotNull
|
|
||||||
import org.junit.Assert.assertTrue
|
|
||||||
import org.junit.Before
|
|
||||||
import org.junit.Test
|
|
||||||
import java.util.UUID
|
|
||||||
|
|
||||||
@ExperimentalCoroutinesApi
|
|
||||||
class ItemEditViewModelTest {
|
|
||||||
|
|
||||||
private val testDispatcher = StandardTestDispatcher()
|
|
||||||
|
|
||||||
private lateinit var createItemUseCase: CreateItemUseCase
|
|
||||||
private lateinit var updateItemUseCase: UpdateItemUseCase
|
|
||||||
private lateinit var getItemDetailsUseCase: GetItemDetailsUseCase
|
|
||||||
private lateinit var viewModel: ItemEditViewModel
|
|
||||||
|
|
||||||
@Before
|
|
||||||
fun setUp() {
|
|
||||||
Dispatchers.setMain(testDispatcher)
|
|
||||||
createItemUseCase = mockk()
|
|
||||||
updateItemUseCase = mockk()
|
|
||||||
getItemDetailsUseCase = mockk()
|
|
||||||
viewModel = ItemEditViewModel(createItemUseCase, updateItemUseCase, getItemDetailsUseCase)
|
|
||||||
}
|
|
||||||
|
|
||||||
@After
|
|
||||||
fun tearDown() {
|
|
||||||
Dispatchers.resetMain()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `loadItem with valid id should update uiState with item`() = runTest {
|
|
||||||
val itemId = UUID.randomUUID().toString()
|
|
||||||
val itemOut = ItemOut(id = itemId, name = "Test Item", description = "Description", quantity = 1, images = emptyList(), location = null, labels = emptyList(), value = 10.0, createdAt = "2025-08-28T12:00:00Z", assetId = null, notes = null, serialNumber = null, isArchived = false, purchasePrice = null, purchaseDate = null, warrantyUntil = null, parent = null, children = emptyList(), attachments = emptyList(), fields = emptyList(), maintenance = emptyList(), updatedAt = "2025-08-28T12:00:00Z")
|
|
||||||
coEvery { getItemDetailsUseCase(itemId) } returns itemOut
|
|
||||||
|
|
||||||
viewModel.loadItem(itemId)
|
|
||||||
testDispatcher.scheduler.advanceUntilIdle()
|
|
||||||
|
|
||||||
val uiState = viewModel.uiState.value
|
|
||||||
assertFalse(uiState.isLoading)
|
|
||||||
assertNotNull(uiState.item)
|
|
||||||
assertEquals(itemId, uiState.item?.id)
|
|
||||||
assertEquals("Test Item", uiState.item?.name)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `loadItem with null id should prepare a new item`() = runTest {
|
|
||||||
viewModel.loadItem(null)
|
|
||||||
testDispatcher.scheduler.advanceUntilIdle()
|
|
||||||
|
|
||||||
val uiState = viewModel.uiState.value
|
|
||||||
assertFalse(uiState.isLoading)
|
|
||||||
assertNotNull(uiState.item)
|
|
||||||
assertEquals("", uiState.item?.id)
|
|
||||||
assertEquals("", uiState.item?.name)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `saveItem should call createItemUseCase for new item`() = runTest {
|
|
||||||
val createdItemSummary = ItemSummary(id = UUID.randomUUID().toString(), name = "New Item", assetId = null, image = null, isArchived = false, labels = emptyList(), location = null, value = 0.0, createdAt = "2025-08-28T12:00:00Z", updatedAt = "2025-08-28T12:00:00Z")
|
|
||||||
coEvery { createItemUseCase(any()) } returns createdItemSummary
|
|
||||||
|
|
||||||
viewModel.loadItem(null)
|
|
||||||
testDispatcher.scheduler.advanceUntilIdle()
|
|
||||||
viewModel.updateName("New Item")
|
|
||||||
viewModel.updateDescription("New Description")
|
|
||||||
viewModel.updateQuantity(2)
|
|
||||||
testDispatcher.scheduler.advanceUntilIdle()
|
|
||||||
|
|
||||||
viewModel.saveItem()
|
|
||||||
testDispatcher.scheduler.advanceUntilIdle()
|
|
||||||
|
|
||||||
val uiState = viewModel.uiState.value
|
|
||||||
assertFalse(uiState.isLoading)
|
|
||||||
assertNotNull(uiState.item)
|
|
||||||
assertEquals(createdItemSummary.id, uiState.item?.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `saveItem should call updateItemUseCase for existing item`() = runTest {
|
|
||||||
val itemId = UUID.randomUUID().toString()
|
|
||||||
val updatedItemOut = ItemOut(id = itemId, name = "Updated Item", description = "Updated Description", quantity = 4, images = emptyList(), location = null, labels = emptyList(), value = 12.0, createdAt = "2025-08-28T12:00:00Z", assetId = null, notes = null, serialNumber = null, isArchived = false, purchasePrice = null, purchaseDate = null, warrantyUntil = null, parent = null, children = emptyList(), attachments = emptyList(), fields = emptyList(), maintenance = emptyList(), updatedAt = "2025-08-28T12:00:00Z")
|
|
||||||
coEvery { getItemDetailsUseCase(itemId) } returns ItemOut(id = itemId, name = "Existing Item", description = "Existing Description", quantity = 3, images = emptyList(), location = null, labels = emptyList(), value = 10.0, createdAt = "2025-08-28T12:00:00Z", assetId = null, notes = null, serialNumber = null, isArchived = false, purchasePrice = null, purchaseDate = null, warrantyUntil = null, parent = null, children = emptyList(), attachments = emptyList(), fields = emptyList(), maintenance = emptyList(), updatedAt = "2025-08-28T12:00:00Z")
|
|
||||||
coEvery { updateItemUseCase(any()) } returns updatedItemOut
|
|
||||||
|
|
||||||
viewModel.loadItem(itemId)
|
|
||||||
testDispatcher.scheduler.advanceUntilIdle()
|
|
||||||
viewModel.updateName("Updated Item")
|
|
||||||
viewModel.updateDescription("Updated Description")
|
|
||||||
viewModel.updateQuantity(4)
|
|
||||||
testDispatcher.scheduler.advanceUntilIdle()
|
|
||||||
|
|
||||||
viewModel.saveItem()
|
|
||||||
testDispatcher.scheduler.advanceUntilIdle()
|
|
||||||
|
|
||||||
val uiState = viewModel.uiState.value
|
|
||||||
assertFalse(uiState.isLoading)
|
|
||||||
assertNotNull(uiState.item)
|
|
||||||
assertEquals(itemId, uiState.item?.id)
|
|
||||||
assertEquals("Updated Item", uiState.item?.name)
|
|
||||||
assertEquals(4, uiState.item?.quantity)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
// [FILE] build.gradle.kts
|
// [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 {
|
plugins {
|
||||||
// [PLUGIN] Android Application plugin
|
id("com.android.application") version "8.12.3" apply false
|
||||||
id("com.android.application") version "8.12.2" apply false
|
id("org.jetbrains.kotlin.android") version "2.0.0" apply false
|
||||||
// [PLUGIN] Kotlin Android plugin
|
id("org.jetbrains.kotlin.plugin.compose") version "2.0.0" apply false
|
||||||
id("org.jetbrains.kotlin.android") version "1.9.22" apply false
|
id("com.google.dagger.hilt.android") version "2.51.1" apply false
|
||||||
// [PLUGIN] Hilt Android plugin
|
id("com.google.devtools.ksp") version "2.0.0-1.0.24" apply false
|
||||||
id("com.google.dagger.hilt.android") version "2.48.1" apply false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// [END_FILE_build.gradle.kts]
|
// [END_FILE_build.gradle.kts]
|
||||||
|
|||||||
@@ -4,50 +4,31 @@
|
|||||||
|
|
||||||
// [ENTITY: Object('Versions')]
|
// [ENTITY: Object('Versions')]
|
||||||
object Versions {
|
object Versions {
|
||||||
// Build
|
|
||||||
const val compileSdk = 34
|
const val compileSdk = 34
|
||||||
const val minSdk = 26
|
const val minSdk = 24
|
||||||
const val targetSdk = 34
|
const val targetSdk = 34
|
||||||
const val versionCode = 1
|
const val versionCode = 1
|
||||||
const val versionName = "1.0"
|
const val versionName = "1.0"
|
||||||
|
const val kotlin = "1.9.10"
|
||||||
// Kotlin
|
|
||||||
const val kotlin = "1.9.22"
|
|
||||||
const val coroutines = "1.7.3"
|
const val coroutines = "1.7.3"
|
||||||
|
const val composeCompiler = "1.5.4"
|
||||||
// Jetpack Compose
|
const val composeBom = "2024.05.00"
|
||||||
const val composeCompiler = "1.5.8"
|
|
||||||
const val composeBom = "2023.10.01"
|
|
||||||
const val activityCompose = "1.8.2"
|
const val activityCompose = "1.8.2"
|
||||||
const val navigationCompose = "2.7.6"
|
const val navigationCompose = "2.7.7"
|
||||||
const val hiltNavigationCompose = "1.1.0"
|
const val hiltNavigationCompose = "1.1.0"
|
||||||
|
|
||||||
// AndroidX
|
|
||||||
const val coreKtx = "1.12.0"
|
const val coreKtx = "1.12.0"
|
||||||
const val lifecycle = "2.6.2"
|
const val lifecycle = "2.7.0"
|
||||||
const val appcompat = "1.6.1"
|
const val appcompat = "1.6.1"
|
||||||
|
|
||||||
// Networking
|
|
||||||
const val retrofit = "2.9.0"
|
const val retrofit = "2.9.0"
|
||||||
const val okhttp = "4.12.0"
|
const val okhttp = "4.12.0"
|
||||||
const val moshi = "1.15.0"
|
const val moshi = "1.15.1"
|
||||||
|
|
||||||
// Database
|
|
||||||
const val room = "2.6.1"
|
const val room = "2.6.1"
|
||||||
|
const val hilt = "2.51.1"
|
||||||
// DI
|
const val hiltCompiler = "1.2.0"
|
||||||
const val hilt = "2.48.1"
|
|
||||||
const val hiltCompiler = "1.1.0"
|
|
||||||
|
|
||||||
// Logging
|
|
||||||
const val timber = "5.0.1"
|
const val timber = "5.0.1"
|
||||||
|
|
||||||
// Testing
|
|
||||||
const val junit = "4.13.2"
|
const val junit = "4.13.2"
|
||||||
const val extJunit = "1.1.5"
|
const val extJunit = "1.1.5"
|
||||||
const val espresso = "3.5.1"
|
const val espresso = "3.5.1"
|
||||||
|
|
||||||
// Testing
|
|
||||||
const val kotest = "5.8.0"
|
const val kotest = "5.8.0"
|
||||||
const val mockk = "1.13.10"
|
const val mockk = "1.13.10"
|
||||||
}
|
}
|
||||||
@@ -55,26 +36,21 @@ object Versions {
|
|||||||
|
|
||||||
// [ENTITY: Object('Libs')]
|
// [ENTITY: Object('Libs')]
|
||||||
object Libs {
|
object Libs {
|
||||||
// Kotlin
|
|
||||||
const val coroutinesCore = "org.jetbrains.kotlinx:kotlinx-coroutines-core:${Versions.coroutines}"
|
const val coroutinesCore = "org.jetbrains.kotlinx:kotlinx-coroutines-core:${Versions.coroutines}"
|
||||||
const val coroutinesAndroid = "org.jetbrains.kotlinx:kotlinx-coroutines-android:${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 coreKtx = "androidx.core:core-ktx:${Versions.coreKtx}"
|
||||||
const val lifecycleRuntime = "androidx.lifecycle:lifecycle-runtime-ktx:${Versions.lifecycle}"
|
const val lifecycleRuntime = "androidx.lifecycle:lifecycle-runtime-ktx:${Versions.lifecycle}"
|
||||||
const val appcompat = "androidx.appcompat:appcompat:${Versions.appcompat}"
|
const val appcompat = "androidx.appcompat:appcompat:${Versions.appcompat}"
|
||||||
|
const val composeUi = "androidx.compose.ui:ui:1.5.4"
|
||||||
// Compose
|
const val composeUiGraphics = "androidx.compose.ui:ui-graphics:1.5.4"
|
||||||
const val composeBom = "androidx.compose:compose-bom:${Versions.composeBom}"
|
const val composeUiToolingPreview = "androidx.compose.ui:ui-tooling-preview:1.5.4"
|
||||||
const val composeUi = "androidx.compose.ui:ui"
|
const val composeMaterial3 = "androidx.compose.material3:material3:1.1.2"
|
||||||
const val composeUiGraphics = "androidx.compose.ui:ui-graphics"
|
const val composeFoundation = "androidx.compose.foundation:foundation:1.5.4"
|
||||||
const val composeUiToolingPreview = "androidx.compose.ui:ui-tooling-preview"
|
const val composeFoundationLayout = "androidx.compose.foundation:foundation-layout:1.5.4"
|
||||||
const val composeMaterial3 = "androidx.compose.material3:material3"
|
const val composeMaterialIconsExtended = "androidx.compose.material:material-icons-extended:1.5.4"
|
||||||
const val activityCompose = "androidx.activity:activity-compose:${Versions.activityCompose}"
|
const val activityCompose = "androidx.activity:activity-compose:${Versions.activityCompose}"
|
||||||
const val navigationCompose = "androidx.navigation:navigation-compose:${Versions.navigationCompose}"
|
const val navigationCompose = "androidx.navigation:navigation-compose:${Versions.navigationCompose}"
|
||||||
const val hiltNavigationCompose = "androidx.hilt:hilt-navigation-compose:${Versions.hiltNavigationCompose}"
|
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 retrofit = "com.squareup.retrofit2:retrofit:${Versions.retrofit}"
|
||||||
const val converterMoshi = "com.squareup.retrofit2:converter-moshi:${Versions.retrofit}"
|
const val converterMoshi = "com.squareup.retrofit2:converter-moshi:${Versions.retrofit}"
|
||||||
const val okhttp = "com.squareup.okhttp3:okhttp:${Versions.okhttp}"
|
const val okhttp = "com.squareup.okhttp3:okhttp:${Versions.okhttp}"
|
||||||
@@ -82,27 +58,18 @@ object Libs {
|
|||||||
const val moshi = "com.squareup.moshi:moshi:${Versions.moshi}"
|
const val moshi = "com.squareup.moshi:moshi:${Versions.moshi}"
|
||||||
const val moshiKotlin = "com.squareup.moshi:moshi-kotlin:${Versions.moshi}"
|
const val moshiKotlin = "com.squareup.moshi:moshi-kotlin:${Versions.moshi}"
|
||||||
const val moshiCodegen = "com.squareup.moshi:moshi-kotlin-codegen:${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 roomRuntime = "androidx.room:room-runtime:${Versions.room}"
|
||||||
const val roomKtx = "androidx.room:room-ktx:${Versions.room}"
|
const val roomKtx = "androidx.room:room-ktx:${Versions.room}"
|
||||||
const val roomCompiler = "androidx.room:room-compiler:${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 hiltAndroid = "com.google.dagger:hilt-android:${Versions.hilt}"
|
||||||
const val hiltCompiler = "com.google.dagger:hilt-android-compiler:${Versions.hilt}"
|
const val hiltCompiler = "com.google.dagger:hilt-android-compiler:${Versions.hilt}"
|
||||||
|
|
||||||
// Logging
|
|
||||||
const val timber = "com.jakewharton.timber:timber:${Versions.timber}"
|
const val timber = "com.jakewharton.timber:timber:${Versions.timber}"
|
||||||
|
|
||||||
// Testing
|
|
||||||
const val junit = "junit:junit:${Versions.junit}"
|
const val junit = "junit:junit:${Versions.junit}"
|
||||||
const val extJunit = "androidx.test.ext:junit:${Versions.extJunit}"
|
const val extJunit = "androidx.test.ext:junit:${Versions.extJunit}"
|
||||||
const val espressoCore = "androidx.test.espresso:espresso-core:${Versions.espresso}"
|
const val espressoCore = "androidx.test.espresso:espresso-core:${Versions.espresso}"
|
||||||
const val composeUiTestJunit4 = "androidx.compose.ui:ui-test-junit4"
|
const val composeUiTestJunit4 = "androidx.compose.ui:ui-test-junit4:1.5.4"
|
||||||
const val composeUiTooling = "androidx.compose.ui:ui-tooling"
|
const val composeUiTooling = "androidx.compose.ui:ui-tooling:1.5.4"
|
||||||
const val composeUiTestManifest = "androidx.compose.ui:ui-test-manifest"
|
const val composeUiTestManifest = "androidx.compose.ui:ui-test-manifest:1.5.4"
|
||||||
|
|
||||||
const val kotestRunnerJunit5 = "io.kotest:kotest-runner-junit5:${Versions.kotest}"
|
const val kotestRunnerJunit5 = "io.kotest:kotest-runner-junit5:${Versions.kotest}"
|
||||||
const val kotestAssertionsCore = "io.kotest:kotest-assertions-core:${Versions.kotest}"
|
const val kotestAssertionsCore = "io.kotest:kotest-assertions-core:${Versions.kotest}"
|
||||||
const val mockk = "io.mockk:mockk:${Versions.mockk}"
|
const val mockk = "io.mockk:mockk:${Versions.mockk}"
|
||||||
|
|||||||
1
data/semantic-ktlint-rules/.gitignore
vendored
@@ -1 +0,0 @@
|
|||||||
/build
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
// Файл: /data/semantic-ktlint-rules/build.gradle.kts
|
|
||||||
|
|
||||||
plugins {
|
|
||||||
kotlin("jvm")
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
// Зависимость для RuleSetProviderV3
|
|
||||||
implementation("com.pinterest.ktlint:ktlint-cli-ruleset-core:1.2.1")
|
|
||||||
// Зависимость для Rule, RuleId и psi-утилит
|
|
||||||
api("com.pinterest.ktlint:ktlint-rule-engine:1.2.1")
|
|
||||||
|
|
||||||
// Зависимости для тестирования остаются без изменений
|
|
||||||
testImplementation(kotlin("test"))
|
|
||||||
testImplementation("com.pinterest.ktlint:ktlint-test:1.2.1")
|
|
||||||
testImplementation("org.assertj:assertj-core:3.24.2")
|
|
||||||
}
|
|
||||||
21
data/semantic-ktlint-rules/proguard-rules.pro
vendored
@@ -1,21 +0,0 @@
|
|||||||
# Add project specific ProGuard rules here.
|
|
||||||
# You can control the set of applied configuration files using the
|
|
||||||
# proguardFiles setting in build.gradle.
|
|
||||||
#
|
|
||||||
# For more details, see
|
|
||||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
|
||||||
|
|
||||||
# If your project uses WebView with JS, uncomment the following
|
|
||||||
# and specify the fully qualified class name to the JavaScript interface
|
|
||||||
# class:
|
|
||||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
|
||||||
# public *;
|
|
||||||
#}
|
|
||||||
|
|
||||||
# Uncomment this to preserve the line number information for
|
|
||||||
# debugging stack traces.
|
|
||||||
#-keepattributes SourceFile,LineNumberTable
|
|
||||||
|
|
||||||
# If you keep the line number information, uncomment this to
|
|
||||||
# hide the original source file name.
|
|
||||||
#-renamesourcefileattribute SourceFile
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
// [PACKAGE] com.busya.ktlint.rules
|
|
||||||
// [FILE] ExampleInstrumentedTest.kt
|
|
||||||
// [SEMANTICS] testing, android, ktlint, rules
|
|
||||||
|
|
||||||
package com.busya.ktlint.rules
|
|
||||||
|
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
|
||||||
|
|
||||||
import org.junit.Test
|
|
||||||
import org.junit.runner.RunWith
|
|
||||||
|
|
||||||
import org.junit.Assert.*
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Instrumented test, which will execute on an Android device.
|
|
||||||
*
|
|
||||||
* See [testing documentation](http://d.android.com/tools/testing).
|
|
||||||
*/
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
|
||||||
class ExampleInstrumentedTest {
|
|
||||||
@Test
|
|
||||||
fun useAppContext() {
|
|
||||||
// Context of the app under test.
|
|
||||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
|
||||||
assertEquals("com.busya.ktlint.rules", appContext.packageName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
|
|
||||||
<application
|
|
||||||
android:allowBackup="true"
|
|
||||||
android:icon="@mipmap/ic_launcher"
|
|
||||||
android:label="@string/app_name"
|
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
|
||||||
android:supportsRtl="true"
|
|
||||||
android:theme="@style/Theme.HomeboxLens" />
|
|
||||||
|
|
||||||
</manifest>
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
// [PACKAGE] com.busya.ktlint.rules
|
|
||||||
// [FILE] CustomRuleSetProvider.kt
|
|
||||||
// [SEMANTICS] ktlint, rules, provider
|
|
||||||
package com.busya.ktlint.rules
|
|
||||||
|
|
||||||
import com.pinterest.ktlint.rule.engine.core.api.RuleProvider
|
|
||||||
import com.pinterest.ktlint.rule.engine.core.api.RuleSetId
|
|
||||||
import com.pinterest.ktlint.cli.ruleset.core.api.RuleSetProviderV3
|
|
||||||
|
|
||||||
class CustomRuleSetProvider : RuleSetProviderV3(RuleSetId("custom")) {
|
|
||||||
override fun getRuleProviders(): Set<RuleProvider> {
|
|
||||||
return setOf(
|
|
||||||
RuleProvider { FileHeaderRule() },
|
|
||||||
RuleProvider { MandatoryEntityDeclarationRule() },
|
|
||||||
RuleProvider { NoStrayCommentsRule() }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
// [PACKAGE] com.busya.ktlint.rules
|
|
||||||
// [FILE] FileHeaderRule.kt
|
|
||||||
// [SEMANTICS] ktlint, rules, file_header
|
|
||||||
package com.busya.ktlint.rules
|
|
||||||
|
|
||||||
import com.pinterest.ktlint.rule.engine.core.api.ElementType
|
|
||||||
import com.pinterest.ktlint.rule.engine.core.api.Rule
|
|
||||||
import com.pinterest.ktlint.rule.engine.core.api.Rule.About
|
|
||||||
import com.pinterest.ktlint.rule.engine.core.api.RuleId
|
|
||||||
import org.jetbrains.kotlin.com.intellij.lang.ASTNode
|
|
||||||
|
|
||||||
class FileHeaderRule : Rule(ruleId = RuleId("custom:file-header-rule"), about = About()) {
|
|
||||||
override fun beforeVisitChildNodes(
|
|
||||||
node: ASTNode,
|
|
||||||
autoCorrect: Boolean,
|
|
||||||
emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit
|
|
||||||
) {
|
|
||||||
if (node.elementType == ElementType.FILE) {
|
|
||||||
val lines = node.text.lines()
|
|
||||||
if (lines.size < 3) {
|
|
||||||
emit(node.startOffset, "File must start with a 3-line semantic header.", false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (!lines[0].startsWith("// [PACKAGE]")) {
|
|
||||||
emit(node.startOffset, "File header missing or incorrect. Line 1 must be '// [PACKAGE] ...'.", false)
|
|
||||||
}
|
|
||||||
if (!lines[1].startsWith("// [FILE]")) {
|
|
||||||
emit(node.startOffset + lines[0].length + 1, "File header missing or incorrect. Line 2 must be '// [FILE] ...'.", false)
|
|
||||||
}
|
|
||||||
if (!lines[2].startsWith("// [SEMANTICS]")) {
|
|
||||||
emit(node.startOffset + lines[0].length + lines[1].length + 2, "File header missing or incorrect. Line 3 must be '// [SEMANTICS] ...'.", false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
// [PACKAGE] com.busya.ktlint.rules
|
|
||||||
// [FILE] MandatoryEntityDeclarationRule.kt
|
|
||||||
// [SEMANTICS] ktlint, rules, entity_declaration
|
|
||||||
package com.busya.ktlint.rules
|
|
||||||
|
|
||||||
import com.pinterest.ktlint.rule.engine.core.api.ElementType
|
|
||||||
import com.pinterest.ktlint.rule.engine.core.api.Rule
|
|
||||||
import com.pinterest.ktlint.rule.engine.core.api.Rule.About
|
|
||||||
import com.pinterest.ktlint.rule.engine.core.api.RuleId
|
|
||||||
import com.pinterest.ktlint.rule.engine.core.api.prevLeaf
|
|
||||||
import org.jetbrains.kotlin.com.intellij.lang.ASTNode
|
|
||||||
import org.jetbrains.kotlin.lexer.KtTokens
|
|
||||||
import org.jetbrains.kotlin.psi.KtDeclaration
|
|
||||||
|
|
||||||
class MandatoryEntityDeclarationRule : Rule(ruleId = RuleId("custom:entity-declaration-rule"), about = About()) {
|
|
||||||
private val entityTypes = setOf(
|
|
||||||
ElementType.CLASS,
|
|
||||||
ElementType.OBJECT_DECLARATION,
|
|
||||||
ElementType.FUN
|
|
||||||
)
|
|
||||||
|
|
||||||
override fun beforeVisitChildNodes(
|
|
||||||
node: ASTNode,
|
|
||||||
autoCorrect: Boolean,
|
|
||||||
emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit
|
|
||||||
) {
|
|
||||||
if (node.elementType in entityTypes) {
|
|
||||||
val ktDeclaration = node.psi as? KtDeclaration ?: return
|
|
||||||
if (node.elementType == ElementType.FUN &&
|
|
||||||
(ktDeclaration.hasModifier(KtTokens.PRIVATE_KEYWORD) ||
|
|
||||||
ktDeclaration.hasModifier(KtTokens.PROTECTED_KEYWORD) ||
|
|
||||||
ktDeclaration.hasModifier(KtTokens.INTERNAL_KEYWORD))
|
|
||||||
) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val prevComment = node.prevLeaf { it.elementType == ElementType.EOL_COMMENT }
|
|
||||||
if (prevComment == null || !prevComment.text.startsWith("// [ENTITY:")) {
|
|
||||||
emit(node.startOffset, "Missing or misplaced '// [ENTITY: ...]' declaration before '${node.elementType}'.", false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
// [PACKAGE] com.busya.ktlint.rules
|
|
||||||
// [FILE] NoStrayCommentsRule.kt
|
|
||||||
// [SEMANTICS] ktlint, rules, comments
|
|
||||||
package com.busya.ktlint.rules
|
|
||||||
|
|
||||||
import com.pinterest.ktlint.rule.engine.core.api.ElementType
|
|
||||||
import com.pinterest.ktlint.rule.engine.core.api.Rule
|
|
||||||
import com.pinterest.ktlint.rule.engine.core.api.Rule.About
|
|
||||||
import com.pinterest.ktlint.rule.engine.core.api.RuleId
|
|
||||||
import org.jetbrains.kotlin.com.intellij.lang.ASTNode
|
|
||||||
|
|
||||||
class NoStrayCommentsRule : Rule(ruleId = RuleId("custom:no-stray-comments-rule"), about = About()) {
|
|
||||||
private val allowedCommentPattern = Regex("""^//\s?\[([A-Z_]+|ENTITY:|RELATION:|AI_NOTE:)]""")
|
|
||||||
override fun beforeVisitChildNodes(
|
|
||||||
node: ASTNode,
|
|
||||||
autoCorrect: Boolean,
|
|
||||||
emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit
|
|
||||||
) {
|
|
||||||
if (node.elementType == ElementType.EOL_COMMENT) {
|
|
||||||
val commentText = node.text
|
|
||||||
if (!allowedCommentPattern.matches(commentText)) {
|
|
||||||
emit(node.startOffset, "Stray comment found. Use semantic anchors like '// [TAG]' or '// [AI_NOTE]:' instead.", false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,170 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="108dp"
|
|
||||||
android:height="108dp"
|
|
||||||
android:viewportWidth="108"
|
|
||||||
android:viewportHeight="108">
|
|
||||||
<path
|
|
||||||
android:fillColor="#3DDC84"
|
|
||||||
android:pathData="M0,0h108v108h-108z" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M9,0L9,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,0L19,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M29,0L29,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M39,0L39,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M49,0L49,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M59,0L59,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M69,0L69,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M79,0L79,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M89,0L89,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M99,0L99,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,9L108,9"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,19L108,19"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,29L108,29"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,39L108,39"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,49L108,49"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,59L108,59"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,69L108,69"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,79L108,79"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,89L108,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,99L108,99"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,29L89,29"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,39L89,39"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,49L89,49"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,59L89,59"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,69L89,69"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,79L89,79"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M29,19L29,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M39,19L39,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M49,19L49,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M59,19L59,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M69,19L69,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M79,19L79,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
</vector>
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:aapt="http://schemas.android.com/aapt"
|
|
||||||
android:width="108dp"
|
|
||||||
android:height="108dp"
|
|
||||||
android:viewportWidth="108"
|
|
||||||
android:viewportHeight="108">
|
|
||||||
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
|
|
||||||
<aapt:attr name="android:fillColor">
|
|
||||||
<gradient
|
|
||||||
android:endX="85.84757"
|
|
||||||
android:endY="92.4963"
|
|
||||||
android:startX="42.9492"
|
|
||||||
android:startY="49.59793"
|
|
||||||
android:type="linear">
|
|
||||||
<item
|
|
||||||
android:color="#44000000"
|
|
||||||
android:offset="0.0" />
|
|
||||||
<item
|
|
||||||
android:color="#00000000"
|
|
||||||
android:offset="1.0" />
|
|
||||||
</gradient>
|
|
||||||
</aapt:attr>
|
|
||||||
</path>
|
|
||||||
<path
|
|
||||||
android:fillColor="#FFFFFF"
|
|
||||||
android:fillType="nonZero"
|
|
||||||
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
|
|
||||||
android:strokeWidth="1"
|
|
||||||
android:strokeColor="#00000000" />
|
|
||||||
</vector>
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<background android:drawable="@drawable/ic_launcher_background" />
|
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
|
||||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
|
||||||
</adaptive-icon>
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<background android:drawable="@drawable/ic_launcher_background" />
|
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
|
||||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
|
||||||
</adaptive-icon>
|
|
||||||
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 982 B |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 7.6 KiB |
@@ -1,16 +0,0 @@
|
|||||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
|
||||||
<!-- Base application theme. -->
|
|
||||||
<style name="Theme.HomeboxLens" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
|
||||||
<!-- Primary brand color. -->
|
|
||||||
<item name="colorPrimary">@color/purple_200</item>
|
|
||||||
<item name="colorPrimaryVariant">@color/purple_700</item>
|
|
||||||
<item name="colorOnPrimary">@color/black</item>
|
|
||||||
<!-- Secondary brand color. -->
|
|
||||||
<item name="colorSecondary">@color/teal_200</item>
|
|
||||||
<item name="colorSecondaryVariant">@color/teal_200</item>
|
|
||||||
<item name="colorOnSecondary">@color/black</item>
|
|
||||||
<!-- Status bar color. -->
|
|
||||||
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
|
|
||||||
<!-- Customize your theme here. -->
|
|
||||||
</style>
|
|
||||||
</resources>
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<color name="purple_200">#FFBB86FC</color>
|
|
||||||
<color name="purple_500">#FF6200EE</color>
|
|
||||||
<color name="purple_700">#FF3700B3</color>
|
|
||||||
<color name="teal_200">#FF03DAC5</color>
|
|
||||||
<color name="teal_700">#FF018786</color>
|
|
||||||
<color name="black">#FF000000</color>
|
|
||||||
<color name="white">#FFFFFFFF</color>
|
|
||||||
</resources>
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
<resources>
|
|
||||||
<string name="app_name">semantic-ktlint-rules</string>
|
|
||||||
</resources>
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
|
||||||
<!-- Base application theme. -->
|
|
||||||
<style name="Theme.HomeboxLens" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
|
||||||
<!-- Primary brand color. -->
|
|
||||||
<item name="colorPrimary">@color/purple_500</item>
|
|
||||||
<item name="colorPrimaryVariant">@color/purple_700</item>
|
|
||||||
<item name="colorOnPrimary">@color/white</item>
|
|
||||||
<!-- Secondary brand color. -->
|
|
||||||
<item name="colorSecondary">@color/teal_200</item>
|
|
||||||
<item name="colorSecondaryVariant">@color/teal_700</item>
|
|
||||||
<item name="colorOnSecondary">@color/black</item>
|
|
||||||
<!-- Status bar color. -->
|
|
||||||
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
|
|
||||||
<!-- Customize your theme here. -->
|
|
||||||
</style>
|
|
||||||
</resources>
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
com.busya.ktlint.rules.CustomRuleSetProvider
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
// [PACKAGE] com.busya.ktlint.rules
|
|
||||||
// [FILE] ExampleUnitTest.kt
|
|
||||||
// [SEMANTICS] testing, ktlint, rules
|
|
||||||
|
|
||||||
package com.busya.ktlint.rules
|
|
||||||
|
|
||||||
import com.pinterest.ktlint.test.KtLintAssertThat.Companion.assertThatRule
|
|
||||||
import org.junit.jupiter.api.Test
|
|
||||||
|
|
||||||
class FileHeaderRuleTest {
|
|
||||||
|
|
||||||
private val ruleAssertThat = assertThatRule { FileHeaderRule() }
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `should pass on correct header`() {
|
|
||||||
val code = """
|
|
||||||
// [PACKAGE] com.example
|
|
||||||
// [FILE] Test.kt
|
|
||||||
// [SEMANTICS] test, example
|
|
||||||
package com.example
|
|
||||||
""".trimIndent()
|
|
||||||
ruleAssertThat(code).hasNoLintViolations()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `should fail on missing header`() {
|
|
||||||
val code = """
|
|
||||||
package com.example
|
|
||||||
""".trimIndent()
|
|
||||||
ruleAssertThat(code)
|
|
||||||
.hasLintViolation(1, 1, "File must start with a 3-line semantic header.")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `should fail on incorrect line 1`() {
|
|
||||||
val code = """
|
|
||||||
// [WRONG_TAG] com.example
|
|
||||||
// [FILE] Test.kt
|
|
||||||
// [SEMANTICS] test, example
|
|
||||||
package com.example
|
|
||||||
""".trimIndent()
|
|
||||||
ruleAssertThat(code)
|
|
||||||
.hasLintViolation(1, 1, "File header missing or incorrect. Line 1 must be '// [PACKAGE] ...'.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -37,7 +37,18 @@ data class ItemOutDto(
|
|||||||
@Json(name = "fields") val fields: List<CustomFieldDto>,
|
@Json(name = "fields") val fields: List<CustomFieldDto>,
|
||||||
@Json(name = "maintenance") val maintenance: List<MaintenanceEntryDto>,
|
@Json(name = "maintenance") val maintenance: List<MaintenanceEntryDto>,
|
||||||
@Json(name = "createdAt") val createdAt: String,
|
@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')]
|
// [END_ENTITY: DataClass('ItemOutDto')]
|
||||||
|
|
||||||
@@ -69,7 +80,18 @@ fun ItemOutDto.toDomain(): ItemOut {
|
|||||||
fields = this.fields.map { it.toDomain() },
|
fields = this.fields.map { it.toDomain() },
|
||||||
maintenance = this.maintenance.map { it.toDomain() },
|
maintenance = this.maintenance.map { it.toDomain() },
|
||||||
createdAt = this.createdAt,
|
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')]
|
// [END_ENTITY: Function('toDomain')]
|
||||||
@@ -27,6 +27,15 @@ interface LabelDao {
|
|||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
suspend fun insertLabels(labels: List<LabelEntity>)
|
suspend fun insertLabels(labels: List<LabelEntity>)
|
||||||
// [END_ENTITY: Function('insertLabels')]
|
// [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_ENTITY: Interface('LabelDao')]
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import com.homebox.lens.data.api.dto.LocationUpdateDto
|
|||||||
import com.homebox.lens.data.api.dto.LabelUpdateDto
|
import com.homebox.lens.data.api.dto.LabelUpdateDto
|
||||||
import com.homebox.lens.data.api.dto.LocationOutDto
|
import com.homebox.lens.data.api.dto.LocationOutDto
|
||||||
import com.homebox.lens.data.db.dao.ItemDao
|
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.data.db.entity.toDomain
|
||||||
import com.homebox.lens.domain.model.*
|
import com.homebox.lens.domain.model.*
|
||||||
import com.homebox.lens.domain.repository.ItemRepository
|
import com.homebox.lens.domain.repository.ItemRepository
|
||||||
@@ -29,7 +30,8 @@ import javax.inject.Singleton
|
|||||||
@Singleton
|
@Singleton
|
||||||
class ItemRepositoryImpl @Inject constructor(
|
class ItemRepositoryImpl @Inject constructor(
|
||||||
private val apiService: HomeboxApiService,
|
private val apiService: HomeboxApiService,
|
||||||
private val itemDao: ItemDao
|
private val itemDao: ItemDao,
|
||||||
|
private val labelDao: LabelDao
|
||||||
) : ItemRepository {
|
) : ItemRepository {
|
||||||
|
|
||||||
// [ENTITY: Function('createItem')]
|
// [ENTITY: Function('createItem')]
|
||||||
@@ -121,6 +123,7 @@ class ItemRepositoryImpl @Inject constructor(
|
|||||||
|
|
||||||
override suspend fun deleteLabel(labelId: String) {
|
override suspend fun deleteLabel(labelId: String) {
|
||||||
apiService.deleteLabel(labelId)
|
apiService.deleteLabel(labelId)
|
||||||
|
labelDao.deleteLabelById(labelId)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun createLocation(newLocationData: LocationCreate): LocationOut {
|
override suspend fun createLocation(newLocationData: LocationCreate): LocationOut {
|
||||||
|
|||||||