akkoma-fe/src/components/style_switcher/style_switcher.js

535 lines
15 KiB
JavaScript
Raw Normal View History

2018-11-19 15:15:27 +00:00
import { set, delete as del } from 'vue'
2020-01-12 02:00:41 +00:00
import {
rgb2hex,
hex2rgb,
getContrastRatioLayers
2020-01-12 02:00:41 +00:00
} from '../../services/color_convert/color_convert.js'
import {
generateColors,
generateShadows,
generateRadii,
generateFonts,
composePreset,
2020-01-12 02:00:41 +00:00
getThemes
} from '../../services/style_setter/style_setter.js'
import {
CURRENT_VERSION,
SLOT_INHERITANCE,
2020-01-16 18:53:05 +00:00
OPACITIES,
getLayers
} from '../../services/theme_data/theme_data.service.js'
import ColorInput from '../color_input/color_input.vue'
2018-11-21 00:14:59 +00:00
import RangeInput from '../range_input/range_input.vue'
import OpacityInput from '../opacity_input/opacity_input.vue'
import ShadowControl from '../shadow_control/shadow_control.vue'
2018-11-25 18:48:16 +00:00
import FontControl from '../font_control/font_control.vue'
2018-10-09 21:07:28 +00:00
import ContrastRatio from '../contrast_ratio/contrast_ratio.vue'
import TabSwitcher from '../tab_switcher/tab_switcher.js'
import Preview from './preview.vue'
import ExportImport from '../export_import/export_import.vue'
import Checkbox from '../checkbox/checkbox.vue'
2018-11-20 17:58:20 +00:00
// List of color values used in v1
2018-11-19 17:22:46 +00:00
const v1OnlyNames = [
'bg',
'fg',
'text',
'link',
'cRed',
'cGreen',
'cBlue',
'cOrange'
].map(_ => _ + 'ColorLocal')
const colorConvert = (color) => {
2020-01-12 23:56:29 +00:00
if (color.startsWith('--') || color === 'transparent') {
return color
} else {
return hex2rgb(color)
}
}
2017-01-16 17:57:03 +00:00
export default {
2017-02-17 17:21:02 +00:00
data () {
return {
availableStyles: [],
selected: this.$store.getters.mergedConfig.theme,
previewShadows: {},
previewColors: {},
previewRadii: {},
2018-11-25 18:48:16 +00:00
previewFonts: {},
shadowsInvalid: true,
colorsInvalid: true,
radiiInvalid: true,
2018-12-11 13:36:06 +00:00
keepColor: false,
keepShadows: false,
keepOpacity: false,
keepRoundness: false,
2018-11-25 18:48:16 +00:00
keepFonts: false,
...Object.keys(SLOT_INHERITANCE)
.map(key => [key, ''])
.reduce((acc, [key, val]) => ({ ...acc, [ key + 'ColorLocal' ]: val }), {}),
2018-10-07 16:59:22 +00:00
2020-01-16 18:53:05 +00:00
...Object.keys(OPACITIES)
.map(key => console.log(key) || [key, ''])
.reduce((acc, [key, val]) => ({ ...acc, [ key + 'OpacityLocal' ]: val }), {}),
2018-11-19 17:22:46 +00:00
shadowSelected: undefined,
shadowsLocal: {},
2018-11-25 18:48:16 +00:00
fontsLocal: {},
2018-11-19 17:22:46 +00:00
2018-04-07 23:39:39 +00:00
btnRadiusLocal: '',
2018-04-15 04:25:59 +00:00
inputRadiusLocal: '',
2018-11-23 04:28:53 +00:00
checkboxRadiusLocal: '',
2018-04-07 23:39:39 +00:00
panelRadiusLocal: '',
avatarRadiusLocal: '',
avatarAltRadiusLocal: '',
attachmentRadiusLocal: '',
tooltipRadiusLocal: ''
2017-02-17 17:21:02 +00:00
}
},
2017-01-16 17:57:03 +00:00
created () {
const self = this
2018-12-10 22:38:20 +00:00
getThemes().then((themesComplete) => {
self.availableStyles = themesComplete
})
},
mounted () {
this.normalizeLocalState(this.$store.getters.mergedConfig.customTheme)
if (typeof this.shadowSelected === 'undefined') {
this.shadowSelected = this.shadowsAvailable[0]
}
},
computed: {
selectedVersion () {
return Array.isArray(this.selected) ? 1 : 2
},
2018-11-19 17:22:46 +00:00
currentColors () {
return Object.keys(SLOT_INHERITANCE)
.map(key => [key, this[key + 'ColorLocal']])
.reduce((acc, [key, val]) => ({ ...acc, [ key ]: val }), {})
},
2018-11-19 17:22:46 +00:00
currentOpacity () {
2020-01-16 18:53:05 +00:00
return Object.keys(OPACITIES)
.map(key => console.log(key) || [key, this[key + 'OpacityLocal']])
2020-01-13 00:08:39 +00:00
.reduce((acc, [key, val]) => ({ ...acc, [ key ]: val }), {})
2018-11-19 17:22:46 +00:00
},
currentRadii () {
return {
btn: this.btnRadiusLocal,
input: this.inputRadiusLocal,
2018-11-23 04:28:53 +00:00
checkbox: this.checkboxRadiusLocal,
2018-11-19 17:22:46 +00:00
panel: this.panelRadiusLocal,
avatar: this.avatarRadiusLocal,
avatarAlt: this.avatarAltRadiusLocal,
tooltip: this.tooltipRadiusLocal,
attachment: this.attachmentRadiusLocal
}
},
preview () {
2018-11-25 18:48:16 +00:00
return composePreset(this.previewColors, this.previewRadii, this.previewShadows, this.previewFonts)
2018-11-19 17:22:46 +00:00
},
2018-10-04 15:16:14 +00:00
previewTheme () {
2018-11-25 18:48:16 +00:00
if (!this.preview.theme.colors) return { colors: {}, opacity: {}, radii: {}, shadows: {}, fonts: {} }
2018-10-04 15:16:14 +00:00
return this.preview.theme
},
// This needs optimization maybe
2018-10-09 21:07:28 +00:00
previewContrast () {
2018-11-19 17:22:46 +00:00
if (!this.previewTheme.colors.bg) return {}
2018-10-09 21:07:28 +00:00
const colors = this.previewTheme.colors
const opacity = this.previewTheme.opacity
if (!colors.bg) return {}
2018-10-09 21:07:28 +00:00
const hints = (ratio) => ({
text: ratio.toPrecision(3) + ':1',
// AA level, AAA level
aa: ratio >= 4.5,
aaa: ratio >= 7,
// same but for 18pt+ texts
laa: ratio >= 3,
laaa: ratio >= 4.5
})
const colorsConverted = Object.entries(colors).reduce((acc, [key, value]) => ({ ...acc, [key]: colorConvert(value) }), {})
const ratios = Object.entries(SLOT_INHERITANCE).reduce((acc, [key, value]) => {
const slotIsBaseText = key === 'text' || key === 'link'
const slotIsText = slotIsBaseText || (
typeof value === 'object' && value !== null && value.textColor
)
if (!slotIsText) return acc
const { layer, variant } = slotIsBaseText ? { layer: 'bg' } : value
const background = variant || layer
const textColors = [
key,
...(background === 'bg' ? ['cRed', 'cGreen', 'cBlue', 'cOrange'] : [])
]
const layers = getLayers(
layer,
variant || layer,
colorsConverted,
opacity
)
return {
...acc,
...textColors.reduce((acc, textColorKey) => {
const newKey = slotIsBaseText
? 'bg' + textColorKey[0].toUpperCase() + textColorKey.slice(1)
: textColorKey
return {
...acc,
[newKey]: getContrastRatioLayers(
colorsConverted[textColorKey],
layers,
colorsConverted[textColorKey]
)
}
}, {})
}
}, {})
2018-10-09 21:07:28 +00:00
return Object.entries(ratios).reduce((acc, [k, v]) => { acc[k] = hints(v); return acc }, {})
},
2018-10-04 15:16:14 +00:00
previewRules () {
2018-11-19 17:22:46 +00:00
if (!this.preview.rules) return ''
2018-11-25 18:48:16 +00:00
return [
...Object.values(this.preview.rules),
'color: var(--text)',
'font-family: var(--interfaceFont, sans-serif)'
].join(';')
},
shadowsAvailable () {
return Object.keys(this.previewTheme.shadows).sort()
},
2018-11-19 15:15:27 +00:00
currentShadowOverriden: {
get () {
2018-11-19 17:22:46 +00:00
return !!this.currentShadow
2018-11-19 15:15:27 +00:00
},
set (val) {
if (val) {
2018-11-19 17:22:46 +00:00
set(this.shadowsLocal, this.shadowSelected, this.currentShadowFallback.map(_ => Object.assign({}, _)))
2018-11-19 15:15:27 +00:00
} else {
del(this.shadowsLocal, this.shadowSelected)
}
}
},
currentShadowFallback () {
return this.previewTheme.shadows[this.shadowSelected]
},
currentShadow: {
get () {
return this.shadowsLocal[this.shadowSelected]
},
set (v) {
set(this.shadowsLocal, this.shadowSelected, v)
}
},
themeValid () {
return !this.shadowsInvalid && !this.colorsInvalid && !this.radiiInvalid
},
exportedTheme () {
2018-12-11 13:36:06 +00:00
const saveEverything = (
!this.keepFonts &&
!this.keepShadows &&
!this.keepOpacity &&
!this.keepRoundness &&
!this.keepColor
)
const source = {
themeEngineVersion: CURRENT_VERSION
}
2018-12-11 13:36:06 +00:00
if (this.keepFonts || saveEverything) {
source.fonts = this.fontsLocal
}
2018-12-11 13:36:06 +00:00
if (this.keepShadows || saveEverything) {
source.shadows = this.shadowsLocal
}
2018-12-11 13:36:06 +00:00
if (this.keepOpacity || saveEverything) {
source.opacity = this.currentOpacity
}
2018-12-11 13:36:06 +00:00
if (this.keepColor || saveEverything) {
source.colors = this.currentColors
}
2018-12-11 13:36:06 +00:00
if (this.keepRoundness || saveEverything) {
source.radii = this.currentRadii
}
const theme = this.previewTheme
return {
// To separate from other random JSON files and possible future source formats
_pleroma_theme_version: 2, theme, source
}
}
},
components: {
ColorInput,
OpacityInput,
RangeInput,
ContrastRatio,
ShadowControl,
FontControl,
TabSwitcher,
Preview,
ExportImport,
Checkbox
},
methods: {
setCustomTheme () {
this.$store.dispatch('setOption', {
name: 'customTheme',
2018-11-19 15:15:27 +00:00
value: {
2018-11-19 17:22:46 +00:00
shadows: this.shadowsLocal,
2018-11-25 18:48:16 +00:00
fonts: this.fontsLocal,
2018-11-19 17:22:46 +00:00
opacity: this.currentOpacity,
colors: this.currentColors,
radii: this.currentRadii
2018-11-19 15:15:27 +00:00
}
})
},
onImport (parsed) {
if (parsed._pleroma_theme_version === 1) {
this.normalizeLocalState(parsed, 1)
2019-12-28 15:02:34 +00:00
} else if (parsed._pleroma_theme_version >= 2) {
this.normalizeLocalState(parsed.theme, 2, parsed.source)
}
},
importValidator (parsed) {
const version = parsed._pleroma_theme_version
return version >= 1 || version <= 2
},
clearAll () {
const state = this.$store.getters.mergedConfig.customTheme
const version = state.colors ? 2 : 'l1'
this.normalizeLocalState(this.$store.getters.mergedConfig.customTheme, version, this.$store.getters.mergedConfig.customThemeSource)
},
2018-11-19 17:22:46 +00:00
// Clears all the extra stuff when loading V1 theme
clearV1 () {
2018-11-19 17:22:46 +00:00
Object.keys(this.$data)
.filter(_ => _.endsWith('ColorLocal') || _.endsWith('OpacityLocal'))
.filter(_ => !v1OnlyNames.includes(_))
.forEach(key => {
set(this.$data, key, undefined)
})
},
clearRoundness () {
Object.keys(this.$data)
.filter(_ => _.endsWith('RadiusLocal'))
.forEach(key => {
set(this.$data, key, undefined)
})
},
clearOpacity () {
Object.keys(this.$data)
.filter(_ => _.endsWith('OpacityLocal'))
.forEach(key => {
set(this.$data, key, undefined)
})
},
clearShadows () {
this.shadowsLocal = {}
},
2018-11-25 18:48:16 +00:00
clearFonts () {
this.fontsLocal = {}
},
2018-10-04 15:16:14 +00:00
/**
* This applies stored theme data onto form. Supports three versions of data:
* v3 (version >= 3) - newest version of themes which supports snapshots for better compatiblity
* v2 (version = 2) - newer version of themes.
* v1 (version = 1) - older version of themes (import from file)
* v1l (version = l1) - older version of theme (load from local storage)
* v1 and v1l differ because of way themes were stored/exported.
* @param {Object} theme - theme data (snapshot)
* @param {Number} version - version of data. 0 means try to guess based on data. "l1" means v1, locastorage type
* @param {Object} source - theme source - this will be used if compatible
* @param {Boolean} source - by default source won't be used if version doesn't match since it might render differently
* this allows importing source anyway
2018-10-04 15:16:14 +00:00
*/
normalizeLocalState (theme, version = 0, source, forceSource = false) {
2019-12-28 15:02:34 +00:00
let input
if (typeof source !== 'undefined') {
if (forceSource || source.themeEngineVersion === CURRENT_VERSION) {
input = source
version = source.themeEngineVersion
} else {
input = theme
}
2019-12-28 15:02:34 +00:00
} else {
input = theme
2019-12-28 15:02:34 +00:00
}
const radii = input.radii || input
const opacity = input.opacity
const shadows = input.shadows || {}
2018-11-25 18:48:16 +00:00
const fonts = input.fonts || {}
2019-12-28 15:02:34 +00:00
const colors = input.colors || input
2018-10-07 16:59:22 +00:00
if (version === 0) {
if (input.version) version = input.version
// Old v1 naming: fg is text, btn is foreground
if (typeof colors.text === 'undefined' && typeof colors.fg !== 'undefined') {
version = 1
}
// New v2 naming: text is text, fg is foreground
if (typeof colors.text !== 'undefined' && typeof colors.fg !== 'undefined') {
version = 2
}
}
2018-10-07 16:59:22 +00:00
// Stuff that differs between V1 and V2
if (version === 1) {
2018-10-07 16:59:22 +00:00
this.fgColorLocal = rgb2hex(colors.btn)
this.textColorLocal = rgb2hex(colors.fg)
}
2018-12-11 13:36:06 +00:00
if (!this.keepColor) {
this.clearV1()
const keys = new Set(version !== 1 ? Object.keys(colors) : [])
if (version === 1 || version === 'l1') {
keys
.add('bg')
.add('link')
.add('cRed')
.add('cBlue')
.add('cGreen')
.add('cOrange')
}
2018-12-11 13:36:06 +00:00
keys.forEach(key => {
2020-01-12 23:56:29 +00:00
const color = colors[key]
const hex = rgb2hex(colors[key])
this[key + 'ColorLocal'] = hex === '#aN' ? color : hex
2018-12-11 13:36:06 +00:00
})
}
if (!this.keepRoundness) {
this.clearRoundness()
2018-11-23 06:02:10 +00:00
Object.entries(radii).forEach(([k, v]) => {
// 'Radius' is kept mostly for v1->v2 localstorage transition
const key = k.endsWith('Radius') ? k.split('Radius')[0] : k
this[key + 'RadiusLocal'] = v
})
}
if (!this.keepShadows) {
this.clearShadows()
this.shadowsLocal = shadows
this.shadowSelected = this.shadowsAvailable[0]
}
2018-11-25 18:48:16 +00:00
if (!this.keepFonts) {
this.clearFonts()
this.fontsLocal = fonts
}
if (opacity && !this.keepOpacity) {
this.clearOpacity()
Object.entries(opacity).forEach(([k, v]) => {
if (typeof v === 'undefined' || v === null || Number.isNaN(v)) return
this[k + 'OpacityLocal'] = v
})
}
}
2017-01-16 17:57:03 +00:00
},
watch: {
currentRadii () {
try {
this.previewRadii = generateRadii({ radii: this.currentRadii })
this.radiiInvalid = false
} catch (e) {
this.radiiInvalid = true
console.warn(e)
}
},
2018-11-25 18:48:16 +00:00
shadowsLocal: {
handler () {
try {
this.previewShadows = generateShadows({ shadows: this.shadowsLocal })
this.shadowsInvalid = false
} catch (e) {
this.shadowsInvalid = true
console.warn(e)
}
},
deep: true
},
fontsLocal: {
handler () {
try {
this.previewFonts = generateFonts({ fonts: this.fontsLocal })
this.fontsInvalid = false
} catch (e) {
this.fontsInvalid = true
console.warn(e)
}
},
deep: true
},
currentColors () {
try {
this.previewColors = generateColors({
opacity: this.currentOpacity,
colors: this.currentColors
})
this.colorsInvalid = false
} catch (e) {
this.colorsInvalid = true
console.warn(e)
}
},
currentOpacity () {
try {
this.previewColors = generateColors({
opacity: this.currentOpacity,
colors: this.currentColors
})
} catch (e) {
console.warn(e)
}
},
2017-01-16 17:57:03 +00:00
selected () {
if (this.selectedVersion === 1) {
if (!this.keepRoundness) {
this.clearRoundness()
}
if (!this.keepShadows) {
this.clearShadows()
}
if (!this.keepOpacity) {
this.clearOpacity()
}
2018-12-11 13:36:06 +00:00
if (!this.keepColor) {
this.clearV1()
this.bgColorLocal = this.selected[1]
this.fgColorLocal = this.selected[2]
this.textColorLocal = this.selected[3]
this.linkColorLocal = this.selected[4]
this.cRedColorLocal = this.selected[5]
this.cGreenColorLocal = this.selected[6]
this.cBlueColorLocal = this.selected[7]
this.cOrangeColorLocal = this.selected[8]
}
2018-11-22 01:37:49 +00:00
} else if (this.selectedVersion >= 2) {
this.normalizeLocalState(this.selected.theme, 2, this.selected.source)
}
2017-01-16 17:57:03 +00:00
}
}
}