Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F85830914
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Flag For Later
Award Token
Authored By
Unknown
Size
9 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/changelog.d/custom.add b/changelog.d/custom.add
new file mode 100644
index 0000000000..97848d7e4a
--- /dev/null
+++ b/changelog.d/custom.add
@@ -0,0 +1 @@
+Added support for fetching /{resource}.custom.ext to allow adding instance-specific themes without altering sourcetree
diff --git a/src/services/style_setter/style_setter.js b/src/services/style_setter/style_setter.js
index 7ea9476472..1892c11161 100644
--- a/src/services/style_setter/style_setter.js
+++ b/src/services/style_setter/style_setter.js
@@ -1,262 +1,283 @@
import { init, getEngineChecksum } from '../theme_data/theme_data_3.service.js'
import { getCssRules } from '../theme_data/css_utils.js'
import { defaultState } from '../../modules/config.js'
import { chunk } from 'lodash'
import pako from 'pako'
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 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.
}
export const generateTheme = (inputRuleset, callbacks, debug) => {
const {
onNewRule = (rule, isLazy) => {},
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 data = await localforage.getItem('pleromafe-theme-cache')
if (!data) return null
let cache
try {
const decoded = new TextDecoder().decode(pako.inflate(data))
cache = JSON.parse(decoded)
console.info(`Loaded theme from cache, size=${cache}`)
} catch (e) {
console.error('Failed to decode theme cache:', e)
return false
}
if (cache.engineChecksum === getEngineChecksum()) {
const eagerStyles = createStyleSheet(EAGER_STYLE_ID)
const lazyStyles = createStyleSheet(LAZY_STYLE_ID)
cache.data[0].forEach(rule => eagerStyles.sheet.insertRule(rule, 'index-max'))
cache.data[1].forEach(rule => lazyStyles.sheet.insertRule(rule, 'index-max'))
adoptStyleSheets([eagerStyles, lazyStyles])
return true
} else {
console.warn('Engine checksum doesn\'t match, cache not usable, clearing')
localStorage.removeItem('pleroma-fe-theme-cache')
}
}
export const applyTheme = (input, onFinish = (data) => {}, debug) => {
const eagerStyles = createStyleSheet(EAGER_STYLE_ID)
const lazyStyles = createStyleSheet(LAZY_STYLE_ID)
const { lazyProcessFunc } = generateTheme(
input,
{
onNewRule (rule, isLazy) {
if (isLazy) {
lazyStyles.sheet.insertRule(rule, 'index-max')
lazyStyles.rules.push(rule)
} else {
eagerStyles.sheet.insertRule(rule, 'index-max')
eagerStyles.rules.push(rule)
}
},
onEagerFinished () {
adoptStyleSheets([eagerStyles])
},
onLazyFinished () {
adoptStyleSheets([eagerStyles, lazyStyles])
const cache = { engineChecksum: getEngineChecksum(), data: [eagerStyles.rules, lazyStyles.rules] }
onFinish(cache)
const compress = (js) => {
return pako.deflate(JSON.stringify(js))
}
localforage.setItem('pleromafe-theme-cache', compress(cache))
}
},
debug
)
setTimeout(lazyProcessFunc, 0)
}
const extractStyleConfig = ({
sidebarColumnWidth,
contentColumnWidth,
notifsColumnWidth,
emojiReactionsScale,
emojiSize,
navbarSize,
panelHeaderSize,
textSize,
forcedRoundness
}) => {
const result = {
sidebarColumnWidth,
contentColumnWidth,
notifsColumnWidth,
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, i18n) => {
const config = extractStyleConfig(input)
if (config === defaultStyleConfig) {
return
}
const head = document.head
const rules = Object
.entries(config)
.filter(([k, v]) => v)
.map(([k, v]) => `--${k}: ${v}`).join(';')
document.getElementById('style-config')?.remove()
const styleEl = document.createElement('style')
styleEl.id = 'style-config'
head.appendChild(styleEl)
const styleSheet = styleEl.sheet
styleSheet.toString()
styleSheet.insertRule(`:root { ${rules} }`, 'index-max')
// TODO find a way to make this not apply to theme previews
if (Object.prototype.hasOwnProperty.call(config, 'forcedRoundness')) {
styleSheet.insertRule(` *:not(.preview-block) {
--roundness: var(--forcedRoundness) !important;
}`, 'index-max')
}
}
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 data = await window.fetch(url, { cache })
- const resources = await data.json()
- return Object.fromEntries(
- 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]
- }
- })
- )
+ const builtinData = await window.fetch(url, { cache })
+ const builtinResources = await builtinData.json()
+ builtin = resourceTransform(builtinResources)
} catch (e) {
- return Promise.reject(e)
+ 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 (e) {
+ custom = []
+ console.warn(`Custom resources at ${customUrl} unavailable`)
+ }
+
+ const total = [...builtin, ...custom]
+ if (total.length === 0) {
+ return Promise.reject(new Error(`Resource at ${url} and ${customUrl} completely unavailable. Panicking`))
}
+ return Promise.resolve(Object.fromEntries(total))
}
diff --git a/static/.gitignore b/static/.gitignore
new file mode 100644
index 0000000000..3332292a15
--- /dev/null
+++ b/static/.gitignore
@@ -0,0 +1 @@
+*.custom.*
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Sun, Nov 1, 7:34 AM (9 h, 7 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1802804
Default Alt Text
(9 KB)
Attached To
Mode
rPUFE pleroma-fe-upstream
Attached
Detach File
Event Timeline