Merge remote-tracking branch 'upstream/develop' into search-mobile-fixes

* upstream/develop: (176 commits)
  fix chrome
  Prevent html-minifier to remove placeholder comment in index.html template
  Add placeholder to insert server generated metatags. Related to #430
  added condition to check for logined user
  fix gradients and minor artifacts
  keep track of new instance options
  fix old MR
  oof
  get rid of slots
  fix timeago font
  added hide_network option, fixed properties naming
  Fix fetching new users, add storing local users in usersObjects with their screen_name as well as id, so that they could be fetched zero-state with screen-name link.
  improve notification subscription
  Fix typo that prevented scope copy from working.
  Refactor arrays to individual options
  Reset enableFollowsExport to true after 2 sec when an export file is available to download
  added check for activatePanel is function or not
  addressed PR comments
  activate panel on user screen click
  added not preload check so hidden toggles asap
  ...
This commit is contained in:
Henry Jameson 2018-12-14 17:10:26 +03:00
commit bd745543b6
97 changed files with 5233 additions and 1934 deletions

View file

@ -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 {

View file

@ -9,12 +9,11 @@
<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>
<video :class="{'small': isSmall}" v-if="type === 'video' && !hidden" @loadeddata="onVideoDataLoad" :src="attachment.url" controls :loop="loopVideo"></video>
<video :class="{'small': isSmall}" v-if="type === 'video' && !hidden" @loadeddata="onVideoDataLoad" :src="attachment.url" controls :loop="loopVideo" playsinline></video>
<audio v-if="type === 'audio'" :src="attachment.url" controls></audio>
@ -161,6 +160,10 @@
display: flex;
flex: 1;
&.hidden {
display: none;
}
.still-image {
width: 100%;
height: 100%;

View file

@ -55,8 +55,8 @@
.chat-heading {
cursor: pointer;
.icon-comment-empty {
color: $fallback--fg;
color: var(--fg, $fallback--fg);
color: $fallback--text;
color: var(--text, $fallback--text);
}
}

View 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>

View 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>

View file

@ -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>

View 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>

View 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
}
}
}
}

View 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>

View file

@ -2,6 +2,9 @@ const InstanceSpecificPanel = {
computed: {
instanceSpecificPanelContent () {
return this.$store.state.instance.instanceSpecificPanelContent
},
show () {
return !this.$store.state.config.hideISP
}
}
}

View file

@ -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">

View file

@ -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">

View file

@ -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
})
},

View file

@ -6,11 +6,13 @@ import { highlightClass, highlightStyle } from '../../services/user_highlighter/
const Notification = {
data () {
return {
userExpanded: false
userExpanded: false,
betterShadow: this.$store.state.interface.browserSupport.cssFilter
}
},
props: [
'notification'
'notification',
'activatePanel'
],
components: {
Status, StillImage, UserCardContent

View file

@ -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,13 +25,13 @@
<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="{ name: 'user-profile', params: { id: notification.action.user.id } }">@{{notification.action.user.screen_name}}</router-link>
<router-link @click.native="activatePanel('timeline')" :to="{ name: 'user-profile', params: { id: notification.action.user.id } }">@{{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>

View file

@ -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

View file

@ -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 {

View file

@ -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">

View 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>

View file

@ -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
},

View file

@ -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);
}
}
}

View 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>

View file

