Browse Source

Autosave progress

Equbuxu 1 year ago
parent
commit
a66cf204f2
34 changed files with 884 additions and 394 deletions
  1. 5 1
      src/PixiEditor.Extensions/Common/UserPreferences/PreferencesConstants.cs
  2. 3 1
      src/PixiEditor.sln.DotSettings
  3. 22 0
      src/PixiEditor/Helpers/AutosaveHelper.cs
  4. 10 16
      src/PixiEditor/Helpers/Converters/AutosaveSettingsPeriodToValueConverter.cs
  5. 4 4
      src/PixiEditor/Helpers/DeadlockDetectionHelper.cs
  6. 2 1
      src/PixiEditor/Models/Constants.cs
  7. 0 25
      src/PixiEditor/Models/DataHolders/AutosaveFilePathInfo.cs
  8. 19 13
      src/PixiEditor/Models/DataHolders/CrashReport.cs
  9. 11 0
      src/PixiEditor/Models/DataHolders/CrashReportAutosaveFilePathInfo.cs
  10. 11 0
      src/PixiEditor/Models/DocumentModels/Autosave/AutosaveHistoryEntry.cs
  11. 8 0
      src/PixiEditor/Models/DocumentModels/Autosave/AutosaveHistorySession.cs
  12. 31 0
      src/PixiEditor/Models/DocumentModels/Autosave/AutosaverAwaitUpdateableChangeEndJob.cs
  13. 78 0
      src/PixiEditor/Models/DocumentModels/Autosave/AutosaverSaveBackupJob.cs
  14. 72 0
      src/PixiEditor/Models/DocumentModels/Autosave/AutosaverSaveUserFileJob.cs
  15. 35 0
      src/PixiEditor/Models/DocumentModels/Autosave/AutosaverWaitJob.cs
  16. 211 0
      src/PixiEditor/Models/DocumentModels/Autosave/DocumentAutosaver.cs
  17. 8 0
      src/PixiEditor/Models/DocumentModels/Autosave/Enums/AutosaveHistoryResult.cs
  18. 7 0
      src/PixiEditor/Models/DocumentModels/Autosave/Enums/AutosaveHistoryType.cs
  19. 9 0
      src/PixiEditor/Models/DocumentModels/Autosave/Enums/AutosaveState.cs
  20. 10 0
      src/PixiEditor/Models/DocumentModels/Autosave/Enums/BackupAutosaveResult.cs
  21. 9 0
      src/PixiEditor/Models/DocumentModels/Autosave/Enums/UserFileAutosaveResult.cs
  22. 14 0
      src/PixiEditor/Models/DocumentModels/Autosave/IAutosaverJob.cs
  23. 12 0
      src/PixiEditor/Models/DocumentModels/Autosave/Structs/AutosaveStateData.cs
  24. 9 0
      src/PixiEditor/Models/DocumentModels/Autosave/Structs/LastBackupAutosaveData.cs
  25. 10 0
      src/PixiEditor/Models/DocumentModels/Autosave/Structs/LastUserFileAutosaveData.cs
  26. 79 219
      src/PixiEditor/ViewModels/SubViewModels/Document/AutosaveDocumentViewModel.cs
  27. 43 12
      src/PixiEditor/ViewModels/SubViewModels/Main/AutosaveViewModel.cs
  28. 81 6
      src/PixiEditor/ViewModels/SubViewModels/Main/FileViewModel.cs
  29. 9 0
      src/PixiEditor/ViewModels/SubViewModels/UserPreferences/Settings/FileSettings.cs
  30. 8 12
      src/PixiEditor/ViewModels/ViewModelMain.cs
  31. 8 3
      src/PixiEditor/Views/Dialogs/SettingsWindow.xaml
  32. 2 1
      src/PixiEditor/Views/MainWindow.xaml
  33. 8 8
      src/PixiEditor/Views/UserControls/AutosaveControl.xaml
  34. 36 72
      src/PixiEditor/Views/UserControls/AutosaveControl.xaml.cs

+ 5 - 1
src/PixiEditor.Extensions/Common/UserPreferences/PreferencesConstants.cs

@@ -24,9 +24,13 @@ public static class PreferencesConstants
     [SyncedPreferenceConstant]
     public const string AutosavePeriodMinutes = nameof(AutosavePeriodMinutes);
     public const double AutosavePeriodDefault = 3;
+    
+    [SyncedPreferenceConstant]
+    public const string AutosaveEnabled = nameof(AutosaveEnabled);
+    public const bool AutosaveEnabledDefault = true;
 
     [LocalPreferenceConstant]
-    public const string UnsavedNextSessionFiles = nameof(UnsavedNextSessionFiles);
+    public const string AutosaveHistory = nameof(AutosaveHistory);
 
     [SyncedPreferenceConstant]
     public const string AutosaveToDocumentPath = nameof(AutosaveToDocumentPath);

+ 3 - 1
src/PixiEditor.sln.DotSettings

@@ -1,2 +1,4 @@
 <wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
-	<s:Boolean x:Key="/Default/UserDictionary/Words/=Autosave/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
+	<s:Boolean x:Key="/Default/UserDictionary/Words/=Autosave/@EntryIndexedValue">True</s:Boolean>
+	<s:Boolean x:Key="/Default/UserDictionary/Words/=Autosaved/@EntryIndexedValue">True</s:Boolean>
+	<s:Boolean x:Key="/Default/UserDictionary/Words/=Autosaver/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

+ 22 - 0
src/PixiEditor/Helpers/AutosaveHelper.cs

@@ -0,0 +1,22 @@
+using System.IO;
+using PixiEditor.Models.IO;
+
+namespace PixiEditor.Helpers;
+
+#nullable  enable
+internal class AutosaveHelper
+{
+    public static Guid? GetAutosaveGuid(string? path)
+    {
+        if (path is null)
+            return null;
+        
+        string guidString = Path.GetFileNameWithoutExtension(path)["autosave-".Length..];
+        return Guid.Parse(guidString);
+    }
+
+    public static string GetAutosavePath(Guid guid)
+    {
+        return Path.Join(Paths.PathToUnsavedFilesFolder, $"autosave-{guid}.pixi");
+    }
+}

+ 10 - 16
src/PixiEditor/Helpers/Converters/AutosaveSettingsPeriodToValueConverter.cs

@@ -1,34 +1,28 @@
 using System.Globalization;
 using PixiEditor.Extensions.Common.Localization;
+using PixiEditor.Extensions.Common.UserPreferences;
 using PixiEditor.Helpers.Converters;
 
 namespace PixiEditor.Helpers.Converters;
 
-internal class AutosaveSettingsPeriodToValueConverter : MarkupConverter
+internal class AutosaveSettingsPeriodToValueConverter : MultiValueMarkupConverter
 {
-    public bool ReturnBoolAutosavingEnabled { get; set; }
-    
-    public override object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+    public override object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
     {
-        if (ReturnBoolAutosavingEnabled)
+        return (values[0], values[1]) switch
         {
-            return value is not -1.0;
-        }
-        
-        return value switch
-        {
-            -1.0 => new LocalizedString("DISABLED"),
-            double d => new LocalizedString(d.ToString(CultureInfo.InvariantCulture)),
-            _ => throw new ArgumentException($"{value} has invalid type")
+            (false, _) => new LocalizedString("DISABLED"),
+            (true, double d) => new LocalizedString(d.ToString(CultureInfo.InvariantCulture)),
+            _ => throw new ArgumentException($"{values[0]} {values[1]} are invalid")
         };
     }
 
-    public override object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+    public override object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
     {
         return value switch
         {
-            LocalizedString { Key: "DISABLED" } => -1,
-            LocalizedString s when double.TryParse(s.Key, out double period) => period,
+            LocalizedString { Key: "DISABLED" } => [false, PreferencesConstants.AutosavePeriodDefault],
+            LocalizedString s when double.TryParse(s.Key, out double period) => [true, period],
             _ => throw new ArgumentException($"{value} has invalid type")
         };
     }

+ 4 - 4
src/PixiEditor/Helpers/DeadlockDetectionHelper.cs

@@ -139,7 +139,7 @@ internal class DeadlockDetectionHelper
 
     private void TrySaveFilesForNextSession()
     {
-        var thread = new Thread(() =>
+        /*var thread = new Thread(() =>
         {
             var viewModel = ViewModelMain.Current;
             
@@ -147,9 +147,9 @@ internal class DeadlockDetectionHelper
             foreach (var document in viewModel.DocumentManagerSubViewModel.Documents)
             {
                 document.AutosaveViewModel.PanicAutosaveFromDeadlockDetector();
-                if (document.AutosaveViewModel.LastSavedPath != null || document.FullFilePath != null)
+                if (document.AutosaveViewModel.LastAutosavedPath != null || document.FullFilePath != null)
                 {
-                    list.Add(new AutosaveFilePathInfo(document.FullFilePath, document.AutosaveViewModel.LastSavedPath));
+                    list.Add(new AutosaveFilePathInfo(document.FullFilePath, document.AutosaveViewModel.LastAutosavedPath));
                 }
             }
         
@@ -157,7 +157,7 @@ internal class DeadlockDetectionHelper
         });
         
         thread.Start();
-        thread.Join(10000);
+        thread.Join(10000);*/
     }
 
     private void ForceNewProcess()

+ 2 - 1
src/PixiEditor/Models/Constants.cs

@@ -5,9 +5,10 @@ internal class Constants
     public const int DefaultCanvasSize = 64;
     public const int MaxPreviewWidth = 128;
     public const int MaxPreviewHeight = 128;
+    public const int MaxAutosaveFilesLifetimeDays = 10;
 
     public const int MaxCanvasSize = 9999;
 
     public const string NativeExtensionNoDot = "pixi";
     public const string NativeExtension = "." + NativeExtensionNoDot;
-}
+}

+ 0 - 25
src/PixiEditor/Models/DataHolders/AutosaveFilePathInfo.cs

