Emoji favicons for the web
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix: code cleanup

+102 -126
+7 -9
README.md
··· 1 - > This README is for Favioli 2.00, which is not currently released. 2 - > Use [`main`](https://github.com/ivebencrazy/favioli) to see the code of the currently deployed Favioli. 3 - 4 1 # Favioli 🤯 5 2 6 3 <p> ··· 24 21 deno install --name=bext --allow-read --allow-write --allow-run --allow-env -f https://deno.land/x/bext/main.ts 25 22 ``` 26 23 27 - | Commands | What they Do | 28 - | ---------------- | ----------------------------------- | 29 - | `bext` | bundles extension and watch code | 30 - | `bext chrome` | bundles extension only for chrome | 31 - | `bext firefox` | bundles extension only for firefox | 32 - | `deno task test` | run code formatter, then unit tests | 24 + | Commands | What they Do | 25 + | ----------------------- | ----------------------------------- | 26 + | `bext` | bundles extension and watch code | 27 + | `bext chrome` | bundles extension only for chrome | 28 + | `bext firefox` | bundles extension only for firefox | 29 + | `deno task test:all` | run code formatter, then unit tests | 30 + | `deno task test:update` | run code formatter, then unit tests | 33 31 34 32 If you have bundled using make commands, you should be able to load your 35 33 unpacked extension using a browser.
+2 -2
deno.json
··· 26 26 "tasks": { 27 27 "test": "deno test -A source", 28 28 "test:all": "deno fmt && deno task check:all && deno task test && deno lint", 29 + "test:update": "deno test -A -- --update source", 29 30 "check:all": "deno task check:background && deno task check:content_script && deno task check:options && deno task check:popup", 30 31 "check:background": "deno check source/background.ts", 31 32 "check:content_script": "deno check source/content_script.ts", 32 33 "check:options": "deno check source/options.tsx", 33 - "check:popup": "deno check source/popup.tsx", 34 - "test:update": "deno test -A -- --update source" 34 + "check:popup": "deno check source/popup.tsx" 35 35 } 36 36 }
+5 -5
source/components/emoji_selector/components/custom_delete.tsx
··· 1 1 /* @jsx h */ 2 + import type { BrowserStorage } from '../../../hooks/use_browser_storage.ts'; 2 3 import type { EmojiMap } from '../../../models/emoji.ts'; 4 + import type { Settings } from '../../../models/settings.ts'; 3 5 import type { SetRoute } from '../types.ts'; 4 - import type { Settings } from '../../../models/settings.ts'; 5 6 6 7 import { Fragment, h } from 'preact'; 7 8 import { useCallback, useContext, useState } from 'preact/hooks'; 8 9 9 - import { SettingsContext } from '../../../models/settings.ts'; 10 10 import { deleteEmoji, emoji } from '../../../models/emoji.ts'; 11 - import { createFaviconURLFromImage } from '../../../utilities/image_helpers.ts'; 11 + import { SettingsContext } from '../../../models/settings.ts'; 12 12 import Only from '../../only.tsx'; 13 13 import { ROUTE } from '../types.ts'; 14 14 import EmojiButton from './emoji_button.tsx'; ··· 24 24 const { cache, saveToStorageBypassCache } = settings; 25 25 return ( 26 26 <div className='emoji-custom-upload'> 27 - <div classname='emoji-group'> 27 + <div className='emoji-group'> 28 28 {Object.keys(customEmojis) 29 29 .map((name) => { 30 30 const emoji = customEmojis[name]; ··· 38 38 await saveToStorageBypassCache({ 39 39 ...cache, 40 40 customEmojiIds: cache.customEmojiIds 41 - .filter((desc) => desc !== emoji.description), 41 + .filter((desc: string) => desc !== emoji.description), 42 42 }); 43 43 setRoute(ROUTE.DEFAULT); 44 44 }
+45 -46
source/components/emoji_selector/components/custom_upload.tsx
··· 1 1 /* @jsx h */ 2 + import type { BrowserStorage } from '../../../hooks/use_browser_storage.ts'; 3 + import type { Settings } from '../../../models/settings.ts'; 2 4 import type { SetRoute } from '../types.ts'; 3 5 4 - import { Fragment, h } from 'preact'; 5 - import { useCallback, useState } from 'preact/hooks'; 6 + import { h } from 'preact'; 7 + import { useCallback, useContext, useState } from 'preact/hooks'; 6 8 7 - import { emoji } from '../../../models/emoji.ts'; 9 + import { createEmoji, emoji, saveEmoji } from '../../../models/emoji.ts'; 10 + import { SettingsContext } from '../../../models/settings.ts'; 8 11 import { createFaviconURLFromImage } from '../../../utilities/image_helpers.ts'; 9 12 import Only from '../../only.tsx'; 10 13 import { ROUTE } from '../types.ts'; 11 14 12 - export default function CustomUpload( 13 - { setRoute, submitCustomEmoji }: { 14 - setRoute: SetRoute; 15 - submitCustomEmoji: (name: string, image: string) => Promise<void>; 16 - }, 17 - ) { 18 - const [image, setSelectedEmoji] = useState(''); 19 - const [name, setName] = useState<string>(''); 15 + export default function CustomUpload({ setRoute }: { setRoute: SetRoute }) { 16 + const settings = useContext<BrowserStorage<Settings>>(SettingsContext); 17 + const { cache, saveToStorageBypassCache } = settings; 18 + const { customEmojiIds } = cache; 20 19 21 - const updateImage = useCallback(async (event: Event) => { 20 + const [imageURL, setImageURL] = useState(''); 21 + const [description, setDescription] = useState<string>(''); 22 + 23 + const updateImageURL = useCallback(async (event: Event) => { 22 24 if (event.target instanceof HTMLInputElement) { 23 25 const file = event.target?.files?.[0]; 24 - if (file?.name && !name) setName(file.name.match(/(.*)\..*$/)?.[1] || ''); 25 - const url = await createFaviconURLFromImage( 26 - URL.createObjectURL(file as Blob), 27 - ); 28 - setSelectedEmoji(url); 26 + const name = (file?.name || '').match(/(.*)\..*$/)?.[1] || ''; 27 + if (name && !description) setDescription(name); // Autofill desc if none 28 + if (file instanceof Blob) { 29 + setImageURL(await createFaviconURLFromImage(URL.createObjectURL(file))); 30 + } 31 + } 32 + }, [description, setImageURL, setDescription]); 33 + 34 + const updateDescription = useCallback(({ target }: Event) => { 35 + if (target instanceof HTMLInputElement) setDescription(target.value || ''); 36 + }, [setDescription]); 37 + 38 + const saveCustomEmoji = useCallback(async () => { 39 + try { 40 + await saveEmoji(createEmoji(description, imageURL)); 41 + const deduped = Array.from(new Set(customEmojiIds.concat(description))); 42 + await saveToStorageBypassCache({ ...cache, customEmojiIds: deduped }); 43 + setRoute(ROUTE.DEFAULT); 44 + } catch (e) { 45 + confirm(e); 29 46 } 30 - }, [setSelectedEmoji, setName, name]); 47 + }, [cache, description, imageURL]); 48 + 49 + const goBack = useCallback(() => setRoute(ROUTE.DEFAULT), [setRoute]); 31 50 32 51 return ( 33 52 <div className='emoji-custom-upload'> 34 53 <div>Upload Custom Favicon</div> 35 - 36 54 <div className='emoji-custom-upload-form'> 37 55 <label className='emoji-group-title'>Favicon Image</label> 38 - <input name='Favicon Image' type='file' onChange={updateImage} /> 39 - <Only if={Boolean(image)}> 40 - <img width={100} height={100} src={image} className='preview' /> 56 + <input name='Favicon Image' type='file' onChange={updateImageURL} /> 57 + <Only if={Boolean(imageURL)}> 58 + <img className='preview' width={100} height={100} src={imageURL} /> 41 59 </Only> 42 60 43 - <label style='margin-top: 20px;' className='emoji-group-title'> 44 - Favicon Name 45 - </label> 61 + <label className='custom-emoji-group-title'>Favicon Name</label> 46 62 <input 47 63 style='margin: 10px 0 20px 0;' 48 64 name='Name' 49 65 placeholder='Name' 50 - value={name} 51 - onChange={useCallback(({ target }: Event) => { 52 - if (target instanceof HTMLInputElement) { 53 - setName(target.value || ''); 54 - } 55 - }, [setName])} 66 + value={description} 67 + onChange={updateDescription} 56 68 /> 57 69 </div> 58 - <button 59 - type='button' 60 - onClick={useCallback(() => submitCustomEmoji(name, image), [ 61 - name, 62 - image, 63 - ])} 64 - > 65 - submit 66 - </button> 67 - <button 68 - type='button' 69 - onClick={useCallback(() => setRoute(ROUTE.DEFAULT), [setRoute])} 70 - > 71 - cancel 72 - </button> 70 + <button type='button' onClick={saveCustomEmoji}>submit</button> 71 + <button type='button' onClick={goBack}>cancel</button> 73 72 </div> 74 73 ); 75 74 }
+1 -10
source/components/emoji_selector/components/popup.tsx
··· 24 24 route, 25 25 setIsOpen, 26 26 setRoute, 27 - submitCustomEmoji, 28 27 }: { 29 28 customEmojis: EmojiMap; 30 29 isOpen: boolean; ··· 34 33 route: Route; 35 34 setIsOpen: SetSwitch; 36 35 setRoute: SetRoute; 37 - submitCustomEmoji: ( 38 - name: string, 39 - image: string, 40 - type: string, 41 - ) => Promise<void>; 42 36 }, 43 37 ) { 44 38 const [groupFilter, setGroupFilter] = useState(''); ··· 55 49 if (route === ROUTE.CREATE_CUSTOM) { 56 50 return ( 57 51 <div className='emoji-selector-popup' ref={popupRef}> 58 - <CustomUpload 59 - setRoute={setRoute} 60 - submitCustomEmoji={submitCustomEmoji} 61 - /> 52 + <CustomUpload setRoute={setRoute} /> 62 53 </div> 63 54 ); 64 55 }
+3 -15
source/components/emoji_selector/mod.tsx
··· 40 40 const settings = useContext<BrowserStorage<Settings>>(SettingsContext); 41 41 const { cache, setCache, saveToStorageBypassCache } = settings; 42 42 43 - const [customEmojis, setCustomEmojis] = useState({}); 43 + const [customEmojis, setCustomEmojis] = useState<EmojiMap>({}); 44 44 const [isOpen, setIsOpen] = useState<boolean>(false); 45 45 const [route, setRoute] = useState<Route>(ROUTE.DEFAULT); 46 46 const [selectedEmoji, setSelectedEmoji] = useState<Emoji>(DEFAULT_EMOJI); ··· 48 48 useEffect(() => { 49 49 const currIds = Object.keys(customEmojis).sort(); 50 50 if (currIds.length > cache.customEmojiIds.length) { 51 - const nextEmojis = {}; 52 - cache.customEmojiIds.forEach((id) => { 51 + const nextEmojis: EmojiMap = {}; 52 + cache.customEmojiIds.forEach((id: string) => { 53 53 nextEmojis[id] = customEmojis[id]; 54 54 }); 55 55 setCustomEmojis(nextEmojis); ··· 109 109 route={route} 110 110 setIsOpen={setIsOpen} 111 111 setRoute={setRoute} 112 - submitCustomEmoji={useCallback(async (description, url) => { 113 - try { 114 - await saveEmoji(createEmoji(description, url)); 115 - const customEmojiIds = Array.from( 116 - new Set(cache.customEmojiIds.concat(description)), 117 - ); 118 - await saveToStorageBypassCache({ ...cache, customEmojiIds }); 119 - setRoute(ROUTE.DEFAULT); 120 - } catch (e) { 121 - confirm(e); 122 - } 123 - }, [settings, cache.customEmojiIds])} 124 112 /> 125 113 </Fragment> 126 114 );
+16 -23
source/components/list.tsx
··· 15 15 16 16 export default function List<Type,>({ type, state }: ListProps<Type>) { 17 17 const listRef = useRef<HTMLInputElement>(null); 18 - const listInputs = state.contents.map( 19 - (listItem: Favicon, index: number) => { 20 - return ( 18 + const { addItem, deleteItem, updateItem, contents } = state; 19 + return ( 20 + <div className='list' ref={listRef}> 21 + {contents.map((listItem: Favicon, index: number) => ( 21 22 <ListInput 22 23 type={type} 23 24 key={index} 24 25 index={index} 25 - value={state.contents[index] || ''} 26 + value={contents[index] || ''} 26 27 autoFocus={index === 0} 27 - updateItem={state.updateItem} 28 - deleteItem={(index) => { 29 - state.deleteItem(index); 28 + updateItem={updateItem} 29 + deleteItem={(index: number) => { 30 + deleteItem(index); 30 31 const firstInput = listRef?.current?.querySelector('input'); 31 32 if (firstInput) firstInput.focus(); 32 33 }} 33 34 /> 34 - ); 35 - }, 36 - ); 37 - 38 - const newItemInput = ( 39 - <ListInput 40 - type={type} 41 - key={state.contents.length} 42 - index={state.contents.length} 43 - addItem={state.addItem} 44 - /> 45 - ); 46 - 47 - return ( 48 - <div className='list' ref={listRef}> 49 - {listInputs.concat(newItemInput)} 35 + )).concat( 36 + <ListInput 37 + type={type} 38 + key={contents.length} 39 + index={contents.length} 40 + addItem={addItem} 41 + />, 42 + )} 50 43 </div> 51 44 ); 52 45 }
-1
source/components/list_input.tsx
··· 68 68 }, [index, deleteItem]); 69 69 70 70 const color = isRegexString(value?.matcher || '') ? 'green' : 'black'; 71 - 72 71 const placeholder = useMemo(() => { 73 72 return choices[Math.floor(Math.random() * choices.length)]; 74 73 }, []);
+1 -1
source/components/only.tsx
··· 3 3 4 4 export interface OnlyProps { 5 5 if: boolean; 6 - children: VNode | string; 6 + children: VNode | string | (VNode | string)[]; 7 7 } 8 8 9 9 export default function Only({ if: predicate, children }: OnlyProps) {
+6 -6
source/hooks/use_selected_favicon.ts
··· 15 15 */ 16 16 export default function useSelectedFavicon( 17 17 url: string, 18 - settings?: Settings, 18 + settings: Settings, 19 19 ): { 20 20 selectedFavicon: Favicon | null; 21 21 selectedEmoji: Emoji | null; 22 - selectedFaviconURL: string; 22 + selectedImageURL: string; 23 23 } { 24 - const { autoselectorVersion, features } = settings || {}; 24 + const { autoselectorVersion, features } = settings; 25 25 const includeFlags = Boolean(features?.enableAutoselectorIncludeCountryFlags); 26 26 27 27 const [selectedFavicon, setFavicon] = useState<Favicon | null>(null); ··· 48 48 })(); 49 49 }, [autoselector, settings, url]); 50 50 51 - const selectedFaviconURL = useMemo((): string => { 51 + const selectedImageURL = useMemo((): string => { 52 52 if (!selectedEmoji) return ''; 53 53 const { imageURL, emoji } = selectedEmoji; 54 54 return imageURL || ··· 59 59 return { 60 60 selectedFavicon: null, 61 61 selectedEmoji: null, 62 - selectedFaviconURL: '', 62 + selectedImageURL: '', 63 63 }; 64 64 } 65 65 66 - return { selectedFavicon, selectedEmoji, selectedFaviconURL }; 66 + return { selectedFavicon, selectedEmoji, selectedImageURL }; 67 67 }
+7 -7
source/popup.tsx
··· 8 8 import Only from './components/only.tsx'; 9 9 import useActiveTab from './hooks/use_active_tab.ts'; 10 10 import useBrowserStorage from './hooks/use_browser_storage.ts'; 11 - import useFavioliIcon from './hooks/use_selected_favicon.ts'; 11 + import useSelectedFavicon from './hooks/use_selected_favicon.ts'; 12 12 import useStatus from './hooks/use_status.ts'; 13 13 import { DEFAULT_SETTINGS, SETTINGS_KEY } from './models/settings.ts'; 14 14 ··· 16 16 const settings = useBrowserStorage<Settings>(SETTINGS_KEY, DEFAULT_SETTINGS); 17 17 const { setCache, cache, error, loading } = settings; 18 18 const { favIconUrl = '', url = '' } = useActiveTab() || {}; 19 - const { selectedFavicon, selectedFaviconURL } = useFavioliIcon(url, cache); 19 + const { selectedFavicon, selectedImageURL } = useSelectedFavicon(url, cache); 20 20 const origin = url ? (new URL(url)).origin : ''; 21 21 22 22 const hasQuickOverride = useMemo(() => { ··· 40 40 }, [selectedFavicon, hasQuickOverride, origin, cache]), 41 41 ); 42 42 43 - const overridable = !hasQuickOverride && selectedFaviconURL !== favIconUrl; 44 - const overridden = !hasQuickOverride && selectedFaviconURL === favIconUrl; 43 + const overridable = !hasQuickOverride && selectedImageURL !== favIconUrl; 44 + const overridden = !hasQuickOverride && selectedImageURL === favIconUrl; 45 45 46 46 return ( 47 47 <div className='popup-wrapper'> ··· 59 59 Favioli Favicon: 60 60 <img 61 61 className='favicon-icon' 62 - src={selectedFaviconURL || ''} 62 + src={selectedImageURL || ''} 63 63 width={20} 64 64 height={20} 65 65 /> ··· 68 68 <div style='padding-top: 10px; text-align: center;'> 69 69 Is a Favioli Favicon?{' '} 70 70 <span style='font-weight: bold;'> 71 - {selectedFaviconURL === favIconUrl ? 'Yes!' : 'No!'} 71 + {selectedImageURL === favIconUrl ? 'Yes!' : 'No!'} 72 72 </span> 73 73 </div> 74 74 <button 75 75 onClick={save} 76 - disabled={!hasQuickOverride && selectedFaviconURL === favIconUrl} 76 + disabled={!hasQuickOverride && selectedImageURL === favIconUrl} 77 77 > 78 78 <Only if={hasQuickOverride}>Remove Quick Override</Only> 79 79 <Only if={overridable}>Quick Override Favicon</Only>
+1 -1
source/utilities/image_helpers.ts
··· 29 29 */ 30 30 export function createFaviconURLFromChar( 31 31 char: string, 32 - showIndicator: boolean = false, 32 + showIndicator = false, 33 33 ): string { 34 34 if (!char || !ctx) return ''; 35 35
+8
static/styles/shared.css
··· 212 212 height: 100%; 213 213 } 214 214 215 + .custom-emoji-group-title { 216 + margin-top: 20px; 217 + text-align: left; 218 + font-size: 0.6em; 219 + font-weight: bold; 220 + width: 100%; 221 + } 222 + 215 223 .custom-emoji-button { 216 224 margin: 5px; 217 225 }