added url search for giphy, install wizard, fixed ui and added minor changes
This commit is contained in:
@@ -9,7 +9,9 @@ namespace EbbesMemeClipboard.ViewModels;
|
||||
|
||||
public partial class PickerViewModel : ObservableObject
|
||||
{
|
||||
private const int RemoteResultLimit = 30;
|
||||
// 50 is Giphy's maximum per request; more results come from paging via offset as the user
|
||||
// scrolls rather than from a single bigger call.
|
||||
private const int RemoteResultLimit = 50;
|
||||
private static readonly TimeSpan SearchDebounce = TimeSpan.FromMilliseconds(350);
|
||||
|
||||
private readonly ILocalMemeLibraryService _library;
|
||||
@@ -23,6 +25,13 @@ public partial class PickerViewModel : ObservableObject
|
||||
/// <summary>Cancels the in-flight remote search when a newer keystroke supersedes it.</summary>
|
||||
private CancellationTokenSource? _searchCts;
|
||||
|
||||
private int _remoteOffset;
|
||||
private bool _remoteHasMore;
|
||||
private bool _isLoadingMore;
|
||||
|
||||
/// <summary>True when the grid is showing a single item resolved from a pasted link.</summary>
|
||||
private bool _showingResolvedLink;
|
||||
|
||||
public ObservableCollection<MemeTileViewModel> Items { get; } = new();
|
||||
|
||||
/// <summary>Favourites for the currently active source only.</summary>
|
||||
@@ -49,6 +58,12 @@ public partial class PickerViewModel : ObservableObject
|
||||
public bool IsLocalSource => ActiveSource == MemeSource.Local;
|
||||
public bool IsGiphySource => ActiveSource == MemeSource.Giphy;
|
||||
|
||||
public string SearchPlaceholder => IsLocalSource
|
||||
? "Search your memes..."
|
||||
: ActiveProvider?.SearchPlaceholder ?? "Search...";
|
||||
|
||||
private IGifProvider? ActiveProvider => _providers.FirstOrDefault(p => p.Source == ActiveSource);
|
||||
|
||||
public event EventHandler? RequestClose;
|
||||
|
||||
public PickerViewModel(
|
||||
@@ -78,6 +93,7 @@ public partial class PickerViewModel : ObservableObject
|
||||
{
|
||||
OnPropertyChanged(nameof(IsLocalSource));
|
||||
OnPropertyChanged(nameof(IsGiphySource));
|
||||
OnPropertyChanged(nameof(SearchPlaceholder));
|
||||
SearchText = string.Empty;
|
||||
RefreshFavorites();
|
||||
// Setting SearchText above only triggers a refresh if the value actually changed, so
|
||||
@@ -264,6 +280,50 @@ public partial class PickerViewModel : ObservableObject
|
||||
RefreshFavorites();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends the next page of remote results. Called as the grid is scrolled near the bottom,
|
||||
/// so browsing feels continuous instead of stopping dead at the first batch.
|
||||
/// </summary>
|
||||
public async Task LoadMoreAsync()
|
||||
{
|
||||
if (IsLocalSource || _showingResolvedLink || !_remoteHasMore || _isLoadingMore) return;
|
||||
|
||||
var provider = ActiveProvider;
|
||||
if (provider is null || !provider.IsConfigured) return;
|
||||
|
||||
_isLoadingMore = true;
|
||||
var queryAtStart = SearchText;
|
||||
try
|
||||
{
|
||||
var results = await provider.SearchAsync(queryAtStart, RemoteResultLimit, _remoteOffset, CancellationToken.None);
|
||||
|
||||
// The query may have changed while this page was in flight; if so the newer search
|
||||
// owns the grid and these results are stale.
|
||||
if (!string.Equals(queryAtStart, SearchText, StringComparison.Ordinal)) return;
|
||||
|
||||
foreach (var result in results)
|
||||
{
|
||||
var tile = new MemeTileViewModel(result, _gifCache);
|
||||
Items.Add(tile);
|
||||
_ = tile.LoadThumbnailAsync();
|
||||
}
|
||||
|
||||
_remoteOffset += results.Count;
|
||||
_remoteHasMore = results.Count >= RemoteResultLimit;
|
||||
SyncFavoriteFlags();
|
||||
StatusMessage = $"{Items.Count} result(s).";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusMessage = $"Couldn't load more: {ex.Message}";
|
||||
_remoteHasMore = false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isLoadingMore = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RefreshAsync()
|
||||
{
|
||||
// Any pending remote search is stale the moment the query or tab changes.
|
||||
@@ -271,13 +331,17 @@ public partial class PickerViewModel : ObservableObject
|
||||
_searchCts?.Dispose();
|
||||
_searchCts = null;
|
||||
|
||||
_remoteOffset = 0;
|
||||
_remoteHasMore = false;
|
||||
_showingResolvedLink = false;
|
||||
|
||||
if (IsLocalSource)
|
||||
{
|
||||
RefreshLocalItems();
|
||||
return;
|
||||
}
|
||||
|
||||
var provider = _providers.FirstOrDefault(p => p.Source == ActiveSource);
|
||||
var provider = ActiveProvider;
|
||||
if (provider is null) return;
|
||||
|
||||
if (!provider.IsConfigured)
|
||||
@@ -297,9 +361,15 @@ public partial class PickerViewModel : ObservableObject
|
||||
await Task.Delay(SearchDebounce, cts.Token);
|
||||
|
||||
IsBusy = true;
|
||||
StatusMessage = "Searching...";
|
||||
|
||||
var results = await provider.SearchAsync(SearchText, RemoteResultLimit, cts.Token);
|
||||
if (provider.CanResolveLink(SearchText))
|
||||
{
|
||||
await ResolveLinkAsync(provider, cts.Token);
|
||||
return;
|
||||
}
|
||||
|
||||
StatusMessage = "Searching...";
|
||||
var results = await provider.SearchAsync(SearchText, RemoteResultLimit, 0, cts.Token);
|
||||
cts.Token.ThrowIfCancellationRequested();
|
||||
|
||||
Items.Clear();
|
||||
@@ -310,6 +380,9 @@ public partial class PickerViewModel : ObservableObject
|
||||
_ = tile.LoadThumbnailAsync(cts.Token);
|
||||
}
|
||||
|
||||
_remoteOffset = results.Count;
|
||||
_remoteHasMore = results.Count >= RemoteResultLimit;
|
||||
|
||||
SyncFavoriteFlags();
|
||||
StatusMessage = results.Count == 0 ? "No results." : $"{results.Count} result(s).";
|
||||
}
|
||||
@@ -330,6 +403,30 @@ public partial class PickerViewModel : ObservableObject
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ResolveLinkAsync(IGifProvider provider, CancellationToken ct)
|
||||
{
|
||||
StatusMessage = "Opening link...";
|
||||
var result = await provider.ResolveLinkAsync(SearchText, ct);
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
Items.Clear();
|
||||
|
||||
if (result is null)
|
||||
{
|
||||
StatusMessage = "Couldn't find that GIF - check the link.";
|
||||
return;
|
||||
}
|
||||
|
||||
var tile = new MemeTileViewModel(result, _gifCache);
|
||||
Items.Add(tile);
|
||||
_ = tile.LoadThumbnailAsync(ct);
|
||||
|
||||
// A single resolved item has nothing to page through.
|
||||
_showingResolvedLink = true;
|
||||
SyncFavoriteFlags();
|
||||
StatusMessage = "Found 1 GIF from link.";
|
||||
}
|
||||
|
||||
private void RefreshLocalItems()
|
||||
{
|
||||
var records = string.IsNullOrWhiteSpace(SearchText)
|
||||
|
||||
Reference in New Issue
Block a user