@ -1,57 +1,61 @@
import oauthApi from '../../services/new_api/oauth.js'
import { validationMixin } from 'vuelidate'
import { required, sameAs } from 'vuelidate/lib/validators'
import { mapActions, mapState } from 'vuex'
const registration = {
mixins: [validationMixin],
data: () => ({
user: {},
error: false,
registering: false
}),
created () {
if ((!this.$store.state.instance.registrationOpen && !this.token) || !!this.$store.state.users.currentUser) {
this.$router.push('/main/all')
user: {
email: '',
fullname: '',
username: '',
password: '',
confirm: ''
}
// Seems like this doesn't work at first page open for some reason
if (this.$store.state.instance.registrationOpen && this.token) {
this.$router.push('/registration')
}),
validations: {
user: {
email: { required },
username: { required },
fullname: { required },
password: { required },
confirm: {
required,
sameAsPassword: sameAs('password')
}
}
},
created () {
if ((!this.registrationOpen && !this.token) || this.signedIn) {
this.$router.push('/main/all')
}
},
computed: {
termsofservice () { return this.$store.state.instance.tos },
token () { return this.$route.params.token }
token () { return this.$route.params.token },
...mapState({
registrationOpen: (state) => state.instance.registrationOpen,
signedIn: (state) => !!state.users.currentUser,
isPending: (state) => state.users.signUpPending,
serverValidationErrors: (state) => state.users.signUpErrors,
termsOfService: (state) => state.instance.tos
})
},
methods: {
submit () {
this.registering = true
...mapActions(['signUp']),
async submit () {
this.user.nickname = this.user.username
this.user.token = this.token
this.$store.state.api.backendInteractor.register(this.user).then(
(response) => {
if (response.ok) {
const data = {
oauth: this.$store.state.oauth,
instance: this.$store.state.instance.server
}
oauthApi.getOrCreateApp(data).then((app) => {
oauthApi.getTokenWithCredentials(
{
app,
instance: data.instance,
username: this.user.username,
password: this.user.password})
.then((result) => {
this.$store.commit('setToken', result.access_token)
this.$store.dispatch('loginUser', result.access_token)
this.$router.push('/main/friends')
})
})
} else {
this.registering = false
response.json().then((data) => {
this.error = data.error
})
}
this.$v.$touch()
if (!this.$v.$invalid) {
try {
await this.signUp(this.user)
this.$router.push('/main/friends')
} catch (error) {
console.warn('Registration failed: ' + error)
}
)
}
}
}
}

View file

@ -7,50 +7,90 @@
<form v-on:submit.prevent='submit(user)' class='registration-form'>
<div class='container'>
<div class='text-fields'>
<div class='form-group'>
<label for='username'>{{$t('login.username')}}</label>
<input :disabled="registering" v-model='user.username' class='form-control' id='username' placeholder='e.g. lain'>
<div class='form-group' :class="{ 'form-group--error': $v.user.username.$error }">
<label class='form--label' for='sign-up-username'>{{$t('login.username')}}</label>
<input :disabled="isPending" v-model.trim='$v.user.username.$model' class='form-control' id='sign-up-username' placeholder='e.g. lain'>
</div>
<div class='form-group'>
<label for='fullname'>{{$t('registration.fullname')}}</label>
<input :disabled="registering" v-model='user.fullname' class='form-control' id='fullname' placeholder='e.g. Lain Iwakura'>
<div class="form-error" v-if="$v.user.username.$dirty">
<ul>
<li v-if="!$v.user.username.required">
<span>{{$t('registration.validations.username_required')}}</span>
</li>
</ul>
</div>
<div class='form-group'>
<label for='email'>{{$t('registration.email')}}</label>
<input :disabled="registering" v-model='user.email' class='form-control' id='email' type="email">
<div class='form-group' :class="{ 'form-group--error': $v.user.fullname.$error }">
<label class='form--label' for='sign-up-fullname'>{{$t('registration.fullname')}}</label>
<input :disabled="isPending" v-model.trim='$v.user.fullname.$model' class='form-control' id='sign-up-fullname' placeholder='e.g. Lain Iwakura'>
</div>
<div class='form-group'>
<label for='bio'>{{$t('registration.bio')}}</label>
<input :disabled="registering" v-model='user.bio' class='form-control' id='bio'>
<div class="form-error" v-if="$v.user.fullname.$dirty">
<ul>
<li v-if="!$v.user.fullname.required">
<span>{{$t('registration.validations.fullname_required')}}</span>
</li>
</ul>
</div>
<div class='form-group'>
<label for='password'>{{$t('login.password')}}</label>
<input :disabled="registering" v-model='user.password' class='form-control' id='password' type='password'>
<div class='form-group' :class="{ 'form-group--error': $v.user.email.$error }">
<label class='form--label' for='email'>{{$t('registration.email')}}</label>
<input :disabled="isPending" v-model='$v.user.email.$model' class='form-control' id='email' type="email">
</div>
<div class='form-group'>
<label for='password_confirmation'>{{$t('registration.password_confirm')}}</label>
<input :disabled="registering" v-model='user.confirm' class='form-control' id='password_confirmation' type='password'>
<div class="form-error" v-if="$v.user.email.$dirty">
<ul>
<li v-if="!$v.user.email.required">
<span>{{$t('registration.validations.email_required')}}</span>
</li>
</ul>
</div>
<!--
<div class='form-group'>
<label for='captcha'>Captcha</label>
<img src='/qvittersimplesecurity/captcha.jpg' alt='captcha' class='captcha'>
<input :disabled="registering" v-model='user.captcha' placeholder='Enter captcha' type='test' class='form-control' id='captcha'>
<label class='form--label' for='bio'>{{$t('registration.bio')}}</label>
<input :disabled="isPending" v-model='user.bio' class='form-control' id='bio'>
</div>
-->
<div class='form-group' :class="{ 'form-group--error': $v.user.password.$error }">
<label class='form--label' for='sign-up-password'>{{$t('login.password')}}</label>
<input :disabled="isPending" v-model='user.password' class='form-control' id='sign-up-password' type='password'>
</div>
<div class="form-error" v-if="$v.user.password.$dirty">
<ul>
<li v-if="!$v.user.password.required">
<span>{{$t('registration.validations.password_required')}}</span>
</li>
</ul>
</div>
<div class='form-group' :class="{ 'form-group--error': $v.user.confirm.$error }">
<label class='form--label' for='sign-up-password-confirmation'>{{$t('registration.password_confirm')}}</label>
<input :disabled="isPending" v-model='user.confirm' class='form-control' id='sign-up-password-confirmation' type='password'>
</div>
<div class="form-error" v-if="$v.user.confirm.$dirty">
<ul>
<li v-if="!$v.user.confirm.required">
<span>{{$t('registration.validations.password_confirmation_required')}}</span>
</li>
<li v-if="!$v.user.confirm.sameAsPassword">
<span>{{$t('registration.validations.password_confirmation_match')}}</span>
</li>
</ul>
</div>
<div class='form-group' v-if='token' >
<label for='token'>{{$t('registration.token')}}</label>
<input disabled='true' v-model='token' class='form-control' id='token' type='text'>
</div>
<div class='form-group'>
<button :disabled="registering" type='submit' class='btn btn-default'>{{$t('general.submit')}}</button>
<button :disabled="isPending" type='submit' class='btn btn-default'>{{$t('general.submit')}}</button>
</div>
</div>
<div class='terms-of-service' v-html="termsofservice">
<div class='terms-of-service' v-html="termsOfService">
</div>
</div>
<div v-if="error" class='form-group'>
<div class='alert error'>{{error}}</div>
<div v-if="serverValidationErrors.length" class='form-group'>
<div class='alert error'>
<span v-for="error in serverValidationErrors">{{error}}</span>
</div>
</div>
</form>
</div>
@ -60,6 +100,7 @@
<script src="./registration.js"></script>
<style lang="scss">
@import '../../_variables.scss';
$validations-cRed: #f04124;
.registration-form {
display: flex;
@ -89,6 +130,55 @@
flex-direction: column;
padding: 0.3em 0.0em 0.3em;
line-height:24px;
margin-bottom: 1em;
}
@keyframes shakeError {
0% {
transform: translateX(0); }
15% {
transform: translateX(0.375rem); }
30% {
transform: translateX(-0.375rem); }
45% {
transform: translateX(0.375rem); }
60% {
transform: translateX(-0.375rem); }
75% {
transform: translateX(0.375rem); }
90% {
transform: translateX(-0.375rem); }
100% {
transform: translateX(0); } }
.form-group--error {
animation-name: shakeError;
animation-duration: .6s;
animation-timing-function: ease-in-out;
}
.form-group--error .form--label {
color: $validations-cRed;
color: var(--cRed, $validations-cRed);
}
.form-error {
margin-top: -0.7em;
text-align: left;
span {
font-size: 12px;
}
}
.form-error ul {
list-style: none;
padding: 0 0 0 5px;
margin-top: 0;
li::before {
content: "• ";
}
}
form textarea {
@ -102,8 +192,6 @@
}
.btn {
//align-self: flex-start;
//width: 10em;
margin-top: 0.6em;
height: 28px;
}

View file

@ -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')
}
}
}

