summaryrefslogtreecommitdiff
path: root/ext/bg
diff options
context:
space:
mode:
authorAlex Yatskov <alex@foosoft.net>2019-11-05 19:04:13 -0800
committerAlex Yatskov <alex@foosoft.net>2019-11-05 19:04:13 -0800
commit08ad2779678cd447bd747c2b155ef9b5135fdf5d (patch)
treefaa54cbf9176989f9bd3c3b90ff3e032189adb20 /ext/bg
parent438498435227cfa59cf9ed3430045b288cd2a7c0 (diff)
parent91c01e0a7eeeb851344a22ace8a5fa0b873a3e57 (diff)
Merge branch 'master' into testing
Diffstat (limited to 'ext/bg')
-rw-r--r--ext/bg/background.html9
-rw-r--r--ext/bg/context.html6
-rw-r--r--ext/bg/guide.html6
-rw-r--r--ext/bg/js/api.js58
-rw-r--r--ext/bg/js/backend.js8
-rw-r--r--ext/bg/js/context.js4
-rw-r--r--ext/bg/js/handlebars.js4
-rw-r--r--ext/bg/js/options.js4
-rw-r--r--ext/bg/js/search.js173
-rw-r--r--ext/bg/js/settings-popup-preview.js7
-rw-r--r--ext/bg/js/translator.js2
-rw-r--r--ext/bg/legal.html6
-rw-r--r--ext/bg/search.html23
-rw-r--r--ext/bg/settings-popup-preview.html6
-rw-r--r--ext/bg/settings.html6
15 files changed, 304 insertions, 18 deletions
diff --git a/ext/bg/background.html b/ext/bg/background.html
index 194d4a45..3ab68639 100644
--- a/ext/bg/background.html
+++ b/ext/bg/background.html
@@ -3,8 +3,17 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1" />
+ <title>Background</title>
+ <link rel="icon" type="image/png" href="/mixed/img/icon16.png" sizes="16x16">
+ <link rel="icon" type="image/png" href="/mixed/img/icon19.png" sizes="19x19">
+ <link rel="icon" type="image/png" href="/mixed/img/icon38.png" sizes="38x38">
+ <link rel="icon" type="image/png" href="/mixed/img/icon48.png" sizes="48x48">
+ <link rel="icon" type="image/png" href="/mixed/img/icon64.png" sizes="64x64">
+ <link rel="icon" type="image/png" href="/mixed/img/icon128.png" sizes="128x128">
</head>
<body>
+ <div id="clipboard-paste-target" contenteditable="true"></div>
+
<script src="/mixed/lib/dexie.min.js"></script>
<script src="/mixed/lib/handlebars.min.js"></script>
<script src="/mixed/lib/jszip.min.js"></script>
diff --git a/ext/bg/context.html b/ext/bg/context.html
index 48fa463f..7e08dddd 100644
--- a/ext/bg/context.html
+++ b/ext/bg/context.html
@@ -3,6 +3,12 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1" />
+ <link rel="icon" type="image/png" href="/mixed/img/icon16.png" sizes="16x16">
+ <link rel="icon" type="image/png" href="/mixed/img/icon19.png" sizes="19x19">
+ <link rel="icon" type="image/png" href="/mixed/img/icon38.png" sizes="38x38">
+ <link rel="icon" type="image/png" href="/mixed/img/icon48.png" sizes="48x48">
+ <link rel="icon" type="image/png" href="/mixed/img/icon64.png" sizes="64x64">
+ <link rel="icon" type="image/png" href="/mixed/img/icon128.png" sizes="128x128">
<link rel="stylesheet" type="text/css" href="/mixed/lib/bootstrap/css/bootstrap.min.css">
<link rel="stylesheet" type="text/css" href="/mixed/lib/bootstrap/css/bootstrap-theme.min.css">
<link rel="stylesheet" type="text/css" href="/mixed/lib/bootstrap-toggle/bootstrap-toggle.min.css">
diff --git a/ext/bg/guide.html b/ext/bg/guide.html
index 2a602f1f..ff9c71ee 100644
--- a/ext/bg/guide.html
+++ b/ext/bg/guide.html
@@ -4,6 +4,12 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Welcome to Yomichan!</title>
+ <link rel="icon" type="image/png" href="/mixed/img/icon16.png" sizes="16x16">
+ <link rel="icon" type="image/png" href="/mixed/img/icon19.png" sizes="19x19">
+ <link rel="icon" type="image/png" href="/mixed/img/icon38.png" sizes="38x38">
+ <link rel="icon" type="image/png" href="/mixed/img/icon48.png" sizes="48x48">
+ <link rel="icon" type="image/png" href="/mixed/img/icon64.png" sizes="64x64">
+ <link rel="icon" type="image/png" href="/mixed/img/icon128.png" sizes="128x128">
<link rel="stylesheet" type="text/css" href="/mixed/lib/bootstrap/css/bootstrap.min.css">
<link rel="stylesheet" type="text/css" href="/mixed/lib/bootstrap/css/bootstrap-theme.min.css">
</head>
diff --git a/ext/bg/js/api.js b/ext/bg/js/api.js
index 93d9c155..3209cc31 100644
--- a/ext/bg/js/api.js
+++ b/ext/bg/js/api.js
@@ -21,6 +21,52 @@ function apiOptionsGet(optionsContext) {
return utilBackend().getOptions(optionsContext);
}
+async function apiOptionsSet(changedOptions, optionsContext, source) {
+ const options = await apiOptionsGet(optionsContext);
+
+ function getValuePaths(obj) {
+ let valuePaths = [];
+ let nodes = [{
+ obj,
+ path: []
+ }];
+ while (nodes.length > 0) {
+ let node = nodes.pop();
+ Object.keys(node.obj).forEach((key) => {
+ let path = node.path.concat(key);
+ let value = node.obj[key];
+ if (typeof value === 'object') {
+ nodes.unshift({
+ obj: value,
+ path: path
+ });
+ } else {
+ valuePaths.push([value, path]);
+ }
+ });
+ }
+ return valuePaths;
+ }
+
+ function modifyOption(path, value, options) {
+ let pivot = options;
+ for (let pathKey of path.slice(0, -1)) {
+ if (!(pathKey in pivot)) {
+ return false;
+ }
+ pivot = pivot[pathKey];
+ }
+ pivot[path[path.length - 1]] = value;
+ return true;
+ }
+
+ for (let [value, path] of getValuePaths(changedOptions)) {
+ modifyOption(path, value, options);
+ }
+
+ await apiOptionsSave(source);
+}
+
function apiOptionsGetFull() {
return utilBackend().getFullOptions();
}
@@ -153,7 +199,7 @@ async function apiCommandExec(command, params) {
}
apiCommandExec.handlers = {
search: async (params) => {
- const url = chrome.extension.getURL('/bg/search.html');
+ const url = chrome.runtime.getURL('/bg/search.html');
if (!(params && params.newTab)) {
try {
const tab = await apiFindTab(1000, (url2) => (
@@ -181,7 +227,7 @@ apiCommandExec.handlers = {
chrome.runtime.openOptionsPage();
} else {
const manifest = chrome.runtime.getManifest();
- const url = chrome.extension.getURL(manifest.options_ui.page);
+ const url = chrome.runtime.getURL(manifest.options_ui.page);
chrome.tabs.create({url});
}
},
@@ -401,3 +447,11 @@ async function apiFocusTab(tab) {
// Edge throws exception for no reason here.
}
}
+
+async function apiClipboardGet() {
+ const clipboardPasteTarget = utilBackend().clipboardPasteTarget;
+ clipboardPasteTarget.innerText = '';
+ clipboardPasteTarget.focus();
+ document.execCommand('paste');
+ return clipboardPasteTarget.innerText;
+}
diff --git a/ext/bg/js/backend.js b/ext/bg/js/backend.js
index f29230a2..71393467 100644
--- a/ext/bg/js/backend.js
+++ b/ext/bg/js/backend.js
@@ -30,6 +30,8 @@ class Backend {
this.isPreparedResolve = null;
this.isPreparedPromise = new Promise((resolve) => (this.isPreparedResolve = resolve));
+ this.clipboardPasteTarget = document.querySelector('#clipboard-paste-target');
+
this.apiForwarder = new BackendApiForwarder();
}
@@ -45,7 +47,7 @@ class Backend {
const options = this.getOptionsSync(this.optionsContext);
if (options.general.showGuide) {
- chrome.tabs.create({url: chrome.extension.getURL('/bg/guide.html')});
+ chrome.tabs.create({url: chrome.runtime.getURL('/bg/guide.html')});
}
this.isPreparedResolve();
@@ -175,6 +177,7 @@ class Backend {
Backend.messageHandlers = {
optionsGet: ({optionsContext}) => apiOptionsGet(optionsContext),
+ optionsSet: ({changedOptions, optionsContext, source}) => apiOptionsSet(changedOptions, optionsContext, source),
kanjiFind: ({text, optionsContext}) => apiKanjiFind(text, optionsContext),
termsFind: ({text, optionsContext}) => apiTermsFind(text, optionsContext),
definitionAdd: ({definition, mode, context, optionsContext}) => apiDefinitionAdd(definition, mode, context, optionsContext),
@@ -187,7 +190,8 @@ Backend.messageHandlers = {
forward: ({action, params}, sender) => apiForward(action, params, sender),
frameInformationGet: (params, sender) => apiFrameInformationGet(sender),
injectStylesheet: ({css}, sender) => apiInjectStylesheet(css, sender),
- getEnvironmentInfo: () => apiGetEnvironmentInfo()
+ getEnvironmentInfo: () => apiGetEnvironmentInfo(),
+ clipboardGet: () => apiClipboardGet()
};
window.yomichan_backend = new Backend();
diff --git a/ext/bg/js/context.js b/ext/bg/js/context.js
index 8e1dbce6..3fb27f0d 100644
--- a/ext/bg/js/context.js
+++ b/ext/bg/js/context.js
@@ -55,8 +55,8 @@ $(document).ready(utilAsync(() => {
const manifest = chrome.runtime.getManifest();
- setupButtonEvents('.action-open-search', 'search', chrome.extension.getURL('/bg/search.html'));
- setupButtonEvents('.action-open-options', 'options', chrome.extension.getURL(manifest.options_ui.page));
+ setupButtonEvents('.action-open-search', 'search', chrome.runtime.getURL('/bg/search.html'));
+ setupButtonEvents('.action-open-options', 'options', chrome.runtime.getURL(manifest.options_ui.page));
setupButtonEvents('.action-open-help', 'help');
const optionsContext = {
diff --git a/ext/bg/js/handlebars.js b/ext/bg/js/handlebars.js
index 92764a20..fba437da 100644
--- a/ext/bg/js/handlebars.js
+++ b/ext/bg/js/handlebars.js
@@ -49,13 +49,13 @@ function handlebarsFuriganaPlain(options) {
let result = '';
for (const seg of segs) {
if (seg.furigana) {
- result += `${seg.text}[${seg.furigana}]`;
+ result += ` ${seg.text}[${seg.furigana}]`;
} else {
result += seg.text;
}
}
- return result;
+ return result.trimLeft();
}
function handlebarsKanjiLinks(options) {
diff --git a/ext/bg/js/options.js b/ext/bg/js/options.js
index 4854cd65..be1ccfbb 100644
--- a/ext/bg/js/options.js
+++ b/ext/bg/js/options.js
@@ -279,7 +279,9 @@ function profileOptionsCreateDefaults() {
popupTheme: 'default',
popupOuterTheme: 'default',
customPopupCss: '',
- customPopupOuterCss: ''
+ customPopupOuterCss: '',
+ enableWanakana: true,
+ enableClipboardMonitor: false
},
audio: {
diff --git a/ext/bg/js/search.js b/ext/bg/js/search.js
index 431478c9..dbfcb15d 100644
--- a/ext/bg/js/search.js
+++ b/ext/bg/js/search.js
@@ -17,6 +17,12 @@
*/
+let IS_FIREFOX = null;
+(async () => {
+ const {browser} = await apiGetEnvironmentInfo();
+ IS_FIREFOX = ['firefox', 'firefox-mobile'].includes(browser);
+})();
+
class DisplaySearch extends Display {
constructor() {
super(document.querySelector('#spinner'), document.querySelector('#content'));
@@ -29,8 +35,14 @@ class DisplaySearch extends Display {
this.search = document.querySelector('#search');
this.query = document.querySelector('#query');
this.intro = document.querySelector('#intro');
+ this.clipboardMonitorEnable = document.querySelector('#clipboard-monitor-enable');
+ this.wanakanaEnable = document.querySelector('#wanakana-enable');
+
this.introVisible = true;
this.introAnimationTimer = null;
+
+ this.clipboardMonitorIntervalId = null;
+ this.clipboardPrevText = null;
}
static create() {
@@ -49,16 +61,69 @@ class DisplaySearch extends Display {
if (this.query !== null) {
this.query.addEventListener('input', () => this.onSearchInput(), false);
+ if (this.wanakanaEnable !== null) {
+ if (this.options.general.enableWanakana === true) {
+ this.wanakanaEnable.checked = true;
+ window.wanakana.bind(this.query);
+ } else {
+ this.wanakanaEnable.checked = false;
+ }
+ this.wanakanaEnable.addEventListener('change', (e) => {
+ const query = DisplaySearch.getSearchQueryFromLocation(window.location.href) || '';
+ if (e.target.checked) {
+ window.wanakana.bind(this.query);
+ this.query.value = window.wanakana.toKana(query);
+ apiOptionsSet({general: {enableWanakana: true}}, this.getOptionsContext());
+ } else {
+ window.wanakana.unbind(this.query);
+ this.query.value = query;
+ apiOptionsSet({general: {enableWanakana: false}}, this.getOptionsContext());
+ }
+ this.onSearchQueryUpdated(this.query.value, false);
+ });
+ }
+
const query = DisplaySearch.getSearchQueryFromLocation(window.location.href);
if (query !== null) {
- this.query.value = window.wanakana.toKana(query);
- this.onSearchQueryUpdated(query, false);
+ if (this.isWanakanaEnabled()) {
+ this.query.value = window.wanakana.toKana(query);
+ } else {
+ this.query.value = query;
+ }
+ this.onSearchQueryUpdated(this.query.value, false);
}
-
- window.wanakana.bind(this.query);
+ }
+ if (this.clipboardMonitorEnable !== null) {
+ if (this.options.general.enableClipboardMonitor === true) {
+ this.clipboardMonitorEnable.checked = true;
+ this.startClipboardMonitor();
+ } else {
+ this.clipboardMonitorEnable.checked = false;
+ }
+ this.clipboardMonitorEnable.addEventListener('change', (e) => {
+ if (e.target.checked) {
+ chrome.permissions.request(
+ {permissions: ['clipboardRead']},
+ (granted) => {
+ if (granted) {
+ this.startClipboardMonitor();
+ apiOptionsSet({general: {enableClipboardMonitor: true}}, this.getOptionsContext());
+ } else {
+ e.target.checked = false;
+ }
+ }
+ );
+ } else {
+ this.stopClipboardMonitor();
+ apiOptionsSet({general: {enableClipboardMonitor: false}}, this.getOptionsContext());
+ }
+ });
}
+ window.addEventListener('popstate', (e) => this.onPopState(e));
+
this.updateSearchButton();
+ this.initClipboardMonitor();
} catch (e) {
this.onError(e);
}
@@ -79,6 +144,11 @@ class DisplaySearch extends Display {
onSearchInput() {
this.updateSearchButton();
+
+ const queryElementRect = this.query.getBoundingClientRect();
+ if (queryElementRect.top < 0 || queryElementRect.bottom > window.innerHeight) {
+ this.query.scrollIntoView();
+ }
}
onSearch(e) {
@@ -90,10 +160,60 @@ class DisplaySearch extends Display {
const query = this.query.value;
const queryString = query.length > 0 ? `?query=${encodeURIComponent(query)}` : '';
- window.history.replaceState(null, '', `${window.location.pathname}${queryString}`);
+ window.history.pushState(null, '', `${window.location.pathname}${queryString}`);
this.onSearchQueryUpdated(query, true);
}
+ onPopState(e) {
+ const query = DisplaySearch.getSearchQueryFromLocation(window.location.href) || '';
+ if (this.query !== null) {
+ if (this.isWanakanaEnabled()) {
+ this.query.value = window.wanakana.toKana(query);
+ } else {
+ this.query.value = query;
+ }
+ }
+
+ this.onSearchQueryUpdated(this.query.value, false);
+ }
+
+ onKeyDown(e) {
+ const key = Display.getKeyFromEvent(e);
+
+ let activeModifierMap = {
+ 'Control': e.ctrlKey,
+ 'Meta': e.metaKey,
+ 'ANY_MOD': true
+ };
+
+ const ignoreKeys = {
+ 'ANY_MOD': ['Tab', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'PageDown', 'PageUp', 'Home', 'End']
+ .concat(
+ Array.from(Array(24).keys())
+ .map(i => `F${i + 1}`)
+ ),
+ 'Control': ['C', 'A', 'Z', 'Y', 'X', 'F', 'G'],
+ 'Meta': ['C', 'A', 'Z', 'Y', 'X', 'F', 'G'],
+ 'OS': [],
+ 'Alt': [],
+ 'AltGraph': [],
+ 'Shift': []
+ }
+
+ let preventFocus = false;
+ for (const [modifier, keys] of Object.entries(ignoreKeys)) {
+ const modifierActive = activeModifierMap[modifier];
+ if (key === modifier || (modifierActive && keys.includes(key))) {
+ preventFocus = true;
+ break;
+ }
+ }
+
+ if (!super.onKeyDown(e) && !preventFocus && document.activeElement !== this.query) {
+ this.query.focus({preventScroll: true});
+ }
+ }
+
async onSearchQueryUpdated(query, animate) {
try {
const valid = (query.length > 0);
@@ -125,6 +245,49 @@ class DisplaySearch extends Display {
}
}
+ initClipboardMonitor() {
+ // ignore copy from search page
+ window.addEventListener('copy', (e) => {
+ this.clipboardPrevText = document.getSelection().toString().trim();
+ });
+ }
+
+ startClipboardMonitor() {
+ this.clipboardMonitorIntervalId = setInterval(async () => {
+ let curText = null;
+ // TODO get rid of this and figure out why apiClipboardGet doesn't work on Firefox
+ if (IS_FIREFOX) {
+ curText = (await navigator.clipboard.readText()).trim();
+ } else if (IS_FIREFOX === false) {
+ curText = (await apiClipboardGet()).trim();
+ }
+ if (curText && (curText !== this.clipboardPrevText)) {
+ if (this.isWanakanaEnabled()) {
+ this.query.value = window.wanakana.toKana(curText);
+ } else {
+ this.query.value = curText;
+ }
+
+ const queryString = curText.length > 0 ? `?query=${encodeURIComponent(curText)}` : '';
+ window.history.pushState(null, '', `${window.location.pathname}${queryString}`);
+ this.onSearchQueryUpdated(this.query.value, true);
+
+ this.clipboardPrevText = curText;
+ }
+ }, 100);
+ }
+
+ stopClipboardMonitor() {
+ if (this.clipboardMonitorIntervalId) {
+ clearInterval(this.clipboardMonitorIntervalId);
+ this.clipboardMonitorIntervalId = null;
+ }
+ }
+
+ isWanakanaEnabled() {
+ return this.wanakanaEnable !== null && this.wanakanaEnable.checked;
+ }
+
getOptionsContext() {
return this.optionsContext;
}
diff --git a/ext/bg/js/settings-popup-preview.js b/ext/bg/js/settings-popup-preview.js
index b12fb726..7d641c46 100644
--- a/ext/bg/js/settings-popup-preview.js
+++ b/ext/bg/js/settings-popup-preview.js
@@ -159,8 +159,11 @@ class SettingsPopupPreview {
range.selectNode(textNode);
const source = new TextSourceRange(range, range.toString(), null);
- this.frontend.textSourceLast = null;
- await this.frontend.searchSource(source, 'script');
+ try {
+ await this.frontend.searchSource(source, 'script');
+ } finally {
+ source.cleanup();
+ }
await this.frontend.lastShowPromise;
if (this.frontend.popup.isVisible()) {
diff --git a/ext/bg/js/translator.js b/ext/bg/js/translator.js
index ee012d96..9d90136b 100644
--- a/ext/bg/js/translator.js
+++ b/ext/bg/js/translator.js
@@ -31,7 +31,7 @@ class Translator {
}
if (!this.deinflector) {
- const url = chrome.extension.getURL('/bg/lang/deinflect.json');
+ const url = chrome.runtime.getURL('/bg/lang/deinflect.json');
const reasons = await requestJson(url, 'GET');
this.deinflector = new Deinflector(reasons);
}
diff --git a/ext/bg/legal.html b/ext/bg/legal.html
index 26ac033d..30927da6 100644
--- a/ext/bg/legal.html
+++ b/ext/bg/legal.html
@@ -4,6 +4,12 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Yomichan Legal</title>
+ <link rel="icon" type="image/png" href="/mixed/img/icon16.png" sizes="16x16">
+ <link rel="icon" type="image/png" href="/mixed/img/icon19.png" sizes="19x19">
+ <link rel="icon" type="image/png" href="/mixed/img/icon38.png" sizes="38x38">
+ <link rel="icon" type="image/png" href="/mixed/img/icon48.png" sizes="48x48">
+ <link rel="icon" type="image/png" href="/mixed/img/icon64.png" sizes="64x64">
+ <link rel="icon" type="image/png" href="/mixed/img/icon128.png" sizes="128x128">
<link rel="stylesheet" type="text/css" href="/mixed/lib/bootstrap/css/bootstrap.min.css">
<link rel="stylesheet" type="text/css" href="/mixed/lib/bootstrap/css/bootstrap-theme.min.css">
</head>
diff --git a/ext/bg/search.html b/ext/bg/search.html
index 9d28b358..91140b95 100644
--- a/ext/bg/search.html
+++ b/ext/bg/search.html
@@ -4,6 +4,12 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Yomichan Search</title>
+ <link rel="icon" type="image/png" href="/mixed/img/icon16.png" sizes="16x16">
+ <link rel="icon" type="image/png" href="/mixed/img/icon19.png" sizes="19x19">
+ <link rel="icon" type="image/png" href="/mixed/img/icon38.png" sizes="38x38">
+ <link rel="icon" type="image/png" href="/mixed/img/icon48.png" sizes="48x48">
+ <link rel="icon" type="image/png" href="/mixed/img/icon64.png" sizes="64x64">
+ <link rel="icon" type="image/png" href="/mixed/img/icon128.png" sizes="128x128">
<link rel="stylesheet" type="text/css" href="/mixed/lib/bootstrap/css/bootstrap.min.css">
<link rel="stylesheet" type="text/css" href="/mixed/lib/bootstrap/css/bootstrap-theme.min.css">
<link rel="stylesheet" type="text/css" href="/mixed/css/display.css">
@@ -19,7 +25,22 @@
<p style="margin-bottom: 0;">Search your installed dictionaries by entering a Japanese expression into the field below.</p>
</div>
- <form class="input-group" style="padding-top: 10px;">
+ <div class="input-group" style="padding-top: 10px; font-size: 20px; user-select: none;">
+ <span title="Enable kana input method" class="input-group-text">
+ <label>
+ あ
+ <input type="checkbox" id="wanakana-enable" />
+ </label>
+ </span>
+ <span title="Enable clipboard monitor" class="input-group-text">
+ <label>
+ <span class="glyphicon glyphicon-paste"></span>
+ <input type="checkbox" id="clipboard-monitor-enable" />
+ </label>
+ </span>
+ </div>
+
+ <form class="input-group">
<input type="text" class="form-control" placeholder="Search for..." id="query" autofocus>
<span class="input-group-btn">
<input type="submit" class="btn btn-default form-control" id="search" value="Search">
diff --git a/ext/bg/settings-popup-preview.html b/ext/bg/settings-popup-preview.html
index 07caa271..d27a9a33 100644
--- a/ext/bg/settings-popup-preview.html
+++ b/ext/bg/settings-popup-preview.html
@@ -4,6 +4,12 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Yomichan Popup Preview</title>
+ <link rel="icon" type="image/png" href="/mixed/img/icon16.png" sizes="16x16">
+ <link rel="icon" type="image/png" href="/mixed/img/icon19.png" sizes="19x19">
+ <link rel="icon" type="image/png" href="/mixed/img/icon38.png" sizes="38x38">
+ <link rel="icon" type="image/png" href="/mixed/img/icon48.png" sizes="48x48">
+ <link rel="icon" type="image/png" href="/mixed/img/icon64.png" sizes="64x64">
+ <link rel="icon" type="image/png" href="/mixed/img/icon128.png" sizes="128x128">
<link rel="stylesheet" type="text/css" href="/fg/css/client.css" id="client-css">
<style>
html {
diff --git a/ext/bg/settings.html b/ext/bg/settings.html
index 9b1c4366..a3b75576 100644
--- a/ext/bg/settings.html
+++ b/ext/bg/settings.html
@@ -4,6 +4,12 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Yomichan Options</title>
+ <link rel="icon" type="image/png" href="/mixed/img/icon16.png" sizes="16x16">
+ <link rel="icon" type="image/png" href="/mixed/img/icon19.png" sizes="19x19">
+ <link rel="icon" type="image/png" href="/mixed/img/icon38.png" sizes="38x38">
+ <link rel="icon" type="image/png" href="/mixed/img/icon48.png" sizes="48x48">
+ <link rel="icon" type="image/png" href="/mixed/img/icon64.png" sizes="64x64">
+ <link rel="icon" type="image/png" href="/mixed/img/icon128.png" sizes="128x128">
<link rel="stylesheet" type="text/css" href="/mixed/lib/bootstrap/css/bootstrap.min.css">
<link rel="stylesheet" type="text/css" href="/mixed/lib/bootstrap/css/bootstrap-theme.min.css">
<link rel="stylesheet" type="text/css" href="/bg/css/settings.css">