aboutsummaryrefslogtreecommitdiff
path: root/src/components/style_switcher/style_switcher.js
diff options
context:
space:
mode:
Diffstat (limited to 'src/components/style_switcher/style_switcher.js')
-rw-r--r--src/components/style_switcher/style_switcher.js599
1 files changed, 515 insertions, 84 deletions
diff --git a/src/components/style_switcher/style_switcher.js b/src/components/style_switcher/style_switcher.js
index 95c15b49..d833341f 100644
--- a/src/components/style_switcher/style_switcher.js
+++ b/src/components/style_switcher/style_switcher.js
@@ -1,4 +1,25 @@
-import { rgbstr2hex } from '../../services/color_convert/color_convert.js'
+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 } 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.jsx'
+
+// List of color values used in v1
+const v1OnlyNames = [
+ 'bg',
+ 'fg',
+ 'text',
+ 'link',
+ 'cRed',
+ 'cGreen',
+ 'cBlue',
+ 'cOrange'
+].map(_ => _ + 'ColorLocal')
export default {
data () {
@@ -6,16 +27,72 @@ export default {
availableStyles: [],
selected: this.$store.state.config.theme,
invalidThemeImported: false,
- bgColorLocal: '',
- btnColorLocal: '',
+
+ previewShadows: {},
+ previewColors: {},
+ previewRadii: {},
+ previewFonts: {},
+
+ shadowsInvalid: true,
+ colorsInvalid: true,
+ radiiInvalid: true,
+
+ keepShadows: false,
+ keepOpacity: false,
+ keepRoundness: false,
+ keepFonts: false,
+
textColorLocal: '',
linkColorLocal: '',
- redColorLocal: '',
- blueColorLocal: '',
- greenColorLocal: '',
- orangeColorLocal: '',
+
+ bgColorLocal: '',
+ bgOpacityLocal: undefined,
+
+ fgColorLocal: '',
+ fgTextColorLocal: undefined,
+ fgLinkColorLocal: undefined,
+
+ btnColorLocal: undefined,
+ btnTextColorLocal: undefined,
+ btnOpacityLocal: undefined,
+
+ inputColorLocal: undefined,
+ inputTextColorLocal: undefined,
+ inputOpacityLocal: undefined,
+
+ panelColorLocal: undefined,
+ panelTextColorLocal: undefined,
+ panelFaintColorLocal: undefined,
+ panelOpacityLocal: undefined,
+
+ topBarColorLocal: undefined,
+ topBarTextColorLocal: undefined,
+ topBarLinkColorLocal: undefined,
+
+ alertErrorColorLocal: 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: '',
@@ -29,19 +106,238 @@ export default {
window.fetch('/static/styles.json')
.then((data) => data.json())
.then((themes) => {
- self.availableStyles = themes
+ return Promise.all(Object.entries(themes).map(([k, v]) => {
+ if (typeof v === 'object') {
+ return Promise.resolve([k, v])
+ } else if (typeof v === 'string') {
+ return window.fetch(v)
+ .then((data) => data.json())
+ .then((theme) => {
+ return [k, theme]
+ })
+ .catch((e) => {
+ console.error(e)
+ return []
+ })
+ }
+ }))
+ })
+ .then((promises) => {
+ return promises
+ .filter(([k, v]) => v)
+ .reduce((acc, [k, v]) => {
+ acc[k] = v
+ return acc
+ }, {})
+ }).then((themesComplete) => {
+ self.availableStyles = themesComplete
})
},
mounted () {
- this.normalizeLocalState(this.$store.state.config.colors, this.$store.state.config.radii)
+ 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
+ },
+ 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,
+ 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
+ }
+ },
+ 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),
+ 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),
+ 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),
+
+ 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
+ }
+ },
+ components: {
+ ColorInput,
+ OpacityInput,
+ RangeInput,
+ ContrastRatio,
+ ShadowControl,
+ FontControl,
+ TabSwitcher
},
methods: {
exportCurrentTheme () {
const stringified = JSON.stringify({
// To separate from other random JSON files and possible future theme formats
- _pleroma_theme_version: 1,
- colors: this.$store.state.config.colors,
- radii: this.$store.state.config.radii
+ _pleroma_theme_version: 2,
+ theme: {
+ shadows: this.shadowsLocal,
+ fonts: this.fontsLocal,
+ opacity: this.currentOpacity,
+ colors: this.currentColors,
+ radii: this.currentRadii
+ }
}, null, 2) // Pretty-print and indent with 2 spaces
// Create an invisible link with a data url and simulate a click
@@ -69,7 +365,9 @@ export default {
try {
const parsed = JSON.parse(target.result)
if (parsed._pleroma_theme_version === 1) {
- this.normalizeLocalState(parsed.colors, parsed.radii)
+ this.normalizeLocalState(parsed, 1)
+ } else if (parsed._pleroma_theme_version === 2) {
+ this.normalizeLocalState(parsed.theme, 2)
} else {
// A theme from the future, spooky
this.invalidThemeImported = true
@@ -89,81 +387,214 @@ export default {
},
setCustomTheme () {
- if (!this.bgColorLocal && !this.btnColorLocal && !this.linkColorLocal) {
- // reset to picked themes
- }
-
- const rgb = (hex) => {
- const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
- return result ? {
- r: parseInt(result[1], 16),
- g: parseInt(result[2], 16),
- b: parseInt(result[3], 16)
- } : null
- }
- const bgRgb = rgb(this.bgColorLocal)
- const btnRgb = rgb(this.btnColorLocal)
- const textRgb = rgb(this.textColorLocal)
- const linkRgb = rgb(this.linkColorLocal)
-
- const redRgb = rgb(this.redColorLocal)
- const blueRgb = rgb(this.blueColorLocal)
- const greenRgb = rgb(this.greenColorLocal)
- const orangeRgb = rgb(this.orangeColorLocal)
-
- if (bgRgb && btnRgb && linkRgb) {
- this.$store.dispatch('setOption', {
- name: 'customTheme',
- value: {
- fg: btnRgb,
- bg: bgRgb,
- text: textRgb,
- link: linkRgb,
- cRed: redRgb,
- cBlue: blueRgb,
- cGreen: greenRgb,
- cOrange: orangeRgb,
- btnRadius: this.btnRadiusLocal,
- inputRadius: this.inputRadiusLocal,
- panelRadius: this.panelRadiusLocal,
- avatarRadius: this.avatarRadiusLocal,
- avatarAltRadius: this.avatarAltRadiusLocal,
- tooltipRadius: this.tooltipRadiusLocal,
- attachmentRadius: this.attachmentRadiusLocal
- }})
- }
- },
-
- normalizeLocalState (colors, radii) {
- this.bgColorLocal = rgbstr2hex(colors.bg)
- this.btnColorLocal = rgbstr2hex(colors.btn)
- this.textColorLocal = rgbstr2hex(colors.fg)
- this.linkColorLocal = rgbstr2hex(colors.link)
-
- this.redColorLocal = rgbstr2hex(colors.cRed)
- this.blueColorLocal = rgbstr2hex(colors.cBlue)
- this.greenColorLocal = rgbstr2hex(colors.cGreen)
- this.orangeColorLocal = rgbstr2hex(colors.cOrange)
-
- this.btnRadiusLocal = radii.btnRadius || 4
- this.inputRadiusLocal = radii.inputRadius || 4
- this.panelRadiusLocal = radii.panelRadius || 10
- this.avatarRadiusLocal = radii.avatarRadius || 5
- this.avatarAltRadiusLocal = radii.avatarAltRadius || 50
- this.tooltipRadiusLocal = radii.tooltipRadius || 2
- this.attachmentRadiusLocal = radii.attachmentRadius || 5
+ this.$store.dispatch('setOption', {
+ name: 'customTheme',
+ value: {
+ shadows: this.shadowsLocal,
+ fonts: this.fontsLocal,
+ opacity: this.currentOpacity,
+ colors: this.currentColors,
+ radii: this.currentRadii
+ }
+ })
+ },
+
+ clearAll () {
+ this.normalizeLocalState(this.$store.state.config.customTheme)
+ },
+
+ // 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.
+ * @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
+ 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)
+ }
+
+ 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])
+ })
+
+ 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 () {
- this.bgColorLocal = this.selected[1]
- this.btnColorLocal = this.selected[2]
- this.textColorLocal = this.selected[3]
- this.linkColorLocal = this.selected[4]
- this.redColorLocal = this.selected[5]
- this.greenColorLocal = this.selected[6]
- this.blueColorLocal = this.selected[7]
- this.orangeColorLocal = this.selected[8]
+ if (this.selectedVersion === 1) {
+ if (!this.keepRoundness) {
+ this.clearRoundness()
+ }
+
+ if (!this.keepShadows) {
+ this.clearShadows()
+ }
+
+ if (!this.keepOpacity) {
+ this.clearOpacity()
+ }
+
+ 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)
+ }
}
}
}