2017-11-21 07:45:09 +11:00
|
|
|
import { times } from 'lodash'
|
2020-01-12 12:44:06 +11:00
|
|
|
import { convert } from 'chromatism'
|
|
|
|
import { rgb2hex, hex2rgb, rgba2css, getCssColor } from '../color_convert/color_convert.js'
|
2020-01-20 09:34:49 +11:00
|
|
|
import { getColors, computeDynamicColor } from '../theme_data/theme_data.service.js'
|
2020-01-07 07:55:14 +11:00
|
|
|
|
2017-11-18 02:24:42 +11:00
|
|
|
// While this is not used anymore right now, I left it in if we want to do custom
|
|
|
|
// styles that aren't just colors, so user can pick from a few different distinct
|
|
|
|
// styles as well as set their own colors in the future.
|
|
|
|
|
2020-01-22 09:37:19 +11:00
|
|
|
export const setStyle = (href) => {
|
2017-01-17 03:44:26 +11:00
|
|
|
/***
|
|
|
|
What's going on here?
|
|
|
|
I want to make it easy for admins to style this application. To have
|
|
|
|
a good set of default themes, I chose the system from base16
|
|
|
|
(https://chriskempson.github.io/base16/) to style all elements. They
|
|
|
|
all have the base00..0F classes. So the only thing an admin needs to
|
|
|
|
do to style Pleroma is to change these colors in that one css file.
|
|
|
|
Some default things (body text color, link color) need to be set dy-
|
|
|
|
namically, so this is done here by waiting for the stylesheet to be
|
|
|
|
loaded and then creating an element with the respective classes.
|
|
|
|
|
|
|
|
It is a bit weird, but should make life for admins somewhat easier.
|
|
|
|
***/
|
|
|
|
const head = document.head
|
|
|
|
const body = document.body
|
2019-08-28 02:17:10 +10:00
|
|
|
body.classList.add('hidden')
|
2017-01-17 03:44:26 +11:00
|
|
|
const cssEl = document.createElement('link')
|
|
|
|
cssEl.setAttribute('rel', 'stylesheet')
|
|
|
|
cssEl.setAttribute('href', href)
|
|
|
|
head.appendChild(cssEl)
|
|
|
|
|
|
|
|
const setDynamic = () => {
|
|
|
|
const baseEl = document.createElement('div')
|
2017-01-21 09:39:38 +11:00
|
|
|
body.appendChild(baseEl)
|
2017-02-23 07:14:55 +11:00
|
|
|
|
|
|
|
let colors = {}
|
|
|
|
times(16, (n) => {
|
|
|
|
const name = `base0${n.toString(16).toUpperCase()}`
|
|
|
|
baseEl.setAttribute('class', name)
|
|
|
|
const color = window.getComputedStyle(baseEl).getPropertyValue('color')
|
|
|
|
colors[name] = color
|
|
|
|
})
|
|
|
|
|
|
|
|
body.removeChild(baseEl)
|
|
|
|
|
2017-01-17 03:44:26 +11:00
|
|
|
const styleEl = document.createElement('style')
|
|
|
|
head.appendChild(styleEl)
|
2018-04-01 04:14:36 +10:00
|
|
|
// const styleSheet = styleEl.sheet
|
2017-01-17 03:44:26 +11:00
|
|
|
|
2019-08-28 02:17:10 +10:00
|
|
|
body.classList.remove('hidden')
|
2017-01-17 03:44:26 +11:00
|
|
|
}
|
2017-11-14 10:37:49 +11:00
|
|
|
|
2017-11-18 02:24:42 +11:00
|
|
|
cssEl.addEventListener('load', setDynamic)
|
|
|
|
}
|
2017-11-14 10:37:49 +11:00
|
|
|
|
2020-01-22 09:37:19 +11:00
|
|
|
export const applyTheme = (input) => {
|
|
|
|
const { rules } = generatePreset(input)
|
2017-11-18 02:24:42 +11:00
|
|
|
const head = document.head
|
|
|
|
const body = document.body
|
2019-08-28 02:17:10 +10:00
|
|
|
body.classList.add('hidden')
|
2017-11-14 10:37:49 +11:00
|
|
|
|
2017-11-18 02:24:42 +11:00
|
|
|
const styleEl = document.createElement('style')
|
|
|
|
head.appendChild(styleEl)
|
|
|
|
const styleSheet = styleEl.sheet
|
2017-11-17 11:17:36 +11:00
|
|
|
|
2018-04-01 12:28:20 +10:00
|
|
|
styleSheet.toString()
|
2018-11-20 04:22:46 +11:00
|
|
|
styleSheet.insertRule(`body { ${rules.radii} }`, 'index-max')
|
|
|
|
styleSheet.insertRule(`body { ${rules.colors} }`, 'index-max')
|
|
|
|
styleSheet.insertRule(`body { ${rules.shadows} }`, 'index-max')
|
2018-11-26 05:48:16 +11:00
|
|
|
styleSheet.insertRule(`body { ${rules.fonts} }`, 'index-max')
|
2019-08-28 02:17:10 +10:00
|
|
|
body.classList.remove('hidden')
|
2017-11-18 02:24:42 +11:00
|
|
|
}
|
|
|
|
|
2020-01-13 11:08:39 +11:00
|
|
|
export const getCssShadow = (input, usesDropShadow) => {
|
2018-11-23 18:17:01 +11:00
|
|
|
if (input.length === 0) {
|
|
|
|
return 'none'
|
|
|
|
}
|
|
|
|
|
2018-12-02 16:47:55 +11:00
|
|
|
return input
|
|
|
|
.filter(_ => usesDropShadow ? _.inset : _)
|
|
|
|
.map((shad) => [
|
|
|
|
shad.x,
|
|
|
|
shad.y,
|
|
|
|
shad.blur,
|
|
|
|
shad.spread
|
|
|
|
].map(_ => _ + 'px').concat([
|
|
|
|
getCssColor(shad.color, shad.alpha),
|
|
|
|
shad.inset ? 'inset' : ''
|
|
|
|
]).join(' ')).join(', ')
|
2018-11-19 12:40:25 +11:00
|
|
|
}
|
|
|
|
|
2018-12-01 00:39:07 +11:00
|
|
|
const getCssShadowFilter = (input) => {
|
|
|
|
if (input.length === 0) {
|
|
|
|
return 'none'
|
|
|
|
}
|
|
|
|
|
|
|
|
return input
|
|
|
|
// drop-shadow doesn't support inset or spread
|
2018-12-02 23:10:18 +11:00
|
|
|
.filter((shad) => !shad.inset && Number(shad.spread) === 0)
|
2018-12-01 00:39:07 +11:00
|
|
|
.map((shad) => [
|
|
|
|
shad.x,
|
|
|
|
shad.y,
|
|
|
|
// drop-shadow's blur is twice as strong compared to box-shadow
|
|
|
|
shad.blur / 2
|
|
|
|
].map(_ => _ + 'px').concat([
|
|
|
|
getCssColor(shad.color, shad.alpha)
|
2018-12-02 16:47:55 +11:00
|
|
|
]).join(' '))
|
|
|
|
.map(_ => `drop-shadow(${_})`)
|
|
|
|
.join(' ')
|
2018-12-01 00:39:07 +11:00
|
|
|
}
|
|
|
|
|
2020-01-12 13:00:41 +11:00
|
|
|
export const generateColors = (themeData) => {
|
2020-01-17 05:53:05 +11:00
|
|
|
const sourceColors = themeData.colors || themeData
|
2018-10-03 04:43:58 +10:00
|
|
|
|
2020-01-07 07:55:14 +11:00
|
|
|
const isLightOnDark = convert(sourceColors.bg).hsl.l < convert(sourceColors.text).hsl.l
|
2018-10-08 06:03:34 +11:00
|
|
|
const mod = isLightOnDark ? 1 : -1
|
|
|
|
|
2020-01-17 05:53:05 +11:00
|
|
|
const { colors, opacity } = getColors(sourceColors, themeData.opacity || {}, mod)
|
2018-10-03 04:43:58 +10:00
|
|
|
|
|
|
|
const htmlColors = Object.entries(colors)
|
2019-07-05 17:02:14 +10:00
|
|
|
.reduce((acc, [k, v]) => {
|
|
|
|
if (!v) return acc
|
|
|
|
acc.solid[k] = rgb2hex(v)
|
2020-01-12 12:44:06 +11:00
|
|
|
acc.complete[k] = typeof v.a === 'undefined' ? rgb2hex(v) : rgba2css(v)
|
2019-07-05 17:02:14 +10:00
|
|
|
return acc
|
|
|
|
}, { complete: {}, solid: {} })
|
2018-10-03 04:43:58 +10:00
|
|
|
return {
|
2018-11-20 04:22:46 +11:00
|
|
|
rules: {
|
|
|
|
colors: Object.entries(htmlColors.complete)
|
|
|
|
.filter(([k, v]) => v)
|
|
|
|
.map(([k, v]) => `--${k}: ${v}`)
|
|
|
|
.join(';')
|
|
|
|
},
|
2018-10-05 01:16:14 +10:00
|
|
|
theme: {
|
2018-10-08 06:03:34 +11:00
|
|
|
colors: htmlColors.solid,
|
2018-11-20 04:22:46 +11:00
|
|
|
opacity
|
2020-01-20 09:34:49 +11:00
|
|
|
},
|
|
|
|
mod
|
2018-11-20 04:22:46 +11:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-01-12 13:00:41 +11:00
|
|
|
export const generateRadii = (input) => {
|
2018-11-23 17:14:52 +11:00
|
|
|
let inputRadii = input.radii || {}
|
|
|
|
// v1 -> v2
|
|
|
|
if (typeof input.btnRadius !== 'undefined') {
|
|
|
|
inputRadii = Object
|
|
|
|
.entries(input)
|
|
|
|
.filter(([k, v]) => k.endsWith('Radius'))
|
|
|
|
.reduce((acc, e) => { acc[e[0].split('Radius')[0]] = e[1]; return acc }, {})
|
|
|
|
}
|
|
|
|
const radii = Object.entries(inputRadii).filter(([k, v]) => v).reduce((acc, [k, v]) => {
|
|
|
|
acc[k] = v
|
2018-11-21 11:14:59 +11:00
|
|
|
return acc
|
|
|
|
}, {
|
2018-11-20 04:22:46 +11:00
|
|
|
btn: 4,
|
|
|
|
input: 4,
|
2018-11-23 19:36:36 +11:00
|
|
|
checkbox: 2,
|
2018-11-20 04:22:46 +11:00
|
|
|
panel: 10,
|
|
|
|
avatar: 5,
|
|
|
|
avatarAlt: 50,
|
|
|
|
tooltip: 2,
|
2018-11-21 11:14:59 +11:00
|
|
|
attachment: 5
|
|
|
|
})
|
2018-11-20 04:22:46 +11:00
|
|
|
|
|
|
|
return {
|
|
|
|
rules: {
|
|
|
|
radii: Object.entries(radii).filter(([k, v]) => v).map(([k, v]) => `--${k}Radius: ${v}px`).join(';')
|
|
|
|
},
|
|
|
|
theme: {
|
2018-10-05 01:16:14 +10:00
|
|
|
radii
|
|
|
|
}
|
2018-10-03 04:43:58 +10:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-01-12 13:00:41 +11:00
|
|
|
export const generateFonts = (input) => {
|
2018-11-26 05:48:16 +11:00
|
|
|
const fonts = Object.entries(input.fonts || {}).filter(([k, v]) => v).reduce((acc, [k, v]) => {
|
|
|
|
acc[k] = Object.entries(v).filter(([k, v]) => v).reduce((acc, [k, v]) => {
|
|
|
|
acc[k] = v
|
|
|
|
return acc
|
|
|
|
}, acc[k])
|
|
|
|
return acc
|
|
|
|
}, {
|
|
|
|
interface: {
|
|
|
|
family: 'sans-serif'
|
|
|
|
},
|
|
|
|
input: {
|
|
|
|
family: 'inherit'
|
|
|
|
},
|
|
|
|
post: {
|
|
|
|
family: 'inherit'
|
|
|
|
},
|
|
|
|
postCode: {
|
|
|
|
family: 'monospace'
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
return {
|
|
|
|
rules: {
|
|
|
|
fonts: Object
|
|
|
|
.entries(fonts)
|
|
|
|
.filter(([k, v]) => v)
|
|
|
|
.map(([k, v]) => `--${k}Font: ${v.family}`).join(';')
|
|
|
|
},
|
|
|
|
theme: {
|
|
|
|
fonts
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-01-20 09:34:49 +11:00
|
|
|
const border = (top, shadow) => ({
|
|
|
|
x: 0,
|
|
|
|
y: top ? 1 : -1,
|
|
|
|
blur: 0,
|
|
|
|
spread: 0,
|
|
|
|
color: shadow ? '#000000' : '#FFFFFF',
|
|
|
|
alpha: 0.2,
|
|
|
|
inset: true
|
|
|
|
})
|
|
|
|
const buttonInsetFakeBorders = [border(true, false), border(false, true)]
|
|
|
|
const inputInsetFakeBorders = [border(true, true), border(false, false)]
|
|
|
|
const hoverGlow = {
|
|
|
|
x: 0,
|
|
|
|
y: 0,
|
|
|
|
blur: 4,
|
|
|
|
spread: 0,
|
|
|
|
color: '--faint',
|
|
|
|
alpha: 1
|
|
|
|
}
|
|
|
|
|
|
|
|
export const DEFAULT_SHADOWS = {
|
|
|
|
panel: [{
|
|
|
|
x: 1,
|
|
|
|
y: 1,
|
|
|
|
blur: 4,
|
2018-11-22 05:32:51 +11:00
|
|
|
spread: 0,
|
2020-01-20 09:34:49 +11:00
|
|
|
color: '#000000',
|
|
|
|
alpha: 0.6
|
|
|
|
}],
|
|
|
|
topBar: [{
|
2018-11-22 05:23:07 +11:00
|
|
|
x: 0,
|
2018-11-23 08:24:16 +11:00
|
|
|
y: 0,
|
|
|
|
blur: 4,
|
2018-11-22 05:32:51 +11:00
|
|
|
spread: 0,
|
2020-01-20 09:34:49 +11:00
|
|
|
color: '#000000',
|
|
|
|
alpha: 0.6
|
|
|
|
}],
|
|
|
|
popup: [{
|
|
|
|
x: 2,
|
|
|
|
y: 2,
|
|
|
|
blur: 3,
|
|
|
|
spread: 0,
|
|
|
|
color: '#000000',
|
|
|
|
alpha: 0.5
|
|
|
|
}],
|
|
|
|
avatar: [{
|
|
|
|
x: 0,
|
|
|
|
y: 1,
|
|
|
|
blur: 8,
|
|
|
|
spread: 0,
|
|
|
|
color: '#000000',
|
|
|
|
alpha: 0.7
|
|
|
|
}],
|
|
|
|
avatarStatus: [],
|
|
|
|
panelHeader: [],
|
|
|
|
button: [{
|
|
|
|
x: 0,
|
|
|
|
y: 0,
|
|
|
|
blur: 2,
|
|
|
|
spread: 0,
|
|
|
|
color: '#000000',
|
2018-11-23 08:24:16 +11:00
|
|
|
alpha: 1
|
2020-01-20 09:34:49 +11:00
|
|
|
}, ...buttonInsetFakeBorders],
|
|
|
|
buttonHover: [hoverGlow, ...buttonInsetFakeBorders],
|
|
|
|
buttonPressed: [hoverGlow, ...inputInsetFakeBorders],
|
|
|
|
input: [...inputInsetFakeBorders, {
|
|
|
|
x: 0,
|
|
|
|
y: 0,
|
|
|
|
blur: 2,
|
|
|
|
inset: true,
|
|
|
|
spread: 0,
|
|
|
|
color: '#000000',
|
|
|
|
alpha: 1
|
|
|
|
}]
|
|
|
|
}
|
|
|
|
export const generateShadows = (input, colors, mod) => {
|
|
|
|
const shadows = Object.entries({
|
|
|
|
...DEFAULT_SHADOWS,
|
2018-11-20 04:22:46 +11:00
|
|
|
...(input.shadows || {})
|
2020-01-20 10:31:54 +11:00
|
|
|
}).reduce((shadowsAcc, [slotName, shadowDefs]) => {
|
|
|
|
const newShadow = shadowDefs.reduce((shadowAcc, def) => [
|
2020-01-20 09:34:49 +11:00
|
|
|
...shadowAcc,
|
|
|
|
{
|
|
|
|
...def,
|
|
|
|
color: rgb2hex(computeDynamicColor(
|
|
|
|
def.color,
|
|
|
|
(variableSlot) => convert(colors[variableSlot]).rgb,
|
|
|
|
mod
|
|
|
|
))
|
|
|
|
}
|
|
|
|
], [])
|
|
|
|
return { ...shadowsAcc, [slotName]: newShadow }
|
|
|
|
}, {})
|
2018-11-20 04:22:46 +11:00
|
|
|
|
|
|
|
return {
|
|
|
|
rules: {
|
2018-12-01 00:39:07 +11:00
|
|
|
shadows: Object
|
|
|
|
.entries(shadows)
|
|
|
|
// TODO for v2.1: if shadow doesn't have non-inset shadows with spread > 0 - optionally
|
|
|
|
// convert all non-inset shadows into filter: drop-shadow() to boost performance
|
2018-12-02 16:47:55 +11:00
|
|
|
.map(([k, v]) => [
|
|
|
|
`--${k}Shadow: ${getCssShadow(v)}`,
|
|
|
|
`--${k}ShadowFilter: ${getCssShadowFilter(v)}`,
|
|
|
|
`--${k}ShadowInset: ${getCssShadow(v, true)}`
|
|
|
|
].join(';'))
|
2018-12-01 00:39:07 +11:00
|
|
|
.join(';')
|
2018-11-20 04:22:46 +11:00
|
|
|
},
|
|
|
|
theme: {
|
|
|
|
shadows
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-01-12 13:00:41 +11:00
|
|
|
export const composePreset = (colors, radii, shadows, fonts) => {
|
2018-11-20 04:22:46 +11:00
|
|
|
return {
|
|
|
|
rules: {
|
|
|
|
...shadows.rules,
|
|
|
|
...colors.rules,
|
2018-11-26 05:48:16 +11:00
|
|
|
...radii.rules,
|
|
|
|
...fonts.rules
|
2018-11-20 04:22:46 +11:00
|
|
|
},
|
|
|
|
theme: {
|
|
|
|
...shadows.theme,
|
|
|
|
...colors.theme,
|
2018-11-26 05:48:16 +11:00
|
|
|
...radii.theme,
|
|
|
|
...fonts.theme
|
2018-11-20 04:22:46 +11:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-01-20 09:34:49 +11:00
|
|
|
export const generatePreset = (input) => {
|
|
|
|
const colors = generateColors(input)
|
|
|
|
return composePreset(
|
|
|
|
colors,
|
|
|
|
generateRadii(input),
|
|
|
|
generateShadows(input, colors.theme.colors, colors.mod),
|
|
|
|
generateFonts(input)
|
|
|
|
)
|
|
|
|
}
|
2018-11-20 04:22:46 +11:00
|
|
|
|
2020-01-12 13:00:41 +11:00
|
|
|
export const getThemes = () => {
|
2018-12-11 09:38:20 +11:00
|
|
|
return window.fetch('/static/styles.json')
|
2017-11-18 02:24:42 +11:00
|
|
|
.then((data) => data.json())
|
|
|
|
.then((themes) => {
|
2020-01-17 09:27:46 +11:00
|
|
|
return Object.entries(themes).map(([k, v]) => {
|
|
|
|
let promise = null
|
2018-12-11 09:38:20 +11:00
|
|
|
if (typeof v === 'object') {
|
2020-01-17 09:27:46 +11:00
|
|
|
promise = Promise.resolve(v)
|
2018-12-11 09:38:20 +11:00
|
|
|
} else if (typeof v === 'string') {
|
2020-01-17 09:27:46 +11:00
|
|
|
promise = window.fetch(v)
|
2018-12-11 09:38:20 +11:00
|
|
|
.then((data) => data.json())
|
|
|
|
.catch((e) => {
|
|
|
|
console.error(e)
|
2020-01-17 09:27:46 +11:00
|
|
|
return null
|
2018-12-11 09:38:20 +11:00
|
|
|
})
|
|
|
|
}
|
2020-01-17 09:27:46 +11:00
|
|
|
return [k, promise]
|
|
|
|
})
|
2018-12-11 09:38:20 +11:00
|
|
|
})
|
|
|
|
.then((promises) => {
|
|
|
|
return promises
|
|
|
|
.reduce((acc, [k, v]) => {
|
|
|
|
acc[k] = v
|
|
|
|
return acc
|
|
|
|
}, {})
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2020-01-20 10:31:54 +11:00
|
|
|
/**
|
|
|
|
* This handles compatibility issues when importing v2 theme's shadows to current format
|
|
|
|
*
|
|
|
|
* Back in v2 shadows allowed you to use dynamic colors however those used pure CSS3 variables
|
|
|
|
*/
|
|
|
|
export const shadows2to3 = (shadows) => {
|
|
|
|
return Object.entries(shadows).reduce((shadowsAcc, [slotName, shadowDefs]) => {
|
2020-01-22 09:37:19 +11:00
|
|
|
const isDynamic = ({ color }) => color.startsWith('--')
|
2020-01-20 10:31:54 +11:00
|
|
|
const newShadow = shadowDefs.reduce((shadowAcc, def) => [
|
|
|
|
...shadowAcc,
|
|
|
|
{
|
|
|
|
...def,
|
|
|
|
alpha: isDynamic(def) ? 1 : def.alpha
|
|
|
|
}
|
|
|
|
], [])
|
|
|
|
return { ...shadowsAcc, [slotName]: newShadow }
|
|
|
|
}, {})
|
|
|
|
}
|
|
|
|
|
2020-01-22 09:37:19 +11:00
|
|
|
export const getPreset = (val) => {
|
2020-01-17 09:27:46 +11:00
|
|
|
return getThemes()
|
2020-01-20 09:34:49 +11:00
|
|
|
.then((themes) => themes[val] ? themes[val] : themes['pleroma-dark'])
|
2020-01-17 09:27:46 +11:00
|
|
|
.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 }
|
|
|
|
}
|
|
|
|
|
2020-01-22 09:37:19 +11:00
|
|
|
return { theme: data, source: theme.source }
|
2020-01-17 09:27:46 +11:00
|
|
|
})
|
2017-01-17 03:44:26 +11:00
|
|
|
}
|
2020-01-22 09:37:19 +11:00
|
|
|
|
|
|
|
export const setPreset = (val) => getPreset(val).then(data => applyTheme(data.theme))
|