diff --git a/VRCOSC.App/AppManager.cs b/VRCOSC.App/AppManager.cs index 60a1fab4..bab6d54b 100644 --- a/VRCOSC.App/AppManager.cs +++ b/VRCOSC.App/AppManager.cs @@ -14,6 +14,7 @@ using System.Windows.Media; using org.mariuszgromada.math.mxparser; using Valve.VR; +using VRCOSC.App.Actions.Files; using VRCOSC.App.Audio; using VRCOSC.App.Audio.Whisper; using VRCOSC.App.ChatBox; @@ -29,7 +30,9 @@ using VRCOSC.App.SDK.Parameters; using VRCOSC.App.SDK.VRChat; using VRCOSC.App.Settings; +using VRCOSC.App.Startup; using VRCOSC.App.UI.Themes; +using VRCOSC.App.UI.Windows; using VRCOSC.App.Updater; using VRCOSC.App.Utils; using VRCOSC.App.VRChatAPI; @@ -404,6 +407,7 @@ private async Task startAsync() { State.Value = AppManagerState.Starting; + StartupManager.GetInstance().OpenFileLocations(); RouterManager.GetInstance().Start(); VRChatOscClient.EnableSend(); ChatBoxManager.GetInstance().Start(); @@ -411,21 +415,42 @@ private async Task startAsync() await ModuleManager.GetInstance().StartAsync(); VRChatLogReader.Start(); + if (ModuleManager.GetInstance().GetRunningModulesOfType().Any()) + { + if (string.IsNullOrWhiteSpace(SettingsManager.GetInstance().GetValue(VRCOSCSetting.SpeechModelPath))) + { + var result = MessageBox.Show("You have enabled modules that require the speech engine.\nWould you like to automatically set it up?", "Set Up Speech Engine?", MessageBoxButton.YesNo); + + if (result == MessageBoxResult.Yes) + { + await installSpeechModel(); + } + } + + SpeechEngine.Initialise(); + } + updateTask = new Repeater(update); updateTask.Start(TimeSpan.FromSeconds(1d / 60d)); VRChatOscClient.OnParameterReceived += onParameterReceived; VRChatOscClient.EnableReceive(); - if (ModuleManager.GetInstance().GetRunningModulesOfType().Any()) - SpeechEngine.Initialise(); - State.Value = AppManagerState.Started; sendMetadataParameters(); sendControlParameters(); } + private Task installSpeechModel() + { + var action = new FileDownloadAction(new Uri("https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-tiny.bin?download=true"), Storage.GetStorageForDirectory("runtime/whisper"), "ggml-tiny.bin"); + + action.OnComplete += () => { SettingsManager.GetInstance().GetObservable(VRCOSCSetting.SpeechModelPath).Value = Storage.GetStorageForDirectory("runtime/whisper").GetFullPath("ggml-tiny.bin"); }; + + return MainWindow.GetInstance().ShowLoadingOverlay("Installing Model", action); + } + private void initialiseOSCClient(IPAddress sendAddress, int sendPort, IPAddress receiveAddress, int receivePort) { try diff --git a/VRCOSC.App/Audio/AudioCapture.cs b/VRCOSC.App/Audio/AudioCapture.cs index f3d1c761..e130fab7 100644 --- a/VRCOSC.App/Audio/AudioCapture.cs +++ b/VRCOSC.App/Audio/AudioCapture.cs @@ -5,6 +5,7 @@ using System.IO; using NAudio.CoreAudioApi; using NAudio.Wave; +using VRCOSC.App.Utils; namespace VRCOSC.App.Audio; @@ -52,26 +53,34 @@ public float[] GetBufferedData() { lock (lockObject) { - var targetWaveFormat = WaveFormat.CreateIeeeFloatWaveFormat(16000, 1); - var inputFormat = capture.WaveFormat; - - var bufferArray = buffer.ToArray(); - var bytesRecorded = bufferArray.Length; - - using var memoryStream = new MemoryStream(bufferArray, 0, bytesRecorded); - using var waveStream = new RawSourceWaveStream(memoryStream, inputFormat); - using var resampler = new MediaFoundationResampler(waveStream, targetWaveFormat); - resampler.ResamplerQuality = 60; - - var maxBytesNeeded = (int)(buffer.Length * (targetWaveFormat.SampleRate / (float)inputFormat.SampleRate) * (targetWaveFormat.BitsPerSample / (float)inputFormat.BitsPerSample) * (targetWaveFormat.Channels / (float)inputFormat.Channels)); - var resampledBuffer = new byte[maxBytesNeeded]; - var bytesRead = resampler.Read(resampledBuffer, 0, maxBytesNeeded); - - Array.Resize(ref resampledBuffer, bytesRead); - - var floatArray = new float[resampledBuffer.Length / sizeof(float)]; - Buffer.BlockCopy(resampledBuffer, 0, floatArray, 0, resampledBuffer.Length); - return floatArray; + try + { + var targetFormat = WaveFormat.CreateIeeeFloatWaveFormat(16000, 1); + var inputFormat = capture.WaveFormat; + + var bufferArray = buffer.ToArray(); + var bytesRecorded = bufferArray.Length; + + using var memoryStream = new MemoryStream(bufferArray, 0, bytesRecorded); + using var waveStream = new RawSourceWaveStream(memoryStream, inputFormat); + using var resampler = new MediaFoundationResampler(waveStream, targetFormat); + resampler.ResamplerQuality = 60; + + var maxBytesNeeded = (int)(buffer.Length * (targetFormat.SampleRate / (float)inputFormat.SampleRate) * (targetFormat.BitsPerSample / (float)inputFormat.BitsPerSample) * (targetFormat.Channels / (float)inputFormat.Channels)); + var resampledBuffer = new byte[maxBytesNeeded]; + var bytesRead = resampler.Read(resampledBuffer, 0, maxBytesNeeded); + + Array.Resize(ref resampledBuffer, bytesRead); + + var floatArray = new float[resampledBuffer.Length / sizeof(float)]; + Buffer.BlockCopy(resampledBuffer, 0, floatArray, 0, resampledBuffer.Length); + return floatArray; + } + catch (Exception e) + { + Logger.Error(e, "The selected microphone has provided bad data"); + return Array.Empty(); + } } } @@ -103,4 +112,4 @@ public void SaveConvertedToFile(float[] data, string filePath) using var waveFileWriter = new WaveFileWriter(filePath, waveFormat); waveFileWriter.Write(byteArray, 0, byteArray.Length); } -} +} \ No newline at end of file diff --git a/VRCOSC.App/Audio/Whisper/AudioProcessor.cs b/VRCOSC.App/Audio/Whisper/AudioProcessor.cs index a22117d9..e3a21f9f 100644 --- a/VRCOSC.App/Audio/Whisper/AudioProcessor.cs +++ b/VRCOSC.App/Audio/Whisper/AudioProcessor.cs @@ -41,7 +41,7 @@ public AudioProcessor(MMDevice device) catch (Exception e) { whisper = null; - ExceptionHandler.Handle(e, "Please make sure the model path for Whisper is correct in the speech settings"); + ExceptionHandler.Handle(e, "The Whisper model path is empty or incorrect. Please go into the app's speech settings and restore the model by clicking 'Auto Install Model'"); } try @@ -50,6 +50,7 @@ public AudioProcessor(MMDevice device) } catch (Exception e) { + audioCapture = null; ExceptionHandler.Handle(e); } } @@ -59,8 +60,10 @@ public void Start() speechResult = null; isProcessing = false; - audioCapture?.ClearBuffer(); - audioCapture?.StartCapture(); + if (whisper is null || audioCapture is null) return; + + audioCapture.ClearBuffer(); + audioCapture.StartCapture(); } public void Stop() diff --git a/VRCOSC.App/ChatBox/Clips/Variables/Instances/DateTimeClipVariable.cs b/VRCOSC.App/ChatBox/Clips/Variables/Instances/DateTimeClipVariable.cs index cd74171c..5e7caa78 100644 --- a/VRCOSC.App/ChatBox/Clips/Variables/Instances/DateTimeClipVariable.cs +++ b/VRCOSC.App/ChatBox/Clips/Variables/Instances/DateTimeClipVariable.cs @@ -20,10 +20,10 @@ public DateTimeClipVariable(ClipVariableReference reference) [ClipVariableOption("datetime_format", "Date/Time Format", "How should the date/time be formatted?")] public string DateTimeFormat { get; set; } = "yyyy/MM/dd HH:mm:ss"; - [ClipVariableOption("timezone_id", "Time Zone ID", "What timezone should this date/time be converted to?\nNote: Daylight savings is handled automatically")] - public string TimeZoneID { get; set; } = TimeZoneInfo.Local.Id; + [ClipVariableOption("timezone_id", "Time Zone ID", "What timezone should this date/time be converted to?\nLeave empty for your local timezone\nNote: Daylight savings is handled automatically")] + public string TimeZoneID { get; set; } = string.Empty; - public override bool IsDefault() => base.IsDefault() && DateTimeFormat == "yyyy/MM/dd HH:mm:ss" && TimeZoneID == TimeZoneInfo.Local.Id; + public override bool IsDefault() => base.IsDefault() && DateTimeFormat == "yyyy/MM/dd HH:mm:ss" && TimeZoneID == string.Empty; public override DateTimeClipVariable Clone() { @@ -41,12 +41,21 @@ protected override string Format(object value) try { - var timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById(TimeZoneID); - var convertedDateTime = TimeZoneInfo.ConvertTimeFromUtc(dateTimeValue.UtcDateTime, timeZoneInfo); + DateTime convertedDateTime; + + if (string.IsNullOrEmpty(TimeZoneID)) + { + convertedDateTime = TimeZoneInfo.ConvertTimeFromUtc(dateTimeValue.UtcDateTime, TimeZoneInfo.Local); + } + else + { + var timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById(TimeZoneID); + convertedDateTime = TimeZoneInfo.ConvertTimeFromUtc(dateTimeValue.UtcDateTime, timeZoneInfo); + } try { - return convertedDateTime.ToString(DateTimeFormat, CultureInfo.CurrentCulture); + return convertedDateTime.ToString(DateTimeFormat, CultureInfo.InvariantCulture); } catch (Exception) { diff --git a/VRCOSC.App/Router/RouterManager.cs b/VRCOSC.App/Router/RouterManager.cs index 50b07c28..0662c201 100644 --- a/VRCOSC.App/Router/RouterManager.cs +++ b/VRCOSC.App/Router/RouterManager.cs @@ -26,7 +26,7 @@ public class RouterManager private bool started; - public RouterManager() + private RouterManager() { serialisationManager = new SerialisationManager(); serialisationManager.RegisterSerialiser(1, new RouterManagerSerialiser(AppManager.GetInstance().Storage, this)); @@ -37,21 +37,18 @@ public void Load() started = false; Routes.Clear(); - Routes.CollectionChanged += (_, e) => + serialisationManager.Deserialise(); + + Routes.OnCollectionChanged((newItems, _) => { - if (e.NewItems is not null) + foreach (var newInstance in newItems) { - foreach (RouterInstance routerInstance in e.NewItems) - { - routerInstance.Name.Subscribe(_ => serialisationManager.Serialise()); - routerInstance.Endpoint.Subscribe(_ => serialisationManager.Serialise()); - } + newInstance.Name.Subscribe(_ => serialisationManager.Serialise()); + newInstance.Endpoint.Subscribe(_ => serialisationManager.Serialise()); } + }, true); - serialisationManager.Serialise(); - }; - - serialisationManager.Deserialise(); + Routes.OnCollectionChanged((_, _) => serialisationManager.Serialise()); } public void Start() @@ -108,4 +105,4 @@ private void onParameterReceived(VRChatOscMessage message) sender.Send(OscEncoder.Encode(message)); } } -} +} \ No newline at end of file diff --git a/VRCOSC.App/Router/Serialisation/RouterManagerSerialiser.cs b/VRCOSC.App/Router/Serialisation/RouterManagerSerialiser.cs index 228fbf36..b636a6c9 100644 --- a/VRCOSC.App/Router/Serialisation/RouterManagerSerialiser.cs +++ b/VRCOSC.App/Router/Serialisation/RouterManagerSerialiser.cs @@ -8,6 +8,7 @@ namespace VRCOSC.App.Router.Serialisation; public class RouterManagerSerialiser : ProfiledSerialiser { + protected override string Directory => "configuration"; protected override string FileName => "router.json"; public RouterManagerSerialiser(Storage storage, RouterManager reference) @@ -21,4 +22,4 @@ protected override bool ExecuteAfterDeserialisation(SerialisableRouterManager da return false; } -} +} \ No newline at end of file diff --git a/VRCOSC.App/SDK/Modules/Heartrate/HeartrateModule.cs b/VRCOSC.App/SDK/Modules/Heartrate/HeartrateModule.cs index 76b7628e..e9b5271f 100644 --- a/VRCOSC.App/SDK/Modules/Heartrate/HeartrateModule.cs +++ b/VRCOSC.App/SDK/Modules/Heartrate/HeartrateModule.cs @@ -11,8 +11,6 @@ namespace VRCOSC.App.SDK.Modules.Heartrate; -[ModuleType(ModuleType.Health)] -[ModulePrefab("VRCOSC-Heartrate", "/~https://github.com/VolcanicArts/VRCOSC/releases/download/2024.220.1/VRCOSC-Heartrate-2023.629.0.unitypackage")] public abstract class HeartrateModule : Module where T : HeartrateProvider { protected T? HeartrateProvider; @@ -332,4 +330,4 @@ private enum HeartrateVariable Current, Average } -} +} \ No newline at end of file diff --git a/VRCOSC.App/SDK/Modules/Module.cs b/VRCOSC.App/SDK/Modules/Module.cs index cf13b345..b4347a31 100644 --- a/VRCOSC.App/SDK/Modules/Module.cs +++ b/VRCOSC.App/SDK/Modules/Module.cs @@ -925,16 +925,7 @@ internal void OnParameterReceived(VRChatOscMessage message) ExceptionHandler.Handle(e, $"Module {FullID} experienced an exception calling {nameof(OnAnyParameterReceived)}"); } - string? parameterName = null; - - foreach (var parameter in Parameters.Values) - { - var match = parameterNameRegex[parameter.Name.Value].Match(receivedParameter.Name); - if (!match.Success) continue; - - parameterName = match.Groups[1].Captures[0].Value; - } - + var parameterName = Parameters.Values.FirstOrDefault(parameter => parameterNameRegex[parameter.Name.Value].Match(receivedParameter.Name).Success)?.Name.Value; if (parameterName is null) return; if (!parameterNameEnum.TryGetValue(parameterName, out var lookup)) return; diff --git a/VRCOSC.App/Startup/Serialisation/SerialisableStartupManager.cs b/VRCOSC.App/Startup/Serialisation/SerialisableStartupManager.cs new file mode 100644 index 00000000..c2f87051 --- /dev/null +++ b/VRCOSC.App/Startup/Serialisation/SerialisableStartupManager.cs @@ -0,0 +1,49 @@ +// Copyright (c) VolcanicArts. Licensed under the GPL-3.0 License. +// See the LICENSE file in the repository root for full license text. + +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; +using VRCOSC.App.Serialisation; + +namespace VRCOSC.App.Startup.Serialisation; + +[JsonObject] +public class SerialisableStartupManager : SerialisableVersion +{ + [JsonProperty("instances")] + public List Instances { get; set; } = []; + + [JsonConstructor] + public SerialisableStartupManager() + { + } + + public SerialisableStartupManager(StartupManager startupManager) + { + Version = 1; + + Instances = startupManager.Instances.Select(startupInstance => new SerialisableStartupInstance(startupInstance)).ToList(); + } +} + +[JsonObject] +public class SerialisableStartupInstance +{ + [JsonProperty("file_location")] + public string FileLocation { get; set; } = string.Empty; + + [JsonProperty("arguments")] + public string Arguments { get; set; } = string.Empty; + + [JsonConstructor] + public SerialisableStartupInstance() + { + } + + public SerialisableStartupInstance(StartupInstance startupInstance) + { + FileLocation = startupInstance.FileLocation.Value; + Arguments = startupInstance.Arguments.Value; + } +} \ No newline at end of file diff --git a/VRCOSC.App/Startup/Serialisation/StartupManagerSerialiser.cs b/VRCOSC.App/Startup/Serialisation/StartupManagerSerialiser.cs new file mode 100644 index 00000000..f19029e8 --- /dev/null +++ b/VRCOSC.App/Startup/Serialisation/StartupManagerSerialiser.cs @@ -0,0 +1,32 @@ +// Copyright (c) VolcanicArts. Licensed under the GPL-3.0 License. +// See the LICENSE file in the repository root for full license text. + +using VRCOSC.App.Serialisation; +using VRCOSC.App.Utils; + +namespace VRCOSC.App.Startup.Serialisation; + +public class StartupManagerSerialiser : Serialiser +{ + protected override string Directory => "configuration"; + protected override string FileName => "startup.json"; + + public StartupManagerSerialiser(Storage storage, StartupManager reference) + : base(storage, reference) + { + } + + protected override bool ExecuteAfterDeserialisation(SerialisableStartupManager data) + { + foreach (var serialisableStartupInstance in data.Instances) + { + Reference.Instances.Add(new StartupInstance + { + FileLocation = { Value = serialisableStartupInstance.FileLocation }, + Arguments = { Value = serialisableStartupInstance.Arguments } + }); + } + + return false; + } +} \ No newline at end of file diff --git a/VRCOSC.App/Startup/StartupManager.cs b/VRCOSC.App/Startup/StartupManager.cs new file mode 100644 index 00000000..a8b9fffa --- /dev/null +++ b/VRCOSC.App/Startup/StartupManager.cs @@ -0,0 +1,84 @@ +// Copyright (c) VolcanicArts. Licensed under the GPL-3.0 License. +// See the LICENSE file in the repository root for full license text. + +using System; +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.IO; +using VRCOSC.App.Serialisation; +using VRCOSC.App.Startup.Serialisation; +using VRCOSC.App.Utils; + +namespace VRCOSC.App.Startup; + +public class StartupManager +{ + private static StartupManager? instance; + public static StartupManager GetInstance() => instance ??= new StartupManager(); + + public ObservableCollection Instances { get; } = []; + + private readonly SerialisationManager serialisationManager; + + private StartupManager() + { + serialisationManager = new SerialisationManager(); + serialisationManager.RegisterSerialiser(1, new StartupManagerSerialiser(AppManager.GetInstance().Storage, this)); + } + + public void Load() + { + serialisationManager.Deserialise(); + + Instances.OnCollectionChanged((newItems, _) => + { + foreach (var newInstance in newItems) + { + newInstance.FileLocation.Subscribe(_ => serialisationManager.Serialise()); + newInstance.Arguments.Subscribe(_ => serialisationManager.Serialise()); + } + }, true); + + Instances.OnCollectionChanged((_, _) => serialisationManager.Serialise()); + } + + public void OpenFileLocations() + { + foreach (var startupInstance in Instances) + { + var fileLocation = startupInstance.FileLocation.Value; + var arguments = startupInstance.Arguments.Value; + + try + { + if (!File.Exists(fileLocation)) + { + ExceptionHandler.Handle($"File location '{fileLocation}' does not exist when attempting to startup"); + continue; + } + + if (fileLocation.EndsWith(".exe")) + { + var processName = new FileInfo(fileLocation).Name.ToLowerInvariant().Replace(".exe", string.Empty); + if (Process.GetProcessesByName(processName).Length > 0) continue; + } + + Process.Start(new ProcessStartInfo(fileLocation, arguments) + { + WorkingDirectory = Path.GetDirectoryName(fileLocation), + UseShellExecute = true + }); + } + catch (Exception e) + { + ExceptionHandler.Handle(e, $"Failed to start '{fileLocation}'"); + } + } + } +} + +public class StartupInstance +{ + public Observable FileLocation { get; } = new(string.Empty); + public Observable Arguments { get; } = new(string.Empty); +} \ No newline at end of file diff --git a/VRCOSC.App/UI/Views/AppSettings/AppSettingsView.xaml.cs b/VRCOSC.App/UI/Views/AppSettings/AppSettingsView.xaml.cs index 98e30b30..c4e15f05 100644 --- a/VRCOSC.App/UI/Views/AppSettings/AppSettingsView.xaml.cs +++ b/VRCOSC.App/UI/Views/AppSettings/AppSettingsView.xaml.cs @@ -174,6 +174,7 @@ public AppSettingsView() SettingsManager.GetInstance().GetObservable(VRCOSCSetting.SelectedMicrophoneID).Subscribe(updateDeviceListAndSelection, true); SettingsManager.GetInstance().GetObservable(VRCOSCSetting.UseCustomEndpoints).Subscribe(value => UsingCustomEndpoints.Value = value ? Visibility.Visible : Visibility.Collapsed, true); + SettingsManager.GetInstance().GetObservable(VRCOSCSetting.SpeechModelPath).Subscribe(_ => OnPropertyChanged(nameof(WhisperModelFilePath))); setPage(0); } diff --git a/VRCOSC.App/UI/Views/Modules/ModulesView.xaml.cs b/VRCOSC.App/UI/Views/Modules/ModulesView.xaml.cs index 6e235808..bd42af32 100644 --- a/VRCOSC.App/UI/Views/Modules/ModulesView.xaml.cs +++ b/VRCOSC.App/UI/Views/Modules/ModulesView.xaml.cs @@ -13,7 +13,9 @@ namespace VRCOSC.App.UI.Views.Modules; public partial class ModulesView { - private WindowManager windowManager; + private WindowManager settingsWindowManager = null!; + private WindowManager parametersWindowManager = null!; + private WindowManager prefabsWindowManager = null!; public ModulesView() { @@ -24,7 +26,9 @@ public ModulesView() private void ModulesView_OnLoaded(object sender, RoutedEventArgs e) { - windowManager = new WindowManager(this); + settingsWindowManager = new WindowManager(this); + parametersWindowManager = new WindowManager(this); + prefabsWindowManager = new WindowManager(this); } private void ImportButton_OnClick(object sender, RoutedEventArgs e) @@ -49,7 +53,7 @@ private void ParametersButton_OnClick(object sender, RoutedEventArgs e) var element = (FrameworkElement)sender; var module = (Module)element.Tag; - windowManager.TrySpawnChild(new ModuleParametersWindow(module)); + parametersWindowManager.TrySpawnChild(new ModuleParametersWindow(module)); } private void SettingsButton_OnClick(object sender, RoutedEventArgs e) @@ -57,7 +61,7 @@ private void SettingsButton_OnClick(object sender, RoutedEventArgs e) var element = (FrameworkElement)sender; var module = (Module)element.Tag; - windowManager.TrySpawnChild(new ModuleSettingsWindow(module)); + settingsWindowManager.TrySpawnChild(new ModuleSettingsWindow(module)); } private void InfoButton_OnClick(object sender, RoutedEventArgs e) @@ -73,6 +77,6 @@ private void PrefabButton_OnClick(object sender, RoutedEventArgs e) var element = (FrameworkElement)sender; var module = (Module)element.Tag; - windowManager.TrySpawnChild(new ModulePrefabsWindow(module)); + prefabsWindowManager.TrySpawnChild(new ModulePrefabsWindow(module)); } } \ No newline at end of file diff --git a/VRCOSC.App/UI/Views/Router/RouterView.xaml b/VRCOSC.App/UI/Views/Router/RouterView.xaml index d295bc5c..a98eaf1e 100644 --- a/VRCOSC.App/UI/Views/Router/RouterView.xaml +++ b/VRCOSC.App/UI/Views/Router/RouterView.xaml @@ -19,22 +19,22 @@ + Height="40" Padding="7"> + FontWeight="SemiBold" /> + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + - - + Click="RemoveInstance_OnClick" + Icon="Solid_Minus"> + @@ -142,15 +94,13 @@ - - - - + + - + \ No newline at end of file diff --git a/VRCOSC.App/UI/Views/Startup/StartupView.xaml b/VRCOSC.App/UI/Views/Startup/StartupView.xaml new file mode 100644 index 00000000..8c1f05e9 --- /dev/null +++ b/VRCOSC.App/UI/Views/Startup/StartupView.xaml @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/VRCOSC.App/UI/Views/Startup/StartupView.xaml.cs b/VRCOSC.App/UI/Views/Startup/StartupView.xaml.cs new file mode 100644 index 00000000..06e5924d --- /dev/null +++ b/VRCOSC.App/UI/Views/Startup/StartupView.xaml.cs @@ -0,0 +1,31 @@ +// Copyright (c) VolcanicArts. Licensed under the GPL-3.0 License. +// See the LICENSE file in the repository root for full license text. + +using System.Windows; +using VRCOSC.App.Startup; +using VRCOSC.App.UI.Core; + +namespace VRCOSC.App.UI.Views.Startup; + +public partial class StartupView +{ + public StartupView() + { + InitializeComponent(); + + DataContext = StartupManager.GetInstance(); + } + + private void AddInstance_OnClick(object sender, RoutedEventArgs e) + { + StartupManager.GetInstance().Instances.Add(new StartupInstance()); + } + + private void RemoveInstance_OnClick(object sender, RoutedEventArgs e) + { + var element = (IconButton)sender; + var instance = (StartupInstance)element.Tag; + + StartupManager.GetInstance().Instances.Remove(instance); + } +} \ No newline at end of file diff --git a/VRCOSC.App/UI/Windows/ChatBox/ChatBoxClipEditWindow.xaml b/VRCOSC.App/UI/Windows/ChatBox/ChatBoxClipEditWindow.xaml index 4dabdd8a..261a844b 100644 --- a/VRCOSC.App/UI/Windows/ChatBox/ChatBoxClipEditWindow.xaml +++ b/VRCOSC.App/UI/Windows/ChatBox/ChatBoxClipEditWindow.xaml @@ -184,7 +184,7 @@ HorizontalAlignment="Left" VerticalAlignment="Center" Text="{Binding DisplayNameWithModule}" - Margin="0 -2 0 0" /> + Margin="0 -3 0 0" /> + Margin="0 -3 0 0" /> - - - - - + + + + + + + + diff --git a/VRCOSC.App/UI/Windows/MainWindow.xaml.cs b/VRCOSC.App/UI/Windows/MainWindow.xaml.cs index ab431eaf..c19bbb43 100644 --- a/VRCOSC.App/UI/Windows/MainWindow.xaml.cs +++ b/VRCOSC.App/UI/Windows/MainWindow.xaml.cs @@ -22,6 +22,7 @@ using VRCOSC.App.Router; using VRCOSC.App.SDK.OVR.Metadata; using VRCOSC.App.Settings; +using VRCOSC.App.Startup; using VRCOSC.App.UI.Core; using VRCOSC.App.UI.Views.AppDebug; using VRCOSC.App.UI.Views.AppSettings; @@ -33,6 +34,7 @@ using VRCOSC.App.UI.Views.Router; using VRCOSC.App.UI.Views.Run; using VRCOSC.App.UI.Views.Settings; +using VRCOSC.App.UI.Views.Startup; using VRCOSC.App.Updater; using VRCOSC.App.Utils; using Application = System.Windows.Application; @@ -53,6 +55,7 @@ public partial class MainWindow public readonly RouterView RouterView; public readonly SettingsView SettingsView; public readonly ChatBoxView ChatBoxView; + public readonly StartupView StartupView; public readonly RunView RunView; public readonly AppDebugView AppDebugView; public readonly ProfilesView ProfilesView; @@ -88,6 +91,7 @@ public MainWindow() RouterView = new RouterView(); SettingsView = new SettingsView(); ChatBoxView = new ChatBoxView(); + StartupView = new StartupView(); RunView = new RunView(); AppDebugView = new AppDebugView(); ProfilesView = new ProfilesView(); @@ -101,25 +105,32 @@ public MainWindow() private void backupV1Files() { - if (!storage.Exists("framework.ini")) return; + try + { + if (!storage.Exists("framework.ini")) return; - var sourceDir = storage.GetFullPath(string.Empty); - var destinationDir = storage.GetStorageForDirectory("v1-backup").GetFullPath(string.Empty); + var sourceDir = storage.GetFullPath(string.Empty); + var destinationDir = storage.GetStorageForDirectory("v1-backup").GetFullPath(string.Empty); - foreach (var file in Directory.GetFiles(sourceDir)) - { - var fileName = Path.GetFileName(file); - var destFile = Path.Combine(destinationDir, fileName); - File.Move(file, destFile); - } + foreach (var file in Directory.GetFiles(sourceDir)) + { + var fileName = Path.GetFileName(file); + var destFile = Path.Combine(destinationDir, fileName); + File.Move(file, destFile, false); + } - foreach (var dir in Directory.GetDirectories(sourceDir)) - { - var dirName = Path.GetFileName(dir); - if (dirName == "v1-backup") continue; + foreach (var dir in Directory.GetDirectories(sourceDir)) + { + var dirName = Path.GetFileName(dir); + if (dirName == "v1-backup") continue; - var destDir = Path.Combine(destinationDir, dirName); - Directory.Move(dir, destDir); + var destDir = Path.Combine(destinationDir, dirName); + Directory.Move(dir, destDir); + } + } + catch (Exception e) + { + Logger.Error(e, "Could not backup V1 files"); } } @@ -155,6 +166,7 @@ private void load() loadingAction.AddAction(new DynamicProgressAction("Loading Modules", () => ModuleManager.GetInstance().LoadAllModules())); loadingAction.AddAction(new DynamicProgressAction("Loading ChatBox", () => ChatBoxManager.GetInstance().Load())); loadingAction.AddAction(new DynamicProgressAction("Loading Router", () => RouterManager.GetInstance().Load())); + loadingAction.AddAction(new DynamicProgressAction("Loading Startup", () => StartupManager.GetInstance().Load())); if (!SettingsManager.GetInstance().GetValue(VRCOSCMetadata.FirstTimeSetupComplete)) { @@ -406,6 +418,11 @@ private void ChatBoxButton_OnClick(object sender, RoutedEventArgs e) setContent(ChatBoxView); } + private void StartupButton_OnClick(object sender, RoutedEventArgs e) + { + setContent(StartupView); + } + private void RunButton_OnClick(object sender, RoutedEventArgs e) { setContent(RunView); diff --git a/VRCOSC.App/VRCOSC.App.csproj b/VRCOSC.App/VRCOSC.App.csproj index 4871c43e..e690bd78 100644 --- a/VRCOSC.App/VRCOSC.App.csproj +++ b/VRCOSC.App/VRCOSC.App.csproj @@ -37,9 +37,9 @@ - - - + + +