diff options
author | Alex Yatskov <alex@foosoft.net> | 2019-11-05 19:04:13 -0800 |
---|---|---|
committer | Alex Yatskov <alex@foosoft.net> | 2019-11-05 19:04:13 -0800 |
commit | 08ad2779678cd447bd747c2b155ef9b5135fdf5d (patch) | |
tree | faa54cbf9176989f9bd3c3b90ff3e032189adb20 /ext/bg | |
parent | 438498435227cfa59cf9ed3430045b288cd2a7c0 (diff) | |
parent | 91c01e0a7eeeb851344a22ace8a5fa0b873a3e57 (diff) |
Merge branch 'master' into testing
Diffstat (limited to 'ext/bg')
-rw-r--r-- | ext/bg/background.html | 9 | ||||
-rw-r--r-- | ext/bg/context.html | 6 | ||||
-rw-r--r-- | ext/bg/guide.html | 6 | ||||
-rw-r--r-- | ext/bg/js/api.js | 58 | ||||
-rw-r--r-- | ext/bg/js/backend.js | 8 | ||||
-rw-r--r-- | ext/bg/js/context.js | 4 | ||||
-rw-r--r-- | ext/bg/js/handlebars.js | 4 | ||||
-rw-r--r-- | ext/bg/js/options.js | 4 | ||||
-rw-r--r-- | ext/bg/js/search.js | 173 | ||||
-rw-r--r-- | ext/bg/js/settings-popup-preview.js | 7 | ||||
-rw-r--r-- | ext/bg/js/translator.js | 2 | ||||
-rw-r--r-- | ext/bg/legal.html | 6 | ||||
-rw-r--r-- | ext/bg/search.html | 23 | ||||
-rw-r--r-- | ext/bg/settings-popup-preview.html | 6 | ||||
-rw-r--r-- | ext/bg/settings.html | 6 |
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"> |