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",
|
"message": "Write new style",
|
||||||
"description": "Label for the button to go to the add style page"
|
"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": {
|
"addStyleTitle": {
|
||||||
"message": "Add Style",
|
"message": "Add Style",
|
||||||
"description": "Title of the page for adding styles"
|
"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"
|
"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": {
|
"appliesDomainOption": {
|
||||||
"message": "URLs on the domain",
|
"message": "URLs on the domain",
|
||||||
"description": "Option to make the style apply to the entered string as a domain"
|
"description": "Option to make the style apply to the entered string as a domain"
|
||||||
|
@ -88,12 +92,17 @@
|
||||||
"message": "Backup",
|
"message": "Backup",
|
||||||
"description": "Heading for backup"
|
"description": "Heading for backup"
|
||||||
},
|
},
|
||||||
|
"backupImport": {
|
||||||
|
"message": "Import backup",
|
||||||
|
"description": "Tooltip for header import/restore backup icon"
|
||||||
|
},
|
||||||
"backupMessage": {
|
"backupMessage": {
|
||||||
"message": "Select a file or drag and drop to this page.",
|
"message": "Select a file or drag and drop to this page.",
|
||||||
"description": "Message for backup"
|
"description": "Message for backup"
|
||||||
},
|
},
|
||||||
"bckpInstStyles": {
|
"bckpInstStyles": {
|
||||||
"message": "Export styles"
|
"message": "Local device",
|
||||||
|
"description": "Selected option to backup indicated styles to local device/drive"
|
||||||
},
|
},
|
||||||
"checkAllUpdates": {
|
"checkAllUpdates": {
|
||||||
"message": "Check all styles for updates",
|
"message": "Check all styles for updates",
|
||||||
|
@ -335,7 +344,11 @@
|
||||||
},
|
},
|
||||||
"exportLabel": {
|
"exportLabel": {
|
||||||
"message": "Export",
|
"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": {
|
"externalFeedback": {
|
||||||
"message": "Feedback",
|
"message": "Feedback",
|
||||||
|
@ -405,6 +418,10 @@
|
||||||
"message": "Enabled",
|
"message": "Enabled",
|
||||||
"description": "Used in various lists/options to indicate that something is 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": {
|
"genericError": {
|
||||||
"message": "Error",
|
"message": "Error",
|
||||||
"description": "Used in various places to indicate some error occurred."
|
"description": "Used in various places to indicate some error occurred."
|
||||||
|
@ -413,6 +430,10 @@
|
||||||
"message": "History",
|
"message": "History",
|
||||||
"description": "Used in various places to show a history log of something"
|
"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": {
|
"genericNext": {
|
||||||
"message": "Next",
|
"message": "Next",
|
||||||
"description": "Used in various places to select/perform the next step/action"
|
"description": "Used in various places to select/perform the next step/action"
|
||||||
|
@ -554,6 +575,10 @@
|
||||||
"message": "Get help",
|
"message": "Get help",
|
||||||
"description": "Homepage link text on the manage page e.g. https://add0n.com/stylus.html#features with chat/FAQ/intro/info"
|
"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": {
|
"linkGetStyles": {
|
||||||
"message": "Get styles",
|
"message": "Get styles",
|
||||||
"description": "Help link text on the manage page e.g. https://userstyles.org"
|
"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."
|
"description": "Label before the replace-with input field in the editor shown on Ctrl-H etc."
|
||||||
},
|
},
|
||||||
"retrieveBckp": {
|
"retrieveBckp": {
|
||||||
"message": "Import styles"
|
"message": "Local source",
|
||||||
|
"description": "Selected option to get a backup of styles from a local device/drive"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"message": "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\">",
|
"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"
|
"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": {
|
"sectionAdd": {
|
||||||
"message": "Add another section",
|
"message": "Add another section",
|
||||||
"description": "Label for the button to add a section"
|
"description": "Label for the button to add a section"
|
||||||
|
@ -1237,33 +1283,34 @@
|
||||||
"shortcutsNote": {
|
"shortcutsNote": {
|
||||||
"message": "Define keyboard shortcuts"
|
"message": "Define keyboard shortcuts"
|
||||||
},
|
},
|
||||||
"sortDateNewestFirst": {
|
"sortColumnEnabled": {
|
||||||
"message": "newest first",
|
"message": "enabled styles",
|
||||||
"description": "Text added to indicate that sorting a date would add the newest entries at the top"
|
"description": "Column name seen in the tooltip when hovering over the header; used to replace the 'sortLabel` placeholder"
|
||||||
},
|
},
|
||||||
"sortDateOldestFirst": {
|
"sortColumnName": {
|
||||||
"message": "oldest first",
|
"message": "style name",
|
||||||
"description": "Text added to indicate that sorting a date would add the oldest entries at the top"
|
"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": {
|
"sortLabel": {
|
||||||
"message": "Select a sort to apply to the installed styles",
|
"message": "Click to sort the \"$name$\" column;\nUse shift + click to sort multiple columns",
|
||||||
"description": "Title on the sort select to indicate it is used for sorting entries"
|
"placeholders": {
|
||||||
},
|
"name": {
|
||||||
"sortLabelTitleAsc": {
|
"content": "$1"
|
||||||
"message": "Title Ascending",
|
}
|
||||||
"description": "Text added to option group to indicate a block of options that apply a title ascending (A to Z) sort"
|
},
|
||||||
},
|
"description": "Title added to links in the manager page header to inform the user on how to sort the columns"
|
||||||
"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"
|
|
||||||
},
|
},
|
||||||
"styleBadRegexp": {
|
"styleBadRegexp": {
|
||||||
"message": "Regexp is invalid.",
|
"message": "Regexp is invalid.",
|
||||||
|
|
|
@ -4,13 +4,108 @@
|
||||||
(() => {
|
(() => {
|
||||||
// toLocaleLowerCase cache, autocleared after 1 minute
|
// toLocaleLowerCase cache, autocleared after 1 minute
|
||||||
const cache = new Map();
|
const cache = new Map();
|
||||||
// top-level style properties to be searched
|
|
||||||
const PARTS = {
|
// Creates an array of intermediate words (2 letter minimum)
|
||||||
name: searchText,
|
// 'usercss' => ["us", "use", "user", "userc", "usercs", "usercss"]
|
||||||
url: searchText,
|
// this makes it so the user can type partial queries and not have the search
|
||||||
sourceCode: searchText,
|
// constantly switching between using & ignoring the filter
|
||||||
sections: searchSections,
|
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
|
* @param params
|
||||||
|
@ -19,76 +114,93 @@
|
||||||
* @returns {number[]} - array of matched styles ids
|
* @returns {number[]} - array of matched styles ids
|
||||||
*/
|
*/
|
||||||
API_METHODS.searchDB = ({query, ids}) => {
|
API_METHODS.searchDB = ({query, ids}) => {
|
||||||
let rx, words, icase, matchUrl;
|
const parts = query.trim().split(/(".*?")|\s+/).filter(Boolean);
|
||||||
query = query.trim();
|
|
||||||
|
|
||||||
if (/^url:/i.test(query)) {
|
const searchFilters = {
|
||||||
matchUrl = query.slice(query.indexOf(':') + 1).trim();
|
words: [],
|
||||||
if (matchUrl) {
|
regex: null, // only last regex expression is used
|
||||||
return styleManager.getStylesByUrl(matchUrl)
|
results: [],
|
||||||
.then(results => results.map(r => r.data.id));
|
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 (!searchFilters.within.length) {
|
||||||
|
searchFilters.within.push(...searchWithin.slice(-1));
|
||||||
}
|
}
|
||||||
if (query.startsWith('/') && /^\/(.+?)\/([gimsuy]*)$/.test(query)) {
|
|
||||||
rx = tryRegExp(RegExp.$1, RegExp.$2);
|
// console.log('matchers', searchFilters);
|
||||||
}
|
// url matches
|
||||||
if (!rx) {
|
if (searchFilters.results.length) {
|
||||||
words = query
|
return searchFilters.results;
|
||||||
.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));
|
|
||||||
}
|
}
|
||||||
|
searchFilters.icase = searchFilters.words.some(w => w === lower(w));
|
||||||
|
query = parts.join(' ').trim();
|
||||||
|
|
||||||
return styleManager.getAllStyles().then(styles => {
|
return styleManager.getAllStyles().then(styles => {
|
||||||
if (ids) {
|
if (ids) {
|
||||||
const idSet = new Set(ids);
|
const idSet = new Set(ids);
|
||||||
styles = styles.filter(s => idSet.has(s.id));
|
styles = styles.filter(s => idSet.has(s.id));
|
||||||
}
|
}
|
||||||
|
|
||||||
const results = [];
|
const results = [];
|
||||||
|
const propResults = [];
|
||||||
|
const hasProps = searchFilters.props.length > 0;
|
||||||
|
const noWords = searchFilters.words.length === 0;
|
||||||
for (const style of styles) {
|
for (const style of styles) {
|
||||||
const id = style.id;
|
const id = style.id;
|
||||||
if (!query || words && !words.length) {
|
if (noWords) {
|
||||||
|
// no query or only filters are matching -> show all styles
|
||||||
results.push(id);
|
results.push(id);
|
||||||
continue;
|
} else {
|
||||||
}
|
const text = searchFilters.within.map(within => within.get(style)).join(' ');
|
||||||
for (const part in PARTS) {
|
if (searchText(text, searchFilters)) {
|
||||||
const text = style[part];
|
|
||||||
if (text && PARTS[part](text, rx, words, icase)) {
|
|
||||||
results.push(id);
|
results.push(id);
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
if (hasProps && searchProps(style, searchFilters) && results.includes(id)) {
|
||||||
if (cache.size) debounce(clearCache, 60e3);
|
propResults.push(id);
|
||||||
return results;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
// results AND propResults
|
||||||
}
|
const finalResults = hasProps
|
||||||
|
? propResults.filter(id => results.includes(id))
|
||||||
|
: results;
|
||||||
|
if (cache.size) debounce(clearCache, 60e3);
|
||||||
|
// console.log('final', finalResults)
|
||||||
|
return finalResults;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
function lower(text) {
|
function lower(text) {
|
||||||
let result = cache.get(text);
|
let result = cache.get(text);
|
||||||
|
|
|
@ -31,7 +31,7 @@
|
||||||
|
|
||||||
const retrying = new Set();
|
const retrying = new Set();
|
||||||
|
|
||||||
API_METHODS.updateCheckAll = checkAllStyles;
|
API_METHODS.updateCheckBulk = checkBulkStyles;
|
||||||
API_METHODS.updateCheck = checkStyle;
|
API_METHODS.updateCheck = checkStyle;
|
||||||
API_METHODS.getUpdaterStates = () => STATES;
|
API_METHODS.getUpdaterStates = () => STATES;
|
||||||
|
|
||||||
|
@ -39,18 +39,22 @@
|
||||||
schedule();
|
schedule();
|
||||||
chrome.alarms.onAlarm.addListener(onAlarm);
|
chrome.alarms.onAlarm.addListener(onAlarm);
|
||||||
|
|
||||||
return {checkAllStyles, checkStyle, STATES};
|
return {checkBulkStyles, checkStyle, STATES};
|
||||||
|
|
||||||
function checkAllStyles({
|
function checkBulkStyles({
|
||||||
save = true,
|
save = true,
|
||||||
ignoreDigest,
|
ignoreDigest,
|
||||||
observe,
|
observe,
|
||||||
|
styleIds = [],
|
||||||
} = {}) {
|
} = {}) {
|
||||||
resetInterval();
|
resetInterval();
|
||||||
checkingAll = true;
|
checkingAll = true;
|
||||||
retrying.clear();
|
retrying.clear();
|
||||||
const port = observe && chrome.runtime.connect({name: 'updater'});
|
const port = observe && chrome.runtime.connect({name: 'updater'});
|
||||||
return styleManager.getAllStyles().then(styles => {
|
return styleManager.getAllStyles().then(styles => {
|
||||||
|
if (styleIds.length) {
|
||||||
|
styles = styles.filter(style => styleIds.includes(style.id));
|
||||||
|
}
|
||||||
styles = styles.filter(style => style.updateUrl);
|
styles = styles.filter(style => style.updateUrl);
|
||||||
if (port) port.postMessage({count: styles.length});
|
if (port) port.postMessage({count: styles.length});
|
||||||
log('');
|
log('');
|
||||||
|
@ -247,7 +251,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function onAlarm({name}) {
|
function onAlarm({name}) {
|
||||||
if (name === ALARM_NAME) checkAllStyles();
|
if (name === ALARM_NAME) checkBulkStyles();
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetInterval() {
|
function resetInterval() {
|
||||||
|
|
|
@ -136,6 +136,7 @@ select {
|
||||||
color: #000;
|
color: #000;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
border: 1px solid hsl(0, 0%, 66%);
|
border: 1px solid hsl(0, 0%, 66%);
|
||||||
|
border-radius: 2px;
|
||||||
padding: 0 20px 0 6px;
|
padding: 0 20px 0 6px;
|
||||||
transition: color .5s;
|
transition: color .5s;
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,11 @@ tDocLoader();
|
||||||
|
|
||||||
|
|
||||||
function t(key, params) {
|
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 cache = !params && t.cache[key];
|
||||||
const s = cache || chrome.i18n.getMessage(key, params);
|
const s = cache || chrome.i18n.getMessage(key, params);
|
||||||
if (s === '') {
|
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
|
// in FF pre-57 legacy addons can override useragent so we assume the worst
|
||||||
// until we know for sure in the async getBrowserInfo()
|
// until we know for sure in the async getBrowserInfo()
|
||||||
// (browserAction.openPopup was added in 57)
|
// (browserAction.openPopup was added in 57)
|
||||||
FIREFOX = browser.runtime.getBrowserInfo ? 51 : 50;
|
FIREFOX = browserApi.runtime.getBrowserInfo ? 51 : 50;
|
||||||
// getBrowserInfo was added in FF 51
|
// 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);
|
FIREFOX = parseFloat(info.version);
|
||||||
document.documentElement.classList.add('moz-appearance-bug', FIREFOX && FIREFOX < 54);
|
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.onlyEnabled.invert': false, // display only disabled styles
|
||||||
'manage.onlyLocal.invert': false, // display only externally installed styles
|
'manage.onlyLocal.invert': false, // display only externally installed styles
|
||||||
'manage.onlyUsercss.invert': false, // display only non-usercss (standard) styles
|
'manage.onlyUsercss.invert': false, // display only non-usercss (standard) styles
|
||||||
// UI element state: expanded/collapsed
|
'manage.export.destination': 'local', // default export destination (local or dropbox)
|
||||||
'manage.backup.expanded': true,
|
|
||||||
'manage.filters.expanded': true,
|
'manage.newUI.favicons': true, // show favicons for the sites in applies-to
|
||||||
'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.newUI.faviconsGray': true, // gray out favicons
|
'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',
|
'manage.newUI.sort': 'title,asc',
|
||||||
|
|
||||||
'editor.options': {}, // CodeMirror.defaults.*
|
'editor.options': {}, // CodeMirror.defaults.*
|
||||||
|
|
816
manage.html
816
manage.html
|
@ -1,4 +1,4 @@
|
||||||
<html id="stylus">
|
<html id="stylus" class="newUI">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
|
@ -6,12 +6,11 @@
|
||||||
<title i18n-text="manageTitle"></title>
|
<title i18n-text="manageTitle"></title>
|
||||||
<link rel="stylesheet" href="global.css">
|
<link rel="stylesheet" href="global.css">
|
||||||
<link rel="stylesheet" href="manage/manage.css">
|
<link rel="stylesheet" href="manage/manage.css">
|
||||||
<link rel="stylesheet" href="manage/config-dialog.css">
|
<link rel="stylesheet" data-href="manage/config-dialog.css">
|
||||||
<link rel="stylesheet" href="msgbox/msgbox.css">
|
<link rel="stylesheet" data-href="msgbox/msgbox.css">
|
||||||
<link rel="stylesheet" href="options/onoffswitch.css">
|
<link rel="stylesheet" data-href="options/onoffswitch.css">
|
||||||
<link rel="stylesheet" href="vendor-overwrites/colorpicker/colorpicker.css">
|
<link rel="stylesheet" data-href="vendor-overwrites/colorpicker/colorpicker.css">
|
||||||
|
<link rel="stylesheet" data-href="manage/tooltips.css">
|
||||||
<style id="style-overrides"></style>
|
|
||||||
|
|
||||||
<style id="firefox-transitions-bug-suppressor">
|
<style id="firefox-transitions-bug-suppressor">
|
||||||
/* restrict to FF */
|
/* restrict to FF */
|
||||||
|
@ -30,120 +29,143 @@
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<template data-id="style">
|
<template data-id="style">
|
||||||
<div class="entry">
|
<div class="entry hide-extra">
|
||||||
<h2 class="style-name">
|
<label class="entry-col entry-filter checkmate" tabindex="0">
|
||||||
<a class="style-name-link"></a>
|
<span class="col-label" i18n-text="genericFilterLabel"></span>
|
||||||
<a target="_blank" class="homepage"></a>
|
<input class="entry-filter-toggle" type="checkbox">
|
||||||
</h2>
|
<svg class="svg-icon checkbox"viewBox="0 0 10 10">
|
||||||
<p class="applies-to">
|
<path class="filled-circle" d="M5 .86a4.14 4.14 0 0 0 0 8.28A4.14 4.14 0 1 0 5 .86z"/>
|
||||||
<label i18n-text="appliesDisplay"></label>
|
<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"/>
|
||||||
<span class="targets"></span>
|
<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"/>
|
||||||
</p>
|
</svg>
|
||||||
<p class="actions">
|
</label>
|
||||||
<a class="style-edit-link">
|
<label class="entry-col entry-state checkmate" tabindex="0">
|
||||||
<button i18n-text="editStyleLabel" tabindex="-1"></button>
|
<span class="col-label" i18n-text="genericEnabledLabel"></span>
|
||||||
</a>
|
<input class="entry-state-toggle" type="checkbox">
|
||||||
<button class="enable" i18n-text="enableStyleLabel"></button>
|
<svg class="svg-icon checkbox"viewBox="0 0 10 10">
|
||||||
<button class="disable" i18n-text="disableStyleLabel"></button>
|
<path class="filled-circle" d="M5 .86a4.14 4.14 0 0 0 0 8.28A4.14 4.14 0 1 0 5 .86z"/>
|
||||||
<button class="delete" i18n-text="deleteStyleLabel"></button>
|
<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"/>
|
||||||
<button class="check-update" i18n-text="checkForUpdate"></button>
|
<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"/>
|
||||||
<button class="update" i18n-text="installUpdate"></button>
|
</svg>
|
||||||
<button class="configure-usercss" i18n-text="configureStyle"></button>
|
</label>
|
||||||
<span class="update-note"></span>
|
<a href="#" class="entry-col entry-name">
|
||||||
</p>
|
<span class="col-label" i18n-text="genericName"></span>
|
||||||
</div>
|
<span class="entry-name-text"></span>
|
||||||
</template>
|
<span class="entry-labels"></span>
|
||||||
|
</a>
|
||||||
<template data-id="styleCompact">
|
<div class="entry-col entry-actions">
|
||||||
<div class="entry">
|
<span class="col-label" i18n-text="optionsActions"></span>
|
||||||
<h2 class="style-name">
|
<a href="#" class="entry-configure-usercss tt-e" i18n-data-title="configureStyle">
|
||||||
<div class="checkmate">
|
<svg class="svg-icon entry-config" viewBox="0 0 24 24">
|
||||||
<input class="checker" type="checkbox" i18n-title="toggleStyle">
|
<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
|
||||||
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
|
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
|
||||||
</div>
|
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
|
||||||
<a class="style-name-link"></a>
|
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
|
||||||
</h2>
|
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
|
||||||
<p class="actions">
|
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
|
||||||
<a target="_blank" class="homepage" tabindex="0"></a>
|
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
|
||||||
<a href="#" class="delete" i18n-title="deleteStyleLabel" tabindex="0">
|
5.34 0 0 1 0 1.46l-.14 1.13.89.7 1.08.84-.7
|
||||||
<svg class="svg-icon" viewBox="0 0 20 20">
|
1.21-1.27-.51-1.04-.42-.9.68c-.43.32-.84.56-1.25.73l-1.06.43-.16 1.13-.2
|
||||||
<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
|
1.35h-1.4l-.19-1.35-.16-1.13-1.06-.43a5.67 5.67 0 0
|
||||||
5.5,16.2 10,11.7 14.5,16.2 16.2,14.5 11.7,10 "/>
|
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>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
</p>
|
<a href="#" class="entry-edit tt-e" i18n-data-title="editStyleLabel">
|
||||||
<div class="applies-to">
|
<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>
|
<div class="targets"></div>
|
||||||
<a href="#" class="expander" tabindex="0">...</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</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">
|
<template data-id="updaterIcons">
|
||||||
<span class="updater-icons">
|
<span class="updater-icons entry-col entry-update-state">
|
||||||
<a href="#" class="check-update" i18n-title="checkForUpdate" tabindex="0">
|
<a href="#" class="check-update tt-e" i18n-data-title="checkForUpdate" tabindex="0">
|
||||||
<svg class="svg-icon" viewBox="0 0 20 20">
|
<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
|
<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"/>
|
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"/>
|
||||||
<path d="M10,6c0.8,0,1.6,0.3,2.3,0.7L10,9h3.9h2.1H16V3l-2.3,2.3C12.7,4.5,11.4,4,10,4
|
<path d="M10,6c0.8,0,1.6,0.3,2.3,0.7L10,9h3.9h2.1H16V3l-2.3,2.3C12.7,4.5,11.4,4,10,4
|
||||||
C7,4,4.6,6.2,4.1,9h2.1C6.6,7.3,8.1,6,10,6z"/>
|
C7,4,4.6,6.2,4.1,9h2.1C6.6,7.3,8.1,6,10,6z"/>
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</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">
|
<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 "/>
|
<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"/>
|
<rect shape-rendering="crispEdges" x="4" y="15" width="12" height="2"/>
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</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">
|
<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"/>
|
<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>
|
</svg>
|
||||||
</span>
|
</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">
|
<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"/>
|
<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>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
<span class="update-note"></span>
|
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template data-id="appliesToTarget">
|
<template data-id="appliesToTarget">
|
||||||
<span class="target"></span>
|
<span class="target tt-w">
|
||||||
</template>
|
<img async="true">
|
||||||
|
<svg class="svg-icon" viewBox="0 0 24 24">
|
||||||
<template data-id="appliesToSeparator">
|
<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"/>
|
||||||
<span class="sep">, </span>
|
</svg>
|
||||||
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template data-id="appliesToEverything">
|
<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>
|
||||||
|
|
||||||
<template data-id="extraAppliesTo">
|
<template data-id="extraAppliesTo">
|
||||||
<details class="applies-to-extra">
|
<a href="#" class="applies-to-extra-expander" tabindex="0">...</a>
|
||||||
<summary class="applies-to-extra-expander" i18n-text="appliesDisplayTruncatedSuffix"></summary>
|
|
||||||
</details>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script src="js/polyfill.js"></script>
|
<script src="js/polyfill.js"></script>
|
||||||
|
@ -158,273 +180,423 @@
|
||||||
<script src="js/localization.js"></script>
|
<script src="js/localization.js"></script>
|
||||||
<script src="manage/filters.js"></script>
|
<script src="manage/filters.js"></script>
|
||||||
<script src="manage/sort.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 data-src="vendor-overwrites/colorpicker/colorconverter.js"></script>
|
||||||
<script src="vendor-overwrites/colorpicker/colorpicker.js"></script>
|
<script data-src="vendor-overwrites/colorpicker/colorpicker.js"></script>
|
||||||
<script src="manage/config-dialog.js"></script>
|
<script data-src="manage/config-dialog.js"></script>
|
||||||
<script src="manage/updater-ui.js"></script>
|
<script src="manage/updater-ui.js"></script>
|
||||||
<script src="manage/object-diff.js"></script>
|
<script data-src="manage/object-diff.js"></script>
|
||||||
<script src="manage/import-export.js"></script>
|
<script data-src="manage/import-export.js"></script>
|
||||||
<script src="manage/incremental-search.js"></script>
|
<script data-src="manage/incremental-search.js"></script>
|
||||||
<script src="msgbox/msgbox.js"></script>
|
<script data-src="msgbox/msgbox.js"></script>
|
||||||
<script src="js/sections-util.js"></script>
|
<script src="js/sections-util.js"></script>
|
||||||
<script src="js/storage-util.js"></script>
|
<script src="js/storage-util.js"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body id="stylus-manage" i18n-dragndrop-hint="dragDropMessage">
|
<body id="stylus-manage" i18n-dragndrop-hint="dragDropMessage">
|
||||||
|
|
||||||
<div id="header">
|
<h1 id="main-header">
|
||||||
<h1 id="manage-heading" i18n-text="manageHeading"></h1>
|
<div>
|
||||||
|
<svg id="stylus-logo" width="32" height="32" viewBox="0 0 48 48">
|
||||||
<div id="manage-settings">
|
<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
|
||||||
<div class="settings-column">
|
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
|
||||||
<details id="filters" data-pref="manage.filters.expanded">
|
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"/>
|
||||||
<summary>
|
<path fill-rule="evenodd" clip-rule="evenodd" fill="#28FEFE" d="M46 20.98V5.62A5.72 5.72 0 0 0
|
||||||
<h2 i18n-text="manageFilters">:
|
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
|
||||||
<div class="filter-stats-wrapper">
|
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
|
||||||
<span id="filters-stats"></span>
|
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
|
||||||
<a id="reset-filters" href="#" tabindex="0">
|
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
|
||||||
<svg class="svg-icon" viewBox="0 0 20 20">
|
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
|
||||||
<title i18n-text="genericResetLabel"></title>
|
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
|
||||||
<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
|
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
|
||||||
5.5,16.2 10,11.7 14.5,16.2 16.2,14.5 11.7,10 "/>
|
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
|
||||||
</svg>
|
0-4.92-2.85-8.13-9.02-10.41z"/>
|
||||||
</a>
|
</svg>
|
||||||
</div>
|
<span class="ext-name">Stylus</span>
|
||||||
</h2>
|
<span class="ext-version"></span>
|
||||||
</summary>
|
<span class="filter-stats-wrapper">
|
||||||
|
<span id="filters-stats"></span>
|
||||||
<div class="filter-selection">
|
<a id="reset-filters" class="tt-e" href="#" tabindex="0" i18n-data-title="genericResetLabel">
|
||||||
<label>
|
<svg class="svg-icon" viewBox="0 0 20 20"><use xlink:href="#svg-icon-x"/></svg>
|
||||||
<div class="checkmate">
|
</a>
|
||||||
<input id="manage.onlyEnabled" type="checkbox"
|
</span>
|
||||||
data-filter=".enabled"
|
</div>
|
||||||
data-filter-hide=".disabled">
|
<div id="main-actions">
|
||||||
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
|
<div class="new-style tt-w">
|
||||||
</div>
|
<svg class="svg-icon" viewBox="0 0 24 24">
|
||||||
</label>
|
<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
|
||||||
<div class="select-resizer">
|
16H5V5h14v14zm-8-2h2v-4h4v-2h-4V7h-2v4H7v2h4z"/>
|
||||||
<select id="manage.onlyEnabled.invert">
|
</svg>
|
||||||
<option i18n-text="manageOnlyEnabled" value="false"></option>
|
<div class="dropdown">
|
||||||
<option i18n-text="manageOnlyDisabled" value="true"></option>
|
<a href="edit.html" id="add-usercss" i18n-text="addUserCSSStyleLabel"></a>
|
||||||
</select>
|
<a href="edit.html" id="add-reg-css" i18n-text="addPlainStyleLabel"></a>
|
||||||
<svg class="svg-icon select-arrow"><use xlink:href="#svg-icon-select-arrow"/></svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<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>
|
|
||||||
</select>
|
|
||||||
<svg class="svg-icon select-arrow"><use xlink:href="#svg-icon-select-arrow"/></svg>
|
|
||||||
</div>
|
|
||||||
</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 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">
|
|
||||||
<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>
|
|
||||||
</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"/>
|
|
||||||
</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>
|
|
||||||
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="settings-column">
|
<span class="spacer"></span>
|
||||||
<details id="options" data-pref="manage.options.expanded">
|
|
||||||
|
|
||||||
<summary><h2 id="options-heading" i18n-text="optionsHeading"></h2></summary>
|
<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>
|
||||||
|
|
||||||
<label>
|
<a href="#" id="manage-shortcuts-button" class="chromium-only tt-w" i18n-data-title="shortcutsNote">
|
||||||
<input id="manage.newUI" type="checkbox">
|
<svg class="svg-icon" viewBox="0 0 24 24">
|
||||||
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
|
<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
|
||||||
<span i18n-text="manageNewUI"></span>
|
0 0-2-2zm-9 3h2v2h-2zm0 3h2v2h-2zM8 8h2v2H8zm0 3h2v2H8zm-3 0h2v2H5zm0-3h2v2H5zm3
|
||||||
</label>
|
6h8v2H8zm6-3h2v2h-2zm0-3h2v2h-2zm3 3h2v2h-2zm0-3h2v2h-2z"/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
|
||||||
<div id="newUIoptions">
|
<span class="spacer"></span>
|
||||||
<div>
|
|
||||||
<label for="manage.newUI.favicons" i18n-text="manageFavicons">
|
|
||||||
<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>
|
|
||||||
</a>
|
|
||||||
</label>
|
|
||||||
<div id="faviconsHelp" class="hidden" i18n-text="manageFaviconsHelp">
|
|
||||||
<div>
|
|
||||||
<label for="manage.newUI.faviconsGray" i18n-text="manageFaviconsGray">
|
|
||||||
<input id="manage.newUI.faviconsGray" type="checkbox">
|
|
||||||
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
|
|
||||||
</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">
|
<div class="manage-backups tt-w" i18n-data-title="backupImport">
|
||||||
<button id="manage-options-button" i18n-text="openOptions"></button>
|
<svg class="svg-icon" viewBox="0 0 24 24">
|
||||||
<button id="manage-shortcuts-button" class="chromium-only"
|
<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"/>
|
||||||
i18n-text="shortcuts"
|
</svg>
|
||||||
i18n-title="shortcutsNote"></button>
|
<div class="dropdown">
|
||||||
<a id="find-editor-styles"
|
<a href="#" id="unfile-all-styles" i18n-text="bckpInstStyles"></a>
|
||||||
href="https://userstyles.org/styles/browse/chrome-extension"
|
<a href="#" id="sync-dropbox-import" i18n-text="syncDropboxStyles"></a>
|
||||||
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 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>
|
||||||
|
|
||||||
|
</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>
|
||||||
|
<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>
|
||||||
|
</span>
|
||||||
|
<button id="bulk-actions-apply" i18n-text="bulkActionsApply" class="tt-e" disabled>
|
||||||
|
<span id="update-progress"></span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
</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="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">
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
</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 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 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>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 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 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="manageFavicons"></span>
|
||||||
|
</label>
|
||||||
|
<div id="faviconsHelp" i18n-text="manageFaviconsHelp">
|
||||||
|
<p></p>
|
||||||
|
<label class="checkmate" tabindex="0">
|
||||||
|
<input id="manage.newUI.faviconsGray" 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>
|
||||||
|
<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>
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="installed"></div>
|
|
||||||
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" style="display: none !important;">
|
<svg xmlns="http://www.w3.org/2000/svg" style="display: none !important;">
|
||||||
<symbol id="svg-icon-checked" viewBox="0 0 1000 1000">
|
<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>
|
||||||
|
|
||||||
<symbol id="svg-icon-select-arrow" viewBox="0 0 1792 1792">
|
<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>
|
||||||
|
|
||||||
<symbol id="svg-icon-help" viewBox="0 0 14 16">
|
<symbol id="svg-icon-help" viewBox="0 0 14 16">
|
||||||
<title i18n-text="helpAlt"></title>
|
<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>
|
||||||
|
|
||||||
<symbol id="svg-icon-config" viewBox="0 0 14 14">
|
<symbol id="svg-icon-config" viewBox="0 0 24 24">
|
||||||
<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"/>
|
<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>
|
</symbol>
|
||||||
</svg>
|
</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 */
|
/* exported filterAndAppend */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
@ -37,8 +37,9 @@ HTMLSelectElement.prototype.adjustWidth = function () {
|
||||||
};
|
};
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
$('#search').oninput = e => {
|
$('#search').oninput = event => {
|
||||||
router.updateSearch('search', e.target.value);
|
router.updateSearch('search', event.target.value);
|
||||||
|
UI.updateFilterLabels();
|
||||||
};
|
};
|
||||||
|
|
||||||
$('#search-help').onclick = event => {
|
$('#search-help').onclick = event => {
|
||||||
|
@ -57,48 +58,13 @@ function init() {
|
||||||
} else {
|
} else {
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
})))),
|
}))
|
||||||
|
)
|
||||||
|
),
|
||||||
buttons: [t('confirmOK')],
|
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 => {
|
$$('[data-filter]').forEach(el => {
|
||||||
el.onchange = filterOnChange;
|
el.onchange = filterOnChange;
|
||||||
if (el.closest('.hidden')) {
|
if (el.closest('.hidden')) {
|
||||||
|
@ -108,10 +74,10 @@ function init() {
|
||||||
|
|
||||||
$('#reset-filters').onclick = event => {
|
$('#reset-filters').onclick = event => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (!filtersSelector.hide) {
|
// if (!filtersSelector.hide) {
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
for (const el of $$('#filters [data-filter]')) {
|
for (const el of $$('#tools-wrapper [data-filter]')) {
|
||||||
let value;
|
let value;
|
||||||
if (el.type === 'checkbox' && el.checked) {
|
if (el.type === 'checkbox' && el.checked) {
|
||||||
value = el.checked = false;
|
value = el.checked = false;
|
||||||
|
@ -127,22 +93,16 @@ function init() {
|
||||||
}
|
}
|
||||||
filterOnChange({forceRefilter: true});
|
filterOnChange({forceRefilter: true});
|
||||||
router.updateSearch('search', '');
|
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});
|
filterOnChange({forceRefilter: true});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function filterOnChange({target: el, forceRefilter}) {
|
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) {
|
if (!forceRefilter) {
|
||||||
const value = getValue(el);
|
const value = getValue(el);
|
||||||
if (value === el.lastValue) {
|
if (value === el.lastValue) {
|
||||||
|
@ -150,7 +110,7 @@ function filterOnChange({target: el, forceRefilter}) {
|
||||||
}
|
}
|
||||||
el.lastValue = value;
|
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 =>
|
const buildFilter = hide =>
|
||||||
(hide ? '' : '.entry.hidden') +
|
(hide ? '' : '.entry.hidden') +
|
||||||
[...enabledFilters.map(el =>
|
[...enabledFilters.map(el =>
|
||||||
|
@ -163,6 +123,7 @@ function filterOnChange({target: el, forceRefilter}) {
|
||||||
hide: buildFilter(true),
|
hide: buildFilter(true),
|
||||||
unhide: buildFilter(false),
|
unhide: buildFilter(false),
|
||||||
});
|
});
|
||||||
|
console.log('filter on change', filtersSelector, installed)
|
||||||
if (installed) {
|
if (installed) {
|
||||||
reapplyFilter().then(sorter.updateStripes);
|
reapplyFilter().then(sorter.updateStripes);
|
||||||
}
|
}
|
||||||
|
@ -262,9 +223,9 @@ function reapplyFilter(container = installed, alreadySearched) {
|
||||||
|
|
||||||
function showFiltersStats() {
|
function showFiltersStats() {
|
||||||
const active = filtersSelector.hide !== '';
|
const active = filtersSelector.hide !== '';
|
||||||
$('#filters summary').classList.toggle('active', active);
|
$('.filter-stats-wrapper').classList.toggle('active', active);
|
||||||
$('#reset-filters').disabled = !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 numHidden = installed.getElementsByClassName('entry hidden').length;
|
||||||
const numShown = numTotal - numHidden;
|
const numShown = numTotal - numHidden;
|
||||||
if (filtersSelector.numShown !== numShown ||
|
if (filtersSelector.numShown !== numShown ||
|
||||||
|
@ -291,6 +252,7 @@ function searchStyles({immediately, container} = {}) {
|
||||||
el.lastValue = query;
|
el.lastValue = query;
|
||||||
|
|
||||||
const entries = container && container.children || container || installed.children;
|
const entries = container && container.children || container || installed.children;
|
||||||
|
console.log('search?', query)
|
||||||
return API.searchDB({
|
return API.searchDB({
|
||||||
query,
|
query,
|
||||||
ids: [...entries].map(el => el.styleId),
|
ids: [...entries].map(el => el.styleId),
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/* global messageBox styleSectionsEqual API onDOMready
|
/* global messageBox styleSectionsEqual API onDOMready
|
||||||
tryJSONparse scrollElementIntoView $ $$ API $create t animateElement
|
tryJSONparse scrollElementIntoView $ $$ API $create t animateElement
|
||||||
|
handleEvent
|
||||||
styleJSONseemsValid */
|
styleJSONseemsValid */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
@ -46,8 +47,9 @@ onDOMready().then(() => {
|
||||||
this.ondragend();
|
this.ondragend();
|
||||||
if (event.dataTransfer.files.length) {
|
if (event.dataTransfer.files.length) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if ($('#only-updates input').checked) {
|
const updates = $('#only-updates');
|
||||||
$('#only-updates input').click();
|
if (updates.checked) {
|
||||||
|
handleEvent.checkFilterSelectors(updates);
|
||||||
}
|
}
|
||||||
importFromFile({file: event.dataTransfer.files[0]});
|
importFromFile({file: event.dataTransfer.files[0]});
|
||||||
}
|
}
|
||||||
|
@ -294,38 +296,36 @@ function importFromString(jsonString) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function exportToFile() {
|
function exportToFile(styles) {
|
||||||
API.getAllStyles().then(styles => {
|
// https://crbug.com/714373
|
||||||
// https://crbug.com/714373
|
document.documentElement.appendChild(
|
||||||
document.documentElement.appendChild(
|
$create('iframe', {
|
||||||
$create('iframe', {
|
onload() {
|
||||||
onload() {
|
const text = JSON.stringify(styles, null, '\t');
|
||||||
const text = JSON.stringify(styles, null, '\t');
|
const type = 'application/json';
|
||||||
const type = 'application/json';
|
this.onload = null;
|
||||||
this.onload = null;
|
this.contentDocument.body.appendChild(
|
||||||
this.contentDocument.body.appendChild(
|
$create('a', {
|
||||||
$create('a', {
|
href: URL.createObjectURL(new Blob([text], {type})),
|
||||||
href: URL.createObjectURL(new Blob([text], {type})),
|
download: generateFileName(),
|
||||||
download: generateFileName(),
|
type,
|
||||||
type,
|
})
|
||||||
})
|
).dispatchEvent(new MouseEvent('click'));
|
||||||
).dispatchEvent(new MouseEvent('click'));
|
},
|
||||||
},
|
// we can't use display:none as some browsers are ignoring such iframes
|
||||||
// we can't use display:none as some browsers are ignoring such iframes
|
style: `
|
||||||
style: `
|
all: unset;
|
||||||
all: unset;
|
width: 0;
|
||||||
width: 0;
|
height: 0;
|
||||||
height: 0;
|
position: fixed;
|
||||||
position: fixed;
|
opacity: 0;
|
||||||
opacity: 0;
|
border: none;
|
||||||
border: none;
|
`.replace(/;/g, '!important;'),
|
||||||
`.replace(/;/g, '!important;'),
|
})
|
||||||
})
|
);
|
||||||
);
|
|
||||||
// we don't remove the iframe or the object URL because the browser may show
|
// 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
|
// 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)
|
// (some browsers like Vivaldi can't download if we revoke the URL)
|
||||||
});
|
|
||||||
|
|
||||||
function generateFileName() {
|
function generateFileName() {
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
|
|
|
@ -38,6 +38,7 @@ onDOMready().then(() => {
|
||||||
let textAtPos = 1e6;
|
let textAtPos = 1e6;
|
||||||
let rotated;
|
let rotated;
|
||||||
const entries = [...installed.children];
|
const entries = [...installed.children];
|
||||||
|
entries.shift(); // remove header
|
||||||
const focusedIndex = entries.indexOf(focusedEntry);
|
const focusedIndex = entries.indexOf(focusedEntry);
|
||||||
if (focusedIndex > 0) {
|
if (focusedIndex > 0) {
|
||||||
if (direction > 0) {
|
if (direction > 0) {
|
||||||
|
@ -63,7 +64,7 @@ onDOMready().then(() => {
|
||||||
}
|
}
|
||||||
if (found && found !== focusedEntry) {
|
if (found && found !== focusedEntry) {
|
||||||
focusedEntry = found;
|
focusedEntry = found;
|
||||||
focusedLink = $('.style-name-link', found);
|
focusedLink = $('.entry-name', found);
|
||||||
focusedName = found.styleNameLowerCase;
|
focusedName = found.styleNameLowerCase;
|
||||||
scrollElementIntoView(found, {invalidMarginRatio: .25});
|
scrollElementIntoView(found, {invalidMarginRatio: .25});
|
||||||
animateElement(found, {className: 'highlight-quick'});
|
animateElement(found, {className: 'highlight-quick'});
|
||||||
|
@ -82,6 +83,7 @@ onDOMready().then(() => {
|
||||||
// focus search field on "/" key
|
// focus search field on "/" key
|
||||||
if (key === '/' || !key && k === 191 && !event.shiftKey) {
|
if (key === '/' || !key && k === 191 && !event.shiftKey) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
$('#tools-wrapper').classList.remove('hidden');
|
||||||
$('#search').focus();
|
$('#search').focus();
|
||||||
return;
|
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);
|
||||||
|
}
|
||||||
|
};
|
1575
manage/manage.css
1575
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('');
|
|
||||||
});
|
|
196
manage/sort.js
196
manage/sort.js
|
@ -1,12 +1,22 @@
|
||||||
/* global installed messageBox t $ $create prefs */
|
/* global installed t $ prefs semverCompare */
|
||||||
/* exported sorter */
|
/* exported sorter */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const sorter = (() => {
|
const sorter = (() => {
|
||||||
|
|
||||||
|
// Set up for only one column
|
||||||
|
const defaultSort = 'title,asc';
|
||||||
|
|
||||||
|
const sortOrder = [
|
||||||
|
'asc',
|
||||||
|
'desc',
|
||||||
|
'' // unsorted
|
||||||
|
];
|
||||||
|
|
||||||
const sorterType = {
|
const sorterType = {
|
||||||
alpha: (a, b) => a < b ? -1 : a === b ? 0 : 1,
|
alpha: (a, b) => a < b ? -1 : a === b ? 0 : 1,
|
||||||
number: (a, b) => (a || 0) - (b || 0),
|
number: (a, b) => (a || 0) - (b || 0),
|
||||||
|
semver: (a, b) => semverCompare(a, b)
|
||||||
};
|
};
|
||||||
|
|
||||||
const tagData = {
|
const tagData = {
|
||||||
|
@ -20,113 +30,129 @@ const sorter = (() => {
|
||||||
parse: ({style}) => style.usercssData ? 0 : 1,
|
parse: ({style}) => style.usercssData ? 0 : 1,
|
||||||
sorter: sorterType.number
|
sorter: sorterType.number
|
||||||
},
|
},
|
||||||
|
enabled: {
|
||||||
|
text: t('genericEnabledLabel'),
|
||||||
|
parse: ({style}) => style.enabled ? 0 : 1,
|
||||||
|
sorter: sorterType.number
|
||||||
|
},
|
||||||
disabled: {
|
disabled: {
|
||||||
text: '', // added as either "enabled" or "disabled" by the addOptions function
|
text: t('genericDisabledLabel'),
|
||||||
parse: ({style}) => style.enabled ? 1 : 0,
|
parse: ({style}) => style.enabled ? 1 : 0,
|
||||||
sorter: sorterType.number
|
sorter: sorterType.number
|
||||||
},
|
},
|
||||||
|
version: {
|
||||||
|
text: '#',
|
||||||
|
parse: ({style}) => (style.usercssData && style.usercssData.version || ''),
|
||||||
|
sorter: sorterType.semver
|
||||||
|
},
|
||||||
dateInstalled: {
|
dateInstalled: {
|
||||||
text: t('dateInstalled'),
|
text: t('dateInstalled'),
|
||||||
parse: ({style}) => style.installDate,
|
parse: ({style}) => style.installDate || '',
|
||||||
sorter: sorterType.number
|
sorter: sorterType.number
|
||||||
},
|
},
|
||||||
dateUpdated: {
|
dateUpdated: {
|
||||||
text: t('dateUpdated'),
|
text: t('dateUpdated'),
|
||||||
parse: ({style}) => style.updateDate,
|
parse: ({style}) => style.updateDate || '',
|
||||||
sorter: sorterType.number
|
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 splitRegex = /\s*,\s*/;
|
||||||
|
const whitespace = /\s+/g;
|
||||||
|
|
||||||
let columns = 1;
|
let columns = 1;
|
||||||
|
let lastSort;
|
||||||
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');
|
|
||||||
}
|
|
||||||
|
|
||||||
function sort({styles}) {
|
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;
|
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) => {
|
return styles.sort((a, b) => {
|
||||||
let types, direction;
|
let types, direction, x, y;
|
||||||
let result = 0;
|
let result = 0;
|
||||||
let index = 0;
|
let index = 0;
|
||||||
// multi-sort
|
// multi-sort
|
||||||
while (result === 0 && index < len) {
|
while (result === 0 && index < len) {
|
||||||
types = tagData[sortBy[index++]];
|
types = tagData[sortBy[index++]];
|
||||||
direction = sortBy[index++] === 'asc' ? 1 : -1;
|
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;
|
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() {
|
function update() {
|
||||||
if (!installed) return;
|
if (!installed) return;
|
||||||
const current = [...installed.children];
|
const current = [...installed.children];
|
||||||
|
current.shift(); // remove header
|
||||||
const sorted = sort({
|
const sorted = sort({
|
||||||
styles: current.map(entry => ({
|
styles: current.map(entry => ({
|
||||||
entry,
|
entry,
|
||||||
|
@ -148,7 +174,7 @@ const sorter = (() => {
|
||||||
let isOdd = false;
|
let isOdd = false;
|
||||||
const flipRows = columns % 2 === 0;
|
const flipRows = columns % 2 === 0;
|
||||||
for (const {classList} of installed.children) {
|
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('odd', isOdd);
|
||||||
classList.toggle('even', !isOdd);
|
classList.toggle('even', !isOdd);
|
||||||
if (flipRows && ++index >= columns) {
|
if (flipRows && ++index >= columns) {
|
||||||
|
@ -162,8 +188,9 @@ const sorter = (() => {
|
||||||
function updateColumnCount() {
|
function updateColumnCount() {
|
||||||
let newValue = 1;
|
let newValue = 1;
|
||||||
for (let el = document.documentElement.lastElementChild;
|
for (let el = document.documentElement.lastElementChild;
|
||||||
el.localName === 'style';
|
el.localName === 'style';
|
||||||
el = el.previousElementSibling) {
|
el = el.previousElementSibling
|
||||||
|
) {
|
||||||
if (el.textContent.includes('--columns:')) {
|
if (el.textContent.includes('--columns:')) {
|
||||||
newValue = Math.max(1, getComputedStyle(document.documentElement).getPropertyValue('--columns') | 0);
|
newValue = Math.max(1, getComputedStyle(document.documentElement).getPropertyValue('--columns') | 0);
|
||||||
break;
|
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() {
|
function init() {
|
||||||
prefs.subscribe(['manage.newUI.sort'], update);
|
prefs.subscribe(['manage.newUI.sort'], update);
|
||||||
$('#sorter-help').onclick = showHelp;
|
|
||||||
addOptions();
|
|
||||||
updateColumnCount();
|
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 */
|
sorter $ $$ $create API onDOMready scrollElementIntoView t chromeLocal */
|
||||||
/* exported handleUpdateInstalled */
|
/* exported handleUpdateInstalled resetUpdates */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
let updateTimer;
|
||||||
|
|
||||||
onDOMready().then(() => {
|
onDOMready().then(() => {
|
||||||
$('#check-all-updates').onclick = checkUpdateAll;
|
$('#check-all-updates-force').onclick = checkUpdateBulk;
|
||||||
$('#check-all-updates-force').onclick = checkUpdateAll;
|
|
||||||
$('#apply-all-updates').onclick = applyUpdateAll;
|
$('#apply-all-updates').onclick = applyUpdateAll;
|
||||||
$('#update-history').onclick = showUpdateHistory;
|
$('#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');
|
document.body.classList.add('update-in-progress');
|
||||||
const btnCheck = $('#check-all-updates');
|
// const btnCheck = $('#check-all-updates');
|
||||||
const btnCheckForce = $('#check-all-updates-force');
|
const btnCheckForce = $('#check-all-updates-force');
|
||||||
const btnApply = $('#apply-all-updates');
|
const btnApply = $('#apply-all-updates');
|
||||||
const noUpdates = $('#update-all-no-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');
|
btnCheckForce.classList.add('hidden');
|
||||||
btnApply.classList.add('hidden');
|
btnApply.classList.add('hidden');
|
||||||
noUpdates.classList.add('hidden');
|
noUpdates.classList.add('hidden');
|
||||||
|
|
||||||
const ignoreDigest = this && this.id === 'check-all-updates-force';
|
const ignoreDigest = this && this.id === 'check-all-updates-force';
|
||||||
$$('.updatable:not(.can-update)' + (ignoreDigest ? '' : ':not(.update-problem)'))
|
$$('.updatable:not(.can-update)' + (ignoreDigest ? '' : ':not(.update-problem)'))
|
||||||
.map(checkUpdate);
|
.map(checkUpdate);
|
||||||
|
@ -53,10 +64,11 @@ function checkUpdateAll() {
|
||||||
chrome.runtime.onConnect.removeListener(onConnect);
|
chrome.runtime.onConnect.removeListener(onConnect);
|
||||||
});
|
});
|
||||||
|
|
||||||
API.updateCheckAll({
|
API.updateCheckBulk({
|
||||||
save: false,
|
save: false,
|
||||||
observe: true,
|
observe: true,
|
||||||
ignoreDigest,
|
ignoreDigest,
|
||||||
|
styleIds,
|
||||||
});
|
});
|
||||||
|
|
||||||
function observer(info, port) {
|
function observer(info, port) {
|
||||||
|
@ -82,21 +94,26 @@ function checkUpdateAll() {
|
||||||
|
|
||||||
port.onMessage.removeListener(observer);
|
port.onMessage.removeListener(observer);
|
||||||
document.body.classList.remove('update-in-progress');
|
document.body.classList.remove('update-in-progress');
|
||||||
btnCheck.disabled = total === 0;
|
// btnCheck.disabled = total === 0;
|
||||||
btnApply.disabled = false;
|
btnApply.disabled = false;
|
||||||
renderUpdatesOnlyFilter({check: updated + skippedEdited > 0});
|
renderUpdatesOnlyFilter({check: updated + skippedEdited > 0});
|
||||||
if (!updated) {
|
if (!updated) {
|
||||||
noUpdates.dataset.skippedEdited = skippedEdited > 0;
|
if (skippedEdited > 0) {
|
||||||
|
noUpdates.dataset.title = t('updateAllCheckSucceededSomeEdited');
|
||||||
|
}
|
||||||
noUpdates.classList.remove('hidden');
|
noUpdates.classList.remove('hidden');
|
||||||
btnCheckForce.classList.toggle('hidden', skippedEdited === 0);
|
btnCheckForce.classList.toggle('hidden', skippedEdited === 0);
|
||||||
|
updateTimer = setTimeout(() => {
|
||||||
|
noUpdates.classList.add('hidden');
|
||||||
|
noUpdates.dataset.title = '';
|
||||||
|
}, 1e4);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function checkUpdate(entry, {single} = {}) {
|
function checkUpdate(entry, {single} = {}) {
|
||||||
$('.update-note', entry).textContent = t('checkingForUpdate');
|
$('.check-update', entry).dataset.title = t('checkingForUpdate');
|
||||||
$('.check-update', entry).title = '';
|
|
||||||
if (single) {
|
if (single) {
|
||||||
API.updateCheck({
|
API.updateCheck({
|
||||||
save: false,
|
save: false,
|
||||||
|
@ -111,7 +128,8 @@ function checkUpdate(entry, {single} = {}) {
|
||||||
|
|
||||||
function reportUpdateState({updated, style, error, STATES}) {
|
function reportUpdateState({updated, style, error, STATES}) {
|
||||||
const isCheckAll = document.body.classList.contains('update-in-progress');
|
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([
|
const newClasses = new Map([
|
||||||
/*
|
/*
|
||||||
When a style is updated/installed, handleUpdateInstalled() clears "updatable"
|
When a style is updated/installed, handleUpdateInstalled() clears "updatable"
|
||||||
|
@ -130,8 +148,10 @@ function reportUpdateState({updated, style, error, STATES}) {
|
||||||
if (updated) {
|
if (updated) {
|
||||||
newClasses.set('can-update', true);
|
newClasses.set('can-update', true);
|
||||||
entry.updatedCode = style;
|
entry.updatedCode = style;
|
||||||
$('.update-note', entry).textContent = '';
|
const onlyUpdates = $('#only-updates');
|
||||||
$('#only-updates').classList.remove('hidden');
|
onlyUpdates.parentElement.classList.remove('hidden');
|
||||||
|
onlyUpdates.checked = true;
|
||||||
|
onlyUpdates.change();
|
||||||
} else if (!entry.classList.contains('can-update')) {
|
} else if (!entry.classList.contains('can-update')) {
|
||||||
const same = (
|
const same = (
|
||||||
error === STATES.SAME_MD5 ||
|
error === STATES.SAME_MD5 ||
|
||||||
|
@ -155,15 +175,14 @@ function reportUpdateState({updated, style, error, STATES}) {
|
||||||
const message = same ? t('updateCheckSucceededNoUpdate') : error;
|
const message = same ? t('updateCheckSucceededNoUpdate') : error;
|
||||||
newClasses.set('no-update', true);
|
newClasses.set('no-update', true);
|
||||||
newClasses.set('update-problem', !same);
|
newClasses.set('update-problem', !same);
|
||||||
$('.update-note', entry).textContent = message;
|
$('.check-update', entry).dataset.title = UI.enabled ? message : '';
|
||||||
$('.check-update', entry).title = newUI.enabled ? message : '';
|
$('.update', entry).dataset.title = t(edited ? 'updateCheckManualUpdateForce' : 'installUpdate');
|
||||||
$('.update', entry).title = t(edited ? 'updateCheckManualUpdateForce' : 'installUpdate');
|
|
||||||
// digest may change silently when forcing an update of a locally edited style
|
// 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
|
// so we need to update it in entry's styleMeta in all open manager tabs
|
||||||
if (error === STATES.SAME_CODE) {
|
if (error === STATES.SAME_CODE) {
|
||||||
for (const view of chrome.extension.getViews({type: 'tab'})) {
|
for (const view of chrome.extension.getViews({type: 'tab'})) {
|
||||||
if (view.location.pathname === location.pathname) {
|
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;
|
if (entry) entry.styleMeta.originalDigest = style.originalDigest;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -196,16 +215,15 @@ function reportUpdateState({updated, style, error, STATES}) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function renderUpdatesOnlyFilter({show, check} = {}) {
|
function renderUpdatesOnlyFilter({show, check} = {}) {
|
||||||
const numUpdatable = $$('.can-update').length;
|
const numUpdatable = $$('.can-update').length;
|
||||||
const mightUpdate = numUpdatable > 0 || $('.update-problem');
|
const mightUpdate = numUpdatable > 0 || $('.update-problem');
|
||||||
const checkbox = $('#only-updates input');
|
const checkbox = $('#only-updates');
|
||||||
show = show !== undefined ? show : mightUpdate;
|
show = show !== undefined ? show : mightUpdate;
|
||||||
check = check !== undefined ? show && check : checkbox.checked && mightUpdate;
|
check = check !== undefined ? show && check : checkbox.checked && mightUpdate;
|
||||||
|
|
||||||
$('#only-updates').classList.toggle('hidden', !show);
|
checkbox.checked = check;
|
||||||
checkbox.checked = check && show;
|
checkbox.parentElement.classList.toggle('hidden', !show);
|
||||||
checkbox.dispatchEvent(new Event('change'));
|
checkbox.dispatchEvent(new Event('change'));
|
||||||
|
|
||||||
const btnApply = $('#apply-all-updates');
|
const btnApply = $('#apply-all-updates');
|
||||||
|
@ -296,7 +314,6 @@ function handleUpdateInstalled(entry, reason) {
|
||||||
const note = t(isNew ? 'installButtonInstalled' : 'updateCompleted');
|
const note = t(isNew ? 'installButtonInstalled' : 'updateCompleted');
|
||||||
entry.classList.add('update-done', ...(isNew ? ['install-done'] : []));
|
entry.classList.add('update-done', ...(isNew ? ['install-done'] : []));
|
||||||
entry.classList.remove('can-update', 'updatable');
|
entry.classList.remove('can-update', 'updatable');
|
||||||
$('.update-note', entry).textContent = note;
|
$('.updated', entry).dataset.title = note;
|
||||||
$('.updated', entry).title = note;
|
|
||||||
renderUpdatesOnlyFilter();
|
renderUpdatesOnlyFilter();
|
||||||
}
|
}
|
||||||
|
|
|
@ -112,6 +112,5 @@
|
||||||
"id": "{7a7a4a92-a2a0-41d1-9fd7-1e92480d612d}",
|
"id": "{7a7a4a92-a2a0-41d1-9fd7-1e92480d612d}",
|
||||||
"strict_min_version": "53.0"
|
"strict_min_version": "53.0"
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2ypG+Z/beZtoYrxxwXYhMwQiAiwRVnSHqdpOSzJdjsXVWdvJjlgEuZcU8kte75w58P45LsRUrwvU6N9x12S6eW84KNEBC8rlZj0RGNoxuhSAcdxneYzjJ9tBkZKOidVedYHHsi3LeaXiLuTNTBR+2lf3uCNcP0ebaFML9uDLdYTGEW4eL3hnEKYPSmT1/xkh4bSGTToCg4YNuWWWoTA0beEOpBWYkPVMarLDQgPzMN5Byu5w3lOS2zL0PPJcmdyk3ez/ZsB4PZKU+h17fVA6+YTvUfxUqLde5i2RiuZhEb6Coo5/W90ZW1yCDC9osjWrxMGYeUMQWHPIgFtDhk4K6QIDAQAB"
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -145,8 +145,10 @@ function messageBox({
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeSelf() {
|
function removeSelf() {
|
||||||
messageBox.element.remove();
|
if (messageBox.element) {
|
||||||
messageBox.element = null;
|
messageBox.element.remove();
|
||||||
|
messageBox.element = null;
|
||||||
|
}
|
||||||
messageBox.resolve = null;
|
messageBox.resolve = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
16
options.html
16
options.html
|
@ -132,10 +132,10 @@
|
||||||
<label>
|
<label>
|
||||||
<span i18n-text="optionsUpdateInterval">
|
<span i18n-text="optionsUpdateInterval">
|
||||||
<a data-cmd="note"
|
<a data-cmd="note"
|
||||||
i18n-title="optionsUpdateImportNote"
|
i18n-title="optionsUpdateImportNote"
|
||||||
href="#"
|
href="#"
|
||||||
class="svg-inline-wrapper"
|
class="svg-inline-wrapper"
|
||||||
tabindex="0">
|
tabindex="0">
|
||||||
<svg class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg>
|
<svg class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg>
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
|
@ -187,10 +187,10 @@
|
||||||
<label>
|
<label>
|
||||||
<span i18n-text="optionsAdvancedExposeIframes">
|
<span i18n-text="optionsAdvancedExposeIframes">
|
||||||
<a data-cmd="note"
|
<a data-cmd="note"
|
||||||
i18n-title="optionsAdvancedExposeIframesNote"
|
i18n-title="optionsAdvancedExposeIframesNote"
|
||||||
href="#"
|
href="#"
|
||||||
class="svg-inline-wrapper"
|
class="svg-inline-wrapper"
|
||||||
tabindex="0">
|
tabindex="0">
|
||||||
<svg class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg>
|
<svg class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg>
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -187,7 +187,7 @@ function checkUpdates() {
|
||||||
chrome.runtime.onConnect.removeListener(onConnect);
|
chrome.runtime.onConnect.removeListener(onConnect);
|
||||||
});
|
});
|
||||||
|
|
||||||
API.updateCheckAll({observe: true});
|
API.updateCheckBulk({observe: true});
|
||||||
|
|
||||||
function observer(info) {
|
function observer(info) {
|
||||||
if ('count' in 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