View file

@ -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="!hideAttachmentsInConvLocal" 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 {

View 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)
} : {}
}
}
}

View 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>

View file

@ -20,7 +20,8 @@ const Status = {
'replies',
'noReplyLinks',
'noHeading',
'inlineExpanded'
'inlineExpanded',
'activatePanel'
],
data () {
return {
@ -33,7 +34,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: {
@ -53,6 +55,9 @@ const Status = {
const user = this.retweet ? (this.statusoid.retweeted_status.user) : this.statusoid.user
return highlightClass(user)
},
deleted () {
return this.statusoid.deleted
},
repeaterStyle () {
const user = this.statusoid.user
const highlight = this.$store.state.config.highlight

View file

@ -1,15 +1,15 @@
<template>
<div class="status-el" v-if="!hideReply" :class="[{ 'status-el_focused': isFocused }, { 'status-conversation': inlineExpanded }]">
<div class="status-el" v-if="!hideReply && !deleted" :class="[{ 'status-el_focused': isFocused }, { 'status-conversation': inlineExpanded }]">
<template v-if="muted && !noReplyLinks">
<div class="media status container muted">
<small><router-link :to="{ name: 'user-profile', params: { id: status.user.id } }">{{status.user.screen_name}}</router-link></small>
<small><router-link @click.native="activatePanel('timeline')" :to="{ name: 'user-profile', params: { id: status.user.id } }">{{status.user.screen_name}}</router-link></small>
<small class="muteWords">{{muteWordHits.join(', ')}}</small>
<a href="#" class="unmute" @click.prevent="toggleMute"><i class="icon-eye-off"></i></a>
</div>
</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>
@ -21,7 +21,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">
@ -34,10 +34,10 @@
<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="{ name: 'user-profile', params: { id: status.user.id } }">{{status.user.screen_name}}</router-link>
<router-link @click.native="activatePanel('timeline')" :to="{ name: 'user-profile', params: { id: status.user.id } }">{{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="{ name: 'user-profile', params: { id: status.in_reply_to_user_id } }">
<router-link @click.native="activatePanel('timeline')" :to="{ name: 'user-profile', params: { id: status.in_reply_to_user_id } }">
{{status.in_reply_to_screen_name}}
</router-link>
</span>
@ -54,7 +54,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">
@ -73,7 +73,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>
@ -146,6 +146,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;
@ -284,8 +285,8 @@
margin-left: 0.2em;
}
a:hover i {
color: $fallback--fg;
color: var(--fg, $fallback--fg);
color: $fallback--text;
color: var(--text, $fallback--text);
}
}
@ -323,6 +324,8 @@
.status-content {
margin-right: 0.5em;
font-family: var(--postFont, sans-serif);
img, video {
max-width: 100%;
max-height: 400px;
@ -339,6 +342,10 @@
overflow: auto;
}
code, samp, kbd, var, pre {
font-family: var(--postCodeFont, monospace);
}
p {
margin: 0;
margin-top: 0.2em;
@ -457,18 +464,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 {
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%;
@ -532,6 +551,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;
}
}

View 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>

View file

@ -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)
}
}
}
}

