Browse Source

saving works

Krzysztof Krysiński 6 months ago
parent
commit
7829f136f2

+ 3 - 0
src/PixiEditor.Extensions.CommonApi/UserPreferences/PreferencesConstants.cs

@@ -24,4 +24,7 @@ public static class PreferencesConstants
 
 
     public const string AutosaveToDocumentPath = "AutosaveToDocumentPath";
     public const string AutosaveToDocumentPath = "AutosaveToDocumentPath";
     public const bool AutosaveToDocumentPathDefault = false;
     public const bool AutosaveToDocumentPathDefault = false;
+
+    public const string LastCrashFile = "LastCrashFile";
+    /*public const string UnsavedNextSessionFiles = "UnsavedNextSessionFiles";*/
 }
 }

+ 1 - 0
src/PixiEditor/Helpers/ServiceCollectionHelpers.cs

@@ -67,6 +67,7 @@ internal static class ServiceCollectionHelpers
             .AddSingleton<ColorsViewModel>()
             .AddSingleton<ColorsViewModel>()
             .AddSingleton<AnimationsViewModel>()
             .AddSingleton<AnimationsViewModel>()
             .AddSingleton<NodeGraphManagerViewModel>()
             .AddSingleton<NodeGraphManagerViewModel>()
+            .AddSingleton<AutosaveViewModel>()
             .AddSingleton<IColorsHandler, ColorsViewModel>(x => x.GetRequiredService<ColorsViewModel>())
             .AddSingleton<IColorsHandler, ColorsViewModel>(x => x.GetRequiredService<ColorsViewModel>())
             .AddSingleton<RegistryViewModel>()
             .AddSingleton<RegistryViewModel>()
             .AddSingleton(static x => new DiscordViewModel(x.GetService<ViewModels_ViewModelMain>(), "764168193685979138"))
             .AddSingleton(static x => new DiscordViewModel(x.GetService<ViewModels_ViewModelMain>(), "764168193685979138"))

+ 20 - 0
src/PixiEditor/ViewModels/CrashReportViewModel.cs

@@ -84,6 +84,26 @@ internal partial class CrashReportViewModel : Window
         return hasRecoveredDocuments;
         return hasRecoveredDocuments;
     }
     }
 
 
+    public static void ShowMissingFilesDialog(CrashReport crashReport)
+    {
+        var dialog = new OptionsDialog<LocalizedString>(
+            "CRASH_NOT_ALL_DOCUMENTS_RECOVERED_TITLE",
+            new LocalizedString("CRASH_NOT_ALL_DOCUMENTS_RECOVERED"), MainWindow.Current)
+        {
+            {
+                "SEND", _ =>
+                {
+                    var sendReportDialog = new SendCrashReportDialog(crashReport);
+                    sendReportDialog.ShowDialog(MainWindow.Current);
+                }
+            },
+            "CLOSE"
+        };
+
+        dialog.ShowDialog(true);
+    }
+
+
     [Conditional("DEBUG")]
     [Conditional("DEBUG")]
     private void SetIsDebug()
     private void SetIsDebug()
     {
     {

+ 1 - 0
src/PixiEditor/ViewModels/Document/DocumentViewModel.cs

@@ -241,6 +241,7 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
         AnimationDataViewModel = new(this, Internals);
         AnimationDataViewModel = new(this, Internals);
 
 
         NodeGraph = new NodeGraphViewModel(this, Internals);
         NodeGraph = new NodeGraphViewModel(this, Internals);
+        AutosaveViewModel = new AutosaveDocumentViewModel(this, Internals);
 
 
         TransformViewModel = new(this);
         TransformViewModel = new(this);
         TransformViewModel.TransformChanged += (args) => Internals.ChangeController.TransformChangedInlet(args);
         TransformViewModel.TransformChanged += (args) => Internals.ChangeController.TransformChangedInlet(args);

+ 60 - 0
src/PixiEditor/ViewModels/SubViewModels/DebugViewModel.cs

@@ -76,6 +76,8 @@ internal class DebugViewModel : SubViewModel<ViewModelMain>
         }
         }
     }
     }
 
 
