From e4a819a0e2ed9c57dc2191428d86a33bb5918862 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Wed, 22 May 2024 19:54:19 +0300
Subject: initial Appearance Tab implementation, added text size/UI scale
 option

---
 .../settings_modal/tabs/appearance_tab.vue         | 71 ++++++++++++++++++++++
 1 file changed, 71 insertions(+)
 create mode 100644 src/components/settings_modal/tabs/appearance_tab.vue

(limited to 'src/components/settings_modal/tabs/appearance_tab.vue')

diff --git a/src/components/settings_modal/tabs/appearance_tab.vue b/src/components/settings_modal/tabs/appearance_tab.vue
new file mode 100644
index 00000000..7730b701
--- /dev/null
+++ b/src/components/settings_modal/tabs/appearance_tab.vue
@@ -0,0 +1,71 @@
+<template>
+  <div :label="$t('settings.general')">
+    <div class="setting-item">
+      <h2>{{ $t('settings.interface') }}</h2>
+      <ul class="setting-list">
+        <li v-if="instanceWallpaperUsed">
+          <BooleanSetting path="hideInstanceWallpaper">
+            {{ $t('settings.hide_wallpaper') }}
+          </BooleanSetting>
+        </li>
+        <li>
+          <BooleanSetting path="disableStickyHeaders">
+            {{ $t('settings.disable_sticky_headers') }}
+          </BooleanSetting>
+        </li>
+        <li>
+          <BooleanSetting path="showScrollbars">
+            {{ $t('settings.show_scrollbars') }}
+          </BooleanSetting>
+        </li>
+        <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>
+      </ul>
+    </div>
+  </div>
+</template>
+
+<script src="./appearance_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>
-- 
cgit v1.2.3-70-g09d2


From 6343b91abf7fbad1cdd8b4648d1fc825d8e9b03c Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Thu, 13 Jun 2024 02:22:47 +0300
Subject: more scaling options

---
 src/App.scss                                       |   5 +-
 src/components/emoji_reactions/emoji_reactions.vue |   2 +-
 .../settings_modal/tabs/appearance_tab.js          |  32 +++++-
 .../settings_modal/tabs/appearance_tab.vue         | 116 ++++++++++++++++++---
 src/components/settings_modal/tabs/general_tab.js  |  33 +-----
 src/components/settings_modal/tabs/general_tab.vue |  60 -----------
 src/i18n/en.json                                   |   4 +
 src/modules/config.js                              |   5 +
 src/modules/instance.js                            |   3 +
 src/panel.scss                                     |   6 +-
 src/services/style_setter/style_setter.js          |  20 ++--
 11 files changed, 165 insertions(+), 121 deletions(-)

(limited to 'src/components/settings_modal/tabs/appearance_tab.vue')

