diff --git a/.gitignore b/.gitignore index 4481bae5..3fdbaad5 100644 --- a/.gitignore +++ b/.gitignore @@ -179,3 +179,4 @@ Assets/StreamingAssets/EphysLink-* Assets/Samples/* Assets/settings_conversion.txt .github/copilot-instructions.md +.nuget/ diff --git a/Assets/Scripts/Models/SavedState.cs b/Assets/Scripts/Models/SavedState.cs new file mode 100644 index 00000000..2b9ddfc3 --- /dev/null +++ b/Assets/Scripts/Models/SavedState.cs @@ -0,0 +1,18 @@ +using System; +using Models.Scene; +using Models.Settings; + +namespace Models +{ + /// + /// Represents the application state for save/load operations. + /// Contains scene, atlas, and rig state slices. + /// + [Serializable] + public class SavedState + { + public SceneState SceneState; + public RigState RigState; + public AtlasSettingsState AtlasSettingsState; + } +} diff --git a/Assets/Scripts/Services/FileStorageService.cs b/Assets/Scripts/Services/FileStorageService.cs new file mode 100644 index 00000000..46accf28 --- /dev/null +++ b/Assets/Scripts/Services/FileStorageService.cs @@ -0,0 +1,66 @@ +using System; +using System.IO; +using UnityEngine; + +namespace Services +{ + /// + /// Provides methods to save and load data to/from local JSON files. + /// + public class FileStorageService + { + /// + /// Saves a value of type to a JSON file. + /// + /// The type of the value to save. + /// The full file path where the JSON file will be saved. + /// The value to save. + /// True if the save was successful, false otherwise. + public bool SaveToFile(string filePath, T value) + { + try + { + var json = JsonUtility.ToJson(value, true); + File.WriteAllText(filePath, json); + Debug.Log($"Saved state to: {filePath}"); + return true; + } + catch (Exception e) + { + Debug.LogError($"Failed to save to file {filePath}: {e.Message}"); + return false; + } + } + + /// + /// Loads a value of type from a JSON file. + /// + /// The type of the value to load. + /// The full file path to the JSON file to load. + /// The loaded value, or default if loading failed. + /// True if the load was successful, false otherwise. + public bool LoadFromFile(string filePath, out T result) where T : new() + { + result = default; + + try + { + if (!File.Exists(filePath)) + { + Debug.LogWarning($"File not found: {filePath}"); + return false; + } + + var json = File.ReadAllText(filePath); + result = JsonUtility.FromJson(json); + Debug.Log($"Loaded state from: {filePath}"); + return true; + } + catch (Exception e) + { + Debug.LogError($"Failed to load from file {filePath}: {e.Message}"); + return false; + } + } + } +} diff --git a/Assets/Scripts/Services/StoreService.cs b/Assets/Scripts/Services/StoreService.cs index 0b3244f6..268e36b8 100644 --- a/Assets/Scripts/Services/StoreService.cs +++ b/Assets/Scripts/Services/StoreService.cs @@ -12,6 +12,7 @@ namespace Services public class StoreService { private readonly LocalStorageService _localStorageService; + private readonly FileStorageService _fileStorageService; /// /// Gets the Redux store instance for partitioned application state. @@ -23,9 +24,11 @@ public class StoreService /// Loads initial state from local storage and configures the Redux store. /// /// The local storage service for state persistence. - public StoreService(LocalStorageService localStorageService) + /// The file storage service for save/load operations. + public StoreService(LocalStorageService localStorageService, FileStorageService fileStorageService) { _localStorageService = localStorageService; + _fileStorageService = fileStorageService; // Initialize state in memory. var initialMainState = _localStorageService.GetValue( @@ -359,5 +362,43 @@ public void Save() Store.GetState(SliceNames.ATLAS_SETTINGS_SLICE) ); } + + /// + /// Saves the current state to a JSON file. + /// + /// The full file path where to save the state. + /// True if the save was successful, false otherwise. + public bool SaveToFile(string filePath) + { + var savedState = new SavedState + { + SceneState = Store.GetState(SliceNames.SCENE_SLICE), + RigState = Store.GetState(SliceNames.RIG_SLICE), + AtlasSettingsState = Store.GetState(SliceNames.ATLAS_SETTINGS_SLICE) + }; + + return _fileStorageService.SaveToFile(filePath, savedState); + } + + /// + /// Loads state from a JSON file and updates both the store and local storage. + /// + /// The full file path from which to load the state. + /// True if the load was successful, false otherwise. + public bool LoadFromFile(string filePath) + { + if (!_fileStorageService.LoadFromFile(filePath, out var savedState)) + return false; + + // Update local storage with the loaded state. + _localStorageService.SetValue(SliceNames.SCENE_SLICE, savedState.SceneState); + _localStorageService.SetValue(SliceNames.RIG_SLICE, savedState.RigState); + _localStorageService.SetValue(SliceNames.ATLAS_SETTINGS_SLICE, savedState.AtlasSettingsState); + + // Note: The scene will be reloaded after this method returns to apply the loaded state. + // The store will be re-initialized from the updated local storage on scene reload. + + return true; + } } } diff --git a/Assets/Scripts/UI/PinpointAppBuilder.cs b/Assets/Scripts/UI/PinpointAppBuilder.cs index e4e1dc08..e199bd8e 100644 --- a/Assets/Scripts/UI/PinpointAppBuilder.cs +++ b/Assets/Scripts/UI/PinpointAppBuilder.cs @@ -29,6 +29,7 @@ protected override void OnConfiguringApp(AppBuilder builder) // Services. builder.services.AddSingleton(); + builder.services.AddSingleton(); builder.services.AddSingleton(); builder.services.AddSingleton(); builder.services.AddSingleton(); diff --git a/Assets/Scripts/UI/ViewModels/MainViewModel.cs b/Assets/Scripts/UI/ViewModels/MainViewModel.cs index 11a413e6..78313943 100644 --- a/Assets/Scripts/UI/ViewModels/MainViewModel.cs +++ b/Assets/Scripts/UI/ViewModels/MainViewModel.cs @@ -2,10 +2,12 @@ using Models; using Models.Scene; using Services; +using SimpleFileBrowser; using Unity.AppUI.MVVM; using Unity.AppUI.Redux; using Unity.AppUI.UI; using UnityEngine; +using UnityEngine.SceneManagement; using Utils.Types; namespace UI.ViewModels @@ -125,6 +127,68 @@ private void SetLeftSidePanelTabIndex(int index) _storeService.Store.Dispatch(MainActions.SET_LEFT_SIDE_PANEL_TAB_INDEX, index); } + [ICommand] + private void SaveStateToFile() + { + FileBrowser.ShowSaveDialog( + onSuccess: (paths) => + { + if (paths.Length > 0) + { + var filePath = paths[0]; + if (_storeService.SaveToFile(filePath)) + { + Debug.Log($"Scene state saved successfully to: {filePath}"); + } + else + { + Debug.LogError("Failed to save scene state"); + } + } + }, + onCancel: () => { }, + pickMode: FileBrowser.PickMode.Files, + allowMultiSelection: false, + initialPath: null, + initialFilename: "scene_state.json", + title: "Save Scene State", + saveButtonText: "Save" + ); + } + + [ICommand] + private void LoadStateFromFile() + { + FileBrowser.ShowLoadDialog( + onSuccess: (paths) => + { + if (paths.Length > 0) + { + var filePath = paths[0]; + if (_storeService.LoadFromFile(filePath)) + { + Debug.Log($"Scene state loaded successfully from: {filePath}"); + Debug.Log("Reloading scene to apply loaded state..."); + + // Reload the scene to apply the loaded state from local storage + SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex); + } + else + { + Debug.LogError("Failed to load scene state"); + } + } + }, + onCancel: () => { }, + pickMode: FileBrowser.PickMode.Files, + allowMultiSelection: false, + initialPath: null, + initialFilename: null, + title: "Load Scene State", + loadButtonText: "Load" + ); + } + #endregion } } diff --git a/Assets/Scripts/UI/Views/MainView.cs b/Assets/Scripts/UI/Views/MainView.cs index 11d908f1..bab709bf 100644 --- a/Assets/Scripts/UI/Views/MainView.cs +++ b/Assets/Scripts/UI/Views/MainView.cs @@ -45,6 +45,8 @@ public MainView(MainViewModel mainViewModel) var leftSidePanelCollapseButton = root.Q