1
0
mirror of https://github.com/Artemis-RGB/Artemis synced 2025-12-12 13:28:33 +00:00

Workshop - Add ability to mark entry as default (admin only)

Workshop - Allow editing other user's entries (admin only)
Workshop - Show last update date in entry list instead of creation date
This commit is contained in:
Robert 2025-04-19 11:37:28 +02:00
parent 10a10b9149
commit 3caf782d8e
13 changed files with 69 additions and 23 deletions

View File

@ -15,7 +15,7 @@
</UserControl.Resources>
<Panel>
<StackPanel IsVisible="{CompiledBinding Entry, Converter={x:Static ObjectConverters.IsNull}}">
<Border Classes="skeleton-text"
<Border Classes="skeleton-text"
HorizontalAlignment="Center"
Margin="30 30 30 0"
Width="80"
@ -43,13 +43,20 @@
ClipToBounds="True">
<Image Stretch="UniformToFill" il:ImageLoader.Source="{CompiledBinding Entry.Id, Converter={StaticResource EntryIconUriConverter}, Mode=OneWay}" />
</Border>
<Button Classes="icon-button"
VerticalAlignment="Top"
HorizontalAlignment="Right"
Command="{CompiledBinding CopyShareLink}"
ToolTip.Tip="Copy share link">
<avalonia:MaterialIcon Kind="ShareVariant" />
</Button>
<StackPanel VerticalAlignment="Top" HorizontalAlignment="Right" Orientation="Horizontal" Spacing="5">
<Button IsVisible="{CompiledBinding IsAdministrator}"
Classes="icon-button"
Command="{CompiledBinding GoToEdit}"
ToolTip.Tip="Edit submission">
<avalonia:MaterialIcon Kind="Edit" />
</Button>
<Button Classes="icon-button"
Command="{CompiledBinding CopyShareLink}"
ToolTip.Tip="Copy share link">
<avalonia:MaterialIcon Kind="ShareVariant" />
</Button>
</StackPanel>
</Panel>
<TextBlock Theme="{StaticResource TitleTextBlockStyle}"
@ -62,15 +69,15 @@
<StackPanel Orientation="Horizontal">
<TextBlock Classes="subtitle" TextTrimming="CharacterEllipsis" Text="{CompiledBinding Entry.Author, FallbackValue=Author}" />
<avalonia:MaterialIcon IsVisible="{CompiledBinding Entry.IsOfficial}"
Kind="ShieldStar"
Kind="ShieldStar"
Foreground="{DynamicResource SystemAccentColorLight1}"
Margin="2 0 0 0"
Width="18"
Height="18"
Height="18"
HorizontalAlignment="Left"
ToolTip.Tip="Official entry by the Artemis team" />
</StackPanel>
<TextBlock Margin="0 8" TextWrapping="Wrap" Text="{CompiledBinding Entry.Summary, FallbackValue=Summary}" />
<!-- Categories -->

View File

