diff options
-rw-r--r-- | LICENSE | 14 | ||||
-rw-r--r-- | README.md | 43 | ||||
-rw-r--r-- | ext/bg/background.html | 1 | ||||
-rw-r--r-- | ext/bg/context.html | 1 | ||||
-rw-r--r-- | ext/bg/guide.html | 1 | ||||
-rw-r--r-- | ext/bg/js/audio.js | 29 | ||||
-rw-r--r-- | ext/bg/js/backend.js | 26 | ||||
-rw-r--r-- | ext/bg/js/database.js | 68 | ||||
-rw-r--r-- | ext/bg/js/options.js | 2 | ||||
-rw-r--r-- | ext/bg/js/settings.js | 201 | ||||
-rw-r--r-- | ext/bg/js/util.js | 4 | ||||
-rw-r--r-- | ext/bg/legal.html | 1 | ||||
-rw-r--r-- | ext/bg/search.html | 1 | ||||
-rw-r--r-- | ext/bg/settings.html | 68 | ||||
-rw-r--r-- | ext/fg/css/client.css | 22 | ||||
-rw-r--r-- | ext/fg/float.html | 1 | ||||
-rw-r--r-- | ext/fg/js/document.js | 12 | ||||
-rw-r--r-- | ext/fg/js/frontend.js | 196 | ||||
-rw-r--r-- | ext/fg/js/popup.js | 4 | ||||
-rw-r--r-- | ext/fg/js/util.js | 12 | ||||
-rw-r--r-- | ext/manifest.json | 4 |
21 files changed, 602 insertions, 109 deletions
diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..811a6915 --- /dev/null +++ b/LICENSE @@ -0,0 +1,14 @@ +Copyright 2016-2019 Alex Yatskov + +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/>. @@ -56,26 +56,26 @@ Please be aware that the non-English dictionaries contain fewer entries than the primary language is not English, you may consider also importing the English version for better coverage. * **[JMdict](http://www.edrdg.org/enamdict/enamdict_doc.html)** (Japanese vocabulary) - * [jmdict_dutch.zip](https://foosoft.net/projects/yomichan/dl/dict/jmdict_dutch.zip) - * [jmdict_english.zip](https://foosoft.net/projects/yomichan/dl/dict/jmdict_english.zip) - * [jmdict_french.zip](https://foosoft.net/projects/yomichan/dl/dict/jmdict_french.zip) - * [jmdict_german.zip](https://foosoft.net/projects/yomichan/dl/dict/jmdict_german.zip) - * [jmdict_hungarian.zip](https://foosoft.net/projects/yomichan/dl/dict/jmdict_hungarian.zip) - * [jmdict_russian.zip](https://foosoft.net/projects/yomichan/dl/dict/jmdict_russian.zip) - * [jmdict_slovenian.zip](https://foosoft.net/projects/yomichan/dl/dict/jmdict_slovenian.zip) - * [jmdict_spanish.zip](https://foosoft.net/projects/yomichan/dl/dict/jmdict_spanish.zip) - * [jmdict_swedish.zip](https://foosoft.net/projects/yomichan/dl/dict/jmdict_swedish.zip) + * [jmdict\_dutch.zip](https://foosoft.net/projects/yomichan/dl/dict/jmdict_dutch.zip) + * [jmdict\_english.zip](https://foosoft.net/projects/yomichan/dl/dict/jmdict_english.zip) + * [jmdict\_french.zip](https://foosoft.net/projects/yomichan/dl/dict/jmdict_french.zip) + * [jmdict\_german.zip](https://foosoft.net/projects/yomichan/dl/dict/jmdict_german.zip) + * [jmdict\_hungarian.zip](https://foosoft.net/projects/yomichan/dl/dict/jmdict_hungarian.zip) + * [jmdict\_russian.zip](https://foosoft.net/projects/yomichan/dl/dict/jmdict_russian.zip) + * [jmdict\_slovenian.zip](https://foosoft.net/projects/yomichan/dl/dict/jmdict_slovenian.zip) + * [jmdict\_spanish.zip](https://foosoft.net/projects/yomichan/dl/dict/jmdict_spanish.zip) + * [jmdict\_swedish.zip](https://foosoft.net/projects/yomichan/dl/dict/jmdict_swedish.zip) * **[JMnedict](http://www.edrdg.org/enamdict/enamdict_doc.html)** (Japanese names) * [jmnedict.zip](https://foosoft.net/projects/yomichan/dl/dict/jmnedict.zip) * **[KireiCake](https://kireicake.com/rikaicakes/)** (Japanese slang) * [kireicake.zip](https://foosoft.net/projects/yomichan/dl/dict/kireicake.zip) * **[KANJIDIC](http://nihongo.monash.edu/kanjidic2/index.html)** (Japanese Kanji) - * [kanjidic_english.zip](https://foosoft.net/projects/yomichan/dl/dict/kanjidic_english.zip) - * [kanjidic_french.zip](https://foosoft.net/projects/yomichan/dl/dict/kanjidic_french.zip) - * [kanjidic_portuguese.zip](https://foosoft.net/projects/yomichan/dl/dict/kanjidic_portuguese.zip) - * [kanjidic_spanish.zip](https://foosoft.net/projects/yomichan/dl/dict/kanjidic_spanish.zip) + * [kanjidic\_english.zip](https://foosoft.net/projects/yomichan/dl/dict/kanjidic_english.zip) + * [kanjidic\_french.zip](https://foosoft.net/projects/yomichan/dl/dict/kanjidic_french.zip) + * [kanjidic\_portuguese.zip](https://foosoft.net/projects/yomichan/dl/dict/kanjidic_portuguese.zip) + * [kanjidic\_spanish.zip](https://foosoft.net/projects/yomichan/dl/dict/kanjidic_spanish.zip) * **[Innocent Corpus](https://forum.koohii.com/post-168613.html#pid168613)** (Term and Kanji frequencies across 5000+ novels) - * [innocent_corpus.zip](https://foosoft.net/projects/yomichan/dl/dict/innocent_corpus.zip) + * [innocent\_corpus.zip](https://foosoft.net/projects/yomichan/dl/dict/innocent_corpus.zip) ## Basic Usage ## @@ -330,18 +330,3 @@ pull request containing this functionality, as I will ultimately be the one main [![Kanji information](https://foosoft.net/projects/yomichan/img/ss-kanji-thumb.png)](https://foosoft.net/projects/yomichan/img/ss-kanji.png) [![Dictionary options](https://foosoft.net/projects/yomichan/img/ss-dictionaries-thumb.png)](https://foosoft.net/projects/yomichan/img/ss-dictionaries.png) [![Anki options](https://foosoft.net/projects/yomichan/img/ss-anki-thumb.png)](https://foosoft.net/projects/yomichan/img/ss-anki.png) - -## License ## - -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/>. 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> • <a href="legal.html">Legal</a></small> + <div class="pull-right bottom-links"> + <small><a href="search.html">Search</a> • <a href="https://foosoft.net/projects/yomichan/" target="_blank">Homepage</a> • <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", |