Skip to content

Commit 7bfc451

Browse files
Merge pull request #433 from Shared-Reality-Lab/refactor-ext
fix: extension refactoring
2 parents 2bc7c70 + f4205cf commit 7bfc451

22 files changed

Lines changed: 1281 additions & 334 deletions

src/README.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# IMAGE Browser Extension Source Code
2+
3+
This directory contains the source code for the IMAGE browser extension, which is designed to make images, charts, and maps more accessible through various rendering techniques.
4+
5+
## Directory Structure
6+
7+
Each folder in this directory has its own README file with a brief description of its purpose and contents:
8+
9+
- **_locales**: Localization files for different languages
10+
- **audio**: Audio files for notifications and feedback
11+
- **charts**: Utilities for handling chart data
12+
- **errors**: Error handling and display
13+
- **feedback**: User feedback collection
14+
- **firstLaunch**: First-time user experience
15+
- **info**: Rendering information display
16+
- **launchpad**: Main interface for accessing features
17+
- **maps**: Map processing utilities
18+
- **monarch**: Tactile rendering features
19+
- **options**: Extension preferences
20+
- **progressBar**: Processing status indicators
21+
- **types**: TypeScript type definitions
22+
23+
## Core Files
24+
25+
- **background.ts**: Main background script
26+
- **content.ts**: Content script for web page interaction
27+
- **config.ts**: Extension configuration
28+
- **utils.ts**: General utility functions
29+
- **manifest.json**: Extension metadata and permissions
30+
31+
For more information about the IMAGE project, visit https://image.a11y.mcgill.ca