+    public bool ModifiedEditorData { get; set; }
+
     public DebugViewModel(ViewModelMain owner)
     public DebugViewModel(ViewModelMain owner)
         : base(owner)
         : base(owner)
     {
     {
@@ -411,4 +413,62 @@ internal class DebugViewModel : SubViewModel<ViewModelMain>
         IsDebugModeEnabled = value;
         IsDebugModeEnabled = value;
         UseDebug = IsDebugBuild || IsDebugModeEnabled;
         UseDebug = IsDebugBuild || IsDebugModeEnabled;
     }
     }
+
+    [Command.Debug("PixiEditor.Debug.BackupUserPreferences", @"%appdata%\PixiEditor\user_preferences.json",
+        "BACKUP_USR_PREFS", "BACKUP_USR_PREFS")]
+    [Command.Debug("PixiEditor.Debug.BackupEditorData", @"%localappdata%\PixiEditor\editor_data.json",
+        "BACKUP_EDITOR_DATA", "BACKUP_EDITOR_DATA")]
+    [Command.Debug("PixiEditor.Debug.BackupShortcutFile", @"%appdata%\PixiEditor\shortcuts.json",
+        "BACKUP_SHORTCUT_FILE", "BACKUP_SHORTCUT_FILE")]
+    public static void BackupFile(string path)
+    {
+        string file = Environment.ExpandEnvironmentVariables(path);
+        string backup = $"{file}.bak";
+
+        if (!File.Exists(file))
+        {
+            NoticeDialog.Show(new LocalizedString("File {0} does not exist\n(Full Path: {1})", path, file),
+                "FILE_NOT_FOUND");
+            return;
+        }
+
+        File.Copy(file, backup, true);
+    }
+
+    [Command.Debug("PixiEditor.Debug.LoadUserPreferencesBackup", @"%appdata%\PixiEditor\user_preferences.json",
+        "LOAD_USR_PREFS_BACKUP", "LOAD_USR_PREFS_BACKUP")]
+    [Command.Debug("PixiEditor.Debug.LoadEditorDataBackup", @"%localappdata%\PixiEditor\editor_data.json",
+        "LOAD_EDITOR_DATA_BACKUP", "LOAD_EDITOR_DATA_BACKUP")]
+    [Command.Debug("PixiEditor.Debug.LoadShortcutFileBackup", @"%appdata%\PixiEditor\shortcuts.json",
+        "LOAD_SHORTCUT_FILE_BACKUP", "LOAD_SHORTCUT_FILE_BACKUP")]
+    public void LoadBackupFile(string path)
+    {
+        if (path.EndsWith("editor_data.json"))
+        {
+            ModifiedEditorData = true;
+        }
+
+        string file = Environment.ExpandEnvironmentVariables(path);
+        string backup = $"{file}.bak";
+
+        if (!File.Exists(backup))
+        {
+            NoticeDialog.Show(new LocalizedString("File {0} does not exist\n(Full Path: {1})", path, file),
+                "FILE_NOT_FOUND");
+            return;
+        }
+
+        if (File.Exists(file))
+        {
+            OptionsDialog<string> dialog =
+                new("ARE_YOU_SURE", $"Are you sure you want to overwrite {path}\n(Full Path: {file})", MainWindow.Current)
+                {
+                    { "Yes", x => File.Delete(file) }, "Cancel"
+                };
+
+            dialog.ShowDialog();
+        }
+
+        File.Copy(backup, file, true);
+    }
 }
 }

+ 207 - 34
src/PixiEditor/ViewModels/SubViewModels/FileViewModel.cs

