135 lines
4.3 KiB
JavaScript
135 lines
4.3 KiB
JavaScript
'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,
|
||
};
|