From 96988d67e0c80cafadd91de6ad5d81cb59eb8093 Mon Sep 17 00:00:00 2001 From: dragonwocky Date: Sat, 30 Oct 2021 19:14:22 +1100 Subject: [PATCH] extension: icon sets part 1 (save/reuse recent uploads) --- repo/icon-sets/app.css | 411 ++++++++++++++++++++++++++++++ repo/icon-sets/client.css | 179 +++++++++++++ repo/icon-sets/client.mjs | 182 +++++++++++++ repo/icon-sets/icons/remove.svg | 3 + repo/icon-sets/icons/restore.svg | 3 + repo/icon-sets/icons/search.svg | 3 + repo/icon-sets/icons/triangle.svg | 1 + repo/icon-sets/mod.js | 185 ++++++++++++++ repo/icon-sets/mod.json | 35 +++ repo/menu/menu.mjs | 25 +- repo/menu/styles.mjs | 1 + repo/registry.json | 1 + 12 files changed, 1023 insertions(+), 6 deletions(-) create mode 100644 repo/icon-sets/app.css create mode 100644 repo/icon-sets/client.css create mode 100644 repo/icon-sets/client.mjs create mode 100644 repo/icon-sets/icons/remove.svg create mode 100644 repo/icon-sets/icons/restore.svg create mode 100644 repo/icon-sets/icons/search.svg create mode 100644 repo/icon-sets/icons/triangle.svg create mode 100644 repo/icon-sets/mod.js create mode 100644 repo/icon-sets/mod.json diff --git a/repo/icon-sets/app.css b/repo/icon-sets/app.css new file mode 100644 index 0000000..6ee9ebb --- /dev/null +++ b/repo/icon-sets/app.css @@ -0,0 +1,411 @@ +/* + * notion-icons + * (c) 2019 jayhxmo (https://jaymo.io/) + * (c) 2020 dragonwocky (https://dragonwocky.me/) + * (c) 2020 CloudHill + * under the MIT license + */ + +/* tab */ + +[hide-active-bar] > :nth-child(2) { + display: none; +} + +.notion-icons--tab, +.notion-icons--tab > div { + color: var(--theme--text) !important; +} + +#notion-icons--active-bar { + border-bottom: 2px solid var(--theme--text); + position: absolute; + bottom: -1px; + left: 8px; + right: 8px; +} + +.notion-icons--restore-button svg { + width: 16px; + height: 16px; + fill: var(--theme--text_ui_info); +} + +/* interactive hover */ + +.notion-icons--tab > div:hover, +.notion-icons--icon:hover, +.notion-icons--toggle:hover, +.notion-icons--restore-button:hover, +.notion-icons--removed-set:hover { + background: var(--theme--interactive_hover); + box-shadow: 0 0 0 0.5px var(--theme--interactive_hover-border) !important; +} + +/* container */ + +#notion-icons { + position: absolute; + top: 1px; + left: 0; + right: 0; + bottom: 0; + z-index: 9999; + display: flex; + flex-direction: column; + align-items: stretch; + background: var(--theme--card); + border-radius: 3px; + overflow: hidden; +} +/* search */ + +.notion-icons--search { + flex-shrink: 0; + height: 28px; + min-width: 0px; + margin: 9px 14px 10px; + padding: 3px 6px; + border-radius: 3px; + display: flex; + align-items: center; + position: relative; + font-size: 14px; + line-height: 1.2; + background: var(--theme--tag_input); + box-shadow: rgba(15, 15, 15, 0.2) 0px 0px 0px 1px inset; + user-select: none; + cursor: text; +} +.notion-dark-theme .notion-icons--search { + background: rgba(15, 15, 15, 0.3); + box-shadow: rgba(15, 15, 15, 0.1) 0px 0px 0px 1px inset; +} + +.notion-icons--search input { + font-size: inherit; + line-height: inherit; + border: none; + background: none; + width: 100%; + display: block; + resize: none; + padding: 0px; +} + +.notion-icons--search svg { + flex-grow: 0; + flex-shrink: 0; + width: 14px; + height: 14px; + display: block; + fill: inherit; + backface-visibility: hidden; + margin-right: 6px; + color: rgba(55, 53, 47, 0.8); +} +.notion-dark-theme .notion-icons--search svg { + color: rgb(202, 204, 206); +} + +/* scroller */ + +.notion-icons--scroller { + padding: 8px 12px; + overflow: hidden auto; + display: flex; + flex-direction: column; +} + +/* divider */ + +.notion-icons--divider { + height: 1px; + margin-bottom: 9px; + border-bottom: 1px solid var(--theme--table-border); +} + +/* icon set */ + +.notion-icons--icon-set { + margin-bottom: 8px; + color: var(--theme--text); + font-size: 11px; + line-height: 1.5; + letter-spacing: 1px; + font-weight: 600; + border-radius: 2px; +} + +.notion-icons--icon-set.error { + color: var(--theme--text_red); + background: var(--theme--block_red); + border: 1px solid var(--theme--tag_red); + padding: 8px 16px; +} +.notion-icons--icon-set.error::after { + content: '!'; + display: block; + font-size: 1.6em; + line-height: 0.9; + float: right; +} + +/* icon set header/toggle */ + +.notion-icons--toggle { + display: flex; + align-items: center; + margin-bottom: 8px; + padding: 0.25em; + border-radius: 2px; + text-transform: uppercase; + user-select: none; + cursor: pointer; + transition: background 200ms, margin-bottom 200ms ease-in; +} +.notion-icons--icon-set.alert .notion-icons--toggle { + color: var(--theme--block_yellow-text); + background: var(--theme--block_yellow); + border: 1px solid var(--theme--tag_yellow); + margin-left: -1px; + margin-right: -1px; +} +.notion-icons--icon-set.alert .notion-icons--toggle:hover { + background: var(--theme--tag_yellow); +} + +.notion-icons--toggle .triangle { + flex-grow: 0; + flex-shrink: 0; + width: 0.9em; + height: 1em; + margin: 0 0.75em 0 0.5em; + transition: transform 200ms ease-out 0s; + transform: rotateZ(180deg); +} + +.notion-icons--author { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.notion-icons--author span, +.notion-icons--author a { + color: var(--theme--text_ui_info); + transition: color 20ms ease-in; +} +.notion-icons--toggle a:hover { + color: var(--theme--primary); +} + +/* icon set body */ + +.notion-icons--body { + display: flex; + flex-wrap: wrap; + align-items: flex-start; + flex-grow: 1; + margin-left: 1.2em; + overflow: hidden; + transition: height 200ms ease-in, opacity 200ms ease-in; +} + +/* hidden icon set */ + +.notion-icons--icon-set[hidden-set] { + padding-bottom: 0; +} +.notion-icons--icon-set[hidden-set] .notion-icons--toggle { + margin-bottom: 0; +} +.notion-icons--icon-set[hidden-set] .triangle { + transform: rotateZ(90deg); +} +.notion-icons--icon-set[hidden-set] .notion-icons--body { + opacity: 0; +} + +/* icons */ + +.notion-icons--icon { + width: 40px; + height: 40px; + padding: 4px; + border-radius: 3px; + display: flex; + align-items: center; + justify-content: center; + position: relative; + user-select: none; + cursor: pointer; + transition: background 20ms ease-in; +} + +.notion-icons--icon img { + width: 100%; + height: 100%; + pointer-events: none; +} +/* spritesheet */ +.notion-icons--icon div { + width: 32px; + height: 32px; + background-size: 32px; + background-repeat: no-repeat; + pointer-events: none; +} + +.notion-icons--icon.error { + font-size: 20px; + background: var(--theme--block_yellow); + border: 1px solid var(--theme--tag_yellow); + color: var(--theme--text_yellow); +} +.notion-icons--icon.error:hover { + background: var(--theme--tag_yellow); +} + +/* tooltip */ + +.notion-icons--tooltip { + position: fixed; + pointer-events: none; + z-index: 99; +} + +.notion-icons--tooltip > div { + position: absolute; + top: 0px; + left: 0px; + width: 40px; + height: 40px; + display: flex; + flex-direction: column; + justify-content: flex-end; + align-items: center; +} + +.notion-icons--tooltip-text { + bottom: calc(100% + 6px); + padding: 4px 8px; + border-radius: 3px; + display: flex; + align-items: center; + flex-direction: column; + position: relative; + max-width: calc(100vw - 24px); + background: rgb(15, 15, 15); + color: rgba(255, 255, 255, 0.9); + box-shadow: rgba(0, 0, 0, 0.3) 0px 1px 4px; + font-size: 12px; + line-height: 1.4; + font-weight: 500; + white-space: nowrap; + overflow: hidden; +} + +.notion-dark-theme .notion-icons--tooltip-text { + background: rgb(202, 204, 206); + color: rgb(15, 15, 15); +} + +/* actions */ + +.notion-icons--actions { + flex-grow: 0; + flex-shrink: 0; + margin-left: auto; + display: flex; + align-items: center; +} + +/* spinner */ + +.notion-icons--spinner { + width: 12px; + height: 12px; +} +.notion-icons--spinner img { + width: 100%; + height: 100%; + animation: rotation 1.3s infinite linear; +} + +/* remove button */ + +.notion-icons--remove-button { + display: flex; + justify-content: center; + align-items: center; + margin-left: 8px; + width: 16px; + height: 16px; + position: relative; +} +.notion-icons--remove-button::before { + content: 'Hide icon set'; + position: absolute; + right: -3px; + padding: 4px 22px 4px 6px; + background: var(--theme--main); + box-shadow: var(--theme--box-shadow); + white-space: nowrap; + opacity: 0; + pointer-events: none; + transition: opacity 50ms ease-in; +} +.notion-icons--remove-button:hover::before { + opacity: 1; + pointer-events: auto; +} +.notion-icons--remove-button svg { + width: 100%; + height: 100%; + fill: var(--theme--text_ui_info); + z-index: 1; +} + +/* restore icon sets modal */ + +.notion-icons--overlay-container { + position: fixed; + top: 0; + bottom: 0; + left: 0; + right: 0; + z-index: 999; + overflow: hidden; +} + +.notion-icons--restore { + max-width: 320px; + max-height: 320px; + position: relative; + border-radius: 3px; + padding: 8px 0; + box-shadow: var(--theme--box-shadow_strong); + background: var(--theme--card); + overflow: hidden auto; +} + +.notion-icons--removed-set { + display: flex; + align-items: center; + width: 100%; + padding: 8px 14px; + user-select: none; + cursor: pointer; + transition: background 0.4s ease; +} + +/* animation */ + +@keyframes rotation { + from { + transform: rotate(0deg); + } + to { + transform: rotate(359deg); + } +} diff --git a/repo/icon-sets/client.css b/repo/icon-sets/client.css new file mode 100644 index 0000000..1ab2fad --- /dev/null +++ b/repo/icon-sets/client.css @@ -0,0 +1,179 @@ +/* + * 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 + */ + +.icon_sets--tab_button { + position: relative; + padding-top: 6px; + padding-bottom: 6px; + flex-shrink: 0; +} +.icon_sets--tab_button > .notion-focusable { + user-select: none; + transition: background 20ms ease-in 0s; + cursor: pointer; + display: inline-flex; + align-items: center; + height: 28px; + border-radius: 3px; + font-size: 14px; + line-height: 1.2; + padding-left: 8px; + padding-right: 8px; + color: var(--theme--text); +} +.icon_sets--tab_button:hover > .notion-focusable { + background: var(--theme--ui_interactive-hover); +} +.icon_sets--tab_button:active > .notion-focusable { + background: var(--theme--ui_interactive-active); +} + +.icon_sets--scroller { + padding: 0; + overflow: hidden; + display: flex; + flex-direction: column; + flex-grow: 1; + z-index: 1; +} +.icon_sets--actions { + display: flex; + padding: 10px 14px; +} + +.icon_sets--link_input { + flex-grow: 1; + font-size: 14px; + line-height: 20px; + padding: 4px 6px; + border-top-left-radius: 3px; + border-bottom-left-radius: 3px; + box-shadow: var(--theme--ui_shadow) 0px 0px 0px 1px inset; + background: var(--theme--ui_input); + cursor: text; + height: 28px; +} +.icon_sets--link_input > input { + font-size: inherit; + line-height: inherit; + border: none; + background: none; + width: 100%; + display: block; + resize: none; + padding: 0px; +} + +.icon_sets--link_submit { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + border-top-right-radius: 3px; + border-bottom-right-radius: 3px; +} + +.icon_sets--upload, +.icon_sets--link_submit { + user-select: none; + transition: background 20ms ease-in 0s; + cursor: pointer; + border: none; + background: var(--theme--accent_blue); + color: var(--theme--accent_blue-text); + line-height: 1.2; + padding: 6px 8px; + height: 28px; + font-size: 14px; + font-weight: 500; +} +.icon_sets--upload:hover, +.icon_sets--link_submit:hover { + background: var(--theme--accent_blue-hover); +} +.icon_sets--upload:active, +.icon_sets--link_submit:active { + background: var(--theme--accent_blue-active); +} + +.icon_sets--upload { + margin-left: 0.5em; + border-radius: 3px; +} + +.icon_sets--list { + height: 100%; + word-break: break-all; + overflow: hidden auto; + padding: 0 14px 10px 14px; +} + +.icon_sets--title { + margin: 6px 0 8px 0; + color: var(--theme--text_secondary); + font-size: 11px; + font-weight: 500; + line-height: 1.2; + user-select: none; + text-transform: uppercase; + cursor: pointer; + border-radius: 2px; + padding: 0.25em; + display: flex; + align-items: center; +} +.icon_sets--title:hover { + background: var(--theme--ui_interactive-hover); +} +.icon_sets--title:active { + background: var(--theme--ui_interactive-active); +} +.icon_sets--title .info { + height: 1em; + margin-left: 0.5em; +} +.icon_sets--title .triangle { + height: 1em; + width: 0.9em; + margin: 0 0.5em 0 0.25em; + transition: transform 200ms ease-out 0s; + transform: rotateZ(180deg); +} +.icon_sets--title[data-collapsed='true'] .triangle { + transform: rotateZ(90deg); +} +.icon_sets--title[data-collapsed='true'] + .icon_sets--set { + height: 0 !important; +} + +.icon_sets--set { + display: flex; + flex-wrap: wrap; + overflow: hidden; + transition: height 200ms ease-out 0s; +} +.icon_sets--icon { + user-select: none; + transition: background 20ms ease-in 0s; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + border-radius: 3px; + width: 32px; + height: 32px; + font-size: 24px; +} +.icon_sets--icon:hover { + background: var(--theme--ui_interactive-hover); +} +.icon_sets--icon:active { + background: var(--theme--ui_interactive-active); +} +.icon_sets--icon > img { + max-width: 24px; + max-height: 24px; +} diff --git a/repo/icon-sets/client.mjs b/repo/icon-sets/client.mjs new file mode 100644 index 0000000..47507ee --- /dev/null +++ b/repo/icon-sets/client.mjs @@ -0,0 +1,182 @@ +/* + * 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 + */ + +export default async function ({ web, components, notion }, db) { + const recentUploads = await db.get(['recent_uploads'], []), + $triangleSvg = web.html` + + `; + + 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 (id, title, $tooltip = undefined) => { + const isCollapsed = await db.get(['collapsed', id], false), + $title = web.html`

`; + web.render( + $title, + $triangleSvg.cloneNode(true), + web.html`${web.escape(title)}` + ); + $title.addEventListener('click', () => { + const newState = $title.dataset.collapsed !== 'true'; + db.set(['collapsed', id], newState); + $title.dataset.collapsed = newState; + }); + if ($tooltip) { + const $infoSvg = web.html`${await components.feather('info', { class: 'info' })}`; + components.setTooltip($infoSvg, $tooltip); + 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`
`, + $recentUploadsTitle = await renderSetTitle( + 'recent_uploads', + 'Recent', + web.html`

Click to reuse an icon
Shift-click to remove it

` + ), + $recentUploads = web.html`
`, + // container + $iconsScroller = web.render( + web.html``, + web.render( + web.html`
`, + $iconsLinkInput, + $iconsLinkSubmit, + $iconsUploadSubmit + ), + web.render($setsList, $recentUploadsTitle, $recentUploads) + ); + + let $mediaMenu, $activeTabUnderline; + const insertIconsTab = async (event) => { + 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($iconsScroller); + + const renderRecentUploads = () => { + web.empty($recentUploads); + 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(url); + }); + } + $recentUploads.style.height = `${$recentUploads.scrollHeight}px`; + }, + renderSets = async () => {}; + + const displayIconsTab = (force = false) => { + if ($iconsTab.contains($activeTabUnderline) && !force) return; + web.render($iconsTab, $activeTabUnderline); + $iconsScroller.style.display = ''; + $emojiScroller.style.display = 'none'; + $emojiFilter.style.display = 'none'; + renderRecentUploads(); + }, + displayEmojiTab = (force = false) => { + if ($emojiTab.contains($activeTabUnderline) && !force) return; + web.render($emojiTab, $activeTabUnderline); + $iconsScroller.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(iconUrl) { + // without this react gets upset + displayEmojiTab(); + $linkTab.firstChild.click(); + await new Promise(requestAnimationFrame); + + // 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; + setIcon(url); + recentUploads.push({ signed: notion.sign(url, notion.getPageID()), url: url }); + 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); + setIcon(url); + recentUploads.push({ signed: notion.sign(url, notion.getPageID()), url: url }); + db.set(['recent_uploads'], recentUploads); + }; + }; + web.addDocumentObserver(insertIconsTab, [mediaMenuSelector]); +} diff --git a/repo/icon-sets/icons/remove.svg b/repo/icon-sets/icons/remove.svg new file mode 100644 index 0000000..b6a57a6 --- /dev/null +++ b/repo/icon-sets/icons/remove.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/repo/icon-sets/icons/restore.svg b/repo/icon-sets/icons/restore.svg new file mode 100644 index 0000000..2e41e4c --- /dev/null +++ b/repo/icon-sets/icons/restore.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/repo/icon-sets/icons/search.svg b/repo/icon-sets/icons/search.svg new file mode 100644 index 0000000..e8c0216 --- /dev/null +++ b/repo/icon-sets/icons/search.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/repo/icon-sets/icons/triangle.svg b/repo/icon-sets/icons/triangle.svg new file mode 100644 index 0000000..8615536 --- /dev/null +++ b/repo/icon-sets/icons/triangle.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/repo/icon-sets/mod.js b/repo/icon-sets/mod.js new file mode 100644 index 0000000..d41d9bd --- /dev/null +++ b/repo/icon-sets/mod.js @@ -0,0 +1,185 @@ +// source => icon data +const enhancerIconSets = new Map(); +getAsync(notionIconsUrl + 'icons.json', (iconsData) => { + const data = JSON.parse(iconsData); + (data.icons || data).forEach((set) => { + enhancerIconSets.set(set.source, set); + }); +}); + +// array +let customIconSets; +if (store().json) { + const customData = JSON.parse(fs.readFileSync(store().json)); + customIconSets = customData.icons || customData; +} + +// convert icons data into renderable +function loadIconSets() { + const iconSets = new DocumentFragment(); + + if (customIconSets) { + customIconSets.forEach((i) => { + iconSets.appendChild(renderIconSet(i)); + }); + + // divider + iconSets.appendChild(createElement('
')); + } + + if (enhancerIconSets.size > 0) { + enhancerIconSets.forEach((i, source) => { + // ignore removed icon sets + if (store().removedSets?.includes(source)) return; + + i.sourceUrl = i.sourceUrl || notionIconsUrl + source; + iconSets.appendChild(renderIconSet(i, true)); + }); + } + + return iconSets; +} + +// returns icon set element +function renderIconSet(iconData, enhancerSet = false) { + const iconSet = createElement('
'); + + try { + const author = iconData.author + ? iconData.authorUrl + ? ` by ${iconData.author}` + : ` by ${iconData.author}` + : ''; + + const toggle = createElement(` +
+ ${menuIcons.triangle} +
${iconData.name}${author}
+
+
+ +
+
+
+ `); + + const iconSetBody = createElement('
'); + + iconSet.append(toggle, iconSetBody); + + const promiseArray = []; + // render icons + 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]; + + const icon = createElement(`
`); + icon.innerHTML = enhancerSet + ? // load sprite sheet + `
` + : ``; + + // add filters to filterMap + const filters = []; + + if (iconData.filter) { + if (iconData.filter === 'source') { + const filename = iconUrl.match(/.*\/(.+?)\./); + if (filename?.length > 1) { + filters.push(...filename[1].split(/[ \-_]/)); + } + } else if (Array.isArray(iconData.filter)) { + filters.push(...iconData.filter[i]); + } + icon.setAttribute('filter', filters.join(' ')); + } + + // add set name and author to filters + filters.push(...iconData.name.toLowerCase().split(' ')); + if (iconData.author) filters.push(...iconData.author.toLowerCase().split(' ')); + + filterMap.set(icon, filters); + + // make sure icons load + if (!enhancerSet) { + promiseArray.push( + new Promise((resolve, reject) => { + icon.firstChild.onload = resolve; + icon.firstChild.onerror = () => { + reject(); + icon.classList.add('error'); + icon.innerHTML = '!'; + }; + }) + ); + } + + garbageCollector.push(icon); + icon.addEventListener('click', () => setPageIcon(iconUrl)); + iconSetBody.appendChild(icon); + } + + // hide spinner after all icons finish loading + (async () => { + const spinner = toggle.querySelector('.notion-icons--spinner'), + loadPromise = Promise.all(promiseArray); + loadPromise.then( + () => spinner.remove(), + () => { + iconSet.classList.add('alert'); + spinner.remove(); + } + ); + })(); + + // add remove icon set button + if (enhancerSet) { + const removeButton = createElement( + `
${menuIcons.remove}
` + ); + removeButton.addEventListener('click', (e) => { + e.stopPropagation(); + removeIconSet(iconData); + }); + iconSet.querySelector('.notion-icons--actions').appendChild(removeButton); + } + + // set up toggle + toggle.addEventListener('click', (e) => { + if (e.target.nodeName === 'A') return; + toggleIconSet(iconSet); + }); + + // hide by default? + if (store().hide) requestAnimationFrame(() => toggleIconSet(iconSet)); + + // tooltip + let timeout; + iconSetBody.addEventListener('mouseover', (e) => { + const el = e.target; + if (!el.hasAttribute('filter')) return; + + document.querySelector('.notion-icons--tooltip')?.remove(); + timeout = setTimeout(() => { + renderTooltip(el, el.getAttribute('filter')); + }, 300); + }); + iconSetBody.addEventListener('mouseout', (e) => { + const el = e.target; + if (!el.hasAttribute('filter')) return; + + document.querySelector('.notion-icons--tooltip')?.remove(); + clearTimeout(timeout); + }); + } catch (err) { + iconSet.classList.add('error'); + iconSet.innerHTML = `Invalid Icon Set: ${iconData.name}`; + } + + return iconSet; +} diff --git a/repo/icon-sets/mod.json b/repo/icon-sets/mod.json new file mode 100644 index 0000000..c8e1502 --- /dev/null +++ b/repo/icon-sets/mod.json @@ -0,0 +1,35 @@ +{ + "name": "icon sets", + "id": "2d1f4809-9581-40dd-9bf3-4239db406483", + "version": "0.4.0", + "description": "upload, save and reuse custom icons directly from the icon picker.", + "tags": ["integration", "customisation"], + "authors": [ + { + "name": "dragonwocky", + "email": "thedragonring.bod@gmail.com", + "homepage": "https://dragonwocky.me/", + "avatar": "https://dragonwocky.me/avatar.jpg" + } + ], + "js": { + "client": ["client.mjs"] + }, + "css": { + "client": ["client.css"] + }, + "options": [ + { + "type": "toggle", + "key": "default_sets", + "label": "load default icon sets from github", + "value": true + }, + { + "type": "file", + "key": "json", + "label": "custom icon sets (.json)", + "extensions": ["json"] + } + ] +} diff --git a/repo/menu/menu.mjs b/repo/menu/menu.mjs index b34d9ee..5ddd9bb 100644 --- a/repo/menu/menu.mjs +++ b/repo/menu/menu.mjs @@ -283,7 +283,7 @@ const $modLists = {}, $search, web.html`${await components.feather('search', { class: 'input-icon' })}` ), - message ? web.html`

