Merge branch 'develop' into feature/new-user-routes
This commit is contained in:
commit
2211c533dd
93 changed files with 5123 additions and 894 deletions
|
@ -11,8 +11,9 @@ const Attachment = {
|
|||
],
|
||||
data () {
|
||||
return {
|
||||
nsfwImage,
|
||||
nsfwImage: this.$store.state.config.nsfwCensorImage || nsfwImage,
|
||||
hideNsfwLocal: this.$store.state.config.hideNsfw,
|
||||
preloadImage: this.$store.state.config.preloadImage,
|
||||
loopVideo: this.$store.state.config.loopVideo,
|
||||
showHidden: false,
|
||||
loading: false,
|
||||
|
@ -46,7 +47,7 @@ const Attachment = {
|
|||
}
|
||||
},
|
||||
toggleHidden () {
|
||||
if (this.img) {
|
||||
if (this.img && !this.preloadImage) {
|
||||
if (this.img.onload) {
|
||||
this.img.onload()
|
||||
} else {
|
||||
|
|
|
@ -9,8 +9,7 @@
|
|||
<div class="hider" v-if="nsfw && hideNsfwLocal && !hidden">
|
||||
<a href="#" @click.prevent="toggleHidden()">Hide</a>
|
||||
</div>
|
||||
|
||||
<a v-if="type === 'image' && !hidden" class="image-attachment" :href="attachment.url" target="_blank" :title="attachment.description">
|
||||
<a v-if="type === 'image' && (!hidden || preloadImage)" class="image-attachment" :class="{'hidden': hidden && preloadImage}" :href="attachment.url" target="_blank" :title="attachment.description">
|
||||
<StillImage :class="{'small': isSmall}" referrerpolicy="no-referrer" :mimetype="attachment.mimetype" :src="attachment.large_thumb_url || attachment.url"/>
|
||||
</a>
|
||||
|
||||
|
@ -161,6 +160,10 @@
|
|||
display: flex;
|
||||
flex: 1;
|
||||
|
||||
&.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.still-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
|
|
@ -57,8 +57,8 @@
|
|||
.chat-heading {
|
||||
cursor: pointer;
|
||||
.icon-comment-empty {
|
||||
color: $fallback--fg;
|
||||
color: var(--fg, $fallback--fg);
|
||||
color: $fallback--text;
|
||||
color: var(--text, $fallback--text);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
53
src/components/color_input/color_input.vue
Normal file
53
src/components/color_input/color_input.vue
Normal file
|
@ -0,0 +1,53 @@
|
|||
<template>
|
||||
<div class="color-control style-control" :class="{ disabled: !present || disabled }">
|
||||
<label :for="name" class="label">
|
||||
{{label}}
|
||||
</label>
|
||||
<input
|
||||
v-if="typeof fallback !== 'undefined'"
|
||||
class="opt exlcude-disabled"
|
||||
:id="name + '-o'"
|
||||
type="checkbox"
|
||||
:checked="present"
|
||||
@input="$emit('input', typeof value === 'undefined' ? fallback : undefined)">
|
||||
<label v-if="typeof fallback !== 'undefined'" class="opt-l" :for="name + '-o'"></label>
|
||||
<input
|
||||
:id="name"
|
||||
class="color-input"
|
||||
type="color"
|
||||
:value="value || fallback"
|
||||
:disabled="!present || disabled"
|
||||
@input="$emit('input', $event.target.value)"
|
||||
>
|
||||
<input
|
||||
:id="name + '-t'"
|
||||
class="text-input"
|
||||
type="text"
|
||||
:value="value || fallback"
|
||||
:disabled="!present || disabled"
|
||||
@input="$emit('input', $event.target.value)"
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: [
|
||||
'name', 'label', 'value', 'fallback', 'disabled'
|
||||
],
|
||||
computed: {
|
||||
present () {
|
||||
return typeof this.value !== 'undefined'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.color-control {
|
||||
input.text-input {
|
||||
max-width: 7em;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
69
src/components/contrast_ratio/contrast_ratio.vue
Normal file
69
src/components/contrast_ratio/contrast_ratio.vue
Normal file
|
@ -0,0 +1,69 @@
|
|||
<template>
|
||||
<span v-if="contrast" class="contrast-ratio">
|
||||
<span :title="hint" class="rating">
|
||||
<span v-if="contrast.aaa">
|
||||
<i class="icon-thumbs-up-alt"/>
|
||||
</span>
|
||||
<span v-if="!contrast.aaa && contrast.aa">
|
||||
<i class="icon-adjust"/>
|
||||
</span>
|
||||
<span v-if="!contrast.aaa && !contrast.aa">
|
||||
<i class="icon-attention"/>
|
||||
</span>
|
||||
</span>
|
||||
<span class="rating" v-if="contrast && large" :title="hint_18pt">
|
||||
<span v-if="contrast.laaa">
|
||||
<i class="icon-thumbs-up-alt"/>
|
||||
</span>
|
||||
<span v-if="!contrast.laaa && contrast.laa">
|
||||
<i class="icon-adjust"/>
|
||||
</span>
|
||||
<span v-if="!contrast.laaa && !contrast.laa">
|
||||
<i class="icon-attention"/>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: [
|
||||
'large', 'contrast'
|
||||
],
|
||||
computed: {
|
||||
hint () {
|
||||
const levelVal = this.contrast.aaa ? 'aaa' : (this.contrast.aa ? 'aa' : 'bad')
|
||||
const level = this.$t(`settings.style.common.contrast.level.${levelVal}`)
|
||||
const context = this.$t('settings.style.common.contrast.context.text')
|
||||
const ratio = this.contrast.text
|
||||
return this.$t('settings.style.common.contrast.hint', { level, context, ratio })
|
||||
},
|
||||
hint_18pt () {
|
||||
const levelVal = this.contrast.laaa ? 'aaa' : (this.contrast.laa ? 'aa' : 'bad')
|
||||
const level = this.$t(`settings.style.common.contrast.level.${levelVal}`)
|
||||
const context = this.$t('settings.style.common.contrast.context.18pt')
|
||||
const ratio = this.contrast.text
|
||||
return this.$t('settings.style.common.contrast.hint', { level, context, ratio })
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.contrast-ratio {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
margin-top: -4px;
|
||||
margin-bottom: 5px;
|
||||
|
||||
.label {
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
.rating {
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -14,8 +14,8 @@
|
|||
.icon-cancel,.delete-status {
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
color: var(--cRed, $fallback--cRed);
|
||||
color: $fallback--cRed;
|
||||
color: var(--cRed, $fallback--cRed);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
54
src/components/font_control/font_control.vue
Normal file
54
src/components/font_control/font_control.vue
Normal file
|
@ -0,0 +1,54 @@
|
|||
<template>
|
||||
<div class="font-control style-control" :class="{ custom: isCustom }">
|
||||
<label :for="preset === 'custom' ? name : name + '-font-switcher'" class="label">
|
||||
{{label}}
|
||||
</label>
|
||||
<input
|
||||
v-if="typeof fallback !== 'undefined'"
|
||||
class="opt exlcude-disabled"
|
||||
type="checkbox"
|
||||
:id="name + '-o'"
|
||||
:checked="present"
|
||||
@input="$emit('input', typeof value === 'undefined' ? fallback : undefined)">
|
||||
<label v-if="typeof fallback !== 'undefined'" class="opt-l" :for="name + '-o'"></label>
|
||||
<label :for="name + '-font-switcher'" class="select" :disabled="!present">
|
||||
<select
|
||||
:disabled="!present"
|
||||
v-model="preset"
|
||||
class="font-switcher"
|
||||
:id="name + '-font-switcher'">
|
||||
<option v-for="option in availableOptions" :value="option">
|
||||
{{ option === 'custom' ? $t('settings.style.fonts.custom') : option }}
|
||||
</option>
|
||||
</select>
|
||||
<i class="icon-down-open"/>
|
||||
</label>
|
||||
<input
|
||||
v-if="isCustom"
|
||||
class="custom-font"
|
||||
type="text"
|
||||
:id="name"
|
||||
v-model="family">
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./font_control.js" ></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
.font-control {
|
||||
input.custom-font {
|
||||
min-width: 10em;
|
||||
}
|
||||
&.custom {
|
||||
.select {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
.custom-font {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -2,6 +2,9 @@ const InstanceSpecificPanel = {
|
|||
computed: {
|
||||
instanceSpecificPanelContent () {
|
||||
return this.$store.state.instance.instanceSpecificPanelContent
|
||||
},
|
||||
show () {
|
||||
return !this.$store.state.config.hideISP
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="instance-specific-panel">
|
||||
<div v-if="show" class="instance-specific-panel">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-body">
|
||||
<div v-html="instanceSpecificPanelContent">
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
<template>
|
||||
<div>
|
||||
<label for="interface-language-switcher">
|
||||
{{ $t('settings.interfaceLanguage') }}
|
||||
</label>
|
||||
<label for="interface-language-switcher" class='select'>
|
||||
<select id="interface-language-switcher" v-model="language">
|
||||
<option v-for="(langCode, i) in languageCodes" :value="langCode">
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/* eslint-env browser */
|
||||
import statusPosterService from '../../services/status_poster/status_poster.service.js'
|
||||
import fileSizeFormatService from '../../services/file_size_format/file_size_format.js'
|
||||
|
||||
const mediaUpload = {
|
||||
mounted () {
|
||||
|
@ -21,6 +22,12 @@ const mediaUpload = {
|
|||
uploadFile (file) {
|
||||
const self = this
|
||||
const store = this.$store
|
||||
if (file.size > store.state.instance.uploadlimit) {
|
||||
const filesize = fileSizeFormatService.fileSizeFormat(file.size)
|
||||
const allowedsize = fileSizeFormatService.fileSizeFormat(store.state.instance.uploadlimit)
|
||||
self.$emit('upload-failed', 'file_too_big', {filesize: filesize.num, filesizeunit: filesize.unit, allowedsize: allowedsize.num, allowedsizeunit: allowedsize.unit})
|
||||
return
|
||||
}
|
||||
const formData = new FormData()
|
||||
formData.append('media', file)
|
||||
|
||||
|
@ -32,7 +39,7 @@ const mediaUpload = {
|
|||
self.$emit('uploaded', fileData)
|
||||
self.uploading = false
|
||||
}, (error) => { // eslint-disable-line handle-callback-err
|
||||
self.$emit('upload-failed')
|
||||
self.$emit('upload-failed', 'default')
|
||||
self.uploading = false
|
||||
})
|
||||
},
|
||||
|
|
|
@ -7,11 +7,13 @@ import generateProfileLink from 'src/services/user_profile_link_generator/user_p
|
|||
const Notification = {
|
||||
data () {
|
||||
return {
|
||||
userExpanded: false
|
||||
userExpanded: false,
|
||||
betterShadow: this.$store.state.interface.browserSupport.cssFilter
|
||||
}
|
||||
},
|
||||
props: [
|
||||
'notification'
|
||||
'notification',
|
||||
'activatePanel'
|
||||
],
|
||||
components: {
|
||||
Status, StillImage, UserCardContent
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
<template>
|
||||
<status v-if="notification.type === 'mention'" :compact="true" :statusoid="notification.status"></status>
|
||||
<status :activatePanel="activatePanel" v-if="notification.type === 'mention'" :compact="true" :statusoid="notification.status"></status>
|
||||
<div class="non-mention" :class="[userClass, { highlighted: userStyle }]" :style="[ userStyle ]"v-else>
|
||||
<a class='avatar-container' :href="notification.action.user.statusnet_profile_url" @click.stop.prevent.capture="toggleUserExpanded">
|
||||
<StillImage class='avatar-compact' :src="notification.action.user.profile_image_url_original"/>
|
||||
<StillImage class='avatar-compact' :class="{'better-shadow': betterShadow}" :src="notification.action.user.profile_image_url_original"/>
|
||||
</a>
|
||||
<div class='notification-right'>
|
||||
<div class="usercard notification-usercard" v-if="userExpanded">
|
||||
|
@ -25,15 +25,15 @@
|
|||
<small>{{$t('notifications.followed_you')}}</small>
|
||||
</span>
|
||||
</div>
|
||||
<small class="timeago"><router-link v-if="notification.status" :to="{ name: 'conversation', params: { id: notification.status.id } }"><timeago :since="notification.action.created_at" :auto-update="240"></timeago></router-link></small>
|
||||
<small class="timeago"><router-link @click.native="activatePanel('timeline')" v-if="notification.status" :to="{ name: 'conversation', params: { id: notification.status.id } }"><timeago :since="notification.action.created_at" :auto-update="240"></timeago></router-link></small>
|
||||
</span>
|
||||
<div class="follow-text" v-if="notification.type === 'follow'">
|
||||
<router-link :to="userProfileLink(notification.action.user)">
|
||||
<router-link @click.native="activatePanel('timeline')" :to="userProfileLink(notification.action.user)">
|
||||
@{{notification.action.user.screen_name}}
|
||||
</router-link>
|
||||
</div>
|
||||
<template v-else>
|
||||
<status v-if="notification.status" class="faint" :compact="true" :statusoid="notification.status" :noHeading="true"></status>
|
||||
<status :activatePanel="activatePanel" v-if="notification.status" class="faint" :compact="true" :statusoid="notification.status" :noHeading="true"></status>
|
||||
<div class="broken-favorite" v-else>
|
||||
{{$t('notifications.broken_favorite')}}
|
||||
</div>
|
||||
|
|
|
@ -4,6 +4,7 @@ import notificationsFetcher from '../../services/notifications_fetcher/notificat
|
|||
import { sortBy, filter } from 'lodash'
|
||||
|
||||
const Notifications = {
|
||||
props: [ 'activatePanel' ],
|
||||
created () {
|
||||
const store = this.$store
|
||||
const credentials = store.state.users.currentUser.credentials
|
||||
|
|
|
@ -4,31 +4,28 @@
|
|||
// a bit of a hack to allow scrolling below notifications
|
||||
padding-bottom: 15em;
|
||||
|
||||
.unseen-count {
|
||||
display: inline-block;
|
||||
background-color: $fallback--cRed;
|
||||
background-color: var(--cRed, $fallback--cRed);
|
||||
text-shadow: 0px 0px 3px rgba(0, 0, 0, 0.5);
|
||||
border-radius: 99px;
|
||||
min-width: 22px;
|
||||
max-width: 22px;
|
||||
min-height: 22px;
|
||||
max-height: 22px;
|
||||
color: white;
|
||||
font-size: 15px;
|
||||
line-height: 22px;
|
||||
text-align: center;
|
||||
vertical-align: middle
|
||||
}
|
||||
|
||||
.loadmore-error {
|
||||
color: $fallback--fg;
|
||||
color: var(--fg, $fallback--fg);
|
||||
color: $fallback--text;
|
||||
color: var(--text, $fallback--text);
|
||||
}
|
||||
|
||||
.unseen {
|
||||
box-shadow: inset 4px 0 0 var(--cRed, $fallback--cRed);
|
||||
padding-left: 0;
|
||||
.notification {
|
||||
position: relative;
|
||||
|
||||
.notification-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&.unseen {
|
||||
.notification-overlay {
|
||||
background-image: linear-gradient(135deg, var(--badgeNotification, $fallback--cRed) 4px, transparent 10px)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -42,21 +39,27 @@
|
|||
.broken-favorite {
|
||||
border-radius: $fallback--tooltipRadius;
|
||||
border-radius: var(--tooltipRadius, $fallback--tooltipRadius);
|
||||
color: $fallback--faint;
|
||||
color: var(--faint, $fallback--faint);
|
||||
background-color: $fallback--cAlertRed;
|
||||
background-color: var(--cAlertRed, $fallback--cAlertRed);
|
||||
color: $fallback--text;
|
||||
color: var(--alertErrorText, $fallback--text);
|
||||
background-color: $fallback--alertError;
|
||||
background-color: var(--alertError, $fallback--alertError);
|
||||
padding: 2px .5em
|
||||
}
|
||||
|
||||
.avatar-compact {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
box-shadow: var(--avatarStatusShadow);
|
||||
border-radius: $fallback--avatarAltRadius;
|
||||
border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);
|
||||
overflow: hidden;
|
||||
line-height: 0;
|
||||
|
||||
&.better-shadow {
|
||||
box-shadow: var(--avatarStatusShadowInset);
|
||||
filter: var(--avatarStatusShadowFilter)
|
||||
}
|
||||
|
||||
&.animated::before {
|
||||
display: none;
|
||||
}
|
||||
|
@ -90,6 +93,9 @@
|
|||
padding: 0.25em 0;
|
||||
color: $fallback--faint;
|
||||
color: var(--faint, $fallback--faint);
|
||||
a {
|
||||
color: var(--faintLink);
|
||||
}
|
||||
}
|
||||
padding: 0;
|
||||
.media-body {
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<div class="panel-heading">
|
||||
<div class="title">
|
||||
{{$t('notifications.notifications')}}
|
||||
<span class="unseen-count" v-if="unseenCount">{{unseenCount}}</span>
|
||||
<span class="badge badge-notification unseen-count" v-if="unseenCount">{{unseenCount}}</span>
|
||||
</div>
|
||||
<div @click.prevent class="loadmore-error alert error" v-if="error">
|
||||
{{$t('timeline.error_fetching')}}
|
||||
|
@ -13,7 +13,8 @@
|
|||
</div>
|
||||
<div class="panel-body">
|
||||
<div v-for="notification in visibleNotifications" :key="notification.action.id" class="notification" :class='{"unseen": !notification.seen}'>
|
||||
<notification :notification="notification"></notification>
|
||||
<div class="notification-overlay"></div>
|
||||
<notification :activatePanel="activatePanel" :notification="notification"></notification>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-footer">
|
||||
|
|
38
src/components/opacity_input/opacity_input.vue
Normal file
38
src/components/opacity_input/opacity_input.vue
Normal file
|
@ -0,0 +1,38 @@
|
|||
<template>
|
||||
<div class="opacity-control style-control" :class="{ disabled: !present || disabled }">
|
||||
<label :for="name" class="label">
|
||||
{{$t('settings.style.common.opacity')}}
|
||||
</label>
|
||||
<input
|
||||
v-if="typeof fallback !== 'undefined'"
|
||||
class="opt exclude-disabled"
|
||||
:id="name + '-o'"
|
||||
type="checkbox"
|
||||
:checked="present"
|
||||
@input="$emit('input', !present ? fallback : undefined)">
|
||||
<label v-if="typeof fallback !== 'undefined'" class="opt-l" :for="name + '-o'"></label>
|
||||
<input
|
||||
:id="name"
|
||||
class="input-number"
|
||||
type="number"
|
||||
:value="value || fallback"
|
||||
:disabled="!present || disabled"
|
||||
@input="$emit('input', $event.target.value)"
|
||||
max="1"
|
||||
min="0"
|
||||
step=".05">
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: [
|
||||
'name', 'value', 'fallback', 'disabled'
|
||||
],
|
||||
computed: {
|
||||
present () {
|
||||
return typeof this.value !== 'undefined'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -46,7 +46,7 @@ const PostStatusForm = {
|
|||
statusText = buildMentionsString({ user: this.repliedUser, attentions: this.attentions }, currentUser)
|
||||
}
|
||||
|
||||
const scope = (this.copyMessageScope && this.$store.state.config.copyScope || this.copyMessageScope === 'direct')
|
||||
const scope = (this.copyMessageScope && this.$store.state.config.scopeCopy || this.copyMessageScope === 'direct')
|
||||
? this.copyMessageScope
|
||||
: this.$store.state.users.currentUser.default_scope
|
||||
|
||||
|
@ -262,6 +262,11 @@ const PostStatusForm = {
|
|||
let index = this.newStatus.files.indexOf(fileInfo)
|
||||
this.newStatus.files.splice(index, 1)
|
||||
},
|
||||
uploadFailed (errString, templateArgs) {
|
||||
templateArgs = templateArgs || {}
|
||||
this.error = this.$t('upload.error.base') + ' ' + this.$t('upload.error.' + errString, templateArgs)
|
||||
this.enableSubmit()
|
||||
},
|
||||
disableSubmit () {
|
||||
this.submitDisabled = true
|
||||
},
|
||||
|
|
|
@ -64,7 +64,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class='form-bottom'>
|
||||
<media-upload @uploading="disableSubmit" @uploaded="addMediaFile" @upload-failed="enableSubmit" :drop-files="dropFiles"></media-upload>
|
||||
<media-upload @uploading="disableSubmit" @uploaded="addMediaFile" @upload-failed="uploadFailed" :drop-files="dropFiles"></media-upload>
|
||||
|
||||
<p v-if="isOverLengthLimit" class="error">{{ charactersLeft }}</p>
|
||||
<p class="faint" v-else-if="hasStatusLengthLimit">{{ charactersLeft }}</p>
|
||||
|
@ -153,8 +153,8 @@
|
|||
padding-bottom: 0;
|
||||
margin-left: $fallback--attachmentRadius;
|
||||
margin-left: var(--attachmentRadius, $fallback--attachmentRadius);
|
||||
background-color: $fallback--btn;
|
||||
background-color: var(--btn, $fallback--btn);
|
||||
background-color: $fallback--fg;
|
||||
background-color: var(--btn, $fallback--fg);
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
@ -258,11 +258,13 @@
|
|||
position: absolute;
|
||||
z-index: 1;
|
||||
box-shadow: 1px 2px 4px rgba(0, 0, 0, 0.5);
|
||||
// this doesn't match original but i don't care, making it uniform.
|
||||
box-shadow: var(--popupShadow);
|
||||
min-width: 75%;
|
||||
background: $fallback--bg;
|
||||
background: var(--bg, $fallback--bg);
|
||||
color: $fallback--lightFg;
|
||||
color: var(--lightFg, $fallback--lightFg);
|
||||
color: $fallback--lightText;
|
||||
color: var(--lightText, $fallback--lightText);
|
||||
}
|
||||
|
||||
.autocomplete {
|
||||
|
@ -291,8 +293,8 @@
|
|||
}
|
||||
|
||||
&.highlighted {
|
||||
background-color: $fallback--btn;
|
||||
background-color: var(--btn, $fallback--btn);
|
||||
background-color: $fallback--fg;
|
||||
background-color: var(--lightBg, $fallback--fg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
48
src/components/range_input/range_input.vue
Normal file
48
src/components/range_input/range_input.vue
Normal file
|
@ -0,0 +1,48 @@
|
|||
<template>
|
||||
<div class="range-control style-control" :class="{ disabled: !present || disabled }">
|
||||
<label :for="name" class="label">
|
||||
{{label}}
|
||||
</label>
|
||||
<input
|
||||
v-if="typeof fallback !== 'undefined'"
|
||||
class="opt exclude-disabled"
|
||||
:id="name + '-o'"
|
||||
type="checkbox"
|
||||
:checked="present"
|
||||
@input="$emit('input', !present ? fallback : undefined)">
|
||||
<label v-if="typeof fallback !== 'undefined'" class="opt-l" :for="name + '-o'"></label>
|
||||
<input
|
||||
:id="name"
|
||||
class="input-number"
|
||||
type="range"
|
||||
:value="value || fallback"
|
||||
:disabled="!present || disabled"
|
||||
@input="$emit('input', $event.target.value)"
|
||||
:max="max || hardMax || 100"
|
||||
:min="min || hardMin || 0"
|
||||
:step="step || 1">
|
||||
<input
|
||||
:id="name"
|
||||
class="input-number"
|
||||
type="number"
|
||||
:value="value || fallback"
|
||||
:disabled="!present || disabled"
|
||||
@input="$emit('input', $event.target.value)"
|
||||
:max="hardMax"
|
||||
:min="hardMin"
|
||||
:step="step || 1">
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: [
|
||||
'name', 'value', 'fallback', 'disabled', 'label', 'max', 'min', 'step', 'hardMin', 'hardMax'
|
||||
],
|
||||
computed: {
|
||||
present () {
|
||||
return typeof this.value !== 'undefined'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -13,6 +13,8 @@ const settings = {
|
|||
hideAttachmentsLocal: user.hideAttachments,
|
||||
hideAttachmentsInConvLocal: user.hideAttachmentsInConv,
|
||||
hideNsfwLocal: user.hideNsfw,
|
||||
hideISPLocal: user.hideISP,
|
||||
preloadImage: user.preloadImage,
|
||||
hidePostStatsLocal: typeof user.hidePostStats === 'undefined'
|
||||
? instance.hidePostStats
|
||||
: user.hidePostStats,
|
||||
|
@ -45,6 +47,7 @@ const settings = {
|
|||
scopeCopyLocal: user.scopeCopy,
|
||||
scopeCopyDefault: this.$t('settings.values.' + instance.scopeCopy),
|
||||
stopGifs: user.stopGifs,
|
||||
webPushNotificationsLocal: user.webPushNotifications,
|
||||
loopSilentAvailable:
|
||||
// Firefox
|
||||
Object.getOwnPropertyDescriptor(HTMLVideoElement.prototype, 'mozHasAudio') ||
|
||||
|
@ -83,6 +86,12 @@ const settings = {
|
|||
hideNsfwLocal (value) {
|
||||
this.$store.dispatch('setOption', { name: 'hideNsfw', value })
|
||||
},
|
||||
preloadImage (value) {
|
||||
this.$store.dispatch('setOption', { name: 'preloadImage', value })
|
||||
},
|
||||
hideISPLocal (value) {
|
||||
this.$store.dispatch('setOption', { name: 'hideISP', value })
|
||||
},
|
||||
'notificationVisibilityLocal.likes' (value) {
|
||||
this.$store.dispatch('setOption', { name: 'notificationVisibility', value: this.$store.state.config.notificationVisibility })
|
||||
},
|
||||
|
@ -134,6 +143,10 @@ const settings = {
|
|||
},
|
||||
stopGifs (value) {
|
||||
this.$store.dispatch('setOption', { name: 'stopGifs', value })
|
||||
},
|
||||
webPushNotificationsLocal (value) {
|
||||
this.$store.dispatch('setOption', { name: 'webPushNotifications', value })
|
||||
if (value) this.$store.dispatch('registerPushNotifications')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,15 +14,24 @@
|
|||
<div @click.prevent class="alert transparent" v-if="!currentSaveStateNotice.error">
|
||||
{{ $t('settings.saving_ok') }}
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</transition>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<keep-alive>
|
||||
<tab-switcher>
|
||||
<div :label="$t('settings.general')" >
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.interfaceLanguage') }}</h2>
|
||||
<interface-language-switcher />
|
||||
<h2>{{ $t('settings.interface') }}</h2>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<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 class="setting-item">
|
||||
<h2>{{$t('nav.timeline')}}</h2>
|
||||
|
@ -109,6 +118,12 @@
|
|||
<input type="checkbox" id="hideNsfw" v-model="hideNsfwLocal">
|
||||
<label for="hideNsfw">{{$t('settings.nsfw_clickthrough')}}</label>
|
||||
</li>
|
||||
<ul class="setting-list suboptions" >
|
||||
<li>
|
||||
<input :disabled="!hideNsfwLocal" type="checkbox" id="preloadImage" v-model="preloadImage">
|
||||
<label for="preloadImage">{{$t('settings.preload_images')}}</label>
|
||||
</li>
|
||||
</ul>
|
||||
<li>
|
||||
<input type="checkbox" id="stopGifs" v-model="stopGifs">
|
||||
<label for="stopGifs">{{$t('settings.stop_gifs')}}</label>
|
||||
|
@ -128,6 +143,18 @@
|
|||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<h2>{{$t('settings.notifications')}}</h2>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<input type="checkbox" id="webPushNotifications" v-model="webPushNotificationsLocal">
|
||||
<label for="webPushNotifications">
|
||||
{{$t('settings.enable_web_push_notifications')}}
|
||||
</label>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div :label="$t('settings.theme')" >
|
||||
|
@ -199,6 +226,7 @@
|
|||
</div>
|
||||
|
||||
</tab-switcher>
|
||||
</keep-alive>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -210,7 +238,7 @@
|
|||
@import '../../_variables.scss';
|
||||
|
||||
.setting-item {
|
||||
border-bottom: 2px solid var(--btn, $fallback--btn);
|
||||
border-bottom: 2px solid var(--fg, $fallback--fg);
|
||||
margin: 1em 1em 1.4em;
|
||||
padding-bottom: 1.4em;
|
||||
|
||||
|
@ -259,12 +287,8 @@
|
|||
|
||||
.btn {
|
||||
min-height: 28px;
|
||||
}
|
||||
|
||||
.submit {
|
||||
margin-top: 1em;
|
||||
min-height: 30px;
|
||||
width: 10em;
|
||||
min-width: 10em;
|
||||
padding: 0 2em;
|
||||
}
|
||||
}
|
||||
.select-multiple {
|
||||
|
|
87
src/components/shadow_control/shadow_control.js
Normal file
87
src/components/shadow_control/shadow_control.js
Normal file
|
@ -0,0 +1,87 @@
|
|||
import ColorInput from '../color_input/color_input.vue'
|
||||
import OpacityInput from '../opacity_input/opacity_input.vue'
|
||||
import { getCssShadow } from '../../services/style_setter/style_setter.js'
|
||||
import { hex2rgb } from '../../services/color_convert/color_convert.js'
|
||||
|
||||
export default {
|
||||
// 'Value' and 'Fallback' can be undefined, but if they are
|
||||
// initially vue won't detect it when they become something else
|
||||
// therefore i'm using "ready" which should be passed as true when
|
||||
// data becomes available
|
||||
props: [
|
||||
'value', 'fallback', 'ready'
|
||||
],
|
||||
data () {
|
||||
return {
|
||||
selectedId: 0,
|
||||
// TODO there are some bugs regarding display of array (it's not getting updated when deleting for some reason)
|
||||
cValue: this.value || this.fallback || []
|
||||
}
|
||||
},
|
||||
components: {
|
||||
ColorInput,
|
||||
OpacityInput
|
||||
},
|
||||
methods: {
|
||||
add () {
|
||||
this.cValue.push(Object.assign({}, this.selected))
|
||||
this.selectedId = this.cValue.length - 1
|
||||
},
|
||||
del () {
|
||||
this.cValue.splice(this.selectedId, 1)
|
||||
this.selectedId = this.cValue.length === 0 ? undefined : this.selectedId - 1
|
||||
},
|
||||
moveUp () {
|
||||
const movable = this.cValue.splice(this.selectedId, 1)[0]
|
||||
this.cValue.splice(this.selectedId - 1, 0, movable)
|
||||
this.selectedId -= 1
|
||||
},
|
||||
moveDn () {
|
||||
const movable = this.cValue.splice(this.selectedId, 1)[0]
|
||||
this.cValue.splice(this.selectedId + 1, 0, movable)
|
||||
this.selectedId += 1
|
||||
}
|
||||
},
|
||||
beforeUpdate () {
|
||||
this.cValue = this.value || this.fallback
|
||||
},
|
||||
computed: {
|
||||
selected () {
|
||||
if (this.ready && this.cValue.length > 0) {
|
||||
return this.cValue[this.selectedId]
|
||||
} else {
|
||||
return {
|
||||
x: 0,
|
||||
y: 0,
|
||||
blur: 0,
|
||||
spread: 0,
|
||||
inset: false,
|
||||
color: '#000000',
|
||||
alpha: 1
|
||||
}
|
||||
}
|
||||
},
|
||||
moveUpValid () {
|
||||
return this.ready && this.selectedId > 0
|
||||
},
|
||||
moveDnValid () {
|
||||
return this.ready && this.selectedId < this.cValue.length - 1
|
||||
},
|
||||
present () {
|
||||
return this.ready &&
|
||||
typeof this.cValue[this.selectedId] !== 'undefined' &&
|
||||
!this.usingFallback
|
||||
},
|
||||
usingFallback () {
|
||||
return typeof this.value === 'undefined'
|
||||
},
|
||||
rgb () {
|
||||
return hex2rgb(this.selected.color)
|
||||
},
|
||||
style () {
|
||||
return this.ready ? {
|
||||
boxShadow: getCssShadow(this.cValue)
|
||||
} : {}
|
||||
}
|
||||
}
|
||||
}
|
243
src/components/shadow_control/shadow_control.vue
Normal file
243
src/components/shadow_control/shadow_control.vue
Normal file
|
@ -0,0 +1,243 @@
|
|||
<template>
|
||||
<div class="shadow-control" :class="{ disabled: !present }">
|
||||
<div class="shadow-preview-container">
|
||||
<div :disabled="!present" class="y-shift-control">
|
||||
<input
|
||||
v-model="selected.y"
|
||||
:disabled="!present"
|
||||
class="input-number"
|
||||
type="number">
|
||||
<div class="wrap">
|
||||
<input
|
||||
v-model="selected.y"
|
||||
:disabled="!present"
|
||||
class="input-range"
|
||||
type="range"
|
||||
max="20"
|
||||
min="-20">
|
||||
</div>
|
||||
</div>
|
||||
<div class="preview-window">
|
||||
<div class="preview-block" :style="style"></div>
|
||||
</div>
|
||||
<div :disabled="!present" class="x-shift-control">
|
||||
<input
|
||||
v-model="selected.x"
|
||||
:disabled="!present"
|
||||
class="input-number"
|
||||
type="number">
|
||||
<div class="wrap">
|
||||
<input
|
||||
v-model="selected.x"
|
||||
:disabled="!present"
|
||||
class="input-range"
|
||||
type="range"
|
||||
max="20"
|
||||
min="-20">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="shadow-tweak">
|
||||
<div :disabled="usingFallback" class="id-control style-control">
|
||||
<label for="shadow-switcher" class="select" :disabled="!ready || usingFallback">
|
||||
<select
|
||||
v-model="selectedId" class="shadow-switcher"
|
||||
:disabled="!ready || usingFallback"
|
||||
id="shadow-switcher">
|
||||
<option v-for="(shadow, index) in cValue" :value="index">
|
||||
{{$t('settings.style.shadows.shadow_id', { value: index })}}
|
||||
</option>
|
||||
</select>
|
||||
<i class="icon-down-open"/>
|
||||
</label>
|
||||
<button class="btn btn-default" :disabled="!ready || !present" @click="del">
|
||||
<i class="icon-cancel"/>
|
||||
</button>
|
||||
<button class="btn btn-default" :disabled="!moveUpValid" @click="moveUp">
|
||||
<i class="icon-up-open"/>
|
||||
</button>
|
||||
<button class="btn btn-default" :disabled="!moveDnValid" @click="moveDn">
|
||||
<i class="icon-down-open"/>
|
||||
</button>
|
||||
<button class="btn btn-default" :disabled="usingFallback" @click="add">
|
||||
<i class="icon-plus"/>
|
||||
</button>
|
||||
</div>
|
||||
<div :disabled="!present" class="inset-control style-control">
|
||||
<label for="inset" class="label">
|
||||
{{$t('settings.style.shadows.inset')}}
|
||||
</label>
|
||||
<input
|
||||
v-model="selected.inset"
|
||||
:disabled="!present"
|
||||
name="inset"
|
||||
id="inset"
|
||||
class="input-inset"
|
||||
type="checkbox">
|
||||
<label class="checkbox-label" for="inset"></label>
|
||||
</div>
|
||||
<div :disabled="!present" class="blur-control style-control">
|
||||
<label for="spread" class="label">
|
||||
{{$t('settings.style.shadows.blur')}}
|
||||
</label>
|
||||
<input
|
||||
v-model="selected.blur"
|
||||
:disabled="!present"
|
||||
name="blur"
|
||||
id="blur"
|
||||
class="input-range"
|
||||
type="range"
|
||||
max="20"
|
||||
min="0">
|
||||
<input
|
||||
v-model="selected.blur"
|
||||
:disabled="!present"
|
||||
class="input-number"
|
||||
type="number"
|
||||
min="0">
|
||||
</div>
|
||||
<div :disabled="!present" class="spread-control style-control">
|
||||
<label for="spread" class="label">
|
||||
{{$t('settings.style.shadows.spread')}}
|
||||
</label>
|
||||
<input
|
||||
v-model="selected.spread"
|
||||
:disabled="!present"
|
||||
name="spread"
|
||||
id="spread"
|
||||
class="input-range"
|
||||
type="range"
|
||||
max="20"
|
||||
min="-20">
|
||||
<input
|
||||
v-model="selected.spread"
|
||||
:disabled="!present"
|
||||
class="input-number"
|
||||
type="number">
|
||||
</div>
|
||||
<ColorInput
|
||||
v-model="selected.color"
|
||||
:disabled="!present"
|
||||
:label="$t('settings.style.common.color')"
|
||||
name="shadow"/>
|
||||
<OpacityInput
|
||||
v-model="selected.alpha"
|
||||
:disabled="!present"/>
|
||||
<p>
|
||||
{{$t('settings.style.shadows.hint')}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./shadow_control.js" ></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
.shadow-control {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
margin-bottom: 1em;
|
||||
|
||||
.shadow-preview-container,
|
||||
.shadow-tweak {
|
||||
margin: 5px 6px 0 0;
|
||||
}
|
||||
.shadow-preview-container {
|
||||
flex: 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
$side: 15em;
|
||||
|
||||
input[type=number] {
|
||||
width: 5em;
|
||||
min-width: 2em;
|
||||
}
|
||||
.x-shift-control,
|
||||
.y-shift-control {
|
||||
display: flex;
|
||||
flex: 0;
|
||||
|
||||
&[disabled=disabled] *{
|
||||
opacity: .5
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.x-shift-control {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.x-shift-control .wrap,
|
||||
input[type=range] {
|
||||
margin: 0;
|
||||
width: $side;
|
||||
height: 2em;
|
||||
}
|
||||
.y-shift-control {
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
.wrap {
|
||||
width: 2em;
|
||||
height: $side;
|
||||
}
|
||||
input[type=range] {
|
||||
transform-origin: 1em 1em;
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
}
|
||||
.preview-window {
|
||||
flex: 1;
|
||||
background-color: #999999;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-image:
|
||||
linear-gradient(45deg, #666666 25%, transparent 25%),
|
||||
linear-gradient(-45deg, #666666 25%, transparent 25%),
|
||||
linear-gradient(45deg, transparent 75%, #666666 75%),
|
||||
linear-gradient(-45deg, transparent 75%, #666666 75%);
|
||||
background-size: 20px 20px;
|
||||
background-position:0 0, 0 10px, 10px -10px, -10px 0;
|
||||
|
||||
border-radius: $fallback--inputRadius;
|
||||
border-radius: var(--inputRadius, $fallback--inputRadius);
|
||||
|
||||
.preview-block {
|
||||
width: 33%;
|
||||
height: 33%;
|
||||
background-color: $fallback--bg;
|
||||
background-color: var(--bg, $fallback--bg);
|
||||
border-radius: $fallback--panelRadius;
|
||||
border-radius: var(--panelRadius, $fallback--panelRadius);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.shadow-tweak {
|
||||
flex: 1;
|
||||
min-width: 280px;
|
||||
|
||||
.id-control {
|
||||
align-items: stretch;
|
||||
.select, .btn {
|
||||
min-width: 1px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
.btn {
|
||||
padding: 0 .4em;
|
||||
margin: 0 .1em;
|
||||
}
|
||||
.select {
|
||||
flex: 1;
|
||||
select {
|
||||
align-self: initial;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -21,7 +21,8 @@ const Status = {
|
|||
'replies',
|
||||
'noReplyLinks',
|
||||
'noHeading',
|
||||
'inlineExpanded'
|
||||
'inlineExpanded',
|
||||
'activatePanel'
|
||||
],
|
||||
data () {
|
||||
return {
|
||||
|
@ -34,7 +35,8 @@ const Status = {
|
|||
showingTall: false,
|
||||
expandingSubject: typeof this.$store.state.config.collapseMessageWithSubject === 'undefined'
|
||||
? !this.$store.state.instance.collapseMessageWithSubject
|
||||
: !this.$store.state.config.collapseMessageWithSubject
|
||||
: !this.$store.state.config.collapseMessageWithSubject,
|
||||
betterShadow: this.$store.state.interface.browserSupport.cssFilter
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<template v-if="muted && !noReplyLinks">
|
||||
<div class="media status container muted">
|
||||
<small>
|
||||
<router-link :to="userProfileLink(status.user.id, status.user.screen_name)">
|
||||
<router-link @click.native="activatePanel('timeline')" :to="userProfileLink(status.user.id, status.user.screen_name)">
|
||||
{{status.user.screen_name}}
|
||||
</router-link>
|
||||
</small>
|
||||
|
@ -13,7 +13,7 @@
|
|||
</template>
|
||||
<template v-else>
|
||||
<div v-if="retweet && !noHeading" :class="[repeaterClass, { highlighted: repeaterStyle }]" :style="[repeaterStyle]" class="media container retweet-info">
|
||||
<StillImage v-if="retweet" class='avatar' :src="statusoid.user.profile_image_url_original"/>
|
||||
<StillImage v-if="retweet" class='avatar' :class='{ "better-shadow": betterShadow }' :src="statusoid.user.profile_image_url_original"/>
|
||||
<div class="media-body faint">
|
||||
<a v-if="retweeterHtml" :href="statusoid.user.statusnet_profile_url" class="user-name" :title="'@'+statusoid.user.screen_name" v-html="retweeterHtml"></a>
|
||||
<a v-else :href="statusoid.user.statusnet_profile_url" class="user-name" :title="'@'+statusoid.user.screen_name">{{retweeter}}</a>
|
||||
|
@ -25,7 +25,7 @@
|
|||
<div :class="[userClass, { highlighted: userStyle, 'is-retweet': retweet }]" :style="[ userStyle ]" class="media status">
|
||||
<div v-if="!noHeading" class="media-left">
|
||||
<a :href="status.user.statusnet_profile_url" @click.stop.prevent.capture="toggleUserExpanded">
|
||||
<StillImage class='avatar' :class="{'avatar-compact': compact}" :src="status.user.profile_image_url_original"/>
|
||||
<StillImage class='avatar' :class="{'avatar-compact': compact, 'better-shadow': betterShadow}" :src="status.user.profile_image_url_original"/>
|
||||
</a>
|
||||
</div>
|
||||
<div class="status-body">
|
||||
|
@ -38,12 +38,12 @@
|
|||
<h4 class="user-name" v-if="status.user.name_html" v-html="status.user.name_html"></h4>
|
||||
<h4 class="user-name" v-else>{{status.user.name}}</h4>
|
||||
<span class="links">
|
||||
<router-link :to="userProfileLink(status.user.id, status.user.screen_name)">
|
||||
<router-link @click.native="activatePanel('timeline')" :to="userProfileLink(status.user.id, status.user.screen_name)">
|
||||
{{status.user.screen_name}}
|
||||
</router-link>
|
||||
<span v-if="status.in_reply_to_screen_name" class="faint reply-info">
|
||||
<i class="icon-right-open"></i>
|
||||
<router-link :to="userProfileLink(status.in_reply_to_user_id, status.in_reply_to_screen_name)">
|
||||
<router-link @click.native="activatePanel('timeline')" :to="userProfileLink(status.in_reply_to_user_id, status.in_reply_to_screen_name)">
|
||||
{{status.in_reply_to_screen_name}}
|
||||
</router-link>
|
||||
</span>
|
||||
|
@ -60,7 +60,7 @@
|
|||
</h4>
|
||||
</div>
|
||||
<div class="media-heading-right">
|
||||
<router-link class="timeago" :to="{ name: 'conversation', params: { id: status.id } }">
|
||||
<router-link class="timeago" @click.native="activatePanel('timeline')" :to="{ name: 'conversation', params: { id: status.id } }">
|
||||
<timeago :since="status.created_at" :auto-update="60"></timeago>
|
||||
</router-link>
|
||||
<div class="visibility-icon" v-if="status.visibility">
|
||||
|
@ -79,7 +79,7 @@
|
|||
</div>
|
||||
|
||||
<div v-if="showPreview" class="status-preview-container">
|
||||
<status class="status-preview" v-if="preview" :noReplyLinks="true" :statusoid="preview" :compact=true></status>
|
||||
<status :activatePanel="activatePanel" class="status-preview" v-if="preview" :noReplyLinks="true" :statusoid="preview" :compact=true></status>
|
||||
<div class="status-preview status-preview-loading" v-else>
|
||||
<i class="icon-spin4 animate-spin"></i>
|
||||
</div>
|
||||
|
@ -152,6 +152,7 @@
|
|||
border-radius: $fallback--tooltipRadius;
|
||||
border-radius: var(--tooltipRadius, $fallback--tooltipRadius);
|
||||
box-shadow: 2px 2px 3px rgba(0, 0, 0, 0.5);
|
||||
box-shadow: var(--popupShadow);
|
||||
margin-top: 0.25em;
|
||||
margin-left: 0.5em;
|
||||
z-index: 50;
|
||||
|
@ -290,8 +291,8 @@
|
|||
margin-left: 0.2em;
|
||||
}
|
||||
a:hover i {
|
||||
color: $fallback--fg;
|
||||
color: var(--fg, $fallback--fg);
|
||||
color: $fallback--text;
|
||||
color: var(--text, $fallback--text);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -329,6 +330,8 @@
|
|||
|
||||
.status-content {
|
||||
margin-right: 0.5em;
|
||||
font-family: var(--postFont, sans-serif);
|
||||
|
||||
img, video {
|
||||
max-width: 100%;
|
||||
max-height: 400px;
|
||||
|
@ -345,6 +348,10 @@
|
|||
overflow: auto;
|
||||
}
|
||||
|
||||
code, samp, kbd, var, pre {
|
||||
font-family: var(--postCodeFont, monospace);
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
margin-top: 0.2em;
|
||||
|
@ -463,18 +470,30 @@
|
|||
.status .avatar-compact {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
box-shadow: var(--avatarStatusShadow);
|
||||
border-radius: $fallback--avatarAltRadius;
|
||||
border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);
|
||||
|
||||
&.better-shadow {
|
||||
box-shadow: var(--avatarStatusShadowInset);
|
||||
filter: var(--avatarStatusShadowFilter)
|
||||
}
|
||||
}
|
||||
|
||||
.avatar.still-image {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
box-shadow: var(--avatarStatusShadow);
|
||||
border-radius: $fallback--avatarRadius;
|
||||
border-radius: var(--avatarRadius, $fallback--avatarRadius);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
|
||||
&.better-shadow {
|
||||
box-shadow: var(--avatarStatusShadowInset);
|
||||
filter: var(--avatarStatusShadowFilter)
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
@ -538,6 +557,7 @@ a.unmute {
|
|||
.status-el:last-child {
|
||||
border-bottom-radius: 0 0 $fallback--panelRadius $fallback--panelRadius;;
|
||||
border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius);
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
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,21 +1,101 @@
|
|||
import { rgbstr2hex } 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 { generateColors, generateShadows, generateRadii, generateFonts, composePreset, getThemes } from '../../services/style_setter/style_setter.js'
|
||||
import ColorInput from '../color_input/color_input.vue'
|
||||
import RangeInput from '../range_input/range_input.vue'
|
||||
import OpacityInput from '../opacity_input/opacity_input.vue'
|
||||
import ShadowControl from '../shadow_control/shadow_control.vue'
|
||||
import FontControl from '../font_control/font_control.vue'
|
||||
import ContrastRatio from '../contrast_ratio/contrast_ratio.vue'
|
||||
import TabSwitcher from '../tab_switcher/tab_switcher.jsx'
|
||||
import Preview from './preview.vue'
|
||||
import ExportImport from '../export_import/export_import.vue'
|
||||
|
||||
// List of color values used in v1
|
||||
const v1OnlyNames = [
|
||||
'bg',
|
||||
'fg',
|
||||
'text',
|
||||
'link',
|
||||
'cRed',
|
||||
'cGreen',
|
||||
'cBlue',
|
||||
'cOrange'
|
||||
].map(_ => _ + 'ColorLocal')
|
||||
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
availableStyles: [],
|
||||
selected: this.$store.state.config.theme,
|
||||
invalidThemeImported: false,
|
||||
bgColorLocal: '',
|
||||
btnColorLocal: '',
|
||||
|
||||
previewShadows: {},
|
||||
previewColors: {},
|
||||
previewRadii: {},
|
||||
previewFonts: {},
|
||||
|
||||
shadowsInvalid: true,
|
||||
colorsInvalid: true,
|
||||
radiiInvalid: true,
|
||||
|
||||
keepColor: false,
|
||||
keepShadows: false,
|
||||
keepOpacity: false,
|
||||
keepRoundness: false,
|
||||
keepFonts: false,
|
||||
|
||||
textColorLocal: '',
|
||||
linkColorLocal: '',
|
||||
redColorLocal: '',
|
||||
blueColorLocal: '',
|
||||
greenColorLocal: '',
|
||||
orangeColorLocal: '',
|
||||
|
||||
bgColorLocal: '',
|
||||
bgOpacityLocal: undefined,
|
||||
|
||||
fgColorLocal: '',
|
||||
fgTextColorLocal: undefined,
|
||||
fgLinkColorLocal: undefined,
|
||||
|
||||
btnColorLocal: undefined,
|
||||
btnTextColorLocal: undefined,
|
||||
btnOpacityLocal: undefined,
|
||||
|
||||
inputColorLocal: undefined,
|
||||
inputTextColorLocal: undefined,
|
||||
inputOpacityLocal: undefined,
|
||||
|
||||
panelColorLocal: undefined,
|
||||
panelTextColorLocal: undefined,
|
||||
panelLinkColorLocal: undefined,
|
||||
panelFaintColorLocal: undefined,
|
||||
panelOpacityLocal: undefined,
|
||||
|
||||
topBarColorLocal: undefined,
|
||||
topBarTextColorLocal: undefined,
|
||||
topBarLinkColorLocal: undefined,
|
||||
|
||||
alertErrorColorLocal: undefined,
|
||||
|
||||
badgeOpacityLocal: undefined,
|
||||
badgeNotificationColorLocal: undefined,
|
||||
|
||||
borderColorLocal: undefined,
|
||||
borderOpacityLocal: undefined,
|
||||
|
||||
faintColorLocal: undefined,
|
||||
faintOpacityLocal: undefined,
|
||||
faintLinkColorLocal: undefined,
|
||||
|
||||
cRedColorLocal: '',
|
||||
cBlueColorLocal: '',
|
||||
cGreenColorLocal: '',
|
||||
cOrangeColorLocal: '',
|
||||
|
||||
shadowSelected: undefined,
|
||||
shadowsLocal: {},
|
||||
fontsLocal: {},
|
||||
|
||||
btnRadiusLocal: '',
|
||||
inputRadiusLocal: '',
|
||||
checkboxRadiusLocal: '',
|
||||
panelRadiusLocal: '',
|
||||
avatarRadiusLocal: '',
|
||||
avatarAltRadiusLocal: '',
|
||||
|
@ -26,144 +106,470 @@ export default {
|
|||
created () {
|
||||
const self = this
|
||||
|
||||
window.fetch('/static/styles.json')
|
||||
.then((data) => data.json())
|
||||
.then((themes) => {
|
||||
self.availableStyles = themes
|
||||
})
|
||||
getThemes().then((themesComplete) => {
|
||||
self.availableStyles = themesComplete
|
||||
})
|
||||
},
|
||||
mounted () {
|
||||
this.normalizeLocalState(this.$store.state.config.colors, this.$store.state.config.radii)
|
||||
this.normalizeLocalState(this.$store.state.config.customTheme)
|
||||
if (typeof this.shadowSelected === 'undefined') {
|
||||
this.shadowSelected = this.shadowsAvailable[0]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
exportCurrentTheme () {
|
||||
const stringified = JSON.stringify({
|
||||
// To separate from other random JSON files and possible future theme formats
|
||||
_pleroma_theme_version: 1,
|
||||
colors: this.$store.state.config.colors,
|
||||
radii: this.$store.state.config.radii
|
||||
}, 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)
|
||||
computed: {
|
||||
selectedVersion () {
|
||||
return Array.isArray(this.selected) ? 1 : 2
|
||||
},
|
||||
currentColors () {
|
||||
return {
|
||||
bg: this.bgColorLocal,
|
||||
text: this.textColorLocal,
|
||||
link: this.linkColorLocal,
|
||||
|
||||
importTheme () {
|
||||
this.invalidThemeImported = false
|
||||
const filePicker = document.createElement('input')
|
||||
filePicker.setAttribute('type', 'file')
|
||||
filePicker.setAttribute('accept', '.json')
|
||||
fg: this.fgColorLocal,
|
||||
fgText: this.fgTextColorLocal,
|
||||
fgLink: this.fgLinkColorLocal,
|
||||
|
||||
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.colors, parsed.radii)
|
||||
} 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])
|
||||
}
|
||||
panel: this.panelColorLocal,
|
||||
panelText: this.panelTextColorLocal,
|
||||
panelLink: this.panelLinkColorLocal,
|
||||
panelFaint: this.panelFaintColorLocal,
|
||||
|
||||
input: this.inputColorLocal,
|
||||
inputText: this.inputTextColorLocal,
|
||||
|
||||
topBar: this.topBarColorLocal,
|
||||
topBarText: this.topBarTextColorLocal,
|
||||
topBarLink: this.topBarLinkColorLocal,
|
||||
|
||||
btn: this.btnColorLocal,
|
||||
btnText: this.btnTextColorLocal,
|
||||
|
||||
alertError: this.alertErrorColorLocal,
|
||||
badgeNotification: this.badgeNotificationColorLocal,
|
||||
|
||||
faint: this.faintColorLocal,
|
||||
faintLink: this.faintLinkColorLocal,
|
||||
border: this.borderColorLocal,
|
||||
|
||||
cRed: this.cRedColorLocal,
|
||||
cBlue: this.cBlueColorLocal,
|
||||
cGreen: this.cGreenColorLocal,
|
||||
cOrange: this.cOrangeColorLocal
|
||||
}
|
||||
},
|
||||
currentOpacity () {
|
||||
return {
|
||||
bg: this.bgOpacityLocal,
|
||||
btn: this.btnOpacityLocal,
|
||||
input: this.inputOpacityLocal,
|
||||
panel: this.panelOpacityLocal,
|
||||
topBar: this.topBarOpacityLocal,
|
||||
border: this.borderOpacityLocal,
|
||||
faint: this.faintOpacityLocal
|
||||
}
|
||||
},
|
||||
currentRadii () {
|
||||
return {
|
||||
btn: this.btnRadiusLocal,
|
||||
input: this.inputRadiusLocal,
|
||||
checkbox: this.checkboxRadiusLocal,
|
||||
panel: this.panelRadiusLocal,
|
||||
avatar: this.avatarRadiusLocal,
|
||||
avatarAlt: this.avatarAltRadiusLocal,
|
||||
tooltip: this.tooltipRadiusLocal,
|
||||
attachment: this.attachmentRadiusLocal
|
||||
}
|
||||
},
|
||||
preview () {
|
||||
return composePreset(this.previewColors, this.previewRadii, this.previewShadows, this.previewFonts)
|
||||
},
|
||||
previewTheme () {
|
||||
if (!this.preview.theme.colors) return { colors: {}, opacity: {}, radii: {}, shadows: {}, fonts: {} }
|
||||
return this.preview.theme
|
||||
},
|
||||
// This needs optimization maybe
|
||||
previewContrast () {
|
||||
if (!this.previewTheme.colors.bg) return {}
|
||||
const colors = this.previewTheme.colors
|
||||
const opacity = this.previewTheme.opacity
|
||||
if (!colors.bg) return {}
|
||||
const hints = (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
|
||||
})
|
||||
|
||||
document.body.appendChild(filePicker)
|
||||
filePicker.click()
|
||||
document.body.removeChild(filePicker)
|
||||
},
|
||||
// fgsfds :DDDD
|
||||
const fgs = {
|
||||
text: hex2rgb(colors.text),
|
||||
panelText: hex2rgb(colors.panelText),
|
||||
panelLink: hex2rgb(colors.panelLink),
|
||||
btnText: hex2rgb(colors.btnText),
|
||||
topBarText: hex2rgb(colors.topBarText),
|
||||
inputText: hex2rgb(colors.inputText),
|
||||
|
||||
link: hex2rgb(colors.link),
|
||||
topBarLink: hex2rgb(colors.topBarLink),
|
||||
|
||||
red: hex2rgb(colors.cRed),
|
||||
green: hex2rgb(colors.cGreen),
|
||||
blue: hex2rgb(colors.cBlue),
|
||||
orange: hex2rgb(colors.cOrange)
|
||||
}
|
||||
|
||||
const bgs = {
|
||||
bg: hex2rgb(colors.bg),
|
||||
btn: hex2rgb(colors.btn),
|
||||
panel: hex2rgb(colors.panel),
|
||||
topBar: hex2rgb(colors.topBar),
|
||||
input: hex2rgb(colors.input),
|
||||
alertError: hex2rgb(colors.alertError),
|
||||
badgeNotification: hex2rgb(colors.badgeNotification)
|
||||
}
|
||||
|
||||
/* This is a bit confusing because "bottom layer" used is text color
|
||||
* This is done to get worst case scenario when background below transparent
|
||||
* layer matches text color, making it harder to read the lower alpha is.
|
||||
*/
|
||||
const ratios = {
|
||||
bgText: getContrastRatio(alphaBlend(bgs.bg, opacity.bg, fgs.text), fgs.text),
|
||||
bgLink: getContrastRatio(alphaBlend(bgs.bg, opacity.bg, fgs.link), fgs.link),
|
||||
bgRed: getContrastRatio(alphaBlend(bgs.bg, opacity.bg, fgs.red), fgs.red),
|
||||
bgGreen: getContrastRatio(alphaBlend(bgs.bg, opacity.bg, fgs.green), fgs.green),
|
||||
bgBlue: getContrastRatio(alphaBlend(bgs.bg, opacity.bg, fgs.blue), fgs.blue),
|
||||
bgOrange: getContrastRatio(alphaBlend(bgs.bg, opacity.bg, fgs.orange), fgs.orange),
|
||||
|
||||
tintText: getContrastRatio(alphaBlend(bgs.bg, 0.5, fgs.panelText), fgs.text),
|
||||
|
||||
panelText: getContrastRatio(alphaBlend(bgs.panel, opacity.panel, fgs.panelText), fgs.panelText),
|
||||
panelLink: getContrastRatio(alphaBlend(bgs.panel, opacity.panel, fgs.panelLink), fgs.panelLink),
|
||||
|
||||
btnText: getContrastRatio(alphaBlend(bgs.btn, opacity.btn, fgs.btnText), fgs.btnText),
|
||||
|
||||
inputText: getContrastRatio(alphaBlend(bgs.input, opacity.input, fgs.inputText), fgs.inputText),
|
||||
|
||||
topBarText: getContrastRatio(alphaBlend(bgs.topBar, opacity.topBar, fgs.topBarText), fgs.topBarText),
|
||||
topBarLink: getContrastRatio(alphaBlend(bgs.topBar, opacity.topBar, fgs.topBarLink), fgs.topBarLink)
|
||||
}
|
||||
|
||||
return Object.entries(ratios).reduce((acc, [k, v]) => { acc[k] = hints(v); return acc }, {})
|
||||
},
|
||||
previewRules () {
|
||||
if (!this.preview.rules) return ''
|
||||
return [
|
||||
...Object.values(this.preview.rules),
|
||||
'color: var(--text)',
|
||||
'font-family: var(--interfaceFont, sans-serif)'
|
||||
].join(';')
|
||||
},
|
||||
shadowsAvailable () {
|
||||
return Object.keys(this.previewTheme.shadows).sort()
|
||||
},
|
||||
currentShadowOverriden: {
|
||||
get () {
|
||||
return !!this.currentShadow
|
||||
},
|
||||
set (val) {
|
||||
if (val) {
|
||||
set(this.shadowsLocal, this.shadowSelected, this.currentShadowFallback.map(_ => Object.assign({}, _)))
|
||||
} else {
|
||||
del(this.shadowsLocal, this.shadowSelected)
|
||||
}
|
||||
}
|
||||
},
|
||||
currentShadowFallback () {
|
||||
return this.previewTheme.shadows[this.shadowSelected]
|
||||
},
|
||||
currentShadow: {
|
||||
get () {
|
||||
return this.shadowsLocal[this.shadowSelected]
|
||||
},
|
||||
set (v) {
|
||||
set(this.shadowsLocal, this.shadowSelected, v)
|
||||
}
|
||||
},
|
||||
themeValid () {
|
||||
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: {
|
||||
ColorInput,
|
||||
OpacityInput,
|
||||
RangeInput,
|
||||
ContrastRatio,
|
||||
ShadowControl,
|
||||
FontControl,
|
||||
TabSwitcher,
|
||||
Preview,
|
||||
ExportImport
|
||||
},
|
||||
methods: {
|
||||
setCustomTheme () {
|
||||
if (!this.bgColorLocal && !this.btnColorLocal && !this.linkColorLocal) {
|
||||
// reset to picked themes
|
||||
}
|
||||
|
||||
const rgb = (hex) => {
|
||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
|
||||
return result ? {
|
||||
r: parseInt(result[1], 16),
|
||||
g: parseInt(result[2], 16),
|
||||
b: parseInt(result[3], 16)
|
||||
} : null
|
||||
}
|
||||
const bgRgb = rgb(this.bgColorLocal)
|
||||
const btnRgb = rgb(this.btnColorLocal)
|
||||
const textRgb = rgb(this.textColorLocal)
|
||||
const linkRgb = rgb(this.linkColorLocal)
|
||||
|
||||
const redRgb = rgb(this.redColorLocal)
|
||||
const blueRgb = rgb(this.blueColorLocal)
|
||||
const greenRgb = rgb(this.greenColorLocal)
|
||||
const orangeRgb = rgb(this.orangeColorLocal)
|
||||
|
||||
if (bgRgb && btnRgb && linkRgb) {
|
||||
this.$store.dispatch('setOption', {
|
||||
name: 'customTheme',
|
||||
value: {
|
||||
fg: btnRgb,
|
||||
bg: bgRgb,
|
||||
text: textRgb,
|
||||
link: linkRgb,
|
||||
cRed: redRgb,
|
||||
cBlue: blueRgb,
|
||||
cGreen: greenRgb,
|
||||
cOrange: orangeRgb,
|
||||
btnRadius: this.btnRadiusLocal,
|
||||
inputRadius: this.inputRadiusLocal,
|
||||
panelRadius: this.panelRadiusLocal,
|
||||
avatarRadius: this.avatarRadiusLocal,
|
||||
avatarAltRadius: this.avatarAltRadiusLocal,
|
||||
tooltipRadius: this.tooltipRadiusLocal,
|
||||
attachmentRadius: this.attachmentRadiusLocal
|
||||
}})
|
||||
this.$store.dispatch('setOption', {
|
||||
name: 'customTheme',
|
||||
value: {
|
||||
shadows: this.shadowsLocal,
|
||||
fonts: this.fontsLocal,
|
||||
opacity: this.currentOpacity,
|
||||
colors: this.currentColors,
|
||||
radii: this.currentRadii
|
||||
}
|
||||
})
|
||||
},
|
||||
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 () {
|
||||
const state = this.$store.state.config.customTheme
|
||||
const version = state.colors ? 2 : 'l1'
|
||||
this.normalizeLocalState(this.$store.state.config.customTheme, version)
|
||||
},
|
||||
|
||||
normalizeLocalState (colors, radii) {
|
||||
this.bgColorLocal = rgbstr2hex(colors.bg)
|
||||
this.btnColorLocal = rgbstr2hex(colors.btn)
|
||||
this.textColorLocal = rgbstr2hex(colors.fg)
|
||||
this.linkColorLocal = rgbstr2hex(colors.link)
|
||||
// Clears all the extra stuff when loading V1 theme
|
||||
clearV1 () {
|
||||
Object.keys(this.$data)
|
||||
.filter(_ => _.endsWith('ColorLocal') || _.endsWith('OpacityLocal'))
|
||||
.filter(_ => !v1OnlyNames.includes(_))
|
||||
.forEach(key => {
|
||||
set(this.$data, key, undefined)
|
||||
})
|
||||
},
|
||||
|
||||
this.redColorLocal = rgbstr2hex(colors.cRed)
|
||||
this.blueColorLocal = rgbstr2hex(colors.cBlue)
|
||||
this.greenColorLocal = rgbstr2hex(colors.cGreen)
|
||||
this.orangeColorLocal = rgbstr2hex(colors.cOrange)
|
||||
clearRoundness () {
|
||||
Object.keys(this.$data)
|
||||
.filter(_ => _.endsWith('RadiusLocal'))
|
||||
.forEach(key => {
|
||||
set(this.$data, key, undefined)
|
||||
})
|
||||
},
|
||||
|
||||
this.btnRadiusLocal = radii.btnRadius || 4
|
||||
this.inputRadiusLocal = radii.inputRadius || 4
|
||||
this.panelRadiusLocal = radii.panelRadius || 10
|
||||
this.avatarRadiusLocal = radii.avatarRadius || 5
|
||||
this.avatarAltRadiusLocal = radii.avatarAltRadius || 50
|
||||
this.tooltipRadiusLocal = radii.tooltipRadius || 2
|
||||
this.attachmentRadiusLocal = radii.attachmentRadius || 5
|
||||
clearOpacity () {
|
||||
Object.keys(this.$data)
|
||||
.filter(_ => _.endsWith('OpacityLocal'))
|
||||
.forEach(key => {
|
||||
set(this.$data, key, undefined)
|
||||
})
|
||||
},
|
||||
|
||||
clearShadows () {
|
||||
this.shadowsLocal = {}
|
||||
},
|
||||
|
||||
clearFonts () {
|
||||
this.fontsLocal = {}
|
||||
},
|
||||
|
||||
/**
|
||||
* This applies stored theme data onto form. Supports three versions of data:
|
||||
* v2 (version = 2) - newer version of themes.
|
||||
* v1 (version = 1) - older version of themes (import from file)
|
||||
* v1l (version = l1) - older version of theme (load from local storage)
|
||||
* v1 and v1l differ because of way themes were stored/exported.
|
||||
* @param {Object} input - input data
|
||||
* @param {Number} version - version of data. 0 means try to guess based on data. "l1" means v1, locastorage type
|
||||
*/
|
||||
normalizeLocalState (input, version = 0) {
|
||||
const colors = input.colors || input
|
||||
const radii = input.radii || input
|
||||
const opacity = input.opacity
|
||||
const shadows = input.shadows || {}
|
||||
const fonts = input.fonts || {}
|
||||
|
||||
if (version === 0) {
|
||||
if (input.version) version = input.version
|
||||
// Old v1 naming: fg is text, btn is foreground
|
||||
if (typeof colors.text === 'undefined' && typeof colors.fg !== 'undefined') {
|
||||
version = 1
|
||||
}
|
||||
// New v2 naming: text is text, fg is foreground
|
||||
if (typeof colors.text !== 'undefined' && typeof colors.fg !== 'undefined') {
|
||||
version = 2
|
||||
}
|
||||
}
|
||||
|
||||
// Stuff that differs between V1 and V2
|
||||
if (version === 1) {
|
||||
this.fgColorLocal = rgb2hex(colors.btn)
|
||||
this.textColorLocal = rgb2hex(colors.fg)
|
||||
}
|
||||
|
||||
if (!this.keepColor) {
|
||||
this.clearV1()
|
||||
const keys = new Set(version !== 1 ? Object.keys(colors) : [])
|
||||
if (version === 1 || version === 'l1') {
|
||||
keys
|
||||
.add('bg')
|
||||
.add('link')
|
||||
.add('cRed')
|
||||
.add('cBlue')
|
||||
.add('cGreen')
|
||||
.add('cOrange')
|
||||
}
|
||||
|
||||
keys.forEach(key => {
|
||||
this[key + 'ColorLocal'] = rgb2hex(colors[key])
|
||||
})
|
||||
}
|
||||
|
||||
if (!this.keepRoundness) {
|
||||
this.clearRoundness()
|
||||
Object.entries(radii).forEach(([k, v]) => {
|
||||
// 'Radius' is kept mostly for v1->v2 localstorage transition
|
||||
const key = k.endsWith('Radius') ? k.split('Radius')[0] : k
|
||||
this[key + 'RadiusLocal'] = v
|
||||
})
|
||||
}
|
||||
|
||||
if (!this.keepShadows) {
|
||||
this.clearShadows()
|
||||
this.shadowsLocal = shadows
|
||||
this.shadowSelected = this.shadowsAvailable[0]
|
||||
}
|
||||
|
||||
if (!this.keepFonts) {
|
||||
this.clearFonts()
|
||||
this.fontsLocal = fonts
|
||||
}
|
||||
|
||||
if (opacity && !this.keepOpacity) {
|
||||
this.clearOpacity()
|
||||
Object.entries(opacity).forEach(([k, v]) => {
|
||||
if (typeof v === 'undefined' || v === null || Number.isNaN(v)) return
|
||||
this[k + 'OpacityLocal'] = v
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
currentRadii () {
|
||||
try {
|
||||
this.previewRadii = generateRadii({ radii: this.currentRadii })
|
||||
this.radiiInvalid = false
|
||||
} catch (e) {
|
||||
this.radiiInvalid = true
|
||||
console.warn(e)
|
||||
}
|
||||
},
|
||||
shadowsLocal: {
|
||||
handler () {
|
||||
try {
|
||||
this.previewShadows = generateShadows({ shadows: this.shadowsLocal })
|
||||
this.shadowsInvalid = false
|
||||
} catch (e) {
|
||||
this.shadowsInvalid = true
|
||||
console.warn(e)
|
||||
}
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
fontsLocal: {
|
||||
handler () {
|
||||
try {
|
||||
this.previewFonts = generateFonts({ fonts: this.fontsLocal })
|
||||
this.fontsInvalid = false
|
||||
} catch (e) {
|
||||
this.fontsInvalid = true
|
||||
console.warn(e)
|
||||
}
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
currentColors () {
|
||||
try {
|
||||
this.previewColors = generateColors({
|
||||
opacity: this.currentOpacity,
|
||||
colors: this.currentColors
|
||||
})
|
||||
this.colorsInvalid = false
|
||||
} catch (e) {
|
||||
this.colorsInvalid = true
|
||||
console.warn(e)
|
||||
}
|
||||
},
|
||||
currentOpacity () {
|
||||
try {
|
||||
this.previewColors = generateColors({
|
||||
opacity: this.currentOpacity,
|
||||
colors: this.currentColors
|
||||
})
|
||||
} catch (e) {
|
||||
console.warn(e)
|
||||
}
|
||||
},
|
||||
selected () {
|
||||
this.bgColorLocal = this.selected[1]
|
||||
this.btnColorLocal = this.selected[2]
|
||||
this.textColorLocal = this.selected[3]
|
||||
this.linkColorLocal = this.selected[4]
|
||||
this.redColorLocal = this.selected[5]
|
||||
this.greenColorLocal = this.selected[6]
|
||||
this.blueColorLocal = this.selected[7]
|
||||
this.orangeColorLocal = this.selected[8]
|
||||
if (this.selectedVersion === 1) {
|
||||
if (!this.keepRoundness) {
|
||||
this.clearRoundness()
|
||||
}
|
||||
|
||||
if (!this.keepShadows) {
|
||||
this.clearShadows()
|
||||
}
|
||||
|
||||
if (!this.keepOpacity) {
|
||||
this.clearOpacity()
|
||||
}
|
||||
|
||||
if (!this.keepColor) {
|
||||
this.clearV1()
|
||||
|
||||
this.bgColorLocal = this.selected[1]
|
||||
this.fgColorLocal = this.selected[2]
|
||||
this.textColorLocal = this.selected[3]
|
||||
this.linkColorLocal = this.selected[4]
|
||||
this.cRedColorLocal = this.selected[5]
|
||||
this.cGreenColorLocal = this.selected[6]
|
||||
this.cBlueColorLocal = this.selected[7]
|
||||
this.cOrangeColorLocal = this.selected[8]
|
||||
}
|
||||
} else if (this.selectedVersion >= 2) {
|
||||
this.normalizeLocalState(this.selected.theme, 2)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
335
src/components/style_switcher/style_switcher.scss
Normal file
335
src/components/style_switcher/style_switcher.scss
Normal file
|
@ -0,0 +1,335 @@
|
|||
@import '../../_variables.scss';
|
||||
.style-switcher {
|
||||
.preset-switcher {
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
.style-control {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
margin-bottom: 5px;
|
||||
|
||||
.label {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
input, select {
|
||||
&:not(.exclude-disabled) {
|
||||
opacity: .5
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
input, select {
|
||||
min-width: 3em;
|
||||
margin: 0;
|
||||
flex: 0;
|
||||
|
||||
&[type=color] {
|
||||
padding: 1px;
|
||||
cursor: pointer;
|
||||
height: 29px;
|
||||
min-width: 2em;
|
||||
border: none;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
&[type=number] {
|
||||
min-width: 5em;
|
||||
}
|
||||
|
||||
&[type=range] {
|
||||
flex: 1;
|
||||
min-width: 3em;
|
||||
}
|
||||
|
||||
&[type=checkbox] + label {
|
||||
margin: 6px 0;
|
||||
}
|
||||
|
||||
&:not([type=number]):not([type=text]) {
|
||||
align-self: flex-start;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tab-switcher {
|
||||
margin: 0 -1em;
|
||||
}
|
||||
|
||||
.reset-container {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.fonts-container,
|
||||
.reset-container,
|
||||
.apply-container,
|
||||
.radius-container,
|
||||
.color-container,
|
||||
{
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.fonts-container,
|
||||
.radius-container {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.color-container{
|
||||
> h4 {
|
||||
width: 99%;
|
||||
}
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.fonts-container,
|
||||
.color-container,
|
||||
.shadow-container,
|
||||
.radius-container,
|
||||
.presets-container {
|
||||
margin: 1em 1em 0;
|
||||
}
|
||||
|
||||
.tab-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
width: 100%;
|
||||
min-height: 30px;
|
||||
|
||||
.btn {
|
||||
min-width: 1px;
|
||||
flex: 0 auto;
|
||||
padding: 0 1em;
|
||||
}
|
||||
|
||||
p {
|
||||
flex: 1;
|
||||
margin: 0;
|
||||
margin-right: .5em;
|
||||
}
|
||||
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.shadow-selector {
|
||||
.override {
|
||||
flex: 1;
|
||||
margin-left: .5em;
|
||||
}
|
||||
.select-container {
|
||||
margin-top: -4px;
|
||||
margin-bottom: -3px;
|
||||
}
|
||||
}
|
||||
|
||||
.save-load,
|
||||
.save-load-options {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: baseline;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.presets,
|
||||
.import-export {
|
||||
margin-bottom: .5em;
|
||||
}
|
||||
|
||||
.import-export {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.override {
|
||||
margin-left: .5em;
|
||||
}
|
||||
}
|
||||
|
||||
.save-load-options {
|
||||
flex-wrap: wrap;
|
||||
margin-top: .5em;
|
||||
justify-content: center;
|
||||
.keep-option {
|
||||
margin: 0 .5em .5em;
|
||||
min-width: 25%;
|
||||
}
|
||||
}
|
||||
|
||||
.preview-container {
|
||||
border-top: 1px dashed;
|
||||
border-bottom: 1px dashed;
|
||||
border-color: $fallback--border;
|
||||
border-color: var(--border, $fallback--border);
|
||||
margin: 1em -1em 0;
|
||||
padding: 1em;
|
||||
background: var(--body-background-image);
|
||||
background-size: cover;
|
||||
background-position: 50% 50%;
|
||||
|
||||
.dummy {
|
||||
.post {
|
||||
font-family: var(--postFont);
|
||||
display: flex;
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
|
||||
h4 {
|
||||
margin-bottom: .25em;
|
||||
}
|
||||
|
||||
.icons {
|
||||
margin-top: .5em;
|
||||
display: flex;
|
||||
|
||||
i {
|
||||
margin-right: 1em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.after-post {
|
||||
margin-top: 1em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.avatar, .avatar-alt{
|
||||
background: linear-gradient(135deg, #b8e1fc 0%,#a9d2f3 10%,#90bae4 25%,#90bcea 37%,#90bff0 50%,#6ba8e5 51%,#a2daf5 83%,#bdf3fd 100%);
|
||||
color: black;
|
||||
font-family: sans-serif;
|
||||
text-align: center;
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
.avatar-alt {
|
||||
flex: 0 auto;
|
||||
margin-left: 28px;
|
||||
font-size: 12px;
|
||||
min-width: 20px;
|
||||
min-height: 20px;
|
||||
line-height: 20px;
|
||||
border-radius: $fallback--avatarAltRadius;
|
||||
border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);
|
||||
}
|
||||
|
||||
.avatar {
|
||||
flex: 0 auto;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
font-size: 14px;
|
||||
line-height: 48px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
|
||||
.checkbox {
|
||||
display: inline-flex;
|
||||
align-items: baseline;
|
||||
margin-right: 1em;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.separator {
|
||||
margin: 1em;
|
||||
border-bottom: 1px solid;
|
||||
border-color: $fallback--border;
|
||||
border-color: var(--border, $fallback--border);
|
||||
}
|
||||
|
||||
.panel-heading {
|
||||
.badge, .alert, .btn, .faint {
|
||||
margin-left: 1em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.faint {
|
||||
text-overflow: ellipsis;
|
||||
min-width: 2em;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
.flex-spacer {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
.btn {
|
||||
margin-left: 0;
|
||||
padding: 0 1em;
|
||||
min-width: 3em;
|
||||
min-height: 30px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.apply-container {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.radius-item,
|
||||
.color-item {
|
||||
min-width: 20em;
|
||||
margin: 5px 6px 0 0;
|
||||
display:flex;
|
||||
flex-direction: column;
|
||||
flex: 1 1 0;
|
||||
|
||||
&.wide {
|
||||
min-width: 60%
|
||||
}
|
||||
|
||||
&:not(.wide):nth-child(2n+1) {
|
||||
margin-right: 7px;
|
||||
|
||||
}
|
||||
|
||||
.color, .opacity {
|
||||
display:flex;
|
||||
align-items: baseline;
|
||||
}
|
||||
}
|
||||
|
||||
.radius-item {
|
||||
flex-basis: auto;
|
||||
}
|
||||
|
||||
.theme-radius-rn,
|
||||
.theme-color-cl {
|
||||
border: 0;
|
||||
box-shadow: none;
|
||||
background: transparent;
|
||||
color: var(--faint, $fallback--faint);
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
.theme-color-cl,
|
||||
.theme-radius-in,
|
||||
.theme-color-in {
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.theme-radius-in {
|
||||
min-width: 1em;
|
||||
}
|
||||
|
||||
.theme-radius-in {
|
||||
max-width: 7em;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.theme-radius-lb{
|
||||
max-width: 50em;
|
||||
}
|
||||
|
||||
.theme-preview-content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
margin-left: .25em;
|
||||
margin-right: .25em;
|
||||
}
|
||||
}
|
|
@ -1,300 +1,276 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="style-switcher">
|
||||
<div class="presets-container">
|
||||
<div>
|
||||
{{$t('settings.presets')}}
|
||||
<label for="style-switcher" class='select'>
|
||||
<select id="style-switcher" v-model="selected" class="style-switcher">
|
||||
<option v-for="style in availableStyles"
|
||||
:value="style"
|
||||
:style="{
|
||||
backgroundColor: style[1],
|
||||
color: style[3]
|
||||
}">
|
||||
{{style[0]}}
|
||||
</option>
|
||||
</select>
|
||||
<i class="icon-down-open"/>
|
||||
</label>
|
||||
<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">
|
||||
{{$t('settings.presets')}}
|
||||
<label for="preset-switcher" class='select'>
|
||||
<select id="preset-switcher" v-model="selected" class="preset-switcher">
|
||||
<option v-for="style in availableStyles"
|
||||
:value="style"
|
||||
:style="{
|
||||
backgroundColor: style[1] || style.theme.colors.bg,
|
||||
color: style[3] || style.theme.colors.text
|
||||
}">
|
||||
{{style[0] || style.name}}
|
||||
</option>
|
||||
</select>
|
||||
<i class="icon-down-open"/>
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
</export-import>
|
||||
</div>
|
||||
<div class="import-export">
|
||||
<button class="btn" @click="exportCurrentTheme">{{ $t('settings.export_theme') }}</button>
|
||||
<button class="btn" @click="importTheme">{{ $t('settings.import_theme') }}</button>
|
||||
<p v-if="invalidThemeImported" class="import-warning">{{ $t('settings.invalid_theme_imported') }}</p>
|
||||
<div class="save-load-options">
|
||||
<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
|
||||
id="keep-shadows"
|
||||
type="checkbox"
|
||||
v-model="keepShadows">
|
||||
<label for="keep-shadows">{{$t('settings.style.switcher.keep_shadows')}}</label>
|
||||
</span>
|
||||
<span class="keep-option">
|
||||
<input
|
||||
id="keep-opacity"
|
||||
type="checkbox"
|
||||
v-model="keepOpacity">
|
||||
<label for="keep-opacity">{{$t('settings.style.switcher.keep_opacity')}}</label>
|
||||
</span>
|
||||
<span class="keep-option">
|
||||
<input
|
||||
id="keep-roundness"
|
||||
type="checkbox"
|
||||
v-model="keepRoundness">
|
||||
<label for="keep-roundness">{{$t('settings.style.switcher.keep_roundness')}}</label>
|
||||
</span>
|
||||
<span class="keep-option">
|
||||
<input
|
||||
id="keep-fonts"
|
||||
type="checkbox"
|
||||
v-model="keepFonts">
|
||||
<label for="keep-fonts">{{$t('settings.style.switcher.keep_fonts')}}</label>
|
||||
</span>
|
||||
<p>{{$t('settings.style.switcher.save_load_hint')}}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="preview-container">
|
||||
<div :style="{
|
||||
'--btnRadius': btnRadiusLocal + 'px',
|
||||
'--inputRadius': inputRadiusLocal + 'px',
|
||||
'--panelRadius': panelRadiusLocal + 'px',
|
||||
'--avatarRadius': avatarRadiusLocal + 'px',
|
||||
'--avatarAltRadius': avatarAltRadiusLocal + 'px',
|
||||
'--tooltipRadius': tooltipRadiusLocal + 'px',
|
||||
'--attachmentRadius': attachmentRadiusLocal + 'px'
|
||||
}">
|
||||
<div class="panel dummy">
|
||||
<div class="panel-heading" :style="{ 'background-color': btnColorLocal, 'color': textColorLocal }">Preview</div>
|
||||
<div class="panel-body theme-preview-content" :style="{ 'background-color': bgColorLocal, 'color': textColorLocal }">
|
||||
<div class="avatar" :style="{
|
||||
'border-radius': avatarRadiusLocal + 'px'
|
||||
}">
|
||||
( ͡° ͜ʖ ͡°)
|
||||
</div>
|
||||
<h4>Content</h4>
|
||||
<br>
|
||||
A bunch of more content and
|
||||
<a :style="{ color: linkColorLocal }">a nice lil' link</a>
|
||||
<i :style="{ color: blueColorLocal }" class="icon-reply"/>
|
||||
<i :style="{ color: greenColorLocal }" class="icon-retweet"/>
|
||||
<i :style="{ color: redColorLocal }" class="icon-cancel"/>
|
||||
<i :style="{ color: orangeColorLocal }" class="icon-star"/>
|
||||
<br>
|
||||
<button class="btn" :style="{ 'background-color': btnColorLocal, 'color': textColorLocal }">Button</button>
|
||||
<preview :style="previewRules"/>
|
||||
</div>
|
||||
|
||||
<keep-alive>
|
||||
<tab-switcher key="style-tweak">
|
||||
<div :label="$t('settings.style.common_colors._tab_label')" class="color-container">
|
||||
<div class="tab-header">
|
||||
<p>{{$t('settings.theme_help')}}</p>
|
||||
<button class="btn" @click="clearOpacity">{{$t('settings.style.switcher.clear_opacity')}}</button>
|
||||
<button class="btn" @click="clearV1">{{$t('settings.style.switcher.clear_all')}}</button>
|
||||
</div>
|
||||
<p>{{$t('settings.theme_help_v2_1')}}</p>
|
||||
<h4>{{ $t('settings.style.common_colors.main') }}</h4>
|
||||
<div class="color-item">
|
||||
<ColorInput name="bgColor" v-model="bgColorLocal" :label="$t('settings.background')"/>
|
||||
<OpacityInput name="bgOpacity" v-model="bgOpacityLocal" :fallback="previewTheme.opacity.bg || 1"/>
|
||||
<ColorInput name="textColor" v-model="textColorLocal" :label="$t('settings.text')"/>
|
||||
<ContrastRatio :contrast="previewContrast.bgText"/>
|
||||
<ColorInput name="linkColor" v-model="linkColorLocal" :label="$t('settings.links')"/>
|
||||
<ContrastRatio :contrast="previewContrast.bgLink"/>
|
||||
</div>
|
||||
<div class="color-item">
|
||||
<ColorInput name="fgColor" v-model="fgColorLocal" :label="$t('settings.foreground')"/>
|
||||
<ColorInput name="fgTextColor" v-model="fgTextColorLocal" :label="$t('settings.text')" :fallback="previewTheme.colors.fgText"/>
|
||||
<ColorInput name="fgLinkColor" v-model="fgLinkColorLocal" :label="$t('settings.links')" :fallback="previewTheme.colors.fgLink"/>
|
||||
<p>{{ $t('settings.style.common_colors.foreground_hint') }}</p>
|
||||
</div>
|
||||
<h4>{{ $t('settings.style.common_colors.rgbo') }}</h4>
|
||||
<div class="color-item">
|
||||
<ColorInput name="cRedColor" v-model="cRedColorLocal" :label="$t('settings.cRed')"/>
|
||||
<ContrastRatio :contrast="previewContrast.bgRed"/>
|
||||
<ColorInput name="cBlueColor" v-model="cBlueColorLocal" :label="$t('settings.cBlue')"/>
|
||||
<ContrastRatio :contrast="previewContrast.bgBlue"/>
|
||||
</div>
|
||||
<div class="color-item">
|
||||
<ColorInput name="cGreenColor" v-model="cGreenColorLocal" :label="$t('settings.cGreen')"/>
|
||||
<ContrastRatio :contrast="previewContrast.bgGreen"/>
|
||||
<ColorInput name="cOrangeColor" v-model="cOrangeColorLocal" :label="$t('settings.cOrange')"/>
|
||||
<ContrastRatio :contrast="previewContrast.bgOrange"/>
|
||||
</div>
|
||||
<p>{{$t('settings.theme_help_v2_2')}}</p>
|
||||
</div>
|
||||
|
||||
<div :label="$t('settings.style.advanced_colors._tab_label')" class="color-container">
|
||||
<div class="tab-header">
|
||||
<p>{{$t('settings.theme_help')}}</p>
|
||||
<button class="btn" @click="clearOpacity">{{$t('settings.style.switcher.clear_opacity')}}</button>
|
||||
<button class="btn" @click="clearV1">{{$t('settings.style.switcher.clear_all')}}</button>
|
||||
</div>
|
||||
<div class="color-item">
|
||||
<h4>{{ $t('settings.style.advanced_colors.alert') }}</h4>
|
||||
<ColorInput name="alertError" v-model="alertErrorColorLocal" :label="$t('settings.style.advanced_colors.alert_error')" :fallback="previewTheme.colors.alertError"/>
|
||||
<ContrastRatio :contrast="previewContrast.alertError"/>
|
||||
</div>
|
||||
<div class="color-item">
|
||||
<h4>{{ $t('settings.style.advanced_colors.badge') }}</h4>
|
||||
<ColorInput name="badgeNotification" v-model="badgeNotificationColorLocal" :label="$t('settings.style.advanced_colors.badge_notification')" :fallback="previewTheme.colors.badgeNotification"/>
|
||||
</div>
|
||||
<div class="color-item">
|
||||
<h4>{{ $t('settings.style.advanced_colors.panel_header') }}</h4>
|
||||
<ColorInput name="panelColor" v-model="panelColorLocal" :fallback="fgColorLocal" :label="$t('settings.background')"/>
|
||||
<OpacityInput name="panelOpacity" v-model="panelOpacityLocal" :fallback="previewTheme.opacity.panel || 1"/>
|
||||
<ColorInput name="panelTextColor" v-model="panelTextColorLocal" :fallback="previewTheme.colors.panelText" :label="$t('settings.text')"/>
|
||||
<ContrastRatio :contrast="previewContrast.panelText" large="1"/>
|
||||
<ColorInput name="panelLinkColor" v-model="panelLinkColorLocal" :fallback="previewTheme.colors.panelLink" :label="$t('settings.links')"/>
|
||||
<ContrastRatio :contrast="previewContrast.panelLink" large="1"/>
|
||||
</div>
|
||||
<div class="color-item">
|
||||
<h4>{{ $t('settings.style.advanced_colors.top_bar') }}</h4>
|
||||
<ColorInput name="topBarColor" v-model="topBarColorLocal" :fallback="fgColorLocal" :label="$t('settings.background')"/>
|
||||
<ColorInput name="topBarTextColor" v-model="topBarTextColorLocal" :fallback="previewTheme.colors.topBarText" :label="$t('settings.text')"/>
|
||||
<ContrastRatio :contrast="previewContrast.topBarText"/>
|
||||
<ColorInput name="topBarLinkColor" v-model="topBarLinkColorLocal" :fallback="previewTheme.colors.topBarLink" :label="$t('settings.links')"/>
|
||||
<ContrastRatio :contrast="previewContrast.topBarLink"/>
|
||||
</div>
|
||||
<div class="color-item">
|
||||
<h4>{{ $t('settings.style.advanced_colors.inputs') }}</h4>
|
||||
<ColorInput name="inputColor" v-model="inputColorLocal" :fallback="fgColorLocal" :label="$t('settings.background')"/>
|
||||
<OpacityInput name="inputOpacity" v-model="inputOpacityLocal" :fallback="previewTheme.opacity.input || 1"/>
|
||||
<ColorInput name="inputTextColor" v-model="inputTextColorLocal" :fallback="previewTheme.colors.inputText" :label="$t('settings.text')"/>
|
||||
<ContrastRatio :contrast="previewContrast.inputText"/>
|
||||
</div>
|
||||
<div class="color-item">
|
||||
<h4>{{ $t('settings.style.advanced_colors.buttons') }}</h4>
|
||||
<ColorInput name="btnColor" v-model="btnColorLocal" :fallback="fgColorLocal" :label="$t('settings.background')"/>
|
||||
<OpacityInput name="btnOpacity" v-model="btnOpacityLocal" :fallback="previewTheme.opacity.btn || 1"/>
|
||||
<ColorInput name="btnTextColor" v-model="btnTextColorLocal" :fallback="previewTheme.colors.btnText" :label="$t('settings.text')"/>
|
||||
<ContrastRatio :contrast="previewContrast.btnText"/>
|
||||
</div>
|
||||
<div class="color-item">
|
||||
<h4>{{ $t('settings.style.advanced_colors.borders') }}</h4>
|
||||
<ColorInput name="borderColor" v-model="borderColorLocal" :fallback="previewTheme.colors.border" :label="$t('settings.style.common.color')"/>
|
||||
<OpacityInput name="borderOpacity" v-model="borderOpacityLocal" :fallback="previewTheme.opacity.border || 1"/>
|
||||
</div>
|
||||
<div class="color-item">
|
||||
<h4>{{ $t('settings.style.advanced_colors.faint_text') }}</h4>
|
||||
<ColorInput name="faintColor" v-model="faintColorLocal" :fallback="previewTheme.colors.faint || 1" :label="$t('settings.text')"/>
|
||||
<ColorInput name="faintLinkColor" v-model="faintLinkColorLocal" :fallback="previewTheme.colors.faintLink" :label="$t('settings.links')"/>
|
||||
<ColorInput name="panelFaintColor" v-model="panelFaintColorLocal" :fallback="previewTheme.colors.panelFaint" :label="$t('settings.style.advanced_colors.panel_header')"/>
|
||||
<OpacityInput name="faintOpacity" v-model="faintOpacityLocal" :fallback="previewTheme.opacity.faint || 0.5"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="color-container">
|
||||
<p>{{$t('settings.theme_help')}}</p>
|
||||
<div class="color-item">
|
||||
<label for="bgcolor" class="theme-color-lb">{{$t('settings.background')}}</label>
|
||||
<input id="bgcolor" class="theme-color-cl" type="color" v-model="bgColorLocal">
|
||||
<input id="bgcolor-t" class="theme-color-in" type="text" v-model="bgColorLocal">
|
||||
</div>
|
||||
<div class="color-item">
|
||||
<label for="fgcolor" class="theme-color-lb">{{$t('settings.foreground')}}</label>
|
||||
<input id="fgcolor" class="theme-color-cl" type="color" v-model="btnColorLocal">
|
||||
<input id="fgcolor-t" class="theme-color-in" type="text" v-model="btnColorLocal">
|
||||
</div>
|
||||
<div class="color-item">
|
||||
<label for="textcolor" class="theme-color-lb">{{$t('settings.text')}}</label>
|
||||
<input id="textcolor" class="theme-color-cl" type="color" v-model="textColorLocal">
|
||||
<input id="textcolor-t" class="theme-color-in" type="text" v-model="textColorLocal">
|
||||
</div>
|
||||
<div class="color-item">
|
||||
<label for="linkcolor" class="theme-color-lb">{{$t('settings.links')}}</label>
|
||||
<input id="linkcolor" class="theme-color-cl" type="color" v-model="linkColorLocal">
|
||||
<input id="linkcolor-t" class="theme-color-in" type="text" v-model="linkColorLocal">
|
||||
</div>
|
||||
<div class="color-item">
|
||||
<label for="redcolor" class="theme-color-lb">{{$t('settings.cRed')}}</label>
|
||||
<input id="redcolor" class="theme-color-cl" type="color" v-model="redColorLocal">
|
||||
<input id="redcolor-t" class="theme-color-in" type="text" v-model="redColorLocal">
|
||||
</div>
|
||||
<div class="color-item">
|
||||
<label for="bluecolor" class="theme-color-lb">{{$t('settings.cBlue')}}</label>
|
||||
<input id="bluecolor" class="theme-color-cl" type="color" v-model="blueColorLocal">
|
||||
<input id="bluecolor-t" class="theme-color-in" type="text" v-model="blueColorLocal">
|
||||
</div>
|
||||
<div class="color-item">
|
||||
<label for="greencolor" class="theme-color-lb">{{$t('settings.cGreen')}}</label>
|
||||
<input id="greencolor" class="theme-color-cl" type="color" v-model="greenColorLocal">
|
||||
<input id="greencolor-t" class="theme-color-in" type="green" v-model="greenColorLocal">
|
||||
</div>
|
||||
<div class="color-item">
|
||||
<label for="orangecolor" class="theme-color-lb">{{$t('settings.cOrange')}}</label>
|
||||
<input id="orangecolor" class="theme-color-cl" type="color" v-model="orangeColorLocal">
|
||||
<input id="orangecolor-t" class="theme-color-in" type="text" v-model="orangeColorLocal">
|
||||
</div>
|
||||
</div>
|
||||
<div :label="$t('settings.style.radii._tab_label')" class="radius-container">
|
||||
<div class="tab-header">
|
||||
<p>{{$t('settings.radii_help')}}</p>
|
||||
<button class="btn" @click="clearRoundness">{{$t('settings.style.switcher.clear_all')}}</button>
|
||||
</div>
|
||||
<RangeInput name="btnRadius" :label="$t('settings.btnRadius')" v-model="btnRadiusLocal" :fallback="previewTheme.radii.btn" max="16" hardMin="0"/>
|
||||
<RangeInput name="inputRadius" :label="$t('settings.inputRadius')" v-model="inputRadiusLocal" :fallback="previewTheme.radii.input" max="9" hardMin="0"/>
|
||||
<RangeInput name="checkboxRadius" :label="$t('settings.checkboxRadius')" v-model="checkboxRadiusLocal" :fallback="previewTheme.radii.checkbox" max="16" hardMin="0"/>
|
||||
<RangeInput name="panelRadius" :label="$t('settings.panelRadius')" v-model="panelRadiusLocal" :fallback="previewTheme.radii.panel" max="50" hardMin="0"/>
|
||||
<RangeInput name="avatarRadius" :label="$t('settings.avatarRadius')" v-model="avatarRadiusLocal" :fallback="previewTheme.radii.avatar" max="28" hardMin="0"/>
|
||||
<RangeInput name="avatarAltRadius" :label="$t('settings.avatarAltRadius')" v-model="avatarAltRadiusLocal" :fallback="previewTheme.radii.avatarAlt" max="28" hardMin="0"/>
|
||||
<RangeInput name="attachmentRadius" :label="$t('settings.attachmentRadius')" v-model="attachmentRadiusLocal" :fallback="previewTheme.radii.attachment" max="50" hardMin="0"/>
|
||||
<RangeInput name="tooltipRadius" :label="$t('settings.tooltipRadius')" v-model="tooltipRadiusLocal" :fallback="previewTheme.radii.tooltip" max="50" hardMin="0"/>
|
||||
</div>
|
||||
|
||||
<div class="radius-container">
|
||||
<p>{{$t('settings.radii_help')}}</p>
|
||||
<div class="radius-item">
|
||||
<label for="btnradius" class="theme-radius-lb">{{$t('settings.btnRadius')}}</label>
|
||||
<input id="btnradius" class="theme-radius-rn" type="range" v-model="btnRadiusLocal" max="16">
|
||||
<input id="btnradius-t" class="theme-radius-in" type="text" v-model="btnRadiusLocal">
|
||||
</div>
|
||||
<div class="radius-item">
|
||||
<label for="inputradius" class="theme-radius-lb">{{$t('settings.inputRadius')}}</label>
|
||||
<input id="inputradius" class="theme-radius-rn" type="range" v-model="inputRadiusLocal" max="16">
|
||||
<input id="inputradius-t" class="theme-radius-in" type="text" v-model="inputRadiusLocal">
|
||||
</div>
|
||||
<div class="radius-item">
|
||||
<label for="panelradius" class="theme-radius-lb">{{$t('settings.panelRadius')}}</label>
|
||||
<input id="panelradius" class="theme-radius-rn" type="range" v-model="panelRadiusLocal" max="50">
|
||||
<input id="panelradius-t" class="theme-radius-in" type="text" v-model="panelRadiusLocal">
|
||||
</div>
|
||||
<div class="radius-item">
|
||||
<label for="avatarradius" class="theme-radius-lb">{{$t('settings.avatarRadius')}}</label>
|
||||
<input id="avatarradius" class="theme-radius-rn" type="range" v-model="avatarRadiusLocal" max="28">
|
||||
<input id="avatarradius-t" class="theme-radius-in" type="green" v-model="avatarRadiusLocal">
|
||||
</div>
|
||||
<div class="radius-item">
|
||||
<label for="avataraltradius" class="theme-radius-lb">{{$t('settings.avatarAltRadius')}}</label>
|
||||
<input id="avataraltradius" class="theme-radius-rn" type="range" v-model="avatarAltRadiusLocal" max="28">
|
||||
<input id="avataraltradius-t" class="theme-radius-in" type="text" v-model="avatarAltRadiusLocal">
|
||||
</div>
|
||||
<div class="radius-item">
|
||||
<label for="attachmentradius" class="theme-radius-lb">{{$t('settings.attachmentRadius')}}</label>
|
||||
<input id="attachmentrradius" class="theme-radius-rn" type="range" v-model="attachmentRadiusLocal" max="50">
|
||||
<input id="attachmentradius-t" class="theme-radius-in" type="text" v-model="attachmentRadiusLocal">
|
||||
</div>
|
||||
<div class="radius-item">
|
||||
<label for="tooltipradius" class="theme-radius-lb">{{$t('settings.tooltipRadius')}}</label>
|
||||
<input id="tooltipradius" class="theme-radius-rn" type="range" v-model="tooltipRadiusLocal" max="20">
|
||||
<input id="tooltipradius-t" class="theme-radius-in" type="text" v-model="tooltipRadiusLocal">
|
||||
</div>
|
||||
</div>
|
||||
<div :label="$t('settings.style.shadows._tab_label')" class="shadow-container">
|
||||
<div class="tab-header shadow-selector">
|
||||
<div class="select-container">
|
||||
{{$t('settings.style.shadows.component')}}
|
||||
<label for="shadow-switcher" class="select">
|
||||
<select id="shadow-switcher" v-model="shadowSelected" class="shadow-switcher">
|
||||
<option v-for="shadow in shadowsAvailable"
|
||||
:value="shadow">
|
||||
{{$t('settings.style.shadows.components.' + shadow)}}
|
||||
</option>
|
||||
</select>
|
||||
<i class="icon-down-open"/>
|
||||
</label>
|
||||
</div>
|
||||
<div class="override">
|
||||
<label for="override" class="label">
|
||||
{{$t('settings.style.shadows.override')}}
|
||||
</label>
|
||||
<input
|
||||
v-model="currentShadowOverriden"
|
||||
name="override"
|
||||
id="override"
|
||||
class="input-override"
|
||||
type="checkbox">
|
||||
<label class="checkbox-label" for="override"></label>
|
||||
</div>
|
||||
<button class="btn" @click="clearShadows">{{$t('settings.style.switcher.clear_all')}}</button>
|
||||
</div>
|
||||
<shadow-control :ready="!!currentShadowFallback" :fallback="currentShadowFallback" v-model="currentShadow"/>
|
||||
<div v-if="shadowSelected === 'avatar' || shadowSelected === 'avatarStatus'">
|
||||
<i18n path="settings.style.shadows.filter_hint.always_drop_shadow" tag="p">
|
||||
<code>filter: drop-shadow()</code>
|
||||
</i18n>
|
||||
<p>{{$t('settings.style.shadows.filter_hint.avatar_inset')}}</p>
|
||||
<i18n path="settings.style.shadows.filter_hint.drop_shadow_syntax" tag="p">
|
||||
<code>drop-shadow</code>
|
||||
<code>spread-radius</code>
|
||||
<code>inset</code>
|
||||
</i18n>
|
||||
<i18n path="settings.style.shadows.filter_hint.inset_classic" tag="p">
|
||||
<code>box-shadow</code>
|
||||
</i18n>
|
||||
<p>{{$t('settings.style.shadows.filter_hint.spread_zero')}}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div :label="$t('settings.style.fonts._tab_label')" class="fonts-container">
|
||||
<div class="tab-header">
|
||||
<p>{{$t('settings.style.fonts.help')}}</p>
|
||||
<button class="btn" @click="clearFonts">{{$t('settings.style.switcher.clear_all')}}</button>
|
||||
</div>
|
||||
<FontControl
|
||||
name="ui"
|
||||
v-model="fontsLocal.interface"
|
||||
:label="$t('settings.style.fonts.components.interface')"
|
||||
:fallback="previewTheme.fonts.interface"
|
||||
no-inherit="1"/>
|
||||
<FontControl
|
||||
name="input"
|
||||
v-model="fontsLocal.input"
|
||||
:label="$t('settings.style.fonts.components.input')"
|
||||
:fallback="previewTheme.fonts.input"/>
|
||||
<FontControl
|
||||
name="post"
|
||||
v-model="fontsLocal.post"
|
||||
:label="$t('settings.style.fonts.components.post')"
|
||||
:fallback="previewTheme.fonts.post"/>
|
||||
<FontControl
|
||||
name="postCode"
|
||||
v-model="fontsLocal.postCode"
|
||||
:label="$t('settings.style.fonts.components.postCode')"
|
||||
:fallback="previewTheme.fonts.postCode"/>
|
||||
</div>
|
||||
</tab-switcher>
|
||||
</keep-alive>
|
||||
|
||||
<div class="apply-container">
|
||||
<button class="btn submit" @click="setCustomTheme">{{$t('general.apply')}}</button>
|
||||
<button class="btn submit" :disabled="!themeValid" @click="setCustomTheme">{{$t('general.apply')}}</button>
|
||||
<button class="btn" @click="clearAll">{{$t('settings.style.switcher.reset')}}</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./style_switcher.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
.style-switcher {
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
.import-warning {
|
||||
color: $fallback--cRed;
|
||||
color: var(--cRed, $fallback--cRed);
|
||||
}
|
||||
|
||||
.apply-container,
|
||||
.radius-container,
|
||||
.color-container,
|
||||
.presets-container {
|
||||
display: flex;
|
||||
|
||||
p {
|
||||
flex: 2 0 100%;
|
||||
margin-top: 2em;
|
||||
margin-bottom: .5em;
|
||||
}
|
||||
}
|
||||
|
||||
.radius-container {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.color-container {
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.presets-container {
|
||||
justify-content: center;
|
||||
.import-export {
|
||||
display: flex;
|
||||
|
||||
.btn {
|
||||
margin-left: .5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.preview-container {
|
||||
border-top: 1px dashed;
|
||||
border-bottom: 1px dashed;
|
||||
border-color: $fallback--border;
|
||||
border-color: var(--border, $fallback--border);
|
||||
margin: 1em -1em 0;
|
||||
padding: 1em;
|
||||
|
||||
.btn {
|
||||
margin-top: 1em;
|
||||
min-height: 30px;
|
||||
width: 10em;
|
||||
}
|
||||
}
|
||||
|
||||
.apply-container {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.radius-item,
|
||||
.color-item {
|
||||
min-width: 20em;
|
||||
display:flex;
|
||||
flex: 1 1 0;
|
||||
align-items: baseline;
|
||||
margin: 5px 6px 5px 0;
|
||||
|
||||
label {
|
||||
color: var(--faint, $fallback--faint);
|
||||
}
|
||||
}
|
||||
|
||||
.radius-item {
|
||||
flex-basis: auto;
|
||||
}
|
||||
|
||||
.theme-radius-rn,
|
||||
.theme-color-cl {
|
||||
border: 0;
|
||||
box-shadow: none;
|
||||
background: transparent;
|
||||
color: var(--faint, $fallback--faint);
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
.theme-color-cl,
|
||||
.theme-radius-in,
|
||||
.theme-color-in {
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.theme-color-in {
|
||||
min-width: 4em;
|
||||
}
|
||||
|
||||
.theme-radius-in {
|
||||
min-width: 1em;
|
||||
}
|
||||
|
||||
.theme-radius-in,
|
||||
.theme-color-in {
|
||||
max-width: 7em;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.theme-radius-lb,
|
||||
.theme-color-lb {
|
||||
flex: 2;
|
||||
min-width: 7em;
|
||||
}
|
||||
|
||||
.theme-radius-lb{
|
||||
max-width: 50em;
|
||||
}
|
||||
|
||||
.theme-color-lb {
|
||||
max-width: 10em;
|
||||
}
|
||||
|
||||
.theme-color-cl {
|
||||
padding: 1px;
|
||||
max-width: 8em;
|
||||
height: 100%;
|
||||
flex: 0;
|
||||
min-width: 2em;
|
||||
cursor: pointer;
|
||||
max-height: 29px;
|
||||
}
|
||||
|
||||
.theme-preview-content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.dummy {
|
||||
.avatar {
|
||||
background: linear-gradient(135deg, #b8e1fc 0%,#a9d2f3 10%,#90bae4 25%,#90bcea 37%,#90bff0 50%,#6ba8e5 51%,#a2daf5 83%,#bdf3fd 100%);
|
||||
color: black;
|
||||
text-align: center;
|
||||
height: 48px;
|
||||
line-height: 48px;
|
||||
width: 48px;
|
||||
float: left;
|
||||
margin-right: 1em;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style src="./style_switcher.scss" lang="scss"></style>
|
||||
|
|
|
@ -18,18 +18,27 @@ export default Vue.component('tab-switcher', {
|
|||
const tabs = this.$slots.default
|
||||
.filter(slot => slot.data)
|
||||
.map((slot, index) => {
|
||||
const classes = ['tab']
|
||||
const classesTab = ['tab']
|
||||
const classesWrapper = ['tab-wrapper']
|
||||
|
||||
if (index === this.active) {
|
||||
classes.push('active')
|
||||
classesTab.push('active')
|
||||
classesWrapper.push('active')
|
||||
}
|
||||
return (<button onClick={this.activateTab(index)} class={ classes.join(' ') }>{slot.data.attrs.label}</button>)
|
||||
return (
|
||||
<div class={ classesWrapper.join(' ')}>
|
||||
<button onClick={this.activateTab(index)} class={ classesTab.join(' ') }>{slot.data.attrs.label}</button>
|
||||
</div>
|
||||
)
|
||||
});
|
||||
const contents = (
|
||||
<div>
|
||||
{this.$slots.default.filter(slot => slot.data)[this.active]}
|
||||
</div>
|
||||
);
|
||||
const contents = this.$slots.default.filter(_=>_.data).map(( slot, index ) => {
|
||||
const active = index === this.active
|
||||
return (
|
||||
<div class={active ? 'active' : 'hidden'}>
|
||||
{slot}
|
||||
</div>
|
||||
)
|
||||
});
|
||||
return (
|
||||
<div class="tab-switcher">
|
||||
<div class="tabs">
|
||||
|
|
|
@ -1,43 +1,75 @@
|
|||
@import '../../_variables.scss';
|
||||
|
||||
.tab-switcher {
|
||||
.contents {
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.tabs {
|
||||
display: flex;
|
||||
position: relative;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
overflow-y: hidden;
|
||||
overflow-x: auto;
|
||||
padding-top: 5px;
|
||||
box-sizing: border-box;
|
||||
|
||||
&::after, &::before {
|
||||
display: block;
|
||||
content: '';
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.tab, &::after, &::before {
|
||||
border-bottom: 1px solid;
|
||||
border-bottom-color: $fallback--btn;
|
||||
border-bottom-color: var(--btn, $fallback--btn);
|
||||
border-bottom-color: $fallback--border;
|
||||
border-bottom-color: var(--border, $fallback--border);
|
||||
}
|
||||
|
||||
.tab {
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
padding: .3em 1em;
|
||||
.tab-wrapper {
|
||||
height: 28px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
|
||||
.tab {
|
||||
width: 100%;
|
||||
min-width: 1px;
|
||||
position: relative;
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
padding: 6px 1em;
|
||||
padding-bottom: 99px;
|
||||
margin-bottom: 6px - 99px;
|
||||
white-space: nowrap;
|
||||
|
||||
&:not(.active) {
|
||||
z-index: 4;
|
||||
|
||||
&:hover {
|
||||
z-index: 6;
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: transparent;
|
||||
z-index: 5;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.active) {
|
||||
border-bottom: 1px solid;
|
||||
border-bottom-color: $fallback--btn;
|
||||
border-bottom-color: var(--btn, $fallback--btn);
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: transparent;
|
||||
border-bottom: none;
|
||||
z-index: 5;
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 7;
|
||||
border-bottom: 1px solid;
|
||||
border-bottom-color: $fallback--border;
|
||||
border-bottom-color: var(--border, $fallback--border);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ import Status from '../status/status.vue'
|
|||
import timelineFetcher from '../../services/timeline_fetcher/timeline_fetcher.service.js'
|
||||
import StatusOrConversation from '../status_or_conversation/status_or_conversation.vue'
|
||||
import UserCard from '../user_card/user_card.vue'
|
||||
import { throttle } from 'lodash'
|
||||
|
||||
const Timeline = {
|
||||
props: [
|
||||
|
@ -88,7 +89,7 @@ const Timeline = {
|
|||
this.paused = false
|
||||
}
|
||||
},
|
||||
fetchOlderStatuses () {
|
||||
fetchOlderStatuses: throttle(function () {
|
||||
const store = this.$store
|
||||
const credentials = store.state.users.currentUser.credentials
|
||||
store.commit('setLoading', { timeline: this.timelineName, value: true })
|
||||
|
@ -101,7 +102,7 @@ const Timeline = {
|
|||
userId: this.userId,
|
||||
tag: this.tag
|
||||
}).then(() => store.commit('setLoading', { timeline: this.timelineName, value: false }))
|
||||
},
|
||||
}, 1000, this),
|
||||
fetchFollowers () {
|
||||
const id = this.userId
|
||||
this.$store.state.api.backendInteractor.fetchFollowers({ id })
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
<button @click.prevent="showNewStatuses" class="loadmore-button" v-if="timeline.newStatusCount > 0 && !timelineError">
|
||||
{{$t('timeline.show_new')}}{{newStatusCountStr}}
|
||||
</button>
|
||||
<div @click.prevent class="loadmore-text" v-if="!timeline.newStatusCount > 0 && !timelineError">
|
||||
<div @click.prevent class="loadmore-text faint" v-if="!timeline.newStatusCount > 0 && !timelineError">
|
||||
{{$t('timeline.up_to_date')}}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -58,15 +58,7 @@
|
|||
|
||||
.timeline {
|
||||
.loadmore-text {
|
||||
opacity: 0.8;
|
||||
background-color: transparent;
|
||||
color: $fallback--faint;
|
||||
color: var(--faint, $fallback--faint);
|
||||
}
|
||||
|
||||
.loadmore-error {
|
||||
color: $fallback--fg;
|
||||
color: var(--fg, $fallback--fg);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -79,7 +71,7 @@
|
|||
border-color: var(--border, $fallback--border);
|
||||
padding: 10px;
|
||||
z-index: 1;
|
||||
background-color: $fallback--btn;
|
||||
background-color: var(--btn, $fallback--btn);
|
||||
background-color: $fallback--fg;
|
||||
background-color: var(--panel, $fallback--fg);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -15,6 +15,9 @@ const UserCard = {
|
|||
components: {
|
||||
UserCardContent
|
||||
},
|
||||
computed: {
|
||||
currentUser () { return this.$store.state.users.currentUser }
|
||||
},
|
||||
methods: {
|
||||
toggleUserExpanded () {
|
||||
this.userExpanded = !this.userExpanded
|
||||
|
|
|
@ -10,13 +10,13 @@
|
|||
<div :title="user.name" v-if="user.name_html" class="user-name">
|
||||
<span v-html="user.name_html"></span>
|
||||
<span class="follows-you" v-if="!userExpanded && showFollows && user.follows_you">
|
||||
{{ $t('user_card.follows_you') }}
|
||||
{{ currentUser.id == user.id ? $t('user_card.its_you') : $t('user_card.follows_you') }}
|
||||
</span>
|
||||
</div>
|
||||
<div :title="user.name" v-else class="user-name">
|
||||
{{ user.name }}
|
||||
<span class="follows-you" v-if="!userExpanded && showFollows && user.follows_you">
|
||||
{{ $t('user_card.follows_you') }}
|
||||
{{ currentUser.id == user.id ? $t('user_card.its_you') : $t('user_card.follows_you') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -6,21 +6,37 @@ export default {
|
|||
props: [ 'user', 'switcher', 'selected', 'hideBio', 'activatePanel' ],
|
||||
data () {
|
||||
return {
|
||||
followRequestInProgress: false,
|
||||
followRequestSent: false,
|
||||
hideUserStatsLocal: typeof this.$store.state.config.hideUserStats === 'undefined'
|
||||
? this.$store.state.instance.hideUserStats
|
||||
: this.$store.state.config.hideUserStats
|
||||
: this.$store.state.config.hideUserStats,
|
||||
betterShadow: this.$store.state.interface.browserSupport.cssFilter
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
headingStyle () {
|
||||
const color = this.$store.state.config.colors.bg
|
||||
const color = this.$store.state.config.customTheme.colors
|
||||
? this.$store.state.config.customTheme.colors.bg // v2
|
||||
: this.$store.state.config.colors.bg // v1
|
||||
|
||||
if (color) {
|
||||
const rgb = hex2rgb(color)
|
||||
const rgb = (typeof color === 'string') ? hex2rgb(color) : color
|
||||
const tintColor = `rgba(${Math.floor(rgb.r)}, ${Math.floor(rgb.g)}, ${Math.floor(rgb.b)}, .5)`
|
||||
|
||||
const gradient = [
|
||||
[tintColor, this.hideBio ? '60%' : ''],
|
||||
this.hideBio ? [
|
||||
color, '100%'
|
||||
] : [
|
||||
tintColor, ''
|
||||
]
|
||||
].map(_ => _.join(' ')).join(', ')
|
||||
|
||||
return {
|
||||
backgroundColor: `rgb(${Math.floor(rgb.r * 0.53)}, ${Math.floor(rgb.g * 0.56)}, ${Math.floor(rgb.b * 0.59)})`,
|
||||
backgroundImage: [
|
||||
`linear-gradient(to bottom, ${tintColor}, ${tintColor})`,
|
||||
`linear-gradient(to bottom, ${gradient})`,
|
||||
`url(${this.user.cover_photo})`
|
||||
].join(', ')
|
||||
}
|
||||
|
@ -71,13 +87,68 @@ export default {
|
|||
methods: {
|
||||
followUser () {
|
||||
const store = this.$store
|
||||
this.followRequestInProgress = true
|
||||
store.state.api.backendInteractor.followUser(this.user.id)
|
||||
.then((followedUser) => store.commit('addNewUsers', [followedUser]))
|
||||
.then(() => {
|
||||
// For locked users we just mark it that we sent the follow request
|
||||
if (this.user.locked) {
|
||||
this.followRequestInProgress = false
|
||||
this.followRequestSent = true
|
||||
return
|
||||
}
|
||||
|
||||
if (this.user.following) {
|
||||
// If we get result immediately, just stop.
|
||||
this.followRequestInProgress = false
|
||||
return
|
||||
}
|
||||
|
||||
// But usually we don't get result immediately, so we ask server
|
||||
// for updated user profile to confirm if we are following them
|
||||
// Sometimes it takes several tries. Sometimes we end up not following
|
||||
// user anyway, probably because they locked themselves and we
|
||||
// don't know that yet.
|
||||
// Recursive Promise, it will call itself up to 3 times.
|
||||
const fetchUser = (attempt) => new Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
store.state.api.backendInteractor.fetchUser({ id: this.user.id })
|
||||
.then((user) => store.commit('addNewUsers', [user]))
|
||||
.then(() => resolve([this.user.following, attempt]))
|
||||
.catch((e) => reject(e))
|
||||
}, 500)
|
||||
}).then(([following, attempt]) => {
|
||||
if (!following && attempt <= 3) {
|
||||
// If we BE reports that we still not following that user - retry,
|
||||
// increment attempts by one
|
||||
return fetchUser(++attempt)
|
||||
} else {
|
||||
// If we run out of attempts, just return whatever status is.
|
||||
return following
|
||||
}
|
||||
})
|
||||
|
||||
return fetchUser(1)
|
||||
.then((following) => {
|
||||
if (following) {
|
||||
// We confirmed and everything its good.
|
||||
this.followRequestInProgress = false
|
||||
} else {
|
||||
// If after all the tries, just treat it as if user is locked
|
||||
this.followRequestInProgress = false
|
||||
this.followRequestSent = true
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
unfollowUser () {
|
||||
const store = this.$store
|
||||
this.followRequestInProgress = true
|
||||
store.state.api.backendInteractor.unfollowUser(this.user.id)
|
||||
.then((unfollowedUser) => store.commit('addNewUsers', [unfollowedUser]))
|
||||
.then(() => {
|
||||
this.followRequestInProgress = false
|
||||
})
|
||||
},
|
||||
blockUser () {
|
||||
const store = this.$store
|
||||
|
|
|
@ -2,20 +2,20 @@
|
|||
<div id="heading" class="profile-panel-background" :style="headingStyle">
|
||||
<div class="panel-heading text-center">
|
||||
<div class='user-info'>
|
||||
<router-link @click.native="activatePanel('timeline')" :to="{ name: 'user-settings' }" style="float: right; margin-top:16px;" v-if="!isOtherUser">
|
||||
<router-link @click.native="activatePanel && activatePanel('timeline')" :to="{ name: 'user-settings' }" style="float: right; margin-top:16px;" v-if="!isOtherUser">
|
||||
<i class="icon-cog usersettings" :title="$t('tool_tip.user_settings')"></i>
|
||||
</router-link>
|
||||
<a :href="user.statusnet_profile_url" target="_blank" class="floater" v-if="isOtherUser">
|
||||
<i class="icon-link-ext usersettings"></i>
|
||||
</a>
|
||||
<div class='container'>
|
||||
<router-link :to="userProfileLink(user)">
|
||||
<StillImage class="avatar" :src="user.profile_image_url_original"/>
|
||||
<router-link @click.native="activatePanel && activatePanel('timeline')" :to="userProfileLink(user)">
|
||||
<StillImage class="avatar" :class='{ "better-shadow": betterShadow }' :src="user.profile_image_url_original"/>
|
||||
</router-link>
|
||||
<div class="name-and-screen-name">
|
||||
<div :title="user.name" class='user-name' v-if="user.name_html" v-html="user.name_html"></div>
|
||||
<div :title="user.name" class='user-name' v-else>{{user.name}}</div>
|
||||
<router-link class='user-screen-name' :to="userProfileLink(user)">
|
||||
<router-link @click.native="activatePanel && activatePanel('timeline')" class='user-screen-name' :to="userProfileLink(user)">
|
||||
<span>@{{user.screen_name}}</span><span v-if="user.locked"><i class="icon icon-lock"></i></span>
|
||||
<span v-if="!hideUserStatsLocal" class="dailyAvg">{{dailyAvg}} {{ $t('user_card.per_day') }}</span>
|
||||
</router-link>
|
||||
|
@ -41,74 +41,87 @@
|
|||
</div>
|
||||
</div>
|
||||
<div v-if="isOtherUser" class="user-interactions">
|
||||
<div class="follow" v-if="loggedIn">
|
||||
<span v-if="user.following">
|
||||
<!--Following them!-->
|
||||
<button @click="unfollowUser" class="pressed">
|
||||
<div class="follow" v-if="loggedIn">
|
||||
<span v-if="user.following">
|
||||
<!--Following them!-->
|
||||
<button @click="unfollowUser" class="pressed" :disabled="followRequestInProgress" :title="$t('user_card.follow_unfollow')">
|
||||
<template v-if="followRequestInProgress">
|
||||
{{ $t('user_card.follow_progress') }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ $t('user_card.following') }}
|
||||
</button>
|
||||
</span>
|
||||
<span v-if="!user.following">
|
||||
<button @click="followUser">
|
||||
</template>
|
||||
</button>
|
||||
</span>
|
||||
<span v-if="!user.following">
|
||||
<button @click="followUser" :disabled="followRequestInProgress" :title="followRequestSent ? $t('user_card.follow_again') : ''">
|
||||
<template v-if="followRequestInProgress">
|
||||
{{ $t('user_card.follow_progress') }}
|
||||
</template>
|
||||
<template v-else-if="followRequestSent">
|
||||
{{ $t('user_card.follow_sent') }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ $t('user_card.follow') }}
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<div class='mute' v-if='isOtherUser'>
|
||||
<span v-if='user.muted'>
|
||||
<button @click="toggleMute" class="pressed">
|
||||
{{ $t('user_card.muted') }}
|
||||
</button>
|
||||
</span>
|
||||
<span v-if='!user.muted'>
|
||||
<button @click="toggleMute">
|
||||
{{ $t('user_card.mute') }}
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<div class="remote-follow" v-if='!loggedIn && user.is_local'>
|
||||
<form method="POST" :action='subscribeUrl'>
|
||||
<input type="hidden" name="nickname" :value="user.screen_name">
|
||||
<input type="hidden" name="profile" value="">
|
||||
<button click="submit" class="remote-button">
|
||||
{{ $t('user_card.remote_follow') }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class='block' v-if='isOtherUser && loggedIn'>
|
||||
<span v-if='user.statusnet_blocking'>
|
||||
<button @click="unblockUser" class="pressed">
|
||||
{{ $t('user_card.blocked') }}
|
||||
</button>
|
||||
</span>
|
||||
<span v-if='!user.statusnet_blocking'>
|
||||
<button @click="blockUser">
|
||||
{{ $t('user_card.block') }}
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<div class='mute' v-if='isOtherUser'>
|
||||
<span v-if='user.muted'>
|
||||
<button @click="toggleMute" class="pressed">
|
||||
{{ $t('user_card.muted') }}
|
||||
</button>
|
||||
</span>
|
||||
<span v-if='!user.muted'>
|
||||
<button @click="toggleMute">
|
||||
{{ $t('user_card.mute') }}
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<div class="remote-follow" v-if='!loggedIn && user.is_local'>
|
||||
<form method="POST" :action='subscribeUrl'>
|
||||
<input type="hidden" name="nickname" :value="user.screen_name">
|
||||
<input type="hidden" name="profile" value="">
|
||||
<button click="submit" class="remote-button">
|
||||
{{ $t('user_card.remote_follow') }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class='block' v-if='isOtherUser && loggedIn'>
|
||||
<span v-if='user.statusnet_blocking'>
|
||||
<button @click="unblockUser" class="pressed">
|
||||
{{ $t('user_card.blocked') }}
|
||||
</button>
|
||||
</span>
|
||||
<span v-if='!user.statusnet_blocking'>
|
||||
<button @click="blockUser">
|
||||
{{ $t('user_card.block') }}
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-body profile-panel-body">
|
||||
<div v-if="!hideUserStatsLocal || switcher" class="user-counts" :class="{clickable: switcher}">
|
||||
<div class="user-count" v-on:click.prevent="setProfileView('statuses')" :class="{selected: selected === 'statuses'}">
|
||||
<h5>{{ $t('user_card.statuses') }}</h5>
|
||||
<span v-if="!hideUserStatsLocal">{{user.statuses_count}} <br></span>
|
||||
</div>
|
||||
<div class="user-count" v-on:click.prevent="setProfileView('friends')" :class="{selected: selected === 'friends'}">
|
||||
<h5>{{ $t('user_card.followees') }}</h5>
|
||||
<span v-if="!hideUserStatsLocal">{{user.friends_count}}</span>
|
||||
</div>
|
||||
<div class="user-count" v-on:click.prevent="setProfileView('followers')" :class="{selected: selected === 'followers'}">
|
||||
<h5>{{ $t('user_card.followers') }}</h5>
|
||||
<span v-if="!hideUserStatsLocal">{{user.followers_count}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<p @click.prevent="linkClicked" v-if="!hideBio && user.description_html" class="profile-bio" v-html="user.description_html"></p>
|
||||
<p v-else-if="!hideBio" class="profile-bio">{{ user.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-body profile-panel-body" v-if="!hideBio">
|
||||
<div v-if="!hideUserStatsLocal || switcher" class="user-counts" :class="{clickable: switcher}">
|
||||
<div class="user-count" v-on:click.prevent="setProfileView('statuses')" :class="{selected: selected === 'statuses'}">
|
||||
<h5>{{ $t('user_card.statuses') }}</h5>
|
||||
<span v-if="!hideUserStatsLocal">{{user.statuses_count}} <br></span>
|
||||
</div>
|
||||
<div class="user-count" v-on:click.prevent="setProfileView('friends')" :class="{selected: selected === 'friends'}">
|
||||
<h5>{{ $t('user_card.followees') }}</h5>
|
||||
<span v-if="!hideUserStatsLocal">{{user.friends_count}}</span>
|
||||
</div>
|
||||
<div class="user-count" v-on:click.prevent="setProfileView('followers')" :class="{selected: selected === 'followers'}">
|
||||
<h5>{{ $t('user_card.followers') }}</h5>
|
||||
<span v-if="!hideUserStatsLocal">{{user.followers_count}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<p @click.prevent="linkClicked" v-if="!hideBio && user.description_html" class="profile-bio" v-html="user.description_html"></p>
|
||||
<p v-else-if="!hideBio" class="profile-bio">{{ user.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./user_card_content.js"></script>
|
||||
|
@ -120,10 +133,15 @@
|
|||
background-size: cover;
|
||||
border-radius: $fallback--panelRadius;
|
||||
border-radius: var(--panelRadius, $fallback--panelRadius);
|
||||
overflow: hidden;
|
||||
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
|
||||
.panel-heading {
|
||||
padding: 0.6em 0em;
|
||||
text-align: center;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -138,15 +156,14 @@
|
|||
}
|
||||
|
||||
.user-info {
|
||||
color: $fallback--lightFg;
|
||||
color: var(--lightFg, $fallback--lightFg);
|
||||
color: $fallback--lightText;
|
||||
color: var(--lightText, $fallback--lightText);
|
||||
padding: 0 16px;
|
||||
|
||||
.container {
|
||||
padding: 16px 10px 6px 10px;
|
||||
display: flex;
|
||||
max-height: 56px;
|
||||
overflow: hidden;
|
||||
|
||||
.avatar {
|
||||
border-radius: $fallback--avatarRadius;
|
||||
|
@ -155,8 +172,14 @@
|
|||
width: 56px;
|
||||
height: 56px;
|
||||
box-shadow: 0px 1px 8px rgba(0,0,0,0.75);
|
||||
box-shadow: var(--avatarShadow);
|
||||
object-fit: cover;
|
||||
|
||||
&.better-shadow {
|
||||
box-shadow: var(--avatarShadowInset);
|
||||
filter: var(--avatarShadowFilter)
|
||||
}
|
||||
|
||||
&.animated::before {
|
||||
display: none;
|
||||
}
|
||||
|
@ -173,8 +196,8 @@
|
|||
}
|
||||
|
||||
.usersettings {
|
||||
color: $fallback--lightFg;
|
||||
color: var(--lightFg, $fallback--lightFg);
|
||||
color: $fallback--lightText;
|
||||
color: var(--lightText, $fallback--lightText);
|
||||
opacity: .8;
|
||||
}
|
||||
|
||||
|
@ -185,6 +208,16 @@
|
|||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: 1 1 0;
|
||||
// This is so that text doesn't get overlapped by avatar's shadow if it has
|
||||
// big one
|
||||
z-index: 1;
|
||||
|
||||
img {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
vertical-align: middle;
|
||||
object-fit: contain
|
||||
}
|
||||
}
|
||||
|
||||
.user-name{
|
||||
|
@ -193,8 +226,8 @@
|
|||
}
|
||||
|
||||
.user-screen-name {
|
||||
color: $fallback--lightFg;
|
||||
color: var(--lightFg, $fallback--lightFg);
|
||||
color: $fallback--lightText;
|
||||
color: var(--lightText, $fallback--lightText);
|
||||
display: inline-block;
|
||||
font-weight: light;
|
||||
font-size: 15px;
|
||||
|
@ -269,8 +302,8 @@
|
|||
padding: .5em 1.5em 0em 1.5em;
|
||||
text-align: center;
|
||||
justify-content: space-between;
|
||||
color: $fallback--lightFg;
|
||||
color: var(--lightFg, $fallback--lightFg);
|
||||
color: $fallback--lightText;
|
||||
color: var(--lightText, $fallback--lightText);
|
||||
|
||||
&.clickable {
|
||||
.user-count {
|
||||
|
|
|
@ -11,6 +11,7 @@ const UserFinder = {
|
|||
},
|
||||
toggleHidden () {
|
||||
this.hidden = !this.hidden
|
||||
this.$emit('toggled', this.hidden)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,15 @@
|
|||
<template>
|
||||
<span class="user-finder-container">
|
||||
<div class="user-finder-container">
|
||||
<i class="icon-spin4 user-finder-icon animate-spin-slow" v-if="loading" />
|
||||
<a href="#" v-if="hidden" :title="$t('finder.find_user')" ><i class="icon-user-plus user-finder-icon" @click.prevent.stop="toggleHidden" /></a>
|
||||
<span v-else>
|
||||
<a href="#" v-if="hidden" :title="$t('finder.find_user')"><i class="icon-user-plus user-finder-icon" @click.prevent.stop="toggleHidden" /></a>
|
||||
<template v-else>
|
||||
<input class="user-finder-input" @keyup.enter="findUser(username)" v-model="username" :placeholder="$t('finder.find_user')" id="user-finder-input" type="text"/>
|
||||
<button class="btn search-button" @click="findUser(username)">
|
||||
<i class="icon-search"/>
|
||||
</button>
|
||||
<i class="icon-cancel user-finder-icon" @click.prevent.stop="toggleHidden"/>
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./user_finder.js"></script>
|
||||
|
@ -15,13 +18,24 @@
|
|||
@import '../../_variables.scss';
|
||||
|
||||
.user-finder-container {
|
||||
height: 29px;
|
||||
max-width: 100%;
|
||||
}
|
||||
display: inline-flex;
|
||||
align-items: baseline;
|
||||
vertical-align: baseline;
|
||||
|
||||
.user-finder-input {
|
||||
max-width: 80%;
|
||||
vertical-align: middle;
|
||||
|
||||
.user-finder-input,
|
||||
.search-button {
|
||||
height: 29px;
|
||||
}
|
||||
.user-finder-input {
|
||||
max-width: 80%;
|
||||
}
|
||||
|
||||
.search-button {
|
||||
margin-left: .5em;
|
||||
margin-right: .5em;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
|
@ -3,6 +3,16 @@
|
|||
<div v-if="user" class="user-profile panel panel-default">
|
||||
<user-card-content :user="user" :switcher="true" :selected="timeline.viewing"></user-card-content>
|
||||
</div>
|
||||
<div v-else class="panel user-profile-placeholder">
|
||||
<div class="panel-heading">
|
||||
<div class="title">
|
||||
{{ $t('settings.profile_tab') }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<i class="icon-spin3 animate-spin"></i>
|
||||
</div>
|
||||
</div>
|
||||
<Timeline :title="$t('user_profile.timeline_title')" :timeline="timeline" :timeline-name="'user'" :user-id="userId"/>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -21,4 +31,12 @@
|
|||
align-items: stretch;
|
||||
}
|
||||
}
|
||||
.user-profile-placeholder {
|
||||
.panel-body {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: middle;
|
||||
padding: 7em;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,20 +1,30 @@
|
|||
import TabSwitcher from '../tab_switcher/tab_switcher.jsx'
|
||||
import StyleSwitcher from '../style_switcher/style_switcher.vue'
|
||||
import fileSizeFormatService from '../../services/file_size_format/file_size_format.js'
|
||||
|
||||
const UserSettings = {
|
||||
data () {
|
||||
return {
|
||||
newname: this.$store.state.users.currentUser.name,
|
||||
newbio: this.$store.state.users.currentUser.description,
|
||||
newlocked: this.$store.state.users.currentUser.locked,
|
||||
newnorichtext: this.$store.state.users.currentUser.no_rich_text,
|
||||
newdefaultScope: this.$store.state.users.currentUser.default_scope,
|
||||
newName: this.$store.state.users.currentUser.name,
|
||||
newBio: this.$store.state.users.currentUser.description,
|
||||
newLocked: this.$store.state.users.currentUser.locked,
|
||||
newNoRichText: this.$store.state.users.currentUser.no_rich_text,
|
||||
newDefaultScope: this.$store.state.users.currentUser.default_scope,
|
||||
newHideNetwork: this.$store.state.users.currentUser.hide_network,
|
||||
followList: null,
|
||||
followImportError: false,
|
||||
followsImported: false,
|
||||
enableFollowsExport: true,
|
||||
uploading: [ false, false, false, false ],
|
||||
previews: [ null, null, null ],
|
||||
avatarUploading: false,
|
||||
bannerUploading: false,
|
||||
backgroundUploading: false,
|
||||
followListUploading: false,
|
||||
avatarPreview: null,
|
||||
bannerPreview: null,
|
||||
backgroundPreview: null,
|
||||
avatarUploadError: null,
|
||||
bannerUploadError: null,
|
||||
backgroundUploadError: null,
|
||||
deletingAccount: false,
|
||||
deleteAccountConfirmPasswordInput: '',
|
||||
deleteAccountError: false,
|
||||
|
@ -40,48 +50,67 @@ const UserSettings = {
|
|||
},
|
||||
vis () {
|
||||
return {
|
||||
public: { selected: this.newdefaultScope === 'public' },
|
||||
unlisted: { selected: this.newdefaultScope === 'unlisted' },
|
||||
private: { selected: this.newdefaultScope === 'private' },
|
||||
direct: { selected: this.newdefaultScope === 'direct' }
|
||||
public: { selected: this.newDefaultScope === 'public' },
|
||||
unlisted: { selected: this.newDefaultScope === 'unlisted' },
|
||||
private: { selected: this.newDefaultScope === 'private' },
|
||||
direct: { selected: this.newDefaultScope === 'direct' }
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateProfile () {
|
||||
const name = this.newname
|
||||
const description = this.newbio
|
||||
const locked = this.newlocked
|
||||
const name = this.newName
|
||||
const description = this.newBio
|
||||
const locked = this.newLocked
|
||||
// Backend notation.
|
||||
/* eslint-disable camelcase */
|
||||
const default_scope = this.newdefaultScope
|
||||
const no_rich_text = this.newnorichtext
|
||||
this.$store.state.api.backendInteractor.updateProfile({params: {name, description, locked, default_scope, no_rich_text}}).then((user) => {
|
||||
if (!user.error) {
|
||||
this.$store.commit('addNewUsers', [user])
|
||||
this.$store.commit('setCurrentUser', user)
|
||||
}
|
||||
})
|
||||
const default_scope = this.newDefaultScope
|
||||
const no_rich_text = this.newNoRichText
|
||||
const hide_network = this.newHideNetwork
|
||||
/* eslint-enable camelcase */
|
||||
this.$store.state.api.backendInteractor
|
||||
.updateProfile({
|
||||
params: {
|
||||
name,
|
||||
description,
|
||||
locked,
|
||||
// Backend notation.
|
||||
/* eslint-disable camelcase */
|
||||
default_scope,
|
||||
no_rich_text,
|
||||
hide_network
|
||||
/* eslint-enable camelcase */
|
||||
}}).then((user) => {
|
||||
if (!user.error) {
|
||||
this.$store.commit('addNewUsers', [user])
|
||||
this.$store.commit('setCurrentUser', user)
|
||||
}
|
||||
})
|
||||
},
|
||||
changeVis (visibility) {
|
||||
this.newdefaultScope = visibility
|
||||
this.newDefaultScope = visibility
|
||||
},
|
||||
uploadFile (slot, e) {
|
||||
const file = e.target.files[0]
|
||||
if (!file) { return }
|
||||
if (file.size > this.$store.state.instance[slot + 'limit']) {
|
||||
const filesize = fileSizeFormatService.fileSizeFormat(file.size)
|
||||
const allowedsize = fileSizeFormatService.fileSizeFormat(this.$store.state.instance[slot + 'limit'])
|
||||
this[slot + 'UploadError'] = this.$t('upload.error.base') + ' ' + this.$t('upload.error.file_too_big', {filesize: filesize.num, filesizeunit: filesize.unit, allowedsize: allowedsize.num, allowedsizeunit: allowedsize.unit})
|
||||
return
|
||||
}
|
||||
// eslint-disable-next-line no-undef
|
||||
const reader = new FileReader()
|
||||
reader.onload = ({target}) => {
|
||||
const img = target.result
|
||||
this.previews[slot] = img
|
||||
this.$forceUpdate() // just changing the array with the index doesn't update the view
|
||||
this[slot + 'Preview'] = img
|
||||
}
|
||||
reader.readAsDataURL(file)
|
||||
},
|
||||
submitAvatar () {
|
||||
if (!this.previews[0]) { return }
|
||||
if (!this.avatarPreview) { return }
|
||||
|
||||
let img = this.previews[0]
|
||||
let img = this.avatarPreview
|
||||
// eslint-disable-next-line no-undef
|
||||
let imginfo = new Image()
|
||||
let cropX, cropY, cropW, cropH
|
||||
|
@ -97,20 +126,25 @@ const UserSettings = {
|
|||
cropX = Math.floor((imginfo.width - imginfo.height) / 2)
|
||||
cropW = imginfo.height
|
||||
}
|
||||
this.uploading[0] = true
|
||||
this.avatarUploading = true
|
||||
this.$store.state.api.backendInteractor.updateAvatar({params: {img, cropX, cropY, cropW, cropH}}).then((user) => {
|
||||
if (!user.error) {
|
||||
this.$store.commit('addNewUsers', [user])
|
||||
this.$store.commit('setCurrentUser', user)
|
||||
this.previews[0] = null
|
||||
this.avatarPreview = null
|
||||
} else {
|
||||
this.avatarUploadError = this.$t('upload.error.base') + user.error
|
||||
}
|
||||
this.uploading[0] = false
|
||||
this.avatarUploading = false
|
||||
})
|
||||
},
|
||||
clearUploadError (slot) {
|
||||
this[slot + 'UploadError'] = null
|
||||
},
|
||||
submitBanner () {
|
||||
if (!this.previews[1]) { return }
|
||||
if (!this.bannerPreview) { return }
|
||||
|
||||
let banner = this.previews[1]
|
||||
let banner = this.bannerPreview
|
||||
// eslint-disable-next-line no-undef
|
||||
let imginfo = new Image()
|
||||
/* eslint-disable camelcase */
|
||||
|
@ -120,22 +154,24 @@ const UserSettings = {
|
|||
height = imginfo.height
|
||||
offset_top = 0
|
||||
offset_left = 0
|
||||
this.uploading[1] = true
|
||||
this.bannerUploading = true
|
||||
this.$store.state.api.backendInteractor.updateBanner({params: {banner, offset_top, offset_left, width, height}}).then((data) => {
|
||||
if (!data.error) {
|
||||
let clone = JSON.parse(JSON.stringify(this.$store.state.users.currentUser))
|
||||
clone.cover_photo = data.url
|
||||
this.$store.commit('addNewUsers', [clone])
|
||||
this.$store.commit('setCurrentUser', clone)
|
||||
this.previews[1] = null
|
||||
this.bannerPreview = null
|
||||
} else {
|
||||
this.bannerUploadError = this.$t('upload.error.base') + data.error
|
||||
}
|
||||
this.uploading[1] = false
|
||||
this.bannerUploading = false
|
||||
})
|
||||
/* eslint-enable camelcase */
|
||||
},
|
||||
submitBg () {
|
||||
if (!this.previews[2]) { return }
|
||||
let img = this.previews[2]
|
||||
if (!this.backgroundPreview) { return }
|
||||
let img = this.backgroundPreview
|
||||
// eslint-disable-next-line no-undef
|
||||
let imginfo = new Image()
|
||||
let cropX, cropY, cropW, cropH
|
||||
|
@ -144,20 +180,22 @@ const UserSettings = {
|
|||
cropY = 0
|
||||
cropW = imginfo.width
|
||||
cropH = imginfo.width
|
||||
this.uploading[2] = true
|
||||
this.backgroundUploading = true
|
||||
this.$store.state.api.backendInteractor.updateBg({params: {img, cropX, cropY, cropW, cropH}}).then((data) => {
|
||||
if (!data.error) {
|
||||
let clone = JSON.parse(JSON.stringify(this.$store.state.users.currentUser))
|
||||
clone.background_image = data.url
|
||||
this.$store.commit('addNewUsers', [clone])
|
||||
this.$store.commit('setCurrentUser', clone)
|
||||
this.previews[2] = null
|
||||
this.backgroundPreview = null
|
||||
} else {
|
||||
this.backgroundUploadError = this.$t('upload.error.base') + data.error
|
||||
}
|
||||
this.uploading[2] = false
|
||||
this.backgroundUploading = false
|
||||
})
|
||||
},
|
||||
importFollows () {
|
||||
this.uploading[3] = true
|
||||
this.followListUploading = true
|
||||
const followList = this.followList
|
||||
this.$store.state.api.backendInteractor.followImport({params: followList})
|
||||
.then((status) => {
|
||||
|
@ -166,7 +204,7 @@ const UserSettings = {
|
|||
} else {
|
||||
this.followImportError = true
|
||||
}
|
||||
this.uploading[3] = false
|
||||
this.followListUploading = false
|
||||
})
|
||||
},
|
||||
/* This function takes an Array of Users
|
||||
|
@ -198,6 +236,7 @@ const UserSettings = {
|
|||
.fetchFriends({id: this.$store.state.users.currentUser.id})
|
||||
.then((friendList) => {
|
||||
this.exportPeople(friendList, 'friends.csv')
|
||||
setTimeout(() => { this.enableFollowsExport = true }, 2000)
|
||||
})
|
||||
},
|
||||
followListChange () {
|
||||
|
|
|
@ -9,11 +9,11 @@
|
|||
<div class="setting-item" >
|
||||
<h2>{{$t('settings.name_bio')}}</h2>
|
||||
<p>{{$t('settings.name')}}</p>
|
||||
<input class='name-changer' id='username' v-model="newname"></input>
|
||||
<input class='name-changer' id='username' v-model="newName"></input>
|
||||
<p>{{$t('settings.bio')}}</p>
|
||||
<textarea class="bio" v-model="newbio"></textarea>
|
||||
<textarea class="bio" v-model="newBio"></textarea>
|
||||
<p>
|
||||
<input type="checkbox" v-model="newlocked" id="account-locked">
|
||||
<input type="checkbox" v-model="newLocked" id="account-locked">
|
||||
<label for="account-locked">{{$t('settings.lock_account_description')}}</label>
|
||||
</p>
|
||||
<div v-if="scopeOptionsEnabled">
|
||||
|
@ -26,47 +26,63 @@
|
|||
</div>
|
||||
</div>
|
||||
<p>
|
||||
<input type="checkbox" v-model="newnorichtext" id="account-no-rich-text">
|
||||
<input type="checkbox" v-model="newNoRichText" id="account-no-rich-text">
|
||||
<label for="account-no-rich-text">{{$t('settings.no_rich_text_description')}}</label>
|
||||
</p>
|
||||
<button :disabled='newname.length <= 0' class="btn btn-default" @click="updateProfile">{{$t('general.submit')}}</button>
|
||||
<p>
|
||||
<input type="checkbox" v-model="newHideNetwork" id="account-hide-network">
|
||||
<label for="account-hide-network">{{$t('settings.hide_network_description')}}</label>
|
||||
</p>
|
||||
<button :disabled='newName.length <= 0' class="btn btn-default" @click="updateProfile">{{$t('general.submit')}}</button>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<h2>{{$t('settings.avatar')}}</h2>
|
||||
<p>{{$t('settings.current_avatar')}}</p>
|
||||
<img :src="user.profile_image_url_original" class="old-avatar"></img>
|
||||
<p>{{$t('settings.set_new_avatar')}}</p>
|
||||
<img class="new-avatar" v-bind:src="previews[0]" v-if="previews[0]">
|
||||
<img class="new-avatar" v-bind:src="avatarPreview" v-if="avatarPreview">
|
||||
</img>
|
||||
<div>
|
||||
<input type="file" @change="uploadFile(0, $event)" ></input>
|
||||
<input type="file" @change="uploadFile('avatar', $event)" ></input>
|
||||
</div>
|
||||
<i class="icon-spin4 animate-spin" v-if="avatarUploading"></i>
|
||||
<button class="btn btn-default" v-else-if="avatarPreview" @click="submitAvatar">{{$t('general.submit')}}</button>
|
||||
<div class='alert error' v-if="avatarUploadError">
|
||||
Error: {{ avatarUploadError }}
|
||||
<i class="icon-cancel" @click="clearUploadError('avatar')"></i>
|
||||
</div>
|
||||
<i class="icon-spin4 animate-spin" v-if="uploading[0]"></i>
|
||||
<button class="btn btn-default" v-else-if="previews[0]" @click="submitAvatar">{{$t('general.submit')}}</button>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<h2>{{$t('settings.profile_banner')}}</h2>
|
||||
<p>{{$t('settings.current_profile_banner')}}</p>
|
||||
<img :src="user.cover_photo" class="banner"></img>
|
||||
<p>{{$t('settings.set_new_profile_banner')}}</p>
|
||||
<img class="banner" v-bind:src="previews[1]" v-if="previews[1]">
|
||||
<img class="banner" v-bind:src="bannerPreview" v-if="bannerPreview">
|
||||
</img>
|
||||
<div>
|
||||
<input type="file" @change="uploadFile(1, $event)" ></input>
|
||||
<input type="file" @change="uploadFile('banner', $event)" ></input>
|
||||
</div>
|
||||
<i class=" icon-spin4 animate-spin uploading" v-if="bannerUploading"></i>
|
||||
<button class="btn btn-default" v-else-if="bannerPreview" @click="submitBanner">{{$t('general.submit')}}</button>
|
||||
<div class='alert error' v-if="bannerUploadError">
|
||||
Error: {{ bannerUploadError }}
|
||||
<i class="icon-cancel" @click="clearUploadError('banner')"></i>
|
||||
</div>
|
||||
<i class=" icon-spin4 animate-spin uploading" v-if="uploading[1]"></i>
|
||||
<button class="btn btn-default" v-else-if="previews[1]" @click="submitBanner">{{$t('general.submit')}}</button>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<h2>{{$t('settings.profile_background')}}</h2>
|
||||
<p>{{$t('settings.set_new_profile_background')}}</p>
|
||||
<img class="bg" v-bind:src="previews[2]" v-if="previews[2]">
|
||||
<img class="bg" v-bind:src="backgroundPreview" v-if="backgroundPreview">
|
||||
</img>
|
||||
<div>
|
||||
<input type="file" @change="uploadFile(2, $event)" ></input>
|
||||
<input type="file" @change="uploadFile('background', $event)" ></input>
|
||||
</div>
|
||||
<i class=" icon-spin4 animate-spin uploading" v-if="backgroundUploading"></i>
|
||||
<button class="btn btn-default" v-else-if="backgroundPreview" @click="submitBg">{{$t('general.submit')}}</button>
|
||||
<div class='alert error' v-if="backgroundUploadError">
|
||||
Error: {{ backgroundUploadError }}
|
||||
<i class="icon-cancel" @click="clearUploadError('background')"></i>
|
||||
</div>
|
||||
<i class=" icon-spin4 animate-spin uploading" v-if="uploading[2]"></i>
|
||||
<button class="btn btn-default" v-else-if="previews[2]" @click="submitBg">{{$t('general.submit')}}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -113,7 +129,7 @@
|
|||
<form v-model="followImportForm">
|
||||
<input type="file" ref="followlist" v-on:change="followListChange"></input>
|
||||
</form>
|
||||
<i class=" icon-spin4 animate-spin uploading" v-if="uploading[3]"></i>
|
||||
<i class=" icon-spin4 animate-spin uploading" v-if="followListUploading"></i>
|
||||
<button class="btn btn-default" v-else @click="importFollows">{{$t('general.submit')}}</button>
|
||||
<div v-if="followsImported">
|
||||
<i class="icon-cross" @click="dismissImported"></i>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue