Merge pull request #160 from toasted-nutbread/mobile

Add support for mobile Firefox
This commit is contained in:
Alex Yatskov 2019-05-05 18:26:02 -07:00 committed by GitHub
commit 61d1168d94
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 297 additions and 72 deletions

View File

@ -2,6 +2,7 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1" />
</head> </head>
<body> <body>
<script src="/mixed/lib/dexie.min.js"></script> <script src="/mixed/lib/dexie.min.js"></script>

View File

@ -2,6 +2,7 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <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.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/css/bootstrap-theme.min.css">
<link rel="stylesheet" type="text/css" href="/mixed/lib/bootstrap-toggle/bootstrap-toggle.min.css"> <link rel="stylesheet" type="text/css" href="/mixed/lib/bootstrap-toggle/bootstrap-toggle.min.css">

View File

@ -2,6 +2,7 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Welcome to Yomichan!</title> <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.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/css/bootstrap-theme.min.css">

View File

@ -28,7 +28,9 @@ class Backend {
await this.translator.prepare(); await this.translator.prepare();
await apiOptionsSet(await optionsLoad()); 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)); chrome.runtime.onMessage.addListener(this.onMessage.bind(this));
if (this.options.general.showGuide) { if (this.options.general.showGuide) {
@ -40,13 +42,13 @@ class Backend {
this.options = utilIsolate(options); this.options = utilIsolate(options);
if (!options.general.enable) { if (!options.general.enable) {
chrome.browserAction.setBadgeBackgroundColor({color: '#555555'}); this.setExtensionBadgeBackgroundColor('#555555');
chrome.browserAction.setBadgeText({text: 'off'}); this.setExtensionBadgeText('off');
} else if (!dictConfigured(options)) { } else if (!dictConfigured(options)) {
chrome.browserAction.setBadgeBackgroundColor({color: '#f0ad4e'}); this.setExtensionBadgeBackgroundColor('#f0ad4e');
chrome.browserAction.setBadgeText({text: '!'}); this.setExtensionBadgeText('!');
} else { } else {
chrome.browserAction.setBadgeText({text: ''}); this.setExtensionBadgeText('');
} }
if (options.anki.enable) { if (options.anki.enable) {
@ -125,6 +127,18 @@ class Backend {
return true; 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(); window.yomichan_backend = new Backend();

View File

@ -228,11 +228,47 @@ class Database {
} }
} }
async importDictionary(archive, callback) { async importDictionary(archive, progressCallback, exceptions) {
if (!this.db) { if (!this.db) {
throw 'Database not initialized'; 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 => { const indexDataLoaded = async summary => {
if (summary.version > 3) { if (summary.version > 3) {
throw 'Unsupported dictionary version'; throw 'Unsupported dictionary version';
@ -247,10 +283,6 @@ class Database {
}; };
const termDataLoaded = async (summary, entries, total, current) => { const termDataLoaded = async (summary, entries, total, current) => {
if (callback) {
callback(total, current);
}
const rows = []; const rows = [];
if (summary.version === 1) { if (summary.version === 1) {
for (const [expression, reading, definitionTags, rules, score, ...glossary] of entries) { 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) => { const termMetaDataLoaded = async (summary, entries, total, current) => {
if (callback) {
callback(total, current);
}
const rows = []; const rows = [];
for (const [expression, mode, data] of entries) { for (const [expression, mode, data] of entries) {
rows.push({ 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) => { const kanjiDataLoaded = async (summary, entries, total, current) => {
if (callback) {
callback(total, current);
}
const rows = []; const rows = [];
if (summary.version === 1) { if (summary.version === 1) {
for (const [character, onyomi, kunyomi, tags, ...meanings] of entries) { 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) => { const kanjiMetaDataLoaded = async (summary, entries, total, current) => {
if (callback) {
callback(total, current);
}
const rows = []; const rows = [];
for (const [character, mode, data] of entries) { for (const [character, mode, data] of entries) {
rows.push({ 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) => { const tagDataLoaded = async (summary, entries, total, current) => {
if (callback) {
callback(total, current);
}
const rows = []; const rows = [];
for (const [name, category, order, notes, score] of entries) { for (const [name, category, order, notes, score] of entries) {
const row = dictTagSanitize({ const row = dictTagSanitize({
@ -372,7 +388,7 @@ class Database {
rows.push(row); rows.push(row);
} }
await this.db.tagMeta.bulkAdd(rows); await bulkAdd(this.db.tagMeta, rows, total, current);
}; };
return await Database.importDictionaryZip( return await Database.importDictionaryZip(

View File

@ -193,7 +193,7 @@ async function onReady() {
await dictionaryGroupsPopulate(options); await dictionaryGroupsPopulate(options);
await formMainDictionaryOptionsPopulate(options); await formMainDictionaryOptionsPopulate(options);
} catch (e) { } catch (e) {
dictionaryErrorShow(e); dictionaryErrorsShow([e]);
} }
try { try {
@ -203,6 +203,8 @@ async function onReady() {
} }
formUpdateVisibility(options); formUpdateVisibility(options);
storageInfoInitialize();
} }
$(document).ready(utilAsync(onReady)); $(document).ready(utilAsync(onReady));
@ -212,36 +214,63 @@ $(document).ready(utilAsync(onReady));
* Dictionary * 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'); const dialog = $('#dict-error');
if (error) { dialog.show().text('');
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) { if (errors !== null && errors.length > 0) {
error = error.toString(); 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) { for (const e in uniqueErrors) {
if (error.includes(match)) { const count = uniqueErrors[e];
error = subst; const div = document.createElement('p');
break; 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 { } else {
dialog.hide(); dialog.hide();
} }
@ -317,7 +346,7 @@ async function onDictionaryPurge(e) {
const dictProgress = $('#dict-purge').show(); const dictProgress = $('#dict-purge').show();
try { try {
dictionaryErrorShow(); dictionaryErrorsShow(null);
dictionarySpinnerShow(true); dictionarySpinnerShow(true);
await utilDatabasePurge(); await utilDatabasePurge();
@ -329,12 +358,16 @@ async function onDictionaryPurge(e) {
await dictionaryGroupsPopulate(options); await dictionaryGroupsPopulate(options);
await formMainDictionaryOptionsPopulate(options); await formMainDictionaryOptionsPopulate(options);
} catch (e) { } catch (e) {
dictionaryErrorShow(e); dictionaryErrorsShow([e]);
} finally { } finally {
dictionarySpinnerShow(false); dictionarySpinnerShow(false);
dictControls.show(); dictControls.show();
dictProgress.hide(); dictProgress.hide();
if (storageEstimate.mostRecent !== null) {
storageUpdateStats();
}
} }
} }
@ -344,25 +377,37 @@ async function onDictionaryImport(e) {
const dictProgress = $('#dict-import-progress').show(); const dictProgress = $('#dict-import-progress').show();
try { try {
dictionaryErrorShow(); dictionaryErrorsShow(null);
dictionarySpinnerShow(true); dictionarySpinnerShow(true);
const setProgress = percent => dictProgress.find('.progress-bar').css('width', `${percent}%`); 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); setProgress(0.0);
const exceptions = [];
const options = await optionsLoad(); 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}; options.dictionaries[summary.title] = {enabled: true, priority: 0, allowSecondarySearches: false};
if (summary.sequenced && options.general.mainDictionary === '') { if (summary.sequenced && options.general.mainDictionary === '') {
options.general.mainDictionary = summary.title; 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 optionsSave(options);
await dictionaryGroupsPopulate(options); await dictionaryGroupsPopulate(options);
await formMainDictionaryOptionsPopulate(options); await formMainDictionaryOptionsPopulate(options);
} catch (e) { } catch (e) {
dictionaryErrorShow(e); dictionaryErrorsShow([e]);
} finally { } finally {
dictionarySpinnerShow(false); dictionarySpinnerShow(false);
@ -520,3 +565,93 @@ async function onAnkiFieldTemplatesReset(e) {
ankiErrorShow(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();
}
}

View File

@ -87,6 +87,6 @@ function utilDatabasePurge() {
return utilBackend().translator.database.purge(); return utilBackend().translator.database.purge();
} }
function utilDatabaseImport(data, progress) { function utilDatabaseImport(data, progress, exceptions) {
return utilBackend().translator.database.importDictionary(data, progress); return utilBackend().translator.database.importDictionary(data, progress, exceptions);
} }

View File

@ -2,6 +2,7 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Yomichan Legal</title> <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.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/css/bootstrap-theme.min.css">

View File

@ -2,6 +2,7 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Yomichan Search</title> <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.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/css/bootstrap-theme.min.css">

View File

@ -2,13 +2,14 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Yomichan Options</title> <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.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/css/bootstrap-theme.min.css">
<style> <style>
#anki-spinner, #anki-general, #anki-error, #anki-spinner, #anki-general, #anki-error,
#dict-spinner, #dict-error, #dict-warning, #dict-purge, #dict-import-progress, #dict-spinner, #dict-error, #dict-warning, #dict-purge, #dict-import-progress,
#debug, .options-advanced { #debug, .options-advanced, .storage-hidden, #storage-spinner {
display: none; display: none;
} }
@ -24,6 +25,21 @@
overflow-x: hidden; overflow-x: hidden;
white-space: pre; 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> </style>
</head> </head>
<body> <body>
@ -192,6 +208,40 @@
</div> </div>
</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>
<div> <div>
<img src="/mixed/img/spinner.gif" class="pull-right" id="anki-spinner" alt> <img src="/mixed/img/spinner.gif" class="pull-right" id="anki-spinner" alt>
@ -310,8 +360,8 @@
<pre id="debug"></pre> <pre id="debug"></pre>
<div class="pull-right"> <div class="pull-right bottom-links">
<small><a href="https://foosoft.net/projects/yomichan/" target="_blank">Homepage</a> &bull; <a href="legal.html">Legal</a></small> <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>
</div> </div>

View File

@ -2,6 +2,7 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title></title> <title></title>
<link rel="stylesheet" href="/mixed/lib/bootstrap/css/bootstrap.min.css"> <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/lib/bootstrap/css/bootstrap-theme.min.css">

View File

@ -256,7 +256,7 @@ class Frontend {
} }
onError(error) { onError(error) {
window.alert(`Error: ${error.toString ? error.toString() : error}`); console.log(error);
} }
popupTimerSet(callback) { popupTimerSet(callback) {

View File

@ -26,11 +26,15 @@ function utilAsync(func) {
function utilInvoke(action, params={}) { function utilInvoke(action, params={}) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
try { try {
chrome.runtime.sendMessage({action, params}, ({result, error}) => { chrome.runtime.sendMessage({action, params}, (response) => {
if (error) { if (response !== null && typeof response === 'object') {
reject(error); if (response.error) {
reject(response.error);
} else {
resolve(response.result);
}
} else { } else {
resolve(result); reject(`Unexpected response of type ${typeof response}`);
} }
}); });
} catch (e) { } catch (e) {