2024-04-24 15:09:52 +03:00
|
|
|
import { init, getEngineChecksum } from '../theme_data/theme_data_3.service.js'
|
2024-02-19 19:59:38 +02:00
|
|
|
import { getCssRules } from '../theme_data/css_utils.js'
|
2025-01-30 21:56:07 +02:00
|
|
|
import { defaultState } from 'src/modules/default_config_state.js'
|
2024-02-27 00:07:45 +02:00
|
|
|
import { chunk } from 'lodash'
|
2024-11-15 00:27:44 +02:00
|
|
|
import localforage from 'localforage'
|
2020-01-06 22:55:14 +02:00
|
|
|
|
2024-05-31 14:33:44 -04:00
|
|
|
// 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 <style> elements.
|
|
|
|
|
// Since the <style> elements we need are already in the DOM, there
|
|
|
|
|
// is nothing to do here.
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-17 05:04:52 +03:00
|
|
|
export const generateTheme = (inputRuleset, callbacks, debug) => {
|
2024-04-03 22:42:34 +03:00
|
|
|
const {
|
2025-02-04 15:23:21 +02:00
|
|
|
onNewRule = () => {},
|
2024-04-03 22:42:34 +03:00
|
|
|
onLazyFinished = () => {},
|
|
|
|
|
onEagerFinished = () => {}
|
|
|
|
|
} = callbacks
|
|
|
|
|
|
2024-07-04 03:20:26 +03:00
|
|
|
const themes3 = init({
|
2024-07-10 22:49:56 +03:00
|
|
|
inputRuleset,
|
2024-07-04 03:20:26 +03:00
|
|
|
debug
|
|
|
|
|
})
|
2017-11-17 02:17:36 +02:00
|
|
|
|
2024-06-21 23:28:24 +03:00
|
|
|
getCssRules(themes3.eager, debug).forEach(rule => {
|
2024-03-06 20:27:05 +02:00
|
|
|
// Hacks to support multiple selectors on same component
|
2024-10-21 23:10:54 +03:00
|
|
|
onNewRule(rule, false)
|
2024-01-31 17:39:51 +02:00
|
|
|
})
|
2024-04-03 22:42:34 +03:00
|
|
|
onEagerFinished()
|
2024-02-26 21:37:40 +02:00
|
|
|
|
2024-02-27 00:07:45 +02:00
|
|
|
// 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
|
2024-03-04 19:03:29 +02:00
|
|
|
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 => {
|
2024-06-21 23:28:24 +03:00
|
|
|
getCssRules(result.filter(x => x), debug).forEach(rule => {
|
2024-10-21 23:10:54 +03:00
|
|
|
onNewRule(rule, true)
|
2024-02-26 21:37:40 +02:00
|
|
|
})
|
2024-03-04 19:03:29 +02:00
|
|
|
// const t1 = performance.now()
|
|
|
|
|
// console.debug('Chunk ' + counter + ' took ' + (t1 - t0) + 'ms')
|
|
|
|
|
// t0 = t1
|
|
|
|
|
counter += 1
|
|
|
|
|
if (counter < chunks.length) {
|
|
|
|
|
setTimeout(processChunk, 0)
|
2024-04-03 22:42:34 +03:00
|
|
|
} else {
|
|
|
|
|
onLazyFinished()
|
2024-03-04 19:03:29 +02:00
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
2024-04-03 22:42:34 +03:00
|
|
|
|
|
|
|
|
return { lazyProcessFunc: processChunk }
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-15 00:27:44 +02:00
|
|
|
export const tryLoadCache = async () => {
|
|
|
|
|
console.info('Trying to load compiled theme data from cache')
|
2025-02-05 08:07:24 +02:00
|
|
|
const cache = await localforage.getItem('pleromafe-theme-cache')
|
|
|
|
|
if (!cache) return null
|
2024-04-22 23:40:39 +03:00
|
|
|
try {
|
2025-02-05 08:07:24 +02:00
|
|
|
if (cache.engineChecksum === getEngineChecksum()) {
|
|
|
|
|
const eagerStyles = createStyleSheet(EAGER_STYLE_ID)
|
|
|
|
|
const lazyStyles = createStyleSheet(LAZY_STYLE_ID)
|
2024-04-22 23:40:39 +03:00
|
|
|
|
2025-02-05 08:07:24 +02:00
|
|
|
cache.data[0].forEach(rule => eagerStyles.sheet.insertRule(rule, 'index-max'))
|
|
|
|
|
cache.data[1].forEach(rule => lazyStyles.sheet.insertRule(rule, 'index-max'))
|
2024-04-22 23:40:39 +03:00
|
|
|
|
2025-02-05 08:07:24 +02:00
|
|
|
adoptStyleSheets([eagerStyles, lazyStyles])
|
2024-04-22 23:40:39 +03:00
|
|
|
|
2025-02-05 08:07:24 +02:00
|
|
|
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
|
2024-04-22 23:40:39 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-12-18 16:29:38 +02:00
|
|
|
export const applyTheme = (
|
|
|
|
|
input,
|
2025-02-04 15:23:21 +02:00
|
|
|
onEagerFinish = () => {},
|
|
|
|
|
onFinish = () => {},
|
2024-12-18 16:29:38 +02:00
|
|
|
debug
|
|
|
|
|
) => {
|
2024-05-31 14:33:44 -04:00
|
|
|
const eagerStyles = createStyleSheet(EAGER_STYLE_ID)
|
|
|
|
|
const lazyStyles = createStyleSheet(LAZY_STYLE_ID)
|
2024-04-03 22:42:34 +03:00
|
|
|
|
2024-12-03 19:30:35 +02:00
|
|
|
const insertRule = (styles, rule) => {
|
2025-03-09 12:01:02 +04:00
|
|
|
try {
|
|
|
|
|
// Try to use modern syntax first
|
2024-12-03 19:30:35 +02:00
|
|
|
try {
|
|
|
|
|
styles.sheet.insertRule(rule, 'index-max')
|
|
|
|
|
styles.rules.push(rule)
|
2025-03-09 12:01:02 +04:00
|
|
|
} catch {
|
|
|
|
|
// Fallback for older browsers that don't support 'index-max'
|
|
|
|
|
styles.sheet.insertRule(rule, styles.sheet.cssRules.length)
|
|
|
|
|
styles.rules.push(rule)
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.warn('Can\'t insert rule due to lack of support', e, rule)
|
|
|
|
|
|
|
|
|
|
// Try to sanitize the rule for better compatibility
|
|
|
|
|
try {
|
|
|
|
|
// Remove any potentially problematic CSS features
|
|
|
|
|
let sanitizedRule = rule
|
|
|
|
|
.replace(/backdrop-filter:[^;]+;/g, '') // Remove backdrop-filter
|
|
|
|
|
.replace(/var\(--shadowFilter\)[^;]*;/g, '') // Remove shadowFilter references
|
|
|
|
|
|
|
|
|
|
if (sanitizedRule !== rule) {
|
|
|
|
|
styles.sheet.insertRule(sanitizedRule, styles.sheet.cssRules.length)
|
|
|
|
|
styles.rules.push(sanitizedRule)
|
|
|
|
|
}
|
|
|
|
|
} catch (e2) {
|
|
|
|
|
console.error('Failed to insert even sanitized rule', e2)
|
2024-12-03 19:30:35 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-17 05:04:52 +03:00
|
|
|
const { lazyProcessFunc } = generateTheme(
|
2024-04-03 22:42:34 +03:00
|
|
|
input,
|
|
|
|
|
{
|
2024-04-03 22:57:44 +03:00
|
|
|
onNewRule (rule, isLazy) {
|
|
|
|
|
if (isLazy) {
|
2024-12-03 19:30:35 +02:00
|
|
|
insertRule(lazyStyles, rule)
|
2024-04-03 22:57:44 +03:00
|
|
|
} else {
|
2024-12-03 19:30:35 +02:00
|
|
|
insertRule(eagerStyles, rule)
|
2024-04-03 22:57:44 +03:00
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
onEagerFinished () {
|
2024-05-31 14:33:44 -04:00
|
|
|
adoptStyleSheets([eagerStyles])
|
2024-12-12 15:42:03 +02:00
|
|
|
onEagerFinish()
|
2025-02-10 23:10:04 +02:00
|
|
|
console.info('Eager part of theme finished, waiting for lazy part to finish to store cache')
|
2024-04-03 22:57:44 +03:00
|
|
|
},
|
|
|
|
|
onLazyFinished () {
|
2024-05-31 14:33:44 -04:00
|
|
|
adoptStyleSheets([eagerStyles, lazyStyles])
|
2025-02-10 23:02:34 +02:00
|
|
|
const cache = { engineChecksum: getEngineChecksum(), data: [eagerStyles.rules, lazyStyles.rules] }
|
2024-04-22 23:40:39 +03:00
|
|
|
onFinish(cache)
|
2025-02-10 23:10:04 +02:00
|
|
|
localforage.setItem('pleromafe-theme-cache', cache)
|
|
|
|
|
console.info('Theme cache stored')
|
2024-04-03 22:42:34 +03:00
|
|
|
}
|
2024-06-21 23:28:24 +03:00
|
|
|
},
|
|
|
|
|
debug
|
2024-04-03 22:42:34 +03:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
setTimeout(lazyProcessFunc, 0)
|
2017-11-17 17:24:42 +02:00
|
|
|
}
|
|
|
|
|
|
2024-06-13 02:22:47 +03:00
|
|
|
const extractStyleConfig = ({
|
2024-05-22 19:54:19 +03:00
|
|
|
sidebarColumnWidth,
|
|
|
|
|
contentColumnWidth,
|
|
|
|
|
notifsColumnWidth,
|
2025-03-10 23:36:04 -04:00
|
|
|
themeEditorMinWidth,
|
2024-05-22 19:54:19 +03:00
|
|
|
emojiReactionsScale,
|
2024-06-13 02:22:47 +03:00
|
|
|
emojiSize,
|
|
|
|
|
navbarSize,
|
|
|
|
|
panelHeaderSize,
|
2024-06-21 22:46:01 +03:00
|
|
|
textSize,
|
|
|
|
|
forcedRoundness
|
|
|
|
|
}) => {
|
|
|
|
|
const result = {
|
|
|
|
|
sidebarColumnWidth,
|
|
|
|
|
contentColumnWidth,
|
|
|
|
|
notifsColumnWidth,
|
2025-03-10 23:36:04 -04:00
|
|
|
themeEditorMinWidth: parseInt(themeEditorMinWidth) === 0 ? 'fit-content' : themeEditorMinWidth,
|
2024-06-21 22:46:01 +03:00
|
|
|
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
|
|
|
|
|
}
|
2022-06-05 17:10:44 +03:00
|
|
|
|
2024-06-13 02:22:47 +03:00
|
|
|
const defaultStyleConfig = extractStyleConfig(defaultState)
|
2022-06-05 17:10:44 +03:00
|
|
|
|
2025-02-04 15:23:21 +02:00
|
|
|
export const applyConfig = (input) => {
|
2024-06-13 02:22:47 +03:00
|
|
|
const config = extractStyleConfig(input)
|
2022-06-05 17:10:44 +03:00
|
|
|
|
2024-06-13 02:22:47 +03:00
|
|
|
if (config === defaultStyleConfig) {
|
2022-06-05 17:10:44 +03:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const rules = Object
|
2024-06-13 02:22:47 +03:00
|
|
|
.entries(config)
|
2025-02-04 15:23:21 +02:00
|
|
|
.filter(([, v]) => v)
|
2022-06-05 17:10:44 +03:00
|
|
|
.map(([k, v]) => `--${k}: ${v}`).join(';')
|
|
|
|
|
|
2025-06-25 22:06:57 +03:00
|
|
|
const styleEl = document.getElementById('theme-holder')
|
2022-06-05 17:10:44 +03:00
|
|
|
const styleSheet = styleEl.sheet
|
|
|
|
|
|
|
|
|
|
styleSheet.insertRule(`:root { ${rules} }`, 'index-max')
|
2024-05-22 19:54:19 +03:00
|
|
|
|
2024-11-14 17:26:14 +02:00
|
|
|
// TODO find a way to make this not apply to theme previews
|
2024-06-21 22:46:01 +03:00
|
|
|
if (Object.prototype.hasOwnProperty.call(config, 'forcedRoundness')) {
|
2024-11-14 17:26:14 +02:00
|
|
|
styleSheet.insertRule(` *:not(.preview-block) {
|
2024-06-21 22:46:01 +03:00
|
|
|
--roundness: var(--forcedRoundness) !important;
|
|
|
|
|
}`, 'index-max')
|
|
|
|
|
}
|
2022-06-05 17:10:44 +03:00
|
|
|
}
|
|
|
|
|
|
2024-11-19 03:18:52 +02:00
|
|
|
export const getResourcesIndex = async (url, parser = JSON.parse) => {
|
2020-02-11 10:42:15 +02:00
|
|
|
const cache = 'no-store'
|
2024-11-19 22:19:11 +02:00
|
|
|
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]
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
2020-02-11 10:42:15 +02:00
|
|
|
|
2024-10-02 02:35:52 +03:00
|
|
|
try {
|
2024-11-19 22:19:11 +02:00
|
|
|
const builtinData = await window.fetch(url, { cache })
|
|
|
|
|
const builtinResources = await builtinData.json()
|
|
|
|
|
builtin = resourceTransform(builtinResources)
|
2025-02-04 15:23:21 +02:00
|
|
|
} catch {
|
2024-11-19 22:19:11 +02:00
|
|
|
builtin = []
|
|
|
|
|
console.warn(`Builtin resources at ${url} unavailable`)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const customData = await window.fetch(customUrl, { cache })
|
|
|
|
|
const customResources = await customData.json()
|
|
|
|
|
custom = resourceTransform(customResources)
|
2025-02-04 15:23:21 +02:00
|
|
|
} catch {
|
2024-11-19 22:19:11 +02:00
|
|
|
custom = []
|
|
|
|
|
console.warn(`Custom resources at ${customUrl} unavailable`)
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-25 19:42:50 +02:00
|
|
|
const total = [...custom, ...builtin]
|
2024-11-19 22:19:11 +02:00
|
|
|
if (total.length === 0) {
|
|
|
|
|
return Promise.reject(new Error(`Resource at ${url} and ${customUrl} completely unavailable. Panicking`))
|
2024-10-02 02:35:52 +03:00
|
|
|
}
|
2024-11-19 22:19:11 +02:00
|
|
|
return Promise.resolve(Object.fromEntries(total))
|
2017-01-16 17:44:26 +01:00
|
|
|
}
|