|
@@ -0,0 +1,202 @@
|
|
|
+using System.IO;
|
|
|
+using System.Windows.Threading;
|
|
|
+using PixiEditor.Extensions.Common.Localization;
|
|
|
+using PixiEditor.Extensions.Common.UserPreferences;
|
|
|
+using PixiEditor.Helpers;
|
|
|
+using PixiEditor.Models.IO;
|
|
|
+using PixiEditor.ViewModels.SubViewModels.Document;
|
|
|
+using Timer = System.Timers.Timer;
|
|
|
+
|
|
|
+namespace PixiEditor.Models.DocumentModels.Public;
|
|
|
+
|
|
|
+internal class AutosaveViewModel : NotifyableObject
|
|
|
+{
|
|
|
+ private readonly Timer savingTimer;
|
|
|
+ private readonly Timer updateTextTimer;
|
|
|
+ private bool saveAfterNextFinish;
|
|
|
+ private string mainMenuText;
|
|
|
+ private int savingFailed;
|
|
|
+ private DateTime nextSave;
|
|
|
+ private Guid tempGuid;
|
|
|
+
|
|
|
+ private DocumentViewModel Document { get; }
|
|
|
+
|
|
|
+ private double AutosavePeriodMinutes { get; set; }
|
|
|
+
|
|
|
+ public LocalizedString MainMenuText
|
|
|
+ {
|
|
|
+ get => mainMenuText;
|
|
|
+ set => SetProperty(ref mainMenuText, value);
|
|
|
+ }
|
|
|
+
|
|
|
+ public AutosaveViewModel(DocumentViewModel document)
|
|
|
+ {
|
|
|
+ Document = document;
|
|
|
+ tempGuid = Guid.NewGuid();
|
|
|
+ savingTimer = new Timer();
|
|
|
+ updateTextTimer = new Timer(TimeSpan.FromSeconds(5));
|
|
|
+
|
|
|
+ savingTimer.Elapsed += (_, _) => TryAutosave();
|
|
|
+ savingTimer.AutoReset = false;
|
|
|
+
|
|
|
+ updateTextTimer.Elapsed += (_, _) => SetAutosaveText();
|
|
|
+
|
|
|
+ var preferences = IPreferences.Current;
|
|
|
+
|
|
|
+ preferences.AddCallback<double>(nameof(AutosavePeriodMinutes), AutosavePeriodChanged);
|
|
|
+ AutosavePeriodChanged(preferences.GetPreference(nameof(AutosavePeriodMinutes), TimeSpan.FromMinutes(3).TotalMinutes));
|
|
|
+ SetAutosaveText();
|
|
|
+
|
|
|
+ savingTimer.Start();
|
|
|
+ updateTextTimer.Start();
|
|
|
+ }
|
|
|
+
|
|
|
+ public void HintFinishedAction()
|
|
|
+ {
|
|
|
+ if (!saveAfterNextFinish)
|
|
|
+ return;
|
|
|
+
|
|
|
+ saveAfterNextFinish = false;
|
|
|
+
|
|
|
+ SafeAutosave();
|
|
|
+ }
|
|
|
+
|
|
|
+ private void SetAutosaveText()
|
|
|
+ {
|
|
|
+ var timeLeft = nextSave - DateTime.Now;
|
|
|
+
|
|
|
+ if (timeLeft.Minutes == 0)
|
|
|
+ {
|
|
|
+ UpdateMainMenuTextSave("AUTOSAVE_SAVING_IN_MINUTE");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ var minute = timeLeft.Minutes < 2
|
|
|
+ ? new LocalizedString("MINUTE_SINGULAR")
|
|
|
+ : new LocalizedString("MINUTE_PLURAL");
|
|
|
+
|
|
|
+ UpdateMainMenuTextSave(new LocalizedString("AUTOSAVE_SAVING_IN", timeLeft.Minutes.ToString(), minute));
|
|
|
+ }
|
|
|
+
|
|
|
+ private void TryAutosave()
|
|
|
+ {
|
|
|
+ if (Document.UpdateableChangeActive)
|
|
|
+ {
|
|
|
+ saveAfterNextFinish = true;
|
|
|
+ UpdateMainMenuTextSave("AUTOSAVE_WAITING_FOR_SAVE");
|
|
|
+
|
|
|
+ savingTimer.Stop();
|
|
|
+ updateTextTimer.Stop();
|
|
|
+
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (Document.AllChangesSaved)
|
|
|
+ {
|
|
|
+ UpdateMainMenuTextSave("AUTOSAVE_SAVED");
|
|
|
+ updateTextTimer.Stop();
|
|
|
+ RestartTimers();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ updateTextTimer.Stop();
|
|
|
+ SafeAutosave();
|
|
|
+ }
|
|
|
+
|
|
|
+ private void SafeAutosave()
|
|
|
+ {
|
|
|
+ try
|
|
|
+ {
|
|
|
+ Autosave();
|
|
|
+ }
|
|
|
+ catch (Exception e)
|
|
|
+ {
|
|
|
+ savingFailed++;
|
|
|
+ UpdateMainMenuTextSave("AUTOSAVE_FAILED_RETRYING");
|
|
|
+
|
|
|
+ if (savingFailed == 1)
|
|
|
+ {
|
|
|
+ CrashHelper.SendExceptionInfoToWebhook(e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void Autosave()
|
|
|
+ {
|
|
|
+ saveAfterNextFinish = false;
|
|
|
+ Document.Busy = true;
|
|
|
+
|
|
|
+ UpdateMainMenuTextSave("AUTOSAVE_SAVING");
|
|
|
+
|
|
|
+ string filePath;
|
|
|
+
|
|
|
+ if (Document.FullFilePath == null || !Document.FullFilePath.EndsWith(".pixi"))
|
|
|
+ {
|
|
|
+ filePath = Path.Combine(Path.GetTempPath(), "PixiEditor", $"autosave-{tempGuid}.pixi");
|
|
|
+ Document.MarkAsSaved();
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ filePath = Document.FullFilePath;
|
|
|
+ }
|
|
|
+
|
|
|
+ var result = Exporter.TrySave(Document, filePath);
|
|
|
+
|
|
|
+ if (result == SaveResult.Success)
|
|
|
+ {
|
|
|
+ savingFailed = 0;
|
|
|
+ UpdateMainMenuTextSave("AUTOSAVE_SAVED");
|
|
|
+ }
|
|
|
+ else if (result is SaveResult.InvalidPath or SaveResult.SecurityError)
|
|
|
+ {
|
|
|
+ UpdateMainMenuTextSave("AUTOSAVE_PLEASE_RESAVE");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ var minute = AutosavePeriodMinutes <= 1
|
|
|
+ ? new LocalizedString("MINUTE_SINGULAR")
|
|
|
+ : new LocalizedString("MINUTE_PLURAL");
|
|
|
+
|
|
|
+ UpdateMainMenuTextSave(new LocalizedString("AUTOSAVE_FAILED_RETRYING", AutosavePeriodMinutes.ToString("0"), minute));
|
|
|
+ savingFailed++;
|
|
|
+
|
|
|
+ if (savingFailed == 3)
|
|
|
+ {
|
|
|
+ CrashHelper.SendExceptionInfoToWebhook(new Exception($"Failed to autosave 3 times in a row due to {result}"));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ RestartTimers();
|
|
|
+ Document.Busy = false;
|
|
|
+ }
|
|
|
+
|
|
|
+ private void RestartTimers()
|
|
|
+ {
|
|
|
+ savingTimer.Start();
|
|
|
+ nextSave = DateTime.Now + TimeSpan.FromMilliseconds(savingTimer.Interval);
|
|
|
+ updateTextTimer.Start();
|
|
|
+ }
|
|
|
+
|
|
|
+ private void AutosavePeriodChanged(double minutes)
|
|
|
+ {
|
|
|
+ var timerEnabled = savingTimer.Enabled;
|
|
|
+ savingTimer.Enabled = false;
|
|
|
+
|
|
|
+ var timeSpan = TimeSpan.FromMinutes(minutes);
|
|
|
+ savingTimer.Interval = timeSpan.TotalMilliseconds;
|
|
|
+ AutosavePeriodMinutes = minutes;
|
|
|
+
|
|
|
+ savingTimer.Enabled = timerEnabled;
|
|
|
+
|
|
|
+ nextSave = DateTime.Now + timeSpan;
|
|
|
+ }
|
|
|
+
|
|
|
+ private void UpdateMainMenuTextSave(LocalizedString text)
|
|
|
+ {
|
|
|
+ Dispatcher.CurrentDispatcher.Invoke(() =>
|
|
|
+ {
|
|
|
+ MainMenuText = text;
|
|
|
+ });
|
|
|
+ }
|
|
|
+}
|