Until now a collected image was a row of text, and after a restart it was not
visible at all. The collect list gains a row thumbnail, and a Gallery page
browses the whole store with filters by source, format and address, paged at
120 tiles, with a built-in viewer showing the full size beside its provenance.
Thumbnails decode straight to the width they are drawn at. That is the whole
memory story: a 4000x3000 JPEG is about 48 MB once decoded, so decoding full
size and scaling afterwards runs out of memory long before the user finishes
scrolling. The cache is bounded and owns its bitmaps, which means its capacity
has to comfortably exceed a page - a bitmap evicted while still on screen would
be disposed out from under the renderer.
Paged rather than infinite-scrolled for the same reason: how much to decode is a
decision the page should make, not one the archive's size makes for it.
Video is not previewed and will not be. Extracting a first frame means FFmpeg,
which is a media stack in exchange for one picture per tile; those tiles show a
format badge instead. The refusal happens before touching the disk, because
attempting it would be an exception per tile.
Rendering the page caught the viewer overlay being see-through: it named a brush
that does not exist, and an unresolved DynamicResource fails silently - the
property just keeps its default. That is the second silent-reference bug to
reach a screenshot, so both kinds now have guards: one resolves every
{DynamicResource} in the XAML against both themes, the other checks every
{l:Loc} key exists. Both were confirmed to fail before being kept.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
87 lines
3.2 KiB
C#
87 lines
3.2 KiB
C#
using AvParser.Core.Collecting;
|
|
|
|
namespace AvParser.UI.HeadlessTests;
|
|
|
|
/// <summary>A store that records what it was asked to do and holds nothing.</summary>
|
|
internal sealed class FakeMediaStore : IMediaStore
|
|
{
|
|
public List<string> Purged { get; } = [];
|
|
|
|
public MediaStoreStats Stats { get; set; } = new(0, 0, 0, 0);
|
|
|
|
public List<StoredMedia> Browse { get; } = [];
|
|
|
|
public List<string> SourceIds { get; } = [];
|
|
|
|
public MediaBrowseQuery? LastBrowse { get; private set; }
|
|
|
|
public int? TotalOverride { get; set; }
|
|
|
|
public bool BrowseThrows { get; set; }
|
|
|
|
public PurgeResult PurgeResult { get; set; } = new(3, 2, 4096);
|
|
|
|
public ShowcaseMode Mode { get; private set; } = ShowcaseMode.HardLink;
|
|
|
|
public Task InitialiseAsync(CancellationToken cancellationToken = default) => Task.CompletedTask;
|
|
|
|
public void Configure(ShowcaseMode mode) => Mode = mode;
|
|
|
|
public Task<string> BeginRunAsync(string sourceId, CancellationToken cancellationToken = default) =>
|
|
Task.FromResult("run");
|
|
|
|
public Task CompleteRunAsync(string runId, RunSummary summary, CancellationToken cancellationToken = default) =>
|
|
Task.CompletedTask;
|
|
|
|
public Task<IReadOnlyDictionary<string, SeenOutcome>> GetSeenAsync(
|
|
string sourceId,
|
|
IReadOnlyCollection<string> urls,
|
|
CancellationToken cancellationToken = default
|
|
) =>
|
|
Task.FromResult<IReadOnlyDictionary<string, SeenOutcome>>(
|
|
new Dictionary<string, SeenOutcome>(StringComparer.Ordinal)
|
|
);
|
|
|
|
public Task RecordSeenAsync(
|
|
string sourceId,
|
|
string url,
|
|
SeenOutcome outcome,
|
|
int? httpStatus = null,
|
|
string? errorCode = null,
|
|
CancellationToken cancellationToken = default
|
|
) => Task.CompletedTask;
|
|
|
|
public Task<IReadOnlySet<string>> LoadTombstonesAsync(CancellationToken cancellationToken = default) =>
|
|
Task.FromResult<IReadOnlySet<string>>(new HashSet<string>(StringComparer.Ordinal));
|
|
|
|
public Task<int> TombstoneAsync(string sha256, string? reason, CancellationToken cancellationToken = default) =>
|
|
Task.FromResult(0);
|
|
|
|
public Task<CollectedItem> StoreAsync(MediaStoreRequest request, CancellationToken cancellationToken = default) =>
|
|
Task.FromResult(new CollectedItem(request.Candidate, request.Blob, CollectStatus.Stored));
|
|
|
|
public Task<PurgeResult> PurgeAsync(PurgeOptions options, CancellationToken cancellationToken = default)
|
|
{
|
|
Purged.Add(options.SourceId);
|
|
|
|
return Task.FromResult(PurgeResult);
|
|
}
|
|
|
|
public Task<int> RebuildShowcaseAsync(string sourceId, CancellationToken cancellationToken = default) =>
|
|
Task.FromResult(0);
|
|
|
|
public Task<MediaStoreStats> GetStatsAsync(CancellationToken cancellationToken = default) => Task.FromResult(Stats);
|
|
|
|
public Task<MediaPage> BrowseAsync(MediaBrowseQuery query, CancellationToken cancellationToken = default)
|
|
{
|
|
LastBrowse = query;
|
|
|
|
return BrowseThrows
|
|
? throw new InvalidOperationException("the index is unreadable")
|
|
: Task.FromResult(new MediaPage(Browse, TotalOverride ?? Browse.Count));
|
|
}
|
|
|
|
public Task<IReadOnlyList<string>> GetSourceIdsAsync(CancellationToken cancellationToken = default) =>
|
|
Task.FromResult<IReadOnlyList<string>>(SourceIds);
|
|
}
|