diff --git a/src/Artemis.UI.Shared/Services/Interfaces/IWindowService.cs b/src/Artemis.UI.Shared/Services/Interfaces/IWindowService.cs
index ff4394fde..15756d2a0 100644
--- a/src/Artemis.UI.Shared/Services/Interfaces/IWindowService.cs
+++ b/src/Artemis.UI.Shared/Services/Interfaces/IWindowService.cs
@@ -16,7 +16,7 @@ public interface IWindowService : IArtemisSharedUIService
///
/// The type of view model to create
/// The created view model
- TViewModel ShowWindow(params object[] parameters);
+ Window ShowWindow(out TViewModel viewModel, params object[] parameters);
///
/// Given a ViewModel, show its corresponding View as a window
diff --git a/src/Artemis.UI.Shared/Services/Window/WindowService.cs b/src/Artemis.UI.Shared/Services/Window/WindowService.cs
index c30688eef..5db1716d2 100644
--- a/src/Artemis.UI.Shared/Services/Window/WindowService.cs
+++ b/src/Artemis.UI.Shared/Services/Window/WindowService.cs
@@ -21,14 +21,13 @@ internal class WindowService : IWindowService
_container = container;
}
- public T ShowWindow(params object[] parameters)
+ public Window ShowWindow(out T viewModel, params object[] parameters)
{
- T viewModel = _container.Resolve(parameters);
+ viewModel = _container.Resolve(parameters);
if (viewModel == null)
throw new ArtemisSharedUIException($"Failed to show window for VM of type {typeof(T).Name}, could not create instance.");
- ShowWindow(viewModel);
- return viewModel;
+ return ShowWindow(viewModel);
}
public Window ShowWindow(object viewModel)
diff --git a/src/Artemis.UI/Artemis.UI.csproj b/src/Artemis.UI/Artemis.UI.csproj
index f7ffa8c34..fd7f60a06 100644
--- a/src/Artemis.UI/Artemis.UI.csproj
+++ b/src/Artemis.UI/Artemis.UI.csproj
@@ -20,6 +20,7 @@
+
@@ -28,6 +29,7 @@
+
@@ -43,9 +45,25 @@
+
+
+
+
+ ProfileListView.axaml
+ Code
+
+
+ LayoutListView.axaml
+ Code
+
+
+ SubmissionsDetailView.axaml
+ Code
+
+
\ No newline at end of file
diff --git a/src/Artemis.UI/Routing/RouteViewModel.cs b/src/Artemis.UI/Routing/RouteViewModel.cs
index 5ecc0f356..78cc458c9 100644
--- a/src/Artemis.UI/Routing/RouteViewModel.cs
+++ b/src/Artemis.UI/Routing/RouteViewModel.cs
@@ -4,18 +4,20 @@ namespace Artemis.UI.Routing;
public class RouteViewModel
{
- public RouteViewModel(string path, string name)
+ public RouteViewModel(string name, string path, string? mathPath = null)
{
Path = path;
Name = name;
+ MathPath = mathPath;
}
public string Path { get; }
public string Name { get; }
+ public string? MathPath { get; }
public bool Matches(string path)
{
- return path.StartsWith(Path, StringComparison.InvariantCultureIgnoreCase);
+ return path.StartsWith(MathPath ?? Path, StringComparison.InvariantCultureIgnoreCase);
}
///
diff --git a/src/Artemis.UI/Routing/Routes.cs b/src/Artemis.UI/Routing/Routes.cs
index 128209f99..4ad88a88d 100644
--- a/src/Artemis.UI/Routing/Routes.cs
+++ b/src/Artemis.UI/Routing/Routes.cs
@@ -6,6 +6,8 @@ using Artemis.UI.Screens.Settings;
using Artemis.UI.Screens.Settings.Updating;
using Artemis.UI.Screens.SurfaceEditor;
using Artemis.UI.Screens.Workshop;
+using Artemis.UI.Screens.Workshop.Entries;
+using Artemis.UI.Screens.Workshop.Entries.Tabs;
using Artemis.UI.Screens.Workshop.Home;
using Artemis.UI.Screens.Workshop.Layout;
using Artemis.UI.Screens.Workshop.Library;
@@ -24,16 +26,22 @@ public static class Routes
#if DEBUG
new RouteRegistration("workshop")
{
- Children = new List()
+ Children = new List
{
new RouteRegistration("offline/{message:string}"),
- new RouteRegistration("profiles/{page:int}"),
- new RouteRegistration("profiles/{entryId:guid}"),
- new RouteRegistration("layouts/{page:int}"),
- new RouteRegistration("layouts/{entryId:guid}"),
+ new RouteRegistration("entries")
+ {
+ Children = new List
+ {
+ new RouteRegistration("profiles/{page:int}"),
+ new RouteRegistration("profiles/details/{entryId:guid}"),
+ new RouteRegistration("layouts/{page:int}"),
+ new RouteRegistration("layouts/details/{entryId:guid}"),
+ }
+ },
new RouteRegistration("library")
{
- Children = new List()
+ Children = new List
{
new RouteRegistration("installed"),
new RouteRegistration("submissions"),
diff --git a/src/Artemis.UI/Screens/Root/RootViewModel.cs b/src/Artemis.UI/Screens/Root/RootViewModel.cs
index d057893e5..8b319b300 100644
--- a/src/Artemis.UI/Screens/Root/RootViewModel.cs
+++ b/src/Artemis.UI/Screens/Root/RootViewModel.cs
@@ -128,7 +128,7 @@ public class RootViewModel : RoutableHostScreen, IMainWindowProv
private void ShowSplashScreen()
{
- _windowService.ShowWindow();
+ _windowService.ShowWindow(out SplashViewModel _);
}
#region Tray commands
diff --git a/src/Artemis.UI/Screens/Settings/SettingsViewModel.cs b/src/Artemis.UI/Screens/Settings/SettingsViewModel.cs
index 63b97b1ab..26e8edcd2 100644
--- a/src/Artemis.UI/Screens/Settings/SettingsViewModel.cs
+++ b/src/Artemis.UI/Screens/Settings/SettingsViewModel.cs
@@ -21,11 +21,11 @@ public class SettingsViewModel : RoutableHostScreen, IMainScreen
_router = router;
SettingTabs = new ObservableCollection
{
- new("settings/general", "General"),
- new("settings/plugins", "Plugins"),
- new("settings/devices", "Devices"),
- new("settings/releases", "Releases"),
- new("settings/about", "About"),
+ new("General", "settings/general"),
+ new("Plugins", "settings/plugins"),
+ new("Devices", "settings/devices"),
+ new("Releases", "settings/releases"),
+ new("About", "settings/about"),
};
// Navigate on tab change
diff --git a/src/Artemis.UI/Screens/Sidebar/SidebarViewModel.cs b/src/Artemis.UI/Screens/Sidebar/SidebarViewModel.cs
index 6b7d6c035..0210d557f 100644
--- a/src/Artemis.UI/Screens/Sidebar/SidebarViewModel.cs
+++ b/src/Artemis.UI/Screens/Sidebar/SidebarViewModel.cs
@@ -41,8 +41,8 @@ public class SidebarViewModel : ActivatableViewModelBase
#if DEBUG
new(MaterialIconKind.TestTube, "Workshop", "workshop", null, new ObservableCollection
{
- new(MaterialIconKind.FolderVideo, "Profiles", "workshop/profiles/1", "workshop/profiles"),
- new(MaterialIconKind.KeyboardVariant, "Layouts", "workshop/layouts/1", "workshop/layouts"),
+ new(MaterialIconKind.FolderVideo, "Profiles", "workshop/entries/profiles/1", "workshop/entries/profiles"),
+ new(MaterialIconKind.KeyboardVariant, "Layouts", "workshop/entries/layouts/1", "workshop/entries/layouts"),
new(MaterialIconKind.Bookshelf, "Library", "workshop/library"),
}),
#endif
diff --git a/src/Artemis.UI/Screens/Workshop/Entries/EntriesView.axaml b/src/Artemis.UI/Screens/Workshop/Entries/EntriesView.axaml
new file mode 100644
index 000000000..ecc131eb9
--- /dev/null
+++ b/src/Artemis.UI/Screens/Workshop/Entries/EntriesView.axaml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Artemis.UI/Screens/Workshop/Entries/EntriesView.axaml.cs b/src/Artemis.UI/Screens/Workshop/Entries/EntriesView.axaml.cs
new file mode 100644
index 000000000..3f2106645
--- /dev/null
+++ b/src/Artemis.UI/Screens/Workshop/Entries/EntriesView.axaml.cs
@@ -0,0 +1,28 @@
+using System.Reactive.Disposables;
+using Artemis.UI.Shared;
+using Avalonia.ReactiveUI;
+using Avalonia.Threading;
+using FluentAvalonia.UI.Controls;
+using ReactiveUI;
+using System;
+
+namespace Artemis.UI.Screens.Workshop.Entries;
+
+public partial class EntriesView : ReactiveUserControl
+{
+ public EntriesView()
+ {
+ InitializeComponent();
+ this.WhenActivated(d => { ViewModel.WhenAnyValue(vm => vm.Screen).WhereNotNull().Subscribe(Navigate).DisposeWith(d); });
+ }
+
+ private void Navigate(ViewModelBase viewModel)
+ {
+ Dispatcher.UIThread.Invoke(() => TabFrame.NavigateFromObject(viewModel));
+ }
+
+ private void NavigationView_OnBackRequested(object? sender, NavigationViewBackRequestedEventArgs e)
+ {
+ ViewModel?.GoBack();
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/Entries/EntriesViewModel.cs b/src/Artemis.UI/Screens/Workshop/Entries/EntriesViewModel.cs
new file mode 100644
index 000000000..903a4d649
--- /dev/null
+++ b/src/Artemis.UI/Screens/Workshop/Entries/EntriesViewModel.cs
@@ -0,0 +1,65 @@
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Reactive.Disposables;
+using System.Threading;
+using System.Threading.Tasks;
+using Artemis.UI.Routing;
+using Artemis.UI.Shared.Routing;
+using ReactiveUI;
+using System;
+using System.Reactive.Linq;
+
+namespace Artemis.UI.Screens.Workshop.Entries;
+
+public class EntriesViewModel : RoutableHostScreen
+{
+ private readonly IRouter _router;
+ private RouteViewModel? _selectedTab;
+ private ObservableAsPropertyHelper? _viewingDetails;
+
+ public EntriesViewModel(IRouter router)
+ {
+ _router = router;
+
+ Tabs = new ObservableCollection
+ {
+ new("Profiles", "workshop/entries/profiles/1", "workshop/entries/profiles"),
+ new("Layouts", "workshop/entries/layouts/1", "workshop/entries/layouts")
+ };
+
+ this.WhenActivated(d =>
+ {
+ // Show back button on details page
+ _viewingDetails = _router.CurrentPath.Select(p => p != null && p.Contains("details")).ToProperty(this, vm => vm.ViewingDetails).DisposeWith(d);
+ // Navigate on tab change
+ this.WhenAnyValue(vm => vm.SelectedTab)
+ .WhereNotNull()
+ .Subscribe(s => router.Navigate(s.Path, new RouterNavigationOptions {IgnoreOnPartialMatch = true}))
+ .DisposeWith(d);
+ });
+ }
+
+ public bool ViewingDetails => _viewingDetails?.Value ?? false;
+ public ObservableCollection Tabs { get; }
+
+ public RouteViewModel? SelectedTab
+ {
+ get => _selectedTab;
+ set => RaiseAndSetIfChanged(ref _selectedTab, value);
+ }
+
+ public override async Task OnNavigating(NavigationArguments args, CancellationToken cancellationToken)
+ {
+ SelectedTab = Tabs.FirstOrDefault(t => t.Matches(args.Path));
+ if (SelectedTab == null)
+ await args.Router.Navigate(Tabs.First().Path);
+ }
+
+ public void GoBack()
+ {
+ if (ViewingDetails)
+ _router.GoBack();
+ else
+ _router.Navigate("workshop");
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/Entries/EntryListItemViewModel.cs b/src/Artemis.UI/Screens/Workshop/Entries/EntryListItemViewModel.cs
index a654b476e..4160048ff 100644
--- a/src/Artemis.UI/Screens/Workshop/Entries/EntryListItemViewModel.cs
+++ b/src/Artemis.UI/Screens/Workshop/Entries/EntryListItemViewModel.cs
@@ -33,10 +33,10 @@ public class EntryListItemViewModel : ActivatableViewModelBase
switch (Entry.EntryType)
{
case EntryType.Layout:
- await _router.Navigate($"workshop/layouts/{Entry.Id}");
+ await _router.Navigate($"workshop/entries/layouts/details/{Entry.Id}");
break;
case EntryType.Profile:
- await _router.Navigate($"workshop/profiles/{Entry.Id}");
+ await _router.Navigate($"workshop/entries/profiles/details/{Entry.Id}");
break;
case EntryType.Plugin:
break;
diff --git a/src/Artemis.UI/Screens/Workshop/Entries/EntryListBaseViewModel.cs b/src/Artemis.UI/Screens/Workshop/Entries/EntryListViewModel.cs
similarity index 94%
rename from src/Artemis.UI/Screens/Workshop/Entries/EntryListBaseViewModel.cs
rename to src/Artemis.UI/Screens/Workshop/Entries/EntryListViewModel.cs
index 970b966fb..ff4d2a138 100644
--- a/src/Artemis.UI/Screens/Workshop/Entries/EntryListBaseViewModel.cs
+++ b/src/Artemis.UI/Screens/Workshop/Entries/EntryListViewModel.cs
@@ -18,7 +18,7 @@ using StrawberryShake;
namespace Artemis.UI.Screens.Workshop.Entries;
-public abstract class EntryListBaseViewModel : RoutableScreen
+public abstract class EntryListViewModel : RoutableScreen
{
private readonly INotificationService _notificationService;
private readonly IWorkshopClient _workshopClient;
@@ -31,7 +31,7 @@ public abstract class EntryListBaseViewModel : RoutableScreen getEntryListViewModel)
{
_workshopClient = workshopClient;
@@ -143,6 +143,4 @@ public abstract class EntryListBaseViewModel : RoutableScreen null;
}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/Entries/EntrySpecificationsView.axaml b/src/Artemis.UI/Screens/Workshop/Entries/EntrySpecificationsView.axaml
new file mode 100644
index 000000000..ea41ca8c5
--- /dev/null
+++ b/src/Artemis.UI/Screens/Workshop/Entries/EntrySpecificationsView.axaml
@@ -0,0 +1,105 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Icon required
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ At least one category is required
+
+
+
+
+
+
+
+
+
+
+
+
+ Markdown supported, a better editor planned
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/Entries/EntrySpecificationsView.axaml.cs b/src/Artemis.UI/Screens/Workshop/Entries/EntrySpecificationsView.axaml.cs
new file mode 100644
index 000000000..e8155b0de
--- /dev/null
+++ b/src/Artemis.UI/Screens/Workshop/Entries/EntrySpecificationsView.axaml.cs
@@ -0,0 +1,34 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Media;
+using Avalonia.Media.Immutable;
+using Avalonia.ReactiveUI;
+using AvaloniaEdit.TextMate;
+using TextMateSharp.Grammars;
+
+namespace Artemis.UI.Screens.Workshop.Entries;
+
+public partial class EntrySpecificationsView : ReactiveUserControl
+{
+ public EntrySpecificationsView()
+ {
+ InitializeComponent();
+
+ RegistryOptions options = new(ThemeName.Dark);
+ TextMate.Installation? install = DescriptionEditor.InstallTextMate(options);
+
+ install.SetGrammar(options.GetScopeByExtension(".md"));
+ }
+
+ #region Overrides of Visual
+
+ ///
+ 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);
+ }
+
+ #endregion
+}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/Entries/EntrySpecificationsViewModel.cs b/src/Artemis.UI/Screens/Workshop/Entries/EntrySpecificationsViewModel.cs
new file mode 100644
index 000000000..a4b3f439c
--- /dev/null
+++ b/src/Artemis.UI/Screens/Workshop/Entries/EntrySpecificationsViewModel.cs
@@ -0,0 +1,173 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Reactive;
+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.Screens.Workshop.Entries.Windows;
+using Artemis.UI.Shared;
+using Artemis.UI.Shared.Services;
+using Artemis.WebClient.Workshop;
+using Avalonia.Controls;
+using AvaloniaEdit.Document;
+using DynamicData;
+using DynamicData.Aggregation;
+using DynamicData.Binding;
+using ReactiveUI;
+using ReactiveUI.Validation.Extensions;
+using ReactiveUI.Validation.Helpers;
+using StrawberryShake;
+using Bitmap = Avalonia.Media.Imaging.Bitmap;
+
+namespace Artemis.UI.Screens.Workshop.Entries;
+
+public class EntrySpecificationsViewModel : ValidatableViewModelBase
+{
+ private readonly IWindowService _windowService;
+ private ObservableAsPropertyHelper? _categoriesValid;
+ private ObservableAsPropertyHelper? _iconValid;
+ private string _description = string.Empty;
+ private string _name = string.Empty;
+ private string _summary = string.Empty;
+ private Bitmap? _iconBitmap;
+ private Window? _previewWindow;
+ private TextDocument? _markdownDocument;
+
+ public EntrySpecificationsViewModel(IWorkshopClient workshopClient, IWindowService windowService)
+ {
+ _windowService = windowService;
+ SelectIcon = ReactiveCommand.CreateFromTask(ExecuteSelectIcon);
+ OpenMarkdownPreview = ReactiveCommand.Create(ExecuteOpenMarkdownPreview);
+
+ // this.WhenAnyValue(vm => vm.Description).Subscribe(d => MarkdownDocument.Text = d);
+ this.WhenActivated(d =>
+ {
+ // Load categories
+ Observable.FromAsync(workshopClient.GetCategories.ExecuteAsync).Subscribe(PopulateCategories).DisposeWith(d);
+
+ this.ClearValidationRules();
+
+ MarkdownDocument = new TextDocument(new StringTextSource(Description));
+ MarkdownDocument.TextChanged += MarkdownDocumentOnTextChanged;
+ Disposable.Create(() =>
+ {
+ _previewWindow?.Close();
+ MarkdownDocument.TextChanged -= MarkdownDocumentOnTextChanged;
+ MarkdownDocument = null;
+ ClearIcon();
+ }).DisposeWith(d);
+ });
+ }
+
+ private void MarkdownDocumentOnTextChanged(object? sender, EventArgs e)
+ {
+ Description = MarkdownDocument.Text;
+ }
+
+ public ReactiveCommand SelectIcon { get; }
+ public ReactiveCommand OpenMarkdownPreview { get; }
+
+ public ObservableCollection Categories { get; } = new();
+ public ObservableCollection Tags { get; } = new();
+ public bool CategoriesValid => _categoriesValid?.Value ?? true;
+ public bool IconValid => _iconValid?.Value ?? true;
+
+ public string Name
+ {
+ get => _name;
+ set => RaiseAndSetIfChanged(ref _name, value);
+ }
+
+ public string Summary
+ {
+ get => _summary;
+ set => RaiseAndSetIfChanged(ref _summary, value);
+ }
+
+ public string Description
+ {
+ get => _description;
+ set => RaiseAndSetIfChanged(ref _description, value);
+ }
+
+ public Bitmap? IconBitmap
+ {
+ get => _iconBitmap;
+ set => RaiseAndSetIfChanged(ref _iconBitmap, value);
+ }
+
+ public TextDocument? MarkdownDocument
+ {
+ get => _markdownDocument;
+ set => RaiseAndSetIfChanged(ref _markdownDocument, value);
+ }
+
+ public List PreselectedCategories { get; set; } = new List();
+
+ public void SetupDataValidation()
+ {
+ // Hopefully this can be avoided in the future
+ // https://github.com/reactiveui/ReactiveUI.Validation/discussions/558
+ this.ValidationRule(vm => vm.Name, s => !string.IsNullOrWhiteSpace(s), "Name is required");
+ this.ValidationRule(vm => vm.Summary, s => !string.IsNullOrWhiteSpace(s), "Summary is required");
+ this.ValidationRule(vm => vm.Description, s => !string.IsNullOrWhiteSpace(s), "Description is required");
+
+ // These don't use inputs that support validation messages, do so manually
+ ValidationHelper iconRule = this.ValidationRule(vm => vm.IconBitmap, s => s != null, "Icon required");
+ ValidationHelper categoriesRule = this.ValidationRule(vm => vm.Categories, Categories.ToObservableChangeSet().AutoRefresh(c => c.IsSelected).Filter(c => c.IsSelected).IsNotEmpty(),
+ "At least one category must be selected"
+ );
+ _iconValid = iconRule.ValidationChanged.Select(c => c.IsValid).ToProperty(this, vm => vm.IconValid);
+ _categoriesValid = categoriesRule.ValidationChanged.Select(c => c.IsValid).ToProperty(this, vm => vm.CategoriesValid);
+ }
+
+ private async Task ExecuteSelectIcon()
+ {
+ string[]? result = await _windowService.CreateOpenFileDialog()
+ .HavingFilter(f => f.WithExtension("png").WithExtension("jpg").WithExtension("bmp").WithName("Bitmap image"))
+ .ShowAsync();
+
+ if (result == null)
+ return;
+
+ IconBitmap?.Dispose();
+ IconBitmap = BitmapExtensions.LoadAndResize(result[0], 128);
+ }
+
+ private void ExecuteOpenMarkdownPreview()
+ {
+ if (_previewWindow != null)
+ {
+ _previewWindow.Activate();
+ return;
+ }
+
+ _previewWindow = _windowService.ShowWindow(out MarkdownPreviewViewModel _, this.WhenAnyValue(vm => vm.Description));
+ _previewWindow.Closed += PreviewWindowOnClosed;
+ }
+
+ private void PreviewWindowOnClosed(object? sender, EventArgs e)
+ {
+ if (_previewWindow != null)
+ _previewWindow.Closed -= PreviewWindowOnClosed;
+ _previewWindow = null;
+ }
+
+ private void ClearIcon()
+ {
+ IconBitmap?.Dispose();
+ IconBitmap = null;
+ }
+
+ private void PopulateCategories(IOperationResult result)
+ {
+ Categories.Clear();
+ if (result.Data != null)
+ Categories.AddRange(result.Data.Categories.Select(c => new CategoryViewModel(c) {IsSelected = PreselectedCategories.Contains(c.Id)}));
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/Entries/Tabs/LayoutListView.axaml b/src/Artemis.UI/Screens/Workshop/Entries/Tabs/LayoutListView.axaml
new file mode 100644
index 000000000..575f24ffc
--- /dev/null
+++ b/src/Artemis.UI/Screens/Workshop/Entries/Tabs/LayoutListView.axaml
@@ -0,0 +1,41 @@
+
+
+
+
+
+ Categories
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/Layout/LayoutListView.axaml.cs b/src/Artemis.UI/Screens/Workshop/Entries/Tabs/LayoutListView.axaml.cs
similarity index 77%
rename from src/Artemis.UI/Screens/Workshop/Layout/LayoutListView.axaml.cs
rename to src/Artemis.UI/Screens/Workshop/Entries/Tabs/LayoutListView.axaml.cs
index dff088223..6b574bb29 100644
--- a/src/Artemis.UI/Screens/Workshop/Layout/LayoutListView.axaml.cs
+++ b/src/Artemis.UI/Screens/Workshop/Entries/Tabs/LayoutListView.axaml.cs
@@ -1,6 +1,6 @@
using Avalonia.ReactiveUI;
-namespace Artemis.UI.Screens.Workshop.Layout;
+namespace Artemis.UI.Screens.Workshop.Entries.Tabs;
public partial class LayoutListView : ReactiveUserControl
{
diff --git a/src/Artemis.UI/Screens/Workshop/Layout/LayoutListViewModel.cs b/src/Artemis.UI/Screens/Workshop/Entries/Tabs/LayoutListViewModel.cs
similarity index 85%
rename from src/Artemis.UI/Screens/Workshop/Layout/LayoutListViewModel.cs
rename to src/Artemis.UI/Screens/Workshop/Entries/Tabs/LayoutListViewModel.cs
index 8f5b108e7..a32408d09 100644
--- a/src/Artemis.UI/Screens/Workshop/Layout/LayoutListViewModel.cs
+++ b/src/Artemis.UI/Screens/Workshop/Entries/Tabs/LayoutListViewModel.cs
@@ -1,13 +1,12 @@
using System;
using Artemis.UI.Screens.Workshop.Categories;
-using Artemis.UI.Screens.Workshop.Entries;
using Artemis.UI.Shared.Routing;
using Artemis.UI.Shared.Services;
using Artemis.WebClient.Workshop;
-namespace Artemis.UI.Screens.Workshop.Layout;
+namespace Artemis.UI.Screens.Workshop.Entries.Tabs;
-public class LayoutListViewModel : EntryListBaseViewModel
+public class LayoutListViewModel : EntryListViewModel
{
///
public LayoutListViewModel(IWorkshopClient workshopClient,
@@ -24,7 +23,7 @@ public class LayoutListViewModel : EntryListBaseViewModel
///
protected override string GetPagePath(int page)
{
- return $"workshop/layouts/{page}";
+ return $"workshop/entries/layouts/{page}";
}
///
diff --git a/src/Artemis.UI/Screens/Workshop/Entries/Tabs/ProfileListView.axaml b/src/Artemis.UI/Screens/Workshop/Entries/Tabs/ProfileListView.axaml
new file mode 100644
index 000000000..e7b733741
--- /dev/null
+++ b/src/Artemis.UI/Screens/Workshop/Entries/Tabs/ProfileListView.axaml
@@ -0,0 +1,42 @@
+
+
+
+
+
+ Categories
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/Profile/ProfileListView.axaml.cs b/src/Artemis.UI/Screens/Workshop/Entries/Tabs/ProfileListView.axaml.cs
similarity index 78%
rename from src/Artemis.UI/Screens/Workshop/Profile/ProfileListView.axaml.cs
rename to src/Artemis.UI/Screens/Workshop/Entries/Tabs/ProfileListView.axaml.cs
index 6c55237f5..62adad88b 100644
--- a/src/Artemis.UI/Screens/Workshop/Profile/ProfileListView.axaml.cs
+++ b/src/Artemis.UI/Screens/Workshop/Entries/Tabs/ProfileListView.axaml.cs
@@ -1,6 +1,6 @@
using Avalonia.ReactiveUI;
-namespace Artemis.UI.Screens.Workshop.Profile;
+namespace Artemis.UI.Screens.Workshop.Entries.Tabs;
public partial class ProfileListView : ReactiveUserControl
{
diff --git a/src/Artemis.UI/Screens/Workshop/Profile/ProfileListViewModel.cs b/src/Artemis.UI/Screens/Workshop/Entries/Tabs/ProfileListViewModel.cs
similarity index 84%
rename from src/Artemis.UI/Screens/Workshop/Profile/ProfileListViewModel.cs
rename to src/Artemis.UI/Screens/Workshop/Entries/Tabs/ProfileListViewModel.cs
index 521c9a351..49df897db 100644
--- a/src/Artemis.UI/Screens/Workshop/Profile/ProfileListViewModel.cs
+++ b/src/Artemis.UI/Screens/Workshop/Entries/Tabs/ProfileListViewModel.cs
@@ -1,13 +1,12 @@
using System;
using Artemis.UI.Screens.Workshop.Categories;
-using Artemis.UI.Screens.Workshop.Entries;
using Artemis.UI.Shared.Routing;
using Artemis.UI.Shared.Services;
using Artemis.WebClient.Workshop;
-namespace Artemis.UI.Screens.Workshop.Profile;
+namespace Artemis.UI.Screens.Workshop.Entries.Tabs;
-public class ProfileListViewModel : EntryListBaseViewModel
+public class ProfileListViewModel : EntryListViewModel
{
///
public ProfileListViewModel(IWorkshopClient workshopClient,
@@ -24,7 +23,7 @@ public class ProfileListViewModel : EntryListBaseViewModel
///
protected override string GetPagePath(int page)
{
- return $"workshop/profiles/{page}";
+ return $"workshop/entries/profiles/{page}";
}
///
diff --git a/src/Artemis.UI/Screens/Workshop/Entries/Windows/MarkdownPreviewView.axaml b/src/Artemis.UI/Screens/Workshop/Entries/Windows/MarkdownPreviewView.axaml
new file mode 100644
index 000000000..8898bef0a
--- /dev/null
+++ b/src/Artemis.UI/Screens/Workshop/Entries/Windows/MarkdownPreviewView.axaml
@@ -0,0 +1,43 @@
+
+
+
+ Markdown Previewer
+
+ In this window you can preview the Markdown you're writing in the main window of the application.
+
+ The preview updates realtime, so it might be a good idea to keep this window visible while you're typing.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/Entries/Windows/MarkdownPreviewView.axaml.cs b/src/Artemis.UI/Screens/Workshop/Entries/Windows/MarkdownPreviewView.axaml.cs
new file mode 100644
index 000000000..ec5b62868
--- /dev/null
+++ b/src/Artemis.UI/Screens/Workshop/Entries/Windows/MarkdownPreviewView.axaml.cs
@@ -0,0 +1,12 @@
+
+using Artemis.UI.Shared;
+
+namespace Artemis.UI.Screens.Workshop.Entries.Windows;
+
+public partial class MarkdownPreviewView : ReactiveAppWindow
+{
+ public MarkdownPreviewView()
+ {
+ InitializeComponent();
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/Entries/Windows/MarkdownPreviewViewModel.cs b/src/Artemis.UI/Screens/Workshop/Entries/Windows/MarkdownPreviewViewModel.cs
new file mode 100644
index 000000000..4e8680d0e
--- /dev/null
+++ b/src/Artemis.UI/Screens/Workshop/Entries/Windows/MarkdownPreviewViewModel.cs
@@ -0,0 +1,21 @@
+using System;
+using Artemis.UI.Shared;
+
+namespace Artemis.UI.Screens.Workshop.Entries.Windows;
+
+public class MarkdownPreviewViewModel : ActivatableViewModelBase
+{
+ public event EventHandler? Closed;
+
+ public IObservable Markdown { get; }
+
+ public MarkdownPreviewViewModel(IObservable markdown)
+ {
+ Markdown = markdown;
+ }
+
+ protected virtual void OnClosed()
+ {
+ Closed?.Invoke(this, EventArgs.Empty);
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/Home/WorkshopHomeView.axaml b/src/Artemis.UI/Screens/Workshop/Home/WorkshopHomeView.axaml
index c3b4593f6..433f87c50 100644
--- a/src/Artemis.UI/Screens/Workshop/Home/WorkshopHomeView.axaml
+++ b/src/Artemis.UI/Screens/Workshop/Home/WorkshopHomeView.axaml
@@ -41,7 +41,7 @@
-
+
+
-
-
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/SpecificationsStepViewModel.cs b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/SpecificationsStepViewModel.cs
index 495e5d591..ebb75cd42 100644
--- a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/SpecificationsStepViewModel.cs
+++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/SpecificationsStepViewModel.cs
@@ -1,93 +1,42 @@
using System;
using System.Collections.Generic;
-using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Reactive;
using System.Reactive.Disposables;
-using System.Reactive.Linq;
-using System.Threading.Tasks;
using Artemis.UI.Extensions;
-using Artemis.UI.Screens.Workshop.Categories;
+using Artemis.UI.Screens.Workshop.Entries;
using Artemis.UI.Screens.Workshop.SubmissionWizard.Steps.Profile;
-using Artemis.UI.Shared.Services;
using Artemis.WebClient.Workshop;
using Avalonia.Threading;
using DynamicData;
-using DynamicData.Aggregation;
-using DynamicData.Binding;
using ReactiveUI;
using ReactiveUI.Validation.Extensions;
-using ReactiveUI.Validation.Helpers;
-using StrawberryShake;
-using Bitmap = Avalonia.Media.Imaging.Bitmap;
namespace Artemis.UI.Screens.Workshop.SubmissionWizard.Steps;
public class SpecificationsStepViewModel : SubmissionViewModel
{
- private readonly IWindowService _windowService;
- private ObservableAsPropertyHelper? _categoriesValid;
- private ObservableAsPropertyHelper? _iconValid;
- private string _description = string.Empty;
- private string _name = string.Empty;
- private string _summary = string.Empty;
- private Bitmap? _iconBitmap;
-
- public SpecificationsStepViewModel(IWorkshopClient workshopClient, IWindowService windowService)
+ public SpecificationsStepViewModel(EntrySpecificationsViewModel entrySpecificationsViewModel)
{
- _windowService = windowService;
+ EntrySpecificationsViewModel = entrySpecificationsViewModel;
GoBack = ReactiveCommand.Create(ExecuteGoBack);
- Continue = ReactiveCommand.Create(ExecuteContinue, ValidationContext.Valid);
- SelectIcon = ReactiveCommand.CreateFromTask(ExecuteSelectIcon);
+ Continue = ReactiveCommand.Create(ExecuteContinue, EntrySpecificationsViewModel.ValidationContext.Valid);
- this.WhenActivated(d =>
+ this.WhenActivated((CompositeDisposable d) =>
{
DisplayName = $"{State.EntryType} Information";
- // Load categories
- Observable.FromAsync(workshopClient.GetCategories.ExecuteAsync).Subscribe(PopulateCategories).DisposeWith(d);
-
// Apply the state
ApplyFromState();
- this.ClearValidationRules();
- Disposable.Create(ClearIcon).DisposeWith(d);
+ EntrySpecificationsViewModel.ClearValidationRules();
});
}
+ public EntrySpecificationsViewModel EntrySpecificationsViewModel { get; }
public override ReactiveCommand Continue { get; }
public override ReactiveCommand GoBack { get; }
- public ReactiveCommand SelectIcon { get; }
-
- public ObservableCollection Categories { get; } = new();
- public ObservableCollection Tags { get; } = new();
- public bool CategoriesValid => _categoriesValid?.Value ?? true;
- public bool IconValid => _iconValid?.Value ?? true;
-
- public string Name
- {
- get => _name;
- set => RaiseAndSetIfChanged(ref _name, value);
- }
-
- public string Summary
- {
- get => _summary;
- set => RaiseAndSetIfChanged(ref _summary, value);
- }
-
- public string Description
- {
- get => _description;
- set => RaiseAndSetIfChanged(ref _description, value);
- }
-
- public Bitmap? IconBitmap
- {
- get => _iconBitmap;
- set => RaiseAndSetIfChanged(ref _iconBitmap, value);
- }
private void ExecuteGoBack()
{
@@ -110,101 +59,61 @@ public class SpecificationsStepViewModel : SubmissionViewModel
private void ExecuteContinue()
{
- if (!ValidationContext.Validations.Any())
+ if (!EntrySpecificationsViewModel.ValidationContext.Validations.Any())
{
// The ValidationContext seems to update asynchronously, so stop and schedule a retry
- SetupDataValidation();
+ EntrySpecificationsViewModel.SetupDataValidation();
Dispatcher.UIThread.Post(ExecuteContinue);
return;
}
ApplyToState();
-
- if (!ValidationContext.GetIsValid())
+
+ if (!EntrySpecificationsViewModel.ValidationContext.GetIsValid())
return;
-
+
State.ChangeScreen();
}
- private async Task ExecuteSelectIcon()
- {
- string[]? result = await _windowService.CreateOpenFileDialog()
- .HavingFilter(f => f.WithExtension("png").WithExtension("jpg").WithExtension("bmp").WithName("Bitmap image"))
- .ShowAsync();
-
- if (result == null)
- return;
-
- IconBitmap?.Dispose();
- IconBitmap = BitmapExtensions.LoadAndResize(result[0], 128);
- }
-
- private void ClearIcon()
- {
- IconBitmap?.Dispose();
- IconBitmap = null;
- }
-
- private void PopulateCategories(IOperationResult result)
- {
- Categories.Clear();
- if (result.Data != null)
- Categories.AddRange(result.Data.Categories.Select(c => new CategoryViewModel(c) {IsSelected = State.Categories.Contains(c.Id)}));
- }
-
- private void SetupDataValidation()
- {
- // Hopefully this can be avoided in the future
- // https://github.com/reactiveui/ReactiveUI.Validation/discussions/558
- this.ValidationRule(vm => vm.Name, s => !string.IsNullOrWhiteSpace(s), "Name is required");
- this.ValidationRule(vm => vm.Summary, s => !string.IsNullOrWhiteSpace(s), "Summary is required");
- this.ValidationRule(vm => vm.Description, s => !string.IsNullOrWhiteSpace(s), "Description is required");
-
- // These don't use inputs that support validation messages, do so manually
- ValidationHelper iconRule = this.ValidationRule(vm => vm.IconBitmap, s => s != null, "Icon required");
- ValidationHelper categoriesRule = this.ValidationRule(vm => vm.Categories, Categories.ToObservableChangeSet().AutoRefresh(c => c.IsSelected).Filter(c => c.IsSelected).IsNotEmpty(),
- "At least one category must be selected"
- );
- _iconValid = iconRule.ValidationChanged.Select(c => c.IsValid).ToProperty(this, vm => vm.IconValid);
- _categoriesValid = categoriesRule.ValidationChanged.Select(c => c.IsValid).ToProperty(this, vm => vm.CategoriesValid);
- }
-
private void ApplyFromState()
{
// Basic fields
- Name = State.Name;
- Summary = State.Summary;
- Description = State.Description;
+ EntrySpecificationsViewModel.Name = State.Name;
+ EntrySpecificationsViewModel.Summary = State.Summary;
+ EntrySpecificationsViewModel.Description = State.Description;
// Tags
- Tags.Clear();
- Tags.AddRange(State.Tags);
+ EntrySpecificationsViewModel.Tags.Clear();
+ EntrySpecificationsViewModel.Tags.AddRange(State.Tags);
+
+ // Categories
+ EntrySpecificationsViewModel.PreselectedCategories = State.Categories;
// Icon
if (State.Icon != null)
{
State.Icon.Seek(0, SeekOrigin.Begin);
- IconBitmap = BitmapExtensions.LoadAndResize(State.Icon, 128);
+ EntrySpecificationsViewModel.IconBitmap = BitmapExtensions.LoadAndResize(State.Icon, 128);
}
}
private void ApplyToState()
{
// Basic fields
- State.Name = Name;
- State.Summary = Summary;
- State.Description = Description;
+ State.Name = EntrySpecificationsViewModel.Name;
+ State.Summary = EntrySpecificationsViewModel.Summary;
+ State.Description = EntrySpecificationsViewModel.Description;
// Categories and tasks
- State.Categories = Categories.Where(c => c.IsSelected).Select(c => c.Id).ToList();
- State.Tags = new List(Tags);
+ State.Categories = EntrySpecificationsViewModel.Categories.Where(c => c.IsSelected).Select(c => c.Id).ToList();
+ State.Tags = new List(EntrySpecificationsViewModel.Tags);
// Icon
State.Icon?.Dispose();
- if (IconBitmap != null)
+ if (EntrySpecificationsViewModel.IconBitmap != null)
{
State.Icon = new MemoryStream();
- IconBitmap.Save(State.Icon);
+ EntrySpecificationsViewModel.IconBitmap.Save(State.Icon);
}
else
{
diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/UploadStepViewModel.cs b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/UploadStepViewModel.cs
index 14e56958a..fc726e908 100644
--- a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/UploadStepViewModel.cs
+++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/UploadStepViewModel.cs
@@ -152,13 +152,13 @@ public class UploadStepViewModel : SubmissionViewModel
switch (State.EntryType)
{
case EntryType.Layout:
- await _router.Navigate($"workshop/layouts/{_entryId.Value}");
+ await _router.Navigate($"workshop/entries/layouts/{_entryId.Value}");
break;
case EntryType.Plugin:
- await _router.Navigate($"workshop/plugins/{_entryId.Value}");
+ await _router.Navigate($"workshop/entries/plugins/{_entryId.Value}");
break;
case EntryType.Profile:
- await _router.Navigate($"workshop/profiles/{_entryId.Value}");
+ await _router.Navigate($"workshop/entries/profiles/{_entryId.Value}");
break;
default:
throw new ArgumentOutOfRangeException();
diff --git a/src/Artemis.UI/Services/DebugService.cs b/src/Artemis.UI/Services/DebugService.cs
index 138bfa994..62b644f29 100644
--- a/src/Artemis.UI/Services/DebugService.cs
+++ b/src/Artemis.UI/Services/DebugService.cs
@@ -22,7 +22,8 @@ public class DebugService : IDebugService
private void CreateDebugger()
{
- _debugViewModel = _windowService.ShowWindow();
+ _windowService.ShowWindow(out DebugViewModel debugViewModel);
+ _debugViewModel = debugViewModel;
}
public void ClearDebugger()
diff --git a/src/Artemis.UI/Styles/Artemis.axaml b/src/Artemis.UI/Styles/Artemis.axaml
index 0bd25e4aa..a4ac040f9 100644
--- a/src/Artemis.UI/Styles/Artemis.axaml
+++ b/src/Artemis.UI/Styles/Artemis.axaml
@@ -1,7 +1,9 @@
+ xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
+ xmlns:aedit="using:AvaloniaEdit"
+ xmlns:aedit2="using:AvaloniaEdit.Editing">
@@ -9,6 +11,18 @@
+
+
+
+
+
+
+