Compare commits

...

13 commits

Author SHA1 Message Date
Henry Jameson
a4ab8e065a Merge branch 'appearance-tab' into shigusegubu-themes3 2024-07-17 22:42:50 +03:00
Henry Jameson
9d5514de9c fix themes v2 editor for anon users and people who never touched themes 2024-07-17 22:25:14 +03:00
Henry Jameson
d2683a6728 new theme selector, RC 2024-07-17 22:10:11 +03:00
Henry Jameson
9bbdad1a6f theme selector new 2024-07-17 19:58:04 +03:00
Henry Jameson
1866dcfdc2 fix checkbox in preview 2024-07-17 17:26:03 +03:00
Henry Jameson
40c9163d21 optimizations, WIP theme selector 2024-07-17 17:19:57 +03:00
Henry Jameson
9d76fcc425 fix admin combo-dropdowns styles 2024-07-16 21:55:57 +03:00
Henry Jameson
a378c999b7 add ability to override underlay color/opacity regardless of theme 2024-07-16 21:01:20 +03:00
Henry Jameson
e029732021 use separate action for setting Theme V2 2024-07-12 02:40:57 +03:00
Henry Jameson
cd92eb56e0 don't recompile if cache exists 2024-07-12 02:19:38 +03:00
Henry Jameson
82ca01ef71 cleanup 2024-07-12 02:14:31 +03:00
Henry Jameson
115335e98a move theme application to interface module 2024-07-12 02:13:08 +03:00
Henry Jameson
41f8c3dad5 fix gaps in sticky headers 2024-07-11 14:54:37 +03:00
17 changed files with 614 additions and 279 deletions

View file