@@ -1,25 +0,0 @@
-using System.IO;
-
-namespace PixiEditor.Models.DataHolders;
-
-public class AutosaveFilePathInfo
-{
-    public string? OriginalPath { get; set; }
-    
-    public string? AutosavePath { get; set; }
-
-    public Guid? GetAutosaveGuid()
-    {
-        if (AutosavePath == null)
-            return null;
-        
-        string guidString = Path.GetFileNameWithoutExtension(AutosavePath)["autosave-".Length..];
-        return Guid.Parse(guidString);
-    }
-    
-    public AutosaveFilePathInfo(string? originalPath, string? autosavePath)
-    {
-        OriginalPath = originalPath;
-        AutosavePath = autosavePath;
-    }
-}

+ 19 - 13
src/PixiEditor/Models/DataHolders/CrashReport.cs

@@ -128,8 +128,8 @@ internal class CrashReport : IDisposable
             .AppendLine($"  Has shared toolbar enabled: {GetPreferenceFormatted("EnableSharedToolbar", true, false)}")
             .AppendLine($"  Right click mode: {GetPreferenceFormatted<RightClickMode>("RightClickMode", true)}")
             .AppendLine($"  Has Rich presence enabled: {GetPreferenceFormatted("EnableRichPresence", true, true)}")
-            .AppendLine(
-                $"  Autosaving Enabled: {GetPreferenceFormatted(PreferencesConstants.AutosavePeriodMinutes, true, PreferencesConstants.AutosavePeriodDefault)}")
+            .AppendLine($"  Autosaving Enabled: {GetPreferenceFormatted(PreferencesConstants.AutosaveEnabled, true, PreferencesConstants.AutosaveEnabledDefault)}")
+            .AppendLine($"  Autosaving Period: {GetPreferenceFormatted(PreferencesConstants.AutosavePeriodMinutes, true, PreferencesConstants.AutosavePeriodDefault)}")
             .AppendLine($"  Debug Mode enabled: {GetPreferenceFormatted("IsDebugModeEnabled", true, false)}")
             .AppendLine("\nUI:")
             .AppendLine($"  MainWindow not null: {GetFormatted(() => MainWindow.Current != null)}")
@@ -287,12 +287,12 @@ internal class CrashReport : IDisposable
         var originalPathsEntry = ZipFile.Entries.First(entry => entry.FullName == "DocumentInfo.json");
 
         // Load original paths
-        Dictionary<string, AutosaveFilePathInfo> paths;
+        Dictionary<string, CrashReportAutosaveFilePathInfo> paths;
         {
             using Stream stream = originalPathsEntry.Open();
             using StreamReader reader = new(stream);
             string json = reader.ReadToEnd();
-            paths = JsonConvert.DeserializeObject<Dictionary<string, AutosaveFilePathInfo>>(json);
+            paths = JsonConvert.DeserializeObject<Dictionary<string, CrashReportAutosaveFilePathInfo>>(json);
         }
 
         // Load .pixi files
@@ -358,7 +358,7 @@ internal class CrashReport : IDisposable
 
         // Write the documents into zip
         int counter = 0;
-        Dictionary<string, AutosaveFilePathInfo> originalPaths = new();
+        Dictionary<string, CrashReportAutosaveFilePathInfo> originalPaths = new();
         foreach (DocumentViewModel document in vm.DocumentManagerSubViewModel.Documents)
         {
             try
@@ -377,22 +377,28 @@ internal class CrashReport : IDisposable
                 documentStream.Write(serialized);
 
                 originalPaths.Add(nameInZip,
-                    new AutosaveFilePathInfo(document.FullFilePath, document.AutosaveViewModel.LastSavedPath));
+                    new CrashReportAutosaveFilePathInfo(document.FullFilePath, document.AutosaveViewModel.LastAutosavedPath));
+            }
+            catch
+            {
+                // ignored
             }
-            catch { }
 
             try
             {
-                if (document.AutosaveViewModel.LastSavedPath != null)
+                if (document.AutosaveViewModel.LastAutosavedPath != null)
                 {
-                    using var file = File.OpenRead(document.AutosaveViewModel.LastSavedPath);
+                    using var file = File.OpenRead(document.AutosaveViewModel.LastAutosavedPath);
                     using var entry = archive
-                        .CreateEntry($"Autosave/{Path.GetFileName(document.AutosaveViewModel.LastSavedPath)}").Open();
+                        .CreateEntry($"Autosave/{Path.GetFileName(document.AutosaveViewModel.LastAutosavedPath)}").Open();
 
                     file.CopyTo(entry);
                 }
             }
-            catch { }
+            catch
+            {
+                // ignored
+            }
 
             counter++;
         }
@@ -420,7 +426,7 @@ internal class CrashReport : IDisposable
 
     public class RecoveredPixi
     {
-        public AutosaveFilePathInfo Path { get; }
+        public CrashReportAutosaveFilePathInfo Path { get; }
 
         public ZipArchiveEntry RecoveredEntry { get; }
 
@@ -446,7 +452,7 @@ internal class CrashReport : IDisposable
             return buffer;
         }
 