@@ -20,6 +20,9 @@ using PixiEditor.Models.IO;
 using PixiEditor.Models.UserData;
 using PixiEditor.Models.UserData;
 using Drawie.Numerics;
 using Drawie.Numerics;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.DependencyInjection;
+using PixiEditor.Extensions.CommonApi.UserPreferences;
+using PixiEditor.Models.DocumentModels.Autosave;
+using PixiEditor.Models.ExceptionHandling;
 using PixiEditor.Models.IO.CustomDocumentFormats;
 using PixiEditor.Models.IO.CustomDocumentFormats;
 using PixiEditor.OperatingSystem;
 using PixiEditor.OperatingSystem;
 using PixiEditor.Parser;
 using PixiEditor.Parser;
@@ -100,6 +103,11 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
     }
     }
 
 
     private void OpenHelloTherePopup()
     private void OpenHelloTherePopup()
+    {
+        new HelloTherePopup(this).Show();
+    }
+
+    private void Owner_OnStartupEvent()
     {
     {
         List<string> args = StartupArgs.Args;
         List<string> args = StartupArgs.Args;
         string file = args.FirstOrDefault(x => Importer.IsSupportedFile(x) && File.Exists(x));
         string file = args.FirstOrDefault(x => Importer.IsSupportedFile(x) && File.Exists(x));
@@ -114,7 +122,7 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
 
 
                 if (lastCrash == null)
                 if (lastCrash == null)
                 {
                 {
-                    MaybeReopenTempAutosavedFiles();
+                    TryReopenTempAutosavedFiles();
                 }
                 }
                 else
                 else
                 {
                 {
@@ -135,23 +143,6 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
             CrashHelper.SendExceptionInfo(exc);
             CrashHelper.SendExceptionInfo(exc);
         }
         }
 
 
-        if (file != null)
-        {
-            OpenFromPath(file);
-        }
-        else if ((Owner.DocumentManagerSubViewModel.Documents.Count == 0 && !args.Contains("--crash")) && !args.Contains("--openedInExisting"))
-        {
-            if (preferences!.GetPreference("ShowStartupWindow", true))
-            {
-                OpenHelloTherePopup();
-            }
-        }
-    }
-
-    private void Owner_OnStartupEvent()
-    {
-        List<string> args = StartupArgs.Args;
-        string file = args.FirstOrDefault(x => Importer.IsSupportedFile(x) && File.Exists(x));
         if (file != null)
         if (file != null)
         {
         {
             OpenFromPath(file);
             OpenFromPath(file);
@@ -159,7 +150,7 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
         else if ((Owner.DocumentManagerSubViewModel.Documents.Count == 0 && !args.Contains("--crash")) &&
         else if ((Owner.DocumentManagerSubViewModel.Documents.Count == 0 && !args.Contains("--crash")) &&
                  !args.Contains("--openedInExisting"))
                  !args.Contains("--openedInExisting"))
         {
         {
-            if (PixiEditorSettings.StartupWindow.ShowStartupWindow.Value)
+            if (preferences!.GetPreference("ShowStartupWindow", true))
             {
             {
                 OpenHelloTherePopup();
                 OpenHelloTherePopup();
             }
             }
@@ -239,25 +230,24 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
     /// <summary>
     /// <summary>
     /// Tries to open the passed file if it isn't already open
     /// Tries to open the passed file if it isn't already open
     /// </summary>
     /// </summary>
-    public void OpenFromPath(string path, bool associatePath = true)
+    public DocumentViewModel OpenFromPath(string path, bool associatePath = true)
     {
     {
         if (MakeExistingDocumentActiveIfOpened(path))
         if (MakeExistingDocumentActiveIfOpened(path))
-            return;
+            return null;
 
 
         try
         try
         {
         {
             if (path.EndsWith(".pixi"))
             if (path.EndsWith(".pixi"))
             {
             {
-                OpenDotPixi(path, associatePath);
-            }
-            else if (IsCustomFormat(path))
-            {
-                OpenCustomFormat(path, associatePath);
+                return OpenDotPixi(path, associatePath);
             }
             }
-            else
+
+            if (IsCustomFormat(path))
             {
             {
-                OpenRegularImage(path, associatePath);
+                return OpenCustomFormat(path, associatePath);
             }
             }
+
+            return OpenRegularImage(path, associatePath);
         }
         }
         catch (RecoverableException ex)
         catch (RecoverableException ex)
         {
         {
@@ -267,6 +257,8 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
         {
         {
             NoticeDialog.Show("OLD_FILE_FORMAT_DESCRIPTION", "OLD_FILE_FORMAT");
             NoticeDialog.Show("OLD_FILE_FORMAT_DESCRIPTION", "OLD_FILE_FORMAT");
         }
         }
+
+        return null;
     }
     }
 
 
     private bool IsCustomFormat(string path)
     private bool IsCustomFormat(string path)
@@ -275,7 +267,7 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
         return documentBuilders.Any(x => x.Extensions.Contains(extension, StringComparer.OrdinalIgnoreCase));
         return documentBuilders.Any(x => x.Extensions.Contains(extension, StringComparer.OrdinalIgnoreCase));
     }
     }
 
 
-    private void OpenCustomFormat(string path, bool associatePath)
+    private DocumentViewModel? OpenCustomFormat(string path, bool associatePath)
     {
     {
         IDocumentBuilder builder = documentBuilders.First(x =>
         IDocumentBuilder builder = documentBuilders.First(x =>
             x.Extensions.Contains(Path.GetExtension(path), StringComparer.OrdinalIgnoreCase));
             x.Extensions.Contains(Path.GetExtension(path), StringComparer.OrdinalIgnoreCase));
@@ -283,7 +275,7 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
         if (!File.Exists(path))
         if (!File.Exists(path))
         {
         {
             NoticeDialog.Show("FILE_NOT_FOUND", "FAILED_TO_OPEN_FILE");
             NoticeDialog.Show("FILE_NOT_FOUND", "FAILED_TO_OPEN_FILE");
-            return;
+            return null;
         }
         }
 
 
         try
         try
@@ -297,6 +289,7 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
             }
             }
 
 
             AddRecentlyOpened(document.FullFilePath);
             AddRecentlyOpened(document.FullFilePath);
+            return document;
         }
         }
         catch (Exception ex)
         catch (Exception ex)
         {
         {
@@ -304,12 +297,14 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
             Console.WriteLine(ex);
             Console.WriteLine(ex);
             CrashHelper.SendExceptionInfo(ex);
             CrashHelper.SendExceptionInfo(ex);
         }
         }
+
+        return null;
     }
     }
 
 
     /// <summary>
     /// <summary>
     /// Opens a .pixi file from path, creates a document from it, and adds it to the system
     /// Opens a .pixi file from path, creates a document from it, and adds it to the system
     /// </summary>
     /// </summary>
-    private void OpenDotPixi(string path, bool associatePath = true)
+    private DocumentViewModel OpenDotPixi(string path, bool associatePath = true)
     {
     {
         DocumentViewModel document = Importer.ImportDocument(path, associatePath);
         DocumentViewModel document = Importer.ImportDocument(path, associatePath);
 
 
@@ -318,26 +313,40 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
 
 
         var fileSize = new FileInfo(path).Length;
         var fileSize = new FileInfo(path).Length;
         Analytics.SendOpenFile(PixiFileType.PixiFile, fileSize, document.SizeBindable);
         Analytics.SendOpenFile(PixiFileType.PixiFile, fileSize, document.SizeBindable);
+
+        return document;
     }
     }
 
 
     /// <summary>
     /// <summary>
     /// Opens a .pixi file from path, creates a document from it, and adds it to the system
     /// Opens a .pixi file from path, creates a document from it, and adds it to the system
     /// </summary>
     /// </summary>
-    public void OpenRecoveredDotPixi(string? originalPath, byte[] dotPixiBytes)
+    public void OpenRecoveredDotPixi(string? originalPath, string? autosavePath, Guid? autosaveGuid, byte[] dotPixiBytes)
     {
     {
         DocumentViewModel document = Importer.ImportDocument(dotPixiBytes, originalPath);
         DocumentViewModel document = Importer.ImportDocument(dotPixiBytes, originalPath);
         document.MarkAsUnsaved();
         document.MarkAsUnsaved();
+
+        if (autosavePath != null)
+        {
+            document.AutosaveViewModel.SetTempFileGuidAndLastSavedPath(autosaveGuid!.Value, autosavePath);
+        }
+
+        AddDocumentViewModelToTheSystem(document);
+    }
+
+    public void OpenFromPixiBytes(byte[] bytes)
+    {
+        DocumentViewModel document = Importer.ImportDocument(bytes, null);
         AddDocumentViewModelToTheSystem(document);
         AddDocumentViewModelToTheSystem(document);
     }
     }
 
 
     /// <summary>
     /// <summary>
     /// Opens a regular image file from path, creates a document from it, and adds it to the system.
     /// Opens a regular image file from path, creates a document from it, and adds it to the system.
     /// </summary>
     /// </summary>
-    private void OpenRegularImage(string path, bool associatePath)
+    private DocumentViewModel OpenRegularImage(string path, bool associatePath)
     {
     {
         var image = Importer.ImportImage(path, VecI.NegativeOne);
         var image = Importer.ImportImage(path, VecI.NegativeOne);
 
 
-        if (image == null) return;
+        if (image == null) return null;
 
 
         var doc = NewDocument(b => b
         var doc = NewDocument(b => b
             .WithSize(image.Size)
             .WithSize(image.Size)
@@ -369,8 +378,58 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
             CrashHelper.SendExceptionInfo(new InvalidFileTypeException(default,
             CrashHelper.SendExceptionInfo(new InvalidFileTypeException(default,
                 $"Invalid file type '{fileType}'"));
                 $"Invalid file type '{fileType}'"));
         }
         }
+
+        return doc;
     }
     }
 
 
+    public void OpenFromReport(CrashReport report, out bool showMissingFilesDialog)
+    {
+        var documents = report.RecoverDocuments(out var info);
+
+        var i = 0;
+
+        Exception firstException = null;
+        Exception secondException = null;
+        Exception thirdException = null;
+
+        foreach (var document in documents)
+        {
+            try
+            {
+                OpenRecoveredDotPixi(document.OriginalPath, document.AutosavePath,
+                    AutosaveHelper.GetAutosaveGuid(document.AutosavePath), document.GetRecoveredBytes());
+                i++;
+            }
+            catch (Exception e)
+            {
+                firstException = e;
+
+                try
+                {
+                    OpenFromPath(document.AutosavePath, false);
+                }
+                catch (Exception deepE)
+                {
+                    secondException = deepE;
+
+                    try
+                    {
+                        OpenRecoveredDotPixi(document.OriginalPath, document.AutosavePath,
+                            AutosaveHelper.GetAutosaveGuid(document.AutosavePath), document.GetAutosaveBytes());
+                    }
+                    catch (Exception veryDeepE)
+                    {
+                        thirdException = veryDeepE;
+                    }
+                }
+            }
+
+            var exceptions = new[] { firstException, secondException, thirdException };
+            CrashHelper.SendExceptionInfo(new AggregateException(exceptions.Where(x => x != null).ToArray()));
+        }
+
+        showMissingFilesDialog = documents.Count != i;
+    }
 
 
     /// <summary>
     /// <summary>
     /// Opens a regular image file from path, creates a document from it, and adds it to the system.
     /// Opens a regular image file from path, creates a document from it, and adds it to the system.
@@ -582,6 +641,120 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
         }
         }
     }
     }
 
 
