aboutsummaryrefslogtreecommitdiff
path: root/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'src/components')
-rw-r--r--src/components/account_actions/account_actions.js4
-rw-r--r--src/components/account_actions/account_actions.vue21
-rw-r--r--src/components/attachment/attachment.vue2
-rw-r--r--src/components/autosuggest/autosuggest.vue4
-rw-r--r--src/components/checkbox/checkbox.vue4
-rw-r--r--src/components/color_input/color_input.scss68
-rw-r--r--src/components/color_input/color_input.vue118
-rw-r--r--src/components/contrast_ratio/contrast_ratio.vue14
-rw-r--r--src/components/dialog_modal/dialog_modal.vue12
-rw-r--r--src/components/emoji_input/emoji_input.js10
-rw-r--r--src/components/emoji_input/emoji_input.vue21
-rw-r--r--src/components/emoji_picker/emoji_picker.scss9
-rw-r--r--src/components/emoji_reactions/emoji_reactions.js45
-rw-r--r--src/components/emoji_reactions/emoji_reactions.vue110
-rw-r--r--src/components/export_import/export_import.vue2
-rw-r--r--src/components/extra_buttons/extra_buttons.js3
-rw-r--r--src/components/extra_buttons/extra_buttons.vue14
-rw-r--r--src/components/follow_button/follow_button.vue2
-rw-r--r--src/components/interactions/interactions.js1
-rw-r--r--src/components/interactions/interactions.vue1
-rw-r--r--src/components/moderation_tools/moderation_tools.js11
-rw-r--r--src/components/moderation_tools/moderation_tools.vue17
-rw-r--r--src/components/mrf_transparency_panel/mrf_transparency_panel.js10
-rw-r--r--src/components/mrf_transparency_panel/mrf_transparency_panel.vue75
-rw-r--r--src/components/nav_panel/nav_panel.vue16
-rw-r--r--src/components/notification/notification.vue9
-rw-r--r--src/components/notifications/notifications.scss8
-rw-r--r--src/components/opacity_input/opacity_input.vue18
-rw-r--r--src/components/poll/poll.vue4
-rw-r--r--src/components/popover/popover.js156
-rw-r--r--src/components/popover/popover.vue118
-rw-r--r--src/components/popper/popper.scss147
-rw-r--r--src/components/range_input/range_input.vue2
-rw-r--r--src/components/react_button/react_button.js30
-rw-r--r--src/components/react_button/react_button.vue36
-rw-r--r--src/components/registration/registration.vue3
-rw-r--r--src/components/selectable_list/selectable_list.vue7
-rw-r--r--src/components/settings/settings.vue14
-rw-r--r--src/components/shadow_control/shadow_control.js44
-rw-r--r--src/components/shadow_control/shadow_control.vue11
-rw-r--r--src/components/side_drawer/side_drawer.vue16
-rw-r--r--src/components/status/status.js45
-rw-r--r--src/components/status/status.vue45
-rw-r--r--src/components/status_popover/status_popover.js11
-rw-r--r--src/components/status_popover/status_popover.vue74
-rw-r--r--src/components/sticker_picker/sticker_picker.vue2
-rw-r--r--src/components/style_switcher/preview.vue188
-rw-r--r--src/components/style_switcher/style_switcher.js592
-rw-r--r--src/components/style_switcher/style_switcher.scss38
-rw-r--r--src/components/style_switcher/style_switcher.vue423
-rw-r--r--src/components/tab_switcher/tab_switcher.scss7
-rw-r--r--src/components/user_card/user_card.js21
-rw-r--r--src/components/user_card/user_card.vue21
-rw-r--r--src/components/user_settings/user_settings.js2
-rw-r--r--src/components/user_settings/user_settings.vue9
55 files changed, 1911 insertions, 784 deletions
diff --git a/src/components/account_actions/account_actions.js b/src/components/account_actions/account_actions.js
index d2153680..5d7ecf7e 100644
--- a/src/components/account_actions/account_actions.js
+++ b/src/components/account_actions/account_actions.js
@@ -1,4 +1,5 @@
import ProgressButton from '../progress_button/progress_button.vue'
+import Popover from '../popover/popover.vue'
const AccountActions = {
props: [
@@ -8,7 +9,8 @@ const AccountActions = {
return { }
},
components: {
- ProgressButton
+ ProgressButton,
+ Popover
},
methods: {
showRepeats () {
diff --git a/src/components/account_actions/account_actions.vue b/src/components/account_actions/account_actions.vue
index d3235be1..483783cf 100644
--- a/src/components/account_actions/account_actions.vue
+++ b/src/components/account_actions/account_actions.vue
@@ -1,13 +1,13 @@
<template>
<div class="account-actions">
- <v-popover
+ <Popover
trigger="click"
- class="account-tools-popover"
- :container="false"
- placement="bottom-end"
- :offset="5"
+ placement="bottom"
>
- <div slot="popover">
+ <div
+ slot="content"
+ class="account-tools-popover"
+ >
<div class="dropdown-menu">
<template v-if="user.following">
<button
@@ -51,10 +51,13 @@
</button>
</div>
</div>
- <div class="btn btn-default ellipsis-button">
+ <div
+ slot="trigger"
+ class="btn btn-default ellipsis-button"
+ >
<i class="icon-ellipsis trigger-button" />
</div>
- </v-popover>
+ </Popover>
</div>
</template>
@@ -62,7 +65,6 @@
<style lang="scss">
@import '../../_variables.scss';
-@import '../popper/popper.scss';
.account-actions {
margin: 0 .8em;
}
@@ -70,6 +72,7 @@
.account-actions button.dropdown-item {
margin-left: 0;
}
+
.account-actions .trigger-button {
color: $fallback--lightText;
color: var(--lightText, $fallback--lightText);
diff --git a/src/components/attachment/attachment.vue b/src/components/attachment/attachment.vue
index 0748b2f0..a7e217c1 100644
--- a/src/components/attachment/attachment.vue
+++ b/src/components/attachment/attachment.vue
@@ -130,6 +130,8 @@
.placeholder {
margin-right: 8px;
margin-bottom: 4px;
+ color: $fallback--link;
+ color: var(--postLink, $fallback--link);
}
.nsfw-placeholder {
diff --git a/src/components/autosuggest/autosuggest.vue b/src/components/autosuggest/autosuggest.vue
index 1f86e996..f283ab82 100644
--- a/src/components/autosuggest/autosuggest.vue
+++ b/src/components/autosuggest/autosuggest.vue
@@ -40,8 +40,8 @@
top: 100%;
right: 0;
max-height: 400px;
- background-color: $fallback--lightBg;
- background-color: var(--lightBg, $fallback--lightBg);
+ background-color: $fallback--bg;
+ background-color: var(--bg, $fallback--bg);
border-style: solid;
border-width: 1px;
border-color: $fallback--border;
diff --git a/src/components/checkbox/checkbox.vue b/src/components/checkbox/checkbox.vue
index 1113f81d..03375b2f 100644
--- a/src/components/checkbox/checkbox.vue
+++ b/src/components/checkbox/checkbox.vue
@@ -87,13 +87,13 @@ export default {
&:checked + .checkbox-indicator::before {
color: $fallback--text;
- color: var(--text, $fallback--text);
+ color: var(--inputText, $fallback--text);
}
&:indeterminate + .checkbox-indicator::before {
content: '–';
color: $fallback--text;
- color: var(--text, $fallback--text);
+ color: var(--inputText, $fallback--text);
}
}
diff --git a/src/components/color_input/color_input.scss b/src/components/color_input/color_input.scss
new file mode 100644
index 00000000..8e9923cf
--- /dev/null
+++ b/src/components/color_input/color_input.scss
@@ -0,0 +1,68 @@
+@import '../../_variables.scss';
+
+.color-input {
+ display: inline-flex;
+
+ &-field.input {
+ display: inline-flex;
+ flex: 0 0 0;
+ max-width: 9em;
+ align-items: stretch;
+ padding: .2em 8px;
+
+ input {
+ background: none;
+ color: $fallback--lightText;
+ color: var(--inputText, $fallback--lightText);
+ border: none;
+ padding: 0;
+ margin: 0;
+
+ &.textColor {
+ flex: 1 0 3em;
+ min-width: 3em;
+ padding: 0;
+ }
+
+ &.nativeColor {
+ flex: 0 0 2em;
+ min-width: 2em;
+ align-self: center;
+ height: 100%;
+ }
+ }
+ .computedIndicator,
+ .transparentIndicator {
+ flex: 0 0 2em;
+ min-width: 2em;
+ align-self: center;
+ height: 100%;
+ }
+ .transparentIndicator {
+ // forgot to install counter-strike source, ooops
+ background-color: #FF00FF;
+ position: relative;
+ &::before, &::after {
+ display: block;
+ content: '';
+ background-color: #000000;
+ position: absolute;
+ height: 50%;
+ width: 50%;
+ }
+ &::after {
+ top: 0;
+ left: 0;
+ }
+ &::before {
+ bottom: 0;
+ right: 0;
+ }
+ }
+ }
+
+ .label {
+ flex: 1 1 auto;
+ }
+
+}
diff --git a/src/components/color_input/color_input.vue b/src/components/color_input/color_input.vue
index 9db62e81..8fb16113 100644
--- a/src/components/color_input/color_input.vue
+++ b/src/components/color_input/color_input.vue
@@ -1,6 +1,6 @@
<template>
<div
- class="color-control style-control"
+ class="color-input style-control"
:class="{ disabled: !present || disabled }"
>
<label
@@ -9,46 +9,100 @@
>
{{ label }}
</label>
- <input
- v-if="typeof fallback !== 'undefined'"
- :id="name + '-o'"
- class="opt exlcude-disabled"
- type="checkbox"
+ <Checkbox
+ v-if="typeof fallback !== 'undefined' && showOptionalTickbox"
:checked="present"
- @input="$emit('input', typeof value === 'undefined' ? fallback : undefined)"
- >
- <label
- v-if="typeof fallback !== 'undefined'"
- class="opt-l"
- :for="name + '-o'"
+ :disabled="disabled"
+ class="opt"
+ @change="$emit('input', typeof value === 'undefined' ? fallback : undefined)"
/>
- <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 class="input color-input-field">
+ <input
+ :id="name + '-t'"
+ class="textColor unstyled"
+ type="text"
+ :value="value || fallback"
+ :disabled="!present || disabled"
+ @input="$emit('input', $event.target.value)"
+ >
+ <input
+ v-if="validColor"
+ :id="name"
+ class="nativeColor unstyled"
+ type="color"
+ :value="value || fallback"
+ :disabled="!present || disabled"
+ @input="$emit('input', $event.target.value)"
+ >
+ <div
+ v-if="transparentColor"
+ class="transparentIndicator"
+ />
+ <div
+ v-if="computedColor"
+ class="computedIndicator"
+ :style="{backgroundColor: fallback}"
+ />
+ </div>
</div>
</template>
-
+<style lang="scss" src="./color_input.scss"></style>
<script>
+import Checkbox from '../checkbox/checkbox.vue'
+import { hex2rgb } from '../../services/color_convert/color_convert.js'
export default {
- props: [
- 'name', 'label', 'value', 'fallback', 'disabled'
- ],
+ components: {
+ Checkbox
+ },
+ props: {
+ // Name of color, used for identifying
+ name: {
+ required: true,
+ type: String
+ },
+ // Readable label
+ label: {
+ required: true,
+ type: String
+ },
+ // Color value, should be required but vue cannot tell the difference
+ // between "property missing" and "property set to undefined"
+ value: {
+ required: false,
+ type: String,
+ default: undefined
+ },
+ // Color fallback to use when value is not defeind
+ fallback: {
+ required: false,
+ type: String,
+ default: undefined
+ },
+ // Disable the control
+ disabled: {
+ required: false,
+ type: Boolean,
+ default: false
+ },
+ // Show "optional" tickbox, for when value might become mandatory
+ showOptionalTickbox: {
+ required: false,
+ type: Boolean,
+ default: true
+ }
+ },
computed: {
present () {
return typeof this.value !== 'undefined'
+ },
+ validColor () {
+ return hex2rgb(this.value || this.fallback)
+ },
+ transparentColor () {
+ return this.value === 'transparent'
+ },
+ computedColor () {
+ return this.value && this.value.startsWith('--')
}
}
}
diff --git a/src/components/contrast_ratio/contrast_ratio.vue b/src/components/contrast_ratio/contrast_ratio.vue
index 15a450a2..ba92bc17 100644
--- a/src/components/contrast_ratio/contrast_ratio.vue
+++ b/src/components/contrast_ratio/contrast_ratio.vue
@@ -37,9 +37,17 @@
<script>
export default {
- props: [
- 'large', 'contrast'
- ],
+ props: {
+ large: {
+ required: false
+ },
+ // TODO: Make theme switcher compute theme initially so that contrast
+ // component won't be called without contrast data
+ contrast: {
+ required: false,
+ type: Object
+ }
+ },
computed: {
hint () {
const levelVal = this.contrast.aaa ? 'aaa' : (this.contrast.aa ? 'aa' : 'bad')
diff --git a/src/components/dialog_modal/dialog_modal.vue b/src/components/dialog_modal/dialog_modal.vue
index 55d7a7d2..3241ce3e 100644
--- a/src/components/dialog_modal/dialog_modal.vue
+++ b/src/components/dialog_modal/dialog_modal.vue
@@ -75,18 +75,18 @@
.dialog-modal-content {
margin: 0;
padding: 1rem 1rem;
- background-color: $fallback--lightBg;
- background-color: var(--lightBg, $fallback--lightBg);
+ background-color: $fallback--bg;
+ background-color: var(--bg, $fallback--bg);
white-space: normal;
}
.dialog-modal-footer {
margin: 0;
padding: .5em .5em;
- background-color: $fallback--lightBg;
- background-color: var(--lightBg, $fallback--lightBg);
- border-top: 1px solid $fallback--bg;
- border-top: 1px solid var(--bg, $fallback--bg);
+ background-color: $fallback--bg;
+ background-color: var(--bg, $fallback--bg);
+ border-top: 1px solid $fallback--border;
+ border-top: 1px solid var(--border, $fallback--border);
display: flex;
justify-content: flex-end;
diff --git a/src/components/emoji_input/emoji_input.js b/src/components/emoji_input/emoji_input.js
index 001a22e9..f4c3479c 100644
--- a/src/components/emoji_input/emoji_input.js
+++ b/src/components/emoji_input/emoji_input.js
@@ -147,7 +147,7 @@ const EmojiInput = {
input.elm.addEventListener('keydown', this.onKeyDown)
input.elm.addEventListener('click', this.onClickInput)
input.elm.addEventListener('transitionend', this.onTransition)
- input.elm.addEventListener('compositionupdate', this.onCompositionUpdate)
+ input.elm.addEventListener('input', this.onInput)
},
unmounted () {
const { input } = this
@@ -159,7 +159,7 @@ const EmojiInput = {
input.elm.removeEventListener('keydown', this.onKeyDown)
input.elm.removeEventListener('click', this.onClickInput)
input.elm.removeEventListener('transitionend', this.onTransition)
- input.elm.removeEventListener('compositionupdate', this.onCompositionUpdate)
+ input.elm.removeEventListener('input', this.onInput)
}
},
methods: {
@@ -406,12 +406,6 @@ const EmojiInput = {
this.resize()
this.$emit('input', e.target.value)
},
- onCompositionUpdate (e) {
- this.showPicker = false
- this.setCaret(e)
- this.resize()
- this.$emit('input', e.target.value)
- },
onClickInput (e) {
this.showPicker = false
},
diff --git a/src/components/emoji_input/emoji_input.vue b/src/components/emoji_input/emoji_input.vue
index a7215670..e9ac09c3 100644
--- a/src/components/emoji_input/emoji_input.vue
+++ b/src/components/emoji_input/emoji_input.vue
@@ -109,10 +109,16 @@
box-shadow: 1px 2px 4px rgba(0, 0, 0, 0.5);
box-shadow: var(--popupShadow);
min-width: 75%;
- background: $fallback--bg;
- background: var(--bg, $fallback--bg);
- color: $fallback--lightText;
- color: var(--lightText, $fallback--lightText);
+ background-color: $fallback--bg;
+ background-color: var(--popover, $fallback--bg);
+ color: $fallback--link;
+ color: var(--popoverText, $fallback--link);
+ --faint: var(--popoverFaintText, $fallback--faint);
+ --faintLink: var(--popoverFaintLink, $fallback--faint);
+ --lightText: var(--popoverLightText, $fallback--lightText);
+ --postLink: var(--popoverPostLink, $fallback--link);
+ --postFaintLink: var(--popoverPostFaintLink, $fallback--link);
+ --icon: var(--popoverIcon, $fallback--icon);
}
}
@@ -157,7 +163,12 @@
&.highlighted {
background-color: $fallback--fg;
- background-color: var(--lightBg, $fallback--fg);
+ background-color: var(--selectedMenuPopover, $fallback--fg);
+ color: var(--selectedMenuPopoverText, $fallback--text);
+ --faint: var(--selectedMenuPopoverFaintText, $fallback--faint);
+ --faintLink: var(--selectedMenuPopoverFaintLink, $fallback--faint);
+ --lightText: var(--selectedMenuPopoverLightText, $fallback--lightText);
+ --icon: var(--selectedMenuPopoverIcon, $fallback--icon);
}
}
}
diff --git a/src/components/emoji_picker/emoji_picker.scss b/src/components/emoji_picker/emoji_picker.scss
index 6608f393..8bd07e45 100644
--- a/src/components/emoji_picker/emoji_picker.scss
+++ b/src/components/emoji_picker/emoji_picker.scss
@@ -8,6 +8,15 @@
left: 0;
margin: 0 !important;
z-index: 1;
+ background-color: $fallback--bg;
+ background-color: var(--popover, $fallback--bg);
+ color: $fallback--link;
+ color: var(--popoverText, $fallback--link);
+ --lightText: var(--popoverLightText, $fallback--faint);
+ --faint: var(--popoverFaintText, $fallback--faint);
+ --faintLink: var(--popoverFaintLink, $fallback--faint);
+ --lightText: var(--popoverLightText, $fallback--lightText);
+ --icon: var(--popoverIcon, $fallback--icon);
.keep-open,
.too-many-emoji {
diff --git a/src/components/emoji_reactions/emoji_reactions.js b/src/components/emoji_reactions/emoji_reactions.js
index 95d52cb6..ae7f53be 100644
--- a/src/components/emoji_reactions/emoji_reactions.js
+++ b/src/components/emoji_reactions/emoji_reactions.js
@@ -1,17 +1,52 @@
+import UserAvatar from '../user_avatar/user_avatar.vue'
+import Popover from '../popover/popover.vue'
+
+const EMOJI_REACTION_COUNT_CUTOFF = 12
const EmojiReactions = {
name: 'EmojiReactions',
+ components: {
+ UserAvatar,
+ Popover
+ },
props: ['status'],
+ data: () => ({
+ showAll: false
+ }),
computed: {
+ tooManyReactions () {
+ return this.status.emoji_reactions.length > EMOJI_REACTION_COUNT_CUTOFF
+ },
emojiReactions () {
- return this.status.emoji_reactions
+ return this.showAll
+ ? this.status.emoji_reactions
+ : this.status.emoji_reactions.slice(0, EMOJI_REACTION_COUNT_CUTOFF)
+ },
+ showMoreString () {
+ return `+${this.status.emoji_reactions.length - EMOJI_REACTION_COUNT_CUTOFF}`
+ },
+ accountsForEmoji () {
+ return this.status.emoji_reactions.reduce((acc, reaction) => {
+ acc[reaction.name] = reaction.accounts || []
+ return acc
+ }, {})
+ },
+ loggedIn () {
+ return !!this.$store.state.users.currentUser
}
},
methods: {
+ toggleShowAll () {
+ this.showAll = !this.showAll
+ },
reactedWith (emoji) {
- const user = this.$store.state.users.currentUser
- const reaction = this.status.emoji_reactions.find(r => r.emoji === emoji)
- return reaction.accounts && reaction.accounts.find(u => u.id === user.id)
+ return this.status.emoji_reactions.find(r => r.name === emoji).me
+ },
+ fetchEmojiReactionsByIfMissing () {
+ const hasNoAccounts = this.status.emoji_reactions.find(r => !r.accounts)
+ if (hasNoAccounts) {
+ this.$store.dispatch('fetchEmojiReactionsBy', this.status.id)
+ }
},
reactWith (emoji) {
this.$store.dispatch('reactWithEmoji', { id: this.status.id, emoji })
@@ -20,6 +55,8 @@ const EmojiReactions = {
this.$store.dispatch('unreactWithEmoji', { id: this.status.id, emoji })
},
emojiOnClick (emoji, event) {
+ if (!this.loggedIn) return
+
if (this.reactedWith(emoji)) {
this.unreact(emoji)
} else {
diff --git a/src/components/emoji_reactions/emoji_reactions.vue b/src/components/emoji_reactions/emoji_reactions.vue
index 00d6d2b7..bac4c605 100644
--- a/src/components/emoji_reactions/emoji_reactions.vue
+++ b/src/components/emoji_reactions/emoji_reactions.vue
@@ -1,15 +1,61 @@
<template>
<div class="emoji-reactions">
- <button
+ <Popover
v-for="(reaction) in emojiReactions"
- :key="reaction.emoji"
- class="emoji-reaction btn btn-default"
- :class="{ 'picked-reaction': reactedWith(reaction.emoji) }"
- @click="emojiOnClick(reaction.emoji, $event)"
+ :key="reaction.name"
+ trigger="hover"
+ placement="top"
+ :offset="{ y: 5 }"
>
- <span class="reaction-emoji">{{ reaction.emoji }}</span>
- <span>{{ reaction.count }}</span>
- </button>
+ <div
+ slot="content"
+ class="reacted-users"
+ >
+ <div v-if="accountsForEmoji[reaction.name].length">
+ <div
+ v-for="(account) in accountsForEmoji[reaction.name]"
+ :key="account.id"
+ class="reacted-user"
+ >
+ <UserAvatar
+ :user="account"
+ class="avatar-small"
+ :compact="true"
+ />
+ <div class="reacted-user-names">
+ <!-- eslint-disable vue/no-v-html -->
+ <span
+ class="reacted-user-name"
+ v-html="account.name_html"
+ />
+ <!-- eslint-enable vue/no-v-html -->
+ <span class="reacted-user-screen-name">{{ account.screen_name }}</span>
+ </div>
+ </div>
+ </div>
+ <div v-else>
+ <i class="icon-spin4 animate-spin" />
+ </div>
+ </div>
+ <button
+ slot="trigger"
+ class="emoji-reaction btn btn-default"
+ :class="{ 'picked-reaction': reactedWith(reaction.name), 'not-clickable': !loggedIn }"
+ @click="emojiOnClick(reaction.name, $event)"
+ @mouseenter="fetchEmojiReactionsByIfMissing()"
+ >
+ <span class="reaction-emoji">{{ reaction.name }}</span>
+ <span>{{ reaction.count }}</span>
+ </button>
+ </Popover>
+ <a
+ v-if="tooManyReactions"
+ class="emoji-reaction-expand faint"
+ href="javascript:void(0)"
+ @click="toggleShowAll"
+ >
+ {{ showAll ? $t('general.show_less') : showMoreString }}
+ </a>
</div>
</template>
@@ -23,6 +69,32 @@
flex-wrap: wrap;
}
+.reacted-users {
+ padding: 0.5em;
+}
+
+.reacted-user {
+ padding: 0.25em;
+ display: flex;
+ flex-direction: row;
+
+ .reacted-user-names {
+ display: flex;
+ flex-direction: column;
+ margin-left: 0.5em;
+ min-width: 5em;
+
+ img {
+ width: 1em;
+ height: 1em;
+ }
+ }
+
+ .reacted-user-screen-name {
+ font-size: 9px;
+ }
+}
+
.emoji-reaction {
padding: 0 0.5em;
margin-right: 0.5em;
@@ -38,10 +110,30 @@
&:focus {
outline: none;
}
+
+ &.not-clickable {
+ cursor: default;
+ &:hover {
+ box-shadow: $fallback--buttonShadow;
+ box-shadow: var(--buttonShadow);
+ }
+ }
+}
+
+.emoji-reaction-expand {
+ padding: 0 0.5em;
+ margin-right: 0.5em;
+ margin-top: 0.5em;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ &:hover {
+ text-decoration: underline;
+ }
}
.picked-reaction {
- border: 1px solid var(--link, $fallback--link);
+ border: 1px solid var(--accent, $fallback--link);
margin-left: -1px; // offset the border, can't use inset shadows either
margin-right: calc(0.5em - 1px);
}
diff --git a/src/components/export_import/export_import.vue b/src/components/export_import/export_import.vue
index 20c6f569..ae00487f 100644
--- a/src/components/export_import/export_import.vue
+++ b/src/components/export_import/export_import.vue
@@ -42,7 +42,7 @@ export default {
},
methods: {
exportData () {
- const stringified = JSON.stringify(this.exportObject) // Pretty-print and indent with 2 spaces
+ const stringified = JSON.stringify(this.exportObject, 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')
diff --git a/src/components/extra_buttons/extra_buttons.js b/src/components/extra_buttons/extra_buttons.js
index 5ac73e97..37485383 100644
--- a/src/components/extra_buttons/extra_buttons.js
+++ b/src/components/extra_buttons/extra_buttons.js
@@ -1,5 +1,8 @@
+import Popover from '../popover/popover.vue'
+
const ExtraButtons = {
props: [ 'status' ],
+ components: { Popover },
methods: {
deleteStatus () {
const confirmed = window.confirm(this.$t('status.delete_confirm'))
diff --git a/src/components/extra_buttons/extra_buttons.vue b/src/components/extra_buttons/extra_buttons.vue
index 746f1c91..3a7f1283 100644
--- a/src/components/extra_buttons/extra_buttons.vue
+++ b/src/components/extra_buttons/extra_buttons.vue
@@ -1,11 +1,11 @@
<template>
- <v-popover
+ <Popover
v-if="canDelete || canMute || canPin"
trigger="click"
placement="top"
class="extra-button-popover"
>
- <div slot="popover">
+ <div slot="content">
<div class="dropdown-menu">
<button
v-if="canMute && !status.thread_muted"
@@ -47,17 +47,17 @@
</button>
</div>
</div>
- <div class="button-icon">
- <i class="icon-ellipsis" />
- </div>
- </v-popover>
+ <i
+ slot="trigger"
+ class="icon-ellipsis button-icon"
+ />
+ </Popover>
</template>
<script src="./extra_buttons.js" ></script>
<style lang="scss">
@import '../../_variables.scss';
-@import '../popper/popper.scss';
.icon-ellipsis {
cursor: pointer;
diff --git a/src/components/follow_button/follow_button.vue b/src/components/follow_button/follow_button.vue
index f0cbb94b..bfdc137b 100644
--- a/src/components/follow_button/follow_button.vue
+++ b/src/components/follow_button/follow_button.vue
@@ -1,7 +1,7 @@
<template>
<button
class="btn btn-default follow-button"
- :class="{ pressed: isPressed }"
+ :class="{ toggled: isPressed }"
:disabled="inProgress"
:title="title"
@click="onClick"
diff --git a/src/components/interactions/interactions.js b/src/components/interactions/interactions.js
index cc31ff20..7fe5e76d 100644
--- a/src/components/interactions/interactions.js
+++ b/src/components/interactions/interactions.js
@@ -10,6 +10,7 @@ const tabModeDict = {
const Interactions = {
data () {
return {
+ allowFollowingMove: this.$store.state.users.currentUser.allow_following_move,
filterMode: tabModeDict['mentions']
}
},
diff --git a/src/components/interactions/interactions.vue b/src/components/interactions/interactions.vue
index a2e252ab..57d5d87c 100644
--- a/src/components/interactions/interactions.vue
+++ b/src/components/interactions/interactions.vue
@@ -22,6 +22,7 @@
:label="$t('interactions.follows')"
/>
<span
+ v-if="!allowFollowingMove"
key="moves"
:label="$t('interactions.moves')"
/>
diff --git a/src/components/moderation_tools/moderation_tools.js b/src/components/moderation_tools/moderation_tools.js
index 757166ed..d4fdc53e 100644
--- a/src/components/moderation_tools/moderation_tools.js
+++ b/src/components/moderation_tools/moderation_tools.js
@@ -1,4 +1,5 @@
import DialogModal from '../dialog_modal/dialog_modal.vue'
+import Popover from '../popover/popover.vue'
const FORCE_NSFW = 'mrf_tag:media-force-nsfw'
const STRIP_MEDIA = 'mrf_tag:media-strip'
@@ -14,7 +15,6 @@ const ModerationTools = {
],
data () {
return {
- showDropDown: false,
tags: {
FORCE_NSFW,
STRIP_MEDIA,
@@ -24,11 +24,13 @@ const ModerationTools = {
SANDBOX,
QUARANTINE
},
- showDeleteUserDialog: false
+ showDeleteUserDialog: false,
+ toggled: false
}
},
components: {
- DialogModal
+ DialogModal,
+ Popover
},
computed: {
tagsSet () {
@@ -89,6 +91,9 @@ const ModerationTools = {
window.history.back()
}
})
+ },
+ setToggled (value) {
+ this.toggled = value
}
}
}
diff --git a/src/components/moderation_tools/moderation_tools.vue b/src/components/moderation_tools/moderation_tools.vue
index 006d6373..b2d5acc5 100644
--- a/src/components/moderation_tools/moderation_tools.vue
+++ b/src/components/moderation_tools/moderation_tools.vue
@@ -1,13 +1,14 @@
<template>
<div>
- <v-popover
+ <Popover
trigger="click"
class="moderation-tools-popover"
- placement="bottom-end"
- @show="showDropDown = true"
- @hide="showDropDown = false"
+ placement="bottom"
+ :offset="{ y: 5 }"
+ @show="setToggled(true)"
+ @close="setToggled(false)"
>
- <div slot="popover">
+ <div slot="content">
<div class="dropdown-menu">
<span v-if="user.is_local">
<button
@@ -122,12 +123,13 @@
</div>
</div>
<button
+ slot="trigger"
class="btn btn-default btn-block"
- :class="{ pressed: showDropDown }"
+ :class="{ toggled }"
>
{{ $t('user_card.admin_menu.moderation') }}
</button>
- </v-popover>
+ </Popover>
<portal to="modal">
<DialogModal
v-if="showDeleteUserDialog"
@@ -160,7 +162,6 @@
<style lang="scss">
@import '../../_variables.scss';
-@import '../popper/popper.scss';
.menu-checkbox {
float: right;
diff --git a/src/components/mrf_transparency_panel/mrf_transparency_panel.js b/src/components/mrf_transparency_panel/mrf_transparency_panel.js
index 6a1baec8..a0b600d2 100644
--- a/src/components/mrf_transparency_panel/mrf_transparency_panel.js
+++ b/src/components/mrf_transparency_panel/mrf_transparency_panel.js
@@ -11,7 +11,10 @@ const MRFTransparencyPanel = {
rejectInstances: state => get(state, 'instance.federationPolicy.mrf_simple.reject', []),
ftlRemovalInstances: state => get(state, 'instance.federationPolicy.mrf_simple.federated_timeline_removal', []),
mediaNsfwInstances: state => get(state, 'instance.federationPolicy.mrf_simple.media_nsfw', []),
- mediaRemovalInstances: state => get(state, 'instance.federationPolicy.mrf_simple.media_removal', [])
+ mediaRemovalInstances: state => get(state, 'instance.federationPolicy.mrf_simple.media_removal', []),
+ keywordsFtlRemoval: state => get(state, 'instance.federationPolicy.mrf_keyword.federated_timeline_removal', []),
+ keywordsReject: state => get(state, 'instance.federationPolicy.mrf_keyword.reject', []),
+ keywordsReplace: state => get(state, 'instance.federationPolicy.mrf_keyword.replace', [])
}),
hasInstanceSpecificPolicies () {
return this.quarantineInstances.length ||
@@ -20,6 +23,11 @@ const MRFTransparencyPanel = {
this.ftlRemovalInstances.length ||
this.mediaNsfwInstances.length ||
this.mediaRemovalInstances.length
+ },
+ hasKeywordPolicies () {
+ return this.keywordsFtlRemoval.length ||
+ this.keywordsReject.length ||
+ this.keywordsReplace.length
}
}
}
diff --git a/src/components/mrf_transparency_panel/mrf_transparency_panel.vue b/src/components/mrf_transparency_panel/mrf_transparency_panel.vue
index d6495dc6..acdf822e 100644
--- a/src/components/mrf_transparency_panel/mrf_transparency_panel.vue
+++ b/src/components/mrf_transparency_panel/mrf_transparency_panel.vue
@@ -6,13 +6,13 @@
<div class="panel panel-default base01-background">
<div class="panel-heading timeline-heading base02-background">
<div class="title">
- {{ $t("about.federation") }}
+ {{ $t("about.mrf.federation") }}
</div>
</div>
<div class="panel-body">
<div class="mrf-section">
- <h2>{{ $t("about.mrf_policies") }}</h2>
- <p>{{ $t("about.mrf_policies_desc") }}</p>
+ <h2>{{ $t("about.mrf.mrf_policies") }}</h2>
+ <p>{{ $t("about.mrf.mrf_policies_desc") }}</p>
<ul>
<li
@@ -23,13 +23,13 @@
</ul>
<h2 v-if="hasInstanceSpecificPolicies">
- {{ $t("about.mrf_policy_simple") }}
+ {{ $t("about.mrf.simple.simple_policies") }}
</h2>
<div v-if="acceptInstances.length">
- <h4>{{ $t("about.mrf_policy_simple_accept") }}</h4>
+ <h4>{{ $t("about.mrf.simple.accept") }}</h4>
- <p>{{ $t("about.mrf_policy_simple_accept_desc") }}</p>
+ <p>{{ $t("about.mrf.simple.accept_desc") }}</p>
<ul>
<li
@@ -41,9 +41,9 @@
</div>
<div v-if="rejectInstances.length">
- <h4>{{ $t("about.mrf_policy_simple_reject") }}</h4>
+ <h4>{{ $t("about.mrf.simple.reject") }}</h4>
- <p>{{ $t("about.mrf_policy_simple_reject_desc") }}</p>
+ <p>{{ $t("about.mrf.simple.reject_desc") }}</p>
<ul>
<li
@@ -55,9 +55,9 @@
</div>
<div v-if="quarantineInstances.length">
- <h4>{{ $t("about.mrf_policy_simple_quarantine") }}</h4>
+ <h4>{{ $t("about.mrf.simple.quarantine") }}</h4>
- <p>{{ $t("about.mrf_policy_simple_quarantine_desc") }}</p>
+ <p>{{ $t("about.mrf.simple.quarantine_desc") }}</p>
<ul>
<li
@@ -69,9 +69,9 @@
</div>
<div v-if="ftlRemovalInstances.length">
- <h4>{{ $t("about.mrf_policy_simple_ftl_removal") }}</h4>
+ <h4>{{ $t("about.mrf.simple.ftl_removal") }}</h4>
- <p>{{ $t("about.mrf_policy_simple_ftl_removal_desc") }}</p>
+ <p>{{ $t("about.mrf.simple.ftl_removal_desc") }}</p>
<ul>
<li
@@ -83,9 +83,9 @@
</div>
<div v-if="mediaNsfwInstances.length">
- <h4>{{ $t("about.mrf_policy_simple_media_nsfw") }}</h4>
+ <h4>{{ $t("about.mrf.simple.media_nsfw") }}</h4>
- <p>{{ $t("about.mrf_policy_simple_media_nsfw_desc") }}</p>
+ <p>{{ $t("about.mrf.simple.media_nsfw_desc") }}</p>
<ul>
<li
@@ -97,9 +97,9 @@
</div>
<div v-if="mediaRemovalInstances.length">
- <h4>{{ $t("about.mrf_policy_simple_media_removal") }}</h4>
+ <h4>{{ $t("about.mrf.simple.media_removal") }}</h4>
- <p>{{ $t("about.mrf_policy_simple_media_removal_desc") }}</p>
+ <p>{{ $t("about.mrf.simple.media_removal_desc") }}</p>
<ul>
<li
@@ -109,6 +109,49 @@
/>
</ul>
</div>
+
+ <h2 v-if="hasKeywordPolicies">
+ {{ $t("about.mrf.keyword.keyword_policies") }}
+ </h2>
+
+ <div v-if="keywordsFtlRemoval.length">
+ <h4>{{ $t("about.mrf.keyword.ftl_removal") }}</h4>
+
+ <ul>
+ <li
+ v-for="keyword in keywordsFtlRemoval"
+ :key="keyword"
+ v-text="keyword"
+ />
+ </ul>
+ </div>
+
+ <div v-if="keywordsReject.length">
+ <h4>{{ $t("about.mrf.keyword.reject") }}</h4>
+
+ <ul>
+ <li
+ v-for="keyword in keywordsReject"
+ :key="keyword"
+ v-text="keyword"
+ />
+ </ul>
+ </div>
+
+ <div v-if="keywordsReplace.length">
+ <h4>{{ $t("about.mrf.keyword.replace") }}</h4>
+
+ <ul>
+ <li
+ v-for="keyword in keywordsReplace"
+ :key="keyword"
+ >
+ {{ keyword.pattern }}
+ {{ $t("about.mrf.keyword.is_replaced_by") }}
+ {{ keyword.replacement }}
+ </li>
+ </ul>
+ </div>
</div>
</div>
</div>
diff --git a/src/components/nav_panel/nav_panel.vue b/src/components/nav_panel/nav_panel.vue
index 0f3296eb..8cd04dc7 100644
--- a/src/components/nav_panel/nav_panel.vue
+++ b/src/components/nav_panel/nav_panel.vue
@@ -100,13 +100,25 @@
&:hover {
background-color: $fallback--lightBg;
- background-color: var(--lightBg, $fallback--lightBg);
+ background-color: var(--selectedMenu, $fallback--lightBg);
+ color: $fallback--link;
+ color: var(--selectedMenuText, $fallback--link);
+ --faint: var(--selectedMenuFaintText, $fallback--faint);
+ --faintLink: var(--selectedMenuFaintLink, $fallback--faint);
+ --lightText: var(--selectedMenuLightText, $fallback--lightText);
+ --icon: var(--selectedMenuIcon, $fallback--icon);
}
&.router-link-active {
font-weight: bolder;
background-color: $fallback--lightBg;
- background-color: var(--lightBg, $fallback--lightBg);
+ background-color: var(--selectedMenu, $fallback--lightBg);
+ color: $fallback--text;
+ color: var(--selectedMenuText, $fallback--text);
+ --faint: var(--selectedMenuFaintText, $fallback--faint);
+ --faintLink: var(--selectedMenuFaintLink, $fallback--faint);
+ --lightText: var(--selectedMenuLightText, $fallback--lightText);
+ --icon: var(--selectedMenuIcon, $fallback--icon);
&:hover {
text-decoration: underline;
diff --git a/src/components/notification/notification.vue b/src/components/notification/notification.vue
index 16124e50..51875747 100644
--- a/src/components/notification/notification.vue
+++ b/src/components/notification/notification.vue
@@ -47,7 +47,7 @@
<span class="notification-details">
<div class="name-and-action">
<!-- eslint-disable vue/no-v-html -->
- <span
+ <bdi
v-if="!!notification.from_profile.name_html"
class="username"
:title="'@'+notification.from_profile.screen_name"
@@ -78,6 +78,13 @@
<i class="fa icon-arrow-curved lit" />
<small>{{ $t('notifications.migrated_to') }}</small>
</span>
+ <span v-if="notification.type === 'pleroma:emoji_reaction'">
+ <small>
+ <i18n path="notifications.reacted_with">
+ <span class="emoji-reaction-emoji">{{ notification.emoji }}</span>
+ </i18n>
+ </small>
+ </span>
</div>
<div
v-if="notification.type === 'follow' || notification.type === 'move'"
diff --git a/src/components/notifications/notifications.scss b/src/components/notifications/notifications.scss
index 148ac7f2..a8f4430f 100644
--- a/src/components/notifications/notifications.scss
+++ b/src/components/notifications/notifications.scss
@@ -68,6 +68,9 @@
a {
color: var(--faintLink);
}
+ .status-content a {
+ color: var(--postFaintLink);
+ }
}
padding: 0;
.media-body {
@@ -78,6 +81,7 @@
.follow-text, .move-text {
padding: 0.5em 0;
+ overflow-wrap: break-word;
}
.status-el {
@@ -94,6 +98,10 @@
min-width: 0;
}
+ .emoji-reaction-emoji {
+ font-size: 16px;
+ }
+
.notification-details {
min-width: 0px;
word-wrap: break-word;
diff --git a/src/components/opacity_input/opacity_input.vue b/src/components/opacity_input/opacity_input.vue
index c677f18c..3cc3942b 100644
--- a/src/components/opacity_input/opacity_input.vue
+++ b/src/components/opacity_input/opacity_input.vue
@@ -9,18 +9,12 @@
>
{{ $t('settings.style.common.opacity') }}
</label>
- <input
+ <Checkbox
v-if="typeof fallback !== 'undefined'"
- :id="name + '-o'"
- class="opt exclude-disabled"
- type="checkbox"
:checked="present"
- @input="$emit('input', !present ? fallback : undefined)"
- >
- <label
- v-if="typeof fallback !== 'undefined'"
- class="opt-l"
- :for="name + '-o'"
+ :disabled="disabled"
+ class="opt"
+ @change="$emit('input', !present ? fallback : undefined)"
/>
<input
:id="name"
@@ -37,7 +31,11 @@
</template>
<script>
+import Checkbox from '../checkbox/checkbox.vue'
export default {
+ components: {
+ Checkbox
+ },
props: [
'name', 'value', 'fallback', 'disabled'
],
diff --git a/src/components/poll/poll.vue b/src/components/poll/poll.vue
index db8e33b3..56e91cca 100644
--- a/src/components/poll/poll.vue
+++ b/src/components/poll/poll.vue
@@ -104,8 +104,10 @@
.result-fill {
height: 100%;
position: absolute;
+ color: $fallback--text;
+ color: var(--pollText, $fallback--text);
background-color: $fallback--lightBg;
- background-color: var(--linkBg, $fallback--lightBg);
+ background-color: var(--poll, $fallback--lightBg);
border-radius: $fallback--panelRadius;
border-radius: var(--panelRadius, $fallback--panelRadius);
top: 0;
diff --git a/src/components/popover/popover.js b/src/components/popover/popover.js
new file mode 100644
index 00000000..5881d266
--- /dev/null
+++ b/src/components/popover/popover.js
@@ -0,0 +1,156 @@
+
+const Popover = {
+ name: 'Popover',
+ props: {
+ // Action to trigger popover: either 'hover' or 'click'
+ trigger: String,
+ // Either 'top' or 'bottom'
+ placement: String,
+ // Takes object with properties 'x' and 'y', values of these can be
+ // 'container' for using offsetParent as boundaries for either axis
+ // or 'viewport'
+ boundTo: Object,
+ // Takes a top/bottom/left/right object, how much space to leave
+ // between boundary and popover element
+ margin: Object,
+ // Takes a x/y object and tells how many pixels to offset from
+ // anchor point on either axis
+ offset: Object,
+ // Additional styles you may want for the popover container
+ popoverClass: String
+ },
+ data () {
+ return {
+ hidden: true,
+ styles: { opacity: 0 },
+ oldSize: { width: 0, height: 0 }
+ }
+ },
+ methods: {
+ updateStyles () {
+ if (this.hidden) {
+ this.styles = {
+ opacity: 0
+ }
+ return
+ }
+
+ // Popover will be anchored around this element, trigger ref is the container, so
+ // its children are what are inside the slot. Expect only one slot="trigger".
+ const anchorEl = (this.$refs.trigger && this.$refs.trigger.children[0]) || this.$el
+ const screenBox = anchorEl.getBoundingClientRect()
+ // Screen position of the origin point for popover
+ const origin = { x: screenBox.left + screenBox.width * 0.5, y: screenBox.top }
+ const content = this.$refs.content
+ // Minor optimization, don't call a slow reflow call if we don't have to
+ const parentBounds = this.boundTo &&
+ (this.boundTo.x === 'container' || this.boundTo.y === 'container') &&
+ this.$el.offsetParent.getBoundingClientRect()
+ const margin = this.margin || {}
+
+ // What are the screen bounds for the popover? Viewport vs container
+ // when using viewport, using default margin values to dodge the navbar
+ const xBounds = this.boundTo && this.boundTo.x === 'container' ? {
+ min: parentBounds.left + (margin.left || 0),
+ max: parentBounds.right - (margin.right || 0)
+ } : {
+ min: 0 + (margin.left || 10),
+ max: window.innerWidth - (margin.right || 10)
+ }
+
+ const yBounds = this.boundTo && this.boundTo.y === 'container' ? {
+ min: parentBounds.top + (margin.top || 0),
+ max: parentBounds.bottom - (margin.bottom || 0)
+ } : {
+ min: 0 + (margin.top || 50),
+ max: window.innerHeight - (margin.bottom || 5)
+ }
+
+ let horizOffset = 0
+
+ // If overflowing from left, move it so that it doesn't
+ if ((origin.x - content.offsetWidth * 0.5) < xBounds.min) {
+ horizOffset += -(origin.x - content.offsetWidth * 0.5) + xBounds.min
+ }
+
+ // If overflowing from right, move it so that it doesn't
+ if ((origin.x + horizOffset + content.offsetWidth * 0.5) > xBounds.max) {
+ horizOffset -= (origin.x + horizOffset + content.offsetWidth * 0.5) - xBounds.max
+ }
+
+ // Default to whatever user wished with placement prop
+ let usingTop = this.placement !== 'bottom'
+
+ // Handle special cases, first force to displaying on top if there's not space on bottom,
+ // regardless of what placement value was. Then check if there's not space on top, and
+ // force to bottom, again regardless of what placement value was.
+ if (origin.y + content.offsetHeight > yBounds.max) usingTop = true
+ if (origin.y - content.offsetHeight < yBounds.min) usingTop = false
+
+ const yOffset = (this.offset && this.offset.y) || 0
+ const translateY = usingTop
+ ? -anchorEl.offsetHeight - yOffset - content.offsetHeight
+ : yOffset
+
+ const xOffset = (this.offset && this.offset.x) || 0
+ const translateX = (anchorEl.offsetWidth * 0.5) - content.offsetWidth * 0.5 + horizOffset + xOffset
+
+ // Note, separate translateX and translateY avoids blurry text on chromium,
+ // single translate or translate3d resulted in blurry text.
+ this.styles = {
+ opacity: 1,
+ transform: `translateX(${Math.floor(translateX)}px) translateY(${Math.floor(translateY)}px)`
+ }
+ },
+ showPopover () {
+ if (this.hidden) this.$emit('show')
+ this.hidden = false
+ this.$nextTick(this.updateStyles)
+ },
+ hidePopover () {
+ if (!this.hidden) this.$emit('close')
+ this.hidden = true
+ this.styles = { opacity: 0 }
+ },
+ onMouseenter (e) {
+ if (this.trigger === 'hover') this.showPopover()
+ },
+ onMouseleave (e) {
+ if (this.trigger === 'hover') this.hidePopover()
+ },
+ onClick (e) {
+ if (this.trigger === 'click') {
+ if (this.hidden) {
+ this.showPopover()
+ } else {
+ this.hidePopover()
+ }
+ }
+ },
+ onClickOutside (e) {
+ if (this.hidden) return
+ if (this.$el.contains(e.target)) return
+ this.hidePopover()
+ }
+ },
+ updated () {
+ // Monitor changes to content size, update styles only when content sizes have changed,
+ // that should be the only time we need to move the popover box if we don't care about scroll
+ // or resize
+ const content = this.$refs.content
+ if (!content) return
+ if (this.oldSize.width !== content.offsetWidth || this.oldSize.height !== content.offsetHeight) {
+ this.updateStyles()
+ this.oldSize = { width: content.offsetWidth, height: content.offsetHeight }
+ }
+ },
+ created () {
+ document.addEventListener('click', this.onClickOutside)
+ },
+ destroyed () {
+ document.removeEventListener('click', this.onClickOutside)
+ this.hidePopover()
+ }
+}
+
+export default Popover
diff --git a/src/components/popover/popover.vue b/src/components/popover/popover.vue
new file mode 100644
index 00000000..a271cb1b
--- /dev/null
+++ b/src/components/popover/popover.vue
@@ -0,0 +1,118 @@
+<template>
+ <div
+ @mouseenter="onMouseenter"
+ @mouseleave="onMouseleave"
+ >
+ <div
+ ref="trigger"
+ @click="onClick"
+ >
+ <slot name="trigger" />
+ </div>
+ <div
+ v-if="!hidden"
+ ref="content"
+ :style="styles"
+ class="popover"
+ :class="popoverClass"
+ >
+ <slot
+ name="content"
+ class="popover-inner"
+ :close="hidePopover"
+ />
+ </div>
+ </div>
+</template>
+
+<script src="./popover.js" />
+
+<style lang=scss>
+@import '../../_variables.scss';
+
+.popover {
+ z-index: 8;
+ position: absolute;
+ min-width: 0;
+ transition: opacity 0.3s;
+
+ box-shadow: 1px 1px 4px rgba(0,0,0,.6);
+ box-shadow: var(--panelShadow);
+ border-radius: $fallback--btnRadius;
+ border-radius: var(--btnRadius, $fallback--btnRadius);
+
+ background-color: $fallback--bg;
+ background-color: var(--popover, $fallback--bg);
+ color: $fallback--text;
+ color: var(--popoverText, $fallback--text);
+ --faint: var(--popoverFaintText, $fallback--faint);
+ --faintLink: var(--popoverFaintLink, $fallback--faint);
+ --lightText: var(--popoverLightText, $fallback--lightText);
+ --postLink: var(--popoverPostLink, $fallback--link);
+ --postFaintLink: var(--popoverPostFaintLink, $fallback--link);
+ --icon: var(--popoverIcon, $fallback--icon);
+}
+
+.dropdown-menu {
+ display: block;
+ padding: .5rem 0;
+ font-size: 1rem;
+ text-align: left;
+ list-style: none;
+ max-width: 100vw;
+ z-index: 10;
+ white-space: nowrap;
+
+ .dropdown-divider {
+ height: 0;
+ margin: .5rem 0;
+ overflow: hidden;
+ border-top: 1px solid $fallback--border;
+ border-top: 1px solid var(--border, $fallback--border);
+ }
+
+ .dropdown-item {
+ line-height: 21px;
+ margin-right: 5px;
+ overflow: auto;
+ display: block;
+ padding: .25rem 1.0rem .25rem 1.5rem;
+ clear: both;
+ font-weight: 400;
+ text-align: inherit;
+ white-space: nowrap;
+ border: none;
+ border-radius: 0px;
+ background-color: transparent;
+ box-shadow: none;
+ width: 100%;
+ height: 100%;
+
+ --btnText: var(--popoverText, $fallback--text);
+
+ &-icon {
+ padding-left: 0.5rem;
+
+ i {
+ margin-right: 0.25rem;
+ color: var(--menuPopoverIcon, $fallback--icon)
+ }
+ }
+
+ &:active, &:hover {
+ background-color: $fallback--lightBg;
+ background-color: var(--selectedMenuPopover, $fallback--lightBg);
+ color: $fallback--link;
+ color: var(--selectedMenuPopoverText, $fallback--link);
+ --faint: var(--selectedMenuPopoverFaintText, $fallback--faint);
+ --faintLink: var(--selectedMenuPopoverFaintLink, $fallback--faint);
+ --lightText: var(--selectedMenuPopoverLightText, $fallback--lightText);
+ --icon: var(--selectedMenuPopoverIcon, $fallback--icon);
+ i {
+ color: var(--selectedMenuPopoverIcon, $fallback--icon);
+ }
+ }
+
+ }
+}
+</style>
diff --git a/src/components/popper/popper.scss b/src/components/popper/popper.scss
deleted file mode 100644
index 06daa871..00000000
--- a/src/components/popper/popper.scss
+++ /dev/null
@@ -1,147 +0,0 @@
-@import '../../_variables.scss';
-
-.tooltip.popover {
- z-index: 8;
-
- .popover-inner {
- box-shadow: 1px 1px 4px rgba(0,0,0,.6);
- box-shadow: var(--panelShadow);
- border-radius: $fallback--btnRadius;
- border-radius: var(--btnRadius, $fallback--btnRadius);
- background-color: $fallback--bg;
- background-color: var(--bg, $fallback--bg);
- }
-
- .popover-arrow {
- width: 0;
- height: 0;
- border-style: solid;
- position: absolute;
- margin: 5px;
- border-color: $fallback--bg;
- border-color: var(--bg, $fallback--bg);
- }
-
- &[x-placement^="top"] {
- margin-bottom: 5px;
-
- .popover-arrow {
- border-width: 5px 5px 0 5px;
- border-left-color: transparent !important;
- border-right-color: transparent !important;
- border-bottom-color: transparent !important;
- bottom: -4px;
- left: calc(50% - 5px);
- margin-top: 0;
- margin-bottom: 0;
- }
- }
-
- &[x-placement^="bottom"] {
- margin-top: 5px;
-
- .popover-arrow {
- border-width: 0 5px 5px 5px;
- border-left-color: transparent !important;
- border-right-color: transparent !important;
- border-top-color: transparent !important;
- top: -4px;
- left: calc(50% - 5px);
- margin-top: 0;
- margin-bottom: 0;
- }
- }
-
- &[x-placement^="right"] {
- margin-left: 5px;
-
- .popover-arrow {
- border-width: 5px 5px 5px 0;
- border-left-color: transparent !important;
- border-top-color: transparent !important;
- border-bottom-color: transparent !important;
- left: -4px;
- top: calc(50% - 5px);
- margin-left: 0;
- margin-right: 0;
- }
- }
-
- &[x-placement^="left"] {
- margin-right: 5px;
-
- .popover-arrow {
- border-width: 5px 0 5px 5px;
- border-top-color: transparent !important;
- border-right-color: transparent !important;
- border-bottom-color: transparent !important;
- right: -4px;
- top: calc(50% - 5px);
- margin-left: 0;
- margin-right: 0;
- }
- }
-
- &[aria-hidden='true'] {
- visibility: hidden;
- opacity: 0;
- transition: opacity .15s, visibility .15s;
- }
-
- &[aria-hidden='false'] {
- visibility: visible;
- opacity: 1;
- transition: opacity .15s;
- }
-}
-
-.dropdown-menu {
- display: block;
- padding: .5rem 0;
- font-size: 1rem;
- text-align: left;
- list-style: none;
- max-width: 100vw;
- z-index: 10;
-
- .dropdown-divider {
- height: 0;
- margin: .5rem 0;
- overflow: hidden;
- border-top: 1px solid $fallback--border;
- border-top: 1px solid var(--border, $fallback--border);
- }
-
- .dropdown-item {
- line-height: 21px;
- margin-right: 5px;
- overflow: auto;
- display: block;
- padding: .25rem 1.0rem .25rem 1.5rem;
- clear: both;
- font-weight: 400;
- text-align: inherit;
- white-space: normal;
- border: none;
- border-radius: 0px;
- background-color: transparent;
- box-shadow: none;
- width: 100%;
- height: 100%;
-
- &-icon {
- padding-left: 0.5rem;
-
- i {
- margin-right: 0.25rem;
- }
- }
-
- &:hover {
- // TODO: improve the look on breeze themes
- background-color: $fallback--fg;
- background-color: var(--btn, $fallback--fg);
- box-shadow: none;
- }
- }
-}
diff --git a/src/components/range_input/range_input.vue b/src/components/range_input/range_input.vue
index aaa2ed26..5857a5c1 100644
--- a/src/components/range_input/range_input.vue
+++ b/src/components/range_input/range_input.vue
@@ -12,7 +12,7 @@
<input
v-if="typeof fallback !== 'undefined'"
:id="name + '-o'"
- class="opt exclude-disabled"
+ class="opt"
type="checkbox"
:checked="present"
@input="$emit('input', !present ? fallback : undefined)"
diff --git a/src/components/react_button/react_button.js b/src/components/react_button/react_button.js
index 6fb2a780..19949563 100644
--- a/src/components/react_button/react_button.js
+++ b/src/components/react_button/react_button.js
@@ -1,29 +1,25 @@
+import Popover from '../popover/popover.vue'
import { mapGetters } from 'vuex'
const ReactButton = {
props: ['status', 'loggedIn'],
data () {
return {
- showTooltip: false,
- filterWord: '',
- popperOptions: {
- modifiers: {
- preventOverflow: { padding: { top: 50 }, boundariesElement: 'viewport' }
- }
- }
+ filterWord: ''
}
},
+ components: {
+ Popover
+ },
methods: {
- openReactionSelect () {
- this.showTooltip = true
- this.filterWord = ''
- },
- closeReactionSelect () {
- this.showTooltip = false
- },
- addReaction (event, emoji) {
- this.$store.dispatch('reactWithEmoji', { id: this.status.id, emoji })
- this.closeReactionSelect()
+ addReaction (event, emoji, close) {
+ const existingReaction = this.status.emoji_reactions.find(r => r.name === emoji)
+ if (existingReaction && existingReaction.me) {
+ this.$store.dispatch('unreactWithEmoji', { id: this.status.id, emoji })
+ } else {
+ this.$store.dispatch('reactWithEmoji', { id: this.status.id, emoji })
+ }
+ close()
}
},
computed: {
diff --git a/src/components/react_button/react_button.vue b/src/components/react_button/react_button.vue
index c925dd71..ab4b4fcd 100644
--- a/src/components/react_button/react_button.vue
+++ b/src/components/react_button/react_button.vue
@@ -1,13 +1,14 @@
<template>
- <v-popover
- :popper-options="popperOptions"
- :open="showTooltip"
- trigger="manual"
+ <Popover
+ trigger="click"
placement="top"
+ :offset="{ y: 5 }"
class="react-button-popover"
- @hide="closeReactionSelect"
>
- <div slot="popover">
+ <div
+ slot="content"
+ slot-scope="{close}"
+ >
<div class="reaction-picker-filter">
<input
v-model="filterWord"
@@ -19,7 +20,7 @@
v-for="emoji in commonEmojis"
:key="emoji"
class="emoji-button"
- @click="addReaction($event, emoji)"
+ @click="addReaction($event, emoji, close)"
>
{{ emoji }}
</span>
@@ -28,23 +29,20 @@
v-for="(emoji, key) in emojis"
:key="key"
class="emoji-button"
- @click="addReaction($event, emoji.replacement)"
+ @click="addReaction($event, emoji.replacement, close)"
>
{{ emoji.replacement }}
</span>
<div class="reaction-bottom-fader" />
</div>
</div>
- <div
+ <i
v-if="loggedIn"
- @click.prevent="openReactionSelect"
- >
- <i
- class="icon-smile button-icon add-reaction-button"
- :title="$t('tool_tip.add_reaction')"
- />
- </div>
- </v-popover>
+ slot="trigger"
+ class="icon-smile button-icon add-reaction-button"
+ :title="$t('tool_tip.add_reaction')"
+ />
+ </Popover>
</template>
<script src="./react_button.js" ></script>
@@ -54,6 +52,10 @@
.reaction-picker-filter {
padding: 0.5em;
+ display: flex;
+ input {
+ flex: 1;
+ }
}
.reaction-picker-divider {
diff --git a/src/components/registration/registration.vue b/src/components/registration/registration.vue
index fdbda007..a83ca1e5 100644
--- a/src/components/registration/registration.vue
+++ b/src/components/registration/registration.vue
@@ -187,6 +187,9 @@
class="form-control"
type="text"
autocomplete="off"
+ autocorrect="off"
+ autocapitalize="off"
+ spellcheck="false"
>
</template>
</div>
diff --git a/src/components/selectable_list/selectable_list.vue b/src/components/selectable_list/selectable_list.vue
index d9ec7ece..a9bb12a1 100644
--- a/src/components/selectable_list/selectable_list.vue
+++ b/src/components/selectable_list/selectable_list.vue
@@ -68,7 +68,12 @@
&-item-selected-inner {
background-color: $fallback--lightBg;
- background-color: var(--lightBg, $fallback--lightBg);
+ background-color: var(--selectedMenu, $fallback--lightBg);
+ color: var(--selectedMenuText, $fallback--text);
+ --faint: var(--selectedMenuFaintText, $fallback--faint);
+ --faintLink: var(--selectedMenuFaintLink, $fallback--faint);
+ --lightText: var(--selectedMenuLightText, $fallback--lightText);
+ --icon: var(--selectedMenuIcon, $fallback--icon);
}
&-header {
diff --git a/src/components/settings/settings.vue b/src/components/settings/settings.vue
index cef492f3..9e14b449 100644
--- a/src/components/settings/settings.vue
+++ b/src/components/settings/settings.vue
@@ -76,9 +76,9 @@
<li>
<Checkbox v-model="useStreamingApi">
{{ $t('settings.useStreamingApi') }}
- <br/>
+ <br>
<small>
- {{ $t('settings.useStreamingApiWarning') }}
+ {{ $t('settings.useStreamingApiWarning') }}
</small>
</Checkbox>
</li>
@@ -92,6 +92,11 @@
{{ $t('settings.reply_link_preview') }}
</Checkbox>
</li>
+ <li>
+ <Checkbox v-model="emojiReactionsOnTimeline">
+ {{ $t('settings.emoji_reactions_on_timeline') }}
+ </Checkbox>
+ </li>
</ul>
</div>
@@ -328,6 +333,11 @@
{{ $t('settings.notification_visibility_moves') }}
</Checkbox>
</li>
+ <li>
+ <Checkbox v-model="notificationVisibility.emojiReactions">
+ {{ $t('settings.notification_visibility_emoji_reactions') }}
+ </Checkbox>
+ </li>
</ul>
</div>
<div>
diff --git a/src/components/shadow_control/shadow_control.js b/src/components/shadow_control/shadow_control.js
index 44e4a22f..f9e7b985 100644
--- a/src/components/shadow_control/shadow_control.js
+++ b/src/components/shadow_control/shadow_control.js
@@ -3,6 +3,17 @@ 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'
+const toModel = (object = {}) => ({
+ x: 0,
+ y: 0,
+ blur: 0,
+ spread: 0,
+ inset: false,
+ color: '#000000',
+ alpha: 1,
+ ...object
+})
+
export default {
// 'Value' and 'Fallback' can be undefined, but if they are
// initially vue won't detect it when they become something else
@@ -15,7 +26,7 @@ export default {
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 || []
+ cValue: (this.value || this.fallback || []).map(toModel)
}
},
components: {
@@ -24,12 +35,12 @@ export default {
},
methods: {
add () {
- this.cValue.push(Object.assign({}, this.selected))
+ this.cValue.push(toModel(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
+ this.selectedId = this.cValue.length === 0 ? undefined : Math.max(this.selectedId - 1, 0)
},
moveUp () {
const movable = this.cValue.splice(this.selectedId, 1)[0]
@@ -46,19 +57,24 @@ export default {
this.cValue = this.value || this.fallback
},
computed: {
+ anyShadows () {
+ return this.cValue.length > 0
+ },
+ anyShadowsFallback () {
+ return this.fallback.length > 0
+ },
selected () {
- if (this.ready && this.cValue.length > 0) {
+ if (this.ready && this.anyShadows) {
return this.cValue[this.selectedId]
} else {
- return {
- x: 0,
- y: 0,
- blur: 0,
- spread: 0,
- inset: false,
- color: '#000000',
- alpha: 1
- }
+ return toModel({})
+ }
+ },
+ currentFallback () {
+ if (this.ready && this.anyShadowsFallback) {
+ return this.fallback[this.selectedId]
+ } else {
+ return toModel({})
}
},
moveUpValid () {
@@ -80,7 +96,7 @@ export default {
},
style () {
return this.ready ? {
- boxShadow: getCssShadow(this.cValue)
+ boxShadow: getCssShadow(this.fallback)
} : {}
}
}
diff --git a/src/components/shadow_control/shadow_control.vue b/src/components/shadow_control/shadow_control.vue
index de8a42d1..815a9e59 100644
--- a/src/components/shadow_control/shadow_control.vue
+++ b/src/components/shadow_control/shadow_control.vue
@@ -191,15 +191,20 @@
v-model="selected.color"
:disabled="!present"
:label="$t('settings.style.common.color')"
+ :fallback="currentFallback.color"
+ :show-optional-tickbox="false"
name="shadow"
/>
<OpacityInput
v-model="selected.alpha"
:disabled="!present"
/>
- <p>
- {{ $t('settings.style.shadows.hint') }}
- </p>
+ <i18n
+ path="settings.style.shadows.hintV3"
+ tag="p"
+ >
+ <code>--variable,mod</code>
+ </i18n>
</div>
</div>
</template>
diff --git a/src/components/side_drawer/side_drawer.vue b/src/components/side_drawer/side_drawer.vue
index 28637afc..df1d22a4 100644
--- a/src/components/side_drawer/side_drawer.vue
+++ b/src/components/side_drawer/side_drawer.vue
@@ -223,7 +223,13 @@
box-shadow: 1px 1px 4px rgba(0, 0, 0, 0.6);
box-shadow: var(--panelShadow);
background-color: $fallback--bg;
- background-color: var(--bg, $fallback--bg);
+ background-color: var(--popover, $fallback--bg);
+ color: $fallback--link;
+ color: var(--popoverText, $fallback--link);
+ --faint: var(--popoverFaintText, $fallback--faint);
+ --faintLink: var(--popoverFaintLink, $fallback--faint);
+ --lightText: var(--popoverLightText, $fallback--lightText);
+ --icon: var(--popoverIcon, $fallback--icon);
.button-icon:before {
width: 1.1em;
@@ -289,7 +295,13 @@
&:hover {
background-color: $fallback--lightBg;
- background-color: var(--lightBg, $fallback--lightBg);
+ background-color: var(--selectedMenuPopover, $fallback--lightBg);
+ color: $fallback--text;
+ color: var(--selectedMenuPopoverText, $fallback--text);
+ --faint: var(--selectedMenuPopoverFaintText, $fallback--faint);
+ --faintLink: var(--selectedMenuPopoverFaintLink, $fallback--faint);
+ --lightText: var(--selectedMenuPopoverLightText, $fallback--lightText);
+ --icon: var(--selectedMenuPopoverIcon, $fallback--icon);
}
}
}
diff --git a/src/components/status/status.js b/src/components/status/status.js
index 81b57667..61d66301 100644
--- a/src/components/status/status.js
+++ b/src/components/status/status.js
@@ -188,23 +188,22 @@ const Status = {
}
return this.status.attentions.length > 0
},
+
+ // When a status has a subject and is also tall, we should only have one show more/less button. If the default is to collapse statuses with subjects, we just treat it like a status with a subject; otherwise, we just treat it like a tall status.
+ mightHideBecauseSubject () {
+ return this.status.summary && (!this.tallStatus || this.localCollapseSubjectDefault)
+ },
+ mightHideBecauseTall () {
+ return this.tallStatus && (!this.status.summary || !this.localCollapseSubjectDefault)
+ },
hideSubjectStatus () {
- if (this.tallStatus && !this.localCollapseSubjectDefault) {
- return false
- }
- return !this.expandingSubject && this.status.summary
+ return this.mightHideBecauseSubject && !this.expandingSubject
},
hideTallStatus () {
- if (this.status.summary && this.localCollapseSubjectDefault) {
- return false
- }
- if (this.showingTall) {
- return false
- }
- return this.tallStatus
+ return this.mightHideBecauseTall && !this.showingTall
},
showingMore () {
- return (this.tallStatus && this.showingTall) || (this.status.summary && this.expandingSubject)
+ return (this.mightHideBecauseTall && this.showingTall) || (this.mightHideBecauseSubject && this.expandingSubject)
},
nsfwClickthrough () {
if (!this.status.nsfw) {
@@ -256,6 +255,16 @@ const Status = {
file => !fileType.fileMatchesSomeType(this.galleryTypes, file)
)
},
+ hasImageAttachments () {
+ return this.status.attachments.some(
+ file => fileType.fileType(file.mimetype) === 'image'
+ )
+ },
+ hasVideoAttachments () {
+ return this.status.attachments.some(
+ file => fileType.fileType(file.mimetype) === 'video'
+ )
+ },
maxThumbnails () {
return this.mergedConfig.maxThumbnails
},
@@ -398,14 +407,10 @@ const Status = {
this.userExpanded = !this.userExpanded
},
toggleShowMore () {
- if (this.showingTall) {
- this.showingTall = false
- } else if (this.expandingSubject && this.status.summary) {
- this.expandingSubject = false
- } else if (this.hideTallStatus) {
- this.showingTall = true
- } else if (this.hideSubjectStatus && this.status.summary) {
- this.expandingSubject = true
+ if (this.mightHideBecauseTall) {
+ this.showingTall = !this.showingTall
+ } else if (this.mightHideBecauseSubject) {
+ this.expandingSubject = !this.expandingSubject
}
},
generateUserProfileLink (id, name) {
diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index d5739304..ca295640 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -177,6 +177,8 @@
<StatusPopover
v-if="!isPreview"
:status-id="status.in_reply_to_status_id"
+ class="reply-to-popover"
+ style="min-width: 0"
>
<a
class="reply-to"
@@ -277,7 +279,21 @@
href="#"
class="cw-status-hider"
@click.prevent="toggleShowMore"
- >{{ $t("general.show_more") }}</a>
+ >
+ {{ $t("general.show_more") }}
+ <span
+ v-if="hasImageAttachments"
+ class="icon-picture"
+ />
+ <span
+ v-if="hasVideoAttachments"
+ class="icon-video"
+ />
+ <span
+ v-if="status.card"
+ class="icon-link"
+ />
+ </a>
<a
v-if="showingMore"
href="#"
@@ -355,6 +371,7 @@
</transition>
<EmojiReactions
+ v-if="(mergedConfig.emojiReactionsOnTimeline || isFocused) && (!noHeading && !isPreview)"
:status="status"
/>
@@ -453,7 +470,15 @@ $status-margin: 0.75em;
&_focused {
background-color: $fallback--lightBg;
- background-color: var(--lightBg, $fallback--lightBg);
+ background-color: var(--selectedPost, $fallback--lightBg);
+ color: $fallback--text;
+ color: var(--selectedPostText, $fallback--text);
+ --lightText: var(--selectedPostLightText, $fallback--light);
+ --faint: var(--selectedPostFaintText, $fallback--faint);
+ --faintLink: var(--selectedPostFaintLink, $fallback--faint);
+ --postLink: var(--selectedPostPostLink, $fallback--faint);
+ --postFaintLink: var(--selectedPostFaintPostLink, $fallback--faint);
+ --icon: var(--selectedPostIcon, $fallback--icon);
}
.timeline & {
@@ -549,11 +574,10 @@ $status-margin: 0.75em;
align-items: stretch;
> .reply-to-and-accountname > a {
+ overflow: hidden;
max-width: 100%;
text-overflow: ellipsis;
- overflow: hidden;
white-space: nowrap;
- display: inline-block;
word-break: break-all;
}
}
@@ -562,7 +586,6 @@ $status-margin: 0.75em;
display: flex;
height: 18px;
margin-right: 0.5em;
- overflow: hidden;
max-width: 100%;
.icon-reply {
transform: scaleX(-1);
@@ -573,6 +596,10 @@ $status-margin: 0.75em;
display: flex;
}
+ .reply-to-popover {
+ min-width: 0;
+ }
+
.reply-to {
display: flex;
}
@@ -580,9 +607,8 @@ $status-margin: 0.75em;
.reply-to-text {
overflow: hidden;
text-overflow: ellipsis;
+ white-space: nowrap;
margin: 0 0.4em 0 0.2em;
- color: $fallback--faint;
- color: var(--faint, $fallback--faint);
}
.replies-separator {
@@ -644,6 +670,11 @@ $status-margin: 0.75em;
line-height: 1.4em;
white-space: pre-wrap;
+ a {
+ color: $fallback--link;
+ color: var(--postLink, $fallback--link);
+ }
+
img, video {
max-width: 100%;
max-height: 400px;
diff --git a/src/components/status_popover/status_popover.js b/src/components/status_popover/status_popover.js
index 19f16bd9..159132a9 100644
--- a/src/components/status_popover/status_popover.js
+++ b/src/components/status_popover/status_popover.js
@@ -7,11 +7,7 @@ const StatusPopover = {
],
data () {
return {
- popperOptions: {
- modifiers: {
- preventOverflow: { padding: { top: 50 }, boundariesElement: 'viewport' }
- }
- }
+ error: false
}
},
computed: {
@@ -20,12 +16,15 @@ const StatusPopover = {
}
},
components: {
- Status: () => import('../status/status.vue')
+ Status: () => import('../status/status.vue'),
+ Popover: () => import('../popover/popover.vue')
},
methods: {
enter () {
if (!this.status) {
this.$store.dispatch('fetchStatus', this.statusId)
+ .then(data => (this.error = false))
+ .catch(e => (this.error = true))
}
}
}
diff --git a/src/components/status_popover/status_popover.vue b/src/components/status_popover/status_popover.vue
index eacf4c06..f5948207 100644
--- a/src/components/status_popover/status_popover.vue
+++ b/src/components/status_popover/status_popover.vue
@@ -1,11 +1,16 @@
<template>
- <v-popover
+ <Popover
+ trigger="hover"
popover-class="status-popover"
- placement="top-start"
- :popper-options="popperOptions"
- @show="enter()"
+ :bound-to="{ x: 'container' }"
+ @show="enter"
>
- <template slot="popover">
+ <template slot="trigger">
+ <slot />
+ </template>
+ <div
+ slot="content"
+ >
<Status
v-if="status"
:is-preview="true"
@@ -13,15 +18,19 @@
:compact="true"
/>
<div
+ v-else-if="error"
+ class="status-preview-no-content faint"
+ >
+ {{ $t('status.status_unavailable') }}
+ </div>
+ <div
v-else
- class="status-preview-loading"
+ class="status-preview-no-content"
>
<i class="icon-spin4 animate-spin" />
</div>
- </template>
-
- <slot />
- </v-popover>
+ </div>
+ </Popover>
</template>
<script src="./status_popover.js" ></script>
@@ -29,50 +38,25 @@
<style lang="scss">
@import '../../_variables.scss';
-.tooltip.popover.status-popover {
+.status-popover {
font-size: 1rem;
min-width: 15em;
max-width: 95%;
- margin-left: 0.5em;
- .popover-inner {
- border-color: $fallback--border;
- border-color: var(--border, $fallback--border);
- border-style: solid;
- border-width: 1px;
- 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);
- }
-
- .popover-arrow::before {
- position: absolute;
- content: '';
- left: -7px;
- border: solid 7px transparent;
- z-index: -1;
- }
-
- &[x-placement^="bottom-start"] .popover-arrow::before {
- top: -2px;
- border-top-width: 0;
- border-bottom-color: $fallback--border;
- border-bottom-color: var(--border, $fallback--border);
- }
-
- &[x-placement^="top-start"] .popover-arrow::before {
- bottom: -2px;
- border-bottom-width: 0;
- border-top-color: $fallback--border;
- border-top-color: var(--border, $fallback--border);
- }
+ border-color: $fallback--border;
+ border-color: var(--border, $fallback--border);
+ border-style: solid;
+ border-width: 1px;
+ 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);
.status-el.status-el {
border: none;
}
- .status-preview-loading {
+ .status-preview-no-content {
padding: 1em;
text-align: center;
diff --git a/src/components/sticker_picker/sticker_picker.vue b/src/components/sticker_picker/sticker_picker.vue
index 3863908a..dc449ccb 100644
--- a/src/components/sticker_picker/sticker_picker.vue
+++ b/src/components/sticker_picker/sticker_picker.vue
@@ -51,7 +51,7 @@
img {
height: 100%;
&:hover {
- filter: drop-shadow(0 0 5px var(--link, $fallback--link));
+ filter: drop-shadow(0 0 5px var(--accent, $fallback--link));
}
}
}
diff --git a/src/components/style_switcher/preview.vue b/src/components/style_switcher/preview.vue
index 101a32bd..9d984659 100644
--- a/src/components/style_switcher/preview.vue
+++ b/src/components/style_switcher/preview.vue
@@ -1,101 +1,117 @@
<template>
- <div class="panel dummy">
- <div class="panel-heading">
- <div class="title">
- {{ $t('settings.style.preview.header') }}
- <span class="badge badge-notification">
- 99
+ <div class="preview-container">
+ <div class="underlay underlay-preview" />
+ <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>
- <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>
+ <div class="panel-body theme-preview-content">
+ <div class="post">
+ <div class="avatar still-image">
+ ( ͡° ͜ʖ ͡°)
+ </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>
+ <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="button-icon icon-reply"
- />
- <i
- style="color: var(--cGreen)"
- class="button-icon icon-retweet"
- />
- <i
- style="color: var(--cOrange)"
- class="button-icon icon-star"
- />
- <i
- style="color: var(--cRed)"
- class="button-icon icon-cancel"
- />
+ <div class="icons">
+ <i
+ style="color: var(--cBlue)"
+ class="button-icon icon-reply"
+ />
+ <i
+ style="color: var(--cGreen)"
+ class="button-icon icon-retweet"
+ />
+ <i
+ style="color: var(--cOrange)"
+ class="button-icon icon-star"
+ />
+ <i
+ style="color: var(--cRed)"
+ class="button-icon icon-cancel"
+ />
+ </div>
</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 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>
- <div class="separator" />
+ <div class="separator" />
- <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
- id="preview_checkbox"
- checked="very yes"
- type="checkbox"
- >
- <label for="preview_checkbox">{{ $t('settings.style.preview.checkbox') }}</label>
+ <span class="alert error">
+ {{ $t('settings.style.preview.error') }}
</span>
- <button class="btn">
- {{ $t('settings.style.preview.button') }}
- </button>
+ <input
+ :value="$t('settings.style.preview.input')"
+ type="text"
+ >
+
+ <div class="actions">
+ <span class="checkbox">
+ <input
+ id="preview_checkbox"
+ checked="very yes"
+ type="checkbox"
+ >
+ <label for="preview_checkbox">{{ $t('settings.style.preview.checkbox') }}</label>
+ </span>
+ <button class="btn">
+ {{ $t('settings.style.preview.button') }}
+ </button>
+ </div>
</div>
</div>
</div>
</template>
+
+<style lang="scss">
+.preview-container {
+ position: relative;
+}
+.underlay-preview {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 10px;
+ right: 10px;
+}
+</style>
diff --git a/src/components/style_switcher/style_switcher.js b/src/components/style_switcher/style_switcher.js
index ebde4475..a7f586f4 100644
--- a/src/components/style_switcher/style_switcher.js
+++ b/src/components/style_switcher/style_switcher.js
@@ -1,6 +1,29 @@
-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 {
+ rgb2hex,
+ hex2rgb,
+ getContrastRatioLayers
+} from '../../services/color_convert/color_convert.js'
+import {
+ DEFAULT_SHADOWS,
+ generateColors,
+ generateShadows,
+ generateRadii,
+ generateFonts,
+ composePreset,
+ getThemes,
+ shadows2to3,
+ colors2to3
+} from '../../services/style_setter/style_setter.js'
+import {
+ SLOT_INHERITANCE
+} from '../../services/theme_data/pleromafe.js'
+import {
+ CURRENT_VERSION,
+ OPACITIES,
+ getLayers,
+ getOpacitySlot
+} from '../../services/theme_data/theme_data.service.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'
@@ -24,11 +47,22 @@ const v1OnlyNames = [
'cOrange'
].map(_ => _ + 'ColorLocal')
+const colorConvert = (color) => {
+ if (color.startsWith('--') || color === 'transparent') {
+ return color
+ } else {
+ return hex2rgb(color)
+ }
+}
+
export default {
data () {
return {
availableStyles: [],
selected: this.$store.getters.mergedConfig.theme,
+ themeWarning: undefined,
+ tempImportFile: undefined,
+ engineVersion: 0,
previewShadows: {},
previewColors: {},
@@ -45,51 +79,13 @@ export default {
keepRoundness: false,
keepFonts: false,
- textColorLocal: '',
- linkColorLocal: '',
-
- bgColorLocal: '',
- bgOpacityLocal: undefined,
-
- fgColorLocal: '',
- fgTextColorLocal: undefined,
- fgLinkColorLocal: undefined,
-
- btnColorLocal: undefined,
- btnTextColorLocal: undefined,
- btnOpacityLocal: undefined,
-
- inputColorLocal: undefined,
- inputTextColorLocal: undefined,
- inputOpacityLocal: undefined,
+ ...Object.keys(SLOT_INHERITANCE)
+ .map(key => [key, ''])
+ .reduce((acc, [key, val]) => ({ ...acc, [ key + 'ColorLocal' ]: val }), {}),
- panelColorLocal: undefined,
- panelTextColorLocal: undefined,
- panelLinkColorLocal: undefined,
- panelFaintColorLocal: undefined,
- panelOpacityLocal: undefined,
-
- topBarColorLocal: undefined,
- topBarTextColorLocal: undefined,
- topBarLinkColorLocal: undefined,
-
- alertErrorColorLocal: undefined,
- alertWarningColorLocal: undefined,
-
- badgeOpacityLocal: undefined,
- badgeNotificationColorLocal: undefined,
-
- borderColorLocal: undefined,
- borderOpacityLocal: undefined,
-
- faintColorLocal: undefined,
- faintOpacityLocal: undefined,
- faintLinkColorLocal: undefined,
-
- cRedColorLocal: '',
- cBlueColorLocal: '',
- cGreenColorLocal: '',
- cOrangeColorLocal: '',
+ ...Object.keys(OPACITIES)
+ .map(key => [key, ''])
+ .reduce((acc, [key, val]) => ({ ...acc, [ key + 'OpacityLocal' ]: val }), {}),
shadowSelected: undefined,
shadowsLocal: {},
@@ -108,69 +104,105 @@ export default {
created () {
const self = this
- getThemes().then((themesComplete) => {
- self.availableStyles = themesComplete
- })
+ getThemes()
+ .then((promises) => {
+ return Promise.all(
+ Object.entries(promises)
+ .map(([k, v]) => v.then(res => [k, res]))
+ )
+ })
+ .then(themes => themes.reduce((acc, [k, v]) => {
+ if (v) {
+ return {
+ ...acc,
+ [k]: v
+ }
+ } else {
+ return acc
+ }
+ }, {}))
+ .then((themesComplete) => {
+ self.availableStyles = themesComplete
+ })
},
mounted () {
- this.normalizeLocalState(this.$store.getters.mergedConfig.customTheme)
+ this.loadThemeFromLocalStorage()
if (typeof this.shadowSelected === 'undefined') {
this.shadowSelected = this.shadowsAvailable[0]
}
},
computed: {
+ themeWarningHelp () {
+ if (!this.themeWarning) return
+ const t = this.$t
+ const pre = 'settings.style.switcher.help.'
+ const {
+ origin,
+ themeEngineVersion,
+ type,
+ noActionsPossible
+ } = this.themeWarning
+ if (origin === 'file') {
+ // Loaded v2 theme from file
+ if (themeEngineVersion === 2 && type === 'wrong_version') {
+ return t(pre + 'v2_imported')
+ }
+ if (themeEngineVersion > CURRENT_VERSION) {
+ return t(pre + 'future_version_imported') + ' ' +
+ (
+ noActionsPossible
+ ? t(pre + 'snapshot_missing')
+ : t(pre + 'snapshot_present')
+ )
+ }
+ if (themeEngineVersion < CURRENT_VERSION) {
+ return t(pre + 'future_version_imported') + ' ' +
+ (
+ noActionsPossible
+ ? t(pre + 'snapshot_missing')
+ : t(pre + 'snapshot_present')
+ )
+ }
+ } else if (origin === 'localStorage') {
+ if (type === 'snapshot_source_mismatch') {
+ return t(pre + 'snapshot_source_mismatch')
+ }
+ // FE upgraded from v2
+ if (themeEngineVersion === 2) {
+ return t(pre + 'upgraded_from_v2')
+ }
+ // Admin downgraded FE
+ if (themeEngineVersion > CURRENT_VERSION) {
+ return t(pre + 'fe_downgraded') + ' ' +
+ (
+ noActionsPossible
+ ? t(pre + 'migration_snapshot_ok')
+ : t(pre + 'migration_snapshot_gone')
+ )
+ }
+ // Admin upgraded FE
+ if (themeEngineVersion < CURRENT_VERSION) {
+ return t(pre + 'fe_upgraded') + ' ' +
+ (
+ noActionsPossible
+ ? t(pre + 'migration_snapshot_ok')
+ : t(pre + 'migration_snapshot_gone')
+ )
+ }
+ }
+ },
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,
- alertWarning: this.alertWarningColorLocal,
- badgeNotification: this.badgeNotificationColorLocal,
-
- faint: this.faintColorLocal,
- faintLink: this.faintLinkColorLocal,
- border: this.borderColorLocal,
-
- cRed: this.cRedColorLocal,
- cBlue: this.cBlueColorLocal,
- cGreen: this.cGreenColorLocal,
- cOrange: this.cOrangeColorLocal
- }
+ return Object.keys(SLOT_INHERITANCE)
+ .map(key => [key, this[key + 'ColorLocal']])
+ .reduce((acc, [key, val]) => ({ ...acc, [ key ]: val }), {})
},
currentOpacity () {
- return {
- bg: this.bgOpacityLocal,
- btn: this.btnOpacityLocal,
- input: this.inputOpacityLocal,
- panel: this.panelOpacityLocal,
- topBar: this.topBarOpacityLocal,
- border: this.borderOpacityLocal,
- faint: this.faintOpacityLocal
- }
+ return Object.keys(OPACITIES)
+ .map(key => [key, this[key + 'OpacityLocal']])
+ .reduce((acc, [key, val]) => ({ ...acc, [ key ]: val }), {})
},
currentRadii () {
return {
@@ -193,75 +225,66 @@ export default {
},
// 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),
- alertWarning: hex2rgb(colors.alertWarning),
- 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)
+ try {
+ 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
+ })
+ const colorsConverted = Object.entries(colors).reduce((acc, [key, value]) => ({ ...acc, [key]: colorConvert(value) }), {})
+
+ const ratios = Object.entries(SLOT_INHERITANCE).reduce((acc, [key, value]) => {
+ const slotIsBaseText = key === 'text' || key === 'link'
+ const slotIsText = slotIsBaseText || (
+ typeof value === 'object' && value !== null && value.textColor
+ )
+ if (!slotIsText) return acc
+ const { layer, variant } = slotIsBaseText ? { layer: 'bg' } : value
+ const background = variant || layer
+ const opacitySlot = getOpacitySlot(background)
+ const textColors = [
+ key,
+ ...(background === 'bg' ? ['cRed', 'cGreen', 'cBlue', 'cOrange'] : [])
+ ]
+
+ const layers = getLayers(
+ layer,
+ variant || layer,
+ opacitySlot,
+ colorsConverted,
+ opacity
+ )
+
+ return {
+ ...acc,
+ ...textColors.reduce((acc, textColorKey) => {
+ const newKey = slotIsBaseText
+ ? 'bg' + textColorKey[0].toUpperCase() + textColorKey.slice(1)
+ : textColorKey
+ return {
+ ...acc,
+ [newKey]: getContrastRatioLayers(
+ colorsConverted[textColorKey],
+ layers,
+ colorsConverted[textColorKey]
+ )
+ }
+ }, {})
+ }
+ }, {})
+
+ return Object.entries(ratios).reduce((acc, [k, v]) => { acc[k] = hints(v); return acc }, {})
+ } catch (e) {
+ console.warn('Failure computing contrasts', e)
}
-
- return Object.entries(ratios).reduce((acc, [k, v]) => { acc[k] = hints(v); return acc }, {})
},
previewRules () {
if (!this.preview.rules) return ''
@@ -272,7 +295,7 @@ export default {
].join(';')
},
shadowsAvailable () {
- return Object.keys(this.previewTheme.shadows).sort()
+ return Object.keys(DEFAULT_SHADOWS).sort()
},
currentShadowOverriden: {
get () {
@@ -287,7 +310,7 @@ export default {
}
},
currentShadowFallback () {
- return this.previewTheme.shadows[this.shadowSelected]
+ return (this.previewTheme.shadows || {})[this.shadowSelected]
},
currentShadow: {
get () {
@@ -309,27 +332,34 @@ export default {
!this.keepColor
)
- const theme = {}
+ const source = {
+ themeEngineVersion: CURRENT_VERSION
+ }
if (this.keepFonts || saveEverything) {
- theme.fonts = this.fontsLocal
+ source.fonts = this.fontsLocal
}
if (this.keepShadows || saveEverything) {
- theme.shadows = this.shadowsLocal
+ source.shadows = this.shadowsLocal
}
if (this.keepOpacity || saveEverything) {
- theme.opacity = this.currentOpacity
+ source.opacity = this.currentOpacity
}
if (this.keepColor || saveEverything) {
- theme.colors = this.currentColors
+ source.colors = this.currentColors
}
if (this.keepRoundness || saveEverything) {
- theme.radii = this.currentRadii
+ source.radii = this.currentRadii
+ }
+
+ const theme = {
+ themeEngineVersion: CURRENT_VERSION,
+ ...this.previewTheme
}
return {
- // To separate from other random JSON files and possible future theme formats
- _pleroma_theme_version: 2, theme
+ // To separate from other random JSON files and possible future source formats
+ _pleroma_theme_version: 2, theme, source
}
}
},
@@ -346,10 +376,128 @@ export default {
Checkbox
},
methods: {
+ loadTheme (
+ {
+ theme,
+ source,
+ _pleroma_theme_version: fileVersion
+ },
+ origin,
+ forceUseSource = false
+ ) {
+ this.dismissWarning()
+ if (!source && !theme) {
+ throw new Error('Can\'t load theme: empty')
+ }
+ const version = (origin === 'localStorage' && !theme.colors)
+ ? 'l1'
+ : fileVersion
+ const snapshotEngineVersion = (theme || {}).themeEngineVersion
+ const themeEngineVersion = (source || {}).themeEngineVersion || 2
+ const versionsMatch = themeEngineVersion === CURRENT_VERSION
+ const sourceSnapshotMismatch = (
+ theme !== undefined &&
+ source !== undefined &&
+ themeEngineVersion !== snapshotEngineVersion
+ )
+ // Force loading of source if user requested it or if snapshot
+ // is unavailable
+ const forcedSourceLoad = (source && forceUseSource) || !theme
+ if (!(versionsMatch && !sourceSnapshotMismatch) &&
+ !forcedSourceLoad &&
+ version !== 'l1' &&
+ origin !== 'defaults'
+ ) {
+ if (sourceSnapshotMismatch && origin === 'localStorage') {
+ this.themeWarning = {
+ origin,
+ themeEngineVersion,
+ type: 'snapshot_source_mismatch'
+ }
+ } else if (!theme) {
+ this.themeWarning = {
+ origin,
+ noActionsPossible: true,
+ themeEngineVersion,
+ type: 'no_snapshot_old_version'
+ }
+ } else if (!versionsMatch) {
+ this.themeWarning = {
+ origin,
+ noActionsPossible: !source,
+ themeEngineVersion,
+ type: 'wrong_version'
+ }
+ }
+ }
+ this.normalizeLocalState(theme, version, source, forcedSourceLoad)
+ },
+ forceLoadLocalStorage () {
+ this.loadThemeFromLocalStorage(true)
+ },
+ dismissWarning () {
+ this.themeWarning = undefined
+ this.tempImportFile = undefined
+ },
+ forceLoad () {
+ const { origin } = this.themeWarning
+ switch (origin) {
+ case 'localStorage':
+ this.loadThemeFromLocalStorage(true)
+ break
+ case 'file':
+ this.onImport(this.tempImportFile, true)
+ break
+ }
+ this.dismissWarning()
+ },
+ forceSnapshot () {
+ const { origin } = this.themeWarning
+ switch (origin) {
+ case 'localStorage':
+ this.loadThemeFromLocalStorage(false, true)
+ break
+ case 'file':
+ console.err('Forcing snapshout from file is not supported yet')
+ break
+ }
+ this.dismissWarning()
+ },
+ loadThemeFromLocalStorage (confirmLoadSource = false, forceSnapshot = false) {
+ const {
+ customTheme: theme,
+ customThemeSource: source
+ } = this.$store.getters.mergedConfig
+ if (!theme && !source) {
+ // Anon user or never touched themes
+ this.loadTheme(
+ this.$store.state.instance.themeData,
+ 'defaults',
+ confirmLoadSource
+ )
+ } else {
+ this.loadTheme(
+ {
+ theme,
+ source: forceSnapshot ? theme : source
+ },
+ 'localStorage',
+ confirmLoadSource
+ )
+ }
+ },
setCustomTheme () {
this.$store.dispatch('setOption', {
name: 'customTheme',
value: {
+ themeEngineVersion: CURRENT_VERSION,
+ ...this.previewTheme
+ }
+ })
+ this.$store.dispatch('setOption', {
+ name: 'customThemeSource',
+ value: {
+ themeEngineVersion: CURRENT_VERSION,
shadows: this.shadowsLocal,
fonts: this.fontsLocal,
opacity: this.currentOpacity,
@@ -358,21 +506,27 @@ export default {
}
})
},
- onImport (parsed) {
- if (parsed._pleroma_theme_version === 1) {
- this.normalizeLocalState(parsed, 1)
- } else if (parsed._pleroma_theme_version === 2) {
- this.normalizeLocalState(parsed.theme, 2)
- }
+ updatePreviewColorsAndShadows () {
+ this.previewColors = generateColors({
+ opacity: this.currentOpacity,
+ colors: this.currentColors
+ })
+ this.previewShadows = generateShadows(
+ { shadows: this.shadowsLocal, opacity: this.previewTheme.opacity, themeEngineVersion: this.engineVersion },
+ this.previewColors.theme.colors,
+ this.previewColors.mod
+ )
+ },
+ onImport (parsed, forceSource = false) {
+ this.tempImportFile = parsed
+ this.loadTheme(parsed, 'file', forceSource)
},
importValidator (parsed) {
const version = parsed._pleroma_theme_version
return version >= 1 || version <= 2
},
clearAll () {
- const state = this.$store.getters.mergedConfig.customTheme
- const version = state.colors ? 2 : 'l1'
- this.normalizeLocalState(this.$store.getters.mergedConfig.customTheme, version)
+ this.loadThemeFromLocalStorage()
},
// Clears all the extra stuff when loading V1 theme
@@ -411,19 +565,37 @@ export default {
/**
* This applies stored theme data onto form. Supports three versions of data:
+ * v3 (version >= 3) - newest version of themes which supports snapshots for better compatiblity
* 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 {Object} theme - theme data (snapshot)
* @param {Number} version - version of data. 0 means try to guess based on data. "l1" means v1, locastorage type
+ * @param {Object} source - theme source - this will be used if compatible
+ * @param {Boolean} source - by default source won't be used if version doesn't match since it might render differently
+ * this allows importing source anyway
*/
- normalizeLocalState (input, version = 0) {
- const colors = input.colors || input
+ normalizeLocalState (theme, version = 0, source, forceSource = false) {
+ let input
+ if (typeof source !== 'undefined') {
+ if (forceSource || source.themeEngineVersion === CURRENT_VERSION) {
+ input = source
+ version = source.themeEngineVersion
+ } else {
+ input = theme
+ }
+ } else {
+ input = theme
+ }
+
const radii = input.radii || input
const opacity = input.opacity
const shadows = input.shadows || {}
const fonts = input.fonts || {}
+ const colors = !input.themeEngineVersion
+ ? colors2to3(input.colors || input)
+ : input.colors || input
if (version === 0) {
if (input.version) version = input.version
@@ -437,6 +609,8 @@ export default {
}
}
+ this.engineVersion = version
+
// Stuff that differs between V1 and V2
if (version === 1) {
this.fgColorLocal = rgb2hex(colors.btn)
@@ -445,7 +619,7 @@ export default {
if (!this.keepColor) {
this.clearV1()
- const keys = new Set(version !== 1 ? Object.keys(colors) : [])
+ const keys = new Set(version !== 1 ? Object.keys(SLOT_INHERITANCE) : [])
if (version === 1 || version === 'l1') {
keys
.add('bg')
@@ -457,7 +631,17 @@ export default {
}
keys.forEach(key => {
- this[key + 'ColorLocal'] = rgb2hex(colors[key])
+ const color = colors[key]
+ const hex = rgb2hex(colors[key])
+ this[key + 'ColorLocal'] = hex === '#aN' ? color : hex
+ })
+ }
+
+ 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
})
}
@@ -472,7 +656,11 @@ export default {
if (!this.keepShadows) {
this.clearShadows()
- this.shadowsLocal = shadows
+ if (version === 2) {
+ this.shadowsLocal = shadows2to3(shadows, this.previewTheme.opacity)
+ } else {
+ this.shadowsLocal = shadows
+ }
this.shadowSelected = this.shadowsAvailable[0]
}
@@ -480,14 +668,6 @@ export default {
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: {
@@ -502,8 +682,9 @@ export default {
},
shadowsLocal: {
handler () {
+ if (Object.getOwnPropertyNames(this.previewColors).length === 1) return
try {
- this.previewShadows = generateShadows({ shadows: this.shadowsLocal })
+ this.updatePreviewColorsAndShadows()
this.shadowsInvalid = false
} catch (e) {
this.shadowsInvalid = true
@@ -526,27 +707,24 @@ export default {
},
currentColors () {
try {
- this.previewColors = generateColors({
- opacity: this.currentOpacity,
- colors: this.currentColors
- })
+ this.updatePreviewColorsAndShadows()
this.colorsInvalid = false
+ this.shadowsInvalid = false
} catch (e) {
this.colorsInvalid = true
+ this.shadowsInvalid = true
console.warn(e)
}
},
currentOpacity () {
try {
- this.previewColors = generateColors({
- opacity: this.currentOpacity,
- colors: this.currentColors
- })
+ this.updatePreviewColorsAndShadows()
} catch (e) {
console.warn(e)
}
},
selected () {
+ this.dismissWarning()
if (this.selectedVersion === 1) {
if (!this.keepRoundness) {
this.clearRoundness()
@@ -573,7 +751,7 @@ export default {
this.cOrangeColorLocal = this.selected[8]
}
} else if (this.selectedVersion >= 2) {
- this.normalizeLocalState(this.selected.theme, 2)
+ this.normalizeLocalState(this.selected.theme, 2, this.selected.source)
}
}
}
diff --git a/src/components/style_switcher/style_switcher.scss b/src/components/style_switcher/style_switcher.scss
index 135c113a..d2a40d13 100644
--- a/src/components/style_switcher/style_switcher.scss
+++ b/src/components/style_switcher/style_switcher.scss
@@ -1,5 +1,15 @@
@import '../../_variables.scss';
.style-switcher {
+ .theme-warning {
+ display: flex;
+ align-items: baseline;
+ margin-bottom: .5em;
+ .buttons {
+ .btn {
+ margin-bottom: .5em;
+ }
+ }
+ }
.preset-switcher {
margin-right: 1em;
}
@@ -15,26 +25,23 @@
&.disabled {
input, select {
- &:not(.exclude-disabled) {
- opacity: .5
- }
+ opacity: .5
}
}
+ .opt {
+ margin: .5em;
+ }
+
+ .color-input {
+ flex: 0 0 0;
+ }
+
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;
}
@@ -42,13 +49,6 @@
&[type=range] {
flex: 1;
min-width: 3em;
- }
-
- &[type=checkbox] + label {
- margin: 6px 0;
- }
-
- &:not([type=number]):not([type=text]) {
align-self: flex-start;
}
}
diff --git a/src/components/style_switcher/style_switcher.vue b/src/components/style_switcher/style_switcher.vue
index ad032041..62c8e634 100644
--- a/src/components/style_switcher/style_switcher.vue
+++ b/src/components/style_switcher/style_switcher.vue
@@ -2,7 +2,53 @@
<div class="style-switcher">
<div class="presets-container">
<div class="save-load">
- <export-import
+ <div
+ v-if="themeWarning"
+ class="theme-warning"
+ >
+ <div class="alert warning">
+ {{ themeWarningHelp }}
+ </div>
+ <div class="buttons">
+ <template v-if="themeWarning.type === 'snapshot_source_mismatch'">
+ <button
+ class="btn"
+ @click="forceLoad"
+ >
+ {{ $t('settings.style.switcher.use_source') }}
+ </button>
+ <button
+ class="btn"
+ @click="forceSnapshot"
+ >
+ {{ $t('settings.style.switcher.use_snapshot') }}
+ </button>
+ </template>
+ <template v-else-if="themeWarning.noActionsPossible">
+ <button
+ class="btn"
+ @click="dismissWarning"
+ >
+ {{ $t('general.dismiss') }}
+ </button>
+ </template>
+ <template v-else>
+ <button
+ class="btn"
+ @click="forceLoad"
+ >
+ {{ $t('settings.style.switcher.load_theme') }}
+ </button>
+ <button
+ class="btn"
+ @click="dismissWarning"
+ >
+ {{ $t('settings.style.switcher.keep_as_is') }}
+ </button>
+ </template>
+ </div>
+ </div>
+ <ExportImport
:export-object="exportedTheme"
:export-label="$t(&quot;settings.export_theme&quot;)"
:import-label="$t(&quot;settings.import_theme&quot;)"
@@ -27,8 +73,8 @@
:key="style.name"
:value="style"
:style="{
- backgroundColor: style[1] || style.theme.colors.bg,
- color: style[3] || style.theme.colors.text
+ backgroundColor: style[1] || (style.theme || style.source).colors.bg,
+ color: style[3] || (style.theme || style.source).colors.text
}"
>
{{ style[0] || style.name }}
@@ -38,7 +84,7 @@
</label>
</div>
</template>
- </export-import>
+ </ExportImport>
</div>
<div class="save-load-options">
<span class="keep-option">
@@ -70,9 +116,7 @@
</div>
</div>
- <div class="preview-container">
- <preview :style="previewRules" />
- </div>
+ <preview :style="previewRules" />
<keep-alive>
<tab-switcher key="style-tweak">
@@ -106,7 +150,7 @@
<OpacityInput
v-model="bgOpacityLocal"
name="bgOpacity"
- :fallback="previewTheme.opacity.bg || 1"
+ :fallback="previewTheme.opacity.bg"
/>
<ColorInput
v-model="textColorLocal"
@@ -115,9 +159,18 @@
/>
<ContrastRatio :contrast="previewContrast.bgText" />
<ColorInput
+ v-model="accentColorLocal"
+ name="accentColor"
+ :fallback="previewTheme.colors.link"
+ :label="$t('settings.accent')"
+ :show-optional-tickbox="typeof linkColorLocal !== 'undefined'"
+ />
+ <ColorInput
v-model="linkColorLocal"
name="linkColor"
+ :fallback="previewTheme.colors.accent"
:label="$t('settings.links')"
+ :show-optional-tickbox="typeof accentColorLocal !== 'undefined'"
/>
<ContrastRatio :contrast="previewContrast.bgLink" />
</div>
@@ -148,13 +201,13 @@
name="cRedColor"
:label="$t('settings.cRed')"
/>
- <ContrastRatio :contrast="previewContrast.bgRed" />
+ <ContrastRatio :contrast="previewContrast.bgCRed" />
<ColorInput
v-model="cBlueColorLocal"
name="cBlueColor"
:label="$t('settings.cBlue')"
/>
- <ContrastRatio :contrast="previewContrast.bgBlue" />
+ <ContrastRatio :contrast="previewContrast.bgCBlue" />
</div>
<div class="color-item">
<ColorInput
@@ -162,13 +215,13 @@
name="cGreenColor"
:label="$t('settings.cGreen')"
/>
- <ContrastRatio :contrast="previewContrast.bgGreen" />
+ <ContrastRatio :contrast="previewContrast.bgCGreen" />
<ColorInput
v-model="cOrangeColorLocal"
name="cOrangeColor"
:label="$t('settings.cOrange')"
/>
- <ContrastRatio :contrast="previewContrast.bgOrange" />
+ <ContrastRatio :contrast="previewContrast.bgCOrange" />
</div>
<p>{{ $t('settings.theme_help_v2_2') }}</p>
</div>
@@ -193,6 +246,14 @@
</button>
</div>
<div class="color-item">
+ <h4>{{ $t('settings.style.advanced_colors.post') }}</h4>
+ <ColorInput
+ v-model="postLinkColorLocal"
+ name="postLinkColor"
+ :fallback="previewTheme.colors.accent"
+ :label="$t('settings.links')"
+ />
+ <ContrastRatio :contrast="previewContrast.postLink" />
<h4>{{ $t('settings.style.advanced_colors.alert') }}</h4>
<ColorInput
v-model="alertErrorColorLocal"
@@ -200,14 +261,53 @@
:label="$t('settings.style.advanced_colors.alert_error')"
:fallback="previewTheme.colors.alertError"
/>
- <ContrastRatio :contrast="previewContrast.alertError" />
+ <ColorInput
+ v-model="alertErrorTextColorLocal"
+ name="alertErrorText"
+ :label="$t('settings.text')"
+ :fallback="previewTheme.colors.alertErrorText"
+ />
+ <ContrastRatio
+ :contrast="previewContrast.alertErrorText"
+ large="true"
+ />
<ColorInput
v-model="alertWarningColorLocal"
name="alertWarning"
:label="$t('settings.style.advanced_colors.alert_warning')"
:fallback="previewTheme.colors.alertWarning"
/>
- <ContrastRatio :contrast="previewContrast.alertWarning" />
+ <ColorInput
+ v-model="alertWarningTextColorLocal"
+ name="alertWarningText"
+ :label="$t('settings.text')"
+ :fallback="previewTheme.colors.alertWarningText"
+ />
+ <ContrastRatio
+ :contrast="previewContrast.alertWarningText"
+ large="true"
+ />
+ <ColorInput
+ v-model="alertNeutralColorLocal"
+ name="alertNeutral"
+ :label="$t('settings.style.advanced_colors.alert_neutral')"
+ :fallback="previewTheme.colors.alertNeutral"
+ />
+ <ColorInput
+ v-model="alertNeutralTextColorLocal"
+ name="alertNeutralText"
+ :label="$t('settings.text')"
+ :fallback="previewTheme.colors.alertNeutralText"
+ />
+ <ContrastRatio
+ :contrast="previewContrast.alertNeutralText"
+ large="true"
+ />
+ <OpacityInput
+ v-model="alertOpacityLocal"
+ name="alertOpacity"
+ :fallback="previewTheme.opacity.alert"
+ />
</div>
<div class="color-item">
<h4>{{ $t('settings.style.advanced_colors.badge') }}</h4>
@@ -217,19 +317,30 @@
:label="$t('settings.style.advanced_colors.badge_notification')"
:fallback="previewTheme.colors.badgeNotification"
/>
+ <ColorInput
+ v-model="badgeNotificationTextColorLocal"
+ name="badgeNotificationText"
+ :label="$t('settings.text')"
+ :fallback="previewTheme.colors.badgeNotificationText"
+ />
+ <ContrastRatio
+ :contrast="previewContrast.badgeNotificationText"
+ large="true"
+ />
</div>
<div class="color-item">
<h4>{{ $t('settings.style.advanced_colors.panel_header') }}</h4>
<ColorInput
v-model="panelColorLocal"
name="panelColor"
- :fallback="fgColorLocal"
+ :fallback="previewTheme.colors.panel"
:label="$t('settings.background')"
/>
<OpacityInput
v-model="panelOpacityLocal"
name="panelOpacity"
- :fallback="previewTheme.opacity.panel || 1"
+ :fallback="previewTheme.opacity.panel"
+ :disabled="panelColorLocal === 'transparent'"
/>
<ColorInput
v-model="panelTextColorLocal"
@@ -239,7 +350,7 @@
/>
<ContrastRatio
:contrast="previewContrast.panelText"
- large="1"
+ large="true"
/>
<ColorInput
v-model="panelLinkColorLocal"
@@ -249,7 +360,7 @@
/>
<ContrastRatio
:contrast="previewContrast.panelLink"
- large="1"
+ large="true"
/>
</div>
<div class="color-item">
@@ -257,7 +368,7 @@
<ColorInput
v-model="topBarColorLocal"
name="topBarColor"
- :fallback="fgColorLocal"
+ :fallback="previewTheme.colors.topBar"
:label="$t('settings.background')"
/>
<ColorInput
@@ -280,13 +391,14 @@
<ColorInput
v-model="inputColorLocal"
name="inputColor"
- :fallback="fgColorLocal"
+ :fallback="previewTheme.colors.input"
:label="$t('settings.background')"
/>
<OpacityInput
v-model="inputOpacityLocal"
name="inputOpacity"
- :fallback="previewTheme.opacity.input || 1"
+ :fallback="previewTheme.opacity.input"
+ :disabled="inputColorLocal === 'transparent'"
/>
<ColorInput
v-model="inputTextColorLocal"
@@ -301,13 +413,14 @@
<ColorInput
v-model="btnColorLocal"
name="btnColor"
- :fallback="fgColorLocal"
+ :fallback="previewTheme.colors.btn"
:label="$t('settings.background')"
/>
<OpacityInput
v-model="btnOpacityLocal"
name="btnOpacity"
- :fallback="previewTheme.opacity.btn || 1"
+ :fallback="previewTheme.opacity.btn"
+ :disabled="btnColorLocal === 'transparent'"
/>
<ColorInput
v-model="btnTextColorLocal"
@@ -316,6 +429,124 @@
:label="$t('settings.text')"
/>
<ContrastRatio :contrast="previewContrast.btnText" />
+ <ColorInput
+ v-model="btnPanelTextColorLocal"
+ name="btnPanelTextColor"
+ :fallback="previewTheme.colors.btnPanelText"
+ :label="$t('settings.style.advanced_colors.panel_header')"
+ />
+ <ContrastRatio :contrast="previewContrast.btnPanelText" />
+ <ColorInput
+ v-model="btnTopBarTextColorLocal"
+ name="btnTopBarTextColor"
+ :fallback="previewTheme.colors.btnTopBarText"
+ :label="$t('settings.style.advanced_colors.top_bar')"
+ />
+ <ContrastRatio :contrast="previewContrast.btnTopBarText" />
+ <h5>{{ $t('settings.style.advanced_colors.pressed') }}</h5>
+ <ColorInput
+ v-model="btnPressedColorLocal"
+ name="btnPressedColor"
+ :fallback="previewTheme.colors.btnPressed"
+ :label="$t('settings.background')"
+ />
+ <ColorInput
+ v-model="btnPressedTextColorLocal"
+ name="btnPressedTextColor"
+ :fallback="previewTheme.colors.btnPressedText"
+ :label="$t('settings.text')"
+ />
+ <ContrastRatio :contrast="previewContrast.btnPressedText" />
+ <ColorInput
+ v-model="btnPressedPanelTextColorLocal"
+ name="btnPressedPanelTextColor"
+ :fallback="previewTheme.colors.btnPressedPanelText"
+ :label="$t('settings.style.advanced_colors.panel_header')"
+ />
+ <ContrastRatio :contrast="previewContrast.btnPressedPanelText" />
+ <ColorInput
+ v-model="btnPressedTopBarTextColorLocal"
+ name="btnPressedTopBarTextColor"
+ :fallback="previewTheme.colors.btnPressedTopBarText"
+ :label="$t('settings.style.advanced_colors.top_bar')"
+ />
+ <ContrastRatio :contrast="previewContrast.btnPressedTopBarText" />
+ <h5>{{ $t('settings.style.advanced_colors.disabled') }}</h5>
+ <ColorInput
+ v-model="btnDisabledColorLocal"
+ name="btnDisabledColor"
+ :fallback="previewTheme.colors.btnDisabled"
+ :label="$t('settings.background')"
+ />
+ <ColorInput
+ v-model="btnDisabledTextColorLocal"
+ name="btnDisabledTextColor"
+ :fallback="previewTheme.colors.btnDisabledText"
+ :label="$t('settings.text')"
+ />
+ <ColorInput
+ v-model="btnDisabledPanelTextColorLocal"
+ name="btnDisabledPanelTextColor"
+ :fallback="previewTheme.colors.btnDisabledPanelText"
+ :label="$t('settings.style.advanced_colors.panel_header')"
+ />
+ <ColorInput
+ v-model="btnDisabledTopBarTextColorLocal"
+ name="btnDisabledTopBarTextColor"
+ :fallback="previewTheme.colors.btnDisabledTopBarText"
+ :label="$t('settings.style.advanced_colors.top_bar')"
+ />
+ <h5>{{ $t('settings.style.advanced_colors.toggled') }}</h5>
+ <ColorInput
+ v-model="btnToggledColorLocal"
+ name="btnToggledColor"
+ :fallback="previewTheme.colors.btnToggled"
+ :label="$t('settings.background')"
+ />
+ <ColorInput
+ v-model="btnToggledTextColorLocal"
+ name="btnToggledTextColor"
+ :fallback="previewTheme.colors.btnToggledText"
+ :label="$t('settings.text')"
+ />
+ <ContrastRatio :contrast="previewContrast.btnToggledText" />
+ <ColorInput
+ v-model="btnToggledPanelTextColorLocal"
+ name="btnToggledPanelTextColor"
+ :fallback="previewTheme.colors.btnToggledPanelText"
+ :label="$t('settings.style.advanced_colors.panel_header')"
+ />
+ <ContrastRatio :contrast="previewContrast.btnToggledPanelText" />
+ <ColorInput
+ v-model="btnToggledTopBarTextColorLocal"
+ name="btnToggledTopBarTextColor"
+ :fallback="previewTheme.colors.btnToggledTopBarText"
+ :label="$t('settings.style.advanced_colors.top_bar')"
+ />
+ <ContrastRatio :contrast="previewContrast.btnToggledTopBarText" />
+ </div>
+ <div class="color-item">
+ <h4>{{ $t('settings.style.advanced_colors.tabs') }}</h4>
+ <ColorInput
+ v-model="tabColorLocal"
+ name="tabColor"
+ :fallback="previewTheme.colors.tab"
+ :label="$t('settings.background')"
+ />
+ <ColorInput
+ v-model="tabTextColorLocal"
+ name="tabTextColor"
+ :fallback="previewTheme.colors.tabText"
+ :label="$t('settings.text')"
+ />
+ <ContrastRatio :contrast="previewContrast.tabText" />
+ <ColorInput
+ v-model="tabActiveTextColorLocal"
+ name="tabActiveTextColor"
+ :fallback="previewTheme.colors.tabActiveText"
+ :label="$t('settings.text')"
+ />
+ <ContrastRatio :contrast="previewContrast.tabActiveText" />
</div>
<div class="color-item">
<h4>{{ $t('settings.style.advanced_colors.borders') }}</h4>
@@ -328,7 +559,8 @@
<OpacityInput
v-model="borderOpacityLocal"
name="borderOpacity"
- :fallback="previewTheme.opacity.border || 1"
+ :fallback="previewTheme.opacity.border"
+ :disabled="borderColorLocal === 'transparent'"
/>
</div>
<div class="color-item">
@@ -336,7 +568,7 @@
<ColorInput
v-model="faintColorLocal"
name="faintColor"
- :fallback="previewTheme.colors.faint || 1"
+ :fallback="previewTheme.colors.faint"
:label="$t('settings.text')"
/>
<ColorInput
@@ -354,8 +586,145 @@
<OpacityInput
v-model="faintOpacityLocal"
name="faintOpacity"
- :fallback="previewTheme.opacity.faint || 0.5"
+ :fallback="previewTheme.opacity.faint"
+ />
+ </div>
+ <div class="color-item">
+ <h4>{{ $t('settings.style.advanced_colors.underlay') }}</h4>
+ <ColorInput
+ v-model="underlayColorLocal"
+ name="underlay"
+ :label="$t('settings.style.advanced_colors.underlay')"
+ :fallback="previewTheme.colors.underlay"
+ />
+ <OpacityInput
+ v-model="underlayOpacityLocal"
+ name="underlayOpacity"
+ :fallback="previewTheme.opacity.underlay"
+ :disabled="underlayOpacityLocal === 'transparent'"
+ />
+ </div>
+ <div class="color-item">
+ <h4>{{ $t('settings.style.advanced_colors.poll') }}</h4>
+ <ColorInput
+ v-model="pollColorLocal"
+ name="poll"
+ :label="$t('settings.background')"
+ :fallback="previewTheme.colors.poll"
+ />
+ <ColorInput
+ v-model="pollTextColorLocal"
+ name="pollText"
+ :label="$t('settings.text')"
+ :fallback="previewTheme.colors.pollText"
+ />
+ </div>
+ <div class="color-item">
+ <h4>{{ $t('settings.style.advanced_colors.icons') }}</h4>
+ <ColorInput
+ v-model="iconColorLocal"
+ name="icon"
+ :label="$t('settings.style.advanced_colors.icons')"
+ :fallback="previewTheme.colors.icon"
+ />
+ </div>
+ <div class="color-item">
+ <h4>{{ $t('settings.style.advanced_colors.highlight') }}</h4>
+ <ColorInput
+ v-model="highlightColorLocal"
+ name="highlight"
+ :label="$t('settings.background')"
+ :fallback="previewTheme.colors.highlight"
+ />
+ <ColorInput
+ v-model="highlightTextColorLocal"
+ name="highlightText"
+ :label="$t('settings.text')"
+ :fallback="previewTheme.colors.highlightText"
+ />
+ <ContrastRatio :contrast="previewContrast.highlightText" />
+ <ColorInput
+ v-model="highlightLinkColorLocal"
+ name="highlightLink"
+ :label="$t('settings.links')"
+ :fallback="previewTheme.colors.highlightLink"
+ />
+ <ContrastRatio :contrast="previewContrast.highlightLink" />
+ </div>
+ <div class="color-item">
+ <h4>{{ $t('settings.style.advanced_colors.popover') }}</h4>
+ <ColorInput
+ v-model="popoverColorLocal"
+ name="popover"
+ :label="$t('settings.background')"
+ :fallback="previewTheme.colors.popover"
+ />
+ <OpacityInput
+ v-model="popoverOpacityLocal"
+ name="popoverOpacity"
+ :fallback="previewTheme.opacity.popover"
+ :disabled="popoverOpacityLocal === 'transparent'"
+ />
+ <ColorInput
+ v-model="popoverTextColorLocal"
+ name="popoverText"
+ :label="$t('settings.text')"
+ :fallback="previewTheme.colors.popoverText"
+ />
+ <ContrastRatio :contrast="previewContrast.popoverText" />
+ <ColorInput
+ v-model="popoverLinkColorLocal"
+ name="popoverLink"
+ :label="$t('settings.links')"
+ :fallback="previewTheme.colors.popoverLink"
+ />
+ <ContrastRatio :contrast="previewContrast.popoverLink" />
+ </div>
+ <div class="color-item">
+ <h4>{{ $t('settings.style.advanced_colors.selectedPost') }}</h4>
+ <ColorInput
+ v-model="selectedPostColorLocal"
+ name="selectedPost"
+ :label="$t('settings.background')"
+ :fallback="previewTheme.colors.selectedPost"
+ />
+ <ColorInput
+ v-model="selectedPostTextColorLocal"
+ name="selectedPostText"
+ :label="$t('settings.text')"
+ :fallback="previewTheme.colors.selectedPostText"
+ />
+ <ContrastRatio :contrast="previewContrast.selectedPostText" />
+ <ColorInput
+ v-model="selectedPostLinkColorLocal"
+ name="selectedPostLink"
+ :label="$t('settings.links')"
+ :fallback="previewTheme.colors.selectedPostLink"
+ />
+ <ContrastRatio :contrast="previewContrast.selectedPostLink" />
+ </div>
+ <div class="color-item">
+ <h4>{{ $t('settings.style.advanced_colors.selectedMenu') }}</h4>
+ <ColorInput
+ v-model="selectedMenuColorLocal"
+ name="selectedMenu"
+ :label="$t('settings.background')"
+ :fallback="previewTheme.colors.selectedMenu"
+ />
+ <ColorInput
+ v-model="selectedMenuTextColorLocal"
+ name="selectedMenuText"
+ :label="$t('settings.text')"
+ :fallback="previewTheme.colors.selectedMenuText"
+ />
+ <ContrastRatio :contrast="previewContrast.selectedMenuText" />
+ <ColorInput
+ v-model="selectedMenuLinkColorLocal"
+ name="selectedMenuLink"
+ :label="$t('settings.links')"
+ :fallback="previewTheme.colors.selectedMenuLink"
/>
+ <ContrastRatio :contrast="previewContrast.selectedMenuLink" />
</div>
</div>
@@ -491,7 +860,7 @@
{{ $t('settings.style.switcher.clear_all') }}
</button>
</div>
- <shadow-control
+ <ShadowControl
v-model="currentShadow"
:ready="!!currentShadowFallback"
:fallback="currentShadowFallback"
diff --git a/src/components/tab_switcher/tab_switcher.scss b/src/components/tab_switcher/tab_switcher.scss
index 3e5eacd5..df585faa 100644
--- a/src/components/tab_switcher/tab_switcher.scss
+++ b/src/components/tab_switcher/tab_switcher.scss
@@ -52,6 +52,11 @@
margin-bottom: 6px - 99px;
white-space: nowrap;
+ color: $fallback--text;
+ color: var(--tabText, $fallback--text);
+ background-color: $fallback--fg;
+ background-color: var(--tab, $fallback--fg);
+
&:not(.active) {
z-index: 4;
@@ -63,6 +68,8 @@
&.active {
background: transparent;
z-index: 5;
+ color: $fallback--text;
+ color: var(--tabActiveText, $fallback--text);
}
img {
diff --git a/src/components/user_card/user_card.js b/src/components/user_card/user_card.js
index 2f649910..1cdbd3fa 100644
--- a/src/components/user_card/user_card.js
+++ b/src/components/user_card/user_card.js
@@ -4,7 +4,6 @@ import ProgressButton from '../progress_button/progress_button.vue'
import FollowButton from '../follow_button/follow_button.vue'
import ModerationTools from '../moderation_tools/moderation_tools.vue'
import AccountActions from '../account_actions/account_actions.vue'
-import { hex2rgb } from '../../services/color_convert/color_convert.js'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
import { mapGetters } from 'vuex'
@@ -30,21 +29,11 @@ export default {
}]
},
style () {
- const color = this.$store.getters.mergedConfig.customTheme.colors
- ? this.$store.getters.mergedConfig.customTheme.colors.bg // v2
- : this.$store.getters.mergedConfig.colors.bg // v1
-
- if (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)`
-
- 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})`,
- `url(${this.user.cover_photo})`
- ].join(', ')
- }
+ return {
+ backgroundImage: [
+ `linear-gradient(to bottom, var(--profileTint), var(--profileTint))`,
+ `url(${this.user.cover_photo})`
+ ].join(', ')
}
},
isOtherUser () {
diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue
index 93d55fff..4ee040e8 100644
--- a/src/components/user_card/user_card.vue
+++ b/src/components/user_card/user_card.vue
@@ -151,7 +151,7 @@
</ProgressButton>
<ProgressButton
v-else
- class="btn btn-default pressed"
+ class="btn btn-default toggled"
:click="unsubscribeUser"
:title="$t('user_card.unsubscribe')"
>
@@ -162,7 +162,7 @@
<div>
<button
v-if="user.muted"
- class="btn btn-default btn-block pressed"
+ class="btn btn-default btn-block toggled"
@click="unmuteUser"
>
{{ $t('user_card.muted') }}
@@ -286,6 +286,7 @@
mask-size: 100% 60%;
border-top-left-radius: calc(var(--panelRadius) - 1px);
border-top-right-radius: calc(var(--panelRadius) - 1px);
+ background-color: var(--profileBg);
&.hide-bio {
mask-size: 100% 40px;
@@ -299,6 +300,11 @@
&-bio {
text-align: center;
+ a {
+ color: $fallback--link;
+ color: var(--postLink, $fallback--link);
+ }
+
img {
object-fit: contain;
vertical-align: middle;
@@ -460,14 +466,13 @@
color: var(--text, $fallback--text);
}
- // TODO use proper colors
.staff {
flex: none;
text-transform: capitalize;
color: $fallback--text;
- color: var(--btnText, $fallback--text);
+ color: var(--alertNeutralText, $fallback--text);
background-color: $fallback--fg;
- background-color: var(--btn, $fallback--fg);
+ background-color: var(--alertNeutral, $fallback--fg);
}
}
@@ -538,12 +543,6 @@
button {
margin: 0;
-
- &.pressed {
- // TODO: This should be themed.
- border-bottom-color: rgba(255, 255, 255, 0.2);
- border-top-color: rgba(0, 0, 0, 0.2);
- }
}
}
}
diff --git a/src/components/user_settings/user_settings.js b/src/components/user_settings/user_settings.js
index 38373056..eca6f9b1 100644
--- a/src/components/user_settings/user_settings.js
+++ b/src/components/user_settings/user_settings.js
@@ -55,6 +55,7 @@ const UserSettings = {
showRole: this.$store.state.users.currentUser.show_role,
role: this.$store.state.users.currentUser.role,
discoverable: this.$store.state.users.currentUser.discoverable,
+ allowFollowingMove: this.$store.state.users.currentUser.allow_following_move,
pickAvatarBtnVisible: true,
bannerUploading: false,
backgroundUploading: false,
@@ -162,6 +163,7 @@ const UserSettings = {
hide_follows: this.hideFollows,
hide_followers: this.hideFollowers,
discoverable: this.discoverable,
+ allow_following_move: this.allowFollowingMove,
hide_follows_count: this.hideFollowsCount,
hide_followers_count: this.hideFollowersCount,
show_role: this.showRole
diff --git a/src/components/user_settings/user_settings.vue b/src/components/user_settings/user_settings.vue
index 2222c293..8b2336b4 100644
--- a/src/components/user_settings/user_settings.vue
+++ b/src/components/user_settings/user_settings.vue
@@ -90,9 +90,7 @@
</Checkbox>
</p>
<p>
- <Checkbox
- v-model="hideFollowers"
- >
+ <Checkbox v-model="hideFollowers">
{{ $t('settings.hide_followers_description') }}
</Checkbox>
</p>
@@ -104,6 +102,11 @@
{{ $t('settings.hide_followers_count_description') }}
</Checkbox>
</p>
+ <p>
+ <Checkbox v-model="allowFollowingMove">
+ {{ $t('settings.allow_following_move') }}
+ </Checkbox>
+ </p>
<p v-if="role === 'admin' || role === 'moderator'">
<Checkbox v-model="showRole">
<template v-if="role === 'admin'">