Работа не теряется: автосохранение, видимое сохранение, запасной файл
Сохранение теперь отвечает в строке состояния: «Проект сохранён: имя, время». Раньше об этом знал только журнал, а он по умолчанию закрыт — нажал Ctrl+S и не знаешь, дошло ли до файла. Проект пишется во временный файл рядом и подставляется целиком; прежний остаётся как .bak. Оборванная запись больше не может оставить огрызок вместо проекта. Автосохранение: пока есть несохранённое, раз в полминуты рядом с проектом пишется копия .autosave. При открытии программа сравнивает её время с временем проекта и, если копия новее, показывает оба времени и предлагает её открыть. После сохранения проекта копия убирается. Журнал рядом с программой больше не переписывается при каждом запуске: запуски дописываются с заголовком, файл урезается при переросте. Прошлый запуск — как раз тот, в котором обычно и надо разбираться. Клавиша \ подтягивает окно к курсору и ставит его посередине: её жмут после реза или выделения. Переключатель «курсор посередине» остался кнопкой с целью.
This commit is contained in:
parent
86c28a3308
commit
18e55d466e
3 changed files with 319 additions and 20 deletions
24
README.md
24
README.md
|
|
@ -76,6 +76,7 @@ xdg-mime query default application/x-nlve-project
|
||||||
|---|---|
|
|---|---|
|
||||||
| Пуск и пауза | Пробел |
|
| Пуск и пауза | Пробел |
|
||||||
| В начало / конец монтажа | Home / End |
|
| В начало / конец монтажа | Home / End |
|
||||||
|
| Показать, где курсор (окно едет к нему) | \ |
|
||||||
| Разрезать по курсору (все дорожки) | S |
|
| Разрезать по курсору (все дорожки) | S |
|
||||||
| Разрезать точно (перекодирует стык) | Shift+S |
|
| Разрезать точно (перекодирует стык) | Shift+S |
|
||||||
| Удалить выделенное | Delete |
|
| Удалить выделенное | Delete |
|
||||||
|
|
@ -144,7 +145,7 @@ xdg-mime query default application/x-nlve-project
|
||||||
липнут при перетаскивании, курсор — при щелчке, и одно бывает нужно без
|
липнут при перетаскивании, курсор — при щелчке, и одно бывает нужно без
|
||||||
другого.
|
другого.
|
||||||
|
|
||||||
**Курсор посередине** (кнопка с целью, клавиша `\`). Включено — при
|
**Курсор посередине** (кнопка с целью). Включено — при
|
||||||
воспроизведении монтаж едет под курсором, и всегда видно, что будет
|
воспроизведении монтаж едет под курсором, и всегда видно, что будет
|
||||||
дальше. Выключено — окно не двигается вовсе, даже когда курсор ушёл за
|
дальше. Выключено — окно не двигается вовсе, даже когда курсор ушёл за
|
||||||
край: отмотал Shift с колесом — там и останешься. Раньше в выключенном
|
край: отмотал Shift с колесом — там и останешься. Раньше в выключенном
|
||||||
|
|
@ -251,6 +252,27 @@ Ctrl+Shift+R, который схлопывает дыру под курсоро
|
||||||
неправильно. Усиление меняет только рисунок, самого звука не касается, и
|
неправильно. Усиление меняет только рисунок, самого звука не касается, и
|
||||||
запоминается между запусками.
|
запоминается между запусками.
|
||||||
|
|
||||||
|
## Сохранение и автосохранение
|
||||||
|
|
||||||
|
**Сохранение видно.** После Ctrl+S внизу появляется «Проект сохранён:
|
||||||
|
имя, время». Раньше об этом говорила только строчка в журнале, а журнал
|
||||||
|
по умолчанию закрыт: нажал — и непонятно, дошло до файла или нет.
|
||||||
|
|
||||||
|
**Запись не оставляет огрызков.** Проект пишется во временный файл рядом
|
||||||
|
и подставляется на место настоящего, только когда записан целиком.
|
||||||
|
Прежний файл при этом становится запасным — `имя.nlve.bak`. Падение или
|
||||||
|
кончившееся место посреди записи больше не оставляют половину проекта.
|
||||||
|
|
||||||
|
**Копия пишется сама.** Пока в проекте есть несохранённое, раз в полминуты
|
||||||
|
рядом появляется `имя.nlve.autosave`. Это не сохранение: файл проекта не
|
||||||
|
трогается, признак «изменён» остаётся.
|
||||||
|
|
||||||
|
При открытии проекта программа смотрит, нет ли рядом копии новее его
|
||||||
|
самого. Есть — значит прошлая работа до файла не дошла, и программа
|
||||||
|
показывает оба времени и предлагает открыть копию. Отказ копию не
|
||||||
|
удаляет: она лежит, пока проект не сохранят заново. После сохранения
|
||||||
|
копия не нужна и убирается.
|
||||||
|
|
||||||
## Файл не совпадает с проектом
|
## Файл не совпадает с проектом
|
||||||
|
|
||||||
Проект имеет одно разрешение и один набор кодеков на всё — иначе куски не
|
Проект имеет одно разрешение и один набор кодеков на всё — иначе куски не
|
||||||
|
|
|
||||||
|
|
@ -95,6 +95,10 @@ type
|
||||||
FStarted: Boolean;
|
FStarted: Boolean;
|
||||||
|
|
||||||
FPreviewTimer: TTimer;
|
FPreviewTimer: TTimer;
|
||||||
|
{ Автосохранение: копия проекта пишется сама собой, пока человек
|
||||||
|
работает. Нужна ровно на один случай — работа пропала, а сохранить
|
||||||
|
её забыли или сохранение не дошло до файла. }
|
||||||
|
FAutoSaveTimer: TTimer;
|
||||||
FPreviewDirty: Boolean;
|
FPreviewDirty: Boolean;
|
||||||
FUpdatingFromPlayer: Boolean;
|
FUpdatingFromPlayer: Boolean;
|
||||||
FLastLoadedEdl: string;
|
FLastLoadedEdl: string;
|
||||||
|
|
@ -217,6 +221,8 @@ type
|
||||||
FSeekWaitTicks: Integer;
|
FSeekWaitTicks: Integer;
|
||||||
{ До какого времени ответ на команду держится в строке состояния. }
|
{ До какого времени ответ на команду держится в строке состояния. }
|
||||||
FStatusHoldUntil: TDateTime;
|
FStatusHoldUntil: TDateTime;
|
||||||
|
{ Автосохранение не удалось — жалуемся один раз, а не каждые полминуты. }
|
||||||
|
FAutoSaveBroken: Boolean;
|
||||||
|
|
||||||
procedure ApplyVolumeAt(const ATime: Double);
|
procedure ApplyVolumeAt(const ATime: Double);
|
||||||
{ Переписывает файл субтитров и отдаёт его плееру. }
|
{ Переписывает файл субтитров и отдаёт его плееру. }
|
||||||
|
|
@ -292,9 +298,16 @@ type
|
||||||
procedure RemindAboutDeps;
|
procedure RemindAboutDeps;
|
||||||
procedure Log(const AText: string);
|
procedure Log(const AText: string);
|
||||||
procedure LogToFile(const AText: string);
|
procedure LogToFile(const AText: string);
|
||||||
|
{ Урезает файл журнала, когда он перерос предел. }
|
||||||
|
procedure TrimLogFile;
|
||||||
{ AAnswer — это ответ на команду человека, а не проходное сообщение:
|
{ AAnswer — это ответ на команду человека, а не проходное сообщение:
|
||||||
такой ответ держится в строке несколько секунд. }
|
такой ответ держится в строке несколько секунд. }
|
||||||
procedure SetStatus(const AText: string; AAnswer: Boolean = False);
|
procedure SetStatus(const AText: string; AAnswer: Boolean = False);
|
||||||
|
{ Автосохранение: копия рядом с проектом. }
|
||||||
|
function AutoSavePath: string;
|
||||||
|
procedure DropAutoSave;
|
||||||
|
procedure AutoSaveTick(Sender: TObject);
|
||||||
|
procedure OfferAutoSave;
|
||||||
procedure UpdateCaption;
|
procedure UpdateCaption;
|
||||||
procedure UpdateTimeLabel;
|
procedure UpdateTimeLabel;
|
||||||
procedure RefreshPool;
|
procedure RefreshPool;
|
||||||
|
|
@ -415,6 +428,18 @@ const
|
||||||
затереть проходным сообщением. Четыре секунды — успеть прочесть. }
|
затереть проходным сообщением. Четыре секунды — успеть прочесть. }
|
||||||
StatusHoldTime = 4 / (24 * 60 * 60);
|
StatusHoldTime = 4 / (24 * 60 * 60);
|
||||||
|
|
||||||
|
{ Как часто пишется копия проекта для восстановления. Полминуты — это
|
||||||
|
потолок потерь: больше получаса работы человек в неё не вложит, а
|
||||||
|
запись проекта стоит миллисекунды. }
|
||||||
|
AutoSaveIntervalMs = 30 * 1000;
|
||||||
|
|
||||||
|
{ Приставка к имени файла проекта у автосохранённой копии. }
|
||||||
|
AutoSaveSuffix = '.autosave';
|
||||||
|
|
||||||
|
{ Предел для файла журнала. Больше двух мегабайт читать всё равно
|
||||||
|
никто не станет, а место жалко. }
|
||||||
|
MaxLogFileSize = 2 * 1024 * 1024;
|
||||||
|
|
||||||
function FormatTimecode(const Seconds: Double): string;
|
function FormatTimecode(const Seconds: Double): string;
|
||||||
var
|
var
|
||||||
Total, H, M, S, Ms: Integer;
|
Total, H, M, S, Ms: Integer;
|
||||||
|
|
@ -460,6 +485,11 @@ begin
|
||||||
FPreviewTimer.Interval := PreviewDebounceMs;
|
FPreviewTimer.Interval := PreviewDebounceMs;
|
||||||
FPreviewTimer.OnTimer := @PreviewTimerTick;
|
FPreviewTimer.OnTimer := @PreviewTimerTick;
|
||||||
|
|
||||||
|
FAutoSaveTimer := TTimer.Create(Self);
|
||||||
|
FAutoSaveTimer.Interval := AutoSaveIntervalMs;
|
||||||
|
FAutoSaveTimer.OnTimer := @AutoSaveTick;
|
||||||
|
FAutoSaveTimer.Enabled := True;
|
||||||
|
|
||||||
{ Список изображений держит миниатюры. Размер задан жёстко, и ffmpeg
|
{ Список изображений держит миниатюры. Размер задан жёстко, и ffmpeg
|
||||||
делает их ровно такими: список изображений в LCL требует одинакового
|
делает их ровно такими: список изображений в LCL требует одинакового
|
||||||
размера у всех элементов. }
|
размера у всех элементов. }
|
||||||
|
|
@ -1245,11 +1275,14 @@ begin
|
||||||
lstPool.ItemIndex := 0;
|
lstPool.ItemIndex := 0;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
{ Спрашивать про клипы на чужих дорожках можно только теперь: окно
|
{ Спрашивать можно только теперь: окно показано, и вопрос не окажется
|
||||||
показано, и вопрос не окажется за кадром. }
|
за кадром. Автосохранённая копия — первым делом: если прошлая работа
|
||||||
|
не дошла до файла, человек должен узнать об этом до того, как начнёт
|
||||||
|
правку поверх старого. }
|
||||||
if FCheckMisplacedOnShow then
|
if FCheckMisplacedOnShow then
|
||||||
begin
|
begin
|
||||||
FCheckMisplacedOnShow := False;
|
FCheckMisplacedOnShow := False;
|
||||||
|
OfferAutoSave;
|
||||||
CheckMisplacedClips;
|
CheckMisplacedClips;
|
||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
@ -1334,12 +1367,57 @@ end;
|
||||||
|
|
||||||
{ --- Мелочи интерфейса --- }
|
{ --- Мелочи интерфейса --- }
|
||||||
|
|
||||||
|
{ Урезание журнала до последних строк, когда файл перерос предел.
|
||||||
|
|
||||||
|
Читается целиком: предел небольшой, и возиться с чтением с конца ради
|
||||||
|
пары мегабайт незачем. }
|
||||||
|
procedure TMainForm.TrimLogFile;
|
||||||
|
var
|
||||||
|
L: TStringList;
|
||||||
|
Keep: Integer;
|
||||||
|
Found: TSearchRec;
|
||||||
|
Size: Int64;
|
||||||
|
begin
|
||||||
|
if not FileExists(FLogFilePath) then
|
||||||
|
Exit;
|
||||||
|
Size := 0;
|
||||||
|
if FindFirst(FLogFilePath, faAnyFile, Found) = 0 then
|
||||||
|
begin
|
||||||
|
Size := Found.Size;
|
||||||
|
FindClose(Found);
|
||||||
|
end;
|
||||||
|
if Size <= MaxLogFileSize then
|
||||||
|
Exit;
|
||||||
|
L := TStringList.Create;
|
||||||
|
try
|
||||||
|
try
|
||||||
|
L.LoadFromFile(FLogFilePath);
|
||||||
|
Keep := L.Count div 2;
|
||||||
|
while L.Count > Keep do
|
||||||
|
L.Delete(0);
|
||||||
|
L.Insert(0, '=== начало журнала урезано ===');
|
||||||
|
L.SaveToFile(FLogFilePath);
|
||||||
|
except
|
||||||
|
{ Не вышло урезать — не беда: журнал не работа, ради которой сюда
|
||||||
|
пришли. }
|
||||||
|
end;
|
||||||
|
finally
|
||||||
|
L.Free;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
{ Дублирование журнала в файл рядом с программой.
|
{ Дублирование журнала в файл рядом с программой.
|
||||||
|
|
||||||
Нужно ровно для одного случая: программа падает до того, как окно
|
Нужно ровно для одного случая: программа падает до того, как окно
|
||||||
успело появиться. Тогда всё, что она о себе рассказала, пропадает
|
успело появиться. Тогда всё, что она о себе рассказала, пропадает
|
||||||
вместе с окном, и причина остаётся неизвестной. Файл переписывается
|
вместе с окном, и причина остаётся неизвестной.
|
||||||
при каждом запуске: интересен последний.
|
|
||||||
|
Файл не переписывается при запуске, а дописывается: разбираться обычно
|
||||||
|
приходится не с тем запуском, который идёт сейчас, а с прошлым, и
|
||||||
|
затирать его нельзя — второй запуск программы уносил бы единственное
|
||||||
|
свидетельство. Каждый запуск отделён строкой с датой и временем. Файл
|
||||||
|
урезается, когда перерастёт предел: бесконечно растущий журнал никому
|
||||||
|
не нужен.
|
||||||
|
|
||||||
Неудача записи ничего не значит — каталог может быть закрыт на запись,
|
Неудача записи ничего не значит — каталог может быть закрыт на запись,
|
||||||
и работать это мешать не должно. }
|
и работать это мешать не должно. }
|
||||||
|
|
@ -1350,14 +1428,19 @@ begin
|
||||||
if FLogFileBroken then
|
if FLogFileBroken then
|
||||||
Exit;
|
Exit;
|
||||||
try
|
try
|
||||||
|
if not FLogFileStarted then
|
||||||
|
begin
|
||||||
|
TrimLogFile;
|
||||||
|
FLogFileStarted := True;
|
||||||
|
LogToFile('');
|
||||||
|
LogToFile('=== запуск ' + FormatDateTime('dd.mm.yyyy hh:nn:ss', Now) +
|
||||||
|
' ===');
|
||||||
|
end;
|
||||||
AssignFile(F, FLogFilePath);
|
AssignFile(F, FLogFilePath);
|
||||||
if FLogFileStarted then
|
if FileExists(FLogFilePath) then
|
||||||
Append(F)
|
Append(F)
|
||||||
else
|
else
|
||||||
begin
|
|
||||||
Rewrite(F);
|
Rewrite(F);
|
||||||
FLogFileStarted := True;
|
|
||||||
end;
|
|
||||||
try
|
try
|
||||||
WriteLn(F, AText);
|
WriteLn(F, AText);
|
||||||
finally
|
finally
|
||||||
|
|
@ -2363,6 +2446,9 @@ begin
|
||||||
FSettings.NoteRecentProject(FProject.FileName);
|
FSettings.NoteRecentProject(FProject.FileName);
|
||||||
RefreshRecentMenu;
|
RefreshRecentMenu;
|
||||||
Log('Открыт проект: ' + FProject.FileName);
|
Log('Открыт проект: ' + FProject.FileName);
|
||||||
|
{ Если рядом лежит копия новее проекта — значит прошлая работа не дошла
|
||||||
|
до файла, и об этом надо сказать сразу при открытии. }
|
||||||
|
OfferAutoSave;
|
||||||
CheckMisplacedClips;
|
CheckMisplacedClips;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
|
@ -3322,12 +3408,21 @@ begin
|
||||||
Exit;
|
Exit;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
{ Обратная косая: пункта меню для неё нет намеренно — сочетание из
|
{ Обратная косая — «покажи, где курсор»: окно едет к нему и ставит его
|
||||||
одной такой клавиши в меню выглядит как опечатка, а работать должно. }
|
посередине. Жмут её чаще всего после реза или выделения, когда курсор
|
||||||
|
стоит на границе и надо к ней вернуться.
|
||||||
|
|
||||||
|
Раньше она переключала режим «курсор посередине». Переключатель
|
||||||
|
остался кнопкой с целью внизу: включают его раз и надолго, а
|
||||||
|
подтягивать окно к курсору нужно постоянно.
|
||||||
|
|
||||||
|
Пункта меню для неё нет намеренно — сочетание из одной такой клавиши
|
||||||
|
в меню выглядит как опечатка, а работать должно. }
|
||||||
if (Key = VK_OEM_5) and (Shift = []) then
|
if (Key = VK_OEM_5) and (Shift = []) then
|
||||||
begin
|
begin
|
||||||
Key := 0;
|
Key := 0;
|
||||||
ToggleCenterPlayhead;
|
FTimeline.ScrollToPlayhead(True);
|
||||||
|
SetStatus('Окно у курсора: ' + FormatTimecode(FTimeline.Playhead), True);
|
||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
|
@ -3391,10 +3486,134 @@ begin
|
||||||
else
|
else
|
||||||
begin
|
begin
|
||||||
UpdateCaption;
|
UpdateCaption;
|
||||||
Log('Проект сохранён.');
|
{ Сохранение должно быть видно. Раньше о нём говорила только строчка
|
||||||
|
в журнале, а журнал по умолчанию закрыт: человек нажимал Ctrl+S,
|
||||||
|
ничего не происходило, и понять, дошло сохранение до файла или нет,
|
||||||
|
было не по чему. }
|
||||||
|
SetStatus('Проект сохранён: ' + ExtractFileName(FProject.FileName) +
|
||||||
|
', ' + FormatDateTime('hh:nn:ss', Now), True);
|
||||||
|
Log('Проект сохранён: ' + FProject.FileName);
|
||||||
|
{ Копия для восстановления больше не нужна: в файле то же самое. }
|
||||||
|
DropAutoSave;
|
||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
{ Где лежит автосохранённая копия проекта. Рядом с самим проектом, чтобы
|
||||||
|
она нашлась там же, где его ищут. У безымянного проекта её нет: некуда
|
||||||
|
положить, и восстанавливать потом нечего. }
|
||||||
|
function TMainForm.AutoSavePath: string;
|
||||||
|
begin
|
||||||
|
if FProject.FileName = '' then
|
||||||
|
Result := ''
|
||||||
|
else
|
||||||
|
Result := FProject.FileName + AutoSaveSuffix;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TMainForm.DropAutoSave;
|
||||||
|
var
|
||||||
|
Path: string;
|
||||||
|
begin
|
||||||
|
Path := AutoSavePath;
|
||||||
|
if (Path <> '') and FileExists(Path) then
|
||||||
|
DeleteFile(Path);
|
||||||
|
end;
|
||||||
|
|
||||||
|
{ Копия проекта пишется, пока в нём есть несохранённое.
|
||||||
|
|
||||||
|
Это не замена сохранению: файл проекта не трогается, имя не меняется,
|
||||||
|
признак «изменён» остаётся. Копия нужна на случай, когда работа всё же
|
||||||
|
пропала — программу закрыли, она упала, сохранение не дошло до файла. }
|
||||||
|
procedure TMainForm.AutoSaveTick(Sender: TObject);
|
||||||
|
var
|
||||||
|
Path, Err: string;
|
||||||
|
begin
|
||||||
|
if FProject = nil then
|
||||||
|
Exit;
|
||||||
|
Path := AutoSavePath;
|
||||||
|
if Path = '' then
|
||||||
|
Exit;
|
||||||
|
{ Несохранённого нет — писать нечего. Копию при этом не трогаем: она
|
||||||
|
может быть единственным следом прошлой работы, а спросить о ней ещё
|
||||||
|
не успели. Убирается она там, где точно больше не нужна: после
|
||||||
|
сохранения проекта и после ответа на вопрос при открытии. }
|
||||||
|
if not FProject.Modified then
|
||||||
|
Exit;
|
||||||
|
if not FProject.SaveCopyTo(Path, Err) then
|
||||||
|
begin
|
||||||
|
{ Жалуемся один раз и больше не мешаем: автосохранение — подстраховка,
|
||||||
|
а не работа, ради которой человек сюда пришёл. }
|
||||||
|
if not FAutoSaveBroken then
|
||||||
|
begin
|
||||||
|
FAutoSaveBroken := True;
|
||||||
|
Log('Автосохранение не работает: ' + Err);
|
||||||
|
end;
|
||||||
|
end
|
||||||
|
else
|
||||||
|
FAutoSaveBroken := False;
|
||||||
|
end;
|
||||||
|
|
||||||
|
{ Проверка при открытии: не осталась ли копия новее самого проекта.
|
||||||
|
|
||||||
|
Так бывает ровно в одном случае — работа не дошла до файла проекта.
|
||||||
|
Молча подменять файл нельзя, поэтому спрашиваем и показываем время. }
|
||||||
|
procedure TMainForm.OfferAutoSave;
|
||||||
|
var
|
||||||
|
Path, Err: string;
|
||||||
|
CopyStamp, ProjStamp: LongInt;
|
||||||
|
CopyTime, ProjTime: TDateTime;
|
||||||
|
begin
|
||||||
|
Path := AutoSavePath;
|
||||||
|
if (Path = '') or (not FileExists(Path)) then
|
||||||
|
Exit;
|
||||||
|
CopyStamp := FileAge(Path);
|
||||||
|
ProjStamp := FileAge(FProject.FileName);
|
||||||
|
if (CopyStamp < 0) or (ProjStamp < 0) then
|
||||||
|
Exit;
|
||||||
|
CopyTime := FileDateToDateTime(CopyStamp);
|
||||||
|
ProjTime := FileDateToDateTime(ProjStamp);
|
||||||
|
{ Копия старше проекта — значит проект сохранён позже неё, и она уже
|
||||||
|
ни о чём не говорит. }
|
||||||
|
if CopyTime <= ProjTime + 1 / (24 * 60 * 60) then
|
||||||
|
begin
|
||||||
|
DeleteFile(Path);
|
||||||
|
Exit;
|
||||||
|
end;
|
||||||
|
|
||||||
|
Log(Format('Найдена автосохранённая копия от %s, проект сохранён %s.',
|
||||||
|
[FormatDateTime('dd.mm hh:nn:ss', CopyTime),
|
||||||
|
FormatDateTime('dd.mm hh:nn:ss', ProjTime)]));
|
||||||
|
|
||||||
|
if ThemedMessageDlg('nlve',
|
||||||
|
Format('Рядом с проектом лежит автосохранённая копия от %s, ' +
|
||||||
|
'а сам проект сохранён %s.' + LineEnding + LineEnding +
|
||||||
|
'Похоже, последние правки не дошли до файла проекта. ' +
|
||||||
|
'Открыть копию?' + LineEnding + LineEnding +
|
||||||
|
'Проект при этом не перезаписывается: копия просто откроется, и ' +
|
||||||
|
'сохранить её поверх проекта можно будет самому. Откажетесь — ' +
|
||||||
|
'копия останется лежать рядом, пока проект не сохранят заново.',
|
||||||
|
[FormatDateTime('dd.mm.yyyy hh:nn:ss', CopyTime),
|
||||||
|
FormatDateTime('dd.mm.yyyy hh:nn:ss', ProjTime)]),
|
||||||
|
mtConfirmation, [mbYes, mbNo], 0) <> mrYes then
|
||||||
|
Exit;
|
||||||
|
|
||||||
|
if not FProject.LoadFromFile(Path, Err) then
|
||||||
|
begin
|
||||||
|
ThemedMessageDlg('nlve', Err, mtError, [mbOK], 0);
|
||||||
|
Exit;
|
||||||
|
end;
|
||||||
|
|
||||||
|
{ Имя возвращаем проектное: копия открыта, но работать человек
|
||||||
|
продолжает с проектом, и Ctrl+S должен писать в него. }
|
||||||
|
FProject.FileName := Copy(Path, 1, Length(Path) - Length(AutoSaveSuffix));
|
||||||
|
FProject.Modified := True;
|
||||||
|
RefreshPool;
|
||||||
|
UpdateCaption;
|
||||||
|
FLastLoadedEdl := '';
|
||||||
|
SchedulePreview;
|
||||||
|
SetStatus('Открыта автосохранённая копия. Проверьте и сохраните: Ctrl+S.',
|
||||||
|
True);
|
||||||
|
end;
|
||||||
|
|
||||||
procedure TMainForm.MenuSaveProject(Sender: TObject);
|
procedure TMainForm.MenuSaveProject(Sender: TObject);
|
||||||
begin
|
begin
|
||||||
DoSaveProject;
|
DoSaveProject;
|
||||||
|
|
|
||||||
|
|
@ -377,6 +377,9 @@ type
|
||||||
|
|
||||||
{ --- Файл проекта --- }
|
{ --- Файл проекта --- }
|
||||||
function SaveToFile(const APath: string; out AError: string): Boolean;
|
function SaveToFile(const APath: string; out AError: string): Boolean;
|
||||||
|
{ Копия проекта в другой файл: имя проекта и признак «изменён» не
|
||||||
|
трогаются. Для автосохранения. }
|
||||||
|
function SaveCopyTo(const APath: string; out AError: string): Boolean;
|
||||||
function LoadFromFile(const APath: string; out AError: string): Boolean;
|
function LoadFromFile(const APath: string; out AError: string): Boolean;
|
||||||
|
|
||||||
property Pool[AIndex: Integer]: TMediaItem read GetPoolItem;
|
property Pool[AIndex: Integer]: TMediaItem read GetPoolItem;
|
||||||
|
|
@ -1843,7 +1846,10 @@ begin
|
||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
function TProject.SaveToFile(const APath: string; out AError: string): Boolean;
|
{ Запись проекта в произвольный файл, не трогая ни имя проекта, ни признак
|
||||||
|
«изменён». Нужна для автосохранения: копия пишется сама собой, и проект
|
||||||
|
от этого не должен считаться сохранённым. }
|
||||||
|
function TProject.SaveCopyTo(const APath: string; out AError: string): Boolean;
|
||||||
var
|
var
|
||||||
Text: TStringList;
|
Text: TStringList;
|
||||||
begin
|
begin
|
||||||
|
|
@ -1851,23 +1857,75 @@ begin
|
||||||
Result := False;
|
Result := False;
|
||||||
Text := TStringList.Create;
|
Text := TStringList.Create;
|
||||||
try
|
try
|
||||||
{ Имя файла назначаем до записи: от него зависит путь к кэшу, который
|
|
||||||
попадает внутрь проекта косвенно — через сгенерированные клипы. }
|
|
||||||
FFileName := ExpandFileName(APath);
|
|
||||||
Text.Text := Serialize;
|
Text.Text := Serialize;
|
||||||
try
|
try
|
||||||
Text.SaveToFile(FFileName);
|
Text.SaveToFile(APath);
|
||||||
FModified := False;
|
|
||||||
Result := True;
|
Result := True;
|
||||||
except
|
except
|
||||||
on E: Exception do
|
on E: Exception do
|
||||||
AError := 'Не удалось сохранить проект: ' + E.Message;
|
AError := 'Не удалось записать ' + APath + ': ' + E.Message;
|
||||||
end;
|
end;
|
||||||
finally
|
finally
|
||||||
Text.Free;
|
Text.Free;
|
||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
{ Сохранение проекта.
|
||||||
|
|
||||||
|
Пишем сперва во временный файл рядом, и только когда он записан целиком,
|
||||||
|
подставляем его на место настоящего. Прежний файл при этом становится
|
||||||
|
запасным (.bak). Так падение или полный диск посреди записи не оставляют
|
||||||
|
от проекта огрызок, а испорченное сохранение всегда можно откатить.
|
||||||
|
|
||||||
|
Признак «изменён» снимается только после того, как файл на месте: если
|
||||||
|
запись не удалась, проект остаётся несохранённым и программа спросит
|
||||||
|
о нём при выходе. }
|
||||||
|
function TProject.SaveToFile(const APath: string; out AError: string): Boolean;
|
||||||
|
var
|
||||||
|
Full, Temp, Backup: string;
|
||||||
|
begin
|
||||||
|
AError := '';
|
||||||
|
Result := False;
|
||||||
|
{ Имя файла назначаем до записи: от него зависит путь к кэшу, который
|
||||||
|
попадает внутрь проекта косвенно — через сгенерированные клипы. }
|
||||||
|
Full := ExpandFileName(APath);
|
||||||
|
FFileName := Full;
|
||||||
|
Temp := Full + '.tmp';
|
||||||
|
Backup := Full + '.bak';
|
||||||
|
|
||||||
|
if not SaveCopyTo(Temp, AError) then
|
||||||
|
Exit;
|
||||||
|
|
||||||
|
try
|
||||||
|
if FileExists(Full) then
|
||||||
|
begin
|
||||||
|
if FileExists(Backup) then
|
||||||
|
DeleteFile(Backup);
|
||||||
|
if not RenameFile(Full, Backup) then
|
||||||
|
begin
|
||||||
|
AError := 'Не удалось отложить прежний файл проекта: ' + Backup;
|
||||||
|
Exit;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
if not RenameFile(Temp, Full) then
|
||||||
|
begin
|
||||||
|
{ Подстановка не удалась — возвращаем прежний файл на место, чтобы
|
||||||
|
не остаться вообще без проекта. }
|
||||||
|
if FileExists(Backup) and (not FileExists(Full)) then
|
||||||
|
RenameFile(Backup, Full);
|
||||||
|
AError := 'Не удалось записать проект: ' + Full;
|
||||||
|
Exit;
|
||||||
|
end;
|
||||||
|
|
||||||
|
FModified := False;
|
||||||
|
Result := True;
|
||||||
|
except
|
||||||
|
on E: Exception do
|
||||||
|
AError := 'Не удалось сохранить проект: ' + E.Message;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
function TProject.LoadFromFile(const APath: string; out AError: string): Boolean;
|
function TProject.LoadFromFile(const APath: string; out AError: string): Boolean;
|
||||||
var
|
var
|
||||||
Text: TStringList;
|
Text: TStringList;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue