mirror of
				https://github.com/notion-enhancer/notion-enhancer.git
				synced 2025-10-31 22:28:08 +11:00 
			
		
		
		
	add menu filters + unspaghetti menu code
This commit is contained in:
		
							parent
							
								
									4e3f921ee3
								
							
						
					
					
						commit
						a55482d62d
					
				
							
								
								
									
										2
									
								
								extension/icons/monstr/party.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								extension/icons/monstr/party.svg
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,2 @@ | |||||||
|  | <!-- https://iconmonstr.com/party-5-svg/ --> | ||||||
|  | <svg width="24" height="24" xmlns="http://www.w3.org/2000/svg" fill-rule="evenodd" clip-rule="evenodd"><path fill="currentColor" d="M4.823 21.933l2.734-1.171-3.241-8.847-1.561 4.372 2.068 5.646zm-2.594-4.174l-2.229 6.241 3.903-1.672-1.674-4.569zm6.248 2.609l2.934-1.258-3.482-9.141-2.215-1.969-.872 2.443 3.635 9.925zm7.523-3.224l-6.453-5.736 2.785 7.308 3.668-1.572zm-.826-5.003l2.201-1.445c.23-.152.295-.462.143-.693-.152-.232-.463-.295-.692-.143l-2.201 1.445c-.231.151-.295.461-.144.692.096.147.256.226.418.226.095 0 .19-.026.275-.082m-2.993-4.312l1.112-2.388c.117-.25.008-.548-.242-.664-.251-.116-.548-.009-.665.242l-1.111 2.388c-.117.25-.008.547.242.664l.211.047c.189 0 .368-.107.453-.289m-2.627.709c1.539-2.963 1.644-5.73.314-8.222-.09-.169-.263-.265-.442-.265-.37 0-.621.398-.44.736 1.166 2.184 1.058 4.637-.32 7.29-.127.245-.031.547.214.674.073.038.152.057.23.057.18 0 .355-.099.444-.27m6.505 6.095c2.017-1.434 4.463-1.64 7.272-.613.327.119.672-.123.672-.47 0-.203-.125-.395-.328-.47-3.136-1.147-5.894-.9-8.196.738-.224.16-.277.472-.117.698.098.136.251.209.407.209.101 0 .202-.03.29-.092m3.757-6.757l-1.697.014.938 1.415-.511 1.618 1.635-.455 1.381.986.073-1.696 1.365-1.009-1.591-.592-.538-1.61-1.055 1.329zm-7.307 3.624c.276-.009.492-.24.483-.517-.056-1.627.36-1.937 1.377-2.051 1.689-.191 1.785-1.312 1.842-1.982.053-.637.071-.851.773-.903.63-.046 1.331-.16 1.76-.659.461-.538.466-1.358.402-2.164-.021-.276-.266-.478-.537-.459-.275.021-.481.262-.459.537.062.787.011 1.23-.165 1.434-.149.174-.48.271-1.074.314-1.553.114-1.644 1.179-1.697 1.816-.057.668-.082.973-.956 1.071-2.075.234-2.315 1.619-2.266 3.08.01.27.231.483.5.483h.017m7.842-8.675c0 1.006.818 1.824 1.825 1.824 1.006 0 1.824-.818 1.824-1.824 0-1.007-.818-1.825-1.824-1.825-1.007 0-1.825.818-1.825 1.825m-6.623-2.841c1.104 0 2 .897 2 2 0 1.104-.896 2-2 2-1.103 0-2-.896-2-2 0-1.103.897-2 2-2"/></svg> | ||||||
| After Width: | Height: | Size: 1.9 KiB | 
| @ -5,12 +5,13 @@ | |||||||
| - improved: split the core mod into the theming & menu mods. | - improved: split the core mod into the theming & menu mods. | ||||||
| - improved: new larger menu layout, with individual mod pages. | - improved: new larger menu layout, with individual mod pages. | ||||||
| - improved: merged bracketed-links into tweaks. | - improved: merged bracketed-links into tweaks. | ||||||
|  | - improved: replaced confusing all-tag filters with themes/extensions/enabled/disabled filters. | ||||||
| - removed: integrated scrollbar tweak (notion now includes by default). | - removed: integrated scrollbar tweak (notion now includes by default). | ||||||
| - removed: js insert. css insert moved to tweaks mod. | - removed: js insert. css insert moved to tweaks mod. | ||||||
|  | - ported: tweaks, bypass-preview. | ||||||
| 
 | 
 | ||||||
| #### todo | #### todo | ||||||
| 
 | 
 | ||||||
