Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F85806188
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
17 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/src/services/style_setter/style_setter.js b/src/services/style_setter/style_setter.js
index c8610507e8..fbdcf56231 100644
--- a/src/services/style_setter/style_setter.js
+++ b/src/services/style_setter/style_setter.js
@@ -1,411 +1,411 @@
import { convert } from 'chromatism'
import { rgb2hex, hex2rgb, rgba2css, getCssColor, relativeLuminance } from '../color_convert/color_convert.js'
import { getColors, computeDynamicColor, getOpacitySlot } from '../theme_data/theme_data.service.js'
export const applyTheme = (input) => {
const { rules } = generatePreset(input)
const head = document.head
const body = document.body
body.classList.add('hidden')
const styleEl = document.createElement('style')
head.appendChild(styleEl)
const styleSheet = styleEl.sheet
styleSheet.toString()
styleSheet.insertRule(`body { ${rules.radii} }`, 'index-max')
styleSheet.insertRule(`body { ${rules.colors} }`, 'index-max')
styleSheet.insertRule(`body { ${rules.shadows} }`, 'index-max')
styleSheet.insertRule(`body { ${rules.fonts} }`, 'index-max')
body.classList.remove('hidden')
}
export const getCssShadow = (input, usesDropShadow) => {
if (input.length === 0) {
return 'none'
}
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(', ')
}
const getCssShadowFilter = (input) => {
if (input.length === 0) {
return 'none'
}
return input
// drop-shadow doesn't support inset or spread
.filter((shad) => !shad.inset && Number(shad.spread) === 0)
.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)
]).join(' '))
.map(_ => `drop-shadow(${_})`)
.join(' ')
}
export const generateColors = (themeData) => {
const sourceColors = !themeData.themeEngineVersion
? colors2to3(themeData.colors || themeData)
: themeData.colors || themeData
const { colors, opacity } = getColors(sourceColors, themeData.opacity || {})
const htmlColors = Object.entries(colors)
.reduce((acc, [k, v]) => {
if (!v) return acc
acc.solid[k] = rgb2hex(v)
acc.complete[k] = typeof v.a === 'undefined' ? rgb2hex(v) : rgba2css(v)
return acc
}, { complete: {}, solid: {} })
return {
rules: {
colors: Object.entries(htmlColors.complete)
.filter(([k, v]) => v)
.map(([k, v]) => `--${k}: ${v}`)
.join(';')
},
theme: {
colors: htmlColors.solid,
opacity
}
}
}
export const generateRadii = (input) => {
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
return acc
}, {
btn: 4,
input: 4,
checkbox: 2,
panel: 10,
avatar: 5,
avatarAlt: 50,
tooltip: 2,
attachment: 5
})
return {
rules: {
radii: Object.entries(radii).filter(([k, v]) => v).map(([k, v]) => `--${k}Radius: ${v}px`).join(';')
},
theme: {
radii
}
}
}
export const generateFonts = (input) => {
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
}
}
}
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,
spread: 0,
color: '#000000',
alpha: 0.6
}],
topBar: [{
x: 0,
y: 0,
blur: 4,
spread: 0,
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',
alpha: 1
}, ...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) => {
// TODO this is a small hack for `mod` to work with shadows
// this is used to get the "context" of shadow, i.e. for `mod` properly depend on background color of element
const hackContextDict = {
button: 'btn',
panel: 'bg',
top: 'topBar',
popup: 'popover',
avatar: 'bg',
panelHeader: 'panel',
input: 'input'
}
const inputShadows = input.shadows && !input.themeEngineVersion
? shadows2to3(input.shadows, input.opacity)
: input.shadows || {}
const shadows = Object.entries({
...DEFAULT_SHADOWS,
...inputShadows
}).reduce((shadowsAcc, [slotName, shadowDefs]) => {
const slotFirstWord = slotName.replace(/[A-Z].*$/, '')
const colorSlotName = hackContextDict[slotFirstWord]
const isLightOnDark = relativeLuminance(convert(colors[colorSlotName]).rgb) < 0.5
const mod = isLightOnDark ? 1 : -1
const newShadow = shadowDefs.reduce((shadowAcc, def) => [
...shadowAcc,
{
...def,
color: rgb2hex(computeDynamicColor(
def.color,
(variableSlot) => convert(colors[variableSlot]).rgb,
mod
))
}
], [])
return { ...shadowsAcc, [slotName]: newShadow }
}, {})
return {
rules: {
shadows: Object
.entries(shadows)
- // TODO for v2.1: if shadow doesn't have non-inset shadows with spread > 0 - optionally
+ // TODO for v2.2: if shadow doesn't have non-inset shadows with spread > 0 - optionally
// convert all non-inset shadows into filter: drop-shadow() to boost performance
.map(([k, v]) => [
`--${k}Shadow: ${getCssShadow(v)}`,
`--${k}ShadowFilter: ${getCssShadowFilter(v)}`,
`--${k}ShadowInset: ${getCssShadow(v, true)}`
].join(';'))
.join(';')
},
theme: {
shadows
}
}
}
export const composePreset = (colors, radii, shadows, fonts) => {
return {
rules: {
...shadows.rules,
...colors.rules,
...radii.rules,
...fonts.rules
},
theme: {
...shadows.theme,
...colors.theme,
...radii.theme,
...fonts.theme
}
}
}
export const generatePreset = (input) => {
const colors = generateColors(input)
return composePreset(
colors,
generateRadii(input),
generateShadows(input, colors.theme.colors, colors.mod),
generateFonts(input)
)
}
export const getThemes = () => {
const cache = 'no-store'
return window.fetch('/static/styles.json', { cache })
.then((data) => data.json())
.then((themes) => {
return Object.entries(themes).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
}, {})
})
}
export const colors2to3 = (colors) => {
return Object.entries(colors).reduce((acc, [slotName, color]) => {
const btnPositions = ['', 'Panel', 'TopBar']
switch (slotName) {
case 'lightBg':
return { ...acc, highlight: color }
case 'btnText':
return {
...acc,
...btnPositions
.reduce(
(statePositionAcc, position) =>
({ ...statePositionAcc, ['btn' + position + 'Text']: color })
, {}
)
}
default:
return { ...acc, [slotName]: color }
}
}, {})
}
/**
* 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, opacity) => {
return Object.entries(shadows).reduce((shadowsAcc, [slotName, shadowDefs]) => {
const isDynamic = ({ color }) => color.startsWith('--')
const getOpacity = ({ color }) => opacity[getOpacitySlot(color.substring(2).split(',')[0])]
const newShadow = shadowDefs.reduce((shadowAcc, def) => [
...shadowAcc,
{
...def,
alpha: isDynamic(def) ? getOpacity(def) || 1 : def.alpha
}
], [])
return { ...shadowsAcc, [slotName]: newShadow }
}, {})
}
export const getPreset = (val) => {
return getThemes()
.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 }
})
}
export const setPreset = (val) => getPreset(val).then(data => applyTheme(data.theme))
diff --git a/static/styles.json b/static/styles.json
index 54c0335f74..3349a8377c 100644
--- a/static/styles.json
+++ b/static/styles.json
@@ -1,17 +1,17 @@
{
"pleroma-dark": [ "Pleroma Dark", "#121a24", "#182230", "#b9b9ba", "#d8a070", "#d31014", "#0fa00f", "#0095ff", "#ffa500" ],
"pleroma-light": [ "Pleroma Light", "#f2f4f6", "#dbe0e8", "#304055", "#f86f0f", "#d31014", "#0fa00f", "#0095ff", "#ffa500" ],
"pleroma-amoled": [ "Pleroma Dark AMOLED", "#000000", "#111111", "#b0b0b1", "#d8a070", "#aa0000", "#0fa00f", "#0095ff", "#d59500"],
"classic-dark": [ "Classic Dark", "#161c20", "#282e32", "#b9b9b9", "#baaa9c", "#d31014", "#0fa00f", "#0095ff", "#ffa500" ],
"bird": [ "Bird", "#f8fafd", "#e6ecf0", "#14171a", "#0084b8", "#e0245e", "#17bf63", "#1b95e0", "#fab81e"],
"ir-black": [ "Ir Black", "#000000", "#242422", "#b5b3aa", "#ff6c60", "#FF6C60", "#A8FF60", "#96CBFE", "#FFFFB6" ],
"monokai": [ "Monokai", "#272822", "#383830", "#f8f8f2", "#f92672", "#F92672", "#a6e22e", "#66d9ef", "#f4bf75" ],
"redmond-xx": "/static/themes/redmond-xx.json",
"redmond-xx-se": "/static/themes/redmond-xx-se.json",
"redmond-xxi": "/static/themes/redmond-xxi.json",
"breezy-dark": "/static/themes/breezy-dark.json",
"breezy-light": "/static/themes/breezy-light.json",
"mammal": "/static/themes/mammal.json",
- "kenomo": "/static/themes/kenomo.json"
+ "paper": "/static/themes/paper.json"
}
diff --git a/static/themes/kenomo.json b/static/themes/kenomo.json
deleted file mode 100644
index 98ddf97436..0000000000
--- a/static/themes/kenomo.json
+++ /dev/null
@@ -1,71 +0,0 @@
-{
- "_pleroma_theme_version": 2,
- "name": "Kenomo",
- "source": {
- "themeEngineVersion": 3,
- "fonts": {},
- "shadows": {
- "panel": [],
- "topBar": [],
- "button": [
- {
- "x": 0,
- "y": 1,
- "blur": 0,
- "spread": 0,
- "color": "#FFFFFF",
- "alpha": 0.2,
- "inset": true
- },
- {
- "x": 0,
- "y": -1,
- "blur": 0,
- "spread": 0,
- "color": "#000000",
- "alpha": 0.2,
- "inset": true
- }
- ],
- "input": [
- {
- "x": 0,
- "y": "0",
- "blur": 0,
- "spread": "1",
- "color": "#576574",
- "alpha": "1",
- "inset": true
- }
- ]
- },
- "opacity": {
- "underlay": "1",
- "border": "0"
- },
- "colors": {
- "bg": "#ffffff",
- "fg": "#f6f6f6",
- "text": "#494949",
- "underlay": "#ffffff",
- "link": "#818181",
- "accent": "#818181",
- "cBlue": "#2e86de",
- "cRed": "#c96248",
- "cGreen": "#0fa00f",
- "cOrange": "#aa7623",
- "postLink": "#2e86de",
- "border": "#ffffff",
- "icon": "#8a8a8a",
- "panel": "transparent",
- "topBarText": "#4b4b4b",
- "tab": "--btn,-30",
- "btn": "#576574"
- },
- "radii": {
- "panel": "0",
- "avatar": "6",
- "avatarAlt": "6"
- }
- }
-}
diff --git a/static/themes/paper.json b/static/themes/paper.json
new file mode 100644
index 0000000000..a3b90a0a11
--- /dev/null
+++ b/static/themes/paper.json
@@ -0,0 +1,172 @@
+{
+ "_pleroma_theme_version": 2,
+ "name": "Paper",
+ "source": {
+ "themeEngineVersion": 3,
+ "fonts": {},
+ "shadows": {
+ "panel": [
+ {
+ "x": "0",
+ "y": "2",
+ "blur": "9",
+ "spread": 0,
+ "inset": false,
+ "color": "#668bb2",
+ "alpha": "0.1"
+ },
+ {
+ "x": "0",
+ "y": "1",
+ "blur": "2",
+ "spread": "-1",
+ "inset": false,
+ "color": "#668bb2",
+ "alpha": "0.1"
+ }
+ ],
+ "topBar": [
+ {
+ "x": 0,
+ "y": "3",
+ "blur": "8",
+ "spread": 0,
+ "inset": false,
+ "color": "#3e618e",
+ "alpha": "0.1"
+ },
+ {
+ "x": 0,
+ "y": "1",
+ "blur": "4",
+ "spread": 0,
+ "inset": false,
+ "color": "#3e618e",
+ "alpha": "0.1"
+ }
+ ],
+ "button": [
+ {
+ "x": 0,
+ "y": "2",
+ "blur": "5",
+ "spread": 0,
+ "color": "#463f78",
+ "alpha": "0.1",
+ "inset": false
+ }
+ ],
+ "input": [
+ {
+ "x": 0,
+ "y": "1",
+ "blur": "2",
+ "spread": 0,
+ "inset": true,
+ "color": "#6277b7",
+ "alpha": "0.1"
+ }
+ ],
+ "buttonHover": [
+ {
+ "x": 0,
+ "y": "2",
+ "blur": "5",
+ "spread": 0,
+ "color": "#494949",
+ "alpha": "0.1"
+ },
+ {
+ "x": 0,
+ "y": "2",
+ "blur": "0",
+ "spread": "20",
+ "color": "#ffffff",
+ "alpha": "1",
+ "inset": true
+ }
+ ],
+ "buttonPressed": [
+ {
+ "x": 0,
+ "y": 0,
+ "blur": "4",
+ "spread": "0",
+ "color": "#494949",
+ "alpha": "0.8",
+ "inset": false
+ }
+ ],
+ "avatarStatus": [
+ {
+ "x": "0",
+ "y": "2",
+ "blur": "4",
+ "spread": "0",
+ "inset": false,
+ "color": "#3e618e",
+ "alpha": "0.1"
+ }
+ ],
+ "avatar": [
+ {
+ "x": 0,
+ "y": "2",
+ "blur": "5",
+ "spread": "0",
+ "color": "#3e618e",
+ "alpha": "0.9"
+ }
+ ],
+ "popup": [
+ {
+ "x": "0",
+ "y": "3",
+ "blur": "11",
+ "spread": 0,
+ "color": "#668bb2",
+ "alpha": "0.2"
+ },
+ {
+ "x": "0",
+ "y": "2",
+ "blur": "3",
+ "spread": "-1",
+ "color": "#668bb2",
+ "alpha": "0.2"
+ }
+ ]
+ },
+ "opacity": {
+ "underlay": "1",
+ "border": "0"
+ },
+ "colors": {
+ "bg": "#ffffff",
+ "fg": "#f6f6f6",
+ "text": "#494949",
+ "underlay": "#ffffff",
+ "link": "#788ca1",
+ "accent": "#97a0aa",
+ "cBlue": "#788ca1",
+ "cRed": "#eed7ce",
+ "cGreen": "#788ca1",
+ "cOrange": "#788ca1",
+ "postLink": "#788ca1",
+ "border": "#ffffff",
+ "icon": "#b6c9c4",
+ "panel": "#ffffff",
+ "topBarText": "#4b4b4b"
+ },
+ "radii": {
+ "btn": "0",
+ "input": "0",
+ "checkbox": "0",
+ "panel": "0",
+ "avatar": "2",
+ "avatarAlt": "2",
+ "tooltip": "0",
+ "attachment": "0"
+ }
+ }
+}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Mon, Oct 12, 3:24 PM (1 d, 17 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1786344
Default Alt Text
(17 KB)
Attached To
Mode
rPUFE pleroma-fe-upstream
Attached
Detach File
Event Timeline
Log In to Comment