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.
+
+
+
+
+
+
+