-        public RecoveredPixi(AutosaveFilePathInfo path, ZipArchiveEntry recoveredEntry, ZipArchiveEntry? autosaveEntry)
+        public RecoveredPixi(CrashReportAutosaveFilePathInfo path, ZipArchiveEntry recoveredEntry, ZipArchiveEntry? autosaveEntry)
         {
             Path = path;
             RecoveredEntry = recoveredEntry;

+ 11 - 0
src/PixiEditor/Models/DataHolders/CrashReportAutosaveFilePathInfo.cs

@@ -0,0 +1,11 @@
+using System.IO;
+
+namespace PixiEditor.Models.DataHolders;
+
+public class CrashReportAutosaveFilePathInfo(string? originalPath, string? autosavePath)
+{
+    public string? OriginalPath { get; set; } = originalPath;
+
+    public string? AutosavePath { get; set; } = autosavePath;
+    
+}

+ 11 - 0
src/PixiEditor/Models/DocumentModels/Autosave/AutosaveHistoryEntry.cs

@@ -0,0 +1,11 @@
+using PixiEditor.Models.DocumentModels.Autosave.Enums;
+
+namespace PixiEditor.Models.DocumentModels.Autosave;
+
+internal class AutosaveHistoryEntry(DateTime dateTime, AutosaveHistoryType type, AutosaveHistoryResult result, Guid tempFileGuid)
+{
+    public DateTime DateTime { get; set; } = dateTime;
+    public AutosaveHistoryType Type { get; set; } = type;
+    public AutosaveHistoryResult Result { get; set; } = result;
+    public Guid TempFileGuid { get; set; } = tempFileGuid;
+}

+ 8 - 0
src/PixiEditor/Models/DocumentModels/Autosave/AutosaveHistorySession.cs

@@ -0,0 +1,8 @@
+namespace PixiEditor.Models.DocumentModels.Autosave;
+
+internal class AutosaveHistorySession(Guid sessionGuid, DateTime launchDateTime)
+{
+    public List<AutosaveHistoryEntry> AutosaveEntries { get; set; } = new();
+    public Guid SessionGuid { get; set; } = sessionGuid;
+    public DateTime LaunchDateTime { get; set; } = launchDateTime;
+}

+ 31 - 0
src/PixiEditor/Models/DocumentModels/Autosave/AutosaverAwaitUpdateableChangeEndJob.cs

@@ -0,0 +1,31 @@
+using PixiEditor.Models.DocumentModels.Autosave.Enums;
+using PixiEditor.Views.UserControls;
+
+namespace PixiEditor.Models.DocumentModels.Autosave;
+
+#nullable enable
+internal class AutosaverAwaitUpdateableChangeEndJob : IAutosaverJob
+{
+    public event EventHandler? OnCompleted;
+    private bool isStopped = true;
+
+    public AutosaveState CorrespondingState => AutosaveState.AwaitingUpdateableChangeEnd;
+
+    public void OnUpdateableChangeEnded()
+    {
+        if (isStopped)
+            return;
+        OnCompleted?.Invoke(this, EventArgs.Empty);
+        isStopped = true;
+    }
+
+    public void Start()
+    {
+        isStopped = false;
+    }
+
+    public void ForceStop()
+    {
+        isStopped = true;
+    }
+}

+ 78 - 0
src/PixiEditor/Models/DocumentModels/Autosave/AutosaverSaveBackupJob.cs

@@ -0,0 +1,78 @@
+using System.IO;
+using System.Windows;
+using System.Windows.Threading;
+using PixiEditor.Models.DocumentModels.Autosave.Enums;
+using PixiEditor.Models.IO;
+using PixiEditor.ViewModels.SubViewModels.Document;
+using PixiEditor.Views.UserControls;
+
+namespace PixiEditor.Models.DocumentModels.Autosave;
+
+#nullable enable
+internal class AutosaverSaveBackupJob(DocumentViewModel documentToSave, int backupAttempt = 1) : IAutosaverJob
+{
+    public event EventHandler? OnCompleted;
+    public event EventHandler<BackupAutosaveResult>? OnNonCompleted;
+    public Exception? Exception { get; private set; }
+    
+    public AutosaveState CorrespondingState => AutosaveState.InProgress;
+    
+    private DispatcherTimer? waitingTimer; 
+    
+    public void Start()
+    {
+        string filePath = documentToSave.AutosaveViewModel.AutosavePath;
+        BackupAutosaveResult result = Autosave(filePath);
+        if (result == BackupAutosaveResult.Success)
+        {
+            documentToSave.AutosaveViewModel.LastAutosavedPath = filePath; 
+            documentToSave.MarkAsAutosaved();
+        }
+        
+        if (result == BackupAutosaveResult.Success)
+            OnCompleted?.Invoke(this, EventArgs.Empty);
+        else
+            OnNonCompleted?.Invoke(this, result);
+    }
+
+    public void OnUpdateableChangeEnded() { }
+    
+    public void ForceStop()
+    {
+        waitingTimer!.Stop();
+    }
+    
+    private BackupAutosaveResult Autosave(string filePath)
+    {
+        if (documentToSave.AllChangesSaved)
+        {
+            documentToSave.AutosaveViewModel.AddAutosaveHistoryEntry(AutosaveHistoryType.Periodic, AutosaveHistoryResult.NothingToSave);
+            return BackupAutosaveResult.NothingToSave;
+        }
+
+        if (documentToSave.UpdateableChangeActive)
+            return BackupAutosaveResult.BlockedByUpdateableChange;
+
+        try
+        {
+            Directory.CreateDirectory(Directory.GetParent(filePath)!.FullName);
+        
+            var result = Exporter.TrySave(documentToSave, filePath);
+        
+            if (result == SaveResult.Success)
+            {
+                documentToSave.MarkAsAutosaved();
+                documentToSave.AutosaveViewModel.AddAutosaveHistoryEntry(AutosaveHistoryType.Periodic, AutosaveHistoryResult.SavedBackup);
+                return BackupAutosaveResult.Success;
+            }
+            
+            Exception = new Exception($"Failed to autosave for the {backupAttempt}. time due to {result}");
+            return BackupAutosaveResult.Error;
+        }
+        catch (Exception e)
+        {
+            Exception = e;
+            return BackupAutosaveResult.Error;
+        }
+    }
+}

+ 72 - 0
src/PixiEditor/Models/DocumentModels/Autosave/AutosaverSaveUserFileJob.cs

@@ -0,0 +1,72 @@
+using System.IO;
+using System.Windows.Threading;
+using PixiEditor.Models.DocumentModels.Autosave.Enums;
+using PixiEditor.ViewModels.SubViewModels.Document;
+namespace PixiEditor.Models.DocumentModels.Autosave;
+
+#nullable enable
+internal class AutosaverSaveUserFileJob(DocumentViewModel document) : IAutosaverJob
+{
+    public event EventHandler? OnCompleted;
+    public event EventHandler<UserFileAutosaveResult>? OnNonCompleted;
+    public Exception? Exception { get; private set; }
+    
+    public AutosaveState CorrespondingState => AutosaveState.InProgress;
+    
+    private DispatcherTimer? waitingTimer;
+
+    public async void Start()
+    {
+        UserFileAutosaveResult result;
+        try
+        {
+            result = await Task.Run(Copy);
+        }
+        catch (Exception e)
+        {
+            result = UserFileAutosaveResult.ExceptionWhileSaving;
+            Exception = e;
+        }
+
+        if (result == UserFileAutosaveResult.Success)
+        {
+            document.MarkAsSaved();
+        }
+        
+        if (result == UserFileAutosaveResult.Success)
+            OnCompleted?.Invoke(this, EventArgs.Empty);
+        else
+            OnNonCompleted?.Invoke(this, result);
+
+        UserFileAutosaveResult Copy()
+        {
+            try
+            {
+                string path = document.FullFilePath;
+                if (!File.Exists(path))
+                    return UserFileAutosaveResult.NoUserFile;
+                
+                File.Copy(document.AutosaveViewModel.LastAutosavedPath, path, true);
+                
+                document.MarkAsSaved();
+                document.AutosaveViewModel.AddAutosaveHistoryEntry(AutosaveHistoryType.Periodic, AutosaveHistoryResult.SavedUserFile);
+                return UserFileAutosaveResult.Success;
+            }
+            catch (Exception e) when (e is UnauthorizedAccessException or DirectoryNotFoundException)
+            {
+                return UserFileAutosaveResult.NoUserFile;
+            }
+            catch (Exception e)
+            {
+                Exception = e;
+                return UserFileAutosaveResult.ExceptionWhileSaving;
+            }
+        }
+    }
+
+    public void OnUpdateableChangeEnded() { }
+    public void ForceStop()
+    {
+        waitingTimer!.Stop();
+    }
+}

+ 35 - 0
src/PixiEditor/Models/DocumentModels/Autosave/AutosaverWaitJob.cs

@@ -0,0 +1,35 @@
+using System.Windows;
+using System.Windows.Threading;
+using PixiEditor.Models.DocumentModels.Autosave.Enums;
+using PixiEditor.Views.UserControls;
+
+namespace PixiEditor.Models.DocumentModels.Autosave;
+
+#nullable enable
+internal class AutosaverWaitJob(TimeSpan duration)
+    : IAutosaverJob
+{
+    public event EventHandler? OnCompleted;
+    private DispatcherTimer? waitingTimer;
+
+    public AutosaveState CorrespondingState => AutosaveState.Idle;
+
+    public void Start()
+    {
+        waitingTimer = new(duration, DispatcherPriority.Normal, WaitEndCallback, Application.Current.Dispatcher);
+        waitingTimer.Start();
+    }
+
+    private void WaitEndCallback(object sender, EventArgs e)
+    {
+        waitingTimer!.Stop();
+        OnCompleted?.Invoke(this, EventArgs.Empty);
+    }
+
+    public void OnUpdateableChangeEnded() { }
+
+    public void ForceStop()
+    {
+        waitingTimer!.Stop();
+    }
+}

+ 211 - 0
src/PixiEditor/Models/DocumentModels/Autosave/DocumentAutosaver.cs

@@ -0,0 +1,211 @@
+using System.IO;
+using System.Windows;
+using PixiEditor.Helpers;
+using PixiEditor.Models.DocumentModels.Autosave.Enums;
+using PixiEditor.Models.DocumentModels.Autosave.Structs;
+using PixiEditor.Models.IO;
+using PixiEditor.ViewModels.SubViewModels.Document;
+using PixiEditor.Views.UserControls;
+
+namespace PixiEditor.Models.DocumentModels.Autosave;
+
+#nullable enable
+internal class DocumentAutosaver : IDisposable
+{
+    public event EventHandler? JobChanged;
+
+    public AutosaveStateData State
+    {
+        get
+        {
+            return new AutosaveStateData
+            {
+                AutosaveInterval = autosavePeriod,
+                AutosaveLaunchDateTime = autosaveStartedAt,
+                AutosaveState = currentJob?.CorrespondingState ?? AutosaveState.Idle,
+                LastBackupAutosaveData = lastBackupAutosaveResult is null ? null : new LastBackupAutosaveData
+                {
+                    Time = lastBackupAutosaveDateTime.Value,
+                    SaveResult = lastBackupAutosaveResult.Value
+                },
+                LastUserFileAutosaveData = lastUserFileAutosaveResult is null ? null : new LastUserFileAutosaveData
+                {
+                    Time = lastUserFileAutosaveDateTime.Value,
+                    SaveResult = lastUserFileAutosaveResult.Value
+                }
+            };
+        }
+    }
+    
+    private readonly DateTime autosaveStartedAt;
+
+    private int backupSaveFailureCount = 0;
+    private int userFileSaveFailureCount = 0;
+    
+    private IAutosaverJob? currentJob;
+
+    private IAutosaverJob? CurrentJob
+    {
+        get => currentJob;
+        set
+        {
+            currentJob = value;
+            JobChanged?.Invoke(this, EventArgs.Empty);
+        }
+    }
+    
+    private readonly bool saveUserFile;
+    private readonly DocumentViewModel document;
+    private readonly TimeSpan autosavePeriod;
+    
+    private bool isDisposed = false;
+
+    private UserFileAutosaveResult? lastUserFileAutosaveResult;
+    private DateTime? lastUserFileAutosaveDateTime;
+    private BackupAutosaveResult? lastBackupAutosaveResult;
+    private DateTime? lastBackupAutosaveDateTime;
+
+    public DocumentAutosaver(DocumentViewModel document, TimeSpan autosavePeriod, bool saveUserFile)
+    {
+        this.document = document;
+        this.autosavePeriod = autosavePeriod;
+        this.saveUserFile = saveUserFile;
+        autosaveStartedAt = DateTime.Now;
+
+        AutosaverWaitJob initialWaitJob = new(autosavePeriod);
+        initialWaitJob.OnCompleted += OnWaitingCompleted;
+        StartJob(initialWaitJob);
+    }
+
+    public void OnUpdateableChangeEnded() => CurrentJob?.OnUpdateableChangeEnded();
+
+    private void OnWaitingCompleted(object? sender, EventArgs args)
+    {
+        if (isDisposed)
+            return;
+        InitiateSaving();
+    }
+
+    private void StartJob(IAutosaverJob job)
+    {
+        CurrentJob = job;
+        job.Start();
+    }
+    
+    private void WaitForNextSave()
+    {
+        AutosaverWaitJob waitJob = new(autosavePeriod);
+        waitJob.OnCompleted += OnWaitingCompleted;
+        StartJob(waitJob);
+    }
+
+    private void InitiateSaving()
+    {
+        AutosaverSaveBackupJob saveBackupJob = new(document, backupSaveFailureCount + 1);
+        saveBackupJob.OnCompleted += OnBackupSavingCompleted;
+        saveBackupJob.OnNonCompleted += OnBackupSavingNonCompleted;
+        StartJob(saveBackupJob);
+    }
+    
+    private void OnBackupSavingCompleted(object? sender, EventArgs args)
+    {
+        if (isDisposed)
+            return;
+
+        lastBackupAutosaveResult = BackupAutosaveResult.Success;
+        lastBackupAutosaveDateTime = DateTime.Now;
+        
+        backupSaveFailureCount = 0;
+        if (saveUserFile)
+        {
+            AutosaverSaveUserFileJob saveUserFileJob = new(document);
+            saveUserFileJob.OnCompleted += OnUserFileSavingCompleted;
+            saveUserFileJob.OnNonCompleted += OnUserFileSavingNonCompleted;
+            StartJob(saveUserFileJob);
+        }
+        else
+        {
+            WaitForNextSave();
+        }
+    }
+    
+    private void OnUserFileSavingCompleted(object? sender, EventArgs args)
+    {
+        if (isDisposed)
+            return;
+
+        lastUserFileAutosaveResult = UserFileAutosaveResult.Success;
+        lastUserFileAutosaveDateTime = DateTime.Now;
+        
+        userFileSaveFailureCount = 0;
+        WaitForNextSave();
+    }
+
+    private void OnBackupSavingNonCompleted(object? sender, BackupAutosaveResult result)
+    {
+        if (isDisposed)
+            return;
+
+        lastBackupAutosaveResult = result;
+        lastBackupAutosaveDateTime = DateTime.Now;
+        
+        switch (result)
+        {
+            case BackupAutosaveResult.Error:
+                backupSaveFailureCount++;
+                if (backupSaveFailureCount < 3)
+                    CrashHelper.SendExceptionInfoToWebhook(((AutosaverSaveBackupJob)sender).Exception);
+                WaitForNextSave();
+                break;
+            
+            case BackupAutosaveResult.NothingToSave:
+                WaitForNextSave();
+                break;
+            
+            case BackupAutosaveResult.BlockedByUpdateableChange:
+                AutosaverAwaitUpdateableChangeEndJob waitJob = new();
+                waitJob.OnCompleted += OnAwaitUpdateableChangeCompleted;
+                StartJob(waitJob);
+                break;
+            
+            default:
+                throw new ArgumentOutOfRangeException(nameof(result), result, null);
+        }
+    }
+
+    private void OnAwaitUpdateableChangeCompleted(object? sender, EventArgs args)
+    {
+        if (isDisposed)
+            return;
+        
+        InitiateSaving();
+    }
+
+    private void OnUserFileSavingNonCompleted(object? sender, UserFileAutosaveResult result)
+    {
+        if (isDisposed)
+            return;
+
+        lastUserFileAutosaveResult = result;
+        lastUserFileAutosaveDateTime = DateTime.Now;
+        
+        switch (result)
+        {
+            case UserFileAutosaveResult.NoUserFile:
+            case UserFileAutosaveResult.ExceptionWhileSaving:
+                userFileSaveFailureCount++;
+                if (userFileSaveFailureCount < 3)
+                    CrashHelper.SendExceptionInfoToWebhook(((AutosaverSaveUserFileJob)sender).Exception);
+                WaitForNextSave();
+                break;
+            default:
+                throw new ArgumentOutOfRangeException(nameof(result), result, null);
+        }
+    }
+    
+    public void Dispose()
+    {
+        CurrentJob?.ForceStop();
+        isDisposed = true;
+    }
+}

+ 8 - 0
src/PixiEditor/Models/DocumentModels/Autosave/Enums/AutosaveHistoryResult.cs

@@ -0,0 +1,8 @@
+namespace PixiEditor.Models.DocumentModels.Autosave.Enums;
+
+internal enum AutosaveHistoryResult
+{
+    SavedUserFile,
+    SavedBackup,
+    NothingToSave
+}

+ 7 - 0
src/PixiEditor/Models/DocumentModels/Autosave/Enums/AutosaveHistoryType.cs

@@ -0,0 +1,7 @@
+namespace PixiEditor.Models.DocumentModels.Autosave.Enums;
+
+internal enum AutosaveHistoryType
+{
+    Periodic,
+    OnClose
+}

+ 9 - 0
src/PixiEditor/Models/DocumentModels/Autosave/Enums/AutosaveState.cs

@@ -0,0 +1,9 @@
+namespace PixiEditor.Models.DocumentModels.Autosave.Enums;
+
+public enum AutosaveState
+{
+    Paused,
+    Idle,
+    AwaitingUpdateableChangeEnd,
+    InProgress
+}

+ 10 - 0
src/PixiEditor/Models/DocumentModels/Autosave/Enums/BackupAutosaveResult.cs

@@ -0,0 +1,10 @@
+#nullable enable
+namespace PixiEditor.Models.DocumentModels.Autosave.Enums;
+
+public enum BackupAutosaveResult
+{
+    Success,
+    Error,
+    NothingToSave,
+    BlockedByUpdateableChange
+}

+ 9 - 0
src/PixiEditor/Models/DocumentModels/Autosave/Enums/UserFileAutosaveResult.cs

@@ -0,0 +1,9 @@
+#nullable enable
+namespace PixiEditor.Models.DocumentModels.Autosave.Enums;
+
+public enum UserFileAutosaveResult
+{
+    Success,
+    NoUserFile,
+    ExceptionWhileSaving
+}

+ 14 - 0
src/PixiEditor/Models/DocumentModels/Autosave/IAutosaverJob.cs

@@ -0,0 +1,14 @@
+using PixiEditor.Models.DocumentModels.Autosave.Enums;
+using PixiEditor.Views.UserControls;
+
+namespace PixiEditor.Models.DocumentModels.Autosave;
+
+#nullable enable
+internal interface IAutosaverJob
+{
+    event EventHandler? OnCompleted;
+    AutosaveState CorrespondingState { get; }
+    void OnUpdateableChangeEnded();
+    void Start();
+    void ForceStop();
+}

+ 12 - 0
src/PixiEditor/Models/DocumentModels/Autosave/Structs/AutosaveStateData.cs

@@ -0,0 +1,12 @@
+using PixiEditor.Models.DocumentModels.Autosave.Enums;
+
+namespace PixiEditor.Models.DocumentModels.Autosave.Structs;
+
+public struct AutosaveStateData
+{
+    public LastBackupAutosaveData? LastBackupAutosaveData { get; set; }
+    public LastUserFileAutosaveData? LastUserFileAutosaveData { get; set; }
+    public AutosaveState AutosaveState { get; set; }
+    public DateTime AutosaveLaunchDateTime { get; set; }
+    public TimeSpan AutosaveInterval { get; set; }
+}

+ 9 - 0
src/PixiEditor/Models/DocumentModels/Autosave/Structs/LastBackupAutosaveData.cs

@@ -0,0 +1,9 @@
+using PixiEditor.Models.DocumentModels.Autosave.Enums;
+
+namespace PixiEditor.Models.DocumentModels.Autosave.Structs;
+
+public struct LastBackupAutosaveData
+{
+    public DateTime Time { get; set; }
+    public BackupAutosaveResult SaveResult { get; set; }
+}

+ 10 - 0
src/PixiEditor/Models/DocumentModels/Autosave/Structs/LastUserFileAutosaveData.cs

@@ -0,0 +1,10 @@
+using PixiEditor.Models.DocumentModels.Autosave.Enums;
+
+namespace PixiEditor.Models.DocumentModels.Autosave.Structs;
+
+public struct LastUserFileAutosaveData
+{
+    public DateTime Time { get; set; }
+    
+    public UserFileAutosaveResult SaveResult { get; set; }
+}

+ 79 - 219
src/PixiEditor/ViewModels/SubViewModels/Document/AutosaveDocumentViewModel.cs

@@ -1,291 +1,151 @@
 using System.IO;
-using System.Windows;
-using System.Windows.Threading;
 using PixiEditor.Extensions.Common.UserPreferences;
 using PixiEditor.Helpers;
 using PixiEditor.Models.DocumentModels;
+using PixiEditor.Models.DocumentModels.Autosave;
+using PixiEditor.Models.DocumentModels.Autosave.Enums;
+using PixiEditor.Models.DocumentModels.Autosave.Structs;
 using PixiEditor.Models.IO;
-using PixiEditor.Views.UserControls;
 
 namespace PixiEditor.ViewModels.SubViewModels.Document;
 
 
 internal class AutosaveDocumentViewModel : NotifyableObject
 {
-    private readonly DispatcherTimer savingTimer;
-    private readonly DispatcherTimer busyTimer;
-    private int savingFailed;
-    private Guid tempGuid;
-    private bool autosaveEnabled = true;
-    private bool waitingForUpdateableChangeEnd = false;
-    private LastAutosaveData? lastAutosaveData = null;
-    private DateTime? autosaveLaunchDateTime = null;
-    
-    private DocumentViewModel Document { get; }
-    
-    private double AutosavePeriodMinutes { get; set; } = -1;
-    
-    private AutosaveStateData autosaveStateData;
-    public AutosaveStateData AutosaveStateData
+    private AutosaveStateData? autosaveStateData;
+    public AutosaveStateData? AutosaveStateData
     {
         get => autosaveStateData;
         set => SetProperty(ref autosaveStateData, value);
     }
 
-    public bool Enabled
+    private bool currentDocumentAutosaveEnabled = true;
+    public bool CurrentDocumentAutosaveEnabled
     {
-        get => autosaveEnabled;
+        get => currentDocumentAutosaveEnabled;
         set
         {
-            if (autosaveEnabled == value)
+            if (currentDocumentAutosaveEnabled == value)
                 return;
             
-            AutosavePeriodChanged(
-                IPreferences.Current!.GetPreference(
-                    PreferencesConstants.AutosavePeriodMinutes, 
-                    PreferencesConstants.AutosavePeriodDefault),
-                value);
-            SetProperty(ref autosaveEnabled, value);
+            SetProperty(ref currentDocumentAutosaveEnabled, value);
+            StopOrStartAutosaverIfNecessary();
         }
     }
 
-    public string LastSavedPath { get; private set; }
+    private DocumentAutosaver? autosaver;
+    private DocumentViewModel Document { get; }
+    private Guid autosaveFileGuid = Guid.NewGuid();
+    public string AutosavePath => AutosaveHelper.GetAutosavePath(autosaveFileGuid);
     
-    public static bool SaveStateEnabled => IPreferences.Current!.GetPreference(PreferencesConstants.SaveSessionStateEnabled, PreferencesConstants.SaveSessionStateDefault);
+    public string LastAutosavedPath { get; set; }
     
-    private bool SaveToDocumentPath => IPreferences.Current!.GetPreference(PreferencesConstants.AutosaveToDocumentPath, PreferencesConstants.AutosaveToDocumentPathDefault);
+    private static bool SaveUserFileEnabled => IPreferences.Current!.GetPreference(PreferencesConstants.AutosaveToDocumentPath, PreferencesConstants.AutosaveToDocumentPathDefault);
+    private static double AutosavePeriod => IPreferences.Current!.GetPreference(PreferencesConstants.AutosavePeriodMinutes, PreferencesConstants.AutosavePeriodDefault); 
+    private static bool AutosaveEnabledGlobally => IPreferences.Current!.GetPreference(PreferencesConstants.AutosaveEnabled, PreferencesConstants.AutosaveEnabledDefault); 
     
     public AutosaveDocumentViewModel(DocumentViewModel document, DocumentInternalParts internals)
     {
         Document = document;
-        tempGuid = Guid.NewGuid();
-
-        var dispatcher = Application.Current.Dispatcher;
-        
-        savingTimer = new DispatcherTimer(DispatcherPriority.Normal);
-        savingTimer.Tick += (_, _) =>
-        {
-            savingTimer.Stop(); 
-            TryAutosave();
-        };
-
-        busyTimer = new DispatcherTimer(DispatcherPriority.Normal, dispatcher) { Interval = TimeSpan.FromMilliseconds(80) };
-        busyTimer.Tick += (_, _) =>
-        {
-            busyTimer!.Stop();
-            Document.Busy = true;
-        };
-
-        internals.ChangeController.UpdateableChangeEnded += OnUpdateableChangeEnded;
-
-        var preferences = IPreferences.Current;
-        
-        preferences!.AddCallback<double>(PreferencesConstants.AutosavePeriodMinutes, (v) => AutosavePeriodChanged(v, autosaveEnabled));
-        AutosavePeriodChanged(preferences.GetPreference(PreferencesConstants.AutosavePeriodMinutes, PreferencesConstants.AutosavePeriodDefault), autosaveEnabled);
+        internals.ChangeController.UpdateableChangeEnded += ((_, _) => autosaver?.OnUpdateableChangeEnded());
+        IPreferences.Current!.AddCallback(PreferencesConstants.AutosaveEnabled, PreferenceUpdateCallback);
+        IPreferences.Current!.AddCallback(PreferencesConstants.AutosavePeriodMinutes, PreferenceUpdateCallback);
+        IPreferences.Current!.AddCallback(PreferencesConstants.AutosaveToDocumentPath, PreferenceUpdateCallback);
+        StopOrStartAutosaverIfNecessary();
     }
 
-    private AutosaveStateData CreateAutosaveStateData()
+    private void PreferenceUpdateCallback(object _)
     {
-        return new AutosaveStateData
-        {
-            LastAutosaveData = lastAutosaveData,
-            AutosaveLaunchDateTime = autosaveLaunchDateTime ?? DateTime.Now,
-            AutosaveInterval = TimeSpan.FromMinutes(AutosavePeriodMinutes),
-            AutosaveState = AutosaveState.Paused
-        };
+        StopOrStartAutosaverIfNecessary();
     }
 
-    public static void AutosaveOnClose()
+    private void StopAutosaver()
     {
-        
+        autosaver?.Dispose();
+        autosaver = null;
+        AutosaveStateData = null;
     }
 
-    public void TryAutosave(bool saveUserFileIfEnabled = true)
+    private void StopOrStartAutosaverIfNecessary()
     {
-        if (Document.AllChangesSaved)
-        {
-            RestartTimers();
-            
-            lastAutosaveData = new LastAutosaveData()
-            {
-                Time = DateTime.Now,
-                BackupSaveResult = BackupAutosaveResult.NothingToSave,
-                UserFileSaveResult = UserFileAutosaveResult.NothingToSave
-            };
-            AutosaveStateData = CreateAutosaveStateData() with { AutosaveState = AutosaveState.Idle };
-            return;
-        }
-
-        if (Document.UpdateableChangeActive)
-        {
-            waitingForUpdateableChangeEnd = true;
+        StopAutosaver();
+        if (!AutosaveEnabledGlobally || !CurrentDocumentAutosaveEnabled)
             return;
-        }
-
-        SafeAutosave(saveUserFileIfEnabled);
+        
+        autosaver = new DocumentAutosaver(Document, TimeSpan.FromMinutes(AutosavePeriod), SaveUserFileEnabled);
+        autosaver.JobChanged += (_, _) => AutosaveStateData = autosaver.State;
+        AutosaveStateData = autosaver.State;
     }
 
-    public void PanicAutosaveFromDeadlockDetector()
+    public bool AutosaveOnClose()
     {
-        string filePath = Path.Join(Paths.PathToUnsavedFilesFolder, $"autosave-{tempGuid}.pixi");
-        Directory.CreateDirectory(Directory.GetParent(filePath)!.FullName);
-
-        var result = Exporter.TrySave(Document, filePath);
-
-        if (result == SaveResult.Success)
-        {
-            LastSavedPath = filePath;
-        }
-    }
+        if (Document.AllChangesSaved)
+            return true;
 
-    private async void SafeAutosave(bool saveUserFile)
-    {
         try
         {
-            await Autosave(saveUserFile);
+            string filePath = AutosavePath;
+            Directory.CreateDirectory(Directory.GetParent(filePath)!.FullName);
+            bool success = Exporter.TrySave(Document, filePath) == SaveResult.Success;
+            if (success)
+                AddAutosaveHistoryEntry(AutosaveHistoryType.OnClose);
+            
+            return success;
         }
         catch (Exception e)
         {
-            savingFailed++;
-            
-            lastAutosaveData = new LastAutosaveData()
-            {
-                Time = DateTime.Now,
-                BackupSaveResult = BackupAutosaveResult.Error,
-                UserFileSaveResult = UserFileAutosaveResult.ExceptionWhileSaving
-            };
-            AutosaveStateData = CreateAutosaveStateData() with { AutosaveState = AutosaveState.Idle };
-            
-            busyTimer.Stop();
-            Document.Busy = false;
-
-            RestartTimers();
-
-            if (savingFailed == 1)
-            {
-                CrashHelper.SendExceptionInfoToWebhook(e);
-            }
+            return false;
         }
     }
-    
-    private async Task Autosave(bool saveUserFile)
-    {
-        AutosaveStateData = CreateAutosaveStateData() with { AutosaveState = AutosaveState.InProgress };
 
-        string filePath = Path.Join(Paths.PathToUnsavedFilesFolder, $"autosave-{tempGuid}.pixi");
-        Directory.CreateDirectory(Directory.GetParent(filePath)!.FullName);
-        
-        busyTimer.Start();
-        var result = Exporter.TrySave(Document, filePath);
+    public void AddAutosaveHistoryEntry(AutosaveHistoryType type, AutosaveHistoryResult result)
+    {
+        List<AutosaveHistorySession>? historySessions = IPreferences.Current!.GetLocalPreference<List<AutosaveHistorySession>>(PreferencesConstants.AutosaveHistory);
+        if (historySessions is null)
+            historySessions = new();
 
-        UserFileAutosaveResult userFileSaveResult = UserFileAutosaveResult.Disabled;
-        
-        if (result == SaveResult.Success)
+        AutosaveHistorySession currentSession;
+        if (historySessions.Count == 0 || historySessions[^1].SessionGuid != ViewModelMain.Current.CurrentSessionId)
         {
-            if (saveUserFile && SaveToDocumentPath && Document.FullFilePath != null)
-            {
-                userFileSaveResult = await CopyTempToUserFile(filePath);
-            }
-            
-            lastAutosaveData = new LastAutosaveData
-            {
-                Time = DateTime.Now,
-                BackupSaveResult = BackupAutosaveResult.Success,
-                UserFileSaveResult = userFileSaveResult
-            };
-            AutosaveStateData = CreateAutosaveStateData() with { AutosaveState = AutosaveState.Idle, LastAutosaveData = lastAutosaveData };
-            
-            Document.MarkAsAutosaved();
-            LastSavedPath = filePath;
+            currentSession = new AutosaveHistorySession(ViewModelMain.Current.CurrentSessionId, ViewModelMain.Current.LaunchDateTime);
+            historySessions.Add(currentSession);
         }
         else
         {
-            busyTimer.Stop();
-            Document.Busy = false;
-            
-            lastAutosaveData = new LastAutosaveData()
-            {
-                Time = DateTime.Now,
-                BackupSaveResult = BackupAutosaveResult.Error,
-                UserFileSaveResult = userFileSaveResult
-            };    
-            AutosaveStateData = CreateAutosaveStateData() with { AutosaveState = AutosaveState.Idle, LastAutosaveData = lastAutosaveData };
-            
-            savingFailed++;
-
-            if (savingFailed < 3)
-            {
-                int savingFailedCopy = savingFailed;
-                Task.Run(() => CrashHelper.SendExceptionInfoToWebhook(new Exception($"Failed to autosave for the {savingFailedCopy}. time due to {result}")));
-            }
+            currentSession = historySessions[^1];
         }
-        
-        busyTimer.Stop();
-        Document.Busy = false;
 
-        RestartTimers();
-    }
-
-    private async Task<UserFileAutosaveResult> CopyTempToUserFile(string tempPath)
-    {
-        if (!File.Exists(Document.FullFilePath))
-            return UserFileAutosaveResult.NoUserFile;
+        AutosaveHistoryEntry entry = new(DateTime.Now, type, result, autosaveFileGuid);
+        currentSession.AutosaveEntries.Add(entry);
         
-        var result = await Task.Run(Copy);
-        Document.MarkAsSaved();
-        return result;
-        
-        UserFileAutosaveResult Copy()
-        {
-            try
-            {
-                File.Copy(tempPath, Document.FullFilePath!, true);
-                return UserFileAutosaveResult.Success;
-            }
-            catch (Exception e) when (e is UnauthorizedAccessException or DirectoryNotFoundException)
-            {
-                return UserFileAutosaveResult.NoUserFile;
-            }
-            catch
-            {
-                return UserFileAutosaveResult.ExceptionWhileSaving;
-            }
-        }
-    }
-
-    private void RestartTimers()
-    {
-        savingTimer.Start();
+        IPreferences.Current.UpdateLocalPreference(PreferencesConstants.AutosaveHistory, historySessions);
     }
 
-    private void AutosavePeriodChanged(double minutes, bool documentEnabled)
+    public void PanicAutosaveFromDeadlockDetector()
     {
-        if ((int)minutes == -1 || !documentEnabled)
-        {
-            AutosavePeriodMinutes = minutes;
-            AutosaveStateData = CreateAutosaveStateData();
-            return;
-        }
-        
-        var timerEnabled = savingTimer.IsEnabled || (int)AutosavePeriodMinutes == -1 || !Enabled;
+        /*
+        string filePath = Path.Join(Paths.PathToUnsavedFilesFolder, $"autosave-{tempGuid}.pixi");
+        Directory.CreateDirectory(Directory.GetParent(filePath)!.FullName);
 
-        savingTimer.IsEnabled = false;
+        var result = Exporter.TrySave(Document, filePath);
 
-        var timeSpan = TimeSpan.FromMinutes(minutes);
-        savingTimer.Interval = timeSpan;
-        AutosavePeriodMinutes = minutes;
-        
-        savingTimer.IsEnabled = timerEnabled;
+        if (result == SaveResult.Success)
+        {
+            LastSavedPath = filePath;
+        }*/
     }
-
-    private void OnUpdateableChangeEnded(object? sender, EventArgs args)
+    
+    public void SetTempFileGuidAndLastSavedPath(Guid guid, string lastSavedPath)
     {
-        
+        autosaveFileGuid = guid;
+        LastAutosavedPath = lastSavedPath;
     }
 
-    public void SetTempFileGuidAndLastSavedPath(Guid guid, string lastSavedPath)
+    public void OnDocumentClosed()
     {
-        tempGuid = guid;
-        LastSavedPath = lastSavedPath;
+        CurrentDocumentAutosaveEnabled = false;
+        IPreferences.Current!.RemoveCallback(PreferencesConstants.AutosaveEnabled, PreferenceUpdateCallback);
+        IPreferences.Current!.RemoveCallback(PreferencesConstants.AutosavePeriodMinutes, PreferenceUpdateCallback);
+        IPreferences.Current!.RemoveCallback(PreferencesConstants.AutosaveToDocumentPath, PreferenceUpdateCallback);
     }
 }

+ 43 - 12
src/PixiEditor/ViewModels/SubViewModels/Main/AutosaveViewModel.cs

@@ -1,28 +1,59 @@
-using PixiEditor.Extensions.Common.UserPreferences;
+using System.IO;
+using PixiEditor.Extensions.Common.UserPreferences;
+using PixiEditor.Helpers;
+using PixiEditor.Models;
 using PixiEditor.Models.Commands.Attributes.Commands;
+using PixiEditor.Models.DocumentModels.Autosave;
+using PixiEditor.Models.IO;
 using PixiEditor.ViewModels.SubViewModels.Document;
 
 namespace PixiEditor.ViewModels.SubViewModels.Main;
 
-internal class AutosaveViewModel : SubViewModel<ViewModelMain>
+#nullable enable
+internal class AutosaveViewModel(ViewModelMain owner, DocumentManagerViewModel documentManager) : SubViewModel<ViewModelMain>(owner)
 {
-    private DocumentManagerViewModel _documentManager;
-    
-    public AutosaveViewModel(ViewModelMain owner, DocumentManagerViewModel documentManager) : base(owner)
-    {
-        _documentManager = documentManager;
-    }
+    public static bool SaveSessionStateEnabled => IPreferences.Current!.GetPreference(PreferencesConstants.SaveSessionStateEnabled, PreferencesConstants.SaveSessionStateDefault);
     
     [Command.Basic("PixiEditor.Autosave.ToggleAutosave", "AUTOSAVE_TOGGLE", "AUTOSAVE_TOGGLE_DESCRIPTION", CanExecute = "PixiEditor.Autosave.HasDocumentAndAutosaveEnabled")]
     public void ToggleAutosave()
     {
-        var autosaveViewModel = _documentManager.ActiveDocument!.AutosaveViewModel;
+        var autosaveViewModel = documentManager.ActiveDocument!.AutosaveViewModel;
 
-        autosaveViewModel.Enabled = !autosaveViewModel.Enabled;
+        autosaveViewModel.CurrentDocumentAutosaveEnabled = !autosaveViewModel.CurrentDocumentAutosaveEnabled;
     }
 
     [Evaluator.CanExecute("PixiEditor.Autosave.HasDocumentAndAutosaveEnabled")]
     public bool HasDocumentAndAutosaveEnabled() => 
-        _documentManager.DocumentNotNull() &&
-        (int)IPreferences.Current.GetPreference<double>(PreferencesConstants.AutosavePeriodMinutes) != -1;
+        documentManager.DocumentNotNull() && IPreferences.Current!.GetPreference<bool>(PreferencesConstants.AutosaveEnabled);
+
+    public void CleanupAutosavedFilesAndHistory()
+    {
+        if (!Directory.Exists(Paths.PathToUnsavedFilesFolder))
+            return;
+
+        List<AutosaveHistorySession>? autosaveHistory = IPreferences.Current!.GetLocalPreference<List<AutosaveHistorySession>>(PreferencesConstants.AutosaveHistory);
+        if (autosaveHistory is null)
+            autosaveHistory = new();
+
+        if (autosaveHistory.Count > 3)
+            autosaveHistory = autosaveHistory[^3..];
+        
+        foreach (var path in Directory.EnumerateFiles(Paths.PathToUnsavedFilesFolder))
+        {
+            try
+            {
+                Guid fileGuid = AutosaveHelper.GetAutosaveGuid(path)!.Value;
+                bool lastWriteIsOld = (DateTime.Now - File.GetLastWriteTime(path)).TotalDays > Constants.MaxAutosaveFilesLifetimeDays; 
+                bool creationDateIsOld = (DateTime.Now - File.GetCreationTime(path)).TotalDays > Constants.MaxAutosaveFilesLifetimeDays;
+                bool presentInHistory = autosaveHistory.Any(sess => sess.AutosaveEntries.Any(entry => entry.TempFileGuid == fileGuid));
+                
+                if (!presentInHistory && lastWriteIsOld && creationDateIsOld)
+                    File.Delete(path);
+            }
+            catch (Exception e)
+            {
+                CrashHelper.SendExceptionInfoToWebhook(e);
+            }
+        }
+    }
 }

+ 81 - 6
src/PixiEditor/ViewModels/SubViewModels/Main/FileViewModel.cs

@@ -17,6 +17,8 @@ using PixiEditor.Models.Commands.Attributes.Commands;
 using PixiEditor.Models.Controllers;
 using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.Dialogs;
+using PixiEditor.Models.DocumentModels.Autosave;
+using PixiEditor.Models.DocumentModels.Autosave.Enums;
 using PixiEditor.Models.IO;
 using PixiEditor.Models.Localization;
 using PixiEditor.Parser;
@@ -87,7 +89,7 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
         }
 
         RecentlyOpened.Remove(path);
-        IPreferences.Current.UpdateLocalPreference(PreferencesConstants.RecentlyOpened, RecentlyOpened.Select(x => x.FilePath));
+        IPreferences.Current!.UpdateLocalPreference(PreferencesConstants.RecentlyOpened, RecentlyOpened.Select(x => x.FilePath));
     }
 
     private void OpenHelloTherePopup()
