Compare commits
67 Commits
master
...
mot-new-ma
Author | SHA1 | Date | |
---|---|---|---|
|
5047ceb1fa | ||
|
2fd08cb041 | ||
|
67e886bd45 | ||
|
9224c7ab6c | ||
|
48b1877b44 | ||
|
89644d0624 | ||
|
84163ba64f | ||
|
cd309aae03 | ||
|
6c747faccf | ||
|
0dfd6680f8 | ||
|
71baf445e0 | ||
|
7d7e7e9f72 | ||
|
f9ccd3eeee | ||
|
44df29613f | ||
|
67682fa856 | ||
|
bd117b8fd7 | ||
|
1bc84bbb49 | ||
|
5cc9e68d69 | ||
|
99f8bbb48f | ||
|
3042054287 | ||
|
67593416fb | ||
|
ebeaba3478 | ||
|
ae6bace200 | ||
|
7abc4f7fe6 | ||
|
30a780a44e | ||
|
e9510c01b7 | ||
|
9b408aaad4 | ||
|
0d87689078 | ||
|
bc3f2e0fcf | ||
|
e873ffd84e | ||
|
c966cfe17e | ||
|
e547d93fdc | ||
|
9fd4e0f57d | ||
|
b5387deb9a | ||
|
403049692c | ||
|
a0ba63bb19 | ||
|
ead5e747b5 | ||
|
44889f6158 | ||
|
a7026bdeee | ||
|
30a69f5bea | ||
|
198315c626 | ||
|
218b6b41ec | ||
|
42a75780d5 | ||
|
596d6a9ca9 | ||
|
8f87494dec | ||
|
d2930e5e66 | ||
|
331be7aa2b | ||
|
bc404e821f | ||
|
529172de5b | ||
|
cf4d4a2e91 | ||
|
de7b0f44f1 | ||
|
e3be7bf18f | ||
|
57c55896e8 | ||
|
9a314523f6 | ||
|
d379e5f34a | ||
|
9368c27990 | ||
|
52f012daf5 | ||
|
0e7ff1c78f | ||
|
7cd84038bf | ||
|
cf695c73d6 | ||
|
17e1860ba6 | ||
|
a1b78476bb | ||
|
f57af7929f | ||
|
fbcc7aac08 | ||
|
5c38441393 | ||
|
ed07cb8460 | ||
|
4475a8ad6a |
|
@ -3,6 +3,14 @@
|
|||
"message": "Write new style",
|
||||
"description": "Label for the button to go to the add style page"
|
||||
},
|
||||
"addPlainStyleLabel": {
|
||||
"message": "New plain style",
|
||||
"description": "Label for the button to go to the add style page"
|
||||
},
|
||||
"addUserCSSStyleLabel": {
|
||||
"message": "New UserCSS style",
|
||||
"description": "Label for the button to go to the add style page"
|
||||
},
|
||||
"addStyleTitle": {
|
||||
"message": "Add Style",
|
||||
"description": "Title of the page for adding styles"
|
||||
|
@ -24,10 +32,6 @@
|
|||
},
|
||||
"description": "Text on the manage screen to describe what the style applies to"
|
||||
},
|
||||
"appliesDisplayTruncatedSuffix": {
|
||||
"message": "and more",
|
||||
"description": "Text added to appliesDisplay when there are more sites for the style than are displayed"
|
||||
},
|
||||
"appliesDomainOption": {
|
||||
"message": "URLs on the domain",
|
||||
"description": "Option to make the style apply to the entered string as a domain"
|
||||
|
@ -88,12 +92,17 @@
|
|||
"message": "Backup",
|
||||
"description": "Heading for backup"
|
||||
},
|
||||
"backupImport": {
|
||||
"message": "Import backup",
|
||||
"description": "Tooltip for header import/restore backup icon"
|
||||
},
|
||||
"backupMessage": {
|
||||
"message": "Select a file or drag and drop to this page.",
|
||||
"description": "Message for backup"
|
||||
},
|
||||
"bckpInstStyles": {
|
||||
"message": "Export styles"
|
||||
"message": "Local device",
|
||||
"description": "Selected option to backup indicated styles to local device/drive"
|
||||
},
|
||||
"checkAllUpdates": {
|
||||
"message": "Check all styles for updates",
|
||||
|
@ -335,7 +344,11 @@
|
|||
},
|
||||
"exportLabel": {
|
||||
"message": "Export",
|
||||
"description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)"
|
||||
"description": "Label for the button to export a style ('edit' page) or bulk styles ('manage' page)"
|
||||
},
|
||||
"exportAllLabel": {
|
||||
"message": "Export All",
|
||||
"description": "Label for the button to export all styles ('manage' page)"
|
||||
},
|
||||
"externalFeedback": {
|
||||
"message": "Feedback",
|
||||
|
@ -405,6 +418,10 @@
|
|||
"message": "Enabled",
|
||||
"description": "Used in various lists/options to indicate that something is enabled"
|
||||
},
|
||||
"genericFilterLabel": {
|
||||
"message": "Filter",
|
||||
"description": "Used in various lists/options to indicate that something is or will be filtered"
|
||||
},
|
||||
"genericError": {
|
||||
"message": "Error",
|
||||
"description": "Used in various places to indicate some error occurred."
|
||||
|
@ -413,6 +430,10 @@
|
|||
"message": "History",
|
||||
"description": "Used in various places to show a history log of something"
|
||||
},
|
||||
"genericName": {
|
||||
"message": "Name",
|
||||
"description": "Used in various places to indicate the style name"
|
||||
},
|
||||
"genericNext": {
|
||||
"message": "Next",
|
||||
"description": "Used in various places to select/perform the next step/action"
|
||||
|
@ -554,6 +575,10 @@
|
|||
"message": "Get help",
|
||||
"description": "Homepage link text on the manage page e.g. https://add0n.com/stylus.html#features with chat/FAQ/intro/info"
|
||||
},
|
||||
"linkChat": {
|
||||
"message": "Chat with us",
|
||||
"description": "Link to open a new browser tab to chat with users & developers on Discord"
|
||||
},
|
||||
"linkGetStyles": {
|
||||
"message": "Get styles",
|
||||
"description": "Help link text on the manage page e.g. https://userstyles.org"
|
||||
|
@ -1164,7 +1189,8 @@
|
|||
"description": "Label before the replace-with input field in the editor shown on Ctrl-H etc."
|
||||
},
|
||||
"retrieveBckp": {
|
||||
"message": "Import styles"
|
||||
"message": "Local source",
|
||||
"description": "Selected option to get a backup of styles from a local device/drive"
|
||||
},
|
||||
"search": {
|
||||
"message": "Search",
|
||||
|
@ -1214,6 +1240,26 @@
|
|||
"message": "</> key focuses the search field.\nPlain text: search within the name, code, homepage URL and sites it is applied to. Words with less than 3 letters are ignored.\nStyles matching a full URL: prefix the search with <url:>, e.g. <url:https://github.com/openstyles/stylus>\nRegular expressions: include slashes and flags, e.g. </body.*?\\ba\\b/simguy>\nExact words: wrap the query in double quotes, e.g. <\".header ~ div\">",
|
||||
"description": "Text in the minihelp displayed when clicking (i) icon to the right of the search input field on the Manage styles page"
|
||||
},
|
||||
"bulkActions": {
|
||||
"message": "Apply actions to selected styles",
|
||||
"description": "Label for bulk actions select dropdown"
|
||||
},
|
||||
"bulkActionsSelect": {
|
||||
"message": "Choose a bulk action",
|
||||
"description": "Placeholder text in dropdown to tell the user to choose an action to apply to all selected styles"
|
||||
},
|
||||
"bulkActionsApply": {
|
||||
"message": "Apply",
|
||||
"description": "Text for button to apply the selected action"
|
||||
},
|
||||
"bulkActionsTooltip": {
|
||||
"message": "Bulk actions can be applied to selected styles in this column",
|
||||
"description": "Select style for bulk action header tooltip"
|
||||
},
|
||||
"bulkActionsError": {
|
||||
"message": "Choose at least one style",
|
||||
"description": "Error displayed in a tooltip when the user attempts to apply an action with no styles selected"
|
||||
},
|
||||
"sectionAdd": {
|
||||
"message": "Add another section",
|
||||
"description": "Label for the button to add a section"
|
||||
|
@ -1237,33 +1283,34 @@
|
|||
"shortcutsNote": {
|
||||
"message": "Define keyboard shortcuts"
|
||||
},
|
||||
"sortDateNewestFirst": {
|
||||
"message": "newest first",
|
||||
"description": "Text added to indicate that sorting a date would add the newest entries at the top"
|
||||
"sortColumnEnabled": {
|
||||
"message": "enabled styles",
|
||||
"description": "Column name seen in the tooltip when hovering over the header; used to replace the 'sortLabel` placeholder"
|
||||
},
|
||||
"sortDateOldestFirst": {
|
||||
"message": "oldest first",
|
||||
"description": "Text added to indicate that sorting a date would add the oldest entries at the top"
|
||||
"sortColumnName": {
|
||||
"message": "style name",
|
||||
"description": "Column name seen in the tooltip when hovering over the header; used to replace the 'sortLabel` placeholder"
|
||||
},
|
||||
"sortColumnVersion": {
|
||||
"message": "style version",
|
||||
"description": "Column name seen in the tooltip when hovering over the header; used to replace the 'sortLabel` placeholder"
|
||||
},
|
||||
"sortColumnLastUpdate": {
|
||||
"message": "last updated",
|
||||
"description": "Column name seen in the tooltip when hovering over the header; used to replace the 'sortLabel` placeholder"
|
||||
},
|
||||
"sortHeader": {
|
||||
"message": "Sort",
|
||||
"description": "Title of sort column, indicating that the style can be manually sorted by dragging & dropping"
|
||||
},
|
||||
"sortLabel": {
|
||||
"message": "Select a sort to apply to the installed styles",
|
||||
"description": "Title on the sort select to indicate it is used for sorting entries"
|
||||
"message": "Click to sort the \"$name$\" column;\nUse shift + click to sort multiple columns",
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"content": "$1"
|
||||
}
|
||||
},
|
||||
"sortLabelTitleAsc": {
|
||||
"message": "Title Ascending",
|
||||
"description": "Text added to option group to indicate a block of options that apply a title ascending (A to Z) sort"
|
||||
},
|
||||
"sortLabelTitleDesc": {
|
||||
"message": "Title Descending",
|
||||
"description": "Text added to option group to indicate a block of options that apply a title descending (Z to A) sort"
|
||||
},
|
||||
"sortStylesHelp": {
|
||||
"message": "Choose the type of sort to apply to the installed entries from within the sort dropdown. The default setting applies an ascending sort (A to Z) to the entry titles. Sorts within the \"Title Descending\" group will apply a descending sort (Z to A) to the title.\nThere are other presets that will allow sorting the entries by multiple criteria. Think of this like sorting a table with multiple columns and each category in a select (between the plus signs) represents a column, or group.\nFor example, if the setting is \"Enabled (first) + Title\", the entries would sort so that all the enabled entries are sorted to the top of the list, then an entry title ascending sort (A to Z) is applied to both the enabled and disabled entries separately.",
|
||||
"description": "Text in the minihelp displayed when clicking (i) icon to the right of the sort input field on the Manage styles page"
|
||||
},
|
||||
"sortStylesHelpTitle": {
|
||||
"message": "Sort contents",
|
||||
"description": "Label for the sort info popup on the Manage styles page"
|
||||
"description": "Title added to links in the manager page header to inform the user on how to sort the columns"
|
||||
},
|
||||
"styleBadRegexp": {
|
||||
"message": "Regexp is invalid.",
|
||||
|
|
|
@ -4,13 +4,108 @@
|
|||
(() => {
|
||||
// toLocaleLowerCase cache, autocleared after 1 minute
|
||||
const cache = new Map();
|
||||
// top-level style properties to be searched
|
||||
const PARTS = {
|
||||
name: searchText,
|
||||
url: searchText,
|
||||
sourceCode: searchText,
|
||||
sections: searchSections,
|
||||
};
|
||||
|
||||
// Creates an array of intermediate words (2 letter minimum)
|
||||
// 'usercss' => ["us", "use", "user", "userc", "usercs", "usercss"]
|
||||
// this makes it so the user can type partial queries and not have the search
|
||||
// constantly switching between using & ignoring the filter
|
||||
const createPartials = id => id.split('').reduce((acc, _, index) => {
|
||||
if (index > 0) {
|
||||
acc.push(id.substring(0, index + 1));
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
const searchWithin = [{
|
||||
id: 'code',
|
||||
labels: createPartials('code'),
|
||||
get: style => style.sections.map(section => section.code).join(' ')
|
||||
}, {
|
||||
id: 'usercss',
|
||||
labels: [...createPartials('usercss'), ...createPartials('meta')],
|
||||
get: style => JSON.stringify(style.usercssData || {})
|
||||
// remove JSON structure; restore urls
|
||||
.replace(/[[\]{},":]/g, ' ').replace(/\s\/\//g, '://')
|
||||
}, {
|
||||
id: 'name', // default
|
||||
labels: createPartials('name'),
|
||||
get: style => style.name
|
||||
}];
|
||||
|
||||
const styleProps = [{
|
||||
id: 'enabled',
|
||||
labels: ['on', ...createPartials('enabled')],
|
||||
check: style => style.enabled
|
||||
}, {
|
||||
id: 'disabled',
|
||||
labels: ['off', ...createPartials('disabled')],
|
||||
check: style => !style.enabled
|
||||
}, {
|
||||
id: 'local',
|
||||
labels: createPartials('local'),
|
||||
check: style => !style.updateUrl
|
||||
}, {
|
||||
id: 'external',
|
||||
labels: createPartials('external'),
|
||||
check: style => style.updateUrl
|
||||
}, {
|
||||
id: 'usercss',
|
||||
labels: createPartials('usercss'),
|
||||
check: style => style.usercssData
|
||||
}, {
|
||||
id: 'non usercss',
|
||||
labels: ['original', ...createPartials('nonusercss')],
|
||||
check: style => !style.usercssData
|
||||
}];
|
||||
|
||||
const matchers = [{
|
||||
id: 'url',
|
||||
test: query => /url:\w+/i.test(query),
|
||||
matches: query => {
|
||||
const matchUrl = query.match(/url:([/.-_\w]+)/);
|
||||
const result = matchUrl && matchUrl[1]
|
||||
? styleManager.getStylesByUrl(matchUrl[1])
|
||||
.then(result => result.map(r => r.data.id))
|
||||
: [];
|
||||
return {result};
|
||||
},
|
||||
}, {
|
||||
id: 'regex',
|
||||
test: query => {
|
||||
const x = query.includes('/') && !query.includes('//') &&
|
||||
/^\/(.+?)\/([gimsuy]*)$/.test(query);
|
||||
// console.log('regex match?', query, x);
|
||||
return x;
|
||||
},
|
||||
matches: () => ({regex: tryRegExp(RegExp.$1, RegExp.$2)})
|
||||
}, {
|
||||
id: 'props',
|
||||
test: query => /is:/.test(query),
|
||||
matches: query => {
|
||||
const label = /is:(\w+)/g.exec(query);
|
||||
return label && label[1]
|
||||
? {prop: styleProps.find(p => p.labels.includes(label[1]))}
|
||||
: {};
|
||||
}
|
||||
}, {
|
||||
id: 'within',
|
||||
test: query => /in:/.test(query),
|
||||
matches: query => {
|
||||
const label = /in:(\w+)/g.exec(query);
|
||||
return label && label[1]
|
||||
? {within: searchWithin.find(s => s.labels.includes(label[1]))}
|
||||
: {};
|
||||
}
|
||||
}, {
|
||||
id: 'default',
|
||||
test: () => true,
|
||||
matches: query => {
|
||||
const word = query.startsWith('"') && query.endsWith('"')
|
||||
? query.slice(1, -1)
|
||||
: query;
|
||||
return {word: word || query};
|
||||
}
|
||||
}];
|
||||
|
||||
/**
|
||||
* @param params
|
||||
|
@ -19,77 +114,94 @@
|
|||
* @returns {number[]} - array of matched styles ids
|
||||
*/
|
||||
API_METHODS.searchDB = ({query, ids}) => {
|
||||
let rx, words, icase, matchUrl;
|
||||
query = query.trim();
|
||||
const parts = query.trim().split(/(".*?")|\s+/).filter(Boolean);
|
||||
|
||||
if (/^url:/i.test(query)) {
|
||||
matchUrl = query.slice(query.indexOf(':') + 1).trim();
|
||||
if (matchUrl) {
|
||||
return styleManager.getStylesByUrl(matchUrl)
|
||||
.then(results => results.map(r => r.data.id));
|
||||
const searchFilters = {
|
||||
words: [],
|
||||
regex: null, // only last regex expression is used
|
||||
results: [],
|
||||
props: [],
|
||||
within: [],
|
||||
};
|
||||
|
||||
const searchText = (text, searchFilters) => {
|
||||
if (searchFilters.regex) return searchFilters.regex.test(text);
|
||||
for (let pass = 1; pass <= (searchFilters.icase ? 2 : 1); pass++) {
|
||||
if (searchFilters.words.every(w => text.includes(w))) return true;
|
||||
text = lower(text);
|
||||
}
|
||||
};
|
||||
|
||||
const searchProps = (style, searchFilters) => {
|
||||
const x = searchFilters.props.every(prop => {
|
||||
const y = prop.check(style)
|
||||
// if (y) console.log('found prop', prop.id, style.id)
|
||||
return y;
|
||||
});
|
||||
// if (x) console.log('found prop', style.id)
|
||||
return x;
|
||||
};
|
||||
|
||||
parts.forEach(part => {
|
||||
matchers.some(matcher => {
|
||||
if (matcher.test(part)) {
|
||||
const {result, regex, word, prop, within} = matcher.matches(part || '');
|
||||
if (result) searchFilters.results.push(result);
|
||||
if (regex) searchFilters.regex = regex; // limited to a single regexp
|
||||
if (word) searchFilters.words.push(word);
|
||||
if (prop) searchFilters.props.push(prop);
|
||||
if (within) searchFilters.within.push(within);
|
||||
return true;
|
||||
}
|
||||
if (query.startsWith('/') && /^\/(.+?)\/([gimsuy]*)$/.test(query)) {
|
||||
rx = tryRegExp(RegExp.$1, RegExp.$2);
|
||||
});
|
||||
});
|
||||
if (!searchFilters.within.length) {
|
||||
searchFilters.within.push(...searchWithin.slice(-1));
|
||||
}
|
||||
if (!rx) {
|
||||
words = query
|
||||
.split(/(".*?")|\s+/)
|
||||
.filter(Boolean)
|
||||
.map(w => w.startsWith('"') && w.endsWith('"')
|
||||
? w.slice(1, -1)
|
||||
: w)
|
||||
.filter(w => w.length > 1);
|
||||
words = words.length ? words : [query];
|
||||
icase = words.some(w => w === lower(w));
|
||||
|
||||
// console.log('matchers', searchFilters);
|
||||
// url matches
|
||||
if (searchFilters.results.length) {
|
||||
return searchFilters.results;
|
||||
}
|
||||
searchFilters.icase = searchFilters.words.some(w => w === lower(w));
|
||||
query = parts.join(' ').trim();
|
||||
|
||||
return styleManager.getAllStyles().then(styles => {
|
||||
if (ids) {
|
||||
const idSet = new Set(ids);
|
||||
styles = styles.filter(s => idSet.has(s.id));
|
||||
}
|
||||
|
||||
const results = [];
|
||||
const propResults = [];
|
||||
const hasProps = searchFilters.props.length > 0;
|
||||
const noWords = searchFilters.words.length === 0;
|
||||
for (const style of styles) {
|
||||
const id = style.id;
|
||||
if (!query || words && !words.length) {
|
||||
if (noWords) {
|
||||
// no query or only filters are matching -> show all styles
|
||||
results.push(id);
|
||||
continue;
|
||||
}
|
||||
for (const part in PARTS) {
|
||||
const text = style[part];
|
||||
if (text && PARTS[part](text, rx, words, icase)) {
|
||||
} else {
|
||||
const text = searchFilters.within.map(within => within.get(style)).join(' ');
|
||||
if (searchText(text, searchFilters)) {
|
||||
results.push(id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (hasProps && searchProps(style, searchFilters) && results.includes(id)) {
|
||||
propResults.push(id);
|
||||
}
|
||||
}
|
||||
// results AND propResults
|
||||
const finalResults = hasProps
|
||||
? propResults.filter(id => results.includes(id))
|
||||
: results;
|
||||
if (cache.size) debounce(clearCache, 60e3);
|
||||
return results;
|
||||
// console.log('final', finalResults)
|
||||
return finalResults;
|
||||
});
|
||||
};
|
||||
|
||||
function searchText(text, rx, words, icase) {
|
||||
if (rx) return rx.test(text);
|
||||
for (let pass = 1; pass <= (icase ? 2 : 1); pass++) {
|
||||
if (words.every(w => text.includes(w))) return true;
|
||||
text = lower(text);
|
||||
}
|
||||
}
|
||||
|
||||
function searchSections(sections, rx, words, icase) {
|
||||
for (const section of sections) {
|
||||
for (const prop in section) {
|
||||
const value = section[prop];
|
||||
if (typeof value === 'string') {
|
||||
if (searchText(value, rx, words, icase)) return true;
|
||||
} else if (Array.isArray(value)) {
|
||||
if (value.some(str => searchText(str, rx, words, icase))) return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function lower(text) {
|
||||
let result = cache.get(text);
|
||||
if (result) return result;
|
||||
|
|
|
@ -31,7 +31,7 @@
|
|||
|
||||
const retrying = new Set();
|
||||
|
||||
API_METHODS.updateCheckAll = checkAllStyles;
|
||||
API_METHODS.updateCheckBulk = checkBulkStyles;
|
||||
API_METHODS.updateCheck = checkStyle;
|
||||
API_METHODS.getUpdaterStates = () => STATES;
|
||||
|
||||
|
@ -39,18 +39,22 @@
|
|||
schedule();
|
||||
chrome.alarms.onAlarm.addListener(onAlarm);
|
||||
|
||||
return {checkAllStyles, checkStyle, STATES};
|
||||
return {checkBulkStyles, checkStyle, STATES};
|
||||
|
||||
function checkAllStyles({
|
||||
function checkBulkStyles({
|
||||
save = true,
|
||||
ignoreDigest,
|
||||
observe,
|
||||
styleIds = [],
|
||||
} = {}) {
|
||||
resetInterval();
|
||||
checkingAll = true;
|
||||
retrying.clear();
|
||||
const port = observe && chrome.runtime.connect({name: 'updater'});
|
||||
return styleManager.getAllStyles().then(styles => {
|
||||
if (styleIds.length) {
|
||||
styles = styles.filter(style => styleIds.includes(style.id));
|
||||
}
|
||||
styles = styles.filter(style => style.updateUrl);
|
||||
if (port) port.postMessage({count: styles.length});
|
||||
log('');
|
||||
|
@ -247,7 +251,7 @@
|
|||
}
|
||||
|
||||
function onAlarm({name}) {
|
||||
if (name === ALARM_NAME) checkAllStyles();
|
||||
if (name === ALARM_NAME) checkBulkStyles();
|
||||
}
|
||||
|
||||
function resetInterval() {
|
||||
|
|
|
@ -136,6 +136,7 @@ select {
|
|||
color: #000;
|
||||
background-color: transparent;
|
||||
border: 1px solid hsl(0, 0%, 66%);
|
||||
border-radius: 2px;
|
||||
padding: 0 20px 0 6px;
|
||||
transition: color .5s;
|
||||
}
|
||||
|
|
|
@ -7,6 +7,11 @@ tDocLoader();
|
|||
|
||||
|
||||
function t(key, params) {
|
||||
if (!params && key.includes(';')) {
|
||||
[key, params] = key.split(';');
|
||||
// sometimes a param like "usercss" is passed; not defined in messages.json
|
||||
params = params ? chrome.i18n.getMessage(params) || params : '';
|
||||
}
|
||||
const cache = !params && t.cache[key];
|
||||
const s = cache || chrome.i18n.getMessage(key, params);
|
||||
if (s === '') {
|
||||
|
|
|
@ -18,9 +18,9 @@ if (!CHROME && !chrome.browserAction.openPopup) {
|
|||
// in FF pre-57 legacy addons can override useragent so we assume the worst
|
||||
// until we know for sure in the async getBrowserInfo()
|
||||
// (browserAction.openPopup was added in 57)
|
||||
FIREFOX = browser.runtime.getBrowserInfo ? 51 : 50;
|
||||
FIREFOX = browserApi.runtime.getBrowserInfo ? 51 : 50;
|
||||
// getBrowserInfo was added in FF 51
|
||||
Promise.resolve(FIREFOX >= 51 ? browser.runtime.getBrowserInfo() : {version: 50}).then(info => {
|
||||
Promise.resolve(FIREFOX >= 51 ? browserApi.runtime.getBrowserInfo() : {version: 50}).then(info => {
|
||||
FIREFOX = parseFloat(info.version);
|
||||
document.documentElement.classList.add('moz-appearance-bug', FIREFOX && FIREFOX < 54);
|
||||
});
|
||||
|
|
12
js/prefs.js
12
js/prefs.js
|
@ -27,15 +27,11 @@ self.prefs = self.INJECTED === 1 ? self.prefs : (() => {
|
|||
'manage.onlyEnabled.invert': false, // display only disabled styles
|
||||
'manage.onlyLocal.invert': false, // display only externally installed styles
|
||||
'manage.onlyUsercss.invert': false, // display only non-usercss (standard) styles
|
||||
// UI element state: expanded/collapsed
|
||||
'manage.backup.expanded': true,
|
||||
'manage.filters.expanded': true,
|
||||
'manage.options.expanded': true,
|
||||
// the new compact layout doesn't look good on Android yet
|
||||
'manage.newUI': !navigator.appVersion.includes('Android'),
|
||||
'manage.newUI.favicons': false, // show favicons for the sites in applies-to
|
||||
'manage.export.destination': 'local', // default export destination (local or dropbox)
|
||||
|
||||
'manage.newUI.favicons': true, // show favicons for the sites in applies-to
|
||||
'manage.newUI.faviconsGray': true, // gray out favicons
|
||||
'manage.newUI.targets': 3, // max number of applies-to targets visible: 0 = none
|
||||
'manage.newUI.targets': 6, // max number of applies-to targets visible: 0 = none
|
||||
'manage.newUI.sort': 'title,asc',
|
||||
|
||||
'editor.options': {}, // CodeMirror.defaults.*
|
||||
|
|
756
manage.html
756
manage.html
|
@ -1,4 +1,4 @@
|
|||
<html id="stylus">
|
||||
<html id="stylus" class="newUI">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
|
@ -6,12 +6,11 @@
|
|||
<title i18n-text="manageTitle"></title>
|
||||
<link rel="stylesheet" href="global.css">
|
||||
<link rel="stylesheet" href="manage/manage.css">
|
||||
<link rel="stylesheet" href="manage/config-dialog.css">
|
||||
<link rel="stylesheet" href="msgbox/msgbox.css">
|
||||
<link rel="stylesheet" href="options/onoffswitch.css">
|
||||
<link rel="stylesheet" href="vendor-overwrites/colorpicker/colorpicker.css">
|
||||
|
||||
<style id="style-overrides"></style>
|
||||
<link rel="stylesheet" data-href="manage/config-dialog.css">
|
||||
<link rel="stylesheet" data-href="msgbox/msgbox.css">
|
||||
<link rel="stylesheet" data-href="options/onoffswitch.css">
|
||||
<link rel="stylesheet" data-href="vendor-overwrites/colorpicker/colorpicker.css">
|
||||
<link rel="stylesheet" data-href="manage/tooltips.css">
|
||||
|
||||
<style id="firefox-transitions-bug-suppressor">
|
||||
/* restrict to FF */
|
||||
|
@ -30,77 +29,96 @@
|
|||
-->
|
||||
|
||||
<template data-id="style">
|
||||
<div class="entry">
|
||||
<h2 class="style-name">
|
||||
<a class="style-name-link"></a>
|
||||
<a target="_blank" class="homepage"></a>
|
||||
</h2>
|
||||
<p class="applies-to">
|
||||
<label i18n-text="appliesDisplay"></label>
|
||||
<span class="targets"></span>
|
||||
</p>
|
||||
<p class="actions">
|
||||
<a class="style-edit-link">
|
||||
<button i18n-text="editStyleLabel" tabindex="-1"></button>
|
||||
<div class="entry hide-extra">
|
||||
<label class="entry-col entry-filter checkmate" tabindex="0">
|
||||
<span class="col-label" i18n-text="genericFilterLabel"></span>
|
||||
<input class="entry-filter-toggle" type="checkbox">
|
||||
<svg class="svg-icon checkbox"viewBox="0 0 10 10">
|
||||
<path class="filled-circle" d="M5 .86a4.14 4.14 0 0 0 0 8.28A4.14 4.14 0 1 0 5 .86z"/>
|
||||
<path class="circle" d="M5 .86a4.14 4.14 0 0 0 0 8.28A4.14 4.14 0 1 0 5 .86zm0 7.5a3.36 3.36 0 1 1 0-6.72 3.36 3.36 0 0 1 0 6.72z"/>
|
||||
<path class="checkmark" d="M6.86 3.21L4.14 5.93 3.07 4.86l-.57.57 1.64 1.71L7.5 3.8l-.64-.58z"/>
|
||||
</svg>
|
||||
</label>
|
||||
<label class="entry-col entry-state checkmate" tabindex="0">
|
||||
<span class="col-label" i18n-text="genericEnabledLabel"></span>
|
||||
<input class="entry-state-toggle" type="checkbox">
|
||||
<svg class="svg-icon checkbox"viewBox="0 0 10 10">
|
||||
<path class="filled-circle" d="M5 .86a4.14 4.14 0 0 0 0 8.28A4.14 4.14 0 1 0 5 .86z"/>
|
||||
<path class="circle" d="M5 .86a4.14 4.14 0 0 0 0 8.28A4.14 4.14 0 1 0 5 .86zm0 7.5a3.36 3.36 0 1 1 0-6.72 3.36 3.36 0 0 1 0 6.72z"/>
|
||||
<path class="checkmark" d="M6.86 3.21L4.14 5.93 3.07 4.86l-.57.57 1.64 1.71L7.5 3.8l-.64-.58z"/>
|
||||
</svg>
|
||||
</label>
|
||||
<a href="#" class="entry-col entry-name">
|
||||
<span class="col-label" i18n-text="genericName"></span>
|
||||
<span class="entry-name-text"></span>
|
||||
<span class="entry-labels"></span>
|
||||
</a>
|
||||
<button class="enable" i18n-text="enableStyleLabel"></button>
|
||||
<button class="disable" i18n-text="disableStyleLabel"></button>
|
||||
<button class="delete" i18n-text="deleteStyleLabel"></button>
|
||||
<button class="check-update" i18n-text="checkForUpdate"></button>
|
||||
<button class="update" i18n-text="installUpdate"></button>
|
||||
<button class="configure-usercss" i18n-text="configureStyle"></button>
|
||||
<span class="update-note"></span>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template data-id="styleCompact">
|
||||
<div class="entry">
|
||||
<h2 class="style-name">
|
||||
<div class="checkmate">
|
||||
<input class="checker" type="checkbox" i18n-title="toggleStyle">
|
||||
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
|
||||
</div>
|
||||
<a class="style-name-link"></a>
|
||||
</h2>
|
||||
<p class="actions">
|
||||
<a target="_blank" class="homepage" tabindex="0"></a>
|
||||
<a href="#" class="delete" i18n-title="deleteStyleLabel" tabindex="0">
|
||||
<svg class="svg-icon" viewBox="0 0 20 20">
|
||||
<polygon points="16.2,5.5 14.5,3.8 10,8.3 5.5,3.8 3.8,5.5 8.3,10 3.8,14.5
|
||||
5.5,16.2 10,11.7 14.5,16.2 16.2,14.5 11.7,10 "/>
|
||||
<div class="entry-col entry-actions">
|
||||
<span class="col-label" i18n-text="optionsActions"></span>
|
||||
<a href="#" class="entry-configure-usercss tt-e" i18n-data-title="configureStyle">
|
||||
<svg class="svg-icon entry-config" viewBox="0 0 24 24">
|
||||
<path d="M19.43 12.98a7.8 7.8 0 0 0 0-1.96l2.11-1.65a.5.5 0 0 0 .12-.64l-2-3.46a.5.5
|
||||
0 0 0-.61-.22l-2.49 1a7.3 7.3 0 0 0-1.69-.98l-.38-2.65A.49.49 0 0 0 14 2h-4a.49.49
|
||||
0 0 0-.49.42l-.38 2.65c-.61.25-1.17.59-1.69.98l-2.49-1a.57.57 0 0 0-.18-.03.5.5 0 0
|
||||
0-.43.25l-2 3.46a.5.5 0 0 0 .12.64l2.11 1.65a7.93 7.93 0 0 0 0 1.96l-2.11 1.65a.5.5
|
||||
0 0 0-.12.64l2 3.46a.5.5 0 0 0 .61.22l2.49-1c.52.4 1.08.73 1.69.98l.38
|
||||
2.65c.03.24.24.42.49.42h4c.25 0 .46-.18.49-.42l.38-2.65a7.68 7.68 0 0 0 1.69-.98l2.49
|
||||
1 .18.03a.5.5 0 0 0 .43-.25l2-3.46a.5.5 0 0 0-.12-.64l-2.11-1.65zm-1.98-1.71a5.34
|
||||
5.34 0 0 1 0 1.46l-.14 1.13.89.7 1.08.84-.7
|
||||
1.21-1.27-.51-1.04-.42-.9.68c-.43.32-.84.56-1.25.73l-1.06.43-.16 1.13-.2
|
||||
1.35h-1.4l-.19-1.35-.16-1.13-1.06-.43a5.67 5.67 0 0
|
||||
1-1.23-.71l-.91-.7-1.06.43-1.27.51-.7-1.21
|
||||
1.08-.84.89-.7-.14-1.13c-.03-.31-.05-.54-.05-.74s.02-.43.05-.73l.14-1.13-.89-.7-1.08-.84.7-1.21
|
||||
1.27.51 1.04.42.9-.68c.43-.32.84-.56 1.25-.73l1.06-.43.16-1.13.2-1.35h1.39l.19 1.35.16
|
||||
1.13 1.06.43c.43.18.83.41 1.23.71l.91.7 1.06-.43 1.27-.51.7 1.21-1.07.85-.89.7.14
|
||||
1.13zM12 8a4 4 0 1 0 0 8 4 4 0 0 0 0-8zm0 6c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2z"/>
|
||||
</svg>
|
||||
</a>
|
||||
</p>
|
||||
<div class="applies-to">
|
||||
<a href="#" class="entry-edit tt-e" i18n-data-title="editStyleLabel">
|
||||
<svg class="svg-icon edit" viewBox="0 0 24 24">
|
||||
<path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM5.92 19H5v-.92l9.06-9.06.92.92L5.92
|
||||
19zM20.71 5.63l-2.34-2.34c-.2-.2-.45-.29-.71-.29s-.51.1-.7.29l-1.83 1.83 3.75 3.75
|
||||
1.83-1.83a1 1 0 0 0 0-1.41z"/>
|
||||
</svg>
|
||||
</a>
|
||||
<a href="#" class="entry-delete tt-e" i18n-data-title="deleteStyleLabel">
|
||||
<svg class="svg-icon remove" viewBox="0 0 24 24">
|
||||
<path d="M6 19c0 1.1.9 2 2 2h8a2 2 0 0 0 2-2V7H6v12zM8 9h8v10H8V9zm7.5-5l-1-1h-5l-1 1H5v2h14V4z"/>
|
||||
</svg>
|
||||
</a>
|
||||
<span class="entry-updater-placeholder"></span>
|
||||
<a href="#" class="entry-homepage tt-w">
|
||||
<svg class="svg-icon home" viewBox="0 0 24 24">
|
||||
<path d="M19 19H5V5h7V3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"/>
|
||||
</svg>
|
||||
|
||||
</a>
|
||||
<a href="#" class="entry-support tt-w">
|
||||
<svg class="svg-icon help" viewBox="0 0 24 24">
|
||||
<path d="M11 18h2v-2h-2v2zm1-16a10 10 0 1 0 0 20 10 10 0 0 0 0-20zm0 18a8.01 8.01 0 0 1
|
||||
0-16 8.01 8.01 0 0 1 0 16zm0-14a4 4 0 0 0-4 4h2c0-1.1.9-2 2-2s2 .9 2 2c0 2-3 1.75-3
|
||||
5h2c0-2.25 3-2.5 3-5a4 4 0 0 0-4-4z"/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
<div class="entry-col entry-version">
|
||||
<span class="col-label">v#</span>
|
||||
<span class="entry-version-value"></span>
|
||||
</div>
|
||||
<div class="entry-col entry-last-update tt-w">
|
||||
<span class="col-label" i18n-text="searchResultUpdated"></span>
|
||||
<span class="entry-last-update-value"></span>
|
||||
</div>
|
||||
<div class="entry-col entry-applies-to">
|
||||
<span class="col-label" i18n-text="appliesLabel"></span>
|
||||
<div class="targets"></div>
|
||||
<a href="#" class="expander" tabindex="0">...</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template data-id="homepageIconBig">
|
||||
<svg class="svg-icon" viewBox="0 0 20 20">
|
||||
<polygon shape-rendering="crispEdges" points="3,3 3,17 17,17 17,13 15,13 15,15 5,15 5,5 7,5 7,3 "/>
|
||||
<polygon points="10,3 12.5,5.5 8,10 10,12 14.5,7.5 17,10 17,3 "/>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<template data-id="homepageIconSmall">
|
||||
<svg class="svg-icon" viewBox="0 0 20 20">
|
||||
<path d="M4,4h5v2H6v8h8v-3h2v5H4V4z M11,3h6v6l-2-2l-4,4L9,9l4-4L11,3z"/>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<template data-id="configureIcon">
|
||||
<a href="#" class="configure-usercss" i18n-title="configureStyle" tabindex="0">
|
||||
<svg class="svg-icon config"><use xlink:href="#svg-icon-config"></use></svg>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<template data-id="updaterIcons">
|
||||
<span class="updater-icons">
|
||||
<a href="#" class="check-update" i18n-title="checkForUpdate" tabindex="0">
|
||||
<span class="updater-icons entry-col entry-update-state">
|
||||
<a href="#" class="check-update tt-e" i18n-data-title="checkForUpdate" tabindex="0">
|
||||
<svg class="svg-icon" viewBox="0 0 20 20">
|
||||
<path d="M18,16.6l-3.1-3.1c0.5-0.7,0.9-1.5,1-2.5h-2.1c-0.4,1.7-2,3-3.9,3c-0.8,0-1.6-0.3-2.3-0.7
|
||||
L10,11H6.1H4.1H4v6l2.3-2.3c1,0.8,2.3,1.3,3.7,1.3c1.3,0,2.5-0.4,3.5-1.1l3.1,3.1L18,16.6z"/>
|
||||
|
@ -108,42 +126,46 @@
|
|||
C7,4,4.6,6.2,4.1,9h2.1C6.6,7.3,8.1,6,10,6z"/>
|
||||
</svg>
|
||||
</a>
|
||||
<a href="#" class="update" i18n-title="installUpdate" tabindex="0">
|
||||
<a href="#" class="update tt-e" i18n-data-title="installUpdate" tabindex="0">
|
||||
<svg class="svg-icon" viewBox="0 0 20 20">
|
||||
<polygon points="16,8 12,8 12,3 8,3 8,8 4,8 10,14 "/>
|
||||
<rect shape-rendering="crispEdges" x="4" y="15" width="12" height="2"/>
|
||||
</svg>
|
||||
</a>
|
||||
<span class="up-to-date" i18n-title="updateCheckSucceededNoUpdate">
|
||||
<span class="up-to-date tt-e" i18n-data-title="updateCheckSucceededNoUpdate">
|
||||
<svg class="svg-icon" viewBox="0 0 20 20">
|
||||
<polygon points="15.83 4.75 8.76 11.82 5.2 8.26 3.51 9.95 8.76 15.19 17.52 6.43 15.83 4.75"/>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="updated" i18n-title="updateCompleted">
|
||||
<span class="updated tt-e" i18n-data-title="updateCompleted">
|
||||
<svg class="svg-icon" viewBox="0 0 20 20">
|
||||
<polygon points="15.83 4.75 8.76 11.82 5.2 8.26 3.51 9.95 8.76 15.19 17.52 6.43 15.83 4.75"/>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="update-note"></span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<template data-id="appliesToTarget">
|
||||
<span class="target"></span>
|
||||
</template>
|
||||
|
||||
<template data-id="appliesToSeparator">
|
||||
<span class="sep">, </span>
|
||||
<span class="target tt-w">
|
||||
<img async="true">
|
||||
<svg class="svg-icon" viewBox="0 0 24 24">
|
||||
<path fill="#666" d="M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20zm7 6h-3l-1-4 4 4zm-7-4l2 4h-4l2-4zM4 14a8 8 0 0 1 0-4h4a17 17 0 0 0 0 4H4zm1 2h3l1 4-4-4zm3-8H5l4-4-1 4zm4 12l-2-4h4l-2 4zm2-6h-4a15 15 0 0 1 0-4h4a15 15 0 0 1 0 4zm1 6l1-4h3l-4 4zm1-6a17 17 0 0 0 0-4h4a8 8 0 0 1 0 4h-4z"/>
|
||||
</svg>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<template data-id="appliesToEverything">
|
||||
<span class="target" i18n-text="appliesToEverything"></span>
|
||||
<span class="target tt-w" i18n-data-title="appliesToEverything">
|
||||
<svg class="svg-icon world" viewBox="0 0 24 24">
|
||||
<path d="M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20zM4 12c0-.61.08-1.21.21-1.78L8.99 15v1c0
|
||||
1.1.9 2 2 2v1.93A8.01 8.01 0 0 1 4 12zm13.89 5.4a2 2 0 0 0-1.9-1.4h-1v-3a1 1 0 0
|
||||
0-1-1h-6v-2h2a1 1 0 0 0 1-1V7h2a2 2 0 0 0 2-2v-.41a8 8 0 0 1 2.9 12.81z"/>
|
||||
</svg>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<template data-id="extraAppliesTo">
|
||||
<details class="applies-to-extra">
|
||||
<summary class="applies-to-extra-expander" i18n-text="appliesDisplayTruncatedSuffix"></summary>
|
||||
</details>
|
||||
<a href="#" class="applies-to-extra-expander" tabindex="0">...</a>
|
||||
</template>
|
||||
|
||||
<script src="js/polyfill.js"></script>
|
||||
|
@ -158,273 +180,423 @@
|
|||
<script src="js/localization.js"></script>
|
||||
<script src="manage/filters.js"></script>
|
||||
<script src="manage/sort.js"></script>
|
||||
<script src="manage/manage.js"></script>
|
||||
<script src="vendor/semver-bundle/semver.js"></script>
|
||||
<script src="manage/manage-ui.js"></script>
|
||||
<script src="manage/manage-actions.js"></script>
|
||||
<script src="manage/bulk-actions.js"></script>
|
||||
|
||||
<script src="vendor-overwrites/colorpicker/colorconverter.js"></script>
|
||||
<script src="vendor-overwrites/colorpicker/colorpicker.js"></script>
|
||||
<script src="manage/config-dialog.js"></script>
|
||||
<script data-src="vendor-overwrites/colorpicker/colorconverter.js"></script>
|
||||
<script data-src="vendor-overwrites/colorpicker/colorpicker.js"></script>
|
||||
<script data-src="manage/config-dialog.js"></script>
|
||||
<script src="manage/updater-ui.js"></script>
|
||||
<script src="manage/object-diff.js"></script>
|
||||
<script src="manage/import-export.js"></script>
|
||||
<script src="manage/incremental-search.js"></script>
|
||||
<script src="msgbox/msgbox.js"></script>
|
||||
<script data-src="manage/object-diff.js"></script>
|
||||
<script data-src="manage/import-export.js"></script>
|
||||
<script data-src="manage/incremental-search.js"></script>
|
||||
<script data-src="msgbox/msgbox.js"></script>
|
||||
<script src="js/sections-util.js"></script>
|
||||
<script src="js/storage-util.js"></script>
|
||||
</head>
|
||||
|
||||
<body id="stylus-manage" i18n-dragndrop-hint="dragDropMessage">
|
||||
|
||||
<div id="header">
|
||||
<h1 id="manage-heading" i18n-text="manageHeading"></h1>
|
||||
|
||||
<div id="manage-settings">
|
||||
<div class="settings-column">
|
||||
<details id="filters" data-pref="manage.filters.expanded">
|
||||
<summary>
|
||||
<h2 i18n-text="manageFilters">:
|
||||
<div class="filter-stats-wrapper">
|
||||
<h1 id="main-header">
|
||||
<div>
|
||||
<svg id="stylus-logo" width="32" height="32" viewBox="0 0 48 48">
|
||||
<path fill="#285959" d="M44.59 20.14c-.02-.86 0-13.6 0-13.6-.02-4.13-2.5-5.62-6.6-5.64H11.28C7.17.92
|
||||
4.59 2.44 4.65 6.54v14.2c0 1.64-3.02 1.55-3.02 1.55v3.48s3.05.03 3.02 1.97v13.88c0 4.15 2.5 5.63
|
||||
6.63 5.63h26.71c4.13.02 6.6-1.45 6.6-5.63V27.35c0-1.54 2.98-1.57 2.98-1.57l.04-3.84s-3-.07-3.02-1.8z"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" fill="#28FEFE" d="M46 20.98V5.62A5.72 5.72 0 0 0
|
||||
40.22.03H8.66a5.63 5.63 0 0 0-5.68 5.59v15.37H0v6h2.98v15.4a5.67 5.67 0 0 0 5.68 5.64h31.56A5.76
|
||||
5.76 0 0 0 46 42.39V26.94h2.03v-5.98l-2.03.02zm-2 5.65v13.76c0 4.19-2.43 5.65-6.56 5.64H10.66c-4.13
|
||||
0-5.69-1.48-5.69-5.64V27.1c.04-1.94-2.93-1.97-2.93-1.97v-2.21s2.93.09 2.93-1.54c.05-.52 0-13.84
|
||||
0-13.84-.05-4.1 1.57-5.57 5.68-5.59h26.79c4.1.02 6.54 1.46 6.56 5.59 0 0 .08 12.87 0 13.4.02 1.73
|
||||
1.98 1.94 1.98 1.94v2.19S44 25.09 44 26.63zm-16.76-6.2c-4.56-1.71-6.47-2.7-6.47-4.92 0-1.77 1.65-3.37
|
||||
5.07-3.37 3.37 0 5.9.98 7.26 1.66l1.76-6.33c-2.08-.98-4.92-1.76-8.91-1.76-8.19 0-13.22 4.51-13.22
|
||||
10.47 0 5.08 3.84 8.29 9.64 10.36 4.2 1.45 5.86 2.7 5.86 4.87 0 2.28-1.92 3.78-5.55 3.78-3.37
|
||||
0-6.68-1.09-8.75-2.17l-1.61 6.47c1.97 1.1 5.9 2.18 9.9 2.18 9.58 0 14.04-4.98 14.04-10.83
|
||||
0-4.92-2.85-8.13-9.02-10.41z"/>
|
||||
</svg>
|
||||
<span class="ext-name">Stylus</span>
|
||||
<span class="ext-version"></span>
|
||||
<span class="filter-stats-wrapper">
|
||||
<span id="filters-stats"></span>
|
||||
<a id="reset-filters" href="#" tabindex="0">
|
||||
<svg class="svg-icon" viewBox="0 0 20 20">
|
||||
<title i18n-text="genericResetLabel"></title>
|
||||
<polygon points="16.2,5.5 14.5,3.8 10,8.3 5.5,3.8 3.8,5.5 8.3,10 3.8,14.5
|
||||
5.5,16.2 10,11.7 14.5,16.2 16.2,14.5 11.7,10 "/>
|
||||
<a id="reset-filters" class="tt-e" href="#" tabindex="0" i18n-data-title="genericResetLabel">
|
||||
<svg class="svg-icon" viewBox="0 0 20 20"><use xlink:href="#svg-icon-x"/></svg>
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
<div id="main-actions">
|
||||
<div class="new-style tt-w">
|
||||
<svg class="svg-icon" viewBox="0 0 24 24">
|
||||
<path d="M19 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2zm0
|
||||
16H5V5h14v14zm-8-2h2v-4h4v-2h-4V7h-2v4H7v2h4z"/>
|
||||
</svg>
|
||||
<div class="dropdown">
|
||||
<a href="edit.html" id="add-usercss" i18n-text="addUserCSSStyleLabel"></a>
|
||||
<a href="edit.html" id="add-reg-css" i18n-text="addPlainStyleLabel"></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span class="spacer"></span>
|
||||
|
||||
<a href="#" id="manage-options-button" i18n-data-title="openOptions" class="tt-w">
|
||||
<svg class="svg-icon ui-config" viewBox="0 0 24 24"><use xlink:href="#svg-icon-config"/></svg>
|
||||
</a>
|
||||
|
||||
<a href="#" id="manage-shortcuts-button" class="chromium-only tt-w" i18n-data-title="shortcutsNote">
|
||||
<svg class="svg-icon" viewBox="0 0 24 24">
|
||||
<path d="M20 7v10H4V7h16m0-2H4a2 2 0 0 0-1.99 2L2 17c0 1.1.9 2 2 2h16a2 2 0 0 0 2-2V7a2 2 0
|
||||
0 0-2-2zm-9 3h2v2h-2zm0 3h2v2h-2zM8 8h2v2H8zm0 3h2v2H8zm-3 0h2v2H5zm0-3h2v2H5zm3
|
||||
6h8v2H8zm6-3h2v2h-2zm0-3h2v2h-2zm3 3h2v2h-2zm0-3h2v2h-2z"/>
|
||||
</svg>
|
||||
</a>
|
||||
|
||||
<span class="spacer"></span>
|
||||
|
||||
<div class="manage-backups tt-w" i18n-data-title="backupImport">
|
||||
<svg class="svg-icon" viewBox="0 0 24 24">
|
||||
<path d="M20.54 5.23l-1.39-1.68A1.45 1.45 0 0 0 18 3H6c-.47 0-.88.21-1.16.55L3.46 5.23C3.17 5.57 3 6.02 3 6.5V19c0 1.1.9 2 2 2h14a2 2 0 0 0 2-2V6.5c0-.48-.17-.93-.46-1.27zM6.24 5h11.52l.81.97H5.44l.8-.97zM5 19V8h14v11H5zm8.45-9h-2.9v3H8l4 4 4-4h-2.55z"/>
|
||||
</svg>
|
||||
<div class="dropdown">
|
||||
<a href="#" id="unfile-all-styles" i18n-text="bckpInstStyles"></a>
|
||||
<a href="#" id="sync-dropbox-import" i18n-text="syncDropboxStyles"></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="manage-backups tt-w" i18n-data-title="exportAllLabel">
|
||||
<svg class="svg-icon" viewBox="0 0 24 24">
|
||||
<path d="M20.5 5.2l-1.4-1.6C19 3.2 18.5 3 18 3H6c-.5 0-.9.2-1.2.6L3.5 5.2A2 2 0 0 0 3 6.5V19c0 1.1.9 2 2 2h14a2 2 0 0 0 2-2V6.5c0-.5-.2-1-.5-1.3zM6.2 5h11.6l.8 1H5.4l.8-1zM5 19V8h14v11H5zm3-5h2.5v3h3v-3H16l-4-4z"/>
|
||||
</svg>
|
||||
<div class="dropdown">
|
||||
<a href="#" id="file-all-styles" i18n-text="bckpInstStyles"></a>
|
||||
<a href="#" id="sync-dropbox-export" i18n-text="syncDropboxStyles"></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span class="spacer"></span>
|
||||
<a href="https://add0n.com/stylus.html#features" target="_blank" i18n-data-title="linkGetHelp" class="tt-w">
|
||||
<svg class="svg-icon" viewBox="0 0 24 24">
|
||||
<path d="M15 4v7H5.17L4 12.17V4h11m1-2H3a1 1 0 0 0-1 1v14l4-4h10a1 1 0 0 0 1-1V3a1 1 0 0
|
||||
0-1-1zm5 4h-2v9H6v2a1 1 0 0 0 1 1h11l4 4V7a1 1 0 0 0-1-1z"/>
|
||||
</svg>
|
||||
</a>
|
||||
<a href="https://discordapp.com/widget?id=379521691774353408" target="_blank" i18n-data-title="linkChat" class="tt-w">
|
||||
<svg class="svg-icon" viewBox="0 0 24 24">
|
||||
<path d="M11 23.59v-3.6c-5.01-.26-9-4.42-9-9.49C2 5.26 6.26 1 11.5 1S21 5.26 21 10.5c0
|
||||
4.95-3.44 9.93-8.57 12.4l-1.43.69zM11.5 3C7.36 3 4 6.36 4 10.5S7.36 18 11.5
|
||||
18H13v2.3c3.64-2.3 6-6.08 6-9.8C19 6.36 15.64 3 11.5 3zm-1 11.5h2v2h-2zm2-1.5h-2c0-3.25
|
||||
3-3 3-5 0-1.1-.9-2-2-2s-2 .9-2 2h-2c0-2.21 1.79-4 4-4s4 1.79 4 4c0 2.5-3 2.75-3 5z"/>
|
||||
</svg>
|
||||
</a>
|
||||
<a href="https://github.com/openstyles/stylus/wiki" target="_blank" i18n-data-title="linkStylusWiki" class="tt-w">
|
||||
<svg class="svg-icon" viewBox="0 0 24 24">
|
||||
<path d="M6 2c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6H6zm0
|
||||
2h7v5h5v11H6V4zm2 8v2h8v-2H8zm0 4v2h8v-2H8z"/>
|
||||
</svg>
|
||||
</a>
|
||||
<a href="https://www.transifex.com/github-7/Stylus" target="_blank" i18n-data-title="linkTranslate" class="tt-w">
|
||||
<svg class="svg-icon" viewBox="0 0 24 24">
|
||||
<path d="M4 2a2 2 0 0 0-2 2v9c0 1.1.9 2 2 2h1v2l2 2h2v1c0 1.1.9 2 2 2h9a2 2 0 0 0 2-2v-9a2 2
|
||||
0 0 0-2-2h-5V4a2 2 0 0 0-2-2zm0 2h9v5h-2c-.66 0-1.23.32-1.6.81-.15-.1-.3-.24-.43-.34C9.6
|
||||
8.8 10.23 8 10.75 7H12V6H9V5H8v1H5v1h1.13a.5.5 0 0 0-.1.5s.17.5.69
|
||||
1.19c.19.24.45.52.75.81-1.15.97-2.13 1.4-2.13 1.4a.5.5 0 0 0 .38.94s1.2-.48
|
||||
2.53-1.65c.23.18.5.35.78.53-.01.1-.03.18-.03.28v2H4zm2.88 3h2.68A9.8 9.8 0 0 1 8.2
|
||||
8.84a6.59 6.59 0 0 1-.69-.75c-.44-.57-.5-.87-.5-.87A.53.53 0 0 0 6.87 7zm7.96 5h1.32L19
|
||||
20h-1.16l-.75-2.19h-3.25L13.13 20H12zm.6.9c-.13.48-1.28 4.1-1.28
|
||||
4.1h2.65s-1.22-3.63-1.34-4.1zM7 15h2v2H7z"/>
|
||||
</svg>
|
||||
|
||||
</a>
|
||||
<a href="https://userstyles.org" target="_blank" i18n-data-title="linkGetStyles" class="tt-w">
|
||||
<svg class="svg-icon" viewBox="0 0 24 24">
|
||||
<path d="M2.53 19.65l1.34.56v-9.03l-2.43 5.86a2.02 2.02 0 0 0 1.09 2.61zm19.5-3.7L17.07
|
||||
3.98a2.01 2.01 0 0 0-2.6-1.08L7.1 5.95a2 2 0 0 0-1.08 2.6l4.96 11.97a2 2 0 0 0 2.6
|
||||
1.08l7.36-3.05a2 2 0 0 0 1.09-2.6zm-9.2 3.8L7.87 7.79l7.35-3.04h.01l4.95 11.95-7.35 3.05z"/>
|
||||
<circle cx="11" cy="9" r="1"/>
|
||||
<path d="M5.88 19.75c0 1.1.9 2 2 2h1.45l-3.45-8.34v6.34z"/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</h2>
|
||||
</summary>
|
||||
|
||||
<div class="filter-selection">
|
||||
<label>
|
||||
<div class="checkmate">
|
||||
<input id="manage.onlyEnabled" type="checkbox"
|
||||
data-filter=".enabled"
|
||||
data-filter-hide=".disabled">
|
||||
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
|
||||
</div>
|
||||
</h1>
|
||||
|
||||
<div id="tools-wrapper">
|
||||
|
||||
<div id="bulk-actions" class="manage-row">
|
||||
<label class="checkmate toggle-all" tabindex="0">
|
||||
<input id="toggle-all-filters" type="checkbox">
|
||||
<svg class="svg-icon checkbox"viewBox="0 0 10 10">
|
||||
<path class="filled-circle" d="M5 .86a4.14 4.14 0 0 0 0 8.28A4.14 4.14 0 1 0 5 .86z"/>
|
||||
<path class="circle" d="M5 .86a4.14 4.14 0 0 0 0 8.28A4.14 4.14 0 1 0 5 .86zm0 7.5a3.36 3.36 0 1 1 0-6.72 3.36 3.36 0 0 1 0 6.72z"/>
|
||||
<path class="checkmark" d="M6.86 3.21L4.14 5.93 3.07 4.86l-.57.57 1.64 1.71L7.5 3.8l-.64-.58z"/>
|
||||
<path class="indeterminate" d="M2.5 4.5h5v1h-5v-1z"/>
|
||||
</svg>
|
||||
</label>
|
||||
<div class="select-resizer">
|
||||
<select id="manage.onlyEnabled.invert">
|
||||
<option i18n-text="manageOnlyEnabled" value="false"></option>
|
||||
<option i18n-text="manageOnlyDisabled" value="true"></option>
|
||||
<span id="bulk-filter-count"></span>
|
||||
<span i18n-text="bulkActions"></span>
|
||||
<span class="select-resizer bulk-actions-select-wrapper">
|
||||
<select id="bulk-actions-select">
|
||||
<option i18n-text="bulkActionsSelect" value=""></option>
|
||||
<option i18n-text="enableStyleLabel" value="enable"></option>
|
||||
<option i18n-text="disableStyleLabel" value="disable"></option>
|
||||
<option i18n-text="exportLabel" value="export"></option>
|
||||
<option i18n-text="checkForUpdate" value="update"></option>
|
||||
<!-- Plan: Reset UserCSS variables -->
|
||||
<!-- <option i18n-text="genericResetLabel" value="reset"></option> -->
|
||||
<option i18n-text="deleteStyleLabel" value="delete"></option>
|
||||
</select>
|
||||
<svg class="svg-icon select-arrow"><use xlink:href="#svg-icon-select-arrow"/></svg>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
<button id="bulk-actions-apply" i18n-text="bulkActionsApply" class="tt-e" disabled>
|
||||
<span id="update-progress"></span>
|
||||
</button>
|
||||
|
||||
<div class="filter-selection">
|
||||
<label>
|
||||
<div class="checkmate">
|
||||
<input id="manage.onlyLocal" type="checkbox"
|
||||
data-filter=":not(.updatable):not(.update-done)"
|
||||
data-filter-hide=".updatable, .update-done">
|
||||
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
|
||||
</div>
|
||||
</label>
|
||||
<div class="select-resizer">
|
||||
<select id="manage.onlyLocal.invert" i18n-title="manageOnlyLocalTooltip">
|
||||
<option i18n-text="manageOnlyLocal" value="false"></option>
|
||||
<option i18n-text="manageOnlyExternal" value="true"></option>
|
||||
<button href="#" id="update-all" class="tt-w" i18n-data-title="checkAllUpdates" type="button">
|
||||
<svg class="svg-icon" viewBox="0 0 24 24">
|
||||
<path d="M11 8v5l4.25 2.52.77-1.28-3.52-2.09V8zm10 2V3l-2.64 2.64A9 9 0 1 0 21 12h-2a7 7 0 1 1-2.05-4.95L14 10h7z"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<span id="bulk-info">
|
||||
<!-- Bulk update -->
|
||||
<span data-bulk="update">
|
||||
<button id="apply-all-updates" class="hidden" i18n-text="applyAllUpdates"></button>
|
||||
<span id="update-all-no-updates" class="tt-e hidden" i18n-text="updateAllCheckSucceededNoUpdate"></span>
|
||||
<button id="check-all-updates-force" class="hidden" i18n-text="checkAllUpdatesForce"></button>
|
||||
</span>
|
||||
|
||||
<!-- Bulk export -->
|
||||
<span data-bulk="export" class="dropdown export hidden">
|
||||
Export to:
|
||||
<span class="select-resizer">
|
||||
<select id="manage.export.destination">
|
||||
<option value="local" i18n-text="bckpInstStyles"></option>
|
||||
<option value="dropbox" i18n-text="syncDropboxStyles"></option>
|
||||
</select>
|
||||
<svg class="svg-icon select-arrow"><use xlink:href="#svg-icon-select-arrow"/></svg>
|
||||
</div>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
</span>
|
||||
|
||||
<svg class="svg-icon busy hidden" viewBox="0 0 24 24">
|
||||
<path d="M12 23h-1v-6.57A5.97 5.97 0 0 1 7 18c-3.25 0-6-2.75-6-6v-1h6.57A5.97 5.97 0 0 1 6
|
||||
7c0-3.25 2.75-6 6-6h1v6.57A5.97 5.97 0 0 1 17 6c3.25 0 6 2.75 6 6v1h-6.57A5.97 5.97 0 0 1
|
||||
18 17c0 3.25-2.75 6-6 6zm1-9.87v7.74c1.7-.46 3-2.04 3-3.87s-1.3-3.41-3-3.87zM3.13 13c.46
|
||||
1.7 2.04 3 3.87 3s3.41-1.3 3.87-3H3.13zm10-2h7.74c-.46-1.7-2.05-3-3.87-3s-3.41 1.3-3.87
|
||||
3zM11 3.13C9.3 3.59 8 5.18 8 7s1.3 3.41 3 3.87V3.13z"/>
|
||||
<!-- supported everwhere?
|
||||
<animateTransform attributeName="transform" attributeType="XML" type="rotate" from="0 0 0"
|
||||
to="360 0 0" dur="1s" repeatCount="indefinite"/>
|
||||
-->
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="filter-selection">
|
||||
<label>
|
||||
<div class="checkmate">
|
||||
<input id="manage.onlyUsercss" type="checkbox"
|
||||
data-filter=".usercss"
|
||||
data-filter-hide=":not(.usercss)">
|
||||
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
|
||||
</div>
|
||||
</label>
|
||||
<div class="select-resizer">
|
||||
<select id="manage.onlyUsercss.invert">
|
||||
<option i18n-text="manageOnlyUsercss" value="false"></option>
|
||||
<option i18n-text="manageOnlyNonUsercss" value="true"></option>
|
||||
</select>
|
||||
<svg class="svg-icon select-arrow"><use xlink:href="#svg-icon-select-arrow"/></svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label id="only-updates" class="hidden">
|
||||
<input type="checkbox"
|
||||
data-filter=".can-update, .update-problem, .update-done"
|
||||
data-filter-hide=":not(.updatable):not(.update-done),
|
||||
.no-update:not(.update-problem),
|
||||
.updatable:not(.can-update):not(.update-problem):not(.update-done)">
|
||||
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
|
||||
<span i18n-text="manageOnlyUpdates"></span>
|
||||
</label>
|
||||
|
||||
<div class="manage-row">
|
||||
<div id="search-wrapper">
|
||||
<input id="search" type="search" i18n-placeholder="searchStyles" spellcheck="false"
|
||||
data-filter=":not(.not-matching)"
|
||||
data-filter-hide=".not-matching">
|
||||
<a href="#" id="search-help" tabindex="0">
|
||||
</div>
|
||||
<div id="filters-wrapper">
|
||||
<button
|
||||
class="reset-filters search-filter tt-w"
|
||||
type="button"
|
||||
i18n-data-title="genericResetLabel"
|
||||
data-filter=".entry"
|
||||
data-filter-hide=".disabled"
|
||||
>
|
||||
<svg class="svg-icon"><use xlink:href="#svg-icon-x"/></svg>
|
||||
</button>
|
||||
<span class="button-group">
|
||||
<label class="search-filter tt-w" i18n-data-title="manageOnlyEnabled">
|
||||
<input
|
||||
id="manage.onlyEnabled"
|
||||
name="enabled"
|
||||
type="radio"
|
||||
data-filter=".enabled"
|
||||
data-filter-hide=".disabled"
|
||||
/>
|
||||
<svg class="svg-icon checkbox-enabled" viewBox="0 0 10 10">
|
||||
<path d="M6.86 3.21L4.14 5.93 3.07 4.86l-.57.57 1.64 1.71L7.5 3.8l-.64-.58z"/>
|
||||
</svg>
|
||||
</label>
|
||||
<label class="search-filter tt-w" i18n-data-title="manageOnlyDisabled">
|
||||
<input id="manage.onlyEnabled.invert" name="enabled" type="radio" />
|
||||
<svg class="svg-icon checkbox"viewBox="0 0 10 10">
|
||||
<path class="circle" d="M5 .86a4.14 4.14 0 0 0 0 8.28A4.14 4.14 0 1 0 5 .86zm0 7.5a3.36 3.36 0 1 1 0-6.72 3.36 3.36 0 0 1 0 6.72z"/>
|
||||
</svg>
|
||||
</label>
|
||||
</span>
|
||||
<span class="button-group">
|
||||
<label class="search-filter tt-w" i18n-data-title="manageOnlyUsercss">
|
||||
<input
|
||||
id="manage.onlyUsercss"
|
||||
name="usercss"
|
||||
type="radio"
|
||||
data-filter=".usercss"
|
||||
data-filter-hide=":not(.usercss)"
|
||||
/>
|
||||
<span>usercss</span> <!-- TODO: localize -->
|
||||
</label>
|
||||
<label class="search-filter tt-w" i18n-data-title="manageOnlyNonUsercss">
|
||||
<input type="radio" id="manage.onlyUsercss.invert" name="usercss" />
|
||||
<span>non-usercss</span> <!-- TODO: localize -->
|
||||
</label>
|
||||
</span>
|
||||
<span class="button-group">
|
||||
<label class="search-filter tt-w" i18n-data-title="manageOnlyLocal">
|
||||
<input
|
||||
id="manage.onlyLocal"
|
||||
name="local"
|
||||
type="radio"
|
||||
data-filter=":not(.updatable):not(.update-done)"
|
||||
data-filter-hide=".updatable, .update-done"
|
||||
/>
|
||||
<span>local</span> <!-- TODO: localize -->
|
||||
</label>
|
||||
<label class="search-filter tt-w" i18n-data-title="manageOnlyExternal">
|
||||
<input id="manage.onlyLocal.invert" name="local" type="radio" />
|
||||
<span>external</span> <!-- TODO: localize -->
|
||||
</label>
|
||||
</span>
|
||||
<label class="search-filter hidden">
|
||||
<input
|
||||
id="only-updates"
|
||||
type="checkbox"
|
||||
data-filter=".can-update, .update-problem, .update-done"
|
||||
data-filter-hide=":not(.updatable):not(.update-done),
|
||||
.no-update:not(.update-problem),
|
||||
.updatable:not(.can-update):not(.update-problem):not(.update-done)"
|
||||
/>
|
||||
<span i18n-text="manageOnlyUpdates"></span>
|
||||
</label>
|
||||
<a href="#" id="search-help">
|
||||
<svg class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</details>
|
||||
|
||||
<div id="sort-wrapper">
|
||||
<div class="sorter-selection" i18n-title="sortLabel">
|
||||
<select id="manage.newUI.sort"></select>
|
||||
<svg class="svg-icon select-arrow"><use xlink:href="#svg-icon-select-arrow"/></svg>
|
||||
</div>
|
||||
<a href="#" id="sorter-help" tabindex="0">
|
||||
<svg class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg>
|
||||
</div>
|
||||
|
||||
<div id="installed" class="manage-col-entries">
|
||||
<header class="entry-header">
|
||||
<div class="entry-col header-filter center-text tt-se" i18n-data-title="bulkActionsTooltip">
|
||||
<svg class="svg-icon no-pointer" width="20" height="20" viewBox="0 0 14 14">
|
||||
<path d="M6.42 7.58L2.92 3.5h8.75l-3.5 4.08v4.09c-1 0-1.75-.76-1.75-1.75V7.58z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<a href="#" class="entry-col sortable header-state center-text tt-se" i18n-text="genericEnabledLabel" i18n-data-title="sortLabel;sortColumnEnabled" data-type="enabled">
|
||||
<span></span>
|
||||
</a>
|
||||
<div class="entry-col header-name">
|
||||
<a href="#" class="sortable tt-se" i18n-text="genericName" i18n-data-title="sortLabel;sortColumnName" data-type="title">
|
||||
<span></span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div id="style-actions">
|
||||
<div id="update-check">
|
||||
<button id="check-all-updates" i18n-text="checkAllUpdates"><span id="update-progress"></span></button>
|
||||
<a href="#" id="update-history" i18n-title="genericHistoryLabel" tabindex="0">
|
||||
<svg class="svg-icon" viewBox="0 0 20 20" i18n-alt="helpAlt">
|
||||
<path d="M13,7H7V6h6Zm6,6.5A5.5,5.5,0,0,1,8.61,16H4V3H16V8.61A5.5,5.5,0,0,1,19,13.5ZM8,14c0-.16,0-.84,0-1H7V12H8.21a5.46,5.46,0,0,1,.39-1H7V10H9.26a5.55,5.55,0,0,1,1.09-1H7V8h7V5H6v9Zm10-.5A4.5,4.5,0,1,0,13.5,18,4.5,4.5,0,0,0,18,13.5ZM14,13V10H13v4h4V13Z"/>
|
||||
<div class="entry-col header-actions" i18n-text="optionsActions"></div>
|
||||
<div class="entry-col header-version">
|
||||
<a href="#" class="sortable tt-sw" i18n-data-title="sortLabel;sortColumnVersion" data-type="version">
|
||||
v#<span></span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="entry-col header-last-update center-text">
|
||||
<a href="#" class="sortable tt-sw" i18n-text="searchResultUpdated" i18n-data-title="sortLabel;sortColumnLastUpdate" data-type="dateUpdated">
|
||||
<span></span>
|
||||
</a>
|
||||
<a href="#" id="update-history" class="tt-sw" i18n-data-title="genericHistoryLabel" tabindex="0">
|
||||
<svg class="svg-icon update-history" viewBox="0 0 24 24" i18n-alt="updateCheckHistory">
|
||||
<path d="M20.8 10.86a7 7 0 0 0-1.53-1.47V7.02L13.35 1H5.47c-1.08 0-1.96.9-1.96 2L3.5
|
||||
19.05c0 1.1.88 2 1.96 2h5.94a6.86 6.86 0 0 0 8.2-.26 7.16 7.16 0 0 0 1.2-9.93zm-2.15
|
||||
8.7a5.34 5.34 0 0 1-7.59-.96 5.53 5.53 0 0 1-1.1-4.05l-1.53-.2c-.2 1.6.14 3.26 1.05
|
||||
4.7h-4V3h7.05l4.77 4.85v.59a6.84 6.84 0 0 0-6.26 1.2L9.62 7.78l-.52 4.3-.05.1 4.37.56L12
|
||||
10.88a5.34 5.34 0 0 1 7.6.95 5.56 5.56 0 0 1-.94 7.72z"/>
|
||||
<path d="M14.38 12.07V16l3.3 2 .56-.95-2.7-1.64v-3.34z"/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div id="update-all">
|
||||
<button id="apply-all-updates" class="hidden" i18n-text="applyAllUpdates"></button>
|
||||
<span id="update-all-no-updates" class="hidden" i18n-text="updateAllCheckSucceededNoUpdate"></span>
|
||||
<button id="check-all-updates-force" class="hidden" i18n-text="checkAllUpdatesForce"></button>
|
||||
</div>
|
||||
|
||||
<div id="add-style-wrapper">
|
||||
<a href="edit.html">
|
||||
<button id="add-style-label" i18n-text="addStyleLabel" tabindex="-1"></button>
|
||||
<div class="entry-col header-applies-to" i18n-text="appliesLabel">
|
||||
<a href="#" id="applies-to-config" class="tt-sw" i18n-data-title="configureStyle" tabIndex="0">
|
||||
<svg class="svg-icon applies-to-config" viewBox="0 0 24 24"><use xlink:href="#svg-icon-config"/></svg>
|
||||
</a>
|
||||
<label id="add-style-as-usercss-wrapper">
|
||||
<input type="checkbox" id="newStyleAsUsercss">
|
||||
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
|
||||
<span i18n-text="manageNewStyleAsUsercss" i18n-title="optionsAdvancedNewStyleAsUsercss"></span>
|
||||
<a id="usercss-wiki"
|
||||
href="https://github.com/openstyles/stylus/wiki/Usercss"
|
||||
i18n-title="externalUsercssDocument"
|
||||
tabindex="0">
|
||||
<svg class="svg-icon" viewBox="0 0 20 20">
|
||||
<path d="M4,4h5v2H6v8h8v-3h2v5H4V4z M11,3h6v6l-2-2l-4,4L9,9l4-4L11,3z"/>
|
||||
</svg>
|
||||
</a>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
</div>
|
||||
|
||||
<div class="settings-column">
|
||||
<details id="options" data-pref="manage.options.expanded">
|
||||
|
||||
<summary><h2 id="options-heading" i18n-text="optionsHeading"></h2></summary>
|
||||
|
||||
<label>
|
||||
<input id="manage.newUI" type="checkbox">
|
||||
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
|
||||
<span i18n-text="manageNewUI"></span>
|
||||
</label>
|
||||
|
||||
<div id="newUIoptions">
|
||||
<div>
|
||||
<label for="manage.newUI.favicons" i18n-text="manageFavicons">
|
||||
<!-- Applies to config, can't put this in a template because these inputs are bound to subscribed prefs -->
|
||||
<div class="hidden">
|
||||
<div id="appliesToConfig">
|
||||
<label class="checkmate" tabindex="0">
|
||||
<input id="manage.newUI.favicons" type="checkbox">
|
||||
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
|
||||
<a href="#" data-toggle-on-click="#faviconsHelp" tabindex="0">
|
||||
<svg class="svg-icon select-arrow">
|
||||
<title i18n-text="optionsSubheading"></title>
|
||||
<use xlink:href="#svg-icon-select-arrow"/>
|
||||
<svg class="svg-icon checkbox"viewBox="0 0 10 10">
|
||||
<path class="filled-circle" d="M5 .86a4.14 4.14 0 0 0 0 8.28A4.14 4.14 0 1 0 5 .86z"/>
|
||||
<path class="circle" d="M5 .86a4.14 4.14 0 0 0 0 8.28A4.14 4.14 0 1 0 5 .86zm0 7.5a3.36 3.36 0 1 1 0-6.72 3.36 3.36 0 0 1 0 6.72z"/>
|
||||
<path class="checkmark" d="M6.86 3.21L4.14 5.93 3.07 4.86l-.57.57 1.64 1.71L7.5 3.8l-.64-.58z"/>
|
||||
</svg>
|
||||
</a>
|
||||
<span i18n-text="manageFavicons"></span>
|
||||
</label>
|
||||
<div id="faviconsHelp" class="hidden" i18n-text="manageFaviconsHelp">
|
||||
<div>
|
||||
<label for="manage.newUI.faviconsGray" i18n-text="manageFaviconsGray">
|
||||
<div id="faviconsHelp" i18n-text="manageFaviconsHelp">
|
||||
<p></p>
|
||||
<label class="checkmate" tabindex="0">
|
||||
<input id="manage.newUI.faviconsGray" type="checkbox">
|
||||
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
|
||||
<svg class="svg-icon checkbox"viewBox="0 0 10 10">
|
||||
<path class="filled-circle" d="M5 .86a4.14 4.14 0 0 0 0 8.28A4.14 4.14 0 1 0 5 .86z"/>
|
||||
<path class="circle" d="M5 .86a4.14 4.14 0 0 0 0 8.28A4.14 4.14 0 1 0 5 .86zm0 7.5a3.36 3.36 0 1 1 0-6.72 3.36 3.36 0 0 1 0 6.72z"/>
|
||||
<path class="checkmark" d="M6.86 3.21L4.14 5.93 3.07 4.86l-.57.57 1.64 1.71L7.5 3.8l-.64-.58z"/>
|
||||
</svg>
|
||||
<span i18n-text="manageFaviconsGray"></span>
|
||||
</label>
|
||||
</div>
|
||||
<label>
|
||||
<input id="manage.newUI.targets" type="number" min="1" max="100" value="3">
|
||||
<span i18n-text="manageMaxTargets"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<label><input id="manage.newUI.targets" type="number" min="1" max="99"><span i18n-text="manageMaxTargets"></span></label>
|
||||
</div>
|
||||
|
||||
<div id="options-buttons">
|
||||
<button id="manage-options-button" i18n-text="openOptions"></button>
|
||||
<button id="manage-shortcuts-button" class="chromium-only"
|
||||
i18n-text="shortcuts"
|
||||
i18n-title="shortcutsNote"></button>
|
||||
<a id="find-editor-styles"
|
||||
href="https://userstyles.org/styles/browse/chrome-extension"
|
||||
i18n-title="editorStylesButton"
|
||||
target="_blank"><button i18n-text="cm_theme" tabindex="-1"></button></a>
|
||||
</div>
|
||||
|
||||
</details>
|
||||
|
||||
<details id="backup" data-pref="manage.backup.expanded">
|
||||
<summary><h2 id="backup-title" i18n-text="backupButtons"></h2></summary>
|
||||
<span id="backup-message" i18n-text="backupMessage"></span>
|
||||
<div id="backup-buttons">
|
||||
<div class="dropdown">
|
||||
<button class="dropbtn">
|
||||
<span>Export</span>
|
||||
<svg class="svg-icon select-arrow"><use xlink:href="#svg-icon-select-arrow"/></svg>
|
||||
</button>
|
||||
|
||||
<div class="dropdown-content">
|
||||
<a href="#" id="file-all-styles" i18n-text="bckpInstStyles"></a>
|
||||
<a id="sync-dropbox-export" i18n-text="syncDropboxStyles" i18n-title="syncDropboxDeprecated"></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dropdown">
|
||||
<button class="dropbtn">
|
||||
<span>Import</span>
|
||||
<svg class="svg-icon select-arrow"><use xlink:href="#svg-icon-select-arrow"/></svg>
|
||||
</button>
|
||||
|
||||
<div class="dropdown-content">
|
||||
<a href="#" id="unfile-all-styles" i18n-text="retrieveBckp"></a>
|
||||
<a id="sync-dropbox-import" i18n-text="retrieveDropboxSync" i18n-title="syncDropboxDeprecated"></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<div id="manage-text">
|
||||
<span><a href="https://userstyles.org" target="_blank" i18n-text="linkGetStyles"></a></span>
|
||||
<span><a href="https://add0n.com/stylus.html#features" target="_blank" i18n-text="linkGetHelp"></a></span>
|
||||
<span><a href="https://github.com/openstyles/stylus/wiki" target="_blank" i18n-text="linkStylusWiki"></a></span>
|
||||
<span><a href="https://www.transifex.com/github-7/Stylus" target="_blank" i18n-text="linkTranslate"></a></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div id="installed"></div>
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" style="display: none !important;">
|
||||
<symbol id="svg-icon-checked" viewBox="0 0 1000 1000">
|
||||
<path fill-rule="evenodd" d="M983.2,184.3L853,69.8c-4-3.5-9.3-5.3-14.5-5c-5.3,0.4-10.3,2.8-13.8,6.8L352.3,609.2L184.4,386.9c-3.2-4.2-8-7-13.2-7.8c-5.3-0.8-10.6,0.6-14.9,3.9L18,487.5c-8.8,6.7-10.6,19.3-3.9,28.1L325,927.2c3.6,4.8,9.3,7.7,15.3,8c0.2,0,0.5,0,0.7,0c5.8,0,11.3-2.5,15.1-6.8L985,212.6C992.3,204.3,991.5,191.6,983.2,184.3z"/>
|
||||
<path fill-rule="evenodd" d="M983.2,184.3L853,69.8c-4-3.5-9.3-5.3-14.5-5c-5.3,0.4-10.3,2.8-13.8,
|
||||
6.8L352.3,609.2L184.4,386.9c-3.2-4.2-8-7-13.2-7.8c-5.3-0.8-10.6,0.6-14.9,3.9L18,487.5c-8.8,
|
||||
6.7-10.6,19.3-3.9,28.1L325,927.2c3.6,4.8,9.3,7.7,15.3,8c0.2,0,0.5,0,0.7,0c5.8,0,11.3-2.5,
|
||||
15.1-6.8L985,212.6C992.3,204.3,991.5,191.6,983.2,184.3z"/>
|
||||
</symbol>
|
||||
|
||||
<symbol id="svg-icon-select-arrow" viewBox="0 0 1792 1792">
|
||||
<path fill-rule="evenodd" d="M1408 704q0 26-19 45l-448 448q-19 19-45 19t-45-19l-448-448q-19-19-19-45t19-45 45-19h896q26 0 45 19t19 45z"/>
|
||||
<path fill-rule="evenodd" d="M1408 704q0 26-19 45l-448 448q-19 19-45
|
||||
19t-45-19l-448-448q-19-19-19-45t19-45 45-19h896q26 0 45 19t19 45z"/>
|
||||
</symbol>
|
||||
|
||||
<symbol id="svg-icon-help" viewBox="0 0 14 16">
|
||||
<title i18n-text="helpAlt"></title>
|
||||
<path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path>
|
||||
<path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28
|
||||
0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8
|
||||
7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27
|
||||
0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56
|
||||
5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7
|
||||
7-7-3.12-7-7 3.14-7 7-7z"></path>
|
||||
</symbol>
|
||||
|
||||
<symbol id="svg-icon-config" viewBox="0 0 14 14">
|
||||
<path d="M6.2,0C5.8,0,5.4,0.4,5.4,0.8v0.7C5,1.7,4.6,1.8,4.3,2L3.8,1.5C3.6,1.4,3.4,1.3,3.2,1.3S2.7,1.4,2.6,1.5L1.5,2.6c-0.3,0.3-0.3,0.9,0,1.2L2,4.3C1.8,4.6,1.7,5,1.5,5.4H0.8C0.4,5.4,0,5.8,0,6.2v1.5c0,0.5,0.4,0.8,0.8,0.8h0.7C1.7,9,1.8,9.4,2,9.7l-0.5,0.5c-0.3,0.3-0.3,0.8,0,1.2l1.1,1.1c0.3,0.3,0.9,0.3,1.2,0L4.3,12c0.4,0.2,0.8,0.4,1.2,0.5v0.7c0,0.5,0.4,0.8,0.8,0.8h1.5c0.5,0,0.8-0.4,0.8-0.8v-0.7C9,12.3,9.4,12.2,9.7,12l0.5,0.5c0.3,0.3,0.9,0.3,1.2,0l1.1-1.1c0.3-0.3,0.3-0.8,0-1.2L12,9.7c0.2-0.4,0.4-0.8,0.5-1.2h0.7c0.5,0,0.8-0.4,0.8-0.8V6.2c0-0.5-0.4-0.8-0.8-0.8h-0.7C12.3,5,12.2,4.6,12,4.3l0.5-0.5c0.3-0.3,0.3-0.9,0-1.2l-1.1-1.1c-0.2-0.2-0.4-0.2-0.6-0.2s-0.4,0.1-0.6,0.2L9.7,2C9.4,1.8,9,1.7,8.6,1.5V0.8C8.6,0.4,8.2,0,7.8,0L6.2,0z M6.8,0.8h0.4c0.2,0,0.4,0.2,0.4,0.4v1.2c0.8,0.1,1.6,0.4,2.3,0.9l0.8-0.8c0.2-0.2,0.4-0.2,0.6,0l0.3,0.3c0.2,0.2,0.2,0.4,0,0.6l-0.8,0.8c0.5,0.7,0.8,1.4,0.9,2.3h1.2c0.2,0,0.4,0.2,0.4,0.4v0.4c0,0.2-0.2,0.4-0.4,0.4h-1.2c-0.1,0.8-0.4,1.6-0.9,2.3l0.8,0.8c0.2,0.2,0.2,0.4,0,0.6l-0.3,0.3c-0.2,0.2-0.4,0.2-0.6,0l-0.8-0.8c-0.7,0.5-1.4,0.8-2.3,0.9v1.2c0,0.2-0.2,0.4-0.4,0.4H6.8c-0.2,0-0.4-0.2-0.4-0.4v-1.2c-0.8-0.1-1.6-0.4-2.3-0.9l-0.8,0.8c-0.2,0.2-0.4,0.2-0.6,0l-0.3-0.3c-0.2-0.2-0.2-0.4,0-0.6l0.8-0.8C2.8,9.2,2.5,8.4,2.4,7.6H1.2C1,7.6,0.8,7.4,0.8,7.2V6.8c0-0.2,0.2-0.4,0.4-0.4h1.2c0.1-0.8,0.4-1.6,0.9-2.3L2.5,3.3c-0.2-0.2-0.2-0.4,0-0.6l0.3-0.3c0.2-0.2,0.4-0.2,0.6,0l0.8,0.8c0.7-0.5,1.4-0.8,2.3-0.9V1.2C6.4,1,6.6,0.8,6.8,0.8L6.8,0.8z M7,3.6C5.1,3.6,3.6,5.1,3.6,7c0,0,0,0,0,0c0,1.9,1.5,3.4,3.4,3.4c1.9,0,3.4-1.5,3.4-3.4C10.4,5.1,8.9,3.6,7,3.6C7,3.6,7,3.6,7,3.6z M7,4.8c1.2,0,2.2,1,2.2,2.2c0,1.2-1,2.2-2.2,2.2c-1.2,0-2.2-1-2.2-2.2C4.8,5.8,5.8,4.8,7,4.8z"/>
|
||||
<symbol id="svg-icon-config" viewBox="0 0 24 24">
|
||||
<path d="M19.43 12.98a7.8 7.8 0 0 0 0-1.96l2.11-1.65a.5.5 0 0 0 .12-.64l-2-3.46a.5.5
|
||||
0 0 0-.61-.22l-2.49 1a7.3 7.3 0 0 0-1.69-.98l-.38-2.65A.49.49 0 0 0 14 2h-4a.49.49
|
||||
0 0 0-.49.42l-.38 2.65c-.61.25-1.17.59-1.69.98l-2.49-1a.57.57 0 0 0-.18-.03.5.5 0 0
|
||||
0-.43.25l-2 3.46a.5.5 0 0 0 .12.64l2.11 1.65a7.93 7.93 0 0 0 0 1.96l-2.11 1.65a.5.5
|
||||
0 0 0-.12.64l2 3.46a.5.5 0 0 0 .61.22l2.49-1c.52.4 1.08.73 1.69.98l.38
|
||||
2.65c.03.24.24.42.49.42h4c.25 0 .46-.18.49-.42l.38-2.65a7.68 7.68 0 0 0 1.69-.98l2.49
|
||||
1 .18.03a.5.5 0 0 0 .43-.25l2-3.46a.5.5 0 0 0-.12-.64l-2.11-1.65zm-1.98-1.71a5.34
|
||||
5.34 0 0 1 0 1.46l-.14 1.13.89.7 1.08.84-.7
|
||||
1.21-1.27-.51-1.04-.42-.9.68c-.43.32-.84.56-1.25.73l-1.06.43-.16 1.13-.2
|
||||
1.35h-1.4l-.19-1.35-.16-1.13-1.06-.43a5.67 5.67 0 0
|
||||
1-1.23-.71l-.91-.7-1.06.43-1.27.51-.7-1.21
|
||||
1.08-.84.89-.7-.14-1.13c-.03-.31-.05-.54-.05-.74s.02-.43.05-.73l.14-1.13-.89-.7-1.08-.84.7-1.21
|
||||
1.27.51 1.04.42.9-.68c.43-.32.84-.56 1.25-.73l1.06-.43.16-1.13.2-1.35h1.39l.19 1.35.16
|
||||
1.13 1.06.43c.43.18.83.41 1.23.71l.91.7 1.06-.43 1.27-.51.7 1.21-1.07.85-.89.7.14
|
||||
1.13zM12 8a4 4 0 1 0 0 8 4 4 0 0 0 0-8zm0 6c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2z"/>
|
||||
</symbol>
|
||||
|
||||
<symbol id="svg-icon-x" viewBox="0 0 20 20">
|
||||
<polygon points="16.2,5.5 14.5,3.8 10,8.3 5.5,3.8 3.8,5.5 8.3,10 3.8,14.5
|
||||
5.5,16.2 10,11.7 14.5,16.2 16.2,14.5 11.7,10 "/>
|
||||
</symbol>
|
||||
</svg>
|
||||
|
||||
|
|
145
manage/bulk-actions.js
Normal file
145
manage/bulk-actions.js
Normal file
|
@ -0,0 +1,145 @@
|
|||
/* global $ $$ API t prefs handleEvent installed exportToFile checkUpdateBulk exportDropbox
|
||||
messageBox */
|
||||
/* exported bulk */
|
||||
'use strict';
|
||||
|
||||
const bulk = {
|
||||
|
||||
init: () => {
|
||||
document.addEventListener('change', bulk.updateBulkFilters);
|
||||
$('#bulk-actions-select').onchange = bulk.handleSelect;
|
||||
$('#bulk-actions-apply').onclick = bulk.handleApply;
|
||||
},
|
||||
|
||||
// Update all button in header
|
||||
updateAll: () => {
|
||||
const toggle = $('#toggle-all-filters');
|
||||
toggle.checked = false; // ensure click will check all styles
|
||||
toggle.click();
|
||||
checkUpdateBulk();
|
||||
},
|
||||
|
||||
checkApply: () => {
|
||||
const checkedEntries = $$('.entry-filter-toggle').filter(entry => entry.checked);
|
||||
if (checkedEntries.length > 0 && $('#bulk-actions-select').value !== '') {
|
||||
$('#bulk-actions-apply').removeAttribute('disabled');
|
||||
} else {
|
||||
$('#bulk-actions-apply').setAttribute('disabled', true);
|
||||
}
|
||||
$('#bulk-filter-count').textContent = checkedEntries.length || '';
|
||||
},
|
||||
|
||||
handleSelect: event => {
|
||||
event.preventDefault();
|
||||
$$('[data-bulk]').forEach(el => el.classList.add('hidden'));
|
||||
|
||||
switch (event.target.value) {
|
||||
case 'enable':
|
||||
break;
|
||||
case 'disable':
|
||||
break;
|
||||
case 'export':
|
||||
$('[data-bulk="export"]').classList.remove('hidden');
|
||||
break;
|
||||
case 'update':
|
||||
$('[data-bulk="update"]').classList.remove('hidden');
|
||||
break;
|
||||
// case 'reset':
|
||||
// break;
|
||||
case 'delete':
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
handleApply: event => {
|
||||
event.preventDefault();
|
||||
let styles;
|
||||
const action = $('#bulk-actions-select').value;
|
||||
const entries = $$('.entry-filter-toggle:checked').map(el => el.closest('.entry'));
|
||||
|
||||
switch (action) {
|
||||
case 'enable':
|
||||
case 'disable': {
|
||||
const isEnabled = action === 'enable';
|
||||
entries.forEach(entry => {
|
||||
const box = $('.entry-state-toggle', entry);
|
||||
entry.classList.toggle('enable', isEnabled);
|
||||
box.checked = isEnabled;
|
||||
handleEvent.toggle.call(box, event, entry);
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'export': {
|
||||
styles = entries.map(entry => entry.styleMeta);
|
||||
const destination = prefs.get('manage.export.destination');
|
||||
if (destination === 'dropbox') {
|
||||
return exportDropbox(styles);
|
||||
}
|
||||
return exportToFile(styles);
|
||||
}
|
||||
case 'update':
|
||||
checkUpdateBulk();
|
||||
break;
|
||||
// case 'reset':
|
||||
// break;
|
||||
case 'delete': {
|
||||
styles = entries.reduce((acc, entry) => {
|
||||
const style = entry.styleMeta;
|
||||
acc[style.id] = style.name;
|
||||
return acc;
|
||||
}, {});
|
||||
bulk.deleteBulk(event, styles);
|
||||
const toggle = $('#toggle-all-filters');
|
||||
toggle.checked = false;
|
||||
toggle.indeterminate = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
$('#bulk-actions-select').value = '';
|
||||
$('#bulk-actions-apply').setAttribute('disabled', true);
|
||||
},
|
||||
|
||||
updateBulkFilters: ({target}) => {
|
||||
// total is undefined until initialized
|
||||
if (installed.dataset.total) {
|
||||
// ignore filter checkboxes
|
||||
if (target.type === 'checkbox' && target.closest('.toggle-all, .entry-filter')) {
|
||||
const bulk = $('#toggle-all-filters');
|
||||
const state = target.checked;
|
||||
const visibleEntries = $$('.entry-filter-toggle')
|
||||
.filter(entry => !entry.closest('.entry').classList.contains('hidden'));
|
||||
bulk.indeterminate = false;
|
||||
if (target === bulk) {
|
||||
visibleEntries.forEach(entry => {
|
||||
entry.checked = state;
|
||||
});
|
||||
} else {
|
||||
if (visibleEntries.length === visibleEntries.filter(entry => entry.checked === state).length) {
|
||||
bulk.checked = state;
|
||||
} else {
|
||||
bulk.checked = false;
|
||||
bulk.indeterminate = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
bulk.checkApply();
|
||||
}
|
||||
},
|
||||
|
||||
deleteBulk: (event, styles) => {
|
||||
messageBox({
|
||||
title: t('deleteStyleConfirm'),
|
||||
contents: Object.values(styles).join(', '),
|
||||
className: 'danger center',
|
||||
buttons: [t('confirmDelete'), t('confirmCancel')],
|
||||
})
|
||||
.then(({button}) => {
|
||||
if (button === 0) {
|
||||
Object.keys(styles).forEach(id => API.deleteStyle(Number(id)));
|
||||
installed.dataset.total -= Object.keys(styles).length;
|
||||
bulk.updateBulkFilters({target: $('#toggle-all-filters')});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
};
|
|
@ -1,4 +1,4 @@
|
|||
/* global installed messageBox sorter $ $$ $create t debounce prefs API router */
|
||||
/* global installed messageBox sorter $ $$ $create t debounce prefs API UI router resetUpdates */
|
||||
/* exported filterAndAppend */
|
||||
'use strict';
|
||||
|
||||
|
@ -37,8 +37,9 @@ HTMLSelectElement.prototype.adjustWidth = function () {
|
|||
};
|
||||
|
||||
function init() {
|
||||
$('#search').oninput = e => {
|
||||
router.updateSearch('search', e.target.value);
|
||||
$('#search').oninput = event => {
|
||||
router.updateSearch('search', event.target.value);
|
||||
UI.updateFilterLabels();
|
||||
};
|
||||
|
||||
$('#search-help').onclick = event => {
|
||||
|
@ -57,48 +58,13 @@ function init() {
|
|||
} else {
|
||||
return s;
|
||||
}
|
||||
})))),
|
||||
}))
|
||||
)
|
||||
),
|
||||
buttons: [t('confirmOK')],
|
||||
});
|
||||
};
|
||||
|
||||
$$('select[id$=".invert"]').forEach(el => {
|
||||
const slave = $('#' + el.id.replace('.invert', ''));
|
||||
const slaveData = slave.dataset;
|
||||
const valueMap = new Map([
|
||||
[false, slaveData.filter],
|
||||
[true, slaveData.filterHide],
|
||||
]);
|
||||
// enable slave control when user switches the value
|
||||
el.oninput = () => {
|
||||
if (!slave.checked) {
|
||||
// oninput occurs before onchange
|
||||
setTimeout(() => {
|
||||
if (!slave.checked) {
|
||||
slave.checked = true;
|
||||
slave.dispatchEvent(new Event('change', {bubbles: true}));
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
// swap slave control's filtering rules
|
||||
el.onchange = event => {
|
||||
const value = el.value === 'true';
|
||||
const filter = valueMap.get(value);
|
||||
if (slaveData.filter === filter) {
|
||||
return;
|
||||
}
|
||||
slaveData.filter = filter;
|
||||
slaveData.filterHide = valueMap.get(!value);
|
||||
debounce(filterOnChange, 0, event);
|
||||
// avoid triggering MutationObserver during page load
|
||||
if (document.readyState === 'complete') {
|
||||
el.adjustWidth();
|
||||
}
|
||||
};
|
||||
el.onchange({target: el});
|
||||
});
|
||||
|
||||
$$('[data-filter]').forEach(el => {
|
||||
el.onchange = filterOnChange;
|
||||
if (el.closest('.hidden')) {
|
||||
|
@ -108,10 +74,10 @@ function init() {
|
|||
|
||||
$('#reset-filters').onclick = event => {
|
||||
event.preventDefault();
|
||||
if (!filtersSelector.hide) {
|
||||
return;
|
||||
}
|
||||
for (const el of $$('#filters [data-filter]')) {
|
||||
// if (!filtersSelector.hide) {
|
||||
// return;
|
||||
// }
|
||||
for (const el of $$('#tools-wrapper [data-filter]')) {
|
||||
let value;
|
||||
if (el.type === 'checkbox' && el.checked) {
|
||||
value = el.checked = false;
|
||||
|
@ -127,22 +93,16 @@ function init() {
|
|||
}
|
||||
filterOnChange({forceRefilter: true});
|
||||
router.updateSearch('search', '');
|
||||
resetUpdates();
|
||||
UI.updateFilterLabels();
|
||||
};
|
||||
|
||||
// Adjust width after selects are visible
|
||||
prefs.subscribe(['manage.filters.expanded'], () => {
|
||||
const el = $('#filters');
|
||||
if (el.open) {
|
||||
$$('select', el).forEach(select => select.adjustWidth());
|
||||
}
|
||||
});
|
||||
|
||||
filterOnChange({forceRefilter: true});
|
||||
}
|
||||
|
||||
|
||||
function filterOnChange({target: el, forceRefilter}) {
|
||||
const getValue = el => (el.type === 'checkbox' ? el.checked : el.value.trim());
|
||||
const getValue = elm => (elm.type === 'search') ? elm.value.trim() : elm.checked;
|
||||
if (!forceRefilter) {
|
||||
const value = getValue(el);
|
||||
if (value === el.lastValue) {
|
||||
|
@ -150,7 +110,7 @@ function filterOnChange({target: el, forceRefilter}) {
|
|||
}
|
||||
el.lastValue = value;
|
||||
}
|
||||
const enabledFilters = $$('#header [data-filter]').filter(el => getValue(el));
|
||||
const enabledFilters = $$('#tools-wrapper [data-filter]').filter(el => getValue(el));
|
||||
const buildFilter = hide =>
|
||||
(hide ? '' : '.entry.hidden') +
|
||||
[...enabledFilters.map(el =>
|
||||
|
@ -163,6 +123,7 @@ function filterOnChange({target: el, forceRefilter}) {
|
|||
hide: buildFilter(true),
|
||||
unhide: buildFilter(false),
|
||||
});
|
||||
console.log('filter on change', filtersSelector, installed)
|
||||
if (installed) {
|
||||
reapplyFilter().then(sorter.updateStripes);
|
||||
}
|
||||
|
@ -262,9 +223,9 @@ function reapplyFilter(container = installed, alreadySearched) {
|
|||
|
||||
function showFiltersStats() {
|
||||
const active = filtersSelector.hide !== '';
|
||||
$('#filters summary').classList.toggle('active', active);
|
||||
$('.filter-stats-wrapper').classList.toggle('active', active);
|
||||
$('#reset-filters').disabled = !active;
|
||||
const numTotal = installed.children.length;
|
||||
const numTotal = installed.children.length - 1; // Don't include the header
|
||||
const numHidden = installed.getElementsByClassName('entry hidden').length;
|
||||
const numShown = numTotal - numHidden;
|
||||
if (filtersSelector.numShown !== numShown ||
|
||||
|
@ -291,6 +252,7 @@ function searchStyles({immediately, container} = {}) {
|
|||
el.lastValue = query;
|
||||
|
||||
const entries = container && container.children || container || installed.children;
|
||||
console.log('search?', query)
|
||||
return API.searchDB({
|
||||
query,
|
||||
ids: [...entries].map(el => el.styleId),
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/* global messageBox styleSectionsEqual API onDOMready
|
||||
tryJSONparse scrollElementIntoView $ $$ API $create t animateElement
|
||||
handleEvent
|
||||
styleJSONseemsValid */
|
||||
'use strict';
|
||||
|
||||
|
@ -46,8 +47,9 @@ onDOMready().then(() => {
|
|||
this.ondragend();
|
||||
if (event.dataTransfer.files.length) {
|
||||
event.preventDefault();
|
||||
if ($('#only-updates input').checked) {
|
||||
$('#only-updates input').click();
|
||||
const updates = $('#only-updates');
|
||||
if (updates.checked) {
|
||||
handleEvent.checkFilterSelectors(updates);
|
||||
}
|
||||
importFromFile({file: event.dataTransfer.files[0]});
|
||||
}
|
||||
|
@ -294,8 +296,7 @@ function importFromString(jsonString) {
|
|||
}
|
||||
|
||||
|
||||
function exportToFile() {
|
||||
API.getAllStyles().then(styles => {
|
||||
function exportToFile(styles) {
|
||||
// https://crbug.com/714373
|
||||
document.documentElement.appendChild(
|
||||
$create('iframe', {
|
||||
|
@ -325,7 +326,6 @@ function exportToFile() {
|
|||
// we don't remove the iframe or the object URL because the browser may show
|
||||
// a download dialog and we don't know how long it'll take until the user confirms it
|
||||
// (some browsers like Vivaldi can't download if we revoke the URL)
|
||||
});
|
||||
|
||||
function generateFileName() {
|
||||
const today = new Date();
|
||||
|
|
|
@ -38,6 +38,7 @@ onDOMready().then(() => {
|
|||
let textAtPos = 1e6;
|
||||
let rotated;
|
||||
const entries = [...installed.children];
|
||||
entries.shift(); // remove header
|
||||
const focusedIndex = entries.indexOf(focusedEntry);
|
||||
if (focusedIndex > 0) {
|
||||
if (direction > 0) {
|
||||
|
@ -63,7 +64,7 @@ onDOMready().then(() => {
|
|||
}
|
||||
if (found && found !== focusedEntry) {
|
||||
focusedEntry = found;
|
||||
focusedLink = $('.style-name-link', found);
|
||||
focusedLink = $('.entry-name', found);
|
||||
focusedName = found.styleNameLowerCase;
|
||||
scrollElementIntoView(found, {invalidMarginRatio: .25});
|
||||
animateElement(found, {className: 'highlight-quick'});
|
||||
|
@ -82,6 +83,7 @@ onDOMready().then(() => {
|
|||
// focus search field on "/" key
|
||||
if (key === '/' || !key && k === 191 && !event.shiftKey) {
|
||||
event.preventDefault();
|
||||
$('#tools-wrapper').classList.remove('hidden');
|
||||
$('#search').focus();
|
||||
return;
|
||||
}
|
||||
|
|
591
manage/manage-actions.js
Normal file
591
manage/manage-actions.js
Normal file
|
@ -0,0 +1,591 @@
|
|||
/*
|
||||
global messageBox getStyleWithNoCode
|
||||
filterAndAppend showFiltersStats
|
||||
checkUpdate handleUpdateInstalled resetUpdates
|
||||
objectDiff
|
||||
configDialog
|
||||
sorter msg prefs API onDOMready $ $$ setupLivePrefs
|
||||
URLS enforceInputRange t
|
||||
getOwnTab getActiveTab openURL animateElement sessionStorageHash debounce
|
||||
scrollElementIntoView CHROME VIVALDI FIREFOX router
|
||||
UI bulk
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
let installed;
|
||||
|
||||
const handleEvent = {};
|
||||
|
||||
Promise.all([
|
||||
API.getAllStyles(true),
|
||||
// FIXME: integrate this into filter.js
|
||||
router.getSearch('search') && API.searchDB({query: router.getSearch('search')}),
|
||||
Promise.all([
|
||||
onDOMready(),
|
||||
prefs.initializing,
|
||||
])
|
||||
.then(() => {
|
||||
initGlobalEvents();
|
||||
if (!VIVALDI) {
|
||||
$$('#header select').forEach(el => el.adjustWidth());
|
||||
}
|
||||
if (FIREFOX && 'update' in (chrome.commands || {})) {
|
||||
const btn = $('#manage-shortcuts-button');
|
||||
btn.classList.remove('chromium-only');
|
||||
btn.onclick = API.optionsCustomizeHotkeys;
|
||||
}
|
||||
}),
|
||||
]).then(args => {
|
||||
UI.init();
|
||||
UI.showStyles(...args);
|
||||
lazyLoad();
|
||||
});
|
||||
|
||||
msg.onExtension(onRuntimeMessage);
|
||||
|
||||
function onRuntimeMessage(msg) {
|
||||
switch (msg.method) {
|
||||
case 'styleUpdated':
|
||||
case 'styleAdded':
|
||||
API.getStyle(msg.style.id, true)
|
||||
.then(style => handleUpdate(style, msg));
|
||||
break;
|
||||
case 'styleDeleted':
|
||||
handleDelete(msg.style.id);
|
||||
break;
|
||||
case 'styleApply':
|
||||
case 'styleReplaceAll':
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
setTimeout(() => {
|
||||
sorter.updateStripes({onlyWhenColumnsChanged: true});
|
||||
}, 0);
|
||||
}
|
||||
|
||||
|
||||
function initGlobalEvents() {
|
||||
installed = $('#installed');
|
||||
installed.onclick = handleEvent.entryClicked;
|
||||
$('#manage-options-button').onclick = event => {
|
||||
event.preventDefault();
|
||||
router.updateHash('#stylus-options');
|
||||
};
|
||||
|
||||
$('#manage-shortcuts-button').onclick = event => {
|
||||
event.preventDefault();
|
||||
openURL({url: URLS.configureCommands});
|
||||
};
|
||||
|
||||
$('#update-all').onclick = event => {
|
||||
event.preventDefault();
|
||||
bulk.updateAll();
|
||||
};
|
||||
|
||||
$('#filters-wrapper').onclick = event => {
|
||||
event.preventDefault();
|
||||
handleEvent.toggleFilter(event.target);
|
||||
};
|
||||
|
||||
$('#search').onsearch = event => {
|
||||
if (event.target.value === '') {
|
||||
console.log('search empty')
|
||||
handleEvent.resetFilters();
|
||||
}
|
||||
}
|
||||
|
||||
$$('#header a[href^="http"]').forEach(a => (a.onclick = handleEvent.external));
|
||||
$$('#add-usercss, #add-reg-css').forEach(a => (a.onclick = handleEvent.newStyle));
|
||||
|
||||
document.addEventListener('visibilitychange', onVisibilityChange);
|
||||
|
||||
document.addEventListener('keydown', event => {
|
||||
if (event.which === 27) {
|
||||
// close all open "applies-to" details
|
||||
$$('.applies-to-extra[open]').forEach(el => {
|
||||
el.removeAttribute('open');
|
||||
});
|
||||
} else if (event.which === 32 && event.target.classList.contains('checkmate')) {
|
||||
// pressing space toggles the containing checkbox
|
||||
$('input[type="checkbox"]', event.target).click();
|
||||
}
|
||||
});
|
||||
|
||||
// triggered automatically by setupLivePrefs() below
|
||||
enforceInputRange($('#manage.newUI.targets'));
|
||||
|
||||
// N.B. triggers existing onchange listeners
|
||||
setupLivePrefs();
|
||||
bulk.init();
|
||||
sorter.init();
|
||||
|
||||
prefs.subscribe([
|
||||
'manage.newUI.favicons',
|
||||
'manage.newUI.faviconsGray',
|
||||
'manage.newUI.targets',
|
||||
], () => switchUI());
|
||||
|
||||
switchUI({styleOnly: true});
|
||||
}
|
||||
|
||||
|
||||
Object.assign(handleEvent, {
|
||||
|
||||
ENTRY_ROUTES: {
|
||||
'.entry-state-toggle': 'toggle',
|
||||
'.entry-style-name': 'name',
|
||||
'.entry-homepage': 'external',
|
||||
'.entry-support': 'external',
|
||||
'.check-update': 'check',
|
||||
'.update': 'update',
|
||||
'.entry-delete': 'delete',
|
||||
'.entry-configure-usercss': 'config',
|
||||
'.sortable': 'updateSort',
|
||||
'#applies-to-config': 'appliesConfig',
|
||||
'.applies-to-extra-expander': 'toggleExtraAppliesTo'
|
||||
},
|
||||
|
||||
entryClicked(event) {
|
||||
const target = event.target;
|
||||
const entry = target.closest('.entry');
|
||||
for (const selector in handleEvent.ENTRY_ROUTES) {
|
||||
for (let el = target; el && el !== entry; el = el.parentElement) {
|
||||
if (el.matches(selector)) {
|
||||
const handler = handleEvent.ENTRY_ROUTES[selector];
|
||||
return handleEvent[handler].call(el, event, entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
name(event) {
|
||||
handleEvent.edit(event);
|
||||
},
|
||||
|
||||
newStyle(event) {
|
||||
event.preventDefault();
|
||||
prefs.set('newStyleAsUsercss', event.target.id === 'add-usercss');
|
||||
window.location.href = 'edit.html';
|
||||
},
|
||||
|
||||
edit(event) {
|
||||
if (event.altKey) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const left = event.button === 0;
|
||||
const middle = event.button === 1;
|
||||
const shift = event.shiftKey;
|
||||
const ctrl = event.ctrlKey;
|
||||
const openWindow = left && shift && !ctrl;
|
||||
const openBackgroundTab = (middle && !shift) || (left && ctrl && !shift);
|
||||
const openForegroundTab = (middle && shift) || (left && ctrl && shift);
|
||||
const url = $('[href]', event.target.closest('.entry')).href;
|
||||
if (openWindow || openBackgroundTab || openForegroundTab) {
|
||||
if (chrome.windows && openWindow) {
|
||||
chrome.windows.create(Object.assign(prefs.get('windowPosition'), {url}));
|
||||
} else {
|
||||
getOwnTab().then(({index}) => {
|
||||
openURL({
|
||||
url,
|
||||
index: index + 1,
|
||||
active: openForegroundTab
|
||||
});
|
||||
});
|
||||
}
|
||||
} else {
|
||||
onVisibilityChange();
|
||||
getActiveTab().then(tab => {
|
||||
sessionStorageHash('manageStylesHistory').set(tab.id, url);
|
||||
location.href = url;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
toggle(event, entry) {
|
||||
API.toggleStyle(entry.styleId, this.matches('.enable') || this.checked);
|
||||
UI.addLabels(entry);
|
||||
},
|
||||
|
||||
toggleExtraAppliesTo(event, entry) {
|
||||
event.preventDefault();
|
||||
entry.classList.toggle('hide-extra');
|
||||
if (event.shiftKey) {
|
||||
const state = entry.classList.contains('hide-extra');
|
||||
$$('.entry').forEach(entry => entry.classList.toggle('hide-extra', state));
|
||||
}
|
||||
},
|
||||
|
||||
resetFilters() {
|
||||
$('#reset-filters').click();
|
||||
// TODO: figure out why we need to press this twice
|
||||
$('#reset-filters').click();
|
||||
resetUpdates();
|
||||
},
|
||||
|
||||
toggleFilter(el) {
|
||||
if (el.classList.contains('reset-filters')) {
|
||||
return handleEvent.resetFilters();
|
||||
}
|
||||
|
||||
const target = (el.nodeName === 'LABEL') ? $('input', el) : el;
|
||||
const type = Object.values(UI.searchFilters).find(filter => filter.id === target.id);
|
||||
const filterQuery = type && type.query || '';
|
||||
const remove = type && type.invert ? UI.searchFilters[type.invert].query : '';
|
||||
const len = filterQuery.length + 1;
|
||||
const search = $('#search');
|
||||
|
||||
let {selectionStart, selectionEnd, value} = search;
|
||||
if (value.includes(filterQuery)) {
|
||||
value = ` ${value} `.replace(` ${filterQuery} `, ' ').trim();
|
||||
if (selectionEnd > value.length) {
|
||||
selectionStart -= len;
|
||||
selectionEnd -= len;
|
||||
}
|
||||
} else {
|
||||
if (selectionEnd === value.length) {
|
||||
selectionStart += len;
|
||||
selectionEnd += len;
|
||||
}
|
||||
value = (` ${value} ${filterQuery} `.replace(` ${remove} `, ' ')).trim();
|
||||
}
|
||||
search.value = value;
|
||||
search.selectionStart = selectionStart;
|
||||
search.selectionEnd = selectionEnd;
|
||||
search.focus();
|
||||
router.updateSearch('search', value);
|
||||
UI.updateFilterLabels();
|
||||
// updates or issues (special case)
|
||||
if (target.dataset.filterSelectors) {
|
||||
handleEvent.checkFilterSelectors(target);
|
||||
}
|
||||
},
|
||||
|
||||
checkFilterSelectors(target) {
|
||||
const selectors = target.dataset.filterSelectors;
|
||||
const checked = target.classList.contains('checked');
|
||||
$$('.entry').forEach(entry => {
|
||||
entry.classList.toggle('hidden', checked && !entry.matches(selectors));
|
||||
});
|
||||
},
|
||||
|
||||
check(event, entry) {
|
||||
event.preventDefault();
|
||||
checkUpdate(entry, {single: true});
|
||||
},
|
||||
|
||||
update(event, entry) {
|
||||
event.preventDefault();
|
||||
const json = entry.updatedCode;
|
||||
json.id = entry.styleId;
|
||||
API[json.usercssData ? 'installUsercss' : 'installStyle'](json);
|
||||
},
|
||||
|
||||
updateSort(event) {
|
||||
event.preventDefault();
|
||||
sorter.updateSort(event);
|
||||
removeSelection();
|
||||
},
|
||||
|
||||
delete(event, entry) {
|
||||
event.preventDefault();
|
||||
const id = entry.styleId;
|
||||
animateElement(entry);
|
||||
messageBox({
|
||||
title: t('deleteStyleConfirm'),
|
||||
contents: entry.styleMeta.name,
|
||||
className: 'danger center',
|
||||
buttons: [t('confirmDelete'), t('confirmCancel')],
|
||||
})
|
||||
.then(({button}) => {
|
||||
if (button === 0) {
|
||||
API.deleteStyle(id);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
external(event) {
|
||||
if (event.shiftKey && !event.altKey && !event.ctrlKey && !event.metaKey) {
|
||||
// Shift-click = the built-in 'open in a new window' action
|
||||
return;
|
||||
}
|
||||
getOwnTab().then(({index}) => {
|
||||
openURL({
|
||||
url: event.target.closest('a').href,
|
||||
index: index + 1,
|
||||
active: !event.ctrlKey || event.shiftKey,
|
||||
});
|
||||
});
|
||||
event.preventDefault();
|
||||
},
|
||||
|
||||
loadFavicons({all = false} = {}) {
|
||||
if (!installed.firstElementChild) return;
|
||||
let favicons = [];
|
||||
if (all) {
|
||||
favicons = $$('img[data-src]', installed);
|
||||
} else {
|
||||
const {left, top} = installed.firstElementChild.getBoundingClientRect();
|
||||
const x = Math.max(0, left);
|
||||
const y = Math.max(0, top);
|
||||
const first = document.elementFromPoint(x, y);
|
||||
const lastOffset = first.offsetTop + window.innerHeight;
|
||||
const numTargets = prefs.get('manage.newUI.targets');
|
||||
let entry = first && first.closest('.entry') || installed.children[0];
|
||||
while (entry && entry.offsetTop <= lastOffset) {
|
||||
favicons.push(...$$('img', entry).slice(0, numTargets).filter(img => img.dataset.src));
|
||||
entry = entry.nextElementSibling;
|
||||
}
|
||||
}
|
||||
let i = 0;
|
||||
for (const img of favicons) {
|
||||
img.src = img.dataset.src;
|
||||
delete img.dataset.src;
|
||||
// loading too many icons at once will block the page while the new layout is recalculated
|
||||
if (++i > 100) break;
|
||||
}
|
||||
if ($('img[data-src]', installed)) {
|
||||
debounce(handleEvent.loadFavicons, 1, {all: true});
|
||||
}
|
||||
},
|
||||
|
||||
config(event, {styleMeta}) {
|
||||
event.preventDefault();
|
||||
configDialog(styleMeta);
|
||||
},
|
||||
|
||||
appliesConfig() {
|
||||
messageBox({
|
||||
title: t('configureStyle'),
|
||||
className: 'config-dialog',
|
||||
contents: [
|
||||
$('#appliesToConfig').cloneNode(true)
|
||||
],
|
||||
buttons: [{
|
||||
textContent: t('confirmClose'),
|
||||
dataset: {cmd: 'close'},
|
||||
}],
|
||||
onshow: box => {
|
||||
box.addEventListener('change', handleEvent.manageFavicons);
|
||||
box.addEventListener('input', handleEvent.manageFavicons);
|
||||
$$('input', box).forEach(el => {
|
||||
el.dataset.id = el.id;
|
||||
el.id = null;
|
||||
});
|
||||
}
|
||||
}).then(() => {
|
||||
const box = $('#message-box');
|
||||
box.removeEventListener('change', handleEvent.manageFavicons);
|
||||
box.removeEventListener('input', handleEvent.manageFavicons);
|
||||
});
|
||||
},
|
||||
|
||||
manageFavicons(event) {
|
||||
event.stopPropagation();
|
||||
const box = $('#message-box-contents');
|
||||
|
||||
let value = $('[data-id="manage.newUI.favicons"]', box).checked;
|
||||
prefs.set('manage.newUI.favicons', value);
|
||||
// Updating the hidden inputs; not the inputs in the message box
|
||||
$('#manage.newUI.favicons').checked = value;
|
||||
|
||||
value = $('[data-id="manage.newUI.faviconsGray"]', box).checked;
|
||||
prefs.set('manage.newUI.faviconsGray', value);
|
||||
$('#manage.newUI.faviconsGray').checked = value;
|
||||
|
||||
value = $('[data-id="manage.newUI.targets"]', box).value;
|
||||
prefs.set('manage.newUI.targets', value);
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
function handleUpdate(style, {reason, method} = {}) {
|
||||
if (reason === 'editPreview' || reason === 'editPreviewEnd') return;
|
||||
let entry;
|
||||
let oldEntry = $(UI.ENTRY_ID_PREFIX + style.id);
|
||||
if (oldEntry && method === 'styleUpdated') {
|
||||
handleToggledOrCodeOnly();
|
||||
}
|
||||
entry = entry || UI.createStyleElement({style});
|
||||
if (oldEntry) {
|
||||
// Make sure to update the filter checkbox since it's state isn't saved to the style
|
||||
$('.entry-filter-toggle', entry).checked = $('.entry-filter-toggle', oldEntry).checked;
|
||||
if (oldEntry.styleNameLowerCase === entry.styleNameLowerCase) {
|
||||
installed.replaceChild(entry, oldEntry);
|
||||
} else {
|
||||
oldEntry.remove();
|
||||
}
|
||||
}
|
||||
if ((reason === 'update' || reason === 'install') && entry.matches('.updatable')) {
|
||||
handleUpdateInstalled(entry, reason);
|
||||
}
|
||||
filterAndAppend({entry}).then(sorter.update);
|
||||
|
||||
if (!entry.matches('.hidden') && reason !== 'import') {
|
||||
animateElement(entry);
|
||||
requestAnimationFrame(() => scrollElementIntoView(entry));
|
||||
}
|
||||
UI.getFaviconImgSrc(entry);
|
||||
|
||||
function handleToggledOrCodeOnly() {
|
||||
const newStyleMeta = getStyleWithNoCode(style);
|
||||
const diff = objectDiff(oldEntry.styleMeta, newStyleMeta)
|
||||
.filter(({key, path}) => path || (!key.startsWith('original') && !key.endsWith('Date')));
|
||||
if (diff.length === 0) {
|
||||
// only code was modified
|
||||
entry = oldEntry;
|
||||
oldEntry = null;
|
||||
}
|
||||
if (diff.length === 1 && diff[0].key === 'enabled') {
|
||||
oldEntry.classList.toggle('enabled', style.enabled);
|
||||
oldEntry.classList.toggle('disabled', !style.enabled);
|
||||
$$('.entry-state-toggle', oldEntry).forEach(el => (el.checked = style.enabled));
|
||||
oldEntry.styleMeta = newStyleMeta;
|
||||
entry = oldEntry;
|
||||
UI.addLabels(entry);
|
||||
oldEntry = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function handleDelete(id) {
|
||||
const node = $(UI.ENTRY_ID_PREFIX + id);
|
||||
if (node) {
|
||||
node.remove();
|
||||
if (node.matches('.can-update')) {
|
||||
const btnApply = $('#apply-all-updates');
|
||||
btnApply.dataset.value = Number(btnApply.dataset.value) - 1;
|
||||
}
|
||||
showFiltersStats();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function switchUI({styleOnly} = {}) {
|
||||
const current = {enabled: true};
|
||||
const changed = {};
|
||||
let someChanged = false;
|
||||
// ensure the global option is processed first
|
||||
for (const el of $$('[id^="manage.newUI."]')) {
|
||||
const id = el.id.replace(/^manage\.newUI\.?/, '');
|
||||
const value = el.type === 'checkbox' ? el.checked : Number(el.value);
|
||||
const valueChanged = value !== UI[id];
|
||||
current[id] = value;
|
||||
changed[id] = valueChanged;
|
||||
someChanged |= valueChanged;
|
||||
}
|
||||
|
||||
if (!styleOnly && !someChanged) {
|
||||
return;
|
||||
}
|
||||
|
||||
Object.assign(UI, current);
|
||||
installed.classList.toggle('has-favicons', UI.favicons);
|
||||
installed.classList.toggle('faviconsGray', UI.faviconsGray);
|
||||
|
||||
if (styleOnly) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const missingFavicons = UI.favicons && !$('.entry-applies-to img[src]');
|
||||
if (changed.targets) {
|
||||
for (const targetWrapper of $$('.entry .targets')) {
|
||||
const targets = $$('.target', targetWrapper);
|
||||
targets.forEach((target, indx) => {
|
||||
target.classList.toggle('extra', indx >= UI.targets);
|
||||
});
|
||||
$('.applies-to-extra-expander', targetWrapper)
|
||||
.classList.toggle('hidden', targets.length <= UI.targets);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (missingFavicons) {
|
||||
debounce(UI.getFaviconImgSrc);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function onVisibilityChange() {
|
||||
switch (document.visibilityState) {
|
||||
// page restored without reloading via history navigation (currently only in FF)
|
||||
// the catch here is that DOM may be outdated so we'll at least refresh the just edited style
|
||||
// assuming other changes aren't important enough to justify making a complicated DOM sync
|
||||
case 'visible':
|
||||
if (sessionStorage.justEditedStyleId) {
|
||||
API.getStyle(Number(sessionStorage.justEditedStyleId), true)
|
||||
.then(style => {
|
||||
handleUpdate(style, {method: 'styleUpdated'});
|
||||
});
|
||||
delete sessionStorage.justEditedStyleId;
|
||||
}
|
||||
break;
|
||||
// going away
|
||||
case 'hidden':
|
||||
history.replaceState({scrollY: window.scrollY}, document.title);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function removeSelection() {
|
||||
const sel = window.getSelection ? window.getSelection() : document.selection;
|
||||
if (sel) {
|
||||
if (sel.removeAllRanges) {
|
||||
sel.removeAllRanges();
|
||||
} else if (sel.empty) {
|
||||
sel.empty();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function lazyLoad() {
|
||||
setTimeout(() => {
|
||||
$$('link[data-href]').forEach(link => {
|
||||
link.href = link.dataset.href;
|
||||
link.removeAttribute('data-href');
|
||||
});
|
||||
$$('script[data-src]').forEach(script => {
|
||||
script.src = script.dataset.src;
|
||||
script.removeAttribute('data-src');
|
||||
});
|
||||
}, 500);
|
||||
}
|
||||
|
||||
function embedOptions() {
|
||||
let options = $('#stylus-embedded-options');
|
||||
if (!options) {
|
||||
options = document.createElement('iframe');
|
||||
options.id = 'stylus-embedded-options';
|
||||
options.src = '/options.html';
|
||||
document.documentElement.appendChild(options);
|
||||
}
|
||||
options.focus();
|
||||
}
|
||||
|
||||
function unembedOptions() {
|
||||
const options = $('#stylus-embedded-options');
|
||||
if (options) {
|
||||
options.contentWindow.document.body.classList.add('scaleout');
|
||||
options.classList.add('fadeout');
|
||||
animateElement(options, {
|
||||
className: 'fadeout',
|
||||
onComplete: () => options.remove(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
router.watch({hash: '#stylus-options'}, state => {
|
||||
if (state) {
|
||||
embedOptions();
|
||||
} else {
|
||||
unembedOptions();
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('closeOptions', () => {
|
||||
router.updateHash('');
|
||||
});
|
387
manage/manage-ui.js
Normal file
387
manage/manage-ui.js
Normal file
|
@ -0,0 +1,387 @@
|
|||
/*
|
||||
global prefs t $ $$ $create template tWordBreak
|
||||
installed sorter filterAndAppend handleEvent
|
||||
animateElement scrollElementIntoView formatDate
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
const UI = {
|
||||
ENTRY_ID_PREFIX_RAW: 'style-',
|
||||
ENTRY_ID_PREFIX: '#style-',
|
||||
|
||||
TARGET_TYPES: ['domains', 'urls', 'urlPrefixes', 'regexps'],
|
||||
GET_FAVICON_URL: 'https://www.google.com/s2/favicons?domain=',
|
||||
OWN_ICON: chrome.runtime.getManifest().icons['16'],
|
||||
|
||||
favicons: prefs.get('manage.newUI.favicons'),
|
||||
faviconsGray: prefs.get('manage.newUI.faviconsGray'),
|
||||
targets: prefs.get('manage.newUI.targets'),
|
||||
|
||||
labels: {
|
||||
'usercss': {
|
||||
is: ({style}) => typeof style.usercssData !== 'undefined',
|
||||
text: 'usercss'
|
||||
},
|
||||
'disabled': {
|
||||
is: ({entry}) => !$('.entry-state-toggle', entry).checked,
|
||||
text: t('genericDisabledLabel')
|
||||
}
|
||||
},
|
||||
|
||||
init: () => {
|
||||
$('.ext-version').textContent = `v${chrome.runtime.getManifest().version}`;
|
||||
|
||||
// translate CSS manually
|
||||
// #update-all-no-updates[data-skipped-edited="true"]::after {
|
||||
// content: " ${t('updateAllCheckSucceededSomeEdited')}";
|
||||
// }
|
||||
document.head.appendChild($create('style', `
|
||||
body.all-styles-hidden-by-filters #installed:after {
|
||||
content: "${t('filteredStylesAllHidden')}";
|
||||
}
|
||||
`));
|
||||
// remove update filter on init
|
||||
const search = $('#search');
|
||||
search.value = search.value.replace(UI.searchFilters.updatable.query, '');
|
||||
// update filter labels to match location.search
|
||||
UI.updateFilterLabels();
|
||||
},
|
||||
|
||||
showStyles: (styles = [], matchUrlIds) => {
|
||||
UI.addHeaderLabels();
|
||||
|
||||
const sorted = sorter.sort({
|
||||
styles: styles.map(style => ({
|
||||
style,
|
||||
name: (style.name || '').toLocaleLowerCase() + '\n' + style.name,
|
||||
})),
|
||||
});
|
||||
let index = 0;
|
||||
let firstRun = true;
|
||||
installed.dataset.total = styles.length;
|
||||
const scrollY = (history.state || {}).scrollY;
|
||||
const shouldRenderAll = scrollY > window.innerHeight || sessionStorage.justEditedStyleId;
|
||||
const renderBin = document.createDocumentFragment();
|
||||
if (scrollY) {
|
||||
renderStyles();
|
||||
} else {
|
||||
requestAnimationFrame(renderStyles);
|
||||
}
|
||||
|
||||
function renderStyles() {
|
||||
const t0 = performance.now();
|
||||
let rendered = 0;
|
||||
while (
|
||||
index < sorted.length &&
|
||||
// eslint-disable-next-line no-unmodified-loop-condition
|
||||
(shouldRenderAll || ++rendered < 20 || performance.now() - t0 < 10)
|
||||
) {
|
||||
const info = sorted[index++];
|
||||
const entry = UI.createStyleElement(info);
|
||||
if (matchUrlIds && !matchUrlIds.includes(info.style.id)) {
|
||||
entry.classList.add('not-matching');
|
||||
rendered--;
|
||||
}
|
||||
renderBin.appendChild(entry);
|
||||
}
|
||||
filterAndAppend({container: renderBin}).then(sorter.updateStripes);
|
||||
if (index < sorted.length) {
|
||||
requestAnimationFrame(renderStyles);
|
||||
if (firstRun) setTimeout(UI.getFaviconImgSrc);
|
||||
firstRun = false;
|
||||
return;
|
||||
}
|
||||
setTimeout(UI.getFaviconImgSrc);
|
||||
if (sessionStorage.justEditedStyleId) {
|
||||
UI.highlightEditedStyle();
|
||||
} else if ('scrollY' in (history.state || {})) {
|
||||
setTimeout(window.scrollTo, 0, 0, history.state.scrollY);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
createStyleElement: ({style, name}) => {
|
||||
// query the sub-elements just once, then reuse the references
|
||||
if (!(UI._parts || {}).UI) {
|
||||
const entry = template['style'];
|
||||
UI._parts = {
|
||||
UI: true,
|
||||
entry,
|
||||
entryClassBase: entry.className,
|
||||
checker: $('.entry-state-toggle', entry) || {},
|
||||
nameLink: $('.entry-name', entry),
|
||||
editLink: $('.entry-edit', entry) || {},
|
||||
editHrefBase: 'edit.html?id=',
|
||||
appliesTo: $('.entry-applies-to', entry),
|
||||
targets: $('.targets', entry),
|
||||
decorations: {
|
||||
urlPrefixesAfter: '*',
|
||||
regexpsBefore: '/',
|
||||
regexpsAfter: '/',
|
||||
},
|
||||
};
|
||||
}
|
||||
const parts = UI._parts;
|
||||
const configurable = style.usercssData && style.usercssData.vars && Object.keys(style.usercssData.vars).length > 0;
|
||||
|
||||
parts.checker.checked = style.enabled;
|
||||
|
||||
$('.entry-name-text', parts.nameLink).textContent = tWordBreak(style.name);
|
||||
parts.nameLink.href = parts.editLink.href = parts.editHrefBase + style.id;
|
||||
|
||||
// clear the code to free up some memory
|
||||
// (note, style is already a deep copy)
|
||||
style.sourceCode = null;
|
||||
style.sections.forEach(section => (section.code = null));
|
||||
|
||||
const entry = parts.entry.cloneNode(true);
|
||||
entry.id = UI.ENTRY_ID_PREFIX_RAW + style.id;
|
||||
entry.styleId = style.id;
|
||||
entry.styleNameLowerCase = name || style.name.toLocaleLowerCase();
|
||||
entry.styleMeta = style;
|
||||
entry.className = parts.entryClassBase + ' ' +
|
||||
(style.enabled ? 'enabled' : 'disabled') +
|
||||
(style.updateUrl ? ' updatable' : '') +
|
||||
(style.usercssData ? ' usercss' : '');
|
||||
|
||||
let el = $('.entry-homepage', entry);
|
||||
el.classList.toggle('invisible', !style.url);
|
||||
el.href = style.url || '';
|
||||
el.dataset.title = style.url ? `${t('externalHomepage')}: ${style.url}` : '';
|
||||
|
||||
const support = style.usercssData && style.usercssData.supportURL || '';
|
||||
el = $('.entry-support', entry);
|
||||
el.classList.toggle('invisible', !support);
|
||||
el.href = support;
|
||||
el.dataset.title = support ? `${t('externalSupport')}: ${support}` : '';
|
||||
|
||||
$('.entry-configure-usercss', entry).classList.toggle('invisible', !configurable);
|
||||
if (style.updateUrl) {
|
||||
$('.entry-updater-placeholder', entry).replaceWith(template.updaterIcons.cloneNode(true));
|
||||
}
|
||||
|
||||
$('.entry-version-value', entry).textContent = style.usercssData && style.usercssData.version || '';
|
||||
|
||||
const lastUpdate = $('.entry-last-update-value', entry);
|
||||
lastUpdate.textContent = UI.getDateString(style.updateDate);
|
||||
// Show install & last update in title
|
||||
lastUpdate.dataset.title = [
|
||||
{prop: 'installDate', name: 'dateInstalled'},
|
||||
{prop: 'updateDate', name: 'dateUpdated'},
|
||||
].map(({prop, name}) => t(name) + ': ' + (formatDate(entry.styleMeta[prop]) || '—')).join('\n');
|
||||
|
||||
UI.createStyleTargetsElement({entry, style});
|
||||
UI.addLabels(entry);
|
||||
|
||||
return entry;
|
||||
},
|
||||
|
||||
getDateString: date => {
|
||||
const newDate = new Date(date);
|
||||
return newDate instanceof Date && isFinite(newDate)
|
||||
? newDate.toISOString().split('T')[0].replace(/-/g, '.')
|
||||
: '';
|
||||
},
|
||||
|
||||
updateFilterLabels: () => {
|
||||
const filterLabels = $$('#filters-wrapper .search-filter input');
|
||||
filterLabels.forEach(cb => {
|
||||
cb.checked = false;
|
||||
cb.parentElement.classList.remove('checked');
|
||||
});
|
||||
const filters = Object.values(UI.searchFilters);
|
||||
$('#search').value.split(' ').forEach(part => {
|
||||
const filter = filters.find(entry => entry.query === part);
|
||||
if (filter) {
|
||||
const button = filterLabels.filter(btn => btn.id === filter.id);
|
||||
if (button.length) {
|
||||
button[0].checked = true;
|
||||
button[0].parentElement.classList.add('checked');
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
createStyleTargetsElement: ({entry, style}) => {
|
||||
const parts = UI._parts;
|
||||
const entryTargets = $('.targets', entry);
|
||||
const targets = parts.targets.cloneNode(true);
|
||||
let container = targets;
|
||||
let numTargets = 0;
|
||||
let extraClass = '';
|
||||
const displayed = new Set();
|
||||
for (const type of UI.TARGET_TYPES) {
|
||||
for (const section of style.sections) {
|
||||
for (const targetValue of section[type] || []) {
|
||||
if (displayed.has(targetValue)) {
|
||||
continue;
|
||||
}
|
||||
displayed.add(targetValue);
|
||||
const element = template.appliesToTarget.cloneNode(true);
|
||||
if (numTargets === UI.targets) {
|
||||
extraClass = ' extra';
|
||||
}
|
||||
element.dataset.type = type;
|
||||
element.dataset.index = numTargets;
|
||||
element.dataset.title =
|
||||
(parts.decorations[type + 'Before'] || '') +
|
||||
targetValue +
|
||||
(parts.decorations[type + 'After'] || '');
|
||||
element.className += extraClass;
|
||||
container.appendChild(element);
|
||||
numTargets++;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Include hidden expander in case user changes UI.targets
|
||||
container.appendChild(template.extraAppliesTo.cloneNode(true));
|
||||
if (numTargets <= UI.targets) {
|
||||
$('.applies-to-extra-expander', container).classList.add('hidden');
|
||||
}
|
||||
if (numTargets) {
|
||||
entryTargets.parentElement.replaceChild(targets, entryTargets);
|
||||
} else if (!entry.classList.contains('global') || !entryTargets.firstElementChild) {
|
||||
if (entryTargets.firstElementChild) {
|
||||
entryTargets.textContent = '';
|
||||
}
|
||||
entryTargets.appendChild(template.appliesToEverything.cloneNode(true));
|
||||
}
|
||||
entry.classList.toggle('global', !numTargets);
|
||||
},
|
||||
|
||||
// This order matters
|
||||
searchFilters: {
|
||||
enabled: {
|
||||
id: 'manage.onlyEnabled',
|
||||
query: 'is:enabled',
|
||||
invert: 'disabled'
|
||||
},
|
||||
disabled: {
|
||||
id: 'manage.onlyEnabled.invert',
|
||||
query: 'is:disabled',
|
||||
invert: 'enabled'
|
||||
},
|
||||
usercss: {
|
||||
id: 'manage.onlyUsercss',
|
||||
query: 'is:usercss',
|
||||
invert: 'original'
|
||||
},
|
||||
original: {
|
||||
id: 'manage.onlyUsercss.invert',
|
||||
query: 'is:nonusercss',
|
||||
invert: 'usercss'
|
||||
},
|
||||
local: {
|
||||
id: 'manage.onlyLocal',
|
||||
query: 'is:local',
|
||||
invert: 'external'
|
||||
},
|
||||
external: {
|
||||
id: 'manage.onlyLocal.invert',
|
||||
query: 'is:external',
|
||||
invert: 'local'
|
||||
},
|
||||
// only checkbox; all others are radio buttons
|
||||
updatable: {
|
||||
id: 'only-updates',
|
||||
query: '', // 'has:updates',
|
||||
},
|
||||
reset: {
|
||||
id: 'reset-filters',
|
||||
query: ''
|
||||
}
|
||||
},
|
||||
|
||||
getFaviconImgSrc: (container = installed) => {
|
||||
if (!UI.favicons) return;
|
||||
const regexpRemoveNegativeLookAhead = /(\?!([^)]+\))|\(\?![\w(]+[^)]+[\w|)]+)/g;
|
||||
// replace extra characters & all but the first group entry "(abc|def|ghi)xyz" => abcxyz
|
||||
const regexpReplaceExtraCharacters = /[\\(]|((\|\w+)+\))/g;
|
||||
const regexpMatchRegExp = /[\w-]+[.(]+(com|org|co|net|im|io|edu|gov|biz|info|de|cn|uk|nl|eu|ru)\b/g;
|
||||
const regexpMatchDomain = /^.*?:\/\/([^/]+)/;
|
||||
for (const target of $$('.target', container)) {
|
||||
const type = target.dataset.type;
|
||||
const targetValue = target.dataset.title;
|
||||
if (!targetValue) continue;
|
||||
let favicon = '';
|
||||
if (type === 'domains') {
|
||||
favicon = UI.GET_FAVICON_URL + targetValue;
|
||||
} else if (targetValue.includes('chrome-extension:') || targetValue.includes('moz-extension:')) {
|
||||
favicon = UI.OWN_ICON;
|
||||
} else if (type === 'regexps') {
|
||||
favicon = targetValue
|
||||
.replace(regexpRemoveNegativeLookAhead, '')
|
||||
.replace(regexpReplaceExtraCharacters, '')
|
||||
.match(regexpMatchRegExp);
|
||||
favicon = favicon ? UI.GET_FAVICON_URL + favicon.shift() : '';
|
||||
} else {
|
||||
favicon = targetValue.includes('://') && targetValue.match(regexpMatchDomain);
|
||||
favicon = favicon ? UI.GET_FAVICON_URL + favicon[1] : '';
|
||||
}
|
||||
if (favicon) {
|
||||
const el = $('img[src], svg', target);
|
||||
if (!el || el.localName === 'svg') {
|
||||
const img = $('img', target);
|
||||
img.dataset.src = favicon;
|
||||
} else if ((target.dataset.src || target.src) !== favicon) {
|
||||
delete el.src;
|
||||
el.dataset.src = favicon;
|
||||
}
|
||||
}
|
||||
}
|
||||
handleEvent.loadFavicons();
|
||||
},
|
||||
|
||||
highlightEditedStyle: () => {
|
||||
if (!sessionStorage.justEditedStyleId) return;
|
||||
const entry = $(UI.ENTRY_ID_PREFIX + sessionStorage.justEditedStyleId);
|
||||
delete sessionStorage.justEditedStyleId;
|
||||
if (entry) {
|
||||
animateElement(entry);
|
||||
requestAnimationFrame(() => scrollElementIntoView(entry));
|
||||
}
|
||||
},
|
||||
|
||||
addHeaderLabels: () => {
|
||||
const header = $('.header-name');
|
||||
const span = document.createElement('span');
|
||||
let labels = $('.header-labels', header);
|
||||
if (labels) {
|
||||
labels.textContent = '';
|
||||
} else {
|
||||
labels = span.cloneNode();
|
||||
}
|
||||
const label = document.createElement('a');
|
||||
labels.className = 'header-labels';
|
||||
label.className = 'header-label sortable tt-s';
|
||||
label.href = '#';
|
||||
Object.keys(UI.labels).forEach(item => {
|
||||
const newLabel = label.cloneNode(true);
|
||||
const text = UI.labels[item].text;
|
||||
newLabel.dataset.type = item;
|
||||
newLabel.textContent = text;
|
||||
newLabel.appendChild(span.cloneNode());
|
||||
newLabel.dataset.title = t('sortLabel', text);
|
||||
labels.appendChild(newLabel);
|
||||
});
|
||||
header.appendChild(labels);
|
||||
},
|
||||
|
||||
addLabels: entry => {
|
||||
const style = entry.styleMeta;
|
||||
const container = $('.entry-labels', entry);
|
||||
const label = document.createElement('span');
|
||||
const labels = document.createElement('span');
|
||||
labels.className = 'entry-labels';
|
||||
label.className = 'entry-label';
|
||||
Object.keys(UI.labels).forEach(item => {
|
||||
if (UI.labels[item].is({entry, style})) {
|
||||
const newLabel = label.cloneNode(true);
|
||||
newLabel.dataset.type = item;
|
||||
newLabel.textContent = UI.labels[item].text;
|
||||
labels.appendChild(newLabel);
|
||||
}
|
||||
});
|
||||
container.replaceWith(labels);
|
||||
}
|
||||
};
|
1573
manage/manage.css
1573
manage/manage.css
File diff suppressed because it is too large
Load Diff
741
manage/manage.js
741
manage/manage.js
|
@ -1,741 +0,0 @@
|
|||
/*
|
||||
global messageBox getStyleWithNoCode
|
||||
filterAndAppend showFiltersStats
|
||||
checkUpdate handleUpdateInstalled
|
||||
objectDiff
|
||||
configDialog
|
||||
sorter msg prefs API onDOMready $ $$ $create template setupLivePrefs
|
||||
URLS enforceInputRange t tWordBreak formatDate
|
||||
getOwnTab getActiveTab openURL animateElement sessionStorageHash debounce
|
||||
scrollElementIntoView CHROME VIVALDI FIREFOX router
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
let installed;
|
||||
|
||||
const ENTRY_ID_PREFIX_RAW = 'style-';
|
||||
const ENTRY_ID_PREFIX = '#' + ENTRY_ID_PREFIX_RAW;
|
||||
|
||||
const newUI = {
|
||||
enabled: prefs.get('manage.newUI'),
|
||||
favicons: prefs.get('manage.newUI.favicons'),
|
||||
faviconsGray: prefs.get('manage.newUI.faviconsGray'),
|
||||
targets: prefs.get('manage.newUI.targets'),
|
||||
renderClass() {
|
||||
document.documentElement.classList.toggle('newUI', newUI.enabled);
|
||||
},
|
||||
};
|
||||
newUI.renderClass();
|
||||
|
||||
const TARGET_TYPES = ['domains', 'urls', 'urlPrefixes', 'regexps'];
|
||||
const GET_FAVICON_URL = 'https://www.google.com/s2/favicons?domain=';
|
||||
const OWN_ICON = chrome.runtime.getManifest().icons['16'];
|
||||
|
||||
const handleEvent = {};
|
||||
|
||||
Promise.all([
|
||||
API.getAllStyles(true),
|
||||
// FIXME: integrate this into filter.js
|
||||
router.getSearch('search') && API.searchDB({query: router.getSearch('search')}),
|
||||
Promise.all([
|
||||
onDOMready(),
|
||||
prefs.initializing,
|
||||
])
|
||||
.then(() => {
|
||||
initGlobalEvents();
|
||||
if (!VIVALDI) {
|
||||
$$('#header select').forEach(el => el.adjustWidth());
|
||||
}
|
||||
if (FIREFOX && 'update' in (chrome.commands || {})) {
|
||||
const btn = $('#manage-shortcuts-button');
|
||||
btn.classList.remove('chromium-only');
|
||||
btn.onclick = API.optionsCustomizeHotkeys;
|
||||
}
|
||||
}),
|
||||
]).then(args => {
|
||||
showStyles(...args);
|
||||
});
|
||||
|
||||
msg.onExtension(onRuntimeMessage);
|
||||
|
||||
function onRuntimeMessage(msg) {
|
||||
switch (msg.method) {
|
||||
case 'styleUpdated':
|
||||
case 'styleAdded':
|
||||
API.getStyle(msg.style.id, true)
|
||||
.then(style => handleUpdate(style, msg));
|
||||
break;
|
||||
case 'styleDeleted':
|
||||
handleDelete(msg.style.id);
|
||||
break;
|
||||
case 'styleApply':
|
||||
case 'styleReplaceAll':
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
setTimeout(sorter.updateStripes, 0, {onlyWhenColumnsChanged: true});
|
||||
}
|
||||
|
||||
|
||||
function initGlobalEvents() {
|
||||
installed = $('#installed');
|
||||
installed.onclick = handleEvent.entryClicked;
|
||||
$('#manage-options-button').onclick = () => {
|
||||
router.updateHash('#stylus-options');
|
||||
};
|
||||
{
|
||||
const btn = $('#manage-shortcuts-button');
|
||||
btn.onclick = btn.onclick || (() => openURL({url: URLS.configureCommands}));
|
||||
}
|
||||
$$('#header a[href^="http"]').forEach(a => (a.onclick = handleEvent.external));
|
||||
// show date installed & last update on hover
|
||||
installed.addEventListener('mouseover', handleEvent.lazyAddEntryTitle);
|
||||
installed.addEventListener('mouseout', handleEvent.lazyAddEntryTitle);
|
||||
|
||||
document.addEventListener('visibilitychange', onVisibilityChange);
|
||||
|
||||
$$('[data-toggle-on-click]').forEach(el => {
|
||||
// dataset on SVG doesn't work in Chrome 49-??, works in 57+
|
||||
const target = $(el.getAttribute('data-toggle-on-click'));
|
||||
el.onclick = event => {
|
||||
event.preventDefault();
|
||||
target.classList.toggle('hidden');
|
||||
if (target.classList.contains('hidden')) {
|
||||
el.removeAttribute('open');
|
||||
} else {
|
||||
el.setAttribute('open', '');
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// triggered automatically by setupLivePrefs() below
|
||||
enforceInputRange($('#manage.newUI.targets'));
|
||||
|
||||
// N.B. triggers existing onchange listeners
|
||||
setupLivePrefs();
|
||||
sorter.init();
|
||||
|
||||
prefs.subscribe([
|
||||
'manage.newUI',
|
||||
'manage.newUI.favicons',
|
||||
'manage.newUI.faviconsGray',
|
||||
'manage.newUI.targets',
|
||||
], () => switchUI());
|
||||
|
||||
switchUI({styleOnly: true});
|
||||
|
||||
// translate CSS manually
|
||||
document.head.appendChild($create('style', `
|
||||
.disabled h2::after {
|
||||
content: "${t('genericDisabledLabel')}";
|
||||
}
|
||||
#update-all-no-updates[data-skipped-edited="true"]::after {
|
||||
content: " ${t('updateAllCheckSucceededSomeEdited')}";
|
||||
}
|
||||
body.all-styles-hidden-by-filters::after {
|
||||
content: "${t('filteredStylesAllHidden')}";
|
||||
}
|
||||
`));
|
||||
}
|
||||
|
||||
function showStyles(styles = [], matchUrlIds) {
|
||||
const sorted = sorter.sort({
|
||||
styles: styles.map(style => ({
|
||||
style,
|
||||
name: (style.name || '').toLocaleLowerCase() + '\n' + style.name,
|
||||
})),
|
||||
});
|
||||
let index = 0;
|
||||
let firstRun = true;
|
||||
installed.dataset.total = styles.length;
|
||||
const scrollY = (history.state || {}).scrollY;
|
||||
const shouldRenderAll = scrollY > window.innerHeight || sessionStorage.justEditedStyleId;
|
||||
const renderBin = document.createDocumentFragment();
|
||||
if (scrollY) {
|
||||
renderStyles();
|
||||
} else {
|
||||
requestAnimationFrame(renderStyles);
|
||||
}
|
||||
|
||||
function renderStyles() {
|
||||
const t0 = performance.now();
|
||||
let rendered = 0;
|
||||
while (
|
||||
index < sorted.length &&
|
||||
// eslint-disable-next-line no-unmodified-loop-condition
|
||||
(shouldRenderAll || ++rendered < 20 || performance.now() - t0 < 10)
|
||||
) {
|
||||
const info = sorted[index++];
|
||||
const entry = createStyleElement(info);
|
||||
if (matchUrlIds && !matchUrlIds.includes(info.style.id)) {
|
||||
entry.classList.add('not-matching');
|
||||
rendered--;
|
||||
}
|
||||
renderBin.appendChild(entry);
|
||||
}
|
||||
filterAndAppend({container: renderBin}).then(sorter.updateStripes);
|
||||
if (index < sorted.length) {
|
||||
requestAnimationFrame(renderStyles);
|
||||
if (firstRun) setTimeout(getFaviconImgSrc);
|
||||
firstRun = false;
|
||||
return;
|
||||
}
|
||||
setTimeout(getFaviconImgSrc);
|
||||
if (sessionStorage.justEditedStyleId) {
|
||||
highlightEditedStyle();
|
||||
} else if ('scrollY' in (history.state || {})) {
|
||||
setTimeout(window.scrollTo, 0, 0, history.state.scrollY);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function createStyleElement({style, name}) {
|
||||
// query the sub-elements just once, then reuse the references
|
||||
if ((createStyleElement.parts || {}).newUI !== newUI.enabled) {
|
||||
const entry = template[`style${newUI.enabled ? 'Compact' : ''}`];
|
||||
createStyleElement.parts = {
|
||||
newUI: newUI.enabled,
|
||||
entry,
|
||||
entryClassBase: entry.className,
|
||||
checker: $('.checker', entry) || {},
|
||||
nameLink: $('.style-name-link', entry),
|
||||
editLink: $('.style-edit-link', entry) || {},
|
||||
editHrefBase: 'edit.html?id=',
|
||||
homepage: $('.homepage', entry),
|
||||
homepageIcon: template[`homepageIcon${newUI.enabled ? 'Small' : 'Big'}`],
|
||||
appliesTo: $('.applies-to', entry),
|
||||
targets: $('.targets', entry),
|
||||
expander: $('.expander', entry),
|
||||
decorations: {
|
||||
urlPrefixesAfter: '*',
|
||||
regexpsBefore: '/',
|
||||
regexpsAfter: '/',
|
||||
},
|
||||
oldConfigure: !newUI.enabled && $('.configure-usercss', entry),
|
||||
oldCheckUpdate: !newUI.enabled && $('.check-update', entry),
|
||||
oldUpdate: !newUI.enabled && $('.update', entry),
|
||||
};
|
||||
}
|
||||
const parts = createStyleElement.parts;
|
||||
const configurable = style.usercssData && style.usercssData.vars && Object.keys(style.usercssData.vars).length > 0;
|
||||
parts.checker.checked = style.enabled;
|
||||
parts.nameLink.textContent = tWordBreak(style.name);
|
||||
parts.nameLink.href = parts.editLink.href = parts.editHrefBase + style.id;
|
||||
parts.homepage.href = parts.homepage.title = style.url || '';
|
||||
if (!newUI.enabled) {
|
||||
parts.oldConfigure.classList.toggle('hidden', !configurable);
|
||||
parts.oldCheckUpdate.classList.toggle('hidden', !style.updateUrl);
|
||||
parts.oldUpdate.classList.toggle('hidden', !style.updateUrl);
|
||||
}
|
||||
|
||||
// clear the code to free up some memory
|
||||
// (note, style is already a deep copy)
|
||||
style.sourceCode = null;
|
||||
style.sections.forEach(section => (section.code = null));
|
||||
|
||||
const entry = parts.entry.cloneNode(true);
|
||||
entry.id = ENTRY_ID_PREFIX_RAW + style.id;
|
||||
entry.styleId = style.id;
|
||||
entry.styleNameLowerCase = name || style.name.toLocaleLowerCase();
|
||||
entry.styleMeta = style;
|
||||
entry.className = parts.entryClassBase + ' ' +
|
||||
(style.enabled ? 'enabled' : 'disabled') +
|
||||
(style.updateUrl ? ' updatable' : '') +
|
||||
(style.usercssData ? ' usercss' : '');
|
||||
|
||||
if (style.url) {
|
||||
$('.homepage', entry).appendChild(parts.homepageIcon.cloneNode(true));
|
||||
}
|
||||
if (style.updateUrl && newUI.enabled) {
|
||||
$('.actions', entry).appendChild(template.updaterIcons.cloneNode(true));
|
||||
}
|
||||
if (configurable && newUI.enabled) {
|
||||
$('.actions', entry).appendChild(template.configureIcon.cloneNode(true));
|
||||
}
|
||||
|
||||
createStyleTargetsElement({entry, style});
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
|
||||
function createStyleTargetsElement({entry, style}) {
|
||||
const parts = createStyleElement.parts;
|
||||
const entryTargets = $('.targets', entry);
|
||||
const targets = parts.targets.cloneNode(true);
|
||||
let container = targets;
|
||||
let numTargets = 0;
|
||||
const displayed = new Set();
|
||||
for (const type of TARGET_TYPES) {
|
||||
for (const section of style.sections) {
|
||||
for (const targetValue of section[type] || []) {
|
||||
if (displayed.has(targetValue)) {
|
||||
continue;
|
||||
}
|
||||
displayed.add(targetValue);
|
||||
const element = template.appliesToTarget.cloneNode(true);
|
||||
if (!newUI.enabled) {
|
||||
if (numTargets === 10) {
|
||||
container = container.appendChild(template.extraAppliesTo.cloneNode(true));
|
||||
} else if (numTargets > 0) {
|
||||
container.appendChild(template.appliesToSeparator.cloneNode(true));
|
||||
}
|
||||
}
|
||||
element.dataset.type = type;
|
||||
element.appendChild(
|
||||
document.createTextNode(
|
||||
(parts.decorations[type + 'Before'] || '') +
|
||||
targetValue +
|
||||
(parts.decorations[type + 'After'] || '')));
|
||||
container.appendChild(element);
|
||||
numTargets++;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (newUI.enabled) {
|
||||
if (numTargets > newUI.targets) {
|
||||
$('.applies-to', entry).classList.add('has-more');
|
||||
}
|
||||
}
|
||||
if (numTargets) {
|
||||
entryTargets.parentElement.replaceChild(targets, entryTargets);
|
||||
} else if (!entry.classList.contains('global') ||
|
||||
!entryTargets.firstElementChild) {
|
||||
if (entryTargets.firstElementChild) {
|
||||
entryTargets.textContent = '';
|
||||
}
|
||||
entryTargets.appendChild(template.appliesToEverything.cloneNode(true));
|
||||
}
|
||||
entry.classList.toggle('global', !numTargets);
|
||||
}
|
||||
|
||||
|
||||
function getFaviconImgSrc(container = installed) {
|
||||
if (!newUI.enabled || !newUI.favicons) return;
|
||||
const regexpRemoveNegativeLookAhead = /(\?!([^)]+\))|\(\?![\w(]+[^)]+[\w|)]+)/g;
|
||||
// replace extra characters & all but the first group entry "(abc|def|ghi)xyz" => abcxyz
|
||||
const regexpReplaceExtraCharacters = /[\\(]|((\|\w+)+\))/g;
|
||||
const regexpMatchRegExp = /[\w-]+[.(]+(com|org|co|net|im|io|edu|gov|biz|info|de|cn|uk|nl|eu|ru)\b/g;
|
||||
const regexpMatchDomain = /^.*?:\/\/([^/]+)/;
|
||||
for (const target of $$('.target', container)) {
|
||||
const type = target.dataset.type;
|
||||
const targetValue = target.textContent;
|
||||
if (!targetValue) continue;
|
||||
let favicon = '';
|
||||
if (type === 'domains') {
|
||||
favicon = GET_FAVICON_URL + targetValue;
|
||||
} else if (targetValue.includes('chrome-extension:') || targetValue.includes('moz-extension:')) {
|
||||
favicon = OWN_ICON;
|
||||
} else if (type === 'regexps') {
|
||||
favicon = targetValue
|
||||
.replace(regexpRemoveNegativeLookAhead, '')
|
||||
.replace(regexpReplaceExtraCharacters, '')
|
||||
.match(regexpMatchRegExp);
|
||||
favicon = favicon ? GET_FAVICON_URL + favicon.shift() : '';
|
||||
} else {
|
||||
favicon = targetValue.includes('://') && targetValue.match(regexpMatchDomain);
|
||||
favicon = favicon ? GET_FAVICON_URL + favicon[1] : '';
|
||||
}
|
||||
if (favicon) {
|
||||
const img = target.children[0];
|
||||
if (!img || img.localName !== 'img') {
|
||||
target.insertAdjacentElement('afterbegin', document.createElement('img'))
|
||||
.dataset.src = favicon;
|
||||
} else if ((img.dataset.src || img.src) !== favicon) {
|
||||
img.src = '';
|
||||
img.dataset.src = favicon;
|
||||
}
|
||||
}
|
||||
}
|
||||
handleEvent.loadFavicons();
|
||||
}
|
||||
|
||||
|
||||
Object.assign(handleEvent, {
|
||||
|
||||
ENTRY_ROUTES: {
|
||||
'.checker, .enable, .disable': 'toggle',
|
||||
'.style-name': 'name',
|
||||
'.homepage': 'external',
|
||||
'.check-update': 'check',
|
||||
'.update': 'update',
|
||||
'.delete': 'delete',
|
||||
'.applies-to .expander': 'expandTargets',
|
||||
'.configure-usercss': 'config'
|
||||
},
|
||||
|
||||
entryClicked(event) {
|
||||
const target = event.target;
|
||||
const entry = target.closest('.entry');
|
||||
for (const selector in handleEvent.ENTRY_ROUTES) {
|
||||
for (let el = target; el && el !== entry; el = el.parentElement) {
|
||||
if (el.matches(selector)) {
|
||||
const handler = handleEvent.ENTRY_ROUTES[selector];
|
||||
return handleEvent[handler].call(el, event, entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
name(event) {
|
||||
if (newUI.enabled) handleEvent.edit(event);
|
||||
},
|
||||
|
||||
edit(event) {
|
||||
if (event.altKey) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const left = event.button === 0;
|
||||
const middle = event.button === 1;
|
||||
const shift = event.shiftKey;
|
||||
const ctrl = event.ctrlKey;
|
||||
const openWindow = left && shift && !ctrl;
|
||||
const openBackgroundTab = (middle && !shift) || (left && ctrl && !shift);
|
||||
const openForegroundTab = (middle && shift) || (left && ctrl && shift);
|
||||
const url = $('[href]', event.target.closest('.entry')).href;
|
||||
if (openWindow || openBackgroundTab || openForegroundTab) {
|
||||
if (chrome.windows && openWindow) {
|
||||
chrome.windows.create(Object.assign(prefs.get('windowPosition'), {url}));
|
||||
} else {
|
||||
getOwnTab().then(({index}) => {
|
||||
openURL({
|
||||
url,
|
||||
index: index + 1,
|
||||
active: openForegroundTab
|
||||
});
|
||||
});
|
||||
}
|
||||
} else {
|
||||
onVisibilityChange();
|
||||
getActiveTab().then(tab => {
|
||||
sessionStorageHash('manageStylesHistory').set(tab.id, url);
|
||||
location.href = url;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
toggle(event, entry) {
|
||||
API.toggleStyle(entry.styleId, this.matches('.enable') || this.checked);
|
||||
},
|
||||
|
||||
check(event, entry) {
|
||||
event.preventDefault();
|
||||
checkUpdate(entry, {single: true});
|
||||
},
|
||||
|
||||
update(event, entry) {
|
||||
event.preventDefault();
|
||||
const json = entry.updatedCode;
|
||||
json.id = entry.styleId;
|
||||
API[json.usercssData ? 'installUsercss' : 'installStyle'](json);
|
||||
},
|
||||
|
||||
delete(event, entry) {
|
||||
event.preventDefault();
|
||||
const id = entry.styleId;
|
||||
animateElement(entry);
|
||||
messageBox({
|
||||
title: t('deleteStyleConfirm'),
|
||||
contents: entry.styleMeta.name,
|
||||
className: 'danger center',
|
||||
buttons: [t('confirmDelete'), t('confirmCancel')],
|
||||
})
|
||||
.then(({button}) => {
|
||||
if (button === 0) {
|
||||
API.deleteStyle(id);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
external(event) {
|
||||
if (event.shiftKey && !event.altKey && !event.ctrlKey && !event.metaKey) {
|
||||
// Shift-click = the built-in 'open in a new window' action
|
||||
return;
|
||||
}
|
||||
getOwnTab().then(({index}) => {
|
||||
openURL({
|
||||
url: event.target.closest('a').href,
|
||||
index: index + 1,
|
||||
active: !event.ctrlKey || event.shiftKey,
|
||||
});
|
||||
});
|
||||
event.preventDefault();
|
||||
},
|
||||
|
||||
expandTargets(event) {
|
||||
event.preventDefault();
|
||||
this.closest('.applies-to').classList.toggle('expanded');
|
||||
},
|
||||
|
||||
loadFavicons({all = false} = {}) {
|
||||
if (!installed.firstElementChild) return;
|
||||
let favicons = [];
|
||||
if (all) {
|
||||
favicons = $$('img[data-src]', installed);
|
||||
} else {
|
||||
const {left, top} = installed.firstElementChild.getBoundingClientRect();
|
||||
const x = Math.max(0, left);
|
||||
const y = Math.max(0, top);
|
||||
const first = document.elementFromPoint(x, y);
|
||||
const lastOffset = first.offsetTop + window.innerHeight;
|
||||
const numTargets = prefs.get('manage.newUI.targets');
|
||||
let entry = first && first.closest('.entry') || installed.children[0];
|
||||
while (entry && entry.offsetTop <= lastOffset) {
|
||||
favicons.push(...$$('img', entry).slice(0, numTargets).filter(img => img.dataset.src));
|
||||
entry = entry.nextElementSibling;
|
||||
}
|
||||
}
|
||||
let i = 0;
|
||||
for (const img of favicons) {
|
||||
img.src = img.dataset.src;
|
||||
delete img.dataset.src;
|
||||
// loading too many icons at once will block the page while the new layout is recalculated
|
||||
if (++i > 100) break;
|
||||
}
|
||||
if ($('img[data-src]', installed)) {
|
||||
debounce(handleEvent.loadFavicons, 1, {all: true});
|
||||
}
|
||||
},
|
||||
|
||||
config(event, {styleMeta}) {
|
||||
event.preventDefault();
|
||||
configDialog(styleMeta);
|
||||
},
|
||||
|
||||
lazyAddEntryTitle({type, target}) {
|
||||
const cell = target.closest('h2.style-name');
|
||||
if (cell) {
|
||||
const link = $('.style-name-link', cell);
|
||||
if (type === 'mouseover' && !link.title) {
|
||||
debounce(handleEvent.addEntryTitle, 50, link);
|
||||
} else {
|
||||
debounce.unregister(handleEvent.addEntryTitle);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
addEntryTitle(link) {
|
||||
const entry = link.closest('.entry');
|
||||
link.title = [
|
||||
{prop: 'installDate', name: 'dateInstalled'},
|
||||
{prop: 'updateDate', name: 'dateUpdated'},
|
||||
].map(({prop, name}) =>
|
||||
t(name) + ': ' + (formatDate(entry.styleMeta[prop]) || '—')).join('\n');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
function handleUpdate(style, {reason, method} = {}) {
|
||||
if (reason === 'editPreview' || reason === 'editPreviewEnd') return;
|
||||
let entry;
|
||||
let oldEntry = $(ENTRY_ID_PREFIX + style.id);
|
||||
if (oldEntry && method === 'styleUpdated') {
|
||||
handleToggledOrCodeOnly();
|
||||
}
|
||||
entry = entry || createStyleElement({style});
|
||||
if (oldEntry) {
|
||||
if (oldEntry.styleNameLowerCase === entry.styleNameLowerCase) {
|
||||
installed.replaceChild(entry, oldEntry);
|
||||
} else {
|
||||
oldEntry.remove();
|
||||
}
|
||||
}
|
||||
if ((reason === 'update' || reason === 'install') && entry.matches('.updatable')) {
|
||||
handleUpdateInstalled(entry, reason);
|
||||
}
|
||||
filterAndAppend({entry}).then(sorter.update);
|
||||
if (!entry.matches('.hidden') && reason !== 'import') {
|
||||
animateElement(entry);
|
||||
requestAnimationFrame(() => scrollElementIntoView(entry));
|
||||
}
|
||||
getFaviconImgSrc(entry);
|
||||
|
||||
function handleToggledOrCodeOnly() {
|
||||
const newStyleMeta = getStyleWithNoCode(style);
|
||||
const diff = objectDiff(oldEntry.styleMeta, newStyleMeta)
|
||||
.filter(({key, path}) => path || (!key.startsWith('original') && !key.endsWith('Date')));
|
||||
if (diff.length === 0) {
|
||||
// only code was modified
|
||||
entry = oldEntry;
|
||||
oldEntry = null;
|
||||
}
|
||||
if (diff.length === 1 && diff[0].key === 'enabled') {
|
||||
oldEntry.classList.toggle('enabled', style.enabled);
|
||||
oldEntry.classList.toggle('disabled', !style.enabled);
|
||||
$$('.checker', oldEntry).forEach(el => (el.checked = style.enabled));
|
||||
oldEntry.styleMeta = newStyleMeta;
|
||||
entry = oldEntry;
|
||||
oldEntry = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function handleDelete(id) {
|
||||
const node = $(ENTRY_ID_PREFIX + id);
|
||||
if (node) {
|
||||
node.remove();
|
||||
if (node.matches('.can-update')) {
|
||||
const btnApply = $('#apply-all-updates');
|
||||
btnApply.dataset.value = Number(btnApply.dataset.value) - 1;
|
||||
}
|
||||
showFiltersStats();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function switchUI({styleOnly} = {}) {
|
||||
const current = {};
|
||||
const changed = {};
|
||||
let someChanged = false;
|
||||
// ensure the global option is processed first
|
||||
for (const el of [$('#manage.newUI'), ...$$('[id^="manage.newUI."]')]) {
|
||||
const id = el.id.replace(/^manage\.newUI\.?/, '') || 'enabled';
|
||||
const value = el.type === 'checkbox' ? el.checked : Number(el.value);
|
||||
const valueChanged = value !== newUI[id] && (id === 'enabled' || current.enabled);
|
||||
current[id] = value;
|
||||
changed[id] = valueChanged;
|
||||
someChanged |= valueChanged;
|
||||
}
|
||||
|
||||
if (!styleOnly && !someChanged) {
|
||||
return;
|
||||
}
|
||||
|
||||
Object.assign(newUI, current);
|
||||
newUI.renderClass();
|
||||
installed.classList.toggle('has-favicons', newUI.favicons);
|
||||
$('#style-overrides').textContent = `
|
||||
.newUI .targets {
|
||||
max-height: ${newUI.targets * 18}px;
|
||||
}
|
||||
` + (newUI.faviconsGray ? `
|
||||
.newUI .target img {
|
||||
-webkit-filter: grayscale(1);
|
||||
filter: grayscale(1);
|
||||
opacity: .25;
|
||||
}
|
||||
` : `
|
||||
.newUI .target img {
|
||||
-webkit-filter: none;
|
||||
filter: none;
|
||||
opacity: 1;
|
||||
}
|
||||
`) + (CHROME >= 3004 ? `
|
||||
.newUI .entry {
|
||||
contain: strict;
|
||||
}
|
||||
.newUI .entry > * {
|
||||
contain: content;
|
||||
}
|
||||
.newUI .entry .actions {
|
||||
contain: none;
|
||||
}
|
||||
.newUI .target {
|
||||
contain: layout style;
|
||||
}
|
||||
.newUI .target img {
|
||||
contain: layout style size;
|
||||
}
|
||||
.newUI .entry.can-update,
|
||||
.newUI .entry.update-problem,
|
||||
.newUI .entry.update-done {
|
||||
contain: none;
|
||||
}
|
||||
` : '');
|
||||
|
||||
if (styleOnly) {
|
||||
return;
|
||||
}
|
||||
|
||||
const missingFavicons = newUI.enabled && newUI.favicons && !$('.applies-to img');
|
||||
if (changed.enabled || (missingFavicons && !createStyleElement.parts)) {
|
||||
installed.textContent = '';
|
||||
API.getAllStyles(true).then(showStyles);
|
||||
return;
|
||||
}
|
||||
if (changed.targets) {
|
||||
for (const targets of $$('.entry .targets')) {
|
||||
const hasMore = targets.children.length > newUI.targets;
|
||||
targets.parentElement.classList.toggle('has-more', hasMore);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (missingFavicons) {
|
||||
debounce(getFaviconImgSrc);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function onVisibilityChange() {
|
||||
switch (document.visibilityState) {
|
||||
// page restored without reloading via history navigation (currently only in FF)
|
||||
// the catch here is that DOM may be outdated so we'll at least refresh the just edited style
|
||||
// assuming other changes aren't important enough to justify making a complicated DOM sync
|
||||
case 'visible':
|
||||
if (sessionStorage.justEditedStyleId) {
|
||||
API.getStyle(Number(sessionStorage.justEditedStyleId), true)
|
||||
.then(style => {
|
||||
handleUpdate(style, {method: 'styleUpdated'});
|
||||
});
|
||||
delete sessionStorage.justEditedStyleId;
|
||||
}
|
||||
break;
|
||||
// going away
|
||||
case 'hidden':
|
||||
history.replaceState({scrollY: window.scrollY}, document.title);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function highlightEditedStyle() {
|
||||
if (!sessionStorage.justEditedStyleId) return;
|
||||
const entry = $(ENTRY_ID_PREFIX + sessionStorage.justEditedStyleId);
|
||||
delete sessionStorage.justEditedStyleId;
|
||||
if (entry) {
|
||||
animateElement(entry);
|
||||
requestAnimationFrame(() => scrollElementIntoView(entry));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function embedOptions() {
|
||||
let options = $('#stylus-embedded-options');
|
||||
if (!options) {
|
||||
options = document.createElement('iframe');
|
||||
options.id = 'stylus-embedded-options';
|
||||
options.src = '/options.html';
|
||||
document.documentElement.appendChild(options);
|
||||
}
|
||||
options.focus();
|
||||
}
|
||||
|
||||
function unembedOptions() {
|
||||
const options = $('#stylus-embedded-options');
|
||||
if (options) {
|
||||
options.contentWindow.document.body.classList.add('scaleout');
|
||||
options.classList.add('fadeout');
|
||||
animateElement(options, {
|
||||
className: 'fadeout',
|
||||
onComplete: () => options.remove(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
router.watch({hash: '#stylus-options'}, state => {
|
||||
if (state) {
|
||||
embedOptions();
|
||||
} else {
|
||||
unembedOptions();
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('closeOptions', () => {
|
||||
router.updateHash('');
|
||||
});
|
194
manage/sort.js
194
manage/sort.js
|
@ -1,12 +1,22 @@
|
|||
/* global installed messageBox t $ $create prefs */
|
||||
/* global installed t $ prefs semverCompare */
|
||||
/* exported sorter */
|
||||
'use strict';
|
||||
|
||||
const sorter = (() => {
|
||||
|
||||
// Set up for only one column
|
||||
const defaultSort = 'title,asc';
|
||||
|
||||
const sortOrder = [
|
||||
'asc',
|
||||
'desc',
|
||||
'' // unsorted
|
||||
];
|
||||
|
||||
const sorterType = {
|
||||
alpha: (a, b) => a < b ? -1 : a === b ? 0 : 1,
|
||||
number: (a, b) => (a || 0) - (b || 0),
|
||||
semver: (a, b) => semverCompare(a, b)
|
||||
};
|
||||
|
||||
const tagData = {
|
||||
|
@ -20,113 +30,129 @@ const sorter = (() => {
|
|||
parse: ({style}) => style.usercssData ? 0 : 1,
|
||||
sorter: sorterType.number
|
||||
},
|
||||
enabled: {
|
||||
text: t('genericEnabledLabel'),
|
||||
parse: ({style}) => style.enabled ? 0 : 1,
|
||||
sorter: sorterType.number
|
||||
},
|
||||
disabled: {
|
||||
text: '', // added as either "enabled" or "disabled" by the addOptions function
|
||||
text: t('genericDisabledLabel'),
|
||||
parse: ({style}) => style.enabled ? 1 : 0,
|
||||
sorter: sorterType.number
|
||||
},
|
||||
version: {
|
||||
text: '#',
|
||||
parse: ({style}) => (style.usercssData && style.usercssData.version || ''),
|
||||
sorter: sorterType.semver
|
||||
},
|
||||
dateInstalled: {
|
||||
text: t('dateInstalled'),
|
||||
parse: ({style}) => style.installDate,
|
||||
parse: ({style}) => style.installDate || '',
|
||||
sorter: sorterType.number
|
||||
},
|
||||
dateUpdated: {
|
||||
text: t('dateUpdated'),
|
||||
parse: ({style}) => style.updateDate,
|
||||
parse: ({style}) => style.updateDate || '',
|
||||
sorter: sorterType.number
|
||||
}
|
||||
};
|
||||
|
||||
// Adding (assumed) most commonly used ('title,asc' should always be first)
|
||||
// whitespace before & after the comma is ignored
|
||||
const selectOptions = [
|
||||
'{groupAsc}',
|
||||
'title,asc',
|
||||
'dateInstalled,desc, title,asc',
|
||||
'dateInstalled,asc, title,asc',
|
||||
'dateUpdated,desc, title,asc',
|
||||
'dateUpdated,asc, title,asc',
|
||||
'usercss,asc, title,asc',
|
||||
'usercss,desc, title,asc',
|
||||
'disabled,asc, title,asc',
|
||||
'disabled,desc, title,asc',
|
||||
'disabled,desc, usercss,asc, title,asc',
|
||||
'{groupDesc}',
|
||||
'title,desc',
|
||||
'usercss,asc, title,desc',
|
||||
'usercss,desc, title,desc',
|
||||
'disabled,desc, title,desc',
|
||||
'disabled,desc, usercss,asc, title,desc'
|
||||
];
|
||||
|
||||
const splitRegex = /\s*,\s*/;
|
||||
const whitespace = /\s+/g;
|
||||
|
||||
let columns = 1;
|
||||
|
||||
function addOptions() {
|
||||
let container;
|
||||
const select = $('#manage.newUI.sort');
|
||||
const renderBin = document.createDocumentFragment();
|
||||
const option = $create('option');
|
||||
const optgroup = $create('optgroup');
|
||||
const meta = {
|
||||
desc: ' \u21E9',
|
||||
enabled: t('genericEnabledLabel'),
|
||||
disabled: t('genericDisabledLabel'),
|
||||
dateNew: ` (${t('sortDateNewestFirst')})`,
|
||||
dateOld: ` (${t('sortDateOldestFirst')})`,
|
||||
groupAsc: t('sortLabelTitleAsc'),
|
||||
groupDesc: t('sortLabelTitleDesc')
|
||||
};
|
||||
const optgroupRegex = /\{\w+\}/;
|
||||
selectOptions.forEach(sort => {
|
||||
if (optgroupRegex.test(sort)) {
|
||||
if (container) {
|
||||
renderBin.appendChild(container);
|
||||
}
|
||||
container = optgroup.cloneNode();
|
||||
container.label = meta[sort.substring(1, sort.length - 1)];
|
||||
return;
|
||||
}
|
||||
let lastTag = '';
|
||||
const opt = option.cloneNode();
|
||||
opt.textContent = sort.split(splitRegex).reduce((acc, val) => {
|
||||
if (tagData[val]) {
|
||||
lastTag = val;
|
||||
return acc + (acc !== '' ? ' + ' : '') + tagData[val].text;
|
||||
}
|
||||
if (lastTag.indexOf('date') > -1) return acc + meta[val === 'desc' ? 'dateNew' : 'dateOld'];
|
||||
if (lastTag === 'disabled') return acc + meta[val === 'desc' ? 'enabled' : 'disabled'];
|
||||
return acc + (meta[val] || '');
|
||||
}, '');
|
||||
opt.value = sort;
|
||||
container.appendChild(opt);
|
||||
});
|
||||
renderBin.appendChild(container);
|
||||
select.appendChild(renderBin);
|
||||
select.value = prefs.get('manage.newUI.sort');
|
||||
}
|
||||
let lastSort;
|
||||
|
||||
function sort({styles}) {
|
||||
const sortBy = prefs.get('manage.newUI.sort').split(splitRegex);
|
||||
let sortBy = prefs.get('manage.newUI.sort').replace(whitespace, '');
|
||||
if (lastSort === sortBy) {
|
||||
return styles;
|
||||
}
|
||||
sortBy = sortBy.split(splitRegex);
|
||||
updateHeaders(sortBy);
|
||||
// Always append an ascending title (default) sort to keep sorts consistent; but don't
|
||||
// show it in the header
|
||||
sortBy = sortBy.concat(defaultSort.split(splitRegex));
|
||||
const len = sortBy.length;
|
||||
|
||||
// Add first column sort to #installed; show sortable column when id column sorted
|
||||
installed.dataset.sort = sortBy[0];
|
||||
|
||||
return styles.sort((a, b) => {
|
||||
let types, direction;
|
||||
let types, direction, x, y;
|
||||
let result = 0;
|
||||
let index = 0;
|
||||
// multi-sort
|
||||
while (result === 0 && index < len) {
|
||||
types = tagData[sortBy[index++]];
|
||||
direction = sortBy[index++] === 'asc' ? 1 : -1;
|
||||
result = types.sorter(types.parse(a), types.parse(b)) * direction;
|
||||
x = types.parse(a);
|
||||
// sort empty values to the bottom
|
||||
if (x === '') {
|
||||
return 1;
|
||||
}
|
||||
y = types.parse(b);
|
||||
if (y === '') {
|
||||
return -1;
|
||||
}
|
||||
result = types.sorter(x, y) * direction;
|
||||
}
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
// Update default sort on init & when all other columns are unsorted
|
||||
function updateHeaders(sortBy) {
|
||||
let header, sortDir;
|
||||
let i = 0;
|
||||
const len = sortBy.length;
|
||||
while (i < len) {
|
||||
header = $(`.entry-header [data-type="${sortBy[i++]}"]`);
|
||||
sortDir = sortBy[i++];
|
||||
if (header) {
|
||||
header.dataset.sortDir = sortDir;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateSort(event) {
|
||||
const sortables = $$('.entry-header .sortable');
|
||||
const elm = event.target;
|
||||
// default sort column only allows asc, desc; not unsorted
|
||||
const len = sortOrder.length - (elm.dataset.type === defaultSort.split(splitRegex)[0] ? 1 : 0);
|
||||
let index = (sortOrder.indexOf(elm.dataset.sortDir) + 1) % len;
|
||||
// shift key for multi-column sorting
|
||||
if (!event.shiftKey) {
|
||||
sortables.forEach(el => {
|
||||
el.dataset.sortDir = '';
|
||||
el.dataset.timestamp = '';
|
||||
});
|
||||
}
|
||||
elm.dataset.sortDir = sortOrder[index];
|
||||
elm.dataset.timestamp = Date.now();
|
||||
|
||||
const newSort = sortables
|
||||
.filter(el => el.dataset.sortDir !== '')
|
||||
.reduce((acc, el) => {
|
||||
const {sortDir, type, timestamp = new Date()} = el.dataset;
|
||||
if (sortDir) {
|
||||
acc.push({sortDir, type, timestamp: parseFloat(timestamp)});
|
||||
}
|
||||
return acc;
|
||||
}, [])
|
||||
.sort((a, b) => a.timestamp - b.timestamp)
|
||||
.reduce((acc, item) => {
|
||||
acc = acc.concat(item.type, item.sortDir);
|
||||
return acc;
|
||||
}, [])
|
||||
.join(',');
|
||||
prefs.set('manage.newUI.sort', newSort || defaultSort);
|
||||
}
|
||||
|
||||
function update() {
|
||||
if (!installed) return;
|
||||
const current = [...installed.children];
|
||||
current.shift(); // remove header
|
||||
const sorted = sort({
|
||||
styles: current.map(entry => ({
|
||||
entry,
|
||||
|
@ -148,7 +174,7 @@ const sorter = (() => {
|
|||
let isOdd = false;
|
||||
const flipRows = columns % 2 === 0;
|
||||
for (const {classList} of installed.children) {
|
||||
if (classList.contains('hidden')) continue;
|
||||
if (classList.contains('hidden') || classList.contains('entry-header')) continue;
|
||||
classList.toggle('odd', isOdd);
|
||||
classList.toggle('even', !isOdd);
|
||||
if (flipRows && ++index >= columns) {
|
||||
|
@ -163,7 +189,8 @@ const sorter = (() => {
|
|||
let newValue = 1;
|
||||
for (let el = document.documentElement.lastElementChild;
|
||||
el.localName === 'style';
|
||||
el = el.previousElementSibling) {
|
||||
el = el.previousElementSibling
|
||||
) {
|
||||
if (el.textContent.includes('--columns:')) {
|
||||
newValue = Math.max(1, getComputedStyle(document.documentElement).getPropertyValue('--columns') | 0);
|
||||
break;
|
||||
|
@ -175,25 +202,10 @@ const sorter = (() => {
|
|||
}
|
||||
}
|
||||
|
||||
function showHelp(event) {
|
||||
event.preventDefault();
|
||||
messageBox({
|
||||
className: 'help-text',
|
||||
title: t('sortStylesHelpTitle'),
|
||||
contents:
|
||||
$create('div',
|
||||
t('sortStylesHelp').split('\n').map(line =>
|
||||
$create('p', line))),
|
||||
buttons: [t('confirmOK')],
|
||||
});
|
||||
}
|
||||
|
||||
function init() {
|
||||
prefs.subscribe(['manage.newUI.sort'], update);
|
||||
$('#sorter-help').onclick = showHelp;
|
||||
addOptions();
|
||||
updateColumnCount();
|
||||
}
|
||||
|
||||
return {init, update, sort, updateStripes};
|
||||
return {init, update, sort, updateSort, updateStripes};
|
||||
})();
|
||||
|
|
187
manage/tooltips.css
Normal file
187
manage/tooltips.css
Normal file
|
@ -0,0 +1,187 @@
|
|||
:root {
|
||||
--tooltip-bkgd: #555;
|
||||
--tooltip-border: #777;
|
||||
--tooltip-text: #fff;
|
||||
--tooltip-error: #d40000;
|
||||
--tooltip-warn: goldenrod;
|
||||
}
|
||||
|
||||
[data-title] {
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
[data-title]:after {
|
||||
-webkit-font-smoothing: subpixel-antialiased;
|
||||
background: var(--tooltip-bkgd);
|
||||
border: 1px solid var(--tooltip-border);
|
||||
border-radius: 3px;
|
||||
color: var(--tooltip-text);
|
||||
content: attr(data-title);
|
||||
font-size: 12px;
|
||||
line-height: 1.5em;
|
||||
letter-spacing: normal;
|
||||
padding: .5em .75em;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
text-shadow: none;
|
||||
text-transform: none;
|
||||
white-space: pre;
|
||||
word-break: break-all;
|
||||
overflow-wrap: break-word;
|
||||
max-width: 50vw;
|
||||
z-index: 1000000;
|
||||
}
|
||||
|
||||
.targets [data-title]:after {
|
||||
white-space: pre-wrap;
|
||||
min-width: 240px;
|
||||
}
|
||||
|
||||
.entry-last-update[data-title]:after {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
[data-title]:after,
|
||||
[data-title]:before {
|
||||
display: none;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
[data-title]:before {
|
||||
border: 6px solid transparent;
|
||||
color: var(--tooltip-bkgd);
|
||||
content: "";
|
||||
height: 0;
|
||||
width: 0;
|
||||
z-index: 1000001;
|
||||
}
|
||||
|
||||
[data-title]:hover:after,
|
||||
[data-title]:hover:before {
|
||||
display: inline-block;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
[data-title].tt-s:after,
|
||||
[data-title].tt-se:after,
|
||||
[data-title].tt-sw:after {
|
||||
margin-top: 6px;
|
||||
right: 50%;
|
||||
top: 100%
|
||||
}
|
||||
|
||||
[data-title].tt-s:before,
|
||||
[data-title].tt-se:before,
|
||||
[data-title].tt-sw:before {
|
||||
border-bottom-color: var(--tooltip-bkgd);
|
||||
bottom: -7px;
|
||||
margin-right: -6px;
|
||||
right: 50%;
|
||||
top: auto
|
||||
}
|
||||
|
||||
[data-title].tt-se:after {
|
||||
left: 50%;
|
||||
margin-left: -16px;
|
||||
right: auto
|
||||
}
|
||||
|
||||
[data-title].tt-sw:after {
|
||||
margin-right: -16px
|
||||
}
|
||||
|
||||
[data-title].tt-n:after,
|
||||
[data-title].tt-ne:after,
|
||||
[data-title].tt-nw:after {
|
||||
bottom: 100%;
|
||||
margin-bottom: 6px;
|
||||
right: 50%
|
||||
}
|
||||
|
||||
[data-title].tt-n:before,
|
||||
[data-title].tt-ne:before,
|
||||
[data-title].tt-nw:before {
|
||||
border-top-color: var(--tooltip-bkgd);
|
||||
bottom: auto;
|
||||
margin-right: -6px;
|
||||
right: 50%;
|
||||
top: -7px
|
||||
}
|
||||
|
||||
[data-title].tt-ne:after {
|
||||
left: 50%;
|
||||
margin-left: -16px;
|
||||
right: auto
|
||||
}
|
||||
|
||||
[data-title].tt-nw:after {
|
||||
margin-right: -16px
|
||||
}
|
||||
|
||||
[data-title].tt-n:after,
|
||||
[data-title].tt-s:after {
|
||||
transform: translateX(50%)
|
||||
}
|
||||
|
||||
[data-title].tt-w:after {
|
||||
bottom: 50%;
|
||||
margin-right: 6px;
|
||||
right: 100%;
|
||||
transform: translateY(50%)
|
||||
}
|
||||
|
||||
[data-title].tt-w:before {
|
||||
border-left-color: var(--tooltip-bkgd);
|
||||
bottom: 50%;
|
||||
left: -7px;
|
||||
margin-top: -6px;
|
||||
top: 50%
|
||||
}
|
||||
|
||||
[data-title].tt-e:after {
|
||||
bottom: 50%;
|
||||
left: 100%;
|
||||
margin-left: 6px;
|
||||
transform: translateY(50%)
|
||||
}
|
||||
|
||||
[data-title].tt-e:before {
|
||||
border-right-color: var(--tooltip-bkgd);
|
||||
bottom: 50%;
|
||||
margin-top: -6px;
|
||||
right: -7px;
|
||||
top: 50%
|
||||
}
|
||||
|
||||
.can-update .updater-icons .update:before {
|
||||
border-right-color: var(--tooltip-warn);
|
||||
}
|
||||
|
||||
.entry-actions .entry-delete:before,
|
||||
.update-problem .check-update:before {
|
||||
border-right-color: var(--tooltip-error);
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
/* Action icons flip to left side; switch tooltip direction */
|
||||
#main-actions [data-title].tt-w:after {
|
||||
bottom: 50%;
|
||||
left: 100%;
|
||||
right: auto;
|
||||
margin-left: 6px;
|
||||
margin-right: 0;
|
||||
transform: translateY(50%)
|
||||
}
|
||||
|
||||
#main-actions [data-title].tt-w:before {
|
||||
border-left-color: transparent;
|
||||
border-right-color: var(--tooltip-bkgd);
|
||||
bottom: 50%;
|
||||
margin-top: -6px;
|
||||
left: auto;
|
||||
right: -7px;
|
||||
top: 50%
|
||||
}
|
||||
}
|
|
@ -1,11 +1,12 @@
|
|||
/* global messageBox ENTRY_ID_PREFIX newUI filtersSelector filterAndAppend
|
||||
/* global messageBox UI handleEvent filtersSelector filterAndAppend
|
||||
sorter $ $$ $create API onDOMready scrollElementIntoView t chromeLocal */
|
||||
/* exported handleUpdateInstalled */
|
||||
/* exported handleUpdateInstalled resetUpdates */
|
||||
'use strict';
|
||||
|
||||
let updateTimer;
|
||||
|
||||
onDOMready().then(() => {
|
||||
$('#check-all-updates').onclick = checkUpdateAll;
|
||||
$('#check-all-updates-force').onclick = checkUpdateAll;
|
||||
$('#check-all-updates-force').onclick = checkUpdateBulk;
|
||||
$('#apply-all-updates').onclick = applyUpdateAll;
|
||||
$('#update-history').onclick = showUpdateHistory;
|
||||
});
|
||||
|
@ -26,18 +27,28 @@ function applyUpdateAll() {
|
|||
});
|
||||
}
|
||||
|
||||
function resetUpdates() {
|
||||
$('#check-all-updates-force').classList.add('hidden');
|
||||
$('#apply-all-updates').classList.add('hidden');
|
||||
$('#update-history').classList.add('hidden');
|
||||
const checkbox = $('#only-updates');
|
||||
checkbox.checked = false;
|
||||
checkbox.parentElement.classList.add('hidden');
|
||||
}
|
||||
|
||||
function checkUpdateAll() {
|
||||
|
||||
function checkUpdateBulk() {
|
||||
clearTimeout(updateTimer);
|
||||
document.body.classList.add('update-in-progress');
|
||||
const btnCheck = $('#check-all-updates');
|
||||
// const btnCheck = $('#check-all-updates');
|
||||
const btnCheckForce = $('#check-all-updates-force');
|
||||
const btnApply = $('#apply-all-updates');
|
||||
const noUpdates = $('#update-all-no-updates');
|
||||
btnCheck.disabled = true;
|
||||
const styleIds = $$('.entry-filter-toggle:checked').map(el => el.closest('.entry').styleMeta.id);
|
||||
// btnCheck.disabled = true;
|
||||
btnCheckForce.classList.add('hidden');
|
||||
btnApply.classList.add('hidden');
|
||||
noUpdates.classList.add('hidden');
|
||||
|
||||
const ignoreDigest = this && this.id === 'check-all-updates-force';
|
||||
$$('.updatable:not(.can-update)' + (ignoreDigest ? '' : ':not(.update-problem)'))
|
||||
.map(checkUpdate);
|
||||
|
@ -53,10 +64,11 @@ function checkUpdateAll() {
|
|||
chrome.runtime.onConnect.removeListener(onConnect);
|
||||
});
|
||||
|
||||
API.updateCheckAll({
|
||||
API.updateCheckBulk({
|
||||
save: false,
|
||||
observe: true,
|
||||
ignoreDigest,
|
||||
styleIds,
|
||||
});
|
||||
|
||||
function observer(info, port) {
|
||||
|
@ -82,21 +94,26 @@ function checkUpdateAll() {
|
|||
|
||||
port.onMessage.removeListener(observer);
|
||||
document.body.classList.remove('update-in-progress');
|
||||
btnCheck.disabled = total === 0;
|
||||
// btnCheck.disabled = total === 0;
|
||||
btnApply.disabled = false;
|
||||
renderUpdatesOnlyFilter({check: updated + skippedEdited > 0});
|
||||
if (!updated) {
|
||||
noUpdates.dataset.skippedEdited = skippedEdited > 0;
|
||||
if (skippedEdited > 0) {
|
||||
noUpdates.dataset.title = t('updateAllCheckSucceededSomeEdited');
|
||||
}
|
||||
noUpdates.classList.remove('hidden');
|
||||
btnCheckForce.classList.toggle('hidden', skippedEdited === 0);
|
||||
updateTimer = setTimeout(() => {
|
||||
noUpdates.classList.add('hidden');
|
||||
noUpdates.dataset.title = '';
|
||||
}, 1e4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function checkUpdate(entry, {single} = {}) {
|
||||
$('.update-note', entry).textContent = t('checkingForUpdate');
|
||||
$('.check-update', entry).title = '';
|
||||
$('.check-update', entry).dataset.title = t('checkingForUpdate');
|
||||
if (single) {
|
||||
API.updateCheck({
|
||||
save: false,
|
||||
|
@ -111,7 +128,8 @@ function checkUpdate(entry, {single} = {}) {
|
|||
|
||||
function reportUpdateState({updated, style, error, STATES}) {
|
||||
const isCheckAll = document.body.classList.contains('update-in-progress');
|
||||
const entry = $(ENTRY_ID_PREFIX + style.id);
|
||||
const entry = $(UI.ENTRY_ID_PREFIX + style.id);
|
||||
if (!entry) return;
|
||||
const newClasses = new Map([
|
||||
/*
|
||||
When a style is updated/installed, handleUpdateInstalled() clears "updatable"
|
||||
|
@ -130,8 +148,10 @@ function reportUpdateState({updated, style, error, STATES}) {
|
|||
if (updated) {
|
||||
newClasses.set('can-update', true);
|
||||
entry.updatedCode = style;
|
||||
$('.update-note', entry).textContent = '';
|
||||
$('#only-updates').classList.remove('hidden');
|
||||
const onlyUpdates = $('#only-updates');
|
||||
onlyUpdates.parentElement.classList.remove('hidden');
|
||||
onlyUpdates.checked = true;
|
||||
onlyUpdates.change();
|
||||
} else if (!entry.classList.contains('can-update')) {
|
||||
const same = (
|
||||
error === STATES.SAME_MD5 ||
|
||||
|
@ -155,15 +175,14 @@ function reportUpdateState({updated, style, error, STATES}) {
|
|||
const message = same ? t('updateCheckSucceededNoUpdate') : error;
|
||||
newClasses.set('no-update', true);
|
||||
newClasses.set('update-problem', !same);
|
||||
$('.update-note', entry).textContent = message;
|
||||
$('.check-update', entry).title = newUI.enabled ? message : '';
|
||||
$('.update', entry).title = t(edited ? 'updateCheckManualUpdateForce' : 'installUpdate');
|
||||
$('.check-update', entry).dataset.title = UI.enabled ? message : '';
|
||||
$('.update', entry).dataset.title = t(edited ? 'updateCheckManualUpdateForce' : 'installUpdate');
|
||||
// digest may change silently when forcing an update of a locally edited style
|
||||
// so we need to update it in entry's styleMeta in all open manager tabs
|
||||
if (error === STATES.SAME_CODE) {
|
||||
for (const view of chrome.extension.getViews({type: 'tab'})) {
|
||||
if (view.location.pathname === location.pathname) {
|
||||
const entry = view.$(ENTRY_ID_PREFIX + style.id);
|
||||
const entry = view.$(UI.ENTRY_ID_PREFIX + style.id);
|
||||
if (entry) entry.styleMeta.originalDigest = style.originalDigest;
|
||||
}
|
||||
}
|
||||
|
@ -196,16 +215,15 @@ function reportUpdateState({updated, style, error, STATES}) {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
function renderUpdatesOnlyFilter({show, check} = {}) {
|
||||
const numUpdatable = $$('.can-update').length;
|
||||
const mightUpdate = numUpdatable > 0 || $('.update-problem');
|
||||
const checkbox = $('#only-updates input');
|
||||
const checkbox = $('#only-updates');
|
||||
show = show !== undefined ? show : mightUpdate;
|
||||
check = check !== undefined ? show && check : checkbox.checked && mightUpdate;
|
||||
|
||||
$('#only-updates').classList.toggle('hidden', !show);
|
||||
checkbox.checked = check && show;
|
||||
checkbox.checked = check;
|
||||
checkbox.parentElement.classList.toggle('hidden', !show);
|
||||
checkbox.dispatchEvent(new Event('change'));
|
||||
|
||||
const btnApply = $('#apply-all-updates');
|
||||
|
@ -296,7 +314,6 @@ function handleUpdateInstalled(entry, reason) {
|
|||
const note = t(isNew ? 'installButtonInstalled' : 'updateCompleted');
|
||||
entry.classList.add('update-done', ...(isNew ? ['install-done'] : []));
|
||||
entry.classList.remove('can-update', 'updatable');
|
||||
$('.update-note', entry).textContent = note;
|
||||
$('.updated', entry).title = note;
|
||||
$('.updated', entry).dataset.title = note;
|
||||
renderUpdatesOnlyFilter();
|
||||
}
|
||||
|
|
|
@ -112,6 +112,5 @@
|
|||
"id": "{7a7a4a92-a2a0-41d1-9fd7-1e92480d612d}",
|
||||
"strict_min_version": "53.0"
|
||||
}
|
||||
},
|
||||
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2ypG+Z/beZtoYrxxwXYhMwQiAiwRVnSHqdpOSzJdjsXVWdvJjlgEuZcU8kte75w58P45LsRUrwvU6N9x12S6eW84KNEBC8rlZj0RGNoxuhSAcdxneYzjJ9tBkZKOidVedYHHsi3LeaXiLuTNTBR+2lf3uCNcP0ebaFML9uDLdYTGEW4eL3hnEKYPSmT1/xkh4bSGTToCg4YNuWWWoTA0beEOpBWYkPVMarLDQgPzMN5Byu5w3lOS2zL0PPJcmdyk3ez/ZsB4PZKU+h17fVA6+YTvUfxUqLde5i2RiuZhEb6Coo5/W90ZW1yCDC9osjWrxMGYeUMQWHPIgFtDhk4K6QIDAQAB"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -145,8 +145,10 @@ function messageBox({
|
|||
}
|
||||
|
||||
function removeSelf() {
|
||||
if (messageBox.element) {
|
||||
messageBox.element.remove();
|
||||
messageBox.element = null;
|
||||
}
|
||||
messageBox.resolve = null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -187,7 +187,7 @@ function checkUpdates() {
|
|||
chrome.runtime.onConnect.removeListener(onConnect);
|
||||
});
|
||||
|
||||
API.updateCheckAll({observe: true});
|
||||
API.updateCheckBulk({observe: true});
|
||||
|
||||
function observer(info) {
|
||||
if ('count' in info) {
|
||||
|
|
177
sync/import-export-dropbox.js
Normal file
177
sync/import-export-dropbox.js
Normal file
|
@ -0,0 +1,177 @@
|
|||
/* global messageBox Dropbox createZipFileFromText readZipFileFromBlob
|
||||
launchWebAuthFlow getRedirectUrlAuthFlow importFromString resolve
|
||||
$ $create t chromeLocal API getOwnTab */
|
||||
/* exported exportDropbox */
|
||||
'use strict';
|
||||
|
||||
const DROPBOX_API_KEY = 'zg52vphuapvpng9';
|
||||
const FILENAME_ZIP_FILE = 'stylus.json';
|
||||
const DROPBOX_FILE = 'stylus.zip';
|
||||
const API_ERROR_STATUS_FILE_NOT_FOUND = 409;
|
||||
const HTTP_STATUS_CANCEL = 499;
|
||||
|
||||
function messageProgressBar(data) {
|
||||
return messageBox({
|
||||
title: `${data.title}`,
|
||||
className: 'config-dialog',
|
||||
contents: [
|
||||
$create('p', data.text)
|
||||
],
|
||||
buttons: [{
|
||||
textContent: t('confirmClose'),
|
||||
dataset: {cmd: 'close'},
|
||||
}],
|
||||
}).then(() => {
|
||||
document.body.style.minWidth = '';
|
||||
document.body.style.minHeight = '';
|
||||
});
|
||||
}
|
||||
|
||||
function hasDropboxAccessToken() {
|
||||
return chromeLocal.getValue('dropbox_access_token');
|
||||
}
|
||||
|
||||
function requestDropboxAccessToken() {
|
||||
const client = new Dropbox.Dropbox({
|
||||
clientId: DROPBOX_API_KEY,
|
||||
fetch
|
||||
});
|
||||
const authUrl = client.getAuthenticationUrl(getRedirectUrlAuthFlow());
|
||||
return launchWebAuthFlow({url: authUrl, interactive: true})
|
||||
.then(urlReturned => {
|
||||
const params = new URLSearchParams(new URL(urlReturned).hash.replace('#', ''));
|
||||
chromeLocal.setValue('dropbox_access_token', params.get('access_token'));
|
||||
return params.get('access_token');
|
||||
});
|
||||
}
|
||||
|
||||
function uploadFileDropbox(client, stylesText) {
|
||||
return client.filesUpload({path: '/' + DROPBOX_FILE, contents: stylesText});
|
||||
}
|
||||
|
||||
function exportDropbox(styles) {
|
||||
const mode = localStorage.installType;
|
||||
const title = t('syncDropboxStyles');
|
||||
const text = mode === 'normal' ? t('connectingDropbox') : t('connectingDropboxNotAllowed');
|
||||
messageProgressBar({title, text});
|
||||
if (mode !== 'normal') return;
|
||||
|
||||
hasDropboxAccessToken()
|
||||
.then(token => token || requestDropboxAccessToken())
|
||||
.then(token => {
|
||||
const client = new Dropbox.Dropbox({
|
||||
clientId: DROPBOX_API_KEY,
|
||||
accessToken: token,
|
||||
fetch
|
||||
});
|
||||
return client.filesDownload({path: '/' + DROPBOX_FILE})
|
||||
.then(() => messageBox.confirm(t('overwriteFileExport')))
|
||||
.then(ok => {
|
||||
// deletes file if user want to
|
||||
if (!ok) {
|
||||
return Promise.reject({status: HTTP_STATUS_CANCEL});
|
||||
}
|
||||
return client.filesDelete({path: '/' + DROPBOX_FILE});
|
||||
})
|
||||
// file deleted with success, process styles and create a file
|
||||
.then(() => {
|
||||
messageProgressBar({title: title, text: t('gettingStyles')});
|
||||
return JSON.stringify(styles, null, '\t');
|
||||
})
|
||||
// create zip file
|
||||
.then(stylesText => {
|
||||
messageProgressBar({title: title, text: t('zipStyles')});
|
||||
return createZipFileFromText(FILENAME_ZIP_FILE, stylesText);
|
||||
})
|
||||
// create file dropbox
|
||||
.then(zipedText => {
|
||||
messageProgressBar({title: title, text: t('uploadingFile')});
|
||||
return uploadFileDropbox(client, zipedText);
|
||||
})
|
||||
// gives feedback to user
|
||||
.then(() => messageProgressBar({title: title, text: t('exportSavedSuccess')}))
|
||||
// handle not found cases and cancel action
|
||||
.catch(error => {
|
||||
console.log(error);
|
||||
// saving file first time
|
||||
if (error.status === API_ERROR_STATUS_FILE_NOT_FOUND) {
|
||||
API.getAllStyles()
|
||||
.then(styles => {
|
||||
messageProgressBar({title: title, text: t('gettingStyles')});
|
||||
return JSON.stringify(styles, null, '\t');
|
||||
})
|
||||
.then(stylesText => {
|
||||
messageProgressBar({title: title, text: t('zipStyles')});
|
||||
return createZipFileFromText(FILENAME_ZIP_FILE, stylesText);
|
||||
})
|
||||
.then(zipedText => {
|
||||
messageProgressBar({title: title, text: t('uploadingFile')});
|
||||
return uploadFileDropbox(client, zipedText);
|
||||
})
|
||||
.then(() => messageProgressBar({title: title, text: t('exportSavedSuccess')}))
|
||||
.catch(err => messageBox.alert(err));
|
||||
return;
|
||||
}
|
||||
|
||||
// user cancelled the flow
|
||||
if (error.status === HTTP_STATUS_CANCEL) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.error(error);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
$('#sync-dropbox-import').onclick = () => {
|
||||
const mode = localStorage.installType;
|
||||
const title = t('syncDropboxStyles');
|
||||
const text = mode === 'normal' ? t('connectingDropbox') : t('connectingDropboxNotAllowed');
|
||||
messageProgressBar({title, text});
|
||||
if (mode !== 'normal') return;
|
||||
|
||||
hasDropboxAccessToken()
|
||||
.then(token => token || requestDropboxAccessToken())
|
||||
.then(token => {
|
||||
const client = new Dropbox.Dropbox({
|
||||
clientId: DROPBOX_API_KEY,
|
||||
accessToken: token,
|
||||
fetch
|
||||
});
|
||||
return client.filesDownload({path: '/' + DROPBOX_FILE})
|
||||
.then(response => {
|
||||
messageProgressBar({title: title, text: t('unzipStyles')});
|
||||
return readZipFileFromBlob(response.fileBlob);
|
||||
})
|
||||
.then(zipedFileBlob => {
|
||||
messageProgressBar({title: title, text: t('readingStyles')});
|
||||
document.body.style.cursor = 'wait';
|
||||
const fReader = new FileReader();
|
||||
fReader.onloadend = event => {
|
||||
const text = event.target.result;
|
||||
const maybeUsercss = !/^[\s\r\n]*\[/.test(text) &&
|
||||
(text.includes('==UserStyle==') || /==UserStyle==/i.test(text));
|
||||
(!maybeUsercss ?
|
||||
importFromString(text) :
|
||||
getOwnTab().then(tab => {
|
||||
tab.url = URL.createObjectURL(new Blob([text], {type: 'text/css'}));
|
||||
return API.openUsercssInstallPage({direct: true, tab})
|
||||
.then(() => URL.revokeObjectURL(tab.url));
|
||||
})
|
||||
).then(numStyles => {
|
||||
document.body.style.cursor = '';
|
||||
resolve(numStyles);
|
||||
});
|
||||
};
|
||||
fReader.readAsText(zipedFileBlob, 'utf-8');
|
||||
})
|
||||
.catch(error => {
|
||||
// no file
|
||||
if (error.status === API_ERROR_STATUS_FILE_NOT_FOUND) {
|
||||
messageBox.alert(t('noFileToImport'));
|
||||
return;
|
||||
}
|
||||
messageBox.alert(error);
|
||||
});
|
||||
});
|
||||
};
|
Loading…
Reference in New Issue
Block a user