View 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;
}
}

View file

@ -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>

View file

@ -25,11 +25,14 @@ export default Vue.component('tab-switcher', {
}
return (<button onClick={this.activateTab(index)} class={ classes.join(' ') }>{slot.data.attrs.label}</button>)
});
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">

View file

@ -1,13 +1,21 @@
@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;
height: 32px;
box-sizing: border-box;
&::after, &::before {
display: block;
@ -17,20 +25,34 @@
.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 {
position: relative;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
padding: .3em 1em;
padding: 5px 1em 99px;
white-space: nowrap;
&:not(.active) {
border-bottom: 1px solid;
border-bottom-color: $fallback--btn;
border-bottom-color: var(--btn, $fallback--btn);
z-index: 4;
&:hover {
z-index: 6;
}
&::after {
content: '';
position: absolute;
left: 0;
right: 0;
top: 26px;
border-bottom: 1px solid;
border-bottom-color: $fallback--border;
border-bottom-color: var(--border, $fallback--border);
}
}
&.active {

View file

@ -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 })

View file

@ -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>

View file

@ -14,6 +14,9 @@ const UserCard = {
components: {
UserCardContent
},
computed: {
currentUser () { return this.$store.state.users.currentUser }
},
methods: {
toggleUserExpanded () {
this.userExpanded = !this.userExpanded

View file

@ -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>
<router-link class='user-screen-name' :to="{ name: 'user-profile', params: { id: user.id } }">

View file

@ -2,24 +2,38 @@ import StillImage from '../still-image/still-image.vue'
import { hex2rgb } from '../../services/color_convert/color_convert.js'
export default {
props: [ 'user', 'switcher', 'selected', 'hideBio' ],
props: [ 'user', 'switcher', 'selected', 'hideBio', 'activatePanel' ],
data () {
return {
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(', ')
}
@ -98,6 +112,14 @@ export default {
const store = this.$store
store.commit('setProfileView', { v })
}
},
linkClicked ({target}) {
if (target.tagName === 'SPAN') {
target = target.parentNode
}
if (target.tagName === 'A') {
window.open(target.href, '_blank')
}
}
}
}

View file

@ -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 to='/user-settings' style="float: right; margin-top:16px;" v-if="!isOtherUser">
<i class="icon-cog usersettings"></i>
<router-link @click.native="activatePanel && activatePanel('timeline')" to='/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="{ name: 'user-profile', params: { id: user.id } }">
<StillImage class="avatar" :src="user.profile_image_url_original"/>
<router-link @click.native="activatePanel && activatePanel('timeline')" :to="{ name: 'user-profile', params: { id: user.id } }">
<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="{ name: 'user-profile', params: { id: user.id } }">
<router-link @click.native="activatePanel && activatePanel('timeline')" class='user-screen-name':to="{ name: 'user-profile', params: { id: user.id } }">
<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,74 @@
</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">
{{ $t('user_card.following') }}
</button>
</span>
<span v-if="!user.following">
<button @click="followUser">
{{ $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>
<div class="follow" v-if="loggedIn">
<span v-if="user.following">
<!--Following them!-->
<button @click="unfollowUser" class="pressed">
{{ $t('user_card.following') }}
</button>
</span>
<span v-if="!user.following">
<button @click="followUser">
{{ $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>
</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 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 +120,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 +143,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 +159,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 +183,8 @@
}
.usersettings {
color: $fallback--lightFg;
color: var(--lightFg, $fallback--lightFg);
color: $fallback--lightText;
color: var(--lightText, $fallback--lightText);
opacity: .8;
}
@ -185,6 +195,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 +213,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 +289,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 {

View file

@ -1,5 +1,5 @@
<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>
<template v-else>
@ -9,7 +9,7 @@
</button>
<i class="icon-cancel user-finder-icon" @click.prevent.stop="toggleHidden"/>
</template>
</span>
</div>
</template>
<script src="./user_finder.js"></script>

View file

@ -3,6 +3,7 @@ import PostStatusForm from '../post_status_form/post_status_form.vue'
import UserCardContent from '../user_card_content/user_card_content.vue'
const UserPanel = {
props: [ 'activatePanel' ],
computed: {
user () { return this.$store.state.users.currentUser }
},

View file

@ -1,7 +1,7 @@
<template>
<div class="user-panel">
<div v-if='user' class="panel panel-default" style="overflow: visible;">
<user-card-content :user="user" :switcher="false" :hideBio="true"></user-card-content>
<user-card-content :activatePanel="activatePanel" :user="user" :switcher="false" :hideBio="true"></user-card-content>
<div class="panel-footer">
<post-status-form v-if='user'></post-status-form>
</div>

View file

@ -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>

View file

@ -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 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 () {

View file

@ -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-no-rich-text">{{$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>