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:
commit
0c9893c8a0
48 changed files with 1757 additions and 707 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue