Added Giphy support, Favourites
This commit is contained in:
+91
-68
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user