@@ -110,7 +112,7 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
 
                 if (lastCrash == null)
                 {
-                    ReopenTempFiles();
+                    MaybeReopenTempAutosavedFiles();
                 }
                 else
                 {
@@ -191,13 +193,86 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
         showMissingFilesDialog = documents.Count != i;
     }
 
-    private void ReopenTempFiles()
+    private void MaybeReopenTempAutosavedFiles()
     {
         var preferences = Owner.Preferences;
-        var files = preferences.GetLocalPreference<AutosaveFilePathInfo[]>(PreferencesConstants.UnsavedNextSessionFiles);
 
-        if (files == null)
+        // Todo sure, no session saving, but shouldn't we still load backups in case of unexpected shutdown?
+        // it probably should be handled elsewhere
+        if (!preferences.GetPreference<bool>(PreferencesConstants.SaveSessionStateEnabled))
+            return;
+        
+        var history = preferences.GetLocalPreference<List<AutosaveHistorySession>>(PreferencesConstants.AutosaveHistory);
+        
+        // There are no autosave attempts .. but what if the user has just launched pixieditor for the first time,
+        // and it unexpectedly closed before auto saving anything. They could've still had some files open, and they won't be reopened in this session
+        // I'll say this is by design
+        if (history is null || history.Count == 0)
             return;
+        var lastSession = history[^1];
+        if (lastSession.AutosaveEntries.Count == 0)
+            return; 
+
+        List<List<AutosaveHistoryEntry>> perDocumentHistories = ( 
+            from entry in lastSession.AutosaveEntries
+            group entry by entry.TempFileGuid into entryGroup
+            select entryGroup.OrderBy(a => a.DateTime).ToList()
+        ).ToList();
+        
+        /*bool shutdownWasUnexpected = lastSession.AutosaveEntries.All(a => a.Type != AutosaveHistoryType.OnClose);
+        if (shutdownWasUnexpected)
+        {
+            List<List<AutosaveHistoryEntry>> lastBackups = ( 
+                from entry in lastSession.AutosaveEntries
+                group entry by entry.TempFileGuid into entryGroup
+                select entryGroup.OrderBy(a => a.DateTime).ToList()
+                ).ToList();
+            // todo notify about files getting recovered after unexpected shutdown
+            // also separate this out into a function
+            return;
+        }*/
+
+        foreach (var documentHistory in perDocumentHistories)
+        {
+            AutosaveHistoryEntry lastEntry = documentHistory[^1]; 
+            try
+            {
+                if (lastEntry.Type != AutosaveHistoryType.OnClose)
+                {
+                    // unexpected shutdown happened, this file wasn't saved on close, but we supposedly have a backup
+                    
+                }
+                else
+                {
+                    switch (lastEntry.Result)
+                    {
+                        case AutosaveHistoryResult.SavedBackup:
+                            // load from autosave
+                            break;
+                        case AutosaveHistoryResult.SavedUserFile:
+                        case AutosaveHistoryResult.NothingToSave:
+                            // load from user file
+                            break;
+                        default:
+                            throw new ArgumentOutOfRangeException();
+                    }                    
+                }
+                
+                
+                
+                string path = AutosaveHelper.GetAutosavePath(entry.TempFileGuid);
+                if (!File.Exists(path))
+                {
+                    // something happened with the file? todo try to recover backup while notifying user
+                    continue;
+                }
+            }
+            catch (Exception e)
+            {
+                CrashHelper.SendExceptionInfoToWebhook(e);
+            }
+
+        }
         
         foreach (var file in files)
         {
@@ -210,7 +285,7 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
 
                     if (file.AutosavePath != null)
                     {
-                        document.AutosaveViewModel.SetTempFileGuidAndLastSavedPath(file.GetAutosaveGuid()!.Value, file.AutosavePath);
+                        document.AutosaveViewModel.SetTempFileGuidAndLastSavedPath(AutosaveHelper.GetAutosaveGuid(file.AutosavePath)!.Value, file.AutosavePath);
                     }
                 }
                 else

+ 9 - 0
src/PixiEditor/ViewModels/SubViewModels/UserPreferences/Settings/FileSettings.cs

@@ -63,6 +63,15 @@ internal class FileSettings : SettingsGroup
         set => RaiseAndUpdatePreference(ref saveSessionStateEnabled, value);
     }
     
