@tobq/boardclip 0.0.0-stage → 2.1.107
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
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Tobi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,171 @@
|
|
|
1
|
-
#
|
|
1
|
+
# BoardClip
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Fast cross-platform clipboard history for desktop. BoardClip runs as a tray app, opens with a global shortcut, and keeps your useful text and image clips searchable, pinnable, and quick to paste.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
Everyday desktop builds are available from the website:
|
|
8
|
+
|
|
9
|
+
[Download BoardClip](https://boardclip.app)
|
|
10
|
+
|
|
11
|
+
With a package manager:
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
# macOS (Homebrew): the app opens with no security prompt
|
|
15
|
+
brew install --cask tobq/tap/boardclip
|
|
16
|
+
|
|
17
|
+
# Windows (Scoop)
|
|
18
|
+
scoop bucket add tobq https://github.com/tobq/homebrew-tap
|
|
19
|
+
scoop install tobq/boardclip
|
|
20
|
+
|
|
21
|
+
# Any OS with Node.js 18+ (then run `boardclip`; update with `npm update -g @tobq/boardclip`)
|
|
22
|
+
npm i -g @tobq/boardclip
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Or try it without installing: `npx @tobq/boardclip`.
|
|
26
|
+
|
|
27
|
+
Terminal install keeps a source checkout on your machine and reuses the same local lifecycle scripts for future updates. This path requires Git, and install/dependency updates also require npm.
|
|
28
|
+
It also registers a normal launcher: `BoardClip` in the Windows Start Menu or `~/Applications/BoardClip.app` on macOS.
|
|
29
|
+
|
|
30
|
+
macOS / Linux:
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
curl -fsSL https://boardclip.sh/install.sh | sh
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Windows PowerShell:
|
|
37
|
+
|
|
38
|
+
```powershell
|
|
39
|
+
irm https://boardclip.sh/install.ps1 | iex
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Prefer to inspect first? Open the script URL in your browser, or download it before running:
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
curl -fsSLO https://boardclip.sh/install.sh
|
|
46
|
+
sh install.sh
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Update
|
|
50
|
+
|
|
51
|
+
macOS / Linux:
|
|
52
|
+
|
|
53
|
+
```sh
|
|
54
|
+
curl -fsSL https://boardclip.sh/update.sh | sh
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Windows PowerShell:
|
|
58
|
+
|
|
59
|
+
```powershell
|
|
60
|
+
irm https://boardclip.sh/update.ps1 | iex
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Local checkout update scripts are also available:
|
|
64
|
+
|
|
65
|
+
```sh
|
|
66
|
+
./update.sh
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
```bat
|
|
70
|
+
update.bat
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Local update scripts are production-safe by default: they fast-forward only and
|
|
74
|
+
refuse to run if tracked app files have local edits. Runtime data files are
|
|
75
|
+
ignored and do not block updates. The public update commands above set
|
|
76
|
+
`BOARDCLIP_UPDATE_ALLOW_DIRTY=1`, so normal installs can repair a dirty checkout
|
|
77
|
+
with Git autostash. For a developer checkout, set that variable yourself only
|
|
78
|
+
when you intentionally want Git to autostash local code changes.
|
|
79
|
+
|
|
80
|
+
## Features
|
|
81
|
+
|
|
82
|
+
- Hotkey popup: Cmd+Shift+V on macOS, Win+V on Windows.
|
|
83
|
+
- Text and image history with automatic deduplication.
|
|
84
|
+
- Numpad quick-paste slots 1-9 for snippets you reuse constantly.
|
|
85
|
+
- Pins and groups for keeping durable clips organized, with include/exclude filters.
|
|
86
|
+
- Native cloud sync through Google Drive, OneDrive, and iCloud folders.
|
|
87
|
+
- Regex search, external editor support, and image export.
|
|
88
|
+
- Local-first data: no hosted clipboard database or BoardClip account.
|
|
89
|
+
|
|
90
|
+
## Keyboard Shortcuts
|
|
91
|
+
|
|
92
|
+
| Shortcut | Action |
|
|
93
|
+
| --- | --- |
|
|
94
|
+
| Cmd+Shift+V / Win+V | Toggle popup |
|
|
95
|
+
| Cmd+Numpad 1-9 / Win+Numpad 1-9 | Quick-paste slot |
|
|
96
|
+
| 1-9 in empty search | Quick-paste slot |
|
|
97
|
+
| Arrow keys | Navigate items |
|
|
98
|
+
| Enter | Paste selected item |
|
|
99
|
+
| Escape | Close popup |
|
|
100
|
+
|
|
101
|
+
## Development
|
|
102
|
+
|
|
103
|
+
Requirements:
|
|
104
|
+
|
|
105
|
+
- Git
|
|
106
|
+
- Node.js and npm
|
|
107
|
+
|
|
108
|
+
Run from a checkout:
|
|
109
|
+
|
|
110
|
+
```sh
|
|
111
|
+
npm install
|
|
112
|
+
npm test
|
|
113
|
+
npm start
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Build desktop binaries locally:
|
|
117
|
+
|
|
118
|
+
```sh
|
|
119
|
+
npm run dist
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Platform-specific package scripts are available as `npm run dist:win`, `npm run dist:mac`, and `npm run dist:linux`. Tagged releases matching `v*` are built by GitHub Actions and attached to the GitHub release.
|
|
123
|
+
|
|
124
|
+
Regenerate the shared app, installer, and site icons:
|
|
125
|
+
|
|
126
|
+
```sh
|
|
127
|
+
npm run sync:icons
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Useful local scripts:
|
|
131
|
+
|
|
132
|
+
```sh
|
|
133
|
+
./start.sh
|
|
134
|
+
./update.sh
|
|
135
|
+
./kill.sh
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
```bat
|
|
139
|
+
start.bat
|
|
140
|
+
update.bat
|
|
141
|
+
kill.bat
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Project Structure
|
|
145
|
+
|
|
146
|
+
- `main.js`: Electron main process, tray, shortcuts, clipboard polling, sync, IPC.
|
|
147
|
+
- `index.html`: single-file renderer UI.
|
|
148
|
+
- `preload.js`: safe renderer API bridge.
|
|
149
|
+
- `lib/clipboard-model.js`: shared pure history/pin/group merge logic.
|
|
150
|
+
- `lib/cloud-accounts.js`: Google Drive, OneDrive, and iCloud folder discovery.
|
|
151
|
+
- `lib/windows-hook*`: Windows low-level keyboard hook for Win+V / Win+Numpad.
|
|
152
|
+
- `site/`: static landing page and remote install/update scripts.
|
|
153
|
+
|
|
154
|
+
## Data
|
|
155
|
+
|
|
156
|
+
Source installs store data in the app checkout. Packaged installs store data in Electron's per-user app data directory:
|
|
157
|
+
|
|
158
|
+
- `clipboard-history.json`
|
|
159
|
+
- `clipboard-settings.json`
|
|
160
|
+
- `clipboard-images/`
|
|
161
|
+
- `clipboard-text/` for large text snippets
|
|
162
|
+
|
|
163
|
+
Cloud sync writes the same files into detected provider folders under `clipboard-tray/`. That folder name is retained for compatibility with earlier installs.
|
|
164
|
+
|
|
165
|
+
## Contributing
|
|
166
|
+
|
|
167
|
+
Issues and pull requests are welcome. Please run `npm test` before opening a PR and keep changes focused; most behavior should go through shared helpers in `lib/clipboard-model.js` instead of duplicating merge logic in UI or IPC code.
|
|
168
|
+
|
|
169
|
+
## License
|
|
170
|
+
|
|
171
|
+
MIT
|
|
Binary file
|
package/bin/boardclip.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// `boardclip` command of the npm package (npm i -g @tobq/boardclip, npx @tobq/boardclip):
|
|
3
|
+
// starts the tray app with the Electron that npm installed beside it, detached
|
|
4
|
+
// so the terminal is free again and closing it does not take the app along.
|
|
5
|
+
// A second start only brings the running app's popup up (single-instance lock).
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
const { spawn } = require('child_process');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
|
|
11
|
+
const appDir = path.join(__dirname, '..');
|
|
12
|
+
const args = process.argv.slice(2);
|
|
13
|
+
|
|
14
|
+
if (args.includes('--version') || args.includes('-v')) {
|
|
15
|
+
console.log(require('../package.json').version);
|
|
16
|
+
process.exit(0);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let electron;
|
|
20
|
+
try {
|
|
21
|
+
electron = require('electron');
|
|
22
|
+
} catch (error) {
|
|
23
|
+
console.error('BoardClip needs its Electron download. Reinstall with: npm i -g @tobq/boardclip');
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const env = { ...process.env };
|
|
28
|
+
// Set by some shells and tools; it would start Electron as plain Node.
|
|
29
|
+
delete env.ELECTRON_RUN_AS_NODE;
|
|
30
|
+
|
|
31
|
+
const child = spawn(electron, [appDir, ...args], {
|
|
32
|
+
cwd: appDir,
|
|
33
|
+
detached: true,
|
|
34
|
+
stdio: 'ignore',
|
|
35
|
+
windowsHide: true,
|
|
36
|
+
env,
|
|
37
|
+
});
|
|
38
|
+
child.on('error', (error) => {
|
|
39
|
+
console.error(`Could not start BoardClip: ${error.message}`);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
});
|
|
42
|
+
child.unref();
|
|
43
|
+
console.log('BoardClip is running in your menu bar / system tray.');
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
const { contextBridge, ipcRenderer } = require('electron');
|
|
2
|
+
|
|
3
|
+
// Bridge for the built-in editor window (editor.html). Main pushes the initial
|
|
4
|
+
// text via 'editor-init'; the renderer streams drafts (every keystroke, for
|
|
5
|
+
// crash-safety) and commits (idle/save/close, to write the clip), keyed by the
|
|
6
|
+
// session id main assigned at open.
|
|
7
|
+
let sessionId = null;
|
|
8
|
+
|
|
9
|
+
contextBridge.exposeInMainWorld('editorApi', {
|
|
10
|
+
onInit: (callback) => {
|
|
11
|
+
ipcRenderer.on('editor-init', (_, init) => {
|
|
12
|
+
sessionId = init && init.sessionId;
|
|
13
|
+
callback(init || {});
|
|
14
|
+
});
|
|
15
|
+
},
|
|
16
|
+
onFind: (callback) => {
|
|
17
|
+
const listener = (_, find) => callback(find || {});
|
|
18
|
+
ipcRenderer.on('editor-find', listener);
|
|
19
|
+
return () => ipcRenderer.removeListener('editor-find', listener);
|
|
20
|
+
},
|
|
21
|
+
onConflict: (callback) => {
|
|
22
|
+
const listener = (_, conflict) => callback(conflict || {});
|
|
23
|
+
ipcRenderer.on('editor-conflict', listener);
|
|
24
|
+
return () => ipcRenderer.removeListener('editor-conflict', listener);
|
|
25
|
+
},
|
|
26
|
+
draft: (payload) => ipcRenderer.send('editor-draft', sessionId, payload),
|
|
27
|
+
// invoke (not send): the promise resolves AFTER the clip write, with the
|
|
28
|
+
// session's current (content-addressed) id — the title-bar tag strip's
|
|
29
|
+
// commit-on-add awaits it before opening the group picker.
|
|
30
|
+
commit: (payload) => ipcRenderer.invoke('editor-commit', sessionId, payload),
|
|
31
|
+
resolveConflict: (payload) => ipcRenderer.invoke('resolve-conflict', payload),
|
|
32
|
+
unifyStep: (payload) => ipcRenderer.invoke('unify-step', sessionId, payload),
|
|
33
|
+
close: () => ipcRenderer.send('editor-close', sessionId),
|
|
34
|
+
// The find bar's mode + match case, remembered per device.
|
|
35
|
+
saveFindPrefs: (prefs) => ipcRenderer.invoke('save-settings', { find_regex: !!(prefs && prefs.regex), find_case: !!(prefs && prefs.caseSensitive) }),
|
|
36
|
+
// A drag on the idle title moves this window (main's window-drag, sender only).
|
|
37
|
+
windowDrag: (phase, dx, dy) => ipcRenderer.send('window-drag', phase, dx, dy),
|
|
38
|
+
// The bar's height + resolved colours, for the native window controls.
|
|
39
|
+
windowChrome: (chrome) => ipcRenderer.send('window-chrome', chrome),
|
|
40
|
+
// Clipboard follow: while the clipboard holds this note, saves update it.
|
|
41
|
+
focused: () => ipcRenderer.send('editor-focus', sessionId),
|
|
42
|
+
copyToClipboard: (payload) => ipcRenderer.invoke('editor-copy', sessionId, payload),
|
|
43
|
+
onClipboardState: (callback) => {
|
|
44
|
+
const listener = (_, state) => callback(state || {});
|
|
45
|
+
ipcRenderer.on('editor-clipboard', listener);
|
|
46
|
+
return () => ipcRenderer.removeListener('editor-clipboard', listener);
|
|
47
|
+
},
|
|
48
|
+
// Clip menu (shared with the popup + viewer). The clip id changes on every
|
|
49
|
+
// committed edit (content-addressed), so the menu asks for the CURRENT id.
|
|
50
|
+
currentId: () => ipcRenderer.invoke('editor-current-id', sessionId),
|
|
51
|
+
state: (id) => ipcRenderer.invoke('clip-window-state', id),
|
|
52
|
+
pin: (id, rev) => ipcRenderer.invoke('pin', id, rev),
|
|
53
|
+
groupCreate: (name) => ipcRenderer.invoke('group-create', name),
|
|
54
|
+
groupAssign: (id, group, rev) => ipcRenderer.invoke('group-assign', id, group, rev),
|
|
55
|
+
numpadAssign: (id, slot, rev) => ipcRenderer.invoke('numpad-assign', id, slot, rev),
|
|
56
|
+
numpadUnassign: (slot) => ipcRenderer.invoke('numpad-unassign', slot),
|
|
57
|
+
deleteSelf: (rev) => ipcRenderer.invoke('editor-delete-clip', sessionId, rev),
|
|
58
|
+
getColorScheme: () => ipcRenderer.invoke('get-color-scheme'),
|
|
59
|
+
onColorSchemeChanged: (callback) => {
|
|
60
|
+
const listener = (_, scheme) => callback(scheme);
|
|
61
|
+
ipcRenderer.on('color-scheme-changed', listener);
|
|
62
|
+
return () => ipcRenderer.removeListener('color-scheme-changed', listener);
|
|
63
|
+
},
|
|
64
|
+
// Appearance (accent, density, corners, theme mode, this window's surface):
|
|
65
|
+
// one event whenever any of it changes - a setting, another device, the OS accent.
|
|
66
|
+
onAppearanceChanged: (callback) => {
|
|
67
|
+
const listener = (_, look) => callback(look || {});
|
|
68
|
+
ipcRenderer.on('appearance-changed', listener);
|
|
69
|
+
return () => ipcRenderer.removeListener('appearance-changed', listener);
|
|
70
|
+
},
|
|
71
|
+
});
|
package/editor.html
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html><head><meta charset="utf-8"><title>Editor</title>
|
|
3
|
+
<script>
|
|
4
|
+
document.documentElement.dataset.theme = matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
|
|
5
|
+
</script>
|
|
6
|
+
<!-- Vendored CodeMirror 5 merge view (offline, shared with the website demo);
|
|
7
|
+
powers the shared reconciliation/unify UI. Load before clipboard-ui-core. -->
|
|
8
|
+
<link rel="stylesheet" href="site/shared/vendor/cm5/codemirror.css">
|
|
9
|
+
<link rel="stylesheet" href="site/shared/vendor/cm5/merge.css">
|
|
10
|
+
<script src="site/shared/vendor/cm5/codemirror.js"></script>
|
|
11
|
+
<script src="site/shared/vendor/cm5/diff-match-patch.js"></script>
|
|
12
|
+
<script src="site/shared/vendor/cm5/merge.js"></script>
|
|
13
|
+
<link rel="stylesheet" href="site/shared/clipboard-popup.css">
|
|
14
|
+
<link rel="stylesheet" href="site/shared/clipboard-window.css">
|
|
15
|
+
<style>
|
|
16
|
+
/* Editor window shell only; the editor itself + tokens come from the shared
|
|
17
|
+
clipboard-popup.css + Core.createEditor (single source with the demo), the
|
|
18
|
+
window base (reset, font, background, glass scrim) from clipboard-window.css. */
|
|
19
|
+
#editorHost { height:100%; }
|
|
20
|
+
</style></head><body>
|
|
21
|
+
<div id="editorHost"></div>
|
|
22
|
+
<div class="toast" id="toast">Copied</div>
|
|
23
|
+
<script src="site/shared/clip-search.js"></script>
|
|
24
|
+
<script src="site/shared/clipboard-ui-core.js"></script>
|
|
25
|
+
<script>
|
|
26
|
+
const Core = window.BoardClipCore;
|
|
27
|
+
const host = document.getElementById('editorHost');
|
|
28
|
+
|
|
29
|
+
// Theme: follow the app's theme_mode setting + live system changes, exactly
|
|
30
|
+
// like the popup (Core.applyTheme on <html>).
|
|
31
|
+
let themeMode = 'system';
|
|
32
|
+
let systemDark = matchMedia('(prefers-color-scheme: dark)').matches;
|
|
33
|
+
function refreshTheme() { Core.applyTheme(document.documentElement, themeMode, systemDark); }
|
|
34
|
+
if (window.editorApi.getColorScheme) window.editorApi.getColorScheme().then((s) => { systemDark = s !== 'light'; refreshTheme(); }).catch(() => {});
|
|
35
|
+
if (window.editorApi.onColorSchemeChanged) window.editorApi.onColorSchemeChanged((s) => { systemDark = s !== 'light'; refreshTheme(); });
|
|
36
|
+
// Later appearance changes (a setting here or on another device, the OS
|
|
37
|
+
// accent, the glass scope) arrive as the same payload as init.
|
|
38
|
+
if (window.editorApi.onAppearanceChanged) window.editorApi.onAppearanceChanged((look) => {
|
|
39
|
+
if (look.themeMode) { themeMode = (look.themeMode === 'light' || look.themeMode === 'dark') ? look.themeMode : 'system'; refreshTheme(); }
|
|
40
|
+
Core.applyAppearance(document.documentElement, look);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
let editor = null;
|
|
44
|
+
let reconcileView = null;
|
|
45
|
+
let windowControls = null; // Core.attachWindowControls, set up on init
|
|
46
|
+
let toastTimer = null;
|
|
47
|
+
function showToast(message) {
|
|
48
|
+
const toast = document.getElementById('toast');
|
|
49
|
+
if (!toast) return;
|
|
50
|
+
toast.textContent = message || 'Copied';
|
|
51
|
+
toast.classList.add('show');
|
|
52
|
+
clearTimeout(toastTimer);
|
|
53
|
+
toastTimer = setTimeout(() => toast.classList.remove('show'), 1200);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// --- Shared clip menu (pin/group/numpad/title/delete), same as the popup rows
|
|
57
|
+
// and the image viewer. Backed by a light state snapshot fetched right before
|
|
58
|
+
// the menu opens (the clip id changes on every committed edit, so the CURRENT
|
|
59
|
+
// id comes from main at open time too). context 'editor' hides "Open in editor".
|
|
60
|
+
let menuState = { item: null, items: [], groups: [], aiGroup: 'AI' };
|
|
61
|
+
let menuClipId = null;
|
|
62
|
+
// Title-bar tag strip: re-render from the CURRENT clip (the id is content-
|
|
63
|
+
// addressed, so it changes on every committed text edit — always re-query).
|
|
64
|
+
// A new note that hasn't committed yet shows just the + (commit-on-add).
|
|
65
|
+
async function refreshTags() {
|
|
66
|
+
if (!editor) return;
|
|
67
|
+
const id = await window.editorApi.currentId();
|
|
68
|
+
if (!id) { editor.setTags(null, { allowAdd: true }); return; }
|
|
69
|
+
menuClipId = id;
|
|
70
|
+
menuState = await window.editorApi.state(id);
|
|
71
|
+
editor.setTags(menuState.item, { allowAdd: true });
|
|
72
|
+
}
|
|
73
|
+
// Menu mutations send the rev from the state snapshot fetched when the menu
|
|
74
|
+
// opened; a stale one is refused. (Text commits keep their own base-text
|
|
75
|
+
// conflict handling, which forks rather than losing typed work.)
|
|
76
|
+
const clipRevOf = (id) => { const it = menuState.items.find((x) => Core.itemId(x) === id); return it ? it.rev : undefined; };
|
|
77
|
+
const revGuard = (run, fallback) => Core.guardRevision(run, { toast: showToast, refresh: () => refreshTags(), fallback });
|
|
78
|
+
const menuAdapter = {
|
|
79
|
+
menuContext: 'editor',
|
|
80
|
+
itemById: (id) => menuState.items.find((it) => Core.itemId(it) === id) || null,
|
|
81
|
+
allItems: () => menuState.items,
|
|
82
|
+
groupNames: () => menuState.groups,
|
|
83
|
+
numpadMap: () => Core.numpadMap(menuState.items),
|
|
84
|
+
protectedGroups: () => new Set([menuState.aiGroup]),
|
|
85
|
+
pin: (id) => revGuard(() => window.editorApi.pin(id, clipRevOf(id)), false),
|
|
86
|
+
toggleGroup: (id, group) => revGuard(() => window.editorApi.groupAssign(id, group, clipRevOf(id)), false),
|
|
87
|
+
createGroup: (name) => window.editorApi.groupCreate(name),
|
|
88
|
+
numpadAssign: (id, slot) => revGuard(() => window.editorApi.numpadAssign(id, slot, clipRevOf(id)), false),
|
|
89
|
+
numpadUnassign: (slot) => window.editorApi.numpadUnassign(slot),
|
|
90
|
+
// The editor's title INPUT is authoritative (it rides every commit); a menu
|
|
91
|
+
// rename would be reverted by the next commit if it only touched the clip.
|
|
92
|
+
// Set the input + commit through the session instead — one write path.
|
|
93
|
+
setClipTitle: (_id, title) => { if (editor) { editor.setTitle(title); editor.commit(); } },
|
|
94
|
+
// The clip menu's "Revert to original": the editor's own revert (one path).
|
|
95
|
+
revertClip: () => { if (editor) editor.revert(); },
|
|
96
|
+
// Delete closes the window; main suppresses the close-commit so the draft
|
|
97
|
+
// can't resurrect the clip.
|
|
98
|
+
deleteClips: async (ids) => revGuard(async () => { await window.editorApi.deleteSelf(clipRevOf((ids && ids[0]) || menuClipId)); return []; }, []),
|
|
99
|
+
// Commit-on-add for the tag strip's + on a new note: force-commit the draft
|
|
100
|
+
// through the ONE existing write path, then re-query the fresh clip id.
|
|
101
|
+
ensureClipId: async () => {
|
|
102
|
+
if (editor) await Promise.resolve(editor.commit());
|
|
103
|
+
const id = await window.editorApi.currentId();
|
|
104
|
+
if (id) await refreshTags(); // picker needs the fresh item in menuState
|
|
105
|
+
return id || null;
|
|
106
|
+
},
|
|
107
|
+
toast: (message) => showToast(message),
|
|
108
|
+
render: () => {},
|
|
109
|
+
refresh: async () => { await refreshTags(); },
|
|
110
|
+
};
|
|
111
|
+
const menuController = Core.createClipController(menuAdapter);
|
|
112
|
+
document.addEventListener('click', (e) => menuController.onClick(e));
|
|
113
|
+
async function openClipMenu(x, y) {
|
|
114
|
+
menuClipId = await window.editorApi.currentId();
|
|
115
|
+
if (!menuClipId) return; // new note not committed yet — no clip to act on
|
|
116
|
+
menuState = await window.editorApi.state(menuClipId);
|
|
117
|
+
menuController.openClipMenu(menuClipId, x, y);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
window.editorApi.onInit((init) => {
|
|
121
|
+
themeMode = (init.themeMode === 'light' || init.themeMode === 'dark') ? init.themeMode : 'system';
|
|
122
|
+
refreshTheme();
|
|
123
|
+
// The appearance payload (this window's surface, the accent colour, density,
|
|
124
|
+
// corners), through the one applier every window uses.
|
|
125
|
+
Core.applyAppearance(document.documentElement, init);
|
|
126
|
+
// The OS's own window controls: the bar reserves their room and reports its
|
|
127
|
+
// colours, so the caption buttons match it (main's setTitleBarOverlay).
|
|
128
|
+
if (!windowControls) windowControls = Core.attachWindowControls(init.windowControls, window.editorApi.windowChrome);
|
|
129
|
+
host.innerHTML = '';
|
|
130
|
+
reconcileView = null;
|
|
131
|
+
editor = Core.createEditor({
|
|
132
|
+
initialText: init.text || '',
|
|
133
|
+
initialTitle: init.noteTitle || '',
|
|
134
|
+
initialFind: init.find || '',
|
|
135
|
+
initialFindRegex: init.findRegex,
|
|
136
|
+
findPrefs: init.findPrefs,
|
|
137
|
+
saveFindPrefs: (prefs) => window.editorApi.saveFindPrefs && window.editorApi.saveFindPrefs(prefs),
|
|
138
|
+
initialFocusTitle: !!init.focusTitle,
|
|
139
|
+
nativeControls: true,
|
|
140
|
+
windowDrag: window.editorApi.windowDrag,
|
|
141
|
+
toastEl: document.getElementById('toast'), // Revert to original offers Undo here
|
|
142
|
+
onInput: (payload) => window.editorApi.draft(payload), // live crash-safe draft, every keystroke
|
|
143
|
+
// idle / Ctrl+S / close -> write the clip. The commit changes the content-
|
|
144
|
+
// addressed id, so refresh the title-bar tag strip once the write lands
|
|
145
|
+
// (returned promise also lets the strip's commit-on-add await the write).
|
|
146
|
+
onCommit: (payload) => window.editorApi.commit(payload).then((r) => { refreshTags(); return r; }),
|
|
147
|
+
onClose: () => window.editorApi.close(),
|
|
148
|
+
// While the clipboard holds this note, every save also updates it (main's
|
|
149
|
+
// clipboard follower); the footer shows that, or a Copy button to start it.
|
|
150
|
+
clipboard: window.editorApi.copyToClipboard ? {
|
|
151
|
+
following: !!init.clipboardFollowing,
|
|
152
|
+
onCopy: (payload) => window.editorApi.copyToClipboard(payload),
|
|
153
|
+
} : null,
|
|
154
|
+
// "..." in the editor chrome -> the shared clip menu (existing clips only;
|
|
155
|
+
// a brand-new note has no clip until its first commit).
|
|
156
|
+
...(init.isNew ? {} : { onMenu: (x, y) => openClipMenu(x, y) }),
|
|
157
|
+
});
|
|
158
|
+
host.appendChild(editor.el);
|
|
159
|
+
windowControls.refresh();
|
|
160
|
+
editor.focus();
|
|
161
|
+
refreshTags();
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// Clipboard follow: main pushes start/stop; regaining focus asks main to
|
|
165
|
+
// re-check (the user may have copied this note, or something else, meanwhile).
|
|
166
|
+
if (window.editorApi.onClipboardState) window.editorApi.onClipboardState((state) => { if (editor) editor.setClipboardState(state); });
|
|
167
|
+
window.addEventListener('focus', () => { if (editor && window.editorApi.focused) window.editorApi.focused(); });
|
|
168
|
+
// Right-click in the editor chrome (bar/foot — not the text area, where the
|
|
169
|
+
// native text menu belongs) opens the same clip menu.
|
|
170
|
+
document.addEventListener('contextmenu', (e) => {
|
|
171
|
+
if (e.target.closest('.bc-editor-area, .bc-bar-title, .bc-find-input, input, textarea')) return;
|
|
172
|
+
if (!editor || reconcileView) return;
|
|
173
|
+
e.preventDefault();
|
|
174
|
+
openClipMenu(e.clientX, e.clientY);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
if (window.editorApi.onFind) {
|
|
178
|
+
window.editorApi.onFind((find) => {
|
|
179
|
+
if (!editor || !find) return;
|
|
180
|
+
if (find.focusTitle && editor.focusTitle) editor.focusTitle();
|
|
181
|
+
else if (find.query) editor.openFind(find.query, find.findRegex);
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (window.editorApi.onConflict) {
|
|
186
|
+
// Mounts the SHARED reconciliation view for both a sync conflict (resolve +
|
|
187
|
+
// close) and a Unify session (record.unify: fold each step, only close on the
|
|
188
|
+
// final merge). One view, two drivers.
|
|
189
|
+
function mountReconcile(record) {
|
|
190
|
+
host.innerHTML = '';
|
|
191
|
+
editor = null;
|
|
192
|
+
reconcileView = Core.createReconciliationView({
|
|
193
|
+
record,
|
|
194
|
+
windowDrag: window.editorApi.windowDrag,
|
|
195
|
+
onResolve: async (resolution) => {
|
|
196
|
+
if (record.unify && window.editorApi.unifyStep) {
|
|
197
|
+
const next = await window.editorApi.unifyStep(resolution);
|
|
198
|
+
if (next && next.done === false && next.nextRecord) { mountReconcile(next.nextRecord); return; }
|
|
199
|
+
window.editorApi.close();
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
await window.editorApi.resolveConflict(resolution);
|
|
203
|
+
window.editorApi.close();
|
|
204
|
+
},
|
|
205
|
+
onClose: () => window.editorApi.close(),
|
|
206
|
+
nativeControls: true,
|
|
207
|
+
});
|
|
208
|
+
host.appendChild(reconcileView.el);
|
|
209
|
+
if (windowControls) windowControls.refresh();
|
|
210
|
+
}
|
|
211
|
+
window.editorApi.onConflict((record) => {
|
|
212
|
+
if (!record || !record.id) return;
|
|
213
|
+
mountReconcile(record);
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Commit any pending edit if the window is closing out from under us.
|
|
218
|
+
window.addEventListener('beforeunload', () => { if (editor) editor.commit(); });
|
|
219
|
+
</script>
|
|
220
|
+
</body></html>
|
package/icon.png
ADDED
|
Binary file
|
package/icon@2x.png
ADDED
|
Binary file
|
package/iconTemplate.png
ADDED
|
Binary file
|
|
Binary file
|