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 = (
''
);
+//DDD kbd
+lazyLoad('keybindings.js', function () {
+ const rows = [];
+ Object.entries(currentKeybindings).forEach(([key, value]) => {
+ rows.push(`| ${key} | ${value.tr} | ${value.desc || ''} |
`);
+ });
+ const table = '| ' + L.hotkey_binding + ' | ' + L.hotkey_action + ' | ' + L.hotkey_context + ' |
' + rows.join('') + '
';
+ 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