moved favourites into submenue
This commit is contained in:
@@ -22,11 +22,8 @@ public sealed class AppSettings
|
|||||||
public bool PlayGifPreviews { get; set; }
|
public bool PlayGifPreviews { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Which tab the picker reopens on. Persisted so the choice survives an app restart, not
|
/// Which source tab the picker reopens on. Persisted so the choice survives an app restart,
|
||||||
/// just a hide/show. Defaults to the favourites view, since that is where the memes you
|
/// not just a hide/show.
|
||||||
/// actually reach for most often live.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public MemeSource LastSource { get; set; } = MemeSource.Local;
|
public MemeSource LastSource { get; set; } = MemeSource.Local;
|
||||||
|
|
||||||
public bool LastShowingFavorites { get; set; } = true;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Collections.Specialized;
|
using System.Collections.Specialized;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Windows.Data;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using CommunityToolkit.Mvvm.Input;
|
using CommunityToolkit.Mvvm.Input;
|
||||||
using EbbesMemeClipboard.Models;
|
using EbbesMemeClipboard.Models;
|
||||||
@@ -35,6 +36,21 @@ public partial class PickerViewModel : ObservableObject
|
|||||||
|
|
||||||
public ObservableCollection<MemeTileViewModel> Items { get; } = new();
|
public ObservableCollection<MemeTileViewModel> Items { get; } = new();
|
||||||
|
|
||||||
|
/// <summary>Favourites for the active source, shown in the favourites submenu.</summary>
|
||||||
|
public ObservableCollection<MemeTileViewModel> Favorites { get; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Holds at most one navigation tile ("Favourites" or "Back") that is rendered as the first
|
||||||
|
/// cell of the grid, ahead of the actual memes.
|
||||||
|
/// </summary>
|
||||||
|
public ObservableCollection<FolderTileViewModel> HeaderTiles { get; } = new();
|
||||||
|
|
||||||
|
private readonly CompositeCollection _mainGrid = new();
|
||||||
|
private readonly CompositeCollection _favoritesGrid = new();
|
||||||
|
|
||||||
|
/// <summary>What the grid binds to: the nav tile followed by either the results or the favourites.</summary>
|
||||||
|
public CompositeCollection GridSource => IsInFavoritesView ? _favoritesGrid : _mainGrid;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private string _searchText = string.Empty;
|
private string _searchText = string.Empty;
|
||||||
|
|
||||||
@@ -47,12 +63,11 @@ public partial class PickerViewModel : ObservableObject
|
|||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private bool _isDialogOpen;
|
private bool _isDialogOpen;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Favourites act as a filtered view of the active source rather than an inline row, so they
|
|
||||||
/// get the full grid and the search box narrows within them.
|
|
||||||
/// </summary>
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private bool _showingFavorites;
|
private bool _hasFavorites;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private bool _isInFavoritesView;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private MemeSource _activeSource = MemeSource.Local;
|
private MemeSource _activeSource = MemeSource.Local;
|
||||||
@@ -60,20 +75,13 @@ public partial class PickerViewModel : ObservableObject
|
|||||||
public bool IsLocalSource => ActiveSource == MemeSource.Local;
|
public bool IsLocalSource => ActiveSource == MemeSource.Local;
|
||||||
public bool IsGiphySource => ActiveSource == MemeSource.Giphy;
|
public bool IsGiphySource => ActiveSource == MemeSource.Giphy;
|
||||||
|
|
||||||
/// <summary>Importing only applies to the local library, and not while browsing favourites.</summary>
|
public string SearchPlaceholder => IsLocalSource
|
||||||
public bool CanImport => IsLocalSource && !ShowingFavorites;
|
? "Search your memes..."
|
||||||
|
: ActiveProvider?.SearchPlaceholder ?? "Search...";
|
||||||
|
|
||||||
public string SearchPlaceholder => ShowingFavorites
|
public bool ShowEmptyState => Items.Count == 0 && !IsBusy && IsLocalSource && !IsInFavoritesView;
|
||||||
? "Search favourites..."
|
|
||||||
: IsLocalSource
|
|
||||||
? "Search your memes..."
|
|
||||||
: ActiveProvider?.SearchPlaceholder ?? "Search...";
|
|
||||||
|
|
||||||
public bool ShowEmptyState => Items.Count == 0 && !IsBusy && (ShowingFavorites || IsLocalSource);
|
public string EmptyStateText => "No memes yet. Click +, drag files in, or paste with Ctrl+V.";
|
||||||
|
|
||||||
public string EmptyStateText => ShowingFavorites
|
|
||||||
? "No favourites here yet. Right-click any meme to add one."
|
|
||||||
: "No memes yet. Click +, drag files in, or paste with Ctrl+V.";
|
|
||||||
|
|
||||||
private IGifProvider? ActiveProvider => _providers.FirstOrDefault(p => p.Source == ActiveSource);
|
private IGifProvider? ActiveProvider => _providers.FirstOrDefault(p => p.Source == ActiveSource);
|
||||||
|
|
||||||
@@ -100,33 +108,55 @@ public partial class PickerViewModel : ObservableObject
|
|||||||
|
|
||||||
Items.CollectionChanged += OnItemsChanged;
|
Items.CollectionChanged += OnItemsChanged;
|
||||||
|
|
||||||
// Restore the tab the picker was last left on. Assigned to the backing fields directly
|
// Restore persisted view state. Assigned to the backing fields directly so restoring
|
||||||
// so restoring doesn't count as a user change and immediately re-save.
|
// doesn't count as a user change and immediately re-save.
|
||||||
_activeSource = _settings.Current.LastSource;
|
_activeSource = _settings.Current.LastSource;
|
||||||
_showingFavorites = _settings.Current.LastShowingFavorites;
|
|
||||||
|
|
||||||
// Seed the grid for the restored view. Remote sources are skipped here because that
|
_mainGrid.Add(new CollectionContainer { Collection = HeaderTiles });
|
||||||
// needs an async call - OnShown covers it when the window is actually opened.
|
_mainGrid.Add(new CollectionContainer { Collection = Items });
|
||||||
if (_showingFavorites) RefreshFavoriteItems();
|
_favoritesGrid.Add(new CollectionContainer { Collection = HeaderTiles });
|
||||||
else if (IsLocalSource) RefreshLocalItems();
|
_favoritesGrid.Add(new CollectionContainer { Collection = Favorites });
|
||||||
|
|
||||||
|
// Seed the grid. Remote sources are skipped here because that needs an async call -
|
||||||
|
// OnShown covers it when the window is actually opened.
|
||||||
|
if (IsLocalSource) RefreshLocalItems();
|
||||||
|
RefreshFavorites();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnItemsChanged(object? sender, NotifyCollectionChangedEventArgs e) =>
|
private void OnItemsChanged(object? sender, NotifyCollectionChangedEventArgs e) =>
|
||||||
OnPropertyChanged(nameof(ShowEmptyState));
|
OnPropertyChanged(nameof(ShowEmptyState));
|
||||||
|
|
||||||
partial void OnSearchTextChanged(string value) => _ = RefreshAsync();
|
partial void OnSearchTextChanged(string value)
|
||||||
|
{
|
||||||
|
// A query is a request to search, so it always leaves the favourites submenu.
|
||||||
|
if (!string.IsNullOrWhiteSpace(value)) IsInFavoritesView = false;
|
||||||
|
UpdateHeaderTile();
|
||||||
|
_ = RefreshAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
partial void OnHasFavoritesChanged(bool value) => UpdateHeaderTile();
|
||||||
|
|
||||||
partial void OnIsBusyChanged(bool value) => OnPropertyChanged(nameof(ShowEmptyState));
|
partial void OnIsBusyChanged(bool value) => OnPropertyChanged(nameof(ShowEmptyState));
|
||||||
|
|
||||||
partial void OnShowingFavoritesChanged(bool value)
|
partial void OnIsInFavoritesViewChanged(bool value)
|
||||||
{
|
{
|
||||||
OnPropertyChanged(nameof(SearchPlaceholder));
|
OnPropertyChanged(nameof(GridSource));
|
||||||
OnPropertyChanged(nameof(CanImport));
|
|
||||||
OnPropertyChanged(nameof(EmptyStateText));
|
|
||||||
OnPropertyChanged(nameof(ShowEmptyState));
|
OnPropertyChanged(nameof(ShowEmptyState));
|
||||||
SearchText = string.Empty;
|
UpdateHeaderTile();
|
||||||
PersistViewState();
|
}
|
||||||
_ = RefreshAsync();
|
|
||||||
|
private void UpdateHeaderTile()
|
||||||
|
{
|
||||||
|
HeaderTiles.Clear();
|
||||||
|
|
||||||
|
if (IsInFavoritesView)
|
||||||
|
{
|
||||||
|
HeaderTiles.Add(new FolderTileViewModel("\u25C4", "Back"));
|
||||||
|
}
|
||||||
|
else if (HasFavorites && string.IsNullOrWhiteSpace(SearchText))
|
||||||
|
{
|
||||||
|
HeaderTiles.Add(new FolderTileViewModel("\u2605", "Favourites"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
partial void OnActiveSourceChanged(MemeSource value)
|
partial void OnActiveSourceChanged(MemeSource value)
|
||||||
@@ -134,27 +164,24 @@ public partial class PickerViewModel : ObservableObject
|
|||||||
OnPropertyChanged(nameof(IsLocalSource));
|
OnPropertyChanged(nameof(IsLocalSource));
|
||||||
OnPropertyChanged(nameof(IsGiphySource));
|
OnPropertyChanged(nameof(IsGiphySource));
|
||||||
OnPropertyChanged(nameof(SearchPlaceholder));
|
OnPropertyChanged(nameof(SearchPlaceholder));
|
||||||
OnPropertyChanged(nameof(CanImport));
|
|
||||||
OnPropertyChanged(nameof(ShowEmptyState));
|
OnPropertyChanged(nameof(ShowEmptyState));
|
||||||
|
|
||||||
|
_settings.Current.LastSource = value;
|
||||||
|
_ = _settings.SaveAsync();
|
||||||
|
|
||||||
|
IsInFavoritesView = false;
|
||||||
SearchText = string.Empty;
|
SearchText = string.Empty;
|
||||||
PersistViewState();
|
RefreshFavorites();
|
||||||
// Setting SearchText above only triggers a refresh if the value actually changed, so
|
// Setting SearchText above only triggers a refresh if the value actually changed, so
|
||||||
// refresh explicitly here to cover switching tabs with an already-empty box.
|
// refresh explicitly here to cover switching tabs with an already-empty box.
|
||||||
_ = RefreshAsync();
|
_ = RefreshAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void PersistViewState()
|
|
||||||
{
|
|
||||||
_settings.Current.LastSource = ActiveSource;
|
|
||||||
_settings.Current.LastShowingFavorites = ShowingFavorites;
|
|
||||||
_ = _settings.SaveAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private void SelectSource(MemeSource source) => ActiveSource = source;
|
private void SelectSource(MemeSource source) => ActiveSource = source;
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private void ToggleFavoritesView() => ShowingFavorites = !ShowingFavorites;
|
private void ToggleFavoritesView() => IsInFavoritesView = !IsInFavoritesView;
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private async Task SelectItemAsync(MemeTileViewModel? tile)
|
private async Task SelectItemAsync(MemeTileViewModel? tile)
|
||||||
@@ -198,11 +225,7 @@ public partial class PickerViewModel : ObservableObject
|
|||||||
if (_favorites.IsFavorite(tile.Source, tile.FavoriteKey))
|
if (_favorites.IsFavorite(tile.Source, tile.FavoriteKey))
|
||||||
{
|
{
|
||||||
await _favorites.RemoveAsync(tile.Source, tile.FavoriteKey);
|
await _favorites.RemoveAsync(tile.Source, tile.FavoriteKey);
|
||||||
tile.IsFavorite = false;
|
|
||||||
StatusMessage = $"Removed {tile.DisplayName} from favourites";
|
StatusMessage = $"Removed {tile.DisplayName} from favourites";
|
||||||
|
|
||||||
// In the favourites view an unfavourited tile no longer belongs on screen.
|
|
||||||
if (ShowingFavorites) Items.Remove(tile);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -215,9 +238,11 @@ public partial class PickerViewModel : ObservableObject
|
|||||||
FullUrl = tile.RemoteResult?.FullUrl,
|
FullUrl = tile.RemoteResult?.FullUrl,
|
||||||
DateAdded = DateTimeOffset.Now,
|
DateAdded = DateTimeOffset.Now,
|
||||||
});
|
});
|
||||||
tile.IsFavorite = true;
|
|
||||||
StatusMessage = $"Added {tile.DisplayName} to favourites";
|
StatusMessage = $"Added {tile.DisplayName} to favourites";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RefreshFavorites();
|
||||||
|
SyncFavoriteFlags();
|
||||||
}
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
@@ -230,7 +255,8 @@ public partial class PickerViewModel : ObservableObject
|
|||||||
// Drop the matching favourite too, otherwise it lingers pointing at a deleted file.
|
// Drop the matching favourite too, otherwise it lingers pointing at a deleted file.
|
||||||
await _favorites.RemoveAsync(MemeSource.Local, tile.Record.Id);
|
await _favorites.RemoveAsync(MemeSource.Local, tile.Record.Id);
|
||||||
|
|
||||||
await RefreshAsync();
|
RefreshLocalItems();
|
||||||
|
RefreshFavorites();
|
||||||
StatusMessage = $"Removed {name}";
|
StatusMessage = $"Removed {name}";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -296,7 +322,6 @@ public partial class PickerViewModel : ObservableObject
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ShowingFavorites = false;
|
|
||||||
ActiveSource = MemeSource.Local;
|
ActiveSource = MemeSource.Local;
|
||||||
RefreshLocalItems();
|
RefreshLocalItems();
|
||||||
StatusMessage = $"Pasted {name}";
|
StatusMessage = $"Pasted {name}";
|
||||||
@@ -320,7 +345,6 @@ public partial class PickerViewModel : ObservableObject
|
|||||||
}
|
}
|
||||||
|
|
||||||
var imported = await _library.ImportAsync(candidates);
|
var imported = await _library.ImportAsync(candidates);
|
||||||
ShowingFavorites = false;
|
|
||||||
ActiveSource = MemeSource.Local;
|
ActiveSource = MemeSource.Local;
|
||||||
RefreshLocalItems();
|
RefreshLocalItems();
|
||||||
StatusMessage = $"Added {imported.Count} meme(s).";
|
StatusMessage = $"Added {imported.Count} meme(s).";
|
||||||
@@ -328,7 +352,9 @@ public partial class PickerViewModel : ObservableObject
|
|||||||
|
|
||||||
public void OnShown()
|
public void OnShown()
|
||||||
{
|
{
|
||||||
|
IsInFavoritesView = false;
|
||||||
SearchText = string.Empty;
|
SearchText = string.Empty;
|
||||||
|
RefreshFavorites();
|
||||||
// Rebuild rather than just reset: picks up memes added since last time, and lets a
|
// Rebuild rather than just reset: picks up memes added since last time, and lets a
|
||||||
// changed "animate GIF previews" setting take effect.
|
// changed "animate GIF previews" setting take effect.
|
||||||
_ = RefreshAsync();
|
_ = RefreshAsync();
|
||||||
@@ -340,7 +366,7 @@ public partial class PickerViewModel : ObservableObject
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task LoadMoreAsync()
|
public async Task LoadMoreAsync()
|
||||||
{
|
{
|
||||||
if (IsLocalSource || ShowingFavorites || _showingResolvedLink || !_remoteHasMore || _isLoadingMore) return;
|
if (IsLocalSource || _showingResolvedLink || !_remoteHasMore || _isLoadingMore) return;
|
||||||
|
|
||||||
var provider = ActiveProvider;
|
var provider = ActiveProvider;
|
||||||
if (provider is null || !provider.IsConfigured) return;
|
if (provider is null || !provider.IsConfigured) return;
|
||||||
@@ -377,7 +403,7 @@ public partial class PickerViewModel : ObservableObject
|
|||||||
|
|
||||||
private async Task RefreshAsync()
|
private async Task RefreshAsync()
|
||||||
{
|
{
|
||||||
// Any pending remote search is stale the moment the query, tab or view changes.
|
// Any pending remote search is stale the moment the query or tab changes.
|
||||||
_searchCts?.Cancel();
|
_searchCts?.Cancel();
|
||||||
_searchCts?.Dispose();
|
_searchCts?.Dispose();
|
||||||
_searchCts = null;
|
_searchCts = null;
|
||||||
@@ -386,12 +412,6 @@ public partial class PickerViewModel : ObservableObject
|
|||||||
_remoteHasMore = false;
|
_remoteHasMore = false;
|
||||||
_showingResolvedLink = false;
|
_showingResolvedLink = false;
|
||||||
|
|
||||||
if (ShowingFavorites)
|
|
||||||
{
|
|
||||||
RefreshFavoriteItems();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (IsLocalSource)
|
if (IsLocalSource)
|
||||||
{
|
{
|
||||||
RefreshLocalItems();
|
RefreshLocalItems();
|
||||||
@@ -487,16 +507,6 @@ public partial class PickerViewModel : ObservableObject
|
|||||||
_ = tile.LoadThumbnailAsync(ct);
|
_ = tile.LoadThumbnailAsync(ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddLocalTile(LocalMemeRecord record)
|
|
||||||
{
|
|
||||||
var tile = new MemeTileViewModel(record, _library.GetFullPath(record), AnimatePreviews)
|
|
||||||
{
|
|
||||||
IsFavorite = _favorites.IsFavorite(MemeSource.Local, record.Id),
|
|
||||||
};
|
|
||||||
Items.Add(tile);
|
|
||||||
_ = tile.LoadThumbnailAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void RefreshLocalItems()
|
private void RefreshLocalItems()
|
||||||
{
|
{
|
||||||
var records = string.IsNullOrWhiteSpace(SearchText)
|
var records = string.IsNullOrWhiteSpace(SearchText)
|
||||||
@@ -506,48 +516,68 @@ public partial class PickerViewModel : ObservableObject
|
|||||||
Items.Clear();
|
Items.Clear();
|
||||||
foreach (var record in records)
|
foreach (var record in records)
|
||||||
{
|
{
|
||||||
AddLocalTile(record);
|
var tile = new MemeTileViewModel(record, _library.GetFullPath(record), AnimatePreviews)
|
||||||
|
{
|
||||||
|
IsFavorite = _favorites.IsFavorite(MemeSource.Local, record.Id),
|
||||||
|
};
|
||||||
|
Items.Add(tile);
|
||||||
|
_ = tile.LoadThumbnailAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
StatusMessage = string.Empty;
|
StatusMessage = string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Favourites for the active source, narrowed by the search box like any other view.</summary>
|
private void RefreshFavorites()
|
||||||
private void RefreshFavoriteItems()
|
|
||||||
{
|
{
|
||||||
Items.Clear();
|
Favorites.Clear();
|
||||||
|
|
||||||
var query = SearchText;
|
|
||||||
foreach (var fav in _favorites.GetForSource(ActiveSource))
|
foreach (var fav in _favorites.GetForSource(ActiveSource))
|
||||||
{
|
{
|
||||||
|
MemeTileViewModel tile;
|
||||||
|
|
||||||
if (fav.Source == MemeSource.Local)
|
if (fav.Source == MemeSource.Local)
|
||||||
{
|
{
|
||||||
var record = _library.GetAll().FirstOrDefault(r => r.Id == fav.Key);
|
var record = _library.GetAll().FirstOrDefault(r => r.Id == fav.Key);
|
||||||
// Favourite pointing at a meme that has since been deleted - skip it rather
|
// Favourite pointing at a meme that has since been deleted - skip it rather
|
||||||
// than rendering a broken tile.
|
// than rendering a broken tile.
|
||||||
if (record is null) continue;
|
if (record is null) continue;
|
||||||
if (!Matches(record.OriginalFileName, query)) continue;
|
tile = new MemeTileViewModel(record, _library.GetFullPath(record), AnimatePreviews);
|
||||||
AddLocalTile(record);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (fav.PreviewUrl is null || fav.FullUrl is null) continue;
|
if (fav.PreviewUrl is null || fav.FullUrl is null) continue;
|
||||||
if (!Matches(fav.Title ?? string.Empty, query)) continue;
|
tile = new MemeTileViewModel(
|
||||||
|
new GifSearchResult
|
||||||
AddRemoteTile(new GifSearchResult
|
{
|
||||||
{
|
Id = fav.Key,
|
||||||
Id = fav.Key,
|
Title = fav.Title ?? "Favourite",
|
||||||
Title = fav.Title ?? "Favourite",
|
PreviewUrl = fav.PreviewUrl,
|
||||||
PreviewUrl = fav.PreviewUrl,
|
FullUrl = fav.FullUrl,
|
||||||
FullUrl = fav.FullUrl,
|
Source = fav.Source,
|
||||||
Source = fav.Source,
|
},
|
||||||
});
|
_gifCache,
|
||||||
|
AnimatePreviews);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tile.IsFavorite = true;
|
||||||
|
Favorites.Add(tile);
|
||||||
|
_ = tile.LoadThumbnailAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
StatusMessage = Items.Count == 0 ? string.Empty : $"{Items.Count} favourite(s).";
|
HasFavorites = Favorites.Count > 0;
|
||||||
|
// Last favourite removed while inside the submenu: nothing left to show, so step out.
|
||||||
|
if (!HasFavorites) IsInFavoritesView = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool Matches(string text, string query) =>
|
/// <summary>Keeps the star state on the main grid in step with the favourites store.</summary>
|
||||||
string.IsNullOrWhiteSpace(query) || text.Contains(query, StringComparison.OrdinalIgnoreCase);
|
private void SyncFavoriteFlags()
|
||||||
|
{
|
||||||
|
foreach (var tile in Items)
|
||||||
|
{
|
||||||
|
tile.IsFavorite = _favorites.IsFavorite(tile.Source, tile.FavoriteKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>A navigation tile occupying the first grid cell (opens or leaves the favourites submenu).</summary>
|
||||||
|
public sealed record FolderTileViewModel(string Glyph, string Label);
|
||||||
|
|||||||
@@ -79,7 +79,7 @@
|
|||||||
<!-- Shared by the favourites row and the main grid so both behave identically.
|
<!-- Shared by the favourites row and the main grid so both behave identically.
|
||||||
No fixed Width: the tile stretches to fill its UniformGrid cell, so a row always
|
No fixed Width: the tile stretches to fill its UniformGrid cell, so a row always
|
||||||
spans the full panel width instead of leaving a ragged gap on the right. -->
|
spans the full panel width instead of leaving a ragged gap on the right. -->
|
||||||
<DataTemplate x:Key="MemeTileTemplate" DataType="{x:Type vm:MemeTileViewModel}">
|
<DataTemplate DataType="{x:Type vm:MemeTileViewModel}">
|
||||||
<Grid Height="118" Margin="4">
|
<Grid Height="118" Margin="4">
|
||||||
<Button Padding="0"
|
<Button Padding="0"
|
||||||
Style="{StaticResource FlatButtonStyle}"
|
Style="{StaticResource FlatButtonStyle}"
|
||||||
@@ -115,6 +115,19 @@
|
|||||||
Visibility="{Binding IsFavorite, Converter={StaticResource BoolToVisibility}}"/>
|
Visibility="{Binding IsFavorite, Converter={StaticResource BoolToVisibility}}"/>
|
||||||
</Grid>
|
</Grid>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
|
<!-- Navigation tile in the first grid cell: opens / leaves the favourites submenu. -->
|
||||||
|
<DataTemplate DataType="{x:Type vm:FolderTileViewModel}">
|
||||||
|
<Button Height="118" Margin="4"
|
||||||
|
Style="{StaticResource FlatButtonStyle}"
|
||||||
|
Command="{Binding DataContext.ToggleFavoritesViewCommand, ElementName=RootWindow}">
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock Text="{Binding Glyph}" FontSize="34" Foreground="#FFC72C"
|
||||||
|
HorizontalAlignment="Center"/>
|
||||||
|
<TextBlock Text="{Binding Label}" FontSize="12" Foreground="#C4C6CC"
|
||||||
|
FontWeight="SemiBold" HorizontalAlignment="Center" Margin="0,4,0,0"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Button>
|
||||||
|
</DataTemplate>
|
||||||
</Window.Resources>
|
</Window.Resources>
|
||||||
|
|
||||||
<Border Background="#F0202225" CornerRadius="10" BorderBrush="#3A3B3E" BorderThickness="1">
|
<Border Background="#F0202225" CornerRadius="10" BorderBrush="#3A3B3E" BorderThickness="1">
|
||||||
@@ -140,28 +153,18 @@
|
|||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<Grid Grid.Row="1" Margin="0,4,0,8">
|
<StackPanel Grid.Row="1" Orientation="Horizontal" Margin="0,4,0,8">
|
||||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Left">
|
<Button Content="Local"
|
||||||
<Button Content="Local"
|
|
||||||
Style="{StaticResource TabButtonStyle}"
|
|
||||||
Tag="{Binding IsLocalSource}"
|
|
||||||
Command="{Binding SelectSourceCommand}"
|
|
||||||
CommandParameter="{x:Static models:MemeSource.Local}"/>
|
|
||||||
<Button Content="Giphy" Margin="6,0,0,0"
|
|
||||||
Style="{StaticResource TabButtonStyle}"
|
|
||||||
Tag="{Binding IsGiphySource}"
|
|
||||||
Command="{Binding SelectSourceCommand}"
|
|
||||||
CommandParameter="{x:Static models:MemeSource.Giphy}"/>
|
|
||||||
</StackPanel>
|
|
||||||
<!-- Right-aligned and separated from the source tabs on purpose: it filters
|
|
||||||
the active source rather than being a source of its own. -->
|
|
||||||
<Button Content="★ Favourites" HorizontalAlignment="Right"
|
|
||||||
Style="{StaticResource TabButtonStyle}"
|
Style="{StaticResource TabButtonStyle}"
|
||||||
Tag="{Binding ShowingFavorites}"
|
Tag="{Binding IsLocalSource}"
|
||||||
Command="{Binding ToggleFavoritesViewCommand}"
|
Command="{Binding SelectSourceCommand}"
|
||||||
ToolTip="Show only your favourites from this source"/>
|
CommandParameter="{x:Static models:MemeSource.Local}"/>
|
||||||
</Grid>
|
<Button Content="Giphy" Margin="6,0,0,0"
|
||||||
|
Style="{StaticResource TabButtonStyle}"
|
||||||
|
Tag="{Binding IsGiphySource}"
|
||||||
|
Command="{Binding SelectSourceCommand}"
|
||||||
|
CommandParameter="{x:Static models:MemeSource.Giphy}"/>
|
||||||
|
</StackPanel>
|
||||||
<Grid Grid.Row="2" Margin="0,0,0,10">
|
<Grid Grid.Row="2" Margin="0,0,0,10">
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="*"/>
|
<ColumnDefinition Width="*"/>
|
||||||
@@ -198,30 +201,31 @@
|
|||||||
<Button Grid.Column="1" Content="+" Width="36" Height="36" Margin="8,0,0,0"
|
<Button Grid.Column="1" Content="+" Width="36" Height="36" Margin="8,0,0,0"
|
||||||
FontSize="18" Style="{StaticResource FlatButtonStyle}"
|
FontSize="18" Style="{StaticResource FlatButtonStyle}"
|
||||||
Command="{Binding ImportFilesCommand}"
|
Command="{Binding ImportFilesCommand}"
|
||||||
Visibility="{Binding CanImport, Converter={StaticResource BoolToVisibility}}"
|
Visibility="{Binding IsLocalSource, Converter={StaticResource BoolToVisibility}}"
|
||||||
ToolTip="Add memes"/>
|
ToolTip="Add memes"/>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<ScrollViewer x:Name="ResultsScrollViewer"
|
<ScrollViewer x:Name="ResultsScrollViewer"
|
||||||
Grid.Row="3" VerticalScrollBarVisibility="Auto" Padding="0,0,4,0"
|
Grid.Row="3" VerticalScrollBarVisibility="Auto" Padding="0,0,4,0"
|
||||||
ScrollChanged="ResultsScrollViewer_OnScrollChanged">
|
ScrollChanged="ResultsScrollViewer_OnScrollChanged">
|
||||||
<Grid>
|
<StackPanel>
|
||||||
<TextBlock Text="{Binding EmptyStateText}"
|
<Grid>
|
||||||
Foreground="#7B7D85" FontSize="13"
|
<TextBlock Text="{Binding EmptyStateText}"
|
||||||
HorizontalAlignment="Center" VerticalAlignment="Center"
|
Foreground="#7B7D85" FontSize="13"
|
||||||
TextWrapping="Wrap" TextAlignment="Center" Width="260"
|
HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||||
Margin="0,40,0,0"
|
TextWrapping="Wrap" TextAlignment="Center" Width="260"
|
||||||
Visibility="{Binding ShowEmptyState, Converter={StaticResource BoolToVisibility}}"/>
|
Margin="0,40,0,0"
|
||||||
|
Visibility="{Binding ShowEmptyState, Converter={StaticResource BoolToVisibility}}"/>
|
||||||
|
|
||||||
<ItemsControl ItemsSource="{Binding Items}"
|
<ItemsControl ItemsSource="{Binding GridSource}">
|
||||||
ItemTemplate="{StaticResource MemeTileTemplate}">
|
<ItemsControl.ItemsPanel>
|
||||||
<ItemsControl.ItemsPanel>
|
<ItemsPanelTemplate>
|
||||||
<ItemsPanelTemplate>
|
<UniformGrid Columns="3"/>
|
||||||
<UniformGrid Columns="3"/>
|
</ItemsPanelTemplate>
|
||||||
</ItemsPanelTemplate>
|
</ItemsControl.ItemsPanel>
|
||||||
</ItemsControl.ItemsPanel>
|
</ItemsControl>
|
||||||
</ItemsControl>
|
</Grid>
|
||||||
</Grid>
|
</StackPanel>
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
|
|
||||||
<Grid Grid.Row="4" Margin="2,8,0,0">
|
<Grid Grid.Row="4" Margin="2,8,0,0">
|
||||||
|
|||||||
@@ -94,8 +94,8 @@ public partial class PickerWindow : Window
|
|||||||
// Switching tab/view or typing replaces the whole grid, so a retained scroll offset
|
// Switching tab/view or typing replaces the whole grid, so a retained scroll offset
|
||||||
// would leave the user part-way down a completely different set of results.
|
// would leave the user part-way down a completely different set of results.
|
||||||
if (e.PropertyName is nameof(PickerViewModel.ActiveSource)
|
if (e.PropertyName is nameof(PickerViewModel.ActiveSource)
|
||||||
or nameof(PickerViewModel.ShowingFavorites)
|
or nameof(PickerViewModel.SearchText)
|
||||||
or nameof(PickerViewModel.SearchText))
|
or nameof(PickerViewModel.IsInFavoritesView))
|
||||||
{
|
{
|
||||||
ResultsScrollViewer.ScrollToTop();
|
ResultsScrollViewer.ScrollToTop();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user