mirror of
https://github.com/Artemis-RGB/Artemis
synced 2025-12-13 05:48:35 +00:00
Entry list - Show icons
Workshop search - Show icons, update design
This commit is contained in:
parent
176a28761f
commit
77bed1bf94
@ -32,6 +32,7 @@
|
|||||||
<!-- Custom styles -->
|
<!-- Custom styles -->
|
||||||
<StyleInclude Source="/Styles/Border.axaml" />
|
<StyleInclude Source="/Styles/Border.axaml" />
|
||||||
<StyleInclude Source="/Styles/BrokenState.axaml" />
|
<StyleInclude Source="/Styles/BrokenState.axaml" />
|
||||||
|
<StyleInclude Source="/Styles/Control.axaml" />
|
||||||
<StyleInclude Source="/Styles/Skeleton.axaml" />
|
<StyleInclude Source="/Styles/Skeleton.axaml" />
|
||||||
<StyleInclude Source="/Styles/Button.axaml" />
|
<StyleInclude Source="/Styles/Button.axaml" />
|
||||||
<StyleInclude Source="/Styles/Condensed.axaml" />
|
<StyleInclude Source="/Styles/Condensed.axaml" />
|
||||||
|
|||||||
25
src/Artemis.UI.Shared/Styles/Control.axaml
Normal file
25
src/Artemis.UI.Shared/Styles/Control.axaml
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<Styles xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||||
|
<Design.PreviewWith>
|
||||||
|
<Border Padding="20">
|
||||||
|
<!-- Add Controls for Previewer Here -->
|
||||||
|
</Border>
|
||||||
|
</Design.PreviewWith>
|
||||||
|
|
||||||
|
<!-- Add Styles Here -->
|
||||||
|
<Style Selector=":is(Control).fade-in">
|
||||||
|
<Setter Property="Opacity" Value="0" />
|
||||||
|
|
||||||
|
<Setter Property="Transitions">
|
||||||
|
<Setter.Value>
|
||||||
|
<Transitions>
|
||||||
|
<DoubleTransition Property="Opacity" Delay="0:0:0.2" Duration="0:0:0.2" Easing="CubicEaseOut"/>
|
||||||
|
</Transitions>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style Selector=":is(Control).faded-in">
|
||||||
|
<Setter Property="Opacity" Value="1" />
|
||||||
|
</Style>
|
||||||
|
</Styles>
|
||||||
@ -14,7 +14,8 @@
|
|||||||
CornerRadius="8"
|
CornerRadius="8"
|
||||||
HorizontalAlignment="Stretch"
|
HorizontalAlignment="Stretch"
|
||||||
HorizontalContentAlignment="Stretch"
|
HorizontalContentAlignment="Stretch"
|
||||||
Command="{CompiledBinding NavigateToEntry}">
|
Command="{CompiledBinding NavigateToEntry}"
|
||||||
|
IsVisible="{CompiledBinding Entry, Converter={x:Static ObjectConverters.IsNotNull}}">
|
||||||
<Grid ColumnDefinitions="Auto,*,Auto">
|
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||||
<!-- Icon -->
|
<!-- Icon -->
|
||||||
<Border Grid.Column="0"
|
<Border Grid.Column="0"
|
||||||
@ -25,7 +26,10 @@
|
|||||||
Width="80"
|
Width="80"
|
||||||
Height="80"
|
Height="80"
|
||||||
ClipToBounds="True">
|
ClipToBounds="True">
|
||||||
<Image Source="{CompiledBinding EntryIcon^}" Stretch="UniformToFill"/>
|
<Image Source="{CompiledBinding EntryIcon}"
|
||||||
|
Stretch="UniformToFill"
|
||||||
|
Classes="fade-in"
|
||||||
|
Classes.faded-in="{CompiledBinding EntryIcon, Converter={x:Static ObjectConverters.IsNotNull}}" />
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<!-- Body -->
|
<!-- Body -->
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Reactive;
|
using System.Reactive;
|
||||||
|
using System.Reactive.Disposables;
|
||||||
using System.Reactive.Linq;
|
using System.Reactive.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@ -15,19 +16,26 @@ namespace Artemis.UI.Screens.Workshop.Entries;
|
|||||||
public class EntryListViewModel : ActivatableViewModelBase
|
public class EntryListViewModel : ActivatableViewModelBase
|
||||||
{
|
{
|
||||||
private readonly IRouter _router;
|
private readonly IRouter _router;
|
||||||
private readonly ObservableAsPropertyHelper<Bitmap?> _entryIcon;
|
private readonly IWorkshopService _workshopService;
|
||||||
|
private ObservableAsPropertyHelper<Bitmap?>? _entryIcon;
|
||||||
|
|
||||||
public EntryListViewModel(IGetEntries_Entries_Items entry, IRouter router, IWorkshopService workshopService)
|
public EntryListViewModel(IGetEntries_Entries_Items entry, IRouter router, IWorkshopService workshopService)
|
||||||
{
|
{
|
||||||
_router = router;
|
_router = router;
|
||||||
|
_workshopService = workshopService;
|
||||||
|
|
||||||
Entry = entry;
|
Entry = entry;
|
||||||
EntryIcon = workshopService.GetEntryIcon(entry.Id, CancellationToken.None);
|
|
||||||
NavigateToEntry = ReactiveCommand.CreateFromTask(ExecuteNavigateToEntry);
|
NavigateToEntry = ReactiveCommand.CreateFromTask(ExecuteNavigateToEntry);
|
||||||
|
|
||||||
|
this.WhenActivated(d =>
|
||||||
|
{
|
||||||
|
_entryIcon = Observable.FromAsync(GetIcon).ToProperty(this, vm => vm.EntryIcon);
|
||||||
|
_entryIcon.DisposeWith(d);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public IGetEntries_Entries_Items Entry { get; }
|
public IGetEntries_Entries_Items Entry { get; }
|
||||||
public Task<Bitmap?> EntryIcon { get; }
|
public Bitmap? EntryIcon => _entryIcon?.Value;
|
||||||
public ReactiveCommand<Unit, Unit> NavigateToEntry { get; }
|
public ReactiveCommand<Unit, Unit> NavigateToEntry { get; }
|
||||||
|
|
||||||
private async Task ExecuteNavigateToEntry()
|
private async Task ExecuteNavigateToEntry()
|
||||||
@ -46,4 +54,12 @@ public class EntryListViewModel : ActivatableViewModelBase
|
|||||||
throw new ArgumentOutOfRangeException();
|
throw new ArgumentOutOfRangeException();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<Bitmap?> GetIcon(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// Take at least 100ms to allow the UI to load and make the whole thing smooth
|
||||||
|
Task<Bitmap?> iconTask = _workshopService.GetEntryIcon(Entry.Id, cancellationToken);
|
||||||
|
await Task.Delay(100, cancellationToken);
|
||||||
|
return await iconTask;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reactive.Disposables;
|
using System.Reactive.Disposables;
|
||||||
using System.Reactive.Linq;
|
using System.Reactive.Linq;
|
||||||
@ -13,7 +14,9 @@ using Artemis.UI.Shared.Routing;
|
|||||||
using Artemis.UI.Shared.Services;
|
using Artemis.UI.Shared.Services;
|
||||||
using Artemis.UI.Shared.Services.Builders;
|
using Artemis.UI.Shared.Services.Builders;
|
||||||
using Artemis.WebClient.Workshop;
|
using Artemis.WebClient.Workshop;
|
||||||
|
using Avalonia.Threading;
|
||||||
using DryIoc.ImTools;
|
using DryIoc.ImTools;
|
||||||
|
using DynamicData;
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
using StrawberryShake;
|
using StrawberryShake;
|
||||||
|
|
||||||
@ -26,7 +29,7 @@ public class ProfileListViewModel : RoutableScreen<ActivatableViewModelBase, Wor
|
|||||||
private readonly IWorkshopClient _workshopClient;
|
private readonly IWorkshopClient _workshopClient;
|
||||||
private readonly ObservableAsPropertyHelper<bool> _showPagination;
|
private readonly ObservableAsPropertyHelper<bool> _showPagination;
|
||||||
private readonly ObservableAsPropertyHelper<bool> _isLoading;
|
private readonly ObservableAsPropertyHelper<bool> _isLoading;
|
||||||
private List<EntryListViewModel>? _entries;
|
private SourceList<IGetEntries_Entries_Items> _entries = new();
|
||||||
private int _page;
|
private int _page;
|
||||||
private int _loadedPage = -1;
|
private int _loadedPage = -1;
|
||||||
private int _totalPages = 1;
|
private int _totalPages = 1;
|
||||||
@ -46,8 +49,16 @@ public class ProfileListViewModel : RoutableScreen<ActivatableViewModelBase, Wor
|
|||||||
|
|
||||||
CategoriesViewModel = categoriesViewModel;
|
CategoriesViewModel = categoriesViewModel;
|
||||||
|
|
||||||
|
_entries.Connect()
|
||||||
|
.ObserveOn(new AvaloniaSynchronizationContext(DispatcherPriority.SystemIdle))
|
||||||
|
.Transform(getEntryListViewModel)
|
||||||
|
.Bind(out ReadOnlyObservableCollection<EntryListViewModel> entries)
|
||||||
|
.Subscribe();
|
||||||
|
Entries = entries;
|
||||||
|
|
||||||
// Respond to page changes
|
// Respond to page changes
|
||||||
this.WhenAnyValue(vm => vm.Page).Skip(1).Subscribe(p => Task.Run(() => router.Navigate($"workshop/profiles/{p}")));
|
this.WhenAnyValue(vm => vm.Page).Skip(1).Subscribe(p => Task.Run(() => router.Navigate($"workshop/profiles/{p}")));
|
||||||
|
|
||||||
// Respond to filter changes
|
// Respond to filter changes
|
||||||
this.WhenActivated(d => CategoriesViewModel.WhenAnyValue(vm => vm.CategoryFilters).Skip(1).Subscribe(_ =>
|
this.WhenActivated(d => CategoriesViewModel.WhenAnyValue(vm => vm.CategoryFilters).Skip(1).Subscribe(_ =>
|
||||||
{
|
{
|
||||||
@ -65,11 +76,7 @@ public class ProfileListViewModel : RoutableScreen<ActivatableViewModelBase, Wor
|
|||||||
|
|
||||||
public CategoriesViewModel CategoriesViewModel { get; }
|
public CategoriesViewModel CategoriesViewModel { get; }
|
||||||
|
|
||||||
public List<EntryListViewModel>? Entries
|
public ReadOnlyObservableCollection<EntryListViewModel> Entries { get; }
|
||||||
{
|
|
||||||
get => _entries;
|
|
||||||
set => RaiseAndSetIfChanged(ref _entries, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public int Page
|
public int Page
|
||||||
{
|
{
|
||||||
@ -99,8 +106,11 @@ public class ProfileListViewModel : RoutableScreen<ActivatableViewModelBase, Wor
|
|||||||
{
|
{
|
||||||
Page = Math.Max(1, parameters.Page);
|
Page = Math.Max(1, parameters.Page);
|
||||||
|
|
||||||
// Throttle page changes
|
// Throttle page changes, wait longer for the first one to keep UI smooth
|
||||||
await Task.Delay(200, cancellationToken);
|
// if (Entries == null)
|
||||||
|
// await Task.Delay(400, cancellationToken);
|
||||||
|
// else
|
||||||
|
await Task.Delay(200, cancellationToken);
|
||||||
|
|
||||||
if (!cancellationToken.IsCancellationRequested)
|
if (!cancellationToken.IsCancellationRequested)
|
||||||
await Query(cancellationToken);
|
await Query(cancellationToken);
|
||||||
@ -116,8 +126,12 @@ public class ProfileListViewModel : RoutableScreen<ActivatableViewModelBase, Wor
|
|||||||
|
|
||||||
if (entries.Data?.Entries?.Items != null)
|
if (entries.Data?.Entries?.Items != null)
|
||||||
{
|
{
|
||||||
Entries = entries.Data.Entries.Items.Select(n => _getEntryListViewModel(n)).ToList();
|
|
||||||
TotalPages = (int) Math.Ceiling(entries.Data.Entries.TotalCount / (double) EntriesPerPage);
|
TotalPages = (int) Math.Ceiling(entries.Data.Entries.TotalCount / (double) EntriesPerPage);
|
||||||
|
_entries.Edit(e =>
|
||||||
|
{
|
||||||
|
e.Clear();
|
||||||
|
e.AddRange(entries.Data.Entries.Items);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
TotalPages = 1;
|
TotalPages = 1;
|
||||||
|
|||||||
@ -0,0 +1,60 @@
|
|||||||
|
<UserControl xmlns="https://github.com/avaloniaui"
|
||||||
|
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:search="clr-namespace:Artemis.UI.Screens.Workshop.Search"
|
||||||
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="80"
|
||||||
|
x:Class="Artemis.UI.Screens.Workshop.Search.SearchResultView"
|
||||||
|
x:DataType="search:SearchResultViewModel">
|
||||||
|
<Grid ColumnDefinitions="Auto,*,Auto" Margin="0 5">
|
||||||
|
<!-- Icon -->
|
||||||
|
<Border Grid.Column="0"
|
||||||
|
CornerRadius="6"
|
||||||
|
Background="{StaticResource ControlStrokeColorOnAccentDefault}"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Margin="0 0 10 0"
|
||||||
|
Width="50"
|
||||||
|
Height="50"
|
||||||
|
ClipToBounds="True">
|
||||||
|
<Image Source="{CompiledBinding EntryIcon}"
|
||||||
|
Stretch="UniformToFill"
|
||||||
|
Classes="fade-in"
|
||||||
|
Classes.faded-in="{CompiledBinding EntryIcon, Converter={x:Static ObjectConverters.IsNotNull}}" />
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- Body -->
|
||||||
|
<Grid Grid.Column="1" VerticalAlignment="Stretch" RowDefinitions="Auto,*,Auto">
|
||||||
|
<TextBlock Grid.Row="0" TextTrimming="CharacterEllipsis">
|
||||||
|
<Run Text="{CompiledBinding Entry.Name, FallbackValue=Title}" />
|
||||||
|
<Run Classes="subtitle" FontSize="12">by</Run>
|
||||||
|
<Run Classes="subtitle" FontSize="12" Text="{CompiledBinding Entry.Author, FallbackValue=Author}" />
|
||||||
|
</TextBlock>
|
||||||
|
<TextBlock Grid.Row="1"
|
||||||
|
Classes="subtitle"
|
||||||
|
TextWrapping="Wrap"
|
||||||
|
TextTrimming="CharacterEllipsis"
|
||||||
|
FontSize="13"
|
||||||
|
Text="{CompiledBinding Entry.Summary, FallbackValue=Summary}">
|
||||||
|
</TextBlock>
|
||||||
|
|
||||||
|
<ItemsControl Grid.Row="2" ItemsSource="{CompiledBinding Entry.Categories}" Margin="0 5">
|
||||||
|
<ItemsControl.ItemsPanel>
|
||||||
|
<ItemsPanelTemplate>
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="5"></StackPanel>
|
||||||
|
</ItemsPanelTemplate>
|
||||||
|
</ItemsControl.ItemsPanel>
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<Border Classes="badge">
|
||||||
|
<TextBlock Text="{CompiledBinding Name}"></TextBlock>
|
||||||
|
</Border>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Border Grid.Column="2" Classes="badge" VerticalAlignment="Top" Margin="0 5 0 0">
|
||||||
|
<TextBlock Text="{CompiledBinding Entry.EntryType}"></TextBlock>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
</UserControl>
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Markup.Xaml;
|
||||||
|
using Avalonia.ReactiveUI;
|
||||||
|
|
||||||
|
namespace Artemis.UI.Screens.Workshop.Search;
|
||||||
|
|
||||||
|
public partial class SearchResultView : ReactiveUserControl<SearchResultViewModel>
|
||||||
|
{
|
||||||
|
public SearchResultView()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,32 @@
|
|||||||
|
using System.Reactive.Disposables;
|
||||||
|
using System.Reactive.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Artemis.UI.Shared;
|
||||||
|
using Artemis.WebClient.Workshop;
|
||||||
|
using Artemis.WebClient.Workshop.Services;
|
||||||
|
using Avalonia.Media.Imaging;
|
||||||
|
using ReactiveUI;
|
||||||
|
|
||||||
|
namespace Artemis.UI.Screens.Workshop.Search;
|
||||||
|
|
||||||
|
public class SearchResultViewModel : ActivatableViewModelBase
|
||||||
|
{
|
||||||
|
private readonly IWorkshopService _workshopService;
|
||||||
|
private ObservableAsPropertyHelper<Bitmap?>? _entryIcon;
|
||||||
|
|
||||||
|
public SearchResultViewModel(ISearchEntries_SearchEntries entry, IWorkshopService workshopService)
|
||||||
|
{
|
||||||
|
_workshopService = workshopService;
|
||||||
|
|
||||||
|
Entry = entry;
|
||||||
|
this.WhenActivated(d =>
|
||||||
|
{
|
||||||
|
_entryIcon = Observable.FromAsync(c => _workshopService.GetEntryIcon(Entry.Id, c)).ToProperty(this, vm => vm.EntryIcon);
|
||||||
|
_entryIcon.DisposeWith(d);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public ISearchEntries_SearchEntries Entry { get; }
|
||||||
|
public Bitmap? EntryIcon => _entryIcon?.Value;
|
||||||
|
}
|
||||||
@ -11,39 +11,19 @@
|
|||||||
<UserControl.Styles>
|
<UserControl.Styles>
|
||||||
<StyleInclude Source="SearchViewStyles.axaml" />
|
<StyleInclude Source="SearchViewStyles.axaml" />
|
||||||
</UserControl.Styles>
|
</UserControl.Styles>
|
||||||
<Panel>
|
<Panel Margin="0 5">
|
||||||
<AutoCompleteBox Name="SearchBox"
|
<AutoCompleteBox Name="SearchBox"
|
||||||
MaxWidth="500"
|
MaxWidth="500"
|
||||||
Watermark="Search"
|
Watermark="Search"
|
||||||
Margin="0 5"
|
MinimumPopulateDelay="0:0:0.8"
|
||||||
ValueMemberBinding="{CompiledBinding Name, DataType=workshop:ISearchEntries_SearchEntries}"
|
ValueMemberBinding="{CompiledBinding Entry.Name, DataType=search:SearchResultViewModel}"
|
||||||
AsyncPopulator="{CompiledBinding SearchAsync}"
|
AsyncPopulator="{CompiledBinding SearchAsync}"
|
||||||
SelectedItem="{CompiledBinding SelectedEntry}"
|
SelectedItem="{CompiledBinding SelectedEntry}"
|
||||||
FilterMode="None"
|
FilterMode="None"
|
||||||
windowing:AppWindow.AllowInteractionInTitleBar="True">
|
windowing:AppWindow.AllowInteractionInTitleBar="True">
|
||||||
<AutoCompleteBox.ItemTemplate>
|
<AutoCompleteBox.ItemTemplate>
|
||||||
<DataTemplate x:DataType="workshop:ISearchEntries_SearchEntries">
|
<DataTemplate>
|
||||||
<Panel>
|
<ContentControl Content="{Binding}" Margin="-5 0 0 0"></ContentControl>
|
||||||
<StackPanel HorizontalAlignment="Left" VerticalAlignment="Center">
|
|
||||||
<TextBlock Text="{CompiledBinding Name}" />
|
|
||||||
<TextBlock Text="{CompiledBinding Summary}" Foreground="{DynamicResource TextFillColorSecondary}" />
|
|
||||||
|
|
||||||
<ItemsControl ItemsSource="{CompiledBinding Categories}">
|
|
||||||
<ItemsControl.ItemsPanel>
|
|
||||||
<ItemsPanelTemplate>
|
|
||||||
<StackPanel Orientation="Horizontal" Spacing="5"></StackPanel>
|
|
||||||
</ItemsPanelTemplate>
|
|
||||||
</ItemsControl.ItemsPanel>
|
|
||||||
<ItemsControl.ItemTemplate>
|
|
||||||
<DataTemplate>
|
|
||||||
<Border Classes="category">
|
|
||||||
<TextBlock Text="{CompiledBinding Name}"></TextBlock>
|
|
||||||
</Border>
|
|
||||||
</DataTemplate>
|
|
||||||
</ItemsControl.ItemTemplate>
|
|
||||||
</ItemsControl>
|
|
||||||
</StackPanel>
|
|
||||||
</Panel>
|
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</AutoCompleteBox.ItemTemplate>
|
</AutoCompleteBox.ItemTemplate>
|
||||||
</AutoCompleteBox>
|
</AutoCompleteBox>
|
||||||
@ -52,6 +32,10 @@
|
|||||||
Height="28"
|
Height="28"
|
||||||
Margin="0 0 50 0"
|
Margin="0 0 50 0"
|
||||||
Content="{CompiledBinding CurrentUserViewModel}"
|
Content="{CompiledBinding CurrentUserViewModel}"
|
||||||
windowing:AppWindow.AllowInteractionInTitleBar="True"/>
|
windowing:AppWindow.AllowInteractionInTitleBar="True" />
|
||||||
|
|
||||||
|
<Border VerticalAlignment="Top" CornerRadius="4 4 0 0" ClipToBounds="True" MaxWidth="500">
|
||||||
|
<ProgressBar IsIndeterminate="True" VerticalAlignment="Top" IsVisible="{CompiledBinding IsLoading}"></ProgressBar>
|
||||||
|
</Border>
|
||||||
</Panel>
|
</Panel>
|
||||||
</UserControl>
|
</UserControl>
|
||||||
@ -7,32 +7,40 @@ using Artemis.UI.Screens.Workshop.CurrentUser;
|
|||||||
using Artemis.UI.Shared;
|
using Artemis.UI.Shared;
|
||||||
using Artemis.UI.Shared.Routing;
|
using Artemis.UI.Shared.Routing;
|
||||||
using Artemis.WebClient.Workshop;
|
using Artemis.WebClient.Workshop;
|
||||||
|
using Artemis.WebClient.Workshop.Services;
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
|
using Serilog;
|
||||||
using StrawberryShake;
|
using StrawberryShake;
|
||||||
|
|
||||||
namespace Artemis.UI.Screens.Workshop.Search;
|
namespace Artemis.UI.Screens.Workshop.Search;
|
||||||
|
|
||||||
public class SearchViewModel : ViewModelBase
|
public class SearchViewModel : ViewModelBase
|
||||||
{
|
{
|
||||||
public CurrentUserViewModel CurrentUserViewModel { get; }
|
private readonly ILogger _logger;
|
||||||
private readonly IRouter _router;
|
private readonly IRouter _router;
|
||||||
private readonly IWorkshopClient _workshopClient;
|
private readonly IWorkshopClient _workshopClient;
|
||||||
|
private readonly IWorkshopService _workshopService;
|
||||||
private EntryType? _entryType;
|
private EntryType? _entryType;
|
||||||
private ISearchEntries_SearchEntries? _selectedEntry;
|
private bool _isLoading;
|
||||||
|
private SearchResultViewModel? _selectedEntry;
|
||||||
|
|
||||||
public SearchViewModel(IWorkshopClient workshopClient, IRouter router, CurrentUserViewModel currentUserViewModel)
|
public SearchViewModel(ILogger logger, IWorkshopClient workshopClient, IWorkshopService workshopService, IRouter router, CurrentUserViewModel currentUserViewModel)
|
||||||
{
|
{
|
||||||
CurrentUserViewModel = currentUserViewModel;
|
_logger = logger;
|
||||||
_workshopClient = workshopClient;
|
_workshopClient = workshopClient;
|
||||||
|
_workshopService = workshopService;
|
||||||
_router = router;
|
_router = router;
|
||||||
|
CurrentUserViewModel = currentUserViewModel;
|
||||||
SearchAsync = ExecuteSearchAsync;
|
SearchAsync = ExecuteSearchAsync;
|
||||||
|
|
||||||
this.WhenAnyValue(vm => vm.SelectedEntry).WhereNotNull().Subscribe(NavigateToEntry);
|
this.WhenAnyValue(vm => vm.SelectedEntry).WhereNotNull().Subscribe(NavigateToEntry);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public CurrentUserViewModel CurrentUserViewModel { get; }
|
||||||
|
|
||||||
public Func<string?, CancellationToken, Task<IEnumerable<object>>> SearchAsync { get; }
|
public Func<string?, CancellationToken, Task<IEnumerable<object>>> SearchAsync { get; }
|
||||||
|
|
||||||
public ISearchEntries_SearchEntries? SelectedEntry
|
public SearchResultViewModel? SelectedEntry
|
||||||
{
|
{
|
||||||
get => _selectedEntry;
|
get => _selectedEntry;
|
||||||
set => RaiseAndSetIfChanged(ref _selectedEntry, value);
|
set => RaiseAndSetIfChanged(ref _selectedEntry, value);
|
||||||
@ -44,13 +52,19 @@ public class SearchViewModel : ViewModelBase
|
|||||||
set => RaiseAndSetIfChanged(ref _entryType, value);
|
set => RaiseAndSetIfChanged(ref _entryType, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void NavigateToEntry(ISearchEntries_SearchEntries entry)
|
public bool IsLoading
|
||||||
|
{
|
||||||
|
get => _isLoading;
|
||||||
|
set => RaiseAndSetIfChanged(ref _isLoading, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void NavigateToEntry(SearchResultViewModel searchResult)
|
||||||
{
|
{
|
||||||
string? url = null;
|
string? url = null;
|
||||||
if (entry.EntryType == WebClient.Workshop.EntryType.Profile)
|
if (searchResult.Entry.EntryType == WebClient.Workshop.EntryType.Profile)
|
||||||
url = $"workshop/profiles/{entry.Id}";
|
url = $"workshop/profiles/{searchResult.Entry.Id}";
|
||||||
if (entry.EntryType == WebClient.Workshop.EntryType.Layout)
|
if (searchResult.Entry.EntryType == WebClient.Workshop.EntryType.Layout)
|
||||||
url = $"workshop/layouts/{entry.Id}";
|
url = $"workshop/layouts/{searchResult.Entry.Id}";
|
||||||
|
|
||||||
if (url != null)
|
if (url != null)
|
||||||
Task.Run(() => _router.Navigate(url));
|
Task.Run(() => _router.Navigate(url));
|
||||||
@ -58,10 +72,25 @@ public class SearchViewModel : ViewModelBase
|
|||||||
|
|
||||||
private async Task<IEnumerable<object>> ExecuteSearchAsync(string? input, CancellationToken cancellationToken)
|
private async Task<IEnumerable<object>> ExecuteSearchAsync(string? input, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(input))
|
try
|
||||||
return new List<object>();
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(input) || input.Length < 2)
|
||||||
|
return new List<object>();
|
||||||
|
|
||||||
IOperationResult<ISearchEntriesResult> results = await _workshopClient.SearchEntries.ExecuteAsync(input, EntryType, cancellationToken);
|
IsLoading = true;
|
||||||
return results.Data?.SearchEntries.Cast<object>() ?? new List<object>();
|
IOperationResult<ISearchEntriesResult> results = await _workshopClient.SearchEntries.ExecuteAsync(input, EntryType, cancellationToken);
|
||||||
|
return results.Data?.SearchEntries.Select(e => new SearchResultViewModel(e, _workshopService) as object) ?? new List<object>();
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
if (e is not TaskCanceledException)
|
||||||
|
_logger.Error(e, "Failed to execute search");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
IsLoading = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new List<object>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -19,7 +19,7 @@
|
|||||||
</Design.PreviewWith>
|
</Design.PreviewWith>
|
||||||
|
|
||||||
<!-- Add Styles Here -->
|
<!-- Add Styles Here -->
|
||||||
<Style Selector="Border.category">
|
<Style Selector="Border.badge">
|
||||||
<Setter Property="Background" Value="{DynamicResource ControlSolidFillColorDefaultBrush}" />
|
<Setter Property="Background" Value="{DynamicResource ControlSolidFillColorDefaultBrush}" />
|
||||||
<Setter Property="CornerRadius" Value="12" />
|
<Setter Property="CornerRadius" Value="12" />
|
||||||
<Setter Property="Padding" Value="6 1"></Setter>
|
<Setter Property="Padding" Value="6 1"></Setter>
|
||||||
|
|||||||
@ -4,6 +4,7 @@ query SearchEntries($input: String! $type: EntryType) {
|
|||||||
name
|
name
|
||||||
summary
|
summary
|
||||||
entryType
|
entryType
|
||||||
|
author
|
||||||
categories {
|
categories {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
|
|||||||
@ -11,6 +11,7 @@ using Artemis.WebClient.Workshop.Repositories;
|
|||||||
using DynamicData;
|
using DynamicData;
|
||||||
using IdentityModel;
|
using IdentityModel;
|
||||||
using IdentityModel.Client;
|
using IdentityModel.Client;
|
||||||
|
using Serilog;
|
||||||
|
|
||||||
namespace Artemis.WebClient.Workshop.Services;
|
namespace Artemis.WebClient.Workshop.Services;
|
||||||
|
|
||||||
@ -22,14 +23,16 @@ internal class AuthenticationService : CorePropertyChanged, IAuthenticationServi
|
|||||||
private readonly SourceList<Claim> _claims;
|
private readonly SourceList<Claim> _claims;
|
||||||
|
|
||||||
private readonly IDiscoveryCache _discoveryCache;
|
private readonly IDiscoveryCache _discoveryCache;
|
||||||
|
private readonly ILogger _logger;
|
||||||
private readonly IHttpClientFactory _httpClientFactory;
|
private readonly IHttpClientFactory _httpClientFactory;
|
||||||
private readonly BehaviorSubject<bool> _isLoggedInSubject = new(false);
|
private readonly BehaviorSubject<bool> _isLoggedInSubject = new(false);
|
||||||
|
|
||||||
private AuthenticationToken? _token;
|
private AuthenticationToken? _token;
|
||||||
private bool _noStoredRefreshToken;
|
private bool _noStoredRefreshToken;
|
||||||
|
|
||||||
public AuthenticationService(IHttpClientFactory httpClientFactory, IDiscoveryCache discoveryCache, IAuthenticationRepository authenticationRepository)
|
public AuthenticationService(ILogger logger, IHttpClientFactory httpClientFactory, IDiscoveryCache discoveryCache, IAuthenticationRepository authenticationRepository)
|
||||||
{
|
{
|
||||||
|
_logger = logger;
|
||||||
_httpClientFactory = httpClientFactory;
|
_httpClientFactory = httpClientFactory;
|
||||||
_discoveryCache = discoveryCache;
|
_discoveryCache = discoveryCache;
|
||||||
_authenticationRepository = authenticationRepository;
|
_authenticationRepository = authenticationRepository;
|
||||||
@ -69,28 +72,37 @@ internal class AuthenticationService : CorePropertyChanged, IAuthenticationServi
|
|||||||
|
|
||||||
private async Task<bool> UseRefreshToken(string refreshToken)
|
private async Task<bool> UseRefreshToken(string refreshToken)
|
||||||
{
|
{
|
||||||
DiscoveryDocumentResponse disco = await GetDiscovery();
|
try
|
||||||
HttpClient client = _httpClientFactory.CreateClient();
|
|
||||||
TokenResponse response = await client.RequestRefreshTokenAsync(new RefreshTokenRequest
|
|
||||||
{
|
{
|
||||||
Address = disco.TokenEndpoint,
|
DiscoveryDocumentResponse disco = await GetDiscovery();
|
||||||
ClientId = CLIENT_ID,
|
HttpClient client = _httpClientFactory.CreateClient();
|
||||||
RefreshToken = refreshToken
|
TokenResponse response = await client.RequestRefreshTokenAsync(new RefreshTokenRequest
|
||||||
});
|
|
||||||
|
|
||||||
if (response.IsError)
|
|
||||||
{
|
|
||||||
if (response.Error is OidcConstants.TokenErrors.ExpiredToken or OidcConstants.TokenErrors.InvalidGrant)
|
|
||||||
{
|
{
|
||||||
SetStoredRefreshToken(null);
|
Address = disco.TokenEndpoint,
|
||||||
return false;
|
ClientId = CLIENT_ID,
|
||||||
|
RefreshToken = refreshToken
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.IsError)
|
||||||
|
{
|
||||||
|
if (response.Error is OidcConstants.TokenErrors.ExpiredToken or OidcConstants.TokenErrors.InvalidGrant)
|
||||||
|
{
|
||||||
|
SetStoredRefreshToken(null);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new ArtemisWebClientException("Failed to request refresh token: " + response.Error);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new ArtemisWebClientException("Failed to request refresh token: " + response.Error);
|
SetCurrentUser(response);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_logger.Error(e, "Failed to use refresh token");
|
||||||
|
SetStoredRefreshToken(null);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
SetCurrentUser(response);
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static byte[] HashSha256(string inputString)
|
private static byte[] HashSha256(string inputString)
|
||||||
@ -134,6 +146,11 @@ internal class AuthenticationService : CorePropertyChanged, IAuthenticationServi
|
|||||||
|
|
||||||
return _token.AccessToken;
|
return _token.AccessToken;
|
||||||
}
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_logger.Error(e, "Failed to retrieve bearer token");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
_authLock.Release();
|
_authLock.Release();
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user