parent
60f59e9f06
commit
e54178a43c
|
@ -342,6 +342,19 @@
|
|||
"message": "Disable",
|
||||
"description": "Label for the button to disable a style"
|
||||
},
|
||||
"draftTitle": {
|
||||
"message": "Draft recovery, created $date$",
|
||||
"placeholders": {
|
||||
"date": {
|
||||
"content": "$1"
|
||||
}
|
||||
},
|
||||
"description": "Title of the modal displayed in the editor when an unsaved draft is found, the $date$ looks like '1 hour ago' in user's current UI language"
|
||||
},
|
||||
"draftAction": {
|
||||
"message": "Choose 'Yes' to load this draft or 'No' to discard it.",
|
||||
"description": "Displayed in the editor after the browser/extension crashed"
|
||||
},
|
||||
"dragDropMessage": {
|
||||
"message": "Drop your backup file anywhere on this page to import.",
|
||||
"description": "Drag'n'drop message"
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/* global API msg */// msg.js
|
||||
/* global addAPI bgReady */// common.js
|
||||
/* global createWorker */// worker-util.js
|
||||
/* global db */
|
||||
/* global prefs */
|
||||
/* global styleMan */
|
||||
/* global syncMan */
|
||||
|
@ -37,6 +38,11 @@ addAPI(/** @namespace API */ {
|
|||
},
|
||||
}))(),
|
||||
|
||||
/** @type IDBObjectStore */
|
||||
drafts: new Proxy({}, {
|
||||
get: (_, cmd) => (...args) => db.exec.call('drafts', cmd, ...args),
|
||||
}),
|
||||
|
||||
styles: styleMan,
|
||||
sync: syncMan,
|
||||
updater: updateMan,
|
||||
|
|
|
@ -2,17 +2,15 @@
|
|||
'use strict';
|
||||
|
||||
/* exported createChromeStorageDB */
|
||||
function createChromeStorageDB() {
|
||||
function createChromeStorageDB(PREFIX) {
|
||||
let INC;
|
||||
|
||||
const PREFIX = 'style-';
|
||||
const METHODS = {
|
||||
return {
|
||||
|
||||
delete(id) {
|
||||
return chromeLocal.remove(PREFIX + id);
|
||||
},
|
||||
|
||||
// FIXME: we don't use this method at all. Should we remove this?
|
||||
get(id) {
|
||||
return chromeLocal.getValue(PREFIX + id);
|
||||
},
|
||||
|
@ -59,8 +57,4 @@ function createChromeStorageDB() {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
return function dbExecChromeStorage(method, ...args) {
|
||||
return METHODS[method](...args);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -52,12 +52,18 @@ const db = (() => {
|
|||
console.warn('Failed to access indexedDB. Switched to storage API.', err);
|
||||
}
|
||||
await require(['/background/db-chrome-storage']); /* global createChromeStorageDB */
|
||||
return 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);
|
||||
};
|
||||
}
|
||||
|
||||
async function dbExecIndexedDB(method, ...args) {
|
||||
const mode = method.startsWith('get') ? 'readonly' : 'readwrite';
|
||||
const store = (await open()).transaction([STORE], mode).objectStore(STORE);
|
||||
const dbName = Object(this) instanceof String ? `${this}` : DATABASE;
|
||||
const store = (await open(dbName)).transaction([STORE], mode).objectStore(STORE);
|
||||
const fn = method === 'putMany' ? putMany : storeRequest;
|
||||
return fn(store, method, ...args);
|
||||
}
|
||||
|
@ -75,9 +81,9 @@ const db = (() => {
|
|||
return Promise.all(items.map(item => storeRequest(store, 'put', item)));
|
||||
}
|
||||
|
||||
function open() {
|
||||
function open(name) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = indexedDB.open(DATABASE, 2);
|
||||
const request = indexedDB.open(name, 2);
|
||||
request.onsuccess = () => resolve(request.result);
|
||||
request.onerror = reject;
|
||||
request.onupgradeneeded = create;
|
||||
|
|
|
@ -61,7 +61,13 @@ const styleMan = (() => {
|
|||
let ready = init();
|
||||
let order = {};
|
||||
|
||||
chrome.runtime.onConnect.addListener(handleLivePreview);
|
||||
chrome.runtime.onConnect.addListener(port => {
|
||||
if (port.name === 'livePreview') {
|
||||
handleLivePreview(port);
|
||||
} else if (port.name.startsWith('draft:')) {
|
||||
handleDraft(port);
|
||||
}
|
||||
});
|
||||
// function handleColorScheme() {
|
||||
colorScheme.onChange(() => {
|
||||
for (const {style: data} of dataMap.values()) {
|
||||
|
@ -106,6 +112,7 @@ const styleMan = (() => {
|
|||
// Must be called after the style is deleted from dataMap
|
||||
API.usw.revoke(id);
|
||||
}
|
||||
API.drafts.delete(id);
|
||||
await msg.broadcast({
|
||||
method: 'styleDeleted',
|
||||
style: {id},
|
||||
|
@ -371,10 +378,12 @@ const styleMan = (() => {
|
|||
style);
|
||||
}
|
||||
|
||||
function handleDraft(port) {
|
||||
const id = port.name.split(':').pop();
|
||||
port.onDisconnect.addListener(() => API.drafts.delete(Number(id) || id));
|
||||
}
|
||||
|
||||
function handleLivePreview(port) {
|
||||
if (port.name !== 'livePreview') {
|
||||
return;
|
||||
}
|
||||
let id;
|
||||
port.onMessage.addListener(style => {
|
||||
if (!id) id = style.id;
|
||||
|
|
35
edit/base.js
35
edit/base.js
|
@ -339,9 +339,15 @@ baseInit.ready.then(() => {
|
|||
function DirtyReporter() {
|
||||
const data = new Map();
|
||||
const listeners = new Set();
|
||||
const dataListeners = new Set();
|
||||
const notifyChange = wasDirty => {
|
||||
if (wasDirty !== (data.size > 0)) {
|
||||
listeners.forEach(cb => cb());
|
||||
const isDirty = data.size > 0;
|
||||
const flipped = isDirty !== wasDirty;
|
||||
if (flipped) {
|
||||
listeners.forEach(cb => cb(isDirty));
|
||||
}
|
||||
if (flipped || isDirty) {
|
||||
dataListeners.forEach(cb => cb(isDirty));
|
||||
}
|
||||
};
|
||||
/** @namespace DirtyReporter */
|
||||
|
@ -358,17 +364,19 @@ function DirtyReporter() {
|
|||
saved.newValue = value;
|
||||
saved.type = 'modify';
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
notifyChange(wasDirty);
|
||||
},
|
||||
clear(obj) {
|
||||
const wasDirty = data.size > 0;
|
||||
if (obj === undefined) {
|
||||
data.clear();
|
||||
} else {
|
||||
data.delete(obj);
|
||||
clear(...objs) {
|
||||
if (data.size && (
|
||||
objs.length
|
||||
? objs.map(data.delete, data).includes(true)
|
||||
: (data.clear(), true)
|
||||
)) {
|
||||
notifyChange(true);
|
||||
}
|
||||
notifyChange(wasDirty);
|
||||
},
|
||||
has(key) {
|
||||
return data.has(key);
|
||||
|
@ -382,6 +390,8 @@ function DirtyReporter() {
|
|||
if (!saved) {
|
||||
if (oldValue !== newValue) {
|
||||
data.set(obj, {type: 'modify', savedValue: oldValue, newValue});
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
} else if (saved.type === 'modify') {
|
||||
if (saved.savedValue === newValue) {
|
||||
|
@ -391,12 +401,17 @@ function DirtyReporter() {
|
|||
}
|
||||
} else if (saved.type === 'add') {
|
||||
saved.newValue = newValue;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
notifyChange(wasDirty);
|
||||
},
|
||||
onChange(cb, add = true) {
|
||||
listeners[add ? 'add' : 'delete'](cb);
|
||||
},
|
||||
onDataChange(cb, add = true) {
|
||||
dataListeners[add ? 'add' : 'delete'](cb);
|
||||
},
|
||||
remove(obj, value) {
|
||||
const wasDirty = data.size > 0;
|
||||
const saved = data.get(obj);
|
||||
|
@ -406,6 +421,8 @@ function DirtyReporter() {
|
|||
data.delete(obj);
|
||||
} else if (saved.type === 'modify') {
|
||||
saved.type = 'remove';
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
notifyChange(wasDirty);
|
||||
},
|
||||
|
|
69
edit/drafts.js
Normal file
69
edit/drafts.js
Normal file
|
@ -0,0 +1,69 @@
|
|||
/* global messageBoxProxy */// dom.js
|
||||
/* global API */// msg.js
|
||||
/* global clamp debounce */// toolbox.js
|
||||
/* global editor */
|
||||
/* global prefs */
|
||||
/* global t */// localization.js
|
||||
'use strict';
|
||||
|
||||
(async function AutosaveDrafts() {
|
||||
const makeId = () => editor.style.id || 'new';
|
||||
let delay;
|
||||
let port;
|
||||
connectPort();
|
||||
|
||||
const draft = await API.drafts.get(makeId());
|
||||
if (draft && draft.isUsercss === editor.isUsercss) {
|
||||
const date = makeRelativeDate(draft.date);
|
||||
if (await messageBoxProxy.confirm(t('draftAction'), 'danger', t('draftTitle', date))) {
|
||||
await editor.replaceStyle(draft.style, draft);
|
||||
} else {
|
||||
API.drafts.delete(makeId());
|
||||
}
|
||||
}
|
||||
|
||||
editor.dirty.onChange(isDirty => isDirty ? connectPort() : port.disconnect());
|
||||
editor.dirty.onDataChange(isDirty => debounce(updateDraft, isDirty ? delay : 0));
|
||||
|
||||
prefs.subscribe('editor.autosaveDraft', (key, val) => {
|
||||
delay = clamp(val * 1000 | 0, 1000, 2 ** 32 - 1);
|
||||
const t = debounce.timers.get(updateDraft);
|
||||
if (t != null) debounce(updateDraft, t ? delay : 0);
|
||||
}, {runNow: true});
|
||||
|
||||
function connectPort() {
|
||||
port = chrome.runtime.connect({name: 'draft:' + makeId()});
|
||||
}
|
||||
|
||||
function makeRelativeDate(date) {
|
||||
let delta = (Date.now() - date) / 1000;
|
||||
if (delta >= 0 && Intl.RelativeTimeFormat) {
|
||||
for (const [span, unit, frac = 1] of [
|
||||
[60, 'second', 0],
|
||||
[60, 'minute', 0],
|
||||
[24, 'hour'],
|
||||
[7, 'day'],
|
||||
[4, 'week'],
|
||||
[12, 'month'],
|
||||
[1e99, 'year'],
|
||||
]) {
|
||||
if (delta < span) {
|
||||
return new Intl.RelativeTimeFormat({style: 'short'}).format(-delta.toFixed(frac), unit);
|
||||
}
|
||||
delta /= span;
|
||||
}
|
||||
}
|
||||
return date.toLocaleString();
|
||||
}
|
||||
|
||||
function updateDraft(isDirty = editor.dirty.isDirty()) {
|
||||
if (!isDirty) return;
|
||||
API.drafts.put({
|
||||
date: Date.now(),
|
||||
id: makeId(),
|
||||
isUsercss: editor.isUsercss,
|
||||
style: editor.getValue(true),
|
||||
si: editor.makeScrollInfo(),
|
||||
});
|
||||
}
|
||||
})();
|
25
edit/edit.js
25
edit/edit.js
|
@ -58,6 +58,7 @@ baseInit.ready.then(async () => {
|
|||
|
||||
require([
|
||||
'/edit/autocomplete',
|
||||
'/edit/drafts',
|
||||
'/edit/global-search',
|
||||
]);
|
||||
});
|
||||
|
@ -139,16 +140,7 @@ window.on('beforeunload', e => {
|
|||
prefs.set('windowPosition', pos);
|
||||
}
|
||||
sessionStore.windowPos = JSON.stringify(pos || {});
|
||||
sessionStore['editorScrollInfo' + editor.style.id] = JSON.stringify({
|
||||
scrollY: window.scrollY,
|
||||
cms: editor.getEditors().map(cm => /** @namespace EditorScrollInfo */({
|
||||
bookmarks: (cm.state.sublimeBookmarks || []).map(b => b.find()),
|
||||
focus: cm.hasFocus(),
|
||||
height: cm.display.wrapper.style.height.replace('100vh', ''),
|
||||
parentHeight: cm.display.wrapper.parentElement.offsetHeight,
|
||||
sel: cm.isClean() && [cm.doc.sel.ranges, cm.doc.sel.primIndex],
|
||||
})),
|
||||
});
|
||||
sessionStore['editorScrollInfo' + editor.style.id] = JSON.stringify(editor.makeScrollInfo());
|
||||
const activeElement = document.activeElement;
|
||||
if (activeElement) {
|
||||
// blurring triggers 'change' or 'input' event if needed
|
||||
|
@ -195,6 +187,19 @@ window.on('beforeunload', e => {
|
|||
}
|
||||
},
|
||||
|
||||
makeScrollInfo() {
|
||||
return {
|
||||
scrollY: window.scrollY,
|
||||
cms: editor.getEditors().map(cm => /** @namespace EditorScrollInfo */({
|
||||
bookmarks: (cm.state.sublimeBookmarks || []).map(b => b.find()),
|
||||
focus: cm.hasFocus(),
|
||||
height: cm.display.wrapper.style.height.replace('100vh', ''),
|
||||
parentHeight: cm.display.wrapper.parentElement.offsetHeight,
|
||||
sel: [cm.doc.sel.ranges, cm.doc.sel.primIndex],
|
||||
})),
|
||||
};
|
||||
},
|
||||
|
||||
async save() {
|
||||
if (dirty.isDirty()) {
|
||||
editor.saving = true;
|
||||
|
|
|
@ -86,15 +86,21 @@ function SectionsEditor() {
|
|||
: null;
|
||||
},
|
||||
|
||||
async replaceStyle(newStyle) {
|
||||
async replaceStyle(newStyle, draft) {
|
||||
const sameCode = styleSectionsEqual(newStyle, getModel());
|
||||
if (!sameCode && !await messageBoxProxy.confirm(t('styleUpdateDiscardChanges'))) {
|
||||
if (!sameCode && !draft && !await messageBoxProxy.confirm(t('styleUpdateDiscardChanges'))) {
|
||||
return;
|
||||
}
|
||||
dirty.clear();
|
||||
if (!draft) {
|
||||
dirty.clear();
|
||||
}
|
||||
// FIXME: avoid recreating all editors?
|
||||
if (!sameCode) {
|
||||
await initSections(newStyle.sections, {replace: true});
|
||||
await initSections(newStyle.sections, {
|
||||
keepDirty: draft,
|
||||
replace: true,
|
||||
si: draft && draft.si,
|
||||
});
|
||||
}
|
||||
editor.useSavedStyle(newStyle);
|
||||
updateLivePreview();
|
||||
|
@ -468,14 +474,15 @@ function SectionsEditor() {
|
|||
async function initSections(src, {
|
||||
focusOn = 0,
|
||||
replace = false,
|
||||
keepDirty = false, // used by import
|
||||
keepDirty = false,
|
||||
si = editor.scrollInfo,
|
||||
} = {}) {
|
||||
editor.ready = false;
|
||||
if (replace) {
|
||||
sections.forEach(s => s.remove(true));
|
||||
sections.length = 0;
|
||||
container.textContent = '';
|
||||
}
|
||||
let si = editor.scrollInfo;
|
||||
if (si && si.cms && si.cms.length === src.length) {
|
||||
si.scrollY2 = si.scrollY + window.innerHeight;
|
||||
container.style.height = si.scrollY2 + 'px';
|
||||
|
@ -503,9 +510,12 @@ function SectionsEditor() {
|
|||
if (!keepDirty) dirty.clear();
|
||||
if (i === focusOn) sections[i].cm.focus();
|
||||
}
|
||||
if (!si) requestAnimationFrame(fitToAvailableSpace);
|
||||
if (!si || si.cms.every(cm => !cm.height)) {
|
||||
requestAnimationFrame(fitToAvailableSpace);
|
||||
}
|
||||
container.style.removeProperty('height');
|
||||
setGlobalProgress();
|
||||
editor.ready = true;
|
||||
}
|
||||
|
||||
/** @param {EditorSection} section */
|
||||
|
|
|
@ -57,7 +57,13 @@ function SourceEditor() {
|
|||
closestVisible: () => cm,
|
||||
getEditors: () => [cm],
|
||||
getEditorTitle: () => '',
|
||||
getValue: () => cm.getValue(),
|
||||
getValue: asObject => asObject
|
||||
? {
|
||||
customName: style.customName,
|
||||
enabled: style.enabled,
|
||||
sourceCode: cm.getValue(),
|
||||
}
|
||||
: cm.getValue(),
|
||||
getSearchableInputs: () => [],
|
||||
prevEditor: nextPrevSection.bind(null, -1),
|
||||
nextEditor: nextPrevSection.bind(null, 1),
|
||||
|
@ -195,7 +201,7 @@ function SourceEditor() {
|
|||
cm.setPreprocessor((style.usercssData || {}).preprocessor);
|
||||
}
|
||||
|
||||
async function replaceStyle(newStyle) {
|
||||
async function replaceStyle(newStyle, draft) {
|
||||
dirty.clear('name');
|
||||
const sameCode = newStyle.sourceCode === cm.getValue();
|
||||
if (sameCode) {
|
||||
|
@ -207,19 +213,26 @@ function SourceEditor() {
|
|||
return;
|
||||
}
|
||||
|
||||
if (await messageBoxProxy.confirm(t('styleUpdateDiscardChanges'))) {
|
||||
if (draft || await messageBoxProxy.confirm(t('styleUpdateDiscardChanges'))) {
|
||||
editor.useSavedStyle(newStyle);
|
||||
if (!sameCode) {
|
||||
const cursor = cm.getCursor();
|
||||
const si0 = draft && draft.si.cms[0];
|
||||
const cursor = !si0 && cm.getCursor();
|
||||
cm.setValue(style.sourceCode);
|
||||
cm.setCursor(cursor);
|
||||
if (si0) {
|
||||
editor.applyScrollInfo(cm, si0);
|
||||
} else {
|
||||
cm.setCursor(cursor);
|
||||
}
|
||||
savedGeneration = cm.changeGeneration();
|
||||
}
|
||||
if (sameCode) {
|
||||
// the code is same but the environment is changed
|
||||
updateLivePreview();
|
||||
}
|
||||
dirty.clear();
|
||||
if (!draft) {
|
||||
dirty.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
/* global $ $create animateElement focusAccessibility moveFocus */// dom.js
|
||||
/* global clamp */// toolbox.js
|
||||
/* global t */// localization.js
|
||||
'use strict';
|
||||
|
||||
|
@ -84,10 +85,6 @@ messageBox.show = async ({
|
|||
messageBox._resolve = resolve;
|
||||
});
|
||||
|
||||
function clamp(value, min, max) {
|
||||
return Math.min(Math.max(value, min), max);
|
||||
}
|
||||
|
||||
function initOwnListeners() {
|
||||
let listening = false;
|
||||
let offsetX = 0;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/* global $$ $ $create focusAccessibility getEventKeyName moveFocus */// dom.js
|
||||
/* global debounce */// toolbox.js
|
||||
/* global clamp debounce */// toolbox.js
|
||||
/* global t */// localization.js
|
||||
'use strict';
|
||||
|
||||
|
@ -34,7 +34,7 @@
|
|||
const key = isSelect ? 'selectedIndex' : 'valueAsNumber';
|
||||
const old = el[key];
|
||||
const rawVal = old + Math.sign(event.deltaY) * (el.step || 1);
|
||||
el[key] = Math.max(el.min || 0, Math.min(el.max || el.length - 1, rawVal));
|
||||
el[key] = clamp(rawVal, el.min || 0, el.max || el.length - 1);
|
||||
if (el[key] !== old) {
|
||||
el.dispatchEvent(new Event('change', {bubbles: true}));
|
||||
}
|
||||
|
|
|
@ -99,6 +99,7 @@
|
|||
'editor.selectByTokens': true,
|
||||
|
||||
'editor.appliesToLineWidget': true, // show applies-to line widget on the editor
|
||||
'editor.autosaveDraft': 10, // seconds
|
||||
'editor.livePreview': true,
|
||||
|
||||
// show CSS colors as clickable colored rectangles
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
CHROME_POPUP_BORDER_BUG
|
||||
RX_META
|
||||
capitalize
|
||||
clamp
|
||||
closeCurrentTab
|
||||
deepEqual
|
||||
download
|
||||
|
@ -132,6 +133,10 @@ if (FIREFOX || OPERA || VIVALDI) {
|
|||
// (detecting FF57 by the feature it added, not navigator.ua which may be spoofed in about:config)
|
||||
const openerTabIdSupported = (!FIREFOX || window.AbortController) && chrome.windows != null;
|
||||
|
||||
function clamp(value, min, max) {
|
||||
return Math.min(Math.max(value, min), max);
|
||||
}
|
||||
|
||||
function getOwnTab() {
|
||||
return browser.tabs.getCurrent();
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
OPERA
|
||||
URLS
|
||||
capitalize
|
||||
clamp
|
||||
ignoreChromeError
|
||||
openURL
|
||||
*/// toolbox.js
|
||||
|
@ -288,7 +289,7 @@ function enforceInputRange(element) {
|
|||
if (type === 'input' && element.checkValidity()) {
|
||||
doNotify();
|
||||
} else if (type === 'change' && !element.checkValidity()) {
|
||||
element.value = Math.max(min, Math.min(max, Number(element.value)));
|
||||
element.value = clamp(Number(element.value), min, max);
|
||||
doNotify();
|
||||
}
|
||||
};
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
FIREFOX
|
||||
URLS
|
||||
capitalize
|
||||
clamp
|
||||
getActiveTab
|
||||
isEmptyObj
|
||||
*/// toolbox.js
|
||||
|
@ -66,7 +67,7 @@ function onRuntimeMessage(msg) {
|
|||
|
||||
function setPopupWidth(_key, width) {
|
||||
document.body.style.width =
|
||||
Math.max(200, Math.min(800, width)) + 'px';
|
||||
clamp(width, 200, 800) + 'px';
|
||||
}
|
||||
|
||||
function toggleSideBorders(_key, state) {
|
||||
|
|
Loading…
Reference in New Issue
Block a user