akkoma-fe/src/components/style_switcher/style_switcher.js
Henry Jameson 0dcb696e26 Merge remote-tracking branch 'upstream/develop' into emoji-optimizations
* upstream/develop: (95 commits)
  Lightbox/modal multi image improvements - #381
  '/api/pleroma/profile/mfa' -> '/api/pleroma/accounts/mfa'
  Add ability to change user's email
  translations-de-batch-1
  eu-translate update
  profile-banner rounding css, fixes #690
  fix indentation
  remove needless ref
  show preview popover when hover numbered replies
  refactor conditions
  do not make too many nested div
  add fetchStatus action
  refactor status loading logic
  split status preview popover into a separate component
  uninstall mobile-detect library
  listen both events
  minor css fix
  restrict distance at top side only
  set different trigger event in desktop and mobile by default
  fix eslint warnings
  ...
2019-11-08 19:48:31 +02:00

581 lines
17 KiB
JavaScript

import { rgb2hex, hex2rgb, getContrastRatio, alphaBlend } from '../../services/color_convert/color_convert.js'
import { set, delete as del } from 'vue'
import { generateColors, generateShadows, generateRadii, generateFonts, composePreset, getThemes } from '../../services/style_setter/style_setter.js'
import ColorInput from '../color_input/color_input.vue'
import RangeInput from '../range_input/range_input.vue'
import OpacityInput from '../opacity_input/opacity_input.vue'
import ShadowControl from '../shadow_control/shadow_control.vue'
import FontControl from '../font_control/font_control.vue'
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'
// List of color values used in v1
const v1OnlyNames = [
'bg',
'fg',
'text',
'link',
'cRed',
'cGreen',
'cBlue',
'cOrange'
].map(_ => _ + 'ColorLocal')
export default {
data () {
return {
availableStyles: [],
selected: this.$store.getters.mergedConfig.theme,
previewShadows: {},
previewColors: {},
previewRadii: {},
previewFonts: {},
shadowsInvalid: true,
colorsInvalid: true,
radiiInvalid: true,
keepColor: false,
keepShadows: false,
keepOpacity: false,
keepRoundness: false,
keepFonts: false,
textColorLocal: '',
linkColorLocal: '',
bgColorLocal: '',
bgOpacityLocal: undefined,
fgColorLocal: '',
fgTextColorLocal: undefined,
fgLinkColorLocal: undefined,
btnColorLocal: undefined,
btnTextColorLocal: undefined,
btnOpacityLocal: undefined,
inputColorLocal: undefined,
inputTextColorLocal: undefined,
inputOpacityLocal: undefined,
panelColorLocal: undefined,
panelTextColorLocal: undefined,
panelLinkColorLocal: undefined,
panelFaintColorLocal: undefined,
panelOpacityLocal: undefined,
topBarColorLocal: undefined,
topBarTextColorLocal: undefined,
topBarLinkColorLocal: undefined,
alertErrorColorLocal: undefined,
alertWarningColorLocal: undefined,
badgeOpacityLocal: undefined,
badgeNotificationColorLocal: undefined,
borderColorLocal: undefined,
borderOpacityLocal: undefined,
faintColorLocal: undefined,
faintOpacityLocal: undefined,
faintLinkColorLocal: undefined,
cRedColorLocal: '',
cBlueColorLocal: '',
cGreenColorLocal: '',
cOrangeColorLocal: '',
shadowSelected: undefined,
shadowsLocal: {},
fontsLocal: {},
btnRadiusLocal: '',
inputRadiusLocal: '',
checkboxRadiusLocal: '',
panelRadiusLocal: '',
avatarRadiusLocal: '',
avatarAltRadiusLocal: '',
attachmentRadiusLocal: '',
tooltipRadiusLocal: ''
}
},
created () {
const self = this
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
},
currentColors () {
return {
bg: this.bgColorLocal,
text: this.textColorLocal,
link: this.linkColorLocal,
fg: this.fgColorLocal,
fgText: this.fgTextColorLocal,
fgLink: this.fgLinkColorLocal,
panel: this.panelColorLocal,
panelText: this.panelTextColorLocal,
panelLink: this.panelLinkColorLocal,
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,
alertWarning: this.alertWarningColorLocal,
badgeNotification: this.badgeNotificationColorLocal,
faint: this.faintColorLocal,
faintLink: this.faintLinkColorLocal,
border: this.borderColorLocal,
cRed: this.cRedColorLocal,
cBlue: this.cBlueColorLocal,
cGreen: this.cGreenColorLocal,
cOrange: this.cOrangeColorLocal
}
},
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,
checkbox: this.checkboxRadiusLocal,
panel: this.panelRadiusLocal,
avatar: this.avatarRadiusLocal,
avatarAlt: this.avatarAltRadiusLocal,
tooltip: this.tooltipRadiusLocal,
attachment: this.attachmentRadiusLocal
}
},
preview () {
return composePreset(this.previewColors, this.previewRadii, this.previewShadows, this.previewFonts)
},
previewTheme () {
if (!this.preview.theme.colors) return { colors: {}, opacity: {}, radii: {}, shadows: {}, fonts: {} }
return this.preview.theme
},
// This needs optimization maybe
previewContrast () {
if (!this.previewTheme.colors.bg) return {}
const colors = this.previewTheme.colors
const opacity = this.previewTheme.opacity
if (!colors.bg) return {}
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),
panelLink: hex2rgb(colors.panelLink),
btnText: hex2rgb(colors.btnText),
topBarText: hex2rgb(colors.topBarText),
inputText: hex2rgb(colors.inputText),
link: hex2rgb(colors.link),
topBarLink: hex2rgb(colors.topBarLink),
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),
topBar: hex2rgb(colors.topBar),
input: hex2rgb(colors.input),
alertError: hex2rgb(colors.alertError),
alertWarning: hex2rgb(colors.alertWarning),
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.
*/
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),
panelText: getContrastRatio(alphaBlend(bgs.panel, opacity.panel, fgs.panelText), fgs.panelText),
panelLink: getContrastRatio(alphaBlend(bgs.panel, opacity.panel, fgs.panelLink), fgs.panelLink),
btnText: getContrastRatio(alphaBlend(bgs.btn, opacity.btn, fgs.btnText), fgs.btnText),
inputText: getContrastRatio(alphaBlend(bgs.input, opacity.input, fgs.inputText), fgs.inputText),
topBarText: getContrastRatio(alphaBlend(bgs.topBar, opacity.topBar, fgs.topBarText), fgs.topBarText),
topBarLink: getContrastRatio(alphaBlend(bgs.topBar, opacity.topBar, fgs.topBarLink), fgs.topBarLink)
}
return Object.entries(ratios).reduce((acc, [k, v]) => { acc[k] = hints(v); return acc }, {})
},
previewRules () {
if (!this.preview.rules) return ''
return [
...Object.values(this.preview.rules),
'color: var(--text)',
'font-family: var(--interfaceFont, sans-serif)'
].join(';')
},
shadowsAvailable () {
return Object.keys(this.previewTheme.shadows).sort()
},
currentShadowOverriden: {
get () {
return !!this.currentShadow
},
set (val) {
if (val) {
set(this.shadowsLocal, this.shadowSelected, this.currentShadowFallback.map(_ => Object.assign({}, _)))
} 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 () {
const saveEverything = (
!this.keepFonts &&
!this.keepShadows &&
!this.keepOpacity &&
!this.keepRoundness &&
!this.keepColor
)
const theme = {}
if (this.keepFonts || saveEverything) {
theme.fonts = this.fontsLocal
}
if (this.keepShadows || saveEverything) {
theme.shadows = this.shadowsLocal
}
if (this.keepOpacity || saveEverything) {
theme.opacity = this.currentOpacity
}
if (this.keepColor || saveEverything) {
theme.colors = this.currentColors
}
if (this.keepRoundness || saveEverything) {
theme.radii = this.currentRadii
}
return {
// To separate from other random JSON files and possible future theme formats
_pleroma_theme_version: 2, theme
}
}
},
components: {
ColorInput,
OpacityInput,
RangeInput,
ContrastRatio,
ShadowControl,
FontControl,
TabSwitcher,
Preview,
ExportImport,
Checkbox
},
methods: {
setCustomTheme () {
this.$store.dispatch('setOption', {
name: 'customTheme',
value: {
shadows: this.shadowsLocal,
fonts: this.fontsLocal,
opacity: this.currentOpacity,
colors: this.currentColors,
radii: this.currentRadii
}
})
},
onImport (parsed) {
if (parsed._pleroma_theme_version === 1) {
this.normalizeLocalState(parsed, 1)
} else if (parsed._pleroma_theme_version === 2) {
this.normalizeLocalState(parsed.theme, 2)
}
},
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)
},
// Clears all the extra stuff when loading V1 theme
clearV1 () {
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 = {}
},
clearFonts () {
this.fontsLocal = {}
},
/**
* This applies stored theme data onto form. Supports three versions of data:
* 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} input - input data
* @param {Number} version - version of data. 0 means try to guess based on data. "l1" means v1, locastorage type
*/
normalizeLocalState (input, version = 0) {
const colors = input.colors || input
const radii = input.radii || input
const opacity = input.opacity
const shadows = input.shadows || {}
const fonts = input.fonts || {}
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
}
}
// Stuff that differs between V1 and V2
if (version === 1) {
this.fgColorLocal = rgb2hex(colors.btn)
this.textColorLocal = rgb2hex(colors.fg)
}
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')
}
keys.forEach(key => {
this[key + 'ColorLocal'] = rgb2hex(colors[key])
})
}
if (!this.keepRoundness) {
this.clearRoundness()
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]
}
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
})
}
}
},
watch: {
currentRadii () {
try {
this.previewRadii = generateRadii({ radii: this.currentRadii })
this.radiiInvalid = false
} catch (e) {
this.radiiInvalid = true
console.warn(e)
}
},
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)
}
},
selected () {
if (this.selectedVersion === 1) {
if (!this.keepRoundness) {
this.clearRoundness()
}
if (!this.keepShadows) {
this.clearShadows()
}
if (!this.keepOpacity) {
this.clearOpacity()
}
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]
}
} else if (this.selectedVersion >= 2) {
this.normalizeLocalState(this.selected.theme, 2)
}
}
}
}