Merge branch 'feature/theming2' into shigusegubu
* feature/theming2: (24 commits) fixed wrong height for selects better layouting for import-export, error display fixes added keep-colors option separated preview and exported from style_switcher revert that, it's actually used, i'm an idiot removed unused function from color_convert cleanup setColors -> applyTheme. For sanity. Also disabled export because nobody uses it and should not use anyway. fixed v2 setting as default theme separate font control js Fix color fallback order Use console.warn instead of console.log Get rid of mutation_types file, use inline approach. Minor fixes Add fallback color rule. Change english validation error messages Clean up the code Validate name presence on client-side as well Better styling for client-side validation. Add I18n for validation errors. Fix broken ToS link. Fix linter errors Add client validation for registration form ...
This commit is contained in:
commit
4159a6b13c
25 changed files with 684 additions and 1390 deletions
|
@ -31,6 +31,7 @@
|
||||||
"vue-router": "^3.0.1",
|
"vue-router": "^3.0.1",
|
||||||
"vue-template-compiler": "^2.3.4",
|
"vue-template-compiler": "^2.3.4",
|
||||||
"vue-timeago": "^3.1.2",
|
"vue-timeago": "^3.1.2",
|
||||||
|
"vuelidate": "^0.7.4",
|
||||||
"vuex": "^3.0.1",
|
"vuex": "^3.0.1",
|
||||||
"whatwg-fetch": "^2.0.3"
|
"whatwg-fetch": "^2.0.3"
|
||||||
},
|
},
|
||||||
|
|
|
@ -119,7 +119,7 @@ input, textarea, .select {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 29px;
|
height: 28px;
|
||||||
line-height: 16px;
|
line-height: 16px;
|
||||||
hyphens: none;
|
hyphens: none;
|
||||||
|
|
||||||
|
@ -136,7 +136,7 @@ input, textarea, .select {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
color: $fallback--text;
|
color: $fallback--text;
|
||||||
color: var(--text, $fallback--text);
|
color: var(--text, $fallback--text);
|
||||||
line-height: 29px;
|
line-height: 28px;
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
@ -156,7 +156,7 @@ input, textarea, .select {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
height: 29px;
|
height: 28px;
|
||||||
line-height: 16px;
|
line-height: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
87
src/components/export_import/export_import.vue
Normal file
87
src/components/export_import/export_import.vue
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
<template>
|
||||||
|
<div class="import-export-container">
|
||||||
|
<slot name="before"/>
|
||||||
|
<button class="btn" @click="exportData">{{ exportLabel }}</button>
|
||||||
|
<button class="btn" @click="importData">{{ importLabel }}</button>
|
||||||
|
<slot name="afterButtons"/>
|
||||||
|
<p v-if="importFailed" class="alert error">{{ importFailedText }}</p>
|
||||||
|
<slot name="afterError"/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: [
|
||||||
|
'exportObject',
|
||||||
|
'importLabel',
|
||||||
|
'exportLabel',
|
||||||
|
'importFailedText',
|
||||||
|
'validator',
|
||||||
|
'onImport',
|
||||||
|
'onImportFailure'
|
||||||
|
],
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
importFailed: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
exportData () {
|
||||||
|
const stringified = JSON.stringify(this.exportObject) // Pretty-print and indent with 2 spaces
|
||||||
|
|
||||||
|
// Create an invisible link with a data url and simulate a click
|
||||||
|
const e = document.createElement('a')
|
||||||
|
e.setAttribute('download', 'pleroma_theme.json')
|
||||||
|
e.setAttribute('href', 'data:application/json;base64,' + window.btoa(stringified))
|
||||||
|
e.style.display = 'none'
|
||||||
|
|
||||||
|
document.body.appendChild(e)
|
||||||
|
e.click()
|
||||||
|
document.body.removeChild(e)
|
||||||
|
},
|
||||||
|
importData () {
|
||||||
|
this.importFailed = false
|
||||||
|
const filePicker = document.createElement('input')
|
||||||
|
filePicker.setAttribute('type', 'file')
|
||||||
|
filePicker.setAttribute('accept', '.json')
|
||||||
|
|
||||||
|
filePicker.addEventListener('change', event => {
|
||||||
|
if (event.target.files[0]) {
|
||||||
|
// eslint-disable-next-line no-undef
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onload = ({target}) => {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(target.result)
|
||||||
|
const valid = this.validator(parsed)
|
||||||
|
if (valid) {
|
||||||
|
this.onImport(parsed)
|
||||||
|
} else {
|
||||||
|
this.importFailed = true
|
||||||
|
// this.onImportFailure(valid)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// This will happen both if there is a JSON syntax error or the theme is missing components
|
||||||
|
this.importFailed = true
|
||||||
|
// this.onImportFailure(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reader.readAsText(event.target.files[0])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
document.body.appendChild(filePicker)
|
||||||
|
filePicker.click()
|
||||||
|
document.body.removeChild(filePicker)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.import-export-container {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: baseline;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
</style>
|
58
src/components/font_control/font_control.js
Normal file
58
src/components/font_control/font_control.js
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
import { set } from 'vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: [
|
||||||
|
'name', 'label', 'value', 'fallback', 'options', 'no-inherit'
|
||||||
|
],
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
lValue: this.value,
|
||||||
|
availableOptions: [
|
||||||
|
this.noInherit ? '' : 'inherit',
|
||||||
|
'custom',
|
||||||
|
...(this.options || []),
|
||||||
|
'serif',
|
||||||
|
'monospace',
|
||||||
|
'sans-serif'
|
||||||
|
].filter(_ => _)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
beforeUpdate () {
|
||||||
|
this.lValue = this.value
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
present () {
|
||||||
|
return typeof this.lValue !== 'undefined'
|
||||||
|
},
|
||||||
|
dValue () {
|
||||||
|
return this.lValue || this.fallback || {}
|
||||||
|
},
|
||||||
|
family: {
|
||||||
|
get () {
|
||||||
|
return this.dValue.family
|
||||||
|
},
|
||||||
|
set (v) {
|
||||||
|
set(this.lValue, 'family', v)
|
||||||
|
this.$emit('input', this.lValue)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
isCustom () {
|
||||||
|
return this.preset === 'custom'
|
||||||
|
},
|
||||||
|
preset: {
|
||||||
|
get () {
|
||||||
|
if (this.family === 'serif' ||
|
||||||
|
this.family === 'sans-serif' ||
|
||||||
|
this.family === 'monospace' ||
|
||||||
|
this.family === 'inherit') {
|
||||||
|
return this.family
|
||||||
|
} else {
|
||||||
|
return 'custom'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
set (v) {
|
||||||
|
this.family = v === 'custom' ? '' : v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -32,66 +32,7 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script src="./font_control.js" ></script>
|
||||||
import { set } from 'vue'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
props: [
|
|
||||||
'name', 'label', 'value', 'fallback', 'options', 'no-inherit'
|
|
||||||
],
|
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
lValue: this.value,
|
|
||||||
availableOptions: [
|
|
||||||
this.noInherit ? '' : 'inherit',
|
|
||||||
'custom',
|
|
||||||
...(this.options || []),
|
|
||||||
'serif',
|
|
||||||
'monospace',
|
|
||||||
'sans-serif'
|
|
||||||
].filter(_ => _)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
beforeUpdate () {
|
|
||||||
this.lValue = this.value
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
present () {
|
|
||||||
return typeof this.lValue !== 'undefined'
|
|
||||||
},
|
|
||||||
dValue () {
|
|
||||||
return this.lValue || this.fallback || {}
|
|
||||||
},
|
|
||||||
family: {
|
|
||||||
get () {
|
|
||||||
return this.dValue.family
|
|
||||||
},
|
|
||||||
set (v) {
|
|
||||||
set(this.lValue, 'family', v)
|
|
||||||
this.$emit('input', this.lValue)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
isCustom () {
|
|
||||||
return this.preset === 'custom'
|
|
||||||
},
|
|
||||||
preset: {
|
|
||||||
get () {
|
|
||||||
if (this.family === 'serif' ||
|
|
||||||
this.family === 'sans-serif' ||
|
|
||||||
this.family === 'monospace' ||
|
|
||||||
this.family === 'inherit') {
|
|
||||||
return this.family
|
|
||||||
} else {
|
|
||||||
return 'custom'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
set (v) {
|
|
||||||
this.family = v === 'custom' ? '' : v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@import '../../_variables.scss';
|
@import '../../_variables.scss';
|
||||||
|
|
|
@ -2,6 +2,9 @@ const InstanceSpecificPanel = {
|
||||||
computed: {
|
computed: {
|
||||||
instanceSpecificPanelContent () {
|
instanceSpecificPanelContent () {
|
||||||
return this.$store.state.instance.instanceSpecificPanelContent
|
return this.$store.state.instance.instanceSpecificPanelContent
|
||||||
|
},
|
||||||
|
show () {
|
||||||
|
return !this.$store.state.config.hideISP
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="instance-specific-panel">
|
<div v-if="show" class="instance-specific-panel">
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<div v-html="instanceSpecificPanelContent">
|
<div v-html="instanceSpecificPanelContent">
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
|
<label for="interface-language-switcher">
|
||||||
|
{{ $t('settings.interfaceLanguage') }}
|
||||||
|
</label>
|
||||||
<label for="interface-language-switcher" class='select'>
|
<label for="interface-language-switcher" class='select'>
|
||||||
<select id="interface-language-switcher" v-model="language">
|
<select id="interface-language-switcher" v-model="language">
|
||||||
<option v-for="(langCode, i) in languageCodes" :value="langCode">
|
<option v-for="(langCode, i) in languageCodes" :value="langCode">
|
||||||
|
|
|
@ -1,57 +1,61 @@
|
||||||
import oauthApi from '../../services/new_api/oauth.js'
|
import { validationMixin } from 'vuelidate'
|
||||||
|
import { required, sameAs } from 'vuelidate/lib/validators'
|
||||||
|
import { mapActions, mapState } from 'vuex'
|
||||||
|
|
||||||
const registration = {
|
const registration = {
|
||||||
|
mixins: [validationMixin],
|
||||||
data: () => ({
|
data: () => ({
|
||||||
user: {},
|
user: {
|
||||||
error: false,
|
email: '',
|
||||||
registering: false
|
fullname: '',
|
||||||
}),
|
username: '',
|
||||||
created () {
|
password: '',
|
||||||
if ((!this.$store.state.instance.registrationOpen && !this.token) || !!this.$store.state.users.currentUser) {
|
confirm: ''
|
||||||
this.$router.push('/main/all')
|
|
||||||
}
|
}
|
||||||
// Seems like this doesn't work at first page open for some reason
|
}),
|
||||||
if (this.$store.state.instance.registrationOpen && this.token) {
|
validations: {
|
||||||
this.$router.push('/registration')
|
user: {
|
||||||
|
email: { required },
|
||||||
|
username: { required },
|
||||||
|
fullname: { required },
|
||||||
|
password: { required },
|
||||||
|
confirm: {
|
||||||
|
required,
|
||||||
|
sameAsPassword: sameAs('password')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created () {
|
||||||
|
if ((!this.registrationOpen && !this.token) || this.signedIn) {
|
||||||
|
this.$router.push('/main/all')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
termsofservice () { return this.$store.state.instance.tos },
|
token () { return this.$route.params.token },
|
||||||
token () { return this.$route.params.token }
|
...mapState({
|
||||||
|
registrationOpen: (state) => state.instance.registrationOpen,
|
||||||
|
signedIn: (state) => !!state.users.currentUser,
|
||||||
|
isPending: (state) => state.users.signUpPending,
|
||||||
|
serverValidationErrors: (state) => state.users.signUpErrors,
|
||||||
|
termsOfService: (state) => state.instance.tos
|
||||||
|
})
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
submit () {
|
...mapActions(['signUp']),
|
||||||
this.registering = true
|
async submit () {
|
||||||
this.user.nickname = this.user.username
|
this.user.nickname = this.user.username
|
||||||
this.user.token = this.token
|
this.user.token = this.token
|
||||||
this.$store.state.api.backendInteractor.register(this.user).then(
|
|
||||||
(response) => {
|
this.$v.$touch()
|
||||||
if (response.ok) {
|
|
||||||
const data = {
|
if (!this.$v.$invalid) {
|
||||||
oauth: this.$store.state.oauth,
|
try {
|
||||||
instance: this.$store.state.instance.server
|
await this.signUp(this.user)
|
||||||
}
|
|
||||||
oauthApi.getOrCreateApp(data).then((app) => {
|
|
||||||
oauthApi.getTokenWithCredentials(
|
|
||||||
{
|
|
||||||
app,
|
|
||||||
instance: data.instance,
|
|
||||||
username: this.user.username,
|
|
||||||
password: this.user.password})
|
|
||||||
.then((result) => {
|
|
||||||
this.$store.commit('setToken', result.access_token)
|
|
||||||
this.$store.dispatch('loginUser', result.access_token)
|
|
||||||
this.$router.push('/main/friends')
|
this.$router.push('/main/friends')
|
||||||
})
|
} catch (error) {
|
||||||
})
|
console.warn('Registration failed: ' + error)
|
||||||
} else {
|
|
||||||
this.registering = false
|
|
||||||
response.json().then((data) => {
|
|
||||||
this.error = data.error
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,50 +7,90 @@
|
||||||
<form v-on:submit.prevent='submit(user)' class='registration-form'>
|
<form v-on:submit.prevent='submit(user)' class='registration-form'>
|
||||||
<div class='container'>
|
<div class='container'>
|
||||||
<div class='text-fields'>
|
<div class='text-fields'>
|
||||||
<div class='form-group'>
|
<div class='form-group' :class="{ 'form-group--error': $v.user.username.$error }">
|
||||||
<label for='username'>{{$t('login.username')}}</label>
|
<label class='form--label' for='sign-up-username'>{{$t('login.username')}}</label>
|
||||||
<input :disabled="registering" v-model='user.username' class='form-control' id='username' placeholder='e.g. lain'>
|
<input :disabled="isPending" v-model.trim='$v.user.username.$model' class='form-control' id='sign-up-username' placeholder='e.g. lain'>
|
||||||
</div>
|
</div>
|
||||||
<div class='form-group'>
|
<div class="form-error" v-if="$v.user.username.$dirty">
|
||||||
<label for='fullname'>{{$t('registration.fullname')}}</label>
|
<ul>
|
||||||
<input :disabled="registering" v-model='user.fullname' class='form-control' id='fullname' placeholder='e.g. Lain Iwakura'>
|
<li v-if="!$v.user.username.required">
|
||||||
|
<span>{{$t('registration.validations.username_required')}}</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class='form-group'>
|
|
||||||
<label for='email'>{{$t('registration.email')}}</label>
|
<div class='form-group' :class="{ 'form-group--error': $v.user.fullname.$error }">
|
||||||
<input :disabled="registering" v-model='user.email' class='form-control' id='email' type="email">
|
<label class='form--label' for='sign-up-fullname'>{{$t('registration.fullname')}}</label>
|
||||||
|
<input :disabled="isPending" v-model.trim='$v.user.fullname.$model' class='form-control' id='sign-up-fullname' placeholder='e.g. Lain Iwakura'>
|
||||||
</div>
|
</div>
|
||||||
<div class='form-group'>
|
<div class="form-error" v-if="$v.user.fullname.$dirty">
|
||||||
<label for='bio'>{{$t('registration.bio')}}</label>
|
<ul>
|
||||||
<input :disabled="registering" v-model='user.bio' class='form-control' id='bio'>
|
<li v-if="!$v.user.fullname.required">
|
||||||
|
<span>{{$t('registration.validations.fullname_required')}}</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class='form-group'>
|
|
||||||
<label for='password'>{{$t('login.password')}}</label>
|
<div class='form-group' :class="{ 'form-group--error': $v.user.email.$error }">
|
||||||
<input :disabled="registering" v-model='user.password' class='form-control' id='password' type='password'>
|
<label class='form--label' for='email'>{{$t('registration.email')}}</label>
|
||||||
|
<input :disabled="isPending" v-model='$v.user.email.$model' class='form-control' id='email' type="email">
|
||||||
</div>
|
</div>
|
||||||
<div class='form-group'>
|
<div class="form-error" v-if="$v.user.email.$dirty">
|
||||||
<label for='password_confirmation'>{{$t('registration.password_confirm')}}</label>
|
<ul>
|
||||||
<input :disabled="registering" v-model='user.confirm' class='form-control' id='password_confirmation' type='password'>
|
<li v-if="!$v.user.email.required">
|
||||||
|
<span>{{$t('registration.validations.email_required')}}</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<!--
|
|
||||||
<div class='form-group'>
|
<div class='form-group'>
|
||||||
<label for='captcha'>Captcha</label>
|
<label class='form--label' for='bio'>{{$t('registration.bio')}}</label>
|
||||||
<img src='/qvittersimplesecurity/captcha.jpg' alt='captcha' class='captcha'>
|
<input :disabled="isPending" v-model='user.bio' class='form-control' id='bio'>
|
||||||
<input :disabled="registering" v-model='user.captcha' placeholder='Enter captcha' type='test' class='form-control' id='captcha'>
|
|
||||||
</div>
|
</div>
|
||||||
-->
|
|
||||||
|
<div class='form-group' :class="{ 'form-group--error': $v.user.password.$error }">
|
||||||
|
<label class='form--label' for='sign-up-password'>{{$t('login.password')}}</label>
|
||||||
|
<input :disabled="isPending" v-model='user.password' class='form-control' id='sign-up-password' type='password'>
|
||||||
|
</div>
|
||||||
|
<div class="form-error" v-if="$v.user.password.$dirty">
|
||||||
|
<ul>
|
||||||
|
<li v-if="!$v.user.password.required">
|
||||||
|
<span>{{$t('registration.validations.password_required')}}</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class='form-group' :class="{ 'form-group--error': $v.user.confirm.$error }">
|
||||||
|
<label class='form--label' for='sign-up-password-confirmation'>{{$t('registration.password_confirm')}}</label>
|
||||||
|
<input :disabled="isPending" v-model='user.confirm' class='form-control' id='sign-up-password-confirmation' type='password'>
|
||||||
|
</div>
|
||||||
|
<div class="form-error" v-if="$v.user.confirm.$dirty">
|
||||||
|
<ul>
|
||||||
|
<li v-if="!$v.user.confirm.required">
|
||||||
|
<span>{{$t('registration.validations.password_confirmation_required')}}</span>
|
||||||
|
</li>
|
||||||
|
<li v-if="!$v.user.confirm.sameAsPassword">
|
||||||
|
<span>{{$t('registration.validations.password_confirmation_match')}}</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class='form-group' v-if='token' >
|
<div class='form-group' v-if='token' >
|
||||||
<label for='token'>{{$t('registration.token')}}</label>
|
<label for='token'>{{$t('registration.token')}}</label>
|
||||||
<input disabled='true' v-model='token' class='form-control' id='token' type='text'>
|
<input disabled='true' v-model='token' class='form-control' id='token' type='text'>
|
||||||
</div>
|
</div>
|
||||||
<div class='form-group'>
|
<div class='form-group'>
|
||||||
<button :disabled="registering" type='submit' class='btn btn-default'>{{$t('general.submit')}}</button>
|
<button :disabled="isPending" type='submit' class='btn btn-default'>{{$t('general.submit')}}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class='terms-of-service' v-html="termsofservice">
|
|
||||||
|
<div class='terms-of-service' v-html="termsOfService">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="error" class='form-group'>
|
<div v-if="serverValidationErrors.length" class='form-group'>
|
||||||
<div class='alert error'>{{error}}</div>
|
<div class='alert error'>
|
||||||
|
<span v-for="error in serverValidationErrors">{{error}}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
@ -60,6 +100,7 @@
|
||||||
<script src="./registration.js"></script>
|
<script src="./registration.js"></script>
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@import '../../_variables.scss';
|
@import '../../_variables.scss';
|
||||||
|
$validations-cRed: #f04124;
|
||||||
|
|
||||||
.registration-form {
|
.registration-form {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -89,6 +130,55 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: 0.3em 0.0em 0.3em;
|
padding: 0.3em 0.0em 0.3em;
|
||||||
line-height:24px;
|
line-height:24px;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes shakeError {
|
||||||
|
0% {
|
||||||
|
transform: translateX(0); }
|
||||||
|
15% {
|
||||||
|
transform: translateX(0.375rem); }
|
||||||
|
30% {
|
||||||
|
transform: translateX(-0.375rem); }
|
||||||
|
45% {
|
||||||
|
transform: translateX(0.375rem); }
|
||||||
|
60% {
|
||||||
|
transform: translateX(-0.375rem); }
|
||||||
|
75% {
|
||||||
|
transform: translateX(0.375rem); }
|
||||||
|
90% {
|
||||||
|
transform: translateX(-0.375rem); }
|
||||||
|
100% {
|
||||||
|
transform: translateX(0); } }
|
||||||
|
|
||||||
|
.form-group--error {
|
||||||
|
animation-name: shakeError;
|
||||||
|
animation-duration: .6s;
|
||||||
|
animation-timing-function: ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group--error .form--label {
|
||||||
|
color: $validations-cRed;
|
||||||
|
color: var(--cRed, $validations-cRed);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-error {
|
||||||
|
margin-top: -0.7em;
|
||||||
|
text-align: left;
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-error ul {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0 0 0 5px;
|
||||||
|
margin-top: 0;
|
||||||
|
|
||||||
|
li::before {
|
||||||
|
content: "• ";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
form textarea {
|
form textarea {
|
||||||
|
@ -102,8 +192,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
//align-self: flex-start;
|
|
||||||
//width: 10em;
|
|
||||||
margin-top: 0.6em;
|
margin-top: 0.6em;
|
||||||
height: 28px;
|
height: 28px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,7 @@ const settings = {
|
||||||
hideAttachmentsLocal: user.hideAttachments,
|
hideAttachmentsLocal: user.hideAttachments,
|
||||||
hideAttachmentsInConvLocal: user.hideAttachmentsInConv,
|
hideAttachmentsInConvLocal: user.hideAttachmentsInConv,
|
||||||
hideNsfwLocal: user.hideNsfw,
|
hideNsfwLocal: user.hideNsfw,
|
||||||
|
hideISPLocal: user.hideISP,
|
||||||
hidePostStatsLocal: typeof user.hidePostStats === 'undefined'
|
hidePostStatsLocal: typeof user.hidePostStats === 'undefined'
|
||||||
? instance.hidePostStats
|
? instance.hidePostStats
|
||||||
: user.hidePostStats,
|
: user.hidePostStats,
|
||||||
|
@ -83,6 +84,9 @@ const settings = {
|
||||||
hideNsfwLocal (value) {
|
hideNsfwLocal (value) {
|
||||||
this.$store.dispatch('setOption', { name: 'hideNsfw', value })
|
this.$store.dispatch('setOption', { name: 'hideNsfw', value })
|
||||||
},
|
},
|
||||||
|
hideISPLocal (value) {
|
||||||
|
this.$store.dispatch('setOption', { name: 'hideISP', value })
|
||||||
|
},
|
||||||
'notificationVisibilityLocal.likes' (value) {
|
'notificationVisibilityLocal.likes' (value) {
|
||||||
this.$store.dispatch('setOption', { name: 'notificationVisibility', value: this.$store.state.config.notificationVisibility })
|
this.$store.dispatch('setOption', { name: 'notificationVisibility', value: this.$store.state.config.notificationVisibility })
|
||||||
},
|
},
|
||||||
|
|
|
@ -22,8 +22,16 @@
|
||||||
<tab-switcher>
|
<tab-switcher>
|
||||||
<div :label="$t('settings.general')" >
|
<div :label="$t('settings.general')" >
|
||||||
<div class="setting-item">
|
<div class="setting-item">
|
||||||
<h2>{{ $t('settings.interfaceLanguage') }}</h2>
|
<h2>{{ $t('settings.interface') }}</h2>
|
||||||
|
<ul class="setting-list">
|
||||||
|
<li>
|
||||||
<interface-language-switcher />
|
<interface-language-switcher />
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<input type="checkbox" id="hideISP" v-model="hideISPLocal">
|
||||||
|
<label for="hideISP">{{$t('settings.hide_isp')}}</label>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="setting-item">
|
<div class="setting-item">
|
||||||
<h2>{{$t('nav.timeline')}}</h2>
|
<h2>{{$t('nav.timeline')}}</h2>
|
||||||
|
|
78
src/components/style_switcher/preview.vue
Normal file
78
src/components/style_switcher/preview.vue
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
<template>
|
||||||
|
<div class="panel dummy">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<div class="title">
|
||||||
|
{{$t('settings.style.preview.header')}}
|
||||||
|
<span class="badge badge-notification">
|
||||||
|
99
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span class="faint">
|
||||||
|
{{$t('settings.style.preview.header_faint')}}
|
||||||
|
</span>
|
||||||
|
<span class="alert error">
|
||||||
|
{{$t('settings.style.preview.error')}}
|
||||||
|
</span>
|
||||||
|
<button class="btn">
|
||||||
|
{{$t('settings.style.preview.button')}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body theme-preview-content">
|
||||||
|
<div class="post">
|
||||||
|
<div class="avatar">
|
||||||
|
( ͡° ͜ʖ ͡°)
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<h4>
|
||||||
|
{{$t('settings.style.preview.content')}}
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
<i18n path="settings.style.preview.text">
|
||||||
|
<code style="font-family: var(--postCodeFont)">
|
||||||
|
{{$t('settings.style.preview.mono')}}
|
||||||
|
</code>
|
||||||
|
<a style="color: var(--link)">
|
||||||
|
{{$t('settings.style.preview.link')}}
|
||||||
|
</a>
|
||||||
|
</i18n>
|
||||||
|
|
||||||
|
<div class="icons">
|
||||||
|
<i style="color: var(--cBlue)" class="icon-reply"/>
|
||||||
|
<i style="color: var(--cGreen)" class="icon-retweet"/>
|
||||||
|
<i style="color: var(--cOrange)" class="icon-star"/>
|
||||||
|
<i style="color: var(--cRed)" class="icon-cancel"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="after-post">
|
||||||
|
<div class="avatar-alt">
|
||||||
|
:^)
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<i18n path="settings.style.preview.fine_print" tag="span" class="faint">
|
||||||
|
<a style="color: var(--faintLink)">
|
||||||
|
{{$t('settings.style.preview.faint_link')}}
|
||||||
|
</a>
|
||||||
|
</i18n>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="separator"></div>
|
||||||
|
|
||||||
|
<span class="alert error">
|
||||||
|
{{$t('settings.style.preview.error')}}
|
||||||
|
</span>
|
||||||
|
<input :value="$t('settings.style.preview.input')" type="text">
|
||||||
|
|
||||||
|
<div class="actions">
|
||||||
|
<span class="checkbox">
|
||||||
|
<input checked="very yes" type="checkbox" id="preview_checkbox">
|
||||||
|
<label for="preview_checkbox">{{$t('settings.style.preview.checkbox')}}</label>
|
||||||
|
</span>
|
||||||
|
<button class="btn">
|
||||||
|
{{$t('settings.style.preview.button')}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
|
@ -1,6 +1,6 @@
|
||||||
import { rgb2hex, hex2rgb, getContrastRatio, alphaBlend } from '../../services/color_convert/color_convert.js'
|
import { rgb2hex, hex2rgb, getContrastRatio, alphaBlend } from '../../services/color_convert/color_convert.js'
|
||||||
import { set, delete as del } from 'vue'
|
import { set, delete as del } from 'vue'
|
||||||
import { generateColors, generateShadows, generateRadii, generateFonts, composePreset } from '../../services/style_setter/style_setter.js'
|
import { generateColors, generateShadows, generateRadii, generateFonts, composePreset, getThemes } from '../../services/style_setter/style_setter.js'
|
||||||
import ColorInput from '../color_input/color_input.vue'
|
import ColorInput from '../color_input/color_input.vue'
|
||||||
import RangeInput from '../range_input/range_input.vue'
|
import RangeInput from '../range_input/range_input.vue'
|
||||||
import OpacityInput from '../opacity_input/opacity_input.vue'
|
import OpacityInput from '../opacity_input/opacity_input.vue'
|
||||||
|
@ -8,6 +8,8 @@ import ShadowControl from '../shadow_control/shadow_control.vue'
|
||||||
import FontControl from '../font_control/font_control.vue'
|
import FontControl from '../font_control/font_control.vue'
|
||||||
import ContrastRatio from '../contrast_ratio/contrast_ratio.vue'
|
import ContrastRatio from '../contrast_ratio/contrast_ratio.vue'
|
||||||
import TabSwitcher from '../tab_switcher/tab_switcher.jsx'
|
import TabSwitcher from '../tab_switcher/tab_switcher.jsx'
|
||||||
|
import Preview from './preview.vue'
|
||||||
|
import ExportImport from '../export_import/export_import.vue'
|
||||||
|
|
||||||
// List of color values used in v1
|
// List of color values used in v1
|
||||||
const v1OnlyNames = [
|
const v1OnlyNames = [
|
||||||
|
@ -26,7 +28,6 @@ export default {
|
||||||
return {
|
return {
|
||||||
availableStyles: [],
|
availableStyles: [],
|
||||||
selected: this.$store.state.config.theme,
|
selected: this.$store.state.config.theme,
|
||||||
invalidThemeImported: false,
|
|
||||||
|
|
||||||
previewShadows: {},
|
previewShadows: {},
|
||||||
previewColors: {},
|
previewColors: {},
|
||||||
|
@ -37,6 +38,7 @@ export default {
|
||||||
colorsInvalid: true,
|
colorsInvalid: true,
|
||||||
radiiInvalid: true,
|
radiiInvalid: true,
|
||||||
|
|
||||||
|
keepColor: false,
|
||||||
keepShadows: false,
|
keepShadows: false,
|
||||||
keepOpacity: false,
|
keepOpacity: false,
|
||||||
keepRoundness: false,
|
keepRoundness: false,
|
||||||
|
@ -104,33 +106,7 @@ export default {
|
||||||
created () {
|
created () {
|
||||||
const self = this
|
const self = this
|
||||||
|
|
||||||
window.fetch('/static/styles.json')
|
getThemes().then((themesComplete) => {
|
||||||
.then((data) => data.json())
|
|
||||||
.then((themes) => {
|
|
||||||
return Promise.all(Object.entries(themes).map(([k, v]) => {
|
|
||||||
if (typeof v === 'object') {
|
|
||||||
return Promise.resolve([k, v])
|
|
||||||
} else if (typeof v === 'string') {
|
|
||||||
return window.fetch(v)
|
|
||||||
.then((data) => data.json())
|
|
||||||
.then((theme) => {
|
|
||||||
return [k, theme]
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
console.error(e)
|
|
||||||
return []
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
})
|
|
||||||
.then((promises) => {
|
|
||||||
return promises
|
|
||||||
.filter(([k, v]) => v)
|
|
||||||
.reduce((acc, [k, v]) => {
|
|
||||||
acc[k] = v
|
|
||||||
return acc
|
|
||||||
}, {})
|
|
||||||
}).then((themesComplete) => {
|
|
||||||
self.availableStyles = themesComplete
|
self.availableStyles = themesComplete
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
@ -319,6 +295,38 @@ export default {
|
||||||
},
|
},
|
||||||
themeValid () {
|
themeValid () {
|
||||||
return !this.shadowsInvalid && !this.colorsInvalid && !this.radiiInvalid
|
return !this.shadowsInvalid && !this.colorsInvalid && !this.radiiInvalid
|
||||||
|
},
|
||||||
|
exportedTheme () {
|
||||||
|
const saveEverything = (
|
||||||
|
!this.keepFonts &&
|
||||||
|
!this.keepShadows &&
|
||||||
|
!this.keepOpacity &&
|
||||||
|
!this.keepRoundness &&
|
||||||
|
!this.keepColor
|
||||||
|
)
|
||||||
|
|
||||||
|
const theme = {}
|
||||||
|
|
||||||
|
if (this.keepFonts || saveEverything) {
|
||||||
|
theme.fonts = this.fontsLocal
|
||||||
|
}
|
||||||
|
if (this.keepShadows || saveEverything) {
|
||||||
|
theme.shadows = this.shadowsLocal
|
||||||
|
}
|
||||||
|
if (this.keepOpacity || saveEverything) {
|
||||||
|
theme.opacity = this.currentOpacity
|
||||||
|
}
|
||||||
|
if (this.keepColor || saveEverything) {
|
||||||
|
theme.colors = this.currentColors
|
||||||
|
}
|
||||||
|
if (this.keepRoundness || saveEverything) {
|
||||||
|
theme.radii = this.currentRadii
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
// To separate from other random JSON files and possible future theme formats
|
||||||
|
_pleroma_theme_version: 2, theme
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
|
@ -328,86 +336,11 @@ export default {
|
||||||
ContrastRatio,
|
ContrastRatio,
|
||||||
ShadowControl,
|
ShadowControl,
|
||||||
FontControl,
|
FontControl,
|
||||||
TabSwitcher
|
TabSwitcher,
|
||||||
|
Preview,
|
||||||
|
ExportImport
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
exportCurrentTheme () {
|
|
||||||
const saveEverything = !this.keepFonts && !this.keepShadows && !this.keepColors && !this.keepOpacity && !this.keepRoundness
|
|
||||||
const theme = {
|
|
||||||
shadows: this.shadowsLocal,
|
|
||||||
fonts: this.fontsLocal,
|
|
||||||
opacity: this.currentOpacity,
|
|
||||||
colors: this.currentColors,
|
|
||||||
radii: this.currentRadii
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.keepFonts && !saveEverything) {
|
|
||||||
delete theme.fonts
|
|
||||||
}
|
|
||||||
if (!this.keepShadows && !saveEverything) {
|
|
||||||
delete theme.shadows
|
|
||||||
}
|
|
||||||
if (!this.keepOpacity && !saveEverything) {
|
|
||||||
delete theme.opacity
|
|
||||||
}
|
|
||||||
if (!this.keepColors && !saveEverything) {
|
|
||||||
delete theme.colors
|
|
||||||
}
|
|
||||||
if (!this.keepRoundness && !saveEverything) {
|
|
||||||
delete theme.radii
|
|
||||||
}
|
|
||||||
|
|
||||||
const stringified = JSON.stringify({
|
|
||||||
// To separate from other random JSON files and possible future theme formats
|
|
||||||
_pleroma_theme_version: 2, theme
|
|
||||||
}, null, 2) // Pretty-print and indent with 2 spaces
|
|
||||||
|
|
||||||
// Create an invisible link with a data url and simulate a click
|
|
||||||
const e = document.createElement('a')
|
|
||||||
e.setAttribute('download', 'pleroma_theme.json')
|
|
||||||
e.setAttribute('href', 'data:application/json;base64,' + window.btoa(stringified))
|
|
||||||
e.style.display = 'none'
|
|
||||||
|
|
||||||
document.body.appendChild(e)
|
|
||||||
e.click()
|
|
||||||
document.body.removeChild(e)
|
|
||||||
},
|
|
||||||
|
|
||||||
importTheme () {
|
|
||||||
this.invalidThemeImported = false
|
|
||||||
const filePicker = document.createElement('input')
|
|
||||||
filePicker.setAttribute('type', 'file')
|
|
||||||
filePicker.setAttribute('accept', '.json')
|
|
||||||
|
|
||||||
filePicker.addEventListener('change', event => {
|
|
||||||
if (event.target.files[0]) {
|
|
||||||
// eslint-disable-next-line no-undef
|
|
||||||
const reader = new FileReader()
|
|
||||||
reader.onload = ({target}) => {
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(target.result)
|
|
||||||
if (parsed._pleroma_theme_version === 1) {
|
|
||||||
this.normalizeLocalState(parsed, 1)
|
|
||||||
} else if (parsed._pleroma_theme_version === 2) {
|
|
||||||
this.normalizeLocalState(parsed.theme, 2)
|
|
||||||
} else {
|
|
||||||
// A theme from the future, spooky
|
|
||||||
this.invalidThemeImported = true
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// This will happen both if there is a JSON syntax error or the theme is missing components
|
|
||||||
this.invalidThemeImported = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
reader.readAsText(event.target.files[0])
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
document.body.appendChild(filePicker)
|
|
||||||
filePicker.click()
|
|
||||||
document.body.removeChild(filePicker)
|
|
||||||
},
|
|
||||||
|
|
||||||
setCustomTheme () {
|
setCustomTheme () {
|
||||||
this.$store.dispatch('setOption', {
|
this.$store.dispatch('setOption', {
|
||||||
name: 'customTheme',
|
name: 'customTheme',
|
||||||
|
@ -420,7 +353,17 @@ export default {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
onImport (parsed) {
|
||||||
|
if (parsed._pleroma_theme_version === 1) {
|
||||||
|
this.normalizeLocalState(parsed, 1)
|
||||||
|
} else if (parsed._pleroma_theme_version === 2) {
|
||||||
|
this.normalizeLocalState(parsed.theme, 2)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
importValidator (parsed) {
|
||||||
|
const version = parsed._pleroma_theme_version
|
||||||
|
return version >= 1 || version <= 2
|
||||||
|
},
|
||||||
clearAll () {
|
clearAll () {
|
||||||
const state = this.$store.state.config.customTheme
|
const state = this.$store.state.config.customTheme
|
||||||
const version = state.colors ? 2 : 'l1'
|
const version = state.colors ? 2 : 'l1'
|
||||||
|
@ -489,14 +432,13 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(version)
|
|
||||||
|
|
||||||
// Stuff that differs between V1 and V2
|
// Stuff that differs between V1 and V2
|
||||||
if (version === 1) {
|
if (version === 1) {
|
||||||
this.fgColorLocal = rgb2hex(colors.btn)
|
this.fgColorLocal = rgb2hex(colors.btn)
|
||||||
this.textColorLocal = rgb2hex(colors.fg)
|
this.textColorLocal = rgb2hex(colors.fg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!this.keepColor) {
|
||||||
this.clearV1()
|
this.clearV1()
|
||||||
const keys = new Set(version !== 1 ? Object.keys(colors) : [])
|
const keys = new Set(version !== 1 ? Object.keys(colors) : [])
|
||||||
if (version === 1 || version === 'l1') {
|
if (version === 1 || version === 'l1') {
|
||||||
|
@ -512,6 +454,7 @@ export default {
|
||||||
keys.forEach(key => {
|
keys.forEach(key => {
|
||||||
this[key + 'ColorLocal'] = rgb2hex(colors[key])
|
this[key + 'ColorLocal'] = rgb2hex(colors[key])
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.keepRoundness) {
|
if (!this.keepRoundness) {
|
||||||
this.clearRoundness()
|
this.clearRoundness()
|
||||||
|
@ -612,6 +555,7 @@ export default {
|
||||||
this.clearOpacity()
|
this.clearOpacity()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!this.keepColor) {
|
||||||
this.clearV1()
|
this.clearV1()
|
||||||
|
|
||||||
this.bgColorLocal = this.selected[1]
|
this.bgColorLocal = this.selected[1]
|
||||||
|
@ -622,6 +566,7 @@ export default {
|
||||||
this.cGreenColorLocal = this.selected[6]
|
this.cGreenColorLocal = this.selected[6]
|
||||||
this.cBlueColorLocal = this.selected[7]
|
this.cBlueColorLocal = this.selected[7]
|
||||||
this.cOrangeColorLocal = this.selected[8]
|
this.cOrangeColorLocal = this.selected[8]
|
||||||
|
}
|
||||||
} else if (this.selectedVersion >= 2) {
|
} else if (this.selectedVersion >= 2) {
|
||||||
this.normalizeLocalState(this.selected.theme, 2)
|
this.normalizeLocalState(this.selected.theme, 2)
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,11 +54,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.import-warning {
|
|
||||||
color: $fallback--cRed;
|
|
||||||
color: var(--cRed, $fallback--cRed);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-switcher {
|
.tab-switcher {
|
||||||
margin: 0 -1em;
|
margin: 0 -1em;
|
||||||
}
|
}
|
||||||
|
@ -154,8 +149,10 @@
|
||||||
.save-load-options {
|
.save-load-options {
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
margin-top: .5em;
|
margin-top: .5em;
|
||||||
span {
|
justify-content: center;
|
||||||
|
.keep-option {
|
||||||
margin: 0 .5em .5em;
|
margin: 0 .5em .5em;
|
||||||
|
min-width: 25%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -248,6 +245,12 @@
|
||||||
.panel-heading {
|
.panel-heading {
|
||||||
.badge, .alert, .btn, .faint {
|
.badge, .alert, .btn, .faint {
|
||||||
margin-left: 1em;
|
margin-left: 1em;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.faint {
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
min-width: 2em;
|
||||||
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
.flex-spacer {
|
.flex-spacer {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|
|
@ -2,6 +2,14 @@
|
||||||
<div class="style-switcher">
|
<div class="style-switcher">
|
||||||
<div class="presets-container">
|
<div class="presets-container">
|
||||||
<div class="save-load">
|
<div class="save-load">
|
||||||
|
<export-import
|
||||||
|
:exportObject='exportedTheme'
|
||||||
|
:exportLabel='$t("settings.export_theme")'
|
||||||
|
:importLabel='$t("settings.import_theme")'
|
||||||
|
:importFailedText='$t("settings.invalid_theme_imported")'
|
||||||
|
:onImport='onImport'
|
||||||
|
:validator='importValidator'>
|
||||||
|
<template slot="before">
|
||||||
<div class="presets">
|
<div class="presets">
|
||||||
{{$t('settings.presets')}}
|
{{$t('settings.presets')}}
|
||||||
<label for="preset-switcher" class='select'>
|
<label for="preset-switcher" class='select'>
|
||||||
|
@ -18,35 +26,39 @@
|
||||||
<i class="icon-down-open"/>
|
<i class="icon-down-open"/>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="import-export">
|
</template>
|
||||||
<button class="btn" @click="exportCurrentTheme">{{ $t('settings.export_theme') }}</button>
|
</export-import>
|
||||||
<button class="btn" @click="importTheme">{{ $t('settings.import_theme') }}</button>
|
|
||||||
<p v-if="invalidThemeImported" class="import-warning">{{ $t('settings.invalid_theme_imported') }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="save-load-options">
|
<div class="save-load-options">
|
||||||
<span>
|
<span class="keep-option">
|
||||||
|
<input
|
||||||
|
id="keep-color"
|
||||||
|
type="checkbox"
|
||||||
|
v-model="keepColor">
|
||||||
|
<label for="keep-color">{{$t('settings.style.switcher.keep_color')}}</label>
|
||||||
|
</span>
|
||||||
|
<span class="keep-option">
|
||||||
<input
|
<input
|
||||||
id="keep-shadows"
|
id="keep-shadows"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
v-model="keepShadows">
|
v-model="keepShadows">
|
||||||
<label for="keep-shadows">{{$t('settings.style.switcher.keep_shadows')}}</label>
|
<label for="keep-shadows">{{$t('settings.style.switcher.keep_shadows')}}</label>
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span class="keep-option">
|
||||||
<input
|
<input
|
||||||
id="keep-opacity"
|
id="keep-opacity"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
v-model="keepOpacity">
|
v-model="keepOpacity">
|
||||||
<label for="keep-opacity">{{$t('settings.style.switcher.keep_opacity')}}</label>
|
<label for="keep-opacity">{{$t('settings.style.switcher.keep_opacity')}}</label>
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span class="keep-option">
|
||||||
<input
|
<input
|
||||||
id="keep-roundness"
|
id="keep-roundness"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
v-model="keepRoundness">
|
v-model="keepRoundness">
|
||||||
<label for="keep-roundness">{{$t('settings.style.switcher.keep_roundness')}}</label>
|
<label for="keep-roundness">{{$t('settings.style.switcher.keep_roundness')}}</label>
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span class="keep-option">
|
||||||
<input
|
<input
|
||||||
id="keep-fonts"
|
id="keep-fonts"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
|
@ -58,82 +70,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="preview-container">
|
<div class="preview-container">
|
||||||
<div class="panel dummy" :style="previewRules">
|
<preview :style="previewRules"/>
|
||||||
<div class="panel-heading">
|
|
||||||
<div class="title">
|
|
||||||
{{$t('settings.style.preview.header')}}
|
|
||||||
<span class="badge badge-notification">
|
|
||||||
99
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<span class="faint">
|
|
||||||
{{$t('settings.style.preview.header_faint')}}
|
|
||||||
</span>
|
|
||||||
<span class="alert error">
|
|
||||||
{{$t('settings.style.preview.error')}}
|
|
||||||
</span>
|
|
||||||
<button class="btn">
|
|
||||||
{{$t('settings.style.preview.button')}}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="panel-body theme-preview-content">
|
|
||||||
<div class="post">
|
|
||||||
<div class="avatar">
|
|
||||||
( ͡° ͜ʖ ͡°)
|
|
||||||
</div>
|
|
||||||
<div class="content">
|
|
||||||
<h4>
|
|
||||||
{{$t('settings.style.preview.content')}}
|
|
||||||
</h4>
|
|
||||||
|
|
||||||
<i18n path="settings.style.preview.text">
|
|
||||||
<code style="font-family: var(--postCodeFont)">
|
|
||||||
{{$t('settings.style.preview.mono')}}
|
|
||||||
</code>
|
|
||||||
<a style="color: var(--link)">
|
|
||||||
{{$t('settings.style.preview.link')}}
|
|
||||||
</a>
|
|
||||||
</i18n>
|
|
||||||
|
|
||||||
<div class="icons">
|
|
||||||
<i style="color: var(--cBlue)" class="icon-reply"/>
|
|
||||||
<i style="color: var(--cGreen)" class="icon-retweet"/>
|
|
||||||
<i style="color: var(--cOrange)" class="icon-star"/>
|
|
||||||
<i style="color: var(--cRed)" class="icon-cancel"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="after-post">
|
|
||||||
<div class="avatar-alt">
|
|
||||||
:^)
|
|
||||||
</div>
|
|
||||||
<div class="content">
|
|
||||||
<i18n path="settings.style.preview.fine_print" tag="span" class="faint">
|
|
||||||
<a style="color: var(--faintLink)">
|
|
||||||
{{$t('settings.style.preview.faint_link')}}
|
|
||||||
</a>
|
|
||||||
</i18n>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="separator"></div>
|
|
||||||
|
|
||||||
<span class="alert error">
|
|
||||||
{{$t('settings.style.preview.error')}}
|
|
||||||
</span>
|
|
||||||
<input :value="$t('settings.style.preview.input')" type="text">
|
|
||||||
|
|
||||||
<div class="actions">
|
|
||||||
<span class="checkbox">
|
|
||||||
<input checked="very yes" type="checkbox" id="preview_checkbox">
|
|
||||||
<label for="preview_checkbox">{{$t('settings.style.preview.checkbox')}}</label>
|
|
||||||
</span>
|
|
||||||
<button class="btn">
|
|
||||||
{{$t('settings.style.preview.button')}}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<keep-alive>
|
<keep-alive>
|
||||||
|
@ -235,6 +172,7 @@
|
||||||
<OpacityInput name="faintOpacity" v-model="faintOpacityLocal" :fallback="previewTheme.opacity.faint || 0.5"/>
|
<OpacityInput name="faintOpacity" v-model="faintOpacityLocal" :fallback="previewTheme.opacity.faint || 0.5"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div :label="$t('settings.style.radii._tab_label')" class="radius-container">
|
<div :label="$t('settings.style.radii._tab_label')" class="radius-container">
|
||||||
<div class="tab-header">
|
<div class="tab-header">
|
||||||
<p>{{$t('settings.radii_help')}}</p>
|
<p>{{$t('settings.radii_help')}}</p>
|
||||||
|
@ -249,6 +187,7 @@
|
||||||
<RangeInput name="attachmentRadius" :label="$t('settings.attachmentRadius')" v-model="attachmentRadiusLocal" :fallback="previewTheme.radii.attachment" max="50" hardMin="0"/>
|
<RangeInput name="attachmentRadius" :label="$t('settings.attachmentRadius')" v-model="attachmentRadiusLocal" :fallback="previewTheme.radii.attachment" max="50" hardMin="0"/>
|
||||||
<RangeInput name="tooltipRadius" :label="$t('settings.tooltipRadius')" v-model="tooltipRadiusLocal" :fallback="previewTheme.radii.tooltip" max="50" hardMin="0"/>
|
<RangeInput name="tooltipRadius" :label="$t('settings.tooltipRadius')" v-model="tooltipRadiusLocal" :fallback="previewTheme.radii.tooltip" max="50" hardMin="0"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div :label="$t('settings.style.shadows._tab_label')" class="shadow-container">
|
<div :label="$t('settings.style.shadows._tab_label')" class="shadow-container">
|
||||||
<div class="tab-header shadow-selector">
|
<div class="tab-header shadow-selector">
|
||||||
<div class="select-container">
|
<div class="select-container">
|
||||||
|
@ -294,6 +233,7 @@
|
||||||
<p>{{$t('settings.style.shadows.filter_hint.spread_zero')}}</p>
|
<p>{{$t('settings.style.shadows.filter_hint.spread_zero')}}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div :label="$t('settings.style.fonts._tab_label')" class="fonts-container">
|
<div :label="$t('settings.style.fonts._tab_label')" class="fonts-container">
|
||||||
<div class="tab-header">
|
<div class="tab-header">
|
||||||
<p>{{$t('settings.style.fonts.help')}}</p>
|
<p>{{$t('settings.style.fonts.help')}}</p>
|
||||||
|
|
|
@ -11,7 +11,8 @@
|
||||||
position: relative;
|
position: relative;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
overflow: hidden;
|
overflow-y: hidden;
|
||||||
|
overflow-x: auto;
|
||||||
padding-top: 5px;
|
padding-top: 5px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
@ -33,6 +34,7 @@
|
||||||
border-bottom-left-radius: 0;
|
border-bottom-left-radius: 0;
|
||||||
border-bottom-right-radius: 0;
|
border-bottom-right-radius: 0;
|
||||||
padding: 5px 1em 99px;
|
padding: 5px 1em 99px;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
&:not(.active) {
|
&:not(.active) {
|
||||||
z-index: 4;
|
z-index: 4;
|
||||||
|
|
|
@ -72,7 +72,15 @@
|
||||||
"fullname": "Display name",
|
"fullname": "Display name",
|
||||||
"password_confirm": "Password confirmation",
|
"password_confirm": "Password confirmation",
|
||||||
"registration": "Registration",
|
"registration": "Registration",
|
||||||
"token": "Invite token"
|
"token": "Invite token",
|
||||||
|
"validations": {
|
||||||
|
"username_required": "cannot be left blank",
|
||||||
|
"fullname_required": "cannot be left blank",
|
||||||
|
"email_required": "cannot be left blank",
|
||||||
|
"password_required": "cannot be left blank",
|
||||||
|
"password_confirmation_required": "cannot be left blank",
|
||||||
|
"password_confirmation_match": "should be the same as password"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"attachmentRadius": "Attachments",
|
"attachmentRadius": "Attachments",
|
||||||
|
@ -116,6 +124,7 @@
|
||||||
"general": "General",
|
"general": "General",
|
||||||
"hide_attachments_in_convo": "Hide attachments in conversations",
|
"hide_attachments_in_convo": "Hide attachments in conversations",
|
||||||
"hide_attachments_in_tl": "Hide attachments in timeline",
|
"hide_attachments_in_tl": "Hide attachments in timeline",
|
||||||
|
"hide_isp": "Hide instance-specific panel",
|
||||||
"hide_post_stats": "Hide post statistics (e.g. the number of favorites)",
|
"hide_post_stats": "Hide post statistics (e.g. the number of favorites)",
|
||||||
"hide_user_stats": "Hide user statistics (e.g. the number of followers)",
|
"hide_user_stats": "Hide user statistics (e.g. the number of followers)",
|
||||||
"import_followers_from_a_csv_file": "Import follows from a csv file",
|
"import_followers_from_a_csv_file": "Import follows from a csv file",
|
||||||
|
@ -124,6 +133,7 @@
|
||||||
"checkboxRadius": "Checkboxes",
|
"checkboxRadius": "Checkboxes",
|
||||||
"instance_default": "(default: {value})",
|
"instance_default": "(default: {value})",
|
||||||
"instance_default_simple" : "(default)",
|
"instance_default_simple" : "(default)",
|
||||||
|
"interface": "Interface",
|
||||||
"interfaceLanguage": "Interface language",
|
"interfaceLanguage": "Interface language",
|
||||||
"invalid_theme_imported": "The selected file is not a supported Pleroma theme. No changes to your theme were made.",
|
"invalid_theme_imported": "The selected file is not a supported Pleroma theme. No changes to your theme were made.",
|
||||||
"limited_availability": "Unavailable in your browser",
|
"limited_availability": "Unavailable in your browser",
|
||||||
|
@ -181,6 +191,7 @@
|
||||||
},
|
},
|
||||||
"style": {
|
"style": {
|
||||||
"switcher": {
|
"switcher": {
|
||||||
|
"keep_color": "Keep colors",
|
||||||
"keep_shadows": "Keep shadows",
|
"keep_shadows": "Keep shadows",
|
||||||
"keep_opacity": "Keep opacity",
|
"keep_opacity": "Keep opacity",
|
||||||
"keep_roundness": "Keep roundness",
|
"keep_roundness": "Keep roundness",
|
||||||
|
@ -273,7 +284,7 @@
|
||||||
"custom": "Custom"
|
"custom": "Custom"
|
||||||
},
|
},
|
||||||
"preview": {
|
"preview": {
|
||||||
"header": "Preview of header",
|
"header": "Preview",
|
||||||
"content": "Content",
|
"content": "Content",
|
||||||
"error": "Example error",
|
"error": "Example error",
|
||||||
"button": "Button",
|
"button": "Button",
|
||||||
|
|
|
@ -55,7 +55,15 @@
|
||||||
"fullname": "Отображаемое имя",
|
"fullname": "Отображаемое имя",
|
||||||
"password_confirm": "Подтверждение пароля",
|
"password_confirm": "Подтверждение пароля",
|
||||||
"registration": "Регистрация",
|
"registration": "Регистрация",
|
||||||
"token": "Код приглашения"
|
"token": "Код приглашения",
|
||||||
|
"validations": {
|
||||||
|
"username_required": "не должно быть пустым",
|
||||||
|
"fullname_required": "не должно быть пустым",
|
||||||
|
"email_required": "не должен быть пустым",
|
||||||
|
"password_required": "не должен быть пустым",
|
||||||
|
"password_confirmation_required": "не должно быть пустым",
|
||||||
|
"password_confirmation_match": "должно совпадать с паролем"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"attachmentRadius": "Прикреплённые файлы",
|
"attachmentRadius": "Прикреплённые файлы",
|
||||||
|
@ -97,10 +105,12 @@
|
||||||
"general": "Общие",
|
"general": "Общие",
|
||||||
"hide_attachments_in_convo": "Прятать вложения в разговорах",
|
"hide_attachments_in_convo": "Прятать вложения в разговорах",
|
||||||
"hide_attachments_in_tl": "Прятать вложения в ленте",
|
"hide_attachments_in_tl": "Прятать вложения в ленте",
|
||||||
|
"hide_isp": "Скрыть серверную панель",
|
||||||
"import_followers_from_a_csv_file": "Импортировать читаемых из файла .csv",
|
"import_followers_from_a_csv_file": "Импортировать читаемых из файла .csv",
|
||||||
"import_theme": "Загрузить Тему",
|
"import_theme": "Загрузить Тему",
|
||||||
"inputRadius": "Поля ввода",
|
"inputRadius": "Поля ввода",
|
||||||
"checkboxRadius": "Чекбоксы",
|
"checkboxRadius": "Чекбоксы",
|
||||||
|
"interface": "Интерфейс",
|
||||||
"interfaceLanguage": "Язык интерфейса",
|
"interfaceLanguage": "Язык интерфейса",
|
||||||
"limited_availability": "Не доступно в вашем браузере",
|
"limited_availability": "Не доступно в вашем браузере",
|
||||||
"links": "Ссылки",
|
"links": "Ссылки",
|
||||||
|
@ -146,6 +156,7 @@
|
||||||
"user_settings": "Настройки пользователя",
|
"user_settings": "Настройки пользователя",
|
||||||
"style": {
|
"style": {
|
||||||
"switcher": {
|
"switcher": {
|
||||||
|
"keep_color": "Оставить цвета",
|
||||||
"keep_shadows": "Оставить тени",
|
"keep_shadows": "Оставить тени",
|
||||||
"keep_opacity": "Оставить прозрачность",
|
"keep_opacity": "Оставить прозрачность",
|
||||||
"keep_roundness": "Оставить скругление",
|
"keep_roundness": "Оставить скругление",
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { set, delete as del } from 'vue'
|
import { set, delete as del } from 'vue'
|
||||||
import { setPreset, setColors } from '../services/style_setter/style_setter.js'
|
import { setPreset, applyTheme } from '../services/style_setter/style_setter.js'
|
||||||
|
|
||||||
const browserLocale = (window.navigator.language || 'en').split('-')[0]
|
const browserLocale = (window.navigator.language || 'en').split('-')[0]
|
||||||
|
|
||||||
|
@ -57,7 +57,7 @@ const config = {
|
||||||
setPreset(value, commit)
|
setPreset(value, commit)
|
||||||
break
|
break
|
||||||
case 'customTheme':
|
case 'customTheme':
|
||||||
setColors(value, commit)
|
applyTheme(value, commit)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
12
src/modules/errors.js
Normal file
12
src/modules/errors.js
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import { capitalize } from 'lodash'
|
||||||
|
|
||||||
|
export function humanizeErrors (errors) {
|
||||||
|
return Object.entries(errors).reduce((errs, [k, val]) => {
|
||||||
|
let message = val.reduce((acc, message) => {
|
||||||
|
let key = capitalize(k.replace(/_/g, ' '))
|
||||||
|
return acc + [key, message].join(' ') + '. '
|
||||||
|
}, '')
|
||||||
|
return [...errs, message]
|
||||||
|
}, [])
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
|
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
|
||||||
import { compact, map, each, merge } from 'lodash'
|
import { compact, map, each, merge } from 'lodash'
|
||||||
import { set } from 'vue'
|
import { set } from 'vue'
|
||||||
|
import oauthApi from '../services/new_api/oauth'
|
||||||
|
import {humanizeErrors} from './errors'
|
||||||
|
|
||||||
// TODO: Unify with mergeOrAdd in statuses.js
|
// TODO: Unify with mergeOrAdd in statuses.js
|
||||||
export const mergeOrAdd = (arr, obj, item) => {
|
export const mergeOrAdd = (arr, obj, item) => {
|
||||||
|
@ -46,15 +48,28 @@ export const mutations = {
|
||||||
setColor (state, { user: {id}, highlighted }) {
|
setColor (state, { user: {id}, highlighted }) {
|
||||||
const user = state.usersObject[id]
|
const user = state.usersObject[id]
|
||||||
set(user, 'highlight', highlighted)
|
set(user, 'highlight', highlighted)
|
||||||
|
},
|
||||||
|
signUpPending (state) {
|
||||||
|
state.signUpPending = true
|
||||||
|
state.signUpErrors = []
|
||||||
|
},
|
||||||
|
signUpSuccess (state) {
|
||||||
|
state.signUpPending = false
|
||||||
|
},
|
||||||
|
signUpFailure (state, errors) {
|
||||||
|
state.signUpPending = false
|
||||||
|
state.signUpErrors = errors
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const defaultState = {
|
export const defaultState = {
|
||||||
|
loggingIn: false,
|
||||||
lastLoginName: false,
|
lastLoginName: false,
|
||||||
currentUser: false,
|
currentUser: false,
|
||||||
loggingIn: false,
|
|
||||||
users: [],
|
users: [],
|
||||||
usersObject: {}
|
usersObject: {},
|
||||||
|
signUpPending: false,
|
||||||
|
signUpErrors: []
|
||||||
}
|
}
|
||||||
|
|
||||||
const users = {
|
const users = {
|
||||||
|
@ -80,6 +95,34 @@ const users = {
|
||||||
store.commit('setUserForStatus', status)
|
store.commit('setUserForStatus', status)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
async signUp (store, userInfo) {
|
||||||
|
store.commit('signUpPending')
|
||||||
|
|
||||||
|
let rootState = store.rootState
|
||||||
|
|
||||||
|
let response = await rootState.api.backendInteractor.register(userInfo)
|
||||||
|
if (response.ok) {
|
||||||
|
const data = {
|
||||||
|
oauth: rootState.oauth,
|
||||||
|
instance: rootState.instance.server
|
||||||
|
}
|
||||||
|
let app = await oauthApi.getOrCreateApp(data)
|
||||||
|
let result = await oauthApi.getTokenWithCredentials({
|
||||||
|
app,
|
||||||
|
instance: data.instance,
|
||||||
|
username: userInfo.username,
|
||||||
|
password: userInfo.password
|
||||||
|
})
|
||||||
|
store.commit('signUpSuccess')
|
||||||
|
store.commit('setToken', result.access_token)
|
||||||
|
store.dispatch('loginUser', result.access_token)
|
||||||
|
} else {
|
||||||
|
let data = await response.json()
|
||||||
|
let errors = humanizeErrors(JSON.parse(data.error))
|
||||||
|
store.commit('signUpFailure', errors)
|
||||||
|
throw Error(errors)
|
||||||
|
}
|
||||||
|
},
|
||||||
logout (store) {
|
logout (store) {
|
||||||
store.commit('clearCurrentUser')
|
store.commit('clearCurrentUser')
|
||||||
store.commit('setToken', false)
|
store.commit('setToken', false)
|
||||||
|
|
|
@ -113,14 +113,6 @@ const hex2rgb = (hex) => {
|
||||||
} : null
|
} : null
|
||||||
}
|
}
|
||||||
|
|
||||||
const rgbstr2hex = (rgb) => {
|
|
||||||
if (rgb[0] === '#') {
|
|
||||||
return rgb
|
|
||||||
}
|
|
||||||
rgb = rgb.match(/\d+/g)
|
|
||||||
return `#${((Number(rgb[0]) << 16) + (Number(rgb[1]) << 8) + Number(rgb[2])).toString(16)}`
|
|
||||||
}
|
|
||||||
|
|
||||||
const mixrgb = (a, b) => {
|
const mixrgb = (a, b) => {
|
||||||
return Object.keys(a).reduce((acc, k) => {
|
return Object.keys(a).reduce((acc, k) => {
|
||||||
acc[k] = (a[k] + b[k]) / 2
|
acc[k] = (a[k] + b[k]) / 2
|
||||||
|
@ -133,7 +125,6 @@ export {
|
||||||
hex2rgb,
|
hex2rgb,
|
||||||
mixrgb,
|
mixrgb,
|
||||||
invert,
|
invert,
|
||||||
rgbstr2hex,
|
|
||||||
getContrastRatio,
|
getContrastRatio,
|
||||||
alphaBlend
|
alphaBlend
|
||||||
}
|
}
|
||||||
|
|
|
@ -71,7 +71,7 @@ const getTextColor = function (bg, text, preserve) {
|
||||||
return text
|
return text
|
||||||
}
|
}
|
||||||
|
|
||||||
const setColors = (input, commit) => {
|
const applyTheme = (input, commit) => {
|
||||||
const { rules, theme } = generatePreset(input)
|
const { rules, theme } = generatePreset(input)
|
||||||
const head = document.head
|
const head = document.head
|
||||||
const body = document.body
|
const body = document.body
|
||||||
|
@ -449,11 +449,43 @@ const generatePreset = (input) => {
|
||||||
return composePreset(colors, radii, shadows, fonts)
|
return composePreset(colors, radii, shadows, fonts)
|
||||||
}
|
}
|
||||||
|
|
||||||
const setPreset = (val, commit) => {
|
const getThemes = () => {
|
||||||
window.fetch('/static/styles.json')
|
return window.fetch('/static/styles.json')
|
||||||
.then((data) => data.json())
|
.then((data) => data.json())
|
||||||
.then((themes) => {
|
.then((themes) => {
|
||||||
|
return Promise.all(Object.entries(themes).map(([k, v]) => {
|
||||||
|
if (typeof v === 'object') {
|
||||||
|
return Promise.resolve([k, v])
|
||||||
|
} else if (typeof v === 'string') {
|
||||||
|
return window.fetch(v)
|
||||||
|
.then((data) => data.json())
|
||||||
|
.then((theme) => {
|
||||||
|
return [k, theme]
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.error(e)
|
||||||
|
return []
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
.then((promises) => {
|
||||||
|
return promises
|
||||||
|
.filter(([k, v]) => v)
|
||||||
|
.reduce((acc, [k, v]) => {
|
||||||
|
acc[k] = v
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const setPreset = (val, commit) => {
|
||||||
|
getThemes().then((themes) => {
|
||||||
const theme = themes[val] ? themes[val] : themes['pleroma-dark']
|
const theme = themes[val] ? themes[val] : themes['pleroma-dark']
|
||||||
|
const isV1 = Array.isArray(theme)
|
||||||
|
const data = isV1 ? {} : theme.theme
|
||||||
|
|
||||||
|
if (isV1) {
|
||||||
const bgRgb = hex2rgb(theme[1])
|
const bgRgb = hex2rgb(theme[1])
|
||||||
const fgRgb = hex2rgb(theme[2])
|
const fgRgb = hex2rgb(theme[2])
|
||||||
const textRgb = hex2rgb(theme[3])
|
const textRgb = hex2rgb(theme[3])
|
||||||
|
@ -464,7 +496,7 @@ const setPreset = (val, commit) => {
|
||||||
const cBlueRgb = hex2rgb(theme[7] || '#0000FF')
|
const cBlueRgb = hex2rgb(theme[7] || '#0000FF')
|
||||||
const cOrangeRgb = hex2rgb(theme[8] || '#E3FF00')
|
const cOrangeRgb = hex2rgb(theme[8] || '#E3FF00')
|
||||||
|
|
||||||
const colors = {
|
data.colors = {
|
||||||
bg: bgRgb,
|
bg: bgRgb,
|
||||||
fg: fgRgb,
|
fg: fgRgb,
|
||||||
text: textRgb,
|
text: textRgb,
|
||||||
|
@ -474,6 +506,7 @@ const setPreset = (val, commit) => {
|
||||||
cGreen: cGreenRgb,
|
cGreen: cGreenRgb,
|
||||||
cOrange: cOrangeRgb
|
cOrange: cOrangeRgb
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// This is a hack, this function is only called during initial load.
|
// This is a hack, this function is only called during initial load.
|
||||||
// We want to cancel loading the theme from config.json if we're already
|
// We want to cancel loading the theme from config.json if we're already
|
||||||
|
@ -482,7 +515,7 @@ const setPreset = (val, commit) => {
|
||||||
// load config -> set preset -> wait for styles.json to load ->
|
// load config -> set preset -> wait for styles.json to load ->
|
||||||
// load persisted state -> set colors -> styles.json loaded -> set colors
|
// load persisted state -> set colors -> styles.json loaded -> set colors
|
||||||
if (!window.themeLoaded) {
|
if (!window.themeLoaded) {
|
||||||
setColors({ colors }, commit)
|
applyTheme(data, commit)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -490,13 +523,14 @@ const setPreset = (val, commit) => {
|
||||||
export {
|
export {
|
||||||
setStyle,
|
setStyle,
|
||||||
setPreset,
|
setPreset,
|
||||||
setColors,
|
applyTheme,
|
||||||
getTextColor,
|
getTextColor,
|
||||||
generateColors,
|
generateColors,
|
||||||
generateRadii,
|
generateRadii,
|
||||||
generateShadows,
|
generateShadows,
|
||||||
generateFonts,
|
generateFonts,
|
||||||
generatePreset,
|
generatePreset,
|
||||||
|
getThemes,
|
||||||
composePreset,
|
composePreset,
|
||||||
getCssShadow,
|
getCssShadow,
|
||||||
getCssShadowFilter
|
getCssShadowFilter
|
||||||
|
|
Loading…
Add table
Reference in a new issue