Merge branch 'appearance-tab' into 'develop'

Themes 3: Intermission: Appearance Tab and fixes

See merge request pleroma/pleroma-fe!1920
This commit is contained in:
HJ 2024-07-24 18:51:17 +00:00
commit 0c9893c8a0
48 changed files with 1757 additions and 707 deletions

View file

@ -1,10 +1,21 @@
import Cookies from 'js-cookie'
import { setPreset, applyTheme, applyConfig } from '../services/style_setter/style_setter.js'
import { applyConfig } from '../services/style_setter/style_setter.js'
import messages from '../i18n/messages'
import { set } from 'lodash'
import localeService from '../services/locale/locale.service.js'
const BACKEND_LANGUAGE_COOKIE_NAME = 'userLanguage'
const APPEARANCE_SETTINGS_KEYS = new Set([
'sidebarColumnWidth',
'contentColumnWidth',
'notifsColumnWidth',
'textSize',
'navbarSize',
'panelHeaderSize',
'forcedRoundness',
'emojiSize',
'emojiReactionsScale'
])
const browserLocale = (window.navigator.language || 'en').split('-')[0]
@ -24,11 +35,30 @@ export const multiChoiceProperties = [
export const defaultState = {
expertLevel: 0, // used to track which settings to show and hide
colors: {},
theme: undefined,
customTheme: undefined,
customThemeSource: undefined,
forceThemeRecompilation: false,
// Theme stuff
theme: undefined, // Very old theme store, stores preset name, still in use
// V1
colors: {}, // VERY old theme store, just colors of V1, probably not even used anymore
// V2
customTheme: undefined, // "snapshot", previously was used as actual theme store for V2 so it's still used in case of PleromaFE downgrade event.
customThemeSource: undefined, // "source", stores original theme data
// V3
themeDebug: false, // debug mode that uses computed backgrounds instead of real ones to debug contrast functions
forceThemeRecompilation: false, // flag that forces recompilation on boot even if cache exists
theme3hacks: { // Hacks, user overrides that are independent of theme used
underlay: 'none',
fonts: {
interface: undefined,
input: undefined,
post: undefined,
monospace: undefined
}
},
hideISP: false,
hideInstanceWallpaper: false,
hideShoutbox: false,
@ -117,7 +147,12 @@ export const defaultState = {
sidebarColumnWidth: '25rem',
contentColumnWidth: '45rem',
notifsColumnWidth: '25rem',
emojiReactionsScale: 1.0,
emojiReactionsScale: undefined,
textSize: undefined, // instance default
emojiSize: undefined, // instance default
navbarSize: undefined, // instance default
panelHeaderSize: undefined, // instance default
forcedRoundness: undefined, // instance default
navbarColumnStretch: false,
greentext: undefined, // instance default
useAtIcon: undefined, // instance default
@ -175,6 +210,10 @@ const config = {
}
},
mutations: {
setOptionTemporarily (state, { name, value }) {
set(state, name, value)
applyConfig(state)
},
setOption (state, { name, value }) {
set(state, name, value)
},
@ -205,6 +244,37 @@ const config = {
setHighlight ({ commit, dispatch }, { user, color, type }) {
commit('setHighlight', { user, color, type })
},
setOptionTemporarily ({ commit, dispatch, state, rootState }, { name, value }) {
if (rootState.interface.temporaryChangesTimeoutId !== null) {
console.warn('Can\'t track more than one temporary change')
return
}
const oldValue = state[name]
commit('setOptionTemporarily', { name, value })
const confirm = () => {
dispatch('setOption', { name, value })
commit('clearTemporaryChanges')
}
const revert = () => {
commit('setOptionTemporarily', { name, value: oldValue })
commit('clearTemporaryChanges')
}
commit('setTemporaryChanges', {
timeoutId: setTimeout(revert, 10000),
confirm,
revert
})
},
setThemeV2 ({ commit, dispatch }, { customTheme, customThemeSource }) {
commit('setOption', { name: 'theme', value: 'custom' })
commit('setOption', { name: 'customTheme', value: customTheme })
commit('setOption', { name: 'customThemeSource', value: customThemeSource })
dispatch('setTheme', { themeData: customThemeSource, recompile: true })
},
setOption ({ commit, dispatch, state }, { name, value }) {
const exceptions = new Set([
'useStreamingApi'
@ -222,24 +292,26 @@ const config = {
dispatch('disableMastoSockets')
dispatch('setOption', { name: 'useStreamingApi', value: false })
})
break
}
}
} else {
commit('setOption', { name, value })
if (APPEARANCE_SETTINGS_KEYS.has(name)) {
applyConfig(state)
}
if (name.startsWith('theme3hacks')) {
dispatch('setTheme', { recompile: true })
}
switch (name) {
case 'theme':
setPreset(value)
if (value === 'custom') break
dispatch('setTheme', { themeName: value, recompile: true, saveData: true })
break
case 'sidebarColumnWidth':
case 'contentColumnWidth':
case 'notifsColumnWidth':
case 'emojiReactionsScale':
applyConfig(state)
break
case 'customTheme':
case 'customThemeSource':
applyTheme(value)
case 'themeDebug': {
dispatch('setTheme', { recompile: true })
break
}
case 'interfaceLanguage':
messages.setLanguage(this.getters.i18n, value)
dispatch('loadUnicodeEmojiData', value)

View file

@ -1,5 +1,3 @@
import { getPreset, applyTheme } from '../services/style_setter/style_setter.js'
import { CURRENT_VERSION } from '../services/theme_data/theme_data.service.js'
import apiService from '../services/api/api.service.js'
import { instanceDefaultProperties } from './config.js'
import { langCodeToCldrName, ensureFinalFallback } from '../i18n/languages.js'
@ -44,7 +42,7 @@ const defaultState = {
registrationOpen: true,
server: 'http://localhost:4040/',
textlimit: 5000,
themeData: undefined,
themeData: undefined, // used for theme editor v2
vapidPublicKey: undefined,
// Stuff from static/config.json
@ -98,6 +96,13 @@ const defaultState = {
sidebarRight: false,
subjectLineBehavior: 'email',
theme: 'pleroma-dark',
emojiReactionsScale: 0.5,
textSize: '14px',
emojiSize: '2.2rem',
navbarSize: '3.5rem',
panelHeaderSize: '3.2rem',
forcedRoundness: -1,
fontsOverride: {},
virtualScrolling: true,
sensitiveByDefault: false,
conversationDisplay: 'linear',
@ -279,9 +284,6 @@ const instance = {
dispatch('initializeSocket')
}
break
case 'theme':
dispatch('setTheme', value)
break
}
},
async getStaticEmoji ({ commit }) {
@ -370,27 +372,6 @@ const instance = {
console.warn(e)
}
},
setTheme ({ commit, rootState }, themeName) {
commit('setInstanceOption', { name: 'theme', value: themeName })
getPreset(themeName)
.then(themeData => {
commit('setInstanceOption', { name: 'themeData', value: themeData })
// No need to apply theme if there's user theme already
const { customTheme } = rootState.config
const { themeApplied } = rootState.interface
if (customTheme || themeApplied) return
// New theme presets don't have 'theme' property, they use 'source'
const themeSource = themeData.source
if (!themeData.theme || (themeSource && themeSource.themeEngineVersion === CURRENT_VERSION)) {
applyTheme(themeSource)
} else {
applyTheme(themeData.theme)
}
commit('setThemeApplied')
})
},
fetchEmoji ({ dispatch, state }) {
if (!state.customEmojiFetched) {
state.customEmojiFetched = true

View file

@ -1,5 +1,13 @@
import { getPreset, 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'
const defaultState = {
localFonts: null,
themeApplied: false,
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,
@ -14,7 +22,8 @@ const defaultState = {
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: [],
@ -36,6 +45,17 @@ const interfaceMod = {
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
},
@ -90,6 +110,10 @@ const interfaceMod = {
},
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: {
@ -164,10 +188,203 @@ const interfaceMod = {
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)
},
setTheme ({ commit, rootState }, { themeName, themeData, recompile, saveData } = {}) {
const {
theme: instanceThemeName
} = rootState.instance
const {
theme: userThemeName,
customTheme: userThemeSnapshot,
customThemeSource: userThemeSource,
forceThemeRecompilation,
themeDebug,
theme3hacks
} = rootState.config
const actualThemeName = userThemeName || instanceThemeName
const forceRecompile = forceThemeRecompilation || recompile
let promise = null
if (themeData) {
promise = Promise.resolve(normalizeThemeData(themeData))
} else if (themeName) {
promise = getPreset(themeName).then(themeData => normalizeThemeData(themeData))
} else if (userThemeSource || userThemeSnapshot) {
if (userThemeSource && userThemeSource.themeEngineVersion === CURRENT_VERSION) {
promise = Promise.resolve(normalizeThemeData(userThemeSource))
} else {
promise = Promise.resolve(normalizeThemeData(userThemeSnapshot))
}
} else if (actualThemeName && actualThemeName !== 'custom') {
promise = getPreset(actualThemeName).then(themeData => {
const realThemeData = normalizeThemeData(themeData)
if (actualThemeName === instanceThemeName) {
// This sole line is the reason why this whole block is above the recompilation check
commit('setInstanceOption', { name: 'themeData', value: { theme: realThemeData } })
}
return realThemeData
})
} else {
throw new Error('Cannot load any theme!')
}
// If we're not not forced to recompile try using
// cache (tryLoadCache return true if load successful)
if (!forceRecompile && !themeDebug && tryLoadCache()) {
commit('setThemeApplied')
return
}
promise
.then(realThemeData => {
const theme2ruleset = convertTheme2To3(realThemeData)
if (saveData) {
commit('setOption', { name: 'theme', value: themeName || actualThemeName })
commit('setOption', { name: 'customTheme', value: realThemeData })
commit('setOption', { name: 'customThemeSource', value: realThemeData })
}
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 ruleset = [
...theme2ruleset,
...hacks
]
applyTheme(
ruleset,
() => commit('setThemeApplied'),
themeDebug
)
})
return promise
}
}
}
export default interfaceMod
export const normalizeThemeData = (input) => {
let themeData = input
if (Array.isArray(themeData)) {
themeData = { colors: {} }
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]
return generatePreset(themeData).theme
}
if (themeData.themeFileVerison === 1) {
return generatePreset(themeData).theme
}
// New theme presets don't have 'theme' property, they use 'source'
const themeSource = themeData.source
let out // shout, shout let it all out
if (!themeData.theme || (themeSource && themeSource.themeEngineVersion === CURRENT_VERSION)) {
out = themeSource || themeData
} else {
out = themeData.theme
}
// generatePreset here basically creates/updates "snapshot",
// while also fixing the 2.2 -> 2.3 colors/shadows/etc
return generatePreset(out).theme
}