Big and ugly commit to clean a bunch of stuff up.
This commit is contained in:
parent
a67d78c099
commit
64cc7a6ed9
12
README.md
12
README.md
|
@ -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`.
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
39
package.json
39
package.json
|
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 82 KiB |
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 105 KiB |
|
@ -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(
|
||||||
if (request.action === 'queue update badge') {
|
async (request: Queue.Message<unknown>) => {
|
||||||
await updateBadge();
|
if (request.action === 'queue update badge') {
|
||||||
}
|
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();
|
||||||
|
|
|
@ -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'});
|
||||||
})();
|
})();
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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';
|
|
|
@ -0,0 +1 @@
|
||||||
|
@import '../../node_modules/modern-normalize/modern-normalize.css';
|
|
@ -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,
|
||||||
);
|
);
|
||||||
})();
|
});
|
||||||
|
|
|
@ -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;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -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',
|
||||||
|
});
|
||||||
|
}
|
|
@ -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>
|
||||||
|
`;
|
||||||
|
}
|
|
@ -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';
|
||||||
|
|
|
@ -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>
|
||||||
|
`;
|
||||||
|
}
|
|
@ -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"
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -1,89 +1,98 @@
|
||||||
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 {
|
||||||
.sort((a, b) => a.added.getTime() - b.added.getTime())
|
const queueItems: Queue.Component[] = this.state.queue
|
||||||
.map((item) => html`<${Item} item=${item} remove=${_removeQItem} />`);
|
.sort((a, b) => a.added.getTime() - b.added.getTime())
|
||||||
|
.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`
|
||||||
|
<main class="page-main">
|
||||||
|
<ul class="q-list">
|
||||||
|
${queueItems}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<details class="usage">
|
||||||
|
<summary>How do I use Queue?</summary>
|
||||||
|
|
||||||
|
<p>Adding links to your queue:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Right-click any link or tab and click "Add to Queue".</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p>Opening the next link from your queue:</p>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
Click on the extension icon to open it in the current tab (when
|
||||||
|
possible).
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Right-click the extension icon and click "Open next link in new
|
||||||
|
tab".
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p>Opening the extension page:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Double-click the extension icon.</li>
|
||||||
|
<li>
|
||||||
|
Right-click the extension icon and click "Open the extension
|
||||||
|
page".
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p>Deleting queue items:</p>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
Click the red button with the ✗ and then confirm it by clicking
|
||||||
|
again.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
</main>
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return html`
|
|
||||||
<main class="page-main">
|
|
||||||
<ul class="q-list">
|
|
||||||
${qItems}
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<details class="usage">
|
|
||||||
<summary>How do I use Queue?</summary>
|
|
||||||
|
|
||||||
<p>Adding links to your queue:</p>
|
|
||||||
<ul>
|
|
||||||
<li>Right-click any link or tab and click "Add to Queue".</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<p>Opening the next link from your queue:</p>
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
Click on the extension icon to open it in the current tab (when
|
|
||||||
possible).
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Right-click the extension icon and click "Open next link in new
|
|
||||||
tab".
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<p>Opening the extension page:</p>
|
|
||||||
<ul>
|
|
||||||
<li>Double-click the extension icon.</li>
|
|
||||||
<li>
|
|
||||||
Right-click the extension icon and click "Open the extension page".
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<p>Deleting queue items:</p>
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
Click the red button with the ✗ and then confirm it by clicking
|
|
||||||
again.
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</details>
|
|
||||||
</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>
|
||||||
|
|
|
@ -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';
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,121 +1,95 @@
|
||||||
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();
|
}
|
||||||
|
|
||||||
// Append properties with a name matching 'qi'x to queue.
|
export default class Settings implements ISettings {
|
||||||
const queue: QItem[] = [];
|
public latestVersion: string;
|
||||||
if (syncSettings !== undefined) {
|
public queue: Queue.Item[];
|
||||||
for (const prop in syncSettings) {
|
public versionGotUpdated: boolean;
|
||||||
if (prop.startsWith('qi')) {
|
|
||||||
queue.push(syncSettings[prop]);
|
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[] = [];
|
||||||
|
|
||||||
|
if (syncSettings !== undefined) {
|
||||||
|
for (const prop in syncSettings) {
|
||||||
|
if (prop.startsWith('qi')) {
|
||||||
|
queue.push(syncSettings[prop]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize all the non-JSON values, as they are stringified when saved.
|
||||||
|
for (const item of queue) {
|
||||||
|
item.added = new Date(item.added);
|
||||||
|
}
|
||||||
|
|
||||||
|
const latestVersion =
|
||||||
|
(syncSettings.latestVersion as string) ?? defaultSettings.latestVersion;
|
||||||
|
|
||||||
|
const versionGotUpdated =
|
||||||
|
(syncSettings.versionGotUpdated as boolean) ??
|
||||||
|
defaultSettings.versionGotUpdated;
|
||||||
|
|
||||||
|
return new Settings({
|
||||||
|
latestVersion,
|
||||||
|
queue,
|
||||||
|
versionGotUpdated,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize all the non-JSON values, as they are stringified when saved.
|
async save(): Promise<void> {
|
||||||
for (const item of queue) {
|
const syncSettings: Record<string, any> = {
|
||||||
item.added = new Date(item.added);
|
latestVersion: this.latestVersion,
|
||||||
|
versionGotUpdated: this.versionGotUpdated,
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const item of this.queue) {
|
||||||
|
syncSettings['qi' + item.id.toString()] = item;
|
||||||
|
}
|
||||||
|
|
||||||
|
await browser.storage.sync.set(syncSettings);
|
||||||
}
|
}
|
||||||
|
|
||||||
const settings: Settings = {
|
newItemId(): number {
|
||||||
latestVersion: syncSettings.latestVersion ?? defaultSettings.latestVersion,
|
const highestItem = this.queue.sort((a, b) => b.id - a.id)[0];
|
||||||
queue,
|
return highestItem === undefined ? 1 : highestItem.id + 1;
|
||||||
versionGotUpdated:
|
}
|
||||||
syncSettings.versionGotUpdated ?? defaultSettings.versionGotUpdated
|
|
||||||
};
|
|
||||||
|
|
||||||
return settings;
|
nextItem(): Queue.Item | undefined {
|
||||||
}
|
return this.queue.sort((a, b) => a.added.getTime() - b.added.getTime())[0];
|
||||||
|
}
|
||||||
/**
|
|
||||||
* Saves the user's settings to local storage.
|
async removeItem(id: number): Promise<void> {
|
||||||
* @param settings The settings to save.
|
const index = this.queue.findIndex((item) => item.id === id);
|
||||||
*/
|
|
||||||
export async function saveSettings(settings: Settings): Promise<Settings> {
|
if (index === -1) {
|
||||||
const syncSettings: any = {
|
debug(`No Queue.Item with ID ${id} found.`);
|
||||||
latestVersion: settings.latestVersion,
|
return;
|
||||||
versionGotUpdated: settings.versionGotUpdated
|
}
|
||||||
};
|
|
||||||
for (const item of settings.queue) {
|
this.queue.splice(index, 1);
|
||||||
syncSettings['qi' + item.id.toString()] = item;
|
await browser.storage.sync.remove('qi' + id.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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);
|
|
||||||
if (index === -1) {
|
|
||||||
log(`No QItem with ID ${id} found.`);
|
|
||||||
return settings;
|
|
||||||
}
|
|
||||||
|
|
||||||
settings.queue.splice(index, 1);
|
|
||||||
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];
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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, ''));
|
||||||
|
}
|
|
@ -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",
|
||||||
|
|
Loading…
Reference in New Issue