Skip to content

Update tests#151

Merged
amikhaylin merged 11 commits intomasterfrom
update-tests
Nov 8, 2025
Merged

Update tests#151
amikhaylin merged 11 commits intomasterfrom
update-tests

Conversation

@amikhaylin
Copy link
Copy Markdown
Owner

No description provided.

Copy link
Copy Markdown

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review by ChatGPT

OPENAI_API_ENDPOINT: https://api.proxyapi.ru/openai/v1
MODEL: gpt-5
PROMPT: "You are an experienced Swift/SwigtUI developer and your job is to review pull requests. Please review the following code for any misunderstandings or violations. Don't spend time commenting on what is already working perfectly. I'm looking for constructive criticism and suggestions for improving the code, only useful and thorough notes"
PROMPT: "Вы опытный разработчик Swift/SwiftUI, и ваша задача — проверять запросы на включение изменений. Пожалуйста, проверьте следующий код на наличие недопониманий или нарушений. Не тратьте время на комментарии того, что и так работает идеально. Я жду конструктивной критики и предложений по улучшению кода, только полезных и содержательных замечаний."
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ниже только полезные замечания по диффу (локализация PROMPT на русский) и предложения по улучшению.

Что можно улучшить прямо сейчас

  • Формулировка: «запросы на включение изменений» звучит неестественно. Лучше: «пул‑реквесты (PR)» или «pull requests». Это снизит двусмысленность.
  • Согласованность языка: у вас есть LANGUAGE: Russian и русскоязычный PROMPT. Либо:
    • оставить LANGUAGE и сделать PROMPT нейтральным, используя LANGUAGE в коде; либо
    • убрать LANGUAGE и поручить контроль языка PROMPT’у; либо
    • явно указать в PROMPT «Отвечай на русском».
  • Кавычки/кодировка: PROMPT в двойных кавычках с длинным тире и кириллицей — ок, но безопаснее и удобнее редактировать как многострочный блок YAML. Это исключит случайные проблемы с экранированием и упростит дальнейшие правки.
  • Структура воркфлоу: проверьте уровень, на котором объявлены env. Сейчас фрагмент выглядит как env внутри job/step. Если эти переменные нужны во всём воркфлоу, лучше вынести в верхнеуровневый env:. Если только для шага — поместить под конкретный steps[*].env.
  • MODEL: gpt-5 — проверьте доступность модели на указанном эндпоинте. Если это кастом у провайдера — ок. Иначе добавьте:
    • возможность переопределения через workflow inputs/vars/secrets;
    • fallback-модель;
    • матрицу для тестов разных моделей.
  • Управление качеством ответов: текущий PROMPT описывает роль, но не формат и критерии. Добавьте требования к выходному формату и фокусу, чтобы уменьшить «воду» и обеспечить практичность: краткость, приоритизация, конкретика с примерами, просьба запрашивать недостающий контекст.

Предлагаемый вариант PROMPT (более управляемый)
Используйте многострочный блок и четкий формат:

PROMPT: |
Ты опытный разработчик Swift/SwiftUI. Твоя задача — делать ревью пул‑реквестов.
Правила:

  • Пиши на русском, кратко и по делу.
  • Не описывай то, что уже работает корректно.
  • Фокус: архитектура, корректность, производительность, читаемость, безопасность, тестопригодность, соответствие Swift/SwiftUI best practices.
  • Давай конкретные правки и примеры кода.
  • Структура ответа:
    1. Критические проблемы (с объяснением и как исправить)
    2. Замечания средней важности
    3. Неблокирующие улучшения
  • Если контекста не хватает, сначала уточни вопросы.
  • Не делай общих советов без привязки к коду.

Технические мелочи YAML

  • Предпочтительно одинарные кавычки для простых однострочных строк без необходимости интерполяций, либо многострочный блок | / > для длинных текстов.
  • Убедитесь, что репозиторий в UTF‑8 (GitHub по умолчанию — да) — длинное тире и кириллица тогда безопасны.

Опционально

  • Вынесите OPENAI_API_ENDPOINT и MODEL в vars/inputs для разных окружений (dev/stage/prod).
  • Добавьте ограничение длины ответа в PROMPT (например, «до 300–500 слов») для контроля расходов токенов.

Итог: локализация корректна, но я рекомендую уточнить терминологию («пул‑реквесты»), формализовать формат ответа, использовать многострочный PROMPT и проверить доступность модели gpt-5 и уровень объявления env.


screenshots/desktop/*.html
screenshots/desktop/ru/*.png
screenshots/desktop/en-US/*.png
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ниже только содержательные замечания по диффу .gitignore — на что обратить внимание и как улучшить.

  • Podfile.lock

    • Убрали из .gitignore — это ошибка для проектов на CocoaPods. Под файл lock обязательно коммитится для детерминированных сборок. Иначе разные разработчики/CI получат разные версии зависимостей.
    • Если вы реально ушли с CocoaPods на SPM — тогда уберите и Pods/ из .gitignore и вообще все, что связано с CocoaPods, чтобы не путать.
  • GoogleService-Info.plist

    • Исключение из .gitignore означает, что файл попадёт в репозиторий. Для Firebase это обычно допустимо, но убедитесь:
      • Есть разделение по схемам/таргетам (Debug/Staging/Release) с разными plist.
      • В plist нет приватных секретов, которые нельзя публиковать.
      • Альтернатива: держать плейсхолдер в репо и подменять реальный файл через CI или шифровать (git-crypt/sops).
  • TestResults.xcresult, TestResults/, TestResults*

    • Паттерн TestResults* избыточно широкий: может случайно скрыть несвязанные файлы/директории с таким префиксом. Заанкорьте и сузьте.
    • .xcresult — это пакет (директория). Универсальней игнорить по маске: *.xcresult (лучше с анкором).
    • Дублирование TestResults.xcresult, TestResults/ и TestResults* — оставьте один понятный вариант.
    • Рекомендация:
      • Добавить: /*.xcresult
      • Если нужен отдельный каталог для артефактов тестов: /TestResults/ (и убрать TestResults*).
  • Скриншоты: переход на screenshots/mobile|watch|desktop

    • Жёстко прописанные локали (ru, en-US) лучше заменить на маски — чтобы не править .gitignore при добавлении новых локалей:
      • /screenshots/**/ *.png
      • /screenshots/**/ *.html
      • /screenshots/**/ *.mp4 (если видео есть не только на mobile — добавьте для watch/desktop тоже).
    • Заанкорьте пути от корня, чтобы не игнорить случайно одноимённые директории в зависимостях: начинайте с /
    • Если это артефакты fastlane, подумайте о выносе в стандартный путь fastlane/screenshots — меньше кастомной логики.
  • Package.resolved

    • Сейчас у вас он игнорится. Для SPM его лучше коммитить, чтобы зафиксировать версии пакетов и иметь воспроизводимые сборки в команде/CI. Рекомендация — убрать Package.resolved из .gitignore.
  • Прочие типичные артефакты Xcode/SPM, которых не хватает

    • Добавьте (если релевантно вашему процессу):
      • /DerivedData/
      • /.build/ (SPM)
      • /build/
      • *.xcarchive
      • *.ipa
      • *.dSYM
      • .DS_Store

