@tobq/boardclip 0.0.0-stage → 2.1.108
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +170 -2
- package/assets/boardclip-icon.png +0 -0
- package/bin/boardclip.js +43 -0
- package/editor-preload.js +71 -0
- package/editor.html +220 -0
- package/icon.png +0 -0
- package/icon@2x.png +0 -0
- package/iconTemplate.png +0 -0
- package/iconTemplate@2x.png +0 -0
- package/index.html +1339 -0
- package/lib/appearance.js +183 -0
- package/lib/auto-update.js +306 -0
- package/lib/backup-worker.js +46 -0
- package/lib/backup.js +275 -0
- package/lib/blob-field-store.js +275 -0
- package/lib/blob-store.js +68 -0
- package/lib/build-info.js +89 -0
- package/lib/clip-blob-store.js +161 -0
- package/lib/clip-revision.js +81 -0
- package/lib/clipboard-capture.js +95 -0
- package/lib/clipboard-follow.js +98 -0
- package/lib/clipboard-model.js +1235 -0
- package/lib/cloud-accounts.js +250 -0
- package/lib/conflict-model.js +186 -0
- package/lib/control-client.js +87 -0
- package/lib/control-server.js +149 -0
- package/lib/diagnostics.js +121 -0
- package/lib/drag-files.js +48 -0
- package/lib/ensure-directory.js +56 -0
- package/lib/fork-names.js +35 -0
- package/lib/fs-probe.js +132 -0
- package/lib/history-feed.js +48 -0
- package/lib/hmac-auth.js +78 -0
- package/lib/macos-hotkey.js +297 -0
- package/lib/macos-paste.js +112 -0
- package/lib/mcp-core.js +270 -0
- package/lib/mcp-installers.js +284 -0
- package/lib/mcp-paths.js +99 -0
- package/lib/p2p-crypto.js +54 -0
- package/lib/p2p-discovery.js +191 -0
- package/lib/quick-paste.js +187 -0
- package/lib/retention.js +43 -0
- package/lib/sync-delta.js +227 -0
- package/lib/sync-journal.js +181 -0
- package/lib/sync-paths.js +73 -0
- package/lib/tailscale.js +80 -0
- package/lib/text-blob-store.js +38 -0
- package/lib/windows-clipboard.js +213 -0
- package/lib/windows-console.js +63 -0
- package/lib/windows-dwm.js +183 -0
- package/lib/windows-hook-worker.js +236 -0
- package/lib/windows-hook.js +91 -0
- package/lib/windows-paste.js +185 -0
- package/main.js +7540 -0
- package/mcp/boardclip-mcp.js +255 -0
- package/mcp-approval-preload.js +22 -0
- package/mcp-approval.html +218 -0
- package/package.json +55 -4
- package/preload.js +106 -0
- package/site/index.html +1624 -0
- package/site/shared/clip-search.js +2330 -0
- package/site/shared/clipboard-popup.css +1046 -0
- package/site/shared/clipboard-tokens.css +326 -0
- package/site/shared/clipboard-ui-core.js +6921 -0
- package/site/shared/clipboard-window.css +36 -0
- package/site/shared/vendor/cm5/README.md +52 -0
- package/site/shared/vendor/cm5/codemirror.css +344 -0
- package/site/shared/vendor/cm5/codemirror.js +9884 -0
- package/site/shared/vendor/cm5/diff-match-patch.js +2228 -0
- package/site/shared/vendor/cm5/merge.css +119 -0
- package/site/shared/vendor/cm5/merge.js +1182 -0
- package/site/shared/vendor/fonts/README.md +19 -0
- package/site/shared/vendor/fonts/material-symbols-rounded.woff2 +0 -0
- package/viewer-preload.js +51 -0
- package/viewer.html +129 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Vendored icon font: Material Symbols Rounded
|
|
2
|
+
|
|
3
|
+
- `material-symbols-rounded.woff2` (561,004 bytes, sha256
|
|
4
|
+
`8fbf735bcf81756bdc32d66e4f04b189b3fa49fd30c50b06faa89834ca94c51a`), Apache-2.0
|
|
5
|
+
(https://github.com/google/material-design-icons/blob/master/LICENSE).
|
|
6
|
+
- Source (2026-10-07): the Google Fonts css2 API, fetched with a Chrome 132 User-Agent so it
|
|
7
|
+
answers with one woff2:
|
|
8
|
+
`https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:opsz,wght,FILL,GRAD@20,400,0..1,0&display=block`
|
|
9
|
+
-> `https://fonts.gstatic.com/s/materialsymbolsrounded/v376/syl7-zNym6YjUruM-QrEh7-nyTnjDwKNJ_190FjpZIvLgyidOK7BDB_Qb9vUdV6_gjDK-P2puF_Zs-obHph2-jOcZTKPqw.woff2`
|
|
10
|
+
- Axes: only what the canon pins on `.mi` (clipboard-popup.css): opsz 20, wght 400, GRAD 0
|
|
11
|
+
fixed; FILL 0..1 kept variable (FILL 1 is the state signal, `--icon-fill`).
|
|
12
|
+
|
|
13
|
+
Declared ONCE, by the `@font-face` at the top of `site/shared/clipboard-popup.css`, which every
|
|
14
|
+
app window and the website load. `font-display: block`, so a window never shows the ligature
|
|
15
|
+
words ("settings", "close") while the font loads, and nothing waits on Google Fonts.
|
|
16
|
+
|
|
17
|
+
Re-vendor: fetch the same css2 URL with a current Chrome User-Agent (a different axis set needs
|
|
18
|
+
the `.mi` rule changed too), download the woff2 it names over this file, update the size, hash
|
|
19
|
+
and date above.
|
|
Binary file
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
const { contextBridge, ipcRenderer } = require('electron');
|
|
2
|
+
|
|
3
|
+
// Bridge for the in-app image viewer window (viewer.html). Main pushes the
|
|
4
|
+
// image + theme via 'viewer-init'; the clip menu reuses the SAME mutation IPC
|
|
5
|
+
// channels the popup uses (pin/group-assign/numpad-assign/...), so the two
|
|
6
|
+
// surfaces cannot drift.
|
|
7
|
+
let clipId = null;
|
|
8
|
+
|
|
9
|
+
contextBridge.exposeInMainWorld('viewerApi', {
|
|
10
|
+
onInit: (callback) => {
|
|
11
|
+
ipcRenderer.on('viewer-init', (_, init) => {
|
|
12
|
+
clipId = init && init.id;
|
|
13
|
+
callback(init || {});
|
|
14
|
+
});
|
|
15
|
+
},
|
|
16
|
+
// Light snapshot (items/groups/numpad map) for the shared clip menu.
|
|
17
|
+
state: () => ipcRenderer.invoke('clip-window-state', clipId),
|
|
18
|
+
pin: (id, rev) => ipcRenderer.invoke('pin', id, rev),
|
|
19
|
+
groupCreate: (name) => ipcRenderer.invoke('group-create', name),
|
|
20
|
+
groupAssign: (id, group, rev) => ipcRenderer.invoke('group-assign', id, group, rev),
|
|
21
|
+
numpadAssign: (id, slot, rev) => ipcRenderer.invoke('numpad-assign', id, slot, rev),
|
|
22
|
+
numpadUnassign: (slot) => ipcRenderer.invoke('numpad-unassign', slot),
|
|
23
|
+
setClipTitle: (id, title, rev) => ipcRenderer.invoke('set-clip-title', id, title, rev),
|
|
24
|
+
deleteItems: (targets) => ipcRenderer.invoke('delete-items', targets), // [{ id, rev }]
|
|
25
|
+
copyImagePath: (id) => ipcRenderer.invoke('copy-image-path', id),
|
|
26
|
+
openImageExternal: (id) => ipcRenderer.invoke('open-image-external', id),
|
|
27
|
+
// Drag this image out as a file (native drag of a temporary copy).
|
|
28
|
+
startDrag: () => ipcRenderer.send('start-drag', [clipId]),
|
|
29
|
+
close: () => ipcRenderer.send('viewer-close'),
|
|
30
|
+
// The bar's height + resolved colours, for the native window controls.
|
|
31
|
+
windowChrome: (chrome) => ipcRenderer.send('window-chrome', chrome),
|
|
32
|
+
// Ctrl/Cmd+= / - / 0, claimed in main before the app menu: zoom the image.
|
|
33
|
+
onZoomKey: (callback) => {
|
|
34
|
+
const listener = (_, act) => callback(act);
|
|
35
|
+
ipcRenderer.on('image-zoom-key', listener);
|
|
36
|
+
return () => ipcRenderer.removeListener('image-zoom-key', listener);
|
|
37
|
+
},
|
|
38
|
+
getColorScheme: () => ipcRenderer.invoke('get-color-scheme'),
|
|
39
|
+
onColorSchemeChanged: (callback) => {
|
|
40
|
+
const listener = (_, scheme) => callback(scheme);
|
|
41
|
+
ipcRenderer.on('color-scheme-changed', listener);
|
|
42
|
+
return () => ipcRenderer.removeListener('color-scheme-changed', listener);
|
|
43
|
+
},
|
|
44
|
+
// Appearance (accent, density, corners, theme mode, this window's surface):
|
|
45
|
+
// one event whenever any of it changes - a setting, another device, the OS accent.
|
|
46
|
+
onAppearanceChanged: (callback) => {
|
|
47
|
+
const listener = (_, look) => callback(look || {});
|
|
48
|
+
ipcRenderer.on('appearance-changed', listener);
|
|
49
|
+
return () => ipcRenderer.removeListener('appearance-changed', listener);
|
|
50
|
+
},
|
|
51
|
+
});
|
package/viewer.html
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html><head><meta charset="utf-8"><title>Image</title>
|
|
3
|
+
<script>
|
|
4
|
+
document.documentElement.dataset.theme = matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
|
|
5
|
+
</script>
|
|
6
|
+
<link rel="stylesheet" href="site/shared/clipboard-popup.css">
|
|
7
|
+
<link rel="stylesheet" href="site/shared/clipboard-window.css">
|
|
8
|
+
<style>
|
|
9
|
+
/* Viewer window shell only; the viewer itself + tokens come from the shared
|
|
10
|
+
clipboard-popup.css + Core.createImageViewer (single source), the window
|
|
11
|
+
base (reset, font, background, glass scrim) from clipboard-window.css. */
|
|
12
|
+
#viewerHost { height:100%; }
|
|
13
|
+
</style></head><body>
|
|
14
|
+
<div id="viewerHost"></div>
|
|
15
|
+
<div class="toast" id="toast">Copied</div>
|
|
16
|
+
<script src="site/shared/clip-search.js"></script>
|
|
17
|
+
<script src="site/shared/clipboard-ui-core.js"></script>
|
|
18
|
+
<script>
|
|
19
|
+
const Core = window.BoardClipCore;
|
|
20
|
+
const host = document.getElementById('viewerHost');
|
|
21
|
+
const toast = document.getElementById('toast');
|
|
22
|
+
let toastTimer = null;
|
|
23
|
+
function showToast(msg) { toast.textContent = msg || 'Copied'; toast.classList.add('show'); clearTimeout(toastTimer); toastTimer = setTimeout(() => toast.classList.remove('show'), 1200); }
|
|
24
|
+
|
|
25
|
+
// Theme: follow the app's theme_mode setting + live system changes (same as editor.html).
|
|
26
|
+
let themeMode = 'system';
|
|
27
|
+
let systemDark = matchMedia('(prefers-color-scheme: dark)').matches;
|
|
28
|
+
function refreshTheme() { Core.applyTheme(document.documentElement, themeMode, systemDark); }
|
|
29
|
+
if (window.viewerApi.getColorScheme) window.viewerApi.getColorScheme().then((s) => { systemDark = s !== 'light'; refreshTheme(); }).catch(() => {});
|
|
30
|
+
if (window.viewerApi.onColorSchemeChanged) window.viewerApi.onColorSchemeChanged((s) => { systemDark = s !== 'light'; refreshTheme(); });
|
|
31
|
+
// Later appearance changes (a setting here or on another device, the OS
|
|
32
|
+
// accent, the glass scope) arrive as the same payload as init.
|
|
33
|
+
if (window.viewerApi.onAppearanceChanged) window.viewerApi.onAppearanceChanged((look) => {
|
|
34
|
+
if (look.themeMode) { themeMode = (look.themeMode === 'light' || look.themeMode === 'dark') ? look.themeMode : 'system'; refreshTheme(); }
|
|
35
|
+
Core.applyAppearance(document.documentElement, look);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
let viewer = null;
|
|
39
|
+
let clipId = null;
|
|
40
|
+
// Light state snapshot backing the shared clip menu (items for numpad previews,
|
|
41
|
+
// groups tree, pin state). Refetched right before the menu opens + after every
|
|
42
|
+
// mutation, so it is always current without a live push channel.
|
|
43
|
+
let state = { item: null, items: [], groups: [], aiGroup: 'AI' };
|
|
44
|
+
async function refreshState() {
|
|
45
|
+
state = await window.viewerApi.state();
|
|
46
|
+
if (state && state.item && viewer) viewer.setTitle(Core.titleOf(state.item) || 'Image');
|
|
47
|
+
// Title-bar tag strip: the clip's groups + the "+" picker (images always
|
|
48
|
+
// exist as clips, so no commit-on-add here; a null item hides the strip).
|
|
49
|
+
if (viewer) viewer.setTags(state ? state.item : null);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// The SAME shared controller the popup uses drives the menu dispatch (pin/
|
|
53
|
+
// group/numpad/rename/delete/save/open-external), against an adapter backed by
|
|
54
|
+
// the viewer IPC. context 'viewer' swaps "Open image" for "Open externally".
|
|
55
|
+
// Mutations send the rev of the clip this window shows; a stale one is refused.
|
|
56
|
+
const clipRevOf = (id) => { const it = state.items.find((x) => Core.itemId(x) === id); return it ? it.rev : undefined; };
|
|
57
|
+
const revGuard = (run, fallback) => Core.guardRevision(run, { toast: showToast, refresh: () => refreshState(), fallback });
|
|
58
|
+
const adapter = {
|
|
59
|
+
menuContext: 'viewer',
|
|
60
|
+
itemById: (id) => state.items.find((it) => Core.itemId(it) === id) || null,
|
|
61
|
+
allItems: () => state.items,
|
|
62
|
+
groupNames: () => state.groups,
|
|
63
|
+
numpadMap: () => Core.numpadMap(state.items),
|
|
64
|
+
protectedGroups: () => new Set([state.aiGroup]),
|
|
65
|
+
pin: (id) => revGuard(() => window.viewerApi.pin(id, clipRevOf(id)), false),
|
|
66
|
+
toggleGroup: (id, group) => revGuard(() => window.viewerApi.groupAssign(id, group, clipRevOf(id)), false),
|
|
67
|
+
createGroup: (name) => window.viewerApi.groupCreate(name),
|
|
68
|
+
numpadAssign: (id, slot) => revGuard(() => window.viewerApi.numpadAssign(id, slot, clipRevOf(id)), false),
|
|
69
|
+
numpadUnassign: (slot) => window.viewerApi.numpadUnassign(slot),
|
|
70
|
+
setClipTitle: (id, title) => revGuard(() => window.viewerApi.setClipTitle(id, title, clipRevOf(id)), false),
|
|
71
|
+
// Delete closes the window (the popup shows the Undo toast on its side).
|
|
72
|
+
deleteClips: async (ids) => {
|
|
73
|
+
const done = await revGuard(async () => { await window.viewerApi.deleteItems(Core.revisionTargets(ids, clipRevOf)); return true; }, false);
|
|
74
|
+
if (done) window.viewerApi.close();
|
|
75
|
+
return [];
|
|
76
|
+
},
|
|
77
|
+
openImageExternal: (item) => window.viewerApi.openImageExternal(Core.itemId(item)),
|
|
78
|
+
saveImage: async (item) => { const d = await window.viewerApi.copyImagePath(Core.itemId(item)); return d && d.path ? 'Path copied' : null; },
|
|
79
|
+
toast: (msg) => showToast(msg),
|
|
80
|
+
render: () => {},
|
|
81
|
+
refresh: () => refreshState(),
|
|
82
|
+
};
|
|
83
|
+
const controller = Core.createClipController(adapter);
|
|
84
|
+
document.addEventListener('click', (e) => controller.onClick(e));
|
|
85
|
+
|
|
86
|
+
async function openMenu(x, y) {
|
|
87
|
+
await refreshState();
|
|
88
|
+
if (!state.item) return;
|
|
89
|
+
controller.openClipMenu(Core.itemId(state.item), x, y);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
window.viewerApi.onInit(async (init) => {
|
|
93
|
+
themeMode = (init.themeMode === 'light' || init.themeMode === 'dark') ? init.themeMode : 'system';
|
|
94
|
+
refreshTheme();
|
|
95
|
+
Core.applyAppearance(document.documentElement, init);
|
|
96
|
+
clipId = init.id;
|
|
97
|
+
host.innerHTML = '';
|
|
98
|
+
viewer = Core.createImageViewer({
|
|
99
|
+
src: init.src,
|
|
100
|
+
title: init.title || 'Image',
|
|
101
|
+
nativeControls: true,
|
|
102
|
+
onMenu: (x, y) => openMenu(x, y),
|
|
103
|
+
onClose: () => window.viewerApi.close(),
|
|
104
|
+
// Drag the image out (title-bar handle; the image itself at fit size or
|
|
105
|
+
// with Alt): a native file drag of a temporary copy, done by main.
|
|
106
|
+
onDragOut: window.viewerApi.startDrag ? () => window.viewerApi.startDrag() : null,
|
|
107
|
+
});
|
|
108
|
+
host.appendChild(viewer.el);
|
|
109
|
+
// The OS's own window controls: the bar reserves their room and reports its
|
|
110
|
+
// colours, so the caption buttons match it (main's setTitleBarOverlay).
|
|
111
|
+
Core.attachWindowControls(init.windowControls, window.viewerApi.windowChrome);
|
|
112
|
+
viewer.focus();
|
|
113
|
+
refreshState();
|
|
114
|
+
});
|
|
115
|
+
// Ctrl/Cmd+= / - / 0 zoom the image (main claims them before the app menu).
|
|
116
|
+
if (window.viewerApi.onZoomKey) window.viewerApi.onZoomKey((act) => { if (viewer) viewer.zoomKey(act); });
|
|
117
|
+
|
|
118
|
+
// Right-click anywhere → the same clip menu (parity with popup rows).
|
|
119
|
+
document.addEventListener('contextmenu', (e) => {
|
|
120
|
+
e.preventDefault();
|
|
121
|
+
openMenu(e.clientX, e.clientY);
|
|
122
|
+
});
|
|
123
|
+
// Escape falls through the viewer root (its handler covers the stage); catch
|
|
124
|
+
// stray focus targets here too. Dialogs/menu capture their own Escape first.
|
|
125
|
+
document.addEventListener('keydown', (e) => {
|
|
126
|
+
if (e.key === 'Escape') { e.preventDefault(); window.viewerApi.close(); }
|
|
127
|
+
});
|
|
128
|
+
</script>
|
|
129
|
+
</body></html>
|