+    private bool autosaveEnabled = GetPreference(PreferencesConstants.AutosaveEnabled, PreferencesConstants.AutosaveEnabledDefault);
+    
+    public bool AutosaveEnabled
+    {
+        get => autosaveEnabled;
+        set => RaiseAndUpdatePreference(ref autosaveEnabled, value);
+    }
+    
+    
     private double autosavePeriodMinutes = GetPreference(PreferencesConstants.AutosavePeriodMinutes, PreferencesConstants.AutosavePeriodDefault);
     
     public double AutosavePeriodMinutes

+ 8 - 12
src/PixiEditor/ViewModels/ViewModelMain.cs

@@ -12,6 +12,7 @@ using PixiEditor.Models.Commands.Attributes.Commands;
 using PixiEditor.Models.Controllers;
 using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.Dialogs;
+using PixiEditor.Models.DocumentModels.Autosave;
 using PixiEditor.Models.DocumentModels.Public;
 using PixiEditor.Models.Enums;
 using PixiEditor.Models.Events;
@@ -110,6 +111,9 @@ internal class ViewModelMain : ViewModelBase
 
     public ActionDisplayList ActionDisplays { get; }
 
+    public Guid CurrentSessionId { get; } = Guid.NewGuid();
+    public DateTime LaunchDateTime { get; } = DateTime.Now;
+
     public ViewModelMain(IServiceProvider serviceProvider)
     {
         Current = this;
@@ -251,22 +255,13 @@ internal class ViewModelMain : ViewModelBase
 
     public void AutosaveAllForNextSession()
     {
-        if (!AutosaveDocumentViewModel.SaveStateEnabled || DebugSubViewModel.ModifiedEditorData)
-        {
+        if (!AutosaveViewModel.SaveSessionStateEnabled || DebugSubViewModel.ModifiedEditorData)
             return;
-        }
         
-        var list = new List<AutosaveFilePathInfo>();
-        foreach (var document in DocumentManagerSubViewModel.Documents)
+        foreach (DocumentViewModel document in DocumentManagerSubViewModel.Documents)
         {
-            document.AutosaveViewModel.TryAutosave();
-            if (document.AutosaveViewModel.LastSavedPath != null || document.FullFilePath != null)
-            {
-                list.Add(new AutosaveFilePathInfo(document.FullFilePath, document.AutosaveViewModel.LastSavedPath));
-            }
+            document.AutosaveViewModel.AutosaveOnClose();
         }
-        
-        Preferences.UpdateLocalPreference(PreferencesConstants.UnsavedNextSessionFiles, list);
     }
 
     /// <summary>
@@ -319,6 +314,7 @@ internal class ViewModelMain : ViewModelBase
 
             // document.Dispose();
             WindowSubViewModel.CloseViewportsForDocument(document);
+            document.AutosaveViewModel.OnDocumentClosed();
 
             return true;
         }

