logo

pleroma-fe

My custom branche(s) on git.pleroma.social/pleroma/pleroma-fe git clone https://anongit.hacktivis.me/git/pleroma-fe.git/
commit: ba4be2cb22aa508589f89fe0c44e8da241e91321
parent f0957bdb4ff1fed060696ac953679cef241e92be
Author: Henry Jameson <me@hjkos.com>
Date:   Wed,  2 Oct 2024 02:35:52 +0300

yet another massive overhaul on how themes are loaded/applied

Diffstat:

Msrc/boot/after_store.js2+-
Msrc/components/settings_modal/tabs/appearance_tab.js86+++++++++++++++++++++++++++++++++++++++++++++----------------------------------
Msrc/components/settings_modal/tabs/theme_tab/theme_tab.js23+++++++++++++++--------
Msrc/modules/instance.js3+++
Msrc/modules/interface.js416++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
Msrc/services/style_setter/style_setter.js100+++++++++++++++++++++++++------------------------------------------------------
Mstatic/palettes/index.json8++++----
7 files changed, 380 insertions(+), 258 deletions(-)

diff --git a/src/boot/after_store.js b/src/boot/after_store.js @@ -350,7 +350,7 @@ const afterStoreSetup = async ({ store, i18n }) => { store.dispatch('setInstanceOption', { name: 'server', value: server }) await setConfig({ store }) - await store.dispatch('setTheme') + await store.dispatch('applyTheme', { recompile: false }) applyConfig(store.state.config) diff --git a/src/components/settings_modal/tabs/appearance_tab.js b/src/components/settings_modal/tabs/appearance_tab.js @@ -8,9 +8,6 @@ import FontControl from 'src/components/font_control/font_control.vue' 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 { init } from 'src/services/theme_data/theme_data_3.service.js' import { @@ -42,8 +39,8 @@ const AppearanceTab = { 'link', 'text', 'cRed', - 'cBlue', 'cGreen', + 'cBlue', 'cOrange' ], intersectionObserver: null, @@ -75,37 +72,50 @@ const AppearanceTab = { Preview }, mounted () { - getThemeResources('/static/styles.json') - .then((themes) => { - this.availableStyles = Object - .entries(themes) - .map(([key, data]) => ({ key, data, name: data.name || data[0], version: 'v2' })) - }) + const updateIndex = (resource) => { + const capitalizedResource = resource[0].toUpperCase() + resource.slice(1) + const currentIndex = this.$store.state.instance[`${resource}sIndex`] - getThemeResources('/static/palettes/index.json') - .then((palettes) => { - const result = {} - console.log(palettes) - Object.entries(palettes).forEach(([k, v]) => { - if (Array.isArray(v)) { - const [ - name, - background, - foreground, - text, - 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 + let promise + if (currentIndex) { + promise = Promise.resolve(currentIndex) + } else { + promise = this.$store.dispatch(`fetch${capitalizedResource}sIndex`) + } + + return promise.then(index => { + return Object + .entries(index) + .map(([k, func]) => [k, func()]) }) + } + + 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) { this.intersectionObserver = new IntersectionObserver((entries, observer) => { @@ -186,11 +196,13 @@ const AppearanceTab = { const { theme } = this.mergedConfig return key === theme }, - setTheme (name) { - this.$store.dispatch('setTheme', { themeName: name, saveData: true, recompile: true }) + async setTheme (name) { + await this.$store.dispatch('setTheme', name) + this.$store.dispatch('applyTheme') }, - setPalette (name) { - this.$store.dispatch('setPalette', { paletteData: name }) + async setPalette (name) { + await this.$store.dispatch('setPalette', name) + this.$store.dispatch('applyTheme') }, previewTheme (key, input) { let theme3 diff --git a/src/components/settings_modal/tabs/theme_tab/theme_tab.js b/src/components/settings_modal/tabs/theme_tab/theme_tab.js @@ -5,9 +5,6 @@ import { relativeLuminance } from 'src/services/color_convert/color_convert.js' import { - getThemeResources -} from 'src/services/style_setter/style_setter.js' -import { newImporter, newExporter } from 'src/services/export_import/export_import.js' @@ -123,12 +120,22 @@ export default { } }, created () { - const self = this + const currentIndex = this.$store.state.instance.themesIndex - getThemeResources('/static/styles.json') - .then((themesComplete) => { - self.availableStyles = Object.values(themesComplete) - }) + let promise + if (currentIndex) { + 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 () { this.loadThemeFromLocalStorage() diff --git a/src/modules/instance.js b/src/modules/instance.js @@ -42,6 +42,9 @@ const defaultState = { registrationOpen: true, server: 'http://localhost:4040/', textlimit: 5000, + themesIndex: undefined, + stylesIndex: undefined, + palettesIndex: undefined, themeData: undefined, // used for theme editor v2 vapidPublicKey: undefined, diff --git a/src/modules/interface.js b/src/modules/interface.js @@ -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 { convertTheme2To3 } from 'src/services/theme_data/theme2_to_theme3.js' @@ -212,35 +212,229 @@ const interfaceMod = { setLastTimeline ({ commit }, value) { commit('setLastTimeline', value) }, - setPalette ({ dispatch, commit }, { paletteData }) { - console.log('PAL', paletteData) - commit('setOption', { name: 'userPalette', value: paletteData }) - dispatch('setTheme', { themeName: null, recompile: true }) + async fetchPalettesIndex ({ commit, state }) { + try { + const value = await getResourcesIndex('/static/palettes/index.json') + commit('setInstanceOption', { name: 'palettesIndex', value }) + return value + } catch (e) { + console.error('Could not fetch palettes index', e) + return {} + } + }, + 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') }, - setTheme ({ commit, rootState }, { themeName, themeData, recompile, saveData } = {}) { + 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 { - theme: instanceThemeName + style: instanceStyleName, + palette: instancePaletteName + } = rootState.instance + let { + theme: instanceThemeV2Name, + themesIndex, + stylesIndex, + palettesIndex } = rootState.instance const { - theme: userThemeName, - customTheme: userThemeSnapshot, - customThemeSource: userThemeSource, - userPalette, + style: userStyleName, + styleCustomData: userStyleCustomData, + palette: userPaletteName, + paletteCustomData: userPaletteCustomData, forceThemeRecompilation, themeDebug, theme3hacks } = rootState.config + let { + theme: userThemeV2Name, + customTheme: userThemeV2Snapshot, + customThemeSource: userThemeV2Source - const userPaletteIss = (() => { - if (!userPalette) return null + } = rootState.config + + 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 = { component: 'Root', directives: {} } Object - .entries(userPalette) + .entries(paletteDataUsed) .filter(([k]) => k !== 'name') .forEach(([k, v]) => { let issRootDirectiveName @@ -258,139 +452,84 @@ const interfaceMod = { }) return result })() - const actualThemeName = userThemeName || instanceThemeName - - const forceRecompile = forceThemeRecompilation || recompile - - let promise = null - - console.log('TEST', actualThemeName, themeData) - - if (themeData) { - promise = Promise.resolve(normalizeThemeData(themeData)) - } else if (themeName) { - promise = getPreset(themeName).then(themeData => normalizeThemeData(themeData)) - } else if (themeName === null) { - promise = Promise.resolve(null) - } else if (userThemeSource || userThemeSnapshot) { - promise = Promise.resolve(normalizeThemeData({ - _pleroma_theme_version: 2, - theme: userThemeSnapshot, - source: userThemeSource - })) - } else if (actualThemeName && actualThemeName !== 'custom') { - promise = getPreset(actualThemeName).then(themeData => { - const realThemeData = normalizeThemeData(themeData) - 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 } }) - } - return realThemeData - }) - } else { - throw new Error('Cannot load any theme!') - } - - // If we're not not forced to recompile try using - // cache (tryLoadCache return true if load successful) - if (!forceRecompile && !themeDebug && tryLoadCache()) { - commit('setThemeApplied') - return - } - promise - .then(realThemeData => { - const theme2ruleset = realThemeData ? convertTheme2To3(realThemeData) : null + const theme2ruleset = themeDataUsed && convertTheme2To3(normalizeThemeData(themeDataUsed)) + const hacks = [] - if (saveData) { - commit('setOption', { name: 'theme', value: themeName || actualThemeName }) - commit('setOption', { name: 'customTheme', value: realThemeData }) - commit('setOption', { name: 'customThemeSource', value: realThemeData }) + 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 } - - 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') { + const newRule = { + component: 'Underlay', + directives: {} } - case 'underlay': { - if (value !== 'none') { - const newRule = { - component: 'Underlay', - directives: {} - } - if (value === 'opaque') { - newRule.directives.opacity = 1 - 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) } - }) - - const ruleset = [ - ...hacks - ] - if (!theme2ruleset && userPaletteIss) { - ruleset.unshift(userPaletteIss) - } - - if (theme2ruleset) { - ruleset.unshift(...theme2ruleset) + break } + } + }) - applyTheme( - ruleset, - () => commit('setThemeApplied'), - themeDebug - ) - }) + const rulesetArray = [ + theme2ruleset, + styleDataUsed, + paletteIss, + hacks + ].filter(x => x) - return promise + return applyTheme( + rulesetArray.flat(), + () => commit('setThemeApplied'), + themeDebug + ) } } } @@ -398,7 +537,6 @@ const interfaceMod = { export default interfaceMod export const normalizeThemeData = (input) => { - console.log(input) if (Array.isArray(input)) { const themeData = { colors: {} } themeData.colors.bg = input[1] diff --git a/src/services/style_setter/style_setter.js b/src/services/style_setter/style_setter.js @@ -1,4 +1,3 @@ -import { hex2rgb } from '../color_convert/color_convert.js' 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' @@ -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 const themes3 = init({ inputRuleset, - ultimateBackgroundColor: inputRuleset[0].directives['--bg'].split('|')[1].trim(), + ultimateBackgroundColor: inputRuleset[0].directives['--bg']?.split('|')[1].trim() || '#000000', debug }) @@ -256,73 +255,36 @@ export const applyConfig = (input) => { body.classList.remove('hidden') } -export const getThemeResources = (url) => { +export const getResourcesIndex = async (url) => { const cache = 'no-store' - return window.fetch(url, { cache }) - .then((data) => data.json()) - .then((resources) => { - return Object.entries(resources).map(([k, v]) => { - let promise = null - if (typeof v === 'object') { - promise = Promise.resolve(v) - } else if (typeof v === 'string') { - promise = window.fetch(v, { cache }) - .then((data) => data.json()) - .catch((e) => { - console.error(e) - return null - }) - } - return [k, promise] - }) - }) - .then((promises) => { - return promises - .reduce((acc, [k, v]) => { - acc[k] = v - return acc - }, {}) - }) - .then((promises) => { - return Promise.all( - Object.entries(promises) - .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 } - }) + 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.json()) + .catch((e) => { + console.error(e) + return null + }) + ] + } else { + console.error(`Unknown resource format - ${k} is a ${typeof v}`) + return [k, null] + } + }) + ) + } catch (e) { + return Promise.reject(e) + } } diff --git a/static/palettes/index.json b/static/palettes/index.json @@ -8,8 +8,8 @@ "text": "#b9b9b9", "link": "#baaa9c", "cRed": "#d31014", - "cBlue": "#0fa00f", - "cGreen": "#0095ff", + "cGreen": "#0fa00f", + "cBlue": "#0095ff", "cOrange": "#ffa500" }, "pleroma-amoled": [ "Pleroma Dark AMOLED", "#000000", "#111111", "#b0b0b1", "#d8a070", "#aa0000", "#0fa00f", "#0095ff", "#d59500"], @@ -20,8 +20,8 @@ "link": "#81a2be", "text": "#c5c8c6", "cRed": "#cc6666", - "cBlue": "#8abeb7", - "cGreen": "#b5bd68", + "cGreen": "#8abeb7", + "cBlue": "#b5bd68", "cOrange": "#de935f", "_cYellow": "#f0c674", "_cPurple": "#b294bb"