From 1a65895bfd441ea57163ee3e185785c82a81b736 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Sun, 25 Nov 2018 21:48:16 +0300
Subject: [PATCH] initial font support

---
 src/App.scss                                  |  3 +
 src/components/font_control/font_control.vue  | 93 +++++++++++++++++++
 .../shadow_control/shadow_control.vue         |  5 +-
 .../style_switcher/style_switcher.js          | 63 ++++++++++---
 .../style_switcher/style_switcher.scss        |  3 +
 .../style_switcher/style_switcher.vue         | 55 ++++++++---
 src/i18n/en.json                              | 13 +++
 src/services/style_setter/style_setter.js     | 48 +++++++++-
 8 files changed, 251 insertions(+), 32 deletions(-)
 create mode 100644 src/components/font_control/font_control.vue

diff --git a/src/App.scss b/src/App.scss
index ceb0bb87..45491fd2 100644
--- a/src/App.scss
+++ b/src/App.scss
@@ -34,6 +34,7 @@ h4 {
 
 body {
   font-family: sans-serif;
+  font-family: var(--interfaceFont, sans-serif);
   font-size: 14px;
   margin: 0;
   color: $fallback--text;
@@ -62,6 +63,7 @@ button {
   box-shadow: var(--buttonShadow);
   font-size: 14px;
   font-family: sans-serif;
+  font-family: var(--interfaceFont, sans-serif);
 
   i[class*=icon-] {
     color: $fallback--text;
@@ -111,6 +113,7 @@ input, textarea, .select {
   color: $fallback--lightText;
   color: var(--inputText, $fallback--lightText);
   font-family: sans-serif;
+  font-family: var(--inputFont, sans-serif);
   font-size: 14px;
   padding: 8px 7px;
   box-sizing: border-box;
diff --git a/src/components/font_control/font_control.vue b/src/components/font_control/font_control.vue
new file mode 100644
index 00000000..424f6259
--- /dev/null
+++ b/src/components/font_control/font_control.vue
@@ -0,0 +1,93 @@
+<template>
+<div class="font-control style-control">
+  <label :for="preset === 'custom' ? name : name + '-font-switcher'" class="label">
+    {{label}}
+  </label>
+  <input
+    v-if="typeof fallback !== 'undefined'"
+    class="opt exlcude-disabled"
+    type="checkbox"
+    :id="name + '-o'"
+    :checked="present"
+    @input="$emit('input', typeof value === 'undefined' ? fallback : undefined)">
+  <label v-if="typeof fallback !== 'undefined'" class="opt-l" :for="name + '-o'"></label>
+  <label :for="name + '-font-switcher'" class="select" :disabled="!present">
+    <select
+      :disabled="!present"
+      v-model="preset"
+      class="font-switcher"
+      id="name + '-font-switcher'">
+      <option v-for="option in options" :value="option">
+        {{ option }}
+      </option>
+    </select>
+    <i class="icon-down-open"/>
+  </label>
+  <input
+    v-if="preset === 'custom'"
+    class="custom-font"
+    type="text"
+    id="name"
+    v-model="family">
+</div>
+</template>
+
+<script>
+import { set } from 'vue'
+
+export default {
+  props: [
+    'name', 'label', 'value', 'fallback', 'options'
+  ],
+  data () {
+    return {
+      lValue: this.value
+    }
+  },
+  beforeUpdate () {
+    this.lValue = this.value
+  },
+  computed: {
+    present () {
+      return typeof this.lValue !== 'undefined'
+    },
+    dValue () {
+      return this.lValue || this.fallback || {}
+    },
+    family: {
+      get () {
+        return this.dValue.family
+      },
+      set (v) {
+        set(this.lValue, 'family', v)
+        this.$emit('input', this.lValue)
+      }
+    },
+    preset: {
+      get () {
+        console.log(this.family)
+        if (this.family === 'serif' ||
+            this.family === 'sans-serif' ||
+            this.family === 'monospace' ||
+            this.family === 'inherit') {
+          return this.family
+        } else {
+          return 'custom'
+        }
+      },
+      set (v) {
+        this.family = v === 'custom' ? '' : v
+      }
+    }
+  }
+}
+</script>
+
+<style lang="scss">
+@import '../../_variables.scss';
+.font-control {
+  input.custom-font {
+    min-width: 10em;
+  }
+}
+</style>
diff --git a/src/components/shadow_control/shadow_control.vue b/src/components/shadow_control/shadow_control.vue
index 85346e17..744925d4 100644
--- a/src/components/shadow_control/shadow_control.vue
+++ b/src/components/shadow_control/shadow_control.vue
@@ -124,9 +124,9 @@
     <OpacityInput
       v-model="selected.alpha"
       :disabled="!present"/>
-    <div>
+    <p>
       {{$t('settings.style.shadows.hint')}}
-    </div>
+    </p>
   </div>
 </div>
 </template>
@@ -139,6 +139,7 @@
   display: flex;
   flex-wrap: wrap;
   justify-content: center;
+  margin-bottom: 1em;
 
   .shadow-preview-container,
   .shadow-tweak {
diff --git a/src/components/style_switcher/style_switcher.js b/src/components/style_switcher/style_switcher.js
index 57faa61b..bbd28bdc 100644
--- a/src/components/style_switcher/style_switcher.js
+++ b/src/components/style_switcher/style_switcher.js
@@ -1,10 +1,11 @@
 import { rgb2hex, hex2rgb, getContrastRatio, alphaBlend } from '../../services/color_convert/color_convert.js'
 import { set, delete as del } from 'vue'
-import { generateColors, generateShadows, generateRadii, composePreset } from '../../services/style_setter/style_setter.js'
+import { generateColors, generateShadows, generateRadii, generateFonts, composePreset } from '../../services/style_setter/style_setter.js'
 import ColorInput from '../color_input/color_input.vue'
 import RangeInput from '../range_input/range_input.vue'
 import OpacityInput from '../opacity_input/opacity_input.vue'
 import ShadowControl from '../shadow_control/shadow_control.vue'
+import FontControl from '../font_control/font_control.vue'
 import ContrastRatio from '../contrast_ratio/contrast_ratio.vue'
 import TabSwitcher from '../tab_switcher/tab_switcher.jsx'
 
@@ -30,6 +31,7 @@ export default {
       previewShadows: {},
       previewColors: {},
       previewRadii: {},
+      previewFonts: {},
 
       shadowsInvalid: true,
       colorsInvalid: true,
@@ -38,6 +40,7 @@ export default {
       keepShadows: false,
       keepOpacity: false,
       keepRoundness: false,
+      keepFonts: false,
 
       textColorLocal: '',
       linkColorLocal: '',
@@ -85,6 +88,7 @@ export default {
 
       shadowSelected: undefined,
       shadowsLocal: {},
+      fontsLocal: {},
 
       btnRadiusLocal: '',
       inputRadiusLocal: '',
@@ -176,10 +180,11 @@ export default {
       }
     },
     preview () {
-      return composePreset(this.previewColors, this.previewRadii, this.previewShadows)
+      return composePreset(this.previewColors, this.previewRadii, this.previewShadows, this.previewFonts)
     },
     previewTheme () {
-      if (!this.preview.theme.colors) return { colors: {}, opacity: {}, radii: {}, shadows: {} }
+      if (!this.preview.theme.colors) return { colors: {}, opacity: {}, radii: {}, shadows: {}, fonts: {} }
+      console.log(this.preview.theme)
       return this.preview.theme
     },
     // This needs optimization maybe
@@ -253,7 +258,11 @@ export default {
     },
     previewRules () {
       if (!this.preview.rules) return ''
-      return [...Object.values(this.preview.rules), 'color: var(--text)'].join(';')
+      return [
+        ...Object.values(this.preview.rules),
+        'color: var(--text)',
+        'font-family: var(--interfaceFont, sans-serif)'
+      ].join(';')
     },
     shadowsAvailable () {
       return Object.keys(this.previewTheme.shadows).sort()
@@ -291,6 +300,7 @@ export default {
     RangeInput,
     ContrastRatio,
     ShadowControl,
+    FontControl,
     TabSwitcher
   },
   methods: {
@@ -300,6 +310,7 @@ export default {
         _pleroma_theme_version: 2,
         theme: {
           shadows: this.shadowsLocal,
+          fonts: this.fontsLocal,
           opacity: this.currentOpacity,
           colors: this.currentColors,
           radii: this.currentRadii
@@ -357,6 +368,7 @@ export default {
         name: 'customTheme',
         value: {
           shadows: this.shadowsLocal,
+          fonts: this.fontsLocal,
           opacity: this.currentOpacity,
           colors: this.currentColors,
           radii: this.currentRadii
@@ -398,6 +410,10 @@ export default {
       this.shadowsLocal = {}
     },
 
+    clearFonts () {
+      this.fontsLocal = {}
+    },
+
     /**
      * This applies stored theme data onto form.
      * @param {Object} input - input data
@@ -408,6 +424,7 @@ export default {
       const radii = input.radii || input
       const opacity = input.opacity
       const shadows = input.shadows || {}
+      const fonts = input.fonts || {}
 
       if (version === 0) {
         if (input.version) version = input.version
@@ -458,6 +475,11 @@ export default {
         this.shadowSelected = this.shadowsAvailable[0]
       }
 
+      if (!this.keepFonts) {
+        this.clearFonts()
+        this.fontsLocal = fonts
+      }
+
       if (opacity && !this.keepOpacity) {
         this.clearOpacity()
         Object.entries(opacity).forEach(([k, v]) => {
@@ -477,14 +499,31 @@ export default {
         console.warn(e)
       }
     },
-    shadowsLocal () {
-      try {
-        this.previewShadows = generateShadows({ shadows: this.shadowsLocal })
-        this.shadowsInvalid = false
-      } catch (e) {
-        this.shadowsInvalid = true
-        console.warn(e)
-      }
+    shadowsLocal: {
+      handler () {
+        try {
+          this.previewShadows = generateShadows({ shadows: this.shadowsLocal })
+          this.shadowsInvalid = false
+        } catch (e) {
+          this.shadowsInvalid = true
+          console.warn(e)
+        }
+      },
+      deep: true
+    },
+    fontsLocal: {
+      handler () {
+        try {
+          this.previewFonts = generateFonts({ fonts: this.fontsLocal })
+          console.log('BENIS')
+          console.log(this.previewFonts)
+          this.fontsInvalid = false
+        } catch (e) {
+          this.fontsInvalid = true
+          console.warn(e)
+        }
+      },
+      deep: true
     },
     currentColors () {
       try {
diff --git a/src/components/style_switcher/style_switcher.scss b/src/components/style_switcher/style_switcher.scss
index ad203856..9c4f4ecd 100644
--- a/src/components/style_switcher/style_switcher.scss
+++ b/src/components/style_switcher/style_switcher.scss
@@ -67,6 +67,7 @@
     flex-wrap: wrap;
   }
 
+  .fonts-container,
   .reset-container,
   .apply-container,
   .radius-container,
@@ -75,6 +76,7 @@
     display: flex;
   }
 
+  .fonts-container,
   .radius-container {
     flex-direction: column;
   }
@@ -87,6 +89,7 @@
     justify-content: space-between;
   }
 
+  .fonts-container,
   .color-container,
   .shadow-container,
   .radius-container,
diff --git a/src/components/style_switcher/style_switcher.vue b/src/components/style_switcher/style_switcher.vue
index 5e9af19e..f64bda3f 100644
--- a/src/components/style_switcher/style_switcher.vue
+++ b/src/components/style_switcher/style_switcher.vue
@@ -75,7 +75,7 @@
     <tab-switcher key="style-tweak">
       <div :label="$t('settings.style.common_colors._tab_label')" class="color-container">
         <div class="tab-header">
-            <p>{{$t('settings.theme_help')}}</p>
+          <p>{{$t('settings.theme_help')}}</p>
           <button class="btn" @click="clearOpacity">{{$t('settings.style.switcher.clear_opacity')}}</button>
           <button class="btn" @click="clearV1">{{$t('settings.style.switcher.clear_all')}}</button>
         </div>
@@ -169,20 +169,18 @@
         </div>
       </div>
       <div :label="$t('settings.style.radii._tab_label')" class="radius-container">
-        <div>
-          <div class="tab-header">
-            <p>{{$t('settings.radii_help')}}</p>
-            <button class="btn" @click="clearRoundness">{{$t('settings.style.switcher.clear_all')}}</button>
-          </div>
-          <RangeInput name="btnRadius" :label="$t('settings.btnRadius')" v-model="btnRadiusLocal" :fallback="previewTheme.radii.btn" max="16" hardMin="0"/>
-          <RangeInput name="inputRadius" :label="$t('settings.inputRadius')" v-model="inputRadiusLocal" :fallback="previewTheme.radii.input" max="9" hardMin="0"/>
-          <RangeInput name="checkboxRadius" :label="$t('settings.checkboxRadius')" v-model="checkboxRadiusLocal" :fallback="previewTheme.radii.checkbox" max="16" hardMin="0"/>
-          <RangeInput name="panelRadius" :label="$t('settings.panelRadius')" v-model="panelRadiusLocal" :fallback="previewTheme.radii.panel" max="50" hardMin="0"/>
-          <RangeInput name="avatarRadius" :label="$t('settings.avatarRadius')" v-model="avatarRadiusLocal" :fallback="previewTheme.radii.avatar" max="28" hardMin="0"/>
-          <RangeInput name="avatarAltRadius" :label="$t('settings.avatarAltRadius')" v-model="avatarAltRadiusLocal" :fallback="previewTheme.radii.avatarAlt" max="28" hardMin="0"/>
-          <RangeInput name="attachmentRadius" :label="$t('settings.attachmentRadius')" v-model="attachmentRadiusLocal" :fallback="previewTheme.radii.attachment" max="50" hardMin="0"/>
-          <RangeInput name="tooltipRadius" :label="$t('settings.tooltipRadius')" v-model="tooltipRadiusLocal" :fallback="previewTheme.radii.tooltip" max="50" hardMin="0"/>
+        <div class="tab-header">
+          <p>{{$t('settings.radii_help')}}</p>
+          <button class="btn" @click="clearRoundness">{{$t('settings.style.switcher.clear_all')}}</button>
         </div>
+        <RangeInput name="btnRadius" :label="$t('settings.btnRadius')" v-model="btnRadiusLocal" :fallback="previewTheme.radii.btn" max="16" hardMin="0"/>
+        <RangeInput name="inputRadius" :label="$t('settings.inputRadius')" v-model="inputRadiusLocal" :fallback="previewTheme.radii.input" max="9" hardMin="0"/>
+        <RangeInput name="checkboxRadius" :label="$t('settings.checkboxRadius')" v-model="checkboxRadiusLocal" :fallback="previewTheme.radii.checkbox" max="16" hardMin="0"/>
+        <RangeInput name="panelRadius" :label="$t('settings.panelRadius')" v-model="panelRadiusLocal" :fallback="previewTheme.radii.panel" max="50" hardMin="0"/>
+        <RangeInput name="avatarRadius" :label="$t('settings.avatarRadius')" v-model="avatarRadiusLocal" :fallback="previewTheme.radii.avatar" max="28" hardMin="0"/>
+        <RangeInput name="avatarAltRadius" :label="$t('settings.avatarAltRadius')" v-model="avatarAltRadiusLocal" :fallback="previewTheme.radii.avatarAlt" max="28" hardMin="0"/>
+        <RangeInput name="attachmentRadius" :label="$t('settings.attachmentRadius')" v-model="attachmentRadiusLocal" :fallback="previewTheme.radii.attachment" max="50" hardMin="0"/>
+        <RangeInput name="tooltipRadius" :label="$t('settings.tooltipRadius')" v-model="tooltipRadiusLocal" :fallback="previewTheme.radii.tooltip" max="50" hardMin="0"/>
       </div>
       <div :label="$t('settings.style.shadows._tab_label')" class="shadow-container">
         <div class="tab-header shadow-selector">
@@ -214,6 +212,35 @@
         </div>
         <shadow-control :ready="!!currentShadowFallback" :fallback="currentShadowFallback" v-model="currentShadow"/>
       </div>
+      <div :label="$t('settings.style.fonts._tab_label')" class="fonts-container">
+        <div class="tab-header">
+          <p>{{$t('settings.style.fonts.help')}}</p>
+        </div>
+        <FontControl
+          name="ui"
+          v-model="fontsLocal.interface"
+          :label="$t('settings.style.fonts.components.interface')"
+          :fallback="previewTheme.fonts.interface"
+          :options="['serif', 'sans-serif', 'monospace', 'custom']" />
+        <FontControl
+          name="input"
+          v-model="fontsLocal.input"
+          :label="$t('settings.style.fonts.components.input')"
+          :fallback="previewTheme.fonts.input"
+          :options="['serif', 'sans-serif', 'monospace', 'inherit', 'custom']" />
+        <FontControl
+          name="post"
+          v-model="fontsLocal.post"
+          :label="$t('settings.style.fonts.components.post')"
+          :fallback="previewTheme.fonts.post"
+          :options="['serif', 'sans-serif', 'monospace', 'inherit', 'custom']" />
+        <FontControl
+          name="postCode"
+          v-model="fontsLocal.postCode"
+          :label="$t('settings.style.fonts.components.postCode')"
+          :fallback="previewTheme.fonts.postCode"
+          :options="['serif', 'sans-serif', 'monospace', 'inherit', 'custom']" />
+      </div>
     </tab-switcher>
   </keep-alive>
 
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 5bd1ddbb..6f439f65 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -236,6 +236,19 @@
           "buttonPressedHover": "Button (pressed+hover)",
           "input": "Input field"
         }
+      },
+      "fonts": {
+        "_tab_label": "Fonts",
+        "help": "Select font to use for elements of UI. For \"custom\" you have to enter exact font name as it appears in system.",
+        "components": {
+          "interface": "Interface",
+          "input": "Input fields",
+          "post": "Post text",
+          "postCode": "Monospaced text in a post (rich text)"
+        },
+        "family": "Font name",
+        "size": "Size (in px)",
+        "weight": "Weight (boldness)"
       }
     }
   },
diff --git a/src/services/style_setter/style_setter.js b/src/services/style_setter/style_setter.js
index e11ee90c..f2c9c13e 100644
--- a/src/services/style_setter/style_setter.js
+++ b/src/services/style_setter/style_setter.js
@@ -85,6 +85,7 @@ const setColors = (input, commit) => {
   styleSheet.insertRule(`body { ${rules.radii} }`, 'index-max')
   styleSheet.insertRule(`body { ${rules.colors} }`, 'index-max')
   styleSheet.insertRule(`body { ${rules.shadows} }`, 'index-max')
+  styleSheet.insertRule(`body { ${rules.fonts} }`, 'index-max')
   body.style.display = 'initial'
 
   // commit('setOption', { name: 'colors', value: htmlColors })
@@ -267,6 +268,41 @@ const generateRadii = (input) => {
   }
 }
 
+const generateFonts = (input) => {
+  const fonts = Object.entries(input.fonts || {}).filter(([k, v]) => v).reduce((acc, [k, v]) => {
+    acc[k] = Object.entries(v).filter(([k, v]) => v).reduce((acc, [k, v]) => {
+      acc[k] = v
+      return acc
+    }, acc[k])
+    return acc
+  }, {
+    interface: {
+      family: 'sans-serif'
+    },
+    input: {
+      family: 'inherit'
+    },
+    post: {
+      family: 'inherit'
+    },
+    postCode: {
+      family: 'monospace'
+    }
+  })
+
+  return {
+    rules: {
+      fonts: Object
+        .entries(fonts)
+        .filter(([k, v]) => v)
+        .map(([k, v]) => `--${k}Font: ${v.family}`).join(';')
+    },
+    theme: {
+      fonts
+    }
+  }
+}
+
 const generateShadows = (input) => {
   const border = (top, shadow) => ({
     x: 0,
@@ -355,17 +391,19 @@ const generateShadows = (input) => {
   }
 }
 
-const composePreset = (colors, radii, shadows) => {
+const composePreset = (colors, radii, shadows, fonts) => {
   return {
     rules: {
       ...shadows.rules,
       ...colors.rules,
-      ...radii.rules
+      ...radii.rules,
+      ...fonts.rules
     },
     theme: {
       ...shadows.theme,
       ...colors.theme,
-      ...radii.theme
+      ...radii.theme,
+      ...fonts.theme
     }
   }
 }
@@ -374,8 +412,9 @@ const generatePreset = (input) => {
   const shadows = generateShadows(input)
   const colors = generateColors(input)
   const radii = generateRadii(input)
+  const fonts = generateFonts(input)
 
-  return composePreset(colors, radii, shadows)
+  return composePreset(colors, radii, shadows, fonts)
 }
 
 const setPreset = (val, commit) => {
@@ -424,6 +463,7 @@ export {
   generateColors,
   generateRadii,
   generateShadows,
+  generateFonts,
   generatePreset,
   composePreset,
   getCssShadow