Compare commits
10 Commits
a370b46fae
...
528a63fdac
Author | SHA1 | Date | |
---|---|---|---|
528a63fdac | |||
2f13cf5d2a | |||
a528c72107 | |||
991fa8189a | |||
|
752d73418b | ||
|
4abbc707a5 | ||
|
c93682f677 | ||
|
096bde44ee | ||
|
a47cbc39e2 | ||
|
9ef7f9d383 |
14
README.md
14
README.md
@ -1,5 +1,8 @@
|
||||
# Yomichan
|
||||
|
||||
*Note: this project is no longer maintained. Please see [this
|
||||
post](https://foosoft.net/posts/sunsetting-the-yomichan-project/) for more information.*
|
||||
|
||||
Yomichan turns your web browser into a tool for building Japanese language literacy by helping you to decipher texts
|
||||
which would be otherwise too difficult tackle. This extension is similar to
|
||||
[Rikaichamp](https://addons.mozilla.org/en-US/firefox/addon/rikaichamp/) for Firefox and
|
||||
@ -43,16 +46,7 @@ New changes are initially introduced into the *testing* version, and after some
|
||||
relatively bug free, they will be promoted to the *stable* version. If you are technically savvy and don't mind
|
||||
submitting issues on GitHub, try the *testing* version; otherwise, the *stable* version will be your best bet.
|
||||
|
||||
* **Google Chrome**
|
||||
([stable](https://chrome.google.com/webstore/detail/yomichan/ogmnaimimemjmbakcfefmnahgdfhfami) or [testing](https://chrome.google.com/webstore/detail/yomichan-testing/bcknnfebhefllbjhagijobjklocakpdm)) \
|
||||
[![](img/chrome-web-store.png)](https://chrome.google.com/webstore/detail/yomichan/ogmnaimimemjmbakcfefmnahgdfhfami)
|
||||
|
||||
* **Mozilla Firefox**
|
||||
([stable](https://addons.mozilla.org/en-US/firefox/addon/yomichan/) or [testing](https://github.com/FooSoft/yomichan/releases)<sup>*</sup>) \
|
||||
[![](img/firefox-marketplace.png)](https://addons.mozilla.org/en-US/firefox/addon/yomichan/) \
|
||||
<sup>*</sup>Unlike Chrome, Firefox does not allow extensions meant for testing to be hosted in the marketplace.
|
||||
You will have to download a desired version and side-load it yourself. You only need to do this once and will get
|
||||
updates automatically.
|
||||
*Note: [Yomichan is no longer available for download](/posts/passing-the-torch-to-yomitan).*
|
||||
|
||||
## Dictionaries
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
"manifest": {
|
||||
"manifest_version": 2,
|
||||
"name": "Yomichan",
|
||||
"version": "22.9.9.2",
|
||||
"version": "22.10.23.0",
|
||||
"description": "Japanese dictionary with Anki integration",
|
||||
"author": "Alex Yatskov",
|
||||
"icons": {
|
||||
@ -259,7 +259,7 @@
|
||||
{
|
||||
"action": "set",
|
||||
"path": ["browser_specific_settings", "gecko", "update_url"],
|
||||
"value": "https://foosoft.net/projects/yomichan/dl/updates.json"
|
||||
"value": "https://raw.githubusercontent.com/FooSoft/yomichan/metadata/updates.json"
|
||||
}
|
||||
],
|
||||
"excludeFiles": [
|
||||
|
@ -41,9 +41,10 @@ class JsonSchemaAjv {
|
||||
|
||||
validate(data) {
|
||||
if (this._validate(data)) { return; }
|
||||
const {errors} = this._validate(data);
|
||||
const message = errors.map((e) => e.toString()).join('\n');
|
||||
throw new Error(message);
|
||||
const {errors} = this._validate;
|
||||
const error = new Error('Schema validation failed');
|
||||
error.data = JSON.parse(JSON.stringify(errors));
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,36 +0,0 @@
|
||||
{
|
||||
"addons": {
|
||||
"alex.testing@foosoft.net": {
|
||||
"updates": [
|
||||
{
|
||||
"version": "21.10.31.1",
|
||||
"update_link": "https://github.com/FooSoft/yomichan/releases/download/21.10.31.1/yomichan_testing-21.10.31.1-an+fx.xpi"
|
||||
},
|
||||
{
|
||||
"version": "22.2.2.0",
|
||||
"update_link": "https://github.com/FooSoft/yomichan/releases/download/22.2.2.0/yomichan_testing-22.2.2.0-an+fx.xpi"
|
||||
},
|
||||
{
|
||||
"version": "22.4.4.0",
|
||||
"update_link": "https://github.com/FooSoft/yomichan/releases/download/22.4.4.0/yomichan_testing-22.4.4.0-an+fx.xpi"
|
||||
},
|
||||
{
|
||||
"version": "22.6.6.0",
|
||||
"update_link": "https://github.com/FooSoft/yomichan/releases/download/22.6.6.0/a708116f79104891acbd-22.6.6.0.xpi"
|
||||
},
|
||||
{
|
||||
"version": "22.9.9.0",
|
||||
"update_link": "https://github.com/FooSoft/yomichan/releases/download/22.9.9.0/a708116f79104891acbd-22.9.9.0.xpi"
|
||||
},
|
||||
{
|
||||
"version": "22.9.9.1",
|
||||
"update_link": "https://github.com/FooSoft/yomichan/releases/download/22.9.9.1/a708116f79104891acbd-22.9.9.1.xpi"
|
||||
},
|
||||
{
|
||||
"version": "22.9.9.2",
|
||||
"update_link": "https://github.com/FooSoft/yomichan/releases/download/22.9.9.2/a708116f79104891acbd-22.9.9.2.xpi"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
@ -11,6 +11,7 @@
|
||||
<link rel="icon" type="image/png" href="/images/icon48.png" sizes="48x48">
|
||||
<link rel="icon" type="image/png" href="/images/icon64.png" sizes="64x64">
|
||||
<link rel="icon" type="image/png" href="/images/icon128.png" sizes="128x128">
|
||||
<link rel="stylesheet" type="text/css" href="/css/background.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@ -61,7 +62,7 @@
|
||||
https://bugzilla.mozilla.org/show_bug.cgi?id=1603985
|
||||
-->
|
||||
<!-- [html-validate-disable close-order] -->
|
||||
<div id="clipboard-image-paste-target" contenteditable="true">
|
||||
<div id="clipboard-rich-content-paste-target" contenteditable="true">
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
29
ext/css/background.css
Normal file
29
ext/css/background.css
Normal file
@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright (C) 2022 Yomichan Authors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/* stylelint-disable declaration-no-important */
|
||||
#clipboard-rich-content-paste-target * {
|
||||
background-image: none !important;
|
||||
list-style-image: none !important;
|
||||
content: none !important;
|
||||
cursor: auto !important;
|
||||
border-image-source: none !important;
|
||||
offset-path: none !important;
|
||||
-webkit-mask-image: none !important;
|
||||
mask-image: none !important;
|
||||
}
|
||||
/* stylelint-enable declaration-no-important */
|
@ -86,6 +86,7 @@ h1 {
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
white-space: pre-wrap;
|
||||
z-index: 1;
|
||||
margin: 0;
|
||||
}
|
||||
.search-button {
|
||||
flex: 0 0 auto;
|
||||
|
@ -38,7 +38,7 @@
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"enum": ["freq"],
|
||||
"const": "freq",
|
||||
"description": "Type of data. \"freq\" corresponds to frequency information."
|
||||
},
|
||||
{
|
||||
|
@ -27,7 +27,7 @@
|
||||
"properties": {
|
||||
"tag": {
|
||||
"type": "string",
|
||||
"enum": ["br"]
|
||||
"const": "br"
|
||||
},
|
||||
"data": {
|
||||
"$ref": "#/definitions/structuredContentData"
|
||||
@ -364,7 +364,7 @@
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["text"]
|
||||
"const": "text"
|
||||
},
|
||||
"text": {
|
||||
"type": "string",
|
||||
@ -381,7 +381,7 @@
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["structured-content"]
|
||||
"const": "structured-content"
|
||||
},
|
||||
"content": {
|
||||
"$ref": "#/definitions/structuredContent",
|
||||
@ -398,7 +398,7 @@
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["image"]
|
||||
"const": "image"
|
||||
},
|
||||
"path": {
|
||||
"type": "string",
|
||||
|
@ -49,7 +49,7 @@
|
||||
{
|
||||
"items": [
|
||||
{},
|
||||
{"enum": ["freq"]},
|
||||
{"const": "freq"},
|
||||
{
|
||||
"oneOf": [
|
||||
{
|
||||
@ -81,7 +81,7 @@
|
||||
{
|
||||
"items": [
|
||||
{},
|
||||
{"enum": ["pitch"]},
|
||||
{"const": "pitch"},
|
||||
{
|
||||
"type": ["object"],
|
||||
"description": "Pitch accent information for the term.",
|
||||
|
@ -38,7 +38,14 @@
|
||||
* wanakana
|
||||
*/
|
||||
|
||||
/**
|
||||
* This class controls the core logic of the extension, including API calls
|
||||
* and various forms of communication between browser tabs and external applications.
|
||||
*/
|
||||
class Backend {
|
||||
/**
|
||||
* Creates a new instance.
|
||||
*/
|
||||
constructor() {
|
||||
this._japaneseUtil = new JapaneseUtil(wanakana);
|
||||
this._environment = new Environment();
|
||||
@ -53,7 +60,7 @@ class Backend {
|
||||
// eslint-disable-next-line no-undef
|
||||
document: (typeof document === 'object' && document !== null ? document : null),
|
||||
pasteTargetSelector: '#clipboard-paste-target',
|
||||
imagePasteTargetSelector: '#clipboard-image-paste-target'
|
||||
richContentPasteTargetSelector: '#clipboard-rich-content-paste-target'
|
||||
});
|
||||
this._clipboardMonitor = new ClipboardMonitor({
|
||||
japaneseUtil: this._japaneseUtil,
|
||||
@ -145,6 +152,10 @@ class Backend {
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the instance.
|
||||
* @returns {Promise<void>} A promise which is resolved when initialization completes.
|
||||
*/
|
||||
prepare() {
|
||||
if (this._preparePromise === null) {
|
||||
const promise = this._prepareInternal();
|
||||
@ -596,7 +607,7 @@ class Backend {
|
||||
}
|
||||
|
||||
async _onApiClipboardGet() {
|
||||
return this._clipboardReader.getText();
|
||||
return this._clipboardReader.getText(false);
|
||||
}
|
||||
|
||||
async _onApiGetDisplayTemplatesHtml() {
|
||||
@ -1773,7 +1784,7 @@ class Backend {
|
||||
|
||||
try {
|
||||
if (clipboardDetails !== null && clipboardDetails.text) {
|
||||
clipboardText = await this._clipboardReader.getText();
|
||||
clipboardText = await this._clipboardReader.getText(false);
|
||||
}
|
||||
} catch (e) {
|
||||
errors.push(serializeError(e));
|
||||
|
@ -15,13 +15,29 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/**
|
||||
* This class is used to generate `fetch()` requests on the background page
|
||||
* with additional controls over anonymity and error handling.
|
||||
*/
|
||||
class RequestBuilder {
|
||||
/**
|
||||
* A progress callback for a fetch read.
|
||||
* @callback ProgressCallback
|
||||
* @param {boolean} complete Whether or not the data has been completely read.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Creates a new instance.
|
||||
*/
|
||||
constructor() {
|
||||
this._onBeforeSendHeadersExtraInfoSpec = ['blocking', 'requestHeaders', 'extraHeaders'];
|
||||
this._textEncoder = new TextEncoder();
|
||||
this._ruleIds = new Set();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the instance.
|
||||
*/
|
||||
async prepare() {
|
||||
try {
|
||||
await this._clearDynamicRules();
|
||||
@ -30,7 +46,14 @@ class RequestBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs an anonymized fetch request, which strips the `Cookie` header and adjust the `Origin` header.
|
||||
* @param {string} url The URL to fetch.
|
||||
* @param {RequestInit} init The initialization parameters passed to the `fetch` function.
|
||||
* @returns {Promise<Response>} The response of the `fetch` call.
|
||||
*/
|
||||
async fetchAnonymous(url, init) {
|
||||
fetch(1, 2);
|
||||
if (isObject(chrome.declarativeNetRequest)) {
|
||||
return await this._fetchAnonymousDeclarative(url, init);
|
||||
}
|
||||
@ -42,6 +65,12 @@ class RequestBuilder {
|
||||
return await this._fetchInternal(url, init, headerModifications);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the array buffer body of a fetch response, with an optional `onProgress` callback.
|
||||
* @param {Response} response The response of a `fetch` call.
|
||||
* @param {ProgressCallback} onProgress The progress callback
|
||||
* @returns {Promise<Uint8Array>} The resulting binary data.
|
||||
*/
|
||||
static async readFetchResponseArrayBuffer(response, onProgress) {
|
||||
let reader;
|
||||
try {
|
||||
|
@ -19,7 +19,13 @@
|
||||
* AnkiUtil
|
||||
*/
|
||||
|
||||
/**
|
||||
* This class controls communication with Anki via the AnkiConnect plugin.
|
||||
*/
|
||||
class AnkiConnect {
|
||||
/**
|
||||
* Creates a new instance.
|
||||
*/
|
||||
constructor() {
|
||||
this._enabled = false;
|
||||
this._server = null;
|
||||
@ -29,30 +35,59 @@ class AnkiConnect {
|
||||
this._apiKey = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the URL of the AnkiConnect server.
|
||||
* @type {string}
|
||||
*/
|
||||
get server() {
|
||||
return this._server;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assigns the URL of the AnkiConnect server.
|
||||
* @param {string} value The new server URL to assign.
|
||||
*/
|
||||
set server(value) {
|
||||
this._server = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets whether or not server communication is enabled.
|
||||
* @type {boolean}
|
||||
*/
|
||||
get enabled() {
|
||||
return this._enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether or not server communication is enabled.
|
||||
* @param {boolean} value The enabled state.
|
||||
*/
|
||||
set enabled(value) {
|
||||
this._enabled = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the API key used when connecting to AnkiConnect.
|
||||
* The value will be `null` if no API key is used.
|
||||
* @type {?string}
|
||||
*/
|
||||
get apiKey() {
|
||||
return this._apiKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the API key used when connecting to AnkiConnect.
|
||||
* @param {?string} value The API key to use, or `null` if no API key should be used.
|
||||
*/
|
||||
set apiKey(value) {
|
||||
this._apiKey = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a connection to AnkiConnect can be established.
|
||||
* @returns {Promise<boolean>} `true` if the connection was made, `false` otherwise.
|
||||
*/
|
||||
async isConnected() {
|
||||
try {
|
||||
await this._invoke('version');
|
||||
@ -62,6 +97,10 @@ class AnkiConnect {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the AnkiConnect API version number.
|
||||
* @returns {Promise<number>} The version number
|
||||
*/
|
||||
async getVersion() {
|
||||
if (!this._enabled) { return null; }
|
||||
await this._checkVersion();
|
||||
|
@ -39,7 +39,7 @@ class ClipboardMonitor extends EventDispatcher {
|
||||
|
||||
let text = null;
|
||||
try {
|
||||
text = await this._clipboardReader.getText();
|
||||
text = await this._clipboardReader.getText(false);
|
||||
} catch (e) {
|
||||
// NOP
|
||||
}
|
||||
|
@ -28,15 +28,15 @@ class ClipboardReader {
|
||||
* @param {object} details Details about how to set up the instance.
|
||||
* @param {?Document} details.document The Document object to be used, or null for no support.
|
||||
* @param {?string} details.pasteTargetSelector The selector for the paste target element.
|
||||
* @param {?string} details.imagePasteTargetSelector The selector for the image paste target element.
|
||||
* @param {?string} details.richContentPasteTargetSelector The selector for the rich content paste target element.
|
||||
*/
|
||||
constructor({document=null, pasteTargetSelector=null, imagePasteTargetSelector=null}) {
|
||||
constructor({document=null, pasteTargetSelector=null, richContentPasteTargetSelector=null}) {
|
||||
this._document = document;
|
||||
this._browser = null;
|
||||
this._pasteTarget = null;
|
||||
this._pasteTargetSelector = pasteTargetSelector;
|
||||
this._imagePasteTarget = null;
|
||||
this._imagePasteTargetSelector = imagePasteTargetSelector;
|
||||
this._richContentPasteTarget = null;
|
||||
this._richContentPasteTargetSelector = richContentPasteTargetSelector;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -56,13 +56,14 @@ class ClipboardReader {
|
||||
|
||||
/**
|
||||
* Gets the text in the clipboard.
|
||||
* @param {boolean} useRichText Whether or not to use rich text for pasting, when possible.
|
||||
* @returns {string} A string containing the clipboard text.
|
||||
* @throws {Error} Error if not supported.
|
||||
*/
|
||||
async getText() {
|
||||
async getText(useRichText) {
|
||||
/*
|
||||
Notes:
|
||||
document.execCommand('paste') doesn't work on Firefox.
|
||||
document.execCommand('paste') sometimes doesn't work on Firefox.
|
||||
See: https://bugzilla.mozilla.org/show_bug.cgi?id=1603985
|
||||
Therefore, navigator.clipboard.readText() is used on Firefox.
|
||||
|
||||
@ -72,7 +73,7 @@ class ClipboardReader {
|
||||
being an extension with clipboard permissions. It effectively asks for the
|
||||
non-extension permission for clipboard access.
|
||||
*/
|
||||
if (this._isFirefox()) {
|
||||
if (this._isFirefox() && !useRichText) {
|
||||
try {
|
||||
return await navigator.clipboard.readText();
|
||||
} catch (e) {
|
||||
@ -86,15 +87,15 @@ class ClipboardReader {
|
||||
throw new Error('Clipboard reading not supported in this context');
|
||||
}
|
||||
|
||||
let target = this._pasteTarget;
|
||||
if (target === null) {
|
||||
target = document.querySelector(this._pasteTargetSelector);
|
||||
if (target === null) {
|
||||
throw new Error('Clipboard paste target does not exist');
|
||||
}
|
||||
this._pasteTarget = target;
|
||||
}
|
||||
|
||||
if (useRichText) {
|
||||
const target = this._getRichContentPasteTarget();
|
||||
target.focus();
|
||||
document.execCommand('paste');
|
||||
const result = target.textContent;
|
||||
this._clearRichContent(target);
|
||||
return result;
|
||||
} else {
|
||||
const target = this._getPasteTarget();
|
||||
target.value = '';
|
||||
target.focus();
|
||||
document.execCommand('paste');
|
||||
@ -102,6 +103,7 @@ class ClipboardReader {
|
||||
target.value = '';
|
||||
return (typeof result === 'string' ? result : '');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the first image in the clipboard.
|
||||
@ -143,23 +145,12 @@ class ClipboardReader {
|
||||
throw new Error('Clipboard reading not supported in this context');
|
||||
}
|
||||
|
||||
let target = this._imagePasteTarget;
|
||||
if (target === null) {
|
||||
target = document.querySelector(this._imagePasteTargetSelector);
|
||||
if (target === null) {
|
||||
throw new Error('Clipboard paste target does not exist');
|
||||
}
|
||||
this._imagePasteTarget = target;
|
||||
}
|
||||
|
||||
const target = this._getRichContentPasteTarget();
|
||||
target.focus();
|
||||
document.execCommand('paste');
|
||||
const image = target.querySelector('img[src^="data:"]');
|
||||
const result = (image !== null ? image.getAttribute('src') : null);
|
||||
for (const image2 of target.querySelectorAll('img')) {
|
||||
image2.removeAttribute('src');
|
||||
}
|
||||
target.textContent = '';
|
||||
this._clearRichContent(target);
|
||||
return result;
|
||||
}
|
||||
|
||||
@ -177,4 +168,28 @@ class ClipboardReader {
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
}
|
||||
|
||||
_getPasteTarget() {
|
||||
if (this._pasteTarget === null) { this._pasteTarget = this._findPasteTarget(this._pasteTargetSelector); }
|
||||
return this._pasteTarget;
|
||||
}
|
||||
|
||||
_getRichContentPasteTarget() {
|
||||
if (this._richContentPasteTarget === null) { this._richContentPasteTarget = this._findPasteTarget(this._richContentPasteTargetSelector); }
|
||||
return this._richContentPasteTarget;
|
||||
}
|
||||
|
||||
_findPasteTarget(selector) {
|
||||
const target = this._document.querySelector(selector);
|
||||
if (target === null) { throw new Error('Clipboard paste target does not exist'); }
|
||||
return target;
|
||||
}
|
||||
|
||||
_clearRichContent(element) {
|
||||
for (const image of element.querySelectorAll('img')) {
|
||||
image.removeAttribute('src');
|
||||
image.removeAttribute('srcset');
|
||||
}
|
||||
element.textContent = '';
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"manifest_version": 2,
|
||||
"name": "Yomichan",
|
||||
"version": "22.9.9.2",
|
||||
"version": "22.10.23.0",
|
||||
"description": "Japanese dictionary with Anki integration",
|
||||
"author": "Alex Yatskov",
|
||||
"icons": {
|
||||
|
Loading…
Reference in New Issue
Block a user