Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 18 additions & 35 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,43 +3,26 @@ name: Upload release
on:
push:
tags:
- 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
- 'v*'

permissions:
contents: write

jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [12.x]
steps:
- uses: actions/checkout@v1
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- name: npm build
run: |
npm ci
npm run build
env:
CI: true
- name: Create Release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: Release ${{ github.ref }}
draft: false
prerelease: false
- name: Upload Release Asset
id: upload-release-asset-zip-extension
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./dezoomify.zip
asset_name: dezoomify.zip
asset_content_type: application/zip
- uses: actions/checkout@v4
- name: Use Node.js 20.x
uses: actions/setup-node@v4
with:
node-version: 20.x
cache: npm
- name: Install dependencies
run: npm ci
- name: Build extension
run: npm run build
- name: Create release
uses: softprops/action-gh-release@v2
with:
files: ./dezoomify.zip
74 changes: 54 additions & 20 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -1,30 +1,64 @@
name: Tests

on: [push]
on:
push:
pull_request:

jobs:
build:

runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Use Node.js 20.x
uses: actions/setup-node@v4
with:
node-version: 20.x
cache: npm
- name: Install dependencies
run: npm ci
- name: Build extension
run: npm run build
- name: Download Firefox Nightly
if: matrix.name == 'firefox'
run: |
NIGHTLY_NAME=$(curl -sL https://archive.mozilla.org/pub/firefox/nightly/latest-mozilla-central/ | rg -o "firefox-[0-9.]+a1.en-US.linux-x86_64.tar.xz" | head -n 1)
curl -sL "https://archive.mozilla.org/pub/firefox/nightly/latest-mozilla-central/${NIGHTLY_NAME}" -o /tmp/firefox-nightly.tar.xz
tar -xf /tmp/firefox-nightly.tar.xz -C /tmp
echo "FIREFOX_BIN=/tmp/firefox/firefox" >> $GITHUB_ENV
- name: TypeScript checks
run: npm run test:types
- uses: actions/upload-artifact@v4
with:
name: dezoomify.zip
path: ./dezoomify.zip

browser-tests:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [12.x]

include:
- name: firefox
playwright: firefox
script: firefox
load: firefox
- name: chrome
playwright: chromium
script: chrome
load: chromium
steps:
- uses: actions/checkout@v1
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- name: npm install, build, and test
run: |
npm ci
npm run build
npm test
env:
CI: true
- uses: actions/upload-artifact@v1
with:
name: dezoomify.zip
path: ./dezoomify.zip
- uses: actions/checkout@v4
- name: Use Node.js 20.x
uses: actions/setup-node@v4
with:
node-version: 20.x
cache: npm
- name: Install dependencies
run: npm ci
- name: Build extension
run: npm run build
- name: Install Playwright browsers
run: npx playwright install --with-deps ${{ matrix.playwright }}
- name: Lint on ${{ matrix.name }}
run: npm run test:${{ matrix.script }}
- name: Load extension on ${{ matrix.name }}
run: xvfb-run -a npm run test:load:${{ matrix.load }}
112 changes: 75 additions & 37 deletions background.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,38 +31,48 @@ const META_REPLACE = [
{ pattern: iiifpath, replacement: '/info.json' },
{ pattern: /getTilesInfo\?object_id=(.*)&callback.*/, replacement: 'getTilesInfo?object_id=$1' },
];
// @ts-ignore
const VALID_RESOURCE_TYPES = new Set(Object.values(chrome.webRequest['ResourceType']));
/** @type {string[]} */
const REQUESTED_RESOURCE_TYPES = [
"main_frame",
"image",
"object",
"object_subrequest",
"sub_frame",
"xmlhttprequest",
"script",
"other"
];
/** @type {Set<string>} */
const VALID_RESOURCE_TYPES = new Set(
Object.values(chrome.webRequest.ResourceType)
);
/**
* @param {string} type
* @returns {type is chrome.webRequest.ResourceType}
*/
const isResourceType = (type) => VALID_RESOURCE_TYPES.has(type);

/**
* Selector for which requests we want to intercept
* @type chrome.webRequest.RequestFilter
*/
const REQUESTS_FILTER = {
urls: ["<all_urls>"],
// @ts-ignore
types: [
"main_frame",
"image",
"object",
"object_subrequest",
"sub_frame",
"xmlhttprequest",
"script",
"other"
].filter(t => VALID_RESOURCE_TYPES.has(t))
types: REQUESTED_RESOURCE_TYPES.filter(isResourceType)
};

/** @type {Map<number, PageListener>} */
const page_listeners = new Map;
const actionApi = chrome.action || chrome.browserAction;
const actionContext = chrome.action ? "action" : "browser_action";

/**
* Handle a click on the extension's icon
* @param {chrome.tabs.Tab} unchecked_tab
*/
async function click(unchecked_tab) {
const tab = checkTab(unchecked_tab);
chrome.browserAction.setBadgeText({ text: '...', tabId: tab.id });
actionApi.setBadgeText({ text: '...', tabId: tab.id });
const listener = page_listeners.get(tab.id);
if (listener && listener.isListening()) {
// Open the images that may have been found, and stop the existing listener
Expand All @@ -77,7 +87,7 @@ async function click(unchecked_tab) {
}
}

chrome.browserAction.onClicked.addListener(click);
actionApi.onClicked.addListener(click);

// Remove page listeners when their tab is closed
chrome.tabs.onRemoved.addListener(tabID => {
Expand All @@ -102,9 +112,8 @@ class PageListener {
this.listener = this.handleRequest.bind(this);
const filter = { tabId: this.tab.id, ...REQUESTS_FILTER };
chrome.webRequest.onBeforeRequest.addListener(this.listener, filter);
this.handleRequest(this.tab);
this.handleRequestUrl(this.tab.url);
this.updateStatus();
this.interval = setInterval(this.updateStatus.bind(this), 1000);
}


Expand All @@ -122,7 +131,6 @@ class PageListener {
*/
stop() {
chrome.webRequest.onBeforeRequest.removeListener(this.listener);
clearInterval(this.interval);
}

/**
Expand All @@ -138,16 +146,25 @@ class PageListener {
*/
hasVisibleResults() {
return new Promise((accept) => {
chrome.browserAction.getTitle({ tabId: this.tab.id }, title => {
accept(title !== getManifest().browser_action.default_title);
actionApi.getTitle({ tabId: this.tab.id }, title => {
accept(title !== getManifest().action.default_title);
});
});
}

/**
* @param {{url:string }} request
* @param {chrome.webRequest.OnBeforeRequestDetails} details
* @returns {chrome.webRequest.BlockingResponse | undefined}
*/
handleRequest(details) {
this.handleRequestUrl(details.url);
return undefined;
}

/**
* @param {string} url
*/
handleRequest({ url }) {
handleRequestUrl(url) {
for (const { pattern, replacement } of META_REPLACE) {
url = url.replace(pattern, replacement);
}
Expand Down Expand Up @@ -185,7 +202,7 @@ class PageListener {
const { host } = new URL(this.tab.url);
if (!this.isListening()) {
badge = '';
title = '' + manifest.browser_action.default_title;
title = '' + manifest.action.default_title;
} else if (found === 0) {
title = `Listening for zoomable image requests from ${host}... ` +
'Zoom on your image and it should be detected.';
Expand All @@ -195,10 +212,17 @@ class PageListener {
title = `Found ${found} images on ${host}. Click to open them.`
}
const tabId = this.tab.id;
chrome.browserAction.setBadgeText({ text: badge, tabId });
chrome.browserAction.setTitle({ title, tabId });
const path = this.isListening() ? manifest.icons : manifest.browser_action.default_icon;
chrome.browserAction.setIcon({ tabId, path });
actionApi.setBadgeText({ text: badge, tabId });
actionApi.setTitle({ title, tabId });
const path = (this.isListening()
? manifest.icons || manifest.action.default_icon
: manifest.action.default_icon || manifest.icons);
if (path) {
actionApi.setIcon({
tabId,
path: /** @type {string | { [index: string]: string }} */ (path)
});
}
}

openFound() {
Expand Down Expand Up @@ -239,12 +263,15 @@ function sameSite(url1, url2) {


/**
* @returns {{browser_action:chrome.runtime.ManifestAction} & chrome.runtime.Manifest} the extension's manifest
* @returns {chrome.runtime.Manifest & { action: chrome.runtime.ManifestAction }}
*/
function getManifest() {
const { browser_action, ...manifest } = chrome.runtime.getManifest();
if (!browser_action) throw new Error(`Invalid manifest: ${manifest}`);
return { browser_action, ...manifest };
const manifest = /** @type {chrome.runtime.Manifest & { browser_action?: chrome.runtime.ManifestAction }} */ (
chrome.runtime.getManifest()
);
const action = manifest.action || manifest.browser_action;
if (!action) throw new Error(`Invalid manifest: ${manifest}`);
return { ...manifest, action };
}

/**
Expand Down Expand Up @@ -288,11 +315,22 @@ const DEFAULT_MENU_ACTIONS = [
/**
* Add right-click menu actions
*/
chrome.contextMenus.removeAll();
DEFAULT_MENU_ACTIONS.forEach(({ title, url, onclick }) => {
chrome.contextMenus.create({
title,
contexts: ["browser_action"],
onclick: onclick || (_ => chrome.tabs.create({ url, active: true, }))
const menuHandlers = new Map();
chrome.contextMenus.removeAll(() => {
DEFAULT_MENU_ACTIONS.forEach(({ title, url, onclick }, index) => {
const id = `dezoomify-menu-${index}`;
const handler = onclick || ((_info, _tab) => {
if (url) chrome.tabs.create({ url, active: true });
});
menuHandlers.set(id, handler);
chrome.contextMenus.create({
id,
title,
contexts: [actionContext]
});
});
});
chrome.contextMenus.onClicked.addListener((info, tab) => {
const handler = menuHandlers.get(String(info.menuItemId));
if (handler) handler(info, tab);
});
20 changes: 16 additions & 4 deletions manifest.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"manifest_version": 2,
"manifest_version": 3,
"name": "Dezoomify",
"version": "0.4.6",
"description": "Download zoomable images from web pages",
Expand All @@ -9,8 +9,7 @@
"128": "icons/color/icon-128.png",
"512": "icons/color/icon-512.png"
},
"browser_action": {
"browser_style": true,
"action": {
"default_icon": {
"24": "icons/grey/icon-24.png",
"96": "icons/grey/icon-96.png",
Expand All @@ -22,12 +21,25 @@
"permissions": [
"activeTab",
"webRequest",
"contextMenus",
"contextMenus"
],
"host_permissions": [
"<all_urls>"
],
"background": {
"service_worker": "background.js",
"scripts": [
"background.js"
]
},
"browser_specific_settings": {
"gecko": {
"id": "{14074c89-8a5f-4813-98df-a7117f062871}",
"data_collection_permissions": {
"required": [
"none"
]
}
}
}
}
Loading