/* * side panel * (c) 2020 dragonwocky (https://dragonwocky.me/) * (c) 2020 CloudHill * under the MIT license */ 'use strict'; const { createElement, getEnhancements } = require('../../pkg/helpers.js'), path = require('path'), fs = require('fs-extra'); module.exports = { id: 'c8b1db83-ee37-45b4-bdb3-a7f3d36113db', tags: ['extension', 'panel'], name: 'side panel', desc: 'adds a side panel to notion.', version: '1.0.0', author: 'CloudHill', hacks: { 'renderer/preload.js'(store, __exports) { // Load icons let icons = {}; (async () => { icons.doubleChevron = await fs.readFile( path.resolve(__dirname, 'icons/double-chevron.svg') ); icons.switcher = await fs.readFile( path.resolve(__dirname, 'icons/switcher.svg') ); icons.reload = await fs.readFile( path.resolve(__dirname, 'icons/reload.svg') ); })(); // Load panel mods let panelMods = getEnhancements().loaded.filter( mod => (mod.panel && (store('mods')[mod.id] || {}).enabled) ); panelMods.forEach(mod => initMod(mod)); document.addEventListener('readystatechange', (event) => { if (document.readyState !== 'complete') return false; if (panelMods.length < 1) return; const attempt_interval = setInterval(enhance, 500); function enhance() { if (!store().width) store().width = 220; let curPanel = {}; const frame = document.querySelector('.notion-frame'); if (!frame) return; clearInterval(attempt_interval); // Initialize panel const container = createElement( '
' ); const panel = createElement( `
` ); frame.after(container); container.appendChild(panel); // Panel contents const header = createElement(`
`); const toggle = createElement( `
${icons.doubleChevron}
` ); const content = createElement( '
' ); const resize = createElement(`
`); panel.append(header, content, resize); // Add switcher if there is more than one panel mods if (panelMods.length > 1) { header.addEventListener('click', renderSwitcher); const switcherIcon = createElement( `
${icons.switcher}
` ) header.appendChild(switcherIcon); } else { header.addEventListener('click', togglePanel); } header.appendChild(toggle); toggle.addEventListener('click', togglePanel); // Keybind document.addEventListener('keyup', e => { const hotkey = { code: 'Backslash', ctrlKey: true, shiftKey: true, metaKey: false, altKey: false, }; for (let prop in hotkey) if (hotkey[prop] !== e[prop]) return; togglePanel(); }); // Restore lock state if (store().locked === 'false') unlockPanel(false); else lockPanel(); enableResize(); // Attempt to load last opened mod let loaded = false; if (store().last_open) { panelMods.forEach(mod => { if (mod.id === store().last_open) { loadContent(mod); loaded = true; } }); } if (!loaded) loadContent(panelMods[0]); function loadContent(mod) { if (curPanel.js && curPanel.js.onSwitch) curPanel.js.onSwitch(); curPanel = mod.panel; store().last_open = mod.id; panel.querySelector('.enhancer-panel--title').innerHTML = mod.panel.name || mod.name; // Reload button let reloadButton = panel.querySelector('.enhancer-panel--reload-button'); if (reloadButton) reloadButton.remove(); if (mod.panel.reload) { reloadButton = createElement( `
${icons.reload}
` ) reloadButton.addEventListener('click', e => { e.stopPropagation(); loadContent(mod); }) panel.querySelector('.enhancer-panel--title').after(reloadButton); } panel.querySelector('.enhancer-panel--icon').innerHTML = mod.panel.icon; document.getElementById('enhancer-panel--content').innerHTML = mod.panel.html; panel.dataset.fullHeight = mod.panel.fullHeight || false; if (curPanel.js && curPanel.js.onLoad) curPanel.js.onLoad(); } function unlockPanel(animate) { panel.dataset.locked = 'false'; setPanelWidth(store().width); if (animate) { panel.animate( [ { opacity: 1, transform: 'none' }, { opacity: 1, transform: 'translateY(60px)', offset: 0.4}, { opacity: 0, transform: `translateX(${store().width - 30}px) translateY(60px)`}, ], { duration: 600, easing: 'ease-out' } ).onfinish = () => { panel.addEventListener('mouseover', showPanel); panel.addEventListener('mouseleave', hidePanel); } } else { panel.addEventListener('mouseover', showPanel); panel.addEventListener('mouseleave', hidePanel); } hidePanel(); if (curPanel.js && curPanel.js.onUnlock) { curPanel.js.onUnlock(); } } function lockPanel() { panel.dataset.locked = 'true'; setPanelWidth(store().width); // Reset animation styles panel.style.opacity = ''; panel.style.transform = ''; // Hover event listeners panel.removeEventListener('mouseover', showPanel); panel.removeEventListener('mouseleave', hidePanel); if (curPanel.js && curPanel.js.onLock) { curPanel.js.onLock(); } } function togglePanel(e) { if (e) e.stopPropagation(); if (isLocked()) unlockPanel(true); else lockPanel(); store().locked = panel.dataset.locked; } function showPanel() { if (!isLocked()) { panel.style.opacity = 1; panel.style.transform = 'translateY(60px)'; } } function hidePanel() { if (!isLocked()) { panel.style.opacity = 0; panel.style.transform = `translateX(${store().width - 30}px) translateY(60px)`; } } function renderSwitcherItem(mod) { if (mod.panel) { const item = createElement( `
${mod.panel.icon}
${mod.panel.name || mod.name}
` ); item.addEventListener('click', () => loadContent(mod)); return item; } } function renderSwitcher() { if (panel.querySelector('.enhancer-panel--overlay-container')) return; // Layer to close switcher const overlayContainer = createElement( '
' ); overlayContainer.addEventListener('click', hideSwitcher) document .querySelector('.notion-app-inner') .appendChild(overlayContainer); // Position switcher below header const rect = panel.querySelector('.enhancer-panel--header').getBoundingClientRect(); const div = createElement(`
`); // Render switcher const switcher = createElement( '
' ); panelMods.forEach(mod => switcher.append(renderSwitcherItem(mod)) ); overlayContainer.appendChild(div); div.firstElementChild.appendChild(switcher); // Fade in switcher.animate( [ {opacity: 0}, {opacity: 1} ], { duration: 200 } ); // Prevent panel from closing if unlocked panel.removeEventListener('mouseleave', hidePanel); } function hideSwitcher() { const overlayContainer = document.querySelector('.enhancer-panel--overlay-container'); overlayContainer.removeEventListener('click', hideSwitcher); // Fade out document.querySelector('.enhancer-panel--switcher').animate( [ {opacity: 1}, {opacity: 0} ], { duration: 200 } ).onfinish = () => overlayContainer.remove(); if (!isLocked()) panel.addEventListener('mouseleave', hidePanel); } function setPanelWidth(width) { store().width = width; panel.style.width = width + 'px'; if (isLocked()) { container.style.width = width + 'px'; frame.style.paddingRight = width + 'px'; panel.style.right = 0; } else { container.style.width = 0; frame.style.paddingRight = 0; panel.style.right = width + 'px'; } } function enableResize() { const handle = panel.querySelector('.enhancer-panel--resize div'); handle.addEventListener('mousedown', initDrag); let startX, startWidth; const div = createElement( '
' ); function initDrag(e) { startX = e.clientX; startWidth = store().width; panel.appendChild(div); // Set transitions container.style.transition = 'width 50ms ease-in'; panel.style.transition = 'width 50ms ease-in, right 50ms ease-in'; frame.style.transition = 'padding-right 50ms ease-in'; handle.style.cursor = ''; // Prevent panel from closing if unlocked panel.removeEventListener('mouseleave', hidePanel); document.body.addEventListener('mousemove', drag); document.body.addEventListener('mouseup', stopDrag); } function drag(e) { e.preventDefault(); let width = startWidth + (startX - e.clientX); if (width < 190) width = 190; if (width > 480) width = 480; setPanelWidth(width); if (curPanel.js && curPanel.js.onResize) { curPanel.js.onResize(); } } function stopDrag() { handle.style.cursor = 'col-resize'; panel.removeChild(div); // Reset transitions container.style.transition = panel.style.transition = frame.style.transition = ''; if (!isLocked()) panel.addEventListener('mouseleave', hidePanel); document.body.removeEventListener('mousemove', drag); document.body.removeEventListener('mouseup', stopDrag); } } function isLocked() { if (panel.dataset.locked === 'true') return true; else return false; } } }); // INITIALIZATION FUNCTIONS async function initMod(mod) { // load panel sites if (mod.id === '0d541743-eb2c-4d77-83a8-3b2f5e8e5dff') { panelMods = panelMods.filter(panelMod => panelMod !== mod); return panelMods.push(...initPanelSites(mod)); } try { if (typeof mod.panel === 'object') { // html mod.panel.html = await fs.readFile( path.resolve(__dirname, `../${mod.dir}/${mod.panel.html}`) ); // name if (!mod.panel.name) mod.panel.name = mod.name; // icon if (mod.panel.icon) { const iconPath = path.resolve(__dirname, `../${mod.dir}/${mod.panel.icon}`); if (await fs.pathExists(iconPath)) mod.panel.icon = await fs.readFile(iconPath); } else { mod.panel.icon = mod.panel.name[0]; } // js if (mod.panel.js) { const jsPath = `../${mod.dir}/${mod.panel.js}`; if (await fs.pathExists(path.resolve(__dirname, jsPath))) { mod.panel.js = require(jsPath)(loadStore(mod), __exports); } } } else if (typeof mod.panel === 'string') { mod.panel.icon = mod.name[0]; mod.panel.html = await fs.readFile( path.resolve(__dirname, `../${mod.dir}/${mod.panel}`) ); } else throw Error; } catch (err) { console.log('invalid panel mod: ' + mod.name); panelMods = panelMods.filter(panelMod => panelMod !== mod); } } function initPanelSites(mod) { let panelSites = []; const sitesPath = store(mod.id).sites; if (sitesPath) { try { const sites = require(sitesPath); const invalid = false; const sitePanelJs = require('../panel-sites/panel.js')(loadStore(mod), __exports); const frameUrl = function(url, mobile) { if (!/(^https?:\/\/)/.test(url)) url = 'https://' + url; return ``; } sites.forEach(site => { if (site.url && site.name) { // get url and icon const iframe = frameUrl(site.url, site.mobile); const icon = ``; const panelMod = { id: `${mod.id}-${site.url}`, panel: { name: site.name, html: iframe, icon: icon, js: sitePanelJs, fullHeight: true, reload: true, }, } panelSites.push(panelMod); } else invalid = true; }); if (invalid) throw Error; } catch (err) { console.log('panel site error'); } } return panelSites; } function loadStore(mod) { return (...args) => { if (!args.length) return store(mod.id, mod.defaults); if (args.length === 1 && typeof args[0] === 'object') return store(mod.id, { ...mod.defaults, ...args[0] }); const other_mod = modules.find((m) => m.id === args[0]); return store(args[0], { ...(other_mod ? other_mod.defaults : {}), ...(args[1] || {}) }) } } }, }, };