Skip to content

Commit

Permalink
add more filters to user list view #38
Browse files Browse the repository at this point in the history
  • Loading branch information
insomniachi committed Jul 15, 2023
1 parent 789ab11 commit 3926eb4
Show file tree
Hide file tree
Showing 7 changed files with 256 additions and 161 deletions.
4 changes: 2 additions & 2 deletions Totoro.Core.Tests/ViewModels/UserListViewModelTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public void UserListViewModel_InitializesProperly()
vm.SetInitialState();

// assert
Assert.Equal(AnimeStatus.Watching, vm.CurrentView);
Assert.Equal(AnimeStatus.Watching, vm.Filter.ListStatus);
Assert.Equal(2, vm.Anime.Count);
}

Expand All @@ -48,7 +48,7 @@ public void UserListViewModel_FilterByStatusWorks(AnimeStatus status, int count)
// act
vm.ChangeCurrentViewCommand.Execute(status);
Assert.Equal(count, vm.Anime.Count);
Assert.Equal(status, vm.CurrentView);
Assert.Equal(status, vm.Filter.ListStatus);
}

[Fact]
Expand Down
44 changes: 28 additions & 16 deletions Totoro.Core/ViewModels/UserListViewModel.cs
Original file line number Diff line number Diff line change
@@ -1,23 +1,26 @@
using System.Linq;
using System.Text.RegularExpressions;

namespace Totoro.Core.ViewModels;


