diff --git a/src/Artemis.UI/Screens/Workshop/Entries/EntryVoteView.axaml b/src/Artemis.UI/Screens/Workshop/Entries/EntryVoteView.axaml
new file mode 100644
index 000000000..113a21d96
--- /dev/null
+++ b/src/Artemis.UI/Screens/Workshop/Entries/EntryVoteView.axaml
@@ -0,0 +1,65 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/Entries/EntryVoteView.axaml.cs b/src/Artemis.UI/Screens/Workshop/Entries/EntryVoteView.axaml.cs
new file mode 100644
index 000000000..5c131be6a
--- /dev/null
+++ b/src/Artemis.UI/Screens/Workshop/Entries/EntryVoteView.axaml.cs
@@ -0,0 +1,14 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using Avalonia.ReactiveUI;
+
+namespace Artemis.UI.Screens.Workshop.Entries;
+
+public partial class EntryVoteView : ReactiveUserControl
+{
+ public EntryVoteView()
+ {
+ InitializeComponent();
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/Entries/EntryVoteViewModel.cs b/src/Artemis.UI/Screens/Workshop/Entries/EntryVoteViewModel.cs
new file mode 100644
index 000000000..14e449974
--- /dev/null
+++ b/src/Artemis.UI/Screens/Workshop/Entries/EntryVoteViewModel.cs
@@ -0,0 +1,91 @@
+using System;
+using System.Reactive.Disposables;
+using System.Threading.Tasks;
+using Artemis.UI.Shared;
+using Artemis.UI.Shared.Services;
+using Artemis.UI.Shared.Services.Builders;
+using Artemis.WebClient.Workshop;
+using Artemis.WebClient.Workshop.Services;
+using PropertyChanged.SourceGenerator;
+using ReactiveUI;
+
+namespace Artemis.UI.Screens.Workshop.Entries;
+
+public partial class EntryVoteViewModel : ActivatableViewModelBase
+{
+ private readonly IEntrySummary _entry;
+ private readonly INotificationService _notificationService;
+ private readonly IVoteClient _voteClient;
+ private bool _voting;
+
+ [Notify] private int _score;
+ [Notify] private bool _upvoted;
+ [Notify] private bool _downvoted;
+
+ public EntryVoteViewModel(IEntrySummary entry, IAuthenticationService authenticationService, INotificationService notificationService, IVoteClient voteClient)
+ {
+ _entry = entry;
+ _notificationService = notificationService;
+ _voteClient = voteClient;
+
+ IsLoggedIn = authenticationService.IsLoggedIn;
+ Score = entry.UpvoteCount - entry.DownvoteCount;
+ this.WhenActivated(d => IsLoggedIn.Subscribe(l => _ = GetVoteStatus(l)).DisposeWith(d));
+ }
+
+ public IObservable IsLoggedIn { get; }
+
+ public async Task CastVote(bool upvote)
+ {
+ // Could use a ReactiveCommand to achieve the same thing but that disables the button
+ // while executing which grays it out for a fraction of a second and looks bad
+ if (_voting)
+ return;
+
+ _voting = true;
+ try
+ {
+ IVoteCount? result;
+ // If the vote was removed, reset the upvote/downvote state
+ if ((Upvoted && upvote) || (Downvoted && !upvote))
+ {
+ result = await _voteClient.ClearVote(_entry.Id);
+ Upvoted = false;
+ Downvoted = false;
+ }
+ else
+ {
+ result = await _voteClient.CastVote(_entry.Id, upvote);
+ Upvoted = upvote;
+ Downvoted = !upvote;
+ }
+
+ if (result != null)
+ Score = result.UpvoteCount - result.DownvoteCount;
+ else
+ _notificationService.CreateNotification().WithTitle("Failed to cast vote").WithMessage("Please try again later.").WithSeverity(NotificationSeverity.Error).Show();
+ }
+ finally
+ {
+ _voting = false;
+ }
+ }
+
+ private async Task GetVoteStatus(bool isLoggedIn)
+ {
+ if (!isLoggedIn)
+ {
+ Upvoted = false;
+ Downvoted = false;
+ }
+ else
+ {
+ bool? vote = await _voteClient.GetVote(_entry.Id);
+ if (vote != null)
+ {
+ Upvoted = vote.Value;
+ Downvoted = !vote.Value;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListInputView.axaml b/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListInputView.axaml
index 8fdc91d29..476ae9bd2 100644
--- a/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListInputView.axaml
+++ b/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListInputView.axaml
@@ -21,6 +21,7 @@
Recently updated
Recently added
Download count
+ Score
diff --git a/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListItemView.axaml b/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListItemView.axaml
index 22d723a06..7473034d3 100644
--- a/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListItemView.axaml
+++ b/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListItemView.axaml
@@ -21,9 +21,12 @@
HorizontalContentAlignment="Stretch"
Command="{CompiledBinding NavigateToEntry}"
IsVisible="{CompiledBinding Entry, Converter={x:Static ObjectConverters.IsNotNull}}">
-
+
+
+
+
-
-
+
@@ -79,7 +82,7 @@
-
+
@@ -89,7 +92,7 @@
-
+
installed
diff --git a/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListItemViewModel.cs b/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListItemViewModel.cs
index d129f9be6..a02325bc4 100644
--- a/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListItemViewModel.cs
+++ b/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListItemViewModel.cs
@@ -1,5 +1,4 @@
using System;
-using System.Reactive;
using System.Reactive.Disposables;
using System.Threading.Tasks;
using Artemis.UI.Shared;
@@ -18,12 +17,12 @@ public partial class EntryListItemViewModel : ActivatableViewModelBase
[Notify] private bool _isInstalled;
[Notify] private bool _updateAvailable;
- public EntryListItemViewModel(IEntrySummary entry, IRouter router, IWorkshopService workshopService)
+ public EntryListItemViewModel(IEntrySummary entry, IRouter router, IWorkshopService workshopService, Func getEntryVoteViewModel)
{
_router = router;
Entry = entry;
- NavigateToEntry = ReactiveCommand.CreateFromTask(ExecuteNavigateToEntry);
+ VoteViewModel = getEntryVoteViewModel(entry);
this.WhenActivated((CompositeDisposable _) =>
{
@@ -34,9 +33,9 @@ public partial class EntryListItemViewModel : ActivatableViewModelBase
}
public IEntrySummary Entry { get; }
- public ReactiveCommand NavigateToEntry { get; }
+ public EntryVoteViewModel VoteViewModel { get; }
- private async Task ExecuteNavigateToEntry()
+ public async Task NavigateToEntry()
{
switch (Entry.EntryType)
{
diff --git a/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListViewModel.cs b/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListViewModel.cs
index a6b5309c7..48388aeb5 100644
--- a/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListViewModel.cs
+++ b/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListViewModel.cs
@@ -137,6 +137,9 @@ public partial class EntryListViewModel : RoutableScreen
if (InputViewModel.SortBy == 2)
return new[] {new EntrySortInput {Downloads = SortEnumType.Desc}};
+ // Sort by score
+ if (InputViewModel.SortBy == 3)
+ return new[] {new EntrySortInput {Score = SortEnumType.Desc}};
// Sort by latest release, then by created at
return new[]
diff --git a/src/Artemis.UI/Screens/Workshop/Library/Tabs/InstalledTabItemView.axaml b/src/Artemis.UI/Screens/Workshop/Library/Tabs/InstalledTabItemView.axaml
index 71ab5983c..b8347c3df 100644
--- a/src/Artemis.UI/Screens/Workshop/Library/Tabs/InstalledTabItemView.axaml
+++ b/src/Artemis.UI/Screens/Workshop/Library/Tabs/InstalledTabItemView.axaml
@@ -11,7 +11,6 @@
x:DataType="tabs:InstalledTabItemViewModel">
-