Skip to content

Commit f0c2053

Browse files
author
John Rogers
committed
initial check in of chrome plugin
1 parent db5652c commit f0c2053

18 files changed

Lines changed: 1491 additions & 0 deletions

.DS_Store

0 Bytes
Binary file not shown.

plugin/.DS_Store

6 KB
Binary file not shown.

plugin/background.js

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
importScripts("./js-base64/base64.js");
2+
3+
const harmonyURL = "https://harmonydata.ac.uk/app/#/";
4+
5+
const createHarmonyUrl = ({ questions, instrument_name }) => {
6+
if (
7+
Array.isArray(questions) &&
8+
questions.length &&
9+
questions.every(
10+
(q) =>
11+
typeof q === "string" ||
12+
q instanceof String ||
13+
(q.question_text &&
14+
(typeof q.question_text === "string" ||
15+
q.question_text instanceof String))
16+
)
17+
) {
18+
const qArray = questions.map((q, i) => {
19+
return {
20+
question_no: q.question_no || i,
21+
question_text: q.question_text || q,
22+
};
23+
});
24+
const iArray = { instrument_name: instrument_name, questions: qArray };
25+
return harmonyURL + "import/" + Base64.encode(JSON.stringify(iArray), true);
26+
} else {
27+
throw new Error(
28+
"questions is not properly formatted - it must be an array of question texts, or an array of objects which each must have a question_text property"
29+
);
30+
}
31+
};
32+
33+
// Create context menu item
34+
chrome.runtime.onInstalled.addListener(() => {
35+
chrome.contextMenus.create({
36+
id: "sendToHarmony",
37+
title: "Send to Harmony",
38+
contexts: ["selection"],
39+
});
40+
// Initialize history in storage
41+
chrome.storage.local.set({ history: [] });
42+
});
43+
44+
// Function to find or create Harmony tab
45+
async function findOrCreateHarmonyTab(url) {
46+
// First, try to find an existing tab with our target name in the URL
47+
const tabs = await chrome.tabs.query({});
48+
const harmonyTab = tabs.find(
49+
(tab) => tab.url && tab.url.includes(harmonyURL)
50+
);
51+
52+
if (harmonyTab) {
53+
// Update existing tab
54+
await chrome.tabs.update(harmonyTab.id, { url: url, active: true });
55+
await chrome.windows.update(harmonyTab.windowId, { focused: true });
56+
} else {
57+
// Create new tab
58+
await chrome.tabs.create({ url: url });
59+
}
60+
}
61+
62+
// Listen for messages from popup
63+
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
64+
if (request.action === "openHarmonyUrl") {
65+
findOrCreateHarmonyTab(request.url);
66+
return true;
67+
}
68+
if (request.action === "returnCopied") {
69+
findOrCreateHarmonyTab(request.url);
70+
return true;
71+
}
72+
});
73+
74+
chrome.contextMenus.onClicked.addListener(function (info, tab) {
75+
if (info.menuItemId === "sendToHarmony") {
76+
if (tab?.id > -1) {
77+
chrome.scripting
78+
.executeScript({
79+
target: { tabId: tab.id },
80+
function: () => {
81+
const selection = document.getSelection();
82+
return selection ? selection.toString() : "";
83+
},
84+
})
85+
.then((resultArray) => {
86+
const result = resultArray[0];
87+
const selectedText =
88+
result && result && result.result ? result.result : ""; // Handle various result possibilities
89+
processSelection(selectedText, tab);
90+
})
91+
.catch((error) => {
92+
console.error("Error getting selected text:", error);
93+
});
94+
} else {
95+
// If tab.id is null (eg in a PDF), trigger the copy command and then read from clipboard
96+
chrome.tabs
97+
.sendMessage({
98+
action: "copySelection",
99+
})
100+
.then((response) => {
101+
if (response?.success) {
102+
try {
103+
navigator.clipboard
104+
.readText()
105+
.then((selectedText) => processSelection(selectedText, tab));
106+
} catch (err) {
107+
console.error("Failed to read clipboard contents: ", err);
108+
}
109+
}
110+
})
111+
.catch((error) => {
112+
console.error("Error executing copy command:", error);
113+
});
114+
}
115+
}
116+
});
117+
118+
async function processSelection(selectedText, tab) {
119+
if (!selectedText) {
120+
return; // Handle cases where no text is selected
121+
}
122+
123+
// Process the selected text here...
124+
const questionsArray = selectedText
125+
.split(/\r?\n|\s*<br\s*\/?>/i)
126+
.filter((line) => line.trim() !== "");
127+
128+
try {
129+
// Create the Harmony URL with the selected text as a question
130+
const harmonyUrl = createHarmonyUrl({
131+
questions: questionsArray,
132+
instrument_name: `Imported from ${tab.title} ${tab.url}`,
133+
});
134+
135+
// Store in history
136+
chrome.storage.local.get(["history"], function (result) {
137+
const history = result.history || [];
138+
history.unshift({
139+
text:
140+
selectedText.substring(0, 100) +
141+
(selectedText.length > 100 ? "..." : ""),
142+
url: tab.url,
143+
timestamp: new Date().toISOString(),
144+
harmonyUrl: harmonyUrl,
145+
});
146+
// Keep only last 10 items
147+
if (history.length > 10) history.pop();
148+
chrome.storage.local.set({ history: history });
149+
});
150+
151+
// Open or update the Harmony tab
152+
await findOrCreateHarmonyTab(harmonyUrl);
153+
154+
// Show success notification
155+
chrome.action.setBadgeText({ text: "✓" });
156+
chrome.action.setBadgeBackgroundColor({ color: "#4CAF50" });
157+
setTimeout(() => {
158+
chrome.action.setBadgeText({ text: "" });
159+
}, 2000);
160+
} catch (error) {
161+
// Show error notification
162+
chrome.action.setBadgeText({ text: "!" });
163+
chrome.action.setBadgeBackgroundColor({ color: "#F44336" });
164+
setTimeout(() => {
165+
chrome.action.setBadgeText({ text: "" });
166+
}, 2000);
167+
console.error("Error:", error);
168+
}
169+
}