+ 8 - 3
src/PixiEditor/Views/Dialogs/SettingsWindow.xaml

@@ -142,8 +142,13 @@
                 <StackPanel Margin="27 5" Orientation="Horizontal">
                     <Label Style="{StaticResource SettingsText}"
                            ui:Translator.Key="AUTOSAVE_SETTINGS_PERIOD"/>
-                    <ComboBox SelectedItem="{Binding SettingsSubViewModel.File.AutosavePeriodMinutes, Mode=TwoWay, Converter={converters:AutosaveSettingsPeriodToValueConverter}}"
-                              MinWidth="100" Margin="7, 0">
+                    <ComboBox MinWidth="100" Margin="7, 0">
+                        <ComboBox.SelectedItem>
+                            <MultiBinding Converter="{converters:AutosaveSettingsPeriodToValueConverter}">
+                                <Binding Path="SettingsSubViewModel.File.AutosaveEnabled"></Binding>
+                                <Binding Path="SettingsSubViewModel.File.AutosavePeriodMinutes"></Binding>
+                            </MultiBinding>
+                        </ComboBox.SelectedItem>
                         <ComboBox.ItemsSource>
                             <x:Array Type="localization:LocalizedString">
                                 <localization:LocalizedString Key="DISABLED" />
@@ -160,7 +165,7 @@
 
                 <CheckBox Margin="27 0 27 10"
                           VerticalAlignment="Center" ui:Translator.Key="AUTOSAVE_SETTINGS_SAVE_USER_FILE" d:Content="Show startup window"