Предлагаемый фрагмент для упрощения и повышения надёжности (адаптируйте под ваш проект):

  • Зафиксировать зависимости:
    • удалить из .gitignore: Package.resolved
    • вернуть в репозиторий: Podfile.lock (если остались на CocoaPods)
  • Сужение и анкоринг путей:
    • /*.xcresult
    • /TestResults/
    • /screenshots/**.png
    • /screenshots/**.html
    • /screenshots/**.mp4
  • Базовые артефакты:
    • /DerivedData/
    • /.build/
    • /build/
    • *.xcarchive
    • *.ipa
    • *.dSYM
    • .DS_Store

Итого:

  • Верните Podfile.lock (или полностью выпилите все следы CocoaPods, если не используете).
  • Не игнорьте Package.resolved — коммитьте его.
  • Унифицируйте игнор тестовых артефактов через *.xcresult и уберите дубли.
  • Обобщите маски для скриншотов и заанкорьте пути от корня.
  • Проверьте политику по GoogleService-Info.plist и осознанность его коммита.

doCompletion: name.completion)
dataContainer.mainContext.insert(status)
project.statuses?.append(status)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Хорошее исправление опечатки, но есть несколько потенциальных проблем и зон для улучшения:

  • Опциональная связь и append:

    • project.statuses?.append(status) ничего не сделает, если statuses == nil. В результате объект не будет привязан к проекту. Лучше:
      • Сделать связь не-опциональной: var statuses: [Status] = []
      • Или инициализировать перед использованием: project.statuses = (project.statuses ?? []) + [status]
  • Тип хранилища (Core Data vs SwiftData):

    • Если это SwiftData: массив связи должен быть не-опциональным и append корректен.
    • Если это Core Data: append подозрителен. Используйте сгенерированные аксессоры (addToStatuses) или mutableSetValue(forKey:), чтобы гарантировать корректную установку связи и инверсии.
  • Нейминг doCompletion:

    • Имя не по гайдлайнам Swift. Для Bool используйте префиксы is/has/should. Например: requiresCompletion, isTerminal, makesItemCompleted. Это повысит читаемость и снизит двусмысленность.
  • Потенциальный off-by-one в order:

    • order += 1 перед использованием сдвигает индексацию. Если нужна 0-базовая, используйте enumerated():
      • for (index, name) in names.enumerated() { order = index }
    • Либо привязывайте порядок к project.statuses.count вместо внешнего счетчика — меньше шансов на рассинхрон.
  • Локализация в тестах:

    • name.localizedString() в фикстурах делает тесты зависимыми от локали окружения. Лучше использовать стабильное значение (rawValue/identifier) в тестах и локализовывать только в UI.
  • Инициализация и вставка в контекст:

    • Убедитесь, что удобный init Status(...) создает объект корректно для вашего стека. В Core Data корректно вызывать init(entity:insertInto: nil) и затем context.insert(status), а в SwiftData — просто context.insert(status). Избегайте смешения паттернов.
  • Согласованность имен:

    • Вы поменяли name.competion -> name.completion. Проверьте, что completion по смыслу не конфликтует с возможным свойством completed у Status. Если это «требуется завершение» для данного типа статуса, лучше назвать name.requiresCompletion.

Предлагаемый вариант (SwiftData-подход, безопасный к nil и индексации):

  • for (index, name) in names.enumerated() {
    let status = Status(name: name.localizedString(), order: index, requiresCompletion: name.requiresCompletion)
    context.insert(status)
    project.statuses.append(status) // statuses: [Status] = []
    }

Для Core Data:

  • for (index, name) in names.enumerated() {
    let status = Status(context: context)
    status.name = name.localizedString()
    status.order = Int16(index)
    status.requiresCompletion = name.requiresCompletion
    project.addToStatuses(status)
    }

let _ = Previewer(container!)

ContentView(selectedSideBarItem: $selectedSidebarItem,
selectedProject: $selectedProject)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ниже только по делу — что стоит поправить или улучшить.

  • Логика DragGesture сейчас некорректна: вы прибавляете gesture.translation.height к уже изменяемому sectionHeight на каждом onChanged. Translation задаётся относительно начала жеста, поэтому при каждом изменении вы повторно добавляете «начальный + дельта», но «начальный» у вас уже изменён. Итог — “уползание” высоты. Решение: запоминайте высоту на старте жеста и считайте относительно неё.
    Пример:

    • Вариант 1 (сразу пишем в AppStorage):
      @State private var dragStartHeight: CGFloat?

      DragGesture()
      .onChanged { value in
      if dragStartHeight == nil { dragStartHeight = CGFloat(sectionHeight) }
      let proposed = (dragStartHeight ?? 0) + value.translation.height
      sectionHeight = Double(min(max(proposed, Self.minHeight), Self.maxHeight))
      }
      .onEnded { _ in dragStartHeight = nil }

    • Вариант 2 (плавнее и без записи в UserDefaults на каждом кадре):
      @State private var liveHeight: CGFloat = 300
      .onAppear { liveHeight = CGFloat(sectionHeight).clamped(Self.minHeight...Self.maxHeight) }
      DragGesture()
      .onChanged { value in
      if dragStartHeight == nil { dragStartHeight = liveHeight }
      liveHeight = ((dragStartHeight ?? 0) + value.translation.height)
      .clamped(Self.minHeight...Self.maxHeight)
      }
      .onEnded { _ in
      sectionHeight = Double(liveHeight) // единственная запись в AppStorage
      dragStartHeight = nil
      }

  • Не пишите в AppStorage на каждом onChanged. Это UserDefaults — частые записи избыточны и могут “шуметь” по перформансу. Лучше держать liveHeight в @State и коммитить в AppStorage в onEnded (см. вариант 2 выше).

  • Приведения типов Double <-> CGFloat разбросаны по коду. Для читаемости сделайте прокси:
    private var sectionHeightCG: CGFloat {
    get { CGFloat(sectionHeight) }
    set { sectionHeight = Double(newValue) }
    }
    Тогда frame(height:) и расчёты будут работать с CGFloat, а хранение останется в Double.

  • Улучшите UX перетаскивания:

    • Добавьте .frame(maxWidth: .infinity) и .contentShape(Rectangle()) у «ручки», чтобы увеличить зону хита. 5pt почти не попадётся.
    • На macOS добавьте .cursor(.resizeUpDown) и небольшой padding по вертикали.
    • По желанию: .animation(.interactiveSpring(), value: liveHeight) для сглаживания.
  • Границы высоты лучше вынести в константы, чтобы не дублировать «магические числа»:
    private static let minHeight: CGFloat = 60
    private static let maxHeight: CGFloat = 300

  • Проверьте семантику хранилища:

    • Если высота должна отличаться между окнами/сценами — используйте @SceneStorage вместо @AppStorage.
    • Если кросс-девайс синхронизация не нужна — оставьте как есть; если нужна — подумайте об iCloud/NSUbiquitous, но это уже вне текущего scope.
  • В превью:

    • Вы сменили let previewer = Previewer(container!) на let _ = Previewer(container!). Если Previewer выполняет только сайд-эффект (сидинговка) и не должен жить — ок. Если он держит какие-то подписки/наблюдения — вы их потеряете. Лучше сделать явный статический сидер: Previewer.seed(container) без необходимости хранить инстанс.
    • Избегайте force unwrap в превью. Если container не собрался — превью упадёт. Сделайте guard:
      guard let container = try? ModelContainer(...) else { return Text("Preview setup failed") }
      Previewer.seed(container)
      return ContentView(...)
  • Защита от «битых» значений в сторадже: при запуске можно нормализовать sectionHeight к допустимому диапазону (например, в .onAppear). Это спасёт, если в UserDefaults окажется некорректное число.

  • Мелочь по API: ключ "SectionHeight" лучше неймспейсить, чтобы избежать коллизий: "ContentView.SectionHeight" или вынести в enum Keys.

Опционально:

  • Подумайте, не завязать maxHeight на доступное пространство через GeometryReader, вместо жёстких 300, чтобы не ломать лэйаут на маленьких окнах.
  • Добавьте двойной клик по «ручке» для сброса к дефолту, если это уместно для UX.

Image(systemName: "xmark.square")
}
}
.padding(10)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Коротко и по делу.

  • Исправление по существу: clear — несуществующий SF Symbol. Переход на xmark.square устраняет проблему с иконкой. Однако для действия “очистить/сбросить” в iOS более привычны:

    • xmark.circle или xmark.circle.fill (универсальный “очистить/закрыть”),
    • multiply.circle.fill — часто используется как кнопка очистки (например, в текстфилдах).
      Если действие ближе к “удалить”, используйте trash(/.fill).
  • Доступность: простая Image лишает пользователей VoiceOver контекста.

    • Замените на Label("Очистить", systemImage: "xmark.circle") + .labelStyle(.iconOnly)
    • Или добавьте .accessibilityLabel("Очистить задачу") и, при необходимости, .accessibilityHint("Сбрасывает выбранную задачу").
  • Хит-таргет: .padding(10) может быть меньше рекомендуемых 44×44.

    • Обеспечьте минимум: .frame(minWidth: 44, minHeight: 44) или увеличьте паддинги/используйте .contentShape(Rectangle).
  • Визуальная иерархия:

    • Если это вспомогательная/вторичная кнопка, добавьте .tint(.secondary) или .foregroundStyle(.secondary) для уменьшения визуального веса.
    • Если в списке/ячееке, задайте .buttonStyle(.borderless) или .plain для предотвращения конфликтов с выделением строки.
  • Анимация UX: если скрываете связанную вью по условию task == nil, можно обернуть в withAnimation { focusTask.task = nil } для более плавного удаления.

  • Совместимость: проверьте доступность выбранного символа под целевые iOS (xmark.circle доступен с iOS 13; если поддерживаете ниже — нужен фоллбек, хотя обычно таргет ≥ iOS 14/15).

Итог: технически смена на xmark.square корректна, но я бы предпочёл xmark.circle(.fill) или multiply.circle.fill, добавил доступность, гарантировал 44×44, и при необходимости настроил стиль/тону кнопки.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ниже — только по делу, что можно улучшить в этом скрипте.

  • Явный shell и строгий режим:

    • Добавьте shebang и строгие опции для предсказуемости ошибок:
      • В начало файла: #!/usr/bin/env bash
      • Сразу после: set -euo pipefail
    • При желании дебага — переключатель: [[ ${VERBOSE:-0} -eq 1 ]] && set -x
  • echo с \n не переносит строку портативно:

    • echo "\n..." в большинстве окружений выведет буквально \n. Замените на printf '\n🔄 Change dir to: %s\n' "$TARGET_DIR_2" и аналогично для финального сообщения.
    • В конце файла нет перевода строки — добавьте.
  • Версионная изоляция fastlane:

    • Запускайте через bundler, чтобы не ловить несовместимые глобальные версии: bundle exec fastlane snapshot / bundle exec fastlane frameit.
    • Перед запуском можно проверить наличие: command -v bundle >/dev/null || { printf 'Bundler не найден\n' >&2; exit 127; }
  • Менеджмент директорий:

    • Относительные пути (../, ../../) хрупкие. Зафиксируйте корень репозитория: REPO_ROOT="$(git rev-parse --show-toplevel)" и формируйте абсолютные TARGET_DIR_*="$REPO_ROOT/...".
    • Избегайте глобального cd: оборачивайте шаг в подшелл или используйте pushd/popd:
      • (cd "$dir" && bundle exec fastlane snapshot) — контекст каталога не «течёт» дальше.
    • Можно выделить утилиту:
      • run_in() { local d=$1; shift; printf '🔄 cd %s\n' "$d"; (cd "$d" && "$@"); }
  • Разделение артефактов iOS/watchOS:

    • Сейчас iOS кладёт в ../screenshots/mobile, а watchOS — по умолчанию (скорее всего в каталог тестов). Явно задайте в соответствующих Snapfile разные output_directory и при необходимости derived_data_path, чтобы не было конфликтов и кросс-загрязнений.
    • Если хотите чистые перезапуски, включите clear_previous_screenshots true.
  • frameit и watchOS:

    • Вы frameit запускаете только для iOS — это норм, т.к. frameit официально не поддерживает watchOS-кейсы. Оставьте как есть, но тогда явно задокументируйте это в скрипте/README, чтобы не возникало ожиданий «а где рамки для watchOS».
  • Единообразная обработка ошибок и логгирование:

    • Завести функцию die() { printf '❌ %s\n' "$*" >&2; exit 1; } и переиспользовать вместо копипасты.
    • Ошибки отправляйте в stderr (>&2), чтобы логи CI были чище.
  • Параллелизм и независимость шагов (по желанию):

    • Сейчас скрипт падает на первом фейле. Если важнее получить максимальный объём артефактов, можно запускать iOS и watchOS в отдельных подпроцессах и сводить статусы в конце.
  • Локаль/эмодзи:

    • Эмодзи в логах в не-UTF8 окружении могут ломать вывод. Если CI нестабилен по локали, либо выставьте LC_ALL=C.UTF-8, либо уберите эмодзи.

Итого минимальный смысловой патч:

  • добавить shebang + set -euo pipefail,
  • заменить echo с \n на printf,
  • вызывать fastlane через bundle exec,
  • зафиксировать абсолютные пути от корня репозитория,
  • явно указать раздельные выходные директории для iOS/watchOS в Snapfile,
  • добавить финальный перевод строки в файл.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ниже только содержательные улучшения, без банальностей.

  • Жесткий режим и корректная обработка ошибок в zsh:

    • Сейчас при падении первого запуска второй все равно выполнится (или, наоборот, скроет первый фэйл, если вернется 0). Добавьте строгий режим и аккумулирование статуса, чтобы выполнить оба запуска и вернуть общий результат.
    • Для zsh используйте:
      • set -e; set -u; set -o pipefail
    • Пример суммирования статусов:
      • status=0
      • xcodebuild … || status=$?
      • xcodebuild … || status=$?
      • exit $status
  • Явно задайте destination, чтобы избежать флаки из‑за автоподбора симулятора:

    • -destination 'platform=iOS Simulator,name=iPhone 15,OS=latest'
    • Опционально зафиксируйте OS конкретной версией, если CI образ фиксирован.
  • Результаты и артефакты:

    • Давайте .xcresult расширение: -resultBundlePath TestResults.xcresult и TestResultsRU.xcresult — удобнее открывать и обрабатывать.
    • Во избежание перезаписи результатов при многократных запусках добавьте timestamp или отдельную папку: TestResults-$(date +%Y%m%d-%H%M%S).xcresult.
    • Если нужна покрытие/отчеты в CI: добавьте -enableCodeCoverage YES и потом сливайте покрытие из двух прогонов xccov merge.
  • Производительность/детерминизм:

    • Задайте общий -derivedDataPath (например, ./DerivedData), чтобы второй прогон переиспользовал сборку.
    • Если у вас есть .xcworkspace — предпочтительнее -workspace … вместо -project … (особенно с SPM/Pods).
    • Включите параллельное тестирование, если релевантно: -parallel-testing-enabled YES.
  • Логи:

    • Для читабельности CI‑логов и сохранения кода возврата используйте пайп с xcbeautify/xcpretty и pipefail:
      • xcodebuild … | xcbeautify || status=$?
    • Либо сохраняйте «сырые» логи в файл через tee для последующего анализа.
  • Устойчивость:

    • Используйте /usr/bin/env zsh в шебанге для большей переносимости окружений.
    • Экранируйте аргументы на случай пробелов в именах: -project "PomPadDo.xcodeproj", -resultBundlePath "TestResultsRU.xcresult".
  • Организация тест‑планов:

    • Если RU/EN отличаются только локалью/языком, рассмотрите один test plan с двумя configurations и запускайте через -testPlan PomPadDo -test-configuration RU и затем EN. Это уменьшит дублирование и облегчит поддержку.
  • Управление тестовым набором:

    • Если часть тестов идентична и не зависит от локали — исключайте их во втором прогоне через -only-testing/-skip-testing, чтобы сэкономить время.

Короткий эскиз улучшенной версии:

#!/usr/bin/env zsh
set -e
set -u
set -o pipefail

DEST="platform=iOS Simulator,name=iPhone 15,OS=latest"
DD="./DerivedData"
status=0

xcodebuild test
-project "PomPadDo.xcodeproj"
-scheme "PomPadDo"
-testPlan "PomPadDo"
-destination "$DEST"
-derivedDataPath "$DD"
-enableCodeCoverage YES
-resultBundlePath "TestResults.xcresult" || status=$?

xcodebuild test
-project "PomPadDo.xcodeproj"
-scheme "PomPadDo"
-testPlan "PomPadDoRU"
-destination "$DEST"
-derivedDataPath "$DD"
-enableCodeCoverage YES
-resultBundlePath "TestResultsRU.xcresult" || status=$?

exit $status

Выше — только то, что улучшает надежность, воспроизводимость и удобство CI/аналитики.

cp -f ../screenshots/desktop/en-US/"Apple Macbook Pro 13 Space Gray-01TodayScreen.png" mac-today.png
cp -f ../screenshots/watch/en-US/"Apple Watch Series 11 (46mm)-02SectionsPanel.png" watch-section.png
cp -f ../screenshots/watch/en-US/"Apple Watch Series 11 (46mm)-01TodayScreen.png" watch-today.png
cp -f ../screenshots/watch/en-US/"Apple Watch Series 11 (46mm)-03TaskDetails.png" watch-menu.png
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ниже — только потенциально проблемные места и улучшения по существу.

  • Хрупкие относительные пути и зависимость от текущей директории. Сейчас файлы копируются в cwd, а источники берутся от ../. Это легко ломается при запуске из другого места.

    • Решение: вычислять путь к скрипту и опираться на него (или на корень репозитория), а также задавать явную папку назначения.
  • Отсутствуют строгие флаги выполнения. Если какой-то исходный файл отсутствует, cp завершится с ошибкой, но остальные команды все равно пойдут. Лучше «падать» сразу и объяснять причину.

    • Решение: set -e, set -u, set -o pipefail; плюс явная проверка существования файла с понятным сообщением.
  • Дублирование кода. 12 одинаковых cp-команд хуже поддерживать, больше риск опечаток.

    • Решение: хранить пары источник→назначение в структуре (ассоциативный массив или файл-манифест) и пройтись циклом.
  • Жестко зашиты конкретные модели устройств в именах файлов. После обновления устройств/симуляторов имена могут измениться — скрипт сломается.

    • Решение: использовать шаблоны/glob’ы и выбирать последний подходящий файл (в zsh это просто), либо параметризовать модель/локаль.
    • Пример: брать «самый новый» матч для iPhone TodayScreen, не завися от конкретной модели.
  • Несогласованность «framed» vs «non-framed». Для mobile — framed, для desktop/watch — нет. Возможно, так задумано; если нет — лучше унифицировать или явно прокомментировать.

  • Портируемость шебанга. Если zsh не гарантирован по пути /bin/zsh, используйте /usr/bin/env zsh.

  • Локаль зашита en-US во всех путях. Имеет смысл вынести в переменную, чтобы можно было собрать картинки для другой локали без правки скрипта.

Предложенный набросок рефакторинга (zsh), кратко и по делу:

#!/usr/bin/env zsh

set -e
set -u
set -o pipefail

Абсолютные пути

SCRIPT_DIR=${0:A:h}
REPO_ROOT=${SCRIPT_DIR:A}/.. # при необходимости скорректируйте
SRC="$REPO_ROOT/screenshots"
OUT="${SCRIPT_DIR}" # или параметром: OUT=${1:-$SCRIPT_DIR}
LOCALE="${LOCALE:-en-US}"

mkdir -p "$OUT"

copy() {
local src="$1" dst="$2"
if [[ ! -e "$src" ]]; then
print -u2 "Ошибка: отсутствует файл: $src"
exit 1
fi
cp -f "$src" "$OUT/$dst"
}

Вариант 1: точные пути (если их надо сохранить)

typeset -A MAP=(
"$SRC/mobile/$LOCALE/iPad Pro (12.9-inch) (4th generation)-04ProjectView_framed.png" ipad-project.png
"$SRC/mobile/$LOCALE/iPad Pro (12.9-inch) (4th generation)-02SectionsPanel_framed.png" ipad-section.png
"$SRC/mobile/$LOCALE/iPad Pro (12.9-inch) (4th generation)-01TodayScreen_framed.png" ipad-today.png
"$SRC/mobile/$LOCALE/iPhone 14 Pro Max-04ProjectView_framed.png" iphone-project.png
"$SRC/mobile/$LOCALE/iPhone 14 Pro Max-02SectionsPanel_framed.png" iphone-section.png
"$SRC/mobile/$LOCALE/iPhone 14 Pro Max-01TodayScreen_framed.png" iphone-today.png
"$SRC/desktop/$LOCALE/Apple Macbook Pro 13 Space Gray-04ProjectView.png" mac-project.png
"$SRC/desktop/$LOCALE/Apple Macbook Pro 13 Space Gray-06FocusTimerView.png" mac-timer.png
"$SRC/desktop/$LOCALE/Apple Macbook Pro 13 Space Gray-01TodayScreen.png" mac-today.png
"$SRC/watch/$LOCALE/Apple Watch Series 11 (46mm)-02SectionsPanel.png" watch-section.png
"$SRC/watch/$LOCALE/Apple Watch Series 11 (46mm)-01TodayScreen.png" watch-today.png
"$SRC/watch/$LOCALE/Apple Watch Series 11 (46mm)-03TaskDetails.png" watch-menu.png
)

for src dst in ${(kv)MAP}; do
copy "$src" "$dst"
done

Вариант 2 (опционально): менее хрупко — шаблоны и «последний» матч в zsh

latest() возвращает один файл по паттерну или падает с ошибкой

latest() {
local pattern="$1"

(N) — не ошибаться при отсутствии, (Om) — сортировка по времени убыв., [1] — первый (самый новый)

local m=(${~pattern}(NOm[1]))
[[ ${#m} -eq 1 ]] || { print -u2 "Не найден ни один файл по паттерну: $pattern"; exit 1; }
print -- "$m[1]"
}

Пример использования:

copy "$(latest "$SRC/mobile/$LOCALE/iPhone *-01TodayScreen_framed.png")" iphone-today.png

Коротко о профите от такого рефакторинга:

  • Скрипт воспроизводим из любого места (не зависит от cwd).
  • Падает с понятной ошибкой при отсутствии исходников.
  • Легче поддерживать/расширять: добавления — это одна строка в MAP.
  • Можно быстро переключать локаль (LOCALE=de-DE ./script.zsh) или менять папку назначения (./script.zsh ./out).
  • Опционально — снижена хрупкость к изменениям названий устройств за счет шаблонов.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ниже — только те замечания, которые могут реально укусить в рантайме или при интеграции.

Структура и совместимость

  • Ключ default в JSON неудобен для Swift-модели: в Swift это ключевое слово, его придётся экранировать как default или маппить через CodingKeys. Лучше переименовать в defaults/global для читаемости и меньшего шанса на ошибки.
  • Добавьте явное поле version в корне конфига. Это позволит эволюционировать схему без хрупких проверок.
  • Определите и документируйте семантику data[].filter: это строгое совпадение, glob или regex? Если regex — укажите флаг (например, "filter_type": "regex") или используйте единый формат (glob), чтобы убрать неоднозначность.
  • Сейчас все элементы data содержат только filter. Если предполагаются пер-итерационные переопределения, лучше заложить механизм мерджа и описать это явно (например, любые поля из default можно переопределить на уровне item).

Платформы и устройства

  • use_platform: "ANY" в комбинации с force_device_type: "MacBook" — противоречиво. Если насильно выбираете девайс, платформа уже не “ANY”. Сделайте:
    • platform: "macOS" | "iOS" | "watchOS" | "visionOS" | "all"
    • device: конкретная модель/пресет (например, "MacBookPro-14-2023"). И валидацию: если есть device — platform должен быть совместим.
  • show_complete_frame: true — термин “complete_frame” может пониматься по‑разному (рамка девайса? еще и тень/фон?). Лучше “showDeviceFrame” или два флага: showDeviceFrame, showBackground.

Ресурсы и пути

  • Относительные пути "./Fonts/..." и "./blue.jpg" ненадежны: рабочая директория в рантайме и на CI не гарантирована. Рекомендуется:
    • класть ресурсы в бандл и резолвить через Bundle.url(forResource:withExtension:subdirectory:),
    • либо использовать asset catalog (изображения/цвета), тогда в JSON хранить логические имена, а не файловые пути.
  • Проверьте наличие и доступность ресурсов при старте (fail-fast): валидатор должен проверять существование файла шрифта и изображения и выдавать понятную ошибку до рендера.

Шрифты и лицензии

  • SF Pro Rounded — проприетарный шрифт Apple. Встраивание OTF в дистрибутив приложения часто нарушает лицензию. Если это для продакшен‑приложения, лучше использовать системный шрифт с design: .rounded в SwiftUI (.system(size:, weight:, design: .rounded)) вместо внешнего .otf.
  • Если всё-таки нужен кастомный .otf локально (например, для генерации скриншотов в туле, не поставляемом пользователю), регистрируйте шрифт один раз per process (CTFontManagerRegisterFontsForURL) и кешируйте результат. Не регистрируйте повторно для каждого item.

Цвета и форматирование

  • "#FFFFFF" не парсится стандартными инициализаторами Color/NSColor. Нужен свой парсер или используйте цветовые ассеты. Если остаётесь на hex — поддержите #RRGGBB и #RRGGBBAA, чтобы можно было управлять прозрачностью текста.
  • Рассмотрите хранение цветов по ключам (semantic tokens) вместо “жестких” hex — проще поддерживать тему/брендинг.

Единицы измерения и масштаб

  • padding: 50 — не ясно, это поинты или пиксели. Зафиксируйте единицу. Для рендеринга маркетинговых изображений лучше оперировать поинтами и умножать на scale девайса. Добавьте scale policy или поле outputScale.
  • Фон-изображение для макетов лучше хранить с нужным разрешением/аспектом. В противном случае добавьте fit policy (aspectFit/aspectFill) и позиционирование.

Надёжность/валидация

  • Добавьте предзагрузочную валидацию:
    • неизвестные поля → warning (или ошибка в strict‑режиме),
    • проверка доменных значений (platform/device/boolean),
    • проверка конфликтов (ANY + force_device_type).
  • Полезно описать JSON Schema (например, draft-07) и гонять конфиг через валидацию в CI.

Моделирование в Swift (минимум для устойчивого парсинга)

  • Используйте keyDecodingStrategy = .convertFromSnakeCase, чтобы show_complete_frame превратился в showCompleteFrame.
  • Явные enum’ы для платформ и устройств с rawValue и валидацией совместимости.
  • Исключите зарезервированные имена:
    • JSON: "defaults"
    • Swift: struct Config { let defaults: Defaults; let data: [Item] }

Семантика заголовка

  • title задан шрифтом и цветом, но нет размера/макета. Если размер не фиксируется специально — добавьте size/lineHeight/align, иначе поведение может отличаться между платформами/рендерами.
  • Если title текст берётся извне — укажите fallback’и (например, если локализация недоступна).

Сортировка и фильтры

  • Префиксы "01…06" удобны визуально, но ломаются при "10" (лексикографическая сортировка). Если порядок важен — добавьте явное поле order: Int в каждом элементе.

Итого — что бы я поменял в этом PR сразу

  • Переименовал "default" → "defaults".
  • Заменил use_platform/force_device_type на platform/device с явными enum и валидацией.
  • Перевёл пути ресурсов из относительных в логические имена + резолв из бандла/ассетов.
  • Убрал прямой шрифт SF Pro Rounded из поставки; переключился на системный rounded дизайн либо оставил OTF только для внутреннего инструмента.
  • Добавил поле version и простую предвальную валидацию (существование файлов, согласованность полей, парсинг цветов).
  • Зафиксировал формат цвета (hex с опциональным альфа) и единицы padding (points).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ниже — только по делу.

Ключевые проблемы и риски

  • Логика формирования newname неверна относительно комментариев. Конструкция
    newname="${file%-}-${file#-}"
    дублирует сегмент вокруг дефисов и не «убирает всё между первым и последним дефисом». Пример: "a-b-c_1.png" → "a-b-b-c_1.png".
  • Комментарий «до первого подчеркивания с цифрой» не соответствует коду: вы режете по первому "_" вообще, без проверки цифры.
  • Ошибка при отсутствии *.png в каталоге (zsh по умолчанию с опцией NOMATCH упадёт). Нужен null_glob или соответствующий квалификатор.
  • Возможны коллизии имён и перезапись файлов. mv без -n перезапишет существующее.
  • Дублирование кода для en-US и ru.
  • Необработанные частные случаи: файлы без дефисов/подчёркиваний; верхний регистр расширения; когда новое имя совпадает со старым.
  • Скрипт зависит от cd; лучше ограничить область (pushd/popd) и явно фейлиться на ошибках.
  • В конце файла нет завершающей новой строки (замечание из diff).

Предложение по исправлению логики
Если цель действительно:

  • отбросить «хвост» начиная с «подчёркивание + цифра»;
  • и сжать имя «между первым и последним дефисом» (то есть "A-B-C_*.png" → "A-C.png"),

то корректно и безопасно так (чистый zsh):

#!/bin/zsh
set -euo pipefail
setopt null_glob

Изолируем поведение от пользовательских опций

emulate -L zsh

for dir in en-US ru; do
[[ -d "$dir" ]] || { print -u2 "Пропуск: нет каталога $dir"; continue; }
pushd "$dir" > /dev/null

Поддержка .png и .PNG

for file in -- .(#i)png(.N); do
# Обрезаем только если '' за которым идут цифры (подчёркивание с цифрой)
base=${file%%
<->
}
# Если такой '_' нет — остаётся весь file без «цифрового хвоста»
[[ "$base" == "$file" ]] && base=${file%.*}

if [[ "$base" == *-* ]]; then
  left=${base%%-*}   # до первого дефиса
  right=${base##*-}  # после последнего дефиса
  newname="${left}-${right}.png"
else
  newname="${base}.png"
fi

# Пропуск, если имя не меняется
[[ "$newname" == "$file" ]] && continue

# Защита от коллизий
if [[ -e "$newname" ]]; then
  print -u2 "Конфликт имён: '$file' -> '$newname' (уже существует). Пропуск."
  continue
fi

# Если используете git — предпочтительнее git mv
if command -v git >/dev/null 2>&1 && git rev-parse --is-inside-work-tree >/dev/null 2>&1 && git ls-files --error-unmatch -- "$file" >/dev/null 2>&1; then
  git mv -v -- "$file" "$newname"
else
  mv -v -n -- "$file" "$newname"
fi

done

popd > /dev/null
done

Почему так лучше

  • Правильное соответствие требованиям: «подчёркивание с цифрой» обрабатывается через zsh-паттерн <->, а «сжатие между дефисами» — через %% / ##.
  • Нет падения при отсутствии файлов (*.png(.N) + setopt null_glob).
  • Нет перезаписей (mv -n) и есть защита от конфликтов.
  • Нет дублирования кода по каталогам, меньше шансов на расхождение логики.
  • Работает и с .PNG.
  • pushd/popd и set -euo pipefail делают скрипт предсказуемее.
  • Не трогаем файлы, если имя бы не изменилось.

Если же требование «сжимать между дефисами» вам не нужно, а нужно только «отрезать по первому '' (или по '' с цифрой)», упрощайте до:
base=${file%%<->*} # или ${file%%} если не требуется проверка на цифру
newname="${base%.
}.png"

И последнее: добавьте перевод строки в конец файла. Это хорошая практика и устраняет предупреждения diff/линтеров.

Copy link
Copy Markdown

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review by ChatGPT

OPENAI_API_ENDPOINT: https://api.proxyapi.ru/openai/v1
MODEL: gpt-5
PROMPT: "You are an experienced Swift/SwigtUI developer and your job is to review pull requests. Please review the following code for any misunderstandings or violations. Don't spend time commenting on what is already working perfectly. I'm looking for constructive criticism and suggestions for improving the code, only useful and thorough notes"
PROMPT: "Вы опытный разработчик Swift/SwiftUI, и ваша задача — проверять запросы на включение изменений. Пожалуйста, проверьте следующий код на наличие недопониманий или нарушений. Не тратьте время на комментарии того, что и так работает идеально. Я жду конструктивной критики и предложений по улучшению кода, только полезных и содержательных замечаний."
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ниже только полезные замечания по диффу (локализация PROMPT на русский) и предложения по улучшению.

Что можно улучшить прямо сейчас

  • Формулировка: «запросы на включение изменений» звучит неестественно. Лучше: «пул‑реквесты (PR)» или «pull requests». Это снизит двусмысленность.
  • Согласованность языка: у вас есть LANGUAGE: Russian и русскоязычный PROMPT. Либо:
    • оставить LANGUAGE и сделать PROMPT нейтральным, используя LANGUAGE в коде; либо
    • убрать LANGUAGE и поручить контроль языка PROMPT’у; либо
    • явно указать в PROMPT «Отвечай на русском».
  • Кавычки/кодировка: PROMPT в двойных кавычках с длинным тире и кириллицей — ок, но безопаснее и удобнее редактировать как многострочный блок YAML. Это исключит случайные проблемы с экранированием и упростит дальнейшие правки.
  • Структура воркфлоу: проверьте уровень, на котором объявлены env. Сейчас фрагмент выглядит как env внутри job/step. Если эти переменные нужны во всём воркфлоу, лучше вынести в верхнеуровневый env:. Если только для шага — поместить под конкретный steps[*].env.
  • MODEL: gpt-5 — проверьте доступность модели на указанном эндпоинте. Если это кастом у провайдера — ок. Иначе добавьте:
    • возможность переопределения через workflow inputs/vars/secrets;
    • fallback-модель;
    • матрицу для тестов разных моделей.
  • Управление качеством ответов: текущий PROMPT описывает роль, но не формат и критерии. Добавьте требования к выходному формату и фокусу, чтобы уменьшить «воду» и обеспечить практичность: краткость, приоритизация, конкретика с примерами, просьба запрашивать недостающий контекст.

Предлагаемый вариант PROMPT (более управляемый)
Используйте многострочный блок и четкий формат:

PROMPT: |
Ты опытный разработчик Swift/SwiftUI. Твоя задача — делать ревью пул‑реквестов.
Правила:

  • Пиши на русском, кратко и по делу.
  • Не описывай то, что уже работает корректно.
  • Фокус: архитектура, корректность, производительность, читаемость, безопасность, тестопригодность, соответствие Swift/SwiftUI best practices.
  • Давай конкретные правки и примеры кода.
  • Структура ответа:
    1. Критические проблемы (с объяснением и как исправить)
    2. Замечания средней важности
    3. Неблокирующие улучшения
  • Если контекста не хватает, сначала уточни вопросы.
  • Не делай общих советов без привязки к коду.

Технические мелочи YAML

  • Предпочтительно одинарные кавычки для простых однострочных строк без необходимости интерполяций, либо многострочный блок | / > для длинных текстов.
  • Убедитесь, что репозиторий в UTF‑8 (GitHub по умолчанию — да) — длинное тире и кириллица тогда безопасны.

Опционально

  • Вынесите OPENAI_API_ENDPOINT и MODEL в vars/inputs для разных окружений (dev/stage/prod).
  • Добавьте ограничение длины ответа в PROMPT (например, «до 300–500 слов») для контроля расходов токенов.

Итог: локализация корректна, но я рекомендую уточнить терминологию («пул‑реквесты»), формализовать формат ответа, использовать многострочный PROMPT и проверить доступность модели gpt-5 и уровень объявления env.


screenshots/desktop/*.html
screenshots/desktop/ru/*.png
screenshots/desktop/en-US/*.png
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ниже только содержательные замечания по диффу .gitignore — на что обратить внимание и как улучшить.

  • Podfile.lock

    • Убрали из .gitignore — это ошибка для проектов на CocoaPods. Под файл lock обязательно коммитится для детерминированных сборок. Иначе разные разработчики/CI получат разные версии зависимостей.
    • Если вы реально ушли с CocoaPods на SPM — тогда уберите и Pods/ из .gitignore и вообще все, что связано с CocoaPods, чтобы не путать.
  • GoogleService-Info.plist

    • Исключение из .gitignore означает, что файл попадёт в репозиторий. Для Firebase это обычно допустимо, но убедитесь:
      • Есть разделение по схемам/таргетам (Debug/Staging/Release) с разными plist.
      • В plist нет приватных секретов, которые нельзя публиковать.
      • Альтернатива: держать плейсхолдер в репо и подменять реальный файл через CI или шифровать (git-crypt/sops).
  • TestResults.xcresult, TestResults/, TestResults*

    • Паттерн TestResults* избыточно широкий: может случайно скрыть несвязанные файлы/директории с таким префиксом. Заанкорьте и сузьте.
    • .xcresult — это пакет (директория). Универсальней игнорить по маске: *.xcresult (лучше с анкором).
    • Дублирование TestResults.xcresult, TestResults/ и TestResults* — оставьте один понятный вариант.
    • Рекомендация:
      • Добавить: /*.xcresult
      • Если нужен отдельный каталог для артефактов тестов: /TestResults/ (и убрать TestResults*).
  • Скриншоты: переход на screenshots/mobile|watch|desktop

    • Жёстко прописанные локали (ru, en-US) лучше заменить на маски — чтобы не править .gitignore при добавлении новых локалей:
      • /screenshots/**/ *.png
      • /screenshots/**/ *.html
      • /screenshots/**/ *.mp4 (если видео есть не только на mobile — добавьте для watch/desktop тоже).
    • Заанкорьте пути от корня, чтобы не игнорить случайно одноимённые директории в зависимостях: начинайте с /
    • Если это артефакты fastlane, подумайте о выносе в стандартный путь fastlane/screenshots — меньше кастомной логики.
  • Package.resolved

    • Сейчас у вас он игнорится. Для SPM его лучше коммитить, чтобы зафиксировать версии пакетов и иметь воспроизводимые сборки в команде/CI. Рекомендация — убрать Package.resolved из .gitignore.
  • Прочие типичные артефакты Xcode/SPM, которых не хватает

    • Добавьте (если релевантно вашему процессу):
      • /DerivedData/
      • /.build/ (SPM)
      • /build/
      • *.xcarchive
      • *.ipa
      • *.dSYM
      • .DS_Store

