1
0
mirror of https://github.com/Artemis-RGB/Artemis synced 2026-02-04 02:43:32 +00:00

Show multiple recent releases per entry if available

This commit is contained in:
Robert 2025-12-31 09:44:14 +01:00
parent 562049681f
commit 559434630d
13 changed files with 170 additions and 91 deletions

View File

@ -175,7 +175,7 @@ public class WindowsUpdateNotificationProvider : IUpdateNotificationProvider
else if (action == "disable-workshop-notifications")
_workshopUpdateService.DisableNotifications();
else if (action == "view-library")
NavigateToRoute("workshop/library");
NavigateToRoute("workshop/library/recently-updated");
}
private void NavigateToRoute(string route)

View File

@ -39,7 +39,7 @@ public partial class EntryInfoViewModel : ActivatableViewModelBase
.DisposeWith(d);
});
IsAdministrator = authenticationService.GetRoles().Contains("Administrator");
IsAdministrator = authenticationService.Roles.Contains("Administrator");
}
public bool IsAdministrator { get; }

View File

@ -69,7 +69,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);
IsAdministrator = authenticationService.GetRoles().Contains("Administrator");
IsAdministrator = authenticationService.Roles.Contains("Administrator");
this.WhenActivatedAsync(async _ => await PopulateCategories());
this.WhenAnyValue(vm => vm.Fit).Subscribe(_ => UpdateIcon());
}

View File

