diff options
50 files changed, 1970 insertions, 447 deletions
| diff --git a/ext/bg/background.html b/ext/bg/background.html index 3b37db87..194d4a45 100644 --- a/ext/bg/background.html +++ b/ext/bg/background.html @@ -27,6 +27,7 @@          <script src="/bg/js/templates.js"></script>          <script src="/bg/js/translator.js"></script>          <script src="/bg/js/util.js"></script> +        <script src="/mixed/js/audio.js"></script>          <script src="/mixed/js/japanese.js"></script>          <script src="/bg/js/backend.js"></script> diff --git a/ext/bg/css/settings.css b/ext/bg/css/settings.css index 12bbe8a8..6284058a 100644 --- a/ext/bg/css/settings.css +++ b/ext/bg/css/settings.css @@ -89,19 +89,24 @@      text-align-last: center;  } -.condition-group>.condition>div:first-child { +.condition-group>.condition>*:first-child, +.audio-source-list>.audio-source>*:first-child {      border-bottom-left-radius: 0;  } -.condition-group>.condition:nth-child(n+2)>div:first-child { +.condition-group>.condition:nth-child(n+2)>*:first-child, +.audio-source-list>.audio-source:nth-child(n+2)>*:first-child {      border-top-left-radius: 0;  } -.condition-group>.condition:nth-child(n+2)>div:last-child>button { +.condition-group>.condition:nth-child(n+2)>div:last-child>button, +.audio-source-list>.audio-source:nth-child(n+2)>*:last-child>button {      border-top-right-radius: 0;  } -.condition-group>.condition:nth-last-child(n+2)>div:last-child>button { +.condition-group>.condition:nth-last-child(n+2)>div:last-child>button, +.audio-source-list>.audio-source:nth-last-child(n+2)>*:last-child>button {      border-bottom-right-radius: 0;  } -.condition-group-options>.condition-add { +.condition-group-options>.condition-add, +.audio-source-options>.audio-source-add {      border-top-left-radius: 0;      border-top-right-radius: 0;  } @@ -110,6 +115,19 @@      display: none;  } +.audio-source-list { +    counter-reset: audio-source-id; +} +.audio-source-list .audio-source-prefix { +    flex: 0 0 auto; +    width: 39px; +    text-align: center; +} +.audio-source-list .audio-source-prefix:after { +    counter-increment: audio-source-id; +    content: counter(audio-source-id); +} +  #custom-popup-css {      width: 100%;      min-height: 34px; diff --git a/ext/bg/guide.html b/ext/bg/guide.html index 7ec1d8d9..2a602f1f 100644 --- a/ext/bg/guide.html +++ b/ext/bg/guide.html @@ -23,7 +23,7 @@                  <li>Click on the <em>monkey wrench</em> icon in the middle to open the options page.</li>                  <li>Import the dictionaries you wish to use for term and Kanji searches.</li>                  <li>Hold down <kbd>Shift</kbd> key or the middle mouse button as you move your mouse over text to display definitions.</li> -                <li>Click on the <img src="/mixed/img/play-audio.png" alt> icon to hear the term pronounced by a native speaker.</li> +                <li>Click on the <img src="/mixed/img/play-audio.svg" alt> icon to hear the term pronounced by a native speaker.</li>                  <li>Click on individual Kanji in the term definition results to view additional information about those characters.</li>              </ol> diff --git a/ext/bg/js/anki.js b/ext/bg/js/anki.js index bd4e46cd..9f851f13 100644 --- a/ext/bg/js/anki.js +++ b/ext/bg/js/anki.js @@ -67,14 +67,39 @@ class AnkiConnect {          if (this.remoteVersion < this.localVersion) {              this.remoteVersion = await this.ankiInvoke('version');              if (this.remoteVersion < this.localVersion) { -                throw 'Extension and plugin versions incompatible'; +                throw new Error('Extension and plugin versions incompatible');              }          }      } +    async findNoteIds(notes) { +        await this.checkVersion(); +        const actions = notes.map(note => ({ +            action: 'findNotes', +            params: { +                query: `deck:"${AnkiConnect.escapeQuery(note.deckName)}" ${AnkiConnect.fieldsToQuery(note.fields)}` +            } +        })); +        return await this.ankiInvoke('multi', {actions}); +    } +      ankiInvoke(action, params) {          return requestJson(this.server, 'POST', {action, params, version: this.localVersion});      } + +    static escapeQuery(text) { +        return text.replace(/"/g, ''); +    } + +    static fieldsToQuery(fields) { +        const fieldNames = Object.keys(fields); +        if (fieldNames.length === 0) { +            return ''; +        } + +        const key = fieldNames[0]; +        return `${key.toLowerCase()}:"${AnkiConnect.escapeQuery(fields[key])}"`; +    }  } @@ -106,4 +131,8 @@ class AnkiNull {      async guiBrowse(query) {          return [];      } + +    async findNoteIds(notes) { +        return []; +    }  } diff --git a/ext/bg/js/api.js b/ext/bg/js/api.js index 222e7ffe..f768e6f9 100644 --- a/ext/bg/js/api.js +++ b/ext/bg/js/api.js @@ -68,7 +68,8 @@ async function apiDefinitionAdd(definition, mode, context, optionsContext) {          await audioInject(              definition,              options.anki.terms.fields, -            options.general.audioSource +            options.audio.sources, +            optionsContext          );      } @@ -97,15 +98,33 @@ async function apiDefinitionsAddable(definitions, modes, optionsContext) {              }          } -        const results = await utilBackend().anki.canAddNotes(notes); +        const cannotAdd = []; +        const anki = utilBackend().anki; +        const results = await anki.canAddNotes(notes);          for (let resultBase = 0; resultBase < results.length; resultBase += modes.length) {              const state = {};              for (let modeOffset = 0; modeOffset < modes.length; ++modeOffset) { -                state[modes[modeOffset]] = results[resultBase + modeOffset]; +                const index = resultBase + modeOffset; +                const result = results[index]; +                const info = {canAdd: result}; +                state[modes[modeOffset]] = info; +                if (!result) { +                    cannotAdd.push([notes[index], info]); +                }              }              states.push(state);          } + +        if (cannotAdd.length > 0) { +            const noteIdsArray = await anki.findNoteIds(cannotAdd.map(e => e[0])); +            for (let i = 0, ii = Math.min(cannotAdd.length, noteIdsArray.length); i < ii; ++i) { +                const noteIds = noteIdsArray[i]; +                if (noteIds.length > 0) { +                    cannotAdd[i][1].noteId = noteIds[0]; +                } +            } +        }      } catch (e) {          // NOP      } @@ -156,8 +175,8 @@ apiCommandExec.handlers = {      }  }; -async function apiAudioGetUrl(definition, source) { -    return audioBuildUrl(definition, source); +async function apiAudioGetUrl(definition, source, optionsContext) { +    return audioBuildUrl(definition, source, optionsContext);  }  async function apiInjectScreenshot(definition, fields, screenshot) { diff --git a/ext/bg/js/audio-ui.js b/ext/bg/js/audio-ui.js new file mode 100644 index 00000000..381129ac --- /dev/null +++ b/ext/bg/js/audio-ui.js @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2019  Alex Yatskov <alex@foosoft.net> + * Author: Alex Yatskov <alex@foosoft.net> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program.  If not, see <http://www.gnu.org/licenses/>. + */ + + +class AudioSourceUI { +    static instantiateTemplate(templateSelector) { +        const template = document.querySelector(templateSelector); +        const content = document.importNode(template.content, true); +        return $(content.firstChild); +    } +} + +AudioSourceUI.Container = class Container { +    constructor(audioSources, container, addButton) { +        this.audioSources = audioSources; +        this.container = container; +        this.addButton = addButton; +        this.children = []; + +        this.container.empty(); + +        for (const audioSource of toIterable(audioSources)) { +            this.children.push(new AudioSourceUI.AudioSource(this, audioSource, this.children.length)); +        } + +        this.addButton.on('click', () => this.onAddAudioSource()); +    } + +    cleanup() { +        for (const child of this.children) { +            child.cleanup(); +        } + +        this.addButton.off('click'); +        this.container.empty(); +    } + +    save() { +        // Override +    } + +    remove(child) { +        const index = this.children.indexOf(child); +        if (index < 0) { +            return; +        } + +        child.cleanup(); +        this.children.splice(index, 1); +        this.audioSources.splice(index, 1); + +        for (let i = index; i < this.children.length; ++i) { +            this.children[i].index = i; +        } +    } + +    onAddAudioSource() { +        const audioSource = this.getUnusedAudioSource(); +        this.audioSources.push(audioSource); +        this.save(); +        this.children.push(new AudioSourceUI.AudioSource(this, audioSource, this.children.length)); +    } + +    getUnusedAudioSource() { +        const audioSourcesAvailable = [ +            'jpod101', +            'jpod101-alternate', +            'jisho', +            'custom' +        ]; +        for (const source of audioSourcesAvailable) { +            if (this.audioSources.indexOf(source) < 0) { +                return source; +            } +        } +        return audioSourcesAvailable[0]; +    } +}; + +AudioSourceUI.AudioSource = class AudioSource { +    constructor(parent, audioSource, index) { +        this.parent = parent; +        this.audioSource = audioSource; +        this.index = index; + +        this.container = AudioSourceUI.instantiateTemplate('#audio-source-template').appendTo(parent.container); +        this.select = this.container.find('.audio-source-select'); +        this.removeButton = this.container.find('.audio-source-remove'); + +        this.select.val(audioSource); + +        this.select.on('change', () => this.onSelectChanged()); +        this.removeButton.on('click', () => this.onRemoveClicked()); +    } + +    cleanup() { +        this.select.off('change'); +        this.removeButton.off('click'); +        this.container.remove(); +    } + +    save() { +        this.parent.save(); +    } + +    onSelectChanged() { +        this.audioSource = this.select.val(); +        this.parent.audioSources[this.index] = this.audioSource; +        this.save(); +    } + +    onRemoveClicked() { +        this.parent.remove(this); +        this.save(); +    } +}; diff --git a/ext/bg/js/audio.js b/ext/bg/js/audio.js index 2e5db7cc..9e0ae67c 100644 --- a/ext/bg/js/audio.js +++ b/ext/bg/js/audio.js @@ -17,8 +17,8 @@   */ -async function audioBuildUrl(definition, mode, cache={}) { -    if (mode === 'jpod101') { +const audioUrlBuilders = { +    'jpod101': async (definition) => {          let kana = definition.reading;          let kanji = definition.expression; @@ -35,84 +35,80 @@ async function audioBuildUrl(definition, mode, cache={}) {              params.push(`kana=${encodeURIComponent(kana)}`);          } -        const url = `https://assets.languagepod101.com/dictionary/japanese/audiomp3.php?${params.join('&')}`; -        return Promise.resolve(url); -    } else if (mode === 'jpod101-alternate') { -        return new Promise((resolve, reject) => { -            const response = cache[definition.expression]; -            if (response) { -                resolve(response); -            } else { -                const data = { -                    post: 'dictionary_reference', -                    match_type: 'exact', -                    search_query: definition.expression -                }; - -                const params = []; -                for (const key in data) { -                    params.push(`${encodeURIComponent(key)}=${encodeURIComponent(data[key])}`); -                } - -                const xhr = new XMLHttpRequest(); -                xhr.open('POST', 'https://www.japanesepod101.com/learningcenter/reference/dictionary_post'); -                xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); -                xhr.addEventListener('error', () => reject('Failed to scrape audio data')); -                xhr.addEventListener('load', () => { -                    cache[definition.expression] = xhr.responseText; -                    resolve(xhr.responseText); -                }); - -                xhr.send(params.join('&')); -            } -        }).then(response => { -            const dom = new DOMParser().parseFromString(response, 'text/html'); -            for (const row of dom.getElementsByClassName('dc-result-row')) { -                try { -                    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 audioUrlNormalize(url, 'https://www.japanesepod101.com', '/learningcenter/reference/'); -                    } -                } catch (e) { -                    // NOP -                } -            } +        return `https://assets.languagepod101.com/dictionary/japanese/audiomp3.php?${params.join('&')}`; +    }, +    'jpod101-alternate': async (definition) => { +        const response = await new Promise((resolve, reject) => { +            const xhr = new XMLHttpRequest(); +            xhr.open('POST', 'https://www.japanesepod101.com/learningcenter/reference/dictionary_post'); +            xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); +            xhr.addEventListener('error', () => reject(new Error('Failed to scrape audio data'))); +            xhr.addEventListener('load', () => resolve(xhr.responseText)); +            xhr.send(`post=dictionary_reference&match_type=exact&search_query=${encodeURIComponent(definition.expression)}`);          }); -    } else if (mode === 'jisho') { -        return new Promise((resolve, reject) => { -            const response = cache[definition.expression]; -            if (response) { -                resolve(response); -            } else { -                const xhr = new XMLHttpRequest(); -                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; -                    resolve(xhr.responseText); -                }); - -                xhr.send(); -            } -        }).then(response => { + +        const dom = new DOMParser().parseFromString(response, 'text/html'); +        for (const row of dom.getElementsByClassName('dc-result-row')) {              try { -                const dom = new DOMParser().parseFromString(response, 'text/html'); -                const audio = dom.getElementById(`audio_${definition.expression}:${definition.reading}`); -                if (audio) { -                    const url = audio.getElementsByTagName('source').item(0).getAttribute('src'); -                    if (url) { -                        return audioUrlNormalize(url, 'https://jisho.org', '/search/'); -                    } +                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 audioUrlNormalize(url, 'https://www.japanesepod101.com', '/learningcenter/reference/');                  }              } catch (e) {                  // NOP              } +        } + +        throw new Error('Failed to find audio URL'); +    }, +    'jisho': async (definition) => { +        const response = await new Promise((resolve, reject) => { +            const xhr = new XMLHttpRequest(); +            xhr.open('GET', `https://jisho.org/search/${definition.expression}`); +            xhr.addEventListener('error', () => reject(new Error('Failed to scrape audio data'))); +            xhr.addEventListener('load', () => resolve(xhr.responseText)); +            xhr.send();          }); + +        const dom = new DOMParser().parseFromString(response, 'text/html'); +        try { +            const audio = dom.getElementById(`audio_${definition.expression}:${definition.reading}`); +            if (audio !== null) { +                const url = audio.getElementsByTagName('source').item(0).getAttribute('src'); +                if (url) { +                    return audioUrlNormalize(url, 'https://jisho.org', '/search/'); +                } +            } +        } catch (e) { +            // NOP +        } + +        throw new Error('Failed to find audio URL'); +    }, +    'custom': async (definition, optionsContext) => { +        const options = await apiOptionsGet(optionsContext); +        const customSourceUrl = options.audio.customSourceUrl; +        return customSourceUrl.replace(/\{([^\}]*)\}/g, (m0, m1) => (definition.hasOwnProperty(m1) ? `${definition[m1]}` : m0));      } -    else { -        return Promise.resolve(); +}; + +async function audioBuildUrl(definition, mode, optionsContext, cache={}) { +    const cacheKey = `${mode}:${definition.expression}`; +    if (cache.hasOwnProperty(cacheKey)) { +        return Promise.resolve(cache[cacheKey]); +    } + +    if (audioUrlBuilders.hasOwnProperty(mode)) { +        const handler = audioUrlBuilders[mode]; +        return handler(definition, optionsContext).then( +            (url) => { +                cache[cacheKey] = url; +                return url; +            }, +            () => null);      } +    return null;  }  function audioUrlNormalize(url, baseUrl, basePath) { @@ -145,9 +141,10 @@ function audioBuildFilename(definition) {          return filename += '.mp3';      } +    return null;  } -async function audioInject(definition, fields, mode) { +async function audioInject(definition, fields, sources, optionsContext) {      let usesAudio = false;      for (const name in fields) {          if (fields[name].includes('{audio}')) { @@ -166,11 +163,12 @@ async function audioInject(definition, fields, mode) {              audioSourceDefinition = definition.expressions[0];          } -        const url = await audioBuildUrl(audioSourceDefinition, mode); -        const filename = audioBuildFilename(audioSourceDefinition); - -        if (url && filename) { -            definition.audio = {url, filename}; +        const {url} = await audioGetFromSources(audioSourceDefinition, sources, optionsContext, false); +        if (url !== null) { +            const filename = audioBuildFilename(audioSourceDefinition); +            if (filename !== null) { +                definition.audio = {url, filename}; +            }          }          return true; diff --git a/ext/bg/js/backend.js b/ext/bg/js/backend.js index 3c5ad885..453f4282 100644 --- a/ext/bg/js/backend.js +++ b/ext/bg/js/backend.js @@ -75,7 +75,7 @@ class Backend {              const promise = handler(params, sender);              promise                  .then(result => callback({result})) -                .catch(error => callback({error: typeof error.toString === 'function' ? error.toString() : error})); +                .catch(error => callback(errorToJson(error)));          }          return true; @@ -181,7 +181,7 @@ Backend.messageHandlers = {      noteView: ({noteId}) => apiNoteView(noteId),      templateRender: ({template, data, dynamic}) => apiTemplateRender(template, data, dynamic),      commandExec: ({command}) => apiCommandExec(command), -    audioGetUrl: ({definition, source}) => apiAudioGetUrl(definition, source), +    audioGetUrl: ({definition, source, optionsContext}) => apiAudioGetUrl(definition, source, optionsContext),      screenshotGet: ({options}, sender) => apiScreenshotGet(options, sender),      forward: ({action, params}, sender) => apiForward(action, params, sender),      frameInformationGet: (params, sender) => apiFrameInformationGet(sender), diff --git a/ext/bg/js/database.js b/ext/bg/js/database.js index e8214c3c..771a71c9 100644 --- a/ext/bg/js/database.js +++ b/ext/bg/js/database.js @@ -25,7 +25,7 @@ class Database {      async prepare() {          if (this.db) { -            throw 'Database already initialized'; +            throw new Error('Database already initialized');          }          this.db = new Dexie('dict'); @@ -48,9 +48,7 @@ class Database {      }      async purge() { -        if (!this.db) { -            throw 'Database not initialized'; -        } +        this.validate();          this.db.close();          await this.db.delete(); @@ -61,9 +59,7 @@ class Database {      }      async findTerms(term, titles) { -        if (!this.db) { -            throw 'Database not initialized'; -        } +        this.validate();          const results = [];          await this.db.terms.where('expression').equals(term).or('reading').equals(term).each(row => { @@ -80,7 +76,12 @@ class Database {          const visited = {};          const results = [];          const createResult = Database.createTerm; -        const filter = (row) => titles.includes(row.dictionary); +        const processRow = (row, index) => { +            if (titles.includes(row.dictionary) && !visited.hasOwnProperty(row.id)) { +                visited[row.id] = true; +                results.push(createResult(row, index)); +            } +        };          const db = this.db.backendDB();          const dbTransaction = db.transaction(['terms'], 'readonly'); @@ -91,8 +92,8 @@ class Database {          for (let i = 0; i < terms.length; ++i) {              const only = IDBKeyRange.only(terms[i]);              promises.push( -                Database.getAll(dbIndex1, only, i, visited, filter, createResult, results), -                Database.getAll(dbIndex2, only, i, visited, filter, createResult, results) +                Database.getAll(dbIndex1, only, i, processRow), +                Database.getAll(dbIndex2, only, i, processRow)              );          } @@ -102,9 +103,7 @@ class Database {      }      async findTermsExact(term, reading, titles) { -        if (!this.db) { -            throw 'Database not initialized'; -        } +        this.validate();          const results = [];          await this.db.terms.where('expression').equals(term).each(row => { @@ -117,9 +116,7 @@ class Database {      }      async findTermsBySequence(sequence, mainDictionary) { -        if (!this.db) { -            throw 'Database not initialized'; -        } +        this.validate();          const results = [];          await this.db.terms.where('sequence').equals(sequence).each(row => { @@ -132,9 +129,7 @@ class Database {      }      async findTermMeta(term, titles) { -        if (!this.db) { -            throw 'Database not initialized'; -        } +        this.validate();          const results = [];          await this.db.termMeta.where('expression').equals(term).each(row => { @@ -152,10 +147,13 @@ class Database {      async findTermMetaBulk(terms, titles) {          const promises = []; -        const visited = {};          const results = [];          const createResult = Database.createTermMeta; -        const filter = (row) => titles.includes(row.dictionary); +        const processRow = (row, index) => { +            if (titles.includes(row.dictionary)) { +                results.push(createResult(row, index)); +            } +        };          const db = this.db.backendDB();          const dbTransaction = db.transaction(['termMeta'], 'readonly'); @@ -164,7 +162,7 @@ class Database {          for (let i = 0; i < terms.length; ++i) {              const only = IDBKeyRange.only(terms[i]); -            promises.push(Database.getAll(dbIndex, only, i, visited, filter, createResult, results)); +            promises.push(Database.getAll(dbIndex, only, i, processRow));          }          await Promise.all(promises); @@ -173,9 +171,7 @@ class Database {      }      async findKanji(kanji, titles) { -        if (!this.db) { -            throw 'Database not initialized'; -        } +        this.validate();          const results = [];          await this.db.kanji.where('character').equals(kanji).each(row => { @@ -196,9 +192,7 @@ class Database {      }      async findKanjiMeta(kanji, titles) { -        if (!this.db) { -            throw 'Database not initialized'; -        } +        this.validate();          const results = [];          await this.db.kanjiMeta.where('character').equals(kanji).each(row => { @@ -224,9 +218,7 @@ class Database {      }      async findTagForTitle(name, title) { -        if (!this.db) { -            throw 'Database not initialized'; -        } +        this.validate();          const cache = (this.tagCache.hasOwnProperty(title) ? this.tagCache[title] : (this.tagCache[title] = {})); @@ -243,17 +235,13 @@ class Database {      }      async summarize() { -        if (this.db) { -            return this.db.dictionaries.toArray(); -        } else { -            throw 'Database not initialized'; -        } +        this.validate(); + +        return this.db.dictionaries.toArray();      }      async importDictionary(archive, progressCallback, exceptions) { -        if (!this.db) { -            throw 'Database not initialized'; -        } +        this.validate();          const maxTransactionLength = 1000;          const bulkAdd = async (table, items, total, current) => { @@ -293,12 +281,12 @@ class Database {          const indexDataLoaded = async summary => {              if (summary.version > 3) { -                throw 'Unsupported dictionary version'; +                throw new Error('Unsupported dictionary version');              }              const count = await this.db.dictionaries.where('title').equals(summary.title).count();              if (count > 0) { -                throw 'Dictionary is already imported'; +                throw new Error('Dictionary is already imported');              }              await this.db.dictionaries.add(summary); @@ -424,6 +412,12 @@ class Database {          );      } +    validate() { +        if (this.db === null) { +            throw new Error('Database not initialized'); +        } +    } +      static async importDictionaryZip(          archive,          indexDataLoaded, @@ -437,12 +431,12 @@ class Database {          const indexFile = zip.files['index.json'];          if (!indexFile) { -            throw 'No dictionary index found in archive'; +            throw new Error('No dictionary index found in archive');          }          const index = JSON.parse(await indexFile.async('string'));          if (!index.title || !index.revision) { -            throw 'Unrecognized dictionary format'; +            throw new Error('Unrecognized dictionary format');          }          const summary = { @@ -537,39 +531,32 @@ class Database {          };      } -    static getAll(dbIndex, query, index, visited, filter, createResult, results) { +    static getAll(dbIndex, query, context, processRow) {          const fn = typeof dbIndex.getAll === 'function' ? Database.getAllFast : Database.getAllUsingCursor; -        return fn(dbIndex, query, index, visited, filter, createResult, results); +        return fn(dbIndex, query, context, processRow);      } -    static getAllFast(dbIndex, query, index, visited, filter, createResult, results) { +    static getAllFast(dbIndex, query, context, processRow) {          return new Promise((resolve, reject) => {              const request = dbIndex.getAll(query);              request.onerror = (e) => reject(e);              request.onsuccess = (e) => {                  for (const row of e.target.result) { -                    if (filter(row, index) && !visited.hasOwnProperty(row.id)) { -                        visited[row.id] = true; -                        results.push(createResult(row, index)); -                    } +                    processRow(row, context);                  }                  resolve();              };          });      } -    static getAllUsingCursor(dbIndex, query, index, visited, filter, createResult, results) { +    static getAllUsingCursor(dbIndex, query, context, processRow) {          return new Promise((resolve, reject) => {              const request = dbIndex.openCursor(query, 'next');              request.onerror = (e) => reject(e);              request.onsuccess = (e) => {                  const cursor = e.target.result;                  if (cursor) { -                    const row = cursor.value; -                    if (filter(row, index) && !visited.hasOwnProperty(row.id)) { -                        visited[row.id] = true; -                        results.push(createResult(row, index)); -                    } +                    processRow(cursor.value, context);                      cursor.continue();                  } else {                      resolve(); diff --git a/ext/bg/js/deinflector.js b/ext/bg/js/deinflector.js index ad77895c..ce4b2961 100644 --- a/ext/bg/js/deinflector.js +++ b/ext/bg/js/deinflector.js @@ -19,51 +19,74 @@  class Deinflector {      constructor(reasons) { -        this.reasons = reasons; +        this.reasons = Deinflector.normalizeReasons(reasons);      }      deinflect(source) {          const results = [{              source,              term: source, -            rules: [], +            rules: 0,              definitions: [],              reasons: []          }];          for (let i = 0; i < results.length; ++i) { -            const entry = results[i]; - -            for (const reason in this.reasons) { -                for (const variant of this.reasons[reason]) { -                    let accept = entry.rules.length === 0; -                    if (!accept) { -                        for (const rule of entry.rules) { -                            if (variant.rulesIn.includes(rule)) { -                                accept = true; -                                break; -                            } -                        } -                    } - -                    if (!accept || !entry.term.endsWith(variant.kanaIn)) { -                        continue; -                    } - -                    const term = entry.term.slice(0, -variant.kanaIn.length) + variant.kanaOut; -                    if (term.length === 0) { +            const {rules, term, reasons} = results[i]; +            for (const [reason, variants] of this.reasons) { +                for (const [kanaIn, kanaOut, rulesIn, rulesOut] of variants) { +                    if ( +                        (rules !== 0 && (rules & rulesIn) === 0) || +                        !term.endsWith(kanaIn) || +                        (term.length - kanaIn.length + kanaOut.length) <= 0 +                    ) {                          continue;                      }                      results.push({                          source, -                        term, -                        rules: variant.rulesOut, +                        term: term.slice(0, -kanaIn.length) + kanaOut, +                        rules: rulesOut,                          definitions: [], -                        reasons: [reason, ...entry.reasons] +                        reasons: [reason, ...reasons]                      });                  }              }          }          return results;      } + +    static normalizeReasons(reasons) { +        const normalizedReasons = []; +        for (const reason in reasons) { +            const variants = []; +            for (const {kanaIn, kanaOut, rulesIn, rulesOut} of reasons[reason]) { +                variants.push([ +                    kanaIn, +                    kanaOut, +                    Deinflector.rulesToRuleFlags(rulesIn), +                    Deinflector.rulesToRuleFlags(rulesOut) +                ]); +            } +            normalizedReasons.push([reason, variants]); +        } +        return normalizedReasons; +    } + +    static rulesToRuleFlags(rules) { +        const ruleTypes = Deinflector.ruleTypes; +        let value = 0; +        for (const rule of rules) { +            value |= ruleTypes[rule]; +        } +        return value; +    }  } + +Deinflector.ruleTypes = { +    'v1':    0b0000001, // Verb ichidan +    'v5':    0b0000010, // Verb godan +    'vs':    0b0000100, // Verb suru +    'vk':    0b0001000, // Verb kuru +    'adj-i': 0b0010000, // Adjective i +    'iru':   0b0100000, // Intermediate -iru endings for progressive or perfect tense +}; diff --git a/ext/bg/js/options.js b/ext/bg/js/options.js index 2c9de1ec..d0aa6fd3 100644 --- a/ext/bg/js/options.js +++ b/ext/bg/js/options.js @@ -74,6 +74,18 @@ const profileOptionsVersionUpdates = [          if (utilStringHashCode(options.anki.fieldTemplates) === -250091611) {              options.anki.fieldTemplates = profileOptionsGetDefaultFieldTemplates();          } +    }, +    (options) => { +        const oldAudioSource = options.general.audioSource; +        const disabled = oldAudioSource === 'disabled'; +        options.audio.enabled = !disabled; +        options.audio.volume = options.general.audioVolume; +        options.audio.autoPlay = options.general.autoPlayAudio; +        options.audio.sources = [disabled ? 'jpod101' : oldAudioSource]; + +        delete options.general.audioSource; +        delete options.general.audioVolume; +        delete options.general.autoPlayAudio;      }  ]; @@ -247,9 +259,6 @@ function profileOptionsCreateDefaults() {      return {          general: {              enable: true, -            audioSource: 'jpod101', -            audioVolume: 100, -            autoPlayAudio: false,              resultOutputMode: 'group',              debugInfo: false,              maxResults: 32, @@ -270,6 +279,14 @@ function profileOptionsCreateDefaults() {              customPopupCss: ''          }, +        audio: { +            enabled: true, +            sources: ['jpod101', 'jpod101-alternate', 'jisho', 'custom'], +            volume: 100, +            autoPlay: false, +            customSourceUrl: '' +        }, +          scanning: {              middleMouse: true,              touchInputEnabled: true, @@ -402,7 +419,7 @@ function optionsLoad() {          chrome.storage.local.get(['options'], store => {              const error = chrome.runtime.lastError;              if (error) { -                reject(error); +                reject(new Error(error));              } else {                  resolve(store.options);              } @@ -431,7 +448,7 @@ function optionsSave(options) {          chrome.storage.local.set({options: JSON.stringify(options)}, () => {              const error = chrome.runtime.lastError;              if (error) { -                reject(error); +                reject(new Error(error));              } else {                  resolve();              } diff --git a/ext/bg/js/request.js b/ext/bg/js/request.js index e4359863..3afc1506 100644 --- a/ext/bg/js/request.js +++ b/ext/bg/js/request.js @@ -22,7 +22,7 @@ function requestJson(url, action, params) {          const xhr = new XMLHttpRequest();          xhr.overrideMimeType('application/json');          xhr.addEventListener('load', () => resolve(xhr.responseText)); -        xhr.addEventListener('error', () => reject('Failed to connect')); +        xhr.addEventListener('error', () => reject(new Error('Failed to connect')));          xhr.open(action, url);          if (params) {              xhr.send(JSON.stringify(params)); @@ -34,7 +34,7 @@ function requestJson(url, action, params) {              return JSON.parse(responseText);          }          catch (e) { -            return Promise.reject('Invalid response'); +            return Promise.reject(new Error('Invalid response'));          }      });  } diff --git a/ext/bg/js/search-frontend.js b/ext/bg/js/search-frontend.js index faec29ef..0c1a61ea 100644 --- a/ext/bg/js/search-frontend.js +++ b/ext/bg/js/search-frontend.js @@ -26,10 +26,8 @@ async function searchFrontendSetup() {      if (!options.scanning.enableOnSearchPage) { return; }      const scriptSrcs = [ -        '/fg/js/api.js',          '/fg/js/frontend-api-receiver.js',          '/fg/js/popup.js', -        '/fg/js/util.js',          '/fg/js/popup-proxy-host.js',          '/fg/js/frontend.js'      ]; diff --git a/ext/bg/js/search.js b/ext/bg/js/search.js index 13ed1e08..ead9ba6f 100644 --- a/ext/bg/js/search.js +++ b/ext/bg/js/search.js @@ -29,7 +29,8 @@ class DisplaySearch extends Display {          this.search = document.querySelector('#search');          this.query = document.querySelector('#query');          this.intro = document.querySelector('#intro'); -        this.introHidden = false; +        this.introVisible = true; +        this.introAnimationTimer = null;          this.dependencies = Object.assign({}, this.dependencies, {docRangeFromPoint, docSentenceExtract}); @@ -38,12 +39,21 @@ class DisplaySearch extends Display {          }          if (this.query !== null) {              this.query.addEventListener('input', () => this.onSearchInput(), false); + +            const query = DisplaySearch.getSearchQueryFromLocation(window.location.href); +            if (query !== null) { +                this.query.value = window.wanakana.toKana(query); +                this.onSearchQueryUpdated(query, false); +            } +              window.wanakana.bind(this.query);          } + +        this.updateSearchButton();      }      onError(error) { -        window.alert(`Error: ${error.toString ? error.toString() : error}`); +        logError(error, true);      }      onSearchClear() { @@ -56,41 +66,102 @@ class DisplaySearch extends Display {      }      onSearchInput() { -        this.search.disabled = (this.query === null || this.query.value.length === 0); +        this.updateSearchButton();      } -    async onSearch(e) { +    onSearch(e) {          if (this.query === null) {              return;          } +        e.preventDefault(); + +        const query = this.query.value; +        const queryString = query.length > 0 ? `?query=${encodeURIComponent(query)}` : ''; +        window.history.replaceState(null, '', `${window.location.pathname}${queryString}`); +        this.onSearchQueryUpdated(query, true); +    } + +    async onSearchQueryUpdated(query, animate) {          try { -            e.preventDefault(); -            this.hideIntro(); -            const {length, definitions} = await apiTermsFind(this.query.value, this.optionsContext); -            super.termsShow(definitions, await apiOptionsGet(this.optionsContext)); +            const valid = (query.length > 0); +            this.setIntroVisible(!valid, animate); +            this.updateSearchButton(); +            if (valid) { +                const {definitions} = await apiTermsFind(query, this.optionsContext); +                this.termsShow(definitions, await apiOptionsGet(this.optionsContext)); +            } else { +                this.container.textContent = ''; +            }          } catch (e) {              this.onError(e);          }      } -    hideIntro() { -        if (this.introHidden) { +    setIntroVisible(visible, animate) { +        if (this.introVisible === visible) {              return;          } -        this.introHidden = true; +        this.introVisible = visible;          if (this.intro === null) {              return;          } -        const size = this.intro.getBoundingClientRect(); -        this.intro.style.height = `${size.height}px`; -        this.intro.style.transition = 'height 0.4s ease-in-out 0s'; -        window.getComputedStyle(this.intro).getPropertyValue('height'); // Commits height so next line can start animation +        if (this.introAnimationTimer !== null) { +            clearTimeout(this.introAnimationTimer); +            this.introAnimationTimer = null; +        } + +        if (visible) { +            this.showIntro(animate); +        } else { +            this.hideIntro(animate); +        } +    } + +    showIntro(animate) { +        if (animate) { +            const duration = 0.4; +            this.intro.style.transition = ''; +            this.intro.style.height = ''; +            const size = this.intro.getBoundingClientRect(); +            this.intro.style.height = `0px`; +            this.intro.style.transition = `height ${duration}s ease-in-out 0s`; +            window.getComputedStyle(this.intro).getPropertyValue('height'); // Commits height so next line can start animation +            this.intro.style.height = `${size.height}px`; +            this.introAnimationTimer = setTimeout(() => { +                this.intro.style.height = ''; +                this.introAnimationTimer = null; +            }, duration * 1000); +        } else { +            this.intro.style.transition = ''; +            this.intro.style.height = ''; +        } +    } + +    hideIntro(animate) { +        if (animate) { +            const duration = 0.4; +            const size = this.intro.getBoundingClientRect(); +            this.intro.style.height = `${size.height}px`; +            this.intro.style.transition = `height ${duration}s ease-in-out 0s`; +            window.getComputedStyle(this.intro).getPropertyValue('height'); // Commits height so next line can start animation +        } else { +            this.intro.style.transition = ''; +        }          this.intro.style.height = '0';      } + +    updateSearchButton() { +        this.search.disabled = this.introVisible && (this.query === null || this.query.value.length === 0); +    } + +    static getSearchQueryFromLocation(url) { +        let match = /^[^\?#]*\?(?:[^&#]*&)?query=([^&#]*)/.exec(url); +        return match !== null ? decodeURIComponent(match[1]) : null; +    }  }  window.yomichan_search = new DisplaySearch(); diff --git a/ext/bg/js/settings.js b/ext/bg/js/settings.js index 9838ea02..f3b5ff16 100644 --- a/ext/bg/js/settings.js +++ b/ext/bg/js/settings.js @@ -26,10 +26,7 @@ async function formRead(options) {      options.general.showGuide = $('#show-usage-guide').prop('checked');      options.general.compactTags = $('#compact-tags').prop('checked');      options.general.compactGlossaries = $('#compact-glossaries').prop('checked'); -    options.general.autoPlayAudio = $('#auto-play-audio').prop('checked');      options.general.resultOutputMode = $('#result-output-mode').val(); -    options.general.audioSource = $('#audio-playback-source').val(); -    options.general.audioVolume = parseFloat($('#audio-playback-volume').val());      options.general.debugInfo = $('#show-debug-info').prop('checked');      options.general.showAdvanced = $('#show-advanced-options').prop('checked');      options.general.maxResults = parseInt($('#max-displayed-results').val(), 10); @@ -44,6 +41,11 @@ async function formRead(options) {      options.general.popupVerticalOffset2 = parseInt($('#popup-vertical-offset2').val(), 10);      options.general.customPopupCss = $('#custom-popup-css').val(); +    options.audio.enabled = $('#audio-playback-enabled').prop('checked'); +    options.audio.autoPlay = $('#auto-play-audio').prop('checked'); +    options.audio.volume = parseFloat($('#audio-playback-volume').val()); +    options.audio.customSourceUrl = $('#audio-custom-source').val(); +      options.scanning.middleMouse = $('#middle-mouse-button-scan').prop('checked');      options.scanning.touchInputEnabled = $('#touch-input-enabled').prop('checked');      options.scanning.selectText = $('#select-matched-text').prop('checked'); @@ -92,10 +94,7 @@ async function formWrite(options) {      $('#show-usage-guide').prop('checked', options.general.showGuide);      $('#compact-tags').prop('checked', options.general.compactTags);      $('#compact-glossaries').prop('checked', options.general.compactGlossaries); -    $('#auto-play-audio').prop('checked', options.general.autoPlayAudio);      $('#result-output-mode').val(options.general.resultOutputMode); -    $('#audio-playback-source').val(options.general.audioSource); -    $('#audio-playback-volume').val(options.general.audioVolume);      $('#show-debug-info').prop('checked', options.general.debugInfo);      $('#show-advanced-options').prop('checked', options.general.showAdvanced);      $('#max-displayed-results').val(options.general.maxResults); @@ -110,6 +109,11 @@ async function formWrite(options) {      $('#popup-vertical-offset2').val(options.general.popupVerticalOffset2);      $('#custom-popup-css').val(options.general.customPopupCss); +    $('#audio-playback-enabled').prop('checked', options.audio.enabled); +    $('#auto-play-audio').prop('checked', options.audio.autoPlay); +    $('#audio-playback-volume').val(options.audio.volume); +    $('#audio-custom-source').val(options.audio.customSourceUrl); +      $('#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); @@ -154,7 +158,7 @@ function formSetupEventListeners() {      $('#dict-file-button').click(onDictionaryImportButtonClick);      $('#field-templates-reset').click(utilAsync(onAnkiFieldTemplatesReset)); -    $('input, select, textarea').not('.anki-model').not('.profile-form *').change(utilAsync(onFormOptionsChanged)); +    $('input, select, textarea').not('.anki-model').not('.ignore-form-changes *').change(utilAsync(onFormOptionsChanged));      $('.anki-model').change(utilAsync(onAnkiModelChanged));  } @@ -244,6 +248,7 @@ async function onReady() {      showExtensionInformation();      formSetupEventListeners(); +    await audioSettingsInitialize();      await profileOptionsSetup();      storageInfoInitialize(); @@ -255,6 +260,20 @@ $(document).ready(utilAsync(onReady));  /* + * Audio + */ + +let audioSourceUI = null; + +async function audioSettingsInitialize() { +    const optionsContext = getOptionsContext(); +    const options = await apiOptionsGet(optionsContext); +    audioSourceUI = new AudioSourceUI.Container(options.audio.sources, $('.audio-source-list'), $('.audio-source-add')); +    audioSourceUI.save = () => apiOptionsSave(); +} + + +/*   * Remote options updates   */ diff --git a/ext/bg/js/templates.js b/ext/bg/js/templates.js index e12d1bf3..c61f5d7f 100644 --- a/ext/bg/js/templates.js +++ b/ext/bg/js/templates.js @@ -61,7 +61,7 @@ templates['kanji.html'] = template({"1":function(container,depth0,helpers,partia    return "<div class=\"entry\" data-type=\"kanji\">\n    <div class=\"actions\">\n"      + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.addable : depth0),{"name":"if","hash":{},"fn":container.program(11, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")      + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.source : depth0),{"name":"if","hash":{},"fn":container.program(13, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") -    + "        <img src=\"/mixed/img/entry-current.png\" class=\"current\" title=\"Current entry (Alt + Up/Down/Home/End/PgUp/PgDn)\" alt>\n    </div>\n\n    <div class=\"glyph\">" +    + "        <img src=\"/mixed/img/entry-current.svg\" class=\"current\" title=\"Current entry (Alt + Up/Down/Home/End/PgUp/PgDn)\" alt>\n    </div>\n\n    <div class=\"glyph\">"      + container.escapeExpression(((helper = (helper = helpers.character || (depth0 != null ? depth0.character : depth0)) != null ? helper : helpers.helperMissing),(typeof helper === "function" ? helper.call(alias1,{"name":"character","hash":{},"data":data}) : helper)))      + "</div>\n\n"      + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.frequencies : depth0),{"name":"if","hash":{},"fn":container.program(15, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") @@ -85,9 +85,9 @@ templates['kanji.html'] = template({"1":function(container,depth0,helpers,partia      + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.debug : depth0),{"name":"if","hash":{},"fn":container.program(31, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")      + "</div>\n";  },"11":function(container,depth0,helpers,partials,data) { -    return "        <a href=\"#\" class=\"action-view-note pending disabled\"><img src=\"/mixed/img/view-note.png\" title=\"View added note (Alt + V)\" alt></a>\n        <a href=\"#\" class=\"action-add-note pending disabled\" data-mode=\"kanji\"><img src=\"/mixed/img/add-kanji.png\" title=\"Add Kanji (Alt + K)\" alt></a>\n"; +    return "        <a href=\"#\" class=\"action-view-note pending disabled\"><img src=\"/mixed/img/view-note.svg\" title=\"View added note (Alt + V)\" alt></a>\n        <a href=\"#\" class=\"action-add-note pending disabled\" data-mode=\"kanji\"><img src=\"/mixed/img/add-term-kanji.svg\" title=\"Add Kanji (Alt + K)\" alt></a>\n";  },"13":function(container,depth0,helpers,partials,data) { -    return "        <a href=\"#\" class=\"source-term\"><img src=\"/mixed/img/source-term.png\" title=\"Source term (Alt + B)\" alt></a>\n"; +    return "        <a href=\"#\" class=\"source-term\"><img src=\"/mixed/img/source-term.svg\" title=\"Source term (Alt + B)\" alt></a>\n";  },"15":function(container,depth0,helpers,partials,data) {      var stack1; @@ -290,7 +290,7 @@ templates['terms.html'] = template({"1":function(container,depth0,helpers,partia      + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.addable : depth0),{"name":"if","hash":{},"fn":container.program(23, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "")      + ((stack1 = helpers.unless.call(alias1,(depth0 != null ? depth0.merged : depth0),{"name":"unless","hash":{},"fn":container.program(25, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "")      + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.source : depth0),{"name":"if","hash":{},"fn":container.program(28, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "") -    + "        <img src=\"/mixed/img/entry-current.png\" class=\"current\" title=\"Current entry (Alt + Up/Down/Home/End/PgUp/PgDn)\" alt>\n    </div>\n\n" +    + "        <img src=\"/mixed/img/entry-current.svg\" class=\"current\" title=\"Current entry (Alt + Up/Down/Home/End/PgUp/PgDn)\" alt>\n    </div>\n\n"      + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.merged : depth0),{"name":"if","hash":{},"fn":container.program(30, data, 0, blockParams, depths),"inverse":container.program(45, data, 0, blockParams, depths),"data":data})) != null ? stack1 : "")      + "\n"      + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.reasons : depth0),{"name":"if","hash":{},"fn":container.program(49, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "") @@ -302,15 +302,15 @@ templates['terms.html'] = template({"1":function(container,depth0,helpers,partia      + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.debug : depth0),{"name":"if","hash":{},"fn":container.program(65, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "")      + "</div>\n";  },"23":function(container,depth0,helpers,partials,data) { -    return "        <a href=\"#\" class=\"action-view-note pending disabled\"><img src=\"/mixed/img/view-note.png\" title=\"View added note (Alt + V)\" alt></a>\n        <a href=\"#\" class=\"action-add-note pending disabled\" data-mode=\"term-kanji\"><img src=\"/mixed/img/add-term-kanji.png\" title=\"Add expression (Alt + E)\" alt></a>\n        <a href=\"#\" class=\"action-add-note pending disabled\" data-mode=\"term-kana\"><img src=\"/mixed/img/add-term-kana.png\" title=\"Add reading (Alt + R)\" alt></a>\n"; +    return "        <a href=\"#\" class=\"action-view-note pending disabled\"><img src=\"/mixed/img/view-note.svg\" title=\"View added note (Alt + V)\" alt></a>\n        <a href=\"#\" class=\"action-add-note pending disabled\" data-mode=\"term-kanji\"><img src=\"/mixed/img/add-term-kanji.svg\" title=\"Add expression (Alt + E)\" alt></a>\n        <a href=\"#\" class=\"action-add-note pending disabled\" data-mode=\"term-kana\"><img src=\"/mixed/img/add-term-kana.svg\" title=\"Add reading (Alt + R)\" alt></a>\n";  },"25":function(container,depth0,helpers,partials,data) {      var stack1;    return ((stack1 = helpers["if"].call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.playback : depth0),{"name":"if","hash":{},"fn":container.program(26, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "");  },"26":function(container,depth0,helpers,partials,data) { -    return "        <a href=\"#\" class=\"action-play-audio\"><img src=\"/mixed/img/play-audio.png\" title=\"Play audio (Alt + P)\" alt></a>\n"; +    return "        <a href=\"#\" class=\"action-play-audio\"><img src=\"/mixed/img/play-audio.svg\" title=\"Play audio (Alt + P)\" alt></a>\n";  },"28":function(container,depth0,helpers,partials,data) { -    return "        <a href=\"#\" class=\"source-term\"><img src=\"/mixed/img/source-term.png\" title=\"Source term (Alt + B)\" alt></a>\n"; +    return "        <a href=\"#\" class=\"source-term\"><img src=\"/mixed/img/source-term.svg\" title=\"Source term (Alt + B)\" alt></a>\n";  },"30":function(container,depth0,helpers,partials,data,blockParams,depths) {      var stack1; @@ -342,7 +342,7 @@ templates['terms.html'] = template({"1":function(container,depth0,helpers,partia    return ((stack1 = container.lambda(depth0, depth0)) != null ? stack1 : "");  },"35":function(container,depth0,helpers,partials,data) { -    return "<a href=\"#\" class=\"action-play-audio\"><img src=\"/mixed/img/play-audio.png\" title=\"Play audio\" alt></a>"; +    return "<a href=\"#\" class=\"action-play-audio\"><img src=\"/mixed/img/play-audio.svg\" title=\"Play audio\" alt></a>";  },"37":function(container,depth0,helpers,partials,data) {      var stack1; diff --git a/ext/bg/js/translator.js b/ext/bg/js/translator.js index 65d746ea..601ee30c 100644 --- a/ext/bg/js/translator.js +++ b/ext/bg/js/translator.js @@ -238,8 +238,10 @@ class Translator {          const definitions = await this.database.findTermsBulk(uniqueDeinflectionTerms, titles);          for (const definition of definitions) { +            const definitionRules = Deinflector.rulesToRuleFlags(definition.rules);              for (const deinflection of uniqueDeinflectionArrays[definition.index]) { -                if (Translator.definitionContainsAnyRule(definition, deinflection.rules)) { +                const deinflectionRules = deinflection.rules; +                if (deinflectionRules === 0 || (definitionRules & deinflectionRules) !== 0) {                      deinflection.definitions.push(definition);                  }              } @@ -248,19 +250,6 @@ class Translator {          return deinflections.filter(e => e.definitions.length > 0);      } -    static definitionContainsAnyRule(definition, rules) { -        if (rules.length === 0) { -            return true; -        } -        const definitionRules = definition.rules; -        for (const rule of rules) { -            if (definitionRules.includes(rule)) { -                return true; -            } -        } -        return false; -    } -      getDeinflections(text) {          const deinflections = []; diff --git a/ext/bg/lang/deinflect.json b/ext/bg/lang/deinflect.json index c7977c88..682093e1 100644 --- a/ext/bg/lang/deinflect.json +++ b/ext/bg/lang/deinflect.json @@ -1186,7 +1186,7 @@              "kanaIn": "て",              "kanaOut": "る",              "rulesIn": [ -              "iru" +                "iru"              ],              "rulesOut": [                  "v1", @@ -1197,7 +1197,7 @@              "kanaIn": "いて",              "kanaOut": "く",              "rulesIn": [ -              "iru" +                "iru"              ],              "rulesOut": [                  "v5" @@ -1207,7 +1207,7 @@              "kanaIn": "いで",              "kanaOut": "ぐ",              "rulesIn": [ -              "iru" +                "iru"              ],              "rulesOut": [                  "v5" @@ -1217,7 +1217,7 @@              "kanaIn": "きて",              "kanaOut": "くる",              "rulesIn": [ -              "iru" +                "iru"              ],              "rulesOut": [                  "vk" @@ -1227,7 +1227,7 @@              "kanaIn": "くて",              "kanaOut": "い",              "rulesIn": [ -              "iru" +                "iru"              ],              "rulesOut": [                  "adj-i" @@ -1237,7 +1237,7 @@              "kanaIn": "して",              "kanaOut": "す",              "rulesIn": [ -              "iru" +                "iru"              ],              "rulesOut": [                  "v5" @@ -1247,7 +1247,7 @@              "kanaIn": "して",              "kanaOut": "する",              "rulesIn": [ -              "iru" +                "iru"              ],              "rulesOut": [                  "vs" @@ -1257,7 +1257,7 @@              "kanaIn": "って",              "kanaOut": "う",              "rulesIn": [ -              "iru" +                "iru"              ],              "rulesOut": [                  "v5" @@ -1267,7 +1267,7 @@              "kanaIn": "って",              "kanaOut": "つ",              "rulesIn": [ -              "iru" +                "iru"              ],              "rulesOut": [                  "v5" @@ -1277,7 +1277,7 @@              "kanaIn": "って",              "kanaOut": "る",              "rulesIn": [ -              "iru" +                "iru"              ],              "rulesOut": [                  "v5" @@ -1287,7 +1287,7 @@              "kanaIn": "んで",              "kanaOut": "ぬ",              "rulesIn": [ -              "iru" +                "iru"              ],              "rulesOut": [                  "v5" @@ -1297,7 +1297,7 @@              "kanaIn": "んで",              "kanaOut": "ぶ",              "rulesIn": [ -              "iru" +                "iru"              ],              "rulesOut": [                  "v5" @@ -1307,7 +1307,7 @@              "kanaIn": "んで",              "kanaOut": "む",              "rulesIn": [ -              "iru" +                "iru"              ],              "rulesOut": [                  "v5" @@ -1317,7 +1317,7 @@              "kanaIn": "のたもうて",              "kanaOut": "のたまう",              "rulesIn": [ -              "iru" +                "iru"              ],              "rulesOut": [                  "v5" @@ -1327,7 +1327,7 @@              "kanaIn": "いって",              "kanaOut": "いく",              "rulesIn": [ -              "iru" +                "iru"              ],              "rulesOut": [                  "v5" @@ -1337,7 +1337,7 @@              "kanaIn": "おうて",              "kanaOut": "おう",              "rulesIn": [ -              "iru" +                "iru"              ],              "rulesOut": [                  "v5" @@ -1347,7 +1347,7 @@              "kanaIn": "こうて",              "kanaOut": "こう",              "rulesIn": [ -              "iru" +                "iru"              ],              "rulesOut": [                  "v5" @@ -1357,7 +1357,7 @@              "kanaIn": "そうて",              "kanaOut": "そう",              "rulesIn": [ -              "iru" +                "iru"              ],              "rulesOut": [                  "v5" @@ -1367,7 +1367,7 @@              "kanaIn": "とうて",              "kanaOut": "とう",              "rulesIn": [ -              "iru" +                "iru"              ],              "rulesOut": [                  "v5" @@ -1377,7 +1377,7 @@              "kanaIn": "行って",              "kanaOut": "行く",              "rulesIn": [ -              "iru" +                "iru"              ],              "rulesOut": [                  "v5" @@ -1387,7 +1387,7 @@              "kanaIn": "逝って",              "kanaOut": "逝く",              "rulesIn": [ -              "iru" +                "iru"              ],              "rulesOut": [                  "v5" @@ -1397,7 +1397,7 @@              "kanaIn": "往って",              "kanaOut": "往く",              "rulesIn": [ -              "iru" +                "iru"              ],              "rulesOut": [                  "v5" @@ -1407,7 +1407,7 @@              "kanaIn": "請うて",              "kanaOut": "請う",              "rulesIn": [ -              "iru" +                "iru"              ],              "rulesOut": [                  "v5" @@ -1417,7 +1417,7 @@              "kanaIn": "乞うて",              "kanaOut": "乞う",              "rulesIn": [ -              "iru" +                "iru"              ],              "rulesOut": [                  "v5" @@ -1427,7 +1427,7 @@              "kanaIn": "恋うて",              "kanaOut": "恋う",              "rulesIn": [ -              "iru" +                "iru"              ],              "rulesOut": [                  "v5" @@ -1437,7 +1437,7 @@              "kanaIn": "問うて",              "kanaOut": "問う",              "rulesIn": [ -              "iru" +                "iru"              ],              "rulesOut": [                  "v5" @@ -1447,7 +1447,7 @@              "kanaIn": "負うて",              "kanaOut": "負う",              "rulesIn": [ -              "iru" +                "iru"              ],              "rulesOut": [                  "v5" @@ -1457,7 +1457,7 @@              "kanaIn": "沿うて",              "kanaOut": "沿う",              "rulesIn": [ -              "iru" +                "iru"              ],              "rulesOut": [                  "v5" @@ -1467,7 +1467,7 @@              "kanaIn": "添うて",              "kanaOut": "添う",              "rulesIn": [ -              "iru" +                "iru"              ],              "rulesOut": [                  "v5" @@ -1477,7 +1477,7 @@              "kanaIn": "副うて",              "kanaOut": "副う",              "rulesIn": [ -              "iru" +                "iru"              ],              "rulesOut": [                  "v5" @@ -1487,21 +1487,11 @@              "kanaIn": "厭うて",              "kanaOut": "厭う",              "rulesIn": [ -              "iru" +                "iru"              ],              "rulesOut": [                  "v5"              ] -        }, -        { -            "kanaIn": "で", -            "kanaOut": "", -            "rulesIn": [ -              "iru" -            ], -            "rulesOut": [ -                "neg-de" -            ]          }      ],      "-zu": [ @@ -2233,8 +2223,7 @@              "kanaIn": "ない",              "kanaOut": "る",              "rulesIn": [ -                "adj-i", -                "neg-de" +                "adj-i"              ],              "rulesOut": [                  "v1", @@ -2245,8 +2234,7 @@              "kanaIn": "かない",              "kanaOut": "く",              "rulesIn": [ -                "adj-i", -                "neg-de" +                "adj-i"              ],              "rulesOut": [                  "v5" @@ -2256,8 +2244,7 @@              "kanaIn": "がない",              "kanaOut": "ぐ",              "rulesIn": [ -                "adj-i", -                "neg-de" +                "adj-i"              ],              "rulesOut": [                  "v5" @@ -2267,8 +2254,7 @@              "kanaIn": "くない",              "kanaOut": "い",              "rulesIn": [ -                "adj-i", -                "neg-de" +                "adj-i"              ],              "rulesOut": [                  "adj-i" @@ -2278,8 +2264,7 @@              "kanaIn": "こない",              "kanaOut": "くる",              "rulesIn": [ -                "adj-i", -                "neg-de" +                "adj-i"              ],              "rulesOut": [                  "vk" @@ -2289,8 +2274,7 @@              "kanaIn": "さない",              "kanaOut": "す",              "rulesIn": [ -                "adj-i", -                "neg-de" +                "adj-i"              ],              "rulesOut": [                  "v5" @@ -2300,8 +2284,7 @@              "kanaIn": "しない",              "kanaOut": "する",              "rulesIn": [ -                "adj-i", -                "neg-de" +                "adj-i"              ],              "rulesOut": [                  "vs" @@ -2311,8 +2294,7 @@              "kanaIn": "たない",              "kanaOut": "つ",              "rulesIn": [ -                "adj-i", -                "neg-de" +                "adj-i"              ],              "rulesOut": [                  "v5" @@ -2322,8 +2304,7 @@              "kanaIn": "なない",              "kanaOut": "ぬ",              "rulesIn": [ -                "adj-i", -                "neg-de" +                "adj-i"              ],              "rulesOut": [                  "v5" @@ -2333,8 +2314,7 @@              "kanaIn": "ばない",              "kanaOut": "ぶ",              "rulesIn": [ -                "adj-i", -                "neg-de" +                "adj-i"              ],              "rulesOut": [                  "v5" @@ -2344,8 +2324,7 @@              "kanaIn": "まない",              "kanaOut": "む",              "rulesIn": [ -                "adj-i", -                "neg-de" +                "adj-i"              ],              "rulesOut": [                  "v5" @@ -2355,8 +2334,7 @@              "kanaIn": "らない",              "kanaOut": "る",              "rulesIn": [ -                "adj-i", -                "neg-de" +                "adj-i"              ],              "rulesOut": [                  "v5" @@ -2366,8 +2344,7 @@              "kanaIn": "わない",              "kanaOut": "う",              "rulesIn": [ -                "adj-i", -                "neg-de" +                "adj-i"              ],              "rulesOut": [                  "v5" @@ -3681,8 +3658,8 @@      ],      "progressive or perfect": [          { -            "kanaIn": "いる", -            "kanaOut": "", +            "kanaIn": "ている", +            "kanaOut": "て",              "rulesIn": [                  "v1"              ], @@ -3691,8 +3668,8 @@              ]          },          { -            "kanaIn": "る", -            "kanaOut": "", +            "kanaIn": "ておる", +            "kanaOut": "て",              "rulesIn": [                  "v1"              ], @@ -3701,14 +3678,54 @@              ]          },          { -            "kanaIn": "おる", -            "kanaOut": "", +            "kanaIn": "てる", +            "kanaOut": "て", +            "rulesIn": [ +                "v1" +            ], +            "rulesOut": [ +                "iru" +            ] +        }, +        { +            "kanaIn": "でいる", +            "kanaOut": "で", +            "rulesIn": [ +                "v1" +            ], +            "rulesOut": [ +                "iru" +            ] +        }, +        { +            "kanaIn": "でおる", +            "kanaOut": "で", +            "rulesIn": [ +                "v1" +            ], +            "rulesOut": [ +                "iru" +            ] +        }, +        { +            "kanaIn": "とる", +            "kanaOut": "て",              "rulesIn": [                  "v1"              ],              "rulesOut": [                  "iru"              ] +        }, +        { +            "kanaIn": "ないでいる", +            "kanaOut": "ない", +            "rulesIn": [ +                "v1" +            ], +            "rulesOut": [ +                "adj-i" +            ]          }      ]  } diff --git a/ext/bg/search.html b/ext/bg/search.html index 668b2436..3284ed43 100644 --- a/ext/bg/search.html +++ b/ext/bg/search.html @@ -20,7 +20,7 @@              <form class="input-group" style="padding-top: 10px;">                  <input type="text" class="form-control" placeholder="Search for..." id="query" autofocus>                  <span class="input-group-btn"> -                    <input type="submit" class="btn btn-default form-control" id="search" value="Search" disabled> +                    <input type="submit" class="btn btn-default form-control" id="search" value="Search">                  </span>              </form> @@ -36,14 +36,14 @@          <script src="/mixed/js/extension.js"></script> -        <script src="/bg/js/api.js"></script> -        <script src="/bg/js/audio.js"></script>          <script src="/bg/js/dictionary.js"></script>          <script src="/bg/js/handlebars.js"></script>          <script src="/bg/js/templates.js"></script> -        <script src="/bg/js/util.js"></script> +        <script src="/fg/js/api.js"></script>          <script src="/fg/js/document.js"></script>          <script src="/fg/js/source.js"></script> +        <script src="/fg/js/util.js"></script> +        <script src="/mixed/js/audio.js"></script>          <script src="/mixed/js/display.js"></script>          <script src="/mixed/js/japanese.js"></script>          <script src="/mixed/js/scroll.js"></script> diff --git a/ext/bg/settings.html b/ext/bg/settings.html index 0bc5e14c..e4710283 100644 --- a/ext/bg/settings.html +++ b/ext/bg/settings.html @@ -14,7 +14,7 @@                  <h1>Yomichan Options</h1>              </div> -            <div class="profile-form"> +            <div class="profile-form ignore-form-changes">                  <h3>Profiles</h3>                  <p class="help-block"> @@ -141,10 +141,6 @@                  </div>                  <div class="checkbox"> -                    <label><input type="checkbox" id="auto-play-audio"> Play audio automatically</label> -                </div> - -                <div class="checkbox">                      <label><input type="checkbox" id="show-advanced-options"> Show advanced options</label>                  </div> @@ -162,16 +158,6 @@                  </div>                  <div class="form-group"> -                    <label for="audio-playback-source">Audio playback source</label> -                    <select class="form-control" id="audio-playback-source"> -                        <option value="disabled">Disabled</option> -                        <option value="jpod101">JapanesePod101</option> -                        <option value="jpod101-alternate">JapanesePod101 (alternate)</option> -                        <option value="jisho">Jisho.org</option> -                    </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> @@ -180,11 +166,6 @@                  </div>                  <div class="form-group options-advanced"> -                    <label for="audio-playback-volume">Audio playback volume <span class="label-light">(percent)</span></label> -                    <input type="number" min="0" max="100" id="audio-playback-volume" class="form-control"> -                </div> - -                <div class="form-group options-advanced">                      <label for="max-displayed-results">Maximum displayed results</label>                      <input type="number" min="1" id="max-displayed-results" class="form-control">                  </div> @@ -257,6 +238,47 @@              </div>              <div> +                <h3>Audio Options</h3> + +                <div class="checkbox"> +                    <label><input type="checkbox" id="audio-playback-enabled"> Enable audio playback in search results</label> +                </div> + +                <div class="checkbox"> +                    <label><input type="checkbox" id="auto-play-audio"> Play audio automatically</label> +                </div> + +                <div class="form-group"> +                    <label for="audio-playback-volume">Audio playback volume <span class="label-light">(percent)</span></label> +                    <input type="number" min="0" max="100" id="audio-playback-volume" class="form-control"> +                </div> + +                <div class="form-group options-advanced"> +                    <label for="audio-custom-source">Custom audio source <span class="label-light">(URL)</span></label> +                    <input type="text" id="audio-custom-source" class="form-control" placeholder="Example: http://localhost/audio.mp3?expression={expression}&reading={reading}"> +                </div> + +                <div class="form-group ignore-form-changes"> +                    <label>Audio playback sources</label> +                    <div class="audio-source-list"></div> +                    <div class="input-group audio-source-options"> +                        <button class="btn btn-default audio-source-add" title="Add audio playback source"><span class="glyphicon glyphicon-plus"></span></button> +                    </div> + +                    <template id="audio-source-template"><div class="input-group audio-source"> +                        <div class="input-group-addon audio-source-prefix"></div> +                        <select class="form-control audio-source-select"> +                            <option value="jpod101">JapanesePod101</option> +                            <option value="jpod101-alternate">JapanesePod101 (alternate)</option> +                            <option value="jisho">Jisho.org</option> +                            <option value="custom">Custom</option> +                        </select> +                        <div class="input-group-btn"><button class="btn btn-danger audio-source-remove" title="Remove"><span class="glyphicon glyphicon-remove"></span></button></div> +                    </div></template> +                </div> +            </div> + +            <div>                  <h3>Scanning Options</h3>                  <div class="checkbox"> @@ -574,6 +596,7 @@          <script src="/bg/js/anki.js"></script>          <script src="/bg/js/api.js"></script> +        <script src="/bg/js/audio-ui.js"></script>          <script src="/bg/js/conditions.js"></script>          <script src="/bg/js/conditions-ui.js"></script>          <script src="/bg/js/dictionary.js"></script> diff --git a/ext/fg/float.html b/ext/fg/float.html index 52c7faa3..fe1aee8f 100644 --- a/ext/fg/float.html +++ b/ext/fg/float.html @@ -39,6 +39,7 @@          <script src="/fg/js/util.js"></script>          <script src="/fg/js/document.js"></script>          <script src="/fg/js/source.js"></script> +        <script src="/mixed/js/audio.js"></script>          <script src="/mixed/js/display.js"></script>          <script src="/mixed/js/scroll.js"></script> diff --git a/ext/fg/js/api.js b/ext/fg/js/api.js index d0ac649a..a553e514 100644 --- a/ext/fg/js/api.js +++ b/ext/fg/js/api.js @@ -45,8 +45,8 @@ function apiTemplateRender(template, data, dynamic) {      return utilInvoke('templateRender', {data, template, dynamic});  } -function apiAudioGetUrl(definition, source) { -    return utilInvoke('audioGetUrl', {definition, source}); +function apiAudioGetUrl(definition, source, optionsContext) { +    return utilInvoke('audioGetUrl', {definition, source, optionsContext});  }  function apiCommandExec(command) { diff --git a/ext/fg/js/float.js b/ext/fg/js/float.js index 88842eef..8fdb6925 100644 --- a/ext/fg/js/float.js +++ b/ext/fg/js/float.js @@ -37,7 +37,7 @@ class DisplayFloat extends Display {          if (window.yomichan_orphaned) {              this.onOrphaned();          } else { -            window.alert(`Error: ${error.toString ? error.toString() : error}`); +            logError(error, true);          }      } diff --git a/ext/fg/js/frontend-api-receiver.js b/ext/fg/js/frontend-api-receiver.js index 687e5c3c..fbfb3ab0 100644 --- a/ext/fg/js/frontend-api-receiver.js +++ b/ext/fg/js/frontend-api-receiver.js @@ -46,9 +46,8 @@ class FrontendApiReceiver {              result => {                  this.sendResult(port, id, senderId, {result});              }, -            e => { -                const error = typeof e.toString === 'function' ? e.toString() : e; -                this.sendResult(port, id, senderId, {error}); +            error => { +                this.sendResult(port, id, senderId, {error: errorToJson(error)});              });      } diff --git a/ext/fg/js/frontend-api-sender.js b/ext/fg/js/frontend-api-sender.js index 2e037e62..c6eeaeb2 100644 --- a/ext/fg/js/frontend-api-sender.js +++ b/ext/fg/js/frontend-api-sender.js @@ -31,7 +31,7 @@ class FrontendApiSender {      invoke(action, params, target) {          if (this.disconnected) { -            return Promise.reject('Disconnected'); +            return Promise.reject(new Error('Disconnected'));          }          if (this.port === null) { @@ -110,8 +110,8 @@ class FrontendApiSender {          clearTimeout(info.timer);          info.timer = null; -        if (typeof data.error === 'string') { -            info.reject(data.error); +        if (typeof data.error !== 'undefined') { +            info.reject(jsonToError(data.error));          } else {              info.resolve(data.result);          } @@ -122,7 +122,7 @@ class FrontendApiSender {          const info = this.callbacks[id];          delete this.callbacks[id];          info.timer = null; -        info.reject(reason); +        info.reject(new Error(reason));      }      static generateId(length) { diff --git a/ext/fg/js/frontend.js b/ext/fg/js/frontend.js index 58dc0e4a..88cb93a9 100644 --- a/ext/fg/js/frontend.js +++ b/ext/fg/js/frontend.js @@ -38,6 +38,9 @@ class Frontend {          this.mouseDownPrevent = false;          this.clickPrevent = false;          this.scrollPrevent = false; + +        this.enabled = false; +        this.eventListeners = [];      }      static create() { @@ -53,23 +56,7 @@ class Frontend {      async prepare() {          try { -            this.options = await apiOptionsGet(this.getOptionsContext()); - -            window.addEventListener('message', this.onWindowMessage.bind(this)); -            window.addEventListener('mousedown', this.onMouseDown.bind(this)); -            window.addEventListener('mousemove', this.onMouseMove.bind(this)); -            window.addEventListener('mouseover', this.onMouseOver.bind(this)); -            window.addEventListener('mouseout', this.onMouseOut.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)); -            } +            await this.updateOptions();              chrome.runtime.onMessage.addListener(this.onRuntimeMessage.bind(this));          } catch (e) { @@ -88,7 +75,6 @@ class Frontend {          if (              this.pendingLookup || -            !this.options.general.enable ||              (e.buttons & 0x1) !== 0x0 // Left mouse button          ) {              return; @@ -145,7 +131,7 @@ class Frontend {      }      onResize() { -        this.searchClear(true); +        this.searchClear(false);      }      onClick(e) { @@ -242,16 +228,59 @@ class Frontend {      }      onError(error) { -        console.log(error); +        logError(error, false);      } -    async updateOptions() { -        this.options = await apiOptionsGet(this.getOptionsContext()); -        if (!this.options.enable) { +    setEnabled(enabled) { +        if (enabled) { +            if (!this.enabled) { +                this.hookEvents(); +                this.enabled = true; +            } +        } else { +            if (this.enabled) { +                this.clearEventListeners(); +                this.enabled = false; +            }              this.searchClear(false);          }      } +    hookEvents() { +        this.addEventListener(window, 'message', this.onWindowMessage.bind(this)); +        this.addEventListener(window, 'mousedown', this.onMouseDown.bind(this)); +        this.addEventListener(window, 'mousemove', this.onMouseMove.bind(this)); +        this.addEventListener(window, 'mouseover', this.onMouseOver.bind(this)); +        this.addEventListener(window, 'mouseout', this.onMouseOut.bind(this)); +        this.addEventListener(window, 'resize', this.onResize.bind(this)); + +        if (this.options.scanning.touchInputEnabled) { +            this.addEventListener(window, 'click', this.onClick.bind(this)); +            this.addEventListener(window, 'touchstart', this.onTouchStart.bind(this)); +            this.addEventListener(window, 'touchend', this.onTouchEnd.bind(this)); +            this.addEventListener(window, 'touchcancel', this.onTouchCancel.bind(this)); +            this.addEventListener(window, 'touchmove', this.onTouchMove.bind(this), {passive: false}); +            this.addEventListener(window, 'contextmenu', this.onContextMenu.bind(this)); +        } +    } + +    addEventListener(node, type, listener, options) { +        node.addEventListener(type, listener, options); +        this.eventListeners.push([node, type, listener, options]); +    } + +    clearEventListeners() { +        for (const [node, type, listener, options] of this.eventListeners) { +            node.removeEventListener(type, listener, options); +        } +        this.eventListeners = []; +    } + +    async updateOptions() { +        this.options = await apiOptionsGet(this.getOptionsContext()); +        this.setEnabled(this.options.general.enable); +    } +      popupTimerSet(callback) {          const delay = this.options.scanning.delay;          if (delay > 0) { @@ -452,7 +481,7 @@ class Frontend {      searchFromTouch(x, y, cause) {          this.popupTimerClear(); -        if (!this.options.general.enable || this.pendingLookup) { +        if (this.pendingLookup) {              return;          } @@ -527,8 +556,8 @@ Frontend.runtimeMessageHandlers = {          self.updateOptions();      }, -    popupSetVisible: (self, {visible}) => { -        self.popup.setVisible(visible); +    popupSetVisibleOverride: (self, {visible}) => { +        self.popup.setVisibleOverride(visible);      }  }; diff --git a/ext/fg/js/popup-proxy-host.js b/ext/fg/js/popup-proxy-host.js index cb9741be..f933639c 100644 --- a/ext/fg/js/popup-proxy-host.js +++ b/ext/fg/js/popup-proxy-host.js @@ -41,7 +41,7 @@ class PopupProxyHost {              show: ({id, elementRect, options}) => this.show(id, elementRect, options),              showOrphaned: ({id, elementRect, options}) => this.show(id, elementRect, options),              hide: ({id, changeFocus}) => this.hide(id, changeFocus), -            setVisible: ({id, visible}) => this.setVisible(id, visible), +            setVisibleOverride: ({id, visible}) => this.setVisibleOverride(id, visible),              containsPoint: ({id, x, y}) => this.containsPoint(id, x, y),              termsShow: ({id, elementRect, writingMode, definitions, options, context}) => this.termsShow(id, elementRect, writingMode, definitions, options, context),              kanjiShow: ({id, elementRect, writingMode, definitions, options, context}) => this.kanjiShow(id, elementRect, writingMode, definitions, options, context), @@ -69,7 +69,7 @@ class PopupProxyHost {      getPopup(id) {          if (!this.popups.hasOwnProperty(id)) { -            throw 'Invalid popup ID'; +            throw new Error('Invalid popup ID');          }          return this.popups[id]; @@ -103,9 +103,9 @@ class PopupProxyHost {          return popup.hide(changeFocus);      } -    async setVisible(id, visible) { +    async setVisibleOverride(id, visible) {          const popup = this.getPopup(id); -        return popup.setVisible(visible); +        return popup.setVisibleOverride(visible);      }      async containsPoint(id, x, y) { diff --git a/ext/fg/js/popup-proxy.js b/ext/fg/js/popup-proxy.js index 072cebc9..efbd28b2 100644 --- a/ext/fg/js/popup-proxy.js +++ b/ext/fg/js/popup-proxy.js @@ -65,9 +65,9 @@ class PopupProxy {          return await this.invokeHostApi('hide', {id: this.id, changeFocus});      } -    async setVisible(visible) { +    async setVisibleOverride(visible) {          const id = await this.getPopupId(); -        return await this.invokeHostApi('setVisible', {id, visible}); +        return await this.invokeHostApi('setVisibleOverride', {id, visible});      }      async containsPoint(x, y) { @@ -98,7 +98,7 @@ class PopupProxy {      invokeHostApi(action, params={}) {          if (typeof this.parentFrameId !== 'number') { -            return Promise.reject('Invalid frame'); +            return Promise.reject(new Error('Invalid frame'));          }          return this.apiSender.invoke(action, params, `popup-proxy-host#${this.parentFrameId}`);      } diff --git a/ext/fg/js/popup.js b/ext/fg/js/popup.js index 9dff6f28..9ca91afa 100644 --- a/ext/fg/js/popup.js +++ b/ext/fg/js/popup.js @@ -34,6 +34,9 @@ class Popup {          this.container.style.height = '0px';          this.injectPromise = null;          this.isInjected = false; +        this.visible = false; +        this.visibleOverride = null; +        this.updateVisibility();      }      inject(options) { @@ -105,9 +108,11 @@ class Popup {          container.style.top = `${y}px`;          container.style.width = `${width}px`;          container.style.height = `${height}px`; -        container.style.visibility = 'visible'; -        this.hideChildren(true); +        this.setVisible(true); +        if (this.child !== null) { +            this.child.hide(true); +        }      }      static getPositionForHorizontalText(elementRect, width, height, maxWidth, maxHeight, optionsGeneral) { @@ -209,41 +214,35 @@ class Popup {      }      hide(changeFocus) { -        if (this.isContainerHidden()) { -            changeFocus = false; +        if (!this.isVisible()) { +            return; +        } + +        this.setVisible(false); +        if (this.child !== null) { +            this.child.hide(false);          } -        this.hideChildren(changeFocus); -        this.hideContainer();          if (changeFocus) {              this.focusParent();          }      } -    hideChildren(changeFocus) { -        // Recursively hides all children. -        if (this.child !== null && !this.child.isContainerHidden()) { -            this.child.hide(changeFocus); -        } -    } - -    hideContainer() { -        this.container.style.visibility = 'hidden'; +    isVisible() { +        return this.isInjected && (this.visibleOverride !== null ? this.visibleOverride : this.visible);      } -    isContainerHidden() { -        return (this.container.style.visibility === 'hidden'); +    setVisible(visible) { +        this.visible = visible; +        this.updateVisibility();      } -    isVisible() { -        return this.isInjected && this.container.style.visibility !== 'hidden'; +    setVisibleOverride(visible) { +        this.visibleOverride = visible; +        this.updateVisibility();      } -    setVisible(visible) { -        if (visible) { -            this.container.style.setProperty('display', ''); -        } else { -            this.container.style.setProperty('display', 'none', 'important'); -        } +    updateVisibility() { +        this.container.style.setProperty('visibility', this.isVisible() ? 'visible' : 'hidden', 'important');      }      focusParent() { diff --git a/ext/fg/js/util.js b/ext/fg/js/util.js index dc99274e..9a7968a7 100644 --- a/ext/fg/js/util.js +++ b/ext/fg/js/util.js @@ -30,19 +30,19 @@ function utilInvoke(action, params={}) {              chrome.runtime.sendMessage(data, (response) => {                  utilCheckLastError(chrome.runtime.lastError);                  if (response !== null && typeof response === 'object') { -                    if (response.error) { -                        reject(response.error); +                    if (typeof response.error !== 'undefined') { +                        reject(jsonToError(response.error));                      } else {                          resolve(response.result);                      }                  } else {                      const message = response === null ? 'Unexpected null response' : `Unexpected response of type ${typeof response}`; -                    reject(`${message} (${JSON.stringify(data)})`); +                    reject(new Error(`${message} (${JSON.stringify(data)})`));                  }              });          } catch (e) {              window.yomichan_orphaned = true; -            reject(e.message); +            reject(e);          }      });  } diff --git a/ext/manifest.json b/ext/manifest.json index fe6e8e2b..c69b556f 100644 --- a/ext/manifest.json +++ b/ext/manifest.json @@ -1,7 +1,7 @@  {      "manifest_version": 2,      "name": "Yomichan (testing)", -    "version": "1.8.5", +    "version": "1.8.8",      "description": "Japanese dictionary with Anki integration (testing)",      "icons": {"16": "mixed/img/icon16.png", "48": "mixed/img/icon48.png", "128": "mixed/img/icon128.png"}, diff --git a/ext/mixed/img/add-kanji.png b/ext/mixed/img/add-kanji.pngBinary files differ deleted file mode 100644 index 6332fefe..00000000 --- a/ext/mixed/img/add-kanji.png +++ /dev/null diff --git a/ext/mixed/img/add-term-kana.png b/ext/mixed/img/add-term-kana.pngBinary files differ deleted file mode 100644 index 41ff8335..00000000 --- a/ext/mixed/img/add-term-kana.png +++ /dev/null diff --git a/ext/mixed/img/add-term-kana.svg b/ext/mixed/img/add-term-kana.svg new file mode 100644 index 00000000..bb964476 --- /dev/null +++ b/ext/mixed/img/add-term-kana.svg @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg width="16" height="16" version="1.1" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> + <defs> +  <linearGradient id="linearGradient4719" x1="-1.7198" x2="-1.7198" y1="2.7781" y2="1.4552" gradientTransform="matrix(3.7795 0 0 3.7795 14 .5)" gradientUnits="userSpaceOnUse"> +   <stop stop-color="#8dba64" offset="0"/> +   <stop stop-color="#b4d495" offset="1"/> +  </linearGradient> +  <linearGradient id="linearGradient4745-5" x1="2.1167" x2="1.4552" y1="2.3812" y2="1.7198" gradientTransform="scale(3.7795 3.7795)" gradientUnits="userSpaceOnUse"> +   <stop stop-color="#376b19" offset="0"/> +   <stop stop-color="#81ab61" offset="1"/> +  </linearGradient> +  <radialGradient id="radialGradient4770-4" cx="2.1167" cy="2.1167" r=".66146" gradientTransform="matrix(2.2677 -7.9311e-7 7.9312e-7 2.2677 2.7 3.7)" gradientUnits="userSpaceOnUse"> +   <stop stop-opacity=".28986" offset="0"/> +   <stop stop-opacity="0" offset="1"/> +  </radialGradient> + </defs> + <g> +  <circle cx="7.5" cy="8.5" r="3" fill="url(#linearGradient4719)"/> +  <circle cx="7.5" cy="8.5" r="3" fill="none" stroke="url(#linearGradient4745-5)"/> +  <circle cx="7.5" cy="8.5" r="1.5" fill="url(#radialGradient4770-4)"/> +  <path d="m6 8h1v-1h1v1h1v1h-1v1h-1v-1h-1v-1" fill="#fff"/> + </g> +</svg> diff --git a/ext/mixed/img/add-term-kanji.png b/ext/mixed/img/add-term-kanji.pngBinary files differ deleted file mode 100644 index 6332fefe..00000000 --- a/ext/mixed/img/add-term-kanji.png +++ /dev/null diff --git a/ext/mixed/img/add-term-kanji.svg b/ext/mixed/img/add-term-kanji.svg new file mode 100644 index 00000000..3737eaec --- /dev/null +++ b/ext/mixed/img/add-term-kanji.svg @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg width="16" height="16" version="1.1" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> + <defs> +  <linearGradient id="linearGradient4582" x1="-1.7198" x2="-1.7198" y1="3.5719" y2=".79375" gradientTransform="matrix(3.7795 0 0 3.7795 14.5 -6.308e-7)" gradientUnits="userSpaceOnUse"> +   <stop stop-color="#6fb558" offset="0"/> +   <stop stop-color="#a5db9b" offset="1"/> +  </linearGradient> +  <linearGradient id="linearGradient4758-7" x1="7.5406" x2="5.1594" y1="3.3073" y2=".92604" gradientTransform="matrix(3.7795 0 0 3.7795 -16 -6e-7)" gradientUnits="userSpaceOnUse"> +   <stop stop-color="#34812c" offset="0"/> +   <stop stop-color="#87b870" offset="1"/> +  </linearGradient> +  <radialGradient id="radialGradient4683-3" cx="2.1167" cy="2.1167" r=".66146" gradientTransform="matrix(4.5354 8.0301e-7 -8.0301e-7 4.5354 -1.6 -1.6)" gradientUnits="userSpaceOnUse"> +   <stop stop-opacity=".28986" offset="0"/> +   <stop stop-opacity="0" offset="1"/> +  </radialGradient> + </defs> + <g> +  <circle cx="8" cy="8" r="6.5" fill="url(#linearGradient4582)"/> +  <circle cx="8" cy="8" r="5.75" fill="none" stroke="#fff" stroke-opacity=".50196" stroke-width="1.5"/> +  <circle cx="8" cy="8" r="6.5" fill="none" stroke="url(#linearGradient4758-7)"/> +  <circle cx="8" cy="8" r="3" fill="url(#radialGradient4683-3)"/> +  <path d="m5 7h2v-2h2v2h2v2h-2v2h-2v-2h-2v-2" fill="#fff"/> + </g> +</svg> diff --git a/ext/mixed/img/entry-current.png b/ext/mixed/img/entry-current.pngBinary files differ deleted file mode 100644 index bab7cc9b..00000000 --- a/ext/mixed/img/entry-current.png +++ /dev/null diff --git a/ext/mixed/img/entry-current.svg b/ext/mixed/img/entry-current.svg new file mode 100644 index 00000000..abf3f76d --- /dev/null +++ b/ext/mixed/img/entry-current.svg @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg width="16" height="16" version="1.1" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> + <defs> +  <linearGradient id="linearGradient4930" x1="3.175" x2="1.0583" y1="3.7042" y2=".52917" gradientTransform="scale(3.7795 3.7795)" gradientUnits="userSpaceOnUse"> +   <stop stop-color="#c47a00" offset="0"/> +   <stop stop-color="#fcbf00" offset="1"/> +  </linearGradient> +  <radialGradient id="radialGradient4938" cx="2.1167" cy="2.1167" r="1.9976" gradientTransform="matrix(3.7795 0 0 3.7753 -5.7066e-7 .0088978)" gradientUnits="userSpaceOnUse"> +   <stop stop-color="#ffeeb5" offset="0"/> +   <stop stop-color="#ffe079" offset="1"/> +  </radialGradient> + </defs> + <g> +  <path d="m10.25 9.375 3 4.5-2.25 1.5-3-4.5-3 4.5-2.25-1.5 3-4.5h-5.25v-2.75h5.25l-3-4.5 2.25-1.5 3 4.5 3-4.5 2.25 1.5-3 4.5h5.25v2.75z" fill="url(#radialGradient4938)" stroke="url(#linearGradient4930)" stroke-linejoin="bevel" stroke-opacity=".93333"/> + </g> +</svg> diff --git a/ext/mixed/img/play-audio.png b/ext/mixed/img/play-audio.pngBinary files differ deleted file mode 100644 index 6056d234..00000000 --- a/ext/mixed/img/play-audio.png +++ /dev/null diff --git a/ext/mixed/img/play-audio.svg b/ext/mixed/img/play-audio.svg new file mode 100644 index 00000000..1d5e2d9c --- /dev/null +++ b/ext/mixed/img/play-audio.svg @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg width="16" height="16" version="1.1" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> + <defs> +  <linearGradient id="linearGradient4825" x1="-.39687" x2="-1.1906" y1="3.2411" y2="1.1906" gradientTransform="matrix(3.7795 0 0 3.7795 8.5 -6.308e-7)" gradientUnits="userSpaceOnUse"> +   <stop stop-color="#777772" offset="0"/> +   <stop stop-color="#a9a9a9" offset="1"/> +  </linearGradient> +  <linearGradient id="linearGradient4869-7" x1=".52917" x2=".52917" y1="2.6458" y2="1.4552" gradientTransform="scale(3.7795 3.7795)" gradientUnits="userSpaceOnUse"> +   <stop stop-color="#fff" stop-opacity="0" offset="0"/> +   <stop stop-color="#fff" offset="1"/> +  </linearGradient> +  <linearGradient id="linearGradient4853-9" x1="-.26458" x2="-1.5875" y1="3.4396" y2=".7276" gradientTransform="matrix(3.7795 0 0 3.7795 8.5 -2.9535e-7)" gradientUnits="userSpaceOnUse"> +   <stop stop-color="#5d5d5a" offset="0"/> +   <stop stop-color="#90908f" offset="1"/> +  </linearGradient> +  <radialGradient id="radialGradient4898-9" cx="15" cy="8.5578" r="3.7188" gradientTransform="matrix(2.4322e-8 -2.1513 1.6807 1.9002e-8 1.3671 40.269)" gradientUnits="userSpaceOnUse"> +   <stop stop-color="#e1293b" offset="0"/> +   <stop stop-color="#e9505a" offset="1"/> +  </radialGradient> + </defs> + <g> +  <path d="m0.5 8v2.5h3.5l3.5 3v-11l-3.5 3h-3.5v2.5" fill="url(#linearGradient4825)"/> +  <path d="m1.25 10.5v-4.25h3l3-2.75" fill="none" stroke="url(#linearGradient4869-7)" stroke-opacity=".50196" stroke-width="1.5"/> +  <path d="m0.5 8v2.5h3.5l3.5 3v-11l-3.5 3h-3.5v2.5" fill="none" stroke="url(#linearGradient4853-9)" stroke-linecap="round" stroke-linejoin="round"/> +  <path d="m12.75 2.75c1.5 3 1.5 7.5 0 10.5 4.25-1.75 4.25-8.75 0-10.5m-2 2c0.75 1.5 0.75 5 0 6.5 3-1.25 3-5.25 0-6.5m-0.75 1.5-1.5 1.75 1.5 1.75c0.5-1 0.5-2.5 0-3.5" fill="url(#radialGradient4898-9)"/> + </g> +</svg> diff --git a/ext/mixed/img/source-term.png b/ext/mixed/img/source-term.pngBinary files differ deleted file mode 100644 index 2e53c698..00000000 --- a/ext/mixed/img/source-term.png +++ /dev/null diff --git a/ext/mixed/img/source-term.svg b/ext/mixed/img/source-term.svg new file mode 100644 index 00000000..a70938f2 --- /dev/null +++ b/ext/mixed/img/source-term.svg @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg width="16" height="16" version="1.1" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> + <defs> +  <linearGradient id="linearGradient5205" x2="-3.9688" y1="3.7042" y2="1.0583" gradientTransform="matrix(3.7795 0 0 3.7795 20.5 -.5)" gradientUnits="userSpaceOnUse"> +   <stop stop-color="#012e5b" offset="0"/> +   <stop stop-color="#6399c6" offset="1"/> +  </linearGradient> +  <linearGradient id="linearGradient5138-3" x1="3.8365" x2=".39687" y1="3.0427" y2=".59531" gradientTransform="matrix(3.7795 0 0 3.7795 5 -6e-7)" gradientUnits="userSpaceOnUse"> +   <stop stop-color="#012e5b" offset="0"/> +   <stop stop-color="#6399c6" offset="1"/> +  </linearGradient> +  <linearGradient id="linearGradient5227-0" x1="2.6458" x2=".52917" y1="3.9688" y2="3.3734" gradientTransform="matrix(3.7795 0 0 3.7795 -5.7066e-7 -7)" gradientUnits="userSpaceOnUse"> +   <stop stop-color="#7cbe76" offset="0"/> +   <stop stop-color="#abd8a2" offset="1"/> +  </linearGradient> +  <linearGradient id="linearGradient5219-8" x1="2.7781" x2=".13229" y1="4.101" y2="3.175" gradientTransform="matrix(3.7795 0 0 3.7795 -5.7066e-7 -7)" gradientUnits="userSpaceOnUse"> +   <stop stop-color="#205e1d" offset="0"/> +   <stop stop-color="#74c768" offset="1"/> +  </linearGradient> + </defs> + <g> +  <path d="m16 3.5h-1.5l-1 1h-1l-1-1h-6v10h5.75l1 1h1.5l1-1h1.25" fill="#5d8bb3" stroke="url(#linearGradient5205)" stroke-width="1px"/> +  <path d="m16 2.25h-1.5l-1 1h-1l-1-1h-5v9.25h5l1 1h1l1-1h1.5" fill="#f8f8f8" stroke="url(#linearGradient5138-3)" stroke-width="1px"/> +  <g fill="none" stroke-width="1px"> +   <path d="m15 8.75h1m-1-2h1m-1-2h1m-8 4h3m-3-2h3m-3-2h3" stroke="#bdbdbd"/> +   <path d="m12.5 10.25v-4.75" stroke="#a6a6a6"/> +   <path d="m13.5 5.75v4.25" stroke="#d8d8d8"/> +  </g> +  <path d="m10.5 7v-1.5h-7v-2l-3 3v1l3 3v-2h7v-1.5" fill="url(#linearGradient5227-0)" stroke="url(#linearGradient5219-8)" stroke-linejoin="round"/> + </g> +</svg> diff --git a/ext/mixed/img/view-note.png b/ext/mixed/img/view-note.pngBinary files differ deleted file mode 100644 index 7d863f94..00000000 --- a/ext/mixed/img/view-note.png +++ /dev/null diff --git a/ext/mixed/img/view-note.svg b/ext/mixed/img/view-note.svg new file mode 100644 index 00000000..3e6f1dce --- /dev/null +++ b/ext/mixed/img/view-note.svg @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg width="16" height="16" version="1.1" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> + <defs> +  <linearGradient id="linearGradient5010" x2="-3.9688" y1="3.7042" y2="1.0583" gradientTransform="matrix(3.7795 0 0 3.7795 15.5 -.5)" gradientUnits="userSpaceOnUse"> +   <stop stop-color="#012e5b" offset="0"/> +   <stop stop-color="#6399c6" offset="1"/> +  </linearGradient> +  <linearGradient id="linearGradient5018-3" x1="3.8365" x2=".39687" y1="3.0427" y2=".59531" gradientTransform="scale(3.7795 3.7795)" gradientUnits="userSpaceOnUse"> +   <stop stop-color="#012e5b" offset="0"/> +   <stop stop-color="#6399c6" offset="1"/> +  </linearGradient> + </defs> + <g stroke-width="1px"> +  <path d="m9.75 13.5-1 1h-1.5l-1-1h-5.75v-10h6l1 1h1l1-1h6v10z" fill="#5d8bb3" stroke="url(#linearGradient5010)"/> +  <path d="m14.5 11.5h-5l-1 1h-1l-1-1h-5v-9.25h5l1 1h1l1-1h5v9.25" fill="#f8f8f8" stroke="url(#linearGradient5018-3)"/> +  <g fill="none"> +   <path d="m10 8.75h3m-3-2h3m-3-2h3m-10 4h3m-3-2h3m-3-2h3" stroke="#bdbdbd"/> +   <path d="m7.5 10.25v-4.75" stroke="#a6a6a6"/> +   <path d="m8.5 5.75v4.25" stroke="#d8d8d8"/> +  </g> + </g> +</svg> diff --git a/ext/mixed/js/audio.js b/ext/mixed/js/audio.js new file mode 100644 index 00000000..b905140c --- /dev/null +++ b/ext/mixed/js/audio.js @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2019  Alex Yatskov <alex@foosoft.net> + * Author: Alex Yatskov <alex@foosoft.net> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program.  If not, see <http://www.gnu.org/licenses/>. + */ + + +function audioGetFromUrl(url) { +    return new Promise((resolve, reject) => { +        const audio = new Audio(url); +        audio.addEventListener('loadeddata', () => { +            if (audio.duration === 5.694694 || audio.duration === 5.720718) { +                // Hardcoded values for invalid audio +                reject(new Error('Could not retrieve audio')); +            } else { +                resolve(audio); +            } +        }); +        audio.addEventListener('error', () => reject(audio.error)); +    }); +} + +async function audioGetFromSources(expression, sources, optionsContext, createAudioObject, cache=null) { +    const key = `${expression.expression}:${expression.reading}`; +    if (cache !== null && cache.hasOwnProperty(expression)) { +        return cache[key]; +    } + +    for (let i = 0, ii = sources.length; i < ii; ++i) { +        const source = sources[i]; +        const url = await apiAudioGetUrl(expression, source, optionsContext); +        if (url === null) { +            continue; +        } + +        try { +            const audio = createAudioObject ? await audioGetFromUrl(url) : null; +            const result = {audio, url, source}; +            if (cache !== null) { +                cache[key] = result; +            } +            return result; +        } catch (e) { +            // NOP +        } +    } +    return {audio: null, source: null}; +} diff --git a/ext/mixed/js/display.js b/ext/mixed/js/display.js index dc64dbea..22181301 100644 --- a/ext/mixed/js/display.js +++ b/ext/mixed/js/display.js @@ -26,6 +26,8 @@ class Display {          this.context = null;          this.sequence = 0;          this.index = 0; +        this.audioPlaying = null; +        this.audioFallback = null;          this.audioCache = {};          this.optionsContext = {};          this.eventListeners = []; @@ -39,11 +41,11 @@ class Display {      }      onError(error) { -        throw 'Override me'; +        throw new Error('Override me');      }      onSearchClear() { -        throw 'Override me'; +        throw new Error('Override me');      }      onSourceTermView(e) { @@ -133,7 +135,7 @@ class Display {          const entry = link.closest('.entry');          const definitionIndex = this.entryIndexFind(entry);          const expressionIndex = Display.indexOf(entry.querySelectorAll('.expression .action-play-audio'), link); -        this.audioPlay(this.definitions[definitionIndex], expressionIndex); +        this.audioPlay(this.definitions[definitionIndex], expressionIndex, definitionIndex);      }      onNoteAdd(e) { @@ -189,7 +191,7 @@ class Display {                  addable: options.anki.enable,                  grouped: options.general.resultOutputMode === 'group',                  merged: options.general.resultOutputMode === 'merge', -                playback: options.general.audioSource !== 'disabled', +                playback: options.audio.enabled,                  compactGlossaries: options.general.compactGlossaries,                  debug: options.general.debugInfo              }; @@ -209,7 +211,7 @@ class Display {              const {index, scroll} = context || {};              this.entryScrollIntoView(index || 0, scroll); -            if (this.options.general.autoPlayAudio && this.options.general.audioSource !== 'disabled') { +            if (this.options.audio.enabled && this.options.audio.autoPlay) {                  this.autoPlayAudio();              } @@ -274,7 +276,7 @@ class Display {      }      autoPlayAudio() { -        this.audioPlay(this.definitions[0], this.firstExpressionIndex); +        this.audioPlay(this.definitions[0], this.firstExpressionIndex, 0);      }      async adderButtonUpdate(modes, sequence) { @@ -286,15 +288,23 @@ class Display {              for (let i = 0; i < states.length; ++i) {                  const state = states[i]; +                let noteId = null;                  for (const mode in state) {                      const button = this.adderButtonFind(i, mode);                      if (button === null) {                          continue;                      } -                    button.classList.toggle('disabled', !state[mode]); +                    const info = state[mode]; +                    if (!info.canAdd && noteId === null && info.noteId) { +                        noteId = info.noteId; +                    } +                    button.classList.toggle('disabled', !info.canAdd);                      button.classList.remove('pending');                  } +                if (noteId !== null) { +                    this.viewerButtonShow(i, noteId); +                }              }          } catch (e) {              this.onError(e); @@ -380,13 +390,9 @@ class Display {                  if (adderButton !== null) {                      adderButton.classList.add('disabled');                  } -                const viewerButton = this.viewerButtonFind(index); -                if (viewerButton !== null) { -                    viewerButton.classList.remove('pending', 'disabled'); -                    viewerButton.dataset.noteId = noteId; -                } +                this.viewerButtonShow(index, noteId);              } else { -                throw 'Note could note be added'; +                throw new Error('Note could not be added');              }          } catch (e) {              this.onError(e); @@ -395,37 +401,44 @@ class Display {          }      } -    async audioPlay(definition, expressionIndex) { +    async audioPlay(definition, expressionIndex, entryIndex) {          try {              this.setSpinnerVisible(true);              const expression = expressionIndex === -1 ? definition : definition.expressions[expressionIndex]; -            let url = await apiAudioGetUrl(expression, this.options.general.audioSource); -            if (!url) { -                url = '/mixed/mp3/button.mp3'; -            } -            for (const key in this.audioCache) { -                this.audioCache[key].pause(); +            if (this.audioPlaying !== null) { +                this.audioPlaying.pause(); +                this.audioPlaying = null;              } -            let audio = this.audioCache[url]; -            if (audio) { -                audio.currentTime = 0; -                audio.volume = this.options.general.audioVolume / 100.0; -                audio.play(); +            const sources = this.options.audio.sources; +            let {audio, source} = await audioGetFromSources(expression, sources, this.optionsContext, true, this.audioCache); +            let info; +            if (audio === null) { +                if (this.audioFallback === null) { +                    this.audioFallback = new Audio('/mixed/mp3/button.mp3'); +                } +                audio = this.audioFallback; +                info = 'Could not find audio';              } else { -                audio = new Audio(url); -                audio.onloadeddata = () => { -                    if (audio.duration === 5.694694 || audio.duration === 5.720718) { -                        audio = new Audio('/mixed/mp3/button.mp3'); -                    } +                info = `From source ${1 + sources.indexOf(source)}: ${source}`; +            } -                    this.audioCache[url] = audio; -                    audio.volume = this.options.general.audioVolume / 100.0; -                    audio.play(); -                }; +            const button = this.audioButtonFindImage(entryIndex); +            if (button !== null) { +                let titleDefault = button.dataset.titleDefault; +                if (!titleDefault) { +                    titleDefault = button.title || ""; +                    button.dataset.titleDefault = titleDefault; +                } +                button.title = `${titleDefault}\n${info}`;              } + +            this.audioPlaying = audio; +            audio.currentTime = 0; +            audio.volume = this.options.audio.volume / 100.0; +            audio.play();          } catch (e) {              this.onError(e);          } finally { @@ -445,7 +458,7 @@ class Display {      async getScreenshot() {          try { -            await this.setPopupVisible(false); +            await this.setPopupVisibleOverride(false);              await Display.delay(1); // Wait for popup to be hidden.              const {format, quality} = this.options.anki.screenshot; @@ -454,7 +467,7 @@ class Display {              return {dataUrl, format};          } finally { -            await this.setPopupVisible(true); +            await this.setPopupVisibleOverride(null);          }      } @@ -462,8 +475,8 @@ class Display {          return this.options.general.resultOutputMode === 'merge' ? 0 : -1;      } -    setPopupVisible(visible) { -        return apiForward('popupSetVisible', {visible}); +    setPopupVisibleOverride(visible) { +        return apiForward('popupSetVisibleOverride', {visible});      }      setSpinnerVisible(visible) { @@ -504,6 +517,20 @@ class Display {          return entry !== null ? entry.querySelector('.action-view-note') : null;      } +    viewerButtonShow(index, noteId) { +        const viewerButton = this.viewerButtonFind(index); +        if (viewerButton === null) { +            return; +        } +        viewerButton.classList.remove('pending', 'disabled'); +        viewerButton.dataset.noteId = noteId; +    } + +    audioButtonFindImage(index) { +        const entry = this.getEntry(index); +        return entry !== null ? entry.querySelector('.action-play-audio>img') : null; +    } +      static delay(time) {          return new Promise((resolve) => setTimeout(resolve, time));      } @@ -539,7 +566,7 @@ class Display {      static getKeyFromEvent(event) {          const key = event.key; -        return key.length === 1 ? key.toUpperCase() : key; +        return (typeof key === 'string' ? (key.length === 1 ? key.toUpperCase() : key) : '');      }  } @@ -633,7 +660,7 @@ Display.onKeyDownHandlers = {          if (e.altKey) {              const entry = self.getEntry(self.index);              if (entry !== null && entry.dataset.type === 'term') { -                self.audioPlay(self.definitions[self.index], self.firstExpressionIndex); +                self.audioPlay(self.definitions[self.index], self.firstExpressionIndex, self.index);              }              return true;          } diff --git a/ext/mixed/js/extension.js b/ext/mixed/js/extension.js index 5c803132..861e52a5 100644 --- a/ext/mixed/js/extension.js +++ b/ext/mixed/js/extension.js @@ -34,7 +34,7 @@ function toIterable(value) {          }      } -    throw 'Could not convert to iterable'; +    throw new Error('Could not convert to iterable');  }  function extensionHasChrome() { @@ -53,6 +53,39 @@ function extensionHasBrowser() {      }  } +function errorToJson(error) { +    return { +        name: error.name, +        message: error.message, +        stack: error.stack +    }; +} + +function jsonToError(jsonError) { +    const error = new Error(jsonError.message); +    error.name = jsonError.name; +    error.stack = jsonError.stack; +    return error; +} + +function logError(error, alert) { +    const manifest = chrome.runtime.getManifest(); +    let errorMessage = `${manifest.name} v${manifest.version} has encountered an error.\n`; +    errorMessage += `Originating URL: ${window.location.href}\n`; + +    const errorString = `${error.toString ? error.toString() : error}`; +    const stack = `${error.stack}`.trimRight(); +    errorMessage += (!stack.startsWith(errorString) ? `${errorString}\n${stack}` : `${stack}`); + +    errorMessage += '\n\nIssues can be reported at https://github.com/FooSoft/yomichan/issues'; + +    console.error(errorMessage); + +    if (alert) { +        window.alert(`${errorString}\n\nCheck the developer console for more details.`); +    } +} +  const EXTENSION_IS_BROWSER_EDGE = (      extensionHasBrowser() &&      (!extensionHasChrome() || (typeof chrome.runtime === 'undefined' && typeof browser.runtime !== 'undefined')) diff --git a/resources/icons.svg b/resources/icons.svg new file mode 100644 index 00000000..4bc46c02 --- /dev/null +++ b/resources/icons.svg @@ -0,0 +1,892 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg +   xmlns:dc="http://purl.org/dc/elements/1.1/" +   xmlns:cc="http://creativecommons.org/ns#" +   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" +   xmlns:svg="http://www.w3.org/2000/svg" +   xmlns="http://www.w3.org/2000/svg" +   xmlns:xlink="http://www.w3.org/1999/xlink" +   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" +   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" +   width="16" +   height="16" +   viewBox="0 0 16 16" +   version="1.1" +   id="svg8" +   inkscape:version="0.92.3 (2405546, 2018-03-11)" +   sodipodi:docname="icons.svg" +   inkscape:export-xdpi="96" +   inkscape:export-ydpi="96"> +  <sodipodi:namedview +     id="base" +     pagecolor="#ffffff" +     bordercolor="#666666" +     borderopacity="1.0" +     inkscape:pageopacity="0.0" +     inkscape:pageshadow="2" +     inkscape:zoom="31.678384" +     inkscape:cx="4.6828273" +     inkscape:cy="7.5629224" +     inkscape:document-units="px" +     inkscape:current-layer="layer16" +     showgrid="true" +     units="px" +     inkscape:snap-center="true" +     inkscape:window-width="1920" +     inkscape:window-height="1018" +     inkscape:window-x="-8" +     inkscape:window-y="-8" +     inkscape:window-maximized="1" +     viewbox-height="16"> +    <inkscape:grid +       dotted="false" +       spacingy="0.25" +       spacingx="0.25" +       empspacing="8" +       id="grid815" +       type="xygrid" /> +  </sodipodi:namedview> +  <defs +     id="defs2"> +    <linearGradient +       id="linearGradient5225" +       inkscape:collect="always"> +      <stop +         id="stop5221" +         offset="0" +         style="stop-color:#7cbe76;stop-opacity:1" /> +      <stop +         id="stop5223" +         offset="1" +         style="stop-color:#abd8a2;stop-opacity:1" /> +    </linearGradient> +    <linearGradient +       id="linearGradient5217" +       inkscape:collect="always"> +      <stop +         id="stop5213" +         offset="0" +         style="stop-color:#205e1d;stop-opacity:1" /> +      <stop +         id="stop5215" +         offset="1" +         style="stop-color:#74c768;stop-opacity:1" /> +    </linearGradient> +    <linearGradient +       id="linearGradient5016" +       inkscape:collect="always"> +      <stop +         id="stop5012" +         offset="0" +         style="stop-color:#012e5b;stop-opacity:1" /> +      <stop +         id="stop5014" +         offset="1" +         style="stop-color:#6399c6;stop-opacity:1" /> +    </linearGradient> +    <linearGradient +       id="linearGradient5008" +       inkscape:collect="always"> +      <stop +         id="stop5004" +         offset="0" +         style="stop-color:#012e5b;stop-opacity:1" /> +      <stop +         id="stop5006" +         offset="1" +         style="stop-color:#6399c6;stop-opacity:1" /> +    </linearGradient> +    <linearGradient +       id="linearGradient4957" +       inkscape:collect="always"> +      <stop +         id="stop4953" +         offset="0" +         style="stop-color:#000000;stop-opacity:1;" /> +      <stop +         id="stop4955" +         offset="1" +         style="stop-color:#000000;stop-opacity:0;" /> +    </linearGradient> +    <linearGradient +       id="linearGradient4936" +       inkscape:collect="always"> +      <stop +         id="stop4932" +         offset="0" +         style="stop-color:#ffeeb5;stop-opacity:1" /> +      <stop +         id="stop4934" +         offset="1" +         style="stop-color:#ffe079;stop-opacity:1" /> +    </linearGradient> +    <linearGradient +       id="linearGradient4928" +       inkscape:collect="always"> +      <stop +         id="stop4924" +         offset="0" +         style="stop-color:#c47a00;stop-opacity:1" /> +      <stop +         id="stop4926" +         offset="1" +         style="stop-color:#fcbf00;stop-opacity:1" /> +    </linearGradient> +    <linearGradient +       id="linearGradient4896" +       inkscape:collect="always"> +      <stop +         id="stop4892" +         offset="0" +         style="stop-color:#e1293b;stop-opacity:1" /> +      <stop +         id="stop4894" +         offset="1" +         style="stop-color:#e9505a;stop-opacity:1" /> +    </linearGradient> +    <linearGradient +       id="linearGradient4867" +       inkscape:collect="always"> +      <stop +         style="stop-color:#ffffff;stop-opacity:0" +         offset="0" +         id="stop4871" /> +      <stop +         id="stop4865" +         offset="1" +         style="stop-color:#ffffff;stop-opacity:1" /> +    </linearGradient> +    <linearGradient +       id="linearGradient4833" +       inkscape:collect="always"> +      <stop +         id="stop4829" +         offset="0" +         style="stop-color:#515150;stop-opacity:0.46859902" /> +      <stop +         id="stop4831" +         offset="1" +         style="stop-color:#bcbcbc;stop-opacity:0.25603864" /> +    </linearGradient> +    <linearGradient +       id="linearGradient4813" +       inkscape:collect="always"> +      <stop +         id="stop4809" +         offset="0" +         style="stop-color:#777772;stop-opacity:1" /> +      <stop +         id="stop4811" +         offset="1" +         style="stop-color:#a9a9a9;stop-opacity:1" /> +    </linearGradient> +    <linearGradient +       id="linearGradient4805" +       inkscape:collect="always"> +      <stop +         id="stop4801" +         offset="0" +         style="stop-color:#5d5d5a;stop-opacity:1" /> +      <stop +         id="stop4803" +         offset="1" +         style="stop-color:#90908f;stop-opacity:1" /> +    </linearGradient> +    <linearGradient +       inkscape:collect="always" +       id="linearGradient4778"> +      <stop +         style="stop-color:#8dba64;stop-opacity:1" +         offset="0" +         id="stop4774" /> +      <stop +         style="stop-color:#b4d495;stop-opacity:1" +         offset="1" +         id="stop4776" /> +    </linearGradient> +    <linearGradient +       id="linearGradient4756" +       inkscape:collect="always"> +      <stop +         id="stop4752" +         offset="0" +         style="stop-color:#34812c;stop-opacity:1;" /> +      <stop +         id="stop4754" +         offset="1" +         style="stop-color:#87b870;stop-opacity:1" /> +    </linearGradient> +    <linearGradient +       id="linearGradient4743" +       inkscape:collect="always"> +      <stop +         id="stop4739" +         offset="0" +         style="stop-color:#376b19;stop-opacity:1" /> +      <stop +         id="stop4741" +         offset="1" +         style="stop-color:#81ab61;stop-opacity:1" /> +    </linearGradient> +    <linearGradient +       id="linearGradient4667" +       inkscape:collect="always"> +      <stop +         id="stop4663" +         offset="0" +         style="stop-color:#000000;stop-opacity:0.28985506" /> +      <stop +         id="stop4665" +         offset="1" +         style="stop-color:#000000;stop-opacity:0;" /> +    </linearGradient> +    <linearGradient +       id="linearGradient4590" +       inkscape:collect="always"> +      <stop +         id="stop4586" +         offset="0" +         style="stop-color:#34812c;stop-opacity:1" /> +      <stop +         id="stop4588" +         offset="1" +         style="stop-color:#87b870;stop-opacity:1" /> +    </linearGradient> +    <linearGradient +       id="linearGradient4580" +       inkscape:collect="always"> +      <stop +         id="stop4576" +         offset="0" +         style="stop-color:#6fb558;stop-opacity:1" /> +      <stop +         id="stop4578" +         offset="1" +         style="stop-color:#a5db9b;stop-opacity:1" /> +    </linearGradient> +    <linearGradient +       gradientTransform="matrix(3.7795275,0,0,3.7795165,14.5,-6.308033e-7)" +       gradientUnits="userSpaceOnUse" +       y2="0.79374993" +       x2="-1.7197917" +       y1="3.5718751" +       x1="-1.7197917" +       id="linearGradient4582" +       xlink:href="#linearGradient4580" +       inkscape:collect="always" /> +    <linearGradient +       gradientTransform="translate(3.9682292,-0.13294281)" +       gradientUnits="userSpaceOnUse" +       y2="1.0583333" +       x2="-3.0427084" +       y1="3.4395833" +       x1="-0.66145837" +       id="linearGradient4592" +       xlink:href="#linearGradient4590" +       inkscape:collect="always" /> +    <radialGradient +       gradientTransform="matrix(4.5354353,8.0300972e-7,-8.0301179e-7,4.5354209,-1.6000039,-1.5999994)" +       gradientUnits="userSpaceOnUse" +       r="0.66145819" +       fy="2.1166666" +       fx="2.1166666" +       cy="2.1166666" +       cx="2.1166666" +       id="radialGradient4683" +       xlink:href="#linearGradient4667" +       inkscape:collect="always" /> +    <linearGradient +       y2="1.4552082" +       x2="-1.7197915" +       y1="2.7781248" +       x1="-1.7197915" +       gradientTransform="matrix(3.7795275,0,0,3.7795165,14,0.5)" +       gradientUnits="userSpaceOnUse" +       id="linearGradient4719" +       xlink:href="#linearGradient4778" +       inkscape:collect="always" /> +    <linearGradient +       gradientTransform="scale(3.7795275,3.7795165)" +       gradientUnits="userSpaceOnUse" +       y2="1.7197917" +       x2="1.4552083" +       y1="2.3812499" +       x1="2.1166668" +       id="linearGradient4745" +       xlink:href="#linearGradient4743" +       inkscape:collect="always" /> +    <linearGradient +       gradientTransform="matrix(3.7795275,0,0,3.7795165,-16,-6.308033e-7)" +       gradientUnits="userSpaceOnUse" +       y2="0.92604166" +       x2="5.1593752" +       y1="3.3072917" +       x1="7.5406251" +       id="linearGradient4758" +       xlink:href="#linearGradient4756" +       inkscape:collect="always" /> +    <radialGradient +       r="0.66145819" +       fy="2.1166666" +       fx="2.1166666" +       cy="2.1166666" +       cx="2.1166666" +       gradientTransform="matrix(2.2677175,-7.9311302e-7,7.9311552e-7,2.2677114,2.6999955,3.6999878)" +       gradientUnits="userSpaceOnUse" +       id="radialGradient4770" +       xlink:href="#linearGradient4667" +       inkscape:collect="always" /> +    <linearGradient +       gradientTransform="matrix(3.7795275,0,0,3.7795165,8.5,-6.308033e-7)" +       y2="1.190625" +       x2="-1.1906251" +       y1="3.2411458" +       x1="-0.39687499" +       gradientUnits="userSpaceOnUse" +       id="linearGradient4825" +       xlink:href="#linearGradient4813" +       inkscape:collect="always" /> +    <linearGradient +       gradientTransform="translate(0.26458335)" +       gradientUnits="userSpaceOnUse" +       y2="1.6536458" +       x2="1.4552083" +       y1="2.5135417" +       x1="1.4552083" +       id="linearGradient4835" +       xlink:href="#linearGradient4833" +       inkscape:collect="always" /> +    <linearGradient +       y2="0.72760415" +       x2="-1.5875" +       y1="3.4395833" +       x1="-0.26458335" +       gradientTransform="matrix(3.7795275,0,0,3.7795165,8.5,-6.308033e-7)" +       gradientUnits="userSpaceOnUse" +       id="linearGradient4853" +       xlink:href="#linearGradient4805" +       inkscape:collect="always" /> +    <linearGradient +       gradientTransform="scale(3.7795275,3.7795165)" +       gradientUnits="userSpaceOnUse" +       y2="1.4552083" +       x2="0.5291667" +       y1="2.6458333" +       x1="0.5291667" +       id="linearGradient4869" +       xlink:href="#linearGradient4867" +       inkscape:collect="always" /> +    <radialGradient +       gradientUnits="userSpaceOnUse" +       gradientTransform="matrix(2.43221e-8,-2.1512541,1.6806723,1.9001584e-8,1.3671203,40.268786)" +       r="3.71875" +       fy="8.5578127" +       fx="15" +       cy="8.5578127" +       cx="15" +       id="radialGradient4898" +       xlink:href="#linearGradient4896" +       inkscape:collect="always" /> +    <linearGradient +       gradientTransform="scale(3.7795275,3.7795165)" +       gradientUnits="userSpaceOnUse" +       y2="0.52916664" +       x2="1.0583334" +       y1="3.7041667" +       x1="3.175" +       id="linearGradient4930" +       xlink:href="#linearGradient4928" +       inkscape:collect="always" /> +    <radialGradient +       gradientUnits="userSpaceOnUse" +       gradientTransform="matrix(3.7795275,0,0,3.7753125,-5.706604e-7,0.00889777)" +       r="1.9976043" +       fy="2.1166666" +       fx="2.1166668" +       cy="2.1166666" +       cx="2.1166668" +       id="radialGradient4938" +       xlink:href="#linearGradient4936" +       inkscape:collect="always" /> +    <linearGradient +       gradientUnits="userSpaceOnUse" +       y2="-4.2666667e-008" +       x2="6.8791666" +       y1="3.175" +       x1="7.9375" +       id="linearGradient4959" +       xlink:href="#linearGradient4957" +       inkscape:collect="always" /> +    <linearGradient +       gradientTransform="matrix(3.7795275,0,0,3.7795165,15.5,-0.5)" +       gradientUnits="userSpaceOnUse" +       y2="1.0583333" +       x2="-3.96875" +       y1="3.7041667" +       x1="0" +       id="linearGradient5010" +       xlink:href="#linearGradient5008" +       inkscape:collect="always" /> +    <linearGradient +       gradientTransform="scale(3.7795275,3.7795165)" +       gradientUnits="userSpaceOnUse" +       y2="0.59531248" +       x2="0.39687499" +       y1="3.0427084" +       x1="3.8364584" +       id="linearGradient5018" +       xlink:href="#linearGradient5016" +       inkscape:collect="always" /> +    <linearGradient +       gradientTransform="matrix(3.7795275,0,0,3.7795165,5,-6.308033e-7)" +       y2="0.59531248" +       x2="0.39687499" +       y1="3.0427084" +       x1="3.8364584" +       gradientUnits="userSpaceOnUse" +       id="linearGradient5138" +       xlink:href="#linearGradient5016" +       inkscape:collect="always" /> +    <linearGradient +       y2="1.0583333" +       x2="-3.96875" +       y1="3.7041667" +       x1="0" +       gradientTransform="translate(5.4239577,-0.13229167)" +       gradientUnits="userSpaceOnUse" +       id="linearGradient5146" +       xlink:href="#linearGradient5008" +       inkscape:collect="always" /> +    <linearGradient +       y2="1.0583333" +       x2="-3.96875" +       y1="3.7041667" +       x1="0" +       gradientTransform="matrix(3.7795275,0,0,3.7795165,20.5,-0.5)" +       gradientUnits="userSpaceOnUse" +       id="linearGradient5205" +       xlink:href="#linearGradient5008" +       inkscape:collect="always" /> +    <linearGradient +       gradientTransform="matrix(3.7795275,0,0,3.7795165,-5.706604e-7,-7)" +       gradientUnits="userSpaceOnUse" +       y2="3.175" +       x2="0.13229167" +       y1="4.1010418" +       x1="2.778125" +       id="linearGradient5219" +       xlink:href="#linearGradient5217" +       inkscape:collect="always" /> +    <linearGradient +       gradientTransform="matrix(3.7795275,0,0,3.7795165,-5.706604e-7,-7)" +       gradientUnits="userSpaceOnUse" +       y2="3.3734374" +       x2="0.5291667" +       y1="3.96875" +       x1="2.6458333" +       id="linearGradient5227" +       xlink:href="#linearGradient5225" +       inkscape:collect="always" /> +  </defs> +  <metadata +     id="metadata5"> +    <rdf:RDF> +      <cc:Work +         rdf:about=""> +        <dc:format>image/svg+xml</dc:format> +        <dc:type +           rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> +        <dc:title /> +      </cc:Work> +    </rdf:RDF> +  </metadata> +  <g +     style="display:inline" +     inkscape:label="Add Term Kanji" +     id="layer5" +     inkscape:groupmode="layer"> +    <g +       style="display:none;opacity:1" +       transform="translate(0,-292.76665)" +       id="layer1" +       inkscape:groupmode="layer" +       inkscape:label="Reference"> +      <image +         width="16" +         height="16" +         preserveAspectRatio="none" +         style="image-rendering:optimizeSpeed" +         xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0 U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAJvSURBVDjLpZPrS5NhGIf9W7YvBYOkhlko qCklWChv2WyKik7blnNris72bi6dus0DLZ0TDxW1odtopDs4D8MDZuLU0kXq61CijSIIasOvv94V TUfLiB74fXngup7nvrnvJABJ/5PfLnTTdcwOj4RsdYmo5glBWP6iOtzwvIKSWstI0Wgx80SBblpK tE9KQs/We7EaWoT/8wbWP61gMmCH0lMDvokT4j25TiQU/ITFkek9Ow6+7WH2gwsmahCPdwyw75uw 9HEO2gUZSkfyI9zBPCJOoJ2SMmg46N61YO/rNoa39Xi41oFuXysMfh36/Fp0b7bAfWAH6RGi0Hgl WNCbzYgJaFjRv6zGuy+b9It96N3SQvNKiV9HvSaDfFEIxXItnPs23BzJQd6DDEVM0OKsoVwBG/1V MzpXVWhbkUM2K4oJBDYuGmbKIJ0qxsAbHfRLzbjcnUbFBIpx/qH3vQv9b3U03IQ/HfFkERTzfFj8 w8jSpR7GBE123uFEYAzaDRIqX/2JAtJbDat/COkd7CNBva2cMvq0MGxp0PRSCPF8BXjWG3FgNHc9 XPT71Ojy3sMFdfJRCeKxEsVtKwFHwALZfCUk3tIfNR8XiJwc1LmL4dg141JPKtj3WUdNFJqLGFVP C4OkR4BxajTWsChY64wmCnMxsWPCHcutKBxMVp5mxA1S+aMComToaqTRUQknLTH62kHOVEE+VQnj ahscNCy0cMBWsSI0TCQcZc5ALkEYckL5A5noWSBhfm2AecMAjbcRWV0pUTh0HE64TNf0mczcnnQy u/MilaFJCae1nw2fbz1DnVOxyGTlKeZft/Ff8x1BRssfACjTwQAAAABJRU5ErkJggg== " +         id="image4539" +         x="4.7683716e-007" +         y="292.76669" /> +    </g> +    <g +       style="display:inline" +       inkscape:label="Background" +       id="layer3" +       inkscape:groupmode="layer"> +      <circle +         cy="8" +         cx="8" +         id="path4552" +         style="fill:url(#linearGradient4582);fill-opacity:1;stroke:none;stroke-width:1;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.96470588" +         r="6.5" /> +    </g> +    <g +       style="display:inline" +       inkscape:label="Inner Outline" +       id="layer4" +       inkscape:groupmode="layer"> +      <circle +         cy="8" +         cx="8" +         id="path4595" +         style="fill:none;fill-opacity:0.95686275;stroke:#ffffff;stroke-width:1.5;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.50196078" +         r="5.75" /> +    </g> +    <g +       style="display:inline" +       inkscape:groupmode="layer" +       id="g4558" +       inkscape:label="Outer Outline"> +      <circle +         cy="8" +         cx="8" +         id="path4750" +         style="fill:none;fill-opacity:1;stroke:url(#linearGradient4758);stroke-width:1;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" +         r="6.5" /> +    </g> +    <g +       inkscape:groupmode="layer" +       id="g4599" +       inkscape:label="Plus Shadow" +       style="display:inline"> +      <circle +         cy="8" +         cx="8" +         id="path4661" +         style="fill:url(#radialGradient4683);fill-opacity:1;stroke:none;stroke-width:1;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" +         r="3" /> +    </g> +    <g +       style="display:inline" +       inkscape:label="Plus" +       id="layer2" +       inkscape:groupmode="layer"> +      <path +         id="path4543" +         d="m 5,7 c 0,0 2,0 2,0 0,0 0,-2 0,-2 0,0 2,0 2,0 0,0 0,2 0,2 0,0 2,0 2,0 0,0 0,2 0,2 0,0 -2,0 -2,0 0,0 0,2 0,2 0,0 -2,0 -2,0 C 7,11 7,9 7,9 7,9 5,9 5,9 5,9 5,7 5,7" +         style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" +         inkscape:connector-curvature="0" /> +    </g> +  </g> +  <g +     style="display:none" +     inkscape:label="Add Term Kana" +     id="layer6" +     inkscape:groupmode="layer"> +    <g +       style="display:none" +       inkscape:label="Reference" +       id="layer10" +       inkscape:groupmode="layer"> +      <image +         width="16" +         height="16" +         preserveAspectRatio="none" +         style="image-rendering:optimizeSpeed" +         xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0 U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAACwSURBVDjLY/j//z8DJZhh1ADsBuRPiazJ mxLxKa3P/39ki8Mnzwq9GqINyOgPbGhcnfh/y5Wp/y882/W/f3fW//B+3f/m2ZI9RBkQ2+7yfePl Cf83Xpv0HwR69qT+79+TDjLgO1EG+FQb/t92Zc5/ZLDp0lSQAf+JMsAyR/p7966k/+27EsCa23cm kOYCoMKW4B6N/727UsA2g2gQn+gwgBrSAcSfQM6G0h2jSRk3BgASP+M7I0sypgAAAABJRU5ErkJg gg== " +         id="image4712" +         x="0" +         y="-1.6125985e-007" /> +    </g> +    <g +       inkscape:groupmode="layer" +       id="g4717" +       inkscape:label="Background" +       style="display:inline"> +      <circle +         cx="7.5" +         cy="8.5" +         style="fill:url(#linearGradient4719);fill-opacity:1;stroke:none;stroke-width:1;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.96470588" +         id="circle4715" +         r="3" /> +    </g> +    <g +       style="display:inline" +       inkscape:label="Outline" +       id="g4737" +       inkscape:groupmode="layer"> +      <circle +         id="ellipse4735" +         style="fill:none;fill-opacity:1;stroke:url(#linearGradient4745);stroke-width:1;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" +         cy="8.5" +         cx="7.5" +         r="3" /> +    </g> +    <g +       style="display:inline" +       inkscape:label="Plus Shadow" +       id="g4766" +       inkscape:groupmode="layer"> +      <circle +         style="fill:url(#radialGradient4770);fill-opacity:1;stroke:none;stroke-width:1;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" +         id="ellipse4764" +         cx="7.5" +         cy="8.5" +         r="1.5" /> +    </g> +    <g +       inkscape:groupmode="layer" +       id="g4762" +       inkscape:label="Plus" +       style="display:inline"> +      <path +         style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" +         d="M 6,8 C 6,8 7,8 7,8 7,8 7,7 7,7 7,7 8,7 8,7 8,7 8,8 8,8 8,8 9,8 9,8 9,8 9,9 9,9 9,9 8,9 8,9 8,9 8,10 8,10 8,10 7,10 7,10 7,10 7,9 7,9 7,9 6,9 6,9 6,9 6,8 6,8" +         id="path4760" +         inkscape:connector-curvature="0" /> +    </g> +  </g> +  <g +     style="display:none" +     inkscape:label="Play Audio" +     id="layer7" +     inkscape:groupmode="layer"> +    <g +       style="display:none" +       inkscape:label="Reference" +       id="layer11" +       inkscape:groupmode="layer"> +      <image +         width="16" +         height="16" +         preserveAspectRatio="none" +         style="image-rendering:optimizeSpeed" +         xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0 U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAH0SURBVDjLxdPPS9tgGAfwgH/ATmPD0w5j MFa3IXOMFImsOKnbmCUTacW1WZM2Mf1ho6OBrohkIdJfWm9aLKhM6GF4Lz3No/+AMC/PYQXBXL1+ 95oxh1jGhsgOX/LywvN5n/fN+3IAuKuEuzagVFoO27b1/Z+BcrnUx4otx7FPLWsJvYpIM2SS9H4P qNWqfK1W8VKplHlW/G1zs4G9vS9YXPx4CaDkXOFES4Om4gceUK2WsbZWR72+gtXVFezsbKHVamF7 ewtm/sMFgBJZhd6pvm4kDndaAo2KOmt5Gfv7X9HpdNBut9FsNmFZFgPrMHKZc4DkjHyi6KC3MZNe hTOuGAH5Xx5ybK/Y3f0Mx3Fg2zaKxSIMw2DjT0inNQ84nogcUUQJHIfZquNT3hzx46DBALizg2o0 1qEoCqLRKERRRDAYhKYlWRK/AJdCMwH2BY28+Qk8fg667wdXKJjY2FiHaeaRzWYQCk1AEASGzSCZ jP/ewtik5r6eBD0dM+nRSMb1j4LuPDnkFhZymJ/PsmLdazmV0jxEkqKsK+niIQ69mKUBwdd9OAx3 SADdHtC53FyK12dVXlVlPpF4zytK7OgMyucNyHLs8m+8+2zJHRwG3fId9LxIbNU+OR6zWU57AR5y 84FKN+71//EqM2iapfv/HtPf5gcdtKR8VW88PgAAAABJRU5ErkJggg== " +         id="image4790" +         x="0" +         y="-1.6125985e-007" /> +    </g> +    <g +       style="display:inline" +       inkscape:label="Speaker" +       id="layer12" +       inkscape:groupmode="layer"> +      <path +         id="path4799" +         d="m 0.5,8 c 0,0 0,2.5 0,2.5 0,0 3.5,0 3.5,0 0,0 3.5,3 3.5,3 0,0 0,-11 0,-11 0,0 -3.5,3 -3.5,3 0,0 -3.5,0 -3.5,0 0,0 0,2.5 0,2.5" +         style="fill:url(#linearGradient4825);fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" +         inkscape:connector-curvature="0" /> +    </g> +    <g +       style="display:inline" +       inkscape:groupmode="layer" +       id="g4819" +       inkscape:label="Speaker Detail"> +      <path +         id="path4839" +         d="m 1.25,10.5 c 0,0 0,-4.25 0,-4.25 0,0 3,0 3,0 0,0 3,-2.75 3,-2.75" +         style="fill:none;stroke:url(#linearGradient4869);stroke-width:1.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.50196078" +         inkscape:connector-curvature="0" /> +    </g> +    <g +       inkscape:groupmode="layer" +       id="g4843" +       inkscape:label="Speaker Outline" +       style="display:inline"> +      <path +         style="fill:none;fill-opacity:1;stroke:url(#linearGradient4853);stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" +         d="m 0.5,8 c 0,0 0,2.5 0,2.5 0,0 3.5,0 3.5,0 0,0 3.5,3 3.5,3 0,0 0,-11 0,-11 0,0 -3.5,3 -3.5,3 0,0 -3.5,0 -3.5,0 0,0 0,2.5 0,2.5" +         id="path4841" +         inkscape:connector-curvature="0" /> +    </g> +    <g +       style="display:inline" +       inkscape:label="Waves" +       id="layer13" +       inkscape:groupmode="layer"> +      <path +         id="path4875" +         d="m 12.75,2.75 c 1.5,3 1.5,7.5 0,10.5 4.25,-1.75 4.25,-8.75 0,-10.5 0,0 0,0 0,0 m -2,2 c 0.75,1.5 0.75,5 0,6.5 3,-1.25 3,-5.25 0,-6.5 0,0 0,0 0,0 M 10,6.25 C 10,6.25 8.5,8 8.5,8 c 0,0 1.5,1.75 1.5,1.75 0.5,-1 0.5,-2.5 0,-3.5 0,0 0,0 0,0" +         style="fill:url(#radialGradient4898);fill-opacity:1;stroke:none;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" +         inkscape:connector-curvature="0" /> +    </g> +  </g> +  <g +     style="display:none" +     inkscape:label="Entry Current" +     id="layer14" +     inkscape:groupmode="layer"> +    <g +       style="display:none" +       inkscape:label="Reference" +       id="layer15" +       inkscape:groupmode="layer"> +      <image +         width="16" +         height="16" +         preserveAspectRatio="none" +         style="image-rendering:optimizeSpeed" +         xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0 U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAJ5SURBVDjLpZPNS1RhFMaff2EWLWo5tGnR aqCFRBAM0cZFwVSQpVHNQAWVMQwaSSZWtimLiKnsO5lEjKzs4y1zRK3oItfMj1FnnJkaUtNrjo45 H3eejpCKNa5anMX73vs855zfOS9I4n9i2SHbCpvph8q8A9PNcCzcz76EM9EETj+DmmqENaeBiJ3m RyuzQy5mwyVMKqiFbzNN0MxgKZOd2zj5GMZE/ZL5ooHZAntGW89s7Bw5Ws25llWcfQHrzHPYE/51 ZOQ0M4Fiitj4UQdbzhZSb+FJ63ZypJqp7p0UsTf+FN6kvoMMl3GmNY9jj+BckcF8/HoFldLzpZIq xhthJPVdkr2cifdb5sXefyAKLFvyzVJJAssisIxstILZ0DEyeJzpHifHfNBGamFZ+C9yC7bhG7BB xCrZZqWQpoiNP6S1TMBFDh4gA0VMdxfy+0NosftQX+8gGKkBY741HLoGhbnXUOZwKTn+gGa4nOlB N9MDxdJzCTmwj+wvEKPDTPUc5Zx+kOk+NxmqZOJTIXsviYGQVgKLAos/n0CbbIAS0ir1eY9kF4O+ 3UzpBYzehhaugQpdR3DwKth7EeyqEoO/oYzXwyKwDDN0ipme/VKFi0l9L8M3oYW8SwxWnIKI1XT7 Vqb6i/ntLoLTHdulhROcUJsZuJJjCsvEPpyf8m8io5U0VB6FtFNIe6da84XFEcYaNrDzLDw5DUZ9 cEwqm6zxGWYGPBTShogtQtoerV0rLA5JKy5+ubya7SdzbKKMyRG7ByPeIfvebKfAWszUdQFavKOI 0bqNbCuF4XfneAvzIaStQrpOxEpIL746rQKOD2VQbSXwtLiXg/wNTNvAOhsl8oEAAAAASUVORK5C YII= " +         id="image4912" +         x="0" +         y="-1.6125985e-007" /> +    </g> +    <g +       style="display:inline" +       inkscape:label="Shape" +       id="layer16" +       inkscape:groupmode="layer"> +      <path +         id="path4922" +         style="fill:url(#radialGradient4938);fill-opacity:1;stroke:url(#linearGradient4930);stroke-width:1;stroke-linecap:butt;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.93333333" +         d="m 10.25,9.375 3,4.5 -2.25,1.5 -3,-4.5 -3,4.5 -2.25,-1.5 3,-4.5 H 0.5 v -2.75 h 5.25 l -3,-4.5 2.25,-1.5 3,4.5 3,-4.5 2.25,1.5 -3,4.5 h 5.25 v 2.75 z" +         inkscape:connector-curvature="0" +         sodipodi:nodetypes="cccccccccccccccccccc" /> +    </g> +  </g> +  <g +     style="display:none" +     inkscape:label="View Note" +     id="layer9" +     inkscape:groupmode="layer"> +    <g +       style="display:none" +       inkscape:label="Reference" +       id="layer17" +       inkscape:groupmode="layer"> +      <image +         width="16" +         height="16" +         preserveAspectRatio="none" +         style="fill:url(#linearGradient4959);fill-opacity:1;image-rendering:optimizeSpeed" +         xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0 U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAIASURBVDjLpVPPaxNREJ6Vt01caH4oWk1T 0ZKlGIo9RG+BUsEK4kEP/Q8qPXnpqRdPBf8A8Wahhx7FQ0GF9FJ6UksqwfTSBDGyB5HkkphC9tfb 7jfbtyQQTx142byZ75v5ZnZWC4KALmICPy+2DkvKIX2f/POz83LxCL7nrz+WPNcll49DrhM9v7xd O9JW330DuXrrqkFSgig5iR2Cfv3t3gNxOnv5BwU+eZ5HuON5/PMPJZKJ+yKQfpW0S7TxdC6WJaWk yvff1LDaFRAeLZj05MHsiPTS6hua0PUqtwC5sHq9zv9RYWl+nu5cETcnJ1M0M5WlWq3GsX6/T+Vy mRzHDluZiGYAAsw0TQahV8uyyGq1qFgskm0bHIO/1+sx1rFtchJhArwEyIQ1Gg2WD2A6nWawHQJV DIWgIJfLhQowTIeE9D0mKAU8qPC0220afsWFQoH93W6X7yCDJ+DEBeBmsxnPIJVKxWQVUwry+XyU wBlKMKwA8jqdDhOVCqVAzQDVvXAXhOdGBFgymYwrGoZBmUyGjxCCdF0fSahaFdgoTHRxfTveMCXv WfkuE3Y+f40qhgT/nMitupzApdvT18bu+YeDQwY9Xl4aG9/d/URiMBhQq/dvZMeVghtT17lSZW9/ rAKsvPa/r9Fc2dw+Pe0/xI6kM9mT5vtXy+Nw2kU/5zOGRpvuMIu0YAAAAABJRU5ErkJggg== " +         id="image4950" +         x="0" +         y="-1.6125985e-007" /> +    </g> +    <g +       style="display:inline" +       inkscape:label="Back Layer" +       id="layer18" +       inkscape:groupmode="layer"> +      <path +         id="path4961" +         d="m 9.75,13.5 -1,1 h -1.5 l -1,-1 H 0.5 v -10 h 6 l 1,1 h 1 l 1,-1 h 6 v 10 z" +         style="display:inline;fill:#5d8bb3;fill-opacity:1;stroke:url(#linearGradient5010);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" +         inkscape:connector-curvature="0" +         sodipodi:nodetypes="ccccccccccccc" /> +    </g> +    <g +       style="display:inline" +       inkscape:groupmode="layer" +       id="g4994" +       inkscape:label="Front Layer"> +      <path +         style="display:inline;fill:#f8f8f8;fill-opacity:1;stroke:url(#linearGradient5018);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" +         d="m 14.5,11.5 c 0,0 -5,0 -5,0 0,0 -1,1 -1,1 0,0 -1,0 -1,0 0,0 -1,-1 -1,-1 0,0 -5,0 -5,0 0,0 0,-9.25 0,-9.25 0,0 5,0 5,0 0,0 1,1 1,1 0,0 1,0 1,0 0,0 1,-1 1,-1 0,0 5,0 5,0 0,0 0,9.25 0,9.25" +         id="path4992" +         inkscape:connector-curvature="0" /> +    </g> +    <g +       style="display:inline" +       inkscape:label="Page Details" +       id="layer19" +       inkscape:groupmode="layer"> +      <path +         d="m 10,8.75 c 0,0 3,0 3,0 m -3,-2 c 0,0 3,0 3,0 m -3,-2 c 0,0 3,0 3,0 m -10,4 c 0,0 3,0 3,0 m -3,-2 c 0,0 3,0 3,0 m -3,-2 c 0,0 3,0 3,0" +         style="display:inline;fill:none;stroke:#bdbdbd;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" +         id="path5022-2-8-0-1-8" +         inkscape:connector-curvature="0" /> +      <path +         id="path5100" +         d="m 7.5,10.25 c 0,0 0,-4.75 0,-4.75" +         style="fill:none;stroke:#a6a6a6;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" +         inkscape:connector-curvature="0" /> +      <path +         id="path5102" +         d="m 8.5,5.75 c 0,0 0,4.25 0,4.25" +         style="fill:none;stroke:#d8d8d8;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" +         inkscape:connector-curvature="0" /> +    </g> +  </g> +  <g +     style="display:none" +     inkscape:label="Source Term" +     id="layer8" +     inkscape:groupmode="layer"> +    <g +       style="display:none" +       inkscape:label="Reference" +       id="layer20" +       inkscape:groupmode="layer"> +      <image +         width="16" +         height="16" +         preserveAspectRatio="none" +         style="image-rendering:optimizeSpeed" +         xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0 U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAI6SURBVDjLpVNNaFNBEP5e8hJiSjVVTCom sdYUMbRVUhSVgFawgqgo6FFBaMEeehQ8WAQFj57FCh4KVixCERXipQhCa6kaEz00uSQIghibNn++ t7tv4+6mTU2bi3Rh+WZn95v5ZndWq1ar2MzQ1zuGHs85xwaPEIF9qz5uWbBW5vjIiY/Sd+n+qz5G KbT1CgRxnwCPmPPBHW5wLolcBTEJxfT7+RtccI5Fwg9RtdYU3Jwddgp4DVwfrXJrBpoNt87trwfm nCP2KYvU9z13ZObTB/04e7izoYRvFrP8qwspV45kMqlsxhj6u7uxd7u+q7V1KwK+NsTj8VoJIvsX n7O9Vx7K5rMgJkVpqQzTICjmSwrl+unQJDKZDMLhMLxerwqqC/IHr8PX29HSCcYZ/C1BhRVigHKK P1SgxTAx8QwyWaFQgGmaSl0qlYIuZFOmMRCLKCITh6lA0zIFkcJkZs1HmCL9e+mhUAj6g+ij6HDs 2udypXLIZd+C7M8sfuVzDdJlSYyyBrK00+n02jNefX55gRgkyAo9I05ycmx5aRlTty/AMAxVKyEE uVwOiUQCkUgEgUBA+eqvIMg9IuNLe/H4V2arEeRwuVz1jG63Gx6PR01d1+FwODY20vm7U0ftNm1m 8fciKCWidrqCNfti9IAKNv5mVvjpxlbWgB9yo2P3zqa9/+LdnLqPMwP9zf+ClC4zZgrFpgrafV7V WLG300qB9j+/sevKvSflcumUbOVtnraF9OTogLbZ7/wXRdt3lZxkvhIAAAAASUVORK5CYII= " +         id="image5115" +         x="0" +         y="-1.6125985e-007" /> +    </g> +    <g +       inkscape:groupmode="layer" +       id="g5144" +       inkscape:label="Back Layer" +       style="display:inline"> +      <path +         id="path5142" +         d="m 16,3.5 c 0,0 -1.5,0 -1.5,0 0,0 -1,1 -1,1 0,0 -1,0 -1,0 0,0 -1,-1 -1,-1 0,0 -6,0 -6,0 0,0 0,10 0,10 0,0 5.75,0 5.75,0 0,0 1,1 1,1 0,0 1.5,0 1.5,0 0,0 1,-1 1,-1 0,0 1.25,0 1.25,0" +         style="display:inline;fill:#5d8bb3;fill-opacity:1;stroke:url(#linearGradient5205);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" +         inkscape:connector-curvature="0" /> +    </g> +    <g +       inkscape:label="Front Layer" +       id="g5136" +       inkscape:groupmode="layer" +       style="display:inline"> +      <path +         id="path5134" +         d="m 16,2.25 c 0,0 -1.5,0 -1.5,0 0,0 -1,1 -1,1 0,0 -1,0 -1,0 0,0 -1,-1 -1,-1 0,0 -5,0 -5,0 0,0 0,9.25 0,9.25 0,0 5,0 5,0 0,0 1,1 1,1 0,0 1,0 1,0 0,0 1,-1 1,-1 0,0 1.5,0 1.5,0" +         style="display:inline;fill:#f8f8f8;fill-opacity:1;stroke:url(#linearGradient5138);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" +         inkscape:connector-curvature="0" /> +    </g> +    <g +       inkscape:groupmode="layer" +       id="g5124" +       inkscape:label="Page Details" +       style="display:inline"> +      <path +         id="path5118" +         style="display:inline;fill:none;stroke:#bdbdbd;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" +         d="m 15,8.75 c 0,0 1,0 1,0 m -1,-2 c 0,0 1,0 1,0 m -1,-2 c 0,0 1,0 1,0 m -8,4 c 0,0 3,0 3,0 m -3,-2 c 0,0 3,0 3,0 m -3,-2 c 0,0 3,0 3,0" +         inkscape:connector-curvature="0" /> +      <path +         style="fill:none;stroke:#a6a6a6;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" +         d="m 12.5,10.25 c 0,0 0,-4.75 0,-4.75" +         id="path5120" +         inkscape:connector-curvature="0" /> +      <path +         style="fill:none;stroke:#d8d8d8;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" +         d="m 13.5,5.75 c 0,0 0,4.25 0,4.25" +         id="path5122" +         inkscape:connector-curvature="0" /> +    </g> +    <g +       style="display:inline" +       inkscape:label="Arrow" +       id="layer21" +       inkscape:groupmode="layer"> +      <path +         id="path5211" +         d="m 10.5,7 c 0,0 0,-1.5 0,-1.5 0,0 -7,0 -7,0 0,0 0,-2 0,-2 0,0 -3,3 -3,3 0,0 0,1 0,1 0,0 3,3 3,3 0,0 0,-2 0,-2 0,0 7,0 7,0 0,0 0,-1.5 0,-1.5" +         style="fill:url(#linearGradient5227);fill-opacity:1;stroke:url(#linearGradient5219);stroke-width:1;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" +         inkscape:connector-curvature="0" /> +    </g> +  </g> +</svg> diff --git a/tmpl/kanji.html b/tmpl/kanji.html index a4bd95ee..0888d141 100644 --- a/tmpl/kanji.html +++ b/tmpl/kanji.html @@ -17,13 +17,13 @@ No data found  <div class="entry" data-type="kanji">      <div class="actions">          {{#if addable}} -        <a href="#" class="action-view-note pending disabled"><img src="/mixed/img/view-note.png" title="View added note (Alt + V)" alt></a> -        <a href="#" class="action-add-note pending disabled" data-mode="kanji"><img src="/mixed/img/add-kanji.png" title="Add Kanji (Alt + K)" alt></a> +        <a href="#" class="action-view-note pending disabled"><img src="/mixed/img/view-note.svg" title="View added note (Alt + V)" alt></a> +        <a href="#" class="action-add-note pending disabled" data-mode="kanji"><img src="/mixed/img/add-term-kanji.svg" title="Add Kanji (Alt + K)" alt></a>          {{/if}}          {{#if source}} -        <a href="#" class="source-term"><img src="/mixed/img/source-term.png" title="Source term (Alt + B)" alt></a> +        <a href="#" class="source-term"><img src="/mixed/img/source-term.svg" title="Source term (Alt + B)" alt></a>          {{/if}} -        <img src="/mixed/img/entry-current.png" class="current" title="Current entry (Alt + Up/Down/Home/End/PgUp/PgDn)" alt> +        <img src="/mixed/img/entry-current.svg" class="current" title="Current entry (Alt + Up/Down/Home/End/PgUp/PgDn)" alt>      </div>      <div class="glyph">{{character}}</div> diff --git a/tmpl/terms.html b/tmpl/terms.html index b9e5e0ad..0b967bb5 100644 --- a/tmpl/terms.html +++ b/tmpl/terms.html @@ -30,26 +30,26 @@  <div class="entry" data-type="term">      <div class="actions">          {{#if addable}} -        <a href="#" class="action-view-note pending disabled"><img src="/mixed/img/view-note.png" title="View added note (Alt + V)" alt></a> -        <a href="#" class="action-add-note pending disabled" data-mode="term-kanji"><img src="/mixed/img/add-term-kanji.png" title="Add expression (Alt + E)" alt></a> -        <a href="#" class="action-add-note pending disabled" data-mode="term-kana"><img src="/mixed/img/add-term-kana.png" title="Add reading (Alt + R)" alt></a> +        <a href="#" class="action-view-note pending disabled"><img src="/mixed/img/view-note.svg" title="View added note (Alt + V)" alt></a> +        <a href="#" class="action-add-note pending disabled" data-mode="term-kanji"><img src="/mixed/img/add-term-kanji.svg" title="Add expression (Alt + E)" alt></a> +        <a href="#" class="action-add-note pending disabled" data-mode="term-kana"><img src="/mixed/img/add-term-kana.svg" title="Add reading (Alt + R)" alt></a>          {{/if}}          {{#unless merged}}          {{#if playback}} -        <a href="#" class="action-play-audio"><img src="/mixed/img/play-audio.png" title="Play audio (Alt + P)" alt></a> +        <a href="#" class="action-play-audio"><img src="/mixed/img/play-audio.svg" title="Play audio (Alt + P)" alt></a>          {{/if}}          {{/unless}}          {{#if source}} -        <a href="#" class="source-term"><img src="/mixed/img/source-term.png" title="Source term (Alt + B)" alt></a> +        <a href="#" class="source-term"><img src="/mixed/img/source-term.svg" title="Source term (Alt + B)" alt></a>          {{/if}} -        <img src="/mixed/img/entry-current.png" class="current" title="Current entry (Alt + Up/Down/Home/End/PgUp/PgDn)" alt> +        <img src="/mixed/img/entry-current.svg" class="current" title="Current entry (Alt + Up/Down/Home/End/PgUp/PgDn)" alt>      </div>      {{#if merged}}      {{~#each expressions~}}      <div class="expression"><span class="expression-{{termFrequency}}">{{#kanjiLinks}}{{#furigana}}{{{.}}}{{/furigana}}{{/kanjiLinks}}</span><div class="peek-wrapper">              {{~#if ../playback~}} -            <a href="#" class="action-play-audio"><img src="/mixed/img/play-audio.png" title="Play audio" alt></a> +            <a href="#" class="action-play-audio"><img src="/mixed/img/play-audio.svg" title="Play audio" alt></a>              {{~/if~}}              {{~#if termTags~}}              <div class="tags"> |