fix: small logic fixes and improvements
This commit is contained in:
@@ -1 +0,0 @@
|
|||||||
MainPackage=C:\Users\jnih\Downloads\gifBrowser\Olive\bin\x64\Release\net10.0-windows10.0.22621.0\win-x64\Olive_0.0.7.0_x64.msix
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
using Microsoft.CommandPalette.Extensions;
|
using Microsoft.CommandPalette.Extensions;
|
||||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||||
|
using Olive.Helpers;
|
||||||
using Olive.Klipy;
|
using Olive.Klipy;
|
||||||
using Olive.Services;
|
using Olive.Services;
|
||||||
|
|
||||||
@@ -7,30 +8,53 @@ namespace Olive.Commands;
|
|||||||
|
|
||||||
internal sealed partial class CopyGifCommand : InvokableCommand
|
internal sealed partial class CopyGifCommand : InvokableCommand
|
||||||
{
|
{
|
||||||
private readonly KlipyGif _gif;
|
private static readonly SemaphoreSlim CopyPasteGate = new(1, 1);
|
||||||
|
|
||||||
public CopyGifCommand(KlipyGif gif)
|
private readonly KlipyGif _gif;
|
||||||
|
private readonly SettingsManager _settingsManager;
|
||||||
|
|
||||||
|
public CopyGifCommand(KlipyGif gif, SettingsManager settingsManager)
|
||||||
{
|
{
|
||||||
_gif = gif;
|
_gif = gif;
|
||||||
|
_settingsManager = settingsManager;
|
||||||
Name = "Copy GIF";
|
Name = "Copy GIF";
|
||||||
Icon = new IconInfo("\uE8C8");
|
Icon = new IconInfo("\uE8C8");
|
||||||
}
|
}
|
||||||
|
|
||||||
public override ICommandResult Invoke()
|
public override ICommandResult Invoke()
|
||||||
{
|
{
|
||||||
_ = CopyAsync();
|
var closeAfterCopy = _settingsManager.CloseAfterCopy;
|
||||||
return CommandResult.Hide();
|
var pasteAfterCopy = _settingsManager.PasteAfterCopy;
|
||||||
|
|
||||||
|
_ = CopyAsync(closeAfterCopy, pasteAfterCopy);
|
||||||
|
return closeAfterCopy ? CommandResult.Dismiss() : CommandResult.KeepOpen();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task CopyAsync()
|
private async Task CopyAsync(bool closeAfterCopy, bool pasteAfterCopy)
|
||||||
{
|
{
|
||||||
using var cancellation = new CancellationTokenSource(TimeSpan.FromSeconds(30));
|
using var cancellation = new CancellationTokenSource(TimeSpan.FromSeconds(30));
|
||||||
|
if (!await CopyPasteGate.WaitAsync(0, cancellation.Token).ConfigureAwait(false))
|
||||||
|
{
|
||||||
|
ShowStatus("A GIF is already being copied.", MessageState.Warning, 2000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
var targetWindowTask = CapturePasteTargetAsync(closeAfterCopy, pasteAfterCopy, cancellation.Token);
|
||||||
|
|
||||||
ShowStatus("Downloading GIF...", MessageState.Info, 800);
|
ShowStatus("Downloading GIF...", MessageState.Info, 800);
|
||||||
var bytes = await KlipyClient.DownloadGifBytesAsync(_gif.GifUrl, cancellation.Token).ConfigureAwait(false);
|
var downloadTask = KlipyClient.DownloadGifBytesAsync(_gif.GifUrl, cancellation.Token);
|
||||||
|
|
||||||
|
var bytes = await downloadTask.ConfigureAwait(false);
|
||||||
|
var targetWindow = await targetWindowTask.ConfigureAwait(false);
|
||||||
await ClipboardService.CopyGifBytesAsync(bytes, cancellation.Token).ConfigureAwait(false);
|
await ClipboardService.CopyGifBytesAsync(bytes, cancellation.Token).ConfigureAwait(false);
|
||||||
ShowStatus("GIF copied - paste with Ctrl + V", MessageState.Success, 3500);
|
|
||||||
|
if (pasteAfterCopy)
|
||||||
|
await PasteShortcutService.SendPasteAfterRestoringFocusAsync(targetWindow, cancellation.Token)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
else
|
||||||
|
ShowStatus("GIF copied - paste with Ctrl + V", MessageState.Success, 3500);
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
@@ -41,6 +65,18 @@ internal sealed partial class CopyGifCommand : InvokableCommand
|
|||||||
{
|
{
|
||||||
ShowStatus("Could not copy this GIF.", MessageState.Error, 3500);
|
ShowStatus("Could not copy this GIF.", MessageState.Error, 3500);
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
CopyPasteGate.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<nint> CapturePasteTargetAsync(bool closeAfterCopy, bool pasteAfterCopy,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (!pasteAfterCopy) return 0;
|
||||||
|
if (closeAfterCopy) await Task.Delay(150, cancellationToken).ConfigureAwait(false);
|
||||||
|
return PasteShortcutService.GetForegroundWindowHandle();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ShowStatus(string message, MessageState state, int duration)
|
private static void ShowStatus(string message, MessageState state, int duration)
|
||||||
|
|||||||
@@ -11,11 +11,13 @@ internal sealed class SettingsManager : JsonSettingsManager
|
|||||||
FilePath = SettingsJsonPath();
|
FilePath = SettingsJsonPath();
|
||||||
Settings.Add(_klipyApiKey);
|
Settings.Add(_klipyApiKey);
|
||||||
Settings.Add(_resultCount);
|
Settings.Add(_resultCount);
|
||||||
|
Settings.Add(_closeAfterCopy);
|
||||||
|
Settings.Add(_pasteAfterCopy);
|
||||||
|
|
||||||
// Load settings from file upon initialization
|
// Load settings from file upon initialization
|
||||||
LoadSettings();
|
LoadSettings();
|
||||||
|
|
||||||
Settings.SettingsChanged += (_, _) => SaveSettings();
|
Settings.SettingsChanged += (_, _) => { SaveSettings(); };
|
||||||
}
|
}
|
||||||
|
|
||||||
public string SettingsPath => FilePath;
|
public string SettingsPath => FilePath;
|
||||||
@@ -31,19 +33,34 @@ internal sealed class SettingsManager : JsonSettingsManager
|
|||||||
ErrorMessage = "Klipy API key is required."
|
ErrorMessage = "Klipy API key is required."
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private const int MinResultCount = 30;
|
||||||
|
private const int MaxResultCount = 100;
|
||||||
|
|
||||||
private readonly IntegerSetting _resultCount = new(
|
private readonly IntegerSetting _resultCount = new(
|
||||||
Namespaced(nameof(ResultCount)),
|
Namespaced(nameof(ResultCount)),
|
||||||
"Result count",
|
"Result count",
|
||||||
"Number of GIF results to load for each search.",
|
"Number of GIF results to load for each search.",
|
||||||
50,
|
50,
|
||||||
20,
|
MinResultCount,
|
||||||
100)
|
MaxResultCount)
|
||||||
{
|
{
|
||||||
Placeholder = "Min 20, max 100",
|
Placeholder = $"Min {MinResultCount}, max {MaxResultCount}",
|
||||||
IsRequired = true,
|
IsRequired = true,
|
||||||
ErrorMessage = "Enter a number between 20 and 100."
|
ErrorMessage = $"Enter a number between {MinResultCount} and {MaxResultCount}."
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private readonly ToggleSetting _closeAfterCopy = new(
|
||||||
|
Namespaced(nameof(CloseAfterCopy)),
|
||||||
|
"Close after copy",
|
||||||
|
"Automatically close Olive after copying a GIF.",
|
||||||
|
true);
|
||||||
|
|
||||||
|
private readonly ToggleSetting _pasteAfterCopy = new(
|
||||||
|
Namespaced(nameof(PasteAfterCopy)),
|
||||||
|
"Paste after copy",
|
||||||
|
"After copying a GIF, send Ctrl+V.",
|
||||||
|
false);
|
||||||
|
|
||||||
public string KlipyApiKey
|
public string KlipyApiKey
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
@@ -64,6 +81,24 @@ internal sealed class SettingsManager : JsonSettingsManager
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool CloseAfterCopy
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
LoadSettings();
|
||||||
|
return _closeAfterCopy.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool PasteAfterCopy
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
LoadSettings();
|
||||||
|
return _pasteAfterCopy.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static string SettingsJsonPath()
|
private static string SettingsJsonPath()
|
||||||
{
|
{
|
||||||
var directory = Utilities.BaseSettingsPath("Olive");
|
var directory = Utilities.BaseSettingsPath("Olive");
|
||||||
|
|||||||
@@ -24,8 +24,22 @@ public sealed partial class OliveCommandsProvider : CommandProvider
|
|||||||
{
|
{
|
||||||
Title = "Olive GIF Picker",
|
Title = "Olive GIF Picker",
|
||||||
Subtitle = "Browse through lots of GIFs and find the one that suits you best!",
|
Subtitle = "Browse through lots of GIFs and find the one that suits you best!",
|
||||||
MoreCommands = [new CommandContextItem(_settingsManager.Settings.SettingsPage)],
|
MoreCommands =
|
||||||
|
[
|
||||||
|
new CommandContextItem(_settingsManager.Settings.SettingsPage)
|
||||||
|
{
|
||||||
|
Title = "Olive Settings",
|
||||||
|
Subtitle = "Configure the Klipy API key and result count.",
|
||||||
|
Icon = new IconInfo("\uE713")
|
||||||
|
}
|
||||||
|
],
|
||||||
Icon = Icon
|
Icon = Icon
|
||||||
|
},
|
||||||
|
new CommandItem(_settingsManager.Settings.SettingsPage)
|
||||||
|
{
|
||||||
|
Title = "Olive Settings",
|
||||||
|
Subtitle = "Configure the Klipy API key and result count.",
|
||||||
|
Icon = new IconInfo("\uE713")
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,11 +11,11 @@
|
|||||||
<Identity
|
<Identity
|
||||||
Name="Olive"
|
Name="Olive"
|
||||||
Publisher="CN=OlivePrivate"
|
Publisher="CN=OlivePrivate"
|
||||||
Version="0.0.42.0"/>
|
Version="0.0.60.0"/>
|
||||||
<Properties>
|
<Properties>
|
||||||
<DisplayName>Olive</DisplayName>
|
<DisplayName>Olive</DisplayName>
|
||||||
<PublisherDisplayName>Private</PublisherDisplayName>
|
<PublisherDisplayName>EndMove 'contact@endmove.eu'</PublisherDisplayName>
|
||||||
<Logo>Assets\StoreLogo.png</Logo>
|
<Logo>Assets\AppLogo150.scale-200.png</Logo>
|
||||||
</Properties>
|
</Properties>
|
||||||
|
|
||||||
<Dependencies>
|
<Dependencies>
|
||||||
|
|||||||
@@ -17,25 +17,27 @@ internal sealed class GifListItemFactory
|
|||||||
_settingsManager = settingsManager;
|
_settingsManager = settingsManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ListItem CreateGifItem(KlipyGif gif)
|
public ListItem CreateGifItem(KlipyGif gif)
|
||||||
{
|
{
|
||||||
return new ListItem(new CopyGifCommand(gif))
|
return new ListItem(new CopyGifCommand(gif, _settingsManager))
|
||||||
{
|
{
|
||||||
Title = string.Empty,
|
Title = string.Empty,
|
||||||
Subtitle = string.Empty,
|
Subtitle = string.Empty,
|
||||||
Icon = new IconInfo(gif.ThumbnailGifUrl.ToString())
|
Icon = new IconInfo(gif.ThumbnailGifUrl.ToString()),
|
||||||
|
MoreCommands = CommonMoreCommands()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public CommandItem InitialContent()
|
public CommandItem InitialContent()
|
||||||
{
|
{
|
||||||
return new CommandItem(new NoOpCommand())
|
return new CommandItem(_settingsManager.HasKlipyApiKey ? new NoOpCommand() : _settingsManager.Settings.SettingsPage)
|
||||||
{
|
{
|
||||||
Title = _settingsManager.HasKlipyApiKey ? "Search GIFs" : "Klipy API key missing",
|
Title = _settingsManager.HasKlipyApiKey ? "Search GIFs from Klipy.com" : "Klipy API key missing",
|
||||||
Subtitle = _settingsManager.HasKlipyApiKey
|
Subtitle = _settingsManager.HasKlipyApiKey
|
||||||
? "Type something like thanks, excited, cat, or confused."
|
? "Type something like thanks, excited, cat, or confused."
|
||||||
: "Open Olive settings and set the Klipy API key.",
|
: "Open Olive settings and set the Klipy API key.",
|
||||||
Icon = _icon
|
Icon = _icon,
|
||||||
|
MoreCommands = CommonMoreCommands()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,17 +47,19 @@ internal sealed class GifListItemFactory
|
|||||||
{
|
{
|
||||||
Title = "Loading GIFs...",
|
Title = "Loading GIFs...",
|
||||||
Subtitle = $"Searching Klipy for \"{search}\"",
|
Subtitle = $"Searching Klipy for \"{search}\"",
|
||||||
Icon = _icon
|
Icon = _icon,
|
||||||
|
MoreCommands = CommonMoreCommands()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public CommandItem MissingApiKeyContent()
|
public CommandItem MissingApiKeyContent()
|
||||||
{
|
{
|
||||||
return new CommandItem(new NoOpCommand())
|
return new CommandItem(_settingsManager.Settings.SettingsPage)
|
||||||
{
|
{
|
||||||
Title = "Klipy API key missing",
|
Title = "Klipy API key missing",
|
||||||
Subtitle = "Open Olive settings and paste your Klipy API key.",
|
Subtitle = "Open Olive settings and paste your Klipy API key.",
|
||||||
Icon = _icon
|
Icon = _icon,
|
||||||
|
MoreCommands = CommonMoreCommands()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,7 +69,8 @@ internal sealed class GifListItemFactory
|
|||||||
{
|
{
|
||||||
Title = "No GIF found",
|
Title = "No GIF found",
|
||||||
Subtitle = $"Try another search for \"{search}\".",
|
Subtitle = $"Try another search for \"{search}\".",
|
||||||
Icon = _icon
|
Icon = _icon,
|
||||||
|
MoreCommands = CommonMoreCommands()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,7 +80,33 @@ internal sealed class GifListItemFactory
|
|||||||
{
|
{
|
||||||
Title = "Search failed",
|
Title = "Search failed",
|
||||||
Subtitle = ex.Message,
|
Subtitle = ex.Message,
|
||||||
Icon = _icon
|
Icon = _icon,
|
||||||
|
MoreCommands = CommonMoreCommands()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private IContextItem[] CommonMoreCommands()
|
||||||
|
{
|
||||||
|
return
|
||||||
|
[
|
||||||
|
new CommandContextItem(_settingsManager.Settings.SettingsPage)
|
||||||
|
{
|
||||||
|
Title = "Olive Settings",
|
||||||
|
Subtitle = "Configure Olive.",
|
||||||
|
Icon = new IconInfo("\uE713")
|
||||||
|
},
|
||||||
|
LinkCommand("Visit Klipy", "Open klipy.com", "https://klipy.com", "\uE774"),
|
||||||
|
LinkCommand("Source code", "Open the Olive repository", "https://git.endmove.eu/EndMove/Olive", "\uE943")
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CommandContextItem LinkCommand(string title, string subtitle, string url, string icon)
|
||||||
|
{
|
||||||
|
return new CommandContextItem(new OpenUrlCommand(url) { Result = CommandResult.Dismiss() })
|
||||||
|
{
|
||||||
|
Title = title,
|
||||||
|
Subtitle = subtitle,
|
||||||
|
Icon = new IconInfo(icon)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ namespace Olive.Pages;
|
|||||||
internal sealed partial class GifPickerPage : IDynamicListPage
|
internal sealed partial class GifPickerPage : IDynamicListPage
|
||||||
{
|
{
|
||||||
private const int KlipyMaxPageSize = 50;
|
private const int KlipyMaxPageSize = 50;
|
||||||
private static readonly TimeSpan SearchDebounceDelay = TimeSpan.FromMilliseconds(500);
|
private static readonly TimeSpan SearchDebounceDelay = TimeSpan.FromMilliseconds(350);
|
||||||
|
|
||||||
private readonly Lock _stateLock = new();
|
private readonly Lock _stateLock = new();
|
||||||
private readonly SettingsManager _settingsManager;
|
private readonly SettingsManager _settingsManager;
|
||||||
@@ -63,7 +63,7 @@ internal sealed partial class GifPickerPage : IDynamicListPage
|
|||||||
|
|
||||||
public IFilters? Filters => null;
|
public IFilters? Filters => null;
|
||||||
|
|
||||||
public IGridProperties GridProperties { get; } = new GalleryGridLayout { ShowTitle = true, ShowSubtitle = false };
|
public IGridProperties GridProperties { get; } = new GalleryGridLayout { ShowTitle = false, ShowSubtitle = false };
|
||||||
|
|
||||||
public ICommandItem EmptyContent
|
public ICommandItem EmptyContent
|
||||||
{
|
{
|
||||||
@@ -181,7 +181,7 @@ internal sealed partial class GifPickerPage : IDynamicListPage
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ReplaceState(gifs.Select(GifListItemFactory.CreateGifItem).ToArray());
|
ReplaceState(gifs.Select(_itemFactory.CreateGifItem).ToArray());
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,119 @@
|
|||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace Olive.Services;
|
||||||
|
|
||||||
|
internal static class PasteShortcutService
|
||||||
|
{
|
||||||
|
private const ushort ControlKey = 0x11;
|
||||||
|
private const ushort VKey = 0x56;
|
||||||
|
private const uint KeyUp = 0x0002;
|
||||||
|
private const uint InputKeyboard = 1;
|
||||||
|
private const int RestoreWindow = 9;
|
||||||
|
private static readonly TimeSpan FocusSettleDelay = TimeSpan.FromMilliseconds(450);
|
||||||
|
|
||||||
|
public static nint GetForegroundWindowHandle()
|
||||||
|
{
|
||||||
|
return GetForegroundWindow();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task SendPasteAfterRestoringFocusAsync(nint targetWindow, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
RestoreTargetWindow(targetWindow);
|
||||||
|
await Task.Delay(FocusSettleDelay, cancellationToken).ConfigureAwait(false);
|
||||||
|
SendKeyChord(ControlKey, VKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RestoreTargetWindow(nint targetWindow)
|
||||||
|
{
|
||||||
|
if (targetWindow == 0 || !IsWindow(targetWindow)) return;
|
||||||
|
|
||||||
|
if (IsIconic(targetWindow)) ShowWindow(targetWindow, RestoreWindow);
|
||||||
|
SetForegroundWindow(targetWindow);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SendKeyChord(ushort modifierKey, ushort key)
|
||||||
|
{
|
||||||
|
var inputs = new[]
|
||||||
|
{
|
||||||
|
KeyDown(modifierKey),
|
||||||
|
KeyDown(key),
|
||||||
|
KeyUpInput(key),
|
||||||
|
KeyUpInput(modifierKey)
|
||||||
|
};
|
||||||
|
|
||||||
|
var sent = SendInput((uint)inputs.Length, inputs, Marshal.SizeOf<Input>());
|
||||||
|
if (sent != inputs.Length) throw new InvalidOperationException("Could not send Ctrl+V to the active app.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Input KeyDown(ushort key)
|
||||||
|
{
|
||||||
|
return new Input
|
||||||
|
{
|
||||||
|
Type = InputKeyboard,
|
||||||
|
Data = new InputUnion { Keyboard = new KeyboardInput { VirtualKey = key } }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Input KeyUpInput(ushort key)
|
||||||
|
{
|
||||||
|
return new Input
|
||||||
|
{
|
||||||
|
Type = InputKeyboard,
|
||||||
|
Data = new InputUnion { Keyboard = new KeyboardInput { VirtualKey = key, Flags = KeyUp } }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
[DllImport("user32.dll", SetLastError = true)]
|
||||||
|
private static extern uint SendInput(uint inputCount, Input[] inputs, int inputSize);
|
||||||
|
|
||||||
|
[DllImport("user32.dll")]
|
||||||
|
private static extern nint GetForegroundWindow();
|
||||||
|
|
||||||
|
[DllImport("user32.dll")]
|
||||||
|
private static extern bool IsWindow(nint windowHandle);
|
||||||
|
|
||||||
|
[DllImport("user32.dll")]
|
||||||
|
private static extern bool IsIconic(nint windowHandle);
|
||||||
|
|
||||||
|
[DllImport("user32.dll")]
|
||||||
|
private static extern bool ShowWindow(nint windowHandle, int commandShow);
|
||||||
|
|
||||||
|
[DllImport("user32.dll")]
|
||||||
|
private static extern bool SetForegroundWindow(nint windowHandle);
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
private struct Input
|
||||||
|
{
|
||||||
|
public uint Type;
|
||||||
|
public InputUnion Data;
|
||||||
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Explicit)]
|
||||||
|
private struct InputUnion
|
||||||
|
{
|
||||||
|
[FieldOffset(0)] public MouseInput Mouse;
|
||||||
|
|
||||||
|
[FieldOffset(0)] public KeyboardInput Keyboard;
|
||||||
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
private struct MouseInput
|
||||||
|
{
|
||||||
|
public int X;
|
||||||
|
public int Y;
|
||||||
|
public uint MouseData;
|
||||||
|
public uint Flags;
|
||||||
|
public uint Time;
|
||||||
|
public nint ExtraInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
private struct KeyboardInput
|
||||||
|
{
|
||||||
|
public ushort VirtualKey;
|
||||||
|
public ushort ScanCode;
|
||||||
|
public uint Flags;
|
||||||
|
public uint Time;
|
||||||
|
public nint ExtraInfo;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,120 +1,19 @@
|
|||||||
# Olive
|
# Olive
|
||||||
|
|
||||||
Olive
|
Olive is a private Microsoft PowerToys Command Palette extension. It searches animated GIFs with Klipy and allows you to quickly use the one that suits you best!
|
||||||
is
|
|
||||||
a
|
|
||||||
private
|
|
||||||
Microsoft
|
|
||||||
PowerToys
|
|
||||||
Command
|
|
||||||
Palette
|
|
||||||
extension.
|
|
||||||
It
|
|
||||||
searches
|
|
||||||
animated
|
|
||||||
GIFs
|
|
||||||
with
|
|
||||||
Klipy
|
|
||||||
and
|
|
||||||
allow
|
|
||||||
you
|
|
||||||
to
|
|
||||||
quickly
|
|
||||||
the
|
|
||||||
one
|
|
||||||
that
|
|
||||||
suits
|
|
||||||
you
|
|
||||||
best!
|
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
-
|
- Windows 10/11 with PowerToys and Command Palette enabled.
|
||||||
|
- .NET SDK 10.
|
||||||
Windows
|
- Windows SDK `10.0.22621.0` or a compatible newer SDK.
|
||||||
10/11
|
- A Klipy API key.
|
||||||
with
|
|
||||||
PowerToys
|
|
||||||
and
|
|
||||||
Command
|
|
||||||
Palette
|
|
||||||
enabled.
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
.NET
|
|
||||||
SDK
|
|
||||||
|
|
||||||
10.
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
Windows
|
|
||||||
SDK
|
|
||||||
`10.0.22621.0`
|
|
||||||
or
|
|
||||||
a
|
|
||||||
compatible
|
|
||||||
newer
|
|
||||||
SDK.
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
A
|
|
||||||
Klipy
|
|
||||||
API
|
|
||||||
key.
|
|
||||||
|
|
||||||
## Klipy API Key
|
## Klipy API Key
|
||||||
|
|
||||||
Olive
|
Olive stores the Klipy key locally in its Command Palette settings. Do not put the key in source code, Git, environment variables, or the package.
|
||||||
stores
|
|
||||||
the
|
|
||||||
Klipy
|
|
||||||
key
|
|
||||||
locally
|
|
||||||
in
|
|
||||||
its
|
|
||||||
Command
|
|
||||||
Palette
|
|
||||||
settings.
|
|
||||||
Do
|
|
||||||
not
|
|
||||||
put
|
|
||||||
the
|
|
||||||
key
|
|
||||||
in
|
|
||||||
source
|
|
||||||
code,
|
|
||||||
Git,
|
|
||||||
environment
|
|
||||||
variables,
|
|
||||||
or
|
|
||||||
the
|
|
||||||
package.
|
|
||||||
|
|
||||||
To
|
To configure it, open Command Palette, open the context commands for `Olive GIF Picker`, open Olive settings, paste the key into `Klipy API key`, then save.
|
||||||
configure
|
|
||||||
it,
|
|
||||||
open
|
|
||||||
Command
|
|
||||||
Palette,
|
|
||||||
open
|
|
||||||
the
|
|
||||||
context
|
|
||||||
commands
|
|
||||||
for
|
|
||||||
`Olive GIF Picker`,
|
|
||||||
open
|
|
||||||
Olive
|
|
||||||
settings,
|
|
||||||
paste
|
|
||||||
the
|
|
||||||
key
|
|
||||||
into
|
|
||||||
`Klipy API key`,
|
|
||||||
then
|
|
||||||
save.
|
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
|
|
||||||
@@ -128,251 +27,71 @@ dotnet build .\Olive.sln -c Debug -p:Platform=x64
|
|||||||
.\scripts\Build-OlivePackage.ps1
|
.\scripts\Build-OlivePackage.ps1
|
||||||
```
|
```
|
||||||
|
|
||||||
Or
|
Or double-click:
|
||||||
double-click:
|
|
||||||
|
|
||||||
```text
|
```text
|
||||||
scripts\Build-OlivePackage.cmd
|
scripts\Build-OlivePackage.cmd
|
||||||
```
|
```
|
||||||
|
|
||||||
This
|
This creates the shareable package in `dist\OlivePackage`.
|
||||||
creates
|
|
||||||
the
|
|
||||||
shareable
|
|
||||||
package
|
|
||||||
in
|
|
||||||
`dist\OlivePackage`.
|
|
||||||
|
|
||||||
The
|
The private signing file is exported to `dist\private\OlivePrivate.pfx`. Do not share it unless the recipient must be able to sign Olive builds.
|
||||||
private
|
|
||||||
signing
|
|
||||||
file
|
|
||||||
is
|
|
||||||
exported
|
|
||||||
to
|
|
||||||
`dist\private\OlivePrivate.pfx`.
|
|
||||||
Do
|
|
||||||
not
|
|
||||||
share
|
|
||||||
it
|
|
||||||
unless
|
|
||||||
the
|
|
||||||
recipient
|
|
||||||
must
|
|
||||||
be
|
|
||||||
able
|
|
||||||
to
|
|
||||||
sign
|
|
||||||
Olive
|
|
||||||
builds.
|
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
From
|
From `dist\OlivePackage`:
|
||||||
`dist\OlivePackage`:
|
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
.\Install-Olive.ps1
|
.\Install-Olive.ps1
|
||||||
```
|
```
|
||||||
|
|
||||||
Or
|
Or double-click:
|
||||||
double-click:
|
|
||||||
|
|
||||||
```text
|
```text
|
||||||
Install-Olive.cmd
|
Install-Olive.cmd
|
||||||
```
|
```
|
||||||
|
|
||||||
The
|
The script asks for administrator rights, imports `OlivePrivate.cer` into `Cert:\LocalMachine\Root`, then opens the `.msix` with Windows App Installer.
|
||||||
script
|
|
||||||
asks
|
|
||||||
for
|
|
||||||
administrator
|
|
||||||
rights,
|
|
||||||
imports
|
|
||||||
`OlivePrivate.cer`
|
|
||||||
into
|
|
||||||
`Cert:\LocalMachine\Root`,
|
|
||||||
then
|
|
||||||
opens
|
|
||||||
the
|
|
||||||
`.msix`
|
|
||||||
with
|
|
||||||
Windows
|
|
||||||
App
|
|
||||||
Installer.
|
|
||||||
|
|
||||||
Restart
|
Restart PowerToys after installation.
|
||||||
PowerToys
|
|
||||||
after
|
|
||||||
installation.
|
|
||||||
|
|
||||||
## Uninstall
|
## Uninstall
|
||||||
|
|
||||||
From
|
From `dist\OlivePackage` or `scripts\`:
|
||||||
`dist\OlivePackage`
|
|
||||||
or
|
|
||||||
`scripts\`:
|
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
.\Uninstall-Olive.ps1
|
.\Uninstall-Olive.ps1
|
||||||
```
|
```
|
||||||
|
|
||||||
Or
|
Or double-click:
|
||||||
double-click:
|
|
||||||
|
|
||||||
```text
|
```text
|
||||||
Uninstall-Olive.cmd
|
Uninstall-Olive.cmd
|
||||||
```
|
```
|
||||||
|
|
||||||
By
|
By default, uninstall removes the Olive package, the Olive certificate from `Cert:\LocalMachine\Root`, and Olive user data.
|
||||||
default,
|
|
||||||
uninstall
|
|
||||||
removes
|
|
||||||
the
|
|
||||||
Olive
|
|
||||||
package,
|
|
||||||
the
|
|
||||||
Olive
|
|
||||||
certificate
|
|
||||||
from
|
|
||||||
`Cert:\LocalMachine\Root`,
|
|
||||||
and
|
|
||||||
Olive
|
|
||||||
user
|
|
||||||
data.
|
|
||||||
|
|
||||||
Use
|
Use `-KeepCertificate` to keep the certificate.
|
||||||
`-KeepCertificate`
|
|
||||||
to
|
|
||||||
keep
|
|
||||||
the
|
|
||||||
certificate.
|
|
||||||
|
|
||||||
Use
|
Use `-KeepUserData` to keep Olive user data.
|
||||||
`-KeepUserData`
|
|
||||||
to
|
|
||||||
keep
|
|
||||||
Olive
|
|
||||||
user
|
|
||||||
data.
|
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
1.
|
1. Open PowerToys Command Palette.
|
||||||
|
2. Launch `Olive GIF Picker`.
|
||||||
Open
|
3. Search for a GIF.
|
||||||
PowerToys
|
4. Select a result with Enter.
|
||||||
Command
|
5. Paste with Ctrl+V in an app that accepts animated GIF files.
|
||||||
Palette.
|
|
||||||
|
|
||||||
2.
|
|
||||||
|
|
||||||
Launch
|
|
||||||
`Olive GIF Picker`.
|
|
||||||
|
|
||||||
3.
|
|
||||||
|
|
||||||
Search
|
|
||||||
for
|
|
||||||
a
|
|
||||||
GIF.
|
|
||||||
|
|
||||||
4.
|
|
||||||
|
|
||||||
Select
|
|
||||||
a
|
|
||||||
result
|
|
||||||
with
|
|
||||||
Enter.
|
|
||||||
|
|
||||||
5.
|
|
||||||
|
|
||||||
Paste
|
|
||||||
with
|
|
||||||
Ctrl+V
|
|
||||||
in
|
|
||||||
an
|
|
||||||
app
|
|
||||||
that
|
|
||||||
accepts
|
|
||||||
animated
|
|
||||||
GIF
|
|
||||||
files.
|
|
||||||
|
|
||||||
## Sharing
|
## Sharing
|
||||||
|
|
||||||
-
|
- Share only `dist\OlivePackage`.
|
||||||
|
- Do not share `dist\private\OlivePrivate.pfx`.
|
||||||
Share
|
- Do not include the Klipy API key in the package.
|
||||||
only
|
- Each user must set the Klipy key in Olive settings.
|
||||||
`dist\OlivePackage`.
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
Do
|
|
||||||
not
|
|
||||||
share
|
|
||||||
`dist\private\OlivePrivate.pfx`.
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
Do
|
|
||||||
not
|
|
||||||
include
|
|
||||||
the
|
|
||||||
Klipy
|
|
||||||
API
|
|
||||||
key
|
|
||||||
in
|
|
||||||
the
|
|
||||||
package.
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
Each
|
|
||||||
user
|
|
||||||
must
|
|
||||||
set
|
|
||||||
the
|
|
||||||
Klipy
|
|
||||||
key
|
|
||||||
in
|
|
||||||
Olive
|
|
||||||
settings.
|
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
-
|
- Olive copies GIFs as files to preserve animation.
|
||||||
|
- Some apps may reject file pasting or use fallback text only.
|
||||||
Olive
|
- GIFs are downloaded in memory when copied.
|
||||||
copies
|
|
||||||
GIFs
|
|
||||||
as
|
|
||||||
files
|
|
||||||
to
|
|
||||||
preserve
|
|
||||||
animation.
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
Some
|
|
||||||
apps
|
|
||||||
may
|
|
||||||
reject
|
|
||||||
file
|
|
||||||
pasting
|
|
||||||
or
|
|
||||||
use
|
|
||||||
fallback
|
|
||||||
text
|
|
||||||
only.
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
GIFs
|
|
||||||
are
|
|
||||||
downloaded
|
|
||||||
in
|
|
||||||
memory
|
|
||||||
when
|
|
||||||
copied.
|
|
||||||
|
|||||||
Reference in New Issue
Block a user