@ -25,7 +25,7 @@ public partial class EntryInfoViewModel : ActivatableViewModelBase
[Notify] private DateTimeOffset? _updatedAt;
[Notify] private bool _canBeManaged;
public EntryInfoViewModel(IRouter router, INotificationService notificationService, IWorkshopService workshopService)
public EntryInfoViewModel(IRouter router, INotificationService notificationService, IWorkshopService workshopService, IAuthenticationService authenticationService)
{
_router = router;
_notificationService = notificationService;
@ -38,8 +38,12 @@ public partial class EntryInfoViewModel : ActivatableViewModelBase
.Subscribe(_ => CanBeManaged = Entry != null && Entry.EntryType != EntryType.Profile && workshopService.GetInstalledEntry(Entry.Id) != null)
.DisposeWith(d);
});
IsAdministrator = authenticationService.GetRoles().Contains("Administrator");
}
public bool IsAdministrator { get; }
public void SetEntry(IEntryDetails? entry)
{
Entry = entry;
@ -55,6 +59,14 @@ public partial class EntryInfoViewModel : ActivatableViewModelBase
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 GoToEdit()
{
if (Entry == null)
return;
await _router.Navigate($"workshop/library/submissions/{Entry.Id}");
}
public async Task GoToManage()
{

View File

@ -63,6 +63,8 @@
<Label Target="Summary" Margin="0 5 0 0">Summary</Label>
<TextBox Name="Summary" Text="{CompiledBinding Summary}"></TextBox>
<CheckBox IsVisible="{CompiledBinding IsAdministrator}" IsChecked="{CompiledBinding IsDefault}">Download by default (admin only)</CheckBox>
</StackPanel>
</Grid>

View File

@ -11,6 +11,7 @@ using Artemis.UI.Screens.Workshop.Categories;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services;
using Artemis.WebClient.Workshop;
using Artemis.WebClient.Workshop.Services;
using Avalonia.Media.Imaging;
using AvaloniaEdit.Document;
using DynamicData;
@ -34,10 +35,11 @@ public partial class EntrySpecificationsViewModel : ValidatableViewModelBase
[Notify] private string _name = string.Empty;
[Notify] private string _summary = string.Empty;
[Notify] private string _description = string.Empty;
[Notify] private bool _isDefault;
[Notify] private Bitmap? _iconBitmap;
[Notify(Setter.Private)] private bool _iconChanged;
public EntrySpecificationsViewModel(IWorkshopClient workshopClient, IWindowService windowService)
public EntrySpecificationsViewModel(IWorkshopClient workshopClient, IWindowService windowService, IAuthenticationService authenticationService)
{
_workshopClient = workshopClient;
_windowService = windowService;
@ -65,8 +67,9 @@ public partial class EntrySpecificationsViewModel : ValidatableViewModelBase
_descriptionValid = descriptionRule.ValidationChanged.Select(c => c.IsValid).ToProperty(this, vm => vm.DescriptionValid);
this.WhenActivatedAsync(async _ => await PopulateCategories());
IsAdministrator = authenticationService.GetRoles().Contains("Administrator");
}
public ReactiveCommand<Unit, Unit> SelectIcon { get; }
public ObservableCollection<CategoryViewModel> Categories { get; } = new();
@ -76,7 +79,8 @@ public partial class EntrySpecificationsViewModel : ValidatableViewModelBase
public bool CategoriesValid => _categoriesValid.Value ;
public bool IconValid => _iconValid.Value;
public bool DescriptionValid => _descriptionValid.Value;
public bool IsAdministrator { get; }
public List<long> PreselectedCategories { get; set; } = new();
private async Task ExecuteSelectIcon()

View File

@ -79,7 +79,7 @@
<!-- Info -->
<StackPanel Grid.Column="2" Grid.Row="0" Margin="0 0 4 0" HorizontalAlignment="Right">
<TextBlock TextAlignment="Right" Text="{CompiledBinding Entry.CreatedAt, FallbackValue=01-01-1337, Converter={StaticResource DateTimeConverter}}" />
<TextBlock TextAlignment="Right" Text="{CompiledBinding Entry.LatestRelease.CreatedAt, FallbackValue=01-01-1337, Converter={StaticResource DateTimeConverter}}" />
<TextBlock TextAlignment="Right">
<avalonia:MaterialIcon Kind="Downloads" />
<Run Classes="h5" Text="{CompiledBinding Entry.Downloads, FallbackValue=0}" />

View File

@ -59,7 +59,7 @@ public partial class SubmissionDetailsViewModel : RoutableScreen
DiscardChanges = ReactiveCommand.CreateFromTask(ExecuteDiscardChanges, this.WhenAnyValue(vm => vm.HasChanges));
SaveChanges = ReactiveCommand.CreateFromTask(ExecuteSaveChanges, this.WhenAnyValue(vm => vm.HasChanges));
}
public ObservableCollection<ImageSubmissionViewModel> Images { get; } = new();
public ReactiveCommand<Unit, Unit> AddImage { get; }
public ReactiveCommand<Unit, Unit> SaveChanges { get; }
@ -105,6 +105,7 @@ public partial class SubmissionDetailsViewModel : RoutableScreen
specificationsViewModel.Name = Entry.Name;
specificationsViewModel.Summary = Entry.Summary;
specificationsViewModel.Description = Entry.Description;
specificationsViewModel.IsDefault = Entry.IsDefault;
specificationsViewModel.PreselectedCategories = Entry.Categories.Select(c => c.Id).ToList();
specificationsViewModel.Tags.Clear();
@ -169,6 +170,7 @@ public partial class SubmissionDetailsViewModel : RoutableScreen
HasChanges = EntrySpecificationsViewModel.Name != Entry.Name ||
EntrySpecificationsViewModel.Description != Entry.Description ||
EntrySpecificationsViewModel.Summary != Entry.Summary ||
EntrySpecificationsViewModel.IsDefault != Entry.IsDefault ||
EntrySpecificationsViewModel.IconChanged ||
!tags.SequenceEqual(Entry.Tags.Select(t => t.Name).OrderBy(t => t)) ||
!categories.SequenceEqual(Entry.Categories.Select(c => c.Id).OrderBy(c => c)) ||
@ -192,6 +194,7 @@ public partial class SubmissionDetailsViewModel : RoutableScreen
Name = EntrySpecificationsViewModel.Name,
Summary = EntrySpecificationsViewModel.Summary,
Description = EntrySpecificationsViewModel.Description,
IsDefault = EntrySpecificationsViewModel.IsDefault,
Categories = EntrySpecificationsViewModel.SelectedCategories,
Tags = EntrySpecificationsViewModel.Tags
};
@ -233,7 +236,7 @@ public partial class SubmissionDetailsViewModel : RoutableScreen
HasChanges = false;
await _router.Reload();
}
private async Task ExecuteAddImage(CancellationToken arg)
{
string[]? result = await _windowService.CreateOpenFileDialog().WithAllowMultiple().HavingFilter(f => f.WithBitmaps()).ShowAsync();

View File

@ -67,7 +67,7 @@
<!-- Info -->
<StackPanel Grid.Column="2" Grid.Row="0" Margin="0 0 4 0" HorizontalAlignment="Right">
<TextBlock TextAlignment="Right" Text="{CompiledBinding Entry.CreatedAt, FallbackValue=01-01-1337, Converter={StaticResource DateTimeConverter}}" />
<TextBlock TextAlignment="Right" Text="{CompiledBinding Entry.LatestRelease.CreatedAt, FallbackValue=01-01-1337, Converter={StaticResource DateTimeConverter}}" />
<TextBlock TextAlignment="Right">
<avalonia:MaterialIcon Kind="Downloads" />
<Run Classes="h5" Text="{CompiledBinding Entry.Downloads, FallbackValue=0}" />

View File

@ -32,6 +32,7 @@ public class SubmissionWizardState : IDisposable
public Stream? Icon { get; set; }
public string Summary { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public bool IsDefault { get; set; }
public List<long> Categories { get; set; } = new();
public List<string> Tags { get; set; } = new();

View File

@ -1,3 +1,4 @@
using System.ComponentModel.DataAnnotations;
using System.Reactive.Disposables;
using Artemis.UI.Screens.Workshop.SubmissionWizard.Steps.Layout;
using Artemis.UI.Screens.Workshop.SubmissionWizard.Steps.Plugin;
@ -10,7 +11,7 @@ namespace Artemis.UI.Screens.Workshop.SubmissionWizard.Steps;
public partial class ChangelogStepViewModel : SubmissionViewModel
{
[Notify] private string? _changelog;
[Notify] private string _changelog = string.Empty;
public ChangelogStepViewModel()
{
@ -18,7 +19,7 @@ public partial class ChangelogStepViewModel : SubmissionViewModel
Continue = ReactiveCommand.Create(ExecuteContinue);
ContinueText = "Submit";
this.WhenActivated((CompositeDisposable _) => Changelog = State.Changelog);
this.WhenActivated((CompositeDisposable _) => Changelog = State.Changelog ?? string.Empty);
}
private void ExecuteContinue()
@ -29,7 +30,7 @@ public partial class ChangelogStepViewModel : SubmissionViewModel
private void ExecuteGoBack()
{
State.Changelog = Changelog;
State.Changelog = string.IsNullOrWhiteSpace(Changelog) ? null : Changelog;
if (State.EntryType == EntryType.Layout)
State.ChangeScreen<LayoutInfoStepViewModel>();
else if (State.EntryType == EntryType.Plugin)

View File

@ -65,6 +65,7 @@ public partial class SpecificationsStepViewModel : SubmissionViewModel
viewModel.Name = State.Name;
viewModel.Summary = State.Summary;
viewModel.Description = State.Description;
viewModel.IsDefault = State.IsDefault;
// Tags
viewModel.Tags.Clear();
@ -93,6 +94,7 @@ public partial class SpecificationsStepViewModel : SubmissionViewModel
State.Name = EntrySpecificationsViewModel.Name;
State.Summary = EntrySpecificationsViewModel.Summary;
State.Description = EntrySpecificationsViewModel.Description;
State.IsDefault = EntrySpecificationsViewModel.IsDefault;
// Categories and tasks
State.Categories = EntrySpecificationsViewModel.Categories.Where(c => c.IsSelected).Select(c => c.Id).ToList();

View File

@ -26,6 +26,10 @@ fragment submittedEntry on Entry {
entryType
downloads
createdAt
isDefault
latestRelease {
...release
}
}
fragment entrySummary on Entry {
@ -38,6 +42,9 @@ fragment entrySummary on Entry {
downloads
createdAt
latestReleaseId
latestRelease {
...release
}
categories {
...category
}

View File

@ -283,6 +283,12 @@ internal class AuthenticationService : CorePropertyChanged, IAuthenticationServi
return emailVerified?.Value.ToLower() == "true";
}
/// <inheritdoc />
public List<string> GetRoles()
{
return Claims.Where(c => c.Type == JwtClaimTypes.Role).Select(c => c.Value).ToList();
}
private async Task<bool> InternalAutoLogin(bool force = false)
{
if (!force && _isLoggedInSubject.Value)

View File

@ -15,4 +15,5 @@ public interface IAuthenticationService : IProtectedArtemisService
Task Login(CancellationToken cancellationToken);
Task Logout();
bool GetIsEmailVerified();
List<string> GetRoles();
}