plugin/content.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
2+
if (request.action === "copySelection") {
3+
try {
4+
// Execute the copy command
5+
document.execCommand("copy");
6+
navigator.clipboard.readText().then((selection) => {
7+
console.log("selectedText", selection);
8+
sendResponse({
9+
action: "returnCopied",
10+
success: true,
11+
selection: selection,
12+
});
13+
});
14+
} catch (error) {
15+
console.error("Error executing copy command:", error);
16+
sendResponse({ success: false, error: error.message });
17+
}
18+
}
19+
return true;
20+
});

plugin/icons/128.png

7.58 KB
Loading

plugin/icons/16.png

518 Bytes
Loading

plugin/icons/48.png

2.5 KB
Loading

plugin/icons/Thumbs.db

7.5 KB
Binary file not shown.

plugin/js-base64/LICENSE.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
Copyright (c) 2014, Dan Kogai
2+
All rights reserved.
3+
4+
Redistribution and use in source and binary forms, with or without
5+
modification, are permitted provided that the following conditions are met:
6+
7+
* Redistributions of source code must retain the above copyright notice, this
8+
list of conditions and the following disclaimer.
9+
10+
* Redistributions in binary form must reproduce the above copyright notice,
11+
this list of conditions and the following disclaimer in the documentation
12+
and/or other materials provided with the distribution.
13+
14+
* Neither the name of {{{project}}} nor the names of its
15+
contributors may be used to endorse or promote products derived from
16+
this software without specific prior written permission.
17+
18+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
22+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
25+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
26+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

