diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..cc24c14
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,14 @@
+root = true
+
+[*]
+charset = utf-8
+end_of_line = lf
+indent_size = 2
+indent_style = space
+insert_final_newline = true
+trim_trailing_whitespace = true
+max_line_length = 120
+
+[*.md]
+trim_trailing_whitespace = false
+max_line_length = 0
\ No newline at end of file
diff --git a/Directory.Packages.props b/Directory.Packages.props
index d06bf66..ffcaa87 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -3,16 +3,16 @@
true
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Olive/Commands/CopyGifCommand.cs b/Olive/Commands/CopyGifCommand.cs
index c98a1b0..bb30416 100644
--- a/Olive/Commands/CopyGifCommand.cs
+++ b/Olive/Commands/CopyGifCommand.cs
@@ -7,45 +7,44 @@ namespace Olive.Commands;
internal sealed partial class CopyGifCommand : InvokableCommand
{
- private readonly KlipyGif _gif;
- private readonly GifCache _cache;
+ private readonly KlipyGif _gif;
- public CopyGifCommand(KlipyGif gif, GifCache cache)
- {
- _gif = gif;
- _cache = cache;
- Name = "Copy GIF";
- Icon = new IconInfo("\uE8C8");
- }
+ public CopyGifCommand(KlipyGif gif)
+ {
+ _gif = gif;
+ Name = "Copy GIF";
+ Icon = new IconInfo("\uE8C8");
+ }
- public override ICommandResult Invoke()
- {
- _ = CopyAsync();
- return CommandResult.KeepOpen();
- }
+ public override ICommandResult Invoke()
+ {
+ _ = CopyAsync();
+ return CommandResult.Hide();
+ }
- private async Task CopyAsync()
+ private async Task CopyAsync()
+ {
+ using var cancellation = new CancellationTokenSource(TimeSpan.FromSeconds(30));
+ try
{
- using var cancellation = new CancellationTokenSource(TimeSpan.FromSeconds(30));
- try
- {
- ShowStatus("Downloading GIF...", MessageState.Info, 800);
- var path = await _cache.GetOrDownloadAsync(_gif, cancellation.Token).ConfigureAwait(false);
- await ClipboardService.CopyGifFileAsync(path, _gif.GifUrl, _gif.Title, cancellation.Token).ConfigureAwait(false);
- ShowStatus("GIF copied - paste with Ctrl + V", MessageState.Success, 3500);
- }
- catch (OperationCanceledException)
- {
- ShowStatus("Copy cancelled or timed out.", MessageState.Warning, 2500);
- }
- catch (Exception ex) when (ex is HttpRequestException or IOException or InvalidOperationException or UnauthorizedAccessException)
- {
- ShowStatus("Could not copy this GIF.", MessageState.Error, 3500);
- }
+ ShowStatus("Downloading GIF...", MessageState.Info, 800);
+ var bytes = await KlipyClient.DownloadGifBytesAsync(_gif.GifUrl, cancellation.Token).ConfigureAwait(false);
+ await ClipboardService.CopyGifBytesAsync(bytes, cancellation.Token).ConfigureAwait(false);
+ ShowStatus("GIF copied - paste with Ctrl + V", MessageState.Success, 3500);
}
+ catch (OperationCanceledException)
+ {
+ ShowStatus("Copy cancelled or timed out.", MessageState.Warning, 2500);
+ }
+ catch (Exception ex) when (ex is HttpRequestException or IOException or InvalidOperationException
+ or UnauthorizedAccessException)
+ {
+ ShowStatus("Could not copy this GIF.", MessageState.Error, 3500);
+ }
+ }
- private static void ShowStatus(string message, MessageState state, int duration)
- {
- new ToastStatusMessage(new StatusMessage { Message = message, State = state }) { Duration = duration }.Show();
- }
+ private static void ShowStatus(string message, MessageState state, int duration)
+ {
+ new ToastStatusMessage(new StatusMessage { Message = message, State = state }) { Duration = duration }.Show();
+ }
}
diff --git a/Olive/Helpers/IntegerSetting.cs b/Olive/Helpers/IntegerSetting.cs
new file mode 100644
index 0000000..165c013
--- /dev/null
+++ b/Olive/Helpers/IntegerSetting.cs
@@ -0,0 +1,67 @@
+using System.Globalization;
+using System.Text.Json.Nodes;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+
+namespace Olive.Helpers;
+
+internal sealed class IntegerSetting : Setting
+{
+ public IntegerSetting(string key, string label, string description, int defaultValue, int minimum, int maximum)
+ : base(key, label, description, defaultValue)
+ {
+ Minimum = minimum;
+ Maximum = maximum;
+ }
+
+ public int Minimum { get; }
+
+ public int Maximum { get; }
+
+ public string Placeholder { get; set; } = string.Empty;
+
+ public override Dictionary ToDictionary()
+ {
+ return new Dictionary
+ {
+ { "type", "Input.Number" },
+ { "title", Label },
+ { "id", Key },
+ { "label", Description },
+ { "value", Value },
+ { "placeholder", Placeholder },
+ { "min", Minimum },
+ { "max", Maximum },
+ { "isRequired", IsRequired },
+ { "errorMessage", ErrorMessage }
+ };
+ }
+
+ public override void Update(JsonObject payload)
+ {
+ if (payload[Key] is null) return;
+
+ int value;
+ try
+ {
+ value = payload[Key]!.GetValue();
+ }
+ catch (InvalidOperationException)
+ {
+ var text = payload[Key]?.GetValue() ?? string.Empty;
+ if (!int.TryParse(text, NumberStyles.Integer, CultureInfo.InvariantCulture, out value))
+ {
+ ErrorMessage =
+ $"Enter a number between {Minimum.ToString(CultureInfo.InvariantCulture)} and {Maximum.ToString(CultureInfo.InvariantCulture)}.";
+ return;
+ }
+ }
+
+ Value = Math.Clamp(value, Minimum, Maximum);
+ ErrorMessage = string.Empty;
+ }
+
+ public override string ToState()
+ {
+ return $"\"{Key}\": {Value}";
+ }
+}
diff --git a/Olive/Helpers/SettingsManager.cs b/Olive/Helpers/SettingsManager.cs
index 495cdf3..cc4ecf0 100644
--- a/Olive/Helpers/SettingsManager.cs
+++ b/Olive/Helpers/SettingsManager.cs
@@ -2,51 +2,78 @@ using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Olive.Helpers;
-internal sealed partial class SettingsManager : JsonSettingsManager
+internal sealed class SettingsManager : JsonSettingsManager
{
- private const string Namespace = "Olive";
+ private const string Namespace = "Olive";
- private readonly TextSetting _klipyApiKey = new(
- Namespaced(nameof(KlipyApiKey)),
- "Klipy API key",
- "Private key used to call the Klipy API. It stays stored locally in Olive settings.",
- string.Empty)
+ public SettingsManager()
+ {
+ FilePath = SettingsJsonPath();
+ Settings.Add(_klipyApiKey);
+ Settings.Add(_resultCount);
+
+ // Load settings from file upon initialization
+ LoadSettings();
+
+ Settings.SettingsChanged += (_, _) => SaveSettings();
+ }
+
+ public string SettingsPath => FilePath;
+
+ private readonly TextSetting _klipyApiKey = new(
+ Namespaced(nameof(KlipyApiKey)),
+ "Klipy API key",
+ "Private key used to call the Klipy API. It stays stored locally in Olive settings.",
+ string.Empty)
+ {
+ Placeholder = "Required",
+ IsRequired = true,
+ ErrorMessage = "Klipy API key is required."
+ };
+
+ private readonly IntegerSetting _resultCount = new(
+ Namespaced(nameof(ResultCount)),
+ "Result count",
+ "Number of GIF results to load for each search.",
+ 50,
+ 20,
+ 100)
+ {
+ Placeholder = "Min 20, max 100",
+ IsRequired = true,
+ ErrorMessage = "Enter a number between 20 and 100."
+ };
+
+ public string KlipyApiKey
+ {
+ get
{
- Placeholder = "Paste your Klipy API key here",
- };
-
- private static string Namespaced(string propertyName) => $"{Namespace}.{propertyName}";
-
- public string KlipyApiKey
- {
- get
- {
- LoadSettings();
- return _klipyApiKey.Value ?? string.Empty;
- }
+ LoadSettings();
+ return _klipyApiKey.Value ?? string.Empty;
}
+ }
- public bool HasKlipyApiKey => !string.IsNullOrWhiteSpace(KlipyApiKey);
+ public bool HasKlipyApiKey => !string.IsNullOrWhiteSpace(KlipyApiKey);
- public string SettingsPath => FilePath;
-
- internal static string SettingsJsonPath()
+ public int ResultCount
+ {
+ get
{
- var directory = Utilities.BaseSettingsPath("Olive");
- Directory.CreateDirectory(directory);
-
- return Path.Combine(directory, "settings.json");
+ LoadSettings();
+ return Math.Clamp(_resultCount.Value, 30, 100);
}
+ }
- public SettingsManager()
- {
- FilePath = SettingsJsonPath();
+ private static string SettingsJsonPath()
+ {
+ var directory = Utilities.BaseSettingsPath("Olive");
+ Directory.CreateDirectory(directory);
- Settings.Add(_klipyApiKey);
+ return Path.Combine(directory, "settings.json");
+ }
- // Load settings from file upon initialization
- LoadSettings();
-
- Settings.SettingsChanged += (_, _) => SaveSettings();
- }
+ private static string Namespaced(string propertyName)
+ {
+ return $"{Namespace}.{propertyName}";
+ }
}
diff --git a/Olive/Klipy/KlipyClient.cs b/Olive/Klipy/KlipyClient.cs
index f3a764c..c9d9b8a 100644
--- a/Olive/Klipy/KlipyClient.cs
+++ b/Olive/Klipy/KlipyClient.cs
@@ -6,98 +6,112 @@ namespace Olive.Klipy;
internal sealed class KlipyClient
{
- private const string BaseUrl = "https://api.klipy.com";
- private static readonly HttpClient HttpClient = new() { Timeout = TimeSpan.FromSeconds(12) };
+ private const string BaseUrl = "https://api.klipy.com";
+ private static readonly HttpClient HttpClient = new() { Timeout = TimeSpan.FromSeconds(12) };
- public static async Task SearchAsync(string apiKey, string query, int page, int perPage, CancellationToken cancellationToken)
+ ///
+ /// Searches Klipy GIFs with the raw query parameters used by Olive and returns converted GIF items plus pagination state.
+ ///
+ /// Klipy application key used in the API route.
+ /// Search text entered by the user.
+ /// One-based Klipy page number to request.
+ /// Number of items to request from Klipy for this page.
+ /// Token used to cancel the HTTP request and JSON parsing.
+ /// Converted GIF results and whether Klipy reports another page.
+ public static async Task SearchAsync(string apiKey, string query, int page, int perPage,
+ CancellationToken cancellationToken)
+ {
+ if (string.IsNullOrWhiteSpace(apiKey))
+ throw new InvalidOperationException("The Klipy API key is missing. Set it in Olive settings.");
+
+ var requestUri = BuildSearchUri(apiKey, query, page, perPage);
+
+ using var response = await HttpClient
+ .GetAsync(requestUri, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
+ if (response.StatusCode == HttpStatusCode.NoContent) return new KlipySearchResult([], false);
+
+ if (!response.IsSuccessStatusCode)
+ throw new HttpRequestException($"Klipy returned {(int)response.StatusCode} {response.ReasonPhrase}.", null,
+ response.StatusCode);
+
+ var payload = await response.Content
+ .ReadFromJsonAsync(KlipyJsonContext.Default.KlipySearchResponse, cancellationToken).ConfigureAwait(false);
+ if (payload is null || !payload.Result) throw new HttpRequestException("Klipy did not return a usable result.");
+
+ var rawItems = payload.Data?.Items ?? [];
+ var hasMore = payload.Data?.HasNext ?? rawItems.Length >= perPage;
+ return new KlipySearchResult(ConvertResults(rawItems), hasMore);
+ }
+
+ ///
+ /// Downloads the selected animated GIF file into memory for clipboard use.
+ ///
+ /// Absolute URL of the GIF file to download.
+ /// Token used to cancel the download.
+ /// Raw GIF file bytes.
+ public static async Task DownloadGifBytesAsync(Uri gifUrl, CancellationToken cancellationToken)
+ {
+ using var response = await HttpClient.GetAsync(gifUrl, HttpCompletionOption.ResponseHeadersRead, cancellationToken)
+ .ConfigureAwait(false);
+ if (!response.IsSuccessStatusCode)
+ throw new HttpRequestException($"Download failed ({(int)response.StatusCode}).", null, response.StatusCode);
+
+ return await response.Content.ReadAsByteArrayAsync(cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ /// Builds the Klipy search endpoint URL with only page, per_page, and q query parameters.
+ ///
+ /// Klipy application key used in the API route.
+ /// Search text to pass as the q query parameter.
+ /// One-based Klipy page number to request.
+ /// Number of items to request from Klipy for this page.
+ /// Fully escaped Klipy search URI.
+ private static Uri BuildSearchUri(string apiKey, string query, int page, int perPage)
+ {
+ var endpoint = $"{BaseUrl}/api/v1/{Uri.EscapeDataString(apiKey)}/gifs/search";
+ var parameters = new Dictionary
{
- if (string.IsNullOrWhiteSpace(apiKey))
- {
- throw new InvalidOperationException("The Klipy API key is missing. Set it in Olive settings.");
- }
+ ["page"] = page.ToString(CultureInfo.InvariantCulture),
+ ["per_page"] = perPage.ToString(CultureInfo.InvariantCulture),
+ ["q"] = query
+ };
- var requestUri = BuildSearchUri(apiKey, query, page, perPage);
+ var queryString = string.Join("&", parameters.Select(pair => string.Create(
+ CultureInfo.InvariantCulture,
+ $"{Uri.EscapeDataString(pair.Key)}={Uri.EscapeDataString(pair.Value)}")));
- using var response = await HttpClient.GetAsync(requestUri, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
- if (response.StatusCode == HttpStatusCode.NoContent)
- {
- return new KlipySearchResult([], HasMore: false);
- }
+ return new Uri($"{endpoint}?{queryString}");
+ }
- if (!response.IsSuccessStatusCode)
- {
- throw new HttpRequestException($"Klipy returned {(int)response.StatusCode} {response.ReasonPhrase}.", null, response.StatusCode);
- }
+ ///
+ /// Converts raw Klipy response items into Olive GIF models and skips entries without usable GIF URLs.
+ ///
+ /// Raw item array from the Klipy search response.
+ /// GIF models that have an ID, thumbnail URL, and full GIF URL.
+ private static List ConvertResults(KlipyItem[]? items)
+ {
+ if (items is null || items.Length == 0) return [];
- var payload = await response.Content.ReadFromJsonAsync(KlipyJsonContext.Default.KlipySearchResponse, cancellationToken).ConfigureAwait(false);
- if (payload is null || !payload.Result)
- {
- throw new HttpRequestException("Klipy did not return a usable result.");
- }
+ var gifs = new List(items.Length);
+ foreach (var item in items)
+ {
+ var id = !string.IsNullOrWhiteSpace(item.Slug) ? item.Slug : item.Id?.ToString(CultureInfo.InvariantCulture);
+ var thumbnailUrl = item.File?.Sm?.Gif?.Url ?? item.File?.Md?.Gif?.Url ?? item.File?.Hd?.Gif?.Url;
+ var fullUrl = item.File?.Hd?.Gif?.Url ?? item.File?.Md?.Gif?.Url ?? item.File?.Sm?.Gif?.Url;
- var rawItems = payload.Data?.Items ?? [];
- var hasMore = payload.Data?.HasNext ?? rawItems.Length >= perPage;
- return new KlipySearchResult(ConvertResults(rawItems), hasMore);
+ if (string.IsNullOrWhiteSpace(id)
+ || !Uri.TryCreate(thumbnailUrl, UriKind.Absolute, out var thumbnailGifUrl)
+ || !Uri.TryCreate(fullUrl, UriKind.Absolute, out var gifUrl))
+ continue;
+
+ var title = string.IsNullOrWhiteSpace(item.Title)
+ ? "GIF Klipy"
+ : WebUtility.HtmlDecode(item.Title).Trim();
+
+ gifs.Add(new KlipyGif(id, title, thumbnailGifUrl, gifUrl));
}
- public static async Task DownloadGifAsync(Uri gifUrl, string destinationPath, CancellationToken cancellationToken)
- {
- using var response = await HttpClient.GetAsync(gifUrl, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
- if (!response.IsSuccessStatusCode)
- {
- throw new HttpRequestException($"Download failed ({(int)response.StatusCode}).", null, response.StatusCode);
- }
-
- await using var input = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- await using var output = File.Create(destinationPath);
- await input.CopyToAsync(output, cancellationToken).ConfigureAwait(false);
- }
-
- private static Uri BuildSearchUri(string apiKey, string query, int page, int perPage)
- {
- var endpoint = $"{BaseUrl}/api/v1/{Uri.EscapeDataString(apiKey)}/gifs/search";
- var parameters = new Dictionary
- {
- ["page"] = page.ToString(CultureInfo.InvariantCulture),
- ["per_page"] = perPage.ToString(CultureInfo.InvariantCulture),
- ["q"] = query,
- };
-
- var queryString = string.Join("&", parameters.Select(pair => string.Create(
- CultureInfo.InvariantCulture,
- $"{Uri.EscapeDataString(pair.Key)}={Uri.EscapeDataString(pair.Value)}")));
-
- return new Uri($"{endpoint}?{queryString}");
- }
-
- private static List ConvertResults(KlipyItem[]? items)
- {
- if (items is null || items.Length == 0)
- {
- return [];
- }
-
- var gifs = new List(items.Length);
- foreach (var item in items)
- {
- var id = !string.IsNullOrWhiteSpace(item.Slug) ? item.Slug : item.Id?.ToString(CultureInfo.InvariantCulture);
- var thumbnailUrl = item.File?.Sm?.Gif?.Url ?? item.File?.Md?.Gif?.Url ?? item.File?.Hd?.Gif?.Url;
- var fullUrl = item.File?.Hd?.Gif?.Url ?? item.File?.Md?.Gif?.Url ?? item.File?.Sm?.Gif?.Url;
-
- if (string.IsNullOrWhiteSpace(id)
- || !Uri.TryCreate(thumbnailUrl, UriKind.Absolute, out var thumbnailGifUrl)
- || !Uri.TryCreate(fullUrl, UriKind.Absolute, out var gifUrl))
- {
- continue;
- }
-
- var title = string.IsNullOrWhiteSpace(item.Title)
- ? "GIF Klipy"
- : WebUtility.HtmlDecode(item.Title).Trim();
-
- gifs.Add(new KlipyGif(id, title, thumbnailGifUrl, gifUrl));
- }
-
- return gifs;
- }
+ return gifs;
+ }
}
diff --git a/Olive/Klipy/KlipyModels.cs b/Olive/Klipy/KlipyModels.cs
index c93ff04..114cae1 100644
--- a/Olive/Klipy/KlipyModels.cs
+++ b/Olive/Klipy/KlipyModels.cs
@@ -3,39 +3,40 @@ using System.Text.Json.Serialization;
namespace Olive.Klipy;
internal sealed record KlipyGif(
- string Id,
- string Title,
- Uri ThumbnailGifUrl,
- Uri GifUrl);
+ string Id,
+ string Title,
+ Uri ThumbnailGifUrl,
+ Uri GifUrl);
internal sealed record KlipySearchResult(
- IReadOnlyList Gifs,
- bool HasMore);
+ IReadOnlyList Gifs,
+ bool HasMore);
internal sealed record KlipySearchResponse(
- [property: JsonPropertyName("result")] bool Result,
- [property: JsonPropertyName("data")] KlipySearchData? Data);
+ [property: JsonPropertyName("result")] bool Result,
+ [property: JsonPropertyName("data")] KlipySearchData? Data);
internal sealed record KlipySearchData(
- [property: JsonPropertyName("data")] KlipyItem[]? Items,
- [property: JsonPropertyName("has_next")] bool? HasNext);
+ [property: JsonPropertyName("data")] KlipyItem[]? Items,
+ [property: JsonPropertyName("has_next")]
+ bool? HasNext);
internal sealed record KlipyItem(
- [property: JsonPropertyName("id")] long? Id,
- [property: JsonPropertyName("slug")] string? Slug,
- [property: JsonPropertyName("title")] string? Title,
- [property: JsonPropertyName("file")] KlipyFile? File);
+ [property: JsonPropertyName("id")] long? Id,
+ [property: JsonPropertyName("slug")] string? Slug,
+ [property: JsonPropertyName("title")] string? Title,
+ [property: JsonPropertyName("file")] KlipyFile? File);
internal sealed record KlipyFile(
- [property: JsonPropertyName("hd")] KlipyFileSize? Hd,
- [property: JsonPropertyName("md")] KlipyFileSize? Md,
- [property: JsonPropertyName("sm")] KlipyFileSize? Sm);
+ [property: JsonPropertyName("hd")] KlipyFileSize? Hd,
+ [property: JsonPropertyName("md")] KlipyFileSize? Md,
+ [property: JsonPropertyName("sm")] KlipyFileSize? Sm);
internal sealed record KlipyFileSize(
- [property: JsonPropertyName("gif")] KlipyMediaFormat? Gif);
+ [property: JsonPropertyName("gif")] KlipyMediaFormat? Gif);
internal sealed record KlipyMediaFormat(
- [property: JsonPropertyName("url")] string? Url);
+ [property: JsonPropertyName("url")] string? Url);
[JsonSerializable(typeof(KlipySearchResponse))]
internal sealed partial class KlipyJsonContext : JsonSerializerContext;
diff --git a/Olive/Olive.csproj b/Olive/Olive.csproj
index 4afa0d7..7ff7906 100644
--- a/Olive/Olive.csproj
+++ b/Olive/Olive.csproj
@@ -18,16 +18,16 @@
-
-
-
-
-
-
+
+
+
+
+
+
-
+
-
+
-
-
-
+
+
+
@@ -74,11 +74,11 @@
-
false
-
+
true
true
true
diff --git a/Olive/OliveCommandsProvider.cs b/Olive/OliveCommandsProvider.cs
index 9c2efc4..19c7d09 100644
--- a/Olive/OliveCommandsProvider.cs
+++ b/Olive/OliveCommandsProvider.cs
@@ -7,25 +7,26 @@ namespace Olive;
public sealed partial class OliveCommandsProvider : CommandProvider
{
- private readonly SettingsManager _settingsManager = new();
+ private readonly SettingsManager _settingsManager = new();
- public OliveCommandsProvider()
- {
- DisplayName = "Olive";
- Icon = IconHelpers.FromRelativePath("Assets\\StoreLogo.png");
- Settings = _settingsManager.Settings;
- }
+ public OliveCommandsProvider()
+ {
+ DisplayName = "Olive";
+ Icon = IconHelpers.FromRelativePath("Assets\\StoreLogo.png");
+ Settings = _settingsManager.Settings;
+ }
- public override ICommandItem[] TopLevelCommands()
- {
- return [
- new CommandItem(new GifPickerPage(_settingsManager))
- {
- Title = "Olive GIF Picker",
- Subtitle = "Browse through lots of GIFs and find the one that suits you best!",
- MoreCommands = [new CommandContextItem(_settingsManager.Settings.SettingsPage)],
- Icon = Icon,
- },
- ];
- }
+ public override ICommandItem[] TopLevelCommands()
+ {
+ return
+ [
+ new CommandItem(new GifPickerPage(_settingsManager))
+ {
+ Title = "Olive GIF Picker",
+ Subtitle = "Browse through lots of GIFs and find the one that suits you best!",
+ MoreCommands = [new CommandContextItem(_settingsManager.Settings.SettingsPage)],
+ Icon = Icon
+ }
+ ];
+ }
}
diff --git a/Olive/OliveExtension.cs b/Olive/OliveExtension.cs
index d0cda7a..316a61c 100644
--- a/Olive/OliveExtension.cs
+++ b/Olive/OliveExtension.cs
@@ -6,23 +6,26 @@ namespace Olive;
[Guid("C7B0BF27-81E6-4A25-B6F7-F11AB1F61C0A")]
public sealed partial class OliveExtension : IExtension, IDisposable
{
- private readonly ManualResetEvent _extensionDisposedEvent;
+ private readonly ManualResetEvent _extensionDisposedEvent;
- private readonly OliveCommandsProvider _provider = new();
+ private readonly OliveCommandsProvider _provider = new();
- public OliveExtension(ManualResetEvent extensionDisposedEvent)
+ public OliveExtension(ManualResetEvent extensionDisposedEvent)
+ {
+ _extensionDisposedEvent = extensionDisposedEvent;
+ }
+
+ public object? GetProvider(ProviderType providerType)
+ {
+ return providerType switch
{
- _extensionDisposedEvent = extensionDisposedEvent;
- }
+ ProviderType.Commands => _provider,
+ _ => null
+ };
+ }
- public object? GetProvider(ProviderType providerType)
- {
- return providerType switch
- {
- ProviderType.Commands => _provider,
- _ => null
- };
- }
-
- public void Dispose() => _extensionDisposedEvent.Set();
+ public void Dispose()
+ {
+ _extensionDisposedEvent.Set();
+ }
}
diff --git a/Olive/Package.appxmanifest b/Olive/Package.appxmanifest
index 48db482..418058f 100644
--- a/Olive/Package.appxmanifest
+++ b/Olive/Package.appxmanifest
@@ -11,7 +11,7 @@
+ Version="0.0.42.0"/>
Olive
Private
@@ -19,8 +19,8 @@
-
-
+
+
@@ -29,35 +29,35 @@
+ Executable="$targetnametoken$.exe"
+ EntryPoint="$targetentrypoint$">
-
-
+
+
-
+
+ Id="Olive"
+ PublicFolder="Public"
+ DisplayName="Olive"
+ Description="Search and copy animated GIFs from Klipy. Powered by EndMove">
-
+
@@ -71,7 +71,7 @@
-
-
+
+
diff --git a/Olive/Pages/GifListItemFactory.cs b/Olive/Pages/GifListItemFactory.cs
new file mode 100644
index 0000000..0f7b4bd
--- /dev/null
+++ b/Olive/Pages/GifListItemFactory.cs
@@ -0,0 +1,81 @@
+using Microsoft.CommandPalette.Extensions;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+using Olive.Commands;
+using Olive.Helpers;
+using Olive.Klipy;
+
+namespace Olive.Pages;
+
+internal sealed class GifListItemFactory
+{
+ private readonly IIconInfo _icon;
+ private readonly SettingsManager _settingsManager;
+
+ public GifListItemFactory(IIconInfo icon, SettingsManager settingsManager)
+ {
+ _icon = icon;
+ _settingsManager = settingsManager;
+ }
+
+ public static ListItem CreateGifItem(KlipyGif gif)
+ {
+ return new ListItem(new CopyGifCommand(gif))
+ {
+ Title = string.Empty,
+ Subtitle = string.Empty,
+ Icon = new IconInfo(gif.ThumbnailGifUrl.ToString())
+ };
+ }
+
+ public CommandItem InitialContent()
+ {
+ return new CommandItem(new NoOpCommand())
+ {
+ Title = _settingsManager.HasKlipyApiKey ? "Search GIFs" : "Klipy API key missing",
+ Subtitle = _settingsManager.HasKlipyApiKey
+ ? "Type something like thanks, excited, cat, or confused."
+ : "Open Olive settings and set the Klipy API key.",
+ Icon = _icon
+ };
+ }
+
+ public CommandItem LoadingContent(string search)
+ {
+ return new CommandItem(new NoOpCommand())
+ {
+ Title = "Loading GIFs...",
+ Subtitle = $"Searching Klipy for \"{search}\"",
+ Icon = _icon
+ };
+ }
+
+ public CommandItem MissingApiKeyContent()
+ {
+ return new CommandItem(new NoOpCommand())
+ {
+ Title = "Klipy API key missing",
+ Subtitle = "Open Olive settings and paste your Klipy API key.",
+ Icon = _icon
+ };
+ }
+
+ public CommandItem NoResultsContent(string search)
+ {
+ return new CommandItem(new NoOpCommand())
+ {
+ Title = "No GIF found",
+ Subtitle = $"Try another search for \"{search}\".",
+ Icon = _icon
+ };
+ }
+
+ public CommandItem SearchErrorContent(Exception ex)
+ {
+ return new CommandItem(new NoOpCommand())
+ {
+ Title = "Search failed",
+ Subtitle = ex.Message,
+ Icon = _icon
+ };
+ }
+}
diff --git a/Olive/Pages/GifPickerPage.cs b/Olive/Pages/GifPickerPage.cs
index 4b2ab07..ca5c11e 100644
--- a/Olive/Pages/GifPickerPage.cs
+++ b/Olive/Pages/GifPickerPage.cs
@@ -1,555 +1,278 @@
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
-using Olive.Commands;
using Olive.Helpers;
using Olive.Klipy;
-using Olive.Services;
using Windows.Foundation;
-using Windows.System;
namespace Olive.Pages;
internal sealed partial class GifPickerPage : IDynamicListPage
{
- private const int PageSize = 30;
- private const int InitialBatchPages = 1;
- private const int LoadMoreBatchPages = 1;
- private static readonly TimeSpan SearchDebounceDelay = TimeSpan.FromMilliseconds(500);
+ private const int KlipyMaxPageSize = 50;
+ private static readonly TimeSpan SearchDebounceDelay = TimeSpan.FromMilliseconds(500);
- private readonly Lock _itemsLock = new();
- private readonly GifCache _cache = new();
- private readonly SettingsManager _settingsManager;
- private readonly List _items = [];
- private readonly IIconInfo _icon = IconHelpers.FromRelativePath("Assets\\StoreLogo.png");
- private readonly IGridProperties _gridProperties = new GalleryGridLayout { ShowTitle = true, ShowSubtitle = false };
- private CancellationTokenSource? _searchCancellation;
- private string _searchText = string.Empty;
- private string _activeSearch = string.Empty;
- private string _title = "Olive";
- private int _searchVersion;
- private int _loadRequestId;
- private int _nextPage = 1;
- private int _loadedItemCount;
- private bool _hasMoreItems;
- private bool _isLoading;
- private bool _isLoadingMore;
- private ICommandItem _emptyContent;
+ private readonly Lock _stateLock = new();
+ private readonly SettingsManager _settingsManager;
+ private readonly GifListItemFactory _itemFactory;
+ private readonly GifPickerPageState _state;
+ private CancellationTokenSource? _searchCancellation;
+ private string _searchText = string.Empty;
+ private string _activeSearch = string.Empty;
+ private int _searchVersion;
+ private ICommandItem _emptyContent;
- public GifPickerPage(SettingsManager settingsManager)
+ public GifPickerPage(SettingsManager settingsManager)
+ {
+ _settingsManager = settingsManager;
+ _itemFactory = new GifListItemFactory(Icon, _settingsManager);
+ _state = new GifPickerPageState();
+ _emptyContent = _itemFactory.InitialContent();
+ }
+
+ public event TypedEventHandler