@ -8,6 +8,7 @@
xmlns:converters="clr-namespace:Artemis.UI.Converters"
xmlns:mdxaml="https://github.com/whistyun/Markdown.Avalonia.Tight"
xmlns:ui="clr-namespace:Artemis.UI"
xmlns:workshop="clr-namespace:Artemis.WebClient.Workshop;assembly=Artemis.WebClient.Workshop"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Workshop.Library.Tabs.RecentlyUpdatedItemView"
x:DataType="tabs:RecentlyUpdatedItemViewModel">
@ -16,27 +17,43 @@
<converters:DateTimeConverter x:Key="DateTimeConverter" />
<ui:ArtemisLinkCommand x:Key="ArtemisLinkCommand" />
</UserControl.Resources>
<Border MinHeight="110"
MaxHeight="140"
Padding="12"
<UserControl.Styles>
<Style Selector="Border.badge">
<Setter Property="Background" Value="{DynamicResource ControlSolidFillColorDefaultBrush}" />
<Setter Property="CornerRadius" Value="12" />
<Setter Property="Padding" Value="6 1"></Setter>
<Setter Property="TextBlock.FontSize" Value="12" />
</Style>
<Style Selector="StackPanel.entry-clickable:pointerover TextBlock">
<Setter Property="TextDecorations" Value="Underline" />
</Style>
<Style Selector="TextBlock.version-clickable:pointerover">
<Setter Property="TextDecorations" Value="Underline" />
</Style>
</UserControl.Styles>
<Border Padding="12"
HorizontalAlignment="Stretch"
Classes="card">
<Grid ColumnDefinitions="Auto,*" RowDefinitions="Auto, Auto">
<StackPanel Grid.Column="0" Grid.Row="0" Orientation="Horizontal">
<StackPanel Grid.Column="0"
Grid.Row="0"
Classes="entry-clickable"
Orientation="Horizontal"
Cursor="Hand"
Background="Transparent"
PointerPressed="Entry_OnPointerPressed">
<!-- Icon -->
<Border CornerRadius="6"
VerticalAlignment="Center"
Margin="0 0 2 0"
Width="20"
Height="20"
Width="18"
Height="18"
ClipToBounds="True">
<Image Stretch="UniformToFill" il:ImageLoader.Source="{CompiledBinding Entry.Id, Converter={StaticResource EntryIconUriConverter}, Mode=OneWay}" />
</Border>
<!-- Title -->
<TextBlock Margin="0 0 0 5" TextTrimming="CharacterEllipsis">
<Run Classes="h5" Text="{CompiledBinding Entry.Name, FallbackValue=Title}" />
<TextBlock Margin="2 0" TextTrimming="CharacterEllipsis">
<Run Text="{CompiledBinding Entry.Name, FallbackValue=Title}" />
<Run Classes="subtitle">by</Run>
<Run Classes="subtitle" Text="{CompiledBinding Entry.Author, FallbackValue=Author}" />
</TextBlock>
@ -44,46 +61,54 @@
IsVisible="{CompiledBinding Entry.IsOfficial}"
Kind="ShieldStar"
Foreground="{DynamicResource SystemAccentColorLight1}"
Margin="2 -2 0 0"
Margin="0 -2 0 0"
Width="18"
Height="18"
HorizontalAlignment="Left"
ToolTip.Tip="Official entry by the Artemis team" />
</StackPanel>
<StackPanel Grid.Column="1" Grid.Row="0">
<!-- Info -->
<StackPanel Margin="0 0 4 0" HorizontalAlignment="Right">
<TextBlock TextAlignment="Right">
<avalonia:MaterialIcon Kind="Harddisk" />
<Run Text="{CompiledBinding Release.Version}" />
</TextBlock>
</StackPanel>
<!-- Install state -->
<StackPanel Margin="0 0 4 0" HorizontalAlignment="Right" VerticalAlignment="Bottom">
<TextBlock TextAlignment="Right" IsVisible="{CompiledBinding NotYetInstalled}">
<avalonia:MaterialIcon Kind="Update" Foreground="{DynamicResource SystemAccentColorLight1}" Width="20" Height="20" />
<Run>not yet installed</Run>
</TextBlock>
</StackPanel>
<StackPanel Grid.Column="1" Grid.Row="0" Orientation="Horizontal" HorizontalAlignment="Right" Spacing="10">
<Border Classes="badge" VerticalAlignment="Top">
<TextBlock Text="{CompiledBinding Entry.EntryType}"></TextBlock>
</Border>
</StackPanel>
<Panel Grid.Column="0" Grid.Row="1" Grid.ColumnSpan="2">
<TextBlock Classes="subtitle" IsVisible="{CompiledBinding Release.Changelog, Converter={x:Static StringConverters.IsNullOrEmpty}}">
There are no release notes for this release.
</TextBlock>
<mdxaml:MarkdownScrollViewer Markdown="{CompiledBinding Release.Changelog}"
MarkdownStyleName="FluentAvalonia"
IsVisible="{CompiledBinding Release.Changelog, Converter={x:Static StringConverters.IsNotNullOrEmpty}}">
<mdxaml:MarkdownScrollViewer.Engine>
<mdxaml:Markdown HyperlinkCommand="{StaticResource ArtemisLinkCommand}" />
</mdxaml:MarkdownScrollViewer.Engine>
<mdxaml:MarkdownScrollViewer.Styles>
<StyleInclude Source="/Styles/Markdown.axaml" />
</mdxaml:MarkdownScrollViewer.Styles>
</mdxaml:MarkdownScrollViewer>
</Panel>
<ItemsControl Grid.Column="0" Grid.Row="1" Grid.ColumnSpan="2" ItemsSource="{CompiledBinding Releases}">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="workshop:IGetRecentUpdates_Entries_Releases_Release">
<StackPanel Orientation="Vertical">
<Border Classes="card-separator" />
<Grid ColumnDefinitions="*,Auto" HorizontalAlignment="Stretch">
<TextBlock Grid.Column="0"
Text="{CompiledBinding Version}"
Classes="version-clickable"
Cursor="Hand"
Background="Transparent"
PointerPressed="Release_OnPointerPressed" />
<TextBlock Grid.Column="1"
Text="{CompiledBinding CreatedAt, Converter={StaticResource DateTimeConverter}, ConverterParameter='humanize'}"
Classes="subtitle"
HorizontalAlignment="Right" />
</Grid>
<TextBlock Classes="subtitle" IsVisible="{CompiledBinding Changelog, Converter={x:Static StringConverters.IsNullOrEmpty}}">
There are no release notes for this release.
</TextBlock>
<mdxaml:MarkdownScrollViewer Markdown="{CompiledBinding Changelog}"
MarkdownStyleName="FluentAvalonia"
IsVisible="{CompiledBinding Changelog, Converter={x:Static StringConverters.IsNotNullOrEmpty}}">
<mdxaml:MarkdownScrollViewer.Engine>
<mdxaml:Markdown HyperlinkCommand="{StaticResource ArtemisLinkCommand}" />
</mdxaml:MarkdownScrollViewer.Engine>
<mdxaml:MarkdownScrollViewer.Styles>
<StyleInclude Source="/Styles/Markdown.axaml" />
</mdxaml:MarkdownScrollViewer.Styles>
</mdxaml:MarkdownScrollViewer>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</Border>
</UserControl>

