massively improved initial theme loading code, added checks and warnings when

loading theme files (import/localStorage/defaults)
This commit is contained in:
Henry Jameson 2020-01-22 00:37:19 +02:00
parent 93dfb4d352
commit 9336140486
8 changed files with 259 additions and 72 deletions

View File

@ -5,6 +5,8 @@ import App from '../App.vue'
import { windowWidth } from '../services/window_utils/window_utils' import { windowWidth } from '../services/window_utils/window_utils'
import { getOrCreateApp, getClientToken } from '../services/new_api/oauth.js' import { getOrCreateApp, getClientToken } from '../services/new_api/oauth.js'
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js' import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
import { CURRENT_VERSION } from '../services/theme_data/theme_data.service.js'
import { applyTheme } from '../services/style_setter/style_setter.js'
const getStatusnetConfig = async ({ store }) => { const getStatusnetConfig = async ({ store }) => {
try { try {
@ -261,7 +263,7 @@ const checkOAuthToken = async ({ store }) => {
try { try {
await store.dispatch('loginUser', store.getters.getUserToken()) await store.dispatch('loginUser', store.getters.getUserToken())
} catch (e) { } catch (e) {
console.log(e) console.error(e)
} }
} }
resolve() resolve()
@ -269,23 +271,29 @@ const checkOAuthToken = async ({ store }) => {
} }
const afterStoreSetup = async ({ store, i18n }) => { const afterStoreSetup = async ({ store, i18n }) => {
if (store.state.config.customTheme) {
// This is a hack to deal with async loading of config.json and themes
// See: style_setter.js, setPreset()
window.themeLoaded = true
store.dispatch('setOption', {
name: 'customTheme',
value: store.state.config.customTheme
})
}
const width = windowWidth() const width = windowWidth()
store.dispatch('setMobileLayout', width <= 800) store.dispatch('setMobileLayout', width <= 800)
await setConfig({ store })
const { customTheme, customThemeSource } = store.state.config
const { theme } = store.state.instance
const customThemePresent = customThemeSource || customTheme
if (customThemePresent) {
if (customThemeSource && customThemeSource.version === CURRENT_VERSION) {
applyTheme(customThemeSource)
} else {
applyTheme(customTheme)
}
} else if (theme) {
// do nothing, it will load asynchronously
} else {
console.error('Failed to load any theme!')
}
// Now we can try getting the server settings and logging in // Now we can try getting the server settings and logging in
await Promise.all([ await Promise.all([
checkOAuthToken({ store }), checkOAuthToken({ store }),
setConfig({ store }),
getTOS({ store }), getTOS({ store }),
getInstancePanel({ store }), getInstancePanel({ store }),
getStickers({ store }), getStickers({ store }),

View File

@ -57,6 +57,8 @@ export default {
return { return {
availableStyles: [], availableStyles: [],
selected: this.$store.getters.mergedConfig.theme, selected: this.$store.getters.mergedConfig.theme,
themeWarning: undefined,
tempImportFile: undefined,
previewShadows: {}, previewShadows: {},
previewColors: {}, previewColors: {},
@ -120,12 +122,62 @@ export default {
}) })
}, },
mounted () { mounted () {
this.normalizeLocalState(this.$store.getters.mergedConfig.customTheme) this.loadThemeFromLocalStorage()
if (typeof this.shadowSelected === 'undefined') { if (typeof this.shadowSelected === 'undefined') {
this.shadowSelected = this.shadowsAvailable[0] this.shadowSelected = this.shadowsAvailable[0]
} }
}, },
computed: { computed: {
themeWarningHelp () {
if (!this.themeWarning) return
const t = this.$t
const pre = 'settings.style.switcher.help.'
const {
origin,
themeEngineVersion,
type,
noActionsPossible
} = this.themeWarning
if (origin === 'file') {
// Loaded v2 theme from file
if (themeEngineVersion === 2 && type === 'wrong_version') {
return t(pre + 'v2_imported')
}
if (themeEngineVersion > CURRENT_VERSION) {
return t(pre + 'future_version_imported') + ' ' +
(
noActionsPossible
? t(pre + 'snapshot_missing')
: t(pre + 'snapshot_present')
)
}
if (themeEngineVersion < CURRENT_VERSION) {
return t(pre + 'future_version_imported') + ' ' +
(
noActionsPossible
? t(pre + 'snapshot_missing')
: t(pre + 'snapshot_present')
)
}
} else if (origin === 'localStorage') {
// FE upgraded from v2
if (themeEngineVersion === 2) {
return 'upgraded_from_v2'
}
// Admin downgraded FE
if (themeEngineVersion > CURRENT_VERSION) {
return noActionsPossible
? 'downgraded_theme'
: 'downgraded_theme_missing_snapshot'
}
// Admin upgraded FE
if (themeEngineVersion < CURRENT_VERSION) {
return noActionsPossible
? 'upgraded_theme'
: 'upgraded_theme_missing_snapshot'
}
}
},
selectedVersion () { selectedVersion () {
return Array.isArray(this.selected) ? 1 : 2 return Array.isArray(this.selected) ? 1 : 2
}, },
@ -308,10 +360,96 @@ export default {
Checkbox Checkbox
}, },
methods: { methods: {
loadTheme (
{
theme,
source,
_pleroma_theme_version: fileVersion
},
origin,
forceUseSource = false
) {
if (!source && !theme) {
throw new Error('Can\'t load theme: empty')
}
const version = (origin === 'localstorage' && !theme.colors)
? 'l1'
: fileVersion
const themeEngineVersion = (source || {}).themeEngineVersion || 2
const versionsMatch = themeEngineVersion === CURRENT_VERSION
// Force loading of source if user requested it or if snapshot
// is unavailable
const forcedSourceLoad = (source && forceUseSource) || !theme
if (!versionsMatch &&
!forcedSourceLoad &&
version !== 'l1' &&
origin !== 'defaults'
) {
if (!theme) {
this.themeWarning = {
origin,
noActionsPossible: true,
themeEngineVersion,
type: 'no_snapshot_old_version'
}
} else if (!versionsMatch) {
this.themeWarning = {
origin,
noActionsPossible: !source,
themeEngineVersion,
type: 'wrong_version'
}
}
}
this.normalizeLocalState(theme, version, source, forcedSourceLoad)
},
forceLoadLocalStorage () {
this.loadThemeFromLocalStorage(true)
},
dismissWarning () {
this.themeWarning = undefined
this.tempImportFile = undefined
},
forceLoad () {
const { origin } = this.themeWarning
switch (origin) {
case 'localstorage':
this.loadThemeFromLocalStorage(true)
break
case 'file':
this.onImport(this.tempImportFile, true)
break
}
},
loadThemeFromLocalStorage (confirmLoadSource = false) {
const {
customTheme: theme,
customThemeSource: source
} = this.$store.getters.mergedConfig
if (!theme && !source) {
// Anon user or never touched themes
this.loadTheme(
this.$store.state.instance.themeData,
'defaults',
confirmLoadSource
)
} else {
this.loadTheme(
{ theme, source },
'localStorage',
confirmLoadSource
)
}
},
setCustomTheme () { setCustomTheme () {
this.$store.dispatch('setOption', { this.$store.dispatch('setOption', {
name: 'customTheme', name: 'customTheme',
value: this.previewTheme
})
this.$store.dispatch('setOption', {
name: 'customThemeSource',
value: { value: {
themeEngineVersion: CURRENT_VERSION,
shadows: this.shadowsLocal, shadows: this.shadowsLocal,
fonts: this.fontsLocal, fonts: this.fontsLocal,
opacity: this.currentOpacity, opacity: this.currentOpacity,
@ -331,21 +469,16 @@ export default {
this.previewColors.mod this.previewColors.mod
) )
}, },
onImport (parsed) { onImport (parsed, forceSource = false) {
if (parsed._pleroma_theme_version === 1) { this.tempImportFile = parsed
this.normalizeLocalState(parsed, 1) this.loadTheme(parsed, 'file', forceSource)
} else if (parsed._pleroma_theme_version >= 2) {
this.normalizeLocalState(parsed.theme, 2, parsed.source)
}
}, },
importValidator (parsed) { importValidator (parsed) {
const version = parsed._pleroma_theme_version const version = parsed._pleroma_theme_version
return version >= 1 || version <= 2 return version >= 1 || version <= 2
}, },
clearAll () { clearAll () {
const state = this.$store.getters.mergedConfig.customTheme this.loadThemeFromLocalStorage()
const version = state.colors ? 2 : 'l1'
this.normalizeLocalState(this.$store.getters.mergedConfig.customTheme, version, this.$store.getters.mergedConfig.customThemeSource)
}, },
// Clears all the extra stuff when loading V1 theme // Clears all the extra stuff when loading V1 theme

View File

@ -1,5 +1,9 @@
@import '../../_variables.scss'; @import '../../_variables.scss';
.style-switcher { .style-switcher {
.theme-warning {
display: flex;
align-items: baseline;
}
.preset-switcher { .preset-switcher {
margin-right: 1em; margin-right: 1em;
} }

View File

@ -1,31 +1,60 @@
<template> <template>
<div class="style-switcher"> <div class="style-switcher">
<div class="presets-container"> <div class="presets-container">
<div class="save-load"> <div class="save-load">
<ExportImport <div class="theme-warning" v-if="themeWarning">
:export-object="exportedTheme" <div class="alert warning">
:export-label="$t(&quot;settings.export_theme&quot;)" {{ themeWarningHelp }}
:import-label="$t(&quot;settings.import_theme&quot;)" </div>
:import-failed-text="$t(&quot;settings.invalid_theme_imported&quot;)" <div class="buttons">
:on-import="onImport" <template v-if="themeWarning.noActionsPossible">
:validator="importValidator" <button
> class="btn"
<template slot="before"> @click="dismissWarning"
<div class="presets"> >
{{ $t('settings.presets') }} {{ $t('general.dismiss') }}
<label </button>
for="preset-switcher" </template>
class="select" <template v-else>
<button
class="btn"
@click="forceLoad"
>
{{ $t('settings.style.switcher.load_theme') }}
</button>
<button
class="btn"
@click="dismissWarning"
>
{{ $t('settings.style.switcher.use_snapshot') }}
</button>
</template>
</div>
</div>
<ExportImport
:export-object="exportedTheme"
:export-label="$t(&quot;settings.export_theme&quot;)"
:import-label="$t(&quot;settings.import_theme&quot;)"
:import-failed-text="$t(&quot;settings.invalid_theme_imported&quot;)"
:on-import="onImport"
:validator="importValidator"
>
<template slot="before">
<div class="presets">
{{ $t('settings.presets') }}
<label
for="preset-switcher"
class="select"
> >
<select <select
id="preset-switcher" id="preset-switcher"
v-model="selected" v-model="selected"
class="preset-switcher" class="preset-switcher"
> >
<option <option
v-for="style in availableStyles" v-for="style in availableStyles"
:key="style.name" :key="style.name"
:value="style" :value="style"
:style="{ :style="{
backgroundColor: style[1] || (style.theme || style.source).colors.bg, backgroundColor: style[1] || (style.theme || style.source).colors.bg,
color: style[3] || (style.theme || style.source).colors.text color: style[3] || (style.theme || style.source).colors.text

View File

@ -46,6 +46,7 @@
"optional": "optional", "optional": "optional",
"show_more": "Show more", "show_more": "Show more",
"show_less": "Show less", "show_less": "Show less",
"dismiss": "Dismiss",
"cancel": "Cancel", "cancel": "Cancel",
"disable": "Disable", "disable": "Disable",
"enable": "Enable", "enable": "Enable",
@ -394,7 +395,16 @@
"save_load_hint": "\"Keep\" options preserve currently set options when selecting or loading themes, it also stores said options when exporting a theme. When all checkboxes unset, exporting theme will save everything.", "save_load_hint": "\"Keep\" options preserve currently set options when selecting or loading themes, it also stores said options when exporting a theme. When all checkboxes unset, exporting theme will save everything.",
"reset": "Reset", "reset": "Reset",
"clear_all": "Clear all", "clear_all": "Clear all",
"clear_opacity": "Clear opacity" "clear_opacity": "Clear opacity",
"load_theme": "Load theme",
"use_snapshot": "Keep as is",
"help": {
"v2_imported": "File you imported was made for older FE. We try to maximize compatibility but there still could be inconsitencies.",
"snapshot_present": "Theme snapshot is loaded, so all values are overriden. You can load theme's actual data instead.",
"snapshot_missing": "No theme snapshot was in the file so it could look different than originally envisioned.",
"future_version_imported": "File you imported was made in newer version of FE.",
"older_version_imported": "File you imported was made in older version of FE."
}
}, },
"common": { "common": {
"color": "Color", "color": "Color",

View File

@ -5,6 +5,9 @@ const browserLocale = (window.navigator.language || 'en').split('-')[0]
export const defaultState = { export const defaultState = {
colors: {}, colors: {},
theme: undefined,
customTheme: undefined,
customThemeSource: undefined,
hideISP: false, hideISP: false,
// bad name: actually hides posts of muted USERS // bad name: actually hides posts of muted USERS
hideMutedPosts: undefined, // instance default hideMutedPosts: undefined, // instance default
@ -93,10 +96,10 @@ const config = {
commit('setOption', { name, value }) commit('setOption', { name, value })
switch (name) { switch (name) {
case 'theme': case 'theme':
setPreset(value, commit) setPreset(value)
break break
case 'customTheme': case 'customTheme':
applyTheme(value, commit) applyTheme(value)
} }
} }
} }

View File

@ -1,5 +1,5 @@
import { set } from 'vue' import { set } from 'vue'
import { setPreset } from '../services/style_setter/style_setter.js' import { getPreset, applyTheme } from '../services/style_setter/style_setter.js'
import { instanceDefaultProperties } from './config.js' import { instanceDefaultProperties } from './config.js'
const defaultState = { const defaultState = {
@ -10,6 +10,7 @@ const defaultState = {
textlimit: 5000, textlimit: 5000,
server: 'http://localhost:4040/', server: 'http://localhost:4040/',
theme: 'pleroma-dark', theme: 'pleroma-dark',
themeData: undefined,
background: '/static/aurora_borealis.jpg', background: '/static/aurora_borealis.jpg',
logo: '/static/logo.png', logo: '/static/logo.png',
logoMask: true, logoMask: true,
@ -96,6 +97,9 @@ const instance = {
dispatch('initializeSocket') dispatch('initializeSocket')
} }
break break
case 'theme':
dispatch('setTheme', value)
break
} }
}, },
async getStaticEmoji ({ commit }) { async getStaticEmoji ({ commit }) {
@ -147,9 +151,16 @@ const instance = {
} }
}, },
setTheme ({ commit }, themeName) { setTheme ({ commit, rootState }, themeName) {
commit('setInstanceOption', { name: 'theme', value: themeName }) commit('setInstanceOption', { name: 'theme', value: themeName })
return setPreset(themeName, commit) 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
if (customTheme) return
applyTheme(themeData.theme)
})
}, },
fetchEmoji ({ dispatch, state }) { fetchEmoji ({ dispatch, state }) {
if (!state.customEmojiFetched) { if (!state.customEmojiFetched) {

View File

@ -7,7 +7,7 @@ import { getColors, computeDynamicColor } from '../theme_data/theme_data.service
// styles that aren't just colors, so user can pick from a few different distinct // styles that aren't just colors, so user can pick from a few different distinct
// styles as well as set their own colors in the future. // styles as well as set their own colors in the future.
export const setStyle = (href, commit) => { export const setStyle = (href) => {
/*** /***
What's going on here? What's going on here?
I want to make it easy for admins to style this application. To have I want to make it easy for admins to style this application. To have
@ -53,8 +53,8 @@ export const setStyle = (href, commit) => {
cssEl.addEventListener('load', setDynamic) cssEl.addEventListener('load', setDynamic)
} }
export const applyTheme = (input, commit) => { export const applyTheme = (input) => {
const { rules, theme } = generatePreset(input) const { rules } = generatePreset(input)
const head = document.head const head = document.head
const body = document.body const body = document.body
body.classList.add('hidden') body.classList.add('hidden')
@ -69,11 +69,6 @@ export const applyTheme = (input, commit) => {
styleSheet.insertRule(`body { ${rules.shadows} }`, 'index-max') styleSheet.insertRule(`body { ${rules.shadows} }`, 'index-max')
styleSheet.insertRule(`body { ${rules.fonts} }`, 'index-max') styleSheet.insertRule(`body { ${rules.fonts} }`, 'index-max')
body.classList.remove('hidden') body.classList.remove('hidden')
// commit('setOption', { name: 'colors', value: htmlColors })
// commit('setOption', { name: 'radii', value: radii })
commit('setOption', { name: 'customTheme', value: input })
commit('setOption', { name: 'colors', value: theme.colors })
} }
export const getCssShadow = (input, usesDropShadow) => { export const getCssShadow = (input, usesDropShadow) => {
@ -387,7 +382,7 @@ export const getThemes = () => {
*/ */
export const shadows2to3 = (shadows) => { export const shadows2to3 = (shadows) => {
return Object.entries(shadows).reduce((shadowsAcc, [slotName, shadowDefs]) => { return Object.entries(shadows).reduce((shadowsAcc, [slotName, shadowDefs]) => {
const isDynamic = ({ color }) => console.log(color) || color.startsWith('--') const isDynamic = ({ color }) => color.startsWith('--')
const newShadow = shadowDefs.reduce((shadowAcc, def) => [ const newShadow = shadowDefs.reduce((shadowAcc, def) => [
...shadowAcc, ...shadowAcc,
{ {
@ -399,7 +394,7 @@ export const shadows2to3 = (shadows) => {
}, {}) }, {})
} }
export const setPreset = (val, commit) => { export const getPreset = (val) => {
return getThemes() return getThemes()
.then((themes) => themes[val] ? themes[val] : themes['pleroma-dark']) .then((themes) => themes[val] ? themes[val] : themes['pleroma-dark'])
.then((theme) => { .then((theme) => {
@ -420,14 +415,8 @@ export const setPreset = (val, commit) => {
data.colors = { bg, fg, text, link, cRed, cBlue, cGreen, cOrange } data.colors = { bg, fg, text, link, cRed, cBlue, cGreen, cOrange }
} }
// This is a hack, this function is only called during initial load. return { theme: data, source: theme.source }
// We want to cancel loading the theme from config.json if we're already
// loading a theme from the persisted state.
// Needed some way of dealing with the async way of things.
// load config -> set preset -> wait for styles.json to load ->
// load persisted state -> set colors -> styles.json loaded -> set colors
if (!window.themeLoaded) {
applyTheme(data, commit)
}
}) })
} }
export const setPreset = (val) => getPreset(val).then(data => applyTheme(data.theme))