//@ts-check // NAME: Keyboard Shortcut // AUTHOR: khanhas // DESCRIPTION: Register a few more keybinds to support keyboard-driven navigation in Spotify client. /// (function KeyboardShortcut() { if (!Spicetify.Keyboard) { setTimeout(KeyboardShortcut, 1000); return; } const SCROLL_STEP = 50; /** * Register your own keybind with function `registerBind` * * Syntax: * registerBind(keyName, ctrl, shift, alt, callback) * * ctrl, shift and alt are boolean, true or false * * Valid keyName: * - BACKSPACE - C - Y - F3 * - TAB - D - Z - F4 * - ENTER - E - WINDOW_LEFT - F5 * - SHIFT - F - WINDOW_RIGHT - F6 * - CTRL - G - SELECT - F7 * - ALT - H - NUMPAD_0 - F8 * - PAUSE/BREAK - I - NUMPAD_1 - F9 * - CAPS - J - NUMPAD_2 - F10 * - ESCAPE - K - NUMPAD_3 - F11 * - SPACE - L - NUMPAD_4 - F12 * - PAGE_UP - M - NUMPAD_5 - NUM_LOCK * - PAGE_DOWN - N - NUMPAD_6 - SCROLL_LOCK * - END - O - NUMPAD_7 - ; * - HOME - P - NUMPAD_8 - = * - ARROW_LEFT - Q - NUMPAD_9 - , * - ARROW_UP - R - MULTIPLY - - * - ARROW_RIGHT - S - ADD - / * - ARROW_DOWN - T - SUBTRACT - ` * - INSERT - U - DECIMAL_POINT - [ * - DELETE - V - DIVIDE - \ * - A - W - F1 - ] * - B - X - F2 - " * * Use one of keyName as a string. If key that you want isn't in that list, * you can also put its keycode number in keyName as a number. * * callback is name of function you want your shortcut to bind to. It also * returns one KeyboardEvent parameter. * * Following are my default keybinds, use them as examples. */ // Ctrl + Tab and Ctrl + Shift + Tab to switch sidebar items registerBind("TAB", true, false, false, rotateSidebarDown); registerBind("TAB", true, true, false, rotateSidebarUp); // Ctrl + Q to open Queue page registerBind("Q", true, false, false, clickQueueButton); // Shift + H and Shift + L to go back and forward page registerBind("H", false, true, false, clickNavigatingBackButton); registerBind("L", false, true, false, clickNavigatingForwardButton); // PageUp, PageDown to focus on iframe app before scrolling registerBind("PAGE_UP", false, true, false, focusOnApp); registerBind("PAGE_DOWN", false, true, false, focusOnApp); // J and K to vertically scroll app registerBind("J", false, false, false, appScrollDown); registerBind("K", false, false, false, appScrollUp); // G and Shift + G to scroll to top and to bottom registerBind("G", false, false, false, appScrollTop); registerBind("G", false, true, false, appScrollBottom); // M to Like/Unlike track registerBind("M", false, false, false, Spicetify.Player.toggleHeart); // Forward Slash to open search page registerBind("/", false, false, false, openSearchPage); if (window.navigator.userAgent.indexOf("Win") === -1) { // CTRL + Arrow Left Next and CTRL + Arrow Right Previous Song registerBind("ARROW_RIGHT", true, false, false, nextSong); registerBind("ARROW_LEFT", true, false, false, previousSong); // CTRL + Arrow Up Increase Volume CTRL + Arrow Down Decrease Volume registerBind("ARROW_UP", true, false, false, increaseVolume); registerBind("ARROW_DOWN", true, false, false, decreaseVolume); } // F to activate Link Follow function const vim = new VimBind(); registerBind("F", false, false, false, vim.activate.bind(vim)); // Esc to cancel Link Follow vim.setCancelKey("ESCAPE"); function rotateSidebarDown() { rotateSidebar(1); } function rotateSidebarUp() { rotateSidebar(-1); } function clickQueueButton() { document.querySelector(".control-button-wrapper .spoticon-queue-16").click(); } function clickNavigatingBackButton() { document.querySelector(".main-topBar-historyButtons .main-topBar-back").click(); } function clickNavigatingForwardButton() { document.querySelector(".main-topBar-historyButtons .main-topBar-forward").click(); } function appScrollDown() { const app = focusOnApp(); if (app) { app.scrollBy(0, SCROLL_STEP); } } function appScrollUp() { const app = focusOnApp(); if (app) { app.scrollBy(0, -SCROLL_STEP); } } function appScrollBottom() { const app = focusOnApp(); app.scroll(0, app.scrollHeight); } function appScrollTop() { const app = focusOnApp(); app.scroll(0, 0); } function nextSong() { document.querySelector(".main-skipForwardButton-button").click(); } function previousSong() { document.querySelector(".main-skipBackButton-button").click(); } function increaseVolume() { Spicetify.Player.origin.setVolume(Spicetify.Player.getVolume() + 0.1); } function decreaseVolume() { Spicetify.Player.origin.setVolume(Spicetify.Player.getVolume() - 0.1); } /** * * @param {KeyboardEvent} event */ function openSearchPage(event) { const searchInput = document.querySelector(".main-topBar-topbarContentWrapper input"); if (searchInput) { searchInput.focus(); } else { const sidebarItem = document.querySelector(`.main-navBar-navBar a[href="/search"]`); if (sidebarItem) { sidebarItem.click(); } } event.preventDefault(); } /** * * @param {Spicetify.Keyboard.ValidKey} keyName * @param {boolean} ctrl * @param {boolean} shift * @param {boolean} alt * @param {(event: KeyboardEvent) => void} callback */ function registerBind(keyName, ctrl, shift, alt, callback) { const key = Spicetify.Keyboard.KEYS[keyName]; Spicetify.Keyboard.registerShortcut( { key, ctrl, shift, alt, }, (event) => { if (!vim.isActive) { callback(event); } } ); } function focusOnApp() { return document.querySelector("main .os-viewport"); } /** * @returns {number} */ function findActiveIndex(allItems) { const active = document.querySelector( ".main-navBar-navBarLinkActive, .main-collectionLinkButton-selected, .main-rootlist-rootlistItemLinkActive" ); if (!active) { return -1; } let index = 0; for (const item of allItems) { if (item === active) { return index; } index++; } } /** * * @param {1 | -1} direction */ function rotateSidebar(direction) { const allItems = document.querySelectorAll( ".main-navBar-navBarLink, .main-collectionLinkButton-collectionLinkButton, .main-rootlist-rootlistItemLink" ); const maxIndex = allItems.length - 1; let index = findActiveIndex(allItems) + direction; if (index < 0) index = maxIndex; else if (index > maxIndex) index = 0; let toClick = allItems[index]; if (!toClick.hasAttribute("href")) { toClick = toClick.querySelector(".main-rootlist-rootlistItemLink"); } toClick.click(); } })(); function VimBind() { const elementQuery = ["[href]", "button", "td.tl-play", "td.tl-number", "tr.TableRow"].join(","); const keyList = "qwertasdfgzxcvyuiophjklbnm".split(""); const lastKeyIndex = keyList.length - 1; this.isActive = false; const vimOverlay = document.createElement("div"); vimOverlay.id = "vim-overlay"; vimOverlay.style.zIndex = "9999"; vimOverlay.style.position = "absolute"; vimOverlay.style.width = "100%"; vimOverlay.style.height = "100%"; vimOverlay.style.display = "none"; vimOverlay.innerHTML = ``; document.body.append(vimOverlay); const mousetrap = new Spicetify.Mousetrap(document); mousetrap.bind(keyList, listenToKeys.bind(this), "keypress"); // Pause mousetrap event emitter const orgStopCallback = mousetrap.stopCallback; mousetrap.stopCallback = () => true; /** * * @param {KeyboardEvent} event */ this.activate = function (event) { vimOverlay.style.display = "block"; const vimkey = getVims(); if (vimkey.length > 0) { vimkey.forEach((e) => e.remove()); return; } let firstKey = 0; let secondKey = 0; getLinks().forEach((e) => { if (e.style.display === "none" || e.style.visibility === "hidden" || e.style.opacity === "0") { return; } const bound = e.getBoundingClientRect(); let owner = document.body; let top = bound.top; let left = bound.left; if ( bound.bottom > owner.clientHeight || bound.left > owner.clientWidth || bound.right < 0 || bound.top < 0 || bound.width === 0 || bound.height === 0 ) { return; } vimOverlay.append(createKey(e, keyList[firstKey] + keyList[secondKey], top, left)); secondKey++; if (secondKey > lastKeyIndex) { secondKey = 0; firstKey++; } }); this.isActive = true; setTimeout(() => (mousetrap.stopCallback = orgStopCallback.bind(mousetrap)), 100); }; /** * * @param {KeyboardEvent} event */ this.deactivate = function (event) { mousetrap.stopCallback = () => true; this.isActive = false; vimOverlay.style.display = "none"; getVims().forEach((e) => e.remove()); }; function getLinks() { const elements = Array.from(document.querySelectorAll(elementQuery)); return elements; } function getVims() { return Array.from(vimOverlay.getElementsByClassName("vim-key")); } /** * @param {KeyboardEvent} event */ function listenToKeys(event) { if (!this.isActive) { return; } const vimkey = getVims(); if (vimkey.length === 0) { this.deactivate(event); return; } for (const div of vimkey) { const text = div.innerText.toLowerCase(); if (text[0] !== event.key) { div.remove(); continue; } const newText = text.slice(1); if (newText.length === 0) { click(div.target); this.deactivate(event); return; } div.innerText = newText; } if (vimOverlay.childNodes.length === 1) { this.deactivate(event); } } function click(element) { if (element.hasAttribute("href") || element.tagName === "BUTTON") { element.click(); return; } const findButton = element.querySelector(`button[data-ta-id="play-button"]`) || element.querySelector(`button[data-button="play"]`); if (findButton) { findButton.click(); return; } alert("Let me know where you found this button, please. I can't click this for you without that information."); return; // TableCell case where play button is hidden // Index number is in first column const index = parseInt(element.firstChild.innerText) - 1; const context = getContextUri(); if (index >= 0 && context) { console.log(index); console.log(context); //Spicetify.PlaybackControl.playFromResolver(context, { index }, () => {}); return; } } function createKey(target, key, top, left) { const div = document.createElement("span"); div.classList.add("vim-key"); div.innerText = key; div.style.top = top + "px"; div.style.left = left + "px"; div.target = target; return div; } function getContextUri() { const username = __spotify.username; const activeApp = localStorage.getItem(username + ":activeApp"); if (activeApp) { try { return JSON.parse(activeApp).uri.replace("app:", ""); } catch { return null; } } return null; } /** * * @param {Spicetify.Keyboard.ValidKey} key */ this.setCancelKey = function (key) { mousetrap.bind(Spicetify.Keyboard.KEYS[key], this.deactivate.bind(this)); }; return this; }