/** * notion-enhancer: icon sets * (c) 2019 jayhxmo (https://jaymo.io/) * (c) 2020 CloudHill (https://github.com/CloudHill) * (c) 2021 dragonwocky (https://dragonwocky.me/) * (https://notion-enhancer.github.io/) under the MIT license */ const getImgData = (url) => new Promise(async (res, rej) => { const blob = await fetch(url).then((res) => res.blob()), reader = new FileReader(); reader.onload = (e) => res(e.target.result); reader.readAsDataURL(blob); }); export default async function ({ web, fs, components, notion }, db) { const recentUploads = await db.get(['recent_uploads'], []), preventQualityReduction = await db.get(['prevent_quality_reduction']), $triangleSvg = web.html` `; const customIconSets = [], customIconsFile = await db.get(['json']); if (customIconsFile?.content) { const iconsData = JSON.parse(customIconsFile.content); customIconSets.push(...(iconsData.icons || iconsData)); } const enhancerIconSets = [], enhancerIconsUrl = 'https://raw.githubusercontent.com/notion-enhancer/icons/main/'; if (await db.get(['default_sets'])) { const iconsData = await fs.getJSON(`${enhancerIconsUrl}/icons.json`); enhancerIconSets.push(...(iconsData.icons || iconsData)); } const mediaMenuSelector = '.notion-media-menu', mediaScrollerSelector = '.notion-media-menu > .notion-scroller', mediaFilterSelector = '.notion-media-menu > :first-child > :last-child', mediaLinkInputSelector = '.notion-focusable-within > input[type=url]', tabBtnSelector = (n) => `.notion-media-menu > :first-child > :first-child > :nth-child(${n})`; const renderSetTitle = async (title, loadPromises = [], $tooltip = undefined) => { const isCollapsed = await db.get(['collapsed', title], false), $title = web.html`

`, $spinner = web.html` `; web.render( $title, $triangleSvg.cloneNode(true), web.html`${title}`, $spinner ); $title.addEventListener('click', () => { const newState = $title.dataset.collapsed !== 'true'; db.set(['collapsed', title], newState); $title.dataset.collapsed = newState; }); // hide spinner after all icons finish loading // doesn't need to be waited on by renderers (async () => { await Promise.all(loadPromises); $spinner.remove(); if ($tooltip) { const $infoSvg = web.html`${await components.feather('info', { class: 'info' })}`; components.addTooltip($infoSvg, $tooltip, { offsetDirection: 'right' }); web.render($title, $infoSvg); } })(); return $title; }; const $iconsTab = web.html`
Icons
`, // actions $iconsLinkInput = web.html``, $iconsLinkSubmit = web.html``, $iconsUploadFile = web.html``, $iconsUploadSubmit = web.render( web.html``, 'Upload an image', $iconsUploadFile ), // sets $setsList = web.html`
`, // container $iconsView = web.render( web.html``, web.render( web.html`
`, web.render( web.html`
`, $iconsLinkInput, $iconsLinkSubmit ), $iconsUploadSubmit ), web.render($setsList) ); let $mediaMenu, $activeTabUnderline; const insertIconsTab = async () => { if (document.contains($mediaMenu)) return; // prevent injection into file upload menus $mediaMenu = document.querySelector(mediaMenuSelector); if (!$mediaMenu || !$mediaMenu.textContent.includes('Emoji')) return; const $emojiTab = document.querySelector(tabBtnSelector(1)), $emojiScroller = document.querySelector(mediaScrollerSelector), $emojiFilter = document.querySelector(mediaFilterSelector), $uploadTab = document.querySelector(tabBtnSelector(2)), $linkTab = document.querySelector(tabBtnSelector(3)); $uploadTab.style.display = 'none'; $linkTab.style.display = 'none'; if ($activeTabUnderline) $activeTabUnderline.remove(); $activeTabUnderline = $emojiTab.children[1] || $uploadTab.children[1] || $linkTab.children[1]; $emojiTab.after($iconsTab); $emojiScroller.after($iconsView); const renderRecentUploads = async () => { const $recentUploads = web.html`
`, loadPromises = []; for (let i = recentUploads.length - 1; i >= 0; i--) { const { signed, url } = recentUploads[i], $icon = web.html` `; web.render($recentUploads, $icon); $icon.addEventListener('click', (event) => { if (event.shiftKey) { recentUploads.splice(i, 1); db.set(['recent_uploads'], recentUploads); $icon.remove(); } else setIcon({ signed, url }); }); loadPromises.push( new Promise((res, rej) => { $icon.firstElementChild.onload = res; $icon.firstElementChild.onerror = res; }) ); } const $recentUploadsTitle = await renderSetTitle( 'Recent', loadPromises, web.html`

Click to reuse an icon
Shift-click to remove it

` ); web.render($setsList, $recentUploadsTitle, $recentUploads); }, renderIconSet = async (iconData, enhancerSet = false) => { try { const $set = web.html`
`; if (iconData.sourceUrl?.endsWith?.('/')) { iconData.sourceUrl = iconData.sourceUrl.slice(0, -1); } const loadPromises = []; for (let i = 0; i < (iconData.count || iconData.source.length); i++) { const iconUrl = iconData.sourceUrl ? Array.isArray(iconData.source) ? `${iconData.sourceUrl}/${iconData.source[i]}.${iconData.extension}` : `${iconData.sourceUrl}/${iconData.source}_${i}.${iconData.extension}` : iconData.source[i], sprite = enhancerSet ? `style=" background-image: url(${enhancerIconsUrl}${iconData.source}/sprite.png); background-position: 0 -${i * 24}px; "` : '', $img = sprite ? web.html`
` : web.html``, $icon = web.render(web.html``, $img); web.render($set, $icon); $icon.addEventListener('click', (event) => { if (!event.shiftKey) setIcon({ signed: iconUrl, url: iconUrl }); }); if (!sprite) { loadPromises.push( new Promise((res, rej) => { $img.onload = res; $img.onerror = res; }) ); } } const author = iconData.author ? iconData.authorUrl ? web.raw`by ${iconData.author} ` : web.raw`by ${web.escape(iconData.author)}` : '', $title = await renderSetTitle( `${web.escape(iconData.name)} ${author}`, loadPromises ); web.render($setsList, $title, $set); } catch (err) { console.log(err); web.render( $setsList, web.html`
Invalid set: ${web.escape(iconData?.name || 'Unknown')}
` ); } }, renderCustomIconSets = async () => { if (customIconSets.length) { web.render($setsList, web.html`
`); } await Promise.all(customIconSets.map((set) => renderIconSet(set))); }, renderEnhancerIconSets = async () => { if (enhancerIconSets.length) { web.render($setsList, web.html`
`); } await Promise.all( enhancerIconSets.map((set) => { set.sourceUrl = set.sourceUrl || enhancerIconsUrl + set.source; return renderIconSet(set, true); }) ); }; const displayIconsTab = async (force = false) => { if ($iconsTab.contains($activeTabUnderline) && !force) return; web.render($iconsTab, $activeTabUnderline); $iconsView.style.display = ''; $emojiScroller.style.display = 'none'; $emojiFilter.style.display = 'none'; web.empty($setsList); await renderRecentUploads(); await renderCustomIconSets(); await renderEnhancerIconSets(); $iconsView.querySelectorAll('.icon_sets--set').forEach(($set) => { $set.style.height = `${$set.scrollHeight}px`; }); }, displayEmojiTab = (force = false) => { if ($emojiTab.contains($activeTabUnderline) && !force) return; web.render($emojiTab, $activeTabUnderline); $iconsView.style.display = 'none'; $emojiScroller.style.display = ''; $emojiFilter.style.display = ''; }; // use onclick instead of eventlistener to override prev $iconsTab.onclick = displayIconsTab; $emojiTab.onclick = displayEmojiTab; // otherwise both may be visible on reopen displayEmojiTab(true); async function setIcon({ signed, url }) { // without this react gets upset displayEmojiTab(); $linkTab.firstChild.click(); await new Promise(requestAnimationFrame); $mediaMenu.parentElement.style.opacity = '0'; const iconUrl = preventQualityReduction ? await getImgData(signed) : url; // call native setter, imitate human input const $notionLinkInput = $mediaMenu.querySelector(mediaLinkInputSelector), proto = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value'); proto.set.call($notionLinkInput, iconUrl); const inputEvent = new Event('input', { bubbles: true }), enterKeydownEvent = new KeyboardEvent('keydown', { bubbles: true, cancelable: true, keyCode: 13, }); $notionLinkInput.dispatchEvent(inputEvent); $notionLinkInput.dispatchEvent(enterKeydownEvent); } const submitLinkIcon = () => { const url = $iconsLinkInput.firstElementChild.value; if (!url) return; const icon = { signed: notion.sign(url, notion.getPageID()), url: url }; setIcon(icon); recentUploads.push(icon); db.set(['recent_uploads'], recentUploads); }; $iconsLinkInput.onkeyup = (event) => { if (event.code === 13) submitLinkIcon(); }; $iconsLinkSubmit.onclick = submitLinkIcon; // upload file to aws, then submit link $iconsUploadSubmit.onclick = () => $iconsUploadFile.click(); $iconsUploadFile.onchange = async (event) => { const file = event.target.files[0], url = await notion.upload(file), icon = { signed: notion.sign(url, notion.getPageID()), url: url }; setIcon(icon); recentUploads.push(icon); db.set(['recent_uploads'], recentUploads); }; }; web.addDocumentObserver(insertIconsTab, [mediaMenuSelector]); }