pleroma-fe/src/components/settings_modal/tabs/appearance_tab.js

422 lines
13 KiB
JavaScript
Raw Normal View History

import BooleanSetting from '../helpers/boolean_setting.vue'
import ChoiceSetting from '../helpers/choice_setting.vue'
import IntegerSetting from '../helpers/integer_setting.vue'
import FloatSetting from '../helpers/float_setting.vue'
2024-06-13 02:22:47 +03:00
import UnitSetting, { defaultHorizontalUnits } from '../helpers/unit_setting.vue'
2024-11-14 21:42:45 +02:00
import PaletteEditor from 'src/components/palette_editor/palette_editor.vue'
2024-06-27 00:59:24 +03:00
import FontControl from 'src/components/font_control/font_control.vue'
2024-07-17 17:19:57 +03:00
import { normalizeThemeData } from 'src/modules/interface'
import { newImporter } from 'src/services/export_import/export_import.js'
2024-07-17 17:19:57 +03:00
import { convertTheme2To3 } from 'src/services/theme_data/theme2_to_theme3.js'
import { init } from 'src/services/theme_data/theme_data_3.service.js'
import {
getCssRules,
getScopedVersion
} from 'src/services/theme_data/css_utils.js'
2024-11-18 03:53:37 +02:00
import { deserialize } from 'src/services/theme_data/iss_deserializer.js'
2024-07-17 17:19:57 +03:00
import SharedComputedObject from '../helpers/shared_computed_object.js'
import ProfileSettingIndicator from '../helpers/profile_setting_indicator.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faGlobe
} from '@fortawesome/free-solid-svg-icons'
import Preview from './theme_tab/theme_preview.vue'
2024-07-17 17:19:57 +03:00
2024-12-04 14:57:28 +02:00
// helper for debugging
// eslint-disable-next-line no-unused-vars
const toValue = (x) => JSON.parse(JSON.stringify(x === undefined ? 'null' : x))
library.add(
faGlobe
)
const AppearanceTab = {
data () {
2024-06-13 02:22:47 +03:00
return {
availableThemesV3: [],
availableThemesV2: [],
2024-11-12 23:24:28 +02:00
bundledPalettes: [],
compilationCache: {},
fileImporter: newImporter({
accept: '.json, .piss',
validator: this.importValidator,
onImport: this.onImport,
2024-11-18 03:53:37 +02:00
parser: this.importParser,
onImportFailure: this.onImportFailure
}),
2024-10-01 00:42:33 +03:00
palettesKeys: [
2024-11-12 23:24:28 +02:00
'bg',
'fg',
2024-10-01 00:42:33 +03:00
'link',
'text',
'cRed',
'cGreen',
'cBlue',
2024-10-01 00:42:33 +03:00
'cOrange'
],
2024-11-14 21:42:45 +02:00
userPalette: {},
2024-07-17 19:58:04 +03:00
intersectionObserver: null,
2024-06-13 02:22:47 +03:00
thirdColumnModeOptions: ['none', 'notifications', 'postform'].map(mode => ({
key: mode,
value: mode,
label: this.$t(`settings.third_column_mode_${mode}`)
2024-06-21 22:46:01 +03:00
})),
forcedRoundnessOptions: ['disabled', 'sharp', 'nonsharp', 'round'].map((mode, i) => ({
key: mode,
value: i - 1,
label: this.$t(`settings.style.themes3.hacks.forced_roundness_mode_${mode}`)
})),
underlayOverrideModes: ['none', 'opaque', 'transparent'].map((mode, i) => ({
key: mode,
value: mode,
label: this.$t(`settings.style.themes3.hacks.underlay_override_mode_${mode}`)
2024-06-13 02:22:47 +03:00
}))
}
},
components: {
BooleanSetting,
ChoiceSetting,
IntegerSetting,
FloatSetting,
UnitSetting,
2024-06-27 00:59:24 +03:00
ProfileSettingIndicator,
2024-07-17 17:19:57 +03:00
FontControl,
2024-11-14 21:42:45 +02:00
Preview,
PaletteEditor
2024-07-17 17:19:57 +03:00
},
2024-07-17 19:58:04 +03:00
mounted () {
this.$store.dispatch('getThemeData')
const updateIndex = (resource) => {
const capitalizedResource = resource[0].toUpperCase() + resource.slice(1)
const currentIndex = this.$store.state.instance[`${resource}sIndex`]
2024-10-01 00:42:33 +03:00
let promise
if (currentIndex) {
promise = Promise.resolve(currentIndex)
} else {
promise = this.$store.dispatch(`fetch${capitalizedResource}sIndex`)
}
return promise.then(index => {
return Object
.entries(index)
.map(([k, func]) => [k, func()])
2024-07-17 19:58:04 +03:00
})
}
updateIndex('style').then(styles => {
styles.forEach(([key, stylePromise]) => stylePromise.then(data => {
const meta = data.find(x => x.component === '@meta')
this.availableThemesV3.push({ key, data, name: meta.directives.name, version: 'v3' })
}))
})
updateIndex('theme').then(themes => {
themes.forEach(([key, themePromise]) => themePromise.then(data => {
2024-12-22 15:07:20 +02:00
if (!data) {
console.warn(`Theme with key ${key} is empty or malformed`)
} else if (Array.isArray(data)) {
console.warn(`Theme with key ${key} is a v1 theme and should be moved to static/palettes/index.json`)
} else if (!data.source && !data.theme) {
console.warn(`Theme with key ${key} is malformed`)
} else {
this.availableThemesV2.push({ key, data, name: data.name, version: 'v2' })
}
}))
})
this.userPalette = this.$store.state.interface.paletteDataUsed || {}
2024-11-12 23:24:28 +02:00
updateIndex('palette').then(bundledPalettes => {
bundledPalettes.forEach(([key, palettePromise]) => palettePromise.then(v => {
2024-11-14 21:42:45 +02:00
let palette
if (Array.isArray(v)) {
const [
name,
2024-11-12 23:24:28 +02:00
bg,
fg,
text,
link,
cRed = '#FF0000',
cGreen = '#00FF00',
cBlue = '#0000FF',
cOrange = '#E3FF00'
] = v
2024-11-14 21:42:45 +02:00
palette = { key, name, bg, fg, text, link, cRed, cBlue, cGreen, cOrange }
} else {
2024-11-14 21:42:45 +02:00
palette = { key, ...v }
}
2024-12-04 15:54:20 +02:00
if (!palette.key.startsWith('style.')) {
this.bundledPalettes.push(palette)
}
}))
})
2024-07-17 19:58:04 +03:00
if (window.IntersectionObserver) {
this.intersectionObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(({ target, isIntersecting }) => {
if (!isIntersecting) return
const theme = this.availableStyles.find(x => x.key === target.dataset.themeKey)
this.$nextTick(() => {
2024-07-17 22:10:11 +03:00
if (theme) theme.ready = true
2024-07-17 19:58:04 +03:00
})
observer.unobserve(target)
})
}, {
root: this.$refs.themeList
})
}
},
updated () {
this.$nextTick(() => {
this.$refs.themeList.querySelectorAll('.theme-preview').forEach(node => {
this.intersectionObserver.observe(node)
2024-07-17 17:19:57 +03:00
})
2024-07-17 19:58:04 +03:00
})
},
watch: {
paletteDataUsed () {
this.userPalette = this.paletteDataUsed || {}
}
},
computed: {
paletteDataUsed () {
return this.$store.state.interface.paletteDataUsed
},
availableStyles () {
return [
...this.availableThemesV3,
...this.availableThemesV2
]
},
2024-11-12 23:24:28 +02:00
availablePalettes () {
return [
...this.bundledPalettes,
...this.stylePalettes
]
},
stylePalettes () {
const ruleset = this.$store.state.interface.styleDataUsed || []
if (!ruleset && ruleset.length === 0) return
const meta = ruleset.find(x => x.component === '@meta')
const result = ruleset.filter(x => x.component.startsWith('@palette'))
2024-11-12 23:24:28 +02:00
.map(x => {
2024-11-19 01:16:51 +02:00
const { variant, directives } = x
2024-11-12 23:24:28 +02:00
const {
bg,
fg,
text,
link,
accent,
cRed,
cBlue,
cGreen,
cOrange,
wallpaper
2024-11-19 01:16:51 +02:00
} = directives
2024-11-12 23:24:28 +02:00
const result = {
2024-11-19 01:16:51 +02:00
name: `${meta.directives.name || this.$t('settings.style.themes3.palette.imported')}: ${variant}`,
2024-12-04 15:54:20 +02:00
key: `style.${variant.toLowerCase().replace(/ /g, '_')}`,
2024-11-12 23:24:28 +02:00
bg,
fg,
text,
link,
accent,
cRed,
cBlue,
cGreen,
cOrange,
wallpaper
}
return Object.fromEntries(Object.entries(result).filter(([k, v]) => v))
})
return result
},
2024-07-17 19:58:04 +03:00
noIntersectionObserver () {
return !window.IntersectionObserver
},
2024-06-13 02:22:47 +03:00
horizontalUnits () {
return defaultHorizontalUnits
},
2024-06-27 00:59:24 +03:00
fontsOverride () {
return this.$store.getters.mergedConfig.fontsOverride
},
2024-06-13 02:22:47 +03:00
columns () {
const mode = this.$store.getters.mergedConfig.thirdColumnMode
const notif = mode === 'none' ? [] : ['notifs']
if (this.$store.getters.mergedConfig.sidebarRight || mode === 'postform') {
return [...notif, 'content', 'sidebar']
} else {
return ['sidebar', 'content', ...notif]
}
},
instanceWallpaperUsed () {
return this.$store.state.instance.background &&
!this.$store.state.users.currentUser.background_image
},
2024-06-13 02:22:47 +03:00
language: {
get: function () { return this.$store.getters.mergedConfig.interfaceLanguage },
set: function (val) {
this.$store.dispatch('setOption', { name: 'interfaceLanguage', value: val })
}
},
customThemeVersion () {
const { themeVersion } = this.$store.state.interface
return themeVersion
},
2024-07-17 22:10:11 +03:00
isCustomThemeUsed () {
2024-11-19 01:16:51 +02:00
const { customTheme, customThemeSource } = this.mergedConfig
return customTheme != null || customThemeSource != null
2024-10-02 16:22:28 +03:00
},
isCustomStyleUsed (name) {
2024-11-19 01:16:51 +02:00
const { styleCustomData } = this.mergedConfig
return styleCustomData != null
2024-07-17 22:10:11 +03:00
},
...SharedComputedObject()
2024-07-17 17:19:57 +03:00
},
methods: {
updateFont (key, value) {
this.$store.dispatch('setOption', {
name: 'theme3hacks',
value: {
...this.mergedConfig.theme3hacks,
fonts: {
...this.mergedConfig.theme3hacks.fonts,
[key]: value
}
}
})
},
importFile () {
this.fileImporter.importData()
},
2024-11-18 03:53:37 +02:00
importParser (file, filename) {
if (filename.endsWith('.json')) {
return JSON.parse(file)
} else if (filename.endsWith('.piss')) {
return deserialize(file)
}
},
onImport (parsed, filename) {
if (filename.endsWith('.json')) {
this.$store.dispatch('setThemeCustom', parsed.source || parsed.theme)
2024-11-18 03:53:37 +02:00
} else if (filename.endsWith('.piss')) {
this.$store.dispatch('setStyleCustom', parsed)
}
},
onImportFailure (result) {
2024-11-18 03:53:37 +02:00
console.error('Failure importing theme:', result)
this.$store.dispatch('pushGlobalNotice', { messageKey: 'settings.invalid_theme_imported', level: 'error' })
},
importValidator (parsed, filename) {
if (filename.endsWith('.json')) {
const version = parsed._pleroma_theme_version
return version >= 1 || version <= 2
2024-11-18 03:53:37 +02:00
} else if (filename.endsWith('.piss')) {
if (!Array.isArray(parsed)) return false
if (parsed.length < 1) return false
if (parsed.find(x => x.component === '@meta') == null) return false
return true
}
},
2024-07-17 22:10:11 +03:00
isThemeActive (key) {
2024-12-04 14:57:28 +02:00
return key === (this.mergedConfig.theme || this.$store.state.instance.theme)
2024-07-17 22:10:11 +03:00
},
2024-10-02 16:22:28 +03:00
isStyleActive (key) {
2024-12-04 14:57:28 +02:00
return key === (this.mergedConfig.style || this.$store.state.instance.style)
2024-10-02 16:22:28 +03:00
},
isPaletteActive (key) {
2024-12-04 14:57:28 +02:00
return key === (this.mergedConfig.palette || this.$store.state.instance.palette)
},
setStyle (name) {
this.$store.dispatch('setStyle', name)
2024-10-02 16:22:28 +03:00
},
setTheme (name) {
this.$store.dispatch('setTheme', name)
},
2024-11-14 21:42:45 +02:00
setPalette (name, data) {
2024-10-02 16:22:28 +03:00
this.$store.dispatch('setPalette', name)
2024-11-14 21:42:45 +02:00
this.userPalette = data
2024-07-17 19:58:04 +03:00
},
2024-11-14 21:42:45 +02:00
setPaletteCustom (data) {
this.$store.dispatch('setPaletteCustom', data)
this.userPalette = data
2024-11-12 23:24:28 +02:00
},
2024-10-02 16:22:28 +03:00
resetTheming (name) {
this.$store.dispatch('setStyle', 'stock')
2024-10-01 00:42:33 +03:00
},
previewTheme (key, version, input) {
2024-10-01 00:42:33 +03:00
let theme3
if (this.compilationCache[key]) {
theme3 = this.compilationCache[key]
} else if (input) {
if (version === 'v2') {
const style = normalizeThemeData(input)
const theme2 = convertTheme2To3(style)
theme3 = init({
inputRuleset: theme2,
ultimateBackgroundColor: '#000000',
liteMode: true,
debug: true,
onlyNormalState: true
})
} else if (version === 'v3') {
const palette = input.find(x => x.component === '@palette')
let paletteRule
if (palette) {
const { directives } = palette
directives.link = directives.link || directives.accent
directives.accent = directives.accent || directives.link
paletteRule = {
component: 'Root',
directives: Object.fromEntries(
Object
.entries(directives)
.filter(([k, v]) => k && k !== 'name')
.map(([k, v]) => ['--' + k, 'color | ' + v])
)
}
} else {
paletteRule = null
}
theme3 = init({
inputRuleset: [...input, paletteRule].filter(x => x),
ultimateBackgroundColor: '#000000',
liteMode: true,
debug: true,
onlyNormalState: true
})
}
2024-10-01 00:42:33 +03:00
} else {
theme3 = init({
inputRuleset: [],
ultimateBackgroundColor: '#000000',
liteMode: true,
debug: true,
onlyNormalState: true
})
}
2024-07-17 17:19:57 +03:00
if (!this.compilationCache[key]) {
this.compilationCache[key] = theme3
}
2024-07-17 17:19:57 +03:00
return getScopedVersion(
getCssRules(theme3.eager),
2024-07-17 19:58:04 +03:00
'#theme-preview-' + key
2024-07-17 17:19:57 +03:00
).join('\n')
}
}
}
export default AppearanceTab