diff options
Diffstat (limited to 'ext/bg/js')
32 files changed, 1216 insertions, 1096 deletions
| diff --git a/ext/bg/js/anki.js b/ext/bg/js/anki.js index 9f851f13..17b93620 100644 --- a/ext/bg/js/anki.js +++ b/ext/bg/js/anki.js @@ -74,7 +74,7 @@ class AnkiConnect {      async findNoteIds(notes) {          await this.checkVersion(); -        const actions = notes.map(note => ({ +        const actions = notes.map((note) => ({              action: 'findNotes',              params: {                  query: `deck:"${AnkiConnect.escapeQuery(note.deckName)}" ${AnkiConnect.fieldsToQuery(note.fields)}` @@ -108,11 +108,11 @@ class AnkiConnect {   */  class AnkiNull { -    async addNote(note) { +    async addNote() {          return null;      } -    async canAddNotes(notes) { +    async canAddNotes() {          return [];      } @@ -124,15 +124,15 @@ class AnkiNull {          return [];      } -    async getModelFieldNames(modelName) { +    async getModelFieldNames() {          return [];      } -    async guiBrowse(query) { +    async guiBrowse() {          return [];      } -    async findNoteIds(notes) { +    async findNoteIds() {          return [];      }  } diff --git a/ext/bg/js/api.js b/ext/bg/js/api.js index 766fb0ed..b489b8d2 100644 --- a/ext/bg/js/api.js +++ b/ext/bg/js/api.js @@ -45,7 +45,7 @@ async function apiOptionsSet(changedOptions, optionsContext, source) {      function modifyOption(path, value, options) {          let pivot = options;          for (const key of path.slice(0, -1)) { -            if (!pivot.hasOwnProperty(key)) { +            if (!hasOwn(pivot, key)) {                  return false;              }              pivot = pivot[key]; @@ -207,7 +207,7 @@ async function apiDefinitionsAddable(definitions, modes, optionsContext) {          }          if (cannotAdd.length > 0) { -            const noteIdsArray = await anki.findNoteIds(cannotAdd.map(e => e[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) { @@ -236,7 +236,7 @@ async function apiTemplateRender(template, data, dynamic) {  async function apiCommandExec(command, params) {      const handlers = apiCommandExec.handlers; -    if (handlers.hasOwnProperty(command)) { +    if (hasOwn(handlers, command)) {          const handler = handlers[command];          handler(params);      } @@ -404,7 +404,9 @@ async function apiGetBrowser() {              if (info.name === 'Fennec') {                  return 'firefox-mobile';              } -        } catch (e) { } +        } catch (e) { +            // NOP +        }          return 'firefox';      } else {          return 'chrome'; diff --git a/ext/bg/js/audio.js b/ext/bg/js/audio.js index cd42a158..dc0ba5eb 100644 --- a/ext/bg/js/audio.js +++ b/ext/bg/js/audio.js @@ -107,12 +107,12 @@ const audioUrlBuilders = {      '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)); +        return customSourceUrl.replace(/\{([^}]*)\}/g, (m0, m1) => (hasOwn(definition, m1) ? `${definition[m1]}` : m0));      }  };  async function audioGetUrl(definition, mode, optionsContext, download) { -    if (audioUrlBuilders.hasOwnProperty(mode)) { +    if (hasOwn(audioUrlBuilders, mode)) {          const handler = audioUrlBuilders[mode];          try {              return await handler(definition, optionsContext, download); @@ -133,7 +133,7 @@ function audioUrlNormalize(url, baseUrl, basePath) {                  // Begins with "/"                  url = baseUrl + url;              } -        } else if (!/^[a-z][a-z0-9\+\-\.]*:/i.test(url)) { +        } else if (!/^[a-z][a-z0-9\-+.]*:/i.test(url)) {              // No URI scheme => relative path              url = baseUrl + basePath + url;          } @@ -171,7 +171,7 @@ async function audioInject(definition, fields, sources, optionsContext) {      try {          let audioSourceDefinition = definition; -        if (definition.hasOwnProperty('expressions')) { +        if (hasOwn(definition, 'expressions')) {              audioSourceDefinition = definition.expressions[0];          } diff --git a/ext/bg/js/backend-api-forwarder.js b/ext/bg/js/backend-api-forwarder.js index 979afd16..db4d30b9 100644 --- a/ext/bg/js/backend-api-forwarder.js +++ b/ext/bg/js/backend-api-forwarder.js @@ -37,8 +37,8 @@ class BackendApiForwarder {          const forwardPort = chrome.tabs.connect(tabId, {name: 'frontend-api-receiver'}); -        port.onMessage.addListener(message => forwardPort.postMessage(message)); -        forwardPort.onMessage.addListener(message => port.postMessage(message)); +        port.onMessage.addListener((message) => forwardPort.postMessage(message)); +        forwardPort.onMessage.addListener((message) => port.postMessage(message));          port.onDisconnect.addListener(() => forwardPort.disconnect());          forwardPort.onDisconnect.addListener(() => port.disconnect());      } diff --git a/ext/bg/js/backend.js b/ext/bg/js/backend.js index 45db9660..d9f9b586 100644 --- a/ext/bg/js/backend.js +++ b/ext/bg/js/backend.js @@ -60,7 +60,7 @@ class Backend {          this.applyOptions();          const callback = () => this.checkLastError(chrome.runtime.lastError); -        chrome.tabs.query({}, tabs => { +        chrome.tabs.query({}, (tabs) => {              for (const tab of tabs) {                  chrome.tabs.sendMessage(tab.id, {action: 'optionsUpdate', params: {source}}, callback);              } @@ -73,12 +73,12 @@ class Backend {      onMessage({action, params}, sender, callback) {          const handlers = Backend.messageHandlers; -        if (handlers.hasOwnProperty(action)) { +        if (hasOwn(handlers, action)) {              const handler = handlers[action];              const promise = handler(params, sender);              promise.then( -                result => callback({result}), -                error => callback({error: errorToJson(error)}) +                (result) => callback({result}), +                (error) => callback({error: errorToJson(error)})              );          } @@ -177,7 +177,7 @@ class Backend {          }      } -    checkLastError(e) { +    checkLastError() {          // NOP      }  } diff --git a/ext/bg/js/conditions-ui.js b/ext/bg/js/conditions-ui.js index 43c6dc08..cc9db087 100644 --- a/ext/bg/js/conditions-ui.js +++ b/ext/bg/js/conditions-ui.js @@ -84,7 +84,7 @@ ConditionsUI.Container = class Container {      createDefaultCondition(type) {          let operator = '';          let value = ''; -        if (this.conditionDescriptors.hasOwnProperty(type)) { +        if (hasOwn(this.conditionDescriptors, type)) {              const conditionDescriptor = this.conditionDescriptors[type];              operator = conditionDescriptor.defaultOperator;              ({value} = this.getOperatorDefaultValue(type, operator)); @@ -96,15 +96,15 @@ ConditionsUI.Container = class Container {      }      getOperatorDefaultValue(type, operator) { -        if (this.conditionDescriptors.hasOwnProperty(type)) { +        if (hasOwn(this.conditionDescriptors, type)) {              const conditionDescriptor = this.conditionDescriptors[type]; -            if (conditionDescriptor.operators.hasOwnProperty(operator)) { +            if (hasOwn(conditionDescriptor.operators, operator)) {                  const operatorDescriptor = conditionDescriptor.operators[operator]; -                if (operatorDescriptor.hasOwnProperty('defaultValue')) { +                if (hasOwn(operatorDescriptor, 'defaultValue')) {                      return {value: operatorDescriptor.defaultValue, fromOperator: true};                  }              } -            if (conditionDescriptor.hasOwnProperty('defaultValue')) { +            if (hasOwn(conditionDescriptor, 'defaultValue')) {                  return {value: conditionDescriptor.defaultValue, fromOperator: false};              }          } @@ -219,7 +219,7 @@ ConditionsUI.Condition = class Condition {          optionGroup.empty();          const type = this.condition.type; -        if (conditionDescriptors.hasOwnProperty(type)) { +        if (hasOwn(conditionDescriptors, type)) {              const conditionDescriptor = conditionDescriptors[type];              const operators = conditionDescriptor.operators;              for (const operatorName of Object.keys(operators)) { @@ -240,23 +240,23 @@ ConditionsUI.Condition = class Condition {          };          const objects = []; -        if (conditionDescriptors.hasOwnProperty(type)) { +        if (hasOwn(conditionDescriptors, type)) {              const conditionDescriptor = conditionDescriptors[type];              objects.push(conditionDescriptor); -            if (conditionDescriptor.operators.hasOwnProperty(operator)) { +            if (hasOwn(conditionDescriptor.operators, operator)) {                  const operatorDescriptor = conditionDescriptor.operators[operator];                  objects.push(operatorDescriptor);              }          }          for (const object of objects) { -            if (object.hasOwnProperty('placeholder')) { +            if (hasOwn(object, 'placeholder')) {                  props.placeholder = object.placeholder;              }              if (object.type === 'number') {                  props.type = 'number';                  for (const prop of ['step', 'min', 'max']) { -                    if (object.hasOwnProperty(prop)) { +                    if (hasOwn(object, prop)) {                          props[prop] = object[prop];                      }                  } diff --git a/ext/bg/js/conditions.js b/ext/bg/js/conditions.js index ed4b14f5..c0f0f301 100644 --- a/ext/bg/js/conditions.js +++ b/ext/bg/js/conditions.js @@ -18,14 +18,14 @@  function conditionsValidateOptionValue(object, value) { -    if (object.hasOwnProperty('validate') && !object.validate(value)) { +    if (hasOwn(object, 'validate') && !object.validate(value)) {          throw new Error('Invalid value for condition');      } -    if (object.hasOwnProperty('transform')) { +    if (hasOwn(object, 'transform')) {          value = object.transform(value); -        if (object.hasOwnProperty('validateTransformed') && !object.validateTransformed(value)) { +        if (hasOwn(object, 'validateTransformed') && !object.validateTransformed(value)) {              throw new Error('Invalid value for condition');          }      } @@ -34,12 +34,12 @@ function conditionsValidateOptionValue(object, value) {  }  function conditionsNormalizeOptionValue(descriptors, type, operator, optionValue) { -    if (!descriptors.hasOwnProperty(type)) { +    if (!hasOwn(descriptors, type)) {          throw new Error('Invalid type');      }      const conditionDescriptor = descriptors[type]; -    if (!conditionDescriptor.operators.hasOwnProperty(operator)) { +    if (!hasOwn(conditionDescriptor.operators, operator)) {          throw new Error('Invalid operator');      } @@ -48,28 +48,28 @@ function conditionsNormalizeOptionValue(descriptors, type, operator, optionValue      let transformedValue = conditionsValidateOptionValue(conditionDescriptor, optionValue);      transformedValue = conditionsValidateOptionValue(operatorDescriptor, transformedValue); -    if (operatorDescriptor.hasOwnProperty('transformReverse')) { +    if (hasOwn(operatorDescriptor, 'transformReverse')) {          transformedValue = operatorDescriptor.transformReverse(transformedValue);      }      return transformedValue;  }  function conditionsTestValueThrowing(descriptors, type, operator, optionValue, value) { -    if (!descriptors.hasOwnProperty(type)) { +    if (!hasOwn(descriptors, type)) {          throw new Error('Invalid type');      }      const conditionDescriptor = descriptors[type]; -    if (!conditionDescriptor.operators.hasOwnProperty(operator)) { +    if (!hasOwn(conditionDescriptor.operators, operator)) {          throw new Error('Invalid operator');      }      const operatorDescriptor = conditionDescriptor.operators[operator]; -    if (operatorDescriptor.hasOwnProperty('transform')) { -        if (operatorDescriptor.hasOwnProperty('transformCache')) { +    if (hasOwn(operatorDescriptor, 'transform')) { +        if (hasOwn(operatorDescriptor, 'transformCache')) {              const key = `${optionValue}`;              const transformCache = operatorDescriptor.transformCache; -            if (transformCache.hasOwnProperty(key)) { +            if (hasOwn(transformCache, key)) {                  optionValue = transformCache[key];              } else {                  optionValue = operatorDescriptor.transform(optionValue); @@ -93,23 +93,23 @@ function conditionsTestValue(descriptors, type, operator, optionValue, value) {  function conditionsClearCaches(descriptors) {      for (const type in descriptors) { -        if (!descriptors.hasOwnProperty(type)) { +        if (!hasOwn(descriptors, type)) {              continue;          }          const conditionDescriptor = descriptors[type]; -        if (conditionDescriptor.hasOwnProperty('transformCache')) { +        if (hasOwn(conditionDescriptor, 'transformCache')) {              conditionDescriptor.transformCache = {};          }          const operatorDescriptors = conditionDescriptor.operators;          for (const operator in operatorDescriptors) { -            if (!operatorDescriptors.hasOwnProperty(operator)) { +            if (!hasOwn(operatorDescriptors, operator)) {                  continue;              }              const operatorDescriptor = operatorDescriptors[operator]; -            if (operatorDescriptor.hasOwnProperty('transformCache')) { +            if (hasOwn(operatorDescriptor, 'transformCache')) {                  operatorDescriptor.transformCache = {};              }          } diff --git a/ext/bg/js/context.js b/ext/bg/js/context.js index b288a79a..0b21f662 100644 --- a/ext/bg/js/context.js +++ b/ext/bg/js/context.js @@ -26,22 +26,24 @@ function showExtensionInfo() {  }  function setupButtonEvents(selector, command, url) { -    const node = document.querySelector(selector); -    node.addEventListener('click', (e) => { -        if (e.button !== 0) { return; } -        apiCommandExec(command, {newTab: e.ctrlKey}); -        e.preventDefault(); -    }, false); -    node.addEventListener('auxclick', (e) => { -        if (e.button !== 1) { return; } -        apiCommandExec(command, {newTab: true}); -        e.preventDefault(); -    }, false); +    const nodes = document.querySelectorAll(selector); +    for (const node of nodes) { +        node.addEventListener('click', (e) => { +            if (e.button !== 0) { return; } +            apiCommandExec(command, {newTab: e.ctrlKey}); +            e.preventDefault(); +        }, false); +        node.addEventListener('auxclick', (e) => { +            if (e.button !== 1) { return; } +            apiCommandExec(command, {newTab: true}); +            e.preventDefault(); +        }, false); -    if (typeof url === 'string') { -        node.href = url; -        node.target = '_blank'; -        node.rel = 'noopener'; +        if (typeof url === 'string') { +            node.href = url; +            node.target = '_blank'; +            node.rel = 'noopener'; +        }      }  } @@ -63,7 +65,7 @@ window.addEventListener('DOMContentLoaded', () => {          depth: 0,          url: window.location.href      }; -    apiOptionsGet(optionsContext).then(options => { +    apiOptionsGet(optionsContext).then((options) => {          const toggle = document.querySelector('#enable-search');          toggle.checked = options.general.enable;          toggle.addEventListener('change', () => apiCommandExec('toggle'), false); diff --git a/ext/bg/js/database.js b/ext/bg/js/database.js index 9b560f78..a20d5f15 100644 --- a/ext/bg/js/database.js +++ b/ext/bg/js/database.js @@ -137,7 +137,7 @@ class Database {          const visited = {};          const results = [];          const processRow = (row, index) => { -            if (titles.includes(row.dictionary) && !visited.hasOwnProperty(row.id)) { +            if (titles.includes(row.dictionary) && !hasOwn(visited, row.id)) {                  visited[row.id] = true;                  results.push(Database.createTerm(row, index));              } @@ -257,7 +257,7 @@ class Database {          const dbTerms = dbTransaction.objectStore('tagMeta');          const dbIndex = dbTerms.index('name');          const only = IDBKeyRange.only(name); -        await Database.getAll(dbIndex, only, null, row => { +        await Database.getAll(dbIndex, only, null, (row) => {              if (title === row.dictionary) {                  result = row;              } @@ -273,7 +273,7 @@ class Database {          const dbTransaction = this.db.transaction(['dictionaries'], 'readonly');          const dbDictionaries = dbTransaction.objectStore('dictionaries'); -        await Database.getAll(dbDictionaries, null, null, info => results.push(info)); +        await Database.getAll(dbDictionaries, null, null, (info) => results.push(info));          return results;      } @@ -308,7 +308,7 @@ class Database {              counts.push(null);              const index = i;              const query2 = IDBKeyRange.only(dictionaryNames[i]); -            const countPromise = Database.getCounts(targets, query2).then(v => counts[index] = v); +            const countPromise = Database.getCounts(targets, query2).then((v) => counts[index] = v);              countPromises.push(countPromise);          }          await Promise.all(countPromises); @@ -346,7 +346,7 @@ class Database {              }          }; -        const indexDataLoaded = async summary => { +        const indexDataLoaded = async (summary) => {              if (summary.version > 3) {                  throw new Error('Unsupported dictionary version');              } @@ -522,13 +522,13 @@ class Database {          await indexDataLoaded(summary); -        const buildTermBankName      = index => `term_bank_${index + 1}.json`; -        const buildTermMetaBankName  = index => `term_meta_bank_${index + 1}.json`; -        const buildKanjiBankName     = index => `kanji_bank_${index + 1}.json`; -        const buildKanjiMetaBankName = index => `kanji_meta_bank_${index + 1}.json`; -        const buildTagBankName       = index => `tag_bank_${index + 1}.json`; +        const buildTermBankName      = (index) => `term_bank_${index + 1}.json`; +        const buildTermMetaBankName  = (index) => `term_meta_bank_${index + 1}.json`; +        const buildKanjiBankName     = (index) => `kanji_bank_${index + 1}.json`; +        const buildKanjiMetaBankName = (index) => `kanji_meta_bank_${index + 1}.json`; +        const buildTagBankName       = (index) => `tag_bank_${index + 1}.json`; -        const countBanks = namer => { +        const countBanks = (namer) => {              let count = 0;              while (zip.files[namer(count)]) {                  ++count; @@ -657,7 +657,7 @@ class Database {          const counts = {};          for (const [objectStoreName, index] of targets) {              const n = objectStoreName; -            const countPromise = Database.getCount(index, query).then(count => counts[n] = count); +            const countPromise = Database.getCount(index, query).then((count) => counts[n] = count);              countPromises.push(countPromise);          }          return Promise.all(countPromises).then(() => counts); diff --git a/ext/bg/js/deinflector.js b/ext/bg/js/deinflector.js index e2fb7461..51f4723c 100644 --- a/ext/bg/js/deinflector.js +++ b/ext/bg/js/deinflector.js @@ -88,5 +88,5 @@ Deinflector.ruleTypes = {      'vs':    0b0000100, // Verb suru      'vk':    0b0001000, // Verb kuru      'adj-i': 0b0010000, // Adjective i -    'iru':   0b0100000, // Intermediate -iru endings for progressive or perfect tense +    'iru':   0b0100000  // Intermediate -iru endings for progressive or perfect tense  }; diff --git a/ext/bg/js/dictionary.js b/ext/bg/js/dictionary.js index 9aa0af9c..0b35e32e 100644 --- a/ext/bg/js/dictionary.js +++ b/ext/bg/js/dictionary.js @@ -81,7 +81,7 @@ function dictTermsUndupe(definitions) {      const definitionGroups = {};      for (const definition of definitions) {          const definitionExisting = definitionGroups[definition.id]; -        if (!definitionGroups.hasOwnProperty(definition.id) || definition.expression.length > definitionExisting.expression.length) { +        if (!hasOwn(definitionGroups, definition.id) || definition.expression.length > definitionExisting.expression.length) {              definitionGroups[definition.id] = definition;          }      } @@ -99,8 +99,8 @@ function dictTermsCompressTags(definitions) {      let lastPartOfSpeech = '';      for (const definition of definitions) { -        const dictionary = JSON.stringify(definition.definitionTags.filter(tag => tag.category === 'dictionary').map(tag => tag.name).sort()); -        const partOfSpeech = JSON.stringify(definition.definitionTags.filter(tag => tag.category === 'partOfSpeech').map(tag => tag.name).sort()); +        const dictionary = JSON.stringify(definition.definitionTags.filter((tag) => tag.category === 'dictionary').map((tag) => tag.name).sort()); +        const partOfSpeech = JSON.stringify(definition.definitionTags.filter((tag) => tag.category === 'partOfSpeech').map((tag) => tag.name).sort());          const filterOutCategories = []; @@ -117,7 +117,7 @@ function dictTermsCompressTags(definitions) {              lastPartOfSpeech = partOfSpeech;          } -        definition.definitionTags = definition.definitionTags.filter(tag => !filterOutCategories.includes(tag.category)); +        definition.definitionTags = definition.definitionTags.filter((tag) => !filterOutCategories.includes(tag.category));      }  } @@ -131,7 +131,7 @@ function dictTermsGroup(definitions, dictionaries) {          }          const keyString = key.toString(); -        if (groups.hasOwnProperty(keyString)) { +        if (hasOwn(groups, keyString)) {              groups[keyString].push(definition);          } else {              groups[keyString] = [definition]; @@ -231,7 +231,7 @@ function dictTermsMergeByGloss(result, definitions, appendTo, mergedIndices) {          result.reading.add(definition.reading);          for (const tag of definition.definitionTags) { -            if (!definitionsByGloss[gloss].definitionTags.find(existingTag => existingTag.name === tag.name)) { +            if (!definitionsByGloss[gloss].definitionTags.find((existingTag) => existingTag.name === tag.name)) {                  definitionsByGloss[gloss].definitionTags.push(tag);              }          } @@ -246,7 +246,7 @@ function dictTermsMergeByGloss(result, definitions, appendTo, mergedIndices) {              }              for (const tag of definition.termTags) { -                if (!result.expressions.get(definition.expression).get(definition.reading).find(existingTag => existingTag.name === tag.name)) { +                if (!result.expressions.get(definition.expression).get(definition.reading).find((existingTag) => existingTag.name === tag.name)) {                      result.expressions.get(definition.expression).get(definition.reading).push(tag);                  }              } @@ -322,7 +322,7 @@ async function dictFieldFormat(field, definition, mode, options, exceptions) {          compactGlossaries: options.general.compactGlossaries      };      const markers = dictFieldFormat.markers; -    const pattern = /\{([\w\-]+)\}/g; +    const pattern = /\{([\w-]+)\}/g;      return await stringReplaceAsync(field, pattern, async (g0, marker) => {          if (!markers.has(marker)) {              return g0; diff --git a/ext/bg/js/handlebars.js b/ext/bg/js/handlebars.js index d6307e1d..8f43cf9a 100644 --- a/ext/bg/js/handlebars.js +++ b/ext/bg/js/handlebars.js @@ -89,7 +89,7 @@ function handlebarsRegexReplace(...args) {      let value = args[args.length - 1].fn(this);      if (args.length >= 3) {          try { -            let flags = args.length > 3 ? args[2] : 'g'; +            const flags = args.length > 3 ? args[2] : 'g';              const regex = new RegExp(args[0], flags);              value = value.replace(regex, args[1]);          } catch (e) { diff --git a/ext/bg/js/mecab.js b/ext/bg/js/mecab.js index 246f8bba..62111f73 100644 --- a/ext/bg/js/mecab.js +++ b/ext/bg/js/mecab.js @@ -60,7 +60,7 @@ class Mecab {      }      onNativeMessage({sequence, data}) { -        if (this.listeners.hasOwnProperty(sequence)) { +        if (hasOwn(this.listeners, sequence)) {              const {callback, timer} = this.listeners[sequence];              clearTimeout(timer);              callback(data); @@ -81,7 +81,7 @@ class Mecab {                      delete this.listeners[sequence];                      reject(new Error(`Mecab invoke timed out in ${Mecab.timeout} ms`));                  }, Mecab.timeout) -            } +            };              this.port.postMessage({action, params, sequence});          }); diff --git a/ext/bg/js/options.js b/ext/bg/js/options.js index b9bf85f3..e53a8a13 100644 --- a/ext/bg/js/options.js +++ b/ext/bg/js/options.js @@ -336,7 +336,7 @@ function profileOptionsSetDefaults(options) {      const combine = (target, source) => {          for (const key in source) { -            if (!target.hasOwnProperty(key)) { +            if (!hasOwn(target, key)) {                  target[key] = source[key];              }          } @@ -389,7 +389,7 @@ function optionsUpdateVersion(options, defaultProfileOptions) {      // Remove invalid      const profiles = options.profiles;      for (let i = profiles.length - 1; i >= 0; --i) { -        if (!utilIsObject(profiles[i])) { +        if (!isObject(profiles[i])) {              profiles.splice(i, 1);          }      } @@ -429,7 +429,7 @@ function optionsUpdateVersion(options, defaultProfileOptions) {  function optionsLoad() {      return new Promise((resolve, reject) => { -        chrome.storage.local.get(['options'], store => { +        chrome.storage.local.get(['options'], (store) => {              const error = chrome.runtime.lastError;              if (error) {                  reject(new Error(error)); @@ -437,17 +437,17 @@ function optionsLoad() {                  resolve(store.options);              }          }); -    }).then(optionsStr => { +    }).then((optionsStr) => {          if (typeof optionsStr === 'string') {              const options = JSON.parse(optionsStr); -            if (utilIsObject(options)) { +            if (isObject(options)) {                  return options;              }          }          return {};      }).catch(() => {          return {}; -    }).then(options => { +    }).then((options) => {          return (              Array.isArray(options.profiles) ?              optionsUpdateVersion(options, {}) : diff --git a/ext/bg/js/page-exit-prevention.js b/ext/bg/js/page-exit-prevention.js new file mode 100644 index 00000000..aee4e3c2 --- /dev/null +++ b/ext/bg/js/page-exit-prevention.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/>. + */ + + +class PageExitPrevention { +  constructor() { +  } + +  start() { +      PageExitPrevention._addInstance(this); +  } + +  end() { +      PageExitPrevention._removeInstance(this); +  } + +  static _addInstance(instance) { +      const size = PageExitPrevention._instances.size; +      PageExitPrevention._instances.set(instance, true); +      if (size === 0) { +          window.addEventListener('beforeunload', PageExitPrevention._onBeforeUnload); +      } +  } + +  static _removeInstance(instance) { +      if ( +          PageExitPrevention._instances.delete(instance) && +          PageExitPrevention._instances.size === 0 +      ) { +          window.removeEventListener('beforeunload', PageExitPrevention._onBeforeUnload); +      } +  } + +  static _onBeforeUnload(e) { +      if (PageExitPrevention._instances.size === 0) { +          return; +      } + +      e.preventDefault(); +      e.returnValue = ''; +      return ''; +  } +} + +PageExitPrevention._instances = new Map(); diff --git a/ext/bg/js/profile-conditions.js b/ext/bg/js/profile-conditions.js index 8272e5dd..ebc6680a 100644 --- a/ext/bg/js/profile-conditions.js +++ b/ext/bg/js/profile-conditions.js @@ -86,7 +86,7 @@ const profileConditionsDescriptor = {                  placeholder: 'Comma separated list of domains',                  defaultValue: 'example.com',                  transformCache: {}, -                transform: (optionValue) => optionValue.split(/[,;\s]+/).map(v => v.trim().toLowerCase()).filter(v => v.length > 0), +                transform: (optionValue) => optionValue.split(/[,;\s]+/).map((v) => v.trim().toLowerCase()).filter((v) => v.length > 0),                  transformReverse: (transformedOptionValue) => transformedOptionValue.join(', '),                  validateTransformed: (transformedOptionValue) => (transformedOptionValue.length > 0),                  test: ({url}, transformedOptionValue) => _profileConditionTestDomainList(url, transformedOptionValue) diff --git a/ext/bg/js/request.js b/ext/bg/js/request.js index 3afc1506..7d73d49b 100644 --- a/ext/bg/js/request.js +++ b/ext/bg/js/request.js @@ -29,7 +29,7 @@ function requestJson(url, action, params) {          } else {              xhr.send();          } -    }).then(responseText => { +    }).then((responseText) => {          try {              return JSON.parse(responseText);          } diff --git a/ext/bg/js/search-query-parser.js b/ext/bg/js/search-query-parser.js index 42e53989..8dc2e30a 100644 --- a/ext/bg/js/search-query-parser.js +++ b/ext/bg/js/search-query-parser.js @@ -38,16 +38,16 @@ class QueryParser {      }      onMouseDown(e) { -        if (Frontend.isMouseButton('primary', e)) { +        if (DOM.isMouseButtonPressed(e, 'primary')) {              this.clickScanPrevent = false;          }      }      onMouseUp(e) {          if ( -            this.search.options.scanning.clickGlossary && +            this.search.options.scanning.enablePopupSearch &&              !this.clickScanPrevent && -            Frontend.isMouseButton('primary', e) +            DOM.isMouseButtonPressed(e, 'primary')          ) {              const selectText = this.search.options.scanning.selectText;              this.onTermLookup(e, {disableScroll: true, selectText}); @@ -55,7 +55,7 @@ class QueryParser {      }      onMouseMove(e) { -        if (this.pendingLookup || Frontend.isMouseButton('primary', e)) { +        if (this.pendingLookup || DOM.isMouseButtonDown(e, 'primary')) {              return;          } @@ -63,7 +63,7 @@ class QueryParser {          const scanningModifier = scanningOptions.modifier;          if (!(              Frontend.isScanningModifierPressed(scanningModifier, e) || -            (scanningOptions.middleMouse && Frontend.isMouseButton('auxiliary', e)) +            (scanningOptions.middleMouse && DOM.isMouseButtonDown(e, 'auxiliary'))          )) {              return;          } @@ -107,7 +107,7 @@ class QueryParser {      }      getParseResult() { -        return this.parseResults.find(r => r.id === this.selectedParser); +        return this.parseResults.find((r) => r.id === this.selectedParser);      }      async setText(text) { @@ -134,7 +134,7 @@ class QueryParser {              });          }          if (this.search.options.parsing.enableMecabParser) { -            let mecabResults = await apiTextParseMecab(text, this.search.getOptionsContext()); +            const mecabResults = await apiTextParseMecab(text, this.search.getOptionsContext());              for (const mecabDictName in mecabResults) {                  results.push({                      name: `MeCab: ${mecabDictName}`, @@ -216,11 +216,11 @@ class QueryParser {      static processParseResultForDisplay(result) {          return result.map((term) => { -            return term.filter(part => part.text.trim()).map((part) => { +            return term.filter((part) => part.text.trim()).map((part) => {                  return {                      text: Array.from(part.text),                      reading: part.reading, -                    raw: !part.reading || !part.reading.trim(), +                    raw: !part.reading || !part.reading.trim()                  };              });          }); diff --git a/ext/bg/js/search.js b/ext/bg/js/search.js index 0922d938..00b7ca4b 100644 --- a/ext/bg/js/search.js +++ b/ext/bg/js/search.js @@ -167,7 +167,7 @@ class DisplaySearch extends Display {          this.onSearchQueryUpdated(query, true);      } -    onPopState(e) { +    onPopState() {          const query = DisplaySearch.getSearchQueryFromLocation(window.location.href) || '';          if (this.query !== null) {              if (this.isWanakanaEnabled()) { @@ -207,7 +207,7 @@ class DisplaySearch extends Display {      async onSearchQueryUpdated(query, animate) {          try {              const details = {}; -            const match = /[\*\uff0a]+$/.exec(query); +            const match = /[*\uff0a]+$/.exec(query);              if (match !== null) {                  details.wildcard = true;                  query = query.substring(0, query.length - match[0].length); @@ -220,6 +220,7 @@ class DisplaySearch extends Display {                  const {definitions} = await apiTermsFind(query, details, this.optionsContext);                  this.setContentTerms(definitions, {                      focus: false, +                    disableHistory: true,                      sentence: {text: query, offset: 0},                      url: window.location.href                  }); @@ -234,7 +235,7 @@ class DisplaySearch extends Display {      onRuntimeMessage({action, params}, sender, callback) {          const handlers = DisplaySearch.runtimeMessageHandlers; -        if (handlers.hasOwnProperty(action)) { +        if (hasOwn(handlers, action)) {              const handler = handlers[action];              const result = handler(this, params);              callback(result); @@ -245,7 +246,7 @@ class DisplaySearch extends Display {      initClipboardMonitor() {          // ignore copy from search page -        window.addEventListener('copy', (e) => { +        window.addEventListener('copy', () => {              this.clipboardPrevText = document.getSelection().toString().trim();          });      } @@ -324,7 +325,7 @@ class DisplaySearch extends Display {              this.intro.style.transition = '';              this.intro.style.height = '';              const size = this.intro.getBoundingClientRect(); -            this.intro.style.height = `0px`; +            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`; @@ -356,7 +357,7 @@ class DisplaySearch extends Display {      }      static getSearchQueryFromLocation(url) { -        let match = /^[^\?#]*\?(?:[^&#]*&)?query=([^&#]*)/.exec(url); +        const match = /^[^?#]*\?(?:[^&#]*&)?query=([^&#]*)/.exec(url);          return match !== null ? decodeURIComponent(match[1]) : null;      }  } diff --git a/ext/bg/js/settings.js b/ext/bg/js/settings.js deleted file mode 100644 index ab267c32..00000000 --- a/ext/bg/js/settings.js +++ /dev/null @@ -1,852 +0,0 @@ -/* - * Copyright (C) 2016-2017  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/>. - */ - -async function getOptionsArray() { -    const optionsFull = await apiOptionsGetFull(); -    return optionsFull.profiles.map(profile => profile.options); -} - -async function formRead(options) { -    options.general.enable = $('#enable').prop('checked'); -    options.general.showGuide = $('#show-usage-guide').prop('checked'); -    options.general.compactTags = $('#compact-tags').prop('checked'); -    options.general.compactGlossaries = $('#compact-glossaries').prop('checked'); -    options.general.resultOutputMode = $('#result-output-mode').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); -    options.general.popupDisplayMode = $('#popup-display-mode').val(); -    options.general.popupHorizontalTextPosition = $('#popup-horizontal-text-position').val(); -    options.general.popupVerticalTextPosition = $('#popup-vertical-text-position').val(); -    options.general.popupWidth = parseInt($('#popup-width').val(), 10); -    options.general.popupHeight = parseInt($('#popup-height').val(), 10); -    options.general.popupHorizontalOffset = parseInt($('#popup-horizontal-offset').val(), 0); -    options.general.popupVerticalOffset = parseInt($('#popup-vertical-offset').val(), 10); -    options.general.popupHorizontalOffset2 = parseInt($('#popup-horizontal-offset2').val(), 0); -    options.general.popupVerticalOffset2 = parseInt($('#popup-vertical-offset2').val(), 10); -    options.general.popupTheme = $('#popup-theme').val(); -    options.general.popupOuterTheme = $('#popup-outer-theme').val(); -    options.general.customPopupCss = $('#custom-popup-css').val(); -    options.general.customPopupOuterCss = $('#custom-popup-outer-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.audio.textToSpeechVoice = $('#text-to-speech-voice').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'); -    options.scanning.alphanumeric = $('#search-alphanumeric').prop('checked'); -    options.scanning.autoHideResults = $('#auto-hide-results').prop('checked'); -    options.scanning.deepDomScan = $('#deep-dom-scan').prop('checked'); -    options.scanning.enablePopupSearch = $('#enable-search-within-first-popup').prop('checked'); -    options.scanning.enableOnPopupExpressions = $('#enable-scanning-of-popup-expressions').prop('checked'); -    options.scanning.enableOnSearchPage = $('#enable-scanning-on-search-page').prop('checked'); -    options.scanning.delay = parseInt($('#scan-delay').val(), 10); -    options.scanning.length = parseInt($('#scan-length').val(), 10); -    options.scanning.modifier = $('#scan-modifier-key').val(); -    options.scanning.popupNestingMaxDepth = parseInt($('#popup-nesting-max-depth').val(), 10); - -    options.parsing.enableScanningParser = $('#parsing-scan-enable').prop('checked'); -    options.parsing.enableMecabParser = $('#parsing-mecab-enable').prop('checked'); -    options.parsing.readingMode = $('#parsing-reading-mode').val(); - -    const optionsAnkiEnableOld = options.anki.enable; -    options.anki.enable = $('#anki-enable').prop('checked'); -    options.anki.tags = utilBackgroundIsolate($('#card-tags').val().split(/[,; ]+/)); -    options.anki.sentenceExt = parseInt($('#sentence-detection-extent').val(), 10); -    options.anki.server = $('#interface-server').val(); -    options.anki.screenshot.format = $('#screenshot-format').val(); -    options.anki.screenshot.quality = parseInt($('#screenshot-quality').val(), 10); -    options.anki.fieldTemplates = $('#field-templates').val(); - -    if (optionsAnkiEnableOld && !ankiErrorShown()) { -        options.anki.terms.deck = $('#anki-terms-deck').val(); -        options.anki.terms.model = $('#anki-terms-model').val(); -        options.anki.terms.fields = utilBackgroundIsolate(ankiFieldsToDict($('#terms .anki-field-value'))); -        options.anki.kanji.deck = $('#anki-kanji-deck').val(); -        options.anki.kanji.model = $('#anki-kanji-model').val(); -        options.anki.kanji.fields = utilBackgroundIsolate(ankiFieldsToDict($('#kanji .anki-field-value'))); -    } -} - -async function formWrite(options) { -    $('#enable').prop('checked', options.general.enable); -    $('#show-usage-guide').prop('checked', options.general.showGuide); -    $('#compact-tags').prop('checked', options.general.compactTags); -    $('#compact-glossaries').prop('checked', options.general.compactGlossaries); -    $('#result-output-mode').val(options.general.resultOutputMode); -    $('#show-debug-info').prop('checked', options.general.debugInfo); -    $('#show-advanced-options').prop('checked', options.general.showAdvanced); -    $('#max-displayed-results').val(options.general.maxResults); -    $('#popup-display-mode').val(options.general.popupDisplayMode); -    $('#popup-horizontal-text-position').val(options.general.popupHorizontalTextPosition); -    $('#popup-vertical-text-position').val(options.general.popupVerticalTextPosition); -    $('#popup-width').val(options.general.popupWidth); -    $('#popup-height').val(options.general.popupHeight); -    $('#popup-horizontal-offset').val(options.general.popupHorizontalOffset); -    $('#popup-vertical-offset').val(options.general.popupVerticalOffset); -    $('#popup-horizontal-offset2').val(options.general.popupHorizontalOffset2); -    $('#popup-vertical-offset2').val(options.general.popupVerticalOffset2); -    $('#popup-theme').val(options.general.popupTheme); -    $('#popup-outer-theme').val(options.general.popupOuterTheme); -    $('#custom-popup-css').val(options.general.customPopupCss); -    $('#custom-popup-outer-css').val(options.general.customPopupOuterCss); - -    $('#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); -    $('#text-to-speech-voice').val(options.audio.textToSpeechVoice).attr('data-value', options.audio.textToSpeechVoice); - -    $('#middle-mouse-button-scan').prop('checked', options.scanning.middleMouse); -    $('#touch-input-enabled').prop('checked', options.scanning.touchInputEnabled); -    $('#select-matched-text').prop('checked', options.scanning.selectText); -    $('#search-alphanumeric').prop('checked', options.scanning.alphanumeric); -    $('#auto-hide-results').prop('checked', options.scanning.autoHideResults); -    $('#deep-dom-scan').prop('checked', options.scanning.deepDomScan); -    $('#enable-search-within-first-popup').prop('checked', options.scanning.enablePopupSearch); -    $('#enable-scanning-of-popup-expressions').prop('checked', options.scanning.enableOnPopupExpressions); -    $('#enable-scanning-on-search-page').prop('checked', options.scanning.enableOnSearchPage); -    $('#scan-delay').val(options.scanning.delay); -    $('#scan-length').val(options.scanning.length); -    $('#scan-modifier-key').val(options.scanning.modifier); -    $('#popup-nesting-max-depth').val(options.scanning.popupNestingMaxDepth); - -    $('#parsing-scan-enable').prop('checked', options.parsing.enableScanningParser); -    $('#parsing-mecab-enable').prop('checked', options.parsing.enableMecabParser); -    $('#parsing-reading-mode').val(options.parsing.readingMode); - -    $('#anki-enable').prop('checked', options.anki.enable); -    $('#card-tags').val(options.anki.tags.join(' ')); -    $('#sentence-detection-extent').val(options.anki.sentenceExt); -    $('#interface-server').val(options.anki.server); -    $('#screenshot-format').val(options.anki.screenshot.format); -    $('#screenshot-quality').val(options.anki.screenshot.quality); -    $('#field-templates').val(options.anki.fieldTemplates); - -    onAnkiTemplatesValidateCompile(); -    await onDictionaryOptionsChanged(options); - -    try { -        await ankiDeckAndModelPopulate(options); -    } catch (e) { -        ankiErrorShow(e); -    } - -    formUpdateVisibility(options); -} - -function formSetupEventListeners() { -    $('input, select, textarea').not('.anki-model').not('.ignore-form-changes *').change(utilAsync(onFormOptionsChanged)); -    $('.anki-model').change(utilAsync(onAnkiModelChanged)); -} - -function formUpdateVisibility(options) { -    document.documentElement.dataset.optionsAnkiEnable = `${!!options.anki.enable}`; -    document.documentElement.dataset.optionsGeneralDebugInfo = `${!!options.general.debugInfo}`; -    document.documentElement.dataset.optionsGeneralShowAdvanced = `${!!options.general.showAdvanced}`; -    document.documentElement.dataset.optionsGeneralResultOutputMode = `${options.general.resultOutputMode}`; - -    if (options.general.debugInfo) { -        const temp = utilIsolate(options); -        temp.anki.fieldTemplates = '...'; -        const text = JSON.stringify(temp, null, 4); -        $('#debug').text(text); -    } -} - -async function onFormOptionsChanged(e) { -    if (!e.originalEvent && !e.isTrigger) { -        return; -    } - -    const optionsContext = getOptionsContext(); -    const options = await apiOptionsGet(optionsContext); -    const optionsAnkiEnableOld = options.anki.enable; -    const optionsAnkiServerOld = options.anki.server; - -    await formRead(options); -    await settingsSaveOptions(); -    formUpdateVisibility(options); - -    try { -        const ankiUpdated = -            options.anki.enable !== optionsAnkiEnableOld || -            options.anki.server !== optionsAnkiServerOld; - -        if (ankiUpdated) { -            ankiSpinnerShow(true); -            await ankiDeckAndModelPopulate(options); -            ankiErrorShow(); -        } -    } catch (error) { -        ankiErrorShow(error); -    } finally { -        ankiSpinnerShow(false); -    } -} - -async function onReady() { -    showExtensionInformation(); - -    formSetupEventListeners(); -    appearanceInitialize(); -    await audioSettingsInitialize(); -    await profileOptionsSetup(); -    await dictSettingsInitialize(); -    ankiTemplatesInitialize(); - -    storageInfoInitialize(); - -    chrome.runtime.onMessage.addListener(onMessage); -} - -$(document).ready(utilAsync(onReady)); - - -/* - * Page exit prevention - */ - -class PageExitPrevention { -    constructor() { -    } - -    start() { -        PageExitPrevention._addInstance(this); -    } - -    end() { -        PageExitPrevention._removeInstance(this); -    } - -    static _addInstance(instance) { -        const size = PageExitPrevention._instances.size; -        PageExitPrevention._instances.set(instance, true); -        if (size === 0) { -            window.addEventListener('beforeunload', PageExitPrevention._onBeforeUnload); -        } -    } - -    static _removeInstance(instance) { -        if ( -            PageExitPrevention._instances.delete(instance) && -            PageExitPrevention._instances.size === 0 -        ) { -            window.removeEventListener('beforeunload', PageExitPrevention._onBeforeUnload); -        } -    } - -    static _onBeforeUnload(e) { -        if (PageExitPrevention._instances.size === 0) { -            return; -        } - -        e.preventDefault(); -        e.returnValue = ''; -        return ''; -    } -} -PageExitPrevention._instances = new Map(); - - -/* - * Appearance - */ - -function appearanceInitialize() { -    let previewVisible = false; -    $('#settings-popup-preview-button').on('click', () => { -        if (previewVisible) { return; } -        showAppearancePreview(); -        previewVisible = true; -    }); -} - -function showAppearancePreview() { -    const container = $('#settings-popup-preview-container'); -    const buttonContainer = $('#settings-popup-preview-button-container'); -    const settings = $('#settings-popup-preview-settings'); -    const text = $('#settings-popup-preview-text'); -    const customCss = $('#custom-popup-css'); -    const customOuterCss = $('#custom-popup-outer-css'); - -    const frame = document.createElement('iframe'); -    frame.src = '/bg/settings-popup-preview.html'; -    frame.id = 'settings-popup-preview-frame'; - -    window.wanakana.bind(text[0]); - -    text.on('input', () => { -        const action = 'setText'; -        const params = {text: text.val()}; -        frame.contentWindow.postMessage({action, params}, '*'); -    }); -    customCss.on('input', () => { -        const action = 'setCustomCss'; -        const params = {css: customCss.val()}; -        frame.contentWindow.postMessage({action, params}, '*'); -    }); -    customOuterCss.on('input', () => { -        const action = 'setCustomOuterCss'; -        const params = {css: customOuterCss.val()}; -        frame.contentWindow.postMessage({action, params}, '*'); -    }); - -    container.append(frame); -    buttonContainer.remove(); -    settings.css('display', ''); -} - - -/* - * 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 = () => settingsSaveOptions(); - -    textToSpeechInitialize(); -} - -function textToSpeechInitialize() { -    if (typeof speechSynthesis === 'undefined') { return; } - -    speechSynthesis.addEventListener('voiceschanged', () => updateTextToSpeechVoices(), false); -    updateTextToSpeechVoices(); - -    $('#text-to-speech-voice-test').on('click', () => textToSpeechTest()); -} - -function updateTextToSpeechVoices() { -    const voices = Array.prototype.map.call(speechSynthesis.getVoices(), (voice, index) => ({voice, index})); -    voices.sort(textToSpeechVoiceCompare); -    if (voices.length > 0) { -        $('#text-to-speech-voice-container').css('display', ''); -    } - -    const select = $('#text-to-speech-voice'); -    select.empty(); -    select.append($('<option>').val('').text('None')); -    for (const {voice} of voices) { -        select.append($('<option>').val(voice.voiceURI).text(`${voice.name} (${voice.lang})`)); -    } - -    select.val(select.attr('data-value')); -} - -function languageTagIsJapanese(languageTag) { -    return ( -        languageTag.startsWith('ja-') || -        languageTag.startsWith('jpn-') -    ); -} - -function textToSpeechVoiceCompare(a, b) { -    const aIsJapanese = languageTagIsJapanese(a.voice.lang); -    const bIsJapanese = languageTagIsJapanese(b.voice.lang); -    if (aIsJapanese) { -        if (!bIsJapanese) { return -1; } -    } else { -        if (bIsJapanese) { return 1; } -    } - -    const aIsDefault = a.voice.default; -    const bIsDefault = b.voice.default; -    if (aIsDefault) { -        if (!bIsDefault) { return -1; } -    } else { -        if (bIsDefault) { return 1; } -    } - -    if (a.index < b.index) { return -1; } -    if (a.index > b.index) { return 1; } -    return 0; -} - -function textToSpeechTest() { -    try { -        const text = $('#text-to-speech-voice-test').attr('data-speech-text') || ''; -        const voiceURI = $('#text-to-speech-voice').val(); -        const voice = audioGetTextToSpeechVoice(voiceURI); -        if (voice === null) { return; } - -        const utterance = new SpeechSynthesisUtterance(text); -        utterance.lang = 'ja-JP'; -        utterance.voice = voice; -        utterance.volume = 1.0; - -        speechSynthesis.speak(utterance); -    } catch (e) { -        // NOP -    } -} - - -/* - * Remote options updates - */ - -function settingsGetSource() { -    return new Promise((resolve) => { -        chrome.tabs.getCurrent((tab) => resolve(`settings${tab ? tab.id : ''}`)); -    }); -} - -async function settingsSaveOptions() { -    const source = await settingsGetSource(); -    await apiOptionsSave(source); -} - -async function onOptionsUpdate({source}) { -    const thisSource = await settingsGetSource(); -    if (source === thisSource) { return; } - -    const optionsContext = getOptionsContext(); -    const options = await apiOptionsGet(optionsContext); -    await formWrite(options); -} - -function onMessage({action, params}, sender, callback) { -    switch (action) { -        case 'optionsUpdate': -            onOptionsUpdate(params); -            break; -        case 'getUrl': -            callback({url: window.location.href}); -            break; -    } -} - - -/* - * Anki - */ - -function ankiSpinnerShow(show) { -    const spinner = $('#anki-spinner'); -    if (show) { -        spinner.show(); -    } else { -        spinner.hide(); -    } -} - -function ankiErrorShow(error) { -    const dialog = $('#anki-error'); -    if (error) { -        dialog.show().text(error); -    } -    else { -        dialog.hide(); -    } -} - -function ankiErrorShown() { -    return $('#anki-error').is(':visible'); -} - -function ankiFieldsToDict(selection) { -    const result = {}; -    selection.each((index, element) => { -        result[$(element).data('field')] = $(element).val(); -    }); - -    return result; -} - -async function ankiDeckAndModelPopulate(options) { -    const ankiFormat = $('#anki-format').hide(); - -    const deckNames = await utilAnkiGetDeckNames(); -    const ankiDeck = $('.anki-deck'); -    ankiDeck.find('option').remove(); -    deckNames.sort().forEach(name => ankiDeck.append($('<option/>', {value: name, text: name}))); - -    const modelNames = await utilAnkiGetModelNames(); -    const ankiModel = $('.anki-model'); -    ankiModel.find('option').remove(); -    modelNames.sort().forEach(name => ankiModel.append($('<option/>', {value: name, text: name}))); - -    $('#anki-terms-deck').val(options.anki.terms.deck); -    await ankiFieldsPopulate($('#anki-terms-model').val(options.anki.terms.model), options); - -    $('#anki-kanji-deck').val(options.anki.kanji.deck); -    await ankiFieldsPopulate($('#anki-kanji-model').val(options.anki.kanji.model), options); - -    ankiFormat.show(); -} - -function ankiCreateFieldTemplate(name, value, markers) { -    const template = document.querySelector('#anki-field-template').content; -    const content = document.importNode(template, true).firstChild; - -    content.querySelector('.anki-field-name').textContent = name; - -    const field = content.querySelector('.anki-field-value'); -    field.dataset.field = name; -    field.value = value; - -    content.querySelector('.anki-field-marker-list').appendChild(ankiGetFieldMarkersHtml(markers)); - -    return content; -} - -function ankiGetFieldMarkersHtml(markers, fragment) { -    const template = document.querySelector('#anki-field-marker-template').content; -    if (!fragment) { -        fragment = new DocumentFragment(); -    } -    for (const marker of markers) { -        const markerNode = document.importNode(template, true).firstChild; -        markerNode.querySelector('.marker-link').textContent = marker; -        fragment.appendChild(markerNode); -    } -    return fragment; -} - -function ankiGetFieldMarkers(type) { -    switch (type) { -        case 'terms': -            return [ -                'audio', -                'cloze-body', -                'cloze-prefix', -                'cloze-suffix', -                'dictionary', -                'expression', -                'furigana', -                'furigana-plain', -                'glossary', -                'glossary-brief', -                'reading', -                'screenshot', -                'sentence', -                'tags', -                'url' -            ]; -        case 'kanji': -            return [ -                'character', -                'dictionary', -                'glossary', -                'kunyomi', -                'onyomi', -                'screenshot', -                'sentence', -                'tags', -                'url' -            ]; -        default: -            return []; -    } -} - -async function ankiFieldsPopulate(element, options) { -    const modelName = element.val(); -    if (!modelName) { -        return; -    } - -    const tab = element.closest('.tab-pane'); -    const tabId = tab.attr('id'); -    const container = tab.find('tbody').empty(); -    const markers = ankiGetFieldMarkers(tabId); - -    for (const name of await utilAnkiGetModelFieldNames(modelName)) { -        const value = options.anki[tabId].fields[name] || ''; -        const html = ankiCreateFieldTemplate(name, value, markers); -        container.append($(html)); -    } - -    tab.find('.anki-field-value').change(utilAsync(onFormOptionsChanged)); -    tab.find('.marker-link').click(onAnkiMarkerClicked); -} - -function onAnkiMarkerClicked(e) { -    e.preventDefault(); -    const link = e.target; -    $(link).closest('.input-group').find('.anki-field-value').val(`{${link.text}}`).trigger('change'); -} - -async function onAnkiModelChanged(e) { -    try { -        if (!e.originalEvent) { -            return; -        } - -        const element = $(this); -        const tab = element.closest('.tab-pane'); -        const tabId = tab.attr('id'); - -        const optionsContext = getOptionsContext(); -        const options = await apiOptionsGet(optionsContext); -        await formRead(options); -        options.anki[tabId].fields = utilBackgroundIsolate({}); -        await settingsSaveOptions(); - -        ankiSpinnerShow(true); -        await ankiFieldsPopulate(element, options); -        ankiErrorShow(); -    } catch (error) { -        ankiErrorShow(error); -    } finally { -        ankiSpinnerShow(false); -    } -} - -function onAnkiFieldTemplatesReset(e) { -    e.preventDefault(); -    $('#field-template-reset-modal').modal('show'); -} - -async function onAnkiFieldTemplatesResetConfirm(e) { -    try { -        e.preventDefault(); - -        $('#field-template-reset-modal').modal('hide'); - -        const optionsContext = getOptionsContext(); -        const options = await apiOptionsGet(optionsContext); -        const fieldTemplates = profileOptionsGetDefaultFieldTemplates(); -        options.anki.fieldTemplates = fieldTemplates; -        $('#field-templates').val(fieldTemplates); -        onAnkiTemplatesValidateCompile(); -        await settingsSaveOptions(); -    } catch (error) { -        ankiErrorShow(error); -    } -} - -function ankiTemplatesInitialize() { -    const markers = new Set(ankiGetFieldMarkers('terms').concat(ankiGetFieldMarkers('kanji'))); -    const fragment = ankiGetFieldMarkersHtml(markers); - -    const list = document.querySelector('#field-templates-list'); -    list.appendChild(fragment); -    for (const node of list.querySelectorAll('.marker-link')) { -        node.addEventListener('click', onAnkiTemplateMarkerClicked, false); -    } - -    $('#field-templates').on('change', onAnkiTemplatesValidateCompile); -    $('#field-template-render').on('click', onAnkiTemplateRender); -    $('#field-templates-reset').on('click', onAnkiFieldTemplatesReset); -    $('#field-templates-reset-confirm').on('click', onAnkiFieldTemplatesResetConfirm); -} - -const ankiTemplatesValidateGetDefinition = (() => { -    let cachedValue = null; -    let cachedText = null; - -    return async (text, optionsContext) => { -        if (cachedText !== text) { -            const {definitions} = await apiTermsFind(text, {}, optionsContext); -            if (definitions.length === 0) { return null; } - -            cachedValue = definitions[0]; -            cachedText = text; -        } -        return cachedValue; -    }; -})(); - -async function ankiTemplatesValidate(infoNode, field, mode, showSuccessResult, invalidateInput) { -    const text = document.querySelector('#field-templates-preview-text').value || ''; -    const exceptions = []; -    let result = `No definition found for ${text}`; -    try { -        const optionsContext = getOptionsContext(); -        const definition = await ankiTemplatesValidateGetDefinition(text, optionsContext); -        if (definition !== null) { -            const options = await apiOptionsGet(optionsContext); -            result = await dictFieldFormat(field, definition, mode, options, exceptions); -        } -    } catch (e) { -        exceptions.push(e); -    } - -    const hasException = exceptions.length > 0; -    infoNode.hidden = !(showSuccessResult || hasException); -    infoNode.textContent = hasException ? exceptions.map(e => `${e}`).join('\n') : (showSuccessResult ? result : ''); -    infoNode.classList.toggle('text-danger', hasException); -    if (invalidateInput) { -        const input = document.querySelector('#field-templates'); -        input.classList.toggle('is-invalid', hasException); -    } -} - -function onAnkiTemplatesValidateCompile() { -    const infoNode = document.querySelector('#field-template-compile-result'); -    ankiTemplatesValidate(infoNode, '{expression}', 'term-kanji', false, true); -} - -function onAnkiTemplateMarkerClicked(e) { -    e.preventDefault(); -    document.querySelector('#field-template-render-text').value = `{${e.target.textContent}}`; -} - -function onAnkiTemplateRender(e) { -    e.preventDefault(); - -    const field = document.querySelector('#field-template-render-text').value; -    const infoNode = document.querySelector('#field-template-render-result'); -    infoNode.hidden = true; -    ankiTemplatesValidate(infoNode, field, 'term-kanji', true, false); -} - - -/* - * Storage - */ - -function storageBytesToLabeledString(size) { -    const base = 1000; -    const labels = [' bytes', 'KB', 'MB', 'GB']; -    let labelIndex = 0; -    while (size >= base) { -        size /= base; -        ++labelIndex; -    } -    const label = labelIndex === 0 ? `${size}` : size.toFixed(1); -    return `${label}${labels[labelIndex]}`; -} - -async function storageEstimate() { -    try { -        return (storageEstimate.mostRecent = await navigator.storage.estimate()); -    } catch (e) { } -    return null; -} -storageEstimate.mostRecent = null; - -async function isStoragePeristent() { -    try { -        return await navigator.storage.persisted(); -    } catch (e) { } -    return false; -} - -async function storageInfoInitialize() { -    storagePersistInitialize(); -    const {browser, platform} = await apiGetEnvironmentInfo(); -    document.documentElement.dataset.browser = browser; -    document.documentElement.dataset.operatingSystem = platform.os; - -    await storageShowInfo(); - -    document.querySelector('#storage-refresh').addEventListener('click', () => storageShowInfo(), false); -} - -async function storageUpdateStats() { -    storageUpdateStats.isUpdating = true; - -    const estimate = await storageEstimate(); -    const valid = (estimate !== null); - -    if (valid) { -        // Firefox reports usage as 0 when persistent storage is enabled. -        const finite = (estimate.usage > 0 || !(await isStoragePeristent())); -        if (finite) { -            document.querySelector('#storage-usage').textContent = storageBytesToLabeledString(estimate.usage); -            document.querySelector('#storage-quota').textContent = storageBytesToLabeledString(estimate.quota); -        } -        document.querySelector('#storage-use-finite').classList.toggle('storage-hidden', !finite); -        document.querySelector('#storage-use-infinite').classList.toggle('storage-hidden', finite); -    } - -    storageUpdateStats.isUpdating = false; -    return valid; -} -storageUpdateStats.isUpdating = false; - -async function storageShowInfo() { -    storageSpinnerShow(true); - -    const valid = await storageUpdateStats(); -    document.querySelector('#storage-use').classList.toggle('storage-hidden', !valid); -    document.querySelector('#storage-error').classList.toggle('storage-hidden', valid); - -    storageSpinnerShow(false); -} - -function storageSpinnerShow(show) { -    const spinner = $('#storage-spinner'); -    if (show) { -        spinner.show(); -    } else { -        spinner.hide(); -    } -} - -async function storagePersistInitialize() { -    if (!(navigator.storage && navigator.storage.persist)) { -        // Not supported -        return; -    } - -    const info = document.querySelector('#storage-persist-info'); -    const button = document.querySelector('#storage-persist-button'); -    const checkbox = document.querySelector('#storage-persist-button-checkbox'); - -    info.classList.remove('storage-hidden'); -    button.classList.remove('storage-hidden'); - -    let persisted = await isStoragePeristent(); -    checkbox.checked = persisted; - -    button.addEventListener('click', async () => { -        if (persisted) { -            return; -        } -        let result = false; -        try { -            result = await navigator.storage.persist(); -        } catch (e) { -            // NOP -        } - -        if (result) { -            persisted = true; -            checkbox.checked = true; -            storageShowInfo(); -        } else { -            $('.storage-persist-fail-warning').removeClass('storage-hidden'); -        } -    }, false); -} - - -/* - * Information - */ - -function showExtensionInformation() { -    const node = document.getElementById('extension-info'); -    if (node === null) { return; } - -    const manifest = chrome.runtime.getManifest(); -    node.textContent = `${manifest.name} v${manifest.version}`; -} diff --git a/ext/bg/js/settings/anki-templates.js b/ext/bg/js/settings/anki-templates.js new file mode 100644 index 00000000..9cdfc134 --- /dev/null +++ b/ext/bg/js/settings/anki-templates.js @@ -0,0 +1,109 @@ +/* + * 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 onAnkiFieldTemplatesReset(e) { +    e.preventDefault(); +    $('#field-template-reset-modal').modal('show'); +} + +function onAnkiFieldTemplatesResetConfirm(e) { +    e.preventDefault(); + +    $('#field-template-reset-modal').modal('hide'); + +    const element = document.querySelector('#field-templates'); +    element.value = profileOptionsGetDefaultFieldTemplates(); +    element.dispatchEvent(new Event('change')); +} + +function ankiTemplatesInitialize() { +    const markers = new Set(ankiGetFieldMarkers('terms').concat(ankiGetFieldMarkers('kanji'))); +    const fragment = ankiGetFieldMarkersHtml(markers); + +    const list = document.querySelector('#field-templates-list'); +    list.appendChild(fragment); +    for (const node of list.querySelectorAll('.marker-link')) { +        node.addEventListener('click', onAnkiTemplateMarkerClicked, false); +    } + +    $('#field-templates').on('change', (e) => onAnkiTemplatesValidateCompile(e)); +    $('#field-template-render').on('click', (e) => onAnkiTemplateRender(e)); +    $('#field-templates-reset').on('click', (e) => onAnkiFieldTemplatesReset(e)); +    $('#field-templates-reset-confirm').on('click', (e) => onAnkiFieldTemplatesResetConfirm(e)); +} + +const ankiTemplatesValidateGetDefinition = (() => { +    let cachedValue = null; +    let cachedText = null; + +    return async (text, optionsContext) => { +        if (cachedText !== text) { +            const {definitions} = await apiTermsFind(text, {}, optionsContext); +            if (definitions.length === 0) { return null; } + +            cachedValue = definitions[0]; +            cachedText = text; +        } +        return cachedValue; +    }; +})(); + +async function ankiTemplatesValidate(infoNode, field, mode, showSuccessResult, invalidateInput) { +    const text = document.querySelector('#field-templates-preview-text').value || ''; +    const exceptions = []; +    let result = `No definition found for ${text}`; +    try { +        const optionsContext = getOptionsContext(); +        const definition = await ankiTemplatesValidateGetDefinition(text, optionsContext); +        if (definition !== null) { +            const options = await apiOptionsGet(optionsContext); +            result = await dictFieldFormat(field, definition, mode, options, exceptions); +        } +    } catch (e) { +        exceptions.push(e); +    } + +    const hasException = exceptions.length > 0; +    infoNode.hidden = !(showSuccessResult || hasException); +    infoNode.textContent = hasException ? exceptions.map((e) => `${e}`).join('\n') : (showSuccessResult ? result : ''); +    infoNode.classList.toggle('text-danger', hasException); +    if (invalidateInput) { +        const input = document.querySelector('#field-templates'); +        input.classList.toggle('is-invalid', hasException); +    } +} + +function onAnkiTemplatesValidateCompile() { +    const infoNode = document.querySelector('#field-template-compile-result'); +    ankiTemplatesValidate(infoNode, '{expression}', 'term-kanji', false, true); +} + +function onAnkiTemplateMarkerClicked(e) { +    e.preventDefault(); +    document.querySelector('#field-template-render-text').value = `{${e.target.textContent}}`; +} + +function onAnkiTemplateRender(e) { +    e.preventDefault(); + +    const field = document.querySelector('#field-template-render-text').value; +    const infoNode = document.querySelector('#field-template-render-result'); +    infoNode.hidden = true; +    ankiTemplatesValidate(infoNode, field, 'term-kanji', true, false); +} diff --git a/ext/bg/js/settings/anki.js b/ext/bg/js/settings/anki.js new file mode 100644 index 00000000..e1aabbaf --- /dev/null +++ b/ext/bg/js/settings/anki.js @@ -0,0 +1,247 @@ +/* + * 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/>. + */ + + +// Private + +let _ankiDataPopulated = false; + + +function _ankiSpinnerShow(show) { +    const spinner = $('#anki-spinner'); +    if (show) { +        spinner.show(); +    } else { +        spinner.hide(); +    } +} + +function _ankiSetError(error) { +    const node = document.querySelector('#anki-error'); +    if (!node) { return; } +    if (error) { +        node.hidden = false; +        node.textContent = `${error}`; +    } +    else { +        node.hidden = true; +        node.textContent = ''; +    } +} + +function _ankiSetDropdownOptions(dropdown, optionValues) { +    const fragment = document.createDocumentFragment(); +    for (const optionValue of optionValues) { +        const option = document.createElement('option'); +        option.value = optionValue; +        option.textContent = optionValue; +        fragment.appendChild(option); +    } +    dropdown.textContent = ''; +    dropdown.appendChild(fragment); +} + +async function _ankiDeckAndModelPopulate(options) { +    const termsDeck = {value: options.anki.terms.deck, selector: '#anki-terms-deck'}; +    const kanjiDeck = {value: options.anki.kanji.deck, selector: '#anki-kanji-deck'}; +    const termsModel = {value: options.anki.terms.model, selector: '#anki-terms-model'}; +    const kanjiModel = {value: options.anki.kanji.model, selector: '#anki-kanji-model'}; +    try { +        _ankiSpinnerShow(true); +        const [deckNames, modelNames] = await Promise.all([utilAnkiGetDeckNames(), utilAnkiGetModelNames()]); +        deckNames.sort(); +        modelNames.sort(); +        termsDeck.values = deckNames; +        kanjiDeck.values = deckNames; +        termsModel.values = modelNames; +        kanjiModel.values = modelNames; +        _ankiSetError(null); +    } catch (error) { +        _ankiSetError(error); +    } finally { +        _ankiSpinnerShow(false); +    } + +    for (const {value, values, selector} of [termsDeck, kanjiDeck, termsModel, kanjiModel]) { +        const node = document.querySelector(selector); +        _ankiSetDropdownOptions(node, Array.isArray(values) ? values : [value]); +        node.value = value; +    } +} + +function _ankiCreateFieldTemplate(name, value, markers) { +    const template = document.querySelector('#anki-field-template').content; +    const content = document.importNode(template, true).firstChild; + +    content.querySelector('.anki-field-name').textContent = name; + +    const field = content.querySelector('.anki-field-value'); +    field.dataset.field = name; +    field.value = value; + +    content.querySelector('.anki-field-marker-list').appendChild(ankiGetFieldMarkersHtml(markers)); + +    return content; +} + +async function _ankiFieldsPopulate(tabId, options) { +    const tab = document.querySelector(`.tab-pane[data-anki-card-type=${tabId}]`); +    const container = tab.querySelector('tbody'); +    const markers = ankiGetFieldMarkers(tabId); + +    const fragment = document.createDocumentFragment(); +    const fields = options.anki[tabId].fields; +    for (const name of Object.keys(fields)) { +        const value = fields[name]; +        const html = _ankiCreateFieldTemplate(name, value, markers); +        fragment.appendChild(html); +    } + +    container.textContent = ''; +    container.appendChild(fragment); + +    for (const node of container.querySelectorAll('.anki-field-value')) { +        node.addEventListener('change', (e) => onFormOptionsChanged(e), false); +    } +    for (const node of container.querySelectorAll('.marker-link')) { +        node.addEventListener('click', (e) => _onAnkiMarkerClicked(e), false); +    } +} + +function _onAnkiMarkerClicked(e) { +    e.preventDefault(); +    const link = e.currentTarget; +    const input = $(link).closest('.input-group').find('.anki-field-value')[0]; +    input.value = `{${link.textContent}}`; +    input.dispatchEvent(new Event('change')); +} + +async function _onAnkiModelChanged(e) { +    const node = e.currentTarget; +    let fieldNames; +    try { +        const modelName = node.value; +        fieldNames = await utilAnkiGetModelFieldNames(modelName); +        _ankiSetError(null); +    } catch (error) { +        _ankiSetError(error); +        return; +    } finally { +        _ankiSpinnerShow(false); +    } + +    const tabId = node.dataset.ankiCardType; +    if (tabId !== 'terms' && tabId !== 'kanji') { return; } + +    const fields = {}; +    for (const name of fieldNames) { +        fields[name] = ''; +    } + +    const optionsContext = getOptionsContext(); +    const options = await apiOptionsGet(optionsContext); +    options.anki[tabId].fields = utilBackgroundIsolate(fields); +    await settingsSaveOptions(); + +    await _ankiFieldsPopulate(tabId, options); +} + + +// Public + +function ankiErrorShown() { +    const node = document.querySelector('#anki-error'); +    return node && !node.hidden; +} + +function ankiFieldsToDict(elements) { +    const result = {}; +    for (const element of elements) { +        result[element.dataset.field] = element.value; +    } +    return result; +} + + +function ankiGetFieldMarkersHtml(markers) { +    const template = document.querySelector('#anki-field-marker-template').content; +    const fragment = document.createDocumentFragment(); +    for (const marker of markers) { +        const markerNode = document.importNode(template, true).firstChild; +        markerNode.querySelector('.marker-link').textContent = marker; +        fragment.appendChild(markerNode); +    } +    return fragment; +} + +function ankiGetFieldMarkers(type) { +    switch (type) { +        case 'terms': +            return [ +                'audio', +                'cloze-body', +                'cloze-prefix', +                'cloze-suffix', +                'dictionary', +                'expression', +                'furigana', +                'furigana-plain', +                'glossary', +                'glossary-brief', +                'reading', +                'screenshot', +                'sentence', +                'tags', +                'url' +            ]; +        case 'kanji': +            return [ +                'character', +                'dictionary', +                'glossary', +                'kunyomi', +                'onyomi', +                'screenshot', +                'sentence', +                'tags', +                'url' +            ]; +        default: +            return []; +    } +} + + +function ankiInitialize() { +    for (const node of document.querySelectorAll('#anki-terms-model,#anki-kanji-model')) { +        node.addEventListener('change', (e) => _onAnkiModelChanged(e), false); +    } +} + +async function onAnkiOptionsChanged(options) { +    if (!options.anki.enable) { +        _ankiDataPopulated = false; +        return; +    } + +    if (_ankiDataPopulated) { return; } + +    await _ankiDeckAndModelPopulate(options); +    _ankiDataPopulated = true; +    await Promise.all([_ankiFieldsPopulate('terms', options), _ankiFieldsPopulate('kanji', options)]); +} diff --git a/ext/bg/js/settings/audio.js b/ext/bg/js/settings/audio.js new file mode 100644 index 00000000..f63551ed --- /dev/null +++ b/ext/bg/js/settings/audio.js @@ -0,0 +1,102 @@ +/* + * 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/>. + */ + + +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 = () => settingsSaveOptions(); + +    textToSpeechInitialize(); +} + +function textToSpeechInitialize() { +    if (typeof speechSynthesis === 'undefined') { return; } + +    speechSynthesis.addEventListener('voiceschanged', () => updateTextToSpeechVoices(), false); +    updateTextToSpeechVoices(); + +    $('#text-to-speech-voice-test').on('click', () => textToSpeechTest()); +} + +function updateTextToSpeechVoices() { +    const voices = Array.prototype.map.call(speechSynthesis.getVoices(), (voice, index) => ({voice, index})); +    voices.sort(textToSpeechVoiceCompare); +    if (voices.length > 0) { +        $('#text-to-speech-voice-container').css('display', ''); +    } + +    const select = $('#text-to-speech-voice'); +    select.empty(); +    select.append($('<option>').val('').text('None')); +    for (const {voice} of voices) { +        select.append($('<option>').val(voice.voiceURI).text(`${voice.name} (${voice.lang})`)); +    } + +    select.val(select.attr('data-value')); +} + +function languageTagIsJapanese(languageTag) { +    return ( +        languageTag.startsWith('ja-') || +        languageTag.startsWith('jpn-') +    ); +} + +function textToSpeechVoiceCompare(a, b) { +    const aIsJapanese = languageTagIsJapanese(a.voice.lang); +    const bIsJapanese = languageTagIsJapanese(b.voice.lang); +    if (aIsJapanese) { +        if (!bIsJapanese) { return -1; } +    } else { +        if (bIsJapanese) { return 1; } +    } + +    const aIsDefault = a.voice.default; +    const bIsDefault = b.voice.default; +    if (aIsDefault) { +        if (!bIsDefault) { return -1; } +    } else { +        if (bIsDefault) { return 1; } +    } + +    if (a.index < b.index) { return -1; } +    if (a.index > b.index) { return 1; } +    return 0; +} + +function textToSpeechTest() { +    try { +        const text = $('#text-to-speech-voice-test').attr('data-speech-text') || ''; +        const voiceURI = $('#text-to-speech-voice').val(); +        const voice = audioGetTextToSpeechVoice(voiceURI); +        if (voice === null) { return; } + +        const utterance = new SpeechSynthesisUtterance(text); +        utterance.lang = 'ja-JP'; +        utterance.voice = voice; +        utterance.volume = 1.0; + +        speechSynthesis.speak(utterance); +    } catch (e) { +        // NOP +    } +} diff --git a/ext/bg/js/settings-dictionaries.js b/ext/bg/js/settings/dictionaries.js index ebd380ac..065a8abc 100644 --- a/ext/bg/js/settings-dictionaries.js +++ b/ext/bg/js/settings/dictionaries.js @@ -62,8 +62,8 @@ class SettingsDictionaryListUI {          this.updateDictionaryOrder(); -        const titles = this.dictionaryEntries.map(e => e.dictionaryInfo.title); -        const removeKeys = Object.keys(this.optionsDictionaries).filter(key => titles.indexOf(key) < 0); +        const titles = this.dictionaryEntries.map((e) => e.dictionaryInfo.title); +        const removeKeys = Object.keys(this.optionsDictionaries).filter((key) => titles.indexOf(key) < 0);          if (removeKeys.length > 0) {              for (const key of toIterable(removeKeys)) {                  delete this.optionsDictionaries[key]; @@ -81,7 +81,7 @@ class SettingsDictionaryListUI {          let changed = false;          let optionsDictionary;          const optionsDictionaries = this.optionsDictionaries; -        if (optionsDictionaries.hasOwnProperty(title)) { +        if (hasOwn(optionsDictionaries, title)) {              optionsDictionary = optionsDictionaries[title];          } else {              optionsDictionary = SettingsDictionaryListUI.createDictionaryOptions(); @@ -161,7 +161,7 @@ class SettingsDictionaryListUI {          delete n.dataset.dict;          $(n).modal('hide'); -        const index = this.dictionaryEntries.findIndex(e => e.dictionaryInfo.title === title); +        const index = this.dictionaryEntries.findIndex((e) => e.dictionaryInfo.title === title);          if (index >= 0) {              this.dictionaryEntries[index].deleteDictionary();          } @@ -377,7 +377,7 @@ async function onDatabaseUpdated(options) {          updateMainDictionarySelect(options, dictionaries); -        const {counts, total} = await utilDatabaseGetDictionaryCounts(dictionaries.map(v => v.title), true); +        const {counts, total} = await utilDatabaseGetDictionaryCounts(dictionaries.map((v) => v.title), true);          dictionaryUI.setCounts(counts, total);      } catch (e) {          dictionaryErrorsShow([e]); @@ -466,7 +466,7 @@ function dictionaryErrorsShow(errors) {          for (let e of errors) {              console.error(e);              e = dictionaryErrorToString(e); -            uniqueErrors[e] = uniqueErrors.hasOwnProperty(e) ? uniqueErrors[e] + 1 : 1; +            uniqueErrors[e] = hasOwn(uniqueErrors, e) ? uniqueErrors[e] + 1 : 1;          }          for (const e in uniqueErrors) { @@ -564,7 +564,7 @@ async function onDictionaryImport(e) {          dictionaryErrorsShow(null);          dictionarySpinnerShow(true); -        const setProgress = percent => dictProgress.find('.progress-bar').css('width', `${percent}%`); +        const setProgress = (percent) => dictProgress.find('.progress-bar').css('width', `${percent}%`);          const updateProgress = (total, current) => {              setProgress(current / total * 100.0);              if (storageEstimate.mostRecent !== null && !storageUpdateStats.isUpdating) { diff --git a/ext/bg/js/settings/main.js b/ext/bg/js/settings/main.js new file mode 100644 index 00000000..7456e7a4 --- /dev/null +++ b/ext/bg/js/settings/main.js @@ -0,0 +1,239 @@ +/* + * Copyright (C) 2016-2017  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/>. + */ + +async function getOptionsArray() { +    const optionsFull = await apiOptionsGetFull(); +    return optionsFull.profiles.map((profile) => profile.options); +} + +async function formRead(options) { +    options.general.enable = $('#enable').prop('checked'); +    options.general.showGuide = $('#show-usage-guide').prop('checked'); +    options.general.compactTags = $('#compact-tags').prop('checked'); +    options.general.compactGlossaries = $('#compact-glossaries').prop('checked'); +    options.general.resultOutputMode = $('#result-output-mode').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); +    options.general.popupDisplayMode = $('#popup-display-mode').val(); +    options.general.popupHorizontalTextPosition = $('#popup-horizontal-text-position').val(); +    options.general.popupVerticalTextPosition = $('#popup-vertical-text-position').val(); +    options.general.popupWidth = parseInt($('#popup-width').val(), 10); +    options.general.popupHeight = parseInt($('#popup-height').val(), 10); +    options.general.popupHorizontalOffset = parseInt($('#popup-horizontal-offset').val(), 0); +    options.general.popupVerticalOffset = parseInt($('#popup-vertical-offset').val(), 10); +    options.general.popupHorizontalOffset2 = parseInt($('#popup-horizontal-offset2').val(), 0); +    options.general.popupVerticalOffset2 = parseInt($('#popup-vertical-offset2').val(), 10); +    options.general.popupTheme = $('#popup-theme').val(); +    options.general.popupOuterTheme = $('#popup-outer-theme').val(); +    options.general.customPopupCss = $('#custom-popup-css').val(); +    options.general.customPopupOuterCss = $('#custom-popup-outer-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.audio.textToSpeechVoice = $('#text-to-speech-voice').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'); +    options.scanning.alphanumeric = $('#search-alphanumeric').prop('checked'); +    options.scanning.autoHideResults = $('#auto-hide-results').prop('checked'); +    options.scanning.deepDomScan = $('#deep-dom-scan').prop('checked'); +    options.scanning.enablePopupSearch = $('#enable-search-within-first-popup').prop('checked'); +    options.scanning.enableOnPopupExpressions = $('#enable-scanning-of-popup-expressions').prop('checked'); +    options.scanning.enableOnSearchPage = $('#enable-scanning-on-search-page').prop('checked'); +    options.scanning.delay = parseInt($('#scan-delay').val(), 10); +    options.scanning.length = parseInt($('#scan-length').val(), 10); +    options.scanning.modifier = $('#scan-modifier-key').val(); +    options.scanning.popupNestingMaxDepth = parseInt($('#popup-nesting-max-depth').val(), 10); + +    options.parsing.enableScanningParser = $('#parsing-scan-enable').prop('checked'); +    options.parsing.enableMecabParser = $('#parsing-mecab-enable').prop('checked'); +    options.parsing.readingMode = $('#parsing-reading-mode').val(); + +    const optionsAnkiEnableOld = options.anki.enable; +    options.anki.enable = $('#anki-enable').prop('checked'); +    options.anki.tags = utilBackgroundIsolate($('#card-tags').val().split(/[,; ]+/)); +    options.anki.sentenceExt = parseInt($('#sentence-detection-extent').val(), 10); +    options.anki.server = $('#interface-server').val(); +    options.anki.screenshot.format = $('#screenshot-format').val(); +    options.anki.screenshot.quality = parseInt($('#screenshot-quality').val(), 10); +    options.anki.fieldTemplates = $('#field-templates').val(); + +    if (optionsAnkiEnableOld && !ankiErrorShown()) { +        options.anki.terms.deck = $('#anki-terms-deck').val(); +        options.anki.terms.model = $('#anki-terms-model').val(); +        options.anki.terms.fields = utilBackgroundIsolate(ankiFieldsToDict(document.querySelectorAll('#terms .anki-field-value'))); +        options.anki.kanji.deck = $('#anki-kanji-deck').val(); +        options.anki.kanji.model = $('#anki-kanji-model').val(); +        options.anki.kanji.fields = utilBackgroundIsolate(ankiFieldsToDict(document.querySelectorAll('#kanji .anki-field-value'))); +    } +} + +async function formWrite(options) { +    $('#enable').prop('checked', options.general.enable); +    $('#show-usage-guide').prop('checked', options.general.showGuide); +    $('#compact-tags').prop('checked', options.general.compactTags); +    $('#compact-glossaries').prop('checked', options.general.compactGlossaries); +    $('#result-output-mode').val(options.general.resultOutputMode); +    $('#show-debug-info').prop('checked', options.general.debugInfo); +    $('#show-advanced-options').prop('checked', options.general.showAdvanced); +    $('#max-displayed-results').val(options.general.maxResults); +    $('#popup-display-mode').val(options.general.popupDisplayMode); +    $('#popup-horizontal-text-position').val(options.general.popupHorizontalTextPosition); +    $('#popup-vertical-text-position').val(options.general.popupVerticalTextPosition); +    $('#popup-width').val(options.general.popupWidth); +    $('#popup-height').val(options.general.popupHeight); +    $('#popup-horizontal-offset').val(options.general.popupHorizontalOffset); +    $('#popup-vertical-offset').val(options.general.popupVerticalOffset); +    $('#popup-horizontal-offset2').val(options.general.popupHorizontalOffset2); +    $('#popup-vertical-offset2').val(options.general.popupVerticalOffset2); +    $('#popup-theme').val(options.general.popupTheme); +    $('#popup-outer-theme').val(options.general.popupOuterTheme); +    $('#custom-popup-css').val(options.general.customPopupCss); +    $('#custom-popup-outer-css').val(options.general.customPopupOuterCss); + +    $('#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); +    $('#text-to-speech-voice').val(options.audio.textToSpeechVoice).attr('data-value', options.audio.textToSpeechVoice); + +    $('#middle-mouse-button-scan').prop('checked', options.scanning.middleMouse); +    $('#touch-input-enabled').prop('checked', options.scanning.touchInputEnabled); +    $('#select-matched-text').prop('checked', options.scanning.selectText); +    $('#search-alphanumeric').prop('checked', options.scanning.alphanumeric); +    $('#auto-hide-results').prop('checked', options.scanning.autoHideResults); +    $('#deep-dom-scan').prop('checked', options.scanning.deepDomScan); +    $('#enable-search-within-first-popup').prop('checked', options.scanning.enablePopupSearch); +    $('#enable-scanning-of-popup-expressions').prop('checked', options.scanning.enableOnPopupExpressions); +    $('#enable-scanning-on-search-page').prop('checked', options.scanning.enableOnSearchPage); +    $('#scan-delay').val(options.scanning.delay); +    $('#scan-length').val(options.scanning.length); +    $('#scan-modifier-key').val(options.scanning.modifier); +    $('#popup-nesting-max-depth').val(options.scanning.popupNestingMaxDepth); + +    $('#parsing-scan-enable').prop('checked', options.parsing.enableScanningParser); +    $('#parsing-mecab-enable').prop('checked', options.parsing.enableMecabParser); +    $('#parsing-reading-mode').val(options.parsing.readingMode); + +    $('#anki-enable').prop('checked', options.anki.enable); +    $('#card-tags').val(options.anki.tags.join(' ')); +    $('#sentence-detection-extent').val(options.anki.sentenceExt); +    $('#interface-server').val(options.anki.server); +    $('#screenshot-format').val(options.anki.screenshot.format); +    $('#screenshot-quality').val(options.anki.screenshot.quality); +    $('#field-templates').val(options.anki.fieldTemplates); + +    onAnkiTemplatesValidateCompile(); +    await onAnkiOptionsChanged(options); +    await onDictionaryOptionsChanged(options); + +    formUpdateVisibility(options); +} + +function formSetupEventListeners() { +    $('input, select, textarea').not('.anki-model').not('.ignore-form-changes *').change((e) => onFormOptionsChanged(e)); +} + +function formUpdateVisibility(options) { +    document.documentElement.dataset.optionsAnkiEnable = `${!!options.anki.enable}`; +    document.documentElement.dataset.optionsGeneralDebugInfo = `${!!options.general.debugInfo}`; +    document.documentElement.dataset.optionsGeneralShowAdvanced = `${!!options.general.showAdvanced}`; +    document.documentElement.dataset.optionsGeneralResultOutputMode = `${options.general.resultOutputMode}`; + +    if (options.general.debugInfo) { +        const temp = utilIsolate(options); +        temp.anki.fieldTemplates = '...'; +        const text = JSON.stringify(temp, null, 4); +        $('#debug').text(text); +    } +} + +async function onFormOptionsChanged() { +    const optionsContext = getOptionsContext(); +    const options = await apiOptionsGet(optionsContext); + +    await formRead(options); +    await settingsSaveOptions(); +    formUpdateVisibility(options); + +    await onAnkiOptionsChanged(options); +} + + +function settingsGetSource() { +    return new Promise((resolve) => { +        chrome.tabs.getCurrent((tab) => resolve(`settings${tab ? tab.id : ''}`)); +    }); +} + +async function settingsSaveOptions() { +    const source = await settingsGetSource(); +    await apiOptionsSave(source); +} + +async function onOptionsUpdate({source}) { +    const thisSource = await settingsGetSource(); +    if (source === thisSource) { return; } + +    const optionsContext = getOptionsContext(); +    const options = await apiOptionsGet(optionsContext); +    await formWrite(options); +} + +function onMessage({action, params}, sender, callback) { +    switch (action) { +        case 'optionsUpdate': +            onOptionsUpdate(params); +            break; +        case 'getUrl': +            callback({url: window.location.href}); +            break; +    } +} + + +function showExtensionInformation() { +    const node = document.getElementById('extension-info'); +    if (node === null) { return; } + +    const manifest = chrome.runtime.getManifest(); +    node.textContent = `${manifest.name} v${manifest.version}`; +} + + +async function onReady() { +    showExtensionInformation(); + +    formSetupEventListeners(); +    appearanceInitialize(); +    await audioSettingsInitialize(); +    await profileOptionsSetup(); +    await dictSettingsInitialize(); +    ankiInitialize(); +    ankiTemplatesInitialize(); + +    storageInfoInitialize(); + +    chrome.runtime.onMessage.addListener(onMessage); +} + +$(document).ready(() => onReady()); diff --git a/ext/bg/js/settings-popup-preview.js b/ext/bg/js/settings/popup-preview-frame.js index 7d641c46..49409968 100644 --- a/ext/bg/js/settings-popup-preview.js +++ b/ext/bg/js/settings/popup-preview-frame.js @@ -106,7 +106,7 @@ class SettingsPopupPreview {      onMessage(e) {          const {action, params} = e.data;          const handlers = SettingsPopupPreview.messageHandlers; -        if (handlers.hasOwnProperty(action)) { +        if (hasOwn(handlers, action)) {              const handler = handlers[action];              handler(this, params);          } diff --git a/ext/bg/js/settings/popup-preview.js b/ext/bg/js/settings/popup-preview.js new file mode 100644 index 00000000..d8579eb1 --- /dev/null +++ b/ext/bg/js/settings/popup-preview.js @@ -0,0 +1,62 @@ +/* + * 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 appearanceInitialize() { +    let previewVisible = false; +    $('#settings-popup-preview-button').on('click', () => { +        if (previewVisible) { return; } +        showAppearancePreview(); +        previewVisible = true; +    }); +} + +function showAppearancePreview() { +    const container = $('#settings-popup-preview-container'); +    const buttonContainer = $('#settings-popup-preview-button-container'); +    const settings = $('#settings-popup-preview-settings'); +    const text = $('#settings-popup-preview-text'); +    const customCss = $('#custom-popup-css'); +    const customOuterCss = $('#custom-popup-outer-css'); + +    const frame = document.createElement('iframe'); +    frame.src = '/bg/settings-popup-preview.html'; +    frame.id = 'settings-popup-preview-frame'; + +    window.wanakana.bind(text[0]); + +    text.on('input', () => { +        const action = 'setText'; +        const params = {text: text.val()}; +        frame.contentWindow.postMessage({action, params}, '*'); +    }); +    customCss.on('input', () => { +        const action = 'setCustomCss'; +        const params = {css: customCss.val()}; +        frame.contentWindow.postMessage({action, params}, '*'); +    }); +    customOuterCss.on('input', () => { +        const action = 'setCustomOuterCss'; +        const params = {css: customOuterCss.val()}; +        frame.contentWindow.postMessage({action, params}, '*'); +    }); + +    container.append(frame); +    buttonContainer.remove(); +    settings.css('display', ''); +} diff --git a/ext/bg/js/settings-profiles.js b/ext/bg/js/settings/profiles.js index 3ae9db14..8c218e97 100644 --- a/ext/bg/js/settings-profiles.js +++ b/ext/bg/js/settings/profiles.js @@ -35,16 +35,16 @@ async function profileOptionsSetup() {  }  function profileOptionsSetupEventListeners() { -    $('#profile-target').change(utilAsync(onTargetProfileChanged)); -    $('#profile-name').change(onProfileNameChanged); -    $('#profile-add').click(utilAsync(onProfileAdd)); -    $('#profile-remove').click(utilAsync(onProfileRemove)); -    $('#profile-remove-confirm').click(utilAsync(onProfileRemoveConfirm)); -    $('#profile-copy').click(utilAsync(onProfileCopy)); -    $('#profile-copy-confirm').click(utilAsync(onProfileCopyConfirm)); +    $('#profile-target').change((e) => onTargetProfileChanged(e)); +    $('#profile-name').change((e) => onProfileNameChanged(e)); +    $('#profile-add').click((e) => onProfileAdd(e)); +    $('#profile-remove').click((e) => onProfileRemove(e)); +    $('#profile-remove-confirm').click((e) => onProfileRemoveConfirm(e)); +    $('#profile-copy').click((e) => onProfileCopy(e)); +    $('#profile-copy-confirm').click((e) => onProfileCopyConfirm(e));      $('#profile-move-up').click(() => onProfileMove(-1));      $('#profile-move-down').click(() => onProfileMove(1)); -    $('.profile-form').find('input, select, textarea').not('.profile-form-manual').change(utilAsync(onProfileOptionsChanged)); +    $('.profile-form').find('input, select, textarea').not('.profile-form-manual').change((e) => onProfileOptionsChanged(e));  }  function tryGetIntegerValue(selector, min, max) { @@ -147,7 +147,7 @@ function profileOptionsCreateCopyName(name, profiles, maxUniqueAttempts) {      let i = 0;      while (true) {          const newName = `${prefix}${space}${index}${suffix}`; -        if (i++ >= maxUniqueAttempts || profiles.findIndex(profile => profile.name === newName) < 0) { +        if (i++ >= maxUniqueAttempts || profiles.findIndex((profile) => profile.name === newName) < 0) {              return newName;          }          if (typeof index !== 'number') { diff --git a/ext/bg/js/settings/storage.js b/ext/bg/js/settings/storage.js new file mode 100644 index 00000000..51ca6855 --- /dev/null +++ b/ext/bg/js/settings/storage.js @@ -0,0 +1,138 @@ +/* + * 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 storageBytesToLabeledString(size) { +    const base = 1000; +    const labels = [' bytes', 'KB', 'MB', 'GB']; +    let labelIndex = 0; +    while (size >= base) { +        size /= base; +        ++labelIndex; +    } +    const label = labelIndex === 0 ? `${size}` : size.toFixed(1); +    return `${label}${labels[labelIndex]}`; +} + +async function storageEstimate() { +    try { +        return (storageEstimate.mostRecent = await navigator.storage.estimate()); +    } catch (e) { +        // NOP +    } +    return null; +} +storageEstimate.mostRecent = null; + +async function isStoragePeristent() { +    try { +        return await navigator.storage.persisted(); +    } catch (e) { +        // NOP +    } +    return false; +} + +async function storageInfoInitialize() { +    storagePersistInitialize(); +    const {browser, platform} = await apiGetEnvironmentInfo(); +    document.documentElement.dataset.browser = browser; +    document.documentElement.dataset.operatingSystem = platform.os; + +    await storageShowInfo(); + +    document.querySelector('#storage-refresh').addEventListener('click', () => storageShowInfo(), false); +} + +async function storageUpdateStats() { +    storageUpdateStats.isUpdating = true; + +    const estimate = await storageEstimate(); +    const valid = (estimate !== null); + +    if (valid) { +        // Firefox reports usage as 0 when persistent storage is enabled. +        const finite = (estimate.usage > 0 || !(await isStoragePeristent())); +        if (finite) { +            document.querySelector('#storage-usage').textContent = storageBytesToLabeledString(estimate.usage); +            document.querySelector('#storage-quota').textContent = storageBytesToLabeledString(estimate.quota); +        } +        document.querySelector('#storage-use-finite').classList.toggle('storage-hidden', !finite); +        document.querySelector('#storage-use-infinite').classList.toggle('storage-hidden', finite); +    } + +    storageUpdateStats.isUpdating = false; +    return valid; +} +storageUpdateStats.isUpdating = false; + +async function storageShowInfo() { +    storageSpinnerShow(true); + +    const valid = await storageUpdateStats(); +    document.querySelector('#storage-use').classList.toggle('storage-hidden', !valid); +    document.querySelector('#storage-error').classList.toggle('storage-hidden', valid); + +    storageSpinnerShow(false); +} + +function storageSpinnerShow(show) { +    const spinner = $('#storage-spinner'); +    if (show) { +        spinner.show(); +    } else { +        spinner.hide(); +    } +} + +async function storagePersistInitialize() { +    if (!(navigator.storage && navigator.storage.persist)) { +        // Not supported +        return; +    } + +    const info = document.querySelector('#storage-persist-info'); +    const button = document.querySelector('#storage-persist-button'); +    const checkbox = document.querySelector('#storage-persist-button-checkbox'); + +    info.classList.remove('storage-hidden'); +    button.classList.remove('storage-hidden'); + +    let persisted = await isStoragePeristent(); +    checkbox.checked = persisted; + +    button.addEventListener('click', async () => { +        if (persisted) { +            return; +        } +        let result = false; +        try { +            result = await navigator.storage.persist(); +        } catch (e) { +            // NOP +        } + +        if (result) { +            persisted = true; +            checkbox.checked = true; +            storageShowInfo(); +        } else { +            $('.storage-persist-fail-warning').removeClass('storage-hidden'); +        } +    }, false); +} diff --git a/ext/bg/js/templates.js b/ext/bg/js/templates.js index 6e377957..9320477f 100644 --- a/ext/bg/js/templates.js +++ b/ext/bg/js/templates.js @@ -33,19 +33,18 @@ 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.svg\" class=\"current\" title=\"Current entry (Alt + Up/Down/Home/End/PgUp/PgDn)\" alt>\n    </div>\n\n    <div class=\"glyph expression-scan-toggle\">"      + 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 : "") +    + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.frequencies : depth0),{"name":"if","hash":{},"fn":container.program(13, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")      + "\n" -    + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.tags : depth0),{"name":"if","hash":{},"fn":container.program(18, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") +    + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.tags : depth0),{"name":"if","hash":{},"fn":container.program(16, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")      + "\n    <table class=\"table table-condensed glyph-data\">\n        <tr>\n            <th>Glossary</th>\n            <th>Readings</th>\n            <th>Statistics</th>\n        </tr>\n        <tr>\n            <td class=\"glossary\">\n" -    + ((stack1 = helpers["if"].call(alias1,((stack1 = (depth0 != null ? depth0.glossary : depth0)) != null ? stack1["1"] : stack1),{"name":"if","hash":{},"fn":container.program(21, data, 0),"inverse":container.program(24, data, 0),"data":data})) != null ? stack1 : "") +    + ((stack1 = helpers["if"].call(alias1,((stack1 = (depth0 != null ? depth0.glossary : depth0)) != null ? stack1["1"] : stack1),{"name":"if","hash":{},"fn":container.program(19, data, 0),"inverse":container.program(22, data, 0),"data":data})) != null ? stack1 : "")      + "            </td>\n            <td class=\"reading\">\n                " -    + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.onyomi : depth0),{"name":"if","hash":{},"fn":container.program(26, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") +    + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.onyomi : depth0),{"name":"if","hash":{},"fn":container.program(24, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")      + "\n                " -    + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.kunyomi : depth0),{"name":"if","hash":{},"fn":container.program(29, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") +    + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.kunyomi : depth0),{"name":"if","hash":{},"fn":container.program(27, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")      + "\n            </td>\n            <td>"      + ((stack1 = container.invokePartial(partials.table,depth0,{"name":"table","hash":{"data":((stack1 = (depth0 != null ? depth0.stats : depth0)) != null ? stack1.misc : stack1)},"data":data,"helpers":helpers,"partials":partials,"decorators":container.decorators})) != null ? stack1 : "")      + "</td>\n        </tr>\n        <tr>\n            <th colspan=\"3\">Classifications</th>\n        </tr>\n        <tr>\n            <td colspan=\"3\">" @@ -55,19 +54,17 @@ templates['kanji.html'] = template({"1":function(container,depth0,helpers,partia      + "</td>\n        </tr>\n        <tr>\n            <th colspan=\"3\">Dictionary Indices</th>\n        </tr>\n        <tr>\n            <td colspan=\"3\">"      + ((stack1 = container.invokePartial(partials.table,depth0,{"name":"table","hash":{"data":((stack1 = (depth0 != null ? depth0.stats : depth0)) != null ? stack1.index : stack1)},"data":data,"helpers":helpers,"partials":partials,"decorators":container.decorators})) != null ? stack1 : "")      + "</td>\n        </tr>\n    </table>\n\n" -    + ((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 : "") +    + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.debug : depth0),{"name":"if","hash":{},"fn":container.program(29, 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.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.svg\" title=\"Source term (Alt + B)\" alt></a>\n"; -},"15":function(container,depth0,helpers,partials,data) {      var stack1;    return "    <div>\n" -    + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.frequencies : depth0),{"name":"each","hash":{},"fn":container.program(16, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") +    + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.frequencies : depth0),{"name":"each","hash":{},"fn":container.program(14, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")      + "    </div>\n"; -},"16":function(container,depth0,helpers,partials,data) { +},"14":function(container,depth0,helpers,partials,data) {      var helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression;    return "        <span class=\"label label-default tag-frequency\">" @@ -75,13 +72,13 @@ templates['kanji.html'] = template({"1":function(container,depth0,helpers,partia      + ":"      + alias4(((helper = (helper = helpers.frequency || (depth0 != null ? depth0.frequency : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"frequency","hash":{},"data":data}) : helper)))      + "</span>\n"; -},"18":function(container,depth0,helpers,partials,data) { +},"16":function(container,depth0,helpers,partials,data) {      var stack1;    return "    <div>\n" -    + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.tags : depth0),{"name":"each","hash":{},"fn":container.program(19, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") +    + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.tags : depth0),{"name":"each","hash":{},"fn":container.program(17, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")      + "    </div>\n"; -},"19":function(container,depth0,helpers,partials,data) { +},"17":function(container,depth0,helpers,partials,data) {      var helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression;    return "        <span class=\"label label-default tag-" @@ -91,68 +88,81 @@ templates['kanji.html'] = template({"1":function(container,depth0,helpers,partia      + "\">"      + alias4(((helper = (helper = helpers.name || (depth0 != null ? depth0.name : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"name","hash":{},"data":data}) : helper)))      + "</span>\n"; -},"21":function(container,depth0,helpers,partials,data) { +},"19":function(container,depth0,helpers,partials,data) {      var stack1;    return "                <ol>" -    + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.glossary : depth0),{"name":"each","hash":{},"fn":container.program(22, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") +    + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.glossary : depth0),{"name":"each","hash":{},"fn":container.program(20, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")      + "</ol>\n"; -},"22":function(container,depth0,helpers,partials,data) { +},"20":function(container,depth0,helpers,partials,data) {      return "<li><span class=\"glossary-item\">"      + container.escapeExpression(container.lambda(depth0, depth0))      + "</span></li>"; -},"24":function(container,depth0,helpers,partials,data) { +},"22":function(container,depth0,helpers,partials,data) {      var stack1;    return "                <span class=\"glossary-item\">"      + container.escapeExpression(container.lambda(((stack1 = (depth0 != null ? depth0.glossary : depth0)) != null ? stack1["0"] : stack1), depth0))      + "</span>\n"; -},"26":function(container,depth0,helpers,partials,data) { +},"24":function(container,depth0,helpers,partials,data) {      var stack1;    return "<dl>" -    + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.onyomi : depth0),{"name":"each","hash":{},"fn":container.program(27, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") +    + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.onyomi : depth0),{"name":"each","hash":{},"fn":container.program(25, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")      + "</dl>"; -},"27":function(container,depth0,helpers,partials,data) { +},"25":function(container,depth0,helpers,partials,data) {      return "<dd>"      + container.escapeExpression(container.lambda(depth0, depth0))      + "</dd>"; -},"29":function(container,depth0,helpers,partials,data) { +},"27":function(container,depth0,helpers,partials,data) {      var stack1;    return "<dl>" -    + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.kunyomi : depth0),{"name":"each","hash":{},"fn":container.program(27, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") +    + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.kunyomi : depth0),{"name":"each","hash":{},"fn":container.program(25, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")      + "</dl>"; -},"31":function(container,depth0,helpers,partials,data) { +},"29":function(container,depth0,helpers,partials,data) {      var stack1, helper, options, buffer =     "    <pre>"; -  stack1 = ((helper = (helper = helpers.dumpObject || (depth0 != null ? depth0.dumpObject : depth0)) != null ? helper : helpers.helperMissing),(options={"name":"dumpObject","hash":{},"fn":container.program(32, data, 0),"inverse":container.noop,"data":data}),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : (container.nullContext || {}),options) : helper)); +  stack1 = ((helper = (helper = helpers.dumpObject || (depth0 != null ? depth0.dumpObject : depth0)) != null ? helper : helpers.helperMissing),(options={"name":"dumpObject","hash":{},"fn":container.program(30, data, 0),"inverse":container.noop,"data":data}),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : (container.nullContext || {}),options) : helper));    if (!helpers.dumpObject) { stack1 = helpers.blockHelperMissing.call(depth0,stack1,options)}    if (stack1 != null) { buffer += stack1; }    return buffer + "</pre>\n"; -},"32":function(container,depth0,helpers,partials,data) { +},"30":function(container,depth0,helpers,partials,data) {      var stack1;    return ((stack1 = container.lambda(depth0, depth0)) != null ? stack1 : ""); -},"34":function(container,depth0,helpers,partials,data,blockParams,depths) { -    var stack1; +},"32":function(container,depth0,helpers,partials,data,blockParams,depths) { +    var stack1, alias1=depth0 != null ? depth0 : (container.nullContext || {}); -  return ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.definitions : depth0),{"name":"each","hash":{},"fn":container.program(35, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : ""); -},"35":function(container,depth0,helpers,partials,data,blockParams,depths) { +  return "<div class=\"term-navigation\">\n    <a href=\"#\" " +    + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.source : depth0),{"name":"if","hash":{},"fn":container.program(33, data, 0, blockParams, depths),"inverse":container.program(35, data, 0, blockParams, depths),"data":data})) != null ? stack1 : "") +    + "><img src=\"/mixed/img/source-term.svg\" title=\"Source term (Alt + B)\" alt></a>\n    <a href=\"#\" " +    + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.next : depth0),{"name":"if","hash":{},"fn":container.program(37, data, 0, blockParams, depths),"inverse":container.program(39, data, 0, blockParams, depths),"data":data})) != null ? stack1 : "") +    + "><img src=\"/mixed/img/source-term.svg\" style=\"transform: scaleX(-1);\" title=\"Next term (Alt + F)\" alt></a>\n</div>\n" +    + ((stack1 = helpers.each.call(alias1,(depth0 != null ? depth0.definitions : depth0),{"name":"each","hash":{},"fn":container.program(41, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : ""); +},"33":function(container,depth0,helpers,partials,data) { +    return "class=\"source-term\""; +},"35":function(container,depth0,helpers,partials,data) { +    return "class=\"source-term term-button-fade\""; +},"37":function(container,depth0,helpers,partials,data) { +    return "class=\"next-term\""; +},"39":function(container,depth0,helpers,partials,data) { +    return "class=\"next-term term-button-fade\""; +},"41":function(container,depth0,helpers,partials,data,blockParams,depths) {      var stack1; -  return ((stack1 = helpers.unless.call(depth0 != null ? depth0 : (container.nullContext || {}),(data && data.first),{"name":"unless","hash":{},"fn":container.program(36, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "") +  return ((stack1 = helpers.unless.call(depth0 != null ? depth0 : (container.nullContext || {}),(data && data.first),{"name":"unless","hash":{},"fn":container.program(42, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "")      + "\n" -    + ((stack1 = container.invokePartial(partials.kanji,depth0,{"name":"kanji","hash":{"root":(depths[1] != null ? depths[1].root : depths[1]),"source":(depths[1] != null ? depths[1].source : depths[1]),"addable":(depths[1] != null ? depths[1].addable : depths[1]),"debug":(depths[1] != null ? depths[1].debug : depths[1])},"data":data,"helpers":helpers,"partials":partials,"decorators":container.decorators})) != null ? stack1 : ""); -},"36":function(container,depth0,helpers,partials,data) { +    + ((stack1 = container.invokePartial(partials.kanji,depth0,{"name":"kanji","hash":{"root":(depths[1] != null ? depths[1].root : depths[1]),"addable":(depths[1] != null ? depths[1].addable : depths[1]),"debug":(depths[1] != null ? depths[1].debug : depths[1])},"data":data,"helpers":helpers,"partials":partials,"decorators":container.decorators})) != null ? stack1 : ""); +},"42":function(container,depth0,helpers,partials,data) {      return "<hr>"; -},"38":function(container,depth0,helpers,partials,data) { +},"44":function(container,depth0,helpers,partials,data) {      return "<p class=\"note\">No results found</p>\n";  },"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data,blockParams,depths) {      var stack1;    return "\n\n" -    + ((stack1 = helpers["if"].call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.definitions : depth0),{"name":"if","hash":{},"fn":container.program(34, data, 0, blockParams, depths),"inverse":container.program(38, data, 0, blockParams, depths),"data":data})) != null ? stack1 : ""); +    + ((stack1 = helpers["if"].call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.definitions : depth0),{"name":"if","hash":{},"fn":container.program(32, data, 0, blockParams, depths),"inverse":container.program(44, data, 0, blockParams, depths),"data":data})) != null ? stack1 : "");  },"main_d":  function(fn, props, container, depth0, data, blockParams, depths) {    var decorators = container.decorators; @@ -306,17 +316,16 @@ templates['terms.html'] = template({"1":function(container,depth0,helpers,partia    return "<div class=\"entry\" data-type=\"term\">\n    <div class=\"actions\">\n"      + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.addable : depth0),{"name":"if","hash":{},"fn":container.program(25, 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(27, 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(30, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "")      + "        <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(32, data, 0, blockParams, depths),"inverse":container.program(47, data, 0, blockParams, depths),"data":data})) != null ? stack1 : "") +    + ((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(50, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "") +    + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.reasons : depth0),{"name":"if","hash":{},"fn":container.program(48, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "")      + "\n" -    + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.frequencies : depth0),{"name":"if","hash":{},"fn":container.program(54, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "") +    + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.frequencies : depth0),{"name":"if","hash":{},"fn":container.program(52, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "")      + "\n    <div class=\"glossary\">\n" -    + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.grouped : depth0),{"name":"if","hash":{},"fn":container.program(57, data, 0, blockParams, depths),"inverse":container.program(63, data, 0, blockParams, depths),"data":data})) != null ? stack1 : "") +    + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.grouped : depth0),{"name":"if","hash":{},"fn":container.program(55, data, 0, blockParams, depths),"inverse":container.program(61, data, 0, blockParams, depths),"data":data})) != null ? stack1 : "")      + "    </div>\n\n" -    + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.debug : depth0),{"name":"if","hash":{},"fn":container.program(66, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "") +    + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.debug : depth0),{"name":"if","hash":{},"fn":container.program(64, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "")      + "</div>\n";  },"25":function(container,depth0,helpers,partials,data) {      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"; @@ -326,47 +335,45 @@ templates['terms.html'] = template({"1":function(container,depth0,helpers,partia    return ((stack1 = helpers["if"].call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.playback : depth0),{"name":"if","hash":{},"fn":container.program(28, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "");  },"28":function(container,depth0,helpers,partials,data) {      return "        <a href=\"#\" class=\"action-play-audio\"><img src=\"/mixed/img/play-audio.svg\" title=\"Play audio (Alt + P)\" alt></a>\n"; -},"30":function(container,depth0,helpers,partials,data) { -    return "        <a href=\"#\" class=\"source-term\"><img src=\"/mixed/img/source-term.svg\" title=\"Source term (Alt + B)\" alt></a>\n"; -},"32":function(container,depth0,helpers,partials,data,blockParams,depths) { +},"30":function(container,depth0,helpers,partials,data,blockParams,depths) {      var stack1; -  return ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.expressions : depth0),{"name":"each","hash":{},"fn":container.program(33, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : ""); -},"33":function(container,depth0,helpers,partials,data,blockParams,depths) { +  return ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.expressions : depth0),{"name":"each","hash":{},"fn":container.program(31, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : ""); +},"31":function(container,depth0,helpers,partials,data,blockParams,depths) {      var stack1, helper, options, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=helpers.helperMissing, alias3="function", buffer =     "<div class=\"expression expression-scan-toggle\"><span class=\"expression-"      + container.escapeExpression(((helper = (helper = helpers.termFrequency || (depth0 != null ? depth0.termFrequency : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"termFrequency","hash":{},"data":data}) : helper)))      + "\">"; -  stack1 = ((helper = (helper = helpers.kanjiLinks || (depth0 != null ? depth0.kanjiLinks : depth0)) != null ? helper : alias2),(options={"name":"kanjiLinks","hash":{},"fn":container.program(34, data, 0, blockParams, depths),"inverse":container.noop,"data":data}),(typeof helper === alias3 ? helper.call(alias1,options) : helper)); +  stack1 = ((helper = (helper = helpers.kanjiLinks || (depth0 != null ? depth0.kanjiLinks : depth0)) != null ? helper : alias2),(options={"name":"kanjiLinks","hash":{},"fn":container.program(32, data, 0, blockParams, depths),"inverse":container.noop,"data":data}),(typeof helper === alias3 ? helper.call(alias1,options) : helper));    if (!helpers.kanjiLinks) { stack1 = helpers.blockHelperMissing.call(depth0,stack1,options)}    if (stack1 != null) { buffer += stack1; }    return buffer + "</span><div class=\"peek-wrapper\">" -    + ((stack1 = helpers["if"].call(alias1,(depths[1] != null ? depths[1].playback : depths[1]),{"name":"if","hash":{},"fn":container.program(37, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "") -    + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.termTags : depth0),{"name":"if","hash":{},"fn":container.program(39, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "") -    + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.frequencies : depth0),{"name":"if","hash":{},"fn":container.program(42, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "") +    + ((stack1 = helpers["if"].call(alias1,(depths[1] != null ? depths[1].playback : depths[1]),{"name":"if","hash":{},"fn":container.program(35, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "") +    + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.termTags : depth0),{"name":"if","hash":{},"fn":container.program(37, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "") +    + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.frequencies : depth0),{"name":"if","hash":{},"fn":container.program(40, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "")      + "</div><span class=\"" -    + ((stack1 = helpers["if"].call(alias1,(data && data.last),{"name":"if","hash":{},"fn":container.program(45, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "") +    + ((stack1 = helpers["if"].call(alias1,(data && data.last),{"name":"if","hash":{},"fn":container.program(43, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "")      + "\">、</span></div>"; -},"34":function(container,depth0,helpers,partials,data) { +},"32":function(container,depth0,helpers,partials,data) {      var stack1, helper, options; -  stack1 = ((helper = (helper = helpers.furigana || (depth0 != null ? depth0.furigana : depth0)) != null ? helper : helpers.helperMissing),(options={"name":"furigana","hash":{},"fn":container.program(35, data, 0),"inverse":container.noop,"data":data}),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : (container.nullContext || {}),options) : helper)); +  stack1 = ((helper = (helper = helpers.furigana || (depth0 != null ? depth0.furigana : depth0)) != null ? helper : helpers.helperMissing),(options={"name":"furigana","hash":{},"fn":container.program(33, data, 0),"inverse":container.noop,"data":data}),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : (container.nullContext || {}),options) : helper));    if (!helpers.furigana) { stack1 = helpers.blockHelperMissing.call(depth0,stack1,options)}    if (stack1 != null) { return stack1; }    else { return ''; } -},"35":function(container,depth0,helpers,partials,data) { +},"33":function(container,depth0,helpers,partials,data) {      var stack1;    return ((stack1 = container.lambda(depth0, depth0)) != null ? stack1 : ""); -},"37":function(container,depth0,helpers,partials,data) { +},"35":function(container,depth0,helpers,partials,data) {      return "<a href=\"#\" class=\"action-play-audio\"><img src=\"/mixed/img/play-audio.svg\" title=\"Play audio\" alt></a>"; -},"39":function(container,depth0,helpers,partials,data) { +},"37":function(container,depth0,helpers,partials,data) {      var stack1;    return "<div class=\"tags\">" -    + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.termTags : depth0),{"name":"each","hash":{},"fn":container.program(40, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") +    + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.termTags : depth0),{"name":"each","hash":{},"fn":container.program(38, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")      + "</div>"; -},"40":function(container,depth0,helpers,partials,data) { +},"38":function(container,depth0,helpers,partials,data) {      var helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression;    return "                <span class=\"label label-default tag-" @@ -376,13 +383,13 @@ templates['terms.html'] = template({"1":function(container,depth0,helpers,partia      + "\">"      + alias4(((helper = (helper = helpers.name || (depth0 != null ? depth0.name : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"name","hash":{},"data":data}) : helper)))      + "</span>\n"; -},"42":function(container,depth0,helpers,partials,data) { +},"40":function(container,depth0,helpers,partials,data) {      var stack1;    return "<div class=\"frequencies\">" -    + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.frequencies : depth0),{"name":"each","hash":{},"fn":container.program(43, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") +    + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.frequencies : depth0),{"name":"each","hash":{},"fn":container.program(41, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")      + "</div>"; -},"43":function(container,depth0,helpers,partials,data) { +},"41":function(container,depth0,helpers,partials,data) {      var helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression;    return "                <span class=\"label label-default tag-frequency\">" @@ -390,45 +397,45 @@ templates['terms.html'] = template({"1":function(container,depth0,helpers,partia      + ":"      + alias4(((helper = (helper = helpers.frequency || (depth0 != null ? depth0.frequency : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"frequency","hash":{},"data":data}) : helper)))      + "</span>\n"; -},"45":function(container,depth0,helpers,partials,data) { +},"43":function(container,depth0,helpers,partials,data) {      return "invisible"; -},"47":function(container,depth0,helpers,partials,data) { +},"45":function(container,depth0,helpers,partials,data) {      var stack1, helper, options, alias1=depth0 != null ? depth0 : (container.nullContext || {}), buffer =     "    <div class=\"expression expression-scan-toggle\">"; -  stack1 = ((helper = (helper = helpers.kanjiLinks || (depth0 != null ? depth0.kanjiLinks : depth0)) != null ? helper : helpers.helperMissing),(options={"name":"kanjiLinks","hash":{},"fn":container.program(34, data, 0),"inverse":container.noop,"data":data}),(typeof helper === "function" ? helper.call(alias1,options) : helper)); +  stack1 = ((helper = (helper = helpers.kanjiLinks || (depth0 != null ? depth0.kanjiLinks : depth0)) != null ? helper : helpers.helperMissing),(options={"name":"kanjiLinks","hash":{},"fn":container.program(32, data, 0),"inverse":container.noop,"data":data}),(typeof helper === "function" ? helper.call(alias1,options) : helper));    if (!helpers.kanjiLinks) { stack1 = helpers.blockHelperMissing.call(depth0,stack1,options)}    if (stack1 != null) { buffer += stack1; }    return buffer + "</div>\n" -    + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.termTags : depth0),{"name":"if","hash":{},"fn":container.program(48, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : ""); -},"48":function(container,depth0,helpers,partials,data) { +    + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.termTags : depth0),{"name":"if","hash":{},"fn":container.program(46, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : ""); +},"46":function(container,depth0,helpers,partials,data) {      var stack1;    return "    <div style=\"display: inline-block;\">\n"      + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.termTags : depth0),{"name":"each","hash":{},"fn":container.program(7, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")      + "    </div>\n"; -},"50":function(container,depth0,helpers,partials,data) { +},"48":function(container,depth0,helpers,partials,data) {      var stack1;    return "    <div class=\"reasons\">\n" -    + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.reasons : depth0),{"name":"each","hash":{},"fn":container.program(51, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") +    + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.reasons : depth0),{"name":"each","hash":{},"fn":container.program(49, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")      + "    </div>\n"; -},"51":function(container,depth0,helpers,partials,data) { +},"49":function(container,depth0,helpers,partials,data) {      var stack1;    return "        <span class=\"reasons\">"      + container.escapeExpression(container.lambda(depth0, depth0))      + "</span> " -    + ((stack1 = helpers.unless.call(depth0 != null ? depth0 : (container.nullContext || {}),(data && data.last),{"name":"unless","hash":{},"fn":container.program(52, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") +    + ((stack1 = helpers.unless.call(depth0 != null ? depth0 : (container.nullContext || {}),(data && data.last),{"name":"unless","hash":{},"fn":container.program(50, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")      + "\n"; -},"52":function(container,depth0,helpers,partials,data) { +},"50":function(container,depth0,helpers,partials,data) {      return "«"; -},"54":function(container,depth0,helpers,partials,data) { +},"52":function(container,depth0,helpers,partials,data) {      var stack1;    return "    <div>\n" -    + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.frequencies : depth0),{"name":"each","hash":{},"fn":container.program(55, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") +    + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.frequencies : depth0),{"name":"each","hash":{},"fn":container.program(53, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")      + "    </div>\n"; -},"55":function(container,depth0,helpers,partials,data) { +},"53":function(container,depth0,helpers,partials,data) {      var helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression;    return "        <span class=\"label label-default tag-frequency\">" @@ -436,61 +443,74 @@ templates['terms.html'] = template({"1":function(container,depth0,helpers,partia      + ":"      + alias4(((helper = (helper = helpers.frequency || (depth0 != null ? depth0.frequency : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"frequency","hash":{},"data":data}) : helper)))      + "</span>\n"; -},"57":function(container,depth0,helpers,partials,data,blockParams,depths) { +},"55":function(container,depth0,helpers,partials,data,blockParams,depths) {      var stack1; -  return ((stack1 = helpers["if"].call(depth0 != null ? depth0 : (container.nullContext || {}),((stack1 = (depth0 != null ? depth0.definitions : depth0)) != null ? stack1["1"] : stack1),{"name":"if","hash":{},"fn":container.program(58, data, 0, blockParams, depths),"inverse":container.program(61, data, 0, blockParams, depths),"data":data})) != null ? stack1 : ""); -},"58":function(container,depth0,helpers,partials,data,blockParams,depths) { +  return ((stack1 = helpers["if"].call(depth0 != null ? depth0 : (container.nullContext || {}),((stack1 = (depth0 != null ? depth0.definitions : depth0)) != null ? stack1["1"] : stack1),{"name":"if","hash":{},"fn":container.program(56, data, 0, blockParams, depths),"inverse":container.program(59, data, 0, blockParams, depths),"data":data})) != null ? stack1 : ""); +},"56":function(container,depth0,helpers,partials,data,blockParams,depths) {      var stack1;    return "        <ol>\n" -    + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.definitions : depth0),{"name":"each","hash":{},"fn":container.program(59, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "") +    + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.definitions : depth0),{"name":"each","hash":{},"fn":container.program(57, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "")      + "        </ol>\n"; -},"59":function(container,depth0,helpers,partials,data,blockParams,depths) { +},"57":function(container,depth0,helpers,partials,data,blockParams,depths) {      var stack1;    return "            <li>"      + ((stack1 = container.invokePartial(partials.definition,depth0,{"name":"definition","hash":{"compactGlossaries":(depths[1] != null ? depths[1].compactGlossaries : depths[1])},"data":data,"helpers":helpers,"partials":partials,"decorators":container.decorators})) != null ? stack1 : "")      + "</li>\n"; -},"61":function(container,depth0,helpers,partials,data) { +},"59":function(container,depth0,helpers,partials,data) {      var stack1;    return ((stack1 = container.invokePartial(partials.definition,((stack1 = (depth0 != null ? depth0.definitions : depth0)) != null ? stack1["0"] : stack1),{"name":"definition","hash":{"compactGlossaries":(depth0 != null ? depth0.compactGlossaries : depth0)},"data":data,"indent":"        ","helpers":helpers,"partials":partials,"decorators":container.decorators})) != null ? stack1 : ""); -},"63":function(container,depth0,helpers,partials,data,blockParams,depths) { +},"61":function(container,depth0,helpers,partials,data,blockParams,depths) {      var stack1; -  return ((stack1 = helpers["if"].call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.merged : depth0),{"name":"if","hash":{},"fn":container.program(57, data, 0, blockParams, depths),"inverse":container.program(64, data, 0, blockParams, depths),"data":data})) != null ? stack1 : ""); -},"64":function(container,depth0,helpers,partials,data) { +  return ((stack1 = helpers["if"].call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.merged : depth0),{"name":"if","hash":{},"fn":container.program(55, data, 0, blockParams, depths),"inverse":container.program(62, data, 0, blockParams, depths),"data":data})) != null ? stack1 : ""); +},"62":function(container,depth0,helpers,partials,data) {      var stack1;    return ((stack1 = container.invokePartial(partials.definition,depth0,{"name":"definition","hash":{"compactGlossaries":(depth0 != null ? depth0.compactGlossaries : depth0)},"data":data,"indent":"        ","helpers":helpers,"partials":partials,"decorators":container.decorators})) != null ? stack1 : "")      + "        "; -},"66":function(container,depth0,helpers,partials,data) { +},"64":function(container,depth0,helpers,partials,data) {      var stack1, helper, options, buffer =     "    <pre>"; -  stack1 = ((helper = (helper = helpers.dumpObject || (depth0 != null ? depth0.dumpObject : depth0)) != null ? helper : helpers.helperMissing),(options={"name":"dumpObject","hash":{},"fn":container.program(35, data, 0),"inverse":container.noop,"data":data}),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : (container.nullContext || {}),options) : helper)); +  stack1 = ((helper = (helper = helpers.dumpObject || (depth0 != null ? depth0.dumpObject : depth0)) != null ? helper : helpers.helperMissing),(options={"name":"dumpObject","hash":{},"fn":container.program(33, data, 0),"inverse":container.noop,"data":data}),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : (container.nullContext || {}),options) : helper));    if (!helpers.dumpObject) { stack1 = helpers.blockHelperMissing.call(depth0,stack1,options)}    if (stack1 != null) { buffer += stack1; }    return buffer + "</pre>\n"; -},"68":function(container,depth0,helpers,partials,data,blockParams,depths) { -    var stack1; - -  return ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.definitions : depth0),{"name":"each","hash":{},"fn":container.program(69, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : ""); -},"69":function(container,depth0,helpers,partials,data,blockParams,depths) { -    var stack1; +},"66":function(container,depth0,helpers,partials,data,blockParams,depths) { +    var stack1, alias1=depth0 != null ? depth0 : (container.nullContext || {}); -  return ((stack1 = helpers.unless.call(depth0 != null ? depth0 : (container.nullContext || {}),(data && data.first),{"name":"unless","hash":{},"fn":container.program(70, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "") +  return "<div class=\"term-navigation\">\n    <a href=\"#\" " +    + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.source : depth0),{"name":"if","hash":{},"fn":container.program(67, data, 0, blockParams, depths),"inverse":container.program(69, data, 0, blockParams, depths),"data":data})) != null ? stack1 : "") +    + "><img src=\"/mixed/img/source-term.svg\" title=\"Source term (Alt + B)\" alt></a>\n    <a href=\"#\" " +    + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.next : depth0),{"name":"if","hash":{},"fn":container.program(71, data, 0, blockParams, depths),"inverse":container.program(73, data, 0, blockParams, depths),"data":data})) != null ? stack1 : "") +    + "><img src=\"/mixed/img/source-term.svg\" style=\"transform: scaleX(-1);\" title=\"Next term (Alt + F)\" alt></a>\n</div>\n" +    + ((stack1 = helpers.each.call(alias1,(depth0 != null ? depth0.definitions : depth0),{"name":"each","hash":{},"fn":container.program(75, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : ""); +},"67":function(container,depth0,helpers,partials,data) { +    return "class=\"source-term\""; +},"69":function(container,depth0,helpers,partials,data) { +    return "class=\"source-term term-button-fade\""; +},"71":function(container,depth0,helpers,partials,data) { +    return "class=\"next-term\""; +},"73":function(container,depth0,helpers,partials,data) { +    return "class=\"next-term term-button-fade\""; +},"75":function(container,depth0,helpers,partials,data,blockParams,depths) { +    var stack1; + +  return ((stack1 = helpers.unless.call(depth0 != null ? depth0 : (container.nullContext || {}),(data && data.first),{"name":"unless","hash":{},"fn":container.program(76, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "")      + "\n" -    + ((stack1 = container.invokePartial(partials.term,depth0,{"name":"term","hash":{"source":(depths[1] != null ? depths[1].source : depths[1]),"compactGlossaries":(depths[1] != null ? depths[1].compactGlossaries : depths[1]),"playback":(depths[1] != null ? depths[1].playback : depths[1]),"addable":(depths[1] != null ? depths[1].addable : depths[1]),"merged":(depths[1] != null ? depths[1].merged : depths[1]),"grouped":(depths[1] != null ? depths[1].grouped : depths[1]),"debug":(depths[1] != null ? depths[1].debug : depths[1])},"data":data,"helpers":helpers,"partials":partials,"decorators":container.decorators})) != null ? stack1 : ""); -},"70":function(container,depth0,helpers,partials,data) { +    + ((stack1 = container.invokePartial(partials.term,depth0,{"name":"term","hash":{"compactGlossaries":(depths[1] != null ? depths[1].compactGlossaries : depths[1]),"playback":(depths[1] != null ? depths[1].playback : depths[1]),"addable":(depths[1] != null ? depths[1].addable : depths[1]),"merged":(depths[1] != null ? depths[1].merged : depths[1]),"grouped":(depths[1] != null ? depths[1].grouped : depths[1]),"debug":(depths[1] != null ? depths[1].debug : depths[1])},"data":data,"helpers":helpers,"partials":partials,"decorators":container.decorators})) != null ? stack1 : ""); +},"76":function(container,depth0,helpers,partials,data) {      return "<hr>"; -},"72":function(container,depth0,helpers,partials,data) { +},"78":function(container,depth0,helpers,partials,data) {      return "<p class=\"note\">No results found.</p>\n";  },"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data,blockParams,depths) {      var stack1;    return "\n\n" -    + ((stack1 = helpers["if"].call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.definitions : depth0),{"name":"if","hash":{},"fn":container.program(68, data, 0, blockParams, depths),"inverse":container.program(72, data, 0, blockParams, depths),"data":data})) != null ? stack1 : ""); +    + ((stack1 = helpers["if"].call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.definitions : depth0),{"name":"if","hash":{},"fn":container.program(66, data, 0, blockParams, depths),"inverse":container.program(78, data, 0, blockParams, depths),"data":data})) != null ? stack1 : "");  },"main_d":  function(fn, props, container, depth0, data, blockParams, depths) {    var decorators = container.decorators; diff --git a/ext/bg/js/translator.js b/ext/bg/js/translator.js index e27cbdff..202014c9 100644 --- a/ext/bg/js/translator.js +++ b/ext/bg/js/translator.js @@ -51,7 +51,7 @@ class Translator {          const definitionsBySequence = dictTermsMergeBySequence(definitions, mainDictionary);          const defaultDefinitions = definitionsBySequence['-1']; -        const sequenceList = Object.keys(definitionsBySequence).map(v => Number(v)).filter(v => v >= 0); +        const sequenceList = Object.keys(definitionsBySequence).map((v) => Number(v)).filter((v) => v >= 0);          const sequencedDefinitions = sequenceList.map((key) => ({              definitions: definitionsBySequence[key],              rawDefinitions: [] @@ -124,7 +124,7 @@ class Translator {          for (const expression of result.expressions.keys()) {              for (const reading of result.expressions.get(expression).keys()) {                  const termTags = result.expressions.get(expression).get(reading); -                const score = termTags.map(tag => tag.score).reduce((p, v) => p + v, 0); +                const score = termTags.map((tag) => tag.score).reduce((p, v) => p + v, 0);                  expressions.push({                      expression: expression,                      reading: reading, @@ -173,7 +173,7 @@ class Translator {      async findTermsMerged(text, details, options) {          const dictionaries = dictEnabledSet(options); -        const secondarySearchTitles = Object.keys(options.dictionaries).filter(dict => options.dictionaries[dict].allowSecondarySearches); +        const secondarySearchTitles = Object.keys(options.dictionaries).filter((dict) => options.dictionaries[dict].allowSecondarySearches);          const titles = Object.keys(dictionaries);          const [definitions, length] = await this.findTermsInternal(text, dictionaries, options.scanning.alphanumeric, details);          const {sequencedDefinitions, defaultDefinitions} = await this.getSequencedDefinitions(definitions, options.general.mainDictionary); @@ -297,7 +297,7 @@ class Translator {          for (const deinflection of deinflections) {              const term = deinflection.term;              let deinflectionArray; -            if (uniqueDeinflectionsMap.hasOwnProperty(term)) { +            if (hasOwn(uniqueDeinflectionsMap, term)) {                  deinflectionArray = uniqueDeinflectionsMap[term];              } else {                  deinflectionArray = []; @@ -320,7 +320,7 @@ class Translator {              }          } -        return deinflections.filter(e => e.definitions.length > 0); +        return deinflections.filter((e) => e.definitions.length > 0);      }      getDeinflections(text) { @@ -355,7 +355,7 @@ class Translator {          const kanjiUnique = {};          const kanjiList = [];          for (const c of text) { -            if (!kanjiUnique.hasOwnProperty(c)) { +            if (!hasOwn(kanjiUnique, c)) {                  kanjiList.push(c);                  kanjiUnique[c] = true;              } @@ -417,7 +417,7 @@ class Translator {              const expression = term.expression;              term.frequencies = []; -            if (termsUniqueMap.hasOwnProperty(expression)) { +            if (hasOwn(termsUniqueMap, expression)) {                  termsUniqueMap[expression].push(term);              } else {                  const termList = [term]; @@ -464,7 +464,7 @@ class Translator {              const category = meta.category;              const group = ( -                stats.hasOwnProperty(category) ? +                hasOwn(stats, category) ?                  stats[category] :                  (stats[category] = [])              ); @@ -484,7 +484,7 @@ class Translator {      async getTagMetaList(names, title) {          const tagMetaList = [];          const cache = ( -            this.tagCache.hasOwnProperty(title) ? +            hasOwn(this.tagCache, title) ?              this.tagCache[title] :              (this.tagCache[title] = {})          ); @@ -492,7 +492,7 @@ class Translator {          for (const name of names) {              const base = Translator.getNameBase(name); -            if (cache.hasOwnProperty(base)) { +            if (hasOwn(cache, base)) {                  tagMetaList.push(cache[base]);              } else {                  const tagMeta = await this.database.findTagForTitle(base, title); diff --git a/ext/bg/js/util.js b/ext/bg/js/util.js index a6126677..3dd5fd55 100644 --- a/ext/bg/js/util.js +++ b/ext/bg/js/util.js @@ -16,12 +16,6 @@   * along with this program.  If not, see <http://www.gnu.org/licenses/>.   */ -function utilAsync(func) { -    return function(...args) { -        func.apply(this, args); -    }; -} -  function utilIsolate(data) {      return JSON.parse(JSON.stringify(data));  } @@ -47,13 +41,13 @@ function utilSetEqual(setA, setB) {  function utilSetIntersection(setA, setB) {      return new Set( -        [...setA].filter(value => setB.has(value)) +        [...setA].filter((value) => setB.has(value))      );  }  function utilSetDifference(setA, setB) {      return new Set( -        [...setA].filter(value => !setB.has(value)) +        [...setA].filter((value) => !setB.has(value))      );  } @@ -117,7 +111,3 @@ function utilReadFile(file) {          reader.readAsBinaryString(file);      });  } - -function utilIsObject(value) { -    return typeof value === 'object' && value !== null && !Array.isArray(value); -} |