+    private void TryReopenTempAutosavedFiles()
+    {
+        var preferences = Owner.Preferences;
+
+        // 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, PreferencesConstants.SaveSessionStateDefault))
+            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:
+                            LoadFromAutosave(lastEntry);
+                            break;
+                        case AutosaveHistoryResult.SavedUserFile:
+                        case AutosaveHistoryResult.NothingToSave:
+                            // load from user file
+                            break;
+                        default:
+                            throw new ArgumentOutOfRangeException();
+                    }
+                }
+            }
+            catch (Exception e)
+            {
+                CrashHelper.SendExceptionInfo(e);
+            }
+        }
+
+        Owner.AutosaveViewModel.CleanupAutosavedFilesAndHistory();
+
+        /*foreach (var file in files)
+        {
+            try
+            {
+                if (file.AutosavePath != null)
+                {
+                    var document = OpenFromPath(file.AutosavePath, false);
+                    document.FullFilePath = file.OriginalPath;
+
+                    if (file.AutosavePath != null)
+                    {
+                        document.AutosaveViewModel.SetTempFileGuidAndLastSavedPath(
+                            AutosaveHelper.GetAutosaveGuid(file.AutosavePath)!.Value, file.AutosavePath);
+                    }
+                }
+                else
+                {
+                    OpenFromPath(file.OriginalPath);
+                }
+            }
+            catch (Exception e)
+            {
+                CrashHelper.SendExceptionInfo(e);
+            }
+        }*/
+    }
+
+    private void LoadFromAutosave(AutosaveHistoryEntry entry)
+    {
+        string path = AutosaveHelper.GetAutosavePath(entry.TempFileGuid);
+        if (path == null)
+        {
+            // TODO: Notify
+            return;
+        }
+
+        var document = OpenFromPath(path, false);
+        document.AutosaveViewModel.SetTempFileGuidAndLastSavedPath(entry.TempFileGuid, path);
+    }
+
     private List<RecentlyOpenedDocument> GetRecentlyOpenedDocuments()
     private List<RecentlyOpenedDocument> GetRecentlyOpenedDocuments()
     {
     {
         var paths = PixiEditorSettings.File.RecentlyOpened.Value.Take(PixiEditorSettings.File.MaxOpenedRecently.Value);
         var paths = PixiEditorSettings.File.RecentlyOpened.Value.Take(PixiEditorSettings.File.MaxOpenedRecently.Value);

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

@@ -54,4 +54,32 @@ internal class FileSettings : SettingsGroup
         get => disableNewsPanel;
         get => disableNewsPanel;
         set => RaiseAndUpdatePreference(ref disableNewsPanel, value, PreferencesConstants.DisableNewsPanel);
         set => RaiseAndUpdatePreference(ref disableNewsPanel, value, PreferencesConstants.DisableNewsPanel);
     }
     }
