Skip to content

Commit

Permalink
feat!: Migrate to MV3
Browse files Browse the repository at this point in the history
- Update manifest
- Switch to newer APIs
- Patch chrome stub with new APIs for testing
- Move clipboard and audio functionality into offscreen page (requires Chrome 109)
- rikaikun now remembers on state between startups; required due to ephemeral nature of new scripts.

BREAKING CHANGE: MV3 with offscreen pages requires at least Chrome 109 which is the new minimum version.
Fixes #187
Fixes #65
  • Loading branch information
melink14 committed Aug 25, 2024
1 parent c24c697 commit ca3c3ca
Show file tree
Hide file tree
Showing 17 changed files with 921 additions and 515 deletions.
5 changes: 0 additions & 5 deletions extension/background.html

This file was deleted.

114 changes: 62 additions & 52 deletions extension/background.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { RcxDict } from './data';
import { RcxMain } from './rikaichan';
import { configPromise } from './configuration';
import { tts } from './texttospeech';
import { setupOffscreenDocument } from './offscreen-setup';

/**
* Returns a promise for fully initialized RcxMain. Async due to config and
Expand All @@ -10,12 +10,13 @@ import { tts } from './texttospeech';
async function createRcxMainPromise(): Promise<RcxMain> {
const config = await configPromise;
const dict = await RcxDict.create(config);
return RcxMain.create(dict, config);
const { enabled } = await chrome.storage.local.get({ enabled: false });
return RcxMain.create(dict, config, enabled);
}
const rcxMainPromise: Promise<RcxMain> = createRcxMainPromise();

// eslint-disable-next-line @typescript-eslint/no-misused-promises
chrome.browserAction.onClicked.addListener(async (tab) => {
chrome.action.onClicked.addListener(async (tab) => {
const rcxMain = await rcxMainPromise;
rcxMain.inlineToggle(tab);
});
Expand All @@ -27,55 +28,64 @@ chrome.tabs.onActivated.addListener(async (activeInfo) => {
rcxMain.onTabSelect(activeInfo.tabId);
});

// Passing a promise to `addListener` here allows us to await the promise in tests.
// eslint-disable-next-line @typescript-eslint/no-misused-promises
chrome.runtime.onMessage.addListener(async (request, sender, response) => {
const rcxMain = await rcxMainPromise;
switch (request.type) {
case 'enable?':
console.log('enable?');
if (sender.tab === undefined) {
throw TypeError('sender.tab is always defined here.');
}
rcxMain.onTabSelect(sender.tab.id);
break;
case 'xsearch':
console.log('xsearch');
response(rcxMain.search(request.text, request.dictOption));
break;
case 'resetDict':
console.log('resetDict');
rcxMain.resetDict();
break;
case 'translate':
console.log('translate');
response(rcxMain.dict.translate(request.title));
break;
case 'makehtml':
console.log('makehtml');
response(rcxMain.dict.makeHtml(request.entry));
break;
case 'switchOnlyReading':
console.log('switchOnlyReading');
void chrome.storage.sync.set({
onlyreading: !rcxMain.config.onlyreading,
});
break;
case 'copyToClip':
console.log('copyToClip');
rcxMain.copyToClip(sender.tab, request.entry);
break;
case 'playTTS':
console.log('playTTS');
tts.play(request.text);
break;
default:
console.log(request);
}
chrome.runtime.onMessage.addListener((request, sender, response) => {
void (async () => {
const rcxMain = await rcxMainPromise;
switch (request.type) {
case 'enable?':
console.log('enable?');
if (sender.tab === undefined) {
throw TypeError('sender.tab is always defined here.');
}

Check warning on line 39 in extension/background.ts

View check run for this annotation

Codecov / codecov/patch

extension/background.ts#L38-L39

Added lines #L38 - L39 were not covered by tests
rcxMain.onTabSelect(sender.tab.id);
break;
case 'xsearch':
console.log('xsearch');
response(rcxMain.search(request.text, request.dictOption));
break;
case 'resetDict':
console.log('resetDict');
rcxMain.resetDict();
break;

Check warning on line 49 in extension/background.ts

View check run for this annotation

Codecov / codecov/patch

extension/background.ts#L47-L49

Added lines #L47 - L49 were not covered by tests
case 'translate':
console.log('translate');
response(rcxMain.dict.translate(request.title));
break;
case 'makehtml':
console.log('makehtml');
response(rcxMain.dict.makeHtml(request.entry));
break;
case 'switchOnlyReading':
console.log('switchOnlyReading');
void chrome.storage.sync.set({
onlyreading: !rcxMain.config.onlyreading,
});
break;

Check warning on line 63 in extension/background.ts

View check run for this annotation

Codecov / codecov/patch

extension/background.ts#L59-L63

Added lines #L59 - L63 were not covered by tests
case 'copyToClip':
console.log('copyToClip');
await rcxMain.copyToClip(sender.tab, request.entry);
break;
case 'playTTS':
console.log('playTTS');
await setupOffscreenDocument();
try {
await chrome.runtime.sendMessage({
target: 'offscreen',
type: 'playTtsOffscreen',
text: request.text,
});
} catch (e) {
throw new Error('Error while having offscreen doc play TTS.', {
cause: e,
});
}
break;

Check warning on line 82 in extension/background.ts

View check run for this annotation

Codecov / codecov/patch

extension/background.ts#L69-L82

Added lines #L69 - L82 were not covered by tests
default:
console.log('Unknown background request type:');
console.log(request);
}
})();
return true;
});

// Clear browser action badge text on first load
// Chrome preserves last state which is usually 'On'
void chrome.browserAction.setBadgeText({ text: '' });

export { rcxMainPromise as TestOnlyRxcMainPromise };
15 changes: 15 additions & 0 deletions extension/clipboard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export function copyToClipboard(text: string) {
const textEl = document.querySelector('#text');
if (textEl === null) {
throw new TypeError('Textarea for clipboard use not defined.');
}
if (!(textEl instanceof HTMLTextAreaElement)) {
throw new TypeError('#text element in offscreen doc not text area.');
}
// `document.execCommand('copy')` works against the user's selection in a web
// page. As such, we must insert the string we want to copy to the web page
// and to select that content in the page before calling `execCommand()`.
textEl.value = text;
textEl.select();
document.execCommand('copy');
}
18 changes: 9 additions & 9 deletions extension/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,8 @@ class RcxDict {
[, , this.nameDict, this.nameIndex] = await Promise.all([
this.loadDictionaries(),
this.loadDeinflectionData(),
this.fileReadAsync(chrome.extension.getURL('data/names.dat')),
this.fileReadAsync(chrome.extension.getURL('data/names.idx')),
this.fileReadAsync(chrome.runtime.getURL('data/names.dat')),
this.fileReadAsync(chrome.runtime.getURL('data/names.idx')),
]);

const ended = +new Date();
Expand Down Expand Up @@ -161,25 +161,25 @@ class RcxDict {
return;
}

this.nameDict = this.fileRead(chrome.extension.getURL('data/names.dat'));
this.nameIndex = this.fileRead(chrome.extension.getURL('data/names.idx'));
this.nameDict = this.fileRead(chrome.runtime.getURL('data/names.dat'));
this.nameIndex = this.fileRead(chrome.runtime.getURL('data/names.idx'));

Check warning on line 165 in extension/data.ts

View check run for this annotation

Codecov / codecov/patch

extension/data.ts#L164-L165

Added lines #L164 - L165 were not covered by tests
}

// Note: These are mostly flat text files; loaded as one continuous string to
// reduce memory use
async loadDictionaries(): Promise<void> {
[this.wordDict, this.wordIndex, this.kanjiData, this.radData] =
await Promise.all([
this.fileReadAsync(chrome.extension.getURL('data/dict.dat')),
this.fileReadAsync(chrome.extension.getURL('data/dict.idx')),
this.fileReadAsync(chrome.extension.getURL('data/kanji.dat')),
this.fileReadAsyncAsArray(chrome.extension.getURL('data/radicals.dat')),
this.fileReadAsync(chrome.runtime.getURL('data/dict.dat')),
this.fileReadAsync(chrome.runtime.getURL('data/dict.idx')),
this.fileReadAsync(chrome.runtime.getURL('data/kanji.dat')),
this.fileReadAsyncAsArray(chrome.runtime.getURL('data/radicals.dat')),
]);
}

async loadDeinflectionData() {
const buffer = await this.fileReadAsyncAsArray(
chrome.extension.getURL('data/deinflect.dat')
chrome.runtime.getURL('data/deinflect.dat')
);
let currentLength = -1;
let group: DeinflectionRuleGroup = {
Expand Down
17 changes: 10 additions & 7 deletions extension/manifest.json
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
{
"manifest_version": 2,
"manifest_version": 3,
"name": "rikaikun",
"version": "2.5.61",
"minimum_chrome_version": "80",
"minimum_chrome_version": "109",
"description": "rikaikun shows the reading and English definition of Japanese words when you hover over Japanese text in the browser.",
"icons": {
"48": "images/icon48.png",
"128": "images/icon128.png"
},
"offline_enabled": true,
"permissions": ["clipboardWrite", "storage"],
"permissions": ["clipboardWrite", "storage", "offscreen"],
"background": {
"page": "background.html",
"persistent": true
"service_worker": "background.js",
"type": "module"
},
"browser_action": {
"action": {
// If only one icon available, setting default_icon to string is allowed.
"default_icon": "images/ba.png"
},
"options_ui": {
Expand All @@ -35,5 +36,7 @@
"all_frames": true
}
],
"web_accessible_resources": ["css/popup.css"]
"web_accessible_resources": [
{ "resources": ["css/popup.css"], "matches": ["*://*/*"] }
]
}
25 changes: 25 additions & 0 deletions extension/offscreen-setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
export async function setupOffscreenDocument() {
// A simple try catch is easier to understand though perhaps less forward compatible.
// See https://groups.google.com/a/chromium.org/g/chromium-extensions/c/D5Jg2ukyvUc.
try {
await chrome.offscreen.createDocument({
url: 'offscreen.html',
reasons: [
chrome.offscreen.Reason.AUDIO_PLAYBACK,
chrome.offscreen.Reason.CLIPBOARD,
],
justification:
'Copying word definitions to clipboard. Playing audio using TTS of the selected word.',
});
} catch (error) {
let message;
if (error instanceof Error) {
message = error.message;
} else {
message = String(error);
}
if (!message.startsWith('Only a single offscreen')) {
throw error;
}
}

Check warning on line 24 in extension/offscreen-setup.ts

View check run for this annotation

Codecov / codecov/patch

extension/offscreen-setup.ts#L15-L24

Added lines #L15 - L24 were not covered by tests
}
8 changes: 8 additions & 0 deletions extension/offscreen.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<html>
<head>
<script type="module" src="offscreen.js"></script>
</head>
<body>
<textarea id="text"></textarea>
</body>
</html>
34 changes: 34 additions & 0 deletions extension/offscreen.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { copyToClipboard } from './clipboard';
import { tts } from './texttospeech';

chrome.runtime.onMessage.addListener(handleMessages);

let timeoutId = 0;
function handleMessages(message: {
target: string;
type: string;
text: string;
}): void {
if (message.target !== 'offscreen') {
return;
}
clearTimeout(timeoutId);
try {
switch (message.type) {
case 'copyToClipboardOffscreen':
// Error if we received the wrong kind of data.
if (typeof message.text !== 'string') {
throw new TypeError(
`Value provided must be a 'string', got '${typeof message.text}'.`
);
}
copyToClipboard(message.text);
break;
case 'playTtsOffscreen':
tts.play(message.text);
break;
}
} finally {
timeoutId = window.setTimeout(window.close, 30000);
}
}
Loading

0 comments on commit ca3c3ca

Please sign in to comment.