View File

@ -1,4 +1,8 @@
using ReactiveUI.Avalonia;
using System.Threading.Tasks;
using Artemis.WebClient.Workshop;
using Avalonia.Controls;
using Avalonia.Input;
using ReactiveUI.Avalonia;
namespace Artemis.UI.Screens.Workshop.Library.Tabs;
@ -8,4 +12,15 @@ public partial class RecentlyUpdatedItemView : ReactiveUserControl<RecentlyUpdat
{
InitializeComponent();
}
private void Entry_OnPointerPressed(object? sender, PointerPressedEventArgs e)
{
ViewModel?.NavigateToEntry();
}
private void Release_OnPointerPressed(object? sender, PointerPressedEventArgs e)
{
object? dataContext = (sender as TextBlock)?.DataContext;
ViewModel?.NavigateToRelease(dataContext as IGetRecentUpdates_Entries_Releases);
}
}

View File

@ -1,23 +1,49 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Routing;
using Artemis.WebClient.Workshop;
using Artemis.WebClient.Workshop.Models;
using Artemis.WebClient.Workshop.Services;
using PropertyChanged.SourceGenerator;
namespace Artemis.UI.Screens.Workshop.Library.Tabs;
public partial class RecentlyUpdatedItemViewModel : ActivatableViewModelBase
{
public IGetRecentUpdates_Entries Entry { get; }
public IGetRecentUpdates_Entries_LatestRelease Release { get; }
public InstalledEntry InstalledEntry { get; }
private readonly IWorkshopService _workshopService;
private readonly IRouter _router;
[Notify] private bool _notYetInstalled;
public RecentlyUpdatedItemViewModel(IGetRecentUpdates_Entries entry, IWorkshopService workshopService)
public RecentlyUpdatedItemViewModel(IGetRecentUpdates_Entries entry, IWorkshopService workshopService, IRouter router)
{
_workshopService = workshopService;
_router = router;
Releases = entry.Releases;
Entry = entry;
Release = entry.LatestRelease ?? throw new InvalidOperationException("Entry does not have a latest release");
InstalledEntry = workshopService.GetInstalledEntry(entry.Id) ?? throw new InvalidOperationException("Entry is not installed");
LatestRelease = Releases.First(r => r.Id == entry.LatestReleaseId);
NotYetInstalled = InstalledEntry.ReleaseId != Releases.Max(r => r.Id);
}
public bool NotYetInstalled => InstalledEntry.ReleaseId < Release.Id;
public InstalledEntry InstalledEntry { get; }
public IGetRecentUpdates_Entries Entry { get; }
public IReadOnlyList<IGetRecentUpdates_Entries_Releases> Releases { get; set; }
public IGetRecentUpdates_Entries_Releases LatestRelease { get; }
public async Task NavigateToEntry()
{
await _workshopService.NavigateToEntry(Entry.Id, Entry.EntryType);
}
public async Task NavigateToRelease(IGetRecentUpdates_Entries_Releases? release)
{
if (release == null)
return;
await _router.Navigate($"workshop/entries/{Entry.EntryType.ToString()}s/details/{Entry.Id}/releases/{release.Id}");
}
}

View File

@ -15,8 +15,8 @@
</Styles>
</UserControl.Styles>
<Grid RowDefinitions="Auto,*">
<Grid Grid.Row="0" Grid.Column="0" MaxWidth="1000" Margin="0 22 0 10">
<Grid RowDefinitions="Auto,*" MaxWidth="1000">
<Grid Grid.Row="0" Grid.Column="0" Margin="0 22 0 10">
<Grid.ColumnDefinitions>
<ColumnDefinition MinWidth="165" MaxWidth="400" />
<ColumnDefinition Width="*" />
@ -34,7 +34,7 @@
</StackPanel>
<ScrollViewer Grid.Row="1" Grid.Column="0" HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto" VerticalAlignment="Top">
<ItemsControl ItemsSource="{CompiledBinding Entries}" Margin="0 0 20 0" MaxWidth="1000">
<ItemsControl ItemsSource="{CompiledBinding Entries}" Margin="0 0 20 0">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel />

View File