+
+    private bool autosaveEnabled = GetPreference(PreferencesConstants.AutosaveEnabled, PreferencesConstants.AutosaveEnabledDefault);
+    public bool AutosaveEnabled
+    {
+        get => autosaveEnabled;
+        set => RaiseAndUpdatePreference(ref autosaveEnabled, value);
+    }
+
+    private bool saveSessionEnabled = GetPreference(PreferencesConstants.SaveSessionStateEnabled, PreferencesConstants.SaveSessionStateDefault);
+    public bool SaveSessionEnabled
+    {
+        get => saveSessionEnabled;
+        set => RaiseAndUpdatePreference(ref saveSessionEnabled, value);
+    }
+
+    private double autosavePeriodMinutes = GetPreference(PreferencesConstants.AutosavePeriodMinutes, PreferencesConstants.AutosavePeriodDefault);
+    public double AutosavePeriodMinutes
+    {
+        get => autosavePeriodMinutes;
+        set => RaiseAndUpdatePreference(ref autosavePeriodMinutes, value);
+    }
+
+    private bool autosaveToDocumentPath = GetPreference(PreferencesConstants.AutosaveToDocumentPath, PreferencesConstants.AutosaveToDocumentPathDefault);
+    public bool AutosaveToDocumentPath
+    {
+        get => autosaveToDocumentPath;
+        set => RaiseAndUpdatePreference(ref autosaveToDocumentPath, value);
+    }
 }
 }

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

