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

496 lines
14 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'
2025-02-28 10:52:04 -05:00
import UnitSetting from '../helpers/unit_setting.vue'
2024-11-14 21:42:45 +02:00
import PaletteEditor from 'src/components/palette_editor/palette_editor.vue'
2025-11-24 17:06:55 +02:00
import Preview from './old_theme_tab/theme_preview.vue'
2024-06-27 00:59:24 +03:00
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'
2026-01-06 16:22:52 +02:00
import { getCssRules } 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'
2026-01-06 16:22:52 +02:00
import {
createStyleSheet,
adoptStyleSheets,
} from 'src/services/style_setter/style_setter.js'
2025-08-04 14:04:28 +03:00
import fileSizeFormatService from 'src/components/../services/file_size_format/file_size_format.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'
2025-02-03 00:14:44 +02:00
import { mapActions } from 'pinia'
import { useInterfaceStore, normalizeThemeData } from 'src/stores/interface'
const AppearanceTab = {
2026-01-06 16:22:52 +02:00
data() {
2024-06-13 02:22:47 +03:00
return {
availableThemesV3: [],
availableThemesV2: [],
2024-11-12 23:24:28 +02:00
bundledPalettes: [],
compilationCache: {},
fileImporter: newImporter({
2024-12-30 03:18:45 +02:00
accept: '.json, .iss',
validator: this.importValidator,
onImport: this.onImport,
2024-11-18 03:53:37 +02:00
parser: this.importParser,
2026-01-06 16:22:52 +02:00
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',
2026-01-06 16:22:52 +02:00
'cOrange',
2024-10-01 00:42:33 +03:00
],
2024-11-14 21:42:45 +02:00
userPalette: {},
2024-07-17 19:58:04 +03:00
intersectionObserver: null,
2026-01-06 16:22:52 +02: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}`,
),
}),
),
2025-02-04 15:23:21 +02:00
underlayOverrideModes: ['none', 'opaque', 'transparent'].map((mode) => ({
key: mode,
value: mode,
2026-01-06 16:22:52 +02:00
label: this.$t(
`settings.style.themes3.hacks.underlay_override_mode_${mode}`,
),
2025-08-04 14:04:28 +03:00
})),
backgroundUploading: false,
background: null,
backgroundPreview: null,
2024-06-13 02:22:47 +03:00
}
},
components: {
BooleanSetting,
ChoiceSetting,
IntegerSetting,
FloatSetting,
UnitSetting,
2024-06-27 00:59:24 +03:00
ProfileSettingIndicator,
2024-11-14 21:42:45 +02:00
Preview,
2026-01-06 16:22:52 +02:00
PaletteEditor,
2024-07-17 17:19:57 +03:00
},
2026-01-06 16:22:52 +02:00
mounted() {
2025-02-03 00:14:44 +02:00
useInterfaceStore().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 {
2025-02-03 00:14:44 +02:00
promise = useInterfaceStore()[`fetch${capitalizedResource}sIndex`]()
}
2026-01-06 16:22:52 +02:00
return promise.then((index) => {
return Object.entries(index).map(([k, func]) => [k, func()])
2024-07-17 19:58:04 +03:00
})
}
2026-01-06 16:22:52 +02: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',
})
}),
)
})
2026-01-06 16:22:52 +02:00
updateIndex('theme').then((themes) => {
themes.forEach(([key, themePromise]) =>
themePromise.then((data) => {
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',
})
}
}),
)
})
2025-01-30 21:56:07 +02:00
this.userPalette = useInterfaceStore().paletteDataUsed || {}
2026-01-06 16:22:52 +02:00
updateIndex('palette').then((bundledPalettes) => {
bundledPalettes.forEach(([key, palettePromise]) =>
palettePromise.then((v) => {
let palette
if (Array.isArray(v)) {
const [
name,
bg,
fg,
text,
link,
cRed = '#FF0000',
cGreen = '#00FF00',
cBlue = '#0000FF',
cOrange = '#E3FF00',
] = v
palette = {
key,
name,
bg,
fg,
text,
link,
cRed,
cBlue,
cGreen,
cOrange,
}
} else {
palette = { key, ...v }
}
if (!palette.key.startsWith('style.')) {
this.bundledPalettes.push(palette)
}
}),
)
})
2024-07-17 19:58:04 +03:00
2025-06-28 21:43:50 +03:00
this.previewTheme('stock', 'v3')
2024-07-17 19:58:04 +03:00
if (window.IntersectionObserver) {
2026-01-06 16:22:52 +02:00
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(() => {
if (theme) this.previewTheme(theme.key, theme.version, theme.data)
})
observer.unobserve(target)
2024-07-17 19:58:04 +03:00
})
2026-01-06 16:22:52 +02:00
},
{
root: this.$refs.themeList,
},
)
2025-06-28 22:21:07 +03:00
} else {
2026-01-06 16:22:52 +02:00
this.availableStyles.forEach((theme) =>
this.previewTheme(theme.key, theme.version, theme.data),
)
2024-07-17 19:58:04 +03:00
}
},
2026-01-06 16:22:52 +02:00
updated() {
2024-07-17 19:58:04 +03:00
this.$nextTick(() => {
2026-01-06 16:22:52 +02:00
this.$refs.themeList
.querySelectorAll('.theme-preview')
.forEach((node) => {
this.intersectionObserver.observe(node)
})
2024-07-17 19:58:04 +03:00
})
},
watch: {
2026-01-06 16:22:52 +02:00
paletteDataUsed() {
this.userPalette = this.paletteDataUsed || {}
2026-01-06 16:22:52 +02:00
},
},
computed: {
2026-01-06 16:22:52 +02:00
isDefaultBackground() {
return !this.$store.state.users.currentUser.background_image
2025-08-05 18:20:06 +03:00
},
2026-01-06 16:22:52 +02:00
switchInProgress() {
2025-01-30 21:56:07 +02:00
return useInterfaceStore().themeChangeInProgress
},
2026-01-06 16:22:52 +02:00
paletteDataUsed() {
2025-01-30 21:56:07 +02:00
return useInterfaceStore().paletteDataUsed
},
2026-01-06 16:22:52 +02:00
availableStyles() {
return [...this.availableThemesV3, ...this.availableThemesV2]
},
2026-01-06 16:22:52 +02:00
availablePalettes() {
return [...this.bundledPalettes, ...this.stylePalettes]
2024-11-12 23:24:28 +02:00
},
2026-01-06 16:22:52 +02:00
stylePalettes() {
2025-01-30 21:56:07 +02:00
const ruleset = useInterfaceStore().styleDataUsed || []
if (!ruleset && ruleset.length === 0) return
2026-01-06 16:22:52 +02:00
const meta = ruleset.find((x) => x.component === '@meta')
const result = ruleset
.filter((x) => x.component.startsWith('@palette'))
.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,
2026-01-06 16:22:52 +02:00
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,
2026-01-06 16:22:52 +02:00
wallpaper,
2024-11-12 23:24:28 +02:00
}
2025-02-04 15:23:21 +02:00
return Object.fromEntries(Object.entries(result).filter(([, v]) => v))
2024-11-12 23:24:28 +02:00
})
return result
},
2026-01-06 16:22:52 +02:00
noIntersectionObserver() {
2024-07-17 19:58:04 +03:00
return !window.IntersectionObserver
},
2026-01-06 16:22:52 +02:00
instanceWallpaper() {
2025-11-27 19:33:47 +02:00
this.$store.state.instance.background
},
2026-01-06 16:22:52 +02:00
instanceWallpaperUsed() {
return (
this.$store.state.instance.background &&
!this.$store.state.users.currentUser.background_image
2026-01-06 16:22:52 +02:00
)
},
2026-01-06 16:22:52 +02:00
customThemeVersion() {
2025-01-30 21:56:07 +02:00
const { themeVersion } = useInterfaceStore()
return themeVersion
},
2026-01-06 16:22:52 +02: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
},
2026-01-06 16:22:52 +02:00
isCustomStyleUsed() {
2024-11-19 01:16:51 +02:00
const { styleCustomData } = this.mergedConfig
return styleCustomData != null
2024-07-17 22:10:11 +03:00
},
2026-01-06 16:22:52 +02:00
...SharedComputedObject(),
2024-07-17 17:19:57 +03:00
},
methods: {
2026-01-06 16:22:52 +02:00
importFile() {
this.fileImporter.importData()
},
2026-01-06 16:22:52 +02:00
importParser(file, filename) {
2024-11-18 03:53:37 +02:00
if (filename.endsWith('.json')) {
return JSON.parse(file)
2024-12-30 03:18:45 +02:00
} else if (filename.endsWith('.iss')) {
2024-11-18 03:53:37 +02:00
return deserialize(file)
}
},
2026-01-06 16:22:52 +02:00
onImport(parsed, filename) {
if (filename.endsWith('.json')) {
2025-02-03 00:14:44 +02:00
useInterfaceStore().setThemeCustom(parsed.source || parsed.theme)
2024-12-30 03:18:45 +02:00
} else if (filename.endsWith('.iss')) {
2025-02-03 00:14:44 +02:00
useInterfaceStore().setStyleCustom(parsed)
}
},
2026-01-06 16:22:52 +02:00
onImportFailure(result) {
2024-11-18 03:53:37 +02:00
console.error('Failure importing theme:', result)
2026-01-06 16:22:52 +02:00
useInterfaceStore().pushGlobalNotice({
messageKey: 'settings.invalid_theme_imported',
level: 'error',
})
},
2026-01-06 16:22:52 +02:00
importValidator(parsed, filename) {
if (filename.endsWith('.json')) {
const version = parsed._pleroma_theme_version
return version >= 1 || version <= 2
2024-12-30 03:18:45 +02:00
} else if (filename.endsWith('.iss')) {
2024-11-18 03:53:37 +02:00
if (!Array.isArray(parsed)) return false
if (parsed.length < 1) return false
2026-01-06 16:22:52 +02:00
if (parsed.find((x) => x.component === '@meta') == null) return false
2024-11-18 03:53:37 +02:00
return true
}
},
2026-01-06 16:22:52 +02:00
isThemeActive(key) {
return (
key === (this.mergedConfig.theme || this.$store.state.instance.theme)
)
2024-07-17 22:10:11 +03:00
},
2026-01-06 16:22:52 +02:00
isStyleActive(key) {
return (
key === (this.mergedConfig.style || this.$store.state.instance.style)
)
2024-10-02 16:22:28 +03:00
},
2026-01-06 16:22:52 +02:00
isPaletteActive(key) {
return (
key ===
(this.mergedConfig.palette || this.$store.state.instance.palette)
)
},
2026-01-06 16:22:52 +02:00
...mapActions(useInterfaceStore, ['setStyle', 'setTheme']),
setPalette(name, data) {
2025-02-03 00:14:44 +02:00
useInterfaceStore().setPalette(name)
2024-11-14 21:42:45 +02:00
this.userPalette = data
2024-07-17 19:58:04 +03:00
},
2026-01-06 16:22:52 +02:00
setPaletteCustom(data) {
2025-02-03 00:14:44 +02:00
useInterfaceStore().setPaletteCustom(data)
2024-11-14 21:42:45 +02:00
this.userPalette = data
2024-11-12 23:24:28 +02:00
},
2026-01-06 16:22:52 +02:00
resetTheming() {
2025-02-03 00:14:44 +02:00
useInterfaceStore().setStyle('stock')
2024-10-01 00:42:33 +03:00
},
2026-01-06 16:22:52 +02: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,
2026-01-06 16:22:52 +02:00
onlyNormalState: true,
})
} else if (version === 'v3') {
2026-01-06 16:22:52 +02:00
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(
2026-01-06 16:22:52 +02:00
Object.entries(directives)
2025-02-04 15:23:21 +02:00
.filter(([k]) => k && k !== 'name')
2026-01-06 16:22:52 +02:00
.map(([k, v]) => ['--' + k, 'color | ' + v]),
),
}
} else {
paletteRule = null
}
theme3 = init({
2026-01-06 16:22:52 +02:00
inputRuleset: [...input, paletteRule].filter((x) => x),
ultimateBackgroundColor: '#000000',
liteMode: true,
2026-01-06 16:22:52 +02:00
onlyNormalState: true,
})
}
2024-10-01 00:42:33 +03:00
} else {
theme3 = init({
inputRuleset: [],
ultimateBackgroundColor: '#000000',
liteMode: true,
2026-01-06 16:22:52 +02:00
onlyNormalState: true,
2024-10-01 00:42:33 +03:00
})
}
2024-07-17 17:19:57 +03:00
if (!this.compilationCache[key]) {
this.compilationCache[key] = theme3
}
2025-07-03 17:11:23 +03:00
const sheet = createStyleSheet('appearance-tab-previews', 90)
2026-01-06 16:22:52 +02:00
sheet.addRule(
[
'#theme-preview-',
key,
' {\n',
getCssRules(theme3.eager).join('\n'),
'\n}',
].join(''),
)
2025-07-02 22:54:45 +03:00
sheet.ready = true
adoptStyleSheets()
2025-08-04 14:04:28 +03:00
},
2026-01-06 16:22:52 +02:00
uploadFile(slot, e) {
2025-08-04 14:04:28 +03:00
const file = e.target.files[0]
2026-01-06 16:22:52 +02:00
if (!file) {
return
}
2025-08-04 14:04:28 +03:00
if (file.size > this.$store.state.instance[slot + 'limit']) {
const filesize = fileSizeFormatService.fileSizeFormat(file.size)
2026-01-06 16:22:52 +02:00
const allowedsize = fileSizeFormatService.fileSizeFormat(
this.$store.state.instance[slot + 'limit'],
)
2025-08-04 14:04:28 +03:00
useInterfaceStore().pushGlobalNotice({
messageKey: 'upload.error.message',
messageArgs: [
this.$t('upload.error.file_too_big', {
filesize: filesize.num,
filesizeunit: filesize.unit,
allowedsize: allowedsize.num,
2026-01-06 16:22:52 +02:00
allowedsizeunit: allowedsize.unit,
}),
2025-08-04 14:04:28 +03:00
],
2026-01-06 16:22:52 +02:00
level: 'error',
2025-08-04 14:04:28 +03:00
})
return
}
const reader = new FileReader()
reader.onload = ({ target }) => {
const img = target.result
this[slot + 'Preview'] = img
this[slot] = file
}
reader.readAsDataURL(file)
},
2026-01-06 16:22:52 +02:00
resetBackground() {
const confirmed = window.confirm(
this.$t('settings.reset_background_confirm'),
)
2025-08-04 14:04:28 +03:00
if (confirmed) {
this.submitBackground('')
}
},
2026-01-06 16:22:52 +02:00
resetUploadedBackground() {
2025-11-27 19:33:47 +02:00
this.backgroundPreview = null
},
2026-01-06 16:22:52 +02:00
submitBackground(background) {
if (!this.backgroundPreview && background !== '') {
return
}
2025-08-04 14:04:28 +03:00
this.backgroundUploading = true
2026-01-06 16:22:52 +02:00
this.$store.state.api.backendInteractor
.updateProfileImages({ background })
2025-08-04 14:04:28 +03:00
.then((data) => {
this.$store.commit('addNewUsers', [data])
this.$store.commit('setCurrentUser', data)
this.backgroundPreview = null
})
.catch(this.displayUploadError)
2026-01-06 16:22:52 +02:00
.finally(() => {
this.backgroundUploading = false
})
2025-08-04 14:04:28 +03:00
},
2026-01-06 16:22:52 +02:00
},
}
export default AppearanceTab