@ -51,7 +51,7 @@ public partial class RecentlyUpdatedViewModel : RoutableScreen
.Transform(getRecentlyUpdatedItemViewModel)
.SortAndBind(
out ReadOnlyObservableCollection<RecentlyUpdatedItemViewModel> entries,
SortExpressionComparer<RecentlyUpdatedItemViewModel>.Descending(p => p.Release.CreatedAt)
SortExpressionComparer<RecentlyUpdatedItemViewModel>.Descending(p => p.LatestRelease.CreatedAt)
)
.Subscribe();

View File

@ -45,27 +45,3 @@ query GetDefaultPlugins {
}
}
}
query GetRecentUpdates($entryIds: [Long!]!, $cutoff: DateTime!) {
entries(
includeDefaults: true
where: {
and: [
{ id: { in: $entryIds } }
{ latestRelease: { createdAt: { gte: $cutoff } } }
]
}
) {
id
author
isOfficial
name
summary
entryType
createdAt
latestRelease {
...release
changelog
}
}
}

View File

@ -0,0 +1,27 @@
query GetRecentUpdates($entryIds: [Long!]!, $cutoff: DateTime!) {
entries(
includeDefaults: true
order: [{ latestRelease: { createdAt: DESC } }]
where: {
and: [
{ id: { in: $entryIds } }
{ releases: { some: { createdAt: { gte: $cutoff } } } }
]
}
) {
id
author
isOfficial
name
summary
entryType
latestReleaseId
releases(
order: [{ createdAt: DESC }]
where: { createdAt: { gte: $cutoff } }
) {
...release
changelog
}
}
}

View File

@ -23,6 +23,7 @@ internal class AuthenticationService : CorePropertyChanged, IAuthenticationServi
private readonly IAuthenticationRepository _authenticationRepository;
private readonly SemaphoreSlim _authLock = new(1, 1);
private readonly SourceList<Claim> _claims;
private readonly SourceList<string> _roles;
private readonly IDiscoveryCache _discoveryCache;
private readonly ILogger _logger;
@ -41,7 +42,10 @@ internal class AuthenticationService : CorePropertyChanged, IAuthenticationServi
_claims = new SourceList<Claim>();
_claims.Connect().Bind(out ReadOnlyObservableCollection<Claim> claims).Subscribe();
_roles = new SourceList<string>();
_roles.Connect().Bind(out ReadOnlyObservableCollection<string> roles).Subscribe();
Claims = claims;
Roles = roles;
}
private async Task<DiscoveryDocumentResponse> GetDiscovery()
@ -68,6 +72,11 @@ internal class AuthenticationService : CorePropertyChanged, IAuthenticationServi
c.Clear();
c.AddRange(token.Claims);
});
_roles.Edit(r =>
{
r.Clear();
r.AddRange(_claims.Items.Where(c => c.Type == JwtClaimTypes.Role).Select(c => c.Value));
});
_isLoggedInSubject.OnNext(true);
}
@ -119,6 +128,9 @@ internal class AuthenticationService : CorePropertyChanged, IAuthenticationServi
/// <inheritdoc />
public ReadOnlyObservableCollection<Claim> Claims { get; }
/// <inheritdoc />
public ReadOnlyObservableCollection<string> Roles { get; }
/// <inheritdoc />
public IObservable<Claim?> GetClaim(string type)
{
@ -278,6 +290,7 @@ internal class AuthenticationService : CorePropertyChanged, IAuthenticationServi
_token = null;
_claims.Clear();
_roles.Clear();
SetStoredRefreshToken(null);
_isLoggedInSubject.OnNext(false);
}
@ -289,12 +302,6 @@ 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

@ -8,6 +8,7 @@ public interface IAuthenticationService : IProtectedArtemisService
{
IObservable<bool> IsLoggedIn { get; }
ReadOnlyObservableCollection<Claim> Claims { get; }
ReadOnlyObservableCollection<string> Roles { get; }
IObservable<Claim?> GetClaim(string type);
Task<string?> GetBearer();
@ -15,5 +16,4 @@ public interface IAuthenticationService : IProtectedArtemisService
Task Login(CancellationToken cancellationToken);
Task Logout();
bool GetIsEmailVerified();
List<string> GetRoles();
}

View File

@ -65,7 +65,10 @@ type Entry {
categories: [Category!]!
tags: [Tag!]!
images: [Image!]!
releases: [Release!]!
releases(
order: [ReleaseSortInput!] @cost(weight: "10")
where: ReleaseFilterInput @cost(weight: "10")
): [Release!]!
dependantReleases: [Release!]!
}