interface.js (23427B)
- import { getResourcesIndex, applyTheme, tryLoadCache } from '../services/style_setter/style_setter.js'
- import { CURRENT_VERSION, generatePreset } from 'src/services/theme_data/theme_data.service.js'
- import { convertTheme2To3 } from 'src/services/theme_data/theme2_to_theme3.js'
- import { deserialize } from '../services/theme_data/iss_deserializer.js'
- // helper for debugging
- // eslint-disable-next-line no-unused-vars
- const toValue = (x) => JSON.parse(JSON.stringify(x === undefined ? 'null' : x))
- const defaultState = {
- localFonts: null,
- themeApplied: false,
- themeChangeInProgress: false,
- themeVersion: 'v3',
- styleNameUsed: null,
- styleDataUsed: null,
- useStylePalette: false, // hack for applying styles from appearance tab
- paletteNameUsed: null,
- paletteDataUsed: null,
- themeNameUsed: null,
- themeDataUsed: null,
- temporaryChangesTimeoutId: null, // used for temporary options that revert after a timeout
- temporaryChangesConfirm: () => {}, // used for applying temporary options
- temporaryChangesRevert: () => {}, // used for reverting temporary options
- settingsModalState: 'hidden',
- settingsModalLoadedUser: false,
- settingsModalLoadedAdmin: false,
- settingsModalTargetTab: null,
- settingsModalMode: 'user',
- settings: {
- currentSaveStateNotice: null,
- noticeClearTimeout: null,
- notificationPermission: null
- },
- browserSupport: {
- cssFilter: window.CSS && window.CSS.supports && (
- window.CSS.supports('filter', 'drop-shadow(0 0)') ||
- window.CSS.supports('-webkit-filter', 'drop-shadow(0 0)')
- ),
- localFonts: typeof window.queryLocalFonts === 'function'
- },
- layoutType: 'normal',
- globalNotices: [],
- layoutHeight: 0,
- lastTimeline: null
- }
- const interfaceMod = {
- state: defaultState,
- mutations: {
- settingsSaved (state, { success, error }) {
- if (success) {
- if (state.noticeClearTimeout) {
- clearTimeout(state.noticeClearTimeout)
- }
- state.settings.currentSaveStateNotice = { error: false, data: success }
- state.settings.noticeClearTimeout = setTimeout(() => delete state.settings.currentSaveStateNotice, 2000)
- } else {
- state.settings.currentSaveStateNotice = { error: true, errorData: error }
- }
- },
- setTemporaryChanges (state, { timeoutId, confirm, revert }) {
- state.temporaryChangesTimeoutId = timeoutId
- state.temporaryChangesConfirm = confirm
- state.temporaryChangesRevert = revert
- },
- clearTemporaryChanges (state) {
- clearTimeout(state.temporaryChangesTimeoutId)
- state.temporaryChangesTimeoutId = null
- state.temporaryChangesConfirm = () => {}
- state.temporaryChangesRevert = () => {}
- },
- setThemeApplied (state) {
- state.themeApplied = true
- },
- setNotificationPermission (state, permission) {
- state.notificationPermission = permission
- },
- setLayoutType (state, value) {
- state.layoutType = value
- },
- closeSettingsModal (state) {
- state.settingsModalState = 'hidden'
- },
- togglePeekSettingsModal (state) {
- switch (state.settingsModalState) {
- case 'minimized':
- state.settingsModalState = 'visible'
- return
- case 'visible':
- state.settingsModalState = 'minimized'
- return
- default:
- throw new Error('Illegal minimization state of settings modal')
- }
- },
- openSettingsModal (state, value) {
- state.settingsModalMode = value
- state.settingsModalState = 'visible'
- if (value === 'user') {
- if (!state.settingsModalLoadedUser) {
- state.settingsModalLoadedUser = true
- }
- } else if (value === 'admin') {
- if (!state.settingsModalLoadedAdmin) {
- state.settingsModalLoadedAdmin = true
- }
- }
- },
- setSettingsModalTargetTab (state, value) {
- state.settingsModalTargetTab = value
- },
- pushGlobalNotice (state, notice) {
- state.globalNotices.push(notice)
- },
- removeGlobalNotice (state, notice) {
- state.globalNotices = state.globalNotices.filter(n => n !== notice)
- },
- setLayoutHeight (state, value) {
- state.layoutHeight = value
- },
- setLayoutWidth (state, value) {
- state.layoutWidth = value
- },
- setLastTimeline (state, value) {
- state.lastTimeline = value
- },
- setFontsList (state, value) {
- // Set is used here so that we filter out duplicate fonts (possibly same font but with different weight)
- state.localFonts = [...(new Set(value.map(font => font.family))).values()]
- }
- },
- actions: {
- setPageTitle ({ rootState }, option = '') {
- document.title = `${option} ${rootState.instance.name}`
- },
- settingsSaved ({ commit, dispatch }, { success, error }) {
- commit('settingsSaved', { success, error })
- },
- setNotificationPermission ({ commit }, permission) {
- commit('setNotificationPermission', permission)
- },
- closeSettingsModal ({ commit }) {
- commit('closeSettingsModal')
- },
- openSettingsModal ({ commit }, value = 'user') {
- commit('openSettingsModal', value)
- },
- togglePeekSettingsModal ({ commit }) {
- commit('togglePeekSettingsModal')
- },
- clearSettingsModalTargetTab ({ commit }) {
- commit('setSettingsModalTargetTab', null)
- },
- openSettingsModalTab ({ commit }, value) {
- commit('setSettingsModalTargetTab', value)
- commit('openSettingsModal', 'user')
- },
- pushGlobalNotice (
- { commit, dispatch, state },
- {
- messageKey,
- messageArgs = {},
- level = 'error',
- timeout = 0
- }) {
- const notice = {
- messageKey,
- messageArgs,
- level
- }
- commit('pushGlobalNotice', notice)
- // Adding a new element to array wraps it in a Proxy, which breaks the comparison
- // TODO: Generate UUID or something instead or relying on !== operator?
- const newNotice = state.globalNotices[state.globalNotices.length - 1]
- if (timeout) {
- setTimeout(() => dispatch('removeGlobalNotice', newNotice), timeout)
- }
- return newNotice
- },
- removeGlobalNotice ({ commit }, notice) {
- commit('removeGlobalNotice', notice)
- },
- setLayoutHeight ({ commit }, value) {
- commit('setLayoutHeight', value)
- },
- // value is optional, assuming it was cached prior
- setLayoutWidth ({ commit, state, rootGetters, rootState }, value) {
- let width = value
- if (value !== undefined) {
- commit('setLayoutWidth', value)
- } else {
- width = state.layoutWidth
- }
- const mobileLayout = width <= 800
- const normalOrMobile = mobileLayout ? 'mobile' : 'normal'
- const { thirdColumnMode } = rootGetters.mergedConfig
- if (thirdColumnMode === 'none' || !rootState.users.currentUser) {
- commit('setLayoutType', normalOrMobile)
- } else {
- const wideLayout = width >= 1300
- commit('setLayoutType', wideLayout ? 'wide' : normalOrMobile)
- }
- },
- queryLocalFonts ({ commit, dispatch, state }) {
- if (state.localFonts !== null) return
- commit('setFontsList', [])
- if (!state.browserSupport.localFonts) {
- return
- }
- window
- .queryLocalFonts()
- .then((fonts) => {
- commit('setFontsList', fonts)
- })
- .catch((e) => {
- dispatch('pushGlobalNotice', {
- messageKey: 'settings.style.themes3.font.font_list_unavailable',
- messageArgs: {
- error: e
- },
- level: 'error'
- })
- })
- },
- setLastTimeline ({ commit }, value) {
- commit('setLastTimeline', value)
- },
- async fetchPalettesIndex ({ commit, state }) {
- try {
- const value = await getResourcesIndex('/static/palettes/index.json')
- commit('setInstanceOption', { name: 'palettesIndex', value })
- return value
- } catch (e) {
- console.error('Could not fetch palettes index', e)
- commit('setInstanceOption', { name: 'palettesIndex', value: { _error: e } })
- return Promise.resolve({})
- }
- },
- setPalette ({ dispatch, commit }, value) {
- dispatch('resetThemeV3Palette')
- dispatch('resetThemeV2')
- commit('setOption', { name: 'palette', value })
- dispatch('applyTheme', { recompile: true })
- },
- setPaletteCustom ({ dispatch, commit }, value) {
- dispatch('resetThemeV3Palette')
- dispatch('resetThemeV2')
- commit('setOption', { name: 'paletteCustomData', value })
- dispatch('applyTheme', { recompile: true })
- },
- async fetchStylesIndex ({ commit, state }) {
- try {
- const value = await getResourcesIndex(
- '/static/styles/index.json',
- deserialize
- )
- commit('setInstanceOption', { name: 'stylesIndex', value })
- return value
- } catch (e) {
- console.error('Could not fetch styles index', e)
- commit('setInstanceOption', { name: 'stylesIndex', value: { _error: e } })
- return Promise.resolve({})
- }
- },
- setStyle ({ dispatch, commit, state }, value) {
- dispatch('resetThemeV3')
- dispatch('resetThemeV2')
- dispatch('resetThemeV3Palette')
- commit('setOption', { name: 'style', value })
- state.useStylePalette = true
- dispatch('applyTheme', { recompile: true }).then(() => {
- state.useStylePalette = false
- })
- },
- setStyleCustom ({ dispatch, commit, state }, value) {
- dispatch('resetThemeV3')
- dispatch('resetThemeV2')
- dispatch('resetThemeV3Palette')
- commit('setOption', { name: 'styleCustomData', value })
- state.useStylePalette = true
- dispatch('applyTheme', { recompile: true }).then(() => {
- state.useStylePalette = false
- })
- },
- async fetchThemesIndex ({ commit, state }) {
- try {
- const value = await getResourcesIndex('/static/styles.json')
- commit('setInstanceOption', { name: 'themesIndex', value })
- return value
- } catch (e) {
- console.error('Could not fetch themes index', e)
- commit('setInstanceOption', { name: 'themesIndex', value: { _error: e } })
- return Promise.resolve({})
- }
- },
- setTheme ({ dispatch, commit }, value) {
- dispatch('resetThemeV3')
- dispatch('resetThemeV3Palette')
- dispatch('resetThemeV2')
- commit('setOption', { name: 'theme', value })
- dispatch('applyTheme', { recompile: true })
- },
- setThemeCustom ({ dispatch, commit }, value) {
- dispatch('resetThemeV3')
- dispatch('resetThemeV3Palette')
- dispatch('resetThemeV2')
- commit('setOption', { name: 'customTheme', value })
- commit('setOption', { name: 'customThemeSource', value })
- dispatch('applyTheme', { recompile: true })
- },
- resetThemeV3 ({ dispatch, commit }) {
- commit('setOption', { name: 'style', value: null })
- commit('setOption', { name: 'styleCustomData', value: null })
- },
- resetThemeV3Palette ({ dispatch, commit }) {
- commit('setOption', { name: 'palette', value: null })
- commit('setOption', { name: 'paletteCustomData', value: null })
- },
- resetThemeV2 ({ dispatch, commit }) {
- commit('setOption', { name: 'theme', value: null })
- commit('setOption', { name: 'customTheme', value: null })
- commit('setOption', { name: 'customThemeSource', value: null })
- },
- async getThemeData ({ dispatch, commit, rootState, state }) {
- const getData = async (resource, index, customData, name) => {
- const capitalizedResource = resource[0].toUpperCase() + resource.slice(1)
- const result = {}
- if (customData) {
- result.nameUsed = 'custom' // custom data overrides name
- result.dataUsed = customData
- } else {
- result.nameUsed = name
- if (result.nameUsed == null) {
- result.dataUsed = null
- return result
- }
- let fetchFunc = index[result.nameUsed]
- // Fallbacks
- if (!fetchFunc) {
- if (resource === 'style' || resource === 'palette') {
- return result
- }
- const newName = Object.keys(index)[0]
- fetchFunc = index[newName]
- console.warn(`${capitalizedResource} with id '${state.styleNameUsed}' not found, trying back to '${newName}'`)
- if (!fetchFunc) {
- console.warn(`${capitalizedResource} doesn't have a fallback, defaulting to stock.`)
- fetchFunc = () => Promise.resolve(null)
- }
- }
- result.dataUsed = await fetchFunc()
- }
- return result
- }
- const {
- style: instanceStyleName,
- palette: instancePaletteName
- } = rootState.instance
- let {
- theme: instanceThemeV2Name,
- themesIndex,
- stylesIndex,
- palettesIndex
- } = rootState.instance
- const {
- style: userStyleName,
- styleCustomData: userStyleCustomData,
- palette: userPaletteName,
- paletteCustomData: userPaletteCustomData
- } = rootState.config
- let {
- theme: userThemeV2Name,
- customTheme: userThemeV2Snapshot,
- customThemeSource: userThemeV2Source
- } = rootState.config
- let majorVersionUsed
- console.debug(
- `User V3 palette: ${userPaletteName}, style: ${userStyleName} , custom: ${!!userStyleCustomData}`
- )
- console.debug(
- `User V2 name: ${userThemeV2Name}, source: ${!!userThemeV2Source}, snapshot: ${!!userThemeV2Snapshot}`
- )
- console.debug(`Instance V3 palette: ${instancePaletteName}, style: ${instanceStyleName}`)
- console.debug('Instance V2 theme: ' + instanceThemeV2Name)
- if (userPaletteName || userPaletteCustomData ||
- userStyleName || userStyleCustomData ||
- (
- // User V2 overrides instance V3
- (instancePaletteName ||
- instanceStyleName) &&
- instanceThemeV2Name == null &&
- userThemeV2Name == null
- )
- ) {
- // Palette and/or style overrides V2 themes
- instanceThemeV2Name = null
- userThemeV2Name = null
- userThemeV2Source = null
- userThemeV2Snapshot = null
- majorVersionUsed = 'v3'
- } else if (
- (userThemeV2Name ||
- userThemeV2Snapshot ||
- userThemeV2Source ||
- instanceThemeV2Name)
- ) {
- majorVersionUsed = 'v2'
- } else {
- // if all fails fallback to v3
- majorVersionUsed = 'v3'
- }
- if (majorVersionUsed === 'v3') {
- const result = await Promise.all([
- dispatch('fetchPalettesIndex'),
- dispatch('fetchStylesIndex')
- ])
- palettesIndex = result[0]
- stylesIndex = result[1]
- } else {
- // Promise.all just to be uniform with v3
- const result = await Promise.all([
- dispatch('fetchThemesIndex')
- ])
- themesIndex = result[0]
- }
- state.themeVersion = majorVersionUsed
- console.debug('Version used', majorVersionUsed)
- if (majorVersionUsed === 'v3') {
- state.themeDataUsed = null
- state.themeNameUsed = null
- const style = await getData(
- 'style',
- stylesIndex,
- userStyleCustomData,
- userStyleName || instanceStyleName
- )
- state.styleNameUsed = style.nameUsed
- state.styleDataUsed = style.dataUsed
- let firstStylePaletteName = null
- style
- .dataUsed
- ?.filter(x => x.component === '@palette')
- .map(x => {
- const cleanDirectives = Object.fromEntries(
- Object
- .entries(x.directives)
- .filter(([k, v]) => k)
- )
- return { name: x.variant, ...cleanDirectives }
- })
- .forEach(palette => {
- const key = 'style.' + palette.name.toLowerCase().replace(/ /g, '_')
- if (!firstStylePaletteName) firstStylePaletteName = key
- palettesIndex[key] = () => Promise.resolve(palette)
- })
- const palette = await getData(
- 'palette',
- palettesIndex,
- userPaletteCustomData,
- state.useStylePalette ? firstStylePaletteName : (userPaletteName || instancePaletteName)
- )
- if (state.useStylePalette) {
- commit('setOption', { name: 'palette', value: firstStylePaletteName })
- }
- state.paletteNameUsed = palette.nameUsed
- state.paletteDataUsed = palette.dataUsed
- if (state.paletteDataUsed) {
- state.paletteDataUsed.link = state.paletteDataUsed.link || state.paletteDataUsed.accent
- state.paletteDataUsed.accent = state.paletteDataUsed.accent || state.paletteDataUsed.link
- }
- if (Array.isArray(state.paletteDataUsed)) {
- const [
- name,
- bg,
- fg,
- text,
- link,
- cRed = '#FF0000',
- cGreen = '#00FF00',
- cBlue = '#0000FF',
- cOrange = '#E3FF00'
- ] = palette.dataUsed
- state.paletteDataUsed = {
- name,
- bg,
- fg,
- text,
- link,
- accent: link,
- cRed,
- cBlue,
- cGreen,
- cOrange
- }
- }
- console.debug('Palette data used', palette.dataUsed)
- } else {
- state.styleNameUsed = null
- state.styleDataUsed = null
- state.paletteNameUsed = null
- state.paletteDataUsed = null
- const theme = await getData(
- 'theme',
- themesIndex,
- userThemeV2Source || userThemeV2Snapshot,
- userThemeV2Name || instanceThemeV2Name
- )
- state.themeNameUsed = theme.nameUsed
- state.themeDataUsed = theme.dataUsed
- }
- },
- async applyTheme (
- { dispatch, commit, rootState, state },
- { recompile = false } = {}
- ) {
- const {
- forceThemeRecompilation,
- themeDebug,
- theme3hacks
- } = rootState.config
- state.themeChangeInProgress = true
- // If we're not not forced to recompile try using
- // cache (tryLoadCache return true if load successful)
- const forceRecompile = forceThemeRecompilation || recompile
- if (!forceRecompile && !themeDebug && await tryLoadCache()) {
- state.themeChangeInProgress = false
- return commit('setThemeApplied')
- }
- window.splashUpdate('splash.theme')
- await dispatch('getThemeData')
- try {
- const paletteIss = (() => {
- if (!state.paletteDataUsed) return null
- const result = {
- component: 'Root',
- directives: {}
- }
- Object
- .entries(state.paletteDataUsed)
- .filter(([k]) => k !== 'name')
- .forEach(([k, v]) => {
- let issRootDirectiveName
- switch (k) {
- case 'background':
- issRootDirectiveName = 'bg'
- break
- case 'foreground':
- issRootDirectiveName = 'fg'
- break
- default:
- issRootDirectiveName = k
- }
- result.directives['--' + issRootDirectiveName] = 'color | ' + v
- })
- return result
- })()
- const theme2ruleset = state.themeDataUsed && convertTheme2To3(normalizeThemeData(state.themeDataUsed))
- const hacks = []
- Object.entries(theme3hacks).forEach(([key, value]) => {
- switch (key) {
- case 'fonts': {
- Object.entries(theme3hacks.fonts).forEach(([fontKey, font]) => {
- if (!font?.family) return
- switch (fontKey) {
- case 'interface':
- hacks.push({
- component: 'Root',
- directives: {
- '--font': 'generic | ' + font.family
- }
- })
- break
- case 'input':
- hacks.push({
- component: 'Input',
- directives: {
- '--font': 'generic | ' + font.family
- }
- })
- break
- case 'post':
- hacks.push({
- component: 'RichContent',
- directives: {
- '--font': 'generic | ' + font.family
- }
- })
- break
- case 'monospace':
- hacks.push({
- component: 'Root',
- directives: {
- '--monoFont': 'generic | ' + font.family
- }
- })
- break
- }
- })
- break
- }
- case 'underlay': {
- if (value !== 'none') {
- const newRule = {
- component: 'Underlay',
- directives: {}
- }
- if (value === 'opaque') {
- newRule.directives.opacity = 1
- newRule.directives.background = '--wallpaper'
- }
- if (value === 'transparent') {
- newRule.directives.opacity = 0
- }
- hacks.push(newRule)
- }
- break
- }
- }
- })
- const rulesetArray = [
- theme2ruleset,
- state.styleDataUsed,
- paletteIss,
- hacks
- ].filter(x => x)
- return applyTheme(
- rulesetArray.flat(),
- () => commit('setThemeApplied'),
- () => {
- state.themeChangeInProgress = false
- },
- themeDebug
- )
- } catch (e) {
- window.splashError(e)
- }
- }
- }
- }
- export default interfaceMod
- export const normalizeThemeData = (input) => {
- let themeData, themeSource
- if (input.themeFileVerison === 1) {
- // this might not be even used at all, some leftover of unimplemented code in V2 editor
- return generatePreset(input).theme
- } else if (
- Object.prototype.hasOwnProperty.call(input, '_pleroma_theme_version') ||
- Object.prototype.hasOwnProperty.call(input, 'source') ||
- Object.prototype.hasOwnProperty.call(input, 'theme')
- ) {
- // We got passed a full theme file
- themeData = input.theme
- themeSource = input.source
- } else if (
- Object.prototype.hasOwnProperty.call(input, 'themeEngineVersion') ||
- Object.prototype.hasOwnProperty.call(input, 'colors')
- ) {
- // We got passed a source/snapshot
- themeData = input
- themeSource = input
- }
- // New theme presets don't have 'theme' property, they use 'source'
- let out // shout, shout let it all out
- if (themeSource && themeSource.themeEngineVersion === CURRENT_VERSION) {
- // There are some themes in wild that have completely broken source
- out = { ...(themeData || {}), ...themeSource }
- } else {
- out = themeData
- }
- // generatePreset here basically creates/updates "snapshot",
- // while also fixing the 2.2 -> 2.3 colors/shadows/etc
- return generatePreset(out).theme
- }