diff --git a/src/Artemis.Core/Models/Surface/LayoutSelection.cs b/src/Artemis.Core/Models/Surface/LayoutSelection.cs
index f64c54eb1..f88f2ac0c 100644
--- a/src/Artemis.Core/Models/Surface/LayoutSelection.cs
+++ b/src/Artemis.Core/Models/Surface/LayoutSelection.cs
@@ -7,6 +7,7 @@ public class LayoutSelection : CorePropertyChanged
{
private string? _type;
private string? _parameter;
+ private string? _errorState;
///
/// Gets or sets what kind of layout reference this is.
@@ -25,4 +26,13 @@ public class LayoutSelection : CorePropertyChanged
get => _parameter;
set => SetAndNotify(ref _parameter, value);
}
+
+ ///
+ /// Gets or sets the error state of the layout reference.
+ ///
+ public string? ErrorState
+ {
+ get => _errorState;
+ set => SetAndNotify(ref _errorState, value);
+ }
}
\ No newline at end of file
diff --git a/src/Artemis.Core/Services/DeviceService.cs b/src/Artemis.Core/Services/DeviceService.cs
index da81a82a6..111594c4a 100644
--- a/src/Artemis.Core/Services/DeviceService.cs
+++ b/src/Artemis.Core/Services/DeviceService.cs
@@ -184,13 +184,14 @@ internal class DeviceService : IDeviceService
device.ApplyLayout(null, false, false);
else
provider?.ApplyLayout(device, layout);
+
+ UpdateLeds();
}
catch (Exception e)
{
+ device.LayoutSelection.ErrorState = e.Message;
_logger.Error(e, "Failed to apply device layout");
}
-
- UpdateLeds();
}
///
diff --git a/src/Artemis.UI.Shared/Routing/Routable/IRoutableHostScreen.cs b/src/Artemis.UI.Shared/Routing/Routable/IRoutableHostScreen.cs
index b09afb676..b524f739a 100644
--- a/src/Artemis.UI.Shared/Routing/Routable/IRoutableHostScreen.cs
+++ b/src/Artemis.UI.Shared/Routing/Routable/IRoutableHostScreen.cs
@@ -9,5 +9,6 @@ internal interface IRoutableHostScreen : IRoutableScreen
{
bool RecycleScreen { get; }
IRoutableScreen? InternalScreen { get; }
+ IRoutableScreen? InternalDefaultScreen { get; }
void InternalChangeScreen(IRoutableScreen? screen);
}
\ No newline at end of file
diff --git a/src/Artemis.UI.Shared/Routing/Routable/RoutableHostScreenOfTScreen.cs b/src/Artemis.UI.Shared/Routing/Routable/RoutableHostScreenOfTScreen.cs
index c61c27820..ca5208f19 100644
--- a/src/Artemis.UI.Shared/Routing/Routable/RoutableHostScreenOfTScreen.cs
+++ b/src/Artemis.UI.Shared/Routing/Routable/RoutableHostScreenOfTScreen.cs
@@ -25,7 +25,13 @@ public abstract class RoutableHostScreen : RoutableScreen, IRoutableHos
protected set => RaiseAndSetIfChanged(ref _recycleScreen, value);
}
+ ///
+ /// Gets the screen to show when no other screen is active.
+ ///
+ public virtual TScreen? DefaultScreen { get; }
+
IRoutableScreen? IRoutableHostScreen.InternalScreen => Screen;
+ IRoutableScreen? IRoutableHostScreen.InternalDefaultScreen => DefaultScreen;
void IRoutableHostScreen.InternalChangeScreen(IRoutableScreen? screen)
{
diff --git a/src/Artemis.UI.Shared/Routing/Routable/RoutableHostScreenOfTScreenTParam.cs b/src/Artemis.UI.Shared/Routing/Routable/RoutableHostScreenOfTScreenTParam.cs
index 89773e8d4..1dfef6b76 100644
--- a/src/Artemis.UI.Shared/Routing/Routable/RoutableHostScreenOfTScreenTParam.cs
+++ b/src/Artemis.UI.Shared/Routing/Routable/RoutableHostScreenOfTScreenTParam.cs
@@ -27,7 +27,13 @@ public abstract class RoutableHostScreen : RoutableScreen RaiseAndSetIfChanged(ref _recycleScreen, value);
}
+ ///
+ /// Gets the screen to show when no other screen is active.
+ ///
+ public virtual TScreen? DefaultScreen { get; }
+
IRoutableScreen? IRoutableHostScreen.InternalScreen => Screen;
+ IRoutableScreen? IRoutableHostScreen.InternalDefaultScreen => DefaultScreen;
void IRoutableHostScreen.InternalChangeScreen(IRoutableScreen? screen)
{
diff --git a/src/Artemis.UI.Shared/Routing/Routable/RoutableScreenOfTParam.cs b/src/Artemis.UI.Shared/Routing/Routable/RoutableScreenOfTParam.cs
index bbe71fe2a..52e8a5e48 100644
--- a/src/Artemis.UI.Shared/Routing/Routable/RoutableScreenOfTParam.cs
+++ b/src/Artemis.UI.Shared/Routing/Routable/RoutableScreenOfTParam.cs
@@ -13,6 +13,11 @@ namespace Artemis.UI.Shared.Routing;
/// The type of parameters the screen expects. It must have a parameterless constructor.
public abstract class RoutableScreen : RoutableScreen, IRoutableScreen where TParam : new()
{
+ ///
+ /// Gets or sets the parameter source of the screen.
+ ///
+ protected ParameterSource ParameterSource { get; set; } = ParameterSource.Segment;
+
///
/// Called while navigating to this screen.
///
@@ -26,15 +31,16 @@ public abstract class RoutableScreen : RoutableScreen, IRoutableScreen w
{
return Task.CompletedTask;
}
-
+
async Task IRoutableScreen.InternalOnNavigating(NavigationArguments args, CancellationToken cancellationToken)
{
Func
/// A task containing a boolean value which indicates whether there was a forward path to go back to.
Task GoForward();
+
+ ///
+ /// Asynchronously navigates upwards to the parent route.
+ ///
+ ///
+ Task GoUp();
///
/// Clears the navigation history.
diff --git a/src/Artemis.UI.Shared/Routing/Router/Navigation.cs b/src/Artemis.UI.Shared/Routing/Router/Navigation.cs
index 0d0f6083c..1df871d34 100644
--- a/src/Artemis.UI.Shared/Routing/Router/Navigation.cs
+++ b/src/Artemis.UI.Shared/Routing/Router/Navigation.cs
@@ -109,12 +109,11 @@ internal class Navigation
// Navigate the child too
if (resolution.Child != null)
await NavigateResolution(resolution.Child, args, childScreen);
- // Make sure there is no child
- else if (childScreen.InternalScreen != null)
- childScreen.InternalChangeScreen(null);
+ // Without a resolution, navigate to the default screen (which may be null)
+ else if (childScreen.InternalScreen != childScreen.InternalDefaultScreen)
+ childScreen.InternalChangeScreen(childScreen.InternalDefaultScreen);
}
-
Completed = true;
}
diff --git a/src/Artemis.UI.Shared/Routing/Router/Router.cs b/src/Artemis.UI.Shared/Routing/Router/Router.cs
index 39e067ec5..269d51fce 100644
--- a/src/Artemis.UI.Shared/Routing/Router/Router.cs
+++ b/src/Artemis.UI.Shared/Routing/Router/Router.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Linq;
using System.Reactive.Subjects;
using System.Threading.Tasks;
using Artemis.Core;
@@ -72,7 +73,13 @@ internal class Router : CorePropertyChanged, IRouter, IDisposable
///
public async Task Navigate(string path, RouterNavigationOptions? options = null)
{
- path = path.ToLower().Trim(' ', '/', '\\');
+ if (path.StartsWith('/') && _currentRouteSubject.Value != null)
+ path = _currentRouteSubject.Value + path;
+ if (path.StartsWith("../") && _currentRouteSubject.Value != null)
+ path = NavigateUp(_currentRouteSubject.Value, path);
+ else
+ path = path.ToLower().Trim(' ', '/', '\\');
+
options ??= new RouterNavigationOptions();
// Routing takes place on the UI thread with processing heavy tasks offloaded by the router itself
@@ -161,6 +168,28 @@ internal class Router : CorePropertyChanged, IRouter, IDisposable
return true;
}
+ ///
+ public async Task GoUp()
+ {
+ string? currentPath = _currentRouteSubject.Value;
+
+ // Keep removing segments until we find a parent route that resolves
+ while (currentPath != null && currentPath.Contains('/'))
+ {
+ string parentPath = currentPath[..currentPath.LastIndexOf('/')];
+ RouteResolution resolution = Resolve(parentPath);
+ if (resolution.Success)
+ {
+ await Navigate(parentPath, new RouterNavigationOptions {AddToHistory = false});
+ return true;
+ }
+
+ currentPath = parentPath;
+ }
+
+ return false;
+ }
+
///
public void ClearHistory()
{
@@ -194,6 +223,24 @@ internal class Router : CorePropertyChanged, IRouter, IDisposable
_logger.Debug("Router disposed, should that be? Stacktrace: \r\n{StackTrace}", Environment.StackTrace);
}
+
+
+ private string NavigateUp(string current, string path)
+ {
+ string[] pathParts = current.Split('/');
+ string[] navigateParts = path.Split('/');
+ int upCount = navigateParts.TakeWhile(part => part == "..").Count();
+
+ if (upCount >= pathParts.Length)
+ {
+ throw new InvalidOperationException("Cannot navigate up beyond the root");
+ }
+
+ IEnumerable remainingCurrentPathParts = pathParts.Take(pathParts.Length - upCount);
+ IEnumerable remainingNavigatePathParts = navigateParts.Skip(upCount);
+
+ return string.Join("/", remainingCurrentPathParts.Concat(remainingNavigatePathParts));
+ }
private void MainWindowServiceOnMainWindowOpened(object? sender, EventArgs e)
{
diff --git a/src/Artemis.UI.Shared/Styles/Skeleton.axaml b/src/Artemis.UI.Shared/Styles/Skeleton.axaml
index 2596c3fe6..0483a9763 100644
--- a/src/Artemis.UI.Shared/Styles/Skeleton.axaml
+++ b/src/Artemis.UI.Shared/Styles/Skeleton.axaml
@@ -8,6 +8,7 @@
+ TitleTextBlockStyle
This is heading 1
This is heading 2
This is heading 3
@@ -22,6 +23,7 @@
+
@@ -39,6 +41,7 @@
+ TitleTextBlockStyle
This is heading 1
This is heading 2
This is heading 3
@@ -51,6 +54,7 @@
+
@@ -68,6 +72,7 @@
+ TitleTextBlockStyle
This is heading 1
This is heading 2
This is heading 3
@@ -125,6 +130,11 @@
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/TreeItemViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/TreeItemViewModel.cs
index c19e68726..c5adf0948 100644
--- a/src/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/TreeItemViewModel.cs
+++ b/src/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/TreeItemViewModel.cs
@@ -34,7 +34,7 @@ public abstract partial class TreeItemViewModel : ActivatableViewModelBase
private RenderProfileElement? _currentProfileElement;
private ObservableAsPropertyHelper? _isFocused;
private TimeSpan _time;
-
+
[Notify] private bool _canPaste;
[Notify] private bool _isExpanded;
[Notify] private bool _isFlyoutOpen;
@@ -100,7 +100,7 @@ public abstract partial class TreeItemViewModel : ActivatableViewModelBase
public ReactiveCommand Paste { get; }
public ReactiveCommand Delete { get; }
public abstract bool SupportsChildren { get; }
-
+
public async Task ShowBrokenStateExceptions()
{
if (ProfileElement == null)
@@ -117,7 +117,7 @@ public abstract partial class TreeItemViewModel : ActivatableViewModelBase
return;
}
}
-
+
public void InsertElement(TreeItemViewModel elementViewModel, int targetIndex)
{
if (elementViewModel.Parent == this && Children.IndexOf(elementViewModel) == targetIndex)
@@ -239,26 +239,22 @@ public abstract partial class TreeItemViewModel : ActivatableViewModelBase
await _windowService.ShowDialogAsync(layer);
await ProfileEditorService.SaveProfileAsync();
}
-
+
private void ExecuteApplyAdaptionHints()
{
if (ProfileElement is not Layer layer)
return;
-
+
ProfileEditorService.ExecuteCommand(new ApplyAdaptionHints(layer, _deviceService.EnabledDevices.ToList()));
}
private async void UpdateCanPaste(bool isFlyoutOpen)
{
- string[] formats = await Shared.UI.Clipboard.GetFormatsAsync();
- //diogotr7: This can be null on Linux sometimes. I'm not sure why.
- if (formats == null!)
- {
- CanPaste = false;
- return;
- }
-
- CanPaste = formats.Contains(ProfileElementExtensions.ClipboardDataFormat);
+ string[]? formats = await Shared.UI.Clipboard.GetFormatsAsync();
+
+ // Can be null on some platforms
+ // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
+ CanPaste = formats != null && formats.Contains(ProfileElementExtensions.ClipboardDataFormat);
}
private bool GetIsFocused(ProfileEditorFocusMode focusMode, RenderProfileElement? currentProfileElement)
diff --git a/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Keyframes/TimelineKeyframeViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Keyframes/TimelineKeyframeViewModel.cs
index c568924bc..3c618d0b8 100644
--- a/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Keyframes/TimelineKeyframeViewModel.cs
+++ b/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Keyframes/TimelineKeyframeViewModel.cs
@@ -174,8 +174,11 @@ public partial class TimelineKeyframeViewModel : ActivatableViewModelBase, IT
private async void UpdateCanPaste(bool isFlyoutOpen)
{
- string[] formats = await Shared.UI.Clipboard.GetFormatsAsync();
- CanPaste = formats.Contains("Artemis.Keyframes");
+ string[]? formats = await Shared.UI.Clipboard.GetFormatsAsync();
+
+ // Can be null on some platforms
+ // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
+ CanPaste = formats != null && formats.Contains("Artemis.Keyframes");
}
#endregion
diff --git a/src/Artemis.UI/Screens/Root/RootView.axaml.cs b/src/Artemis.UI/Screens/Root/RootView.axaml.cs
index 04acf1892..90cd6afb8 100644
--- a/src/Artemis.UI/Screens/Root/RootView.axaml.cs
+++ b/src/Artemis.UI/Screens/Root/RootView.axaml.cs
@@ -19,7 +19,7 @@ public partial class RootView : ReactiveUserControl
{
try
{
- Dispatcher.UIThread.Invoke(() => RootFrame.NavigateFromObject(viewModel));
+ RootFrame.NavigateFromObject(viewModel);
}
catch (Exception)
{
diff --git a/src/Artemis.UI/Screens/Settings/SettingsView.axaml.cs b/src/Artemis.UI/Screens/Settings/SettingsView.axaml.cs
index 6e04c8c28..cb03c7a44 100644
--- a/src/Artemis.UI/Screens/Settings/SettingsView.axaml.cs
+++ b/src/Artemis.UI/Screens/Settings/SettingsView.axaml.cs
@@ -18,7 +18,7 @@ public partial class SettingsView : ReactiveUserControl
private void Navigate(ViewModelBase viewModel)
{
- Dispatcher.UIThread.Invoke(() => TabFrame.NavigateFromObject(viewModel));
+ TabFrame.NavigateFromObject(viewModel);
}
private void NavigationView_OnBackRequested(object? sender, NavigationViewBackRequestedEventArgs e)
diff --git a/src/Artemis.UI/Screens/Settings/Tabs/ReleasesTabView.axaml.cs b/src/Artemis.UI/Screens/Settings/Tabs/ReleasesTabView.axaml.cs
index cebc6f402..aadd8b4a6 100644
--- a/src/Artemis.UI/Screens/Settings/Tabs/ReleasesTabView.axaml.cs
+++ b/src/Artemis.UI/Screens/Settings/Tabs/ReleasesTabView.axaml.cs
@@ -17,17 +17,13 @@ public partial class ReleasesTabView : ReactiveUserControl
private void Navigate(ViewModelBase viewModel)
{
- Dispatcher.UIThread.Invoke(() =>
+ try
{
- try
- {
- ReleaseFrame.NavigateFromObject(viewModel);
- }
- catch (Exception)
- {
- // ignored
- }
- });
+ ReleaseFrame.NavigateFromObject(viewModel);
+ }
+ catch (Exception)
+ {
+ // ignored
+ }
}
-
}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Settings/Updating/ReleaseDetailsView.axaml b/src/Artemis.UI/Screens/Settings/Updating/ReleaseDetailsView.axaml
index ef4b22704..be8207516 100644
--- a/src/Artemis.UI/Screens/Settings/Updating/ReleaseDetailsView.axaml
+++ b/src/Artemis.UI/Screens/Settings/Updating/ReleaseDetailsView.axaml
@@ -43,7 +43,7 @@
- Release info
+ Release info
@@ -124,7 +124,7 @@
-
+
File size
-
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryInfoView.axaml b/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryInfoView.axaml
index 2d0ecbffa..a105aa15e 100644
--- a/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryInfoView.axaml
+++ b/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryInfoView.axaml
@@ -13,71 +13,90 @@
-
-
-
-
-
-
+
+
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryInfoView.axaml.cs b/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryInfoView.axaml.cs
index 5c6ec0a0c..2d9a96dc7 100644
--- a/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryInfoView.axaml.cs
+++ b/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryInfoView.axaml.cs
@@ -1,10 +1,11 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
+using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.Workshop.Entries.Details;
-public partial class EntryInfoView : UserControl
+public partial class EntryInfoView : ReactiveUserControl
{
public EntryInfoView()
{
diff --git a/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryInfoViewModel.cs b/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryInfoViewModel.cs
index d5c4de433..6cf02efb1 100644
--- a/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryInfoViewModel.cs
+++ b/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryInfoViewModel.cs
@@ -1,28 +1,66 @@
using System;
+using System.Linq;
+using System.Reactive.Disposables;
+using System.Reactive.Linq;
using System.Threading.Tasks;
using Artemis.Core;
using Artemis.UI.Shared;
+using Artemis.UI.Shared.Routing;
using Artemis.UI.Shared.Services;
using Artemis.WebClient.Workshop;
+using Artemis.WebClient.Workshop.Extensions;
+using Artemis.WebClient.Workshop.Models;
+using Artemis.WebClient.Workshop.Services;
+using PropertyChanged.SourceGenerator;
+using ReactiveUI;
namespace Artemis.UI.Screens.Workshop.Entries.Details;
-public class EntryInfoViewModel : ViewModelBase
+public partial class EntryInfoViewModel : ActivatableViewModelBase
{
+ private readonly IRouter _router;
private readonly INotificationService _notificationService;
- public IEntryDetails Entry { get; }
- public DateTimeOffset? UpdatedAt { get; }
-
- public EntryInfoViewModel(IEntryDetails entry, INotificationService notificationService)
+ private readonly IWorkshopService _workshopService;
+ [Notify] private IEntryDetails? _entry;
+ [Notify] private DateTimeOffset? _updatedAt;
+ [Notify] private bool _canBeManaged;
+
+ public EntryInfoViewModel(IRouter router, INotificationService notificationService, IWorkshopService workshopService)
{
+ _router = router;
_notificationService = notificationService;
+ _workshopService = workshopService;
+
+ this.WhenActivated(d =>
+ {
+ Observable.FromEventPattern(x => workshopService.OnInstalledEntrySaved += x, x => workshopService.OnInstalledEntrySaved -= x)
+ .StartWith([])
+ .Subscribe(_ => CanBeManaged = Entry != null && Entry.EntryType != EntryType.Profile && workshopService.GetInstalledEntry(Entry.Id) != null)
+ .DisposeWith(d);
+ });
+ }
+
+ public void SetEntry(IEntryDetails? entry)
+ {
Entry = entry;
- UpdatedAt = Entry.LatestRelease?.CreatedAt ?? Entry.CreatedAt;
+ UpdatedAt = Entry != null && Entry.Releases.Any() ? Entry.Releases.Max(r => r.CreatedAt) : Entry?.CreatedAt;
+ CanBeManaged = Entry != null && Entry.EntryType != EntryType.Profile && _workshopService.GetInstalledEntry(Entry.Id) != null;
}
public async Task CopyShareLink()
{
+ if (Entry == null)
+ return;
+
await Shared.UI.Clipboard.SetTextAsync($"{WorkshopConstants.WORKSHOP_URL}/entries/{Entry.Id}/{StringUtilities.UrlFriendly(Entry.Name)}");
_notificationService.CreateNotification().WithTitle("Copied share link to clipboard.").Show();
}
+
+ public async Task GoToManage()
+ {
+ if (Entry == null)
+ return;
+
+ await _router.Navigate($"{Entry.GetEntryPath()}/manage");
+ }
}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryReleasesView.axaml b/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryReleasesView.axaml
deleted file mode 100644
index 78944e621..000000000
--- a/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryReleasesView.axaml
+++ /dev/null
@@ -1,52 +0,0 @@
-
-
-
-
-
-
- Latest release
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Created
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryReleasesView.axaml.cs b/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryReleasesView.axaml.cs
deleted file mode 100644
index 523edc7e5..000000000
--- a/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryReleasesView.axaml.cs
+++ /dev/null
@@ -1,13 +0,0 @@
-using Avalonia;
-using Avalonia.Controls;
-using Avalonia.Markup.Xaml;
-
-namespace Artemis.UI.Screens.Workshop.Entries.Details;
-
-public partial class EntryReleasesView : UserControl
-{
- public EntryReleasesView()
- {
- InitializeComponent();
- }
-}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryReleasesViewModel.cs b/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryReleasesViewModel.cs
deleted file mode 100644
index 12bd22b3a..000000000
--- a/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryReleasesViewModel.cs
+++ /dev/null
@@ -1,74 +0,0 @@
-using System;
-using System.Reactive;
-using System.Threading;
-using System.Threading.Tasks;
-using Artemis.UI.Shared;
-using Artemis.UI.Shared.Services;
-using Artemis.UI.Shared.Services.Builders;
-using Artemis.UI.Shared.Utilities;
-using Artemis.WebClient.Workshop;
-using Artemis.WebClient.Workshop.Handlers.InstallationHandlers;
-using Artemis.WebClient.Workshop.Models;
-using Humanizer;
-using ReactiveUI;
-
-namespace Artemis.UI.Screens.Workshop.Entries.Details;
-
-public class EntryReleasesViewModel : ViewModelBase
-{
- private readonly EntryInstallationHandlerFactory _factory;
- private readonly IWindowService _windowService;
- private readonly INotificationService _notificationService;
-
- public EntryReleasesViewModel(IEntryDetails entry, EntryInstallationHandlerFactory factory, IWindowService windowService, INotificationService notificationService)
- {
- _factory = factory;
- _windowService = windowService;
- _notificationService = notificationService;
-
- Entry = entry;
- DownloadLatestRelease = ReactiveCommand.CreateFromTask(ExecuteDownloadLatestRelease);
- OnInstallationStarted = Confirm;
- }
-
- public IEntryDetails Entry { get; }
- public ReactiveCommand DownloadLatestRelease { get; }
-
- public Func> OnInstallationStarted { get; set; }
- public Func? OnInstallationFinished { get; set; }
-
- private async Task ExecuteDownloadLatestRelease(CancellationToken cancellationToken)
- {
- if (Entry.LatestRelease == null)
- return;
-
- if (await OnInstallationStarted(Entry))
- return;
-
- IEntryInstallationHandler installationHandler = _factory.CreateHandler(Entry.EntryType);
- EntryInstallResult result = await installationHandler.InstallAsync(Entry, Entry.LatestRelease, new Progress(), cancellationToken);
- if (result.IsSuccess && result.Entry != null)
- {
- if (OnInstallationFinished != null)
- await OnInstallationFinished(result.Entry);
- _notificationService.CreateNotification().WithTitle($"{Entry.EntryType.Humanize(LetterCasing.Sentence)} installed").WithSeverity(NotificationSeverity.Success).Show();
- }
- else
- {
- _notificationService.CreateNotification()
- .WithTitle($"Failed to install {Entry.EntryType.Humanize(LetterCasing.LowerCase)}")
- .WithMessage(result.Message)
- .WithSeverity(NotificationSeverity.Error).Show();
- }
- }
-
- private async Task Confirm(IEntryDetails entryDetails)
- {
- bool confirm = await _windowService.ShowConfirmContentDialog(
- "Install latest release",
- $"Are you sure you want to download and install version {entryDetails.LatestRelease?.Version} of {entryDetails.Name}?"
- );
-
- return !confirm;
- }
-}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/Entries/Details/EntrySpecificationsView.axaml b/src/Artemis.UI/Screens/Workshop/Entries/Details/EntrySpecificationsView.axaml
index 8b7fb56e6..16a0b4b5a 100644
--- a/src/Artemis.UI/Screens/Workshop/Entries/Details/EntrySpecificationsView.axaml
+++ b/src/Artemis.UI/Screens/Workshop/Entries/Details/EntrySpecificationsView.axaml
@@ -2,18 +2,15 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
- xmlns:entries="clr-namespace:Artemis.UI.Screens.Workshop.Entries"
xmlns:tagsInput="clr-namespace:Artemis.UI.Shared.TagsInput;assembly=Artemis.UI.Shared"
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
xmlns:categories="clr-namespace:Artemis.UI.Screens.Workshop.Categories"
- xmlns:avaloniaEdit="https://github.com/avaloniaui/avaloniaedit"
- xmlns:mdxaml="https://github.com/whistyun/Markdown.Avalonia.Tight"
- xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:details="clr-namespace:Artemis.UI.Screens.Workshop.Entries.Details"
+ xmlns:controls="clr-namespace:Artemis.UI.Controls"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="800"
x:Class="Artemis.UI.Screens.Workshop.Entries.Details.EntrySpecificationsView"
x:DataType="details:EntrySpecificationsViewModel">
-
+
@@ -95,48 +92,9 @@
-
-
-
-
- Synchronized scrolling
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
diff --git a/src/Artemis.UI/Screens/Workshop/Entries/Details/EntrySpecificationsView.axaml.cs b/src/Artemis.UI/Screens/Workshop/Entries/Details/EntrySpecificationsView.axaml.cs
index 37c85d118..1374349e9 100644
--- a/src/Artemis.UI/Screens/Workshop/Entries/Details/EntrySpecificationsView.axaml.cs
+++ b/src/Artemis.UI/Screens/Workshop/Entries/Details/EntrySpecificationsView.axaml.cs
@@ -1,100 +1,11 @@
-using System.Linq;
-using Avalonia;
-using Avalonia.Controls;
-using Avalonia.Media;
-using Avalonia.Media.Immutable;
using Avalonia.ReactiveUI;
-using AvaloniaEdit.TextMate;
-using ReactiveUI;
-using TextMateSharp.Grammars;
-using VisualExtensions = Artemis.UI.Shared.Extensions.VisualExtensions;
namespace Artemis.UI.Screens.Workshop.Entries.Details;
public partial class EntrySpecificationsView : ReactiveUserControl
{
- private ScrollViewer? _editorScrollViewer;
- private ScrollViewer? _previewScrollViewer;
- private bool _updating;
-
public EntrySpecificationsView()
{
InitializeComponent();
-
- DescriptionEditor.Options.AllowScrollBelowDocument = false;
- RegistryOptions options = new(ThemeName.Dark);
- TextMate.Installation? install = TextMate.InstallTextMate(DescriptionEditor, options);
-
- install.SetGrammar(options.GetScopeByExtension(".md"));
-
- this.WhenActivated(_ => SetupScrollSync());
- }
-
- protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
- {
- if (this.TryFindResource("SystemAccentColorLight3", out object? resource) && resource is Color color)
- DescriptionEditor.TextArea.TextView.LinkTextForegroundBrush = new ImmutableSolidColorBrush(color);
-
- base.OnAttachedToVisualTree(e);
- }
-
- private void SetupScrollSync()
- {
- if (_editorScrollViewer != null)
- _editorScrollViewer.PropertyChanged -= EditorScrollViewerOnPropertyChanged;
- if (_previewScrollViewer != null)
- _previewScrollViewer.PropertyChanged -= PreviewScrollViewerOnPropertyChanged;
-
- _editorScrollViewer = VisualExtensions.GetVisualChildrenOfType(DescriptionEditor).FirstOrDefault();
- _previewScrollViewer = VisualExtensions.GetVisualChildrenOfType(DescriptionPreview).FirstOrDefault();
-
- if (_editorScrollViewer != null)
- _editorScrollViewer.PropertyChanged += EditorScrollViewerOnPropertyChanged;
- if (_previewScrollViewer != null)
- _previewScrollViewer.PropertyChanged += PreviewScrollViewerOnPropertyChanged;
- }
-
- private void EditorScrollViewerOnPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
- {
- if (e.Property.Name != nameof(ScrollViewer.Offset) || _updating || SynchronizedScrolling.IsChecked != true)
- return;
-
- try
- {
- _updating = true;
- SynchronizeScrollViewers(_editorScrollViewer, _previewScrollViewer);
- }
- finally
- {
- _updating = false;
- }
- }
-
- private void PreviewScrollViewerOnPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
- {
- if (e.Property.Name != nameof(ScrollViewer.Offset) || _updating || SynchronizedScrolling.IsChecked != true)
- return;
-
- try
- {
- _updating = true;
- SynchronizeScrollViewers(_previewScrollViewer, _editorScrollViewer);
- }
- finally
- {
- _updating = false;
- }
- }
-
- private void SynchronizeScrollViewers(ScrollViewer? source, ScrollViewer? target)
- {
- if (source == null || target == null)
- return;
-
- double sourceScrollableHeight = source.Extent.Height - source.Viewport.Height;
- double targetScrollableHeight = target.Extent.Height - target.Viewport.Height;
-
- if (sourceScrollableHeight != 0)
- target.Offset = new Vector(target.Offset.X, targetScrollableHeight * (source.Offset.Y / sourceScrollableHeight));
}
}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/Entries/Details/EntrySpecificationsViewModel.cs b/src/Artemis.UI/Screens/Workshop/Entries/Details/EntrySpecificationsViewModel.cs
index f06262a3f..041af76cc 100644
--- a/src/Artemis.UI/Screens/Workshop/Entries/Details/EntrySpecificationsViewModel.cs
+++ b/src/Artemis.UI/Screens/Workshop/Entries/Details/EntrySpecificationsViewModel.cs
@@ -35,7 +35,6 @@ public partial class EntrySpecificationsViewModel : ValidatableViewModelBase
[Notify] private string _summary = string.Empty;
[Notify] private string _description = string.Empty;
[Notify] private Bitmap? _iconBitmap;
- [Notify] private TextDocument? _markdownDocument;
[Notify(Setter.Private)] private bool _iconChanged;
public EntrySpecificationsViewModel(IWorkshopClient workshopClient, IWindowService windowService)
@@ -65,20 +64,7 @@ public partial class EntrySpecificationsViewModel : ValidatableViewModelBase
_categoriesValid = categoriesRule.ValidationChanged.Select(c => c.IsValid).ToProperty(this, vm => vm.CategoriesValid);
_descriptionValid = descriptionRule.ValidationChanged.Select(c => c.IsValid).ToProperty(this, vm => vm.DescriptionValid);
- this.WhenActivatedAsync(async d =>
- {
- // Load categories
- await PopulateCategories();
-
- MarkdownDocument = new TextDocument(new StringTextSource(Description));
- MarkdownDocument.TextChanged += MarkdownDocumentOnTextChanged;
- Disposable.Create(() =>
- {
- MarkdownDocument.TextChanged -= MarkdownDocumentOnTextChanged;
- MarkdownDocument = null;
- ClearIcon();
- }).DisposeWith(d);
- });
+ this.WhenActivatedAsync(async _ => await PopulateCategories());
}
public ReactiveCommand SelectIcon { get; }
@@ -92,12 +78,7 @@ public partial class EntrySpecificationsViewModel : ValidatableViewModelBase
public bool DescriptionValid => _descriptionValid.Value;
public List PreselectedCategories { get; set; } = new();
-
- private void MarkdownDocumentOnTextChanged(object? sender, EventArgs e)
- {
- Description = MarkdownDocument?.Text ?? string.Empty;
- }
-
+
private async Task ExecuteSelectIcon()
{
string[]? result = await _windowService.CreateOpenFileDialog()
@@ -112,12 +93,6 @@ public partial class EntrySpecificationsViewModel : ValidatableViewModelBase
IconChanged = true;
}
- private void ClearIcon()
- {
- IconBitmap?.Dispose();
- IconBitmap = null;
- }
-
private async Task PopulateCategories()
{
IOperationResult categories = await _workshopClient.GetCategories.ExecuteAsync();
diff --git a/src/Artemis.UI/Screens/Workshop/Entries/EntriesView.axaml.cs b/src/Artemis.UI/Screens/Workshop/Entries/EntriesView.axaml.cs
index 3f2106645..1299973d2 100644
--- a/src/Artemis.UI/Screens/Workshop/Entries/EntriesView.axaml.cs
+++ b/src/Artemis.UI/Screens/Workshop/Entries/EntriesView.axaml.cs
@@ -18,7 +18,7 @@ public partial class EntriesView : ReactiveUserControl
private void Navigate(ViewModelBase viewModel)
{
- Dispatcher.UIThread.Invoke(() => TabFrame.NavigateFromObject(viewModel));
+ TabFrame.NavigateFromObject(viewModel);
}
private void NavigationView_OnBackRequested(object? sender, NavigationViewBackRequestedEventArgs e)
diff --git a/src/Artemis.UI/Screens/Workshop/Entries/EntriesViewModel.cs b/src/Artemis.UI/Screens/Workshop/Entries/EntriesViewModel.cs
index e76014c34..760193832 100644
--- a/src/Artemis.UI/Screens/Workshop/Entries/EntriesViewModel.cs
+++ b/src/Artemis.UI/Screens/Workshop/Entries/EntriesViewModel.cs
@@ -53,8 +53,8 @@ public partial class EntriesViewModel : RoutableHostScreen
public void GoBack()
{
- if (ViewingDetails)
- _router.GoBack();
+ if (ViewingDetails && SelectedTab != null)
+ _router.Navigate(SelectedTab.Path);
else
_router.Navigate("workshop");
}
diff --git a/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListItemView.axaml b/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListItemView.axaml
index 91932033a..712d7fc68 100644
--- a/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListItemView.axaml
+++ b/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListItemView.axaml
@@ -79,11 +79,11 @@
-
+
installed
-
+
update available
diff --git a/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListView.axaml b/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListView.axaml
new file mode 100644
index 000000000..597282d98
--- /dev/null
+++ b/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListView.axaml
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+
+
+
+
+
+ Categories
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Looks like your current filters gave no results
+
+ Modify or clear your filters to view other entries
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/Entries/Tabs/LayoutListView.axaml.cs b/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListView.axaml.cs
similarity index 51%
rename from src/Artemis.UI/Screens/Workshop/Entries/Tabs/LayoutListView.axaml.cs
rename to src/Artemis.UI/Screens/Workshop/Entries/List/EntryListView.axaml.cs
index a003f47bb..e4a15ed77 100644
--- a/src/Artemis.UI/Screens/Workshop/Entries/Tabs/LayoutListView.axaml.cs
+++ b/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListView.axaml.cs
@@ -1,38 +1,30 @@
-using System;
-using System.Reactive.Disposables;
using System.Threading;
-using Artemis.UI.Shared.Routing;
using Avalonia.Controls;
using Avalonia.ReactiveUI;
-using Avalonia.Threading;
using ReactiveUI;
-namespace Artemis.UI.Screens.Workshop.Entries.Tabs;
+namespace Artemis.UI.Screens.Workshop.Entries.List;
-public partial class LayoutListView : ReactiveUserControl
+public partial class EntryListView : ReactiveUserControl
{
- public LayoutListView()
+ public EntryListView()
{
InitializeComponent();
EntriesScrollViewer.SizeChanged += (_, _) => UpdateEntriesPerFetch();
-
- this.WhenActivated(d =>
- {
- UpdateEntriesPerFetch();
- ViewModel.WhenAnyValue(vm => vm.Screen).WhereNotNull().Subscribe(Navigate).DisposeWith(d);
- });
+
+ this.WhenActivated(_ => UpdateEntriesPerFetch());
}
private void ScrollViewer_OnScrollChanged(object? sender, ScrollChangedEventArgs e)
{
+ if (ViewModel == null)
+ return;
+
// When near the bottom of EntriesScrollViewer, call FetchMore on the view model
if (EntriesScrollViewer.Offset.Y != 0 && EntriesScrollViewer.Extent.Height - (EntriesScrollViewer.Viewport.Height + EntriesScrollViewer.Offset.Y) < 100)
- ViewModel?.FetchMore(CancellationToken.None);
- }
+ ViewModel.FetchMore(CancellationToken.None);
- private void Navigate(RoutableScreen viewModel)
- {
- Dispatcher.UIThread.Invoke(() => RouterFrame.NavigateFromObject(viewModel), DispatcherPriority.ApplicationIdle);
+ ViewModel.ScrollOffset = EntriesScrollViewer.Offset;
}
private void UpdateEntriesPerFetch()
diff --git a/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListViewModel.cs b/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListViewModel.cs
index 92322920d..f335c9128 100644
--- a/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListViewModel.cs
+++ b/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListViewModel.cs
@@ -6,6 +6,7 @@ using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Threading;
using System.Threading.Tasks;
+using Artemis.UI.Extensions;
using Artemis.UI.Screens.Workshop.Categories;
using Artemis.UI.Shared.Routing;
using Artemis.UI.Shared.Services;
@@ -15,29 +16,28 @@ using DynamicData;
using PropertyChanged.SourceGenerator;
using ReactiveUI;
using StrawberryShake;
+using Vector = Avalonia.Vector;
namespace Artemis.UI.Screens.Workshop.Entries.List;
-public abstract partial class EntryListViewModel : RoutableHostScreen
+public partial class EntryListViewModel : RoutableScreen
{
private readonly SourceList _entries = new();
private readonly INotificationService _notificationService;
private readonly IWorkshopClient _workshopClient;
- private readonly string _route;
private IGetEntriesv2_EntriesV2_PageInfo? _currentPageInfo;
[Notify] private bool _initializing = true;
[Notify] private bool _fetchingMore;
[Notify] private int _entriesPerFetch;
+ [Notify] private Vector _scrollOffset;
- protected EntryListViewModel(string route,
- IWorkshopClient workshopClient,
+ protected EntryListViewModel(IWorkshopClient workshopClient,
CategoriesViewModel categoriesViewModel,
EntryListInputViewModel entryListInputViewModel,
INotificationService notificationService,
Func getEntryListViewModel)
{
- _route = route;
_workshopClient = workshopClient;
_notificationService = notificationService;
@@ -50,37 +50,31 @@ public abstract partial class EntryListViewModel : RoutableHostScreen
{
- // Respond to filter query input changes
InputViewModel.WhenAnyValue(vm => vm.Search).Skip(1).Throttle(TimeSpan.FromMilliseconds(200)).Subscribe(_ => Reset()).DisposeWith(d);
CategoriesViewModel.WhenAnyValue(vm => vm.CategoryFilters).Skip(1).Subscribe(_ => Reset()).DisposeWith(d);
});
+
+ // Load entries when the view model is first activated
+ this.WhenActivatedAsync(async _ =>
+ {
+ if (_entries.Count == 0)
+ {
+ await Task.Delay(250);
+ await FetchMore(CancellationToken.None);
+ Initializing = false;
+ }
+ });
}
public CategoriesViewModel CategoriesViewModel { get; }
public EntryListInputViewModel InputViewModel { get; }
+ public EntryType? EntryType { get; set; }
public ReadOnlyObservableCollection Entries { get; }
-
- public override async Task OnNavigating(NavigationArguments args, CancellationToken cancellationToken)
- {
- if (_entries.Count == 0)
- {
- await Task.Delay(250, cancellationToken);
- await FetchMore(cancellationToken);
- Initializing = false;
- }
- }
-
- public override Task OnClosing(NavigationArguments args)
- {
- // Clear search if not navigating to a child
- if (!args.Path.StartsWith(_route))
- InputViewModel.ClearLastSearch();
- return base.OnClosing(args);
- }
-
+
public async Task FetchMore(CancellationToken cancellationToken)
{
if (FetchingMore || _currentPageInfo != null && !_currentPageInfo.HasNextPage)
@@ -119,12 +113,19 @@ public abstract partial class EntryListViewModel : RoutableHostScreen GetSort()
+ private IReadOnlyList GetSort()
{
// Sort by created at
if (InputViewModel.SortBy == 1)
diff --git a/src/Artemis.UI/Screens/Workshop/Entries/Tabs/LayoutListView.axaml b/src/Artemis.UI/Screens/Workshop/Entries/Tabs/LayoutListView.axaml
deleted file mode 100644
index 1c0c4182c..000000000
--- a/src/Artemis.UI/Screens/Workshop/Entries/Tabs/LayoutListView.axaml
+++ /dev/null
@@ -1,67 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
- Categories
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Looks like your current filters gave no results
-
- Modify or clear your filters to view other device layouts
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/Entries/Tabs/LayoutListViewModel.cs b/src/Artemis.UI/Screens/Workshop/Entries/Tabs/LayoutListViewModel.cs
deleted file mode 100644
index 11c97846c..000000000
--- a/src/Artemis.UI/Screens/Workshop/Entries/Tabs/LayoutListViewModel.cs
+++ /dev/null
@@ -1,34 +0,0 @@
-using System;
-using Artemis.UI.Screens.Workshop.Categories;
-using Artemis.UI.Screens.Workshop.Entries.List;
-using Artemis.UI.Shared.Routing;
-using Artemis.UI.Shared.Services;
-using Artemis.WebClient.Workshop;
-
-namespace Artemis.UI.Screens.Workshop.Entries.Tabs;
-
-public class LayoutListViewModel : List.EntryListViewModel
-{
- public LayoutListViewModel(IWorkshopClient workshopClient,
- CategoriesViewModel categoriesViewModel,
- EntryListInputViewModel entryListInputViewModel,
- INotificationService notificationService,
- Func getEntryListViewModel)
- : base("workshop/entries/layouts", workshopClient, categoriesViewModel, entryListInputViewModel, notificationService, getEntryListViewModel)
- {
- entryListInputViewModel.SearchWatermark = "Search layouts";
- }
-
- protected override EntryFilterInput GetFilter()
- {
- return new EntryFilterInput
- {
- And = new[]
- {
- new EntryFilterInput {EntryType = new EntryTypeOperationFilterInput {Eq = EntryType.Layout}},
- new EntryFilterInput(){LatestReleaseId = new LongOperationFilterInput {Gt = 0}},
- base.GetFilter()
- }
- };
- }
-}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/Entries/Tabs/PluginListView.axaml b/src/Artemis.UI/Screens/Workshop/Entries/Tabs/PluginListView.axaml
deleted file mode 100644
index 7b6cb1f0c..000000000
--- a/src/Artemis.UI/Screens/Workshop/Entries/Tabs/PluginListView.axaml
+++ /dev/null
@@ -1,67 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
- Categories
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Looks like your current filters gave no results
-
- Modify or clear your filters to view other plugins
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/Entries/Tabs/PluginListView.axaml.cs b/src/Artemis.UI/Screens/Workshop/Entries/Tabs/PluginListView.axaml.cs
deleted file mode 100644
index 8cfaa1696..000000000
--- a/src/Artemis.UI/Screens/Workshop/Entries/Tabs/PluginListView.axaml.cs
+++ /dev/null
@@ -1,43 +0,0 @@
-using System;
-using System.Reactive.Disposables;
-using System.Threading;
-using Artemis.UI.Shared.Routing;
-using Avalonia.Controls;
-using Avalonia.ReactiveUI;
-using Avalonia.Threading;
-using ReactiveUI;
-
-namespace Artemis.UI.Screens.Workshop.Entries.Tabs;
-
-public partial class PluginListView : ReactiveUserControl
-{
- public PluginListView()
- {
- InitializeComponent();
- EntriesScrollViewer.SizeChanged += (_, _) => UpdateEntriesPerFetch();
-
- this.WhenActivated(d =>
- {
- UpdateEntriesPerFetch();
- ViewModel.WhenAnyValue(vm => vm.Screen).WhereNotNull().Subscribe(Navigate).DisposeWith(d);
- });
- }
-
- private void ScrollViewer_OnScrollChanged(object? sender, ScrollChangedEventArgs e)
- {
- // When near the bottom of EntriesScrollViewer, call FetchMore on the view model
- if (EntriesScrollViewer.Offset.Y != 0 && EntriesScrollViewer.Extent.Height - (EntriesScrollViewer.Viewport.Height + EntriesScrollViewer.Offset.Y) < 100)
- ViewModel?.FetchMore(CancellationToken.None);
- }
-
- private void Navigate(RoutableScreen viewModel)
- {
- Dispatcher.UIThread.Invoke(() => RouterFrame.NavigateFromObject(viewModel), DispatcherPriority.ApplicationIdle);
- }
-
- private void UpdateEntriesPerFetch()
- {
- if (ViewModel != null)
- ViewModel.EntriesPerFetch = (int) (EntriesScrollViewer.Viewport.Height / 120);
- }
-}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/Entries/Tabs/PluginListViewModel.cs b/src/Artemis.UI/Screens/Workshop/Entries/Tabs/PluginListViewModel.cs
deleted file mode 100644
index c7ea484a6..000000000
--- a/src/Artemis.UI/Screens/Workshop/Entries/Tabs/PluginListViewModel.cs
+++ /dev/null
@@ -1,33 +0,0 @@
-using System;
-using Artemis.UI.Screens.Workshop.Categories;
-using Artemis.UI.Screens.Workshop.Entries.List;
-using Artemis.UI.Shared.Routing;
-using Artemis.UI.Shared.Services;
-using Artemis.WebClient.Workshop;
-
-namespace Artemis.UI.Screens.Workshop.Entries.Tabs;
-
-public class PluginListViewModel : EntryListViewModel
-{
- public PluginListViewModel(IWorkshopClient workshopClient,
- CategoriesViewModel categoriesViewModel,
- EntryListInputViewModel entryListInputViewModel,
- INotificationService notificationService,
- Func getEntryListViewModel)
- : base("workshop/entries/plugins", workshopClient, categoriesViewModel, entryListInputViewModel, notificationService, getEntryListViewModel)
- {
- entryListInputViewModel.SearchWatermark = "Search plugins";
- }
-
- protected override EntryFilterInput GetFilter()
- {
- return new EntryFilterInput
- {
- And = new[]
- {
- new EntryFilterInput {EntryType = new EntryTypeOperationFilterInput {Eq = EntryType.Plugin}},
- base.GetFilter()
- }
- };
- }
-}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/Entries/Tabs/ProfileListView.axaml b/src/Artemis.UI/Screens/Workshop/Entries/Tabs/ProfileListView.axaml
deleted file mode 100644
index 03028d4b8..000000000
--- a/src/Artemis.UI/Screens/Workshop/Entries/Tabs/ProfileListView.axaml
+++ /dev/null
@@ -1,67 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
- Categories
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Looks like your current filters gave no results
-
- Modify or clear your filters to view some awesome profiles
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/Entries/Tabs/ProfileListView.axaml.cs b/src/Artemis.UI/Screens/Workshop/Entries/Tabs/ProfileListView.axaml.cs
deleted file mode 100644
index b25ba45f3..000000000
--- a/src/Artemis.UI/Screens/Workshop/Entries/Tabs/ProfileListView.axaml.cs
+++ /dev/null
@@ -1,43 +0,0 @@
-using System;
-using System.Reactive.Disposables;
-using System.Threading;
-using Artemis.UI.Shared.Routing;
-using Avalonia.Controls;
-using Avalonia.ReactiveUI;
-using Avalonia.Threading;
-using ReactiveUI;
-
-namespace Artemis.UI.Screens.Workshop.Entries.Tabs;
-
-public partial class ProfileListView : ReactiveUserControl
-{
- public ProfileListView()
- {
- InitializeComponent();
- EntriesScrollViewer.SizeChanged += (_, _) => UpdateEntriesPerFetch();
-
- this.WhenActivated(d =>
- {
- UpdateEntriesPerFetch();
- ViewModel.WhenAnyValue(vm => vm.Screen).WhereNotNull().Subscribe(Navigate).DisposeWith(d);
- });
- }
-
- private void ScrollViewer_OnScrollChanged(object? sender, ScrollChangedEventArgs e)
- {
- // When near the bottom of EntriesScrollViewer, call FetchMore on the view model
- if (EntriesScrollViewer.Offset.Y != 0 && EntriesScrollViewer.Extent.Height - (EntriesScrollViewer.Viewport.Height + EntriesScrollViewer.Offset.Y) < 100)
- ViewModel?.FetchMore(CancellationToken.None);
- }
-
- private void Navigate(RoutableScreen viewModel)
- {
- Dispatcher.UIThread.Invoke(() => RouterFrame.NavigateFromObject(viewModel), DispatcherPriority.ApplicationIdle);
- }
-
- private void UpdateEntriesPerFetch()
- {
- if (ViewModel != null)
- ViewModel.EntriesPerFetch = (int) (EntriesScrollViewer.Viewport.Height / 120);
- }
-}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/Entries/Tabs/ProfileListViewModel.cs b/src/Artemis.UI/Screens/Workshop/Entries/Tabs/ProfileListViewModel.cs
deleted file mode 100644
index 09ed5410b..000000000
--- a/src/Artemis.UI/Screens/Workshop/Entries/Tabs/ProfileListViewModel.cs
+++ /dev/null
@@ -1,33 +0,0 @@
-using System;
-using Artemis.UI.Screens.Workshop.Categories;
-using Artemis.UI.Screens.Workshop.Entries.List;
-using Artemis.UI.Shared.Routing;
-using Artemis.UI.Shared.Services;
-using Artemis.WebClient.Workshop;
-
-namespace Artemis.UI.Screens.Workshop.Entries.Tabs;
-
-public class ProfileListViewModel : List.EntryListViewModel
-{
- public ProfileListViewModel(IWorkshopClient workshopClient,
- CategoriesViewModel categoriesViewModel,
- EntryListInputViewModel entryListInputViewModel,
- INotificationService notificationService,
- Func getEntryListViewModel)
- : base("workshop/entries/profiles", workshopClient, categoriesViewModel, entryListInputViewModel, notificationService, getEntryListViewModel)
- {
- entryListInputViewModel.SearchWatermark = "Search profiles";
- }
-
- protected override EntryFilterInput GetFilter()
- {
- return new EntryFilterInput
- {
- And = new[]
- {
- new EntryFilterInput {EntryType = new EntryTypeOperationFilterInput {Eq = EntryType.Profile}},
- base.GetFilter()
- }
- };
- }
-}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleaseItemView.axaml b/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleaseItemView.axaml
new file mode 100644
index 000000000..1a328d64a
--- /dev/null
+++ b/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleaseItemView.axaml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Created
+
+
+
+
+
+
diff --git a/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleaseItemView.axaml.cs b/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleaseItemView.axaml.cs
new file mode 100644
index 000000000..ffc325a79
--- /dev/null
+++ b/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleaseItemView.axaml.cs
@@ -0,0 +1,11 @@
+using Avalonia.ReactiveUI;
+
+namespace Artemis.UI.Screens.Workshop.EntryReleases;
+
+public partial class EntryReleaseItemView : ReactiveUserControl
+{
+ public EntryReleaseItemView()
+ {
+ InitializeComponent();
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleaseItemViewModel.cs b/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleaseItemViewModel.cs
new file mode 100644
index 000000000..bafa65507
--- /dev/null
+++ b/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleaseItemViewModel.cs
@@ -0,0 +1,41 @@
+using System;
+using System.Reactive.Disposables;
+using System.Reactive.Linq;
+using Artemis.UI.Shared;
+using Artemis.WebClient.Workshop;
+using Artemis.WebClient.Workshop.Models;
+using Artemis.WebClient.Workshop.Services;
+using PropertyChanged.SourceGenerator;
+using ReactiveUI;
+
+namespace Artemis.UI.Screens.Workshop.EntryReleases;
+
+public partial class EntryReleaseItemViewModel : ActivatableViewModelBase
+{
+ private readonly IWorkshopService _workshopService;
+ private readonly IEntryDetails _entry;
+ [Notify] private bool _isCurrentVersion;
+
+ public EntryReleaseItemViewModel(IWorkshopService workshopService, IEntryDetails entry, IRelease release)
+ {
+ _workshopService = workshopService;
+ _entry = entry;
+
+ Release = release;
+ UpdateIsCurrentVersion();
+
+ this.WhenActivated(d =>
+ {
+ Observable.FromEventPattern(x => _workshopService.OnInstalledEntrySaved += x, x => _workshopService.OnInstalledEntrySaved -= x)
+ .Subscribe(_ => UpdateIsCurrentVersion())
+ .DisposeWith(d);
+ });
+ }
+
+ public IRelease Release { get; }
+
+ private void UpdateIsCurrentVersion()
+ {
+ IsCurrentVersion = _workshopService.GetInstalledEntry(_entry.Id)?.ReleaseId == Release.Id;
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleaseView.axaml b/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleaseView.axaml
new file mode 100644
index 000000000..62a991f55
--- /dev/null
+++ b/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleaseView.axaml
@@ -0,0 +1,132 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Release info
+
+
+
+
+
+
+ Cancel
+
+
+
+
+
+
+ Install
+
+
+ Re-install
+
+
+
+
+
+
+
+
+ Version
+
+
+
+
+
+ Release date
+
+
+
+
+
+ File size
+
+
+
+
+
+
+
+
+
+ Release notes
+
+
+
+ There are no release notes for this release.
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleaseView.axaml.cs b/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleaseView.axaml.cs
new file mode 100644
index 000000000..ea7532c70
--- /dev/null
+++ b/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleaseView.axaml.cs
@@ -0,0 +1,14 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using Avalonia.ReactiveUI;
+
+namespace Artemis.UI.Screens.Workshop.EntryReleases;
+
+public partial class EntryReleaseView : ReactiveUserControl
+{
+ public EntryReleaseView()
+ {
+ InitializeComponent();
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleaseViewModel.cs b/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleaseViewModel.cs
new file mode 100644
index 000000000..32b1de42a
--- /dev/null
+++ b/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleaseViewModel.cs
@@ -0,0 +1,119 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Artemis.UI.Screens.Workshop.Parameters;
+using Artemis.UI.Shared.Routing;
+using Artemis.UI.Shared.Services;
+using Artemis.UI.Shared.Services.Builders;
+using Artemis.UI.Shared.Utilities;
+using Artemis.WebClient.Workshop;
+using Artemis.WebClient.Workshop.Handlers.InstallationHandlers;
+using Artemis.WebClient.Workshop.Services;
+using PropertyChanged.SourceGenerator;
+using StrawberryShake;
+
+namespace Artemis.UI.Screens.Workshop.EntryReleases;
+
+public partial class EntryReleaseViewModel : RoutableScreen
+{
+ private readonly IWorkshopClient _client;
+ private readonly IRouter _router;
+ private readonly INotificationService _notificationService;
+ private readonly IWindowService _windowService;
+ private readonly IWorkshopService _workshopService;
+ private readonly EntryInstallationHandlerFactory _factory;
+ private readonly Progress _progress = new();
+
+ [Notify] private IGetReleaseById_Release? _release;
+ [Notify] private float _installProgress;
+ [Notify] private bool _installationInProgress;
+ [Notify] private bool _isCurrentVersion;
+
+ private CancellationTokenSource? _cts;
+
+ public EntryReleaseViewModel(IWorkshopClient client, IRouter router, INotificationService notificationService, IWindowService windowService, IWorkshopService workshopService,
+ EntryInstallationHandlerFactory factory)
+ {
+ _client = client;
+ _router = router;
+ _notificationService = notificationService;
+ _windowService = windowService;
+ _workshopService = workshopService;
+ _factory = factory;
+ _progress.ProgressChanged += (_, f) => InstallProgress = f.ProgressPercentage;
+ }
+
+ public async Task Close()
+ {
+ await _router.GoUp();
+ }
+
+ public async Task Install()
+ {
+ if (Release == null)
+ return;
+
+ _cts = new CancellationTokenSource();
+ InstallProgress = 0;
+ InstallationInProgress = true;
+ try
+ {
+ IEntryInstallationHandler handler = _factory.CreateHandler(Release.Entry.EntryType);
+ EntryInstallResult result = await handler.InstallAsync(Release.Entry, Release, _progress, _cts.Token);
+ if (result.IsSuccess)
+ {
+ _notificationService.CreateNotification().WithTitle("Installation succeeded").WithSeverity(NotificationSeverity.Success).Show();
+ IsCurrentVersion = true;
+ InstallationInProgress = false;
+ await Manage();
+ }
+ else if (!_cts.IsCancellationRequested)
+ _notificationService.CreateNotification().WithTitle("Installation failed").WithMessage(result.Message).WithSeverity(NotificationSeverity.Error).Show();
+ }
+ catch (Exception e)
+ {
+ InstallationInProgress = false;
+ _windowService.ShowExceptionDialog("Failed to install workshop entry", e);
+ }
+ }
+
+ public async Task Manage()
+ {
+ if (Release?.Entry.EntryType != EntryType.Profile)
+ await _router.Navigate("../../manage");
+ }
+
+ public async Task Reinstall()
+ {
+ if (await _windowService.ShowConfirmContentDialog("Reinstall entry", "Are you sure you want to reinstall this entry?"))
+ await Install();
+ }
+
+ public void Cancel()
+ {
+ _cts?.Cancel();
+ }
+
+ ///
+ public override async Task OnNavigating(ReleaseDetailParameters parameters, NavigationArguments args, CancellationToken cancellationToken)
+ {
+ IOperationResult result = await _client.GetReleaseById.ExecuteAsync(parameters.ReleaseId, cancellationToken);
+ Release = result.Data?.Release;
+ IsCurrentVersion = Release != null && _workshopService.GetInstalledEntry(Release.Entry.Id)?.ReleaseId == Release.Id;
+ }
+
+ #region Overrides of RoutableScreen
+
+ ///
+ public override Task OnClosing(NavigationArguments args)
+ {
+ if (!InstallationInProgress)
+ return Task.CompletedTask;
+
+ args.Cancel();
+ _notificationService.CreateNotification().WithMessage("Please wait for the installation to finish").Show();
+ return Task.CompletedTask;
+ }
+
+ #endregion
+}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleasesView.axaml b/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleasesView.axaml
new file mode 100644
index 000000000..0e0346d83
--- /dev/null
+++ b/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleasesView.axaml
@@ -0,0 +1,13 @@
+
+ Releases
+
+
+
+
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleasesView.axaml.cs b/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleasesView.axaml.cs
new file mode 100644
index 000000000..39b603187
--- /dev/null
+++ b/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleasesView.axaml.cs
@@ -0,0 +1,12 @@
+using Avalonia.Controls;
+using Avalonia.ReactiveUI;
+
+namespace Artemis.UI.Screens.Workshop.EntryReleases;
+
+public partial class EntryReleasesView : ReactiveUserControl
+{
+ public EntryReleasesView()
+ {
+ InitializeComponent();
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleasesViewModel.cs b/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleasesViewModel.cs
new file mode 100644
index 000000000..a478318e3
--- /dev/null
+++ b/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleasesViewModel.cs
@@ -0,0 +1,44 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reactive.Disposables;
+using System.Threading.Tasks;
+using Artemis.UI.Shared;
+using Artemis.UI.Shared.Routing;
+using Artemis.WebClient.Workshop;
+using Artemis.WebClient.Workshop.Extensions;
+using PropertyChanged.SourceGenerator;
+using ReactiveUI;
+
+namespace Artemis.UI.Screens.Workshop.EntryReleases;
+
+public partial class EntryReleasesViewModel : ActivatableViewModelBase
+{
+ private readonly IRouter _router;
+ [Notify] private EntryReleaseItemViewModel? _selectedRelease;
+
+ public EntryReleasesViewModel(IEntryDetails entry, IRouter router, Func getEntryReleaseItemViewModel)
+ {
+ _router = router;
+
+ Entry = entry;
+ Releases = Entry.Releases.OrderByDescending(r => r.CreatedAt).Take(5).Select(r => getEntryReleaseItemViewModel(r)).ToList();
+
+ this.WhenActivated(d =>
+ {
+ router.CurrentPath.Subscribe(p =>
+ SelectedRelease = p != null && p.StartsWith($"{Entry.GetEntryPath()}/releases") && float.TryParse(p.Split('/').Last(), out float releaseId)
+ ? Releases.FirstOrDefault(r => r.Release.Id == releaseId)
+ : null)
+ .DisposeWith(d);
+
+ this.WhenAnyValue(vm => vm.SelectedRelease)
+ .WhereNotNull()
+ .Subscribe(s => _router.Navigate($"{Entry.GetEntryPath()}/releases/{s.Release.Id}"))
+ .DisposeWith(d);
+ });
+ }
+
+ public IEntryDetails Entry { get; }
+ public List Releases { get; }
+}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/Layout/LayoutDescriptionView.axaml b/src/Artemis.UI/Screens/Workshop/Layout/LayoutDescriptionView.axaml
new file mode 100644
index 000000000..e4cf60508
--- /dev/null
+++ b/src/Artemis.UI/Screens/Workshop/Layout/LayoutDescriptionView.axaml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/Layout/LayoutDescriptionView.axaml.cs b/src/Artemis.UI/Screens/Workshop/Layout/LayoutDescriptionView.axaml.cs
new file mode 100644
index 000000000..5276e422f
--- /dev/null
+++ b/src/Artemis.UI/Screens/Workshop/Layout/LayoutDescriptionView.axaml.cs
@@ -0,0 +1,14 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using Avalonia.ReactiveUI;
+
+namespace Artemis.UI.Screens.Workshop.Layout;
+
+public partial class LayoutDescriptionView : ReactiveUserControl
+{
+ public LayoutDescriptionView()
+ {
+ InitializeComponent();
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/Layout/LayoutDescriptionViewModel.cs b/src/Artemis.UI/Screens/Workshop/Layout/LayoutDescriptionViewModel.cs
new file mode 100644
index 000000000..3fb8f678e
--- /dev/null
+++ b/src/Artemis.UI/Screens/Workshop/Layout/LayoutDescriptionViewModel.cs
@@ -0,0 +1,10 @@
+using Artemis.UI.Shared.Routing;
+using Artemis.WebClient.Workshop;
+using PropertyChanged.SourceGenerator;
+
+namespace Artemis.UI.Screens.Workshop.Layout;
+
+public partial class LayoutDescriptionViewModel : RoutableScreen
+{
+ [Notify] private IEntryDetails? _entry;
+}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/Layout/LayoutDetailsView.axaml b/src/Artemis.UI/Screens/Workshop/Layout/LayoutDetailsView.axaml
index 3dd205871..7f021d50f 100644
--- a/src/Artemis.UI/Screens/Workshop/Layout/LayoutDetailsView.axaml
+++ b/src/Artemis.UI/Screens/Workshop/Layout/LayoutDetailsView.axaml
@@ -3,7 +3,8 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:layout="clr-namespace:Artemis.UI.Screens.Workshop.Layout"
- xmlns:mdxaml="https://github.com/whistyun/Markdown.Avalonia.Tight"
+ xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
+ xmlns:ui="clr-namespace:Artemis.UI"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="800"
x:Class="Artemis.UI.Screens.Workshop.Layout.LayoutDetailsView"
x:DataType="layout:LayoutDetailsViewModel">
@@ -12,21 +13,17 @@
-
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
diff --git a/src/Artemis.UI/Screens/Workshop/Layout/LayoutDetailsView.axaml.cs b/src/Artemis.UI/Screens/Workshop/Layout/LayoutDetailsView.axaml.cs
index 57e93b1e1..c97e73859 100644
--- a/src/Artemis.UI/Screens/Workshop/Layout/LayoutDetailsView.axaml.cs
+++ b/src/Artemis.UI/Screens/Workshop/Layout/LayoutDetailsView.axaml.cs
@@ -1,4 +1,7 @@
+using System;
+using System.Reactive.Disposables;
using Avalonia.ReactiveUI;
+using ReactiveUI;
namespace Artemis.UI.Screens.Workshop.Layout;
@@ -7,5 +10,9 @@ public partial class LayoutDetailsView : ReactiveUserControl ViewModel.WhenAnyValue(vm => vm.Screen)
+ .WhereNotNull()
+ .Subscribe(screen => RouterFrame.NavigateFromObject(screen))
+ .DisposeWith(d));
}
}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/Layout/LayoutDetailsViewModel.cs b/src/Artemis.UI/Screens/Workshop/Layout/LayoutDetailsViewModel.cs
index 9663f5ad4..7379c1217 100644
--- a/src/Artemis.UI/Screens/Workshop/Layout/LayoutDetailsViewModel.cs
+++ b/src/Artemis.UI/Screens/Workshop/Layout/LayoutDetailsViewModel.cs
@@ -1,87 +1,64 @@
using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
using System.Threading;
using System.Threading.Tasks;
-using Artemis.Core;
-using Artemis.Core.Services;
using Artemis.UI.Screens.Workshop.Entries.Details;
-using Artemis.UI.Screens.Workshop.Layout.Dialogs;
+using Artemis.UI.Screens.Workshop.EntryReleases;
using Artemis.UI.Screens.Workshop.Parameters;
using Artemis.UI.Shared.Routing;
-using Artemis.UI.Shared.Services;
using Artemis.WebClient.Workshop;
-using Artemis.WebClient.Workshop.Models;
-using Artemis.WebClient.Workshop.Services;
using PropertyChanged.SourceGenerator;
using StrawberryShake;
namespace Artemis.UI.Screens.Workshop.Layout;
-public partial class LayoutDetailsViewModel : RoutableScreen
+public partial class LayoutDetailsViewModel : RoutableHostScreen
{
private readonly IWorkshopClient _client;
- private readonly IDeviceService _deviceService;
- private readonly IWindowService _windowService;
- private readonly Func _getEntryInfoViewModel;
+ private readonly LayoutDescriptionViewModel _layoutDescriptionViewModel;
private readonly Func _getEntryReleasesViewModel;
private readonly Func _getEntryImagesViewModel;
[Notify] private IEntryDetails? _entry;
- [Notify] private EntryInfoViewModel? _entryInfoViewModel;
[Notify] private EntryReleasesViewModel? _entryReleasesViewModel;
[Notify] private EntryImagesViewModel? _entryImagesViewModel;
public LayoutDetailsViewModel(IWorkshopClient client,
- IDeviceService deviceService,
- IWindowService windowService,
- Func getEntryInfoViewModel,
+ LayoutDescriptionViewModel layoutDescriptionViewModel,
+ EntryInfoViewModel entryInfoViewModel,
Func getEntryReleasesViewModel,
Func getEntryImagesViewModel)
{
_client = client;
- _deviceService = deviceService;
- _windowService = windowService;
- _getEntryInfoViewModel = getEntryInfoViewModel;
+ _layoutDescriptionViewModel = layoutDescriptionViewModel;
_getEntryReleasesViewModel = getEntryReleasesViewModel;
_getEntryImagesViewModel = getEntryImagesViewModel;
+
+ RecycleScreen = false;
+ EntryInfoViewModel = entryInfoViewModel;
}
+ public override RoutableScreen DefaultScreen => _layoutDescriptionViewModel;
+ public EntryInfoViewModel EntryInfoViewModel { get; }
+
public override async Task OnNavigating(WorkshopDetailParameters parameters, NavigationArguments args, CancellationToken cancellationToken)
{
- await GetEntry(parameters.EntryId, cancellationToken);
+ if (Entry?.Id != parameters.EntryId)
+ await GetEntry(parameters.EntryId, cancellationToken);
}
private async Task GetEntry(long entryId, CancellationToken cancellationToken)
{
+ Task grace = Task.Delay(300, cancellationToken);
IOperationResult result = await _client.GetEntryById.ExecuteAsync(entryId, cancellationToken);
if (result.IsErrorResult())
return;
+
+ // Let the UI settle to avoid lag when deep linking
+ await grace;
Entry = result.Data?.Entry;
- EntryInfoViewModel = Entry != null ? _getEntryInfoViewModel(Entry) : null;
+ EntryInfoViewModel.SetEntry(Entry);
EntryReleasesViewModel = Entry != null ? _getEntryReleasesViewModel(Entry) : null;
EntryImagesViewModel = Entry != null ? _getEntryImagesViewModel(Entry) : null;
-
- if (EntryReleasesViewModel != null)
- EntryReleasesViewModel.OnInstallationFinished = OnInstallationFinished;
- }
-
- private async Task OnInstallationFinished(InstalledEntry installedEntry)
- {
- // Find compatible devices
- ArtemisLayout layout = new(Path.Combine(installedEntry.GetReleaseDirectory().FullName, "layout.xml"));
- List devices = _deviceService.Devices.Where(d => d.RgbDevice.DeviceInfo.DeviceType == layout.RgbLayout.Type).ToList();
-
- // If any are found, offer to apply
- if (devices.Any())
- {
- await _windowService.CreateContentDialog()
- .WithTitle("Apply layout to devices")
- .WithViewModel(out DeviceSelectionDialogViewModel vm, devices, installedEntry)
- .WithCloseButtonText(null)
- .HavingPrimaryButton(b => b.WithText("Continue").WithCommand(vm.Apply))
- .ShowAsync();
- }
+ _layoutDescriptionViewModel.Entry = Entry;
}
}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/Layout/LayoutListView.axaml b/src/Artemis.UI/Screens/Workshop/Layout/LayoutListView.axaml
new file mode 100644
index 000000000..6c1b6e8ae
--- /dev/null
+++ b/src/Artemis.UI/Screens/Workshop/Layout/LayoutListView.axaml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/Layout/LayoutListView.axaml.cs b/src/Artemis.UI/Screens/Workshop/Layout/LayoutListView.axaml.cs
new file mode 100644
index 000000000..cb60c4707
--- /dev/null
+++ b/src/Artemis.UI/Screens/Workshop/Layout/LayoutListView.axaml.cs
@@ -0,0 +1,20 @@
+using System;
+using System.Reactive.Disposables;
+using Artemis.UI.Shared.Routing;
+using Avalonia.ReactiveUI;
+using Avalonia.Threading;
+using ReactiveUI;
+
+namespace Artemis.UI.Screens.Workshop.Layout;
+
+public partial class LayoutListView : ReactiveUserControl
+{
+ public LayoutListView()
+ {
+ InitializeComponent();
+ this.WhenActivated(d => ViewModel.WhenAnyValue(vm => vm.Screen)
+ .WhereNotNull()
+ .Subscribe(screen => RouterFrame.NavigateFromObject(screen))
+ .DisposeWith(d));
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/Layout/LayoutListViewModel.cs b/src/Artemis.UI/Screens/Workshop/Layout/LayoutListViewModel.cs
new file mode 100644
index 000000000..a6165a585
--- /dev/null
+++ b/src/Artemis.UI/Screens/Workshop/Layout/LayoutListViewModel.cs
@@ -0,0 +1,17 @@
+using Artemis.UI.Screens.Workshop.Entries.List;
+using Artemis.UI.Shared.Routing;
+using Artemis.WebClient.Workshop;
+
+namespace Artemis.UI.Screens.Workshop.Layout;
+
+public class LayoutListViewModel : RoutableHostScreen
+{
+ private readonly EntryListViewModel _entryListViewModel;
+ public override RoutableScreen DefaultScreen => _entryListViewModel;
+
+ public LayoutListViewModel(EntryListViewModel entryListViewModel)
+ {
+ _entryListViewModel = entryListViewModel;
+ _entryListViewModel.EntryType = EntryType.Layout;
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/Layout/LayoutManageView.axaml b/src/Artemis.UI/Screens/Workshop/Layout/LayoutManageView.axaml
new file mode 100644
index 000000000..13be66d55
--- /dev/null
+++ b/src/Artemis.UI/Screens/Workshop/Layout/LayoutManageView.axaml
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+ Manage layout
+
+
+
+
+
+ This layout is made for devices of type
+ .
+ Unfortunately, none were detected.
+
+
+
+ Select the devices on which you would like to apply the downloaded layout.
+
+
+
+
+
+
+
+
+
+
+
+ Apply
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/Layout/LayoutManageView.axaml.cs b/src/Artemis.UI/Screens/Workshop/Layout/LayoutManageView.axaml.cs
new file mode 100644
index 000000000..bc26260c9
--- /dev/null
+++ b/src/Artemis.UI/Screens/Workshop/Layout/LayoutManageView.axaml.cs
@@ -0,0 +1,14 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using Avalonia.ReactiveUI;
+
+namespace Artemis.UI.Screens.Workshop.Layout;
+
+public partial class LayoutManageView : ReactiveUserControl
+{
+ public LayoutManageView()
+ {
+ InitializeComponent();
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/Layout/LayoutManageViewModel.cs b/src/Artemis.UI/Screens/Workshop/Layout/LayoutManageViewModel.cs
new file mode 100644
index 000000000..9ce907d57
--- /dev/null
+++ b/src/Artemis.UI/Screens/Workshop/Layout/LayoutManageViewModel.cs
@@ -0,0 +1,103 @@
+using System.Collections.ObjectModel;
+using System.IO;
+using System.Linq;
+using System.Reactive;
+using System.Threading;
+using System.Threading.Tasks;
+using Artemis.Core;
+using Artemis.Core.Services;
+using Artemis.UI.DryIoc.Factories;
+using Artemis.UI.Screens.SurfaceEditor;
+using Artemis.UI.Screens.Workshop.Parameters;
+using Artemis.UI.Shared.Routing;
+using Artemis.UI.Shared.Services;
+using Artemis.WebClient.Workshop.Models;
+using Artemis.WebClient.Workshop.Providers;
+using Artemis.WebClient.Workshop.Services;
+using Avalonia.Threading;
+using PropertyChanged.SourceGenerator;
+using ReactiveUI;
+
+namespace Artemis.UI.Screens.Workshop.Layout;
+
+public partial class LayoutManageViewModel : RoutableScreen
+{
+ private readonly ISurfaceVmFactory _surfaceVmFactory;
+ private readonly IRouter _router;
+ private readonly IWorkshopService _workshopService;
+ private readonly IDeviceService _deviceService;
+ private readonly WorkshopLayoutProvider _layoutProvider;
+ private readonly IWindowService _windowService;
+ [Notify] private ArtemisLayout? _layout;
+ [Notify] private InstalledEntry? _entry;
+ [Notify] private ObservableCollection? _devices;
+
+ public LayoutManageViewModel(ISurfaceVmFactory surfaceVmFactory,
+ IRouter router,
+ IWorkshopService workshopService,
+ IDeviceService deviceService,
+ WorkshopLayoutProvider layoutProvider,
+ IWindowService windowService)
+ {
+ _surfaceVmFactory = surfaceVmFactory;
+ _router = router;
+ _workshopService = workshopService;
+ _deviceService = deviceService;
+ _layoutProvider = layoutProvider;
+ _windowService = windowService;
+ Apply = ReactiveCommand.Create(ExecuteApply);
+ ParameterSource = ParameterSource.Route;
+ }
+
+ public ReactiveCommand Apply { get; }
+
+ public async Task Close()
+ {
+ await _router.GoUp();
+ }
+
+ public override async Task OnNavigating(WorkshopDetailParameters parameters, NavigationArguments args, CancellationToken cancellationToken)
+ {
+ InstalledEntry? installedEntry = _workshopService.GetInstalledEntry(parameters.EntryId);
+ if (installedEntry == null)
+ {
+ // TODO: Fix cancelling without this workaround, currently navigation is stopped but the page still opens
+ Dispatcher.UIThread.InvokeAsync(async () =>
+ {
+ await _windowService.ShowConfirmContentDialog("Entry not found", "The entry you're trying to manage could not be found.", "Go back", null);
+ await Close();
+ });
+ return;
+ }
+
+ Layout = new ArtemisLayout(Path.Combine(installedEntry.GetReleaseDirectory().FullName, "layout.xml"));
+ if (!Layout.IsValid)
+ {
+ // TODO: Fix cancelling without this workaround, currently navigation is stopped but the page still opens
+ Dispatcher.UIThread.InvokeAsync(async () =>
+ {
+ await _windowService.ShowConfirmContentDialog("Invalid layout", "The layout of the entry you're trying to manage is invalid.", "Go back", null);
+ await Close();
+ });
+ return;
+ }
+
+ Entry = installedEntry;
+ Devices = new ObservableCollection(_deviceService.Devices
+ .Where(d => d.RgbDevice.DeviceInfo.DeviceType == Layout.RgbLayout.Type)
+ .Select(_surfaceVmFactory.ListDeviceViewModel));
+ }
+
+ private void ExecuteApply()
+ {
+ if (Devices == null)
+ return;
+
+ foreach (ListDeviceViewModel listDeviceViewModel in Devices.Where(d => d.IsSelected))
+ {
+ _layoutProvider.ConfigureDevice(listDeviceViewModel.Device, Entry);
+ _deviceService.SaveDevice(listDeviceViewModel.Device);
+ _deviceService.LoadDeviceLayout(listDeviceViewModel.Device);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/Library/SubmissionDetailView.axaml b/src/Artemis.UI/Screens/Workshop/Library/SubmissionDetailView.axaml
deleted file mode 100644
index dbb99d606..000000000
--- a/src/Artemis.UI/Screens/Workshop/Library/SubmissionDetailView.axaml
+++ /dev/null
@@ -1,85 +0,0 @@
-
-
-
-
-
-
-
-
- Management
-
-
-
-
-
- downloads
-
-
-
-
- Created
-
-
-
-
-
-
-
- Create new release
-
-
- Delete submission
-
-
-
-
-
- View workshop page
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Add image
-
-
-
-
- Discard changes
- Save
-
-
-
-
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/Library/SubmissionDetailView.axaml.cs b/src/Artemis.UI/Screens/Workshop/Library/SubmissionDetailView.axaml.cs
deleted file mode 100644
index 729f02a2b..000000000
--- a/src/Artemis.UI/Screens/Workshop/Library/SubmissionDetailView.axaml.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-using Avalonia.ReactiveUI;
-
-namespace Artemis.UI.Screens.Workshop.Library;
-
-public partial class SubmissionDetailView : ReactiveUserControl
-{
- public SubmissionDetailView()
- {
- InitializeComponent();
- }
-}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/Library/SubmissionDetailsView.axaml b/src/Artemis.UI/Screens/Workshop/Library/SubmissionDetailsView.axaml
new file mode 100644
index 000000000..538b24b83
--- /dev/null
+++ b/src/Artemis.UI/Screens/Workshop/Library/SubmissionDetailsView.axaml
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Add image
+
+
+
+
+ Discard changes
+ Save
+
+
+
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/Library/SubmissionDetailsView.axaml.cs b/src/Artemis.UI/Screens/Workshop/Library/SubmissionDetailsView.axaml.cs
new file mode 100644
index 000000000..77a1f67ec
--- /dev/null
+++ b/src/Artemis.UI/Screens/Workshop/Library/SubmissionDetailsView.axaml.cs
@@ -0,0 +1,14 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using Avalonia.ReactiveUI;
+
+namespace Artemis.UI.Screens.Workshop.Library;
+
+public partial class SubmissionDetailsView : ReactiveUserControl
+{
+ public SubmissionDetailsView()
+ {
+ InitializeComponent();
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/Library/SubmissionDetailViewModel.cs b/src/Artemis.UI/Screens/Workshop/Library/SubmissionDetailsViewModel.cs
similarity index 82%
rename from src/Artemis.UI/Screens/Workshop/Library/SubmissionDetailViewModel.cs
rename to src/Artemis.UI/Screens/Workshop/Library/SubmissionDetailsViewModel.cs
index 839511a5d..7829b7261 100644
--- a/src/Artemis.UI/Screens/Workshop/Library/SubmissionDetailViewModel.cs
+++ b/src/Artemis.UI/Screens/Workshop/Library/SubmissionDetailsViewModel.cs
@@ -8,8 +8,7 @@ using System.Reactive;
using System.Threading;
using System.Threading.Tasks;
using Artemis.UI.Screens.Workshop.Image;
-using Artemis.UI.Screens.Workshop.Parameters;
-using Artemis.UI.Screens.Workshop.SubmissionWizard;
+using Artemis.UI.Shared;
using Artemis.UI.Shared.Routing;
using Artemis.UI.Shared.Services;
using Artemis.WebClient.Workshop;
@@ -25,7 +24,7 @@ using EntrySpecificationsViewModel = Artemis.UI.Screens.Workshop.Entries.Details
namespace Artemis.UI.Screens.Workshop.Library;
-public partial class SubmissionDetailViewModel : RoutableScreen
+public partial class SubmissionDetailsViewModel : RoutableScreen
{
private readonly IWorkshopClient _client;
private readonly IWindowService _windowService;
@@ -40,7 +39,7 @@ public partial class SubmissionDetailViewModel : RoutableScreen vm.HasChanges));
SaveChanges = ReactiveCommand.CreateFromTask(ExecuteSaveChanges, this.WhenAnyValue(vm => vm.HasChanges));
}
-
+
public ObservableCollection Images { get; } = new();
- public ReactiveCommand CreateRelease { get; }
- public ReactiveCommand DeleteSubmission { get; }
- public ReactiveCommand ViewWorkshopPage { get; }
public ReactiveCommand AddImage { get; }
public ReactiveCommand SaveChanges { get; }
public ReactiveCommand DiscardChanges { get; }
- public override async Task OnNavigating(WorkshopDetailParameters parameters, NavigationArguments args, CancellationToken cancellationToken)
+ public async Task SetEntry(IGetSubmittedEntryById_Entry? entry, CancellationToken cancellationToken)
{
- IOperationResult result = await _client.GetSubmittedEntryById.ExecuteAsync(parameters.EntryId, cancellationToken);
- if (result.IsErrorResult())
- return;
-
- Entry = result.Data?.Entry;
+ Entry = entry;
await ApplyDetailsFromEntry(cancellationToken);
- ApplyImagesFromEntry();
}
- public override async Task OnClosing(NavigationArguments args)
+ public async Task OnClosing(NavigationArguments args)
{
if (!HasChanges)
return;
@@ -91,6 +79,8 @@ public partial class SubmissionDetailViewModel : RoutableScreen(Entry);
- }
-
- private async Task ExecuteDeleteSubmission(CancellationToken cancellationToken)
- {
- if (Entry == null)
- return;
-
- bool confirmed = await _windowService.ShowConfirmContentDialog(
- "Delete submission?",
- "You cannot undo this by yourself.\r\n" +
- "Users that have already downloaded your submission will keep it.");
- if (!confirmed)
- return;
-
- IOperationResult result = await _client.RemoveEntry.ExecuteAsync(Entry.Id, cancellationToken);
- result.EnsureNoErrors();
- await _router.Navigate("workshop/library/submissions");
- }
-
+
private async Task ExecuteAddImage(CancellationToken arg)
{
string[]? result = await _windowService.CreateOpenFileDialog().WithAllowMultiple().HavingFilter(f => f.WithBitmaps()).ShowAsync();
@@ -297,12 +264,6 @@ public partial class SubmissionDetailViewModel : RoutableScreen
+
+
+
+
+
+
+
+
+
+ Management
+
+
+
+
+
+ downloads
+
+
+
+
+ Created
+
+
+
+
+
+
+
+ Create new release
+
+
+ Delete submission
+
+
+
+
+
+
+ Releases
+
+
+
+
+
+
+
+
+
+ Created
+
+
+
+
+
+
+
+
+
+ View workshop page
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/Library/SubmissionManagementView.axaml.cs b/src/Artemis.UI/Screens/Workshop/Library/SubmissionManagementView.axaml.cs
new file mode 100644
index 000000000..3e14dacb8
--- /dev/null
+++ b/src/Artemis.UI/Screens/Workshop/Library/SubmissionManagementView.axaml.cs
@@ -0,0 +1,18 @@
+using System;
+using System.Reactive.Disposables;
+using Avalonia.ReactiveUI;
+using ReactiveUI;
+
+namespace Artemis.UI.Screens.Workshop.Library;
+
+public partial class SubmissionManagementView : ReactiveUserControl
+{
+ public SubmissionManagementView()
+ {
+ InitializeComponent();
+ this.WhenActivated(d => ViewModel.WhenAnyValue(vm => vm.Screen)
+ .WhereNotNull()
+ .Subscribe(screen => RouterFrame.NavigateFromObject(screen))
+ .DisposeWith(d));
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/Library/SubmissionManagementViewModel.cs b/src/Artemis.UI/Screens/Workshop/Library/SubmissionManagementViewModel.cs
new file mode 100644
index 000000000..2f7a67147
--- /dev/null
+++ b/src/Artemis.UI/Screens/Workshop/Library/SubmissionManagementViewModel.cs
@@ -0,0 +1,111 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reactive.Disposables;
+using System.Threading;
+using System.Threading.Tasks;
+using Artemis.UI.Screens.Workshop.Parameters;
+using Artemis.UI.Screens.Workshop.SubmissionWizard;
+using Artemis.UI.Shared.Routing;
+using Artemis.UI.Shared.Services;
+using Artemis.WebClient.Workshop;
+using Artemis.WebClient.Workshop.Services;
+using PropertyChanged.SourceGenerator;
+using ReactiveUI;
+using StrawberryShake;
+
+namespace Artemis.UI.Screens.Workshop.Library;
+
+public partial class SubmissionManagementViewModel : RoutableHostScreen
+{
+ private readonly IWorkshopClient _client;
+ private readonly IWindowService _windowService;
+ private readonly IRouter _router;
+ private readonly IWorkshopService _workshopService;
+ private readonly SubmissionDetailsViewModel _detailsViewModel;
+
+ [Notify] private IGetSubmittedEntryById_Entry? _entry;
+ [Notify] private List? _releases;
+ [Notify] private IGetSubmittedEntryById_Entry_Releases? _selectedRelease;
+
+ public SubmissionManagementViewModel(IWorkshopClient client, IRouter router, IWindowService windowService, IWorkshopService workshopService, SubmissionDetailsViewModel detailsViewModel)
+ {
+ _detailsViewModel = detailsViewModel;
+ _client = client;
+ _router = router;
+ _windowService = windowService;
+ _workshopService = workshopService;
+
+ RecycleScreen = false;
+
+ this.WhenActivated(d =>
+ {
+ this.WhenAnyValue(vm => vm.SelectedRelease)
+ .WhereNotNull()
+ .Subscribe(r => _router.Navigate($"workshop/library/submissions/{Entry?.Id}/releases/{r.Id}"))
+ .DisposeWith(d);
+ });
+ }
+
+ public override RoutableScreen DefaultScreen => _detailsViewModel;
+
+ public async Task ViewWorkshopPage()
+ {
+ if (Entry != null)
+ await _workshopService.NavigateToEntry(Entry.Id, Entry.EntryType);
+ }
+
+ public async Task CreateRelease()
+ {
+ if (Entry != null)
+ await _windowService.ShowDialogAsync(Entry);
+ }
+
+ public async Task DeleteSubmission()
+ {
+ if (Entry == null)
+ return;
+
+ bool confirmed = await _windowService.ShowConfirmContentDialog(
+ "Delete submission?",
+ "You cannot undo this by yourself.\r\n" +
+ "Users that have already downloaded your submission will keep it.");
+ if (!confirmed)
+ return;
+
+ IOperationResult result = await _client.RemoveEntry.ExecuteAsync(Entry.Id);
+ result.EnsureNoErrors();
+ await _router.Navigate("workshop/library/submissions");
+ }
+
+ public override async Task OnNavigating(WorkshopDetailParameters parameters, NavigationArguments args, CancellationToken cancellationToken)
+ {
+ // If there is a 2nd parameter, it's a release ID
+ SelectedRelease = args.RouteParameters.Length > 1 ? Releases?.FirstOrDefault(r => r.Id == (long) args.RouteParameters[1]) : null;
+
+ // OnNavigating may just be getting called to update the selected release
+ if (Entry?.Id == parameters.EntryId)
+ {
+ // Reapply the entry when closing a release, this is mainly because the entry icon probably got disposed
+ if (SelectedRelease == null)
+ await _detailsViewModel.SetEntry(Entry, cancellationToken);
+
+ // No need to reload the entry since it's the same
+ return;
+ }
+
+ IOperationResult result = await _client.GetSubmittedEntryById.ExecuteAsync(parameters.EntryId, cancellationToken);
+ if (result.IsErrorResult())
+ return;
+
+ Entry = result.Data?.Entry;
+ Releases = Entry?.Releases.OrderByDescending(r => r.CreatedAt).ToList();
+
+ await _detailsViewModel.SetEntry(Entry, cancellationToken);
+ }
+
+ public override async Task OnClosing(NavigationArguments args)
+ {
+ await _detailsViewModel.OnClosing(args);
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/Library/SubmissionReleaseView.axaml b/src/Artemis.UI/Screens/Workshop/Library/SubmissionReleaseView.axaml
new file mode 100644
index 000000000..2040309d0
--- /dev/null
+++ b/src/Artemis.UI/Screens/Workshop/Library/SubmissionReleaseView.axaml
@@ -0,0 +1,99 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Release management
+ Delete release
+
+
+
+
+
+ Version
+
+
+
+
+
+ Release date
+
+
+
+
+
+ File size
+
+
+
+
+
+
+
+
+
+ Release notes
+
+
+
+
+
+
+
+ Discard changes
+ Save
+
+
+
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/Library/SubmissionReleaseView.axaml.cs b/src/Artemis.UI/Screens/Workshop/Library/SubmissionReleaseView.axaml.cs
new file mode 100644
index 000000000..de58a90f6
--- /dev/null
+++ b/src/Artemis.UI/Screens/Workshop/Library/SubmissionReleaseView.axaml.cs
@@ -0,0 +1,11 @@
+using Avalonia.ReactiveUI;
+
+namespace Artemis.UI.Screens.Workshop.Library;
+
+public partial class SubmissionReleaseView : ReactiveUserControl
+{
+ public SubmissionReleaseView()
+ {
+ InitializeComponent();
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/Library/SubmissionReleaseViewModel.cs b/src/Artemis.UI/Screens/Workshop/Library/SubmissionReleaseViewModel.cs
new file mode 100644
index 000000000..025d2ce5a
--- /dev/null
+++ b/src/Artemis.UI/Screens/Workshop/Library/SubmissionReleaseViewModel.cs
@@ -0,0 +1,110 @@
+using System;
+using System.Reactive;
+using System.Reactive.Disposables;
+using System.Threading;
+using System.Threading.Tasks;
+using Artemis.UI.Screens.Workshop.Parameters;
+using Artemis.UI.Shared.Routing;
+using Artemis.UI.Shared.Services;
+using Artemis.UI.Shared.Services.Builders;
+using Artemis.WebClient.Workshop;
+using Avalonia.Layout;
+using AvaloniaEdit.Document;
+using PropertyChanged.SourceGenerator;
+using ReactiveUI;
+using StrawberryShake;
+
+namespace Artemis.UI.Screens.Workshop.Library;
+
+public partial class SubmissionReleaseViewModel : RoutableScreen
+{
+ private readonly IWorkshopClient _client;
+ private readonly IRouter _router;
+ private readonly IWindowService _windowService;
+ private readonly INotificationService _notificationService;
+
+ [Notify] private IGetReleaseById_Release? _release;
+ [Notify] private string? _changelog;
+ [Notify] private bool _hasChanges;
+
+ public SubmissionReleaseViewModel(IWorkshopClient client, IRouter router, IWindowService windowService, INotificationService notificationService)
+ {
+ _client = client;
+ _router = router;
+ _windowService = windowService;
+ _notificationService = notificationService;
+ this.WhenAnyValue(vm => vm.Changelog, vm => vm.Release, (current, release) => current != release?.Changelog).Subscribe(hasChanges => HasChanges = hasChanges);
+
+ Discard = ReactiveCommand.Create(ExecuteDiscard, this.WhenAnyValue(vm => vm.HasChanges));
+ Save = ReactiveCommand.CreateFromTask(ExecuteSave, this.WhenAnyValue(vm => vm.HasChanges));
+ }
+
+ public ReactiveCommand Discard { get; set; }
+ public ReactiveCommand Save { get; set; }
+
+ public override async Task OnNavigating(ReleaseDetailParameters parameters, NavigationArguments args, CancellationToken cancellationToken)
+ {
+ IOperationResult result = await _client.GetReleaseById.ExecuteAsync(parameters.ReleaseId, cancellationToken);
+ Release = result.Data?.Release;
+ Changelog = Release?.Changelog;
+ }
+
+ public override async Task OnClosing(NavigationArguments args)
+ {
+ if (!HasChanges)
+ return;
+
+ bool confirmed = await _windowService.ShowConfirmContentDialog("You have unsaved changes", "Do you want to discard your unsaved changes?");
+ if (!confirmed)
+ args.Cancel();
+ }
+
+ public async Task DeleteRelease()
+ {
+ if (Release == null)
+ return;
+
+ bool confirmed = await _windowService.ShowConfirmContentDialog(
+ "Delete release?",
+ "This cannot be undone.\r\n" +
+ "Users that have already downloaded this release will keep it.");
+ if (!confirmed)
+ return;
+
+ await _client.RemoveRelease.ExecuteAsync(Release.Id);
+ _notificationService.CreateNotification()
+ .WithTitle("Deleted release.")
+ .WithSeverity(NotificationSeverity.Success)
+ .WithHorizontalPosition(HorizontalAlignment.Left)
+ .Show();
+
+ HasChanges = false;
+ await Close();
+ }
+
+ public async Task Close()
+ {
+ await _router.GoUp();
+ }
+
+ private async Task ExecuteSave(CancellationToken cancellationToken)
+ {
+ if (Release == null)
+ return;
+
+ await _client.UpdateRelease.ExecuteAsync(new UpdateReleaseInput {Id = Release.Id, Changelog = Changelog}, cancellationToken);
+ _notificationService.CreateNotification()
+ .WithTitle("Saved changelog.")
+ .WithSeverity(NotificationSeverity.Success)
+ .WithHorizontalPosition(HorizontalAlignment.Left)
+ .Show();
+
+ HasChanges = false;
+ }
+
+ private void ExecuteDiscard()
+ {
+ Changelog = Release?.Changelog;
+ HasChanges = false;
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/Library/WorkshopLibraryVIew.axaml.cs b/src/Artemis.UI/Screens/Workshop/Library/WorkshopLibraryVIew.axaml.cs
index 7758b5729..5fa303000 100644
--- a/src/Artemis.UI/Screens/Workshop/Library/WorkshopLibraryVIew.axaml.cs
+++ b/src/Artemis.UI/Screens/Workshop/Library/WorkshopLibraryVIew.axaml.cs
@@ -18,9 +18,9 @@ public partial class WorkshopLibraryView : ReactiveUserControl TabFrame.NavigateFromObject(viewModel));
+ TabFrame.NavigateFromObject(viewModel);
}
-
+
private void NavigationView_OnBackRequested(object? sender, NavigationViewBackRequestedEventArgs e)
{
ViewModel?.GoBack();
diff --git a/src/Artemis.UI/Screens/Workshop/Library/WorkshopLibraryViewModel.cs b/src/Artemis.UI/Screens/Workshop/Library/WorkshopLibraryViewModel.cs
index 413914803..af02fa985 100644
--- a/src/Artemis.UI/Screens/Workshop/Library/WorkshopLibraryViewModel.cs
+++ b/src/Artemis.UI/Screens/Workshop/Library/WorkshopLibraryViewModel.cs
@@ -53,7 +53,7 @@ public partial class WorkshopLibraryViewModel : RoutableHostScreen
-
-
-
-
-
- Plugin features
-
-
-
-
-
diff --git a/src/Artemis.UI/Screens/Workshop/Plugins/Dialogs/PluginDialogView.axaml.cs b/src/Artemis.UI/Screens/Workshop/Plugins/Dialogs/PluginDialogView.axaml.cs
deleted file mode 100644
index 19329553e..000000000
--- a/src/Artemis.UI/Screens/Workshop/Plugins/Dialogs/PluginDialogView.axaml.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-using Avalonia.ReactiveUI;
-
-namespace Artemis.UI.Screens.Workshop.Plugins.Dialogs;
-
-public partial class PluginDialogView : ReactiveUserControl
-{
- public PluginDialogView()
- {
- InitializeComponent();
- }
-}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/Plugins/Dialogs/PluginDialogViewModel.cs b/src/Artemis.UI/Screens/Workshop/Plugins/Dialogs/PluginDialogViewModel.cs
deleted file mode 100644
index 863417326..000000000
--- a/src/Artemis.UI/Screens/Workshop/Plugins/Dialogs/PluginDialogViewModel.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-using System.Collections.ObjectModel;
-using System.Linq;
-using System.Reactive.Linq;
-using Artemis.Core;
-using Artemis.UI.DryIoc.Factories;
-using Artemis.UI.Screens.Plugins;
-using Artemis.UI.Screens.Plugins.Features;
-using Artemis.UI.Shared;
-using ReactiveUI;
-
-namespace Artemis.UI.Screens.Workshop.Plugins.Dialogs;
-
-public class PluginDialogViewModel : ContentDialogViewModelBase
-{
- public PluginDialogViewModel(Plugin plugin, ISettingsVmFactory settingsVmFactory)
- {
- PluginViewModel = settingsVmFactory.PluginViewModel(plugin, ReactiveCommand.Create(() => {}, Observable.Empty()));
- PluginFeatures = new ObservableCollection(plugin.Features.Select(f => settingsVmFactory.PluginFeatureViewModel(f, false)));
- }
-
- public PluginViewModel PluginViewModel { get; }
- public ObservableCollection PluginFeatures { get; }
-}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/Plugins/PluginDescriptionView.axaml b/src/Artemis.UI/Screens/Workshop/Plugins/PluginDescriptionView.axaml
new file mode 100644
index 000000000..24c91abf8
--- /dev/null
+++ b/src/Artemis.UI/Screens/Workshop/Plugins/PluginDescriptionView.axaml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ Used by these profiles
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/Plugins/PluginDescriptionView.axaml.cs b/src/Artemis.UI/Screens/Workshop/Plugins/PluginDescriptionView.axaml.cs
new file mode 100644
index 000000000..bffef29b2
--- /dev/null
+++ b/src/Artemis.UI/Screens/Workshop/Plugins/PluginDescriptionView.axaml.cs
@@ -0,0 +1,14 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using Avalonia.ReactiveUI;
+
+namespace Artemis.UI.Screens.Workshop.Plugins;
+
+public partial class PluginDescriptionView : ReactiveUserControl
+{
+ public PluginDescriptionView()
+ {
+ InitializeComponent();
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/Plugins/PluginDescriptionViewModel.cs b/src/Artemis.UI/Screens/Workshop/Plugins/PluginDescriptionViewModel.cs
new file mode 100644
index 000000000..0cae2bbcd
--- /dev/null
+++ b/src/Artemis.UI/Screens/Workshop/Plugins/PluginDescriptionViewModel.cs
@@ -0,0 +1,41 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Artemis.UI.Screens.Workshop.Entries.List;
+using Artemis.UI.Shared.Routing;
+using Artemis.WebClient.Workshop;
+using PropertyChanged.SourceGenerator;
+
+namespace Artemis.UI.Screens.Workshop.Plugins;
+
+public partial class PluginDescriptionViewModel : RoutableScreen
+{
+ [Notify] private IEntryDetails? _entry;
+ [Notify] private List? _dependants;
+ private readonly IWorkshopClient _client;
+ private readonly Func _getEntryListViewModel;
+
+ public PluginDescriptionViewModel(IWorkshopClient client, Func getEntryListViewModel)
+ {
+ _client = client;
+ _getEntryListViewModel = getEntryListViewModel;
+ }
+
+ public async Task SetEntry(IEntryDetails? entry, CancellationToken cancellationToken)
+ {
+ Entry = entry;
+
+ if (entry != null)
+ {
+ IReadOnlyList? dependants = (await _client.GetDependantEntries.ExecuteAsync(entry.Id, 0, 25, cancellationToken)).Data?.Entries?.Items;
+ Dependants = dependants != null && dependants.Any() ? dependants.Select(_getEntryListViewModel).OrderByDescending(d => d.Entry.Downloads).Take(10).ToList() : null;
+ }
+ else
+ {
+ Dependants = null;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/Plugins/PluginDetailsView.axaml b/src/Artemis.UI/Screens/Workshop/Plugins/PluginDetailsView.axaml
index dcbb4f5d1..9b99c42c3 100644
--- a/src/Artemis.UI/Screens/Workshop/Plugins/PluginDetailsView.axaml
+++ b/src/Artemis.UI/Screens/Workshop/Plugins/PluginDetailsView.axaml
@@ -2,9 +2,10 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
- xmlns:mdxaml="https://github.com/whistyun/Markdown.Avalonia.Tight"
xmlns:plugins="clr-namespace:Artemis.UI.Screens.Workshop.Plugins"
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
+ xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
+ xmlns:ui="clr-namespace:Artemis.UI"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Workshop.Plugins.PluginDetailsView"
x:DataType="plugins:PluginDetailsViewModel">
@@ -15,47 +16,44 @@
-
- Admin required
-
-
-
- Supported platforms
-
-
-
-
+
+
+
+
+
+
+
-
+
+
+ Admin required
+
+
+
+ Supported platforms
+
+
+
+
+
+
+
+
-
+
-
-
-
-
-
-
-
-
-
-
-
- Used by these profiles
-
-
-
-
-
-
-
+
+
+
+
+
-
+
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/Plugins/PluginDetailsView.axaml.cs b/src/Artemis.UI/Screens/Workshop/Plugins/PluginDetailsView.axaml.cs
index c50ffbe89..fe248c549 100644
--- a/src/Artemis.UI/Screens/Workshop/Plugins/PluginDetailsView.axaml.cs
+++ b/src/Artemis.UI/Screens/Workshop/Plugins/PluginDetailsView.axaml.cs
@@ -1,4 +1,7 @@
+using System;
+using System.Reactive.Disposables;
using Avalonia.ReactiveUI;
+using ReactiveUI;
namespace Artemis.UI.Screens.Workshop.Plugins;
@@ -7,5 +10,9 @@ public partial class PluginDetailsView : ReactiveUserControl ViewModel.WhenAnyValue(vm => vm.Screen)
+ .WhereNotNull()
+ .Subscribe(screen => RouterFrame.NavigateFromObject(screen))
+ .DisposeWith(d));
}
}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/Plugins/PluginDetailsViewModel.cs b/src/Artemis.UI/Screens/Workshop/Plugins/PluginDetailsViewModel.cs
index 64b556639..802698aab 100644
--- a/src/Artemis.UI/Screens/Workshop/Plugins/PluginDetailsViewModel.cs
+++ b/src/Artemis.UI/Screens/Workshop/Plugins/PluginDetailsViewModel.cs
@@ -1,104 +1,68 @@
using System;
-using System.Collections.Generic;
using System.Collections.ObjectModel;
-using System.Linq;
using System.Threading;
using System.Threading.Tasks;
-using Artemis.Core;
-using Artemis.Core.Services;
using Artemis.UI.Screens.Workshop.Entries.Details;
using Artemis.UI.Screens.Workshop.Entries.List;
+using Artemis.UI.Screens.Workshop.EntryReleases;
using Artemis.UI.Screens.Workshop.Parameters;
-using Artemis.UI.Screens.Workshop.Plugins.Dialogs;
using Artemis.UI.Shared.Routing;
-using Artemis.UI.Shared.Services;
using Artemis.WebClient.Workshop;
-using Artemis.WebClient.Workshop.Models;
using PropertyChanged.SourceGenerator;
using StrawberryShake;
namespace Artemis.UI.Screens.Workshop.Plugins;
-public partial class PluginDetailsViewModel : RoutableScreen
+public partial class PluginDetailsViewModel : RoutableHostScreen
{
private readonly IWorkshopClient _client;
- private readonly IWindowService _windowService;
- private readonly IPluginManagementService _pluginManagementService;
- private readonly Func _getEntryInfoViewModel;
+ private readonly PluginDescriptionViewModel _pluginDescriptionViewModel;
private readonly Func _getEntryReleasesViewModel;
private readonly Func _getEntryImagesViewModel;
- private readonly Func _getEntryListViewModel;
[Notify] private IGetPluginEntryById_Entry? _entry;
- [Notify] private EntryInfoViewModel? _entryInfoViewModel;
[Notify] private EntryReleasesViewModel? _entryReleasesViewModel;
[Notify] private EntryImagesViewModel? _entryImagesViewModel;
[Notify] private ReadOnlyObservableCollection? _dependants;
-
+
public PluginDetailsViewModel(IWorkshopClient client,
- IWindowService windowService,
- IPluginManagementService pluginManagementService,
- Func getEntryInfoViewModel,
+ PluginDescriptionViewModel pluginDescriptionViewModel,
+ EntryInfoViewModel entryInfoViewModel,
Func getEntryReleasesViewModel,
- Func getEntryImagesViewModel,
- Func getEntryListViewModel)
+ Func getEntryImagesViewModel)
{
_client = client;
- _windowService = windowService;
- _pluginManagementService = pluginManagementService;
- _getEntryInfoViewModel = getEntryInfoViewModel;
+ _pluginDescriptionViewModel = pluginDescriptionViewModel;
_getEntryReleasesViewModel = getEntryReleasesViewModel;
_getEntryImagesViewModel = getEntryImagesViewModel;
- _getEntryListViewModel = getEntryListViewModel;
+
+ EntryInfoViewModel = entryInfoViewModel;
+ RecycleScreen = false;
}
+ public override RoutableScreen DefaultScreen => _pluginDescriptionViewModel;
+ public EntryInfoViewModel EntryInfoViewModel { get; }
+
public override async Task OnNavigating(WorkshopDetailParameters parameters, NavigationArguments args, CancellationToken cancellationToken)
{
- await GetEntry(parameters.EntryId, cancellationToken);
+ if (Entry?.Id != parameters.EntryId)
+ await GetEntry(parameters.EntryId, cancellationToken);
}
private async Task GetEntry(long entryId, CancellationToken cancellationToken)
{
+ Task grace = Task.Delay(300, cancellationToken);
IOperationResult result = await _client.GetPluginEntryById.ExecuteAsync(entryId, cancellationToken);
if (result.IsErrorResult())
return;
+ // Let the UI settle to avoid lag when deep linking
+ await grace;
+
Entry = result.Data?.Entry;
- EntryInfoViewModel = Entry != null ? _getEntryInfoViewModel(Entry) : null;
+ EntryInfoViewModel.SetEntry(Entry);
EntryReleasesViewModel = Entry != null ? _getEntryReleasesViewModel(Entry) : null;
EntryImagesViewModel = Entry != null ? _getEntryImagesViewModel(Entry) : null;
- if (EntryReleasesViewModel != null)
- {
- EntryReleasesViewModel.OnInstallationStarted = OnInstallationStarted;
- EntryReleasesViewModel.OnInstallationFinished = OnInstallationFinished;
- }
-
- IReadOnlyList? dependants = (await _client.GetDependantEntries.ExecuteAsync(entryId, 0, 25, cancellationToken)).Data?.Entries?.Items;
- Dependants = dependants != null && dependants.Any()
- ? new ReadOnlyObservableCollection(new ObservableCollection(dependants.Select(_getEntryListViewModel)))
- : null;
- }
-
- private async Task OnInstallationStarted(IEntryDetails entryDetails)
- {
- bool confirm = await _windowService.ShowConfirmContentDialog(
- "Installing plugin",
- $"You are about to install version {entryDetails.LatestRelease?.Version} of {entryDetails.Name}. \r\n\r\n" +
- "Plugins are NOT verified by Artemis and could harm your PC, if you have doubts about a plugin please ask on Discord!",
- "I trust this plugin, install it"
- );
-
- return !confirm;
- }
-
- private async Task OnInstallationFinished(InstalledEntry installedEntry)
- {
- if (!installedEntry.TryGetMetadata("PluginId", out Guid pluginId))
- return;
- Plugin? plugin = _pluginManagementService.GetAllPlugins().FirstOrDefault(p => p.Guid == pluginId);
- if (plugin == null)
- return;
-
- await _windowService.CreateContentDialog().WithTitle("Manage plugin").WithViewModel(out PluginDialogViewModel _, plugin).WithFullScreen().ShowAsync();
+ await _pluginDescriptionViewModel.SetEntry(Entry, cancellationToken);
}
}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/Plugins/PluginListView.axaml b/src/Artemis.UI/Screens/Workshop/Plugins/PluginListView.axaml
new file mode 100644
index 000000000..da72fd3fe
--- /dev/null
+++ b/src/Artemis.UI/Screens/Workshop/Plugins/PluginListView.axaml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/Plugins/PluginListView.axaml.cs b/src/Artemis.UI/Screens/Workshop/Plugins/PluginListView.axaml.cs
new file mode 100644
index 000000000..f5d9d54c1
--- /dev/null
+++ b/src/Artemis.UI/Screens/Workshop/Plugins/PluginListView.axaml.cs
@@ -0,0 +1,18 @@
+using System;
+using System.Reactive.Disposables;
+using Avalonia.ReactiveUI;
+using ReactiveUI;
+
+namespace Artemis.UI.Screens.Workshop.Plugins;
+
+public partial class PluginListView : ReactiveUserControl
+{
+ public PluginListView()
+ {
+ InitializeComponent();
+ this.WhenActivated(d => ViewModel.WhenAnyValue(vm => vm.Screen)
+ .WhereNotNull()
+ .Subscribe(screen => RouterFrame.NavigateFromObject(screen))
+ .DisposeWith(d));
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/Plugins/PluginListViewModel.cs b/src/Artemis.UI/Screens/Workshop/Plugins/PluginListViewModel.cs
new file mode 100644
index 000000000..7401c2659
--- /dev/null
+++ b/src/Artemis.UI/Screens/Workshop/Plugins/PluginListViewModel.cs
@@ -0,0 +1,17 @@
+using Artemis.UI.Screens.Workshop.Entries.List;
+using Artemis.UI.Shared.Routing;
+using Artemis.WebClient.Workshop;
+
+namespace Artemis.UI.Screens.Workshop.Plugins;
+
+public class PluginListViewModel : RoutableHostScreen
+{
+ private readonly EntryListViewModel _entryListViewModel;
+ public override RoutableScreen DefaultScreen => _entryListViewModel;
+
+ public PluginListViewModel(EntryListViewModel entryListViewModel)
+ {
+ _entryListViewModel = entryListViewModel;
+ _entryListViewModel.EntryType = EntryType.Plugin;
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/Plugins/PluginManageView.axaml b/src/Artemis.UI/Screens/Workshop/Plugins/PluginManageView.axaml
new file mode 100644
index 000000000..cf84560af
--- /dev/null
+++ b/src/Artemis.UI/Screens/Workshop/Plugins/PluginManageView.axaml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+ Manage plugin
+
+
+
+
+
+
+
+
+
+ Plugin features
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/Plugins/PluginManageView.axaml.cs b/src/Artemis.UI/Screens/Workshop/Plugins/PluginManageView.axaml.cs
new file mode 100644
index 000000000..d5136c54e
--- /dev/null
+++ b/src/Artemis.UI/Screens/Workshop/Plugins/PluginManageView.axaml.cs
@@ -0,0 +1,14 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using Avalonia.ReactiveUI;
+
+namespace Artemis.UI.Screens.Workshop.Plugins;
+
+public partial class PluginManageView : ReactiveUserControl
+{
+ public PluginManageView()
+ {
+ InitializeComponent();
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/Plugins/PluginManageViewModel.cs b/src/Artemis.UI/Screens/Workshop/Plugins/PluginManageViewModel.cs
new file mode 100644
index 000000000..9bd558217
--- /dev/null
+++ b/src/Artemis.UI/Screens/Workshop/Plugins/PluginManageViewModel.cs
@@ -0,0 +1,77 @@
+using System;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Artemis.Core;
+using Artemis.Core.Services;
+using Artemis.UI.DryIoc.Factories;
+using Artemis.UI.Screens.Plugins;
+using Artemis.UI.Screens.Plugins.Features;
+using Artemis.UI.Screens.Workshop.Parameters;
+using Artemis.UI.Shared.Routing;
+using Artemis.UI.Shared.Services;
+using Artemis.WebClient.Workshop.Models;
+using Artemis.WebClient.Workshop.Services;
+using Avalonia.Threading;
+using PropertyChanged.SourceGenerator;
+using ReactiveUI;
+
+namespace Artemis.UI.Screens.Workshop.Plugins;
+
+public partial class PluginManageViewModel : RoutableScreen
+{
+ private readonly ISettingsVmFactory _settingsVmFactory;
+ private readonly IRouter _router;
+ private readonly IWorkshopService _workshopService;
+ private readonly IPluginManagementService _pluginManagementService;
+ private readonly IWindowService _windowService;
+ [Notify] private PluginViewModel? _pluginViewModel;
+ [Notify] private ObservableCollection? _pluginFeatures;
+
+ public PluginManageViewModel(ISettingsVmFactory settingsVmFactory, IRouter router, IWorkshopService workshopService, IPluginManagementService pluginManagementService, IWindowService windowService)
+ {
+ _settingsVmFactory = settingsVmFactory;
+ _router = router;
+ _workshopService = workshopService;
+ _pluginManagementService = pluginManagementService;
+ _windowService = windowService;
+ ParameterSource = ParameterSource.Route;
+ }
+
+ public async Task Close()
+ {
+ await _router.GoUp();
+ }
+
+ ///
+ public override async Task OnNavigating(WorkshopDetailParameters parameters, NavigationArguments args, CancellationToken cancellationToken)
+ {
+ InstalledEntry? installedEntry = _workshopService.GetInstalledEntry(parameters.EntryId);
+ if (installedEntry == null || !installedEntry.TryGetMetadata("PluginId", out Guid pluginId))
+ {
+ // TODO: Fix cancelling without this workaround, currently navigation is stopped but the page still opens
+ Dispatcher.UIThread.InvokeAsync(async () =>
+ {
+ await _windowService.ShowConfirmContentDialog("Invalid plugin", "The plugin you're trying to manage is invalid or doesn't exist", "Go back", null);
+ await Close();
+ });
+ return;
+ }
+
+ Plugin? plugin = _pluginManagementService.GetAllPlugins().FirstOrDefault(p => p.Guid == pluginId);
+ if (plugin == null)
+ {
+ // TODO: Fix cancelling without this workaround, currently navigation is stopped but the page still opens
+ Dispatcher.UIThread.InvokeAsync(async () =>
+ {
+ await _windowService.ShowConfirmContentDialog("Invalid plugin", "The plugin you're trying to manage is invalid or doesn't exist", "Go back", null);
+ await Close();
+ });
+ return;
+ }
+
+ PluginViewModel = _settingsVmFactory.PluginViewModel(plugin, ReactiveCommand.Create(() => { }));
+ PluginFeatures = new ObservableCollection(plugin.Features.Select(f => _settingsVmFactory.PluginFeatureViewModel(f, false)));
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/Profile/ProfileDescriptionView.axaml b/src/Artemis.UI/Screens/Workshop/Profile/ProfileDescriptionView.axaml
new file mode 100644
index 000000000..5b29dc430
--- /dev/null
+++ b/src/Artemis.UI/Screens/Workshop/Profile/ProfileDescriptionView.axaml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ Required plugins
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/Profile/ProfileDescriptionView.axaml.cs b/src/Artemis.UI/Screens/Workshop/Profile/ProfileDescriptionView.axaml.cs
new file mode 100644
index 000000000..5fed996a6
--- /dev/null
+++ b/src/Artemis.UI/Screens/Workshop/Profile/ProfileDescriptionView.axaml.cs
@@ -0,0 +1,14 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using Avalonia.ReactiveUI;
+
+namespace Artemis.UI.Screens.Workshop.Profile;
+
+public partial class ProfileDescriptionView : ReactiveUserControl
+{
+ public ProfileDescriptionView()
+ {
+ InitializeComponent();
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/Profile/ProfileDescriptionViewModel.cs b/src/Artemis.UI/Screens/Workshop/Profile/ProfileDescriptionViewModel.cs
new file mode 100644
index 000000000..fd16c6cac
--- /dev/null
+++ b/src/Artemis.UI/Screens/Workshop/Profile/ProfileDescriptionViewModel.cs
@@ -0,0 +1,41 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Artemis.UI.Screens.Workshop.Entries.List;
+using Artemis.UI.Shared.Routing;
+using Artemis.WebClient.Workshop;
+using PropertyChanged.SourceGenerator;
+
+namespace Artemis.UI.Screens.Workshop.Profile;
+
+public partial class ProfileDescriptionViewModel : RoutableScreen
+{
+ private readonly IWorkshopClient _client;
+ private readonly Func _getEntryListViewModel;
+ [Notify] private IEntryDetails? _entry;
+ [Notify] private List? _dependencies;
+
+ public ProfileDescriptionViewModel(IWorkshopClient client, Func getEntryListViewModel)
+ {
+ _client = client;
+ _getEntryListViewModel = getEntryListViewModel;
+ }
+
+ public async Task SetEntry(IEntryDetails? entry, CancellationToken cancellationToken)
+ {
+ Entry = entry;
+
+ if (entry != null)
+ {
+ IReadOnlyList? dependencies = (await _client.GetLatestDependencies.ExecuteAsync(entry.Id, cancellationToken)).Data?.Entry?.LatestRelease?.Dependencies;
+ Dependencies = dependencies != null && dependencies.Any() ? dependencies.Select(_getEntryListViewModel).ToList() : null;
+ }
+ else
+ {
+ Dependencies = null;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/Profile/ProfileDetailsView.axaml b/src/Artemis.UI/Screens/Workshop/Profile/ProfileDetailsView.axaml
index 499a5b978..b6d9a52b0 100644
--- a/src/Artemis.UI/Screens/Workshop/Profile/ProfileDetailsView.axaml
+++ b/src/Artemis.UI/Screens/Workshop/Profile/ProfileDetailsView.axaml
@@ -3,7 +3,8 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:profile="clr-namespace:Artemis.UI.Screens.Workshop.Profile"
- xmlns:mdxaml="https://github.com/whistyun/Markdown.Avalonia.Tight"
+ xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
+ xmlns:ui="clr-namespace:Artemis.UI"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="800"
x:Class="Artemis.UI.Screens.Workshop.Profile.ProfileDetailsView"
x:DataType="profile:ProfileDetailsViewModel">
@@ -12,34 +13,19 @@
-
+
-
-
-
-
-
-
-
-
-
-
-
- Required plugins
-
-
-
-
-
-
-
+
+
+
+
+
-
-
+
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/Profile/ProfileDetailsView.axaml.cs b/src/Artemis.UI/Screens/Workshop/Profile/ProfileDetailsView.axaml.cs
index 1150bd94c..38b60575c 100644
--- a/src/Artemis.UI/Screens/Workshop/Profile/ProfileDetailsView.axaml.cs
+++ b/src/Artemis.UI/Screens/Workshop/Profile/ProfileDetailsView.axaml.cs
@@ -1,4 +1,7 @@
+using System;
+using System.Reactive.Disposables;
using Avalonia.ReactiveUI;
+using ReactiveUI;
namespace Artemis.UI.Screens.Workshop.Profile;
@@ -7,5 +10,9 @@ public partial class ProfileDetailsView : ReactiveUserControl ViewModel.WhenAnyValue(vm => vm.Screen)
+ .WhereNotNull()
+ .Subscribe(screen => RouterFrame.NavigateFromObject(screen))
+ .DisposeWith(d));
}
}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/Profile/ProfileDetailsViewModel.cs b/src/Artemis.UI/Screens/Workshop/Profile/ProfileDetailsViewModel.cs
index 8aadc53d4..e9b8480a2 100644
--- a/src/Artemis.UI/Screens/Workshop/Profile/ProfileDetailsViewModel.cs
+++ b/src/Artemis.UI/Screens/Workshop/Profile/ProfileDetailsViewModel.cs
@@ -5,7 +5,7 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Artemis.UI.Screens.Workshop.Entries.Details;
-using Artemis.UI.Screens.Workshop.Entries.List;
+using Artemis.UI.Screens.Workshop.EntryReleases;
using Artemis.UI.Screens.Workshop.Parameters;
using Artemis.UI.Shared.Routing;
using Artemis.WebClient.Workshop;
@@ -14,52 +14,56 @@ using StrawberryShake;
namespace Artemis.UI.Screens.Workshop.Profile;
-public partial class ProfileDetailsViewModel : RoutableScreen
+public partial class ProfileDetailsViewModel : RoutableHostScreen
{
private readonly IWorkshopClient _client;
- private readonly Func _getEntryInfoViewModel;
+ private readonly ProfileDescriptionViewModel _profileDescriptionViewModel;
private readonly Func _getEntryReleasesViewModel;
private readonly Func _getEntryImagesViewModel;
- private readonly Func _getEntryListViewModel;
[Notify] private IEntryDetails? _entry;
- [Notify] private EntryInfoViewModel? _entryInfoViewModel;
[Notify] private EntryReleasesViewModel? _entryReleasesViewModel;
[Notify] private EntryImagesViewModel? _entryImagesViewModel;
- [Notify] private ReadOnlyObservableCollection? _dependencies;
public ProfileDetailsViewModel(IWorkshopClient client,
- Func getEntryInfoViewModel,
+ ProfileDescriptionViewModel profileDescriptionViewModel,
+ EntryInfoViewModel entryInfoViewModel,
Func getEntryReleasesViewModel,
- Func getEntryImagesViewModel,
- Func getEntryListViewModel)
+ Func getEntryImagesViewModel)
{
_client = client;
- _getEntryInfoViewModel = getEntryInfoViewModel;
+ _profileDescriptionViewModel = profileDescriptionViewModel;
_getEntryReleasesViewModel = getEntryReleasesViewModel;
_getEntryImagesViewModel = getEntryImagesViewModel;
- _getEntryListViewModel = getEntryListViewModel;
+
+ EntryInfoViewModel = entryInfoViewModel;
+ RecycleScreen = false;
}
+ public override RoutableScreen DefaultScreen => _profileDescriptionViewModel;
+ public EntryInfoViewModel EntryInfoViewModel { get; }
+
public override async Task OnNavigating(WorkshopDetailParameters parameters, NavigationArguments args, CancellationToken cancellationToken)
{
- await GetEntry(parameters.EntryId, cancellationToken);
+ if (Entry?.Id != parameters.EntryId)
+ await GetEntry(parameters.EntryId, cancellationToken);
}
private async Task GetEntry(long entryId, CancellationToken cancellationToken)
{
+ Task grace = Task.Delay(300, cancellationToken);
IOperationResult result = await _client.GetEntryById.ExecuteAsync(entryId, cancellationToken);
if (result.IsErrorResult())
return;
+ // Let the UI settle to avoid lag when deep linking
+ await grace;
+
Entry = result.Data?.Entry;
- EntryInfoViewModel = Entry != null ? _getEntryInfoViewModel(Entry) : null;
+ EntryInfoViewModel.SetEntry(Entry);
EntryReleasesViewModel = Entry != null ? _getEntryReleasesViewModel(Entry) : null;
EntryImagesViewModel = Entry != null ? _getEntryImagesViewModel(Entry) : null;
-
- IReadOnlyList? dependencies = (await _client.GetLatestDependencies.ExecuteAsync(entryId, cancellationToken)).Data?.Entry?.LatestRelease?.Dependencies;
- Dependencies = dependencies != null && dependencies.Any()
- ? new ReadOnlyObservableCollection(new ObservableCollection(dependencies.Select(_getEntryListViewModel)))
- : null;
+
+ await _profileDescriptionViewModel.SetEntry(Entry, cancellationToken);
}
}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/Profile/ProfileListView.axaml b/src/Artemis.UI/Screens/Workshop/Profile/ProfileListView.axaml
new file mode 100644
index 000000000..70ca1997a
--- /dev/null
+++ b/src/Artemis.UI/Screens/Workshop/Profile/ProfileListView.axaml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/Profile/ProfileListView.axaml.cs b/src/Artemis.UI/Screens/Workshop/Profile/ProfileListView.axaml.cs
new file mode 100644
index 000000000..1d7abe1ce
--- /dev/null
+++ b/src/Artemis.UI/Screens/Workshop/Profile/ProfileListView.axaml.cs
@@ -0,0 +1,18 @@
+using System;
+using System.Reactive.Disposables;
+using Avalonia.ReactiveUI;
+using ReactiveUI;
+
+namespace Artemis.UI.Screens.Workshop.Profile;
+
+public partial class ProfileListView : ReactiveUserControl
+{
+ public ProfileListView()
+ {
+ InitializeComponent();
+ this.WhenActivated(d => ViewModel.WhenAnyValue(vm => vm.Screen)
+ .WhereNotNull()
+ .Subscribe(screen => RouterFrame.NavigateFromObject(screen))
+ .DisposeWith(d));
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/Profile/ProfileListViewModel.cs b/src/Artemis.UI/Screens/Workshop/Profile/ProfileListViewModel.cs
new file mode 100644
index 000000000..75faa19a1
--- /dev/null
+++ b/src/Artemis.UI/Screens/Workshop/Profile/ProfileListViewModel.cs
@@ -0,0 +1,17 @@
+using Artemis.UI.Screens.Workshop.Entries.List;
+using Artemis.UI.Shared.Routing;
+using Artemis.WebClient.Workshop;
+
+namespace Artemis.UI.Screens.Workshop.Profile;
+
+public class ProfileListViewModel : RoutableHostScreen
+{
+ private readonly EntryListViewModel _entryListViewModel;
+ public override RoutableScreen DefaultScreen => _entryListViewModel;
+
+ public ProfileListViewModel(EntryListViewModel entryListViewModel)
+ {
+ _entryListViewModel = entryListViewModel;
+ _entryListViewModel.EntryType = EntryType.Profile;
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Models/SubmissionWizardState.cs b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Models/SubmissionWizardState.cs
index c92f6290b..aaf688ef6 100644
--- a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Models/SubmissionWizardState.cs
+++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Models/SubmissionWizardState.cs
@@ -38,6 +38,7 @@ public class SubmissionWizardState : IDisposable
public List Images { get; set; } = new();
public IEntrySource? EntrySource { get; set; }
+ public string? Changelog { get; set; }
public void ChangeScreen() where TSubmissionViewModel : SubmissionViewModel
{
diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/ReleaseWizardView.axaml.cs b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/ReleaseWizardView.axaml.cs
index 50bbc4ace..ca6667b66 100644
--- a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/ReleaseWizardView.axaml.cs
+++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/ReleaseWizardView.axaml.cs
@@ -8,7 +8,7 @@ using ReactiveUI;
namespace Artemis.UI.Screens.Workshop.SubmissionWizard;
-public partial class ReleaseWizardView: ReactiveAppWindow
+public partial class ReleaseWizardView : ReactiveAppWindow
{
public ReleaseWizardView()
{
@@ -25,7 +25,7 @@ public partial class ReleaseWizardView: ReactiveAppWindow Frame.NavigateFromObject(viewModel));
+ Frame.NavigateFromObject(viewModel);
}
catch (Exception e)
{
diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/ChangelogStepView.axaml b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/ChangelogStepView.axaml
new file mode 100644
index 000000000..ff6ddd67d
--- /dev/null
+++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/ChangelogStepView.axaml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+ Changelog
+
+ If you want to inform your users what has changed in this release, you can provide a changelog. This is optional but recommended.
+
+
+
+
+
+
diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/ChangelogStepView.axaml.cs b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/ChangelogStepView.axaml.cs
new file mode 100644
index 000000000..2733746f7
--- /dev/null
+++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/ChangelogStepView.axaml.cs
@@ -0,0 +1,14 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using Avalonia.ReactiveUI;
+
+namespace Artemis.UI.Screens.Workshop.SubmissionWizard.Steps;
+
+public partial class ChangelogStepView : ReactiveUserControl
+{
+ public ChangelogStepView()
+ {
+ InitializeComponent();
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/ChangelogStepViewModel.cs b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/ChangelogStepViewModel.cs
new file mode 100644
index 000000000..e2e97795f
--- /dev/null
+++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/ChangelogStepViewModel.cs
@@ -0,0 +1,40 @@
+using System.Reactive.Disposables;
+using Artemis.UI.Screens.Workshop.SubmissionWizard.Steps.Layout;
+using Artemis.UI.Screens.Workshop.SubmissionWizard.Steps.Plugin;
+using Artemis.UI.Screens.Workshop.SubmissionWizard.Steps.Profile;
+using Artemis.WebClient.Workshop;
+using PropertyChanged.SourceGenerator;
+using ReactiveUI;
+
+namespace Artemis.UI.Screens.Workshop.SubmissionWizard.Steps;
+
+public partial class ChangelogStepViewModel : SubmissionViewModel
+{
+ [Notify] private string? _changelog;
+
+ public ChangelogStepViewModel()
+ {
+ GoBack = ReactiveCommand.Create(ExecuteGoBack);
+ Continue = ReactiveCommand.Create(ExecuteContinue);
+ ContinueText = "Submit";
+
+ this.WhenActivated((CompositeDisposable _) => Changelog = State.Changelog);
+ }
+
+ private void ExecuteContinue()
+ {
+ State.Changelog = Changelog;
+ State.ChangeScreen();
+ }
+
+ private void ExecuteGoBack()
+ {
+ State.Changelog = Changelog;
+ if (State.EntryType == EntryType.Layout)
+ State.ChangeScreen();
+ else if (State.EntryType == EntryType.Plugin)
+ State.ChangeScreen();
+ else if (State.EntryType == EntryType.Profile)
+ State.ChangeScreen();
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Layout/LayoutInfoStepViewModel.cs b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Layout/LayoutInfoStepViewModel.cs
index 23c3b293e..3ebe3b748 100644
--- a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Layout/LayoutInfoStepViewModel.cs
+++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Layout/LayoutInfoStepViewModel.cs
@@ -98,6 +98,6 @@ public partial class LayoutInfoStepViewModel : SubmissionViewModel
if (State.EntryId == null)
State.ChangeScreen();
else
- State.ChangeScreen();
+ State.ChangeScreen();
}
}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Plugin/PluginSelectionStepViewModel.cs b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Plugin/PluginSelectionStepViewModel.cs
index 2d8cc45d9..4008c5f57 100644
--- a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Plugin/PluginSelectionStepViewModel.cs
+++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Plugin/PluginSelectionStepViewModel.cs
@@ -78,6 +78,6 @@ public partial class PluginSelectionStepViewModel : SubmissionViewModel
if (State.EntryId == null)
State.ChangeScreen();
else
- State.ChangeScreen();
+ State.ChangeScreen();
}
}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Profile/ProfileAdaptionHintsStepViewModel.cs b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Profile/ProfileAdaptionHintsStepViewModel.cs
index 0e3556a96..87d7872dc 100644
--- a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Profile/ProfileAdaptionHintsStepViewModel.cs
+++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Profile/ProfileAdaptionHintsStepViewModel.cs
@@ -64,6 +64,6 @@ public class ProfileAdaptionHintsStepViewModel : SubmissionViewModel
if (State.EntryId == null)
State.ChangeScreen();
else
- State.ChangeScreen();
+ State.ChangeScreen();
}
}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/UploadStepViewModel.cs b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/UploadStepViewModel.cs
index 81e464311..c205e24aa 100644
--- a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/UploadStepViewModel.cs
+++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/UploadStepViewModel.cs
@@ -70,7 +70,7 @@ public partial class UploadStepViewModel : SubmissionViewModel
// Create a release for the new entry
IEntryUploadHandler uploadHandler = _entryUploadHandlerFactory.CreateHandler(State.EntryType);
- EntryUploadResult uploadResult = await uploadHandler.CreateReleaseAsync(_entryId.Value, State.EntrySource!, cancellationToken);
+ EntryUploadResult uploadResult = await uploadHandler.CreateReleaseAsync(_entryId.Value, State.EntrySource!, State.Changelog, cancellationToken);
if (!uploadResult.IsSuccess)
throw new ArtemisWorkshopException(uploadResult.Message);
diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/SubmissionWizardView.axaml.cs b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/SubmissionWizardView.axaml.cs
index 6c7ece92f..377bc17cb 100644
--- a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/SubmissionWizardView.axaml.cs
+++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/SubmissionWizardView.axaml.cs
@@ -25,7 +25,7 @@ public partial class SubmissionWizardView : ReactiveAppWindow Frame.NavigateFromObject(viewModel));
+ Frame.NavigateFromObject(viewModel);
}
catch (Exception e)
{
diff --git a/src/Artemis.UI/Styles/Markdown.axaml b/src/Artemis.UI/Styles/Markdown.axaml
index 6f03f8291..c56fb22af 100644
--- a/src/Artemis.UI/Styles/Markdown.axaml
+++ b/src/Artemis.UI/Styles/Markdown.axaml
@@ -1,7 +1,13 @@
+ xmlns:ctxt="clr-namespace:ColorTextBlock.Avalonia;assembly=ColorTextBlock.Avalonia"
+ xmlns:controls="clr-namespace:Markdown.Avalonia.Controls;assembly=Markdown.Avalonia">
+
+
+ Markdown.Xaml support ```inline code ``` and block code.
+
+
@@ -60,4 +66,21 @@
+
+
+
+
\ No newline at end of file
diff --git a/src/Artemis.WebClient.Workshop/Artemis.WebClient.Workshop.csproj b/src/Artemis.WebClient.Workshop/Artemis.WebClient.Workshop.csproj
index 3e348ce4e..a51abe913 100644
--- a/src/Artemis.WebClient.Workshop/Artemis.WebClient.Workshop.csproj
+++ b/src/Artemis.WebClient.Workshop/Artemis.WebClient.Workshop.csproj
@@ -48,6 +48,9 @@
MSBuild:GenerateGraphQLCode
+
+ MSBuild:GenerateGraphQLCode
+
diff --git a/src/Artemis.WebClient.Workshop/Extensions/EntryExtensions.cs b/src/Artemis.WebClient.Workshop/Extensions/EntryExtensions.cs
new file mode 100644
index 000000000..d901b1865
--- /dev/null
+++ b/src/Artemis.WebClient.Workshop/Extensions/EntryExtensions.cs
@@ -0,0 +1,9 @@
+namespace Artemis.WebClient.Workshop.Extensions;
+
+public static class EntryExtensions
+{
+ public static string GetEntryPath(this IEntryDetails entry)
+ {
+ return $"workshop/entries/{entry.EntryType.ToString().ToLower()}s/details/{entry.Id}";
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/IEntryInstallationHandler.cs b/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/IEntryInstallationHandler.cs
index aacd0d580..555ed11c4 100644
--- a/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/IEntryInstallationHandler.cs
+++ b/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/IEntryInstallationHandler.cs
@@ -6,6 +6,6 @@ namespace Artemis.WebClient.Workshop.Handlers.InstallationHandlers;
public interface IEntryInstallationHandler
{
- Task InstallAsync(IEntryDetails entry, IRelease release, Progress progress, CancellationToken cancellationToken);
+ Task InstallAsync(IEntrySummary entry, IRelease release, Progress progress, CancellationToken cancellationToken);
Task UninstallAsync(InstalledEntry installedEntry, CancellationToken cancellationToken);
}
\ No newline at end of file
diff --git a/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/Implementations/LayoutEntryInstallationHandler.cs b/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/Implementations/LayoutEntryInstallationHandler.cs
index 3014950f7..4e8b469bd 100644
--- a/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/Implementations/LayoutEntryInstallationHandler.cs
+++ b/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/Implementations/LayoutEntryInstallationHandler.cs
@@ -25,7 +25,7 @@ public class LayoutEntryInstallationHandler : IEntryInstallationHandler
_defaultLayoutProvider = defaultLayoutProvider;
}
- public async Task InstallAsync(IEntryDetails entry, IRelease release, Progress progress, CancellationToken cancellationToken)
+ public async Task InstallAsync(IEntrySummary entry, IRelease release, Progress progress, CancellationToken cancellationToken)
{
using MemoryStream stream = new();
diff --git a/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/Implementations/PluginEntryInstallationHandler.cs b/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/Implementations/PluginEntryInstallationHandler.cs
index 592b13037..476b658dc 100644
--- a/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/Implementations/PluginEntryInstallationHandler.cs
+++ b/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/Implementations/PluginEntryInstallationHandler.cs
@@ -22,7 +22,7 @@ public class PluginEntryInstallationHandler : IEntryInstallationHandler
_pluginManagementService = pluginManagementService;
}
- public async Task InstallAsync(IEntryDetails entry, IRelease release, Progress progress, CancellationToken cancellationToken)
+ public async Task InstallAsync(IEntrySummary entry, IRelease release, Progress progress, CancellationToken cancellationToken)
{
// Ensure there is an installed entry
InstalledEntry? installedEntry = _workshopService.GetInstalledEntry(entry.Id);
diff --git a/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/Implementations/ProfileEntryInstallationHandler.cs b/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/Implementations/ProfileEntryInstallationHandler.cs
index 1219f84d6..1264c43b7 100644
--- a/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/Implementations/ProfileEntryInstallationHandler.cs
+++ b/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/Implementations/ProfileEntryInstallationHandler.cs
@@ -20,7 +20,7 @@ public class ProfileEntryInstallationHandler : IEntryInstallationHandler
_workshopService = workshopService;
}
- public async Task InstallAsync(IEntryDetails entry, IRelease release, Progress progress, CancellationToken cancellationToken)
+ public async Task InstallAsync(IEntrySummary entry, IRelease release, Progress progress, CancellationToken cancellationToken)
{
using MemoryStream stream = new();
diff --git a/src/Artemis.WebClient.Workshop/Handlers/UploadHandlers/IEntryUploadHandler.cs b/src/Artemis.WebClient.Workshop/Handlers/UploadHandlers/IEntryUploadHandler.cs
index c0bc829be..a25dc689b 100644
--- a/src/Artemis.WebClient.Workshop/Handlers/UploadHandlers/IEntryUploadHandler.cs
+++ b/src/Artemis.WebClient.Workshop/Handlers/UploadHandlers/IEntryUploadHandler.cs
@@ -1,8 +1,6 @@
-using Artemis.UI.Shared.Utilities;
-
-namespace Artemis.WebClient.Workshop.Handlers.UploadHandlers;
+namespace Artemis.WebClient.Workshop.Handlers.UploadHandlers;
public interface IEntryUploadHandler
{
- Task CreateReleaseAsync(long entryId, IEntrySource entrySource, CancellationToken cancellationToken);
+ Task CreateReleaseAsync(long entryId, IEntrySource entrySource, string? changelog, CancellationToken cancellationToken);
}
\ No newline at end of file
diff --git a/src/Artemis.WebClient.Workshop/Handlers/UploadHandlers/Implementations/LayoutEntryUploadHandler.cs b/src/Artemis.WebClient.Workshop/Handlers/UploadHandlers/Implementations/LayoutEntryUploadHandler.cs
index 38e2c9789..0ed8edfc6 100644
--- a/src/Artemis.WebClient.Workshop/Handlers/UploadHandlers/Implementations/LayoutEntryUploadHandler.cs
+++ b/src/Artemis.WebClient.Workshop/Handlers/UploadHandlers/Implementations/LayoutEntryUploadHandler.cs
@@ -18,7 +18,7 @@ public class LayoutEntryUploadHandler : IEntryUploadHandler
}
///
- public async Task CreateReleaseAsync(long entryId, IEntrySource entrySource, CancellationToken cancellationToken)
+ public async Task CreateReleaseAsync(long entryId, IEntrySource entrySource, string? changelog, CancellationToken cancellationToken)
{
if (entrySource is not LayoutEntrySource source)
throw new InvalidOperationException("Can only create releases for layouts");
@@ -62,6 +62,8 @@ public class LayoutEntryUploadHandler : IEntryUploadHandler
MultipartFormDataContent content = new();
StreamContent streamContent = new(archiveStream);
streamContent.Headers.ContentType = new MediaTypeHeaderValue("application/zip");
+ if (!string.IsNullOrWhiteSpace(changelog))
+ content.Add(new StringContent(changelog), "Changelog");
content.Add(streamContent, "file", "file.zip");
// Submit
diff --git a/src/Artemis.WebClient.Workshop/Handlers/UploadHandlers/Implementations/PluginEntryUploadHandler.cs b/src/Artemis.WebClient.Workshop/Handlers/UploadHandlers/Implementations/PluginEntryUploadHandler.cs
index f8c51034c..926f8485b 100644
--- a/src/Artemis.WebClient.Workshop/Handlers/UploadHandlers/Implementations/PluginEntryUploadHandler.cs
+++ b/src/Artemis.WebClient.Workshop/Handlers/UploadHandlers/Implementations/PluginEntryUploadHandler.cs
@@ -14,7 +14,7 @@ public class PluginEntryUploadHandler : IEntryUploadHandler
}
///
- public async Task CreateReleaseAsync(long entryId, IEntrySource entrySource, CancellationToken cancellationToken)
+ public async Task CreateReleaseAsync(long entryId, IEntrySource entrySource, string? changelog, CancellationToken cancellationToken)
{
if (entrySource is not PluginEntrySource source)
throw new InvalidOperationException("Can only create releases for plugins");
@@ -27,6 +27,8 @@ public class PluginEntryUploadHandler : IEntryUploadHandler
MultipartFormDataContent content = new();
StreamContent streamContent = new(fileStream);
streamContent.Headers.ContentType = new MediaTypeHeaderValue("application/zip");
+ if (!string.IsNullOrWhiteSpace(changelog))
+ content.Add(new StringContent(changelog), "Changelog");
content.Add(streamContent, "file", "file.zip");
// Submit
diff --git a/src/Artemis.WebClient.Workshop/Handlers/UploadHandlers/Implementations/ProfileEntryUploadHandler.cs b/src/Artemis.WebClient.Workshop/Handlers/UploadHandlers/Implementations/ProfileEntryUploadHandler.cs
index 3dbbd68e9..381bd0199 100644
--- a/src/Artemis.WebClient.Workshop/Handlers/UploadHandlers/Implementations/ProfileEntryUploadHandler.cs
+++ b/src/Artemis.WebClient.Workshop/Handlers/UploadHandlers/Implementations/ProfileEntryUploadHandler.cs
@@ -17,7 +17,7 @@ public class ProfileEntryUploadHandler : IEntryUploadHandler
}
///
- public async Task CreateReleaseAsync(long entryId, IEntrySource entrySource, CancellationToken cancellationToken)
+ public async Task CreateReleaseAsync(long entryId, IEntrySource entrySource, string? changelog, CancellationToken cancellationToken)
{
if (entrySource is not ProfileEntrySource source)
throw new InvalidOperationException("Can only create releases for profile configurations");
@@ -32,6 +32,8 @@ public class ProfileEntryUploadHandler : IEntryUploadHandler
StreamContent streamContent = new(archiveStream);
streamContent.Headers.ContentType = new MediaTypeHeaderValue("application/zip");
content.Add(JsonContent.Create(source.Dependencies.Select(d => new {PluginId = d.Plugin.Guid, FeatureId = d.Id}).ToList()), "ReleaseDependencies");
+ if (!string.IsNullOrWhiteSpace(changelog))
+ content.Add(new StringContent(changelog), "Changelog");
content.Add(streamContent, "file", "file.zip");
// Submit
diff --git a/src/Artemis.WebClient.Workshop/Models/InstalledEntry.cs b/src/Artemis.WebClient.Workshop/Models/InstalledEntry.cs
index ce90444b0..401e5495e 100644
--- a/src/Artemis.WebClient.Workshop/Models/InstalledEntry.cs
+++ b/src/Artemis.WebClient.Workshop/Models/InstalledEntry.cs
@@ -16,7 +16,7 @@ public class InstalledEntry
Load();
}
- public InstalledEntry(IEntryDetails entry, IRelease release)
+ public InstalledEntry(IEntrySummary entry, IRelease release)
{
Entity = new EntryEntity();
diff --git a/src/Artemis.WebClient.Workshop/Mutations/UpdateEntry.graphql b/src/Artemis.WebClient.Workshop/Mutations/UpdateEntry.graphql
index fe53ec3e5..1a667c53b 100644
--- a/src/Artemis.WebClient.Workshop/Mutations/UpdateEntry.graphql
+++ b/src/Artemis.WebClient.Workshop/Mutations/UpdateEntry.graphql
@@ -3,3 +3,16 @@ mutation UpdateEntry ($input: UpdateEntryInput!) {
id
}
}
+
+
+mutation UpdateRelease($input: UpdateReleaseInput!) {
+ updateRelease(input: $input) {
+ id
+ }
+}
+
+mutation RemoveRelease($input: Long!) {
+ removeRelease(id: $input) {
+ id
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.WebClient.Workshop/Queries/Fragments.graphql b/src/Artemis.WebClient.Workshop/Queries/Fragments.graphql
index 0325293c5..ea41aa36a 100644
--- a/src/Artemis.WebClient.Workshop/Queries/Fragments.graphql
+++ b/src/Artemis.WebClient.Workshop/Queries/Fragments.graphql
@@ -54,12 +54,12 @@ fragment entryDetails on Entry {
categories {
...category
}
- latestRelease {
- ...release
- }
images {
...image
}
+ releases {
+ ...release
+ }
}
fragment release on Release {
diff --git a/src/Artemis.WebClient.Workshop/Queries/GetReleaseById.graphql b/src/Artemis.WebClient.Workshop/Queries/GetReleaseById.graphql
new file mode 100644
index 000000000..84ee9604a
--- /dev/null
+++ b/src/Artemis.WebClient.Workshop/Queries/GetReleaseById.graphql
@@ -0,0 +1,9 @@
+query GetReleaseById($id: Long!) {
+ release(id: $id) {
+ ...release
+ changelog
+ entry {
+ ...entrySummary
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.WebClient.Workshop/Queries/GetSubmittedEntryById.graphql b/src/Artemis.WebClient.Workshop/Queries/GetSubmittedEntryById.graphql
index 1bcd98fd6..6aece2ca4 100644
--- a/src/Artemis.WebClient.Workshop/Queries/GetSubmittedEntryById.graphql
+++ b/src/Artemis.WebClient.Workshop/Queries/GetSubmittedEntryById.graphql
@@ -14,5 +14,10 @@ query GetSubmittedEntryById($id: Long!) {
images {
...image
}
+ releases {
+ id
+ version
+ createdAt
+ }
}
}
\ No newline at end of file
diff --git a/src/Artemis.WebClient.Workshop/Services/Interfaces/IWorkshopService.cs b/src/Artemis.WebClient.Workshop/Services/Interfaces/IWorkshopService.cs
index ea61b3b6d..304f4ef05 100644
--- a/src/Artemis.WebClient.Workshop/Services/Interfaces/IWorkshopService.cs
+++ b/src/Artemis.WebClient.Workshop/Services/Interfaces/IWorkshopService.cs
@@ -20,4 +20,6 @@ public interface IWorkshopService
void Initialize();
public record WorkshopStatus(bool IsReachable, string Message);
+
+ event EventHandler? OnInstalledEntrySaved;
}
\ No newline at end of file
diff --git a/src/Artemis.WebClient.Workshop/Services/WorkshopService.cs b/src/Artemis.WebClient.Workshop/Services/WorkshopService.cs
index 95b90ccc5..715a5a1bb 100644
--- a/src/Artemis.WebClient.Workshop/Services/WorkshopService.cs
+++ b/src/Artemis.WebClient.Workshop/Services/WorkshopService.cs
@@ -172,6 +172,8 @@ public class WorkshopService : IWorkshopService
{
entry.Save();
_entryRepository.Save(entry.Entity);
+
+ OnInstalledEntrySaved?.Invoke(this, entry);
}
///
@@ -231,4 +233,6 @@ public class WorkshopService : IWorkshopService
_logger.Warning(e, "Failed to remove orphaned workshop entry at {Directory}", directory);
}
}
+
+ public event EventHandler? OnInstalledEntrySaved;
}
\ No newline at end of file
diff --git a/src/Artemis.WebClient.Workshop/schema.graphql b/src/Artemis.WebClient.Workshop/schema.graphql
index 442923f3d..f26e43af8 100644
--- a/src/Artemis.WebClient.Workshop/schema.graphql
+++ b/src/Artemis.WebClient.Workshop/schema.graphql
@@ -100,8 +100,10 @@ type Mutation {
addLayoutInfo(input: CreateLayoutInfoInput!): LayoutInfo
removeEntry(id: Long!): Entry
removeLayoutInfo(id: Long!): LayoutInfo!
+ removeRelease(id: Long!): Release!
updateEntry(input: UpdateEntryInput!): Entry
updateEntryImage(input: UpdateEntryImageInput!): Image
+ updateRelease(input: UpdateReleaseInput!): Release
}
"Information about pagination in a connection."
@@ -158,6 +160,7 @@ type Query {
entry(id: Long!): Entry
pluginInfo(pluginGuid: UUID!): PluginInfo
pluginInfos(order: [PluginInfoSortInput!], skip: Int, take: Int, where: PluginInfoFilterInput): PluginInfosCollectionSegment
+ release(id: Long!): Release
searchEntries(input: String!, order: [EntrySortInput!], type: EntryType, where: EntryFilterInput): [Entry!]!
searchKeyboardLayout(deviceProvider: UUID!, logicalLayout: String, model: String!, physicalLayout: KeyboardLayoutType!, vendor: String!): LayoutInfo
searchLayout(deviceProvider: UUID!, deviceType: RGBDeviceType!, model: String!, vendor: String!): LayoutInfo
@@ -165,6 +168,7 @@ type Query {
}
type Release {
+ changelog: String
createdAt: DateTime!
dependencies: [Entry!]!
downloadSize: Long!
@@ -498,6 +502,7 @@ input RGBDeviceTypeOperationFilterInput {
input ReleaseFilterInput {
and: [ReleaseFilterInput!]
+ changelog: StringOperationFilterInput
createdAt: DateTimeOperationFilterInput
dependencies: ListFilterInputTypeOfEntryFilterInput
downloadSize: LongOperationFilterInput
@@ -511,6 +516,7 @@ input ReleaseFilterInput {
}
input ReleaseSortInput {
+ changelog: SortEnumType
createdAt: SortEnumType
downloadSize: SortEnumType
downloads: SortEnumType
@@ -558,6 +564,11 @@ input UpdateEntryInput {
tags: [String!]!
}
+input UpdateReleaseInput {
+ changelog: String
+ id: Long!
+}
+
input UuidOperationFilterInput {
eq: UUID
gt: UUID