Files
SwipeAnything/CONTRIBUTING.md
T
ilia 0c9919f93c Add second adapter, thumbnails, a11y pass, and a real test suite
- Immich adapter (adapters/immich.js): swipe a self-hosted photo library
  over its REST API, proving the adapter contract works for remote
  sources, not just the filesystem.
- Refactor the preview contract from resolvePreviewPath() to
  streamPreview()/streamThumbnail(), so adapters can serve previews from
  anywhere (local file, proxied fetch, etc).
- macOS Quick Look-backed thumbnail cache (lib/thumbnails.js): real
  resized thumbnails, HEIC/HEIF, and video poster frames, with a hard
  timeout and graceful fallback to the original file everywhere else.
- Native "Browse..." folder picker (osascript) in Settings, with manual
  typing as the fallback on other platforms.
- Confirm-guarded "Empty trash" action -- the only place this project
  permanently deletes anything.
- Session resume: progress now survives a server restart and an
  unchanged Rescan via .swipeanything-session.json.
- Accessibility pass: live region item announcements, aria-labels on
  action buttons, focus-trapped shortcuts dialog with focus restore,
  visible focus rings, a real role="progressbar", and <button>s instead
  of <a href="#"> in the header.
- node:test suite (23 tests) covering the folder adapter, the Immich
  adapter (mocked fetch, no live server needed), and the HTTP API
  end-to-end; wired up as `npm test`.
- Also includes the keyboard shortcuts (up/down undo/skip), swipe stamps,
  and shortcuts modal from the previous session that hadn't been
  committed yet.
2026-07-25 19:41:47 -04:00

149 lines
5.2 KiB
Markdown

# Contributing
Adapters are the whole point of this project — the swipe UI, keyboard
shortcuts, drag gestures, undo stack, and settings form are all generic and
work for any adapter that implements the contract below.
## Writing a new adapter
1. Create `adapters/your-adapter.js` and extend the base class:
```js
const { Adapter } = require('./base');
class YourAdapter extends Adapter {
static id = 'your-adapter'; // used in swipeanything.config.json
static label = 'Your Adapter'; // shown in the settings UI
static description = 'One line describing what this swipes through.';
// Settings UI is generated from this — no frontend code needed.
static configSchema = [
{ key: 'someSetting', label: 'Some setting', type: 'text', required: true },
];
// Optional: override the default keep/reject pair, e.g. add a third
// "later" action. Each needs a distinct key and direction.
static actions = [
{ id: 'keep', label: 'Keep', key: 'ArrowRight', direction: 'right' },
{ id: 'reject', label: 'Reject', key: 'ArrowLeft', direction: 'left', isDestructive: true },
];
async init() {
// Validate settings / open a connection. Throw a descriptive Error
// to surface a message in the settings UI.
}
async list() {
// Return the full queue: [{ id, title, subtitle?, previewType?, meta? }]
}
async applyAction(item, actionId) {
// Perform the effect. Return whatever undo() needs, or null/undefined.
}
async undo(record) {
// Reverse applyAction() using the record it returned.
}
// Optional: write a full-size preview straight to the Express response.
// Return true if you handled it, false to let the server 404.
async streamPreview(itemId, res) {
return false;
}
// Optional: same, but for a smaller/faster thumbnail (used for the
// card image and as a video poster frame). Falls back to streamPreview
// in the UI if omitted or it returns false.
async streamThumbnail(itemId, res) {
return false;
}
// Optional: describe a reversible "trash" so the UI offers a
// confirm-guarded "Empty trash" action. Return null if you don't have one.
async describeTrash() {
return null;
}
async emptyTrash() {
throw new Error('not supported');
}
describeSource() {
return ''; // short string shown in the UI header
}
}
module.exports = { YourAdapter };
```
See `adapters/folder.js` for a local-filesystem implementation (`res.sendFile`
+ a Quick Look thumbnail cache) and `adapters/immich.js` for a remote-API
implementation (proxying a fetch response straight into `res`) of
`streamPreview`/`streamThumbnail`.
2. Register it in `adapters/registry.js`:
```js
const { YourAdapter } = require('./your-adapter');
const ADAPTERS = {
[FolderAdapter.id]: FolderAdapter,
[YourAdapter.id]: YourAdapter,
};
```
3. That's it — it now shows up in the Settings page automatically, with a
generated form from `configSchema`, and the swipe UI works against it.
## Design constraints to keep in mind
- **Non-destructive by default.** Follow the folder adapter's lead: prefer
"move to a recoverable place" / "mark as read" / "flag a row" over
irreversible deletes, and make `undo()` actually reverse it.
- **No adapter-specific frontend code.** If you find yourself editing
`public/app.js` to special-case your adapter, the contract is probably
missing something generic — open an issue/PR to discuss extending
`configSchema`, `actions`, or the item shape instead of forking the UI.
- **Validate in `init()`**, not `list()`. Throwing from `init()` surfaces a
clean error in the settings form before anything gets saved.
- **Keep `list()` fast enough for a session.** It's called once per session
(or on "Rescan"), not per card — pagination/streaming can come later if
a real adapter needs it.
## Local development
```bash
npm install
npm start
```
There's no build step — edit files under `public/` or `adapters/` and
refresh the browser.
## Testing
```bash
npm test
```
Runs Node's built-in test runner (`node --test`) against `test/`. No extra
dev dependencies, no separate test server to start.
- `test/folder-adapter.test.js` exercises the filesystem adapter against real
temp directories (list/reject/undo/emptyTrash/path-traversal guard).
- `test/immich-adapter.test.js` exercises the API-based adapter with
`global.fetch` mocked out — no live Immich server needed. Follow this
pattern for other network-backed adapters.
- `test/api.test.js` boots the real Express app (`server.start(0)`) against an
isolated config/session file (via the `SWIPEANYTHING_CONFIG_PATH` /
`SWIPEANYTHING_SESSION_PATH` env vars) and drives it over real HTTP.
Please add or extend tests for new adapters and API changes — a fetch-mocked
adapter test is usually enough; you don't need a live backend to contribute one.
## Pull requests
Keep adapters self-contained in their own file. Include a short section in
the PR description covering: what it swipes through, what each action does,
and how undo works.