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  [](https://foosoft.net/projects/yomichan/img/ss-kanji.png)  [](https://foosoft.net/projects/yomichan/img/ss-dictionaries.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",  |