浏览代码

Implemented autosaving

CPKreuz 1 年之前
父节点
当前提交
4b96289330

+ 12 - 1
src/PixiEditor/Data/Localization/Languages/en.json

@@ -589,5 +589,16 @@
   "COPY_COLOR": "Copy color",
 
   "FAILED_DOWNLOADING_TITLE": "Downloading update failed",
-  "FAILED_DOWNLOADING": "Failed downloading the update, you might not have enough space on the disk"
+  "FAILED_DOWNLOADING": "Failed downloading the update, you might not have enough space on the disk",
+  
+  "AUTOSAVE_SAVING": "Saving...",
+  "AUTOSAVE_SAVED": "Saved.",
+  "AUTOSAVE_SAVING_IN_MINUTE": "Saving in less than a minute.",
+  "AUTOSAVE_SAVING_IN": "Saving in about {0} {1}.",
+  "AUTOSAVE_WAITING_FOR_SAVE": "Waiting to autosave.",
+  "AUTOSAVE_PLEASE_RESAVE": "Error while saving. Save file manually to enable autosaving.",
+  "AUTOSAVE_FAILED_RETRYING": "Error while saving. Retrying in {0} {1}.",
+  
+  "MINUTE_SINGULAR": "minute",
+  "MINUTE_PLURAL": "minutes"
 }

+ 1 - 0
src/PixiEditor/Models/DocumentModels/ActionAccumulator.cs

@@ -39,6 +39,7 @@ internal class ActionAccumulator
         queuedActions.AddRange(actions);
         queuedActions.Add(new ChangeBoundary_Action());
         TryExecuteAccumulatedActions();
+        document.AutosaveViewModel.HintFinishedAction();
     }
 
     public void AddActions(params IAction[] actions)

+ 202 - 0
src/PixiEditor/ViewModels/SubViewModels/Document/AutosaveViewModel.cs

@@ -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;
+        });
+    }
+}

+ 2 - 0
src/PixiEditor/ViewModels/SubViewModels/Document/DocumentViewModel.cs

@@ -150,6 +150,7 @@ internal partial class DocumentViewModel : NotifyableObject
     public DocumentTransformViewModel TransformViewModel { get; }
     public ReferenceLayerViewModel ReferenceLayerViewModel { get; }
     public LineToolOverlayViewModel LineToolOverlayViewModel { get; }
+    public AutosaveViewModel AutosaveViewModel { get; }
 
     private DocumentInternalParts Internals { get; }
 
@@ -160,6 +161,7 @@ internal partial class DocumentViewModel : NotifyableObject
         StructureHelper = new DocumentStructureModule(this);
         EventInlet = new DocumentEventsModule(this, Internals);
         Operations = new DocumentOperationsModule(this, Internals);
+        AutosaveViewModel = new AutosaveViewModel(this);
 
         StructureRoot = new FolderViewModel(this, Internals, Internals.Tracker.Document.StructureRoot.GuidValue);
 

+ 2 - 0
src/PixiEditor/Views/MainWindow.xaml

@@ -468,6 +468,8 @@
                                    Foreground="White"/>
                     </Grid>
                 </Border>
+                <Label ui1:Translator.LocalizedString="{Binding DocumentManagerSubViewModel.ActiveDocument.AutosaveViewModel.MainMenuText, FallbackValue=''}"
+                       Foreground="White"/>
                 <StackPanel
                     DockPanel.Dock="Right"
                     VerticalAlignment="Top"