public class AnimeCollectionFilter : ReactiveObject
public partial class AnimeCollectionFilter : ReactiveObject
{
[Reactive] public AnimeStatus ListStatus { get; set; } = AnimeStatus.Watching;
[Reactive] public string SearchText { get; set; }
[Reactive] public int? Year { get; set; }
[Reactive] public string Year { get; set; }
[Reactive] public AiringStatus? AiringStatus { get; set; }
public ObservableCollection<string> Genres { get; } = new();
[Reactive] public ObservableCollection<string> Genres { get; set; } = new();

[GeneratedRegex(@"(19[5-9][0-9])|(20\d{2})")]
private partial Regex YearRegex();

public bool IsVisible(AnimeModel model)
{
var listStatusCheck = model.Tracking.Status == ListStatus;
var searchTextStatus = string.IsNullOrEmpty(SearchText) ||
model.Title.Contains(SearchText, StringComparison.InvariantCultureIgnoreCase) ||
model.AlternativeTitles.Any(x => x.Contains(SearchText, StringComparison.InvariantCultureIgnoreCase));
var yearCheck = Year is null || model.Season.Year == Year;
var yearCheck = string.IsNullOrEmpty(Year) || !YearRegex().IsMatch(Year) || model.Season.Year.ToString() == Year;
var genresCheck = !Genres.Any() || Genres.All(x => model.Genres.Any(y => string.Equals(y, x, StringComparison.InvariantCultureIgnoreCase)));
var airingStatusCheck = AiringStatus is null || AiringStatus == model.AiringStatus;

Expand All @@ -33,6 +36,7 @@ public class UserListViewModel : NavigatableViewModel, IHaveState
private readonly SourceCache<AnimeModel, long> _searchCache = new(x => x.Id);
private readonly ReadOnlyObservableCollection<AnimeModel> _anime;
private readonly ReadOnlyObservableCollection<AnimeModel> _searchResults;
private readonly HashSet<string> _genres = new();

public UserListViewModel(ITrackingServiceContext trackingService,
IAnimeServiceContext animeService,
Expand All @@ -42,14 +46,14 @@ public UserListViewModel(ITrackingServiceContext trackingService,
_viewService = viewService;
IsAuthenticated = trackingService.IsAuthenticated;

ChangeCurrentViewCommand = ReactiveCommand.Create<AnimeStatus>(x => CurrentView = x);
ChangeCurrentViewCommand = ReactiveCommand.Create<AnimeStatus>(x => Filter.ListStatus = x);
RefreshCommand = ReactiveCommand.CreateFromTask(SetInitialState);
SetDisplayMode = ReactiveCommand.Create<DisplayMode>(x => Mode = x);

_animeCache
.Connect()
.RefCount()
.Filter(Filter.WhenAnyPropertyChanged().Select(x => (Func<AnimeModel,bool>)x.IsVisible))
.Filter(this.WhenAnyValue(x => x.Filter).SelectMany(x => x.WhenAnyPropertyChanged()).Select(x => (Func<AnimeModel, bool>)x.IsVisible))
.Sort(SortExpressionComparer<AnimeModel>.Descending(x => x.MeanScore))
.Bind(out _anime)
.DisposeMany()
Expand All @@ -73,24 +77,20 @@ public UserListViewModel(ITrackingServiceContext trackingService,
.Subscribe(list => _searchCache.EditDiff(list, (first, second) => first.Id == second.Id));
}

[Reactive] public AnimeStatus CurrentView { get; set; } = AnimeStatus.Watching;
[Reactive] public bool IsLoading { get; set; }
[Reactive] public DisplayMode Mode { get; set; } = DisplayMode.Grid;
[Reactive] public string SearchText { get; set; }
[Reactive] public string QuickAddSearchText { get; set; }
[Reactive] public List<string> Genres { get; set; }
[Reactive] public AnimeCollectionFilter Filter { get; set; } = new();

public bool IsAuthenticated { get; }
public AnimeCollectionFilter Filter { get; } = new();
public ReadOnlyObservableCollection<AnimeModel> QuickSearchResults => _searchResults;
public ReadOnlyObservableCollection<AnimeModel> Anime => _anime;
public ICommand ChangeCurrentViewCommand { get; }
public ICommand RefreshCommand { get; }
public ICommand SetDisplayMode { get; }

private Func<AnimeModel, bool> FilterByStatusPredicate(AnimeStatus status) => x => x.Tracking.Status == status;
private static Func<AnimeModel, bool> FilterByTitle(string title) => x => string.IsNullOrEmpty(title) ||
x.Title.ToLower().Contains(title) ||
(x.AlternativeTitles?.Any(x => x.ToLower().Contains(title)) ?? true);

public Task SetInitialState()
{
if (!IsAuthenticated)
Expand All @@ -99,18 +99,29 @@ public Task SetInitialState()
}

IsLoading = true;
_genres.Clear();

_animeCache.Clear();
_trackingService.GetAnime()
.ObserveOn(RxApp.MainThreadScheduler)
.Finally(() =>
{
IsLoading = false;
Genres = new(_genres);
Filter.RaisePropertyChanged(nameof(Filter.ListStatus));
})
.Subscribe(list =>
{
_animeCache.EditDiff(list, (item1, item2) => item1.Id == item2.Id);

foreach (var anime in list)
{
foreach (var genre in anime.Genres)
{
_genres.Add(genre);
}
}

IsLoading = false;
}, RxApp.DefaultExceptionHandler.OnError)
.DisposeWith(Garbage);
Expand All @@ -126,14 +137,15 @@ public void StoreState(IState state)
}

state.AddOrUpdate(_animeCache.Items, nameof(Anime));
state.AddOrUpdate(CurrentView);
state.AddOrUpdate(Filter);
}

public void RestoreState(IState state)
{
var anime = state.GetValue<IEnumerable<AnimeModel>>(nameof(Anime));
_animeCache.Edit(x => x.AddOrUpdate(anime));
CurrentView = state.GetValue<AnimeStatus>(nameof(CurrentView));
Filter = state.GetValue<AnimeCollectionFilter>(nameof(Filter));
Filter.RaisePropertyChanged(nameof(Filter.ListStatus));
}

public async Task<Unit> UpdateAnime(IAnimeModel model) => await _viewService.UpdateTracking(model);
Expand Down
5 changes: 5 additions & 0 deletions Totoro.WinUI/App.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,12 @@ public partial class App : Application, IEnableLogger
.Build();

public static TotoroCommands Commands { get; private set; }

#if RELEASE
public static bool HandleClosedEvents { get; set; } = true;
#else
public static bool HandleClosedEvents { get; set; }
#endif

public static T GetService<T>()
where T : class
Expand Down
9 changes: 0 additions & 9 deletions Totoro.WinUI/Services/ActivationService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,21 +43,12 @@ public async Task ActivateAsync(object activationArgs)
// Execute tasks before activation.
await InitializeAsync();


// Handle activation via ActivationHandlers.
await HandleActivationAsync(activationArgs);

// Activate the MainWindow.
App.MainWindow.Activate();
App.MainWindow.Maximize();
App.MainWindow.Closed += (sender, args) =>
{
if (App.HandleClosedEvents)
{
args.Handled = true;
App.MainWindow.Hide();
}
};
App.MainWindow.AppWindow.Closing += AppWindow_Closing;

// Execute tasks after activation.
Expand Down
23 changes: 20 additions & 3 deletions Totoro.WinUI/Views/ShellPage.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,19 @@ public ShellPage()

private void OnLoaded(object sender, RoutedEventArgs e)
{
App.MainWindow.Closed += (sender, args) =>
{
if (App.HandleClosedEvents)
{
args.Handled = true;
App.MainWindow.Hide();
}
else
{
TrayIcon?.Dispose();
}
};

App.MainWindow.ExtendsContentIntoTitleBar = true;
App.MainWindow.SetTitleBar(AppTitleBar);
App.MainWindow.Activated += MainWindow_Activated;
Expand All @@ -51,8 +64,7 @@ private void OnLoaded(object sender, RoutedEventArgs e)

private void MainWindow_Activated(object sender, WindowActivatedEventArgs args)
{
var resource = args.WindowActivationState == WindowActivationState.Deactivated ? "WindowCaptionForegroundDisabled" : "WindowCaptionForeground";
//AppTitleBarText.Foreground = (SolidColorBrush)App.Current.Resources[resource];

}

private void NavigationViewControl_DisplayModeChanged(NavigationView sender, NavigationViewDisplayModeChangedEventArgs args)
Expand Down Expand Up @@ -100,14 +112,19 @@ private void Feedback_Tapped(object sender, TappedRoutedEventArgs e)
public ICommand ShowHideWindowCommand { get; }
public ICommand ExitApplicationCommand { get; }

public void ShowHideWindow()
public static void ShowHideWindow()
{
var window = App.MainWindow;
if (window == null)
{
return;
}

if(!App.HandleClosedEvents)
{
return;
}

if (window.Visible)
{
window.Hide();
Expand Down
Loading

0 comments on commit 3926eb4

Please sign in to comment.