Предлагаемый фрагмент для упрощения и повышения надёжности (адаптируйте под ваш проект):

  • Зафиксировать зависимости:
    • удалить из .gitignore: Package.resolved
    • вернуть в репозиторий: Podfile.lock (если остались на CocoaPods)
  • Сужение и анкоринг путей:
    • /*.xcresult
    • /TestResults/
    • /screenshots/**.png
    • /screenshots/**.html
    • /screenshots/**.mp4
  • Базовые артефакты:
    • /DerivedData/
    • /.build/
    • /build/
    • *.xcarchive
    • *.ipa
    • *.dSYM
    • .DS_Store

Итого:

  • Верните Podfile.lock (или полностью выпилите все следы CocoaPods, если не используете).
  • Не игнорьте Package.resolved — коммитьте его.
  • Унифицируйте игнор тестовых артефактов через *.xcresult и уберите дубли.
  • Обобщите маски для скриншотов и заанкорьте пути от корня.
  • Проверьте политику по GoogleService-Info.plist и осознанность его коммита.

doCompletion: name.completion)
dataContainer.mainContext.insert(status)
project.statuses?.append(status)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Хорошее исправление опечатки, но есть несколько потенциальных проблем и зон для улучшения:

  • Опциональная связь и append:

    • project.statuses?.append(status) ничего не сделает, если statuses == nil. В результате объект не будет привязан к проекту. Лучше:
      • Сделать связь не-опциональной: var statuses: [Status] = []
      • Или инициализировать перед использованием: project.statuses = (project.statuses ?? []) + [status]
  • Тип хранилища (Core Data vs SwiftData):

    • Если это SwiftData: массив связи должен быть не-опциональным и append корректен.
    • Если это Core Data: append подозрителен. Используйте сгенерированные аксессоры (addToStatuses) или mutableSetValue(forKey:), чтобы гарантировать корректную установку связи и инверсии.
  • Нейминг doCompletion:

    • Имя не по гайдлайнам Swift. Для Bool используйте префиксы is/has/should. Например: requiresCompletion, isTerminal, makesItemCompleted. Это повысит читаемость и снизит двусмысленность.
  • Потенциальный off-by-one в order:

    • order += 1 перед использованием сдвигает индексацию. Если нужна 0-базовая, используйте enumerated():
      • for (index, name) in names.enumerated() { order = index }
    • Либо привязывайте порядок к project.statuses.count вместо внешнего счетчика — меньше шансов на рассинхрон.
  • Локализация в тестах:

    • name.localizedString() в фикстурах делает тесты зависимыми от локали окружения. Лучше использовать стабильное значение (rawValue/identifier) в тестах и локализовывать только в UI.
  • Инициализация и вставка в контекст:

    • Убедитесь, что удобный init Status(...) создает объект корректно для вашего стека. В Core Data корректно вызывать init(entity:insertInto: nil) и затем context.insert(status), а в SwiftData — просто context.insert(status). Избегайте смешения паттернов.
  • Согласованность имен:

    • Вы поменяли name.competion -> name.completion. Проверьте, что completion по смыслу не конфликтует с возможным свойством completed у Status. Если это «требуется завершение» для данного типа статуса, лучше назвать name.requiresCompletion.

Предлагаемый вариант (SwiftData-подход, безопасный к nil и индексации):

  • for (index, name) in names.enumerated() {
    let status = Status(name: name.localizedString(), order: index, requiresCompletion: name.requiresCompletion)
    context.insert(status)
    project.statuses.append(status) // statuses: [Status] = []
    }

Для Core Data:

  • for (index, name) in names.enumerated() {
    let status = Status(context: context)
    status.name = name.localizedString()
    status.order = Int16(index)
    status.requiresCompletion = name.requiresCompletion
    project.addToStatuses(status)
    }

let _ = Previewer(container!)

ContentView(selectedSideBarItem: $selectedSidebarItem,
selectedProject: $selectedProject)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ниже только по делу — что стоит поправить или улучшить.

  • Логика DragGesture сейчас некорректна: вы прибавляете gesture.translation.height к уже изменяемому sectionHeight на каждом onChanged. Translation задаётся относительно начала жеста, поэтому при каждом изменении вы повторно добавляете «начальный + дельта», но «начальный» у вас уже изменён. Итог — “уползание” высоты. Решение: запоминайте высоту на старте жеста и считайте относительно неё.
    Пример:

    • Вариант 1 (сразу пишем в AppStorage):
      @State private var dragStartHeight: CGFloat?

      DragGesture()
      .onChanged { value in
      if dragStartHeight == nil { dragStartHeight = CGFloat(sectionHeight) }
      let proposed = (dragStartHeight ?? 0) + value.translation.height
      sectionHeight = Double(min(max(proposed, Self.minHeight), Self.maxHeight))
      }
      .onEnded { _ in dragStartHeight = nil }

    • Вариант 2 (плавнее и без записи в UserDefaults на каждом кадре):
      @State private var liveHeight: CGFloat = 300
      .onAppear { liveHeight = CGFloat(sectionHeight).clamped(Self.minHeight...Self.maxHeight) }
      DragGesture()
      .onChanged { value in
      if dragStartHeight == nil { dragStartHeight = liveHeight }
      liveHeight = ((dragStartHeight ?? 0) + value.translation.height)
      .clamped(Self.minHeight...Self.maxHeight)
      }
      .onEnded { _ in
      sectionHeight = Double(liveHeight) // единственная запись в AppStorage
      dragStartHeight = nil
      }

  • Не пишите в AppStorage на каждом onChanged. Это UserDefaults — частые записи избыточны и могут “шуметь” по перформансу. Лучше держать liveHeight в @State и коммитить в AppStorage в onEnded (см. вариант 2 выше).

  • Приведения типов Double <-> CGFloat разбросаны по коду. Для читаемости сделайте прокси:
    private var sectionHeightCG: CGFloat {
    get { CGFloat(sectionHeight) }
    set { sectionHeight = Double(newValue) }
    }
    Тогда frame(height:) и расчёты будут работать с CGFloat, а хранение останется в Double.

  • Улучшите UX перетаскивания:

    • Добавьте .frame(maxWidth: .infinity) и .contentShape(Rectangle()) у «ручки», чтобы увеличить зону хита. 5pt почти не попадётся.
    • На macOS добавьте .cursor(.resizeUpDown) и небольшой padding по вертикали.
    • По желанию: .animation(.interactiveSpring(), value: liveHeight) для сглаживания.
  • Границы высоты лучше вынести в константы, чтобы не дублировать «магические числа»:
    private static let minHeight: CGFloat = 60
    private static let maxHeight: CGFloat = 300

  • Проверьте семантику хранилища:

    • Если высота должна отличаться между окнами/сценами — используйте @SceneStorage вместо @AppStorage.
    • Если кросс-девайс синхронизация не нужна — оставьте как есть; если нужна — подумайте об iCloud/NSUbiquitous, но это уже вне текущего scope.
  • В превью:

    • Вы сменили let previewer = Previewer(container!) на let _ = Previewer(container!). Если Previewer выполняет только сайд-эффект (сидинговка) и не должен жить — ок. Если он держит какие-то подписки/наблюдения — вы их потеряете. Лучше сделать явный статический сидер: Previewer.seed(container) без необходимости хранить инстанс.
    • Избегайте force unwrap в превью. Если container не собрался — превью упадёт. Сделайте guard:
      guard let container = try? ModelContainer(...) else { return Text("Preview setup failed") }
      Previewer.seed(container)
      return ContentView(...)
  • Защита от «битых» значений в сторадже: при запуске можно нормализовать sectionHeight к допустимому диапазону (например, в .onAppear). Это спасёт, если в UserDefaults окажется некорректное число.

  • Мелочь по API: ключ "SectionHeight" лучше неймспейсить, чтобы избежать коллизий: "ContentView.SectionHeight" или вынести в enum Keys.

Опционально:

  • Подумайте, не завязать maxHeight на доступное пространство через GeometryReader, вместо жёстких 300, чтобы не ломать лэйаут на маленьких окнах.
  • Добавьте двойной клик по «ручке» для сброса к дефолту, если это уместно для UX.

Image(systemName: "xmark.square")
}
}
.padding(10)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Коротко и по делу.

  • Исправление по существу: clear — несуществующий SF Symbol. Переход на xmark.square устраняет проблему с иконкой. Однако для действия “очистить/сбросить” в iOS более привычны:

    • xmark.circle или xmark.circle.fill (универсальный “очистить/закрыть”),
    • multiply.circle.fill — часто используется как кнопка очистки (например, в текстфилдах).
      Если действие ближе к “удалить”, используйте trash(/.fill).
  • Доступность: простая Image лишает пользователей VoiceOver контекста.

    • Замените на Label("Очистить", systemImage: "xmark.circle") + .labelStyle(.iconOnly)
    • Или добавьте .accessibilityLabel("Очистить задачу") и, при необходимости, .accessibilityHint("Сбрасывает выбранную задачу").
  • Хит-таргет: .padding(10) может быть меньше рекомендуемых 44×44.

    • Обеспечьте минимум: .frame(minWidth: 44, minHeight: 44) или увеличьте паддинги/используйте .contentShape(Rectangle).
  • Визуальная иерархия:

    • Если это вспомогательная/вторичная кнопка, добавьте .tint(.secondary) или .foregroundStyle(.secondary) для уменьшения визуального веса.
    • Если в списке/ячееке, задайте .buttonStyle(.borderless) или .plain для предотвращения конфликтов с выделением строки.
  • Анимация UX: если скрываете связанную вью по условию task == nil, можно обернуть в withAnimation { focusTask.task = nil } для более плавного удаления.

  • Совместимость: проверьте доступность выбранного символа под целевые iOS (xmark.circle доступен с iOS 13; если поддерживаете ниже — нужен фоллбек, хотя обычно таргет ≥ iOS 14/15).

Итог: технически смена на xmark.square корректна, но я бы предпочёл xmark.circle(.fill) или multiply.circle.fill, добавил доступность, гарантировал 44×44, и при необходимости настроил стиль/тону кнопки.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ниже — только по делу, что можно улучшить в этом скрипте.

  • Явный shell и строгий режим:

    • Добавьте shebang и строгие опции для предсказуемости ошибок:
      • В начало файла: #!/usr/bin/env bash
      • Сразу после: set -euo pipefail
    • При желании дебага — переключатель: [[ ${VERBOSE:-0} -eq 1 ]] && set -x
  • echo с \n не переносит строку портативно:

    • echo "\n..." в большинстве окружений выведет буквально \n. Замените на printf '\n🔄 Change dir to: %s\n' "$TARGET_DIR_2" и аналогично для финального сообщения.
    • В конце файла нет перевода строки — добавьте.
  • Версионная изоляция fastlane:

    • Запускайте через bundler, чтобы не ловить несовместимые глобальные версии: bundle exec fastlane snapshot / bundle exec fastlane frameit.
    • Перед запуском можно проверить наличие: command -v bundle >/dev/null || { printf 'Bundler не найден\n' >&2; exit 127; }
  • Менеджмент директорий:

    • Относительные пути (../, ../../) хрупкие. Зафиксируйте корень репозитория: REPO_ROOT="$(git rev-parse --show-toplevel)" и формируйте абсолютные TARGET_DIR_*="$REPO_ROOT/...".
    • Избегайте глобального cd: оборачивайте шаг в подшелл или используйте pushd/popd:
      • (cd "$dir" && bundle exec fastlane snapshot) — контекст каталога не «течёт» дальше.
    • Можно выделить утилиту:
      • run_in() { local d=$1; shift; printf '🔄 cd %s\n' "$d"; (cd "$d" && "$@"); }
  • Разделение артефактов iOS/watchOS:

    • Сейчас iOS кладёт в ../screenshots/mobile, а watchOS — по умолчанию (скорее всего в каталог тестов). Явно задайте в соответствующих Snapfile разные output_directory и при необходимости derived_data_path, чтобы не было конфликтов и кросс-загрязнений.
    • Если хотите чистые перезапуски, включите clear_previous_screenshots true.
  • frameit и watchOS:

    • Вы frameit запускаете только для iOS — это норм, т.к. frameit официально не поддерживает watchOS-кейсы. Оставьте как есть, но тогда явно задокументируйте это в скрипте/README, чтобы не возникало ожиданий «а где рамки для watchOS».
  • Единообразная обработка ошибок и логгирование:

    • Завести функцию die() { printf '❌ %s\n' "$*" >&2; exit 1; } и переиспользовать вместо копипасты.
    • Ошибки отправляйте в stderr (>&2), чтобы логи CI были чище.
  • Параллелизм и независимость шагов (по желанию):

    • Сейчас скрипт падает на первом фейле. Если важнее получить максимальный объём артефактов, можно запускать iOS и watchOS в отдельных подпроцессах и сводить статусы в конце.
  • Локаль/эмодзи:

    • Эмодзи в логах в не-UTF8 окружении могут ломать вывод. Если CI нестабилен по локали, либо выставьте LC_ALL=C.UTF-8, либо уберите эмодзи.

Итого минимальный смысловой патч:

  • добавить shebang + set -euo pipefail,
  • заменить echo с \n на printf,
  • вызывать fastlane через bundle exec,
  • зафиксировать абсолютные пути от корня репозитория,
  • явно указать раздельные выходные директории для iOS/watchOS в Snapfile,
  • добавить финальный перевод строки в файл.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ниже только содержательные улучшения, без банальностей.

  • Жесткий режим и корректная обработка ошибок в zsh:

    • Сейчас при падении первого запуска второй все равно выполнится (или, наоборот, скроет первый фэйл, если вернется 0). Добавьте строгий режим и аккумулирование статуса, чтобы выполнить оба запуска и вернуть общий результат.
    • Для zsh используйте:
      • set -e; set -u; set -o pipefail
    • Пример суммирования статусов:
      • status=0
      • xcodebuild … || status=$?
      • xcodebuild … || status=$?
      • exit $status
  • Явно задайте destination, чтобы избежать флаки из‑за автоподбора симулятора:

    • -destination 'platform=iOS Simulator,name=iPhone 15,OS=latest'
    • Опционально зафиксируйте OS конкретной версией, если CI образ фиксирован.
  • Результаты и артефакты:

    • Давайте .xcresult расширение: -resultBundlePath TestResults.xcresult и TestResultsRU.xcresult — удобнее открывать и обрабатывать.
    • Во избежание перезаписи результатов при многократных запусках добавьте timestamp или отдельную папку: TestResults-$(date +%Y%m%d-%H%M%S).xcresult.
    • Если нужна покрытие/отчеты в CI: добавьте -enableCodeCoverage YES и потом сливайте покрытие из двух прогонов xccov merge.
  • Производительность/детерминизм:

    • Задайте общий -derivedDataPath (например, ./DerivedData), чтобы второй прогон переиспользовал сборку.
    • Если у вас есть .xcworkspace — предпочтительнее -workspace … вместо -project … (особенно с SPM/Pods).
    • Включите параллельное тестирование, если релевантно: -parallel-testing-enabled YES.
  • Логи:

    • Для читабельности CI‑логов и сохранения кода возврата используйте пайп с xcbeautify/xcpretty и pipefail:
      • xcodebuild … | xcbeautify || status=$?
    • Либо сохраняйте «сырые» логи в файл через tee для последующего анализа.
  • Устойчивость:

    • Используйте /usr/bin/env zsh в шебанге для большей переносимости окружений.
    • Экранируйте аргументы на случай пробелов в именах: -project "PomPadDo.xcodeproj", -resultBundlePath "TestResultsRU.xcresult".
  • Организация тест‑планов:

    • Если RU/EN отличаются только локалью/языком, рассмотрите один test plan с двумя configurations и запускайте через -testPlan PomPadDo -test-configuration RU и затем EN. Это уменьшит дублирование и облегчит поддержку.
  • Управление тестовым набором:

    • Если часть тестов идентична и не зависит от локали — исключайте их во втором прогоне через -only-testing/-skip-testing, чтобы сэкономить время.

Короткий эскиз улучшенной версии:

#!/usr/bin/env zsh
set -e
set -u
set -o pipefail

DEST="platform=iOS Simulator,name=iPhone 15,OS=latest"
DD="./DerivedData"
status=0

xcodebuild test
-project "PomPadDo.xcodeproj"
-scheme "PomPadDo"
-testPlan "PomPadDo"
-destination "$DEST"
-derivedDataPath "$DD"
-enableCodeCoverage YES
-resultBundlePath "TestResults.xcresult" || status=$?

xcodebuild test
-project "PomPadDo.xcodeproj"
-scheme "PomPadDo"
-testPlan "PomPadDoRU"
-destination "$DEST"
-derivedDataPath "$DD"
-enableCodeCoverage YES
-resultBundlePath "TestResultsRU.xcresult" || status=$?

exit $status

Выше — только то, что улучшает надежность, воспроизводимость и удобство CI/аналитики.

cp -f ../screenshots/desktop/en-US/"Apple Macbook Pro 13 Space Gray-01TodayScreen.png" mac-today.png
cp -f ../screenshots/watch/en-US/"Apple Watch Series 11 (46mm)-02SectionsPanel.png" watch-section.png
cp -f ../screenshots/watch/en-US/"Apple Watch Series 11 (46mm)-01TodayScreen.png" watch-today.png
cp -f ../screenshots/watch/en-US/"Apple Watch Series 11 (46mm)-03TaskDetails.png" watch-menu.png
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ниже — только потенциально проблемные места и улучшения по существу.

  • Хрупкие относительные пути и зависимость от текущей директории. Сейчас файлы копируются в cwd, а источники берутся от ../. Это легко ломается при запуске из другого места.

    • Решение: вычислять путь к скрипту и опираться на него (или на корень репозитория), а также задавать явную папку назначения.
  • Отсутствуют строгие флаги выполнения. Если какой-то исходный файл отсутствует, cp завершится с ошибкой, но остальные команды все равно пойдут. Лучше «падать» сразу и объяснять причину.

    • Решение: set -e, set -u, set -o pipefail; плюс явная проверка существования файла с понятным сообщением.
  • Дублирование кода. 12 одинаковых cp-команд хуже поддерживать, больше риск опечаток.

    • Решение: хранить пары источник→назначение в структуре (ассоциативный массив или файл-манифест) и пройтись циклом.
  • Жестко зашиты конкретные модели устройств в именах файлов. После обновления устройств/симуляторов имена могут измениться — скрипт сломается.

    • Решение: использовать шаблоны/glob’ы и выбирать последний подходящий файл (в zsh это просто), либо параметризовать модель/локаль.
    • Пример: брать «самый новый» матч для iPhone TodayScreen, не завися от конкретной модели.
  • Несогласованность «framed» vs «non-framed». Для mobile — framed, для desktop/watch — нет. Возможно, так задумано; если нет — лучше унифицировать или явно прокомментировать.

  • Портируемость шебанга. Если zsh не гарантирован по пути /bin/zsh, используйте /usr/bin/env zsh.

  • Локаль зашита en-US во всех путях. Имеет смысл вынести в переменную, чтобы можно было собрать картинки для другой локали без правки скрипта.

Предложенный набросок рефакторинга (zsh), кратко и по делу:

#!/usr/bin/env zsh

set -e
set -u
set -o pipefail

Абсолютные пути

SCRIPT_DIR=${0:A:h}
REPO_ROOT=${SCRIPT_DIR:A}/.. # при необходимости скорректируйте
SRC="$REPO_ROOT/screenshots"
OUT="${SCRIPT_DIR}" # или параметром: OUT=${1:-$SCRIPT_DIR}
LOCALE="${LOCALE:-en-US}"

mkdir -p "$OUT"

copy() {
local src="$1" dst="$2"
if [[ ! -e "$src" ]]; then
print -u2 "Ошибка: отсутствует файл: $src"
exit 1
fi
cp -f "$src" "$OUT/$dst"
}

Вариант 1: точные пути (если их надо сохранить)

typeset -A MAP=(
"$SRC/mobile/$LOCALE/iPad Pro (12.9-inch) (4th generation)-04ProjectView_framed.png" ipad-project.png
"$SRC/mobile/$LOCALE/iPad Pro (12.9-inch) (4th generation)-02SectionsPanel_framed.png" ipad-section.png
"$SRC/mobile/$LOCALE/iPad Pro (12.9-inch) (4th generation)-01TodayScreen_framed.png" ipad-today.png
"$SRC/mobile/$LOCALE/iPhone 14 Pro Max-04ProjectView_framed.png" iphone-project.png
"$SRC/mobile/$LOCALE/iPhone 14 Pro Max-02SectionsPanel_framed.png" iphone-section.png
"$SRC/mobile/$LOCALE/iPhone 14 Pro Max-01TodayScreen_framed.png" iphone-today.png
"$SRC/desktop/$LOCALE/Apple Macbook Pro 13 Space Gray-04ProjectView.png" mac-project.png
"$SRC/desktop/$LOCALE/Apple Macbook Pro 13 Space Gray-06FocusTimerView.png" mac-timer.png
"$SRC/desktop/$LOCALE/Apple Macbook Pro 13 Space Gray-01TodayScreen.png" mac-today.png
"$SRC/watch/$LOCALE/Apple Watch Series 11 (46mm)-02SectionsPanel.png" watch-section.png
"$SRC/watch/$LOCALE/Apple Watch Series 11 (46mm)-01TodayScreen.png" watch-today.png
"$SRC/watch/$LOCALE/Apple Watch Series 11 (46mm)-03TaskDetails.png" watch-menu.png
)

for src dst in ${(kv)MAP}; do
copy "$src" "$dst"
done

Вариант 2 (опционально): менее хрупко — шаблоны и «последний» матч в zsh

latest() возвращает один файл по паттерну или падает с ошибкой

latest() {
local pattern="$1"

(N) — не ошибаться при отсутствии, (Om) — сортировка по времени убыв., [1] — первый (самый новый)

local m=(${~pattern}(NOm[1]))
[[ ${#m} -eq 1 ]] || { print -u2 "Не найден ни один файл по паттерну: $pattern"; exit 1; }
print -- "$m[1]"
}

Пример использования:

copy "$(latest "$SRC/mobile/$LOCALE/iPhone *-01TodayScreen_framed.png")" iphone-today.png

Коротко о профите от такого рефакторинга:

  • Скрипт воспроизводим из любого места (не зависит от cwd).
  • Падает с понятной ошибкой при отсутствии исходников.
  • Легче поддерживать/расширять: добавления — это одна строка в MAP.
  • Можно быстро переключать локаль (LOCALE=de-DE ./script.zsh) или менять папку назначения (./script.zsh ./out).
  • Опционально — снижена хрупкость к изменениям названий устройств за счет шаблонов.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ниже — только те замечания, которые могут реально укусить в рантайме или при интеграции.

Структура и совместимость

  • Ключ default в JSON неудобен для Swift-модели: в Swift это ключевое слово, его придётся экранировать как default или маппить через CodingKeys. Лучше переименовать в defaults/global для читаемости и меньшего шанса на ошибки.
  • Добавьте явное поле version в корне конфига. Это позволит эволюционировать схему без хрупких проверок.
  • Определите и документируйте семантику data[].filter: это строгое совпадение, glob или regex? Если regex — укажите флаг (например, "filter_type": "regex") или используйте единый формат (glob), чтобы убрать неоднозначность.
  • Сейчас все элементы data содержат только filter. Если предполагаются пер-итерационные переопределения, лучше заложить механизм мерджа и описать это явно (например, любые поля из default можно переопределить на уровне item).

Платформы и устройства

  • use_platform: "ANY" в комбинации с force_device_type: "MacBook" — противоречиво. Если насильно выбираете девайс, платформа уже не “ANY”. Сделайте:
    • platform: "macOS" | "iOS" | "watchOS" | "visionOS" | "all"
    • device: конкретная модель/пресет (например, "MacBookPro-14-2023"). И валидацию: если есть device — platform должен быть совместим.
  • show_complete_frame: true — термин “complete_frame” может пониматься по‑разному (рамка девайса? еще и тень/фон?). Лучше “showDeviceFrame” или два флага: showDeviceFrame, showBackground.

Ресурсы и пути

  • Относительные пути "./Fonts/..." и "./blue.jpg" ненадежны: рабочая директория в рантайме и на CI не гарантирована. Рекомендуется:
    • класть ресурсы в бандл и резолвить через Bundle.url(forResource:withExtension:subdirectory:),
    • либо использовать asset catalog (изображения/цвета), тогда в JSON хранить логические имена, а не файловые пути.
  • Проверьте наличие и доступность ресурсов при старте (fail-fast): валидатор должен проверять существование файла шрифта и изображения и выдавать понятную ошибку до рендера.

Шрифты и лицензии

  • SF Pro Rounded — проприетарный шрифт Apple. Встраивание OTF в дистрибутив приложения часто нарушает лицензию. Если это для продакшен‑приложения, лучше использовать системный шрифт с design: .rounded в SwiftUI (.system(size:, weight:, design: .rounded)) вместо внешнего .otf.
  • Если всё-таки нужен кастомный .otf локально (например, для генерации скриншотов в туле, не поставляемом пользователю), регистрируйте шрифт один раз per process (CTFontManagerRegisterFontsForURL) и кешируйте результат. Не регистрируйте повторно для каждого item.

Цвета и форматирование

  • "#FFFFFF" не парсится стандартными инициализаторами Color/NSColor. Нужен свой парсер или используйте цветовые ассеты. Если остаётесь на hex — поддержите #RRGGBB и #RRGGBBAA, чтобы можно было управлять прозрачностью текста.
  • Рассмотрите хранение цветов по ключам (semantic tokens) вместо “жестких” hex — проще поддерживать тему/брендинг.

Единицы измерения и масштаб

  • padding: 50 — не ясно, это поинты или пиксели. Зафиксируйте единицу. Для рендеринга маркетинговых изображений лучше оперировать поинтами и умножать на scale девайса. Добавьте scale policy или поле outputScale.
  • Фон-изображение для макетов лучше хранить с нужным разрешением/аспектом. В противном случае добавьте fit policy (aspectFit/aspectFill) и позиционирование.

Надёжность/валидация

  • Добавьте предзагрузочную валидацию:
    • неизвестные поля → warning (или ошибка в strict‑режиме),
    • проверка доменных значений (platform/device/boolean),
    • проверка конфликтов (ANY + force_device_type).
  • Полезно описать JSON Schema (например, draft-07) и гонять конфиг через валидацию в CI.

Моделирование в Swift (минимум для устойчивого парсинга)

  • Используйте keyDecodingStrategy = .convertFromSnakeCase, чтобы show_complete_frame превратился в showCompleteFrame.
  • Явные enum’ы для платформ и устройств с rawValue и валидацией совместимости.
  • Исключите зарезервированные имена:
    • JSON: "defaults"
    • Swift: struct Config { let defaults: Defaults; let data: [Item] }

Семантика заголовка

  • title задан шрифтом и цветом, но нет размера/макета. Если размер не фиксируется специально — добавьте size/lineHeight/align, иначе поведение может отличаться между платформами/рендерами.
  • Если title текст берётся извне — укажите fallback’и (например, если локализация недоступна).

Сортировка и фильтры

  • Префиксы "01…06" удобны визуально, но ломаются при "10" (лексикографическая сортировка). Если порядок важен — добавьте явное поле order: Int в каждом элементе.

Итого — что бы я поменял в этом PR сразу

  • Переименовал "default" → "defaults".
  • Заменил use_platform/force_device_type на platform/device с явными enum и валидацией.
  • Перевёл пути ресурсов из относительных в логические имена + резолв из бандла/ассетов.
  • Убрал прямой шрифт SF Pro Rounded из поставки; переключился на системный rounded дизайн либо оставил OTF только для внутреннего инструмента.
  • Добавил поле version и простую предвальную валидацию (существование файлов, согласованность полей, парсинг цветов).
  • Зафиксировал формат цвета (hex с опциональным альфа) и единицы padding (points).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ниже — только по делу.

Ключевые проблемы и риски

  • Логика формирования newname неверна относительно комментариев. Конструкция
    newname="${file%-}-${file#-}"
    дублирует сегмент вокруг дефисов и не «убирает всё между первым и последним дефисом». Пример: "a-b-c_1.png" → "a-b-b-c_1.png".
  • Комментарий «до первого подчеркивания с цифрой» не соответствует коду: вы режете по первому "_" вообще, без проверки цифры.
  • Ошибка при отсутствии *.png в каталоге (zsh по умолчанию с опцией NOMATCH упадёт). Нужен null_glob или соответствующий квалификатор.
  • Возможны коллизии имён и перезапись файлов. mv без -n перезапишет существующее.
  • Дублирование кода для en-US и ru.
  • Необработанные частные случаи: файлы без дефисов/подчёркиваний; верхний регистр расширения; когда новое имя совпадает со старым.
  • Скрипт зависит от cd; лучше ограничить область (pushd/popd) и явно фейлиться на ошибках.
  • В конце файла нет завершающей новой строки (замечание из diff).

Предложение по исправлению логики
Если цель действительно:

  • отбросить «хвост» начиная с «подчёркивание + цифра»;
  • и сжать имя «между первым и последним дефисом» (то есть "A-B-C_*.png" → "A-C.png"),

то корректно и безопасно так (чистый zsh):

#!/bin/zsh
set -euo pipefail
setopt null_glob

Изолируем поведение от пользовательских опций

emulate -L zsh

for dir in en-US ru; do
[[ -d "$dir" ]] || { print -u2 "Пропуск: нет каталога $dir"; continue; }
pushd "$dir" > /dev/null

Поддержка .png и .PNG

for file in -- .(#i)png(.N); do
# Обрезаем только если '' за которым идут цифры (подчёркивание с цифрой)
base=${file%%
<->
}
# Если такой '_' нет — остаётся весь file без «цифрового хвоста»
[[ "$base" == "$file" ]] && base=${file%.*}

if [[ "$base" == *-* ]]; then
  left=${base%%-*}   # до первого дефиса
  right=${base##*-}  # после последнего дефиса
  newname="${left}-${right}.png"
else
  newname="${base}.png"
fi

# Пропуск, если имя не меняется
[[ "$newname" == "$file" ]] && continue

# Защита от коллизий
if [[ -e "$newname" ]]; then
  print -u2 "Конфликт имён: '$file' -> '$newname' (уже существует). Пропуск."
  continue
fi

# Если используете git — предпочтительнее git mv
if command -v git >/dev/null 2>&1 && git rev-parse --is-inside-work-tree >/dev/null 2>&1 && git ls-files --error-unmatch -- "$file" >/dev/null 2>&1; then
  git mv -v -- "$file" "$newname"
else
  mv -v -n -- "$file" "$newname"
fi

done

popd > /dev/null
done

Почему так лучше

  • Правильное соответствие требованиям: «подчёркивание с цифрой» обрабатывается через zsh-паттерн <->, а «сжатие между дефисами» — через %% / ##.
  • Нет падения при отсутствии файлов (*.png(.N) + setopt null_glob).
  • Нет перезаписей (mv -n) и есть защита от конфликтов.
  • Нет дублирования кода по каталогам, меньше шансов на расхождение логики.
  • Работает и с .PNG.
  • pushd/popd и set -euo pipefail делают скрипт предсказуемее.
  • Не трогаем файлы, если имя бы не изменилось.

Если же требование «сжимать между дефисами» вам не нужно, а нужно только «отрезать по первому '' (или по '' с цифрой)», упрощайте до:
base=${file%%<->*} # или ${file%%} если не требуется проверка на цифру
newname="${base%.
}.png"

И последнее: добавьте перевод строки в конец файла. Это хорошая практика и устраняет предупреждения diff/линтеров.

@amikhaylin amikhaylin merged commit d97cd85 into master Nov 8, 2025
1 check passed
@amikhaylin amikhaylin deleted the update-tests branch November 8, 2025 10:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant