nlve/src/uExportForm.pas
4aiman ed889e5d03 Уголки участка, курсор посередине и журнал сборки рядом с роликом
Отметки участка стали видимыми уголками на линейке — жёлтые треугольники
по краям, за них можно тянуть. Клавишами это выходило долго: поставил,
посмотрел, промахнулся, ставь заново. Край прилипает к границам клипов,
как и курсор, и края не пропускаются друг за друга.

Клавиша \ и кнопка с целью переключают, держать ли курсор посередине
монтажа при воспроизведении. Раньше монтаж ехал под курсором всегда, а
это мешает, когда разглядываешь конкретное место.

В окне сборки вместо надписи про участок появилась галка «только
отмеченный участок». Надпись легко проглядеть, а вопрос из тех, где
ошибка стоит часа работы: соберёшь кусок вместо ролика или наоборот.
Снятая галка собирает весь монтаж, отметки при этом остаются.

Журнал сборки пишется рядом с готовым файлом и под тем же именем. Общий
журнал программы перезаписывается следующим запуском, а разбираться,
почему в ролике не то, приходится потом. В журнале: время, проект,
участок, кадр и битрейт, что с текстом, и полный список кусков — с
пометками, где слои, где текст и что пересобирается. Строка пишется
сразу, чтобы после падения осталась последняя.
2026-08-31 11:00:30 +03:00

322 lines
12 KiB
ObjectPascal
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{
uExportForm — окно сборки ролика.
Здесь выбирается не только имя файла, но и то, что от него зависит:
контейнер и способ попадания текста в ролик. Разделять их нельзя —
выбор одного меняет допустимое в другом.
Почему при MP4 вжигание становится обязательным. Отдельной дорожкой
субтитров со всем оформлением умеет только Matroska. В MP4 их кладут
как mov_text, а он не хранит ни цвета, ни обводки, ни положения — от
надписи остаётся голый текст в углу, и это хуже, чем ничего. Поэтому
при MP4 галка ставится и запирается: выбора всё равно нет.
Запертой она остаётся ровно пока выбран MP4. Переключение на MKV
возвращает и галку, и прежний ответ человека — тот, что он давал до
того, как MP4 всё за него решил.
}
unit uExportForm;
{$mode objfpc}{$H+}
interface
uses
Classes, SysUtils, Controls, Forms, Graphics, StdCtrls, Dialogs,
uTheme;
type
{ Что человек выбрал в окне. }
TExportChoice = record
Path: string;
{ Вжигать ли текст в картинку. При MP4 всегда True. }
BurnText: Boolean;
{ Собирать только отмеченный участок. Имеет смысл, только когда
участок отмечен. }
RegionOnly: Boolean;
end;
{ Показывает окно.
AHasText — есть ли в проекте надписи: без них про текст и спрашивать
незачем. APreferMkv — в монтаже есть точные резы, и MKV надёжнее.
ABurnDefault — ответ, данный в прошлый раз.
ARegion — описание отмеченного участка. Не пусто — в окне появляется
галка «только отмеченный участок». Галка, а не надпись: надпись легко
проглядеть, а тут вопрос из тех, где ошибка стоит часа работы — соберёшь
кусок вместо ролика или наоборот. }
function AskExport(var AChoice: TExportChoice; AHasText, APreferMkv: Boolean;
const ADefaultDir: string; const ARegion: string = ''): Boolean;
implementation
type
TExportDialog = class(TComponent)
public
Form: TForm;
edPath: TEdit;
BtnMp4: TThemedButton;
BtnMkv: TThemedButton;
cbBurn: TCheckBox;
cbRegion: TCheckBox;
lblNote: TLabel;
HasText: Boolean;
{ Ответ человека про вжигание, сохранённый до того, как MP4 навязал
своё. К нему возвращаемся при переходе на MKV. }
BurnWanted: Boolean;
{ Идёт ли согласование: пока идёт, ответы человека не запоминаются.
Установка галки кодом вызывает тот же обработчик, что и щелчок, и
без этого признака навязанное при MP4 значение записывалось бы как
осознанный выбор — при возврате на MKV галка так и оставалась
стоять. }
Applying: Boolean;
procedure FormatClick(Sender: TObject);
procedure BrowseClick(Sender: TObject);
procedure BurnClick(Sender: TObject);
procedure RegionClick(Sender: TObject);
function IsMp4: Boolean;
procedure ApplyFormat;
end;
function TExportDialog.IsMp4: Boolean;
begin
Result := BtnMp4.Down;
end;
{ Приводит имя файла и галку в согласие с выбранным контейнером. }
procedure TExportDialog.ApplyFormat;
var
Ext: string;
begin
Applying := True;
try
if IsMp4 then
Ext := '.mp4'
else
Ext := '.mkv';
if Trim(edPath.Text) <> '' then
edPath.Text := ChangeFileExt(edPath.Text, Ext);
if not HasText then
begin
lblNote.Caption := '';
Exit;
end;
if IsMp4 then
begin
{ Сначала запираем, потом ставим: наоборот обработчик успел бы счесть
навязанное значение осознанным. }
cbBurn.Enabled := False;
cbBurn.Checked := True;
lblNote.Caption := 'В MP4 отдельная дорожка субтитров теряет цвет, ' +
'обводку и положение, поэтому текст вжигается. Участки с текстом ' +
'придётся пересобрать.';
end
else
begin
cbBurn.Enabled := True;
cbBurn.Checked := BurnWanted;
if cbBurn.Checked then
lblNote.Caption := 'Участки с текстом придётся пересобрать. ' +
'Текст будет виден в любом проигрывателе.'
else
lblNote.Caption := 'Текст ляжет отдельной дорожкой субтитров: ' +
'сборка останется мгновенной, но показывать его будет не всякий ' +
'проигрыватель.';
end;
finally
Applying := False;
end;
end;
procedure TExportDialog.FormatClick(Sender: TObject);
begin
BtnMp4.Down := Sender = BtnMp4;
BtnMkv.Down := Sender = BtnMkv;
ApplyFormat;
end;
procedure TExportDialog.BurnClick(Sender: TObject);
begin
if Applying then
Exit;
{ Запоминаем только осознанный ответ: при MP4 галка стоит не по воле
человека, и запоминать её значило бы соврать ему в следующий раз. }
if cbBurn.Enabled then
BurnWanted := cbBurn.Checked;
ApplyFormat;
end;
{ Галка участка ничего не пересчитывает: она только запоминает ответ.
Всё, что от неё зависит, решается уже после закрытия окна. }
procedure TExportDialog.RegionClick(Sender: TObject);
begin
end;
procedure TExportDialog.BrowseClick(Sender: TObject);
var
Dlg: TSaveDialog;
begin
Dlg := TSaveDialog.Create(nil);
try
Dlg.Title := 'Куда собрать ролик';
Dlg.Options := Dlg.Options + [ofOverwritePrompt, ofPathMustExist];
if IsMp4 then
begin
Dlg.DefaultExt := '.mp4';
Dlg.Filter := 'MP4|*.mp4';
end
else
begin
Dlg.DefaultExt := '.mkv';
Dlg.Filter := 'Matroska|*.mkv';
end;
Dlg.FileName := edPath.Text;
if Dlg.Execute then
edPath.Text := Dlg.FileName;
finally
Dlg.Free;
end;
end;
function AskExport(var AChoice: TExportChoice; AHasText, APreferMkv: Boolean;
const ADefaultDir: string; const ARegion: string): Boolean;
var
Dlg: TExportDialog;
Lbl: TLabel;
BtnBrowse, BtnOk, BtnCancel: TThemedButton;
Dir: string;
begin
Result := False;
Dlg := TExportDialog.Create(nil);
try
Dlg.HasText := AHasText;
Dlg.BurnWanted := AChoice.BurnText;
Dlg.Form := TForm.CreateNew(nil);
Dlg.Form.Caption := 'Сборка ролика';
Dlg.Form.Position := poScreenCenter;
Dlg.Form.ClientWidth := 620;
Dlg.Form.ClientHeight := 272;
Lbl := TLabel.Create(Dlg.Form);
Lbl.Parent := Dlg.Form;
Lbl.SetBounds(12, 12, 200, 20);
Lbl.Caption := 'Куда собрать';
Dlg.edPath := TEdit.Create(Dlg.Form);
Dlg.edPath.Parent := Dlg.Form;
Dlg.edPath.SetBounds(12, 34, 490, 26);
Dlg.edPath.Anchors := [akTop, akLeft, akRight];
BtnBrowse := TThemedButton.Create(Dlg.Form);
BtnBrowse.Parent := Dlg.Form;
BtnBrowse.SetBounds(510, 33, 96, 28);
BtnBrowse.Anchors := [akTop, akRight];
BtnBrowse.Caption := 'Обзор...';
BtnBrowse.OnClick := @Dlg.BrowseClick;
Lbl := TLabel.Create(Dlg.Form);
Lbl.Parent := Dlg.Form;
Lbl.SetBounds(12, 76, 100, 20);
Lbl.Caption := 'Контейнер';
Dlg.BtnMp4 := TThemedButton.Create(Dlg.Form);
Dlg.BtnMp4.Parent := Dlg.Form;
Dlg.BtnMp4.SetBounds(110, 72, 90, 28);
Dlg.BtnMp4.Caption := 'MP4';
Dlg.BtnMp4.TabStop := False;
Dlg.BtnMp4.OnClick := @Dlg.FormatClick;
Dlg.BtnMkv := TThemedButton.Create(Dlg.Form);
Dlg.BtnMkv.Parent := Dlg.Form;
Dlg.BtnMkv.SetBounds(206, 72, 90, 28);
Dlg.BtnMkv.Caption := 'MKV';
Dlg.BtnMkv.TabStop := False;
Dlg.BtnMkv.OnClick := @Dlg.FormatClick;
if ARegion <> '' then
begin
Lbl := TLabel.Create(Dlg.Form);
Lbl.Parent := Dlg.Form;
Lbl.SetBounds(310, 76, 300, 20);
Lbl.Anchors := [akTop, akLeft, akRight];
Lbl.Caption := 'Только участок: ' + ARegion;
Lbl.Font.Color := TColor($30A0D0);
end;
Dlg.cbRegion := TCheckBox.Create(Dlg.Form);
Dlg.cbRegion.Parent := Dlg.Form;
Dlg.cbRegion.SetBounds(12, 112, 596, 22);
Dlg.cbRegion.Anchors := [akTop, akLeft, akRight];
Dlg.cbRegion.Caption := 'Только отмеченный участок: ' + ARegion;
Dlg.cbRegion.Visible := ARegion <> '';
Dlg.cbRegion.Checked := ARegion <> '';
Dlg.cbRegion.OnClick := @Dlg.RegionClick;
Dlg.cbBurn := TCheckBox.Create(Dlg.Form);
Dlg.cbBurn.Parent := Dlg.Form;
Dlg.cbBurn.SetBounds(12, 138, 300, 22);
Dlg.cbBurn.Caption := 'Вжечь текст в картинку';
Dlg.cbBurn.Visible := AHasText;
Dlg.cbBurn.OnClick := @Dlg.BurnClick;
Dlg.lblNote := TLabel.Create(Dlg.Form);
Dlg.lblNote.Parent := Dlg.Form;
Dlg.lblNote.SetBounds(12, 164, 596, 52);
Dlg.lblNote.Anchors := [akTop, akLeft, akRight];
Dlg.lblNote.AutoSize := False;
Dlg.lblNote.WordWrap := True;
BtnOk := TThemedButton.Create(Dlg.Form);
BtnOk.Parent := Dlg.Form;
BtnOk.SetBounds(400, 230, 100, 30);
BtnOk.Anchors := [akRight, akBottom];
BtnOk.Caption := 'Собрать';
BtnOk.ModalResult := mrOk;
BtnOk.Default := True;
BtnCancel := TThemedButton.Create(Dlg.Form);
BtnCancel.Parent := Dlg.Form;
BtnCancel.SetBounds(508, 230, 100, 30);
BtnCancel.Anchors := [akRight, akBottom];
BtnCancel.Caption := 'Отмена';
BtnCancel.ModalResult := mrCancel;
BtnCancel.Cancel := True;
ApplyDarkTheme(Dlg.Form);
{ Начальное имя: где были в прошлый раз, с расширением по контейнеру. }
Dir := ADefaultDir;
if Dir = '' then
Dir := GetCurrentDir;
if AChoice.Path <> '' then
Dlg.edPath.Text := AChoice.Path
else
Dlg.edPath.Text := IncludeTrailingPathDelimiter(Dir) + 'ролик.mkv';
{ Точные резы просят Matroska: в MP4 перекодированный стык может
воспроизводиться неправильно. }
Dlg.BtnMkv.Down := APreferMkv or
(LowerCase(ExtractFileExt(Dlg.edPath.Text)) = '.mkv');
Dlg.BtnMp4.Down := not Dlg.BtnMkv.Down;
Dlg.ApplyFormat;
if Dlg.Form.ShowModal <> mrOk then
Exit;
AChoice.Path := Trim(Dlg.edPath.Text);
if AChoice.Path = '' then
Exit;
AChoice.BurnText := AHasText and Dlg.cbBurn.Checked;
AChoice.RegionOnly := (ARegion <> '') and Dlg.cbRegion.Checked;
Result := True;
finally
Dlg.Form.Free;
Dlg.Free;
end;
end;
end.