@@ -60,12 +60,13 @@ internal partial class ViewModelMain : ViewModelBase, ICommandsHandler
     public MenuBarViewModel MenuBarViewModel { get; set; }
     public MenuBarViewModel MenuBarViewModel { get; set; }
     public AnimationsViewModel AnimationsSubViewModel { get; set; }
     public AnimationsViewModel AnimationsSubViewModel { get; set; }
     public NodeGraphManagerViewModel NodeGraphManager { get; set; }
     public NodeGraphManagerViewModel NodeGraphManager { get; set; }
+    public AutosaveViewModel AutosaveViewModel { get; set; }
 
 
     public IPreferences Preferences { get; set; }
     public IPreferences Preferences { get; set; }
     public ILocalizationProvider LocalizationProvider { get; set; }
     public ILocalizationProvider LocalizationProvider { get; set; }
 
 
-    public ConfigManager Config { get; set; }    
-    
+    public ConfigManager Config { get; set; }
+
     public LocalizedString ActiveActionDisplay
     public LocalizedString ActiveActionDisplay
     {
     {
         get
         get
@@ -100,11 +101,11 @@ internal partial class ViewModelMain : ViewModelBase, ICommandsHandler
     {
     {
         Services = services;
         Services = services;
 
 
-        Config = new ConfigManager(); 
+        Config = new ConfigManager();
 
 
         Preferences = services.GetRequiredService<IPreferences>();
         Preferences = services.GetRequiredService<IPreferences>();
         Preferences.Init();
         Preferences.Init();
-        
+
         SupportedFilesHelper.InitFileTypes(services.GetServices<IoFileType>());
         SupportedFilesHelper.InitFileTypes(services.GetServices<IoFileType>());
 
 
         CommandController = services.GetService<CommandController>();
         CommandController = services.GetService<CommandController>();
@@ -155,11 +156,13 @@ internal partial class ViewModelMain : ViewModelBase, ICommandsHandler
         ToolsSubViewModel?.SetupToolsTooltipShortcuts();
         ToolsSubViewModel?.SetupToolsTooltipShortcuts();
 
 
         SearchSubViewModel = services.GetService<SearchViewModel>();
         SearchSubViewModel = services.GetService<SearchViewModel>();
-        
+
         AnimationsSubViewModel = services.GetService<AnimationsViewModel>();
         AnimationsSubViewModel = services.GetService<AnimationsViewModel>();
-        
+
         NodeGraphManager = services.GetService<NodeGraphManagerViewModel>();
         NodeGraphManager = services.GetService<NodeGraphManagerViewModel>();
-        
+
+        AutosaveViewModel = services.GetService<AutosaveViewModel>();
+
         ExtensionsSubViewModel = services.GetService<ExtensionsViewModel>(); // Must be last
         ExtensionsSubViewModel = services.GetService<ExtensionsViewModel>(); // Must be last
 
 
         DocumentManagerSubViewModel.ActiveDocumentChanged += OnActiveDocumentChanged;
         DocumentManagerSubViewModel.ActiveDocumentChanged += OnActiveDocumentChanged;
@@ -178,6 +181,7 @@ internal partial class ViewModelMain : ViewModelBase, ICommandsHandler
     [RelayCommand]
     [RelayCommand]
     public async Task CloseWindow()
     public async Task CloseWindow()
     {
     {
+        AutosaveAllForNextSession();
         UserWantsToClose = await DisposeAllDocumentsWithSaveConfirmation();
         UserWantsToClose = await DisposeAllDocumentsWithSaveConfirmation();
 
 
         if (UserWantsToClose)
         if (UserWantsToClose)
@@ -232,6 +236,17 @@ internal partial class ViewModelMain : ViewModelBase, ICommandsHandler
         return true;
         return true;
     }
     }
 
 
+    public void AutosaveAllForNextSession()
+    {
+        if (!AutosaveViewModel.SaveSessionStateEnabled || DebugSubViewModel.ModifiedEditorData)
+            return;
+
+        foreach (DocumentViewModel document in DocumentManagerSubViewModel.Documents)
+        {
+            document.AutosaveViewModel.AutosaveOnClose();
+        }
+    }
+
     /// <summary>
     /// <summary>
     /// Disposes the active document after showing the unsaved changes confirmation dialog.
     /// Disposes the active document after showing the unsaved changes confirmation dialog.
     /// </summary>
     /// </summary>
@@ -263,7 +278,8 @@ internal partial class ViewModelMain : ViewModelBase, ICommandsHandler
         if (result != ConfirmationType.Canceled)
         if (result != ConfirmationType.Canceled)
         {
         {
             if (!DocumentManagerSubViewModel.Documents.Remove(document))
             if (!DocumentManagerSubViewModel.Documents.Remove(document))
-                throw new InvalidOperationException("Trying to close a document that's not in the documents collection. Likely, the document wasn't added there after creation by mistake.");
+                throw new InvalidOperationException(
+                    "Trying to close a document that's not in the documents collection. Likely, the document wasn't added there after creation by mistake.");
 
 
             if (DocumentManagerSubViewModel.ActiveDocument == document)
             if (DocumentManagerSubViewModel.ActiveDocument == document)
             {
             {
@@ -278,6 +294,7 @@ internal partial class ViewModelMain : ViewModelBase, ICommandsHandler
 
 
             return true;
             return true;
         }
         }
+
         return false;
         return false;
     }
     }
 
 

+ 1 - 12
src/PixiEditor/Views/MainWindow.axaml.cs

@@ -100,18 +100,7 @@ internal partial class MainWindow : Window
 
 
         var i = 0;
         var i = 0;
 
 
-        foreach (var document in documents)
-        {
-            try
-            {
-                fileVM.OpenRecoveredDotPixi(document.OriginalPath, document.GetRecoveredBytes());
-                i++;
-            }
-            catch (Exception e)
-            {
-                CrashHelper.SendExceptionInfo(e);
-            }
-        }
+        fileVM.OpenFromReport(report, out showMissingFilesDialog);
 
 
         showMissingFilesDialog = documents.Count != i;
         showMissingFilesDialog = documents.Count != i;
 
 

+ 1 - 1
src/PixiEditor/Views/Windows/BetaExampleButton.axaml.cs

@@ -97,7 +97,7 @@ public partial class BetaExampleButton : UserControl
         Application.Current.ForDesktopMainWindow(mainWindow => mainWindow.Activate());
         Application.Current.ForDesktopMainWindow(mainWindow => mainWindow.Activate());
         CloseCommand.Execute(null);
         CloseCommand.Execute(null);
 
 
-        ViewModelMain.Current.FileSubViewModel.OpenRecoveredDotPixi(null, bytes);
+        ViewModelMain.Current.FileSubViewModel.OpenFromPixiBytes(bytes);
         ViewModelMain.Current.DocumentManagerSubViewModel.Documents[^1].Operations.UseSrgbProcessing();
         ViewModelMain.Current.DocumentManagerSubViewModel.Documents[^1].Operations.UseSrgbProcessing();
         ViewModelMain.Current.DocumentManagerSubViewModel.Documents[^1].Operations.ClearUndo();
         ViewModelMain.Current.DocumentManagerSubViewModel.Documents[^1].Operations.ClearUndo();
         Analytics.SendOpenExample(FileName);
         Analytics.SendOpenExample(FileName);