diff --git a/src/App.scss b/src/App.scss
index 32ca9189..a2710436 100644
--- a/src/App.scss
+++ b/src/App.scss
@@ -5,7 +5,7 @@
 :root {
   --fontSize: 14px;
   --status-margin: 0.75em;
-  --navbar-height: 3.5rem;
+  --navbar-height: var(--navbarSize, 3.5rem);
   --post-line-height: 1.4;
   // Z-Index stuff
   --ZI_media_modal: 9000;
@@ -21,6 +21,9 @@
 
 html {
   font-size: var(--textSize);
+
+  --navbar-height: var(--navbarSize, 3.5rem);
+  --emoji-size: var(--emojiSize, 32px);
   // overflow-x: clip causes my browser's tab to crash with SIGILL lul
 }
 
diff --git a/src/components/emoji_reactions/emoji_reactions.vue b/src/components/emoji_reactions/emoji_reactions.vue
index ad4a3c0b..3ab4c125 100644
--- 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/settings_modal/tabs/appearance_tab.js b/src/components/settings_modal/tabs/appearance_tab.js
index b96106bd..542ab0c1 100644
--- a/src/components/settings_modal/tabs/appearance_tab.js
+++ b/src/components/settings_modal/tabs/appearance_tab.js
@@ -2,7 +2,7 @@ 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 from '../helpers/unit_setting.vue'
+import UnitSetting, { defaultHorizontalUnits } from '../helpers/unit_setting.vue'
 
 import SharedComputedObject from '../helpers/shared_computed_object.js'
 import ProfileSettingIndicator from '../helpers/profile_setting_indicator.vue'
@@ -17,7 +17,13 @@ library.add(
 
 const AppearanceTab = {
   data () {
-    return {}
+    return {
+      thirdColumnModeOptions: ['none', 'notifications', 'postform'].map(mode => ({
+        key: mode,
+        value: mode,
+        label: this.$t(`settings.third_column_mode_${mode}`)
+      }))
+    }
   },
   components: {
     BooleanSetting,
@@ -28,10 +34,32 @@ const AppearanceTab = {
     ProfileSettingIndicator
   },
   computed: {
+    horizontalUnits () {
+      return defaultHorizontalUnits
+    },
+    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()
   }
 }
diff --git a/src/components/settings_modal/tabs/appearance_tab.vue b/src/components/settings_modal/tabs/appearance_tab.vue
index 7730b701..75126df7 100644
--- a/src/components/settings_modal/tabs/appearance_tab.vue
+++ b/src/components/settings_modal/tabs/appearance_tab.vue
@@ -1,23 +1,8 @@
 <template>
   <div :label="$t('settings.general')">
     <div class="setting-item">
-      <h2>{{ $t('settings.interface') }}</h2>
+      <h2>{{ $t('settings.scale_and_layout') }}</h2>
       <ul class="setting-list">
-        <li v-if="instanceWallpaperUsed">
-          <BooleanSetting path="hideInstanceWallpaper">
-            {{ $t('settings.hide_wallpaper') }}
-          </BooleanSetting>
-        </li>
-        <li>
-          <BooleanSetting path="disableStickyHeaders">
-            {{ $t('settings.disable_sticky_headers') }}
-          </BooleanSetting>
-        </li>
-        <li>
-          <BooleanSetting path="showScrollbars">
-            {{ $t('settings.show_scrollbars') }}
-          </BooleanSetting>
-        </li>
         <li>
           <UnitSetting
             path="textSize"
@@ -49,6 +34,105 @@
             </small>
           </div>
         </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': 64, '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': 64, '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 v-if="instanceWallpaperUsed">
+          <BooleanSetting path="hideInstanceWallpaper">
+            {{ $t('settings.hide_wallpaper') }}
+          </BooleanSetting>
+        </li>
       </ul>
     </div>
   </div>
diff --git a/src/components/settings_modal/tabs/general_tab.js b/src/components/settings_modal/tabs/general_tab.js
index 7d701d34..96caab07 100644
--- 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
index 240c0762..496d0d56 100644
--- a/src/components/settings_modal/tabs/general_tab.vue
+++ b/src/components/settings_modal/tabs/general_tab.vue
@@ -93,43 +93,6 @@
             {{ $t('settings.hide_shoutbox') }}
           </BooleanSetting>
         </li>
-        <li>
-          <h3>{{ $t('settings.columns') }}</h3>
-        </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">
@@ -262,15 +225,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
@@ -513,17 +467,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/i18n/en.json b/src/i18n/en.json
index 0c3f26b9..8d66ea0a 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -382,6 +382,10 @@
     "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",
     "mfa": {
       "otp": "OTP",
       "setup_otp": "Setup OTP",
diff --git a/src/modules/config.js b/src/modules/config.js
index 4d5e8efc..90d3353a 100644
--- a/src/modules/config.js
+++ b/src/modules/config.js
@@ -117,6 +117,8 @@ export const defaultState = {
   notifsColumnWidth: '25rem',
   emojiReactionsScale: undefined,
   textSize: undefined, // instance default
+  emojiSize: undefined, // instance default
+  navbarSize: undefined, // instance default
   navbarColumnStretch: false,
   greentext: undefined, // instance default
   useAtIcon: undefined, // instance default
@@ -262,6 +264,9 @@ const config = {
           case 'contentColumnWidth':
           case 'notifsColumnWidth':
           case 'textSize':
+          case 'navbarSize':
+          case 'panelHeaderSize':
+          case 'emojiSize':
           case 'emojiReactionsScale':
             applyConfig(state)
             break
diff --git a/src/modules/instance.js b/src/modules/instance.js
index 4a75d948..ecfa299f 100644
--- a/src/modules/instance.js
+++ b/src/modules/instance.js
@@ -100,6 +100,9 @@ const defaultState = {
   theme: 'pleroma-dark',
   emojiReactionsScale: 1.0,
   textSize: '14px',
+  emojiSize: '2.2em',
+  navbarSize: '3.5em',
+  panelHeaderSize: '3.2em',
   virtualScrolling: true,
   sensitiveByDefault: false,
   conversationDisplay: 'linear',
diff --git a/src/panel.scss b/src/panel.scss
index 833e4208..7b38cca8 100644
--- a/src/panel.scss
+++ b/src/panel.scss
@@ -60,9 +60,9 @@
 
 .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(--panelHeaderSize) * 0.15);
+  --__panel-heading-gap: calc(var(--panelHeaderSize) * 0.1);
+  --__panel-heading-height: var(--panelHeaderSize, 3.2em);
   --__panel-heading-height-inner: calc(var(--__panel-heading-height) - 2 * var(--panel-heading-height-padding, 0));
 
   backdrop-filter: var(--__panel-backdrop-filter);
diff --git a/src/services/style_setter/style_setter.js b/src/services/style_setter/style_setter.js
index a98456d3..7cf4af11 100644
--- a/src/services/style_setter/style_setter.js
+++ b/src/services/style_setter/style_setter.js
@@ -148,26 +148,34 @@ export const applyTheme = async (input, onFinish = (data) => {}) => {
   return Promise.resolve()
 }
 
-const configColumns = ({
+const extractStyleConfig = ({
   sidebarColumnWidth,
   contentColumnWidth,
   notifsColumnWidth,
   emojiReactionsScale,
+  roundnessOverride,
+  emojiSize,
+  navbarSize,
+  panelHeaderSize,
   textSize
 }) => ({
   sidebarColumnWidth,
   contentColumnWidth,
   notifsColumnWidth,
   emojiReactionsScale,
+  roundnessOverride,
+  emojiSize,
+  navbarSize,
+  panelHeaderSize,
   textSize
 })
 
-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
   }
 
@@ -176,7 +184,7 @@ 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(';')
 
-- 
cgit v1.2.3-70-g09d2


From a16e158216784071949f91074beca4b2e7382017 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Wed, 19 Jun 2024 22:16:31 +0300
Subject: better defaults, fonts scale with panel scale

---
 src/components/settings_modal/tabs/appearance_tab.vue | 4 ++--
 src/modules/instance.js                               | 8 ++++----
 src/panel.scss                                        | 3 ++-
 3 files changed, 8 insertions(+), 7 deletions(-)

(limited to 'src/components/settings_modal/tabs/appearance_tab.vue')

diff --git a/src/components/settings_modal/tabs/appearance_tab.vue b/src/components/settings_modal/tabs/appearance_tab.vue
index 75126df7..929853f4 100644
--- a/src/components/settings_modal/tabs/appearance_tab.vue
+++ b/src/components/settings_modal/tabs/appearance_tab.vue
@@ -62,7 +62,7 @@
             path="navbarSize"
             step="0.1"
             :units="['px', 'rem']"
-            :reset-default="{ 'px': 64, 'rem': 3.5 }"
+            :reset-default="{ 'px': 55, 'rem': 3.5 }"
           >
             {{ $t('settings.navbar_size') }}
           </UnitSetting>
@@ -73,7 +73,7 @@
             path="panelHeaderSize"
             step="0.1"
             :units="['px', 'rem']"
-            :reset-default="{ 'px': 64, 'rem': 3.2 }"
+            :reset-default="{ 'px': 52, 'rem': 3.2 }"
             timed-apply-mode
           >
             {{ $t('settings.panel_header_size') }}
diff --git a/src/modules/instance.js b/src/modules/instance.js
index ecfa299f..30eaddf1 100644
--- a/src/modules/instance.js
+++ b/src/modules/instance.js
@@ -98,11 +98,11 @@ const defaultState = {
   sidebarRight: false,
   subjectLineBehavior: 'email',
   theme: 'pleroma-dark',
-  emojiReactionsScale: 1.0,
+  emojiReactionsScale: 0.5,
   textSize: '14px',
-  emojiSize: '2.2em',
-  navbarSize: '3.5em',
-  panelHeaderSize: '3.2em',
+  emojiSize: '2.2rem',
+  navbarSize: '3.5rem',
+  panelHeaderSize: '3.2rem',
   virtualScrolling: true,
   sensitiveByDefault: false,
   conversationDisplay: 'linear',
diff --git a/src/panel.scss b/src/panel.scss
index 7b38cca8..8a54338c 100644
--- a/src/panel.scss
+++ b/src/panel.scss
@@ -60,11 +60,12 @@
 
 .panel-heading,
 .panel-footer {
-  --panel-heading-height-padding: calc(var(--panelHeaderSize) * 0.15);
+  --panel-heading-height-padding: calc(var(--panelHeaderSize) * 0.2);
   --__panel-heading-gap: calc(var(--panelHeaderSize) * 0.1);
   --__panel-heading-height: var(--panelHeaderSize, 3.2em);
   --__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;
-- 
cgit v1.2.3-70-g09d2


From eed05f1380fd60f9564558c82f142c317ef670f5 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Fri, 21 Jun 2024 22:46:01 +0300
Subject: forced roundness tweak config

---
 .../settings_modal/tabs/appearance_tab.js          |  5 +++
 .../settings_modal/tabs/appearance_tab.vue         |  9 ++++
 src/i18n/en.json                                   |  6 +++
 src/modules/config.js                              |  3 ++
 src/modules/instance.js                            |  1 +
 src/services/style_setter/style_setter.js          | 51 ++++++++++++++++------
 6 files changed, 62 insertions(+), 13 deletions(-)

(limited to 'src/components/settings_modal/tabs/appearance_tab.vue')

diff --git a/src/components/settings_modal/tabs/appearance_tab.js b/src/components/settings_modal/tabs/appearance_tab.js
index 542ab0c1..d9a0429a 100644
--- a/src/components/settings_modal/tabs/appearance_tab.js
+++ b/src/components/settings_modal/tabs/appearance_tab.js
@@ -22,6 +22,11 @@ const AppearanceTab = {
         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.forced_roundness_mode_${mode}`)
       }))
     }
   },
diff --git a/src/components/settings_modal/tabs/appearance_tab.vue b/src/components/settings_modal/tabs/appearance_tab.vue
index 929853f4..dd9e8070 100644
--- a/src/components/settings_modal/tabs/appearance_tab.vue
+++ b/src/components/settings_modal/tabs/appearance_tab.vue
@@ -128,6 +128,15 @@
     <div class="setting-item">
       <h2>{{ $t('settings.visual_tweaks') }}</h2>
       <ul class="setting-list">
+        <li>
+          <ChoiceSetting
+            id="forcedRoundness"
+            path="forcedRoundness"
+            :options="forcedRoundnessOptions"
+          >
+            {{ $t('settings.force_interface_roundness') }}
+          </ChoiceSetting>
+        </li>
         <li v-if="instanceWallpaperUsed">
           <BooleanSetting path="hideInstanceWallpaper">
             {{ $t('settings.hide_wallpaper') }}
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 8d66ea0a..628be1bf 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -386,6 +386,12 @@
     "navbar_size": "Top bar size",
     "panel_header_size": "Panel header size",
     "visual_tweaks": "Minor visual tweaks",
+    "force_interface_roundness": "Override interface roundness/sharpness",
+    "forced_roundness_mode_disabled": "Use theme defaults",
+    "forced_roundness_mode_sharp": "Force sharp edges",
+    "forced_roundness_mode_nonsharp": "Force not-so-sharp (1px roundness) edges",
+    "forced_roundness_mode_round": "Force round edges",
+    "scale_and_layout": "Interface scale and layout",
     "mfa": {
       "otp": "OTP",
       "setup_otp": "Setup OTP",
diff --git a/src/modules/config.js b/src/modules/config.js
index 90d3353a..2623bc26 100644
--- a/src/modules/config.js
+++ b/src/modules/config.js
@@ -119,6 +119,8 @@ export const defaultState = {
   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
@@ -266,6 +268,7 @@ const config = {
           case 'textSize':
           case 'navbarSize':
           case 'panelHeaderSize':
+          case 'forcedRoundness':
           case 'emojiSize':
           case 'emojiReactionsScale':
             applyConfig(state)
diff --git a/src/modules/instance.js b/src/modules/instance.js
index 30eaddf1..e28837b3 100644
--- a/src/modules/instance.js
+++ b/src/modules/instance.js
@@ -103,6 +103,7 @@ const defaultState = {
   emojiSize: '2.2rem',
   navbarSize: '3.5rem',
   panelHeaderSize: '3.2rem',
+  forcedRoundness: -1,
   virtualScrolling: true,
   sensitiveByDefault: false,
   conversationDisplay: 'linear',
diff --git a/src/services/style_setter/style_setter.js b/src/services/style_setter/style_setter.js
index d516d0d1..ee0f8607 100644
--- a/src/services/style_setter/style_setter.js
+++ b/src/services/style_setter/style_setter.js
@@ -153,22 +153,41 @@ const extractStyleConfig = ({
   contentColumnWidth,
   notifsColumnWidth,
   emojiReactionsScale,
-  roundnessOverride,
   emojiSize,
   navbarSize,
   panelHeaderSize,
-  textSize
-}) => ({
-  sidebarColumnWidth,
-  contentColumnWidth,
-  notifsColumnWidth,
-  emojiReactionsScale,
-  roundnessOverride,
-  emojiSize,
-  navbarSize,
-  panelHeaderSize,
-  textSize
-})
+  textSize,
+  forcedRoundness
+}) => {
+  const result = {
+    sidebarColumnWidth,
+    contentColumnWidth,
+    notifsColumnWidth,
+    emojiReactionsScale,
+    emojiSize,
+    navbarSize,
+    panelHeaderSize,
+    textSize
+  }
+
+  console.log(forcedRoundness)
+  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 defaultStyleConfig = extractStyleConfig(defaultState)
 
@@ -197,6 +216,12 @@ export const applyConfig = (input) => {
   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')
 }
 
-- 
cgit v1.2.3-70-g09d2


From f9e407de639ce4d20beacd3eca3501639ae161bd Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Fri, 21 Jun 2024 23:28:24 +0300
Subject: made theme debug easier by making it an actual option

---
 src/boot/after_store.js                               | 10 ++++++----
 src/components/settings_modal/tabs/appearance_tab.vue | 16 ++++++++++++++++
 src/components/settings_modal/tabs/general_tab.vue    |  8 --------
 src/i18n/en.json                                      |  1 +
 src/modules/config.js                                 | 12 ++++++++++--
 src/modules/instance.js                               |  6 +++---
 src/services/style_setter/style_setter.js             | 17 +++++++++++------
 src/services/theme_data/css_utils.js                  | 12 +++++-------
 src/services/theme_data/theme_data_3.service.js       |  2 +-
 9 files changed, 53 insertions(+), 31 deletions(-)

(limited to 'src/components/settings_modal/tabs/appearance_tab.vue')

diff --git a/src/boot/after_store.js b/src/boot/after_store.js
index bcab7a66..b93e28a3 100644
--- a/src/boot/after_store.js
+++ b/src/boot/after_store.js
@@ -353,18 +353,20 @@ const afterStoreSetup = async ({ store, i18n }) => {
 
   await setConfig({ store })
 
-  const { customTheme, customThemeSource, forceThemeRecompilation } = store.state.config
+  const { customTheme, customThemeSource, forceThemeRecompilation, themeDebug } = store.state.config
   const { theme } = store.state.instance
   const customThemePresent = customThemeSource || customTheme
 
-  if (!forceThemeRecompilation && tryLoadCache()) {
+  console.log('DEBUG INITIAL', themeDebug, forceThemeRecompilation)
+
+  if (!forceThemeRecompilation && !themeDebug && tryLoadCache()) {
     store.commit('setThemeApplied')
   } else {
     if (customThemePresent) {
       if (customThemeSource && customThemeSource.themeEngineVersion === CURRENT_VERSION) {
-        applyTheme(customThemeSource)
+        applyTheme(customThemeSource, () => {}, themeDebug)
       } else {
-        applyTheme(customTheme)
+        applyTheme(customTheme, () => {}, themeDebug)
       }
       store.commit('setThemeApplied')
     } else if (theme) {
diff --git a/src/components/settings_modal/tabs/appearance_tab.vue b/src/components/settings_modal/tabs/appearance_tab.vue
index dd9e8070..5356292e 100644
--- a/src/components/settings_modal/tabs/appearance_tab.vue
+++ b/src/components/settings_modal/tabs/appearance_tab.vue
@@ -142,6 +142,22 @@
             {{ $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>
diff --git a/src/components/settings_modal/tabs/general_tab.vue b/src/components/settings_modal/tabs/general_tab.vue
index 496d0d56..4ece6cf4 100644
--- a/src/components/settings_modal/tabs/general_tab.vue
+++ b/src/components/settings_modal/tabs/general_tab.vue
@@ -148,14 +148,6 @@
     <div class="setting-item">
       <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"
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 628be1bf..3116843d 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -391,6 +391,7 @@
     "forced_roundness_mode_sharp": "Force sharp edges",
     "forced_roundness_mode_nonsharp": "Force not-so-sharp (1px roundness) edges",
     "forced_roundness_mode_round": "Force round edges",
+    "theme_debug": "Show what background theme engine assumes when dealing with transparancy (DEBUG)",
     "scale_and_layout": "Interface scale and layout",
     "mfa": {
       "otp": "OTP",
diff --git a/src/modules/config.js b/src/modules/config.js
index 2623bc26..a977c9c5 100644
--- a/src/modules/config.js
+++ b/src/modules/config.js
@@ -147,6 +147,7 @@ export const defaultState = {
   maxDepthInThread: undefined, // instance default
   autocompleteSelect: undefined, // instance default
   closingDrawerMarksAsSeen: undefined, // instance default
+  themeDebug: false,
   unseenAtTop: undefined, // instance default
   ignoreInactionableSeen: undefined // instance default
 }
@@ -274,9 +275,16 @@ const config = {
             applyConfig(state)
             break
           case 'customTheme':
-          case 'customThemeSource':
-            applyTheme(value)
+          case 'customThemeSource': {
+            const { themeDebug } = state
+            applyTheme(value, () => {}, themeDebug)
             break
+          }
+          case 'themeDebug': {
+            const { customTheme, customThemeSource } = state
+            applyTheme(customTheme || customThemeSource, () => {}, value)
+            break
+          }
           case 'interfaceLanguage':
             messages.setLanguage(this.getters.i18n, value)
             dispatch('loadUnicodeEmojiData', value)
diff --git a/src/modules/instance.js b/src/modules/instance.js
index e28837b3..6997e75d 100644
--- a/src/modules/instance.js
+++ b/src/modules/instance.js
@@ -383,16 +383,16 @@ const instance = {
         .then(themeData => {
           commit('setInstanceOption', { name: 'themeData', value: themeData })
           // No need to apply theme if there's user theme already
-          const { customTheme } = rootState.config
+          const { customTheme, themeDebug } = 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)
+            applyTheme(themeSource, null, themeDebug)
           } else {
-            applyTheme(themeData.theme)
+            applyTheme(themeData.theme, null, themeDebug)
           }
           commit('setThemeApplied')
         })
diff --git a/src/services/style_setter/style_setter.js b/src/services/style_setter/style_setter.js
index ee0f8607..126b6946 100644
--- a/src/services/style_setter/style_setter.js
+++ b/src/services/style_setter/style_setter.js
@@ -6,7 +6,7 @@ import { getCssRules } from '../theme_data/css_utils.js'
 import { defaultState } from '../../modules/config.js'
 import { chunk } from 'lodash'
 
-export const generateTheme = async (input, callbacks) => {
+export const generateTheme = async (input, callbacks, debug) => {
   const {
     onNewRule = (rule, isLazy) => {},
     onLazyFinished = () => {},
@@ -22,9 +22,11 @@ export const generateTheme = async (input, callbacks) => {
   }
 
   // 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(extraRules, extraRules[0].directives['--bg'].split('|')[1].trim(), debug)
 
-  getCssRules(themes3.eager, themes3.staticVars).forEach(rule => {
+  console.log('DEBUG 2 IS', debug)
+
+  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)
@@ -54,7 +56,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 = [
@@ -113,12 +115,14 @@ export const tryLoadCache = () => {
   }
 }
 
-export const applyTheme = async (input, onFinish = (data) => {}) => {
+export const applyTheme = async (input, onFinish = (data) => {}, debug) => {
   const styleSheet = new CSSStyleSheet()
   const styleArray = []
   const lazyStyleSheet = new CSSStyleSheet()
   const lazyStyleArray = []
 
+  console.log('DEBUG IS', debug)
+
   const { lazyProcessFunc } = await generateTheme(
     input,
     {
@@ -140,7 +144,8 @@ export const applyTheme = async (input, onFinish = (data) => {}) => {
         onFinish(cache)
         localStorage.setItem('pleroma-fe-theme-cache', JSON.stringify(cache))
       }
-    }
+    },
+    debug
   )
 
   setTimeout(lazyProcessFunc, 0)
diff --git a/src/services/theme_data/css_utils.js b/src/services/theme_data/css_utils.js
index a89eac3b..8423e8ac 100644
--- 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)};
diff --git a/src/services/theme_data/theme_data_3.service.js b/src/services/theme_data/theme_data_3.service.js
index 15b4493e..51ecc051 100644
--- a/src/services/theme_data/theme_data_3.service.js
+++ b/src/services/theme_data/theme_data_3.service.js
@@ -149,7 +149,7 @@ const ruleToSelector = genericRuleToSelector(components)
 
 export const getEngineChecksum = () => engineChecksum
 
-export const init = (extraRuleset, ultimateBackgroundColor) => {
+export const init = (extraRuleset, ultimateBackgroundColor, debug) => {
   const staticVars = {}
   const stacked = {}
   const computed = {}
-- 
cgit v1.2.3-70-g09d2


From 80cbf29bdf7c6cd3695d1e4cf49a8e38bff847ed Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Thu, 27 Jun 2024 00:59:24 +0300
Subject: fields for user font overrides

---
 src/components/font_control/font_control.js        |  2 +-
 .../settings_modal/tabs/appearance_tab.js          |  8 +++-
 .../settings_modal/tabs/appearance_tab.vue         | 45 ++++++++++++++++++++++
 src/i18n/en.json                                   |  1 +
 src/modules/config.js                              |  2 +
 src/modules/instance.js                            |  1 +
 6 files changed, 57 insertions(+), 2 deletions(-)

(limited to 'src/components/settings_modal/tabs/appearance_tab.vue')

diff --git a/src/components/font_control/font_control.js b/src/components/font_control/font_control.js
index f6f45c8c..08d0907e 100644
--- a/src/components/font_control/font_control.js
+++ b/src/components/font_control/font_control.js
@@ -30,7 +30,7 @@ export default {
   emits: ['update:modelValue'],
   data () {
     return {
-      manualEntry: true,
+      manualEntry: false,
       availableOptions: [
         this.noInherit ? '' : 'inherit',
         'serif',
diff --git a/src/components/settings_modal/tabs/appearance_tab.js b/src/components/settings_modal/tabs/appearance_tab.js
index d9a0429a..3776464a 100644
--- a/src/components/settings_modal/tabs/appearance_tab.js
+++ b/src/components/settings_modal/tabs/appearance_tab.js
@@ -4,6 +4,8 @@ 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 SharedComputedObject from '../helpers/shared_computed_object.js'
 import ProfileSettingIndicator from '../helpers/profile_setting_indicator.vue'
 import { library } from '@fortawesome/fontawesome-svg-core'
@@ -36,12 +38,16 @@ const AppearanceTab = {
     IntegerSetting,
     FloatSetting,
     UnitSetting,
-    ProfileSettingIndicator
+    ProfileSettingIndicator,
+    FontControl
   },
   computed: {
     horizontalUnits () {
       return defaultHorizontalUnits
     },
+    fontsOverride () {
+      return this.$store.getters.mergedConfig.fontsOverride
+    },
     columns () {
       const mode = this.$store.getters.mergedConfig.thirdColumnMode
 
diff --git a/src/components/settings_modal/tabs/appearance_tab.vue b/src/components/settings_modal/tabs/appearance_tab.vue
index 5356292e..fb24cc6b 100644
--- a/src/components/settings_modal/tabs/appearance_tab.vue
+++ b/src/components/settings_modal/tabs/appearance_tab.vue
@@ -34,6 +34,51 @@
             </small>
           </div>
         </li>
+        <li>
+          <h3>{{ $t('settings.style.interface_font_user_override') }}</h3>
+          <ul class="setting-list">
+            <li>
+              <FontControl
+                :model-value="fontsOverride.interface"
+                name="ui"
+                :label="$t('settings.style.fonts.components.interface')"
+                :fallback="{ family: 'sans-serif' }"
+                no-inherit="1"
+                @update:modelValue="v => $store.dispatch('setOption', { name: 'fontsOverride', value: { ...fontsOverride, interface: v } })"
+              />
+            </li>
+            <li>
+              <FontControl
+                v-if="expertLevel > 0"
+                :model-value="fontsOverride.input"
+                name="input"
+                :fallback="{ family: 'inherit' }"
+                :label="$t('settings.style.fonts.components.input')"
+                @update:modelValue="v => $store.dispatch('setOption', { name: 'fontsOverride', value: { ...fontsOverride, input: v } })"
+              />
+            </li>
+            <li>
+              <FontControl
+                v-if="expertLevel > 0"
+                :model-value="fontsOverride.post"
+                name="post"
+                :fallback="{ family: 'inherit' }"
+                :label="$t('settings.style.fonts.components.post')"
+                @update:modelValue="v => $store.dispatch('setOption', { name: 'fontsOverride', value: { ...fontsOverride, post: v } })"
+              />
+            </li>
+            <li>
+              <FontControl
+                v-if="expertLevel > 0"
+                :model-value="fontsOverride.postCode"
+                name="postCode"
+                :fallback="{ family: 'monospace' }"
+                :label="$t('settings.style.fonts.components.postCode')"
+                @update:modelValue="v => $store.dispatch('setOption', { name: 'fontsOverride', value: { ...fontsOverride, postCode: v } })"
+              />
+            </li>
+          </ul>
+        </li>
         <li>
           <UnitSetting
             path="emojiSize"
diff --git a/src/i18n/en.json b/src/i18n/en.json
index d995163a..a2c355fc 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -765,6 +765,7 @@
           "select": "Select font"
         }
       },
+      "interface_font_user_override": "Override theme/browser font used",
       "switcher": {
         "keep_color": "Keep colors",
         "keep_shadows": "Keep shadows",
diff --git a/src/modules/config.js b/src/modules/config.js
index a977c9c5..59d056d9 100644
--- a/src/modules/config.js
+++ b/src/modules/config.js
@@ -121,6 +121,7 @@ export const defaultState = {
   navbarSize: undefined, // instance default
   panelHeaderSize: undefined, // instance default
   forcedRoundness: undefined, // instance default
+  fontsOverride: undefined, // instance default
   navbarColumnStretch: false,
   greentext: undefined, // instance default
   useAtIcon: undefined, // instance default
@@ -184,6 +185,7 @@ const config = {
       applyConfig(state)
     },
     setOption (state, { name, value }) {
+      console.log('SET OPTION', state, name, value)
       set(state, name, value)
     },
     setHighlight (state, { user, color, type }) {
diff --git a/src/modules/instance.js b/src/modules/instance.js
index 6997e75d..602503ed 100644
--- a/src/modules/instance.js
+++ b/src/modules/instance.js
@@ -104,6 +104,7 @@ const defaultState = {
   navbarSize: '3.5rem',
   panelHeaderSize: '3.2rem',
   forcedRoundness: -1,
+  fontsOverride: {},
   virtualScrolling: true,
   sensitiveByDefault: false,
   conversationDisplay: 'linear',
-- 
cgit v1.2.3-70-g09d2


From c6ccab778f78bf65cebcad1c5e0943d427254098 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Wed, 10 Jul 2024 22:49:56 +0300
Subject: MASSIVELY streamlined theme setting process, now EVERYTHING happens
 in a vuex action "setTheme" instead of several different applyTheme()s
 scattered around

---
 src/boot/after_store.js                            |  29 +-----
 .../settings_modal/tabs/appearance_tab.js          |   5 +
 .../settings_modal/tabs/appearance_tab.vue         |   9 ++
 .../settings_modal/tabs/theme_tab/theme_tab.js     |   9 +-
 src/modules/config.js                              |  44 +++++----
 src/modules/instance.js                            | 101 ++++++++++++++++-----
 src/services/style_setter/style_setter.js          |  23 +----
 src/services/theme_data/theme2_to_theme3.js        |   1 +
 src/services/theme_data/theme_data_3.service.js    |   5 +-
 .../specs/services/theme_data/theme_data3.spec.js  |  44 +++++----
 10 files changed, 159 insertions(+), 111 deletions(-)

(limited to 'src/components/settings_modal/tabs/appearance_tab.vue')

diff --git a/src/boot/after_store.js b/src/boot/after_store.js
index b93e28a3..a486bd4c 100644
--- 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,29 +349,7 @@ const afterStoreSetup = async ({ store, i18n }) => {
   store.dispatch('setInstanceOption', { name: 'server', value: server })
 
   await setConfig({ store })
-
-  const { customTheme, customThemeSource, forceThemeRecompilation, themeDebug } = store.state.config
-  const { theme } = store.state.instance
-  const customThemePresent = customThemeSource || customTheme
-
-  console.log('DEBUG INITIAL', themeDebug, forceThemeRecompilation)
-
-  if (!forceThemeRecompilation && !themeDebug && tryLoadCache()) {
-    store.commit('setThemeApplied')
-  } else {
-    if (customThemePresent) {
-      if (customThemeSource && customThemeSource.themeEngineVersion === CURRENT_VERSION) {
-        applyTheme(customThemeSource, () => {}, themeDebug)
-      } else {
-        applyTheme(customTheme, () => {}, themeDebug)
-      }
-      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/settings_modal/tabs/appearance_tab.js b/src/components/settings_modal/tabs/appearance_tab.js
index 3776464a..d340fd00 100644
--- a/src/components/settings_modal/tabs/appearance_tab.js
+++ b/src/components/settings_modal/tabs/appearance_tab.js
@@ -29,6 +29,11 @@ const AppearanceTab = {
         key: mode,
         value: i - 1,
         label: this.$t(`settings.forced_roundness_mode_${mode}`)
+      })),
+      underlayOverrideModes: ['none', 'opaque', 'transparent'].map((mode, i) => ({
+        key: mode,
+        value: mode,
+        label: this.$t(`settings.underlay_override_mode_${mode}`)
       }))
     }
   },
diff --git a/src/components/settings_modal/tabs/appearance_tab.vue b/src/components/settings_modal/tabs/appearance_tab.vue
index fb24cc6b..d3df76a1 100644
--- a/src/components/settings_modal/tabs/appearance_tab.vue
+++ b/src/components/settings_modal/tabs/appearance_tab.vue
@@ -182,6 +182,15 @@
             {{ $t('settings.force_interface_roundness') }}
           </ChoiceSetting>
         </li>
+        <li>
+          <ChoiceSetting
+            id="underlayOverride"
+            path="theme3hacks.underlay"
+            :options="underlayOverrideModes"
+          >
+            {{ $t('settings.underlay_overrides') }}
+          </ChoiceSetting>
+        </li>
         <li v-if="instanceWallpaperUsed">
           <BooleanSetting path="hideInstanceWallpaper">
             {{ $t('settings.hide_wallpaper') }}
diff --git a/src/components/settings_modal/tabs/theme_tab/theme_tab.js b/src/components/settings_modal/tabs/theme_tab/theme_tab.js
index 72e2b625..7ca3b066 100644
--- a/src/components/settings_modal/tabs/theme_tab/theme_tab.js
+++ b/src/components/settings_modal/tabs/theme_tab/theme_tab.js
@@ -502,6 +502,7 @@ export default {
       this.$store.dispatch('setOption', {
         name: 'customTheme',
         value: {
+          ignore: true,
           themeFileVersion: this.selectedVersion,
           themeEngineVersion: CURRENT_VERSION,
           ...this.previewTheme
@@ -699,13 +700,13 @@ export default {
       }
     },
     updateTheme3Preview () {
-      console.log(this.previewTheme)
       const theme2 = convertTheme2To3(this.previewTheme)
       const theme3 = init({
-        extraRuleset: theme2,
+        inputRuleset: theme2,
         ultimateBackgroundColor: '#000000',
         liteMode: true
       })
+
       this.themeV3Preview = getCssRules(theme3.eager)
         .map(x => {
           if (x.startsWith('html')) {
@@ -722,7 +723,7 @@ export default {
   watch: {
     currentRadii () {
       try {
-        this.previewTheme.radii = generateRadii({ radii: this.currentRadii }).theme
+        this.previewTheme.radii = generateRadii({ radii: this.currentRadii }).theme.radii
         this.radiiInvalid = false
       } catch (e) {
         this.radiiInvalid = true
@@ -744,7 +745,7 @@ export default {
     fontsLocal: {
       handler () {
         try {
-          this.previewTheme.fonts = generateFonts({ fonts: this.fontsLocal }).theme
+          this.previewTheme.fonts = generateFonts({ fonts: this.fontsLocal }).theme.fonts
           this.fontsInvalid = false
         } catch (e) {
           this.fontsInvalid = true
diff --git a/src/modules/config.js b/src/modules/config.js
index 59d056d9..56151d2a 100644
--- 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]
 
@@ -81,6 +92,11 @@ export const defaultState = {
     chatMention: true,
     polls: true
   },
+  palette: null,
+  theme3hacks: {
+    underlay: 'none',
+    badgeColor: null
+  },
   webPushNotifications: false,
   webPushAlwaysShowNotifications: false,
   muteWords: [],
@@ -185,7 +201,6 @@ const config = {
       applyConfig(state)
     },
     setOption (state, { name, value }) {
-      console.log('SET OPTION', state, name, value)
       set(state, name, value)
     },
     setHighlight (state, { user, color, type }) {
@@ -261,30 +276,23 @@ const config = {
         }
       } else {
         commit('setOption', { name, value })
+        if (
+          name.startsWith('theme3hacks') ||
+            APPEARANCE_SETTINGS_KEYS.has(name)
+        ) {
+          applyConfig(state)
+        }
         switch (name) {
           case 'theme':
-            setPreset(value)
-            break
-          case 'sidebarColumnWidth':
-          case 'contentColumnWidth':
-          case 'notifsColumnWidth':
-          case 'textSize':
-          case 'navbarSize':
-          case 'panelHeaderSize':
-          case 'forcedRoundness':
-          case 'emojiSize':
-          case 'emojiReactionsScale':
-            applyConfig(state)
+            dispatch('setTheme', { themeName: value, recompile: true })
             break
           case 'customTheme':
           case 'customThemeSource': {
-            const { themeDebug } = state
-            applyTheme(value, () => {}, themeDebug)
+            if (!value.ignore) dispatch('setTheme', { themeData: value })
             break
           }
           case 'themeDebug': {
-            const { customTheme, customThemeSource } = state
-            applyTheme(customTheme || customThemeSource, () => {}, value)
+            dispatch('setTheme', { recompile: true })
             break
           }
           case 'interfaceLanguage':
diff --git a/src/modules/instance.js b/src/modules/instance.js
index 602503ed..85a966b8 100644
--- a/src/modules/instance.js
+++ b/src/modules/instance.js
@@ -1,5 +1,6 @@
-import { getPreset, applyTheme } from '../services/style_setter/style_setter.js'
-import { CURRENT_VERSION } from '../services/theme_data/theme_data.service.js'
+import { getPreset, applyTheme, tryLoadCache } from '../services/style_setter/style_setter.js'
+import { CURRENT_VERSION, generatePreset } from 'src/services/theme_data/theme_data.service.js'
+import { convertTheme2To3 } from 'src/services/theme_data/theme2_to_theme3.js'
 import apiService from '../services/api/api.service.js'
 import { instanceDefaultProperties } from './config.js'
 import { langCodeToCldrName, ensureFinalFallback } from '../i18n/languages.js'
@@ -286,9 +287,6 @@ const instance = {
             dispatch('initializeSocket')
           }
           break
-        case 'theme':
-          dispatch('setTheme', value)
-          break
       }
     },
     async getStaticEmoji ({ commit }) {
@@ -378,25 +376,86 @@ const instance = {
       }
     },
 
-    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, themeDebug } = 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, null, themeDebug)
+    setTheme ({ commit, state, rootState }, { themeName, themeData, recompile } = {}) {
+      // const {
+      //   themeApplied
+      // } = rootState.interface
+      const {
+        theme: instanceThemeName
+      } = state
+
+      const {
+        customTheme: userThemeSnapshot,
+        customThemeSource: userThemeSource,
+        forceThemeRecompilation,
+        themeDebug
+      } = rootState.config
+
+      const forceRecompile = forceThemeRecompilation || recompile
+
+      // If we're not not forced to recompile try using
+      // cache (tryLoadCache return true if load successful)
+      if (!forceRecompile && !themeDebug && tryLoadCache()) {
+        commit('setThemeApplied')
+      }
+
+      const normalizeThemeData = (themeData) => {
+        console.log('NORMAL', themeData)
+        if (themeData.themeFileVerison === 1) {
+          return generatePreset(themeData).theme
+        }
+        // New theme presets don't have 'theme' property, they use 'source'
+        const themeSource = themeData.source
+
+        let out // shout, shout let it all out
+        if (!themeData.theme || (themeSource && themeSource.themeEngineVersion === CURRENT_VERSION)) {
+          out = themeSource || themeData
+        } else {
+          out = themeData.theme
+        }
+
+        // generatePreset here basically creates/updates "snapshot",
+        // while also fixing the 2.2 -> 2.3 colors/shadows/etc
+        return generatePreset(out).theme
+      }
+
+      let promise = null
+
+      if (themeName) {
+        // commit('setInstanceOption', { name: 'theme', value: themeName })
+        promise = getPreset(themeName)
+          .then(themeData => {
+            // commit('setInstanceOption', { name: 'themeData', value: themeData })
+            return normalizeThemeData(themeData)
+          })
+      } else if (themeData) {
+        promise = Promise.resolve(normalizeThemeData(themeData))
+      } else {
+        if (userThemeSource || userThemeSnapshot) {
+          if (userThemeSource && userThemeSource.themeEngineVersion === CURRENT_VERSION) {
+            promise = Promise.resolve(normalizeThemeData(userThemeSource))
           } else {
-            applyTheme(themeData.theme, null, themeDebug)
+            promise = Promise.resolve(normalizeThemeData(userThemeSnapshot))
           }
-          commit('setThemeApplied')
+        } else if (instanceThemeName) {
+          promise = getPreset(themeName).then(themeData => normalizeThemeData(themeData))
+        }
+      }
+
+      promise
+        .then(realThemeData => {
+          console.log('FR FR 1', realThemeData)
+          const ruleset = convertTheme2To3(realThemeData)
+          console.log('FR FR 2', ruleset)
+
+          applyTheme(
+            ruleset,
+            () => commit('setThemeApplied'),
+            themeDebug
+          )
         })
+
+      return promise
     },
     fetchEmoji ({ dispatch, state }) {
       if (!state.customEmojiFetched) {
diff --git a/src/services/style_setter/style_setter.js b/src/services/style_setter/style_setter.js
index 78e7428d..e54a95bf 100644
--- 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,30 +43,20 @@ const adoptStyleSheets = (styles) => {
   // is nothing to do here.
 }
 
-export const generateTheme = async (input, callbacks, debug) => {
+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,
-    ultimateBackgroundColor: extraRules[0].directives['--bg'].split('|')[1].trim(),
+    inputRuleset,
+    ultimateBackgroundColor: inputRuleset[0].directives['--bg'].split('|')[1].trim(),
     debug
   })
 
-  console.log('DEBUG 2 IS', debug)
-
   getCssRules(themes3.eager, debug).forEach(rule => {
     // Hacks to support multiple selectors on same component
     if (rule.match(/::-webkit-scrollbar-button/)) {
@@ -162,8 +150,6 @@ export const applyTheme = async (input, onFinish = (data) => {}, debug) => {
   const eagerStyles = createStyleSheet(EAGER_STYLE_ID)
   const lazyStyles = createStyleSheet(LAZY_STYLE_ID)
 
-  console.log('DEBUG IS', debug)
-
   const { lazyProcessFunc } = await generateTheme(
     input,
     {
@@ -216,7 +202,6 @@ const extractStyleConfig = ({
     textSize
   }
 
-  console.log(forcedRoundness)
   switch (forcedRoundness) {
     case 'disable':
       break
@@ -325,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/theme2_to_theme3.js b/src/services/theme_data/theme2_to_theme3.js
index 2c97d18b..b54366bd 100644
--- a/src/services/theme_data/theme2_to_theme3.js
+++ b/src/services/theme_data/theme2_to_theme3.js
@@ -265,6 +265,7 @@ 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 = { source: '2to3' }
 
diff --git a/src/services/theme_data/theme_data_3.service.js b/src/services/theme_data/theme_data_3.service.js
index e98b19a7..e802a893 100644
--- a/src/services/theme_data/theme_data_3.service.js
+++ b/src/services/theme_data/theme_data_3.service.js
@@ -150,12 +150,13 @@ const ruleToSelector = genericRuleToSelector(components)
 export const getEngineChecksum = () => engineChecksum
 
 export const init = ({
-  extraRuleset,
+  inputRuleset,
   ultimateBackgroundColor,
   debug = false,
   liteMode = false,
   rootComponentName = 'Root'
 }) => {
+  if (!inputRuleset) throw new Error('Ruleset is null or undefined!')
   const staticVars = {}
   const stacked = {}
   const computed = {}
@@ -164,7 +165,7 @@ export const init = ({
     ...Object.values(components)
       .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
diff --git a/test/unit/specs/services/theme_data/theme_data3.spec.js b/test/unit/specs/services/theme_data/theme_data3.spec.js
index bb8d785c..92a87de9 100644
--- 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({ ruleset: [], 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({
+        ruleset: [{
+          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({
+        ruleset: [{
+          component: 'Root',
+          directives: {
+            '--bg': 'color | #008080'
+          }
+        }, {
+          component: 'Panel',
+          directives: {
+            opacity: 0.5
+          }
+        }],
+        ultimateBackgroundColor: '#DEADAF'
+      })
 
       expect(out.staticVars).to.have.property('bg').equal('#008080')
 
-- 
cgit v1.2.3-70-g09d2


From a378c999b78c9138abb8ea1982dca2f976381f10 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Tue, 16 Jul 2024 21:01:20 +0300
Subject: add ability to override underlay color/opacity regardless of theme

---
 .../settings_modal/tabs/appearance_tab.js          |  4 +--
 .../settings_modal/tabs/appearance_tab.vue         |  4 +--
 src/i18n/en.json                                   | 16 ++++++----
 src/modules/config.js                              |  8 ++---
 src/modules/interface.js                           | 34 ++++++++++++++++++++--
 src/services/theme_data/theme2_to_theme3.js        |  4 ++-
 6 files changed, 54 insertions(+), 16 deletions(-)

(limited to 'src/components/settings_modal/tabs/appearance_tab.vue')

diff --git a/src/components/settings_modal/tabs/appearance_tab.js b/src/components/settings_modal/tabs/appearance_tab.js
index d340fd00..4f988508 100644
--- a/src/components/settings_modal/tabs/appearance_tab.js
+++ b/src/components/settings_modal/tabs/appearance_tab.js
@@ -28,12 +28,12 @@ const AppearanceTab = {
       forcedRoundnessOptions: ['disabled', 'sharp', 'nonsharp', 'round'].map((mode, i) => ({
         key: mode,
         value: i - 1,
-        label: this.$t(`settings.forced_roundness_mode_${mode}`)
+        label: this.$t(`settings.style.themes3.hacks.forced_roundness_mode_${mode}`)
       })),
       underlayOverrideModes: ['none', 'opaque', 'transparent'].map((mode, i) => ({
         key: mode,
         value: mode,
-        label: this.$t(`settings.underlay_override_mode_${mode}`)
+        label: this.$t(`settings.style.themes3.hacks.underlay_override_mode_${mode}`)
       }))
     }
   },
diff --git a/src/components/settings_modal/tabs/appearance_tab.vue b/src/components/settings_modal/tabs/appearance_tab.vue
index d3df76a1..85084e29 100644
--- a/src/components/settings_modal/tabs/appearance_tab.vue
+++ b/src/components/settings_modal/tabs/appearance_tab.vue
@@ -179,7 +179,7 @@
             path="forcedRoundness"
             :options="forcedRoundnessOptions"
           >
-            {{ $t('settings.force_interface_roundness') }}
+            {{ $t('settings.style.themes3.hacks.force_interface_roundness') }}
           </ChoiceSetting>
         </li>
         <li>
@@ -188,7 +188,7 @@
             path="theme3hacks.underlay"
             :options="underlayOverrideModes"
           >
-            {{ $t('settings.underlay_overrides') }}
+            {{ $t('settings.style.themes3.hacks.underlay_overrides') }}
           </ChoiceSetting>
         </li>
         <li v-if="instanceWallpaperUsed">
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 770a4a65..a1946aed 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -386,11 +386,6 @@
     "navbar_size": "Top bar size",
     "panel_header_size": "Panel header size",
     "visual_tweaks": "Minor visual tweaks",
-    "force_interface_roundness": "Override interface roundness/sharpness",
-    "forced_roundness_mode_disabled": "Use theme defaults",
-    "forced_roundness_mode_sharp": "Force sharp edges",
-    "forced_roundness_mode_nonsharp": "Force not-so-sharp (1px roundness) edges",
-    "forced_roundness_mode_round": "Force round edges",
     "theme_debug": "Show what background theme engine assumes when dealing with transparancy (DEBUG)",
     "scale_and_layout": "Interface scale and layout",
     "mfa": {
@@ -749,6 +744,17 @@
       "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" : {
diff --git a/src/modules/config.js b/src/modules/config.js
index 109f2742..906a64b0 100644
--- a/src/modules/config.js
+++ b/src/modules/config.js
@@ -282,12 +282,12 @@ const config = {
         }
       } else {
         commit('setOption', { name, value })
-        if (
-          name.startsWith('theme3hacks') ||
-            APPEARANCE_SETTINGS_KEYS.has(name)
-        ) {
+        if (APPEARANCE_SETTINGS_KEYS.has(name)) {
           applyConfig(state)
         }
+        if (name.startsWith('theme3hacks')) {
+          dispatch('setTheme', { recompile: true })
+        }
         switch (name) {
           case 'theme':
             dispatch('setTheme', { themeName: value, recompile: true })
diff --git a/src/modules/interface.js b/src/modules/interface.js
index 6a749bc3..a9cd70e5 100644
--- a/src/modules/interface.js
+++ b/src/modules/interface.js
@@ -221,7 +221,8 @@ const interfaceMod = {
         customTheme: userThemeSnapshot,
         customThemeSource: userThemeSource,
         forceThemeRecompilation,
-        themeDebug
+        themeDebug,
+        theme3hacks
       } = rootState.config
 
       const forceRecompile = forceThemeRecompilation || recompile
@@ -275,7 +276,36 @@ const interfaceMod = {
 
       promise
         .then(realThemeData => {
-          const ruleset = convertTheme2To3(realThemeData)
+          const theme2ruleset = convertTheme2To3(realThemeData)
+          const hacks = []
+
+          Object.entries(theme3hacks).forEach(([key, value]) => {
+            switch (key) {
+              case 'underlay': {
+                if (value !== 'none') {
+                  const newRule = {
+                    component: 'Underlay',
+                    directives: {}
+                  }
+                  if (value === 'opaque') {
+                    newRule.directives.opacity = 1
+                    newRule.directives.background = '--wallpaper'
+                  }
+                  if (value === 'transparent') {
+                    newRule.directives.opacity = 0
+                  }
+                  console.log('NEW RULE', newRule)
+                  hacks.push(newRule)
+                }
+                break
+              }
+            }
+          })
+
+          const ruleset = [
+            ...theme2ruleset,
+            ...hacks
+          ]
 
           applyTheme(
             ruleset,
diff --git a/src/services/theme_data/theme2_to_theme3.js b/src/services/theme_data/theme2_to_theme3.js
index b54366bd..95eb03c1 100644
--- 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([
-- 
cgit v1.2.3-70-g09d2


From 40c9163d215b5ac7b69437f3585586b2174211ca Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Wed, 17 Jul 2024 17:19:57 +0300
Subject: optimizations, WIP theme selector

---
 src/components/panel.style.js                      |  10 ++
 src/components/root.style.js                       |   5 +
 .../settings_modal/tabs/appearance_tab.js          |  61 +++++++++++-
 .../settings_modal/tabs/appearance_tab.vue         |  30 ++++++
 .../settings_modal/tabs/theme_tab/preview.vue      | 104 +++++++++++++++++++-
 .../settings_modal/tabs/theme_tab/theme_tab.js     |  20 ++--
 .../settings_modal/tabs/theme_tab/theme_tab.scss   | 105 ---------------------
 src/components/status/post.style.js                |   9 ++
 src/modules/interface.js                           |  54 +++++++----
 src/services/theme_data/css_utils.js               |  12 +++
 src/services/theme_data/iss_utils.js               |  39 +++++++-
 src/services/theme_data/theme_data_3.service.js    |  70 ++++++++++----
 12 files changed, 361 insertions(+), 158 deletions(-)

(limited to 'src/components/settings_modal/tabs/appearance_tab.vue')

diff --git a/src/components/panel.style.js b/src/components/panel.style.js
index ad16c18f..1bba4766 100644
--- 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
index f9bdf16e..4bd735aa 100644
--- 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
index 4f988508..10de0397 100644
--- 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
index 85084e29..2488720d 100644
--- a/src/components/settings_modal/tabs/appearance_tab.vue
+++ b/src/components/settings_modal/tabs/appearance_tab.vue
@@ -1,5 +1,21 @@
 <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">
@@ -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
index 1837620f..7ec34afd 100644
--- 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
index 8d025ea4..a19550d1 100644
--- 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
index 4cb37c1e..3ae8864c 100644
--- 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
index 8dce527e..d0038424 100644
--- 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
index a9cd70e5..a0746052 100644
--- 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
index 8423e8ac..9bce4834 100644
--- 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
index 83ca8242..75f8dd93 100644
--- 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
index e802a893..cf58da11 100644
--- 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'),
-- 
cgit v1.2.3-70-g09d2


From 9bbdad1a6f4c3d52569f4c58c04dace95d9a6bb3 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Wed, 17 Jul 2024 19:58:04 +0300
Subject: theme selector new

---
 .../settings_modal/tabs/appearance_tab.js          | 54 ++++++++++++++++++----
 .../settings_modal/tabs/appearance_tab.vue         | 49 ++++++++++++++------
 2 files changed, 80 insertions(+), 23 deletions(-)

(limited to 'src/components/settings_modal/tabs/appearance_tab.vue')

diff --git a/src/components/settings_modal/tabs/appearance_tab.js b/src/components/settings_modal/tabs/appearance_tab.js
index 10de0397..ef367e41 100644
--- a/src/components/settings_modal/tabs/appearance_tab.js
+++ b/src/components/settings_modal/tabs/appearance_tab.js
@@ -35,6 +35,7 @@ const AppearanceTab = {
   data () {
     return {
       availableStyles: [],
+      intersectionObserver: null,
       thirdColumnModeOptions: ['none', 'notifications', 'postform'].map(mode => ({
         key: mode,
         value: mode,
@@ -62,9 +63,7 @@ const AppearanceTab = {
     FontControl,
     Preview
   },
-  created () {
-    const self = this
-
+  mounted () {
     getThemes()
       .then((promises) => {
         return Promise.all(
@@ -74,19 +73,48 @@ const AppearanceTab = {
       })
       .then(themes => themes.reduce((acc, [k, v]) => {
         if (v) {
-          return {
+          return [
             ...acc,
-            [k]: v
-          }
+            {
+              name: v.name || v[0],
+              key: k,
+              data: v
+            }
+          ]
         } else {
           return acc
         }
-      }, {}))
+      }, []))
       .then((themesComplete) => {
-        self.availableStyles = 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(() => {
+            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
     },
@@ -119,7 +147,12 @@ const AppearanceTab = {
     ...SharedComputedObject()
   },
   methods: {
-    previewTheme (input) {
+    isThemeActive (key, name) {
+      console.log(this.$store.getters.mergedConfig)
+      const { customTheme, themeName, customThemeSource } = this.$store.getters.mergedConfig
+      console.log(customTheme, customThemeSource, themeName)
+    },
+    previewTheme (key, input) {
       const style = normalizeThemeData(input)
       const x = 2
       if (x === 1) return
@@ -128,12 +161,13 @@ const AppearanceTab = {
         inputRuleset: theme2,
         ultimateBackgroundColor: '#000000',
         liteMode: true,
+        debug: true,
         onlyNormalState: true
       })
 
       return getScopedVersion(
         getCssRules(theme3.eager),
-        '#theme-preview-' + (input.name || input[0]).replace(/ /g, '_')
+        '#theme-preview-' + key
       ).join('\n')
     }
   }
diff --git a/src/components/settings_modal/tabs/appearance_tab.vue b/src/components/settings_modal/tabs/appearance_tab.vue
index 2488720d..0954d3a7 100644
--- a/src/components/settings_modal/tabs/appearance_tab.vue
+++ b/src/components/settings_modal/tabs/appearance_tab.vue
@@ -2,18 +2,26 @@
   <div :label="$t('settings.general')">
     <div class="setting-item">
       <h2>{{ $t('settings.theme') }}</h2>
-      <ul class="theme-list">
-        <li
+      <ul
+        class="input theme-list"
+        ref="themeList"
+      >
+        <button
           v-for="style in availableStyles"
-          :key="style.name || style[0]"
-          class="theme-preview"
+          :data-theme-key="style.key"
+          :key="style.key"
+          class="button-default 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)"/>
+          <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 :id="'theme-preview-' + (style[0] || style.name).replace(/ /g,'_')"/>
-        </li>
+          <preview :class="{ placeholder: ready }" :id="'theme-preview-' + style.key"/>
+          <h4 class="theme-name">{{ style.name }}</h4>
+        </button>
       </ul>
     </div>
     <div class="setting-item">
@@ -252,13 +260,28 @@
   list-style: none;
   display: flex;
   flex-wrap: wrap;
-}
+  margin: 0 -0.5em;
+  height: 15em;
+  overflow-x: hidden;
+  overflow-y: auto;
+  scrollbar-gutter: stable;
+
+  .theme-preview {
+    width: 21rem;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    margin: 0.5em;
 
-.theme-preview {
-  width: 10rem;
+    &.placeholder {
+      opacity: 0.2;
+    }
 
-  .preview-container {
-    zoom: 0.33;
+    .preview-container {
+      zoom: 0.5;
+      border: none;
+      border-radius: var(--roundness);
+    }
   }
 }
 </style>
-- 
cgit v1.2.3-70-g09d2


From d2683a672859cb0fb46037540f62f4fdb07407c1 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Wed, 17 Jul 2024 22:10:11 +0300
Subject: new theme selector, RC

---
 .../settings_modal/tabs/appearance_tab.js          | 17 +++++++---
 .../settings_modal/tabs/appearance_tab.vue         | 23 ++++++++++---
 .../settings_modal/tabs/theme_tab/theme_tab.js     |  2 +-
 src/i18n/en.json                                   |  1 +
 src/modules/config.js                              | 35 +++++++++++++-------
 src/modules/interface.js                           | 38 +++++++++++++---------
 6 files changed, 78 insertions(+), 38 deletions(-)

(limited to 'src/components/settings_modal/tabs/appearance_tab.vue')

diff --git a/src/components/settings_modal/tabs/appearance_tab.js b/src/components/settings_modal/tabs/appearance_tab.js
index ef367e41..7ec22641 100644
--- a/src/components/settings_modal/tabs/appearance_tab.js
+++ b/src/components/settings_modal/tabs/appearance_tab.js
@@ -95,7 +95,7 @@ const AppearanceTab = {
           if (!isIntersecting) return
           const theme = this.availableStyles.find(x => x.key === target.dataset.themeKey)
           this.$nextTick(() => {
-            theme.ready = true
+            if (theme) theme.ready = true
           })
           observer.unobserve(target)
         })
@@ -144,13 +144,20 @@ const AppearanceTab = {
         this.$store.dispatch('setOption', { name: 'interfaceLanguage', value: val })
       }
     },
+    isCustomThemeUsed () {
+      const { theme } = this.mergedConfig
+      return theme === 'custom' || theme === null
+    },
     ...SharedComputedObject()
   },
   methods: {
-    isThemeActive (key, name) {
-      console.log(this.$store.getters.mergedConfig)
-      const { customTheme, themeName, customThemeSource } = this.$store.getters.mergedConfig
-      console.log(customTheme, customThemeSource, themeName)
+    isThemeActive (key) {
+      const { theme } = this.mergedConfig
+      console.log(key, theme)
+      return key === theme
+    },
+    setTheme (name) {
+      this.$store.dispatch('setTheme', { themeName: name, saveData: true, recompile: true })
     },
     previewTheme (key, input) {
       const style = normalizeThemeData(input)
diff --git a/src/components/settings_modal/tabs/appearance_tab.vue b/src/components/settings_modal/tabs/appearance_tab.vue
index 0954d3a7..748a69ce 100644
--- a/src/components/settings_modal/tabs/appearance_tab.vue
+++ b/src/components/settings_modal/tabs/appearance_tab.vue
@@ -3,14 +3,24 @@
     <div class="setting-item">
       <h2>{{ $t('settings.theme') }}</h2>
       <ul
-        class="input theme-list"
+        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
@@ -260,14 +270,17 @@
   list-style: none;
   display: flex;
   flex-wrap: wrap;
-  margin: 0 -0.5em;
-  height: 15em;
+  margin: -0.5em 0;
+  height: 25em;
   overflow-x: hidden;
   overflow-y: auto;
   scrollbar-gutter: stable;
+  border-radius: var(--roundness);
+  border: 1px solid var(--border);
+  padding: 0;
 
   .theme-preview {
-    width: 21rem;
+    width: 19rem;
     display: flex;
     flex-direction: column;
     align-items: center;
@@ -278,9 +291,11 @@
     }
 
     .preview-container {
+      pointer-events: none;
       zoom: 0.5;
       border: none;
       border-radius: var(--roundness);
+      text-align: left;
     }
   }
 }
diff --git a/src/components/settings_modal/tabs/theme_tab/theme_tab.js b/src/components/settings_modal/tabs/theme_tab/theme_tab.js
index a19550d1..39dc372e 100644
--- a/src/components/settings_modal/tabs/theme_tab/theme_tab.js
+++ b/src/components/settings_modal/tabs/theme_tab/theme_tab.js
@@ -606,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 {
diff --git a/src/i18n/en.json b/src/i18n/en.json
index a1946aed..c3357ef5 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -740,6 +740,7 @@
     "enable_web_push_always_show_tip": "Some browsers (Chromium, Chrome) require that push messages always result in a notification, otherwise generic 'Website was updated in background' is shown, enable this to prevent this notification from showing, as Chrome seem to hide push notifications if tab is in focus. Can result in showing duplicate notifications on other browsers.",
     "more_settings": "More settings",
     "style": {
+      "custom_theme_used": "(Custom theme)",
       "themes2_outdated": "Editor for Themes V2 is no longer supported and presented here for sake of legacy.",
       "update_preview": "Update preview",
       "themes3": {
diff --git a/src/modules/config.js b/src/modules/config.js
index 906a64b0..e54a883c 100644
--- a/src/modules/config.js
+++ b/src/modules/config.js
@@ -35,11 +35,26 @@ export const multiChoiceProperties = [
 
 export const defaultState = {
   expertLevel: 0, // used to track which settings to show and hide
-  colors: {},
-  theme: undefined,
-  customTheme: undefined,
-  customThemeSource: undefined,
-  forceThemeRecompilation: false,
+
+  // Theme  stuff
+  theme: undefined, // Very old theme store, stores preset name, still in use
+
+  // V1
+  colors: {}, // VERY old theme store, just colors of V1, probably not even used anymore
+
+  // V2
+  customTheme: undefined, // "snapshot", previously was used as actual theme store for V2 so it's still used in case of PleromaFE downgrade event.
+  customThemeSource: undefined, // "source", stores original theme data
+
+  // V3
+  themeDebug: false, // debug mode that uses computed backgrounds instead of real ones to debug contrast functions
+  forceThemeRecompilation: false, //  flag that forces recompilation on boot even if cache exists
+  palette: null, // not used yet, will be used for V3
+  theme3hacks: { // Hacks, user overrides that are independent of theme used
+    underlay: 'none',
+    badgeColor: null
+  },
+
   hideISP: false,
   hideInstanceWallpaper: false,
   hideShoutbox: false,
@@ -92,11 +107,6 @@ export const defaultState = {
     chatMention: true,
     polls: true
   },
-  palette: null,
-  theme3hacks: {
-    underlay: 'none',
-    badgeColor: null
-  },
   webPushNotifications: false,
   webPushAlwaysShowNotifications: false,
   muteWords: [],
@@ -164,7 +174,6 @@ export const defaultState = {
   maxDepthInThread: undefined, // instance default
   autocompleteSelect: undefined, // instance default
   closingDrawerMarksAsSeen: undefined, // instance default
-  themeDebug: false,
   unseenAtTop: undefined, // instance default
   ignoreInactionableSeen: undefined // instance default
 }
@@ -256,6 +265,7 @@ const config = {
       })
     },
     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 })
@@ -290,7 +300,8 @@ const config = {
         }
         switch (name) {
           case 'theme':
-            dispatch('setTheme', { themeName: value, recompile: true })
+            if (value === 'custom') break
+            dispatch('setTheme', { themeName: value, recompile: true, saveData: true })
             break
           case 'themeDebug': {
             dispatch('setTheme', { recompile: true })
diff --git a/src/modules/interface.js b/src/modules/interface.js
index a0746052..b56b60ec 100644
--- a/src/modules/interface.js
+++ b/src/modules/interface.js
@@ -212,12 +212,13 @@ const interfaceMod = {
     setLastTimeline ({ commit }, value) {
       commit('setLastTimeline', value)
     },
-    setTheme ({ commit, rootState }, { themeName, themeData, recompile } = {}) {
+    setTheme ({ commit, rootState }, { themeName, themeData, recompile, saveData } = {}) {
       const {
         theme: instanceThemeName
       } = rootState.instance
 
       const {
+        theme: userThemeName,
         customTheme: userThemeSnapshot,
         customThemeSource: userThemeSource,
         forceThemeRecompilation,
@@ -225,6 +226,8 @@ const interfaceMod = {
         theme3hacks
       } = rootState.config
 
+      const actualThemeName = userThemeName || instanceThemeName
+
       const forceRecompile = forceThemeRecompilation || recompile
 
       // If we're not not forced to recompile try using
@@ -236,28 +239,31 @@ const interfaceMod = {
 
       let promise = null
 
-      if (themeName) {
-        promise = getPreset(themeName)
-          .then(themeData => {
-            return normalizeThemeData(themeData)
-          })
-      } else if (themeData) {
+      if (themeData) {
         promise = Promise.resolve(normalizeThemeData(themeData))
-      } else {
-        if (userThemeSource || userThemeSnapshot) {
-          if (userThemeSource && userThemeSource.themeEngineVersion === CURRENT_VERSION) {
-            promise = Promise.resolve(normalizeThemeData(userThemeSource))
-          } else {
-            promise = Promise.resolve(normalizeThemeData(userThemeSnapshot))
-          }
-        } else if (instanceThemeName) {
-          promise = getPreset(themeName).then(themeData => normalizeThemeData(themeData))
+      } 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 => normalizeThemeData(themeData))
+      } else {
+        throw new Error('Cannot load any theme!')
       }
 
       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]) => {
-- 
cgit v1.2.3-70-g09d2


From 665faf892a418fa1e7aeda1d1c82d926a2ed4f3a Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Sun, 21 Jul 2024 23:27:11 +0300
Subject: font overrides + fixes for font component

---
 src/components/font_control/font_control.vue       |  2 +-
 .../settings_modal/tabs/appearance_tab.js          | 14 +++++++-
 .../settings_modal/tabs/appearance_tab.vue         | 18 +++++-----
 src/modules/config.js                              |  9 +++--
 src/modules/interface.js                           | 40 ++++++++++++++++++++++
 5 files changed, 70 insertions(+), 13 deletions(-)

(limited to 'src/components/settings_modal/tabs/appearance_tab.vue')

diff --git a/src/components/font_control/font_control.vue b/src/components/font_control/font_control.vue
index f3ab41a2..b5423a8c 100644
--- a/src/components/font_control/font_control.vue
+++ b/src/components/font_control/font_control.vue
@@ -84,7 +84,7 @@
           :id="name + '-local-font-switcher'"
           :model-value="modelValue.family"
           class="custom-font"
-          @update:modelValue="$emit('update:modelValue', { ...(modelValue || {}), family: $event.target.value })"
+          @update:modelValue="v => $emit('update:modelValue', { ...(modelValue || {}), family: v })"
         >
           <optgroup
             :label="$t('settings.style.themes3.font.group-builtin')"
diff --git a/src/components/settings_modal/tabs/appearance_tab.js b/src/components/settings_modal/tabs/appearance_tab.js
index 7ec22641..98d38288 100644
--- a/src/components/settings_modal/tabs/appearance_tab.js
+++ b/src/components/settings_modal/tabs/appearance_tab.js
@@ -151,9 +151,21 @@ const AppearanceTab = {
     ...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
-      console.log(key, theme)
       return key === theme
     },
     setTheme (name) {
diff --git a/src/components/settings_modal/tabs/appearance_tab.vue b/src/components/settings_modal/tabs/appearance_tab.vue
index 748a69ce..3d3655d7 100644
--- a/src/components/settings_modal/tabs/appearance_tab.vue
+++ b/src/components/settings_modal/tabs/appearance_tab.vue
@@ -73,42 +73,42 @@
           <ul class="setting-list">
             <li>
               <FontControl
-                :model-value="fontsOverride.interface"
+                :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 => $store.dispatch('setOption', { name: 'fontsOverride', value: { ...fontsOverride, interface: v } })"
+                @update:modelValue="v => updateFont('interface', v)"
               />
             </li>
             <li>
               <FontControl
                 v-if="expertLevel > 0"
-                :model-value="fontsOverride.input"
+                :model-value="mergedConfig.theme3hacks.fonts.input"
                 name="input"
                 :fallback="{ family: 'inherit' }"
                 :label="$t('settings.style.fonts.components.input')"
-                @update:modelValue="v => $store.dispatch('setOption', { name: 'fontsOverride', value: { ...fontsOverride, input: v } })"
+                @update:modelValue="v => updateFont('input', v)"
               />
             </li>
             <li>
               <FontControl
                 v-if="expertLevel > 0"
-                :model-value="fontsOverride.post"
+                :model-value="mergedConfig.theme3hacks.fonts.post"
                 name="post"
                 :fallback="{ family: 'inherit' }"
                 :label="$t('settings.style.fonts.components.post')"
-                @update:modelValue="v => $store.dispatch('setOption', { name: 'fontsOverride', value: { ...fontsOverride, post: v } })"
+                @update:modelValue="v => updateFont('post', v)"
               />
             </li>
             <li>
               <FontControl
                 v-if="expertLevel > 0"
-                :model-value="fontsOverride.postCode"
+                :model-value="mergedConfig.theme3hacks.fonts.monospace"
                 name="postCode"
                 :fallback="{ family: 'monospace' }"
-                :label="$t('settings.style.fonts.components.postCode')"
-                @update:modelValue="v => $store.dispatch('setOption', { name: 'fontsOverride', value: { ...fontsOverride, postCode: v } })"
+                :label="$t('settings.style.fonts.components.monospace')"
+                @update:modelValue="v => updateFont('monospace', v)"
               />
             </li>
           </ul>
diff --git a/src/modules/config.js b/src/modules/config.js
index e54a883c..88e339cf 100644
--- a/src/modules/config.js
+++ b/src/modules/config.js
@@ -52,7 +52,13 @@ export const defaultState = {
   palette: null, // not used yet, will be used for V3
   theme3hacks: { // Hacks, user overrides that are independent of theme used
     underlay: 'none',
-    badgeColor: null
+    badgeColor: null,
+    fonts: {
+      interface: undefined,
+      input: undefined,
+      post: undefined,
+      monospace: undefined
+    }
   },
 
   hideISP: false,
@@ -147,7 +153,6 @@ export const defaultState = {
   navbarSize: undefined, // instance default
   panelHeaderSize: undefined, // instance default
   forcedRoundness: undefined, // instance default
-  fontsOverride: undefined, // instance default
   navbarColumnStretch: false,
   greentext: undefined, // instance default
   useAtIcon: undefined, // instance default
diff --git a/src/modules/interface.js b/src/modules/interface.js
index 38ee88db..206c68a7 100644
--- a/src/modules/interface.js
+++ b/src/modules/interface.js
@@ -274,6 +274,46 @@ const interfaceMod = {
 
           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 = {
-- 
cgit v1.2.3-70-g09d2


From 292895913892ac5eb7e1e8855775e97a7ea1eb18 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Tue, 23 Jul 2024 16:29:09 +0300
Subject: fix huge previews on firefox

---
 src/components/settings_modal/tabs/appearance_tab.vue | 1 +
 1 file changed, 1 insertion(+)

(limited to 'src/components/settings_modal/tabs/appearance_tab.vue')

diff --git a/src/components/settings_modal/tabs/appearance_tab.vue b/src/components/settings_modal/tabs/appearance_tab.vue
index 3d3655d7..2b5c4db9 100644
--- a/src/components/settings_modal/tabs/appearance_tab.vue
+++ b/src/components/settings_modal/tabs/appearance_tab.vue
@@ -280,6 +280,7 @@
   padding: 0;
 
   .theme-preview {
+    font-size: 1rem; // fix for firefox
     width: 19rem;
     display: flex;
     flex-direction: column;
-- 
cgit v1.2.3-70-g09d2


From f4544f0742508713a9cef2db08c6fa844345d9e0 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Tue, 23 Jul 2024 21:35:29 +0300
Subject: elaboration and spacing

---
 .../settings_modal/helpers/unit_setting.vue        |  1 +
 .../settings_modal/tabs/appearance_tab.vue         | 86 ++++++++++++----------
 .../settings_modal/tabs/theme_tab/theme_tab.scss   |  5 ++
 .../settings_modal/tabs/theme_tab/theme_tab.vue    |  2 +-
 src/i18n/en.json                                   |  3 +-
 5 files changed, 57 insertions(+), 40 deletions(-)

(limited to 'src/components/settings_modal/tabs/appearance_tab.vue')

diff --git a/src/components/settings_modal/helpers/unit_setting.vue b/src/components/settings_modal/helpers/unit_setting.vue
index e5da9012..40ab6880 100644
--- a/src/components/settings_modal/helpers/unit_setting.vue
+++ b/src/components/settings_modal/helpers/unit_setting.vue
@@ -9,6 +9,7 @@
     >
       <slot />
     </label>
+    {{ ' ' }}
     <input
       :id="path"
       class="input number-input"
diff --git a/src/components/settings_modal/tabs/appearance_tab.vue b/src/components/settings_modal/tabs/appearance_tab.vue
index 2b5c4db9..55f0d1be 100644
--- a/src/components/settings_modal/tabs/appearance_tab.vue
+++ b/src/components/settings_modal/tabs/appearance_tab.vue
@@ -1,5 +1,5 @@
 <template>
-  <div :label="$t('settings.general')">
+  <div class="appearance-tab" :label="$t('settings.general')">
     <div class="setting-item">
       <h2>{{ $t('settings.theme') }}</h2>
       <ul
@@ -34,6 +34,9 @@
         </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">
@@ -254,49 +257,56 @@
 <script src="./appearance_tab.js"></script>
 
 <style lang="scss">
-.column-settings {
-  display: flex;
-  justify-content: space-evenly;
-  flex-wrap: wrap;
-}
+.appearance-tab {
+  .theme-notice {
+    padding: 0.5em;
+    margin: 1em;
+  }
 
-.column-settings .size-label {
-  display: block;
-  margin-bottom: 0.5em;
-  margin-top: 0.5em;
-}
+  .column-settings {
+    display: flex;
+    justify-content: space-evenly;
+    flex-wrap: wrap;
+  }
 
-.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;
+  .column-settings .size-label {
+    display: block;
+    margin-bottom: 0.5em;
+    margin-top: 0.5em;
+  }
 
-  .theme-preview {
-    font-size: 1rem; // fix for firefox
-    width: 19rem;
+  .theme-list {
+    list-style: none;
     display: flex;
-    flex-direction: column;
-    align-items: center;
-    margin: 0.5em;
+    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;
 
-    &.placeholder {
-      opacity: 0.2;
-    }
+    .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;
+      .preview-container {
+        pointer-events: none;
+        zoom: 0.5;
+        border: none;
+        border-radius: var(--roundness);
+        text-align: left;
+      }
     }
   }
 }
diff --git a/src/components/settings_modal/tabs/theme_tab/theme_tab.scss b/src/components/settings_modal/tabs/theme_tab/theme_tab.scss
index 3ae8864c..84933fb8 100644
--- 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 {
diff --git a/src/components/settings_modal/tabs/theme_tab/theme_tab.vue b/src/components/settings_modal/tabs/theme_tab/theme_tab.vue
index dd23145c..4498c143 100644
--- a/src/components/settings_modal/tabs/theme_tab/theme_tab.vue
+++ b/src/components/settings_modal/tabs/theme_tab/theme_tab.vue
@@ -1,6 +1,6 @@
 <template>
   <div class="theme-tab">
-    <div class="alert warning">
+    <div class="alert warning deprecation-warning">
       {{ $t("settings.style.themes2_outdated") }}
     </div>
     <div class="presets-container">
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 7c6f4217..97973c15 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -741,7 +741,8 @@
     "more_settings": "More settings",
     "style": {
       "custom_theme_used": "(Custom theme)",
-      "themes2_outdated": "Editor for Themes V2 is no longer supported and presented here for sake of legacy.",
+      "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",
       "update_preview": "Update preview",
       "themes3": {
         "define": "Override",
-- 
cgit v1.2.3-70-g09d2


From aa17a92d4ad8ca9b17b5ddb20d416e38efc0e800 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Fri, 26 Jul 2024 15:41:00 +0300
Subject: fix missing zoom in appearance tab

---
 src/components/settings_modal/tabs/appearance_tab.vue | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

(limited to 'src/components/settings_modal/tabs/appearance_tab.vue')

diff --git a/src/components/settings_modal/tabs/appearance_tab.vue b/src/components/settings_modal/tabs/appearance_tab.vue
index 55f0d1be..de6eb8e7 100644
--- a/src/components/settings_modal/tabs/appearance_tab.vue
+++ b/src/components/settings_modal/tabs/appearance_tab.vue
@@ -300,7 +300,7 @@
         opacity: 0.2;
       }
 
-      .preview-container {
+      .theme-preview-container {
         pointer-events: none;
         zoom: 0.5;
         border: none;
-- 
cgit v1.2.3-70-g09d2

