Files
SwipeAnything/adapters/base.js
T
ilia ce43c5de6d
CI / Gitleaks (pull_request) Successful in 17s
CI / Unit tests (pull_request) Successful in 21s
Add organize folders, details sheet, and richer previews.
Map keys 0–9 to destinations, inspect files before deciding, and preview
PDF/RAW/ZIP/video with an OLED dark UI polish plus demo seed and tests.
2026-07-26 21:38:36 -04:00

172 lines
5.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use strict';
/**
* Base contract every SwipeAnything adapter implements.
*
* An adapter turns "some collection of things" (files, emails, database
* rows, ...) into a queue of swipeable cards, and knows how to apply and
* undo the actions a user can take on each card. The core server and UI
* never know what an adapter actually touches -- they only talk to this
* interface.
*
* See CONTRIBUTING.md for a walkthrough of writing a new adapter, and
* adapters/folder.js (local files) / adapters/immich.js (remote API) for
* two different reference implementations.
*/
class Adapter {
/** Unique machine id, e.g. "folder". Stored in swipeanything.config.json. */
static id = 'base';
/** Human-friendly name shown in the settings UI. */
static label = 'Base adapter';
/** One-line description shown in the settings UI. */
static description = '';
/**
* Declares the settings this adapter needs, so the settings UI can render
* a generic form without adapter-specific frontend code. Each entry:
* {
* key: string,
* label: string,
* type: 'text' | 'password' | 'checkbox' | 'number' | 'select' | 'folder' | 'folderMap',
* default?: any,
* options?: Array<{ value: string, label: string }>, // for type 'select'
* placeholder?: string,
* required?: boolean,
* }
* `type: 'folder'` renders a text field plus a "Browse..." button backed
* by a native folder picker where available (see server.js /api/browse-folder).
* `type: 'folderMap'` renders keys 09, each with an optional label and a
* folder path (for organize-into-buckets flows).
*/
static configSchema = [];
/**
* Default actions available on every card. Prefer overriding getActions()
* when the set depends on settings (e.g. numbered destination folders).
* {
* id: string,
* label: string,
* key: string, // KeyboardEvent.key that triggers it
* direction?: 'left' | 'right' | 'up' | 'down',
* isDestructive?: boolean,
* group?: 'primary' | 'organize', // UI layout hint
* }
*/
static actions = [
{ id: 'keep', label: 'Keep', key: 'ArrowRight', direction: 'right' },
{ id: 'reject', label: 'Reject', key: 'ArrowLeft', direction: 'left', isDestructive: true },
];
constructor(settings = {}) {
this.settings = settings;
}
/**
* Actions for the current session. Defaults to the static `actions` list;
* override when actions depend on settings (folder destinations, etc.).
*/
getActions() {
return this.constructor.actions;
}
/**
* Optional async setup: validate settings, open a mailbox, connect to a
* database, create a trash directory, etc. Throw a descriptive Error to
* surface a validation message in the settings UI.
*/
async init() {}
/**
* Returns the full queue of items to review, in order. Called once per
* session (see the "Rescan" action in the UI to rebuild it).
* @returns {Promise<Array<{
* id: string,
* title: string,
* subtitle?: string,
* previewType?: 'image' | 'audio' | 'video' | 'pdf' | 'archive' | 'text' | 'none',
* meta?: Record<string, string | number>,
* }>>}
*/
async list() {
throw new Error(`${this.constructor.name} must implement list()`);
}
/**
* Applies `actionId` to `item`. Return whatever undo() needs to reverse
* the effect, or null/undefined if the action has no side effect (e.g.
* "keep" on a filesystem adapter just leaves the file alone).
*/
async applyAction(item, actionId) {
throw new Error(`${this.constructor.name} must implement applyAction()`);
}
/** Reverses the effect described by the record returned from applyAction(). */
async undo(record) {
throw new Error(`${this.constructor.name} must implement undo()`);
}
/**
* Writes the full-size preview for `itemId` directly to the Express
* response (res.sendFile for local files, a piped fetch for remote
* APIs, etc). Return true if you wrote a response, false to let the
* server respond 404 (e.g. this item has no visual preview).
*/
async streamPreview(itemId, res) {
return false;
}
/**
* Same as streamPreview, but for a smaller/faster thumbnail (used for
* the card image and as a video poster frame). Optional -- the default
* does nothing, and the frontend falls back to streamPreview.
*/
async streamThumbnail(itemId, res) {
return false;
}
/**
* Optional: richer details for the inspect-before-deciding panel.
* @returns {Promise<{ fields: Array<{ label: string, value: string }>, path?: string, actions?: Array<{ id: string, label: string }> } | null>}
*/
async getDetails(itemId) {
return null;
}
/**
* Optional: reveal / open the item in the native file manager.
* Return true if handled.
*/
async reveal(itemId) {
return false;
}
/**
* Optional: describes a reversible "trash" this adapter maintains, so
* the UI can offer an explicit, confirm-guarded "Empty trash" action.
* Return null if this adapter has no such concept.
* @returns {Promise<{ count: number, label: string } | null>}
*/
async describeTrash() {
return null;
}
/** Optional: permanently clears whatever describeTrash() described. */
async emptyTrash() {
throw new Error(`${this.constructor.name} does not support emptyTrash()`);
}
/** Optional short string describing the source, shown in the UI header. */
describeSource() {
return '';
}
/** Optional UI hints (e.g. showDetailsByDefault) merged into /api/queue. */
uiHints() {
return {};
}
}
module.exports = { Adapter };