This commit is contained in:
2026-09-12 13:59:12 +08:00
commit 941ce4baab
71 changed files with 13037 additions and 0 deletions

134
favorites.js Normal file
View File

@@ -0,0 +1,134 @@
'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,
};