diff --git a/.github/workflows/docfx.yml b/.github/workflows/docfx.yml index a26bf9032..bd3301f37 100644 --- a/.github/workflows/docfx.yml +++ b/.github/workflows/docfx.yml @@ -33,4 +33,4 @@ jobs: username: ${{ secrets.FTP_USER }} password: ${{ secrets.FTP_PASSWORD }} local-dir: docfx/docfx_project/_site/ - server-dir: /httpdocs/docs/ + server-dir: /docs/ diff --git a/src/Artemis.UI.Shared/Providers/IProtocolProvider.cs b/src/Artemis.UI.Shared/Providers/IProtocolProvider.cs new file mode 100644 index 000000000..145f0aa91 --- /dev/null +++ b/src/Artemis.UI.Shared/Providers/IProtocolProvider.cs @@ -0,0 +1,21 @@ +using System.Threading.Tasks; + +namespace Artemis.UI.Shared.Providers; + +/// +/// Represents a provider associating with a custom protocol, e.g. artemis:// +/// +public interface IProtocolProvider +{ + /// + /// Associate Artemis with the provided custom protocol. + /// + /// The protocol to associate Artemis with. + Task AssociateWithProtocol(string protocol); + + /// + /// Disassociate Artemis with the provided custom protocol. + /// + /// The protocol to disassociate Artemis with. + Task DisassociateWithProtocol(string protocol); +} \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Routing/Router/Router.cs b/src/Artemis.UI.Shared/Routing/Router/Router.cs index 540cf25e8..a4afe117d 100644 --- a/src/Artemis.UI.Shared/Routing/Router/Router.cs +++ b/src/Artemis.UI.Shared/Routing/Router/Router.cs @@ -72,6 +72,7 @@ internal class Router : CorePropertyChanged, IRouter, IDisposable /// public async Task Navigate(string path, RouterNavigationOptions? options = null) { + path = path.ToLower().Trim(' ', '/', '\\'); options ??= new RouterNavigationOptions(); // Routing takes place on the UI thread with processing heavy tasks offloaded by the router itself diff --git a/src/Artemis.UI.Windows/App.axaml.cs b/src/Artemis.UI.Windows/App.axaml.cs index 9854a1ca5..41ed8e923 100644 --- a/src/Artemis.UI.Windows/App.axaml.cs +++ b/src/Artemis.UI.Windows/App.axaml.cs @@ -74,6 +74,9 @@ public class App : Application return false; } + + string? route = (ApplicationLifetime as IClassicDesktopStyleApplicationLifetime)?.Args?.FirstOrDefault(a => a.Contains("route")); + route = route?.Split("artemis://")[1]; string url = File.ReadAllText(Path.Combine(Constants.DataFolder, "webserver.txt")); using HttpClient client = new(); try @@ -81,7 +84,7 @@ public class App : Application CancellationTokenSource cts = new(); cts.CancelAfter(2000); - HttpResponseMessage httpResponseMessage = client.Send(new HttpRequestMessage(HttpMethod.Post, url + "remote/bring-to-foreground"), cts.Token); + HttpResponseMessage httpResponseMessage = client.Send(new HttpRequestMessage(HttpMethod.Post, url + "remote/bring-to-foreground") {Content = new StringContent(route ?? "")}, cts.Token); httpResponseMessage.EnsureSuccessStatusCode(); return true; } diff --git a/src/Artemis.UI.Windows/DryIoc/ContainerExtensions.cs b/src/Artemis.UI.Windows/DryIoc/ContainerExtensions.cs index 337e21744..a2906d46c 100644 --- a/src/Artemis.UI.Windows/DryIoc/ContainerExtensions.cs +++ b/src/Artemis.UI.Windows/DryIoc/ContainerExtensions.cs @@ -24,5 +24,6 @@ public static class UIContainerExtensions container.Register(); container.Register(serviceKey: WindowsInputProvider.Id); container.Register(); + container.Register(); } } \ No newline at end of file diff --git a/src/Artemis.UI.Windows/Providers/ProtocolProvider.cs b/src/Artemis.UI.Windows/Providers/ProtocolProvider.cs new file mode 100644 index 000000000..c3ec1622f --- /dev/null +++ b/src/Artemis.UI.Windows/Providers/ProtocolProvider.cs @@ -0,0 +1,34 @@ +using System; +using System.Threading.Tasks; +using Artemis.Core; +using Artemis.UI.Shared.Providers; +using Microsoft.Win32; + +namespace Artemis.UI.Windows.Providers; + +public class ProtocolProvider : IProtocolProvider +{ + /// + public async Task AssociateWithProtocol(string protocol) + { + string key = $"HKEY_CURRENT_USER\\Software\\Classes\\{protocol}"; + Registry.SetValue($"{key}", null, "URL:artemis protocol"); + Registry.SetValue($"{key}", "URL Protocol", ""); + Registry.SetValue($"{key}\\DefaultIcon", null, $"\"{Constants.ExecutablePath}\",1"); + Registry.SetValue($"{key}\\shell\\open\\command", null, $"\"{Constants.ExecutablePath}\", \"--route=%1\""); + } + + /// + public async Task DisassociateWithProtocol(string protocol) + { + try + { + string key = $"HKEY_CURRENT_USER\\Software\\Classes\\{protocol}"; + Registry.CurrentUser.DeleteSubKeyTree(key); + } + catch (ArgumentException) + { + // Ignore errors (which means that the protocol wasn't associated before) + } + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Controllers/RemoteController.cs b/src/Artemis.UI/Controllers/RemoteController.cs index abf1e7c89..6ace7f441 100644 --- a/src/Artemis.UI/Controllers/RemoteController.cs +++ b/src/Artemis.UI/Controllers/RemoteController.cs @@ -1,6 +1,8 @@ using System; +using System.IO; using Artemis.Core; using Artemis.Core.Services; +using Artemis.UI.Shared.Routing; using Artemis.UI.Shared.Services.MainWindow; using Avalonia.Threading; using EmbedIO; @@ -13,11 +15,13 @@ public class RemoteController : WebApiController { private readonly ICoreService _coreService; private readonly IMainWindowService _mainWindowService; + private readonly IRouter _router; - public RemoteController(ICoreService coreService, IMainWindowService mainWindowService) + public RemoteController(ICoreService coreService, IMainWindowService mainWindowService, IRouter router) { _coreService = coreService; _mainWindowService = mainWindowService; + _router = router; } [Route(HttpVerbs.Any, "/status")] @@ -29,7 +33,15 @@ public class RemoteController : WebApiController [Route(HttpVerbs.Post, "/remote/bring-to-foreground")] public void PostBringToForeground() { - Dispatcher.UIThread.Post(() => _mainWindowService.OpenMainWindow()); + using StreamReader reader = new(Request.InputStream); + string route = reader.ReadToEnd(); + + Dispatcher.UIThread.InvokeAsync(async () => + { + if (!string.IsNullOrWhiteSpace(route)) + await _router.Navigate(route); + _mainWindowService.OpenMainWindow(); + }); } [Route(HttpVerbs.Post, "/remote/restart")] diff --git a/src/Artemis.UI/Screens/Root/RootViewModel.cs b/src/Artemis.UI/Screens/Root/RootViewModel.cs index 4f6580793..4763511a9 100644 --- a/src/Artemis.UI/Screens/Root/RootViewModel.cs +++ b/src/Artemis.UI/Screens/Root/RootViewModel.cs @@ -14,6 +14,7 @@ using Artemis.UI.Shared.Routing; using Artemis.UI.Shared.Services; using Artemis.UI.Shared.Services.MainWindow; using Avalonia; +using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Threading; using ReactiveUI; @@ -181,6 +182,7 @@ public class RootViewModel : RoutableHostScreen, IMainWindowProv } _lifeTime.MainWindow.Activate(); + _lifeTime.MainWindow.WindowState = WindowState.Normal; OnMainWindowOpened(); } diff --git a/src/Artemis.UI/Screens/Settings/Tabs/GeneralTabView.axaml b/src/Artemis.UI/Screens/Settings/Tabs/GeneralTabView.axaml index 378f37b64..861293d66 100644 --- a/src/Artemis.UI/Screens/Settings/Tabs/GeneralTabView.axaml +++ b/src/Artemis.UI/Screens/Settings/Tabs/GeneralTabView.axaml @@ -45,6 +45,19 @@ + + + Associate with Artemis links + + Open Artemis when navigating to artemis:// links, allows opening workshop entries from your browser. + + + + + + + + Enable Mica effect @@ -57,7 +70,7 @@ - + Startup delay diff --git a/src/Artemis.UI/Screens/Settings/Tabs/GeneralTabViewModel.cs b/src/Artemis.UI/Screens/Settings/Tabs/GeneralTabViewModel.cs index 74adce108..6e3ee0d0b 100644 --- a/src/Artemis.UI/Screens/Settings/Tabs/GeneralTabViewModel.cs +++ b/src/Artemis.UI/Screens/Settings/Tabs/GeneralTabViewModel.cs @@ -29,6 +29,7 @@ namespace Artemis.UI.Screens.Settings; public class GeneralTabViewModel : RoutableScreen { private readonly IAutoRunProvider? _autoRunProvider; + private readonly IProtocolProvider? _protocolProvider; private readonly IDebugService _debugService; private readonly PluginSetting _defaultLayerBrushDescriptor; private readonly INotificationService _notificationService; @@ -52,6 +53,7 @@ public class GeneralTabViewModel : RoutableScreen _updateService = updateService; _notificationService = notificationService; _autoRunProvider = container.Resolve(IfUnresolved.ReturnDefault); + _protocolProvider = container.Resolve(IfUnresolved.ReturnDefault); List layerBrushProviders = pluginManagementService.GetFeaturesOfType(); List graphicsContextProviders = container.Resolve>(); @@ -74,13 +76,16 @@ public class GeneralTabViewModel : RoutableScreen this.WhenActivated(d => { UIAutoRun.SettingChanged += UIAutoRunOnSettingChanged; + UIUseProtocol.SettingChanged += UIUseProtocolOnSettingChanged; UIAutoRunDelay.SettingChanged += UIAutoRunDelayOnSettingChanged; EnableMica.SettingChanged += EnableMicaOnSettingChanged; Dispatcher.UIThread.InvokeAsync(ApplyAutoRun); + Dispatcher.UIThread.Invoke(ApplyProtocolAssociation); Disposable.Create(() => { UIAutoRun.SettingChanged -= UIAutoRunOnSettingChanged; + UIUseProtocol.SettingChanged -= UIUseProtocolOnSettingChanged; UIAutoRunDelay.SettingChanged -= UIAutoRunDelayOnSettingChanged; EnableMica.SettingChanged -= EnableMicaOnSettingChanged; @@ -148,6 +153,7 @@ public class GeneralTabViewModel : RoutableScreen } public PluginSetting UIAutoRun => _settingsService.GetSetting("UI.AutoRun", false); + public PluginSetting UIUseProtocol => _settingsService.GetSetting("UI.UseProtocol", true); public PluginSetting UIAutoRunDelay => _settingsService.GetSetting("UI.AutoRunDelay", 15); public PluginSetting UIShowOnStartup => _settingsService.GetSetting("UI.ShowOnStartup", true); public PluginSetting EnableMica => _settingsService.GetSetting("UI.EnableMica", true); @@ -223,11 +229,34 @@ public class GeneralTabViewModel : RoutableScreen _windowService.ShowExceptionDialog("Failed to apply auto-run", exception); } } + + private void ApplyProtocolAssociation() + { + if (_protocolProvider == null) + return; + + try + { + if (UIUseProtocol.Value) + _protocolProvider.AssociateWithProtocol("artemis"); + else + _protocolProvider.DisassociateWithProtocol("artemis"); + } + catch (Exception exception) + { + _windowService.ShowExceptionDialog("Failed to apply protocol association", exception); + } + } private async void UIAutoRunOnSettingChanged(object? sender, EventArgs e) { await ApplyAutoRun(); } + + private void UIUseProtocolOnSettingChanged(object? sender, EventArgs e) + { + ApplyProtocolAssociation(); + } private async void UIAutoRunDelayOnSettingChanged(object? sender, EventArgs e) { diff --git a/src/Artemis.UI/Screens/StartupWizard/StartupWizardViewModel.cs b/src/Artemis.UI/Screens/StartupWizard/StartupWizardViewModel.cs index 05bbc060c..9661d7e23 100644 --- a/src/Artemis.UI/Screens/StartupWizard/StartupWizardViewModel.cs +++ b/src/Artemis.UI/Screens/StartupWizard/StartupWizardViewModel.cs @@ -20,6 +20,7 @@ namespace Artemis.UI.Screens.StartupWizard; public class StartupWizardViewModel : DialogViewModelBase { private readonly IAutoRunProvider? _autoRunProvider; + private readonly IProtocolProvider? _protocolProvider; private readonly IRgbService _rgbService; private readonly ISettingsService _settingsService; private readonly IWindowService _windowService; @@ -39,6 +40,7 @@ public class StartupWizardViewModel : DialogViewModelBase _rgbService = rgbService; _windowService = windowService; _autoRunProvider = container.Resolve(IfUnresolved.ReturnDefault); + _protocolProvider = container.Resolve(IfUnresolved.ReturnDefault); Continue = ReactiveCommand.Create(ExecuteContinue); GoBack = ReactiveCommand.Create(ExecuteGoBack); @@ -58,11 +60,13 @@ public class StartupWizardViewModel : DialogViewModelBase this.WhenActivated(d => { UIAutoRun.SettingChanged += UIAutoRunOnSettingChanged; + UIUseProtocol.SettingChanged += UIUseProtocolOnSettingChanged; UIAutoRunDelay.SettingChanged += UIAutoRunDelayOnSettingChanged; Disposable.Create(() => { UIAutoRun.SettingChanged -= UIAutoRunOnSettingChanged; + UIUseProtocol.SettingChanged -= UIUseProtocolOnSettingChanged; UIAutoRunDelay.SettingChanged -= UIAutoRunDelayOnSettingChanged; _settingsService.SaveAllSettings(); @@ -81,6 +85,7 @@ public class StartupWizardViewModel : DialogViewModelBase public bool IsAutoRunSupported => _autoRunProvider != null; public PluginSetting UIAutoRun => _settingsService.GetSetting("UI.AutoRun", false); + public PluginSetting UIUseProtocol => _settingsService.GetSetting("UI.UseProtocol", true); public PluginSetting UIAutoRunDelay => _settingsService.GetSetting("UI.AutoRunDelay", 15); public PluginSetting UIShowOnStartup => _settingsService.GetSetting("UI.ShowOnStartup", true); public PluginSetting UICheckForUpdates => _settingsService.GetSetting("UI.Updating.AutoCheck", true); @@ -177,11 +182,34 @@ public class StartupWizardViewModel : DialogViewModelBase } } + private void ApplyProtocolAssociation() + { + if (_protocolProvider == null) + return; + + try + { + if (UIUseProtocol.Value) + _protocolProvider.AssociateWithProtocol("artemis"); + else + _protocolProvider.DisassociateWithProtocol("artemis"); + } + catch (Exception exception) + { + _windowService.ShowExceptionDialog("Failed to apply protocol association", exception); + } + } + private async void UIAutoRunOnSettingChanged(object? sender, EventArgs e) { await ApplyAutoRun(); } + private void UIUseProtocolOnSettingChanged(object? sender, EventArgs e) + { + ApplyProtocolAssociation(); + } + private async void UIAutoRunDelayOnSettingChanged(object? sender, EventArgs e) { if (_autoRunProvider == null || !UIAutoRun.Value) diff --git a/src/Artemis.UI/Screens/StartupWizard/Steps/SettingsStep.axaml b/src/Artemis.UI/Screens/StartupWizard/Steps/SettingsStep.axaml index de06af76b..d8955f74e 100644 --- a/src/Artemis.UI/Screens/StartupWizard/Steps/SettingsStep.axaml +++ b/src/Artemis.UI/Screens/StartupWizard/Steps/SettingsStep.axaml @@ -42,6 +42,19 @@ + + + + Associate with Artemis links + + Open Artemis when navigating to artemis:// links, allows opening workshop entries from your browser. + + + + + + +