| - tag sort |  | ||||||
| - documentation e.g. \_file | - documentation e.g. \_file | ||||||
| - complete/bugfix theming variables | - complete/bugfix theming variables | ||||||
| - color pickers | - color pickers | ||||||
|  | |||||||
| @ -4,6 +4,8 @@ | |||||||
|  * (https://notion-enhancer.github.io/) under the MIT license
 |  * (https://notion-enhancer.github.io/) under the MIT license
 | ||||||
|  */ |  */ | ||||||
| 
 | 
 | ||||||
|  | 'use strict'; | ||||||
|  | 
 | ||||||
| import { web } from '../../api.js'; | import { web } from '../../api.js'; | ||||||
| 
 | 
 | ||||||
| web.whenReady().then(async () => { | web.whenReady().then(async () => { | ||||||
| @ -23,7 +25,7 @@ function getCurrentPage() { | |||||||
|   return { type: 'page', id: location.pathname.split(/(-|\/)/g).reverse()[0] }; |   return { type: 'page', id: location.pathname.split(/(-|\/)/g).reverse()[0] }; | ||||||
| } | } | ||||||
| let lastPage = getCurrentPage(); | let lastPage = getCurrentPage(); | ||||||
| web.observeDocument((event) => { | web.addDocumentObserver((event) => { | ||||||
|   const currentPage = getCurrentPage(); |   const currentPage = getCurrentPage(); | ||||||
|   if (currentPage.id !== lastPage.id || currentPage.type !== lastPage.type) { |   if (currentPage.id !== lastPage.id || currentPage.type !== lastPage.type) { | ||||||
|     const openAsPage = document.querySelector( |     const openAsPage = document.querySelector( | ||||||
|  | |||||||
| @ -1,7 +1,7 @@ | |||||||
| { | { | ||||||
|   "name": "bypass-preview", |   "name": "bypass-preview", | ||||||
|   "id": "cb6fd684-f113-4a7a-9423-8f0f0cff069f", |   "id": "cb6fd684-f113-4a7a-9423-8f0f0cff069f", | ||||||
|   "description": "go straight to the normal full view when opening a page..", |   "description": "go straight to the normal full view when opening a page.", | ||||||
|   "version": "0.2.0", |   "version": "0.2.0", | ||||||
|   "tags": ["extension", "automation"], |   "tags": ["extension", "automation"], | ||||||
|   "authors": [ |   "authors": [ | ||||||
|  | |||||||
| @ -1 +1,3 @@ | |||||||
| # menu | # menu | ||||||
|  | 
 | ||||||
|  | [theming mod link test](?view=mod&id=0f0bf8b6-eae6-4273-b307-8fc43f2ee082) | ||||||
|  | |||||||
| @ -25,6 +25,7 @@ web.whenReady([sidebarSelector]).then(async () => { | |||||||
|       list: await fs.getJSON('https://notion-enhancer.github.io/notifications.json'), |       list: await fs.getJSON('https://notion-enhancer.github.io/notifications.json'), | ||||||
|       dismissed: await storage.get(_id, 'notifications', []), |       dismissed: await storage.get(_id, 'notifications', []), | ||||||
|     }; |     }; | ||||||
|  |   console.log($enhancerSidebarElement); | ||||||
|   notifications.waiting = notifications.list.filter( |   notifications.waiting = notifications.list.filter( | ||||||
|     ({ id }) => !notifications.dismissed.includes(id) |     ({ id }) => !notifications.dismissed.includes(id) | ||||||
|   ); |   ); | ||||||
| @ -48,4 +49,4 @@ web.whenReady([sidebarSelector]).then(async () => { | |||||||
|   setTheme(); |   setTheme(); | ||||||
|   document.querySelector(sidebarSelector).appendChild($enhancerSidebarElement); |   document.querySelector(sidebarSelector).appendChild($enhancerSidebarElement); | ||||||
| }); | }); | ||||||
| web.hotkeyListener(await storage.get(_id, 'hotkey.focustoggle'), env.openEnhancerMenu); | web.addHotkeyListener(await storage.get(_id, 'hotkey.focustoggle'), env.openEnhancerMenu); | ||||||
|  | |||||||
| @ -11,6 +11,7 @@ | |||||||
|   -webkit-text-size-adjust: 100%; |   -webkit-text-size-adjust: 100%; | ||||||
|   font-size: inherit; |   font-size: inherit; | ||||||
|   font-family: inherit; |   font-family: inherit; | ||||||
|  |   fill: currentColor; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| html { | html { | ||||||
| @ -43,7 +44,7 @@ header > * { | |||||||
|   margin: 0 1.25rem 0.1em 0; |   margin: 0 1.25rem 0.1em 0; | ||||||
|   font-size: var(--theme--font_heading1-size); |   font-size: var(--theme--font_heading1-size); | ||||||
| } | } | ||||||
| header h1 a:not([data-view-active]) { | header h1 a { | ||||||
|   text-decoration: none; |   text-decoration: none; | ||||||
| } | } | ||||||
| header h1 img { | header h1 img { | ||||||
| @ -80,7 +81,7 @@ main { | |||||||
|   main { |   main { | ||||||
|     grid-template-columns: 1fr 1fr; |     grid-template-columns: 1fr 1fr; | ||||||
|   } |   } | ||||||
|   [data-view='mod'] main > .documentation--buttons { |   main > .action--buttons { | ||||||
|     grid-column: span 2; |     grid-column: span 2; | ||||||
|   } |   } | ||||||
|   [data-view='mod'] main .library--card, |   [data-view='mod'] main .library--card, | ||||||
| @ -97,7 +98,7 @@ main { | |||||||
|   main { |   main { | ||||||
|     grid-template-columns: 1fr 1fr 1fr; |     grid-template-columns: 1fr 1fr 1fr; | ||||||
|   } |   } | ||||||
|   [data-view='mod'] main > .documentation--buttons { |   main > .action--buttons { | ||||||
|     grid-column: span 3; |     grid-column: span 3; | ||||||
|   } |   } | ||||||
|   [data-view='mod'] main > .documentation--body { |   [data-view='mod'] main > .documentation--body { | ||||||
| @ -108,7 +109,7 @@ main { | |||||||
|   main { |   main { | ||||||
|     grid-template-columns: 1fr 1fr 1fr 1fr; |     grid-template-columns: 1fr 1fr 1fr 1fr; | ||||||
|   } |   } | ||||||
|   [data-view='mod'] main > .documentation--buttons { |   main > .action--buttons { | ||||||
|     grid-column: span 4; |     grid-column: span 4; | ||||||
|   } |   } | ||||||
|   [data-view='mod'] main > .documentation--body { |   [data-view='mod'] main > .documentation--body { | ||||||
| @ -119,7 +120,7 @@ main { | |||||||
|   main { |   main { | ||||||
|     grid-template-columns: 1fr 1fr 1fr 1fr 1fr; |     grid-template-columns: 1fr 1fr 1fr 1fr 1fr; | ||||||
|   } |   } | ||||||
|   [data-view='mod'] main > .documentation--buttons { |   main > .action--buttons { | ||||||
|     grid-column: span 5; |     grid-column: span 5; | ||||||
|   } |   } | ||||||
|   [data-view='mod'] main > .documentation--body { |   [data-view='mod'] main > .documentation--body { | ||||||
| @ -136,7 +137,7 @@ main article img { | |||||||
|   max-width: 100%; |   max-width: 100%; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .documentation--buttons, | .action--buttons, | ||||||
| .library--expand { | .library--expand { | ||||||
|   margin: 0; |   margin: 0; | ||||||
|   display: flex; |   display: flex; | ||||||
| @ -146,14 +147,14 @@ main article img { | |||||||
| .library--expand a { | .library--expand a { | ||||||
|   margin-left: auto; |   margin-left: auto; | ||||||
| } | } | ||||||
| .documentation--buttons a, | .action--buttons a, | ||||||
| .library--expand a { | .library--expand a { | ||||||
|   border-radius: 3px; |   border-radius: 3px; | ||||||
|   padding: 0.35rem 0.45rem; |   padding: 0.35rem 0.45rem; | ||||||
|   text-decoration: none; |   text-decoration: none; | ||||||
|   display: flex; |   display: flex; | ||||||
| } | } | ||||||
| .documentation--buttons .documentation--reload { | .action--buttons .action--alert { | ||||||
|   cursor: pointer; |   cursor: pointer; | ||||||
|   border-radius: 3px; |   border-radius: 3px; | ||||||
|   padding: 0.35rem 0.45rem; |   padding: 0.35rem 0.45rem; | ||||||
| @ -164,31 +165,32 @@ main article img { | |||||||
|   opacity: 0; |   opacity: 0; | ||||||
|   transition: opacity 200ms ease-in-out; |   transition: opacity 200ms ease-in-out; | ||||||
| } | } | ||||||
| .documentation--buttons .documentation--reload[data-triggered] { | .action--buttons .action--alert[data-triggered] { | ||||||
|   pointer-events: all; |   pointer-events: all; | ||||||
|   opacity: 1; |   opacity: 1; | ||||||
| } | } | ||||||
| .documentation--buttons .documentation--reload[data-triggered]:hover { | .action--buttons .action--alert[data-triggered]:hover { | ||||||
|   background: none; |   background: none; | ||||||
|   color: var(--theme--block_grey-text); |   color: var(--theme--block_grey-text); | ||||||
|   box-shadow: var(--theme--block_grey) 0px 0px 0px 1px inset; |   box-shadow: var(--theme--block_grey) 0px 0px 0px 1px inset; | ||||||
| } | } | ||||||
| .documentation--buttons span, | .action--buttons span, | ||||||
| .library--expand span { | .library--expand span { | ||||||
|   color: var(--theme--text_property); |   color: var(--theme--text_property); | ||||||
| } | } | ||||||
| .documentation--buttons a:hover, | .action--buttons a:hover, | ||||||
|  | .action--buttons a.action--active, | ||||||
| .library--expand a:hover { | .library--expand a:hover { | ||||||
|   background: var(--theme--button-hover); |   background: var(--theme--button-hover); | ||||||
| } | } | ||||||
| .documentation--buttons svg, | .action--buttons svg, | ||||||
| .library--expand svg { | .library--expand svg { | ||||||
|   width: 1em; |   width: 1em; | ||||||
|   height: 1em; |   height: 1em; | ||||||
|   padding-top: 2px; |   padding-top: 2px; | ||||||
|   margin-right: 0.3rem; |   margin-right: 0.3rem; | ||||||
| } | } | ||||||
| .documentation--buttons svg *, | .action--buttons svg *, | ||||||
| .library--expand svg * { | .library--expand svg * { | ||||||
|   fill: var(--theme--text_property); |   fill: var(--theme--text_property); | ||||||
| } | } | ||||||
| @ -269,7 +271,7 @@ label p > span:not([class]), | |||||||
| label > span:not([class]) { | label > span:not([class]) { | ||||||
|   font-size: 1rem; |   font-size: 1rem; | ||||||
| } | } | ||||||
| label [data-icon='fa/question-circle'] { | label [data-icon='fa/solid/question-circle'] { | ||||||
|   height: var(--theme--font_ui_small-size); |   height: var(--theme--font_ui_small-size); | ||||||
|   width: var(--theme--font_ui_small-size); |   width: var(--theme--font_ui_small-size); | ||||||
|   margin-left: 0.25em; |   margin-left: 0.25em; | ||||||
| @ -306,7 +308,7 @@ label [data-icon='fa/question-circle'] { | |||||||
|   height: 0.8rem; |   height: 0.8rem; | ||||||
|   width: 0.8rem; |   width: 0.8rem; | ||||||
|   left: 0.325rem; |   left: 0.325rem; | ||||||
|   top: 0.225rem; |   top: 0.2rem; | ||||||
|   position: absolute; |   position: absolute; | ||||||
|   border-radius: 100%; |   border-radius: 100%; | ||||||
|   background: var(--theme--toggle_dot); |   background: var(--theme--toggle_dot); | ||||||
| @ -419,22 +421,7 @@ label [data-icon='fa/question-circle'] { | |||||||
|   overflow-x: auto; |   overflow-x: auto; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .tooltip { | .notifications { | ||||||
|   position: absolute; |  | ||||||
|   background: var(--theme--tooltip); |  | ||||||
|   color: var(--theme--tooltip-text); |  | ||||||
|   font-size: var(--theme--font_ui_small-size); |  | ||||||
|   padding: 0.15rem 0.4rem; |  | ||||||
|   box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06) !important; |  | ||||||
|   border-radius: 3px; |  | ||||||
|   max-width: 20rem; |  | ||||||
|   display: none; |  | ||||||
| } |  | ||||||
| .tooltip p { |  | ||||||
|   margin: 0.25rem 0; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .notification--list { |  | ||||||
|   position: absolute; |   position: absolute; | ||||||
|   bottom: 1.5rem; |   bottom: 1.5rem; | ||||||
|   right: 1.5rem; |   right: 1.5rem; | ||||||
| @ -459,7 +446,7 @@ label [data-icon='fa/question-circle'] { | |||||||
|   transform-origin: 100% 100%; |   transform-origin: 100% 100%; | ||||||
|   opacity: 0; |   opacity: 0; | ||||||
| } | } | ||||||
| .notification svg { | .notification :not(.notification--dismiss) > svg { | ||||||
|   height: 1.5rem; |   height: 1.5rem; | ||||||
|   width: 1.5rem; |   width: 1.5rem; | ||||||
|   margin-top: 0.25rem; |   margin-top: 0.25rem; | ||||||
| @ -478,27 +465,20 @@ label [data-icon='fa/question-circle'] { | |||||||
|   right: 0.75rem; |   right: 0.75rem; | ||||||
|   background: none; |   background: none; | ||||||
|   border: none; |   border: none; | ||||||
|   padding: 0.25rem 0.35rem; |   padding: 0.15rem 0 0.15rem 0.5rem; | ||||||
|   font-size: var(--theme--font_body-size); |   width: var(--theme--font_body-size); | ||||||
|   color: currentColor; |   color: currentColor; | ||||||
|   cursor: pointer; |   cursor: pointer; | ||||||
|   transition: opacity 200ms ease-in-out; |   transition: opacity 200ms ease-in-out; | ||||||
|   opacity: 0; |   opacity: 0; | ||||||
| } | } | ||||||
|  | .notification .notification--dismiss svg { | ||||||
|  |   width: 100%; | ||||||
|  | } | ||||||
| .notification:hover .notification--dismiss, | .notification:hover .notification--dismiss, | ||||||
| .notification:focus-within .notification--dismiss { | .notification:focus-within .notification--dismiss { | ||||||
|   opacity: 1; |   opacity: 1; | ||||||
| } | } | ||||||
| .notification.celebration, |  | ||||||
| .notification.information { |  | ||||||
|   background: var(--theme--block_blue); |  | ||||||
|   color: var(--theme--block_blue-text); |  | ||||||
| } |  | ||||||
| .notification.warning, |  | ||||||
| .notification.danger { |  | ||||||
|   background: var(--theme--block_red); |  | ||||||
|   color: var(--theme--block_red-text); |  | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| ::-webkit-scrollbar { | ::-webkit-scrollbar { | ||||||
|   background: transparent; |   background: transparent; | ||||||
|  | |||||||
| @ -8,21 +8,23 @@ | |||||||
|   <body data-view> |   <body data-view> | ||||||
|     <header> |     <header> | ||||||
|       <h1> |       <h1> | ||||||
|         <img data-view-target="notion" alt="" width="24" src="../../icons/colour.svg" /> |         <img data-notion alt="" width="24" src="../../icons/colour.svg" /> | ||||||
|         <a href="?view=library" data-view-target="library">library</a> |         <a href="?view=library">library</a> | ||||||
|       </h1> |       </h1> | ||||||
|       <h1> |       <h1> | ||||||
|         <i data-icon="fa/info"></i><a href="https://notion-enhancer.github.io/">website</a> |         <i data-icon="fa/solid/info"></i> | ||||||
|  |         <a href="https://notion-enhancer.github.io/">website</a> | ||||||
|       </h1> |       </h1> | ||||||
|       <h1> |       <h1> | ||||||
|         <span data-icon="fa/code"></span |         <i data-icon="fa/solid/code"></i> | ||||||
|         ><a href="https://github.com/notion-enhancer/extension">source code</a> |         <a href="https://github.com/notion-enhancer/extension">source code</a> | ||||||
|  |       </h1> | ||||||
|  |       <h1> | ||||||
|  |         <i data-icon="fa/brands/discord"></i> | ||||||
|  |         <a href="https://discord.gg/sFWPXtA">support</a> | ||||||
|       </h1> |       </h1> | ||||||
|       <h1><i data-icon="fa/discord"></i><a href="https://discord.gg/sFWPXtA">support</a></h1> |  | ||||||
|     </header> |     </header> | ||||||
|     <main></main> |     <main></main> | ||||||
|     <section class="tooltip"></section> |  | ||||||
|     <footer class="notification--list"></footer> |  | ||||||
|     <script src="./menu.js" type="module"></script> |     <script src="./menu.js" type="module"></script> | ||||||
|   </body> |   </body> | ||||||
| </html> | </html> | ||||||
|  | |||||||
| @ -9,112 +9,124 @@ | |||||||
| const _id = 'a6621988-551d-495a-97d8-3c568bca2e9e'; | const _id = 'a6621988-551d-495a-97d8-3c568bca2e9e'; | ||||||
| import { env, storage, web, fmt, fs, registry } from '../../api.js'; | import { env, storage, web, fmt, fs, registry } from '../../api.js'; | ||||||
| 
 | 
 | ||||||
| for (const mod of await registry.get((mod) => registry.enabled(mod.id))) { | for (const mod of await registry.get((mod) => registry.isEnabled(mod.id))) { | ||||||
|   for (const sheet of mod.css?.menu || []) { |   for (const sheet of mod.css?.menu || []) { | ||||||
|     web.loadStyleset(`repo/${mod._dir}/${sheet}`); |     web.loadStyleset(`repo/${mod._dir}/${sheet}`); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | async function loadTheme() { | ||||||
|  |   document.documentElement.className = `notion-${ | ||||||
|  |     (await storage.get(_id, 'theme')) || 'dark' | ||||||
|  |   }-theme`;
 | ||||||
|  | } | ||||||
|  | window.addEventListener('focus', loadTheme); | ||||||
|  | loadTheme(); | ||||||
| 
 | 
 | ||||||
| document | document.querySelector('img[data-notion]').addEventListener('click', env.focusNotion); | ||||||
|   .querySelector('img[data-view-target="notion"]') | web.addHotkeyListener(await storage.get(_id, 'hotkey.focustoggle'), env.focusNotion); | ||||||
|   .addEventListener('click', env.focusNotion); | web.addDocumentObserver(web.loadIcons); | ||||||
| web.hotkeyListener(await storage.get(_id, 'hotkey.focustoggle'), env.focusNotion); |  | ||||||
| 
 | 
 | ||||||
| const tooltips = { | const notifications = { | ||||||
|   $el: document.querySelector('.tooltip'), |   $el: web.createElement(web.html`<footer class="notifications"></footer>`), | ||||||
|   add($parent, selector, text) { |   push({ heading, message, icon, color }, onDismiss = () => {}) { | ||||||
|     text = fmt.md.render(text); |     const $notif = web.createElement(web.html` | ||||||
|     $parent.addEventListener('mouseover', (event) => { |     <div role="alert" class="notification" style=" | ||||||
|       if (event.target.matches(selector) || event.target.matches(`${selector} *`)) { |       background: var(--theme--block_${web.escapeHtml(color)}); | ||||||
|         this.$el.innerHTML = text; |       color: var(--theme--block_${web.escapeHtml(color)}-text); | ||||||
|         this.$el.style.display = 'block'; |     "> | ||||||
|       } |       <div><i data-icon="${web.escapeHtml(icon)}"></i></div> | ||||||
|     }); |       <div> | ||||||
|     $parent.addEventListener('mousemove', (event) => { |         <h3>${web.escapeHtml(heading)}</h3> | ||||||
|       this.$el.style.top = event.clientY - this.$el.clientHeight + 'px'; |         <p>${fmt.md.renderInline(message)}</p> | ||||||
|       this.$el.style.left = |       </div> | ||||||
|         event.clientX < window.innerWidth / 2 ? event.clientX + 20 + 'px' : ''; |       <button class="notification--dismiss"><i data-icon="fa/solid/times"></i></button> | ||||||
|     }); |     </div>`); | ||||||
|     $parent.addEventListener('mouseout', (event) => { |     this.$el.append($notif); | ||||||
|       if (event.target.matches(selector) || event.target.matches(`${selector} *`)) { |     setTimeout(() => { | ||||||
|         this.$el.style.display = ''; |       $notif.style.opacity = 1; | ||||||
|       } |     }, 100); | ||||||
|  |     $notif.querySelector('.notification--dismiss').addEventListener('click', (event) => { | ||||||
|  |       $notif.style.opacity = 0; | ||||||
|  |       $notif.style.transform = 'scaleY(0)'; | ||||||
|  |       $notif.style.marginTop = `-${ | ||||||
|  |         $notif.offsetHeight / parseFloat(getComputedStyle(document.documentElement).fontSize) | ||||||
|  |       }rem`;
 | ||||||
|  |       setTimeout(() => $notif.remove(), 400); | ||||||
|  |       onDismiss(); | ||||||
|     }); |     }); | ||||||
|  |     return $notif; | ||||||
|   }, |   }, | ||||||
| }; | }; | ||||||
|  | document.body.append(notifications.$el); | ||||||
|  | for (const error of await registry.errors()) { | ||||||
|  |   notifications.push({ | ||||||
|  |     heading: `error: ${error.source}`, | ||||||
|  |     message: error.message, | ||||||
|  |     color: 'red', | ||||||
|  |     icon: 'fa/solid/exclamation-triangle', | ||||||
|  |   }); | ||||||
|  | } | ||||||
|  | for (const notification of await (async () => { | ||||||
|  |   const dismissed = await storage.get('_notifications', 'dismissed', []); | ||||||
|  |   return (await fs.getJSON('https://notion-enhancer.github.io/notifications.json')) | ||||||
|  |     .sort((a, b) => b.id - a.id) | ||||||
|  |     .filter(({ id }) => !dismissed.includes(id)); | ||||||
|  | })()) { | ||||||
|  |   if ( | ||||||
|  |     (!notification.versions || notification.versions.includes(env.version)) && | ||||||
|  |     (!notification.environments || notification.environments.includes(env.name)) | ||||||
|  |   ) { | ||||||
|  |     notifications.push(notification, async () => { | ||||||
|  |       const dismissed = await storage.get('_notifications', 'dismissed', []); | ||||||
|  |       storage.set('_notifications', 'dismissed', [ | ||||||
|  |         ...new Set([...dismissed, notification.id]), | ||||||
|  |       ]); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | import * as router from './router.js'; | ||||||
| 
 | 
 | ||||||
| const components = {}; | const components = {}; | ||||||
| components.card = { | components.card = async (mod) => { | ||||||
|   preview: ({ preview = '' }) => |   const $card = web.createElement(web.html` | ||||||
|     preview |   <article class="library--card" data-mod='${mod.id}'> | ||||||
|       ? web.createElement(web.html`<img
 |   ${ | ||||||
|  |     mod.preview | ||||||
|  |       ? web.html`<img
 | ||||||
|           alt="" |           alt="" | ||||||
|           class="library--preview" |           class="library--preview" | ||||||
|       src="${web.escapeHtml(preview)}" |           src="${web.escapeHtml(mod.preview)}" | ||||||
|       />`) |         />` | ||||||
|       : '', |       : '' | ||||||
|   async name({ name, id, version, tags }) { |   } | ||||||
|     if (registry.CORE.includes(id)) |   <div> | ||||||
|       return web.createElement(web.html`<div class="library--title"><h2>
 |     <label | ||||||
|       <span> |       for="enable--${web.escapeHtml(mod.id)}" | ||||||
|         ${web.escapeHtml(name)} |  | ||||||
|         <span class="library--version">v${web.escapeHtml(version)}</span> |  | ||||||
|       </span> |  | ||||||
|     </h2></div>`); |  | ||||||
|     const $el = web.createElement(web.html`<label
 |  | ||||||
|       for="enable--${web.escapeHtml(id)}" |  | ||||||
|       class="library--title library--toggle_label" |       class="library--title library--toggle_label" | ||||||
|     > |     > | ||||||
|       <input type="checkbox" id="enable--${web.escapeHtml(id)}" |       <input type="checkbox" id="enable--${web.escapeHtml(mod.id)}" | ||||||
|       ${(await storage.get('_enabled', id, false)) ? 'checked' : ''}/> |       ${(await registry.isEnabled(mod.id)) ? 'checked' : ''}/> | ||||||
|       <h2> |       <h2> | ||||||
|         <span> |         <span> | ||||||
|           ${web.escapeHtml(name)} |           ${web.escapeHtml(mod.name)} | ||||||
|           <span class="library--version">v${web.escapeHtml(version)}</span> |           <span class="library--version">v${web.escapeHtml(mod.version)}</span> | ||||||
|         </span> |         </span> | ||||||
|         <span class="library--toggle"></span> |         ${ | ||||||
|  |           registry.CORE.includes(mod.id) ? '' : web.html`<span class="library--toggle"></span>` | ||||||
|  |         } | ||||||
|       </h2> |       </h2> | ||||||
|     </label>`); |     </label> | ||||||
|     $el.addEventListener('change', async (event) => { |     <ul class="library--tags"> | ||||||
|       storage.set('_enabled', id, event.target.checked); |       ${mod.tags.map((tag) => web.html`<li>#${web.escapeHtml(tag)}</li>`).join('')} | ||||||
|       if ( |     </ul> | ||||||
|         event.target.checked && |     <p class="library--description markdown">${fmt.md.renderInline(mod.description)}</p> | ||||||
|         tags.includes('theme') && |     <ul class="library--authors"> | ||||||
|         (await storage.get(_id, 'themes.autoresolve', true)) |       ${mod.authors | ||||||
|       ) { |  | ||||||
|         const themes = await registry.get( |  | ||||||
|           (mod) => |  | ||||||
|             mod.tags.includes('theme') && |  | ||||||
|             mod.id !== id && |  | ||||||
|             ((mod.tags.includes('dark') && tags.includes('dark')) || |  | ||||||
|               (mod.tags.includes('light') && tags.includes('light'))) |  | ||||||
|         ); |  | ||||||
|         for (const theme of themes) { |  | ||||||
|           if (document.body.dataset.view === 'library') { |  | ||||||
|             const $toggle = document.getElementById(`enable--${theme.id}`); |  | ||||||
|             if ($toggle.checked) $toggle.click(); |  | ||||||
|           } else storage.set('_enabled', theme.id, false); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|     }); |  | ||||||
|     return $el; |  | ||||||
|   }, |  | ||||||
|   tags: ({ tags = [] }) => |  | ||||||
|     web.createElement(web.html`<ul class="library--tags">
 |  | ||||||
|       ${tags.map((tag) => web.html`<li>#${web.escapeHtml(tag)}</li>`).join('')} |  | ||||||
|     </ul>`), |  | ||||||
|   description: ({ description }) => |  | ||||||
|     web.createElement( |  | ||||||
|       web.html`<p class="library--description markdown">${fmt.md.renderInline( |  | ||||||
|         description |  | ||||||
|       )}</p>` |  | ||||||
|     ), |  | ||||||
|   authors: ({ authors }) => |  | ||||||
|     web.createElement(web.html`<ul class="library--authors">
 |  | ||||||
|       ${authors |  | ||||||
|         .map( |         .map( | ||||||
|           (author) => |           (author) => | ||||||
|             web.html`<li>
 |             web.html` | ||||||
|  |             <li> | ||||||
|               <a href="${web.escapeHtml(author.url)}"> |               <a href="${web.escapeHtml(author.url)}"> | ||||||
|                 <img alt="" src="${web.escapeHtml(author.icon)}" /> |                 <img alt="" src="${web.escapeHtml(author.icon)}" /> | ||||||
|                 <span>${web.escapeHtml(author.name)}</span> |                 <span>${web.escapeHtml(author.name)}</span> | ||||||
| @ -122,33 +134,25 @@ components.card = { | |||||||
|             </li>` |             </li>` | ||||||
|         ) |         ) | ||||||
|         .join('')} |         .join('')} | ||||||
|     </ul>`), |     </ul> | ||||||
|   expand: async ({ id }) => |     <p class="library--expand"> | ||||||
|     web.createElement( |       <a href="?view=mod&id=${web.escapeHtml(mod.id)}"> | ||||||
|       web.html`<p class="library--expand">
 |         <span><i data-icon="fa/solid/long-arrow-alt-right"></i></span> | ||||||
|         <a href="?view=mod&id=${web.escapeHtml(id)}"> |  | ||||||
|           <span><i data-icon="fa/long-arrow-alt-right"></i></span> |  | ||||||
|         <span>settings & documentation</span> |         <span>settings & documentation</span> | ||||||
|       </a> |       </a> | ||||||
|       </p>` |     </p> | ||||||
|     ), |   </div> | ||||||
|   async _generate(mod) { |   </article>`); | ||||||
|     const card = web.createElement(web.html`<article class="library--card"></article>`), |   $card.querySelector('.library--title input').addEventListener('change', async (event) => { | ||||||
|       body = web.createElement(web.html`<div></div>`); |     storage.set('_mods', mod.id, event.target.checked); | ||||||
|     card.append(this.preview(mod)); |   }); | ||||||
|     body.append(await this.name(mod)); |   return $card; | ||||||
|     body.append(this.tags(mod)); |  | ||||||
|     body.append(this.description(mod)); |  | ||||||
|     body.append(this.authors(mod)); |  | ||||||
|     body.append(await this.expand(mod)); |  | ||||||
|     card.append(body); |  | ||||||
|     return card; |  | ||||||
|   }, |  | ||||||
| }; | }; | ||||||
| components.options = { | components.options = { | ||||||
|   async toggle(id, { key, label, tooltip }) { |   async toggle(id, { key, label, tooltip }) { | ||||||
|     const state = await storage.get(id, key), |     const state = await storage.get(id, key), | ||||||
|       opt = web.createElement(web.html`<label
 |       opt = web.createElement(web.html` | ||||||
|  |       <label | ||||||
|         for="toggle--${web.escapeHtml(`${id}.${key}`)}" |         for="toggle--${web.escapeHtml(`${id}.${key}`)}" | ||||||
|         class="library--toggle_label" |         class="library--toggle_label" | ||||||
|       > |       > | ||||||
| @ -156,24 +160,25 @@ components.options = { | |||||||
|         ${state ? 'checked' : ''}/> |         ${state ? 'checked' : ''}/> | ||||||
|         <p> |         <p> | ||||||
|           <span data-tooltip>${web.escapeHtml(label)} |           <span data-tooltip>${web.escapeHtml(label)} | ||||||
|         ${tooltip ? web.html`<i data-icon="fa/question-circle"></i>` : ''}</span> |           ${tooltip ? web.html`<i data-icon="fa/solid/question-circle"></i>` : ''}</span> | ||||||
|           <span class="library--toggle"></span> |           <span class="library--toggle"></span> | ||||||
|         </p> |         </p> | ||||||
|       </label>`); |       </label>`); | ||||||
|     opt.addEventListener('change', (event) => storage.set(id, key, event.target.checked)); |     opt.addEventListener('change', (event) => storage.set(id, key, event.target.checked)); | ||||||
|     if (tooltip) tooltips.add(opt, '[data-tooltip]', tooltip); |     if (tooltip) web.addTooltip(opt.querySelector('[data-tooltip]'), tooltip); | ||||||
|     return opt; |     return opt; | ||||||
|   }, |   }, | ||||||
|   async select(id, { key, label, tooltip, values }) { |   async select(id, { key, label, tooltip, values }) { | ||||||
|     const state = await storage.get(id, key), |     const state = await storage.get(id, key), | ||||||
|       opt = web.createElement(web.html`<label
 |       opt = web.createElement(web.html` | ||||||
|  |       <label | ||||||
|         for="select--${web.escapeHtml(`${id}.${key}`)}" |         for="select--${web.escapeHtml(`${id}.${key}`)}" | ||||||
|         class="library--select_label" |         class="library--select_label" | ||||||
|       > |       > | ||||||
|         <p><span data-tooltip>${web.escapeHtml(label)} |         <p><span data-tooltip>${web.escapeHtml(label)} | ||||||
|       ${tooltip ? web.html`<i data-icon="fa/question-circle"></i>` : ''}</span></p> |         ${tooltip ? web.html`<i data-icon="fa/solid/question-circle"></i>` : ''}</span></p> | ||||||
|         <p class="library--select"> |         <p class="library--select"> | ||||||
|         <span><i data-icon="fa/caret-down"></i></span> |           <span><i data-icon="fa/solid/caret-down"></i></span> | ||||||
|           <select id="select--${web.escapeHtml(`${id}.${key}`)}"> |           <select id="select--${web.escapeHtml(`${id}.${key}`)}"> | ||||||
|             ${values.map( |             ${values.map( | ||||||
|               (value) => |               (value) => | ||||||
| @ -185,17 +190,18 @@ components.options = { | |||||||
|         </p> |         </p> | ||||||
|       </label>`); |       </label>`); | ||||||
|     opt.addEventListener('change', (event) => storage.set(id, key, event.target.value)); |     opt.addEventListener('change', (event) => storage.set(id, key, event.target.value)); | ||||||
|     if (tooltip) tooltips.add(opt, '[data-tooltip]', tooltip); |     if (tooltip) web.addTooltip(opt.querySelector('[data-tooltip]'), tooltip); | ||||||
|     return opt; |     return opt; | ||||||
|   }, |   }, | ||||||
|   async text(id, { key, label, tooltip }) { |   async text(id, { key, label, tooltip }) { | ||||||
|     const state = await storage.get(id, key), |     const state = await storage.get(id, key), | ||||||
|       opt = web.createElement(web.html`<label
 |       opt = web.createElement(web.html` | ||||||
|  |       <label | ||||||
|         for="text--${web.escapeHtml(`${id}.${key}`)}" |         for="text--${web.escapeHtml(`${id}.${key}`)}" | ||||||
|         class="library--text_label" |         class="library--text_label" | ||||||
|       > |       > | ||||||
|         <p><span data-tooltip>${web.escapeHtml(label)} |         <p><span data-tooltip>${web.escapeHtml(label)} | ||||||
|       ${tooltip ? web.html`<i data-icon="fa/question-circle"></i>` : ''}</span></p> |         ${tooltip ? web.html`<i data-icon="fa/solid/question-circle"></i>` : ''}</span></p> | ||||||
|         <textarea id="text--${web.escapeHtml(`${id}.${key}`)}" |         <textarea id="text--${web.escapeHtml(`${id}.${key}`)}" | ||||||
|         rows="1">${web.escapeHtml(state)}</textarea> |         rows="1">${web.escapeHtml(state)}</textarea> | ||||||
|       </label>`); |       </label>`); | ||||||
| @ -207,27 +213,29 @@ components.options = { | |||||||
|       ); |       ); | ||||||
|     }); |     }); | ||||||
|     opt.addEventListener('change', (event) => storage.set(id, key, event.target.value)); |     opt.addEventListener('change', (event) => storage.set(id, key, event.target.value)); | ||||||
|     if (tooltip) tooltips.add(opt, '[data-tooltip]', tooltip); |     if (tooltip) web.addTooltip(opt.querySelector('[data-tooltip]'), tooltip); | ||||||
|     return opt; |     return opt; | ||||||
|   }, |   }, | ||||||
|   async number(id, { key, label, tooltip }) { |   async number(id, { key, label, tooltip }) { | ||||||
|     const state = await storage.get(id, key), |     const state = await storage.get(id, key), | ||||||
|       opt = web.createElement(web.html`<label
 |       opt = web.createElement(web.html` | ||||||
|  |       <label | ||||||
|         for="number--${web.escapeHtml(`${id}.${key}`)}" |         for="number--${web.escapeHtml(`${id}.${key}`)}" | ||||||
|         class="library--number_label" |         class="library--number_label" | ||||||
|       > |       > | ||||||
|         <p><span data-tooltip>${web.escapeHtml(label)} |         <p><span data-tooltip>${web.escapeHtml(label)} | ||||||
|       ${tooltip ? web.html`<i data-icon="fa/question-circle"></i>` : ''}</span></p> |         ${tooltip ? web.html`<i data-icon="fa/solid/question-circle"></i>` : ''}</span></p> | ||||||
|         <input id="number--${web.escapeHtml(`${id}.${key}`)}" |         <input id="number--${web.escapeHtml(`${id}.${key}`)}" | ||||||
|         type="number" value="${web.escapeHtml(state.toString())}"/> |         type="number" value="${web.escapeHtml(state.toString())}"/> | ||||||
|       </label>`); |       </label>`); | ||||||
|     opt.addEventListener('change', (event) => storage.set(id, key, event.target.value)); |     opt.addEventListener('change', (event) => storage.set(id, key, event.target.value)); | ||||||
|     if (tooltip) tooltips.add(opt, '[data-tooltip]', tooltip); |     if (tooltip) web.addTooltip(opt.querySelector('[data-tooltip]'), tooltip); | ||||||
|     return opt; |     return opt; | ||||||
|   }, |   }, | ||||||
|   async file(id, { key, label, tooltip, extensions }) { |   async file(id, { key, label, tooltip, extensions }) { | ||||||
|     const state = await storage.get(id, key), |     const state = await storage.get(id, key), | ||||||
|       opt = web.createElement(web.html`<label
 |       opt = web.createElement(web.html` | ||||||
|  |       <label | ||||||
|         for="file--${web.escapeHtml(`${id}.${key}`)}" |         for="file--${web.escapeHtml(`${id}.${key}`)}" | ||||||
|         class="library--file_label" |         class="library--file_label" | ||||||
|       > |       > | ||||||
| @ -241,10 +249,10 @@ components.options = { | |||||||
|           )} |           )} | ||||||
|         /> |         /> | ||||||
|         <p class="library--file_title"><span data-tooltip>${web.escapeHtml(label)} |         <p class="library--file_title"><span data-tooltip>${web.escapeHtml(label)} | ||||||
|       <i data-icon="fa/question-circle"></i></span> |         <i data-icon="fa/solid/question-circle"></i></span> | ||||||
|       <span class="library--file_remove"><i data-icon="fa/minus"></i></span></p> |         <span class="library--file_remove"><i data-icon="fa/solid/minus"></i></span></p> | ||||||
|         <p class="library--file"> |         <p class="library--file"> | ||||||
|         <span><i data-icon="fa/file"></i></span> |           <span><i data-icon="fa/solid/file"></i></span> | ||||||
|           <span class="library--file_path">${web.escapeHtml(state || 'choose file...')}</span> |           <span class="library--file_path">${web.escapeHtml(state || 'choose file...')}</span> | ||||||
|         </p> |         </p> | ||||||
|       </label>`); |       </label>`); | ||||||
| @ -272,281 +280,190 @@ components.options = { | |||||||
|     opt.addEventListener('click', (event) => { |     opt.addEventListener('click', (event) => { | ||||||
|       document.documentElement.scrollTop = 0; |       document.documentElement.scrollTop = 0; | ||||||
|     }); |     }); | ||||||
|     tooltips.add( |     web.addTooltip( | ||||||
|       opt, |       opt.querySelector('[data-tooltip]'), | ||||||
|       '[data-tooltip]', |       `${tooltip ? `${tooltip}\n\n` : ''}**warning:** ${ | ||||||
|       `${ |         'browser extensions do not have true filesystem access, ' + | ||||||
|         tooltip ? `${tooltip}\n\n` : '' |         'so file content is only saved on selection. re-select files to apply edits.' | ||||||
|       }**warning:** browser extensions do not have true filesystem access, |       }` | ||||||
|       so file content is only saved on selection. re-select files to apply edits.` |  | ||||||
|     ); |     ); | ||||||
|     return opt; |     return opt; | ||||||
|   }, |   }, | ||||||
|   async _generate(mod) { | }; | ||||||
|     const card = await components.card._generate(mod); | 
 | ||||||
|     card.querySelector('.library--expand').remove(); | const actionButtons = { | ||||||
|     if (mod.options && mod.options.length) { |   _reloadTriggered: false, | ||||||
|       const options = web.createElement(web.html`<div class="library--options"></div>`), |   async reload($fragment = document) { | ||||||
|         inputs = await Promise.all( |     let $reload = $fragment.querySelector('[data-reload]'); | ||||||
|           mod.options |     if (!$reload) { | ||||||
|             .filter((opt) => !opt.environments || opt.environments.includes(env.name)) |       $reload = web.createElement(web.html` | ||||||
|             .map((opt) => this[opt.type](mod.id, opt)) |       <button class="action--alert" data-reload> | ||||||
|         ); |         <span><i data-icon="fa/solid/redo"></i></span> | ||||||
|       inputs.forEach((opt) => options.append(opt)); |         <span>reload tabs to apply changes</span> | ||||||
|       card.append(options); |       </button>`); | ||||||
|  |       $reload.addEventListener('click', env.reloadTabs); | ||||||
|     } |     } | ||||||
|     return card; |     if (this._reloadTriggered) { | ||||||
|  |       $fragment.querySelector('.action--buttons').append($reload); | ||||||
|  |       await new Promise((res, rej) => requestAnimationFrame(res)); | ||||||
|  |       $reload.dataset.triggered = true; | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   async clearFilters($fragment = document) { | ||||||
|  |     let $clearFilters = $fragment.querySelector('[data-clear-filters]'); | ||||||
|  |     if (!$clearFilters) { | ||||||
|  |       $clearFilters = web.createElement(web.html` | ||||||
|  |       <a class="action--alert" href="?view=library" data-clear-filters> | ||||||
|  |         <span><i data-icon="fa/solid/times"></i></span> | ||||||
|  |         <span>clear filters</span> | ||||||
|  |       </a>`); | ||||||
|  |     } | ||||||
|  |     const search = router.getSearch(); | ||||||
|  |     if (search.get('tag') || search.has('enabled') || search.has('disabled')) { | ||||||
|  |       $fragment.querySelector('.action--buttons').append($clearFilters); | ||||||
|  |       await new Promise((res, rej) => requestAnimationFrame(res)); | ||||||
|  |       $clearFilters.dataset.triggered = true; | ||||||
|  |     } else $clearFilters.remove(); | ||||||
|   }, |   }, | ||||||
| }; | }; | ||||||
| components.documentation = { | storage.addChangeListener(async (event) => { | ||||||
|   _reloadTriggered: false, |   actionButtons._reloadTriggered = true; | ||||||
|   buttons({ _dir }) { |   actionButtons.reload(); | ||||||
|     const $el = web.createElement(web.html`<p class="documentation--buttons">
 |   router.load(); | ||||||
|  | 
 | ||||||
|  |   if (event.namespace === '_mods' && event.new === true) { | ||||||
|  |     const enabledTheme = (await registry.get()).find((mod) => mod.id === event.key); | ||||||
|  |     if ( | ||||||
|  |       enabledTheme.tags.includes('theme') && | ||||||
|  |       (await storage.get(_id, 'themes.autoresolve', true)) | ||||||
|  |     ) { | ||||||
|  |       for (const theme of await registry.get( | ||||||
|  |         (mod) => | ||||||
|  |           mod.tags.includes('theme') && | ||||||
|  |           mod.id !== enabledTheme.id && | ||||||
|  |           ((mod.tags.includes('dark') && enabledTheme.tags.includes('dark')) || | ||||||
|  |             (mod.tags.includes('light') && enabledTheme.tags.includes('light'))) | ||||||
|  |       )) { | ||||||
|  |         if (document.body.dataset.view === 'library') { | ||||||
|  |           const $toggle = document.getElementById(`enable--${theme.id}`); | ||||||
|  |           if ($toggle.checked) $toggle.click(); | ||||||
|  |         } else storage.set('_mods', theme.id, false); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | router.addView( | ||||||
|  |   'library', | ||||||
|  |   async () => { | ||||||
|  |     const $fragment = web.createFragment(web.html` | ||||||
|  |     <p class="action--buttons"> | ||||||
|  |       <a href="?view=library&tag=theme"> | ||||||
|  |         <span><i data-icon="fa/solid/palette"></i></span> | ||||||
|  |         <span>themes</span> | ||||||
|  |       </a> | ||||||
|  |       <a href="?view=library&tag=extension"> | ||||||
|  |         <span><i data-icon="fa/solid/plus"></i></span> | ||||||
|  |         <span>extensions</span> | ||||||
|  |       </a> | ||||||
|  |       <a href="?view=library&enabled"> | ||||||
|  |         <span><i data-icon="fa/solid/toggle-on"></i></span> | ||||||
|  |         <span>enabled</span> | ||||||
|  |       </a> | ||||||
|  |       <a href="?view=library&disabled"> | ||||||
|  |         <span><i data-icon="fa/solid/toggle-off"></i></span> | ||||||
|  |         <span>disabled</span> | ||||||
|  |       </a> | ||||||
|  |     </p>`); | ||||||
|  |     for (const mod of await registry.get( | ||||||
|  |       (mod) => !mod.environments || mod.environments.includes(env.name) | ||||||
|  |     )) { | ||||||
|  |       $fragment.append(await components.card(mod)); | ||||||
|  |     } | ||||||
|  |     actionButtons.reload($fragment); | ||||||
|  |     actionButtons.clearFilters($fragment); | ||||||
|  |     return $fragment; | ||||||
|  |   }, | ||||||
|  |   async (search = router.getSearch()) => { | ||||||
|  |     for (const [filter, active] of [ | ||||||
|  |       ['tag=theme', search.get('tag') === 'theme'], | ||||||
|  |       ['tag=extension', search.get('tag') === 'extension'], | ||||||
|  |       ['enabled', search.has('enabled')], | ||||||
|  |       ['disabled', search.has('disabled')], | ||||||
|  |     ]) { | ||||||
|  |       document | ||||||
|  |         .querySelector(`.action--buttons > [href="?view=library&${filter}"]`) | ||||||
|  |         .classList[active ? 'add' : 'remove']('action--active'); | ||||||
|  |     } | ||||||
|  |     for (const card of document.querySelectorAll('main > .library--card')) { | ||||||
|  |       const { tags } = (await registry.get()).find((mod) => mod.id === card.dataset.mod), | ||||||
|  |         isEnabled = await registry.isEnabled(card.dataset.mod); | ||||||
|  |       if ( | ||||||
|  |         (search.has('tag') ? tags.includes(search.get('tag')) : true) && | ||||||
|  |         (search.has('enabled') && search.has('disabled') | ||||||
|  |           ? true | ||||||
|  |           : search.has('enabled') | ||||||
|  |           ? isEnabled | ||||||
|  |           : search.has('disabled') | ||||||
|  |           ? !isEnabled | ||||||
|  |           : true) | ||||||
|  |       ) { | ||||||
|  |         card.style.display = ''; | ||||||
|  |       } else card.style.display = 'none'; | ||||||
|  |     } | ||||||
|  |     actionButtons.clearFilters(); | ||||||
|  |   } | ||||||
|  | ); | ||||||
|  | 
 | ||||||
|  | router.addView( | ||||||
|  |   'mod', | ||||||
|  |   async () => { | ||||||
|  |     const mod = (await registry.get()).find((mod) => mod.id === router.getSearch().get('id')); | ||||||
|  |     if (!mod) return false; | ||||||
|  |     const $fragment = web.createFragment(web.html` | ||||||
|  |     <p class="action--buttons"> | ||||||
|       <a href="?view=library"> |       <a href="?view=library"> | ||||||
|         <span><i data-icon="fa/long-arrow-alt-left"></i></span> |         <span><i data-icon="fa/solid/long-arrow-alt-left"></i></span> | ||||||
|         <span>back to library</span> |         <span>back to library</span> | ||||||
|       </a> |       </a> | ||||||
|       <a |       <a href="https://github.com/notion-enhancer/extension/tree/main/repo/${encodeURIComponent(
 | ||||||
|         href="https://github.com/notion-enhancer/extension/tree/main/repo/${encodeURIComponent(
 |         mod._dir | ||||||
|           _dir |       )}"> | ||||||
|         )}" |         <span><i data-icon="fa/solid/code"></i></span> | ||||||
|       > |  | ||||||
|         <span><i data-icon="fa/code"></i></span> |  | ||||||
|         <span>view source code</span> |         <span>view source code</span> | ||||||
|       </a> |       </a> | ||||||
|       <button class="documentation--reload"${this._reloadTriggered ? ' data-triggered' : ''}> |  | ||||||
|         <span><i data-icon="fa/redo"></i></span> |  | ||||||
|         <span>reload tabs to apply changes</span> |  | ||||||
|       </button> |  | ||||||
|     </p>`); |     </p>`); | ||||||
|     storage.onChange(() => { |     const $card = await components.card(mod); | ||||||
|       const $reload = $el.querySelector('.documentation--reload'); |     $card.querySelector('.library--expand').remove(); | ||||||
|       if (document.body.contains($el) && !$reload.dataset.triggered) { |     if (mod.options && mod.options.length) { | ||||||
|         $reload.dataset.triggered = true; |       const options = web.createElement(web.html`<div class="library--options"></div>`); | ||||||
|         this._reloadTriggered = true; |       mod.options | ||||||
|  |         .filter((opt) => !opt.environments || opt.environments.includes(env.name)) | ||||||
|  |         .forEach(async (opt) => | ||||||
|  |           options.append(await components.options[opt.type](mod.id, opt)) | ||||||
|  |         ); | ||||||
|  |       $card.append(options); | ||||||
|     } |     } | ||||||
|     }); |     $fragment.append( | ||||||
|     $el.querySelector('.documentation--reload').addEventListener('click', env.reloadTabs); |       $card, | ||||||
|     return $el; |       web.createElement(web.html` | ||||||
|   }, |       <article class="documentation--body markdown"> | ||||||
|   readme: async (mod) => { |  | ||||||
|     const readme = web.createElement(web.html`<article class="documentation--body markdown">
 |  | ||||||
|         ${ |         ${ | ||||||
|           (await fs.isFile(`repo/${mod._dir}/README.md`)) |           (await fs.isFile(`repo/${mod._dir}/README.md`)) | ||||||
|             ? fmt.md.render(await fs.getText(`repo/${mod._dir}/README.md`)) |             ? fmt.md.render(await fs.getText(`repo/${mod._dir}/README.md`)) | ||||||
|             : '' |             : '' | ||||||
|         } |         } | ||||||
|     </article>`); |       </article>`) | ||||||
|     fmt.Prism.highlightAllUnder(readme); |  | ||||||
|     return readme; |  | ||||||
|   }, |  | ||||||
| }; |  | ||||||
| const views = { |  | ||||||
|   $container: document.querySelector('main'), |  | ||||||
|   _router(event) { |  | ||||||
|     event.preventDefault(); |  | ||||||
|     let anchor, |  | ||||||
|       i = 0; |  | ||||||
|     do { |  | ||||||
|       anchor = event.path[i]; |  | ||||||
|       i++; |  | ||||||
|     } while (anchor.nodeName !== 'A'); |  | ||||||
|     if (location.search !== anchor.getAttribute('href')) { |  | ||||||
|       window.history.pushState( |  | ||||||
|         { search: anchor.getAttribute('href'), hash: '' }, |  | ||||||
|         '', |  | ||||||
|         anchor.href |  | ||||||
|     ); |     ); | ||||||
|       this._load(); |     fmt.Prism.highlightAllUnder($fragment); | ||||||
|  |     actionButtons.reload($fragment); | ||||||
|  |     return $fragment; | ||||||
|  |   }, | ||||||
|  |   () => { | ||||||
|  |     if (document.querySelector('[data-mod]').dataset.mod !== router.getSearch().get('id')) | ||||||
|  |       router.load(true); | ||||||
|   } |   } | ||||||
|   }, | ); | ||||||
|   _navigator(event) { |  | ||||||
|     event.preventDefault(); |  | ||||||
|     const hash = event.target.getAttribute('href').slice(1); |  | ||||||
|     document.getElementById(hash).scrollIntoView(true); |  | ||||||
|     document.documentElement.scrollTop = 0; |  | ||||||
|     history.replaceState({ search: location.search, hash }, null, `#${hash}`); |  | ||||||
|   }, |  | ||||||
|   _reset() { |  | ||||||
|     document |  | ||||||
|       .querySelectorAll('a[href^="?"]') |  | ||||||
|       .forEach((a) => a.removeEventListener('click', this._router)); |  | ||||||
|     document |  | ||||||
|       .querySelectorAll('a[href^="#"]') |  | ||||||
|       .forEach((a) => a.removeEventListener('click', this._navigator)); |  | ||||||
|     this.$container.style.opacity = 0; |  | ||||||
|     return new Promise((res, rej) => { |  | ||||||
|       setTimeout(() => { |  | ||||||
|         this.$container.innerHTML = ''; |  | ||||||
|         this.$container.style.opacity = ''; |  | ||||||
|         document.body.dataset.view = ''; |  | ||||||
|         document |  | ||||||
|           .querySelector('[data-view-target][data-view-active]') |  | ||||||
|           ?.removeAttribute('data-view-active'); |  | ||||||
|         res(); |  | ||||||
|       }, 200); |  | ||||||
|     }); |  | ||||||
|   }, |  | ||||||
|   async _load() { |  | ||||||
|     await this._reset(); |  | ||||||
| 
 | 
 | ||||||
|     const search = new Map( | router.setDefaultView('library'); | ||||||
|       location.search | router.load(); | ||||||
|         .slice(1) |  | ||||||
|         .split('&') |  | ||||||
|         .map((query) => query.split('=')) |  | ||||||
|     ); |  | ||||||
|     switch (search.get('view')) { |  | ||||||
|       case 'mod': |  | ||||||
|         const mod = (await registry.get()).find((mod) => mod.id === search.get('id')); |  | ||||||
|         if (mod) { |  | ||||||
|           await this.mod(mod); |  | ||||||
|           break; |  | ||||||
|         } |  | ||||||
|       case 'library': |  | ||||||
|         await this.library(); |  | ||||||
|         break; |  | ||||||
|       default: |  | ||||||
|         window.history.replaceState( |  | ||||||
|           { search: '?view=library', hash: '' }, |  | ||||||
|           null, |  | ||||||
|           '?view=library' |  | ||||||
|         ); |  | ||||||
|         return this._load(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     setTimeout(() => { |  | ||||||
|       document.getElementById(location.hash.slice(1))?.scrollIntoView(true); |  | ||||||
|       document.documentElement.scrollTop = 0; |  | ||||||
|     }, 50); |  | ||||||
|     document |  | ||||||
|       .querySelectorAll('img') |  | ||||||
|       .forEach((img) => (img.onerror = (event) => event.target.remove())); |  | ||||||
|     document |  | ||||||
|       .querySelectorAll('a[href^="?"]') |  | ||||||
|       .forEach((a) => a.addEventListener('click', this._router)); |  | ||||||
|     document |  | ||||||
|       .querySelectorAll('a[href^="#"]') |  | ||||||
|       .forEach((a) => a.addEventListener('click', this._navigator)); |  | ||||||
|     document.querySelectorAll('[data-icon]').forEach((icon) => |  | ||||||
|       fs.getText(`icons/${icon.dataset.icon}.svg`).then((svg) => { |  | ||||||
|         svg = web.createElement(svg); |  | ||||||
|         for (const attr of icon.attributes) { |  | ||||||
|           svg.setAttribute(attr.name, attr.value); |  | ||||||
|         } |  | ||||||
|         icon.replaceWith(svg); |  | ||||||
|       }) |  | ||||||
|     ); |  | ||||||
|   }, |  | ||||||
|   async mod(mod) { |  | ||||||
|     document.body.dataset.view = 'mod'; |  | ||||||
|     document.querySelector('header [data-view-target="library"]').dataset.active = true; |  | ||||||
|     this.$container.append(components.documentation.buttons(mod)); |  | ||||||
|     this.$container.append(await components.options._generate(mod)); |  | ||||||
|     this.$container.append(await components.documentation.readme(mod)); |  | ||||||
|   }, |  | ||||||
|   async library() { |  | ||||||
|     document.body.dataset.view = 'library'; |  | ||||||
|     document.querySelector('header [data-view-target="library"]').dataset.active = true; |  | ||||||
|     for (const mod of await registry.get( |  | ||||||
|       (mod) => !mod.environments || mod.environments.includes(env.name) |  | ||||||
|     )) { |  | ||||||
|       this.$container.append(await components.card._generate(mod)); |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
| }; |  | ||||||
| views._router = views._router.bind(views); |  | ||||||
| views._navigator = views._navigator.bind(views); |  | ||||||
| views._load(); |  | ||||||
| window.addEventListener('popstate', (event) => { |  | ||||||
|   if (event.state) views._load(); |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| const notifications = { |  | ||||||
|   $list: document.querySelector('.notification--list'), |  | ||||||
|   push({ heading, message = '', type = 'information' }, onDismiss = () => {}) { |  | ||||||
|     let svg = '', |  | ||||||
|       className = 'notification'; |  | ||||||
|     switch (type) { |  | ||||||
|       case 'celebration': |  | ||||||
|         svg = web.html`<i data-icon="monster/party"></i>`; |  | ||||||
|         className += ' celebration'; |  | ||||||
|         break; |  | ||||||
|       case 'information': |  | ||||||
|         svg = web.html`<i data-icon="fa/info"></i>`; |  | ||||||
|         className += ' information'; |  | ||||||
|         break; |  | ||||||
|       case 'warning': |  | ||||||
|         svg = web.html`<i data-icon="fa/exclamation-triangle"></i>`; |  | ||||||
|         className += ' warning'; |  | ||||||
|         break; |  | ||||||
|     } |  | ||||||
|     const $notif = web.createElement(web.html`<div role="alert" class="${className}">
 |  | ||||||
|       <div>${svg}</div> |  | ||||||
|       <div> |  | ||||||
|         <h3>${web.escapeHtml(heading)}</h3> |  | ||||||
|         <p>${fmt.md.renderInline(message)}</p> |  | ||||||
|       </div> |  | ||||||
|       <button class="notification--dismiss">×</button> |  | ||||||
|     </div>`); |  | ||||||
|     $notif.querySelector('.notification--dismiss').addEventListener('click', (event) => { |  | ||||||
|       $notif.style.opacity = 0; |  | ||||||
|       $notif.style.transform = 'scaleY(0)'; |  | ||||||
|       $notif.style.marginTop = `-${ |  | ||||||
|         $notif.offsetHeight / parseFloat(getComputedStyle(document.documentElement).fontSize) |  | ||||||
|       }rem`;
 |  | ||||||
|       setTimeout(() => $notif.remove(), 400); |  | ||||||
|       onDismiss(); |  | ||||||
|     }); |  | ||||||
|     setTimeout(() => { |  | ||||||
|       $notif.style.opacity = 1; |  | ||||||
|     }, 100); |  | ||||||
|     return this.$list.append($notif); |  | ||||||
|   }, |  | ||||||
|   async fetch() { |  | ||||||
|     const notifications = { |  | ||||||
|       list: await fs.getJSON('https://notion-enhancer.github.io/notifications.json'), |  | ||||||
|       dismissed: await storage.get(_id, 'notifications', []), |  | ||||||
|     }; |  | ||||||
|     notifications.list = notifications.list.sort((a, b) => b.id - a.id); |  | ||||||
|     notifications.waiting = notifications.list.filter( |  | ||||||
|       ({ id }) => !notifications.dismissed.includes(id) |  | ||||||
|     ); |  | ||||||
|     for (const notification of notifications.waiting) { |  | ||||||
|       if ( |  | ||||||
|         notification.heading && |  | ||||||
|         notification.appears_on && |  | ||||||
|         (notification.appears_on.versions.includes('*') || |  | ||||||
|           notification.appears_on.versions.includes(env.version)) && |  | ||||||
|         notification.appears_on.extension |  | ||||||
|       ) { |  | ||||||
|         this.push(notification, async () => { |  | ||||||
|           const dismissed = await storage.get(_id, 'notifications', []); |  | ||||||
|           storage.set('_notifications', 'external', [ |  | ||||||
|             ...new Set([...dismissed, notification.id]), |  | ||||||
|           ]); |  | ||||||
|         }); |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
| }; |  | ||||||
| for (const error of await registry.errors()) { |  | ||||||
|   notifications.push({ |  | ||||||
|     heading: `error: ${error.source}`, |  | ||||||
|     message: error.message, |  | ||||||
|     type: 'warning', |  | ||||||
|   }); |  | ||||||
| } |  | ||||||
| notifications.fetch(); |  | ||||||
| 
 |  | ||||||
| async function theme() { |  | ||||||
|   document.documentElement.className = `notion-${ |  | ||||||
|     (await storage.get(_id, 'theme')) || 'dark' |  | ||||||
|   }-theme`;
 |  | ||||||
| } |  | ||||||
| window.addEventListener('focus', theme); |  | ||||||
| theme(); |  | ||||||
| 
 |  | ||||||
| // registry.errors().then((err) => {
 |  | ||||||
| //   document.querySelector('[data-section="alerts"]').innerHTML = JSON.stringify(err);
 |  | ||||||
| // });
 |  | ||||||
|  | |||||||
| @ -13,8 +13,9 @@ | |||||||
|     } |     } | ||||||
|   ], |   ], | ||||||
|   "css": { |   "css": { | ||||||
|     "client": ["client.css"], |     "frame": ["tooltips.css"], | ||||||
|     "menu": ["menu.css", "markdown.css"] |     "client": ["client.css", "tooltips.css"], | ||||||
|  |     "menu": ["menu.css", "markdown.css", "tooltips.css"] | ||||||
|   }, |   }, | ||||||
|   "js": { |   "js": { | ||||||
|     "client": ["client.js"] |     "client": ["client.js"] | ||||||
|  | |||||||
| @ -0,0 +1,100 @@ | |||||||
|  | /* | ||||||
|  |  * notion-enhancer core: menu | ||||||
|  |  * (c) 2021 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
 | ||||||
|  |  * (https://notion-enhancer.github.io/) under the MIT license
 | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | 'use strict'; | ||||||
|  | 
 | ||||||
|  | import { web } from '../../api.js'; | ||||||
|  | 
 | ||||||
|  | export const getSearch = () => | ||||||
|  |   new Map( | ||||||
|  |     location.search | ||||||
|  |       .slice(1) | ||||||
|  |       .split('&') | ||||||
|  |       .map((query) => query.split('=')) | ||||||
|  |   ); | ||||||
|  | 
 | ||||||
|  | let defaultView = ''; | ||||||
|  | const views = new Map(), | ||||||
|  |   filters = new Map(); | ||||||
|  | 
 | ||||||
|  | export function setDefaultView(name) { | ||||||
|  |   defaultView = name; | ||||||
|  | } | ||||||
|  | export function addView(name, loader, filter = () => {}) { | ||||||
|  |   views.set(name, loader); | ||||||
|  |   filters.set(name, filter); | ||||||
|  | } | ||||||
|  | export function removeView(name) { | ||||||
|  |   views.delete(name); | ||||||
|  |   filters.delete(name); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function router(event) { | ||||||
|  |   event.preventDefault(); | ||||||
|  |   const anchor = event.path.find((anchor) => anchor.nodeName === 'A'); | ||||||
|  |   if (location.search !== anchor.getAttribute('href')) { | ||||||
|  |     window.history.pushState( | ||||||
|  |       { search: anchor.getAttribute('href'), hash: '' }, | ||||||
|  |       '', | ||||||
|  |       anchor.href | ||||||
|  |     ); | ||||||
|  |     load(); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | function navigator(event) { | ||||||
|  |   event.preventDefault(); | ||||||
|  |   const anchor = event.path.find((anchor) => anchor.nodeName === 'A'), | ||||||
|  |     hash = anchor.getAttribute('href').slice(1); | ||||||
|  |   document.getElementById(hash).scrollIntoView(true); | ||||||
|  |   document.documentElement.scrollTop = 0; | ||||||
|  |   history.replaceState({ search: location.search, hash }, null, `#${hash}`); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export async function load(force = false) { | ||||||
|  |   const $container = document.querySelector('main'), | ||||||
|  |     search = getSearch(), | ||||||
|  |     fallbackView = () => | ||||||
|  |       window.history.replaceState( | ||||||
|  |         { search: `?view=${defaultView}`, hash: '' }, | ||||||
|  |         null, | ||||||
|  |         `?view=${defaultView}` | ||||||
|  |       ); | ||||||
|  |   if (force || !search.get('view') || document.body.dataset.view !== search.get('view')) { | ||||||
|  |     if (views.get(search.get('view'))) { | ||||||
|  |       const $body = await (views.get(search.get('view')) || (() => void 0))(); | ||||||
|  |       if ($body) { | ||||||
|  |         $container.style.opacity = 0; | ||||||
|  |         await new Promise((res, rej) => | ||||||
|  |           setTimeout(() => { | ||||||
|  |             document.body.dataset.view = search.get('view'); | ||||||
|  |             $container.innerHTML = ''; | ||||||
|  |             $container.append($body); | ||||||
|  |             requestAnimationFrame(() => { | ||||||
|  |               $container.style.opacity = ''; | ||||||
|  |               setTimeout(res, 200); | ||||||
|  |             }); | ||||||
|  |           }, 200) | ||||||
|  |         ); | ||||||
|  |       } else return fallbackView(); | ||||||
|  |     } else return fallbackView(); | ||||||
|  |   } | ||||||
|  |   if (filters.get(search.get('view'))) filters.get(search.get('view'))(search); | ||||||
|  | } | ||||||
|  | window.addEventListener('popstate', (event) => { | ||||||
|  |   if (event.state) load(); | ||||||
|  |   document.getElementById(location.hash.slice(1))?.scrollIntoView(true); | ||||||
|  |   document.documentElement.scrollTop = 0; | ||||||
|  | }); | ||||||
|  | web.addDocumentObserver((mutation) => { | ||||||
|  |   mutation.target.querySelectorAll('a[href^="?"]').forEach((a) => { | ||||||
|  |     a.removeEventListener('click', router); | ||||||
|  |     a.addEventListener('click', router); | ||||||
|  |   }); | ||||||
|  |   mutation.target.querySelectorAll('a[href^="#"]').forEach((a) => { | ||||||
|  |     a.removeEventListener('click', navigator); | ||||||
|  |     a.addEventListener('click', navigator); | ||||||
|  |   }); | ||||||
|  | }); | ||||||
| @ -0,0 +1,20 @@ | |||||||
|  | /* | ||||||
|  |  * notion-enhancer core: tooltips | ||||||
|  |  * (c) 2021 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/) | ||||||
|  |  * (https://notion-enhancer.github.io/) under the MIT license | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | .enhancer--tooltip { | ||||||
|  |   position: absolute; | ||||||
|  |   background: var(--theme--tooltip); | ||||||
|  |   color: var(--theme--tooltip-text); | ||||||
|  |   font-size: var(--theme--font_ui_small-size); | ||||||
|  |   padding: 0.15rem 0.4rem; | ||||||
|  |   box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06) !important; | ||||||
|  |   border-radius: 3px; | ||||||
|  |   max-width: 20rem; | ||||||
|  |   display: none; | ||||||
|  | } | ||||||
|  | .enhancer--tooltip p { | ||||||
|  |   margin: 0.25rem 0; | ||||||
|  | } | ||||||
| @ -14,9 +14,8 @@ web.whenReady().then(async () => { | |||||||
|   if (cssInsert) { |   if (cssInsert) { | ||||||
|     document.body.append( |     document.body.append( | ||||||
|       web.createElement( |       web.createElement( | ||||||
|         web.html`<style id="${await storage.get(_id, 'insert.css')}@${_id}">
 |         web.html` | ||||||
|         ${cssInsert} |         <style id="${await storage.get(_id, 'insert.css')}@${_id}">${cssInsert}</style>` | ||||||
|         </style>` |  | ||||||
|       ) |       ) | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user