yet another massive overhaul on how themes are loaded/applied

This commit is contained in:
Henry Jameson 2024-10-02 02:35:52 +03:00
parent f0957bdb4f
commit ba4be2cb22
7 changed files with 381 additions and 259 deletions

View file

@ -350,7 +350,7 @@ const afterStoreSetup = async ({ store, i18n }) => {
store.dispatch('setInstanceOption', { name: 'server', value: server }) store.dispatch('setInstanceOption', { name: 'server', value: server })
await setConfig({ store }) await setConfig({ store })
await store.dispatch('setTheme') await store.dispatch('applyTheme', { recompile: false })
applyConfig(store.state.config) applyConfig(store.state.config)

View file

@ -8,9 +8,6 @@ import FontControl from 'src/components/font_control/font_control.vue'
import { normalizeThemeData } from 'src/modules/interface' import { normalizeThemeData } from 'src/modules/interface'
import {
getThemeResources
} from 'src/services/style_setter/style_setter.js'
import { convertTheme2To3 } from 'src/services/theme_data/theme2_to_theme3.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 { init } from 'src/services/theme_data/theme_data_3.service.js'
import { import {
@ -42,8 +39,8 @@ const AppearanceTab = {
'link', 'link',
'text', 'text',
'cRed', 'cRed',
'cBlue',
'cGreen', 'cGreen',
'cBlue',
'cOrange' 'cOrange'
], ],
intersectionObserver: null, intersectionObserver: null,
@ -75,37 +72,50 @@ const AppearanceTab = {
Preview Preview
}, },
mounted () { mounted () {
getThemeResources('/static/styles.json') const updateIndex = (resource) => {
.then((themes) => { const capitalizedResource = resource[0].toUpperCase() + resource.slice(1)
this.availableStyles = Object const currentIndex = this.$store.state.instance[`${resource}sIndex`]
.entries(themes)
.map(([key, data]) => ({ key, data, name: data.name || data[0], version: 'v2' }))
})
getThemeResources('/static/palettes/index.json') let promise
.then((palettes) => { if (currentIndex) {
const result = {} promise = Promise.resolve(currentIndex)
console.log(palettes) } else {
Object.entries(palettes).forEach(([k, v]) => { promise = this.$store.dispatch(`fetch${capitalizedResource}sIndex`)
if (Array.isArray(v)) { }
const [
name, return promise.then(index => {
background, return Object
foreground, .entries(index)
text, .map(([k, func]) => [k, func()])
link,
cRed = '#FF0000',
cBlue = '#0000FF',
cGreen = '#00FF00',
cOrange = '#E3FF00'
] = v
result[k] = { name, background, foreground, text, link, cRed, cBlue, cGreen, cOrange }
} else {
result[k] = v
}
})
this.availablePalettes = result
}) })
}
updateIndex('theme').then(themes => {
themes.forEach(([key, themePromise]) => themePromise.then(data => {
this.availableStyles.push({ key, data, name: data.name, version: 'v2' })
}))
})
updateIndex('palette').then(palettes => {
palettes.forEach(([key, palettePromise]) => palettePromise.then(v => {
if (Array.isArray(v)) {
const [
name,
background,
foreground,
text,
link,
cRed = '#FF0000',
cGreen = '#00FF00',
cBlue = '#0000FF',
cOrange = '#E3FF00'
] = v
this.availablePalettes.push({ key, name, background, foreground, text, link, cRed, cBlue, cGreen, cOrange })
} else {
this.availablePalettes.push({ key, ...v })
}
}))
})
if (window.IntersectionObserver) { if (window.IntersectionObserver) {
this.intersectionObserver = new IntersectionObserver((entries, observer) => { this.intersectionObserver = new IntersectionObserver((entries, observer) => {
@ -186,11 +196,13 @@ const AppearanceTab = {
const { theme } = this.mergedConfig const { theme } = this.mergedConfig
return key === theme return key === theme
}, },
setTheme (name) { async setTheme (name) {
this.$store.dispatch('setTheme', { themeName: name, saveData: true, recompile: true }) await this.$store.dispatch('setTheme', name)
this.$store.dispatch('applyTheme')
}, },
setPalette (name) { async setPalette (name) {
this.$store.dispatch('setPalette', { paletteData: name }) await this.$store.dispatch('setPalette', name)
this.$store.dispatch('applyTheme')
}, },
previewTheme (key, input) { previewTheme (key, input) {
let theme3 let theme3

View file

@ -4,9 +4,6 @@ import {
getContrastRatioLayers, getContrastRatioLayers,
relativeLuminance relativeLuminance
} from 'src/services/color_convert/color_convert.js' } from 'src/services/color_convert/color_convert.js'
import {
getThemeResources
} from 'src/services/style_setter/style_setter.js'
import { import {
newImporter, newImporter,
newExporter newExporter
@ -123,12 +120,22 @@ export default {
} }
}, },
created () { created () {
const self = this const currentIndex = this.$store.state.instance.themesIndex
getThemeResources('/static/styles.json') let promise
.then((themesComplete) => { if (currentIndex) {
self.availableStyles = Object.values(themesComplete) promise = Promise.resolve(currentIndex)
}) } else {
promise = this.$store.dispatch('fetchThemesIndex')
}
promise.then(themesIndex => {
Object
.values(themesIndex)
.forEach(themeFunc => {
themeFunc().then(themeData => this.availableStyles.push(themeData))
})
})
}, },
mounted () { mounted () {
this.loadThemeFromLocalStorage() this.loadThemeFromLocalStorage()

View file

@ -42,6 +42,9 @@ const defaultState = {
registrationOpen: true, registrationOpen: true,
server: 'http://localhost:4040/', server: 'http://localhost:4040/',
textlimit: 5000, textlimit: 5000,
themesIndex: undefined,
stylesIndex: undefined,
palettesIndex: undefined,
themeData: undefined, // used for theme editor v2 themeData: undefined, // used for theme editor v2
vapidPublicKey: undefined, vapidPublicKey: undefined,

View file

@ -1,4 +1,4 @@
import { getPreset, applyTheme, tryLoadCache } from '../services/style_setter/style_setter.js' import { getResourcesIndex, applyTheme, tryLoadCache } from '../services/style_setter/style_setter.js'
import { CURRENT_VERSION, generatePreset } from 'src/services/theme_data/theme_data.service.js' import { CURRENT_VERSION, generatePreset } from 'src/services/theme_data/theme_data.service.js'
import { convertTheme2To3 } from 'src/services/theme_data/theme2_to_theme3.js' import { convertTheme2To3 } from 'src/services/theme_data/theme2_to_theme3.js'
@ -212,35 +212,229 @@ const interfaceMod = {
setLastTimeline ({ commit }, value) { setLastTimeline ({ commit }, value) {
commit('setLastTimeline', value) commit('setLastTimeline', value)
}, },
setPalette ({ dispatch, commit }, { paletteData }) { async fetchPalettesIndex ({ commit, state }) {
console.log('PAL', paletteData) try {
commit('setOption', { name: 'userPalette', value: paletteData }) const value = await getResourcesIndex('/static/palettes/index.json')
dispatch('setTheme', { themeName: null, recompile: true }) commit('setInstanceOption', { name: 'palettesIndex', value })
return value
} catch (e) {
console.error('Could not fetch palettes index', e)
return {}
}
}, },
setTheme ({ commit, rootState }, { themeName, themeData, recompile, saveData } = {}) { setPalette ({ dispatch, commit }, value) {
dispatch('resetV3')
dispatch('resetV2')
commit('setOption', { name: 'palette', value })
dispatch('applyTheme')
},
setPaletteCustom ({ dispatch, commit }, value) {
dispatch('resetV3')
dispatch('resetV2')
commit('setOption', { name: 'paletteCustomData', value })
dispatch('applyTheme')
},
async fetchStylesIndex ({ commit, state }) {
try {
const value = await getResourcesIndex('/static/styles/index.json')
commit('setInstanceOption', { name: 'stylesIndex', value })
return value
} catch (e) {
console.error('Could not fetch styles index', e)
return Promise.resolve({})
}
},
setStyle ({ dispatch, commit }, value) {
dispatch('resetV3')
dispatch('resetV2')
commit('setOption', { name: 'style', value })
dispatch('applyTheme')
},
setStyleCustom ({ dispatch, commit }, value) {
dispatch('resetV3')
dispatch('resetV2')
commit('setOption', { name: 'styleCustomData', value })
dispatch('applyTheme')
},
async fetchThemesIndex ({ commit, state }) {
try {
const value = await getResourcesIndex('/static/styles.json')
commit('setInstanceOption', { name: 'themesIndex', value })
return value
} catch (e) {
console.error('Could not fetch themes index', e)
return Promise.resolve({})
}
},
setTheme ({ dispatch, commit }, value) {
dispatch('resetV3')
dispatch('resetV2')
commit('setOption', { name: 'theme', value })
dispatch('applyTheme')
},
setThemeCustom ({ dispatch, commit }, value) {
dispatch('resetV3')
dispatch('resetV2')
commit('setOption', { name: 'customTheme', value })
commit('setOption', { name: 'customThemeSource', value })
dispatch('applyTheme')
},
resetV3 ({ dispatch, commit }) {
commit('setOption', { name: 'style', value: null })
commit('setOption', { name: 'styleCustomData', value: null })
commit('setOption', { name: 'palette', value: null })
commit('setOption', { name: 'paletteCustomData', value: null })
},
resetV2 ({ dispatch, commit }) {
commit('setOption', { name: 'theme', value: null })
commit('setOption', { name: 'customTheme', value: null })
commit('setOption', { name: 'customThemeSource', value: null })
},
async applyTheme (
{ dispatch, commit, rootState },
{ recompile = true } = {}
) {
// If we're not not forced to recompile try using
// cache (tryLoadCache return true if load successful)
const { const {
theme: instanceThemeName style: instanceStyleName,
palette: instancePaletteName
} = rootState.instance
let {
theme: instanceThemeV2Name,
themesIndex,
stylesIndex,
palettesIndex
} = rootState.instance } = rootState.instance
const { const {
theme: userThemeName, style: userStyleName,
customTheme: userThemeSnapshot, styleCustomData: userStyleCustomData,
customThemeSource: userThemeSource, palette: userPaletteName,
userPalette, paletteCustomData: userPaletteCustomData,
forceThemeRecompilation, forceThemeRecompilation,
themeDebug, themeDebug,
theme3hacks theme3hacks
} = rootState.config } = rootState.config
let {
theme: userThemeV2Name,
customTheme: userThemeV2Snapshot,
customThemeSource: userThemeV2Source
const userPaletteIss = (() => { } = rootState.config
if (!userPalette) return null
const forceRecompile = forceThemeRecompilation || recompile
if (!forceRecompile && !themeDebug && tryLoadCache()) {
return commit('setThemeApplied')
}
let majorVersionUsed
if (userPaletteName || userPaletteCustomData ||
userStyleName || userStyleCustomData ||
instancePaletteName ||
instanceStyleName
) {
// Palette and/or style overrides V2 themes
instanceThemeV2Name = null
userThemeV2Name = null
userThemeV2Source = null
userThemeV2Snapshot = null
majorVersionUsed = 'v3'
if (!palettesIndex || !stylesIndex) {
const result = await Promise.all([
dispatch('fetchPalettesIndex'),
dispatch('fetchStylesIndex')
])
palettesIndex = result[0]
stylesIndex = result[1]
}
} else {
majorVersionUsed = 'v2'
// Promise.all just to be uniform with v3
const result = await Promise.all([
dispatch('fetchThemesIndex')
])
themesIndex = result[0]
}
let styleDataUsed = null
let styleNameUsed = null
let paletteDataUsed = null
let paletteNameUsed = null
let themeNameUsed = null
let themeDataUsed = null
if (majorVersionUsed === 'v3') {
if (userStyleCustomData) {
styleNameUsed = 'custom' // custom data overrides name
styleDataUsed = userStyleCustomData
} else {
styleNameUsed = userStyleName || instanceStyleName
let styleFetchFunc = stylesIndex[themeNameUsed]
if (!styleFetchFunc) {
const newName = Object.keys(stylesIndex)[0]
styleFetchFunc = stylesIndex[newName]
console.warn(`Style with id '${styleNameUsed}' not found, falling back to '${newName}'`)
}
styleDataUsed = await styleFetchFunc?.()
}
if (userPaletteCustomData) {
paletteNameUsed = 'custom' // custom data overrides name
paletteDataUsed = userPaletteCustomData
} else {
paletteNameUsed = userPaletteName || instanceStyleName
let paletteFetchFunc = palettesIndex[themeNameUsed]
if (!paletteFetchFunc) {
const newName = Object.keys(palettesIndex)[0]
paletteFetchFunc = palettesIndex[newName]
console.warn(`Palette with id '${paletteNameUsed}' not found, falling back to '${newName}'`)
}
paletteDataUsed = await paletteFetchFunc?.()
}
} else {
if (userThemeV2Snapshot || userThemeV2Source) {
themeNameUsed = 'custom' // custom data overrides name
themeDataUsed = userThemeV2Snapshot || userThemeV2Source
} else {
themeNameUsed = userThemeV2Name || instanceThemeV2Name
let themeFetchFunc = themesIndex[themeNameUsed]
if (!themeFetchFunc) {
const newName = Object.keys(themesIndex)[0]
themeFetchFunc = themesIndex[newName]
console.warn(`Theme with id '${themeNameUsed}' not found, falling back to '${newName}'`)
}
themeDataUsed = await themeFetchFunc?.()
}
// Themes v2 editor support
commit('setInstanceOption', { name: 'themeData', value: themeDataUsed })
}
const paletteIss = (() => {
if (!paletteDataUsed) return null
const result = { const result = {
component: 'Root', component: 'Root',
directives: {} directives: {}
} }
Object Object
.entries(userPalette) .entries(paletteDataUsed)
.filter(([k]) => k !== 'name') .filter(([k]) => k !== 'name')
.forEach(([k, v]) => { .forEach(([k, v]) => {
let issRootDirectiveName let issRootDirectiveName
@ -258,139 +452,84 @@ const interfaceMod = {
}) })
return result return result
})() })()
const actualThemeName = userThemeName || instanceThemeName
const forceRecompile = forceThemeRecompilation || recompile const theme2ruleset = themeDataUsed && convertTheme2To3(normalizeThemeData(themeDataUsed))
const hacks = []
let promise = null Object.entries(theme3hacks).forEach(([key, value]) => {
switch (key) {
console.log('TEST', actualThemeName, themeData) case 'fonts': {
Object.entries(theme3hacks.fonts).forEach(([fontKey, font]) => {
if (themeData) { if (!font?.family) return
promise = Promise.resolve(normalizeThemeData(themeData)) switch (fontKey) {
} else if (themeName) { case 'interface':
promise = getPreset(themeName).then(themeData => normalizeThemeData(themeData)) hacks.push({
} else if (themeName === null) { component: 'Root',
promise = Promise.resolve(null) directives: {
} else if (userThemeSource || userThemeSnapshot) { '--font': 'generic | ' + font.family
promise = Promise.resolve(normalizeThemeData({ }
_pleroma_theme_version: 2, })
theme: userThemeSnapshot, break
source: userThemeSource case 'input':
})) hacks.push({
} else if (actualThemeName && actualThemeName !== 'custom') { component: 'Input',
promise = getPreset(actualThemeName).then(themeData => { directives: {
const realThemeData = normalizeThemeData(themeData) '--font': 'generic | ' + font.family
if (actualThemeName === instanceThemeName) { }
// This sole line is the reason why this whole block is above the recompilation check })
commit('setInstanceOption', { name: 'themeData', value: { theme: realThemeData } }) break
} case 'post':
return realThemeData hacks.push({
}) component: 'RichContent',
} else { directives: {
throw new Error('Cannot load any theme!') '--font': 'generic | ' + font.family
} }
})
// If we're not not forced to recompile try using break
// cache (tryLoadCache return true if load successful) case 'monospace':
if (!forceRecompile && !themeDebug && tryLoadCache()) { hacks.push({
commit('setThemeApplied') component: 'Root',
return directives: {
} '--monoFont': 'generic | ' + font.family
}
promise })
.then(realThemeData => { break
const theme2ruleset = realThemeData ? convertTheme2To3(realThemeData) : null
if (saveData) {
commit('setOption', { name: 'theme', value: themeName || actualThemeName })
commit('setOption', { name: 'customTheme', value: realThemeData })
commit('setOption', { name: 'customThemeSource', value: realThemeData })
}
const hacks = []
Object.entries(theme3hacks).forEach(([key, value]) => {
switch (key) {
case 'fonts': {
Object.entries(theme3hacks.fonts).forEach(([fontKey, font]) => {
if (!font?.family) return
switch (fontKey) {
case 'interface':
hacks.push({
component: 'Root',
directives: {
'--font': 'generic | ' + font.family
}
})
break
case 'input':
hacks.push({
component: 'Input',
directives: {
'--font': 'generic | ' + font.family
}
})
break
case 'post':
hacks.push({
component: 'RichContent',
directives: {
'--font': 'generic | ' + font.family
}
})
break
case 'monospace':
hacks.push({
component: 'Root',
directives: {
'--monoFont': 'generic | ' + font.family
}
})
break
}
})
break
} }
case 'underlay': { })
if (value !== 'none') { break
const newRule = { }
component: 'Underlay', case 'underlay': {
directives: {} if (value !== 'none') {
} const newRule = {
if (value === 'opaque') { component: 'Underlay',
newRule.directives.opacity = 1 directives: {}
newRule.directives.background = '--wallpaper'
}
if (value === 'transparent') {
newRule.directives.opacity = 0
}
hacks.push(newRule)
}
break
} }
if (value === 'opaque') {
newRule.directives.opacity = 1
newRule.directives.background = '--wallpaper'
}
if (value === 'transparent') {
newRule.directives.opacity = 0
}
hacks.push(newRule)
} }
}) break
const ruleset = [
...hacks
]
if (!theme2ruleset && userPaletteIss) {
ruleset.unshift(userPaletteIss)
} }
}
})
if (theme2ruleset) { const rulesetArray = [
ruleset.unshift(...theme2ruleset) theme2ruleset,
} styleDataUsed,
paletteIss,
hacks
].filter(x => x)
applyTheme( return applyTheme(
ruleset, rulesetArray.flat(),
() => commit('setThemeApplied'), () => commit('setThemeApplied'),
themeDebug themeDebug
) )
})
return promise
} }
} }
} }
@ -398,7 +537,6 @@ const interfaceMod = {
export default interfaceMod export default interfaceMod
export const normalizeThemeData = (input) => { export const normalizeThemeData = (input) => {
console.log(input)
if (Array.isArray(input)) { if (Array.isArray(input)) {
const themeData = { colors: {} } const themeData = { colors: {} }
themeData.colors.bg = input[1] themeData.colors.bg = input[1]

View file

@ -1,4 +1,3 @@
import { hex2rgb } from '../color_convert/color_convert.js'
import { init, getEngineChecksum } from '../theme_data/theme_data_3.service.js' import { init, getEngineChecksum } from '../theme_data/theme_data_3.service.js'
import { getCssRules } from '../theme_data/css_utils.js' import { getCssRules } from '../theme_data/css_utils.js'
import { defaultState } from '../../modules/config.js' import { defaultState } from '../../modules/config.js'
@ -53,7 +52,7 @@ export const generateTheme = async (inputRuleset, callbacks, debug) => {
// Assuming that "worst case scenario background" is panel background since it's the most likely one // Assuming that "worst case scenario background" is panel background since it's the most likely one
const themes3 = init({ const themes3 = init({
inputRuleset, inputRuleset,
ultimateBackgroundColor: inputRuleset[0].directives['--bg'].split('|')[1].trim(), ultimateBackgroundColor: inputRuleset[0].directives['--bg']?.split('|')[1].trim() || '#000000',
debug debug
}) })
@ -256,73 +255,36 @@ export const applyConfig = (input) => {
body.classList.remove('hidden') body.classList.remove('hidden')
} }
export const getThemeResources = (url) => { export const getResourcesIndex = async (url) => {
const cache = 'no-store' const cache = 'no-store'
return window.fetch(url, { cache }) try {
.then((data) => data.json()) const data = await window.fetch(url, { cache })
.then((resources) => { const resources = await data.json()
return Object.entries(resources).map(([k, v]) => { return Object.fromEntries(
let promise = null Object
if (typeof v === 'object') { .entries(resources)
promise = Promise.resolve(v) .map(([k, v]) => {
} else if (typeof v === 'string') { if (typeof v === 'object') {
promise = window.fetch(v, { cache }) return [k, () => Promise.resolve(v)]
.then((data) => data.json()) } else if (typeof v === 'string') {
.catch((e) => { return [
console.error(e) k,
return null () => window
}) .fetch(v, { cache })
} .then((data) => data.json())
return [k, promise] .catch((e) => {
}) console.error(e)
}) return null
.then((promises) => { })
return promises ]
.reduce((acc, [k, v]) => { } else {
acc[k] = v console.error(`Unknown resource format - ${k} is a ${typeof v}`)
return acc return [k, null]
}, {}) }
}) })
.then((promises) => { )
return Promise.all( } catch (e) {
Object.entries(promises) return Promise.reject(e)
.map(([k, v]) => v.then(res => [k, res])) }
)
})
.then(themes => themes.reduce((acc, [k, v]) => {
if (v) {
return {
...acc,
[k]: v
}
} else {
return acc
}
}, {}))
}
export const getPreset = (val) => {
return getThemeResources('/static/styles.json')
.then((themes) => themes[val] ? themes[val] : themes['pleroma-dark'])
.then((theme) => {
const isV1 = Array.isArray(theme)
const data = isV1 ? {} : theme.theme
if (isV1) {
const bg = hex2rgb(theme[1])
const fg = hex2rgb(theme[2])
const text = hex2rgb(theme[3])
const link = hex2rgb(theme[4])
const cRed = hex2rgb(theme[5] || '#FF0000')
const cGreen = hex2rgb(theme[6] || '#00FF00')
const cBlue = hex2rgb(theme[7] || '#0000FF')
const cOrange = hex2rgb(theme[8] || '#E3FF00')
data.colors = { bg, fg, text, link, cRed, cBlue, cGreen, cOrange }
}
return { theme: data, source: theme.source }
})
} }

View file

@ -8,8 +8,8 @@
"text": "#b9b9b9", "text": "#b9b9b9",
"link": "#baaa9c", "link": "#baaa9c",
"cRed": "#d31014", "cRed": "#d31014",
"cBlue": "#0fa00f", "cGreen": "#0fa00f",
"cGreen": "#0095ff", "cBlue": "#0095ff",
"cOrange": "#ffa500" "cOrange": "#ffa500"
}, },
"pleroma-amoled": [ "Pleroma Dark AMOLED", "#000000", "#111111", "#b0b0b1", "#d8a070", "#aa0000", "#0fa00f", "#0095ff", "#d59500"], "pleroma-amoled": [ "Pleroma Dark AMOLED", "#000000", "#111111", "#b0b0b1", "#d8a070", "#aa0000", "#0fa00f", "#0095ff", "#d59500"],
@ -20,8 +20,8 @@
"link": "#81a2be", "link": "#81a2be",
"text": "#c5c8c6", "text": "#c5c8c6",
"cRed": "#cc6666", "cRed": "#cc6666",
"cBlue": "#8abeb7", "cGreen": "#8abeb7",
"cGreen": "#b5bd68", "cBlue": "#b5bd68",
"cOrange": "#de935f", "cOrange": "#de935f",
"_cYellow": "#f0c674", "_cYellow": "#f0c674",
"_cPurple": "#b294bb" "_cPurple": "#b294bb"