${web.escape(message)}

` : '', + message ? web.render(web.html`

`, message) : '', $list ); } @@ -322,7 +322,8 @@ $notionNavItem.addEventListener('click', env.focusNotion); const $coreNavItem = web.html`core`, $extensionsNavItem = web.html`extensions`, - $themesNavItem = web.html`themes`; + $themesNavItem = web.html`themes`, + $integrationsNavItem = web.html`integrations`; web.render( document.body, @@ -336,6 +337,7 @@ web.render( $coreNavItem, $extensionsNavItem, $themesNavItem, + $integrationsNavItem, web.html`docs`, web.html`community` ), @@ -367,10 +369,9 @@ router.addView('core', async () => { await generators.modList( 'extension', - `Extensions modify and extend the functionality - or layout of the Notion client. They don't interfere - with Notion's data structures, so they can be safely - enabled or disabled at any time.` + `Extensions build on the functionality and layout of + the Notion client, modifying and interacting with + existing interfaces.` ); router.addView('extensions', async () => { web.empty($main); @@ -391,6 +392,18 @@ router.addView('themes', async () => { return web.render($main, await generators.modList('theme')); }); +await generators.modList( + 'integration', + web.html`Integrations are extensions that use an unofficial API + to access and modify content. They are used just like + normal extensions, but may be more dangerous to use.` +); +router.addView('integrations', async () => { + web.empty($main); + selectNavItem($integrationsNavItem); + return web.render($main, await generators.modList('integration')); +}); + router.setDefaultView('extensions'); router.addQueryListener('id', openSidebarMenu); diff --git a/repo/menu/styles.mjs b/repo/menu/styles.mjs index 21c452d..d247a19 100644 --- a/repo/menu/styles.mjs +++ b/repo/menu/styles.mjs @@ -86,6 +86,7 @@ const customClasses = { 'file-latest': apply`block w-full text-left text-foreground-secondary text-xs mt-2 hover:line-through cursor-pointer`, 'search-container': apply`block mx-2.5 my-2.5 relative`, 'search': apply`input pr-12`, + 'danger': apply`bg-red-paragraph text-red-paragraph-text`, }; setup({ diff --git a/repo/registry.json b/repo/registry.json index f51e23f..757e649 100644 --- a/repo/registry.json +++ b/repo/registry.json @@ -24,6 +24,7 @@ "right-to-left", "simpler-databases", "emoji-sets", + "icon-sets", "bypass-preview", "topbar-icons", "word-counter",