@tobq/boardclip 2.1.113 → 2.1.115
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/README.md +5 -2
- package/editor-preload.js +2 -0
- package/editor.html +12 -0
- package/index.html +115 -58
- package/lib/clipboard-model.js +588 -23
- package/lib/mcp-core.js +240 -69
- package/lib/sync-delta.js +5 -2
- package/lib/windows-hook-policy.js +245 -0
- package/lib/windows-hook-worker.js +18 -77
- package/lib/windows-hook.js +32 -9
- package/main.js +865 -202
- package/mcp/boardclip-mcp.js +68 -39
- package/mcp-approval.html +4 -2
- package/package.json +1 -1
- package/preload.js +11 -2
- package/site/index.html +108 -37
- package/site/shared/clip-search.js +291 -134
- package/site/shared/clipboard-popup.css +8 -20
- package/site/shared/clipboard-ui-core.js +405 -329
- package/viewer.html +1 -1
package/README.md
CHANGED
|
@@ -93,10 +93,13 @@ when you intentionally want Git to autostash local code changes.
|
|
|
93
93
|
| --- | --- |
|
|
94
94
|
| Cmd+Shift+V / Win+V | Toggle popup |
|
|
95
95
|
| Cmd+Numpad 1-9 / Win+Numpad 1-9 | Quick-paste slot |
|
|
96
|
-
| 1-9
|
|
96
|
+
| 1-9 right after the popup opens | Quick-paste slot |
|
|
97
|
+
| N right after the popup opens | New note |
|
|
98
|
+
| Win+N (Windows; record one on macOS in Settings) | New note |
|
|
97
99
|
| Arrow keys | Navigate items |
|
|
98
100
|
| Enter | Paste selected item |
|
|
99
101
|
| Escape | Close popup |
|
|
102
|
+
| Cmd+N / Ctrl+N in the note editor | New note |
|
|
100
103
|
|
|
101
104
|
## Development
|
|
102
105
|
|
|
@@ -148,7 +151,7 @@ kill.bat
|
|
|
148
151
|
- `preload.js`: safe renderer API bridge.
|
|
149
152
|
- `lib/clipboard-model.js`: shared pure history/pin/group merge logic.
|
|
150
153
|
- `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.
|
|
154
|
+
- `lib/windows-hook*`: Windows low-level keyboard hook for Win+V / Win+N / Numpad.
|
|
152
155
|
- `site/`: static landing page and remote install/update scripts.
|
|
153
156
|
|
|
154
157
|
## Data
|
package/editor-preload.js
CHANGED
|
@@ -31,6 +31,8 @@ contextBridge.exposeInMainWorld('editorApi', {
|
|
|
31
31
|
resolveConflict: (payload) => ipcRenderer.invoke('resolve-conflict', payload),
|
|
32
32
|
unifyStep: (payload) => ipcRenderer.invoke('unify-step', sessionId, payload),
|
|
33
33
|
close: () => ipcRenderer.send('editor-close', sessionId),
|
|
34
|
+
// Ctrl/Cmd+N here: the popup's New note, through the same IPC.
|
|
35
|
+
newNote: (options) => ipcRenderer.invoke('new-note', options || {}),
|
|
34
36
|
// The find bar's mode + match case, remembered per device.
|
|
35
37
|
saveFindPrefs: (prefs) => ipcRenderer.invoke('save-settings', { find_regex: !!(prefs && prefs.regex), find_case: !!(prefs && prefs.caseSensitive) }),
|
|
36
38
|
// A drag on the idle title moves this window (main's window-drag, sender only).
|
package/editor.html
CHANGED
|
@@ -174,6 +174,18 @@ document.addEventListener('contextmenu', (e) => {
|
|
|
174
174
|
openClipMenu(e.clientX, e.clientY);
|
|
175
175
|
});
|
|
176
176
|
|
|
177
|
+
// Ctrl+N (Cmd+N on a Mac) while this window has the focus: a new note, through
|
|
178
|
+
// the popup's own New note action (the 'new-note' IPC -> openEditor, one path).
|
|
179
|
+
// This note is saved first. A held chord opens one note, not one per key
|
|
180
|
+
// repeat; the merge view has no note, so it opens none.
|
|
181
|
+
document.addEventListener('keydown', (e) => {
|
|
182
|
+
if (!editor || !Core.newNoteKey(e)) return;
|
|
183
|
+
e.preventDefault();
|
|
184
|
+
if (e.repeat) return;
|
|
185
|
+
editor.commit();
|
|
186
|
+
window.editorApi.newNote();
|
|
187
|
+
});
|
|
188
|
+
|
|
177
189
|
if (window.editorApi.onFind) {
|
|
178
190
|
window.editorApi.onFind((find) => {
|
|
179
191
|
if (!editor || !find) return;
|
package/index.html
CHANGED
|
@@ -38,7 +38,7 @@ let groups = [];
|
|
|
38
38
|
// as tokens inside `query`; the chip bar's active/excluded state is DERIVED from it. No
|
|
39
39
|
// separate filter Sets (that dual-model was the drift Forge killed).
|
|
40
40
|
function parsedQuery() { return Core.search.parseQuery(query); }
|
|
41
|
-
function facetState() { return Core.search.facetState(parsedQuery()); }
|
|
41
|
+
function facetState() { return Core.search.facetState(parsedQuery(), groups); }
|
|
42
42
|
function activeFilterSet() { return facetState().active; }
|
|
43
43
|
// Ranking override for the Best/Recent toggle: null = default (relevance while searching,
|
|
44
44
|
// history order when idle), 'best' | 'new' when the user has flipped it for this query.
|
|
@@ -51,9 +51,11 @@ let diagnosticsEnabled = false;
|
|
|
51
51
|
let searchDocs = [], searchTextLower = [];
|
|
52
52
|
let itemLookup = new Map(), groupCounts = new Map(), builtinCounts = {}, numpadIdMap = {};
|
|
53
53
|
|
|
54
|
-
|
|
54
|
+
// New note: the header button and the popup's N quick key.
|
|
55
|
+
function newNote() {
|
|
55
56
|
window.api.newNote({ sourceGroups: Core.sourceGroupsFromFilters(activeFilterSet()) });
|
|
56
|
-
}
|
|
57
|
+
}
|
|
58
|
+
document.getElementById('newNoteBtn').addEventListener('click', newNote);
|
|
57
59
|
|
|
58
60
|
// Renderer exceptions used to vanish (a tray popup has no DevTools open): a thrown
|
|
59
61
|
// handler left the UI half-updated with nothing in the log. Route window errors +
|
|
@@ -384,7 +386,10 @@ const revGuard = (run, fallback) => Core.guardRevision(run, { toast: showToast,
|
|
|
384
386
|
const appAdapter = {
|
|
385
387
|
itemById: (id) => itemById(id),
|
|
386
388
|
numpadMap: () => numpadMap(),
|
|
387
|
-
|
|
389
|
+
// The popup's quick keys (controller.armQuickKeys): 1-9 and N right after it opens.
|
|
390
|
+
quickPaste: (slot) => window.api.numpadPasteAndHide(slot),
|
|
391
|
+
newNote: () => newNote(),
|
|
392
|
+
protectedGroups:() => new Set([aiGroupName]),
|
|
388
393
|
// The query TEXT is the source of truth: a chip click rewrites it through the
|
|
389
394
|
// controller (Search.applyFacet with the remembered or / and).
|
|
390
395
|
getQuery: () => query,
|
|
@@ -404,17 +409,20 @@ const appAdapter = {
|
|
|
404
409
|
numpadAssign: (id, slot) => revGuard(() => window.api.numpadAssign(id, slot, clipRevOf(id)), false),
|
|
405
410
|
numpadUnassign: (slot) => window.api.numpadUnassign(slot),
|
|
406
411
|
toggleGroup: (id, group) => revGuard(() => window.api.groupAssign(id, group, clipRevOf(id)), false), // applyGroupAssign toggles
|
|
407
|
-
|
|
412
|
+
// "todo, forge" creates (or reuses) both: main answers with the tag names.
|
|
413
|
+
createGroup: async (text) => { const names = await window.api.groupCreate(text); await refreshGroups({ render: false }); return names; },
|
|
408
414
|
deleteGroup: async (group) => {
|
|
409
415
|
// Drop every group: / -group: token of the deleted group from the query.
|
|
410
416
|
query = Core.search.stripFacet(query, { kind: 'group', value: group });
|
|
411
417
|
if (typeof searchBox !== 'undefined' && searchBox) searchBox.setQuery(query);
|
|
412
|
-
await window.api.groupDelete(group);
|
|
418
|
+
const result = await window.api.groupDelete(group); // { ok, name, ids, shared }: what its Undo puts back
|
|
413
419
|
await refreshGroups({ render: false });
|
|
420
|
+
return result;
|
|
414
421
|
},
|
|
422
|
+
restoreGroup: async (result) => { await window.api.groupRestore(result); await refreshGroups({ render: false }); },
|
|
415
423
|
deleteClip: (id) => revGuard(() => window.api.deleteItem(id, clipRevOf(id)), false),
|
|
416
424
|
clearUnpinned: () => window.api.deleteAll(),
|
|
417
|
-
setGroupSharedAi: (group) => window.api.setGroupSharedAi(group,
|
|
425
|
+
setGroupSharedAi: (group, shared) => window.api.setGroupSharedAi(group, shared), // shared: the switch's new state
|
|
418
426
|
// Always the id-based path: window.api.copy() only takes a string, so it
|
|
419
427
|
// would silently drop the clip's html/rtf. paste(id) puts every captured
|
|
420
428
|
// format on the clipboard for both text and image clips.
|
|
@@ -447,11 +455,7 @@ const appAdapter = {
|
|
|
447
455
|
groupAssignMany: (ids, group, shouldHave) => revGuard(() => window.api.groupAssignMany(Core.revisionTargets(ids, clipRevOf), group, shouldHave), false),
|
|
448
456
|
pasteMany: (ids) => window.api.pasteMany(ids),
|
|
449
457
|
startUnify: (ids) => window.api.startUnify(ids),
|
|
450
|
-
offerUndo: ({
|
|
451
|
-
message: `Deleted ${count} clip${count === 1 ? '' : 's'}`,
|
|
452
|
-
actionLabel: 'Undo',
|
|
453
|
-
onAction: undo,
|
|
454
|
-
}),
|
|
458
|
+
offerUndo: ({ message, undo }) => Core.showActionToast(toast, { message, actionLabel: 'Undo', onAction: undo }),
|
|
455
459
|
toast: (msg) => showToast(msg),
|
|
456
460
|
render: () => rerenderList(), // view-only re-render (expand/filter): no IPC
|
|
457
461
|
refresh: async () => { await refresh({ force: true }); if (document.getElementById('settingsView').classList.contains('show')) openSettings(); }, // after a mutation: re-fetch history THEN re-render settings if open (await so the settings panel reads rebuilt indexes, not stale ones)
|
|
@@ -486,9 +490,12 @@ document.addEventListener('wheel', (e) => controller.onWheel(e), { passive: fals
|
|
|
486
490
|
document.addEventListener('dragstart', (e) => controller.onDragstart(e));
|
|
487
491
|
document.addEventListener('mouseover', (e) => controller.onMouseover(e));
|
|
488
492
|
document.addEventListener('mouseout', (e) => controller.onMouseout(e));
|
|
493
|
+
// Any press in the popup, the search field included, ends the quick keys.
|
|
494
|
+
document.addEventListener('pointerdown', (e) => controller.onPointerdown(e), true);
|
|
489
495
|
|
|
490
|
-
// Popup reset (called by the main process when the window is shown).
|
|
496
|
+
// Popup reset (called by the main process when the window is shown and hidden).
|
|
491
497
|
window.resetPopupState = function() {
|
|
498
|
+
controller.armQuickKeys(); // 1-9 and N act until the first click or other key
|
|
492
499
|
appDialogs.dismiss();
|
|
493
500
|
controller.closeMenu(); // a popover left open at blur-hide would eat keys on re-show
|
|
494
501
|
controller.clearSelection({ paint: false });
|
|
@@ -498,6 +505,7 @@ window.resetPopupState = function() {
|
|
|
498
505
|
document.getElementById('mainView')?.classList.remove('hidden');
|
|
499
506
|
document.body.style.pointerEvents = '';
|
|
500
507
|
searchBox.closeOptions({ instant: true }); // every open starts with the options panel shut
|
|
508
|
+
cancelShortcutRecording(); // Settings closed above: a recording never outlives its row
|
|
501
509
|
window.focusSearch();
|
|
502
510
|
};
|
|
503
511
|
|
|
@@ -524,20 +532,6 @@ document.getElementById('copyDiagnostics').addEventListener('click', async () =>
|
|
|
524
532
|
});
|
|
525
533
|
|
|
526
534
|
// --- Search ---
|
|
527
|
-
searchEl.addEventListener('keydown', e => {
|
|
528
|
-
// A digit typed while a menu or popover is open belongs to it, never a
|
|
529
|
-
// quick-paste (typing "1ndex" with the + picker open pasted slot 1).
|
|
530
|
-
if (controller.isMenuOpen()) return;
|
|
531
|
-
if (query === '' && !e.altKey && !e.ctrlKey && !e.metaKey && e.key >= '1' && e.key <= '9') {
|
|
532
|
-
const n = +e.key;
|
|
533
|
-
const nmap = numpadMap();
|
|
534
|
-
if (n in nmap) {
|
|
535
|
-
e.preventDefault();
|
|
536
|
-
window.api.numpadPasteAndHide(n);
|
|
537
|
-
return;
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
});
|
|
541
535
|
const searchClear = document.getElementById('searchClear');
|
|
542
536
|
// Shared query-state change path (typed input AND autocomplete-applied tokens).
|
|
543
537
|
function onQueryChanged(v) {
|
|
@@ -748,47 +742,93 @@ function shortcutLabel(accelerator) {
|
|
|
748
742
|
.replace(/\+/g, ' + ');
|
|
749
743
|
}
|
|
750
744
|
|
|
745
|
+
// The recordable shortcuts: their Settings rows (the shared settings body)
|
|
746
|
+
// and the main-process setter each recording is saved through. canTurnOff:
|
|
747
|
+
// the shortcut may be none even though its default is a chord (the setter
|
|
748
|
+
// takes SHORTCUT_OFF; Windows' Win+N then stays Windows').
|
|
749
|
+
const SHORTCUT_OFF = 'none';
|
|
750
|
+
const SHORTCUT_KINDS = {
|
|
751
|
+
show: { status: 'shortcutStatus', record: 'shortcutRecord', reset: 'shortcutReset', set: (s) => window.api.setShowShortcut(s) },
|
|
752
|
+
quickPaste: { status: 'quickPasteStatus', record: 'quickPasteRecord', reset: 'quickPasteReset', set: (s) => window.api.setQuickPasteShortcut(s) },
|
|
753
|
+
newNote: { status: 'newNoteShortcutStatus', record: 'newNoteShortcutRecord', reset: 'newNoteShortcutReset', set: (s) => window.api.setNewNoteShortcut(s),
|
|
754
|
+
canTurnOff: (info) => !!info.newNoteDefault },
|
|
755
|
+
};
|
|
756
|
+
|
|
751
757
|
function shortcutStatusEl(kind) {
|
|
752
|
-
return document.getElementById(kind
|
|
758
|
+
return document.getElementById(SHORTCUT_KINDS[kind].status);
|
|
753
759
|
}
|
|
754
760
|
|
|
755
761
|
function shortcutRecordEl(kind) {
|
|
756
|
-
return document.getElementById(kind
|
|
762
|
+
return document.getElementById(SHORTCUT_KINDS[kind].record);
|
|
757
763
|
}
|
|
758
764
|
|
|
759
765
|
function shortcutResetEl(kind) {
|
|
760
|
-
return document.getElementById(kind
|
|
766
|
+
return document.getElementById(SHORTCUT_KINDS[kind].reset);
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// The row's reset slot holds the one action its state has: back to the
|
|
770
|
+
// default once changed (restart_alt), Turn off at a default chord that can be
|
|
771
|
+
// off (close), else nothing, the slot kept so the row never shifts.
|
|
772
|
+
function renderShortcutReset(kind, info, custom) {
|
|
773
|
+
const button = shortcutResetEl(kind);
|
|
774
|
+
if (!button.dataset.resetTitle) button.dataset.resetTitle = button.title;
|
|
775
|
+
const canTurnOff = SHORTCUT_KINDS[kind].canTurnOff;
|
|
776
|
+
const off = !custom && !!(canTurnOff && canTurnOff(info));
|
|
777
|
+
const title = off ? 'Turn off' : button.dataset.resetTitle;
|
|
778
|
+
button.dataset.apply = off ? SHORTCUT_OFF : '';
|
|
779
|
+
button.title = title;
|
|
780
|
+
button.setAttribute('aria-label', title);
|
|
781
|
+
button.querySelector('.mi').textContent = off ? 'close' : 'restart_alt';
|
|
782
|
+
button.style.visibility = custom || off ? 'visible' : 'hidden';
|
|
761
783
|
}
|
|
762
784
|
|
|
763
785
|
function setShortcutStatus(kind, message, isError = false) {
|
|
764
786
|
Core.setSettingHelp(shortcutStatusEl(kind), message, isError ? 'error' : '');
|
|
765
787
|
}
|
|
766
788
|
|
|
789
|
+
// A start waits for main to suspend the shortcuts; a stop in that wait ends
|
|
790
|
+
// the start there (the generation moved) and still resumes them.
|
|
791
|
+
let shortcutRecordStarting = false;
|
|
792
|
+
let shortcutRecordGen = 0;
|
|
767
793
|
function stopShortcutRecording({ resume = true } = {}) {
|
|
768
794
|
const kind = shortcutRecording;
|
|
795
|
+
const starting = shortcutRecordStarting;
|
|
769
796
|
shortcutRecording = null;
|
|
770
|
-
|
|
797
|
+
shortcutRecordStarting = false;
|
|
798
|
+
shortcutRecordGen += 1;
|
|
799
|
+
for (const button of Object.keys(SHORTCUT_KINDS).map(shortcutRecordEl)) {
|
|
771
800
|
if (button) button.classList.remove('recording');
|
|
772
801
|
}
|
|
773
|
-
if (resume && kind && window.api.resumeShortcuts) {
|
|
802
|
+
if (resume && (kind || starting) && window.api.resumeShortcuts) {
|
|
774
803
|
window.api.resumeShortcuts().catch(() => {});
|
|
775
804
|
}
|
|
776
805
|
return kind;
|
|
777
806
|
}
|
|
778
807
|
|
|
808
|
+
// A recording lives only while its row is on screen: Esc, the popup hiding or
|
|
809
|
+
// showing (resetPopupState) and Settings closing all cancel it the same way,
|
|
810
|
+
// which resumes the shortcuts main suspended for it and puts the row's label
|
|
811
|
+
// back. (Left running, the recorder ate every key the search got and saved the
|
|
812
|
+
// next chord as a global shortcut.)
|
|
813
|
+
async function cancelShortcutRecording() {
|
|
814
|
+
if (!shortcutRecording && !shortcutRecordStarting) return;
|
|
815
|
+
stopShortcutRecording();
|
|
816
|
+
const settings = await window.api.getSettings().catch(() => null);
|
|
817
|
+
if (settings) renderShortcutSettings(settings);
|
|
818
|
+
}
|
|
819
|
+
|
|
779
820
|
function renderShortcutSettings(settings, { clearStatus = true } = {}) {
|
|
780
821
|
const info = settings.shortcut_info || { show: 'CommandOrControl+Shift+V', custom: false };
|
|
781
822
|
const record = shortcutRecordEl('show');
|
|
782
|
-
const reset = shortcutResetEl('show');
|
|
783
823
|
record.textContent = shortcutLabel(info.show);
|
|
784
824
|
record.title = info.show;
|
|
785
825
|
record.classList.remove('recording');
|
|
786
|
-
|
|
826
|
+
renderShortcutReset('show', info, info.custom);
|
|
787
827
|
const quickRecord = shortcutRecordEl('quickPaste');
|
|
788
|
-
const quickReset = shortcutResetEl('quickPaste');
|
|
789
828
|
const quickShortcut = info.quickPaste || '';
|
|
790
829
|
if (quickShortcut) {
|
|
791
|
-
|
|
830
|
+
const numpad = /\+num[1-9]$/i.test(quickShortcut);
|
|
831
|
+
quickRecord.textContent = `${shortcutLabel(quickShortcut.replace(/\+(?:num)?[1-9]$/i, ''))} + ${numpad ? 'Num ' : ''}1-9`;
|
|
792
832
|
quickRecord.title = `${quickShortcut.replace(/[1-9]$/, '1')} ... ${quickShortcut.replace(/[1-9]$/, '9')}`;
|
|
793
833
|
} else {
|
|
794
834
|
quickRecord.textContent = 'Not set';
|
|
@@ -796,10 +836,16 @@ function renderShortcutSettings(settings, { clearStatus = true } = {}) {
|
|
|
796
836
|
}
|
|
797
837
|
quickRecord.classList.toggle('unset', !quickShortcut);
|
|
798
838
|
quickRecord.classList.remove('recording');
|
|
799
|
-
|
|
839
|
+
renderShortcutReset('quickPaste', info, info.quickPasteCustom);
|
|
840
|
+
const noteRecord = shortcutRecordEl('newNote');
|
|
841
|
+
const noteShortcut = info.newNote || '';
|
|
842
|
+
noteRecord.textContent = noteShortcut ? shortcutLabel(noteShortcut) : 'Not set';
|
|
843
|
+
noteRecord.title = noteShortcut || 'Record new note shortcut';
|
|
844
|
+
noteRecord.classList.toggle('unset', !noteShortcut);
|
|
845
|
+
noteRecord.classList.remove('recording');
|
|
846
|
+
renderShortcutReset('newNote', info, info.newNoteCustom);
|
|
800
847
|
if (clearStatus) {
|
|
801
|
-
setShortcutStatus(
|
|
802
|
-
setShortcutStatus('quickPaste', '');
|
|
848
|
+
for (const kind of Object.keys(SHORTCUT_KINDS)) setShortcutStatus(kind, '');
|
|
803
849
|
}
|
|
804
850
|
}
|
|
805
851
|
|
|
@@ -868,9 +914,7 @@ function shortcutFromEvent(e) {
|
|
|
868
914
|
|
|
869
915
|
async function applyShortcut(kind, shortcut) {
|
|
870
916
|
stopShortcutRecording({ resume: false });
|
|
871
|
-
const result = kind
|
|
872
|
-
? await window.api.setQuickPasteShortcut(shortcut)
|
|
873
|
-
: await window.api.setShowShortcut(shortcut);
|
|
917
|
+
const result = await SHORTCUT_KINDS[kind].set(shortcut);
|
|
874
918
|
if (!result || !result.ok) {
|
|
875
919
|
const settings = await window.api.getSettings().catch(() => null);
|
|
876
920
|
if (settings) renderShortcutSettings(settings, { clearStatus: false });
|
|
@@ -883,9 +927,13 @@ async function applyShortcut(kind, shortcut) {
|
|
|
883
927
|
|
|
884
928
|
async function startShortcutRecording(kind) {
|
|
885
929
|
stopShortcutRecording();
|
|
930
|
+
const gen = shortcutRecordGen;
|
|
931
|
+
shortcutRecordStarting = true;
|
|
886
932
|
if (window.api.suspendShortcuts) {
|
|
887
933
|
await window.api.suspendShortcuts().catch(() => {});
|
|
888
934
|
}
|
|
935
|
+
if (gen !== shortcutRecordGen) return; // cancelled while main suspended them (that stop resumed them)
|
|
936
|
+
shortcutRecordStarting = false;
|
|
889
937
|
shortcutRecording = kind;
|
|
890
938
|
const record = shortcutRecordEl(kind);
|
|
891
939
|
record.classList.add('recording');
|
|
@@ -893,32 +941,40 @@ async function startShortcutRecording(kind) {
|
|
|
893
941
|
setShortcutStatus(kind, '');
|
|
894
942
|
}
|
|
895
943
|
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
944
|
+
for (const kind of Object.keys(SHORTCUT_KINDS)) {
|
|
945
|
+
shortcutRecordEl(kind).addEventListener('click', () => startShortcutRecording(kind));
|
|
946
|
+
shortcutResetEl(kind).addEventListener('click', (e) => applyShortcut(kind, e.currentTarget.dataset.apply || ''));
|
|
947
|
+
}
|
|
900
948
|
|
|
901
|
-
|
|
902
|
-
|
|
949
|
+
// The recording row takes one chord: shortcut = its accelerator ('' = not a
|
|
950
|
+
// shortcut), key = its non-modifier key's name ('' = none or no name).
|
|
951
|
+
async function recordShortcut(shortcut, key) {
|
|
903
952
|
const kind = shortcutRecording;
|
|
904
|
-
|
|
905
|
-
e.stopPropagation();
|
|
906
|
-
if (e.key === 'Escape') {
|
|
907
|
-
stopShortcutRecording();
|
|
908
|
-
const settings = await window.api.getSettings().catch(() => null);
|
|
909
|
-
if (settings) renderShortcutSettings(settings);
|
|
910
|
-
return;
|
|
911
|
-
}
|
|
912
|
-
const shortcut = shortcutFromEvent(e);
|
|
953
|
+
if (!kind) return;
|
|
913
954
|
if (!shortcut) {
|
|
914
|
-
setShortcutStatus(kind,
|
|
955
|
+
setShortcutStatus(kind, key ? 'Add a shortcut modifier.' : 'Press a non-modifier key.', true);
|
|
915
956
|
return;
|
|
916
957
|
}
|
|
917
958
|
const resolved = kind === 'show' && window.api.resolveShowShortcut
|
|
918
959
|
? await window.api.resolveShowShortcut(shortcut)
|
|
919
960
|
: shortcut;
|
|
920
961
|
await applyShortcut(kind, resolved);
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
document.addEventListener('keydown', async (e) => {
|
|
965
|
+
if (!shortcutRecording) return;
|
|
966
|
+
e.preventDefault();
|
|
967
|
+
e.stopPropagation();
|
|
968
|
+
if (e.key === 'Escape') {
|
|
969
|
+
await cancelShortcutRecording();
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
972
|
+
await recordShortcut(shortcutFromEvent(e), shortcutKeyFromEvent(e));
|
|
921
973
|
}, true);
|
|
974
|
+
// Windows: every Win/Ctrl/Alt chord pressed while recording is taken by the
|
|
975
|
+
// keyboard hook before Windows acts on it (Win+1 would switch to a taskbar
|
|
976
|
+
// app and hide the popup) and arrives here instead of as a keydown.
|
|
977
|
+
if (window.api.onShortcutRecorded) window.api.onShortcutRecorded((chord) => recordShortcut(chord.shortcut, chord.key));
|
|
922
978
|
|
|
923
979
|
document.getElementById('addGroupBtn').addEventListener('click', () => controller.addGroup(null));
|
|
924
980
|
document.getElementById('syncAccounts').addEventListener('change', async (e) => {
|
|
@@ -1186,6 +1242,7 @@ async function renderConflicts() {
|
|
|
1186
1242
|
}
|
|
1187
1243
|
|
|
1188
1244
|
function closeSettings() {
|
|
1245
|
+
cancelShortcutRecording();
|
|
1189
1246
|
document.getElementById('settingsView').classList.remove('show');
|
|
1190
1247
|
document.getElementById('mainView').classList.remove('hidden');
|
|
1191
1248
|
searchEl.focus();
|