diff options
Diffstat (limited to 'ext/bg/js')
-rw-r--r-- | ext/bg/js/audio.js | 29 | ||||
-rw-r--r-- | ext/bg/js/backend.js | 26 | ||||
-rw-r--r-- | ext/bg/js/database.js | 68 | ||||
-rw-r--r-- | ext/bg/js/options.js | 2 | ||||
-rw-r--r-- | ext/bg/js/settings.js | 201 | ||||
-rw-r--r-- | ext/bg/js/util.js | 4 |
6 files changed, 261 insertions, 69 deletions
diff --git a/ext/bg/js/audio.js b/ext/bg/js/audio.js index 549288f5..2e5db7cc 100644 --- a/ext/bg/js/audio.js +++ b/ext/bg/js/audio.js @@ -69,10 +69,10 @@ async function audioBuildUrl(definition, mode, cache={}) { const dom = new DOMParser().parseFromString(response, 'text/html'); for (const row of dom.getElementsByClassName('dc-result-row')) { try { - const url = row.getElementsByClassName('ill-onebuttonplayer').item(0).getAttribute('data-url'); + const url = row.querySelector('audio>source[src]').getAttribute('src'); const reading = row.getElementsByClassName('dc-vocab_kana').item(0).innerText; if (url && reading && (!definition.reading || definition.reading === reading)) { - return url; + return audioUrlNormalize(url, 'https://www.japanesepod101.com', '/learningcenter/reference/'); } } catch (e) { // NOP @@ -86,7 +86,7 @@ async function audioBuildUrl(definition, mode, cache={}) { resolve(response); } else { const xhr = new XMLHttpRequest(); - xhr.open('GET', `http://jisho.org/search/${definition.expression}`); + xhr.open('GET', `https://jisho.org/search/${definition.expression}`); xhr.addEventListener('error', () => reject('Failed to scrape audio data')); xhr.addEventListener('load', () => { cache[definition.expression] = xhr.responseText; @@ -100,7 +100,10 @@ async function audioBuildUrl(definition, mode, cache={}) { const dom = new DOMParser().parseFromString(response, 'text/html'); const audio = dom.getElementById(`audio_${definition.expression}:${definition.reading}`); if (audio) { - return audio.getElementsByTagName('source').item(0).getAttribute('src'); + const url = audio.getElementsByTagName('source').item(0).getAttribute('src'); + if (url) { + return audioUrlNormalize(url, 'https://jisho.org', '/search/'); + } } } catch (e) { // NOP @@ -112,6 +115,24 @@ async function audioBuildUrl(definition, mode, cache={}) { } } +function audioUrlNormalize(url, baseUrl, basePath) { + if (url) { + if (url[0] === '/') { + if (url.length >= 2 && url[1] === '/') { + // Begins with "//" + url = baseUrl.substr(0, baseUrl.indexOf(':') + 1) + url; + } else { + // Begins with "/" + url = baseUrl + url; + } + } else if (!/^[a-z][a-z0-9\+\-\.]*:/i.test(url)) { + // No URI scheme => relative path + url = baseUrl + basePath + url; + } + } + return url; +} + function audioBuildFilename(definition) { if (definition.reading || definition.expression) { let filename = 'yomichan'; diff --git a/ext/bg/js/backend.js b/ext/bg/js/backend.js index 01340419..c191a150 100644 --- a/ext/bg/js/backend.js +++ b/ext/bg/js/backend.js @@ -28,7 +28,9 @@ class Backend { await this.translator.prepare(); await apiOptionsSet(await optionsLoad()); - chrome.commands.onCommand.addListener(this.onCommand.bind(this)); + if (chrome.commands !== null && typeof chrome.commands === 'object') { + chrome.commands.onCommand.addListener(this.onCommand.bind(this)); + } chrome.runtime.onMessage.addListener(this.onMessage.bind(this)); if (this.options.general.showGuide) { @@ -40,13 +42,13 @@ class Backend { this.options = utilIsolate(options); if (!options.general.enable) { - chrome.browserAction.setBadgeBackgroundColor({color: '#555555'}); - chrome.browserAction.setBadgeText({text: 'off'}); + this.setExtensionBadgeBackgroundColor('#555555'); + this.setExtensionBadgeText('off'); } else if (!dictConfigured(options)) { - chrome.browserAction.setBadgeBackgroundColor({color: '#f0ad4e'}); - chrome.browserAction.setBadgeText({text: '!'}); + this.setExtensionBadgeBackgroundColor('#f0ad4e'); + this.setExtensionBadgeText('!'); } else { - chrome.browserAction.setBadgeText({text: ''}); + this.setExtensionBadgeText(''); } if (options.anki.enable) { @@ -125,6 +127,18 @@ class Backend { return true; } + + setExtensionBadgeBackgroundColor(color) { + if (typeof chrome.browserAction.setBadgeBackgroundColor === 'function') { + chrome.browserAction.setBadgeBackgroundColor({color}); + } + } + + setExtensionBadgeText(text) { + if (typeof chrome.browserAction.setBadgeText === 'function') { + chrome.browserAction.setBadgeText({text}); + } + } } window.yomichan_backend = new Backend(); diff --git a/ext/bg/js/database.js b/ext/bg/js/database.js index 3c7f6aab..093ec102 100644 --- a/ext/bg/js/database.js +++ b/ext/bg/js/database.js @@ -228,11 +228,47 @@ class Database { } } - async importDictionary(archive, callback) { + async importDictionary(archive, progressCallback, exceptions) { if (!this.db) { throw 'Database not initialized'; } + const maxTransactionLength = 1000; + const bulkAdd = async (table, items, total, current) => { + if (items.length < maxTransactionLength) { + if (progressCallback) { + progressCallback(total, current); + } + + try { + await table.bulkAdd(items); + } catch (e) { + if (exceptions) { + exceptions.push(e); + } else { + throw e; + } + } + } else { + for (let i = 0; i < items.length; i += maxTransactionLength) { + if (progressCallback) { + progressCallback(total, current + i / items.length); + } + + let count = Math.min(maxTransactionLength, items.length - i); + try { + await table.bulkAdd(items.slice(i, i + count)); + } catch (e) { + if (exceptions) { + exceptions.push(e); + } else { + throw e; + } + } + } + } + }; + const indexDataLoaded = async summary => { if (summary.version > 3) { throw 'Unsupported dictionary version'; @@ -247,10 +283,6 @@ class Database { }; const termDataLoaded = async (summary, entries, total, current) => { - if (callback) { - callback(total, current); - } - const rows = []; if (summary.version === 1) { for (const [expression, reading, definitionTags, rules, score, ...glossary] of entries) { @@ -280,14 +312,10 @@ class Database { } } - await this.db.terms.bulkAdd(rows); + await bulkAdd(this.db.terms, rows, total, current); }; const termMetaDataLoaded = async (summary, entries, total, current) => { - if (callback) { - callback(total, current); - } - const rows = []; for (const [expression, mode, data] of entries) { rows.push({ @@ -298,14 +326,10 @@ class Database { }); } - await this.db.termMeta.bulkAdd(rows); + await bulkAdd(this.db.termMeta, rows, total, current); }; const kanjiDataLoaded = async (summary, entries, total, current) => { - if (callback) { - callback(total, current); - } - const rows = []; if (summary.version === 1) { for (const [character, onyomi, kunyomi, tags, ...meanings] of entries) { @@ -332,14 +356,10 @@ class Database { } } - await this.db.kanji.bulkAdd(rows); + await bulkAdd(this.db.kanji, rows, total, current); }; const kanjiMetaDataLoaded = async (summary, entries, total, current) => { - if (callback) { - callback(total, current); - } - const rows = []; for (const [character, mode, data] of entries) { rows.push({ @@ -350,14 +370,10 @@ class Database { }); } - await this.db.kanjiMeta.bulkAdd(rows); + await bulkAdd(this.db.kanjiMeta, rows, total, current); }; const tagDataLoaded = async (summary, entries, total, current) => { - if (callback) { - callback(total, current); - } - const rows = []; for (const [name, category, order, notes, score] of entries) { const row = dictTagSanitize({ @@ -372,7 +388,7 @@ class Database { rows.push(row); } - await this.db.tagMeta.bulkAdd(rows); + await bulkAdd(this.db.tagMeta, rows, total, current); }; return await Database.importDictionaryZip( diff --git a/ext/bg/js/options.js b/ext/bg/js/options.js index b5e1a27d..bad56db6 100644 --- a/ext/bg/js/options.js +++ b/ext/bg/js/options.js @@ -190,6 +190,7 @@ function optionsSetDefaults(options) { debugInfo: false, maxResults: 32, showAdvanced: false, + popupDisplayMode: 'default', popupWidth: 400, popupHeight: 250, popupHorizontalOffset: 0, @@ -202,6 +203,7 @@ function optionsSetDefaults(options) { scanning: { middleMouse: true, + touchInputEnabled: true, selectText: true, alphanumeric: true, autoHideResults: false, diff --git a/ext/bg/js/settings.js b/ext/bg/js/settings.js index b5c733e2..60a1886b 100644 --- a/ext/bg/js/settings.js +++ b/ext/bg/js/settings.js @@ -31,12 +31,14 @@ async function formRead() { optionsNew.general.debugInfo = $('#show-debug-info').prop('checked'); optionsNew.general.showAdvanced = $('#show-advanced-options').prop('checked'); optionsNew.general.maxResults = parseInt($('#max-displayed-results').val(), 10); + optionsNew.general.popupDisplayMode = $('#popup-display-mode').val(); optionsNew.general.popupWidth = parseInt($('#popup-width').val(), 10); optionsNew.general.popupHeight = parseInt($('#popup-height').val(), 10); optionsNew.general.popupHorizontalOffset = parseInt($('#popup-horizontal-offset').val(), 0); optionsNew.general.popupVerticalOffset = parseInt($('#popup-vertical-offset').val(), 10); optionsNew.scanning.middleMouse = $('#middle-mouse-button-scan').prop('checked'); + optionsNew.scanning.touchInputEnabled = $('#touch-input-enabled').prop('checked'); optionsNew.scanning.selectText = $('#select-matched-text').prop('checked'); optionsNew.scanning.alphanumeric = $('#search-alphanumeric').prop('checked'); optionsNew.scanning.autoHideResults = $('#auto-hide-results').prop('checked'); @@ -162,12 +164,14 @@ async function onReady() { $('#show-debug-info').prop('checked', options.general.debugInfo); $('#show-advanced-options').prop('checked', options.general.showAdvanced); $('#max-displayed-results').val(options.general.maxResults); + $('#popup-display-mode').val(options.general.popupDisplayMode); $('#popup-width').val(options.general.popupWidth); $('#popup-height').val(options.general.popupHeight); $('#popup-horizontal-offset').val(options.general.popupHorizontalOffset); $('#popup-vertical-offset').val(options.general.popupVerticalOffset); $('#middle-mouse-button-scan').prop('checked', options.scanning.middleMouse); + $('#touch-input-enabled').prop('checked', options.scanning.touchInputEnabled); $('#select-matched-text').prop('checked', options.scanning.selectText); $('#search-alphanumeric').prop('checked', options.scanning.alphanumeric); $('#auto-hide-results').prop('checked', options.scanning.autoHideResults); @@ -191,7 +195,7 @@ async function onReady() { await dictionaryGroupsPopulate(options); await formMainDictionaryOptionsPopulate(options); } catch (e) { - dictionaryErrorShow(e); + dictionaryErrorsShow([e]); } try { @@ -201,6 +205,8 @@ async function onReady() { } formUpdateVisibility(options); + + storageInfoInitialize(); } $(document).ready(utilAsync(onReady)); @@ -210,36 +216,63 @@ $(document).ready(utilAsync(onReady)); * Dictionary */ -function dictionaryErrorShow(error) { +function dictionaryErrorToString(error) { + if (error.toString) { + error = error.toString(); + } else { + error = `${error}`; + } + + for (const [match, subst] of dictionaryErrorToString.overrides) { + if (error.includes(match)) { + error = subst; + break; + } + } + + return error; +} +dictionaryErrorToString.overrides = [ + [ + 'A mutation operation was attempted on a database that did not allow mutations.', + 'Access to IndexedDB appears to be restricted. Firefox seems to require that the history preference is set to "Remember history" before IndexedDB use of any kind is allowed.' + ], + [ + 'The operation failed for reasons unrelated to the database itself and not covered by any other error code.', + 'Unable to access IndexedDB due to a possibly corrupt user profile. Try using the "Refresh Firefox" feature to reset your user profile.' + ], + [ + 'BulkError', + 'Unable to finish importing dictionary data into IndexedDB. This may indicate that you do not have sufficient disk space available to complete this operation.' + ] +]; + +function dictionaryErrorsShow(errors) { const dialog = $('#dict-error'); - if (error) { - const overrides = [ - [ - 'A mutation operation was attempted on a database that did not allow mutations.', - 'Access to IndexedDB appears to be restricted. Firefox seems to require that the history preference is set to "Remember history" before IndexedDB use of any kind is allowed.' - ], - [ - 'The operation failed for reasons unrelated to the database itself and not covered by any other error code.', - 'Unable to access IndexedDB due to a possibly corrupt user profile. Try using the "Refresh Firefox" feature to reset your user profile.' - ], - [ - 'BulkError', - 'Unable to finish importing dictionary data into IndexedDB. This may indicate that you do not have sufficient disk space available to complete this operation.' - ] - ]; - - if (error.toString) { - error = error.toString(); + dialog.show().text(''); + + if (errors !== null && errors.length > 0) { + const uniqueErrors = {}; + for (let e of errors) { + e = dictionaryErrorToString(e); + uniqueErrors[e] = uniqueErrors.hasOwnProperty(e) ? uniqueErrors[e] + 1 : 1; } - for (const [match, subst] of overrides) { - if (error.includes(match)) { - error = subst; - break; + for (const e in uniqueErrors) { + const count = uniqueErrors[e]; + const div = document.createElement('p'); + if (count > 1) { + div.textContent = `${e} `; + const em = document.createElement('em'); + em.textContent = `(${count})`; + div.appendChild(em); + } else { + div.textContent = `${e}`; } + dialog.append($(div)); } - dialog.show().text(error); + dialog.show(); } else { dialog.hide(); } @@ -315,7 +348,7 @@ async function onDictionaryPurge(e) { const dictProgress = $('#dict-purge').show(); try { - dictionaryErrorShow(); + dictionaryErrorsShow(null); dictionarySpinnerShow(true); await utilDatabasePurge(); @@ -327,12 +360,16 @@ async function onDictionaryPurge(e) { await dictionaryGroupsPopulate(options); await formMainDictionaryOptionsPopulate(options); } catch (e) { - dictionaryErrorShow(e); + dictionaryErrorsShow([e]); } finally { dictionarySpinnerShow(false); dictControls.show(); dictProgress.hide(); + + if (storageEstimate.mostRecent !== null) { + storageUpdateStats(); + } } } @@ -342,25 +379,37 @@ async function onDictionaryImport(e) { const dictProgress = $('#dict-import-progress').show(); try { - dictionaryErrorShow(); + dictionaryErrorsShow(null); dictionarySpinnerShow(true); const setProgress = percent => dictProgress.find('.progress-bar').css('width', `${percent}%`); - const updateProgress = (total, current) => setProgress(current / total * 100.0); + const updateProgress = (total, current) => { + setProgress(current / total * 100.0); + if (storageEstimate.mostRecent !== null && !storageUpdateStats.isUpdating) { + storageUpdateStats(); + } + }; setProgress(0.0); + const exceptions = []; const options = await optionsLoad(); - const summary = await utilDatabaseImport(e.target.files[0], updateProgress); + const summary = await utilDatabaseImport(e.target.files[0], updateProgress, exceptions); options.dictionaries[summary.title] = {enabled: true, priority: 0, allowSecondarySearches: false}; if (summary.sequenced && options.general.mainDictionary === '') { options.general.mainDictionary = summary.title; } + + if (exceptions.length > 0) { + exceptions.push(`Dictionary may not have been imported properly: ${exceptions.length} error${exceptions.length === 1 ? '' : 's'} reported.`); + dictionaryErrorsShow(exceptions); + } + await optionsSave(options); await dictionaryGroupsPopulate(options); await formMainDictionaryOptionsPopulate(options); } catch (e) { - dictionaryErrorShow(e); + dictionaryErrorsShow([e]); } finally { dictionarySpinnerShow(false); @@ -518,3 +567,93 @@ async function onAnkiFieldTemplatesReset(e) { ankiErrorShow(e); } } + + +/* + * Storage + */ + +async function getBrowser() { + if (typeof chrome !== "undefined") { + if (typeof browser !== "undefined") { + try { + const info = await browser.runtime.getBrowserInfo(); + if (info.name === "Fennec") { + return "firefox-mobile"; + } + } catch (e) { } + return "firefox"; + } else { + return "chrome"; + } + } else { + return "edge"; + } +} + +function storageBytesToLabeledString(size) { + const base = 1000; + const labels = ["bytes", "KB", "MB", "GB"]; + let labelIndex = 0; + while (size >= base) { + size /= base; + ++labelIndex; + } + const label = size.toFixed(1); + return `${label}${labels[labelIndex]}`; +} + +async function storageEstimate() { + try { + return (storageEstimate.mostRecent = await navigator.storage.estimate()); + } catch (e) { } + return null; +} +storageEstimate.mostRecent = null; + +async function storageInfoInitialize() { + const browser = await getBrowser(); + const container = document.querySelector("#storage-info"); + container.setAttribute("data-browser", browser); + + await storageShowInfo(); + + container.classList.remove("storage-hidden"); + + document.querySelector("#storage-refresh").addEventListener('click', () => storageShowInfo(), false); +} + +async function storageUpdateStats() { + storageUpdateStats.isUpdating = true; + + const estimate = await storageEstimate(); + const valid = (estimate !== null); + + if (valid) { + document.querySelector("#storage-usage").textContent = storageBytesToLabeledString(estimate.usage); + document.querySelector("#storage-quota").textContent = storageBytesToLabeledString(estimate.quota); + } + + storageUpdateStats.isUpdating = false; + return valid; +} +storageUpdateStats.isUpdating = false; + +async function storageShowInfo() { + storageSpinnerShow(true); + + const valid = await storageUpdateStats(); + document.querySelector("#storage-use").classList.toggle("storage-hidden", !valid); + document.querySelector("#storage-error").classList.toggle("storage-hidden", valid); + + storageSpinnerShow(false); +} + +function storageSpinnerShow(show) { + const spinner = $('#storage-spinner'); + if (show) { + spinner.show(); + } else { + spinner.hide(); + } +} diff --git a/ext/bg/js/util.js b/ext/bg/js/util.js index 216cef3f..34b06ddb 100644 --- a/ext/bg/js/util.js +++ b/ext/bg/js/util.js @@ -87,6 +87,6 @@ function utilDatabasePurge() { return utilBackend().translator.database.purge(); } -function utilDatabaseImport(data, progress) { - return utilBackend().translator.database.importDictionary(data, progress); +function utilDatabaseImport(data, progress, exceptions) { + return utilBackend().translator.database.importDictionary(data, progress, exceptions); } |