diff --git a/copyparty/__init__.py b/copyparty/__init__.py index 6baaaf5f..4d5221bf 100644 --- a/copyparty/__init__.py +++ b/copyparty/__init__.py @@ -123,6 +123,7 @@ web/ui.css web/up2k.js web/util.js web/w.hash.js +web/keybindings.js """ RES = set(zs.strip().split("\n")) RESM = { diff --git a/copyparty/web/browser.html b/copyparty/web/browser.html index b467af72..d4559fe0 100644 --- a/copyparty/web/browser.html +++ b/copyparty/web/browser.html @@ -68,6 +68,7 @@
+

🌲 diff --git a/copyparty/web/browser.js b/copyparty/web/browser.js index 090ae45b..b491f05c 100644 --- a/copyparty/web/browser.js +++ b/copyparty/web/browser.js @@ -90,6 +90,48 @@ if (1) ] ], + "hotkeys": { + "ESC": "close various things", + "ENTER": "open selected file", + "HELP": "open help", + "PREV_SONG": "prev song", + "NEXT_SONG": "next song", + "TREE_PREV": "go to the previous dir", + "TREE_NEXT": "go to the next dir", + "PLAY_PAUSE": "play/pause", + "DOWNLOAD": "download selected files", + "CUT": "cut selected file(s)", + "COPY": "copy selected file(s)", + "PASTE": "paste (move/copy) here", + "DELETE": "delete selected file(s)", + "RENAME": "rename selected file(s)", + "TOGGLE_BREADCRUMBS": "toggle breadcrumbs / navpane", + "TOGGLE_GRID": "toggle grid / list view", + "TOGGLE_THUMBNAILS": "toggle thumbnails / icons", + "TOGGLE_FOLDER_TREE": "toggle folders / textfiles in navpane", + "FAST_FORWARD": "skip 10sec forward", + "REWIND": "skip 10sec back", + "NAVPANE_SHRINK": "shrink navpane", + "NAVPANE_GROW": "grow navpane", + "NAVPANE_SHRINK_DESC": "While in grid mode with nav panel visible", + "NAVPANE_GROW_DESC": "While in grid mode with nav panel visible", + "SELECT_SONG": "Select playing song", + "SELECT_SONG_DESC": "While audio is playing", + "SELECT_FILE": "Select file", + "SELECT_FILE_DESC": "While in file list", + "EDIT_FILE": "Edit file", + "EDIT_FILE_DESC": "While in file list", + "TOGGLE_GRID_MULTISELECT": "Toggle grid multi select", + "TOGGLE_GRID_MULTISELECT_DESC": "While in grid mode", + "GRID_SHRINK_THUMBNAILS": "Shrink thumbnails", + "GRID_SHRINK_THUMBNAILS_DESC": "While in grid mode", + "GRID_GROW_THUMBNAILS": "Grow thumbnails", + "GRID_GROW_THUMBNAILS_DESC": "While in grid mode", + }, + "hotkey_binding": "Hotkey keybindings", + "hotkey_action": "Hotkey action", + "hotkey_context": "Context", + "m_ok": "OK", "m_ng": "Cancel", @@ -712,6 +754,7 @@ ebi('ops').innerHTML = ( '📟' + '🎺' + '⚙️' + + '⚙️' + (IE ? '' + L.ot_noie + '' : '') + '
' ); @@ -963,6 +1006,16 @@ ebi('op_cfg').innerHTML = ( '

' + L.cl_hiddenc + '  ' + (MOBILE ? '' + L.cl_hidec + ' / ' : '') + '' + L.cl_reset + '

' ); +//DDD kbd +lazyLoad('keybindings.js', function () { + const rows = []; + Object.entries(currentKeybindings).forEach(([key, value]) => { + rows.push(`${key}${value.tr}${value.desc || ''}`); + }); + const table = '' + rows.join('') + '
' + L.hotkey_binding + '' + L.hotkey_action + '' + L.hotkey_context + '
'; + ebi('op_hotkey').innerHTML = '
' + table + '
'; +}); + // navpane ebi('tree').innerHTML = ( @@ -1003,6 +1056,7 @@ QS('#op_msg input[type="submit"]').value = L.ab_msg; function opclick(e) { var dest = this.getAttribute('data-dest'); + console.log('Clicked on the top thing with dest', dest); if (QS('#op_' + dest + '.act')) dest = ''; @@ -5880,24 +5934,25 @@ var ahotkeys = function (e) { if (QS('#bbox-overlay.visible') || modal.busy) return; - var k = (e.key || e.code) + '', pos = -1, n, - ae = document.activeElement, - aet = ae && ae != document.body ? ae.nodeName.toLowerCase() : ''; - if (k.startsWith('Key')) - k = k.slice(3); - else if (k.startsWith('Digit')) - k = k.slice(5); + var kkey = (e.key || e.code) + '', pos = -1, n, + activeElement = document.activeElement, + activeNode = activeElement && activeElement != document.body ? activeElement.nodeName.toLowerCase() : ''; - var kl = k.toLowerCase(); + if (kkey.startsWith('Key')) + kkey = kkey.slice(3); + else if (kkey.startsWith('Digit')) + kkey = kkey.slice(5); + + var keyLower = kkey.toLowerCase(); if (dbg_kbd) - console.log('KBD', k, kl, e.key, e.code, e.keyCode, e.which); + console.log('KBD', kkey, keyLower, e.key, e.code, e.keyCode, e.which); if (konmai < 0) noop(); - else if (konmak[konmai] != kl) - konmai = konmai && kl == konmak[0] ? (konmai<3?konmai:1):0; + else if (konmak[konmai] != keyLower) + konmai = konmai && keyLower == konmak[0] ? (konmai<3?konmai:1):0; else if (++konmai >= konmak.length) { konmai = -1; document.documentElement.scrollTop = 0; @@ -5912,62 +5967,46 @@ var ahotkeys = function (e) { return ev(e); } - if (k == 'Escape' || k == 'Esc') { - ae && ae.blur(); - tt.hide(); + var in_ftab = (activeNode == 'tr' || activeNode == 'td') && activeElement.closest('#files'); - if (ebi('hkhelp')) - return qsr('#hkhelp'); + //QQQ + const action = getHotkeyEvent(e, { + activeElement, + activeNode, + in_ftab, + treectl, + gridMode: thegrid.en, + mp, + showfile + }); - if (toast.visible) - return toast.hide(); + if (!action) return; - if (ebi('rn_cancel')) - return ebi('rn_cancel').click(); - - if (ebi('sh_abrt')) - return ebi('sh_abrt').click(); - - if (QS('.opview.act')) - return QS('#ops>a').click(); - - if (widget.is_open) - return widget.close(); - - if (showfile.active()) - return thegrid.setvis(true); - - if (!treectl.hidden) - return treectl.detree(); - - if (QS('#unsearch')) - return QS('#unsearch').click(); - - if (thegrid.en) - return ebi('griden').click(); + if (action === HOTKEY_ACTIONS.ESCAPE) { + return escape(); } - var in_ftab = (aet == 'tr' || aet == 'td') && ae.closest('#files'); + //TODO Ftab in context. can listen for these if (in_ftab) { var d = '', rem = 0; - if (aet == 'td') ae = ae.closest('tr'); //ie11 - if (k == 'ArrowUp' || k == 'Up') d = 'previous'; - if (k == 'ArrowDown' || k == 'Down') d = 'next'; - if (k == 'PageUp') { d = 'previous'; rem = 0.6; } - if (k == 'PageDown') { d = 'next'; rem = 0.6; } + if (activeNode == 'td') activeElement = activeElement.closest('tr'); //ie11 + if (kkey == 'ArrowUp' || kkey == 'Up') d = 'previous'; + if (kkey == 'ArrowDown' || kkey == 'Down') d = 'next'; + if (kkey == 'PageUp') { d = 'previous'; rem = 0.6; } + if (kkey == 'PageDown') { d = 'next'; rem = 0.6; } if (d) { - fselfunw(e, ae, d, rem); + fselfunw(e, activeElement, d, rem); return ev(e); } - if (k == 'Space' || k == 'Spacebar' || k == ' ') { - clmod(ae, 'sel', 't'); - msel.origin_tr(ae); + if (kkey == 'Space' || kkey == 'Spacebar' || kkey == ' ') { + clmod(activeElement, 'sel', 't'); + msel.origin_tr(activeElement); msel.selui(); return ev(e); } } - if (in_ftab || !aet || (ae && ae.closest('#ggrid'))) { - if ((kl == 'a') && ctrl(e)) { + if (in_ftab || !activeNode || (activeElement && activeElement.closest('#ggrid'))) { + if ((keyLower == 'a') && ctrl(e)) { var ntot = treectl.lsc.files.length + treectl.lsc.dirs.length, sel = msel.getsel(), all = msel.getall(); @@ -5982,124 +6021,111 @@ var ahotkeys = function (e) { } } - if (ae && ae.closest('pre')) { - if ((kl == 'a') && ctrl(e)) { - var sel = document.getSelection(), - ran = document.createRange(); + //TODO does not work + if (action === HOTKEY_ACTIONS.SELECT_ALL) { + var sel = document.getSelection(), + ran = document.createRange(); - sel.removeAllRanges(); - ran.selectNode(ae.closest('pre')); - sel.addRange(ran); - return ev(e); - } + sel.removeAllRanges(); + ran.selectNode(activeElement.closest('pre')); + sel.addRange(ran); + return ev(e); } - if (k.endsWith('Enter') && ae && (ae.onclick || ae.hasAttribute('tabIndex'))) - return ev(e) && ae.click() || true; + if (action === HOTKEY_ACTIONS.ENTER && activeElement && (activeElement.onclick || activeElement.hasAttribute('tabIndex'))) + return ev(e) && activeElement.click() || true; - if (aet && aet != 'a' && aet != 'tr' && aet != 'td' && aet != 'div' && aet != 'pre') - return; - - if (k == '?') + if (action === HOTKEY_ACTIONS.HELP) return hkhelp(); - if (!e.shiftKey && ctrl(e)) { - var sel = window.getSelection && window.getSelection() || {}; - sel = sel && !sel.isCollapsed && sel.direction != 'none'; - - if (kl == 'x') - return fileman.cut(e); - - if (kl == 'c' && !sel) - return fileman.cpy(e); - - if (kl == 'v') - return fileman.d_paste(e); - - if (kl == 'k') - return fileman.delete(e); - - return; + if (action === HOTKEY_ACTIONS.CUT) { + return fileman.cut(e); + } + if (action === HOTKEY_ACTIONS.COPY) { + return fileman.cpy(e); + } + if (action === HOTKEY_ACTIONS.PASTE) { + return fileman.d_paste(e); + } + if (action === HOTKEY_ACTIONS.DELETE) { + return fileman.delete(e); } - if (e.shiftKey && kl != 'a' && kl != 'd') - return; + if (action === HOTKEY_ACTIONS.PREV_SONG) { + return prev_song() || true; + } + if (action === HOTKEY_ACTIONS.NEXT_SONG) { + return next_song() || true; + } + if (action === HOTKEY_ACTIONS.TREE_PREV) { + return tree_neigh(-1, 1) || true; + } + if (action === HOTKEY_ACTIONS.TREE_NEXT) { + return tree_neigh(1, 1) || true; + } - if (/^[0-9]$/.test(k)) - pos = parseInt(k) * 0.1; + if (action === HOTKEY_ACTIONS.PLAY_PAUSE) + return playpause() || true; + + //TODO uncovered use case + if (/^[0-9]$/.test(kkey)) + pos = parseInt(kkey) * 0.1; if (pos !== -1) return seek_au_mul(pos) || true; - if (kl == 'j') - return prev_song() || true; + if (action === HOTKEY_ACTIONS.FAST_FORWARD) + return seek_au_rel(10) || true; + if (action === HOTKEY_ACTIONS.REWIND) + return seek_au_rel(-10) || true; - if (kl == 'l') - return next_song() || true; - - if (kl == 'p') - return playpause() || true; - - n = kl == 'u' ? -10 : kl == 'o' ? 10 : 0; - if (n !== 0) - return seek_au_rel(n) || true; - - if (kl == 'y') + if (action === HOTKEY_ACTIONS.DOWNLOAD) return msel.getsel().length ? ebi('seldl').click() : showfile.active() ? ebi('dldoc').click() : dl_song(); - n = kl == 'i' ? -1 : kl == 'k' ? 1 : 0; - if (n !== 0) - return tree_neigh(n, 1); - - if (kl == 'm') + if (action === HOTKEY_ACTIONS.TREE_UP) return tree_up(); - if (kl == 'b') + if (action === HOTKEY_ACTIONS.TOGGLE_BREADCRUMBS) return treectl.hidden ? treectl.entree() : treectl.detree(); - if (kl == 'g') + if (action === HOTKEY_ACTIONS.TOGGLE_GRID) return ebi('griden').click(); - if (kl == 't') + if (action === HOTKEY_ACTIONS.TOGGLE_THUMBNAILS) return ebi('thumbs').click(); - if (kl == 'v') + if (action === HOTKEY_ACTIONS.TOGGLE_FOLDER_TREE) return ebi('filetree').click(); - if (k == 'F2') + if (action === HOTKEY_ACTIONS.RENAME) return fileman.rename(); - if (!treectl.hidden && (!e.shiftKey || !thegrid.en)) { - if (kl == 'a') - return QS('#twig').click(); + if (action === HOTKEY_ACTIONS.NAVPANE_SHRINK) + return QS('#twig').click(); + if (action === HOTKEY_ACTIONS.NAVPANE_GROW) + return QS('#twobytwo').click(); + + if (action === HOTKEY_ACTIONS.SELECT_FILE) + showfile.tglsel(); + if (action === HOTKEY_ACTIONS.EDIT_FILE && ebi('editdoc').style.display != 'none') + ebi('editdoc').click(); - if (kl == 'd') - return QS('#twobytwo').click(); + if (action === HOTKEY_ACTIONS.SELECT_SONG) { + return sel_song(); } - if (showfile.active()) { - if (kl == 's') - showfile.tglsel(); - if (kl == 'e' && ebi('editdoc').style.display != 'none') - ebi('editdoc').click(); + if (action === HOTKEY_ACTIONS.TOGGLE_GRID_MULTISELECT) { + return ebi('gridsel').click(); } - if (mp && mp.au && !mp.au.paused) { - if (kl == 's') - return sel_song(); + if (action === HOTKEY_ACTIONS.GRID_SHRINK_THUMBNAILS) { + return QSA('#ghead a[z]')[0].click(); } - - if (thegrid.en) { - if (kl == 's') - return ebi('gridsel').click(); - - if (kl == 'a') - return QSA('#ghead a[z]')[0].click(); - - if (kl == 'd') - return QSA('#ghead a[z]')[1].click(); + + if (action === HOTKEY_ACTIONS.GRID_GROW_THUMBNAILS) { + return QSA('#ghead a[z]')[1].click(); } }; @@ -9380,6 +9406,42 @@ function reload_browser() { thegrid.setdirty(); msel.render(); } + +function escape() { + window.activeElement && window.activeElement.blur(); + tt.hide(); + + if (ebi('hkhelp')) + return qsr('#hkhelp'); + + if (toast.visible) + return toast.hide(); + + if (ebi('rn_cancel')) + return ebi('rn_cancel').click(); + + if (ebi('sh_abrt')) + return ebi('sh_abrt').click(); + + if (QS('.opview.act')) + return QS('#ops>a').click(); + + if (widget.is_open) + return widget.close(); + + if (showfile.active()) + return thegrid.setvis(true); + + if (!treectl.hidden) + return treectl.detree(); + + if (QS('#unsearch')) + return QS('#unsearch').click(); + + if (thegrid.en) + return ebi('griden').click(); +} + treectl.hydrate(); if (!fullui && (window.ui_nombar || /[?&]nombar\b/.exec(sloc0))) ebi('ops').style.display = 'none'; diff --git a/copyparty/web/keybindings.js b/copyparty/web/keybindings.js new file mode 100644 index 00000000..76a69a02 --- /dev/null +++ b/copyparty/web/keybindings.js @@ -0,0 +1,291 @@ +const HOTKEY_ACTIONS = { + ESCAPE: { + code: "escape", + tr: L.hotkeys.ESC, + }, + ENTER: { + code: "enter", + tr: L.hotkeys.ENTER, + }, + HELP: { + code: "?", + tr: L.hotkeys.HELP, + }, + PREV_SONG: { + code: "prevSong", + tr: L.hotkeys.PREV_SONG, + }, + NEXT_SONG: { + code: "nextSong", + tr: L.hotkeys.NEXT_SONG, + }, + TREE_PREV: { + code: "treePrev", + tr: L.hotkeys.TREE_PREV, + }, + TREE_NEXT: { + code: "treeNext", + tr: L.hotkeys.TREE_NEXT, + }, + TREE_UP: { + code: "treeUp", + tr: L.hotkeys.TREE_UP, + }, + PLAY_PAUSE: { + code: "playPause", + tr: L.hotkeys.PLAY_PAUSE, + }, + DOWNLOAD: { + code: "download", + tr: L.hotkeys.DOWNLOAD, + }, + CUT: { + code: "cut", + tr: L.hotkeys.CUT, + }, + COPY: { + code: "copy", + tr: L.hotkeys.COPY, + context: (ctx) => { + var sel = window.getSelection && window.getSelection() || {}; + sel = sel && !sel.isCollapsed && sel.direction != 'none'; + return !sel; + }, + }, + PASTE: { + code: "paste", + tr: L.hotkeys.PASTE, + }, + DELETE: { + code: "delete", + tr: L.hotkeys.DELETE, + }, + FAST_FORWARD: { + code: "fastForward", + tr: L.hotkeys.FAST_FORWARD, + }, + REWIND: { + code: "rewind", + tr: L.hotkeys.REWIND, + }, + TOGGLE_BREADCRUMBS: { + code: "toggleBreadcrumbs", + tr: L.hotkeys.TOGGLE_BREADCRUMBS, + }, + TOGGLE_GRID: { + code: "toggleGrid", + tr: L.hotkeys.TOGGLE_GRID, + }, + TOGGLE_THUMBNAILS: { + code: "toggleThumbnails", + tr: L.hotkeys.TOGGLE_THUMBNAILS, + }, + TOGGLE_FOLDER_TREE: { + code: "toggleFolderTree", + tr: L.hotkeys.TOGGLE_FOLDER_TREE, + }, + RENAME: { + code: "rename", + tr: L.hotkeys.RENAME, + }, + NAVPANE_SHRINK: { + code: "navpaneShrink", + tr: L.hotkeys.NAVPANE_SHRINK, + context: (ctx) => { + return !ctx.treectl.hidden && ctx.gridMode; + }, + desc: L.hotkeys.NAVPANE_SHRINK_DESC + }, + NAVPANE_GROW: { + code: "navpaneGrow", + tr: L.hotkeys.NAVPANE_GROW, + context: (ctx) => { + return !ctx.treectl.hidden && ctx.gridMode; + }, + desc: L.hotkeys.NAVPANE_GROW_DESC + }, + SELECT_ALL: { + code: "selectAll", + tr: L.hotkeys.SELECT_ALL, + context: (ctx) => { + return ctx.activeElement && ctx.activeElement.closest('pre') + }, + }, + SELECT_SONG: { + code: "selectSong", + tr: L.hotkeys.SELECT_SONG, + context: (ctx) => { + return ctx.mp && ctx.mp.au && !ctx.mp.au.paused; + }, + desc: L.hotkeys.SELECT_SONG_DESC + }, + SELECT_FILE: { + code: "selectFile", + tr: L.hotkeys.SELECT_FILE, + context: (ctx) => { + return ctx.showfile.active(); + }, + desc: L.hotkeys.SELECT_FILE_DESC + }, + EDIT_FILE: { + code: "editFile", + tr: L.hotkeys.EDIT_FILE, + context: (ctx) => { + return ctx.showfile.active(); + }, + desc: L.hotkeys.EDIT_FILE_DESC + }, + + TOGGLE_GRID_MULTISELECT: { + code: "toggleGridMultiSelect", + tr: L.hotkeys.TOGGLE_GRID_MULTISELECT, + context: (ctx) => { + return ctx.gridMode; + }, + desc: L.hotkeys.TOGGLE_GRID_MULTISELECT_DESC + }, + GRID_SHRINK_THUMBNAILS: { + code: "gridShrinkThumbnails", + tr: L.hotkeys.GRID_SHRINK_THUMBNAILS, + context: (ctx) => { + return ctx.gridMode; + }, + desc: L.hotkeys.GRID_SHRINK_THUMBNAILS_DESC + }, + GRID_GROW_THUMBNAILS: { + code: "gridGrowThumbnails", + tr: L.hotkeys.GRID_GROW_THUMBNAILS, + context: (ctx) => { + return ctx.gridMode; + }, + desc: L.hotkeys.GRID_GROW_THUMBNAILS_DESC + }, +} + +const defaultKeybindings = { + "escape": HOTKEY_ACTIONS.ESCAPE, + "enter": HOTKEY_ACTIONS.ENTER, + "shift+?": HOTKEY_ACTIONS.HELP, + "?": HOTKEY_ACTIONS.HELP, + "J": HOTKEY_ACTIONS.PREV_SONG, + "L": HOTKEY_ACTIONS.NEXT_SONG, + "K": HOTKEY_ACTIONS.TREE_NEXT, + "I": HOTKEY_ACTIONS.TREE_PREV, + "P": HOTKEY_ACTIONS.PLAY_PAUSE, + "Y": HOTKEY_ACTIONS.DOWNLOAD, + "ctrl+X": HOTKEY_ACTIONS.CUT, + "ctrl+C": HOTKEY_ACTIONS.COPY, + "ctrl+V": HOTKEY_ACTIONS.PASTE, + "ctrl+K": HOTKEY_ACTIONS.DELETE, + "meta+X": HOTKEY_ACTIONS.CUT, + "meta+C": HOTKEY_ACTIONS.COPY, + "meta+V": HOTKEY_ACTIONS.PASTE, + "meta+K": HOTKEY_ACTIONS.DELETE, + "O": HOTKEY_ACTIONS.FAST_FORWARD, + "U": HOTKEY_ACTIONS.REWIND, + "M": HOTKEY_ACTIONS.TREE_UP, + "B": HOTKEY_ACTIONS.TOGGLE_BREADCRUMBS, + "G": HOTKEY_ACTIONS.TOGGLE_GRID, + "T": HOTKEY_ACTIONS.TOGGLE_THUMBNAILS, + "V": HOTKEY_ACTIONS.TOGGLE_FOLDER_TREE, + "F2": HOTKEY_ACTIONS.RENAME, + "A": HOTKEY_ACTIONS.NAVPANE_SHRINK, + "D": HOTKEY_ACTIONS.NAVPANE_GROW, + "ctrl+A": HOTKEY_ACTIONS.SELECT_ALL, + "S": HOTKEY_ACTIONS.SELECT_SONG, + "E": HOTKEY_ACTIONS.EDIT_FILE, + "S": HOTKEY_ACTIONS.SELECT_FILE, + "shift+S": HOTKEY_ACTIONS.TOGGLE_GRID_MULTISELECT, + "shift+A": HOTKEY_ACTIONS.GRID_SHRINK_THUMBNAILS, + "shift+D": HOTKEY_ACTIONS.GRID_GROW_THUMBNAILS, +}; +//TODO bit of a mess +const currentKeybindings = readKeybindings(); +const keybindingTree = toTree(currentKeybindings); + +/** + * Reads the keybindings from localStorage + * @returns {object} Current user Keybidnds + */ +function readKeybindings() { + const s = sread("keybindings"); + if (!s) { + return defaultKeybindings; + } + + return JSON.parse(s); +} + +/** + * Resets the keybindings to default + * @returns {object} Default keybindings + */ +function resetKeybindings() { + srem("keybindings"); + return defaultKeybindings; +} + +/** + * Sets a keybinding for a specific action + * @param {string} combo Key combination + * @param {function} callback Callback function + */ +function setKeybinding(event) { + console.log('Not implemented setKeybinding', event); + // sset("keybindings", JSON.stringify(keybindings)); +} + +function getHotkeyEvent(event, context = {}) { + const hash = event.ctrlKey << 2 | event.metaKey << 1 | event.shiftKey; + let kkey = (event.key || event.code) + ''; + const matchingHotkeys = keybindingTree[hash][kkey.toLowerCase()] || []; + console.log('getHotkeyEvent', event, context, hash, matchingHotkeys); + if (matchingHotkeys.length < 2) { + return matchingHotkeys[0]; + } + + var validIndex = -1; + for (var i = 0; i < matchingHotkeys.length; i++) { + var hotkey = matchingHotkeys[i]; + if (hotkey.context) { + if (hotkey.context(context)) return hotkey; + } else { + validIndex = i; + } + } + + console.log('More than 1 matching hotkey without sufficient context check found', event, context, hash, matchingHotkeys); + return matchingHotkeys[validIndex]; +} + +/** + * Convers human readable keybindings into a tree structure for fast lookup + * @param {object} keybindings + * @see getHotkeyEvent for usage + * @returns {object[]} tree of keybindings for fast lookup + */ +function toTree(keybindings) { + var tree = [...Array(8)].map(() => ({})); + + for (var combo in keybindings) { + var action = keybindings[combo]; + + var parts = combo.split("+").map(function (p) { + return p.trim().toLowerCase(); + }); + + var hash = 0; + hash += parts.indexOf("shift") !== -1 ? 1 : 0; + hash += parts.indexOf("ctrl") !== -1 ? 2 : 0; + hash += parts.indexOf("meta") !== -1 ? 4 : 0; + var key = parts[parts.length - 1]; // last part is the key + + console.log('binding', combo, action, hash, key); + if (!tree[hash][key]) tree[hash][key] = []; + tree[hash][key].push(action); + } + + console.log('Tree', keybindings, '=>', tree); + + return tree; +} \ No newline at end of file diff --git a/copyparty/web/util.js b/copyparty/web/util.js index a63f25b1..5752ce35 100644 --- a/copyparty/web/util.js +++ b/copyparty/web/util.js @@ -2330,3 +2330,26 @@ function xhrchk(xhr, prefix, e404, lvl, tag) { return fun(0, prefix + xhr.status + ": " + errtxt, tag); } + +/** + * Lazy load a script from the server + * @param {string} scriptName name of the script you want to load + * @param {function|undefined} callback callback to run when the script is loaded + */ +function lazyLoad(scriptName, callback) { + const scripts = Array.from(QSA('script')); + if (scripts.some(s => s.src.endsWith(scriptName))) { + callback && callback(); + return; + } + + var s = document.createElement('script'); + s.src = '/.cpr/' + scriptName; + s.onload = function () { + callback && callback(); + }; + s.onerror = function (e) { + console.log('failed loading ' + scriptName, e); + }; + document.head.appendChild(s); +} \ No newline at end of file diff --git a/scripts/sfx.ls b/scripts/sfx.ls index 59a13916..fbe93005 100644 --- a/scripts/sfx.ls +++ b/scripts/sfx.ls @@ -137,3 +137,4 @@ copyparty/web/ui.css, copyparty/web/up2k.js, copyparty/web/util.js, copyparty/web/w.hash.js, +copyparty/web/keybindings.js, \ No newline at end of file