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

459 lines
14 KiB
JavaScript
Raw Normal View History

import { rgb2hex, hex2rgb, getContrastRatio, alphaBlend } from '../../services/color_convert/color_convert.js'
2018-11-19 15:15:27 +00:00
import { set, delete as del } from 'vue'
2018-11-19 17:22:46 +00:00
import { generateColors, generateShadows, generateRadii, composePreset } from '../../services/style_setter/style_setter.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-10-09 21:07:28 +00:00
import ContrastRatio from '../contrast_ratio/contrast_ratio.vue'
import TabSwitcher from '../tab_switcher/tab_switcher.jsx'
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')
2017-01-16 17:57:03 +00:00
export default {
2017-02-17 17:21:02 +00:00
data () {
return {
availableStyles: [],
selected: this.$store.state.config.theme,
invalidThemeImported: false,
textColorLocal: '',
linkColorLocal: '',
bgColorLocal: '',
bgOpacityLocal: undefined,
2018-10-04 15:16:14 +00:00
fgColorLocal: '',
fgTextColorLocal: undefined,
fgLinkColorLocal: undefined,
btnColorLocal: undefined,
btnTextColorLocal: undefined,
btnOpacityLocal: undefined,
inputColorLocal: undefined,
inputTextColorLocal: undefined,
inputOpacityLocal: undefined,
panelColorLocal: undefined,
panelTextColorLocal: undefined,
2018-10-07 16:59:22 +00:00
panelFaintColorLocal: undefined,
panelOpacityLocal: undefined,
topBarColorLocal: undefined,
topBarTextColorLocal: undefined,
2018-10-04 15:16:14 +00:00
topBarLinkColorLocal: undefined,
2018-11-13 13:30:01 +00:00
alertErrorColorLocal: undefined,
badgeOpacityLocal: undefined,
badgeNotificationColorLocal: undefined,
2018-10-04 15:16:14 +00:00
2018-10-07 16:59:22 +00:00
borderColorLocal: undefined,
borderOpacityLocal: undefined,
faintColorLocal: undefined,
faintOpacityLocal: undefined,
faintLinkColorLocal: undefined,
cRedColorLocal: '',
cBlueColorLocal: '',
cGreenColorLocal: '',
cOrangeColorLocal: '',
2018-11-19 17:22:46 +00:00
shadowSelected: undefined,
shadowsLocal: {},
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
window.fetch('/static/styles.json')
2017-01-16 17:57:03 +00:00
.then((data) => data.json())
.then((themes) => {
self.availableStyles = themes
})
},
mounted () {
this.normalizeLocalState(this.$store.state.config.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 {
2018-11-19 17:22:46 +00:00
bg: this.bgColorLocal,
text: this.textColorLocal,
link: this.linkColorLocal,
fg: this.fgColorLocal,
fgText: this.fgTextColorLocal,
fgLink: this.fgLinkColorLocal,
panel: this.panelColorLocal,
panelText: this.panelTextColorLocal,
panelFaint: this.panelFaintColorLocal,
input: this.inputColorLocal,
inputText: this.inputTextColorLocal,
topBar: this.topBarColorLocal,
topBarText: this.topBarTextColorLocal,
topBarLink: this.topBarLinkColorLocal,
btn: this.btnColorLocal,
btnText: this.btnTextColorLocal,
alertError: this.alertErrorColorLocal,
badgeNotification: this.badgeNotificationColorLocal,
faint: this.faintColorLocal,
faintLink: this.faintLinkColorLocal,
border: this.borderColorLocal,
cRed: this.cRedColorLocal,
cBlue: this.cBlueColorLocal,
cGreen: this.cGreenColorLocal,
cOrange: this.cOrangeColorLocal
}
},
2018-11-19 17:22:46 +00:00
currentOpacity () {
return {
bg: this.bgOpacityLocal,
btn: this.btnOpacityLocal,
input: this.inputOpacityLocal,
panel: this.panelOpacityLocal,
topBar: this.topBarOpacityLocal,
border: this.borderOpacityLocal,
faint: this.faintOpacityLocal
}
},
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
}
},
previewColors () {
if (this.currentColors.bg) {
return generateColors({
opacity: this.currentOpacity,
colors: this.currentColors
})
} else {
2018-10-04 15:16:14 +00:00
return {}
}
2018-10-04 15:16:14 +00:00
},
2018-11-19 17:22:46 +00:00
previewRadii () {
2018-11-21 00:14:59 +00:00
return generateRadii({ radii: this.currentRadii })
2018-11-19 17:22:46 +00:00
},
previewShadows () {
return generateShadows({ shadows: this.shadowsLocal })
},
preview () {
return composePreset(this.previewColors, this.previewRadii, this.previewShadows)
},
2018-10-04 15:16:14 +00:00
previewTheme () {
2018-11-19 17:22:46 +00:00
if (!this.preview.theme.colors) return { colors: {}, opacity: {}, radii: {}, shadows: {} }
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
})
// fgsfds :DDDD
const fgs = {
text: hex2rgb(colors.text),
panelText: hex2rgb(colors.panelText),
btnText: hex2rgb(colors.btnText),
topBarText: hex2rgb(colors.topBarText),
2018-11-13 13:30:01 +00:00
inputText: hex2rgb(colors.inputText),
link: hex2rgb(colors.link),
topBarLink: hex2rgb(colors.topBarLink),
2018-10-21 12:25:21 +00:00
red: hex2rgb(colors.cRed),
green: hex2rgb(colors.cGreen),
blue: hex2rgb(colors.cBlue),
orange: hex2rgb(colors.cOrange)
}
const bgs = {
bg: hex2rgb(colors.bg),
btn: hex2rgb(colors.btn),
panel: hex2rgb(colors.panel),
2018-11-13 13:30:01 +00:00
topBar: hex2rgb(colors.topBar),
input: hex2rgb(colors.input),
alertError: hex2rgb(colors.alertError),
badgeNotification: hex2rgb(colors.badgeNotification)
}
/* This is a bit confusing because "bottom layer" used is text color
* This is done to get worst case scenario when background below transparent
* layer matches text color, making it harder to read the lower alpha is.
*/
2018-10-09 21:07:28 +00:00
const ratios = {
bgText: getContrastRatio(alphaBlend(bgs.bg, opacity.bg, fgs.text), fgs.text),
bgLink: getContrastRatio(alphaBlend(bgs.bg, opacity.bg, fgs.link), fgs.link),
bgRed: getContrastRatio(alphaBlend(bgs.bg, opacity.bg, fgs.red), fgs.red),
bgGreen: getContrastRatio(alphaBlend(bgs.bg, opacity.bg, fgs.green), fgs.green),
bgBlue: getContrastRatio(alphaBlend(bgs.bg, opacity.bg, fgs.blue), fgs.blue),
bgOrange: getContrastRatio(alphaBlend(bgs.bg, opacity.bg, fgs.orange), fgs.orange),
tintText: getContrastRatio(alphaBlend(bgs.bg, 0.5, fgs.panelText), fgs.text),
2018-10-09 21:07:28 +00:00
panelText: getContrastRatio(alphaBlend(bgs.panel, opacity.panel, fgs.panelText), fgs.panelText),
2018-10-09 21:07:28 +00:00
btnText: getContrastRatio(alphaBlend(bgs.btn, opacity.btn, fgs.btnText), fgs.btnText),
2018-10-09 21:07:28 +00:00
inputText: getContrastRatio(alphaBlend(bgs.input, opacity.input, fgs.inputText), fgs.inputText),
2018-11-13 13:30:01 +00:00
topBarText: getContrastRatio(alphaBlend(bgs.topBar, opacity.topBar, fgs.topBarText), fgs.topBarText),
topBarLink: getContrastRatio(alphaBlend(bgs.topBar, opacity.topBar, fgs.topBarLink), fgs.topBarLink)
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 ''
return [...Object.values(this.preview.rules), 'color: var(--text)'].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)
}
}
},
components: {
ColorInput,
2018-10-09 21:07:28 +00:00
OpacityInput,
2018-11-21 00:14:59 +00:00
RangeInput,
ContrastRatio,
ShadowControl,
TabSwitcher
},
methods: {
2018-06-27 23:08:06 +00:00
exportCurrentTheme () {
const stringified = JSON.stringify({
// To separate from other random JSON files and possible future theme formats
_pleroma_theme_version: 2,
2018-11-19 17:22:46 +00:00
theme: {
shadows: this.shadowsLocal,
opacity: this.currentOpacity,
colors: this.currentColors,
radii: this.currentRadii
}
2018-06-27 23:08:06 +00:00
}, null, 2) // Pretty-print and indent with 2 spaces
// Create an invisible link with a data url and simulate a click
const e = document.createElement('a')
e.setAttribute('download', 'pleroma_theme.json')
e.setAttribute('href', 'data:application/json;base64,' + window.btoa(stringified))
e.style.display = 'none'
document.body.appendChild(e)
e.click()
document.body.removeChild(e)
},
2018-06-28 00:59:57 +00:00
importTheme () {
this.invalidThemeImported = false
2018-06-28 00:59:57 +00:00
const filePicker = document.createElement('input')
filePicker.setAttribute('type', 'file')
filePicker.setAttribute('accept', '.json')
filePicker.addEventListener('change', event => {
if (event.target.files[0]) {
// eslint-disable-next-line no-undef
const reader = new FileReader()
reader.onload = ({target}) => {
try {
const parsed = JSON.parse(target.result)
if (parsed._pleroma_theme_version === 1) {
this.normalizeLocalState(parsed, 1)
} else if (parsed._pleroma_theme_version === 2) {
2018-10-04 15:16:14 +00:00
this.normalizeLocalState(parsed.theme, 2)
} else {
// A theme from the future, spooky
this.invalidThemeImported = true
}
} catch (e) {
// This will happen both if there is a JSON syntax error or the theme is missing components
this.invalidThemeImported = true
}
2018-06-28 00:59:57 +00:00
}
reader.readAsText(event.target.files[0])
}
})
document.body.appendChild(filePicker)
filePicker.click()
document.body.removeChild(filePicker)
},
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,
opacity: this.currentOpacity,
colors: this.currentColors,
radii: this.currentRadii
2018-11-19 15:15:27 +00:00
}
})
},
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)
})
},
2018-10-04 15:16:14 +00:00
/**
* This applies stored theme data onto form.
* @param {Object} input - input data
* @param {Number} version - version of data. 0 means try to guess based on data.
*/
normalizeLocalState (input, version = 0) {
const colors = input.colors || input
const radii = input.radii || input
const opacity = input.opacity || input
const shadows = input.shadows || {}
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-10-07 16:59:22 +00:00
const keys = new Set(version !== 1 ? Object.keys(colors) : [])
if (version === 1) {
// V1 ignores the rest
this.clearV1()
keys
.add('bg')
.add('link')
.add('cRed')
.add('cBlue')
.add('cGreen')
.add('cOrange')
}
keys.forEach(key => {
this[key + 'ColorLocal'] = rgb2hex(colors[key])
})
// TODO optimize this
2018-11-21 00:14:59 +00:00
this.btnRadiusLocal = radii.btn
this.inputRadiusLocal = radii.input
2018-11-23 04:28:53 +00:00
this.checkboxRadiusLocal = radii.checkbox
2018-11-21 00:14:59 +00:00
this.panelRadiusLocal = radii.panel
this.avatarRadiusLocal = radii.avatar
this.avatarAltRadiusLocal = radii.avatarAlt
this.tooltipRadiusLocal = radii.tooltip
this.attachmentRadiusLocal = radii.attachment
this.shadowsLocal = shadows
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: {
selected () {
if (this.selectedVersion === 1) {
2018-10-04 15:16:14 +00:00
this.clearV1()
this.bgColorLocal = this.selected[1]
2018-10-07 16:59:22 +00:00
this.fgColorLocal = this.selected[2]
this.textColorLocal = this.selected[3]
this.linkColorLocal = this.selected[4]
2018-10-21 12:25:21 +00:00
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)
}
2017-01-16 17:57:03 +00:00
}
}
}