logo

pleroma-fe

My custom branche(s) on git.pleroma.social/pleroma/pleroma-fe git clone https://anongit.hacktivis.me/git/pleroma-fe.git/
commit: 216ca52073212942ffb6f75b63993a5f5c32a5d6
parent a5689464d0829ae038fbba72f3ab96afd917bd8d
Author: marcin mikołajczak <git@mkljczk.pl>
Date:   Thu,  3 Oct 2024 21:52:44 +0200

Merge remote-tracking branch 'origin/develop' into bookmark-folders

Signed-off-by: marcin mikołajczak <git@mkljczk.pl>

Diffstat:

Achangelog.d/better-shadow-control.fix1+
Achangelog.d/splashscreen.add1+
Mindex.html129+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Mpackage.json3++-
Msrc/App.js22++++++++++++++++++++++
Msrc/App.scss166+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/pleromatan_apology.png2++
Asrc/assets/pleromatan_apology_fox.png2++
Msrc/boot/after_store.js21++++++++++++---------
Msrc/components/button.style.js11+++++++++++
Msrc/components/checkbox/checkbox.vue32+++++++++++++++++++++-----------
Msrc/components/color_input/color_input.scss24++++++++++++++++++------
Msrc/components/color_input/color_input.vue20++++++++++++++++----
Asrc/components/component_preview/component_preview.vue212+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/components/input.style.js43++++++++++++++++++++++++++++++++++++-------
Msrc/components/opacity_input/opacity_input.vue2++
Msrc/components/select/select.vue35++++++++++++++++++++++++++++++++++-
Msrc/components/settings_modal/tabs/theme_tab/theme_tab.js13++++++++++++-
Msrc/components/settings_modal/tabs/theme_tab/theme_tab.scss15++++++++-------
Msrc/components/settings_modal/tabs/theme_tab/theme_tab.vue57+++++++++++----------------------------------------------
Msrc/components/shadow_control/shadow_control.js136++++++++++++++++++++++++++++++++++++++++++-------------------------------------
Asrc/components/shadow_control/shadow_control.scss105+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/components/shadow_control/shadow_control.vue356++++++++++++++++++++++++++++++++-----------------------------------------------
Msrc/i18n/en.json16++++++++++++++++
Msrc/i18n/nan-TW.json43+++++++++++++++++++++++++++++++++++++------
Msrc/main.js114++++++++++++++++++++++++++++++++++++++++++++++---------------------------------
Msrc/services/style_setter/style_setter.js27+++++++++++++++------------
Msrc/services/theme_data/theme_data.service.js2+-
Msrc/services/theme_data/theme_data_3.service.js36+++++++++++++++++++++++++++---------
Rsrc/assets/pleromatan_apology.png -> static/pleromatan_apology.png0
Rsrc/assets/pleromatan_apology_fox.png -> static/pleromatan_apology_fox.png0
Astatic/pleromatan_orz.png0
Astatic/pleromatan_orz_fox.png0
33 files changed, 1200 insertions(+), 446 deletions(-)

