diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 73b2063808..e7ed173fc4 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -3,6 +3,14 @@ "message": "Write new style", "description": "Label for the button to go to the add style page" }, + "addPlainStyleLabel": { + "message": "New plain style", + "description": "Label for the button to go to the add style page" + }, + "addUserCSSStyleLabel": { + "message": "New UserCSS style", + "description": "Label for the button to go to the add style page" + }, "addStyleTitle": { "message": "Add Style", "description": "Title of the page for adding styles" @@ -24,10 +32,6 @@ }, "description": "Text on the manage screen to describe what the style applies to" }, - "appliesDisplayTruncatedSuffix": { - "message": "and more", - "description": "Text added to appliesDisplay when there are more sites for the style than are displayed" - }, "appliesDomainOption": { "message": "URLs on the domain", "description": "Option to make the style apply to the entered string as a domain" @@ -88,12 +92,17 @@ "message": "Backup", "description": "Heading for backup" }, + "backupImport": { + "message": "Import backup", + "description": "Tooltip for header import/restore backup icon" + }, "backupMessage": { "message": "Select a file or drag and drop to this page.", "description": "Message for backup" }, "bckpInstStyles": { - "message": "Export styles" + "message": "Local device", + "description": "Selected option to backup indicated styles to local device/drive" }, "checkAllUpdates": { "message": "Check all styles for updates", @@ -335,7 +344,11 @@ }, "exportLabel": { "message": "Export", - "description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)" + "description": "Label for the button to export a style ('edit' page) or bulk styles ('manage' page)" + }, + "exportAllLabel": { + "message": "Export All", + "description": "Label for the button to export all styles ('manage' page)" }, "externalFeedback": { "message": "Feedback", @@ -405,6 +418,10 @@ "message": "Enabled", "description": "Used in various lists/options to indicate that something is enabled" }, + "genericFilterLabel": { + "message": "Filter", + "description": "Used in various lists/options to indicate that something is or will be filtered" + }, "genericError": { "message": "Error", "description": "Used in various places to indicate some error occurred." @@ -413,6 +430,10 @@ "message": "History", "description": "Used in various places to show a history log of something" }, + "genericName": { + "message": "Name", + "description": "Used in various places to indicate the style name" + }, "genericNext": { "message": "Next", "description": "Used in various places to select/perform the next step/action" @@ -554,6 +575,10 @@ "message": "Get help", "description": "Homepage link text on the manage page e.g. https://add0n.com/stylus.html#features with chat/FAQ/intro/info" }, + "linkChat": { + "message": "Chat with us", + "description": "Link to open a new browser tab to chat with users & developers on Discord" + }, "linkGetStyles": { "message": "Get styles", "description": "Help link text on the manage page e.g. https://userstyles.org" @@ -1164,7 +1189,8 @@ "description": "Label before the replace-with input field in the editor shown on Ctrl-H etc." }, "retrieveBckp": { - "message": "Import styles" + "message": "Local source", + "description": "Selected option to get a backup of styles from a local device/drive" }, "search": { "message": "Search", @@ -1214,6 +1240,26 @@ "message": " key focuses the search field.\nPlain text: search within the name, code, homepage URL and sites it is applied to. Words with less than 3 letters are ignored.\nStyles matching a full URL: prefix the search with , e.g. \nRegular expressions: include slashes and flags, e.g. \nExact words: wrap the query in double quotes, e.g. <\".header ~ div\">", "description": "Text in the minihelp displayed when clicking (i) icon to the right of the search input field on the Manage styles page" }, + "bulkActions": { + "message": "Apply actions to selected styles", + "description": "Label for bulk actions select dropdown" + }, + "bulkActionsSelect": { + "message": "Choose a bulk action", + "description": "Placeholder text in dropdown to tell the user to choose an action to apply to all selected styles" + }, + "bulkActionsApply": { + "message": "Apply", + "description": "Text for button to apply the selected action" + }, + "bulkActionsTooltip": { + "message": "Bulk actions can be applied to selected styles in this column", + "description": "Select style for bulk action header tooltip" + }, + "bulkActionsError": { + "message": "Choose at least one style", + "description": "Error displayed in a tooltip when the user attempts to apply an action with no styles selected" + }, "sectionAdd": { "message": "Add another section", "description": "Label for the button to add a section" @@ -1237,33 +1283,34 @@ "shortcutsNote": { "message": "Define keyboard shortcuts" }, - "sortDateNewestFirst": { - "message": "newest first", - "description": "Text added to indicate that sorting a date would add the newest entries at the top" - }, - "sortDateOldestFirst": { - "message": "oldest first", - "description": "Text added to indicate that sorting a date would add the oldest entries at the top" + "sortColumnEnabled": { + "message": "enabled styles", + "description": "Column name seen in the tooltip when hovering over the header; used to replace the 'sortLabel` placeholder" }, - "sortLabel": { - "message": "Select a sort to apply to the installed styles", - "description": "Title on the sort select to indicate it is used for sorting entries" + "sortColumnName": { + "message": "style name", + "description": "Column name seen in the tooltip when hovering over the header; used to replace the 'sortLabel` placeholder" }, - "sortLabelTitleAsc": { - "message": "Title Ascending", - "description": "Text added to option group to indicate a block of options that apply a title ascending (A to Z) sort" + "sortColumnVersion": { + "message": "style version", + "description": "Column name seen in the tooltip when hovering over the header; used to replace the 'sortLabel` placeholder" }, - "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" + "sortColumnLastUpdate": { + "message": "last updated", + "description": "Column name seen in the tooltip when hovering over the header; used to replace the 'sortLabel` placeholder" }, - "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" + "sortHeader": { + "message": "Sort", + "description": "Title of sort column, indicating that the style can be manually sorted by dragging & dropping" }, - "sortStylesHelpTitle": { - "message": "Sort contents", - "description": "Label for the sort info popup on the Manage styles page" + "sortLabel": { + "message": "Click to sort the \"$name$\" column;\nUse shift + click to sort multiple columns", + "placeholders": { + "name": { + "content": "$1" + } + }, + "description": "Title added to links in the manager page header to inform the user on how to sort the columns" }, "styleBadRegexp": { "message": "Regexp is invalid.", diff --git a/background/search-db.js b/background/search-db.js index 75318304ad..4f9c9a2d98 100644 --- a/background/search-db.js +++ b/background/search-db.js @@ -4,13 +4,108 @@ (() => { // toLocaleLowerCase cache, autocleared after 1 minute const cache = new Map(); - // top-level style properties to be searched - const PARTS = { - name: searchText, - url: searchText, - sourceCode: searchText, - sections: searchSections, - }; + + // Creates an array of intermediate words (2 letter minimum) + // 'usercss' => ["us", "use", "user", "userc", "usercs", "usercss"] + // this makes it so the user can type partial queries and not have the search + // constantly switching between using & ignoring the filter + const createPartials = id => id.split('').reduce((acc, _, index) => { + if (index > 0) { + acc.push(id.substring(0, index + 1)); + } + return acc; + }, []); + + const searchWithin = [{ + id: 'code', + labels: createPartials('code'), + get: style => style.sections.map(section => section.code).join(' ') + }, { + id: 'usercss', + labels: [...createPartials('usercss'), ...createPartials('meta')], + get: style => JSON.stringify(style.usercssData || {}) + // remove JSON structure; restore urls + .replace(/[[\]{},":]/g, ' ').replace(/\s\/\//g, '://') + }, { + id: 'name', // default + labels: createPartials('name'), + get: style => style.name + }]; + + const styleProps = [{ + id: 'enabled', + labels: ['on', ...createPartials('enabled')], + check: style => style.enabled + }, { + id: 'disabled', + labels: ['off', ...createPartials('disabled')], + check: style => !style.enabled + }, { + id: 'local', + labels: createPartials('local'), + check: style => !style.updateUrl + }, { + id: 'external', + labels: createPartials('external'), + check: style => style.updateUrl + }, { + id: 'usercss', + labels: createPartials('usercss'), + check: style => style.usercssData + }, { + id: 'non usercss', + labels: ['original', ...createPartials('nonusercss')], + check: style => !style.usercssData + }]; + + const matchers = [{ + id: 'url', + test: query => /url:\w+/i.test(query), + matches: query => { + const matchUrl = query.match(/url:([/.-_\w]+)/); + const result = matchUrl && matchUrl[1] + ? styleManager.getStylesByUrl(matchUrl[1]) + .then(result => result.map(r => r.data.id)) + : []; + return {result}; + }, + }, { + id: 'regex', + test: query => { + const x = query.includes('/') && !query.includes('//') && + /^\/(.+?)\/([gimsuy]*)$/.test(query); + // console.log('regex match?', query, x); + return x; + }, + matches: () => ({regex: tryRegExp(RegExp.$1, RegExp.$2)}) + }, { + id: 'props', + test: query => /is:/.test(query), + matches: query => { + const label = /is:(\w+)/g.exec(query); + return label && label[1] + ? {prop: styleProps.find(p => p.labels.includes(label[1]))} + : {}; + } + }, { + id: 'within', + test: query => /in:/.test(query), + matches: query => { + const label = /in:(\w+)/g.exec(query); + return label && label[1] + ? {within: searchWithin.find(s => s.labels.includes(label[1]))} + : {}; + } + }, { + id: 'default', + test: () => true, + matches: query => { + const word = query.startsWith('"') && query.endsWith('"') + ? query.slice(1, -1) + : query; + return {word: word || query}; + } + }]; /** * @param params @@ -19,77 +114,94 @@ * @returns {number[]} - array of matched styles ids */ API_METHODS.searchDB = ({query, ids}) => { - let rx, words, icase, matchUrl; - query = query.trim(); + const parts = query.trim().split(/(".*?")|\s+/).filter(Boolean); - if (/^url:/i.test(query)) { - matchUrl = query.slice(query.indexOf(':') + 1).trim(); - if (matchUrl) { - return styleManager.getStylesByUrl(matchUrl) - .then(results => results.map(r => r.data.id)); + const searchFilters = { + words: [], + regex: null, // only last regex expression is used + results: [], + props: [], + within: [], + }; + + const searchText = (text, searchFilters) => { + if (searchFilters.regex) return searchFilters.regex.test(text); + for (let pass = 1; pass <= (searchFilters.icase ? 2 : 1); pass++) { + if (searchFilters.words.every(w => text.includes(w))) return true; + text = lower(text); } + }; + + const searchProps = (style, searchFilters) => { + const x = searchFilters.props.every(prop => { + const y = prop.check(style) + // if (y) console.log('found prop', prop.id, style.id) + return y; + }); + // if (x) console.log('found prop', style.id) + return x; + }; + + parts.forEach(part => { + matchers.some(matcher => { + if (matcher.test(part)) { + const {result, regex, word, prop, within} = matcher.matches(part || ''); + if (result) searchFilters.results.push(result); + if (regex) searchFilters.regex = regex; // limited to a single regexp + if (word) searchFilters.words.push(word); + if (prop) searchFilters.props.push(prop); + if (within) searchFilters.within.push(within); + return true; + } + }); + }); + if (!searchFilters.within.length) { + searchFilters.within.push(...searchWithin.slice(-1)); } - if (query.startsWith('/') && /^\/(.+?)\/([gimsuy]*)$/.test(query)) { - rx = tryRegExp(RegExp.$1, RegExp.$2); - } - if (!rx) { - words = query - .split(/(".*?")|\s+/) - .filter(Boolean) - .map(w => w.startsWith('"') && w.endsWith('"') - ? w.slice(1, -1) - : w) - .filter(w => w.length > 1); - words = words.length ? words : [query]; - icase = words.some(w => w === lower(w)); + + // console.log('matchers', searchFilters); + // url matches + if (searchFilters.results.length) { + return searchFilters.results; } + searchFilters.icase = searchFilters.words.some(w => w === lower(w)); + query = parts.join(' ').trim(); return styleManager.getAllStyles().then(styles => { if (ids) { const idSet = new Set(ids); styles = styles.filter(s => idSet.has(s.id)); } + const results = []; + const propResults = []; + const hasProps = searchFilters.props.length > 0; + const noWords = searchFilters.words.length === 0; for (const style of styles) { const id = style.id; - if (!query || words && !words.length) { + if (noWords) { + // no query or only filters are matching -> show all styles results.push(id); - continue; - } - for (const part in PARTS) { - const text = style[part]; - if (text && PARTS[part](text, rx, words, icase)) { + } else { + const text = searchFilters.within.map(within => within.get(style)).join(' '); + if (searchText(text, searchFilters)) { results.push(id); - break; } } + if (hasProps && searchProps(style, searchFilters) && results.includes(id)) { + propResults.push(id); + } } + // results AND propResults + const finalResults = hasProps + ? propResults.filter(id => results.includes(id)) + : results; if (cache.size) debounce(clearCache, 60e3); - return results; + // console.log('final', finalResults) + return finalResults; }); }; - function searchText(text, rx, words, icase) { - if (rx) return rx.test(text); - for (let pass = 1; pass <= (icase ? 2 : 1); pass++) { - if (words.every(w => text.includes(w))) return true; - text = lower(text); - } - } - - function searchSections(sections, rx, words, icase) { - for (const section of sections) { - for (const prop in section) { - const value = section[prop]; - if (typeof value === 'string') { - if (searchText(value, rx, words, icase)) return true; - } else if (Array.isArray(value)) { - if (value.some(str => searchText(str, rx, words, icase))) return true; - } - } - } - } - function lower(text) { let result = cache.get(text); if (result) return result; diff --git a/background/update.js b/background/update.js index 2a0e02a7a1..10177550e9 100644 --- a/background/update.js +++ b/background/update.js @@ -31,7 +31,7 @@ const retrying = new Set(); - API_METHODS.updateCheckAll = checkAllStyles; + API_METHODS.updateCheckBulk = checkBulkStyles; API_METHODS.updateCheck = checkStyle; API_METHODS.getUpdaterStates = () => STATES; @@ -39,18 +39,22 @@ schedule(); chrome.alarms.onAlarm.addListener(onAlarm); - return {checkAllStyles, checkStyle, STATES}; + return {checkBulkStyles, checkStyle, STATES}; - function checkAllStyles({ + function checkBulkStyles({ save = true, ignoreDigest, observe, + styleIds = [], } = {}) { resetInterval(); checkingAll = true; retrying.clear(); const port = observe && chrome.runtime.connect({name: 'updater'}); return styleManager.getAllStyles().then(styles => { + if (styleIds.length) { + styles = styles.filter(style => styleIds.includes(style.id)); + } styles = styles.filter(style => style.updateUrl); if (port) port.postMessage({count: styles.length}); log(''); @@ -247,7 +251,7 @@ } function onAlarm({name}) { - if (name === ALARM_NAME) checkAllStyles(); + if (name === ALARM_NAME) checkBulkStyles(); } function resetInterval() { diff --git a/global.css b/global.css index c010a958f7..031aeb3d9c 100644 --- a/global.css +++ b/global.css @@ -136,6 +136,7 @@ select { color: #000; background-color: transparent; border: 1px solid hsl(0, 0%, 66%); + border-radius: 2px; padding: 0 20px 0 6px; transition: color .5s; } diff --git a/js/localization.js b/js/localization.js index e4a8de107f..ffd725dd8a 100644 --- a/js/localization.js +++ b/js/localization.js @@ -7,6 +7,11 @@ tDocLoader(); function t(key, params) { + if (!params && key.includes(';')) { + [key, params] = key.split(';'); + // sometimes a param like "usercss" is passed; not defined in messages.json + params = params ? chrome.i18n.getMessage(params) || params : ''; + } const cache = !params && t.cache[key]; const s = cache || chrome.i18n.getMessage(key, params); if (s === '') { diff --git a/js/messaging.js b/js/messaging.js index 93a4e5a727..f47d942562 100644 --- a/js/messaging.js +++ b/js/messaging.js @@ -18,9 +18,9 @@ if (!CHROME && !chrome.browserAction.openPopup) { // in FF pre-57 legacy addons can override useragent so we assume the worst // until we know for sure in the async getBrowserInfo() // (browserAction.openPopup was added in 57) - FIREFOX = browser.runtime.getBrowserInfo ? 51 : 50; + FIREFOX = browserApi.runtime.getBrowserInfo ? 51 : 50; // getBrowserInfo was added in FF 51 - Promise.resolve(FIREFOX >= 51 ? browser.runtime.getBrowserInfo() : {version: 50}).then(info => { + Promise.resolve(FIREFOX >= 51 ? browserApi.runtime.getBrowserInfo() : {version: 50}).then(info => { FIREFOX = parseFloat(info.version); document.documentElement.classList.add('moz-appearance-bug', FIREFOX && FIREFOX < 54); }); diff --git a/js/prefs.js b/js/prefs.js index b227d2c69b..64eaa081dc 100644 --- a/js/prefs.js +++ b/js/prefs.js @@ -27,15 +27,11 @@ self.prefs = self.INJECTED === 1 ? self.prefs : (() => { 'manage.onlyEnabled.invert': false, // display only disabled styles 'manage.onlyLocal.invert': false, // display only externally installed styles 'manage.onlyUsercss.invert': false, // display only non-usercss (standard) styles - // UI element state: expanded/collapsed - 'manage.backup.expanded': true, - 'manage.filters.expanded': true, - 'manage.options.expanded': true, - // the new compact layout doesn't look good on Android yet - 'manage.newUI': !navigator.appVersion.includes('Android'), - 'manage.newUI.favicons': false, // show favicons for the sites in applies-to + 'manage.export.destination': 'local', // default export destination (local or dropbox) + + 'manage.newUI.favicons': true, // show favicons for the sites in applies-to 'manage.newUI.faviconsGray': true, // gray out favicons - 'manage.newUI.targets': 3, // max number of applies-to targets visible: 0 = none + 'manage.newUI.targets': 6, // max number of applies-to targets visible: 0 = none 'manage.newUI.sort': 'title,asc', 'editor.options': {}, // CodeMirror.defaults.* diff --git a/manage.html b/manage.html index 76e0c7ce5f..a977d808c9 100644 --- a/manage.html +++ b/manage.html @@ -1,4 +1,4 @@ - + @@ -6,12 +6,11 @@ - - - - - - + + + + +