pleroma-fe/src/modules/interface.js

654 lines
20 KiB
JavaScript
Raw Normal View History

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'
2018-09-09 19:36:13 +03:00
const defaultState = {
localFonts: null,
themeApplied: false,
themeVersion: 'v3',
temporaryChangesTimeoutId: null, // used for temporary options that revert after a timeout
temporaryChangesConfirm: () => {}, // used for applying temporary options
temporaryChangesRevert: () => {}, // used for reverting temporary options
2020-05-03 17:36:12 +03:00
settingsModalState: 'hidden',
2023-03-14 21:50:43 +02:00
settingsModalLoadedUser: false,
settingsModalLoadedAdmin: false,
settingsModalTargetTab: null,
2023-03-14 21:50:43 +02:00
settingsModalMode: 'user',
2018-09-09 19:36:13 +03:00
settings: {
currentSaveStateNotice: null,
2018-12-13 18:04:09 +07:00
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'
2019-03-03 16:33:40 +02:00
},
layoutType: 'normal',
2020-05-07 16:10:53 +03:00
globalNotices: [],
layoutHeight: 0,
lastTimeline: null
2018-09-09 19:36:13 +03:00
}
const interfaceMod = {
state: defaultState,
mutations: {
settingsSaved (state, { success, error }) {
if (success) {
if (state.noticeClearTimeout) {
clearTimeout(state.noticeClearTimeout)
}
2021-04-25 13:24:08 +03:00
state.settings.currentSaveStateNotice = { error: false, data: success }
state.settings.noticeClearTimeout = setTimeout(() => delete state.settings.currentSaveStateNotice, 2000)
2018-09-09 19:36:13 +03:00
} else {
2021-04-25 13:24:08 +03:00
state.settings.currentSaveStateNotice = { error: true, errorData: error }
2018-09-09 19:36:13 +03:00
}
2018-12-13 18:04:09 +07:00
},
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
},
2018-12-13 18:04:09 +07:00
setNotificationPermission (state, permission) {
state.notificationPermission = permission
2019-03-03 16:33:40 +02:00
},
setLayoutType (state, value) {
state.layoutType = value
2020-05-03 17:36:12 +03:00
},
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')
}
},
2023-03-14 21:50:43 +02:00
openSettingsModal (state, value) {
state.settingsModalMode = value
2020-05-03 17:36:12 +03:00
state.settingsModalState = 'visible'
2023-03-14 21:50:43 +02:00
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)
2020-05-07 16:10:53 +03:00
},
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()]
2018-09-09 19:36:13 +03:00
}
},
actions: {
2018-09-09 21:21:23 +03:00
setPageTitle ({ rootState }, option = '') {
document.title = `${option} ${rootState.instance.name}`
2018-09-09 19:36:13 +03:00
},
settingsSaved ({ commit, dispatch }, { success, error }) {
commit('settingsSaved', { success, error })
2018-12-13 18:04:09 +07:00
},
setNotificationPermission ({ commit }, permission) {
commit('setNotificationPermission', permission)
2019-03-03 16:33:40 +02:00
},
2020-05-03 17:36:12 +03:00
closeSettingsModal ({ commit }) {
commit('closeSettingsModal')
},
2023-03-14 21:50:43 +02:00
openSettingsModal ({ commit }, value = 'user') {
commit('openSettingsModal', value)
2020-05-03 17:36:12 +03:00
},
togglePeekSettingsModal ({ commit }) {
commit('togglePeekSettingsModal')
},
clearSettingsModalTargetTab ({ commit }) {
commit('setSettingsModalTargetTab', null)
},
openSettingsModalTab ({ commit }, value) {
commit('setSettingsModalTargetTab', value)
2023-06-13 14:00:20 -04:00
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)
2020-05-07 16:10:53 +03:00
},
setLayoutHeight ({ commit }, value) {
commit('setLayoutHeight', value)
},
// value is optional, assuming it was cached prior
2022-05-09 23:24:35 +03:00
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
2022-05-09 23:24:35 +03:00
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)
return {}
}
},
setPalette ({ dispatch, commit }, value) {
2024-10-02 16:22:28 +03:00
dispatch('resetThemeV3Palette')
dispatch('resetThemeV2')
commit('setOption', { name: 'palette', value })
dispatch('applyTheme')
},
setPaletteCustom ({ dispatch, commit }, value) {
2024-10-02 16:22:28 +03:00
dispatch('resetThemeV3Palette')
dispatch('resetThemeV2')
commit('setOption', { name: 'paletteCustomData', value })
dispatch('applyTheme')
},
async fetchStylesIndex ({ commit, state }) {
try {
const value = await getResourcesIndex('/static/styles/index.json')
commit('setInstanceOption', { name: 'stylesIndex', value })
return value
} catch (e) {
console.error('Could not fetch styles index', e)
return Promise.resolve({})
}
},
setStyle ({ dispatch, commit }, value) {
2024-10-02 16:22:28 +03:00
dispatch('resetThemeV3')
dispatch('resetThemeV2')
commit('setOption', { name: 'style', value })
dispatch('applyTheme')
},
setStyleCustom ({ dispatch, commit }, value) {
2024-10-02 16:22:28 +03:00
dispatch('resetThemeV3')
dispatch('resetThemeV2')
commit('setOption', { name: 'styleCustomData', value })
dispatch('applyTheme')
},
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)
return Promise.resolve({})
}
},
setTheme ({ dispatch, commit }, value) {
2024-10-02 16:22:28 +03:00
dispatch('resetThemeV3')
dispatch('resetThemeV3Palette')
dispatch('resetThemeV2')
commit('setOption', { name: 'theme', value })
dispatch('applyTheme')
2024-10-01 00:42:33 +03:00
},
setThemeCustom ({ dispatch, commit }, value) {
2024-10-02 16:22:28 +03:00
dispatch('resetThemeV3')
dispatch('resetThemeV3Palette')
dispatch('resetThemeV2')
commit('setOption', { name: 'customTheme', value })
commit('setOption', { name: 'customThemeSource', value })
dispatch('applyTheme')
},
2024-10-02 16:22:28 +03:00
resetThemeV3 ({ dispatch, commit }) {
commit('setOption', { name: 'style', value: null })
commit('setOption', { name: 'styleCustomData', value: null })
2024-10-02 16:22:28 +03:00
},
resetThemeV3Palette ({ dispatch, commit }) {
commit('setOption', { name: 'palette', value: null })
commit('setOption', { name: 'paletteCustomData', value: null })
},
2024-10-02 16:22:28 +03:00
resetThemeV2 ({ dispatch, commit }) {
commit('setOption', { name: 'theme', value: null })
commit('setOption', { name: 'customTheme', value: null })
commit('setOption', { name: 'customThemeSource', value: null })
},
async applyTheme (
{ dispatch, commit, rootState, state },
{ recompile = true } = {}
) {
// If we're not not forced to recompile try using
// cache (tryLoadCache return true if load successful)
const {
style: instanceStyleName,
palette: instancePaletteName
} = rootState.instance
let {
theme: instanceThemeV2Name,
themesIndex,
stylesIndex,
palettesIndex
} = rootState.instance
const {
style: userStyleName,
styleCustomData: userStyleCustomData,
palette: userPaletteName,
paletteCustomData: userPaletteCustomData,
forceThemeRecompilation,
themeDebug,
theme3hacks
} = rootState.config
let {
theme: userThemeV2Name,
customTheme: userThemeV2Snapshot,
customThemeSource: userThemeV2Source
} = rootState.config
const forceRecompile = forceThemeRecompilation || recompile
if (!forceRecompile && !themeDebug && tryLoadCache()) {
return commit('setThemeApplied')
}
let majorVersionUsed
2024-10-02 16:22:28 +03:00
console.log(
`USER V3 palette: ${userPaletteName}, style: ${userStyleName} `
2024-10-02 16:22:28 +03:00
)
console.log(
`USER V2 name: ${userThemeV2Name}, source: ${userThemeV2Source}, snapshot: ${userThemeV2Snapshot}`
2024-10-02 16:22:28 +03:00
)
console.log(`INST V3 palette: ${instancePaletteName}, style: ${instanceStyleName}`)
console.log('INST V2 theme: ' + instanceThemeV2Name)
2024-10-02 16:22:28 +03:00
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'
if (!palettesIndex || !stylesIndex) {
const result = await Promise.all([
dispatch('fetchPalettesIndex'),
dispatch('fetchStylesIndex')
])
palettesIndex = result[0]
stylesIndex = result[1]
}
2024-10-02 16:22:28 +03:00
} else if (
userThemeV2Name ||
userThemeV2Snapshot ||
userThemeV2Source ||
instanceThemeV2Name
) {
majorVersionUsed = 'v2'
// Promise.all just to be uniform with v3
const result = await Promise.all([
dispatch('fetchThemesIndex')
])
themesIndex = result[0]
2024-10-02 16:22:28 +03:00
} else {
majorVersionUsed = 'v3'
}
state.themeVersion = majorVersionUsed
let styleDataUsed = null
let styleNameUsed = null
let paletteDataUsed = null
let paletteNameUsed = null
let themeNameUsed = null
let themeDataUsed = null
2024-10-02 16:22:28 +03:00
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 {
2024-10-02 16:22:28 +03:00
result.nameUsed = name
if (result.nameUsed === 'stock') {
result.dataUsed = null
return result
}
2024-10-02 16:22:28 +03:00
let fetchFunc = index[result.nameUsed]
// Fallbacks
if (!fetchFunc) {
const newName = Object.keys(index)[0]
fetchFunc = index[newName]
console.warn(`${capitalizedResource} with id '${styleNameUsed}' not found, trying back to '${newName}'`)
if (!fetchFunc) {
console.warn(`${capitalizedResource} doesn't have a fallback, defaulting to stock.`)
fetchFunc = () => Promise.resolve(null)
}
}
2024-10-02 16:22:28 +03:00
result.dataUsed = await fetchFunc()
}
2024-10-02 16:22:28 +03:00
return result
}
console.log('VERSION', majorVersionUsed)
if (majorVersionUsed === 'v3') {
const palette = await getData(
'palette',
palettesIndex,
userPaletteCustomData,
userPaletteName || instancePaletteName
)
paletteNameUsed = palette.nameUsed
paletteDataUsed = palette.dataUsed
if (Array.isArray(paletteDataUsed)) {
const [
name,
background,
foreground,
text,
link,
cRed = '#FF0000',
cGreen = '#00FF00',
2024-10-02 16:29:33 +03:00
cBlue = '#0000FF',
2024-10-02 16:22:28 +03:00
cOrange = '#E3FF00'
] = paletteDataUsed
paletteDataUsed = { name, background, foreground, text, link, cRed, cBlue, cGreen, cOrange }
}
2024-10-02 16:22:28 +03:00
console.log('PAL', userPaletteName, paletteNameUsed)
console.log('PAL', paletteDataUsed)
const style = await getData(
'style',
stylesIndex,
userStyleCustomData,
userStyleName || instanceStyleName
)
styleNameUsed = style.nameUsed
styleDataUsed = style.dataUsed
} else {
const theme = await getData(
'theme',
themesIndex,
userThemeV2Source || userThemeV2Snapshot,
userThemeV2Name || instanceThemeV2Name
)
themeNameUsed = theme.nameUsed
2024-10-04 14:58:23 +03:00
themeDataUsed = { ...theme.dataUsed, themeEngineVersion: CURRENT_VERSION }
2024-10-02 16:22:28 +03:00
// Themes v2 editor support
commit('setInstanceOption', { name: 'themeData', value: themeDataUsed })
}
2024-10-02 16:22:28 +03:00
console.log('STYLE', styleNameUsed, paletteNameUsed, themeNameUsed)
// commit('setOption', { name: 'palette', value: paletteNameUsed })
// commit('setOption', { name: 'style', value: styleNameUsed })
// commit('setOption', { name: 'theme', value: themeNameUsed })
const paletteIss = (() => {
if (!paletteDataUsed) return null
2024-10-01 00:42:33 +03:00
const result = {
component: 'Root',
directives: {}
}
2024-10-02 16:22:28 +03:00
console.log('PALETTE', paletteDataUsed)
2024-10-01 00:42:33 +03:00
Object
.entries(paletteDataUsed)
2024-10-01 00:42:33 +03:00
.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 = themeDataUsed && convertTheme2To3(normalizeThemeData(themeDataUsed))
const hacks = []
2024-07-17 22:10:11 +03:00
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
2024-07-17 22:10:11 +03:00
}
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
2024-10-01 00:42:33 +03:00
}
}
})
const rulesetArray = [
theme2ruleset,
styleDataUsed,
paletteIss,
hacks
].filter(x => x)
return applyTheme(
rulesetArray.flat(),
() => commit('setThemeApplied'),
themeDebug
)
2018-09-09 19:36:13 +03:00
}
}
}
export default interfaceMod
2024-07-17 17:19:57 +03:00
export const normalizeThemeData = (input) => {
2024-07-25 19:19:46 +03:00
if (Array.isArray(input)) {
const themeData = { colors: {} }
2024-07-17 17:19:57 +03:00
themeData.colors.bg = input[1]
themeData.colors.fg = input[2]
themeData.colors.text = input[3]
themeData.colors.link = input[4]
themeData.colors.cRed = input[5]
themeData.colors.cGreen = input[6]
themeData.colors.cBlue = input[7]
themeData.colors.cOrange = input[8]
2024-10-01 00:42:33 +03:00
return generatePreset(themeData).source || generatePreset(themeData).theme
2024-07-17 17:19:57 +03:00
}
2024-07-25 19:19:46 +03:00
let themeData, themeSource
2024-07-17 17:19:57 +03:00
2024-07-25 19:19:46 +03:00
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')) {
// We got passed a source/snapshot
themeData = input
themeSource = input
}
2024-07-17 17:19:57 +03:00
// New theme presets don't have 'theme' property, they use 'source'
let out // shout, shout let it all out
2024-07-25 19:19:46 +03:00
if (themeSource && themeSource.themeEngineVersion === CURRENT_VERSION) {
// There are some themes in wild that have completely broken source
out = { ...(themeData || {}), ...themeSource }
2024-07-17 17:19:57 +03:00
} else {
2024-07-25 19:19:46 +03:00
out = themeData
2024-07-17 17:19:57 +03:00
}
// generatePreset here basically creates/updates "snapshot",
// while also fixing the 2.2 -> 2.3 colors/shadows/etc
return generatePreset(out).theme
}