Page MenuHomePhorge

No OneTemporary

Size
53 KB
Referenced Files
None
Subscribers
None
diff --git a/src/components/component_preview/component_preview.js b/src/components/component_preview/component_preview.js
index d87e0f9ea8..c0a2aad72d 100644
--- a/src/components/component_preview/component_preview.js
+++ b/src/components/component_preview/component_preview.js
@@ -1,83 +1,82 @@
import Checkbox from 'src/components/checkbox/checkbox.vue'
import ColorInput from 'src/components/color_input/color_input.vue'
import genRandomSeed from 'src/services/random_seed/random_seed.service.js'
+import { createStyleSheet, adoptStyleSheets } from 'src/services/style_setter/style_setter.js'
export default {
components: {
Checkbox,
ColorInput
},
props: [
'shadow',
'shadowControl',
'previewClass',
'previewStyle',
'previewCss',
'disabled',
'invalid',
'noColorControl'
],
emits: ['update:shadow'],
data () {
return {
colorOverride: undefined,
lightGrid: false,
zoom: 100,
randomSeed: genRandomSeed()
}
},
mounted () {
this.update()
},
computed: {
hideControls () {
return typeof this.shadow === 'string'
}
},
watch: {
previewCss () {
this.update()
},
previewStyle () {
this.update()
},
zoom () {
this.update()
}
},
methods: {
updateProperty (axis, value) {
this.$emit('update:shadow', { axis, value: Number(value) })
},
update () {
- const styleEl = document.getElementById('component-style-holder')
- const styleSheet = styleEl.sheet
+ const sheet = createStyleSheet('style-component-preview')
- for (let i = styleSheet.cssRules.length - 1; i >= 0; --i) {
- styleSheet.deleteRule(i)
- }
-
- const result = []
+ sheet.clear()
+ const result = [this.previewCss]
if (this.colorOverride) result.push(`--background: ${this.colorOverride}`)
const styleRule = [
'#component-preview-', this.randomSeed, ' {\n',
'.preview-block {\n',
`zoom: ${this.zoom / 100};`,
this.previewStyle,
'\n}',
'\n}'
].join('')
- styleSheet.insertRule(styleRule)
- styleSheet.insertRule([
+ sheet.addRule(styleRule)
+ sheet.addRule([
'#component-preview-', this.randomSeed, ' {\n',
- this.previewCss,
...result,
'\n}'
- ].join(''), 'index-max')
+ ].join(''))
+
+ sheet.ready = true
+ adoptStyleSheets()
}
}
}
diff --git a/src/components/settings_modal/tabs/appearance_tab.js b/src/components/settings_modal/tabs/appearance_tab.js
index 9067ac3007..e0f446a0e1 100644
--- a/src/components/settings_modal/tabs/appearance_tab.js
+++ b/src/components/settings_modal/tabs/appearance_tab.js
@@ -1,427 +1,429 @@
import BooleanSetting from '../helpers/boolean_setting.vue'
import ChoiceSetting from '../helpers/choice_setting.vue'
import IntegerSetting from '../helpers/integer_setting.vue'
import FloatSetting from '../helpers/float_setting.vue'
import UnitSetting from '../helpers/unit_setting.vue'
import { defaultHorizontalUnits } from '../helpers/unit_setting.js'
import PaletteEditor from 'src/components/palette_editor/palette_editor.vue'
import Preview from './theme_tab/theme_preview.vue'
import FontControl from 'src/components/font_control/font_control.vue'
import { newImporter } from 'src/services/export_import/export_import.js'
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 { deserialize } from 'src/services/theme_data/iss_deserializer.js'
+import { createStyleSheet, adoptStyleSheets } from 'src/services/style_setter/style_setter.js'
import SharedComputedObject from '../helpers/shared_computed_object.js'
import ProfileSettingIndicator from '../helpers/profile_setting_indicator.vue'
import { mapActions } from 'pinia'
import { useInterfaceStore, normalizeThemeData } from 'src/stores/interface'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faGlobe
} from '@fortawesome/free-solid-svg-icons'
library.add(
faGlobe
)
const AppearanceTab = {
data () {
return {
availableThemesV3: [],
availableThemesV2: [],
bundledPalettes: [],
compilationCache: {},
fileImporter: newImporter({
accept: '.json, .iss',
validator: this.importValidator,
onImport: this.onImport,
parser: this.importParser,
onImportFailure: this.onImportFailure
}),
palettesKeys: [
'bg',
'fg',
'link',
'text',
'cRed',
'cGreen',
'cBlue',
'cOrange'
],
userPalette: {},
intersectionObserver: null,
thirdColumnModeOptions: ['none', 'notifications', 'postform'].map(mode => ({
key: mode,
value: mode,
label: this.$t(`settings.third_column_mode_${mode}`)
})),
forcedRoundnessOptions: ['disabled', 'sharp', 'nonsharp', 'round'].map((mode, i) => ({
key: mode,
value: i - 1,
label: this.$t(`settings.style.themes3.hacks.forced_roundness_mode_${mode}`)
})),
underlayOverrideModes: ['none', 'opaque', 'transparent'].map((mode) => ({
key: mode,
value: mode,
label: this.$t(`settings.style.themes3.hacks.underlay_override_mode_${mode}`)
}))
}
},
components: {
BooleanSetting,
ChoiceSetting,
IntegerSetting,
FloatSetting,
UnitSetting,
ProfileSettingIndicator,
FontControl,
Preview,
PaletteEditor
},
mounted () {
useInterfaceStore().getThemeData()
const updateIndex = (resource) => {
const capitalizedResource = resource[0].toUpperCase() + resource.slice(1)
const currentIndex = this.$store.state.instance[`${resource}sIndex`]
let promise
if (currentIndex) {
promise = Promise.resolve(currentIndex)
} else {
promise = useInterfaceStore()[`fetch${capitalizedResource}sIndex`]()
}
return promise.then(index => {
return Object
.entries(index)
.map(([k, func]) => [k, func()])
})
}
updateIndex('style').then(styles => {
styles.forEach(([key, stylePromise]) => stylePromise.then(data => {
const meta = data.find(x => x.component === '@meta')
this.availableThemesV3.push({ key, data, name: meta.directives.name, version: 'v3' })
}))
})
updateIndex('theme').then(themes => {
themes.forEach(([key, themePromise]) => themePromise.then(data => {
if (!data) {
console.warn(`Theme with key ${key} is empty or malformed`)
} else if (Array.isArray(data)) {
console.warn(`Theme with key ${key} is a v1 theme and should be moved to static/palettes/index.json`)
} else if (!data.source && !data.theme) {
console.warn(`Theme with key ${key} is malformed`)
} else {
this.availableThemesV2.push({ key, data, name: data.name, version: 'v2' })
}
}))
})
this.userPalette = useInterfaceStore().paletteDataUsed || {}
updateIndex('palette').then(bundledPalettes => {
bundledPalettes.forEach(([key, palettePromise]) => palettePromise.then(v => {
let palette
if (Array.isArray(v)) {
const [
name,
bg,
fg,
text,
link,
cRed = '#FF0000',
cGreen = '#00FF00',
cBlue = '#0000FF',
cOrange = '#E3FF00'
] = v
palette = { key, name, bg, fg, text, link, cRed, cBlue, cGreen, cOrange }
} else {
palette = { key, ...v }
}
if (!palette.key.startsWith('style.')) {
this.bundledPalettes.push(palette)
}
}))
})
this.previewTheme('stock', 'v3')
if (window.IntersectionObserver) {
this.intersectionObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(({ target, isIntersecting }) => {
if (!isIntersecting) return
const theme = this.availableStyles.find(x => x.key === target.dataset.themeKey)
this.$nextTick(() => {
if (theme) this.previewTheme(theme.key, theme.version, theme.data)
})
observer.unobserve(target)
})
}, {
root: this.$refs.themeList
})
} else {
this.availableStyles.forEach(theme => this.previewTheme(theme.key, theme.version, theme.data))
}
},
updated () {
this.$nextTick(() => {
this.$refs.themeList.querySelectorAll('.theme-preview').forEach(node => {
this.intersectionObserver.observe(node)
})
})
},
watch: {
paletteDataUsed () {
this.userPalette = this.paletteDataUsed || {}
}
},
computed: {
switchInProgress () {
return useInterfaceStore().themeChangeInProgress
},
paletteDataUsed () {
return useInterfaceStore().paletteDataUsed
},
availableStyles () {
return [
...this.availableThemesV3,
...this.availableThemesV2
]
},
availablePalettes () {
return [
...this.bundledPalettes,
...this.stylePalettes
]
},
stylePalettes () {
const ruleset = useInterfaceStore().styleDataUsed || []
if (!ruleset && ruleset.length === 0) return
const meta = ruleset.find(x => x.component === '@meta')
const result = ruleset.filter(x => x.component.startsWith('@palette'))
.map(x => {
const { variant, directives } = x
const {
bg,
fg,
text,
link,
accent,
cRed,
cBlue,
cGreen,
cOrange,
wallpaper
} = directives
const result = {
name: `${meta.directives.name || this.$t('settings.style.themes3.palette.imported')}: ${variant}`,
key: `style.${variant.toLowerCase().replace(/ /g, '_')}`,
bg,
fg,
text,
link,
accent,
cRed,
cBlue,
cGreen,
cOrange,
wallpaper
}
return Object.fromEntries(Object.entries(result).filter(([, v]) => v))
})
return result
},
noIntersectionObserver () {
return !window.IntersectionObserver
},
horizontalUnits () {
return defaultHorizontalUnits
},
fontsOverride () {
return this.$store.getters.mergedConfig.fontsOverride
},
columns () {
const mode = this.$store.getters.mergedConfig.thirdColumnMode
const notif = mode === 'none' ? [] : ['notifs']
if (this.$store.getters.mergedConfig.sidebarRight || mode === 'postform') {
return [...notif, 'content', 'sidebar']
} else {
return ['sidebar', 'content', ...notif]
}
},
instanceWallpaperUsed () {
return this.$store.state.instance.background &&
!this.$store.state.users.currentUser.background_image
},
language: {
get: function () { return this.$store.getters.mergedConfig.interfaceLanguage },
set: function (val) {
this.$store.dispatch('setOption', { name: 'interfaceLanguage', value: val })
}
},
customThemeVersion () {
const { themeVersion } = useInterfaceStore()
return themeVersion
},
isCustomThemeUsed () {
const { customTheme, customThemeSource } = this.mergedConfig
return customTheme != null || customThemeSource != null
},
isCustomStyleUsed () {
const { styleCustomData } = this.mergedConfig
return styleCustomData != null
},
...SharedComputedObject()
},
methods: {
updateFont (key, value) {
this.$store.dispatch('setOption', {
name: 'theme3hacks',
value: {
...this.mergedConfig.theme3hacks,
fonts: {
...this.mergedConfig.theme3hacks.fonts,
[key]: value
}
}
})
},
importFile () {
this.fileImporter.importData()
},
importParser (file, filename) {
if (filename.endsWith('.json')) {
return JSON.parse(file)
} else if (filename.endsWith('.iss')) {
return deserialize(file)
}
},
onImport (parsed, filename) {
if (filename.endsWith('.json')) {
useInterfaceStore().setThemeCustom(parsed.source || parsed.theme)
} else if (filename.endsWith('.iss')) {
useInterfaceStore().setStyleCustom(parsed)
}
},
onImportFailure (result) {
console.error('Failure importing theme:', result)
useInterfaceStore().pushGlobalNotice({ messageKey: 'settings.invalid_theme_imported', level: 'error' })
},
importValidator (parsed, filename) {
if (filename.endsWith('.json')) {
const version = parsed._pleroma_theme_version
return version >= 1 || version <= 2
} else if (filename.endsWith('.iss')) {
if (!Array.isArray(parsed)) return false
if (parsed.length < 1) return false
if (parsed.find(x => x.component === '@meta') == null) return false
return true
}
},
isThemeActive (key) {
return key === (this.mergedConfig.theme || this.$store.state.instance.theme)
},
isStyleActive (key) {
return key === (this.mergedConfig.style || this.$store.state.instance.style)
},
isPaletteActive (key) {
return key === (this.mergedConfig.palette || this.$store.state.instance.palette)
},
...mapActions(useInterfaceStore, [
'setStyle',
'setTheme'
]),
setPalette (name, data) {
useInterfaceStore().setPalette(name)
this.userPalette = data
},
setPaletteCustom (data) {
useInterfaceStore().setPaletteCustom(data)
this.userPalette = data
},
resetTheming () {
useInterfaceStore().setStyle('stock')
},
previewTheme (key, version, input) {
let theme3
if (this.compilationCache[key]) {
theme3 = this.compilationCache[key]
} else if (input) {
if (version === 'v2') {
const style = normalizeThemeData(input)
const theme2 = convertTheme2To3(style)
theme3 = init({
inputRuleset: theme2,
ultimateBackgroundColor: '#000000',
liteMode: true,
debug: true,
onlyNormalState: true
})
} else if (version === 'v3') {
const palette = input.find(x => x.component === '@palette')
let paletteRule
if (palette) {
const { directives } = palette
directives.link = directives.link || directives.accent
directives.accent = directives.accent || directives.link
paletteRule = {
component: 'Root',
directives: Object.fromEntries(
Object
.entries(directives)
.filter(([k]) => k && k !== 'name')
.map(([k, v]) => ['--' + k, 'color | ' + v])
)
}
} else {
paletteRule = null
}
theme3 = init({
inputRuleset: [...input, paletteRule].filter(x => x),
ultimateBackgroundColor: '#000000',
liteMode: true,
onlyNormalState: true
})
}
} else {
theme3 = init({
inputRuleset: [],
ultimateBackgroundColor: '#000000',
liteMode: true,
onlyNormalState: true
})
}
if (!this.compilationCache[key]) {
this.compilationCache[key] = theme3
}
- const styleEl = document.getElementById('theme-preview-holder')
- const styleSheet = styleEl.sheet
- styleSheet.insertRule([
+
+ const sheet = createStyleSheet('appearance-tab-previews')
+ sheet.addRule([
'#theme-preview-',
key,
' {\n',
getCssRules(theme3.eager).join('\n'),
'\n}'
- ].join(''), 'index-max')
-
+ ].join(''))
+ sheet.ready = true
+ adoptStyleSheets()
}
}
}
export default AppearanceTab
diff --git a/src/components/settings_modal/tabs/style_tab/style_tab.js b/src/components/settings_modal/tabs/style_tab/style_tab.js
index fcbab2c47c..f92579bb2b 100644
--- a/src/components/settings_modal/tabs/style_tab/style_tab.js
+++ b/src/components/settings_modal/tabs/style_tab/style_tab.js
@@ -1,855 +1,846 @@
-import { ref, reactive, computed, watch, watchEffect, provide, getCurrentInstance } from 'vue'
+import { ref, reactive, computed, watch, provide, getCurrentInstance } from 'vue'
import { useInterfaceStore } from 'src/stores/interface'
import { get, set, unset, throttle } from 'lodash'
import Select from 'src/components/select/select.vue'
import SelectMotion from 'src/components/select/select_motion.vue'
import Checkbox from 'src/components/checkbox/checkbox.vue'
import ComponentPreview from 'src/components/component_preview/component_preview.vue'
import StringSetting from '../../helpers/string_setting.vue'
import ShadowControl from 'src/components/shadow_control/shadow_control.vue'
import ColorInput from 'src/components/color_input/color_input.vue'
import PaletteEditor from 'src/components/palette_editor/palette_editor.vue'
import OpacityInput from 'src/components/opacity_input/opacity_input.vue'
import RoundnessInput from 'src/components/roundness_input/roundness_input.vue'
import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'
import Tooltip from 'src/components/tooltip/tooltip.vue'
import ContrastRatio from 'src/components/contrast_ratio/contrast_ratio.vue'
import Preview from '../theme_tab/theme_preview.vue'
import VirtualDirectivesTab from './virtual_directives_tab.vue'
+import { createStyleSheet, adoptStyleSheets } from 'src/services/style_setter/style_setter.js'
import { init, findColor } from 'src/services/theme_data/theme_data_3.service.js'
import { getCssRules } from 'src/services/theme_data/css_utils.js'
import { serialize } from 'src/services/theme_data/iss_serializer.js'
import { deserializeShadow, deserialize } from 'src/services/theme_data/iss_deserializer.js'
import {
rgb2hex,
hex2rgb,
getContrastRatio
} from 'src/services/color_convert/color_convert.js'
import {
newImporter,
newExporter
} from 'src/services/export_import/export_import.js'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faFloppyDisk,
faFolderOpen,
faFile,
faArrowsRotate,
faCheck
} from '@fortawesome/free-solid-svg-icons'
// helper for debugging
// eslint-disable-next-line no-unused-vars
const toValue = (x) => JSON.parse(JSON.stringify(x === undefined ? 'null' : x))
// helper to make states comparable
const normalizeStates = (states) => ['normal', ...(states?.filter(x => x !== 'normal') || [])].join(':')
library.add(
faFile,
faFloppyDisk,
faFolderOpen,
faArrowsRotate,
faCheck
)
export default {
components: {
Select,
SelectMotion,
Checkbox,
Tooltip,
StringSetting,
ComponentPreview,
TabSwitcher,
ShadowControl,
ColorInput,
PaletteEditor,
OpacityInput,
RoundnessInput,
ContrastRatio,
Preview,
VirtualDirectivesTab
},
setup () {
const exports = {}
const interfaceStore = useInterfaceStore()
// All rules that are made by editor
const allEditedRules = ref(interfaceStore.styleDataUsed || {})
const styleDataUsed = computed(() => interfaceStore.styleDataUsed)
watch([styleDataUsed], () => {
onImport(interfaceStore.styleDataUsed)
}, { once: true })
exports.isActive = computed(() => {
const tabSwitcher = getCurrentInstance().parent.ctx
return tabSwitcher ? tabSwitcher.isActive('style') : false
})
// ## Meta stuff
exports.name = ref('')
exports.author = ref('')
exports.license = ref('')
exports.website = ref('')
const metaOut = computed(() => {
return [
'@meta {',
` name: ${exports.name.value};`,
` author: ${exports.author.value};`,
` license: ${exports.license.value};`,
` website: ${exports.website.value};`,
'}'
].join('\n')
})
const metaRule = computed(() => ({
component: '@meta',
directives: {
name: exports.name.value,
author: exports.author.value,
license: exports.license.value,
website: exports.website.value
}
}))
// ## Palette stuff
const palettes = reactive([
{
name: 'default',
bg: '#121a24',
fg: '#182230',
text: '#b9b9ba',
link: '#d8a070',
accent: '#d8a070',
cRed: '#FF0000',
cBlue: '#0095ff',
cGreen: '#0fa00f',
cOrange: '#ffa500'
},
{
name: 'light',
bg: '#f2f6f9',
fg: '#d6dfed',
text: '#304055',
underlay: '#5d6086',
accent: '#f55b1b',
cBlue: '#0095ff',
cRed: '#d31014',
cGreen: '#0fa00f',
cOrange: '#ffa500',
border: '#d8e6f9'
}
])
exports.palettes = palettes
// This is kinda dumb but you cannot "replace" reactive() object
// and so v-model simply fails when you try to chage (increase only?)
// length of the array. Since linter complains about mutating modelValue
// inside SelectMotion, the next best thing is to just wipe existing array
// and replace it with new one.
const onPalettesUpdate = (e) => {
palettes.splice(0, palettes.length)
palettes.push(...e)
}
exports.onPalettesUpdate = onPalettesUpdate
const selectedPaletteId = ref(0)
const selectedPalette = computed({
get () {
return palettes[selectedPaletteId.value]
},
set (newPalette) {
palettes[selectedPaletteId.value] = newPalette
}
})
exports.selectedPaletteId = selectedPaletteId
exports.selectedPalette = selectedPalette
provide('selectedPalette', selectedPalette)
watch([selectedPalette], () => updateOverallPreview())
exports.getNewPalette = () => ({
name: 'new palette',
bg: '#121a24',
fg: '#182230',
text: '#b9b9ba',
link: '#d8a070',
accent: '#d8a070',
cRed: '#FF0000',
cBlue: '#0095ff',
cGreen: '#0fa00f',
cOrange: '#ffa500'
})
// Raw format
const palettesRule = computed(() => {
return palettes.map(palette => {
const { name, ...rest } = palette
return {
component: '@palette',
variant: name,
directives: Object
.entries(rest)
.filter(([k, v]) => v && k)
.reduce((acc, [k, v]) => ({ ...acc, [k]: v }), {})
}
})
})
// Text format
const palettesOut = computed(() => {
return palettes.map(({ name, ...palette }) => {
const entries = Object
.entries(palette)
.filter(([k, v]) => v && k)
.map(([slot, data]) => ` ${slot}: ${data};`)
.join('\n')
return `@palette.${name} {\n${entries}\n}`
}).join('\n\n')
})
// ## Components stuff
// Getting existing components
const componentsContext = import.meta.glob(
['/src/**/*.style.js', '/src/**/*.style.json'],
{ eager: true }
)
const componentKeysAll = Object.keys(componentsContext)
const componentsMap = new Map(
componentKeysAll
.map(
key => [key, componentsContext[key].default]
).filter(([, component]) => !component.virtual && !component.notEditable)
)
exports.componentsMap = componentsMap
const componentKeys = [...componentsMap.keys()]
exports.componentKeys = componentKeys
// Component list and selection
const selectedComponentKey = ref(componentsMap.keys().next().value)
exports.selectedComponentKey = selectedComponentKey
const selectedComponent = computed(() => componentsMap.get(selectedComponentKey.value))
const selectedComponentName = computed(() => selectedComponent.value.name)
// Selection basis
exports.selectedComponentVariants = computed(() => {
return Object.keys({ normal: null, ...(selectedComponent.value.variants || {}) })
})
exports.selectedComponentStates = computed(() => {
const all = Object.keys({ normal: null, ...(selectedComponent.value.states || {}) })
return all.filter(x => x !== 'normal')
})
// selection
const selectedVariant = ref('normal')
exports.selectedVariant = selectedVariant
const selectedState = reactive(new Set())
exports.selectedState = selectedState
exports.updateSelectedStates = (state, v) => {
if (v) {
selectedState.add(state)
} else {
selectedState.delete(state)
}
}
// Reset variant and state on component change
const updateSelectedComponent = () => {
selectedVariant.value = 'normal'
selectedState.clear()
}
watch(
selectedComponentName,
updateSelectedComponent
)
// ### Rules stuff aka meat and potatoes
// The native structure of separate rules and the child -> parent
// relation isn't very convenient for editor, we replace the array
// and child -> parent structure with map and parent -> child structure
const rulesToEditorFriendly = (rules, root = {}) => rules.reduce((acc, rule) => {
const { parent: rParent, component: rComponent } = rule
const parent = rParent ?? rule
const hasChildren = !!rParent
const child = hasChildren ? rule : null
const {
component: pComponent,
variant: pVariant = 'normal',
state: pState = [] // no relation to Intel CPUs whatsoever
} = parent
const pPath = `${hasChildren ? pComponent : rComponent}.${pVariant}.${normalizeStates(pState)}`
let output = get(acc, pPath)
if (!output) {
set(acc, pPath, {})
output = get(acc, pPath)
}
if (hasChildren) {
output._children = output._children ?? {}
const {
component: cComponent,
variant: cVariant = 'normal',
state: cState = [],
directives
} = child
const cPath = `${cComponent}.${cVariant}.${normalizeStates(cState)}`
set(output._children, cPath, { directives })
} else {
output.directives = parent.directives
}
return acc
}, root)
const editorFriendlyFallbackStructure = computed(() => {
const root = {}
componentKeys.forEach((componentKey) => {
const componentValue = componentsMap.get(componentKey)
const { defaultRules, name } = componentValue
rulesToEditorFriendly(
defaultRules.map((rule) => ({ ...rule, component: name })),
root
)
})
return root
})
// Checking whether component can support some "directives" which
// are actually virtual subcomponents, i.e. Text, Link etc
exports.componentHas = (subComponent) => {
return !!selectedComponent.value.validInnerComponents?.find(x => x === subComponent)
}
// Path for lodash's get and set
const getPath = (component, directive) => {
const pathSuffix = component ? `._children.${component}.normal.normal` : ''
const path = `${selectedComponentName.value}.${selectedVariant.value}.${normalizeStates([...selectedState])}${pathSuffix}.directives.${directive}`
return path
}
// Templates for directives
const isElementPresent = (component, directive, defaultValue = '') => computed({
get () {
return get(allEditedRules.value, getPath(component, directive)) != null
},
set (value) {
if (value) {
const fallback = get(
editorFriendlyFallbackStructure.value,
getPath(component, directive)
)
set(allEditedRules.value, getPath(component, directive), fallback ?? defaultValue)
} else {
unset(allEditedRules.value, getPath(component, directive))
}
exports.updateOverallPreview()
}
})
const getEditedElement = (component, directive, postProcess = x => x) => computed({
get () {
let usedRule
const fallback = editorFriendlyFallbackStructure.value
const real = allEditedRules.value
const path = getPath(component, directive)
usedRule = get(real, path) // get real
if (usedRule === '') {
return usedRule
}
if (!usedRule) {
usedRule = get(fallback, path)
}
return postProcess(usedRule)
},
set (value) {
if (value != null) {
set(allEditedRules.value, getPath(component, directive), value)
} else {
unset(allEditedRules.value, getPath(component, directive))
}
exports.updateOverallPreview()
}
})
// All the editable stuff for the component
exports.editedBackgroundColor = getEditedElement(null, 'background')
exports.isBackgroundColorPresent = isElementPresent(null, 'background', '#FFFFFF')
exports.editedOpacity = getEditedElement(null, 'opacity')
exports.isOpacityPresent = isElementPresent(null, 'opacity', 1)
exports.editedRoundness = getEditedElement(null, 'roundness')
exports.isRoundnessPresent = isElementPresent(null, 'roundness', '0')
exports.editedTextColor = getEditedElement('Text', 'textColor')
exports.isTextColorPresent = isElementPresent('Text', 'textColor', '#000000')
exports.editedTextAuto = getEditedElement('Text', 'textAuto')
exports.isTextAutoPresent = isElementPresent('Text', 'textAuto', '#000000')
exports.editedLinkColor = getEditedElement('Link', 'textColor')
exports.isLinkColorPresent = isElementPresent('Link', 'textColor', '#000080')
exports.editedIconColor = getEditedElement('Icon', 'textColor')
exports.isIconColorPresent = isElementPresent('Icon', 'textColor', '#909090')
exports.editedBorderColor = getEditedElement('Border', 'textColor')
exports.isBorderColorPresent = isElementPresent('Border', 'textColor', '#909090')
const getContrast = (bg, text) => {
try {
const bgRgb = hex2rgb(bg)
const textRgb = hex2rgb(text)
const ratio = getContrastRatio(bgRgb, textRgb)
return {
// TODO this ideally should be part of <ContractRatio />
ratio,
text: ratio.toPrecision(3) + ':1',
// AA level, AAA level
aa: ratio >= 4.5,
aaa: ratio >= 7,
// same but for 18pt+ texts
laa: ratio >= 3,
laaa: ratio >= 4.5
}
} catch (e) {
console.warn('Failure computing contrast', e)
return { error: e }
}
}
const normalizeShadows = (shadows) => {
return shadows?.map(shadow => {
if (typeof shadow === 'object') {
return shadow
}
if (typeof shadow === 'string') {
try {
return deserializeShadow(shadow)
} catch (e) {
console.warn(e)
return shadow
}
}
return null
})
}
provide('normalizeShadows', normalizeShadows)
// Shadow is partially edited outside the ShadowControl
// for better space utilization
const editedShadow = getEditedElement(null, 'shadow', normalizeShadows)
exports.editedShadow = editedShadow
const editedSubShadowId = ref(null)
exports.editedSubShadowId = editedSubShadowId
const editedSubShadow = computed(() => {
if (editedShadow.value == null || editedSubShadowId.value == null) return null
return editedShadow.value[editedSubShadowId.value]
})
exports.editedSubShadow = editedSubShadow
exports.isShadowPresent = isElementPresent(null, 'shadow', [])
exports.onSubShadow = (id) => {
if (id != null) {
editedSubShadowId.value = id
} else {
editedSubShadow.value = null
}
}
exports.updateSubShadow = (axis, value) => {
if (!editedSubShadow.value || editedSubShadowId.value == null) return
const newEditedShadow = [...editedShadow.value]
newEditedShadow[editedSubShadowId.value] = {
...newEditedShadow[editedSubShadowId.value],
[axis]: value
}
editedShadow.value = newEditedShadow
}
exports.isShadowTabOpen = ref(false)
exports.onTabSwitch = (tab) => {
exports.isShadowTabOpen.value = tab === 'shadow'
}
// component preview
exports.editorHintStyle = computed(() => {
const editorHint = selectedComponent.value.editor
const styles = []
if (editorHint && Object.keys(editorHint).length > 0) {
if (editorHint.aspect != null) {
styles.push(`aspect-ratio: ${editorHint.aspect} !important;`)
}
if (editorHint.border != null) {
styles.push(`border-width: ${editorHint.border}px !important;`)
}
}
return styles.join('; ')
})
const editorFriendlyToOriginal = computed(() => {
const resultRules = []
const convert = (component, data = {}, parent) => {
const variants = Object.entries(data || {})
variants.forEach(([variant, variantData]) => {
const states = Object.entries(variantData)
states.forEach(([jointState, stateData]) => {
const state = jointState.split(/:/g)
const result = {
component,
variant,
state,
directives: stateData.directives || {}
}
if (parent) {
result.parent = {
component: parent
}
}
resultRules.push(result)
// Currently we only support single depth for simplicity's sake
if (!parent) {
Object.entries(stateData._children || {}).forEach(([cName, child]) => convert(cName, child, component))
}
})
})
}
[...componentsMap.values()].forEach(({ name }) => {
convert(name, allEditedRules.value[name])
})
return resultRules
})
const allCustomVirtualDirectives = [...componentsMap.values()]
.map(c => {
return c
.defaultRules
.filter(c => c.component === 'Root')
.map(x => Object.entries(x.directives))
.flat()
})
.filter(x => x)
.flat()
.map(([name, value]) => {
const [valType, valVal] = value.split('|')
return {
name: name.substring(2),
valType: valType?.trim(),
value: valVal?.trim()
}
})
const virtualDirectives = ref(allCustomVirtualDirectives)
exports.virtualDirectives = virtualDirectives
exports.updateVirtualDirectives = (value) => {
virtualDirectives.value = value
}
// Raw format
const virtualDirectivesRule = computed(() => ({
component: 'Root',
directives: Object.fromEntries(
virtualDirectives.value.map(vd => [`--${vd.name}`, `${vd.valType} | ${vd.value}`])
)
}))
// Text format
const virtualDirectivesOut = computed(() => {
return [
'Root {',
...virtualDirectives.value
.filter(vd => vd.name && vd.valType && vd.value)
.map(vd => ` --${vd.name}: ${vd.valType} | ${vd.value};`),
'}'
].join('\n')
})
exports.computeColor = (color) => {
let computedColor
try {
computedColor = findColor(color, { dynamicVars: dynamicVars.value, staticVars: staticVars.value })
if (computedColor) {
return rgb2hex(computedColor)
}
} catch (e) {
console.warn(e)
}
return null
}
provide('computeColor', exports.computeColor)
exports.contrast = computed(() => {
return getContrast(
exports.computeColor(previewColors.value.background),
exports.computeColor(previewColors.value.text)
)
})
// ## Export and Import
const styleExporter = newExporter({
filename: () => exports.name.value ?? 'pleroma_theme',
mime: 'text/plain',
extension: 'iss',
getExportedObject: () => exportStyleData.value
})
const onImport = parsed => {
const editorComponents = parsed.filter(x => x.component.startsWith('@'))
const rootComponent = parsed.find(x => x.component === 'Root')
const rules = parsed.filter(x => !x.component.startsWith('@') && x.component !== 'Root')
const metaIn = editorComponents.find(x => x.component === '@meta').directives
const palettesIn = editorComponents.filter(x => x.component === '@palette')
exports.name.value = metaIn.name
exports.license.value = metaIn.license
exports.author.value = metaIn.author
exports.website.value = metaIn.website
const newVirtualDirectives = Object
.entries(rootComponent.directives)
.map(([name, value]) => {
const [valType, valVal] = value.split('|').map(x => x.trim())
return { name: name.substring(2), valType, value: valVal }
})
virtualDirectives.value = newVirtualDirectives
onPalettesUpdate(palettesIn.map(x => ({ name: x.variant, ...x.directives })))
allEditedRules.value = rulesToEditorFriendly(rules)
exports.updateOverallPreview()
}
const styleImporter = newImporter({
accept: '.iss',
parser (string) { return deserialize(string) },
onImportFailure (result) {
console.error('Failure importing style:', result)
useInterfaceStore().pushGlobalNotice({ messageKey: 'settings.invalid_theme_imported', level: 'error' })
},
onImport
})
// Raw format
const exportRules = computed(() => [
metaRule.value,
...palettesRule.value,
virtualDirectivesRule.value,
...editorFriendlyToOriginal.value
])
// Text format
const exportStyleData = computed(() => {
return [
metaOut.value,
palettesOut.value,
virtualDirectivesOut.value,
serialize(editorFriendlyToOriginal.value)
].join('\n\n')
})
exports.clearStyle = () => {
onImport(interfaceStore.styleDataUsed)
}
exports.exportStyle = () => {
styleExporter.exportData()
}
exports.importStyle = () => {
styleImporter.importData()
}
exports.applyStyle = () => {
useInterfaceStore().setStyleCustom(exportRules.value)
}
const overallPreviewRules = ref([])
exports.overallPreviewRules = overallPreviewRules
watch([overallPreviewRules], () => {
let css = null
try {
css = getCssRules(overallPreviewRules.value).map(r => r.replace('html', '&'))
} catch (e) {
console.error(e)
return
}
- const styleEl = document.getElementById('editor-overall-holder')
- const styleSheet = styleEl.sheet
+ const sheet = createStyleSheet('style-tab-overall-preview')
- console.log(styleSheet)
- console.log('BEFORE', styleSheet.cssRules)
- for (let i = styleSheet.cssRules.length - 1; i >= 0; --i) {
- styleSheet.deleteRule(i)
- }
-
- styleSheet.insertRule([
- '#edited-style-preview {\n',
- css.join('\n'),
- '\n}'
- ].join(''), 'index-max')
- styleSheet.insertRule([
+ sheet.clear()
+ sheet.addRule([
'#edited-style-preview {\n',
css.join('\n'),
'\n}'
- ].join(''), 'index-max')
- console.log('AFTER', styleSheet.cssRules)
+ ].join(''))
+ sheet.ready = true
+ adoptStyleSheets()
})
- const updateOverallPreview = () => {
+ const updateOverallPreview = throttle(() => {
try {
overallPreviewRules.value = init({
inputRuleset: [
...exportRules.value,
{
component: 'Root',
directives: Object.fromEntries(
Object
.entries(selectedPalette.value)
.filter(([k, v]) => k && v && k !== 'name')
.map(([k, v]) => [`--${k}`, `color | ${v}`])
)
}
],
ultimateBackgroundColor: '#000000',
debug: true
}).eager
} catch (e) {
console.error('Could not compile preview theme', e)
return null
}
- }
+ }, 1000)
//
// Apart from "hover" we can't really show how component looks like in
// certain states, so we have to fake them.
const simulatePseudoSelectors = (css, prefix) => css
.replace(prefix, '.preview-block')
.replace(':active', '.preview-active')
.replace(':hover', '.preview-hover')
.replace(':active', '.preview-active')
.replace(':focus', '.preview-focus')
.replace(':focus-within', '.preview-focus-within')
.replace(':disabled', '.preview-disabled')
const previewRules = computed(() => {
const filtered = overallPreviewRules.value.filter(r => {
const componentMatch = r.component === selectedComponentName.value
const parentComponentMatch = r.parent?.component === selectedComponentName.value
if (!componentMatch && !parentComponentMatch) return false
const rule = parentComponentMatch ? r.parent : r
if (rule.component !== selectedComponentName.value) return false
if (rule.variant !== selectedVariant.value) return false
const ruleState = new Set(rule.state.filter(x => x !== 'normal'))
const differenceA = [...ruleState].filter(x => !selectedState.has(x))
const differenceB = [...selectedState].filter(x => !ruleState.has(x))
return (differenceA.length + differenceB.length) === 0
})
const sorted = [...filtered]
.filter(x => x.component === selectedComponentName.value)
.sort((a, b) => {
const aSelectorLength = a.selector.split(/ /g).length
const bSelectorLength = b.selector.split(/ /g).length
return aSelectorLength - bSelectorLength
})
const prefix = sorted[0].selector
return filtered.filter(x => x.selector.startsWith(prefix))
})
exports.previewClass = computed(() => {
const selectors = []
if (!!selectedComponent.value.variants?.normal || selectedVariant.value !== 'normal') {
selectors.push(selectedComponent.value.variants[selectedVariant.value])
}
if (selectedState.size > 0) {
selectedState.forEach(state => {
const original = selectedComponent.value.states[state]
selectors.push(simulatePseudoSelectors(original))
})
}
return selectors.map(x => x.substring(1)).join('')
})
exports.previewCss = computed(() => {
try {
const prefix = previewRules.value[0].selector
const scoped = getCssRules(previewRules.value).map(x => simulatePseudoSelectors(x, prefix))
return scoped.join('\n')
} catch (e) {
console.error('Invalid ruleset', e)
return null
}
})
const dynamicVars = computed(() => {
return previewRules.value[0].dynamicVars
})
const staticVars = computed(() => {
const rootComponent = overallPreviewRules.value.find(r => {
return r.component === 'Root'
})
const rootDirectivesEntries = Object.entries(rootComponent.directives)
const directives = {}
rootDirectivesEntries
.filter(([k, v]) => k.startsWith('--') && v.startsWith('color | '))
.map(([k, v]) => [k.substring(2), v.substring('color | '.length)])
.forEach(([k, v]) => {
directives[k] = findColor(v, { dynamicVars: {}, staticVars: directives })
})
return directives
})
provide('staticVars', staticVars)
exports.staticVars = staticVars
const previewColors = computed(() => {
const stacked = dynamicVars.value.stacked
const background = typeof stacked === 'string' ? stacked : rgb2hex(stacked)
return {
text: previewRules.value.find(r => r.component === 'Text')?.virtualDirectives['--text'],
link: previewRules.value.find(r => r.component === 'Link')?.virtualDirectives['--link'],
border: previewRules.value.find(r => r.component === 'Border')?.virtualDirectives['--border'],
icon: previewRules.value.find(r => r.component === 'Icon')?.virtualDirectives['--icon'],
background
}
})
exports.previewColors = previewColors
exports.updateOverallPreview = updateOverallPreview
updateOverallPreview()
watch(
[
allEditedRules.value,
palettes,
selectedPalette,
selectedState,
selectedVariant
],
updateOverallPreview
)
return exports
}
}
diff --git a/src/services/style_setter/style_setter.js b/src/services/style_setter/style_setter.js
index 5b696185a9..8e861d5d77 100644
--- a/src/services/style_setter/style_setter.js
+++ b/src/services/style_setter/style_setter.js
@@ -1,300 +1,303 @@
import { init, getEngineChecksum } from '../theme_data/theme_data_3.service.js'
import { getCssRules } from '../theme_data/css_utils.js'
import { defaultState } from 'src/modules/default_config_state.js'
-import { chunk } from 'lodash'
+import { chunk, throttle } from 'lodash'
import localforage from 'localforage'
// On platforms where this is not supported, it will return undefined
// Otherwise it will return an array
const supportsAdoptedStyleSheets = !!document.adoptedStyleSheets
const stylesheets = {}
-const createStyleSheet = (id) => {
+export const createStyleSheet = (id) => {
+ if (stylesheets[id]) return stylesheets[id]
const newStyleSheet = {
rules: [],
ready: false,
+ clear () {
+ this.rules = []
+ },
addRule (rule) {
this.rules.push(
rule
.replace(/backdrop-filter:[^;]+;/g, '') // Remove backdrop-filter
.replace(/var\(--shadowFilter\)[^;]*;/g, '') // Remove shadowFilter references
)
}
}
stylesheets[id] = newStyleSheet
return newStyleSheet
}
-export const adoptStyleSheets = () => {
+export const adoptStyleSheets = throttle(() => {
if (supportsAdoptedStyleSheets) {
document.adoptedStyleSheets = Object
.values(stylesheets)
.filter(x => x.ready)
.map(x => {
const css = new CSSStyleSheet()
x.rules.forEach(r => css.insertRule(r, css.cssRules.length))
return css
})
} else {
const holder = document.getElementById('custom-styles-holder')
Object
.values(stylesheets)
.forEach(sheet => {
sheet.rules.forEach(r => holder.sheet.insertRule(r))
})
-
}
// Some older browsers do not support document.adoptedStyleSheets.
// In this case, we use the <style> elements.
// Since the <style> elements we need are already in the DOM, there
// is nothing to do here.
-}
+}, 500)
const EAGER_STYLE_ID = 'pleroma-eager-styles'
const LAZY_STYLE_ID = 'pleroma-lazy-styles'
export const generateTheme = (inputRuleset, callbacks, debug) => {
const {
onNewRule = () => {},
onLazyFinished = () => {},
onEagerFinished = () => {}
} = callbacks
const themes3 = init({
inputRuleset,
debug
})
getCssRules(themes3.eager, debug).forEach(rule => {
// Hacks to support multiple selectors on same component
onNewRule(rule, false)
})
onEagerFinished()
// Optimization - instead of processing all lazy rules in one go, process them in small chunks
// so that UI can do other things and be somewhat responsive while less important rules are being
// processed
let counter = 0
const chunks = chunk(themes3.lazy, 200)
// let t0 = performance.now()
const processChunk = () => {
const chunk = chunks[counter]
Promise.all(chunk.map(x => x())).then(result => {
getCssRules(result.filter(x => x), debug).forEach(rule => {
onNewRule(rule, true)
})
// const t1 = performance.now()
// console.debug('Chunk ' + counter + ' took ' + (t1 - t0) + 'ms')
// t0 = t1
counter += 1
if (counter < chunks.length) {
setTimeout(processChunk, 0)
} else {
onLazyFinished()
}
})
}
return { lazyProcessFunc: processChunk }
}
export const tryLoadCache = async () => {
console.info('Trying to load compiled theme data from cache')
const cache = await localforage.getItem('pleromafe-theme-cache')
if (!cache) return null
try {
if (cache.engineChecksum === getEngineChecksum()) {
const eagerStyles = createStyleSheet(EAGER_STYLE_ID)
const lazyStyles = createStyleSheet(LAZY_STYLE_ID)
cache.data[0].forEach(rule => eagerStyles.addRule(rule, 'index-max'))
cache.data[1].forEach(rule => lazyStyles.addRule(rule, 'index-max'))
eagerStyles.ready = true
lazyStyles.ready = true
adoptStyleSheets()
console.info(`Loaded theme from cache`)
return true
} else {
console.warn('Engine checksum doesn\'t match, cache not usable, clearing')
localStorage.removeItem('pleroma-fe-theme-cache')
}
} catch (e) {
console.error('Failed to load theme cache:', e)
return false
}
}
export const applyTheme = (
input,
onEagerFinish = () => {},
onFinish = () => {},
debug
) => {
const eagerStyles = createStyleSheet(EAGER_STYLE_ID)
const lazyStyles = createStyleSheet(LAZY_STYLE_ID)
const { lazyProcessFunc } = generateTheme(
input,
{
onNewRule (rule, isLazy) {
if (isLazy) {
lazyStyles.addRule(rule)
} else {
eagerStyles.addRule(rule)
}
},
onEagerFinished () {
eagerStyles.ready = true
adoptStyleSheets()
onEagerFinish()
console.info('Eager part of theme finished, waiting for lazy part to finish to store cache')
},
onLazyFinished () {
eagerStyles.ready = true
adoptStyleSheets()
const cache = { engineChecksum: getEngineChecksum(), data: [eagerStyles.rules, lazyStyles.rules] }
onFinish(cache)
localforage.setItem('pleromafe-theme-cache', cache)
console.info('Theme cache stored')
}
},
debug
)
setTimeout(lazyProcessFunc, 0)
}
const extractStyleConfig = ({
sidebarColumnWidth,
contentColumnWidth,
notifsColumnWidth,
themeEditorMinWidth,
emojiReactionsScale,
emojiSize,
navbarSize,
panelHeaderSize,
textSize,
forcedRoundness
}) => {
const result = {
sidebarColumnWidth,
contentColumnWidth,
notifsColumnWidth,
themeEditorMinWidth: parseInt(themeEditorMinWidth) === 0 ? 'fit-content' : themeEditorMinWidth,
emojiReactionsScale,
emojiSize,
navbarSize,
panelHeaderSize,
textSize
}
switch (forcedRoundness) {
case 'disable':
break
case '0':
result.forcedRoundness = '0'
break
case '1':
result.forcedRoundness = '1px'
break
case '2':
result.forcedRoundness = '0.4rem'
break
default:
}
return result
}
const defaultStyleConfig = extractStyleConfig(defaultState)
export const applyConfig = (input) => {
const config = extractStyleConfig(input)
if (config === defaultStyleConfig) {
return
}
const rules = Object
.entries(config)
.filter(([, v]) => v)
.map(([k, v]) => `--${k}: ${v}`).join(';')
const styleSheet = createStyleSheet('theme-holder')
styleSheet.addRule(`:root { ${rules} }`, 'index-max')
// TODO find a way to make this not apply to theme previews
if (Object.prototype.hasOwnProperty.call(config, 'forcedRoundness')) {
styleSheet.addRule(` *:not(.preview-block) {
--roundness: var(--forcedRoundness) !important;
}`, 'index-max')
}
styleSheet.ready = true
adoptStyleSheets()
}
export const getResourcesIndex = async (url, parser = JSON.parse) => {
const cache = 'no-store'
const customUrl = url.replace(/\.(\w+)$/, '.custom.$1')
let builtin
let custom
const resourceTransform = (resources) => {
return Object
.entries(resources)
.map(([k, v]) => {
if (typeof v === 'object') {
return [k, () => Promise.resolve(v)]
} else if (typeof v === 'string') {
return [
k,
() => window
.fetch(v, { cache })
.then(data => data.text())
.then(text => parser(text))
.catch(e => {
console.error(e)
return null
})
]
} else {
console.error(`Unknown resource format - ${k} is a ${typeof v}`)
return [k, null]
}
})
}
try {
const builtinData = await window.fetch(url, { cache })
const builtinResources = await builtinData.json()
builtin = resourceTransform(builtinResources)
} catch {
builtin = []
console.warn(`Builtin resources at ${url} unavailable`)
}
try {
const customData = await window.fetch(customUrl, { cache })
const customResources = await customData.json()
custom = resourceTransform(customResources)
} catch {
custom = []
console.warn(`Custom resources at ${customUrl} unavailable`)
}
const total = [...custom, ...builtin]
if (total.length === 0) {
return Promise.reject(new Error(`Resource at ${url} and ${customUrl} completely unavailable. Panicking`))
}
return Promise.resolve(Object.fromEntries(total))
}

File Metadata

Mime Type
text/x-diff
Expires
Fri, Oct 9, 9:58 AM (1 d, 20 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1784670
Default Alt Text
(53 KB)

Event Timeline