Merge branch 'settings-shuffle' into 'develop'

Settings shuffle

See merge request pleroma/pleroma-fe!2186
This commit is contained in:
HJ 2025-12-01 09:01:46 +00:00
commit 0252d39c75
59 changed files with 2614 additions and 1528 deletions

View file

@ -0,0 +1,3 @@
rearranged and split settings to make more sense and be less of a wall of text
on mobile settings now take up full width and presented in navigation style
improved styles for settings

View file

@ -1,18 +1,27 @@
.color-input { .color-input {
display: inline-flex; display: inline-flex;
flex-wrap: wrap;
max-width: 10em;
&.-compact {
max-width: none;
}
.label { .label {
flex: 1 1 auto; flex: 1 1 auto;
grid-area: label;
} }
.opt { .opt {
grid-area: checkbox;
margin-right: 0.5em; margin-right: 0.5em;
} }
&-field.input { &-field.input {
display: inline-flex; flex: 1 1 10em;
flex: 0 0 0; max-width: 10em;
max-width: 9em; grid-area: input;
display: flex;
align-items: stretch; align-items: stretch;
input { input {

View file

@ -1,7 +1,7 @@
<template> <template>
<div <div
class="color-input style-control" class="color-input style-control"
:class="{ disabled: !present || disabled }" :class="{ disabled: !present || disabled, '-compact': compact }"
> >
<label <label
:for="name" :for="name"
@ -127,6 +127,10 @@ export default {
required: false, required: false,
type: Boolean, type: Boolean,
default: false default: false
},
compact: {
required: false,
type: Boolean
} }
}, },
emits: ['update:modelValue'], emits: ['update:modelValue'],

View file

@ -104,6 +104,7 @@
v-model="colorOverride" v-model="colorOverride"
class="input-color-input" class="input-color-input"
fallback="#606060" fallback="#606060"
:compact="true"
:label="$t('settings.style.shadows.color_override')" :label="$t('settings.style.shadows.color_override')"
/> />
</div> </div>

View file

@ -23,6 +23,9 @@
<style lang="scss"> <style lang="scss">
.exporter { .exporter {
display: flex;
flex-direction: column;
&-processing { &-processing {
margin: 0.25em; margin: 0.25em;
} }

View file

@ -1,13 +1,5 @@
<template> <template>
<div class="font-control"> <div class="font-control">
<label
:id="name + '-label'"
:for="manualEntry ? name : name + '-font-switcher'"
class="label"
>
{{ $t('settings.style.themes3.font.label', { label }) }}
</label>
{{ ' ' }}
<Checkbox <Checkbox
v-if="typeof fallback !== 'undefined'" v-if="typeof fallback !== 'undefined'"
:id="name + '-o'" :id="name + '-o'"
@ -15,8 +7,15 @@
:model-value="present" :model-value="present"
@change="$emit('update:modelValue', typeof modelValue === 'undefined' ? fallback : undefined)" @change="$emit('update:modelValue', typeof modelValue === 'undefined' ? fallback : undefined)"
> >
{{ $t('settings.style.themes3.define') }} <i18n-t
scope="global"
keypath="settings.style.fonts.override"
tag="span"
>
{{ label }}
</i18n-t>
</Checkbox> </Checkbox>
{{ ' ' }}
<div <div
v-if="modelValue?.family" v-if="modelValue?.family"
class="font-input" class="font-input"
@ -143,10 +142,6 @@
margin-left: 2em; margin-left: 2em;
margin-top: 0.5em; margin-top: 0.5em;
} }
.font-checkbox {
margin-left: 1em;
}
} }
.invalid-tooltip { .invalid-tooltip {

View file

@ -1,5 +1,5 @@
<template> <template>
<div class="importer"> <div class="importer btn-group">
<form> <form>
<input <input
ref="input" ref="input"
@ -19,7 +19,7 @@
class="btn button-default" class="btn button-default"
@click="submit" @click="submit"
> >
{{ submitButtonLabel || $t('importer.submit') }} {{ submitButtonLabel || $t('importer.import') }}
</button> </button>
<div v-if="success"> <div v-if="success">
<button <button

View file

@ -8,7 +8,7 @@
class="label" class="label"
:class="{ faint: !present || disabled }" :class="{ faint: !present || disabled }"
> >
{{ label }} {{ label || $t('settings.style.themes3.editor.opacity') }}
</label> </label>
<Checkbox <Checkbox
v-if="typeof fallback !== 'undefined'" v-if="typeof fallback !== 'undefined'"

View file

@ -1,8 +1,9 @@
<template> <template>
<div <div
class="PaletteEditor" class="PaletteEditor"
:class="{ '-compact': compact, '-apply': apply }" :class="{ '-compact': compact, '-apply': apply, '-mobile': mobile }"
> >
<div class="palette">
<ColorInput <ColorInput
v-for="key in paletteKeys" v-for="key in paletteKeys"
:key="key" :key="key"
@ -12,6 +13,8 @@
:label="$t('settings.style.themes3.palette.' + key)" :label="$t('settings.style.themes3.palette.' + key)"
@update:model-value="value => updatePalette(key, value)" @update:model-value="value => updatePalette(key, value)"
/> />
</div>
<div class="buttons">
<button <button
class="btn button-default palette-import-button" class="btn button-default palette-import-button"
@click="importPalette" @click="importPalette"
@ -26,6 +29,8 @@
<FAIcon icon="file-export" /> <FAIcon icon="file-export" />
{{ $t('settings.style.themes3.palette.export') }} {{ $t('settings.style.themes3.palette.export') }}
</button> </button>
</div>
<div class="buttons">
<button <button
v-if="apply" v-if="apply"
class="btn button-default palette-apply-button" class="btn button-default palette-apply-button"
@ -36,9 +41,12 @@
{{ $t('settings.style.themes3.palette.apply') }} {{ $t('settings.style.themes3.palette.apply') }}
</button> </button>
</div> </div>
</div>
</template> </template>
<script setup> <script setup>
import { computed } from 'vue'
import ColorInput from 'src/components/color_input/color_input.vue' import ColorInput from 'src/components/color_input/color_input.vue'
import { import {
newImporter, newImporter,
@ -51,6 +59,8 @@ import {
faFileExport faFileExport
} from '@fortawesome/free-solid-svg-icons' } from '@fortawesome/free-solid-svg-icons'
import { useInterfaceStore } from 'src/stores/interface'
library.add( library.add(
faFileImport, faFileImport,
faFileExport faFileExport
@ -104,6 +114,10 @@ const applyPalette = () => {
emit('applyPalette', getExportedObject()) emit('applyPalette', getExportedObject())
} }
const mobile = computed(() => {
return useInterfaceStore().layoutType === 'mobile'
})
const fallback = (key) => { const fallback = (key) => {
if (key === 'accent') { if (key === 'accent') {
return props.modelValue.link return props.modelValue.link
@ -129,13 +143,28 @@ const updatePalette = (paletteKey, value) => {
<style lang="scss"> <style lang="scss">
.PaletteEditor { .PaletteEditor {
display: grid;
justify-content: space-around; justify-content: space-around;
grid-template-columns: repeat(4, 1fr); grid-template-columns: repeat(4, 1fr);
grid-template-rows: repeat(5, 1fr) auto; grid-template-rows: repeat(5, 1fr) auto;
grid-gap: 0.5em; grid-gap: 0.5em;
align-items: baseline; align-items: baseline;
.buttons {
margin-top: 0.5em;
display: grid;
gap: 0.5em
}
.palette {
display: grid;
grid-template-rows: 1fr;
grid-auto-flow: row;
grid-auto-rows: auto;
grid-template-columns: repeat(auto-fill, 10em);
grid-gap: 0.5em;
margin-bottom: 0.5em;
}
.palette-import-button { .palette-import-button {
grid-column: 1 / span 2; grid-column: 1 / span 2;
} }
@ -171,24 +200,22 @@ const updatePalette = (paletteKey, value) => {
grid-column: 1 / span 2; 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;
} }
&.-mobile {
&.-apply { &.-apply {
.palette-apply-button { .palette-apply-button {
grid-column: 1; grid-column: 1 / span 2;
} }
} }
.color-input {
display: grid;
gap: 0.5em;
label {
flex: 1;
}
} }
} }
} }

View file

@ -299,8 +299,8 @@ const Popover = {
if (this.trigger === 'click') { if (this.trigger === 'click') {
document.removeEventListener('click', this.onClickOutside) document.removeEventListener('click', this.onClickOutside)
} }
this.scrollable.removeEventListener('scroll', this.onScroll) this.scrollable?.removeEventListener('scroll', this.onScroll)
this.scrollable.removeEventListener('resize', this.onResize) this.scrollable?.removeEventListener('resize', this.onResize)
}, },
resizePopover () { resizePopover () {
setTimeout(() => { setTimeout(() => {

View file

@ -104,8 +104,8 @@
.visibility-tray { .visibility-tray {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
padding-top: 0.5em;
align-items: baseline; align-items: baseline;
margin-left: -0.5em;
} }
.visibility-notice { .visibility-notice {

View file

@ -14,13 +14,33 @@ library.add(
) )
const ScopeSelector = { const ScopeSelector = {
props: [ props: {
'showAll', showAll: {
'userDefault', required: true,
'originalScope', type: Boolean
'initialScope', },
'onScopeChange' userDefault: {
], required: true,
type: String
},
originalScope: {
required: false,
type: String
},
initialScope: {
required: false,
type: String
},
onScopeChange: {
required: true,
type: Function
},
unstyled: {
required: false,
type: Boolean,
default: true
}
},
data () { data () {
return { return {
currentScope: this.initialScope currentScope: this.initialScope
@ -43,11 +63,12 @@ const ScopeSelector = {
return this.shouldShow('direct') return this.shouldShow('direct')
}, },
css () { css () {
const style = this.unstyled ? 'button-unstyled' : 'button-default'
return { return {
public: { toggled: this.currentScope === 'public' }, public: [style, { toggled: this.currentScope === 'public' }],
unlisted: { toggled: this.currentScope === 'unlisted' }, unlisted: [style, { toggled: this.currentScope === 'unlisted' }],
private: { toggled: this.currentScope === 'private' }, private: [style, { toggled: this.currentScope === 'private' }],
direct: { toggled: this.currentScope === 'direct' } direct: [style, { toggled: this.currentScope === 'direct' }]
} }
} }
}, },

View file

@ -1,11 +1,11 @@
<template> <template>
<div <div
v-if="!showNothing" v-if="!showNothing"
class="ScopeSelector" class="ScopeSelector btn-group"
> >
<button <button
v-if="showDirect" v-if="showDirect"
class="button-unstyled scope" class="scope"
:class="css.direct" :class="css.direct"
:title="$t('post_status.scope.direct')" :title="$t('post_status.scope.direct')"
type="button" type="button"
@ -19,7 +19,7 @@
{{ ' ' }} {{ ' ' }}
<button <button
v-if="showPrivate" v-if="showPrivate"
class="button-unstyled scope" class="scope"
:class="css.private" :class="css.private"
:title="$t('post_status.scope.private')" :title="$t('post_status.scope.private')"
type="button" type="button"
@ -33,7 +33,7 @@
{{ ' ' }} {{ ' ' }}
<button <button
v-if="showUnlisted" v-if="showUnlisted"
class="button-unstyled scope" class="scope"
:class="css.unlisted" :class="css.unlisted"
:title="$t('post_status.scope.unlisted')" :title="$t('post_status.scope.unlisted')"
type="button" type="button"
@ -47,7 +47,7 @@
{{ ' ' }} {{ ' ' }}
<button <button
v-if="showPublic" v-if="showPublic"
class="button-unstyled scope" class="scope"
:class="css.public" :class="css.public"
:title="$t('post_status.scope.public')" :title="$t('post_status.scope.public')"
type="button" type="button"
@ -65,12 +65,14 @@
<style lang="scss"> <style lang="scss">
.ScopeSelector { .ScopeSelector {
display: inline-block;
.scope { .scope {
display: inline-block; display: inline-block;
cursor: pointer;
min-width: 1.3em; min-width: 1.3em;
min-height: 1.3em; min-height: 1.3em;
text-align: center; text-align: center;
padding: 0.5em 0.25em
} }
} }
</style> </style>

View file

@ -0,0 +1,213 @@
// eslint-disable-next-line no-unused
import { h, Fragment } from 'vue'
import { mapState } from 'pinia'
import { throttle } from 'lodash'
import { mapState as mapPiniaState } from 'pinia'
import { FontAwesomeIcon as FAIcon } from '@fortawesome/vue-fontawesome'
import './vertical_tab_switcher.scss'
import { useInterfaceStore } from 'src/stores/interface'
const findFirstUsable = (slots) => slots.findIndex(_ => _.props)
export default {
name: 'VerticalTabSwitcher',
props: {
renderOnlyFocused: {
required: false,
type: Boolean,
default: false
},
onSwitch: {
required: false,
type: Function,
default: undefined
},
activeTab: {
required: false,
type: String,
default: undefined
},
bodyScrollLock: {
required: false,
type: Boolean,
default: false
},
parentCollapsed: {
required: false,
type: Boolean,
default: null
}
},
data () {
return {
active: findFirstUsable(this.slots()),
resizeHandler: null,
navSide: 'tabs',
}
},
computed: {
activeIndex () {
// In case of controlled component
if (this.activeTab) {
return this.slots().findIndex(slot => slot && slot.props && this.activeTab === slot.props.key)
} else {
return this.active
}
},
isActive () {
return tabName => {
const isWanted = slot => slot.props && slot.props['data-tab-name'] === tabName
return this.$slots.default().findIndex(isWanted) === this.activeIndex
}
},
...mapPiniaState(useInterfaceStore, {
mobileLayout: store => store.layoutType === 'mobile'
}),
},
beforeUpdate () {
const currentSlot = this.slots()[this.active]
if (!currentSlot.props) {
this.active = findFirstUsable(this.slots())
}
},
methods: {
clickTab (index) {
return (e) => {
e.preventDefault()
this.setTab(index)
}
},
setTab (index) {
if (typeof this.onSwitch === 'function') {
this.onSwitch.call(null, this.slots()[index].key)
}
this.active = index
this.changeNavSide('content')
},
changeNavSide (side) {
if (this.navSide !== side) {
this.navSide = side
}
},
// DO NOT put it to computed, it doesn't work (caching?)
slots () {
if (this.$slots.default()[0].type === Fragment) {
return this.$slots.default()[0].children
}
return this.$slots.default()
}
},
render () {
const tabs = this.slots()
.map((slot, index) => {
const props = slot.props
if (!props) return
const classesTab = ['vertical-tab', 'menu-item']
if (this.activeIndex === index && useInterfaceStore().layoutType !== 'mobile') {
classesTab.push('-active')
}
return (
<button
disabled={props.disabled}
onClick={this.clickTab(index)}
class={classesTab.join(' ')}
type="button"
role="tab"
title={props.label}
>
{!props.icon ? '' : (<FAIcon class="tab-icon" size="1x" fixed-width icon={props.icon}/>)}
<span class="text">
{props.label}
</span>
</button>
)
})
const contents = this.slots().map((slot, index) => {
const props = slot.props
if (!props) return
const active = this.activeIndex === index
let delayRender = slot.props['delay-render']
if (delayRender && active) {
slot.props['delay-render'] = false
delayRender = false
}
const renderSlot = (!delayRender && (!this.renderOnlyFocused || active))
? slot
: ''
const headerClasses = ['tab-content-label']
const header = (
<h2 class={headerClasses}>
<button
type="button"
onClick={() => this.changeNavSide('tabs')}
title={this.$t('nav.back')}
class="button-unstyled"
>
<FAIcon
size="lg"
class="back-button-icon"
icon="chevron-left"
/>
</button>
{props.label}
</h2>
)
const wrapperClasses = ['tab-content-wrapper', active ? '-active' : '-hidden' ]
const contentClasses = ['tab-content']
if (props['full-width']) {
contentClasses.push('-full-width')
}
if (props['full-height']) {
contentClasses.push('-full-height')
}
return (
<div class={wrapperClasses} >
<div class="tab-mobile-header">
{header}
</div>
<div class="tab-slot-wrapper">
<div class={contentClasses} >
{renderSlot}
</div>
</div>
</div>
)
})
const rootClasses = ['vertical-tab-switcher']
if (useInterfaceStore().layoutType === 'mobile') {
rootClasses.push('-mobile')
}
if (this.navSide === 'tabs') {
rootClasses.push('-nav-tabs')
} else {
rootClasses.push('-nav-contents')
}
return (
<div ref="root" class={ rootClasses.join(' ') }>
<div
class="tabs"
role="tablist"
ref="nav"
>
{tabs}
</div>
<div
role="tabpanel"
class={'contents' + (this.scrollableTabs ? ' scrollable-tabs' : '')}
v-body-scroll-lock={this.bodyScrollLock}
ref="contents"
>
{contents}
</div>
</div>
)
}
}

View file

@ -0,0 +1,181 @@
.vertical-tab-switcher {
display: flex;
flex-direction: row;
container-type: inline-size;
> .tabs {
flex: 0 0 15em;
flex-direction: column;
overflow: hidden auto;
white-space: nowrap;
text-overflow: ellipsis;
width: 15em;
min-width: 15em;
border-right: 1px solid;
border-right-color: var(--border);
box-sizing: border-box;
> .menu-item {
padding: 0.5em 1em;
.tab-icon {
vertical-align: middle;
margin-right: 0.75em;
}
}
}
> .contents {
flex: 1 0 35em;
.tab-content {
align-self: center;
&:not(.-full-width) {
max-width: 40em;
}
&.-full-width {
align-self: stretch;
}
&.-full-height {
flex: 1;
}
}
.tab-content-label {
box-sizing: border-box;
margin: 0;
border-bottom: 1px solid var(--border);
display: none;
button {
box-sizing: border-box;
padding: 0.5em;
}
}
.tab-slot-wrapper {
flex: 1 1 auto;
height: 100%;
overflow-y: auto;
display: flex;
flex-direction: column;
}
.tab-content-wrapper {
display: flex;
flex-direction: column;
height: 100%;
flex: 1 1 auto;
&.-hidden {
display: none;
}
}
}
&.-mobile {
> .contents {
.tab-content-label {
display: block
}
}
&.-nav-contents {
> .contents {
display: block;
flex-grow: 1;
flex-shrink: 1;
}
> .tabs {
display: none;
flex-grow: 0;
flex-shrink: 1;
}
}
&.-nav-tabs {
> .tabs {
display: block;
flex-grow: 1;
}
> .contents {
display: none;
flex-grow: 0;
flex-shrink: 1;
}
}
}
@supports (container-type: inline-size) {
&,
&.-mobile {
&.-nav-contents,
&.-nav-tabs {
/* I THINK it's a false positive and eslint doesn't understand the @-rule */
/* stylelint-disable no-descending-specificity */
> .contents {
display: block;
flex-grow: 1;
}
> .tabs {
display: block;
flex-grow: 0;
}
/* stylelint-enable no-descending-specificity */
}
}
@container (width < 50em) {
> .contents {
.tab-content-label {
display: block
}
}
&.-mobile {
> .contents {
.tab-content-label {
display: block
}
}
}
&,
&.-mobile {
&.-nav-contents {
> .contents {
display: block;
flex-grow: 1;
}
> .tabs {
display: none;
flex-grow: 0;
flex-shrink: 1;
}
}
&.-nav-tabs {
/* stylelint-disable no-descending-specificity */
> .tabs {
display: block;
flex-grow: 1;
}
> .contents {
display: none;
flex-grow: 0;
flex-shrink: 1;
}
/* stylelint-enable no-descending-specificity */
}
}
}
}
}

View file

@ -1,21 +1,48 @@
.settings-modal { .settings-modal {
overflow: hidden; overflow: hidden;
h4 { h2 {
font-size: 1.3rem;
font-weight: 500;
margin-top: 1em;
margin-bottom: 1em;
}
h3 {
font-size: 1.2rem;
font-weight: 500;
margin-top: 1em;
margin-bottom: 0.5em; margin-bottom: 0.5em;
border-bottom: 1px solid var(--border);
padding-bottom: 0.25em;
box-sizing: border-box;
padding-left: 0.5em;
}
h4 {
font-size: 1.1rem;
margin-top: 1em;
margin-bottom: 0.5em;
}
h5 {
font-size: 1rem;
margin-bottom: 0.5em;
margin-top: 0;
} }
.setting-list, .setting-list,
.option-list { .option-list {
list-style-type: none; list-style-type: none;
padding-left: 2em; padding-left: 2em;
margin: 0;
.btn:not(.dropdown-button) { .btn:not(.dropdown-button) {
padding: 0 2em; padding: 0 2em;
} }
li { li {
margin-bottom: 0.5em; margin: 1em 0;
} }
.suboptions { .suboptions {
@ -23,9 +50,11 @@
} }
&.two-column { &.two-column {
column-count: 2; display: grid;
grid-template-columns: 1fr 1fr;
> li { > li {
margin: 0;
break-inside: avoid; break-inside: avoid;
} }
} }
@ -42,7 +71,7 @@
transition: transform; transition: transform;
transition-timing-function: ease-in-out; transition-timing-function: ease-in-out;
transition-duration: 300ms; transition-duration: 300ms;
width: 1000px; width: 70em;
max-width: 90vw; max-width: 90vw;
height: 90vh; height: 90vh;
@ -77,18 +106,30 @@
} }
&.-mobile { &.-mobile {
.setting-list, .tabs {
.menu-item {
font-size: 1.2em;
padding-top: 0.75em;
padding-bottom: 0.75em;
}
}
.setting-list:not(.suboptions),
.option-list { .option-list {
padding-left: 0.25em; padding-left: 0.25em;
/* stylelint-disable no-descending-specificity */
// it makes no sense
> li { > li {
margin: 1em 0; margin: 1em 0;
line-height: 1.5em; line-height: 1.5em;
vertical-align: middle; vertical-align: middle;
} }
/* stylelint-enable no-descending-specificity */
&.two-column { &.two-column {
column-count: 1; grid-template-columns: 1fr;
} }
} }
} }

View file

@ -1,4 +1,4 @@
import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx' import VerticalTabSwitcher from './helpers/vertical_tab_switcher.jsx'
import InstanceTab from './admin_tabs/instance_tab.vue' import InstanceTab from './admin_tabs/instance_tab.vue'
import LimitsTab from './admin_tabs/limits_tab.vue' import LimitsTab from './admin_tabs/limits_tab.vue'
@ -31,7 +31,7 @@ library.add(
const SettingsModalAdminContent = { const SettingsModalAdminContent = {
components: { components: {
TabSwitcher, VerticalTabSwitcher,
InstanceTab, InstanceTab,
LimitsTab, LimitsTab,

View file

@ -1,5 +1,5 @@
<template> <template>
<tab-switcher <vertical-tab-switcher
v-if="adminDescriptionsLoaded && (noDb || adminDbLoaded)" v-if="adminDescriptionsLoaded && (noDb || adminDbLoaded)"
ref="tabSwitcher" ref="tabSwitcher"
class="settings_tab-switcher" class="settings_tab-switcher"
@ -71,7 +71,7 @@
> >
<EmojiTab /> <EmojiTab />
</div> </div>
</tab-switcher> </vertical-tab-switcher>
</template> </template>
<script src="./settings_modal_admin_content.js"></script> <script src="./settings_modal_admin_content.js"></script>

View file

@ -1,4 +1,4 @@
import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx' import VerticalTabSwitcher from './helpers/vertical_tab_switcher.jsx'
import DataImportExportTab from './tabs/data_import_export_tab.vue' import DataImportExportTab from './tabs/data_import_export_tab.vue'
import MutesAndBlocksTab from './tabs/mutes_and_blocks_tab.vue' import MutesAndBlocksTab from './tabs/mutes_and_blocks_tab.vue'
@ -7,42 +7,54 @@ import FilteringTab from './tabs/filtering_tab.vue'
import SecurityTab from './tabs/security_tab/security_tab.vue' import SecurityTab from './tabs/security_tab/security_tab.vue'
import ProfileTab from './tabs/profile_tab.vue' import ProfileTab from './tabs/profile_tab.vue'
import GeneralTab from './tabs/general_tab.vue' import GeneralTab from './tabs/general_tab.vue'
import PostsTab from './tabs/posts_tab.vue'
import ComposingTab from './tabs/composing_tab.vue'
import ClutterTab from './tabs/clutter_tab.vue'
import LayoutTab from './tabs/layout_tab.vue'
import AppearanceTab from './tabs/appearance_tab.vue' import AppearanceTab from './tabs/appearance_tab.vue'
import VersionTab from './tabs/version_tab.vue' import DeveloperTab from './tabs/developer_tab.vue'
import ThemeTab from './tabs/theme_tab/theme_tab.vue' import OldThemeTab from './tabs/old_theme_tab/old_theme_tab.vue'
import StyleTab from './tabs/style_tab/style_tab.vue' import StyleTab from './tabs/style_tab/style_tab.vue'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { import {
faWrench, faWrench,
faUser, faUser,
faMessage,
faFilter, faFilter,
faPaintBrush, faPaintBrush,
faPalette, faPalette,
faBell, faBell,
faDownload, faDownload,
faEyeSlash, faEyeSlash,
faInfo, faWindowRestore,
faWindowRestore faCode,
faBroom,
faLock,
faColumns
} from '@fortawesome/free-solid-svg-icons' } from '@fortawesome/free-solid-svg-icons'
import { useInterfaceStore } from 'src/stores/interface' import { useInterfaceStore } from 'src/stores/interface'
library.add( library.add(
faWrench, faWrench,
faUser, faUser,
faFilter, faMessage,
faPaintBrush, faWindowRestore,
faPalette, faColumns,
faBell, faBell,
faDownload, faFilter,
faEyeSlash, faEyeSlash,
faInfo, faBroom,
faWindowRestore faLock,
faDownload,
faPalette,
faPaintBrush,
faCode
) )
const SettingsModalContent = { const SettingsModalContent = {
components: { components: {
TabSwitcher, VerticalTabSwitcher,
DataImportExportTab, DataImportExportTab,
MutesAndBlocksTab, MutesAndBlocksTab,
@ -51,10 +63,14 @@ const SettingsModalContent = {
SecurityTab, SecurityTab,
ProfileTab, ProfileTab,
GeneralTab, GeneralTab,
PostsTab,
ComposingTab,
ClutterTab,
LayoutTab,
AppearanceTab, AppearanceTab,
StyleTab, StyleTab,
VersionTab, DeveloperTab,
ThemeTab OldThemeTab
}, },
computed: { computed: {
isLoggedIn () { isLoggedIn () {
@ -68,9 +84,12 @@ const SettingsModalContent = {
}, },
expertLevel () { expertLevel () {
return this.$store.state.config.expertLevel return this.$store.state.config.expertLevel
}
}, },
isMobileLayout () { data () {
return useInterfaceStore().layoutType === 'mobile' return {
navCollapsed: false,
navHideHeader: false
} }
}, },
methods: { methods: {

View file

@ -1,23 +1,11 @@
.settings_tab-switcher { .settings_tab-switcher {
height: 100%; height: 100%;
h1 { [full-height="true"] {
margin-bottom: 0.5em; height: 100%
margin-top: 0.5em;
}
h4 {
margin-bottom: 0;
margin-top: 0.25em;
}
h5 {
margin-bottom: 0;
margin-top: 0.25em;
} }
.setting-item { .setting-item {
border-bottom: 2px solid var(--border);
margin: 1em 1em 1.4em; margin: 1em 1em 1.4em;
padding-bottom: 1.4em; padding-bottom: 1.4em;

View file

@ -1,10 +1,11 @@
<template> <template>
<tab-switcher <vertical-tab-switcher
ref="tabSwitcher" ref="tabSwitcher"
class="settings_tab-switcher" class="settings_tab-switcher"
:side-tab-bar="true"
:scrollable-tabs="true" :scrollable-tabs="true"
:child-collapsed="childCollapsed"
:body-scroll-lock="bodyLock" :body-scroll-lock="bodyLock"
:hide-header="navHideHeader"
> >
<div <div
:label="$t('settings.general')" :label="$t('settings.general')"
@ -14,6 +15,32 @@
<GeneralTab /> <GeneralTab />
</div> </div>
<div <div
v-if="isLoggedIn"
:label="$t('settings.profile_tab')"
icon="user"
data-tab-name="profile"
:full-width="true"
>
<ProfileTab />
</div>
<div
:label="$t('settings.composing')"
icon="pen-alt"
data-tab-name="composing"
:delay-render="true"
>
<ComposingTab />
</div>
<div
:label="$t('settings.posts')"
icon="message"
data-tab-name="posts"
:delay-render="true"
>
<PostsTab />
</div>
<div
:full-width="true"
:label="$t('settings.appearance')" :label="$t('settings.appearance')"
icon="window-restore" icon="window-restore"
data-tab-name="appearance" data-tab-name="appearance"
@ -22,39 +49,48 @@
<AppearanceTab /> <AppearanceTab />
</div> </div>
<div <div
v-if="expertLevel > 0" :full-width="true"
:label="$t('settings.style.themes3.editor.title')" :label="$t('settings.layout')"
icon="palette" icon="table-columns"
data-tab-name="style" data-tab-name="layout"
:delay-render="true" :delay-render="true"
> >
<StyleTab /> <LayoutTab />
</div>
<div
v-if="expertLevel > 0"
:label="$t('settings.theme_old')"
icon="paint-brush"
data-tab-name="theme"
:delay-render="true"
>
<ThemeTab />
</div>
<div
v-if="isLoggedIn"
:label="$t('settings.profile_tab')"
icon="user"
data-tab-name="profile"
>
<ProfileTab />
</div> </div>
<div <div
v-if="isLoggedIn" v-if="isLoggedIn"
:full-width="true"
:label="$t('settings.notifications')" :label="$t('settings.notifications')"
icon="bell" icon="bell"
data-tab-name="notifications" data-tab-name="notifications"
> >
<NotificationsTab /> <NotificationsTab />
</div> </div>
<div
:label="$t('settings.filtering')"
:full-width="true"
icon="filter"
data-tab-name="filtering"
>
<FilteringTab />
</div>
<div
v-if="isLoggedIn"
:label="$t('settings.mutes_and_blocks')"
icon="eye-slash"
data-tab-name="mutesAndBlocks"
:full-width="true"
:full-height="true"
>
<MutesAndBlocksTab />
</div>
<div
:label="$t('settings.clutter')"
icon="broom"
data-tab-name="clutter"
>
<ClutterTab />
</div>
<div <div
v-if="isLoggedIn" v-if="isLoggedIn"
:label="$t('settings.security_tab')" :label="$t('settings.security_tab')"
@ -63,22 +99,6 @@
> >
<SecurityTab /> <SecurityTab />
</div> </div>
<div
:label="$t('settings.filtering')"
icon="filter"
data-tab-name="filtering"
>
<FilteringTab />
</div>
<div
v-if="isLoggedIn"
:label="$t('settings.mutes_and_blocks')"
:fullHeight="true"
icon="eye-slash"
data-tab-name="mutesAndBlocks"
>
<MutesAndBlocksTab />
</div>
<div <div
v-if="isLoggedIn" v-if="isLoggedIn"
:label="$t('settings.data_import_export_tab')" :label="$t('settings.data_import_export_tab')"
@ -88,13 +108,34 @@
<DataImportExportTab /> <DataImportExportTab />
</div> </div>
<div <div
:label="$t('settings.version.title')" v-if="expertLevel > 0"
icon="info" :label="$t('settings.style.themes3.editor.title')"
data-tab-name="version" icon="palette"
data-tab-name="style"
:delay-render="true"
:full-width="true"
> >
<VersionTab /> <StyleTab />
</div> </div>
</tab-switcher> <div
v-if="expertLevel > 0"
:label="$t('settings.theme_old')"
icon="paint-brush"
data-tab-name="theme"
:delay-render="true"
:full-width="true"
>
<OldThemeTab />
</div>
<div
v-if="expertLevel > 0"
:label="$t('settings.developer')"
icon="code"
data-tab-name="developer"
>
<DeveloperTab />
</div>
</vertical-tab-switcher>
</template> </template>
<script src="./settings_modal_user_content.js"></script> <script src="./settings_modal_user_content.js"></script>

View file

@ -3,10 +3,8 @@ import ChoiceSetting from '../helpers/choice_setting.vue'
import IntegerSetting from '../helpers/integer_setting.vue' import IntegerSetting from '../helpers/integer_setting.vue'
import FloatSetting from '../helpers/float_setting.vue' import FloatSetting from '../helpers/float_setting.vue'
import UnitSetting from '../helpers/unit_setting.vue' import UnitSetting from '../helpers/unit_setting.vue'
import { defaultHorizontalUnits } from '../helpers/unit_setting.js'
import PaletteEditor from 'src/components/palette_editor/palette_editor.vue' import PaletteEditor from 'src/components/palette_editor/palette_editor.vue'
import Preview from './theme_tab/theme_preview.vue' import Preview from './old_theme_tab/theme_preview.vue'
import FontControl from 'src/components/font_control/font_control.vue'
import { newImporter } from 'src/services/export_import/export_import.js' import { newImporter } from 'src/services/export_import/export_import.js'
import { convertTheme2To3 } from 'src/services/theme_data/theme2_to_theme3.js' import { convertTheme2To3 } from 'src/services/theme_data/theme2_to_theme3.js'
@ -24,15 +22,6 @@ import ProfileSettingIndicator from '../helpers/profile_setting_indicator.vue'
import { mapActions } from 'pinia' import { mapActions } from 'pinia'
import { useInterfaceStore, normalizeThemeData } from 'src/stores/interface' import { useInterfaceStore, normalizeThemeData } from 'src/stores/interface'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faGlobe
} from '@fortawesome/free-solid-svg-icons'
library.add(
faGlobe
)
const AppearanceTab = { const AppearanceTab = {
data () { data () {
return { return {
@ -59,11 +48,6 @@ const AppearanceTab = {
], ],
userPalette: {}, userPalette: {},
intersectionObserver: null, intersectionObserver: null,
thirdColumnModeOptions: ['none', 'notifications', 'postform'].map(mode => ({
key: mode,
value: mode,
label: this.$t(`settings.third_column_mode_${mode}`)
})),
forcedRoundnessOptions: ['disabled', 'sharp', 'nonsharp', 'round'].map((mode, i) => ({ forcedRoundnessOptions: ['disabled', 'sharp', 'nonsharp', 'round'].map((mode, i) => ({
key: mode, key: mode,
value: i - 1, value: i - 1,
@ -86,7 +70,6 @@ const AppearanceTab = {
FloatSetting, FloatSetting,
UnitSetting, UnitSetting,
ProfileSettingIndicator, ProfileSettingIndicator,
FontControl,
Preview, Preview,
PaletteEditor PaletteEditor
}, },
@ -253,33 +236,14 @@ const AppearanceTab = {
noIntersectionObserver () { noIntersectionObserver () {
return !window.IntersectionObserver return !window.IntersectionObserver
}, },
horizontalUnits () { instanceWallpaper () {
return defaultHorizontalUnits console.log(this.$store.state.instance.background)
}, this.$store.state.instance.background
fontsOverride () {
return this.$store.getters.mergedConfig.fontsOverride
},
columns () {
const mode = this.$store.getters.mergedConfig.thirdColumnMode
const notif = mode === 'none' ? [] : ['notifs']
if (this.$store.getters.mergedConfig.sidebarRight || mode === 'postform') {
return [...notif, 'content', 'sidebar']
} else {
return ['sidebar', 'content', ...notif]
}
}, },
instanceWallpaperUsed () { instanceWallpaperUsed () {
return this.$store.state.instance.background && return this.$store.state.instance.background &&
!this.$store.state.users.currentUser.background_image !this.$store.state.users.currentUser.background_image
}, },
language: {
get: function () { return this.$store.getters.mergedConfig.interfaceLanguage },
set: function (val) {
this.$store.dispatch('setOption', { name: 'interfaceLanguage', value: val })
}
},
customThemeVersion () { customThemeVersion () {
const { themeVersion } = useInterfaceStore() const { themeVersion } = useInterfaceStore()
return themeVersion return themeVersion
@ -295,18 +259,6 @@ const AppearanceTab = {
...SharedComputedObject() ...SharedComputedObject()
}, },
methods: { methods: {
updateFont (key, value) {
this.$store.dispatch('setOption', {
name: 'theme3hacks',
value: {
...this.mergedConfig.theme3hacks,
fonts: {
...this.mergedConfig.theme3hacks.fonts,
[key]: value
}
}
})
},
importFile () { importFile () {
this.fileImporter.importData() this.fileImporter.importData()
}, },
@ -463,6 +415,9 @@ const AppearanceTab = {
this.submitBackground('') this.submitBackground('')
} }
}, },
resetUploadedBackground () {
this.backgroundPreview = null
},
submitBackground (background) { submitBackground (background) {
if (!this.backgroundPreview && background !== '') { return } if (!this.backgroundPreview && background !== '') { return }

View file

@ -1,27 +1,16 @@
.appearance-tab { .appearance-tab {
h3 {
border: none
}
.palette, .palette,
.theme-notice { .theme-notice {
padding: 0.5em; padding: 0.5em;
margin: 1em;
} }
.setting-item { .theme-name {
padding-bottom: 0; font-weight: 900;
padding-bottom: 0.5em;
&.heading {
display: grid;
align-items: baseline;
grid-template-columns: 1fr auto auto auto;
grid-gap: 0.5em;
h2 {
flex: 1 0 auto;
}
}
}
h4 {
margin: 0.5em 0;
} }
input[type="file"] { input[type="file"] {
@ -29,7 +18,31 @@
height: auto; height: auto;
} }
.banner-background {
display: flex;
gap: 1em;
flex-wrap: wrap;
h4 {
margin: 0;
}
}
.banner-background-input {
flex: 1;
display: flex;
flex-direction: column;
gap: 0.5em;
.custom-bg-control {
display: grid;
gap: 0.5em;
grid-template-columns: 1fr 1fr;
}
}
.banner-background-preview { .banner-background-preview {
display: flex;
max-width: 100%; max-width: 100%;
width: 300px; width: 300px;
position: relative; position: relative;
@ -37,32 +50,85 @@
img { img {
width: 100%; width: 100%;
} }
.fun-monitor {
position: relative;
pointer-events: none;
display: flex;
flex-direction: column;
align-items: center;
* {
line-height: 1;
} }
.reset-button { &-display-bezel,
&-display-screen {
aspect-ratio: 16 / 9;
width: 16em;
}
img {
object-fit: cover;
}
.wallpaper {
position: absolute; position: absolute;
top: 0.2em; inset: 0;
right: 0.2em; background-color: var(--wallpaper);
border-radius: var(--roundness); }
&-display-uploading {
position: absolute;
inset: 0;
z-index: 1;
display: flex;
place-items: center;
place-content: center;
background-color: rgb(0 0 0 / 60%); background-color: rgb(0 0 0 / 60%);
opacity: 0.7; font-size: 4em;
width: 1.5em;
height: 1.5em;
text-align: center;
line-height: 1.5em;
font-size: 1.5em;
cursor: pointer;
&:hover {
opacity: 1;
} }
svg { &-display-screen {
color: white; padding: 0;
overflow: hidden;
position: relative;
&-overlay {
background: transparent;
position: absolute;
inset: 0;
z-index: 2;
}
&-image {
aspect-ratio: 16 / 9
} }
} }
&-display-bezel {
padding: 1em;
margin: 0;
order: 1;
z-index: 3;
}
&-neck {
width: 5em;
height: 3em;
margin-top: -1em;
margin-bottom: -0.5em;
order: 2
}
&-stand {
width: 8em;
height: 1em;
order: 3;
z-index: 1
}
}
}
.palettes-container { .palettes-container {
height: 15em; height: 15em;
@ -70,7 +136,8 @@
scrollbar-gutter: stable; scrollbar-gutter: stable;
border-radius: var(--roundness); border-radius: var(--roundness);
border: 1px solid var(--border); border: 1px solid var(--border);
margin: -0.5em; margin-bottom: 0.5em;
margin-top: 0;
} }
.palettes { .palettes {
@ -80,9 +147,9 @@
padding: 0.5em; padding: 0.5em;
width: 100%; width: 100%;
h4 { h5 {
margin: 0;
grid-column: 1 / span 2; grid-column: 1 / span 2;
margin-bottom: 0;
} }
} }
@ -160,7 +227,7 @@
.theme-preview { .theme-preview {
font-size: 1rem; // fix for firefox font-size: 1rem; // fix for firefox
width: 19rem; width: 14rem;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;

View file

@ -1,10 +1,15 @@
<template> <template>
<div <div
class="appearance-tab" class="appearance-tab"
:label="$t('settings.general')" :label="$t('settings.interface')"
icon="table-columns"
> >
<div class="setting-item"> <div
<h2>{{ $t('settings.theme') }}</h2> class="setting-item"
:label="$t('settings.theme')"
icon="paintbrush"
>
<h3>{{ $t('settings.style.style_section') }}</h3>
<ul <ul
ref="themeList" ref="themeList"
class="theme-list" class="theme-list"
@ -17,10 +22,10 @@
@click="resetTheming" @click="resetTheming"
> >
<preview id="theme-preview-stock" /> <preview id="theme-preview-stock" />
<h4 class="theme-name"> <span class="theme-name">
{{ $t('settings.style.stock_theme_used') }} {{ $t('settings.style.stock_theme_used') }}
<span class="alert neutral version">v3</span> <span class="alert neutral version">v3</span>
</h4> </span>
</button> </button>
<button <button
v-if="isCustomThemeUsed" v-if="isCustomThemeUsed"
@ -28,10 +33,10 @@
class="button-default theme-preview toggled" class="button-default theme-preview toggled"
> >
<preview /> <preview />
<h4 class="theme-name"> <span class="theme-name">
{{ $t('settings.style.custom_theme_used') }} {{ $t('settings.style.custom_theme_used') }}
<span class="alert neutral version">v2</span> <span class="alert neutral version">v2</span>
</h4> </span>
</button> </button>
<button <button
v-if="isCustomStyleUsed" v-if="isCustomStyleUsed"
@ -39,10 +44,10 @@
class="button-default theme-preview toggled" class="button-default theme-preview toggled"
> >
<preview /> <preview />
<h4 class="theme-name"> <span class="theme-name">
{{ $t('settings.style.custom_style_used') }} {{ $t('settings.style.custom_style_used') }}
<span class="alert neutral version">v3</span> <span class="alert neutral version">v3</span>
</h4> </span>
</button> </button>
<button <button
v-for="style in availableStyles" v-for="style in availableStyles"
@ -54,10 +59,10 @@
@click="style.version === 'v2' ? setTheme(style.key) : setStyle(style.key)" @click="style.version === 'v2' ? setTheme(style.key) : setStyle(style.key)"
> >
<preview :id="'theme-preview-' + style.key" /> <preview :id="'theme-preview-' + style.key" />
<h4 class="theme-name"> <span class="theme-name">
{{ style.name }} {{ style.name }}
<span class="alert neutral version">{{ style.version }}</span> <span class="alert neutral version">{{ style.version }}</span>
</h4> </span>
</button> </button>
</ul> </ul>
<div class="import-file-container"> <div class="import-file-container">
@ -70,16 +75,14 @@
<FAIcon icon="folder-open" /> <FAIcon icon="folder-open" />
{{ $t('settings.style.themes3.editor.load_style') }} {{ $t('settings.style.themes3.editor.load_style') }}
</button> </button>
</div> <h4>{{ $t('settings.style.themes3.palette.label') }}</h4>
<div class="setting-item">
<h2>{{ $t('settings.style.themes3.palette.label') }}</h2>
<div <div
v-if="customThemeVersion === 'v3'" v-if="customThemeVersion === 'v3'"
class="palettes-container" class="palettes-container"
> >
<h4 v-if="stylePalettes?.length > 0"> <h5 v-if="stylePalettes?.length > 0">
{{ $t('settings.style.themes3.palette.style') }} {{ $t('settings.style.themes3.palette.style') }}
</h4> </h5>
<div class="palettes"> <div class="palettes">
<button <button
v-for="p in stylePalettes || []" v-for="p in stylePalettes || []"
@ -103,7 +106,7 @@
/> />
</div> </div>
</button> </button>
<h4>{{ $t('settings.style.themes3.palette.bundled') }}</h4> <h5>{{ $t('settings.style.themes3.palette.bundled') }}</h5>
<button <button
v-for="p in bundledPalettes" v-for="p in bundledPalettes"
:key="p.name" :key="p.name"
@ -130,9 +133,9 @@
</div> </div>
<div> <div>
<template v-if="customThemeVersion === 'v3'"> <template v-if="customThemeVersion === 'v3'">
<h4 v-if="expertLevel > 0"> <h5 v-if="expertLevel > 0">
{{ $t('settings.style.themes3.palette.user') }} {{ $t('settings.style.themes3.palette.user') }}
</h4> </h5>
<PaletteEditor <PaletteEditor
v-if="expertLevel > 0" v-if="expertLevel > 0"
v-model="userPalette" v-model="userPalette"
@ -150,236 +153,73 @@
</template> </template>
</div> </div>
</div> </div>
</div> <h3>{{ $t('settings.background') }}</h3>
<div class="setting-item"> <div class="banner-background">
<h2>{{ $t('settings.background') }}</h2>
<div class="banner-background-preview"> <div class="banner-background-preview">
<img :src="user.background_image"> <div class="fun-monitor">
<button <div class="fun-monitor-stand button-default" />
v-if="!isDefaultBackground" <div class="fun-monitor-neck button-default" />
class="button-unstyled reset-button" <div class="fun-monitor-display-bezel button-default">
:title="$t('settings.reset_profile_background')" <div class="fun-monitor-display-screen input">
@click="resetBackground" <img
v-if="backgroundPreview || user.background_image || instanceWallpaper"
class="fun-monitor-display-screen-image"
:src="backgroundPreview || user.background_image || instanceWallpaper"
/>
<div v-else class="wallpaper" />
<div class="fun-monitor-display-screen-overlay input" />
<div
v-if="backgroundUploading"
class="fun-monitor-display-uploading"
> >
<FAIcon <FAIcon
icon="times" class="fun-monitor-display-screen-uploading"
type="button" spin
icon="circle-notch"
/> />
</button>
</div> </div>
<p>{{ $t('settings.set_new_background') }}</p> </div>
<img </div>
v-if="backgroundPreview" </div>
class="banner-background-preview" </div>
:src="backgroundPreview" <div class="banner-background-input">
> <h4>{{ $t('settings.set_new_background') }}</h4>
<div>
<input <input
type="file" type="file"
class="input" class="input"
@change="uploadFile('background', $event)" @change="uploadFile('background', $event)"
> >
</div> <div class="custom-bg-control">
<FAIcon
v-if="backgroundUploading"
class="uploading"
spin
icon="circle-notch"
/>
<button <button
v-else-if="backgroundPreview" :disabled="!backgroundPreview"
class="btn button-default" class="btn button-default"
@click="submitBackground(background)" @click="submitBackground(background)"
> >
{{ $t('settings.save') }} {{ $t('settings.save') }}
</button> </button>
<button
:disabled="!backgroundPreview"
class="btn button-default"
@click="resetUploadedBackground"
>
{{ $t('settings.reset') }}
</button>
</div> </div>
<div class="setting-item"> <button
<h2>{{ $t('settings.scale_and_layout') }}</h2> v-if="!isDefaultBackground"
class="btn button-default reset-button"
:title="$t('settings.reset_profile_background')"
@click="resetBackground"
>
{{ $t('settings.reset_profile_background') }}
</button>
</div>
</div>
<h3>{{ $t('settings.visual_tweaks') }}</h3>
<div class="alert neutral theme-notice"> <div class="alert neutral theme-notice">
{{ $t("settings.style.appearance_tab_note") }} {{ $t("settings.style.visual_tweaks_section_note") }}
</div> </div>
<ul class="setting-list"> <ul class="setting-list">
<li>
<UnitSetting
path="textSize"
:step="0.1"
:units="['px', 'rem']"
:reset-default="{ 'px': 14, 'rem': 1 }"
timed-apply-mode
>
{{ $t('settings.text_size') }}
</UnitSetting>
<div>
<small>
<i18n-t
scope="global"
keypath="settings.text_size_tip"
tag="span"
>
<code>px</code>
<code>rem</code>
</i18n-t>
<br>
<i18n-t
scope="global"
keypath="settings.text_size_tip2"
tag="span"
>
<code>14px</code>
</i18n-t>
</small>
</div>
</li>
<li>
<UnitSetting
path="emojiSize"
:step="0.1"
:units="['px', 'rem']"
:reset-default="{ 'px': 32, 'rem': 2.2 }"
>
{{ $t('settings.emoji_size') }}
</UnitSetting>
<ul
class="setting-list suboptions"
>
<li>
<FloatSetting
v-if="user"
path="emojiReactionsScale"
expert="1"
>
{{ $t('settings.emoji_reactions_scale') }}
</FloatSetting>
</li>
</ul>
</li>
<li>
<UnitSetting
path="navbarSize"
:step="0.1"
:units="['px', 'rem']"
:reset-default="{ 'px': 55, 'rem': 3.5 }"
>
{{ $t('settings.navbar_size') }}
</UnitSetting>
</li>
<h3>{{ $t('settings.style.interface_font_user_override') }}</h3>
<li>
<FontControl
:model-value="mergedConfig.theme3hacks.fonts.interface"
name="ui"
:label="$t('settings.style.fonts.components.interface')"
:fallback="{ family: 'sans-serif' }"
no-inherit="1"
@update:model-value="v => updateFont('interface', v)"
/>
</li>
<li>
<FontControl
v-if="expertLevel > 0"
:model-value="mergedConfig.theme3hacks.fonts.input"
name="input"
:fallback="{ family: 'inherit' }"
:label="$t('settings.style.fonts.components.input')"
@update:model-value="v => updateFont('input', v)"
/>
</li>
<li>
<FontControl
v-if="expertLevel > 0"
:model-value="mergedConfig.theme3hacks.fonts.post"
name="post"
:fallback="{ family: 'inherit' }"
:label="$t('settings.style.fonts.components.post')"
@update:model-value="v => updateFont('post', v)"
/>
</li>
<li>
<FontControl
v-if="expertLevel > 0"
:model-value="mergedConfig.theme3hacks.fonts.monospace"
name="postCode"
:fallback="{ family: 'monospace' }"
:label="$t('settings.style.fonts.components.monospace')"
@update:model-value="v => updateFont('monospace', v)"
/>
</li>
<h3>{{ $t('settings.columns') }}</h3>
<li>
<UnitSetting
path="panelHeaderSize"
:step="0.1"
:units="['px', 'rem']"
:reset-default="{ 'px': 52, 'rem': 3.2 }"
timed-apply-mode
>
{{ $t('settings.panel_header_size') }}
</UnitSetting>
</li>
<li>
<BooleanSetting path="sidebarRight">
{{ $t('settings.right_sidebar') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="navbarColumnStretch">
{{ $t('settings.navbar_column_stretch') }}
</BooleanSetting>
</li>
<li>
<ChoiceSetting
v-if="user"
id="thirdColumnMode"
path="thirdColumnMode"
:options="thirdColumnModeOptions"
>
{{ $t('settings.third_column_mode') }}
</ChoiceSetting>
</li>
<li v-if="expertLevel > 0">
{{ $t('settings.column_sizes') }}
<div class="column-settings">
<UnitSetting
v-for="column in columns"
:key="column"
:path="column + 'ColumnWidth'"
:units="horizontalUnits"
expert="1"
>
{{ $t('settings.column_sizes_' + column) }}
</UnitSetting>
</div>
</li>
<li>
<BooleanSetting path="disableStickyHeaders">
{{ $t('settings.disable_sticky_headers') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="showScrollbars">
{{ $t('settings.show_scrollbars') }}
</BooleanSetting>
</li>
<li>
<UnitSetting
path="themeEditorMinWidth"
:units="['px', 'rem']"
expert="1"
>
{{ $t('settings.theme_editor_min_width') }}
</UnitSetting>
</li>
</ul>
</div>
<div class="setting-item">
<h2>{{ $t('settings.visual_tweaks') }}</h2>
<ul class="setting-list">
<li>
<BooleanSetting path="modalMobileCenter">
{{ $t('settings.mobile_center_dialog') }}
</BooleanSetting>
</li>
<li> <li>
<ChoiceSetting <ChoiceSetting
id="forcedRoundness" id="forcedRoundness"
@ -403,22 +243,6 @@
{{ $t('settings.hide_wallpaper') }} {{ $t('settings.hide_wallpaper') }}
</BooleanSetting> </BooleanSetting>
</li> </li>
<li>
<BooleanSetting
path="forceThemeRecompilation"
:expert="1"
>
{{ $t('settings.force_theme_recompilation_debug') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting
path="themeDebug"
:expert="1"
>
{{ $t('settings.theme_debug') }}
</BooleanSetting>
</li>
</ul> </ul>
</div> </div>
</div> </div>

View file

@ -0,0 +1,194 @@
import { mapState, mapActions } from 'pinia'
import { mapState as mapVuexState } from 'vuex'
import { v4 as uuidv4 } from 'uuid';
import { useServerSideStorageStore } from 'src/stores/serverSideStorage'
import BooleanSetting from '../helpers/boolean_setting.vue'
import ChoiceSetting from '../helpers/choice_setting.vue'
import UnitSetting from '../helpers/unit_setting.vue'
import IntegerSetting from '../helpers/integer_setting.vue'
import HelpIndicator from '../helpers/help_indicator.vue'
import Checkbox from 'src/components/checkbox/checkbox.vue'
import Select from 'src/components/select/select.vue'
import SharedComputedObject from '../helpers/shared_computed_object.js'
const ClutterTab = {
components: {
BooleanSetting,
ChoiceSetting,
UnitSetting,
IntegerSetting,
Checkbox,
Select,
HelpIndicator
},
computed: {
instanceSpecificPanelPresent () { return this.$store.state.instance.showInstanceSpecificPanel },
...SharedComputedObject(),
...mapState(
useServerSideStorageStore,
{
muteFilters: store => Object.entries(store.prefsStorage.simple.muteFilters),
muteFiltersObject: store => store.prefsStorage.simple.muteFilters
}
),
...mapVuexState({
blockExpirationSupported: state => state.instance.blockExpiration
}),
onMuteDefaultActionLv1: {
get () {
const value = this.$store.state.config.onMuteDefaultAction
if (value === 'ask' || value === 'forever') {
return value
} else {
return 'temporarily'
}
},
set (value) {
let realValue = value
if (value !== 'ask' && value !== 'forever') {
realValue = '14d'
}
this.$store.dispatch('setOption', { name: 'onMuteDefaultAction', value: realValue })
}
},
onBlockDefaultActionLv1: {
get () {
const value = this.$store.state.config.onBlockDefaultAction
if (value === 'ask' || value === 'forever') {
return value
} else {
return 'temporarily'
}
},
set (value) {
let realValue = value
if (value !== 'ask' && value !== 'forever') {
realValue = '14d'
}
this.$store.dispatch('setOption', { name: 'onBlockDefaultAction', value: realValue })
}
},
muteFiltersDraft () {
return Object.entries(this.muteFiltersDraftObject)
},
muteFiltersExpired () {
const now = Date.now()
return Object
.entries(this.muteFiltersDraftObject)
.filter(([, { expires }]) => expires != null && expires <= now)
}
},
methods: {
...mapActions(useServerSideStorageStore, ['setPreference', 'unsetPreference', 'pushServerSideStorage']),
getDatetimeLocal (timestamp) {
const date = new Date(timestamp)
const fmt = new Intl.NumberFormat("en-US", {minimumIntegerDigits: 2})
const datetime = [
date.getFullYear(),
'-',
fmt.format(date.getMonth() + 1),
'-',
fmt.format(date.getDate()),
'T',
fmt.format(date.getHours()),
':',
fmt.format(date.getMinutes())
].join('')
return datetime
},
checkRegexValid (id) {
const filter = this.muteFiltersObject[id]
if (filter.type !== 'regexp') return true
if (filter.type !== 'user_regexp') return true
const { value } = filter
let valid = true
try {
new RegExp(value)
} catch {
valid = false
console.error('Invalid RegExp: ' + value)
}
return valid
},
createFilter (filter = {
type: 'word',
value: '',
name: 'New Filter',
enabled: true,
expires: null,
hide: false,
}) {
const newId = uuidv4()
filter.order = this.muteFilters.length + 2
this.muteFiltersDraftObject[newId] = filter
this.setPreference({ path: 'simple.muteFilters.' + newId , value: filter })
this.pushServerSideStorage()
},
exportFilter(id) {
this.exportedFilter = { ...this.muteFiltersDraftObject[id] }
delete this.exportedFilter.order
this.filterExporter.exportData()
},
importFilter() {
this.filterImporter.importData()
},
copyFilter (id) {
const filter = { ...this.muteFiltersDraftObject[id] }
const newId = uuidv4()
this.muteFiltersDraftObject[newId] = filter
this.setPreference({ path: 'simple.muteFilters.' + newId , value: filter })
this.pushServerSideStorage()
},
deleteFilter (id) {
delete this.muteFiltersDraftObject[id]
this.unsetPreference({ path: 'simple.muteFilters.' + id , value: null })
this.pushServerSideStorage()
},
purgeExpiredFilters () {
this.muteFiltersExpired.forEach(([id]) => {
console.log(id)
delete this.muteFiltersDraftObject[id]
this.unsetPreference({ path: 'simple.muteFilters.' + id , value: null })
})
this.pushServerSideStorage()
},
updateFilter(id, field, value) {
const filter = { ...this.muteFiltersDraftObject[id] }
if (field === 'expires-never') {
if (!value) {
const offset = 1000 * 60 * 60 * 24 * 14 // 2 weeks
const date = Date.now() + offset
filter.expires = date
} else {
filter.expires = null
}
} else if (field === 'expires') {
const parsed = Date.parse(value)
filter.expires = parsed.valueOf()
} else {
filter[field] = value
}
this.muteFiltersDraftObject[id] = filter
this.muteFiltersDraftDirty[id] = true
},
saveFilter(id) {
this.setPreference({ path: 'simple.muteFilters.' + id , value: this.muteFiltersDraftObject[id] })
this.pushServerSideStorage()
this.muteFiltersDraftDirty[id] = false
},
},
// Updating nested properties
watch: {
replyVisibility () {
this.$store.dispatch('queueFlushAll')
}
}
}
export default ClutterTab

View file

@ -0,0 +1,88 @@
<template>
<div class="clutter-tab">
<div class="setting-item">
<h3>{{ $t('settings.interface') }}</h3>
<ul class="setting-list">
<li>
<BooleanSetting path="alwaysShowSubjectInput">
{{ $t('settings.subject_input_always_show') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="minimalScopesMode">
{{ $t('settings.minimal_scopes_mode') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="hidePostStats">
{{ $t('settings.hide_post_stats') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting
path="hideUserStats"
>
{{ $t('settings.hide_user_stats') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="hideBotIndication">
{{ $t('settings.hide_actor_type_indication') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="hideScrobbles">
{{ $t('settings.hide_scrobbles') }}
</BooleanSetting>
<ul class="setting-list suboptions">
<li>
<UnitSetting
key="hideScrobblesAfter"
path="hideScrobblesAfter"
:units="['m', 'h', 'd']"
unit-set="time"
>
{{ $t('settings.hide_scrobbles_after') }}
</UnitSetting>
</li>
</ul>
</li>
</ul>
<h3>{{ $t('settings.attachments') }}</h3>
<ul class="setting-list">
<li>
<IntegerSetting
path="maxThumbnails"
:min="0"
>
{{ $t('settings.max_thumbnails') }}
</IntegerSetting>
</li>
<li>
<BooleanSetting path="hideAttachments">
{{ $t('settings.hide_attachments_in_tl') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="hideAttachmentsInConv">
{{ $t('settings.hide_attachments_in_convo') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="userCardHidePersonalMarks">
{{ $t('settings.user_card_hide_personal_marks') }}
</BooleanSetting>
</li>
<li v-if="instanceShoutboxPresent">
<BooleanSetting
path="hideShoutbox"
>
{{ $t('settings.hide_shoutbox') }}
</BooleanSetting>
</li>
</ul>
</div>
</div>
</template>
<script src="./clutter_tab.js"></script>

View file

@ -0,0 +1,178 @@
import { mapState } from 'vuex'
import BooleanSetting from '../helpers/boolean_setting.vue'
import ChoiceSetting from '../helpers/choice_setting.vue'
import ScopeSelector from 'src/components/scope_selector/scope_selector.vue'
import IntegerSetting from '../helpers/integer_setting.vue'
import FloatSetting from '../helpers/float_setting.vue'
import UnitSetting from '../helpers/unit_setting.vue'
import InterfaceLanguageSwitcher from 'src/components/interface_language_switcher/interface_language_switcher.vue'
import Select from 'src/components/select/select.vue'
import ProfileSettingIndicator from '../helpers/profile_setting_indicator.vue'
import FontControl from 'src/components/font_control/font_control.vue'
import SharedComputedObject from '../helpers/shared_computed_object.js'
import localeService from 'src/services/locale/locale.service.js'
import { clearCache, cacheKey, emojiCacheKey } from 'src/services/sw/sw.js'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faGlobe,
faMessage,
faPenAlt,
faDatabase,
faSliders
} from '@fortawesome/free-solid-svg-icons'
library.add(
faGlobe,
faMessage,
faPenAlt,
faDatabase,
faSliders
)
const ComposingTab = {
props: {
parentCollapsed: {
required: true,
type: Boolean
}
},
data () {
return {
subjectLineOptions: ['email', 'noop', 'masto'].map(mode => ({
key: mode,
value: mode,
label: this.$t(`settings.subject_line_${mode === 'masto' ? 'mastodon' : mode}`)
})),
conversationDisplayOptions: ['tree', 'linear'].map(mode => ({
key: mode,
value: mode,
label: this.$t(`settings.conversation_display_${mode}`)
})),
absoluteTime12hOptions: ['24h', '12h'].map(mode => ({
key: mode,
value: mode,
label: this.$t(`settings.absolute_time_format_12h_${mode}`)
})),
conversationOtherRepliesButtonOptions: ['below', 'inside'].map(mode => ({
key: mode,
value: mode,
label: this.$t(`settings.conversation_other_replies_button_${mode}`)
})),
mentionLinkDisplayOptions: ['short', 'full_for_remote', 'full'].map(mode => ({
key: mode,
value: mode,
label: this.$t(`settings.mention_link_display_${mode}`)
})),
userPopoverAvatarActionOptions: ['close', 'zoom', 'open'].map(mode => ({
key: mode,
value: mode,
label: this.$t(`settings.user_popover_avatar_action_${mode}`)
})),
unsavedPostActionOptions: ['save', 'discard', 'confirm'].map(mode => ({
key: mode,
value: mode,
label: this.$t(`settings.unsaved_post_action_${mode}`)
})),
loopSilentAvailable:
// Firefox
Object.getOwnPropertyDescriptor(HTMLVideoElement.prototype, 'mozHasAudio') ||
// Chrome-likes
Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'webkitAudioDecodedByteCount') ||
// Future spec, still not supported in Nightly 63 as of 08/2018
Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'audioTracks'),
emailLanguage: this.$store.state.users.currentUser.language || ['']
}
},
components: {
BooleanSetting,
ChoiceSetting,
IntegerSetting,
FloatSetting,
UnitSetting,
InterfaceLanguageSwitcher,
ProfileSettingIndicator,
ScopeSelector,
Select,
FontControl
},
computed: {
postFormats () {
return this.$store.state.instance.postFormats || []
},
postContentOptions () {
return this.postFormats.map(format => ({
key: format,
value: format,
label: this.$t(`post_status.content_type["${format}"]`)
}))
},
language: {
get: function () { return this.$store.getters.mergedConfig.interfaceLanguage },
set: function (val) {
this.$store.dispatch('setOption', { name: 'interfaceLanguage', value: val })
}
},
...SharedComputedObject(),
...mapState({
blockExpirationSupported: state => state.instance.blockExpiration,
})
},
methods: {
changeDefaultScope (value) {
this.$store.dispatch('setProfileOption', { name: 'defaultScope', value })
},
clearCache (key) {
clearCache(key)
.then(() => {
this.$store.dispatch('settingsSaved', { success: true })
})
.catch(error => {
this.$store.dispatch('settingsSaved', { error })
})
},
tooSmall () {
this.$emit('tooSmall')
},
tooBig () {
this.$emit('tooBig')
},
getNavMode () {
return this.$refs.tabSwitcher.getNavMode()
},
clearAssetCache () {
this.clearCache(cacheKey)
},
clearEmojiCache () {
this.clearCache(emojiCacheKey)
},
updateProfile () {
const params = {
language: localeService.internalToBackendLocaleMulti(this.emailLanguage)
}
this.$store.state.api.backendInteractor
.updateProfile({ params })
.then((user) => {
this.$store.commit('addNewUsers', [user])
this.$store.commit('setCurrentUser', user)
})
},
updateFont (key, value) {
this.$store.dispatch('setOption', {
name: 'theme3hacks',
value: {
...this.mergedConfig.theme3hacks,
fonts: {
...this.mergedConfig.theme3hacks.fonts,
[key]: value
}
}
})
},
}
}
export default ComposingTab

View file

@ -0,0 +1,115 @@
<template>
<div :label="$t('settings.posts')">
<div class="setting-item">
<h3>{{ $t('settings.general') }}</h3>
<ul class="setting-list">
<li>
<label for="default-vis">
{{ $t('settings.default_vis') }}
{{ ' ' }}
<ScopeSelector
class="scope-selector"
:show-all="true"
:user-default="$store.state.profileConfig.defaultScope"
:initial-scope="$store.state.profileConfig.defaultScope"
:on-scope-change="changeDefaultScope"
:unstyled="false"
uns
/>
<ProfileSettingIndicator :is-profile="true" />
</label>
</li>
<li>
<!-- <BooleanSetting source="profile" path="defaultNSFW"> -->
<BooleanSetting path="sensitiveByDefault">
{{ $t('settings.sensitive_by_default') }}
</BooleanSetting>
</li>
<li v-if="postFormats.length > 0">
<ChoiceSetting
id="postContentType"
path="postContentType"
:options="postContentOptions"
>
{{ $t('settings.default_post_status_content_type') }}
</ChoiceSetting>
</li>
<li>
<BooleanSetting path="padEmoji">
{{ $t('settings.pad_emoji') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting
path="autocompleteSelect"
expert="1"
>
{{ $t('settings.autocomplete_select_first') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting
path="autoSaveDraft"
>
{{ $t('settings.auto_save_draft') }}
</BooleanSetting>
</li>
<li v-if="!mergedConfig.autoSaveDraft">
<ChoiceSetting
id="unsavedPostAction"
path="unsavedPostAction"
:options="unsavedPostActionOptions"
expert="1"
>
{{ $t('settings.unsaved_post_action') }}
</ChoiceSetting>
</li>
</ul>
<h3>{{ $t('settings.replies') }}</h3>
<ul class="setting-list">
<li>
<BooleanSetting
path="scopeCopy"
>
{{ $t('settings.scope_copy') }}
</BooleanSetting>
</li>
<li>
<ChoiceSetting
id="subjectLineBehavior"
path="subjectLineBehavior"
:options="subjectLineOptions"
>
{{ $t('settings.subject_line_behavior') }}
</ChoiceSetting>
</li>
</ul>
<h3 v-if="expertLevel > 0">
{{ $t('settings.attachments') }}
</h3>
<ul class="setting-list">
<li>
<BooleanSetting
path="imageCompression"
expert="1"
>
{{ $t('settings.image_compression') }}
</BooleanSetting>
<ul class="setting-list suboptions">
<li>
<BooleanSetting
path="alwaysUseJpeg"
expert="1"
parent-path="imageCompression"
>
{{ $t('settings.always_use_jpeg') }}
</BooleanSetting>
</li>
</ul>
</li>
</ul>
</div>
</div>
</template>
<script src="./composing_tab.js"></script>

View file

@ -0,0 +1,21 @@
.data-import-export-tab {
.importer-exporter {
display: inline-flex;
flex-direction: column;
gap: 0.5em;
}
table {
td, th {
line-height: 1.5;
}
th {
padding: 0 0.5em;
}
td {
padding: 0.5em;
}
}
}

View file

@ -1,62 +1,77 @@
<template> <template>
<div <div
class="data-import-export-tab"
:label="$t('settings.data_import_export_tab')" :label="$t('settings.data_import_export_tab')"
> >
<div class="setting-item"> <div class="setting-item">
<h2>{{ $t('settings.follow_import') }}</h2> <h3>{{ $t('settings.import_export.title') }}</h3>
<ul class="setting-list">
<li>
<h4>{{ $t('settings.import_export.follows') }}</h4>
<p>{{ $t('settings.import_followers_from_a_csv_file') }}</p> <p>{{ $t('settings.import_followers_from_a_csv_file') }}</p>
<div class="importer-exporter">
<Importer <Importer
:submit-handler="importFollows" :submit-handler="importFollows"
:success-message="$t('settings.follows_imported')" :success-message="$t('settings.follows_imported')"
:error-message="$t('settings.follow_import_error')" :error-message="$t('settings.follow_import_error')"
/> />
</div>
<div class="setting-item">
<h2>{{ $t('settings.follow_export') }}</h2>
<Exporter <Exporter
:get-content="getFollowsContent" :get-content="getFollowsContent"
filename="friends.csv" filename="friends.csv"
:export-button-label="$t('settings.follow_export_button')" :export-button-label="$t('settings.follow_export_button')"
/> />
</div> </div>
<div class="setting-item"> </li>
<h2>{{ $t('settings.block_import') }}</h2> <li>
<p>{{ $t('settings.import_blocks_from_a_csv_file') }}</p> <h4>{{ $t('settings.import_export.mutes') }}</h4>
<Importer
:submit-handler="importBlocks"
:success-message="$t('settings.blocks_imported')"
:error-message="$t('settings.block_import_error')"
/>
</div>
<div class="setting-item">
<h2>{{ $t('settings.block_export') }}</h2>
<Exporter
:get-content="getBlocksContent"
filename="blocks.csv"
:export-button-label="$t('settings.block_export_button')"
/>
</div>
<div class="setting-item">
<h2>{{ $t('settings.mute_import') }}</h2>
<p>{{ $t('settings.import_mutes_from_a_csv_file') }}</p> <p>{{ $t('settings.import_mutes_from_a_csv_file') }}</p>
<div class="importer-exporter">
<Importer <Importer
:submit-handler="importMutes" :submit-handler="importMutes"
:success-message="$t('settings.mutes_imported')" :success-message="$t('settings.mutes_imported')"
:error-message="$t('settings.mute_import_error')" :error-message="$t('settings.mute_import_error')"
/> />
</div>
<div class="setting-item">
<h2>{{ $t('settings.mute_export') }}</h2>
<Exporter <Exporter
:get-content="getMutesContent" :get-content="getMutesContent"
filename="mutes.csv" filename="friends.csv"
:export-button-label="$t('settings.mute_export_button')" :export-button-label="$t('settings.mute_export_button')"
/> />
</div> </div>
<div class="setting-item"> </li>
<h2>{{ $t('settings.account_backup') }}</h2> <li>
<h4>{{ $t('settings.import_export.blocks') }}</h4>
<p>{{ $t('settings.import_blocks_from_a_csv_file') }}</p>
<div class="importer-exporter">
<Importer
:submit-handler="importBlocks"
:success-message="$t('settings.blocks_imported')"
:error-message="$t('settings.block_import_error')"
/>
<Exporter
:get-content="getBlocksContent"
filename="friends.csv"
:export-button-label="$t('settings.block_export_button')"
/>
</div>
</li>
</ul>
<h3>{{ $t('settings.account_backup') }}</h3>
<div class="setting-list">
<p>{{ $t('settings.account_backup_description') }}</p> <p>{{ $t('settings.account_backup_description') }}</p>
<table> <button
class="btn button-default"
@click="addBackup"
>
{{ $t('settings.add_backup') }}
</button>
<p v-if="addedBackup">
{{ $t('settings.added_backup') }}
</p>
<template v-if="addBackupError !== false">
<p>{{ $t('settings.add_backup_error', { error: addBackupError }) }}</p>
</template>
</div>
<table class="setting-list">
<thead> <thead>
<tr> <tr>
<th>{{ $t('settings.account_backup_table_head') }}</th> <th>{{ $t('settings.account_backup_table_head') }}</th>
@ -111,21 +126,9 @@
/> />
</button> </button>
</div> </div>
<button
class="btn button-default"
@click="addBackup"
>
{{ $t('settings.add_backup') }}
</button>
<p v-if="addedBackup">
{{ $t('settings.added_backup') }}
</p>
<template v-if="addBackupError !== false">
<p>{{ $t('settings.add_backup_error', { error: addBackupError }) }}</p>
</template>
</div> </div>
</div> </div>
</template> </template>
<script src="./data_import_export_tab.js"></script> <script src="./data_import_export_tab.js"></script>
<!-- <style lang="scss" src="./profile.scss"></style> --> <style lang="scss" src="./data_import_export_tab.scss"></style>

View file

@ -0,0 +1,46 @@
import BooleanSetting from '../helpers/boolean_setting.vue'
import SharedComputedObject from '../helpers/shared_computed_object.js'
import { clearCache, cacheKey, emojiCacheKey } from 'src/services/sw/sw.js'
const pleromaFeCommitUrl = 'https://git.pleroma.social/pleroma/pleroma-fe/commit/'
const VersionTab = {
data () {
const instance = this.$store.state.instance
return {
backendVersion: instance.backendVersion,
backendRepository: instance.backendRepository,
frontendVersion: instance.frontendVersion
}
},
components: {
BooleanSetting
},
computed: {
frontendVersionLink () {
return pleromaFeCommitUrl + this.frontendVersion
},
...SharedComputedObject(),
},
methods: {
clearAssetCache () {
this.clearCache(cacheKey)
},
clearEmojiCache () {
this.clearCache(emojiCacheKey)
},
clearCache (key) {
clearCache(key)
.then(() => {
this.$store.dispatch('settingsSaved', { success: true })
})
.catch(error => {
this.$store.dispatch('settingsSaved', { error })
})
}
}
}
export default VersionTab

View file

@ -0,0 +1,10 @@
.developer-tab {
dt {
font-weight: 900;
}
dd {
margin-top: 0.5em;
margin-bottom: 1em;
}
}

View file

@ -0,0 +1,72 @@
<template>
<div
:label="$t('settings.developer')"
class="developer-tab"
>
<div class="setting-item">
<h3>{{ $t('settings.version.title') }}</h3>
<dl class="setting-list">
<dt>{{ $t('settings.version.backend_version') }}</dt>
<dd>
<a
:href="backendRepository"
target="_blank"
>
{{ backendVersion }}
</a>
</dd>
<dt>{{ $t('settings.version.frontend_version') }}</dt>
<dd>
<a
:href="frontendVersionLink"
target="_blank"
>
{{ frontendVersion }}
</a>
</dd>
</dl>
<h3>{{ $t('settings.debug') }}</h3>
<ul class="setting-list">
<li>
<BooleanSetting path="virtualScrolling">
{{ $t('settings.virtual_scrolling') }}
</BooleanSetting>
</li>
<li>
<button
class="btn button-default"
@click="clearAssetCache"
>
{{ $t('settings.clear_asset_cache') }}
</button>
</li>
<li>
<button
class="btn button-default"
@click="clearEmojiCache"
>
{{ $t('settings.clear_emoji_cache') }}
</button>
</li>
<li>
<BooleanSetting
path="themeDebug"
:expert="1"
>
{{ $t('settings.theme_debug') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting
path="forceThemeRecompilation"
:expert="1"
>
{{ $t('settings.force_theme_recompilation_debug') }}
</BooleanSetting>
</li>
</ul>
</div>
</div>
</template>
<script src="./developer_tab.js" />
<style lang="scss" src="./developer_tab.scss"></style>

View file

@ -91,6 +91,7 @@ const FilteringTab = {
HelpIndicator HelpIndicator
}, },
computed: { computed: {
instanceSpecificPanelPresent () { return this.$store.state.instance.showInstanceSpecificPanel },
...SharedComputedObject(), ...SharedComputedObject(),
...mapState( ...mapState(
useServerSideStorageStore, useServerSideStorageStore,

View file

@ -1,10 +1,7 @@
<template> <template>
<div <div class="filtering-tab">
:label="$t('settings.filtering')"
class="filtering-tab"
>
<div class="setting-item"> <div class="setting-item">
<h2>{{ $t('settings.filter.clutter') }}</h2> <h3>{{ $t('settings.filter.mute_filter') }}</h3>
<ul class="setting-list"> <ul class="setting-list">
<li> <li>
<ChoiceSetting <ChoiceSetting
@ -16,70 +13,6 @@
{{ $t('settings.replies_in_timeline') }} {{ $t('settings.replies_in_timeline') }}
</ChoiceSetting> </ChoiceSetting>
</li> </li>
<li>
<BooleanSetting
expert="1"
path="hidePostStats"
>
{{ $t('settings.hide_post_stats') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting
expert="1"
path="hideUserStats"
>
{{ $t('settings.hide_user_stats') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="hideBotIndication">
{{ $t('settings.hide_actor_type_indication') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="hideScrobbles">
{{ $t('settings.hide_scrobbles') }}
</BooleanSetting>
<ul class="setting-list suboptions">
<li>
<UnitSetting
key="hideScrobblesAfter"
path="hideScrobblesAfter"
:units="['m', 'h', 'd']"
unit-set="time"
expert="1"
>
{{ $t('settings.hide_scrobbles_after') }}
</UnitSetting>
</li>
</ul>
</li>
<h3>{{ $t('settings.attachments') }}</h3>
<li>
<IntegerSetting
path="maxThumbnails"
expert="1"
:min="0"
>
{{ $t('settings.max_thumbnails') }}
</IntegerSetting>
</li>
<li>
<BooleanSetting path="hideAttachments">
{{ $t('settings.hide_attachments_in_tl') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="hideAttachmentsInConv">
{{ $t('settings.hide_attachments_in_convo') }}
</BooleanSetting>
</li>
</ul>
</div>
<div class="setting-item">
<h2>{{ $t('settings.filter.mute_filter') }}</h2>
<ul class="setting-list">
<li> <li>
{{ $t('user_card.default_mute_expiration') }} {{ $t('user_card.default_mute_expiration') }}
<Select <Select

View file

@ -2,129 +2,55 @@ import { mapState } from 'vuex'
import BooleanSetting from '../helpers/boolean_setting.vue' import BooleanSetting from '../helpers/boolean_setting.vue'
import ChoiceSetting from '../helpers/choice_setting.vue' import ChoiceSetting from '../helpers/choice_setting.vue'
import ScopeSelector from 'src/components/scope_selector/scope_selector.vue'
import IntegerSetting from '../helpers/integer_setting.vue'
import FloatSetting from '../helpers/float_setting.vue'
import UnitSetting from '../helpers/unit_setting.vue' import UnitSetting from '../helpers/unit_setting.vue'
import FloatSetting from '../helpers/float_setting.vue'
import InterfaceLanguageSwitcher from 'src/components/interface_language_switcher/interface_language_switcher.vue' import InterfaceLanguageSwitcher from 'src/components/interface_language_switcher/interface_language_switcher.vue'
import Select from 'src/components/select/select.vue'
import ProfileSettingIndicator from '../helpers/profile_setting_indicator.vue' import ProfileSettingIndicator from '../helpers/profile_setting_indicator.vue'
import FontControl from 'src/components/font_control/font_control.vue'
import SharedComputedObject from '../helpers/shared_computed_object.js' import SharedComputedObject from '../helpers/shared_computed_object.js'
import localeService from 'src/services/locale/locale.service.js' import localeService from 'src/services/locale/locale.service.js'
import { clearCache, cacheKey, emojiCacheKey } from 'src/services/sw/sw.js'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faGlobe
} from '@fortawesome/free-solid-svg-icons'
library.add(
faGlobe
)
const GeneralTab = { const GeneralTab = {
props: {
parentCollapsed: {
required: true,
type: Boolean
}
},
data () { data () {
return { return {
subjectLineOptions: ['email', 'noop', 'masto'].map(mode => ({
key: mode,
value: mode,
label: this.$t(`settings.subject_line_${mode === 'masto' ? 'mastodon' : mode}`)
})),
conversationDisplayOptions: ['tree', 'linear'].map(mode => ({
key: mode,
value: mode,
label: this.$t(`settings.conversation_display_${mode}`)
})),
absoluteTime12hOptions: ['24h', '12h'].map(mode => ({ absoluteTime12hOptions: ['24h', '12h'].map(mode => ({
key: mode, key: mode,
value: mode, value: mode,
label: this.$t(`settings.absolute_time_format_12h_${mode}`) label: this.$t(`settings.absolute_time_format_12h_${mode}`)
})), })),
conversationOtherRepliesButtonOptions: ['below', 'inside'].map(mode => ({
key: mode,
value: mode,
label: this.$t(`settings.conversation_other_replies_button_${mode}`)
})),
mentionLinkDisplayOptions: ['short', 'full_for_remote', 'full'].map(mode => ({
key: mode,
value: mode,
label: this.$t(`settings.mention_link_display_${mode}`)
})),
userPopoverAvatarActionOptions: ['close', 'zoom', 'open'].map(mode => ({
key: mode,
value: mode,
label: this.$t(`settings.user_popover_avatar_action_${mode}`)
})),
unsavedPostActionOptions: ['save', 'discard', 'confirm'].map(mode => ({
key: mode,
value: mode,
label: this.$t(`settings.unsaved_post_action_${mode}`)
})),
loopSilentAvailable:
// Firefox
Object.getOwnPropertyDescriptor(HTMLVideoElement.prototype, 'mozHasAudio') ||
// Chrome-likes
Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'webkitAudioDecodedByteCount') ||
// Future spec, still not supported in Nightly 63 as of 08/2018
Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'audioTracks'),
emailLanguage: this.$store.state.users.currentUser.language || [''] emailLanguage: this.$store.state.users.currentUser.language || ['']
} }
}, },
components: { components: {
BooleanSetting, BooleanSetting,
ChoiceSetting, ChoiceSetting,
IntegerSetting,
FloatSetting,
UnitSetting, UnitSetting,
FloatSetting,
FontControl,
InterfaceLanguageSwitcher, InterfaceLanguageSwitcher,
ProfileSettingIndicator, ProfileSettingIndicator
ScopeSelector,
Select
}, },
computed: { computed: {
postFormats () {
return this.$store.state.instance.postFormats || []
},
postContentOptions () {
return this.postFormats.map(format => ({
key: format,
value: format,
label: this.$t(`post_status.content_type["${format}"]`)
}))
},
language: { language: {
get: function () { return this.$store.getters.mergedConfig.interfaceLanguage }, get: function () { return this.$store.getters.mergedConfig.interfaceLanguage },
set: function (val) { set: function (val) {
this.$store.dispatch('setOption', { name: 'interfaceLanguage', value: val }) this.$store.dispatch('setOption', { name: 'interfaceLanguage', value: val })
} }
}, },
instanceShoutboxPresent () { return this.$store.state.instance.shoutAvailable },
instanceSpecificPanelPresent () { return this.$store.state.instance.showInstanceSpecificPanel },
...SharedComputedObject(), ...SharedComputedObject(),
...mapState({ ...mapState({
blockExpirationSupported: state => state.instance.blockExpiration, blockExpirationSupported: state => state.instance.blockExpiration,
}) })
}, },
methods: { methods: {
changeDefaultScope (value) {
this.$store.dispatch('setProfileOption', { name: 'defaultScope', value })
},
clearCache (key) {
clearCache(key)
.then(() => {
this.$store.dispatch('settingsSaved', { success: true })
})
.catch(error => {
this.$store.dispatch('settingsSaved', { error })
})
},
clearAssetCache () {
this.clearCache(cacheKey)
},
clearEmojiCache () {
this.clearCache(emojiCacheKey)
},
updateProfile () { updateProfile () {
const params = { const params = {
language: localeService.internalToBackendLocaleMulti(this.emailLanguage) language: localeService.internalToBackendLocaleMulti(this.emailLanguage)
@ -137,6 +63,18 @@ const GeneralTab = {
this.$store.commit('setCurrentUser', user) this.$store.commit('setCurrentUser', user)
}) })
}, },
updateFont (key, value) {
this.$store.dispatch('setOption', {
name: 'theme3hacks',
value: {
...this.mergedConfig.theme3hacks,
fonts: {
...this.mergedConfig.theme3hacks.fonts,
[key]: value
}
}
})
},
} }
} }

View file

@ -1,7 +1,7 @@
<template> <template>
<div :label="$t('settings.general')"> <div>
<div class="setting-item"> <div class="setting-item">
<h2>{{ $t('settings.interface') }}</h2> <h3>{{ $t('settings.format_and_language') }}</h3>
<ul class="setting-list"> <ul class="setting-list">
<li> <li>
<interface-language-switcher <interface-language-switcher
@ -20,16 +20,98 @@
{{ $t('settings.email_language') }} {{ $t('settings.email_language') }}
</interface-language-switcher> </interface-language-switcher>
</li> </li>
<li v-if="instanceSpecificPanelPresent"> <li>
<BooleanSetting path="hideISP"> <BooleanSetting path="useAbsoluteTimeFormat">
{{ $t('settings.hide_isp') }} {{ $t('settings.absolute_time_format') }}
</BooleanSetting> </BooleanSetting>
</li> </li>
<li> <li>
<BooleanSetting path="stopGifs"> <ChoiceSetting
{{ $t('settings.stop_gifs') }} id="absoluteTime12h"
</BooleanSetting> path="absoluteTime12h"
:options="absoluteTime12hOptions"
>
{{ $t('settings.absolute_time_format_12h') }}
</ChoiceSetting>
</li> </li>
</ul>
<h3>{{ $t('settings.scale_and_font') }}</h3>
<ul class="setting-list">
<li>
<UnitSetting
path="textSize"
:step="0.1"
:units="['px', 'rem']"
:reset-default="{ 'px': 14, 'rem': 1 }"
timed-apply-mode
>
{{ $t('settings.text_size') }}
</UnitSetting>
<div>
<small>
<i18n-t
scope="global"
keypath="settings.text_size_tip"
tag="span"
>
<code>px</code>
<code>rem</code>
</i18n-t>
<br>
<i18n-t
scope="global"
keypath="settings.text_size_tip2"
tag="span"
>
<code>14px</code>
</i18n-t>
</small>
</div>
</li>
<li>
<FontControl
:model-value="mergedConfig.theme3hacks.fonts.interface"
name="ui"
:label="$t('settings.style.fonts.components_inline.interface')"
:fallback="{ family: 'sans-serif' }"
no-inherit="1"
@update:model-value="v => updateFont('interface', v)"
/>
</li>
<li>
<FontControl
:model-value="mergedConfig.theme3hacks.fonts.input"
name="input"
:fallback="{ family: 'inherit' }"
:label="$t('settings.style.fonts.components_inline.input')"
@update:model-value="v => updateFont('input', v)"
/>
</li>
<li>
<UnitSetting
path="emojiSize"
:step="0.1"
:units="['px', 'rem']"
:reset-default="{ 'px': 32, 'rem': 2.2 }"
>
{{ $t('settings.emoji_size') }}
</UnitSetting>
<ul
class="setting-list suboptions"
>
<li>
<FloatSetting
v-if="user"
path="emojiReactionsScale"
>
{{ $t('settings.emoji_reactions_scale') }}
</FloatSetting>
</li>
</ul>
</li>
</ul>
<h3>{{ $t('settings.timelines') }}</h3>
<ul class="setting-list">
<li> <li>
<BooleanSetting path="streaming"> <BooleanSetting path="streaming">
{{ $t('settings.streaming') }} {{ $t('settings.streaming') }}
@ -53,72 +135,14 @@
{{ $t('settings.useStreamingApi') }} {{ $t('settings.useStreamingApi') }}
</BooleanSetting> </BooleanSetting>
</li> </li>
<li> </ul>
<BooleanSetting <h3 v-if="expertLevel > 0">
path="virtualScrolling" {{ $t('settings.confirmations') }}
expert="1" </h3>
<ul
v-if="expertLevel > 0"
class="setting-list"
> >
{{ $t('settings.virtual_scrolling') }}
</BooleanSetting>
</li>
<li>
<ChoiceSetting
id="userPopoverAvatarAction"
path="userPopoverAvatarAction"
:options="userPopoverAvatarActionOptions"
expert="1"
>
{{ $t('settings.user_popover_avatar_action') }}
</ChoiceSetting>
</li>
<li>
<BooleanSetting
path="userPopoverOverlay"
expert="1"
>
{{ $t('settings.user_popover_avatar_overlay') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting
path="userCardLeftJustify"
expert="1"
>
{{ $t('settings.user_card_left_justify') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting
path="userCardHidePersonalMarks"
expert="1"
>
{{ $t('settings.user_card_hide_personal_marks') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting
path="alwaysShowNewPostButton"
expert="1"
>
{{ $t('settings.always_show_post_button') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting
path="autohideFloatingPostButton"
expert="1"
>
{{ $t('settings.autohide_floating_post_button') }}
</BooleanSetting>
</li>
<li v-if="instanceShoutboxPresent">
<BooleanSetting
path="hideShoutbox"
expert="1"
>
{{ $t('settings.hide_shoutbox') }}
</BooleanSetting>
</li>
<li class="select-multiple"> <li class="select-multiple">
<span class="label">{{ $t('settings.confirm_dialogs') }}</span> <span class="label">{{ $t('settings.confirm_dialogs') }}</span>
<ul class="option-list"> <ul class="option-list">
@ -179,383 +203,6 @@
</li> </li>
</ul> </ul>
</div> </div>
<div class="setting-item">
<h2>{{ $t('settings.post_look_feel') }}</h2>
<ul class="setting-list">
<li>
<ChoiceSetting
id="conversationDisplay"
path="conversationDisplay"
:options="conversationDisplayOptions"
>
{{ $t('settings.conversation_display') }}
</ChoiceSetting>
</li>
<ul
v-if="mergedConfig.conversationDisplay !== 'linear'"
class="setting-list suboptions"
>
<li>
<BooleanSetting path="conversationTreeAdvanced">
{{ $t('settings.tree_advanced') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting
path="conversationTreeFadeAncestors"
:expert="1"
>
{{ $t('settings.tree_fade_ancestors') }}
</BooleanSetting>
</li>
<li>
<IntegerSetting
path="maxDepthInThread"
:min="3"
:expert="1"
>
{{ $t('settings.max_depth_in_thread') }}
</IntegerSetting>
</li>
<li>
<ChoiceSetting
id="conversationOtherRepliesButton"
path="conversationOtherRepliesButton"
:options="conversationOtherRepliesButtonOptions"
:expert="1"
>
{{ $t('settings.conversation_other_replies_button') }}
</ChoiceSetting>
</li>
</ul>
<li>
<BooleanSetting path="collapseMessageWithSubject">
{{ $t('settings.collapse_subject') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting
path="emojiReactionsOnTimeline"
expert="1"
>
{{ $t('settings.emoji_reactions_on_timeline') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting
v-if="user"
source="profile"
path="stripRichContent"
expert="1"
>
{{ $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>
<li>
<ChoiceSetting
id="absoluteTime12h"
path="absoluteTime12h"
:options="absoluteTime12hOptions"
:expert="1"
>
{{ $t('settings.absolute_time_format_12h') }}
</ChoiceSetting>
</li>
</ul>
<h3>{{ $t('settings.attachments') }}</h3>
<li>
<BooleanSetting
path="imageCompression"
expert="1"
>
{{ $t('settings.image_compression') }}
</BooleanSetting>
</li>
<ul class="setting-list suboptions">
<li>
<BooleanSetting
path="alwaysUseJpeg"
expert="1"
parent-path="imageCompression"
>
{{ $t('settings.always_use_jpeg') }}
</BooleanSetting>
</li>
</ul>
<li>
<BooleanSetting
path="useContainFit"
expert="1"
>
{{ $t('settings.use_contain_fit') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="hideNsfw">
{{ $t('settings.nsfw_clickthrough') }}
</BooleanSetting>
</li>
<ul class="setting-list suboptions">
<li>
<BooleanSetting
path="preloadImage"
expert="1"
parent-path="hideNsfw"
>
{{ $t('settings.preload_images') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting
path="useOneClickNsfw"
expert="1"
parent-path="hideNsfw"
>
{{ $t('settings.use_one_click_nsfw') }}
</BooleanSetting>
</li>
</ul>
<li>
<BooleanSetting
path="loopVideo"
expert="1"
>
{{ $t('settings.loop_video') }}
</BooleanSetting>
<ul class="setting-list suboptions">
<li>
<BooleanSetting
path="loopVideoSilentOnly"
expert="1"
parent-path="loopVideo"
:disabled="!loopSilentAvailable"
>
{{ $t('settings.loop_video_silent_only') }}
</BooleanSetting>
<div
v-if="!loopSilentAvailable"
class="unavailable"
>
<FAIcon icon="globe" />! {{ $t('settings.limited_availability') }}
</div>
</li>
</ul>
</li>
<li>
<BooleanSetting
path="playVideosInModal"
expert="1"
>
{{ $t('settings.play_videos_in_modal') }}
</BooleanSetting>
</li>
<h3>{{ $t('settings.mention_links') }}</h3>
<li>
<ChoiceSetting
id="mentionLinkDisplay"
path="mentionLinkDisplay"
:options="mentionLinkDisplayOptions"
>
{{ $t('settings.mention_link_display') }}
</ChoiceSetting>
</li>
<li>
<BooleanSetting
path="mentionLinkShowTooltip"
expert="1"
>
{{ $t('settings.mention_link_use_tooltip') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="mentionLinkShowAvatar">
{{ $t('settings.mention_link_show_avatar') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting
path="mentionLinkFadeDomain"
expert="1"
>
{{ $t('settings.mention_link_fade_domain') }}
</BooleanSetting>
</li>
<li v-if="user">
<BooleanSetting
path="mentionLinkBoldenYou"
expert="1"
>
{{ $t('settings.mention_link_bolden_you') }}
</BooleanSetting>
</li>
<h3 v-if="expertLevel > 0">
{{ $t('settings.fun') }}
</h3>
<li>
<BooleanSetting
path="greentext"
expert="1"
>
{{ $t('settings.greentext') }}
</BooleanSetting>
</li>
<li v-if="user">
<BooleanSetting
path="mentionLinkShowYous"
expert="1"
>
{{ $t('settings.show_yous') }}
</BooleanSetting>
</li>
</ul>
</div>
<div
v-if="user"
class="setting-item"
>
<h2>{{ $t('settings.composing') }}</h2>
<ul class="setting-list">
<li>
<label for="default-vis">
{{ $t('settings.default_vis') }} <ProfileSettingIndicator :is-profile="true" />
<ScopeSelector
class="scope-selector"
:show-all="true"
:user-default="$store.state.profileConfig.defaultScope"
:initial-scope="$store.state.profileConfig.defaultScope"
:on-scope-change="changeDefaultScope"
/>
</label>
</li>
<li>
<!-- <BooleanSetting source="profile" path="defaultNSFW"> -->
<BooleanSetting path="sensitiveByDefault">
{{ $t('settings.sensitive_by_default') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting
path="scopeCopy"
expert="1"
>
{{ $t('settings.scope_copy') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting
path="alwaysShowSubjectInput"
expert="1"
>
{{ $t('settings.subject_input_always_show') }}
</BooleanSetting>
</li>
<li>
<ChoiceSetting
id="subjectLineBehavior"
path="subjectLineBehavior"
:options="subjectLineOptions"
expert="1"
>
{{ $t('settings.subject_line_behavior') }}
</ChoiceSetting>
</li>
<li v-if="postFormats.length > 0">
<ChoiceSetting
id="postContentType"
path="postContentType"
:options="postContentOptions"
>
{{ $t('settings.post_status_content_type') }}
</ChoiceSetting>
</li>
<li>
<BooleanSetting
path="minimalScopesMode"
expert="1"
>
{{ $t('settings.minimal_scopes_mode') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting
path="padEmoji"
expert="1"
>
{{ $t('settings.pad_emoji') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting
path="autocompleteSelect"
expert="1"
>
{{ $t('settings.autocomplete_select_first') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting
path="autoSaveDraft"
>
{{ $t('settings.auto_save_draft') }}
</BooleanSetting>
</li>
<li v-if="!mergedConfig.autoSaveDraft">
<ChoiceSetting
id="unsavedPostAction"
path="unsavedPostAction"
:options="unsavedPostActionOptions"
>
{{ $t('settings.unsaved_post_action') }}
</ChoiceSetting>
</li>
</ul>
</div>
<div
class="setting-item"
>
<h2>{{ $t('settings.cache') }}</h2>
<ul class="setting-list">
<li>
<button
class="btn button-default"
@click="clearAssetCache"
>
{{ $t('settings.clear_asset_cache') }}
</button>
</li>
<li>
<button
class="btn button-default"
@click="clearEmojiCache"
>
{{ $t('settings.clear_emoji_cache') }}
</button>
</li>
</ul>
</div>
</div> </div>
</template> </template>

View file

@ -0,0 +1,50 @@
import BooleanSetting from '../helpers/boolean_setting.vue'
import ChoiceSetting from '../helpers/choice_setting.vue'
import UnitSetting from '../helpers/unit_setting.vue'
import ProfileSettingIndicator from '../helpers/profile_setting_indicator.vue'
import SharedComputedObject from '../helpers/shared_computed_object.js'
const GeneralTab = {
props: {
parentCollapsed: {
required: true,
type: Boolean
}
},
data () {
return {
thirdColumnModeOptions: ['none', 'notifications', 'postform'].map(mode => ({
key: mode,
value: mode,
label: this.$t(`settings.third_column_mode_${mode}`)
}))
}
},
components: {
BooleanSetting,
ChoiceSetting,
UnitSetting,
ProfileSettingIndicator
},
computed: {
postFormats () {
return this.$store.state.instance.postFormats || []
},
instanceShoutboxPresent () { return this.$store.state.instance.shoutAvailable },
columns () {
const mode = this.$store.getters.mergedConfig.thirdColumnMode
const notif = mode === 'none' ? [] : ['notifs']
if (this.$store.getters.mergedConfig.sidebarRight || mode === 'postform') {
return [...notif, 'content', 'sidebar']
} else {
return ['sidebar', 'content', ...notif]
}
},
...SharedComputedObject(),
}
}
export default GeneralTab

View file

@ -0,0 +1,127 @@
<template>
<div :label="$t('settings.layout')">
<div class="setting-item">
<h3>{{ $t('settings.general') }}</h3>
<ul class="setting-list">
<li>
<BooleanSetting path="modalMobileCenter">
{{ $t('settings.mobile_center_dialog') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting
path="alwaysShowNewPostButton"
expert="1"
>
{{ $t('settings.always_show_post_button') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting
path="autohideFloatingPostButton"
expert="1"
>
{{ $t('settings.autohide_floating_post_button') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting
path="userPopoverOverlay"
expert="1"
>
{{ $t('settings.user_popover_avatar_overlay') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="userCardLeftJustify">
{{ $t('settings.user_card_left_justify') }}
</BooleanSetting>
</li>
<li>
<UnitSetting
path="themeEditorMinWidth"
:units="['px', 'rem']"
expert="1"
>
{{ $t('settings.theme_editor_min_width') }}
</UnitSetting>
</li>
<li>
<UnitSetting
path="navbarSize"
:step="0.1"
:units="['px', 'rem']"
:reset-default="{ 'px': 55, 'rem': 3.5 }"
>
{{ $t('settings.navbar_size') }}
</UnitSetting>
</li>
</ul>
<h3>{{ $t('settings.columns') }}</h3>
<ul class="setting-list">
<li>
<BooleanSetting path="disableStickyHeaders">
{{ $t('settings.disable_sticky_headers') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="showScrollbars">
{{ $t('settings.show_scrollbars') }}
</BooleanSetting>
</li>
<li v-if="instanceSpecificPanelPresent">
<BooleanSetting path="hideISP">
{{ $t('settings.hide_isp') }}
</BooleanSetting>
</li>
<li>
<UnitSetting
path="panelHeaderSize"
:step="0.1"
:units="['px', 'rem']"
:reset-default="{ 'px': 52, 'rem': 3.2 }"
timed-apply-mode
>
{{ $t('settings.panel_header_size') }}
</UnitSetting>
</li>
<li>
<BooleanSetting path="sidebarRight">
{{ $t('settings.right_sidebar') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="navbarColumnStretch">
{{ $t('settings.navbar_column_stretch') }}
</BooleanSetting>
</li>
<li>
<ChoiceSetting
v-if="user"
id="thirdColumnMode"
path="thirdColumnMode"
:options="thirdColumnModeOptions"
>
{{ $t('settings.third_column_mode') }}
</ChoiceSetting>
</li>
<li v-if="expertLevel > 0">
<h4> {{ $t('settings.column_sizes') }} </h4>
<div class="column-settings">
<UnitSetting
v-for="column in columns"
:key="column"
:path="column + 'ColumnWidth'"
:units="horizontalUnits"
expert="1"
>
{{ $t('settings.column_sizes_' + column) }}
</UnitSetting>
</div>
</li>
</ul>
</div>
</div>
</template>
<script src="./layout_tab.js"></script>

View file

@ -1,7 +1,7 @@
<template> <template>
<div :label="$t('settings.notifications')"> <div :label="$t('settings.notifications')">
<div class="setting-item"> <div class="setting-item">
<h2>{{ $t('settings.notification_setting_annoyance') }}</h2> <h3>{{ $t('settings.notification_setting_annoyance') }}</h3>
<ul class="setting-list"> <ul class="setting-list">
<li> <li>
<BooleanSetting path="closingDrawerMarksAsSeen"> <BooleanSetting path="closingDrawerMarksAsSeen">
@ -29,7 +29,7 @@
</ul> </ul>
</div> </div>
<div class="setting-item"> <div class="setting-item">
<h2>{{ $t('settings.notification_setting_filters') }}</h2> <h3>{{ $t('settings.notification_setting_filters') }}</h3>
<ul class="setting-list"> <ul class="setting-list">
<li> <li>
<BooleanSetting <BooleanSetting
@ -40,13 +40,13 @@
</BooleanSetting> </BooleanSetting>
</li> </li>
<li> <li>
<h3> {{ $t('settings.notification_visibility') }}</h3> <h4> {{ $t('settings.notification_visibility') }}</h4>
<p v-if="expertLevel > 0"> <p v-if="expertLevel > 0">
{{ $t('settings.notification_setting_filters_chrome_push') }} {{ $t('settings.notification_setting_filters_chrome_push') }}
</p> </p>
<ul class="setting-list two-column"> <ul class="setting-list two-column">
<li> <li>
<h4> {{ $t('settings.notification_visibility_mentions') }}</h4> <h5> {{ $t('settings.notification_visibility_mentions') }}</h5>
<ul class="setting-list"> <ul class="setting-list">
<li> <li>
<BooleanSetting path="notificationVisibility.mentions"> <BooleanSetting path="notificationVisibility.mentions">
@ -61,7 +61,7 @@
</ul> </ul>
</li> </li>
<li> <li>
<h4> {{ $t('settings.notification_visibility_statuses') }}</h4> <h5> {{ $t('settings.notification_visibility_statuses') }}</h5>
<ul class="setting-list"> <ul class="setting-list">
<li> <li>
<BooleanSetting path="notificationVisibility.statuses"> <BooleanSetting path="notificationVisibility.statuses">
@ -76,7 +76,7 @@
</ul> </ul>
</li> </li>
<li> <li>
<h4> {{ $t('settings.notification_visibility_likes') }}</h4> <h5> {{ $t('settings.notification_visibility_likes') }}</h5>
<ul class="setting-list"> <ul class="setting-list">
<li> <li>
<BooleanSetting path="notificationVisibility.likes"> <BooleanSetting path="notificationVisibility.likes">
@ -91,7 +91,7 @@
</ul> </ul>
</li> </li>
<li> <li>
<h4> {{ $t('settings.notification_visibility_repeats') }}</h4> <h5> {{ $t('settings.notification_visibility_repeats') }}</h5>
<ul class="setting-list"> <ul class="setting-list">
<li> <li>
<BooleanSetting path="notificationVisibility.repeats"> <BooleanSetting path="notificationVisibility.repeats">
@ -106,7 +106,7 @@
</ul> </ul>
</li> </li>
<li> <li>
<h4> {{ $t('settings.notification_visibility_emoji_reactions') }}</h4> <h5> {{ $t('settings.notification_visibility_emoji_reactions') }}</h5>
<ul class="setting-list"> <ul class="setting-list">
<li> <li>
<BooleanSetting path="notificationVisibility.emojiReactions"> <BooleanSetting path="notificationVisibility.emojiReactions">
@ -121,7 +121,7 @@
</ul> </ul>
</li> </li>
<li> <li>
<h4> {{ $t('settings.notification_visibility_follows') }}</h4> <h5> {{ $t('settings.notification_visibility_follows') }}</h5>
<ul class="setting-list"> <ul class="setting-list">
<li> <li>
<BooleanSetting path="notificationVisibility.follows"> <BooleanSetting path="notificationVisibility.follows">
@ -136,7 +136,7 @@
</ul> </ul>
</li> </li>
<li> <li>
<h4> {{ $t('settings.notification_visibility_follow_requests') }}</h4> <h5> {{ $t('settings.notification_visibility_follow_requests') }}</h5>
<ul class="setting-list"> <ul class="setting-list">
<li> <li>
<BooleanSetting path="notificationVisibility.followRequest"> <BooleanSetting path="notificationVisibility.followRequest">
@ -151,7 +151,7 @@
</ul> </ul>
</li> </li>
<li> <li>
<h4> {{ $t('settings.notification_visibility_moves') }}</h4> <h5> {{ $t('settings.notification_visibility_moves') }}</h5>
<ul class="setting-list"> <ul class="setting-list">
<li> <li>
<BooleanSetting path="notificationVisibility.moves"> <BooleanSetting path="notificationVisibility.moves">
@ -166,7 +166,7 @@
</ul> </ul>
</li> </li>
<li> <li>
<h4> {{ $t('settings.notification_visibility_polls') }}</h4> <h5> {{ $t('settings.notification_visibility_polls') }}</h5>
<ul class="setting-list"> <ul class="setting-list">
<li> <li>
<BooleanSetting path="notificationVisibility.polls"> <BooleanSetting path="notificationVisibility.polls">
@ -181,7 +181,7 @@
</ul> </ul>
</li> </li>
<li v-if="canReceiveReports"> <li v-if="canReceiveReports">
<h4> {{ $t('settings.notification_visibility_reports') }}</h4> <h5> {{ $t('settings.notification_visibility_reports') }}</h5>
<ul class="setting-list"> <ul class="setting-list">
<li> <li>
<BooleanSetting path="notificationVisibility.reports"> <BooleanSetting path="notificationVisibility.reports">
@ -201,8 +201,6 @@
<BooleanSetting path="showExtraNotifications"> <BooleanSetting path="showExtraNotifications">
{{ $t('settings.notification_show_extra') }} {{ $t('settings.notification_show_extra') }}
</BooleanSetting> </BooleanSetting>
</li>
<li>
<ul class="setting-list suboptions"> <ul class="setting-list suboptions">
<li> <li>
<BooleanSetting <BooleanSetting
@ -245,7 +243,7 @@
v-if="expertLevel > 0" v-if="expertLevel > 0"
class="setting-item" class="setting-item"
> >
<h2>{{ $t('settings.notification_setting_privacy') }}</h2> <h3>{{ $t('settings.notification_setting_privacy') }}</h3>
<ul class="setting-list"> <ul class="setting-list">
<li> <li>
<BooleanSetting <BooleanSetting

View file

@ -1,4 +1,4 @@
.theme-tab { .old-theme-tab {
min-width: var(--themeEditorMinWidth, fit-content); min-width: var(--themeEditorMinWidth, fit-content);
.deprecation-warning { .deprecation-warning {

View file

@ -1,5 +1,5 @@
<template> <template>
<div class="theme-tab"> <div class="old-theme-tab">
<div class="alert warning deprecation-warning"> <div class="alert warning deprecation-warning">
{{ $t("settings.style.themes2_outdated") }} {{ $t("settings.style.themes2_outdated") }}
</div> </div>
@ -1020,6 +1020,6 @@
</div> </div>
</template> </template>
<script src="./theme_tab.js"></script> <script src="./old_theme_tab.js"></script>
<style src="./theme_tab.scss" lang="scss"></style> <style src="./old_theme_tab.scss" lang="scss"></style>

View file

@ -0,0 +1,78 @@
import BooleanSetting from '../helpers/boolean_setting.vue'
import ChoiceSetting from '../helpers/choice_setting.vue'
import IntegerSetting from '../helpers/integer_setting.vue'
import ProfileSettingIndicator from '../helpers/profile_setting_indicator.vue'
import FontControl from 'src/components/font_control/font_control.vue'
import SharedComputedObject from '../helpers/shared_computed_object.js'
const GeneralTab = {
props: {
parentCollapsed: {
required: true,
type: Boolean
}
},
data () {
return {
conversationDisplayOptions: ['tree', 'linear'].map(mode => ({
key: mode,
value: mode,
label: this.$t(`settings.conversation_display_${mode}`)
})),
conversationOtherRepliesButtonOptions: ['below', 'inside'].map(mode => ({
key: mode,
value: mode,
label: this.$t(`settings.conversation_other_replies_button_${mode}`)
})),
mentionLinkDisplayOptions: ['short', 'full_for_remote', 'full'].map(mode => ({
key: mode,
value: mode,
label: this.$t(`settings.mention_link_display_${mode}`)
})),
userPopoverAvatarActionOptions: ['close', 'zoom', 'open'].map(mode => ({
key: mode,
value: mode,
label: this.$t(`settings.user_popover_avatar_action_${mode}`)
})),
unsavedPostActionOptions: ['save', 'discard', 'confirm'].map(mode => ({
key: mode,
value: mode,
label: this.$t(`settings.unsaved_post_action_${mode}`)
})),
loopSilentAvailable:
// Firefox
Object.getOwnPropertyDescriptor(HTMLVideoElement.prototype, 'mozHasAudio') ||
// Chrome-likes
Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'webkitAudioDecodedByteCount') ||
// Future spec, still not supported in Nightly 63 as of 08/2018
Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'audioTracks')
}
},
components: {
BooleanSetting,
ChoiceSetting,
IntegerSetting,
FontControl,
ProfileSettingIndicator
},
computed: {
...SharedComputedObject(),
},
methods: {
updateFont (key, value) {
this.$store.dispatch('setOption', {
name: 'theme3hacks',
value: {
...this.mergedConfig.theme3hacks,
fonts: {
...this.mergedConfig.theme3hacks.fonts,
[key]: value
}
}
})
},
}
}
export default GeneralTab

View file

@ -0,0 +1,5 @@
.posts-tab {
.greentext {
color: var(--funtextGreentext);
}
}

View file

@ -0,0 +1,257 @@
<template>
<div class="posts-tab">
<div class="setting-item">
<h3>{{ $t('settings.posts_appearance') }}</h3>
<ul class="setting-list">
<li>
<BooleanSetting path="collapseMessageWithSubject">
{{ $t('settings.collapse_subject') }}
</BooleanSetting>
</li>
<li>
<ChoiceSetting
id="conversationDisplay"
path="conversationDisplay"
:options="conversationDisplayOptions"
>
{{ $t('settings.conversation_display') }}
</ChoiceSetting>
<ul
v-if="mergedConfig.conversationDisplay !== 'linear'"
class="setting-list suboptions"
>
<li>
<BooleanSetting path="conversationTreeAdvanced">
{{ $t('settings.tree_advanced') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting
path="conversationTreeFadeAncestors"
:expert="1"
>
{{ $t('settings.tree_fade_ancestors') }}
</BooleanSetting>
</li>
<li>
<IntegerSetting
path="maxDepthInThread"
:min="3"
:expert="1"
>
{{ $t('settings.max_depth_in_thread') }}
</IntegerSetting>
</li>
<li>
<ChoiceSetting
id="conversationOtherRepliesButton"
path="conversationOtherRepliesButton"
:options="conversationOtherRepliesButtonOptions"
:expert="1"
>
{{ $t('settings.conversation_other_replies_button') }}
</ChoiceSetting>
</li>
</ul>
</li>
<li>
<FontControl
:model-value="mergedConfig.theme3hacks.fonts.post"
name="post"
:fallback="{ family: 'inherit' }"
:label="$t('settings.style.fonts.components.post')"
@update:model-value="v => updateFont('post', v)"
/>
</li>
<li>
<FontControl
:model-value="mergedConfig.theme3hacks.fonts.monospace"
name="postCode"
:fallback="{ family: 'monospace' }"
:label="$t('settings.style.fonts.components.monospace')"
@update:model-value="v => updateFont('monospace', v)"
/>
</li>
<li>
<BooleanSetting path="greentext">
<i18n-t
keypath="settings.plaintext_quotes"
tag="span"
>
<span class="greentext">
{{ $t('settings.greentext_quotes') }}
</span>
</i18n-t>
</BooleanSetting>
</li>
<li>
<BooleanSetting
path="emojiReactionsOnTimeline"
expert="1"
>
{{ $t('settings.emoji_reactions_on_timeline') }}
</BooleanSetting>
</li>
</ul>
<h3>{{ $t('settings.mention_links') }}</h3>
<ul class="setting-list">
<li>
<ChoiceSetting
id="mentionLinkDisplay"
path="mentionLinkDisplay"
:options="mentionLinkDisplayOptions"
>
{{ $t('settings.mention_link_display') }}
</ChoiceSetting>
<ul class="setting-list suboptions">
<li>
<BooleanSetting
v-if="mergedConfig.mentionLinkDisplay !== 'short'"
path="mentionLinkFadeDomain"
>
{{ $t('settings.mention_link_fade_domain') }}
</BooleanSetting>
</li>
</ul>
</li>
<li>
<BooleanSetting
path="mentionLinkShowTooltip"
expert="1"
>
{{ $t('settings.mention_link_use_tooltip') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="mentionLinkShowAvatar">
{{ $t('settings.mention_link_show_avatar') }}
</BooleanSetting>
</li>
<li v-if="user">
<BooleanSetting
path="mentionLinkBoldenYou"
expert="1"
>
{{ $t('settings.mention_link_bolden_you') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting
v-if="user"
source="profile"
path="stripRichContent"
expert="1"
>
{{ $t('settings.no_rich_text_description') }}
</BooleanSetting>
<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>
</li>
</ul>
<h3>{{ $t('settings.attachments') }}</h3>
<ul class="setting-list">
<li>
<BooleanSetting path="stopGifs">
{{ $t('settings.stop_gifs') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="hideNsfw">
{{ $t('settings.nsfw_clickthrough') }}
</BooleanSetting>
<ul class="setting-list suboptions">
<li>
<BooleanSetting
path="preloadImage"
expert="1"
parent-path="hideNsfw"
>
{{ $t('settings.preload_images') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting
path="useOneClickNsfw"
expert="1"
parent-path="hideNsfw"
>
{{ $t('settings.use_one_click_nsfw') }}
</BooleanSetting>
</li>
</ul>
</li>
<li>
<BooleanSetting
path="loopVideo"
expert="1"
>
{{ $t('settings.loop_video') }}
</BooleanSetting>
<ul class="setting-list suboptions">
<li>
<BooleanSetting
path="loopVideoSilentOnly"
expert="1"
parent-path="loopVideo"
:disabled="!loopSilentAvailable"
>
{{ $t('settings.loop_video_silent_only') }}
</BooleanSetting>
<div
v-if="!loopSilentAvailable"
class="unavailable"
>
<FAIcon icon="globe" />! {{ $t('settings.limited_availability') }}
</div>
</li>
</ul>
</li>
<li>
<BooleanSetting
path="playVideosInModal"
expert="1"
>
{{ $t('settings.play_videos_in_modal') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting
path="useContainFit"
expert="1"
>
{{ $t('settings.use_contain_fit') }}
</BooleanSetting>
</li>
</ul>
<h3 v-if="expertLevel > 0">
{{ $t('settings.fun') }}
</h3>
<ul
v-if="expertLevel > 0"
class="setting-list"
>
<li v-if="user">
<BooleanSetting path="mentionLinkShowYous">
{{ $t('settings.show_yous') }}
</BooleanSetting>
</li>
</ul>
</div>
</div>
</template>
<script src="./posts_tab.js"></script>
<style src="./posts_tab.scss"></style>

View file

@ -1,7 +1,6 @@
<template> <template>
<div class="profile-tab"> <div class="profile-tab">
<div class="setting-item profile-edit"> <div class="setting-item profile-edit">
<h2>{{ $t('settings.account_profile_edit') }}</h2>
<UserCard <UserCard
:user-id="user.id" :user-id="user.id"
:editable="true" :editable="true"
@ -9,7 +8,7 @@
/> />
</div> </div>
<div class="setting-item"> <div class="setting-item">
<h2>{{ $t('settings.account_privacy') }}</h2> <h3>{{ $t('settings.account_privacy') }}</h3>
<ul class="setting-list"> <ul class="setting-list">
<li> <li>
<Checkbox v-model="locked"> <Checkbox v-model="locked">

View file

@ -1,31 +1,35 @@
<template> <template>
<div :label="$t('settings.security_tab')"> <div :label="$t('settings.security_tab')">
<div class="setting-item"> <div class="setting-item">
<h2>{{ $t('settings.change_email') }}</h2> <h3>{{ $t('settings.change_email') }}</h3>
<div> <ul class="setting-list">
<p>{{ $t('settings.new_email') }}</p> <li>
<h4>{{ $t('settings.new_email') }}</h4>
<input <input
v-model="newEmail" v-model="newEmail"
type="email" type="email"
autocomplete="email" autocomplete="email"
class="input" class="input"
> >
</div> </li>
<div> <li>
<p>{{ $t('settings.current_password') }}</p> <h4>{{ $t('settings.current_password') }}</h4>
<input <input
v-model="changeEmailPassword" v-model="changeEmailPassword"
type="password" type="password"
autocomplete="current-password" autocomplete="current-password"
class="input" class="input"
> >
</div> </li>
<li>
<button <button
class="btn button-default" class="btn button-default"
@click="changeEmail" @click="changeEmail"
> >
{{ $t('settings.save') }} {{ $t('settings.save') }}
</button> </button>
</li>
<li>
<p v-if="changedEmail"> <p v-if="changedEmail">
{{ $t('settings.changed_email') }} {{ $t('settings.changed_email') }}
</p> </p>
@ -33,40 +37,46 @@
<p>{{ $t('settings.change_email_error') }}</p> <p>{{ $t('settings.change_email_error') }}</p>
<p>{{ changeEmailError }}</p> <p>{{ changeEmailError }}</p>
</template> </template>
</li>
</ul>
</div> </div>
<div class="setting-item"> <div class="setting-item">
<h2>{{ $t('settings.change_password') }}</h2> <h3>{{ $t('settings.change_password') }}</h3>
<div> <ul class="setting-list">
<p>{{ $t('settings.current_password') }}</p> <li>
<h4>{{ $t('settings.current_password') }}</h4>
<input <input
v-model="changePasswordInputs[0]" v-model="changePasswordInputs[0]"
type="password" type="password"
class="input" class="input"
> >
</div> </li>
<div> <li>
<p>{{ $t('settings.new_password') }}</p> <h4>{{ $t('settings.new_password') }}</h4>
<input <input
v-model="changePasswordInputs[1]" v-model="changePasswordInputs[1]"
type="password" type="password"
class="input" class="input"
> >
</div> </li>
<div> <li>
<p>{{ $t('settings.confirm_new_password') }}</p> <h4>{{ $t('settings.confirm_new_password') }}</h4>
<input <input
v-model="changePasswordInputs[2]" v-model="changePasswordInputs[2]"
type="password" type="password"
class="input" class="input"
> >
</div> </li>
<li>
<button <button
class="btn button-default" class="btn button-default"
@click="changePassword" @click="changePassword"
> >
{{ $t('settings.save') }} {{ $t('settings.save') }}
</button> </button>
</li>
<li>
<p v-if="changedPassword"> <p v-if="changedPassword">
{{ $t('settings.changed_password') }} {{ $t('settings.changed_password') }}
</p> </p>
@ -76,41 +86,12 @@
<p v-if="changePasswordError"> <p v-if="changePasswordError">
{{ changePasswordError }} {{ changePasswordError }}
</p> </p>
</li>
</ul>
</div> </div>
<div class="setting-item"> <div class="setting-item">
<h2>{{ $t('settings.oauth_tokens') }}</h2> <h3>{{ $t('settings.account_alias') }}</h3>
<table class="oauth-tokens">
<thead>
<tr>
<th>{{ $t('settings.app_name') }}</th>
<th>{{ $t('settings.valid_until') }}</th>
<th />
</tr>
</thead>
<tbody>
<tr
v-for="oauthToken in oauthTokens"
:key="oauthToken.id"
>
<td>{{ oauthToken.appName }}</td>
<td>{{ oauthToken.validUntil }}</td>
<td class="actions">
<button
class="btn button-default"
@click="revokeToken(oauthToken.id)"
>
{{ $t('settings.revoke_token') }}
</button>
</td>
</tr>
</tbody>
</table>
</div>
<mfa />
<div class="setting-item">
<h2>{{ $t('settings.account_alias') }}</h2>
<table> <table>
<thead> <thead>
<tr> <tr>
@ -178,8 +159,40 @@
</template> </template>
</div> </div>
<div class="setting-item"> <div class="setting-item">
<h2>{{ $t('settings.move_account') }}</h2> <h3>{{ $t('settings.oauth_tokens') }}</h3>
<table class="oauth-tokens">
<thead>
<tr>
<th>{{ $t('settings.app_name') }}</th>
<th>{{ $t('settings.valid_until') }}</th>
<th />
</tr>
</thead>
<tbody>
<tr
v-for="oauthToken in oauthTokens"
:key="oauthToken.id"
>
<td>{{ oauthToken.appName }}</td>
<td>{{ oauthToken.validUntil }}</td>
<td class="actions">
<button
class="btn button-default"
@click="revokeToken(oauthToken.id)"
>
{{ $t('settings.revoke_token') }}
</button>
</td>
</tr>
</tbody>
</table>
</div>
<mfa />
<div class="setting-item">
<h3>{{ $t('settings.move_account') }}</h3>
<p>{{ $t('settings.move_account_notes') }}</p> <p>{{ $t('settings.move_account_notes') }}</p>
<div> <div>
<i18n-t <i18n-t
@ -222,7 +235,7 @@
</div> </div>
<div class="setting-item"> <div class="setting-item">
<h2>{{ $t('settings.delete_account') }}</h2> <h3>{{ $t('settings.delete_account') }}</h3>
<p v-if="!deletingAccount"> <p v-if="!deletingAccount">
{{ $t('settings.delete_account_description') }} {{ $t('settings.delete_account_description') }}
</p> </p>

View file

@ -15,7 +15,7 @@ import RoundnessInput from 'src/components/roundness_input/roundness_input.vue'
import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx' import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'
import Tooltip from 'src/components/tooltip/tooltip.vue' import Tooltip from 'src/components/tooltip/tooltip.vue'
import ContrastRatio from 'src/components/contrast_ratio/contrast_ratio.vue' import ContrastRatio from 'src/components/contrast_ratio/contrast_ratio.vue'
import Preview from '../theme_tab/theme_preview.vue' import Preview from '../old_theme_tab/theme_preview.vue'
import VirtualDirectivesTab from './virtual_directives_tab.vue' import VirtualDirectivesTab from './virtual_directives_tab.vue'

View file

@ -2,14 +2,13 @@
min-width: var(--themeEditorMinWidth, fit-content); min-width: var(--themeEditorMinWidth, fit-content);
.style-control { .style-control {
display: flex;
flex-wrap: wrap;
align-items: baseline; align-items: baseline;
margin-bottom: 0.5em; margin-bottom: 0.5em;
.label { .label {
display: inline-block;
margin-right: 0.5em; margin-right: 0.5em;
flex: 1 1 0; flex: 1 1 auto;
line-height: 2; line-height: 2;
min-height: 2em; min-height: 2em;
} }
@ -18,10 +17,6 @@
margin-left: 1em; margin-left: 1em;
} }
.color-input {
flex: 0 0 0;
}
input, input,
select { select {
min-width: 3em; min-width: 3em;
@ -50,21 +45,24 @@
} }
} }
.setting-item.heading {
padding: 0;
}
.meta-preview { .meta-preview {
display: grid; display: grid;
grid-template: grid-template:
"meta meta preview preview" "meta preview";
"meta meta preview preview" grid-gap: 1em;
"meta meta preview preview" grid-template-columns: min-content 6fr;
"meta meta preview preview";
grid-gap: 0.5em; .theme-preview-container {
grid-template-columns: min-content min-content 6fr max-content; margin: 0;
}
ul.setting-list { ul.setting-list {
padding: 0; padding: 0;
margin: 0; margin: 0;
display: grid;
grid-template-rows: subgrid;
grid-area: meta; grid-area: meta;
> li { > li {
@ -114,6 +112,7 @@
.list-edit-area { .list-edit-area {
grid-area: editor; grid-area: editor;
align-items: baseline;
} }
.list-select { .list-select {
@ -134,21 +133,6 @@
} }
} }
.palette-editor {
width: min-content;
.list-edit-area {
display: grid;
align-self: baseline;
grid-template-rows: subgrid;
grid-template-columns: 1fr;
}
.palette-editor-single {
grid-row: 2 / span 2;
}
}
.variables-editor { .variables-editor {
.variable-selector { .variable-selector {
display: grid; display: grid;
@ -249,6 +233,46 @@
justify-items: center; justify-items: center;
} }
} }
.-mobile & {
.meta-preview {
grid-template:
"meta"
"preview"
}
.list-editor {
display: grid;
grid-template-areas:
"label"
"selector"
"movement"
"editor"
"editor"
"editor"
"editor";
grid-template-columns: 1fr;
grid-template-rows: auto 1fr auto;
}
.component-editor {
grid-template-columns: 1fr;
grid-template-rows: auto;
grid-template-areas:
"component"
"variant"
"state"
"preview"
"settings";
}
.variable-selector {
grid-template-columns: 1fr;
grid-template-rows: auto;
grid-auto-flow: row;
grid-gap: 0.5em;
}
}
} }
.extra-content { .extra-content {

View file

@ -4,7 +4,6 @@
<template> <template>
<div class="StyleTab"> <div class="StyleTab">
<div class="setting-item heading"> <div class="setting-item heading">
<h2> {{ $t('settings.style.themes3.editor.title') }} </h2>
<div class="meta-preview"> <div class="meta-preview">
<Preview id="edited-style-preview" /> <Preview id="edited-style-preview" />
<teleport <teleport
@ -85,6 +84,7 @@
key="component" key="component"
class="setting-item component-editor" class="setting-item component-editor"
:label="$t('settings.style.themes3.editor.component_tab')" :label="$t('settings.style.themes3.editor.component_tab')"
:full-width="true"
> >
<div class="component-selector"> <div class="component-selector">
<label for="component-selector"> <label for="component-selector">
@ -332,6 +332,7 @@
key="palette" key="palette"
:label="$t('settings.style.themes3.editor.palette_tab')" :label="$t('settings.style.themes3.editor.palette_tab')"
class="setting-item list-editor palette-editor" class="setting-item list-editor palette-editor"
:full-width="true"
> >
<label <label
class="list-select-label" class="list-select-label"
@ -379,6 +380,7 @@
key="variables" key="variables"
:label="$t('settings.style.themes3.editor.variables_tab')" :label="$t('settings.style.themes3.editor.variables_tab')"
:model-value="virtualDirectives" :model-value="virtualDirectives"
:full-width="true"
@update:model-value="updateVirtualDirectives" @update:model-value="updateVirtualDirectives"
/> />
</tab-switcher> </tab-switcher>

View file

@ -1,19 +0,0 @@
const pleromaFeCommitUrl = 'https://git.pleroma.social/pleroma/pleroma-fe/commit/'
const VersionTab = {
data () {
const instance = this.$store.state.instance
return {
backendVersion: instance.backendVersion,
backendRepository: instance.backendRepository,
frontendVersion: instance.frontendVersion
}
},
computed: {
frontendVersionLink () {
return pleromaFeCommitUrl + this.frontendVersion
}
}
}
export default VersionTab

View file

@ -1,31 +0,0 @@
<template>
<div :label="$t('settings.version.title')">
<div class="setting-item">
<ul class="setting-list">
<li>
<p>{{ $t('settings.version.backend_version') }}</p>
<ul class="option-list">
<li>
<a
:href="backendRepository"
target="_blank"
>{{ backendVersion }}</a>
</li>
</ul>
</li>
<li>
<p>{{ $t('settings.version.frontend_version') }}</p>
<ul class="option-list">
<li>
<a
:href="frontendVersionLink"
target="_blank"
>{{ frontendVersion }}</a>
</li>
</ul>
</li>
</ul>
</div>
</div>
</template>
<script src="./version_tab.js" />

View file

@ -6,6 +6,10 @@
grid-gap: 0.5em; grid-gap: 0.5em;
justify-content: stretch; justify-content: stretch;
.style-control {
display: flex;
}
&.-compact { &.-compact {
grid-template-columns: 10em 1fr; grid-template-columns: 10em 1fr;
grid-template-rows: auto auto; grid-template-rows: auto auto;
@ -110,7 +114,6 @@
.shadow-preview { .shadow-preview {
grid-area: preview; grid-area: preview;
min-width: 25em;
margin-left: 0.125em; margin-left: 0.125em;
place-self: start center; place-self: start center;
} }

View file

@ -168,6 +168,7 @@
:disabled="disabled || !present" :disabled="disabled || !present"
:label="$t('settings.style.common.color')" :label="$t('settings.style.common.color')"
:fallback="getColorFallback" :fallback="getColorFallback"
:compact="true"
:show-optional-checkbox="false" :show-optional-checkbox="false"
name="shadow" name="shadow"
@update:model-value="e => updateProperty('color', e)" @update:model-value="e => updateProperty('color', e)"

View file

@ -31,11 +31,6 @@ export default {
type: Boolean, type: Boolean,
default: false default: false
}, },
sideTabBar: {
required: false,
type: Boolean,
default: false
},
bodyScrollLock: { bodyScrollLock: {
required: false, required: false,
type: Boolean, type: Boolean,
@ -143,8 +138,11 @@ export default {
if (!props) return if (!props) return
const active = this.activeIndex === index const active = this.activeIndex === index
const classes = [ active ? 'active' : 'hidden' ] const classes = [ active ? 'active' : 'hidden' ]
if (props.fullHeight) { if (props.fullHeight || props['full-height']) {
classes.push('full-height') classes.push('-full-height')
}
if (props.fullWidth || props['full-width']) {
classes.push('-full-width')
} }
let delayRender = slot.props['delay-render'] let delayRender = slot.props['delay-render']
if (delayRender && active) { if (delayRender && active) {
@ -157,29 +155,28 @@ export default {
return ( return (
<div class={classes}> <div class={classes}>
{
this.sideTabBar
? <h1 class="mobile-label">{props.label}</h1>
: ''
}
{renderSlot} {renderSlot}
</div> </div>
) )
}) })
return ( return (
<div class={'tab-switcher ' + (this.sideTabBar ? 'side-tabs' : 'top-tabs')}> <div
class="tab-switcher top-tabs"
ref="root"
>
<div <div
class="tabs" class="tabs"
role="tablist" role="tablist"
ref="nav"
> >
{tabs} {tabs}
</div> </div>
<div <div
ref="contents"
role="tabpanel" role="tabpanel"
class={'contents' + (this.scrollableTabs ? ' scrollable-tabs' : '')} class={'contents' + (this.scrollableTabs ? ' scrollable-tabs' : '')}
v-body-scroll-lock={this.bodyScrollLock} v-body-scroll-lock={this.bodyScrollLock}
ref="content"
> >
{contents} {contents}
</div> </div>

View file

@ -52,104 +52,6 @@
} }
} }
&.side-tabs {
flex-direction: row;
@media all and (width <= 800px) {
overflow-x: auto;
}
> .contents {
flex: 1 1 auto;
}
> .tabs {
flex: 0 0 auto;
overflow: hidden auto;
flex-direction: column;
&::after,
&::before {
flex-shrink: 0;
flex-basis: 0.5em;
content: "";
border-right: 1px solid;
border-right-color: var(--border);
}
&::after {
flex-grow: 1;
}
&::before {
flex-grow: 0;
}
.tab-wrapper {
min-width: 10em;
display: flex;
flex-direction: column;
@media all and (width <= 800px) {
min-width: 4em;
}
&:not(.active)::after {
top: 0;
right: 0;
bottom: 0;
border-right: 1px solid;
border-right-color: var(--border);
}
&::before {
flex: 0 0 6px;
content: "";
border-right: 1px solid;
border-right-color: var(--border);
}
&:last-child .tab {
margin-bottom: 0;
}
}
.tab {
flex: 1;
box-sizing: content-box;
max-width: 9em;
min-width: 1px;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
padding-left: 1em;
padding-right: calc(1em + 200px);
margin-right: -200px;
margin-left: 1em;
&:not(.active) {
margin-top: 0;
margin-left: 1.5em;
}
@media all and (width <= 800px) {
padding-left: 0.25em;
padding-right: calc(0.25em + 200px);
margin-right: calc(0.25em - 200px);
margin-left: 0.25em;
&:not(.active) {
margin-top: 0;
margin-left: 0.5em;
}
.text {
display: none;
}
}
}
}
}
.contents { .contents {
flex: 1 0 auto; flex: 1 0 auto;
min-height: 0; min-height: 0;
@ -158,7 +60,7 @@
display: none; display: none;
} }
.full-height:not(.hidden) { .-full-height:not(.hidden) {
height: 100%; height: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -168,6 +70,15 @@
} }
} }
.-full-width:not(.hidden) {
display: flex;
flex-direction: column;
> *:not(.mobile-label) {
width: auto;
}
}
&.scrollable-tabs { &.scrollable-tabs {
overflow-y: auto; overflow-y: auto;
} }

View file

@ -132,6 +132,7 @@
}, },
"importer": { "importer": {
"submit": "Submit", "submit": "Submit",
"import": "Import",
"success": "Imported successfully.", "success": "Imported successfully.",
"error": "An error occured while importing this file." "error": "An error occured while importing this file."
}, },
@ -403,7 +404,10 @@
"setting_server_side": "This setting is tied to your profile and affects all sessions and clients", "setting_server_side": "This setting is tied to your profile and affects all sessions and clients",
"enter_current_password_to_confirm": "Enter your current password to confirm your identity", "enter_current_password_to_confirm": "Enter your current password to confirm your identity",
"post_look_feel": "Posts Look & Feel", "post_look_feel": "Posts Look & Feel",
"mention_links": "Mention links", "posts": "Posts",
"developer": "Developer",
"debug": "Debug",
"mention_links": "Mention Links",
"appearance": "Appearance", "appearance": "Appearance",
"confirm_new_setting": "Confirm new setting?", "confirm_new_setting": "Confirm new setting?",
"confirm_new_question": "Does this look ok? Setting will be reverted in 10 seconds.", "confirm_new_question": "Does this look ok? Setting will be reverted in 10 seconds.",
@ -416,9 +420,14 @@
"navbar_size": "Top bar size", "navbar_size": "Top bar size",
"panel_header_size": "Panel header size", "panel_header_size": "Panel header size",
"visual_tweaks": "Minor visual tweaks", "visual_tweaks": "Minor visual tweaks",
"theme_debug": "Show what background theme engine assumes when dealing with transparancy (DEBUG)", "theme_debug": "Show what background theme engine assumes when dealing with transparancy",
"scale_and_layout": "Interface scale and layout", "scale_and_layout": "Interface scale and layout",
"timelines": "Timelines",
"format_and_language": "Format and Language",
"confirmations": "Confirmations",
"layout": "Layout",
"enabled": "Enabled", "enabled": "Enabled",
"clutter": "Clutter",
"filter": { "filter": {
"clutter": "Remove clutter", "clutter": "Remove clutter",
"mute_filter": "Mute Filters", "mute_filter": "Mute Filters",
@ -533,6 +542,7 @@
"chatMessageRadius": "Chat message", "chatMessageRadius": "Chat message",
"collapse_subject": "Collapse posts with subjects", "collapse_subject": "Collapse posts with subjects",
"composing": "Composing", "composing": "Composing",
"replies": "Replying",
"confirm_new_password": "Confirm new password", "confirm_new_password": "Confirm new password",
"current_password": "Current password", "current_password": "Current password",
"confirm_dialogs": "Ask for confirmation when", "confirm_dialogs": "Ask for confirmation when",
@ -594,6 +604,12 @@
"follow_export_button": "Export your follows to a csv file", "follow_export_button": "Export your follows to a csv file",
"follow_import": "Follow import", "follow_import": "Follow import",
"follow_import_error": "Error importing followers", "follow_import_error": "Error importing followers",
"import_export": {
"title": "Import / Export",
"follows": "List of users you follow",
"blocks": "List of users you block",
"mutes": "List of users you mute"
},
"follows_imported": "Follows imported! Processing them will take a while.", "follows_imported": "Follows imported! Processing them will take a while.",
"accent": "Accent", "accent": "Accent",
"foreground": "Foreground", "foreground": "Foreground",
@ -745,7 +761,7 @@
"subject_line_email": "Like email: \"re: subject\"", "subject_line_email": "Like email: \"re: subject\"",
"subject_line_mastodon": "Like mastodon: copy as is", "subject_line_mastodon": "Like mastodon: copy as is",
"subject_line_noop": "Do not copy", "subject_line_noop": "Do not copy",
"force_theme_recompilation_debug": "Disable theme cahe, force recompile on each boot (DEBUG)", "force_theme_recompilation_debug": "Disable theme cahe, force recompile on each boot",
"conversation_display": "Conversation display style", "conversation_display": "Conversation display style",
"conversation_display_tree": "Tree-style", "conversation_display_tree": "Tree-style",
"conversation_display_tree_quick": "Tree view", "conversation_display_tree_quick": "Tree view",
@ -760,6 +776,8 @@
"column_sizes_sidebar": "Sidebar", "column_sizes_sidebar": "Sidebar",
"column_sizes_content": "Content", "column_sizes_content": "Content",
"column_sizes_notifs": "Notifications", "column_sizes_notifs": "Notifications",
"layout": "Layout",
"scale_and_font": "Scale and Font",
"theme_editor_min_width": "Minimum width of theme editor (0 for \"fit-content\")", "theme_editor_min_width": "Minimum width of theme editor (0 for \"fit-content\")",
"tree_advanced": "Allow more flexible navigation in tree view", "tree_advanced": "Allow more flexible navigation in tree view",
"tree_fade_ancestors": "Display ancestors of the current status in faint text", "tree_fade_ancestors": "Display ancestors of the current status in faint text",
@ -770,6 +788,7 @@
"conversation_other_replies_button_inside": "Inside statuses", "conversation_other_replies_button_inside": "Inside statuses",
"max_depth_in_thread": "Maximum number of levels in thread to display by default", "max_depth_in_thread": "Maximum number of levels in thread to display by default",
"post_status_content_type": "Post status content type", "post_status_content_type": "Post status content type",
"default_post_status_content_type": "Default post status content type",
"sensitive_by_default": "Mark posts as sensitive by default", "sensitive_by_default": "Mark posts as sensitive by default",
"stop_gifs": "Pause animated images until you hover on them", "stop_gifs": "Pause animated images until you hover on them",
"streaming": "Automatically show new posts when scrolled to the top", "streaming": "Automatically show new posts when scrolled to the top",
@ -811,8 +830,11 @@
"user_popover_avatar_overlay": "Show user popover over user avatar", "user_popover_avatar_overlay": "Show user popover over user avatar",
"user_card_left_justify": "Justify user bio to the left", "user_card_left_justify": "Justify user bio to the left",
"user_card_hide_personal_marks": "Hide personal marks (highlight/note) in user profiles", "user_card_hide_personal_marks": "Hide personal marks (highlight/note) in user profiles",
"posts_appearance": "Posts Appearance",
"fun": "Fun", "fun": "Fun",
"greentext": "Meme arrows", "greentext": "Meme arrows",
"plaintext_quotes": "Highlight plaintext {0}",
"greentext_quotes": ">quotes",
"show_yous": "Show (You)s", "show_yous": "Show (You)s",
"notifications": "Notifications", "notifications": "Notifications",
"notification_setting_annoyance": "Annoyance", "notification_setting_annoyance": "Annoyance",
@ -832,11 +854,13 @@
"enable_web_push_always_show_tip": "Some browsers (Chromium, Chrome) require that push messages always result in a notification, otherwise generic 'Website was updated in background' is shown, enable this to prevent this notification from showing, as Chrome seem to hide push notifications if tab is in focus. Can result in showing duplicate notifications on other browsers.", "enable_web_push_always_show_tip": "Some browsers (Chromium, Chrome) require that push messages always result in a notification, otherwise generic 'Website was updated in background' is shown, enable this to prevent this notification from showing, as Chrome seem to hide push notifications if tab is in focus. Can result in showing duplicate notifications on other browsers.",
"more_settings": "More settings", "more_settings": "More settings",
"style": { "style": {
"style_section": "Style",
"custom_theme_used": "(Custom theme)", "custom_theme_used": "(Custom theme)",
"custom_style_used": "(Custom style)", "custom_style_used": "(Custom style)",
"stock_theme_used": "(Stock theme)", "stock_theme_used": "(Stock theme)",
"themes2_outdated": "Editor for Themes V2 is being phased out and will eventually be replaced with a new one that takes advantage of new Themes V3 engine. It should still work but experience might be degraded and inconsistent.", "themes2_outdated": "Editor for Themes V2 is being phased out and will eventually be replaced with a new one that takes advantage of new Themes V3 engine. It should still work but experience might be degraded and inconsistent.",
"appearance_tab_note": "Changes on this tab do not affect the theme used, so exported theme will be different from what seen in the UI", "appearance_tab_note": "Changes on this tab do not affect the theme used, so exported theme will be different from what seen in the UI",
"visual_tweaks_section_note": "Changes in this section do not affect the theme used, exported theme will be different from what seen in the UI",
"update_preview": "Update preview", "update_preview": "Update preview",
"themes3": { "themes3": {
"define": "Override", "define": "Override",
@ -1075,6 +1099,13 @@
"post": "Post text", "post": "Post text",
"monospace": "Monospaced text" "monospace": "Monospaced text"
}, },
"components_inline": {
"interface": "interface",
"input": "input fields",
"post": "post text",
"monospace": "monospaced text"
},
"override": "Override {0} font",
"family": "Font name", "family": "Font name",
"size": "Size (in px)", "size": "Size (in px)",
"weight": "Weight (boldness)", "weight": "Weight (boldness)",