plugin/js-base64/README.md

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
[![CI via GitHub Actions](https://github.com/dankogai/js-base64/actions/workflows/node.js.yml/badge.svg)](https://github.com/dankogai/js-base64/actions/workflows/node.js.yml)
2+
3+
# base64.js
4+
5+
Yet another [Base64] transcoder.
6+
7+
[Base64]: http://en.wikipedia.org/wiki/Base64
8+
9+
## Install
10+
11+
```shell
12+
$ npm install --save js-base64
13+
```
14+
15+
## Usage
16+
17+
### In Browser
18+
19+
Locally…
20+
21+
```html
22+
<script src="base64.js"></script>
23+
```
24+
25+
… or Directly from CDN. In which case you don't even need to install.
26+
27+
```html
28+
<script src="https://cdn.jsdelivr.net/npm/js-base64@3.7.7/base64.min.js"></script>
29+
```
30+
31+
This good old way loads `Base64` in the global context (`window`). Though `Base64.noConflict()` is made available, you should consider using ES6 Module to avoid tainting `window`.
32+
33+
### As an ES6 Module
34+
35+
locally…
36+
37+
```javascript
38+
import { Base64 } from 'js-base64';
39+
```
40+
41+
```javascript
42+
// or if you prefer no Base64 namespace
43+
import { encode, decode } from 'js-base64';
44+
```
45+
46+
or even remotely.
47+
48+
```html
49+
<script type="module">
50+
// note jsdelivr.net does not automatically minify .mjs
51+
import { Base64 } from 'https://cdn.jsdelivr.net/npm/js-base64@3.7.7/base64.mjs';
52+
</script>
53+
```
54+
55+
```html
56+
<script type="module">
57+
// or if you prefer no Base64 namespace
58+
import { encode, decode } from 'https://cdn.jsdelivr.net/npm/js-base64@3.7.7/base64.mjs';
59+
</script>
60+
```
61+
62+
### node.js (commonjs)
63+
64+
```javascript
65+
const {Base64} = require('js-base64');
66+
```
67+
68+
Unlike the case above, the global context is no longer modified.
69+
70+
You can also use [esm] to `import` instead of `require`.
71+
72+
[esm]: https://github.com/standard-things/esm
73+
74+
```javascript
75+
require=require('esm')(module);
76+
import {Base64} from 'js-base64';
77+
```
78+
79+
## SYNOPSIS
80+
81+
```javascript
82+
let latin = 'dankogai';
83+
let utf8 = '小飼弾'
84+
let u8s = new Uint8Array([100,97,110,107,111,103,97,105]);
85+
Base64.encode(latin); // ZGFua29nYWk=
86+
Base64.encode(latin, true); // ZGFua29nYWk skips padding
87+
Base64.encodeURI(latin); // ZGFua29nYWk
88+
Base64.btoa(latin); // ZGFua29nYWk=
89+
Base64.btoa(utf8); // raises exception
90+
Base64.fromUint8Array(u8s); // ZGFua29nYWk=
91+
Base64.fromUint8Array(u8s, true); // ZGFua29nYW which is URI safe
92+
Base64.encode(utf8); // 5bCP6aO85by+
93+
Base64.encode(utf8, true) // 5bCP6aO85by-
94+
Base64.encodeURI(utf8); // 5bCP6aO85by-
95+
```
96+
97+
```javascript
98+
Base64.decode( 'ZGFua29nYWk=');// dankogai
99+
Base64.decode( 'ZGFua29nYWk'); // dankogai
100+
Base64.atob( 'ZGFua29nYWk=');// dankogai
101+
Base64.atob( '5bCP6aO85by+');// '小飼弾' which is nonsense
102+
Base64.toUint8Array('ZGFua29nYWk=');// u8s above
103+
Base64.decode( '5bCP6aO85by+');// 小飼弾
104+
// note .decodeURI() is unnecessary since it accepts both flavors
105+
Base64.decode( '5bCP6aO85by-');// 小飼弾
106+
```
107+
108+
```javascript
109+
Base64.isValid(0); // false: 0 is not string
110+
Base64.isValid(''); // true: a valid Base64-encoded empty byte
111+
Base64.isValid('ZA=='); // true: a valid Base64-encoded 'd'
112+
Base64.isValid('Z A='); // true: whitespaces are okay
113+
Base64.isValid('ZA'); // true: padding ='s can be omitted
114+
Base64.isValid('++'); // true: can be non URL-safe
115+
Base64.isValid('--'); // true: or URL-safe
116+
Base64.isValid('+-'); // false: can't mix both
117+
```
118+
119+
### Built-in Extensions
120+
121+
By default `Base64` leaves built-in prototypes untouched. But you can extend them as below.
122+
123+
```javascript
124+
// you have to explicitly extend String.prototype
125+
Base64.extendString();
126+
// once extended, you can do the following
127+
'dankogai'.toBase64(); // ZGFua29nYWk=
128+
'小飼弾'.toBase64(); // 5bCP6aO85by+
129+
'小飼弾'.toBase64(true); // 5bCP6aO85by-
130+
'小飼弾'.toBase64URI(); // 5bCP6aO85by- ab alias of .toBase64(true)
131+
'小飼弾'.toBase64URL(); // 5bCP6aO85by- an alias of .toBase64URI()
132+
'ZGFua29nYWk='.fromBase64(); // dankogai
133+
'5bCP6aO85by+'.fromBase64(); // 小飼弾
134+
'5bCP6aO85by-'.fromBase64(); // 小飼弾
135+
'5bCP6aO85by-'.toUint8Array();// u8s above
136+
```
137+
138+
```javascript
139+
// you have to explicitly extend Uint8Array.prototype
140+
Base64.extendUint8Array();
141+
// once extended, you can do the following
142+
u8s.toBase64(); // 'ZGFua29nYWk='
143+
u8s.toBase64URI(); // 'ZGFua29nYWk'
144+
u8s.toBase64URL(); // 'ZGFua29nYWk' an alias of .toBase64URI()
145+
```
146+
147+
```javascript
148+
// extend all at once
149+
Base64.extendBuiltins()
150+
```
151+
152+
## `.decode()` vs `.atob` (and `.encode()` vs `btoa()`)
153+
154+
Suppose you have:
155+
156+
```
157+
var pngBase64 =
158+
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=";
159+
```
160+
161+
Which is a Base64-encoded 1x1 transparent PNG, **DO NOT USE** `Base64.decode(pngBase64)`.  Use `Base64.atob(pngBase64)` instead.  `Base64.decode()` decodes to UTF-8 string while `Base64.atob()` decodes to bytes, which is compatible to browser built-in `atob()` (Which is absent in node.js).  The same rule applies to the opposite direction.
162+
163+
Or even better, `Base64.toUint8Array(pngBase64)`.
164+
165+
## Brief History
166+
167+
* Since version 3.3 it is written in TypeScript. Now `base64.mjs` is compiled from `base64.ts` then `base64.js` is generated from `base64.mjs`.
168+
* Since version 3.7 `base64.js` is ES5-compatible again (hence IE11-compatible).
169+
* Since 3.0 `js-base64` switch to ES2015 module so it is no longer compatible with legacy browsers like IE (see above)

0 commit comments

Comments
 (0)