Merge branch 'settings-shuffle' into shigusegubu-themes3

This commit is contained in:
Henry Jameson 2025-11-24 20:54:21 +02:00
commit 1abbba698d
61 changed files with 2948 additions and 1940 deletions

View file

@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## 2.9.3
### Fixed
- Being unable to update profile
## 2.9.2 ## 2.9.2
### Changed ### Changed
- BREAKING: due to some internal technical changes logging into AdminFE through PleromaFE is no longer possible - BREAKING: due to some internal technical changes logging into AdminFE through PleromaFE is no longer possible

2
changelog.d/broken.fix Normal file
View file

@ -0,0 +1,2 @@
Fix display of the broken/deleted/banned users

View file

@ -0,0 +1 @@
Make every configuration option default-overridable by instance admins

View file

@ -1,6 +1,6 @@
{ {
"name": "pleroma_fe", "name": "pleroma_fe",
"version": "2.9.2", "version": "2.9.3",
"description": "Pleroma frontend, the default frontend of Pleroma social network server", "description": "Pleroma frontend, the default frontend of Pleroma social network server",
"author": "Pleroma contributors <https://git.pleroma.social/pleroma/pleroma-fe/-/blob/develop/CONTRIBUTORS.md>", "author": "Pleroma contributors <https://git.pleroma.social/pleroma/pleroma-fe/-/blob/develop/CONTRIBUTORS.md>",
"private": false, "private": false,
@ -17,12 +17,12 @@
"lint-fix": "eslint --fix src test/unit/specs test/e2e/specs" "lint-fix": "eslint --fix src test/unit/specs test/e2e/specs"
}, },
"dependencies": { "dependencies": {
"@babel/runtime": "7.28.3", "@babel/runtime": "7.28.4",
"@chenfengyuan/vue-qrcode": "2.0.0", "@chenfengyuan/vue-qrcode": "2.0.0",
"@fortawesome/fontawesome-svg-core": "6.7.2", "@fortawesome/fontawesome-svg-core": "7.1.0",
"@fortawesome/free-regular-svg-icons": "6.7.2", "@fortawesome/free-regular-svg-icons": "7.1.0",
"@fortawesome/free-solid-svg-icons": "6.7.2", "@fortawesome/free-solid-svg-icons": "7.1.0",
"@fortawesome/vue-fontawesome": "3.1.1", "@fortawesome/vue-fontawesome": "3.1.2",
"@kazvmoe-infra/pinch-zoom-element": "1.3.0", "@kazvmoe-infra/pinch-zoom-element": "1.3.0",
"@kazvmoe-infra/unicode-emoji-json": "0.4.0", "@kazvmoe-infra/unicode-emoji-json": "0.4.0",
"@ruffle-rs/ruffle": "0.1.0-nightly.2025.6.22", "@ruffle-rs/ruffle": "0.1.0-nightly.2025.6.22",
@ -39,7 +39,7 @@
"js-cookie": "3.0.5", "js-cookie": "3.0.5",
"localforage": "1.10.0", "localforage": "1.10.0",
"parse-link-header": "2.0.0", "parse-link-header": "2.0.0",
"phoenix": "1.8.0", "phoenix": "1.8.1",
"pinia": "^3.0.0", "pinia": "^3.0.0",
"punycode.js": "2.3.1", "punycode.js": "2.3.1",
"qrcode": "1.5.4", "qrcode": "1.5.4",
@ -47,15 +47,15 @@
"url": "0.11.4", "url": "0.11.4",
"utf8": "3.0.0", "utf8": "3.0.0",
"uuid": "11.1.0", "uuid": "11.1.0",
"vue": "3.5.19", "vue": "3.5.22",
"vue-i18n": "11", "vue-i18n": "11",
"vue-router": "4.5.1", "vue-router": "4.5.1",
"vue-virtual-scroller": "^2.0.0-beta.7", "vue-virtual-scroller": "^2.0.0-beta.7",
"vuex": "4.1.0" "vuex": "4.1.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "7.28.3", "@babel/core": "7.28.4",
"@babel/eslint-parser": "7.28.0", "@babel/eslint-parser": "7.28.4",
"@babel/plugin-transform-runtime": "7.28.3", "@babel/plugin-transform-runtime": "7.28.3",
"@babel/preset-env": "7.28.3", "@babel/preset-env": "7.28.3",
"@babel/register": "7.28.3", "@babel/register": "7.28.3",
@ -66,24 +66,24 @@
"@vitest/ui": "^3.0.7", "@vitest/ui": "^3.0.7",
"@vue/babel-helper-vue-jsx-merge-props": "1.4.0", "@vue/babel-helper-vue-jsx-merge-props": "1.4.0",
"@vue/babel-plugin-jsx": "1.5.0", "@vue/babel-plugin-jsx": "1.5.0",
"@vue/compiler-sfc": "3.5.19", "@vue/compiler-sfc": "3.5.22",
"@vue/test-utils": "2.4.6", "@vue/test-utils": "2.4.6",
"autoprefixer": "10.4.21", "autoprefixer": "10.4.21",
"babel-plugin-lodash": "3.3.4", "babel-plugin-lodash": "3.3.4",
"chai": "5.3.2", "chai": "5.3.3",
"chalk": "5.6.0", "chalk": "5.6.2",
"chromedriver": "135.0.4", "chromedriver": "135.0.4",
"connect-history-api-fallback": "2.0.0", "connect-history-api-fallback": "2.0.0",
"cross-spawn": "7.0.6", "cross-spawn": "7.0.6",
"custom-event-polyfill": "1.0.7", "custom-event-polyfill": "1.0.7",
"eslint": "9.33.0", "eslint": "9.37.0",
"vue-eslint-parser": "10.2.0", "vue-eslint-parser": "10.2.0",
"eslint-config-standard": "17.1.0", "eslint-config-standard": "17.1.0",
"eslint-formatter-friendly": "7.0.0", "eslint-formatter-friendly": "7.0.0",
"eslint-plugin-import": "2.32.0", "eslint-plugin-import": "2.32.0",
"eslint-plugin-n": "17.21.3", "eslint-plugin-n": "17.23.1",
"eslint-plugin-promise": "7.2.1", "eslint-plugin-promise": "7.2.1",
"eslint-plugin-vue": "10.4.0", "eslint-plugin-vue": "10.5.0",
"eventsource-polyfill": "0.9.6", "eventsource-polyfill": "0.9.6",
"express": "5.1.0", "express": "5.1.0",
"function-bind": "1.1.2", "function-bind": "1.1.2",
@ -96,14 +96,14 @@
"postcss": "8.5.6", "postcss": "8.5.6",
"postcss-html": "^1.5.0", "postcss-html": "^1.5.0",
"postcss-scss": "^4.0.6", "postcss-scss": "^4.0.6",
"sass": "1.89.2", "sass": "1.93.2",
"selenium-server": "3.141.59", "selenium-server": "3.141.59",
"semver": "7.7.2", "semver": "7.7.3",
"serve-static": "2.2.0", "serve-static": "2.2.0",
"shelljs": "0.10.0", "shelljs": "0.10.0",
"sinon": "20.0.0", "sinon": "20.0.0",
"sinon-chai": "4.0.0", "sinon-chai": "4.0.1",
"stylelint": "16.19.1", "stylelint": "16.25.0",
"stylelint-config-html": "^1.1.0", "stylelint-config-html": "^1.1.0",
"stylelint-config-recommended": "^16.0.0", "stylelint-config-recommended": "^16.0.0",
"stylelint-config-recommended-scss": "^14.0.0", "stylelint-config-recommended-scss": "^14.0.0",

View file

@ -24,6 +24,7 @@ import { useI18nStore } from 'src/stores/i18n'
import { useInterfaceStore } from 'src/stores/interface' import { useInterfaceStore } from 'src/stores/interface'
import { useAnnouncementsStore } from 'src/stores/announcements' import { useAnnouncementsStore } from 'src/stores/announcements'
import { useAuthFlowStore } from 'src/stores/auth_flow' import { useAuthFlowStore } from 'src/stores/auth_flow'
import { staticOrApiConfigDefault, instanceDefaultConfig } from 'src/modules/default_config_state.js'
let staticInitialResults = null let staticInitialResults = null
@ -130,50 +131,15 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => {
} }
const copyInstanceOption = (name) => { const copyInstanceOption = (name) => {
store.dispatch('setInstanceOption', { name, value: config[name] }) if (typeof config[name] !== 'undefined') {
store.dispatch('setInstanceOption', { name, value: config[name] })
}
} }
copyInstanceOption('theme') Object.keys(staticOrApiConfigDefault).forEach(copyInstanceOption)
copyInstanceOption('style') Object.keys(instanceDefaultConfig).forEach(copyInstanceOption)
copyInstanceOption('palette')
copyInstanceOption('embeddedToS')
copyInstanceOption('nsfwCensorImage')
copyInstanceOption('background')
copyInstanceOption('hidePostStats')
copyInstanceOption('hideBotIndication')
copyInstanceOption('hideUserStats')
copyInstanceOption('hideFilteredStatuses')
copyInstanceOption('logo')
store.dispatch('setInstanceOption', {
name: 'logoMask',
value: typeof config.logoMask === 'undefined'
? true
: config.logoMask
})
store.dispatch('setInstanceOption', {
name: 'logoMargin',
value: typeof config.logoMargin === 'undefined'
? 0
: config.logoMargin
})
copyInstanceOption('logoLeft')
useAuthFlowStore().setInitialStrategy(config.loginMethod) useAuthFlowStore().setInitialStrategy(config.loginMethod)
copyInstanceOption('redirectRootNoLogin')
copyInstanceOption('redirectRootLogin')
copyInstanceOption('showInstanceSpecificPanel')
copyInstanceOption('minimalScopesMode')
copyInstanceOption('hideMutedPosts')
copyInstanceOption('collapseMessageWithSubject')
copyInstanceOption('scopeCopy')
copyInstanceOption('subjectLineBehavior')
copyInstanceOption('postContentType')
copyInstanceOption('alwaysShowSubjectInput')
copyInstanceOption('showFeaturesPanel')
copyInstanceOption('hideSitename')
copyInstanceOption('sidebarRight')
} }
const getTOS = async ({ store }) => { const getTOS = async ({ store }) => {

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

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

@ -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 {
@ -42,7 +69,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,6 +104,12 @@
} }
&.-mobile { &.-mobile {
.tabs {
.menu-item {
font-size: 1.2em
}
}
.setting-list, .setting-list,
.option-list { .option-list {
padding-left: 0.25em; padding-left: 0.25em;

View file

@ -1,4 +1,4 @@
import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx' import VerticalTabSwitcher from 'src/components/tab_switcher/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 'src/components/tab_switcher/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,15 +7,20 @@ 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,
@ -23,13 +28,16 @@ import {
faDownload, faDownload,
faEyeSlash, faEyeSlash,
faInfo, faInfo,
faWindowRestore faWindowRestore,
faCode,
faBroom
} 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,
faMessage,
faFilter, faFilter,
faPaintBrush, faPaintBrush,
faPalette, faPalette,
@ -37,12 +45,14 @@ library.add(
faDownload, faDownload,
faEyeSlash, faEyeSlash,
faInfo, faInfo,
faWindowRestore faWindowRestore,
faBroom,
faCode
) )
const SettingsModalContent = { const SettingsModalContent = {
components: { components: {
TabSwitcher, VerticalTabSwitcher,
DataImportExportTab, DataImportExportTab,
MutesAndBlocksTab, MutesAndBlocksTab,
@ -51,10 +61,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 +82,12 @@ const SettingsModalContent = {
}, },
expertLevel () { expertLevel () {
return this.$store.state.config.expertLevel return this.$store.state.config.expertLevel
}, }
isMobileLayout () { },
return useInterfaceStore().layoutType === 'mobile' data () {
return {
navCollapsed: false,
navHideHeader: false
} }
}, },
methods: { methods: {

View file

@ -1,21 +1,6 @@
.settings_tab-switcher { .settings_tab-switcher {
height: 100%; height: 100%;
h1 {
margin-bottom: 0.5em;
margin-top: 0.5em;
}
h4 {
margin-bottom: 0;
margin-top: 0.25em;
}
h5 {
margin-bottom: 0;
margin-top: 0.25em;
}
.setting-item { .setting-item {
border-bottom: 2px solid var(--border); border-bottom: 2px solid var(--border);
margin: 1em 1em 1.4em; margin: 1em 1em 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,49 +49,26 @@
<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
v-if="isLoggedIn"
:label="$t('settings.security_tab')"
icon="lock"
data-tab-name="security"
>
<SecurityTab />
</div>
<div <div
:label="$t('settings.filtering')" :label="$t('settings.filtering')"
:full-width="true"
icon="filter" icon="filter"
data-tab-name="filtering" data-tab-name="filtering"
> >
@ -73,12 +77,28 @@
<div <div
v-if="isLoggedIn" v-if="isLoggedIn"
:label="$t('settings.mutes_and_blocks')" :label="$t('settings.mutes_and_blocks')"
:fullHeight="true"
icon="eye-slash" icon="eye-slash"
data-tab-name="mutesAndBlocks" data-tab-name="mutesAndBlocks"
:full-width="true"
> >
<MutesAndBlocksTab /> <MutesAndBlocksTab />
</div> </div>
<div
:label="$t('settings.clutter')"
icon="broom"
data-tab-name="clutter"
>
<ClutterTab />
</div>
<div
v-if="isLoggedIn"
:label="$t('settings.security_tab')"
icon="lock"
data-tab-name="security"
:full-width="true"
>
<SecurityTab />
</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

@ -1,12 +1,12 @@
import VerticalTabSwitcher from 'src/components/tab_switcher/vertical_tab_switcher.jsx'
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 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'
@ -26,11 +26,17 @@ import { useInterfaceStore, normalizeThemeData } from 'src/stores/interface'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { import {
faGlobe faGlobe,
faDashboard,
faPaintRoller,
faTableColumns
} from '@fortawesome/free-solid-svg-icons' } from '@fortawesome/free-solid-svg-icons'
library.add( library.add(
faGlobe faGlobe,
faPaintRoller,
faDashboard,
faTableColumns
) )
const AppearanceTab = { const AppearanceTab = {
@ -59,11 +65,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,9 +87,9 @@ const AppearanceTab = {
FloatSetting, FloatSetting,
UnitSetting, UnitSetting,
ProfileSettingIndicator, ProfileSettingIndicator,
FontControl,
Preview, Preview,
PaletteEditor PaletteEditor,
VerticalTabSwitcher
}, },
mounted () { mounted () {
useInterfaceStore().getThemeData() useInterfaceStore().getThemeData()
@ -253,33 +254,10 @@ const AppearanceTab = {
noIntersectionObserver () { noIntersectionObserver () {
return !window.IntersectionObserver return !window.IntersectionObserver
}, },
horizontalUnits () {
return defaultHorizontalUnits
},
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 +273,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()
}, },

View file

@ -1,28 +1,19 @@
.appearance-tab { .appearance-tab {
h3 {
border: none
}
.palette, .palette,
.theme-notice { .theme-notice {
padding: 0.5em; padding: 0.5em;
margin: 1em; 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"] {
padding: 0.25em; padding: 0.25em;
@ -71,6 +62,7 @@
border-radius: var(--roundness); border-radius: var(--roundness);
border: 1px solid var(--border); border: 1px solid var(--border);
margin: -0.5em; margin: -0.5em;
margin-top: 0;
} }
.palettes { .palettes {
@ -80,9 +72,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 +152,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">
@ -72,14 +77,14 @@
</button> </button>
</div> </div>
<div class="setting-item"> <div class="setting-item">
<h2>{{ $t('settings.style.themes3.palette.label') }}</h2> <h4>{{ $t('settings.style.themes3.palette.label') }}</h4>
<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 +108,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 +135,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,9 +155,7 @@
</template> </template>
</div> </div>
</div> </div>
</div> <h3>{{ $t('settings.background') }}</h3>
<div class="setting-item">
<h2>{{ $t('settings.background') }}</h2>
<div class="banner-background-preview"> <div class="banner-background-preview">
<img :src="user.background_image"> <img :src="user.background_image">
<button <button
@ -193,193 +196,11 @@
> >
{{ $t('settings.save') }} {{ $t('settings.save') }}
</button> </button>
</div> <h3>{{ $t('settings.visual_tweaks') }}</h3>
<div class="setting-item">
<h2>{{ $t('settings.scale_and_layout') }}</h2>
<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 +224,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,104 @@
<template>
<div class="clutter-tab">
<div class="setting-item">
<h3>{{ $t('settings.interface') }}</h3>
<ul class="setting-list">
<li>
<BooleanSetting
path="alwaysShowSubjectInput"
expert="1"
>
{{ $t('settings.subject_input_always_show') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting
path="minimalScopesMode"
expert="1"
>
{{ $t('settings.minimal_scopes_mode') }}
</BooleanSetting>
</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>
</ul>
<h3>{{ $t('settings.attachments') }}</h3>
<ul class="setting-list">
<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>
<li>
<BooleanSetting
path="userCardHidePersonalMarks"
expert="1"
>
{{ $t('settings.user_card_hide_personal_marks') }}
</BooleanSetting>
</li>
<li v-if="instanceShoutboxPresent">
<BooleanSetting
path="hideShoutbox"
expert="1"
>
{{ $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,111 @@
<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"
>
{{ $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>
</li>
<ul class="setting-list suboptions">
<li>
<BooleanSetting
path="alwaysUseJpeg"
expert="1"
parent-path="imageCompression"
>
{{ $t('settings.always_use_jpeg') }}
</BooleanSetting>
</li>
</ul>
</ul>
</div>
</div>
</template>
<script src="./composing_tab.js"></script>

View file

@ -0,0 +1,11 @@
.data-import-export-tab {
h3 {
margin-right: -2em;
}
.importer-exporter {
display: inline-flex;
flex-direction: column;
gap: 0.5em;
}
}

View file

@ -1,60 +1,61 @@
<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>
<p>{{ $t('settings.import_followers_from_a_csv_file') }}</p> <ul class="setting-list">
<Importer <li>
:submit-handler="importFollows" <h4>{{ $t('settings.import_export.follows') }}</h4>
:success-message="$t('settings.follows_imported')" <p>{{ $t('settings.import_followers_from_a_csv_file') }}</p>
:error-message="$t('settings.follow_import_error')" <div class="importer-exporter">
/> <Importer
</div> :submit-handler="importFollows"
<div class="setting-item"> :success-message="$t('settings.follows_imported')"
<h2>{{ $t('settings.follow_export') }}</h2> :error-message="$t('settings.follow_import_error')"
<Exporter />
:get-content="getFollowsContent" <Exporter
filename="friends.csv" :get-content="getFollowsContent"
:export-button-label="$t('settings.follow_export_button')" filename="friends.csv"
/> :export-button-label="$t('settings.follow_export_button')"
</div> />
<div class="setting-item"> </div>
<h2>{{ $t('settings.block_import') }}</h2> </li>
<p>{{ $t('settings.import_blocks_from_a_csv_file') }}</p> <li>
<Importer <h4>{{ $t('settings.import_export.mutes') }}</h4>
:submit-handler="importBlocks" <p>{{ $t('settings.import_mutes_from_a_csv_file') }}</p>
:success-message="$t('settings.blocks_imported')" <div class="importer-exporter">
:error-message="$t('settings.block_import_error')" <Importer
/> :submit-handler="importMutes"
</div> :success-message="$t('settings.mutes_imported')"
<div class="setting-item"> :error-message="$t('settings.mute_import_error')"
<h2>{{ $t('settings.block_export') }}</h2> />
<Exporter <Exporter
:get-content="getBlocksContent" :get-content="getMutesContent"
filename="blocks.csv" filename="friends.csv"
:export-button-label="$t('settings.block_export_button')" :export-button-label="$t('settings.mute_export_button')"
/> />
</div> </div>
<div class="setting-item"> </li>
<h2>{{ $t('settings.mute_import') }}</h2> <li>
<p>{{ $t('settings.import_mutes_from_a_csv_file') }}</p> <h4>{{ $t('settings.import_export.blocks') }}</h4>
<Importer <p>{{ $t('settings.import_blocks_from_a_csv_file') }}</p>
:submit-handler="importMutes" <div class="importer-exporter">
:success-message="$t('settings.mutes_imported')" <Importer
:error-message="$t('settings.mute_import_error')" :submit-handler="importBlocks"
/> :success-message="$t('settings.blocks_imported')"
</div> :error-message="$t('settings.block_import_error')"
<div class="setting-item"> />
<h2>{{ $t('settings.mute_export') }}</h2> <Exporter
<Exporter :get-content="getBlocksContent"
:get-content="getMutesContent" filename="friends.csv"
filename="mutes.csv" :export-button-label="$t('settings.block_export_button')"
:export-button-label="$t('settings.mute_export_button')" />
/> </div>
</div> </li>
<div class="setting-item"> </ul>
<h2>{{ $t('settings.account_backup') }}</h2> <h3>{{ $t('settings.account_backup') }}</h3>
<p>{{ $t('settings.account_backup_description') }}</p> <p>{{ $t('settings.account_backup_description') }}</p>
<table> <table>
<thead> <thead>
@ -128,4 +129,4 @@
</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

@ -1,3 +1,9 @@
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 pleromaFeCommitUrl = 'https://git.pleroma.social/pleroma/pleroma-fe/commit/'
const VersionTab = { const VersionTab = {
@ -9,10 +15,14 @@ const VersionTab = {
frontendVersion: instance.frontendVersion frontendVersion: instance.frontendVersion
} }
}, },
components: {
BooleanSetting
},
computed: { computed: {
frontendVersionLink () { frontendVersionLink () {
return pleromaFeCommitUrl + this.frontendVersion return pleromaFeCommitUrl + this.frontendVersion
} },
...SharedComputedObject(),
} }
} }

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,57 @@ 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 { defaultHorizontalUnits } from '../helpers/unit_setting.js'
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 { 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 +65,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,9 @@
{{ $t('settings.useStreamingApi') }} {{ $t('settings.useStreamingApi') }}
</BooleanSetting> </BooleanSetting>
</li> </li>
<li> </ul>
<BooleanSetting <h3 v-if="expertLevel > 0">{{ $t('settings.confirmations') }}</h3>
path="virtualScrolling" <ul v-if="expertLevel > 0" class="setting-list">
expert="1"
>
{{ $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 +198,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,49 @@
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 || []
},
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,130 @@
<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="disableStickyHeaders">
{{ $t('settings.disable_sticky_headers') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="showScrollbars">
{{ $t('settings.show_scrollbars') }}
</BooleanSetting>
</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>
<UnitSetting
path="themeEditorMinWidth"
:units="['px', 'rem']"
expert="1"
>
{{ $t('settings.theme_editor_min_width') }}
</UnitSetting>
</li>
</ul>
<h3>{{ $t('settings.columns') }}</h3>
<ul class="setting-list">
<li>
<UnitSetting
path="navbarSize"
:step="0.1"
:units="['px', 'rem']"
:reset-default="{ 'px': 55, 'rem': 3.5 }"
>
{{ $t('settings.navbar_size') }}
</UnitSetting>
</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">
{{ $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>
</ul>
</div>
</div>
</template>
<script src="./layout_tab.js"></script>

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,76 @@
import BooleanSetting from '../helpers/boolean_setting.vue'
import ChoiceSetting from '../helpers/choice_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,
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,255 @@
<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>
</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>
<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="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>
</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
v-if="mergedConfig.mentionLinkDisplay !== 'short'"
path="mentionLinkFadeDomain"
>
{{ $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>
<li>
<BooleanSetting
v-if="user"
source="profile"
path="stripRichContent"
expert="1"
>
{{ $t('settings.no_rich_text_description') }}
</BooleanSetting>
</li>
<ul
v-if="mergedConfig.useAbsoluteTimeFormat"
class="setting-list suboptions"
>
<li>
<UnitSetting
path="absoluteTimeFormatMinAge"
unit-set="time"
:units="['s', 'm', 'h', 'd']"
:min="0"
>
{{ $t('settings.absolute_time_format_min_age') }}
</UnitSetting>
</li>
</ul>
</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>
</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>
<li>
<BooleanSetting
path="useContainFit"
expert="1"
>
{{ $t('settings.use_contain_fit') }}
</BooleanSetting>
</li>
</ul>
<h3 v-if="expertLevel > 0">
{{ $t('settings.fun') }}
</h3>
<ul class="setting-list">
<li v-if="user">
<BooleanSetting
path="mentionLinkShowYous"
expert="1"
>
{{ $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

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

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

@ -452,7 +452,7 @@ const Status = {
}, },
scrobblePresent () { scrobblePresent () {
if (this.mergedConfig.hideScrobbles) return false if (this.mergedConfig.hideScrobbles) return false
if (!this.status.user.latestScrobble) return false if (!this.status.user?.latestScrobble) return false
const value = this.mergedConfig.hideScrobblesAfter.match(/\d+/gs)[0] const value = this.mergedConfig.hideScrobblesAfter.match(/\d+/gs)[0]
const unit = this.mergedConfig.hideScrobblesAfter.match(/\D+/gs)[0] const unit = this.mergedConfig.hideScrobblesAfter.match(/\D+/gs)[0]
let multiplier = 60 * 1000 // minutes is smallest unit let multiplier = 60 * 1000 // minutes is smallest unit
@ -474,7 +474,7 @@ const Status = {
return this.status.user.latestScrobble.artist return this.status.user.latestScrobble.artist
}, },
scrobble () { scrobble () {
return this.status.user.latestScrobble return this.status.user?.latestScrobble
} }
}, },
methods: { methods: {

View file

@ -108,6 +108,10 @@
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
flex: 1 1 0; flex: 1 1 0;
&.unknown {
min-width: 8em;
}
} }
.heading-left { .heading-left {

View file

@ -109,6 +109,7 @@
class="left-side" class="left-side"
> >
<a <a
v-if="status.user?.name"
:href="$router.resolve(userProfileLink).href" :href="$router.resolve(userProfileLink).href"
@click.prevent @click.prevent
> >
@ -120,10 +121,17 @@
class="post-avatar" class="post-avatar"
:show-actor-type-indicator="showActorTypeIndicator" :show-actor-type-indicator="showActorTypeIndicator"
:compact="compact" :compact="compact"
:user="status.user" :user="status?.user"
/> />
</UserPopover> </UserPopover>
</a> </a>
<UserAvatar
v-else
:user="status?.user"
class="post-avatar"
:compact="compact"
:title="$t('status.unknown_user_info')"
/>
</div> </div>
<div class="right-side"> <div class="right-side">
<div <div
@ -133,29 +141,30 @@
<div class="heading-name-row"> <div class="heading-name-row">
<div class="heading-left"> <div class="heading-left">
<h4 <h4
v-if="status.user.name_html"
class="status-username" class="status-username"
:title="status.user.name" :title="status.user?.name ?? $t('status.unknown_user_info')"
> >
<RichContent <user-link
:html="status.user.name" v-if="status.user?.name"
:emoji="status.user.emoji" class="account-name"
:is-local="status.user.is_local" :title="status.user?.screen_name_ui"
/> :user="status?.user"
:at="false"
>
<RichContent
:html="status.user.name"
:emoji="status.user.emoji"
:is-local="status.user.is_local"
/>
<span>{{ status.user.name }}</span>
</user-link>
<span
v-else
class="account-name unknown"
>
{{ $t('status.unknown_user') }}
</span>
</h4> </h4>
<h4
v-else
class="status-username"
:title="status.user.name"
>
{{ status.user.name }}
</h4>
<user-link
class="account-name"
:title="status.user.screen_name_ui"
:user="status.user"
:at="false"
/>
<img <img
v-if="!!(status.user && status.user.favicon)" v-if="!!(status.user && status.user.favicon)"
class="status-favicon" class="status-favicon"

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,
@ -157,29 +152,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;

View file

@ -0,0 +1,207 @@
// 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
const classes = ['tab-content-wrapper', active ? '-active' : '-hidden' ]
if (props.fullHeight) {
classes.push('-full-height')
}
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')}
class="button-unstyled"
>
<FAIcon
size="lg"
class="back-button-icon"
icon="chevron-left"
/>
</button>
{props.label}
</h2>
)
return (
<div class={classes} >
<div class="tab-mobile-header">
{header}
</div>
<div class="tab-slot-wrapper">
<div class={ ['tab-content', props['full-width'] ? '-full-width' : null].join(' ') } >
{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,176 @@
.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;
height: 100%;
&:not(.-full-width) {
max-width: 40em;
}
&.-full-width {
align-self: stretch;
}
}
.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 {
flex: 1 1 auto;
height: 100%;
&.-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

@ -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,6 +404,9 @@
"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",
"posts": "Posts",
"developer": "Developer",
"debug": "Debug",
"mention_links": "Mention links", "mention_links": "Mention links",
"appearance": "Appearance", "appearance": "Appearance",
"confirm_new_setting": "Confirm new setting?", "confirm_new_setting": "Confirm new setting?",
@ -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)",
@ -1346,6 +1377,8 @@
"show_content": "Show content", "show_content": "Show content",
"hide_content": "Hide content", "hide_content": "Hide content",
"status_deleted": "This post was deleted", "status_deleted": "This post was deleted",
"unknown_user": "unknown user",
"unknown_user_info": "Unable to fetch information about this user",
"nsfw": "NSFW", "nsfw": "NSFW",
"expand": "Expand", "expand": "Expand",
"you": "(You)", "you": "(You)",

View file

@ -6,7 +6,7 @@ import localeService from '../services/locale/locale.service.js'
import { useI18nStore } from 'src/stores/i18n.js' import { useI18nStore } from 'src/stores/i18n.js'
import { useInterfaceStore } from 'src/stores/interface.js' import { useInterfaceStore } from 'src/stores/interface.js'
import { defaultState } from './default_config_state.js' import { instanceDefaultConfig, defaultState } from './default_config_state.js'
const BACKEND_LANGUAGE_COOKIE_NAME = 'userLanguage' const BACKEND_LANGUAGE_COOKIE_NAME = 'userLanguage'
const APPEARANCE_SETTINGS_KEYS = new Set([ const APPEARANCE_SETTINGS_KEYS = new Set([
@ -38,9 +38,7 @@ export const multiChoiceProperties = [
] ]
// caching the instance default properties // caching the instance default properties
export const instanceDefaultProperties = Object.entries(defaultState) export const instanceDefaultProperties = Object.keys(instanceDefaultConfig)
.filter(([, value]) => value === undefined)
.map(([key]) => key)
const config = { const config = {
state: { ...defaultState }, state: { ...defaultState },

View file

@ -1,45 +1,43 @@
const browserLocale = (window.navigator.language || 'en').split('-')[0] const browserLocale = (window.navigator.language || 'en').split('-')[0]
export const defaultState = { /// Instance config entries provided by static config or pleroma api
expertLevel: 0, // used to track which settings to show and hide /// Put settings here only if it does not make sense for a normal user
/// to override it.
// Theme stuff export const staticOrApiConfigDefault = {
theme: undefined, // Very old theme store, stores preset name, still in use theme: 'pleroma-dark',
// V1
colors: {}, // VERY old theme store, just colors of V1, probably not even used anymore
// V2
customTheme: undefined, // "snapshot", previously was used as actual theme store for V2 so it's still used in case of PleromaFE downgrade event.
customThemeSource: undefined, // "source", stores original theme data
// V3
style: null,
styleCustomData: null,
palette: null, palette: null,
paletteCustomData: null, style: null,
themeDebug: false, // debug mode that uses computed backgrounds instead of real ones to debug contrast functions defaultAvatar: '/images/avi.png',
forceThemeRecompilation: false, // flag that forces recompilation on boot even if cache exists defaultBanner: '/images/banner.png',
theme3hacks: { // Hacks, user overrides that are independent of theme used background: '/static/aurora_borealis.jpg',
underlay: 'none', embeddedToS: true,
fonts: { logo: '/static/logo.svg',
interface: undefined, logoMargin: '.2em',
input: undefined, logoMask: true,
post: undefined, logoLeft: false,
monospace: undefined redirectRootLogin: '/main/friends',
} redirectRootNoLogin: '/main/all',
}, hideSitename: false,
nsfwCensorImage: undefined,
showFeaturesPanel: true,
showInstanceSpecificPanel: false,
}
/// This object contains setting entries that makes sense
/// at the user level. The defaults can also be overriden by
/// instance admins in the frontend_configuration endpoint or static config.
export const instanceDefaultConfig = {
expertLevel: 0, // used to track which settings to show and hide
hideISP: false, hideISP: false,
hideInstanceWallpaper: false, hideInstanceWallpaper: false,
hideShoutbox: false, hideShoutbox: false,
// bad name: actually hides posts of muted USERS // bad name: actually hides posts of muted USERS
hideMutedPosts: undefined, // instance default hideMutedPosts: false,
hideMutedThreads: undefined, // instance default hideMutedThreads: true,
hideWordFilteredPosts: undefined, // instance default hideWordFilteredPosts: false,
muteBotStatuses: undefined, // instance default muteBotStatuses: false,
muteSensitiveStatuses: undefined, // instance default muteSensitiveStatuses: false,
collapseMessageWithSubject: undefined, // instance default collapseMessageWithSubject: false,
padEmoji: true, padEmoji: true,
hideAttachments: false, hideAttachments: false,
hideAttachmentsInConv: false, hideAttachmentsInConv: false,
@ -50,6 +48,9 @@ export const defaultState = {
preloadImage: true, preloadImage: true,
loopVideo: true, loopVideo: true,
loopVideoSilentOnly: true, loopVideoSilentOnly: true,
/// This is not the streaming API configuration, but rather an option
/// for automatically loading new posts into the timeline without
/// the user clicking the Show New button.
streaming: false, streaming: false,
emojiReactionsOnTimeline: true, emojiReactionsOnTimeline: true,
alwaysShowNewPostButton: false, alwaysShowNewPostButton: false,
@ -86,39 +87,37 @@ export const defaultState = {
}, },
webPushNotifications: false, webPushNotifications: false,
webPushAlwaysShowNotifications: false, webPushAlwaysShowNotifications: false,
muteWords: [],
highlight: {},
interfaceLanguage: browserLocale, interfaceLanguage: browserLocale,
hideScopeNotice: false, hideScopeNotice: false,
useStreamingApi: false, useStreamingApi: false,
sidebarRight: undefined, // instance default sidebarRight: false,
scopeCopy: undefined, // instance default scopeCopy: true,
subjectLineBehavior: undefined, // instance default subjectLineBehavior: 'email',
alwaysShowSubjectInput: undefined, // instance default alwaysShowSubjectInput: true,
postContentType: undefined, // instance default postContentType: 'text/plain',
minimalScopesMode: undefined, // instance default minimalScopesMode: false,
// This hides statuses filtered via a word filter // This hides statuses filtered via a word filter
hideFilteredStatuses: undefined, // instance default hideFilteredStatuses: false,
// Confirmations // Confirmations
modalOnRepeat: undefined, // instance default modalOnRepeat: false,
modalOnUnfollow: undefined, // instance default modalOnUnfollow: false,
modalOnBlock: undefined, // instance default modalOnBlock: true,
modalOnMute: undefined, // instance default modalOnMute: false,
modalOnMuteConversation: undefined, // instance default modalOnMuteConversation: false,
modalOnMuteDomain: undefined, // instance default modalOnMuteDomain: true,
modalOnDelete: undefined, // instance default modalOnDelete: true,
modalOnLogout: undefined, // instance default modalOnLogout: true,
modalOnApproveFollow: undefined, // instance default modalOnApproveFollow: false,
modalOnDenyFollow: undefined, // instance default modalOnDenyFollow: false,
modalOnRemoveUserFromFollowers: undefined, // instance default modalOnRemoveUserFromFollowers: false,
// Expiry confirmations/default actions // Expiry confirmations/default actions
onMuteDefaultAction: 'ask', onMuteDefaultAction: 'ask',
onBlockDefaultAction: 'ask', onBlockDefaultAction: 'ask',
modalMobileCenter: undefined, modalMobileCenter: false,
playVideosInModal: false, playVideosInModal: false,
useOneClickNsfw: false, useOneClickNsfw: false,
useContainFit: true, useContainFit: true,
@ -131,45 +130,93 @@ export const defaultState = {
sidebarColumnWidth: '25rem', sidebarColumnWidth: '25rem',
contentColumnWidth: '45rem', contentColumnWidth: '45rem',
notifsColumnWidth: '25rem', notifsColumnWidth: '25rem',
themeEditorMinWidth: undefined, // instance default themeEditorMinWidth: '0rem',
emojiReactionsScale: undefined, emojiReactionsScale: 0.5,
textSize: undefined, // instance default textSize: '1rem',
emojiSize: undefined, // instance default emojiSize: '2.2rem',
navbarSize: undefined, // instance default navbarSize: '3.5rem',
panelHeaderSize: undefined, // instance default panelHeaderSize: '3.2rem',
forcedRoundness: undefined, // instance default forcedRoundness: -1,
navbarColumnStretch: false, navbarColumnStretch: false,
greentext: undefined, // instance default greentext: false,
mentionLinkDisplay: undefined, // instance default mentionLinkDisplay: 'short',
mentionLinkShowTooltip: undefined, // instance default mentionLinkShowTooltip: true,
mentionLinkShowAvatar: undefined, // instance default mentionLinkShowAvatar: false,
mentionLinkFadeDomain: undefined, // instance default mentionLinkFadeDomain: true,
mentionLinkShowYous: undefined, // instance default mentionLinkShowYous: false,
mentionLinkBoldenYou: undefined, // instance default mentionLinkBoldenYou: true,
hidePostStats: undefined, // instance default hidePostStats: false,
hideBotIndication: undefined, // instance default hideBotIndication: false,
hideUserStats: undefined, // instance default hideUserStats: false,
virtualScrolling: undefined, // instance default virtualScrolling: true,
sensitiveByDefault: undefined, // instance default sensitiveByDefault: false,
conversationDisplay: undefined, // instance default conversationDisplay: 'linear',
conversationTreeAdvanced: undefined, // instance default conversationTreeAdvanced: false,
conversationOtherRepliesButton: undefined, // instance default conversationOtherRepliesButton: 'below',
conversationTreeFadeAncestors: undefined, // instance default conversationTreeFadeAncestors: false,
showExtraNotifications: undefined, // instance default showExtraNotifications: true,
showExtraNotificationsTip: undefined, // instance default showExtraNotificationsTip: true,
showChatsInExtraNotifications: undefined, // instance default showChatsInExtraNotifications: true,
showAnnouncementsInExtraNotifications: undefined, // instance default showAnnouncementsInExtraNotifications: true,
showFollowRequestsInExtraNotifications: undefined, // instance default showFollowRequestsInExtraNotifications: true,
maxDepthInThread: undefined, // instance default maxDepthInThread: 6,
autocompleteSelect: undefined, // instance default autocompleteSelect: false,
closingDrawerMarksAsSeen: undefined, // instance default closingDrawerMarksAsSeen: true,
unseenAtTop: undefined, // instance default unseenAtTop: false,
ignoreInactionableSeen: undefined, // instance default ignoreInactionableSeen: false,
unsavedPostAction: undefined, // instance default unsavedPostAction: 'confirm',
autoSaveDraft: undefined, // instance default autoSaveDraft: false,
useAbsoluteTimeFormat: undefined, // instance default useAbsoluteTimeFormat: false,
absoluteTimeFormatMinAge: undefined, // instance default absoluteTimeFormatMinAge: '0d',
absoluteTime12h: undefined, // instance default absoluteTime12h: '24h',
imageCompression: true, imageCompression: true,
alwaysUseJpeg: false alwaysUseJpeg: false
} }
export const makeUndefined = c => Object.fromEntries(Object.keys(c).map(key => [key, undefined]))
/// For properties with special processing or properties that does not
/// make sense to be overriden on a instance-wide level.
export const defaultState = {
// Set these to undefined so it does not interfere with default settings check
...makeUndefined(instanceDefaultConfig),
// Special processing
// Theme stuff
theme: undefined, // Very old theme store, stores preset name, still in use
// V1
colors: {}, // VERY old theme store, just colors of V1, probably not even used anymore
// V2
customTheme: undefined, // "snapshot", previously was used as actual theme store for V2 so it's still used in case of PleromaFE downgrade event.
customThemeSource: undefined, // "source", stores original theme data
// V3
style: null,
styleCustomData: null,
palette: null,
paletteCustomData: null,
themeDebug: false, // debug mode that uses computed backgrounds instead of real ones to debug contrast functions
forceThemeRecompilation: false, // flag that forces recompilation on boot even if cache exists
theme3hacks: { // Hacks, user overrides that are independent of theme used
underlay: 'none',
fonts: {
interface: undefined,
input: undefined,
post: undefined,
monospace: undefined
}
},
// Special handling: These fields are not of a primitive type, and
// might cause problems with current code because it specifically checks
// them in state.config (not getters.mergedConfig).
// Specifically, muteWords is now deprecated in favour of a server-side configuration.
muteWords: [],
highlight: {},
// If there are any configurations that does not make sense to
// have instance-wide default, put it here and explain why.
}

View file

@ -4,6 +4,7 @@ import { ensureFinalFallback } from '../i18n/languages.js'
import { useInterfaceStore } from 'src/stores/interface.js' import { useInterfaceStore } from 'src/stores/interface.js'
// See build/emojis_plugin for more details // See build/emojis_plugin for more details
import { annotationsLoader } from 'virtual:pleroma-fe/emoji-annotations' import { annotationsLoader } from 'virtual:pleroma-fe/emoji-annotations'
import { staticOrApiConfigDefault, instanceDefaultConfig } from './default_config_state.js';
const SORTED_EMOJI_GROUP_IDS = [ const SORTED_EMOJI_GROUP_IDS = [
'smileys-and-emotion', 'smileys-and-emotion',
@ -52,90 +53,15 @@ const defaultState = {
vapidPublicKey: undefined, vapidPublicKey: undefined,
// Stuff from static/config.json // Stuff from static/config.json
alwaysShowSubjectInput: true,
defaultAvatar: '/images/avi.png',
defaultBanner: '/images/banner.png',
background: '/static/aurora_borealis.jpg',
embeddedToS: true,
collapseMessageWithSubject: false,
greentext: false,
mentionLinkDisplay: 'short',
mentionLinkShowTooltip: true,
mentionLinkShowAvatar: false,
mentionLinkFadeDomain: true,
mentionLinkShowYous: false,
mentionLinkBoldenYou: true,
hideFilteredStatuses: false,
// bad name: actually hides posts of muted USERS
hideMutedPosts: false,
hideMutedThreads: true,
hideWordFilteredPosts: false,
hidePostStats: false,
hideBotIndication: false,
hideSitename: false,
hideUserStats: false,
muteBotStatuses: false,
muteSensitiveStatuses: false,
modalOnRepeat: false,
modalOnUnfollow: false,
modalOnBlock: true,
modalOnMute: false,
modalOnMuteConversation: false,
modalOnMuteDomain: true,
modalOnDelete: true,
modalOnLogout: true,
modalOnApproveFollow: false,
modalOnDenyFollow: false,
modalOnRemoveUserFromFollowers: false,
modalMobileCenter: false,
loginMethod: 'password', loginMethod: 'password',
logo: '/static/logo.svg',
logoMargin: '.2em',
logoMask: true,
logoLeft: false,
disableUpdateNotification: false, disableUpdateNotification: false,
minimalScopesMode: false,
nsfwCensorImage: undefined,
postContentType: 'text/plain',
redirectRootLogin: '/main/friends',
redirectRootNoLogin: '/main/all',
scopeCopy: true,
showFeaturesPanel: true,
showInstanceSpecificPanel: false,
sidebarRight: false,
subjectLineBehavior: 'email',
theme: 'pleroma-dark',
palette: null,
style: null,
emojiReactionsScale: 0.5,
textSize: '1rem',
emojiSize: '2.2rem',
navbarSize: '3.5rem',
panelHeaderSize: '3.2rem',
themeEditorMinWidth: '0rem',
forcedRoundness: -1,
fontsOverride: {}, fontsOverride: {},
virtualScrolling: true,
sensitiveByDefault: false, // Instance-wide configurations that should not be changed by individual users
conversationDisplay: 'linear', ...staticOrApiConfigDefault,
conversationTreeAdvanced: false, // Instance admins can override default settings for the whole instance
conversationOtherRepliesButton: 'below', ...instanceDefaultConfig,
conversationTreeFadeAncestors: false,
showExtraNotifications: true,
showExtraNotificationsTip: true,
showChatsInExtraNotifications: true,
showAnnouncementsInExtraNotifications: true,
showFollowRequestsInExtraNotifications: true,
maxDepthInThread: 6,
autocompleteSelect: false,
closingDrawerMarksAsSeen: true,
unseenAtTop: false,
ignoreInactionableSeen: false,
unsavedPostAction: 'confirm',
autoSaveDraft: false,
useAbsoluteTimeFormat: false,
absoluteTimeFormatMinAge: '0d',
absoluteTime12h: '24h',
// Nasty stuff // Nasty stuff
customEmoji: [], customEmoji: [],

View file

@ -111,7 +111,10 @@ const sortTimeline = (timeline) => {
const getLatestScrobble = (state, user) => { const getLatestScrobble = (state, user) => {
const scrobblesSupport = state.pleromaScrobblesAvailable const scrobblesSupport = state.pleromaScrobblesAvailable
if (!scrobblesSupport) return
if (!scrobblesSupport || !user.name || user.id === 'undefined') {
return
}
if (state.scrobblesNextFetch[user.id] && state.scrobblesNextFetch[user.id] > Date.now()) { if (state.scrobblesNextFetch[user.id] && state.scrobblesNextFetch[user.id] > Date.now()) {
return return

View file

@ -2,7 +2,7 @@ export const muteFilterHits = (muteFilters, status) => {
const statusText = status.text.toLowerCase() const statusText = status.text.toLowerCase()
const statusSummary = status.summary.toLowerCase() const statusSummary = status.summary.toLowerCase()
const replyToUser = status.in_reply_to_screen_name?.toLowerCase() const replyToUser = status.in_reply_to_screen_name?.toLowerCase()
const poster = status.user.screen_name.toLowerCase() const poster = status.user.screen_name?.toLowerCase()
const mentions = (status.attentions || []).map(att => att.screen_name.toLowerCase()) const mentions = (status.attentions || []).map(att => att.screen_name.toLowerCase())

View file

@ -46,7 +46,7 @@ const highlightStyle = (prefs) => {
const highlightClass = (user) => { const highlightClass = (user) => {
return 'USER____' + user.screen_name return 'USER____' + user.screen_name
.replace(/\./g, '_') ?.replace(/\./g, '_')
.replace(/@/g, '_AT_') .replace(/@/g, '_AT_')
} }

1138
yarn.lock

File diff suppressed because it is too large Load diff