logo

pleroma-fe

My custom branche(s) on git.pleroma.social/pleroma/pleroma-fe git clone https://hacktivis.me/git/pleroma-fe.git
commit: e8d7d341f03846a3faf85670866b2cb8d9d12cc9
parent 0ca9a2c8c0d04b3468c93de4a53fac8197581d8b
Author: Henry Jameson <me@hjkos.com>
Date:   Thu, 25 Jul 2024 11:53:58 +0300

Merge remote-tracking branch 'origin/develop' into fix-develop-issues

Diffstat:

Achangelog.d/appearance-tab.change1+
Achangelog.d/emoji-scale.add1+
Achangelog.d/firefox-redmon.fix1+
Achangelog.d/theme-selector.add1+
Achangelog.d/ui-scale.add1+
Achangelog.d/user-overrides.add1+
Apreview.style.js0
Msrc/App.scss48+++++++++++++++++++++++++++++++-----------------
Msrc/boot/after_store.js27++-------------------------
Msrc/components/emoji_picker/emoji_picker.js35+++++++++++++++++++++++++++++------
Msrc/components/emoji_picker/emoji_picker.scss75++++++++++++++++++++++++++++++++++++++++++---------------------------------
Msrc/components/emoji_reactions/emoji_reactions.vue2+-
Msrc/components/font_control/font_control.js70+++++++++++++++++++++++++++++++++-------------------------------------
Msrc/components/font_control/font_control.vue166++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------
Msrc/components/mobile_nav/mobile_nav.vue8++++----
Msrc/components/navigation/navigation_pins.vue1+
Msrc/components/notification/notification.style.js3++-
Msrc/components/panel.style.js10++++++++++
Msrc/components/root.style.js5+++++
Msrc/components/settings_modal/helpers/number_setting.vue1+
Msrc/components/settings_modal/helpers/setting.js10+++++++++-
Msrc/components/settings_modal/helpers/unit_setting.js24++++++++++++++++++++----
Msrc/components/settings_modal/helpers/unit_setting.vue3++-
Msrc/components/settings_modal/settings_modal.js3+++
Msrc/components/settings_modal/settings_modal.vue12++++++++++++
Msrc/components/settings_modal/settings_modal_user_content.js8++++++--
Msrc/components/settings_modal/settings_modal_user_content.vue47+++++++++++++++++++++++++++--------------------
Asrc/components/settings_modal/tabs/appearance_tab.js195+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/components/settings_modal/tabs/appearance_tab.vue313+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/components/settings_modal/tabs/filtering_tab.vue18------------------
Msrc/components/settings_modal/tabs/general_tab.js33+--------------------------------
Msrc/components/settings_modal/tabs/general_tab.vue83-------------------------------------------------------------------------------
Msrc/components/settings_modal/tabs/theme_tab/preview.vue121+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
Msrc/components/settings_modal/tabs/theme_tab/theme_tab.js98+++++++++++++++++++++++++++++++++++++++++++------------------------------------
Msrc/components/settings_modal/tabs/theme_tab/theme_tab.scss114+++++++------------------------------------------------------------------------
Msrc/components/settings_modal/tabs/theme_tab/theme_tab.vue203++++++++++++++++++++++++++++++++++++++++++-------------------------------------
Msrc/components/status/post.style.js9+++++++++
Msrc/i18n/en.json52+++++++++++++++++++++++++++++++++++++++++++++++++++-
Msrc/modules/config.js106++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
Msrc/modules/instance.js35++++++++---------------------------
Msrc/modules/interface.js219++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Msrc/panel.scss7++++---
Msrc/services/style_setter/style_setter.js89+++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------
Msrc/services/theme_data/css_utils.js24+++++++++++++++++-------
Msrc/services/theme_data/iss_utils.js39++++++++++++++++++++++++++++++++++++++-
Msrc/services/theme_data/theme2_to_theme3.js25++++++++++++++-----------
Msrc/services/theme_data/theme_data.service.js1-
Msrc/services/theme_data/theme_data_3.service.js85+++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------
Mtest/unit/specs/services/theme_data/theme_data3.spec.js44+++++++++++++++++++++++++-------------------
49 files changed, 1754 insertions(+), 723 deletions(-)

