Compare commits
45 commits
66cebb9452
...
e7406974d5
Author | SHA1 | Date | |
---|---|---|---|
|
e7406974d5 | ||
|
13838a75a9 | ||
|
20e6382df6 | ||
|
dee3cf7883 | ||
|
ab295c588d | ||
|
02ecd8bb6c | ||
|
424da4c311 | ||
|
e876c98d5e | ||
|
2eb8e1e095 | ||
|
e8d0e45b5b | ||
|
ba4be2cb22 | ||
|
f0957bdb4f | ||
|
07a48315a1 | ||
|
89b05cfc57 | ||
|
d2cce99086 | ||
|
d5571216fe | ||
|
596a1e4961 | ||
|
05ab57a8e6 | ||
|
c677bbf102 | ||
|
c2dfe62481 | ||
|
b599407b67 | ||
|
9753db1c67 | ||
|
2a98ea6ddc | ||
|
6230edcbec | ||
|
dd4cab74d1 | ||
|
58ac749755 | ||
|
0cec6dc356 | ||
|
4ddc191928 | ||
|
d234ad8672 | ||
|
e541e2c682 | ||
|
45f6e003c4 | ||
|
487d9c447d | ||
|
aa922faf62 | ||
|
415180e8fa | ||
|
fb40694e8e | ||
|
ef795becf6 | ||
|
b4a1bcd070 | ||
|
f1468a3f5d | ||
|
e7eb1059c3 | ||
|
e1d3ebc943 | ||
|
d5549ac1ee | ||
|
9db5552f30 | ||
|
22d3c13135 | ||
|
c73965bee0 | ||
|
144d426864 |
48 changed files with 2299 additions and 558 deletions
|
@ -327,7 +327,11 @@ const setConfig = async ({ store }) => {
|
|||
|
||||
const checkOAuthToken = async ({ store }) => {
|
||||
if (store.getters.getUserToken()) {
|
||||
return store.dispatch('loginUser', store.getters.getUserToken())
|
||||
try {
|
||||
await store.dispatch('loginUser', store.getters.getUserToken())
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
@ -345,26 +349,19 @@ 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 })
|
||||
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)
|
||||
}
|
||||
await store.dispatch('applyTheme', { recompile: false })
|
||||
|
||||
applyConfig(store.state.config, i18n.global)
|
||||
applyConfig(store.state.config)
|
||||
|
||||
// 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')
|
||||
|
@ -398,9 +395,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
|
||||
}
|
||||
|
||||
|
|
|
@ -14,6 +14,10 @@ export default {
|
|||
warning: '.warning',
|
||||
success: '.success'
|
||||
},
|
||||
editor: {
|
||||
border: 1,
|
||||
aspect: '3 / 1'
|
||||
},
|
||||
defaultRules: [
|
||||
{
|
||||
directives: {
|
||||
|
|
|
@ -5,7 +5,7 @@ export default {
|
|||
defaultRules: [
|
||||
{
|
||||
directives: {
|
||||
textColor: '$mod(--parent, 10)',
|
||||
textColor: '$mod(--parent 10)',
|
||||
textAuto: 'no-auto'
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,9 +9,9 @@ export default {
|
|||
// However, cascading still works, so resulting state will be result of merging of all relevant states/variants
|
||||
// normal: '' // normal state is implicitly added, it is always included
|
||||
toggled: '.toggled',
|
||||
pressed: ':active',
|
||||
focused: ':focus-visible',
|
||||
pressed: ':focus:active',
|
||||
hover: ':hover:not(:disabled)',
|
||||
focused: ':focus-within',
|
||||
disabled: ':disabled'
|
||||
},
|
||||
// Variants are mutually exclusive, each component implicitly has "normal" variant, and all other variants inherit from it.
|
||||
|
@ -22,6 +22,9 @@ export default {
|
|||
// Overall the compuation difficulty is N*((1/6)M^3+M) where M is number of distinct states and N is number of variants.
|
||||
// This (currently) is further multipled by number of places where component can exist.
|
||||
},
|
||||
editor: {
|
||||
aspect: '2 / 1'
|
||||
},
|
||||
// This lists all other components that can possibly exist within one. Recursion is currently not supported (and probably won't be supported ever).
|
||||
validInnerComponents: [
|
||||
'Text',
|
||||
|
@ -32,10 +35,11 @@ export default {
|
|||
{
|
||||
component: 'Root',
|
||||
directives: {
|
||||
'--defaultButtonHoverGlow': 'shadow | 0 0 4 --text',
|
||||
'--defaultButtonHoverGlow': 'shadow | 0 0 4 --text / 0.5',
|
||||
'--defaultButtonFocusGlow': 'shadow | 0 0 4 4 --link / 0.5',
|
||||
'--defaultButtonShadow': 'shadow | 0 0 2 #000000',
|
||||
'--defaultButtonBevel': 'shadow | $borderSide(#FFFFFF, top, 0.2), $borderSide(#000000, bottom, 0.2)',
|
||||
'--pressedButtonBevel': 'shadow | $borderSide(#FFFFFF, bottom, 0.2), $borderSide(#000000, top, 0.2)'
|
||||
'--defaultButtonBevel': 'shadow | $borderSide(#FFFFFF top 0.2 2), $borderSide(#000000 bottom 0.2 2)',
|
||||
'--pressedButtonBevel': 'shadow | $borderSide(#FFFFFF bottom 0.2 2), $borderSide(#000000 top 0.2 2)'
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -53,6 +57,12 @@ export default {
|
|||
shadow: ['--defaultButtonHoverGlow', '--defaultButtonBevel']
|
||||
}
|
||||
},
|
||||
{
|
||||
state: ['focused'],
|
||||
directives: {
|
||||
shadow: ['--defaultButtonFocusGlow', '--defaultButtonBevel']
|
||||
}
|
||||
},
|
||||
{
|
||||
state: ['pressed'],
|
||||
directives: {
|
||||
|
@ -60,9 +70,9 @@ export default {
|
|||
}
|
||||
},
|
||||
{
|
||||
state: ['hover', 'pressed'],
|
||||
state: ['pressed', 'hover'],
|
||||
directives: {
|
||||
shadow: ['--defaultButtonHoverGlow', '--pressedButtonBevel']
|
||||
shadow: ['--pressedButtonBevel', '--defaultButtonHoverGlow']
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -82,7 +92,7 @@ export default {
|
|||
{
|
||||
state: ['disabled'],
|
||||
directives: {
|
||||
background: '$blend(--inheritedBackground, 0.25, --parent)',
|
||||
background: '$blend(--inheritedBackground 0.25 --parent)',
|
||||
shadow: ['--defaultButtonBevel']
|
||||
}
|
||||
},
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
export default {
|
||||
name: 'ButtonUnstyled',
|
||||
selector: '.button-unstyled',
|
||||
notEditable: true,
|
||||
states: {
|
||||
toggled: '.toggled',
|
||||
disabled: ':disabled',
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
:model-value="present"
|
||||
:disabled="disabled"
|
||||
class="opt"
|
||||
@update:modelValue="update(typeof modelValue === 'undefined' ? fallback : undefined)"
|
||||
@update:modelValue="updateValue(typeof modelValue === 'undefined' ? fallback : undefined)"
|
||||
/>
|
||||
<div
|
||||
class="input color-input-field"
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
<template>
|
||||
<div class="ComponentPreview">
|
||||
<div
|
||||
class="ComponentPreview"
|
||||
:class="{ '-shadow-controls': shadowControl }"
|
||||
>
|
||||
<label
|
||||
class="header"
|
||||
v-show="shadowControl"
|
||||
class="header"
|
||||
:class="{ faint: disabled }"
|
||||
>
|
||||
{{ $t('settings.style.shadows.offset') }}
|
||||
|
@ -29,12 +32,15 @@
|
|||
>
|
||||
<div
|
||||
class="preview-window"
|
||||
:class="{ disabled: disabled && shadowControl, '-light-grid': lightGrid }"
|
||||
:class="{ '-light-grid': lightGrid }"
|
||||
>
|
||||
<div
|
||||
class="preview-block"
|
||||
:class="previewClass"
|
||||
:style="previewStyle"
|
||||
/>
|
||||
>
|
||||
TEST
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
v-show="shadowControl"
|
||||
|
@ -59,15 +65,41 @@
|
|||
<Checkbox
|
||||
id="lightGrid"
|
||||
v-model="lightGrid"
|
||||
:disabled="shadow == null"
|
||||
name="lightGrid"
|
||||
class="input-light-grid"
|
||||
>
|
||||
{{ $t('settings.style.shadows.light_grid') }}
|
||||
</Checkbox>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Checkbox
|
||||
},
|
||||
props: [
|
||||
'shadow',
|
||||
'shadowControl',
|
||||
'previewClass',
|
||||
'previewStyle',
|
||||
'disabled'
|
||||
],
|
||||
emits: ['update:shadow'],
|
||||
data () {
|
||||
return {
|
||||
lightGrid: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateProperty (axis, value) {
|
||||
this.$emit('update:shadow', { axis, value })
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.ComponentPreview {
|
||||
display: grid;
|
||||
|
@ -163,40 +195,20 @@
|
|||
}
|
||||
|
||||
.preview-block {
|
||||
width: 33%;
|
||||
height: 33%;
|
||||
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);
|
||||
background: var(--background);
|
||||
box-shadow: var(--shadow);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
}
|
||||
}
|
||||
</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>
|
||||
|
|
|
@ -6,7 +6,7 @@ export default {
|
|||
{
|
||||
component: 'Icon',
|
||||
directives: {
|
||||
textColor: '$blend(--stack, 0.5, --parent--text)',
|
||||
textColor: '$blend(--stack 0.5 --parent--text)',
|
||||
textAuto: 'no-auto'
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,3 @@
|
|||
const hoverGlow = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
blur: 4,
|
||||
spread: 0,
|
||||
color: '--text',
|
||||
alpha: 1
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'Input',
|
||||
selector: '.input',
|
||||
|
@ -27,7 +18,9 @@ export default {
|
|||
{
|
||||
component: 'Root',
|
||||
directives: {
|
||||
'--defaultInputBevel': 'shadow | $borderSide(#FFFFFF, bottom, 0.2), $borderSide(#000000, top, 0.2)'
|
||||
'--defaultInputBevel': 'shadow | $borderSide(#FFFFFF bottom 0.2), $borderSide(#000000 top 0.2)',
|
||||
'--defaultInputHoverGlow': 'shadow | 0 0 4 --text / 0.5',
|
||||
'--defaultInputFocusGlow': 'shadow | 0 0 4 4 --link / 0.5'
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -54,7 +47,19 @@ export default {
|
|||
{
|
||||
state: ['hover'],
|
||||
directives: {
|
||||
shadow: [hoverGlow, '--defaultInputBevel']
|
||||
shadow: ['--defaultInputHoverGlow', '--defaultInputBevel']
|
||||
}
|
||||
},
|
||||
{
|
||||
state: ['focused'],
|
||||
directives: {
|
||||
shadow: ['--defaultInputFocusGlow', '--defaultInputBevel']
|
||||
}
|
||||
},
|
||||
{
|
||||
state: ['focused', 'hover'],
|
||||
directives: {
|
||||
shadow: ['--defaultInputFocusGlow', '--defaultInputHoverGlow', '--defaultInputBevel']
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
@ -24,21 +24,21 @@ export default {
|
|||
{
|
||||
state: ['hover'],
|
||||
directives: {
|
||||
background: '$mod(--bg, 5)',
|
||||
background: '$mod(--bg 5)',
|
||||
opacity: 1
|
||||
}
|
||||
},
|
||||
{
|
||||
state: ['active'],
|
||||
directives: {
|
||||
background: '$mod(--bg, 10)',
|
||||
background: '$mod(--bg 10)',
|
||||
opacity: 1
|
||||
}
|
||||
},
|
||||
{
|
||||
state: ['active', 'hover'],
|
||||
directives: {
|
||||
background: '$mod(--bg, 15)',
|
||||
background: '$mod(--bg 15)',
|
||||
opacity: 1
|
||||
}
|
||||
},
|
||||
|
|
|
@ -2,6 +2,7 @@ export default {
|
|||
name: 'Modals',
|
||||
selector: '.modal-view',
|
||||
lazy: true,
|
||||
notEditable: true,
|
||||
validInnerComponents: [
|
||||
'Panel'
|
||||
],
|
||||
|
|
123
src/components/palette_editor/palette_editor.vue
Normal file
123
src/components/palette_editor/palette_editor.vue
Normal file
|
@ -0,0 +1,123 @@
|
|||
<template>
|
||||
<div class="PaletteEditor">
|
||||
<div class="colors">
|
||||
<ColorInput
|
||||
v-for="key in paletteKeys"
|
||||
:key="key"
|
||||
:model-value="props.modelValue[key]"
|
||||
:fallback="fallback(key)"
|
||||
:label="$t('settings.style.themes3.palette.' + key)"
|
||||
@update:modelValue="value => updatePalette(key, value)"
|
||||
/>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<button
|
||||
class="btn button-default"
|
||||
@click="importPalette"
|
||||
>
|
||||
<FAIcon icon="file-import" />
|
||||
{{ $t('settings.style.themes3.palette.import') }}
|
||||
</button>
|
||||
<button
|
||||
class="btn button-default"
|
||||
@click="exportPalette"
|
||||
>
|
||||
<FAIcon icon="file-export" />
|
||||
{{ $t('settings.style.themes3.palette.export') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import ColorInput from 'src/components/color_input/color_input.vue'
|
||||
import {
|
||||
// newImporter,
|
||||
newExporter
|
||||
} from 'src/services/export_import/export_import.js'
|
||||
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faFileImport,
|
||||
faFileExport
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(
|
||||
faFileImport,
|
||||
faFileExport
|
||||
)
|
||||
|
||||
const props = defineProps(['modelValue'])
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
const paletteExporter = newExporter({
|
||||
filename: 'pleroma.palette.json',
|
||||
getExportedObject: () => props.modelValue
|
||||
})
|
||||
/*
|
||||
const themeImporter = newImporter({
|
||||
validator: importValidator,
|
||||
onImport,
|
||||
onImportFailure,
|
||||
})
|
||||
*/
|
||||
|
||||
const exportPalette = () => {
|
||||
paletteExporter.exportData()
|
||||
}
|
||||
|
||||
const importPalette = () => {
|
||||
// TODO
|
||||
}
|
||||
|
||||
const paletteKeys = [
|
||||
'bg',
|
||||
'fg',
|
||||
'text',
|
||||
'link',
|
||||
'accent',
|
||||
'cRed',
|
||||
'cBlue',
|
||||
'cGreen',
|
||||
'cOrange',
|
||||
'extra1',
|
||||
'extra2',
|
||||
'extra3'
|
||||
]
|
||||
|
||||
const fallback = (key) => {
|
||||
if (key === 'accent') {
|
||||
return props.modelValue.link
|
||||
}
|
||||
if (key === 'link') {
|
||||
return props.modelValue.accent
|
||||
}
|
||||
if (key.startsWith('extra')) {
|
||||
return '#008080'
|
||||
}
|
||||
}
|
||||
|
||||
const updatePalette = (paletteKey, value) => {
|
||||
emit('update:modelValue', {
|
||||
...props.modelValue,
|
||||
[paletteKey]: value
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.PaletteEditor {
|
||||
.colors {
|
||||
display: grid;
|
||||
justify-content: space-around;
|
||||
grid-template-columns: repeat(4, min-content);
|
||||
grid-template-rows: repeat(auto-fit, min-content);
|
||||
grid-gap: 0.5em;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-gap: 0.5em;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,6 +1,7 @@
|
|||
export default {
|
||||
name: 'Root',
|
||||
selector: ':root',
|
||||
notEditable: true,
|
||||
validInnerComponents: [
|
||||
'Underlay',
|
||||
'Modals',
|
||||
|
@ -42,7 +43,7 @@ export default {
|
|||
|
||||
// Selection colors
|
||||
'--selectionBackground': 'color | --accent',
|
||||
'--selectionText': 'color | $textColor(--accent, --text, no-preserve)'
|
||||
'--selectionText': 'color | $textColor(--accent --text no-preserve)'
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
export default {
|
||||
name: 'Scrollbar',
|
||||
selector: '::-webkit-scrollbar',
|
||||
notEditable: true, // for now
|
||||
defaultRules: [
|
||||
{
|
||||
directives: {
|
||||
|
|
|
@ -31,6 +31,7 @@ const hoverGlow = {
|
|||
export default {
|
||||
name: 'ScrollbarElement',
|
||||
selector: '::-webkit-scrollbar-button',
|
||||
notEditable: true, // for now
|
||||
states: {
|
||||
pressed: ':active',
|
||||
hover: ':hover:not(:disabled)',
|
||||
|
@ -82,7 +83,7 @@ export default {
|
|||
{
|
||||
state: ['disabled'],
|
||||
directives: {
|
||||
background: '$blend(--inheritedBackground, 0.25, --parent)',
|
||||
background: '$blend(--inheritedBackground 0.25 --parent)',
|
||||
shadow: [...buttonInsetFakeBorders]
|
||||
}
|
||||
},
|
||||
|
|
|
@ -61,12 +61,13 @@ label.Select {
|
|||
&:disabled {
|
||||
background-color: var(--background);
|
||||
opacity: 1; /* override browser */
|
||||
color: var(--faint);
|
||||
|
||||
select {
|
||||
&[multiple],
|
||||
&[size] {
|
||||
option.-active {
|
||||
color: var(--text);
|
||||
color: var(--faint);
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,9 +10,13 @@ export default {
|
|||
ProfileSettingIndicator
|
||||
},
|
||||
props: {
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
path: {
|
||||
type: [String, Array],
|
||||
required: true
|
||||
required: false
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
|
@ -68,7 +72,7 @@ export default {
|
|||
}
|
||||
},
|
||||
created () {
|
||||
if (this.realDraftMode && this.realSource !== 'admin') {
|
||||
if (this.realDraftMode && (this.realSource !== 'admin' || this.path == null)) {
|
||||
this.draft = this.state
|
||||
}
|
||||
},
|
||||
|
@ -76,14 +80,14 @@ export default {
|
|||
draft: {
|
||||
// TODO allow passing shared draft object?
|
||||
get () {
|
||||
if (this.realSource === 'admin') {
|
||||
if (this.realSource === 'admin' || this.path == null) {
|
||||
return get(this.$store.state.adminSettings.draft, this.canonPath)
|
||||
} else {
|
||||
return this.localDraft
|
||||
}
|
||||
},
|
||||
set (value) {
|
||||
if (this.realSource === 'admin') {
|
||||
if (this.realSource === 'admin' || this.path == null) {
|
||||
this.$store.commit('updateAdminDraft', { path: this.canonPath, value })
|
||||
} else {
|
||||
this.localDraft = value
|
||||
|
@ -91,6 +95,9 @@ export default {
|
|||
}
|
||||
},
|
||||
state () {
|
||||
if (this.path == null) {
|
||||
return this.modelValue
|
||||
}
|
||||
const value = get(this.configSource, this.canonPath)
|
||||
if (value === undefined) {
|
||||
return this.defaultState
|
||||
|
@ -145,6 +152,9 @@ export default {
|
|||
return this.backendDescription?.suggestions
|
||||
},
|
||||
shouldBeDisabled () {
|
||||
if (this.path == null) {
|
||||
return this.disabled
|
||||
}
|
||||
const parentValue = this.parentPath !== undefined ? get(this.configSource, this.parentPath) : null
|
||||
return this.disabled || (parentValue !== null ? (this.parentInvert ? parentValue : !parentValue) : false)
|
||||
},
|
||||
|
@ -159,6 +169,9 @@ export default {
|
|||
}
|
||||
},
|
||||
configSink () {
|
||||
if (this.path == null) {
|
||||
return (k, v) => this.$emit('modelValue:update', v)
|
||||
}
|
||||
switch (this.realSource) {
|
||||
case 'profile':
|
||||
return (k, v) => this.$store.dispatch('setProfileOption', { name: k, value: v })
|
||||
|
@ -184,6 +197,7 @@ export default {
|
|||
return this.realSource === 'profile'
|
||||
},
|
||||
isChanged () {
|
||||
if (this.path == null) return false
|
||||
switch (this.realSource) {
|
||||
case 'profile':
|
||||
case 'admin':
|
||||
|
@ -193,9 +207,11 @@ export default {
|
|||
}
|
||||
},
|
||||
canonPath () {
|
||||
if (this.path == null) return null
|
||||
return Array.isArray(this.path) ? this.path : this.path.split('.')
|
||||
},
|
||||
isDirty () {
|
||||
if (this.path == null) return false
|
||||
if (this.realSource === 'admin' && this.canonPath.length > 3) {
|
||||
return false // should not show draft buttons for "grouped" values
|
||||
} else {
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
</template>
|
||||
<slot v-else />
|
||||
</label>
|
||||
{{ ' ' }}
|
||||
<input
|
||||
:id="path"
|
||||
class="input string-input"
|
||||
|
|
|
@ -10,6 +10,10 @@
|
|||
list-style-type: none;
|
||||
padding-left: 2em;
|
||||
|
||||
.btn:not(.dropdown-button) {
|
||||
padding: 0 2em;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
@ -54,10 +58,6 @@
|
|||
.btn {
|
||||
min-height: 2em;
|
||||
}
|
||||
|
||||
.btn:not(.dropdown-button) {
|
||||
padding: 0 2em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ import GeneralTab from './tabs/general_tab.vue'
|
|||
import AppearanceTab from './tabs/appearance_tab.vue'
|
||||
import VersionTab from './tabs/version_tab.vue'
|
||||
import ThemeTab from './tabs/theme_tab/theme_tab.vue'
|
||||
import StyleTab from './tabs/style_tab/style_tab.vue'
|
||||
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
|
@ -17,6 +18,7 @@ import {
|
|||
faUser,
|
||||
faFilter,
|
||||
faPaintBrush,
|
||||
faPalette,
|
||||
faBell,
|
||||
faDownload,
|
||||
faEyeSlash,
|
||||
|
@ -29,6 +31,7 @@ library.add(
|
|||
faUser,
|
||||
faFilter,
|
||||
faPaintBrush,
|
||||
faPalette,
|
||||
faBell,
|
||||
faDownload,
|
||||
faEyeSlash,
|
||||
|
@ -48,6 +51,7 @@ const SettingsModalContent = {
|
|||
ProfileTab,
|
||||
GeneralTab,
|
||||
AppearanceTab,
|
||||
StyleTab,
|
||||
VersionTab,
|
||||
ThemeTab
|
||||
},
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
|
||||
> div,
|
||||
> label {
|
||||
display: block;
|
||||
margin-bottom: 0.5em;
|
||||
|
||||
&:last-child {
|
||||
|
|
|
@ -20,6 +20,13 @@
|
|||
>
|
||||
<AppearanceTab />
|
||||
</div>
|
||||
<div
|
||||
:label="$t('settings.style.themes3.editor.title')"
|
||||
icon="palette"
|
||||
data-tab-name="style"
|
||||
>
|
||||
<StyleTab />
|
||||
</div>
|
||||
<div
|
||||
:label="$t('settings.theme')"
|
||||
icon="paint-brush"
|
||||
|
|
|
@ -8,9 +8,7 @@ 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 { newImporter } from 'src/services/export_import/export_import.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 {
|
||||
|
@ -35,6 +33,23 @@ const AppearanceTab = {
|
|||
data () {
|
||||
return {
|
||||
availableStyles: [],
|
||||
availablePalettes: [],
|
||||
themeImporter: newImporter({
|
||||
accept: '.json, .piss',
|
||||
validator: this.importValidator,
|
||||
onImport: this.onImport,
|
||||
onImportFailure: this.onImportFailure
|
||||
}),
|
||||
palettesKeys: [
|
||||
'background',
|
||||
'foreground',
|
||||
'link',
|
||||
'text',
|
||||
'cRed',
|
||||
'cGreen',
|
||||
'cBlue',
|
||||
'cOrange'
|
||||
],
|
||||
intersectionObserver: null,
|
||||
thirdColumnModeOptions: ['none', 'notifications', 'postform'].map(mode => ({
|
||||
key: mode,
|
||||
|
@ -64,29 +79,49 @@ const AppearanceTab = {
|
|||
Preview
|
||||
},
|
||||
mounted () {
|
||||
getThemes()
|
||||
.then((promises) => {
|
||||
return Promise.all(
|
||||
Object.entries(promises)
|
||||
.map(([k, v]) => v.then(res => [k, res]))
|
||||
)
|
||||
})
|
||||
.then(themes => themes.reduce((acc, [k, v]) => {
|
||||
if (v) {
|
||||
return [
|
||||
...acc,
|
||||
{
|
||||
name: v.name || v[0],
|
||||
key: k,
|
||||
data: v
|
||||
}
|
||||
]
|
||||
const updateIndex = (resource) => {
|
||||
const capitalizedResource = resource[0].toUpperCase() + resource.slice(1)
|
||||
const currentIndex = this.$store.state.instance[`${resource}sIndex`]
|
||||
|
||||
let promise
|
||||
if (currentIndex) {
|
||||
promise = Promise.resolve(currentIndex)
|
||||
} else {
|
||||
return acc
|
||||
promise = this.$store.dispatch(`fetch${capitalizedResource}sIndex`)
|
||||
}
|
||||
}, []))
|
||||
.then((themesComplete) => {
|
||||
this.availableStyles = themesComplete
|
||||
|
||||
return promise.then(index => {
|
||||
return Object
|
||||
.entries(index)
|
||||
.map(([k, func]) => [k, func()])
|
||||
})
|
||||
}
|
||||
|
||||
updateIndex('theme').then(themes => {
|
||||
themes.forEach(([key, themePromise]) => themePromise.then(data => {
|
||||
this.availableStyles.push({ key, data, name: data.name, version: 'v2' })
|
||||
}))
|
||||
})
|
||||
|
||||
updateIndex('palette').then(palettes => {
|
||||
palettes.forEach(([key, palettePromise]) => palettePromise.then(v => {
|
||||
if (Array.isArray(v)) {
|
||||
const [
|
||||
name,
|
||||
background,
|
||||
foreground,
|
||||
text,
|
||||
link,
|
||||
cRed = '#FF0000',
|
||||
cGreen = '#00FF00',
|
||||
cBlue = '#0000FF',
|
||||
cOrange = '#E3FF00'
|
||||
] = v
|
||||
this.availablePalettes.push({ key, name, background, foreground, text, link, cRed, cBlue, cGreen, cOrange })
|
||||
} else {
|
||||
this.availablePalettes.push({ key, ...v })
|
||||
}
|
||||
}))
|
||||
})
|
||||
|
||||
if (window.IntersectionObserver) {
|
||||
|
@ -146,13 +181,16 @@ const AppearanceTab = {
|
|||
},
|
||||
isCustomThemeUsed () {
|
||||
const { theme } = this.mergedConfig
|
||||
return theme === 'custom' || theme === null
|
||||
return theme === 'custom'
|
||||
},
|
||||
isCustomStyleUsed (name) {
|
||||
const { style } = this.mergedConfig
|
||||
return style === 'custom'
|
||||
},
|
||||
...SharedComputedObject()
|
||||
},
|
||||
methods: {
|
||||
updateFont (key, value) {
|
||||
console.log(key, value)
|
||||
this.$store.dispatch('setOption', {
|
||||
name: 'theme3hacks',
|
||||
value: {
|
||||
|
@ -164,25 +202,76 @@ const AppearanceTab = {
|
|||
}
|
||||
})
|
||||
},
|
||||
importTheme () {
|
||||
this.themeImporter.importData()
|
||||
},
|
||||
onImport (parsed, filename) {
|
||||
if (filename.endsWith('.json')) {
|
||||
this.$store.dispatch('setThemeCustom', parsed.source || parsed.theme)
|
||||
this.$store.dispatch('applyTheme')
|
||||
}
|
||||
|
||||
// this.loadTheme(parsed, 'file', forceSource)
|
||||
},
|
||||
onImportFailure (result) {
|
||||
this.$store.dispatch('pushGlobalNotice', { messageKey: 'settings.invalid_theme_imported', level: 'error' })
|
||||
},
|
||||
importValidator (parsed, filename) {
|
||||
if (filename.endsWith('.json')) {
|
||||
const version = parsed._pleroma_theme_version
|
||||
return version >= 1 || version <= 2
|
||||
}
|
||||
},
|
||||
isThemeActive (key) {
|
||||
const { theme } = this.mergedConfig
|
||||
return key === theme
|
||||
},
|
||||
isStyleActive (key) {
|
||||
const { style } = this.mergedConfig
|
||||
return key === style
|
||||
},
|
||||
isPaletteActive (key) {
|
||||
const { palette } = this.mergedConfig
|
||||
return key === palette
|
||||
},
|
||||
importStyle () {
|
||||
|
||||
},
|
||||
setTheme (name) {
|
||||
this.$store.dispatch('setTheme', { themeName: name, saveData: true, recompile: true })
|
||||
this.$store.dispatch('setTheme', name)
|
||||
this.$store.dispatch('applyTheme')
|
||||
},
|
||||
setPalette (name) {
|
||||
this.$store.dispatch('setPalette', name)
|
||||
this.$store.dispatch('applyTheme')
|
||||
},
|
||||
resetTheming (name) {
|
||||
this.$store.dispatch('resetThemeV2')
|
||||
this.$store.dispatch('resetThemeV3')
|
||||
this.$store.dispatch('setStyle', 'stock')
|
||||
this.$store.dispatch('applyTheme')
|
||||
},
|
||||
previewTheme (key, input) {
|
||||
let theme3
|
||||
if (input) {
|
||||
const style = normalizeThemeData(input)
|
||||
const x = 2
|
||||
if (x === 1) return
|
||||
const theme2 = convertTheme2To3(style)
|
||||
const theme3 = init({
|
||||
theme3 = init({
|
||||
inputRuleset: theme2,
|
||||
ultimateBackgroundColor: '#000000',
|
||||
liteMode: true,
|
||||
debug: true,
|
||||
onlyNormalState: true
|
||||
})
|
||||
} else {
|
||||
theme3 = init({
|
||||
inputRuleset: [],
|
||||
ultimateBackgroundColor: '#000000',
|
||||
liteMode: true,
|
||||
debug: true,
|
||||
onlyNormalState: true
|
||||
})
|
||||
}
|
||||
|
||||
return getScopedVersion(
|
||||
getCssRules(theme3.eager),
|
||||
|
|
91
src/components/settings_modal/tabs/appearance_tab.scss
Normal file
91
src/components/settings_modal/tabs/appearance_tab.scss
Normal file
|
@ -0,0 +1,91 @@
|
|||
.appearance-tab {
|
||||
.palette,
|
||||
.theme-notice {
|
||||
padding: 0.5em;
|
||||
margin: 1em;
|
||||
}
|
||||
|
||||
.setting-item {
|
||||
padding-bottom: 0;
|
||||
|
||||
&.heading {
|
||||
display: grid;
|
||||
align-items: baseline;
|
||||
grid-template-columns: 1fr auto auto auto;
|
||||
grid-gap: 0.5em;
|
||||
|
||||
h2 {
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.palettes {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-gap: 0.5em;
|
||||
}
|
||||
|
||||
.palette-entry {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
> label {
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
|
||||
.palette-square {
|
||||
flex: 0 0 auto;
|
||||
display: inline-block;
|
||||
min-width: 1em;
|
||||
min-height: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
.column-settings {
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.column-settings .size-label {
|
||||
display: block;
|
||||
margin-bottom: 0.5em;
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
.theme-list {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin: -0.5em 0;
|
||||
height: 25em;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
scrollbar-gutter: stable;
|
||||
border-radius: var(--roundness);
|
||||
border: 1px solid var(--border);
|
||||
padding: 0;
|
||||
|
||||
.theme-preview {
|
||||
font-size: 1rem; // fix for firefox
|
||||
width: 19rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin: 0.5em;
|
||||
|
||||
&.placeholder {
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
.theme-preview-container {
|
||||
pointer-events: none;
|
||||
zoom: 0.5;
|
||||
border: none;
|
||||
border-radius: var(--roundness);
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,23 +1,56 @@
|
|||
<template>
|
||||
<div class="appearance-tab" :label="$t('settings.general')">
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.theme') }}</h2>
|
||||
<ul
|
||||
class="theme-list"
|
||||
ref="themeList"
|
||||
<div
|
||||
class="appearance-tab"
|
||||
:label="$t('settings.general')"
|
||||
>
|
||||
<div class="setting-item heading">
|
||||
<h2>{{ $t('settings.theme') }}</h2>
|
||||
<button
|
||||
class="btn button-default"
|
||||
@click="importTheme"
|
||||
>
|
||||
<FAIcon icon="folder-open" />
|
||||
{{ $t('settings.style.themes3.editor.load_style') }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<ul
|
||||
ref="themeList"
|
||||
class="theme-list"
|
||||
>
|
||||
<button
|
||||
class="button-default theme-preview"
|
||||
data-theme-key="stock"
|
||||
:class="{ toggled: isStyleActive('stock') }"
|
||||
@click="resetTheming"
|
||||
>
|
||||
<!-- eslint-disable vue/no-v-text-v-html-on-component -->
|
||||
<component
|
||||
:is="'style'"
|
||||
v-html="previewTheme('stock')"
|
||||
/>
|
||||
<!-- eslint-enable vue/no-v-text-v-html-on-component -->
|
||||
<preview />
|
||||
<h4 class="theme-name">
|
||||
{{ $t('settings.style.stock_theme_used') }}
|
||||
<span class="alert neutral version">v3</span>
|
||||
</h4>
|
||||
</button>
|
||||
<button
|
||||
v-if="isCustomThemeUsed"
|
||||
disabled
|
||||
class="button-default theme-preview"
|
||||
>
|
||||
<preview />
|
||||
<h4 class="theme-name">{{ $t('settings.style.custom_theme_used') }}</h4>
|
||||
<h4 class="theme-name">
|
||||
{{ $t('settings.style.custom_theme_used') }}
|
||||
<span class="alert neutral version">v2</span>
|
||||
</h4>
|
||||
</button>
|
||||
<button
|
||||
v-for="style in availableStyles"
|
||||
:data-theme-key="style.key"
|
||||
:key="style.key"
|
||||
:data-theme-key="style.key"
|
||||
class="button-default theme-preview"
|
||||
:class="{ toggled: isThemeActive(style.key) }"
|
||||
@click="setTheme(style.key)"
|
||||
|
@ -29,10 +62,33 @@
|
|||
v-html="previewTheme(style.key, style.data)"
|
||||
/>
|
||||
<!-- eslint-enable vue/no-v-text-v-html-on-component -->
|
||||
<preview :class="{ placeholder: ready }" :id="'theme-preview-' + style.key"/>
|
||||
<h4 class="theme-name">{{ style.name }}</h4>
|
||||
<preview :id="'theme-preview-' + style.key" />
|
||||
<h4 class="theme-name">
|
||||
{{ style.name }}
|
||||
<span class="alert neutral version">{{ style.version }}</span>
|
||||
</h4>
|
||||
</button>
|
||||
</ul>
|
||||
<h3>{{ $t('settings.style.themes3.palette.label') }}</h3>
|
||||
<div class="palettes">
|
||||
<button
|
||||
v-for="p in availablePalettes"
|
||||
:key="p.name"
|
||||
class="btn button-default palette-entry"
|
||||
:class="{ toggled: isPaletteActive(p.key) }"
|
||||
@click="() => setPalette(p.key)"
|
||||
>
|
||||
<label>
|
||||
{{ p.name }}
|
||||
</label>
|
||||
<span
|
||||
v-for="c in palettesKeys"
|
||||
:key="c"
|
||||
class="palette-square"
|
||||
:style="{ backgroundColor: p[c], border: '1px solid ' + (p[c] ?? 'var(--text)') }"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="alert neutral theme-notice">
|
||||
{{ $t("settings.style.appearance_tab_note") }}
|
||||
|
@ -60,7 +116,7 @@
|
|||
<code>px</code>
|
||||
<code>rem</code>
|
||||
</i18n-t>
|
||||
<br/>
|
||||
<br>
|
||||
<i18n-t
|
||||
scope="global"
|
||||
keypath="settings.text_size_tip2"
|
||||
|
@ -256,58 +312,4 @@
|
|||
|
||||
<script src="./appearance_tab.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
.appearance-tab {
|
||||
.theme-notice {
|
||||
padding: 0.5em;
|
||||
margin: 1em;
|
||||
}
|
||||
|
||||
.column-settings {
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.column-settings .size-label {
|
||||
display: block;
|
||||
margin-bottom: 0.5em;
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
.theme-list {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin: -0.5em 0;
|
||||
height: 25em;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
scrollbar-gutter: stable;
|
||||
border-radius: var(--roundness);
|
||||
border: 1px solid var(--border);
|
||||
padding: 0;
|
||||
|
||||
.theme-preview {
|
||||
font-size: 1rem; // fix for firefox
|
||||
width: 19rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin: 0.5em;
|
||||
|
||||
&.placeholder {
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
.theme-preview-container {
|
||||
pointer-events: none;
|
||||
zoom: 0.5;
|
||||
border: none;
|
||||
border-radius: var(--roundness);
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style lang="scss" src="./appearance_tab.scss"></style>
|
||||
|
|
550
src/components/settings_modal/tabs/style_tab/style_tab.js
Normal file
550
src/components/settings_modal/tabs/style_tab/style_tab.js
Normal file
|
@ -0,0 +1,550 @@
|
|||
import { ref, reactive, computed, watch } from 'vue'
|
||||
import { get, set } from 'lodash'
|
||||
|
||||
import Select from 'src/components/select/select.vue'
|
||||
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||
import ComponentPreview from 'src/components/component_preview/component_preview.vue'
|
||||
import StringSetting from '../../helpers/string_setting.vue'
|
||||
import ShadowControl from 'src/components/shadow_control/shadow_control.vue'
|
||||
import ColorInput from 'src/components/color_input/color_input.vue'
|
||||
import PaletteEditor from 'src/components/palette_editor/palette_editor.vue'
|
||||
import OpacityInput from 'src/components/opacity_input/opacity_input.vue'
|
||||
import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'
|
||||
import Tooltip from 'src/components/tooltip/tooltip.vue'
|
||||
import ContrastRatio from 'src/components/contrast_ratio/contrast_ratio.vue'
|
||||
|
||||
import { init } from 'src/services/theme_data/theme_data_3.service.js'
|
||||
import { getCssRules } from 'src/services/theme_data/css_utils.js'
|
||||
import { serialize } from 'src/services/theme_data/iss_serializer.js'
|
||||
import { deserialize } from 'src/services/theme_data/iss_deserializer.js'
|
||||
import {
|
||||
// rgb2hex,
|
||||
hex2rgb,
|
||||
getContrastRatio
|
||||
} from 'src/services/color_convert/color_convert.js'
|
||||
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { faFloppyDisk, faFolderOpen, faFile } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
// helper for debugging
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const toValue = (x) => JSON.parse(JSON.stringify(x === undefined ? 'null' : x))
|
||||
|
||||
// helper to make states comparable
|
||||
const normalizeStates = (states) => ['normal', ...(states?.filter(x => x !== 'normal') || [])].join(':')
|
||||
|
||||
library.add(
|
||||
faFile,
|
||||
faFloppyDisk,
|
||||
faFolderOpen
|
||||
)
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Select,
|
||||
Checkbox,
|
||||
Tooltip,
|
||||
StringSetting,
|
||||
ComponentPreview,
|
||||
TabSwitcher,
|
||||
ShadowControl,
|
||||
ColorInput,
|
||||
PaletteEditor,
|
||||
OpacityInput,
|
||||
ContrastRatio
|
||||
},
|
||||
setup () {
|
||||
// ### Meta stuff
|
||||
const name = ref('')
|
||||
const author = ref('')
|
||||
const license = ref('')
|
||||
const website = ref('')
|
||||
|
||||
const metaOut = computed(() => {
|
||||
return `@meta {
|
||||
name: ${name.value};
|
||||
author: ${author.value};
|
||||
license: ${license.value};
|
||||
website: ${website.value};
|
||||
}`
|
||||
})
|
||||
|
||||
// ### Palette stuff
|
||||
const palettes = reactive({
|
||||
light: {
|
||||
bg: '#f2f6f9',
|
||||
fg: '#d6dfed',
|
||||
text: '#304055',
|
||||
underlay: '#5d6086',
|
||||
accent: '#f55b1b',
|
||||
cBlue: '#0095ff',
|
||||
cRed: '#d31014',
|
||||
cGreen: '#0fa00f',
|
||||
cOrange: '#ffa500',
|
||||
border: '#d8e6f9'
|
||||
},
|
||||
dark: {
|
||||
bg: '#121a24',
|
||||
fg: '#182230',
|
||||
text: '#b9b9ba',
|
||||
link: '#d8a070',
|
||||
accent: '#d8a070',
|
||||
cRed: '#FF0000',
|
||||
cBlue: '#0095ff',
|
||||
cGreen: '#0fa00f',
|
||||
cOrange: '#ffa500'
|
||||
}
|
||||
})
|
||||
|
||||
const palettesOut = computed(() => {
|
||||
return Object.entries(palettes).map(([name, palette]) => {
|
||||
const entries = Object
|
||||
.entries(palette)
|
||||
.map(([slot, data]) => ` ${slot}: ${data};`)
|
||||
.join('\n')
|
||||
|
||||
return `@palette.${name} {\n${entries}\n}`
|
||||
}).join('\n\n')
|
||||
})
|
||||
|
||||
const editedPalette = ref('dark')
|
||||
const palette = computed({
|
||||
get () {
|
||||
return palettes[editedPalette.value]
|
||||
},
|
||||
set (newPalette) {
|
||||
palettes[editedPalette.value] = newPalette
|
||||
}
|
||||
})
|
||||
|
||||
// ### I18n stuff
|
||||
// The paths in i18n are getting ridicously long, this effectively shortens them
|
||||
const getI18nPath = (componentName) => `settings.style.themes3.editor.components.${componentName}`
|
||||
// vue i18n doesn't seem to have (working) mechanic to have a fallback so we have to
|
||||
// make do ourselves
|
||||
const fallbackI18n = (translated, fallback) => {
|
||||
if (translated.startsWith('settings.style.themes3')) {
|
||||
return fallback
|
||||
}
|
||||
return translated
|
||||
}
|
||||
const getFriendlyNamePath = (componentName) => getI18nPath(componentName) + '.friendlyName'
|
||||
const getVariantPath = (componentName, variant) => {
|
||||
return variant === 'normal'
|
||||
? 'settings.style.themes3.editor.components.normal.variant'
|
||||
: `${getI18nPath(componentName)}.variants.${variant}`
|
||||
}
|
||||
const getStatePath = (componentName, state) => {
|
||||
return state === 'normal'
|
||||
? 'settings.style.themes3.editor.components.normal.state'
|
||||
: `${getI18nPath(componentName)}.states.${state}`
|
||||
}
|
||||
|
||||
// ### Initialization stuff
|
||||
// Getting existing components
|
||||
const componentsContext = require.context('src', true, /\.style.js(on)?$/)
|
||||
const componentKeysAll = componentsContext.keys()
|
||||
|
||||
const componentsMap = new Map(
|
||||
componentKeysAll
|
||||
.map(
|
||||
key => [key, componentsContext(key).default]
|
||||
).filter(([key, component]) => !component.virtual && !component.notEditable)
|
||||
)
|
||||
const componentKeys = [...componentsMap.keys()]
|
||||
|
||||
// Initializing selected component and its computed descendants
|
||||
const selectedComponentKey = ref(componentsMap.keys().next().value)
|
||||
const selectedComponent = computed(() => componentsMap.get(selectedComponentKey.value))
|
||||
const selectedComponentName = computed(() => selectedComponent.value.name)
|
||||
|
||||
const selectedVariant = ref('normal')
|
||||
const selectedComponentVariantsAll = computed(() => {
|
||||
return Object.keys({ normal: null, ...(selectedComponent.value.variants || {}) })
|
||||
})
|
||||
|
||||
const selectedState = reactive(new Set())
|
||||
const selectedComponentStatesAll = computed(() => {
|
||||
return Object.keys({ normal: null, ...(selectedComponent.value.states || {}) })
|
||||
})
|
||||
const selectedComponentStates = computed(() => {
|
||||
return selectedComponentStatesAll.value.filter(x => x !== 'normal')
|
||||
})
|
||||
const updateSelectedStates = (state, v) => {
|
||||
if (v) {
|
||||
selectedState.add(state)
|
||||
} else {
|
||||
selectedState.delete(state)
|
||||
}
|
||||
}
|
||||
|
||||
// ### Preview stuff
|
||||
const editorHintStyle = computed(() => {
|
||||
const editorHint = selectedComponent.value.editor
|
||||
const styles = []
|
||||
if (editorHint && Object.keys(editorHint).length > 0) {
|
||||
if (editorHint.aspect != null) {
|
||||
styles.push(`aspect-ratio: ${editorHint.aspect} !important;`)
|
||||
}
|
||||
if (editorHint.border != null) {
|
||||
styles.push(`border-width: ${editorHint.border}px !important;`)
|
||||
}
|
||||
}
|
||||
return styles.join('; ')
|
||||
})
|
||||
|
||||
// Apart from "hover" we can't really show how component looks like in
|
||||
// certain states, so we have to fake them.
|
||||
const simulatePseudoSelectors = css => css
|
||||
.replace(selectedComponent.value.selector, '.ComponentPreview .preview-block')
|
||||
.replace(':active', '.preview-active')
|
||||
.replace(':hover', '.preview-hover')
|
||||
.replace(':active', '.preview-active')
|
||||
.replace(':focus', '.preview-focus')
|
||||
.replace(':focus-within', '.preview-focus-within')
|
||||
.replace(':disabled', '.preview-disabled')
|
||||
|
||||
const previewRules = reactive([])
|
||||
const previewClass = computed(() => {
|
||||
const selectors = []
|
||||
if (!!selectedComponent.value.variants?.normal || selectedVariant.value !== 'normal') {
|
||||
selectors.push(selectedComponent.value.variants[selectedVariant.value])
|
||||
}
|
||||
if (selectedState.size > 0) {
|
||||
selectedState.forEach(state => {
|
||||
const original = selectedComponent.value.states[state]
|
||||
selectors.push(simulatePseudoSelectors(original))
|
||||
})
|
||||
}
|
||||
return selectors.map(x => x.substring(1)).join('')
|
||||
})
|
||||
const previewCss = computed(() => {
|
||||
const scoped = getCssRules(previewRules)
|
||||
.map(simulatePseudoSelectors)
|
||||
return scoped.join('\n')
|
||||
})
|
||||
|
||||
// ### Rules stuff aka meat and potatoes
|
||||
// The native structure of separate rules and the child -> parent
|
||||
// relation isn't very convenient for editor, we replace the array
|
||||
// and child -> parent structure with map and parent -> child structure
|
||||
const editorFriendlyFallbackStructure = computed(() => {
|
||||
const root = {}
|
||||
|
||||
componentKeys.forEach((componentKey) => {
|
||||
const componentValue = componentsMap.get(componentKey)
|
||||
const { defaultRules } = componentValue
|
||||
defaultRules.forEach((rule) => {
|
||||
const { parent: rParent } = rule
|
||||
const parent = rParent ?? rule
|
||||
const hasChildren = !!rParent
|
||||
const child = hasChildren ? rule : null
|
||||
|
||||
const {
|
||||
component: pComponent,
|
||||
variant: pVariant = 'normal',
|
||||
state: pState = [] // no relation to Intel CPUs whatsoever
|
||||
} = parent
|
||||
|
||||
const pPath = `${hasChildren ? pComponent : componentValue.name}.${pVariant}.${normalizeStates(pState)}`
|
||||
|
||||
let output = get(root, pPath)
|
||||
if (!output) {
|
||||
set(root, pPath, {})
|
||||
output = get(root, pPath)
|
||||
}
|
||||
|
||||
if (hasChildren) {
|
||||
output._children = output._children ?? {}
|
||||
const {
|
||||
component: cComponent,
|
||||
variant: cVariant = 'normal',
|
||||
state: cState = [],
|
||||
directives
|
||||
} = child
|
||||
|
||||
const cPath = `${cComponent}.${cVariant}.${normalizeStates(cState)}`
|
||||
set(output._children, cPath, directives)
|
||||
} else {
|
||||
output.directives = parent.directives
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return root
|
||||
})
|
||||
|
||||
// All rules that are made by editor
|
||||
const allEditedRules = reactive({})
|
||||
|
||||
// Checkging whether component can support some "directives" which
|
||||
// are actually virtual subcomponents, i.e. Text, Link etc
|
||||
const componentHas = (subComponent) => {
|
||||
return !!selectedComponent.value.validInnerComponents?.find(x => x === subComponent)
|
||||
}
|
||||
|
||||
// Path is path for lodash's get and set
|
||||
const getPath = (component, directive) => {
|
||||
const pathSuffix = component ? `._children.${component}.normal.normal` : ''
|
||||
const path = `${selectedComponentName.value}.${selectedVariant.value}.${normalizeStates([...selectedState])}${pathSuffix}.directives.${directive}`
|
||||
return path
|
||||
}
|
||||
|
||||
// Templates for directives
|
||||
const isElementPresent = (component, directive, defaultValue = '') => computed({
|
||||
get () {
|
||||
return get(allEditedRules, getPath(component, directive)) != null
|
||||
},
|
||||
set (value) {
|
||||
if (value) {
|
||||
const fallback = get(
|
||||
editorFriendlyFallbackStructure.value,
|
||||
getPath(component, directive)
|
||||
)
|
||||
set(allEditedRules, getPath(component, directive), fallback ?? defaultValue)
|
||||
} else {
|
||||
set(allEditedRules, getPath(component, directive), null)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const getEditedElement = (component, directive) => computed({
|
||||
get () {
|
||||
let usedRule
|
||||
const fallback = editorFriendlyFallbackStructure.value
|
||||
const real = allEditedRules
|
||||
const path = getPath(component, directive)
|
||||
|
||||
usedRule = get(real, path) // get real
|
||||
if (!usedRule) {
|
||||
usedRule = get(fallback, path)
|
||||
}
|
||||
|
||||
return usedRule
|
||||
},
|
||||
set (value) {
|
||||
set(allEditedRules, getPath(component, directive), value)
|
||||
}
|
||||
})
|
||||
|
||||
// All the editable stuff for the component
|
||||
const editedBackgroundColor = getEditedElement(null, 'background')
|
||||
const editedOpacity = getEditedElement(null, 'opacity')
|
||||
const editedTextColor = getEditedElement('Text', 'textColor')
|
||||
const editedTextAuto = getEditedElement('Text', 'textAuto')
|
||||
const editedLinkColor = getEditedElement('Link', 'textColor')
|
||||
const editedIconColor = getEditedElement('Icon', 'textColor')
|
||||
const editedShadow = getEditedElement(null, 'shadow')
|
||||
|
||||
// Shadow is partially edited outside the ShadowControl
|
||||
// for better space utilization
|
||||
const editedSubShadowId = ref(null)
|
||||
const editedSubShadow = computed(() => {
|
||||
if (editedShadow.value == null || editedSubShadowId.value == null) return null
|
||||
return editedShadow.value[editedSubShadowId.value]
|
||||
})
|
||||
|
||||
const updateSubShadow = (axis, value) => {
|
||||
if (!editedSubShadow.value || editedSubShadowId.value == null) return
|
||||
const newEditedShadow = [...editedShadow.value]
|
||||
|
||||
newEditedShadow[editedSubShadowId.value] = {
|
||||
...newEditedShadow[editedSubShadowId.value],
|
||||
[axis]: value
|
||||
}
|
||||
|
||||
editedShadow.value = newEditedShadow
|
||||
}
|
||||
|
||||
const onSubShadow = (id) => {
|
||||
if (id != null) {
|
||||
editedSubShadowId.value = id
|
||||
} else {
|
||||
editedSubShadow.value = null
|
||||
}
|
||||
}
|
||||
|
||||
// Whether specific directives present in the edited rule or not
|
||||
// Somewhat serves double-duty as it creates/removes the directive
|
||||
// when set
|
||||
const isBackgroundColorPresent = isElementPresent(null, 'background', '#FFFFFF')
|
||||
const isOpacityPresent = isElementPresent(null, 'opacity', 1)
|
||||
const isTextColorPresent = isElementPresent('Text', 'textColor', '#000000')
|
||||
const isTextAutoPresent = isElementPresent('Text', 'textAuto', '#000000')
|
||||
const isLinkColorPresent = isElementPresent('Link', 'textColor', '#000080')
|
||||
const isIconColorPresent = isElementPresent('Icon', 'textColor', '#909090')
|
||||
const isShadowPresent = isElementPresent(null, 'shadow', [])
|
||||
|
||||
const editorFriendlyToOriginal = computed(() => {
|
||||
const resultRules = []
|
||||
|
||||
const convert = (component, data = {}, parent) => {
|
||||
const variants = Object.entries(data || {})
|
||||
|
||||
variants.forEach(([variant, variantData]) => {
|
||||
const states = Object.entries(variantData)
|
||||
|
||||
states.forEach(([jointState, stateData]) => {
|
||||
const state = jointState.split(/:/g)
|
||||
const result = {
|
||||
component,
|
||||
variant,
|
||||
state,
|
||||
directives: stateData.directives || {}
|
||||
}
|
||||
|
||||
if (parent) {
|
||||
result.parent = {
|
||||
component: parent
|
||||
}
|
||||
}
|
||||
|
||||
resultRules.push(result)
|
||||
|
||||
// Currently we only support single depth for simplicity's sake
|
||||
if (!parent) {
|
||||
Object.entries(stateData._children || {}).forEach(([cName, child]) => convert(cName, child, component))
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
convert(selectedComponentName.value, allEditedRules[selectedComponentName.value])
|
||||
|
||||
return resultRules
|
||||
})
|
||||
|
||||
const updatePreview = () => {
|
||||
previewRules.splice(0, previewRules.length)
|
||||
previewRules.push(...init({
|
||||
inputRuleset: editorFriendlyToOriginal.value,
|
||||
initialStaticVars: {
|
||||
...palette.value
|
||||
},
|
||||
ultimateBackgroundColor: '#000000',
|
||||
rootComponentName: selectedComponentName.value,
|
||||
editMode: true,
|
||||
debug: true
|
||||
}).eager)
|
||||
}
|
||||
|
||||
const updateSelectedComponent = () => {
|
||||
selectedVariant.value = 'normal'
|
||||
selectedState.clear()
|
||||
updatePreview()
|
||||
}
|
||||
updateSelectedComponent()
|
||||
|
||||
watch(
|
||||
allEditedRules,
|
||||
updatePreview
|
||||
)
|
||||
|
||||
watch(
|
||||
palettes,
|
||||
updatePreview
|
||||
)
|
||||
|
||||
watch(
|
||||
editedPalette,
|
||||
updatePreview
|
||||
)
|
||||
|
||||
watch(
|
||||
selectedComponentName,
|
||||
updateSelectedComponent
|
||||
)
|
||||
|
||||
// TODO this is VERY primitive right now, need to make it
|
||||
// support variables, fallbacks etc.
|
||||
const getContrast = (bg, text) => {
|
||||
try {
|
||||
const bgRgb = hex2rgb(bg)
|
||||
const textRgb = hex2rgb(text)
|
||||
|
||||
const ratio = getContrastRatio(bgRgb, textRgb)
|
||||
return {
|
||||
// TODO this ideally should be part of <ContractRatio />
|
||||
ratio,
|
||||
text: ratio.toPrecision(3) + ':1',
|
||||
// AA level, AAA level
|
||||
aa: ratio >= 4.5,
|
||||
aaa: ratio >= 7,
|
||||
// same but for 18pt+ texts
|
||||
laa: ratio >= 3,
|
||||
laaa: ratio >= 4.5
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failure computing contrast', e)
|
||||
return { error: e }
|
||||
}
|
||||
}
|
||||
|
||||
const isShadowTabOpen = ref(false)
|
||||
const onTabSwitch = (tab) => {
|
||||
isShadowTabOpen.value = tab === 'shadow'
|
||||
}
|
||||
|
||||
const exportStyle = () => {
|
||||
console.log('ORIG', toValue(editorFriendlyToOriginal.value))
|
||||
console.log('SERI', serialize(editorFriendlyToOriginal.value))
|
||||
|
||||
const result = [
|
||||
metaOut.value,
|
||||
palettesOut.value,
|
||||
serialize(editorFriendlyToOriginal.value)
|
||||
].join('\n\n')
|
||||
|
||||
console.log('RESULT', result)
|
||||
console.log('DESERI', deserialize(result))
|
||||
}
|
||||
|
||||
return {
|
||||
name,
|
||||
author,
|
||||
license,
|
||||
website,
|
||||
palette,
|
||||
editedPalette,
|
||||
componentKeys,
|
||||
componentsMap,
|
||||
selectedComponent,
|
||||
selectedComponentName,
|
||||
selectedComponentKey,
|
||||
selectedComponentVariantsAll,
|
||||
selectedComponentStates,
|
||||
selectedVariant,
|
||||
selectedState,
|
||||
updateSelectedStates,
|
||||
editedBackgroundColor,
|
||||
editedOpacity,
|
||||
editedTextColor,
|
||||
editedTextAuto,
|
||||
editedLinkColor,
|
||||
editedIconColor,
|
||||
editedShadow,
|
||||
editedSubShadow,
|
||||
onSubShadow,
|
||||
updateSubShadow,
|
||||
getContrast,
|
||||
isBackgroundColorPresent,
|
||||
isOpacityPresent,
|
||||
isTextColorPresent,
|
||||
isTextAutoPresent,
|
||||
isLinkColorPresent,
|
||||
isIconColorPresent,
|
||||
isShadowPresent,
|
||||
previewCss,
|
||||
previewClass,
|
||||
editorHintStyle,
|
||||
getFriendlyNamePath,
|
||||
fallbackI18n,
|
||||
getVariantPath,
|
||||
getStatePath,
|
||||
componentHas,
|
||||
isShadowTabOpen,
|
||||
onTabSwitch,
|
||||
exportStyle
|
||||
}
|
||||
}
|
||||
}
|
178
src/components/settings_modal/tabs/style_tab/style_tab.scss
Normal file
178
src/components/settings_modal/tabs/style_tab/style_tab.scss
Normal file
|
@ -0,0 +1,178 @@
|
|||
.StyleTab {
|
||||
.style-control {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: baseline;
|
||||
margin-bottom: 0.5em;
|
||||
|
||||
.label {
|
||||
margin-right: 0.5em;
|
||||
flex: 1 1 0;
|
||||
line-height: 2;
|
||||
min-height: 2em;
|
||||
}
|
||||
|
||||
&.suboption {
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
.color-input {
|
||||
flex: 0 0 0;
|
||||
}
|
||||
|
||||
input,
|
||||
select {
|
||||
min-width: 3em;
|
||||
margin: 0;
|
||||
flex: 0;
|
||||
|
||||
&[type="number"] {
|
||||
min-width: 9em;
|
||||
|
||||
&.-small {
|
||||
min-width: 5em;
|
||||
}
|
||||
}
|
||||
|
||||
&[type="range"] {
|
||||
flex: 1;
|
||||
min-width: 9em;
|
||||
align-self: center;
|
||||
margin: 0 0.25em;
|
||||
}
|
||||
|
||||
&[type="checkbox"] + i {
|
||||
height: 1.1em;
|
||||
align-self: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.setting-item {
|
||||
padding-bottom: 0;
|
||||
|
||||
.btn {
|
||||
padding: 0 0.5em;
|
||||
}
|
||||
|
||||
&:not(:first-child) {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
&.heading {
|
||||
display: grid;
|
||||
align-items: baseline;
|
||||
grid-template-columns: 1fr auto auto auto;
|
||||
grid-gap: 0.5em;
|
||||
|
||||
h2 {
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
&.metadata {
|
||||
display: flex;
|
||||
|
||||
.setting-item {
|
||||
flex: 2 0 auto;
|
||||
}
|
||||
|
||||
li {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.palette-editor {
|
||||
> .label:not(.Select) {
|
||||
font-weight: bold;
|
||||
justify-self: right;
|
||||
}
|
||||
}
|
||||
|
||||
.component-editor {
|
||||
display: grid;
|
||||
grid-template-columns: 6fr 3fr 4fr;
|
||||
grid-template-rows: auto auto 1fr;
|
||||
grid-gap: 0.5em;
|
||||
grid-template-areas:
|
||||
"component component variant"
|
||||
"state state state"
|
||||
"preview settings settings";
|
||||
|
||||
.component-selector {
|
||||
grid-area: component;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.component-selector,
|
||||
.state-selector,
|
||||
.variant-selector {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr minmax(1fr, 10em);
|
||||
grid-template-rows: auto;
|
||||
grid-auto-flow: column;
|
||||
grid-gap: 0.5em;
|
||||
align-items: baseline;
|
||||
|
||||
> label:not(.Select) {
|
||||
font-weight: bold;
|
||||
justify-self: right;
|
||||
}
|
||||
}
|
||||
|
||||
.state-selector {
|
||||
grid-area: state;
|
||||
grid-template-columns: minmax(min-content, 7em) 1fr;
|
||||
}
|
||||
|
||||
.variant-selector {
|
||||
grid-area: variant;
|
||||
}
|
||||
|
||||
.state-selector-list {
|
||||
display: grid;
|
||||
list-style: none;
|
||||
grid-auto-flow: dense;
|
||||
grid-template-columns: repeat(5, minmax(min-content, 1fr));
|
||||
grid-auto-rows: 1fr;
|
||||
grid-gap: 0.5em;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.preview-container {
|
||||
--border: none;
|
||||
--shadow: none;
|
||||
--roundness: none;
|
||||
|
||||
grid-area: preview;
|
||||
}
|
||||
|
||||
.component-settings {
|
||||
grid-area: settings;
|
||||
}
|
||||
|
||||
.editor-tab {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 2em;
|
||||
grid-column-gap: 0.5em;
|
||||
align-items: center;
|
||||
grid-auto-rows: min-content;
|
||||
grid-auto-flow: dense;
|
||||
border-left: 1px solid var(--border);
|
||||
border-right: 1px solid var(--border);
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
.shadow-tab {
|
||||
grid-template-columns: 1fr;
|
||||
justify-items: center;
|
||||
}
|
||||
}
|
||||
}
|
282
src/components/settings_modal/tabs/style_tab/style_tab.vue
Normal file
282
src/components/settings_modal/tabs/style_tab/style_tab.vue
Normal file
|
@ -0,0 +1,282 @@
|
|||
<script src="./style_tab.js">
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="StyleTab">
|
||||
<div class="setting-item heading">
|
||||
<h2>{{ $t('settings.style.themes3.editor.title') }}</h2>
|
||||
<button
|
||||
class="btn button-default"
|
||||
@click="clearTheme"
|
||||
>
|
||||
<FAIcon icon="file" />
|
||||
{{ $t('settings.style.themes3.editor.new_style') }}
|
||||
</button>
|
||||
<button
|
||||
class="btn button-default"
|
||||
@click="importStyle"
|
||||
>
|
||||
<FAIcon icon="folder-open" />
|
||||
{{ $t('settings.style.themes3.editor.load_style') }}
|
||||
</button>
|
||||
<button
|
||||
class="btn button-default"
|
||||
@click="exportStyle"
|
||||
>
|
||||
<FAIcon icon="floppy-disk" />
|
||||
{{ $t('settings.style.themes3.editor.save_style') }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="setting-item metadata">
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<StringSetting v-model="name">
|
||||
{{ $t('settings.style.themes3.editor.style_name') }}
|
||||
</StringSetting>
|
||||
</li>
|
||||
<li>
|
||||
<StringSetting v-model="author">
|
||||
{{ $t('settings.style.themes3.editor.style_author') }}
|
||||
</StringSetting>
|
||||
</li>
|
||||
<li>
|
||||
<StringSetting v-model="license">
|
||||
{{ $t('settings.style.themes3.editor.style_license') }}
|
||||
</StringSetting>
|
||||
</li>
|
||||
<li>
|
||||
<StringSetting v-model="website">
|
||||
{{ $t('settings.style.themes3.editor.style_website') }}
|
||||
</StringSetting>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="setting-item component-editor">
|
||||
<div class="component-selector">
|
||||
<label for="component-selector">
|
||||
{{ $t('settings.style.themes3.editor.component_selector') }}
|
||||
{{ ' ' }}
|
||||
</label>
|
||||
<Select
|
||||
id="component-selector"
|
||||
v-model="selectedComponentKey"
|
||||
>
|
||||
<option
|
||||
v-for="key in componentKeys"
|
||||
:key="'component-' + key"
|
||||
:value="key"
|
||||
>
|
||||
{{ fallbackI18n($t(getFriendlyNamePath(componentsMap.get(key).name)), componentsMap.get(key).name) }}
|
||||
</option>
|
||||
</Select>
|
||||
</div>
|
||||
<div
|
||||
v-if="selectedComponentVariantsAll.length > 1"
|
||||
class="variant-selector"
|
||||
>
|
||||
<label for="variant-selector">
|
||||
{{ $t('settings.style.themes3.editor.variant_selector') }}
|
||||
</label>
|
||||
<Select
|
||||
v-model="selectedVariant"
|
||||
>
|
||||
<option
|
||||
v-for="variant in selectedComponentVariantsAll"
|
||||
:key="'component-variant-' + variant"
|
||||
:value="variant"
|
||||
>
|
||||
{{ fallbackI18n($t(getVariantPath(selectedComponentName, variant)), variant) }}
|
||||
</option>
|
||||
</Select>
|
||||
</div>
|
||||
<div
|
||||
v-if="selectedComponentStates.length > 0"
|
||||
class="state-selector"
|
||||
>
|
||||
<label>
|
||||
{{ $t('settings.style.themes3.editor.states_selector') }}
|
||||
</label>
|
||||
<ul
|
||||
class="state-selector-list"
|
||||
>
|
||||
<li
|
||||
v-for="state in selectedComponentStates"
|
||||
:key="'component-state-' + state"
|
||||
>
|
||||
<Checkbox
|
||||
:value="selectedState.has(state)"
|
||||
@update:modelValue="(v) => updateSelectedStates(state, v)"
|
||||
>
|
||||
{{ fallbackI18n($t(getStatePath(selectedComponentName, state)), state) }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="preview-container">
|
||||
<!-- eslint-disable vue/no-v-html vue/no-v-text-v-html-on-component -->
|
||||
<component
|
||||
:is="'style'"
|
||||
v-html="previewCss"
|
||||
/>
|
||||
<!-- eslint-enable vue/no-v-html vue/no-v-text-v-html-on-component -->
|
||||
<ComponentPreview
|
||||
class="component-preview"
|
||||
:show-text="componentHas('Text')"
|
||||
:shadow-control="isShadowTabOpen"
|
||||
:preview-class="previewClass"
|
||||
:preview-style="editorHintStyle"
|
||||
:disabled="!editedSubShadow"
|
||||
:shadow="editedSubShadow"
|
||||
@update:shadow="({ axis, value }) => updateSubShadow(axis, value)"
|
||||
/>
|
||||
</div>
|
||||
<tab-switcher
|
||||
ref="tabSwitcher"
|
||||
class="component-settings"
|
||||
:on-switch="onTabSwitch"
|
||||
>
|
||||
<div
|
||||
key="main"
|
||||
class="editor-tab"
|
||||
:label="$t('settings.style.themes3.editor.main_tab')"
|
||||
>
|
||||
<ColorInput
|
||||
v-model="editedBackgroundColor"
|
||||
:disabled="!isBackgroundColorPresent"
|
||||
:label="$t('settings.style.themes3.editor.background')"
|
||||
/>
|
||||
<Tooltip :text="$t('settings.style.themes3.editor.include_in_rule')">
|
||||
<Checkbox v-model="isBackgroundColorPresent" />
|
||||
</Tooltip>
|
||||
<OpacityInput
|
||||
v-model="editedOpacity"
|
||||
:disabled="!isOpacityPresent"
|
||||
:label="$t('settings.style.themes3.editor.opacity')"
|
||||
/>
|
||||
<Tooltip :text="$t('settings.style.themes3.editor.include_in_rule')">
|
||||
<Checkbox v-model="isOpacityPresent" />
|
||||
</Tooltip>
|
||||
<ColorInput
|
||||
v-if="componentHas('Text')"
|
||||
v-model="editedTextColor"
|
||||
:label="$t('settings.style.themes3.editor.text_color')"
|
||||
:disabled="!isTextColorPresent"
|
||||
/>
|
||||
<Tooltip
|
||||
v-if="componentHas('Text')"
|
||||
:text="$t('settings.style.themes3.editor.include_in_rule')"
|
||||
>
|
||||
<Checkbox v-model="isTextColorPresent" />
|
||||
</Tooltip>
|
||||
<div class="style-control suboption">
|
||||
<label
|
||||
for="textAuto"
|
||||
class="label"
|
||||
:class="{ faint: !isTextAutoPresent }"
|
||||
>
|
||||
{{ $t('settings.style.themes3.editor.text_auto.label') }}
|
||||
</label>
|
||||
<Select
|
||||
id="textAuto"
|
||||
v-model="editedTextAuto"
|
||||
:disabled="!isTextAutoPresent"
|
||||
>
|
||||
<option value="no-preserve">
|
||||
{{ $t('settings.style.themes3.editor.text_auto.no-preserve') }}
|
||||
</option>
|
||||
<option value="no-auto">
|
||||
{{ $t('settings.style.themes3.editor.text_auto.no-auto') }}
|
||||
</option>
|
||||
<option value="preserve">
|
||||
{{ $t('settings.style.themes3.editor.text_auto.preserve') }}
|
||||
</option>
|
||||
</Select>
|
||||
</div>
|
||||
<Tooltip
|
||||
v-if="componentHas('Text')"
|
||||
:text="$t('settings.style.themes3.editor.include_in_rule')"
|
||||
>
|
||||
<Checkbox v-model="isTextAutoPresent" />
|
||||
</Tooltip>
|
||||
<div>
|
||||
<ContrastRatio :contrast="getContrast(editedBackgroundColor, editedTextColor)" />
|
||||
</div>
|
||||
<div>
|
||||
<!-- spacer for missing checkbox -->
|
||||
</div>
|
||||
<ColorInput
|
||||
v-if="componentHas('Link')"
|
||||
v-model="editedLinkColor"
|
||||
:label="$t('settings.style.themes3.editor.link_color')"
|
||||
:disabled="!isLinkColorPresent"
|
||||
/>
|
||||
<Tooltip
|
||||
v-if="componentHas('Link')"
|
||||
:text="$t('settings.style.themes3.editor.include_in_rule')"
|
||||
>
|
||||
<Checkbox v-model="isLinkColorPresent" />
|
||||
</Tooltip>
|
||||
<ColorInput
|
||||
v-if="componentHas('Icon')"
|
||||
v-model="editedIconColor"
|
||||
:label="$t('settings.style.themes3.editor.icon_color')"
|
||||
:disabled="!isIconColorPresent"
|
||||
/>
|
||||
<Tooltip
|
||||
v-if="componentHas('Icon')"
|
||||
:text="$t('settings.style.themes3.editor.include_in_rule')"
|
||||
>
|
||||
<Checkbox v-model="isIconColorPresent" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div
|
||||
key="shadow"
|
||||
class="editor-tab shadow-tab"
|
||||
:label="$t('settings.style.themes3.editor.shadows_tab')"
|
||||
>
|
||||
<Checkbox
|
||||
v-model="isShadowPresent"
|
||||
class="style-control"
|
||||
>
|
||||
{{ $t('settings.style.themes3.editor.include_in_rule') }}
|
||||
</checkbox>
|
||||
<ShadowControl
|
||||
v-model="editedShadow"
|
||||
:disabled="!isShadowPresent"
|
||||
:no-preview="true"
|
||||
:separate-inset="shadowSelected === 'avatar' || shadowSelected === 'avatarStatus'"
|
||||
@subShadowSelected="onSubShadow"
|
||||
/>
|
||||
</div>
|
||||
</tab-switcher>
|
||||
</div>
|
||||
<div class="setting-item palette-editor">
|
||||
<div class="label">
|
||||
<label for="palette-selector">
|
||||
{{ $t('settings.style.themes3.palette.label') }}
|
||||
{{ ' ' }}
|
||||
</label>
|
||||
<Select
|
||||
id="palette-selector"
|
||||
v-model="editedPalette"
|
||||
>
|
||||
<option
|
||||
key="dark"
|
||||
value="dark"
|
||||
>
|
||||
{{ $t('settings.style.themes3.palette.dark') }}
|
||||
</option>
|
||||
<option
|
||||
key="light"
|
||||
value="light"
|
||||
>
|
||||
{{ $t('settings.style.themes3.palette.light') }}
|
||||
</option>
|
||||
</Select>
|
||||
</div>
|
||||
<PaletteEditor v-model="palette" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style src="./style_tab.scss" lang="scss"></style>
|
|
@ -4,9 +4,6 @@ import {
|
|||
getContrastRatioLayers,
|
||||
relativeLuminance
|
||||
} from 'src/services/color_convert/color_convert.js'
|
||||
import {
|
||||
getThemes
|
||||
} from 'src/services/style_setter/style_setter.js'
|
||||
import {
|
||||
newImporter,
|
||||
newExporter
|
||||
|
@ -123,27 +120,21 @@ export default {
|
|||
}
|
||||
},
|
||||
created () {
|
||||
const self = this
|
||||
const currentIndex = this.$store.state.instance.themesIndex
|
||||
|
||||
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
|
||||
}
|
||||
let promise
|
||||
if (currentIndex) {
|
||||
promise = Promise.resolve(currentIndex)
|
||||
} else {
|
||||
return acc
|
||||
promise = this.$store.dispatch('fetchThemesIndex')
|
||||
}
|
||||
}, {}))
|
||||
.then((themesComplete) => {
|
||||
self.availableStyles = themesComplete
|
||||
|
||||
promise.then(themesIndex => {
|
||||
Object
|
||||
.values(themesIndex)
|
||||
.forEach(themeFunc => {
|
||||
themeFunc().then(themeData => this.availableStyles.push(themeData))
|
||||
})
|
||||
})
|
||||
},
|
||||
mounted () {
|
||||
|
@ -412,9 +403,6 @@ export default {
|
|||
forceUseSource = false
|
||||
) {
|
||||
this.dismissWarning()
|
||||
if (!source && !theme) {
|
||||
throw new Error('Can\'t load theme: empty')
|
||||
}
|
||||
const version = (origin === 'localStorage' && !theme.colors)
|
||||
? 'l1'
|
||||
: fileVersion
|
||||
|
@ -494,14 +482,7 @@ export default {
|
|||
customTheme: theme,
|
||||
customThemeSource: source
|
||||
} = this.$store.getters.mergedConfig
|
||||
if (!theme && !source) {
|
||||
// Anon user or never touched themes
|
||||
this.loadTheme(
|
||||
this.$store.state.instance.themeData,
|
||||
'defaults',
|
||||
confirmLoadSource
|
||||
)
|
||||
} else {
|
||||
if (theme || source) {
|
||||
this.loadTheme(
|
||||
{
|
||||
theme,
|
||||
|
|
|
@ -45,12 +45,16 @@
|
|||
flex: 0;
|
||||
|
||||
&[type="number"] {
|
||||
min-width: 9em;
|
||||
|
||||
&.-small {
|
||||
min-width: 5em;
|
||||
}
|
||||
}
|
||||
|
||||
&[type="range"] {
|
||||
flex: 1;
|
||||
min-width: 2em;
|
||||
min-width: 9em;
|
||||
align-self: center;
|
||||
margin: 0 0.5em;
|
||||
}
|
||||
|
|
|
@ -34,9 +34,9 @@ const toModel = (object = {}) => ({
|
|||
|
||||
export default {
|
||||
props: [
|
||||
'modelValue', 'fallback', 'separateInset', 'noPreview'
|
||||
'modelValue', 'fallback', 'separateInset', 'noPreview', 'disabled'
|
||||
],
|
||||
emits: ['update:modelValue'],
|
||||
emits: ['update:modelValue', 'subShadowSelected'],
|
||||
data () {
|
||||
return {
|
||||
selectedId: 0,
|
||||
|
@ -93,9 +93,13 @@ export default {
|
|||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
selected (value) {
|
||||
this.$emit('subShadowSelected', this.selectedId)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateProperty: throttle(function (prop, value) {
|
||||
console.log(prop, value)
|
||||
this.cValue[this.selectedId][prop] = value
|
||||
if (prop === 'inset' && value === false && this.separateInset) {
|
||||
this.cValue[this.selectedId].spread = 0
|
||||
|
|
|
@ -71,9 +71,13 @@
|
|||
&.-no-preview {
|
||||
.shadow-tweak {
|
||||
order: 0;
|
||||
flex: 2 0 20em;
|
||||
flex: 2 0 8em;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.input-range {
|
||||
min-width: 5em;
|
||||
}
|
||||
}
|
||||
|
||||
.inset-alert {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div
|
||||
class="label shadow-control"
|
||||
:class="{ disabled: !present, '-no-preview': noPreview }"
|
||||
:class="{ disabled: disabled || !present, '-no-preview': noPreview }"
|
||||
>
|
||||
<ComponentPreview
|
||||
v-if="!noPreview"
|
||||
|
@ -9,7 +9,7 @@
|
|||
:shadow-control="true"
|
||||
:shadow="selected"
|
||||
:preview-style="style"
|
||||
:disabled="!present"
|
||||
:disabled="disabled || !present"
|
||||
@update:shadow="({ axis, value }) => updateProperty(axis, value)"
|
||||
/>
|
||||
<div class="shadow-switcher">
|
||||
|
@ -34,7 +34,7 @@
|
|||
>
|
||||
<button
|
||||
class="btn button-default"
|
||||
:disabled="shadowsAreNull"
|
||||
:disabled="disabled || shadowsAreNull"
|
||||
@click="add"
|
||||
>
|
||||
<FAIcon
|
||||
|
@ -44,8 +44,8 @@
|
|||
</button>
|
||||
<button
|
||||
class="btn button-default"
|
||||
:disabled="!moveUpValid"
|
||||
:class="{ disabled: !moveUpValid }"
|
||||
:disabled="disabled || !moveUpValid"
|
||||
:class="{ disabled: disabled || !moveUpValid }"
|
||||
@click="moveUp"
|
||||
>
|
||||
<FAIcon
|
||||
|
@ -55,8 +55,8 @@
|
|||
</button>
|
||||
<button
|
||||
class="btn button-default"
|
||||
:disabled="!moveDnValid"
|
||||
:class="{ disabled: !moveDnValid }"
|
||||
:disabled="disabled || !moveDnValid"
|
||||
:class="{ disabled: disabled || !moveDnValid }"
|
||||
@click="moveDn"
|
||||
>
|
||||
<FAIcon
|
||||
|
@ -66,8 +66,8 @@
|
|||
</button>
|
||||
<button
|
||||
class="btn button-default"
|
||||
:disabled="!present"
|
||||
:class="{ disabled: !present }"
|
||||
:disabled="disabled || !present"
|
||||
:class="{ disabled: disabled || !present }"
|
||||
@click="del"
|
||||
>
|
||||
<FAIcon
|
||||
|
@ -79,34 +79,34 @@
|
|||
</div>
|
||||
<div class="shadow-tweak">
|
||||
<div
|
||||
:class="{ disabled: !present }"
|
||||
:class="{ disabled: disabled || !present }"
|
||||
class="name-control style-control"
|
||||
>
|
||||
<label
|
||||
for="name"
|
||||
class="label"
|
||||
:class="{ faint: !present }"
|
||||
:class="{ faint: disabled || !present }"
|
||||
>
|
||||
{{ $t('settings.style.shadows.name') }}
|
||||
</label>
|
||||
<input
|
||||
id="name"
|
||||
:value="selected?.name"
|
||||
:disabled="!present"
|
||||
:class="{ disabled: !present }"
|
||||
:disabled="disabled || !present"
|
||||
:class="{ disabled: disabled || !present }"
|
||||
name="name"
|
||||
class="input input-string"
|
||||
@input="e => updateProperty('name', e.target.value)"
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
:disabled="!present"
|
||||
:disabled="disabled || !present"
|
||||
class="inset-control style-control"
|
||||
>
|
||||
<Checkbox
|
||||
id="inset"
|
||||
:value="selected?.inset"
|
||||
:disabled="!present"
|
||||
:disabled="disabled || !present"
|
||||
name="inset"
|
||||
class="input-inset input-boolean"
|
||||
@input="e => updateProperty('inset', e.target.checked)"
|
||||
|
@ -117,22 +117,22 @@
|
|||
</Checkbox>
|
||||
</div>
|
||||
<div
|
||||
:disabled="!present"
|
||||
:class="{ disabled: !present }"
|
||||
:disabled="disabled || !present"
|
||||
:class="{ disabled: disabled || !present }"
|
||||
class="blur-control style-control"
|
||||
>
|
||||
<label
|
||||
for="blur"
|
||||
class="label"
|
||||
:class="{ faint: !present }"
|
||||
:class="{ faint: disabled || !present }"
|
||||
>
|
||||
{{ $t('settings.style.shadows.blur') }}
|
||||
</label>
|
||||
<input
|
||||
id="blur"
|
||||
:value="selected?.blur"
|
||||
:disabled="!present"
|
||||
:class="{ disabled: !present }"
|
||||
:disabled="disabled || !present"
|
||||
:class="{ disabled: disabled || !present }"
|
||||
name="blur"
|
||||
class="input input-range"
|
||||
type="range"
|
||||
|
@ -142,9 +142,9 @@
|
|||
>
|
||||
<input
|
||||
:value="selected?.blur"
|
||||
:disabled="!present"
|
||||
:class="{ disabled: !present }"
|
||||
class="input input-number"
|
||||
class="input input-number -small"
|
||||
:disabled="disabled || !present"
|
||||
:class="{ disabled: disabled || !present }"
|
||||
type="number"
|
||||
min="0"
|
||||
@input="e => updateProperty('blur', e.target.value)"
|
||||
|
@ -152,20 +152,20 @@
|
|||
</div>
|
||||
<div
|
||||
class="spread-control style-control"
|
||||
:class="{ disabled: !present || (separateInset && !selected?.inset) }"
|
||||
:class="{ disabled: disabled || !present || (separateInset && !selected?.inset) }"
|
||||
>
|
||||
<label
|
||||
for="spread"
|
||||
class="label"
|
||||
:class="{ faint: !present || (separateInset && !selected?.inset) }"
|
||||
:class="{ faint: disabled || !present || (separateInset && !selected?.inset) }"
|
||||
>
|
||||
{{ $t('settings.style.shadows.spread') }}
|
||||
</label>
|
||||
<input
|
||||
id="spread"
|
||||
:value="selected?.spread"
|
||||
:disabled="!present || (separateInset && !selected?.inset)"
|
||||
:class="{ disabled: !present || (separateInset && !selected?.inset) }"
|
||||
:disabled="disabled || !present || (separateInset && !selected?.inset)"
|
||||
:class="{ disabled: disabled || !present || (separateInset && !selected?.inset) }"
|
||||
name="spread"
|
||||
class="input input-range"
|
||||
type="range"
|
||||
|
@ -175,16 +175,16 @@
|
|||
>
|
||||
<input
|
||||
:value="selected?.spread"
|
||||
:disabled="{ disabled: !present || (separateInset && !selected?.inset) }"
|
||||
:class="{ disabled: !present || (separateInset && !selected?.inset) }"
|
||||
class="input input-number"
|
||||
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
|
||||
:model-value="selected?.color"
|
||||
:disabled="!present"
|
||||
:disabled="disabled || !present"
|
||||
:label="$t('settings.style.common.color')"
|
||||
:fallback="currentFallback?.color"
|
||||
:show-optional-tickbox="false"
|
||||
|
@ -193,13 +193,13 @@
|
|||
/>
|
||||
<OpacityInput
|
||||
:model-value="selected?.alpha"
|
||||
:disabled="!present"
|
||||
:disabled="disabled || !present"
|
||||
@update:modelValue="e => updateProperty('alpha', e)"
|
||||
/>
|
||||
<i18n-t
|
||||
scope="global"
|
||||
keypath="settings.style.shadows.hintV3"
|
||||
:class="{ faint: !present }"
|
||||
:class="{ faint: disabled || !present }"
|
||||
tag="p"
|
||||
>
|
||||
<code>--variable,mod</code>
|
||||
|
|
|
@ -39,7 +39,7 @@ export default {
|
|||
{
|
||||
state: ['disabled'],
|
||||
directives: {
|
||||
background: '$blend(--inheritedBackground, 0.25, --parent)',
|
||||
background: '$blend(--inheritedBackground 0.25 --parent)',
|
||||
shadow: ['--defaultButtonBevel']
|
||||
}
|
||||
},
|
||||
|
|
24
src/components/tooltip/tooltip.vue
Normal file
24
src/components/tooltip/tooltip.vue
Normal file
|
@ -0,0 +1,24 @@
|
|||
<template>
|
||||
<Popover trigger="hover">
|
||||
<template #trigger>
|
||||
<slot />
|
||||
</template>
|
||||
<template #content>
|
||||
<div class="tooltip">
|
||||
{{ props.text }}
|
||||
</div>
|
||||
</template>
|
||||
</Popover>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import Popover from 'src/components/popover/popover.vue'
|
||||
|
||||
const props = defineProps(['text'])
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.tooltip {
|
||||
margin: 0.5em 1em;
|
||||
}
|
||||
</style>
|
|
@ -25,7 +25,7 @@ export default {
|
|||
color: '#000000',
|
||||
alpha: 0.6
|
||||
}],
|
||||
'--profileTint': 'color | $alpha(--background, 0.5)'
|
||||
'--profileTint': 'color | $alpha(--background 0.5)'
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
@ -748,11 +748,96 @@
|
|||
"more_settings": "More settings",
|
||||
"style": {
|
||||
"custom_theme_used": "(Custom theme)",
|
||||
"stock_theme_used": "(Stock theme)",
|
||||
"themes2_outdated": "Editor for Themes V2 is being phased out and will eventually be replaced with a new one that takes advantage of new Themes V3 engine. It should still work but experience might be degraded and inconsistent.",
|
||||
"appearance_tab_note": "Changes on this tab do not affect the theme used, so exported theme will be different from what seen in the UI",
|
||||
"update_preview": "Update preview",
|
||||
"themes3": {
|
||||
"define": "Override",
|
||||
"palette": {
|
||||
"label": "Palette",
|
||||
"import": "Import",
|
||||
"export": "Export",
|
||||
"dark": "Dark mode",
|
||||
"light": "Light mode",
|
||||
"bg": "Panel background",
|
||||
"fg": "Buttons etc.",
|
||||
"text": "Text",
|
||||
"link": "Links",
|
||||
"accent": "Accent color",
|
||||
"cRed": "Red color",
|
||||
"cBlue": "Blue color",
|
||||
"cGreen": "Green color",
|
||||
"cOrange": "Orange color",
|
||||
"extra1": "Extra 1",
|
||||
"extra2": "Extra 2",
|
||||
"extra3": "Extra 3"
|
||||
},
|
||||
"editor": {
|
||||
"title": "Style",
|
||||
"new_style": "New",
|
||||
"load_style": "Open",
|
||||
"save_style": "Save",
|
||||
"style_name": "Stylesheet name",
|
||||
"style_author": "Made by",
|
||||
"style_license": "License",
|
||||
"style_website": "Website",
|
||||
"component_selector": "Component",
|
||||
"variant_selector": "Variant",
|
||||
"states_selector": "States",
|
||||
"main_tab": "Main",
|
||||
"shadows_tab": "Shadows",
|
||||
"background": "Background color",
|
||||
"text_color": "Text color",
|
||||
"icon_color": "Icon color",
|
||||
"link_color": "Link color",
|
||||
"include_in_rule": "Add to rule",
|
||||
"text_auto": {
|
||||
"label": "Auto-contrast",
|
||||
"no-preserve": "Black or White",
|
||||
"preserve": "Keep color",
|
||||
"no-auto": "Disabled"
|
||||
},
|
||||
"components": {
|
||||
"normal": {
|
||||
"state": "Normal",
|
||||
"variant": "Default"
|
||||
},
|
||||
"Alert": {
|
||||
"friendlyName": "Alert",
|
||||
"variants": {
|
||||
"error": "Error",
|
||||
"warning": "Warning",
|
||||
"success": "Success"
|
||||
}
|
||||
},
|
||||
"Button": {
|
||||
"friendlyName": "Button",
|
||||
"variants": {
|
||||
"danger": "Dangerous"
|
||||
},
|
||||
"states": {
|
||||
"toggled": "Toggled",
|
||||
"pressed": "Pressed",
|
||||
"hover": "Hovered",
|
||||
"focused": "Has focus",
|
||||
"disabled": "Disabled"
|
||||
}
|
||||
},
|
||||
"Input": {
|
||||
"friendlyName": "Input fields",
|
||||
"variants": {
|
||||
"checkbox": "Checkbox",
|
||||
"radio": "Radio"
|
||||
},
|
||||
"states": {
|
||||
"hover": "Hovered",
|
||||
"focus": "Focused",
|
||||
"disabled": "Disabled"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"hacks": {
|
||||
"underlay_overrides": "Change underlay",
|
||||
"underlay_override_mode_none": "Theme default",
|
||||
|
|
|
@ -47,6 +47,8 @@ export const defaultState = {
|
|||
customThemeSource: undefined, // "source", stores original theme data
|
||||
|
||||
// V3
|
||||
style: null,
|
||||
palette: null,
|
||||
themeDebug: false, // debug mode that uses computed backgrounds instead of real ones to debug contrast functions
|
||||
forceThemeRecompilation: false, // flag that forces recompilation on boot even if cache exists
|
||||
theme3hacks: { // Hacks, user overrides that are independent of theme used
|
||||
|
|
|
@ -42,6 +42,9 @@ const defaultState = {
|
|||
registrationOpen: true,
|
||||
server: 'http://localhost:4040/',
|
||||
textlimit: 5000,
|
||||
themesIndex: undefined,
|
||||
stylesIndex: undefined,
|
||||
palettesIndex: undefined,
|
||||
themeData: undefined, // used for theme editor v2
|
||||
vapidPublicKey: undefined,
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { getPreset, applyTheme, tryLoadCache } from '../services/style_setter/style_setter.js'
|
||||
import { getResourcesIndex, 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'
|
||||
|
||||
|
@ -212,65 +212,315 @@ const interfaceMod = {
|
|||
setLastTimeline ({ commit }, value) {
|
||||
commit('setLastTimeline', value)
|
||||
},
|
||||
setTheme ({ commit, rootState }, { themeName, themeData, recompile, saveData } = {}) {
|
||||
async fetchPalettesIndex ({ commit, state }) {
|
||||
try {
|
||||
const value = await getResourcesIndex('/static/palettes/index.json')
|
||||
commit('setInstanceOption', { name: 'palettesIndex', value })
|
||||
return value
|
||||
} catch (e) {
|
||||
console.error('Could not fetch palettes index', e)
|
||||
return {}
|
||||
}
|
||||
},
|
||||
setPalette ({ dispatch, commit }, value) {
|
||||
dispatch('resetThemeV3Palette')
|
||||
dispatch('resetThemeV2')
|
||||
|
||||
commit('setOption', { name: 'palette', value })
|
||||
|
||||
dispatch('applyTheme')
|
||||
},
|
||||
setPaletteCustom ({ dispatch, commit }, value) {
|
||||
dispatch('resetThemeV3Palette')
|
||||
dispatch('resetThemeV2')
|
||||
|
||||
commit('setOption', { name: 'paletteCustomData', value })
|
||||
|
||||
dispatch('applyTheme')
|
||||
},
|
||||
async fetchStylesIndex ({ commit, state }) {
|
||||
try {
|
||||
const value = await getResourcesIndex('/static/styles/index.json')
|
||||
commit('setInstanceOption', { name: 'stylesIndex', value })
|
||||
return value
|
||||
} catch (e) {
|
||||
console.error('Could not fetch styles index', e)
|
||||
return Promise.resolve({})
|
||||
}
|
||||
},
|
||||
setStyle ({ dispatch, commit }, value) {
|
||||
dispatch('resetThemeV3')
|
||||
dispatch('resetThemeV2')
|
||||
|
||||
commit('setOption', { name: 'style', value })
|
||||
|
||||
dispatch('applyTheme')
|
||||
},
|
||||
setStyleCustom ({ dispatch, commit }, value) {
|
||||
dispatch('resetThemeV3')
|
||||
dispatch('resetThemeV2')
|
||||
|
||||
commit('setOption', { name: 'styleCustomData', value })
|
||||
|
||||
dispatch('applyTheme')
|
||||
},
|
||||
async fetchThemesIndex ({ commit, state }) {
|
||||
try {
|
||||
const value = await getResourcesIndex('/static/styles.json')
|
||||
commit('setInstanceOption', { name: 'themesIndex', value })
|
||||
return value
|
||||
} catch (e) {
|
||||
console.error('Could not fetch themes index', e)
|
||||
return Promise.resolve({})
|
||||
}
|
||||
},
|
||||
setTheme ({ dispatch, commit }, value) {
|
||||
dispatch('resetThemeV3')
|
||||
dispatch('resetThemeV3Palette')
|
||||
dispatch('resetThemeV2')
|
||||
|
||||
commit('setOption', { name: 'theme', value })
|
||||
|
||||
dispatch('applyTheme')
|
||||
},
|
||||
setThemeCustom ({ dispatch, commit }, value) {
|
||||
dispatch('resetThemeV3')
|
||||
dispatch('resetThemeV3Palette')
|
||||
dispatch('resetThemeV2')
|
||||
|
||||
commit('setOption', { name: 'customTheme', value })
|
||||
commit('setOption', { name: 'customThemeSource', value })
|
||||
|
||||
dispatch('applyTheme')
|
||||
},
|
||||
resetThemeV3 ({ dispatch, commit }) {
|
||||
commit('setOption', { name: 'style', value: null })
|
||||
commit('setOption', { name: 'styleCustomData', value: null })
|
||||
},
|
||||
resetThemeV3Palette ({ dispatch, commit }) {
|
||||
commit('setOption', { name: 'palette', value: null })
|
||||
commit('setOption', { name: 'paletteCustomData', value: null })
|
||||
},
|
||||
resetThemeV2 ({ dispatch, commit }) {
|
||||
commit('setOption', { name: 'theme', value: null })
|
||||
commit('setOption', { name: 'customTheme', value: null })
|
||||
commit('setOption', { name: 'customThemeSource', value: null })
|
||||
},
|
||||
async applyTheme (
|
||||
{ dispatch, commit, rootState },
|
||||
{ recompile = true } = {}
|
||||
) {
|
||||
// If we're not not forced to recompile try using
|
||||
// cache (tryLoadCache return true if load successful)
|
||||
|
||||
const {
|
||||
theme: instanceThemeName
|
||||
style: instanceStyleName,
|
||||
palette: instancePaletteName
|
||||
} = rootState.instance
|
||||
let {
|
||||
theme: instanceThemeV2Name,
|
||||
themesIndex,
|
||||
stylesIndex,
|
||||
palettesIndex
|
||||
} = rootState.instance
|
||||
|
||||
const {
|
||||
theme: userThemeName,
|
||||
customTheme: userThemeSnapshot,
|
||||
customThemeSource: userThemeSource,
|
||||
style: userStyleName,
|
||||
styleCustomData: userStyleCustomData,
|
||||
palette: userPaletteName,
|
||||
paletteCustomData: userPaletteCustomData,
|
||||
forceThemeRecompilation,
|
||||
themeDebug,
|
||||
theme3hacks
|
||||
} = rootState.config
|
||||
|
||||
const actualThemeName = userThemeName || instanceThemeName
|
||||
let {
|
||||
theme: userThemeV2Name,
|
||||
customTheme: userThemeV2Snapshot,
|
||||
customThemeSource: userThemeV2Source
|
||||
} = rootState.config
|
||||
|
||||
const forceRecompile = forceThemeRecompilation || recompile
|
||||
|
||||
let promise = null
|
||||
|
||||
if (themeData) {
|
||||
promise = Promise.resolve(normalizeThemeData(themeData))
|
||||
} else if (themeName) {
|
||||
promise = getPreset(themeName).then(themeData => normalizeThemeData(themeData))
|
||||
} else if (userThemeSource || userThemeSnapshot) {
|
||||
promise = Promise.resolve(normalizeThemeData({
|
||||
_pleroma_theme_version: 2,
|
||||
theme: userThemeSnapshot,
|
||||
source: userThemeSource
|
||||
}))
|
||||
} else if (actualThemeName && actualThemeName !== 'custom') {
|
||||
promise = getPreset(actualThemeName).then(themeData => {
|
||||
const realThemeData = normalizeThemeData(themeData)
|
||||
if (actualThemeName === instanceThemeName) {
|
||||
// This sole line is the reason why this whole block is above the recompilation check
|
||||
commit('setInstanceOption', { name: 'themeData', value: { theme: realThemeData } })
|
||||
}
|
||||
return realThemeData
|
||||
})
|
||||
} else {
|
||||
throw new Error('Cannot load any theme!')
|
||||
}
|
||||
|
||||
// If we're not not forced to recompile try using
|
||||
// cache (tryLoadCache return true if load successful)
|
||||
if (!forceRecompile && !themeDebug && tryLoadCache()) {
|
||||
commit('setThemeApplied')
|
||||
return
|
||||
return commit('setThemeApplied')
|
||||
}
|
||||
|
||||
promise
|
||||
.then(realThemeData => {
|
||||
const theme2ruleset = convertTheme2To3(realThemeData)
|
||||
let majorVersionUsed
|
||||
|
||||
if (saveData) {
|
||||
commit('setOption', { name: 'theme', value: themeName || actualThemeName })
|
||||
commit('setOption', { name: 'customTheme', value: realThemeData })
|
||||
commit('setOption', { name: 'customThemeSource', value: realThemeData })
|
||||
console.log(
|
||||
`USER V3 palette: ${userPaletteName}, style: ${userStyleName} `
|
||||
)
|
||||
console.log(
|
||||
`USER V2 name: ${userThemeV2Name}, source: ${userThemeV2Source}, snapshot: ${userThemeV2Snapshot}`
|
||||
)
|
||||
|
||||
console.log(`INST V3 palette: ${instancePaletteName}, style: ${instanceStyleName}`)
|
||||
console.log('INST V2 theme: ' + instanceThemeV2Name)
|
||||
|
||||
if (userPaletteName || userPaletteCustomData ||
|
||||
userStyleName || userStyleCustomData ||
|
||||
(
|
||||
// User V2 overrides instance V3
|
||||
(instancePaletteName ||
|
||||
instanceStyleName) &&
|
||||
instanceThemeV2Name == null &&
|
||||
userThemeV2Name == null
|
||||
)
|
||||
) {
|
||||
// Palette and/or style overrides V2 themes
|
||||
instanceThemeV2Name = null
|
||||
userThemeV2Name = null
|
||||
userThemeV2Source = null
|
||||
userThemeV2Snapshot = null
|
||||
|
||||
majorVersionUsed = 'v3'
|
||||
if (!palettesIndex || !stylesIndex) {
|
||||
const result = await Promise.all([
|
||||
dispatch('fetchPalettesIndex'),
|
||||
dispatch('fetchStylesIndex')
|
||||
])
|
||||
|
||||
palettesIndex = result[0]
|
||||
stylesIndex = result[1]
|
||||
}
|
||||
} else if (
|
||||
userThemeV2Name ||
|
||||
userThemeV2Snapshot ||
|
||||
userThemeV2Source ||
|
||||
instanceThemeV2Name
|
||||
) {
|
||||
majorVersionUsed = 'v2'
|
||||
// Promise.all just to be uniform with v3
|
||||
const result = await Promise.all([
|
||||
dispatch('fetchThemesIndex')
|
||||
])
|
||||
themesIndex = result[0]
|
||||
} else {
|
||||
majorVersionUsed = 'v3'
|
||||
}
|
||||
|
||||
let styleDataUsed = null
|
||||
let styleNameUsed = null
|
||||
let paletteDataUsed = null
|
||||
let paletteNameUsed = null
|
||||
let themeNameUsed = null
|
||||
let themeDataUsed = null
|
||||
|
||||
const getData = async (resource, index, customData, name) => {
|
||||
const capitalizedResource = resource[0].toUpperCase() + resource.slice(1)
|
||||
const result = {}
|
||||
|
||||
if (customData) {
|
||||
result.nameUsed = 'custom' // custom data overrides name
|
||||
result.dataUsed = customData
|
||||
} else {
|
||||
result.nameUsed = name
|
||||
|
||||
if (result.nameUsed === 'stock') {
|
||||
result.dataUsed = null
|
||||
return result
|
||||
}
|
||||
|
||||
let fetchFunc = index[result.nameUsed]
|
||||
// Fallbacks
|
||||
if (!fetchFunc) {
|
||||
const newName = Object.keys(index)[0]
|
||||
fetchFunc = index[newName]
|
||||
console.warn(`${capitalizedResource} with id '${styleNameUsed}' not found, trying back to '${newName}'`)
|
||||
if (!fetchFunc) {
|
||||
console.warn(`${capitalizedResource} doesn't have a fallback, defaulting to stock.`)
|
||||
fetchFunc = () => Promise.resolve(null)
|
||||
}
|
||||
}
|
||||
result.dataUsed = await fetchFunc()
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
console.log('VERSION', majorVersionUsed)
|
||||
|
||||
if (majorVersionUsed === 'v3') {
|
||||
const palette = await getData(
|
||||
'palette',
|
||||
palettesIndex,
|
||||
userPaletteCustomData,
|
||||
userPaletteName || instancePaletteName
|
||||
)
|
||||
paletteNameUsed = palette.nameUsed
|
||||
paletteDataUsed = palette.dataUsed
|
||||
if (Array.isArray(paletteDataUsed)) {
|
||||
const [
|
||||
name,
|
||||
background,
|
||||
foreground,
|
||||
text,
|
||||
link,
|
||||
cRed = '#FF0000',
|
||||
cGreen = '#00FF00',
|
||||
cBlue = '#0000FF',
|
||||
cOrange = '#E3FF00'
|
||||
] = paletteDataUsed
|
||||
paletteDataUsed = { name, background, foreground, text, link, cRed, cBlue, cGreen, cOrange }
|
||||
}
|
||||
console.log('PAL', userPaletteName, paletteNameUsed)
|
||||
console.log('PAL', paletteDataUsed)
|
||||
|
||||
const style = await getData(
|
||||
'style',
|
||||
stylesIndex,
|
||||
userStyleCustomData,
|
||||
userStyleName || instanceStyleName
|
||||
)
|
||||
styleNameUsed = style.nameUsed
|
||||
styleDataUsed = style.dataUsed
|
||||
} else {
|
||||
const theme = await getData(
|
||||
'theme',
|
||||
themesIndex,
|
||||
userThemeV2Source || userThemeV2Snapshot,
|
||||
userThemeV2Name || instanceThemeV2Name
|
||||
)
|
||||
themeNameUsed = theme.nameUsed
|
||||
themeDataUsed = theme.dataUsed
|
||||
|
||||
// Themes v2 editor support
|
||||
commit('setInstanceOption', { name: 'themeData', value: themeDataUsed })
|
||||
}
|
||||
|
||||
console.log('STYLE', styleNameUsed, paletteNameUsed, themeNameUsed)
|
||||
|
||||
// commit('setOption', { name: 'palette', value: paletteNameUsed })
|
||||
// commit('setOption', { name: 'style', value: styleNameUsed })
|
||||
// commit('setOption', { name: 'theme', value: themeNameUsed })
|
||||
|
||||
const paletteIss = (() => {
|
||||
if (!paletteDataUsed) return null
|
||||
const result = {
|
||||
component: 'Root',
|
||||
directives: {}
|
||||
}
|
||||
|
||||
console.log('PALETTE', paletteDataUsed)
|
||||
Object
|
||||
.entries(paletteDataUsed)
|
||||
.filter(([k]) => k !== 'name')
|
||||
.forEach(([k, v]) => {
|
||||
let issRootDirectiveName
|
||||
switch (k) {
|
||||
case 'background':
|
||||
issRootDirectiveName = 'bg'
|
||||
break
|
||||
case 'foreground':
|
||||
issRootDirectiveName = 'fg'
|
||||
break
|
||||
default:
|
||||
issRootDirectiveName = k
|
||||
}
|
||||
result.directives['--' + issRootDirectiveName] = 'color | ' + v
|
||||
})
|
||||
return result
|
||||
})()
|
||||
|
||||
const theme2ruleset = themeDataUsed && convertTheme2To3(normalizeThemeData(themeDataUsed))
|
||||
const hacks = []
|
||||
|
||||
Object.entries(theme3hacks).forEach(([key, value]) => {
|
||||
|
@ -335,19 +585,18 @@ const interfaceMod = {
|
|||
}
|
||||
})
|
||||
|
||||
const ruleset = [
|
||||
...theme2ruleset,
|
||||
...hacks
|
||||
]
|
||||
const rulesetArray = [
|
||||
theme2ruleset,
|
||||
styleDataUsed,
|
||||
paletteIss,
|
||||
hacks
|
||||
].filter(x => x)
|
||||
|
||||
applyTheme(
|
||||
ruleset,
|
||||
return applyTheme(
|
||||
rulesetArray.flat(),
|
||||
() => commit('setThemeApplied'),
|
||||
themeDebug
|
||||
)
|
||||
})
|
||||
|
||||
return promise
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -365,7 +614,7 @@ export const normalizeThemeData = (input) => {
|
|||
themeData.colors.cGreen = input[6]
|
||||
themeData.colors.cBlue = input[7]
|
||||
themeData.colors.cOrange = input[8]
|
||||
return generatePreset(themeData).theme
|
||||
return generatePreset(themeData).source || generatePreset(themeData).theme
|
||||
}
|
||||
|
||||
let themeData, themeSource
|
||||
|
|
|
@ -20,6 +20,7 @@ export const newExporter = ({
|
|||
})
|
||||
|
||||
export const newImporter = ({
|
||||
accept = '.json',
|
||||
onImport,
|
||||
onImportFailure,
|
||||
validator = () => true
|
||||
|
@ -27,18 +28,19 @@ export const newImporter = ({
|
|||
importData () {
|
||||
const filePicker = document.createElement('input')
|
||||
filePicker.setAttribute('type', 'file')
|
||||
filePicker.setAttribute('accept', '.json')
|
||||
filePicker.setAttribute('accept', accept)
|
||||
|
||||
filePicker.addEventListener('change', event => {
|
||||
if (event.target.files[0]) {
|
||||
const filename = event.target.files[0].name
|
||||
// eslint-disable-next-line no-undef
|
||||
const reader = new FileReader()
|
||||
reader.onload = ({ target }) => {
|
||||
try {
|
||||
const parsed = JSON.parse(target.result)
|
||||
const validationResult = validator(parsed)
|
||||
const validationResult = validator(parsed, filename)
|
||||
if (validationResult === true) {
|
||||
onImport(parsed)
|
||||
onImport(parsed, filename)
|
||||
} else {
|
||||
onImportFailure({ validationResult })
|
||||
}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { hex2rgb } from '../color_convert/color_convert.js'
|
||||
import { init, getEngineChecksum } from '../theme_data/theme_data_3.service.js'
|
||||
import { getCssRules } from '../theme_data/css_utils.js'
|
||||
import { defaultState } from '../../modules/config.js'
|
||||
|
@ -43,17 +42,16 @@ const adoptStyleSheets = (styles) => {
|
|||
// is nothing to do here.
|
||||
}
|
||||
|
||||
export const generateTheme = (inputRuleset, callbacks, debug) => {
|
||||
export const generateTheme = async (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 +144,12 @@ export const tryLoadCache = () => {
|
|||
}
|
||||
}
|
||||
|
||||
export const applyTheme = (input, onFinish = (data) => {}, debug) => {
|
||||
export const applyTheme = async (input, onFinish = (data) => {}, debug) => {
|
||||
console.log('INPUT', input)
|
||||
const eagerStyles = createStyleSheet(EAGER_STYLE_ID)
|
||||
const lazyStyles = createStyleSheet(LAZY_STYLE_ID)
|
||||
|
||||
const { lazyProcessFunc } = generateTheme(
|
||||
const { lazyProcessFunc } = await generateTheme(
|
||||
input,
|
||||
{
|
||||
onNewRule (rule, isLazy) {
|
||||
|
@ -169,22 +168,15 @@ export const applyTheme = (input, onFinish = (data) => {}, debug) => {
|
|||
adoptStyleSheets([eagerStyles, lazyStyles])
|
||||
const cache = { engineChecksum: getEngineChecksum(), data: [eagerStyles.rules, lazyStyles.rules] }
|
||||
onFinish(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 = ({
|
||||
|
@ -229,7 +221,7 @@ const extractStyleConfig = ({
|
|||
|
||||
const defaultStyleConfig = extractStyleConfig(defaultState)
|
||||
|
||||
export const applyConfig = (input, i18n) => {
|
||||
export const applyConfig = (input) => {
|
||||
const config = extractStyleConfig(input)
|
||||
|
||||
if (config === defaultStyleConfig) {
|
||||
|
@ -237,6 +229,8 @@ export const applyConfig = (input, i18n) => {
|
|||
}
|
||||
|
||||
const head = document.head
|
||||
const body = document.body
|
||||
body.classList.add('hidden')
|
||||
|
||||
const rules = Object
|
||||
.entries(config)
|
||||
|
@ -257,59 +251,40 @@ export const applyConfig = (input, i18n) => {
|
|||
--roundness: var(--forcedRoundness) !important;
|
||||
}`, 'index-max')
|
||||
}
|
||||
|
||||
body.classList.remove('hidden')
|
||||
}
|
||||
|
||||
export const getThemes = () => {
|
||||
export const getResourcesIndex = async (url) => {
|
||||
const cache = 'no-store'
|
||||
|
||||
return window.fetch('/static/styles.json', { cache })
|
||||
.then((data) => data.json())
|
||||
.then((themes) => {
|
||||
return Object.entries(themes).map(([k, v]) => {
|
||||
let promise = null
|
||||
try {
|
||||
const data = await window.fetch(url, { cache })
|
||||
const resources = await data.json()
|
||||
return Object.fromEntries(
|
||||
Object
|
||||
.entries(resources)
|
||||
.map(([k, v]) => {
|
||||
if (typeof v === 'object') {
|
||||
promise = Promise.resolve(v)
|
||||
return [k, () => Promise.resolve(v)]
|
||||
} else if (typeof v === 'string') {
|
||||
promise = window.fetch(v, { cache })
|
||||
return [
|
||||
k,
|
||||
() => window
|
||||
.fetch(v, { cache })
|
||||
.then((data) => data.json())
|
||||
.catch((e) => {
|
||||
console.error(e)
|
||||
return null
|
||||
})
|
||||
]
|
||||
} else {
|
||||
console.error(`Unknown resource format - ${k} is a ${typeof v}`)
|
||||
return [k, null]
|
||||
}
|
||||
return [k, promise]
|
||||
})
|
||||
})
|
||||
.then((promises) => {
|
||||
return promises
|
||||
.reduce((acc, [k, v]) => {
|
||||
acc[k] = v
|
||||
return acc
|
||||
}, {})
|
||||
})
|
||||
}
|
||||
|
||||
export const getPreset = (val) => {
|
||||
return getThemes()
|
||||
.then((themes) => themes[val] ? themes[val] : themes['pleroma-dark'])
|
||||
.then((theme) => {
|
||||
const isV1 = Array.isArray(theme)
|
||||
const data = isV1 ? {} : theme.theme
|
||||
|
||||
if (isV1) {
|
||||
const bg = hex2rgb(theme[1])
|
||||
const fg = hex2rgb(theme[2])
|
||||
const text = hex2rgb(theme[3])
|
||||
const link = hex2rgb(theme[4])
|
||||
|
||||
const cRed = hex2rgb(theme[5] || '#FF0000')
|
||||
const cGreen = hex2rgb(theme[6] || '#00FF00')
|
||||
const cBlue = hex2rgb(theme[7] || '#0000FF')
|
||||
const cOrange = hex2rgb(theme[8] || '#E3FF00')
|
||||
|
||||
data.colors = { bg, fg, text, link, cRed, cBlue, cGreen, cOrange }
|
||||
)
|
||||
} catch (e) {
|
||||
return Promise.reject(e)
|
||||
}
|
||||
|
||||
return { theme: data, source: theme.source }
|
||||
})
|
||||
}
|
||||
|
|
|
@ -2,25 +2,6 @@ import { convert } from 'chromatism'
|
|||
|
||||
import { hex2rgb, rgba2css } from '../color_convert/color_convert.js'
|
||||
|
||||
export const parseCssShadow = (text) => {
|
||||
const dimensions = /(\d[a-z]*\s?){2,4}/.exec(text)?.[0]
|
||||
const inset = /inset/.exec(text)?.[0]
|
||||
const color = text.replace(dimensions, '').replace(inset, '')
|
||||
|
||||
const [x, y, blur = 0, spread = 0] = dimensions.split(/ /).filter(x => x).map(x => x.trim())
|
||||
const isInset = inset?.trim() === 'inset'
|
||||
const colorString = color.split(/ /).filter(x => x).map(x => x.trim())[0]
|
||||
|
||||
return {
|
||||
x,
|
||||
y,
|
||||
blur,
|
||||
spread,
|
||||
inset: isInset,
|
||||
color: colorString
|
||||
}
|
||||
}
|
||||
|
||||
export const getCssColorString = (color, alpha = 1) => rgba2css({ ...convert(color).rgb, a: alpha })
|
||||
|
||||
export const getCssShadow = (input, usesDropShadow) => {
|
||||
|
@ -84,6 +65,9 @@ export const getCssRules = (rules, debug) => rules.map(rule => {
|
|||
].join(';\n ')
|
||||
}
|
||||
case 'shadow': {
|
||||
if (!rule.dynamicVars.shadow) {
|
||||
return ''
|
||||
}
|
||||
return ' ' + [
|
||||
'--shadow: ' + getCssShadow(rule.dynamicVars.shadow),
|
||||
'--shadowFilter: ' + getCssShadowFilter(rule.dynamicVars.shadow),
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { flattenDeep } from 'lodash'
|
||||
|
||||
const parseShadow = string => {
|
||||
export const parseShadow = string => {
|
||||
const modes = ['_full', 'inset', 'x', 'y', 'blur', 'spread', 'color', 'alpha']
|
||||
const regexPrep = [
|
||||
// inset keyword (optional)
|
||||
|
@ -26,7 +26,12 @@ const parseShadow = string => {
|
|||
const numeric = new Set(['x', 'y', 'blur', 'spread', 'alpha'])
|
||||
const { x, y, blur, spread, alpha, inset, color } = Object.fromEntries(modes.map((mode, i) => {
|
||||
if (numeric.has(mode)) {
|
||||
return [mode, Number(result[i])]
|
||||
const number = Number(result[i])
|
||||
if (Number.isNaN(number)) {
|
||||
if (mode === 'alpha') return [mode, 1]
|
||||
return [mode, 0]
|
||||
}
|
||||
return [mode, number]
|
||||
} else if (mode === 'inset') {
|
||||
return [mode, !!result[i]]
|
||||
} else {
|
||||
|
@ -136,7 +141,7 @@ export const deserialize = (input) => {
|
|||
|
||||
output.directives = Object.fromEntries(content.map(d => {
|
||||
const [property, value] = d.split(':')
|
||||
let realValue = value.trim()
|
||||
let realValue = (value || '').trim()
|
||||
if (property === 'shadow') {
|
||||
if (realValue === 'none') {
|
||||
realValue = []
|
||||
|
|
|
@ -3,7 +3,7 @@ import { alphaBlend, getTextColor, relativeLuminance } from '../color_convert/co
|
|||
|
||||
export const process = (text, functions, { findColor, findShadow }, { dynamicVars, staticVars }) => {
|
||||
const { funcName, argsString } = /\$(?<funcName>\w+)\((?<argsString>[#a-zA-Z0-9-,.'"\s]*)\)/.exec(text).groups
|
||||
const args = argsString.split(/,/g).map(a => a.trim())
|
||||
const args = argsString.split(/ /g).map(a => a.trim())
|
||||
|
||||
const func = functions[funcName]
|
||||
if (args.length < func.argsNeeded) {
|
||||
|
|
|
@ -22,7 +22,7 @@ import {
|
|||
normalizeCombination,
|
||||
findRules
|
||||
} from './iss_utils.js'
|
||||
import { parseCssShadow } from './css_utils.js'
|
||||
import { parseShadow } from './iss_deserializer.js'
|
||||
|
||||
// Ensuring the order of components
|
||||
const components = {
|
||||
|
@ -48,7 +48,7 @@ const findShadow = (shadows, { dynamicVars, staticVars }) => {
|
|||
const variableSlot = variable.substring(2)
|
||||
return findShadow(staticVars[variableSlot], { dynamicVars, staticVars })
|
||||
} else {
|
||||
targetShadow = parseCssShadow(shadow)
|
||||
targetShadow = parseShadow(shadow)
|
||||
}
|
||||
} else {
|
||||
targetShadow = shadow
|
||||
|
@ -172,11 +172,13 @@ export const init = ({
|
|||
ultimateBackgroundColor,
|
||||
debug = false,
|
||||
liteMode = false,
|
||||
editMode = false,
|
||||
onlyNormalState = false,
|
||||
rootComponentName = 'Root'
|
||||
rootComponentName = 'Root',
|
||||
initialStaticVars = {}
|
||||
}) => {
|
||||
if (!inputRuleset) throw new Error('Ruleset is null or undefined!')
|
||||
const staticVars = {}
|
||||
const staticVars = { ...initialStaticVars }
|
||||
const stacked = {}
|
||||
const computed = {}
|
||||
|
||||
|
@ -228,7 +230,14 @@ export const init = ({
|
|||
})
|
||||
.map(({ data }) => data)
|
||||
|
||||
if (!ultimateBackgroundColor) {
|
||||
console.warn('No ultimate background color provided, falling back to panel color')
|
||||
const rootRule = ruleset.findLast((x) => (x.component === 'Root' && x.directives?.['--bg']))
|
||||
ultimateBackgroundColor = rootRule.directives['--bg'].split('|')[1].trim()
|
||||
}
|
||||
|
||||
const virtualComponents = new Set(Object.values(components).filter(c => c.virtual).map(c => c.name))
|
||||
const nonEditableComponents = new Set(Object.values(components).filter(c => c.notEditable).map(c => c.name))
|
||||
|
||||
const processCombination = (combination) => {
|
||||
const selector = ruleToSelector(combination, true)
|
||||
|
@ -238,7 +247,11 @@ export const init = ({
|
|||
const soloSelector = selector.split(/ /g).slice(-1)[0]
|
||||
|
||||
const lowerLevelSelector = parentSelector
|
||||
const lowerLevelBackground = computed[lowerLevelSelector]?.background
|
||||
let lowerLevelBackground = computed[lowerLevelSelector]?.background
|
||||
if (editMode && !lowerLevelBackground) {
|
||||
// FIXME hack for editor until it supports handling component backgrounds
|
||||
lowerLevelBackground = '#00FFFF'
|
||||
}
|
||||
const lowerLevelVirtualDirectives = computed[lowerLevelSelector]?.virtualDirectives
|
||||
const lowerLevelVirtualDirectivesRaw = computed[lowerLevelSelector]?.virtualDirectivesRaw
|
||||
|
||||
|
@ -397,27 +410,27 @@ export const init = ({
|
|||
const dynamicSlots = Object.entries(computedDirectives).filter(([k, v]) => k.startsWith('--'))
|
||||
|
||||
dynamicSlots.forEach(([k, v]) => {
|
||||
const [type, ...value] = v.split('|').map(x => x.trim()) // woah, Extreme!
|
||||
const [type, value] = v.split('|').map(x => x.trim()) // woah, Extreme!
|
||||
switch (type) {
|
||||
case 'color': {
|
||||
const color = findColor(value[0], { dynamicVars, staticVars })
|
||||
const color = findColor(value, { dynamicVars, staticVars })
|
||||
dynamicVars[k] = color
|
||||
if (combination.component === 'Root') {
|
||||
if (combination.component === rootComponentName) {
|
||||
staticVars[k.substring(2)] = color
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'shadow': {
|
||||
const shadow = value
|
||||
const shadow = value.split(/,/g).map(s => s.trim())
|
||||
dynamicVars[k] = shadow
|
||||
if (combination.component === 'Root') {
|
||||
if (combination.component === rootComponentName) {
|
||||
staticVars[k.substring(2)] = shadow
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'generic': {
|
||||
dynamicVars[k] = value
|
||||
if (combination.component === 'Root') {
|
||||
if (combination.component === rootComponentName) {
|
||||
staticVars[k.substring(2)] = value
|
||||
}
|
||||
break
|
||||
|
@ -443,11 +456,15 @@ export const init = ({
|
|||
variants: originalVariants = {}
|
||||
} = component
|
||||
|
||||
const validInnerComponents = (
|
||||
liteMode
|
||||
? (component.validInnerComponentsLite || component.validInnerComponents)
|
||||
: component.validInnerComponents
|
||||
) || []
|
||||
let validInnerComponents
|
||||
if (editMode) {
|
||||
const temp = (component.validInnerComponentsLite || component.validInnerComponents || [])
|
||||
validInnerComponents = temp.filter(c => virtualComponents.has(c) && !nonEditableComponents.has(c))
|
||||
} else if (liteMode) {
|
||||
validInnerComponents = (component.validInnerComponentsLite || component.validInnerComponents || [])
|
||||
} else {
|
||||
validInnerComponents = component.validInnerComponents || []
|
||||
}
|
||||
|
||||
// Normalizing states and variants to always include "normal"
|
||||
const states = { normal: '', ...originalStates }
|
||||
|
|
32
static/palettes/index.json
Normal file
32
static/palettes/index.json
Normal file
|
@ -0,0 +1,32 @@
|
|||
{
|
||||
"pleroma-light": [ "Pleroma Light", "#f2f4f6", "#dbe0e8", "#304055", "#f86f0f", "#d31014", "#0fa00f", "#0095ff", "#ffa500" ],
|
||||
"pleroma-dark": [ "Pleroma Dark", "#121a24", "#182230", "#b9b9ba", "#d8a070", "#d31014", "#0fa00f", "#0095ff", "#ffa500" ],
|
||||
"classic-dark": {
|
||||
"name": "Classic Dark",
|
||||
"background": "#161c20",
|
||||
"foreground": "#282e32",
|
||||
"text": "#b9b9b9",
|
||||
"link": "#baaa9c",
|
||||
"cRed": "#d31014",
|
||||
"cGreen": "#0fa00f",
|
||||
"cBlue": "#0095ff",
|
||||
"cOrange": "#ffa500"
|
||||
},
|
||||
"pleroma-amoled": [ "Pleroma Dark AMOLED", "#000000", "#111111", "#b0b0b1", "#d8a070", "#aa0000", "#0fa00f", "#0095ff", "#d59500"],
|
||||
"tomorrow-night": {
|
||||
"name": "Tomorrow Night",
|
||||
"background": "#1d1f21",
|
||||
"foreground": "#373b41",
|
||||
"link": "#81a2be",
|
||||
"text": "#c5c8c6",
|
||||
"cRed": "#cc6666",
|
||||
"cBlue": "#8abeb7",
|
||||
"cGreen": "#b5bd68",
|
||||
"cOrange": "#de935f",
|
||||
"_cYellow": "#f0c674",
|
||||
"_cPurple": "#b294bb"
|
||||
},
|
||||
"bird": [ "Bird", "#f8fafd", "#e6ecf0", "#14171a", "#0084b8", "#e0245e", "#17bf63", "#1b95e0", "#fab81e"],
|
||||
"ir-black": [ "Ir Black", "#000000", "#242422", "#b5b3aa", "#ff6c60", "#FF6C60", "#A8FF60", "#96CBFE", "#FFFFB6" ],
|
||||
"monokai": [ "Monokai", "#272822", "#383830", "#f8f8f2", "#f92672", "#F92672", "#a6e22e", "#66d9ef", "#f4bf75" ]
|
||||
}
|
|
@ -3,12 +3,6 @@
|
|||
"sigsegv2": [ "SigSeg部", "#003238", "#00616c", "#e8f9fb", "#81ffff", "#ff7b66", "#4ae619", "#00ddff", "#ccef53" ],
|
||||
"pleroma-dark": "/static/themes/pleroma-dark.json",
|
||||
"pleroma-light": "/static/themes/pleroma-light.json",
|
||||
"pleroma-amoled": [ "Pleroma Dark AMOLED", "#000000", "#111111", "#b0b0b1", "#d8a070", "#aa0000", "#0fa00f", "#0095ff", "#d59500"],
|
||||
"classic-dark": [ "Classic Dark", "#161c20", "#282e32", "#b9b9b9", "#baaa9c", "#d31014", "#0fa00f", "#0095ff", "#ffa500" ],
|
||||
"bird": [ "Bird", "#f8fafd", "#e6ecf0", "#14171a", "#0084b8", "#e0245e", "#17bf63", "#1b95e0", "#fab81e"],
|
||||
"ir-black": [ "Ir Black", "#000000", "#242422", "#b5b3aa", "#ff6c60", "#FF6C60", "#A8FF60", "#96CBFE", "#FFFFB6" ],
|
||||
"monokai": [ "Monokai", "#272822", "#383830", "#f8f8f2", "#f92672", "#F92672", "#a6e22e", "#66d9ef", "#f4bf75" ],
|
||||
|
||||
"redmond-xx": "/static/themes/redmond-xx.json",
|
||||
"redmond-xx-se": "/static/themes/redmond-xx-se.json",
|
||||
"redmond-xxi": "/static/themes/redmond-xxi.json",
|
||||
|
|
Loading…
Add table
Reference in a new issue