update
This commit is contained in:
63
main.js
63
main.js
@@ -41,7 +41,7 @@ const { createEchoSuppress } = require('./lib/echo-suppress');
|
||||
|
||||
// ---------- 配置 ----------
|
||||
|
||||
const APP_NAME = '剪贴板';
|
||||
const APP_NAME = 'Clipboard';
|
||||
|
||||
let appConfig = configMod.loadConfig();
|
||||
let MAX_ITEMS = appConfig.maxItems; // 运行时可变
|
||||
@@ -102,7 +102,10 @@ const listCols = (p = '') =>
|
||||
const orderBy = (p = '') => `${p}created_at DESC, ${p}id DESC`;
|
||||
|
||||
function openDb() {
|
||||
fs.mkdirSync(appConfig.dataDir, { recursive: true });
|
||||
// 必须 mkdir effectiveDataDir()(含 session 兜底),不是 appConfig.dataDir:
|
||||
// 用户配置的目录不可用(盘符缺失)时启动已切到 sessionDataDir,这里再去
|
||||
// mkdir 那个坏目录会直接抛 ENOENT —— 兜底逻辑被这一行废掉,照样弹"启动失败"。
|
||||
fs.mkdirSync(effectiveDataDir(), { recursive: true });
|
||||
ftsAvailable = null; // 换库后能力可能不同,重新探测
|
||||
clipCount = null; // 行数缓存绑定旧库,换库后必须回源重查
|
||||
db = new Database(getDbPath());
|
||||
@@ -144,7 +147,6 @@ function openDb() {
|
||||
stmt.getMeta = db.prepare(`SELECT ${listCols()} FROM clips WHERE id = ?`);
|
||||
stmt.getImage = db.prepare(`SELECT image FROM clips WHERE id = ?`);
|
||||
stmt.moveToTop = db.prepare(`UPDATE clips SET top_at = ?, created_at = ? WHERE id = ?`);
|
||||
stmt.clearTop = db.prepare(`UPDATE clips SET top_at = NULL WHERE id = ?`);
|
||||
stmt.delete = db.prepare(`DELETE FROM clips WHERE id = ?`);
|
||||
stmt.clearNonTop = db.prepare(`DELETE FROM clips WHERE top_at IS NULL`);
|
||||
// 之前每次入库都现场 prepare 一遍,热路径上白白编译
|
||||
@@ -167,7 +169,7 @@ function openDb() {
|
||||
}
|
||||
|
||||
function openFavDb() {
|
||||
fs.mkdirSync(appConfig.dataDir, { recursive: true });
|
||||
fs.mkdirSync(effectiveDataDir(), { recursive: true }); // 同 openDb:兜底目录
|
||||
favStmtCache.clear(); // 语句绑定在旧连接上,换库后必须丢掉
|
||||
favDb = favorites.open(getFavDbPath());
|
||||
}
|
||||
@@ -287,15 +289,12 @@ function pollClipboard() {
|
||||
}
|
||||
|
||||
// 文本变化检测(规则见 lib/echo-suppress.js)。
|
||||
// readText 抛错时(text === null)**不调 observe** —— 否则 last 会被改成
|
||||
// '' 触发 'empty' 直接退出,但剪贴板上可能有图片;图片分支被一并跳过,
|
||||
// 用户刚复制的那张图就静默丢失。等下次 tick readText 恢复时再正常观察。
|
||||
// readText 抛错时(text === null)**不调 observe** —— 一次读取失败不等于
|
||||
// 剪贴板被清空:把基线 last 改成 '' 后,下个 tick 读回原来的文本会判
|
||||
// 'new' 把旧内容重复入一次库。跳过本次观察,等 readText 恢复再正常分发;
|
||||
// 图片分支不受影响,照常探测。
|
||||
if (text !== null) {
|
||||
const verdict = textSuppress.observe(text);
|
||||
if (verdict === 'empty') {
|
||||
// 剪贴板被清空或原本就空,跳过(不消化回声标记)
|
||||
return;
|
||||
}
|
||||
if (verdict === 'suppressed') {
|
||||
devLog('[poll] suppressed (was our own paste)');
|
||||
return;
|
||||
@@ -318,6 +317,12 @@ function pollClipboard() {
|
||||
}
|
||||
return;
|
||||
}
|
||||
// verdict === 'empty':剪贴板上没有文本 —— 可能被清空,也可能上面是
|
||||
// 一张图(readText 读不到图片)。之前这里直接 return:从「复制文本」
|
||||
// 切到「复制图片」时,图片要晚一个 tick 才入库(等下一次 observe('')
|
||||
// 判 unchanged 才轮到图片分支)。落空继续走图片分支即可 —— 真为空时
|
||||
// shouldReadImage 守卫零开销拦掉。「empty 不消化回声标记」的规则不变
|
||||
// (由 lib/echo-suppress.js 内部保证),这里只是不再提前退出。
|
||||
// verdict === 'unchanged' —— 文本没变,继续看图片分支
|
||||
} else {
|
||||
devLog('[poll] readText skipped (last read failed), still probing image branch');
|
||||
@@ -455,7 +460,7 @@ function applyPollMs(ms) {
|
||||
startPolling(); // startPolling 内部会先 stopPolling
|
||||
}
|
||||
|
||||
// 窗口置顶:不落盘,重启回到默认(开启)——与本功能开发前的行为一致
|
||||
// 窗口置顶:落盘持久化,重启后保持用户的选择(与其他设置项一致)。
|
||||
let alwaysOnTop = true;
|
||||
|
||||
function applyAlwaysOnTop(on) {
|
||||
@@ -464,6 +469,14 @@ function applyAlwaysOnTop(on) {
|
||||
try { mainWindow.setAlwaysOnTop(alwaysOnTop); }
|
||||
catch (e) { devWarn('[window] setAlwaysOnTop failed:', e.message); }
|
||||
}
|
||||
// 值真的变了才写盘:启动时的同步调用(applyAlwaysOnTop(appConfig.alwaysOnTop))
|
||||
// 值与磁盘一致,靠这个判断避免每次启动都白写一次 config.json。
|
||||
// 写盘失败只打日志 —— 置顶本会话已生效,重启还原成旧值,比设置项失败轻。
|
||||
if (appConfig.alwaysOnTop !== alwaysOnTop) {
|
||||
appConfig.alwaysOnTop = alwaysOnTop;
|
||||
try { configMod.saveConfig(appConfig); }
|
||||
catch (e) { console.error('[window] save alwaysOnTop failed:', e.message); }
|
||||
}
|
||||
return alwaysOnTop;
|
||||
}
|
||||
|
||||
@@ -485,7 +498,10 @@ function createFramelessWindow({ width, height, file, onClose, minimizable = fal
|
||||
// 窗口又变回置顶,而按钮状态仍显示"已关闭"。
|
||||
alwaysOnTop,
|
||||
// 背景色跟随主题:之前写死 #FFFFFF,5/6 的主题首帧会闪白。
|
||||
backgroundColor: configMod.themeBgColor(appConfig.theme),
|
||||
// 必须用 effectiveTheme 而不是 raw theme —— 「跟随系统」开着时 raw 主题
|
||||
// 和实际显示的不一致(如 raw 是暗色但系统是浅色),首帧会闪一下错色。
|
||||
backgroundColor: configMod.themeBgColor(
|
||||
configMod.effectiveTheme(appConfig, nativeTheme.shouldUseDarkColors)),
|
||||
// OS 级窗口图标:alt-tab、任务管理器进程图标、Win+Tab 任务视图。
|
||||
// 工厂被主窗口和设置窗口共用,一行设置两窗生效。
|
||||
icon: path.join(__dirname, 'icon.ico'),
|
||||
@@ -1116,7 +1132,13 @@ function registerIpc() {
|
||||
return applyThemeChange({ followSystem: followSystem === true });
|
||||
});
|
||||
|
||||
ipcMain.handle('settings:get', () => ({ ...appConfig }));
|
||||
ipcMain.handle('settings:get', () => ({
|
||||
...appConfig,
|
||||
// 实际在用的数据目录(含 session 兜底)。settings:open-dir 开的就是它;
|
||||
// 设置页只显示 appConfig.dataDir 的话,兜底状态下用户看到的目录和
|
||||
// 「打开数据目录」进去的目录对不上。
|
||||
dataDirEffective: effectiveDataDir(),
|
||||
}));
|
||||
|
||||
ipcMain.handle('settings:set', (_e, { key, value }) => {
|
||||
appConfig = applyPatch(appConfig, { key, value });
|
||||
@@ -1393,13 +1415,9 @@ if (!gotLock) {
|
||||
registerIpc();
|
||||
devLog('[main] ipc registered');
|
||||
|
||||
// 启动时把模块级 alwaysOnTop 同步到磁盘配置 —— 之前硬编码 true,
|
||||
// 用户在主窗口点了图钉关闭置顶之后重启,配置里若有 ever-set 的
|
||||
// false 状态也会被无视。应用层只暴露主窗口图钉一个入口,
|
||||
// 所以这个字段大多停留在默认值;只是把"读 config、推模块变量"
|
||||
// 的链路补上,避免和磁盘分叉。
|
||||
// 必须在 createMainWindow 之前调用:模块级 alwaysOnTop 默认 true,
|
||||
// 窗口构造时会读它,晚了就要先建后改(多一次 setAlwaysOnTop 调用)。
|
||||
// 启动时从磁盘配置恢复置顶开关(applyAlwaysOnTop 在切换时已落盘,
|
||||
// 见该函数注释)。必须在 createMainWindow 之前调用:窗口构造时会
|
||||
// 读模块级 alwaysOnTop,晚了就要先建后改(多一次 setAlwaysOnTop)。
|
||||
applyAlwaysOnTop(appConfig.alwaysOnTop);
|
||||
|
||||
createMainWindow(); // 内部已经挂好自适应轮询的 show/hide 监听
|
||||
@@ -1436,6 +1454,9 @@ if (!gotLock) {
|
||||
? '\n\n可能是数据目录不可用(盘符缺失、权限不足等)。\n请到「设置 → 通用 → 数据保存位置」换一个位置,或检查该盘符/文件夹是否可访问。'
|
||||
: '';
|
||||
dialog.showErrorBox(APP_NAME, '启动失败:' + e.message + hint);
|
||||
// 启动失败时窗口 / 托盘 / 轮询全没起来,window-all-closed 又拦着不退
|
||||
// —— 不主动退出的话进程变成无界面僵尸,只能去任务管理器里杀。
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user