@ -211,7 +211,7 @@ nav {
.app-layout {
--miniColumn: 25rem;
--maxiColumn: 45rem;
--columnGap: 1em;
--columnGap: 1rem;
--effectiveSidebarColumnWidth: minmax(var(--miniColumn), var(--sidebarColumnWidth, var(--miniColumn)));
--effectiveNotifsColumnWidth: minmax(var(--miniColumn), var(--notifsColumnWidth, var(--miniColumn)));
--effectiveContentColumnWidth: minmax(var(--miniColumn), var(--contentColumnWidth, var(--maxiColumn)));
@ -671,7 +671,8 @@ option {
display: inline-flex;
vertical-align: middle;
> * {
> *,
> * .button-default {
--_roundness-left: 0;
--_roundness-right: 0;
@ -679,11 +680,13 @@ option {
flex: 1 1 auto;
}
> *:first-child {
> *:first-child,
> *:first-child .button-default {
--_roundness-left: var(--roundness);
}
> *:last-child {
> *:last-child,
> *:last-child .button-default {
--_roundness-right: var(--roundness);
}
}

View file

@ -20,6 +20,16 @@ export default {
'Tab',
'ListItem'
],
validInnerComponentsLite: [
'Text',
'Link',
'Icon',
'Border',
'Button',
'Input',
'PanelHeader',
'Alert'
],
defaultRules: [
{
directives: {

View file

@ -12,6 +12,11 @@ export default {
'Alert',
'Button' // mobile post button
],
validInnerComponentsLite: [
'Underlay',
'Scrollbar',
'ScrollbarElement'
],
defaultRules: [
{
directives: {

View file

@ -6,6 +6,18 @@ import UnitSetting, { defaultHorizontalUnits } from '../helpers/unit_setting.vue
import FontControl from 'src/components/font_control/font_control.vue'
import { normalizeThemeData } from 'src/modules/interface'
import {
getThemes
} from 'src/services/style_setter/style_setter.js'
import { convertTheme2To3 } from 'src/services/theme_data/theme2_to_theme3.js'
import { init } from 'src/services/theme_data/theme_data_3.service.js'
import {
getCssRules,
getScopedVersion
} from 'src/services/theme_data/css_utils.js'
import SharedComputedObject from '../helpers/shared_computed_object.js'
import ProfileSettingIndicator from '../helpers/profile_setting_indicator.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
@ -13,6 +25,8 @@ import {
faGlobe
} from '@fortawesome/free-solid-svg-icons'
import Preview from './theme_tab/preview.vue'
library.add(
faGlobe
)
@ -20,6 +34,8 @@ library.add(
const AppearanceTab = {
data () {
return {
availableStyles: [],
intersectionObserver: null,
thirdColumnModeOptions: ['none', 'notifications', 'postform'].map(mode => ({
key: mode,
value: mode,
@ -28,12 +44,12 @@ const AppearanceTab = {
forcedRoundnessOptions: ['disabled', 'sharp', 'nonsharp', 'round'].map((mode, i) => ({
key: mode,
value: i - 1,
label: this.$t(`settings.forced_roundness_mode_${mode}`)
label: this.$t(`settings.style.themes3.hacks.forced_roundness_mode_${mode}`)
})),
underlayOverrideModes: ['none', 'opaque', 'transparent'].map((mode, i) => ({
key: mode,
value: mode,
label: this.$t(`settings.underlay_override_mode_${mode}`)
label: this.$t(`settings.style.themes3.hacks.underlay_override_mode_${mode}`)
}))
}
},
@ -44,9 +60,61 @@ const AppearanceTab = {
FloatSetting,
UnitSetting,
ProfileSettingIndicator,
FontControl
FontControl,
Preview
},
mounted () {
getThemes()
.then((promises) => {
return Promise.all(
Object.entries(promises)
.map(([k, v]) => v.then(res => [k, res]))
)
})
.then(themes => themes.reduce((acc, [k, v]) => {
if (v) {
return [
...acc,
{
name: v.name || v[0],
key: k,
data: v
}
]
} else {
return acc
}
}, []))
.then((themesComplete) => {
this.availableStyles = themesComplete
})
if (window.IntersectionObserver) {
this.intersectionObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(({ target, isIntersecting }) => {
if (!isIntersecting) return
const theme = this.availableStyles.find(x => x.key === target.dataset.themeKey)
this.$nextTick(() => {
if (theme) theme.ready = true
})
observer.unobserve(target)
})
}, {
root: this.$refs.themeList
})
}
},
updated () {
this.$nextTick(() => {
this.$refs.themeList.querySelectorAll('.theme-preview').forEach(node => {
this.intersectionObserver.observe(node)
})
})
},
computed: {
noIntersectionObserver () {
return !window.IntersectionObserver
},
horizontalUnits () {
return defaultHorizontalUnits
},
@ -76,7 +144,39 @@ const AppearanceTab = {
this.$store.dispatch('setOption', { name: 'interfaceLanguage', value: val })
}
},
isCustomThemeUsed () {
const { theme } = this.mergedConfig
return theme === 'custom' || theme === null
},
...SharedComputedObject()
},
methods: {
isThemeActive (key) {
const { theme } = this.mergedConfig
console.log(key, theme)
return key === theme
},
setTheme (name) {
this.$store.dispatch('setTheme', { themeName: name, saveData: true, recompile: true })
},
previewTheme (key, input) {
const style = normalizeThemeData(input)
const x = 2
if (x === 1) return
const theme2 = convertTheme2To3(style)
const theme3 = init({
inputRuleset: theme2,
ultimateBackgroundColor: '#000000',
liteMode: true,
debug: true,
onlyNormalState: true
})
return getScopedVersion(
getCssRules(theme3.eager),
'#theme-preview-' + key
).join('\n')
}
}
}

View file

@ -1,5 +1,39 @@
<template>
<div :label="$t('settings.general')">
<div class="setting-item">
<h2>{{ $t('settings.theme') }}</h2>
<ul
class="theme-list"
ref="themeList"
>
<button
v-if="isCustomThemeUsed"
disabled
class="button-default theme-preview"
>
<preview />
<h4 class="theme-name">{{ $t('settings.style.custom_theme_used') }}</h4>
</button>
<button
v-for="style in availableStyles"
:data-theme-key="style.key"
:key="style.key"
class="button-default theme-preview"
:class="{ toggled: isThemeActive(style.key) }"
@click="setTheme(style.key)"
>
<!-- eslint-disable vue/no-v-text-v-html-on-component -->
<component
:is="'style'"
v-if="style.ready || noIntersectionObserver"
v-html="previewTheme(style.key, style.data)"
/>
<!-- eslint-enable vue/no-v-text-v-html-on-component -->
<preview :class="{ placeholder: ready }" :id="'theme-preview-' + style.key"/>
<h4 class="theme-name">{{ style.name }}</h4>
</button>
</ul>
</div>
<div class="setting-item">
<h2>{{ $t('settings.scale_and_layout') }}</h2>
<ul class="setting-list">
@ -179,7 +213,7 @@
path="forcedRoundness"
:options="forcedRoundnessOptions"
>
{{ $t('settings.force_interface_roundness') }}
{{ $t('settings.style.themes3.hacks.force_interface_roundness') }}
</ChoiceSetting>
</li>
<li>
@ -188,7 +222,7 @@
path="theme3hacks.underlay"
:options="underlayOverrideModes"
>
{{ $t('settings.underlay_overrides') }}
{{ $t('settings.style.themes3.hacks.underlay_overrides') }}
</ChoiceSetting>
</li>
<li v-if="instanceWallpaperUsed">
@ -231,4 +265,38 @@
margin-bottom: 0.5em;
margin-top: 0.5em;
}
.theme-list {
list-style: none;
display: flex;
flex-wrap: wrap;
margin: -0.5em 0;
height: 25em;
overflow-x: hidden;
overflow-y: auto;
scrollbar-gutter: stable;
border-radius: var(--roundness);
border: 1px solid var(--border);
padding: 0;
.theme-preview {
width: 19rem;
display: flex;
flex-direction: column;
align-items: center;
margin: 0.5em;
&.placeholder {
opacity: 0.2;
}
.preview-container {
pointer-events: none;
zoom: 0.5;
border: none;
border-radius: var(--roundness);
text-align: left;
}
}
}
</style>

View file

@ -99,15 +99,9 @@
>
<div class="actions">
<span class="checkbox">
<input
id="preview_checkbox"
checked="very yes"
type="checkbox"
class="input"
>
<label for="preview_checkbox">{{ $t('settings.style.preview.checkbox') }}</label>
</span>
<Checkbox>
{{ $t('settings.style.preview.checkbox') }}
</Checkbox>
<button class="btn button-default">
{{ $t('settings.style.preview.button') }}
</button>
@ -118,6 +112,7 @@
</template>
<script>
import Checkbox from 'src/components/checkbox/checkbox.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faTimes,
@ -133,12 +128,116 @@ library.add(
faReply
)
export default {}
export default {
components: {
Checkbox
}
}
</script>
<style lang="scss">
.preview-container {
position: relative;
border-top: 1px dashed;
border-bottom: 1px dashed;
border-color: var(--border);
margin: 1em 0;
padding: 1em;
background-color: var(--wallpaper);
background-image: var(--body-background-image);
background-size: cover;
background-position: 50% 50%;
.theme-preview-content {
padding: 20px;
}
.dummy {
.post {
font-family: var(--postFont);
display: flex;
.content {
flex: 1;
h4 {
margin-bottom: 0.25em;
}
.icons {
margin-top: 0.5em;
display: flex;
i {
margin-right: 1em;
}
}
}
}
.after-post {
margin-top: 1em;
display: flex;
align-items: center;
}
.avatar,
.avatar-alt {
background:
linear-gradient(
135deg,
#b8e1fc 0%,
#a9d2f3 10%,
#90bae4 25%,
#90bcea 37%,
#90bff0 50%,
#6ba8e5 51%,
#a2daf5 83%,
#bdf3fd 100%
);
color: black;
font-family: sans-serif;
text-align: center;
margin-right: 1em;
}
.avatar-alt {
flex: 0 auto;
margin-left: 28px;
font-size: 12px;
min-width: 20px;
min-height: 20px;
line-height: 20px;
}
.avatar {
flex: 0 auto;
width: 48px;
height: 48px;
font-size: 14px;
line-height: 48px;
}
.actions {
display: flex;
align-items: baseline;
.checkbox {
margin-right: 1em;
flex: 1;
}
}
.separator {
margin: 1em;
border-bottom: 1px solid;
border-color: var(--border);
}
.btn {
min-width: 3em;
}
}
}
.underlay-preview {
@ -148,4 +247,4 @@ export default {}
left: 10px;
right: 10px;
}
</style>
</style>

View file

@ -30,7 +30,10 @@ import {
import { convertTheme2To3 } from 'src/services/theme_data/theme2_to_theme3.js'
import { init } from 'src/services/theme_data/theme_data_3.service.js'
import { getCssRules } from 'src/services/theme_data/css_utils.js'
import {
getCssRules,
getScopedVersion
} from 'src/services/theme_data/css_utils.js'
import ColorInput from 'src/components/color_input/color_input.vue'
import RangeInput from 'src/components/range_input/range_input.vue'
@ -499,18 +502,14 @@ export default {
}
},
setCustomTheme () {
this.$store.dispatch('setOption', {
name: 'customTheme',
value: {
this.$store.dispatch('setThemeV2', {
customTheme: {
ignore: true,
themeFileVersion: this.selectedVersion,
themeEngineVersion: CURRENT_VERSION,
...this.previewTheme
}
})
this.$store.dispatch('setOption', {
name: 'customThemeSource',
value: {
},
customThemeSource: {
themeFileVersion: this.selectedVersion,
themeEngineVersion: CURRENT_VERSION,
shadows: this.shadowsLocal,
@ -607,7 +606,7 @@ export default {
normalizeLocalState (theme, version = 0, source, forceSource = false) {
let input
if (typeof source !== 'undefined') {
if (forceSource || source.themeEngineVersion === CURRENT_VERSION) {
if (forceSource || source?.themeEngineVersion === CURRENT_VERSION) {
input = source
version = source.themeEngineVersion
} else {
@ -707,17 +706,10 @@ export default {
liteMode: true
})
this.themeV3Preview = getCssRules(theme3.eager)
.map(x => {
if (x.startsWith('html')) {
return x.replace('html', '#theme-preview')
} else if (x.startsWith('#content')) {
return x.replace('#content', '#theme-preview')
} else {
return '#theme-preview > ' + x
}
})
.join('\n')
this.themeV3Preview = getScopedVersion(
getCssRules(theme3.eager),
'#theme-preview'
).join('\n')
}
},
watch: {

View file

@ -161,107 +161,6 @@
}
}
.preview-container {
border-top: 1px dashed;
border-bottom: 1px dashed;
border-color: var(--border);
margin: 1em 0;
padding: 1em;
background-color: var(--wallpaper);
background-image: var(--body-background-image);
background-size: cover;
background-position: 50% 50%;
.dummy {
.post {
font-family: var(--postFont);
display: flex;
.content {
flex: 1;
h4 {
margin-bottom: 0.25em;
}
.icons {
margin-top: 0.5em;
display: flex;
i {
margin-right: 1em;
}
}
}
}
.after-post {
margin-top: 1em;
display: flex;
align-items: center;
}
.avatar,
.avatar-alt {
background:
linear-gradient(
135deg,
#b8e1fc 0%,
#a9d2f3 10%,
#90bae4 25%,
#90bcea 37%,
#90bff0 50%,
#6ba8e5 51%,
#a2daf5 83%,
#bdf3fd 100%
);
color: black;
font-family: sans-serif;
text-align: center;
margin-right: 1em;
}
.avatar-alt {
flex: 0 auto;
margin-left: 28px;
font-size: 12px;
min-width: 20px;
min-height: 20px;
line-height: 20px;
}
.avatar {
flex: 0 auto;
width: 48px;
height: 48px;
font-size: 14px;
line-height: 48px;
}
.actions {
display: flex;
align-items: baseline;
.checkbox {
display: inline-flex;
align-items: baseline;
margin-right: 1em;
flex: 1;
}
}
.separator {
margin: 1em;
border-bottom: 1px solid;
border-color: var(--border);
}
.btn {
min-width: 3em;
}
}
}
.radius-item {
flex-basis: auto;
}
@ -314,10 +213,6 @@
max-width: 50em;
}
.theme-preview-content {
padding: 20px;
}
.theme-warning {
display: flex;
align-items: baseline;

View file

@ -17,6 +17,15 @@ export default {
'Attachment',
'PollGraph'
],
validInnerComponentsLite: [
'Text',
'Link',
'Icon',
'Border',
'ButtonUnstyled',
'RichContent',
'Avatar'
],
defaultRules: [
{
directives: {

View file

@ -386,11 +386,6 @@
"navbar_size": "Top bar size",
"panel_header_size": "Panel header size",
"visual_tweaks": "Minor visual tweaks",
"force_interface_roundness": "Override interface roundness/sharpness",
"forced_roundness_mode_disabled": "Use theme defaults",
"forced_roundness_mode_sharp": "Force sharp edges",
"forced_roundness_mode_nonsharp": "Force not-so-sharp (1px roundness) edges",
"forced_roundness_mode_round": "Force round edges",
"theme_debug": "Show what background theme engine assumes when dealing with transparancy (DEBUG)",
"scale_and_layout": "Interface scale and layout",
"mfa": {
@ -745,10 +740,22 @@
"enable_web_push_always_show_tip": "Some browsers (Chromium, Chrome) require that push messages always result in a notification, otherwise generic 'Website was updated in background' is shown, enable this to prevent this notification from showing, as Chrome seem to hide push notifications if tab is in focus. Can result in showing duplicate notifications on other browsers.",
"more_settings": "More settings",
"style": {
"custom_theme_used": "(Custom theme)",
"themes2_outdated": "Editor for Themes V2 is no longer supported and presented here for sake of legacy.",
"update_preview": "Update preview",
"themes3": {
"define": "Override",
"hacks": {
"underlay_overrides": "Change underlay",
"underlay_override_mode_none": "Theme default",
"underlay_override_mode_opaque": "Replace with solid color",
"underlay_override_mode_transparent": "Remove entirely (might break some themes)",
"force_interface_roundness": "Override interface roundness/sharpness",
"forced_roundness_mode_disabled": "Use theme defaults",
"forced_roundness_mode_sharp": "Force sharp edges",
"forced_roundness_mode_nonsharp": "Force not-so-sharp (1px roundness) edges",
"forced_roundness_mode_round": "Force round edges"
},
"font": {
"group-builtin": "Browser default fonts",
"builtin" : {

View file

@ -35,11 +35,26 @@ 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
palette: null, // not used yet, will be used for V3
theme3hacks: { // Hacks, user overrides that are independent of theme used
underlay: 'none',
badgeColor: null
},
hideISP: false,
hideInstanceWallpaper: false,
hideShoutbox: false,
@ -92,11 +107,6 @@ export const defaultState = {
chatMention: true,
polls: true
},
palette: null,
theme3hacks: {
underlay: 'none',
badgeColor: null
},
webPushNotifications: false,
webPushAlwaysShowNotifications: false,
muteWords: [],
@ -164,7 +174,6 @@ export const defaultState = {
maxDepthInThread: undefined, // instance default
autocompleteSelect: undefined, // instance default
closingDrawerMarksAsSeen: undefined, // instance default
themeDebug: false,
unseenAtTop: undefined, // instance default
ignoreInactionableSeen: undefined // instance default
}
@ -255,6 +264,12 @@ const config = {
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'
@ -272,25 +287,22 @@ const config = {
dispatch('disableMastoSockets')
dispatch('setOption', { name: 'useStreamingApi', value: false })
})
break
}
}
} else {
commit('setOption', { name, value })
if (
name.startsWith('theme3hacks') ||
APPEARANCE_SETTINGS_KEYS.has(name)
) {
if (APPEARANCE_SETTINGS_KEYS.has(name)) {
applyConfig(state)
}
if (name.startsWith('theme3hacks')) {
dispatch('setTheme', { recompile: true })
}
switch (name) {
case 'theme':
dispatch('setTheme', { themeName: value, recompile: true })
if (value === 'custom') break
dispatch('setTheme', { themeName: value, recompile: true, saveData: true })
break
case 'customTheme':
case 'customThemeSource': {
if (!value.ignore) dispatch('setTheme', { themeData: value })
break
}
case 'themeDebug': {
dispatch('setTheme', { recompile: true })
break

View file

@ -1,6 +1,3 @@
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'
import apiService from '../services/api/api.service.js'
import { instanceDefaultProperties } from './config.js'
import { langCodeToCldrName, ensureFinalFallback } from '../i18n/languages.js'
@ -45,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
@ -375,88 +372,6 @@ const instance = {
console.warn(e)
}
},
setTheme ({ commit, state, rootState }, { themeName, themeData, recompile } = {}) {
// const {
// themeApplied
// } = rootState.interface
const {
theme: instanceThemeName
} = state
const {
customTheme: userThemeSnapshot,
customThemeSource: userThemeSource,
forceThemeRecompilation,
themeDebug
} = rootState.config
const forceRecompile = forceThemeRecompilation || recompile
// If we're not not forced to recompile try using
// cache (tryLoadCache return true if load successful)
if (!forceRecompile && !themeDebug && tryLoadCache()) {
commit('setThemeApplied')
}
const normalizeThemeData = (themeData) => {
console.log('NORMAL', themeData)
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
}
let promise = null
if (themeName) {
// commit('setInstanceOption', { name: 'theme', value: themeName })
promise = getPreset(themeName)
.then(themeData => {
// commit('setInstanceOption', { name: 'themeData', value: themeData })
return normalizeThemeData(themeData)
})
} else if (themeData) {
promise = Promise.resolve(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 (instanceThemeName) {
promise = getPreset(themeName).then(themeData => normalizeThemeData(themeData))
}
}
promise
.then(realThemeData => {
console.log('FR FR 1', realThemeData)
const ruleset = convertTheme2To3(realThemeData)
console.log('FR FR 2', ruleset)
applyTheme(
ruleset,
() => commit('setThemeApplied'),
themeDebug
)
})
return promise
},
fetchEmoji ({ dispatch, state }) {
if (!state.customEmojiFetched) {
state.customEmojiFetched = true

View file

@ -1,3 +1,7 @@
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,
@ -207,8 +211,141 @@ const interfaceMod = {
},
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 '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
}
console.log('NEW RULE', newRule)
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
}

View file

@ -159,3 +159,15 @@ export const getCssRules = (rules, debug) => rules.map(rule => {
footer
].join('\n')
}).filter(x => x)
export const getScopedVersion = (rules, newScope) => {
return rules.map(x => {
if (x.startsWith('html')) {
return x.replace('html', newScope)
} else if (x.startsWith('#content')) {
return x.replace('#content', newScope)
} else {
return newScope + ' > ' + x
}
})
}

View file

@ -39,7 +39,23 @@ export const getAllPossibleCombinations = (array) => {
return combos.reduce((acc, x) => [...acc, ...x], [])
}
// Converts rule, parents and their criteria into a CSS (or path if ignoreOutOfTreeSelector == true) selector
/**
* Converts rule, parents and their criteria into a CSS (or path if ignoreOutOfTreeSelector == true)
* selector.
*
* "path" here refers to "fake" selector that cannot be actually used in UI but is used for internal
* purposes
*
* @param {Object} components - object containing all components definitions
*
* @returns {Function}
* @param {Object} rule - rule in question to convert to CSS selector
* @param {boolean} ignoreOutOfTreeSelector - wthether to ignore aformentioned field in
* component definition and use selector
* @param {boolean} isParent - (mostly) internal argument used when recursing
*
* @returns {String} CSS selector (or path)
*/
export const genericRuleToSelector = components => (rule, ignoreOutOfTreeSelector, isParent) => {
if (!rule && !isParent) return null
const component = components[rule.component]
@ -79,6 +95,17 @@ export const genericRuleToSelector = components => (rule, ignoreOutOfTreeSelecto
return selectors.trim()
}
/**
* Check if combination matches
*
* @param {Object} criteria - criteria to match against
* @param {Object} subject - rule/combination to check match
* @param {boolean} strict - strict checking:
* By default every variant and state inherits from "normal" state/variant
* so when checking if combination matches, it WILL match against "normal"
* state/variant. In strict mode inheritance is ignored an "normal" does
* not match
*/
export const combinationsMatch = (criteria, subject, strict) => {
if (criteria.component !== subject.component) return false
@ -101,6 +128,15 @@ export const combinationsMatch = (criteria, subject, strict) => {
return true
}
/**
* Search for rule that matches `criteria` in set of rules
* meant to be used in a ruleset.filter() function
*
* @param {Object} criteria - criteria to search for
* @param {boolean} strict - whether search strictly or not (see combinationsMatch)
*
* @return function that returns true/false if subject matches
*/
export const findRules = (criteria, strict) => subject => {
// If we searching for "general" rules - ignore "specific" ones
if (criteria.parent === null && !!subject.parent) return false
@ -125,6 +161,7 @@ export const findRules = (criteria, strict) => subject => {
return true
}
// Pre-fills 'normal' state/variant if missing
export const normalizeCombination = rule => {
rule.variant = rule.variant ?? 'normal'
rule.state = [...new Set(['normal', ...(rule.state || [])])]

View file

@ -12,7 +12,9 @@ export const basePaletteKeys = new Set([
'cBlue',
'cRed',
'cGreen',
'cOrange'
'cOrange',
'wallpaper'
])
export const fontsKeys = new Set([

View file

@ -149,11 +149,30 @@ const ruleToSelector = genericRuleToSelector(components)
export const getEngineChecksum = () => engineChecksum
/**
* Initializes and compiles the theme according to the ruleset
*
* @param {Object[]} inputRuleset - set of rules to compile theme against. Acts as an override to
* component default rulesets
* @param {string} ultimateBackgroundColor - Color that will be the "final" background for
* calculating contrast ratios and making text automatically accessible. Really used for cases when
* stuff is transparent.
* @param {boolean} debug - print out debug information in console, mostly just performance stuff
* @param {boolean} liteMode - use validInnerComponentsLite instead of validInnerComponents, meant to
* generatate theme previews and such that need to be compiled faster and don't require a lot of other
* components present in "normal" mode
* @param {boolean} onlyNormalState - only use components 'normal' states, meant for generating theme
* previews since states are the biggest factor for compilation time and are completely unnecessary
* when previewing multiple themes at same time
* @param {string} rootComponentName - [UNTESTED] which component to start from, meant for previewing a
* part of the theme (i.e. just the button) for themes 3 editor.
*/
export const init = ({
inputRuleset,
ultimateBackgroundColor,
debug = false,
liteMode = false,
onlyNormalState = false,
rootComponentName = 'Root'
}) => {
if (!inputRuleset) throw new Error('Ruleset is null or undefined!')
@ -402,11 +421,16 @@ export const init = ({
const processInnerComponent = (component, parent) => {
const combinations = []
const {
validInnerComponents = [],
states: originalStates = {},
variants: originalVariants = {}
} = component
const validInnerComponents = (
liteMode
? (component.validInnerComponentsLite || component.validInnerComponents)
: component.validInnerComponents
) || []
// Normalizing states and variants to always include "normal"
const states = { normal: '', ...originalStates }
const variants = { normal: '', ...originalVariants }
@ -418,22 +442,26 @@ export const init = ({
// Optimization: we only really need combinations without "normal" because all states implicitly have it
const permutationStateKeys = Object.keys(states).filter(s => s !== 'normal')
const stateCombinations = [
['normal'],
...getAllPossibleCombinations(permutationStateKeys)
.map(combination => ['normal', ...combination])
.filter(combo => {
// Optimization: filter out some hard-coded combinations that don't make sense
if (combo.indexOf('disabled') >= 0) {
return !(
combo.indexOf('hover') >= 0 ||
combo.indexOf('focused') >= 0 ||
combo.indexOf('pressed') >= 0
)
}
return true
})
]
const stateCombinations = onlyNormalState
? [
['normal']
]
: [
['normal'],
...getAllPossibleCombinations(permutationStateKeys)
.map(combination => ['normal', ...combination])
.filter(combo => {
// Optimization: filter out some hard-coded combinations that don't make sense
if (combo.indexOf('disabled') >= 0) {
return !(
combo.indexOf('hover') >= 0 ||
combo.indexOf('focused') >= 0 ||
combo.indexOf('pressed') >= 0
)
}
return true
})
]
const stateVariantCombination = Object.keys(variants).map(variant => {
return stateCombinations.map(state => ({ variant, state }))
@ -460,7 +488,9 @@ export const init = ({
const t0 = performance.now()
const combinations = processInnerComponent(components[rootComponentName] ?? components.Root)
const t1 = performance.now()
console.debug('Tree traveral took ' + (t1 - t0) + ' ms')
if (debug) {
console.debug('Tree traveral took ' + (t1 - t0) + ' ms')
}
const result = combinations.map((combination) => {
if (combination.lazy) {
@ -470,7 +500,9 @@ export const init = ({
}
}).filter(x => x)
const t2 = performance.now()
console.debug('Eager processing took ' + (t2 - t1) + ' ms')
if (debug) {
console.debug('Eager processing took ' + (t2 - t1) + ' ms')
}
return {
lazy: result.filter(x => typeof x === 'function'),