Merge branch 'appearance-tab' into 'develop'

Themes 3: Intermission: Appearance Tab and fixes

See merge request pleroma/pleroma-fe!1920
merge-requests/1931/head
HJ 2 months ago
commit 0c9893c8a0

@ -0,0 +1 @@
Reorganized Settings modal to move out visual stuff into Appearance tab

@ -0,0 +1 @@
Ability to change size of emoji

@ -0,0 +1 @@
Bug with firefox and redmond themes

@ -0,0 +1 @@
Theme selector with visual previews of the theme

@ -0,0 +1 @@
Ability to resize UI (and certain components) scale independent of browser/text scale

@ -0,0 +1 @@
Ability to override certain aspects of UI style independent of theme used (UI roundness, fonts, underlay)

@ -3,9 +3,10 @@
@import "./panel"; @import "./panel";
:root { :root {
--font-size: 14px; --fontSize: 14px;
--status-margin: 0.75em; --status-margin: 0.75em;
--navbar-height: 3.5rem; --navbar-height: var(--navbarSize, 3.5rem);
--panel-header-height: var(--panelHeaderSize, 3.2rem);
--post-line-height: 1.4; --post-line-height: 1.4;
// Z-Index stuff // Z-Index stuff
--ZI_media_modal: 9000; --ZI_media_modal: 9000;
@ -20,7 +21,10 @@
} }
html { html {
font-size: var(--font-size); font-size: var(--textSize);
--navbar-height: var(--navbarSize, 3.5rem);
--emoji-size: var(--emojiSize, 32px);
// overflow-x: clip causes my browser's tab to crash with SIGILL lul // overflow-x: clip causes my browser's tab to crash with SIGILL lul
} }
@ -156,6 +160,7 @@ nav {
box-shadow: var(--shadow); box-shadow: var(--shadow);
box-sizing: border-box; box-sizing: border-box;
height: var(--navbar-height); height: var(--navbar-height);
font-size: calc(var(--navbar-height) / 3.5);
position: fixed; position: fixed;
} }
@ -207,7 +212,7 @@ nav {
.app-layout { .app-layout {
--miniColumn: 25rem; --miniColumn: 25rem;
--maxiColumn: 45rem; --maxiColumn: 45rem;
--columnGap: 1em; --columnGap: 1rem;
--effectiveSidebarColumnWidth: minmax(var(--miniColumn), var(--sidebarColumnWidth, var(--miniColumn))); --effectiveSidebarColumnWidth: minmax(var(--miniColumn), var(--sidebarColumnWidth, var(--miniColumn)));
--effectiveNotifsColumnWidth: minmax(var(--miniColumn), var(--notifsColumnWidth, var(--miniColumn))); --effectiveNotifsColumnWidth: minmax(var(--miniColumn), var(--notifsColumnWidth, var(--miniColumn)));
--effectiveContentColumnWidth: minmax(var(--miniColumn), var(--contentColumnWidth, var(--maxiColumn))); --effectiveContentColumnWidth: minmax(var(--miniColumn), var(--contentColumnWidth, var(--maxiColumn)));
@ -371,7 +376,6 @@ nav {
user-select: none; user-select: none;
color: var(--text); color: var(--text);
border: none; border: none;
border-radius: var(--roundness);
cursor: pointer; cursor: pointer;
background-color: var(--background); background-color: var(--background);
box-shadow: var(--shadow); box-shadow: var(--shadow);
@ -507,7 +511,6 @@ textarea {
--_padding: 0.5em; --_padding: 0.5em;
border: none; border: none;
border-radius: var(--roundness);
background-color: var(--background); background-color: var(--background);
color: var(--text); color: var(--text);
box-shadow: var(--shadow); box-shadow: var(--shadow);
@ -613,6 +616,17 @@ textarea {
} }
} }
.input,
.button-default {
--_roundness-left: var(--roundness);
--_roundness-right: var(--roundness);
border-top-left-radius: var(--_roundness-left);
border-bottom-left-radius: var(--_roundness-left);
border-top-right-radius: var(--_roundness-right);
border-bottom-right-radius: var(--_roundness-right);
}
// Textareas should have stock line-height + vertical padding instead of huge line-height // Textareas should have stock line-height + vertical padding instead of huge line-height
textarea.input { textarea.input {
padding: var(--_padding); padding: var(--_padding);
@ -658,22 +672,23 @@ option {
display: inline-flex; display: inline-flex;
vertical-align: middle; vertical-align: middle;
button, > *,
.button-dropdown { > * .button-default {
--_roundness-left: 0;
--_roundness-right: 0;
position: relative; position: relative;
flex: 1 1 auto; flex: 1 1 auto;
}
&:not(:last-child), > *:first-child,
&:not(:last-child) .button-default { > *:first-child .button-default {
border-top-right-radius: 0; --_roundness-left: var(--roundness);
border-bottom-right-radius: 0; }
}
&:not(:first-child), > *:last-child,
&:not(:first-child) .button-default { > *:last-child .button-default {
border-top-left-radius: 0; --_roundness-right: var(--roundness);
border-bottom-left-radius: 0;
}
} }
} }

@ -13,8 +13,7 @@ import VBodyScrollLock from 'src/directives/body_scroll_lock'
import { windowWidth, windowHeight } from '../services/window_utils/window_utils' import { windowWidth, windowHeight } 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 { applyConfig } from '../services/style_setter/style_setter.js'
import { applyTheme, applyConfig, tryLoadCache } from '../services/style_setter/style_setter.js'
import FaviconService from '../services/favicon_service/favicon_service.js' import FaviconService from '../services/favicon_service/favicon_service.js'
import { initServiceWorker, updateFocus } from '../services/sw/sw.js' import { initServiceWorker, updateFocus } from '../services/sw/sw.js'
@ -160,8 +159,6 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => {
copyInstanceOption('showFeaturesPanel') copyInstanceOption('showFeaturesPanel')
copyInstanceOption('hideSitename') copyInstanceOption('hideSitename')
copyInstanceOption('sidebarRight') copyInstanceOption('sidebarRight')
return store.dispatch('setTheme', config.theme)
} }
const getTOS = async ({ store }) => { const getTOS = async ({ store }) => {
@ -352,27 +349,7 @@ const afterStoreSetup = async ({ store, i18n }) => {
store.dispatch('setInstanceOption', { name: 'server', value: server }) store.dispatch('setInstanceOption', { name: 'server', value: server })
await setConfig({ store }) await setConfig({ store })
await store.dispatch('setTheme')
const { customTheme, customThemeSource, forceThemeRecompilation } = store.state.config
const { theme } = store.state.instance
const customThemePresent = customThemeSource || customTheme
if (!forceThemeRecompilation && tryLoadCache()) {
store.commit('setThemeApplied')
} else {
if (customThemePresent) {
if (customThemeSource && customThemeSource.themeEngineVersion === CURRENT_VERSION) {
applyTheme(customThemeSource)
} else {
applyTheme(customTheme)
}
store.commit('setThemeApplied')
} else if (theme) {
// do nothing, it will load asynchronously
} else {
console.error('Failed to load any theme!')
}
}
applyConfig(store.state.config) applyConfig(store.state.config)

@ -120,6 +120,7 @@ const EmojiPicker = {
groupRefs: {}, groupRefs: {},
emojiRefs: {}, emojiRefs: {},
filteredEmojiGroups: [], filteredEmojiGroups: [],
emojiSize: 0,
width: 0 width: 0
} }
}, },
@ -130,6 +131,23 @@ const EmojiPicker = {
Popover Popover
}, },
methods: { methods: {
updateEmojiSize () {
const css = window.getComputedStyle(this.$refs.popover.$el)
const emojiSize = css.getPropertyValue('--emojiSize')
const emojiSizeUnit = emojiSize.replace(/[0-9,.]+/, '')
const emojiSizeValue = Number(emojiSize.replace(/[^0-9,.]+/, ''))
const fontSize = css.getPropertyValue('font-size').replace(/[^0-9,.]+/, '')
let emojiSizeReal
if (emojiSizeUnit.endsWith('em')) {
emojiSizeReal = emojiSizeValue * fontSize
} else {
emojiSizeReal = emojiSizeValue
}
const fullEmojiSize = emojiSizeReal + (2 * 0.2 * fontSize)
this.emojiSize = fullEmojiSize
},
showPicker () { showPicker () {
this.$refs.popover.showPopover() this.$refs.popover.showPopover()
this.onShowing() this.onShowing()
@ -224,6 +242,7 @@ const EmojiPicker = {
}, },
onShowing () { onShowing () {
const oldContentLoaded = this.contentLoaded const oldContentLoaded = this.contentLoaded
this.updateEmojiSize()
this.recalculateItemPerRow() this.recalculateItemPerRow()
this.$nextTick(() => { this.$nextTick(() => {
this.$refs.search.focus() this.$refs.search.focus()
@ -266,16 +285,20 @@ const EmojiPicker = {
}, },
computed: { computed: {
minItemSize () { minItemSize () {
return this.emojiHeight return this.emojiSize
}, },
emojiHeight () { // used to watch it
return 32 + 4 fontSize () {
this.$nextTick(() => {
this.updateEmojiSize()
})
return this.$store.getters.mergedConfig.fontSize
}, },
emojiWidth () { emojiHeight () {
return 32 + 4 return this.emojiSize
}, },
itemPerRow () { itemPerRow () {
return this.width ? Math.floor(this.width / this.emojiWidth - 1) : 6 return this.width ? Math.floor(this.width / this.emojiSize) : 6
}, },
activeGroupView () { activeGroupView () {
return this.showingStickers ? '' : this.activeGroup return this.showingStickers ? '' : this.activeGroup

@ -1,9 +1,6 @@
$emoji-picker-header-height: 36px;
$emoji-picker-header-picture-width: 32px;
$emoji-picker-header-picture-height: 32px;
$emoji-picker-emoji-size: 32px;
.emoji-picker { .emoji-picker {
--__emoji-picker-header: 2.2em;
width: 25em; width: 25em;
max-width: calc(100vw - 20px); // popover gives 10px margin from window edge max-width: calc(100vw - 20px); // popover gives 10px margin from window edge
display: flex; display: flex;
@ -13,24 +10,26 @@ $emoji-picker-emoji-size: 32px;
display: inline-flex; display: inline-flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
width: $emoji-picker-header-picture-width; width: var(--__emoji-picker-header);
max-width: $emoji-picker-header-picture-width; max-width: var(--__emoji-picker-header);
height: $emoji-picker-header-picture-height; height: var(--__emoji-picker-header);
max-height: $emoji-picker-header-picture-height; max-height: var(--__emoji-picker-header);
.still-image { .still-image {
max-width: 100%; width: var(--__emoji-picker-header);
max-height: 100%; max-width: var(--__emoji-picker-header);
height: 100%; height: var(--__emoji-picker-header);
width: 100%; max-height: var(--__emoji-picker-header);
object-fit: contain; object-fit: contain;
--_still_image-label-scale: 0.5;
} }
} }
.keep-open, .keep-open,
.too-many-emoji, .too-many-emoji,
.hide-custom-emoji { .hide-custom-emoji {
padding: 7px; padding: 0.5em;
line-height: normal; line-height: normal;
} }
@ -44,13 +43,13 @@ $emoji-picker-emoji-size: 32px;
} }
.keep-open-label { .keep-open-label {
padding: 0 7px; padding: 0 0.5em;
display: flex; display: flex;
} }
.heading { .heading {
display: flex; display: flex;
padding: 10px 7px 5px; padding: 0.7em 0.5em 0;
} }
.content { .content {
@ -65,13 +64,14 @@ $emoji-picker-emoji-size: 32px;
display: flex; display: flex;
flex-flow: row nowrap; flex-flow: row nowrap;
overflow-x: auto; overflow-x: auto;
overflow-y: hidden;
} }
.additional-tabs { .additional-tabs {
display: flex; display: flex;
border-left: 1px solid; border-left: 1px solid;
border-left-color: var(--border); border-left-color: var(--border);
padding-left: 7px; padding-left: 0.5em;
flex: 0 0 auto; flex: 0 0 auto;
} }
@ -80,25 +80,29 @@ $emoji-picker-emoji-size: 32px;
flex-basis: auto; flex-basis: auto;
display: flex; display: flex;
align-content: center; align-content: center;
scrollbar-width: thin;
&-item { &-item {
padding: 0 7px; padding: 0 0.5em;
cursor: pointer; cursor: pointer;
font-size: 1.85em; width: var(--__emoji-picker-header);
width: $emoji-picker-header-picture-width; max-width: var(--__emoji-picker-header);
max-width: $emoji-picker-header-picture-width; height: var(--__emoji-picker-header);
height: $emoji-picker-header-picture-height; max-height: var(--__emoji-picker-header);
max-height: $emoji-picker-header-picture-height;
display: flex; display: flex;
align-items: center; align-items: center;
.svg-inline--fa {
font-size: 1.85em;
}
&.disabled { &.disabled {
opacity: 0.5; opacity: 0.5;
pointer-events: none; pointer-events: none;
} }
&.toggled { &.toggled {
border-bottom: 4px solid; border-bottom: 0.2em solid;
} }
} }
} }
@ -125,7 +129,7 @@ $emoji-picker-emoji-size: 32px;
.emoji { .emoji {
&-search { &-search {
padding: 5px; padding: 0.3em;
flex: 0 0 auto; flex: 0 0 auto;
input { input {
@ -139,6 +143,7 @@ $emoji-picker-emoji-size: 32px;
flex: 1 1 1px; flex: 1 1 1px;
position: relative; position: relative;
overflow: auto; overflow: auto;
scrollbar-gutter: stable both-edges;
user-select: none; user-select: none;
mask: mask:
linear-gradient(to top, white 0, transparent 100%) bottom no-repeat, linear-gradient(to top, white 0, transparent 100%) bottom no-repeat,
@ -165,13 +170,13 @@ $emoji-picker-emoji-size: 32px;
display: flex; display: flex;
align-items: center; align-items: center;
flex-wrap: wrap; flex-wrap: wrap;
padding-left: 5px;
justify-content: left; justify-content: left;
&-title { &-title {
font-size: 0.85em; font-size: 0.85em;
width: 100%; width: 100%;
margin: 0; margin: 0;
padding-left: 0.3em;
&.disabled { &.disabled {
display: none; display: none;
@ -180,24 +185,28 @@ $emoji-picker-emoji-size: 32px;
} }
&-item { &-item {
width: $emoji-picker-emoji-size; width: var(--emoji-size);
height: $emoji-picker-emoji-size; height: var(--emoji-size);
box-sizing: border-box; box-sizing: border-box;
display: flex; display: flex;
line-height: $emoji-picker-emoji-size; line-height: var(--emoji-size);
align-items: center; align-items: center;
justify-content: center; justify-content: center;
margin: 4px; margin: 0.2em;
cursor: pointer; cursor: pointer;
.emoji-picker-emoji.-custom { .emoji-picker-emoji.-custom {
object-fit: contain; object-fit: contain;
max-width: 100%; width: var(--emoji-size);
max-height: 100%; max-width: var(--emoji-size);
height: var(--emoji-size);
max-height: var(--emoji-size);
--_still_image-label-scale: 0.5;
} }
.emoji-picker-emoji.-unicode { .emoji-picker-emoji.-unicode {
font-size: 24px; font-size: 1.6em;
overflow: hidden; overflow: hidden;
} }
} }

@ -79,7 +79,7 @@
margin-top: 0.25em; margin-top: 0.25em;
flex-wrap: wrap; flex-wrap: wrap;
--emoji-size: calc(1.25em * var(--emojiReactionsScale, 1)); --emoji-size: calc(var(--emojiSize, 1.25em) * var(--emojiReactionsScale, 1));
.emoji-reaction-container { .emoji-reaction-container {
display: flex; display: flex;

@ -1,63 +1,59 @@
import { set } from 'lodash'
import Select from '../select/select.vue' import Select from '../select/select.vue'
import Checkbox from 'src/components/checkbox/checkbox.vue'
import Popover from 'src/components/popover/popover.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faExclamationTriangle,
faKeyboard,
faFont
} from '@fortawesome/free-solid-svg-icons'
library.add(
faExclamationTriangle,
faKeyboard,
faFont
)
export default { export default {
components: { components: {
Select Select,
Checkbox,
Popover
}, },
props: [ props: [
'name', 'label', 'modelValue', 'fallback', 'options', 'no-inherit' 'name', 'label', 'modelValue', 'fallback', 'options', 'no-inherit'
], ],
mounted () {
this.$store.dispatch('queryLocalFonts')
},
emits: ['update:modelValue'], emits: ['update:modelValue'],
data () { data () {
return { return {
lValue: this.modelValue, manualEntry: false,
availableOptions: [ availableOptions: [
this.noInherit ? '' : 'inherit', this.noInherit ? '' : 'inherit',
'custom',
...(this.options || []),
'serif', 'serif',
'sans-serif',
'monospace', 'monospace',
'sans-serif' ...(this.options || [])
].filter(_ => _) ].filter(_ => _)
} }
}, },
beforeUpdate () { methods: {
this.lValue = this.modelValue toggleManualEntry () {
this.manualEntry = !this.manualEntry
}
}, },
computed: { computed: {
present () { present () {
return typeof this.lValue !== 'undefined' return typeof this.modelValue !== 'undefined'
},
dValue () {
return this.lValue || this.fallback || {}
},
family: {
get () {
return this.dValue.family
},
set (v) {
set(this.lValue, 'family', v)
this.$emit('update:modelValue', this.lValue)
}
}, },
isCustom () { localFontsList () {
return this.preset === 'custom' return this.$store.state.interface.localFonts
}, },
preset: { localFontsSize () {
get () { return this.$store.state.interface.localFonts?.length
if (this.family === 'serif' ||
this.family === 'sans-serif' ||
this.family === 'monospace' ||
this.family === 'inherit') {
return this.family
} else {
return 'custom'
}
},
set (v) {
this.family = v === 'custom' ? '' : v
}
} }
} }
} }

@ -1,6 +1,6 @@
<template> <template>
<div <div
class="font-control style-control" class="font-control"
:class="{ custom: isCustom }" :class="{ custom: isCustom }"
> >
<label <label
@ -10,43 +10,121 @@
> >
{{ label }} {{ label }}
</label> </label>
<input {{ ' ' }}
<Checkbox
v-if="typeof fallback !== 'undefined'" v-if="typeof fallback !== 'undefined'"
:id="name + '-o'" :id="name + '-o'"
:aria-labelledby="name + '-label'" :modelValue="present"
class="input -checkbox opt exlcude-disabled visible-for-screenreader-only"
type="checkbox"
:checked="present"
@change="$emit('update:modelValue', typeof modelValue === 'undefined' ? fallback : undefined)" @change="$emit('update:modelValue', typeof modelValue === 'undefined' ? fallback : undefined)"
> >
<label {{ $t('settings.style.themes3.define') }}
v-if="typeof fallback !== 'undefined'" </Checkbox>
class="opt-l" <p v-if="modelValue?.family">
:for="name + '-o'" <label
:aria-hidden="true" v-if="manualEntry"
/> :id="name + '-label'"
{{ ' ' }} :for="preset === 'custom' ? name : name + '-font-switcher'"
<Select class="label"
:id="name + '-font-switcher'"
v-model="preset"
:disabled="!present"
class="font-switcher"
>
<option
v-for="option in availableOptions"
:key="option"
:value="option"
> >
{{ option === 'custom' ? $t('settings.style.fonts.custom') : option }} <i18n-t
</option> keypath="settings.style.themes3.font.entry"
</Select> tag="span"
<input >
v-if="isCustom" <template #fontFamily>
:id="name" <code>font-family</code>
v-model="family" </template>
class="input custom-font" </i18n-t>
type="text" </label>
> <label
v-else
:id="name + '-label'"
:for="preset === 'custom' ? name : name + '-font-switcher'"
class="label"
>
{{ $t('settings.style.themes3.font.select') }}
</label>
{{ ' ' }}
<span
v-if="manualEntry"
class="btn-group"
>
<button
class="btn button-default"
@click="toggleManualEntry"
:title="$t('settings.style.themes3.font.lookup_local_fonts')"
>
<FAIcon
fixed-width
icon="font"
/>
</button>
<input
:id="name"
:model-value="modelValue.family"
class="input custom-font"
type="text"
@update:modelValue="$emit('update:modelValue', { ...(modelValue || {}), family: $event.target.value })"
>
</span>
<span
v-else
class="btn-group"
>
<button
class="btn button-default"
@click="toggleManualEntry"
:title="$t('settings.style.themes3.font.enter_manually')"
>
<FAIcon
fixed-width
icon="keyboard"
/>
</button>
<Select
:id="name + '-local-font-switcher'"
:model-value="modelValue?.family"
class="custom-font"
@update:modelValue="v => $emit('update:modelValue', { ...(modelValue || {}), family: v })"
>
<optgroup
:label="$t('settings.style.themes3.font.group-builtin')"
>
<option
v-for="option in availableOptions"
:key="option"
:value="option"
:style="{ fontFamily: option === 'inherit' ? null : option }"
>
{{ $t('settings.style.themes3.font.builtin.' + option) }}
</option>
</optgroup>
<optgroup
v-if="localFontsSize > 0"
:label="$t('settings.style.themes3.font.group-local')"
>
<option
v-for="option in localFontsList"
:key="option"
:value="option"
:style="{ fontFamily: option }"
>
{{ option }}
</option>
</optgroup>
<optgroup
v-else
:label="$t('settings.style.themes3.font.group-local')"
>
<option disabled>
{{ $t('settings.style.themes3.font.local-unavailable1') }}
</option>
<option disabled>
{{ $t('settings.style.themes3.font.local-unavailable2') }}
</option>
</optgroup>
</Select>
</span>
</p>
</div> </div>
</template> </template>
@ -54,21 +132,15 @@
<style lang="scss"> <style lang="scss">
.font-control { .font-control {
input.custom-font { .custom-font {
min-width: 10em; min-width: 20em;
max-width: 20em;
} }
}
&.custom { .invalid-tooltip {
/* TODO Should make proper joiners... */ margin: 0.5em 1em;
.font-switcher { min-width: 10em;
border-top-right-radius: 0; text-align: center;
border-bottom-right-radius: 0;
}
.custom-font {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
}
} }
</style> </style>

@ -129,7 +129,7 @@
.mobile-nav { .mobile-nav {
display: grid; display: grid;
line-height: var(--navbar-height); line-height: var(--navbar-height);
grid-template-rows: 50px; grid-template-rows: var(--navbar-height);
grid-template-columns: 2fr auto; grid-template-columns: 2fr auto;
width: 100%; width: 100%;
box-sizing: border-box; box-sizing: border-box;
@ -190,8 +190,8 @@
justify-content: space-between; justify-content: space-between;
z-index: calc(var(--ZI_navbar) + 100); z-index: calc(var(--ZI_navbar) + 100);
width: 100%; width: 100%;
height: 50px; height: 3.5em;
line-height: 50px; line-height: 3.5em;
position: absolute; position: absolute;
box-shadow: var(--shadow); box-shadow: var(--shadow);
@ -214,7 +214,7 @@
} }
.mobile-notifications { .mobile-notifications {
margin-top: 50px; margin-top: 3.5em;
width: 100vw; width: 100vw;
height: calc(100vh - var(--navbar-height)); height: calc(100vh - var(--navbar-height));
overflow-x: hidden; overflow-x: hidden;

@ -49,6 +49,7 @@
} }
&.toggled { &.toggled {
margin-bottom: -4px;
border-bottom: 4px solid; border-bottom: 4px solid;
} }
} }

@ -11,7 +11,8 @@ export default {
'RichContent', 'RichContent',
'Input', 'Input',
'Avatar', 'Avatar',
'Attachment' 'Attachment',
'PollGraph'
], ],
defaultRules: [] defaultRules: []
} }

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

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

@ -15,6 +15,7 @@
</template> </template>
<slot v-else /> <slot v-else />
</label> </label>
{{ ' ' }}
<input <input
:id="path" :id="path"
class="input number-input" class="input number-input"

@ -48,6 +48,10 @@ export default {
draftMode: { draftMode: {
type: Boolean, type: Boolean,
default: undefined default: undefined
},
timedApplyMode: {
type: Boolean,
default: false
} }
}, },
inject: { inject: {
@ -161,7 +165,11 @@ export default {
case 'admin': case 'admin':
return (k, v) => this.$store.dispatch('pushAdminSetting', { path: k, value: v }) return (k, v) => this.$store.dispatch('pushAdminSetting', { path: k, value: v })
default: default:
return (k, v) => this.$store.dispatch('setOption', { name: k, value: v }) if (this.timedApplyMode) {
return (k, v) => this.$store.dispatch('setOptionTemporarily', { name: k, value: v })
} else {
return (k, v) => this.$store.dispatch('setOption', { name: k, value: v })
}
} }
}, },
defaultState () { defaultState () {

@ -21,15 +21,23 @@ export default {
unitSet: { unitSet: {
type: String, type: String,
default: 'none' default: 'none'
},
step: {
type: Number,
default: 1
},
resetDefault: {
type: Object,
default: null
} }
}, },
computed: { computed: {
...Setting.computed, ...Setting.computed,
stateUnit () { stateUnit () {
return this.state.replace(/\d+/, '') return typeof this.state === 'string' ? this.state.replace(/[0-9,.]+/, '') : ''
}, },
stateValue () { stateValue () {
return this.state.replace(/\D+/, '') return typeof this.state === 'string' ? this.state.replace(/[^0-9,.]+/, '') : ''
} }
}, },
methods: { methods: {
@ -39,10 +47,18 @@ export default {
return this.$t(['settings', 'units', this.unitSet, value].join('.')) return this.$t(['settings', 'units', this.unitSet, value].join('.'))
}, },
updateValue (e) { updateValue (e) {
this.configSink(this.path, parseInt(e.target.value) + this.stateUnit) this.configSink(this.path, parseFloat(e.target.value) + this.stateUnit)
}, },
updateUnit (e) { updateUnit (e) {
this.configSink(this.path, this.stateValue + e.target.value) let value = this.stateValue
const newUnit = e.target.value
if (this.resetDefault) {
const replaceValue = this.resetDefault[newUnit]
if (replaceValue != null) {
value = replaceValue
}
}
this.configSink(this.path, value + newUnit)
} }
} }
} }

@ -9,11 +9,12 @@
> >
<slot /> <slot />
</label> </label>
{{ ' ' }}
<input <input
:id="path" :id="path"
class="input number-input" class="input number-input"
type="number" type="number"
step="1" :step="step"
:disabled="disabled" :disabled="disabled"
:min="min || 0" :min="min || 0"
:value="stateValue" :value="stateValue"

@ -4,6 +4,7 @@ import AsyncComponentError from 'src/components/async_component_error/async_comp
import getResettableAsyncComponent from 'src/services/resettable_async_component.js' import getResettableAsyncComponent from 'src/services/resettable_async_component.js'
import Popover from '../popover/popover.vue' import Popover from '../popover/popover.vue'
import Checkbox from 'src/components/checkbox/checkbox.vue' import Checkbox from 'src/components/checkbox/checkbox.vue'
import ConfirmModal from 'src/components/confirm_modal/confirm_modal.vue'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { cloneDeep, isEqual } from 'lodash' import { cloneDeep, isEqual } from 'lodash'
import { import {
@ -53,6 +54,7 @@ const SettingsModal = {
Modal, Modal,
Popover, Popover,
Checkbox, Checkbox,
ConfirmModal,
SettingsModalUserContent: getResettableAsyncComponent( SettingsModalUserContent: getResettableAsyncComponent(
() => import('./settings_modal_user_content.vue'), () => import('./settings_modal_user_content.vue'),
{ {
@ -165,6 +167,7 @@ const SettingsModal = {
}, },
computed: { computed: {
currentSaveStateNotice () { currentSaveStateNotice () {
console.log(this.$store.state.interface.settings.currentSaveStateNotice)
return this.$store.state.interface.settings.currentSaveStateNotice return this.$store.state.interface.settings.currentSaveStateNotice
}, },
modalActivated () { modalActivated () {

@ -147,6 +147,18 @@
</span> </span>
</div> </div>
</div> </div>
<teleport to="#modal">
<ConfirmModal
v-if="$store.state.interface.temporaryChangesTimeoutId"
:title="$t('settings.confirm_new_setting')"
:cancel-text="$t('settings.revert')"
:confirm-text="$t('settings.confirm')"
@cancelled="$store.state.interface.temporaryChangesRevert"
@accepted="$store.state.interface.temporaryChangesConfirm"
>
{{ $t('settings.confirm_new_question') }}
</ConfirmModal>
</teleport>
</Modal> </Modal>
</template> </template>

@ -7,6 +7,7 @@ import FilteringTab from './tabs/filtering_tab.vue'
import SecurityTab from './tabs/security_tab/security_tab.vue' import SecurityTab from './tabs/security_tab/security_tab.vue'
import ProfileTab from './tabs/profile_tab.vue' import ProfileTab from './tabs/profile_tab.vue'
import GeneralTab from './tabs/general_tab.vue' import GeneralTab from './tabs/general_tab.vue'
import AppearanceTab from './tabs/appearance_tab.vue'
import VersionTab from './tabs/version_tab.vue' import VersionTab from './tabs/version_tab.vue'
import ThemeTab from './tabs/theme_tab/theme_tab.vue' import ThemeTab from './tabs/theme_tab/theme_tab.vue'
@ -19,7 +20,8 @@ import {
faBell, faBell,
faDownload, faDownload,
faEyeSlash, faEyeSlash,
faInfo faInfo,
faWindowRestore
} from '@fortawesome/free-solid-svg-icons' } from '@fortawesome/free-solid-svg-icons'
library.add( library.add(
@ -30,7 +32,8 @@ library.add(
faBell, faBell,
faDownload, faDownload,
faEyeSlash, faEyeSlash,
faInfo faInfo,
faWindowRestore
) )
const SettingsModalContent = { const SettingsModalContent = {
@ -44,6 +47,7 @@ const SettingsModalContent = {
SecurityTab, SecurityTab,
ProfileTab, ProfileTab,
GeneralTab, GeneralTab,
AppearanceTab,
VersionTab, VersionTab,
ThemeTab ThemeTab
}, },

@ -13,6 +13,20 @@
> >
<GeneralTab /> <GeneralTab />
</div> </div>
<div
:label="$t('settings.appearance')"
icon="window-restore"
data-tab-name="appearance"
>
<AppearanceTab />
</div>
<div
:label="$t('settings.theme')"
icon="paint-brush"
data-tab-name="theme"
>
<ThemeTab />
</div>
<div <div
v-if="isLoggedIn" v-if="isLoggedIn"
:label="$t('settings.profile_tab')" :label="$t('settings.profile_tab')"
@ -21,6 +35,14 @@
> >
<ProfileTab /> <ProfileTab />
</div> </div>
<div
v-if="isLoggedIn"
:label="$t('settings.notifications')"
icon="bell"
data-tab-name="notifications"
>
<NotificationsTab />
</div>
<div <div
v-if="isLoggedIn" v-if="isLoggedIn"
:label="$t('settings.security_tab')" :label="$t('settings.security_tab')"
@ -36,20 +58,14 @@
> >
<FilteringTab /> <FilteringTab />
</div> </div>
<div
:label="$t('settings.theme')"
icon="paint-brush"
data-tab-name="theme"
>
<ThemeTab />
</div>
<div <div
v-if="isLoggedIn" v-if="isLoggedIn"
:label="$t('settings.notifications')" :label="$t('settings.mutes_and_blocks')"
icon="bell" :fullHeight="true"
data-tab-name="notifications" icon="eye-slash"
data-tab-name="mutesAndBlocks"
> >
<NotificationsTab /> <MutesAndBlocksTab />
</div> </div>
<div <div
v-if="isLoggedIn" v-if="isLoggedIn"
@ -59,15 +75,6 @@
> >
<DataImportExportTab /> <DataImportExportTab />
</div> </div>
<div
v-if="isLoggedIn"
:label="$t('settings.mutes_and_blocks')"
:fullHeight="true"
icon="eye-slash"
data-tab-name="mutesAndBlocks"
>
<MutesAndBlocksTab />
</div>
<div <div
:label="$t('settings.version.title')" :label="$t('settings.version.title')"
icon="info" icon="info"

@ -0,0 +1,195 @@
import BooleanSetting from '../helpers/boolean_setting.vue'
import ChoiceSetting from '../helpers/choice_setting.vue'
import IntegerSetting from '../helpers/integer_setting.vue'
import FloatSetting from '../helpers/float_setting.vue'
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'
import {
faGlobe
} from '@fortawesome/free-solid-svg-icons'
import Preview from './theme_tab/preview.vue'
library.add(
faGlobe
)
const AppearanceTab = {
data () {
return {
availableStyles: [],
intersectionObserver: null,
thirdColumnModeOptions: ['none', 'notifications', 'postform'].map(mode => ({
key: mode,
value: mode,
label: this.$t(`settings.third_column_mode_${mode}`)
})),
forcedRoundnessOptions: ['disabled', 'sharp', 'nonsharp', 'round'].map((mode, i) => ({
key: mode,
value: i - 1,
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.style.themes3.hacks.underlay_override_mode_${mode}`)
}))
}
},
components: {
BooleanSetting,
ChoiceSetting,
IntegerSetting,
FloatSetting,
UnitSetting,
ProfileSettingIndicator,
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
},
fontsOverride () {
return this.$store.getters.mergedConfig.fontsOverride
},
columns () {
const mode = this.$store.getters.mergedConfig.thirdColumnMode
const notif = mode === 'none' ? [] : ['notifs']
if (this.$store.getters.mergedConfig.sidebarRight || mode === 'postform') {
return [...notif, 'content', 'sidebar']
} else {
return ['sidebar', 'content', ...notif]
}
},
instanceSpecificPanelPresent () { return this.$store.state.instance.showInstanceSpecificPanel },
instanceWallpaperUsed () {
return this.$store.state.instance.background &&
!this.$store.state.users.currentUser.background_image
},
instanceShoutboxPresent () { return this.$store.state.instance.shoutAvailable },
language: {
get: function () { return this.$store.getters.mergedConfig.interfaceLanguage },
set: function (val) {
this.$store.dispatch('setOption', { name: 'interfaceLanguage', value: val })
}
},
isCustomThemeUsed () {
const { theme } = this.mergedConfig
return theme === 'custom' || theme === null
},
...SharedComputedObject()
},
methods: {
updateFont (key, value) {
console.log(key, value)
this.$store.dispatch('setOption', {
name: 'theme3hacks',
value: {
...this.mergedConfig.theme3hacks,
fonts: {
...this.mergedConfig.theme3hacks.fonts,
[key]: value
}
}
})
},
isThemeActive (key) {
const { theme } = this.mergedConfig
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')
}
}
}
export default AppearanceTab

@ -0,0 +1,313 @@
<template>
<div class="appearance-tab" :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="alert neutral theme-notice">
{{ $t("settings.style.appearance_tab_note") }}
</div>
<div class="setting-item">
<h2>{{ $t('settings.scale_and_layout') }}</h2>
<ul class="setting-list">
<li>
<UnitSetting
path="textSize"
step="0.1"
:units="['px', 'rem']"
:reset-default="{ 'px': 14, 'rem': 1 }"
timed-apply-mode
>
{{ $t('settings.text_size') }}
</UnitSetting>
<div>
<small>
<i18n-t
scope="global"
keypath="settings.text_size_tip"
tag="span"
>
<code>px</code>
<code>rem</code>
</i18n-t>
<br/>
<i18n-t
scope="global"
keypath="settings.text_size_tip2"
tag="span"
>
<code>14px</code>
</i18n-t>
</small>
</div>
</li>
<li>
<h3>{{ $t('settings.style.interface_font_user_override') }}</h3>
<ul class="setting-list">
<li>
<FontControl
:model-value="mergedConfig.theme3hacks.fonts.interface"
name="ui"
:label="$t('settings.style.fonts.components.interface')"
:fallback="{ family: 'sans-serif' }"
no-inherit="1"
@update:modelValue="v => updateFont('interface', v)"
/>
</li>
<li>
<FontControl
v-if="expertLevel > 0"
:model-value="mergedConfig.theme3hacks.fonts.input"
name="input"
:fallback="{ family: 'inherit' }"
:label="$t('settings.style.fonts.components.input')"
@update:modelValue="v => updateFont('input', v)"
/>
</li>
<li>
<FontControl
v-if="expertLevel > 0"
:model-value="mergedConfig.theme3hacks.fonts.post"
name="post"
:fallback="{ family: 'inherit' }"
:label="$t('settings.style.fonts.components.post')"
@update:modelValue="v => updateFont('post', v)"
/>
</li>
<li>
<FontControl
v-if="expertLevel > 0"
:model-value="mergedConfig.theme3hacks.fonts.monospace"
name="postCode"
:fallback="{ family: 'monospace' }"
:label="$t('settings.style.fonts.components.monospace')"
@update:modelValue="v => updateFont('monospace', v)"
/>
</li>
</ul>
</li>
<li>
<UnitSetting
path="emojiSize"
step="0.1"
:units="['px', 'rem']"
:reset-default="{ 'px': 32, 'rem': 2.2 }"
>
{{ $t('settings.emoji_size') }}
</UnitSetting>
<ul
class="setting-list suboptions"
>
<li>
<FloatSetting
v-if="user"
path="emojiReactionsScale"
expert="1"
>
{{ $t('settings.emoji_reactions_scale') }}
</FloatSetting>
</li>
</ul>
</li>
<li>
<UnitSetting
path="navbarSize"
step="0.1"
:units="['px', 'rem']"
:reset-default="{ 'px': 55, 'rem': 3.5 }"
>
{{ $t('settings.navbar_size') }}
</UnitSetting>
</li>
<h3>{{ $t('settings.columns') }}</h3>
<li>
<UnitSetting
path="panelHeaderSize"
step="0.1"
:units="['px', 'rem']"
:reset-default="{ 'px': 52, 'rem': 3.2 }"
timed-apply-mode
>
{{ $t('settings.panel_header_size') }}
</UnitSetting>
</li>
<li>
<BooleanSetting path="sidebarRight">
{{ $t('settings.right_sidebar') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="navbarColumnStretch">
{{ $t('settings.navbar_column_stretch') }}
</BooleanSetting>
</li>
<li>
<ChoiceSetting
v-if="user"
id="thirdColumnMode"
path="thirdColumnMode"
:options="thirdColumnModeOptions"
>
{{ $t('settings.third_column_mode') }}
</ChoiceSetting>
</li>
<li v-if="expertLevel > 0">
{{ $t('settings.column_sizes') }}
<div class="column-settings">
<UnitSetting
v-for="column in columns"
:key="column"
:path="column + 'ColumnWidth'"
:units="horizontalUnits"
expert="1"
>
{{ $t('settings.column_sizes_' + column) }}
</UnitSetting>
</div>
</li>
<li>
<BooleanSetting path="disableStickyHeaders">
{{ $t('settings.disable_sticky_headers') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="showScrollbars">
{{ $t('settings.show_scrollbars') }}
</BooleanSetting>
</li>
</ul>
</div>
<div class="setting-item">
<h2>{{ $t('settings.visual_tweaks') }}</h2>
<ul class="setting-list">
<li>
<ChoiceSetting
id="forcedRoundness"
path="forcedRoundness"
:options="forcedRoundnessOptions"
>
{{ $t('settings.style.themes3.hacks.force_interface_roundness') }}
</ChoiceSetting>
</li>
<li>
<ChoiceSetting
id="underlayOverride"
path="theme3hacks.underlay"
:options="underlayOverrideModes"
>
{{ $t('settings.style.themes3.hacks.underlay_overrides') }}
</ChoiceSetting>
</li>
<li v-if="instanceWallpaperUsed">
<BooleanSetting path="hideInstanceWallpaper">
{{ $t('settings.hide_wallpaper') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting
path="forceThemeRecompilation"
:expert="1"
>
{{ $t('settings.force_theme_recompilation_debug') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting
path="themeDebug"
:expert="1"
>
{{ $t('settings.theme_debug') }}
</BooleanSetting>
</li>
</ul>
</div>
</div>
</template>
<script src="./appearance_tab.js"></script>
<style lang="scss">
.appearance-tab {
.theme-notice {
padding: 0.5em;
margin: 1em;
}
.column-settings {
display: flex;
justify-content: space-evenly;
flex-wrap: wrap;
}
.column-settings .size-label {
display: block;
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 {
font-size: 1rem; // fix for firefox
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>

@ -3,7 +3,7 @@ import ChoiceSetting from '../helpers/choice_setting.vue'
import ScopeSelector from 'src/components/scope_selector/scope_selector.vue' import ScopeSelector from 'src/components/scope_selector/scope_selector.vue'
import IntegerSetting from '../helpers/integer_setting.vue' import IntegerSetting from '../helpers/integer_setting.vue'
import FloatSetting from '../helpers/float_setting.vue' import FloatSetting from '../helpers/float_setting.vue'
import UnitSetting, { defaultHorizontalUnits } from '../helpers/unit_setting.vue' import UnitSetting from '../helpers/unit_setting.vue'
import InterfaceLanguageSwitcher from 'src/components/interface_language_switcher/interface_language_switcher.vue' import InterfaceLanguageSwitcher from 'src/components/interface_language_switcher/interface_language_switcher.vue'
import SharedComputedObject from '../helpers/shared_computed_object.js' import SharedComputedObject from '../helpers/shared_computed_object.js'
@ -40,11 +40,6 @@ const GeneralTab = {
value: mode, value: mode,
label: this.$t(`settings.mention_link_display_${mode}`) label: this.$t(`settings.mention_link_display_${mode}`)
})), })),
thirdColumnModeOptions: ['none', 'notifications', 'postform'].map(mode => ({
key: mode,
value: mode,
label: this.$t(`settings.third_column_mode_${mode}`)
})),
userPopoverAvatarActionOptions: ['close', 'zoom', 'open'].map(mode => ({ userPopoverAvatarActionOptions: ['close', 'zoom', 'open'].map(mode => ({
key: mode, key: mode,
value: mode, value: mode,
@ -70,9 +65,6 @@ const GeneralTab = {
ProfileSettingIndicator ProfileSettingIndicator
}, },
computed: { computed: {
horizontalUnits () {
return defaultHorizontalUnits
},
postFormats () { postFormats () {
return this.$store.state.instance.postFormats || [] return this.$store.state.instance.postFormats || []
}, },
@ -83,29 +75,6 @@ const GeneralTab = {
label: this.$t(`post_status.content_type["${format}"]`) label: this.$t(`post_status.content_type["${format}"]`)
})) }))
}, },
columns () {
const mode = this.$store.getters.mergedConfig.thirdColumnMode
const notif = mode === 'none' ? [] : ['notifs']
if (this.$store.getters.mergedConfig.sidebarRight || mode === 'postform') {
return [...notif, 'content', 'sidebar']
} else {
return ['sidebar', 'content', ...notif]
}
},
instanceSpecificPanelPresent () { return this.$store.state.instance.showInstanceSpecificPanel },
instanceWallpaperUsed () {
return this.$store.state.instance.background &&
!this.$store.state.users.currentUser.background_image
},
instanceShoutboxPresent () { return this.$store.state.instance.shoutAvailable },
language: {
get: function () { return this.$store.getters.mergedConfig.interfaceLanguage },
set: function (val) {
this.$store.dispatch('setOption', { name: 'interfaceLanguage', value: val })
}
},
...SharedComputedObject() ...SharedComputedObject()
}, },
methods: { methods: {

@ -15,11 +15,6 @@
{{ $t('settings.hide_isp') }} {{ $t('settings.hide_isp') }}
</BooleanSetting> </BooleanSetting>
</li> </li>
<li v-if="instanceWallpaperUsed">
<BooleanSetting path="hideInstanceWallpaper">
{{ $t('settings.hide_wallpaper') }}
</BooleanSetting>
</li>
<li> <li>
<BooleanSetting path="stopGifs"> <BooleanSetting path="stopGifs">
{{ $t('settings.stop_gifs') }} {{ $t('settings.stop_gifs') }}
@ -98,53 +93,6 @@
{{ $t('settings.hide_shoutbox') }} {{ $t('settings.hide_shoutbox') }}
</BooleanSetting> </BooleanSetting>
</li> </li>
<li>
<h3>{{ $t('settings.columns') }}</h3>
</li>
<li>
<BooleanSetting path="disableStickyHeaders">
{{ $t('settings.disable_sticky_headers') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="showScrollbars">
{{ $t('settings.show_scrollbars') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="sidebarRight">
{{ $t('settings.right_sidebar') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="navbarColumnStretch">
{{ $t('settings.navbar_column_stretch') }}
</BooleanSetting>
</li>
<li>
<ChoiceSetting
v-if="user"
id="thirdColumnMode"
path="thirdColumnMode"
:options="thirdColumnModeOptions"
>
{{ $t('settings.third_column_mode') }}
</ChoiceSetting>
</li>
<li v-if="expertLevel > 0">
{{ $t('settings.column_sizes') }}
<div class="column-settings">
<UnitSetting
v-for="column in columns"
:key="column"
:path="column + 'ColumnWidth'"
:units="horizontalUnits"
expert="1"
>
{{ $t('settings.column_sizes_' + column) }}
</UnitSetting>
</div>
</li>
<li class="select-multiple"> <li class="select-multiple">
<span class="label">{{ $t('settings.confirm_dialogs') }}</span> <span class="label">{{ $t('settings.confirm_dialogs') }}</span>
<ul class="option-list"> <ul class="option-list">
@ -200,14 +148,6 @@
<div class="setting-item"> <div class="setting-item">
<h2>{{ $t('settings.post_look_feel') }}</h2> <h2>{{ $t('settings.post_look_feel') }}</h2>
<ul class="setting-list"> <ul class="setting-list">
<li>
<BooleanSetting
path="forceThemeRecompilation"
:expert="1"
>
{{ $t('settings.force_theme_recompilation_debug') }}
</BooleanSetting>
</li>
<li> <li>
<ChoiceSetting <ChoiceSetting
id="conversationDisplay" id="conversationDisplay"
@ -277,15 +217,6 @@
{{ $t('settings.no_rich_text_description') }} {{ $t('settings.no_rich_text_description') }}
</BooleanSetting> </BooleanSetting>
</li> </li>
<li>
<FloatSetting
v-if="user"
path="emojiReactionsScale"
expert="1"
>
{{ $t('settings.emoji_reactions_scale') }}
</FloatSetting>
</li>
<h3>{{ $t('settings.attachments') }}</h3> <h3>{{ $t('settings.attachments') }}</h3>
<li> <li>
<BooleanSetting <BooleanSetting
@ -528,17 +459,3 @@
</template> </template>
<script src="./general_tab.js"></script> <script src="./general_tab.js"></script>
<style lang="scss">
.column-settings {
display: flex;
justify-content: space-evenly;
flex-wrap: wrap;
}
.column-settings .size-label {
display: block;
margin-bottom: 0.5em;
margin-top: 0.5em;
}
</style>

@ -99,15 +99,9 @@
> >
<div class="actions"> <div class="actions">
<span class="checkbox"> <Checkbox>
<input {{ $t('settings.style.preview.checkbox') }}
id="preview_checkbox" </Checkbox>
checked="very yes"
type="checkbox"
class="input"
>
<label for="preview_checkbox">{{ $t('settings.style.preview.checkbox') }}</label>
</span>
<button class="btn button-default"> <button class="btn button-default">
{{ $t('settings.style.preview.button') }} {{ $t('settings.style.preview.button') }}
</button> </button>
@ -118,6 +112,7 @@
</template> </template>
<script> <script>
import Checkbox from 'src/components/checkbox/checkbox.vue'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { import {
faTimes, faTimes,
@ -133,12 +128,116 @@ library.add(
faReply faReply
) )
export default {} export default {
components: {
Checkbox
}
}
</script> </script>
<style lang="scss"> <style lang="scss">
.preview-container { .preview-container {
position: relative; 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 { .underlay-preview {
@ -148,4 +247,4 @@ export default {}
left: 10px; left: 10px;
right: 10px; right: 10px;
} }
</style> </style>

@ -1,7 +1,8 @@
import { import {
rgb2hex, rgb2hex,
hex2rgb, hex2rgb,
getContrastRatioLayers getContrastRatioLayers,
relativeLuminance
} from 'src/services/color_convert/color_convert.js' } from 'src/services/color_convert/color_convert.js'
import { import {
getThemes getThemes
@ -23,10 +24,17 @@ import {
generateShadows, generateShadows,
generateRadii, generateRadii,
generateFonts, generateFonts,
composePreset,
shadows2to3, shadows2to3,
colors2to3 colors2to3
} from 'src/services/theme_data/theme_data.service.js' } from 'src/services/theme_data/theme_data.service.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 ColorInput from 'src/components/color_input/color_input.vue' import ColorInput from 'src/components/color_input/color_input.vue'
import RangeInput from 'src/components/range_input/range_input.vue' import RangeInput from 'src/components/range_input/range_input.vue'
import OpacityInput from 'src/components/opacity_input/opacity_input.vue' import OpacityInput from 'src/components/opacity_input/opacity_input.vue'
@ -62,6 +70,7 @@ const colorConvert = (color) => {
export default { export default {
data () { data () {
return { return {
themeV3Preview: [],
themeImporter: newImporter({ themeImporter: newImporter({
validator: this.importValidator, validator: this.importValidator,
onImport: this.onImport, onImport: this.onImport,
@ -78,10 +87,7 @@ export default {
tempImportFile: undefined, tempImportFile: undefined,
engineVersion: 0, engineVersion: 0,
previewShadows: {}, previewTheme: {},
previewColors: {},
previewRadii: {},
previewFonts: {},
shadowsInvalid: true, shadowsInvalid: true,
colorsInvalid: true, colorsInvalid: true,
@ -232,13 +238,6 @@ export default {
chatMessage: this.chatMessageRadiusLocal chatMessage: this.chatMessageRadiusLocal
} }
}, },
preview () {
return composePreset(this.previewColors, this.previewRadii, this.previewShadows, this.previewFonts)
},
previewTheme () {
if (!this.preview.theme.colors) return { colors: {}, opacity: {}, radii: {}, shadows: {}, fonts: {} }
return this.preview.theme
},
// This needs optimization maybe // This needs optimization maybe
previewContrast () { previewContrast () {
try { try {
@ -306,14 +305,6 @@ export default {
return {} return {}
} }
}, },
previewRules () {
if (!this.preview.rules) return ''
return [
...Object.values(this.preview.rules),
'color: var(--text)',
'font-family: var(--interfaceFont, sans-serif)'
].join(';')
},
shadowsAvailable () { shadowsAvailable () {
return Object.keys(DEFAULT_SHADOWS).sort() return Object.keys(DEFAULT_SHADOWS).sort()
}, },
@ -511,17 +502,14 @@ export default {
} }
}, },
setCustomTheme () { setCustomTheme () {
this.$store.dispatch('setOption', { this.$store.dispatch('setThemeV2', {
name: 'customTheme', customTheme: {
value: { ignore: true,
themeFileVersion: this.selectedVersion, themeFileVersion: this.selectedVersion,
themeEngineVersion: CURRENT_VERSION, themeEngineVersion: CURRENT_VERSION,
...this.previewTheme ...this.previewTheme
} },
}) customThemeSource: {
this.$store.dispatch('setOption', {
name: 'customThemeSource',
value: {
themeFileVersion: this.selectedVersion, themeFileVersion: this.selectedVersion,
themeEngineVersion: CURRENT_VERSION, themeEngineVersion: CURRENT_VERSION,
shadows: this.shadowsLocal, shadows: this.shadowsLocal,
@ -532,16 +520,24 @@ export default {
} }
}) })
}, },
updatePreviewColorsAndShadows () { updatePreviewColors () {
this.previewColors = generateColors({ const result = generateColors({
opacity: this.currentOpacity, opacity: this.currentOpacity,
colors: this.currentColors colors: this.currentColors
}) })
this.previewShadows = generateShadows( this.previewTheme.colors = result.theme.colors
{ shadows: this.shadowsLocal, opacity: this.previewTheme.opacity, themeEngineVersion: this.engineVersion }, this.previewTheme.opacity = result.theme.opacity
this.previewColors.theme.colors, },
this.previewColors.mod updatePreviewShadows () {
) this.previewTheme.shadows = generateShadows(
{
shadows: this.shadowsLocal,
opacity: this.previewTheme.opacity,
themeEngineVersion: this.engineVersion
},
this.previewTheme.colors,
relativeLuminance(this.previewTheme.colors.bg) < 0.5 ? 1 : -1
).theme.shadows
}, },
importTheme () { this.themeImporter.importData() }, importTheme () { this.themeImporter.importData() },
exportTheme () { this.themeExporter.exportData() }, exportTheme () { this.themeExporter.exportData() },
@ -610,7 +606,7 @@ export default {
normalizeLocalState (theme, version = 0, source, forceSource = false) { normalizeLocalState (theme, version = 0, source, forceSource = false) {
let input let input
if (typeof source !== 'undefined') { if (typeof source !== 'undefined') {
if (forceSource || source.themeEngineVersion === CURRENT_VERSION) { if (forceSource || source?.themeEngineVersion === CURRENT_VERSION) {
input = source input = source
version = source.themeEngineVersion version = source.themeEngineVersion
} else { } else {
@ -692,6 +688,8 @@ export default {
} else { } else {
this.shadowsLocal = shadows this.shadowsLocal = shadows
} }
this.updatePreviewColors()
this.updatePreviewShadows()
this.shadowSelected = this.shadowsAvailable[0] this.shadowSelected = this.shadowsAvailable[0]
} }
@ -699,12 +697,25 @@ export default {
this.clearFonts() this.clearFonts()
this.fontsLocal = fonts this.fontsLocal = fonts
} }
},
updateTheme3Preview () {
const theme2 = convertTheme2To3(this.previewTheme)
const theme3 = init({
inputRuleset: theme2,
ultimateBackgroundColor: '#000000',
liteMode: true
})
this.themeV3Preview = getScopedVersion(
getCssRules(theme3.eager),
'#theme-preview'
).join('\n')
} }
}, },
watch: { watch: {
currentRadii () { currentRadii () {
try { try {
this.previewRadii = generateRadii({ radii: this.currentRadii }) this.previewTheme.radii = generateRadii({ radii: this.currentRadii }).theme.radii
this.radiiInvalid = false this.radiiInvalid = false
} catch (e) { } catch (e) {
this.radiiInvalid = true this.radiiInvalid = true
@ -713,9 +724,8 @@ export default {
}, },
shadowsLocal: { shadowsLocal: {
handler () { handler () {
if (Object.getOwnPropertyNames(this.previewColors).length === 1) return
try { try {
this.updatePreviewColorsAndShadows() this.updatePreviewShadows()
this.shadowsInvalid = false this.shadowsInvalid = false
} catch (e) { } catch (e) {
this.shadowsInvalid = true this.shadowsInvalid = true
@ -727,7 +737,7 @@ export default {
fontsLocal: { fontsLocal: {
handler () { handler () {
try { try {
this.previewFonts = generateFonts({ fonts: this.fontsLocal }) this.previewTheme.fonts = generateFonts({ fonts: this.fontsLocal }).theme.fonts
this.fontsInvalid = false this.fontsInvalid = false
} catch (e) { } catch (e) {
this.fontsInvalid = true this.fontsInvalid = true
@ -738,18 +748,16 @@ export default {
}, },
currentColors () { currentColors () {
try { try {
this.updatePreviewColorsAndShadows() this.updatePreviewColors()
this.colorsInvalid = false this.colorsInvalid = false
this.shadowsInvalid = false
} catch (e) { } catch (e) {
this.colorsInvalid = true this.colorsInvalid = true
this.shadowsInvalid = true
console.warn(e) console.warn(e)
} }
}, },
currentOpacity () { currentOpacity () {
try { try {
this.updatePreviewColorsAndShadows() this.updatePreviewColors()
} catch (e) { } catch (e) {
console.warn(e) console.warn(e)
} }

@ -1,4 +1,9 @@
.theme-tab { .theme-tab {
.deprecation-warning {
padding: 0.5em;
margin: 2em;
}
padding-bottom: 2em; padding-bottom: 2em;
.preset-switcher { .preset-switcher {
@ -10,6 +15,10 @@
margin-right: 0.25em; margin-right: 0.25em;
} }
.btn-group .btn {
margin: 0;
}
.style-control { .style-control {
display: flex; display: flex;
align-items: baseline; align-items: baseline;
@ -157,107 +166,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 { .radius-item {
flex-basis: auto; flex-basis: auto;
} }
@ -310,10 +218,6 @@
max-width: 50em; max-width: 50em;
} }
.theme-preview-content {
padding: 20px;
}
.theme-warning { .theme-warning {
display: flex; display: flex;
align-items: baseline; align-items: baseline;

@ -1,5 +1,8 @@
<template> <template>
<div class="theme-tab"> <div class="theme-tab">
<div class="alert warning deprecation-warning">
{{ $t("settings.style.themes2_outdated") }}
</div>
<div class="presets-container"> <div class="presets-container">
<div class="save-load"> <div class="save-load">
<div <div
@ -120,7 +123,19 @@
</div> </div>
</div> </div>
<preview :style="previewRules" /> <!-- eslint-disable vue/no-v-text-v-html-on-component -->
<component :is="'style'" v-html="themeV3Preview"/>
<!-- eslint-enable vue/no-v-text-v-html-on-component -->
<preview id="theme-preview"/>
<div>
<button
class="btn button-default"
@click="updateTheme3Preview"
>
{{ $t("settings.style.update_preview") }}
</button>
</div>
<keep-alive> <keep-alive>
<tab-switcher key="style-tweak"> <tab-switcher key="style-tweak">
@ -156,7 +171,7 @@
<OpacityInput <OpacityInput
v-model="bgOpacityLocal" v-model="bgOpacityLocal"
name="bgOpacity" name="bgOpacity"
:fallback="previewTheme.opacity.bg" :fallback="previewTheme.opacity?.bg"
/> />
<ColorInput <ColorInput
v-model="textColorLocal" v-model="textColorLocal"
@ -167,14 +182,14 @@
<ColorInput <ColorInput
v-model="accentColorLocal" v-model="accentColorLocal"
name="accentColor" name="accentColor"
:fallback="previewTheme.colors.link" :fallback="previewTheme.colors?.link"
:label="$t('settings.accent')" :label="$t('settings.accent')"
:show-optional-tickbox="typeof linkColorLocal !== 'undefined'" :show-optional-tickbox="typeof linkColorLocal !== 'undefined'"
/> />
<ColorInput <ColorInput
v-model="linkColorLocal" v-model="linkColorLocal"
name="linkColor" name="linkColor"
:fallback="previewTheme.colors.accent" :fallback="previewTheme.colors?.accent"
:label="$t('settings.links')" :label="$t('settings.links')"
:show-optional-tickbox="typeof accentColorLocal !== 'undefined'" :show-optional-tickbox="typeof accentColorLocal !== 'undefined'"
/> />
@ -190,13 +205,13 @@
v-model="fgTextColorLocal" v-model="fgTextColorLocal"
name="fgTextColor" name="fgTextColor"
:label="$t('settings.text')" :label="$t('settings.text')"
:fallback="previewTheme.colors.fgText" :fallback="previewTheme.colors?.fgText"
/> />
<ColorInput <ColorInput
v-model="fgLinkColorLocal" v-model="fgLinkColorLocal"
name="fgLinkColor" name="fgLinkColor"
:label="$t('settings.links')" :label="$t('settings.links')"
:fallback="previewTheme.colors.fgLink" :fallback="previewTheme.colors?.fgLink"
/> />
<p>{{ $t('settings.style.common_colors.foreground_hint') }}</p> <p>{{ $t('settings.style.common_colors.foreground_hint') }}</p>
</div> </div>
@ -256,14 +271,14 @@
<ColorInput <ColorInput
v-model="postLinkColorLocal" v-model="postLinkColorLocal"
name="postLinkColor" name="postLinkColor"
:fallback="previewTheme.colors.accent" :fallback="previewTheme.colors?.accent"
:label="$t('settings.links')" :label="$t('settings.links')"
/> />
<ContrastRatio :contrast="previewContrast.postLink" /> <ContrastRatio :contrast="previewContrast.postLink" />
<ColorInput <ColorInput
v-model="postGreentextColorLocal" v-model="postGreentextColorLocal"
name="postGreentextColor" name="postGreentextColor"
:fallback="previewTheme.colors.cGreen" :fallback="previewTheme.colors?.cGreen"
:label="$t('settings.greentext')" :label="$t('settings.greentext')"
/> />
<ContrastRatio :contrast="previewContrast.postGreentext" /> <ContrastRatio :contrast="previewContrast.postGreentext" />
@ -272,13 +287,13 @@
v-model="alertErrorColorLocal" v-model="alertErrorColorLocal"
name="alertError" name="alertError"
:label="$t('settings.style.advanced_colors.alert_error')" :label="$t('settings.style.advanced_colors.alert_error')"
:fallback="previewTheme.colors.alertError" :fallback="previewTheme.colors?.alertError"
/> />
<ColorInput <ColorInput
v-model="alertErrorTextColorLocal" v-model="alertErrorTextColorLocal"
name="alertErrorText" name="alertErrorText"
:label="$t('settings.text')" :label="$t('settings.text')"
:fallback="previewTheme.colors.alertErrorText" :fallback="previewTheme.colors?.alertErrorText"
/> />
<ContrastRatio <ContrastRatio
:contrast="previewContrast.alertErrorText" :contrast="previewContrast.alertErrorText"
@ -288,13 +303,13 @@
v-model="alertWarningColorLocal" v-model="alertWarningColorLocal"
name="alertWarning" name="alertWarning"
:label="$t('settings.style.advanced_colors.alert_warning')" :label="$t('settings.style.advanced_colors.alert_warning')"
:fallback="previewTheme.colors.alertWarning" :fallback="previewTheme.colors?.alertWarning"
/> />
<ColorInput <ColorInput
v-model="alertWarningTextColorLocal" v-model="alertWarningTextColorLocal"
name="alertWarningText" name="alertWarningText"
:label="$t('settings.text')" :label="$t('settings.text')"
:fallback="previewTheme.colors.alertWarningText" :fallback="previewTheme.colors?.alertWarningText"
/> />
<ContrastRatio <ContrastRatio
:contrast="previewContrast.alertWarningText" :contrast="previewContrast.alertWarningText"
@ -304,13 +319,13 @@
v-model="alertNeutralColorLocal" v-model="alertNeutralColorLocal"
name="alertNeutral" name="alertNeutral"
:label="$t('settings.style.advanced_colors.alert_neutral')" :label="$t('settings.style.advanced_colors.alert_neutral')"
:fallback="previewTheme.colors.alertNeutral" :fallback="previewTheme.colors?.alertNeutral"
/> />
<ColorInput <ColorInput
v-model="alertNeutralTextColorLocal" v-model="alertNeutralTextColorLocal"
name="alertNeutralText" name="alertNeutralText"
:label="$t('settings.text')" :label="$t('settings.text')"
:fallback="previewTheme.colors.alertNeutralText" :fallback="previewTheme.colors?.alertNeutralText"
/> />
<ContrastRatio <ContrastRatio
:contrast="previewContrast.alertNeutralText" :contrast="previewContrast.alertNeutralText"
@ -319,7 +334,7 @@
<OpacityInput <OpacityInput
v-model="alertOpacityLocal" v-model="alertOpacityLocal"
name="alertOpacity" name="alertOpacity"
:fallback="previewTheme.opacity.alert" :fallback="previewTheme.opacity?.alert"
/> />
</div> </div>
<div class="color-item"> <div class="color-item">
@ -328,13 +343,13 @@
v-model="badgeNotificationColorLocal" v-model="badgeNotificationColorLocal"
name="badgeNotification" name="badgeNotification"
:label="$t('settings.style.advanced_colors.badge_notification')" :label="$t('settings.style.advanced_colors.badge_notification')"
:fallback="previewTheme.colors.badgeNotification" :fallback="previewTheme.colors?.badgeNotification"
/> />
<ColorInput <ColorInput
v-model="badgeNotificationTextColorLocal" v-model="badgeNotificationTextColorLocal"
name="badgeNotificationText" name="badgeNotificationText"
:label="$t('settings.text')" :label="$t('settings.text')"
:fallback="previewTheme.colors.badgeNotificationText" :fallback="previewTheme.colors?.badgeNotificationText"
/> />
<ContrastRatio <ContrastRatio
:contrast="previewContrast.badgeNotificationText" :contrast="previewContrast.badgeNotificationText"
@ -346,19 +361,19 @@
<ColorInput <ColorInput
v-model="panelColorLocal" v-model="panelColorLocal"
name="panelColor" name="panelColor"
:fallback="previewTheme.colors.panel" :fallback="previewTheme.colors?.panel"
:label="$t('settings.background')" :label="$t('settings.background')"
/> />
<OpacityInput <OpacityInput
v-model="panelOpacityLocal" v-model="panelOpacityLocal"
name="panelOpacity" name="panelOpacity"
:fallback="previewTheme.opacity.panel" :fallback="previewTheme.opacity?.panel"
:disabled="panelColorLocal === 'transparent'" :disabled="panelColorLocal === 'transparent'"
/> />
<ColorInput <ColorInput
v-model="panelTextColorLocal" v-model="panelTextColorLocal"
name="panelTextColor" name="panelTextColor"
:fallback="previewTheme.colors.panelText" :fallback="previewTheme.colors?.panelText"
:label="$t('settings.text')" :label="$t('settings.text')"
/> />
<ContrastRatio <ContrastRatio
@ -368,7 +383,7 @@
<ColorInput <ColorInput
v-model="panelLinkColorLocal" v-model="panelLinkColorLocal"
name="panelLinkColor" name="panelLinkColor"
:fallback="previewTheme.colors.panelLink" :fallback="previewTheme.colors?.panelLink"
:label="$t('settings.links')" :label="$t('settings.links')"
/> />
<ContrastRatio <ContrastRatio
@ -381,20 +396,20 @@
<ColorInput <ColorInput
v-model="topBarColorLocal" v-model="topBarColorLocal"
name="topBarColor" name="topBarColor"
:fallback="previewTheme.colors.topBar" :fallback="previewTheme.colors?.topBar"
:label="$t('settings.background')" :label="$t('settings.background')"
/> />
<ColorInput <ColorInput
v-model="topBarTextColorLocal" v-model="topBarTextColorLocal"
name="topBarTextColor" name="topBarTextColor"
:fallback="previewTheme.colors.topBarText" :fallback="previewTheme.colors?.topBarText"
:label="$t('settings.text')" :label="$t('settings.text')"
/> />
<ContrastRatio :contrast="previewContrast.topBarText" /> <ContrastRatio :contrast="previewContrast.topBarText" />
<ColorInput <ColorInput
v-model="topBarLinkColorLocal" v-model="topBarLinkColorLocal"
name="topBarLinkColor" name="topBarLinkColor"
:fallback="previewTheme.colors.topBarLink" :fallback="previewTheme.colors?.topBarLink"
:label="$t('settings.links')" :label="$t('settings.links')"
/> />
<ContrastRatio :contrast="previewContrast.topBarLink" /> <ContrastRatio :contrast="previewContrast.topBarLink" />
@ -404,19 +419,19 @@
<ColorInput <ColorInput
v-model="inputColorLocal" v-model="inputColorLocal"
name="inputColor" name="inputColor"
:fallback="previewTheme.colors.input" :fallback="previewTheme.colors?.input"
:label="$t('settings.background')" :label="$t('settings.background')"
/> />
<OpacityInput <OpacityInput
v-model="inputOpacityLocal" v-model="inputOpacityLocal"
name="inputOpacity" name="inputOpacity"
:fallback="previewTheme.opacity.input" :fallback="previewTheme.opacity?.input"
:disabled="inputColorLocal === 'transparent'" :disabled="inputColorLocal === 'transparent'"
/> />
<ColorInput <ColorInput
v-model="inputTextColorLocal" v-model="inputTextColorLocal"
name="inputTextColor" name="inputTextColor"
:fallback="previewTheme.colors.inputText" :fallback="previewTheme.colors?.inputText"
:label="$t('settings.text')" :label="$t('settings.text')"
/> />
<ContrastRatio :contrast="previewContrast.inputText" /> <ContrastRatio :contrast="previewContrast.inputText" />
@ -426,33 +441,33 @@
<ColorInput <ColorInput
v-model="btnColorLocal" v-model="btnColorLocal"
name="btnColor" name="btnColor"
:fallback="previewTheme.colors.btn" :fallback="previewTheme.colors?.btn"
:label="$t('settings.background')" :label="$t('settings.background')"
/> />
<OpacityInput <OpacityInput
v-model="btnOpacityLocal" v-model="btnOpacityLocal"
name="btnOpacity" name="btnOpacity"
:fallback="previewTheme.opacity.btn" :fallback="previewTheme.opacity?.btn"
:disabled="btnColorLocal === 'transparent'" :disabled="btnColorLocal === 'transparent'"
/> />
<ColorInput <ColorInput
v-model="btnTextColorLocal" v-model="btnTextColorLocal"
name="btnTextColor" name="btnTextColor"
:fallback="previewTheme.colors.btnText" :fallback="previewTheme.colors?.btnText"
:label="$t('settings.text')" :label="$t('settings.text')"
/> />
<ContrastRatio :contrast="previewContrast.btnText" /> <ContrastRatio :contrast="previewContrast.btnText" />
<ColorInput <ColorInput
v-model="btnPanelTextColorLocal" v-model="btnPanelTextColorLocal"
name="btnPanelTextColor" name="btnPanelTextColor"
:fallback="previewTheme.colors.btnPanelText" :fallback="previewTheme.colors?.btnPanelText"
:label="$t('settings.style.advanced_colors.panel_header')" :label="$t('settings.style.advanced_colors.panel_header')"
/> />
<ContrastRatio :contrast="previewContrast.btnPanelText" /> <ContrastRatio :contrast="previewContrast.btnPanelText" />
<ColorInput <ColorInput
v-model="btnTopBarTextColorLocal" v-model="btnTopBarTextColorLocal"
name="btnTopBarTextColor" name="btnTopBarTextColor"
:fallback="previewTheme.colors.btnTopBarText" :fallback="previewTheme.colors?.btnTopBarText"
:label="$t('settings.style.advanced_colors.top_bar')" :label="$t('settings.style.advanced_colors.top_bar')"
/> />
<ContrastRatio :contrast="previewContrast.btnTopBarText" /> <ContrastRatio :contrast="previewContrast.btnTopBarText" />
@ -460,27 +475,27 @@
<ColorInput <ColorInput
v-model="btnPressedColorLocal" v-model="btnPressedColorLocal"
name="btnPressedColor" name="btnPressedColor"
:fallback="previewTheme.colors.btnPressed" :fallback="previewTheme.colors?.btnPressed"
:label="$t('settings.background')" :label="$t('settings.background')"
/> />
<ColorInput <ColorInput
v-model="btnPressedTextColorLocal" v-model="btnPressedTextColorLocal"
name="btnPressedTextColor" name="btnPressedTextColor"
:fallback="previewTheme.colors.btnPressedText" :fallback="previewTheme.colors?.btnPressedText"
:label="$t('settings.text')" :label="$t('settings.text')"
/> />
<ContrastRatio :contrast="previewContrast.btnPressedText" /> <ContrastRatio :contrast="previewContrast.btnPressedText" />
<ColorInput <ColorInput
v-model="btnPressedPanelTextColorLocal" v-model="btnPressedPanelTextColorLocal"
name="btnPressedPanelTextColor" name="btnPressedPanelTextColor"
:fallback="previewTheme.colors.btnPressedPanelText" :fallback="previewTheme.colors?.btnPressedPanelText"
:label="$t('settings.style.advanced_colors.panel_header')" :label="$t('settings.style.advanced_colors.panel_header')"
/> />
<ContrastRatio :contrast="previewContrast.btnPressedPanelText" /> <ContrastRatio :contrast="previewContrast.btnPressedPanelText" />
<ColorInput <ColorInput
v-model="btnPressedTopBarTextColorLocal" v-model="btnPressedTopBarTextColorLocal"
name="btnPressedTopBarTextColor" name="btnPressedTopBarTextColor"
:fallback="previewTheme.colors.btnPressedTopBarText" :fallback="previewTheme.colors?.btnPressedTopBarText"
:label="$t('settings.style.advanced_colors.top_bar')" :label="$t('settings.style.advanced_colors.top_bar')"
/> />
<ContrastRatio :contrast="previewContrast.btnPressedTopBarText" /> <ContrastRatio :contrast="previewContrast.btnPressedTopBarText" />
@ -488,52 +503,52 @@
<ColorInput <ColorInput
v-model="btnDisabledColorLocal" v-model="btnDisabledColorLocal"
name="btnDisabledColor" name="btnDisabledColor"
:fallback="previewTheme.colors.btnDisabled" :fallback="previewTheme.colors?.btnDisabled"
:label="$t('settings.background')" :label="$t('settings.background')"
/> />
<ColorInput <ColorInput
v-model="btnDisabledTextColorLocal" v-model="btnDisabledTextColorLocal"
name="btnDisabledTextColor" name="btnDisabledTextColor"
:fallback="previewTheme.colors.btnDisabledText" :fallback="previewTheme.colors?.btnDisabledText"
:label="$t('settings.text')" :label="$t('settings.text')"
/> />
<ColorInput <ColorInput
v-model="btnDisabledPanelTextColorLocal" v-model="btnDisabledPanelTextColorLocal"
name="btnDisabledPanelTextColor" name="btnDisabledPanelTextColor"
:fallback="previewTheme.colors.btnDisabledPanelText" :fallback="previewTheme.colors?.btnDisabledPanelText"
:label="$t('settings.style.advanced_colors.panel_header')" :label="$t('settings.style.advanced_colors.panel_header')"
/> />
<ColorInput <ColorInput
v-model="btnDisabledTopBarTextColorLocal" v-model="btnDisabledTopBarTextColorLocal"
name="btnDisabledTopBarTextColor" name="btnDisabledTopBarTextColor"
:fallback="previewTheme.colors.btnDisabledTopBarText" :fallback="previewTheme.colors?.btnDisabledTopBarText"
:label="$t('settings.style.advanced_colors.top_bar')" :label="$t('settings.style.advanced_colors.top_bar')"
/> />
<h5>{{ $t('settings.style.advanced_colors.toggled') }}</h5> <h5>{{ $t('settings.style.advanced_colors.toggled') }}</h5>
<ColorInput <ColorInput
v-model="btnToggledColorLocal" v-model="btnToggledColorLocal"
name="btnToggledColor" name="btnToggledColor"
:fallback="previewTheme.colors.btnToggled" :fallback="previewTheme.colors?.btnToggled"
:label="$t('settings.background')" :label="$t('settings.background')"
/> />
<ColorInput <ColorInput
v-model="btnToggledTextColorLocal" v-model="btnToggledTextColorLocal"
name="btnToggledTextColor" name="btnToggledTextColor"
:fallback="previewTheme.colors.btnToggledText" :fallback="previewTheme.colors?.btnToggledText"
:label="$t('settings.text')" :label="$t('settings.text')"
/> />
<ContrastRatio :contrast="previewContrast.btnToggledText" /> <ContrastRatio :contrast="previewContrast.btnToggledText" />
<ColorInput <ColorInput
v-model="btnToggledPanelTextColorLocal" v-model="btnToggledPanelTextColorLocal"
name="btnToggledPanelTextColor" name="btnToggledPanelTextColor"
:fallback="previewTheme.colors.btnToggledPanelText" :fallback="previewTheme.colors?.btnToggledPanelText"
:label="$t('settings.style.advanced_colors.panel_header')" :label="$t('settings.style.advanced_colors.panel_header')"
/> />
<ContrastRatio :contrast="previewContrast.btnToggledPanelText" /> <ContrastRatio :contrast="previewContrast.btnToggledPanelText" />
<ColorInput <ColorInput
v-model="btnToggledTopBarTextColorLocal" v-model="btnToggledTopBarTextColorLocal"
name="btnToggledTopBarTextColor" name="btnToggledTopBarTextColor"
:fallback="previewTheme.colors.btnToggledTopBarText" :fallback="previewTheme.colors?.btnToggledTopBarText"
:label="$t('settings.style.advanced_colors.top_bar')" :label="$t('settings.style.advanced_colors.top_bar')"
/> />
<ContrastRatio :contrast="previewContrast.btnToggledTopBarText" /> <ContrastRatio :contrast="previewContrast.btnToggledTopBarText" />
@ -543,20 +558,20 @@
<ColorInput <ColorInput
v-model="tabColorLocal" v-model="tabColorLocal"
name="tabColor" name="tabColor"
:fallback="previewTheme.colors.tab" :fallback="previewTheme.colors?.tab"
:label="$t('settings.background')" :label="$t('settings.background')"
/> />
<ColorInput <ColorInput
v-model="tabTextColorLocal" v-model="tabTextColorLocal"
name="tabTextColor" name="tabTextColor"
:fallback="previewTheme.colors.tabText" :fallback="previewTheme.colors?.tabText"
:label="$t('settings.text')" :label="$t('settings.text')"
/> />
<ContrastRatio :contrast="previewContrast.tabText" /> <ContrastRatio :contrast="previewContrast.tabText" />
<ColorInput <ColorInput
v-model="tabActiveTextColorLocal" v-model="tabActiveTextColorLocal"
name="tabActiveTextColor" name="tabActiveTextColor"
:fallback="previewTheme.colors.tabActiveText" :fallback="previewTheme.colors?.tabActiveText"
:label="$t('settings.text')" :label="$t('settings.text')"
/> />
<ContrastRatio :contrast="previewContrast.tabActiveText" /> <ContrastRatio :contrast="previewContrast.tabActiveText" />
@ -566,13 +581,13 @@
<ColorInput <ColorInput
v-model="borderColorLocal" v-model="borderColorLocal"
name="borderColor" name="borderColor"
:fallback="previewTheme.colors.border" :fallback="previewTheme.colors?.border"
:label="$t('settings.style.common.color')" :label="$t('settings.style.common.color')"
/> />
<OpacityInput <OpacityInput
v-model="borderOpacityLocal" v-model="borderOpacityLocal"
name="borderOpacity" name="borderOpacity"
:fallback="previewTheme.opacity.border" :fallback="previewTheme.opacity?.border"
:disabled="borderColorLocal === 'transparent'" :disabled="borderColorLocal === 'transparent'"
/> />
</div> </div>
@ -581,25 +596,25 @@
<ColorInput <ColorInput
v-model="faintColorLocal" v-model="faintColorLocal"
name="faintColor" name="faintColor"
:fallback="previewTheme.colors.faint" :fallback="previewTheme.colors?.faint"
:label="$t('settings.text')" :label="$t('settings.text')"
/> />
<ColorInput <ColorInput
v-model="faintLinkColorLocal" v-model="faintLinkColorLocal"
name="faintLinkColor" name="faintLinkColor"
:fallback="previewTheme.colors.faintLink" :fallback="previewTheme.colors?.faintLink"
:label="$t('settings.links')" :label="$t('settings.links')"
/> />
<ColorInput <ColorInput
v-model="panelFaintColorLocal" v-model="panelFaintColorLocal"
name="panelFaintColor" name="panelFaintColor"
:fallback="previewTheme.colors.panelFaint" :fallback="previewTheme.colors?.panelFaint"
:label="$t('settings.style.advanced_colors.panel_header')" :label="$t('settings.style.advanced_colors.panel_header')"
/> />
<OpacityInput <OpacityInput
v-model="faintOpacityLocal" v-model="faintOpacityLocal"
name="faintOpacity" name="faintOpacity"
:fallback="previewTheme.opacity.faint" :fallback="previewTheme.opacity?.faint"
/> />
</div> </div>
<div class="color-item"> <div class="color-item">
@ -608,12 +623,12 @@
v-model="underlayColorLocal" v-model="underlayColorLocal"
name="underlay" name="underlay"
:label="$t('settings.style.advanced_colors.underlay')" :label="$t('settings.style.advanced_colors.underlay')"
:fallback="previewTheme.colors.underlay" :fallback="previewTheme.colors?.underlay"
/> />
<OpacityInput <OpacityInput
v-model="underlayOpacityLocal" v-model="underlayOpacityLocal"
name="underlayOpacity" name="underlayOpacity"
:fallback="previewTheme.opacity.underlay" :fallback="previewTheme.opacity?.underlay"
:disabled="underlayOpacityLocal === 'transparent'" :disabled="underlayOpacityLocal === 'transparent'"
/> />
</div> </div>
@ -623,7 +638,7 @@
v-model="wallpaperColorLocal" v-model="wallpaperColorLocal"
name="wallpaper" name="wallpaper"
:label="$t('settings.style.advanced_colors.wallpaper')" :label="$t('settings.style.advanced_colors.wallpaper')"
:fallback="previewTheme.colors.wallpaper" :fallback="previewTheme.colors?.wallpaper"
/> />
</div> </div>
<div class="color-item"> <div class="color-item">
@ -632,13 +647,13 @@
v-model="pollColorLocal" v-model="pollColorLocal"
name="poll" name="poll"
:label="$t('settings.background')" :label="$t('settings.background')"
:fallback="previewTheme.colors.poll" :fallback="previewTheme.colors?.poll"
/> />
<ColorInput <ColorInput
v-model="pollTextColorLocal" v-model="pollTextColorLocal"
name="pollText" name="pollText"
:label="$t('settings.text')" :label="$t('settings.text')"
:fallback="previewTheme.colors.pollText" :fallback="previewTheme.colors?.pollText"
/> />
</div> </div>
<div class="color-item"> <div class="color-item">
@ -647,7 +662,7 @@
v-model="iconColorLocal" v-model="iconColorLocal"
name="icon" name="icon"
:label="$t('settings.style.advanced_colors.icons')" :label="$t('settings.style.advanced_colors.icons')"
:fallback="previewTheme.colors.icon" :fallback="previewTheme.colors?.icon"
/> />
</div> </div>
<div class="color-item"> <div class="color-item">
@ -656,20 +671,20 @@
v-model="highlightColorLocal" v-model="highlightColorLocal"
name="highlight" name="highlight"
:label="$t('settings.background')" :label="$t('settings.background')"
:fallback="previewTheme.colors.highlight" :fallback="previewTheme.colors?.highlight"
/> />
<ColorInput <ColorInput
v-model="highlightTextColorLocal" v-model="highlightTextColorLocal"
name="highlightText" name="highlightText"
:label="$t('settings.text')" :label="$t('settings.text')"
:fallback="previewTheme.colors.highlightText" :fallback="previewTheme.colors?.highlightText"
/> />
<ContrastRatio :contrast="previewContrast.highlightText" /> <ContrastRatio :contrast="previewContrast.highlightText" />
<ColorInput <ColorInput
v-model="highlightLinkColorLocal" v-model="highlightLinkColorLocal"
name="highlightLink" name="highlightLink"
:label="$t('settings.links')" :label="$t('settings.links')"
:fallback="previewTheme.colors.highlightLink" :fallback="previewTheme.colors?.highlightLink"
/> />
<ContrastRatio :contrast="previewContrast.highlightLink" /> <ContrastRatio :contrast="previewContrast.highlightLink" />
</div> </div>
@ -679,26 +694,26 @@
v-model="popoverColorLocal" v-model="popoverColorLocal"
name="popover" name="popover"
:label="$t('settings.background')" :label="$t('settings.background')"
:fallback="previewTheme.colors.popover" :fallback="previewTheme.colors?.popover"
/> />
<OpacityInput <OpacityInput
v-model="popoverOpacityLocal" v-model="popoverOpacityLocal"
name="popoverOpacity" name="popoverOpacity"
:fallback="previewTheme.opacity.popover" :fallback="previewTheme.opacity?.popover"
:disabled="popoverOpacityLocal === 'transparent'" :disabled="popoverOpacityLocal === 'transparent'"
/> />
<ColorInput <ColorInput
v-model="popoverTextColorLocal" v-model="popoverTextColorLocal"
name="popoverText" name="popoverText"
:label="$t('settings.text')" :label="$t('settings.text')"
:fallback="previewTheme.colors.popoverText" :fallback="previewTheme.colors?.popoverText"
/> />
<ContrastRatio :contrast="previewContrast.popoverText" /> <ContrastRatio :contrast="previewContrast.popoverText" />
<ColorInput <ColorInput
v-model="popoverLinkColorLocal" v-model="popoverLinkColorLocal"
name="popoverLink" name="popoverLink"
:label="$t('settings.links')" :label="$t('settings.links')"
:fallback="previewTheme.colors.popoverLink" :fallback="previewTheme.colors?.popoverLink"
/> />
<ContrastRatio :contrast="previewContrast.popoverLink" /> <ContrastRatio :contrast="previewContrast.popoverLink" />
</div> </div>
@ -708,20 +723,20 @@
v-model="selectedPostColorLocal" v-model="selectedPostColorLocal"
name="selectedPost" name="selectedPost"
:label="$t('settings.background')" :label="$t('settings.background')"
:fallback="previewTheme.colors.selectedPost" :fallback="previewTheme.colors?.selectedPost"
/> />
<ColorInput <ColorInput
v-model="selectedPostTextColorLocal" v-model="selectedPostTextColorLocal"
name="selectedPostText" name="selectedPostText"
:label="$t('settings.text')" :label="$t('settings.text')"
:fallback="previewTheme.colors.selectedPostText" :fallback="previewTheme.colors?.selectedPostText"
/> />
<ContrastRatio :contrast="previewContrast.selectedPostText" /> <ContrastRatio :contrast="previewContrast.selectedPostText" />
<ColorInput <ColorInput
v-model="selectedPostLinkColorLocal" v-model="selectedPostLinkColorLocal"
name="selectedPostLink" name="selectedPostLink"
:label="$t('settings.links')" :label="$t('settings.links')"
:fallback="previewTheme.colors.selectedPostLink" :fallback="previewTheme.colors?.selectedPostLink"
/> />
<ContrastRatio :contrast="previewContrast.selectedPostLink" /> <ContrastRatio :contrast="previewContrast.selectedPostLink" />
</div> </div>
@ -731,20 +746,20 @@
v-model="selectedMenuColorLocal" v-model="selectedMenuColorLocal"
name="selectedMenu" name="selectedMenu"
:label="$t('settings.background')" :label="$t('settings.background')"
:fallback="previewTheme.colors.selectedMenu" :fallback="previewTheme.colors?.selectedMenu"
/> />
<ColorInput <ColorInput
v-model="selectedMenuTextColorLocal" v-model="selectedMenuTextColorLocal"
name="selectedMenuText" name="selectedMenuText"
:label="$t('settings.text')" :label="$t('settings.text')"
:fallback="previewTheme.colors.selectedMenuText" :fallback="previewTheme.colors?.selectedMenuText"
/> />
<ContrastRatio :contrast="previewContrast.selectedMenuText" /> <ContrastRatio :contrast="previewContrast.selectedMenuText" />
<ColorInput <ColorInput
v-model="selectedMenuLinkColorLocal" v-model="selectedMenuLinkColorLocal"
name="selectedMenuLink" name="selectedMenuLink"
:label="$t('settings.links')" :label="$t('settings.links')"
:fallback="previewTheme.colors.selectedMenuLink" :fallback="previewTheme.colors?.selectedMenuLink"
/> />
<ContrastRatio :contrast="previewContrast.selectedMenuLink" /> <ContrastRatio :contrast="previewContrast.selectedMenuLink" />
</div> </div>
@ -753,57 +768,57 @@
<ColorInput <ColorInput
v-model="chatBgColorLocal" v-model="chatBgColorLocal"
name="chatBgColor" name="chatBgColor"
:fallback="previewTheme.colors.bg" :fallback="previewTheme.colors?.bg"
:label="$t('settings.background')" :label="$t('settings.background')"
/> />
<h5>{{ $t('settings.style.advanced_colors.chat.incoming') }}</h5> <h5>{{ $t('settings.style.advanced_colors.chat.incoming') }}</h5>
<ColorInput <ColorInput
v-model="chatMessageIncomingBgColorLocal" v-model="chatMessageIncomingBgColorLocal"
name="chatMessageIncomingBgColor" name="chatMessageIncomingBgColor"
:fallback="previewTheme.colors.bg" :fallback="previewTheme.colors?.bg"
:label="$t('settings.background')" :label="$t('settings.background')"
/> />
<ColorInput <ColorInput
v-model="chatMessageIncomingTextColorLocal" v-model="chatMessageIncomingTextColorLocal"
name="chatMessageIncomingTextColor" name="chatMessageIncomingTextColor"
:fallback="previewTheme.colors.text" :fallback="previewTheme.colors?.text"
:label="$t('settings.text')" :label="$t('settings.text')"
/> />
<ColorInput <ColorInput
v-model="chatMessageIncomingLinkColorLocal" v-model="chatMessageIncomingLinkColorLocal"
name="chatMessageIncomingLinkColor" name="chatMessageIncomingLinkColor"
:fallback="previewTheme.colors.link" :fallback="previewTheme.colors?.link"
:label="$t('settings.links')" :label="$t('settings.links')"
/> />
<ColorInput <ColorInput
v-model="chatMessageIncomingBorderColorLocal" v-model="chatMessageIncomingBorderColorLocal"
name="chatMessageIncomingBorderLinkColor" name="chatMessageIncomingBorderLinkColor"
:fallback="previewTheme.colors.fg" :fallback="previewTheme.colors?.fg"
:label="$t('settings.style.advanced_colors.chat.border')" :label="$t('settings.style.advanced_colors.chat.border')"
/> />
<h5>{{ $t('settings.style.advanced_colors.chat.outgoing') }}</h5> <h5>{{ $t('settings.style.advanced_colors.chat.outgoing') }}</h5>
<ColorInput <ColorInput
v-model="chatMessageOutgoingBgColorLocal" v-model="chatMessageOutgoingBgColorLocal"
name="chatMessageOutgoingBgColor" name="chatMessageOutgoingBgColor"
:fallback="previewTheme.colors.bg" :fallback="previewTheme.colors?.bg"
:label="$t('settings.background')" :label="$t('settings.background')"
/> />
<ColorInput <ColorInput
v-model="chatMessageOutgoingTextColorLocal" v-model="chatMessageOutgoingTextColorLocal"
name="chatMessageOutgoingTextColor" name="chatMessageOutgoingTextColor"
:fallback="previewTheme.colors.text" :fallback="previewTheme.colors?.text"
:label="$t('settings.text')" :label="$t('settings.text')"
/> />
<ColorInput <ColorInput
v-model="chatMessageOutgoingLinkColorLocal" v-model="chatMessageOutgoingLinkColorLocal"
name="chatMessageOutgoingLinkColor" name="chatMessageOutgoingLinkColor"
:fallback="previewTheme.colors.link" :fallback="previewTheme.colors?.link"
:label="$t('settings.links')" :label="$t('settings.links')"
/> />
<ColorInput <ColorInput
v-model="chatMessageOutgoingBorderColorLocal" v-model="chatMessageOutgoingBorderColorLocal"
name="chatMessageOutgoingBorderLinkColor" name="chatMessageOutgoingBorderLinkColor"
:fallback="previewTheme.colors.bg" :fallback="previewTheme.colors?.bg"
:label="$t('settings.style.advanced_colors.chat.border')" :label="$t('settings.style.advanced_colors.chat.border')"
/> />
</div> </div>
@ -826,7 +841,7 @@
v-model="btnRadiusLocal" v-model="btnRadiusLocal"
name="btnRadius" name="btnRadius"
:label="$t('settings.btnRadius')" :label="$t('settings.btnRadius')"
:fallback="previewTheme.radii.btn" :fallback="previewTheme.radii?.btn"
max="16" max="16"
hard-min="0" hard-min="0"
/> />
@ -834,7 +849,7 @@
v-model="inputRadiusLocal" v-model="inputRadiusLocal"
name="inputRadius" name="inputRadius"
:label="$t('settings.inputRadius')" :label="$t('settings.inputRadius')"
:fallback="previewTheme.radii.input" :fallback="previewTheme.radii?.input"
max="9" max="9"
hard-min="0" hard-min="0"
/> />
@ -842,7 +857,7 @@
v-model="checkboxRadiusLocal" v-model="checkboxRadiusLocal"
name="checkboxRadius" name="checkboxRadius"
:label="$t('settings.checkboxRadius')" :label="$t('settings.checkboxRadius')"
:fallback="previewTheme.radii.checkbox" :fallback="previewTheme.radii?.checkbox"
max="16" max="16"
hard-min="0" hard-min="0"
/> />
@ -850,7 +865,7 @@
v-model="panelRadiusLocal" v-model="panelRadiusLocal"
name="panelRadius" name="panelRadius"
:label="$t('settings.panelRadius')" :label="$t('settings.panelRadius')"
:fallback="previewTheme.radii.panel" :fallback="previewTheme.radii?.panel"
max="50" max="50"
hard-min="0" hard-min="0"
/> />
@ -858,7 +873,7 @@
v-model="avatarRadiusLocal" v-model="avatarRadiusLocal"
name="avatarRadius" name="avatarRadius"
:label="$t('settings.avatarRadius')" :label="$t('settings.avatarRadius')"
:fallback="previewTheme.radii.avatar" :fallback="previewTheme.radii?.avatar"
max="28" max="28"
hard-min="0" hard-min="0"
/> />
@ -866,7 +881,7 @@
v-model="avatarAltRadiusLocal" v-model="avatarAltRadiusLocal"
name="avatarAltRadius" name="avatarAltRadius"
:label="$t('settings.avatarAltRadius')" :label="$t('settings.avatarAltRadius')"
:fallback="previewTheme.radii.avatarAlt" :fallback="previewTheme.radii?.avatarAlt"
max="28" max="28"
hard-min="0" hard-min="0"
/> />
@ -874,7 +889,7 @@
v-model="attachmentRadiusLocal" v-model="attachmentRadiusLocal"
name="attachmentRadius" name="attachmentRadius"
:label="$t('settings.attachmentRadius')" :label="$t('settings.attachmentRadius')"
:fallback="previewTheme.radii.attachment" :fallback="previewTheme.radii?.attachment"
max="50" max="50"
hard-min="0" hard-min="0"
/> />
@ -882,7 +897,7 @@
v-model="tooltipRadiusLocal" v-model="tooltipRadiusLocal"
name="tooltipRadius" name="tooltipRadius"
:label="$t('settings.tooltipRadius')" :label="$t('settings.tooltipRadius')"
:fallback="previewTheme.radii.tooltip" :fallback="previewTheme.radii?.tooltip"
max="50" max="50"
hard-min="0" hard-min="0"
/> />
@ -890,7 +905,7 @@
v-model="chatMessageRadiusLocal" v-model="chatMessageRadiusLocal"
name="chatMessageRadius" name="chatMessageRadius"
:label="$t('settings.chatMessageRadius')" :label="$t('settings.chatMessageRadius')"
:fallback="previewTheme.radii.chatMessage || 2" :fallback="previewTheme.radii?.chatMessage || 2"
max="50" max="50"
hard-min="0" hard-min="0"
/> />
@ -996,26 +1011,26 @@
v-model="fontsLocal.interface" v-model="fontsLocal.interface"
name="ui" name="ui"
:label="$t('settings.style.fonts.components.interface')" :label="$t('settings.style.fonts.components.interface')"
:fallback="previewTheme.fonts.interface" :fallback="previewTheme.fonts?.interface"
no-inherit="1" no-inherit="1"
/> />
<FontControl <FontControl
v-model="fontsLocal.input" v-model="fontsLocal.input"
name="input" name="input"
:label="$t('settings.style.fonts.components.input')" :label="$t('settings.style.fonts.components.input')"
:fallback="previewTheme.fonts.input" :fallback="previewTheme.fonts?.input"
/> />
<FontControl <FontControl
v-model="fontsLocal.post" v-model="fontsLocal.post"
name="post" name="post"
:label="$t('settings.style.fonts.components.post')" :label="$t('settings.style.fonts.components.post')"
:fallback="previewTheme.fonts.post" :fallback="previewTheme.fonts?.post"
/> />
<FontControl <FontControl
v-model="fontsLocal.postCode" v-model="fontsLocal.postCode"
name="postCode" name="postCode"
:label="$t('settings.style.fonts.components.postCode')" :label="$t('settings.style.fonts.components.postCode')"
:fallback="previewTheme.fonts.postCode" :fallback="previewTheme.fonts?.postCode"
/> />
</div> </div>
</tab-switcher> </tab-switcher>

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

@ -376,6 +376,20 @@
"enter_current_password_to_confirm": "Enter your current password to confirm your identity", "enter_current_password_to_confirm": "Enter your current password to confirm your identity",
"post_look_feel": "Posts Look & Feel", "post_look_feel": "Posts Look & Feel",
"mention_links": "Mention links", "mention_links": "Mention links",
"appearance": "Appearance",
"confirm_new_setting": "Confirm new setting?",
"confirm_new_question": "Does this look ok? Setting will be reverted in 10 seconds.",
"revert": "Revert",
"confirm": "Confirm",
"text_size": "Text and interface size",
"text_size_tip": "Use {0} for absolute values, {1} will scale with browser default text size.",
"text_size_tip2": "Values other than {0} might break some things and themes",
"emoji_size": "Emoji size",
"navbar_size": "Top bar size",
"panel_header_size": "Panel header size",
"visual_tweaks": "Minor visual tweaks",
"theme_debug": "Show what background theme engine assumes when dealing with transparancy (DEBUG)",
"scale_and_layout": "Interface scale and layout",
"mfa": { "mfa": {
"otp": "OTP", "otp": "OTP",
"setup_otp": "Setup OTP", "setup_otp": "Setup OTP",
@ -729,6 +743,42 @@
"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.", "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", "more_settings": "More settings",
"style": { "style": {
"custom_theme_used": "(Custom theme)",
"themes2_outdated": "Editor for Themes V2 is being phased out and will eventually be replaced with a new one that takes advantage of new Themes V3 engine. It should still work but experience might be degraded and inconsistent.",
"appearance_tab_note": "Changes on this tab do not affect the theme used, so exported theme will be different from what seen in the UI",
"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" : {
"serif": "Serif",
"sans-serif": "Sans-serif",
"monospace": "Monospace",
"inherit": "Unchanged"
},
"group-local": "Locally installed fonts",
"local-unavailable1": "List of locally installed fonts unavailalbe",
"local-unavailable2": "Use manual entry to specify custom font",
"font_list_unavailable": "Couldn't get locally installed fonts: {error}",
"lookup_local_fonts": "Load list of fonts installed on this computer",
"enter_manually": "Enter font name family manually",
"entry": "Enter {fontFamily}",
"select": "Select font"
}
},
"interface_font_user_override": "Override theme/browser font used",
"switcher": { "switcher": {
"keep_color": "Keep colors", "keep_color": "Keep colors",
"keep_shadows": "Keep shadows", "keep_shadows": "Keep shadows",
@ -852,7 +902,7 @@
"interface": "Interface", "interface": "Interface",
"input": "Input fields", "input": "Input fields",
"post": "Post text", "post": "Post text",
"postCode": "Monospaced text in a post (rich text)" "monospace": "Monospaced text"
}, },
"family": "Font name", "family": "Font name",
"size": "Size (in px)", "size": "Size (in px)",

@ -1,10 +1,21 @@
import Cookies from 'js-cookie' 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 messages from '../i18n/messages'
import { set } from 'lodash' import { set } from 'lodash'
import localeService from '../services/locale/locale.service.js' import localeService from '../services/locale/locale.service.js'
const BACKEND_LANGUAGE_COOKIE_NAME = 'userLanguage' 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] const browserLocale = (window.navigator.language || 'en').split('-')[0]
@ -24,11 +35,30 @@ export const multiChoiceProperties = [
export const defaultState = { export const defaultState = {
expertLevel: 0, // used to track which settings to show and hide expertLevel: 0, // used to track which settings to show and hide
colors: {},
theme: undefined, // Theme stuff
customTheme: undefined, theme: undefined, // Very old theme store, stores preset name, still in use
customThemeSource: undefined,
forceThemeRecompilation: false, // 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, hideISP: false,
hideInstanceWallpaper: false, hideInstanceWallpaper: false,
hideShoutbox: false, hideShoutbox: false,
@ -117,7 +147,12 @@ export const defaultState = {
sidebarColumnWidth: '25rem', sidebarColumnWidth: '25rem',
contentColumnWidth: '45rem', contentColumnWidth: '45rem',
notifsColumnWidth: '25rem', 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, navbarColumnStretch: false,
greentext: undefined, // instance default greentext: undefined, // instance default
useAtIcon: undefined, // instance default useAtIcon: undefined, // instance default
@ -175,6 +210,10 @@ const config = {
} }
}, },
mutations: { mutations: {
setOptionTemporarily (state, { name, value }) {
set(state, name, value)
applyConfig(state)
},
setOption (state, { name, value }) { setOption (state, { name, value }) {
set(state, name, value) set(state, name, value)
}, },
@ -205,6 +244,37 @@ const config = {
setHighlight ({ commit, dispatch }, { user, color, type }) { setHighlight ({ commit, dispatch }, { user, color, type }) {
commit('setHighlight', { 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 }) { setOption ({ commit, dispatch, state }, { name, value }) {
const exceptions = new Set([ const exceptions = new Set([
'useStreamingApi' 'useStreamingApi'
@ -222,24 +292,26 @@ const config = {
dispatch('disableMastoSockets') dispatch('disableMastoSockets')
dispatch('setOption', { name: 'useStreamingApi', value: false }) dispatch('setOption', { name: 'useStreamingApi', value: false })
}) })
break
} }
} }
} else { } else {
commit('setOption', { name, value }) commit('setOption', { name, value })
if (APPEARANCE_SETTINGS_KEYS.has(name)) {
applyConfig(state)
}
if (name.startsWith('theme3hacks')) {
dispatch('setTheme', { recompile: true })
}
switch (name) { switch (name) {
case 'theme': case 'theme':
setPreset(value) if (value === 'custom') break
dispatch('setTheme', { themeName: value, recompile: true, saveData: true })
break break
case 'sidebarColumnWidth': case 'themeDebug': {
case 'contentColumnWidth': dispatch('setTheme', { recompile: true })
case 'notifsColumnWidth':
case 'emojiReactionsScale':
applyConfig(state)
break
case 'customTheme':
case 'customThemeSource':
applyTheme(value)
break break
}
case 'interfaceLanguage': case 'interfaceLanguage':
messages.setLanguage(this.getters.i18n, value) messages.setLanguage(this.getters.i18n, value)
dispatch('loadUnicodeEmojiData', 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 apiService from '../services/api/api.service.js'
import { instanceDefaultProperties } from './config.js' import { instanceDefaultProperties } from './config.js'
import { langCodeToCldrName, ensureFinalFallback } from '../i18n/languages.js' import { langCodeToCldrName, ensureFinalFallback } from '../i18n/languages.js'
@ -44,7 +42,7 @@ const defaultState = {
registrationOpen: true, registrationOpen: true,
server: 'http://localhost:4040/', server: 'http://localhost:4040/',
textlimit: 5000, textlimit: 5000,
themeData: undefined, themeData: undefined, // used for theme editor v2
vapidPublicKey: undefined, vapidPublicKey: undefined,
// Stuff from static/config.json // Stuff from static/config.json
@ -98,6 +96,13 @@ const defaultState = {
sidebarRight: false, sidebarRight: false,
subjectLineBehavior: 'email', subjectLineBehavior: 'email',
theme: 'pleroma-dark', theme: 'pleroma-dark',
emojiReactionsScale: 0.5,
textSize: '14px',
emojiSize: '2.2rem',
navbarSize: '3.5rem',
panelHeaderSize: '3.2rem',
forcedRoundness: -1,
fontsOverride: {},
virtualScrolling: true, virtualScrolling: true,
sensitiveByDefault: false, sensitiveByDefault: false,
conversationDisplay: 'linear', conversationDisplay: 'linear',
@ -279,9 +284,6 @@ const instance = {
dispatch('initializeSocket') dispatch('initializeSocket')
} }
break break
case 'theme':
dispatch('setTheme', value)
break
} }
}, },
async getStaticEmoji ({ commit }) { async getStaticEmoji ({ commit }) {
@ -370,27 +372,6 @@ const instance = {
console.warn(e) 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 }) { fetchEmoji ({ dispatch, state }) {
if (!state.customEmojiFetched) { if (!state.customEmojiFetched) {
state.customEmojiFetched = true 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 = { const defaultState = {
localFonts: null,
themeApplied: false, 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', settingsModalState: 'hidden',
settingsModalLoadedUser: false, settingsModalLoadedUser: false,
settingsModalLoadedAdmin: false, settingsModalLoadedAdmin: false,
@ -14,7 +22,8 @@ const defaultState = {
cssFilter: window.CSS && window.CSS.supports && ( cssFilter: window.CSS && window.CSS.supports && (
window.CSS.supports('filter', 'drop-shadow(0 0)') || window.CSS.supports('filter', 'drop-shadow(0 0)') ||
window.CSS.supports('-webkit-filter', 'drop-shadow(0 0)') window.CSS.supports('-webkit-filter', 'drop-shadow(0 0)')
) ),
localFonts: typeof window.queryLocalFonts === 'function'
}, },
layoutType: 'normal', layoutType: 'normal',
globalNotices: [], globalNotices: [],
@ -36,6 +45,17 @@ const interfaceMod = {
state.settings.currentSaveStateNotice = { error: true, errorData: error } 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) { setThemeApplied (state) {
state.themeApplied = true state.themeApplied = true
}, },
@ -90,6 +110,10 @@ const interfaceMod = {
}, },
setLastTimeline (state, value) { setLastTimeline (state, value) {
state.lastTimeline = 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: { actions: {
@ -164,10 +188,203 @@ const interfaceMod = {
commit('setLayoutType', wideLayout ? 'wide' : normalOrMobile) 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) { setLastTimeline ({ commit }, value) {
commit('setLastTimeline', 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 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
}

@ -60,11 +60,12 @@
.panel-heading, .panel-heading,
.panel-footer { .panel-footer {
--panel-heading-height-padding: 0.6em; --panel-heading-height-padding: calc(var(--panel-header-height) * 0.2);
--__panel-heading-gap: 0.5em; --__panel-heading-gap: calc(var(--panel-header-height) * 0.1565);
--__panel-heading-height: 3.2em; --__panel-heading-height: var(--panel-header-height);
--__panel-heading-height-inner: calc(var(--__panel-heading-height) - 2 * var(--panel-heading-height-padding, 0)); --__panel-heading-height-inner: calc(var(--__panel-heading-height) - 2 * var(--panel-heading-height-padding, 0));
font-size: calc(var(--panelHeaderSize) / 3.2);
backdrop-filter: var(--__panel-backdrop-filter); backdrop-filter: var(--__panel-backdrop-filter);
position: relative; position: relative;
box-sizing: border-box; box-sizing: border-box;

@ -1,7 +1,5 @@
import { hex2rgb } from '../color_convert/color_convert.js' import { hex2rgb } from '../color_convert/color_convert.js'
import { generatePreset } from '../theme_data/theme_data.service.js'
import { init, getEngineChecksum } from '../theme_data/theme_data_3.service.js' import { init, getEngineChecksum } from '../theme_data/theme_data_3.service.js'
import { convertTheme2To3 } from '../theme_data/theme2_to_theme3.js'
import { getCssRules } from '../theme_data/css_utils.js' import { getCssRules } from '../theme_data/css_utils.js'
import { defaultState } from '../../modules/config.js' import { defaultState } from '../../modules/config.js'
import { chunk } from 'lodash' import { chunk } from 'lodash'
@ -45,25 +43,21 @@ const adoptStyleSheets = (styles) => {
// is nothing to do here. // is nothing to do here.
} }
export const generateTheme = async (input, callbacks) => { export const generateTheme = async (inputRuleset, callbacks, debug) => {
const { const {
onNewRule = (rule, isLazy) => {}, onNewRule = (rule, isLazy) => {},
onLazyFinished = () => {}, onLazyFinished = () => {},
onEagerFinished = () => {} onEagerFinished = () => {}
} = callbacks } = callbacks
let extraRules
if (input.themeFileVersion === 1) {
extraRules = convertTheme2To3(input)
} else {
const { theme } = generatePreset(input)
extraRules = convertTheme2To3(theme)
}
// Assuming that "worst case scenario background" is panel background since it's the most likely one // Assuming that "worst case scenario background" is panel background since it's the most likely one
const themes3 = init(extraRules, extraRules[0].directives['--bg'].split('|')[1].trim()) const themes3 = init({
inputRuleset,
ultimateBackgroundColor: inputRuleset[0].directives['--bg'].split('|')[1].trim(),
debug
})
getCssRules(themes3.eager, themes3.staticVars).forEach(rule => { getCssRules(themes3.eager, debug).forEach(rule => {
// Hacks to support multiple selectors on same component // Hacks to support multiple selectors on same component
if (rule.match(/::-webkit-scrollbar-button/)) { if (rule.match(/::-webkit-scrollbar-button/)) {
const parts = rule.split(/[{}]/g) const parts = rule.split(/[{}]/g)
@ -93,7 +87,7 @@ export const generateTheme = async (input, callbacks) => {
const processChunk = () => { const processChunk = () => {
const chunk = chunks[counter] const chunk = chunks[counter]
Promise.all(chunk.map(x => x())).then(result => { Promise.all(chunk.map(x => x())).then(result => {
getCssRules(result.filter(x => x), themes3.staticVars).forEach(rule => { getCssRules(result.filter(x => x), debug).forEach(rule => {
if (rule.match(/\.modal-view/)) { if (rule.match(/\.modal-view/)) {
const parts = rule.split(/[{}]/g) const parts = rule.split(/[{}]/g)
const newRule = [ const newRule = [
@ -152,7 +146,7 @@ export const tryLoadCache = () => {
} }
} }
export const applyTheme = async (input, onFinish = (data) => {}) => { export const applyTheme = async (input, onFinish = (data) => {}, debug) => {
const eagerStyles = createStyleSheet(EAGER_STYLE_ID) const eagerStyles = createStyleSheet(EAGER_STYLE_ID)
const lazyStyles = createStyleSheet(LAZY_STYLE_ID) const lazyStyles = createStyleSheet(LAZY_STYLE_ID)
@ -177,7 +171,8 @@ export const applyTheme = async (input, onFinish = (data) => {}) => {
onFinish(cache) onFinish(cache)
localStorage.setItem('pleroma-fe-theme-cache', JSON.stringify(cache)) localStorage.setItem('pleroma-fe-theme-cache', JSON.stringify(cache))
} }
} },
debug
) )
setTimeout(lazyProcessFunc, 0) setTimeout(lazyProcessFunc, 0)
@ -185,15 +180,52 @@ export const applyTheme = async (input, onFinish = (data) => {}) => {
return Promise.resolve() return Promise.resolve()
} }
const configColumns = ({ sidebarColumnWidth, contentColumnWidth, notifsColumnWidth, emojiReactionsScale }) => const extractStyleConfig = ({
({ sidebarColumnWidth, contentColumnWidth, notifsColumnWidth, emojiReactionsScale }) sidebarColumnWidth,
contentColumnWidth,
notifsColumnWidth,
emojiReactionsScale,
emojiSize,
navbarSize,
panelHeaderSize,
textSize,
forcedRoundness
}) => {
const result = {
sidebarColumnWidth,
contentColumnWidth,
notifsColumnWidth,
emojiReactionsScale,
emojiSize,
navbarSize,
panelHeaderSize,
textSize
}
switch (forcedRoundness) {
case 'disable':
break
case '0':
result.forcedRoundness = '0'
break
case '1':
result.forcedRoundness = '1px'
break
case '2':
result.forcedRoundness = '0.4rem'
break
default:
}
return result
}
const defaultConfigColumns = configColumns(defaultState) const defaultStyleConfig = extractStyleConfig(defaultState)
export const applyConfig = (config) => { export const applyConfig = (input) => {
const columns = configColumns(config) const config = extractStyleConfig(input)
if (columns === defaultConfigColumns) { if (config === defaultStyleConfig) {
return return
} }
@ -202,16 +234,25 @@ export const applyConfig = (config) => {
body.classList.add('hidden') body.classList.add('hidden')
const rules = Object const rules = Object
.entries(columns) .entries(config)
.filter(([k, v]) => v) .filter(([k, v]) => v)
.map(([k, v]) => `--${k}: ${v}`).join(';') .map(([k, v]) => `--${k}: ${v}`).join(';')
document.getElementById('style-config')?.remove()
const styleEl = document.createElement('style') const styleEl = document.createElement('style')
styleEl.id = 'style-config'
head.appendChild(styleEl) head.appendChild(styleEl)
const styleSheet = styleEl.sheet const styleSheet = styleEl.sheet
styleSheet.toString() styleSheet.toString()
styleSheet.insertRule(`:root { ${rules} }`, 'index-max') styleSheet.insertRule(`:root { ${rules} }`, 'index-max')
if (Object.prototype.hasOwnProperty.call(config, 'forcedRoundness')) {
styleSheet.insertRule(` * {
--roundness: var(--forcedRoundness) !important;
}`, 'index-max')
}
body.classList.remove('hidden') body.classList.remove('hidden')
} }
@ -269,5 +310,3 @@ export const getPreset = (val) => {
return { theme: data, source: theme.source } return { theme: data, source: theme.source }
}) })
} }
export const setPreset = (val) => getPreset(val).then(data => applyTheme(data))

@ -2,11 +2,6 @@ import { convert } from 'chromatism'
import { hex2rgb, rgba2css } from '../color_convert/color_convert.js' import { hex2rgb, rgba2css } from '../color_convert/color_convert.js'
// This changes what backgrounds are used to "stacked" solid colors so you can see
// what theme engine "thinks" is actual background color is for purposes of text color
// generation and for when --stacked variable is used
const DEBUG = false
export const parseCssShadow = (text) => { export const parseCssShadow = (text) => {
const dimensions = /(\d[a-z]*\s?){2,4}/.exec(text)?.[0] const dimensions = /(\d[a-z]*\s?){2,4}/.exec(text)?.[0]
const inset = /inset/.exec(text)?.[0] const inset = /inset/.exec(text)?.[0]
@ -66,7 +61,10 @@ export const getCssShadowFilter = (input) => {
.join(' ') .join(' ')
} }
export const getCssRules = (rules) => rules.map(rule => { // `debug` changes what backgrounds are used to "stacked" solid colors so you can see
// what theme engine "thinks" is actual background color is for purposes of text color
// generation and for when --stacked variable is used
export const getCssRules = (rules, debug) => rules.map(rule => {
let selector = rule.selector let selector = rule.selector
if (!selector) { if (!selector) {
selector = 'html' selector = 'html'
@ -93,7 +91,7 @@ export const getCssRules = (rules) => rules.map(rule => {
].join(';\n ') ].join(';\n ')
} }
case 'background': { case 'background': {
if (DEBUG) { if (debug) {
return ` return `
--background: ${getCssColorString(rule.dynamicVars.stacked)}; --background: ${getCssColorString(rule.dynamicVars.stacked)};
background-color: ${getCssColorString(rule.dynamicVars.stacked)}; background-color: ${getCssColorString(rule.dynamicVars.stacked)};
@ -161,3 +159,15 @@ export const getCssRules = (rules) => rules.map(rule => {
footer footer
].join('\n') ].join('\n')
}).filter(x => x) }).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
}
})
}

@ -39,7 +39,23 @@ export const getAllPossibleCombinations = (array) => {
return combos.reduce((acc, x) => [...acc, ...x], []) 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) => { export const genericRuleToSelector = components => (rule, ignoreOutOfTreeSelector, isParent) => {
if (!rule && !isParent) return null if (!rule && !isParent) return null
const component = components[rule.component] const component = components[rule.component]
@ -79,6 +95,17 @@ export const genericRuleToSelector = components => (rule, ignoreOutOfTreeSelecto
return selectors.trim() 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) => { export const combinationsMatch = (criteria, subject, strict) => {
if (criteria.component !== subject.component) return false if (criteria.component !== subject.component) return false
@ -101,6 +128,15 @@ export const combinationsMatch = (criteria, subject, strict) => {
return true 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 => { export const findRules = (criteria, strict) => subject => {
// If we searching for "general" rules - ignore "specific" ones // If we searching for "general" rules - ignore "specific" ones
if (criteria.parent === null && !!subject.parent) return false if (criteria.parent === null && !!subject.parent) return false
@ -125,6 +161,7 @@ export const findRules = (criteria, strict) => subject => {
return true return true
} }
// Pre-fills 'normal' state/variant if missing
export const normalizeCombination = rule => { export const normalizeCombination = rule => {
rule.variant = rule.variant ?? 'normal' rule.variant = rule.variant ?? 'normal'
rule.state = [...new Set(['normal', ...(rule.state || [])])] rule.state = [...new Set(['normal', ...(rule.state || [])])]

@ -12,7 +12,9 @@ export const basePaletteKeys = new Set([
'cBlue', 'cBlue',
'cRed', 'cRed',
'cGreen', 'cGreen',
'cOrange' 'cOrange',
'wallpaper'
]) ])
export const fontsKeys = new Set([ export const fontsKeys = new Set([
@ -138,7 +140,7 @@ export const convertTheme2To3 = (data) => {
Object.keys(data.opacity || {}).forEach(key => { Object.keys(data.opacity || {}).forEach(key => {
if (!opacityKeys.has(key) || data.opacity[key] === undefined) return null if (!opacityKeys.has(key) || data.opacity[key] === undefined) return null
const originalOpacity = data.opacity[key] const originalOpacity = data.opacity[key]
const rule = {} const rule = { source: '2to3' }
switch (key) { switch (key) {
case 'alert': case 'alert':
@ -213,7 +215,7 @@ export const convertTheme2To3 = (data) => {
Object.keys(data.radii || {}).forEach(key => { Object.keys(data.radii || {}).forEach(key => {
if (!radiiKeys.has(key) || data.radii[key] === undefined) return null if (!radiiKeys.has(key) || data.radii[key] === undefined) return null
const originalRadius = data.radii[key] const originalRadius = data.radii[key]
const rule = {} const rule = { source: '2to3' }
switch (key) { switch (key) {
case 'btn': case 'btn':
@ -265,8 +267,9 @@ export const convertTheme2To3 = (data) => {
const newRules = [] const newRules = []
Object.keys(data.fonts || {}).forEach(key => { Object.keys(data.fonts || {}).forEach(key => {
if (!fontsKeys.has(key)) return if (!fontsKeys.has(key)) return
if (!data.fonts[key]) return
const originalFont = data.fonts[key].family const originalFont = data.fonts[key].family
const rule = {} const rule = { source: '2to3' }
switch (key) { switch (key) {
case 'interface': case 'interface':
@ -300,7 +303,7 @@ export const convertTheme2To3 = (data) => {
Object.keys(data.shadows || {}).forEach(key => { Object.keys(data.shadows || {}).forEach(key => {
if (!shadowsKeys.has(key)) return if (!shadowsKeys.has(key)) return
const originalShadow = data.shadows[key] const originalShadow = data.shadows[key]
const rule = {} const rule = { source: '2to3' }
switch (key) { switch (key) {
case 'panel': case 'panel':
@ -369,7 +372,7 @@ export const convertTheme2To3 = (data) => {
const extendedRules = Object.entries(extendedBaseKeys).map(([prefix, keys]) => { const extendedRules = Object.entries(extendedBaseKeys).map(([prefix, keys]) => {
if (nonComponentPrefixes.has(prefix)) return null if (nonComponentPrefixes.has(prefix)) return null
const rule = {} const rule = { source: '2to3' }
if (prefix === 'alertPopup') { if (prefix === 'alertPopup') {
rule.component = 'Alert' rule.component = 'Alert'
rule.parent = { component: 'Popover' } rule.parent = { component: 'Popover' }
@ -402,7 +405,7 @@ export const convertTheme2To3 = (data) => {
const leftoverKey = key.replace(prefix, '') const leftoverKey = key.replace(prefix, '')
const parts = (leftoverKey || 'Bg').match(/[A-Z][a-z]*/g) const parts = (leftoverKey || 'Bg').match(/[A-Z][a-z]*/g)
const last = parts.slice(-1)[0] const last = parts.slice(-1)[0]
let newRule = { directives: {} } let newRule = { source: '2to3', directives: {} }
let variantArray = [] let variantArray = []
switch (last) { switch (last) {
@ -462,12 +465,12 @@ export const convertTheme2To3 = (data) => {
if (prefix === 'popover' && variantArray[0] === 'Post') { if (prefix === 'popover' && variantArray[0] === 'Post') {
newRule.component = 'Post' newRule.component = 'Post'
newRule.parent = { component: 'Popover' } newRule.parent = { source: '2to3hack', component: 'Popover' }
variantArray = variantArray.filter(x => x !== 'Post') variantArray = variantArray.filter(x => x !== 'Post')
} }
if (prefix === 'selectedMenu' && variantArray[0] === 'Popover') { if (prefix === 'selectedMenu' && variantArray[0] === 'Popover') {
newRule.parent = { component: 'Popover' } newRule.parent = { source: '2to3hack', component: 'Popover' }
variantArray = variantArray.filter(x => x !== 'Popover') variantArray = variantArray.filter(x => x !== 'Popover')
} }
@ -477,12 +480,12 @@ export const convertTheme2To3 = (data) => {
case 'alert': { case 'alert': {
const hasPanel = variantArray.find(x => x === 'Panel') const hasPanel = variantArray.find(x => x === 'Panel')
if (hasPanel) { if (hasPanel) {
newRule.parent = { component: 'PanelHeader' } newRule.parent = { source: '2to3hack', component: 'PanelHeader', parent: newRule.parent }
variantArray = variantArray.filter(x => x !== 'Panel') variantArray = variantArray.filter(x => x !== 'Panel')
} }
const hasTop = variantArray.find(x => x === 'Top') // TopBar const hasTop = variantArray.find(x => x === 'Top') // TopBar
if (hasTop) { if (hasTop) {
newRule.parent = { component: 'TopBar' } newRule.parent = { source: '2to3hack', component: 'TopBar', parent: newRule.parent }
variantArray = variantArray.filter(x => x !== 'Top' && x !== 'Bar') variantArray = variantArray.filter(x => x !== 'Top' && x !== 'Bar')
} }
break break

@ -117,7 +117,6 @@ export const topoSort = (
// Put it into the output list // Put it into the output list
output.push(node) output.push(node)
} else if (grays.has(node)) { } else if (grays.has(node)) {
console.debug('Cyclic depenency in topoSort, ignoring')
output.push(node) output.push(node)
} else if (blacks.has(node)) { } else if (blacks.has(node)) {
// do nothing // do nothing

@ -149,16 +149,42 @@ const ruleToSelector = genericRuleToSelector(components)
export const getEngineChecksum = () => engineChecksum export const getEngineChecksum = () => engineChecksum
export const init = (extraRuleset, ultimateBackgroundColor) => { /**
* 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!')
const staticVars = {} const staticVars = {}
const stacked = {} const stacked = {}
const computed = {} const computed = {}
const rulesetUnsorted = [ const rulesetUnsorted = [
...Object.values(components) ...Object.values(components)
.map(c => (c.defaultRules || []).map(r => ({ component: c.name, ...r }))) .map(c => (c.defaultRules || []).map(r => ({ component: c.name, ...r, source: 'Built-in' })))
.reduce((acc, arr) => [...acc, ...arr], []), .reduce((acc, arr) => [...acc, ...arr], []),
...extraRuleset ...inputRuleset
].map(rule => { ].map(rule => {
normalizeCombination(rule) normalizeCombination(rule)
let currentParent = rule.parent let currentParent = rule.parent
@ -395,11 +421,16 @@ export const init = (extraRuleset, ultimateBackgroundColor) => {
const processInnerComponent = (component, parent) => { const processInnerComponent = (component, parent) => {
const combinations = [] const combinations = []
const { const {
validInnerComponents = [],
states: originalStates = {}, states: originalStates = {},
variants: originalVariants = {} variants: originalVariants = {}
} = component } = component
const validInnerComponents = (
liteMode
? (component.validInnerComponentsLite || component.validInnerComponents)
: component.validInnerComponents
) || []
// Normalizing states and variants to always include "normal" // Normalizing states and variants to always include "normal"
const states = { normal: '', ...originalStates } const states = { normal: '', ...originalStates }
const variants = { normal: '', ...originalVariants } const variants = { normal: '', ...originalVariants }
@ -411,22 +442,26 @@ export const init = (extraRuleset, ultimateBackgroundColor) => {
// Optimization: we only really need combinations without "normal" because all states implicitly have it // Optimization: we only really need combinations without "normal" because all states implicitly have it
const permutationStateKeys = Object.keys(states).filter(s => s !== 'normal') const permutationStateKeys = Object.keys(states).filter(s => s !== 'normal')
const stateCombinations = [ const stateCombinations = onlyNormalState
['normal'], ? [
...getAllPossibleCombinations(permutationStateKeys) ['normal']
.map(combination => ['normal', ...combination]) ]
.filter(combo => { : [
// Optimization: filter out some hard-coded combinations that don't make sense ['normal'],
if (combo.indexOf('disabled') >= 0) { ...getAllPossibleCombinations(permutationStateKeys)
return !( .map(combination => ['normal', ...combination])
combo.indexOf('hover') >= 0 || .filter(combo => {
combo.indexOf('focused') >= 0 || // Optimization: filter out some hard-coded combinations that don't make sense
combo.indexOf('pressed') >= 0 if (combo.indexOf('disabled') >= 0) {
) return !(
} combo.indexOf('hover') >= 0 ||
return true combo.indexOf('focused') >= 0 ||
}) combo.indexOf('pressed') >= 0
] )
}
return true
})
]
const stateVariantCombination = Object.keys(variants).map(variant => { const stateVariantCombination = Object.keys(variants).map(variant => {
return stateCombinations.map(state => ({ variant, state })) return stateCombinations.map(state => ({ variant, state }))
@ -451,9 +486,11 @@ export const init = (extraRuleset, ultimateBackgroundColor) => {
} }
const t0 = performance.now() const t0 = performance.now()
const combinations = processInnerComponent(components.Root) const combinations = processInnerComponent(components[rootComponentName] ?? components.Root)
const t1 = performance.now() 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) => { const result = combinations.map((combination) => {
if (combination.lazy) { if (combination.lazy) {
@ -463,7 +500,9 @@ export const init = (extraRuleset, ultimateBackgroundColor) => {
} }
}).filter(x => x) }).filter(x => x)
const t2 = performance.now() const t2 = performance.now()
console.debug('Eager processing took ' + (t2 - t1) + ' ms') if (debug) {
console.debug('Eager processing took ' + (t2 - t1) + ' ms')
}
return { return {
lazy: result.filter(x => typeof x === 'function'), lazy: result.filter(x => typeof x === 'function'),

@ -66,7 +66,7 @@ describe('Theme Data 3', () => {
this.timeout(5000) this.timeout(5000)
it('Test initialization without anything', () => { it('Test initialization without anything', () => {
const out = init([], '#DEADAF') const out = init({ inputRuleset: [], ultimateBackgroundColor: '#DEADAF' })
expect(out).to.have.property('eager') expect(out).to.have.property('eager')
expect(out).to.have.property('lazy') expect(out).to.have.property('lazy')
@ -85,13 +85,16 @@ describe('Theme Data 3', () => {
}) })
it('Test initialization with a basic palette', () => { it('Test initialization with a basic palette', () => {
const out = init([{ const out = init({
component: 'Root', inputRuleset: [{
directives: { component: 'Root',
'--bg': 'color | #008080', directives: {
'--fg': 'color | #00C0A0' '--bg': 'color | #008080',
} '--fg': 'color | #00C0A0'
}], '#DEADAF') }
}],
ultimateBackgroundColor: '#DEADAF'
})
expect(out.staticVars).to.have.property('bg').equal('#008080') expect(out.staticVars).to.have.property('bg').equal('#008080')
expect(out.staticVars).to.have.property('fg').equal('#00C0A0') expect(out.staticVars).to.have.property('fg').equal('#00C0A0')
@ -105,17 +108,20 @@ describe('Theme Data 3', () => {
}) })
it('Test initialization with opacity', () => { it('Test initialization with opacity', () => {
const out = init([{ const out = init({
component: 'Root', inputRuleset: [{
directives: { component: 'Root',
'--bg': 'color | #008080' directives: {
} '--bg': 'color | #008080'
}, { }
component: 'Panel', }, {
directives: { component: 'Panel',
opacity: 0.5 directives: {
} opacity: 0.5
}], '#DEADAF') }
}],
ultimateBackgroundColor: '#DEADAF'
})
expect(out.staticVars).to.have.property('bg').equal('#008080') expect(out.staticVars).to.have.property('bg').equal('#008080')

Loading…
Cancel
Save