Implement media rule editor and enhance gallery functionality
- Introduced a rule editor in the gallery for users to define actions when a media item is found again, allowing for better management of media rules. - Updated `GalleryViewModel` to handle rule actions, including saving, editing, and removing rules, with appropriate UI bindings. - Enhanced UI components in `GalleryView.axaml` to support rule editing, including action selection and source management. - Added localization strings for new rule-related features in both English and Russian. - Improved unit tests to cover new rule management functionalities, ensuring robust behavior during rule creation and editing. These changes significantly enhance the user experience by providing a more interactive and flexible way to manage media items in the gallery.
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
using AvParser.Core.Collecting;
|
||||
using AvParser.Core.Collecting.Sources;
|
||||
using AvParser.UI.Tests.Fakes;
|
||||
using AvParser.UI.ViewModels;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
@@ -25,6 +26,49 @@ public class GalleryViewModelTests
|
||||
IsAnimated = animated,
|
||||
};
|
||||
|
||||
/// <summary>An in-memory catalog, so a rule can be scoped to a source in tests.</summary>
|
||||
private sealed class FakeUserSourceStore(IEnumerable<PatternSourceConfig> seed) : IUserSourceStore
|
||||
{
|
||||
private readonly List<PatternSourceConfig> _configs = [.. seed];
|
||||
|
||||
public event EventHandler? Changed
|
||||
{
|
||||
add { }
|
||||
remove { }
|
||||
}
|
||||
|
||||
public IReadOnlyList<PatternSourceConfig> List() => [.. _configs];
|
||||
|
||||
public Task<PatternSourceConfig> AddAsync(
|
||||
PatternSourceConfig config,
|
||||
CancellationToken cancellationToken = default
|
||||
) => Task.FromResult(config);
|
||||
|
||||
public Task<bool> UpdateAsync(PatternSourceConfig config, CancellationToken cancellationToken = default) =>
|
||||
Task.FromResult(false);
|
||||
|
||||
public Task<bool> RemoveAsync(string id, CancellationToken cancellationToken = default) =>
|
||||
Task.FromResult(false);
|
||||
}
|
||||
|
||||
private static PatternSourceConfig Config(string id)
|
||||
{
|
||||
PatternSourceConfig.TryCreate(
|
||||
id,
|
||||
"https://imgtest.example/x/",
|
||||
6,
|
||||
8,
|
||||
IdAlphabet.Alphanumeric,
|
||||
null,
|
||||
".jpg",
|
||||
allowDirectConnection: false,
|
||||
out var config,
|
||||
id
|
||||
);
|
||||
|
||||
return config!;
|
||||
}
|
||||
|
||||
private static (GalleryViewModel Page, FakeMediaStore Store, FakeThumbnailCache Thumbnails) Build(
|
||||
params StoredMedia[] items
|
||||
)
|
||||
@@ -36,6 +80,7 @@ public class GalleryViewModelTests
|
||||
var thumbnails = new FakeThumbnailCache();
|
||||
var page = new GalleryViewModel(
|
||||
store,
|
||||
new MediaSourceCatalog(new FakeUserSourceStore([Config("url-list"), Config("other")])),
|
||||
thumbnails,
|
||||
NullLogger<GalleryViewModel>.Instance,
|
||||
ImmediateSequencer.Instance
|
||||
@@ -171,6 +216,90 @@ public class GalleryViewModelTests
|
||||
page.IsLoading.ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task A_new_rule_starts_scoped_to_the_source_the_picture_came_from()
|
||||
{
|
||||
var (page, _, _) = Build(Media("https://a.test/1.png"));
|
||||
await page.LoadAsync(0, TestContext.Current.CancellationToken);
|
||||
page.Selected = page.Items[0];
|
||||
|
||||
await page.EditRuleCommand.Execute().ToTask(TestContext.Current.CancellationToken);
|
||||
|
||||
page.IsRuleEditorOpen.ShouldBeTrue();
|
||||
page.HasRule.ShouldBeFalse();
|
||||
page.RuleSources.Single(source => source.IsSelected).Id.ShouldBe("url-list");
|
||||
page.RuleAction.Value.ShouldBe(MediaRuleAction.Skip);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Saving_a_skip_rule_records_it_and_drops_the_copy_that_prompted_it()
|
||||
{
|
||||
var (page, store, _) = Build(Media("https://a.test/1.png"));
|
||||
await page.LoadAsync(0, TestContext.Current.CancellationToken);
|
||||
page.Selected = page.Items[0];
|
||||
var hash = page.Selected.Media.Sha256;
|
||||
|
||||
await page.EditRuleCommand.Execute().ToTask(TestContext.Current.CancellationToken);
|
||||
await page.SaveRuleCommand.Execute().ToTask(TestContext.Current.CancellationToken);
|
||||
|
||||
var rule = store.Rules.ShouldHaveSingleItem();
|
||||
rule.Sha256.ShouldBe(hash);
|
||||
rule.Action.ShouldBe(MediaRuleAction.Skip);
|
||||
rule.AppliesTo("url-list").ShouldBeTrue();
|
||||
rule.AppliesTo("other").ShouldBeFalse();
|
||||
|
||||
// The thing the rule was made about is gone, so the viewer cannot keep showing it.
|
||||
page.IsRuleEditorOpen.ShouldBeFalse();
|
||||
page.Selected.ShouldBeNull();
|
||||
page.StatusMessage.ShouldNotBeNull().ShouldContain("Rule saved");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task A_rule_with_no_source_ticked_covers_every_source()
|
||||
{
|
||||
var (page, store, _) = Build(Media("https://a.test/1.png"));
|
||||
await page.LoadAsync(0, TestContext.Current.CancellationToken);
|
||||
page.Selected = page.Items[0];
|
||||
|
||||
await page.EditRuleCommand.Execute().ToTask(TestContext.Current.CancellationToken);
|
||||
|
||||
foreach (var source in page.RuleSources)
|
||||
{
|
||||
source.IsSelected = false;
|
||||
}
|
||||
|
||||
page.RuleAction = page.RuleActions.Single(option => option.Value == MediaRuleAction.RetryElsewhere);
|
||||
await page.SaveRuleCommand.Execute().ToTask(TestContext.Current.CancellationToken);
|
||||
|
||||
var rule = store.Rules.ShouldHaveSingleItem();
|
||||
rule.SourceIds.ShouldBeNull();
|
||||
rule.AppliesTo("anything-at-all").ShouldBeTrue();
|
||||
rule.Action.ShouldBe(MediaRuleAction.RetryElsewhere);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task An_existing_rule_comes_back_into_the_editor()
|
||||
{
|
||||
var (page, store, _) = Build(Media("https://a.test/1.png"));
|
||||
await page.LoadAsync(0, TestContext.Current.CancellationToken);
|
||||
page.Selected = page.Items[0];
|
||||
|
||||
store.Rules.Add(
|
||||
new MediaRule(page.Selected.Media.Sha256, MediaRuleAction.RetryElsewhere) { SourceIds = "other" }
|
||||
);
|
||||
|
||||
await page.EditRuleCommand.Execute().ToTask(TestContext.Current.CancellationToken);
|
||||
|
||||
page.HasRule.ShouldBeTrue();
|
||||
page.RuleAction.Value.ShouldBe(MediaRuleAction.RetryElsewhere);
|
||||
page.RuleSources.Single(source => source.IsSelected).Id.ShouldBe("other");
|
||||
|
||||
await page.RemoveRuleCommand.Execute().ToTask(TestContext.Current.CancellationToken);
|
||||
|
||||
store.Rules.ShouldBeEmpty();
|
||||
page.HasRule.ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Disposing_twice_is_safe()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user