Conversation
Stop focus from focuss tasks view
Fixed errors Update screenshots
Tests for watchOS Highlight focused task in lists
more glad UI
| 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, и ваша задача — проверять запросы на включение изменений. Пожалуйста, проверьте следующий код на наличие недопониманий или нарушений. Не тратьте время на комментарии того, что и так работает идеально. Я жду конструктивной критики и предложений по улучшению кода, только полезных и содержательных замечаний." |
There was a problem hiding this comment.
Ниже только полезные замечания по диффу (локализация 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.
- Давай конкретные правки и примеры кода.
- Структура ответа:
- Критические проблемы (с объяснением и как исправить)
- Замечания средней важности
- Неблокирующие улучшения
- Если контекста не хватает, сначала уточни вопросы.
- Не делай общих советов без привязки к коду.
Технические мелочи 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 |
There was a problem hiding this comment.
Ниже только содержательные замечания по диффу .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).
- Исключение из .gitignore означает, что файл попадёт в репозиторий. Для Firebase это обычно допустимо, но убедитесь:
-
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 — меньше кастомной логики.
- Жёстко прописанные локали (ru, en-US) лучше заменить на маски — чтобы не править .gitignore при добавлении новых локалей:
-
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) | ||
| } |
There was a problem hiding this comment.
Хорошее исправление опечатки, но есть несколько потенциальных проблем и зон для улучшения:
-
Опциональная связь и append:
- project.statuses?.append(status) ничего не сделает, если statuses == nil. В результате объект не будет привязан к проекту. Лучше:
- Сделать связь не-опциональной: var statuses: [Status] = []
- Или инициализировать перед использованием: project.statuses = (project.statuses ?? []) + [status]
- project.statuses?.append(status) ничего не сделает, если statuses == nil. В результате объект не будет привязан к проекту. Лучше:
-
Тип хранилища (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 вместо внешнего счетчика — меньше шансов на рассинхрон.
- order += 1 перед использованием сдвигает индексацию. Если нужна 0-базовая, используйте enumerated():
-
Локализация в тестах:
- 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) |
There was a problem hiding this comment.
Ниже только по делу — что стоит поправить или улучшить.
-
Логика 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) |
There was a problem hiding this comment.
Коротко и по делу.
-
Исправление по существу: 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, и при необходимости настроил стиль/тону кнопки.
gen_screenshots.sh
Outdated
There was a problem hiding this comment.
Ниже — только по делу, что можно улучшить в этом скрипте.
-
Явный shell и строгий режим:
- Добавьте shebang и строгие опции для предсказуемости ошибок:
- В начало файла:
#!/usr/bin/env bash - Сразу после:
set -euo pipefail
- В начало файла:
- При желании дебага — переключатель:
[[ ${VERBOSE:-0} -eq 1 ]] && set -x
- Добавьте shebang и строгие опции для предсказуемости ошибок:
-
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; }
- Запускайте через bundler, чтобы не ловить несовместимые глобальные версии:
-
Менеджмент директорий:
- Относительные пути (
../,../../) хрупкие. Зафиксируйте корень репозитория: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.
- Сейчас iOS кладёт в
-
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, либо уберите эмодзи.
- Эмодзи в логах в не-UTF8 окружении могут ломать вывод. Если CI нестабилен по локали, либо выставьте
Итого минимальный смысловой патч:
- добавить shebang + set -euo pipefail,
- заменить echo с \n на printf,
- вызывать fastlane через bundle exec,
- зафиксировать абсолютные пути от корня репозитория,
- явно указать раздельные выходные директории для iOS/watchOS в Snapfile,
- добавить финальный перевод строки в файл.
mac_screenshots.sh
Outdated
There was a problem hiding this comment.
Ниже только содержательные улучшения, без банальностей.
-
Жесткий режим и корректная обработка ошибок в 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 для последующего анализа.
- Для читабельности CI‑логов и сохранения кода возврата используйте пайп с xcbeautify/xcpretty и pipefail:
-
Устойчивость:
- Используйте /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 |
There was a problem hiding this comment.
Ниже — только потенциально проблемные места и улучшения по существу.
-
Хрупкие относительные пути и зависимость от текущей директории. Сейчас файлы копируются в 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).
- Опционально — снижена хрупкость к изменениям названий устройств за счет шаблонов.
screenshots/desktop/Framefile.json
Outdated
There was a problem hiding this comment.
Ниже — только те замечания, которые могут реально укусить в рантайме или при интеграции.
Структура и совместимость
- Ключ 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).
screenshots/desktop/rename.sh
Outdated
There was a problem hiding this comment.
Ниже — только по делу.
Ключевые проблемы и риски
- Логика формирования 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/линтеров.
| 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, и ваша задача — проверять запросы на включение изменений. Пожалуйста, проверьте следующий код на наличие недопониманий или нарушений. Не тратьте время на комментарии того, что и так работает идеально. Я жду конструктивной критики и предложений по улучшению кода, только полезных и содержательных замечаний." |
There was a problem hiding this comment.
Ниже только полезные замечания по диффу (локализация 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.
- Давай конкретные правки и примеры кода.
- Структура ответа:
- Критические проблемы (с объяснением и как исправить)
- Замечания средней важности
- Неблокирующие улучшения
- Если контекста не хватает, сначала уточни вопросы.
- Не делай общих советов без привязки к коду.
Технические мелочи 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 |
There was a problem hiding this comment.
Ниже только содержательные замечания по диффу .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).
- Исключение из .gitignore означает, что файл попадёт в репозиторий. Для Firebase это обычно допустимо, но убедитесь:
-
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 — меньше кастомной логики.
- Жёстко прописанные локали (ru, en-US) лучше заменить на маски — чтобы не править .gitignore при добавлении новых локалей:
-
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) | ||
| } |
There was a problem hiding this comment.
Хорошее исправление опечатки, но есть несколько потенциальных проблем и зон для улучшения:
-
Опциональная связь и append:
- project.statuses?.append(status) ничего не сделает, если statuses == nil. В результате объект не будет привязан к проекту. Лучше:
- Сделать связь не-опциональной: var statuses: [Status] = []
- Или инициализировать перед использованием: project.statuses = (project.statuses ?? []) + [status]
- project.statuses?.append(status) ничего не сделает, если statuses == nil. В результате объект не будет привязан к проекту. Лучше:
-
Тип хранилища (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 вместо внешнего счетчика — меньше шансов на рассинхрон.
- order += 1 перед использованием сдвигает индексацию. Если нужна 0-базовая, используйте enumerated():
-
Локализация в тестах:
- 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) |
There was a problem hiding this comment.
Ниже только по делу — что стоит поправить или улучшить.
-
Логика 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) |
There was a problem hiding this comment.
Коротко и по делу.
-
Исправление по существу: 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, и при необходимости настроил стиль/тону кнопки.
gen_screenshots.sh
Outdated
There was a problem hiding this comment.
Ниже — только по делу, что можно улучшить в этом скрипте.
-
Явный shell и строгий режим:
- Добавьте shebang и строгие опции для предсказуемости ошибок:
- В начало файла:
#!/usr/bin/env bash - Сразу после:
set -euo pipefail
- В начало файла:
- При желании дебага — переключатель:
[[ ${VERBOSE:-0} -eq 1 ]] && set -x
- Добавьте shebang и строгие опции для предсказуемости ошибок:
-
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; }
- Запускайте через bundler, чтобы не ловить несовместимые глобальные версии:
-
Менеджмент директорий:
- Относительные пути (
../,../../) хрупкие. Зафиксируйте корень репозитория: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.
- Сейчас iOS кладёт в
-
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, либо уберите эмодзи.
- Эмодзи в логах в не-UTF8 окружении могут ломать вывод. Если CI нестабилен по локали, либо выставьте
Итого минимальный смысловой патч:
- добавить shebang + set -euo pipefail,
- заменить echo с \n на printf,
- вызывать fastlane через bundle exec,
- зафиксировать абсолютные пути от корня репозитория,
- явно указать раздельные выходные директории для iOS/watchOS в Snapfile,
- добавить финальный перевод строки в файл.
mac_screenshots.sh
Outdated
There was a problem hiding this comment.
Ниже только содержательные улучшения, без банальностей.
-
Жесткий режим и корректная обработка ошибок в 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 для последующего анализа.
- Для читабельности CI‑логов и сохранения кода возврата используйте пайп с xcbeautify/xcpretty и pipefail:
-
Устойчивость:
- Используйте /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 |
There was a problem hiding this comment.
Ниже — только потенциально проблемные места и улучшения по существу.
-
Хрупкие относительные пути и зависимость от текущей директории. Сейчас файлы копируются в 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).
- Опционально — снижена хрупкость к изменениям названий устройств за счет шаблонов.
screenshots/desktop/Framefile.json
Outdated
There was a problem hiding this comment.
Ниже — только те замечания, которые могут реально укусить в рантайме или при интеграции.
Структура и совместимость
- Ключ 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).
screenshots/desktop/rename.sh
Outdated
There was a problem hiding this comment.
Ниже — только по делу.
Ключевые проблемы и риски
- Логика формирования 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/линтеров.
No description provided.