diff --git a/.github/workflows/test_build.yml b/.github/workflows/test_build.yml new file mode 100644 index 0000000..bb69641 --- /dev/null +++ b/.github/workflows/test_build.yml @@ -0,0 +1,41 @@ +name: "Test and build workflow" +on: + push: + branches: master + pull_request: + branches: "*" + types: [opened, synchronize, reopened] + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: "22" + cache: "yarn" + cache-dependency-path: yarn.lock + + - name: Install Yarn + run: | + npm install -g yarn + yarn --version + + - name: Run install + run: yarn install + + - name: Run tests + run: yarn test + + - name: Build + run: yarn build + + - name: Upload build artifact + uses: actions/upload-artifact@v4 + with: + name: speed-reader + path: ${{github.workspace}}/build/ diff --git a/.gitignore b/.gitignore index 81f647a..3c4377d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ node_modules .cache +.parcel-cache dist build extension.zip diff --git a/README.md b/README.md index 3088679..dcfde62 100644 --- a/README.md +++ b/README.md @@ -13,19 +13,20 @@ There are other extensions that do the same, but none for Firefox that doesn't r ## Usage -Simply select the text you want to speed-read and click the extension button. - +Simply select the text you want to speed-read and click the extension button, +right click the page and then the extension button, or by default +use CTRL+ALT+U, see [extension shortcuts](https://support.mozilla.org/en-US/kb/manage-extension-shortcuts-firefox) on how to change (thanks @sheldoncork). ### Hotkeys -|Button|Action| -|-|-| -|Spacebar|Toggle pause| -|Escape|Close it| -|Arrow Up|Speed Up| -|Arrow Down|Speed Down| -|Arrow Left|Previous word (useful when paused)| -|Arrow Right|Next word (useful when paused)| +| Button | Action | +| ----------- | ---------------------------------- | +| Spacebar | Toggle pause | +| Escape | Close it | +| Arrow Up | Speed Up | +| Arrow Down | Speed Down | +| Arrow Left | Previous word (useful when paused) | +| Arrow Right | Next word (useful when paused) | You can also click anywhere on the background to close it. @@ -53,4 +54,4 @@ yarn build Creates a `build/speed-reader.js` file that's ready to be executed. -It is possible to simply add that file as a bookmarklet to achieve the same result as installing the extension. +It is possible to simply add that file as a [bookmarklet](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Your_first_WebExtension#installing) to achieve the same result as installing the extension. diff --git a/extension.js b/extension.js deleted file mode 100644 index d603a11..0000000 --- a/extension.js +++ /dev/null @@ -1,28 +0,0 @@ -browser.browserAction.onClicked.addListener(() => { - browser.storage.sync.get('speed-reader-settings').then(e => { - const settings = { - fontFamily: 'monospace', - backgroundColor: 'hsl(0, 0%, 15%)', - textColor: 'hsl(0, 0%, 90%)', - middleLetterColor: 'hsl(25, 50%, 50%)', - fontSize: '30px', - fullScreen: false, - width: '90%', - height: 'auto', - speedIncrement: 50, - intialSpeed: 400, - wordAmount: 1, - ...(e['speed-reader-settings'] || {}) - }; - - browser.tabs.executeScript({ - code: ` - window.speedReaderSettings = ${JSON.stringify(settings)}; - ` - }); - - browser.tabs.executeScript({ - file: '/build/speed-reader.js' - }); - }); -}); diff --git a/extension.ts b/extension.ts new file mode 100644 index 0000000..57064b5 --- /dev/null +++ b/extension.ts @@ -0,0 +1,48 @@ +import browser from "webextension-polyfill"; +import { defaultSettings, Settings } from "./src/main/Settings"; + +declare global { + interface Window { + speedReaderSettings: Settings; + } +} + +browser.runtime.onInstalled.addListener(() => { + browser.contextMenus.create({ + id: "speed-reader", + title: "Speed Reader", + contexts: ["selection"], + }); +}); + +async function runSpeedReader(): Promise { + const [tab] = await browser.tabs.query({ active: true, currentWindow: true }); + if (!tab?.id) return; + + const settings = await browser.storage.sync.get("speed-reader-settings"); + const finalSettings: Settings = { + ...defaultSettings, + ...(settings["speed-reader-settings"] || {}), + }; + + await browser.scripting.executeScript({ + target: { tabId: tab.id }, + func: (settings: Settings) => { + window.speedReaderSettings = settings; + }, + args: [finalSettings], + }); + + await browser.scripting.executeScript({ + target: { tabId: tab.id }, + files: ["/build/speed-reader.js"], + }); +} + +browser.action.onClicked.addListener(runSpeedReader); + +browser.contextMenus.onClicked.addListener((info) => { + if (info.menuItemId == "speed-reader") { + runSpeedReader(); + } +}); diff --git a/manifest.json b/manifest.json index 934ad4f..d00ef59 100644 --- a/manifest.json +++ b/manifest.json @@ -1,13 +1,13 @@ { - "manifest_version": 2, + "manifest_version": 3, "name": "Speed Reader", - "version": "1.4", + "version": "1.6", "description": "Speed-read the web", "homepage_url": "https://github.com/filipesabella/speed-reader", "browser_specific_settings": { "gecko": { "id": "{9ad66b79-fd5e-477a-8915-eea26c83ff42}", - "strict_min_version": "42.0" + "strict_min_version": "109.0" } }, "icons": { @@ -15,17 +15,28 @@ "96": "icons/speed-reader-96.png" }, "permissions": [ + "contextMenus", "activeTab", - "storage" + "storage", + "scripting" ], - "browser_action": { + "action": { "default_icon": "icons/speed-reader-36.png", "default_title": "Speed Reader" }, + "commands": { + "_execute_action": { + "suggested_key": { + "default": "Ctrl+Alt+U" + }, + "description": "Speed-read the selected text" + } + }, "background": { "scripts": [ - "extension.js" - ] + "build/extension.js" + ], + "service_worker": "build/extension.js" }, "options_ui": { "page": "build/settings.html" diff --git a/package.json b/package.json index 6986ed3..29bded2 100644 --- a/package.json +++ b/package.json @@ -1,28 +1,34 @@ { "name": "speed-reader", - "version": "1.0.4", - "main": "index.js", + "version": "1.6", "license": "MIT", "scripts": { "start": "parcel src/test/test.html", "start-settings": "parcel src/settings/settings.html", - "test": "mocha src/test/**/*.test.ts -r jsdom-global/register -r ts-node/register", - "build-settings": "parcel build src/settings/settings.html --out-dir build/ --out-file settings --public-url ./ --no-source-maps --no-minify", - "build-main": "parcel build src/main/speed-reader.ts --out-dir build/ --out-file speed-reader.js --no-source-maps --no-minify", - "build": "yarn test && yarn build-settings && yarn build-main", - "package": "rm -rf build && yarn build && zip -r extension.zip build icons extension.js manifest.json" + "test": "mocha --require ts-node/register src/test/**/*.test.ts", + "build-settings": "parcel build src/settings/settings.html --dist-dir build/ --public-url ./ --no-source-maps", + "build-main": "parcel build src/main/speed-reader.ts --dist-dir build/ --no-source-maps", + "build-extension": "parcel build extension.ts --dist-dir build/ --no-source-maps", + "build": "yarn test && yarn build-settings && yarn build-main && yarn build-extension", + "package": "rm -rf build && yarn build && zip -r extension.zip build icons manifest.json" }, "devDependencies": { - "@types/chai": "^4.2.14", - "@types/mocha": "^8.2.0", - "@types/node": "^14.14.14", + "@parcel/core": "^2.14.4", + "@parcel/transformer-less": "2.14.4", + "@types/node": "^22.12.0", + "@types/webextension-polyfill": "^0.12.3", + "@types/mocha": "^7.0.2", + "@types/chai": "^4.2.11", "chai": "^4.2.0", - "jsdom": "^16.4.0", + "mocha": "^7.1.1", + "ts-node": "^8.8.1", + "jsdom": "^26.0.0", "jsdom-global": "^3.0.2", - "less": "^4.1.2", - "mocha": "^8.2.1", - "parcel-bundler": "^1.12.4", - "ts-node": "^9.1.1", - "typescript": "^4.1.3" + "less": "^4.2.2", + "parcel": "^2.14.4", + "typescript": "^5.7.3" + }, + "dependencies": { + "webextension-polyfill": "^0.12.0" } } diff --git a/src/main/Renderer.ts b/src/main/Renderer.ts index 13703ea..201c913 100644 --- a/src/main/Renderer.ts +++ b/src/main/Renderer.ts @@ -3,7 +3,7 @@ import { Settings } from './Settings'; import { remainingTime } from './words'; export class Renderer { - private container: HTMLDivElement; + private container!: HTMLDivElement; constructor(private readonly words: Iterator) { } @@ -80,7 +80,7 @@ export class Renderer { const handleEvent = (type: 'press' | 'down' | 'up') => (e: KeyboardEvent) => { - const handler = eventHandlers[type][e.code]; + const handler = (eventHandlers[type] as { [key: string]: () => void })[e.code]; if (handler) { e.preventDefault(); handler(); @@ -122,7 +122,7 @@ export class Renderer { return [ word.substring(0, middleIndex).replace(/\s/g, ' '), word.charAt(middleIndex), - word.substr(middleIndex + 1).replace(/\s/g, ' '), + word.substring(middleIndex + 1).replace(/\s/g, ' '), ]; } diff --git a/src/main/Settings.ts b/src/main/Settings.ts index 908d887..54a2b47 100644 --- a/src/main/Settings.ts +++ b/src/main/Settings.ts @@ -12,11 +12,10 @@ export type Settings = { height: string; speedIncrement: number; initialSpeed: number; + punctuationDelayMultiplier: number; wordAmount: number; }; -// duplicated in extension.js. when in the mood, de-duplicate by building -// extension.js with parcel bundler to support importing from settings.ts export const defaultSettings = { fontFamily: 'monospace', backgroundColor: 'hsl(0, 0%, 15%)', @@ -28,6 +27,7 @@ export const defaultSettings = { height: 'auto', speedIncrement: 30, initialSpeed: 400, + punctuationDelayMultiplier: 2, wordAmount: 1, }; diff --git a/src/main/words.ts b/src/main/words.ts index fcf4ab8..be7373a 100644 --- a/src/main/words.ts +++ b/src/main/words.ts @@ -1,4 +1,5 @@ import { Iterator } from './Iterator'; +import { defaultSettings } from './Settings'; export function textToWords(text: string, wordAmount: number) : Iterator { @@ -44,7 +45,8 @@ export function timeoutForWord(interval: number, word: string): number { if (word.match(/\n$/)) { intervalMultiplier = 3; } else if (word.match(/[,\.\?\!\:]$/)) { - intervalMultiplier = 2; + intervalMultiplier = defaultSettings.punctuationDelayMultiplier < 1 ? + 1 : defaultSettings.punctuationDelayMultiplier; // don't allow < 1 on the multiplier } return interval * intervalMultiplier; diff --git a/src/settings/settings.html b/src/settings/settings.html index 14cd14e..ff08aed 100644 --- a/src/settings/settings.html +++ b/src/settings/settings.html @@ -31,6 +31,10 @@

Speed Reader Settings

+ + Speed Reader Settings Hello - + diff --git a/src/settings/settings.ts b/src/settings/settings.ts index aab2bac..e85eb79 100644 --- a/src/settings/settings.ts +++ b/src/settings/settings.ts @@ -11,11 +11,11 @@ import { .querySelector('#speed-reader-settings form')!; loadSettingsFromStorage().then(settings => { container.querySelectorAll('input').forEach(e => { - const attribute = e.name; + const attribute = e.name as keyof Settings; if (e.type === 'checkbox') { - e.checked = settings[attribute]; + e.checked = settings[attribute] as boolean; } else { - e.value = settings[attribute]; + e.value = settings[attribute] as string; } e.onchange = e.onkeyup = () => { @@ -67,16 +67,16 @@ import { function readSettingsFromForm(): Settings { const container = document .querySelector('#speed-reader-settings form')!; - const settings = { ...defaultSettings }; + const settings: Settings = { ...defaultSettings }; container.querySelectorAll('input').forEach(e => { - const attribute = e.name; + const attribute = e.name as keyof Settings; if (e.type === 'checkbox') { - settings[attribute] = e.checked; + (settings as any)[attribute] = e.checked; } else if (e.type === 'number') { - settings[attribute] = + (settings as any)[attribute] = parseInt(e.value) || defaultSettings.speedIncrement; } else { - settings[attribute] = e.value; + (settings as any)[attribute] = e.value; } }); diff --git a/src/test/test.html b/src/test/test.html index 4287761..dbb3bbb 100644 --- a/src/test/test.html +++ b/src/test/test.html @@ -115,7 +115,7 @@

- +