Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0919f34b14 | ||
|
|
66138eb3ea | ||
|
|
ff677ed780 | ||
|
|
a065f5c6e6 | ||
|
|
98770521b1 |
@@ -45,3 +45,11 @@ Planner__UndoDeleteDurationSeconds=10
|
|||||||
|
|
||||||
# Размер иконки камеры в PDF (пункты).
|
# Размер иконки камеры в PDF (пункты).
|
||||||
Pdf__CameraSizePoints=18
|
Pdf__CameraSizePoints=18
|
||||||
|
|
||||||
|
# Пример переопределения подсказки процентов (индекс 0 — первая строка списка).
|
||||||
|
# Planner__IntensityPresets__0__Percent=100
|
||||||
|
# Planner__IntensityPresets__0__Repetitions=1
|
||||||
|
|
||||||
|
# Непрерывный PDF: ширина и предел высоты одной страницы в см.
|
||||||
|
Pdf__PageWidthCentimeters=29.7
|
||||||
|
Pdf__MaxContinuousPageHeightCentimeters=500
|
||||||
|
|||||||
@@ -32,6 +32,12 @@
|
|||||||
|
|
||||||
## Работа с проектом
|
## Работа с проектом
|
||||||
|
|
||||||
|
- С 13 октября 2026 года сервер содержит рабочие данные. Не выполнять серверные операции без согласованной задачи; прежнее разрешение на первоначальное развёртывание не разрешает последующие обновления.
|
||||||
|
- При обновлении сохранять серверные `.env`, `app.env` и существующие учётные записи без изменений, если пользователь отдельно не поручил их изменить.
|
||||||
|
- Резервные копии рабочей БД хранить вне сервера приложения и вне Git. Для текущего окружения использовать `~/Documents/TrainingPlannerBackups`; временный серверный дамп удалять только после успешного переноса и сверки контрольных сумм.
|
||||||
|
- Миграции должны сохранять текущие данные. Перед применением на сервере проверить SQL, создать резервную копию и проверить её восстановление и миграцию в отдельной тестовой БД. Не запускать тесты на рабочей базе.
|
||||||
|
- Не пересоздавать рабочую БД, не удалять её тома и не заменять её локальной базой. Потенциально разрушающие изменения требуют отдельного согласования и плана переноса данных.
|
||||||
|
- Учитывать автоматическое применение миграций при запуске приложения: перезапуск новой версии также может изменить БД. При обнаружении проблемы на сервере остановиться и спросить пользователя до исправления.
|
||||||
- После изменений выполнять сборку и проверки, соответствующие изменению.
|
- После изменений выполнять сборку и проверки, соответствующие изменению.
|
||||||
- Поддерживать README и `.env.example` в соответствии с реальной конфигурацией.
|
- Поддерживать README и `.env.example` в соответствии с реальной конфигурацией.
|
||||||
- Не заявлять о проверке Docker, БД, PDF или UI, если соответствующая проверка не выполнена.
|
- Не заявлять о проверке Docker, БД, PDF или UI, если соответствующая проверка не выполнена.
|
||||||
|
|||||||
+1
-1
@@ -14,6 +14,6 @@ FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS runtime
|
|||||||
RUN apt-get update && apt-get install -y --no-install-recommends fonts-dejavu-core libgssapi-krb5-2 && rm -rf /var/lib/apt/lists/*
|
RUN apt-get update && apt-get install -y --no-install-recommends fonts-dejavu-core libgssapi-krb5-2 && rm -rf /var/lib/apt/lists/*
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
RUN mkdir -p /app/keys && chown $APP_UID /app/keys
|
RUN mkdir -p /app/keys && chown $APP_UID /app/keys
|
||||||
COPY --from=build /app ./
|
COPY --from=build --chown=$APP_UID:$APP_UID /app ./
|
||||||
USER $APP_UID
|
USER $APP_UID
|
||||||
ENTRYPOINT ["dotnet", "TrainingPlanner.Web.dll"]
|
ENTRYPOINT ["dotnet", "TrainingPlanner.Web.dll"]
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
- Поиск в конфигураторе: адаптивный выпадающий список с фильтром названия и группы. Двойной клик или Enter сразу добавляет упражнение; одиночный клик выбирает его для кнопки «Добавить». Точное совпадение и начало названия выше остальных; регистр и «ё/е» не мешают поиску.
|
- Поиск в конфигураторе: адаптивный выпадающий список с фильтром названия и группы. Двойной клик или Enter сразу добавляет упражнение; одиночный клик выбирает его для кнопки «Добавить». Точное совпадение и начало названия выше остальных; регистр и «ё/е» не мешают поиску.
|
||||||
- Удаление из библиотеки через корзину с подтверждением; упражнения в ранее составленных программах сохраняются.
|
- Удаление из библиотеки через корзину с подтверждением; упражнения в ранее составленных программах сохраняются.
|
||||||
- Недели, тренировки и перемещаемые блоки. Перетаскивание за маркер или кнопки вверх/вниз.
|
- Недели, тренировки и перемещаемые блоки. Перетаскивание за маркер или кнопки вверх/вниз.
|
||||||
- Общие подходы, текстовые повторения и вес; текстовые разминка и примечание; процент от максимума вручную.
|
- Общие подходы, текстовые повторения и вес; текстовые разминка и примечание; процент от максимума выбирается из компактного списка с подсказкой по повторениям. Подсказка не меняет поле повторений. Список задан в `Planner:IntensityPresets`; ранее сохранённые нестандартные значения сохраняются.
|
||||||
- Отдых в минутах. Итог — количество подходов × отдых, включая последний подход. Время тренировки задаётся вручную.
|
- Отдых в минутах. Итог — количество подходов × отдых, включая последний подход. Время тренировки задаётся вручную.
|
||||||
- Копирование блоков, тренировок, недель и всей программы. Каждое упражнение — отдельный блок; сетов нет.
|
- Копирование блоков, тренировок, недель и всей программы. Каждое упражнение — отдельный блок; сетов нет.
|
||||||
- Перетаскивание недель с автоматической перенумерацией и сохранением порядка.
|
- Перетаскивание недель с автоматической перенумерацией и сохранением порядка.
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
- Удаление тренировки через иконку корзины с подтверждением; после удаления последней тренировки неделю можно заполнить заново.
|
- Удаление тренировки через иконку корзины с подтверждением; после удаления последней тренировки неделю можно заполнить заново.
|
||||||
- Автосохранение, ручное сохранение, отмена последнего удаления блока в течение 10 секунд только в исходной тренировке (настройка `.env`: `Planner__UndoDeleteDurationSeconds`) и защита от перезаписи из другой вкладки.
|
- Автосохранение, ручное сохранение, отмена последнего удаления блока в течение 10 секунд только в исходной тренировке (настройка `.env`: `Planner__UndoDeleteDurationSeconds`) и защита от перезаписи из другой вкладки.
|
||||||
- В начале PDF — таблица с именем, целью, режимом тренировок и состоянием здоровья клиента. Заметки тренера не экспортируются.
|
- В начале PDF — таблица с именем, целью, режимом тренировок и состоянием здоровья клиента. Заметки тренера не экспортируются.
|
||||||
- Альбомный PDF: одна непрерывная таблица на неделю, цветные разделители тренировок с отдельными итогами, одна строка на упражнение, кликабельные ссылки и значки камеры. Ширина таблицы — 27,5 см; PNG камеры встроен в сборку, выровнен по центру ячейки. Размер иконки задаётся `Pdf__CameraSizePoints`.
|
- PDF одной длинной страницей: высота подбирается по содержимому без уменьшения шрифта, недели идут подряд. Одна таблица на неделю, цветные разделители тренировок с отдельными итогами, одна строка на упражнение, кликабельные ссылки и значки камеры. Ширина таблицы — 27,5 см; PNG камеры встроен в сборку, выровнен по центру ячейки. Размер иконки задаётся `Pdf__CameraSizePoints`.
|
||||||
|
|
||||||
Интерфейс первой версии рассчитан на компьютер. Полная адаптация под iPhone 15 отложена. Клиентского кабинета, загрузки видео, регистрации и работы без интернета нет. Blazor Interactive Server требует соединения с сервером.
|
Интерфейс первой версии рассчитан на компьютер. Полная адаптация под iPhone 15 отложена. Клиентского кабинета, загрузки видео, регистрации и работы без интернета нет. Blazor Interactive Server требует соединения с сервером.
|
||||||
|
|
||||||
@@ -115,3 +115,5 @@ dotnet ef migrations add MigrationName --project src/TrainingPlanner.Web --outpu
|
|||||||
### Обновление старых программ
|
### Обновление старых программ
|
||||||
|
|
||||||
Перед применением миграции ExercisePrescription сделайте резервную копию БД. Общие вес и повторения берутся из первого подхода, старые сеты разделяются на одиночные упражнения, отдых переводится из секунд в минуты. Разные веса последующих подходов больше не используются. Для возврата к прежней модели нужны резервная копия и прежняя версия приложения. Переменные `PLANNER_DEFAULT_REST_SECONDS` и `PLANNER_DEFAULT_ROUNDS` заменены на `PLANNER_DEFAULT_REST_MINUTES` и `PLANNER_DEFAULT_SETS_COUNT`.
|
Перед применением миграции ExercisePrescription сделайте резервную копию БД. Общие вес и повторения берутся из первого подхода, старые сеты разделяются на одиночные упражнения, отдых переводится из секунд в минуты. Разные веса последующих подходов больше не используются. Для возврата к прежней модели нужны резервная копия и прежняя версия приложения. Переменные `PLANNER_DEFAULT_REST_SECONDS` и `PLANNER_DEFAULT_ROUNDS` заменены на `PLANNER_DEFAULT_REST_MINUTES` и `PLANNER_DEFAULT_SETS_COUNT`.
|
||||||
|
|
||||||
|
Непрерывный PDF: `Pdf__PageWidthCentimeters=29.7`, `Pdf__MaxContinuousPageHeightCentimeters=500`. При превышении предела экспорт сообщает об ошибке, не обрезая программу.
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
# Развёртывание на сервере
|
||||||
|
|
||||||
|
## Текущее правило эксплуатации — 13 октября 2026
|
||||||
|
|
||||||
|
Пользователь завершил настройку сервера; база содержит рабочие данные. Описанные ниже параметры первоначального развёртывания — историческая справка, а не подтверждение текущей конфигурации.
|
||||||
|
|
||||||
|
Без согласованной задачи сервер не изменять. Перед миграцией проверить SQL на потерю данных, создать резервную копию, проверить её восстановление и выполнение миграции в отдельной тестовой БД. Сохранность текущих данных обязательна. Не пересоздавать базу, не удалять тома и не подменять рабочие данные локальными. Учитывать миграции, автоматически выполняемые при старте приложения. При обнаружении проблемы остановиться и согласовать дальнейшие действия.
|
||||||
|
|
||||||
|
## Первоначальное развёртывание — 30 сентября 2026
|
||||||
|
|
||||||
|
- Подключение: `ssh trainplan` (алиас из локальной SSH-конфигурации).
|
||||||
|
- Каталог развёртывания: `/home/dimass/training-planner`.
|
||||||
|
- Исходники: подкаталог `repository`, ветка `main`, коммит `ad3cd6f`.
|
||||||
|
- Compose-проект: `trainingplanner`, отдельные сеть и тома.
|
||||||
|
- Приложение: `127.0.0.1:8001`, только через локальный reverse proxy.
|
||||||
|
- PostgreSQL не публикует порт на хосте. При первоначальном развёртывании была создана пустая база; сейчас она содержит рабочие данные.
|
||||||
|
- `compose.yaml`, `.env`, `app.env` и `access.txt` находятся вне Git-репозитория.
|
||||||
|
- Логин тренера: `trainer`; сгенерированный пароль находится в `access.txt`, доступном только владельцу.
|
||||||
|
|
||||||
|
## Управление
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd /home/dimass/training-planner
|
||||||
|
docker compose ps
|
||||||
|
docker compose logs --tail 100 app
|
||||||
|
```
|
||||||
|
|
||||||
|
Не выполнять `docker system prune`, `docker volume prune` или `docker compose down -v`: на сервере есть другие проекты, а тома этого приложения содержат базу и ключи авторизации.
|
||||||
|
|
||||||
|
## HTTPS — исторические результаты проверки
|
||||||
|
|
||||||
|
При проверке 30 сентября 2026 года Nginx для `trainplan.samsonium.ru` слушает только порт 80 и проксирует на `http://127.0.0.1:8001`. На порту 443 выдаётся сертификат `search.samsonium.ru`, истёкший 19 ноября 2025 года. Конфигурация Nginx и сертификаты при развёртывании приложения не изменяются.
|
||||||
|
|
||||||
|
На момент первоначальной проверки требовалась настройка HTTPS администратором. 13 октября пользователь сообщил, что завершил настройку сервера; повторная проверка не выполнялась. Прежнюю проблему сертификата не считать актуальной без новой проверки в рамках согласованной задачи.
|
||||||
|
|
||||||
|
## Обновление — 13 октября 2026
|
||||||
|
|
||||||
|
- Образ приложения: `training-planner:9877052`, код ветки `master`. Исходники сборки: `/home/dimass/training-planner/releases/9877052`. Старый checkout не перезаписан.
|
||||||
|
- Обновлён только app через `docker compose up -d --no-deps app`; контейнер БД остался прежним. Новых миграций нет.
|
||||||
|
- `.env`, `app.env` и существующие учётные записи, включая хеши паролей, не изменились: проверено контрольными суммами. Количество клиентов, программ, библиотечных упражнений и миграций осталось прежним.
|
||||||
|
- HTTPS страницы входа отвечает 200, новый контейнер работает без перезапусков. Вход под рабочей учётной записью и тестовые изменения рабочей БД не выполнялись.
|
||||||
|
- Резервная копия находится на компьютере пользователя: `~/Documents/TrainingPlannerBackups/before-9877052-20261013-225214/database.dump`. Проверены читаемость каталога архива и совпадение SHA-256 после переноса. Восстановление не выполнялось, миграций в обновлении нет.
|
||||||
|
- После проверки переноса временный серверный дамп удалён. На сервере остались резервный Compose и контрольные суммы для проверки обновления; env-файлы не изменялись.
|
||||||
@@ -115,3 +115,23 @@
|
|||||||
|
|
||||||
- Имя скачиваемого файла: «Имя клиента — dd.MM.yyyy.pdf», дата создания программы без времени, в том же локальном времени сервера, что и история программ. Недопустимые символы имени файла заменяются подчёркиванием.
|
- Имя скачиваемого файла: «Имя клиента — dd.MM.yyyy.pdf», дата создания программы без времени, в том же локальном времени сервера, что и история программ. Недопустимые символы имени файла заменяются подчёркиванием.
|
||||||
- Сборка и Docker-обновление успешны. Проверен Content-Disposition с кириллическим именем клиента и форматом даты. Собственные тестовые записи удалены, скриншоты не выполнялись.
|
- Сборка и Docker-обновление успешны. Проверен Content-Disposition с кириллическим именем клиента и форматом даты. Собственные тестовые записи удалены, скриншоты не выполнялись.
|
||||||
|
|
||||||
|
## Список процентов — 14 октября 2026
|
||||||
|
|
||||||
|
- Поле процента заменено компактным списком из девяти значений с ориентиром повторений. Значения вынесены в Planner:IntensityPresets. Сохранённые нестандартные проценты отображаются отдельным пунктом.
|
||||||
|
- Локальная сборка и Docker-обновление выполнены. Без скриншотов проверены все девять значений, значение по умолчанию 80, сохранение 85 после перезагрузки и неизменность поля повторений. Собственные тестовые данные удалены.
|
||||||
|
- По уточнению пользователя убран title списка (подсказка при наведении). Сервер и его данные не затрагивались, миграций нет.
|
||||||
|
|
||||||
|
## Непрерывная страница PDF — 14 октября 2026
|
||||||
|
|
||||||
|
- Все недели экспортируются на одну страницу с высотой по содержимому, без уменьшения текста. Цветные заголовки, таблица клиента и отступ перед первой неделей сохранены.
|
||||||
|
- Сборка успешна, 16 тестов прошли, локальное Docker-приложение обновлено.
|
||||||
|
- Проверены два экспорта: короткий с четырьмя пустыми неделями и длинный с семью неделями. Оба содержат одну страницу шириной 29,7 см; высота соответственно 493 и 2019 pt. Текст находится внутри границ страницы, после последней таблицы остаётся обычный нижний отступ.
|
||||||
|
- В длинном экспорте сохранены все 36 гиперссылок «Видео» с исходными адресами и 12 изображений камеры. Проверка выполнена по структуре, тексту и координатам PDF без скриншотов. Собственные тестовые данные удалены.
|
||||||
|
- Сервер не затрагивался; изменений БД и миграций нет.
|
||||||
|
|
||||||
|
## Исправление зависимости PDF — 14 октября 2026
|
||||||
|
|
||||||
|
- Для транзитивной зависимости PDFsharp добавлена явная ссылка System.Security.Cryptography.Pkcs 8.0.2 вместо 8.0.1. Это исправленная версия для GHSA-2fmv-rv2c-59x7 и GHSA-846f-f764-76mx; аудит NuGet не отключён.
|
||||||
|
- Выполнены восстановление пакетов без кеша, сборка без предупреждений, 16 тестов и локальная Docker-сборка. Аудит прямых и транзитивных пакетов Web не обнаружил известных уязвимостей в текущем источнике NuGet.
|
||||||
|
- После обновления зависимости экспорт из локального Docker снова проверен: одна страница, семь недель, 36 ссылок «Видео», 12 камер; текст внутри страницы. Скриншоты не выполнялись, собственные тестовые записи удалены.
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace TrainingPlanner.Application;
|
||||||
|
|
||||||
|
public class IntensityPreset
|
||||||
|
{
|
||||||
|
public decimal Percent { get; set; }
|
||||||
|
public string Repetitions { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ public class PlannerOptions
|
|||||||
{
|
{
|
||||||
public string DefaultWorkoutName { get; set; } = string.Empty;
|
public string DefaultWorkoutName { get; set; } = string.Empty;
|
||||||
public string DefaultWorkoutDuration { get; set; } = string.Empty;
|
public string DefaultWorkoutDuration { get; set; } = string.Empty;
|
||||||
|
public List<IntensityPreset> IntensityPresets { get; set; } = [];
|
||||||
public decimal DefaultIntensityPercent { get; set; }
|
public decimal DefaultIntensityPercent { get; set; }
|
||||||
public int DefaultWeekCount { get; set; }
|
public int DefaultWeekCount { get; set; }
|
||||||
public int DefaultSetsCount { get; set; }
|
public int DefaultSetsCount { get; set; }
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
using MigraDoc.DocumentObjectModel;
|
||||||
|
using MigraDoc.Rendering;
|
||||||
|
|
||||||
|
namespace TrainingPlanner.Infrastructure.Pdf;
|
||||||
|
|
||||||
|
public class ContinuousPdfLayout(PdfOptions options)
|
||||||
|
{
|
||||||
|
public void Fit(Document document)
|
||||||
|
{
|
||||||
|
var measurement = new DocumentRenderer(document.Clone());
|
||||||
|
measurement.PrepareDocument();
|
||||||
|
if (measurement.FormattedDocument?.PageCount != 1)
|
||||||
|
throw new InvalidOperationException("Программа слишком длинная для одной страницы PDF. Сократите её объём.");
|
||||||
|
var elements = measurement.GetRenderInfoFromPage(1)
|
||||||
|
?? throw new InvalidOperationException("Не удалось рассчитать высоту PDF.");
|
||||||
|
var contentBottom = elements.Max(info => info.LayoutInfo.ContentArea.Y.Point + info.LayoutInfo.ContentArea.Height.Point);
|
||||||
|
document.Sections[0].PageSetup.PageHeight = Unit.FromPoint(
|
||||||
|
Math.Ceiling(contentBottom + Unit.FromCentimeter(options.MarginCentimeters).Point));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,8 @@ public class PdfOptions
|
|||||||
public double FontSize { get; set; }
|
public double FontSize { get; set; }
|
||||||
public double CameraSizePoints { get; set; }
|
public double CameraSizePoints { get; set; }
|
||||||
public double HeadingSize { get; set; }
|
public double HeadingSize { get; set; }
|
||||||
|
public double PageWidthCentimeters { get; set; }
|
||||||
|
public double MaxContinuousPageHeightCentimeters { get; set; }
|
||||||
public double MarginCentimeters { get; set; }
|
public double MarginCentimeters { get; set; }
|
||||||
public double ClientLabelWidthCentimeters { get; set; }
|
public double ClientLabelWidthCentimeters { get; set; }
|
||||||
public int ClientDetailsGapLines { get; set; }
|
public int ClientDetailsGapLines { get; set; }
|
||||||
|
|||||||
@@ -33,6 +33,12 @@ public class PdfWeekTableBuilder(PdfOptions options)
|
|||||||
"Отдых, мин", "Съёмка", "Время тренировки" };
|
"Отдых, мин", "Съёмка", "Время тренировки" };
|
||||||
for (var index = 0; index < labels.Length; index++)
|
for (var index = 0; index < labels.Length; index++)
|
||||||
header.Cells[index].AddParagraph(labels[index]);
|
header.Cells[index].AddParagraph(labels[index]);
|
||||||
|
if (week.Workouts.Count == 0)
|
||||||
|
{
|
||||||
|
var empty = table.AddRow();
|
||||||
|
empty.Cells[0].MergeRight = table.Columns.Count - 1;
|
||||||
|
empty.Cells[0].AddParagraph("Тренировки не назначены.");
|
||||||
|
}
|
||||||
foreach (var workout in week.Workouts)
|
foreach (var workout in week.Workouts)
|
||||||
AddWorkout(table, workout);
|
AddWorkout(table, workout);
|
||||||
for (var rowIndex = 0; rowIndex < table.Rows.Count; rowIndex++)
|
for (var rowIndex = 0; rowIndex < table.Rows.Count; rowIndex++)
|
||||||
@@ -85,8 +91,7 @@ public class PdfWeekTableBuilder(PdfOptions options)
|
|||||||
if (!string.IsNullOrWhiteSpace(exercise.VideoUrl))
|
if (!string.IsNullOrWhiteSpace(exercise.VideoUrl))
|
||||||
{
|
{
|
||||||
var link = row.Cells[6].AddParagraph().AddHyperlink(exercise.VideoUrl, HyperlinkType.Url);
|
var link = row.Cells[6].AddParagraph().AddHyperlink(exercise.VideoUrl, HyperlinkType.Url);
|
||||||
// Invisible break opportunities keep long URLs within the cell; the target stays unchanged.
|
link.AddText("Видео");
|
||||||
link.AddText(string.Join("\u200B", exercise.VideoUrl.Select(character => character.ToString())));
|
|
||||||
link.Font.Color = Color.Parse(options.AccentColor);
|
link.Font.Color = Color.Parse(options.AccentColor);
|
||||||
link.Font.Underline = Underline.Single;
|
link.Font.Underline = Underline.Single;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,16 +26,11 @@ public class PlanPdfExporter : IPlanPdfExporter
|
|||||||
normal.Font.Name = options.FontFamily;
|
normal.Font.Name = options.FontFamily;
|
||||||
normal.Font.Size = options.FontSize;
|
normal.Font.Size = options.FontSize;
|
||||||
normal.ParagraphFormat.SpaceAfter = Unit.FromPoint(options.FontSize / 2);
|
normal.ParagraphFormat.SpaceAfter = Unit.FromPoint(options.FontSize / 2);
|
||||||
var firstWeek = true;
|
|
||||||
foreach (var week in plan.Weeks)
|
|
||||||
{
|
|
||||||
var section = document.AddSection();
|
var section = document.AddSection();
|
||||||
section.PageSetup.PageFormat = PageFormat.A4;
|
section.PageSetup.PageWidth = Unit.FromCentimeter(options.PageWidthCentimeters);
|
||||||
section.PageSetup.Orientation = Orientation.Landscape;
|
section.PageSetup.PageHeight = Unit.FromCentimeter(options.MaxContinuousPageHeightCentimeters);
|
||||||
section.PageSetup.TopMargin = section.PageSetup.BottomMargin = Unit.FromCentimeter(options.MarginCentimeters);
|
section.PageSetup.TopMargin = section.PageSetup.BottomMargin = Unit.FromCentimeter(options.MarginCentimeters);
|
||||||
section.PageSetup.LeftMargin = section.PageSetup.RightMargin = Unit.FromCentimeter(options.MarginCentimeters);
|
section.PageSetup.LeftMargin = section.PageSetup.RightMargin = Unit.FromCentimeter(options.MarginCentimeters);
|
||||||
if (firstWeek)
|
|
||||||
{
|
|
||||||
new PdfClientTableBuilder(options).Add(section, client);
|
new PdfClientTableBuilder(options).Add(section, client);
|
||||||
for (var line = 0; line < options.ClientDetailsGapLines; line++)
|
for (var line = 0; line < options.ClientDetailsGapLines; line++)
|
||||||
{
|
{
|
||||||
@@ -44,16 +39,10 @@ public class PlanPdfExporter : IPlanPdfExporter
|
|||||||
spacer.Format.SpaceAfter = Unit.FromPoint(0);
|
spacer.Format.SpaceAfter = Unit.FromPoint(0);
|
||||||
spacer.Format.LineSpacingRule = LineSpacingRule.Single;
|
spacer.Format.LineSpacingRule = LineSpacingRule.Single;
|
||||||
}
|
}
|
||||||
firstWeek = false;
|
foreach (var week in plan.Weeks)
|
||||||
}
|
|
||||||
var footer = section.Footers.Primary.AddParagraph();
|
|
||||||
footer.Format.Alignment = ParagraphAlignment.Right;
|
|
||||||
footer.AddPageField();
|
|
||||||
if (week.Workouts.Count == 0)
|
|
||||||
section.AddParagraph($"Неделя {week.Position + 1}. Тренировки не назначены.");
|
|
||||||
else
|
|
||||||
tables.Add(section, week);
|
tables.Add(section, week);
|
||||||
}
|
|
||||||
|
new ContinuousPdfLayout(options).Fit(document);
|
||||||
var renderer = new PdfDocumentRenderer { Document = document };
|
var renderer = new PdfDocumentRenderer { Document = document };
|
||||||
renderer.RenderDocument();
|
renderer.RenderDocument();
|
||||||
using var stream = new MemoryStream();
|
using var stream = new MemoryStream();
|
||||||
|
|||||||
@@ -9,6 +9,8 @@
|
|||||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="10.0.8" />
|
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="10.0.8" />
|
||||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0" />
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0" />
|
||||||
<PackageReference Include="PDFsharp-MigraDoc" Version="6.2.4" />
|
<PackageReference Include="PDFsharp-MigraDoc" Version="6.2.4" />
|
||||||
|
<!-- PDFsharp requests 8.0.1; pin its security-patched compatible dependency. -->
|
||||||
|
<PackageReference Include="System.Security.Cryptography.Pkcs" Version="8.0.2" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
@inject PlannerOptions Options
|
||||||
|
|
||||||
<div class="exercise-editor">
|
<div class="exercise-editor">
|
||||||
<div class="exercise-heading">
|
<div class="exercise-heading">
|
||||||
<label class="grow">
|
<label class="grow">
|
||||||
@@ -22,8 +24,27 @@
|
|||||||
<input @bind="Exercise.Weight" @bind:event="oninput" @bind:after="Changed" placeholder="60 или Подобрать" />
|
<input @bind="Exercise.Weight" @bind:event="oninput" @bind:after="Changed" placeholder="60 или Подобрать" />
|
||||||
</label>
|
</label>
|
||||||
<label>От максимума, %
|
<label>От максимума, %
|
||||||
<input type="number" min="0" max="100" step="any" @bind="Exercise.IntensityPercent"
|
<span class="intensity-field">
|
||||||
@bind:after="Changed" placeholder="—" />
|
<select aria-label="От максимума, %" class="intensity-select" @bind="Exercise.IntensityPercent" @bind:after="Changed">
|
||||||
|
<option value="">—</option>
|
||||||
|
@if (Exercise.IntensityPercent.HasValue && Options.IntensityPresets.All(item => item.Percent != Exercise.IntensityPercent.Value))
|
||||||
|
{
|
||||||
|
<option value="@Exercise.IntensityPercent.Value">@Exercise.IntensityPercent.Value% — своё значение</option>
|
||||||
|
}
|
||||||
|
@foreach (var preset in Options.IntensityPresets)
|
||||||
|
{
|
||||||
|
<option value="@preset.Percent">@preset.Percent% — @preset.Repetitions повт.</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
<span class="intensity-value" aria-hidden="true">
|
||||||
|
<span>@(Exercise.IntensityPercent.HasValue ? $"{Exercise.IntensityPercent.Value}%" : "—")</span>
|
||||||
|
@if (SelectedIntensity != null)
|
||||||
|
{
|
||||||
|
<span class="intensity-hint">@SelectedIntensity.Repetitions повт.</span>
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
<span class="intensity-arrow" aria-hidden="true">⌄</span>
|
||||||
|
</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<label>Разминочные подходы
|
<label>Разминочные подходы
|
||||||
@@ -40,5 +61,7 @@
|
|||||||
[Parameter, EditorRequired] public WorkoutExercise Exercise { get; set; } = null!;
|
[Parameter, EditorRequired] public WorkoutExercise Exercise { get; set; } = null!;
|
||||||
[Parameter] public EventCallback OnChanged { get; set; }
|
[Parameter] public EventCallback OnChanged { get; set; }
|
||||||
|
|
||||||
|
private IntensityPreset? SelectedIntensity => Options.IntensityPresets.FirstOrDefault(item => item.Percent == Exercise.IntensityPercent);
|
||||||
|
|
||||||
private Task Changed() => OnChanged.InvokeAsync();
|
private Task Changed() => OnChanged.InvokeAsync();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,17 @@
|
|||||||
"Planner": {
|
"Planner": {
|
||||||
"DefaultWorkoutDuration": "1 час",
|
"DefaultWorkoutDuration": "1 час",
|
||||||
"DefaultIntensityPercent": 80,
|
"DefaultIntensityPercent": 80,
|
||||||
|
"IntensityPresets": [
|
||||||
|
{ "Percent": 100, "Repetitions": "1" },
|
||||||
|
{ "Percent": 95, "Repetitions": "2–3" },
|
||||||
|
{ "Percent": 90, "Repetitions": "3–5" },
|
||||||
|
{ "Percent": 85, "Repetitions": "5–7" },
|
||||||
|
{ "Percent": 80, "Repetitions": "6–10" },
|
||||||
|
{ "Percent": 75, "Repetitions": "8–12" },
|
||||||
|
{ "Percent": 70, "Repetitions": "10–15" },
|
||||||
|
{ "Percent": 65, "Repetitions": "12–17" },
|
||||||
|
{ "Percent": 60, "Repetitions": "15–20" }
|
||||||
|
],
|
||||||
"DefaultWeekCount": 4,
|
"DefaultWeekCount": 4,
|
||||||
"DefaultSetsCount": 3,
|
"DefaultSetsCount": 3,
|
||||||
"DefaultRepetitions": "10",
|
"DefaultRepetitions": "10",
|
||||||
@@ -57,6 +68,8 @@
|
|||||||
"MaximumReceiveMessageBytes": 1048576
|
"MaximumReceiveMessageBytes": 1048576
|
||||||
},
|
},
|
||||||
"Pdf": {
|
"Pdf": {
|
||||||
|
"PageWidthCentimeters": 29.7,
|
||||||
|
"MaxContinuousPageHeightCentimeters": 500,
|
||||||
"ClientDetailsGapLines": 5,
|
"ClientDetailsGapLines": 5,
|
||||||
"ClientLabelWidthCentimeters": 4.5,
|
"ClientLabelWidthCentimeters": 4.5,
|
||||||
"FontPath": "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
|
"FontPath": "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
|
||||||
|
|||||||
@@ -786,3 +786,10 @@ summary {
|
|||||||
.import-table td { border:1px solid var(--line); padding:4px; }
|
.import-table td { border:1px solid var(--line); padding:4px; }
|
||||||
.import-table input { width:100%; min-width:0; border-color:transparent; }
|
.import-table input { width:100%; min-width:0; border-color:transparent; }
|
||||||
.exercise-import .actions { margin-top:20px; }
|
.exercise-import .actions { margin-top:20px; }
|
||||||
|
|
||||||
|
.intensity-field { display:block; position:relative; width:100%; max-width:220px; }
|
||||||
|
.intensity-select { width:100%; appearance:none; color:transparent; font-variant-numeric:tabular-nums; }
|
||||||
|
.intensity-select option { color:var(--text, #202637); background:white; }
|
||||||
|
.intensity-value { position:absolute; inset:0 34px 0 12px; display:flex; align-items:center; justify-content:space-between; gap:8px; pointer-events:none; color:var(--text, #202637); font-weight:400; font-variant-numeric:tabular-nums; }
|
||||||
|
.intensity-hint { color:var(--muted); font-size:12px; white-space:nowrap; }
|
||||||
|
.intensity-arrow { position:absolute; right:12px; top:50%; transform:translateY(-50%); color:var(--muted); pointer-events:none; }
|
||||||
|
|||||||
Reference in New Issue
Block a user