separate storage for order + important styles (#1390)
* use Proxy for `db` * don't merge arrays in deepMerge by default * extract sync and cache from styleMan
This commit is contained in:
parent
46785b0550
commit
26b75e77b3
|
@ -1693,6 +1693,9 @@
|
|||
"styleIncludeLabel": {
|
||||
"message": "Custom included sites"
|
||||
},
|
||||
"styleInjectionImportance": {
|
||||
"message": "Toggle style's importance"
|
||||
},
|
||||
"styleInjectionOrder": {
|
||||
"message": "Style injection order",
|
||||
"description": "Tooltip for the button in the manager to open the dialog and also the title of this dialog"
|
||||
|
@ -1701,6 +1704,10 @@
|
|||
"message": "Drag'n'drop a style to change its position. Styles are injected sequentially in the order shown below so a style further down the list can override the earlier styles.",
|
||||
"description": "Hint in the injection order dialog in the manager"
|
||||
},
|
||||
"styleInjectionOrderHint_prio": {
|
||||
"message": "Important styles listed below are always injected last so they can override any newly installed styles. Click the style's mark to toggle its importance.",
|
||||
"description": "Hint at the bottom of the injection order dialog in the manager"
|
||||
},
|
||||
"styleExcludeLabel": {
|
||||
"message": "Custom excluded sites"
|
||||
},
|
||||
|
|
|
@ -39,11 +39,7 @@ addAPI(/** @namespace API */ {
|
|||
},
|
||||
}))(),
|
||||
|
||||
/** @type IDBObjectStore */
|
||||
drafts: new Proxy({}, {
|
||||
get: (_, cmd) => (...args) => db.exec.call('drafts', cmd, ...args),
|
||||
}),
|
||||
|
||||
drafts: db.open('drafts'),
|
||||
styles: styleMan,
|
||||
sync: syncMan,
|
||||
updater: updateMan,
|
||||
|
|
|
@ -5,16 +5,19 @@
|
|||
* Common stuff that's loaded first so it's immediately available to all background scripts
|
||||
*/
|
||||
|
||||
/* exported
|
||||
addAPI
|
||||
bgReady
|
||||
compareRevision
|
||||
*/
|
||||
|
||||
const bgReady = {};
|
||||
bgReady.styles = new Promise(r => (bgReady._resolveStyles = r));
|
||||
bgReady.all = new Promise(r => (bgReady._resolveAll = r));
|
||||
|
||||
const uuidIndex = Object.assign(new Map(), {
|
||||
custom: {},
|
||||
/** `obj` must have a unique `id`, a UUIDv4 `_id`, and Date.now() for `_rev`. */
|
||||
addCustomId(obj, {get = () => obj, set}) {
|
||||
Object.defineProperty(uuidIndex.custom, obj.id, {get, set});
|
||||
},
|
||||
});
|
||||
|
||||
/* exported addAPI */
|
||||
function addAPI(methods) {
|
||||
for (const [key, val] of Object.entries(methods)) {
|
||||
const old = API[key];
|
||||
|
@ -26,6 +29,64 @@ function addAPI(methods) {
|
|||
}
|
||||
}
|
||||
|
||||
function compareRevision(rev1, rev2) {
|
||||
return rev1 - rev2;
|
||||
/* exported createCache */
|
||||
/** Creates a FIFO limit-size map. */
|
||||
function createCache({size = 1000, onDeleted} = {}) {
|
||||
const map = new Map();
|
||||
const buffer = Array(size);
|
||||
let index = 0;
|
||||
let lastIndex = 0;
|
||||
return {
|
||||
get(id) {
|
||||
const item = map.get(id);
|
||||
return item && item.data;
|
||||
},
|
||||
set(id, data) {
|
||||
if (map.size === size) {
|
||||
// full
|
||||
map.delete(buffer[lastIndex].id);
|
||||
if (onDeleted) {
|
||||
onDeleted(buffer[lastIndex].id, buffer[lastIndex].data);
|
||||
}
|
||||
lastIndex = (lastIndex + 1) % size;
|
||||
}
|
||||
const item = {id, data, index};
|
||||
map.set(id, item);
|
||||
buffer[index] = item;
|
||||
index = (index + 1) % size;
|
||||
},
|
||||
delete(id) {
|
||||
const item = map.get(id);
|
||||
if (!item) {
|
||||
return false;
|
||||
}
|
||||
map.delete(item.id);
|
||||
const lastItem = buffer[lastIndex];
|
||||
lastItem.index = item.index;
|
||||
buffer[item.index] = lastItem;
|
||||
lastIndex = (lastIndex + 1) % size;
|
||||
if (onDeleted) {
|
||||
onDeleted(item.id, item.data);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
clear() {
|
||||
map.clear();
|
||||
index = lastIndex = 0;
|
||||
},
|
||||
has: id => map.has(id),
|
||||
*entries() {
|
||||
for (const [id, item] of map) {
|
||||
yield [id, item.data];
|
||||
}
|
||||
},
|
||||
*values() {
|
||||
for (const item of map.values()) {
|
||||
yield item.data;
|
||||
}
|
||||
},
|
||||
get size() {
|
||||
return map.size;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -4,6 +4,8 @@
|
|||
/* exported createChromeStorageDB */
|
||||
function createChromeStorageDB(PREFIX) {
|
||||
let INC;
|
||||
const isMain = !PREFIX;
|
||||
if (!PREFIX) PREFIX = 'style-';
|
||||
|
||||
return {
|
||||
|
||||
|
@ -19,7 +21,9 @@ function createChromeStorageDB(PREFIX) {
|
|||
const all = await chromeLocal.get();
|
||||
if (!INC) prepareInc(all);
|
||||
return Object.entries(all)
|
||||
.map(([key, val]) => key.startsWith(PREFIX) && Number(key.slice(PREFIX.length)) && val)
|
||||
.map(([key, val]) => key.startsWith(PREFIX) &&
|
||||
(!isMain || Number(key.slice(PREFIX.length))) &&
|
||||
val)
|
||||
.filter(Boolean);
|
||||
},
|
||||
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
/* global addAPI */// common.js
|
||||
/* global chromeLocal */// storage-util.js
|
||||
/* global cloneError */// worker-util.js
|
||||
/* global prefs */
|
||||
'use strict';
|
||||
|
||||
/*
|
||||
|
@ -11,16 +13,29 @@
|
|||
|
||||
/* exported db */
|
||||
const db = (() => {
|
||||
const DATABASE = 'stylish';
|
||||
const STORE = 'styles';
|
||||
let exec = async (...args) => (
|
||||
exec = await tryUsingIndexedDB().catch(useChromeStorage)
|
||||
)(...args);
|
||||
const DB = 'stylish';
|
||||
const FALLBACK = 'dbInChromeStorage';
|
||||
const dbApi = {
|
||||
async exec(...args) {
|
||||
dbApi.exec = await tryUsingIndexedDB().catch(useChromeStorage);
|
||||
return dbApi.exec(...args);
|
||||
},
|
||||
const getStoreName = dbName => dbName === DB ? 'styles' : 'data';
|
||||
const proxies = {};
|
||||
const proxyHandler = {
|
||||
get: ({dbName}, cmd) => (...args) => exec(dbName, cmd, ...args),
|
||||
};
|
||||
/** @return {IDBObjectStore | {putMany: function(items:?[]):Promise<?[]>}} */
|
||||
const getProxy = (dbName = DB) => proxies[dbName] || (
|
||||
proxies[dbName] = new Proxy({dbName}, proxyHandler)
|
||||
);
|
||||
addAPI(/** @namespace API */ {
|
||||
/** Storage for big items that may exceed 8kB limit of chrome.storage.sync.
|
||||
* To make an item syncable register it with uuidIndex.addCustomId. */
|
||||
prefsDb: getProxy(prefs.STORAGE_KEY),
|
||||
});
|
||||
return {
|
||||
styles: getProxy(),
|
||||
open: getProxy,
|
||||
};
|
||||
return dbApi;
|
||||
|
||||
async function tryUsingIndexedDB() {
|
||||
// we use chrome.storage.local fallback if IndexedDB doesn't save data,
|
||||
|
@ -40,9 +55,9 @@ const db = (() => {
|
|||
|
||||
async function testDB() {
|
||||
const id = `${performance.now()}.${Math.random()}.${Date.now()}`;
|
||||
await dbExecIndexedDB('put', {id});
|
||||
const e = await dbExecIndexedDB('get', id);
|
||||
await dbExecIndexedDB('delete', e.id); // throws if `e` or id is null
|
||||
await dbExecIndexedDB(DB, 'put', {id});
|
||||
const e = await dbExecIndexedDB(DB, 'get', id);
|
||||
await dbExecIndexedDB(DB, 'delete', e.id); // throws if `e` or id is null
|
||||
}
|
||||
|
||||
async function useChromeStorage(err) {
|
||||
|
@ -53,17 +68,17 @@ const db = (() => {
|
|||
}
|
||||
await require(['/background/db-chrome-storage']); /* global createChromeStorageDB */
|
||||
const BASES = {};
|
||||
return function dbExecChromeStorage(method, ...args) {
|
||||
const prefix = Object(this) instanceof String ? `${this}-` : 'style-';
|
||||
const baseApi = BASES[prefix] || (BASES[prefix] = createChromeStorageDB(prefix));
|
||||
return baseApi[method](...args);
|
||||
};
|
||||
return (dbName, method, ...args) => (
|
||||
BASES[dbName] || (
|
||||
BASES[dbName] = createChromeStorageDB(dbName !== DB && `${dbName}-`)
|
||||
)
|
||||
)[method](...args);
|
||||
}
|
||||
|
||||
async function dbExecIndexedDB(method, ...args) {
|
||||
async function dbExecIndexedDB(dbName, method, ...args) {
|
||||
const mode = method.startsWith('get') ? 'readonly' : 'readwrite';
|
||||
const dbName = Object(this) instanceof String ? `${this}` : DATABASE;
|
||||
const store = (await open(dbName)).transaction([STORE], mode).objectStore(STORE);
|
||||
const storeName = getStoreName(dbName);
|
||||
const store = (await open(dbName)).transaction([storeName], mode).objectStore(storeName);
|
||||
const fn = method === 'putMany' ? putMany : storeRequest;
|
||||
return fn(store, method, ...args);
|
||||
}
|
||||
|
@ -92,7 +107,8 @@ const db = (() => {
|
|||
|
||||
function create(event) {
|
||||
if (event.oldVersion === 0) {
|
||||
event.target.result.createObjectStore(STORE, {
|
||||
const idb = event.target.result;
|
||||
idb.createObjectStore(getStoreName(idb.name), {
|
||||
keyPath: 'id',
|
||||
autoIncrement: true,
|
||||
});
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/* global API msg */// msg.js
|
||||
/* global CHROME URLS isEmptyObj stringAsRegExp tryRegExp tryURL */// toolbox.js
|
||||
/* global bgReady compareRevision */// common.js
|
||||
/* global CHROME URLS deepEqual isEmptyObj mapObj stringAsRegExp tryRegExp tryURL */// toolbox.js
|
||||
/* global bgReady createCache uuidIndex */// common.js
|
||||
/* global calcStyleDigest styleCodeEmpty styleSectionGlobal */// sections-util.js
|
||||
/* global db */
|
||||
/* global prefs */
|
||||
|
@ -18,18 +18,26 @@ The live preview feature relies on `runtime.connect` and `port.onDisconnect`
|
|||
to cleanup the temporary code. See livePreview in /edit.
|
||||
*/
|
||||
|
||||
const styleUtil = {};
|
||||
|
||||
/* exported styleMan */
|
||||
const styleMan = (() => {
|
||||
|
||||
Object.assign(styleUtil, {
|
||||
id2style,
|
||||
handleSave,
|
||||
uuid2style,
|
||||
});
|
||||
|
||||
//#region Declarations
|
||||
|
||||
/** @typedef {{
|
||||
style: StyleObj
|
||||
preview?: StyleObj
|
||||
appliesTo: Set<string>
|
||||
style: StyleObj,
|
||||
preview?: StyleObj,
|
||||
appliesTo: Set<string>,
|
||||
}} StyleMapData */
|
||||
/** @type {Map<number,StyleMapData>} */
|
||||
const dataMap = new Map();
|
||||
const uuidIndex = new Map();
|
||||
/** @typedef {Object<styleId,{id: number, code: string[]}>} StyleSectionsToApply */
|
||||
/** @type {Map<string,{maybeMatch: Set<styleId>, sections: StyleSectionsToApply}>} */
|
||||
const cachedStyleForUrl = createCache({
|
||||
|
@ -57,9 +65,17 @@ const styleMan = (() => {
|
|||
_rev: () => Date.now(),
|
||||
};
|
||||
const DELETE_IF_NULL = ['id', 'customName', 'md5Url', 'originalMd5'];
|
||||
const INJ_ORDER = 'injectionOrder';
|
||||
const order = {main: {}, prio: {}};
|
||||
const orderWrap = {
|
||||
id: INJ_ORDER,
|
||||
value: mapObj(order, () => []),
|
||||
_id: `${chrome.runtime.id}-${INJ_ORDER}`,
|
||||
_rev: 0,
|
||||
};
|
||||
uuidIndex.addCustomId(orderWrap, {set: setOrder});
|
||||
/** @type {Promise|boolean} will be `true` to avoid wasting a microtask tick on each `await` */
|
||||
let ready = init();
|
||||
let order = {};
|
||||
|
||||
chrome.runtime.onConnect.addListener(port => {
|
||||
if (port.name === 'livePreview') {
|
||||
|
@ -77,17 +93,6 @@ const styleMan = (() => {
|
|||
}
|
||||
});
|
||||
|
||||
prefs.subscribe(['injectionOrder'], (key, value) => {
|
||||
order = {};
|
||||
value.forEach((uid, i) => {
|
||||
const id = uuidIndex.get(uid);
|
||||
if (id) {
|
||||
order[id] = i;
|
||||
}
|
||||
});
|
||||
msg.broadcast({method: 'styleSort', order});
|
||||
});
|
||||
|
||||
//#endregion
|
||||
//#region Exports
|
||||
|
||||
|
@ -96,18 +101,23 @@ const styleMan = (() => {
|
|||
/** @returns {Promise<number>} style id */
|
||||
async delete(id, reason) {
|
||||
if (ready.then) await ready;
|
||||
const data = id2data(id);
|
||||
const {style, appliesTo} = data;
|
||||
await db.exec('delete', id);
|
||||
if (reason !== 'sync') {
|
||||
API.sync.delete(style._id, Date.now());
|
||||
}
|
||||
const {style, appliesTo} = dataMap.get(id);
|
||||
const sync = reason !== 'sync';
|
||||
const uuid = style._id;
|
||||
db.styles.delete(id);
|
||||
if (sync) API.sync.delete(uuid, Date.now());
|
||||
for (const url of appliesTo) {
|
||||
const cache = cachedStyleForUrl.get(url);
|
||||
if (cache) delete cache.sections[id];
|
||||
}
|
||||
dataMap.delete(id);
|
||||
uuidIndex.delete(style._id);
|
||||
uuidIndex.delete(uuid);
|
||||
mapObj(orderWrap.value, (group, type) => {
|
||||
delete order[type][id];
|
||||
const i = group.indexOf(uuid);
|
||||
if (i >= 0) group.splice(i, 1);
|
||||
});
|
||||
setOrder(orderWrap, {calc: false});
|
||||
if (style._usw && style._usw.token) {
|
||||
// Must be called after the style is deleted from dataMap
|
||||
API.usw.revoke(id);
|
||||
|
@ -120,17 +130,6 @@ const styleMan = (() => {
|
|||
return id;
|
||||
},
|
||||
|
||||
/** @returns {Promise<number>} style id */
|
||||
async deleteByUUID(_id, rev) {
|
||||
if (ready.then) await ready;
|
||||
const id = uuidIndex.get(_id);
|
||||
const oldDoc = id && id2style(id);
|
||||
if (oldDoc && compareRevision(oldDoc._rev, rev) <= 0) {
|
||||
// FIXME: does it make sense to set reason to 'sync' in deleteByUUID?
|
||||
return styleMan.delete(id, 'sync');
|
||||
}
|
||||
},
|
||||
|
||||
/** @returns {Promise<StyleObj>} */
|
||||
async editSave(style) {
|
||||
if (ready.then) await ready;
|
||||
|
@ -154,15 +153,27 @@ const styleMan = (() => {
|
|||
/** @returns {Promise<StyleObj[]>} */
|
||||
async getAll() {
|
||||
if (ready.then) await ready;
|
||||
return Array.from(dataMap.values(), data2style);
|
||||
return getAllAsArray();
|
||||
},
|
||||
|
||||
/** @returns {Promise<StyleObj>} */
|
||||
async getByUUID(uuid) {
|
||||
/** @returns {Promise<Object<string,StyleObj[]>>}>} */
|
||||
async getAllOrdered(keys) {
|
||||
if (ready.then) await ready;
|
||||
return id2style(uuidIndex.get(uuid));
|
||||
const res = mapObj(orderWrap.value, group => group.map(uuid2style));
|
||||
if (res.main.length + res.prio.length < dataMap.size) {
|
||||
for (const {style} of dataMap.values()) {
|
||||
if (!(style.id in order.main) && !(style.id in order.prio)) {
|
||||
res.main.push(style);
|
||||
}
|
||||
}
|
||||
}
|
||||
return keys
|
||||
? mapObj(res, group => group.map(style => mapObj(style, null, keys)))
|
||||
: res;
|
||||
},
|
||||
|
||||
getOrder: () => orderWrap.value,
|
||||
|
||||
/** @returns {Promise<StyleSectionsToApply>} */
|
||||
async getSectionsByUrl(url, id, isInitialApply) {
|
||||
if (ready.then) await ready;
|
||||
|
@ -210,7 +221,7 @@ const styleMan = (() => {
|
|||
const result = [];
|
||||
const styles = id
|
||||
? [id2style(id)].filter(Boolean)
|
||||
: Array.from(dataMap.values(), data2style);
|
||||
: getAllAsArray();
|
||||
const query = createMatchQuery(url);
|
||||
for (const style of styles) {
|
||||
let excluded = false;
|
||||
|
@ -262,11 +273,10 @@ const styleMan = (() => {
|
|||
await usercssMan.buildCode(style);
|
||||
}
|
||||
}
|
||||
const events = await db.exec('putMany', items);
|
||||
return Promise.all(items.map((item, i) => {
|
||||
afterSave(item, events[i]);
|
||||
return handleSave(item, {reason: 'import'});
|
||||
}));
|
||||
const events = await db.styles.putMany(items);
|
||||
return Promise.all(items.map((item, i) =>
|
||||
handleSave(item, {reason: 'import'}, events[i])
|
||||
));
|
||||
},
|
||||
|
||||
/** @returns {Promise<StyleObj>} */
|
||||
|
@ -279,33 +289,13 @@ const styleMan = (() => {
|
|||
return saveStyle(style, {reason});
|
||||
},
|
||||
|
||||
/** @returns {Promise<?StyleObj>} */
|
||||
async putByUUID(doc) {
|
||||
if (ready.then) await ready;
|
||||
const id = uuidIndex.get(doc._id);
|
||||
if (id) {
|
||||
doc.id = id;
|
||||
} else {
|
||||
delete doc.id;
|
||||
}
|
||||
const oldDoc = id && id2style(id);
|
||||
let diff = -1;
|
||||
if (oldDoc) {
|
||||
diff = compareRevision(oldDoc._rev, doc._rev);
|
||||
if (diff > 0) {
|
||||
API.sync.put(oldDoc._id, oldDoc._rev);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (diff < 0) {
|
||||
doc.id = await db.exec('put', doc);
|
||||
uuidIndex.set(doc._id, doc.id);
|
||||
return handleSave(doc, {reason: 'sync'});
|
||||
}
|
||||
},
|
||||
|
||||
save: saveStyle,
|
||||
|
||||
async setOrder(value) {
|
||||
if (ready.then) await ready;
|
||||
return setOrder({value}, {broadcast: true, sync: true});
|
||||
},
|
||||
|
||||
/** @returns {Promise<number>} style id */
|
||||
async toggle(id, enabled) {
|
||||
if (ready.then) await ready;
|
||||
|
@ -343,12 +333,12 @@ const styleMan = (() => {
|
|||
|
||||
/** @returns {?StyleObj} */
|
||||
function id2style(id) {
|
||||
return (dataMap.get(id) || {}).style;
|
||||
return (dataMap.get(Number(id)) || {}).style;
|
||||
}
|
||||
|
||||
/** @returns {?StyleObj} */
|
||||
function data2style(data) {
|
||||
return data && data.style;
|
||||
function uuid2style(uuid) {
|
||||
return id2style(uuidIndex.get(uuid));
|
||||
}
|
||||
|
||||
/** @returns {StyleObj} */
|
||||
|
@ -369,6 +359,7 @@ const styleMan = (() => {
|
|||
style,
|
||||
appliesTo: new Set(),
|
||||
});
|
||||
uuidIndex.set(style._id, style.id);
|
||||
}
|
||||
|
||||
/** @returns {StyleObj} */
|
||||
|
@ -472,29 +463,24 @@ const styleMan = (() => {
|
|||
fixKnownProblems(style);
|
||||
}
|
||||
|
||||
function afterSave(style, newId) {
|
||||
if (style.id == null) {
|
||||
style.id = newId;
|
||||
}
|
||||
uuidIndex.set(style._id, style.id);
|
||||
API.sync.put(style._id, style._rev);
|
||||
}
|
||||
|
||||
async function saveStyle(style, handlingOptions) {
|
||||
beforeSave(style);
|
||||
const newId = await db.exec('put', style);
|
||||
afterSave(style, newId);
|
||||
return handleSave(style, handlingOptions);
|
||||
const newId = await db.styles.put(style);
|
||||
return handleSave(style, handlingOptions, newId);
|
||||
}
|
||||
|
||||
function handleSave(style, {reason, broadcast = true}) {
|
||||
const data = id2data(style.id);
|
||||
function handleSave(style, {reason, broadcast = true}, id = style.id) {
|
||||
if (style.id == null) style.id = id;
|
||||
const data = id2data(id);
|
||||
const method = data ? 'styleUpdated' : 'styleAdded';
|
||||
if (!data) {
|
||||
storeInMap(style);
|
||||
} else {
|
||||
data.style = style;
|
||||
}
|
||||
if (reason !== 'sync') {
|
||||
API.sync.putDoc(style);
|
||||
}
|
||||
if (broadcast) broadcastStyleUpdated(style, reason, method);
|
||||
return style;
|
||||
}
|
||||
|
@ -519,15 +505,14 @@ const styleMan = (() => {
|
|||
}
|
||||
|
||||
async function init() {
|
||||
const styles = await db.exec('getAll') || [];
|
||||
const orderPromise = API.prefsDb.get(INJ_ORDER);
|
||||
const styles = await db.styles.getAll() || [];
|
||||
const updated = await Promise.all(styles.map(fixKnownProblems).filter(Boolean));
|
||||
if (updated.length) {
|
||||
await db.exec('putMany', updated);
|
||||
}
|
||||
for (const style of styles) {
|
||||
storeInMap(style);
|
||||
uuidIndex.set(style._id, style.id);
|
||||
await db.styles.putMany(updated);
|
||||
}
|
||||
setOrder(await orderPromise, {store: false});
|
||||
styles.forEach(storeInMap);
|
||||
ready = true;
|
||||
bgReady._resolveStyles();
|
||||
}
|
||||
|
@ -725,71 +710,40 @@ const styleMan = (() => {
|
|||
}
|
||||
}
|
||||
|
||||
/** @returns {StyleObj[]} */
|
||||
function getAllAsArray() {
|
||||
return Array.from(dataMap.values(), v => v.style);
|
||||
}
|
||||
|
||||
/** uuidv4 helper: converts to a 4-digit hex string and adds "-" at required positions */
|
||||
function hex4dashed(num, i) {
|
||||
return (num + 0x10000).toString(16).slice(-4) + (i >= 1 && i <= 4 ? '-' : '');
|
||||
}
|
||||
|
||||
async function setOrder(data, {broadcast, calc = true, store = true, sync} = {}) {
|
||||
if (!data || !data.value || deepEqual(data.value, orderWrap.value)) {
|
||||
return;
|
||||
}
|
||||
Object.assign(orderWrap, data, sync && {_rev: Date.now()});
|
||||
if (calc) {
|
||||
for (const [type, group] of Object.entries(data.value)) {
|
||||
const dst = order[type] = {};
|
||||
group.forEach((uuid, i) => {
|
||||
const id = uuidIndex.get(uuid);
|
||||
if (id) dst[id] = i;
|
||||
});
|
||||
}
|
||||
}
|
||||
if (broadcast) {
|
||||
msg.broadcast({method: 'styleSort', order});
|
||||
}
|
||||
if (store) {
|
||||
await API.prefsDb.put(orderWrap);
|
||||
}
|
||||
if (sync) {
|
||||
API.sync.putDoc(orderWrap);
|
||||
}
|
||||
}
|
||||
|
||||
//#endregion
|
||||
})();
|
||||
|
||||
/** Creates a FIFO limit-size map. */
|
||||
function createCache({size = 1000, onDeleted} = {}) {
|
||||
const map = new Map();
|
||||
const buffer = Array(size);
|
||||
let index = 0;
|
||||
let lastIndex = 0;
|
||||
return {
|
||||
get(id) {
|
||||
const item = map.get(id);
|
||||
return item && item.data;
|
||||
},
|
||||
set(id, data) {
|
||||
if (map.size === size) {
|
||||
// full
|
||||
map.delete(buffer[lastIndex].id);
|
||||
if (onDeleted) {
|
||||
onDeleted(buffer[lastIndex].id, buffer[lastIndex].data);
|
||||
}
|
||||
lastIndex = (lastIndex + 1) % size;
|
||||
}
|
||||
const item = {id, data, index};
|
||||
map.set(id, item);
|
||||
buffer[index] = item;
|
||||
index = (index + 1) % size;
|
||||
},
|
||||
delete(id) {
|
||||
const item = map.get(id);
|
||||
if (!item) {
|
||||
return false;
|
||||
}
|
||||
map.delete(item.id);
|
||||
const lastItem = buffer[lastIndex];
|
||||
lastItem.index = item.index;
|
||||
buffer[item.index] = lastItem;
|
||||
lastIndex = (lastIndex + 1) % size;
|
||||
if (onDeleted) {
|
||||
onDeleted(item.id, item.data);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
clear() {
|
||||
map.clear();
|
||||
index = lastIndex = 0;
|
||||
},
|
||||
has: id => map.has(id),
|
||||
*entries() {
|
||||
for (const [id, item] of map) {
|
||||
yield [id, item.data];
|
||||
}
|
||||
},
|
||||
*values() {
|
||||
for (const item of map.values()) {
|
||||
yield item.data;
|
||||
}
|
||||
},
|
||||
get size() {
|
||||
return map.size;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
/* global API msg */// msg.js
|
||||
/* global bgReady uuidIndex */// common.js
|
||||
/* global chromeLocal chromeSync */// storage-util.js
|
||||
/* global compareRevision */// common.js
|
||||
/* global db */
|
||||
/* global iconMan */
|
||||
/* global prefs */
|
||||
/* global styleUtil */
|
||||
/* global tokenMan */
|
||||
'use strict';
|
||||
|
||||
|
@ -28,11 +30,12 @@ const syncMan = (() => {
|
|||
errorMessage: null,
|
||||
login: false,
|
||||
};
|
||||
const compareRevision = (rev1, rev2) => rev1 - rev2;
|
||||
let lastError = null;
|
||||
let ctrl;
|
||||
let currentDrive;
|
||||
/** @type {Promise|boolean} will be `true` to avoid wasting a microtask tick on each `await` */
|
||||
let ready = prefs.ready.then(() => {
|
||||
let ready = bgReady.styles.then(() => {
|
||||
ready = true;
|
||||
prefs.subscribe('sync.enabled',
|
||||
(_, val) => val === 'none'
|
||||
|
@ -79,11 +82,11 @@ const syncMan = (() => {
|
|||
}
|
||||
},
|
||||
|
||||
async put(...args) {
|
||||
async putDoc({_id, _rev}) {
|
||||
if (ready.then) await ready;
|
||||
if (!currentDrive) return;
|
||||
schedule();
|
||||
return ctrl.put(...args);
|
||||
return ctrl.put(_id, _rev);
|
||||
},
|
||||
|
||||
async setDriveOptions(driveName, options) {
|
||||
|
@ -178,17 +181,33 @@ const syncMan = (() => {
|
|||
async function initController() {
|
||||
await require(['/vendor/db-to-cloud/db-to-cloud.min']); /* global dbToCloud */
|
||||
ctrl = dbToCloud.dbToCloud({
|
||||
onGet(id) {
|
||||
return API.styles.getByUUID(id);
|
||||
onGet: styleUtil.uuid2style,
|
||||
async onPut(doc) {
|
||||
const id = uuidIndex.get(doc._id);
|
||||
const oldCust = uuidIndex.custom[id];
|
||||
const oldDoc = oldCust || styleUtil.id2style(id);
|
||||
const diff = oldDoc ? compareRevision(oldDoc._rev, doc._rev) : -1;
|
||||
if (!diff) return;
|
||||
if (diff > 0) {
|
||||
syncMan.putDoc(oldDoc);
|
||||
} else if (oldCust) {
|
||||
uuidIndex.custom[id] = doc;
|
||||
} else {
|
||||
delete doc.id;
|
||||
if (id) doc.id = id;
|
||||
doc.id = await db.styles.put(doc);
|
||||
await styleUtil.handleSave(doc, {reason: 'sync'});
|
||||
}
|
||||
},
|
||||
onPut(doc) {
|
||||
return API.styles.putByUUID(doc);
|
||||
},
|
||||
onDelete(id, rev) {
|
||||
return API.styles.deleteByUUID(id, rev);
|
||||
onDelete(_id, rev) {
|
||||
const id = uuidIndex.get(_id);
|
||||
const oldDoc = styleUtil.id2style(id);
|
||||
return oldDoc &&
|
||||
compareRevision(oldDoc._rev, rev) <= 0 &&
|
||||
API.styles.delete(id, 'sync');
|
||||
},
|
||||
async onFirstSync() {
|
||||
for (const i of await API.styles.getAll()) {
|
||||
for (const i of Object.values(uuidIndex.custom).concat(await API.styles.getAll())) {
|
||||
ctrl.put(i._id, i._rev);
|
||||
}
|
||||
},
|
||||
|
|
|
@ -234,7 +234,7 @@ const updateMan = (() => {
|
|||
if (err && etag && !style.etag) {
|
||||
// first check of ETAG, gonna write it directly to DB as it's too trivial to sync or announce
|
||||
style.etag = etag;
|
||||
await db.exec('put', style);
|
||||
await db.styles.put(style);
|
||||
}
|
||||
return err
|
||||
? Promise.reject(err)
|
||||
|
|
|
@ -13,19 +13,16 @@
|
|||
let hasStyles = false;
|
||||
let isDisabled = false;
|
||||
let isTab = !chrome.tabs || location.pathname !== '/popup.html';
|
||||
let order = {};
|
||||
const order = {main: [], prio: []};
|
||||
const calcOrder = ({id}) =>
|
||||
(order.prio[id] || 0) * 1e6 ||
|
||||
order.main[id] ||
|
||||
id + .5e6; // no order = at the end of `main`
|
||||
const isFrame = window !== parent;
|
||||
const isFrameAboutBlank = isFrame && location.href === 'about:blank';
|
||||
const isUnstylable = !chrome.app && document instanceof XMLDocument;
|
||||
const styleInjector = StyleInjector({
|
||||
compare: (a, b) => {
|
||||
const ia = order[a.id];
|
||||
const ib = order[b.id];
|
||||
if (ia === ib) return 0;
|
||||
if (ia == null) return 1;
|
||||
if (ib == null) return -1;
|
||||
return ia - ib;
|
||||
},
|
||||
compare: (a, b) => calcOrder(a) - calcOrder(b),
|
||||
onUpdate: onInjectorUpdate,
|
||||
});
|
||||
// dynamic iframes don't have a URL yet so we'll use their parent's URL (hash isn't inherited)
|
||||
|
@ -110,7 +107,7 @@
|
|||
await API.styles.getSectionsByUrl(matchUrl, null, true);
|
||||
if (styles.cfg) {
|
||||
isDisabled = styles.cfg.disableAll;
|
||||
order = styles.cfg.order || {};
|
||||
Object.assign(order, styles.cfg.order);
|
||||
delete styles.cfg;
|
||||
}
|
||||
hasStyles = !isDisabled;
|
||||
|
@ -179,7 +176,7 @@
|
|||
break;
|
||||
|
||||
case 'styleSort':
|
||||
order = request.order;
|
||||
Object.assign(order, request.order);
|
||||
styleInjector.sort();
|
||||
break;
|
||||
|
||||
|
|
|
@ -1,13 +1,20 @@
|
|||
#message-box.injection-order > div {
|
||||
.injection-order > div {
|
||||
height: 100%;
|
||||
max-width: 80vw;
|
||||
}
|
||||
#message-box.injection-order #message-box-contents {
|
||||
.injection-order #incremental-search {
|
||||
transform: scaleY(.55);
|
||||
transform-origin: top;
|
||||
}
|
||||
.injection-order #message-box-contents,
|
||||
.injection-order section {
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
--border: 1px solid rgba(128, 128, 128, .25);
|
||||
}
|
||||
.injection-order section[data-main] {
|
||||
flex: 1 0;
|
||||
}
|
||||
.injection-order header {
|
||||
padding: 1rem;
|
||||
|
@ -16,39 +23,77 @@
|
|||
box-sizing: border-box;
|
||||
}
|
||||
.injection-order ol {
|
||||
height: 100%;
|
||||
padding: 1px 0 0; /* 1px for keyboard-focused element's outline */
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
overflow-y: auto;
|
||||
border-top: var(--border);
|
||||
}
|
||||
.injection-order a {
|
||||
display: block;
|
||||
.injection-order ol:empty {
|
||||
display: none;
|
||||
}
|
||||
.injection-order [data-prio] header {
|
||||
background-color: hsla(40, 80%, 50%, 0.4);
|
||||
}
|
||||
.injection-order [data-prio] {
|
||||
height: min-content;
|
||||
min-height: 2em;
|
||||
max-height: 50%;
|
||||
}
|
||||
.injection-order-entry {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
position: relative; /* for incremental-search */
|
||||
padding: 1px 1px 1px 1rem; /* keyboard focus outline */
|
||||
color: #000;
|
||||
text-decoration: none;
|
||||
transition: transform .25s ease-in-out;
|
||||
z-index: 1;
|
||||
user-select: none;
|
||||
padding: 0.3em .5em;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
cursor: move;
|
||||
}
|
||||
.injection-order a.enabled {
|
||||
.injection-order-entry a[href] {
|
||||
padding: .4em 0;
|
||||
cursor: inherit;
|
||||
}
|
||||
.injection-order-entry.enabled a[href] {
|
||||
font-weight: bold;
|
||||
}
|
||||
.injection-order a:hover {
|
||||
.injection-order-entry a {
|
||||
text-decoration: none;
|
||||
}
|
||||
.injection-order-entry a[href]:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.injection-order a:not(:first-child) {
|
||||
border-top: var(--border);
|
||||
.injection-order-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 .5rem;
|
||||
cursor: pointer;
|
||||
opacity: .5;
|
||||
transition: .15s;
|
||||
}
|
||||
.injection-order a::before {
|
||||
content: "\2261";
|
||||
padding: 0.6em;
|
||||
font-weight: normal;
|
||||
.injection-order-toggle::after {
|
||||
content: '\2606';
|
||||
font-size: 20px;
|
||||
line-height: 1;
|
||||
transition: .15s;
|
||||
}
|
||||
.injection-order-entry:hover .injection-order-toggle {
|
||||
opacity: 1;
|
||||
}
|
||||
[data-prio] .injection-order-toggle::after {
|
||||
content: '\2605';
|
||||
}
|
||||
.injection-order-toggle:hover::after {
|
||||
color: hsl(30, 80%, 50%);
|
||||
}
|
||||
.injection-order [data-prio] header,
|
||||
.injection-order ol,
|
||||
.injection-order #message-box-buttons,
|
||||
.injection-order-entry:nth-child(n + 2) {
|
||||
border-top: 1px solid rgba(128, 128, 128, .25);
|
||||
}
|
||||
.injection-order .draggable-list-target {
|
||||
position: relative;
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
/* global $create messageBoxProxy */// dom.js
|
||||
/* global API */// msg.js
|
||||
/* global DraggableList */
|
||||
/* global prefs */
|
||||
/* global t */// localization.js
|
||||
'use strict';
|
||||
|
||||
|
@ -10,70 +9,75 @@ async function InjectionOrder(show = true) {
|
|||
if (!show) {
|
||||
return messageBoxProxy.close();
|
||||
}
|
||||
const entries = (await getOrderedStyles()).map(makeEntry);
|
||||
const ol = $create('ol');
|
||||
let maxTranslateY;
|
||||
ol.append(...entries.map(l => l.el));
|
||||
ol.on('d:dragstart', ({detail: d}) => {
|
||||
d.origin.dataTransfer.setDragImage(new Image(), 0, 0);
|
||||
maxTranslateY = ol.scrollHeight + ol.offsetTop - d.dragTarget.offsetHeight - d.dragTarget.offsetTop;
|
||||
});
|
||||
ol.on('d:dragmove', ({detail: d}) => {
|
||||
d.origin.stopPropagation(); // preserves dropEffect
|
||||
d.origin.dataTransfer.dropEffect = 'move';
|
||||
const y = Math.min(d.currentPos.y - d.startPos.y, maxTranslateY);
|
||||
d.dragTarget.style.transform = `translateY(${y}px)`;
|
||||
});
|
||||
ol.on('d:dragend', ({detail: d}) => {
|
||||
const [item] = entries.splice(d.originalIndex, 1);
|
||||
entries.splice(d.spliceIndex, 0, item);
|
||||
ol.insertBefore(d.dragTarget, d.insertBefore);
|
||||
prefs.set('injectionOrder', entries.map(l => l.style._id));
|
||||
});
|
||||
DraggableList(ol, {scrollContainer: ol});
|
||||
|
||||
const SEL_ENTRY = '.injection-order-entry';
|
||||
const groups = await API.styles.getAllOrdered(['_id', 'id', 'name', 'enabled']);
|
||||
const ols = {};
|
||||
const parts = {};
|
||||
const entry = $create('li' + SEL_ENTRY, [
|
||||
parts.name = $create('a', {
|
||||
target: '_blank',
|
||||
draggable: false,
|
||||
}),
|
||||
$create('a.injection-order-toggle', {
|
||||
tabIndex: 0,
|
||||
draggable: false,
|
||||
title: t('styleInjectionImportance'),
|
||||
}),
|
||||
]);
|
||||
await messageBoxProxy.show({
|
||||
title: t('styleInjectionOrder'),
|
||||
contents: $create('fragment', [
|
||||
$create('header', t('styleInjectionOrderHint')),
|
||||
ol,
|
||||
]),
|
||||
contents: $create('fragment', Object.entries(groups).map(makeList)),
|
||||
className: 'injection-order center-dialog',
|
||||
blockScroll: true,
|
||||
buttons: [t('confirmClose')],
|
||||
});
|
||||
|
||||
async function getOrderedStyles() {
|
||||
const [styles] = await Promise.all([
|
||||
API.styles.getAll(),
|
||||
prefs.ready,
|
||||
]);
|
||||
const styleSet = new Set(styles);
|
||||
const uuidIndex = new Map();
|
||||
for (const s of styleSet) {
|
||||
uuidIndex.set(s._id, s);
|
||||
}
|
||||
const orderedStyles = [];
|
||||
for (const uid of prefs.get('injectionOrder')) {
|
||||
const s = uuidIndex.get(uid);
|
||||
if (s) {
|
||||
uuidIndex.delete(uid);
|
||||
orderedStyles.push(s);
|
||||
styleSet.delete(s);
|
||||
}
|
||||
}
|
||||
orderedStyles.push(...styleSet);
|
||||
return orderedStyles;
|
||||
function makeEntry(style) {
|
||||
entry.classList.toggle('enabled', style.enabled);
|
||||
parts.name.href = '/edit.html?id=' + style.id;
|
||||
parts.name.textContent = style.name;
|
||||
return Object.assign(entry.cloneNode(true), {
|
||||
styleNameLowerCase: style.name.toLocaleLowerCase(),
|
||||
});
|
||||
}
|
||||
|
||||
function makeEntry(style) {
|
||||
return {
|
||||
style,
|
||||
el: $create('a', {
|
||||
className: style.enabled ? 'enabled' : '',
|
||||
href: '/edit.html?id=' + style.id,
|
||||
target: '_blank',
|
||||
}, style.name),
|
||||
};
|
||||
function makeList([type, styles]) {
|
||||
const ids = groups[type] = styles.map(s => s._id);
|
||||
const ol = ols[type] = $create('ol.scroller');
|
||||
let maxTranslateY;
|
||||
ol.append(...styles.map(makeEntry));
|
||||
ol.on('d:dragstart', ({detail: d}) => {
|
||||
d.origin.dataTransfer.setDragImage(new Image(), 0, 0);
|
||||
maxTranslateY =
|
||||
ol.scrollHeight + ol.offsetTop - d.dragTarget.offsetHeight - d.dragTarget.offsetTop;
|
||||
});
|
||||
ol.on('d:dragmove', ({detail: d}) => {
|
||||
d.origin.stopPropagation(); // preserves dropEffect
|
||||
d.origin.dataTransfer.dropEffect = 'move';
|
||||
const y = Math.min(d.currentPos.y - d.startPos.y, maxTranslateY);
|
||||
d.dragTarget.style.transform = `translateY(${y}px)`;
|
||||
});
|
||||
ol.on('d:dragend', ({detail: d}) => {
|
||||
const [item] = ids.splice(d.originalIndex, 1);
|
||||
ids.splice(d.spliceIndex, 0, item);
|
||||
ol.insertBefore(d.dragTarget, d.insertBefore);
|
||||
API.styles.setOrder(groups);
|
||||
});
|
||||
ol.on('click', e => {
|
||||
if (e.target.closest('.injection-order-toggle')) {
|
||||
const el = e.target.closest(SEL_ENTRY);
|
||||
const i = [].indexOf.call(el.parentNode.children, el);
|
||||
const [item] = ids.splice(i, 1);
|
||||
const type2 = type === 'main' ? 'prio' : 'main';
|
||||
groups[type2].push(item);
|
||||
ols[type2].appendChild(el);
|
||||
API.styles.setOrder(groups);
|
||||
}
|
||||
});
|
||||
DraggableList(ol, {scrollContainer: ol});
|
||||
return $create('section', {dataset: {[type]: ''}}, [
|
||||
$create('header', t(`styleInjectionOrderHint${type === 'main' ? '' : '_' + type}`)),
|
||||
ol,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -278,6 +278,12 @@ function onDOMready() {
|
|||
: new Promise(resolve => document.on('DOMContentLoaded', resolve, {once: true}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Scrolls `window` or the closest parent with `class="scroller"` if the element is not visible,
|
||||
* centering the element in the view
|
||||
* @param {HTMLElement} element
|
||||
* @param {number} [invalidMarginRatio] - for example, 0.10 will center the element if it's in the top/bottom 10% of the scroller
|
||||
*/
|
||||
function scrollElementIntoView(element, {invalidMarginRatio = 0} = {}) {
|
||||
// align to the top/bottom of the visible area if wasn't visible
|
||||
if (!element.parentNode) return;
|
||||
|
@ -286,7 +292,8 @@ function scrollElementIntoView(element, {invalidMarginRatio = 0} = {}) {
|
|||
const windowHeight = window.innerHeight;
|
||||
if (top < Math.max(parentTop, windowHeight * invalidMarginRatio) ||
|
||||
top > Math.min(parentBottom, windowHeight) - height - windowHeight * invalidMarginRatio) {
|
||||
window.scrollBy(0, top - windowHeight / 2 + height);
|
||||
const scroller = element.closest('.scroller');
|
||||
scroller.scrollBy(0, top - (scroller ? scroller.clientHeight : windowHeight) / 2 + height);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -134,8 +134,6 @@
|
|||
'popupWidth': 246, // popup width in pixels
|
||||
|
||||
'updateInterval': 24, // user-style automatic update interval, hours (0 = disable)
|
||||
|
||||
'injectionOrder': [],
|
||||
};
|
||||
const knownKeys = Object.keys(defaults);
|
||||
/** @type {PrefsValues} */
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
getTab
|
||||
ignoreChromeError
|
||||
isEmptyObj
|
||||
mapObj
|
||||
onTabReady
|
||||
openURL
|
||||
sessionStore
|
||||
|
@ -288,6 +289,24 @@ function isEmptyObj(obj) {
|
|||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {?Object} obj
|
||||
* @param {function(val:?, key:string, obj:Object):T} [fn]
|
||||
* @param {string[]} [keys]
|
||||
* @returns {?Object<string,T>}
|
||||
* @template T
|
||||
*/
|
||||
function mapObj(obj, fn, keys) {
|
||||
if (!obj) return obj;
|
||||
const res = {};
|
||||
for (const k of keys || Object.keys(obj)) {
|
||||
if (!keys || k in obj) {
|
||||
res[k] = fn ? fn(obj[k], k, obj) : obj[k];
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* js engine can't optimize the entire function if it contains try-catch
|
||||
* so we should keep it isolated from normal code in a minimal wrapper
|
||||
|
@ -349,13 +368,13 @@ Object.assign(debounce, {
|
|||
},
|
||||
});
|
||||
|
||||
function deepMerge(src, dst) {
|
||||
function deepMerge(src, dst, mergeArrays) {
|
||||
if (!src || typeof src !== 'object') {
|
||||
return src;
|
||||
}
|
||||
if (Array.isArray(src)) {
|
||||
// using `Array` that belongs to this `window`; not using Array.from as it's slower
|
||||
if (!dst) dst = Array.prototype.map.call(src, deepCopy);
|
||||
if (!dst || !mergeArrays) dst = Array.prototype.map.call(src, deepCopy);
|
||||
else for (const v of src) dst.push(deepMerge(v));
|
||||
} else {
|
||||
// using an explicit {} that belongs to this `window`
|
||||
|
|
|
@ -314,7 +314,10 @@
|
|||
</label>
|
||||
</div>
|
||||
<button id="manage-options-button" i18n-text="openOptions"></button>
|
||||
<button id="injection-order-button" i18n-title="styleInjectionOrder">↑↓</button>
|
||||
<button id="injection-order-button" i18n-title="styleInjectionOrder">
|
||||
<svg class="svg-icon"><use xlink:href="#svg-icon-reorder"/></svg>
|
||||
|
||||
</button>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
|
@ -380,6 +383,10 @@
|
|||
<symbol id="svg-icon-config" viewBox="0 0 16 16">
|
||||
<path d="M13.3,12.8l1.5-2.6l-2.2-1.5c0-0.2,0.1-0.5,0.1-0.7c0-0.2,0-0.5-0.1-0.7l2.2-1.5l-1.5-2.6l-2.4,1.2 c-0.4-0.3-0.8-0.5-1.2-0.7L9.5,1h-3L6.3,3.7C5.9,3.8,5.5,4.1,5.1,4.4L2.7,3.2L1.2,5.8l2.2,1.5c0,0.2-0.1,0.5-0.1,0.7 c0,0.2,0,0.5,0.1,0.7l-2.2,1.5l1.5,2.6l2.4-1.2c0.4,0.3,0.8,0.5,1.2,0.7L6.5,15h3l0.2-2.7c0.4-0.2,0.8-0.4,1.2-0.7L13.3,12.8z M8,10.3c-1.3,0-2.3-1-2.3-2.3c0-1.3,1-2.3,2.3-2.3c1.3,0,2.3,1,2.3,2.3C10.3,9.3,9.3,10.3,8,10.3z"/>
|
||||
</symbol>
|
||||
|
||||
<symbol id="svg-icon-reorder" viewBox="0 0 16 16">
|
||||
<path d="M0,16h7v-6H0V16z M1,11h5v4H1V11z M0,5h7V0H0V5z M1,1h5v3H1V1z M13,11v3l-4-3.5L13,7v3h2V3H8V2h8v9H13z M7,9H0V8h7V9z M7,7H0V6h7V7z"/>
|
||||
</symbol>
|
||||
</svg>
|
||||
|
||||
</body>
|
||||
|
|
|
@ -105,6 +105,7 @@ async function importFromString(jsonString) {
|
|||
const oldStyles = Array.isArray(json) && json.length ? await API.styles.getAll() : [];
|
||||
const oldStylesById = new Map(oldStyles.map(style => [style.id, style]));
|
||||
const oldStylesByName = new Map(oldStyles.map(style => [style.name.trim(), style]));
|
||||
const oldOrder = await API.styles.getOrder();
|
||||
const items = [];
|
||||
const infos = [];
|
||||
const stats = {
|
||||
|
@ -116,11 +117,14 @@ async function importFromString(jsonString) {
|
|||
codeOnly: {names: [], ids: [], legend: 'importReportLegendUpdatedCode', dirty: true},
|
||||
invalid: {names: [], legend: 'importReportLegendInvalid'},
|
||||
};
|
||||
let order;
|
||||
await Promise.all(json.map(analyze));
|
||||
changeQueue.length = 0;
|
||||
changeQueue.time = performance.now();
|
||||
(await API.styles.importMany(items))
|
||||
.forEach((style, i) => updateStats(style, infos[i]));
|
||||
// TODO: set each style's order during import on-the-fly
|
||||
await API.styles.setOrder(order);
|
||||
return done();
|
||||
|
||||
function analyze(item, index) {
|
||||
|
@ -168,6 +172,8 @@ async function importFromString(jsonString) {
|
|||
async function analyzeStorage(storage) {
|
||||
analyzePrefs(storage[prefs.STORAGE_KEY], prefs.knownKeys, prefs.values, true);
|
||||
delete storage[prefs.STORAGE_KEY];
|
||||
order = storage.order;
|
||||
delete storage.order;
|
||||
if (!isEmptyObj(storage)) {
|
||||
analyzePrefs(storage, Object.values(chromeSync.LZ_KEY), await chromeSync.getLZValues());
|
||||
}
|
||||
|
@ -285,7 +291,7 @@ async function importFromString(jsonString) {
|
|||
};
|
||||
}
|
||||
|
||||
function undo() {
|
||||
async function undo() {
|
||||
const newIds = [
|
||||
...stats.metaAndCode.ids,
|
||||
...stats.metaOnly.ids,
|
||||
|
@ -293,6 +299,8 @@ async function importFromString(jsonString) {
|
|||
...stats.added.ids,
|
||||
];
|
||||
let tasks = Promise.resolve();
|
||||
// TODO: delete all deletable at once
|
||||
// TODO: import all importable at once
|
||||
for (const id of newIds) {
|
||||
tasks = tasks.then(() => API.styles.delete(id));
|
||||
const oldStyle = oldStylesById.get(id);
|
||||
|
@ -300,13 +308,13 @@ async function importFromString(jsonString) {
|
|||
tasks = tasks.then(() => API.styles.importMany([oldStyle]));
|
||||
}
|
||||
}
|
||||
// taskUI is superfast and updates style list only in this page,
|
||||
// which should account for 99.99999999% of cases, supposedly
|
||||
return tasks.then(() => messageBoxProxy.show({
|
||||
await tasks;
|
||||
await API.styles.setOrder(oldOrder);
|
||||
await messageBoxProxy.show({
|
||||
title: t('importReportUndoneTitle'),
|
||||
contents: newIds.length + ' ' + t('importReportUndone'),
|
||||
buttons: [t('confirmClose')],
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
function bindClick() {
|
||||
|
@ -344,6 +352,7 @@ async function exportToFile(e) {
|
|||
const data = [
|
||||
Object.assign({
|
||||
[prefs.STORAGE_KEY]: prefs.values,
|
||||
order: await API.styles.getOrder(),
|
||||
}, await chromeSync.getLZValues()),
|
||||
...(await API.styles.getAll()).map(cleanupStyle),
|
||||
];
|
||||
|
|
12
manage/incremental-search.css
Normal file
12
manage/incremental-search.css
Normal file
|
@ -0,0 +1,12 @@
|
|||
#incremental-search {
|
||||
position: absolute;
|
||||
color: transparent;
|
||||
border: 1px solid hsla(180, 100%, 50%, .25);
|
||||
margin: -1px -2px;
|
||||
overflow: hidden;
|
||||
resize: none;
|
||||
background-color: hsla(180, 100%, 50%, .1);
|
||||
box-sizing: content-box;
|
||||
pointer-events: none;
|
||||
z-index: 2147483647,
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
/* global debounce */// toolbox.js
|
||||
/* global installed */// manage.js
|
||||
/* global
|
||||
$$
|
||||
$
|
||||
$create
|
||||
$isTextInput
|
||||
|
@ -9,44 +10,41 @@
|
|||
*/// dom.js
|
||||
'use strict';
|
||||
|
||||
(() => {
|
||||
(async () => {
|
||||
await require(['/manage/incremental-search.css']);
|
||||
let prevText, focusedLink, focusedEntry;
|
||||
let prevTime = performance.now();
|
||||
let focusedName = '';
|
||||
const input = $create('textarea', {
|
||||
id: 'incremental-search',
|
||||
spellcheck: false,
|
||||
attributes: {tabindex: -1},
|
||||
oninput: incrementalSearch,
|
||||
});
|
||||
replaceInlineStyle({
|
||||
opacity: '0',
|
||||
position: 'absolute',
|
||||
color: 'transparent',
|
||||
border: '1px solid hsla(180, 100%, 100%, .5)',
|
||||
margin: '-1px -2px',
|
||||
overflow: 'hidden',
|
||||
resize: 'none',
|
||||
'background-color': 'hsla(180, 100%, 100%, .2)',
|
||||
'box-sizing': 'content-box',
|
||||
'pointer-events': 'none',
|
||||
});
|
||||
document.body.appendChild(input);
|
||||
window.on('keydown', maybeRefocus, true);
|
||||
|
||||
function incrementalSearch({key}, immediately) {
|
||||
function incrementalSearch(event, immediately) {
|
||||
const {key} = event;
|
||||
if (!immediately) {
|
||||
debounce(incrementalSearch, 100, {}, true);
|
||||
return;
|
||||
}
|
||||
const direction = key === 'ArrowUp' ? -1 : key === 'ArrowDown' ? 1 : 0;
|
||||
const text = input.value.toLocaleLowerCase();
|
||||
if (direction) {
|
||||
event.preventDefault();
|
||||
}
|
||||
if (!text.trim() || !direction && (text === prevText || focusedName.startsWith(text))) {
|
||||
prevText = text;
|
||||
return;
|
||||
}
|
||||
let textAtPos = 1e6;
|
||||
let rotated;
|
||||
const entries = [...installed.children];
|
||||
const entries = $('#message-box') ? $$('.injection-order-entry') : [...installed.children];
|
||||
const focusedIndex = entries.indexOf(focusedEntry);
|
||||
if (focusedIndex > 0) {
|
||||
if (direction > 0) {
|
||||
|
@ -74,7 +72,7 @@
|
|||
}
|
||||
if (found && found !== focusedEntry) {
|
||||
focusedEntry = found;
|
||||
focusedLink = $('.style-name-link', found);
|
||||
focusedLink = $('a', found);
|
||||
focusedName = found.styleNameLowerCase;
|
||||
scrollElementIntoView(found, {invalidMarginRatio: .25});
|
||||
animateElement(found, 'highlight-quick');
|
||||
|
@ -84,12 +82,17 @@
|
|||
opacity: '1',
|
||||
});
|
||||
focusedLink.prepend(input);
|
||||
input.focus();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
function maybeRefocus(event) {
|
||||
if (event.altKey || event.metaKey || $('#message-box')) {
|
||||
if (event.altKey || event.metaKey) {
|
||||
return;
|
||||
}
|
||||
const modal = $('#message-box');
|
||||
if (modal && !modal.classList.contains('injection-order')) {
|
||||
return;
|
||||
}
|
||||
const inTextInput = $isTextInput(event.target);
|
||||
|
@ -99,7 +102,7 @@
|
|||
(code === 'Slash' || key === '/') && !ctrl && !inTextInput) {
|
||||
// focus search field on "/" or Ctrl-F key
|
||||
event.preventDefault();
|
||||
$('#search').focus();
|
||||
if (!modal) $('#search').focus();
|
||||
return;
|
||||
}
|
||||
if (ctrl || inTextInput && event.target !== input) {
|
||||
|
|
|
@ -112,6 +112,19 @@ a:hover {
|
|||
left: 2px;
|
||||
}
|
||||
|
||||
#injection-order-button {
|
||||
--w: 16px;
|
||||
width: var(--w);
|
||||
box-sizing: content-box;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
#injection-order-button > svg {
|
||||
position: absolute;
|
||||
width: var(--w);
|
||||
height: var(--w);
|
||||
}
|
||||
|
||||
#installed {
|
||||
position: relative;
|
||||
padding-left: var(--header-width);
|
||||
|
|
|
@ -93,11 +93,11 @@ newUI.renderClass();
|
|||
|
||||
showStyles(styles, ids);
|
||||
|
||||
require([
|
||||
window.on('load', () => require([
|
||||
'/manage/import-export',
|
||||
'/manage/incremental-search',
|
||||
'/manage/updater-ui',
|
||||
]);
|
||||
]), {once: true});
|
||||
})();
|
||||
|
||||
msg.onExtension(onRuntimeMessage);
|
||||
|
|
Loading…
Reference in New Issue
Block a user