Big and ugly commit to clean a bunch of stuff up.

This commit is contained in:
Bauke 2021-09-01 12:46:29 +02:00
parent a67d78c099
commit 64cc7a6ed9
Signed by: Bauke
GPG Key ID: C1C0F29952BCF558
27 changed files with 2991 additions and 3404 deletions

View File

@ -4,22 +4,16 @@
[![Queue on AMO](https://img.shields.io/amo/v/holllo-queue)](https://addons.mozilla.org/firefox/addon/holllo-queue) [![Queue on AMO](https://img.shields.io/amo/v/holllo-queue)](https://addons.mozilla.org/firefox/addon/holllo-queue)
![Queue 0.1.5](./docs/screenshots/queue-version-0-1-5.png) ![Queue 0.1.5](./screenshots/queue-version-0-1-5.png)
## Installation ## Installation
* Queue is available [through AMO](https://addons.mozilla.org/firefox/addon/holllo-queue/). * Queue is available [through Mozilla Addons](https://addons.mozilla.org/firefox/addon/holllo-queue/).
* Or via manual installation by either building from source yourself or using a prebuilt version available in the [Releases page](https://github.com/Holllo/queue/releases). * Or via manual installation by either building from source yourself or using a prebuilt version available in the [Releases page](https://github.com/Holllo/queue/releases).
## Development ## Development
[Node.js LTS](https://nodejs.org) and [Yarn](https://yarnpkg.com/) are required to build and develop the extension. As well as a relatively recent version of [Firefox](https://www.mozilla.org/firefox/). [NodeJS](https://nodejs.org) and [Yarn](https://yarnpkg.com) are required to build and develop the extension. As well as a relatively recent version of [Firefox](https://www.mozilla.org/firefox/).
To get started, [a script](https://github.com/Holllo/queue/blob/main/docs/scripts/clone-and-install.sh) to clone the repository and install the dependencies is available. You can download and execute the script in one go with the following command.
```sh
bash -c "$(curl -fsSL https://raw.githubusercontent.com/Holllo/queue/main/docs/scripts/clone-and-install.sh)"
```
To test the extension, run `yarn start:firefox`. To test the extension, run `yarn start:firefox`.

View File

@ -1,36 +0,0 @@
#!/usr/bin/env bash
set -e
required_commands=(
"git"
"yarn"
)
for cmd in ${required_commands[*]}; do
if ! command -v "$cmd" &> /dev/null; then
echo "Command \`$cmd\` could not be found and is required for this script to function."
exit
fi
done
echo "Cloning git repository"
echo "$ git clone 'https://github.com/Holllo/queue'"
git clone 'https://github.com/Holllo/queue'
echo
echo "Changing directory to the git repository"
echo "$ cd 'queue'"
cd 'queue'
echo
echo "Installing the dependencies"
echo "$ yarn --silent"
echo
yarn --silent
echo
echo "Building the extension"
echo "$ yarn build"
yarn build
echo

View File

@ -13,28 +13,26 @@
"clean": "trash '.cache/' 'build/' 'web-ext-artifacts/'", "clean": "trash '.cache/' 'build/' 'web-ext-artifacts/'",
"build": "yarn clean && parcel build 'source/manifest.json' -d 'build/' && web-ext build --source-dir 'build/' && yarn zip-source", "build": "yarn clean && parcel build 'source/manifest.json' -d 'build/' && web-ext build --source-dir 'build/' && yarn zip-source",
"zip-source": "zip -r 'web-ext-artifacts/queue-source.zip' 'README.md' 'yarn.lock' 'tsconfig.json' 'package.json' '.gitignore' 'LICENSE' 'source/'", "zip-source": "zip -r 'web-ext-artifacts/queue-source.zip' 'README.md' 'yarn.lock' 'tsconfig.json' 'package.json' '.gitignore' 'LICENSE' 'source/'",
"test": "xo && stylelint 'source/scss/**/*.scss' && tsc --noEmit" "test": "xo && stylelint 'source/scss/**/*.scss' && tsc"
}, },
"dependencies": { "dependencies": {
"htm": "^3.0.4", "htm": "^3.1.0",
"modern-normalize": "^1.0.0", "modern-normalize": "^1.1.0",
"preact": "^10.5.7", "preact": "^10.5.14",
"webextension-polyfill-ts": "^0.22.0" "webextension-polyfill": "^0.8.0"
}, },
"devDependencies": { "devDependencies": {
"eslint-config-xo-typescript": "^0.35.0", "@types/webextension-polyfill": "^0.8.0",
"husky": "^4.3.0", "parcel-bundler": "^1.12.5",
"parcel-bundler": "^1.12.4",
"parcel-plugin-web-extension": "^1.6.1", "parcel-plugin-web-extension": "^1.6.1",
"sass": "^1.29.0", "sass": "^1.38.2",
"stylelint": "^13.8.0", "stylelint": "^13.13.1",
"stylelint-config-xo-scss": "^0.14.0", "stylelint-config-xo-scss": "^0.14.0",
"stylelint-config-xo-space": "^0.15.1", "stylelint-config-xo-space": "^0.15.1",
"trash-cli": "^3.1.0", "trash-cli": "^4.0.0",
"typescript": "^4.1.2", "typescript": "^4.4.2",
"web-ext": "^5.3.0", "web-ext": "^6.3.0",
"web-ext-types": "^3.2.1", "xo": "^0.44.0"
"xo": "^0.34.2"
}, },
"stylelint": { "stylelint": {
"extends": [ "extends": [
@ -61,17 +59,12 @@
"prettier": true, "prettier": true,
"rules": { "rules": {
"@typescript-eslint/no-implicit-any-catch": "off", "@typescript-eslint/no-implicit-any-catch": "off",
"@typescript-eslint/no-loop-func": "off" "@typescript-eslint/no-loop-func": "off",
"no-await-in-loop": "off"
}, },
"space": true "space": true
}, },
"browserslist": [ "browserslist": [
"last 2 Chrome versions" "last 2 Chrome versions"
], ]
"husky": {
"hooks": {
"pre-commit": "yarn test",
"pre-push": "yarn test"
}
}
} }

View File

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 82 KiB

View File

Before

Width:  |  Height:  |  Size: 105 KiB

After

Width:  |  Height:  |  Size: 105 KiB

View File

@ -1,41 +1,36 @@
import {browser, Menus} from 'webextension-polyfill-ts'; import browser, {Menus} from 'webextension-polyfill';
import {
error, import {error} from './utilities/log';
getManifest, import {getManifest, updateBadge} from './utilities/browser';
getNextQItem, import {versionAsNumber} from './utilities/version';
getSettings, import Settings from './utilities/settings';
migrate, import {migrate} from './utilities/migrations';
newQItemID, import {Queue} from './types.d';
QItem,
QMessage,
removeQItem,
saveSettings,
updateBadge,
versionAsNumber
} from '.';
let timeoutID: number | null = null; let timeoutID: number | null = null;
browser.browserAction.onClicked.addListener(async () => { browser.browserAction.onClicked.addListener(async () => {
const settings = await Settings.get();
// When the extension icon is initially clicked, create a timeout for 500ms // When the extension icon is initially clicked, create a timeout for 500ms
// that will open the next queue item when it expires. // that will open the next queue item when it expires.
// If the icon is clicked again in those 500ms, open the options page instead. // If the icon is clicked again in those 500ms, open the options page instead.
if (timeoutID === null) { if (timeoutID === null) {
timeoutID = window.setTimeout(async () => { timeoutID = window.setTimeout(async () => {
timeoutID = null; timeoutID = null;
const nextItem = await getNextQItem(); const nextItem = settings.nextItem();
if (nextItem === undefined) { if (nextItem === undefined) {
await openOptionsPage(); await openOptionsPage();
} else { } else {
const tabs = await browser.tabs.query({ const tabs = await browser.tabs.query({
currentWindow: true, currentWindow: true,
active: true active: true,
}); });
const message: QMessage<QItem> = { const message: Queue.Message<Queue.Item> = {
action: 'queue open url', action: 'queue open url',
data: nextItem data: nextItem,
}; };
try { try {
@ -44,7 +39,7 @@ browser.browserAction.onClicked.addListener(async () => {
await browser.tabs.create({active: true, url: nextItem.url}); await browser.tabs.create({active: true, url: nextItem.url});
} }
await removeQItem(nextItem.id); await settings.removeItem(nextItem.id);
} }
}, 500); }, 500);
} else { } else {
@ -54,15 +49,17 @@ browser.browserAction.onClicked.addListener(async () => {
} }
}); });
browser.runtime.onMessage.addListener(async (request: QMessage<unknown>) => { browser.runtime.onMessage.addListener(
async (request: Queue.Message<unknown>) => {
if (request.action === 'queue update badge') { if (request.action === 'queue update badge') {
await updateBadge(); await updateBadge(await Settings.get());
} }
}); },
);
browser.runtime.onInstalled.addListener(async () => { browser.runtime.onInstalled.addListener(async () => {
const manifest = getManifest(); const manifest = getManifest();
const settings = await getSettings(); const settings = await Settings.get();
const versionGotUpdated = const versionGotUpdated =
versionAsNumber(manifest.version) > versionAsNumber(settings.latestVersion); versionAsNumber(manifest.version) > versionAsNumber(settings.latestVersion);
@ -93,9 +90,7 @@ async function openOptionsPage() {
return browser.runtime.openOptionsPage(); return browser.runtime.openOptionsPage();
} }
/** /** The callback function for custom context menu entries. */
* The callback function for custom context menu entries.
*/
function contextCreated() { function contextCreated() {
if ( if (
browser.runtime.lastError !== null && browser.runtime.lastError !== null &&
@ -109,27 +104,27 @@ const contextMenus: Menus.CreateCreatePropertiesType[] = [
{ {
id: 'queue-add-new-link', id: 'queue-add-new-link',
title: 'Add to Queue', title: 'Add to Queue',
contexts: ['link'] contexts: ['link'],
}, },
{ {
id: 'queue-add-new-link-tab', id: 'queue-add-new-link-tab',
title: 'Add to Queue', title: 'Add to Queue',
contexts: ['tab'] contexts: ['tab'],
}, },
{ {
id: 'queue-open-next-link-in-new-tab', id: 'queue-open-next-link-in-new-tab',
title: 'Open next link in new tab', title: 'Open next link in new tab',
contexts: ['browser_action'] contexts: ['browser_action'],
}, },
{ {
id: 'queue-open-options-page', id: 'queue-open-options-page',
title: 'Open the extension page', title: 'Open the extension page',
contexts: ['browser_action'] contexts: ['browser_action'],
} },
]; ];
const contextMenuIDs: Set<string> = new Set( const contextMenuIDs: Set<string> = new Set(
contextMenus.map((value) => value.id!) contextMenus.map((value) => value.id!),
); );
for (const menu of contextMenus) { for (const menu of contextMenus) {
@ -142,7 +137,7 @@ browser.contextMenus.onClicked.addListener(async (info, tab) => {
return; return;
} }
const settings = await getSettings(); const settings = await Settings.get();
if (id.includes('queue-add-new-link')) { if (id.includes('queue-add-new-link')) {
let text: string | undefined; let text: string | undefined;
@ -160,20 +155,20 @@ browser.contextMenus.onClicked.addListener(async (info, tab) => {
settings.queue.push({ settings.queue.push({
added: new Date(), added: new Date(),
id: newQItemID(settings.queue), id: settings.newItemId(),
text: text ?? url, text: text ?? url,
url url,
}); });
await saveSettings(settings); await settings.save();
await updateBadge(settings); await updateBadge(settings);
} else if (id === 'queue-open-next-link-in-new-tab') { } else if (id === 'queue-open-next-link-in-new-tab') {
const nextItem = await getNextQItem(settings); const nextItem = settings.nextItem();
if (nextItem === undefined) { if (nextItem === undefined) {
await openOptionsPage(); await openOptionsPage();
} else { } else {
await browser.tabs.create({active: true, url: nextItem.url}); await browser.tabs.create({active: true, url: nextItem.url});
await removeQItem(nextItem.id); await settings.removeItem(nextItem.id);
} }
} else if (id === 'queue-open-options-page') { } else if (id === 'queue-open-options-page') {
await openOptionsPage(); await openOptionsPage();

View File

@ -1,8 +1,8 @@
import {browser} from 'webextension-polyfill-ts'; import browser from 'webextension-polyfill';
import {initializeBackgroundMessageHandler} from '.';
initializeBackgroundMessageHandler(); import {backgroundHandler} from './utilities/browser';
(async () => { (async () => {
backgroundHandler();
await browser.runtime.sendMessage({action: 'queue update badge'}); await browser.runtime.sendMessage({action: 'queue update badge'});
})(); })();

View File

@ -6,8 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Queue</title> <title>Queue</title>
<link rel="shortcut icon" href="./assets/queue.png" type="image/png"> <link rel="shortcut icon" href="./assets/queue.png" type="image/png">
<link rel="stylesheet" <link rel="stylesheet" href="./scss/modern-normalize.scss">
href="../node_modules/modern-normalize/modern-normalize.css">
<link rel="stylesheet" href="./scss/index.scss"> <link rel="stylesheet" href="./scss/index.scss">
</head> </head>

View File

@ -1,12 +0,0 @@
import {html} from 'htm/preact';
type QMessageAction = 'queue open url' | 'queue update badge';
export type QMessage<T> = {
action: QMessageAction;
data: T;
};
export type QComponent = ReturnType<typeof html>;
export * from './utilities';

View File

@ -0,0 +1 @@
@import '../../node_modules/modern-normalize/modern-normalize.css';

View File

@ -1,34 +1,31 @@
import {html, render} from 'htm/preact'; import {html, render} from 'htm/preact';
import { import browser from 'webextension-polyfill';
initializeBackgroundMessageHandler,
log,
getManifest,
getSettings,
PageFooter,
PageHeader,
PageMain,
saveSettings
} from '.';
(async () => { import {backgroundHandler, getManifest} from './utilities/browser';
initializeBackgroundMessageHandler(); import {debug} from './utilities/log';
import {PageFooter, PageHeader, PageMain} from './utilities/components';
import Settings from './utilities/settings';
window.addEventListener('DOMContentLoaded', async () => {
window.HollloQueue = { window.HollloQueue = {
dumpBackup: async () => { dumpBackup: async () => {
log(JSON.stringify(await browser.storage.local.get(), null, 2)); debug(JSON.stringify(await browser.storage.local.get(), null, 2));
}, },
dumpSettings: async () => { dumpSettings: async () => {
log(JSON.stringify(await getSettings(), null, 2)); debug(JSON.stringify(await Settings.get(), null, 2));
} },
}; };
backgroundHandler();
await browser.runtime.sendMessage({action: 'queue update badge'});
const manifest = getManifest(); const manifest = getManifest();
const settings = await getSettings(); const settings = await Settings.get();
const showVersionUpdated = settings.versionGotUpdated; const showVersionUpdated = settings.versionGotUpdated;
if (showVersionUpdated) { if (showVersionUpdated) {
settings.versionGotUpdated = false; settings.versionGotUpdated = false;
await saveSettings(settings); await settings.save();
} }
render( render(
@ -40,6 +37,6 @@ import {
showVersionUpdated=${showVersionUpdated} showVersionUpdated=${showVersionUpdated}
/> />
`, `,
document.body document.body,
); );
})(); });

29
source/types.d.ts vendored
View File

@ -1,3 +1,6 @@
import {html} from 'htm/preact';
import browser from 'webextension-polyfill';
// TypeScript fix to make it see this file as a module. // TypeScript fix to make it see this file as a module.
export {}; export {};
@ -11,3 +14,29 @@ declare global {
HollloQueue: HollloQueue; HollloQueue: HollloQueue;
} }
} }
export namespace Queue {
type Component = ReturnType<typeof html>;
type Item = {
added: Date;
id: number;
text?: string;
url: string;
};
type Manifest = {nodeEnv?: string} & browser.Manifest.ManifestBase;
type Message<T> = {
action: MessageAction;
data: T;
};
type MessageAction = 'queue open url' | 'queue update badge';
type Migration = {
date: Date;
upgrade: (previous: Record<string, any>) => Record<string, any>;
version: string;
};
}

View File

@ -0,0 +1,38 @@
import browser from 'webextension-polyfill';
import {Queue} from '../types.d';
import Settings from './settings';
/** Initializes the background message handler. */
export function backgroundHandler() {
browser.runtime.onMessage.addListener((request: Queue.Message<unknown>) => {
if (request.action === 'queue open url') {
// @ts-expect-error Changing <unknown> to <Queue.Item>.
const message: Queue.Message<Queue.Item> = request;
window.location.href = message.data.url;
}
});
}
/** Returns the WebExtension Manifest. */
export function getManifest(): Queue.Manifest {
return browser.runtime.getManifest();
}
/**
* Updates the extension icon badge with the number of saved items. This can
* only be run from the background script.
*/
export async function updateBadge(settings: Settings): Promise<void> {
await browser.browserAction.setBadgeBackgroundColor({
color: '#2a2041',
});
await browser.browserAction.setBadgeText({
text: settings.queue.length === 0 ? null : settings.queue.length.toString(),
});
browser.browserAction.setBadgeTextColor({
color: '#f2efff',
});
}

View File

@ -0,0 +1,58 @@
import {html} from 'htm/preact';
import {useState} from 'preact/hooks';
import {Queue} from '../../types.d';
type ConfirmButtonProps = {
// Extra classes to add to the button.
class: string;
// The click handler to call when confirmed.
clickHandler: (event: MouseEvent) => void;
// The class to add when in the confirm state.
confirmClass: string;
// The text to use when in the confirm state.
confirmText: string;
// The text to use when in the default state.
text: string;
// The timeout for the confirm state to return back to default.
timeout: number;
// The title to add to the element.
title: string;
};
/**
* Creates a button that requires 2 clicks to trigger the main click handler.
* @param props The ConfirmButton properties.
*/
export function ConfirmButton(props: ConfirmButtonProps): Queue.Component {
let timeoutHandle: number | undefined;
const [isConfirmed, setIsConfirmed] = useState(false);
const click = (event: MouseEvent) => {
if (isConfirmed) {
clearTimeout(timeoutHandle);
props.clickHandler(event);
timeoutHandle = undefined;
setIsConfirmed(false);
} else {
timeoutHandle = window.setTimeout(() => {
setIsConfirmed(false);
}, props.timeout);
setIsConfirmed(true);
}
};
const confirmedClass = isConfirmed ? props.confirmClass : '';
const text = isConfirmed ? props.confirmText : props.text;
const title = isConfirmed ? `Confirm ${props.title}` : props.title;
return html`
<button
class="${props.class} ${confirmedClass}"
onClick=${click}
title="${title}"
>
${text}
</button>
`;
}

View File

@ -1,79 +1,3 @@
import {html} from 'htm/preact';
import {useState} from 'preact/hooks';
import {QComponent} from '../..';
type LinkProps = {
class: string;
text: string;
url: string;
};
/**
* Creates a new <a/> element with target="_blank" and rel="noopener".
* @param props The Link properties.
*/
export function Link(props: LinkProps): QComponent {
return html`
<a class=${props.class} href=${props.url} target="_blank" rel="noopener">
${props.text}
</a>
`;
}
type ConfirmButtonProps = {
// Extra classes to add to the button.
class: string;
// The click handler to call when confirmed.
clickHandler: (event: MouseEvent) => void;
// The class to add when in the confirm state.
confirmClass: string;
// The text to use when in the confirm state.
confirmText: string;
// The text to use when in the default state.
text: string;
// The timeout for the confirm state to return back to default.
timeout: number;
// The title to add to the element.
title: string;
};
/**
* Creates a button that requires 2 clicks to trigger the main click handler.
* @param props The ConfirmButton properties.
*/
export function ConfirmButton(props: ConfirmButtonProps): QComponent {
let timeoutHandle: number | undefined;
const [isConfirmed, setIsConfirmed] = useState(false);
const click = (event: MouseEvent) => {
if (isConfirmed) {
clearTimeout(timeoutHandle);
props.clickHandler(event);
timeoutHandle = undefined;
setIsConfirmed(false);
} else {
timeoutHandle = window.setTimeout(() => {
setIsConfirmed(false);
}, props.timeout);
setIsConfirmed(true);
}
};
const confirmedClass = isConfirmed ? props.confirmClass : '';
const text = isConfirmed ? props.confirmText : props.text;
const title = isConfirmed ? `Confirm ${props.title}` : props.title;
return html`
<button
class="${props.class} ${confirmedClass}"
onClick=${click}
title="${title}"
>
${text}
</button>
`;
}
export * from './page-footer'; export * from './page-footer';
export * from './page-header'; export * from './page-header';
export * from './page-main'; export * from './page-main';

View File

@ -0,0 +1,20 @@
import {html} from 'htm/preact';
import {Queue} from '../../types.d';
type LinkProps = {
class: string;
text: string;
url: string;
};
/**
* Creates a new <a/> element with target="_blank" and rel="noopener".
* @param props The Link properties.
*/
export function Link(props: LinkProps): Queue.Component {
return html`
<a class=${props.class} href=${props.url} target="_blank" rel="noopener">
${props.text}
</a>
`;
}

View File

@ -1,12 +1,14 @@
import {html} from 'htm/preact'; import {html} from 'htm/preact';
import {Link, QComponent, QManifest, Settings} from '../..';
import {Queue} from '../../types.d';
import {Link} from './link';
type FooterProps = { type FooterProps = {
manifest: QManifest; manifest: Queue.Manifest;
showVersionUpdated: boolean; showVersionUpdated: boolean;
}; };
export function PageFooter(props: FooterProps): QComponent { export function PageFooter(props: FooterProps): Queue.Component {
const donateLink = html`<${Link} const donateLink = html`<${Link}
text="Donate" text="Donate"
url="https://liberapay.com/Holllo" url="https://liberapay.com/Holllo"

View File

@ -1,7 +1,8 @@
import {html} from 'htm/preact'; import {html} from 'htm/preact';
import {QComponent} from '../..';
export function PageHeader(): QComponent { import {Queue} from '../../types.d';
export function PageHeader(): Queue.Component {
return html` return html`
<header class="page-header"> <header class="page-header">
<h1> <h1>

View File

@ -1,40 +1,46 @@
import {html} from 'htm/preact'; import {Component, html} from 'htm/preact';
import {useState} from 'preact/hooks'; import browser from 'webextension-polyfill';
import {browser} from 'webextension-polyfill-ts';
import { import Settings from '../settings';
ConfirmButton, import {Queue} from '../../types.d';
Link, import {Link} from './link';
QComponent, import {ConfirmButton} from './confirm-button';
QItem,
removeQItem,
Settings
} from '../..';
type MainProps = { type MainProps = {
settings: Settings; settings: Settings;
}; };
export function PageMain(props: MainProps): QComponent { type MainState = {
const [queue, updateQueue] = useState(props.settings.queue); queue: Queue.Item[];
};
const _removeQItem = async (id: number) => { export class PageMain extends Component<MainProps, MainState> {
const updated = await removeQItem(id); constructor(props: MainProps) {
updateQueue(updated.queue); super(props);
this.state = {
queue: props.settings.queue,
};
}
removeItem = async (id: number) => {
await this.props.settings.removeItem(id);
this.setState({queue: this.props.settings.queue});
await browser.runtime.sendMessage({action: 'queue update badge'}); await browser.runtime.sendMessage({action: 'queue update badge'});
}; };
const qItems: QComponent[] = queue render(): Queue.Component {
const queueItems: Queue.Component[] = this.state.queue
.sort((a, b) => a.added.getTime() - b.added.getTime()) .sort((a, b) => a.added.getTime() - b.added.getTime())
.map((item) => html`<${Item} item=${item} remove=${_removeQItem} />`); .map((item) => html`<${Item} item=${item} remove=${this.removeItem} />`);
if (qItems.length === 0) { if (queueItems.length === 0) {
qItems.push(html`<li>No items queued. 🤷</li>`); queueItems.push(html`<li>No items queued. 🤷</li>`);
} }
return html` return html`
<main class="page-main"> <main class="page-main">
<ul class="q-list"> <ul class="q-list">
${qItems} ${queueItems}
</ul> </ul>
<details class="usage"> <details class="usage">
@ -61,7 +67,8 @@ export function PageMain(props: MainProps): QComponent {
<ul> <ul>
<li>Double-click the extension icon.</li> <li>Double-click the extension icon.</li>
<li> <li>
Right-click the extension icon and click "Open the extension page". Right-click the extension icon and click "Open the extension
page".
</li> </li>
</ul> </ul>
@ -75,15 +82,17 @@ export function PageMain(props: MainProps): QComponent {
</details> </details>
</main> </main>
`; `;
}
} }
type ItemProps = { type ItemProps = {
item: QItem; item: Queue.Item;
remove: (id: number) => Promise<void>; remove: (id: number) => Promise<void>;
}; };
function Item(props: ItemProps): QComponent { function Item(props: ItemProps): Queue.Component {
const {added, id, text, url} = props.item; const added = props.item.added.toLocaleString();
const {id, text, url} = props.item;
return html` return html`
<li class="q-item"> <li class="q-item">
@ -104,11 +113,8 @@ function Item(props: ItemProps): QComponent {
</div> </div>
<p> <p>
<time <time datetime=${added} title="Link queued on ${added}.">
datetime=${added.toLocaleString()} ${added}
title="Link queued on ${added.toLocaleString()}."
>
${added.toLocaleString()}
</time> </time>
</p> </p>
</li> </li>

View File

@ -1,86 +0,0 @@
import {browser, Manifest} from 'webextension-polyfill-ts';
import {getSettings, QItem, QMessage, Settings} from '..';
/**
* Initializes the background message handler.
*/
export function initializeBackgroundMessageHandler() {
browser.runtime.onMessage.addListener((request: QMessage<unknown>) => {
if (request.action === 'queue open url') {
// TypeScript can't assign QMessage<unknown> to QMessage<QItem> but since
// we know it's correct, just ignore the error.
// @ts-expect-error
const message: QMessage<QItem> = request;
window.location.href = message.data.url;
}
});
}
/**
* The WebExtension Manifest with an extra nodeEnv property.
*/
export type QManifest = {nodeEnv?: string} & Manifest.ManifestBase;
/**
* Returns the WebExtension Manifest.
*/
export function getManifest(): QManifest {
const manifest: Manifest.ManifestBase = browser.runtime.getManifest();
return {...manifest};
}
/**
* Logs a thing in the console in the debug level.
* @param thing The thing to log.
*/
export function log(thing: unknown) {
console.debug('[Queue]', thing);
}
/**
* Logs a thing in the console in the error level.
* @param thing The thing to log.
*/
export function error(thing: unknown) {
console.error('[Queue]', thing);
}
/**
* Updates the extension icon badge with the number of saved items. This can
* only be run from the background script.
* @param settings Optional user settings to use.
*/
export async function updateBadge(settings?: Settings): Promise<void> {
if (settings === undefined) {
settings = await getSettings();
}
let text: string | null = null;
if (settings.queue.length > 0) {
text = settings.queue.length.toString();
}
await Promise.all([
browser.browserAction.setBadgeBackgroundColor({
color: '#2a2041'
}),
browser.browserAction.setBadgeTextColor({
color: '#f2efff'
}),
browser.browserAction.setBadgeText({
text
})
]);
}
/**
* Returns a version string as a number by removing the periods.
* @param version
*/
export function versionAsNumber(version: string): number {
return Number(version.replace(/\./g, ''));
}
export * from './components';
export * from './migrations';
export * from './settings';

15
source/utilities/log.ts Normal file
View File

@ -0,0 +1,15 @@
export function debug(input: unknown): void {
console.debug('[Queue]', stringify(input));
}
export function error(input: unknown): void {
console.error('[Queue]', stringify(input));
}
function stringify(input: unknown): unknown {
if (typeof input === 'object') {
input = JSON.stringify(input, null, 2);
}
return input;
}

View File

@ -1,9 +1,9 @@
import {Migration, QItem} from '../..'; import {Queue} from '../../types.d';
export const migration_2020_11_26: Migration = { export const migration_2020_11_26: Queue.Migration = {
date: new Date('2020-11-26T14:32:00.000Z'), date: new Date('2020-11-26T14:32:00.000Z'),
version: '0.1.7', version: '0.1.7',
upgrade upgrade,
}; };
/** /**
@ -12,7 +12,8 @@ export const migration_2020_11_26: Migration = {
* Relevant commit: a668da05a179851b2a1117ef2d6aa9cef48d4964 * Relevant commit: a668da05a179851b2a1117ef2d6aa9cef48d4964
*/ */
function upgrade(previous: Record<string, any>): Record<string, any> { function upgrade(previous: Record<string, any>): Record<string, any> {
const items: QItem[] = previous.queue ?? []; // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const items: Queue.Item[] = previous.queue ?? [];
const next: Record<string, any> = previous; const next: Record<string, any> = previous;
delete next.queue; delete next.queue;

View File

@ -1,17 +1,13 @@
import {log, versionAsNumber} from '../..'; import {Queue} from '../../types.d';
import {debug} from '../log';
import {versionAsNumber} from '../version';
import {migration_2020_11_26} from './2020-11-26'; import {migration_2020_11_26} from './2020-11-26';
export type Migration = { const migrations: Queue.Migration[] = [migration_2020_11_26];
date: Date;
version: string;
upgrade: (previous: Record<string, any>) => Record<string, any>;
};
const migrations: Migration[] = [migration_2020_11_26];
export function migrate( export function migrate(
latestVersion: string, latestVersion: string,
previous: Record<string, any> previous: Record<string, any>,
): Record<string, any> { ): Record<string, any> {
let next = previous; let next = previous;
@ -22,7 +18,7 @@ export function migrate(
continue; continue;
} }
log(`Running migration ${migration.date.toISOString()}`); debug(`Running migration ${migration.date.toISOString()}`);
next = migration.upgrade(next); next = migration.upgrade(next);
} }

View File

@ -1,33 +1,37 @@
import {browser} from 'webextension-polyfill-ts'; import browser from 'webextension-polyfill';
import {log} from '.';
export type QItem = { import {Queue} from '../types.d';
added: Date; import {debug} from './log';
id: number;
text?: string;
url: string;
};
export type Settings = { const defaultSettings: ISettings = {
latestVersion: string;
queue: QItem[];
versionGotUpdated: boolean;
};
const defaultSettings: Settings = {
latestVersion: '0.0.0', latestVersion: '0.0.0',
queue: [], queue: [],
versionGotUpdated: false versionGotUpdated: false,
}; };
/** interface ISettings {
* Returns the user's settings. latestVersion: string;
*/ queue: Queue.Item[];
export async function getSettings(): Promise<Settings> { versionGotUpdated: boolean;
const syncSettings: any = await browser.storage.sync.get(); }
export default class Settings implements ISettings {
public latestVersion: string;
public queue: Queue.Item[];
public versionGotUpdated: boolean;
private constructor(settings: ISettings) {
this.latestVersion = settings.latestVersion;
this.queue = settings.queue;
this.versionGotUpdated = settings.versionGotUpdated;
}
static async get(): Promise<Settings> {
const syncSettings: Record<string, any> = await browser.storage.sync.get();
// Append properties with a name matching 'qi'x to the queue.
const queue: Queue.Item[] = [];
// Append properties with a name matching 'qi'x to queue.
const queue: QItem[] = [];
if (syncSettings !== undefined) { if (syncSettings !== undefined) {
for (const prop in syncSettings) { for (const prop in syncSettings) {
if (prop.startsWith('qi')) { if (prop.startsWith('qi')) {
@ -41,81 +45,51 @@ export async function getSettings(): Promise<Settings> {
item.added = new Date(item.added); item.added = new Date(item.added);
} }
const settings: Settings = { const latestVersion =
latestVersion: syncSettings.latestVersion ?? defaultSettings.latestVersion, (syncSettings.latestVersion as string) ?? defaultSettings.latestVersion;
const versionGotUpdated =
(syncSettings.versionGotUpdated as boolean) ??
defaultSettings.versionGotUpdated;
return new Settings({
latestVersion,
queue, queue,
versionGotUpdated: versionGotUpdated,
syncSettings.versionGotUpdated ?? defaultSettings.versionGotUpdated });
}
async save(): Promise<void> {
const syncSettings: Record<string, any> = {
latestVersion: this.latestVersion,
versionGotUpdated: this.versionGotUpdated,
}; };
return settings; for (const item of this.queue) {
}
/**
* Saves the user's settings to local storage.
* @param settings The settings to save.
*/
export async function saveSettings(settings: Settings): Promise<Settings> {
const syncSettings: any = {
latestVersion: settings.latestVersion,
versionGotUpdated: settings.versionGotUpdated
};
for (const item of settings.queue) {
syncSettings['qi' + item.id.toString()] = item; syncSettings['qi' + item.id.toString()] = item;
} }
await browser.storage.sync.set(syncSettings); await browser.storage.sync.set(syncSettings);
return settings;
}
/**
* Returns a new ID to use for a new QItem.
* @param items All the queue items.
*/
export function newQItemID(items: QItem[]): number {
const highestItem = items.sort((a, b) => b.id - a.id)[0];
if (highestItem === undefined) {
return 1;
} }
return highestItem.id + 1; newItemId(): number {
} const highestItem = this.queue.sort((a, b) => b.id - a.id)[0];
return highestItem === undefined ? 1 : highestItem.id + 1;
/**
* Removes a QItem from the Settings with the specified ID.
* @param id The ID of the QItem to be removed.
* @param settings Optional user settings to use.
*/
export async function removeQItem(
id: number,
settings?: Settings
): Promise<Settings> {
if (settings === undefined) {
settings = await getSettings();
} }
const index = settings.queue.findIndex((item) => item.id === id); nextItem(): Queue.Item | undefined {
return this.queue.sort((a, b) => a.added.getTime() - b.added.getTime())[0];
}
async removeItem(id: number): Promise<void> {
const index = this.queue.findIndex((item) => item.id === id);
if (index === -1) { if (index === -1) {
log(`No QItem with ID ${id} found.`); debug(`No Queue.Item with ID ${id} found.`);
return settings; return;
} }
settings.queue.splice(index, 1); this.queue.splice(index, 1);
await browser.storage.sync.remove('qi' + id.toString()); await browser.storage.sync.remove('qi' + id.toString());
return settings;
}
/**
* Returns the next QItem.
* @param settings Optional user settings to use.
*/
export async function getNextQItem(
settings?: Settings
): Promise<QItem | undefined> {
if (settings === undefined) {
settings = await getSettings();
} }
settings.queue.sort((a, b) => a.added.getTime() - b.added.getTime());
return settings.queue[0];
} }

View File

@ -0,0 +1,4 @@
/** Returns a version string as a number by removing the dots. */
export function versionAsNumber(version: string): number {
return Number(version.replace(/\./g, ''));
}

View File

@ -5,13 +5,10 @@
"ES2019" "ES2019"
], ],
"module": "commonjs", "module": "commonjs",
"noEmit": true,
"outDir": "build/", "outDir": "build/",
"strict": true, "strict": true,
"target": "es6", "target": "es6"
"typeRoots": [
"node_modules/@types",
"node_modules/web-ext-types"
],
}, },
"include": [ "include": [
"source/**/*.ts", "source/**/*.ts",

5463
yarn.lock

File diff suppressed because it is too large Load Diff