aboutsummaryrefslogtreecommitdiff
path: root/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'src/components')
-rw-r--r--src/components/attachment/attachment.js5
-rw-r--r--src/components/attachment/attachment.vue9
-rw-r--r--src/components/chat_panel/chat_panel.vue4
-rw-r--r--src/components/color_input/color_input.vue53
-rw-r--r--src/components/contrast_ratio/contrast_ratio.vue69
-rw-r--r--src/components/delete_button/delete_button.vue2
-rw-r--r--src/components/export_import/export_import.vue87
-rw-r--r--src/components/font_control/font_control.js58
-rw-r--r--src/components/font_control/font_control.vue54
-rw-r--r--src/components/instance_specific_panel/instance_specific_panel.js3
-rw-r--r--src/components/instance_specific_panel/instance_specific_panel.vue2
-rw-r--r--src/components/interface_language_switcher/interface_language_switcher.vue3
-rw-r--r--src/components/media_upload/media_upload.js9
-rw-r--r--src/components/notification/notification.js6
-rw-r--r--src/components/notification/notification.vue10
-rw-r--r--src/components/notifications/notifications.js1
-rw-r--r--src/components/notifications/notifications.scss58
-rw-r--r--src/components/notifications/notifications.vue5
-rw-r--r--src/components/opacity_input/opacity_input.vue38
-rw-r--r--src/components/post_status_form/post_status_form.js7
-rw-r--r--src/components/post_status_form/post_status_form.vue16
-rw-r--r--src/components/range_input/range_input.vue48
-rw-r--r--src/components/registration/registration.js84
-rw-r--r--src/components/registration/registration.vue146
-rw-r--r--src/components/settings/settings.js13
-rw-r--r--src/components/settings/settings.vue44
-rw-r--r--src/components/shadow_control/shadow_control.js87
-rw-r--r--src/components/shadow_control/shadow_control.vue243
-rw-r--r--src/components/status/status.js9
-rw-r--r--src/components/status/status.vue40
-rw-r--r--src/components/style_switcher/preview.vue78
-rw-r--r--src/components/style_switcher/style_switcher.js672
-rw-r--r--src/components/style_switcher/style_switcher.scss335
-rw-r--r--src/components/style_switcher/style_switcher.vue538
-rw-r--r--src/components/tab_switcher/tab_switcher.jsx13
-rw-r--r--src/components/tab_switcher/tab_switcher.scss36
-rw-r--r--src/components/timeline/timeline.js5
-rw-r--r--src/components/timeline/timeline.vue16
-rw-r--r--src/components/user_card/user_card.js3
-rw-r--r--src/components/user_card/user_card.vue4
-rw-r--r--src/components/user_card_content/user_card_content.js32
-rw-r--r--src/components/user_card_content/user_card_content.vue170
-rw-r--r--src/components/user_finder/user_finder.vue4
-rw-r--r--src/components/user_panel/user_panel.js1
-rw-r--r--src/components/user_panel/user_panel.vue2
-rw-r--r--src/components/user_profile/user_profile.vue18
-rw-r--r--src/components/user_settings/user_settings.js121
-rw-r--r--src/components/user_settings/user_settings.vue52
48 files changed, 2585 insertions, 728 deletions
diff --git a/src/components/attachment/attachment.js b/src/components/attachment/attachment.js
index 41730720..97c4f283 100644
--- a/src/components/attachment/attachment.js
+++ b/src/components/attachment/attachment.js
@@ -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 {
diff --git a/src/components/attachment/attachment.vue b/src/components/attachment/attachment.vue
index 8795b131..5eaa0d1d 100644
--- a/src/components/attachment/attachment.vue
+++ b/src/components/attachment/attachment.vue
@@ -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%;
diff --git a/src/components/chat_panel/chat_panel.vue b/src/components/chat_panel/chat_panel.vue
index 30070d3e..f174319a 100644
--- a/src/components/chat_panel/chat_panel.vue
+++ b/src/components/chat_panel/chat_panel.vue
@@ -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);
}
}
diff --git a/src/components/color_input/color_input.vue b/src/components/color_input/color_input.vue
new file mode 100644
index 00000000..34eec248
--- /dev/null
+++ b/src/components/color_input/color_input.vue
@@ -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>
diff --git a/src/components/contrast_ratio/contrast_ratio.vue b/src/components/contrast_ratio/contrast_ratio.vue
new file mode 100644
index 00000000..bd971d00
--- /dev/null
+++ b/src/components/contrast_ratio/contrast_ratio.vue
@@ -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>
diff --git a/src/components/delete_button/delete_button.vue b/src/components/delete_button/delete_button.vue
index d13547e2..b458b0dc 100644
--- a/src/components/delete_button/delete_button.vue
+++ b/src/components/delete_button/delete_button.vue
@@ -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>
diff --git a/src/components/export_import/export_import.vue b/src/components/export_import/export_import.vue
new file mode 100644
index 00000000..451a2668
--- /dev/null
+++ b/src/components/export_import/export_import.vue
@@ -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>
diff --git a/src/components/font_control/font_control.js b/src/components/font_control/font_control.js
new file mode 100644
index 00000000..8e2b0e45
--- /dev/null
+++ b/src/components/font_control/font_control.js
@@ -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
+ }
+ }
+ }
+}
diff --git a/src/components/font_control/font_control.vue b/src/components/font_control/font_control.vue
new file mode 100644
index 00000000..ed36b280
--- /dev/null
+++ b/src/components/font_control/font_control.vue
@@ -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>
diff --git a/src/components/instance_specific_panel/instance_specific_panel.js b/src/components/instance_specific_panel/instance_specific_panel.js
index 09e3d055..9bb5e945 100644
--- a/src/components/instance_specific_panel/instance_specific_panel.js
+++ b/src/components/instance_specific_panel/instance_specific_panel.js
@@ -2,6 +2,9 @@ const InstanceSpecificPanel = {
computed: {
instanceSpecificPanelContent () {
return this.$store.state.instance.instanceSpecificPanelContent
+ },
+ show () {
+ return !this.$store.state.config.hideISP
}
}
}
diff --git a/src/components/instance_specific_panel/instance_specific_panel.vue b/src/components/instance_specific_panel/instance_specific_panel.vue
index ca8e00c0..a7b74667 100644
--- a/src/components/instance_specific_panel/instance_specific_panel.vue
+++ b/src/components/instance_specific_panel/instance_specific_panel.vue
@@ -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">
diff --git a/src/components/interface_language_switcher/interface_language_switcher.vue b/src/components/interface_language_switcher/interface_language_switcher.vue
index 4b541888..3f58af2c 100644
--- a/src/components/interface_language_switcher/interface_language_switcher.vue
+++ b/src/components/interface_language_switcher/interface_language_switcher.vue
@@ -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">
diff --git a/src/components/media_upload/media_upload.js b/src/components/media_upload/media_upload.js
index 66337c3f..42d900d3 100644
--- a/src/components/media_upload/media_upload.js
+++ b/src/components/media_upload/media_upload.js
@@ -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
})
},
diff --git a/src/components/notification/notification.js b/src/components/notification/notification.js
index c786f2cc..345fe3ee 100644
--- a/src/components/notification/notification.js
+++ b/src/components/notification/notification.js
@@ -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
diff --git a/src/components/notification/notification.vue b/src/components/notification/notification.vue
index 13a5c0aa..e84ce0b6 100644
--- a/src/components/notification/notification.vue
+++ b/src/components/notification/notification.vue
@@ -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>
diff --git a/src/components/notifications/notifications.js b/src/components/notifications/notifications.js
index 945ffd1f..4b7a591d 100644
--- a/src/components/notifications/notifications.js
+++ b/src/components/notifications/notifications.js
@@ -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
diff --git a/src/components/notifications/notifications.scss b/src/components/notifications/notifications.scss
index a137ccd5..a6468e01 100644
--- a/src/components/notifications/notifications.scss
+++ b/src/components/notifications/notifications.scss
@@ -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 {
diff --git a/src/components/notifications/notifications.vue b/src/components/notifications/notifications.vue
index 7a4322f9..bef48567 100644
--- a/src/components/notifications/notifications.vue
+++ b/src/components/notifications/notifications.vue
@@ -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">
diff --git a/src/components/opacity_input/opacity_input.vue b/src/components/opacity_input/opacity_input.vue
new file mode 100644
index 00000000..3926915b
--- /dev/null
+++ b/src/components/opacity_input/opacity_input.vue
@@ -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>
diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js
index 789243cf..0ce2aff0 100644
--- a/src/components/post_status_form/post_status_form.js
+++ b/src/components/post_status_form/post_status_form.js
@@ -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
},
diff --git a/src/components/post_status_form/post_status_form.vue b/src/components/post_status_form/post_status_form.vue
index e4c46b9a..4776c819 100644
--- a/src/components/post_status_form/post_status_form.vue
+++ b/src/components/post_status_form/post_status_form.vue
@@ -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);
}
}
}
diff --git a/src/components/range_input/range_input.vue b/src/components/range_input/range_input.vue
new file mode 100644
index 00000000..3e50664b
--- /dev/null
+++ b/src/components/range_input/range_input.vue
@@ -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>
diff --git a/src/components/registration/registration.js b/src/components/registration/registration.js
index f7f8a720..e5ead8bc 100644
--- a/src/components/registration/registration.js
+++ b/src/components/registration/registration.js
@@ -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
+ user: {
+ email: '',
+ fullname: '',
+ username: '',
+ password: '',
+ confirm: ''
+ }
}),
+ validations: {
+ user: {
+ email: { required },
+ username: { required },
+ fullname: { required },
+ password: { required },
+ confirm: {
+ required,
+ sameAsPassword: sameAs('password')
+ }
+ }
+ },
created () {
- if ((!this.$store.state.instance.registrationOpen && !this.token) || !!this.$store.state.users.currentUser) {
+ if ((!this.registrationOpen && !this.token) || this.signedIn) {
this.$router.push('/main/all')
}
- // Seems like this doesn't work at first page open for some reason
- if (this.$store.state.instance.registrationOpen && this.token) {
- this.$router.push('/registration')
- }
},
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)
}
- )
+ }
}
}
}
diff --git a/src/components/registration/registration.vue b/src/components/registration/registration.vue
index 087cab6b..8cb1392b 100644
--- a/src/components/registration/registration.vue
+++ b/src/components/registration/registration.vue
@@ -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;
}
diff --git a/src/components/settings/settings.js b/src/components/settings/settings.js
index 91a2014a..681ccda8 100644
--- a/src/components/settings/settings.js
+++ b/src/components/settings/settings.js
@@ -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')
}
}
}
diff --git a/src/components/settings/settings.vue b/src/components/settings/settings.vue
index de506e4d..3f920de5 100644
--- a/src/components/settings/settings.vue
+++ b/src/components/settings/settings.vue
@@ -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 {
diff --git a/src/components/shadow_control/shadow_control.js b/src/components/shadow_control/shadow_control.js
new file mode 100644
index 00000000..44e4a22f
--- /dev/null
+++ b/src/components/shadow_control/shadow_control.js
@@ -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)
+ } : {}
+ }
+ }
+}
diff --git a/src/components/shadow_control/shadow_control.vue b/src/components/shadow_control/shadow_control.vue
new file mode 100644
index 00000000..744925d4
--- /dev/null
+++ b/src/components/shadow_control/shadow_control.vue
@@ -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>
diff --git a/src/components/status/status.js b/src/components/status/status.js
index 10716583..9a63d047 100644
--- a/src/components/status/status.js
+++ b/src/components/status/status.js
@@ -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
diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index 9f65f281..067980ac 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -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;
}
}
diff --git a/src/components/style_switcher/preview.vue b/src/components/style_switcher/preview.vue
new file mode 100644
index 00000000..09a136e9
--- /dev/null
+++ b/src/components/style_switcher/preview.vue
@@ -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>
diff --git a/src/components/style_switcher/style_switcher.js b/src/components/style_switcher/style_switcher.js
index 95c15b49..6a4e1cba 100644
--- a/src/components/style_switcher/style_switcher.js
+++ b/src/components/style_switcher/style_switcher.js
@@ -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({
+ computed: {
+ selectedVersion () {
+ return Array.isArray(this.selected) ? 1 : 2
+ },
+ currentColors () {
+ return {
+ bg: this.bgColorLocal,
+ text: this.textColorLocal,
+ link: this.linkColorLocal,
+
+ fg: this.fgColorLocal,
+ fgText: this.fgTextColorLocal,
+ fgLink: this.fgLinkColorLocal,
+
+ 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
+ })
+
+ // 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: 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)
- },
-
- importTheme () {
- this.invalidThemeImported = false
- const filePicker = document.createElement('input')
- filePicker.setAttribute('type', 'file')
- filePicker.setAttribute('accept', '.json')
-
- filePicker.addEventListener('change', event => {
- if (event.target.files[0]) {
- // eslint-disable-next-line no-undef
- const reader = new FileReader()
- reader.onload = ({target}) => {
- try {
- const parsed = JSON.parse(target.result)
- if (parsed._pleroma_theme_version === 1) {
- this.normalizeLocalState(parsed.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])
+ _pleroma_theme_version: 2, theme
+ }
+ }
+ },
+ components: {
+ ColorInput,
+ OpacityInput,
+ RangeInput,
+ ContrastRatio,
+ ShadowControl,
+ FontControl,
+ TabSwitcher,
+ Preview,
+ ExportImport
+ },
+ methods: {
+ setCustomTheme () {
+ 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)
+ },
- document.body.appendChild(filePicker)
- filePicker.click()
- document.body.removeChild(filePicker)
+ // 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)
+ })
},
- 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
- }})
- }
- },
-
- normalizeLocalState (colors, radii) {
- this.bgColorLocal = rgbstr2hex(colors.bg)
- this.btnColorLocal = rgbstr2hex(colors.btn)
- this.textColorLocal = rgbstr2hex(colors.fg)
- this.linkColorLocal = rgbstr2hex(colors.link)
-
- this.redColorLocal = rgbstr2hex(colors.cRed)
- this.blueColorLocal = rgbstr2hex(colors.cBlue)
- this.greenColorLocal = rgbstr2hex(colors.cGreen)
- this.orangeColorLocal = rgbstr2hex(colors.cOrange)
-
- 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
+ clearRoundness () {
+ Object.keys(this.$data)
+ .filter(_ => _.endsWith('RadiusLocal'))
+ .forEach(key => {
+ set(this.$data, key, undefined)
+ })
+ },
+
+ 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)
+ }
}
}
}
diff --git a/src/components/style_switcher/style_switcher.scss b/src/components/style_switcher/style_switcher.scss
new file mode 100644
index 00000000..135c113a
--- /dev/null
+++ b/src/components/style_switcher/style_switcher.scss
@@ -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;
+ }
+}
diff --git a/src/components/style_switcher/style_switcher.vue b/src/components/style_switcher/style_switcher.vue
index 72a338bd..84963c81 100644
--- a/src/components/style_switcher/style_switcher.vue
+++ b/src/components/style_switcher/style_switcher.vue
@@ -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>
- </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.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 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.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 :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>
diff --git a/src/components/tab_switcher/tab_switcher.jsx b/src/components/tab_switcher/tab_switcher.jsx
index 3fff38f6..9e3dee04 100644
--- a/src/components/tab_switcher/tab_switcher.jsx
+++ b/src/components/tab_switcher/tab_switcher.jsx
@@ -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">
diff --git a/src/components/tab_switcher/tab_switcher.scss b/src/components/tab_switcher/tab_switcher.scss
index 374a19c5..fbd3321b 100644
--- a/src/components/tab_switcher/tab_switcher.scss
+++ b/src/components/tab_switcher/tab_switcher.scss
@@ -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 {
diff --git a/src/components/timeline/timeline.js b/src/components/timeline/timeline.js
index a651f619..f28b85bd 100644
--- a/src/components/timeline/timeline.js
+++ b/src/components/timeline/timeline.js
@@ -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 })
diff --git a/src/components/timeline/timeline.vue b/src/components/timeline/timeline.vue
index 2dd4376a..bc7f74c2 100644
--- a/src/components/timeline/timeline.vue
+++ b/src/components/timeline/timeline.vue
@@ -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>
diff --git a/src/components/user_card/user_card.js b/src/components/user_card/user_card.js
index a019627a..b8eb28e3 100644
--- a/src/components/user_card/user_card.js
+++ b/src/components/user_card/user_card.js
@@ -14,6 +14,9 @@ const UserCard = {
components: {
UserCardContent
},
+ computed: {
+ currentUser () { return this.$store.state.users.currentUser }
+ },
methods: {
toggleUserExpanded () {
this.userExpanded = !this.userExpanded
diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue
index 5a8e5531..eb0d7576 100644
--- a/src/components/user_card/user_card.vue
+++ b/src/components/user_card/user_card.vue
@@ -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 } }">
diff --git a/src/components/user_card_content/user_card_content.js b/src/components/user_card_content/user_card_content.js
index b5dd9b91..be7a2349 100644
--- a/src/components/user_card_content/user_card_content.js
+++ b/src/components/user_card_content/user_card_content.js
@@ -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')
+ }
}
}
}
diff --git a/src/components/user_card_content/user_card_content.vue b/src/components/user_card_content/user_card_content.vue
index 84669d7f..08b25595 100644
--- a/src/components/user_card_content/user_card_content.vue
+++ b/src/components/user_card_content/user_card_content.vue
@@ -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>
- </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 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="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 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="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 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>
- <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 {
diff --git a/src/components/user_finder/user_finder.vue b/src/components/user_finder/user_finder.vue
index e0a7962a..9efdfab7 100644
--- a/src/components/user_finder/user_finder.vue
+++ b/src/components/user_finder/user_finder.vue
@@ -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>
diff --git a/src/components/user_panel/user_panel.js b/src/components/user_panel/user_panel.js
index 15804b88..eb7cb09c 100644
--- a/src/components/user_panel/user_panel.js
+++ b/src/components/user_panel/user_panel.js
@@ -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 }
},
diff --git a/src/components/user_panel/user_panel.vue b/src/components/user_panel/user_panel.vue
index 2d5cb500..83eb099f 100644
--- a/src/components/user_panel/user_panel.vue
+++ b/src/components/user_panel/user_panel.vue
@@ -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>
diff --git a/src/components/user_profile/user_profile.vue b/src/components/user_profile/user_profile.vue
index 91d4acd2..4d2853a6 100644
--- a/src/components/user_profile/user_profile.vue
+++ b/src/components/user_profile/user_profile.vue
@@ -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>
diff --git a/src/components/user_settings/user_settings.js b/src/components/user_settings/user_settings.js
index 761e674a..ca7bf319 100644
--- a/src/components/user_settings/user_settings.js
+++ b/src/components/user_settings/user_settings.js
@@ -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 () {
diff --git a/src/components/user_settings/user_settings.vue b/src/components/user_settings/user_settings.vue
index 234a7d86..67b65b57 100644
--- a/src/components/user_settings/user_settings.vue
+++ b/src/components/user_settings/user_settings.vue
@@ -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>