Export training plan as one continuous PDF page
This commit is contained in:
@@ -49,3 +49,7 @@ Pdf__CameraSizePoints=18
|
|||||||
# Пример переопределения подсказки процентов (индекс 0 — первая строка списка).
|
# Пример переопределения подсказки процентов (индекс 0 — первая строка списка).
|
||||||
# Planner__IntensityPresets__0__Percent=100
|
# Planner__IntensityPresets__0__Percent=100
|
||||||
# Planner__IntensityPresets__0__Repetitions=1
|
# Planner__IntensityPresets__0__Repetitions=1
|
||||||
|
|
||||||
|
# Непрерывный PDF: ширина и предел высоты одной страницы в см.
|
||||||
|
Pdf__PageWidthCentimeters=29.7
|
||||||
|
Pdf__MaxContinuousPageHeightCentimeters=500
|
||||||
|
|||||||
@@ -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`. При превышении предела экспорт сообщает об ошибке, не обрезая программу.
|
||||||
|
|||||||
@@ -121,3 +121,11 @@
|
|||||||
- Поле процента заменено компактным списком из девяти значений с ориентиром повторений. Значения вынесены в Planner:IntensityPresets. Сохранённые нестандартные проценты отображаются отдельным пунктом.
|
- Поле процента заменено компактным списком из девяти значений с ориентиром повторений. Значения вынесены в Planner:IntensityPresets. Сохранённые нестандартные проценты отображаются отдельным пунктом.
|
||||||
- Локальная сборка и Docker-обновление выполнены. Без скриншотов проверены все девять значений, значение по умолчанию 80, сохранение 85 после перезагрузки и неизменность поля повторений. Собственные тестовые данные удалены.
|
- Локальная сборка и Docker-обновление выполнены. Без скриншотов проверены все девять значений, значение по умолчанию 80, сохранение 85 после перезагрузки и неизменность поля повторений. Собственные тестовые данные удалены.
|
||||||
- По уточнению пользователя убран title списка (подсказка при наведении). Сервер и его данные не затрагивались, миграций нет.
|
- По уточнению пользователя убран title списка (подсказка при наведении). Сервер и его данные не затрагивались, миграций нет.
|
||||||
|
|
||||||
|
## Непрерывная страница PDF — 14 октября 2026
|
||||||
|
|
||||||
|
- Все недели экспортируются на одну страницу с высотой по содержимому, без уменьшения текста. Цветные заголовки, таблица клиента и отступ перед первой неделей сохранены.
|
||||||
|
- Сборка успешна, 16 тестов прошли, локальное Docker-приложение обновлено.
|
||||||
|
- Проверены два экспорта: короткий с четырьмя пустыми неделями и длинный с семью неделями. Оба содержат одну страницу шириной 29,7 см; высота соответственно 493 и 2019 pt. Текст находится внутри границ страницы, после последней таблицы остаётся обычный нижний отступ.
|
||||||
|
- В длинном экспорте сохранены все 36 гиперссылок «Видео» с исходными адресами и 12 изображений камеры. Проверка выполнена по структуре, тексту и координатам PDF без скриншотов. Собственные тестовые данные удалены.
|
||||||
|
- Сервер не затрагивался; изменений БД и миграций нет.
|
||||||
|
|||||||
@@ -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++)
|
||||||
|
|||||||
@@ -26,34 +26,23 @@ 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;
|
var section = document.AddSection();
|
||||||
foreach (var week in plan.Weeks)
|
section.PageSetup.PageWidth = Unit.FromCentimeter(options.PageWidthCentimeters);
|
||||||
|
section.PageSetup.PageHeight = Unit.FromCentimeter(options.MaxContinuousPageHeightCentimeters);
|
||||||
|
section.PageSetup.TopMargin = section.PageSetup.BottomMargin = Unit.FromCentimeter(options.MarginCentimeters);
|
||||||
|
section.PageSetup.LeftMargin = section.PageSetup.RightMargin = Unit.FromCentimeter(options.MarginCentimeters);
|
||||||
|
new PdfClientTableBuilder(options).Add(section, client);
|
||||||
|
for (var line = 0; line < options.ClientDetailsGapLines; line++)
|
||||||
{
|
{
|
||||||
var section = document.AddSection();
|
var spacer = section.AddParagraph();
|
||||||
section.PageSetup.PageFormat = PageFormat.A4;
|
spacer.Format.SpaceBefore = Unit.FromPoint(0);
|
||||||
section.PageSetup.Orientation = Orientation.Landscape;
|
spacer.Format.SpaceAfter = Unit.FromPoint(0);
|
||||||
section.PageSetup.TopMargin = section.PageSetup.BottomMargin = Unit.FromCentimeter(options.MarginCentimeters);
|
spacer.Format.LineSpacingRule = LineSpacingRule.Single;
|
||||||
section.PageSetup.LeftMargin = section.PageSetup.RightMargin = Unit.FromCentimeter(options.MarginCentimeters);
|
|
||||||
if (firstWeek)
|
|
||||||
{
|
|
||||||
new PdfClientTableBuilder(options).Add(section, client);
|
|
||||||
for (var line = 0; line < options.ClientDetailsGapLines; line++)
|
|
||||||
{
|
|
||||||
var spacer = section.AddParagraph();
|
|
||||||
spacer.Format.SpaceBefore = Unit.FromPoint(0);
|
|
||||||
spacer.Format.SpaceAfter = Unit.FromPoint(0);
|
|
||||||
spacer.Format.LineSpacingRule = LineSpacingRule.Single;
|
|
||||||
}
|
|
||||||
firstWeek = false;
|
|
||||||
}
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
foreach (var week in plan.Weeks)
|
||||||
|
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();
|
||||||
|
|||||||
@@ -68,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",
|
||||||
|
|||||||
Reference in New Issue
Block a user