commit: 40c9163d215b5ac7b69437f3585586b2174211ca
parent 9d76fcc425abe1236304c270765ffdb4e6d0ba6e
Author: Henry Jameson <me@hjkos.com>
Date: Wed, 17 Jul 2024 17:19:57 +0300
optimizations, WIP theme selector
Diffstat:
12 files changed, 361 insertions(+), 158 deletions(-)
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/tabs/appearance_tab.js b/src/components/settings_modal/tabs/appearance_tab.js
@@ -6,6 +6,18 @@ import UnitSetting, { defaultHorizontalUnits } from '../helpers/unit_setting.vue
import FontControl from 'src/components/font_control/font_control.vue'
+import { normalizeThemeData } from 'src/modules/interface'
+
+import {
+ getThemes
+} from 'src/services/style_setter/style_setter.js'
+import { convertTheme2To3 } from 'src/services/theme_data/theme2_to_theme3.js'
+import { init } from 'src/services/theme_data/theme_data_3.service.js'
+import {
+ getCssRules,
+ getScopedVersion
+} from 'src/services/theme_data/css_utils.js'
+
import SharedComputedObject from '../helpers/shared_computed_object.js'
import ProfileSettingIndicator from '../helpers/profile_setting_indicator.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
@@ -13,6 +25,8 @@ import {
faGlobe
} from '@fortawesome/free-solid-svg-icons'
+import Preview from './theme_tab/preview.vue'
+
library.add(
faGlobe
)
@@ -20,6 +34,7 @@ library.add(
const AppearanceTab = {
data () {
return {
+ availableStyles: [],
thirdColumnModeOptions: ['none', 'notifications', 'postform'].map(mode => ({
key: mode,
value: mode,
@@ -44,7 +59,32 @@ const AppearanceTab = {
FloatSetting,
UnitSetting,
ProfileSettingIndicator,
- FontControl
+ FontControl,
+ Preview
+ },
+ created () {
+ const self = this
+
+ 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,
+ [k]: v
+ }
+ } else {
+ return acc
+ }
+ }, {}))
+ .then((themesComplete) => {
+ self.availableStyles = themesComplete
+ })
},
computed: {
horizontalUnits () {
@@ -77,6 +117,25 @@ const AppearanceTab = {
}
},
...SharedComputedObject()
+ },
+ methods: {
+ previewTheme (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,
+ onlyNormalState: true
+ })
+
+ return getScopedVersion(
+ getCssRules(theme3.eager),
+ '#theme-preview-' + (input.name || input[0]).replace(/ /g, '_')
+ ).join('\n')
+ }
}
}
diff --git a/src/components/settings_modal/tabs/appearance_tab.vue b/src/components/settings_modal/tabs/appearance_tab.vue
@@ -1,6 +1,22 @@
<template>
<div :label="$t('settings.general')">
<div class="setting-item">
+ <h2>{{ $t('settings.theme') }}</h2>
+ <ul class="theme-list">
+ <li
+ v-for="style in availableStyles"
+ :key="style.name || style[0]"
+ class="theme-preview"
+ >
+ <h6>{{ style[0] || style.name }}</h6>
+ <!-- eslint-disable vue/no-v-text-v-html-on-component -->
+ <component :is="'style'" v-html="previewTheme(style)"/>
+ <!-- eslint-enable vue/no-v-text-v-html-on-component -->
+ <preview :id="'theme-preview-' + (style[0] || style.name).replace(/ /g,'_')"/>
+ </li>
+ </ul>
+ </div>
+ <div class="setting-item">
<h2>{{ $t('settings.scale_and_layout') }}</h2>
<ul class="setting-list">
<li>
@@ -231,4 +247,18 @@
margin-bottom: 0.5em;
margin-top: 0.5em;
}
+
+.theme-list {
+ list-style: none;
+ display: flex;
+ flex-wrap: wrap;
+}
+
+.theme-preview {
+ width: 10rem;
+
+ .preview-container {
+ zoom: 0.33;
+ }
+}
</style>
diff --git a/src/components/settings_modal/tabs/theme_tab/preview.vue b/src/components/settings_modal/tabs/theme_tab/preview.vue
@@ -139,6 +139,108 @@ export default {}
<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 {
+ 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;
+ }
+ }
}
.underlay-preview {
@@ -148,4 +250,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
@@ -30,7 +30,10 @@ import {
import { convertTheme2To3 } from 'src/services/theme_data/theme2_to_theme3.js'
import { init } from 'src/services/theme_data/theme_data_3.service.js'
-import { getCssRules } from 'src/services/theme_data/css_utils.js'
+import {
+ getCssRules,
+ getScopedVersion
+} from 'src/services/theme_data/css_utils.js'
import ColorInput from 'src/components/color_input/color_input.vue'
import RangeInput from 'src/components/range_input/range_input.vue'
@@ -703,17 +706,10 @@ export default {
liteMode: true
})
- this.themeV3Preview = getCssRules(theme3.eager)
- .map(x => {
- if (x.startsWith('html')) {
- return x.replace('html', '#theme-preview')
- } else if (x.startsWith('#content')) {
- return x.replace('#content', '#theme-preview')
- } else {
- return '#theme-preview > ' + x
- }
- })
- .join('\n')
+ this.themeV3Preview = getScopedVersion(
+ getCssRules(theme3.eager),
+ '#theme-preview'
+ ).join('\n')
}
},
watch: {
diff --git a/src/components/settings_modal/tabs/theme_tab/theme_tab.scss b/src/components/settings_modal/tabs/theme_tab/theme_tab.scss
@@ -161,107 +161,6 @@
}
}
- .preview-container {
- border-top: 1px dashed;
- border-bottom: 1px dashed;
- border-color: var(--border);
- margin: 1em 0;
- padding: 1em;
- background-color: var(--wallpaper);
- background-image: var(--body-background-image);
- background-size: cover;
- background-position: 50% 50%;
-
- .dummy {
- .post {
- font-family: var(--postFont);
- display: flex;
-
- .content {
- flex: 1;
-
- h4 {
- margin-bottom: 0.25em;
- }
-
- .icons {
- margin-top: 0.5em;
- display: flex;
-
- i {
- margin-right: 1em;
- }
- }
- }
- }
-
- .after-post {
- margin-top: 1em;
- display: flex;
- align-items: center;
- }
-
- .avatar,
- .avatar-alt {
- background:
- linear-gradient(
- 135deg,
- #b8e1fc 0%,
- #a9d2f3 10%,
- #90bae4 25%,
- #90bcea 37%,
- #90bff0 50%,
- #6ba8e5 51%,
- #a2daf5 83%,
- #bdf3fd 100%
- );
- color: black;
- font-family: sans-serif;
- text-align: center;
- margin-right: 1em;
- }
-
- .avatar-alt {
- flex: 0 auto;
- margin-left: 28px;
- font-size: 12px;
- min-width: 20px;
- min-height: 20px;
- line-height: 20px;
- }
-
- .avatar {
- flex: 0 auto;
- width: 48px;
- height: 48px;
- font-size: 14px;
- line-height: 48px;
- }
-
- .actions {
- display: flex;
- align-items: baseline;
-
- .checkbox {
- display: inline-flex;
- align-items: baseline;
- margin-right: 1em;
- flex: 1;
- }
- }
-
- .separator {
- margin: 1em;
- border-bottom: 1px solid;
- border-color: var(--border);
- }
-
- .btn {
- min-width: 3em;
- }
- }
- }
-
.radius-item {
flex-basis: auto;
}
@@ -314,10 +213,6 @@
max-width: 50em;
}
- .theme-preview-content {
- padding: 20px;
- }
-
.theme-warning {
display: flex;
align-items: baseline;
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/modules/interface.js b/src/modules/interface.js
@@ -234,25 +234,6 @@ const interfaceMod = {
return
}
- const normalizeThemeData = (themeData) => {
- if (themeData.themeFileVerison === 1) {
- return generatePreset(themeData).theme
- }
- // New theme presets don't have 'theme' property, they use 'source'
- const themeSource = themeData.source
-
- let out // shout, shout let it all out
- if (!themeData.theme || (themeSource && themeSource.themeEngineVersion === CURRENT_VERSION)) {
- out = themeSource || themeData
- } else {
- out = themeData.theme
- }
-
- // generatePreset here basically creates/updates "snapshot",
- // while also fixing the 2.2 -> 2.3 colors/shadows/etc
- return generatePreset(out).theme
- }
-
let promise = null
if (themeName) {
@@ -320,3 +301,38 @@ const interfaceMod = {
}
export default interfaceMod
+
+export const normalizeThemeData = (input) => {
+ let themeData = input
+
+ if (Array.isArray(themeData)) {
+ themeData = { colors: {} }
+ themeData.colors.bg = input[1]
+ themeData.colors.fg = input[2]
+ themeData.colors.text = input[3]
+ themeData.colors.link = input[4]
+ themeData.colors.cRed = input[5]
+ themeData.colors.cGreen = input[6]
+ themeData.colors.cBlue = input[7]
+ themeData.colors.cOrange = input[8]
+ return generatePreset(themeData).theme
+ }
+
+ if (themeData.themeFileVerison === 1) {
+ return generatePreset(themeData).theme
+ }
+
+ // New theme presets don't have 'theme' property, they use 'source'
+ const themeSource = themeData.source
+
+ let out // shout, shout let it all out
+ if (!themeData.theme || (themeSource && themeSource.themeEngineVersion === CURRENT_VERSION)) {
+ out = themeSource || themeData
+ } else {
+ out = themeData.theme
+ }
+
+ // generatePreset here basically creates/updates "snapshot",
+ // while also fixing the 2.2 -> 2.3 colors/shadows/etc
+ return generatePreset(out).theme
+}
diff --git a/src/services/theme_data/css_utils.js b/src/services/theme_data/css_utils.js
@@ -159,3 +159,15 @@ export const getCssRules = (rules, debug) => rules.map(rule => {
footer
].join('\n')
}).filter(x => x)
+
+export const getScopedVersion = (rules, newScope) => {
+ return rules.map(x => {
+ if (x.startsWith('html')) {
+ return x.replace('html', newScope)
+ } else if (x.startsWith('#content')) {
+ return x.replace('#content', newScope)
+ } else {
+ return newScope + ' > ' + x
+ }
+ })
+}
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/theme_data_3.service.js b/src/services/theme_data/theme_data_3.service.js
@@ -149,11 +149,30 @@ const ruleToSelector = genericRuleToSelector(components)
export const getEngineChecksum = () => engineChecksum
+/**
+ * Initializes and compiles the theme according to the ruleset
+ *
+ * @param {Object[]} inputRuleset - set of rules to compile theme against. Acts as an override to
+ * component default rulesets
+ * @param {string} ultimateBackgroundColor - Color that will be the "final" background for
+ * calculating contrast ratios and making text automatically accessible. Really used for cases when
+ * stuff is transparent.
+ * @param {boolean} debug - print out debug information in console, mostly just performance stuff
+ * @param {boolean} liteMode - use validInnerComponentsLite instead of validInnerComponents, meant to
+ * generatate theme previews and such that need to be compiled faster and don't require a lot of other
+ * components present in "normal" mode
+ * @param {boolean} onlyNormalState - only use components 'normal' states, meant for generating theme
+ * previews since states are the biggest factor for compilation time and are completely unnecessary
+ * when previewing multiple themes at same time
+ * @param {string} rootComponentName - [UNTESTED] which component to start from, meant for previewing a
+ * part of the theme (i.e. just the button) for themes 3 editor.
+ */
export const init = ({
inputRuleset,
ultimateBackgroundColor,
debug = false,
liteMode = false,
+ onlyNormalState = false,
rootComponentName = 'Root'
}) => {
if (!inputRuleset) throw new Error('Ruleset is null or undefined!')
@@ -402,11 +421,16 @@ export const init = ({
const processInnerComponent = (component, parent) => {
const combinations = []
const {
- validInnerComponents = [],
states: originalStates = {},
variants: originalVariants = {}
} = component
+ const validInnerComponents = (
+ liteMode
+ ? (component.validInnerComponentsLite || component.validInnerComponents)
+ : component.validInnerComponents
+ ) || []
+
// Normalizing states and variants to always include "normal"
const states = { normal: '', ...originalStates }
const variants = { normal: '', ...originalVariants }
@@ -418,22 +442,26 @@ export const init = ({
// Optimization: we only really need combinations without "normal" because all states implicitly have it
const permutationStateKeys = Object.keys(states).filter(s => s !== 'normal')
- const stateCombinations = [
- ['normal'],
- ...getAllPossibleCombinations(permutationStateKeys)
- .map(combination => ['normal', ...combination])
- .filter(combo => {
- // Optimization: filter out some hard-coded combinations that don't make sense
- if (combo.indexOf('disabled') >= 0) {
- return !(
- combo.indexOf('hover') >= 0 ||
- combo.indexOf('focused') >= 0 ||
- combo.indexOf('pressed') >= 0
- )
- }
- return true
- })
- ]
+ const stateCombinations = onlyNormalState
+ ? [
+ ['normal']
+ ]
+ : [
+ ['normal'],
+ ...getAllPossibleCombinations(permutationStateKeys)
+ .map(combination => ['normal', ...combination])
+ .filter(combo => {
+ // Optimization: filter out some hard-coded combinations that don't make sense
+ if (combo.indexOf('disabled') >= 0) {
+ return !(
+ combo.indexOf('hover') >= 0 ||
+ combo.indexOf('focused') >= 0 ||
+ combo.indexOf('pressed') >= 0
+ )
+ }
+ return true
+ })
+ ]
const stateVariantCombination = Object.keys(variants).map(variant => {
return stateCombinations.map(state => ({ variant, state }))
@@ -460,7 +488,9 @@ export const init = ({
const t0 = performance.now()
const combinations = processInnerComponent(components[rootComponentName] ?? components.Root)
const t1 = performance.now()
- console.debug('Tree traveral took ' + (t1 - t0) + ' ms')
+ if (debug) {
+ console.debug('Tree traveral took ' + (t1 - t0) + ' ms')
+ }
const result = combinations.map((combination) => {
if (combination.lazy) {
@@ -470,7 +500,9 @@ export const init = ({
}
}).filter(x => x)
const t2 = performance.now()
- console.debug('Eager processing took ' + (t2 - t1) + ' ms')
+ if (debug) {
+ console.debug('Eager processing took ' + (t2 - t1) + ' ms')
+ }
return {
lazy: result.filter(x => typeof x === 'function'),