Added Giphy support, Favourites

This commit is contained in:
Ebbe Baß
2026-08-14 13:55:34 +02:00
parent 747cd2a8aa
commit 4c755919d4
24 changed files with 1087 additions and 160 deletions
+91 -68
View File
@@ -32,9 +32,18 @@ Everything below is **implemented and verified working by actually running the a
- Multi-format clipboard write (see "Clipboard formats" below)
- Three insert modes, chosen in Settings: **Copy only / Paste into active window /
Paste and send instantly**
- Settings window: hotkey capture (incl. Windows key), insert mode radio buttons
- Consistent dark styling across all menus (tray + tile right-click)
- Persistence: memes and settings survive restarts
- Settings window: hotkey capture (incl. Windows key), insert mode radio buttons,
**autostart toggle**, **Giphy API key field**
- **Source tabs in the picker: Local / Giphy**, with debounced Giphy search, remote
thumbnails, download-on-select caching, and the required "Powered by GIPHY" badge
- **Windows autostart** via the HKCU Run key, toggled in Settings (verified: checkbox and
registry track each other in both directions)
- **Per-source favourites**: right-click any tile → "Add to favourites", shown in a
★ Favourites section at the top of that tab, with a star overlay on favourited tiles.
Scoped per source (Local favourites don't appear on the Giphy tab) and persisted.
- App title shown top-left of the picker; the title row doubles as the drag surface
- Consistent dark styling across all menus (tray + tile right-click) **and scrollbars**
- Persistence: memes, favourites and settings survive restarts
**Build is clean** (0 warnings, 0 errors). Working tree was clean at last check;
`publish/` is gitignored.
@@ -43,10 +52,18 @@ Everything below is **implemented and verified working by actually running the a
```
%AppData%\EbbesMemeClipboard\
settings.json InsertMode, HotkeyModifiers, HotkeyKey
settings.json InsertMode, HotkeyModifiers, HotkeyKey, GiphyApiKey
favorites.json list of FavoriteRecord (Source, Key, Title, Preview/FullUrl, DateAdded)
Library\
index.json list of LocalMemeRecord (Id, FileName, OriginalFileName, DateAdded)
<guid>.png/.jpg/.gif imported files, stored under a GUID name
%LocalAppData%\EbbesMemeClipboard\
GifCache\ downloaded Giphy GIFs (re-downloadable cache, so Local not Roaming)
HKCU\Software\Microsoft\Windows\CurrentVersion\Run\EbbesMemeClipboard
autostart entry; the registry is the single source of truth for
this (deliberately NOT mirrored into settings.json, to avoid drift)
```
---
@@ -78,18 +95,26 @@ faster.
```
src/EbbesMemeClipboard/
App.xaml / App.xaml.cs composition root: DI container, tray icon, hotkey registration
Styles/DarkMenuStyles.xaml implicit ContextMenu/MenuItem/Separator styles (app-wide)
Styles/DarkMenuStyles.xaml implicit ContextMenu/MenuItem/Separator styles (app-wide)
Styles/DarkScrollBarStyles.xaml implicit slim dark ScrollBar style (app-wide)
Models/
LocalMemeRecord.cs Id, FileName, OriginalFileName, DateAdded
AppSettings.cs InsertMode, HotkeyModifiers, HotkeyKey
AppSettings.cs InsertMode, HotkeyModifiers, HotkeyKey, GiphyApiKey
InsertMode.cs CopyOnly | PasteIntoActiveWindow | PasteAndSend
MemeSource.cs Local | Giphy
GifSearchResult.cs provider-agnostic remote hit
FavoriteRecord.cs per-source favourite (remote ones carry their own URLs)
Services/
ILocalMemeLibraryService / LocalMemeLibraryService import/search/remove, index.json
IClipboardService / ClipboardService multi-format clipboard write
IGlobalHotkeyService / GlobalHotkeyService RegisterHotKey + message-only HwndSource
ISettingsService / SettingsService System.Text.Json persistence
IAutoPasteService / AutoPasteService foreground restore + SendInput paste
ImageDecoding.cs first-frame decode helper
IAutostartService / AutostartService HKCU Run key
IGifProvider / GiphyGifProvider remote search behind a provider interface
IGifCacheService / GifCacheService download + cache remote GIFs
IFavoritesService / FavoritesService per-source favourites, favorites.json
ImageDecoding.cs first-frame decode helper (file + bytes)
HotkeyFormatter.cs "Ctrl + Alt + M" display strings
Native/
NativeMethods.cs P/Invoke declarations
@@ -132,9 +157,15 @@ code-behind fields. Look elements up by `Tag`/traversal instead.
|---|---|
| `CF_HDROP` (file drop list) | Discord/Slack/Teams/Explorer paste the real file — preserves GIF animation |
| `CF_DIB` (`SetImage`) | Bitmap-only apps like Paint |
| `"PNG"` (registered format) | **Chromium-based apps (Teams, Slack, Discord, browsers) prefer this over CF_DIB** and may paste nothing at all without it |
| `"PNG"` (registered format) | **Chromium-based apps (Teams, Slack, Discord, browsers) prefer this over CF_DIB** and may paste nothing at all without it. **Deliberately omitted for GIFs** — see below |
| `CF_HTML` | The other route web-based compose boxes check; carries a base64 `data:` URI |
**The `"PNG"` format is skipped for GIFs on purpose.** A PNG can only hold one static frame,
and since Chromium *prefers* that format over all others, offering it for a GIF is exactly
what made animated GIFs paste as a still first frame. Omitting it lets those targets fall
through to `CF_HTML`, which carries the full animated data URI. Verified: a GIF's clipboard
formats are FileDrop/Bitmap/HTML with no PNG, while a static image still gets PNG.
Two traps here:
- WPF does **NOT** auto-wrap `DataFormats.Html`. The `Version/StartHTML/EndHTML/
StartFragment/EndFragment` header with exact **byte** offsets must be built by hand
@@ -206,79 +237,68 @@ Automated UI testing of this app is unusually fiddly. What works:
for a name containing "Meme Clipboard". This finds it even when tucked in the overflow
area, unlike screenshots.
Clean up after testing: kill `EbbesMemeClipboard` processes and remove
`%AppData%\EbbesMemeClipboard` so seeded test data doesn't leak into real use.
Clean up after testing: kill `EbbesMemeClipboard` processes, remove
`%AppData%\EbbesMemeClipboard` and `%LocalAppData%\EbbesMemeClipboard`, and **check the
autostart Run key isn't left enabled** if autostart was exercised — that one writes to the
user's real machine state, not just app-local files.
---
## Pending requests (not yet implemented)
## Giphy integration notes
From the user's most recent batch. **None of these are started.**
**Tenor is dead.** The user originally asked for Tenor; research confirmed Google **shut
down the public Tenor API on 2026-06-30** (corroborated by 9to5Google, Shacknews,
Slashdot). Not buildable. **Giphy** was chosen instead and is what's implemented.
### 1. Windows auto-start, with a toggle in Settings
Straightforward. Write `HKCU\Software\Microsoft\Windows\CurrentVersion\Run`.
**Important**: use `Environment.ProcessPath`, **not** `Assembly.Location` — the latter
returns an empty string in single-file published mode. Add an `IAutostartService` +
a checkbox in `SettingsWindow`.
### 2. Tab/dropdown for meme sources (Local, Giphy)
UI shell for multiple sources, plus the Giphy integration behind it.
**Critical context — Tenor is dead.** The user originally asked for Tenor; research during
planning confirmed Google **shut down the public Tenor API on 2026-06-30** (corroborated by
9to5Google, Shacknews, Slashdot). It is not buildable. The user chose **Giphy** as the
replacement. Notes:
- `GET https://api.giphy.com/v1/gifs/search?api_key={key}&q={query}&limit={n}&rating=pg-13`
- Response: `data[].images.{fixed_width,downsized,original}.url`
- Free "beta" key is 100 req/hr — plenty for personal use. Production tier is ~$9k/yr and
irrelevant here. **The user needs to supply their own key** (add a field in Settings).
- Giphy's ToS **requires a visible "Powered by GIPHY" badge** wherever results appear.
This is a real UI requirement, not optional attribution.
- Build behind an `IGifProvider` interface so another provider can be swapped in later.
- Remote GIFs must be **downloaded to a local cache file before** they can go on the
clipboard as CF_HDROP.
- `GET https://api.giphy.com/v1/gifs/search?api_key={key}&q={q}&limit={n}&rating=pg-13`
- Empty query falls back to `/trending` with the same parameters.
- Response: `data[].images.{fixed_width,original}.url` — `fixed_width` for grid
thumbnails, `original` for the full-quality animated file that gets pasted.
- **The user supplies their own key** in Settings. Free tier is 100 req/hr; the ~$9k/yr
production tier is irrelevant for personal use. Without a key the tab shows guidance
instead of failing at request time (`IGifProvider.IsConfigured`).
- Searches are **debounced 350ms** and cancel the previous in-flight request — without
that, every keystroke burns a request against the free rate limit.
- Giphy's ToS **requires the visible "Powered by GIPHY" badge**, rendered in the picker
footer whenever the Giphy tab is active. Don't remove it.
- Klipy (`api.klipy.com`) is a viable alternative if Giphy becomes a problem — founded by
ex-Tenor people, deliberately mirrors Tenor's API shape, free.
ex-Tenor people, deliberately mirrors Tenor's API shape, free. Add it as a second
`IGifProvider`; the tab strip and view models are already provider-agnostic.
### 3. "Central store specific to the application for all local memes"
**⚠️ STILL AMBIGUOUS — ASK THE USER BEFORE BUILDING.**
The app *already* stores memes centrally in `%AppData%\EbbesMemeClipboard\Library`. When
asked to clarify, the user selected "Something else" but the conversation moved on before
they explained. Options offered (and rejected) were: (a) save Giphy results into the local
library, (b) configurable library folder location. So it's something other than those two —
**get a concrete description first.**
**Untested by the assistant**: actual Giphy search results, remote thumbnail loading, and
download-on-select were never exercised against the live API, because no API key was
available. The code paths are written but unverified end-to-end — if something misbehaves
once a key is entered, start with `GiphyGifProvider.SearchAsync` (JSON field names) and
`GifCacheService.GetOrDownloadAsync`.
### 4. Fix GIFs — "clicking a GIF doesn't copy/paste it correctly"
User confirmed this specific symptom (not thumbnails, not crashes, not lag).
---
**Strong root-cause hypothesis (not yet verified — verify before/while fixing):**
`ClipboardService` sets the `"PNG"` clipboard format from `firstFrame` — i.e. **frame 0 of
the GIF re-encoded as a static PNG**. Chromium-based apps *prefer* the `"PNG"` format over
everything else (that preference is exactly why it was added, to fix static-image pasting
into Teams). So for a GIF, Teams/Discord grab the static first frame instead of the animated
GIF → the GIF pastes as a still image.
## Pending requests
**Suggested fix**: skip the `"PNG"` format when the file is a GIF, so Chromium falls back to
`CF_HTML` (which already carries the full animated `data:image/gif;base64,...` URI) or
`CF_HDROP`. `ImageDecoding.IsGif()` already exists for the check. Keep `CF_DIB` so Paint
still gets something. Test in both Teams and Paint, since these two want opposite things.
### ⚠️ 1. "Central store specific to the application for all local memes" — STILL AMBIGUOUS
**Ask the user before building.** The app *already* stores memes centrally in
`%AppData%\EbbesMemeClipboard\Library`. When asked to clarify, the user chose "Something
else" but the conversation moved on before they explained. Options offered *and rejected*
were: (a) save Giphy results into the local library, (b) configurable library folder
location. So it's something other than those two — get a concrete description first.
**Also worth fixing while in there** (separate, minor, pre-existing):
`ImageDecoding.DecodeFirstFrame` **ignores `decodePixelWidth` for GIFs** — it returns the
`GifBitmapDecoder` frame before reaching the `DecodePixelWidth` logic. So GIF thumbnails
decode at full resolution while static images are capped at 150px. A library with many
large GIFs will use more memory than it should.
### 2. Paste-send reliability ("Teams is sometimes too slow")
Not yet addressed. Current delays in `AutoPasteService`: `FocusSettleDelay` 200ms (before
Ctrl+V), `PasteSettleDelay` 400ms (before Enter). Both are fixed sleeps, so a slow app will
still occasionally miss.
### 5. Paste-send reliability ("Teams is sometimes too slow")
Current delays in `AutoPasteService`: `FocusSettleDelay` 200ms (before Ctrl+V),
`PasteSettleDelay` 400ms (before Enter). Both are fixed sleeps — a slow app will still miss.
Bumping the constants is the cheap fix but slows every paste for everyone. Better: **poll
for readiness instead of guessing** — verify the target window is genuinely foreground
before sending Ctrl+V, and confirm rather than firing Enter blind. Note the failure mode
already observed: if Enter fires when the paste didn't land, **Teams sends an empty
message**, so the Enter step should be conservative — a wrong guess posts to a real chat.
Bumping the constants is the cheap fix, but it makes every paste slower for everyone. A
better approach is to **poll for readiness instead of guessing**: e.g. verify the target
window is actually foreground before sending Ctrl+V, and for the send step consider
retrying/confirming rather than firing Enter blind. Note the failure mode already seen:
if Enter fires when the paste didn't land, **Teams sends an empty message** — so the Enter
step should be conservative, since a wrong guess posts to a real chat.
### 3. Possible follow-ups (not requested, just noted)
- **Single-instance protection** (named Mutex + named pipe). Currently two instances fight
over the global hotkey; this caused a confusing phantom "regression" during development.
- **`WPF-UI` is referenced but unused** — intended for Mica/Fluent polish. Use it or drop it.
- **Animated GIF previews on hover** (currently static first frame only) — `GifFramePlayer`
control was designed for this in the original plan but never built.
---
@@ -294,6 +314,9 @@ step should be conservative, since a wrong guess posts to a real chat.
chat apps. Default insert mode is `PasteIntoActiveWindow`.
- No Settings-window dark title bar (content is dark, title bar is standard OS chrome).
User was offered a fix and it wasn't prioritized.
- **Autostart state lives only in the registry**, not in `settings.json`. Mirroring it would
let the two drift apart if the entry is removed via Task Manager or another tool; the
Settings checkbox reads live registry state each time the window opens.
## Known limitations (inherent, not bugs)