Merge branch 'develop' into 'tusooa/save-draft'

# Conflicts:
#   src/boot/routes.js
#   src/i18n/en.json
#   src/main.js
#   src/modules/config.js
#   src/modules/instance.js
This commit is contained in:
HJ 2024-12-26 23:51:54 +00:00
commit 3cda070507
282 changed files with 8443 additions and 1913 deletions

View file

@ -86,6 +86,7 @@
<i18n-t
keypath="user_card.block_confirm"
tag="span"
scope="global"
>
<template #user>
<span
@ -107,6 +108,7 @@
<i18n-t
keypath="user_card.remove_follower_confirm"
tag="span"
scope="global"
>
<template #user>
<span

View file

@ -14,6 +14,10 @@ export default {
warning: '.warning',
success: '.success'
},
editor: {
border: 1,
aspect: '3 / 1'
},
defaultRules: [
{
directives: {
@ -27,7 +31,9 @@ export default {
component: 'Alert'
},
component: 'Border',
textColor: '--parent'
directives: {
textColor: '--parent'
}
},
{
variant: 'error',

View file

@ -34,8 +34,9 @@
id="announcement-all-day"
v-model="announcement.allDay"
:disabled="disabled"
/>
<label for="announcement-all-day">{{ $t('announcements.all_day_prompt') }}</label>
>
{{ $t('announcements.all_day_prompt') }}
</Checkbox>
</span>
</div>
</template>

View file

@ -1,9 +1,9 @@
<template>
<div class="panel panel-default announcements-page">
<div class="panel-heading">
<span>
<h1 class="title">
{{ $t('announcements.page_header') }}
</span>
</h1>
</div>
<div class="panel-body">
<section

View file

@ -1,6 +1,7 @@
export default {
name: 'Attachment',
selector: '.Attachment',
notEditable: true,
validInnerComponents: [
'Border',
'ButtonUnstyled',

View file

@ -48,7 +48,7 @@
flex: 1 0;
margin: 0;
--emoji-size: 14px;
--emoji-size: 1em;
&-collapsed-content {
margin-left: 0.7em;

View file

@ -0,0 +1,22 @@
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faEllipsisH
} from '@fortawesome/free-solid-svg-icons'
library.add(
faEllipsisH
)
const BookmarkFolderCard = {
props: [
'folder',
'allBookmarks'
],
computed: {
firstLetter () {
return this.folder ? this.folder.name[0] : null
}
}
}
export default BookmarkFolderCard

View file

@ -0,0 +1,111 @@
<template>
<div
v-if="allBookmarks"
class="bookmark-folder-card"
>
<router-link
:to="{ name: 'bookmarks' }"
class="bookmark-folder-name"
>
<span class="icon">
<FAIcon
fixed-width
class="fa-scale-110 menu-icon"
icon="bookmark"
/>
</span>{{ $t('nav.all_bookmarks') }}
</router-link>
</div>
<div
v-else
class="bookmark-folder-card"
>
<router-link
:to="{ name: 'bookmark-folder', params: { id: folder.id } }"
class="bookmark-folder-name"
>
<img
v-if="folder.emoji_url"
class="iconEmoji iconEmoji-image"
:src="folder.emoji_url"
:alt="folder.emoji"
:title="folder.emoji"
>
<span
v-else-if="folder.emoji"
class="iconEmoji"
>
<span>
{{ folder.emoji }}
</span>
</span>
<span
v-else-if="firstLetter"
class="icon iconLetter fa-scale-110"
>{{ firstLetter }}</span>{{ folder.name }}
</router-link>
<router-link
:to="{ name: 'bookmark-folder-edit', params: { id: folder.id } }"
class="button-folder-edit"
>
<FAIcon
class="fa-scale-110 fa-old-padding"
icon="ellipsis-h"
/>
</router-link>
</div>
</template>
<script src="./bookmark_folder_card.js"></script>
<style lang="scss">
.bookmark-folder-card {
display: flex;
align-items: center;
}
a.bookmark-folder-name {
display: flex;
align-items: center;
flex-grow: 1;
.icon,
.iconLetter,
.iconEmoji {
display: inline-block;
height: 2.5rem;
width: 2.5rem;
margin-right: 0.5rem;
}
.icon,
.iconLetter {
font-size: 1.5rem;
line-height: 2.5rem;
text-align: center;
}
.iconEmoji {
text-align: center;
object-fit: contain;
vertical-align: middle;
> span {
font-size: 1.5rem;
line-height: 2.5rem;
}
}
img.iconEmoji {
padding: 0.25em;
box-sizing: border-box;
}
}
.bookmark-folder-name,
.button-folder-edit {
margin: 0;
padding: 1em;
color: var(--link);
}
</style>

View file

@ -0,0 +1,80 @@
import EmojiPicker from '../emoji_picker/emoji_picker.vue'
import apiService from '../../services/api/api.service'
const BookmarkFolderEdit = {
data () {
return {
name: '',
nameDraft: '',
emoji: '',
emojiUrl: null,
emojiDraft: '',
emojiUrlDraft: null,
emojiPickerExpanded: false,
reallyDelete: false
}
},
components: {
EmojiPicker
},
created () {
if (!this.id) return
const credentials = this.$store.state.users.currentUser.credentials
apiService.fetchBookmarkFolders({ credentials })
.then((folders) => {
const folder = folders.find(folder => folder.id === this.id)
if (!folder) return
this.nameDraft = this.name = folder.name
this.emojiDraft = this.emoji = folder.emoji
this.emojiUrlDraft = this.emojiUrl = folder.emoji_url
})
},
computed: {
id () {
return this.$route.params.id
}
},
methods: {
selectEmoji (event) {
this.emojiDraft = event.insertion
this.emojiUrlDraft = event.insertionUrl
},
showEmojiPicker () {
if (!this.emojiPickerExpanded) {
this.$refs.picker.showPicker()
}
},
onShowPicker () {
this.emojiPickerExpanded = true
},
onClosePicker () {
this.emojiPickerExpanded = false
},
updateFolder () {
this.$store.dispatch('setBookmarkFolder', { folderId: this.id, name: this.nameDraft, emoji: this.emojiDraft })
.then(() => {
this.$router.push({ name: 'bookmark-folders' })
})
},
createFolder () {
this.$store.dispatch('createBookmarkFolder', { name: this.nameDraft, emoji: this.emojiDraft })
.then(() => {
this.$router.push({ name: 'bookmark-folders' })
})
.catch((e) => {
this.$store.dispatch('pushGlobalNotice', {
messageKey: 'bookmark_folders.error',
messageArgs: [e.message],
level: 'error'
})
})
},
deleteFolder () {
this.$store.dispatch('deleteBookmarkFolder', { folderId: this.id })
this.$router.push({ name: 'bookmark-folders' })
}
}
}
export default BookmarkFolderEdit

View file

@ -0,0 +1,200 @@
<template>
<div class="panel-default panel BookmarkFolderEdit">
<div
ref="header"
class="panel-heading folder-edit-heading"
>
<button
class="button-unstyled go-back-button"
@click="$router.back"
>
<FAIcon
size="lg"
icon="chevron-left"
/>
</button>
<h1 class="title">
<i18n-t
v-if="id"
keypath="bookmark_folders.editing_folder"
scope="global"
>
<template #folderName>
{{ name }}
</template>
</i18n-t>
<i18n-t
v-else
keypath="bookmark_folders.creating_folder"
scope="global"
/>
</h1>
</div>
<div class="panel-body">
<div class="input-wrap">
<label for="folder-edit-title">{{ $t('bookmark_folders.emoji') }}</label>
<button
class="input input-emoji"
:title="$t('bookmark_folder.emoji_pick')"
@click="showEmojiPicker"
>
<img
v-if="emojiUrlDraft"
class="iconEmoji iconEmoji-image"
:src="emojiUrlDraft"
:alt="emojiDraft"
:title="emojiDraft"
>
<span
v-else-if="emojiDraft"
class="iconEmoji"
>
<span>
{{ emojiDraft }}
</span>
</span>
</button>
<EmojiPicker
ref="picker"
class="emoji-picker-panel"
@emoji="selectEmoji"
@show="onShowPicker"
@close="onClosePicker"
/>
</div>
<div class="input-wrap">
<label for="folder-edit-title">{{ $t('bookmark_folders.name') }}</label>
<input
id="folder-edit-title"
ref="name"
v-model="nameDraft"
class="input"
>
</div>
</div>
<div class="panel-footer">
<span class="spacer" />
<button
v-if="!id"
class="btn button-default footer-button"
@click="createFolder"
>
{{ $t('bookmark_folders.create') }}
</button>
<button
v-else-if="!reallyDelete"
class="btn button-default footer-button"
@click="reallyDelete = true"
>
{{ $t('bookmark_folders.delete') }}
</button>
<template v-else>
{{ $t('bookmark_folders.really_delete') }}
<button
class="btn button-default footer-button"
@click="deleteFolder"
>
{{ $t('general.yes') }}
</button>
<button
class="btn button-default footer-button"
@click="reallyDelete = false"
>
{{ $t('general.no') }}
</button>
</template>
<div
v-if="id && !reallyDelete"
>
<button
class="btn button-default follow-button"
@click="updateFolder"
>
{{ $t('bookmark_folders.update_folder') }}
</button>
</div>
</div>
</div>
</template>
<script src="./bookmark_folder_edit.js"></script>
<style lang="scss">
.BookmarkFolderEdit {
--panel-body-padding: 0.5em;
overflow: hidden;
display: flex;
flex-direction: column;
.folder-edit-heading {
grid-template-columns: auto minmax(50%, 1fr);
}
.panel-body {
display: flex;
gap: 0.5em;
}
.emoji-picker-panel {
position: absolute;
z-index: 20;
margin-top: 2px;
&.hide {
display: none;
}
}
.input-emoji {
height: 2.5em;
width: 2.5em;
padding: 0;
.iconEmoji {
display: inline-block;
text-align: center;
object-fit: contain;
vertical-align: middle;
height: 2.5em;
width: 2.5em;
> span {
font-size: 1.5rem;
line-height: 2.5rem;
}
}
img.iconEmoji {
padding: 0.25em;
box-sizing: border-box;
}
}
.input-wrap {
display: flex;
flex-direction: column;
gap: 0.5em;
}
.go-back-button {
text-align: center;
line-height: 1;
height: 100%;
align-self: start;
width: var(--__panel-heading-height-inner);
}
.btn {
margin: 0 0.5em;
}
.panel-footer {
grid-template-columns: minmax(10%, 1fr);
.footer-button {
min-width: 9em;
}
}
}
</style>

View file

@ -0,0 +1,27 @@
import BookmarkFolderCard from '../bookmark_folder_card/bookmark_folder_card.vue'
const BookmarkFolders = {
data () {
return {
isNew: false
}
},
components: {
BookmarkFolderCard
},
computed: {
bookmarkFolders () {
return this.$store.state.bookmarkFolders.allFolders
}
},
methods: {
cancelNewFolder () {
this.isNew = false
},
newFolder () {
this.isNew = true
}
}
}
export default BookmarkFolders

View file

@ -0,0 +1,37 @@
<template>
<div class="Bookmark-folders panel panel-default">
<div class="panel-heading">
<h1 class="title">
{{ $t('nav.bookmark_folders') }}
</h1>
<router-link
:to="{ name: 'bookmark-folder-new' }"
class="button-default btn new-folder-button"
>
{{ $t("bookmark_folders.new") }}
</router-link>
</div>
<div class="panel-body">
<BookmarkFolderCard
:all-bookmarks="true"
class="list-item"
/>
<BookmarkFolderCard
v-for="folder in bookmarkFolders.slice().reverse()"
:key="folder"
:folder="folder"
class="list-item"
/>
</div>
</div>
</template>
<script src="./bookmark_folders.js"></script>
<style lang="scss">
.Bookmark-folders {
.new-folder-button {
padding: 0 0.5em;
}
}
</style>

View file

@ -0,0 +1,16 @@
import { mapState } from 'vuex'
import NavigationEntry from 'src/components/navigation/navigation_entry.vue'
import { getBookmarkFolderEntries } from 'src/components/navigation/filter.js'
export const BookmarkFoldersMenuContent = {
components: {
NavigationEntry
},
computed: {
...mapState({
folders: getBookmarkFolderEntries
})
}
}
export default BookmarkFoldersMenuContent

View file

@ -0,0 +1,19 @@
<template>
<ul>
<NavigationEntry
:item="{
name: 'bookmarks',
routeObject: { name: 'bookmarks' },
label: 'nav.all_bookmarks',
icon: 'bookmark'
}"
/>
<NavigationEntry
v-for="item in folders"
:key="item.id"
:item="item"
/>
</ul>
</template>
<script src="./bookmark_folders_menu_content.js"></script>

View file

@ -1,16 +1,31 @@
import Timeline from '../timeline/timeline.vue'
const Bookmarks = {
computed: {
timeline () {
return this.$store.state.statuses.timelines.bookmarks
}
created () {
this.$store.commit('clearTimeline', { timeline: 'bookmarks' })
this.$store.dispatch('startFetchingTimeline', { timeline: 'bookmarks', bookmarkFolderId: this.folderId || null })
},
components: {
Timeline
},
computed: {
folderId () {
return this.$route.params.id
},
timeline () {
return this.$store.state.statuses.timelines.bookmarks
}
},
watch: {
folderId () {
this.$store.commit('clearTimeline', { timeline: 'bookmarks' })
this.$store.dispatch('stopFetchingTimeline', 'bookmarks')
this.$store.dispatch('startFetchingTimeline', { timeline: 'bookmarks', bookmarkFolderId: this.folderId || null })
}
},
unmounted () {
this.$store.commit('clearTimeline', { timeline: 'bookmarks' })
this.$store.dispatch('stopFetchingTimeline', 'bookmarks')
}
}

View file

@ -3,6 +3,7 @@
:title="$t('nav.bookmarks')"
:timeline="timeline"
:timeline-name="'bookmarks'"
:bookmark-folder-id="folderId"
/>
</template>

View file

@ -5,7 +5,7 @@ export default {
defaultRules: [
{
directives: {
textColor: '$mod(--parent, 10)',
textColor: '$mod(--parent 10)',
textAuto: 'no-auto'
}
}

View file

@ -9,9 +9,9 @@ export default {
// However, cascading still works, so resulting state will be result of merging of all relevant states/variants
// normal: '' // normal state is implicitly added, it is always included
toggled: '.toggled',
pressed: ':active',
focused: ':focus-visible',
pressed: ':focus:active',
hover: ':hover:not(:disabled)',
focused: ':focus-within',
disabled: ':disabled'
},
// Variants are mutually exclusive, each component implicitly has "normal" variant, and all other variants inherit from it.
@ -22,6 +22,9 @@ export default {
// Overall the compuation difficulty is N*((1/6)M^3+M) where M is number of distinct states and N is number of variants.
// This (currently) is further multipled by number of places where component can exist.
},
editor: {
aspect: '2 / 1'
},
// This lists all other components that can possibly exist within one. Recursion is currently not supported (and probably won't be supported ever).
validInnerComponents: [
'Text',
@ -32,10 +35,11 @@ export default {
{
component: 'Root',
directives: {
'--defaultButtonHoverGlow': 'shadow | 0 0 4 --text',
'--defaultButtonShadow': 'shadow | 0 0 2 #000000',
'--defaultButtonBevel': 'shadow | $borderSide(#FFFFFF, top, 0.2) | $borderSide(#000000, bottom, 0.2)',
'--pressedButtonBevel': 'shadow | $borderSide(#FFFFFF, bottom, 0.2)| $borderSide(#000000, top, 0.2)'
'--buttonDefaultHoverGlow': 'shadow | 0 0 4 --text / 0.5',
'--buttonDefaultFocusGlow': 'shadow | 0 0 4 4 --link / 0.5',
'--buttonDefaultShadow': 'shadow | 0 0 2 #000000',
'--buttonDefaultBevel': 'shadow | $borderSide(#FFFFFF top 0.2 2), $borderSide(#000000 bottom 0.2 2)',
'--buttonPressedBevel': 'shadow | $borderSide(#FFFFFF bottom 0.2 2), $borderSide(#000000 top 0.2 2)'
}
},
{
@ -43,47 +47,60 @@ export default {
// like within it
directives: {
background: '--fg',
shadow: ['--defaultButtonShadow', '--defaultButtonBevel'],
shadow: ['--buttonDefaultShadow', '--buttonDefaultBevel'],
roundness: 3
}
},
{
state: ['hover'],
directives: {
shadow: ['--defaultButtonHoverGlow', '--defaultButtonBevel']
shadow: ['--buttonDefaultHoverGlow', '--buttonDefaultBevel']
}
},
{
state: ['focused'],
directives: {
shadow: ['--buttonDefaultFocusGlow', '--buttonDefaultBevel']
}
},
{
state: ['pressed'],
directives: {
shadow: ['--defaultButtonShadow', '--pressedButtonBevel']
shadow: ['--buttonDefaultShadow', '--buttonPressedBevel']
}
},
{
state: ['hover', 'pressed'],
state: ['pressed', 'hover'],
directives: {
shadow: ['--defaultButtonHoverGlow', '--pressedButtonBevel']
shadow: ['--buttonPressedBevel', '--buttonDefaultHoverGlow']
}
},
{
state: ['toggled'],
directives: {
background: '--inheritedBackground,-14.2',
shadow: ['--defaultButtonShadow', '--pressedButtonBevel']
shadow: ['--buttonDefaultShadow', '--buttonPressedBevel']
}
},
{
state: ['toggled', 'hover'],
directives: {
background: '--inheritedBackground,-14.2',
shadow: ['--defaultButtonHoverGlow', '--pressedButtonBevel']
shadow: ['--buttonDefaultHoverGlow', '--buttonPressedBevel']
}
},
{
state: ['toggled', 'disabled'],
directives: {
background: '$blend(--inheritedBackground 0.25 --parent)',
shadow: ['--buttonPressedBevel']
}
},
{
state: ['disabled'],
directives: {
background: '$blend(--inheritedBackground, 0.25, --parent)',
shadow: ['--defaultButtonBevel']
background: '$blend(--inheritedBackground 0.25 --parent)',
shadow: ['--buttonDefaultBevel']
}
},
{
@ -96,6 +113,17 @@ export default {
textOpacity: 0.25,
textOpacityMode: 'blend'
}
},
{
component: 'Icon',
parent: {
component: 'Button',
state: ['disabled']
},
directives: {
textOpacity: 0.25,
textOpacityMode: 'blend'
}
}
]
}

View file

@ -1,6 +1,7 @@
export default {
name: 'ButtonUnstyled',
selector: '.button-unstyled',
notEditable: true,
states: {
toggled: '.toggled',
disabled: ':disabled',

View file

@ -7,9 +7,9 @@
class="chat-list panel panel-default"
>
<div class="panel-heading -sticky">
<span class="title">
<h1 class="title">
{{ $t("chats.chats") }}
</span>
</h1>
<button
class="button-default"
@click="newChat"

View file

@ -32,7 +32,7 @@
text-overflow: ellipsis;
white-space: nowrap;
--emoji-size: 14px;
--emoji-size: 1em;
.username {
max-width: 100%;

View file

@ -3,6 +3,13 @@
class="checkbox"
:class="{ disabled, indeterminate, 'indeterminate-fix': indeterminateTransitionFix }"
>
<span
v-if="!!$slots.before"
class="label -before"
:class="{ faint: disabled }"
>
<slot name="before" />
</span>
<input
type="checkbox"
class="visible-for-screenreader-only"
@ -14,11 +21,13 @@
<i
class="input -checkbox checkbox-indicator"
:aria-hidden="true"
:class="{ disabled }"
@transitionend.capture="onTransitionEnd"
/>
<span
v-if="!!$slots.default"
class="label"
class="label -after"
:class="{ faint: disabled }"
>
<slot />
</span>
@ -61,21 +70,26 @@ export default {
display: inline-block;
min-height: 1.2em;
&-indicator,
& .label {
vertical-align: middle;
}
& > &-indicator {
/* Reset .input stuff */
padding: 0;
margin: 0;
position: relative;
line-height: inherit;
display: inline;
padding-left: 1.2em;
display: inline-block;
width: 1.2em;
height: 1.2em;
box-shadow: none;
}
&-indicator::before {
position: absolute;
right: 0;
top: 0;
inset: 0;
display: block;
content: "✓";
transition: color 200ms;
@ -93,14 +107,9 @@ export default {
box-sizing: border-box;
}
&.disabled {
.checkbox-indicator::before,
.label {
opacity: 0.5;
}
.label {
color: var(--text);
.disabled {
.checkbox-indicator::before {
background-color: var(--background);
}
}
@ -121,8 +130,14 @@ export default {
}
}
& > span {
margin-left: 0.5em;
& > .label {
&.-after {
margin-left: 0.5em;
}
&.-before {
margin-right: 0.5em;
}
}
}
</style>

View file

@ -1,12 +1,19 @@
.color-input {
display: inline-flex;
.label {
flex: 1 1 auto;
}
.opt {
margin-right: 0.5em;
}
&-field.input {
display: inline-flex;
flex: 0 0 0;
max-width: 9em;
align-items: stretch;
padding: 0.2em 8px;
input {
color: var(--text);
@ -25,6 +32,7 @@
.nativeColor {
cursor: pointer;
flex: 0 0 auto;
padding: 0;
input {
appearance: none;
@ -41,10 +49,10 @@
.invalidIndicator,
.transparentIndicator {
flex: 0 0 2em;
margin: 0 0.5em;
margin: 0.2em 0.5em;
min-width: 2em;
align-self: stretch;
min-height: 1.5em;
min-height: 1.1em;
border-radius: var(--roundness);
}
@ -81,9 +89,17 @@
border-bottom-right-radius: var(--roundness);
}
}
}
.label {
flex: 1 1 auto;
&.disabled,
&:disabled {
.nativeColor input,
.computedIndicator,
.validIndicator,
.invalidIndicator,
.transparentIndicator {
/* stylelint-disable-next-line declaration-no-important */
opacity: 0.25 !important;
}
}
}
}

View file

@ -6,24 +6,29 @@
<label
:for="name"
class="label"
:class="{ faint: !present || disabled }"
>
{{ label }}
</label>
<Checkbox
v-if="typeof fallback !== 'undefined' && showOptionalTickbox"
v-if="typeof fallback !== 'undefined' && showOptionalCheckbox && !hideOptionalCheckbox"
:model-value="present"
:disabled="disabled"
class="opt"
@update:modelValue="$emit('update:modelValue', typeof modelValue === 'undefined' ? fallback : undefined)"
@update:modelValue="updateValue(typeof modelValue === 'undefined' ? fallback : undefined)"
/>
<div class="input color-input-field">
<div
class="input color-input-field"
:class="{ disabled: !present || disabled }"
>
<input
:id="name + '-t'"
class="textColor unstyled"
:class="{ disabled: !present || disabled }"
type="text"
:value="modelValue || fallback"
:disabled="!present || disabled"
@input="$emit('update:modelValue', $event.target.value)"
@input="updateValue($event.target.value)"
>
<div
v-if="validColor"
@ -51,7 +56,8 @@
type="color"
:value="modelValue || fallback"
:disabled="!present || disabled"
@input="$emit('update:modelValue', $event.target.value)"
:class="{ disabled: !present || disabled }"
@input="updateValue($event.target.value)"
>
</label>
</div>
@ -60,6 +66,7 @@
<script>
import Checkbox from '../checkbox/checkbox.vue'
import { hex2rgb } from '../../services/color_convert/color_convert.js'
import { throttle } from 'lodash'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
@ -105,10 +112,16 @@ export default {
default: false
},
// Show "optional" tickbox, for when value might become mandatory
showOptionalTickbox: {
showOptionalCheckbox: {
required: false,
type: Boolean,
default: true
},
// Force "optional" tickbox to hide
hideOptionalCheckbox: {
required: false,
type: Boolean,
default: false
}
},
emits: ['update:modelValue'],
@ -123,8 +136,13 @@ export default {
return this.modelValue === 'transparent'
},
computedColor () {
return this.modelValue && this.modelValue.startsWith('--')
return this.modelValue && (this.modelValue.startsWith('--') || this.modelValue.startsWith('$'))
}
},
methods: {
updateValue: throttle(function (value) {
this.$emit('update:modelValue', value)
}, 100)
}
}
</script>

View file

@ -0,0 +1,323 @@
<template>
<div
class="ComponentPreview"
:class="{ '-shadow-controls': shadowControl }"
>
<!-- eslint-disable vue/no-v-html vue/no-v-text-v-html-on-component -->
<component
:is="'style'"
v-html="previewCss"
/>
<!-- eslint-enable vue/no-v-html vue/no-v-text-v-html-on-component -->
<label
v-show="shadowControl"
role="heading"
class="header"
:class="{ faint: disabled }"
>
{{ $t('settings.style.shadows.offset') }}
</label>
<label
v-show="shadowControl && !hideControls"
class="x-shift-number"
>
{{ $t('settings.style.shadows.offset-x') }}
<input
:value="shadow?.x"
:disabled="disabled"
:class="{ disabled }"
class="input input-number"
type="number"
@input="e => updateProperty('x', e.target.value)"
>
</label>
<label
v-show="shadowControl && !hideControls"
class="y-shift-number"
>
{{ $t('settings.style.shadows.offset-y') }}
<input
:value="shadow?.y"
:disabled="disabled"
:class="{ disabled }"
class="input input-number"
type="number"
@input="e => updateProperty('y', e.target.value)"
>
</label>
<input
v-show="shadowControl && !hideControls"
:value="shadow?.x"
:disabled="disabled"
:class="{ disabled }"
class="input input-range x-shift-slider"
type="range"
max="20"
min="-20"
@input="e => updateProperty('x', e.target.value)"
>
<input
v-show="shadowControl && !hideControls"
:value="shadow?.y"
:disabled="disabled"
:class="{ disabled }"
class="input input-range y-shift-slider"
type="range"
max="20"
min="-20"
@input="e => updateProperty('y', e.target.value)"
>
<div
class="preview-window"
:class="{ '-light-grid': lightGrid }"
>
<div
class="preview-block"
:class="previewClass"
:style="style"
>
{{ $t('settings.style.themes3.editor.test_string') }}
</div>
<div
v-if="invalid"
class="invalid-container"
>
<div class="alert error invalid-label">
{{ $t('settings.style.themes3.editor.invalid') }}
</div>
</div>
</div>
<div class="assists">
<Checkbox
v-model="lightGrid"
name="lightGrid"
class="input-light-grid"
>
{{ $t('settings.style.shadows.light_grid') }}
</Checkbox>
<div class="style-control">
<label class="label">
{{ $t('settings.style.shadows.zoom') }}
</label>
<input
v-model="zoom"
class="input input-number y-shift-number"
type="number"
>
</div>
<ColorInput
v-if="!noColorControl"
v-model="colorOverride"
class="input-color-input"
fallback="#606060"
:label="$t('settings.style.shadows.color_override')"
/>
</div>
</div>
</template>
<script>
import Checkbox from 'src/components/checkbox/checkbox.vue'
import ColorInput from 'src/components/color_input/color_input.vue'
export default {
components: {
Checkbox,
ColorInput
},
props: [
'shadow',
'shadowControl',
'previewClass',
'previewStyle',
'previewCss',
'disabled',
'invalid',
'noColorControl'
],
emits: ['update:shadow'],
data () {
return {
colorOverride: undefined,
lightGrid: false,
zoom: 100
}
},
computed: {
style () {
const result = [
this.previewStyle,
`zoom: ${this.zoom / 100}`
]
if (this.colorOverride) result.push(`--background: ${this.colorOverride}`)
return result
},
hideControls () {
return typeof this.shadow === 'string'
}
},
methods: {
updateProperty (axis, value) {
this.$emit('update:shadow', { axis, value: Number(value) })
}
}
}
</script>
<style lang="scss">
.ComponentPreview {
display: grid;
grid-template-columns: 1em 1fr 1fr 1em;
grid-template-rows: 2em 1fr 1fr 1fr 1em 2em max-content;
grid-template-areas:
"header header header header "
"preview preview preview y-slide"
"preview preview preview y-slide"
"preview preview preview y-slide"
"x-slide x-slide x-slide . "
"x-num x-num y-num y-num "
"assists assists assists assists";
grid-gap: 0.5em;
&:not(.-shadow-controls) {
grid-template-areas:
"header header header header "
"preview preview preview y-slide"
"preview preview preview y-slide"
"preview preview preview y-slide"
"assists assists assists assists";
grid-template-rows: 2em 1fr 1fr 1fr max-content;
}
.header {
grid-area: header;
justify-self: center;
align-self: baseline;
line-height: 2;
}
.invalid-container {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
display: grid;
align-items: center;
justify-items: center;
background-color: rgba(100 0 0 / 50%);
.alert {
padding: 0.5em 1em;
}
}
.assists {
grid-area: assists;
display: grid;
grid-auto-flow: rows;
grid-auto-rows: 2em;
grid-gap: 0.5em;
}
.input-light-grid {
justify-self: center;
}
.input-number {
min-width: 2em;
}
.x-shift-number {
grid-area: x-num;
justify-self: right;
}
.y-shift-number {
grid-area: y-num;
justify-self: left;
}
.x-shift-number,
.y-shift-number {
input {
max-width: 4em;
}
}
.x-shift-slider {
grid-area: x-slide;
height: auto;
align-self: start;
min-width: 10em;
}
.y-shift-slider {
grid-area: y-slide;
writing-mode: vertical-lr;
justify-self: left;
min-height: 10em;
}
.x-shift-slider,
.y-shift-slider {
padding: 0;
}
.preview-window {
--__grid-color1: rgb(102 102 102);
--__grid-color2: rgb(153 153 153);
--__grid-color1-disabled: rgba(102 102 102 / 20%);
--__grid-color2-disabled: rgba(153 153 153 / 20%);
&.-light-grid {
--__grid-color1: rgb(205 205 205);
--__grid-color2: rgb(255 255 255);
--__grid-color1-disabled: rgba(205 205 205 / 20%);
--__grid-color2-disabled: rgba(255 255 255 / 20%);
}
position: relative;
grid-area: preview;
aspect-ratio: 1;
display: flex;
align-items: center;
justify-content: center;
min-width: 10em;
min-height: 10em;
background-color: var(--__grid-color2);
background-image:
linear-gradient(45deg, var(--__grid-color1) 25%, transparent 25%),
linear-gradient(-45deg, var(--__grid-color1) 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, var(--__grid-color1) 75%),
linear-gradient(-45deg, transparent 75%, var(--__grid-color1) 75%);
background-size: 20px 20px;
background-position: 0 0, 0 10px, 10px -10px, -10px 0;
border-radius: var(--roundness);
&.disabled {
background-color: var(--__grid-color2-disabled);
background-image:
linear-gradient(45deg, var(--__grid-color1-disabled) 25%, transparent 25%),
linear-gradient(-45deg, var(--__grid-color1-disabled) 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, var(--__grid-color1-disabled) 75%),
linear-gradient(-45deg, transparent 75%, var(--__grid-color1-disabled) 75%);
}
.preview-block {
background: var(--background, var(--bg));
display: flex;
justify-content: center;
align-items: center;
min-width: 33%;
min-height: 33%;
max-width: 80%;
max-height: 80%;
border-width: 0;
border-style: solid;
border-color: var(--border);
border-radius: var(--roundness);
box-shadow: var(--shadow);
}
}
}
</style>

View file

@ -3,39 +3,62 @@
v-if="contrast"
class="contrast-ratio"
>
<span
:title="hint"
<span v-if="showRatio">
{{ contrast.text }}
</span>
<Tooltip
:text="hint"
class="rating"
>
<span v-if="contrast.aaa">
<FAIcon icon="thumbs-up" />
<FAIcon
icon="thumbs-up"
:size="showRatio ? 'lg' : ''"
/>
</span>
<span v-if="!contrast.aaa && contrast.aa">
<FAIcon icon="adjust" />
<FAIcon
icon="adjust"
:size="showRatio ? 'lg' : ''"
/>
</span>
<span v-if="!contrast.aaa && !contrast.aa">
<FAIcon icon="exclamation-triangle" />
<FAIcon
icon="exclamation-triangle"
:size="showRatio ? 'lg' : ''"
/>
</span>
</span>
<span
</Tooltip>
<Tooltip
v-if="contrast && large"
:text="hint_18pt"
class="rating"
:title="hint_18pt"
>
<span v-if="contrast.laaa">
<FAIcon icon="thumbs-up" />
<FAIcon
icon="thumbs-up"
:size="showRatio ? 'large' : ''"
/>
</span>
<span v-if="!contrast.laaa && contrast.laa">
<FAIcon icon="adjust" />
<FAIcon
icon="adjust"
:size="showRatio ? 'lg' : ''"
/>
</span>
<span v-if="!contrast.laaa && !contrast.laa">
<FAIcon icon="exclamation-triangle" />
<FAIcon
icon="exclamation-triangle"
:size="showRatio ? 'lg' : ''"
/>
</span>
</span>
</Tooltip>
</span>
</template>
<script>
import Tooltip from 'src/components/tooltip/tooltip.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faAdjust,
@ -50,6 +73,9 @@ library.add(
)
export default {
components: {
Tooltip
},
props: {
large: {
required: false,
@ -62,6 +88,11 @@ export default {
required: false,
type: Object,
default: () => ({})
},
showRatio: {
required: false,
type: Boolean,
default: false
}
},
computed: {
@ -87,8 +118,7 @@ export default {
.contrast-ratio {
display: flex;
justify-content: flex-end;
margin-top: -4px;
margin-bottom: 5px;
align-items: baseline;
.label {
margin-right: 1em;
@ -96,7 +126,6 @@ export default {
.rating {
display: inline-block;
text-align: center;
margin-left: 0.5em;
}
}

View file

@ -9,7 +9,9 @@
v-if="isExpanded"
class="panel-heading conversation-heading -sticky"
>
<span class="title"> {{ $t('timeline.conversation') }} </span>
<h1 class="title">
{{ $t('timeline.conversation') }}
</h1>
<button
v-if="collapsable"
class="button-unstyled -link"

View file

@ -8,9 +8,9 @@
@click.stop=""
>
<div class="panel-heading dialog-modal-heading">
<div class="title">
<h1 class="title">
<slot name="header" />
</div>
</h1>
</div>
<div class="panel-body dialog-modal-content">
<slot name="default" />

View file

@ -6,7 +6,9 @@
>
<div class="edit-form-modal-panel panel">
<div class="panel-heading">
{{ $t('post_status.edit_status') }}
<h1 class="title">
{{ $t('post_status.edit_status') }}
</h1>
</div>
<EditStatusForm
ref="editStatusForm"

View file

@ -97,7 +97,7 @@ const EmojiPicker = {
enableStickerPicker: {
required: false,
type: Boolean,
default: false
default: true
},
hideCustomEmoji: {
required: false,
@ -105,7 +105,11 @@ const EmojiPicker = {
default: false
}
},
inject: ['popoversZLayer'],
inject: {
popoversZLayer: {
default: ''
}
},
data () {
return {
keyword: '',
@ -150,7 +154,9 @@ const EmojiPicker = {
},
showPicker () {
this.$refs.popover.showPopover()
this.onShowing()
this.$nextTick(() => {
this.onShowing()
})
},
hidePicker () {
this.$refs.popover.hidePopover()
@ -178,7 +184,7 @@ const EmojiPicker = {
if (!this.keepOpen) {
this.$refs.popover.hidePopover()
}
this.$emit('emoji', { insertion: value, keepOpen: this.keepOpen })
this.$emit('emoji', { insertion: value, insertionUrl: emoji.imageUrl, keepOpen: this.keepOpen })
},
onScroll (startIndex, endIndex, visibleStartIndex, visibleEndIndex) {
const target = this.$refs['emoji-groups'].$el

View file

@ -89,6 +89,7 @@
class="emoji-groups"
:class="groupsScrolledClass"
:min-item-size="minItemSize"
:buffer="minItemSize"
:items="emojiItems"
:emit-update="true"
@update="onScroll"

View file

@ -1,6 +1,7 @@
import Popover from '../popover/popover.vue'
import genRandomSeed from '../../services/random_seed/random_seed.service.js'
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
import StatusBookmarkFolderMenu from '../status_bookmark_folder_menu/status_bookmark_folder_menu.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faEllipsisH,
@ -36,7 +37,8 @@ const ExtraButtons = {
props: ['status'],
components: {
Popover,
ConfirmModal
ConfirmModal,
StatusBookmarkFolderMenu
},
data () {
return {
@ -145,6 +147,9 @@ const ExtraButtons = {
canBookmark () {
return !!this.currentUser
},
bookmarkFolders () {
return this.$store.state.instance.pleromaBookmarkFoldersAvailable
},
statusLink () {
return `${this.$store.state.instance.server}${this.$router.resolve({ name: 'conversation', params: { id: this.status.id } }).href}`
},

View file

@ -87,6 +87,10 @@
icon="bookmark"
/><span>{{ $t("status.unbookmark") }}</span>
</button>
<StatusBookmarkFolderMenu
v-if="status.bookmarked && bookmarkFolders"
:status="status"
/>
</template>
<button
v-if="ownStatus && editingAvailable"

View file

@ -56,6 +56,7 @@
tag="span"
class="notification tip extra-notification"
keypath="notifications.configuration_tip"
scope="global"
>
<template #theSettings>
<button

View file

@ -2,9 +2,9 @@
<div class="features-panel">
<div class="panel panel-default base01-background">
<div class="panel-heading timeline-heading base02-background base04">
<div class="title">
<h1 class="title">
{{ $t('features_panel.title') }}
</div>
</h1>
</div>
<div class="panel-body features-panel">
<ul>

View file

@ -17,6 +17,7 @@
@cancelled="hideConfirmUnfollow"
>
<i18n-t
scope="global"
keypath="user_card.unfollow_confirm"
tag="span"
>

View file

@ -1,9 +1,9 @@
<template>
<div class="settings panel panel-default">
<div class="panel-heading">
<div class="title">
<h1 class="title">
{{ $t('nav.friend_requests') }}
</div>
</h1>
</div>
<div class="panel-body">
<FollowRequestCard

View file

@ -1,11 +1,8 @@
<template>
<div
class="font-control"
:class="{ custom: isCustom }"
>
<div class="font-control">
<label
:id="name + '-label'"
:for="preset === 'custom' ? name : name + '-font-switcher'"
:for="manualEntry ? name : name + '-font-switcher'"
class="label"
>
{{ label }}
@ -14,7 +11,7 @@
<Checkbox
v-if="typeof fallback !== 'undefined'"
:id="name + '-o'"
:modelValue="present"
:model-value="present"
@change="$emit('update:modelValue', typeof modelValue === 'undefined' ? fallback : undefined)"
>
{{ $t('settings.style.themes3.define') }}
@ -23,12 +20,13 @@
<label
v-if="manualEntry"
:id="name + '-label'"
:for="preset === 'custom' ? name : name + '-font-switcher'"
:for="manualEntry ? name : name + '-font-switcher'"
class="label"
>
<i18n-t
keypath="settings.style.themes3.font.entry"
tag="span"
scope="global"
>
<template #fontFamily>
<code>font-family</code>
@ -38,7 +36,7 @@
<label
v-else
:id="name + '-label'"
:for="preset === 'custom' ? name : name + '-font-switcher'"
:for="manualEntry ? name : name + '-font-switcher'"
class="label"
>
{{ $t('settings.style.themes3.font.select') }}
@ -50,8 +48,8 @@
>
<button
class="btn button-default"
@click="toggleManualEntry"
:title="$t('settings.style.themes3.font.lookup_local_fonts')"
@click="toggleManualEntry"
>
<FAIcon
fixed-width
@ -72,8 +70,8 @@
>
<button
class="btn button-default"
@click="toggleManualEntry"
:title="$t('settings.style.themes3.font.enter_manually')"
@click="toggleManualEntry"
>
<FAIcon
fixed-width

View file

@ -6,7 +6,7 @@ export default {
{
component: 'Icon',
directives: {
textColor: '$blend(--stack, 0.5, --parent--text)',
textColor: '$blend(--stack 0.5 --parent--text)',
textAuto: 'no-auto'
}
}

View file

@ -1,32 +1,26 @@
const hoverGlow = {
x: 0,
y: 0,
blur: 4,
spread: 0,
color: '--text',
alpha: 1
}
export default {
name: 'Input',
selector: '.input',
variant: {
states: {
hover: ':hover:not(.disabled)',
focused: ':focus-within',
disabled: '.disabled'
},
variants: {
checkbox: '.-checkbox',
radio: '.-radio'
},
states: {
disabled: ':disabled',
hover: ':hover:not(:disabled)',
focused: ':focus-within'
},
validInnerComponents: [
'Text'
'Text',
'Icon'
],
defaultRules: [
{
component: 'Root',
directives: {
'--defaultInputBevel': 'shadow | $borderSide(#FFFFFF, bottom, 0.2)| $borderSide(#000000, top, 0.2)'
'--defaultInputBevel': 'shadow | $borderSide(#FFFFFF bottom 0.2), $borderSide(#000000 top 0.2)',
'--defaultInputHoverGlow': 'shadow | 0 0 4 --text / 0.5',
'--defaultInputFocusGlow': 'shadow | 0 0 4 4 --link / 0.5'
}
},
{
@ -53,7 +47,47 @@ export default {
{
state: ['hover'],
directives: {
shadow: [hoverGlow, '--defaultInputBevel']
shadow: ['--defaultInputHoverGlow', '--defaultInputBevel']
}
},
{
state: ['focused'],
directives: {
shadow: ['--defaultInputFocusGlow', '--defaultInputBevel']
}
},
{
state: ['focused', 'hover'],
directives: {
shadow: ['--defaultInputFocusGlow', '--defaultInputHoverGlow', '--defaultInputBevel']
}
},
{
state: ['disabled'],
directives: {
background: '--parent'
}
},
{
component: 'Text',
parent: {
component: 'Input',
state: ['disabled']
},
directives: {
textOpacity: 0.25,
textOpacityMode: 'blend'
}
},
{
component: 'Icon',
parent: {
component: 'Input',
state: ['disabled']
},
directives: {
textOpacity: 0.25,
textOpacityMode: 'blend'
}
}
]

View file

@ -1,9 +1,9 @@
<template>
<div class="panel panel-default">
<div class="panel-heading">
<div class="title">
<h1 class="title">
{{ $t("nav.interactions") }}
</div>
</h1>
</div>
<tab-switcher
ref="tabSwitcher"

View file

@ -2,7 +2,9 @@
<div class="Lists panel panel-default">
<div class="panel-heading">
<div class="title">
{{ $t('lists.lists') }}
<h1 class="title">
{{ $t('lists.lists') }}
</h1>
</div>
<router-link
:to="{ name: 'lists-new' }"

View file

@ -17,6 +17,7 @@
<i18n-t
v-if="id"
keypath="lists.editing_list"
scope="global"
>
<template #listTitle>
{{ title }}
@ -25,6 +26,7 @@
<i18n-t
v-else
keypath="lists.creating_list"
scope="global"
/>
</div>
</div>

View file

@ -3,7 +3,9 @@
<!-- Default panel contents -->
<div class="panel-heading">
{{ $t('login.login') }}
<h1 class="title">
{{ $t('login.login') }}
</h1>
</div>
<div class="panel-body">

View file

@ -53,7 +53,9 @@ const MentionLink = {
this.$router.push(link)
},
handleSelection () {
this.hasSelection = document.getSelection().containsNode(this.$refs.full, true)
if (this.$refs.full) {
this.hasSelection = document.getSelection().containsNode(this.$refs.full, true)
}
}
},
mounted () {

View file

@ -24,21 +24,21 @@ export default {
{
state: ['hover'],
directives: {
background: '$mod(--bg, 5)',
background: '$mod(--bg 5)',
opacity: 1
}
},
{
state: ['active'],
directives: {
background: '$mod(--bg, 10)',
background: '$mod(--bg 10)',
opacity: 1
}
},
{
state: ['active', 'hover'],
directives: {
background: '$mod(--bg, 15)',
background: '$mod(--bg 15)',
opacity: 1
}
},

View file

@ -3,7 +3,9 @@
<!-- Default panel contents -->
<div class="panel-heading">
{{ $t('login.heading.recovery') }}
<h1 class="title">
{{ $t('login.heading.recovery') }}
</h1>
</div>
<div class="panel-body">

View file

@ -3,7 +3,9 @@
<!-- Default panel contents -->
<div class="panel-heading">
{{ $t('login.heading.totp') }}
<h1 class="title">
{{ $t('login.heading.totp') }}
</h1>
</div>
<div class="panel-body">

View file

@ -50,13 +50,13 @@
@touchmove.stop="notificationsTouchMove"
>
<div class="panel-heading mobile-notifications-header">
<span class="title">
<h1 class="title">
{{ $t('notifications.notifications') }}
<span
v-if="unseenCountBadgeText"
class="badge -notification unseen-count"
>{{ unseenCountBadgeText }}</span>
</span>
</h1>
<span class="spacer" />
<button
v-if="notificationsAtTop"

View file

@ -1,7 +1,8 @@
export default {
name: 'Modals',
selector: '.modal-view',
selector: ['.modal-view', '#modal', '.shout-panel'],
lazy: true,
notEditable: true,
validInnerComponents: [
'Panel'
],

View file

@ -1,3 +1,4 @@
import BookmarkFoldersMenuContent from 'src/components/bookmark_folders_menu/bookmark_folders_menu_content.vue'
import ListsMenuContent from 'src/components/lists_menu/lists_menu_content.vue'
import { mapState, mapGetters } from 'vuex'
import { TIMELINES, ROOT_ITEMS } from 'src/components/navigation/navigation.js'
@ -43,6 +44,7 @@ const NavPanel = {
created () {
},
components: {
BookmarkFoldersMenuContent,
ListsMenuContent,
NavigationEntry,
NavigationPins,
@ -53,6 +55,7 @@ const NavPanel = {
editMode: false,
showTimelines: false,
showLists: false,
showBookmarkFolders: false,
timelinesList: Object.entries(TIMELINES).map(([k, v]) => ({ ...v, name: k })),
rootList: Object.entries(ROOT_ITEMS).map(([k, v]) => ({ ...v, name: k }))
}
@ -64,6 +67,9 @@ const NavPanel = {
toggleLists () {
this.showLists = !this.showLists
},
toggleBookmarkFolders () {
this.showBookmarkFolders = !this.showBookmarkFolders
},
toggleEditMode () {
this.editMode = !this.editMode
},
@ -92,7 +98,8 @@ const NavPanel = {
pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable,
supportsAnnouncements: state => state.announcements.supportsAnnouncements,
pinnedItems: state => new Set(state.serverSideStorage.prefsStorage.collections.pinnedNavItems),
collapsed: state => state.serverSideStorage.prefsStorage.simple.collapseNav
collapsed: state => state.serverSideStorage.prefsStorage.simple.collapseNav,
bookmarkFolders: state => state.instance.pleromaBookmarkFoldersAvailable
}),
timelinesItems () {
return filterNavigation(
@ -104,7 +111,8 @@ const NavPanel = {
hasAnnouncements: this.supportsAnnouncements,
isFederating: this.federating,
isPrivate: this.privateMode,
currentUser: this.currentUser
currentUser: this.currentUser,
supportsBookmarkFolders: this.bookmarkFolders
}
)
},
@ -118,7 +126,8 @@ const NavPanel = {
hasAnnouncements: this.supportsAnnouncements,
isFederating: this.federating,
isPrivate: this.privateMode,
currentUser: this.currentUser
currentUser: this.currentUser,
supportsBookmarkFolders: this.bookmarkFolders
}
)
},

View file

@ -83,6 +83,39 @@
class="timelines"
/>
</div>
<NavigationEntry
v-if="currentUser && bookmarkFolders"
:show-pin="false"
:item="{ icon: 'bookmark', label: 'nav.bookmarks' }"
:aria-expanded="showBookmarkFolders ? 'true' : 'false'"
@click="toggleBookmarkFolders"
>
<router-link
:title="$t('bookmarks.manage_bookmark_folders')"
class="button-unstyled extra-button"
:to="{ name: 'bookmark-folders' }"
@click.stop
>
<FAIcon
fixed-width
icon="wrench"
/>
</router-link>
<FAIcon
class="timelines-chevron"
fixed-width
:icon="showBookmarkFolders ? 'chevron-up' : 'chevron-down'"
/>
</NavigationEntry>
<div
v-show="showBookmarkFolders"
class="timelines-background menu-item-collapsible"
:class="{ '-expanded': showBookmarkFolders }"
>
<BookmarkFoldersMenuContent
class="timelines"
/>
</div>
<NavigationEntry
v-for="item in rootItems"
:key="item.name"
@ -92,7 +125,7 @@
<NavigationEntry
v-if="!forceEditMode && currentUser"
:show-pin="false"
:item="{ label: editMode ? $t('nav.edit_finish') : $t('nav.edit_pinned'), icon: editMode ? 'check' : 'wrench' }"
:item="{ labelRaw: editMode ? $t('nav.edit_finish') : $t('nav.edit_pinned'), icon: editMode ? 'check' : 'wrench' }"
@click="toggleEditMode"
/>
</ul>

View file

@ -1,4 +1,4 @@
export const filterNavigation = (list = [], { hasChats, hasAnnouncements, isFederating, isPrivate, currentUser }) => {
export const filterNavigation = (list = [], { hasChats, hasAnnouncements, isFederating, isPrivate, currentUser, supportsBookmarkFolders }) => {
return list.filter(({ criteria, anon, anonRoute }) => {
const set = new Set(criteria || [])
if (!isFederating && set.has('federating')) return false
@ -7,6 +7,7 @@ export const filterNavigation = (list = [], { hasChats, hasAnnouncements, isFede
if ((!currentUser || !currentUser.locked) && set.has('lockedUser')) return false
if (!hasChats && set.has('chats')) return false
if (!hasAnnouncements && set.has('announcements')) return false
if (supportsBookmarkFolders && set.has('!supportsBookmarkFolders')) return false
return true
})
}
@ -17,3 +18,12 @@ export const getListEntries = state => state.lists.allLists.map(list => ({
labelRaw: list.title,
iconLetter: list.title[0]
}))
export const getBookmarkFolderEntries = state => state.bookmarkFolders.allFolders.map(folder => ({
name: 'bookmark-folder-' + folder.id,
routeObject: { name: 'bookmark-folder', params: { id: folder.id } },
labelRaw: folder.name,
iconEmoji: folder.emoji,
iconEmojiUrl: folder.emoji_url,
iconLetter: folder.name[0]
}))

View file

@ -1,11 +1,16 @@
// routes that take :username property
export const USERNAME_ROUTES = new Set([
'bookmarks',
'dms',
'interactions',
'notifications',
'chat',
'chats',
'user-profile'
'chats'
])
// routes that take :name property
export const NAME_ROUTES = new Set([
'user-profile',
'legacy-user-profile'
])
export const TIMELINES = {
@ -32,7 +37,8 @@ export const TIMELINES = {
bookmarks: {
route: 'bookmarks',
icon: 'bookmark',
label: 'nav.bookmarks'
label: 'nav.bookmarks',
criteria: ['!supportsBookmarkFolders']
},
favorites: {
routeObject: { name: 'user-profile', query: { tab: 'favorites' } },
@ -103,7 +109,9 @@ export function routeTo (item, currentUser) {
}
if (USERNAME_ROUTES.has(route.name)) {
route.params = { username: currentUser.screen_name, name: currentUser.screen_name }
route.params = { username: currentUser.screen_name }
} else if (NAME_ROUTES.has(route.name)) {
route.params = { name: currentUser.screen_name }
}
return route

View file

@ -22,11 +22,25 @@
:icon="item.icon"
/>
</span>
<img
v-if="item.iconEmojiUrl"
class="menu-icon iconEmoji iconEmoji-image"
:src="item.iconEmojiUrl"
:alt="item.iconEmoji"
:title="item.iconEmoji"
>
<span
v-if="item.iconLetter"
class="icon iconLetter fa-scale-110 menu-icon"
>{{ item.iconLetter }}
v-else-if="item.iconEmoji"
class="menu-icon iconEmoji"
>
<span>
{{ item.iconEmoji }}
</span>
</span>
<span
v-else-if="item.iconLetter"
class="icon iconLetter fa-scale-110 menu-icon"
>{{ item.iconLetter }}</span>
<span class="label">
{{ item.labelRaw || $t(item.label) }}
</span>
@ -111,5 +125,23 @@
.badge {
margin: 0 var(--__horizontal-gap);
}
.iconEmoji {
display: inline-block;
text-align: center;
object-fit: contain;
vertical-align: middle;
height: var(--__line-height);
width: var(--__line-height);
> span {
font-size: 1.5rem;
}
}
img.iconEmoji {
padding: 0.25rem;
box-sizing: border-box;
}
}
</style>

View file

@ -10,7 +10,7 @@
background-color: transparent !important;
}
--emoji-size: 14px;
--emoji-size: 1em;
&:hover {
--_still-image-img-visibility: visible;
@ -26,6 +26,7 @@
overflow: hidden;
display: flex;
flex-wrap: nowrap;
gap: 1ex;
& .status-username,
& .mute-thread,

View file

@ -47,7 +47,6 @@
>
<UserAvatar
class="post-avatar"
:bot="botIndicator"
:compact="true"
:better-shadow="betterShadow"
:user="notification.from_profile"

View file

@ -14,13 +14,13 @@
v-if="!noHeading"
class="notifications-heading panel-heading -sticky"
>
<div class="title">
<h1 class="title">
{{ $t('notifications.notifications') }}
<span
v-if="unseenCountBadgeText"
class="badge -notification unseen-count"
>{{ unseenCountBadgeText }}</span>
</div>
</h1>
<div
v-if="showScrollTop"
class="rightside-button"

View file

@ -6,8 +6,9 @@
<label
:for="name"
class="label"
:class="{ faint: !present || disabled }"
>
{{ $t('settings.style.common.opacity') }}
{{ label }}
</label>
<Checkbox
v-if="typeof fallback !== 'undefined'"
@ -22,6 +23,7 @@
type="number"
:value="modelValue || fallback"
:disabled="!present || disabled"
:class="{ disabled: !present || disabled }"
max="1"
min="0"
step=".05"
@ -37,7 +39,7 @@ export default {
Checkbox
},
props: [
'name', 'modelValue', 'fallback', 'disabled'
'name', 'label', 'modelValue', 'fallback', 'disabled'
],
emits: ['update:modelValue'],
computed: {

View file

@ -0,0 +1,193 @@
<template>
<div
class="PaletteEditor"
:class="{ '-compact': compact, '-apply': apply }"
>
<ColorInput
v-for="key in paletteKeys"
:key="key"
:name="key"
:model-value="props.modelValue[key]"
:fallback="fallback(key)"
:label="$t('settings.style.themes3.palette.' + key)"
@update:modelValue="value => updatePalette(key, value)"
/>
<button
class="btn button-default palette-import-button"
@click="importPalette"
>
<FAIcon icon="file-import" />
{{ $t('settings.style.themes3.palette.import') }}
</button>
<button
class="btn button-default palette-export-button"
@click="exportPalette"
>
<FAIcon icon="file-export" />
{{ $t('settings.style.themes3.palette.export') }}
</button>
<button
v-if="apply"
class="btn button-default palette-apply-button"
@click="applyPalette"
>
{{ $t('settings.style.themes3.palette.apply') }}
</button>
</div>
</template>
<script setup>
import ColorInput from 'src/components/color_input/color_input.vue'
import {
newImporter,
newExporter
} from 'src/services/export_import/export_import.js'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faFileImport,
faFileExport
} from '@fortawesome/free-solid-svg-icons'
library.add(
faFileImport,
faFileExport
)
const paletteKeys = [
'bg',
'fg',
'text',
'link',
'accent',
'cRed',
'cBlue',
'cGreen',
'cOrange',
'wallpaper'
]
const props = defineProps(['modelValue', 'compact', 'apply'])
const emit = defineEmits(['update:modelValue', 'applyPalette'])
const getExportedObject = () => paletteKeys.reduce((acc, key) => {
const value = props.modelValue[key]
if (value == null) {
return acc
} else {
return { ...acc, [key]: props.modelValue[key] }
}
}, {})
const paletteExporter = newExporter({
filename: 'pleroma_palette',
extension: 'json',
getExportedObject
})
const paletteImporter = newImporter({
accept: '.json',
onImport (parsed, filename) {
emit('update:modelValue', parsed)
}
})
const exportPalette = () => {
paletteExporter.exportData()
}
const importPalette = () => {
paletteImporter.importData()
}
const applyPalette = (data) => {
emit('applyPalette', getExportedObject())
}
const fallback = (key) => {
if (key === 'accent') {
return props.modelValue.link
}
if (key === 'link') {
return props.modelValue.accent
}
if (key.startsWith('extra')) {
return '#FF00FF'
}
if (key.startsWith('wallpaper')) {
return '#008080'
}
}
const updatePalette = (paletteKey, value) => {
emit('update:modelValue', {
...props.modelValue,
[paletteKey]: value
})
}
</script>
<style lang="scss">
.PaletteEditor {
display: grid;
justify-content: space-around;
grid-template-columns: repeat(4, 1fr);
grid-template-rows: repeat(5, 1fr) auto;
grid-gap: 0.5em;
align-items: baseline;
.palette-import-button {
grid-column: 1 / span 2;
}
.palette-export-button {
grid-column: 3 / span 2;
}
.palette-apply-button {
grid-column: 1 / span 2;
}
.color-input.style-control {
margin: 0;
}
&.-compact {
grid-template-columns: repeat(2, 1fr);
grid-template-rows: repeat(5, 1fr) auto;
.palette-import-button {
grid-column: 1;
}
.palette-export-button {
grid-column: 2;
}
&.-apply {
grid-template-rows: repeat(5, 1fr) auto auto;
.palette-apply-button {
grid-column: 1 / span 2;
}
}
.-mobile & {
grid-template-columns: 1fr;
grid-template-rows: repeat(10, 1fr) auto;
.palette-import-button {
grid-column: 1;
}
.palette-export-button {
grid-column: 1;
}
&.-apply {
.palette-apply-button {
grid-column: 1;
}
}
}
}
}
</style>

View file

@ -1,7 +1,9 @@
<template>
<div class="settings panel panel-default">
<div class="panel-heading">
{{ $t('password_reset.password_reset') }}
<h1 class="title">
{{ $t('password_reset.password_reset') }}
</h1>
</div>
<div class="panel-body">
<form

View file

@ -76,6 +76,13 @@
>
{{ $t('polls.vote') }}
</button>
<span
v-if="poll.pleroma?.non_anonymous"
:title="$t('polls.non_anonymous_title')"
>
{{ $t('polls.non_anonymous') }}
&nbsp;·&nbsp;
</span>
<div class="total">
<template v-if="typeof poll.voters_count === 'number'">
{{ $tc("polls.people_voted_count", poll.voters_count, { count: poll.voters_count }) }}

View file

@ -53,7 +53,11 @@ const Popover = {
default: {}
}
},
inject: ['popoversZLayer'], // override popover z layer
inject: { // override popover z layer
popoversZLayer: {
default: ''
}
},
data () {
return {
// lockReEntry is a flag that is set when mouse cursor is leaving the popover's content

View file

@ -103,6 +103,36 @@
icon="circle-notch"
/>
</div>
<div
v-if="quotable"
role="radiogroup"
class="btn-group reply-or-quote-selector"
>
<button
:id="`reply-or-quote-option-${randomSeed}-reply`"
class="btn button-default reply-or-quote-option"
:class="{ toggled: !newStatus.quoting }"
tabindex="0"
role="radio"
:aria-labelledby="`reply-or-quote-option-${randomSeed}-reply`"
:aria-checked="!newStatus.quoting"
@click="newStatus.quoting = false"
>
{{ $t('post_status.reply_option') }}
</button>
<button
:id="`reply-or-quote-option-${randomSeed}-quote`"
class="btn button-default reply-or-quote-option"
:class="{ toggled: newStatus.quoting }"
tabindex="0"
role="radio"
:aria-labelledby="`reply-or-quote-option-${randomSeed}-quote`"
:aria-checked="newStatus.quoting"
@click="newStatus.quoting = true"
>
{{ $t('post_status.quote_option') }}
</button>
</div>
</div>
<div
v-if="showPreview"
@ -126,36 +156,6 @@
class="preview-status"
/>
</div>
<div
v-if="quotable"
role="radiogroup"
class="btn-group reply-or-quote-selector"
>
<button
:id="`reply-or-quote-option-${randomSeed}-reply`"
class="btn button-default reply-or-quote-option"
:class="{ toggled: !newStatus.quoting }"
tabindex="0"
role="radio"
:aria-labelledby="`reply-or-quote-option-${randomSeed}-reply`"
:aria-checked="!newStatus.quoting"
@click="newStatus.quoting = false"
>
{{ $t('post_status.reply_option') }}
</button>
<button
:id="`reply-or-quote-option-${randomSeed}-quote`"
class="btn button-default reply-or-quote-option"
:class="{ toggled: newStatus.quoting }"
tabindex="0"
role="radio"
:aria-labelledby="`reply-or-quote-option-${randomSeed}-quote`"
:aria-checked="newStatus.quoting"
@click="newStatus.quoting = true"
>
{{ $t('post_status.quote_option') }}
</button>
</div>
<EmojiInput
v-if="!disableSubject && (newStatus.spoilerText || alwaysShowSubject)"
v-model="newStatus.spoilerText"
@ -181,10 +181,10 @@
:suggest="emojiUserSuggestor"
:placement="emojiPickerPlacement"
class="input form-control main-input"
enable-sticker-picker
enable-emoji-picker
hide-emoji-button
:newline-on-ctrl-enter="submitOnEnter"
enable-sticker-picker
@input="onEmojiInputInput"
@sticker-uploaded="addMediaFile"
@sticker-upload-failed="uploadFailed"
@ -235,7 +235,6 @@
class="text-format"
>
<Select
id="post-content-type"
v-model="newStatus.contentType"
class="input form-control"
:attrs="{ 'aria-label': $t('post_status.content_type_selection') }"
@ -427,13 +426,14 @@
.preview-heading {
display: flex;
padding-left: 0.5em;
flex-wrap: wrap;
}
.preview-toggle {
flex: 1;
flex: 10 0 auto;
cursor: pointer;
user-select: none;
padding-left: 0.5em;
&:hover {
text-decoration: underline;
@ -464,7 +464,10 @@
}
.reply-or-quote-selector {
flex: 1 0 auto;
margin-bottom: 0.5em;
display: grid;
grid-template-columns: 1fr 1fr;
}
.text-format {

View file

@ -7,7 +7,9 @@
>
<div class="post-form-modal-panel panel">
<div class="panel-heading">
{{ $t('post_status.new_status') }}
<h1 class="title">
{{ $t('post_status.new_status') }}
</h1>
</div>
<PostStatusForm
class="panel-body"

View file

@ -2,7 +2,7 @@
<span class="ReactButton">
<EmojiPicker
ref="picker"
:enable-sticker-picker="enableStickerPicker"
:enable-sticker-picker="false"
:hide-custom-emoji="hideCustomEmoji"
class="emoji-picker-panel"
@emoji="addReaction"

View file

@ -1,7 +1,9 @@
<template>
<div class="settings panel panel-default">
<div class="panel-heading">
{{ $t('registration.registration') }}
<h1 class="title">
{{ $t('registration.registration') }}
</h1>
</div>
<div
v-if="!hasSignUpNotice"

View file

@ -1,7 +1,9 @@
<template>
<div class="panel panel-default">
<div class="panel-heading">
{{ $t('remote_user_resolver.remote_user_resolver') }}
<h1 class="title">
{{ $t('remote_user_resolver.remote_user_resolver') }}
</h1>
</div>
<div class="panel-body">
<p>

View file

@ -17,6 +17,7 @@
@cancelled="hideConfirmRemoveUserFromFollowers"
>
<i18n-t
scope="global"
keypath="user_card.remove_follower_confirm"
tag="span"
>

View file

@ -1,6 +1,7 @@
export default {
name: 'RichContent',
selector: '.RichContent',
notEditable: true,
validInnerComponents: [
'Text',
'FunText',

View file

@ -1,6 +1,7 @@
export default {
name: 'Root',
selector: ':root',
notEditable: true,
validInnerComponents: [
'Underlay',
'Modals',
@ -42,7 +43,7 @@ export default {
// Selection colors
'--selectionBackground': 'color | --accent',
'--selectionText': 'color | $textColor(--accent, --text, no-preserve)'
'--selectionText': 'color | $textColor(--accent --text no-preserve)'
}
}
]

View file

@ -0,0 +1,51 @@
<template>
<div
class="roundness-control style-control"
:class="{ disabled: !present || disabled }"
>
<label
:for="name"
class="label"
:class="{ faint: !present || disabled }"
>
{{ label }}
</label>
<Checkbox
v-if="typeof fallback !== 'undefined'"
:model-value="present"
:disabled="disabled"
class="opt"
@update:modelValue="$emit('update:modelValue', !present ? fallback : undefined)"
/>
<input
:id="name"
class="input input-number"
type="number"
:value="modelValue || fallback"
:disabled="!present || disabled"
:class="{ disabled: !present || disabled }"
max="999"
min="0"
step="1"
@input="$emit('update:modelValue', $event.target.value)"
>
</div>
</template>
<script>
import Checkbox from '../checkbox/checkbox.vue'
export default {
components: {
Checkbox
},
props: [
'name', 'label', 'modelValue', 'fallback', 'disabled'
],
emits: ['update:modelValue'],
computed: {
present () {
return typeof this.modelValue !== 'undefined'
}
}
}
</script>

View file

@ -1,6 +1,7 @@
export default {
name: 'Scrollbar',
selector: '::-webkit-scrollbar',
selector: ['::-webkit-scrollbar-button', '::-webkit-scrollbar-thumb', '::-webkit-resizer'],
notEditable: true, // for now
defaultRules: [
{
directives: {

View file

@ -31,6 +31,7 @@ const hoverGlow = {
export default {
name: 'ScrollbarElement',
selector: '::-webkit-scrollbar-button',
notEditable: true, // for now
states: {
pressed: ':active',
hover: ':hover:not(:disabled)',
@ -82,7 +83,7 @@ export default {
{
state: ['disabled'],
directives: {
background: '$blend(--inheritedBackground, 0.25, --parent)',
background: '$blend(--inheritedBackground 0.25 --parent)',
shadow: [...buttonInsetFakeBorders]
}
},

View file

@ -1,9 +1,9 @@
<template>
<div class="Search panel panel-default">
<div class="panel-heading">
<div class="title">
<h1 class="title">
{{ $t('nav.search') }}
</div>
</h1>
</div>
<div class="panel-body search-input-container">
<input

View file

@ -6,13 +6,14 @@
<select
:disabled="disabled"
:value="modelValue"
v-bind="attrs"
v-bind="$attrs"
@change="$emit('update:modelValue', $event.target.value)"
>
<slot />
</select>
{{ ' ' }}
<FAIcon
v-if="!$attrs.size && !$attrs.multiple"
class="select-down-icon"
icon="chevron-down"
/>
@ -39,6 +40,39 @@ label.Select {
z-index: 1;
height: 2em;
line-height: 16px;
&[multiple],
&[size] {
height: 100%;
padding: 0.2em;
option {
background-color: transparent;
&:checked,
&.-active {
color: var(--selectionText);
background-color: var(--selectionBackground);
}
}
}
}
&.disabled,
&:disabled {
background-color: var(--background);
opacity: 1; /* override browser */
color: var(--faint);
select {
&[multiple],
&[size] {
option.-active {
color: var(--faint);
background: transparent;
}
}
}
}
.select-down-icon {
@ -50,7 +84,7 @@ label.Select {
width: 0.875em;
font-family: var(--font);
line-height: 2;
z-index: 0;
z-index: 1;
pointer-events: none;
}
}

View file

@ -0,0 +1,136 @@
<template>
<div
class="SelectMotion btn-group"
>
<button
class="btn button-default"
:disabled="disabled"
@click="add"
>
<FAIcon
fixed-width
icon="plus"
/>
</button>
<button
class="btn button-default"
:disabled="disabled || !moveUpValid"
:class="{ disabled: disabled || !moveUpValid }"
@click="moveUp"
>
<FAIcon
fixed-width
icon="chevron-up"
/>
</button>
<button
class="btn button-default"
:disabled="disabled || !moveDnValid"
:class="{ disabled: disabled || !moveDnValid }"
@click="moveDn"
>
<FAIcon
fixed-width
icon="chevron-down"
/>
</button>
<button
class="btn button-default"
:disabled="disabled || !present"
:class="{ disabled: disabled || !present }"
@click="del"
>
<FAIcon
fixed-width
icon="times"
/>
</button>
</div>
</template>
<script setup>
import { computed, defineEmits, defineProps, nextTick } from 'vue'
const props = defineProps({
modelValue: {
type: Array,
required: true
},
selectedId: {
type: Number,
required: true
},
disabled: {
type: Boolean,
default: false
},
getAddValue: {
type: Function,
required: true
}
})
const emit = defineEmits(['update:modelValue', 'update:selectedId'])
const moveUpValid = computed(() => {
return props.selectedId > 0
})
const present = computed(() => props.modelValue[props.selectedId] != null)
const moveUp = async () => {
const newModel = [...props.modelValue]
const movable = newModel.splice(props.selectedId, 1)[0]
newModel.splice(props.selectedId - 1, 0, movable)
emit('update:modelValue', newModel)
await nextTick()
emit('update:selectedId', props.selectedId - 1)
}
const moveDnValid = computed(() => {
return props.selectedId < props.modelValue.length - 1
})
const moveDn = async () => {
const newModel = [...props.modelValue]
const movable = newModel.splice(props.selectedId.value, 1)[0]
newModel.splice(props.selectedId + 1, 0, movable)
emit('update:modelValue', newModel)
await nextTick()
emit('update:selectedId', props.selectedId + 1)
}
const add = async () => {
const newModel = [...props.modelValue, props.getAddValue()]
emit('update:modelValue', newModel)
await nextTick()
emit('update:selectedId', Math.max(newModel.length - 1, 0))
}
const del = async () => {
const newModel = [...props.modelValue]
newModel.splice(props.selectedId, 1)
emit('update:modelValue', newModel)
await nextTick()
emit('update:selectedId', newModel.length === 0 ? undefined : Math.max(props.selectedId - 1, 0))
}
</script>
<style lang="scss">
.SelectMotion {
flex: 0 0 auto;
display: grid;
grid-auto-columns: 1fr;
grid-auto-flow: column;
margin-top: 0.25em;
.button-default {
margin: 0;
padding: 0;
}
}
</style>

View file

@ -49,11 +49,13 @@
<span v-if="adminDraft && adminDraft[':pleroma'][':frontends'][':primary']?.name === frontend.name">
<i18n-t
v-if="adminDraft && adminDraft[':pleroma'][':frontends'][':primary']?.ref === frontend.refs[0]"
scope="global"
keypath="admin_dash.frontend.is_default"
/>
<i18n-t
v-else
keypath="admin_dash.frontend.is_default_custom"
scope="global"
>
<template #version>
<code>{{ adminDraft && adminDraft[':pleroma'][':frontends'][':primary'].ref }}</code>
@ -120,7 +122,10 @@
@click.prevent="update(frontend, ref)"
@click="close"
>
<i18n-t keypath="admin_dash.frontend.install_version">
<i18n-t
keypath="admin_dash.frontend.install_version"
scope="global"
>
<template #version>
<code>{{ ref }}</code>
</template>
@ -177,7 +182,10 @@
@click.prevent="setDefault(frontend, ref)"
@click="close"
>
<i18n-t keypath="admin_dash.frontend.set_default_version">
<i18n-t
keypath="admin_dash.frontend.set_default_version"
scope="global"
>
<template #version>
<code>{{ ref }}</code>
</template>

View file

@ -14,7 +14,6 @@ library.add(
)
const LimitsTab = {
data () {},
components: {
BooleanSetting,
ChoiceSetting,

View file

@ -48,18 +48,14 @@
:attachment="attachment"
size="small"
hide-description
@setMedia="onMedia"
@naturalSizeLoad="onNaturalSizeLoad"
/>
<div class="controls control-upload">
<MediaUpload
ref="mediaUpload"
class="media-upload-icon"
:drop-files="dropFiles"
normal-button
:accept-types="acceptTypes"
@uploaded="setMediaFile"
@upload-failed="uploadFailed"
/>
</div>
</div>

View file

@ -112,7 +112,10 @@ export default {
components: { Popover, ConfirmModal, StillImage },
inject: ['emojiAddr'],
props: {
placement: String,
placement: {
type: String,
required: true
},
disabled: {
type: Boolean,
default: false
@ -120,8 +123,14 @@ export default {
newUpload: Boolean,
title: String,
packName: String,
title: {
type: String,
required: true
},
packName: {
type: String,
required: true
},
shortcode: {
type: String,
// Only exists when this is not a new upload

View file

@ -1,7 +1,7 @@
<template>
<NumberSetting
v-bind="$attrs"
truncate="1"
:truncate="1"
>
<slot />
</NumberSetting>

View file

@ -4,6 +4,21 @@ export default {
...Setting,
props: {
...Setting.props,
min: {
type: Number,
required: false,
default: 1
},
max: {
type: Number,
required: false,
default: 1
},
step: {
type: Number,
required: false,
default: 1
},
truncate: {
type: Number,
required: false,

View file

@ -10,9 +10,13 @@ export default {
ProfileSettingIndicator
},
props: {
modelValue: {
type: String,
default: null
},
path: {
type: [String, Array],
required: true
required: false
},
disabled: {
type: Boolean,
@ -68,7 +72,7 @@ export default {
}
},
created () {
if (this.realDraftMode && this.realSource !== 'admin') {
if (this.realDraftMode && (this.realSource !== 'admin' || this.path == null)) {
this.draft = this.state
}
},
@ -76,14 +80,14 @@ export default {
draft: {
// TODO allow passing shared draft object?
get () {
if (this.realSource === 'admin') {
if (this.realSource === 'admin' || this.path == null) {
return get(this.$store.state.adminSettings.draft, this.canonPath)
} else {
return this.localDraft
}
},
set (value) {
if (this.realSource === 'admin') {
if (this.realSource === 'admin' || this.path == null) {
this.$store.commit('updateAdminDraft', { path: this.canonPath, value })
} else {
this.localDraft = value
@ -91,6 +95,9 @@ export default {
}
},
state () {
if (this.path == null) {
return this.modelValue
}
const value = get(this.configSource, this.canonPath)
if (value === undefined) {
return this.defaultState
@ -145,6 +152,9 @@ export default {
return this.backendDescription?.suggestions
},
shouldBeDisabled () {
if (this.path == null) {
return this.disabled
}
const parentValue = this.parentPath !== undefined ? get(this.configSource, this.parentPath) : null
return this.disabled || (parentValue !== null ? (this.parentInvert ? parentValue : !parentValue) : false)
},
@ -159,6 +169,9 @@ export default {
}
},
configSink () {
if (this.path == null) {
return (k, v) => this.$emit('update:modelValue', v)
}
switch (this.realSource) {
case 'profile':
return (k, v) => this.$store.dispatch('setProfileOption', { name: k, value: v })
@ -184,6 +197,7 @@ export default {
return this.realSource === 'profile'
},
isChanged () {
if (this.path == null) return false
switch (this.realSource) {
case 'profile':
case 'admin':
@ -193,9 +207,11 @@ export default {
}
},
canonPath () {
if (this.path == null) return null
return Array.isArray(this.path) ? this.path : this.path.split('.')
},
isDirty () {
if (this.path == null) return false
if (this.realSource === 'admin' && this.canonPath.length > 3) {
return false // should not show draft buttons for "grouped" values
} else {

View file

@ -5,6 +5,7 @@
>
<label
:for="path"
class="setting-label"
:class="{ 'faint': shouldBeDisabled }"
>
<template v-if="backendDescriptionLabel">
@ -15,6 +16,7 @@
</template>
<slot v-else />
</label>
{{ ' ' }}
<input
:id="path"
class="input string-input"

View file

@ -10,31 +10,33 @@
<slot />
</label>
{{ ' ' }}
<input
:id="path"
class="input number-input"
type="number"
:step="step"
:disabled="disabled"
:min="min || 0"
:value="stateValue"
@change="updateValue"
>
<Select
:id="path"
:model-value="stateUnit"
:disabled="disabled"
class="unit-input unstyled"
@change="updateUnit"
>
<option
v-for="option in units"
:key="option"
:value="option"
<span class="no-break">
<input
:id="path"
class="input number-input"
type="number"
:step="step"
:disabled="disabled"
:min="min || 0"
:value="stateValue"
@change="updateValue"
>
{{ getUnitString(option) }}
</option>
</Select>
<Select
:id="path"
:model-value="stateUnit"
:disabled="disabled"
class="unit-input unstyled"
@change="updateUnit"
>
<option
v-for="option in units"
:key="option"
:value="option"
>
{{ getUnitString(option) }}
</option>
</Select>
</span>
{{ ' ' }}
<ModifiedIndicator
:changed="isChanged"
@ -47,6 +49,10 @@
<style lang="scss">
.UnitSetting {
.no-break {
display: inline-block;
}
.number-input {
max-width: 6.5em;
text-align: right;

View file

@ -167,7 +167,6 @@ const SettingsModal = {
},
computed: {
currentSaveStateNotice () {
console.log(this.$store.state.interface.settings.currentSaveStateNotice)
return this.$store.state.interface.settings.currentSaveStateNotice
},
modalActivated () {

View file

@ -10,6 +10,10 @@
list-style-type: none;
padding-left: 2em;
.btn:not(.dropdown-button) {
padding: 0 2em;
}
li {
margin-bottom: 0.5em;
}
@ -54,10 +58,6 @@
.btn {
min-height: 2em;
}
.btn:not(.dropdown-button) {
padding: 0 2em;
}
}
}
@ -76,6 +76,23 @@
}
}
&.-mobile {
.setting-list,
.option-list {
padding-left: 0.25em;
> li {
margin: 1em 0;
line-height: 1.5em;
vertical-align: center;
}
&.two-column {
column-count: 1;
}
}
}
&.peek {
.settings-modal-panel {
/* Explanation:

View file

@ -7,9 +7,9 @@
>
<div class="settings-modal-panel panel">
<div class="panel-heading">
<span class="title">
<h1 class="title">
{{ modalMode === 'user' ? $t('settings.settings') : $t('admin_dash.window_title') }}
</span>
</h1>
<transition name="fade">
<div
v-if="currentSaveStateNotice"
@ -110,7 +110,10 @@
{{ $t("settings.expert_mode") }}
</Checkbox>
<span v-if="modalMode === 'admin'">
<i18n-t keypath="admin_dash.wip_notice">
<i18n-t
scope="global"
keypath="admin_dash.wip_notice"
>
<template #adminFeLink>
<a
href="/pleroma/admin/#/login-pleroma"

View file

@ -17,10 +17,13 @@
}
.select-multiple {
margin-top: 0.5em;
display: flex;
flex-direction: column;
.option-list {
margin: 0;
margin-top: 0.5em;
padding-left: 0.5em;
}
}

View file

@ -17,7 +17,10 @@
<div :label="$t('admin_dash.tabs.nodb')">
<div class="setting-item">
<h2>{{ $t('admin_dash.nodb.heading') }}</h2>
<i18n-t keypath="admin_dash.nodb.text">
<i18n-t
scope="global"
keypath="admin_dash.nodb.text"
>
<template #documentation>
<a
href="https://docs-develop.pleroma.social/backend/configuration/howto_database_config/"

View file

@ -10,6 +10,7 @@ import GeneralTab from './tabs/general_tab.vue'
import AppearanceTab from './tabs/appearance_tab.vue'
import VersionTab from './tabs/version_tab.vue'
import ThemeTab from './tabs/theme_tab/theme_tab.vue'
import StyleTab from './tabs/style_tab/style_tab.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
@ -17,6 +18,7 @@ import {
faUser,
faFilter,
faPaintBrush,
faPalette,
faBell,
faDownload,
faEyeSlash,
@ -29,6 +31,7 @@ library.add(
faUser,
faFilter,
faPaintBrush,
faPalette,
faBell,
faDownload,
faEyeSlash,
@ -48,6 +51,7 @@ const SettingsModalContent = {
ProfileTab,
GeneralTab,
AppearanceTab,
StyleTab,
VersionTab,
ThemeTab
},
@ -60,6 +64,12 @@ const SettingsModalContent = {
},
bodyLock () {
return this.$store.state.interface.settingsModalState === 'visible'
},
expertLevel () {
return this.$store.state.config.expertLevel
},
isMobileLayout () {
return this.$store.state.interface.layoutType === 'mobile'
}
},
methods: {

View file

@ -1,6 +1,21 @@
.settings_tab-switcher {
height: 100%;
h1 {
margin-bottom: 0.5em;
margin-top: 0.5em;
}
h4 {
margin-bottom: 0;
margin-top: 0.25em;
}
h5 {
margin-bottom: 0;
margin-top: 0.25em;
}
.setting-item {
border-bottom: 2px solid var(--border);
margin: 1em 1em 1.4em;
@ -8,7 +23,6 @@
> div,
> label {
display: block;
margin-bottom: 0.5em;
&:last-child {
@ -17,10 +31,13 @@
}
.select-multiple {
margin-top: 1em;
display: flex;
flex-direction: column;
.option-list {
margin: 0;
margin-top: 0.5em;
padding-left: 0.5em;
}
}

View file

@ -17,13 +17,25 @@
:label="$t('settings.appearance')"
icon="window-restore"
data-tab-name="appearance"
:delay-render="true"
>
<AppearanceTab />
</div>
<div
:label="$t('settings.theme')"
v-if="expertLevel > 0 && !isMobileLayout"
:label="$t('settings.style.themes3.editor.title')"
icon="palette"
data-tab-name="style"
:delay-render="true"
>
<StyleTab />
</div>
<div
v-if="expertLevel > 0 && !isMobileLayout"
:label="$t('settings.theme_old')"
icon="paint-brush"
data-tab-name="theme"
:delay-render="true"
>
<ThemeTab />
</div>

View file

@ -3,20 +3,20 @@ import ChoiceSetting from '../helpers/choice_setting.vue'
import IntegerSetting from '../helpers/integer_setting.vue'
import FloatSetting from '../helpers/float_setting.vue'
import UnitSetting, { defaultHorizontalUnits } from '../helpers/unit_setting.vue'
import PaletteEditor from 'src/components/palette_editor/palette_editor.vue'
import FontControl from 'src/components/font_control/font_control.vue'
import { normalizeThemeData } from 'src/modules/interface'
import {
getThemes
} from 'src/services/style_setter/style_setter.js'
import { newImporter } from 'src/services/export_import/export_import.js'
import { convertTheme2To3 } from 'src/services/theme_data/theme2_to_theme3.js'
import { init } from 'src/services/theme_data/theme_data_3.service.js'
import {
getCssRules,
getScopedVersion
} from 'src/services/theme_data/css_utils.js'
import { deserialize } from 'src/services/theme_data/iss_deserializer.js'
import SharedComputedObject from '../helpers/shared_computed_object.js'
import ProfileSettingIndicator from '../helpers/profile_setting_indicator.vue'
@ -27,6 +27,10 @@ import {
import Preview from './theme_tab/theme_preview.vue'
// helper for debugging
// eslint-disable-next-line no-unused-vars
const toValue = (x) => JSON.parse(JSON.stringify(x === undefined ? 'null' : x))
library.add(
faGlobe
)
@ -34,7 +38,28 @@ library.add(
const AppearanceTab = {
data () {
return {
availableStyles: [],
availableThemesV3: [],
availableThemesV2: [],
bundledPalettes: [],
compilationCache: {},
fileImporter: newImporter({
accept: '.json, .piss',
validator: this.importValidator,
onImport: this.onImport,
parser: this.importParser,
onImportFailure: this.onImportFailure
}),
palettesKeys: [
'bg',
'fg',
'link',
'text',
'cRed',
'cGreen',
'cBlue',
'cOrange'
],
userPalette: {},
intersectionObserver: null,
thirdColumnModeOptions: ['none', 'notifications', 'postform'].map(mode => ({
key: mode,
@ -61,33 +86,77 @@ const AppearanceTab = {
UnitSetting,
ProfileSettingIndicator,
FontControl,
Preview
Preview,
PaletteEditor
},
mounted () {
getThemes()
.then((promises) => {
return Promise.all(
Object.entries(promises)
.map(([k, v]) => v.then(res => [k, res]))
)
this.$store.dispatch('getThemeData')
const updateIndex = (resource) => {
const capitalizedResource = resource[0].toUpperCase() + resource.slice(1)
const currentIndex = this.$store.state.instance[`${resource}sIndex`]
let promise
if (currentIndex) {
promise = Promise.resolve(currentIndex)
} else {
promise = this.$store.dispatch(`fetch${capitalizedResource}sIndex`)
}
return promise.then(index => {
return Object
.entries(index)
.map(([k, func]) => [k, func()])
})
.then(themes => themes.reduce((acc, [k, v]) => {
if (v) {
return [
...acc,
{
name: v.name || v[0],
key: k,
data: v
}
]
}
updateIndex('style').then(styles => {
styles.forEach(([key, stylePromise]) => stylePromise.then(data => {
const meta = data.find(x => x.component === '@meta')
this.availableThemesV3.push({ key, data, name: meta.directives.name, version: 'v3' })
}))
})
updateIndex('theme').then(themes => {
themes.forEach(([key, themePromise]) => themePromise.then(data => {
if (!data) {
console.warn(`Theme with key ${key} is empty or malformed`)
} else if (Array.isArray(data)) {
console.warn(`Theme with key ${key} is a v1 theme and should be moved to static/palettes/index.json`)
} else if (!data.source && !data.theme) {
console.warn(`Theme with key ${key} is malformed`)
} else {
return acc
this.availableThemesV2.push({ key, data, name: data.name, version: 'v2' })
}
}, []))
.then((themesComplete) => {
this.availableStyles = themesComplete
})
}))
})
this.userPalette = this.$store.state.interface.paletteDataUsed || {}
updateIndex('palette').then(bundledPalettes => {
bundledPalettes.forEach(([key, palettePromise]) => palettePromise.then(v => {
let palette
if (Array.isArray(v)) {
const [
name,
bg,
fg,
text,
link,
cRed = '#FF0000',
cGreen = '#00FF00',
cBlue = '#0000FF',
cOrange = '#E3FF00'
] = v
palette = { key, name, bg, fg, text, link, cRed, cBlue, cGreen, cOrange }
} else {
palette = { key, ...v }
}
if (!palette.key.startsWith('style.')) {
this.bundledPalettes.push(palette)
}
}))
})
if (window.IntersectionObserver) {
this.intersectionObserver = new IntersectionObserver((entries, observer) => {
@ -111,7 +180,65 @@ const AppearanceTab = {
})
})
},
watch: {
paletteDataUsed () {
this.userPalette = this.paletteDataUsed || {}
}
},
computed: {
paletteDataUsed () {
return this.$store.state.interface.paletteDataUsed
},
availableStyles () {
return [
...this.availableThemesV3,
...this.availableThemesV2
]
},
availablePalettes () {
return [
...this.bundledPalettes,
...this.stylePalettes
]
},
stylePalettes () {
const ruleset = this.$store.state.interface.styleDataUsed || []
if (!ruleset && ruleset.length === 0) return
const meta = ruleset.find(x => x.component === '@meta')
const result = ruleset.filter(x => x.component.startsWith('@palette'))
.map(x => {
const { variant, directives } = x
const {
bg,
fg,
text,
link,
accent,
cRed,
cBlue,
cGreen,
cOrange,
wallpaper
} = directives
const result = {
name: `${meta.directives.name || this.$t('settings.style.themes3.palette.imported')}: ${variant}`,
key: `style.${variant.toLowerCase().replace(/ /g, '_')}`,
bg,
fg,
text,
link,
accent,
cRed,
cBlue,
cGreen,
cOrange,
wallpaper
}
return Object.fromEntries(Object.entries(result).filter(([k, v]) => v))
})
return result
},
noIntersectionObserver () {
return !window.IntersectionObserver
},
@ -132,27 +259,32 @@ const AppearanceTab = {
return ['sidebar', 'content', ...notif]
}
},
instanceSpecificPanelPresent () { return this.$store.state.instance.showInstanceSpecificPanel },
instanceWallpaperUsed () {
return this.$store.state.instance.background &&
!this.$store.state.users.currentUser.background_image
},
instanceShoutboxPresent () { return this.$store.state.instance.shoutAvailable },
language: {
get: function () { return this.$store.getters.mergedConfig.interfaceLanguage },
set: function (val) {
this.$store.dispatch('setOption', { name: 'interfaceLanguage', value: val })
}
},
customThemeVersion () {
const { themeVersion } = this.$store.state.interface
return themeVersion
},
isCustomThemeUsed () {
const { theme } = this.mergedConfig
return theme === 'custom' || theme === null
const { customTheme, customThemeSource } = this.mergedConfig
return customTheme != null || customThemeSource != null
},
isCustomStyleUsed (name) {
const { styleCustomData } = this.mergedConfig
return styleCustomData != null
},
...SharedComputedObject()
},
methods: {
updateFont (key, value) {
console.log(key, value)
this.$store.dispatch('setOption', {
name: 'theme3hacks',
value: {
@ -164,25 +296,120 @@ const AppearanceTab = {
}
})
},
importFile () {
this.fileImporter.importData()
},
importParser (file, filename) {
if (filename.endsWith('.json')) {
return JSON.parse(file)
} else if (filename.endsWith('.piss')) {
return deserialize(file)
}
},
onImport (parsed, filename) {
if (filename.endsWith('.json')) {
this.$store.dispatch('setThemeCustom', parsed.source || parsed.theme)
} else if (filename.endsWith('.piss')) {
this.$store.dispatch('setStyleCustom', parsed)
}
},
onImportFailure (result) {
console.error('Failure importing theme:', result)
this.$store.dispatch('pushGlobalNotice', { messageKey: 'settings.invalid_theme_imported', level: 'error' })
},
importValidator (parsed, filename) {
if (filename.endsWith('.json')) {
const version = parsed._pleroma_theme_version
return version >= 1 || version <= 2
} else if (filename.endsWith('.piss')) {
if (!Array.isArray(parsed)) return false
if (parsed.length < 1) return false
if (parsed.find(x => x.component === '@meta') == null) return false
return true
}
},
isThemeActive (key) {
const { theme } = this.mergedConfig
return key === theme
return key === (this.mergedConfig.theme || this.$store.state.instance.theme)
},
isStyleActive (key) {
return key === (this.mergedConfig.style || this.$store.state.instance.style)
},
isPaletteActive (key) {
return key === (this.mergedConfig.palette || this.$store.state.instance.palette)
},
setStyle (name) {
this.$store.dispatch('setStyle', name)
},
setTheme (name) {
this.$store.dispatch('setTheme', { themeName: name, saveData: true, recompile: true })
this.$store.dispatch('setTheme', name)
},
previewTheme (key, input) {
const style = normalizeThemeData(input)
const x = 2
if (x === 1) return
const theme2 = convertTheme2To3(style)
const theme3 = init({
inputRuleset: theme2,
ultimateBackgroundColor: '#000000',
liteMode: true,
debug: true,
onlyNormalState: true
})
setPalette (name, data) {
this.$store.dispatch('setPalette', name)
this.userPalette = data
},
setPaletteCustom (data) {
this.$store.dispatch('setPaletteCustom', data)
this.userPalette = data
},
resetTheming (name) {
this.$store.dispatch('setStyle', 'stock')
},
previewTheme (key, version, input) {
let theme3
if (this.compilationCache[key]) {
theme3 = this.compilationCache[key]
} else if (input) {
if (version === 'v2') {
const style = normalizeThemeData(input)
const theme2 = convertTheme2To3(style)
theme3 = init({
inputRuleset: theme2,
ultimateBackgroundColor: '#000000',
liteMode: true,
debug: true,
onlyNormalState: true
})
} else if (version === 'v3') {
const palette = input.find(x => x.component === '@palette')
let paletteRule
if (palette) {
const { directives } = palette
directives.link = directives.link || directives.accent
directives.accent = directives.accent || directives.link
paletteRule = {
component: 'Root',
directives: Object.fromEntries(
Object
.entries(directives)
.filter(([k, v]) => k && k !== 'name')
.map(([k, v]) => ['--' + k, 'color | ' + v])
)
}
} else {
paletteRule = null
}
theme3 = init({
inputRuleset: [...input, paletteRule].filter(x => x),
ultimateBackgroundColor: '#000000',
liteMode: true,
debug: true,
onlyNormalState: true
})
}
} else {
theme3 = init({
inputRuleset: [],
ultimateBackgroundColor: '#000000',
liteMode: true,
debug: true,
onlyNormalState: true
})
}
if (!this.compilationCache[key]) {
this.compilationCache[key] = theme3
}
return getScopedVersion(
getCssRules(theme3.eager),

View file

@ -0,0 +1,120 @@
.appearance-tab {
.palette,
.theme-notice {
padding: 0.5em;
margin: 1em;
}
.setting-item {
padding-bottom: 0;
&.heading {
display: grid;
align-items: baseline;
grid-template-columns: 1fr auto auto auto;
grid-gap: 0.5em;
h2 {
flex: 1 0 auto;
}
}
}
.palettes {
display: grid;
grid-template-columns: 1fr 1fr;
grid-gap: 0.5em;
h4,
.unsupported-theme-v2,
.userPalette {
grid-column: 1 / span 2;
}
}
.palette-entry {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 0.5em;
.palette-label label {
text-align: center;
}
.palette-square {
flex: 0 0 auto;
display: inline-block;
min-width: 1em;
min-height: 1em;
}
}
.column-settings {
display: flex;
justify-content: space-evenly;
flex-wrap: wrap;
}
.column-settings .size-label {
display: block;
margin-bottom: 0.5em;
margin-top: 0.5em;
}
.modal-view.-mobile & {
.palette-entry {
flex-wrap: wrap;
justify-content: center;
}
.palette-label {
line-height: 1.5em;
margin-top: 0.5em;
width: 100%;
}
.palette-preview {
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr;
grid-template-rows: 1em 1em;
margin-bottom: 0.5em;
}
}
.theme-list {
list-style: none;
display: flex;
flex-wrap: wrap;
margin: -0.5em 0;
height: 25em;
overflow-x: hidden;
overflow-y: auto;
scrollbar-gutter: stable;
border-radius: var(--roundness);
border: 1px solid var(--border);
padding: 0;
margin-bottom: 1em;
.theme-preview {
font-size: 1rem; // fix for firefox
width: 19rem;
display: flex;
flex-direction: column;
align-items: center;
margin: 0.5em;
&.placeholder {
opacity: 0.2;
}
.theme-preview-container {
pointer-events: none;
zoom: 0.5;
border: none;
border-radius: var(--roundness);
text-align: left;
}
}
}
}

View file

@ -1,49 +1,170 @@
<template>
<div class="appearance-tab" :label="$t('settings.general')">
<div
class="appearance-tab"
:label="$t('settings.general')"
>
<div class="setting-item">
<h2>{{ $t('settings.theme') }}</h2>
<ul
class="theme-list"
ref="themeList"
class="theme-list"
>
<button
class="button-default theme-preview"
data-theme-key="stock"
:class="{ toggled: isStyleActive('stock') }"
@click="resetTheming"
>
<!-- eslint-disable vue/no-v-text-v-html-on-component -->
<!-- eslint-disable vue/no-v-html -->
<component
:is="'style'"
v-html="previewTheme('stock', 'v3')"
/>
<!-- eslint-enable vue/no-v-html -->
<!-- eslint-enable vue/no-v-text-v-html-on-component -->
<preview id="theme-preview-stock" />
<h4 class="theme-name">
{{ $t('settings.style.stock_theme_used') }}
<span class="alert neutral version">v3</span>
</h4>
</button>
<button
v-if="isCustomThemeUsed"
disabled
class="button-default theme-preview"
class="button-default theme-preview toggled"
>
<preview />
<h4 class="theme-name">{{ $t('settings.style.custom_theme_used') }}</h4>
<h4 class="theme-name">
{{ $t('settings.style.custom_theme_used') }}
<span class="alert neutral version">v2</span>
</h4>
</button>
<button
v-if="isCustomStyleUsed"
disabled
class="button-default theme-preview toggled"
>
<preview />
<h4 class="theme-name">
{{ $t('settings.style.custom_style_used') }}
<span class="alert neutral version">v3</span>
</h4>
</button>
<button
v-for="style in availableStyles"
:data-theme-key="style.key"
:key="style.key"
:data-theme-key="style.key"
class="button-default theme-preview"
:class="{ toggled: isThemeActive(style.key) }"
@click="setTheme(style.key)"
:class="{ toggled: isStyleActive(style.key) }"
@click="style.version === 'v2' ? setTheme(style.key) : setStyle(style.key)"
>
<!-- eslint-disable vue/no-v-text-v-html-on-component -->
<component
:is="'style'"
v-if="style.ready || noIntersectionObserver"
v-html="previewTheme(style.key, style.data)"
/>
<!-- eslint-disable vue/no-v-html -->
<div v-if="style.ready || noIntersectionObserver">
<component
:is="'style'"
v-html="previewTheme(style.key, style.version, style.data)"
/>
</div>
<!-- eslint-enable vue/no-v-html -->
<!-- eslint-enable vue/no-v-text-v-html-on-component -->
<preview :class="{ placeholder: ready }" :id="'theme-preview-' + style.key"/>
<h4 class="theme-name">{{ style.name }}</h4>
<preview :id="'theme-preview-' + style.key" />
<h4 class="theme-name">
{{ style.name }}
<span class="alert neutral version">{{ style.version }}</span>
</h4>
</button>
</ul>
</div>
<div class="alert neutral theme-notice">
{{ $t("settings.style.appearance_tab_note") }}
<div class="import-file-container">
<button
class="btn button-default"
@click="importFile"
>
<FAIcon icon="folder-open" />
{{ $t('settings.style.themes3.editor.load_style') }}
</button>
</div>
<div class="setting-item">
<h2>{{ $t('settings.style.themes3.palette.label') }}</h2>
<div class="palettes">
<template v-if="customThemeVersion === 'v3'">
<h4>{{ $t('settings.style.themes3.palette.bundled') }}</h4>
<button
v-for="p in bundledPalettes"
:key="p.name"
class="btn button-default palette-entry"
:class="{ toggled: isPaletteActive(p.key) }"
@click="() => setPalette(p.key, p)"
>
<div class="palette-label">
<label>
{{ p.name }}
</label>
</div>
<div class="palette-preview">
<span
v-for="c in palettesKeys"
:key="c"
class="palette-square"
:style="{ backgroundColor: p[c], border: '1px solid ' + (p[c] ?? 'var(--text)') }"
/>
</div>
</button>
<h4 v-if="stylePalettes?.length > 0">
{{ $t('settings.style.themes3.palette.style') }}
</h4>
<button
v-for="p in stylePalettes || []"
:key="p.name"
class="btn button-default palette-entry"
:class="{ toggled: isPaletteActive(p.key) }"
@click="() => setPalette(p.key, p)"
>
<div class="palette-label">
<label>
{{ p.name ?? $t('settings.style.themes3.palette.user') }}
</label>
</div>
<div class="palette-preview">
<span
v-for="c in palettesKeys"
:key="c"
class="palette-square"
:style="{ backgroundColor: p[c], border: '1px solid ' + (p[c] ?? 'var(--text)') }"
/>
</div>
</button>
<h4 v-if="expertLevel > 0">
{{ $t('settings.style.themes3.palette.user') }}
</h4>
<PaletteEditor
v-if="expertLevel > 0"
v-model="userPalette"
class="userPalette"
:compact="true"
:apply="true"
@applyPalette="data => setPaletteCustom(data)"
/>
</template>
<template v-else-if="customThemeVersion === 'v2'">
<div class="alert neutral theme-notice unsupported-theme-v2">
{{ $t('settings.style.themes3.palette.v2_unsupported') }}
</div>
</template>
</div>
</div>
</div>
<div class="setting-item">
<h2>{{ $t('settings.scale_and_layout') }}</h2>
<div class="alert neutral theme-notice">
{{ $t("settings.style.appearance_tab_note") }}
</div>
<ul class="setting-list">
<li>
<UnitSetting
path="textSize"
step="0.1"
:step="0.1"
:units="['px', 'rem']"
:reset-default="{ 'px': 14, 'rem': 1 }"
timed-apply-mode
@ -60,7 +181,7 @@
<code>px</code>
<code>rem</code>
</i18n-t>
<br/>
<br>
<i18n-t
scope="global"
keypath="settings.text_size_tip2"
@ -119,7 +240,7 @@
<li>
<UnitSetting
path="emojiSize"
step="0.1"
:step="0.1"
:units="['px', 'rem']"
:reset-default="{ 'px': 32, 'rem': 2.2 }"
>
@ -142,7 +263,7 @@
<li>
<UnitSetting
path="navbarSize"
step="0.1"
:step="0.1"
:units="['px', 'rem']"
:reset-default="{ 'px': 55, 'rem': 3.5 }"
>
@ -153,7 +274,7 @@
<li>
<UnitSetting
path="panelHeaderSize"
step="0.1"
:step="0.1"
:units="['px', 'rem']"
:reset-default="{ 'px': 52, 'rem': 3.2 }"
timed-apply-mode
@ -256,58 +377,4 @@
<script src="./appearance_tab.js"></script>
<style lang="scss">
.appearance-tab {
.theme-notice {
padding: 0.5em;
margin: 1em;
}
.column-settings {
display: flex;
justify-content: space-evenly;
flex-wrap: wrap;
}
.column-settings .size-label {
display: block;
margin-bottom: 0.5em;
margin-top: 0.5em;
}
.theme-list {
list-style: none;
display: flex;
flex-wrap: wrap;
margin: -0.5em 0;
height: 25em;
overflow-x: hidden;
overflow-y: auto;
scrollbar-gutter: stable;
border-radius: var(--roundness);
border: 1px solid var(--border);
padding: 0;
.theme-preview {
font-size: 1rem; // fix for firefox
width: 19rem;
display: flex;
flex-direction: column;
align-items: center;
margin: 0.5em;
&.placeholder {
opacity: 0.2;
}
.theme-preview-container {
pointer-events: none;
zoom: 0.5;
border: none;
border-radius: var(--roundness);
text-align: left;
}
}
}
}
</style>
<style lang="scss" src="./appearance_tab.scss"></style>

View file

@ -106,7 +106,7 @@
key="hideScrobblesAfter"
path="hideScrobblesAfter"
:units="['m', 'h', 'd']"
unitSet="time"
unit-set="time"
expert="1"
>
{{ $t('settings.hide_scrobbles_after') }}

View file

@ -86,6 +86,8 @@ const GeneralTab = {
this.$store.dispatch('setOption', { name: 'interfaceLanguage', value: val })
}
},
instanceShoutboxPresent () { return this.$store.state.instance.shoutAvailable },
instanceSpecificPanelPresent () { return this.$store.state.instance.showInstanceSpecificPanel },
...SharedComputedObject()
},
methods: {

View file

@ -217,6 +217,29 @@
{{ $t('settings.no_rich_text_description') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting
path="useAbsoluteTimeFormat"
expert="1"
>
{{ $t('settings.absolute_time_format') }}
</BooleanSetting>
</li>
<ul
v-if="mergedConfig.useAbsoluteTimeFormat"
class="setting-list suboptions"
>
<li>
<UnitSetting
path="absoluteTimeFormatMinAge"
unit-set="time"
:units="['s', 'm', 'h', 'd']"
:min="0"
>
{{ $t('settings.absolute_time_format_min_age') }}
</UnitSetting>
</li>
</ul>
<h3>{{ $t('settings.attachments') }}</h3>
<li>
<BooleanSetting

Some files were not shown because too many files have changed in this diff Show more