src/_locales/README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Localization Files
2+
3+
This directory contains localization files for the IMAGE browser extension, allowing the extension to be used in multiple languages.
4+
5+
## Structure
6+
7+
- **en/**: English localization
8+
- **messages.json**: Contains all English text strings used in the extension
9+
- **fr/**: French localization
10+
- **messages.json**: Contains all French text strings used in the extension
11+
12+
## Usage
13+
14+
The extension uses the WebExtension i18n API to load the appropriate strings based on the user's browser language or their selected preference. Each string has a unique identifier and can include a description to provide context for translators.
15+
16+
Example from messages.json:
17+
```json
18+
"extensionName": {
19+
"message": "IMAGE Extension",
20+
"description": "Extension name"
21+
}
22+
```
23+
24+
These strings are referenced in the code using `browser.i18n.getMessage("extensionName")`.

src/audio/README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Audio Files
2+
3+
This directory contains audio files used by the IMAGE browser extension for providing auditory feedback to users during various stages of the rendering process.
4+
5+
## Files
6+
7+
- **IMAGE-Error.mp3**: Played when an error occurs during the rendering process
8+
- **IMAGE-Processing.mp3**: Played when the extension is processing an image
9+
- **IMAGE-RequestSent.mp3**: Played when a request is sent to the IMAGE server
10+
- **IMAGE-ResultsArrived.mp3**: Played when results arrive from the IMAGE server
11+
12+
## Usage
13+
14+
These audio files provide important non-visual cues to users, particularly those who rely on screen readers or other assistive technologies. They help users understand the current state of the extension without requiring visual feedback.
15+
16+
The audio files are played at appropriate times during the extension's operation, such as when a request is sent to the server or when results are received.

src/background-utils.ts

Lines changed: 291 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,291 @@
1+
/*
2+
* Copyright (c) 2021 IMAGE Project, Shared Reality Lab, McGill University
3+
*
4+
* This program is free software: you can redistribute it and/or modify
5+
* it under the terms of the GNU General Public License as
6+
* published by the Free Software Foundation, either version 3 of the
7+
* License, or (at your option) any later version.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU General Public License for more details.
13+
* You should have received a copy of the GNU General Public License
14+
* and our Additional Terms along with this program.
15+
* If not, see <https://github.com/Shared-Reality-Lab/IMAGE-browser/LICENSE>.
16+
*/
17+
import browser from "webextension-polyfill";
18+
import { v4 as uuidv4 } from "uuid";
19+
import { IMAGERequest } from "./types/request.schema";
20+
import { getAllStorageSyncData, getCapabilities, getRenderers, getLanguage, windowsPanel } from './utils';
21+
import { encryptData, monarchPopUp, saveToLocalStorage } from "./monarch/utils";
22+
import { TatStorageData } from "./monarch/types";
23+
import { openRenderingsinWindow } from "./config";
24+
25+
/**
26+
* Generates a query for remote resources
27+
*
28+
* @param message - Object containing context, URL, dimensions, and graphic blob
29+
* @returns Promise resolving to an IMAGERequest object
30+
*/
31+
export async function generateQuery(message: { context: string, url: string, dims: [number, number], graphicBlob: string }): Promise<IMAGERequest> {
32+
let renderers = await getRenderers();
33+
let capabilities = await getCapabilities();
34+
console.debug("inside generate query");
35+
return {
36+
"request_uuid": uuidv4(),
37+
"timestamp": Math.round(Date.now() / 1000),
38+
"URL": message.url,
39+
"graphic": message.graphicBlob,
40+
"dimensions": message.dims,
41+
"context": message.context,
42+
"language": await getLanguage(),
43+
"capabilities": capabilities,
44+
"renderers": renderers
45+
} as IMAGERequest;
46+
}
47+
48+
/**
49+
* Generates a query for local resources
50+
*
51+
* @param message - Object containing context, dimensions, image, and graphic blob
52+
* @returns Promise resolving to an IMAGERequest object
53+
*/
54+
export async function generateLocalQuery(message: { context: string, dims: [number, number], image: string, graphicBlob: string }): Promise<IMAGERequest> {
55+
let renderers = await getRenderers();
56+
let capabilities = await getCapabilities();
57+
return {
58+
"request_uuid": uuidv4(),
59+
"timestamp": Math.round(Date.now() / 1000),
60+
"graphic": message.graphicBlob,
61+
"dimensions": message.dims,
62+
"context": message.context,
63+
"language": await getLanguage(),
64+
"capabilities": capabilities,
65+
"renderers": renderers
66+
} as IMAGERequest;
67+
}
68+
69+
/**
70+
* Generates a query for chart resources
71+
*
72+
* @param message - Object containing highChartsData
73+
* @returns Promise resolving to an IMAGERequest object
74+
*/
75+
export async function generateChartQuery(message: { highChartsData: { [k: string]: unknown } }): Promise<IMAGERequest> {
76+
let renderers = await getRenderers();
77+
let capabilities = await getCapabilities();
78+
return {
79+
"request_uuid": uuidv4(),
80+
"timestamp": Math.round(Date.now() / 1000),
81+
"highChartsData": message.highChartsData,
82+
"language": await getLanguage(),
83+
"capabilities": capabilities,
84+
"renderers": renderers
85+
} as IMAGERequest;
86+
}
87+
88+
/**
89+
* Processes tactile rendering data for Monarch or Tactile Authoring Tool
90+
*
91+
* @param tactileSvgGraphic - SVG graphic data in base64 format
92+
* @param query - The IMAGERequest object
93+
* @param message - The message object containing options
94+
* @param serverUrl - The server URL to send requests to
95+
*/
96+
export async function processTactileRendering(tactileSvgGraphic: string, query: IMAGERequest, message: any, serverUrl: RequestInfo) {
97+
let items = await getAllStorageSyncData();
98+
let encodedSvg = tactileSvgGraphic.split("data:image/svg+xml;base64,")[1];
99+
let svgDom = atob(encodedSvg);
100+
console.log("SVG DOM", svgDom);
101+
let reqTitle = items["monarchTitle"];
102+
let reqSecretKey = items["monarchSecretKey"];
103+
let reqChannelId = items["monarchChannelId"];
104+
let encryptionKey = items["monarchEncryptionKey"];
105+
const flowType = reqChannelId ? "update" : "create";
106+
let monarchTargetUrl = `${serverUrl}/monarch`;
107+
monarchTargetUrl = monarchTargetUrl.replace(/([^:]\/)\/+/g, "$1");
108+
let monarchFetchUrl = flowType == "update" ?
109+
`${monarchTargetUrl}/update/${reqChannelId}` :
110+
`${monarchTargetUrl}/create`;
111+
let encryptedGraphicBlob = query["graphic"] && await encryptData(query["graphic"], encryptionKey);
112+
let encryptedCoordinates = query["coordinates"] && await encryptData(JSON.stringify(query["coordinates"]), encryptionKey);
113+
let encryptedPlaceId = query["placeID"] && await encryptData(query["placeID"], encryptionKey);
114+
const reqData = await encryptData(svgDom, encryptionKey);
115+
const reqBody = {
116+
"data": reqData,
117+
"layer": "None",
118+
"title": reqTitle,
119+
"secret": reqSecretKey,
120+
"graphicBlob": encryptedGraphicBlob,
121+
"coordinates": encryptedCoordinates,
122+
"placeID": encryptedPlaceId
123+
};
124+
125+
if (message["sendToMonarch"]) {
126+
/** Send Graphic to Monarch flow - Make curl request to monarch */
127+
const response = await fetch(monarchFetchUrl,
128+
{
129+
"method": "POST",
130+
"headers": {
131+
"Content-Type": "application/json"
132+
},
133+
"body": JSON.stringify(reqBody)
134+
});
135+
136+
let responseJSON = {
137+
"id": reqChannelId || "",
138+
"secret": ""
139+
};
140+
if (flowType == "create") {
141+
responseJSON = await response.json();
142+
browser.storage.sync.set({
143+
"monarchChannelId": responseJSON["id"],
144+
"monarchSecretKey": responseJSON["secret"]
145+
});
146+
}
147+
let currentTab = await browser.tabs.query({ active: true, currentWindow: true });
148+
149+
// Check if the current tab is an extension page
150+
if (currentTab[0].url && !currentTab[0].url.startsWith('chrome-extension://')) {
151+
browser.scripting.executeScript({
152+
target: { tabId: currentTab[0].id || 0 },
153+
func: monarchPopUp,
154+
args: [responseJSON["id"], flowType]
155+
});
156+
} else {
157+
// If we're on an extension page, use notifications instead of alert
158+
const message = flowType === "create"
159+
? `New channel created with code ${responseJSON["id"]}`
160+
: `Graphic in channel ${responseJSON["id"]} has been updated!`;
161+
162+
browser.notifications.create({
163+
type: 'basic',
164+
iconUrl: 'image-icon-128.png',
165+
title: 'Monarch Response',
166+
message: message
167+
});
168+
}
169+
170+
} else {
171+
/** Handle "Load in Tactile Authoring Tool" flow */
172+
const tatStorageData: TatStorageData = {
173+
channelId: items["monarchChannelId"],
174+
graphicTitle: items["monarchTitle"],
175+
secretKey: items["monarchSecretKey"],
176+
graphicBlob: query["graphic"],
177+
coordinates: query["coordinates"] && JSON.stringify(query["coordinates"]),
178+
placeID: query["placeID"],
179+
}
180+
let tatTargetUrl = `${serverUrl}/tat/`;
181+
tatTargetUrl = tatTargetUrl.replace(/([^:]\/)\/+/g, "$1");
182+
let tabs = await browser.tabs.query({ url: tatTargetUrl });
183+
184+
/** encrypt data before storing in local storage */
185+
let encryptedSvgData = await encryptData(svgDom, encryptionKey);
186+
let encryptedTatData: TatStorageData = {channelId:"", graphicTitle:"", secretKey:""};
187+
for (let key of Object.keys(tatStorageData)) {
188+
let stringToEncrypt = tatStorageData[key as keyof TatStorageData];
189+
if (stringToEncrypt){
190+
encryptedTatData[key as keyof TatStorageData] = await encryptData(tatStorageData[key as keyof TatStorageData], encryptionKey);
191+
}
192+
}
193+
194+
if (tabs && tabs.length > 0) {
195+
let existingTab = tabs[0];
196+
browser.scripting.executeScript({
197+
target: { tabId: existingTab.id || 0 },
198+
func: saveToLocalStorage,
199+
args: [encryptedSvgData, encryptedTatData, existingTab]
200+
});
201+
browser.tabs.update(existingTab.id, { active: true });
202+
}
203+
else {
204+
let authoringTool = browser.tabs.create({
205+
url: tatTargetUrl
206+
});
207+
authoringTool.then((tab) => {
208+
browser.scripting.executeScript({
209+
target: { tabId: tab.id || 0 },
210+
func: saveToLocalStorage,
211+
args: [encryptedSvgData, encryptedTatData]
212+
});
213+
}, (error) => { console.log(error) });
214+
}
215+
}
216+
}
217+
218+
/**
219+
* Converts a Blob to a base64 string
220+
*
221+
* @param blob - The Blob to convert
222+
* @returns Promise resolving to the base64 string
223+
*/
224+
export function blobToBase64(blob: Blob) {
225+
return new Promise((resolve, reject) => {
226+
const reader = new FileReader();
227+
reader.onload = () => resolve(reader.result as string);
228+
reader.onerror = () => reject(reader.error);
229+
reader.readAsDataURL(blob);
230+
});
231+
}
232+
233+
/**
234+
* Creates a panel or tab to display renderings
235+
*
236+
* @param query - The IMAGERequest object
237+
* @param graphicUrl - URL of the graphic
238+
* @returns Promise resolving to the created window or tab
239+
*/
240+
export function createPanel(query: IMAGERequest, graphicUrl: string) {
241+
let window = windowsPanel ? browser.windows.create({
242+
type: "normal",
243+
url: `info/info.html?uuid=${query["request_uuid"]}&graphicUrl=${graphicUrl}&dimensions=${query["dimensions"]}`,
244+
height: 1080,
245+
width: 1920
246+
}) : browser.tabs.create({
247+
url: `info/info.html?uuid=${query["request_uuid"]}&graphicUrl=${graphicUrl}&dimensions=${query["dimensions"]}`,
248+
});
249+
return window;
250+
}
251+
252+
/**
253+
* Toggles map options based on debug and monarch settings
254+
*
255+
* @param showDebugOptions - Boolean indicating whether to show debug options
256+
* @param monarchEnabled - Boolean indicating whether monarch is enabled
257+
*/
258+
export function toggleMapOptions(showDebugOptions: Boolean, monarchEnabled: Boolean) {
259+
let mapButtonContainer = document.getElementById("map-button-container");
260+
let mapSelectContainer = document.getElementById("map-select-container");
261+
if (mapButtonContainer && mapSelectContainer) {
262+
if (monarchEnabled) {
263+
mapSelectContainer.style.display = "flex";
264+
mapButtonContainer.style.display = "none";
265+
} else {
266+
mapSelectContainer.style.display = "none";
267+
mapButtonContainer.style.display = "flex";
268+
}
269+
}
270+
}
271+
272+
/**
273+
* Creates an offscreen document to keep the service worker running
274+
*/
275+
export async function createOffscreen() {
276+
// @ts-ignore
277+
await browser.offscreen.createDocument({
278+
url: 'offscreen.html',
279+
reasons: ['BLOBS'],
280+
justification: 'keep service worker running',
281+
}).catch(() => { });
282+
}
283+
284+
/**
285+
* Callback function for browser.contextMenus.create
286+
*/
287+
export function onCreated(): void {
288+
if (browser.runtime.lastError) {
289+
//console.error(browser.runtime.lastError);
290+
}
291+
}

0 commit comments

Comments
 (0)