From 23dc2d1a5b6b3db7e5daa30c71eda1add2273e34 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Mon, 19 Feb 2024 20:05:49 +0200 Subject: refactor ISS stuff into separate file --- src/services/theme_data/iss_utils.js | 119 +++++++++++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 src/services/theme_data/iss_utils.js (limited to 'src/services/theme_data/iss_utils.js') diff --git a/src/services/theme_data/iss_utils.js b/src/services/theme_data/iss_utils.js new file mode 100644 index 00000000..6568f576 --- /dev/null +++ b/src/services/theme_data/iss_utils.js @@ -0,0 +1,119 @@ +// "Unrolls" a tree structure of item: { parent: { ...item2, parent: { ...item3, parent: {...} } }} +// into an array [item2, item3] for iterating +export const unroll = (item) => { + const out = [] + let currentParent = item + while (currentParent) { + out.push(currentParent) + currentParent = currentParent.parent + } + return out +} + +// This gives you an array of arrays of all possible unique (i.e. order-insensitive) combinations +export const getAllPossibleCombinations = (array) => { + const combos = [array.map(x => [x])] + for (let comboSize = 2; comboSize <= array.length; comboSize++) { + const previous = combos[combos.length - 1] + const selfSet = new Set() + const newCombos = previous.map(self => { + self.forEach(x => selfSet.add(x)) + const nonSelf = array.filter(x => !selfSet.has(x)) + return nonSelf.map(x => [...self, x]) + }) + const flatCombos = newCombos.reduce((acc, x) => [...acc, ...x], []) + combos.push(flatCombos) + } + return combos.reduce((acc, x) => [...acc, ...x], []) +} + +// Converts rule, parents and their criteria into a CSS (or path if ignoreOutOfTreeSelector == true) selector +export const genericRuleToSelector = components => (rule, ignoreOutOfTreeSelector, isParent) => { + if (!rule && !isParent) return null + const component = components[rule.component] + const { states, variants, selector, outOfTreeSelector } = component + + const applicableStates = ((rule.state || []).filter(x => x !== 'normal')).map(state => states[state]) + + const applicableVariantName = (rule.variant || 'normal') + let applicableVariant = '' + if (applicableVariantName !== 'normal') { + applicableVariant = variants[applicableVariantName] + } else { + applicableVariant = variants?.normal ?? '' + } + + let realSelector + if (selector === ':root') { + realSelector = '' + } else if (isParent) { + realSelector = selector + } else { + if (outOfTreeSelector && !ignoreOutOfTreeSelector) realSelector = outOfTreeSelector + else realSelector = selector + } + + const selectors = [realSelector, applicableVariant, ...applicableStates] + .toSorted((a, b) => { + if (a.startsWith(':')) return 1 + if (/^[a-z]/.exec(a)) return -1 + else return 0 + }) + .join('') + + if (rule.parent) { + return (genericRuleToSelector(components)(rule.parent, ignoreOutOfTreeSelector, true) + ' ' + selectors).trim() + } + return selectors.trim() +} + +export const combinationsMatch = (criteria, subject, strict) => { + if (criteria.component !== subject.component) return false + + // All variants inherit from normal + if (subject.variant !== 'normal' || strict) { + if (criteria.variant !== subject.variant) return false + } + + // Subject states > 1 essentially means state is "normal" and therefore matches + if (subject.state.length > 1 || strict) { + const subjectStatesSet = new Set(subject.state) + const criteriaStatesSet = new Set(criteria.state) + + const setsAreEqual = + [...criteriaStatesSet].every(state => subjectStatesSet.has(state)) && + [...subjectStatesSet].every(state => criteriaStatesSet.has(state)) + + if (!setsAreEqual) return false + } + return true +} + +export const findRules = (criteria, strict) => subject => { + // If we searching for "general" rules - ignore "specific" ones + if (criteria.parent === null && !!subject.parent) return false + if (!combinationsMatch(criteria, subject, strict)) return false + + if (criteria.parent !== undefined && criteria.parent !== null) { + if (!subject.parent && !strict) return true + const pathCriteria = unroll(criteria) + const pathSubject = unroll(subject) + if (pathCriteria.length < pathSubject.length) return false + + // Search: .a .b .c + // Matches: .a .b .c; .b .c; .c; .z .a .b .c + // Does not match .a .b .c .d, .a .b .e + for (let i = 0; i < pathCriteria.length; i++) { + const criteriaParent = pathCriteria[i] + const subjectParent = pathSubject[i] + if (!subjectParent) return true + if (!combinationsMatch(criteriaParent, subjectParent, strict)) return false + } + } + return true +} + +export const normalizeCombination = rule => { + rule.variant = rule.variant ?? 'normal' + rule.state = [...new Set(['normal', ...(rule.state || [])])] +} -- cgit v1.2.3-70-g09d2 From c1568ad2ba283336378e135ce329bb4c4c1b92f2 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Mon, 25 Mar 2024 18:18:48 +0200 Subject: fix massive issue in getAllPossibleCombinations --- src/services/theme_data/iss_utils.js | 16 +++++++++++++--- .../unit/specs/services/theme_data/theme_data3.spec.js | 18 ++++++++++++++++-- 2 files changed, 29 insertions(+), 5 deletions(-) (limited to 'src/services/theme_data/iss_utils.js') diff --git a/src/services/theme_data/iss_utils.js b/src/services/theme_data/iss_utils.js index 6568f576..2d9dd0b5 100644 --- a/src/services/theme_data/iss_utils.js +++ b/src/services/theme_data/iss_utils.js @@ -11,18 +11,28 @@ export const unroll = (item) => { } // This gives you an array of arrays of all possible unique (i.e. order-insensitive) combinations +// Can only accept primitives. Duplicates are not supported and can cause unexpected behavior export const getAllPossibleCombinations = (array) => { const combos = [array.map(x => [x])] for (let comboSize = 2; comboSize <= array.length; comboSize++) { const previous = combos[combos.length - 1] - const selfSet = new Set() const newCombos = previous.map(self => { + const selfSet = new Set() self.forEach(x => selfSet.add(x)) const nonSelf = array.filter(x => !selfSet.has(x)) return nonSelf.map(x => [...self, x]) }) const flatCombos = newCombos.reduce((acc, x) => [...acc, ...x], []) - combos.push(flatCombos) + const uniqueComboStrings = new Set() + const uniqueCombos = flatCombos.map(x => x.toSorted()).filter(x => { + if (uniqueComboStrings.has(x.join())) { + return false + } else { + uniqueComboStrings.add(x.join()) + return true + } + }) + combos.push(uniqueCombos) } return combos.reduce((acc, x) => [...acc, ...x], []) } @@ -31,7 +41,7 @@ export const getAllPossibleCombinations = (array) => { export const genericRuleToSelector = components => (rule, ignoreOutOfTreeSelector, isParent) => { if (!rule && !isParent) return null const component = components[rule.component] - const { states, variants, selector, outOfTreeSelector } = component + const { states = {}, variants = {}, selector, outOfTreeSelector } = component const applicableStates = ((rule.state || []).filter(x => x !== 'normal')).map(state => states[state]) diff --git a/test/unit/specs/services/theme_data/theme_data3.spec.js b/test/unit/specs/services/theme_data/theme_data3.spec.js index 25a9dda4..37d343f9 100644 --- a/test/unit/specs/services/theme_data/theme_data3.spec.js +++ b/test/unit/specs/services/theme_data/theme_data3.spec.js @@ -11,9 +11,23 @@ import { describe.only('Theme Data 3', () => { describe('getAllPossibleCombinations', () => { - it('test simple case', () => { + it('test simple 3 values case', () => { const out = getAllPossibleCombinations([1, 2, 3]).map(x => x.sort((a, b) => a - b)) - expect(out).to.eql([[1], [2], [3], [1, 2], [1, 3], [2, 3], [1, 2, 3]]) + expect(out).to.eql([ + [1], [2], [3], + [1, 2], [1, 3], [2, 3], + [1, 2, 3] + ]) + }) + + it('test simple 4 values case', () => { + const out = getAllPossibleCombinations([1, 2, 3, 4]).map(x => x.sort((a, b) => a - b)) + expect(out).to.eql([ + [1], [2], [3], [4], + [1, 2], [1, 3], [1, 4], [2, 3], [2, 4], [3, 4], + [1, 2, 3], [1, 2, 4], [1, 3, 4], [2, 3, 4], + [1, 2, 3, 4] + ]) }) }) -- cgit v1.2.3-70-g09d2 From dc37c7b28b2142227f73a36acc27a7ac77c7e59a Mon Sep 17 00:00:00 2001 From: tusooa Date: Fri, 31 May 2024 14:33:44 -0400 Subject: Fix Themes v3 not working on Safari --- changelog.d/themesv3-on-safari.fix | 1 + index.html | 2 + src/services/style_setter/style_setter.js | 69 +++++++++++++++++++------ src/services/theme_data/iss_utils.js | 6 ++- src/services/theme_data/theme_data_3.service.js | 4 +- 5 files changed, 62 insertions(+), 20 deletions(-) create mode 100644 changelog.d/themesv3-on-safari.fix (limited to 'src/services/theme_data/iss_utils.js') diff --git a/changelog.d/themesv3-on-safari.fix b/changelog.d/themesv3-on-safari.fix new file mode 100644 index 00000000..af767b3a --- /dev/null +++ b/changelog.d/themesv3-on-safari.fix @@ -0,0 +1 @@ +Fix Themes v3 on Safari not working diff --git a/index.html b/index.html index e790fb57..6d9c4ce5 100644 --- a/index.html +++ b/index.html @@ -4,6 +4,8 @@ + + diff --git a/src/services/style_setter/style_setter.js b/src/services/style_setter/style_setter.js index 369d2c9f..83faa0b3 100644 --- a/src/services/style_setter/style_setter.js +++ b/src/services/style_setter/style_setter.js @@ -6,6 +6,45 @@ import { getCssRules } from '../theme_data/css_utils.js' import { defaultState } from '../../modules/config.js' import { chunk } from 'lodash' +// On platforms where this is not supported, it will return undefined +// Otherwise it will return an array +const supportsAdoptedStyleSheets = !!document.adoptedStyleSheets + +const createStyleSheet = (id) => { + if (supportsAdoptedStyleSheets) { + return { + el: null, + sheet: new CSSStyleSheet(), + rules: [] + } + } + + const el = document.getElementById(id) + // Clear all rules in it + for (let i = el.sheet.cssRules.length - 1; i >= 0; --i) { + el.sheet.deleteRule(i) + } + + return { + el, + sheet: el.sheet, + rules: [] + } +} + +const EAGER_STYLE_ID = 'pleroma-eager-styles' +const LAZY_STYLE_ID = 'pleroma-lazy-styles' + +const adoptStyleSheets = (styles) => { + if (supportsAdoptedStyleSheets) { + document.adoptedStyleSheets = styles.map(s => s.sheet) + } + // Some older browsers do not support document.adoptedStyleSheets. + // In this case, we use the diff --git a/src/components/settings_modal/tabs/theme_tab/preview.vue b/src/components/settings_modal/tabs/theme_tab/preview.vue index 1837620f..7ec34afd 100644 --- a/src/components/settings_modal/tabs/theme_tab/preview.vue +++ b/src/components/settings_modal/tabs/theme_tab/preview.vue @@ -139,6 +139,108 @@ export default {} + diff --git a/src/components/settings_modal/tabs/theme_tab/theme_tab.js b/src/components/settings_modal/tabs/theme_tab/theme_tab.js index 8d025ea4..a19550d1 100644 --- a/src/components/settings_modal/tabs/theme_tab/theme_tab.js +++ b/src/components/settings_modal/tabs/theme_tab/theme_tab.js @@ -30,7 +30,10 @@ import { import { convertTheme2To3 } from 'src/services/theme_data/theme2_to_theme3.js' import { init } from 'src/services/theme_data/theme_data_3.service.js' -import { getCssRules } from 'src/services/theme_data/css_utils.js' +import { + getCssRules, + getScopedVersion +} from 'src/services/theme_data/css_utils.js' import ColorInput from 'src/components/color_input/color_input.vue' import RangeInput from 'src/components/range_input/range_input.vue' @@ -703,17 +706,10 @@ export default { liteMode: true }) - this.themeV3Preview = getCssRules(theme3.eager) - .map(x => { - if (x.startsWith('html')) { - return x.replace('html', '#theme-preview') - } else if (x.startsWith('#content')) { - return x.replace('#content', '#theme-preview') - } else { - return '#theme-preview > ' + x - } - }) - .join('\n') + this.themeV3Preview = getScopedVersion( + getCssRules(theme3.eager), + '#theme-preview' + ).join('\n') } }, watch: { diff --git a/src/components/settings_modal/tabs/theme_tab/theme_tab.scss b/src/components/settings_modal/tabs/theme_tab/theme_tab.scss index 4cb37c1e..3ae8864c 100644 --- a/src/components/settings_modal/tabs/theme_tab/theme_tab.scss +++ b/src/components/settings_modal/tabs/theme_tab/theme_tab.scss @@ -161,107 +161,6 @@ } } - .preview-container { - border-top: 1px dashed; - border-bottom: 1px dashed; - border-color: var(--border); - margin: 1em 0; - padding: 1em; - background-color: var(--wallpaper); - background-image: var(--body-background-image); - background-size: cover; - background-position: 50% 50%; - - .dummy { - .post { - font-family: var(--postFont); - display: flex; - - .content { - flex: 1; - - h4 { - margin-bottom: 0.25em; - } - - .icons { - margin-top: 0.5em; - display: flex; - - i { - margin-right: 1em; - } - } - } - } - - .after-post { - margin-top: 1em; - display: flex; - align-items: center; - } - - .avatar, - .avatar-alt { - background: - linear-gradient( - 135deg, - #b8e1fc 0%, - #a9d2f3 10%, - #90bae4 25%, - #90bcea 37%, - #90bff0 50%, - #6ba8e5 51%, - #a2daf5 83%, - #bdf3fd 100% - ); - color: black; - font-family: sans-serif; - text-align: center; - margin-right: 1em; - } - - .avatar-alt { - flex: 0 auto; - margin-left: 28px; - font-size: 12px; - min-width: 20px; - min-height: 20px; - line-height: 20px; - } - - .avatar { - flex: 0 auto; - width: 48px; - height: 48px; - font-size: 14px; - line-height: 48px; - } - - .actions { - display: flex; - align-items: baseline; - - .checkbox { - display: inline-flex; - align-items: baseline; - margin-right: 1em; - flex: 1; - } - } - - .separator { - margin: 1em; - border-bottom: 1px solid; - border-color: var(--border); - } - - .btn { - min-width: 3em; - } - } - } - .radius-item { flex-basis: auto; } @@ -314,10 +213,6 @@ max-width: 50em; } - .theme-preview-content { - padding: 20px; - } - .theme-warning { display: flex; align-items: baseline; diff --git a/src/components/status/post.style.js b/src/components/status/post.style.js index 8dce527e..d0038424 100644 --- a/src/components/status/post.style.js +++ b/src/components/status/post.style.js @@ -17,6 +17,15 @@ export default { 'Attachment', 'PollGraph' ], + validInnerComponentsLite: [ + 'Text', + 'Link', + 'Icon', + 'Border', + 'ButtonUnstyled', + 'RichContent', + 'Avatar' + ], defaultRules: [ { directives: { diff --git a/src/modules/interface.js b/src/modules/interface.js index a9cd70e5..a0746052 100644 --- a/src/modules/interface.js +++ b/src/modules/interface.js @@ -234,25 +234,6 @@ const interfaceMod = { return } - const normalizeThemeData = (themeData) => { - if (themeData.themeFileVerison === 1) { - return generatePreset(themeData).theme - } - // New theme presets don't have 'theme' property, they use 'source' - const themeSource = themeData.source - - let out // shout, shout let it all out - if (!themeData.theme || (themeSource && themeSource.themeEngineVersion === CURRENT_VERSION)) { - out = themeSource || themeData - } else { - out = themeData.theme - } - - // generatePreset here basically creates/updates "snapshot", - // while also fixing the 2.2 -> 2.3 colors/shadows/etc - return generatePreset(out).theme - } - let promise = null if (themeName) { @@ -320,3 +301,38 @@ const interfaceMod = { } export default interfaceMod + +export const normalizeThemeData = (input) => { + let themeData = input + + if (Array.isArray(themeData)) { + themeData = { colors: {} } + themeData.colors.bg = input[1] + themeData.colors.fg = input[2] + themeData.colors.text = input[3] + themeData.colors.link = input[4] + themeData.colors.cRed = input[5] + themeData.colors.cGreen = input[6] + themeData.colors.cBlue = input[7] + themeData.colors.cOrange = input[8] + return generatePreset(themeData).theme + } + + if (themeData.themeFileVerison === 1) { + return generatePreset(themeData).theme + } + + // New theme presets don't have 'theme' property, they use 'source' + const themeSource = themeData.source + + let out // shout, shout let it all out + if (!themeData.theme || (themeSource && themeSource.themeEngineVersion === CURRENT_VERSION)) { + out = themeSource || themeData + } else { + out = themeData.theme + } + + // generatePreset here basically creates/updates "snapshot", + // while also fixing the 2.2 -> 2.3 colors/shadows/etc + return generatePreset(out).theme +} diff --git a/src/services/theme_data/css_utils.js b/src/services/theme_data/css_utils.js index 8423e8ac..9bce4834 100644 --- a/src/services/theme_data/css_utils.js +++ b/src/services/theme_data/css_utils.js @@ -159,3 +159,15 @@ export const getCssRules = (rules, debug) => rules.map(rule => { footer ].join('\n') }).filter(x => x) + +export const getScopedVersion = (rules, newScope) => { + return rules.map(x => { + if (x.startsWith('html')) { + return x.replace('html', newScope) + } else if (x.startsWith('#content')) { + return x.replace('#content', newScope) + } else { + return newScope + ' > ' + x + } + }) +} diff --git a/src/services/theme_data/iss_utils.js b/src/services/theme_data/iss_utils.js index 83ca8242..75f8dd93 100644 --- a/src/services/theme_data/iss_utils.js +++ b/src/services/theme_data/iss_utils.js @@ -39,7 +39,23 @@ export const getAllPossibleCombinations = (array) => { return combos.reduce((acc, x) => [...acc, ...x], []) } -// Converts rule, parents and their criteria into a CSS (or path if ignoreOutOfTreeSelector == true) selector +/** + * Converts rule, parents and their criteria into a CSS (or path if ignoreOutOfTreeSelector == true) + * selector. + * + * "path" here refers to "fake" selector that cannot be actually used in UI but is used for internal + * purposes + * + * @param {Object} components - object containing all components definitions + * + * @returns {Function} + * @param {Object} rule - rule in question to convert to CSS selector + * @param {boolean} ignoreOutOfTreeSelector - wthether to ignore aformentioned field in + * component definition and use selector + * @param {boolean} isParent - (mostly) internal argument used when recursing + * + * @returns {String} CSS selector (or path) + */ export const genericRuleToSelector = components => (rule, ignoreOutOfTreeSelector, isParent) => { if (!rule && !isParent) return null const component = components[rule.component] @@ -79,6 +95,17 @@ export const genericRuleToSelector = components => (rule, ignoreOutOfTreeSelecto return selectors.trim() } +/** + * Check if combination matches + * + * @param {Object} criteria - criteria to match against + * @param {Object} subject - rule/combination to check match + * @param {boolean} strict - strict checking: + * By default every variant and state inherits from "normal" state/variant + * so when checking if combination matches, it WILL match against "normal" + * state/variant. In strict mode inheritance is ignored an "normal" does + * not match + */ export const combinationsMatch = (criteria, subject, strict) => { if (criteria.component !== subject.component) return false @@ -101,6 +128,15 @@ export const combinationsMatch = (criteria, subject, strict) => { return true } +/** + * Search for rule that matches `criteria` in set of rules + * meant to be used in a ruleset.filter() function + * + * @param {Object} criteria - criteria to search for + * @param {boolean} strict - whether search strictly or not (see combinationsMatch) + * + * @return function that returns true/false if subject matches + */ export const findRules = (criteria, strict) => subject => { // If we searching for "general" rules - ignore "specific" ones if (criteria.parent === null && !!subject.parent) return false @@ -125,6 +161,7 @@ export const findRules = (criteria, strict) => subject => { return true } +// Pre-fills 'normal' state/variant if missing export const normalizeCombination = rule => { rule.variant = rule.variant ?? 'normal' rule.state = [...new Set(['normal', ...(rule.state || [])])] diff --git a/src/services/theme_data/theme_data_3.service.js b/src/services/theme_data/theme_data_3.service.js index e802a893..cf58da11 100644 --- a/src/services/theme_data/theme_data_3.service.js +++ b/src/services/theme_data/theme_data_3.service.js @@ -149,11 +149,30 @@ const ruleToSelector = genericRuleToSelector(components) export const getEngineChecksum = () => engineChecksum +/** + * Initializes and compiles the theme according to the ruleset + * + * @param {Object[]} inputRuleset - set of rules to compile theme against. Acts as an override to + * component default rulesets + * @param {string} ultimateBackgroundColor - Color that will be the "final" background for + * calculating contrast ratios and making text automatically accessible. Really used for cases when + * stuff is transparent. + * @param {boolean} debug - print out debug information in console, mostly just performance stuff + * @param {boolean} liteMode - use validInnerComponentsLite instead of validInnerComponents, meant to + * generatate theme previews and such that need to be compiled faster and don't require a lot of other + * components present in "normal" mode + * @param {boolean} onlyNormalState - only use components 'normal' states, meant for generating theme + * previews since states are the biggest factor for compilation time and are completely unnecessary + * when previewing multiple themes at same time + * @param {string} rootComponentName - [UNTESTED] which component to start from, meant for previewing a + * part of the theme (i.e. just the button) for themes 3 editor. + */ export const init = ({ inputRuleset, ultimateBackgroundColor, debug = false, liteMode = false, + onlyNormalState = false, rootComponentName = 'Root' }) => { if (!inputRuleset) throw new Error('Ruleset is null or undefined!') @@ -402,11 +421,16 @@ export const init = ({ const processInnerComponent = (component, parent) => { const combinations = [] const { - validInnerComponents = [], states: originalStates = {}, variants: originalVariants = {} } = component + const validInnerComponents = ( + liteMode + ? (component.validInnerComponentsLite || component.validInnerComponents) + : component.validInnerComponents + ) || [] + // Normalizing states and variants to always include "normal" const states = { normal: '', ...originalStates } const variants = { normal: '', ...originalVariants } @@ -418,22 +442,26 @@ export const init = ({ // Optimization: we only really need combinations without "normal" because all states implicitly have it const permutationStateKeys = Object.keys(states).filter(s => s !== 'normal') - const stateCombinations = [ - ['normal'], - ...getAllPossibleCombinations(permutationStateKeys) - .map(combination => ['normal', ...combination]) - .filter(combo => { - // Optimization: filter out some hard-coded combinations that don't make sense - if (combo.indexOf('disabled') >= 0) { - return !( - combo.indexOf('hover') >= 0 || - combo.indexOf('focused') >= 0 || - combo.indexOf('pressed') >= 0 - ) - } - return true - }) - ] + const stateCombinations = onlyNormalState + ? [ + ['normal'] + ] + : [ + ['normal'], + ...getAllPossibleCombinations(permutationStateKeys) + .map(combination => ['normal', ...combination]) + .filter(combo => { + // Optimization: filter out some hard-coded combinations that don't make sense + if (combo.indexOf('disabled') >= 0) { + return !( + combo.indexOf('hover') >= 0 || + combo.indexOf('focused') >= 0 || + combo.indexOf('pressed') >= 0 + ) + } + return true + }) + ] const stateVariantCombination = Object.keys(variants).map(variant => { return stateCombinations.map(state => ({ variant, state })) @@ -460,7 +488,9 @@ export const init = ({ const t0 = performance.now() const combinations = processInnerComponent(components[rootComponentName] ?? components.Root) const t1 = performance.now() - console.debug('Tree traveral took ' + (t1 - t0) + ' ms') + if (debug) { + console.debug('Tree traveral took ' + (t1 - t0) + ' ms') + } const result = combinations.map((combination) => { if (combination.lazy) { @@ -470,7 +500,9 @@ export const init = ({ } }).filter(x => x) const t2 = performance.now() - console.debug('Eager processing took ' + (t2 - t1) + ' ms') + if (debug) { + console.debug('Eager processing took ' + (t2 - t1) + ' ms') + } return { lazy: result.filter(x => typeof x === 'function'), -- cgit v1.2.3-70-g09d2