Files
Clipboard/favorites.js
2026-09-12 13:59:12 +08:00

135 lines
4.3 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';
const Database = require('better-sqlite3');
const { checkAgainstCap } = require('./lib/favorites-cap');
const { likePatterns } = require('./lib/search-plan');
function open(dbPath) {
const db = new Database(dbPath);
db.pragma('journal_mode = WAL');
db.pragma('synchronous = NORMAL');
db.exec(`
CREATE TABLE IF NOT EXISTS favorites (
id INTEGER PRIMARY KEY AUTOINCREMENT,
type TEXT NOT NULL,
text TEXT,
image BLOB,
preview TEXT,
source_clip_id INTEGER,
created_at REAL NOT NULL,
favorited_at REAL NOT NULL,
UNIQUE(source_clip_id)
);
CREATE INDEX IF NOT EXISTS idx_fav_favorited_at ON favorites(favorited_at DESC);
`);
return db;
}
function close(db) {
if (db) db.close();
}
// 列表不取 image BLOB —— 图片走 getImage() 按需拉。
const LIST_COLS = `id, type, text, preview, source_clip_id, created_at, favorited_at,
(image IS NOT NULL) AS has_image`;
// 每个连接一套预编译语句,避免每次调用都重新 prepare
function stmts(db) {
if (!db._stmt) {
db._stmt = {
insert: db.prepare(`
INSERT INTO favorites(type, text, image, preview, source_clip_id, created_at, favorited_at)
VALUES(?, ?, ?, ?, ?, ?, ?)
`),
getMeta: db.prepare(`SELECT ${LIST_COLS} FROM favorites WHERE id = ?`),
getImage: db.prepare('SELECT image FROM favorites WHERE id = ?'),
getRaw: db.prepare('SELECT * FROM favorites WHERE id = ?'),
bySource: db.prepare('SELECT id FROM favorites WHERE source_clip_id = ?'),
remove: db.prepare('DELETE FROM favorites WHERE id = ?'),
count: db.prepare('SELECT COUNT(*) AS n FROM favorites'),
list: db.prepare(`SELECT ${LIST_COLS} FROM favorites ORDER BY favorited_at DESC LIMIT ?`),
};
}
return db._stmt;
}
function rowToClip(row) {
if (!row) return null;
return {
id: row.id,
type: row.type,
text: row.text || '',
hasImage: row.has_image != null ? row.has_image === 1 : row.image != null,
preview: row.preview || '',
sourceClipId: row.source_clip_id,
createdAt: row.created_at,
favoritedAt: row.favorited_at,
};
}
function add(db, sourceClip) {
const cap = checkAgainstCap(db);
if (!cap.ok) {
const err = new Error(cap.message);
err.ok = false; err.code = 'FAV_LIMIT';
throw err;
}
const now = Date.now() / 1000;
const info = stmts(db).insert.run(
sourceClip.type,
sourceClip.type === 'text' ? (sourceClip.text || '') : null,
sourceClip.type === 'image' ? (sourceClip.image || null) : null,
sourceClip.preview || '',
sourceClip.id,
sourceClip.createdAt || now,
now,
);
return rowToClip(stmts(db).getMeta.get(info.lastInsertRowid));
}
function isFavorited(db, clipId) {
const row = stmts(db).bySource.get(clipId);
return row ? row.id : null;
}
function remove(db, favId) {
return stmts(db).remove.run(favId).changes > 0;
}
function get(db, favId) {
return rowToClip(stmts(db).getMeta.get(favId));
}
function getImage(db, favId) {
const row = stmts(db).getImage.get(favId);
if (!row || !row.image) return null;
return Buffer.from(row.image).toString('base64');
}
// 原始行(含 image BLOB仅供主进程"复制收藏到系统剪贴板"用。
// 字段形状type / text / image与 clips 表一致,可直接喂 lib/clip-payload。
// 渲染进程不要用它 —— 图片走 getImage() 按需拉。
function getRaw(db, favId) {
return stmts(db).getRaw.get(favId) || null;
}
function count(db) {
return stmts(db).count.get().n;
}
function list(db, query = '', limit = 5000) {
const s = stmts(db);
// 与历史搜索同语义:每个 token 独立子串 AND% _ \ 已转义lib/search-plan
// 不转义时搜 "100%" 里的 % 会当通配符命中一片无关记录。
const patterns = likePatterns(query);
if (!patterns.length) return s.list.all(limit).map(rowToClip);
const where = patterns.map(() => `text LIKE ? ESCAPE '\\'`).join(' AND ');
const rows = db.prepare(
`SELECT ${LIST_COLS} FROM favorites WHERE ${where} ORDER BY favorited_at DESC LIMIT ?`
).all(...patterns, limit);
return rows.map(rowToClip);
}
module.exports = {
open, close, rowToClip, add, isFavorited, remove, get, getImage, getRaw, count, list,
};