aboutsummaryrefslogtreecommitdiff
path: root/ext
diff options
context:
space:
mode:
Diffstat (limited to 'ext')
-rw-r--r--ext/bg/background.html1
-rw-r--r--ext/bg/context.html1
-rw-r--r--ext/bg/guide.html1
-rw-r--r--ext/bg/js/audio.js29
-rw-r--r--ext/bg/js/backend.js26
-rw-r--r--ext/bg/js/database.js68
-rw-r--r--ext/bg/js/options.js2
-rw-r--r--ext/bg/js/settings.js201
-rw-r--r--ext/bg/js/util.js4
-rw-r--r--ext/bg/legal.html1
-rw-r--r--ext/bg/search.html1
-rw-r--r--ext/bg/settings.html68
-rw-r--r--ext/fg/css/client.css22
-rw-r--r--ext/fg/float.html1
-rw-r--r--ext/fg/js/document.js12
-rw-r--r--ext/fg/js/frontend.js196
-rw-r--r--ext/fg/js/popup.js4
-rw-r--r--ext/fg/js/util.js12
-rw-r--r--ext/manifest.json4
19 files changed, 574 insertions, 80 deletions
diff --git a/ext/bg/background.html b/ext/bg/background.html
index 97b20f46..3262f2a1 100644
--- a/ext/bg/background.html
+++ b/ext/bg/background.html
@@ -2,6 +2,7 @@
<html lang="en">
<head>
<meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
</head>
<body>
<script src="/mixed/lib/dexie.min.js"></script>
diff --git a/ext/bg/context.html b/ext/bg/context.html
index 8a72acc7..01b4fb30 100644
--- a/ext/bg/context.html
+++ b/ext/bg/context.html
@@ -2,6 +2,7 @@
<html lang="en">
<head>
<meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
<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/lib/bootstrap-toggle/bootstrap-toggle.min.css">
diff --git a/ext/bg/guide.html b/ext/bg/guide.html
index 4b01ae7c..6f98d264 100644
--- a/ext/bg/guide.html
+++ b/ext/bg/guide.html
@@ -2,6 +2,7 @@
<html lang="en">
<head>
<meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Welcome to Yomichan!</title>
<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">
diff --git a/ext/bg/js/audio.js b/ext/bg/js/audio.js
index 549288f5..2e5db7cc 100644
--- a/ext/bg/js/audio.js
+++ b/ext/bg/js/audio.js
@@ -69,10 +69,10 @@ async function audioBuildUrl(definition, mode, cache={}) {
const dom = new DOMParser().parseFromString(response, 'text/html');
for (const row of dom.getElementsByClassName('dc-result-row')) {
try {
- const url = row.getElementsByClassName('ill-onebuttonplayer').item(0).getAttribute('data-url');
+ const url = row.querySelector('audio>source[src]').getAttribute('src');
const reading = row.getElementsByClassName('dc-vocab_kana').item(0).innerText;
if (url && reading && (!definition.reading || definition.reading === reading)) {
- return url;
+ return audioUrlNormalize(url, 'https://www.japanesepod101.com', '/learningcenter/reference/');
}
} catch (e) {
// NOP
@@ -86,7 +86,7 @@ async function audioBuildUrl(definition, mode, cache={}) {
resolve(response);
} else {
const xhr = new XMLHttpRequest();
- xhr.open('GET', `http://jisho.org/search/${definition.expression}`);
+ xhr.open('GET', `https://jisho.org/search/${definition.expression}`);
xhr.addEventListener('error', () => reject('Failed to scrape audio data'));
xhr.addEventListener('load', () => {
cache[definition.expression] = xhr.responseText;
@@ -100,7 +100,10 @@ async function audioBuildUrl(definition, mode, cache={}) {
const dom = new DOMParser().parseFromString(response, 'text/html');
const audio = dom.getElementById(`audio_${definition.expression}:${definition.reading}`);
if (audio) {
- return audio.getElementsByTagName('source').item(0).getAttribute('src');
+ const url = audio.getElementsByTagName('source').item(0).getAttribute('src');
+ if (url) {
+ return audioUrlNormalize(url, 'https://jisho.org', '/search/');
+ }
}
} catch (e) {
// NOP
@@ -112,6 +115,24 @@ async function audioBuildUrl(definition, mode, cache={}) {
}
}
+function audioUrlNormalize(url, baseUrl, basePath) {
+ if (url) {
+ if (url[0] === '/') {
+ if (url.length >= 2 && url[1] === '/') {
+ // Begins with "//"
+ url = baseUrl.substr(0, baseUrl.indexOf(':') + 1) + url;
+ } else {
+ // Begins with "/"
+ url = baseUrl + url;
+ }
+ } else if (!/^[a-z][a-z0-9\+\-\.]*:/i.test(url)) {
+ // No URI scheme => relative path
+ url = baseUrl + basePath + url;
+ }
+ }
+ return url;
+}
+
function audioBuildFilename(definition) {
if (definition.reading || definition.expression) {
let filename = 'yomichan';
diff --git a/ext/bg/js/backend.js b/ext/bg/js/backend.js
index 01340419..c191a150 100644
--- a/ext/bg/js/backend.js
+++ b/ext/bg/js/backend.js
@@ -28,7 +28,9 @@ class Backend {
await this.translator.prepare();
await apiOptionsSet(await optionsLoad());
- chrome.commands.onCommand.addListener(this.onCommand.bind(this));
+ if (chrome.commands !== null && typeof chrome.commands === 'object') {
+ chrome.commands.onCommand.addListener(this.onCommand.bind(this));
+ }
chrome.runtime.onMessage.addListener(this.onMessage.bind(this));
if (this.options.general.showGuide) {
@@ -40,13 +42,13 @@ class Backend {
this.options = utilIsolate(options);
if (!options.general.enable) {
- chrome.browserAction.setBadgeBackgroundColor({color: '#555555'});
- chrome.browserAction.setBadgeText({text: 'off'});
+ this.setExtensionBadgeBackgroundColor('#555555');
+ this.setExtensionBadgeText('off');
} else if (!dictConfigured(options)) {
- chrome.browserAction.setBadgeBackgroundColor({color: '#f0ad4e'});
- chrome.browserAction.setBadgeText({text: '!'});
+ this.setExtensionBadgeBackgroundColor('#f0ad4e');
+ this.setExtensionBadgeText('!');
} else {
- chrome.browserAction.setBadgeText({text: ''});
+ this.setExtensionBadgeText('');
}
if (options.anki.enable) {
@@ -125,6 +127,18 @@ class Backend {
return true;
}
+
+ setExtensionBadgeBackgroundColor(color) {
+ if (typeof chrome.browserAction.setBadgeBackgroundColor === 'function') {
+ chrome.browserAction.setBadgeBackgroundColor({color});
+ }
+ }
+
+ setExtensionBadgeText(text) {
+ if (typeof chrome.browserAction.setBadgeText === 'function') {
+ chrome.browserAction.setBadgeText({text});
+ }
+ }
}
window.yomichan_backend = new Backend();
diff --git a/ext/bg/js/database.js b/ext/bg/js/database.js
index 3c7f6aab..093ec102 100644
--- a/ext/bg/js/database.js
+++ b/ext/bg/js/database.js
@@ -228,11 +228,47 @@ class Database {
}
}
- async importDictionary(archive, callback) {
+ async importDictionary(archive, progressCallback, exceptions) {
if (!this.db) {
throw 'Database not initialized';
}
+ const maxTransactionLength = 1000;
+ const bulkAdd = async (table, items, total, current) => {
+ if (items.length < maxTransactionLength) {
+ if (progressCallback) {
+ progressCallback(total, current);
+ }
+
+ try {
+ await table.bulkAdd(items);
+ } catch (e) {
+ if (exceptions) {
+ exceptions.push(e);
+ } else {
+ throw e;
+ }
+ }
+ } else {
+ for (let i = 0; i < items.length; i += maxTransactionLength) {
+ if (progressCallback) {
+ progressCallback(total, current + i / items.length);
+ }
+
+ let count = Math.min(maxTransactionLength, items.length - i);
+ try {
+ await table.bulkAdd(items.slice(i, i + count));
+ } catch (e) {
+ if (exceptions) {
+ exceptions.push(e);
+ } else {
+ throw e;
+ }
+ }
+ }
+ }
+ };
+
const indexDataLoaded = async summary => {
if (summary.version > 3) {
throw 'Unsupported dictionary version';
@@ -247,10 +283,6 @@ class Database {
};
const termDataLoaded = async (summary, entries, total, current) => {
- if (callback) {
- callback(total, current);
- }
-
const rows = [];
if (summary.version === 1) {
for (const [expression, reading, definitionTags, rules, score, ...glossary] of entries) {
@@ -280,14 +312,10 @@ class Database {
}
}
- await this.db.terms.bulkAdd(rows);
+ await bulkAdd(this.db.terms, rows, total, current);
};
const termMetaDataLoaded = async (summary, entries, total, current) => {
- if (callback) {
- callback(total, current);
- }
-
const rows = [];
for (const [expression, mode, data] of entries) {
rows.push({
@@ -298,14 +326,10 @@ class Database {
});
}
- await this.db.termMeta.bulkAdd(rows);
+ await bulkAdd(this.db.termMeta, rows, total, current);
};
const kanjiDataLoaded = async (summary, entries, total, current) => {
- if (callback) {
- callback(total, current);
- }
-
const rows = [];
if (summary.version === 1) {
for (const [character, onyomi, kunyomi, tags, ...meanings] of entries) {
@@ -332,14 +356,10 @@ class Database {
}
}
- await this.db.kanji.bulkAdd(rows);
+ await bulkAdd(this.db.kanji, rows, total, current);
};
const kanjiMetaDataLoaded = async (summary, entries, total, current) => {
- if (callback) {
- callback(total, current);
- }
-
const rows = [];
for (const [character, mode, data] of entries) {
rows.push({
@@ -350,14 +370,10 @@ class Database {
});
}
- await this.db.kanjiMeta.bulkAdd(rows);
+ await bulkAdd(this.db.kanjiMeta, rows, total, current);
};
const tagDataLoaded = async (summary, entries, total, current) => {
- if (callback) {
- callback(total, current);
- }
-
const rows = [];
for (const [name, category, order, notes, score] of entries) {
const row = dictTagSanitize({
@@ -372,7 +388,7 @@ class Database {
rows.push(row);
}
- await this.db.tagMeta.bulkAdd(rows);
+ await bulkAdd(this.db.tagMeta, rows, total, current);
};
return await Database.importDictionaryZip(
diff --git a/ext/bg/js/options.js b/ext/bg/js/options.js
index b5e1a27d..bad56db6 100644
--- a/ext/bg/js/options.js
+++ b/ext/bg/js/options.js
@@ -190,6 +190,7 @@ function optionsSetDefaults(options) {
debugInfo: false,
maxResults: 32,
showAdvanced: false,
+ popupDisplayMode: 'default',
popupWidth: 400,
popupHeight: 250,
popupHorizontalOffset: 0,
@@ -202,6 +203,7 @@ function optionsSetDefaults(options) {
scanning: {
middleMouse: true,
+ touchInputEnabled: true,
selectText: true,
alphanumeric: true,
autoHideResults: false,
diff --git a/ext/bg/js/settings.js b/ext/bg/js/settings.js
index b5c733e2..60a1886b 100644
--- a/ext/bg/js/settings.js
+++ b/ext/bg/js/settings.js
@@ -31,12 +31,14 @@ async function formRead() {
optionsNew.general.debugInfo = $('#show-debug-info').prop('checked');
optionsNew.general.showAdvanced = $('#show-advanced-options').prop('checked');
optionsNew.general.maxResults = parseInt($('#max-displayed-results').val(), 10);
+ optionsNew.general.popupDisplayMode = $('#popup-display-mode').val();
optionsNew.general.popupWidth = parseInt($('#popup-width').val(), 10);
optionsNew.general.popupHeight = parseInt($('#popup-height').val(), 10);
optionsNew.general.popupHorizontalOffset = parseInt($('#popup-horizontal-offset').val(), 0);
optionsNew.general.popupVerticalOffset = parseInt($('#popup-vertical-offset').val(), 10);
optionsNew.scanning.middleMouse = $('#middle-mouse-button-scan').prop('checked');
+ optionsNew.scanning.touchInputEnabled = $('#touch-input-enabled').prop('checked');
optionsNew.scanning.selectText = $('#select-matched-text').prop('checked');
optionsNew.scanning.alphanumeric = $('#search-alphanumeric').prop('checked');
optionsNew.scanning.autoHideResults = $('#auto-hide-results').prop('checked');
@@ -162,12 +164,14 @@ async function onReady() {
$('#show-debug-info').prop('checked', options.general.debugInfo);
$('#show-advanced-options').prop('checked', options.general.showAdvanced);
$('#max-displayed-results').val(options.general.maxResults);
+ $('#popup-display-mode').val(options.general.popupDisplayMode);
$('#popup-width').val(options.general.popupWidth);
$('#popup-height').val(options.general.popupHeight);
$('#popup-horizontal-offset').val(options.general.popupHorizontalOffset);
$('#popup-vertical-offset').val(options.general.popupVerticalOffset);
$('#middle-mouse-button-scan').prop('checked', options.scanning.middleMouse);
+ $('#touch-input-enabled').prop('checked', options.scanning.touchInputEnabled);
$('#select-matched-text').prop('checked', options.scanning.selectText);
$('#search-alphanumeric').prop('checked', options.scanning.alphanumeric);
$('#auto-hide-results').prop('checked', options.scanning.autoHideResults);
@@ -191,7 +195,7 @@ async function onReady() {
await dictionaryGroupsPopulate(options);
await formMainDictionaryOptionsPopulate(options);
} catch (e) {
- dictionaryErrorShow(e);
+ dictionaryErrorsShow([e]);
}
try {
@@ -201,6 +205,8 @@ async function onReady() {
}
formUpdateVisibility(options);
+
+ storageInfoInitialize();
}
$(document).ready(utilAsync(onReady));
@@ -210,36 +216,63 @@ $(document).ready(utilAsync(onReady));
* Dictionary
*/
-function dictionaryErrorShow(error) {
+function dictionaryErrorToString(error) {
+ if (error.toString) {
+ error = error.toString();
+ } else {
+ error = `${error}`;
+ }
+
+ for (const [match, subst] of dictionaryErrorToString.overrides) {
+ if (error.includes(match)) {
+ error = subst;
+ break;
+ }
+ }
+
+ return error;
+}
+dictionaryErrorToString.overrides = [
+ [
+ 'A mutation operation was attempted on a database that did not allow mutations.',
+ 'Access to IndexedDB appears to be restricted. Firefox seems to require that the history preference is set to "Remember history" before IndexedDB use of any kind is allowed.'
+ ],
+ [
+ 'The operation failed for reasons unrelated to the database itself and not covered by any other error code.',
+ 'Unable to access IndexedDB due to a possibly corrupt user profile. Try using the "Refresh Firefox" feature to reset your user profile.'
+ ],
+ [
+ 'BulkError',
+ 'Unable to finish importing dictionary data into IndexedDB. This may indicate that you do not have sufficient disk space available to complete this operation.'
+ ]
+];
+
+function dictionaryErrorsShow(errors) {
const dialog = $('#dict-error');
- if (error) {
- const overrides = [
- [
- 'A mutation operation was attempted on a database that did not allow mutations.',
- 'Access to IndexedDB appears to be restricted. Firefox seems to require that the history preference is set to "Remember history" before IndexedDB use of any kind is allowed.'
- ],
- [
- 'The operation failed for reasons unrelated to the database itself and not covered by any other error code.',
- 'Unable to access IndexedDB due to a possibly corrupt user profile. Try using the "Refresh Firefox" feature to reset your user profile.'
- ],
- [
- 'BulkError',
- 'Unable to finish importing dictionary data into IndexedDB. This may indicate that you do not have sufficient disk space available to complete this operation.'
- ]
- ];
-
- if (error.toString) {
- error = error.toString();
+ dialog.show().text('');
+
+ if (errors !== null && errors.length > 0) {
+ const uniqueErrors = {};
+ for (let e of errors) {
+ e = dictionaryErrorToString(e);
+ uniqueErrors[e] = uniqueErrors.hasOwnProperty(e) ? uniqueErrors[e] + 1 : 1;
}
- for (const [match, subst] of overrides) {
- if (error.includes(match)) {
- error = subst;
- break;
+ for (const e in uniqueErrors) {
+ const count = uniqueErrors[e];
+ const div = document.createElement('p');
+ if (count > 1) {
+ div.textContent = `${e} `;
+ const em = document.createElement('em');
+ em.textContent = `(${count})`;
+ div.appendChild(em);
+ } else {
+ div.textContent = `${e}`;
}
+ dialog.append($(div));
}
- dialog.show().text(error);
+ dialog.show();
} else {
dialog.hide();
}
@@ -315,7 +348,7 @@ async function onDictionaryPurge(e) {
const dictProgress = $('#dict-purge').show();
try {
- dictionaryErrorShow();
+ dictionaryErrorsShow(null);
dictionarySpinnerShow(true);
await utilDatabasePurge();
@@ -327,12 +360,16 @@ async function onDictionaryPurge(e) {
await dictionaryGroupsPopulate(options);
await formMainDictionaryOptionsPopulate(options);
} catch (e) {
- dictionaryErrorShow(e);
+ dictionaryErrorsShow([e]);
} finally {
dictionarySpinnerShow(false);
dictControls.show();
dictProgress.hide();
+
+ if (storageEstimate.mostRecent !== null) {
+ storageUpdateStats();
+ }
}
}
@@ -342,25 +379,37 @@ async function onDictionaryImport(e) {
const dictProgress = $('#dict-import-progress').show();
try {
- dictionaryErrorShow();
+ dictionaryErrorsShow(null);
dictionarySpinnerShow(true);
const setProgress = percent => dictProgress.find('.progress-bar').css('width', `${percent}%`);
- const updateProgress = (total, current) => setProgress(current / total * 100.0);
+ const updateProgress = (total, current) => {
+ setProgress(current / total * 100.0);
+ if (storageEstimate.mostRecent !== null && !storageUpdateStats.isUpdating) {
+ storageUpdateStats();
+ }
+ };
setProgress(0.0);
+ const exceptions = [];
const options = await optionsLoad();
- const summary = await utilDatabaseImport(e.target.files[0], updateProgress);
+ const summary = await utilDatabaseImport(e.target.files[0], updateProgress, exceptions);
options.dictionaries[summary.title] = {enabled: true, priority: 0, allowSecondarySearches: false};
if (summary.sequenced && options.general.mainDictionary === '') {
options.general.mainDictionary = summary.title;
}
+
+ if (exceptions.length > 0) {
+ exceptions.push(`Dictionary may not have been imported properly: ${exceptions.length} error${exceptions.length === 1 ? '' : 's'} reported.`);
+ dictionaryErrorsShow(exceptions);
+ }
+
await optionsSave(options);
await dictionaryGroupsPopulate(options);
await formMainDictionaryOptionsPopulate(options);
} catch (e) {
- dictionaryErrorShow(e);
+ dictionaryErrorsShow([e]);
} finally {
dictionarySpinnerShow(false);
@@ -518,3 +567,93 @@ async function onAnkiFieldTemplatesReset(e) {
ankiErrorShow(e);
}
}
+
+
+/*
+ * Storage
+ */
+
+async function getBrowser() {
+ if (typeof chrome !== "undefined") {
+ 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";
+ }
+ } else {
+ return "edge";
+ }
+}
+
+function storageBytesToLabeledString(size) {
+ const base = 1000;
+ const labels = ["bytes", "KB", "MB", "GB"];
+ let labelIndex = 0;
+ while (size >= base) {
+ size /= base;
+ ++labelIndex;
+ }
+ const label = size.toFixed(1);
+ return `${label}${labels[labelIndex]}`;
+}
+
+async function storageEstimate() {
+ try {
+ return (storageEstimate.mostRecent = await navigator.storage.estimate());
+ } catch (e) { }
+ return null;
+}
+storageEstimate.mostRecent = null;
+
+async function storageInfoInitialize() {
+ const browser = await getBrowser();
+ const container = document.querySelector("#storage-info");
+ container.setAttribute("data-browser", browser);
+
+ await storageShowInfo();
+
+ container.classList.remove("storage-hidden");
+
+ document.querySelector("#storage-refresh").addEventListener('click', () => storageShowInfo(), false);
+}
+
+async function storageUpdateStats() {
+ storageUpdateStats.isUpdating = true;
+
+ const estimate = await storageEstimate();
+ const valid = (estimate !== null);
+
+ if (valid) {
+ document.querySelector("#storage-usage").textContent = storageBytesToLabeledString(estimate.usage);
+ document.querySelector("#storage-quota").textContent = storageBytesToLabeledString(estimate.quota);
+ }
+
+ storageUpdateStats.isUpdating = false;
+ return valid;
+}
+storageUpdateStats.isUpdating = false;
+
+async function storageShowInfo() {
+ storageSpinnerShow(true);
+
+ const valid = await storageUpdateStats();
+ document.querySelector("#storage-use").classList.toggle("storage-hidden", !valid);
+ document.querySelector("#storage-error").classList.toggle("storage-hidden", valid);
+
+ storageSpinnerShow(false);
+}
+
+function storageSpinnerShow(show) {
+ const spinner = $('#storage-spinner');
+ if (show) {
+ spinner.show();
+ } else {
+ spinner.hide();
+ }
+}
diff --git a/ext/bg/js/util.js b/ext/bg/js/util.js
index 216cef3f..34b06ddb 100644
--- a/ext/bg/js/util.js
+++ b/ext/bg/js/util.js
@@ -87,6 +87,6 @@ function utilDatabasePurge() {
return utilBackend().translator.database.purge();
}
-function utilDatabaseImport(data, progress) {
- return utilBackend().translator.database.importDictionary(data, progress);
+function utilDatabaseImport(data, progress, exceptions) {
+ return utilBackend().translator.database.importDictionary(data, progress, exceptions);
}
diff --git a/ext/bg/legal.html b/ext/bg/legal.html
index a289281d..28c7fb21 100644
--- a/ext/bg/legal.html
+++ b/ext/bg/legal.html
@@ -2,6 +2,7 @@
<html lang="en">
<head>
<meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Yomichan Legal</title>
<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">
diff --git a/ext/bg/search.html b/ext/bg/search.html
index 5fbd467a..0d6c7cad 100644
--- a/ext/bg/search.html
+++ b/ext/bg/search.html
@@ -2,6 +2,7 @@
<html lang="en">
<head>
<meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Yomichan Search</title>
<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">
diff --git a/ext/bg/settings.html b/ext/bg/settings.html
index 5570b82f..1b4e5c84 100644
--- a/ext/bg/settings.html
+++ b/ext/bg/settings.html
@@ -2,13 +2,14 @@
<html lang="en">
<head>
<meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Yomichan Options</title>
<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">
<style>
#anki-spinner, #anki-general, #anki-error,
#dict-spinner, #dict-error, #dict-warning, #dict-purge, #dict-import-progress,
- #debug, .options-advanced {
+ #debug, .options-advanced, .storage-hidden, #storage-spinner {
display: none;
}
@@ -24,6 +25,21 @@
overflow-x: hidden;
white-space: pre;
}
+
+ .bottom-links {
+ padding-bottom: 1em;
+ }
+
+ [data-show-for-browser] {
+ 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] {
+ display: initial;
+ }
</style>
</head>
<body>
@@ -74,6 +90,14 @@
</select>
</div>
+ <div class="form-group">
+ <label for="popup-display-mode">Popup display mode</label>
+ <select class="form-control" id="popup-display-mode">
+ <option value="default">Default</option>
+ <option value="full-width">Full width</option>
+ </select>
+ </div>
+
<div class="form-group options-advanced">
<label for="audio-playback-volume">Audio playback volume (percent)</label>
<input type="number" min="0" max="100" id="audio-playback-volume" class="form-control">
@@ -109,6 +133,10 @@
</div>
<div class="checkbox">
+ <label><input type="checkbox" id="touch-input-enabled"> Touch input enabled</label>
+ </div>
+
+ <div class="checkbox">
<label><input type="checkbox" id="select-matched-text"> Select matched text</label>
</div>
@@ -183,6 +211,40 @@
</div>
</div>
+ <div id="storage-info" class="storage-hidden">
+ <div>
+ <img src="/mixed/img/spinner.gif" class="pull-right" id="storage-spinner" />
+ <h3>Storage</h3>
+ </div>
+
+ <div id="storage-use" class="storage-hidden">
+ <p class="help-block">
+ Yomichan is using approximately <strong id="storage-usage"></strong> of <strong id="storage-quota"></strong>.
+ </p>
+ </div>
+
+ <div id="storage-error" class="storage-hidden">
+ <p class="help-block">
+ Could not detect how much storage Yomichan is using.
+ </p>
+ <div data-show-for-browser="firefox firefox-mobile"><div class="alert alert-danger options-advanced">
+ On Firefox and Firefox for Android, the storage information feature may be hidden behind a browser flag.
+
+ If you would like to enable this flag, open <a href="about:config" target="_blank">about:config</a> and search for the
+ <strong>dom.storageManager.enabled</strong> option. If this option has a value of <strong>false</strong>, toggling it to
+ <strong>true</strong> may allow storage information to be calculated.
+ </div></div>
+ </div>
+
+ <div data-show-for-browser="firefox-mobile"><div class="alert alert-warning">
+ If you are using Firefox for Android, you will have to make sure you have enough free space on your device to install dictionaries.
+ </div></div>
+
+ <div>
+ <input type="button" value="Refresh" id="storage-refresh" />
+ </div>
+ </div>
+
<div>
<div>
<img src="/mixed/img/spinner.gif" class="pull-right" id="anki-spinner" alt>
@@ -301,8 +363,8 @@
<pre id="debug"></pre>
- <div class="pull-right">
- <small><a href="https://foosoft.net/projects/yomichan/" target="_blank">Homepage</a> &bull; <a href="legal.html">Legal</a></small>
+ <div class="pull-right bottom-links">
+ <small><a href="search.html">Search</a> &bull; <a href="https://foosoft.net/projects/yomichan/" target="_blank">Homepage</a> &bull; <a href="legal.html">Legal</a></small>
</div>
</div>
diff --git a/ext/fg/css/client.css b/ext/fg/css/client.css
index b5b1f6bd..a9b8e025 100644
--- a/ext/fg/css/client.css
+++ b/ext/fg/css/client.css
@@ -27,3 +27,25 @@ iframe#yomichan-float {
visibility: hidden;
z-index: 2147483647;
}
+
+iframe#yomichan-float.yomichan-float-full-width {
+ border-left: none;
+ border-right: none;
+ left: 0 !important;
+ right: 0 !important;
+ width: 100% !important;
+ box-sizing: border-box;
+ resize: none;
+}
+
+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 {
+ border-top: none;
+ top: 0 !important;
+ bottom: auto !important;
+}
diff --git a/ext/fg/float.html b/ext/fg/float.html
index 89872cce..fed7eeab 100644
--- a/ext/fg/float.html
+++ b/ext/fg/float.html
@@ -2,6 +2,7 @@
<html lang="en">
<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">
diff --git a/ext/fg/js/document.js b/ext/fg/js/document.js
index 9366832e..13acb036 100644
--- a/ext/fg/js/document.js
+++ b/ext/fg/js/document.js
@@ -85,14 +85,24 @@ function docRangeFromPoint(point) {
range.setEnd(position.offsetNode, position.offset);
return range;
}
+ return null;
};
}
const range = document.caretRangeFromPoint(point.x, point.y);
+ if (range === null) {
+ return;
+ }
if(imposter !== null) imposter.style.zIndex = -2147483646;
- const rect = range.getClientRects()[0];
+ const rects = range.getClientRects();
+
+ if (rects.length === 0) {
+ return;
+ }
+
+ const rect = rects[0];
if (point.y > rect.bottom + 2) {
return;
}
diff --git a/ext/fg/js/frontend.js b/ext/fg/js/frontend.js
index 266f9640..bd652f3b 100644
--- a/ext/fg/js/frontend.js
+++ b/ext/fg/js/frontend.js
@@ -26,6 +26,14 @@ class Frontend {
this.textSourceLast = null;
this.pendingLookup = false;
this.options = null;
+
+ this.primaryTouchIdentifier = null;
+ this.contextMenuChecking = false;
+ this.contextMenuPrevent = false;
+ this.contextMenuPreviousRange = null;
+ this.mouseDownPrevent = false;
+ this.clickPrevent = false;
+ this.scrollPrevent = false;
}
async prepare() {
@@ -39,6 +47,15 @@ class Frontend {
window.addEventListener('mouseup', this.onMouseUp.bind(this));
window.addEventListener('resize', this.onResize.bind(this));
+ if (this.options.scanning.touchInputEnabled) {
+ window.addEventListener('click', this.onClick.bind(this));
+ window.addEventListener('touchstart', this.onTouchStart.bind(this));
+ window.addEventListener('touchend', this.onTouchEnd.bind(this));
+ window.addEventListener('touchcancel', this.onTouchCancel.bind(this));
+ window.addEventListener('touchmove', this.onTouchMove.bind(this), {passive: false});
+ window.addEventListener('contextmenu', this.onContextMenu.bind(this));
+ }
+
chrome.runtime.onMessage.addListener(this.onBgMessage.bind(this));
} catch (e) {
this.onError(e);
@@ -79,7 +96,7 @@ class Frontend {
const search = async () => {
try {
- await this.searchAt({x: e.clientX, y: e.clientY});
+ await this.searchAt({x: e.clientX, y: e.clientY}, 'mouse');
} catch (e) {
this.onError(e);
}
@@ -93,6 +110,14 @@ class Frontend {
}
onMouseDown(e) {
+ if (this.mouseDownPrevent) {
+ this.setMouseDownPrevent(false, false);
+ this.setClickPrevent(true);
+ e.preventDefault();
+ e.stopPropagation();
+ return false;
+ }
+
this.mousePosLast = {x: e.clientX, y: e.clientY};
this.popupTimerClear();
this.searchClear();
@@ -133,6 +158,85 @@ class Frontend {
this.searchClear();
}
+ onClick(e) {
+ if (this.clickPrevent) {
+ this.setClickPrevent(false);
+ e.preventDefault();
+ e.stopPropagation();
+ return false;
+ }
+ }
+
+ onTouchStart(e) {
+ if (this.primaryTouchIdentifier !== null && this.getIndexOfTouch(e.touches, this.primaryTouchIdentifier) >= 0) {
+ return;
+ }
+
+ this.setPrimaryTouch(this.getPrimaryTouch(e.changedTouches));
+ }
+
+ onTouchEnd(e) {
+ if (this.primaryTouchIdentifier === null) {
+ return;
+ }
+
+ if (this.getIndexOfTouch(e.changedTouches, this.primaryTouchIdentifier) < 0) {
+ return;
+ }
+
+ this.setPrimaryTouch(this.getPrimaryTouch(this.excludeTouches(e.touches, e.changedTouches)));
+ }
+
+ onTouchCancel(e) {
+ this.onTouchEnd(e);
+ }
+
+ onTouchMove(e) {
+ if (!this.scrollPrevent || this.primaryTouchIdentifier === null) {
+ return;
+ }
+
+ const touches = e.changedTouches;
+ const index = this.getIndexOfTouch(touches, this.primaryTouchIdentifier);
+ if (index < 0) {
+ return;
+ }
+
+ const touch = touches[index];
+ this.searchFromTouch(touch.clientX, touch.clientY, 'touchMove');
+
+ e.preventDefault(); // Disable scroll
+ }
+
+ onContextMenu(e) {
+ if (this.contextMenuPrevent) {
+ this.setContextMenuPrevent(false, false);
+ e.preventDefault();
+ e.stopPropagation();
+ return false;
+ }
+ }
+
+ onAfterSearch(newRange, type, searched, success) {
+ if (type === 'mouse') {
+ return;
+ }
+
+ if (
+ !this.contextMenuChecking ||
+ (this.contextMenuPreviousRange === null ? newRange === null : this.contextMenuPreviousRange.equals(newRange))) {
+ return;
+ }
+
+ if (type === 'touchStart' && newRange !== null) {
+ this.scrollPrevent = true;
+ }
+
+ this.setContextMenuPrevent(true, false);
+ this.setMouseDownPrevent(true, false);
+ this.contextMenuChecking = false;
+ }
+
onBgMessage({action, params}, sender, callback) {
const handlers = {
optionsSet: options => {
@@ -152,7 +256,7 @@ class Frontend {
}
onError(error) {
- window.alert(`Error: ${error.toString ? error.toString() : error}`);
+ console.log(error);
}
popupTimerSet(callback) {
@@ -167,18 +271,22 @@ class Frontend {
}
}
- async searchAt(point) {
+ async searchAt(point, type) {
if (this.pendingLookup || this.popup.containsPoint(point)) {
return;
}
const textSource = docRangeFromPoint(point);
let hideResults = !textSource || !textSource.containsPoint(point);
+ let searched = false;
+ let success = false;
try {
if (!hideResults && (!this.textSourceLast || !this.textSourceLast.equals(textSource))) {
+ searched = true;
this.pendingLookup = true;
hideResults = !await this.searchTerms(textSource) && !await this.searchKanji(textSource);
+ success = true;
}
} catch (e) {
if (window.yomichan_orphaned) {
@@ -196,6 +304,7 @@ class Frontend {
}
this.pendingLookup = false;
+ this.onAfterSearch(this.textSourceLast, type, searched, success);
}
}
@@ -262,6 +371,87 @@ class Frontend {
this.textSourceLast = null;
}
+
+ getPrimaryTouch(touchList) {
+ return touchList.length > 0 ? touchList[0] : null;
+ }
+
+ getIndexOfTouch(touchList, identifier) {
+ for (let i in touchList) {
+ let t = touchList[i];
+ if (t.identifier === identifier) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ excludeTouches(touchList, excludeTouchList) {
+ const result = [];
+ for (let r of touchList) {
+ if (this.getIndexOfTouch(excludeTouchList, r.identifier) < 0) {
+ result.push(r);
+ }
+ }
+ return result;
+ }
+
+ setPrimaryTouch(touch) {
+ if (touch === null) {
+ this.primaryTouchIdentifier = null;
+ this.contextMenuPreviousRange = null;
+ this.contextMenuChecking = false;
+ this.scrollPrevent = false;
+ this.setContextMenuPrevent(false, true);
+ this.setMouseDownPrevent(false, true);
+ this.setClickPrevent(false);
+ }
+ else {
+ this.primaryTouchIdentifier = touch.identifier;
+ this.contextMenuPreviousRange = this.textSourceLast ? this.textSourceLast.clone() : null;
+ this.contextMenuChecking = true;
+ this.scrollPrevent = false;
+ this.setContextMenuPrevent(false, false);
+ this.setMouseDownPrevent(false, false);
+ this.setClickPrevent(false);
+
+ this.searchFromTouch(touch.clientX, touch.clientY, 'touchStart');
+ }
+ }
+
+ setContextMenuPrevent(value, delay) {
+ if (!delay) {
+ this.contextMenuPrevent = value;
+ }
+ }
+
+ setMouseDownPrevent(value, delay) {
+ if (!delay) {
+ this.mouseDownPrevent = value;
+ }
+ }
+
+ setClickPrevent(value) {
+ this.clickPrevent = value;
+ }
+
+ searchFromTouch(x, y, type) {
+ this.popupTimerClear();
+
+ if (!this.options.general.enable || this.pendingLookup) {
+ return;
+ }
+
+ const search = async () => {
+ try {
+ await this.searchAt({x, y}, type);
+ } catch (e) {
+ this.onError(e);
+ }
+ };
+
+ search();
+ }
}
window.yomichan_frontend = new Frontend();
diff --git a/ext/fg/js/popup.js b/ext/fg/js/popup.js
index 58e0672e..d2acf4d0 100644
--- a/ext/fg/js/popup.js
+++ b/ext/fg/js/popup.js
@@ -62,6 +62,7 @@ class Popup {
}
}
+ let above = false;
let y = 0;
let height = Math.max(containerHeight, options.general.popupHeight);
const yBelow = elementRect.bottom + options.general.popupVerticalOffset;
@@ -75,11 +76,14 @@ class Popup {
} else {
height = Math.max(height - overflowAbove, 0);
y = Math.max(yAbove - height, 0);
+ above = true;
}
} else {
y = yBelow;
}
+ this.container.classList.toggle('yomichan-float-full-width', options.general.popupDisplayMode === 'full-width');
+ this.container.classList.toggle('yomichan-float-above', above);
this.container.style.left = `${x}px`;
this.container.style.top = `${y}px`;
this.container.style.width = `${width}px`;
diff --git a/ext/fg/js/util.js b/ext/fg/js/util.js
index 5eff4018..954b3988 100644
--- a/ext/fg/js/util.js
+++ b/ext/fg/js/util.js
@@ -26,11 +26,15 @@ function utilAsync(func) {
function utilInvoke(action, params={}) {
return new Promise((resolve, reject) => {
try {
- chrome.runtime.sendMessage({action, params}, ({result, error}) => {
- if (error) {
- reject(error);
+ chrome.runtime.sendMessage({action, params}, (response) => {
+ if (response !== null && typeof response === 'object') {
+ if (response.error) {
+ reject(response.error);
+ } else {
+ resolve(response.result);
+ }
} else {
- resolve(result);
+ reject(`Unexpected response of type ${typeof response}`);
}
});
} catch (e) {
diff --git a/ext/manifest.json b/ext/manifest.json
index 7ee66f4f..ef8b84ec 100644
--- a/ext/manifest.json
+++ b/ext/manifest.json
@@ -27,6 +27,10 @@
}],
"minimum_chrome_version": "57.0.0.0",
"options_page": "bg/settings.html",
+ "options_ui": {
+ "page": "bg/settings.html",
+ "open_in_tab": true
+ },
"permissions": [
"<all_urls>",
"storage",