-                          IsEnabled="{Binding SettingsSubViewModel.File.AutosavePeriodMinutes, Converter={converters:AutosaveSettingsPeriodToValueConverter ReturnBoolAutosavingEnabled=True}}"
+                          IsEnabled="{Binding SettingsSubViewModel.File.AutosaveEnabled}"
                           IsChecked="{Binding SettingsSubViewModel.File.AutosaveToDocumentPath}"/>
 
                 <Label Style="{StaticResource SettingsHeader}" d:Content="Default new file size" ui:Translator.Key="DEFAULT_NEW_SIZE"/>

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

@@ -495,8 +495,9 @@
                     </Grid>
                 </Border>
                 <usercontrols:AutosaveControl
+                    Visibility="{Binding DocumentManagerSubViewModel.ActiveDocument, Converter={converters:NotNullToVisibilityConverter}}"
                     AutosaveStateData="{Binding DocumentManagerSubViewModel.ActiveDocument.AutosaveViewModel.AutosaveStateData}"
-                    AutosaveEnabled="{Binding DocumentManagerSubViewModel.ActiveDocument.AutosaveViewModel.Enabled}"
+                    AutosaveEnabled="{Binding DocumentManagerSubViewModel.ActiveDocument.AutosaveViewModel.CurrentDocumentAutosaveEnabled}"
                     MinWidth="50"
                     Height="25"
                     Margin="5,0,0,0"

+ 8 - 8
src/PixiEditor/Views/UserControls/AutosaveControl.xaml

@@ -10,8 +10,8 @@
              xmlns:converters="clr-namespace:PixiEditor.Helpers.Converters"
              xmlns:document="clr-namespace:PixiEditor.ViewModels.SubViewModels.Document"
              mc:Ignorable="d"
-             d:DesignWidth="280" d:DesignHeight="25"
-             d:DataContext="{d:DesignInstance local:AutosaveControl}">
+             x:Name="uc"
+             d:DesignWidth="280" d:DesignHeight="25">
     <Border Padding="8, 0"
             CornerRadius="5"
             BorderThickness="0"
@@ -35,16 +35,16 @@
                 <ColumnDefinition />
             </Grid.ColumnDefinitions>
             <TextBlock
-                Text="{Binding IconText, FallbackValue='', Mode=OneWay}"
+                Text="{Binding IconText, FallbackValue='', Mode=OneWay, ElementName=uc}"
                 FontFamily="{StaticResource Feather}"
                 FontSize="16" Margin="0,1,5,0"
                 VerticalAlignment="Center"
-                Foreground="{Binding IconBrush, Mode=OneWay}">
+                Foreground="{Binding IconBrush, Mode=OneWay, ElementName=uc}">
                 <TextBlock.Style>
                     <Style TargetType="TextBlock">
                         <Style.Triggers>
                             <DataTrigger
-                                Binding="{Binding PulseIcon, Mode=OneWay}"
+                                Binding="{Binding PulseIcon, Mode=OneWay, ElementName=uc}"
                                 Value="True">
                                 <DataTrigger.EnterActions>
                                     <BeginStoryboard x:Name="storyboard">
@@ -87,13 +87,13 @@
                 </StackPanel.Resources>
                 <Button Command="{xaml:Command PixiEditor.Autosave.ToggleAutosave}">
                     <TextBlock
-                        Text="{Binding AutosaveEnabled, Mode=OneWay, Converter={converters:BoolToValueConverter FalseValue='', TrueValue=''}}"
+                        Text="{Binding AutosaveEnabled, ElementName=uc, Mode=OneWay, Converter={converters:BoolToValueConverter FalseValue='', TrueValue=''}}"
                         FontFamily="{StaticResource Feather}" />
                 </Button>
             </StackPanel>
             <TextBlock
                 Grid.Column="2"
-                ui:Translator.LocalizedString="{Binding Text, Mode=OneWay}"
+                ui:Translator.LocalizedString="{Binding Text, Mode=OneWay, ElementName=uc}"
                 Foreground="White"
                 VerticalAlignment="Center">
                 <TextBlock.Style>
@@ -103,7 +103,7 @@
                                 <MultiDataTrigger.Conditions>
                                     <Condition Binding="{Binding IsMouseOver, ElementName=autosaveControl}"
                                                Value="False" />
-                                    <Condition Binding="{Binding IsForceExpanded}" Value="False" />
+                                    <Condition Binding="{Binding IsForceExpanded, ElementName=uc}" Value="False" />
                                 </MultiDataTrigger.Conditions>
                                 <Setter Property="Visibility" Value="Hidden" />
                             </MultiDataTrigger>

+ 36 - 72
src/PixiEditor/Views/UserControls/AutosaveControl.xaml.cs

@@ -5,63 +5,24 @@ using System.Windows.Controls;
 using System.Windows.Media;
 using System.Windows.Threading;
 using PixiEditor.Extensions.Common.Localization;
