Skip to content

Commit

Permalink
Add support for custom icons uploaded by user #21
Browse files Browse the repository at this point in the history
  • Loading branch information
OlegWock committed Apr 20, 2023
1 parent 7349841 commit 4c72a01
Show file tree
Hide file tree
Showing 25 changed files with 702 additions and 148 deletions.
17 changes: 17 additions & 0 deletions declarations.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,20 @@ declare module 'webextension-polyfill' {
}
}
}


declare global {
type FileSystemDirectoryNamesIterator = AsyncIterable<string>;
type FileSystemDirectoryHandlesIterator = AsyncIterable<FileSystemDirectoryHandle | FileSystemFileHandle>;
type FileSystemDirectoryEntriesIterator = AsyncIterable<[string, FileSystemDirectoryHandle | FileSystemFileHandle]>;

interface FileSystemDirectoryHandle {
keys(): FileSystemDirectoryNamesIterator,
values(): FileSystemDirectoryHandlesIterator,
entries(): FileSystemDirectoryEntriesIterator,
}

interface FileSystemFileHandle {
createWritable(): Promise<FileSystemWritableFileStream>,
}
}
9 changes: 8 additions & 1 deletion generate-icons-assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,18 @@ sets.forEach((set, i, arr) => {


const allSetsTs = `
export const allSets = [\n${sets.map(s => ` ${JSON.stringify(s.name)}, // https://icon-sets.iconify.design/${s.name}/`).join('\n')}\n];
import { CUSTOM_ICONS_AVAILABLE } from '@utils/custom-icons';
export const allSets = [\n${sets.map(s => ` ${JSON.stringify(s.name)}, // https://icon-sets.iconify.design/${s.name}/`).join('\n')},\n];
export const iconSetPrettyNames: Record<string, string> = {
${sets.map(s => ` '${s.name}': ${JSON.stringify(s.data.info.name)},`).join('\n')}
} as const;
if (CUSTOM_ICONS_AVAILABLE) {
// Icons uploaded by user
allSets.push('custom');
iconSetPrettyNames['custom'] = 'Custom icons';
}
`

const iconsBySet: Record<string, string[]> = {};
Expand Down
9 changes: 5 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "Anori",
"description": "Customizable new tab: compose your own page from widgets",
"version": "1.4.1",
"version": "1.5.0",
"repository": "git@github.com:OlegWock/anori.git",
"author": "OlegWock <olegwock@gmail.com>",
"license": "MIT",
Expand Down Expand Up @@ -40,8 +40,8 @@
"@types/react-grid-layout": "^1.3.2",
"@types/react-window": "^1.8.5",
"@types/webextension-polyfill": "^0.10.0",
"@typescript-eslint/eslint-plugin": "^5.50.0",
"@typescript-eslint/parser": "^5.50.0",
"@typescript-eslint/eslint-plugin": "^5.59.0",
"@typescript-eslint/parser": "^5.59.0",
"babel-loader": "^8.2.5",
"clean-webpack-plugin": "^4.0.0",
"clsx": "^1.2.1",
Expand All @@ -55,6 +55,7 @@
"inject-react-anywhere": "^1.0.0",
"jotai": "^2.0.0",
"jotai-optics": "^0.3.0",
"jszip": "^3.10.1",
"moment-timezone": "^0.5.40",
"moment-timezone-data-webpack-plugin": "^1.5.1",
"optics-ts": "^2.4.0",
Expand All @@ -73,7 +74,7 @@
"to-string-loader": "^1.2.0",
"ts-loader": "^9.4.2",
"ts-node": "^10.9.1",
"typescript": "^4.9.5",
"typescript": "^5.0.4",
"walk-sync": "^3.0.0",
"webextension-polyfill": "^0.10.0",
"webpack": "^5.75.0",
Expand Down
4 changes: 1 addition & 3 deletions src/background.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import browser from 'webextension-polyfill';

console.log('Background init');

const VERSIONS_WITH_CHANGES = ['1.1.0', '1.2.0'];
const VERSIONS_WITH_CHANGES = ['1.1.0', '1.2.0', '1.5.0'];

const compareVersions = (v1: string, v2: string): -1 | 0 | 1 => {
// v1 is newer than v2 => -1
Expand Down Expand Up @@ -54,8 +54,6 @@ browser.runtime.onInstalled.addListener((details) => {
}
});

console.log('Planted onInstalled handler');

const runScheduledCallbacks = async () => {
const { scheduledCallbacksInfo } = await browser.storage.session.get({
'scheduledCallbacksInfo': {},
Expand Down
5 changes: 3 additions & 2 deletions src/components/Button.scss
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
padding: 6px 18px;
border-radius: 24px;

&:hover {
&:hover:not(:disabled):not([aria-disabled="true"]) {
background: rgba(255, 255, 255, 0.1);
}

Expand All @@ -29,8 +29,9 @@
}
}

&:disabled {
&:disabled, &[aria-disabled="true"] {
cursor: not-allowed;
color: var(--text-disabled);
}


Expand Down
26 changes: 17 additions & 9 deletions src/components/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,26 @@ export interface ButtonProps extends Omit<React.ComponentProps<typeof motion.but
block?: boolean,
withoutBorder?: boolean,
active?: boolean,
visuallyDisabled?: boolean,
}

export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(({ size = "normal", withoutBorder = false, active, block = false, ...props}, ref) => {
return <motion.button {...props} ref={ref} className={classNames('Button', {
[`Button-normal`]: true,
[`Button-size-${size}`]: true,
[`Button-block`]: block,
'with-border': !withoutBorder,
'active': active,
}, props.className)}/>
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(({ size = "normal", disabled, visuallyDisabled, withoutBorder = false, active, block = false, onClick, ...props }, ref) => {
return <motion.button
{...props}
ref={ref}
disabled={visuallyDisabled ? undefined : disabled}
aria-disabled={visuallyDisabled ? 'true' : undefined}
onClick={visuallyDisabled ? undefined : onClick}
className={classNames('Button', {
[`Button-normal`]: true,
[`Button-size-${size}`]: true,
[`Button-block`]: block,
'with-border': !withoutBorder,
'active': active,
}, props.className)}
/>
});

export const LinkButton = React.forwardRef<HTMLButtonElement, React.ComponentProps<"button">>((props, ref) => {
return <button {...props} ref={ref} className={classNames('LinkButton', props.className)}/>
return <button {...props} ref={ref} className={classNames('LinkButton', props.className)} />
});
3 changes: 1 addition & 2 deletions src/components/Hint.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { IconProps } from '@iconify/react';
import './Hint.scss';
import { Icon } from './Icon';
import { Icon, IconProps } from './Icon';
import { Tooltip } from './Tooltip';
import clsx from 'clsx';

Expand Down
3 changes: 3 additions & 0 deletions src/components/Icon.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.CustomIcon {

}
42 changes: 37 additions & 5 deletions src/components/Icon.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { RefAttributes, useLayoutEffect } from 'react';
import { ComponentPropsWithoutRef, RefAttributes, useLayoutEffect } from 'react';
import browser from 'webextension-polyfill';
import { allSets } from './icons/all-sets';
import { IconifyJSON, Icon as OfflineIcon, addCollection } from '@iconify/react/dist/offline';
import { useForceRerender } from '@utils/hooks';
import { guid } from '@utils/misc';
import { forwardRef } from 'react';
import { useCustomIcon } from '@utils/custom-icons';
import './Icon.scss';
import clsx from 'clsx';


const isFamilyLoaded = (family: string) => {
Expand All @@ -20,6 +23,7 @@ const loadFamily = async (family: string) => {
};

export const requestIconsFamily = async (family: string) => {
if (family === 'custom') return;
if (!allSets.includes(family)) {
console.error(`Unknown icons family ${family}, please make sure it's included in generate-icons script in root of project and run that script to regenerate icons.`);
return;
Expand Down Expand Up @@ -50,12 +54,37 @@ const loadedFamilies: string[] = [];
const loadingPromises: Record<string, Promise<void> | undefined> = {};
const familyLoadedCallbacks: Record<string, (family: string) => void> = {};

export type IconProps = {
icon: string,
width?: number | string,
height?: number | string,
className?: string,
} & ComponentPropsWithoutRef<"div">;

const CustomIcon = forwardRef<RefAttributes<HTMLElement>, IconProps>(({icon, className, ...props}, ref) => {
const iconInfo = useCustomIcon(icon);

if (!iconInfo) {
// @ts-ignore incorrect ref types?
return (<div ref={ref} style={{
background: '#ffffff',
borderRadius: 8,
opacity: 0.35,
width: props.width || props.height || 24,
height: props.height || props.width || 24,
}} />);
}

// @ts-ignore incorrect ref types?
return (<img className={clsx('CustomIcon', className)} ref={ref} src={iconInfo.urlObject} {...props} />);
});

export const Icon = forwardRef<RefAttributes<SVGSVGElement>, IconProps>((props, ref) => {
const [family, iconName] = props.icon.split(':');
const rerender = useForceRerender();

export const Icon = forwardRef<RefAttributes<SVGSVGElement>, Omit<React.ComponentProps<typeof OfflineIcon>, 'icon'> & { icon: string }>((props, ref) => {
useLayoutEffect(() => {
const family = props.icon.split(':')[0];
if (!family || isFamilyLoaded(family)) return;
if (!family || isFamilyLoaded(family) || family === 'custom') return;

const callbackId = subscribeToLoadEvents((loadedFamily) => {
if (loadedFamily === family) {
Expand All @@ -68,7 +97,10 @@ export const Icon = forwardRef<RefAttributes<SVGSVGElement>, Omit<React.Componen
return () => unsubscribe(callbackId);
}, [props.icon]);

const rerender = useForceRerender();
if (family === 'custom') {
return (<CustomIcon {...props} icon={iconName} />);
}

// @ts-ignore incorrect ref typing
return (<OfflineIcon {...props} ref={ref}>
<div style={{
Expand Down
10 changes: 10 additions & 0 deletions src/components/IconPicker.scss
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,16 @@
margin-bottom: 8px;
}

.no-custom-icons-alert {
display: flex;
justify-content: center;
align-items: center;
padding: 48px 24px;
flex-direction: column;
gap: 24px;
text-align: center;
}

.icons-grid {
// TODO: will be good to migrate to <ScrollArea> component instead of this mixin, but we don't own underlaying grid component...
@include scroll-mixin();
Expand Down
18 changes: 15 additions & 3 deletions src/components/IconPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { Icon } from './Icon';
import { Tooltip } from './Tooltip';
import { atom, useAtom } from 'jotai';
import { useEffect } from 'react';
import { useCustomIcons } from '@utils/custom-icons';

type IconPickerProps = PopoverRenderProps<{
onSelected: (icon: string) => void,
Expand Down Expand Up @@ -52,10 +53,18 @@ export const IconPicker = ({ data, close }: IconPickerProps) => {
const [selectedFamily, setSelectedFamily] = useState(ALL_SETS);
const [query, setQuery] = useState('');
const [iconsBySet, setIconsBySet] = useAtom(iconsBySetAtom);
const { customIcons } = useCustomIcons();

const iconsList = useMemo(() => {
if (iconsBySet === null) return [];
const base = selectedFamily === ALL_SETS ? Object.entries(iconsBySet) : [[selectedFamily, iconsBySet[selectedFamily]]] as const;
let base: [string, string[]][];
if (selectedFamily === ALL_SETS) {
base = [...Object.entries(iconsBySet), ['custom', customIcons.map(i => i.name)]];
} else if (selectedFamily === 'custom') {
base = [[selectedFamily, customIcons.map(i => i.name)]]
} else {
base = [[selectedFamily, iconsBySet[selectedFamily]]];
}
return base
.map(([family, icons]) => icons.map(icon => `${family}:${icon}`))
.flat()
Expand Down Expand Up @@ -91,7 +100,10 @@ export const IconPicker = ({ data, close }: IconPickerProps) => {
<section>
<label>Icons: </label>
<Input ref={data.inputRef} value={query} onChange={e => setQuery(e.target.value)} placeholder='Search' className='icons-search' />
<FixedSizeList<GridItemData>
{(selectedFamily === 'custom' && iconsList.length === 0) ? <div className='no-custom-icons-alert'>
<p>Custom icons are icons which you upload for later use in Anori. </p>
<p>Currently, you don't have any custom icons. To upload your first custom icon please head to settings.</p>
</div> : <FixedSizeList<GridItemData>
className="icons-grid"
height={350}
itemCount={ROWS}
Expand All @@ -106,7 +118,7 @@ export const IconPicker = ({ data, close }: IconPickerProps) => {
}}
>
{IconRow}
</FixedSizeList>
</FixedSizeList>}
</section>

</div>)
Expand Down
40 changes: 31 additions & 9 deletions src/components/WhatsNew.scss
Original file line number Diff line number Diff line change
@@ -1,14 +1,36 @@
.WhatsNew {
max-width: 600px;
overflow: hidden;

&-content {
display: flex;
flex-direction: column;
gap: 24px;
.WhatsNew-modal {
padding: 0px !important;

.modal-header {
padding: 24px;
padding-bottom: 0px;
}

ul {
margin-left: 22px;
.WhatsNew {
max-width: 600px;
overflow: hidden;
display: flex;
flex-direction: column;

&-scrollarea {
margin: 0 6px;
margin-bottom: 12px;
}

&-content {
display: flex;
flex-direction: column;
gap: 24px;
padding: 0 18px;
}

ul {
margin-left: 22px;
}

p:not(:first-of-type) {
margin-top: 2rem;
}
}
}
27 changes: 26 additions & 1 deletion src/components/WhatsNew.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,33 @@ import './WhatsNew.scss';

export const WhatsNew = () => {
return (<div className="WhatsNew">
<ScrollArea>
<ScrollArea className='WhatsNew-scrollarea'>
<div className='WhatsNew-content'>
<section>
<h2>1.5.0</h2>
<p>
Huge news! Now you can upload your own icons and use them for folders or bookmarks.
Anori supports jpg, gif, png and svg. You can upload your first icon in settings. And here
are a few cool icon packs for your inspiration, enjoy!
</p>
<ul>
<li><a target="_blank" href='https://www.svgrepo.com/collection/stylized-app-icons/'>Cute stylized app icons</a></li>
<li><a target="_blank" href='https://www.svgrepo.com/collection/landscape-circled-vectors/'>Landscapes</a></li>
<li><a target="_blank" href='https://www.svgrepo.com/collection/animal-sticker-stamp-vectors/'>Animals</a></li>
<li><a target="_blank" href='https://www.pngrepo.com/collection/traveling-flat-icons/'>Traveling flat icons</a></li>
</ul>
<p>
This feature uses a kinda experimental API which support only recently landed in Firefox, so if
you don't see 'Custom icons' section in settings, make sure you're using at least Firefox 111.
Chrome users should be fine as is.
</p>

<p>
<strong>Please note.</strong> To support custom icons in backups, format of backups also changed
(now it's zip which includes your custom icons). So if you use this feature you might
want to export a fresh backup.
</p>
</section>
<section>
<h2>1.4.0</h2>
<ul>
Expand Down
Loading

0 comments on commit 4c72a01

Please sign in to comment.