diff --git a/changelog.d/appearance-tab.change b/changelog.d/appearance-tab.change @@ -0,0 +1 @@ +Reorganized Settings modal to move out visual stuff into Appearance tab diff --git a/changelog.d/emoji-scale.add b/changelog.d/emoji-scale.add @@ -0,0 +1 @@ +Ability to change size of emoji diff --git a/changelog.d/firefox-redmon.fix b/changelog.d/firefox-redmon.fix @@ -0,0 +1 @@ +Bug with firefox and redmond themes diff --git a/changelog.d/theme-selector.add b/changelog.d/theme-selector.add @@ -0,0 +1 @@ +Theme selector with visual previews of the theme diff --git a/changelog.d/ui-scale.add b/changelog.d/ui-scale.add @@ -0,0 +1 @@ +Ability to resize UI (and certain components) scale independent of browser/text scale diff --git a/changelog.d/user-overrides.add b/changelog.d/user-overrides.add @@ -0,0 +1 @@ +Ability to override certain aspects of UI style independent of theme used (UI roundness, fonts, underlay) diff --git a/preview.style.js b/preview.style.js diff --git a/src/App.scss b/src/App.scss @@ -4,7 +4,8 @@ :root { --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; // Z-Index stuff --ZI_media_modal: 9000; @@ -19,7 +20,9 @@ } html { - font-size: var(--font-size, 14px); + font-size: var(--textSize, 14px); + --navbar-height: var(--navbarSize, 3.5rem); + --emoji-size: var(--emojiSize, 32px); // overflow-x: clip causes my browser's tab to crash with SIGILL lul } @@ -155,6 +158,7 @@ nav { box-shadow: var(--shadow); box-sizing: border-box; height: var(--navbar-height); + font-size: calc(var(--navbar-height) / 3.5); position: fixed; } @@ -206,7 +210,7 @@ nav { .app-layout { --miniColumn: 25rem; --maxiColumn: 45rem; - --columnGap: 1em; + --columnGap: 1rem; --effectiveSidebarColumnWidth: minmax(var(--miniColumn), var(--sidebarColumnWidth, var(--miniColumn))); --effectiveNotifsColumnWidth: minmax(var(--miniColumn), var(--notifsColumnWidth, var(--miniColumn))); --effectiveContentColumnWidth: minmax(var(--miniColumn), var(--contentColumnWidth, var(--maxiColumn))); @@ -370,7 +374,6 @@ nav { user-select: none; color: var(--text); border: none; - border-radius: var(--roundness); cursor: pointer; background-color: var(--background); box-shadow: var(--shadow); @@ -506,7 +509,6 @@ textarea { --_padding: 0.5em; border: none; - border-radius: var(--roundness); background-color: var(--background); color: var(--text); box-shadow: var(--shadow); @@ -612,6 +614,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 textarea.input { padding: var(--_padding); @@ -657,22 +670,23 @@ option { display: inline-flex; vertical-align: middle; - button, - .button-dropdown { + > *, + > * .button-default { + --_roundness-left: 0; + --_roundness-right: 0; + position: relative; flex: 1 1 auto; + } - &:not(:last-child), - &:not(:last-child) .button-default { - border-top-right-radius: 0; - border-bottom-right-radius: 0; - } + > *:first-child, + > *:first-child .button-default { + --_roundness-left: var(--roundness); + } - &:not(:first-child), - &:not(:first-child) .button-default { - border-top-left-radius: 0; - border-bottom-left-radius: 0; - } + > *:last-child, + > *:last-child .button-default { + --_roundness-right: var(--roundness); } } diff --git a/src/boot/after_store.js b/src/boot/after_store.js @@ -13,8 +13,7 @@ import VBodyScrollLock from 'src/directives/body_scroll_lock' import { windowWidth, windowHeight } from '../services/window_utils/window_utils' import { getOrCreateApp, getClientToken } from '../services/new_api/oauth.js' import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js' -import { CURRENT_VERSION } from '../services/theme_data/theme_data.service.js' -import { applyTheme, applyConfig, tryLoadCache } from '../services/style_setter/style_setter.js' +import { applyConfig } from '../services/style_setter/style_setter.js' import FaviconService from '../services/favicon_service/favicon_service.js' import { initServiceWorker, updateFocus } from '../services/sw/sw.js' @@ -160,8 +159,6 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => { copyInstanceOption('showFeaturesPanel') copyInstanceOption('hideSitename') copyInstanceOption('sidebarRight') - - return store.dispatch('setTheme', config.theme) } const getTOS = async ({ store }) => { @@ -352,27 +349,7 @@ const afterStoreSetup = async ({ store, i18n }) => { store.dispatch('setInstanceOption', { name: 'server', value: server }) await setConfig({ store }) - - 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!') - } - } + await store.dispatch('setTheme') applyConfig(store.state.config) diff --git a/src/components/emoji_picker/emoji_picker.js b/src/components/emoji_picker/emoji_picker.js @@ -120,6 +120,7 @@ const EmojiPicker = { groupRefs: {}, emojiRefs: {}, filteredEmojiGroups: [], + emojiSize: 0, width: 0 } }, @@ -130,6 +131,23 @@ const EmojiPicker = { Popover }, 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 () { this.$refs.popover.showPopover() this.onShowing() @@ -224,6 +242,7 @@ const EmojiPicker = { }, onShowing () { const oldContentLoaded = this.contentLoaded + this.updateEmojiSize() this.recalculateItemPerRow() this.$nextTick(() => { this.$refs.search.focus() @@ -266,16 +285,20 @@ const EmojiPicker = { }, computed: { minItemSize () { - return this.emojiHeight + return this.emojiSize }, - emojiHeight () { - return 32 + 4 + // used to watch it + fontSize () { + this.$nextTick(() => { + this.updateEmojiSize() + }) + return this.$store.getters.mergedConfig.fontSize }, - emojiWidth () { - return 32 + 4 + emojiHeight () { + return this.emojiSize }, itemPerRow () { - return this.width ? Math.floor(this.width / this.emojiWidth - 1) : 6 + return this.width ? Math.floor(this.width / this.emojiSize) : 6 }, activeGroupView () { return this.showingStickers ? '' : this.activeGroup diff --git a/src/components/emoji_picker/emoji_picker.scss b/src/components/emoji_picker/emoji_picker.scss @@ -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-header: 2.2em; + width: 25em; max-width: calc(100vw - 20px); // popover gives 10px margin from window edge display: flex; @@ -13,24 +10,26 @@ $emoji-picker-emoji-size: 32px; display: inline-flex; justify-content: center; align-items: center; - width: $emoji-picker-header-picture-width; - max-width: $emoji-picker-header-picture-width; - height: $emoji-picker-header-picture-height; - max-height: $emoji-picker-header-picture-height; + width: var(--__emoji-picker-header); + max-width: var(--__emoji-picker-header); + height: var(--__emoji-picker-header); + max-height: var(--__emoji-picker-header); .still-image { - max-width: 100%; - max-height: 100%; - height: 100%; - width: 100%; + width: var(--__emoji-picker-header); + max-width: var(--__emoji-picker-header); + height: var(--__emoji-picker-header); + max-height: var(--__emoji-picker-header); object-fit: contain; + + --_still_image-label-scale: 0.5; } } .keep-open, .too-many-emoji, .hide-custom-emoji { - padding: 7px; + padding: 0.5em; line-height: normal; } @@ -44,13 +43,13 @@ $emoji-picker-emoji-size: 32px; } .keep-open-label { - padding: 0 7px; + padding: 0 0.5em; display: flex; } .heading { display: flex; - padding: 10px 7px 5px; + padding: 0.7em 0.5em 0; } .content { @@ -65,13 +64,14 @@ $emoji-picker-emoji-size: 32px; display: flex; flex-flow: row nowrap; overflow-x: auto; + overflow-y: hidden; } .additional-tabs { display: flex; border-left: 1px solid; border-left-color: var(--border); - padding-left: 7px; + padding-left: 0.5em; flex: 0 0 auto; } @@ -80,25 +80,29 @@ $emoji-picker-emoji-size: 32px; flex-basis: auto; display: flex; align-content: center; + scrollbar-width: thin; &-item { - padding: 0 7px; + padding: 0 0.5em; cursor: pointer; - font-size: 1.85em; - width: $emoji-picker-header-picture-width; - max-width: $emoji-picker-header-picture-width; - height: $emoji-picker-header-picture-height; - max-height: $emoji-picker-header-picture-height; + width: var(--__emoji-picker-header); + max-width: var(--__emoji-picker-header); + height: var(--__emoji-picker-header); + max-height: var(--__emoji-picker-header); display: flex; align-items: center; + .svg-inline--fa { + font-size: 1.85em; + } + &.disabled { opacity: 0.5; pointer-events: none; } &.toggled { - border-bottom: 4px solid; + border-bottom: 0.2em solid; } } } @@ -125,7 +129,7 @@ $emoji-picker-emoji-size: 32px; .emoji { &-search { - padding: 5px; + padding: 0.3em; flex: 0 0 auto; input { @@ -139,6 +143,7 @@ $emoji-picker-emoji-size: 32px; flex: 1 1 1px; position: relative; overflow: auto; + scrollbar-gutter: stable both-edges; user-select: none; mask: linear-gradient(to top, white 0, transparent 100%) bottom no-repeat, @@ -165,13 +170,13 @@ $emoji-picker-emoji-size: 32px; display: flex; align-items: center; flex-wrap: wrap; - padding-left: 5px; justify-content: left; &-title { font-size: 0.85em; width: 100%; margin: 0; + padding-left: 0.3em; &.disabled { display: none; @@ -180,24 +185,28 @@ $emoji-picker-emoji-size: 32px; } &-item { - width: $emoji-picker-emoji-size; - height: $emoji-picker-emoji-size; + width: var(--emoji-size); + height: var(--emoji-size); box-sizing: border-box; display: flex; - line-height: $emoji-picker-emoji-size; + line-height: var(--emoji-size); align-items: center; justify-content: center; - margin: 4px; + margin: 0.2em; cursor: pointer; .emoji-picker-emoji.-custom { object-fit: contain; - max-width: 100%; - max-height: 100%; + width: var(--emoji-size); + max-width: var(--emoji-size); + height: var(--emoji-size); + max-height: var(--emoji-size); + + --_still_image-label-scale: 0.5; } .emoji-picker-emoji.-unicode { - font-size: 24px; + font-size: 1.6em; overflow: hidden; } } diff --git a/src/components/emoji_reactions/emoji_reactions.vue b/src/components/emoji_reactions/emoji_reactions.vue @@ -79,7 +79,7 @@ margin-top: 0.25em; flex-wrap: wrap; - --emoji-size: calc(1.25em * var(--emojiReactionsScale, 1)); + --emoji-size: calc(var(--emojiSize, 1.25em) * var(--emojiReactionsScale, 1)); .emoji-reaction-container { display: flex; diff --git a/src/components/font_control/font_control.js b/src/components/font_control/font_control.js @@ -1,63 +1,59 @@ -import { set } from 'lodash' 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 { components: { - Select + Select, + Checkbox, + Popover }, props: [ 'name', 'label', 'modelValue', 'fallback', 'options', 'no-inherit' ], + mounted () { + this.$store.dispatch('queryLocalFonts') + }, emits: ['update:modelValue'], data () { return { - lValue: this.modelValue, + manualEntry: false, availableOptions: [ this.noInherit ? '' : 'inherit', - 'custom', - ...(this.options || []), 'serif', + 'sans-serif', 'monospace', - 'sans-serif' + ...(this.options || []) ].filter(_ => _) } }, - beforeUpdate () { - this.lValue = this.modelValue + methods: { + toggleManualEntry () { + this.manualEntry = !this.manualEntry + } }, computed: { present () { - return typeof this.lValue !== '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) - } + return typeof this.modelValue !== 'undefined' }, - isCustom () { - return this.preset === 'custom' + localFontsList () { + return this.$store.state.interface.localFonts }, - preset: { - get () { - 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 - } + localFontsSize () { + return this.$store.state.interface.localFonts?.length } } } diff --git a/src/components/font_control/font_control.vue b/src/components/font_control/font_control.vue @@ -1,6 +1,6 @@ <template> <div - class="font-control style-control" + class="font-control" :class="{ custom: isCustom }" > <label @@ -10,43 +10,121 @@ > {{ label }} </label> - <input + {{ ' ' }} + <Checkbox v-if="typeof fallback !== 'undefined'" :id="name + '-o'" - :aria-labelledby="name + '-label'" - class="input -checkbox opt exlcude-disabled visible-for-screenreader-only" - type="checkbox" - :checked="present" + :modelValue="present" @change="$emit('update:modelValue', typeof modelValue === 'undefined' ? fallback : undefined)" > - <label - v-if="typeof fallback !== 'undefined'" - class="opt-l" - :for="name + '-o'" - :aria-hidden="true" - /> - {{ ' ' }} - <Select - :id="name + '-font-switcher'" - v-model="preset" - :disabled="!present" - class="font-switcher" - > - <option - v-for="option in availableOptions" - :key="option" - :value="option" + {{ $t('settings.style.themes3.define') }} + </Checkbox> + <p v-if="modelValue?.family"> + <label + v-if="manualEntry" + :id="name + '-label'" + :for="preset === 'custom' ? name : name + '-font-switcher'" + class="label" > - {{ option === 'custom' ? $t('settings.style.fonts.custom') : option }} - </option> - </Select> - <input - v-if="isCustom" - :id="name" - v-model="family" - class="input custom-font" - type="text" - > + <i18n-t + keypath="settings.style.themes3.font.entry" + tag="span" + > + <template #fontFamily> + <code>font-family</code> + </template> + </i18n-t> + </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> </template> @@ -54,21 +132,15 @@ <style lang="scss"> .font-control { - input.custom-font { - min-width: 10em; + .custom-font { + min-width: 20em; + max-width: 20em; } +} - &.custom { - /* TODO Should make proper joiners... */ - .font-switcher { - border-top-right-radius: 0; - border-bottom-right-radius: 0; - } - - .custom-font { - border-top-left-radius: 0; - border-bottom-left-radius: 0; - } - } +.invalid-tooltip { + margin: 0.5em 1em; + min-width: 10em; + text-align: center; } </style> diff --git a/src/components/mobile_nav/mobile_nav.vue b/src/components/mobile_nav/mobile_nav.vue @@ -129,7 +129,7 @@ .mobile-nav { display: grid; line-height: var(--navbar-height); - grid-template-rows: 50px; + grid-template-rows: var(--navbar-height); grid-template-columns: 2fr auto; width: 100%; box-sizing: border-box; @@ -190,8 +190,8 @@ justify-content: space-between; z-index: calc(var(--ZI_navbar) + 100); width: 100%; - height: 50px; - line-height: 50px; + height: 3.5em; + line-height: 3.5em; position: absolute; box-shadow: var(--shadow); @@ -214,7 +214,7 @@ } .mobile-notifications { - margin-top: 50px; + margin-top: 3.5em; width: 100vw; height: calc(100vh - var(--navbar-height)); overflow-x: hidden; diff --git a/src/components/navigation/navigation_pins.vue b/src/components/navigation/navigation_pins.vue @@ -49,6 +49,7 @@ } &.toggled { + margin-bottom: -4px; border-bottom: 4px solid; } } diff --git a/src/components/notification/notification.style.js b/src/components/notification/notification.style.js @@ -11,7 +11,8 @@ export default { 'RichContent', 'Input', 'Avatar', - 'Attachment' + 'Attachment', + 'PollGraph' ], defaultRules: [] } diff --git a/src/components/panel.style.js b/src/components/panel.style.js @@ -20,6 +20,16 @@ export default { 'Tab', 'ListItem' ], + validInnerComponentsLite: [ + 'Text', + 'Link', + 'Icon', + 'Border', + 'Button', + 'Input', + 'PanelHeader', + 'Alert' + ], defaultRules: [ { directives: { diff --git a/src/components/root.style.js b/src/components/root.style.js @@ -12,6 +12,11 @@ export default { 'Alert', 'Button' // mobile post button ], + validInnerComponentsLite: [ + 'Underlay', + 'Scrollbar', + 'ScrollbarElement' + ], defaultRules: [ { directives: { diff --git a/src/components/settings_modal/helpers/number_setting.vue b/src/components/settings_modal/helpers/number_setting.vue @@ -15,6 +15,7 @@ </template> <slot v-else /> </label> + {{ ' ' }} <input :id="path" class="input number-input" diff --git a/src/components/settings_modal/helpers/setting.js b/src/components/settings_modal/helpers/setting.js @@ -48,6 +48,10 @@ export default { draftMode: { type: Boolean, default: undefined + }, + timedApplyMode: { + type: Boolean, + default: false } }, inject: { @@ -161,7 +165,11 @@ export default { case 'admin': return (k, v) => this.$store.dispatch('pushAdminSetting', { path: k, value: v }) 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 () { diff --git a/src/components/settings_modal/helpers/unit_setting.js b/src/components/settings_modal/helpers/unit_setting.js @@ -21,15 +21,23 @@ export default { unitSet: { type: String, default: 'none' + }, + step: { + type: Number, + default: 1 + }, + resetDefault: { + type: Object, + default: null } }, computed: { ...Setting.computed, stateUnit () { - return this.state.replace(/\d+/, '') + return typeof this.state === 'string' ? this.state.replace(/[0-9,.]+/, '') : '' }, stateValue () { - return this.state.replace(/\D+/, '') + return typeof this.state === 'string' ? this.state.replace(/[^0-9,.]+/, '') : '' } }, methods: { @@ -39,10 +47,18 @@ export default { return this.$t(['settings', 'units', this.unitSet, value].join('.')) }, updateValue (e) { - this.configSink(this.path, parseInt(e.target.value) + this.stateUnit) + this.configSink(this.path, parseFloat(e.target.value) + this.stateUnit) }, 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) } } } diff --git a/src/components/settings_modal/helpers/unit_setting.vue b/src/components/settings_modal/helpers/unit_setting.vue @@ -9,11 +9,12 @@ > <slot /> </label> + {{ ' ' }} <input :id="path" class="input number-input" type="number" - step="1" + :step="step" :disabled="disabled" :min="min || 0" :value="stateValue" diff --git a/src/components/settings_modal/settings_modal.js b/src/components/settings_modal/settings_modal.js @@ -4,6 +4,7 @@ import AsyncComponentError from 'src/components/async_component_error/async_comp import getResettableAsyncComponent from 'src/services/resettable_async_component.js' import Popover from '../popover/popover.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 { cloneDeep, isEqual } from 'lodash' import { @@ -53,6 +54,7 @@ const SettingsModal = { Modal, Popover, Checkbox, + ConfirmModal, SettingsModalUserContent: getResettableAsyncComponent( () => import('./settings_modal_user_content.vue'), { @@ -165,6 +167,7 @@ const SettingsModal = { }, computed: { currentSaveStateNotice () { + console.log(this.$store.state.interface.settings.currentSaveStateNotice) return this.$store.state.interface.settings.currentSaveStateNotice }, modalActivated () { diff --git a/src/components/settings_modal/settings_modal.vue b/src/components/settings_modal/settings_modal.vue @@ -147,6 +147,18 @@ </span> </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> </template> diff --git a/src/components/settings_modal/settings_modal_user_content.js b/src/components/settings_modal/settings_modal_user_content.js @@ -7,6 +7,7 @@ import FilteringTab from './tabs/filtering_tab.vue' import SecurityTab from './tabs/security_tab/security_tab.vue' import ProfileTab from './tabs/profile_tab.vue' import GeneralTab from './tabs/general_tab.vue' +import AppearanceTab from './tabs/appearance_tab.vue' import VersionTab from './tabs/version_tab.vue' import ThemeTab from './tabs/theme_tab/theme_tab.vue' @@ -19,7 +20,8 @@ import { faBell, faDownload, faEyeSlash, - faInfo + faInfo, + faWindowRestore } from '@fortawesome/free-solid-svg-icons' library.add( @@ -30,7 +32,8 @@ library.add( faBell, faDownload, faEyeSlash, - faInfo + faInfo, + faWindowRestore ) const SettingsModalContent = { @@ -44,6 +47,7 @@ const SettingsModalContent = { SecurityTab, ProfileTab, GeneralTab, + AppearanceTab, VersionTab, ThemeTab }, diff --git a/src/components/settings_modal/settings_modal_user_content.vue b/src/components/settings_modal/settings_modal_user_content.vue @@ -14,6 +14,20 @@ <GeneralTab /> </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 v-if="isLoggedIn" :label="$t('settings.profile_tab')" icon="user" @@ -23,6 +37,14 @@ </div> <div v-if="isLoggedIn" + :label="$t('settings.notifications')" + icon="bell" + data-tab-name="notifications" + > + <NotificationsTab /> + </div> + <div + v-if="isLoggedIn" :label="$t('settings.security_tab')" icon="lock" data-tab-name="security" @@ -37,19 +59,13 @@ <FilteringTab /> </div> <div - :label="$t('settings.theme')" - icon="paint-brush" - data-tab-name="theme" - > - <ThemeTab /> - </div> - <div v-if="isLoggedIn" - :label="$t('settings.notifications')" - icon="bell" - data-tab-name="notifications" + :label="$t('settings.mutes_and_blocks')" + :fullHeight="true" + icon="eye-slash" + data-tab-name="mutesAndBlocks" > - <NotificationsTab /> + <MutesAndBlocksTab /> </div> <div v-if="isLoggedIn" @@ -60,15 +76,6 @@ <DataImportExportTab /> </div> <div - v-if="isLoggedIn" - :label="$t('settings.mutes_and_blocks')" - :fullHeight="true" - icon="eye-slash" - data-tab-name="mutesAndBlocks" - > - <MutesAndBlocksTab /> - </div> - <div :label="$t('settings.version.title')" icon="info" data-tab-name="version" diff --git a/src/components/settings_modal/tabs/appearance_tab.js b/src/components/settings_modal/tabs/appearance_tab.js @@ -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 diff --git a/src/components/settings_modal/tabs/appearance_tab.vue b/src/components/settings_modal/tabs/appearance_tab.vue @@ -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> diff --git a/src/components/settings_modal/tabs/filtering_tab.vue b/src/components/settings_modal/tabs/filtering_tab.vue @@ -50,24 +50,6 @@ </BooleanSetting> </li> <li> - <BooleanSetting path="hideMutedFederationRestrictions"> - {{ $t('settings.hide_muted_federation_restrictions') }} - </BooleanSetting> - <ul - class="setting-list suboptions" - :class="[{disabled: !streaming}]" - > - <li - v-for="item in muteFederationRestrictionsLevels" - :key="'mute_' + item + '_federation_restriction'" - > - <BooleanSetting :path="'muteFederationRestrictions.' + item"> - {{ $t('settings.mute_' + item + '_federation_restriction') }} - </BooleanSetting> - </li> - </ul> - </li> - <li> <BooleanSetting path="hidePostStats"> {{ $t('settings.hide_post_stats') }} </BooleanSetting> diff --git a/src/components/settings_modal/tabs/general_tab.js b/src/components/settings_modal/tabs/general_tab.js @@ -3,7 +3,7 @@ import ChoiceSetting from '../helpers/choice_setting.vue' import ScopeSelector from 'src/components/scope_selector/scope_selector.vue' import IntegerSetting from '../helpers/integer_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 SharedComputedObject from '../helpers/shared_computed_object.js' @@ -40,11 +40,6 @@ const GeneralTab = { value: 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 => ({ key: mode, value: mode, @@ -70,9 +65,6 @@ const GeneralTab = { ProfileSettingIndicator }, computed: { - horizontalUnits () { - return defaultHorizontalUnits - }, postFormats () { return this.$store.state.instance.postFormats || [] }, @@ -83,29 +75,6 @@ const GeneralTab = { 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() }, methods: { diff --git a/src/components/settings_modal/tabs/general_tab.vue b/src/components/settings_modal/tabs/general_tab.vue @@ -15,11 +15,6 @@ {{ $t('settings.hide_isp') }} </BooleanSetting> </li> - <li v-if="instanceWallpaperUsed"> - <BooleanSetting path="hideInstanceWallpaper"> - {{ $t('settings.hide_wallpaper') }} - </BooleanSetting> - </li> <li> <BooleanSetting path="stopGifs"> {{ $t('settings.stop_gifs') }} @@ -98,53 +93,6 @@ {{ $t('settings.hide_shoutbox') }} </BooleanSetting> </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"> <span class="label">{{ $t('settings.confirm_dialogs') }}</span> <ul class="option-list"> @@ -201,14 +149,6 @@ <h2>{{ $t('settings.post_look_feel') }}</h2> <ul class="setting-list"> <li> - <BooleanSetting - path="forceThemeRecompilation" - :expert="1" - > - {{ $t('settings.force_theme_recompilation_debug') }} - </BooleanSetting> - </li> - <li> <ChoiceSetting id="conversationDisplay" path="conversationDisplay" @@ -277,15 +217,6 @@ {{ $t('settings.no_rich_text_description') }} </BooleanSetting> </li> - <li> - <FloatSetting - v-if="user" - path="emojiReactionsScale" - expert="1" - > - {{ $t('settings.emoji_reactions_scale') }} - </FloatSetting> - </li> <h3>{{ $t('settings.attachments') }}</h3> <li> <BooleanSetting @@ -528,17 +459,3 @@ </template> <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> diff --git a/src/components/settings_modal/tabs/theme_tab/preview.vue b/src/components/settings_modal/tabs/theme_tab/preview.vue @@ -99,15 +99,9 @@ > <div class="actions"> - <span class="checkbox"> - <input - id="preview_checkbox" - checked="very yes" - type="checkbox" - class="input" - > - <label for="preview_checkbox">{{ $t('settings.style.preview.checkbox') }}</label> - </span> + <Checkbox> + {{ $t('settings.style.preview.checkbox') }} + </Checkbox> <button class="btn button-default"> {{ $t('settings.style.preview.button') }} </button> @@ -118,6 +112,7 @@ </template> <script> +import Checkbox from 'src/components/checkbox/checkbox.vue' import { library } from '@fortawesome/fontawesome-svg-core' import { faTimes, @@ -133,12 +128,116 @@ library.add( faReply ) -export default {} +export default { + components: { + Checkbox + } +} </script> <style lang="scss"> .preview-container { position: relative; + border-top: 1px dashed; + border-bottom: 1px dashed; + border-color: var(--border); + margin: 1em 0; + padding: 1em; + background-color: var(--wallpaper); + background-image: var(--body-background-image); + background-size: cover; + background-position: 50% 50%; + + .theme-preview-content { + padding: 20px; + } + + .dummy { + .post { + font-family: var(--postFont); + display: flex; + + .content { + flex: 1; + + h4 { + margin-bottom: 0.25em; + } + + .icons { + margin-top: 0.5em; + display: flex; + + i { + margin-right: 1em; + } + } + } + } + + .after-post { + margin-top: 1em; + display: flex; + align-items: center; + } + + .avatar, + .avatar-alt { + background: + linear-gradient( + 135deg, + #b8e1fc 0%, + #a9d2f3 10%, + #90bae4 25%, + #90bcea 37%, + #90bff0 50%, + #6ba8e5 51%, + #a2daf5 83%, + #bdf3fd 100% + ); + color: black; + font-family: sans-serif; + text-align: center; + margin-right: 1em; + } + + .avatar-alt { + flex: 0 auto; + margin-left: 28px; + font-size: 12px; + min-width: 20px; + min-height: 20px; + line-height: 20px; + } + + .avatar { + flex: 0 auto; + width: 48px; + height: 48px; + font-size: 14px; + line-height: 48px; + } + + .actions { + display: flex; + align-items: baseline; + + .checkbox { + margin-right: 1em; + flex: 1; + } + } + + .separator { + margin: 1em; + border-bottom: 1px solid; + border-color: var(--border); + } + + .btn { + min-width: 3em; + } + } } .underlay-preview { @@ -148,4 +247,4 @@ export default {} left: 10px; right: 10px; } -</style> + </style> diff --git a/src/components/settings_modal/tabs/theme_tab/theme_tab.js b/src/components/settings_modal/tabs/theme_tab/theme_tab.js @@ -1,7 +1,8 @@ import { rgb2hex, hex2rgb, - getContrastRatioLayers + getContrastRatioLayers, + relativeLuminance } from 'src/services/color_convert/color_convert.js' import { getThemes @@ -23,10 +24,17 @@ import { generateShadows, generateRadii, generateFonts, - composePreset, shadows2to3, colors2to3 } 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 RangeInput from 'src/components/range_input/range_input.vue' import OpacityInput from 'src/components/opacity_input/opacity_input.vue' @@ -62,6 +70,7 @@ const colorConvert = (color) => { export default { data () { return { + themeV3Preview: [], themeImporter: newImporter({ validator: this.importValidator, onImport: this.onImport, @@ -78,10 +87,7 @@ export default { tempImportFile: undefined, engineVersion: 0, - previewShadows: {}, - previewColors: {}, - previewRadii: {}, - previewFonts: {}, + previewTheme: {}, shadowsInvalid: true, colorsInvalid: true, @@ -232,13 +238,6 @@ export default { 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 previewContrast () { try { @@ -306,14 +305,6 @@ export default { return {} } }, - previewRules () { - if (!this.preview.rules) return '' - return [ - ...Object.values(this.preview.rules), - 'color: var(--text)', - 'font-family: var(--interfaceFont, sans-serif)' - ].join(';') - }, shadowsAvailable () { return Object.keys(DEFAULT_SHADOWS).sort() }, @@ -511,17 +502,14 @@ export default { } }, setCustomTheme () { - this.$store.dispatch('setOption', { - name: 'customTheme', - value: { + this.$store.dispatch('setThemeV2', { + customTheme: { + ignore: true, themeFileVersion: this.selectedVersion, themeEngineVersion: CURRENT_VERSION, ...this.previewTheme - } - }) - this.$store.dispatch('setOption', { - name: 'customThemeSource', - value: { + }, + customThemeSource: { themeFileVersion: this.selectedVersion, themeEngineVersion: CURRENT_VERSION, shadows: this.shadowsLocal, @@ -532,16 +520,24 @@ export default { } }) }, - updatePreviewColorsAndShadows () { - this.previewColors = generateColors({ + updatePreviewColors () { + const result = generateColors({ opacity: this.currentOpacity, colors: this.currentColors }) - this.previewShadows = generateShadows( - { shadows: this.shadowsLocal, opacity: this.previewTheme.opacity, themeEngineVersion: this.engineVersion }, - this.previewColors.theme.colors, - this.previewColors.mod - ) + this.previewTheme.colors = result.theme.colors + this.previewTheme.opacity = result.theme.opacity + }, + 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() }, exportTheme () { this.themeExporter.exportData() }, @@ -610,7 +606,7 @@ export default { normalizeLocalState (theme, version = 0, source, forceSource = false) { let input if (typeof source !== 'undefined') { - if (forceSource || source.themeEngineVersion === CURRENT_VERSION) { + if (forceSource || source?.themeEngineVersion === CURRENT_VERSION) { input = source version = source.themeEngineVersion } else { @@ -692,6 +688,8 @@ export default { } else { this.shadowsLocal = shadows } + this.updatePreviewColors() + this.updatePreviewShadows() this.shadowSelected = this.shadowsAvailable[0] } @@ -699,12 +697,25 @@ export default { this.clearFonts() 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: { currentRadii () { try { - this.previewRadii = generateRadii({ radii: this.currentRadii }) + this.previewTheme.radii = generateRadii({ radii: this.currentRadii }).theme.radii this.radiiInvalid = false } catch (e) { this.radiiInvalid = true @@ -713,9 +724,8 @@ export default { }, shadowsLocal: { handler () { - if (Object.getOwnPropertyNames(this.previewColors).length === 1) return try { - this.updatePreviewColorsAndShadows() + this.updatePreviewShadows() this.shadowsInvalid = false } catch (e) { this.shadowsInvalid = true @@ -727,7 +737,7 @@ export default { fontsLocal: { handler () { try { - this.previewFonts = generateFonts({ fonts: this.fontsLocal }) + this.previewTheme.fonts = generateFonts({ fonts: this.fontsLocal }).theme.fonts this.fontsInvalid = false } catch (e) { this.fontsInvalid = true @@ -738,18 +748,16 @@ export default { }, currentColors () { try { - this.updatePreviewColorsAndShadows() + this.updatePreviewColors() this.colorsInvalid = false - this.shadowsInvalid = false } catch (e) { this.colorsInvalid = true - this.shadowsInvalid = true console.warn(e) } }, currentOpacity () { try { - this.updatePreviewColorsAndShadows() + this.updatePreviewColors() } catch (e) { console.warn(e) } diff --git a/src/components/settings_modal/tabs/theme_tab/theme_tab.scss b/src/components/settings_modal/tabs/theme_tab/theme_tab.scss @@ -1,4 +1,9 @@ .theme-tab { + .deprecation-warning { + padding: 0.5em; + margin: 2em; + } + padding-bottom: 2em; .preset-switcher { @@ -10,6 +15,10 @@ margin-right: 0.25em; } + .btn-group .btn { + margin: 0; + } + .style-control { display: flex; 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 { flex-basis: auto; } @@ -310,10 +218,6 @@ max-width: 50em; } - .theme-preview-content { - padding: 20px; - } - .theme-warning { display: flex; align-items: baseline; diff --git a/src/components/settings_modal/tabs/theme_tab/theme_tab.vue b/src/components/settings_modal/tabs/theme_tab/theme_tab.vue @@ -1,5 +1,8 @@ <template> <div class="theme-tab"> + <div class="alert warning deprecation-warning"> + {{ $t("settings.style.themes2_outdated") }} + </div> <div class="presets-container"> <div class="save-load"> <div @@ -120,7 +123,19 @@ </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> <tab-switcher key="style-tweak"> @@ -156,7 +171,7 @@ <OpacityInput v-model="bgOpacityLocal" name="bgOpacity" - :fallback="previewTheme.opacity.bg" + :fallback="previewTheme.opacity?.bg" /> <ColorInput v-model="textColorLocal" @@ -167,14 +182,14 @@ <ColorInput v-model="accentColorLocal" name="accentColor" - :fallback="previewTheme.colors.link" + :fallback="previewTheme.colors?.link" :label="$t('settings.accent')" :show-optional-tickbox="typeof linkColorLocal !== 'undefined'" /> <ColorInput v-model="linkColorLocal" name="linkColor" - :fallback="previewTheme.colors.accent" + :fallback="previewTheme.colors?.accent" :label="$t('settings.links')" :show-optional-tickbox="typeof accentColorLocal !== 'undefined'" /> @@ -190,13 +205,13 @@ v-model="fgTextColorLocal" name="fgTextColor" :label="$t('settings.text')" - :fallback="previewTheme.colors.fgText" + :fallback="previewTheme.colors?.fgText" /> <ColorInput v-model="fgLinkColorLocal" name="fgLinkColor" :label="$t('settings.links')" - :fallback="previewTheme.colors.fgLink" + :fallback="previewTheme.colors?.fgLink" /> <p>{{ $t('settings.style.common_colors.foreground_hint') }}</p> </div> @@ -256,14 +271,14 @@ <ColorInput v-model="postLinkColorLocal" name="postLinkColor" - :fallback="previewTheme.colors.accent" + :fallback="previewTheme.colors?.accent" :label="$t('settings.links')" /> <ContrastRatio :contrast="previewContrast.postLink" /> <ColorInput v-model="postGreentextColorLocal" name="postGreentextColor" - :fallback="previewTheme.colors.cGreen" + :fallback="previewTheme.colors?.cGreen" :label="$t('settings.greentext')" /> <ContrastRatio :contrast="previewContrast.postGreentext" /> @@ -272,13 +287,13 @@ v-model="alertErrorColorLocal" name="alertError" :label="$t('settings.style.advanced_colors.alert_error')" - :fallback="previewTheme.colors.alertError" + :fallback="previewTheme.colors?.alertError" /> <ColorInput v-model="alertErrorTextColorLocal" name="alertErrorText" :label="$t('settings.text')" - :fallback="previewTheme.colors.alertErrorText" + :fallback="previewTheme.colors?.alertErrorText" /> <ContrastRatio :contrast="previewContrast.alertErrorText" @@ -288,13 +303,13 @@ v-model="alertWarningColorLocal" name="alertWarning" :label="$t('settings.style.advanced_colors.alert_warning')" - :fallback="previewTheme.colors.alertWarning" + :fallback="previewTheme.colors?.alertWarning" /> <ColorInput v-model="alertWarningTextColorLocal" name="alertWarningText" :label="$t('settings.text')" - :fallback="previewTheme.colors.alertWarningText" + :fallback="previewTheme.colors?.alertWarningText" /> <ContrastRatio :contrast="previewContrast.alertWarningText" @@ -304,13 +319,13 @@ v-model="alertNeutralColorLocal" name="alertNeutral" :label="$t('settings.style.advanced_colors.alert_neutral')" - :fallback="previewTheme.colors.alertNeutral" + :fallback="previewTheme.colors?.alertNeutral" /> <ColorInput v-model="alertNeutralTextColorLocal" name="alertNeutralText" :label="$t('settings.text')" - :fallback="previewTheme.colors.alertNeutralText" + :fallback="previewTheme.colors?.alertNeutralText" /> <ContrastRatio :contrast="previewContrast.alertNeutralText" @@ -319,7 +334,7 @@ <OpacityInput v-model="alertOpacityLocal" name="alertOpacity" - :fallback="previewTheme.opacity.alert" + :fallback="previewTheme.opacity?.alert" /> </div> <div class="color-item"> @@ -328,13 +343,13 @@ v-model="badgeNotificationColorLocal" name="badgeNotification" :label="$t('settings.style.advanced_colors.badge_notification')" - :fallback="previewTheme.colors.badgeNotification" + :fallback="previewTheme.colors?.badgeNotification" /> <ColorInput v-model="badgeNotificationTextColorLocal" name="badgeNotificationText" :label="$t('settings.text')" - :fallback="previewTheme.colors.badgeNotificationText" + :fallback="previewTheme.colors?.badgeNotificationText" /> <ContrastRatio :contrast="previewContrast.badgeNotificationText" @@ -346,19 +361,19 @@ <ColorInput v-model="panelColorLocal" name="panelColor" - :fallback="previewTheme.colors.panel" + :fallback="previewTheme.colors?.panel" :label="$t('settings.background')" /> <OpacityInput v-model="panelOpacityLocal" name="panelOpacity" - :fallback="previewTheme.opacity.panel" + :fallback="previewTheme.opacity?.panel" :disabled="panelColorLocal === 'transparent'" /> <ColorInput v-model="panelTextColorLocal" name="panelTextColor" - :fallback="previewTheme.colors.panelText" + :fallback="previewTheme.colors?.panelText" :label="$t('settings.text')" /> <ContrastRatio @@ -368,7 +383,7 @@ <ColorInput v-model="panelLinkColorLocal" name="panelLinkColor" - :fallback="previewTheme.colors.panelLink" + :fallback="previewTheme.colors?.panelLink" :label="$t('settings.links')" /> <ContrastRatio @@ -381,20 +396,20 @@ <ColorInput v-model="topBarColorLocal" name="topBarColor" - :fallback="previewTheme.colors.topBar" + :fallback="previewTheme.colors?.topBar" :label="$t('settings.background')" /> <ColorInput v-model="topBarTextColorLocal" name="topBarTextColor" - :fallback="previewTheme.colors.topBarText" + :fallback="previewTheme.colors?.topBarText" :label="$t('settings.text')" /> <ContrastRatio :contrast="previewContrast.topBarText" /> <ColorInput v-model="topBarLinkColorLocal" name="topBarLinkColor" - :fallback="previewTheme.colors.topBarLink" + :fallback="previewTheme.colors?.topBarLink" :label="$t('settings.links')" /> <ContrastRatio :contrast="previewContrast.topBarLink" /> @@ -404,19 +419,19 @@ <ColorInput v-model="inputColorLocal" name="inputColor" - :fallback="previewTheme.colors.input" + :fallback="previewTheme.colors?.input" :label="$t('settings.background')" /> <OpacityInput v-model="inputOpacityLocal" name="inputOpacity" - :fallback="previewTheme.opacity.input" + :fallback="previewTheme.opacity?.input" :disabled="inputColorLocal === 'transparent'" /> <ColorInput v-model="inputTextColorLocal" name="inputTextColor" - :fallback="previewTheme.colors.inputText" + :fallback="previewTheme.colors?.inputText" :label="$t('settings.text')" /> <ContrastRatio :contrast="previewContrast.inputText" /> @@ -426,33 +441,33 @@ <ColorInput v-model="btnColorLocal" name="btnColor" - :fallback="previewTheme.colors.btn" + :fallback="previewTheme.colors?.btn" :label="$t('settings.background')" /> <OpacityInput v-model="btnOpacityLocal" name="btnOpacity" - :fallback="previewTheme.opacity.btn" + :fallback="previewTheme.opacity?.btn" :disabled="btnColorLocal === 'transparent'" /> <ColorInput v-model="btnTextColorLocal" name="btnTextColor" - :fallback="previewTheme.colors.btnText" + :fallback="previewTheme.colors?.btnText" :label="$t('settings.text')" /> <ContrastRatio :contrast="previewContrast.btnText" /> <ColorInput v-model="btnPanelTextColorLocal" name="btnPanelTextColor" - :fallback="previewTheme.colors.btnPanelText" + :fallback="previewTheme.colors?.btnPanelText" :label="$t('settings.style.advanced_colors.panel_header')" /> <ContrastRatio :contrast="previewContrast.btnPanelText" /> <ColorInput v-model="btnTopBarTextColorLocal" name="btnTopBarTextColor" - :fallback="previewTheme.colors.btnTopBarText" + :fallback="previewTheme.colors?.btnTopBarText" :label="$t('settings.style.advanced_colors.top_bar')" /> <ContrastRatio :contrast="previewContrast.btnTopBarText" /> @@ -460,27 +475,27 @@ <ColorInput v-model="btnPressedColorLocal" name="btnPressedColor" - :fallback="previewTheme.colors.btnPressed" + :fallback="previewTheme.colors?.btnPressed" :label="$t('settings.background')" /> <ColorInput v-model="btnPressedTextColorLocal" name="btnPressedTextColor" - :fallback="previewTheme.colors.btnPressedText" + :fallback="previewTheme.colors?.btnPressedText" :label="$t('settings.text')" /> <ContrastRatio :contrast="previewContrast.btnPressedText" /> <ColorInput v-model="btnPressedPanelTextColorLocal" name="btnPressedPanelTextColor" - :fallback="previewTheme.colors.btnPressedPanelText" + :fallback="previewTheme.colors?.btnPressedPanelText" :label="$t('settings.style.advanced_colors.panel_header')" /> <ContrastRatio :contrast="previewContrast.btnPressedPanelText" /> <ColorInput v-model="btnPressedTopBarTextColorLocal" name="btnPressedTopBarTextColor" - :fallback="previewTheme.colors.btnPressedTopBarText" + :fallback="previewTheme.colors?.btnPressedTopBarText" :label="$t('settings.style.advanced_colors.top_bar')" /> <ContrastRatio :contrast="previewContrast.btnPressedTopBarText" /> @@ -488,52 +503,52 @@ <ColorInput v-model="btnDisabledColorLocal" name="btnDisabledColor" - :fallback="previewTheme.colors.btnDisabled" + :fallback="previewTheme.colors?.btnDisabled" :label="$t('settings.background')" /> <ColorInput v-model="btnDisabledTextColorLocal" name="btnDisabledTextColor" - :fallback="previewTheme.colors.btnDisabledText" + :fallback="previewTheme.colors?.btnDisabledText" :label="$t('settings.text')" /> <ColorInput v-model="btnDisabledPanelTextColorLocal" name="btnDisabledPanelTextColor" - :fallback="previewTheme.colors.btnDisabledPanelText" + :fallback="previewTheme.colors?.btnDisabledPanelText" :label="$t('settings.style.advanced_colors.panel_header')" /> <ColorInput v-model="btnDisabledTopBarTextColorLocal" name="btnDisabledTopBarTextColor" - :fallback="previewTheme.colors.btnDisabledTopBarText" + :fallback="previewTheme.colors?.btnDisabledTopBarText" :label="$t('settings.style.advanced_colors.top_bar')" /> <h5>{{ $t('settings.style.advanced_colors.toggled') }}</h5> <ColorInput v-model="btnToggledColorLocal" name="btnToggledColor" - :fallback="previewTheme.colors.btnToggled" + :fallback="previewTheme.colors?.btnToggled" :label="$t('settings.background')" /> <ColorInput v-model="btnToggledTextColorLocal" name="btnToggledTextColor" - :fallback="previewTheme.colors.btnToggledText" + :fallback="previewTheme.colors?.btnToggledText" :label="$t('settings.text')" /> <ContrastRatio :contrast="previewContrast.btnToggledText" /> <ColorInput v-model="btnToggledPanelTextColorLocal" name="btnToggledPanelTextColor" - :fallback="previewTheme.colors.btnToggledPanelText" + :fallback="previewTheme.colors?.btnToggledPanelText" :label="$t('settings.style.advanced_colors.panel_header')" /> <ContrastRatio :contrast="previewContrast.btnToggledPanelText" /> <ColorInput v-model="btnToggledTopBarTextColorLocal" name="btnToggledTopBarTextColor" - :fallback="previewTheme.colors.btnToggledTopBarText" + :fallback="previewTheme.colors?.btnToggledTopBarText" :label="$t('settings.style.advanced_colors.top_bar')" /> <ContrastRatio :contrast="previewContrast.btnToggledTopBarText" /> @@ -543,20 +558,20 @@ <ColorInput v-model="tabColorLocal" name="tabColor" - :fallback="previewTheme.colors.tab" + :fallback="previewTheme.colors?.tab" :label="$t('settings.background')" /> <ColorInput v-model="tabTextColorLocal" name="tabTextColor" - :fallback="previewTheme.colors.tabText" + :fallback="previewTheme.colors?.tabText" :label="$t('settings.text')" /> <ContrastRatio :contrast="previewContrast.tabText" /> <ColorInput v-model="tabActiveTextColorLocal" name="tabActiveTextColor" - :fallback="previewTheme.colors.tabActiveText" + :fallback="previewTheme.colors?.tabActiveText" :label="$t('settings.text')" /> <ContrastRatio :contrast="previewContrast.tabActiveText" /> @@ -566,13 +581,13 @@ <ColorInput v-model="borderColorLocal" name="borderColor" - :fallback="previewTheme.colors.border" + :fallback="previewTheme.colors?.border" :label="$t('settings.style.common.color')" /> <OpacityInput v-model="borderOpacityLocal" name="borderOpacity" - :fallback="previewTheme.opacity.border" + :fallback="previewTheme.opacity?.border" :disabled="borderColorLocal === 'transparent'" /> </div> @@ -581,25 +596,25 @@ <ColorInput v-model="faintColorLocal" name="faintColor" - :fallback="previewTheme.colors.faint" + :fallback="previewTheme.colors?.faint" :label="$t('settings.text')" /> <ColorInput v-model="faintLinkColorLocal" name="faintLinkColor" - :fallback="previewTheme.colors.faintLink" + :fallback="previewTheme.colors?.faintLink" :label="$t('settings.links')" /> <ColorInput v-model="panelFaintColorLocal" name="panelFaintColor" - :fallback="previewTheme.colors.panelFaint" + :fallback="previewTheme.colors?.panelFaint" :label="$t('settings.style.advanced_colors.panel_header')" /> <OpacityInput v-model="faintOpacityLocal" name="faintOpacity" - :fallback="previewTheme.opacity.faint" + :fallback="previewTheme.opacity?.faint" /> </div> <div class="color-item"> @@ -608,12 +623,12 @@ v-model="underlayColorLocal" name="underlay" :label="$t('settings.style.advanced_colors.underlay')" - :fallback="previewTheme.colors.underlay" + :fallback="previewTheme.colors?.underlay" /> <OpacityInput v-model="underlayOpacityLocal" name="underlayOpacity" - :fallback="previewTheme.opacity.underlay" + :fallback="previewTheme.opacity?.underlay" :disabled="underlayOpacityLocal === 'transparent'" /> </div> @@ -623,7 +638,7 @@ v-model="wallpaperColorLocal" name="wallpaper" :label="$t('settings.style.advanced_colors.wallpaper')" - :fallback="previewTheme.colors.wallpaper" + :fallback="previewTheme.colors?.wallpaper" /> </div> <div class="color-item"> @@ -632,13 +647,13 @@ v-model="pollColorLocal" name="poll" :label="$t('settings.background')" - :fallback="previewTheme.colors.poll" + :fallback="previewTheme.colors?.poll" /> <ColorInput v-model="pollTextColorLocal" name="pollText" :label="$t('settings.text')" - :fallback="previewTheme.colors.pollText" + :fallback="previewTheme.colors?.pollText" /> </div> <div class="color-item"> @@ -647,7 +662,7 @@ v-model="iconColorLocal" name="icon" :label="$t('settings.style.advanced_colors.icons')" - :fallback="previewTheme.colors.icon" + :fallback="previewTheme.colors?.icon" /> </div> <div class="color-item"> @@ -656,20 +671,20 @@ v-model="highlightColorLocal" name="highlight" :label="$t('settings.background')" - :fallback="previewTheme.colors.highlight" + :fallback="previewTheme.colors?.highlight" /> <ColorInput v-model="highlightTextColorLocal" name="highlightText" :label="$t('settings.text')" - :fallback="previewTheme.colors.highlightText" + :fallback="previewTheme.colors?.highlightText" /> <ContrastRatio :contrast="previewContrast.highlightText" /> <ColorInput v-model="highlightLinkColorLocal" name="highlightLink" :label="$t('settings.links')" - :fallback="previewTheme.colors.highlightLink" + :fallback="previewTheme.colors?.highlightLink" /> <ContrastRatio :contrast="previewContrast.highlightLink" /> </div> @@ -679,26 +694,26 @@ v-model="popoverColorLocal" name="popover" :label="$t('settings.background')" - :fallback="previewTheme.colors.popover" + :fallback="previewTheme.colors?.popover" /> <OpacityInput v-model="popoverOpacityLocal" name="popoverOpacity" - :fallback="previewTheme.opacity.popover" + :fallback="previewTheme.opacity?.popover" :disabled="popoverOpacityLocal === 'transparent'" /> <ColorInput v-model="popoverTextColorLocal" name="popoverText" :label="$t('settings.text')" - :fallback="previewTheme.colors.popoverText" + :fallback="previewTheme.colors?.popoverText" /> <ContrastRatio :contrast="previewContrast.popoverText" /> <ColorInput v-model="popoverLinkColorLocal" name="popoverLink" :label="$t('settings.links')" - :fallback="previewTheme.colors.popoverLink" + :fallback="previewTheme.colors?.popoverLink" /> <ContrastRatio :contrast="previewContrast.popoverLink" /> </div> @@ -708,20 +723,20 @@ v-model="selectedPostColorLocal" name="selectedPost" :label="$t('settings.background')" - :fallback="previewTheme.colors.selectedPost" + :fallback="previewTheme.colors?.selectedPost" /> <ColorInput v-model="selectedPostTextColorLocal" name="selectedPostText" :label="$t('settings.text')" - :fallback="previewTheme.colors.selectedPostText" + :fallback="previewTheme.colors?.selectedPostText" /> <ContrastRatio :contrast="previewContrast.selectedPostText" /> <ColorInput v-model="selectedPostLinkColorLocal" name="selectedPostLink" :label="$t('settings.links')" - :fallback="previewTheme.colors.selectedPostLink" + :fallback="previewTheme.colors?.selectedPostLink" /> <ContrastRatio :contrast="previewContrast.selectedPostLink" /> </div> @@ -731,20 +746,20 @@ v-model="selectedMenuColorLocal" name="selectedMenu" :label="$t('settings.background')" - :fallback="previewTheme.colors.selectedMenu" + :fallback="previewTheme.colors?.selectedMenu" /> <ColorInput v-model="selectedMenuTextColorLocal" name="selectedMenuText" :label="$t('settings.text')" - :fallback="previewTheme.colors.selectedMenuText" + :fallback="previewTheme.colors?.selectedMenuText" /> <ContrastRatio :contrast="previewContrast.selectedMenuText" /> <ColorInput v-model="selectedMenuLinkColorLocal" name="selectedMenuLink" :label="$t('settings.links')" - :fallback="previewTheme.colors.selectedMenuLink" + :fallback="previewTheme.colors?.selectedMenuLink" /> <ContrastRatio :contrast="previewContrast.selectedMenuLink" /> </div> @@ -753,57 +768,57 @@ <ColorInput v-model="chatBgColorLocal" name="chatBgColor" - :fallback="previewTheme.colors.bg" + :fallback="previewTheme.colors?.bg" :label="$t('settings.background')" /> <h5>{{ $t('settings.style.advanced_colors.chat.incoming') }}</h5> <ColorInput v-model="chatMessageIncomingBgColorLocal" name="chatMessageIncomingBgColor" - :fallback="previewTheme.colors.bg" + :fallback="previewTheme.colors?.bg" :label="$t('settings.background')" /> <ColorInput v-model="chatMessageIncomingTextColorLocal" name="chatMessageIncomingTextColor" - :fallback="previewTheme.colors.text" + :fallback="previewTheme.colors?.text" :label="$t('settings.text')" /> <ColorInput v-model="chatMessageIncomingLinkColorLocal" name="chatMessageIncomingLinkColor" - :fallback="previewTheme.colors.link" + :fallback="previewTheme.colors?.link" :label="$t('settings.links')" /> <ColorInput v-model="chatMessageIncomingBorderColorLocal" name="chatMessageIncomingBorderLinkColor" - :fallback="previewTheme.colors.fg" + :fallback="previewTheme.colors?.fg" :label="$t('settings.style.advanced_colors.chat.border')" /> <h5>{{ $t('settings.style.advanced_colors.chat.outgoing') }}</h5> <ColorInput v-model="chatMessageOutgoingBgColorLocal" name="chatMessageOutgoingBgColor" - :fallback="previewTheme.colors.bg" + :fallback="previewTheme.colors?.bg" :label="$t('settings.background')" /> <ColorInput v-model="chatMessageOutgoingTextColorLocal" name="chatMessageOutgoingTextColor" - :fallback="previewTheme.colors.text" + :fallback="previewTheme.colors?.text" :label="$t('settings.text')" /> <ColorInput v-model="chatMessageOutgoingLinkColorLocal" name="chatMessageOutgoingLinkColor" - :fallback="previewTheme.colors.link" + :fallback="previewTheme.colors?.link" :label="$t('settings.links')" /> <ColorInput v-model="chatMessageOutgoingBorderColorLocal" name="chatMessageOutgoingBorderLinkColor" - :fallback="previewTheme.colors.bg" + :fallback="previewTheme.colors?.bg" :label="$t('settings.style.advanced_colors.chat.border')" /> </div> @@ -826,7 +841,7 @@ v-model="btnRadiusLocal" name="btnRadius" :label="$t('settings.btnRadius')" - :fallback="previewTheme.radii.btn" + :fallback="previewTheme.radii?.btn" max="16" hard-min="0" /> @@ -834,7 +849,7 @@ v-model="inputRadiusLocal" name="inputRadius" :label="$t('settings.inputRadius')" - :fallback="previewTheme.radii.input" + :fallback="previewTheme.radii?.input" max="9" hard-min="0" /> @@ -842,7 +857,7 @@ v-model="checkboxRadiusLocal" name="checkboxRadius" :label="$t('settings.checkboxRadius')" - :fallback="previewTheme.radii.checkbox" + :fallback="previewTheme.radii?.checkbox" max="16" hard-min="0" /> @@ -850,7 +865,7 @@ v-model="panelRadiusLocal" name="panelRadius" :label="$t('settings.panelRadius')" - :fallback="previewTheme.radii.panel" + :fallback="previewTheme.radii?.panel" max="50" hard-min="0" /> @@ -858,7 +873,7 @@ v-model="avatarRadiusLocal" name="avatarRadius" :label="$t('settings.avatarRadius')" - :fallback="previewTheme.radii.avatar" + :fallback="previewTheme.radii?.avatar" max="28" hard-min="0" /> @@ -866,7 +881,7 @@ v-model="avatarAltRadiusLocal" name="avatarAltRadius" :label="$t('settings.avatarAltRadius')" - :fallback="previewTheme.radii.avatarAlt" + :fallback="previewTheme.radii?.avatarAlt" max="28" hard-min="0" /> @@ -874,7 +889,7 @@ v-model="attachmentRadiusLocal" name="attachmentRadius" :label="$t('settings.attachmentRadius')" - :fallback="previewTheme.radii.attachment" + :fallback="previewTheme.radii?.attachment" max="50" hard-min="0" /> @@ -882,7 +897,7 @@ v-model="tooltipRadiusLocal" name="tooltipRadius" :label="$t('settings.tooltipRadius')" - :fallback="previewTheme.radii.tooltip" + :fallback="previewTheme.radii?.tooltip" max="50" hard-min="0" /> @@ -890,7 +905,7 @@ v-model="chatMessageRadiusLocal" name="chatMessageRadius" :label="$t('settings.chatMessageRadius')" - :fallback="previewTheme.radii.chatMessage || 2" + :fallback="previewTheme.radii?.chatMessage || 2" max="50" hard-min="0" /> @@ -996,26 +1011,26 @@ v-model="fontsLocal.interface" name="ui" :label="$t('settings.style.fonts.components.interface')" - :fallback="previewTheme.fonts.interface" + :fallback="previewTheme.fonts?.interface" no-inherit="1" /> <FontControl v-model="fontsLocal.input" name="input" :label="$t('settings.style.fonts.components.input')" - :fallback="previewTheme.fonts.input" + :fallback="previewTheme.fonts?.input" /> <FontControl v-model="fontsLocal.post" name="post" :label="$t('settings.style.fonts.components.post')" - :fallback="previewTheme.fonts.post" + :fallback="previewTheme.fonts?.post" /> <FontControl v-model="fontsLocal.postCode" name="postCode" :label="$t('settings.style.fonts.components.postCode')" - :fallback="previewTheme.fonts.postCode" + :fallback="previewTheme.fonts?.postCode" /> </div> </tab-switcher> diff --git a/src/components/status/post.style.js b/src/components/status/post.style.js @@ -17,6 +17,15 @@ export default { 'Attachment', 'PollGraph' ], + validInnerComponentsLite: [ + 'Text', + 'Link', + 'Icon', + 'Border', + 'ButtonUnstyled', + 'RichContent', + 'Avatar' + ], defaultRules: [ { directives: { diff --git a/src/i18n/en.json b/src/i18n/en.json @@ -376,6 +376,20 @@ "enter_current_password_to_confirm": "Enter your current password to confirm your identity", "post_look_feel": "Posts Look & Feel", "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": { "otp": "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.", "more_settings": "More settings", "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": { "keep_color": "Keep colors", "keep_shadows": "Keep shadows", @@ -852,7 +902,7 @@ "interface": "Interface", "input": "Input fields", "post": "Post text", - "postCode": "Monospaced text in a post (rich text)" + "monospace": "Monospaced text" }, "family": "Font name", "size": "Size (in px)", diff --git a/src/modules/config.js b/src/modules/config.js @@ -1,10 +1,21 @@ import Cookies from 'js-cookie' -import { setPreset, applyTheme, applyConfig } from '../services/style_setter/style_setter.js' +import { applyConfig } from '../services/style_setter/style_setter.js' import messages from '../i18n/messages' import { set } from 'lodash' import localeService from '../services/locale/locale.service.js' const BACKEND_LANGUAGE_COOKIE_NAME = 'userLanguage' +const APPEARANCE_SETTINGS_KEYS = new Set([ + 'sidebarColumnWidth', + 'contentColumnWidth', + 'notifsColumnWidth', + 'textSize', + 'navbarSize', + 'panelHeaderSize', + 'forcedRoundness', + 'emojiSize', + 'emojiReactionsScale' +]) const browserLocale = (window.navigator.language || 'en').split('-')[0] @@ -24,11 +35,30 @@ export const multiChoiceProperties = [ export const defaultState = { expertLevel: 0, // used to track which settings to show and hide - colors: {}, - theme: undefined, - customTheme: undefined, - customThemeSource: undefined, - forceThemeRecompilation: false, + + // Theme stuff + theme: undefined, // Very old theme store, stores preset name, still in use + + // V1 + colors: {}, // VERY old theme store, just colors of V1, probably not even used anymore + + // V2 + customTheme: undefined, // "snapshot", previously was used as actual theme store for V2 so it's still used in case of PleromaFE downgrade event. + customThemeSource: undefined, // "source", stores original theme data + + // V3 + themeDebug: false, // debug mode that uses computed backgrounds instead of real ones to debug contrast functions + forceThemeRecompilation: false, // flag that forces recompilation on boot even if cache exists + theme3hacks: { // Hacks, user overrides that are independent of theme used + underlay: 'none', + fonts: { + interface: undefined, + input: undefined, + post: undefined, + monospace: undefined + } + }, + hideISP: false, hideInstanceWallpaper: false, hideShoutbox: false, @@ -117,7 +147,12 @@ export const defaultState = { sidebarColumnWidth: '25rem', contentColumnWidth: '45rem', notifsColumnWidth: '25rem', - emojiReactionsScale: 1.0, + emojiReactionsScale: undefined, + textSize: undefined, // instance default + emojiSize: undefined, // instance default + navbarSize: undefined, // instance default + panelHeaderSize: undefined, // instance default + forcedRoundness: undefined, // instance default navbarColumnStretch: false, greentext: undefined, // instance default useAtIcon: undefined, // instance default @@ -175,6 +210,10 @@ const config = { } }, mutations: { + setOptionTemporarily (state, { name, value }) { + set(state, name, value) + applyConfig(state) + }, setOption (state, { name, value }) { set(state, name, value) }, @@ -205,6 +244,37 @@ const config = { setHighlight ({ commit, dispatch }, { user, color, type }) { commit('setHighlight', { user, color, type }) }, + setOptionTemporarily ({ commit, dispatch, state, rootState }, { name, value }) { + if (rootState.interface.temporaryChangesTimeoutId !== null) { + console.warn('Can\'t track more than one temporary change') + return + } + const oldValue = state[name] + + commit('setOptionTemporarily', { name, value }) + + const confirm = () => { + dispatch('setOption', { name, value }) + commit('clearTemporaryChanges') + } + + const revert = () => { + commit('setOptionTemporarily', { name, value: oldValue }) + commit('clearTemporaryChanges') + } + + commit('setTemporaryChanges', { + timeoutId: setTimeout(revert, 10000), + confirm, + revert + }) + }, + setThemeV2 ({ commit, dispatch }, { customTheme, customThemeSource }) { + commit('setOption', { name: 'theme', value: 'custom' }) + commit('setOption', { name: 'customTheme', value: customTheme }) + commit('setOption', { name: 'customThemeSource', value: customThemeSource }) + dispatch('setTheme', { themeData: customThemeSource, recompile: true }) + }, setOption ({ commit, dispatch, state }, { name, value }) { const exceptions = new Set([ 'useStreamingApi' @@ -222,24 +292,26 @@ const config = { dispatch('disableMastoSockets') dispatch('setOption', { name: 'useStreamingApi', value: false }) }) + break } } } else { commit('setOption', { name, value }) + if (APPEARANCE_SETTINGS_KEYS.has(name)) { + applyConfig(state) + } + if (name.startsWith('theme3hacks')) { + dispatch('setTheme', { recompile: true }) + } switch (name) { case 'theme': - setPreset(value) + if (value === 'custom') break + dispatch('setTheme', { themeName: value, recompile: true, saveData: true }) break - case 'sidebarColumnWidth': - case 'contentColumnWidth': - case 'notifsColumnWidth': - case 'emojiReactionsScale': - applyConfig(state) - break - case 'customTheme': - case 'customThemeSource': - applyTheme(value) + case 'themeDebug': { + dispatch('setTheme', { recompile: true }) break + } case 'interfaceLanguage': messages.setLanguage(this.getters.i18n, value) dispatch('loadUnicodeEmojiData', value) diff --git a/src/modules/instance.js b/src/modules/instance.js @@ -1,5 +1,3 @@ -import { getPreset, applyTheme } from '../services/style_setter/style_setter.js' -import { CURRENT_VERSION } from '../services/theme_data/theme_data.service.js' import apiService from '../services/api/api.service.js' import { instanceDefaultProperties } from './config.js' import { langCodeToCldrName, ensureFinalFallback } from '../i18n/languages.js' @@ -44,7 +42,7 @@ const defaultState = { registrationOpen: true, server: 'http://localhost:4040/', textlimit: 5000, - themeData: undefined, + themeData: undefined, // used for theme editor v2 vapidPublicKey: undefined, // Stuff from static/config.json @@ -98,6 +96,13 @@ const defaultState = { sidebarRight: false, subjectLineBehavior: 'email', theme: 'pleroma-dark', + emojiReactionsScale: 0.5, + textSize: '14px', + emojiSize: '2.2rem', + navbarSize: '3.5rem', + panelHeaderSize: '3.2rem', + forcedRoundness: -1, + fontsOverride: {}, virtualScrolling: true, sensitiveByDefault: false, conversationDisplay: 'linear', @@ -279,9 +284,6 @@ const instance = { dispatch('initializeSocket') } break - case 'theme': - dispatch('setTheme', value) - break } }, async getStaticEmoji ({ commit }) { @@ -370,27 +372,6 @@ const instance = { console.warn(e) } }, - - setTheme ({ commit, rootState }, themeName) { - commit('setInstanceOption', { name: 'theme', value: themeName }) - getPreset(themeName) - .then(themeData => { - commit('setInstanceOption', { name: 'themeData', value: themeData }) - // No need to apply theme if there's user theme already - const { customTheme } = rootState.config - const { themeApplied } = rootState.interface - if (customTheme || themeApplied) return - - // New theme presets don't have 'theme' property, they use 'source' - const themeSource = themeData.source - if (!themeData.theme || (themeSource && themeSource.themeEngineVersion === CURRENT_VERSION)) { - applyTheme(themeSource) - } else { - applyTheme(themeData.theme) - } - commit('setThemeApplied') - }) - }, fetchEmoji ({ dispatch, state }) { if (!state.customEmojiFetched) { state.customEmojiFetched = true diff --git a/src/modules/interface.js b/src/modules/interface.js @@ -1,5 +1,13 @@ +import { getPreset, applyTheme, tryLoadCache } from '../services/style_setter/style_setter.js' +import { CURRENT_VERSION, generatePreset } from 'src/services/theme_data/theme_data.service.js' +import { convertTheme2To3 } from 'src/services/theme_data/theme2_to_theme3.js' + const defaultState = { + localFonts: null, themeApplied: false, + temporaryChangesTimeoutId: null, // used for temporary options that revert after a timeout + temporaryChangesConfirm: () => {}, // used for applying temporary options + temporaryChangesRevert: () => {}, // used for reverting temporary options settingsModalState: 'hidden', settingsModalLoadedUser: false, settingsModalLoadedAdmin: false, @@ -14,7 +22,8 @@ const defaultState = { cssFilter: window.CSS && window.CSS.supports && ( window.CSS.supports('filter', 'drop-shadow(0 0)') || window.CSS.supports('-webkit-filter', 'drop-shadow(0 0)') - ) + ), + localFonts: typeof window.queryLocalFonts === 'function' }, layoutType: 'normal', globalNotices: [], @@ -36,6 +45,17 @@ const interfaceMod = { state.settings.currentSaveStateNotice = { error: true, errorData: error } } }, + setTemporaryChanges (state, { timeoutId, confirm, revert }) { + state.temporaryChangesTimeoutId = timeoutId + state.temporaryChangesConfirm = confirm + state.temporaryChangesRevert = revert + }, + clearTemporaryChanges (state) { + clearTimeout(state.temporaryChangesTimeoutId) + state.temporaryChangesTimeoutId = null + state.temporaryChangesConfirm = () => {} + state.temporaryChangesRevert = () => {} + }, setThemeApplied (state) { state.themeApplied = true }, @@ -90,6 +110,10 @@ const interfaceMod = { }, setLastTimeline (state, value) { state.lastTimeline = value + }, + setFontsList (state, value) { + // Set is used here so that we filter out duplicate fonts (possibly same font but with different weight) + state.localFonts = [...(new Set(value.map(font => font.family))).values()] } }, actions: { @@ -164,10 +188,203 @@ const interfaceMod = { commit('setLayoutType', wideLayout ? 'wide' : normalOrMobile) } }, + queryLocalFonts ({ commit, dispatch, state }) { + if (state.localFonts !== null) return + commit('setFontsList', []) + if (!state.browserSupport.localFonts) { + return + } + window + .queryLocalFonts() + .then((fonts) => { + commit('setFontsList', fonts) + }) + .catch((e) => { + dispatch('pushGlobalNotice', { + messageKey: 'settings.style.themes3.font.font_list_unavailable', + messageArgs: { + error: e + }, + level: 'error' + }) + }) + }, setLastTimeline ({ commit }, value) { commit('setLastTimeline', value) + }, + setTheme ({ commit, rootState }, { themeName, themeData, recompile, saveData } = {}) { + const { + theme: instanceThemeName + } = rootState.instance + + const { + theme: userThemeName, + customTheme: userThemeSnapshot, + customThemeSource: userThemeSource, + forceThemeRecompilation, + themeDebug, + theme3hacks + } = rootState.config + + const actualThemeName = userThemeName || instanceThemeName + + const forceRecompile = forceThemeRecompilation || recompile + + let promise = null + + if (themeData) { + promise = Promise.resolve(normalizeThemeData(themeData)) + } else if (themeName) { + promise = getPreset(themeName).then(themeData => normalizeThemeData(themeData)) + } else if (userThemeSource || userThemeSnapshot) { + if (userThemeSource && userThemeSource.themeEngineVersion === CURRENT_VERSION) { + promise = Promise.resolve(normalizeThemeData(userThemeSource)) + } else { + promise = Promise.resolve(normalizeThemeData(userThemeSnapshot)) + } + } else if (actualThemeName && actualThemeName !== 'custom') { + promise = getPreset(actualThemeName).then(themeData => { + const realThemeData = normalizeThemeData(themeData) + if (actualThemeName === instanceThemeName) { + // This sole line is the reason why this whole block is above the recompilation check + commit('setInstanceOption', { name: 'themeData', value: { theme: realThemeData } }) + } + return realThemeData + }) + } else { + throw new Error('Cannot load any theme!') + } + + // If we're not not forced to recompile try using + // cache (tryLoadCache return true if load successful) + if (!forceRecompile && !themeDebug && tryLoadCache()) { + commit('setThemeApplied') + return + } + + promise + .then(realThemeData => { + const theme2ruleset = convertTheme2To3(realThemeData) + + if (saveData) { + commit('setOption', { name: 'theme', value: themeName || actualThemeName }) + commit('setOption', { name: 'customTheme', value: realThemeData }) + commit('setOption', { name: 'customThemeSource', value: realThemeData }) + } + const hacks = [] + + Object.entries(theme3hacks).forEach(([key, value]) => { + switch (key) { + case 'fonts': { + Object.entries(theme3hacks.fonts).forEach(([fontKey, font]) => { + if (!font?.family) return + switch (fontKey) { + case 'interface': + hacks.push({ + component: 'Root', + directives: { + '--font': 'generic | ' + font.family + } + }) + break + case 'input': + hacks.push({ + component: 'Input', + directives: { + '--font': 'generic | ' + font.family + } + }) + break + case 'post': + hacks.push({ + component: 'RichContent', + directives: { + '--font': 'generic | ' + font.family + } + }) + break + case 'monospace': + hacks.push({ + component: 'Root', + directives: { + '--monoFont': 'generic | ' + font.family + } + }) + break + } + }) + break + } + case 'underlay': { + if (value !== 'none') { + const newRule = { + component: 'Underlay', + directives: {} + } + if (value === 'opaque') { + newRule.directives.opacity = 1 + newRule.directives.background = '--wallpaper' + } + if (value === 'transparent') { + newRule.directives.opacity = 0 + } + hacks.push(newRule) + } + break + } + } + }) + + const ruleset = [ + ...theme2ruleset, + ...hacks + ] + + applyTheme( + ruleset, + () => commit('setThemeApplied'), + themeDebug + ) + }) + + return promise } } } export default interfaceMod + +export const normalizeThemeData = (input) => { + let themeData = input + + if (Array.isArray(themeData)) { + themeData = { colors: {} } + themeData.colors.bg = input[1] + themeData.colors.fg = input[2] + themeData.colors.text = input[3] + themeData.colors.link = input[4] + themeData.colors.cRed = input[5] + themeData.colors.cGreen = input[6] + themeData.colors.cBlue = input[7] + themeData.colors.cOrange = input[8] + return generatePreset(themeData).theme + } + + if (themeData.themeFileVerison === 1) { + return generatePreset(themeData).theme + } + + // New theme presets don't have 'theme' property, they use 'source' + const themeSource = themeData.source + + let out // shout, shout let it all out + if (!themeData.theme || (themeSource && themeSource.themeEngineVersion === CURRENT_VERSION)) { + out = themeSource || themeData + } else { + out = themeData.theme + } + + // generatePreset here basically creates/updates "snapshot", + // while also fixing the 2.2 -> 2.3 colors/shadows/etc + return generatePreset(out).theme +} diff --git a/src/panel.scss b/src/panel.scss @@ -60,11 +60,12 @@ .panel-heading, .panel-footer { - --panel-heading-height-padding: 0.6em; - --__panel-heading-gap: 0.5em; - --__panel-heading-height: 3.2em; + --panel-heading-height-padding: calc(var(--panel-header-height) * 0.2); + --__panel-heading-gap: calc(var(--panel-header-height) * 0.1565); + --__panel-heading-height: var(--panel-header-height); --__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); position: relative; box-sizing: border-box; diff --git a/src/services/style_setter/style_setter.js b/src/services/style_setter/style_setter.js @@ -1,7 +1,5 @@ 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 { convertTheme2To3 } from '../theme_data/theme2_to_theme3.js' import { getCssRules } from '../theme_data/css_utils.js' import { defaultState } from '../../modules/config.js' import { chunk } from 'lodash' @@ -45,25 +43,21 @@ const adoptStyleSheets = (styles) => { // is nothing to do here. } -export const generateTheme = async (input, callbacks) => { +export const generateTheme = async (inputRuleset, callbacks, debug) => { const { onNewRule = (rule, isLazy) => {}, onLazyFinished = () => {}, onEagerFinished = () => {} } = 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 - 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 if (rule.match(/::-webkit-scrollbar-button/)) { const parts = rule.split(/[{}]/g) @@ -93,7 +87,7 @@ export const generateTheme = async (input, callbacks) => { const processChunk = () => { const chunk = chunks[counter] 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/)) { const parts = rule.split(/[{}]/g) 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 lazyStyles = createStyleSheet(LAZY_STYLE_ID) @@ -177,7 +171,8 @@ export const applyTheme = async (input, onFinish = (data) => {}) => { onFinish(cache) localStorage.setItem('pleroma-fe-theme-cache', JSON.stringify(cache)) } - } + }, + debug ) setTimeout(lazyProcessFunc, 0) @@ -185,15 +180,52 @@ export const applyTheme = async (input, onFinish = (data) => {}) => { return Promise.resolve() } -const configColumns = ({ sidebarColumnWidth, contentColumnWidth, notifsColumnWidth, emojiReactionsScale }) => - ({ sidebarColumnWidth, contentColumnWidth, notifsColumnWidth, emojiReactionsScale }) +const extractStyleConfig = ({ + 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) => { - const columns = configColumns(config) +export const applyConfig = (input) => { + const config = extractStyleConfig(input) - if (columns === defaultConfigColumns) { + if (config === defaultStyleConfig) { return } @@ -202,16 +234,25 @@ export const applyConfig = (config) => { body.classList.add('hidden') const rules = Object - .entries(columns) + .entries(config) .filter(([k, v]) => v) .map(([k, v]) => `--${k}: ${v}`).join(';') + document.getElementById('style-config')?.remove() const styleEl = document.createElement('style') + styleEl.id = 'style-config' head.appendChild(styleEl) const styleSheet = styleEl.sheet styleSheet.toString() 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') } @@ -269,5 +310,3 @@ export const getPreset = (val) => { return { theme: data, source: theme.source } }) } - -export const setPreset = (val) => getPreset(val).then(data => applyTheme(data)) diff --git a/src/services/theme_data/css_utils.js b/src/services/theme_data/css_utils.js @@ -2,11 +2,6 @@ import { convert } from 'chromatism' 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) => { const dimensions = /(\d[a-z]*\s?){2,4}/.exec(text)?.[0] const inset = /inset/.exec(text)?.[0] @@ -66,7 +61,10 @@ export const getCssShadowFilter = (input) => { .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 if (!selector) { selector = 'html' @@ -93,7 +91,7 @@ export const getCssRules = (rules) => rules.map(rule => { ].join(';\n ') } case 'background': { - if (DEBUG) { + if (debug) { return ` --background: ${getCssColorString(rule.dynamicVars.stacked)}; background-color: ${getCssColorString(rule.dynamicVars.stacked)}; @@ -161,3 +159,15 @@ export const getCssRules = (rules) => rules.map(rule => { footer ].join('\n') }).filter(x => x) + +export const getScopedVersion = (rules, newScope) => { + return rules.map(x => { + if (x.startsWith('html')) { + return x.replace('html', newScope) + } else if (x.startsWith('#content')) { + return x.replace('#content', newScope) + } else { + return newScope + ' > ' + x + } + }) +} diff --git a/src/services/theme_data/iss_utils.js b/src/services/theme_data/iss_utils.js @@ -39,7 +39,23 @@ export const getAllPossibleCombinations = (array) => { return combos.reduce((acc, x) => [...acc, ...x], []) } -// Converts rule, parents and their criteria into a CSS (or path if ignoreOutOfTreeSelector == true) selector +/** + * Converts rule, parents and their criteria into a CSS (or path if ignoreOutOfTreeSelector == true) + * selector. + * + * "path" here refers to "fake" selector that cannot be actually used in UI but is used for internal + * purposes + * + * @param {Object} components - object containing all components definitions + * + * @returns {Function} + * @param {Object} rule - rule in question to convert to CSS selector + * @param {boolean} ignoreOutOfTreeSelector - wthether to ignore aformentioned field in + * component definition and use selector + * @param {boolean} isParent - (mostly) internal argument used when recursing + * + * @returns {String} CSS selector (or path) + */ export const genericRuleToSelector = components => (rule, ignoreOutOfTreeSelector, isParent) => { if (!rule && !isParent) return null const component = components[rule.component] @@ -79,6 +95,17 @@ export const genericRuleToSelector = components => (rule, ignoreOutOfTreeSelecto return selectors.trim() } +/** + * Check if combination matches + * + * @param {Object} criteria - criteria to match against + * @param {Object} subject - rule/combination to check match + * @param {boolean} strict - strict checking: + * By default every variant and state inherits from "normal" state/variant + * so when checking if combination matches, it WILL match against "normal" + * state/variant. In strict mode inheritance is ignored an "normal" does + * not match + */ export const combinationsMatch = (criteria, subject, strict) => { if (criteria.component !== subject.component) return false @@ -101,6 +128,15 @@ export const combinationsMatch = (criteria, subject, strict) => { return true } +/** + * Search for rule that matches `criteria` in set of rules + * meant to be used in a ruleset.filter() function + * + * @param {Object} criteria - criteria to search for + * @param {boolean} strict - whether search strictly or not (see combinationsMatch) + * + * @return function that returns true/false if subject matches + */ export const findRules = (criteria, strict) => subject => { // If we searching for "general" rules - ignore "specific" ones if (criteria.parent === null && !!subject.parent) return false @@ -125,6 +161,7 @@ export const findRules = (criteria, strict) => subject => { return true } +// Pre-fills 'normal' state/variant if missing export const normalizeCombination = rule => { rule.variant = rule.variant ?? 'normal' rule.state = [...new Set(['normal', ...(rule.state || [])])] diff --git a/src/services/theme_data/theme2_to_theme3.js b/src/services/theme_data/theme2_to_theme3.js @@ -12,7 +12,9 @@ export const basePaletteKeys = new Set([ 'cBlue', 'cRed', 'cGreen', - 'cOrange' + 'cOrange', + + 'wallpaper' ]) export const fontsKeys = new Set([ @@ -138,7 +140,7 @@ export const convertTheme2To3 = (data) => { Object.keys(data.opacity || {}).forEach(key => { if (!opacityKeys.has(key) || data.opacity[key] === undefined) return null const originalOpacity = data.opacity[key] - const rule = {} + const rule = { source: '2to3' } switch (key) { case 'alert': @@ -213,7 +215,7 @@ export const convertTheme2To3 = (data) => { Object.keys(data.radii || {}).forEach(key => { if (!radiiKeys.has(key) || data.radii[key] === undefined) return null const originalRadius = data.radii[key] - const rule = {} + const rule = { source: '2to3' } switch (key) { case 'btn': @@ -265,8 +267,9 @@ export const convertTheme2To3 = (data) => { const newRules = [] Object.keys(data.fonts || {}).forEach(key => { if (!fontsKeys.has(key)) return + if (!data.fonts[key]) return const originalFont = data.fonts[key].family - const rule = {} + const rule = { source: '2to3' } switch (key) { case 'interface': @@ -300,7 +303,7 @@ export const convertTheme2To3 = (data) => { Object.keys(data.shadows || {}).forEach(key => { if (!shadowsKeys.has(key)) return const originalShadow = data.shadows[key] - const rule = {} + const rule = { source: '2to3' } switch (key) { case 'panel': @@ -369,7 +372,7 @@ export const convertTheme2To3 = (data) => { const extendedRules = Object.entries(extendedBaseKeys).map(([prefix, keys]) => { if (nonComponentPrefixes.has(prefix)) return null - const rule = {} + const rule = { source: '2to3' } if (prefix === 'alertPopup') { rule.component = 'Alert' rule.parent = { component: 'Popover' } @@ -402,7 +405,7 @@ export const convertTheme2To3 = (data) => { const leftoverKey = key.replace(prefix, '') const parts = (leftoverKey || 'Bg').match(/[A-Z][a-z]*/g) const last = parts.slice(-1)[0] - let newRule = { directives: {} } + let newRule = { source: '2to3', directives: {} } let variantArray = [] switch (last) { @@ -462,12 +465,12 @@ export const convertTheme2To3 = (data) => { if (prefix === 'popover' && variantArray[0] === 'Post') { newRule.component = 'Post' - newRule.parent = { component: 'Popover' } + newRule.parent = { source: '2to3hack', component: 'Popover' } variantArray = variantArray.filter(x => x !== 'Post') } if (prefix === 'selectedMenu' && variantArray[0] === 'Popover') { - newRule.parent = { component: 'Popover' } + newRule.parent = { source: '2to3hack', component: 'Popover' } variantArray = variantArray.filter(x => x !== 'Popover') } @@ -477,12 +480,12 @@ export const convertTheme2To3 = (data) => { case 'alert': { const hasPanel = variantArray.find(x => x === 'Panel') if (hasPanel) { - newRule.parent = { component: 'PanelHeader' } + newRule.parent = { source: '2to3hack', component: 'PanelHeader', parent: newRule.parent } variantArray = variantArray.filter(x => x !== 'Panel') } const hasTop = variantArray.find(x => x === 'Top') // TopBar if (hasTop) { - newRule.parent = { component: 'TopBar' } + newRule.parent = { source: '2to3hack', component: 'TopBar', parent: newRule.parent } variantArray = variantArray.filter(x => x !== 'Top' && x !== 'Bar') } break diff --git a/src/services/theme_data/theme_data.service.js b/src/services/theme_data/theme_data.service.js @@ -117,7 +117,6 @@ export const topoSort = ( // Put it into the output list output.push(node) } else if (grays.has(node)) { - console.debug('Cyclic depenency in topoSort, ignoring') output.push(node) } else if (blacks.has(node)) { // do nothing diff --git a/src/services/theme_data/theme_data_3.service.js b/src/services/theme_data/theme_data_3.service.js @@ -149,16 +149,42 @@ const ruleToSelector = genericRuleToSelector(components) 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 stacked = {} const computed = {} const rulesetUnsorted = [ ...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], []), - ...extraRuleset + ...inputRuleset ].map(rule => { normalizeCombination(rule) let currentParent = rule.parent @@ -395,11 +421,16 @@ export const init = (extraRuleset, ultimateBackgroundColor) => { const processInnerComponent = (component, parent) => { const combinations = [] const { - validInnerComponents = [], states: originalStates = {}, variants: originalVariants = {} } = component + const validInnerComponents = ( + liteMode + ? (component.validInnerComponentsLite || component.validInnerComponents) + : component.validInnerComponents + ) || [] + // Normalizing states and variants to always include "normal" const states = { normal: '', ...originalStates } const variants = { normal: '', ...originalVariants } @@ -411,22 +442,26 @@ export const init = (extraRuleset, ultimateBackgroundColor) => { // Optimization: we only really need combinations without "normal" because all states implicitly have it const permutationStateKeys = Object.keys(states).filter(s => s !== 'normal') - const stateCombinations = [ - ['normal'], - ...getAllPossibleCombinations(permutationStateKeys) - .map(combination => ['normal', ...combination]) - .filter(combo => { - // Optimization: filter out some hard-coded combinations that don't make sense - if (combo.indexOf('disabled') >= 0) { - return !( - combo.indexOf('hover') >= 0 || - combo.indexOf('focused') >= 0 || - combo.indexOf('pressed') >= 0 - ) - } - return true - }) - ] + const stateCombinations = onlyNormalState + ? [ + ['normal'] + ] + : [ + ['normal'], + ...getAllPossibleCombinations(permutationStateKeys) + .map(combination => ['normal', ...combination]) + .filter(combo => { + // Optimization: filter out some hard-coded combinations that don't make sense + if (combo.indexOf('disabled') >= 0) { + return !( + combo.indexOf('hover') >= 0 || + combo.indexOf('focused') >= 0 || + combo.indexOf('pressed') >= 0 + ) + } + return true + }) + ] const stateVariantCombination = Object.keys(variants).map(variant => { return stateCombinations.map(state => ({ variant, state })) @@ -451,9 +486,11 @@ export const init = (extraRuleset, ultimateBackgroundColor) => { } const t0 = performance.now() - const combinations = processInnerComponent(components.Root) + const combinations = processInnerComponent(components[rootComponentName] ?? components.Root) const t1 = performance.now() - console.debug('Tree traveral took ' + (t1 - t0) + ' ms') + if (debug) { + console.debug('Tree traveral took ' + (t1 - t0) + ' ms') + } const result = combinations.map((combination) => { if (combination.lazy) { @@ -463,7 +500,9 @@ export const init = (extraRuleset, ultimateBackgroundColor) => { } }).filter(x => x) const t2 = performance.now() - console.debug('Eager processing took ' + (t2 - t1) + ' ms') + if (debug) { + console.debug('Eager processing took ' + (t2 - t1) + ' ms') + } return { lazy: result.filter(x => typeof x === 'function'), diff --git a/test/unit/specs/services/theme_data/theme_data3.spec.js b/test/unit/specs/services/theme_data/theme_data3.spec.js @@ -66,7 +66,7 @@ describe('Theme Data 3', () => { this.timeout(5000) 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('lazy') @@ -85,13 +85,16 @@ describe('Theme Data 3', () => { }) it('Test initialization with a basic palette', () => { - const out = init([{ - component: 'Root', - directives: { - '--bg': 'color | #008080', - '--fg': 'color | #00C0A0' - } - }], '#DEADAF') + const out = init({ + inputRuleset: [{ + component: 'Root', + directives: { + '--bg': 'color | #008080', + '--fg': 'color | #00C0A0' + } + }], + ultimateBackgroundColor: '#DEADAF' + }) expect(out.staticVars).to.have.property('bg').equal('#008080') expect(out.staticVars).to.have.property('fg').equal('#00C0A0') @@ -105,17 +108,20 @@ describe('Theme Data 3', () => { }) it('Test initialization with opacity', () => { - const out = init([{ - component: 'Root', - directives: { - '--bg': 'color | #008080' - } - }, { - component: 'Panel', - directives: { - opacity: 0.5 - } - }], '#DEADAF') + const out = init({ + inputRuleset: [{ + component: 'Root', + directives: { + '--bg': 'color | #008080' + } + }, { + component: 'Panel', + directives: { + opacity: 0.5 + } + }], + ultimateBackgroundColor: '#DEADAF' + }) expect(out.staticVars).to.have.property('bg').equal('#008080')