+using PixiEditor.Models.DocumentModels.Autosave.Enums;
+using PixiEditor.Models.DocumentModels.Autosave.Structs;
 
 namespace PixiEditor.Views.UserControls;
 
-public enum AutosaveState
-{
-    Paused,
-    Idle,
-    AwaitingUpdateableChangeEnd,
-    InProgress
-}
-
-public enum UserFileAutosaveResult
-{
-    Success,
-    NoUserFile,
-    ExceptionWhileSaving,
-    Disabled,
-    NothingToSave
-}
-
-public enum BackupAutosaveResult
-{
-    Success,
-    Error,
-    NothingToSave
-}
-
-public struct LastAutosaveData
-{
-    public DateTime Time { get; set; }
-    
-    public UserFileAutosaveResult UserFileSaveResult { get; set; }
-    
-    public BackupAutosaveResult BackupSaveResult { get; set; }
-}
-
-public struct AutosaveStateData
-{
-    public LastAutosaveData? LastAutosaveData { get; set; }
-    public AutosaveState AutosaveState { get; set; }
-    public DateTime AutosaveLaunchDateTime { get; set; }
-    public TimeSpan AutosaveInterval { get; set; }
-}
-
 public partial class AutosaveControl : UserControl, INotifyPropertyChanged
 {
     public static readonly DependencyProperty AutosaveStateDataProperty =
-        DependencyProperty.Register(nameof(AutosaveStateData), typeof(AutosaveStateData), typeof(AutosaveControl), new PropertyMetadata(OnStateChanged));
+        DependencyProperty.Register(nameof(AutosaveStateData), typeof(AutosaveStateData?), typeof(AutosaveControl), new PropertyMetadata(OnStateChanged));
     
-    public AutosaveStateData AutosaveStateData
+    public AutosaveStateData? AutosaveStateData
     {
-        get => (AutosaveStateData)GetValue(AutosaveStateDataProperty);
+        get => (AutosaveStateData?)GetValue(AutosaveStateDataProperty);
         set => SetValue(AutosaveStateDataProperty, value);
     }
     
     public static readonly DependencyProperty AutosaveEnabledProperty =
-        DependencyProperty.Register(nameof(AutosaveEnabled), typeof(AutosaveStateData), typeof(AutosaveControl));
+        DependencyProperty.Register(nameof(AutosaveEnabled), typeof(bool), typeof(AutosaveControl));
     
     public bool AutosaveEnabled
     {
@@ -75,6 +36,8 @@ public partial class AutosaveControl : UserControl, INotifyPropertyChanged
     private const string WarnIcon = "\ue81e";
     private const string SaveIcon = "\ue8bc";
     private const string PauseIcon = "\ue8a2";
+    
+    private const double TimerIntervalSeconds = 3.8;
 
     private readonly Brush errorBrush = new SolidColorBrush(Color.FromArgb(255, 214, 66, 56));
     private readonly Brush warnBrush = new SolidColorBrush(Color.FromArgb(255, 219, 189, 53));
@@ -83,7 +46,6 @@ public partial class AutosaveControl : UserControl, INotifyPropertyChanged
     private readonly Brush inactiveBrush = new SolidColorBrush(Color.FromArgb(255, 120, 120, 120));
     
     private DispatcherTimer textUpdateTimer;
-    private const double timerIntervalSeconds = 3.8;
 
     private string iconText;
 
@@ -128,9 +90,8 @@ public partial class AutosaveControl : UserControl, INotifyPropertyChanged
     public AutosaveControl()
     {
         InitializeComponent();
-        DataContext = this;
 
-        textUpdateTimer = new DispatcherTimer(TimeSpan.FromSeconds(timerIntervalSeconds), DispatcherPriority.Normal, (_, _) => Update(), Application.Current.Dispatcher)
+        textUpdateTimer = new DispatcherTimer(TimeSpan.FromSeconds(TimerIntervalSeconds), DispatcherPriority.Normal, (_, _) => Update(), Application.Current.Dispatcher)
         {
             IsEnabled = true
         };
@@ -138,46 +99,49 @@ public partial class AutosaveControl : UserControl, INotifyPropertyChanged
 
     private void Update()
     {
-        var data = AutosaveStateData;
-        if (data.AutosaveState is AutosaveState.Paused)
+        if (AutosaveStateData is null || AutosaveStateData.Value.AutosaveState is AutosaveState.Paused )
         {
             UpdateTextSave("AUTOSAVE_DISABLED", false, PauseIcon, activeBrush, false);
+            textUpdateTimer.Stop();
             return;
         }
+        if (!textUpdateTimer.IsEnabled)
+            textUpdateTimer.Start();
         
-        if (data.LastAutosaveData is null)
+        var data = AutosaveStateData.Value;
+        if (data.LastBackupAutosaveData is null)
         {
-            SetWaitingToSave();
+            SetWaitingToSave(data);
             return;
         }
         
-        if (AutosaveStateData.AutosaveState is AutosaveState.Idle)
+        if (data.AutosaveState is AutosaveState.Idle)
         {
-            if ((DateTime.Now - data.LastAutosaveData.Value.Time).TotalSeconds < (timerIntervalSeconds - 0.1))
+            if ((DateTime.Now - data.LastBackupAutosaveData.Value.Time).TotalSeconds < (TimerIntervalSeconds - 0.1))
             {
                 // just autosaved, show result
                 bool showingError = false;
-                if (data.LastAutosaveData.Value.UserFileSaveResult is UserFileAutosaveResult.NoUserFile)
+                if (data.LastUserFileAutosaveData?.SaveResult is UserFileAutosaveResult.NoUserFile)
                 {
                     UpdateTextSave("AUTOSAVE_PLEASE_RESAVE", true, SaveIcon, errorBrush, true);
                     showingError = true;
                 }
-                if (data.LastAutosaveData.Value.BackupSaveResult is BackupAutosaveResult.Error || 
-                    data.LastAutosaveData.Value.UserFileSaveResult is UserFileAutosaveResult.ExceptionWhileSaving)
+                if (data.LastBackupAutosaveData.Value.SaveResult is BackupAutosaveResult.Error || 
+                    data.LastUserFileAutosaveData?.SaveResult is UserFileAutosaveResult.ExceptionWhileSaving)
                 {
-                    SetWaitingToSave();
+                    SetWaitingToSave(data);
                     showingError = true;
                 }
                 if (showingError)
                     return;
 
-                if (data.LastAutosaveData.Value.BackupSaveResult is BackupAutosaveResult.NothingToSave)
+                if (data.LastBackupAutosaveData.Value.SaveResult is BackupAutosaveResult.NothingToSave)
                 {
                     UpdateTextSave("AUTOSAVE_NOTHING_CHANGED", false, SaveIcon, inactiveBrush, false);
                     return;
                 }
 
-                if (data.LastAutosaveData.Value.BackupSaveResult is BackupAutosaveResult.Success)
+                if (data.LastBackupAutosaveData.Value.SaveResult is BackupAutosaveResult.Success)
                 {
                     UpdateTextSave("AUTOSAVE_SAVED", true, SaveIcon, successBrush, false);
                     return;
@@ -185,37 +149,37 @@ public partial class AutosaveControl : UserControl, INotifyPropertyChanged
             }
             else
             {
-                SetWaitingToSave();
+                SetWaitingToSave(data);
                 return;
             }
         }
         
-        if (AutosaveStateData.AutosaveState is AutosaveState.AwaitingUpdateableChangeEnd)
+        if (data.AutosaveState is AutosaveState.AwaitingUpdateableChangeEnd)
         {
             UpdateTextSave("AUTOSAVE_WAITING_FOR_SAVE", true, SaveIcon, activeBrush, true);
             return;
         }
 
-        if (AutosaveStateData.AutosaveState is AutosaveState.InProgress)
+        if (data.AutosaveState is AutosaveState.InProgress)
         {
             UpdateTextSave("AUTOSAVE_SAVING", true, SaveIcon, activeBrush, true);
             return;
         }
     }
 
-    private void SetWaitingToSave()
+    private void SetWaitingToSave(AutosaveStateData data)
     {
-        var data = AutosaveStateData;
-        TimeSpan timeLeft = data.LastAutosaveData switch
+        TimeSpan timeLeft = data.LastBackupAutosaveData switch
         {
             null => data.AutosaveInterval - (DateTime.Now - data.AutosaveLaunchDateTime),
             { } lastData => data.AutosaveInterval - (DateTime.Now - lastData.Time)
         };
         
-        bool error = data.LastAutosaveData switch
+        bool error = (data.LastBackupAutosaveData, data.LastUserFileAutosaveData) switch
         {
-            null => false,
-            { } lastData => lastData.BackupSaveResult != BackupAutosaveResult.Error && lastData.UserFileSaveResult != UserFileAutosaveResult.ExceptionWhileSaving
+            (null, null) => false,
+            ({ } backup, null) => backup.SaveResult == BackupAutosaveResult.Error,
+            ({ } backup, { } autosave) => backup.SaveResult == BackupAutosaveResult.Error || autosave.SaveResult == UserFileAutosaveResult.ExceptionWhileSaving
         };
         
         if (timeLeft.Minutes == 0 && !error)
@@ -224,8 +188,8 @@ public partial class AutosaveControl : UserControl, INotifyPropertyChanged
             return;
         }
         
-        var adjusted = timeLeft.Add(TimeSpan.FromSeconds(30));
-        var minute = adjusted.Minutes < 2
+        TimeSpan adjusted = timeLeft.Add(TimeSpan.FromSeconds(30));
+        LocalizedString minute = adjusted.Minutes < 2
             ? new LocalizedString("MINUTE_SINGULAR")
             : new LocalizedString("MINUTE_PLURAL");
         
@@ -257,7 +221,7 @@ public partial class AutosaveControl : UserControl, INotifyPropertyChanged
         PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
     }
 
-    protected bool SetField<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
+    private bool SetField<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
     {
         if (EqualityComparer<T>.Default.Equals(field, value)) return false;
         field = value;