diff --git a/changelog.d/better-shadow-control.fix b/changelog.d/better-shadow-control.fix @@ -0,0 +1 @@ +Updated shadow editor, hopefully fixed long-standing bugs, added ability to specify shadow's name. diff --git a/changelog.d/splashscreen.add b/changelog.d/splashscreen.add @@ -0,0 +1 @@ +Splash screen + loading indicator to make process of identifying initialization issues and load performance diff --git a/index.html b/index.html @@ -4,13 +4,138 @@ <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1,user-scalable=no"> <link rel="icon" type="image/png" href="/favicon.png"> + <!-- putting styles here to avoid having to wait for styles to load up --> + <style id="splashscreen"> + #splash { + --scale: 1; + width: 100vw; + height: 100vh; + display: grid; + grid-template-rows: auto; + grid-template-columns: auto; + align-content: center; + align-items: center; + justify-content: center; + justify-items: center; + flex-direction: column; + background: #0f161e; + font-family: sans-serif; + color: #b9b9ba; + position: absolute; + z-index: 9999; + font-size: calc(1vw + 1vh + 1vmin); + } + + #splash-credit { + position: absolute; + font-size: 14px; + bottom: 16px; + right: 16px; + } + + #splash-container { + align-items: center; + } + + #mascot-container { + display: flex; + align-items: flex-end; + justify-content: center; + perspective: 60em; + perspective-origin: 0 -15em; + transform-style: preserve-3d; + } + + #mascot { + width: calc(10em * var(--scale)); + height: calc(10em * var(--scale)); + object-fit: contain; + object-position: bottom; + transform: translateZ(-2em); + } + + #throbber { + display: grid; + width: calc(5em * 0.5 * var(--scale)); + height: calc(8em * 0.5 * var(--scale)); + margin-left: 4.1em; + z-index: 2; + grid-template-rows: repeat(8, 1fr); + grid-template-columns: repeat(5, 1fr); + grid-template-areas: "P P . L L" + "P P . L L" + "P P . L L" + "P P . L L" + "P P . . ." + "P P . . ." + "P P . E E" + "P P . E E"; + } + + .chunk { + background-color: #e2b188; + box-shadow: 0.01em 0.01em 0.1em 0 #e2b188; + } + + #chunk-P { + grid-area: P; + border-top-left-radius: calc(var(--logoChunkSize) / 2); + } + + #chunk-L { + grid-area: L; + border-bottom-right-radius: calc(var(--logoChunkSize) / 2); + } + + #chunk-E { + grid-area: E; + border-bottom-right-radius: calc(var(--logoChunkSize) / 2); + } + + #status { + margin-top: 1em; + line-height: 2; + width: 100%; + text-align: center; + } + + @media (prefers-reduced-motion) { + #throbber { + animation: none !important; + } + } + </style> <style id="pleroma-eager-styles" type="text/css"></style> <style id="pleroma-lazy-styles" type="text/css"></style> <!--server-generated-meta--> </head> - <body class="hidden"> + <body style="margin: 0; padding: 0"> <noscript>To use Pleroma, please enable JavaScript.</noscript> - <div id="app"></div> + <div id="splash"> + <!-- we are hiding entire graphic so no point showing credit --> + <div aria-hidden="true" id="splash-credit"> + Art by pipivovott + </div> + <div id="splash-container"> + <div aria-hidden="true" id="mascot-container"> + <div id="throbber"> + <div class="chunk" id="chunk-P"> + </div> + <div class="chunk" id="chunk-L"> + </div> + <div class="chunk" id="chunk-E"> + </div> + </div> + <img id="mascot" src="/static/pleromatan_apology.png"> + </div> + <div id="status" class="css-ok"> + <!-- (。>﹏<) --> + <!-- it's a pseudographic, don't want screenreader read out nonsense --> + <span aria-hidden="true" class="initial-text">(。&gt;﹏&lt;)</span> + </div> + </div> + </div> + <div id="app" class="hidden"></div> <div id="modal"></div> <!-- built files will be auto injected --> <div id="popovers" /> diff --git a/package.json b/package.json @@ -132,5 +132,6 @@ "engines": { "node": ">= 16.0.0", "npm": ">= 3.0.0" - } + }, + "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" } diff --git a/src/App.js b/src/App.js @@ -44,16 +44,29 @@ export default { data: () => ({ mobileActivePanel: 'timeline' }), + watch: { + themeApplied (value) { + this.removeSplash() + } + }, created () { // Load the locale from the storage const val = this.$store.getters.mergedConfig.interfaceLanguage this.$store.dispatch('setOption', { name: 'interfaceLanguage', value: val }) window.addEventListener('resize', this.updateMobileState) }, + mounted () { + if (this.$store.state.interface.themeApplied) { + this.removeSplash() + } + }, unmounted () { window.removeEventListener('resize', this.updateMobileState) }, computed: { + themeApplied () { + return this.$store.state.interface.themeApplied + }, classes () { return [ { @@ -130,6 +143,15 @@ export default { updateMobileState () { this.$store.dispatch('setLayoutWidth', windowWidth()) this.$store.dispatch('setLayoutHeight', windowHeight()) + }, + removeSplash () { + document.querySelector('#status').textContent = this.$t('splash.fun_' + Math.ceil(Math.random() * 4)) + const splashscreenRoot = document.querySelector('#splash') + splashscreenRoot.addEventListener('transitionend', () => { + splashscreenRoot.remove() + }) + splashscreenRoot.classList.add('hidden') + document.querySelector('#app').classList.remove('hidden') } } } diff --git a/src/App.scss b/src/App.scss @@ -914,3 +914,169 @@ option { color: var(--selectionText); background-color: var(--selectionBackground); } + +#splash { + pointer-events: none; + transition: opacity 2s; + opacity: 1; + + &.hidden { + opacity: 0; + } + + #status { + &.css-ok { + &::before { + display: inline-block; + content: "CSS OK"; + } + } + + .initial-text { + display: none; + } + } + + #throbber { + animation-duration: 3s; + animation-name: bounce; + animation-iteration-count: infinite; + animation-direction: normal; + transform-origin: bottom center; + + &.dead { + animation-name: dead; + animation-duration: 2s; + animation-iteration-count: 1; + transform: rotateX(90deg) rotateY(0) rotateZ(-45deg); + } + + @keyframes dead { + 0% { + transform: rotateX(0) rotateY(0) rotateZ(0); + } + + 5% { + transform: rotateX(0) rotateY(0) rotateZ(1deg); + } + + 10% { + transform: rotateX(0) rotateY(0) rotateZ(-2deg); + } + + 15% { + transform: rotateX(0) rotateY(0) rotateZ(3deg); + } + + 20% { + transform: rotateX(0) rotateY(0) rotateZ(0); + } + + 25% { + transform: rotateX(0) rotateY(0) rotateZ(0); + } + + 30% { + transform: rotateX(10deg) rotateY(0) rotateZ(0); + } + + 35% { + transform: rotateX(-10deg) rotateY(0) rotateZ(0); + } + + 40% { + transform: rotateX(10deg) rotateY(0) rotateZ(0); + } + + 45% { + transform: rotateX(-10deg) rotateY(0) rotateZ(0); + } + + 50% { + transform: rotateX(10deg) rotateY(0) rotateZ(0); + } + + 100% { + transform: rotateX(90deg) rotateY(0) rotateZ(-45deg); + transition-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06); /* easeInQuint */ + } + } + + @keyframes bounce { + 0% { + scale: 1 1; + translate: 0 0; + animation-timing-function: ease-out; + } + + 10% { + scale: 1.2 0.8; + translate: 0 0; + transform: rotateZ(var(--defaultZ)); + animation-timing-function: ease-out; + } + + 30% { + scale: 0.9 1.1; + translate: 0 -40%; + transform: rotateZ(var(--defaultZ)); + animation-timing-function: ease-in; + } + + 40% { + scale: 1.1 0.9; + translate: 0 -50%; + transform: rotateZ(var(--defaultZ)); + animation-timing-function: ease-in; + } + + 45% { + scale: 0.9 1.1; + translate: 0 -45%; + transform: rotateZ(var(--defaultZ)); + animation-timing-function: ease-in; + } + + 50% { + scale: 1.05 0.95; + translate: 0 -40%; + animation-timing-function: ease-in; + } + + 55% { + scale: 0.985 1.025; + translate: 0 -35%; + transform: rotateZ(var(--defaultZ)); + animation-timing-function: ease-in; + } + + 60% { + scale: 1.0125 0.9985; + translate: 0 -30%; + transform: rotateZ(var(--defaultZ)); + animation-timing-function: ease-in; + } + + 80% { + scale: 1.0063 0.9938; + translate: 0 -10%; + transform: rotateZ(var(--defaultZ)); + animation-timing-function: ease-in-ou; + } + + 90% { + scale: 1.2 0.8; + translate: 0 0; + transform: rotateZ(var(--defaultZ)); + animation-timing-function: ease-out; + } + + 100% { + scale: 1 1; + translate: 0 0; + transform: rotateZ(var(--defaultZ)); + animation-timing-function: ease-out; + } + } + } +} diff --git a/src/assets/pleromatan_apology.png b/src/assets/pleromatan_apology.png @@ -0,0 +1 @@ +../../static/pleromatan_apology.png +\ No newline at end of file diff --git a/src/assets/pleromatan_apology_fox.png b/src/assets/pleromatan_apology_fox.png @@ -0,0 +1 @@ +../../static/pleromatan_apology_fox.png +\ No newline at end of file diff --git a/src/boot/after_store.js b/src/boot/after_store.js @@ -328,11 +328,7 @@ const setConfig = async ({ store }) => { const checkOAuthToken = async ({ store }) => { if (store.getters.getUserToken()) { - try { - await store.dispatch('loginUser', store.getters.getUserToken()) - } catch (e) { - console.error(e) - } + return store.dispatch('loginUser', store.getters.getUserToken()) } return Promise.resolve() } @@ -350,19 +346,26 @@ const afterStoreSetup = async ({ store, i18n }) => { const server = (typeof overrides.target !== 'undefined') ? overrides.target : window.location.origin store.dispatch('setInstanceOption', { name: 'server', value: server }) + document.querySelector('#status').textContent = i18n.global.t('splash.settings') await setConfig({ store }) - await store.dispatch('setTheme') + document.querySelector('#status').textContent = i18n.global.t('splash.theme') + try { + await store.dispatch('setTheme').catch((e) => { console.error('Error setting theme', e) }) + } catch (e) { + return Promise.reject(e) + } - applyConfig(store.state.config) + applyConfig(store.state.config, i18n.global) // Now we can try getting the server settings and logging in // Most of these are preloaded into the index.html so blocking is minimized + document.querySelector('#status').textContent = i18n.global.t('splash.instance') await Promise.all([ checkOAuthToken({ store }), getInstancePanel({ store }), getNodeInfo({ store }), getInstanceConfig({ store }) - ]) + ]).catch(e => Promise.reject(e)) // Start fetching things that don't need to block the UI store.dispatch('fetchMutes') @@ -396,9 +399,9 @@ const afterStoreSetup = async ({ store, i18n }) => { // remove after vue 3.3 app.config.unwrapInjectedRef = true + document.querySelector('#status').textContent = i18n.global.t('splash.almost') app.mount('#app') - return app } diff --git a/src/components/button.style.js b/src/components/button.style.js @@ -96,6 +96,17 @@ export default { textOpacity: 0.25, textOpacityMode: 'blend' } + }, + { + component: 'Icon', + parent: { + component: 'Button', + state: ['disabled'] + }, + directives: { + textOpacity: 0.25, + textOpacityMode: 'blend' + } } ] } diff --git a/src/components/checkbox/checkbox.vue b/src/components/checkbox/checkbox.vue @@ -3,6 +3,13 @@ class="checkbox" :class="{ disabled, indeterminate, 'indeterminate-fix': indeterminateTransitionFix }" > + <span + v-if="!!$slots.before" + class="label -before" + :class="{ faint: disabled }" + > + <slot name="before" /> + </span> <input type="checkbox" class="visible-for-screenreader-only" @@ -14,11 +21,13 @@ <i class="input -checkbox checkbox-indicator" :aria-hidden="true" + :class="{ disabled }" @transitionend.capture="onTransitionEnd" /> <span v-if="!!$slots.default" - class="label" + class="label -after" + :class="{ faint: disabled }" > <slot /> </span> @@ -93,14 +102,9 @@ export default { box-sizing: border-box; } - &.disabled { - .checkbox-indicator::before, - .label { - opacity: 0.5; - } - - .label { - color: var(--text); + .disabled { + .checkbox-indicator::before { + background-color: var(--background); } } @@ -121,8 +125,14 @@ export default { } } - & > span { - margin-left: 0.5em; + & > .label { + &.-after { + margin-left: 0.5em; + } + + &.-before { + margin-right: 0.5em; + } } } </style> diff --git a/src/components/color_input/color_input.scss b/src/components/color_input/color_input.scss @@ -1,12 +1,15 @@ .color-input { display: inline-flex; + .label { + flex: 1 1 auto; + } + &-field.input { display: inline-flex; flex: 0 0 0; max-width: 9em; align-items: stretch; - padding: 0.2em 8px; input { color: var(--text); @@ -25,6 +28,7 @@ .nativeColor { cursor: pointer; flex: 0 0 auto; + padding: 0; input { appearance: none; @@ -41,10 +45,10 @@ .invalidIndicator, .transparentIndicator { flex: 0 0 2em; - margin: 0 0.5em; + margin: 0.2em 0.5em; min-width: 2em; align-self: stretch; - min-height: 1.5em; + min-height: 1.1em; border-radius: var(--roundness); } @@ -81,9 +85,17 @@ border-bottom-right-radius: var(--roundness); } } - } - .label { - flex: 1 1 auto; + &.disabled, + &:disabled { + .nativeColor input, + .computedIndicator, + .validIndicator, + .invalidIndicator, + .transparentIndicator { + /* stylelint-disable-next-line declaration-no-important */ + opacity: 0.25 !important; + } + } } } diff --git a/src/components/color_input/color_input.vue b/src/components/color_input/color_input.vue @@ -6,6 +6,7 @@ <label :for="name" class="label" + :class="{ faint: !present || disabled }" > {{ label }} </label> @@ -14,16 +15,20 @@ :model-value="present" :disabled="disabled" class="opt" - @update:modelValue="$emit('update:modelValue', typeof modelValue === 'undefined' ? fallback : undefined)" + @update:modelValue="update(typeof modelValue === 'undefined' ? fallback : undefined)" /> - <div class="input color-input-field"> + <div + class="input color-input-field" + :class="{ disabled: !present || disabled }" + > <input :id="name + '-t'" class="textColor unstyled" + :class="{ disabled: !present || disabled }" type="text" :value="modelValue || fallback" :disabled="!present || disabled" - @input="$emit('update:modelValue', $event.target.value)" + @input="updateValue($event.target.value)" > <div v-if="validColor" @@ -51,7 +56,8 @@ type="color" :value="modelValue || fallback" :disabled="!present || disabled" - @input="$emit('update:modelValue', $event.target.value)" + :class="{ disabled: !present || disabled }" + @input="updateValue($event.target.value)" > </label> </div> @@ -60,6 +66,7 @@ <script> import Checkbox from '../checkbox/checkbox.vue' import { hex2rgb } from '../../services/color_convert/color_convert.js' +import { throttle } from 'lodash' import { library } from '@fortawesome/fontawesome-svg-core' import { @@ -125,6 +132,11 @@ export default { computedColor () { return this.modelValue && this.modelValue.startsWith('--') } + }, + methods: { + updateValue: throttle(function (value) { + this.$emit('update:modelValue', value) + }, 100) } } </script> diff --git a/src/components/component_preview/component_preview.vue b/src/components/component_preview/component_preview.vue @@ -0,0 +1,212 @@ +<template> +<div + class="ComponentPreview" + :class="{ '-shadow-controls': shadowControl }" +> + <label + class="header" + v-show="shadowControl" + :class="{ faint: disabled }" + > + {{ $t('settings.style.shadows.offset') }} + </label> + <input + v-show="shadowControl" + :value="shadow?.y" + :disabled="disabled" + :class="{ disabled }" + class="input input-number y-shift-number" + type="number" + @input="e => updateProperty('y', e.target.value)" + > + <input + v-show="shadowControl" + :value="shadow?.y" + :disabled="disabled" + :class="{ disabled }" + class="input input-range y-shift-slider" + type="range" + max="20" + min="-20" + @input="e => updateProperty('y', e.target.value)" + > + <div + class="preview-window" + :class="{ '-light-grid': lightGrid }" + > + <div + class="preview-block" + :style="previewStyle" + /> + </div> + <input + v-show="shadowControl" + :value="shadow?.x" + :disabled="disabled" + :class="{ disabled }" + class="input input-number x-shift-number" + type="number" + @input="e => updateProperty('x', e.target.value)" + > + <input + v-show="shadowControl" + :value="shadow?.x" + :disabled="disabled" + :class="{ disabled }" + class="input input-range x-shift-slider" + type="range" + max="20" + min="-20" + @input="e => updateProperty('x', e.target.value)" + > + <Checkbox + id="lightGrid" + v-model="lightGrid" + :disabled="shadow == null" + name="lightGrid" + class="input-light-grid" + > + {{ $t('settings.style.shadows.light_grid') }} + </Checkbox> +</div> +</template> + +<style lang="scss"> +.ComponentPreview { + display: grid; + grid-template-columns: 3em 1fr 3em; + grid-template-rows: 2em 1fr 2em; + grid-template-areas: + ". header y-num " + ". preview y-slide" + "x-num x-slide . " + "options options options"; + grid-gap: 0.5em; + + .header { + grid-area: header; + justify-self: center; + align-self: baseline; + line-height: 2; + } + + .input-light-grid { + grid-area: options; + justify-self: center; + } + + .input-number { + min-width: 2em; + } + + .x-shift-number { + grid-area: x-num; + } + + .x-shift-slider { + grid-area: x-slide; + height: auto; + align-self: start; + min-width: 10em; + } + + .y-shift-number { + grid-area: y-num; + } + + .y-shift-slider { + grid-area: y-slide; + writing-mode: vertical-lr; + justify-self: left; + min-height: 10em; + } + + .x-shift-slider, + .y-shift-slider { + padding: 0; + } + + .preview-window { + --__grid-color1: rgb(102 102 102); + --__grid-color2: rgb(153 153 153); + --__grid-color1-disabled: rgba(102 102 102 / 20%); + --__grid-color2-disabled: rgba(153 153 153 / 20%); + + &.-light-grid { + --__grid-color1: rgb(205 205 205); + --__grid-color2: rgb(255 255 255); + --__grid-color1-disabled: rgba(205 205 205 / 20%); + --__grid-color2-disabled: rgba(255 255 255 / 20%); + } + + grid-area: preview; + aspect-ratio: 1; + display: flex; + align-items: center; + justify-content: center; + min-width: 10em; + min-height: 10em; + background-color: var(--__grid-color2); + background-image: + linear-gradient(45deg, var(--__grid-color1) 25%, transparent 25%), + linear-gradient(-45deg, var(--__grid-color1) 25%, transparent 25%), + linear-gradient(45deg, transparent 75%, var(--__grid-color1) 75%), + linear-gradient(-45deg, transparent 75%, var(--__grid-color1) 75%); + background-size: 20px 20px; + background-position: 0 0, 0 10px, 10px -10px, -10px 0; + border-radius: var(--roundness); + + &.disabled { + background-color: var(--__grid-color2-disabled); + background-image: + linear-gradient(45deg, var(--__grid-color1-disabled) 25%, transparent 25%), + linear-gradient(-45deg, var(--__grid-color1-disabled) 25%, transparent 25%), + linear-gradient(45deg, transparent 75%, var(--__grid-color1-disabled) 75%), + linear-gradient(-45deg, transparent 75%, var(--__grid-color1-disabled) 75%); + } + + .preview-block { + background: var(--background, var(--bg)); + display: flex; + justify-content: center; + align-items: center; + min-width: 33%; + min-height: 33%; + max-width: 80%; + max-height: 80%; + border-width: 0; + border-style: solid; + border-color: var(--border); + border-radius: var(--roundness); + box-shadow: var(--shadow); + } + } +} +</style> +<script> +import Checkbox from 'src/components/checkbox/checkbox.vue' + +export default { + props: [ + 'shadow', + 'shadowControl', + 'previewClass', + 'previewStyle', + 'disabled' + ], + data () { + return { + lightGrid: false + } + }, + emits: ['update:shadow'], + components: { + Checkbox + }, + methods: { + updateProperty (axis, value) { + this.$emit('update:shadow', { axis, value }) + } + } +} +</script> diff --git a/src/components/input.style.js b/src/components/input.style.js @@ -10,17 +10,18 @@ const hoverGlow = { export default { name: 'Input', selector: '.input', - variant: { + states: { + hover: ':hover:not(.disabled)', + focused: ':focus-within', + disabled: '.disabled' + }, + variants: { checkbox: '.-checkbox', radio: '.-radio' }, - states: { - disabled: ':disabled', - hover: ':hover:not(:disabled)', - focused: ':focus-within' - }, validInnerComponents: [ - 'Text' + 'Text', + 'Icon' ], defaultRules: [ { @@ -55,6 +56,34 @@ export default { directives: { shadow: [hoverGlow, '--defaultInputBevel'] } + }, + { + state: ['disabled'], + directives: { + background: '--parent' + } + }, + { + component: 'Text', + parent: { + component: 'Input', + state: ['disabled'] + }, + directives: { + textOpacity: 0.25, + textOpacityMode: 'blend' + } + }, + { + component: 'Icon', + parent: { + component: 'Input', + state: ['disabled'] + }, + directives: { + textOpacity: 0.25, + textOpacityMode: 'blend' + } } ] } diff --git a/src/components/opacity_input/opacity_input.vue b/src/components/opacity_input/opacity_input.vue @@ -6,6 +6,7 @@ <label :for="name" class="label" + :class="{ faint: !present || disabled }" > {{ $t('settings.style.common.opacity') }} </label> @@ -22,6 +23,7 @@ type="number" :value="modelValue || fallback" :disabled="!present || disabled" + :class="{ disabled: !present || disabled }" max="1" min="0" step=".05" diff --git a/src/components/select/select.vue b/src/components/select/select.vue @@ -6,13 +6,14 @@ <select :disabled="disabled" :value="modelValue" - v-bind="attrs" + v-bind="$attrs" @change="$emit('update:modelValue', $event.target.value)" > <slot /> </select> {{ ' ' }} <FAIcon + v-if="!$attrs.size && !$attrs.multiple" class="select-down-icon" icon="chevron-down" /> @@ -39,6 +40,38 @@ label.Select { z-index: 1; height: 2em; line-height: 16px; + + &[multiple], + &[size] { + height: 100%; + padding: 0.2em; + + option { + background-color: transparent; + + &.-active { + color: var(--selectionText); + background-color: var(--selectionBackground); + } + } + } + } + + &.disabled, + &:disabled { + background-color: var(--background); + opacity: 1; /* override browser */ + color: var(--faint); + + select { + &[multiple], + &[size] { + option.-active { + color: var(--faint); + background: transparent; + } + } + } } .select-down-icon { diff --git a/src/components/settings_modal/tabs/theme_tab/theme_tab.js b/src/components/settings_modal/tabs/theme_tab/theme_tab.js @@ -314,7 +314,18 @@ export default { }, set (val) { if (val) { - this.shadowsLocal[this.shadowSelected] = this.currentShadowFallback.map(_ => Object.assign({}, _)) + this.shadowsLocal[this.shadowSelected] = (this.currentShadowFallback || []) + .map(s => ({ + name: null, + x: 0, + y: 0, + blur: 0, + spread: 0, + inset: false, + color: '#000000', + alpha: 1, + ...s + })) } else { delete this.shadowsLocal[this.shadowSelected] } diff --git a/src/components/settings_modal/tabs/theme_tab/theme_tab.scss b/src/components/settings_modal/tabs/theme_tab/theme_tab.scss @@ -25,7 +25,9 @@ margin-bottom: 5px; .label { + margin-right: 1em; flex: 1; + line-height: 2; } .opt { @@ -48,15 +50,14 @@ &[type="range"] { flex: 1; - min-width: 3em; - align-self: flex-start; + min-width: 2em; + align-self: center; + margin: 0 0.5em; } - } - &.disabled { - input, - select { - opacity: 0.5; + &[type="checkbox"] + i { + height: 1.1em; + align-self: center; } } } diff --git a/src/components/settings_modal/tabs/theme_tab/theme_tab.vue b/src/components/settings_modal/tabs/theme_tab/theme_tab.vue @@ -123,10 +123,13 @@ </div> </div> - <!-- eslint-disable vue/no-v-text-v-html-on-component --> - <component :is="'style'" v-html="themeV3Preview"/> - <!-- eslint-enable vue/no-v-text-v-html-on-component --> - <preview id="theme-preview"/> + <!-- eslint-disable vue/no-v-html vue/no-v-text-v-html-on-component --> + <component + :is="'style'" + v-html="themeV3Preview" + /> + <!-- eslint-enable vue/no-v-html vue/no-v-text-v-html-on-component --> + <preview id="theme-preview" /> <div> <button @@ -934,24 +937,14 @@ </Select> </div> <div class="override"> - <label - for="override" - class="label" - > - {{ $t('settings.style.shadows.override') }} - </label> - {{ ' ' }} - <input + <Checkbox id="override" v-model="currentShadowOverriden" name="override" class="input-override" - type="checkbox" > - <label - class="checkbox-label" - for="override" - /> + {{ $t('settings.style.shadows.override') }} + </Checkbox> </div> <button class="btn button-default" @@ -962,38 +955,10 @@ </div> <ShadowControl v-model="currentShadow" - :ready="!!currentShadowFallback" + :separate-inset="shadowSelected === 'avatar' || shadowSelected === 'avatarStatus'" :fallback="currentShadowFallback" /> - <div v-if="shadowSelected === 'avatar' || shadowSelected === 'avatarStatus'"> - <i18n-t - scope="global" - keypath="settings.style.shadows.filter_hint.always_drop_shadow" - tag="p" - > - <code>filter: drop-shadow()</code> - </i18n-t> - <p>{{ $t('settings.style.shadows.filter_hint.avatar_inset') }}</p> - <i18n-t - scope="global" - keypath="settings.style.shadows.filter_hint.drop_shadow_syntax" - tag="p" - > - <code>drop-shadow</code> - <code>spread-radius</code> - <code>inset</code> - </i18n-t> - <i18n-t - scope="global" - keypath="settings.style.shadows.filter_hint.inset_classic" - tag="p" - > - <code>box-shadow</code> - </i18n-t> - <p>{{ $t('settings.style.shadows.filter_hint.spread_zero') }}</p> - </div> </div> - <div :label="$t('settings.style.fonts._tab_label')" class="fonts-container" diff --git a/src/components/shadow_control/shadow_control.js b/src/components/shadow_control/shadow_control.js @@ -1,9 +1,12 @@ -import ColorInput from '../color_input/color_input.vue' -import OpacityInput from '../opacity_input/opacity_input.vue' -import Select from '../select/select.vue' -import { getCssShadow } from '../../services/theme_data/theme_data.service.js' -import { hex2rgb } from '../../services/color_convert/color_convert.js' +import ColorInput from 'src/components/color_input/color_input.vue' +import OpacityInput from 'src/components/opacity_input/opacity_input.vue' +import Select from 'src/components/select/select.vue' +import Checkbox from 'src/components/checkbox/checkbox.vue' +import Popover from 'src/components/popover/popover.vue' +import ComponentPreview from 'src/components/component_preview/component_preview.vue' +import { getCssShadow, getCssShadowFilter } from '../../services/theme_data/theme_data.service.js' import { library } from '@fortawesome/fontawesome-svg-core' +import { throttle } from 'lodash' import { faTimes, faChevronDown, @@ -30,93 +33,100 @@ const toModel = (object = {}) => ({ }) export default { - // 'modelValue' and 'Fallback' can be undefined, but if they are - // initially vue won't detect it when they become something else - // therefore i'm using "ready" which should be passed as true when - // data becomes available props: [ - 'modelValue', 'fallback', 'ready' + 'modelValue', 'fallback', 'separateInset', 'noPreview', 'disabled' ], - emits: ['update:modelValue'], + emits: ['update:modelValue', 'subShadowSelected'], data () { return { selectedId: 0, // TODO there are some bugs regarding display of array (it's not getting updated when deleting for some reason) - cValue: (this.modelValue || this.fallback || []).map(toModel) + cValue: (this.modelValue ?? this.fallback ?? []).map(toModel) } }, components: { ColorInput, OpacityInput, - Select + Select, + Checkbox, + Popover, + ComponentPreview + }, + beforeUpdate () { + this.cValue = (this.modelValue ?? this.fallback ?? []).map(toModel) + }, + computed: { + selected () { + const selected = this.cValue[this.selectedId] + if (selected) { + return { ...selected } + } + return null + }, + present () { + return this.selected != null && !this.usingFallback + }, + shadowsAreNull () { + return this.modelValue == null + }, + currentFallback () { + return this.fallback?.[this.selectedId] + }, + moveUpValid () { + return this.selectedId > 0 + }, + moveDnValid () { + return this.selectedId < this.cValue.length - 1 + }, + usingFallback () { + return this.modelValue == null + }, + style () { + if (this.separateInset) { + return { + filter: getCssShadowFilter(this.cValue), + boxShadow: getCssShadow(this.cValue, true) + } + } + return { + boxShadow: getCssShadow(this.cValue) + } + } + }, + watch: { + selected (value) { + this.$emit('subShadowSelected', this.selectedId) + } }, methods: { + updateProperty: throttle(function (prop, value) { + this.cValue[this.selectedId][prop] = value + if (prop === 'inset' && value === false && this.separateInset) { + this.cValue[this.selectedId].spread = 0 + } + this.$emit('update:modelValue', this.cValue) + }, 100), add () { this.cValue.push(toModel(this.selected)) - this.selectedId = this.cValue.length - 1 + this.selectedId = Math.max(this.cValue.length - 1, 0) + this.$emit('update:modelValue', this.cValue) }, del () { this.cValue.splice(this.selectedId, 1) this.selectedId = this.cValue.length === 0 ? undefined : Math.max(this.selectedId - 1, 0) + this.$emit('update:modelValue', this.cValue) }, moveUp () { const movable = this.cValue.splice(this.selectedId, 1)[0] this.cValue.splice(this.selectedId - 1, 0, movable) this.selectedId -= 1 + this.$emit('update:modelValue', this.cValue) }, moveDn () { const movable = this.cValue.splice(this.selectedId, 1)[0] this.cValue.splice(this.selectedId + 1, 0, movable) this.selectedId += 1 - } - }, - beforeUpdate () { - this.cValue = this.modelValue || this.fallback - }, - computed: { - anyShadows () { - return this.cValue.length > 0 - }, - anyShadowsFallback () { - return this.fallback.length > 0 - }, - selected () { - if (this.ready && this.anyShadows) { - return this.cValue[this.selectedId] - } else { - return toModel({}) - } - }, - currentFallback () { - if (this.ready && this.anyShadowsFallback) { - return this.fallback[this.selectedId] - } else { - return toModel({}) - } - }, - moveUpValid () { - return this.ready && this.selectedId > 0 - }, - moveDnValid () { - return this.ready && this.selectedId < this.cValue.length - 1 - }, - present () { - return this.ready && - typeof this.cValue[this.selectedId] !== 'undefined' && - !this.usingFallback - }, - usingFallback () { - return typeof this.modelValue === 'undefined' - }, - rgb () { - return hex2rgb(this.selected.color) - }, - style () { - return this.ready - ? { - boxShadow: getCssShadow(this.fallback) - } - : {} + this.$emit('update:modelValue', this.cValue) } } } diff --git a/src/components/shadow_control/shadow_control.scss b/src/components/shadow_control/shadow_control.scss @@ -0,0 +1,105 @@ +.settings-modal .settings-modal-panel .shadow-control { + display: flex; + flex-wrap: wrap; + justify-content: stretch; + grid-gap: 0.25em; + margin-bottom: 1em; + + .shadow-switcher { + order: 1; + flex: 1 0 6em; + min-width: 6em; + margin-right: 0.125em; + display: flex; + flex-direction: column; + + .shadow-list { + flex: 1 0 auto; + } + + .arrange-buttons { + flex: 0 0 auto; + display: grid; + grid-auto-columns: 1fr; + grid-auto-flow: column; + margin-top: 0.25em; + + .button-default { + margin: 0; + padding: 0; + } + } + } + + .shadow-tweak { + order: 3; + flex: 2 0 10em; + min-width: 10em; + margin-left: 0.125em; + margin-right: 0.125em; + + /* hack */ + .input-boolean { + flex: 1; + display: flex; + + .label { + flex: 1; + } + } + + .input-string { + flex: 1 0 5em; + } + + .id-control { + align-items: stretch; + + .shadow-switcher, + .btn { + min-width: 1px; + margin-right: 5px; + } + + .btn { + padding: 0 0.4em; + margin: 0 0.1em; + } + } + } + + &.-no-preview { + .shadow-tweak { + order: 0; + flex: 2 0 8em; + max-width: 100%; + } + + .input-range { + min-width: 5em; + } + } + + .inset-alert { + padding: 0.25em 0.5em; + } + + &.disabled { + .inset-alert { + opacity: 0.2; + } + } + + .shadow-preview { + order: 2; + flex: 3 3 15em; + min-width: 10em; + margin-left: 0.125em; + align-self: start; + } +} + +.inset-tooltip { + padding: 0.5em; + max-width: 30em; +} diff --git a/src/components/shadow_control/shadow_control.vue b/src/components/shadow_control/shadow_control.vue @@ -1,91 +1,51 @@ <template> <div - class="shadow-control" - :class="{ disabled: !present }" + class="label shadow-control" + :class="{ disabled: disabled || !present, '-no-preview': noPreview }" > - <div class="shadow-preview-container"> - <div - :disabled="!present" - class="y-shift-control" - > - <input - v-model="selected.y" - :disabled="!present" - class="input input-number" - type="number" - > - <div class="wrap"> - <input - v-model="selected.y" - :disabled="!present" - class="input input-range" - type="range" - max="20" - min="-20" - > - </div> - </div> - <div class="preview-window"> - <div - class="preview-block" - :style="style" - /> - </div> - <div - :disabled="!present" - class="x-shift-control" + <ComponentPreview + v-if="!noPreview" + class="shadow-preview" + :shadow-control="true" + :shadow="selected" + :preview-style="style" + :disabled="disabled || !present" + @update:shadow="({ axis, value }) => updateProperty(axis, value)" + /> + <div class="shadow-switcher"> + <Select + id="shadow-list" + v-model="selectedId" + class="shadow-list" + size="10" + :disabled="shadowsAreNull" > - <input - v-model="selected.x" - :disabled="!present" - class="input input-number" - type="number" + <option + v-for="(shadow, index) in cValue" + :key="index" + :value="index" + :class="{ '-active': index === Number(selectedId) }" > - <div class="wrap"> - <input - v-model="selected.x" - :disabled="!present" - class="input input-range" - type="range" - max="20" - min="-20" - > - </div> - </div> - </div> - - <div class="shadow-tweak"> + {{ shadow?.name ?? $t('settings.style.shadows.shadow_id', { value: index }) }} + </option> + </Select> <div - :disabled="usingFallback" - class="id-control style-control" + class="id-control btn-group arrange-buttons" > - <Select - id="shadow-switcher" - v-model="selectedId" - class="shadow-switcher" - :disabled="!ready || usingFallback" - > - <option - v-for="(shadow, index) in cValue" - :key="index" - :value="index" - > - {{ $t('settings.style.shadows.shadow_id', { value: index }) }} - </option> - </Select> <button class="btn button-default" - :disabled="!ready || !present" - @click="del" + :disabled="disabled || shadowsAreNull" + @click="add" > <FAIcon fixed-width - icon="times" + icon="plus" /> </button> <button class="btn button-default" - :disabled="!moveUpValid" + :disabled="disabled || !moveUpValid" + :class="{ disabled: disabled || !moveUpValid }" @click="moveUp" > <FAIcon @@ -95,7 +55,8 @@ </button> <button class="btn button-default" - :disabled="!moveDnValid" + :disabled="disabled || !moveDnValid" + :class="{ disabled: disabled || !moveDnValid }" @click="moveDn" > <FAIcon @@ -105,222 +66,191 @@ </button> <button class="btn button-default" - :disabled="usingFallback" - @click="add" + :disabled="disabled || !present" + :class="{ disabled: disabled || !present }" + @click="del" > <FAIcon fixed-width - icon="plus" + icon="times" /> </button> </div> + </div> + <div class="shadow-tweak"> <div - :disabled="!present" - class="inset-control style-control" + :class="{ disabled: disabled || !present }" + class="name-control style-control" > <label - for="inset" + for="name" class="label" + :class="{ faint: disabled || !present }" > - {{ $t('settings.style.shadows.inset') }} + {{ $t('settings.style.shadows.name') }} </label> <input + id="name" + :value="selected?.name" + :disabled="disabled || !present" + :class="{ disabled: disabled || !present }" + name="name" + class="input input-string" + @input="e => updateProperty('name', e.target.value)" + > + </div> + <div + :disabled="disabled || !present" + class="inset-control style-control" + > + <Checkbox id="inset" - v-model="selected.inset" - :disabled="!present" + :value="selected?.inset" + :disabled="disabled || !present" name="inset" - class="input -checkbox input-inset visible-for-screenreader-only" - type="checkbox" + class="input-inset input-boolean" + @input="e => updateProperty('inset', e.target.checked)" > - <label - class="checkbox-label" - for="inset" - :aria-hidden="true" - /> + <template #before> + {{ $t('settings.style.shadows.inset') }} + </template> + </Checkbox> </div> <div - :disabled="!present" + :disabled="disabled || !present" + :class="{ disabled: disabled || !present }" class="blur-control style-control" > <label - for="spread" + for="blur" class="label" + :class="{ faint: disabled || !present }" > {{ $t('settings.style.shadows.blur') }} </label> <input id="blur" - v-model="selected.blur" - :disabled="!present" + :value="selected?.blur" + :disabled="disabled || !present" + :class="{ disabled: disabled || !present }" name="blur" class="input input-range" type="range" max="20" min="0" + @input="e => updateProperty('blur', e.target.value)" > <input - v-model="selected.blur" - :disabled="!present" - class="input input-number" + :value="selected?.blur" + class="input input-number -small" + :disabled="disabled || !present" + :class="{ disabled: disabled || !present }" type="number" min="0" + @input="e => updateProperty('blur', e.target.value)" > </div> <div - :disabled="!present" class="spread-control style-control" + :class="{ disabled: disabled || !present || (separateInset && !selected?.inset) }" > <label for="spread" class="label" + :class="{ faint: disabled || !present || (separateInset && !selected?.inset) }" > {{ $t('settings.style.shadows.spread') }} </label> <input id="spread" - v-model="selected.spread" - :disabled="!present" + :value="selected?.spread" + :disabled="disabled || !present || (separateInset && !selected?.inset)" + :class="{ disabled: disabled || !present || (separateInset && !selected?.inset) }" name="spread" class="input input-range" type="range" max="20" min="-20" + @input="e => updateProperty('spread', e.target.value)" > <input - v-model="selected.spread" - :disabled="!present" - class="input input-number" + :value="selected?.spread" + class="input input-number -small" + :class="{ disabled: disabled || !present || (separateInset && !selected?.inset) }" + :disabled="{ disabled: disabled || !present || (separateInset && !selected?.inset) }" type="number" + @input="e => updateProperty('spread', e.target.value)" > </div> <ColorInput - v-model="selected.color" - :disabled="!present" + :model-value="selected?.color" + :disabled="disabled || !present" :label="$t('settings.style.common.color')" - :fallback="currentFallback.color" + :fallback="currentFallback?.color" :show-optional-tickbox="false" name="shadow" + @update:modelValue="e => updateProperty('color', e)" /> <OpacityInput - v-model="selected.alpha" - :disabled="!present" + :model-value="selected?.alpha" + :disabled="disabled || !present" + @update:modelValue="e => updateProperty('alpha', e)" /> <i18n-t scope="global" keypath="settings.style.shadows.hintV3" + :class="{ faint: disabled || !present }" tag="p" > <code>--variable,mod</code> </i18n-t> + <Popover + v-if="separateInset" + trigger="hover" + > + <template #trigger> + <div + class="inset-alert alert warning" + > + <FAIcon icon="exclamation-triangle" /> + &nbsp; + {{ $t('settings.style.shadows.filter_hint.avatar_inset_short') }} + </div> + </template> + <template #content> + <div class="inset-tooltip"> + <i18n-t + scope="global" + keypath="settings.style.shadows.filter_hint.always_drop_shadow" + tag="p" + > + <code>filter: drop-shadow()</code> + </i18n-t> + <p>{{ $t('settings.style.shadows.filter_hint.avatar_inset') }}</p> + <i18n-t + scope="global" + keypath="settings.style.shadows.filter_hint.drop_shadow_syntax" + tag="p" + > + <code>drop-shadow</code> + <code>spread-radius</code> + <code>inset</code> + </i18n-t> + <i18n-t + scope="global" + keypath="settings.style.shadows.filter_hint.inset_classic" + tag="p" + > + <code>box-shadow</code> + </i18n-t> + <p>{{ $t('settings.style.shadows.filter_hint.spread_zero') }}</p> + </div> + </template> + </Popover> </div> </div> </template> <script src="./shadow_control.js"></script> -<style lang="scss"> -.shadow-control { - display: flex; - flex-wrap: wrap; - justify-content: center; - margin-bottom: 1em; - - .shadow-preview-container, - .shadow-tweak { - margin: 5px 6px 0 0; - } - - .shadow-preview-container { - flex: 0; - display: flex; - flex-wrap: wrap; - - input[type="number"] { - width: 5em; - min-width: 2em; - } - - .x-shift-control, - .y-shift-control { - display: flex; - flex: 0; - - &[disabled="disabled"] * { - opacity: 0.5; - } - } - - .x-shift-control { - align-items: flex-start; - } - - .x-shift-control .wrap, - input[type="range"] { - margin: 0; - width: 15em; - height: 2em; - } - - .y-shift-control { - flex-direction: column; - align-items: flex-end; - - .wrap { - width: 2em; - height: 15em; - } - - input[type="range"] { - transform-origin: 1em 1em; - transform: rotate(90deg); - } - } - - .preview-window { - flex: 1; - background-color: #999; - display: flex; - align-items: center; - justify-content: center; - background-image: - linear-gradient(45deg, #666 25%, transparent 25%), - linear-gradient(-45deg, #666 25%, transparent 25%), - linear-gradient(45deg, transparent 75%, #666 75%), - linear-gradient(-45deg, transparent 75%, #666 75%); - background-size: 20px 20px; - background-position: 0 0, 0 10px, 10px -10px, -10px 0; - border-radius: var(--roundness); - - .preview-block { - width: 33%; - height: 33%; - border-radius: var(--roundness); - } - } - } - - .shadow-tweak { - flex: 1; - min-width: 280px; - - .id-control { - align-items: stretch; - - .shadow-switcher { - flex: 1; - } - - .shadow-switcher, - .btn { - min-width: 1px; - margin-right: 5px; - } - - .btn { - padding: 0 0.4em; - margin: 0 0.1em; - } - } - } -} -</style> +<style src="./shadow_control.scss" lang="scss"></style> diff --git a/src/i18n/en.json b/src/i18n/en.json @@ -876,6 +876,9 @@ "component": "Component", "override": "Override", "shadow_id": "Shadow #{value}", + "offset": "Shadow offset", + "light_grid": "Use light checkerboard", + "name": "Name", "blur": "Blur", "spread": "Spread", "inset": "Inset", @@ -883,6 +886,7 @@ "filter_hint": { "always_drop_shadow": "Warning, this shadow always uses {0} when browser supports it.", "drop_shadow_syntax": "{0} does not support {1} parameter and {2} keyword.", + "avatar_inset_short": "Separate inset shadow", "avatar_inset": "Please note that combining both inset and non-inset shadows on avatars might give unexpected results with transparent avatars.", "spread_zero": "Shadows with spread > 0 will appear as if it was set to zero", "inset_classic": "Inset shadows will be using {0}" @@ -1408,6 +1412,18 @@ "unicode_domain_indicator": { "tooltip": "This domain contains non-ascii characters." }, + "splash": { + "loading": "Loading...", + "theme": "Applying theme, please wait warmly...", + "instance": "Getting instance info...", + "settings": "Applying settings...", + "almost": "Reticulating splines...", + "fun_1": "Drink more water", + "fun_2": "Take it easy!", + "fun_3": "Suya...", + "fun_4": "My Pleroma machine is full power!", + "error": "Something went wrong" + }, "bookmark_folders": { "select_folder": "Select bookmark folder", "creating_folder": "Creating bookmark folder", diff --git a/src/i18n/nan-TW.json b/src/i18n/nan-TW.json @@ -190,7 +190,8 @@ "mobile_notifications_close": "關掉通知", "announcements": "公告", "search": "Tshuē", - "mobile_notifications_mark_as_seen": "Lóng 標做有讀" + "mobile_notifications_mark_as_seen": "Lóng 標做有讀", + "quotes": "引用" }, "notifications": { "broken_favorite": "狀態毋知影,leh tshiau-tshuē……", @@ -212,7 +213,8 @@ "unread_follow_requests": "{num}ê新ê跟tuè請求", "configuration_tip": "用{theSettings},lí通自訂siánn物佇tsia顯示。{dismiss}", "configuration_tip_settings": "設定", - "configuration_tip_dismiss": "Mài koh顯示" + "configuration_tip_dismiss": "Mài koh顯示", + "subscribed_status": "有發送ê" }, "polls": { "add_poll": "開投票", @@ -252,7 +254,8 @@ }, "load_all_hint": "載入頭前 {saneAmount} ê 繪文字,規个攏載入效能可能 ē khah 食力。", "load_all": "Kā {emojiAmount} ê 繪文字攏載入", - "regional_indicator": "地區指引 {letter}" + "regional_indicator": "地區指引 {letter}", + "hide_custom_emoji": "Khàm掉自訂ê繪文字" }, "errors": { "storage_unavailable": "Pleroma buē-tàng the̍h 著瀏覽器儲存 ê。Lí ê 登入狀態抑是局部設定 buē 儲存,mā 凡勢 tú 著意料外 ê 問題。拍開 cookie 看māi。" @@ -263,7 +266,8 @@ "emoji_reactions": "繪文字 ê 反應", "reports": "檢舉", "moves": "用者 ê 移民", - "load_older": "載入 koh khah 早 ê 互動" + "load_older": "載入 koh khah 早 ê 互動", + "statuses": "訂ê" }, "post_status": { "edit_status": "編輯狀態", @@ -935,7 +939,34 @@ "notification_extra_chats": "顯示bô讀ê開講", "notification_extra_announcements": "顯示bô讀ê公告", "notification_extra_follow_requests": "顯示新ê跟tuè請求", - "notification_extra_tip": "顯示自訂其他通知ê撇步" + "notification_extra_tip": "顯示自訂其他通知ê撇步", + "confirm_new_setting": "Lí敢確認新ê設定?", + "text_size_tip": "用 {0} 做絕對值,{1} ē根據瀏覽器ê標準文字sài-suh放大縮小。", + "theme_debug": "佇處理透明ê時,顯示背景主題ia̋n-jín 所假使ê(DEBUG)", + "units": { + "time": { + "s": "秒鐘", + "m": "分鐘", + "h": "點鐘", + "d": "工" + } + }, + "actor_type": "Tsit ê口座是:", + "actor_type_Person": "一般ê用者", + "actor_type_description": "標記lí ê口座做群組,ē hōo自動轉送提起伊ê狀態。", + "actor_type_Group": "群組", + "actor_type_Service": "機器lâng", + "appearance": "外觀", + "confirm_new_question": "Tse看起來kám好?設定ē佇10秒鐘後改轉去。", + "revert": "改轉去", + "confirm": "確認", + "text_size": "文字kap界面ê sài-suh", + "text_size_tip2": "毋是 {0} ê值可能ē破壞一寡物件kap主題", + "emoji_size": "繪文字ê sài-suh", + "navbar_size": "頂 liâu-á êsài-suh", + "panel_header_size": "面pang標題ê sài-suh", + "visual_tweaks": "細細ê外觀調整", + "scale_and_layout": "界面ê sài-suh kap排列" }, "status": { "favorites": "收藏", @@ -1001,7 +1032,7 @@ "show_only_conversation_under_this": "Kan-ta顯示tsit ê狀態ê回應", "status_history": "狀態ê歷史", "reaction_count_label": "{num}ê lâng用表情反應", - "hide_quote": "Khàm條引用ê狀態", + "hide_quote": "Khàm掉引用ê狀態", "display_quote": "顯示引用ê狀態", "invisible_quote": "引用ê狀態bē當用:{link}", "more_actions": "佇tsit ê狀態ê其他動作" diff --git a/src/main.js b/src/main.js @@ -58,56 +58,76 @@ const persistedStateOptions = { }; (async () => { - let storageError = false - const plugins = [pushNotifications] - try { - const persistedState = await createPersistedState(persistedStateOptions) - plugins.push(persistedState) - } catch (e) { - console.error(e) - storageError = true + const isFox = Math.floor(Math.random() * 2) > 0 ? '_fox' : '' + + const splashError = (i18n, e) => { + const throbber = document.querySelector('#throbber') + throbber.addEventListener('animationend', () => { + document.querySelector('#mascot').src = `/static/pleromatan_orz${isFox}.png` + }) + throbber.classList.add('dead') + document.querySelector('#status').textContent = i18n.global.t('splash.error') + console.error('PleromaFE failed to initialize: ', e) } - const store = createStore({ - modules: { - i18n: { - getters: { - i18n: () => i18n.global - } + + try { + let storageError + const plugins = [pushNotifications] + try { + const persistedState = await createPersistedState(persistedStateOptions) + plugins.push(persistedState) + } catch (e) { + console.error('Storage error', e) + storageError = e + } + document.querySelector('#mascot').src = `/static/pleromatan_apology${isFox}.png` + document.querySelector('#status').removeAttribute('class') + document.querySelector('#status').textContent = i18n.global.t('splash.loading') + document.querySelector('#splash-credit').textContent = i18n.global.t('update.art_by', { linkToArtist: 'pipivovott' }) + const store = createStore({ + modules: { + i18n: { + getters: { + i18n: () => i18n.global + } + }, + interface: interfaceModule, + instance: instanceModule, + // TODO refactor users/statuses modules, they depend on each other + users: usersModule, + statuses: statusesModule, + notifications: notificationsModule, + lists: listsModule, + api: apiModule, + config: configModule, + profileConfig: profileConfigModule, + serverSideStorage: serverSideStorageModule, + adminSettings: adminSettingsModule, + shout: shoutModule, + oauth: oauthModule, + authFlow: authFlowModule, + mediaViewer: mediaViewerModule, + oauthTokens: oauthTokensModule, + reports: reportsModule, + polls: pollsModule, + postStatus: postStatusModule, + editStatus: editStatusModule, + statusHistory: statusHistoryModule, + chats: chatsModule, + announcements: announcementsModule, + bookmarkFolders: bookmarkFoldersModule }, - interface: interfaceModule, - instance: instanceModule, - // TODO refactor users/statuses modules, they depend on each other - users: usersModule, - statuses: statusesModule, - notifications: notificationsModule, - lists: listsModule, - api: apiModule, - config: configModule, - profileConfig: profileConfigModule, - serverSideStorage: serverSideStorageModule, - adminSettings: adminSettingsModule, - shout: shoutModule, - oauth: oauthModule, - authFlow: authFlowModule, - mediaViewer: mediaViewerModule, - oauthTokens: oauthTokensModule, - reports: reportsModule, - polls: pollsModule, - postStatus: postStatusModule, - editStatus: editStatusModule, - statusHistory: statusHistoryModule, - chats: chatsModule, - announcements: announcementsModule, - bookmarkFolders: bookmarkFoldersModule - }, - plugins, - strict: false // Socket modifies itself, let's ignore this for now. - // strict: process.env.NODE_ENV !== 'production' - }) - if (storageError) { - store.dispatch('pushGlobalNotice', { messageKey: 'errors.storage_unavailable', level: 'error' }) + plugins, + strict: false // Socket modifies itself, let's ignore this for now. + // strict: process.env.NODE_ENV !== 'production' + }) + if (storageError) { + store.dispatch('pushGlobalNotice', { messageKey: 'errors.storage_unavailable', level: 'error' }) + } + return await afterStoreSetup({ store, i18n }) + } catch (e) { + splashError(i18n, e) } - afterStoreSetup({ store, i18n }) })() // These are inlined by webpack's DefinePlugin diff --git a/src/services/style_setter/style_setter.js b/src/services/style_setter/style_setter.js @@ -43,16 +43,16 @@ const adoptStyleSheets = (styles) => { // is nothing to do here. } -export const generateTheme = async (inputRuleset, callbacks, debug) => { +export const generateTheme = (inputRuleset, callbacks, debug) => { const { onNewRule = (rule, isLazy) => {}, onLazyFinished = () => {}, onEagerFinished = () => {} } = callbacks - // Assuming that "worst case scenario background" is panel background since it's the most likely one const themes3 = init({ inputRuleset, + // Assuming that "worst case scenario background" is panel background since it's the most likely one ultimateBackgroundColor: inputRuleset[0].directives['--bg'].split('|')[1].trim(), debug }) @@ -146,11 +146,11 @@ export const tryLoadCache = () => { } } -export const applyTheme = async (input, onFinish = (data) => {}, debug) => { +export const applyTheme = (input, onFinish = (data) => {}, debug) => { const eagerStyles = createStyleSheet(EAGER_STYLE_ID) const lazyStyles = createStyleSheet(LAZY_STYLE_ID) - const { lazyProcessFunc } = await generateTheme( + const { lazyProcessFunc } = generateTheme( input, { onNewRule (rule, isLazy) { @@ -169,15 +169,22 @@ export const applyTheme = async (input, onFinish = (data) => {}, debug) => { adoptStyleSheets([eagerStyles, lazyStyles]) const cache = { engineChecksum: getEngineChecksum(), data: [eagerStyles.rules, lazyStyles.rules] } onFinish(cache) - localStorage.setItem('pleroma-fe-theme-cache', JSON.stringify(cache)) + try { + localStorage.setItem('pleroma-fe-theme-cache', JSON.stringify(cache)) + } catch (e) { + localStorage.removeItem('pleroma-fe-theme-cache') + try { + localStorage.setItem('pleroma-fe-theme-cache', JSON.stringify(cache)) + } catch (e) { + console.warn('cannot save cache!', e) + } + } } }, debug ) setTimeout(lazyProcessFunc, 0) - - return Promise.resolve() } const extractStyleConfig = ({ @@ -222,7 +229,7 @@ const extractStyleConfig = ({ const defaultStyleConfig = extractStyleConfig(defaultState) -export const applyConfig = (input) => { +export const applyConfig = (input, i18n) => { const config = extractStyleConfig(input) if (config === defaultStyleConfig) { @@ -230,8 +237,6 @@ export const applyConfig = (input) => { } const head = document.head - const body = document.body - body.classList.add('hidden') const rules = Object .entries(config) @@ -252,8 +257,6 @@ export const applyConfig = (input) => { --roundness: var(--forcedRoundness) !important; }`, 'index-max') } - - body.classList.remove('hidden') } export const getThemes = () => { diff --git a/src/services/theme_data/theme_data.service.js b/src/services/theme_data/theme_data.service.js @@ -452,7 +452,7 @@ export const getCssShadow = (input, usesDropShadow) => { ]).join(' ')).join(', ') } -const getCssShadowFilter = (input) => { +export const getCssShadowFilter = (input) => { if (input.length === 0) { return 'none' } diff --git a/src/services/theme_data/theme_data_3.service.js b/src/services/theme_data/theme_data_3.service.js @@ -182,7 +182,7 @@ export const init = ({ const rulesetUnsorted = [ ...Object.values(components) - .map(c => (c.defaultRules || []).map(r => ({ component: c.name, ...r, source: 'Built-in' }))) + .map(c => (c.defaultRules || []).map(r => ({ source: 'Built-in', component: c.name, ...r }))) .reduce((acc, arr) => [...acc, ...arr], []), ...inputRuleset ].map(rule => { @@ -198,18 +198,33 @@ export const init = ({ const ruleset = rulesetUnsorted .map((data, index) => ({ data, index })) - .sort(({ data: a, index: ai }, { data: b, index: bi }) => { + .toSorted(({ data: a, index: ai }, { data: b, index: bi }) => { const parentsA = unroll(a).length const parentsB = unroll(b).length - if (parentsA === parentsB) { - if (a.component === 'Text') return -1 - if (b.component === 'Text') return 1 + let aScore = 0 + let bScore = 0 + + aScore += parentsA * 1000 + bScore += parentsB * 1000 + + aScore += a.variant !== 'normal' ? 100 : 0 + bScore += b.variant !== 'normal' ? 100 : 0 + + aScore += a.state.filter(x => x !== 'normal').length * 1000 + bScore += b.state.filter(x => x !== 'normal').length * 1000 + + aScore += a.component === 'Text' ? 1 : 0 + bScore += b.component === 'Text' ? 1 : 0 + + // Debug + a.specifityScore = aScore + b.specifityScore = bScore + + if (aScore === bScore) { return ai - bi } - if (parentsA === 0 && parentsB !== 0) return -1 - if (parentsB === 0 && parentsA !== 0) return 1 - return parentsA - parentsB + return aScore - bScore }) .map(({ data }) => data) @@ -235,7 +250,10 @@ export const init = ({ // Inheriting all of the applicable rules const existingRules = ruleset.filter(findRules(combination)) - const computedDirectives = existingRules.map(r => r.directives).reduce((acc, directives) => ({ ...acc, ...directives }), {}) + const computedDirectives = + existingRules + .map(r => r.directives) + .reduce((acc, directives) => ({ ...acc, ...directives }), {}) const computedRule = { ...combination, directives: computedDirectives diff --git a/src/assets/pleromatan_apology.png b/static/pleromatan_apology.png Binary files differ. diff --git a/src/assets/pleromatan_apology_fox.png b/static/pleromatan_apology_fox.png Binary files differ. diff --git a/static/pleromatan_orz.png b/static/pleromatan_orz.png Binary files differ. diff --git a/static/pleromatan_orz_fox.png b/static/pleromatan_orz_fox.png Binary files differ.