summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlex Yatskov <alex@foosoft.net>2019-10-20 11:23:20 -0700
committerAlex Yatskov <alex@foosoft.net>2019-10-20 11:23:20 -0700
commit438498435227cfa59cf9ed3430045b288cd2a7c0 (patch)
tree6a05520e5d6fa8d26d372673a9ed3e5d2da7e3fd
parent06d7713189be9eb51669d3842b78278371e6cfa4 (diff)
parentd32fd1381b6cd5141a21c22f9ef639b2fe9774fb (diff)
Merge branch 'master' into testing
-rw-r--r--README.md4
-rw-r--r--ext/bg/context.html97
-rw-r--r--ext/bg/css/settings.css40
-rw-r--r--ext/bg/js/api.js174
-rw-r--r--ext/bg/js/audio.js38
-rw-r--r--ext/bg/js/backend.js11
-rw-r--r--ext/bg/js/context.js47
-rw-r--r--ext/bg/js/database.js167
-rw-r--r--ext/bg/js/dictionary.js4
-rw-r--r--ext/bg/js/handlebars.js5
-rw-r--r--ext/bg/js/options.js10
-rw-r--r--ext/bg/js/search-frontend.js5
-rw-r--r--ext/bg/js/search.js71
-rw-r--r--ext/bg/js/settings-popup-preview.js183
-rw-r--r--ext/bg/js/settings.js233
-rw-r--r--ext/bg/js/templates.js223
-rw-r--r--ext/bg/js/translator.js306
-rw-r--r--ext/bg/js/util.js2
-rw-r--r--ext/bg/lang/deinflect.json6
-rw-r--r--ext/bg/search.html4
-rw-r--r--ext/bg/settings-popup-preview.html125
-rw-r--r--ext/bg/settings.html104
-rw-r--r--ext/fg/css/client.css17
-rw-r--r--ext/fg/float.html12
-rw-r--r--ext/fg/js/api.js12
-rw-r--r--ext/fg/js/document.js10
-rw-r--r--ext/fg/js/float.js51
-rw-r--r--ext/fg/js/frontend-initialize.js20
-rw-r--r--ext/fg/js/frontend.js61
-rw-r--r--ext/fg/js/popup-nested.js5
-rw-r--r--ext/fg/js/popup-proxy-host.js44
-rw-r--r--ext/fg/js/popup-proxy.js25
-rw-r--r--ext/fg/js/popup.js144
-rw-r--r--ext/fg/js/source.js26
-rw-r--r--ext/manifest.json8
-rw-r--r--ext/mixed/css/display-dark.css50
-rw-r--r--ext/mixed/css/display-default.css50
-rw-r--r--ext/mixed/css/display.css96
-rw-r--r--ext/mixed/js/audio.js128
-rw-r--r--ext/mixed/js/display.js200
-rw-r--r--tmpl/terms.html50
41 files changed, 2159 insertions, 709 deletions
diff --git a/README.md b/README.md
index b05a050e..01c890cb 100644
--- a/README.md
+++ b/README.md
@@ -162,10 +162,10 @@ Flashcard fields can be configured with the following steps:
`{furigana-plain}` | Term expressed as Kanji with Furigana displayed next to it in brackets (e.g. 日本語[にほんご]).
`{glossary}` | List of definitions for the term (output format depends on whether running in *grouped* mode).
`{reading}` | Kana reading for the term (empty for terms where the expression is the reading).
+ `{screenshot}` | Screenshot of the web page taken at the time the term was added.
`{sentence}` | Sentence, quote, or phrase in which the term appears in the source content.
`{tags}` | Grammar and usage tags providing information about the term (unavailable in *grouped* mode).
`{url}` | Address of the web page in which the term appeared in.
- `{screenshot}` | Screenshot of the web page taken at the time the term was added.
#### Markers for Kanji Cards ####
@@ -179,9 +179,9 @@ Flashcard fields can be configured with the following steps:
`{glossary}` | List of definitions for the Kanji.
`{kunyomi}` | Kunyomi (Japanese reading) for the Kanji expressed as Katakana.
`{onyomi}` | Onyomi (Chinese reading) for the Kanji expressed as Hiragana.
+ `{screenshot}` | Screenshot of the web page taken at the time the Kanji was added.
`{sentence}` | Sentence, quote, or phrase in which the character appears in the source content.
`{url}` | Address of the web page in which the Kanji appeared in.
- `{screenshot}` | Screenshot of the web page taken at the time the Kanji was added.
When creating your model for Yomichan, *please make sure that you pick a unique field to be first*; fields that will
contain `{expression}` or `{character}` are ideal candidates for this. Anki does not require duplicate flashcards to be
diff --git a/ext/bg/context.html b/ext/bg/context.html
index 51346838..48fa463f 100644
--- a/ext/bg/context.html
+++ b/ext/bg/context.html
@@ -9,24 +9,107 @@
<style type="text/css">
body {
padding: 10px;
- text-align: center;
}
+ h3 {
+ margin-top: 10px;
+ }
+ label {
+ font-weight: normal;
+ }
+
+ #mini {
+ text-align: center;
+ }
+ #full {
+ display: none;
+ }
.btn-group {
display: flex;
+ justify-content: center;
margin-top: 10px;
white-space: nowrap;
}
+
+ html:root[data-mode=full] #mini {
+ display: none;
+ }
+ html:root[data-mode=full] #full {
+ display: initial;
+ }
+
+ .link-group {
+ display: block;
+ line-height: 1.5em;
+ margin: 0 -10px;
+ padding: 0.5em 10px;
+ cursor: pointer;
+ color: #333;
+ text-decoration: none;
+ background-color: transparent;
+ transition: background-color 0.125s linear 0s;
+ max-width: none;
+ }
+ .link-group:hover,
+ .link-group:active {
+ color: #333;
+ text-decoration: none;
+ }
+ .link-group:hover>.link-group-label,
+ .link-group:active>.link-group-label {
+ text-decoration: underline;
+ }
+ .link-group:hover {
+ background-color: rgba(0, 0, 0, 0.05);
+ }
+ .link-group:active {
+ background-color: rgba(0, 0, 0, 0.1);
+ }
+ .link-group-icon {
+ width: 16px;
+ height: 16px;
+ text-align: center;
+ vertical-align: middle;
+ display: inline-block;
+ margin-right: 0.25em;
+ }
+ .link-group-icon>input {
+ margin: 0;
+ padding: 0;
+ }
+ .link-group-icon>.glyphicon {
+ top: 0;
+ }
+ .link-group-label {
+ vertical-align: middle;
+ }
</style>
</head>
<body>
- <div>
- <input type="checkbox" id="enable-search">
+ <div id="mini">
+ <div>
+ <input type="checkbox" id="enable-search">
+ </div>
+ <div class="btn-group">
+ <a title="Search (Alt + Insert)&#10;(Middle click to open in new tab)" class="btn btn-default btn-xs action-open-search"><span class="glyphicon glyphicon-search"></span></a>
+ <a title="Options&#10;(Middle click to open in new tab)" class="btn btn-default btn-xs action-open-options"><span class="glyphicon glyphicon-wrench"></span></a>
+ <a title="Help" class="btn btn-default btn-xs action-open-help"><span class="glyphicon glyphicon-question-sign"></span></a>
+ </div>
</div>
- <div class="btn-group">
- <button type="button" id="open-search" title="Search (Alt + Insert)" class="btn btn-default btn-xs"><span class="glyphicon glyphicon-search"></span></button>
- <button type="button" id="open-options" title="Options" class="btn btn-default btn-xs"><span class="glyphicon glyphicon-wrench"></span></button>
- <button type="button" id="open-help" title="Help" class="btn btn-default btn-xs"><span class="glyphicon glyphicon-question-sign"></span></button>
+ <div id="full">
+ <h3 id="extension-info">Yomichan</h3>
+ <label class="link-group">
+ <span class="link-group-icon"><input type="checkbox" id="enable-search2" /></span><span class="link-group-label">Enable content scanning</span>
+ </label>
+ <a class="link-group action-open-options">
+ <span class="link-group-icon"><span class="glyphicon glyphicon-chevron-right"></span></span><span class="link-group-label">Options</span>
+ </a>
+ <a class="link-group action-open-search">
+ <span class="link-group-icon"><span class="glyphicon glyphicon-chevron-right"></span></span><span class="link-group-label">Search</span>
+ </a>
+ <a class="link-group action-open-help">
+ <span class="link-group-icon"><span class="glyphicon glyphicon-chevron-right"></span></span><span class="link-group-label">Help</span>
+ </a>
</div>
<script src="/mixed/lib/jquery.min.js"></script>
diff --git a/ext/bg/css/settings.css b/ext/bg/css/settings.css
index 6284058a..b3d5b884 100644
--- a/ext/bg/css/settings.css
+++ b/ext/bg/css/settings.css
@@ -128,7 +128,8 @@
content: counter(audio-source-id);
}
-#custom-popup-css {
+#custom-popup-css,
+#custom-popup-outer-css {
width: 100%;
min-height: 34px;
height: 96px;
@@ -137,14 +138,41 @@
white-space: pre;
}
-[data-show-for-browser] {
+.btn-inner-middle {
+ vertical-align: middle;
+}
+.storage-button-inner {
+ pointer-events: none;
+}
+input[type=checkbox].storage-button-checkbox {
+ margin: 0 0.375em 0 0;
+ padding: 0;
+}
+
+#settings-popup-preview-frame {
+ background-color: transparent;
+ border: none;
+ margin: 0;
+ padding: 0;
+ width: 100%;
+ height: 320px;
+}
+
+[data-show-for-browser],
+[data-show-for-operating-system] {
display: none;
}
-[data-browser=edge] [data-show-for-browser~=edge],
-[data-browser=chrome] [data-show-for-browser~=chrome],
-[data-browser=firefox] [data-show-for-browser~=firefox],
-[data-browser=firefox-mobile] [data-show-for-browser~=firefox-mobile] {
+html:root[data-browser=edge] [data-show-for-browser~=edge],
+html:root[data-browser=chrome] [data-show-for-browser~=chrome],
+html:root[data-browser=firefox] [data-show-for-browser~=firefox],
+html:root[data-browser=firefox-mobile] [data-show-for-browser~=firefox-mobile],
+html:root[data-operating-system=mac] [data-show-for-operating-system~=mac],
+html:root[data-operating-system=win] [data-show-for-operating-system~=win],
+html:root[data-operating-system=android] [data-show-for-operating-system~=android],
+html:root[data-operating-system=cros] [data-show-for-operating-system~=cros],
+html:root[data-operating-system=linux] [data-show-for-operating-system~=linux],
+html:root[data-operating-system=openbsd] [data-show-for-operating-system~=openbsd] {
display: initial;
}
diff --git a/ext/bg/js/api.js b/ext/bg/js/api.js
index f768e6f9..93d9c155 100644
--- a/ext/bg/js/api.js
+++ b/ext/bg/js/api.js
@@ -144,24 +144,46 @@ async function apiTemplateRender(template, data, dynamic) {
}
}
-async function apiCommandExec(command) {
+async function apiCommandExec(command, params) {
const handlers = apiCommandExec.handlers;
if (handlers.hasOwnProperty(command)) {
const handler = handlers[command];
- handler();
+ handler(params);
}
}
apiCommandExec.handlers = {
- search: () => {
- chrome.tabs.create({url: chrome.extension.getURL('/bg/search.html')});
+ search: async (params) => {
+ const url = chrome.extension.getURL('/bg/search.html');
+ if (!(params && params.newTab)) {
+ try {
+ const tab = await apiFindTab(1000, (url2) => (
+ url2 !== null &&
+ url2.startsWith(url) &&
+ (url2.length === url.length || url2[url.length] === '?' || url2[url.length] === '#')
+ ));
+ if (tab !== null) {
+ await apiFocusTab(tab);
+ return;
+ }
+ } catch (e) {
+ // NOP
+ }
+ }
+ chrome.tabs.create({url});
},
help: () => {
chrome.tabs.create({url: 'https://foosoft.net/projects/yomichan/'});
},
- options: () => {
- chrome.runtime.openOptionsPage();
+ options: (params) => {
+ if (!(params && params.newTab)) {
+ chrome.runtime.openOptionsPage();
+ } else {
+ const manifest = chrome.runtime.getManifest();
+ const url = chrome.extension.getURL(manifest.options_ui.page);
+ chrome.tabs.create({url});
+ }
},
toggle: async () => {
@@ -176,7 +198,7 @@ apiCommandExec.handlers = {
};
async function apiAudioGetUrl(definition, source, optionsContext) {
- return audioBuildUrl(definition, source, optionsContext);
+ return audioGetUrl(definition, source, optionsContext);
}
async function apiInjectScreenshot(definition, fields, screenshot) {
@@ -241,3 +263,141 @@ function apiFrameInformationGet(sender) {
const frameId = sender.frameId;
return Promise.resolve({frameId});
}
+
+function apiInjectStylesheet(css, sender) {
+ if (!sender.tab) {
+ return Promise.reject(new Error('Invalid tab'));
+ }
+
+ const tabId = sender.tab.id;
+ const frameId = sender.frameId;
+ const details = {
+ code: css,
+ runAt: 'document_start',
+ cssOrigin: 'user',
+ allFrames: false
+ };
+ if (typeof frameId === 'number') {
+ details.frameId = frameId;
+ }
+
+ return new Promise((resolve, reject) => {
+ chrome.tabs.insertCSS(tabId, details, () => {
+ const e = chrome.runtime.lastError;
+ if (e) {
+ reject(new Error(e.message));
+ } else {
+ resolve();
+ }
+ });
+ });
+}
+
+async function apiGetEnvironmentInfo() {
+ const browser = await apiGetBrowser();
+ const platform = await new Promise((resolve) => chrome.runtime.getPlatformInfo(resolve));
+ return {
+ browser,
+ platform: {
+ os: platform.os
+ }
+ };
+}
+
+async function apiGetBrowser() {
+ if (EXTENSION_IS_BROWSER_EDGE) {
+ return 'edge';
+ }
+ if (typeof browser !== 'undefined') {
+ try {
+ const info = await browser.runtime.getBrowserInfo();
+ if (info.name === 'Fennec') {
+ return 'firefox-mobile';
+ }
+ } catch (e) { }
+ return 'firefox';
+ } else {
+ return 'chrome';
+ }
+}
+
+function apiGetTabUrl(tab) {
+ return new Promise((resolve) => {
+ chrome.tabs.sendMessage(tab.id, {action: 'getUrl'}, {frameId: 0}, (response) => {
+ let url = null;
+ if (!chrome.runtime.lastError) {
+ url = (response !== null && typeof response === 'object' && !Array.isArray(response) ? response.url : null);
+ if (url !== null && typeof url !== 'string') {
+ url = null;
+ }
+ }
+ resolve({tab, url});
+ });
+ });
+}
+
+async function apiFindTab(timeout, checkUrl) {
+ // This function works around the need to have the "tabs" permission to access tab.url.
+ const tabs = await new Promise((resolve) => chrome.tabs.query({}, resolve));
+ let matchPromiseResolve = null;
+ const matchPromise = new Promise((resolve) => { matchPromiseResolve = resolve; });
+
+ const checkTabUrl = ({tab, url}) => {
+ if (checkUrl(url, tab)) {
+ matchPromiseResolve(tab);
+ }
+ };
+
+ const promises = [];
+ for (const tab of tabs) {
+ const promise = apiGetTabUrl(tab);
+ promise.then(checkTabUrl);
+ promises.push(promise);
+ }
+
+ const racePromises = [
+ matchPromise,
+ Promise.all(promises).then(() => null)
+ ];
+ if (typeof timeout === 'number') {
+ racePromises.push(new Promise((resolve) => setTimeout(() => resolve(null), timeout)));
+ }
+
+ return await Promise.race(racePromises);
+}
+
+async function apiFocusTab(tab) {
+ await new Promise((resolve, reject) => {
+ chrome.tabs.update(tab.id, {active: true}, () => {
+ const e = chrome.runtime.lastError;
+ if (e) { reject(e); }
+ else { resolve(); }
+ });
+ });
+
+ if (!(typeof chrome.windows === 'object' && chrome.windows !== null)) {
+ // Windows not supported (e.g. on Firefox mobile)
+ return;
+ }
+
+ try {
+ const tabWindow = await new Promise((resolve) => {
+ chrome.windows.get(tab.windowId, {}, (tabWindow) => {
+ const e = chrome.runtime.lastError;
+ if (e) { reject(e); }
+ else { resolve(tabWindow); }
+ });
+ });
+ if (!tabWindow.focused) {
+ await new Promise((resolve, reject) => {
+ chrome.windows.update(tab.windowId, {focused: true}, () => {
+ const e = chrome.runtime.lastError;
+ if (e) { reject(e); }
+ else { resolve(); }
+ });
+ });
+ }
+ } catch (e) {
+ // Edge throws exception for no reason here.
+ }
+}
diff --git a/ext/bg/js/audio.js b/ext/bg/js/audio.js
index 9e0ae67c..3efcce46 100644
--- a/ext/bg/js/audio.js
+++ b/ext/bg/js/audio.js
@@ -86,6 +86,24 @@ const audioUrlBuilders = {
throw new Error('Failed to find audio URL');
},
+ 'text-to-speech': async (definition, optionsContext) => {
+ const options = await apiOptionsGet(optionsContext);
+ const voiceURI = options.audio.textToSpeechVoice;
+ if (!voiceURI) {
+ throw new Error('No voice');
+ }
+
+ return `tts:?text=${encodeURIComponent(definition.expression)}&voice=${encodeURIComponent(voiceURI)}`;
+ },
+ 'text-to-speech-reading': async (definition, optionsContext) => {
+ const options = await apiOptionsGet(optionsContext);
+ const voiceURI = options.audio.textToSpeechVoice;
+ if (!voiceURI) {
+ throw new Error('No voice');
+ }
+
+ return `tts:?text=${encodeURIComponent(definition.reading || definition.expression)}&voice=${encodeURIComponent(voiceURI)}`;
+ },
'custom': async (definition, optionsContext) => {
const options = await apiOptionsGet(optionsContext);
const customSourceUrl = options.audio.customSourceUrl;
@@ -93,20 +111,14 @@ const audioUrlBuilders = {
}
};
-async function audioBuildUrl(definition, mode, optionsContext, cache={}) {
- const cacheKey = `${mode}:${definition.expression}`;
- if (cache.hasOwnProperty(cacheKey)) {
- return Promise.resolve(cache[cacheKey]);
- }
-
+async function audioGetUrl(definition, mode, optionsContext, download) {
if (audioUrlBuilders.hasOwnProperty(mode)) {
const handler = audioUrlBuilders[mode];
- return handler(definition, optionsContext).then(
- (url) => {
- cache[cacheKey] = url;
- return url;
- },
- () => null);
+ try {
+ return await handler(definition, optionsContext, download);
+ } catch (e) {
+ // NOP
+ }
}
return null;
}
@@ -163,7 +175,7 @@ async function audioInject(definition, fields, sources, optionsContext) {
audioSourceDefinition = definition.expressions[0];
}
- const {url} = await audioGetFromSources(audioSourceDefinition, sources, optionsContext, false);
+ const {url} = await audioGetFromSources(audioSourceDefinition, sources, optionsContext, true);
if (url !== null) {
const filename = audioBuildFilename(audioSourceDefinition);
if (filename !== null) {
diff --git a/ext/bg/js/backend.js b/ext/bg/js/backend.js
index 453f4282..f29230a2 100644
--- a/ext/bg/js/backend.js
+++ b/ext/bg/js/backend.js
@@ -73,9 +73,10 @@ class Backend {
if (handlers.hasOwnProperty(action)) {
const handler = handlers[action];
const promise = handler(params, sender);
- promise
- .then(result => callback({result}))
- .catch(error => callback(errorToJson(error)));
+ promise.then(
+ result => callback({result}),
+ error => callback({error: errorToJson(error)})
+ );
}
return true;
@@ -180,11 +181,13 @@ Backend.messageHandlers = {
definitionsAddable: ({definitions, modes, optionsContext}) => apiDefinitionsAddable(definitions, modes, optionsContext),
noteView: ({noteId}) => apiNoteView(noteId),
templateRender: ({template, data, dynamic}) => apiTemplateRender(template, data, dynamic),
- commandExec: ({command}) => apiCommandExec(command),
+ commandExec: ({command, params}) => apiCommandExec(command, params),
audioGetUrl: ({definition, source, optionsContext}) => apiAudioGetUrl(definition, source, optionsContext),
screenshotGet: ({options}, sender) => apiScreenshotGet(options, sender),
forward: ({action, params}, sender) => apiForward(action, params, sender),
frameInformationGet: (params, sender) => apiFrameInformationGet(sender),
+ injectStylesheet: ({css}, sender) => apiInjectStylesheet(css, sender),
+ getEnvironmentInfo: () => apiGetEnvironmentInfo()
};
window.yomichan_backend = new Backend();
diff --git a/ext/bg/js/context.js b/ext/bg/js/context.js
index 0f88e9c0..8e1dbce6 100644
--- a/ext/bg/js/context.js
+++ b/ext/bg/js/context.js
@@ -17,10 +17,47 @@
*/
+function showExtensionInfo() {
+ const node = document.getElementById('extension-info');
+ if (node === null) { return; }
+
+ const manifest = chrome.runtime.getManifest();
+ node.textContent = `${manifest.name} v${manifest.version}`;
+}
+
+function setupButtonEvents(selector, command, url) {
+ const node = $(selector);
+ node.on('click', (e) => {
+ if (e.button !== 0) { return; }
+ apiCommandExec(command, {newTab: e.ctrlKey});
+ e.preventDefault();
+ })
+ .on('auxclick', (e) => {
+ if (e.button !== 1) { return; }
+ apiCommandExec(command, {newTab: true});
+ e.preventDefault();
+ });
+
+ if (typeof url === 'string') {
+ node.attr('href', url);
+ node.attr('target', '_blank');
+ node.attr('rel', 'noopener');
+ }
+}
+
$(document).ready(utilAsync(() => {
- $('#open-search').click(() => apiCommandExec('search'));
- $('#open-options').click(() => apiCommandExec('options'));
- $('#open-help').click(() => apiCommandExec('help'));
+ showExtensionInfo();
+
+ apiGetEnvironmentInfo().then(({browser}) => {
+ // Firefox mobile opens this page as a full webpage.
+ document.documentElement.dataset.mode = (browser === 'firefox-mobile' ? 'full' : 'mini');
+ });
+
+ const manifest = chrome.runtime.getManifest();
+
+ setupButtonEvents('.action-open-search', 'search', chrome.extension.getURL('/bg/search.html'));
+ setupButtonEvents('.action-open-options', 'options', chrome.extension.getURL(manifest.options_ui.page));
+ setupButtonEvents('.action-open-help', 'help');
const optionsContext = {
depth: 0,
@@ -31,5 +68,9 @@ $(document).ready(utilAsync(() => {
toggle.prop('checked', options.general.enable).change();
toggle.bootstrapToggle();
toggle.change(() => apiCommandExec('toggle'));
+
+ const toggle2 = $('#enable-search2');
+ toggle2.prop('checked', options.general.enable).change();
+ toggle2.change(() => apiCommandExec('toggle'));
});
}));
diff --git a/ext/bg/js/database.js b/ext/bg/js/database.js
index 771a71c9..9f477b24 100644
--- a/ext/bg/js/database.js
+++ b/ext/bg/js/database.js
@@ -20,7 +20,6 @@
class Database {
constructor() {
this.db = null;
- this.tagCache = {};
}
async prepare() {
@@ -53,33 +52,20 @@ class Database {
this.db.close();
await this.db.delete();
this.db = null;
- this.tagCache = {};
await this.prepare();
}
- async findTerms(term, titles) {
+ async findTermsBulk(termList, titles) {
this.validate();
- const results = [];
- await this.db.terms.where('expression').equals(term).or('reading').equals(term).each(row => {
- if (titles.includes(row.dictionary)) {
- results.push(Database.createTerm(row));
- }
- });
-
- return results;
- }
-
- async findTermsBulk(terms, titles) {
const promises = [];
const visited = {};
const results = [];
- const createResult = Database.createTerm;
const processRow = (row, index) => {
if (titles.includes(row.dictionary) && !visited.hasOwnProperty(row.id)) {
visited[row.id] = true;
- results.push(createResult(row, index));
+ results.push(Database.createTerm(row, index));
}
};
@@ -89,8 +75,8 @@ class Database {
const dbIndex1 = dbTerms.index('expression');
const dbIndex2 = dbTerms.index('reading');
- for (let i = 0; i < terms.length; ++i) {
- const only = IDBKeyRange.only(terms[i]);
+ for (let i = 0; i < termList.length; ++i) {
+ const only = IDBKeyRange.only(termList[i]);
promises.push(
Database.getAll(dbIndex1, only, i, processRow),
Database.getAll(dbIndex2, only, i, processRow)
@@ -102,66 +88,50 @@ class Database {
return results;
}
- async findTermsExact(term, reading, titles) {
+ async findTermsExactBulk(termList, readingList, titles) {
this.validate();
+ const promises = [];
const results = [];
- await this.db.terms.where('expression').equals(term).each(row => {
- if (row.reading === reading && titles.includes(row.dictionary)) {
- results.push(Database.createTerm(row));
+ const processRow = (row, index) => {
+ if (row.reading === readingList[index] && titles.includes(row.dictionary)) {
+ results.push(Database.createTerm(row, index));
}
- });
+ };
- return results;
- }
+ const db = this.db.backendDB();
+ const dbTransaction = db.transaction(['terms'], 'readonly');
+ const dbTerms = dbTransaction.objectStore('terms');
+ const dbIndex = dbTerms.index('expression');
- async findTermsBySequence(sequence, mainDictionary) {
- this.validate();
+ for (let i = 0; i < termList.length; ++i) {
+ const only = IDBKeyRange.only(termList[i]);
+ promises.push(Database.getAll(dbIndex, only, i, processRow));
+ }
- const results = [];
- await this.db.terms.where('sequence').equals(sequence).each(row => {
- if (row.dictionary === mainDictionary) {
- results.push(Database.createTerm(row));
- }
- });
+ await Promise.all(promises);
return results;
}
- async findTermMeta(term, titles) {
+ async findTermsBySequenceBulk(sequenceList, mainDictionary) {
this.validate();
- const results = [];
- await this.db.termMeta.where('expression').equals(term).each(row => {
- if (titles.includes(row.dictionary)) {
- results.push({
- mode: row.mode,
- data: row.data,
- dictionary: row.dictionary
- });
- }
- });
-
- return results;
- }
-
- async findTermMetaBulk(terms, titles) {
const promises = [];
const results = [];
- const createResult = Database.createTermMeta;
const processRow = (row, index) => {
- if (titles.includes(row.dictionary)) {
- results.push(createResult(row, index));
+ if (row.dictionary === mainDictionary) {
+ results.push(Database.createTerm(row, index));
}
};
const db = this.db.backendDB();
- const dbTransaction = db.transaction(['termMeta'], 'readonly');
- const dbTerms = dbTransaction.objectStore('termMeta');
- const dbIndex = dbTerms.index('expression');
+ const dbTransaction = db.transaction(['terms'], 'readonly');
+ const dbTerms = dbTransaction.objectStore('terms');
+ const dbIndex = dbTerms.index('sequence');
- for (let i = 0; i < terms.length; ++i) {
- const only = IDBKeyRange.only(terms[i]);
+ for (let i = 0; i < sequenceList.length; ++i) {
+ const only = IDBKeyRange.only(sequenceList[i]);
promises.push(Database.getAll(dbIndex, only, i, processRow));
}
@@ -170,67 +140,59 @@ class Database {
return results;
}
- async findKanji(kanji, titles) {
- this.validate();
+ async findTermMetaBulk(termList, titles) {
+ return this.findGenericBulk('termMeta', 'expression', termList, titles, Database.createMeta);
+ }
- const results = [];
- await this.db.kanji.where('character').equals(kanji).each(row => {
- if (titles.includes(row.dictionary)) {
- results.push({
- character: row.character,
- onyomi: dictFieldSplit(row.onyomi),
- kunyomi: dictFieldSplit(row.kunyomi),
- tags: dictFieldSplit(row.tags),
- glossary: row.meanings,
- stats: row.stats,
- dictionary: row.dictionary
- });
- }
- });
+ async findKanjiBulk(kanjiList, titles) {
+ return this.findGenericBulk('kanji', 'character', kanjiList, titles, Database.createKanji);
+ }
- return results;
+ async findKanjiMetaBulk(kanjiList, titles) {
+ return this.findGenericBulk('kanjiMeta', 'character', kanjiList, titles, Database.createMeta);
}
- async findKanjiMeta(kanji, titles) {
+ async findGenericBulk(tableName, indexName, indexValueList, titles, createResult) {
this.validate();
+ const promises = [];
const results = [];
- await this.db.kanjiMeta.where('character').equals(kanji).each(row => {
+ const processRow = (row, index) => {
if (titles.includes(row.dictionary)) {
- results.push({
- mode: row.mode,
- data: row.data,
- dictionary: row.dictionary
- });
+ results.push(createResult(row, index));
}
- });
+ };
- return results;
- }
+ const db = this.db.backendDB();
+ const dbTransaction = db.transaction([tableName], 'readonly');
+ const dbTerms = dbTransaction.objectStore(tableName);
+ const dbIndex = dbTerms.index(indexName);
- findTagForTitleCached(name, title) {
- if (this.tagCache.hasOwnProperty(title)) {
- const cache = this.tagCache[title];
- if (cache.hasOwnProperty(name)) {
- return cache[name];
- }
+ for (let i = 0; i < indexValueList.length; ++i) {
+ const only = IDBKeyRange.only(indexValueList[i]);
+ promises.push(Database.getAll(dbIndex, only, i, processRow));
}
+
+ await Promise.all(promises);
+
+ return results;
}
async findTagForTitle(name, title) {
this.validate();
- const cache = (this.tagCache.hasOwnProperty(title) ? this.tagCache[title] : (this.tagCache[title] = {}));
-
let result = null;
- await this.db.tagMeta.where('name').equals(name).each(row => {
+ const db = this.db.backendDB();
+ const dbTransaction = db.transaction(['tagMeta'], 'readonly');
+ const dbTerms = dbTransaction.objectStore('tagMeta');
+ const dbIndex = dbTerms.index('name');
+ const only = IDBKeyRange.only(name);
+ await Database.getAll(dbIndex, only, null, row => {
if (title === row.dictionary) {
result = row;
}
});
- cache[name] = result;
-
return result;
}
@@ -522,7 +484,20 @@ class Database {
};
}
- static createTermMeta(row, index) {
+ static createKanji(row, index) {
+ return {
+ index,
+ character: row.character,
+ onyomi: dictFieldSplit(row.onyomi),
+ kunyomi: dictFieldSplit(row.kunyomi),
+ tags: dictFieldSplit(row.tags),
+ glossary: row.meanings,
+ stats: row.stats,
+ dictionary: row.dictionary
+ };
+ }
+
+ static createMeta(row, index) {
return {
index,
mode: row.mode,
diff --git a/ext/bg/js/dictionary.js b/ext/bg/js/dictionary.js
index 498eafcd..191058c1 100644
--- a/ext/bg/js/dictionary.js
+++ b/ext/bg/js/dictionary.js
@@ -342,10 +342,10 @@ async function dictFieldFormat(field, definition, mode, options) {
'kunyomi',
'onyomi',
'reading',
+ 'screenshot',
'sentence',
'tags',
- 'url',
- 'screenshot'
+ 'url'
];
for (const marker of markers) {
diff --git a/ext/bg/js/handlebars.js b/ext/bg/js/handlebars.js
index 66d5fa2b..92764a20 100644
--- a/ext/bg/js/handlebars.js
+++ b/ext/bg/js/handlebars.js
@@ -75,6 +75,10 @@ function handlebarsMultiLine(options) {
return options.fn(this).split('\n').join('<br>');
}
+function handlebarsSanitizeCssClass(options) {
+ return options.fn(this).replace(/[^_a-z0-9\u00a0-\uffff]/ig, '_');
+}
+
function handlebarsRegisterHelpers() {
if (Handlebars.partials !== Handlebars.templates) {
Handlebars.partials = Handlebars.templates;
@@ -83,6 +87,7 @@ function handlebarsRegisterHelpers() {
Handlebars.registerHelper('furiganaPlain', handlebarsFuriganaPlain);
Handlebars.registerHelper('kanjiLinks', handlebarsKanjiLinks);
Handlebars.registerHelper('multiLine', handlebarsMultiLine);
+ Handlebars.registerHelper('sanitizeCssClass', handlebarsSanitizeCssClass);
}
}
diff --git a/ext/bg/js/options.js b/ext/bg/js/options.js
index d0aa6fd3..4854cd65 100644
--- a/ext/bg/js/options.js
+++ b/ext/bg/js/options.js
@@ -276,15 +276,19 @@ function profileOptionsCreateDefaults() {
compactTags: false,
compactGlossaries: false,
mainDictionary: '',
- customPopupCss: ''
+ popupTheme: 'default',
+ popupOuterTheme: 'default',
+ customPopupCss: '',
+ customPopupOuterCss: ''
},
audio: {
enabled: true,
- sources: ['jpod101', 'jpod101-alternate', 'jisho', 'custom'],
+ sources: ['jpod101'],
volume: 100,
autoPlay: false,
- customSourceUrl: ''
+ customSourceUrl: '',
+ textToSpeechVoice: ''
},
scanning: {
diff --git a/ext/bg/js/search-frontend.js b/ext/bg/js/search-frontend.js
index 0c1a61ea..b21dac17 100644
--- a/ext/bg/js/search-frontend.js
+++ b/ext/bg/js/search-frontend.js
@@ -25,11 +25,14 @@ async function searchFrontendSetup() {
const options = await apiOptionsGet(optionsContext);
if (!options.scanning.enableOnSearchPage) { return; }
+ window.frontendInitializationData = {depth: 1, proxy: false};
+
const scriptSrcs = [
'/fg/js/frontend-api-receiver.js',
'/fg/js/popup.js',
'/fg/js/popup-proxy-host.js',
- '/fg/js/frontend.js'
+ '/fg/js/frontend.js',
+ '/fg/js/frontend-initialize.js'
];
for (const src of scriptSrcs) {
const script = document.createElement('script');
diff --git a/ext/bg/js/search.js b/ext/bg/js/search.js
index ead9ba6f..431478c9 100644
--- a/ext/bg/js/search.js
+++ b/ext/bg/js/search.js
@@ -31,25 +31,37 @@ class DisplaySearch extends Display {
this.intro = document.querySelector('#intro');
this.introVisible = true;
this.introAnimationTimer = null;
+ }
- this.dependencies = Object.assign({}, this.dependencies, {docRangeFromPoint, docSentenceExtract});
+ static create() {
+ const instance = new DisplaySearch();
+ instance.prepare();
+ return instance;
+ }
- if (this.search !== null) {
- this.search.addEventListener('click', (e) => this.onSearch(e), false);
- }
- if (this.query !== null) {
- this.query.addEventListener('input', () => this.onSearchInput(), false);
+ async prepare() {
+ try {
+ await this.initialize();
- const query = DisplaySearch.getSearchQueryFromLocation(window.location.href);
- if (query !== null) {
- this.query.value = window.wanakana.toKana(query);
- this.onSearchQueryUpdated(query, false);
+ if (this.search !== null) {
+ this.search.addEventListener('click', (e) => this.onSearch(e), false);
}
+ if (this.query !== null) {
+ this.query.addEventListener('input', () => this.onSearchInput(), false);
- window.wanakana.bind(this.query);
- }
+ const query = DisplaySearch.getSearchQueryFromLocation(window.location.href);
+ if (query !== null) {
+ this.query.value = window.wanakana.toKana(query);
+ this.onSearchQueryUpdated(query, false);
+ }
- this.updateSearchButton();
+ window.wanakana.bind(this.query);
+ }
+
+ this.updateSearchButton();
+ } catch (e) {
+ this.onError(e);
+ }
}
onError(error) {
@@ -89,7 +101,11 @@ class DisplaySearch extends Display {
this.updateSearchButton();
if (valid) {
const {definitions} = await apiTermsFind(query, this.optionsContext);
- this.termsShow(definitions, await apiOptionsGet(this.optionsContext));
+ this.setContentTerms(definitions, {
+ focus: false,
+ sentence: null,
+ url: window.location.href
+ });
} else {
this.container.textContent = '';
}
@@ -98,6 +114,25 @@ class DisplaySearch extends Display {
}
}
+ onRuntimeMessage({action, params}, sender, callback) {
+ const handlers = DisplaySearch.runtimeMessageHandlers;
+ if (handlers.hasOwnProperty(action)) {
+ const handler = handlers[action];
+ const result = handler(this, params);
+ callback(result);
+ } else {
+ return super.onRuntimeMessage({action, params}, sender, callback);
+ }
+ }
+
+ getOptionsContext() {
+ return this.optionsContext;
+ }
+
+ setCustomCss() {
+ // No custom CSS
+ }
+
setIntroVisible(visible, animate) {
if (this.introVisible === visible) {
return;
@@ -164,4 +199,10 @@ class DisplaySearch extends Display {
}
}
-window.yomichan_search = new DisplaySearch();
+DisplaySearch.runtimeMessageHandlers = {
+ getUrl: () => {
+ return {url: window.location.href};
+ }
+};
+
+window.yomichan_search = DisplaySearch.create();
diff --git a/ext/bg/js/settings-popup-preview.js b/ext/bg/js/settings-popup-preview.js
new file mode 100644
index 00000000..b12fb726
--- /dev/null
+++ b/ext/bg/js/settings-popup-preview.js
@@ -0,0 +1,183 @@
+/*
+ * Copyright (C) 2019 Alex Yatskov <alex@foosoft.net>
+ * Author: Alex Yatskov <alex@foosoft.net>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+
+class SettingsPopupPreview {
+ constructor() {
+ this.frontend = null;
+ this.apiOptionsGetOld = apiOptionsGet;
+ this.popupInjectOuterStylesheetOld = Popup.injectOuterStylesheet;
+ this.popupShown = false;
+ this.themeChangeTimeout = null;
+ }
+
+ static create() {
+ const instance = new SettingsPopupPreview();
+ instance.prepare();
+ return instance;
+ }
+
+ async prepare() {
+ // Setup events
+ window.addEventListener('resize', (e) => this.onWindowResize(e), false);
+ window.addEventListener('message', (e) => this.onMessage(e), false);
+
+ const themeDarkCheckbox = document.querySelector('#theme-dark-checkbox');
+ if (themeDarkCheckbox !== null) {
+ themeDarkCheckbox.addEventListener('change', () => this.onThemeDarkCheckboxChanged(themeDarkCheckbox), false);
+ }
+
+ // Overwrite API functions
+ window.apiOptionsGet = (...args) => this.apiOptionsGet(...args);
+
+ // Overwrite frontend
+ this.frontend = Frontend.create();
+ window.yomichan_frontend = this.frontend;
+
+ this.frontend.setEnabled = function () {};
+ this.frontend.searchClear = function () {};
+
+ this.frontend.popup.childrenSupported = false;
+ this.frontend.popup.interactive = false;
+
+ await this.frontend.isPrepared();
+
+ // Overwrite popup
+ Popup.injectOuterStylesheet = (...args) => this.popupInjectOuterStylesheet(...args);
+
+ // Update search
+ this.updateSearch();
+ }
+
+ async apiOptionsGet(...args) {
+ const options = await this.apiOptionsGetOld(...args);
+ options.general.enable = true;
+ options.general.debugInfo = false;
+ options.general.popupWidth = 400;
+ options.general.popupHeight = 250;
+ options.general.popupHorizontalOffset = 0;
+ options.general.popupVerticalOffset = 10;
+ options.general.popupHorizontalOffset2 = 10;
+ options.general.popupVerticalOffset2 = 0;
+ options.general.popupHorizontalTextPosition = 'below';
+ options.general.popupVerticalTextPosition = 'before';
+ options.scanning.selectText = false;
+ return options;
+ }
+
+ popupInjectOuterStylesheet(...args) {
+ // This simulates the stylesheet priorities when injecting using the web extension API.
+ const result = this.popupInjectOuterStylesheetOld(...args);
+
+ const outerStylesheet = Popup.outerStylesheet;
+ const node = document.querySelector('#client-css');
+ if (node !== null && outerStylesheet !== null) {
+ node.parentNode.insertBefore(outerStylesheet, node);
+ }
+
+ return result;
+ }
+
+ onWindowResize() {
+ if (this.frontend === null) { return; }
+ const textSource = this.frontend.textSourceLast;
+ if (textSource === null) { return; }
+
+ const elementRect = textSource.getRect();
+ const writingMode = textSource.getWritingMode();
+ this.frontend.popup.showContent(elementRect, writingMode);
+ }
+
+ onMessage(e) {
+ const {action, params} = e.data;
+ const handlers = SettingsPopupPreview.messageHandlers;
+ if (handlers.hasOwnProperty(action)) {
+ const handler = handlers[action];
+ handler(this, params);
+ }
+ }
+
+ onThemeDarkCheckboxChanged(node) {
+ document.documentElement.classList.toggle('dark', node.checked);
+ if (this.themeChangeTimeout !== null) {
+ clearTimeout(this.themeChangeTimeout);
+ }
+ this.themeChangeTimeout = setTimeout(() => {
+ this.themeChangeTimeout = null;
+ this.frontend.popup.updateTheme();
+ }, 300);
+ }
+
+ setText(text) {
+ const exampleText = document.querySelector('#example-text');
+ if (exampleText === null) { return; }
+
+ exampleText.textContent = text;
+ this.updateSearch();
+ }
+
+ setInfoVisible(visible) {
+ const node = document.querySelector('.placeholder-info');
+ if (node === null) { return; }
+
+ node.classList.toggle('placeholder-info-visible', visible);
+ }
+
+ setCustomCss(css) {
+ if (this.frontend === null) { return; }
+ this.frontend.popup.setCustomCss(css);
+ }
+
+ setCustomOuterCss(css) {
+ if (this.frontend === null) { return; }
+ this.frontend.popup.setCustomOuterCss(css, true);
+ }
+
+ async updateSearch() {
+ const exampleText = document.querySelector('#example-text');
+ if (exampleText === null) { return; }
+
+ const textNode = exampleText.firstChild;
+ if (textNode === null) { return; }
+
+ const range = document.createRange();
+ range.selectNode(textNode);
+ const source = new TextSourceRange(range, range.toString(), null);
+
+ this.frontend.textSourceLast = null;
+ await this.frontend.searchSource(source, 'script');
+ await this.frontend.lastShowPromise;
+
+ if (this.frontend.popup.isVisible()) {
+ this.popupShown = true;
+ }
+
+ this.setInfoVisible(!this.popupShown);
+ }
+}
+
+SettingsPopupPreview.messageHandlers = {
+ setText: (self, {text}) => self.setText(text),
+ setCustomCss: (self, {css}) => self.setCustomCss(css),
+ setCustomOuterCss: (self, {css}) => self.setCustomOuterCss(css)
+};
+
+SettingsPopupPreview.instance = SettingsPopupPreview.create();
+
+
+
diff --git a/ext/bg/js/settings.js b/ext/bg/js/settings.js
index f3b5ff16..05a0604a 100644
--- a/ext/bg/js/settings.js
+++ b/ext/bg/js/settings.js
@@ -39,12 +39,16 @@ async function formRead(options) {
options.general.popupVerticalOffset = parseInt($('#popup-vertical-offset').val(), 10);
options.general.popupHorizontalOffset2 = parseInt($('#popup-horizontal-offset2').val(), 0);
options.general.popupVerticalOffset2 = parseInt($('#popup-vertical-offset2').val(), 10);
+ options.general.popupTheme = $('#popup-theme').val();
+ options.general.popupOuterTheme = $('#popup-outer-theme').val();
options.general.customPopupCss = $('#custom-popup-css').val();
+ options.general.customPopupOuterCss = $('#custom-popup-outer-css').val();
options.audio.enabled = $('#audio-playback-enabled').prop('checked');
options.audio.autoPlay = $('#auto-play-audio').prop('checked');
options.audio.volume = parseFloat($('#audio-playback-volume').val());
options.audio.customSourceUrl = $('#audio-custom-source').val();
+ options.audio.textToSpeechVoice = $('#text-to-speech-voice').val();
options.scanning.middleMouse = $('#middle-mouse-button-scan').prop('checked');
options.scanning.touchInputEnabled = $('#touch-input-enabled').prop('checked');
@@ -107,12 +111,16 @@ async function formWrite(options) {
$('#popup-vertical-offset').val(options.general.popupVerticalOffset);
$('#popup-horizontal-offset2').val(options.general.popupHorizontalOffset2);
$('#popup-vertical-offset2').val(options.general.popupVerticalOffset2);
+ $('#popup-theme').val(options.general.popupTheme);
+ $('#popup-outer-theme').val(options.general.popupOuterTheme);
$('#custom-popup-css').val(options.general.customPopupCss);
+ $('#custom-popup-outer-css').val(options.general.customPopupOuterCss);
$('#audio-playback-enabled').prop('checked', options.audio.enabled);
$('#auto-play-audio').prop('checked', options.audio.autoPlay);
$('#audio-playback-volume').val(options.audio.volume);
$('#audio-custom-source').val(options.audio.customSourceUrl);
+ $('#text-to-speech-voice').val(options.audio.textToSpeechVoice).attr('data-value', options.audio.textToSpeechVoice);
$('#middle-mouse-button-scan').prop('checked', options.scanning.middleMouse);
$('#touch-input-enabled').prop('checked', options.scanning.touchInputEnabled);
@@ -248,6 +256,7 @@ async function onReady() {
showExtensionInformation();
formSetupEventListeners();
+ appearanceInitialize();
await audioSettingsInitialize();
await profileOptionsSetup();
@@ -260,6 +269,55 @@ $(document).ready(utilAsync(onReady));
/*
+ * Appearance
+ */
+
+function appearanceInitialize() {
+ let previewVisible = false;
+ $('#settings-popup-preview-button').on('click', () => {
+ if (previewVisible) { return; }
+ showAppearancePreview();
+ previewVisible = true;
+ });
+}
+
+function showAppearancePreview() {
+ const container = $('#settings-popup-preview-container');
+ const buttonContainer = $('#settings-popup-preview-button-container');
+ const settings = $('#settings-popup-preview-settings');
+ const text = $('#settings-popup-preview-text');
+ const customCss = $('#custom-popup-css');
+ const customOuterCss = $('#custom-popup-outer-css');
+
+ const frame = document.createElement('iframe');
+ frame.src = '/bg/settings-popup-preview.html';
+ frame.id = 'settings-popup-preview-frame';
+
+ window.wanakana.bind(text[0]);
+
+ text.on('input', () => {
+ const action = 'setText';
+ const params = {text: text.val()};
+ frame.contentWindow.postMessage({action, params}, '*');
+ });
+ customCss.on('input', () => {
+ const action = 'setCustomCss';
+ const params = {css: customCss.val()};
+ frame.contentWindow.postMessage({action, params}, '*');
+ });
+ customOuterCss.on('input', () => {
+ const action = 'setCustomOuterCss';
+ const params = {css: customOuterCss.val()};
+ frame.contentWindow.postMessage({action, params}, '*');
+ });
+
+ container.append(frame);
+ buttonContainer.remove();
+ settings.css('display', '');
+}
+
+
+/*
* Audio
*/
@@ -270,6 +328,81 @@ async function audioSettingsInitialize() {
const options = await apiOptionsGet(optionsContext);
audioSourceUI = new AudioSourceUI.Container(options.audio.sources, $('.audio-source-list'), $('.audio-source-add'));
audioSourceUI.save = () => apiOptionsSave();
+
+ textToSpeechInitialize();
+}
+
+function textToSpeechInitialize() {
+ if (typeof speechSynthesis === 'undefined') { return; }
+
+ speechSynthesis.addEventListener('voiceschanged', () => updateTextToSpeechVoices(), false);
+ updateTextToSpeechVoices();
+
+ $('#text-to-speech-voice-test').on('click', () => textToSpeechTest());
+}
+
+function updateTextToSpeechVoices() {
+ const voices = Array.prototype.map.call(speechSynthesis.getVoices(), (voice, index) => ({voice, index}));
+ voices.sort(textToSpeechVoiceCompare);
+ if (voices.length > 0) {
+ $('#text-to-speech-voice-container').css('display', '');
+ }
+
+ const select = $('#text-to-speech-voice');
+ select.empty();
+ select.append($('<option>').val('').text('None'));
+ for (const {voice} of voices) {
+ select.append($('<option>').val(voice.voiceURI).text(`${voice.name} (${voice.lang})`));
+ }
+
+ select.val(select.attr('data-value'));
+}
+
+function languageTagIsJapanese(languageTag) {
+ return (
+ languageTag.startsWith('ja-') ||
+ languageTag.startsWith('jpn-')
+ );
+}
+
+function textToSpeechVoiceCompare(a, b) {
+ const aIsJapanese = languageTagIsJapanese(a.voice.lang);
+ const bIsJapanese = languageTagIsJapanese(b.voice.lang);
+ if (aIsJapanese) {
+ if (!bIsJapanese) { return -1; }
+ } else {
+ if (bIsJapanese) { return 1; }
+ }
+
+ const aIsDefault = a.voice.default;
+ const bIsDefault = b.voice.default;
+ if (aIsDefault) {
+ if (!bIsDefault) { return -1; }
+ } else {
+ if (bIsDefault) { return 1; }
+ }
+
+ if (a.index < b.index) { return -1; }
+ if (a.index > b.index) { return 1; }
+ return 0;
+}
+
+function textToSpeechTest() {
+ try {
+ const text = $('#text-to-speech-voice-test').attr('data-speech-text') || '';
+ const voiceURI = $('#text-to-speech-voice').val();
+ const voice = audioGetTextToSpeechVoice(voiceURI);
+ if (voice === null) { return; }
+
+ const utterance = new SpeechSynthesisUtterance(text);
+ utterance.lang = 'ja-JP';
+ utterance.voice = voice;
+ utterance.volume = 1.0;
+
+ speechSynthesis.speak(utterance);
+ } catch (e) {
+ // NOP
+ }
}
@@ -297,9 +430,14 @@ async function onOptionsUpdate({source}) {
await formWrite(options);
}
-function onMessage({action, params}) {
- if (action === 'optionsUpdate') {
- onOptionsUpdate(params);
+function onMessage({action, params}, sender, callback) {
+ switch (action) {
+ case 'optionsUpdate':
+ onOptionsUpdate(params);
+ break;
+ case 'getUrl':
+ callback({url: window.location.href});
+ break;
}
}
@@ -607,10 +745,10 @@ async function ankiFieldsPopulate(element, options) {
'glossary',
'glossary-brief',
'reading',
+ 'screenshot',
'sentence',
'tags',
- 'url',
- 'screenshot'
+ 'url'
],
'kanji': [
'character',
@@ -618,6 +756,7 @@ async function ankiFieldsPopulate(element, options) {
'glossary',
'kunyomi',
'onyomi',
+ 'screenshot',
'sentence',
'tags',
'url'
@@ -685,32 +824,15 @@ async function onAnkiFieldTemplatesReset(e) {
* Storage
*/
-async function getBrowser() {
- if (EXTENSION_IS_BROWSER_EDGE) {
- return 'edge';
- }
- if (typeof browser !== 'undefined') {
- try {
- const info = await browser.runtime.getBrowserInfo();
- if (info.name === 'Fennec') {
- return 'firefox-mobile';
- }
- } catch (e) { }
- return 'firefox';
- } else {
- return 'chrome';
- }
-}
-
function storageBytesToLabeledString(size) {
const base = 1000;
- const labels = ['bytes', 'KB', 'MB', 'GB'];
+ const labels = [' bytes', 'KB', 'MB', 'GB'];
let labelIndex = 0;
while (size >= base) {
size /= base;
++labelIndex;
}
- const label = size.toFixed(1);
+ const label = labelIndex === 0 ? `${size}` : size.toFixed(1);
return `${label}${labels[labelIndex]}`;
}
@@ -722,15 +844,21 @@ async function storageEstimate() {
}
storageEstimate.mostRecent = null;
+async function isStoragePeristent() {
+ try {
+ return await navigator.storage.persisted();
+ } catch (e) { }
+ return false;
+}
+
async function storageInfoInitialize() {
- const browser = await getBrowser();
- const container = document.querySelector('#storage-info');
- container.setAttribute('data-browser', browser);
+ storagePersistInitialize();
+ const {browser, platform} = await apiGetEnvironmentInfo();
+ document.documentElement.dataset.browser = browser;
+ document.documentElement.dataset.operatingSystem = platform.os;
await storageShowInfo();
- container.classList.remove('storage-hidden');
-
document.querySelector('#storage-refresh').addEventListener('click', () => storageShowInfo(), false);
}
@@ -741,8 +869,14 @@ async function storageUpdateStats() {
const valid = (estimate !== null);
if (valid) {
- document.querySelector('#storage-usage').textContent = storageBytesToLabeledString(estimate.usage);
- document.querySelector('#storage-quota').textContent = storageBytesToLabeledString(estimate.quota);
+ // Firefox reports usage as 0 when persistent storage is enabled.
+ const finite = (estimate.usage > 0 || !(await isStoragePeristent()));
+ if (finite) {
+ document.querySelector('#storage-usage').textContent = storageBytesToLabeledString(estimate.usage);
+ document.querySelector('#storage-quota').textContent = storageBytesToLabeledString(estimate.quota);
+ }
+ document.querySelector('#storage-use-finite').classList.toggle('storage-hidden', !finite);
+ document.querySelector('#storage-use-infinite').classList.toggle('storage-hidden', finite);
}
storageUpdateStats.isUpdating = false;
@@ -769,6 +903,43 @@ function storageSpinnerShow(show) {
}
}
+async function storagePersistInitialize() {
+ if (!(navigator.storage && navigator.storage.persist)) {
+ // Not supported
+ return;
+ }
+
+ const info = document.querySelector('#storage-persist-info');
+ const button = document.querySelector('#storage-persist-button');
+ const checkbox = document.querySelector('#storage-persist-button-checkbox');
+
+ info.classList.remove('storage-hidden');
+ button.classList.remove('storage-hidden');
+
+ let persisted = await isStoragePeristent();
+ checkbox.checked = persisted;
+
+ button.addEventListener('click', async () => {
+ if (persisted) {
+ return;
+ }
+ let result = false;
+ try {
+ result = await navigator.storage.persist();
+ } catch (e) {
+ // NOP
+ }
+
+ if (result) {
+ persisted = true;
+ checkbox.checked = true;
+ storageShowInfo();
+ } else {
+ $('.storage-persist-fail-warning').removeClass('storage-hidden');
+ }
+ }, false);
+}
+
/*
* Information
diff --git a/ext/bg/js/templates.js b/ext/bg/js/templates.js
index c61f5d7f..59516d97 100644
--- a/ext/bg/js/templates.js
+++ b/ext/bg/js/templates.js
@@ -208,148 +208,157 @@ templates['model.html'] = template({"1":function(container,depth0,helpers,partia
+ " </ul>\n </div>\n </div>\n </td>\n</tr>\n";
},"useData":true});
templates['terms.html'] = template({"1":function(container,depth0,helpers,partials,data) {
- var stack1, alias1=depth0 != null ? depth0 : (container.nullContext || {});
-
- return ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.definitionTags : depth0),{"name":"if","hash":{},"fn":container.program(2, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
- + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.only : depth0),{"name":"if","hash":{},"fn":container.program(7, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
- + ((stack1 = helpers["if"].call(alias1,((stack1 = (depth0 != null ? depth0.glossary : depth0)) != null ? stack1["1"] : stack1),{"name":"if","hash":{},"fn":container.program(11, data, 0),"inverse":container.program(17, data, 0),"data":data})) != null ? stack1 : "");
+ var stack1, helper, options, alias1=depth0 != null ? depth0 : (container.nullContext || {}), buffer =
+ "<div class=\"dict-";
+ stack1 = ((helper = (helper = helpers.sanitizeCssClass || (depth0 != null ? depth0.sanitizeCssClass : depth0)) != null ? helper : helpers.helperMissing),(options={"name":"sanitizeCssClass","hash":{},"fn":container.program(2, data, 0),"inverse":container.noop,"data":data}),(typeof helper === "function" ? helper.call(alias1,options) : helper));
+ if (!helpers.sanitizeCssClass) { stack1 = helpers.blockHelperMissing.call(depth0,stack1,options)}
+ if (stack1 != null) { buffer += stack1; }
+ return buffer + "\">\n"
+ + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.definitionTags : depth0),{"name":"if","hash":{},"fn":container.program(4, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.only : depth0),{"name":"if","hash":{},"fn":container.program(9, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ + ((stack1 = helpers["if"].call(alias1,((stack1 = (depth0 != null ? depth0.glossary : depth0)) != null ? stack1["1"] : stack1),{"name":"if","hash":{},"fn":container.program(13, data, 0),"inverse":container.program(19, data, 0),"data":data})) != null ? stack1 : "")
+ + "</div>\n";
},"2":function(container,depth0,helpers,partials,data) {
+ var helper;
+
+ return container.escapeExpression(((helper = (helper = helpers.dictionary || (depth0 != null ? depth0.dictionary : depth0)) != null ? helper : helpers.helperMissing),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : (container.nullContext || {}),{"name":"dictionary","hash":{},"data":data}) : helper)));
+},"4":function(container,depth0,helpers,partials,data) {
var stack1, alias1=depth0 != null ? depth0 : (container.nullContext || {});
- return "<div "
- + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.compactGlossaries : depth0),{"name":"if","hash":{},"fn":container.program(3, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ return " <div "
+ + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.compactGlossaries : depth0),{"name":"if","hash":{},"fn":container.program(5, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ ">\n"
- + ((stack1 = helpers.each.call(alias1,(depth0 != null ? depth0.definitionTags : depth0),{"name":"each","hash":{},"fn":container.program(5, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
- + "</div>\n";
-},"3":function(container,depth0,helpers,partials,data) {
- return "class=\"compact-info\"";
+ + ((stack1 = helpers.each.call(alias1,(depth0 != null ? depth0.definitionTags : depth0),{"name":"each","hash":{},"fn":container.program(7, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ + " </div>\n";
},"5":function(container,depth0,helpers,partials,data) {
+ return "class=\"compact-info\"";
+},"7":function(container,depth0,helpers,partials,data) {
var helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression;
- return " <span class=\"label label-default tag-"
+ return " <span class=\"label label-default tag-"
+ alias4(((helper = (helper = helpers.category || (depth0 != null ? depth0.category : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"category","hash":{},"data":data}) : helper)))
+ "\" title=\""
+ alias4(((helper = (helper = helpers.notes || (depth0 != null ? depth0.notes : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"notes","hash":{},"data":data}) : helper)))
+ "\">"
+ alias4(((helper = (helper = helpers.name || (depth0 != null ? depth0.name : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"name","hash":{},"data":data}) : helper)))
+ "</span>\n";
-},"7":function(container,depth0,helpers,partials,data) {
+},"9":function(container,depth0,helpers,partials,data) {
var stack1, alias1=depth0 != null ? depth0 : (container.nullContext || {});
- return "<div "
- + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.compactGlossaries : depth0),{"name":"if","hash":{},"fn":container.program(3, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
- + ">\n ("
- + ((stack1 = helpers.each.call(alias1,(depth0 != null ? depth0.only : depth0),{"name":"each","hash":{},"fn":container.program(8, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
- + " only)\n</div>\n";
-},"8":function(container,depth0,helpers,partials,data) {
+ return " <div "
+ + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.compactGlossaries : depth0),{"name":"if","hash":{},"fn":container.program(5, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ + ">\n ("
+ + ((stack1 = helpers.each.call(alias1,(depth0 != null ? depth0.only : depth0),{"name":"each","hash":{},"fn":container.program(10, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ + " only)\n </div>\n";
+},"10":function(container,depth0,helpers,partials,data) {
var stack1;
return ((stack1 = container.lambda(depth0, depth0)) != null ? stack1 : "")
- + ((stack1 = helpers.unless.call(depth0 != null ? depth0 : (container.nullContext || {}),(data && data.last),{"name":"unless","hash":{},"fn":container.program(9, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ + ((stack1 = helpers.unless.call(depth0 != null ? depth0 : (container.nullContext || {}),(data && data.last),{"name":"unless","hash":{},"fn":container.program(11, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ "\n";
-},"9":function(container,depth0,helpers,partials,data) {
- return ", ";
},"11":function(container,depth0,helpers,partials,data) {
+ return ", ";
+},"13":function(container,depth0,helpers,partials,data) {
var stack1, alias1=depth0 != null ? depth0 : (container.nullContext || {});
- return "<ul "
- + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.compactGlossaries : depth0),{"name":"if","hash":{},"fn":container.program(12, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ return " <ul "
+ + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.compactGlossaries : depth0),{"name":"if","hash":{},"fn":container.program(14, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ ">\n"
- + ((stack1 = helpers.each.call(alias1,(depth0 != null ? depth0.glossary : depth0),{"name":"each","hash":{},"fn":container.program(14, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
- + "</ul>\n";
-},"12":function(container,depth0,helpers,partials,data) {
- return "class=\"compact-glossary\"";
+ + ((stack1 = helpers.each.call(alias1,(depth0 != null ? depth0.glossary : depth0),{"name":"each","hash":{},"fn":container.program(16, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ + " </ul>\n";
},"14":function(container,depth0,helpers,partials,data) {
+ return "class=\"compact-glossary\"";
+},"16":function(container,depth0,helpers,partials,data) {
var stack1, helper, options, buffer =
- " <li><span class=\"glossary-item\">";
- stack1 = ((helper = (helper = helpers.multiLine || (depth0 != null ? depth0.multiLine : depth0)) != null ? helper : helpers.helperMissing),(options={"name":"multiLine","hash":{},"fn":container.program(15, data, 0),"inverse":container.noop,"data":data}),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : (container.nullContext || {}),options) : helper));
+ " <li><span class=\"glossary-item\">";
+ stack1 = ((helper = (helper = helpers.multiLine || (depth0 != null ? depth0.multiLine : depth0)) != null ? helper : helpers.helperMissing),(options={"name":"multiLine","hash":{},"fn":container.program(17, data, 0),"inverse":container.noop,"data":data}),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : (container.nullContext || {}),options) : helper));
if (!helpers.multiLine) { stack1 = helpers.blockHelperMissing.call(depth0,stack1,options)}
if (stack1 != null) { buffer += stack1; }
return buffer + "</span></li>\n";
-},"15":function(container,depth0,helpers,partials,data) {
- return container.escapeExpression(container.lambda(depth0, depth0));
},"17":function(container,depth0,helpers,partials,data) {
+ return container.escapeExpression(container.lambda(depth0, depth0));
+},"19":function(container,depth0,helpers,partials,data) {
var stack1, helper, options, alias1=depth0 != null ? depth0 : (container.nullContext || {}), buffer =
- "<div class=\"glossary-item "
- + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.compactGlossaries : depth0),{"name":"if","hash":{},"fn":container.program(18, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ " <div class=\"glossary-item "
+ + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.compactGlossaries : depth0),{"name":"if","hash":{},"fn":container.program(20, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ "\">";
- stack1 = ((helper = (helper = helpers.multiLine || (depth0 != null ? depth0.multiLine : depth0)) != null ? helper : helpers.helperMissing),(options={"name":"multiLine","hash":{},"fn":container.program(20, data, 0),"inverse":container.noop,"data":data}),(typeof helper === "function" ? helper.call(alias1,options) : helper));
+ stack1 = ((helper = (helper = helpers.multiLine || (depth0 != null ? depth0.multiLine : depth0)) != null ? helper : helpers.helperMissing),(options={"name":"multiLine","hash":{},"fn":container.program(22, data, 0),"inverse":container.noop,"data":data}),(typeof helper === "function" ? helper.call(alias1,options) : helper));
if (!helpers.multiLine) { stack1 = helpers.blockHelperMissing.call(depth0,stack1,options)}
if (stack1 != null) { buffer += stack1; }
return buffer + "</div>\n";
-},"18":function(container,depth0,helpers,partials,data) {
- return "compact-glossary";
},"20":function(container,depth0,helpers,partials,data) {
+ return "compact-glossary";
+},"22":function(container,depth0,helpers,partials,data) {
var stack1;
return container.escapeExpression(container.lambda(((stack1 = (depth0 != null ? depth0.glossary : depth0)) != null ? stack1["0"] : stack1), depth0));
-},"22":function(container,depth0,helpers,partials,data,blockParams,depths) {
+},"24":function(container,depth0,helpers,partials,data,blockParams,depths) {
var stack1, alias1=depth0 != null ? depth0 : (container.nullContext || {});
return "<div class=\"entry\" data-type=\"term\">\n <div class=\"actions\">\n"
- + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.addable : depth0),{"name":"if","hash":{},"fn":container.program(23, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "")
- + ((stack1 = helpers.unless.call(alias1,(depth0 != null ? depth0.merged : depth0),{"name":"unless","hash":{},"fn":container.program(25, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "")
- + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.source : depth0),{"name":"if","hash":{},"fn":container.program(28, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.addable : depth0),{"name":"if","hash":{},"fn":container.program(25, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ + ((stack1 = helpers.unless.call(alias1,(depth0 != null ? depth0.merged : depth0),{"name":"unless","hash":{},"fn":container.program(27, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.source : depth0),{"name":"if","hash":{},"fn":container.program(30, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ " <img src=\"/mixed/img/entry-current.svg\" class=\"current\" title=\"Current entry (Alt + Up/Down/Home/End/PgUp/PgDn)\" alt>\n </div>\n\n"
- + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.merged : depth0),{"name":"if","hash":{},"fn":container.program(30, data, 0, blockParams, depths),"inverse":container.program(45, data, 0, blockParams, depths),"data":data})) != null ? stack1 : "")
+ + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.merged : depth0),{"name":"if","hash":{},"fn":container.program(32, data, 0, blockParams, depths),"inverse":container.program(47, data, 0, blockParams, depths),"data":data})) != null ? stack1 : "")
+ "\n"
- + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.reasons : depth0),{"name":"if","hash":{},"fn":container.program(49, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.reasons : depth0),{"name":"if","hash":{},"fn":container.program(50, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ "\n"
- + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.frequencies : depth0),{"name":"if","hash":{},"fn":container.program(53, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.frequencies : depth0),{"name":"if","hash":{},"fn":container.program(54, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ "\n <div class=\"glossary\">\n"
- + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.grouped : depth0),{"name":"if","hash":{},"fn":container.program(56, data, 0, blockParams, depths),"inverse":container.program(62, data, 0, blockParams, depths),"data":data})) != null ? stack1 : "")
+ + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.grouped : depth0),{"name":"if","hash":{},"fn":container.program(57, data, 0, blockParams, depths),"inverse":container.program(63, data, 0, blockParams, depths),"data":data})) != null ? stack1 : "")
+ " </div>\n\n"
- + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.debug : depth0),{"name":"if","hash":{},"fn":container.program(65, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.debug : depth0),{"name":"if","hash":{},"fn":container.program(66, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ "</div>\n";
-},"23":function(container,depth0,helpers,partials,data) {
- return " <a href=\"#\" class=\"action-view-note pending disabled\"><img src=\"/mixed/img/view-note.svg\" title=\"View added note (Alt + V)\" alt></a>\n <a href=\"#\" class=\"action-add-note pending disabled\" data-mode=\"term-kanji\"><img src=\"/mixed/img/add-term-kanji.svg\" title=\"Add expression (Alt + E)\" alt></a>\n <a href=\"#\" class=\"action-add-note pending disabled\" data-mode=\"term-kana\"><img src=\"/mixed/img/add-term-kana.svg\" title=\"Add reading (Alt + R)\" alt></a>\n";
},"25":function(container,depth0,helpers,partials,data) {
+ return " <a href=\"#\" class=\"action-view-note pending disabled\"><img src=\"/mixed/img/view-note.svg\" title=\"View added note (Alt + V)\" alt></a>\n <a href=\"#\" class=\"action-add-note pending disabled\" data-mode=\"term-kanji\"><img src=\"/mixed/img/add-term-kanji.svg\" title=\"Add expression (Alt + E)\" alt></a>\n <a href=\"#\" class=\"action-add-note pending disabled\" data-mode=\"term-kana\"><img src=\"/mixed/img/add-term-kana.svg\" title=\"Add reading (Alt + R)\" alt></a>\n";
+},"27":function(container,depth0,helpers,partials,data) {
var stack1;
- return ((stack1 = helpers["if"].call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.playback : depth0),{"name":"if","hash":{},"fn":container.program(26, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "");
-},"26":function(container,depth0,helpers,partials,data) {
- return " <a href=\"#\" class=\"action-play-audio\"><img src=\"/mixed/img/play-audio.svg\" title=\"Play audio (Alt + P)\" alt></a>\n";
+ return ((stack1 = helpers["if"].call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.playback : depth0),{"name":"if","hash":{},"fn":container.program(28, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "");
},"28":function(container,depth0,helpers,partials,data) {
+ return " <a href=\"#\" class=\"action-play-audio\"><img src=\"/mixed/img/play-audio.svg\" title=\"Play audio (Alt + P)\" alt></a>\n";
+},"30":function(container,depth0,helpers,partials,data) {
return " <a href=\"#\" class=\"source-term\"><img src=\"/mixed/img/source-term.svg\" title=\"Source term (Alt + B)\" alt></a>\n";
-},"30":function(container,depth0,helpers,partials,data,blockParams,depths) {
+},"32":function(container,depth0,helpers,partials,data,blockParams,depths) {
var stack1;
- return ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.expressions : depth0),{"name":"each","hash":{},"fn":container.program(31, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "");
-},"31":function(container,depth0,helpers,partials,data,blockParams,depths) {
+ return ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.expressions : depth0),{"name":"each","hash":{},"fn":container.program(33, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "");
+},"33":function(container,depth0,helpers,partials,data,blockParams,depths) {
var stack1, helper, options, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=helpers.helperMissing, alias3="function", buffer =
"<div class=\"expression\"><span class=\"expression-"
+ container.escapeExpression(((helper = (helper = helpers.termFrequency || (depth0 != null ? depth0.termFrequency : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"termFrequency","hash":{},"data":data}) : helper)))
+ "\">";
- stack1 = ((helper = (helper = helpers.kanjiLinks || (depth0 != null ? depth0.kanjiLinks : depth0)) != null ? helper : alias2),(options={"name":"kanjiLinks","hash":{},"fn":container.program(32, data, 0, blockParams, depths),"inverse":container.noop,"data":data}),(typeof helper === alias3 ? helper.call(alias1,options) : helper));
+ stack1 = ((helper = (helper = helpers.kanjiLinks || (depth0 != null ? depth0.kanjiLinks : depth0)) != null ? helper : alias2),(options={"name":"kanjiLinks","hash":{},"fn":container.program(34, data, 0, blockParams, depths),"inverse":container.noop,"data":data}),(typeof helper === alias3 ? helper.call(alias1,options) : helper));
if (!helpers.kanjiLinks) { stack1 = helpers.blockHelperMissing.call(depth0,stack1,options)}
if (stack1 != null) { buffer += stack1; }
return buffer + "</span><div class=\"peek-wrapper\">"
- + ((stack1 = helpers["if"].call(alias1,(depths[1] != null ? depths[1].playback : depths[1]),{"name":"if","hash":{},"fn":container.program(35, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "")
- + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.termTags : depth0),{"name":"if","hash":{},"fn":container.program(37, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "")
- + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.frequencies : depth0),{"name":"if","hash":{},"fn":container.program(40, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ + ((stack1 = helpers["if"].call(alias1,(depths[1] != null ? depths[1].playback : depths[1]),{"name":"if","hash":{},"fn":container.program(37, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.termTags : depth0),{"name":"if","hash":{},"fn":container.program(39, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.frequencies : depth0),{"name":"if","hash":{},"fn":container.program(42, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ "</div><span class=\""
- + ((stack1 = helpers["if"].call(alias1,(data && data.last),{"name":"if","hash":{},"fn":container.program(43, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ + ((stack1 = helpers["if"].call(alias1,(data && data.last),{"name":"if","hash":{},"fn":container.program(45, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ "\">、</span></div>";
-},"32":function(container,depth0,helpers,partials,data) {
+},"34":function(container,depth0,helpers,partials,data) {
var stack1, helper, options;
- stack1 = ((helper = (helper = helpers.furigana || (depth0 != null ? depth0.furigana : depth0)) != null ? helper : helpers.helperMissing),(options={"name":"furigana","hash":{},"fn":container.program(33, data, 0),"inverse":container.noop,"data":data}),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : (container.nullContext || {}),options) : helper));
+ stack1 = ((helper = (helper = helpers.furigana || (depth0 != null ? depth0.furigana : depth0)) != null ? helper : helpers.helperMissing),(options={"name":"furigana","hash":{},"fn":container.program(35, data, 0),"inverse":container.noop,"data":data}),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : (container.nullContext || {}),options) : helper));
if (!helpers.furigana) { stack1 = helpers.blockHelperMissing.call(depth0,stack1,options)}
if (stack1 != null) { return stack1; }
else { return ''; }
-},"33":function(container,depth0,helpers,partials,data) {
+},"35":function(container,depth0,helpers,partials,data) {
var stack1;
return ((stack1 = container.lambda(depth0, depth0)) != null ? stack1 : "");
-},"35":function(container,depth0,helpers,partials,data) {
- return "<a href=\"#\" class=\"action-play-audio\"><img src=\"/mixed/img/play-audio.svg\" title=\"Play audio\" alt></a>";
},"37":function(container,depth0,helpers,partials,data) {
+ return "<a href=\"#\" class=\"action-play-audio\"><img src=\"/mixed/img/play-audio.svg\" title=\"Play audio\" alt></a>";
+},"39":function(container,depth0,helpers,partials,data) {
var stack1;
return "<div class=\"tags\">"
- + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.termTags : depth0),{"name":"each","hash":{},"fn":container.program(38, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.termTags : depth0),{"name":"each","hash":{},"fn":container.program(40, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ "</div>";
-},"38":function(container,depth0,helpers,partials,data) {
+},"40":function(container,depth0,helpers,partials,data) {
var helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression;
return " <span class=\"label label-default tag-"
@@ -359,13 +368,13 @@ templates['terms.html'] = template({"1":function(container,depth0,helpers,partia
+ "\">"
+ alias4(((helper = (helper = helpers.name || (depth0 != null ? depth0.name : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"name","hash":{},"data":data}) : helper)))
+ "</span>\n";
-},"40":function(container,depth0,helpers,partials,data) {
+},"42":function(container,depth0,helpers,partials,data) {
var stack1;
return "<div class=\"frequencies\">"
- + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.frequencies : depth0),{"name":"each","hash":{},"fn":container.program(41, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.frequencies : depth0),{"name":"each","hash":{},"fn":container.program(43, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ "</div>";
-},"41":function(container,depth0,helpers,partials,data) {
+},"43":function(container,depth0,helpers,partials,data) {
var helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression;
return " <span class=\"label label-default tag-frequency\">"
@@ -373,55 +382,45 @@ templates['terms.html'] = template({"1":function(container,depth0,helpers,partia
+ ":"
+ alias4(((helper = (helper = helpers.frequency || (depth0 != null ? depth0.frequency : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"frequency","hash":{},"data":data}) : helper)))
+ "</span>\n";
-},"43":function(container,depth0,helpers,partials,data) {
- return "invisible";
},"45":function(container,depth0,helpers,partials,data) {
+ return "invisible";
+},"47":function(container,depth0,helpers,partials,data) {
var stack1, helper, options, alias1=depth0 != null ? depth0 : (container.nullContext || {}), buffer =
" <div class=\"expression\">";
- stack1 = ((helper = (helper = helpers.kanjiLinks || (depth0 != null ? depth0.kanjiLinks : depth0)) != null ? helper : helpers.helperMissing),(options={"name":"kanjiLinks","hash":{},"fn":container.program(32, data, 0),"inverse":container.noop,"data":data}),(typeof helper === "function" ? helper.call(alias1,options) : helper));
+ stack1 = ((helper = (helper = helpers.kanjiLinks || (depth0 != null ? depth0.kanjiLinks : depth0)) != null ? helper : helpers.helperMissing),(options={"name":"kanjiLinks","hash":{},"fn":container.program(34, data, 0),"inverse":container.noop,"data":data}),(typeof helper === "function" ? helper.call(alias1,options) : helper));
if (!helpers.kanjiLinks) { stack1 = helpers.blockHelperMissing.call(depth0,stack1,options)}
if (stack1 != null) { buffer += stack1; }
return buffer + "</div>\n"
- + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.termTags : depth0),{"name":"if","hash":{},"fn":container.program(46, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "");
-},"46":function(container,depth0,helpers,partials,data) {
+ + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.termTags : depth0),{"name":"if","hash":{},"fn":container.program(48, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "");
+},"48":function(container,depth0,helpers,partials,data) {
var stack1;
return " <div style=\"display: inline-block;\">\n"
- + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.termTags : depth0),{"name":"each","hash":{},"fn":container.program(47, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.termTags : depth0),{"name":"each","hash":{},"fn":container.program(7, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ " </div>\n";
-},"47":function(container,depth0,helpers,partials,data) {
- var helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression;
-
- return " <span class=\"label label-default tag-"
- + alias4(((helper = (helper = helpers.category || (depth0 != null ? depth0.category : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"category","hash":{},"data":data}) : helper)))
- + "\" title=\""
- + alias4(((helper = (helper = helpers.notes || (depth0 != null ? depth0.notes : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"notes","hash":{},"data":data}) : helper)))
- + "\">"
- + alias4(((helper = (helper = helpers.name || (depth0 != null ? depth0.name : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"name","hash":{},"data":data}) : helper)))
- + "</span>\n";
-},"49":function(container,depth0,helpers,partials,data) {
+},"50":function(container,depth0,helpers,partials,data) {
var stack1;
return " <div class=\"reasons\">\n"
- + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.reasons : depth0),{"name":"each","hash":{},"fn":container.program(50, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.reasons : depth0),{"name":"each","hash":{},"fn":container.program(51, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ " </div>\n";
-},"50":function(container,depth0,helpers,partials,data) {
+},"51":function(container,depth0,helpers,partials,data) {
var stack1;
return " <span class=\"reasons\">"
+ container.escapeExpression(container.lambda(depth0, depth0))
+ "</span> "
- + ((stack1 = helpers.unless.call(depth0 != null ? depth0 : (container.nullContext || {}),(data && data.last),{"name":"unless","hash":{},"fn":container.program(51, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ + ((stack1 = helpers.unless.call(depth0 != null ? depth0 : (container.nullContext || {}),(data && data.last),{"name":"unless","hash":{},"fn":container.program(52, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ "\n";
-},"51":function(container,depth0,helpers,partials,data) {
+},"52":function(container,depth0,helpers,partials,data) {
return "&laquo;";
-},"53":function(container,depth0,helpers,partials,data) {
+},"54":function(container,depth0,helpers,partials,data) {
var stack1;
return " <div>\n"
- + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.frequencies : depth0),{"name":"each","hash":{},"fn":container.program(54, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.frequencies : depth0),{"name":"each","hash":{},"fn":container.program(55, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ " </div>\n";
-},"54":function(container,depth0,helpers,partials,data) {
+},"55":function(container,depth0,helpers,partials,data) {
var helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression;
return " <span class=\"label label-default tag-frequency\">"
@@ -429,67 +428,67 @@ templates['terms.html'] = template({"1":function(container,depth0,helpers,partia
+ ":"
+ alias4(((helper = (helper = helpers.frequency || (depth0 != null ? depth0.frequency : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"frequency","hash":{},"data":data}) : helper)))
+ "</span>\n";
-},"56":function(container,depth0,helpers,partials,data,blockParams,depths) {
+},"57":function(container,depth0,helpers,partials,data,blockParams,depths) {
var stack1;
- return ((stack1 = helpers["if"].call(depth0 != null ? depth0 : (container.nullContext || {}),((stack1 = (depth0 != null ? depth0.definitions : depth0)) != null ? stack1["1"] : stack1),{"name":"if","hash":{},"fn":container.program(57, data, 0, blockParams, depths),"inverse":container.program(60, data, 0, blockParams, depths),"data":data})) != null ? stack1 : "");
-},"57":function(container,depth0,helpers,partials,data,blockParams,depths) {
+ return ((stack1 = helpers["if"].call(depth0 != null ? depth0 : (container.nullContext || {}),((stack1 = (depth0 != null ? depth0.definitions : depth0)) != null ? stack1["1"] : stack1),{"name":"if","hash":{},"fn":container.program(58, data, 0, blockParams, depths),"inverse":container.program(61, data, 0, blockParams, depths),"data":data})) != null ? stack1 : "");
+},"58":function(container,depth0,helpers,partials,data,blockParams,depths) {
var stack1;
return " <ol>\n"
- + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.definitions : depth0),{"name":"each","hash":{},"fn":container.program(58, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.definitions : depth0),{"name":"each","hash":{},"fn":container.program(59, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ " </ol>\n";
-},"58":function(container,depth0,helpers,partials,data,blockParams,depths) {
+},"59":function(container,depth0,helpers,partials,data,blockParams,depths) {
var stack1;
return " <li>"
+ ((stack1 = container.invokePartial(partials.definition,depth0,{"name":"definition","hash":{"compactGlossaries":(depths[1] != null ? depths[1].compactGlossaries : depths[1])},"data":data,"helpers":helpers,"partials":partials,"decorators":container.decorators})) != null ? stack1 : "")
+ "</li>\n";
-},"60":function(container,depth0,helpers,partials,data) {
+},"61":function(container,depth0,helpers,partials,data) {
var stack1;
return ((stack1 = container.invokePartial(partials.definition,((stack1 = (depth0 != null ? depth0.definitions : depth0)) != null ? stack1["0"] : stack1),{"name":"definition","hash":{"compactGlossaries":(depth0 != null ? depth0.compactGlossaries : depth0)},"data":data,"indent":" ","helpers":helpers,"partials":partials,"decorators":container.decorators})) != null ? stack1 : "");
-},"62":function(container,depth0,helpers,partials,data,blockParams,depths) {
+},"63":function(container,depth0,helpers,partials,data,blockParams,depths) {
var stack1;
- return ((stack1 = helpers["if"].call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.merged : depth0),{"name":"if","hash":{},"fn":container.program(56, data, 0, blockParams, depths),"inverse":container.program(63, data, 0, blockParams, depths),"data":data})) != null ? stack1 : "");
-},"63":function(container,depth0,helpers,partials,data) {
+ return ((stack1 = helpers["if"].call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.merged : depth0),{"name":"if","hash":{},"fn":container.program(57, data, 0, blockParams, depths),"inverse":container.program(64, data, 0, blockParams, depths),"data":data})) != null ? stack1 : "");
+},"64":function(container,depth0,helpers,partials,data) {
var stack1;
return ((stack1 = container.invokePartial(partials.definition,depth0,{"name":"definition","hash":{"compactGlossaries":(depth0 != null ? depth0.compactGlossaries : depth0)},"data":data,"indent":" ","helpers":helpers,"partials":partials,"decorators":container.decorators})) != null ? stack1 : "")
+ " ";
-},"65":function(container,depth0,helpers,partials,data) {
+},"66":function(container,depth0,helpers,partials,data) {
var stack1, helper, options, buffer =
" <pre>";
- stack1 = ((helper = (helper = helpers.dumpObject || (depth0 != null ? depth0.dumpObject : depth0)) != null ? helper : helpers.helperMissing),(options={"name":"dumpObject","hash":{},"fn":container.program(33, data, 0),"inverse":container.noop,"data":data}),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : (container.nullContext || {}),options) : helper));
+ stack1 = ((helper = (helper = helpers.dumpObject || (depth0 != null ? depth0.dumpObject : depth0)) != null ? helper : helpers.helperMissing),(options={"name":"dumpObject","hash":{},"fn":container.program(35, data, 0),"inverse":container.noop,"data":data}),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : (container.nullContext || {}),options) : helper));
if (!helpers.dumpObject) { stack1 = helpers.blockHelperMissing.call(depth0,stack1,options)}
if (stack1 != null) { buffer += stack1; }
return buffer + "</pre>\n";
-},"67":function(container,depth0,helpers,partials,data,blockParams,depths) {
+},"68":function(container,depth0,helpers,partials,data,blockParams,depths) {
var stack1;
- return ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.definitions : depth0),{"name":"each","hash":{},"fn":container.program(68, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "");
-},"68":function(container,depth0,helpers,partials,data,blockParams,depths) {
+ return ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.definitions : depth0),{"name":"each","hash":{},"fn":container.program(69, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "");
+},"69":function(container,depth0,helpers,partials,data,blockParams,depths) {
var stack1;
- return ((stack1 = helpers.unless.call(depth0 != null ? depth0 : (container.nullContext || {}),(data && data.first),{"name":"unless","hash":{},"fn":container.program(69, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ return ((stack1 = helpers.unless.call(depth0 != null ? depth0 : (container.nullContext || {}),(data && data.first),{"name":"unless","hash":{},"fn":container.program(70, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ "\n"
+ ((stack1 = container.invokePartial(partials.term,depth0,{"name":"term","hash":{"source":(depths[1] != null ? depths[1].source : depths[1]),"compactGlossaries":(depths[1] != null ? depths[1].compactGlossaries : depths[1]),"playback":(depths[1] != null ? depths[1].playback : depths[1]),"addable":(depths[1] != null ? depths[1].addable : depths[1]),"merged":(depths[1] != null ? depths[1].merged : depths[1]),"grouped":(depths[1] != null ? depths[1].grouped : depths[1]),"debug":(depths[1] != null ? depths[1].debug : depths[1])},"data":data,"helpers":helpers,"partials":partials,"decorators":container.decorators})) != null ? stack1 : "");
-},"69":function(container,depth0,helpers,partials,data) {
+},"70":function(container,depth0,helpers,partials,data) {
return "<hr>";
-},"71":function(container,depth0,helpers,partials,data) {
+},"72":function(container,depth0,helpers,partials,data) {
return "<p class=\"note\">No results found.</p>\n";
},"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data,blockParams,depths) {
var stack1;
return "\n\n"
- + ((stack1 = helpers["if"].call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.definitions : depth0),{"name":"if","hash":{},"fn":container.program(67, data, 0, blockParams, depths),"inverse":container.program(71, data, 0, blockParams, depths),"data":data})) != null ? stack1 : "");
+ + ((stack1 = helpers["if"].call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.definitions : depth0),{"name":"if","hash":{},"fn":container.program(68, data, 0, blockParams, depths),"inverse":container.program(72, data, 0, blockParams, depths),"data":data})) != null ? stack1 : "");
},"main_d": function(fn, props, container, depth0, data, blockParams, depths) {
var decorators = container.decorators;
fn = decorators.inline(fn,props,container,{"name":"inline","hash":{},"fn":container.program(1, data, 0, blockParams, depths),"inverse":container.noop,"args":["definition"],"data":data}) || fn;
- fn = decorators.inline(fn,props,container,{"name":"inline","hash":{},"fn":container.program(22, data, 0, blockParams, depths),"inverse":container.noop,"args":["term"],"data":data}) || fn;
+ fn = decorators.inline(fn,props,container,{"name":"inline","hash":{},"fn":container.program(24, data, 0, blockParams, depths),"inverse":container.noop,"args":["term"],"data":data}) || fn;
return fn;
}
diff --git a/ext/bg/js/translator.js b/ext/bg/js/translator.js
index 601ee30c..ee012d96 100644
--- a/ext/bg/js/translator.js
+++ b/ext/bg/js/translator.js
@@ -21,6 +21,7 @@ class Translator {
constructor() {
this.database = null;
this.deinflector = null;
+ this.tagCache = {};
}
async prepare() {
@@ -36,6 +37,11 @@ class Translator {
}
}
+ async purgeDatabase() {
+ this.tagCache = {};
+ await this.database.purge();
+ }
+
async findTermsGrouped(text, dictionaries, alphanumeric, options) {
const titles = Object.keys(dictionaries);
const {length, definitions} = await this.findTerms(text, dictionaries, alphanumeric);
@@ -52,94 +58,121 @@ class Translator {
return {length, definitions: definitionsGrouped};
}
- async findTermsMerged(text, dictionaries, alphanumeric, options) {
- const secondarySearchTitles = Object.keys(options.dictionaries).filter(dict => options.dictionaries[dict].allowSecondarySearches);
- const titles = Object.keys(dictionaries);
- const {length, definitions} = await this.findTerms(text, dictionaries, alphanumeric);
+ async getSequencedDefinitions(definitions, mainDictionary) {
+ const definitionsBySequence = dictTermsMergeBySequence(definitions, mainDictionary);
+ const defaultDefinitions = definitionsBySequence['-1'];
- const definitionsBySequence = dictTermsMergeBySequence(definitions, options.general.mainDictionary);
+ const sequenceList = Object.keys(definitionsBySequence).map(v => Number(v)).filter(v => v >= 0);
+ const sequencedDefinitions = sequenceList.map((key) => ({
+ definitions: definitionsBySequence[key],
+ rawDefinitions: []
+ }));
- const definitionsMerged = [];
- const mergedByTermIndices = new Set();
- for (const sequence in definitionsBySequence) {
- if (sequence < 0) {
- continue;
- }
+ for (const definition of await this.database.findTermsBySequenceBulk(sequenceList, mainDictionary)) {
+ sequencedDefinitions[definition.index].rawDefinitions.push(definition);
+ }
- const result = definitionsBySequence[sequence];
+ return {sequencedDefinitions, defaultDefinitions};
+ }
- const rawDefinitionsBySequence = await this.database.findTermsBySequence(Number(sequence), options.general.mainDictionary);
+ async getMergedSecondarySearchResults(text, expressionsMap, secondarySearchTitles) {
+ if (secondarySearchTitles.length === 0) {
+ return [];
+ }
- for (const definition of rawDefinitionsBySequence) {
- const definitionTags = await this.expandTags(definition.definitionTags, definition.dictionary);
- definitionTags.push(dictTagBuildSource(definition.dictionary));
- definition.definitionTags = definitionTags;
- const termTags = await this.expandTags(definition.termTags, definition.dictionary);
- definition.termTags = termTags;
+ const expressionList = [];
+ const readingList = [];
+ for (const expression of expressionsMap.keys()) {
+ if (expression === text) { continue; }
+ for (const reading of expressionsMap.get(expression).keys()) {
+ expressionList.push(expression);
+ readingList.push(reading);
}
+ }
- const definitionsByGloss = dictTermsMergeByGloss(result, rawDefinitionsBySequence);
-
- const secondarySearchResults = [];
- if (secondarySearchTitles.length > 0) {
- for (const expression of result.expressions.keys()) {
- if (expression === text) {
- continue;
- }
-
- for (const reading of result.expressions.get(expression).keys()) {
- for (const definition of await this.database.findTermsExact(expression, reading, secondarySearchTitles)) {
- const definitionTags = await this.expandTags(definition.definitionTags, definition.dictionary);
- definitionTags.push(dictTagBuildSource(definition.dictionary));
- definition.definitionTags = definitionTags;
- const termTags = await this.expandTags(definition.termTags, definition.dictionary);
- definition.termTags = termTags;
- secondarySearchResults.push(definition);
- }
- }
- }
- }
+ const definitions = await this.database.findTermsExactBulk(expressionList, readingList, secondarySearchTitles);
+ for (const definition of definitions) {
+ const definitionTags = await this.expandTags(definition.definitionTags, definition.dictionary);
+ definitionTags.push(dictTagBuildSource(definition.dictionary));
+ definition.definitionTags = definitionTags;
+ const termTags = await this.expandTags(definition.termTags, definition.dictionary);
+ definition.termTags = termTags;
+ }
- dictTermsMergeByGloss(result, definitionsBySequence['-1'].concat(secondarySearchResults), definitionsByGloss, mergedByTermIndices);
+ if (definitions.length > 1) {
+ definitions.sort((a, b) => a.index - b.index);
+ }
- for (const gloss in definitionsByGloss) {
- const definition = definitionsByGloss[gloss];
- dictTagsSort(definition.definitionTags);
- result.definitions.push(definition);
- }
+ return definitions;
+ }
- dictTermsSort(result.definitions, dictionaries);
-
- const expressions = [];
- for (const expression of result.expressions.keys()) {
- for (const reading of result.expressions.get(expression).keys()) {
- const termTags = result.expressions.get(expression).get(reading);
- expressions.push({
- expression: expression,
- reading: reading,
- termTags: dictTagsSort(termTags),
- termFrequency: (score => {
- if (score > 0) {
- return 'popular';
- } else if (score < 0) {
- return 'rare';
- } else {
- return 'normal';
- }
- })(termTags.map(tag => tag.score).reduce((p, v) => p + v, 0))
- });
- }
+ async getMergedDefinition(text, dictionaries, sequencedDefinition, defaultDefinitions, secondarySearchTitles, mergedByTermIndices) {
+ const result = sequencedDefinition.definitions;
+ const rawDefinitionsBySequence = sequencedDefinition.rawDefinitions;
+
+ for (const definition of rawDefinitionsBySequence) {
+ const definitionTags = await this.expandTags(definition.definitionTags, definition.dictionary);
+ definitionTags.push(dictTagBuildSource(definition.dictionary));
+ definition.definitionTags = definitionTags;
+ const termTags = await this.expandTags(definition.termTags, definition.dictionary);
+ definition.termTags = termTags;
+ }
+
+ const definitionsByGloss = dictTermsMergeByGloss(result, rawDefinitionsBySequence);
+ const secondarySearchResults = await this.getMergedSecondarySearchResults(text, result.expressions, secondarySearchTitles);
+
+ dictTermsMergeByGloss(result, defaultDefinitions.concat(secondarySearchResults), definitionsByGloss, mergedByTermIndices);
+
+ for (const gloss in definitionsByGloss) {
+ const definition = definitionsByGloss[gloss];
+ dictTagsSort(definition.definitionTags);
+ result.definitions.push(definition);
+ }
+
+ dictTermsSort(result.definitions, dictionaries);
+
+ const expressions = [];
+ for (const expression of result.expressions.keys()) {
+ for (const reading of result.expressions.get(expression).keys()) {
+ const termTags = result.expressions.get(expression).get(reading);
+ const score = termTags.map(tag => tag.score).reduce((p, v) => p + v, 0);
+ expressions.push({
+ expression: expression,
+ reading: reading,
+ termTags: dictTagsSort(termTags),
+ termFrequency: Translator.scoreToTermFrequency(score)
+ });
}
+ }
- result.expressions = expressions;
+ result.expressions = expressions;
+ result.expression = Array.from(result.expression);
+ result.reading = Array.from(result.reading);
- result.expression = Array.from(result.expression);
- result.reading = Array.from(result.reading);
+ return result;
+ }
+
+ async findTermsMerged(text, dictionaries, alphanumeric, options) {
+ const secondarySearchTitles = Object.keys(options.dictionaries).filter(dict => options.dictionaries[dict].allowSecondarySearches);
+ const titles = Object.keys(dictionaries);
+ const {length, definitions} = await this.findTerms(text, dictionaries, alphanumeric);
+ const {sequencedDefinitions, defaultDefinitions} = await this.getSequencedDefinitions(definitions, options.general.mainDictionary);
+ const definitionsMerged = [];
+ const mergedByTermIndices = new Set();
+ for (const sequencedDefinition of sequencedDefinitions) {
+ const result = await this.getMergedDefinition(
+ text,
+ dictionaries,
+ sequencedDefinition,
+ defaultDefinitions,
+ secondarySearchTitles,
+ mergedByTermIndices
+ );
definitionsMerged.push(result);
}
- const strayDefinitions = definitionsBySequence['-1'].filter((definition, index) => !mergedByTermIndices.has(index));
+ const strayDefinitions = defaultDefinitions.filter((definition, index) => !mergedByTermIndices.has(index));
for (const groupedDefinition of dictTermsGroup(strayDefinitions, dictionaries)) {
groupedDefinition.expressions = [{expression: groupedDefinition.expression, reading: groupedDefinition.reading}];
definitionsMerged.push(groupedDefinition);
@@ -277,33 +310,44 @@ class Translator {
}
async findKanji(text, dictionaries) {
- let definitions = [];
- const processed = {};
const titles = Object.keys(dictionaries);
+ const kanjiUnique = {};
+ const kanjiList = [];
for (const c of text) {
- if (!processed[c]) {
- definitions.push(...await this.database.findKanji(c, titles));
- processed[c] = true;
+ if (!kanjiUnique.hasOwnProperty(c)) {
+ kanjiList.push(c);
+ kanjiUnique[c] = true;
}
}
+ const definitions = await this.database.findKanjiBulk(kanjiList, titles);
+ if (definitions.length === 0) {
+ return definitions;
+ }
+
+ if (definitions.length > 1) {
+ definitions.sort((a, b) => a.index - b.index);
+ }
+
+ const kanjiList2 = [];
for (const definition of definitions) {
+ kanjiList2.push(definition.character);
+
const tags = await this.expandTags(definition.tags, definition.dictionary);
tags.push(dictTagBuildSource(definition.dictionary));
definition.tags = dictTagsSort(tags);
definition.stats = await this.expandStats(definition.stats, definition.dictionary);
-
definition.frequencies = [];
- for (const meta of await this.database.findKanjiMeta(definition.character, titles)) {
- if (meta.mode === 'freq') {
- definition.frequencies.push({
- character: meta.character,
- frequency: meta.data,
- dictionary: meta.dictionary
- });
- }
- }
+ }
+
+ for (const meta of await this.database.findKanjiMetaBulk(kanjiList2, titles)) {
+ if (meta.mode !== 'freq') { continue; }
+ definitions[meta.index].frequencies.push({
+ character: meta.character,
+ frequency: meta.data,
+ dictionary: meta.dictionary
+ });
}
return definitions;
@@ -359,56 +403,76 @@ class Translator {
}
async expandTags(names, title) {
- const tags = [];
- for (const name of names) {
- const base = Translator.getNameBase(name);
- let meta = this.database.findTagForTitleCached(base, title);
- if (typeof meta === 'undefined') {
- meta = await this.database.findTagForTitle(base, title);
- }
-
- const tag = Object.assign({}, meta !== null ? meta : {}, {name});
-
- tags.push(dictTagSanitize(tag));
- }
-
- return tags;
+ const tagMetaList = await this.getTagMetaList(names, title);
+ return tagMetaList.map((meta, index) => {
+ const name = names[index];
+ const tag = dictTagSanitize(Object.assign({}, meta !== null ? meta : {}, {name}));
+ return dictTagSanitize(tag);
+ });
}
async expandStats(items, title) {
- const stats = {};
- for (const name in items) {
- const base = Translator.getNameBase(name);
- let meta = this.database.findTagForTitleCached(base, title);
- if (typeof meta === 'undefined') {
- meta = await this.database.findTagForTitle(base, title);
- if (meta === null) {
- continue;
- }
- }
+ const names = Object.keys(items);
+ const tagMetaList = await this.getTagMetaList(names, title);
- const group = stats[meta.category] = stats[meta.category] || [];
+ const stats = {};
+ for (let i = 0; i < names.length; ++i) {
+ const name = names[i];
+ const meta = tagMetaList[i];
+ if (meta === null) { continue; }
+
+ const category = meta.category;
+ const group = (
+ stats.hasOwnProperty(category) ?
+ stats[category] :
+ (stats[category] = [])
+ );
const stat = Object.assign({}, meta, {name, value: items[name]});
-
group.push(dictTagSanitize(stat));
}
+ const sortCompare = (a, b) => a.notes - b.notes;
for (const category in stats) {
- stats[category].sort((a, b) => {
- if (a.notes < b.notes) {
- return -1;
- } else if (a.notes > b.notes) {
- return 1;
- } else {
- return 0;
- }
- });
+ stats[category].sort(sortCompare);
}
return stats;
}
+ async getTagMetaList(names, title) {
+ const tagMetaList = [];
+ const cache = (
+ this.tagCache.hasOwnProperty(title) ?
+ this.tagCache[title] :
+ (this.tagCache[title] = {})
+ );
+
+ for (const name of names) {
+ const base = Translator.getNameBase(name);
+
+ if (cache.hasOwnProperty(base)) {
+ tagMetaList.push(cache[base]);
+ } else {
+ const tagMeta = await this.database.findTagForTitle(base, title);
+ cache[base] = tagMeta;
+ tagMetaList.push(tagMeta);
+ }
+ }
+
+ return tagMetaList;
+ }
+
+ static scoreToTermFrequency(score) {
+ if (score > 0) {
+ return 'popular';
+ } else if (score < 0) {
+ return 'rare';
+ } else {
+ return 'normal';
+ }
+ }
+
static getNameBase(name) {
const pos = name.indexOf(':');
return (pos >= 0 ? name.substr(0, pos) : name);
diff --git a/ext/bg/js/util.js b/ext/bg/js/util.js
index 73a8396f..1ca0833b 100644
--- a/ext/bg/js/util.js
+++ b/ext/bg/js/util.js
@@ -89,7 +89,7 @@ function utilAnkiGetModelFieldNames(modelName) {
}
function utilDatabasePurge() {
- return utilBackend().translator.database.purge();
+ return utilBackend().translator.purgeDatabase();
}
async function utilDatabaseImport(data, progress, exceptions) {
diff --git a/ext/bg/lang/deinflect.json b/ext/bg/lang/deinflect.json
index 682093e1..a0b6baa1 100644
--- a/ext/bg/lang/deinflect.json
+++ b/ext/bg/lang/deinflect.json
@@ -3671,7 +3671,7 @@
"kanaIn": "ておる",
"kanaOut": "て",
"rulesIn": [
- "v1"
+ "v5"
],
"rulesOut": [
"iru"
@@ -3701,7 +3701,7 @@
"kanaIn": "でおる",
"kanaOut": "で",
"rulesIn": [
- "v1"
+ "v5"
],
"rulesOut": [
"iru"
@@ -3711,7 +3711,7 @@
"kanaIn": "とる",
"kanaOut": "て",
"rulesIn": [
- "v1"
+ "v5"
],
"rulesOut": [
"iru"
diff --git a/ext/bg/search.html b/ext/bg/search.html
index 3284ed43..9d28b358 100644
--- a/ext/bg/search.html
+++ b/ext/bg/search.html
@@ -1,5 +1,5 @@
<!DOCTYPE html>
-<html lang="en">
+<html lang="en" data-yomichan-page="search">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1" />
@@ -7,6 +7,8 @@
<link rel="stylesheet" type="text/css" href="/mixed/lib/bootstrap/css/bootstrap.min.css">
<link rel="stylesheet" type="text/css" href="/mixed/lib/bootstrap/css/bootstrap-theme.min.css">
<link rel="stylesheet" type="text/css" href="/mixed/css/display.css">
+ <link rel="stylesheet" type="text/css" href="/mixed/css/display-default.css" data-yomichan-theme-name="default">
+ <link rel="stylesheet alternate" type="text/css" href="/mixed/css/display-dark.css" data-yomichan-theme-name="dark">
</head>
<body>
<div class="container">
diff --git a/ext/bg/settings-popup-preview.html b/ext/bg/settings-popup-preview.html
new file mode 100644
index 00000000..07caa271
--- /dev/null
+++ b/ext/bg/settings-popup-preview.html
@@ -0,0 +1,125 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
+ <title>Yomichan Popup Preview</title>
+ <link rel="stylesheet" type="text/css" href="/fg/css/client.css" id="client-css">
+ <style>
+ html {
+ transition: background-color 0.25s linear 0s, color 0.25s linear 0s;
+ color: #333333;
+ }
+ html.dark {
+ background-color: #1e1e1e;
+ color: #d4d4d4;
+ }
+ html, body {
+ margin: 0;
+ padding: 0;
+ border: 0;
+ overflow: hidden;
+ width: 100%;
+ height: 100%;
+ font-family: "Helvetica Neue", Helvetica, Arial ,sans-serif;
+ font-size: 14px;
+ }
+ iframe.yomichan-float {
+ resize: none;
+ }
+ .vertical-align-outer {
+ width: 100%;
+ height: 100%;
+ white-space: nowrap;
+ }
+ .vertical-align-outer::before {
+ content: "";
+ display: inline-block;
+ vertical-align: middle;
+ width: 0;
+ height: 100%;
+ }
+ .vertical-align-inner {
+ display: inline-block;
+ vertical-align: middle;
+ white-space: normal;
+ width: 100%;
+ }
+ .horizontal-size {
+ max-width: 400px;
+ padding: 15px;
+ margin: 0 auto;
+ }
+ .example-text-container {
+ font-size: 24px;
+ line-height: 1.25em;
+ height: 1.25em;
+ }
+ .popup-placeholder {
+ height: 250px;
+ padding-top: 10px;
+ border: 1px solid rgba(0, 0, 0, 0);
+ }
+ .placeholder-info {
+ visibility: hidden;
+ opacity: 0;
+ transition: opacity 0.5s linear 0s, visibility 0s linear 0.5s;
+ }
+ .placeholder-info.placeholder-info-visible {
+ visibility: visible;
+ opacity: 1;
+ transition: opacity 0.5s linear 0s, visibility 0s linear 0s;
+ }
+
+ .options {
+ float: right;
+ font-size: 14px;
+ line-height: 30px;
+ }
+ .theme-button {
+ display: inline-block;
+ margin-left: 0.5em;
+ text-decoration: none;
+ cursor: pointer;
+ white-space: nowrap;
+ line-height: 0;
+ }
+ .theme-button>input {
+ vertical-align: middle;
+ margin: 0 0.25em 0 0;
+ padding: 0;
+ }
+ .theme-button>span {
+ vertical-align: middle;
+ }
+ .theme-button:hover>span {
+ text-decoration: underline;
+ }
+ </style>
+ </head>
+ <body>
+ <div class="vertical-align-outer"><div class="vertical-align-inner"><div class="horizontal-size">
+ <div class="example-text-container">
+ <div class="options"><label class="theme-button"><input type="checkbox" id="theme-dark-checkbox" /><span>dark</span></label></div>
+ <span id="example-text">読め</span>
+ </div>
+ <div class="popup-placeholder">
+ <div class="vertical-align-outer"><div class="vertical-align-inner placeholder-info">
+ This page uses the dictionaries you have installed in order to show a preview.
+ If you see this message, make sure you have a dictionary installed.
+ </div></div>
+ </div>
+ </div></div></div>
+
+ <script src="/mixed/js/extension.js"></script>
+ <script src="/fg/js/api.js"></script>
+ <script src="/fg/js/document.js"></script>
+ <script src="/fg/js/frontend-api-receiver.js"></script>
+ <script src="/fg/js/popup.js"></script>
+ <script src="/fg/js/source.js"></script>
+ <script src="/fg/js/util.js"></script>
+ <script src="/fg/js/popup-proxy-host.js"></script>
+ <script src="/fg/js/frontend.js"></script>
+ <script src="/bg/js/settings-popup-preview.js"></script>
+ </body>
+</html>
diff --git a/ext/bg/settings.html b/ext/bg/settings.html
index e4710283..9b1c4366 100644
--- a/ext/bg/settings.html
+++ b/ext/bg/settings.html
@@ -231,9 +231,49 @@
</div>
</div>
+ <div class="form-group">
+ <div class="row">
+ <div class="col-xs-6">
+ <label for="popup-theme">Popup theme</label>
+ <select class="form-control" id="popup-theme">
+ <option value="default">Light</option>
+ <option value="dark">Dark</option>
+ </select>
+ </div>
+ <div class="col-xs-6">
+ <label for="popup-outer-theme">Popup shadow theme</label>
+ <select class="form-control" id="popup-outer-theme">
+ <option value="auto">Auto-detect</option>
+ <option value="default">Light</option>
+ <option value="dark">Dark</option>
+ </select>
+ </div>
+ </div>
+ </div>
+
<div class="form-group options-advanced">
- <label for="custom-popup-css">Custom popup CSS</label>
- <div><textarea autocomplete="off" spellcheck="false" wrap="soft" id="custom-popup-css" class="form-control"></textarea></div>
+ <div class="row">
+ <div class="col-xs-6">
+ <label for="custom-popup-css">Custom popup CSS</label>
+ <div><textarea autocomplete="off" spellcheck="false" wrap="soft" id="custom-popup-css" class="form-control"></textarea></div>
+ </div>
+ <div class="col-xs-6">
+ <label for="custom-popup-outer-css">Custom popup outer CSS</label>
+ <div><textarea autocomplete="off" spellcheck="false" wrap="soft" id="custom-popup-outer-css" class="form-control" placeholder="iframe.yomichan-float { /*styles*/ }"></textarea></div>
+ </div>
+ </div>
+ </div>
+
+ <div class="form-group ignore-form-changes" style="display: none;" id="settings-popup-preview-settings">
+ <label for="settings-popup-preview-text">Popup preview text</label>
+ <input type="text" id="settings-popup-preview-text" class="form-control" value="読め">
+ </div>
+
+ <div class="form-group ignore-form-changes">
+ <div id="settings-popup-preview-button-container">
+ <button class="btn btn-default" id="settings-popup-preview-button">Show popup preview</button>
+ </div>
+ <div id="settings-popup-preview-container"></div>
</div>
</div>
@@ -253,6 +293,16 @@
<input type="number" min="0" max="100" id="audio-playback-volume" class="form-control">
</div>
+ <div class="form-group" style="display: none;" id="text-to-speech-voice-container">
+ <label for="text-to-speech-voice">Text-to-speech voice</label>
+ <div class="input-group">
+ <select class="form-control" id="text-to-speech-voice"></select>
+ <div class="input-group-btn">
+ <button class="btn btn-default" id="text-to-speech-voice-test" title="Test voice" data-speech-text="よみちゃん"><span class="glyphicon glyphicon-volume-up"></span></button>
+ </div>
+ </div>
+ </div>
+
<div class="form-group options-advanced">
<label for="audio-custom-source">Custom audio source <span class="label-light">(URL)</span></label>
<input type="text" id="audio-custom-source" class="form-control" placeholder="Example: http://localhost/audio.mp3?expression={expression}&reading={reading}">
@@ -269,8 +319,10 @@
<div class="input-group-addon audio-source-prefix"></div>
<select class="form-control audio-source-select">
<option value="jpod101">JapanesePod101</option>
- <option value="jpod101-alternate">JapanesePod101 (alternate)</option>
+ <option value="jpod101-alternate">JapanesePod101 (Alternate)</option>
<option value="jisho">Jisho.org</option>
+ <option value="text-to-speech">Text-to-speech</option>
+ <option value="text-to-speech-reading">Text-to-speech (Kana reading)</option>
<option value="custom">Custom</option>
</select>
<div class="input-group-btn"><button class="btn btn-danger audio-source-remove" title="Remove"><span class="glyphicon glyphicon-remove"></span></button></div>
@@ -395,16 +447,27 @@
</div>
</div>
- <div id="storage-info" class="storage-hidden">
+ <div id="storage-info">
<div>
<img src="/mixed/img/spinner.gif" class="pull-right" id="storage-spinner" />
<h3>Storage</h3>
</div>
- <div id="storage-use" class="storage-hidden">
+ <div id="storage-persist-info" class="storage-hidden">
<p class="help-block">
+ Web browsers may sometimes clear stored data if the device is running low on storage space.
+ This can result in the stored dictionary data being deleted unexpectedly, causing Yomichan to stop working for no apparent reason.
+ In order to prevent this, persistent storage must be enable by clicking the "Persistent Storage" button below.
+ </p>
+ </div>
+
+ <div id="storage-use" class="storage-hidden">
+ <p class="help-block storage-hidden" id="storage-use-finite">
Yomichan is using approximately <strong id="storage-usage"></strong> of <strong id="storage-quota"></strong>.
</p>
+ <p class="help-block storage-hidden" id="storage-use-infinite">
+ Yomichan is permitted <strong>unlimited storage</strong>.
+ </p>
</div>
<div id="storage-error" class="storage-hidden">
@@ -425,8 +488,23 @@
</div></div>
<div>
- <button class="btn btn-default" id="storage-refresh">Refresh</button>
+ <button class="btn btn-default" id="storage-refresh"><span class="btn-inner-middle">Refresh</span></button>
+ <button class="btn btn-default storage-hidden ignore-form-changes" id="storage-persist-button"><span class="storage-button-inner"><input type="checkbox" class="btn-inner-middle storage-button-checkbox" id="storage-persist-button-checkbox" readonly /><span class="btn-inner-middle">Persistent Storage</span></span></button>
</div>
+
+ <p></p>
+
+ <div data-show-for-browser="firefox-mobile"><div class="alert alert-warning storage-persist-fail-warning storage-hidden">
+ <p>It may not be possible to enable Persistent Storage on Firefox for Android.</p>
+ </div></div>
+
+ <div data-show-for-browser="chrome"><div class="alert alert-warning storage-persist-fail-warning storage-hidden">
+ <p>
+ It may not be possible to enable Persistent Storage on Chrome-based browsers.
+ However, the Yomichan extension has permission for unlimited storage which should
+ prevent Chrome from deleting data.<sup><a href="https://bugs.chromium.org/p/chromium/issues/detail?id=680392#c15" target="_blank" rel="noopener">[1]</a></sup>
+ </p>
+ </div></div>
</div>
<div>
@@ -441,13 +519,21 @@
<a href="https://foosoft.net/projects/anki-connect/" target="_blank" rel="noopener">AnkiConnect</a> plugin.
</p>
- <div class="alert alert-danger" id="anki-error"></div>
-
<div class="checkbox">
<label><input type="checkbox" id="anki-enable"> Enable Anki integration</label>
</div>
<div id="anki-general">
+ <div data-show-for-operating-system="mac">
+ <div class="alert alert-warning" id="anki-mac-warning">
+ Notice for Mac OS X users:
+ If Yomichan has issues connecting to AnkiConnect, you may have to tweak some system settings.
+ See <a href="https://foosoft.net/projects/anki-connect/#notes-for-mac-os-x-users" target="_blank" rel="noopener">this link</a> for more details.
+ </div>
+ </div>
+
+ <div class="alert alert-danger" id="anki-error"></div>
+
<div class="form-group">
<label for="card-tags">Card tags <span class="label-light">(comma or space separated)</span></label>
<input type="text" id="card-tags" class="form-control">
@@ -591,6 +677,7 @@
<script src="/mixed/lib/jquery.min.js"></script>
<script src="/mixed/lib/bootstrap/js/bootstrap.min.js"></script>
<script src="/mixed/lib/handlebars.min.js"></script>
+ <script src="/mixed/lib/wanakana.min.js"></script>
<script src="/mixed/js/extension.js"></script>
@@ -605,6 +692,7 @@
<script src="/bg/js/profile-conditions.js"></script>
<script src="/bg/js/templates.js"></script>
<script src="/bg/js/util.js"></script>
+ <script src="/mixed/js/audio.js"></script>
<script src="/bg/js/settings-profiles.js"></script>
<script src="/bg/js/settings.js"></script>
diff --git a/ext/fg/css/client.css b/ext/fg/css/client.css
index a2b06d0f..633c88ef 100644
--- a/ext/fg/css/client.css
+++ b/ext/fg/css/client.css
@@ -17,11 +17,11 @@
*/
-iframe#yomichan-float {
+iframe.yomichan-float {
all: initial;
background-color: #fff;
border: 1px solid #999;
- box-shadow: 0 0 10px rgba(0, 0, 0, .5);
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
position: fixed;
resize: both;
visibility: hidden;
@@ -29,7 +29,14 @@ iframe#yomichan-float {
box-sizing: border-box;
}
-iframe#yomichan-float.yomichan-float-full-width {
+iframe.yomichan-float[data-yomichan-theme=dark],
+iframe.yomichan-float[data-yomichan-theme=auto][data-yomichan-site-color=dark] {
+ background-color: #1e1e1e;
+ border: 1px solid #666;
+ box-shadow: 0 0 10px rgba(255, 255, 255, 0.5);
+}
+
+iframe.yomichan-float.yomichan-float-full-width {
border-left: none;
border-right: none;
left: 0 !important;
@@ -39,13 +46,13 @@ iframe#yomichan-float.yomichan-float-full-width {
resize: none;
}
-iframe#yomichan-float.yomichan-float-full-width:not(.yomichan-float-above) {
+iframe.yomichan-float.yomichan-float-full-width:not(.yomichan-float-above) {
border-bottom: none;
top: auto !important;
bottom: 0 !important;
}
-iframe#yomichan-float.yomichan-float-full-width.yomichan-float-above {
+iframe.yomichan-float.yomichan-float-full-width.yomichan-float-above {
border-top: none;
top: 0 !important;
bottom: auto !important;
diff --git a/ext/fg/float.html b/ext/fg/float.html
index fe1aee8f..580a7963 100644
--- a/ext/fg/float.html
+++ b/ext/fg/float.html
@@ -1,18 +1,12 @@
<!DOCTYPE html>
-<html lang="en">
+<html lang="en" data-yomichan-page="float">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title></title>
- <link rel="stylesheet" href="/mixed/lib/bootstrap/css/bootstrap.min.css">
- <link rel="stylesheet" href="/mixed/lib/bootstrap/css/bootstrap-theme.min.css">
<link rel="stylesheet" href="/mixed/css/display.css">
- <style type="text/css">
- .entry, .note {
- padding-left: 10px;
- padding-right: 10px;
- }
- </style>
+ <link rel="stylesheet" type="text/css" href="/mixed/css/display-default.css" data-yomichan-theme-name="default">
+ <link rel="stylesheet alternate" type="text/css" href="/mixed/css/display-dark.css" data-yomichan-theme-name="dark">
</head>
<body>
<div id="spinner">
diff --git a/ext/fg/js/api.js b/ext/fg/js/api.js
index a553e514..b0746b85 100644
--- a/ext/fg/js/api.js
+++ b/ext/fg/js/api.js
@@ -49,8 +49,8 @@ function apiAudioGetUrl(definition, source, optionsContext) {
return utilInvoke('audioGetUrl', {definition, source, optionsContext});
}
-function apiCommandExec(command) {
- return utilInvoke('commandExec', {command});
+function apiCommandExec(command, params) {
+ return utilInvoke('commandExec', {command, params});
}
function apiScreenshotGet(options) {
@@ -64,3 +64,11 @@ function apiForward(action, params) {
function apiFrameInformationGet() {
return utilInvoke('frameInformationGet');
}
+
+function apiInjectStylesheet(css) {
+ return utilInvoke('injectStylesheet', {css});
+}
+
+function apiGetEnvironmentInfo() {
+ return utilInvoke('getEnvironmentInfo');
+}
diff --git a/ext/fg/js/document.js b/ext/fg/js/document.js
index 94a68e6c..a168705e 100644
--- a/ext/fg/js/document.js
+++ b/ext/fg/js/document.js
@@ -27,8 +27,8 @@ function docImposterCreate(element, isTextarea) {
const elementStyle = window.getComputedStyle(element);
const elementRect = element.getBoundingClientRect();
const documentRect = document.documentElement.getBoundingClientRect();
- const left = elementRect.left - documentRect.left;
- const top = elementRect.top - documentRect.top;
+ let left = elementRect.left - documentRect.left;
+ let top = elementRect.top - documentRect.top;
// Container
const container = document.createElement('div');
@@ -82,6 +82,12 @@ function docImposterCreate(element, isTextarea) {
docSetImposterStyle(imposterStyle, 'width', `${width}px`);
docSetImposterStyle(imposterStyle, 'height', `${height}px`);
}
+ if (imposterRect.x !== elementRect.x || imposterRect.y !== elementRect.y) {
+ left += (elementRect.left - imposterRect.left);
+ top += (elementRect.top - imposterRect.top);
+ docSetImposterStyle(imposterStyle, 'left', `${left}px`);
+ docSetImposterStyle(imposterStyle, 'top', `${top}px`);
+ }
imposter.scrollTop = element.scrollTop;
imposter.scrollLeft = element.scrollLeft;
diff --git a/ext/fg/js/float.js b/ext/fg/js/float.js
index 8fdb6925..089c9422 100644
--- a/ext/fg/js/float.js
+++ b/ext/fg/js/float.js
@@ -21,39 +21,23 @@ class DisplayFloat extends Display {
constructor() {
super(document.querySelector('#spinner'), document.querySelector('#definitions'));
this.autoPlayAudioTimer = null;
- this.styleNode = null;
this.optionsContext = {
depth: 0,
url: window.location.href
};
- this.dependencies = Object.assign({}, this.dependencies, {docRangeFromPoint, docSentenceExtract});
-
window.addEventListener('message', (e) => this.onMessage(e), false);
}
onError(error) {
if (window.yomichan_orphaned) {
- this.onOrphaned();
+ this.setContentOrphaned();
} else {
logError(error, true);
}
}
- onOrphaned() {
- const definitions = document.querySelector('#definitions');
- const errorOrphaned = document.querySelector('#error-orphaned');
-
- if (definitions !== null) {
- definitions.style.setProperty('display', 'none', 'important');
- }
-
- if (errorOrphaned !== null) {
- errorOrphaned.style.setProperty('display', 'block', 'important');
- }
- }
-
onSearchClear() {
window.parent.postMessage('popupClose', '*');
}
@@ -84,6 +68,10 @@ class DisplayFloat extends Display {
super.onKeyDown(e);
}
+ getOptionsContext() {
+ return this.optionsContext;
+ }
+
autoPlayAudio() {
this.clearAutoPlayTimer();
this.autoPlayAudioTimer = window.setTimeout(() => super.autoPlayAudio(), 400);
@@ -96,29 +84,15 @@ class DisplayFloat extends Display {
}
}
- initialize(options, popupInfo, url) {
- const css = options.general.customPopupCss;
- if (css) {
- this.setStyle(css);
- }
+ async initialize(options, popupInfo, url, childrenSupported) {
+ await super.initialize(options);
const {id, depth, parentFrameId} = popupInfo;
this.optionsContext.depth = depth;
this.optionsContext.url = url;
- popupNestedInitialize(id, depth, parentFrameId, url);
- }
-
- setStyle(css) {
- const parent = document.head;
-
- if (this.styleNode === null) {
- this.styleNode = document.createElement('style');
- }
-
- this.styleNode.textContent = css;
- if (this.styleNode.parentNode !== parent) {
- parent.appendChild(this.styleNode);
+ if (childrenSupported) {
+ popupNestedInitialize(id, depth, parentFrameId, url);
}
}
}
@@ -134,11 +108,10 @@ DisplayFloat.onKeyDownHandlers = {
};
DisplayFloat.messageHandlers = {
- termsShow: (self, {definitions, options, context}) => self.termsShow(definitions, options, context),
- kanjiShow: (self, {definitions, options, context}) => self.kanjiShow(definitions, options, context),
+ setContent: (self, {type, details}) => self.setContent(type, details),
clearAutoPlayTimer: (self) => self.clearAutoPlayTimer(),
- orphaned: (self) => self.onOrphaned(),
- initialize: (self, {options, popupInfo, url}) => self.initialize(options, popupInfo, url)
+ setCustomCss: (self, {css}) => self.setCustomCss(css),
+ initialize: (self, {options, popupInfo, url, childrenSupported}) => self.initialize(options, popupInfo, url, childrenSupported)
};
window.yomichan_display = new DisplayFloat();
diff --git a/ext/fg/js/frontend-initialize.js b/ext/fg/js/frontend-initialize.js
new file mode 100644
index 00000000..37a82faa
--- /dev/null
+++ b/ext/fg/js/frontend-initialize.js
@@ -0,0 +1,20 @@
+/*
+ * Copyright (C) 2019 Alex Yatskov <alex@foosoft.net>
+ * Author: Alex Yatskov <alex@foosoft.net>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+
+window.yomichan_frontend = Frontend.create();
diff --git a/ext/fg/js/frontend.js b/ext/fg/js/frontend.js
index 88cb93a9..e854f74e 100644
--- a/ext/fg/js/frontend.js
+++ b/ext/fg/js/frontend.js
@@ -41,14 +41,18 @@ class Frontend {
this.enabled = false;
this.eventListeners = [];
+
+ this.isPreparedPromiseResolve = null;
+ this.isPreparedPromise = new Promise((resolve) => { this.isPreparedPromiseResolve = resolve; });
+
+ this.lastShowPromise = Promise.resolve();
}
static create() {
- const initializationData = window.frontendInitializationData;
- const isNested = (initializationData !== null && typeof initializationData === 'object');
- const {id, depth, parentFrameId, ignoreNodes, url} = isNested ? initializationData : {};
+ const data = window.frontendInitializationData || {};
+ const {id, depth=0, parentFrameId, ignoreNodes, url, proxy=false} = data;
- const popup = isNested ? new PopupProxy(depth + 1, id, parentFrameId, url) : PopupProxyHost.instance.createPopup(null);
+ const popup = proxy ? new PopupProxy(depth + 1, id, parentFrameId, url) : PopupProxyHost.instance.createPopup(null, depth);
const frontend = new Frontend(popup, ignoreNodes);
frontend.prepare();
return frontend;
@@ -59,11 +63,16 @@ class Frontend {
await this.updateOptions();
chrome.runtime.onMessage.addListener(this.onRuntimeMessage.bind(this));
+ this.isPreparedPromiseResolve();
} catch (e) {
this.onError(e);
}
}
+ isPrepared() {
+ return this.isPreparedPromise;
+ }
+
onMouseOver(e) {
if (e.target === this.popup.container && this.popupTimer !== null) {
this.popupTimerClear();
@@ -130,8 +139,14 @@ class Frontend {
}
}
- onResize() {
- this.searchClear(false);
+ async onResize() {
+ if (this.textSourceLast !== null && await this.popup.isVisibleAsync()) {
+ const textSource = this.textSourceLast;
+ this.lastShowPromise = this.popup.showContent(
+ textSource.getRect(),
+ textSource.getWritingMode()
+ );
+ }
}
onClick(e) {
@@ -222,8 +237,8 @@ class Frontend {
const handlers = Frontend.runtimeMessageHandlers;
if (handlers.hasOwnProperty(action)) {
const handler = handlers[action];
- handler(this, params);
- callback();
+ const result = handler(this, params);
+ callback(result);
}
}
@@ -279,6 +294,7 @@ class Frontend {
async updateOptions() {
this.options = await apiOptionsGet(this.getOptionsContext());
this.setEnabled(this.options.general.enable);
+ await this.popup.setOptions(this.options);
}
popupTimerSet(callback) {
@@ -303,6 +319,10 @@ class Frontend {
}
const textSource = docRangeFromPoint(x, y, this.options);
+ return await this.searchSource(textSource, cause);
+ }
+
+ async searchSource(textSource, cause) {
let hideResults = textSource === null;
let searched = false;
let success = false;
@@ -318,10 +338,10 @@ class Frontend {
} catch (e) {
if (window.yomichan_orphaned) {
if (textSource && this.options.scanning.modifier !== 'none') {
- this.popup.showOrphaned(
+ this.lastShowPromise = this.popup.showContent(
textSource.getRect(),
textSource.getWritingMode(),
- this.options
+ 'orphaned'
);
}
} else {
@@ -357,12 +377,11 @@ class Frontend {
const sentence = docSentenceExtract(textSource, this.options.anki.sentenceExt);
const url = window.location.href;
- this.popup.termsShow(
+ this.lastShowPromise = this.popup.showContent(
textSource.getRect(),
textSource.getWritingMode(),
- definitions,
- this.options,
- {sentence, url, focus}
+ 'terms',
+ {definitions, context: {sentence, url, focus}}
);
this.textSourceLast = textSource;
@@ -388,12 +407,11 @@ class Frontend {
const sentence = docSentenceExtract(textSource, this.options.anki.sentenceExt);
const url = window.location.href;
- this.popup.kanjiShow(
+ this.lastShowPromise = this.popup.showContent(
textSource.getRect(),
textSource.getWritingMode(),
- definitions,
- this.options,
- {sentence, url, focus}
+ 'kanji',
+ {definitions, context: {sentence, url, focus}}
);
this.textSourceLast = textSource;
@@ -558,8 +576,9 @@ Frontend.runtimeMessageHandlers = {
popupSetVisibleOverride: (self, {visible}) => {
self.popup.setVisibleOverride(visible);
+ },
+
+ getUrl: () => {
+ return {url: window.location.href};
}
};
-
-
-window.yomichan_frontend = Frontend.create();
diff --git a/ext/fg/js/popup-nested.js b/ext/fg/js/popup-nested.js
index b36de2ec..cec95aea 100644
--- a/ext/fg/js/popup-nested.js
+++ b/ext/fg/js/popup-nested.js
@@ -35,13 +35,14 @@ async function popupNestedInitialize(id, depth, parentFrameId, url) {
const ignoreNodes = options.scanning.enableOnPopupExpressions ? [] : [ '.expression', '.expression *' ];
- window.frontendInitializationData = {id, depth, parentFrameId, ignoreNodes, url};
+ window.frontendInitializationData = {id, depth, parentFrameId, ignoreNodes, url, proxy: true};
const scriptSrcs = [
'/fg/js/frontend-api-sender.js',
'/fg/js/popup.js',
'/fg/js/popup-proxy.js',
- '/fg/js/frontend.js'
+ '/fg/js/frontend.js',
+ '/fg/js/frontend-initialize.js'
];
for (const src of scriptSrcs) {
const script = document.createElement('script');
diff --git a/ext/fg/js/popup-proxy-host.js b/ext/fg/js/popup-proxy-host.js
index f933639c..d8dec4df 100644
--- a/ext/fg/js/popup-proxy-host.js
+++ b/ext/fg/js/popup-proxy-host.js
@@ -38,21 +38,23 @@ class PopupProxyHost {
this.apiReceiver = new FrontendApiReceiver(`popup-proxy-host#${frameId}`, {
createNestedPopup: ({parentId}) => this.createNestedPopup(parentId),
- show: ({id, elementRect, options}) => this.show(id, elementRect, options),
- showOrphaned: ({id, elementRect, options}) => this.show(id, elementRect, options),
+ setOptions: ({id, options}) => this.setOptions(id, options),
hide: ({id, changeFocus}) => this.hide(id, changeFocus),
+ isVisibleAsync: ({id}) => this.isVisibleAsync(id),
setVisibleOverride: ({id, visible}) => this.setVisibleOverride(id, visible),
containsPoint: ({id, x, y}) => this.containsPoint(id, x, y),
- termsShow: ({id, elementRect, writingMode, definitions, options, context}) => this.termsShow(id, elementRect, writingMode, definitions, options, context),
- kanjiShow: ({id, elementRect, writingMode, definitions, options, context}) => this.kanjiShow(id, elementRect, writingMode, definitions, options, context),
+ showContent: ({id, elementRect, writingMode, type, details}) => this.showContent(id, elementRect, writingMode, type, details),
+ setCustomCss: ({id, css}) => this.setCustomCss(id, css),
clearAutoPlayTimer: ({id}) => this.clearAutoPlayTimer(id)
});
}
- createPopup(parentId) {
+ createPopup(parentId, depth) {
const parent = (typeof parentId === 'string' && this.popups.hasOwnProperty(parentId) ? this.popups[parentId] : null);
- const depth = (parent !== null ? parent.depth + 1 : 0);
const id = `${this.nextId}`;
+ if (parent !== null) {
+ depth = parent.depth + 1;
+ }
++this.nextId;
const popup = new Popup(id, depth, this.frameIdPromise);
if (parent !== null) {
@@ -64,7 +66,7 @@ class PopupProxyHost {
}
async createNestedPopup(parentId) {
- return this.createPopup(parentId).id;
+ return this.createPopup(parentId, 0).id;
}
getPopup(id) {
@@ -86,26 +88,24 @@ class PopupProxyHost {
return new DOMRect(x, y, jsonRect.width, jsonRect.height);
}
- async show(id, elementRect, options) {
+ async setOptions(id, options) {
const popup = this.getPopup(id);
- elementRect = this.jsonRectToDOMRect(popup, elementRect);
- return await popup.show(elementRect, options);
+ return await popup.setOptions(options);
}
- async showOrphaned(id, elementRect, options) {
+ async hide(id, changeFocus) {
const popup = this.getPopup(id);
- elementRect = this.jsonRectToDOMRect(popup, elementRect);
- return await popup.showOrphaned(elementRect, options);
+ return popup.hide(changeFocus);
}
- async hide(id, changeFocus) {
+ async isVisibleAsync(id) {
const popup = this.getPopup(id);
- return popup.hide(changeFocus);
+ return await popup.isVisibleAsync();
}
async setVisibleOverride(id, visible) {
const popup = this.getPopup(id);
- return popup.setVisibleOverride(visible);
+ return await popup.setVisibleOverride(visible);
}
async containsPoint(id, x, y) {
@@ -113,18 +113,16 @@ class PopupProxyHost {
return await popup.containsPoint(x, y);
}
- async termsShow(id, elementRect, writingMode, definitions, options, context) {
+ async showContent(id, elementRect, writingMode, type, details) {
const popup = this.getPopup(id);
elementRect = this.jsonRectToDOMRect(popup, elementRect);
- if (!PopupProxyHost.popupCanShow(popup)) { return false; }
- return await popup.termsShow(elementRect, writingMode, definitions, options, context);
+ if (!PopupProxyHost.popupCanShow(popup)) { return Promise.resolve(false); }
+ return await popup.showContent(elementRect, writingMode, type, details);
}
- async kanjiShow(id, elementRect, writingMode, definitions, options, context) {
+ async setCustomCss(id, css) {
const popup = this.getPopup(id);
- elementRect = this.jsonRectToDOMRect(popup, elementRect);
- if (!PopupProxyHost.popupCanShow(popup)) { return false; }
- return await popup.kanjiShow(elementRect, writingMode, definitions, options, context);
+ return popup.setCustomCss(css);
}
async clearAutoPlayTimer(id) {
diff --git a/ext/fg/js/popup-proxy.js b/ext/fg/js/popup-proxy.js
index efbd28b2..e62a4868 100644
--- a/ext/fg/js/popup-proxy.js
+++ b/ext/fg/js/popup-proxy.js
@@ -46,16 +46,9 @@ class PopupProxy {
return id;
}
- async show(elementRect, options) {
+ async setOptions(options) {
const id = await this.getPopupId();
- elementRect = PopupProxy.DOMRectToJson(elementRect);
- return await this.invokeHostApi('show', {id, elementRect, options});
- }
-
- async showOrphaned(elementRect, options) {
- const id = await this.getPopupId();
- elementRect = PopupProxy.DOMRectToJson(elementRect);
- return await this.invokeHostApi('showOrphaned', {id, elementRect, options});
+ return await this.invokeHostApi('setOptions', {id, options});
}
async hide(changeFocus) {
@@ -65,6 +58,11 @@ class PopupProxy {
return await this.invokeHostApi('hide', {id: this.id, changeFocus});
}
+ async isVisibleAsync() {
+ const id = await this.getPopupId();
+ return await this.invokeHostApi('isVisibleAsync', {id});
+ }
+
async setVisibleOverride(visible) {
const id = await this.getPopupId();
return await this.invokeHostApi('setVisibleOverride', {id, visible});
@@ -77,16 +75,15 @@ class PopupProxy {
return await this.invokeHostApi('containsPoint', {id: this.id, x, y});
}
- async termsShow(elementRect, writingMode, definitions, options, context) {
+ async showContent(elementRect, writingMode, type=null, details=null) {
const id = await this.getPopupId();
elementRect = PopupProxy.DOMRectToJson(elementRect);
- return await this.invokeHostApi('termsShow', {id, elementRect, writingMode, definitions, options, context});
+ return await this.invokeHostApi('showContent', {id, elementRect, writingMode, type, details});
}
- async kanjiShow(elementRect, writingMode, definitions, options, context) {
+ async setCustomCss(css) {
const id = await this.getPopupId();
- elementRect = PopupProxy.DOMRectToJson(elementRect);
- return await this.invokeHostApi('kanjiShow', {id, elementRect, writingMode, definitions, options, context});
+ return await this.invokeHostApi('setCustomCss', {id, css});
}
async clearAutoPlayTimer() {
diff --git a/ext/fg/js/popup.js b/ext/fg/js/popup.js
index 9ca91afa..b5eb9fe2 100644
--- a/ext/fg/js/popup.js
+++ b/ext/fg/js/popup.js
@@ -25,8 +25,9 @@ class Popup {
this.frameId = null;
this.parent = null;
this.child = null;
+ this.childrenSupported = true;
this.container = document.createElement('iframe');
- this.container.id = 'yomichan-float';
+ this.container.className = 'yomichan-float';
this.container.addEventListener('mousedown', e => e.stopPropagation());
this.container.addEventListener('scroll', e => e.stopPropagation());
this.container.setAttribute('src', chrome.extension.getURL('/fg/float.html'));
@@ -36,17 +37,19 @@ class Popup {
this.isInjected = false;
this.visible = false;
this.visibleOverride = null;
+ this.options = null;
+ this.stylesheetInjectedViaApi = false;
this.updateVisibility();
}
- inject(options) {
+ inject() {
if (this.injectPromise === null) {
- this.injectPromise = this.createInjectPromise(options);
+ this.injectPromise = this.createInjectPromise();
}
return this.injectPromise;
}
- async createInjectPromise(options) {
+ async createInjectPromise() {
try {
const {frameId} = await this.frameIdPromise;
if (typeof frameId === 'number') {
@@ -60,30 +63,44 @@ class Popup {
const parentFrameId = (typeof this.frameId === 'number' ? this.frameId : null);
this.container.addEventListener('load', () => {
this.invokeApi('initialize', {
- options: {
- general: {
- customPopupCss: options.general.customPopupCss
- }
- },
+ options: this.options,
popupInfo: {
id: this.id,
depth: this.depth,
parentFrameId
},
- url: this.url
+ url: this.url,
+ childrenSupported: this.childrenSupported
});
resolve();
});
this.observeFullscreen();
this.onFullscreenChanged();
+ this.setCustomOuterCss(this.options.general.customPopupOuterCss, false);
this.isInjected = true;
});
}
- async show(elementRect, writingMode, options) {
- await this.inject(options);
+ isInitialized() {
+ return this.options !== null;
+ }
+
+ async setOptions(options) {
+ this.options = options;
+ this.updateTheme();
+ }
+
+ async showContent(elementRect, writingMode, type=null, details=null) {
+ if (!this.isInitialized()) { return; }
+ await this.show(elementRect, writingMode);
+ if (type === null) { return; }
+ this.invokeApi('setContent', {type, details});
+ }
- const optionsGeneral = options.general;
+ async show(elementRect, writingMode) {
+ await this.inject();
+
+ const optionsGeneral = this.options.general;
const container = this.container;
const containerRect = container.getBoundingClientRect();
const getPosition = (
@@ -208,11 +225,6 @@ class Popup {
return [position, size, after];
}
- async showOrphaned(elementRect, writingMode, options) {
- await this.show(elementRect, writingMode, options);
- this.invokeApi('orphaned');
- }
-
hide(changeFocus) {
if (!this.isVisible()) {
return;
@@ -227,6 +239,10 @@ class Popup {
}
}
+ async isVisibleAsync() {
+ return this.isVisible();
+ }
+
isVisible() {
return this.isInjected && (this.visibleOverride !== null ? this.visibleOverride : this.visible);
}
@@ -261,6 +277,44 @@ class Popup {
}
}
+ updateTheme() {
+ this.container.dataset.yomichanTheme = this.options.general.popupOuterTheme;
+ this.container.dataset.yomichanSiteColor = this.getSiteColor();
+ }
+
+ getSiteColor() {
+ const color = [255, 255, 255];
+ Popup.addColor(color, Popup.getColorInfo(window.getComputedStyle(document.documentElement).backgroundColor));
+ Popup.addColor(color, Popup.getColorInfo(window.getComputedStyle(document.body).backgroundColor));
+ const dark = (color[0] < 128 && color[1] < 128 && color[2] < 128);
+ return dark ? 'dark' : 'light';
+ }
+
+ static addColor(target, color) {
+ if (color === null) { return; }
+
+ const a = color[3];
+ if (a <= 0.0) { return; }
+
+ const aInv = 1.0 - a;
+ for (let i = 0; i < 3; ++i) {
+ target[i] = target[i] * aInv + color[i] * a;
+ }
+ }
+
+ static getColorInfo(cssColor) {
+ const m = /^\s*rgba?\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*([\d\.]+)\s*)?\)\s*$/.exec(cssColor);
+ if (m === null) { return null; }
+
+ const m4 = m[4];
+ return [
+ Number.parseInt(m[1], 10),
+ Number.parseInt(m[2], 10),
+ Number.parseInt(m[3], 10),
+ m4 ? Math.max(0.0, Math.min(1.0, Number.parseFloat(m4))) : 1.0
+ ];
+ }
+
async containsPoint(x, y) {
for (let popup = this; popup !== null && popup.isVisible(); popup = popup.child) {
const rect = popup.container.getBoundingClientRect();
@@ -271,14 +325,25 @@ class Popup {
return false;
}
- async termsShow(elementRect, writingMode, definitions, options, context) {
- await this.show(elementRect, writingMode, options);
- this.invokeApi('termsShow', {definitions, options, context});
+ async setCustomCss(css) {
+ this.invokeApi('setCustomCss', {css});
}
- async kanjiShow(elementRect, writingMode, definitions, options, context) {
- await this.show(elementRect, writingMode, options);
- this.invokeApi('kanjiShow', {definitions, options, context});
+ async setCustomOuterCss(css, injectDirectly) {
+ // Cannot repeatedly inject stylesheets using web extension APIs since there is no way to remove them.
+ if (this.stylesheetInjectedViaApi) { return; }
+
+ if (injectDirectly || Popup.isOnExtensionPage()) {
+ Popup.injectOuterStylesheet(css);
+ } else {
+ if (!css) { return; }
+ try {
+ await apiInjectStylesheet(css);
+ this.stylesheetInjectedViaApi = true;
+ } catch (e) {
+ // NOP
+ }
+ }
}
clearAutoPlayTimer() {
@@ -322,4 +387,35 @@ class Popup {
get url() {
return window.location.href;
}
+
+ static isOnExtensionPage() {
+ try {
+ const url = chrome.runtime.getURL('/');
+ return window.location.href.substr(0, url.length) === url;
+ } catch (e) {
+ // NOP
+ }
+ }
+
+ static injectOuterStylesheet(css) {
+ if (Popup.outerStylesheet === null) {
+ if (!css) { return; }
+ Popup.outerStylesheet = document.createElement('style');
+ Popup.outerStylesheet.id = "yomichan-popup-outer-stylesheet";
+ }
+
+ const outerStylesheet = Popup.outerStylesheet;
+ if (css) {
+ outerStylesheet.textContent = css;
+
+ const par = document.head;
+ if (par && outerStylesheet.parentNode !== par) {
+ par.appendChild(outerStylesheet);
+ }
+ } else {
+ outerStylesheet.textContent = '';
+ }
+ }
}
+
+Popup.outerStylesheet = null;
diff --git a/ext/fg/js/source.js b/ext/fg/js/source.js
index ee4f58e2..c3da9f46 100644
--- a/ext/fg/js/source.js
+++ b/ext/fg/js/source.js
@@ -229,13 +229,29 @@ class TextSourceRange {
}
static getElementWritingMode(element) {
- if (element === null) {
- return 'horizontal-tb';
+ if (element !== null) {
+ const style = window.getComputedStyle(element);
+ const writingMode = style.writingMode;
+ if (typeof writingMode === 'string') {
+ return TextSourceRange.normalizeWritingMode(writingMode);
+ }
}
+ return 'horizontal-tb';
+ }
- const style = window.getComputedStyle(element);
- const writingMode = style.writingMode;
- return typeof writingMode === 'string' ? writingMode : 'horizontal-tb';
+ static normalizeWritingMode(writingMode) {
+ switch (writingMode) {
+ case 'lr':
+ case 'lr-tb':
+ case 'rl':
+ return 'horizontal-tb';
+ case 'tb':
+ return 'vertical-lr';
+ case 'tb-rl':
+ return 'vertical-rl';
+ default:
+ return writingMode;
+ }
}
static getNodesInRange(range) {
diff --git a/ext/manifest.json b/ext/manifest.json
index c69b556f..d4a72c8a 100644
--- a/ext/manifest.json
+++ b/ext/manifest.json
@@ -1,7 +1,7 @@
{
"manifest_version": 2,
"name": "Yomichan (testing)",
- "version": "1.8.8",
+ "version": "1.8.9",
"description": "Japanese dictionary with Anki integration (testing)",
"icons": {"16": "mixed/img/icon16.png", "48": "mixed/img/icon48.png", "128": "mixed/img/icon128.png"},
@@ -26,7 +26,8 @@
"fg/js/source.js",
"fg/js/util.js",
"fg/js/popup-proxy-host.js",
- "fg/js/frontend.js"
+ "fg/js/frontend.js",
+ "fg/js/frontend-initialize.js"
],
"css": ["fg/css/client.css"],
"all_frames": true
@@ -40,7 +41,8 @@
"permissions": [
"<all_urls>",
"storage",
- "clipboardWrite"
+ "clipboardWrite",
+ "unlimitedStorage"
],
"commands": {
"toggle": {
diff --git a/ext/mixed/css/display-dark.css b/ext/mixed/css/display-dark.css
new file mode 100644
index 00000000..34a0ccd1
--- /dev/null
+++ b/ext/mixed/css/display-dark.css
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2019 Alex Yatskov <alex@foosoft.net>
+ * Author: Alex Yatskov <alex@foosoft.net>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the entrys 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 <http://www.gnu.org/licenses/>.
+ */
+
+
+body { background-color: #1e1e1e; color: #d4d4d4; }
+
+hr { border-top-color: #2f2f2f; }
+
+.tag-default { background-color: #69696e; }
+.tag-name { background-color: #489148; }
+.tag-expression { background-color: #b07f39; }
+.tag-popular { background-color: #025caa; }
+.tag-frequent { background-color: #4490a7; }
+.tag-archaism { background-color: #b04340; }
+.tag-dictionary { background-color: #9057ad; }
+.tag-frequency { background-color: #489148; }
+.tag-partOfSpeech { background-color: #565656; }
+
+.reasons { color: #888888; }
+.glossary li { color: #888888; }
+.glossary-item { color: #d4d4d4; }
+.label { color: #e1e1e1; }
+
+.expression .kanji-link {
+ border-bottom-color: #888888;
+ color: #CCCCCC;
+}
+
+.expression-popular, .expression-popular .kanji-link {
+ color: #0275d8;
+}
+
+.expression-rare, .expression-rare .kanji-link {
+ color: #666666;
+}
diff --git a/ext/mixed/css/display-default.css b/ext/mixed/css/display-default.css
new file mode 100644
index 00000000..176c5387
--- /dev/null
+++ b/ext/mixed/css/display-default.css
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2019 Alex Yatskov <alex@foosoft.net>
+ * Author: Alex Yatskov <alex@foosoft.net>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the entrys 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 <http://www.gnu.org/licenses/>.
+ */
+
+
+body { background-color: #ffffff; color: #333333; }
+
+hr { border-top-color: #eeeeee; }
+
+.tag-default { background-color: #8a8a91; }
+.tag-name { background-color: #5cb85c; }
+.tag-expression { background-color: #f0ad4e; }
+.tag-popular { background-color: #0275d8; }
+.tag-frequent { background-color: #5bc0de; }
+.tag-archaism { background-color: #d9534f; }
+.tag-dictionary { background-color: #aa66cc; }
+.tag-frequency { background-color: #5cb85c; }
+.tag-partOfSpeech { background-color: #565656; }
+
+.reasons { color: #777777; }
+.glossary li { color: #777777; }
+.glossary-item { color: #000000; }
+.label { color: #ffffff; }
+
+.expression .kanji-link {
+ border-bottom-color: #777777;
+ color: #333333;
+}
+
+.expression-popular, .expression-popular .kanji-link {
+ color: #0275d8;
+}
+
+.expression-rare, .expression-rare .kanji-link {
+ color: #999999;
+}
diff --git a/ext/mixed/css/display.css b/ext/mixed/css/display.css
index 8a4cf4a7..7793ddeb 100644
--- a/ext/mixed/css/display.css
+++ b/ext/mixed/css/display.css
@@ -30,9 +30,31 @@
* General
*/
+html:root[data-yomichan-page=float]:not([data-yomichan-theme]),
+html:root[data-yomichan-page=float]:not([data-yomichan-theme]) body {
+ background-color: transparent;
+}
+
+body {
+ font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+ font-size: 14px;
+ line-height: 1.42857143;
+ margin: 0;
+ border: 0;
+ padding: 0;
+}
+
hr {
padding: 0px;
margin: 0px;
+ border: 0;
+ border-top-width: 1px;
+ border-top-style: solid;
+}
+
+ol, ul {
+ margin-top: 0;
+ margin-bottom: 10px;
}
#spinner {
@@ -60,40 +82,10 @@ hr {
padding-bottom: 10px;
}
-.tag-default {
- background-color: #8a8a91;
-}
-
-.tag-name {
- background-color: #5cb85c;
-}
-
-.tag-expression {
- background-color: #f0ad4e;
-}
-
-.tag-popular {
- background-color: #0275d8;
-}
-
-.tag-frequent {
- background-color: #5bc0de;
-}
-
-.tag-archaism {
- background-color: #d9534f;
-}
-
-.tag-dictionary {
- background-color: #aa66cc;
-}
-
-.tag-frequency {
- background-color: #5cb85c;
-}
-
-.tag-partOfSpeech {
- background-color: #565656;
+html:root[data-yomichan-page=float] .entry,
+html:root[data-yomichan-page=float] .note {
+ padding-left: 10px;
+ padding-right: 10px;
}
.actions .disabled {
@@ -103,6 +95,7 @@ hr {
.actions .disabled img {
-webkit-filter: grayscale(100%);
+ filter: grayscale(100%);
opacity: 0.25;
}
@@ -111,7 +104,7 @@ hr {
}
.actions {
- display: inline-block;
+ display: block;
float: right;
}
@@ -127,19 +120,11 @@ hr {
}
.expression .kanji-link {
- border-bottom: 1px #777 dashed;
- color: #333;
+ border-bottom-width: 1px;
+ border-bottom-style: dashed;
text-decoration: none;
}
-.expression-popular, .expression-popular .kanji-link {
- color: #0275d8;
-}
-
-.expression-rare, .expression-rare .kanji-link {
- color: #999;
-}
-
.expression .peek-wrapper {
font-size: 14px;
white-space: nowrap;
@@ -173,7 +158,6 @@ hr {
}
.reasons {
- color: #777;
display: inline-block;
}
@@ -199,14 +183,6 @@ hr {
content: " | ";
}
-.glossary li {
- color: #777;
-}
-
-.glossary-item {
- color: #000;
-}
-
div.glossary-item.compact-glossary {
display: inline;
}
@@ -234,3 +210,15 @@ div.glossary-item.compact-glossary {
.entry:not(.entry-current) .current {
display: none;
}
+
+.label {
+ display: inline;
+ padding: 0.2em 0.6em 0.3em;
+ font-size: 75%;
+ font-weight: 700;
+ line-height: 1;
+ text-align: center;
+ white-space: nowrap;
+ vertical-align: baseline;
+ border-radius: 0.25em;
+}
diff --git a/ext/mixed/js/audio.js b/ext/mixed/js/audio.js
index b905140c..cf8b8d24 100644
--- a/ext/mixed/js/audio.js
+++ b/ext/mixed/js/audio.js
@@ -17,7 +17,90 @@
*/
-function audioGetFromUrl(url) {
+class TextToSpeechAudio {
+ constructor(text, voice) {
+ this.text = text;
+ this.voice = voice;
+ this._utterance = null;
+ this._volume = 1;
+ }
+
+ get currentTime() {
+ return 0;
+ }
+ set currentTime(value) {
+ // NOP
+ }
+
+ get volume() {
+ return this._volume;
+ }
+ set volume(value) {
+ this._volume = value;
+ if (this._utterance !== null) {
+ this._utterance.volume = value;
+ }
+ }
+
+ play() {
+ try {
+ if (this._utterance === null) {
+ this._utterance = new SpeechSynthesisUtterance(this.text || '');
+ this._utterance.lang = 'ja-JP';
+ this._utterance.volume = this._volume;
+ this._utterance.voice = this.voice;
+ }
+
+ speechSynthesis.cancel();
+ speechSynthesis.speak(this._utterance);
+
+ } catch (e) {
+ // NOP
+ }
+ }
+
+ pause() {
+ try {
+ speechSynthesis.cancel();
+ } catch (e) {
+ // NOP
+ }
+ }
+
+ static createFromUri(ttsUri) {
+ const m = /^tts:[^#\?]*\?([^#]*)/.exec(ttsUri);
+ if (m === null) { return null; }
+
+ const searchParameters = {};
+ for (const group of m[1].split('&')) {
+ const sep = group.indexOf('=');
+ if (sep < 0) { continue; }
+ searchParameters[decodeURIComponent(group.substr(0, sep))] = decodeURIComponent(group.substr(sep + 1));
+ }
+
+ if (!searchParameters.text) { return null; }
+
+ const voice = audioGetTextToSpeechVoice(searchParameters.voice);
+ if (voice === null) { return null; }
+
+ return new TextToSpeechAudio(searchParameters.text, voice);
+ }
+
+}
+
+function audioGetFromUrl(url, download) {
+ const tts = TextToSpeechAudio.createFromUri(url);
+ if (tts !== null) {
+ if (download) {
+ throw new Error('Download not supported for text-to-speech');
+ }
+ return Promise.resolve(tts);
+ }
+
+ if (download) {
+ return Promise.resolve(null);
+ }
+
return new Promise((resolve, reject) => {
const audio = new Audio(url);
audio.addEventListener('loadeddata', () => {
@@ -32,7 +115,7 @@ function audioGetFromUrl(url) {
});
}
-async function audioGetFromSources(expression, sources, optionsContext, createAudioObject, cache=null) {
+async function audioGetFromSources(expression, sources, optionsContext, download, cache=null) {
const key = `${expression.expression}:${expression.reading}`;
if (cache !== null && cache.hasOwnProperty(expression)) {
return cache[key];
@@ -46,7 +129,7 @@ async function audioGetFromSources(expression, sources, optionsContext, createAu
}
try {
- const audio = createAudioObject ? await audioGetFromUrl(url) : null;
+ const audio = await audioGetFromUrl(url, download);
const result = {audio, url, source};
if (cache !== null) {
cache[key] = result;
@@ -56,5 +139,42 @@ async function audioGetFromSources(expression, sources, optionsContext, createAu
// NOP
}
}
- return {audio: null, source: null};
+ return {audio: null, url: null, source: null};
+}
+
+function audioGetTextToSpeechVoice(voiceURI) {
+ try {
+ for (const voice of speechSynthesis.getVoices()) {
+ if (voice.voiceURI === voiceURI) {
+ return voice;
+ }
+ }
+ } catch (e) {
+ // NOP
+ }
+ return null;
+}
+
+function audioPrepareTextToSpeech(options) {
+ if (
+ audioPrepareTextToSpeech.state ||
+ !options.audio.textToSpeechVoice ||
+ !(
+ options.audio.sources.includes('text-to-speech') ||
+ options.audio.sources.includes('text-to-speech-reading')
+ )
+ ) {
+ // Text-to-speech not in use.
+ return;
+ }
+
+ // Chrome needs this value called once before it will become populated.
+ // The first call will return an empty list.
+ audioPrepareTextToSpeech.state = true;
+ try {
+ speechSynthesis.getVoices();
+ } catch (e) {
+ // NOP
+ }
}
+audioPrepareTextToSpeech.state = false;
diff --git a/ext/mixed/js/display.js b/ext/mixed/js/display.js
index 22181301..b40228b0 100644
--- a/ext/mixed/js/display.js
+++ b/ext/mixed/js/display.js
@@ -29,15 +29,16 @@ class Display {
this.audioPlaying = null;
this.audioFallback = null;
this.audioCache = {};
- this.optionsContext = {};
- this.eventListeners = [];
+ this.styleNode = null;
- this.dependencies = {};
+ this.eventListeners = [];
+ this.persistentEventListeners = [];
+ this.interactive = false;
+ this.eventListenersActive = false;
this.windowScroll = new WindowScroll();
- document.addEventListener('keydown', this.onKeyDown.bind(this));
- document.addEventListener('wheel', this.onWheel.bind(this), {passive: false});
+ this.setInteractive(true);
}
onError(error) {
@@ -73,8 +74,8 @@ class Display {
context.source.source = this.context.source;
}
- const kanjiDefs = await apiKanjiFind(link.textContent, this.optionsContext);
- this.kanjiShow(kanjiDefs, this.options, context);
+ const definitions = await apiKanjiFind(link.textContent, this.getOptionsContext());
+ this.setContentKanji(definitions, context);
} catch (e) {
this.onError(e);
}
@@ -84,8 +85,6 @@ class Display {
try {
e.preventDefault();
- const {docRangeFromPoint, docSentenceExtract} = this.dependencies;
-
const clickedElement = e.target;
const textSource = docRangeFromPoint(e.clientX, e.clientY, this.options);
if (textSource === null) {
@@ -96,7 +95,7 @@ class Display {
try {
textSource.setEndOffset(this.options.scanning.length);
- ({definitions, length} = await apiTermsFind(textSource.text(), this.optionsContext));
+ ({definitions, length} = await apiTermsFind(textSource.text(), this.getOptionsContext()));
if (definitions.length === 0) {
return false;
}
@@ -123,7 +122,7 @@ class Display {
context.source.source = this.context.source;
}
- this.termsShow(definitions, this.options, context);
+ this.setContentTerms(definitions, context);
} catch (e) {
this.onError(e);
}
@@ -172,16 +171,124 @@ class Display {
}
}
- async termsShow(definitions, options, context) {
+ onRuntimeMessage({action, params}, sender, callback) {
+ const handlers = Display.runtimeMessageHandlers;
+ if (handlers.hasOwnProperty(action)) {
+ const handler = handlers[action];
+ const result = handler(this, params);
+ callback(result);
+ }
+ }
+
+ getOptionsContext() {
+ throw new Error('Override me');
+ }
+
+ isInitialized() {
+ return this.options !== null;
+ }
+
+ async initialize(options=null) {
+ await this.updateOptions(options);
+ chrome.runtime.onMessage.addListener(this.onRuntimeMessage.bind(this));
+ }
+
+ async updateOptions(options) {
+ this.options = options ? options : await apiOptionsGet(this.getOptionsContext());
+ this.updateTheme(this.options.general.popupTheme);
+ this.setCustomCss(this.options.general.customPopupCss);
+ audioPrepareTextToSpeech(this.options);
+ }
+
+ updateTheme(themeName) {
+ document.documentElement.dataset.yomichanTheme = themeName;
+
+ const stylesheets = document.querySelectorAll('link[data-yomichan-theme-name]');
+ for (const stylesheet of stylesheets) {
+ const match = (stylesheet.dataset.yomichanThemeName === themeName);
+ stylesheet.rel = (match ? 'stylesheet' : 'stylesheet alternate');
+ }
+ }
+
+ setCustomCss(css) {
+ if (this.styleNode === null) {
+ if (css.length === 0) { return; }
+ this.styleNode = document.createElement('style');
+ }
+
+ this.styleNode.textContent = css;
+
+ const parent = document.head;
+ if (this.styleNode.parentNode !== parent) {
+ parent.appendChild(this.styleNode);
+ }
+ }
+
+ setInteractive(interactive) {
+ interactive = !!interactive;
+ if (this.interactive === interactive) { return; }
+ this.interactive = interactive;
+
+ if (interactive) {
+ Display.addEventListener(this.persistentEventListeners, document, 'keydown', this.onKeyDown.bind(this), false);
+ Display.addEventListener(this.persistentEventListeners, document, 'wheel', this.onWheel.bind(this), {passive: false});
+ } else {
+ Display.clearEventListeners(this.persistentEventListeners);
+ }
+ this.setEventListenersActive(this.eventListenersActive);
+ }
+
+ setEventListenersActive(active) {
+ active = !!active && this.interactive;
+ if (this.eventListenersActive === active) { return; }
+ this.eventListenersActive = active;
+
+ if (active) {
+ this.addEventListeners('.action-add-note', 'click', this.onNoteAdd.bind(this));
+ this.addEventListeners('.action-view-note', 'click', this.onNoteView.bind(this));
+ this.addEventListeners('.action-play-audio', 'click', this.onAudioPlay.bind(this));
+ this.addEventListeners('.kanji-link', 'click', this.onKanjiLookup.bind(this));
+ this.addEventListeners('.source-term', 'click', this.onSourceTermView.bind(this));
+ if (this.options.scanning.enablePopupSearch) {
+ this.addEventListeners('.glossary-item', 'click', this.onTermLookup.bind(this));
+ }
+ } else {
+ Display.clearEventListeners(this.eventListeners);
+ }
+ }
+
+ addEventListeners(selector, type, listener, options) {
+ this.container.querySelectorAll(selector).forEach((node) => {
+ Display.addEventListener(this.eventListeners, node, type, listener, options);
+ });
+ }
+
+ setContent(type, details) {
+ switch (type) {
+ case 'terms':
+ return this.setContentTerms(details.definitions, details.context);
+ case 'kanji':
+ return this.setContentKanji(details.definitions, details.context);
+ case 'orphaned':
+ return this.setContentOrphaned();
+ default:
+ return Promise.resolve();
+ }
+ }
+
+ async setContentTerms(definitions, context) {
+ if (!this.isInitialized()) { return; }
+
try {
- this.clearEventListeners();
+ const options = this.options;
+
+ this.setEventListenersActive(false);
if (!context || context.focus !== false) {
window.focus();
}
this.definitions = definitions;
- this.options = options;
this.context = context;
const sequence = ++this.sequence;
@@ -211,18 +318,11 @@ class Display {
const {index, scroll} = context || {};
this.entryScrollIntoView(index || 0, scroll);
- if (this.options.audio.enabled && this.options.audio.autoPlay) {
+ if (options.audio.enabled && options.audio.autoPlay) {
this.autoPlayAudio();
}
- this.addEventListeners('.action-add-note', 'click', this.onNoteAdd.bind(this));
- this.addEventListeners('.action-view-note', 'click', this.onNoteView.bind(this));
- this.addEventListeners('.action-play-audio', 'click', this.onAudioPlay.bind(this));
- this.addEventListeners('.kanji-link', 'click', this.onKanjiLookup.bind(this));
- this.addEventListeners('.source-term', 'click', this.onSourceTermView.bind(this));
- if (this.options.scanning.enablePopupSearch) {
- this.addEventListeners('.glossary-item', 'click', this.onTermLookup.bind(this));
- }
+ this.setEventListenersActive(true);
await this.adderButtonUpdate(['term-kanji', 'term-kana'], sequence);
} catch (e) {
@@ -230,16 +330,19 @@ class Display {
}
}
- async kanjiShow(definitions, options, context) {
+ async setContentKanji(definitions, context) {
+ if (!this.isInitialized()) { return; }
+
try {
- this.clearEventListeners();
+ const options = this.options;
+
+ this.setEventListenersActive(false);
if (!context || context.focus !== false) {
window.focus();
}
this.definitions = definitions;
- this.options = options;
this.context = context;
const sequence = ++this.sequence;
@@ -265,9 +368,7 @@ class Display {
const {index, scroll} = context || {};
this.entryScrollIntoView(index || 0, scroll);
- this.addEventListeners('.action-add-note', 'click', this.onNoteAdd.bind(this));
- this.addEventListeners('.action-view-note', 'click', this.onNoteView.bind(this));
- this.addEventListeners('.source-term', 'click', this.onSourceTermView.bind(this));
+ this.setEventListenersActive(true);
await this.adderButtonUpdate(['kanji'], sequence);
} catch (e) {
@@ -275,13 +376,26 @@ class Display {
}
}
+ async setContentOrphaned() {
+ const definitions = document.querySelector('#definitions');
+ const errorOrphaned = document.querySelector('#error-orphaned');
+
+ if (definitions !== null) {
+ definitions.style.setProperty('display', 'none', 'important');
+ }
+
+ if (errorOrphaned !== null) {
+ errorOrphaned.style.setProperty('display', 'block', 'important');
+ }
+ }
+
autoPlayAudio() {
this.audioPlay(this.definitions[0], this.firstExpressionIndex, 0);
}
async adderButtonUpdate(modes, sequence) {
try {
- const states = await apiDefinitionsAddable(this.definitions, modes, this.optionsContext);
+ const states = await apiDefinitionsAddable(this.definitions, modes, this.getOptionsContext());
if (!states || sequence !== this.sequence) {
return;
}
@@ -353,7 +467,7 @@ class Display {
source: this.context.source.source
};
- this.termsShow(this.context.source.definitions, this.options, context);
+ this.setContentTerms(this.context.source.definitions, context);
}
}
@@ -383,7 +497,7 @@ class Display {
}
}
- const noteId = await apiDefinitionAdd(definition, mode, context, this.optionsContext);
+ const noteId = await apiDefinitionAdd(definition, mode, context, this.getOptionsContext());
if (noteId) {
const index = this.definitions.indexOf(definition);
const adderButton = this.adderButtonFind(index, mode);
@@ -413,7 +527,7 @@ class Display {
}
const sources = this.options.audio.sources;
- let {audio, source} = await audioGetFromSources(expression, sources, this.optionsContext, true, this.audioCache);
+ let {audio, source} = await audioGetFromSources(expression, sources, this.getOptionsContext(), false, this.audioCache);
let info;
if (audio === null) {
if (this.audioFallback === null) {
@@ -544,18 +658,16 @@ class Display {
return -1;
}
- addEventListeners(selector, type, listener, options) {
- this.container.querySelectorAll(selector).forEach((node) => {
- node.addEventListener(type, listener, options);
- this.eventListeners.push([node, type, listener, options]);
- });
+ static addEventListener(eventListeners, object, type, listener, options) {
+ object.addEventListener(type, listener, options);
+ eventListeners.push([object, type, listener, options]);
}
- clearEventListeners() {
- for (const [node, type, listener, options] of this.eventListeners) {
- node.removeEventListener(type, listener, options);
+ static clearEventListeners(eventListeners) {
+ for (const [object, type, listener, options] of eventListeners) {
+ object.removeEventListener(type, listener, options);
}
- this.eventListeners = [];
+ eventListeners.length = 0;
}
static getElementTop(element) {
@@ -675,3 +787,7 @@ Display.onKeyDownHandlers = {
return false;
}
};
+
+Display.runtimeMessageHandlers = {
+ optionsUpdate: (self) => self.updateOptions(null)
+};
diff --git a/tmpl/terms.html b/tmpl/terms.html
index 0b967bb5..3dee43d7 100644
--- a/tmpl/terms.html
+++ b/tmpl/terms.html
@@ -1,29 +1,31 @@
{{#*inline "definition"}}
-{{#if definitionTags}}
-<div {{#if compactGlossaries}}class="compact-info"{{/if}}>
- {{#each definitionTags}}
- <span class="label label-default tag-{{category}}" title="{{notes}}">{{name}}</span>
- {{/each}}
-</div>
-{{/if}}
-{{#if only}}
-<div {{#if compactGlossaries}}class="compact-info"{{/if}}>
- (
- {{~#each only~}}
- {{{.}}}{{#unless @last}}, {{/unless}}
- {{/each}}
- only)
+<div class="dict-{{#sanitizeCssClass}}{{dictionary}}{{/sanitizeCssClass}}">
+ {{#if definitionTags}}
+ <div {{#if compactGlossaries}}class="compact-info"{{/if}}>
+ {{#each definitionTags}}
+ <span class="label label-default tag-{{category}}" title="{{notes}}">{{name}}</span>
+ {{/each}}
+ </div>
+ {{/if}}
+ {{#if only}}
+ <div {{#if compactGlossaries}}class="compact-info"{{/if}}>
+ (
+ {{~#each only~}}
+ {{{.}}}{{#unless @last}}, {{/unless}}
+ {{/each}}
+ only)
+ </div>
+ {{/if}}
+ {{#if glossary.[1]}}
+ <ul {{#if compactGlossaries}}class="compact-glossary"{{/if}}>
+ {{#each glossary}}
+ <li><span class="glossary-item">{{#multiLine}}{{.}}{{/multiLine}}</span></li>
+ {{/each}}
+ </ul>
+ {{else}}
+ <div class="glossary-item {{#if compactGlossaries}}compact-glossary{{/if}}">{{#multiLine}}{{glossary.[0]}}{{/multiLine}}</div>
+ {{/if}}
</div>
-{{/if}}
-{{#if glossary.[1]}}
-<ul {{#if compactGlossaries}}class="compact-glossary"{{/if}}>
- {{#each glossary}}
- <li><span class="glossary-item">{{#multiLine}}{{.}}{{/multiLine}}</span></li>
- {{/each}}
-</ul>
-{{else}}
-<div class="glossary-item {{#if compactGlossaries}}compact-glossary{{/if}}">{{#multiLine}}{{glossary.[0]}}{{/multiLine}}</div>
-{{/if}}
{{/inline}}
{{#*inline "term"}}