aboutsummaryrefslogtreecommitdiff
path: root/src/components/settings_modal
diff options
context:
space:
mode:
Diffstat (limited to 'src/components/settings_modal')
-rw-r--r--src/components/settings_modal/admin_tabs/emoji_tab.js257
-rw-r--r--src/components/settings_modal/admin_tabs/emoji_tab.scss59
-rw-r--r--src/components/settings_modal/admin_tabs/emoji_tab.vue358
-rw-r--r--src/components/settings_modal/admin_tabs/frontends_tab.js113
-rw-r--r--src/components/settings_modal/admin_tabs/frontends_tab.scss29
-rw-r--r--src/components/settings_modal/admin_tabs/frontends_tab.vue209
-rw-r--r--src/components/settings_modal/admin_tabs/instance_tab.js38
-rw-r--r--src/components/settings_modal/admin_tabs/instance_tab.vue206
-rw-r--r--src/components/settings_modal/admin_tabs/limits_tab.js29
-rw-r--r--src/components/settings_modal/admin_tabs/limits_tab.vue136
-rw-r--r--src/components/settings_modal/helpers/attachment_setting.js44
-rw-r--r--src/components/settings_modal/helpers/attachment_setting.vue126
-rw-r--r--src/components/settings_modal/helpers/boolean_setting.js63
-rw-r--r--src/components/settings_modal/helpers/boolean_setting.vue34
-rw-r--r--src/components/settings_modal/helpers/choice_setting.js68
-rw-r--r--src/components/settings_modal/helpers/choice_setting.vue20
-rw-r--r--src/components/settings_modal/helpers/draft_buttons.vue88
-rw-r--r--src/components/settings_modal/helpers/emoji_editing_popover.vue227
-rw-r--r--src/components/settings_modal/helpers/float_setting.vue16
-rw-r--r--src/components/settings_modal/helpers/group_setting.js13
-rw-r--r--src/components/settings_modal/helpers/group_setting.vue15
-rw-r--r--src/components/settings_modal/helpers/integer_setting.js44
-rw-r--r--src/components/settings_modal/helpers/integer_setting.vue36
-rw-r--r--src/components/settings_modal/helpers/modified_indicator.vue12
-rw-r--r--src/components/settings_modal/helpers/number_setting.js24
-rw-r--r--src/components/settings_modal/helpers/number_setting.vue46
-rw-r--r--src/components/settings_modal/helpers/profile_setting_indicator.vue (renamed from src/components/settings_modal/helpers/server_side_indicator.vue)12
-rw-r--r--src/components/settings_modal/helpers/setting.js246
-rw-r--r--src/components/settings_modal/helpers/shared_computed_object.js56
-rw-r--r--src/components/settings_modal/helpers/size_setting.js67
-rw-r--r--src/components/settings_modal/helpers/string_setting.js5
-rw-r--r--src/components/settings_modal/helpers/string_setting.vue42
-rw-r--r--src/components/settings_modal/helpers/unit_setting.js64
-rw-r--r--src/components/settings_modal/helpers/unit_setting.vue (renamed from src/components/settings_modal/helpers/size_setting.vue)31
-rw-r--r--src/components/settings_modal/settings_modal.js40
-rw-r--r--src/components/settings_modal/settings_modal.scss26
-rw-r--r--src/components/settings_modal/settings_modal.vue60
-rw-r--r--src/components/settings_modal/settings_modal_admin_content.js95
-rw-r--r--src/components/settings_modal/settings_modal_admin_content.scss (renamed from src/components/settings_modal/settings_modal_content.scss)15
-rw-r--r--src/components/settings_modal/settings_modal_admin_content.vue76
-rw-r--r--src/components/settings_modal/settings_modal_user_content.js (renamed from src/components/settings_modal/settings_modal_content.js)8
-rw-r--r--src/components/settings_modal/settings_modal_user_content.scss45
-rw-r--r--src/components/settings_modal/settings_modal_user_content.vue (renamed from src/components/settings_modal/settings_modal_content.vue)51
-rw-r--r--src/components/settings_modal/tabs/appearance_tab.js195
-rw-r--r--src/components/settings_modal/tabs/appearance_tab.vue313
-rw-r--r--src/components/settings_modal/tabs/filtering_tab.js2
-rw-r--r--src/components/settings_modal/tabs/filtering_tab.vue39
-rw-r--r--src/components/settings_modal/tabs/general_tab.js37
-rw-r--r--src/components/settings_modal/tabs/general_tab.vue129
-rw-r--r--src/components/settings_modal/tabs/mutes_and_blocks_tab.js7
-rw-r--r--src/components/settings_modal/tabs/notifications_tab.js4
-rw-r--r--src/components/settings_modal/tabs/notifications_tab.vue250
-rw-r--r--src/components/settings_modal/tabs/profile_tab.js13
-rw-r--r--src/components/settings_modal/tabs/profile_tab.scss8
-rw-r--r--src/components/settings_modal/tabs/profile_tab.vue80
-rw-r--r--src/components/settings_modal/tabs/security_tab/mfa.vue7
-rw-r--r--src/components/settings_modal/tabs/security_tab/mfa_backup_codes.vue7
-rw-r--r--src/components/settings_modal/tabs/security_tab/mfa_totp.vue1
-rw-r--r--src/components/settings_modal/tabs/security_tab/security_tab.vue31
-rw-r--r--src/components/settings_modal/tabs/theme_tab/theme_preview.vue (renamed from src/components/settings_modal/tabs/theme_tab/preview.vue)143
-rw-r--r--src/components/settings_modal/tabs/theme_tab/theme_tab.js121
-rw-r--r--src/components/settings_modal/tabs/theme_tab/theme_tab.scss122
-rw-r--r--src/components/settings_modal/tabs/theme_tab/theme_tab.vue203
63 files changed, 4129 insertions, 832 deletions
diff --git a/src/components/settings_modal/admin_tabs/emoji_tab.js b/src/components/settings_modal/admin_tabs/emoji_tab.js
new file mode 100644
index 00000000..58e1468f
--- /dev/null
+++ b/src/components/settings_modal/admin_tabs/emoji_tab.js
@@ -0,0 +1,257 @@
+import { clone, assign } from 'lodash'
+import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'
+import StringSetting from '../helpers/string_setting.vue'
+import Checkbox from 'components/checkbox/checkbox.vue'
+import StillImage from 'components/still-image/still-image.vue'
+import Select from 'components/select/select.vue'
+import Popover from 'components/popover/popover.vue'
+import ConfirmModal from 'components/confirm_modal/confirm_modal.vue'
+import ModifiedIndicator from '../helpers/modified_indicator.vue'
+import EmojiEditingPopover from '../helpers/emoji_editing_popover.vue'
+
+const EmojiTab = {
+ components: {
+ TabSwitcher,
+ StringSetting,
+ Checkbox,
+ StillImage,
+ Select,
+ Popover,
+ ConfirmModal,
+ ModifiedIndicator,
+ EmojiEditingPopover
+ },
+
+ data () {
+ return {
+ knownLocalPacks: { },
+ knownRemotePacks: { },
+ editedMetadata: { },
+ packName: '',
+ newPackName: '',
+ deleteModalVisible: false,
+ remotePackInstance: '',
+ remotePackDownloadAs: ''
+ }
+ },
+
+ provide () {
+ return { emojiAddr: this.emojiAddr }
+ },
+
+ computed: {
+ pack () {
+ return this.packName !== '' ? this.knownPacks[this.packName] : undefined
+ },
+ packMeta () {
+ if (this.editedMetadata[this.packName] === undefined) {
+ this.editedMetadata[this.packName] = clone(this.pack.pack)
+ }
+
+ return this.editedMetadata[this.packName]
+ },
+ knownPacks () {
+ // Copy the object itself but not the children, so they are still passed by reference and modified
+ const result = clone(this.knownLocalPacks)
+ for (const instName in this.knownRemotePacks) {
+ for (const instPack in this.knownRemotePacks[instName]) {
+ result[`${instPack}@${instName}`] = this.knownRemotePacks[instName][instPack]
+ }
+ }
+
+ return result
+ },
+ downloadWillReplaceLocal () {
+ return (this.remotePackDownloadAs.trim() === '' && this.pack.remote && this.pack.remote.baseName in this.knownLocalPacks) ||
+ (this.remotePackDownloadAs in this.knownLocalPacks)
+ }
+ },
+
+ methods: {
+ reloadEmoji () {
+ this.$store.state.api.backendInteractor.reloadEmoji()
+ },
+ importFromFS () {
+ this.$store.state.api.backendInteractor.importEmojiFromFS()
+ },
+ emojiAddr (name) {
+ if (this.pack.remote !== undefined) {
+ // Remote pack
+ return `${this.pack.remote.instance}/emoji/${encodeURIComponent(this.pack.remote.baseName)}/${name}`
+ } else {
+ return `${this.$store.state.instance.server}/emoji/${encodeURIComponent(this.packName)}/${name}`
+ }
+ },
+
+ createEmojiPack () {
+ this.$store.state.api.backendInteractor.createEmojiPack(
+ { name: this.newPackName }
+ ).then(resp => resp.json()).then(resp => {
+ if (resp === 'ok') {
+ return this.refreshPackList()
+ } else {
+ this.displayError(resp.error)
+ return Promise.reject(resp)
+ }
+ }).then(done => {
+ this.$refs.createPackPopover.hidePopover()
+
+ this.packName = this.newPackName
+ this.newPackName = ''
+ })
+ },
+ deleteEmojiPack () {
+ this.$store.state.api.backendInteractor.deleteEmojiPack(
+ { name: this.packName }
+ ).then(resp => resp.json()).then(resp => {
+ if (resp === 'ok') {
+ return this.refreshPackList()
+ } else {
+ this.displayError(resp.error)
+ return Promise.reject(resp)
+ }
+ }).then(done => {
+ delete this.editedMetadata[this.packName]
+
+ this.deleteModalVisible = false
+ this.packName = ''
+ })
+ },
+
+ metaEdited (prop) {
+ if (!this.pack) return
+
+ const def = this.pack.pack[prop] || ''
+ const edited = this.packMeta[prop] || ''
+ return edited !== def
+ },
+ savePackMetadata () {
+ this.$store.state.api.backendInteractor.saveEmojiPackMetadata({ name: this.packName, newData: this.packMeta }).then(
+ resp => resp.json()
+ ).then(resp => {
+ if (resp.error !== undefined) {
+ this.displayError(resp.error)
+ return
+ }
+
+ // Update actual pack data
+ this.pack.pack = resp
+ // Delete edited pack data, should auto-update itself
+ delete this.editedMetadata[this.packName]
+ })
+ },
+
+ updatePackFiles (newFiles) {
+ this.pack.files = newFiles
+ this.sortPackFiles(this.packName)
+ },
+
+ loadPacksPaginated (listFunction) {
+ const pageSize = 25
+ const allPacks = {}
+
+ return listFunction({ instance: this.remotePackInstance, page: 1, pageSize: 0 })
+ .then(data => data.json())
+ .then(data => {
+ if (data.error !== undefined) { return Promise.reject(data.error) }
+
+ let resultingPromise = Promise.resolve({})
+ for (let i = 0; i < Math.ceil(data.count / pageSize); i++) {
+ resultingPromise = resultingPromise.then(() => listFunction({ instance: this.remotePackInstance, page: i, pageSize })
+ ).then(data => data.json()).then(pageData => {
+ if (pageData.error !== undefined) { return Promise.reject(pageData.error) }
+
+ assign(allPacks, pageData.packs)
+ })
+ }
+
+ return resultingPromise
+ })
+ .then(finished => allPacks)
+ .catch(data => {
+ this.displayError(data)
+ })
+ },
+
+ refreshPackList () {
+ this.loadPacksPaginated(this.$store.state.api.backendInteractor.listEmojiPacks)
+ .then(allPacks => {
+ this.knownLocalPacks = allPacks
+ for (const name of Object.keys(this.knownLocalPacks)) {
+ this.sortPackFiles(name)
+ }
+ })
+ },
+ listRemotePacks () {
+ this.loadPacksPaginated(this.$store.state.api.backendInteractor.listRemoteEmojiPacks)
+ .then(allPacks => {
+ let inst = this.remotePackInstance
+ if (!inst.startsWith('http')) { inst = 'https://' + inst }
+ const instUrl = new URL(inst)
+ inst = instUrl.host
+
+ for (const packName in allPacks) {
+ allPacks[packName].remote = {
+ baseName: packName,
+ instance: instUrl.origin
+ }
+ }
+
+ this.knownRemotePacks[inst] = allPacks
+ for (const pack in this.knownRemotePacks[inst]) {
+ this.sortPackFiles(`${pack}@${inst}`)
+ }
+
+ this.$refs.remotePackPopover.hidePopover()
+ })
+ .catch(data => {
+ this.displayError(data)
+ })
+ },
+ downloadRemotePack () {
+ if (this.remotePackDownloadAs.trim() === '') {
+ this.remotePackDownloadAs = this.pack.remote.baseName
+ }
+
+ this.$store.state.api.backendInteractor.downloadRemoteEmojiPack({
+ instance: this.pack.remote.instance, packName: this.pack.remote.baseName, as: this.remotePackDownloadAs
+ })
+ .then(data => data.json())
+ .then(resp => {
+ if (resp === 'ok') {
+ this.$refs.dlPackPopover.hidePopover()
+
+ return this.refreshPackList()
+ } else {
+ this.displayError(resp.error)
+ return Promise.reject(resp)
+ }
+ }).then(done => {
+ this.packName = this.remotePackDownloadAs
+ this.remotePackDownloadAs = ''
+ })
+ },
+ displayError (msg) {
+ this.$store.dispatch('pushGlobalNotice', {
+ messageKey: 'admin_dash.emoji.error',
+ messageArgs: [msg],
+ level: 'error'
+ })
+ },
+ sortPackFiles (nameOfPack) {
+ // Sort by key
+ const sorted = Object.keys(this.knownPacks[nameOfPack].files).sort().reduce((acc, key) => {
+ if (key.length === 0) return acc
+ acc[key] = this.knownPacks[nameOfPack].files[key]
+ return acc
+ }, {})
+ this.knownPacks[nameOfPack].files = sorted
+ }
+ },
+
+ mounted () {
+ this.refreshPackList()
+ }
+}
+
+export default EmojiTab
diff --git a/src/components/settings_modal/admin_tabs/emoji_tab.scss b/src/components/settings_modal/admin_tabs/emoji_tab.scss
new file mode 100644
index 00000000..3e77e019
--- /dev/null
+++ b/src/components/settings_modal/admin_tabs/emoji_tab.scss
@@ -0,0 +1,59 @@
+.emoji-tab {
+ .btn-group .btn:not(:first-child) {
+ margin-left: 0.5em;
+ }
+
+ .pack-info-wrapper {
+ margin-top: 1em;
+ }
+
+ .emoji-info-input {
+ width: 100%;
+ }
+
+ .emoji-data-input {
+ width: 40%;
+ margin-left: 0.5em;
+ margin-right: 0.5em;
+ }
+
+ .emoji {
+ width: 32px;
+ height: 32px;
+ }
+
+ .emoji-unsaved {
+ box-shadow: 0 3px 5px var(--cBlue);
+ }
+
+ .emoji-list {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 1em 1em;
+ }
+}
+
+.emoji-tab-popover-button:not(:first-child) {
+ margin-left: 0.5em;
+}
+
+.emoji-tab-popover-input {
+ margin-bottom: 0.5em;
+
+ label {
+ display: block;
+ margin-bottom: 0.5em;
+ }
+
+ input {
+ width: 20em;
+ }
+
+ .emoji-tab-popover-file {
+ padding-top: 3px;
+ }
+
+ .warning {
+ color: var(--cOrange);
+ }
+}
diff --git a/src/components/settings_modal/admin_tabs/emoji_tab.vue b/src/components/settings_modal/admin_tabs/emoji_tab.vue
new file mode 100644
index 00000000..5742d2ce
--- /dev/null
+++ b/src/components/settings_modal/admin_tabs/emoji_tab.vue
@@ -0,0 +1,358 @@
+<template>
+ <div
+ class="emoji-tab"
+ :label="$t('admin_dash.tabs.emoji')"
+ >
+ <div class="setting-item">
+ <h2>{{ $t('admin_dash.tabs.emoji') }}</h2>
+
+ <ul class="setting-list">
+ <h3>{{ $t('admin_dash.emoji.global_actions') }}</h3>
+
+ <li class="btn-group setting-item">
+ <button
+ class="button button-default btn"
+ type="button"
+ @click="reloadEmoji"
+ >
+ {{ $t('admin_dash.emoji.reload') }}
+ </button>
+ <button
+ class="button button-default btn"
+ type="button"
+ @click="importFromFS"
+ >
+ {{ $t('admin_dash.emoji.importFS') }}
+ </button>
+ </li>
+
+ <li class="btn-group setting-item">
+ <button
+ class="button button-default btn"
+ type="button"
+ @click="$refs.remotePackPopover.showPopover"
+ >
+ {{ $t('admin_dash.emoji.remote_packs') }}
+
+ <Popover
+ ref="remotePackPopover"
+ popover-class="emoji-tab-edit-popover popover-default"
+ trigger="click"
+ placement="bottom"
+ bound-to-selector=".emoji-tab"
+ :bound-to="{ x: 'container' }"
+ :offset="{ y: 5 }"
+ >
+ <template #content>
+ <div class="emoji-tab-popover-input">
+ <h3>{{ $t('admin_dash.emoji.remote_pack_instance') }}</h3>
+ <input
+ v-model="remotePackInstance"
+ class="input"
+ :placeholder="$t('admin_dash.emoji.remote_pack_instance')"
+ >
+ <button
+ class="button button-default btn emoji-tab-popover-button"
+ type="button"
+ @click="listRemotePacks"
+ >
+ {{ $t('admin_dash.emoji.do_list') }}
+ </button>
+ </div>
+ </template>
+ </Popover>
+ </button>
+ </li>
+
+ <h3>{{ $t('admin_dash.emoji.emoji_packs') }}</h3>
+
+ <li>
+ <h4>{{ $t('admin_dash.emoji.edit_pack') }}</h4>
+
+ <Select
+ v-model="packName"
+ class="form-control"
+ >
+ <option
+ value=""
+ disabled
+ hidden
+ >
+ {{ $t('admin_dash.emoji.emoji_pack') }}
+ </option>
+ <option
+ v-for="(pack, listPackName) in knownPacks"
+ :key="listPackName"
+ :label="listPackName"
+ >
+ {{ listPackName }}
+ </option>
+ </Select>
+
+ <button
+ class="button button-default btn emoji-tab-popover-button"
+ type="button"
+ @click="$refs.createPackPopover.showPopover"
+ >
+ {{ $t('admin_dash.emoji.create_pack') }}
+ </button>
+ <Popover
+ ref="createPackPopover"
+ popover-class="emoji-tab-edit-popover popover-default"
+ trigger="click"
+ placement="bottom"
+ bound-to-selector=".emoji-tab"
+ :bound-to="{ x: 'container' }"
+ :offset="{ y: 5 }"
+ >
+ <template #content>
+ <div class="emoji-tab-popover-input">
+ <h3>{{ $t('admin_dash.emoji.new_pack_name') }}</h3>
+ <input
+ v-model="newPackName"
+ :placeholder="$t('admin_dash.emoji.new_pack_name')"
+ class="input"
+ >
+ <button
+ class="button button-default btn emoji-tab-popover-button"
+ type="button"
+ @click="createEmojiPack"
+ >
+ {{ $t('admin_dash.emoji.create') }}
+ </button>
+ </div>
+ </template>
+ </Popover>
+ </li>
+ </ul>
+
+ <div v-if="pack">
+ <div class="pack-info-wrapper">
+ <ul class="setting-list">
+ <li>
+ <label>
+ {{ $t('admin_dash.emoji.description') }}
+ <ModifiedIndicator
+ :changed="metaEdited('description')"
+ message-key="admin_dash.emoji.metadata_changed"
+ />
+
+ <textarea
+ v-model="packMeta.description"
+ :disabled="pack.remote !== undefined"
+ class="bio resize-height input"
+ />
+ </label>
+ </li>
+ <li>
+ <label>
+ {{ $t('admin_dash.emoji.homepage') }}
+ <ModifiedIndicator
+ :changed="metaEdited('homepage')"
+ message-key="admin_dash.emoji.metadata_changed"
+ />
+
+ <input
+ v-model="packMeta.homepage"
+ class="emoji-info-input input"
+ :disabled="pack.remote !== undefined"
+ >
+ </label>
+ </li>
+ <li>
+ <label>
+ {{ $t('admin_dash.emoji.fallback_src') }}
+ <ModifiedIndicator
+ :changed="metaEdited('fallback-src')"
+ message-key="admin_dash.emoji.metadata_changed"
+ />
+
+ <input
+ v-model="packMeta['fallback-src']"
+ class="emoji-info-input input"
+ :disabled="pack.remote !== undefined"
+ >
+ </label>
+ </li>
+ <li>
+ <label>
+ {{ $t('admin_dash.emoji.fallback_sha256') }}
+
+ <input
+ v-model="packMeta['fallback-src-sha256']"
+ :disabled="true"
+ class="emoji-info-input input"
+ >
+ </label>
+ </li>
+ <li>
+ <Checkbox
+ v-model="packMeta['share-files']"
+ :disabled="pack.remote !== undefined"
+ >
+ {{ $t('admin_dash.emoji.share') }}
+ </Checkbox>
+
+ <ModifiedIndicator
+ :changed="metaEdited('share-files')"
+ message-key="admin_dash.emoji.metadata_changed"
+ />
+ </li>
+ <li class="btn-group">
+ <button
+ v-if="pack.remote === undefined"
+ class="button button-default btn"
+ type="button"
+ @click="savePackMetadata"
+ >
+ {{ $t('admin_dash.emoji.save_meta') }}
+ </button>
+ <button
+ v-if="pack.remote === undefined"
+ class="button button-default btn"
+ type="button"
+ @click="savePackMetadata"
+ >
+ {{ $t('admin_dash.emoji.revert_meta') }}
+ </button>
+
+ <button
+ v-if="pack.remote === undefined"
+ class="button button-default btn"
+ type="button"
+ @click="deleteModalVisible = true"
+ >
+ {{ $t('admin_dash.emoji.delete_pack') }}
+
+ <ConfirmModal
+ v-if="deleteModalVisible"
+ :title="$t('admin_dash.emoji.delete_title')"
+ :cancel-text="$t('status.delete_confirm_cancel_button')"
+ :confirm-text="$t('status.delete_confirm_accept_button')"
+ @cancelled="deleteModalVisible = false"
+ @accepted="deleteEmojiPack"
+ >
+ {{ $t('admin_dash.emoji.delete_confirm', [packName]) }}
+ </ConfirmModal>
+ </button>
+
+ <button
+ v-if="pack.remote !== undefined"
+ class="button button-default btn"
+ type="button"
+ @click="$refs.dlPackPopover.showPopover"
+ >
+ {{ $t('admin_dash.emoji.download_pack') }}
+
+ <Popover
+ ref="dlPackPopover"
+ trigger="click"
+ placement="bottom"
+ bound-to-selector=".emoji-tab"
+ popover-class="emoji-tab-edit-popover popover-default"
+ :bound-to="{ x: 'container' }"
+ :offset="{ y: 5 }"
+ >
+ <template #content>
+ <h3>{{ $t('admin_dash.emoji.downloading_pack', [packName]) }}</h3>
+ <div>
+ <div>
+ <div class="emoji-tab-popover-input">
+ <label>
+ {{ $t('admin_dash.emoji.download_as_name') }}
+ <input
+ v-model="remotePackDownloadAs"
+ class="emoji-data-input input"
+ :placeholder="$t('admin_dash.emoji.download_as_name_full')"
+ >
+ </label>
+
+ <div
+ v-if="downloadWillReplaceLocal"
+ class="warning"
+ >
+ <em>{{ $t('admin_dash.emoji.replace_warning') }}</em>
+ </div>
+ </div>
+
+ <button
+ class="button button-default btn"
+ type="button"
+ @click="downloadRemotePack"
+ >
+ {{ $t('admin_dash.emoji.download') }}
+ </button>
+ </div>
+ </div>
+ </template>
+ </Popover>
+ </button>
+ </li>
+ </ul>
+ </div>
+
+ <ul class="setting-list">
+ <h4>
+ {{ $t('admin_dash.emoji.files') }}
+
+ <ModifiedIndicator
+ v-if="pack"
+ :changed="$refs.emojiPopovers && $refs.emojiPopovers.some(p => p.isEdited)"
+ message-key="admin_dash.emoji.emoji_changed"
+ />
+ </h4>
+
+ <div
+ v-if="pack"
+ class="emoji-list"
+ >
+ <EmojiEditingPopover
+ v-if="pack.remote === undefined"
+ placement="bottom"
+ new-upload
+ :title="$t('admin_dash.emoji.adding_new')"
+ :pack-name="packName"
+ @updatePackFiles="updatePackFiles"
+ @displayError="displayError"
+ >
+ <template #trigger>
+ <FAIcon
+ icon="plus"
+ size="2x"
+ :title="$t('admin_dash.emoji.add_file')"
+ />
+ </template>
+ </EmojiEditingPopover>
+
+ <EmojiEditingPopover
+ v-for="(file, shortcode) in pack.files"
+ ref="emojiPopovers"
+ :key="shortcode"
+ placement="top"
+ :title="$t('admin_dash.emoji.editing', [shortcode])"
+ :disabled="pack.remote !== undefined"
+ :shortcode="shortcode"
+ :file="file"
+ :pack-name="packName"
+ @updatePackFiles="updatePackFiles"
+ @displayError="displayError"
+ >
+ <template #trigger>
+ <StillImage
+ class="emoji"
+ :src="emojiAddr(file)"
+ :title="`:${shortcode}:`"
+ :alt="`:${shortcode}:`"
+ />
+ </template>
+ </EmojiEditingPopover>
+ </div>
+ </ul>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script src="./emoji_tab.js"></script>
+
+<style lang="scss" src="./emoji_tab.scss"></style>
diff --git a/src/components/settings_modal/admin_tabs/frontends_tab.js b/src/components/settings_modal/admin_tabs/frontends_tab.js
new file mode 100644
index 00000000..f57310ee
--- /dev/null
+++ b/src/components/settings_modal/admin_tabs/frontends_tab.js
@@ -0,0 +1,113 @@
+import BooleanSetting from '../helpers/boolean_setting.vue'
+import ChoiceSetting from '../helpers/choice_setting.vue'
+import IntegerSetting from '../helpers/integer_setting.vue'
+import StringSetting from '../helpers/string_setting.vue'
+import GroupSetting from '../helpers/group_setting.vue'
+import Popover from 'src/components/popover/popover.vue'
+import PanelLoading from 'src/components/panel_loading/panel_loading.vue'
+
+import SharedComputedObject from '../helpers/shared_computed_object.js'
+import { library } from '@fortawesome/fontawesome-svg-core'
+import {
+ faGlobe
+} from '@fortawesome/free-solid-svg-icons'
+
+library.add(
+ faGlobe
+)
+
+const FrontendsTab = {
+ provide () {
+ return {
+ defaultDraftMode: true,
+ defaultSource: 'admin'
+ }
+ },
+ data () {
+ return {
+ working: false
+ }
+ },
+ components: {
+ BooleanSetting,
+ ChoiceSetting,
+ IntegerSetting,
+ StringSetting,
+ GroupSetting,
+ PanelLoading,
+ Popover
+ },
+ created () {
+ if (this.user.rights.admin) {
+ this.$store.dispatch('loadFrontendsStuff')
+ }
+ },
+ computed: {
+ frontends () {
+ return this.$store.state.adminSettings.frontends
+ },
+ ...SharedComputedObject()
+ },
+ methods: {
+ canInstall (frontend) {
+ const fe = this.frontends.find(f => f.name === frontend.name)
+ if (!fe) return false
+ return fe.refs.includes(frontend.ref)
+ },
+ getSuggestedRef (frontend) {
+ if (this.adminDraft) {
+ const defaultFe = this.adminDraft[':pleroma'][':frontends'][':primary']
+ if (defaultFe?.name === frontend.name && this.canInstall(defaultFe)) {
+ return defaultFe.ref
+ } else {
+ return frontend.refs[0]
+ }
+ } else {
+ return frontend.refs[0]
+ }
+ },
+ update (frontend, suggestRef) {
+ const ref = suggestRef || this.getSuggestedRef(frontend)
+ const { name } = frontend
+ const payload = { name, ref }
+
+ this.working = true
+ this.$store.state.api.backendInteractor.installFrontend({ payload })
+ .finally(() => {
+ this.working = false
+ })
+ .then(async (response) => {
+ this.$store.dispatch('loadFrontendsStuff')
+ if (response.error) {
+ const reason = await response.error.json()
+ this.$store.dispatch('pushGlobalNotice', {
+ level: 'error',
+ messageKey: 'admin_dash.frontend.failure_installing_frontend',
+ messageArgs: {
+ version: name + '/' + ref,
+ reason: reason.error
+ },
+ timeout: 5000
+ })
+ } else {
+ this.$store.dispatch('pushGlobalNotice', {
+ level: 'success',
+ messageKey: 'admin_dash.frontend.success_installing_frontend',
+ messageArgs: {
+ version: name + '/' + ref
+ },
+ timeout: 2000
+ })
+ }
+ })
+ },
+ setDefault (frontend, suggestRef) {
+ const ref = suggestRef || this.getSuggestedRef(frontend)
+ const { name } = frontend
+
+ this.$store.commit('updateAdminDraft', { path: [':pleroma', ':frontends', ':primary'], value: { name, ref } })
+ }
+ }
+}
+
+export default FrontendsTab
diff --git a/src/components/settings_modal/admin_tabs/frontends_tab.scss b/src/components/settings_modal/admin_tabs/frontends_tab.scss
new file mode 100644
index 00000000..420d20b3
--- /dev/null
+++ b/src/components/settings_modal/admin_tabs/frontends_tab.scss
@@ -0,0 +1,29 @@
+.frontends-tab {
+ .cards-list {
+ padding: 0;
+ }
+
+ .relative {
+ position: relative;
+ }
+
+ .overlay {
+ position: absolute;
+ background: var(--bg);
+ // fix buttons showing through
+ z-index: 2;
+ opacity: 0.9;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ }
+
+ dd {
+ text-overflow: ellipsis;
+ word-wrap: nowrap;
+ white-space: nowrap;
+ overflow-x: hidden;
+ max-width: 10em;
+ }
+}
diff --git a/src/components/settings_modal/admin_tabs/frontends_tab.vue b/src/components/settings_modal/admin_tabs/frontends_tab.vue
new file mode 100644
index 00000000..8fb3d399
--- /dev/null
+++ b/src/components/settings_modal/admin_tabs/frontends_tab.vue
@@ -0,0 +1,209 @@
+<template>
+ <div
+ class="frontends-tab"
+ :label="$t('admin_dash.tabs.frontends')"
+ >
+ <div class="setting-item">
+ <h2>{{ $t('admin_dash.tabs.frontends') }}</h2>
+ <p>{{ $t('admin_dash.frontend.wip_notice') }}</p>
+ <ul
+ v-if="adminDraft"
+ class="setting-list"
+ >
+ <li>
+ <h3>{{ $t('admin_dash.frontend.default_frontend') }}</h3>
+ <p>{{ $t('admin_dash.frontend.default_frontend_tip') }}</p>
+ <ul class="setting-list">
+ <li>
+ <StringSetting path=":pleroma.:frontends.:primary.name" />
+ </li>
+ <li>
+ <StringSetting path=":pleroma.:frontends.:primary.ref" />
+ </li>
+ <li>
+ <GroupSetting path=":pleroma.:frontends.:primary" />
+ </li>
+ </ul>
+ </li>
+ </ul>
+ <div
+ v-else
+ class="setting-list"
+ >
+ {{ $t('admin_dash.frontend.default_frontend_unavail') }}
+ </div>
+
+ <div class="setting-list relative">
+ <PanelLoading
+ v-if="working"
+ class="overlay"
+ />
+ <h3>{{ $t('admin_dash.frontend.available_frontends') }}</h3>
+ <ul class="cards-list">
+ <li
+ v-for="frontend in frontends"
+ :key="frontend.name"
+ >
+ <strong>{{ frontend.name }}</strong>
+ {{ ' ' }}
+ <span v-if="adminDraft && adminDraft[':pleroma'][':frontends'][':primary']?.name === frontend.name">
+ <i18n-t
+ v-if="adminDraft && adminDraft[':pleroma'][':frontends'][':primary']?.ref === frontend.refs[0]"
+ keypath="admin_dash.frontend.is_default"
+ />
+ <i18n-t
+ v-else
+ keypath="admin_dash.frontend.is_default_custom"
+ >
+ <template #version>
+ <code>{{ adminDraft && adminDraft[':pleroma'][':frontends'][':primary'].ref }}</code>
+ </template>
+ </i18n-t>
+ </span>
+ <dl>
+ <dt>{{ $t('admin_dash.frontend.repository') }}</dt>
+ <dd>
+ <a
+ :href="frontend.git"
+ target="_blank"
+ >{{ frontend.git }}</a>
+ </dd>
+ <template v-if="expertLevel">
+ <dt>{{ $t('admin_dash.frontend.versions') }}</dt>
+ <dd
+ v-for="ref in frontend.refs"
+ :key="ref"
+ >
+ <code>{{ ref }}</code>
+ </dd>
+ </template>
+ <dt v-if="expertLevel">
+ {{ $t('admin_dash.frontend.build_url') }}
+ </dt>
+ <dd v-if="expertLevel">
+ <a
+ :href="frontend.build_url"
+ target="_blank"
+ >{{ frontend.build_url }}</a>
+ </dd>
+ </dl>
+ <div>
+ <span class="btn-group">
+ <button
+ class="button button-default btn"
+ type="button"
+ @click="update(frontend)"
+ >
+ {{
+ frontend.installed
+ ? $t('admin_dash.frontend.reinstall')
+ : $t('admin_dash.frontend.install')
+ }}
+ <code>
+ {{
+ getSuggestedRef(frontend)
+ }}
+ </code>
+ </button>
+ <Popover
+ v-if="frontend.refs.length > 1"
+ trigger="click"
+ class="button-dropdown"
+ placement="bottom"
+ >
+ <template #content="{close}">
+ <div class="dropdown-menu">
+ <button
+ v-for="ref in frontend.refs"
+ :key="ref"
+ class="menu-item dropdown-item"
+ @click.prevent="update(frontend, ref)"
+ @click="close"
+ >
+ <i18n-t keypath="admin_dash.frontend.install_version">
+ <template #version>
+ <code>{{ ref }}</code>
+ </template>
+ </i18n-t>
+ </button>
+ </div>
+ </template>
+ <template #trigger>
+ <button
+ class="button button-default btn dropdown-button"
+ type="button"
+ :title="$t('admin_dash.frontend.more_install_options')"
+ >
+ <FAIcon icon="chevron-down" />
+ </button>
+ </template>
+ </Popover>
+ </span>
+ <span
+ v-if="frontend.installed && frontend.name !== 'admin-fe'"
+ class="btn-group"
+ >
+ <button
+ class="button button-default btn"
+ type="button"
+ :disabled="
+ !adminDraft || adminDraft[':pleroma'][':frontends'][':primary']?.name === frontend.name &&
+ adminDraft[':pleroma'][':frontends'][':primary']?.ref === frontend.refs[0]
+ "
+ @click="setDefault(frontend)"
+ >
+ {{
+ $t('admin_dash.frontend.set_default')
+ }}
+ <code>
+ {{
+ getSuggestedRef(frontend)
+ }}
+ </code>
+ </button>
+ {{ ' ' }}
+ <Popover
+ v-if="frontend.refs.length > 1"
+ trigger="click"
+ class="button-dropdown"
+ placement="bottom"
+ >
+ <template #content="{close}">
+ <div class="dropdown-menu">
+ <button
+ v-for="ref in frontend.installedRefs || frontend.refs"
+ :key="ref"
+ class="menu-item dropdown-item"
+ @click.prevent="setDefault(frontend, ref)"
+ @click="close"
+ >
+ <i18n-t keypath="admin_dash.frontend.set_default_version">
+ <template #version>
+ <code>{{ ref }}</code>
+ </template>
+ </i18n-t>
+ </button>
+ </div>
+ </template>
+ <template #trigger>
+ <button
+ class="button button-default btn dropdown-button"
+ type="button"
+ :title="$t('admin_dash.frontend.more_default_options')"
+ >
+ <FAIcon icon="chevron-down" />
+ </button>
+ </template>
+ </Popover>
+ </span>
+ </div>
+ </li>
+ </ul>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script src="./frontends_tab.js"></script>
+
+<style lang="scss" src="./frontends_tab.scss"></style>
diff --git a/src/components/settings_modal/admin_tabs/instance_tab.js b/src/components/settings_modal/admin_tabs/instance_tab.js
new file mode 100644
index 00000000..b07bafe8
--- /dev/null
+++ b/src/components/settings_modal/admin_tabs/instance_tab.js
@@ -0,0 +1,38 @@
+import BooleanSetting from '../helpers/boolean_setting.vue'
+import ChoiceSetting from '../helpers/choice_setting.vue'
+import IntegerSetting from '../helpers/integer_setting.vue'
+import StringSetting from '../helpers/string_setting.vue'
+import GroupSetting from '../helpers/group_setting.vue'
+import AttachmentSetting from '../helpers/attachment_setting.vue'
+
+import SharedComputedObject from '../helpers/shared_computed_object.js'
+import { library } from '@fortawesome/fontawesome-svg-core'
+import {
+ faGlobe
+} from '@fortawesome/free-solid-svg-icons'
+
+library.add(
+ faGlobe
+)
+
+const InstanceTab = {
+ provide () {
+ return {
+ defaultDraftMode: true,
+ defaultSource: 'admin'
+ }
+ },
+ components: {
+ BooleanSetting,
+ ChoiceSetting,
+ IntegerSetting,
+ StringSetting,
+ AttachmentSetting,
+ GroupSetting
+ },
+ computed: {
+ ...SharedComputedObject()
+ }
+}
+
+export default InstanceTab
diff --git a/src/components/settings_modal/admin_tabs/instance_tab.vue b/src/components/settings_modal/admin_tabs/instance_tab.vue
new file mode 100644
index 00000000..32e8df25
--- /dev/null
+++ b/src/components/settings_modal/admin_tabs/instance_tab.vue
@@ -0,0 +1,206 @@
+<template>
+ <div :label="$t('admin_dash.tabs.instance')">
+ <div class="setting-item">
+ <h2>{{ $t('admin_dash.instance.instance') }}</h2>
+ <ul class="setting-list">
+ <li>
+ <StringSetting path=":pleroma.:instance.:name" />
+ </li>
+ <!-- See https://git.pleroma.social/pleroma/pleroma/-/merge_requests/3963 -->
+ <li v-if="adminDraft[':pleroma'][':instance'][':favicon'] !== undefined">
+ <AttachmentSetting
+ compact
+ path=":pleroma.:instance.:favicon"
+ />
+ </li>
+ <li>
+ <StringSetting path=":pleroma.:instance.:email" />
+ </li>
+ <li>
+ <StringSetting path=":pleroma.:instance.:description" />
+ </li>
+ <li>
+ <StringSetting path=":pleroma.:instance.:short_description" />
+ </li>
+ <li>
+ <AttachmentSetting
+ compact
+ path=":pleroma.:instance.:instance_thumbnail"
+ />
+ </li>
+ <li>
+ <AttachmentSetting path=":pleroma.:instance.:background_image" />
+ </li>
+ </ul>
+ </div>
+ <div class="setting-item">
+ <h2>{{ $t('admin_dash.instance.registrations') }}</h2>
+ <ul class="setting-list">
+ <li>
+ <BooleanSetting path=":pleroma.:instance.:registrations_open" />
+ <ul class="setting-list suboptions">
+ <li>
+ <BooleanSetting
+ path=":pleroma.:instance.:invites_enabled"
+ parent-path=":pleroma.:instance.:registrations_open"
+ parent-invert
+ />
+ </li>
+ </ul>
+ </li>
+ <li>
+ <BooleanSetting path=":pleroma.:instance.:birthday_required" />
+ <ul class="setting-list suboptions">
+ <li>
+ <IntegerSetting
+ path=":pleroma.:instance.:birthday_min_age"
+ parent-path=":pleroma.:instance.:birthday_required"
+ />
+ </li>
+ </ul>
+ </li>
+ <li>
+ <BooleanSetting path=":pleroma.:instance.:account_activation_required" />
+ </li>
+ <li>
+ <BooleanSetting path=":pleroma.:instance.:account_approval_required" />
+ </li>
+ <li>
+ <h3>{{ $t('admin_dash.instance.captcha_header') }}</h3>
+ <ul class="setting-list">
+ <li>
+ <BooleanSetting :path="[':pleroma', 'Pleroma.Captcha', ':enabled']" />
+ <ul class="setting-list suboptions">
+ <li>
+ <ChoiceSetting
+ :path="[':pleroma', 'Pleroma.Captcha', ':method']"
+ :parent-path="[':pleroma', 'Pleroma.Captcha', ':enabled']"
+ :option-label-map="{
+ 'Pleroma.Captcha.Native': $t('admin_dash.captcha.native'),
+ 'Pleroma.Captcha.Kocaptcha': $t('admin_dash.captcha.kocaptcha')
+ }"
+ />
+ <IntegerSetting
+ :path="[':pleroma', 'Pleroma.Captcha', ':seconds_valid']"
+ :parent-path="[':pleroma', 'Pleroma.Captcha', ':enabled']"
+ />
+ </li>
+ <li
+ v-if="adminDraft[':pleroma']['Pleroma.Captcha'][':enabled'] && adminDraft[':pleroma']['Pleroma.Captcha'][':method'] === 'Pleroma.Captcha.Kocaptcha'"
+ >
+ <h4>{{ $t('admin_dash.instance.kocaptcha') }}</h4>
+ <ul class="setting-list">
+ <li>
+ <StringSetting :path="[':pleroma', 'Pleroma.Captcha.Kocaptcha', ':endpoint']" />
+ </li>
+ </ul>
+ </li>
+ </ul>
+ </li>
+ </ul>
+ </li>
+ </ul>
+ </div>
+ <div class="setting-item">
+ <h2>{{ $t('admin_dash.instance.access') }}</h2>
+ <ul class="setting-list">
+ <li>
+ <BooleanSetting
+ override-backend-description
+ override-backend-description-label
+ path=":pleroma.:instance.:public"
+ />
+ </li>
+ <li>
+ <ChoiceSetting
+ override-backend-description
+ override-backend-description-label
+ path=":pleroma.:instance.:limit_to_local_content"
+ />
+ </li>
+ <li v-if="expertLevel">
+ <h3>{{ $t('admin_dash.instance.restrict.header') }}</h3>
+ <p>
+ {{ $t('admin_dash.instance.restrict.description') }}
+ </p>
+ <ul class="setting-list">
+ <li>
+ <h4>{{ $t('admin_dash.instance.restrict.timelines') }}</h4>
+ <ul class="setting-list">
+ <li>
+ <BooleanSetting
+ path=":pleroma.:restrict_unauthenticated.:timelines.:local"
+ indeterminate-state=":if_instance_is_private"
+ swap-description-and-label
+ hide-description
+ />
+ </li>
+ <li>
+ <BooleanSetting
+ path=":pleroma.:restrict_unauthenticated.:timelines.:federated"
+ indeterminate-state=":if_instance_is_private"
+ swap-description-and-label
+ hide-description
+ />
+ </li>
+ <li>
+ <GroupSetting path=":pleroma.:restrict_unauthenticated.:timelines" />
+ </li>
+ </ul>
+ </li>
+ <li>
+ <h4>{{ $t('admin_dash.instance.restrict.profiles') }}</h4>
+ <ul class="setting-list">
+ <li>
+ <BooleanSetting
+ path=":pleroma.:restrict_unauthenticated.:profiles.:local"
+ indeterminate-state=":if_instance_is_private"
+ swap-description-and-label
+ hide-description
+ />
+ </li>
+ <li>
+ <BooleanSetting
+ path=":pleroma.:restrict_unauthenticated.:profiles.:remote"
+ indeterminate-state=":if_instance_is_private"
+ swap-description-and-label
+ hide-description
+ />
+ </li>
+ <li>
+ <GroupSetting path=":pleroma.:restrict_unauthenticated.:profiles" />
+ </li>
+ </ul>
+ </li>
+ <li>
+ <h4>{{ $t('admin_dash.instance.restrict.activities') }}</h4>
+ <ul class="setting-list">
+ <li>
+ <BooleanSetting
+ path=":pleroma.:restrict_unauthenticated.:activities.:local"
+ indeterminate-state=":if_instance_is_private"
+ swap-description-and-label
+ hide-description
+ />
+ </li>
+ <li>
+ <BooleanSetting
+ path=":pleroma.:restrict_unauthenticated.:activities.:remote"
+ indeterminate-state=":if_instance_is_private"
+ swap-description-and-label
+ hide-description
+ />
+ </li>
+ <li>
+ <GroupSetting path=":pleroma.:restrict_unauthenticated.:activities" />
+ </li>
+ </ul>
+ </li>
+ </ul>
+ </li>
+ </ul>
+ </div>
+ </div>
+</template>
+
+<script src="./instance_tab.js"></script>
diff --git a/src/components/settings_modal/admin_tabs/limits_tab.js b/src/components/settings_modal/admin_tabs/limits_tab.js
new file mode 100644
index 00000000..684739c3
--- /dev/null
+++ b/src/components/settings_modal/admin_tabs/limits_tab.js
@@ -0,0 +1,29 @@
+import BooleanSetting from '../helpers/boolean_setting.vue'
+import ChoiceSetting from '../helpers/choice_setting.vue'
+import IntegerSetting from '../helpers/integer_setting.vue'
+import StringSetting from '../helpers/string_setting.vue'
+
+import SharedComputedObject from '../helpers/shared_computed_object.js'
+import { library } from '@fortawesome/fontawesome-svg-core'
+import {
+ faGlobe
+} from '@fortawesome/free-solid-svg-icons'
+
+library.add(
+ faGlobe
+)
+
+const LimitsTab = {
+ data () {},
+ components: {
+ BooleanSetting,
+ ChoiceSetting,
+ IntegerSetting,
+ StringSetting
+ },
+ computed: {
+ ...SharedComputedObject()
+ }
+}
+
+export default LimitsTab
diff --git a/src/components/settings_modal/admin_tabs/limits_tab.vue b/src/components/settings_modal/admin_tabs/limits_tab.vue
new file mode 100644
index 00000000..ef4b9271
--- /dev/null
+++ b/src/components/settings_modal/admin_tabs/limits_tab.vue
@@ -0,0 +1,136 @@
+<template>
+ <div :label="$t('admin_dash.tabs.limits')">
+ <div class="setting-item">
+ <h2>{{ $t('admin_dash.limits.arbitrary_limits') }}</h2>
+ <ul class="setting-list">
+ <li>
+ <h3>{{ $t('admin_dash.limits.posts') }}</h3>
+ <ul class="setting-list">
+ <li>
+ <IntegerSetting
+ source="admin"
+ path=":pleroma.:instance.:limit"
+ draft-mode
+ />
+ </li>
+ <li>
+ <IntegerSetting
+ source="admin"
+ path=":pleroma.:instance.:remote_limit"
+ expert="1"
+ draft-mode
+ />
+ </li>
+ </ul>
+ </li>
+ <li>
+ <h3>{{ $t('admin_dash.limits.uploads') }}</h3>
+ <ul class="setting-list">
+ <li>
+ <IntegerSetting
+ source="admin"
+ path=":pleroma.:instance.:description_limit"
+ draft-mode
+ />
+ </li>
+ <li>
+ <IntegerSetting
+ source="admin"
+ path=":pleroma.:instance.:upload_limit"
+ draft-mode
+ />
+ </li>
+ <li>
+ <IntegerSetting
+ source="admin"
+ path=":pleroma.:instance.:max_media_attachments"
+ draft-mode
+ />
+ </li>
+ </ul>
+ </li>
+ <li>
+ <h3>{{ $t('admin_dash.limits.users') }}</h3>
+ <ul class="setting-list">
+ <li>
+ <IntegerSetting
+ source="admin"
+ path=":pleroma.:instance.:max_pinned_statuses"
+ draft-mode
+ />
+ </li>
+ <li>
+ <IntegerSetting
+ source="admin"
+ path=":pleroma.:instance.:user_bio_length"
+ draft-mode
+ />
+ </li>
+ <li>
+ <IntegerSetting
+ source="admin"
+ path=":pleroma.:instance.:user_name_length"
+ draft-mode
+ />
+ </li>
+ <li>
+ <h4>{{ $t('admin_dash.limits.profile_fields') }}</h4>
+ <ul class="setting-list">
+ <li>
+ <IntegerSetting
+ source="admin"
+ path=":pleroma.:instance.:max_account_fields"
+ draft-mode
+ />
+ </li>
+ <li>
+ <IntegerSetting
+ source="admin"
+ path=":pleroma.:instance.:max_remote_account_fields"
+ draft-mode
+ expert="1"
+ />
+ </li>
+ <li>
+ <IntegerSetting
+ source="admin"
+ path=":pleroma.:instance.:account_field_name_length"
+ draft-mode
+ />
+ </li>
+ <li>
+ <IntegerSetting
+ source="admin"
+ path=":pleroma.:instance.:account_field_value_length"
+ draft-mode
+ />
+ </li>
+ </ul>
+ </li>
+ <li>
+ <h4>{{ $t('admin_dash.limits.user_uploads') }}</h4>
+ <ul class="setting-list">
+ <li>
+ <IntegerSetting
+ source="admin"
+ path=":pleroma.:instance.:avatar_upload_limit"
+ draft-mode
+ />
+ </li>
+ <li>
+ <IntegerSetting
+ source="admin"
+ path=":pleroma.:instance.:banner_upload_limit"
+ draft-mode
+ />
+ </li>
+ </ul>
+ </li>
+ </ul>
+ </li>
+ </ul>
+ </div>
+ </div>
+</template>
+
+<script src="./limits_tab.js"></script>
diff --git a/src/components/settings_modal/helpers/attachment_setting.js b/src/components/settings_modal/helpers/attachment_setting.js
new file mode 100644
index 00000000..c4c04b2b
--- /dev/null
+++ b/src/components/settings_modal/helpers/attachment_setting.js
@@ -0,0 +1,44 @@
+import Setting from './setting.js'
+import { fileTypeExt } from 'src/services/file_type/file_type.service.js'
+import MediaUpload from 'src/components/media_upload/media_upload.vue'
+import Attachment from 'src/components/attachment/attachment.vue'
+
+export default {
+ ...Setting,
+ props: {
+ ...Setting.props,
+ compact: Boolean,
+ acceptTypes: {
+ type: String,
+ required: false,
+ default: 'image/*'
+ }
+ },
+ components: {
+ ...Setting.components,
+ MediaUpload,
+ Attachment
+ },
+ computed: {
+ ...Setting.computed,
+ attachment () {
+ const path = this.realDraftMode ? this.draft : this.state
+ // The "server" part is primarily for local dev, but could be useful for alt-domain or multiuser usage.
+ const url = path.includes('://') ? path : this.$store.state.instance.server + path
+ return {
+ mimetype: fileTypeExt(url),
+ url
+ }
+ }
+ },
+ methods: {
+ ...Setting.methods,
+ setMediaFile (fileInfo) {
+ if (this.realDraftMode) {
+ this.draft = fileInfo.url
+ } else {
+ this.configSink(this.path, fileInfo.url)
+ }
+ }
+ }
+}
diff --git a/src/components/settings_modal/helpers/attachment_setting.vue b/src/components/settings_modal/helpers/attachment_setting.vue
new file mode 100644
index 00000000..96c80ab1
--- /dev/null
+++ b/src/components/settings_modal/helpers/attachment_setting.vue
@@ -0,0 +1,126 @@
+<template>
+ <span
+ v-if="matchesExpertLevel"
+ class="AttachmentSetting"
+ :class="{ '-compact': compact }"
+ >
+ <label
+ :for="path"
+ :class="{ 'faint': shouldBeDisabled }"
+ >
+ <template v-if="backendDescriptionLabel">
+ {{ backendDescriptionLabel + ' ' }}
+ </template>
+ <template v-else-if="source === 'admin'">
+ MISSING LABEL FOR {{ path }}
+ </template>
+ <slot v-else />
+
+ </label>
+ <p
+ v-if="backendDescriptionDescription"
+ class="setting-description"
+ :class="{ 'faint': shouldBeDisabled }"
+ >
+ {{ backendDescriptionDescription + ' ' }}
+ </p>
+ <div class="attachment-input">
+ <div class="controls control-field">
+ <label for="path">{{ $t('settings.url') }}</label>
+ <input
+ :id="path"
+ class="input string-input"
+ :disabled="shouldBeDisabled"
+ :value="realDraftMode ? draft : state"
+ @change="update"
+ >
+ {{ ' ' }}
+ <ModifiedIndicator
+ :changed="isChanged"
+ :onclick="reset"
+ />
+ <ProfileSettingIndicator :is-profile="isProfileSetting" />
+ </div>
+ <div v-if="!compact">{{ $t('settings.preview') }}</div>
+ <Attachment
+ class="attachment"
+ :compact="compact"
+ :attachment="attachment"
+ size="small"
+ hide-description
+ @setMedia="onMedia"
+ @naturalSizeLoad="onNaturalSizeLoad"
+ />
+ <div class="controls control-upload">
+ <MediaUpload
+ ref="mediaUpload"
+ class="media-upload-icon"
+ :drop-files="dropFiles"
+ normal-button
+ :accept-types="acceptTypes"
+ @uploaded="setMediaFile"
+ @upload-failed="uploadFailed"
+ />
+ </div>
+ </div>
+ <DraftButtons />
+ </span>
+</template>
+
+<script src="./attachment_setting.js"></script>
+
+<style lang="scss">
+.AttachmentSetting {
+ .attachment {
+ display: block;
+ width: 100%;
+ height: 15em;
+ margin-bottom: 0.5em;
+ }
+
+ .attachment-input {
+ margin-left: 1em;
+ display: flex;
+ flex-direction: column;
+ width: 20em;
+ }
+
+ &.-compact {
+ .attachment-input {
+ flex-direction: row;
+ align-items: flex-end;
+ }
+
+ .attachment {
+ flex: 0;
+ order: 0;
+ display: block;
+ min-width: 4em;
+ height: 4em;
+ align-self: center;
+ margin-bottom: 0;
+ }
+
+ .control-field {
+ order: 1;
+ min-width: 12em;
+ margin-left: 0.5em;
+ }
+
+ .control-upload {
+ order: 2;
+ min-width: 12em;
+ padding: 0 0.5em;
+ }
+ }
+
+ .controls {
+ margin-bottom: 0.5em;
+
+ input,
+ button {
+ width: 100%;
+ }
+ }
+}
+</style>
diff --git a/src/components/settings_modal/helpers/boolean_setting.js b/src/components/settings_modal/helpers/boolean_setting.js
index 2e6992cb..199d3d0f 100644
--- a/src/components/settings_modal/helpers/boolean_setting.js
+++ b/src/components/settings_modal/helpers/boolean_setting.js
@@ -1,56 +1,31 @@
-import { get, set } from 'lodash'
import Checkbox from 'src/components/checkbox/checkbox.vue'
-import ModifiedIndicator from './modified_indicator.vue'
-import ServerSideIndicator from './server_side_indicator.vue'
+import Setting from './setting.js'
+
export default {
+ ...Setting,
+ props: {
+ ...Setting.props,
+ indeterminateState: [String, Object]
+ },
components: {
- Checkbox,
- ModifiedIndicator,
- ServerSideIndicator
+ ...Setting.components,
+ Checkbox
},
- props: [
- 'path',
- 'disabled',
- 'expert'
- ],
computed: {
- pathDefault () {
- const [firstSegment, ...rest] = this.path.split('.')
- return [firstSegment + 'DefaultValue', ...rest].join('.')
- },
- state () {
- const value = get(this.$parent, this.path)
- if (value === undefined) {
- return this.defaultState
- } else {
- return value
- }
- },
- defaultState () {
- return get(this.$parent, this.pathDefault)
- },
- isServerSide () {
- return this.path.startsWith('serverSide_')
- },
- isChanged () {
- return !this.path.startsWith('serverSide_') && this.state !== this.defaultState
- },
- matchesExpertLevel () {
- return (this.expert || 0) <= this.$parent.expertLevel
+ ...Setting.computed,
+ isIndeterminate () {
+ return this.visibleState === this.indeterminateState
}
},
methods: {
- update (e) {
- const [firstSegment, ...rest] = this.path.split('.')
- set(this.$parent, this.path, e)
- // Updating nested properties does not trigger update on its parent.
- // probably still not as reliable, but works for depth=1 at least
- if (rest.length > 0) {
- set(this.$parent, firstSegment, { ...get(this.$parent, firstSegment) })
+ ...Setting.methods,
+ getValue (e) {
+ // Basic tri-state toggle implementation
+ if (!!this.indeterminateState && !e && this.visibleState === true) {
+ // If we have indeterminate state, switching from true to false first goes through indeterminate
+ return this.indeterminateState
}
- },
- reset () {
- set(this.$parent, this.path, this.defaultState)
+ return e
}
}
}
diff --git a/src/components/settings_modal/helpers/boolean_setting.vue b/src/components/settings_modal/helpers/boolean_setting.vue
index 41142966..5a9eab34 100644
--- a/src/components/settings_modal/helpers/boolean_setting.vue
+++ b/src/components/settings_modal/helpers/boolean_setting.vue
@@ -4,23 +4,37 @@
class="BooleanSetting"
>
<Checkbox
- :model-value="state"
- :disabled="disabled"
+ :model-value="visibleState"
+ :disabled="shouldBeDisabled"
+ :indeterminate="isIndeterminate"
@update:modelValue="update"
>
<span
- v-if="!!$slots.default"
class="label"
+ :class="{ 'faint': shouldBeDisabled }"
>
- <slot />
+ <template v-if="backendDescriptionLabel">
+ {{ backendDescriptionLabel }}
+ </template>
+ <template v-else-if="source === 'admin'">
+ MISSING LABEL FOR {{ path }}
+ </template>
+ <slot v-else />
</span>
- {{ ' ' }}
- <ModifiedIndicator
- :changed="isChanged"
- :onclick="reset"
- />
- <ServerSideIndicator :server-side="isServerSide" />
</Checkbox>
+ <ModifiedIndicator
+ :changed="isChanged"
+ :onclick="reset"
+ />
+ <ProfileSettingIndicator :is-profile="isProfileSetting" />
+ <DraftButtons />
+ <p
+ v-if="backendDescriptionDescription"
+ class="setting-description"
+ :class="{ 'faint': shouldBeDisabled }"
+ >
+ {{ backendDescriptionDescription + ' ' }}
+ </p>
</label>
</template>
diff --git a/src/components/settings_modal/helpers/choice_setting.js b/src/components/settings_modal/helpers/choice_setting.js
index 3da559fe..bdeece76 100644
--- a/src/components/settings_modal/helpers/choice_setting.js
+++ b/src/components/settings_modal/helpers/choice_setting.js
@@ -1,51 +1,41 @@
-import { get, set } from 'lodash'
import Select from 'src/components/select/select.vue'
-import ModifiedIndicator from './modified_indicator.vue'
-import ServerSideIndicator from './server_side_indicator.vue'
+import Setting from './setting.js'
+
export default {
+ ...Setting,
components: {
- Select,
- ModifiedIndicator,
- ServerSideIndicator
+ ...Setting.components,
+ Select
},
- props: [
- 'path',
- 'disabled',
- 'options',
- 'expert'
- ],
- computed: {
- pathDefault () {
- const [firstSegment, ...rest] = this.path.split('.')
- return [firstSegment + 'DefaultValue', ...rest].join('.')
+ props: {
+ ...Setting.props,
+ options: {
+ type: Array,
+ required: false
},
- state () {
- const value = get(this.$parent, this.path)
- if (value === undefined) {
- return this.defaultState
- } else {
- return value
+ optionLabelMap: {
+ type: Object,
+ required: false,
+ default: {}
+ }
+ },
+ computed: {
+ ...Setting.computed,
+ realOptions () {
+ if (this.realSource === 'admin') {
+ return this.backendDescriptionSuggestions.map(x => ({
+ key: x,
+ value: x,
+ label: this.optionLabelMap[x] || x
+ }))
}
- },
- defaultState () {
- return get(this.$parent, this.pathDefault)
- },
- isServerSide () {
- return this.path.startsWith('serverSide_')
- },
- isChanged () {
- return !this.path.startsWith('serverSide_') && this.state !== this.defaultState
- },
- matchesExpertLevel () {
- return (this.expert || 0) <= this.$parent.expertLevel
+ return this.options
}
},
methods: {
- update (e) {
- set(this.$parent, this.path, e)
- },
- reset () {
- set(this.$parent, this.path, this.defaultState)
+ ...Setting.methods,
+ getValue (e) {
+ return e
}
}
}
diff --git a/src/components/settings_modal/helpers/choice_setting.vue b/src/components/settings_modal/helpers/choice_setting.vue
index 8fdbb5d3..114e9b7d 100644
--- a/src/components/settings_modal/helpers/choice_setting.vue
+++ b/src/components/settings_modal/helpers/choice_setting.vue
@@ -3,15 +3,20 @@
v-if="matchesExpertLevel"
class="ChoiceSetting"
>
- <slot />
+ <template v-if="backendDescriptionLabel">
+ {{ backendDescriptionLabel }}
+ </template>
+ <template v-else>
+ <slot />
+ </template>
{{ ' ' }}
<Select
- :model-value="state"
+ :model-value="realDraftMode ? draft :state"
:disabled="disabled"
@update:modelValue="update"
>
<option
- v-for="option in options"
+ v-for="option in realOptions"
:key="option.key"
:value="option.value"
>
@@ -23,7 +28,14 @@
:changed="isChanged"
:onclick="reset"
/>
- <ServerSideIndicator :server-side="isServerSide" />
+ <ProfileSettingIndicator :is-profile="isProfileSetting" />
+ <DraftButtons />
+ <p
+ v-if="backendDescriptionDescription"
+ class="setting-description"
+ >
+ {{ backendDescriptionDescription + ' ' }}
+ </p>
</label>
</template>
diff --git a/src/components/settings_modal/helpers/draft_buttons.vue b/src/components/settings_modal/helpers/draft_buttons.vue
new file mode 100644
index 00000000..46a70e86
--- /dev/null
+++ b/src/components/settings_modal/helpers/draft_buttons.vue
@@ -0,0 +1,88 @@
+<!-- this is a helper exclusive to Setting components -->
+<!-- TODO make it reusable -->
+<template>
+ <span
+ class="DraftButtons"
+ >
+ <Popover
+ v-if="$parent.isDirty"
+ trigger="hover"
+ normal-button
+ :trigger-attrs="{ 'aria-label': $t('settings.commit_value_tooltip') }"
+ @click="$parent.commitDraft"
+ >
+ <template #trigger>
+ {{ $t('settings.commit_value') }}
+ </template>
+ <template #content>
+ <div class="modified-tooltip">
+ {{ $t('settings.commit_value_tooltip') }}
+ </div>
+ </template>
+ </Popover>
+ <Popover
+ v-if="$parent.isDirty"
+ trigger="hover"
+ normal-button
+ :trigger-attrs="{ 'aria-label': $t('settings.reset_value_tooltip') }"
+ @click="$parent.reset"
+ >
+ <template #trigger>
+ {{ $t('settings.reset_value') }}
+ </template>
+ <template #content>
+ <div class="modified-tooltip">
+ {{ $t('settings.reset_value_tooltip') }}
+ </div>
+ </template>
+ </Popover>
+ <Popover
+ v-if="$parent.canHardReset"
+ trigger="hover"
+ normal-button
+ :trigger-attrs="{ 'aria-label': $t('settings.hard_reset_value_tooltip') }"
+ @click="$parent.hardReset"
+ >
+ <template #trigger>
+ {{ $t('settings.hard_reset_value') }}
+ </template>
+ <template #content>
+ <div class="modified-tooltip">
+ {{ $t('settings.hard_reset_value_tooltip') }}
+ </div>
+ </template>
+ </Popover>
+ </span>
+</template>
+
+<script>
+import Popover from 'src/components/popover/popover.vue'
+import { library } from '@fortawesome/fontawesome-svg-core'
+import { faWrench } from '@fortawesome/free-solid-svg-icons'
+
+library.add(
+ faWrench
+)
+
+export default {
+ components: { Popover },
+ props: ['changed']
+}
+</script>
+
+<style lang="scss">
+.DraftButtons {
+ display: inline-block;
+ position: relative;
+
+ .button-default {
+ margin-left: 0.5em;
+ }
+}
+
+.draft-tooltip {
+ margin: 0.5em 1em;
+ min-width: 10em;
+ text-align: center;
+}
+</style>
diff --git a/src/components/settings_modal/helpers/emoji_editing_popover.vue b/src/components/settings_modal/helpers/emoji_editing_popover.vue
new file mode 100644
index 00000000..f0465dd5
--- /dev/null
+++ b/src/components/settings_modal/helpers/emoji_editing_popover.vue
@@ -0,0 +1,227 @@
+<template>
+ <Popover
+ ref="emojiPopover"
+ trigger="click"
+ :placement="placement"
+ bound-to-selector=".emoji-list"
+ popover-class="emoji-tab-edit-popover popover-default"
+ :bound-to="{ x: 'container' }"
+ :offset="{ y: 5 }"
+ :disabled="disabled"
+ :class="{'emoji-unsaved': isEdited}"
+ >
+ <template #trigger>
+ <slot name="trigger" />
+ </template>
+ <template #content>
+ <h3>
+ {{ title }}
+ </h3>
+
+ <StillImage
+ v-if="emojiPreview"
+ class="emoji"
+ :src="emojiPreview"
+ />
+ <div
+ v-else
+ class="emoji"
+ />
+
+ <div
+ v-if="newUpload"
+ class="emoji-tab-popover-input"
+ >
+ <input
+ type="file"
+ accept="image/*"
+ class="emoji-tab-popover-file input"
+ @change="uploadFile = $event.target.files"
+ >
+ </div>
+ <div>
+ <div class="emoji-tab-popover-input">
+ <label>
+ {{ $t('admin_dash.emoji.shortcode') }}
+ <input
+ v-model="editedShortcode"
+ class="emoji-data-input input"
+ :placeholder="$t('admin_dash.emoji.new_shortcode')"
+ >
+ </label>
+ </div>
+
+ <div class="emoji-tab-popover-input">
+ <label>
+ {{ $t('admin_dash.emoji.filename') }}
+
+ <input
+ v-model="editedFile"
+ class="emoji-data-input input"
+ :placeholder="$t('admin_dash.emoji.new_filename')"
+ >
+ </label>
+ </div>
+
+ <button
+ class="button button-default btn"
+ type="button"
+ :disabled="newUpload ? uploadFile.length == 0 : !isEdited"
+ @click="newUpload ? uploadEmoji() : saveEditedEmoji()"
+ >
+ {{ $t('admin_dash.emoji.save') }}
+ </button>
+
+ <template v-if="!newUpload">
+ <button
+ class="button button-default btn emoji-tab-popover-button"
+ type="button"
+ @click="deleteModalVisible = true"
+ >
+ {{ $t('admin_dash.emoji.delete') }}
+ </button>
+ <button
+ class="button button-default btn emoji-tab-popover-button"
+ type="button"
+ @click="revertEmoji"
+ >
+ {{ $t('admin_dash.emoji.revert') }}
+ </button>
+ <ConfirmModal
+ v-if="deleteModalVisible"
+ :title="$t('admin_dash.emoji.delete_title')"
+ :cancel-text="$t('status.delete_confirm_cancel_button')"
+ :confirm-text="$t('status.delete_confirm_accept_button')"
+ @cancelled="deleteModalVisible = false"
+ @accepted="deleteEmoji"
+ >
+ {{ $t('admin_dash.emoji.delete_confirm', [shortcode]) }}
+ </ConfirmModal>
+ </template>
+ </div>
+ </template>
+ </Popover>
+</template>
+
+<script>
+import Popover from 'components/popover/popover.vue'
+import ConfirmModal from 'components/confirm_modal/confirm_modal.vue'
+import StillImage from 'components/still-image/still-image.vue'
+
+export default {
+ components: { Popover, ConfirmModal, StillImage },
+ inject: ['emojiAddr'],
+ props: {
+ placement: String,
+ disabled: {
+ type: Boolean,
+ default: false
+ },
+
+ newUpload: Boolean,
+
+ title: String,
+ packName: String,
+ shortcode: {
+ type: String,
+ // Only exists when this is not a new upload
+ default: ''
+ },
+ file: {
+ type: String,
+ // Only exists when this is not a new upload
+ default: ''
+ }
+ },
+ emits: ['updatePackFiles', 'displayError'],
+ data () {
+ return {
+ uploadFile: [],
+ editedShortcode: this.shortcode,
+ editedFile: this.file,
+ deleteModalVisible: false
+ }
+ },
+ computed: {
+ emojiPreview () {
+ if (this.newUpload && this.uploadFile.length > 0) {
+ return URL.createObjectURL(this.uploadFile[0])
+ } else if (!this.newUpload) {
+ return this.emojiAddr(this.file)
+ }
+
+ return null
+ },
+ isEdited () {
+ return !this.newUpload && (this.editedShortcode !== this.shortcode || this.editedFile !== this.file)
+ }
+ },
+ methods: {
+ saveEditedEmoji () {
+ if (!this.isEdited) return
+
+ this.$store.state.api.backendInteractor.updateEmojiFile(
+ { packName: this.packName, shortcode: this.shortcode, newShortcode: this.editedShortcode, newFilename: this.editedFile, force: false }
+ ).then(resp => {
+ if (resp.error !== undefined) {
+ this.$emit('displayError', resp.error)
+ return Promise.reject(resp.error)
+ }
+
+ return resp.json()
+ }).then(resp => this.$emit('updatePackFiles', resp))
+ },
+ uploadEmoji () {
+ this.$store.state.api.backendInteractor.addNewEmojiFile({
+ packName: this.packName,
+ file: this.uploadFile[0],
+ shortcode: this.editedShortcode,
+ filename: this.editedFile
+ }).then(resp => resp.json()).then(resp => {
+ if (resp.error !== undefined) {
+ this.$emit('displayError', resp.error)
+ return
+ }
+
+ this.$emit('updatePackFiles', resp)
+ this.$refs.emojiPopover.hidePopover()
+
+ this.editedFile = ''
+ this.editedShortcode = ''
+ this.uploadFile = []
+ })
+ },
+ revertEmoji () {
+ this.editedFile = this.file
+ this.editedShortcode = this.shortcode
+ },
+ deleteEmoji () {
+ this.deleteModalVisible = false
+
+ this.$store.state.api.backendInteractor.deleteEmojiFile(
+ { packName: this.packName, shortcode: this.shortcode }
+ ).then(resp => resp.json()).then(resp => {
+ if (resp.error !== undefined) {
+ this.$emit('displayError', resp.error)
+ return
+ }
+
+ this.$emit('updatePackFiles', resp)
+ })
+ }
+ }
+}
+</script>
+
+<style lang="scss">
+ .emoji-tab-edit-popover {
+ padding-left: 0.5em;
+ padding-right: 0.5em;
+ padding-bottom: 0.5em;
+
+ .emoji {
+ width: 32px;
+ height: 32px;
+ }
+ }
+</style>
diff --git a/src/components/settings_modal/helpers/float_setting.vue b/src/components/settings_modal/helpers/float_setting.vue
new file mode 100644
index 00000000..15edb3c3
--- /dev/null
+++ b/src/components/settings_modal/helpers/float_setting.vue
@@ -0,0 +1,16 @@
+<template>
+ <NumberSetting
+ v-bind="$attrs"
+ >
+ <slot />
+ </NumberSetting>
+</template>
+
+<script>
+import NumberSetting from './number_setting.vue'
+export default {
+ components: {
+ NumberSetting
+ }
+}
+</script>
diff --git a/src/components/settings_modal/helpers/group_setting.js b/src/components/settings_modal/helpers/group_setting.js
new file mode 100644
index 00000000..23a2a202
--- /dev/null
+++ b/src/components/settings_modal/helpers/group_setting.js
@@ -0,0 +1,13 @@
+import { isEqual } from 'lodash'
+
+import Setting from './setting.js'
+
+export default {
+ ...Setting,
+ computed: {
+ ...Setting.computed,
+ isDirty () {
+ return !isEqual(this.state, this.draft)
+ }
+ }
+}
diff --git a/src/components/settings_modal/helpers/group_setting.vue b/src/components/settings_modal/helpers/group_setting.vue
new file mode 100644
index 00000000..a4df4bf3
--- /dev/null
+++ b/src/components/settings_modal/helpers/group_setting.vue
@@ -0,0 +1,15 @@
+<template>
+ <span
+ v-if="matchesExpertLevel"
+ class="GroupSetting"
+ >
+ <ModifiedIndicator
+ :changed="isChanged"
+ :onclick="reset"
+ />
+ <ProfileSettingIndicator :is-profile="isProfileSetting" />
+ <DraftButtons />
+ </span>
+</template>
+
+<script src="./group_setting.js"></script>
diff --git a/src/components/settings_modal/helpers/integer_setting.js b/src/components/settings_modal/helpers/integer_setting.js
deleted file mode 100644
index e64d0cee..00000000
--- a/src/components/settings_modal/helpers/integer_setting.js
+++ /dev/null
@@ -1,44 +0,0 @@
-import { get, set } from 'lodash'
-import ModifiedIndicator from './modified_indicator.vue'
-export default {
- components: {
- ModifiedIndicator
- },
- props: {
- path: String,
- disabled: Boolean,
- min: Number,
- expert: [Number, String]
- },
- computed: {
- pathDefault () {
- const [firstSegment, ...rest] = this.path.split('.')
- return [firstSegment + 'DefaultValue', ...rest].join('.')
- },
- state () {
- const value = get(this.$parent, this.path)
- if (value === undefined) {
- return this.defaultState
- } else {
- return value
- }
- },
- defaultState () {
- return get(this.$parent, this.pathDefault)
- },
- isChanged () {
- return this.state !== this.defaultState
- },
- matchesExpertLevel () {
- return (this.expert || 0) <= this.$parent.expertLevel
- }
- },
- methods: {
- update (e) {
- set(this.$parent, this.path, parseInt(e.target.value))
- },
- reset () {
- set(this.$parent, this.path, this.defaultState)
- }
- }
-}
diff --git a/src/components/settings_modal/helpers/integer_setting.vue b/src/components/settings_modal/helpers/integer_setting.vue
index 695e2673..43fa7e1a 100644
--- a/src/components/settings_modal/helpers/integer_setting.vue
+++ b/src/components/settings_modal/helpers/integer_setting.vue
@@ -1,27 +1,17 @@
<template>
- <span
- v-if="matchesExpertLevel"
- class="IntegerSetting"
+ <NumberSetting
+ v-bind="$attrs"
+ truncate="1"
>
- <label :for="path">
- <slot />
- </label>
- <input
- :id="path"
- class="number-input"
- type="number"
- step="1"
- :disabled="disabled"
- :min="min || 0"
- :value="state"
- @change="update"
- >
- {{ ' ' }}
- <ModifiedIndicator
- :changed="isChanged"
- :onclick="reset"
- />
- </span>
+ <slot />
+ </NumberSetting>
</template>
-<script src="./integer_setting.js"></script>
+<script>
+import NumberSetting from './number_setting.vue'
+export default {
+ components: {
+ NumberSetting
+ }
+}
+</script>
diff --git a/src/components/settings_modal/helpers/modified_indicator.vue b/src/components/settings_modal/helpers/modified_indicator.vue
index 8311533a..a747cebd 100644
--- a/src/components/settings_modal/helpers/modified_indicator.vue
+++ b/src/components/settings_modal/helpers/modified_indicator.vue
@@ -5,17 +5,17 @@
>
<Popover
trigger="hover"
+ :trigger-attrs="{ 'aria-label': $t('settings.setting_changed') }"
>
<template #trigger>
&nbsp;
<FAIcon
icon="wrench"
- :aria-label="$t('settings.setting_changed')"
/>
</template>
<template #content>
<div class="modified-tooltip">
- {{ $t('settings.setting_changed') }}
+ {{ $t(messageKey) }}
</div>
</template>
</Popover>
@@ -33,7 +33,13 @@ library.add(
export default {
components: { Popover },
- props: ['changed']
+ props: {
+ changed: Boolean,
+ messageKey: {
+ type: String,
+ default: 'settings.setting_changed'
+ }
+ }
}
</script>
diff --git a/src/components/settings_modal/helpers/number_setting.js b/src/components/settings_modal/helpers/number_setting.js
new file mode 100644
index 00000000..676a0d22
--- /dev/null
+++ b/src/components/settings_modal/helpers/number_setting.js
@@ -0,0 +1,24 @@
+import Setting from './setting.js'
+
+export default {
+ ...Setting,
+ props: {
+ ...Setting.props,
+ truncate: {
+ type: Number,
+ required: false,
+ default: 1
+ }
+ },
+ methods: {
+ ...Setting.methods,
+ getValue (e) {
+ if (!this.truncate === 1) {
+ return parseInt(e.target.value)
+ } else if (this.truncate > 1) {
+ return Math.trunc(e.target.value / this.truncate) * this.truncate
+ }
+ return parseFloat(e.target.value)
+ }
+ }
+}
diff --git a/src/components/settings_modal/helpers/number_setting.vue b/src/components/settings_modal/helpers/number_setting.vue
new file mode 100644
index 00000000..32dc6f83
--- /dev/null
+++ b/src/components/settings_modal/helpers/number_setting.vue
@@ -0,0 +1,46 @@
+<template>
+ <span
+ v-if="matchesExpertLevel"
+ class="NumberSetting"
+ >
+ <label
+ :for="path"
+ :class="{ 'faint': shouldBeDisabled }"
+ >
+ <template v-if="backendDescriptionLabel">
+ {{ backendDescriptionLabel + ' ' }}
+ </template>
+ <template v-else-if="source === 'admin'">
+ MISSING LABEL FOR {{ path }}
+ </template>
+ <slot v-else />
+ </label>
+ {{ ' ' }}
+ <input
+ :id="path"
+ class="input number-input"
+ type="number"
+ :step="step || 1"
+ :disabled="shouldBeDisabled"
+ :min="min || 0"
+ :value="realDraftMode ? draft :state"
+ @change="update"
+ >
+ {{ ' ' }}
+ <ModifiedIndicator
+ :changed="isChanged"
+ :onclick="reset"
+ />
+ <ProfileSettingIndicator :is-profile="isProfileSetting" />
+ <DraftButtons />
+ <p
+ v-if="backendDescriptionDescription"
+ class="setting-description"
+ :class="{ 'faint': shouldBeDisabled }"
+ >
+ {{ backendDescriptionDescription + ' ' }}
+ </p>
+ </span>
+</template>
+
+<script src="./number_setting.js"></script>
diff --git a/src/components/settings_modal/helpers/server_side_indicator.vue b/src/components/settings_modal/helpers/profile_setting_indicator.vue
index bf181959..d160781b 100644
--- a/src/components/settings_modal/helpers/server_side_indicator.vue
+++ b/src/components/settings_modal/helpers/profile_setting_indicator.vue
@@ -1,7 +1,7 @@
<template>
<span
- v-if="serverSide"
- class="ServerSideIndicator"
+ v-if="isProfile"
+ class="ProfileSettingIndicator"
>
<Popover
trigger="hover"
@@ -14,7 +14,7 @@
/>
</template>
<template #content>
- <div class="serverside-tooltip">
+ <div class="profilesetting-tooltip">
{{ $t('settings.setting_server_side') }}
</div>
</template>
@@ -33,17 +33,17 @@ library.add(
export default {
components: { Popover },
- props: ['serverSide']
+ props: ['isProfile']
}
</script>
<style lang="scss">
-.ServerSideIndicator {
+.ProfileSettingIndicator {
display: inline-block;
position: relative;
}
-.serverside-tooltip {
+.profilesetting-tooltip {
margin: 0.5em 1em;
min-width: 10em;
text-align: center;
diff --git a/src/components/settings_modal/helpers/setting.js b/src/components/settings_modal/helpers/setting.js
new file mode 100644
index 00000000..3b3e6268
--- /dev/null
+++ b/src/components/settings_modal/helpers/setting.js
@@ -0,0 +1,246 @@
+import ModifiedIndicator from './modified_indicator.vue'
+import ProfileSettingIndicator from './profile_setting_indicator.vue'
+import DraftButtons from './draft_buttons.vue'
+import { get, set, cloneDeep } from 'lodash'
+
+export default {
+ components: {
+ ModifiedIndicator,
+ DraftButtons,
+ ProfileSettingIndicator
+ },
+ props: {
+ path: {
+ type: [String, Array],
+ required: true
+ },
+ disabled: {
+ type: Boolean,
+ default: false
+ },
+ parentPath: {
+ type: [String, Array]
+ },
+ parentInvert: {
+ type: Boolean,
+ default: false
+ },
+ expert: {
+ type: [Number, String],
+ default: 0
+ },
+ source: {
+ type: String,
+ default: undefined
+ },
+ hideDescription: {
+ type: Boolean
+ },
+ swapDescriptionAndLabel: {
+ type: Boolean
+ },
+ overrideBackendDescription: {
+ type: Boolean
+ },
+ overrideBackendDescriptionLabel: {
+ type: Boolean
+ },
+ draftMode: {
+ type: Boolean,
+ default: undefined
+ },
+ timedApplyMode: {
+ type: Boolean,
+ default: false
+ }
+ },
+ inject: {
+ defaultSource: {
+ default: 'default'
+ },
+ defaultDraftMode: {
+ default: false
+ }
+ },
+ data () {
+ return {
+ localDraft: null
+ }
+ },
+ created () {
+ if (this.realDraftMode && this.realSource !== 'admin') {
+ this.draft = this.state
+ }
+ },
+ computed: {
+ draft: {
+ // TODO allow passing shared draft object?
+ get () {
+ if (this.realSource === 'admin') {
+ return get(this.$store.state.adminSettings.draft, this.canonPath)
+ } else {
+ return this.localDraft
+ }
+ },
+ set (value) {
+ if (this.realSource === 'admin') {
+ this.$store.commit('updateAdminDraft', { path: this.canonPath, value })
+ } else {
+ this.localDraft = value
+ }
+ }
+ },
+ state () {
+ const value = get(this.configSource, this.canonPath)
+ if (value === undefined) {
+ return this.defaultState
+ } else {
+ return value
+ }
+ },
+ visibleState () {
+ return this.realDraftMode ? this.draft : this.state
+ },
+ realSource () {
+ return this.source || this.defaultSource
+ },
+ realDraftMode () {
+ return typeof this.draftMode === 'undefined' ? this.defaultDraftMode : this.draftMode
+ },
+ backendDescription () {
+ return get(this.$store.state.adminSettings.descriptions, this.path)
+ },
+ backendDescriptionLabel () {
+ if (this.realSource !== 'admin') return ''
+ if (!this.backendDescription || this.overrideBackendDescriptionLabel) {
+ return this.$t([
+ 'admin_dash',
+ 'temp_overrides',
+ ...this.canonPath.map(p => p.replace(/\./g, '_DOT_')),
+ 'label'
+ ].join('.'))
+ } else {
+ return this.swapDescriptionAndLabel
+ ? this.backendDescription?.description
+ : this.backendDescription?.label
+ }
+ },
+ backendDescriptionDescription () {
+ if (this.realSource !== 'admin') return ''
+ if (this.hideDescription) return null
+ if (!this.backendDescription || this.overrideBackendDescription) {
+ return this.$t([
+ 'admin_dash',
+ 'temp_overrides',
+ ...this.canonPath.map(p => p.replace(/\./g, '_DOT_')),
+ 'description'
+ ].join('.'))
+ } else {
+ return this.swapDescriptionAndLabel
+ ? this.backendDescription?.label
+ : this.backendDescription?.description
+ }
+ },
+ backendDescriptionSuggestions () {
+ return this.backendDescription?.suggestions
+ },
+ shouldBeDisabled () {
+ const parentValue = this.parentPath !== undefined ? get(this.configSource, this.parentPath) : null
+ return this.disabled || (parentValue !== null ? (this.parentInvert ? parentValue : !parentValue) : false)
+ },
+ configSource () {
+ switch (this.realSource) {
+ case 'profile':
+ return this.$store.state.profileConfig
+ case 'admin':
+ return this.$store.state.adminSettings.config
+ default:
+ return this.$store.getters.mergedConfig
+ }
+ },
+ configSink () {
+ switch (this.realSource) {
+ case 'profile':
+ return (k, v) => this.$store.dispatch('setProfileOption', { name: k, value: v })
+ case 'admin':
+ return (k, v) => this.$store.dispatch('pushAdminSetting', { path: k, value: v })
+ default:
+ if (this.timedApplyMode) {
+ return (k, v) => this.$store.dispatch('setOptionTemporarily', { name: k, value: v })
+ } else {
+ return (k, v) => this.$store.dispatch('setOption', { name: k, value: v })
+ }
+ }
+ },
+ defaultState () {
+ switch (this.realSource) {
+ case 'profile':
+ return {}
+ default:
+ return get(this.$store.getters.defaultConfig, this.path)
+ }
+ },
+ isProfileSetting () {
+ return this.realSource === 'profile'
+ },
+ isChanged () {
+ switch (this.realSource) {
+ case 'profile':
+ case 'admin':
+ return false
+ default:
+ return this.state !== this.defaultState
+ }
+ },
+ canonPath () {
+ return Array.isArray(this.path) ? this.path : this.path.split('.')
+ },
+ isDirty () {
+ if (this.realSource === 'admin' && this.canonPath.length > 3) {
+ return false // should not show draft buttons for "grouped" values
+ } else {
+ return this.realDraftMode && this.draft !== this.state
+ }
+ },
+ canHardReset () {
+ return this.realSource === 'admin' && this.$store.state.adminSettings.modifiedPaths &&
+ this.$store.state.adminSettings.modifiedPaths.has(this.canonPath.join(' -> '))
+ },
+ matchesExpertLevel () {
+ return (this.expert || 0) <= this.$store.state.config.expertLevel > 0
+ }
+ },
+ methods: {
+ getValue (e) {
+ return e.target.value
+ },
+ update (e) {
+ if (this.realDraftMode) {
+ this.draft = this.getValue(e)
+ } else {
+ this.configSink(this.path, this.getValue(e))
+ }
+ },
+ commitDraft () {
+ if (this.realDraftMode) {
+ this.configSink(this.path, this.draft)
+ }
+ },
+ reset () {
+ if (this.realDraftMode) {
+ this.draft = cloneDeep(this.state)
+ } else {
+ set(this.$store.getters.mergedConfig, this.path, cloneDeep(this.defaultState))
+ }
+ },
+ hardReset () {
+ switch (this.realSource) {
+ case 'admin':
+ return this.$store.dispatch('resetAdminSetting', { path: this.path })
+ .then(() => { this.draft = this.state })
+ default:
+ console.warn('Hard reset not implemented yet!')
+ }
+ }
+ }
+}
diff --git a/src/components/settings_modal/helpers/shared_computed_object.js b/src/components/settings_modal/helpers/shared_computed_object.js
index 12431dca..bb3d36ac 100644
--- a/src/components/settings_modal/helpers/shared_computed_object.js
+++ b/src/components/settings_modal/helpers/shared_computed_object.js
@@ -1,52 +1,18 @@
-import { defaultState as configDefaultState } from 'src/modules/config.js'
-import { defaultState as serverSideConfigDefaultState } from 'src/modules/serverSideConfig.js'
-
const SharedComputedObject = () => ({
user () {
return this.$store.state.users.currentUser
},
- // Getting values for default properties
- ...Object.keys(configDefaultState)
- .map(key => [
- key + 'DefaultValue',
- function () {
- return this.$store.getters.defaultConfig[key]
- }
- ])
- .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}),
- // Generating computed values for vuex properties
- ...Object.keys(configDefaultState)
- .map(key => [key, {
- get () { return this.$store.getters.mergedConfig[key] },
- set (value) {
- this.$store.dispatch('setOption', { name: key, value })
- }
- }])
- .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}),
- ...Object.keys(serverSideConfigDefaultState)
- .map(key => ['serverSide_' + key, {
- get () { return this.$store.state.serverSideConfig[key] },
- set (value) {
- this.$store.dispatch('setServerSideOption', { name: key, value })
- }
- }])
- .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}),
- // Special cases (need to transform values or perform actions first)
- useStreamingApi: {
- get () { return this.$store.getters.mergedConfig.useStreamingApi },
- set (value) {
- const promise = value
- ? this.$store.dispatch('enableMastoSockets')
- : this.$store.dispatch('disableMastoSockets')
-
- promise.then(() => {
- this.$store.dispatch('setOption', { name: 'useStreamingApi', value })
- }).catch((e) => {
- console.error('Failed starting MastoAPI Streaming socket', e)
- this.$store.dispatch('disableMastoSockets')
- this.$store.dispatch('setOption', { name: 'useStreamingApi', value: false })
- })
- }
+ expertLevel () {
+ return this.$store.getters.mergedConfig.expertLevel > 0
+ },
+ mergedConfig () {
+ return this.$store.getters.mergedConfig
+ },
+ adminConfig () {
+ return this.$store.state.adminSettings.config
+ },
+ adminDraft () {
+ return this.$store.state.adminSettings.draft
}
})
diff --git a/src/components/settings_modal/helpers/size_setting.js b/src/components/settings_modal/helpers/size_setting.js
deleted file mode 100644
index 58697412..00000000
--- a/src/components/settings_modal/helpers/size_setting.js
+++ /dev/null
@@ -1,67 +0,0 @@
-import { get, set } from 'lodash'
-import ModifiedIndicator from './modified_indicator.vue'
-import Select from 'src/components/select/select.vue'
-
-export const allCssUnits = ['cm', 'mm', 'in', 'px', 'pt', 'pc', 'em', 'ex', 'ch', 'rem', 'vw', 'vh', 'vmin', 'vmax', '%']
-export const defaultHorizontalUnits = ['px', 'rem', 'vw']
-export const defaultVerticalUnits = ['px', 'rem', 'vh']
-
-export default {
- components: {
- ModifiedIndicator,
- Select
- },
- props: {
- path: String,
- disabled: Boolean,
- min: Number,
- units: {
- type: [String],
- default: () => allCssUnits
- },
- expert: [Number, String]
- },
- computed: {
- pathDefault () {
- const [firstSegment, ...rest] = this.path.split('.')
- return [firstSegment + 'DefaultValue', ...rest].join('.')
- },
- stateUnit () {
- return (this.state || '').replace(/\d+/, '')
- },
- stateValue () {
- return (this.state || '').replace(/\D+/, '')
- },
- state () {
- const value = get(this.$parent, this.path)
- if (value === undefined) {
- return this.defaultState
- } else {
- return value
- }
- },
- defaultState () {
- return get(this.$parent, this.pathDefault)
- },
- isChanged () {
- return this.state !== this.defaultState
- },
- matchesExpertLevel () {
- return (this.expert || 0) <= this.$parent.expertLevel
- }
- },
- methods: {
- update (e) {
- set(this.$parent, this.path, e)
- },
- reset () {
- set(this.$parent, this.path, this.defaultState)
- },
- updateValue (e) {
- set(this.$parent, this.path, parseInt(e.target.value) + this.stateUnit)
- },
- updateUnit (e) {
- set(this.$parent, this.path, this.stateValue + e.target.value)
- }
- }
-}
diff --git a/src/components/settings_modal/helpers/string_setting.js b/src/components/settings_modal/helpers/string_setting.js
new file mode 100644
index 00000000..b368cfc8
--- /dev/null
+++ b/src/components/settings_modal/helpers/string_setting.js
@@ -0,0 +1,5 @@
+import Setting from './setting.js'
+
+export default {
+ ...Setting
+}
diff --git a/src/components/settings_modal/helpers/string_setting.vue b/src/components/settings_modal/helpers/string_setting.vue
new file mode 100644
index 00000000..7b30d1b9
--- /dev/null
+++ b/src/components/settings_modal/helpers/string_setting.vue
@@ -0,0 +1,42 @@
+<template>
+ <label
+ v-if="matchesExpertLevel"
+ class="StringSetting"
+ >
+ <label
+ :for="path"
+ :class="{ 'faint': shouldBeDisabled }"
+ >
+ <template v-if="backendDescriptionLabel">
+ {{ backendDescriptionLabel + ' ' }}
+ </template>
+ <template v-else-if="source === 'admin'">
+ MISSING LABEL FOR {{ path }}
+ </template>
+ <slot v-else />
+ </label>
+ <input
+ :id="path"
+ class="input string-input"
+ :disabled="shouldBeDisabled"
+ :value="realDraftMode ? draft : state"
+ @change="update"
+ >
+ {{ ' ' }}
+ <ModifiedIndicator
+ :changed="isChanged"
+ :onclick="reset"
+ />
+ <ProfileSettingIndicator :is-profile="isProfileSetting" />
+ <DraftButtons />
+ <p
+ v-if="backendDescriptionDescription"
+ class="setting-description"
+ :class="{ 'faint': shouldBeDisabled }"
+ >
+ {{ backendDescriptionDescription + ' ' }}
+ </p>
+ </label>
+</template>
+
+<script src="./string_setting.js"></script>
diff --git a/src/components/settings_modal/helpers/unit_setting.js b/src/components/settings_modal/helpers/unit_setting.js
new file mode 100644
index 00000000..daeddd81
--- /dev/null
+++ b/src/components/settings_modal/helpers/unit_setting.js
@@ -0,0 +1,64 @@
+import Select from 'src/components/select/select.vue'
+import Setting from './setting.js'
+
+export const allCssUnits = ['cm', 'mm', 'in', 'px', 'pt', 'pc', 'em', 'ex', 'ch', 'rem', 'vw', 'vh', 'vmin', 'vmax', '%']
+export const defaultHorizontalUnits = ['px', 'rem', 'vw']
+export const defaultVerticalUnits = ['px', 'rem', 'vh']
+
+export default {
+ ...Setting,
+ components: {
+ ...Setting.components,
+ Select
+ },
+ props: {
+ ...Setting.props,
+ min: Number,
+ units: {
+ type: Array,
+ default: () => allCssUnits
+ },
+ unitSet: {
+ type: String,
+ default: 'none'
+ },
+ step: {
+ type: Number,
+ default: 1
+ },
+ resetDefault: {
+ type: Object,
+ default: null
+ }
+ },
+ computed: {
+ ...Setting.computed,
+ stateUnit () {
+ return typeof this.state === 'string' ? this.state.replace(/[0-9,.]+/, '') : ''
+ },
+ stateValue () {
+ return typeof this.state === 'string' ? this.state.replace(/[^0-9,.]+/, '') : ''
+ }
+ },
+ methods: {
+ ...Setting.methods,
+ getUnitString (value) {
+ if (this.unitSet === 'none') return value
+ return this.$t(['settings', 'units', this.unitSet, value].join('.'))
+ },
+ updateValue (e) {
+ this.configSink(this.path, parseFloat(e.target.value) + this.stateUnit)
+ },
+ updateUnit (e) {
+ let value = this.stateValue
+ const newUnit = e.target.value
+ if (this.resetDefault) {
+ const replaceValue = this.resetDefault[newUnit]
+ if (replaceValue != null) {
+ value = replaceValue
+ }
+ }
+ this.configSink(this.path, value + newUnit)
+ }
+ }
+}
diff --git a/src/components/settings_modal/helpers/size_setting.vue b/src/components/settings_modal/helpers/unit_setting.vue
index 5a78f100..40ab6880 100644
--- a/src/components/settings_modal/helpers/size_setting.vue
+++ b/src/components/settings_modal/helpers/unit_setting.vue
@@ -1,7 +1,7 @@
<template>
<span
v-if="matchesExpertLevel"
- class="SizeSetting"
+ class="UnitSetting"
>
<label
:for="path"
@@ -9,11 +9,12 @@
>
<slot />
</label>
+ {{ ' ' }}
<input
:id="path"
- class="number-input"
+ class="input number-input"
type="number"
- step="1"
+ :step="step"
:disabled="disabled"
:min="min || 0"
:value="stateValue"
@@ -23,7 +24,7 @@
:id="path"
:model-value="stateUnit"
:disabled="disabled"
- class="css-unit-input"
+ class="unit-input unstyled"
@change="updateUnit"
>
<option
@@ -31,7 +32,7 @@
:key="option"
:value="option"
>
- {{ option }}
+ {{ getUnitString(option) }}
</option>
</Select>
{{ ' ' }}
@@ -42,14 +43,20 @@
</span>
</template>
-<script src="./size_setting.js"></script>
+<script src="./unit_setting.js"></script>
<style lang="scss">
-.css-unit-input,
-.css-unit-input select {
- margin-left: 0.5em;
- width: 4em;
- max-width: 4em;
- min-width: 4em;
+.UnitSetting {
+ .number-input {
+ max-width: 6.5em;
+ text-align: right;
+ }
+
+ .unit-input,
+ .unit-input select {
+ min-width: 4em;
+ width: auto;
+ }
}
+
</style>
diff --git a/src/components/settings_modal/settings_modal.js b/src/components/settings_modal/settings_modal.js
index 0a72dca1..63c9b24a 100644
--- a/src/components/settings_modal/settings_modal.js
+++ b/src/components/settings_modal/settings_modal.js
@@ -4,8 +4,9 @@ import AsyncComponentError from 'src/components/async_component_error/async_comp
import getResettableAsyncComponent from 'src/services/resettable_async_component.js'
import Popover from '../popover/popover.vue'
import Checkbox from 'src/components/checkbox/checkbox.vue'
+import ConfirmModal from 'src/components/confirm_modal/confirm_modal.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
-import { cloneDeep } from 'lodash'
+import { cloneDeep, isEqual } from 'lodash'
import {
newImporter,
newExporter
@@ -53,8 +54,17 @@ const SettingsModal = {
Modal,
Popover,
Checkbox,
- SettingsModalContent: getResettableAsyncComponent(
- () => import('./settings_modal_content.vue'),
+ ConfirmModal,
+ SettingsModalUserContent: getResettableAsyncComponent(
+ () => import('./settings_modal_user_content.vue'),
+ {
+ loadingComponent: PanelLoading,
+ errorComponent: AsyncComponentError,
+ delay: 0
+ }
+ ),
+ SettingsModalAdminContent: getResettableAsyncComponent(
+ () => import('./settings_modal_admin_content.vue'),
{
loadingComponent: PanelLoading,
errorComponent: AsyncComponentError,
@@ -147,17 +157,30 @@ const SettingsModal = {
PLEROMAFE_SETTINGS_MINOR_VERSION
]
return clone
+ },
+ resetAdminDraft () {
+ this.$store.commit('resetAdminDraft')
+ },
+ pushAdminDraft () {
+ this.$store.dispatch('pushAdminDraft')
}
},
computed: {
currentSaveStateNotice () {
+ console.log(this.$store.state.interface.settings.currentSaveStateNotice)
return this.$store.state.interface.settings.currentSaveStateNotice
},
modalActivated () {
return this.$store.state.interface.settingsModalState !== 'hidden'
},
- modalOpenedOnce () {
- return this.$store.state.interface.settingsModalLoaded
+ modalMode () {
+ return this.$store.state.interface.settingsModalMode
+ },
+ modalOpenedOnceUser () {
+ return this.$store.state.interface.settingsModalLoadedUser
+ },
+ modalOpenedOnceAdmin () {
+ return this.$store.state.interface.settingsModalLoadedAdmin
},
modalPeeked () {
return this.$store.state.interface.settingsModalState === 'minimized'
@@ -167,9 +190,14 @@ const SettingsModal = {
return this.$store.state.config.expertLevel > 0
},
set (value) {
- console.log(value)
this.$store.dispatch('setOption', { name: 'expertLevel', value: value ? 1 : 0 })
}
+ },
+ adminDraftAny () {
+ return !isEqual(
+ this.$store.state.adminSettings.config,
+ this.$store.state.adminSettings.draft
+ )
}
}
}
diff --git a/src/components/settings_modal/settings_modal.scss b/src/components/settings_modal/settings_modal.scss
index f5861229..d01553db 100644
--- a/src/components/settings_modal/settings_modal.scss
+++ b/src/components/settings_modal/settings_modal.scss
@@ -1,8 +1,10 @@
-@import "src/variables";
-
.settings-modal {
overflow: hidden;
+ h4 {
+ margin-bottom: 0.5em;
+ }
+
.setting-list,
.option-list {
list-style-type: none;
@@ -15,6 +17,20 @@
.suboptions {
margin-top: 0.3em;
}
+
+ &.two-column {
+ column-count: 2;
+
+ > li {
+ break-inside: avoid;
+ }
+ }
+ }
+
+ .setting-description {
+ margin-top: 0.2em;
+ margin-bottom: 2em;
+ font-size: 70%;
}
.settings-modal-panel {
@@ -37,7 +53,9 @@
.btn {
min-height: 2em;
- min-width: 10em;
+ }
+
+ .btn:not(.dropdown-button) {
padding: 0 2em;
}
}
@@ -45,6 +63,8 @@
.settings-footer {
display: flex;
+ flex-wrap: wrap;
+ line-height: 2;
>* {
margin-right: 0.5em;
diff --git a/src/components/settings_modal/settings_modal.vue b/src/components/settings_modal/settings_modal.vue
index 7b457371..90dbbde0 100644
--- a/src/components/settings_modal/settings_modal.vue
+++ b/src/components/settings_modal/settings_modal.vue
@@ -8,13 +8,13 @@
<div class="settings-modal-panel panel">
<div class="panel-heading">
<span class="title">
- {{ $t('settings.settings') }}
+ {{ modalMode === 'user' ? $t('settings.settings') : $t('admin_dash.window_title') }}
</span>
<transition name="fade">
<div
v-if="currentSaveStateNotice"
class="alert"
- :class="{ transparent: !currentSaveStateNotice.error, error: currentSaveStateNotice.error}"
+ :class="{ success: !currentSaveStateNotice.error, error: currentSaveStateNotice.error}"
@click.prevent
>
{{ currentSaveStateNotice.error ? $t('settings.saving_err') : $t('settings.saving_ok') }}
@@ -42,10 +42,12 @@
</button>
</div>
<div class="panel-body">
- <SettingsModalContent v-if="modalOpenedOnce" />
+ <SettingsModalUserContent v-if="modalMode === 'user' && modalOpenedOnceUser" />
+ <SettingsModalAdminContent v-if="modalMode === 'admin' && modalOpenedOnceAdmin" />
</div>
- <div class="panel-footer settings-footer">
+ <div class="panel-footer settings-footer -flexible-height">
<Popover
+ v-if="modalMode === 'user'"
class="export"
trigger="click"
placement="top"
@@ -68,7 +70,7 @@
<template #content="{close}">
<div class="dropdown-menu">
<button
- class="button-default dropdown-item dropdown-item-icon"
+ class="menu-item dropdown-item dropdown-item-icon"
@click.prevent="backup"
@click="close"
>
@@ -78,7 +80,7 @@
/><span>{{ $t("settings.file_export_import.backup_settings") }}</span>
</button>
<button
- class="button-default dropdown-item dropdown-item-icon"
+ class="menu-item dropdown-item dropdown-item-icon"
@click.prevent="backupWithTheme"
@click="close"
>
@@ -88,7 +90,7 @@
/><span>{{ $t("settings.file_export_import.backup_settings_theme") }}</span>
</button>
<button
- class="button-default dropdown-item dropdown-item-icon"
+ class="menu-item dropdown-item dropdown-item-icon"
@click.prevent="restore"
@click="close"
>
@@ -107,12 +109,56 @@
>
{{ $t("settings.expert_mode") }}
</Checkbox>
+ <span v-if="modalMode === 'admin'">
+ <i18n-t keypath="admin_dash.wip_notice">
+ <template #adminFeLink>
+ <a
+ href="/pleroma/admin/#/login-pleroma"
+ target="_blank"
+ >
+ {{ $t("admin_dash.old_ui_link") }}
+ </a>
+ </template>
+ </i18n-t>
+ </span>
<span
id="unscrolled-content"
class="extra-content"
/>
+ <span
+ v-if="modalMode === 'admin'"
+ class="admin-buttons"
+ >
+ <button
+ class="button-default btn"
+ :disabled="!adminDraftAny"
+ @click="resetAdminDraft"
+ >
+ {{ $t("admin_dash.reset_all") }}
+ </button>
+ {{ ' ' }}
+ <button
+ class="button-default btn"
+ :disabled="!adminDraftAny"
+ @click="pushAdminDraft"
+ >
+ {{ $t("admin_dash.commit_all") }}
+ </button>
+ </span>
</div>
</div>
+ <teleport to="#modal">
+ <ConfirmModal
+ v-if="$store.state.interface.temporaryChangesTimeoutId"
+ :title="$t('settings.confirm_new_setting')"
+ :cancel-text="$t('settings.revert')"
+ :confirm-text="$t('settings.confirm')"
+ @cancelled="$store.state.interface.temporaryChangesRevert"
+ @accepted="$store.state.interface.temporaryChangesConfirm"
+ >
+ {{ $t('settings.confirm_new_question') }}
+ </ConfirmModal>
+ </teleport>
</Modal>
</template>
diff --git a/src/components/settings_modal/settings_modal_admin_content.js b/src/components/settings_modal/settings_modal_admin_content.js
new file mode 100644
index 00000000..ce835bf2
--- /dev/null
+++ b/src/components/settings_modal/settings_modal_admin_content.js
@@ -0,0 +1,95 @@
+import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'
+
+import InstanceTab from './admin_tabs/instance_tab.vue'
+import LimitsTab from './admin_tabs/limits_tab.vue'
+import FrontendsTab from './admin_tabs/frontends_tab.vue'
+import EmojiTab from './admin_tabs/emoji_tab.vue'
+
+import { library } from '@fortawesome/fontawesome-svg-core'
+import {
+ faWrench,
+ faHand,
+ faLaptopCode,
+ faPaintBrush,
+ faBell,
+ faDownload,
+ faEyeSlash,
+ faInfo
+} from '@fortawesome/free-solid-svg-icons'
+
+library.add(
+ faWrench,
+ faHand,
+ faLaptopCode,
+ faPaintBrush,
+ faBell,
+ faDownload,
+ faEyeSlash,
+ faInfo
+)
+
+const SettingsModalAdminContent = {
+ components: {
+ TabSwitcher,
+
+ InstanceTab,
+ LimitsTab,
+ FrontendsTab,
+ EmojiTab
+ },
+ computed: {
+ user () {
+ return this.$store.state.users.currentUser
+ },
+ isLoggedIn () {
+ return !!this.$store.state.users.currentUser
+ },
+ open () {
+ return this.$store.state.interface.settingsModalState !== 'hidden'
+ },
+ bodyLock () {
+ return this.$store.state.interface.settingsModalState === 'visible'
+ },
+ adminDbLoaded () {
+ return this.$store.state.adminSettings.loaded
+ },
+ adminDescriptionsLoaded () {
+ return this.$store.state.adminSettings.descriptions !== null
+ },
+ noDb () {
+ return this.$store.state.adminSettings.dbConfigEnabled === false
+ }
+ },
+ created () {
+ if (this.user.rights.admin) {
+ this.$store.dispatch('loadAdminStuff')
+ }
+ },
+ methods: {
+ onOpen () {
+ const targetTab = this.$store.state.interface.settingsModalTargetTab
+ // We're being told to open in specific tab
+ if (targetTab) {
+ const tabIndex = this.$refs.tabSwitcher.$slots.default().findIndex(elm => {
+ return elm.props && elm.props['data-tab-name'] === targetTab
+ })
+ if (tabIndex >= 0) {
+ this.$refs.tabSwitcher.setTab(tabIndex)
+ }
+ }
+ // Clear the state of target tab, so that next time settings is opened
+ // it doesn't force it.
+ this.$store.dispatch('clearSettingsModalTargetTab')
+ }
+ },
+ mounted () {
+ this.onOpen()
+ },
+ watch: {
+ open: function (value) {
+ if (value) this.onOpen()
+ }
+ }
+}
+
+export default SettingsModalAdminContent
diff --git a/src/components/settings_modal/settings_modal_content.scss b/src/components/settings_modal/settings_modal_admin_content.scss
index 87df7982..a5314fe1 100644
--- a/src/components/settings_modal/settings_modal_content.scss
+++ b/src/components/settings_modal/settings_modal_admin_content.scss
@@ -1,10 +1,8 @@
-@import "src/variables";
-
.settings_tab-switcher {
height: 100%;
.setting-item {
- border-bottom: 2px solid var(--fg, $fallback--fg);
+ border-bottom: 2px solid var(--border);
margin: 1em 1em 1.4em;
padding-bottom: 1.4em;
@@ -33,10 +31,6 @@
margin-bottom: 1em;
}
- select {
- min-width: 10em;
- }
-
textarea {
width: 100%;
max-width: 100%;
@@ -45,12 +39,7 @@
.unavailable,
.unavailable svg {
- color: var(--cRed, $fallback--cRed);
- color: $fallback--cRed;
- }
-
- .number-input {
- max-width: 6em;
+ color: var(--cRed);
}
}
}
diff --git a/src/components/settings_modal/settings_modal_admin_content.vue b/src/components/settings_modal/settings_modal_admin_content.vue
new file mode 100644
index 00000000..65e23b7e
--- /dev/null
+++ b/src/components/settings_modal/settings_modal_admin_content.vue
@@ -0,0 +1,76 @@
+<template>
+ <tab-switcher
+ v-if="adminDescriptionsLoaded && (noDb || adminDbLoaded)"
+ ref="tabSwitcher"
+ class="settings_tab-switcher"
+ :side-tab-bar="true"
+ :scrollable-tabs="true"
+ :render-only-focused="true"
+ :body-scroll-lock="bodyLock"
+ >
+ <div
+ v-if="noDb"
+ :label="$t('admin_dash.tabs.nodb')"
+ icon="exclamation-triangle"
+ data-tab-name="nodb-notice"
+ >
+ <div :label="$t('admin_dash.tabs.nodb')">
+ <div class="setting-item">
+ <h2>{{ $t('admin_dash.nodb.heading') }}</h2>
+ <i18n-t keypath="admin_dash.nodb.text">
+ <template #documentation>
+ <a
+ href="https://docs-develop.pleroma.social/backend/configuration/howto_database_config/"
+ target="_blank"
+ >
+ {{ $t("admin_dash.nodb.documentation") }}
+ </a>
+ </template>
+ <template #property>
+ <code>config :pleroma, configurable_from_database</code>
+ </template>
+ <template #value>
+ <code>true</code>
+ </template>
+ </i18n-t>
+ <p>{{ $t('admin_dash.nodb.text2') }}</p>
+ </div>
+ </div>
+ </div>
+ <div
+ v-if="adminDbLoaded"
+ :label="$t('admin_dash.tabs.instance')"
+ icon="wrench"
+ data-tab-name="general"
+ >
+ <InstanceTab />
+ </div>
+ <div
+ v-if="adminDbLoaded"
+ :label="$t('admin_dash.tabs.limits')"
+ icon="hand"
+ data-tab-name="limits"
+ >
+ <LimitsTab />
+ </div>
+ <div
+ :label="$t('admin_dash.tabs.frontends')"
+ icon="laptop-code"
+ data-tab-name="frontends"
+ >
+ <FrontendsTab />
+ </div>
+
+ <div
+ :label="$t('admin_dash.tabs.emoji')"
+ icon="face-smile-beam"
+ data-tab-name="emoji"
+ >
+ <EmojiTab />
+ </div>
+ </tab-switcher>
+</template>
+
+<script src="./settings_modal_admin_content.js"></script>
+
+<style src="./settings_modal_admin_content.scss" lang="scss"></style>
diff --git a/src/components/settings_modal/settings_modal_content.js b/src/components/settings_modal/settings_modal_user_content.js
index 9ac0301f..ebd5329f 100644
--- a/src/components/settings_modal/settings_modal_content.js
+++ b/src/components/settings_modal/settings_modal_user_content.js
@@ -7,6 +7,7 @@ import FilteringTab from './tabs/filtering_tab.vue'
import SecurityTab from './tabs/security_tab/security_tab.vue'
import ProfileTab from './tabs/profile_tab.vue'
import GeneralTab from './tabs/general_tab.vue'
+import AppearanceTab from './tabs/appearance_tab.vue'
import VersionTab from './tabs/version_tab.vue'
import ThemeTab from './tabs/theme_tab/theme_tab.vue'
@@ -19,7 +20,8 @@ import {
faBell,
faDownload,
faEyeSlash,
- faInfo
+ faInfo,
+ faWindowRestore
} from '@fortawesome/free-solid-svg-icons'
library.add(
@@ -30,7 +32,8 @@ library.add(
faBell,
faDownload,
faEyeSlash,
- faInfo
+ faInfo,
+ faWindowRestore
)
const SettingsModalContent = {
@@ -44,6 +47,7 @@ const SettingsModalContent = {
SecurityTab,
ProfileTab,
GeneralTab,
+ AppearanceTab,
VersionTab,
ThemeTab
},
diff --git a/src/components/settings_modal/settings_modal_user_content.scss b/src/components/settings_modal/settings_modal_user_content.scss
new file mode 100644
index 00000000..a5314fe1
--- /dev/null
+++ b/src/components/settings_modal/settings_modal_user_content.scss
@@ -0,0 +1,45 @@
+.settings_tab-switcher {
+ height: 100%;
+
+ .setting-item {
+ border-bottom: 2px solid var(--border);
+ margin: 1em 1em 1.4em;
+ padding-bottom: 1.4em;
+
+ > div,
+ > label {
+ display: block;
+ margin-bottom: 0.5em;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+ }
+
+ .select-multiple {
+ display: flex;
+
+ .option-list {
+ margin: 0;
+ padding-left: 0.5em;
+ }
+ }
+
+ &:last-child {
+ border-bottom: none;
+ padding-bottom: 0;
+ margin-bottom: 1em;
+ }
+
+ textarea {
+ width: 100%;
+ max-width: 100%;
+ height: 100px;
+ }
+
+ .unavailable,
+ .unavailable svg {
+ color: var(--cRed);
+ }
+ }
+}
diff --git a/src/components/settings_modal/settings_modal_content.vue b/src/components/settings_modal/settings_modal_user_content.vue
index 0be76d22..1441d892 100644
--- a/src/components/settings_modal/settings_modal_content.vue
+++ b/src/components/settings_modal/settings_modal_user_content.vue
@@ -14,6 +14,20 @@
<GeneralTab />
</div>
<div
+ :label="$t('settings.appearance')"
+ icon="window-restore"
+ data-tab-name="appearance"
+ >
+ <AppearanceTab />
+ </div>
+ <div
+ :label="$t('settings.theme')"
+ icon="paint-brush"
+ data-tab-name="theme"
+ >
+ <ThemeTab />
+ </div>
+ <div
v-if="isLoggedIn"
:label="$t('settings.profile_tab')"
icon="user"
@@ -23,6 +37,14 @@
</div>
<div
v-if="isLoggedIn"
+ :label="$t('settings.notifications')"
+ icon="bell"
+ data-tab-name="notifications"
+ >
+ <NotificationsTab />
+ </div>
+ <div
+ v-if="isLoggedIn"
:label="$t('settings.security_tab')"
icon="lock"
data-tab-name="security"
@@ -37,19 +59,13 @@
<FilteringTab />
</div>
<div
- :label="$t('settings.theme')"
- icon="paint-brush"
- data-tab-name="theme"
- >
- <ThemeTab />
- </div>
- <div
v-if="isLoggedIn"
- :label="$t('settings.notifications')"
- icon="bell"
- data-tab-name="notifications"
+ :label="$t('settings.mutes_and_blocks')"
+ :fullHeight="true"
+ icon="eye-slash"
+ data-tab-name="mutesAndBlocks"
>
- <NotificationsTab />
+ <MutesAndBlocksTab />
</div>
<div
v-if="isLoggedIn"
@@ -60,15 +76,6 @@
<DataImportExportTab />
</div>
<div
- v-if="isLoggedIn"
- :label="$t('settings.mutes_and_blocks')"
- :fullHeight="true"
- icon="eye-slash"
- data-tab-name="mutesAndBlocks"
- >
- <MutesAndBlocksTab />
- </div>
- <div
:label="$t('settings.version.title')"
icon="info"
data-tab-name="version"
@@ -78,6 +85,6 @@
</tab-switcher>
</template>
-<script src="./settings_modal_content.js"></script>
+<script src="./settings_modal_user_content.js"></script>
-<style src="./settings_modal_content.scss" lang="scss"></style>
+<style src="./settings_modal_user_content.scss" lang="scss"></style>
diff --git a/src/components/settings_modal/tabs/appearance_tab.js b/src/components/settings_modal/tabs/appearance_tab.js
new file mode 100644
index 00000000..b5fd6c4c
--- /dev/null
+++ b/src/components/settings_modal/tabs/appearance_tab.js
@@ -0,0 +1,195 @@
+import BooleanSetting from '../helpers/boolean_setting.vue'
+import ChoiceSetting from '../helpers/choice_setting.vue'
+import IntegerSetting from '../helpers/integer_setting.vue'
+import FloatSetting from '../helpers/float_setting.vue'
+import UnitSetting, { defaultHorizontalUnits } from '../helpers/unit_setting.vue'
+
+import FontControl from 'src/components/font_control/font_control.vue'
+
+import { normalizeThemeData } from 'src/modules/interface'
+
+import {
+ getThemes
+} from 'src/services/style_setter/style_setter.js'
+import { convertTheme2To3 } from 'src/services/theme_data/theme2_to_theme3.js'
+import { init } from 'src/services/theme_data/theme_data_3.service.js'
+import {
+ getCssRules,
+ getScopedVersion
+} from 'src/services/theme_data/css_utils.js'
+
+import SharedComputedObject from '../helpers/shared_computed_object.js'
+import ProfileSettingIndicator from '../helpers/profile_setting_indicator.vue'
+import { library } from '@fortawesome/fontawesome-svg-core'
+import {
+ faGlobe
+} from '@fortawesome/free-solid-svg-icons'
+
+import Preview from './theme_tab/theme_preview.vue'
+
+library.add(
+ faGlobe
+)
+
+const AppearanceTab = {
+ data () {
+ return {
+ availableStyles: [],
+ 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) => ({
+ key: mode,
+ value: i - 1,
+ label: this.$t(`settings.style.themes3.hacks.forced_roundness_mode_${mode}`)
+ })),
+ underlayOverrideModes: ['none', 'opaque', 'transparent'].map((mode, i) => ({
+ key: mode,
+ value: mode,
+ label: this.$t(`settings.style.themes3.hacks.underlay_override_mode_${mode}`)
+ }))
+ }
+ },
+ components: {
+ BooleanSetting,
+ ChoiceSetting,
+ IntegerSetting,
+ FloatSetting,
+ UnitSetting,
+ ProfileSettingIndicator,
+ FontControl,
+ Preview
+ },
+ mounted () {
+ getThemes()
+ .then((promises) => {
+ return Promise.all(
+ Object.entries(promises)
+ .map(([k, v]) => v.then(res => [k, res]))
+ )
+ })
+ .then(themes => themes.reduce((acc, [k, v]) => {
+ if (v) {
+ return [
+ ...acc,
+ {
+ name: v.name || v[0],
+ key: k,
+ data: v
+ }
+ ]
+ } else {
+ return acc
+ }
+ }, []))
+ .then((themesComplete) => {
+ this.availableStyles = themesComplete
+ })
+
+ if (window.IntersectionObserver) {
+ this.intersectionObserver = new IntersectionObserver((entries, observer) => {
+ entries.forEach(({ target, isIntersecting }) => {
+ if (!isIntersecting) return
+ const theme = this.availableStyles.find(x => x.key === target.dataset.themeKey)
+ this.$nextTick(() => {
+ if (theme) theme.ready = true
+ })
+ observer.unobserve(target)
+ })
+ }, {
+ root: this.$refs.themeList
+ })
+ }
+ },
+ updated () {
+ this.$nextTick(() => {
+ this.$refs.themeList.querySelectorAll('.theme-preview').forEach(node => {
+ this.intersectionObserver.observe(node)
+ })
+ })
+ },
+ computed: {
+ noIntersectionObserver () {
+ 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]
+ }
+ },
+ instanceSpecificPanelPresent () { return this.$store.state.instance.showInstanceSpecificPanel },
+ instanceWallpaperUsed () {
+ return this.$store.state.instance.background &&
+ !this.$store.state.users.currentUser.background_image
+ },
+ instanceShoutboxPresent () { return this.$store.state.instance.shoutAvailable },
+ language: {
+ get: function () { return this.$store.getters.mergedConfig.interfaceLanguage },
+ set: function (val) {
+ this.$store.dispatch('setOption', { name: 'interfaceLanguage', value: val })
+ }
+ },
+ isCustomThemeUsed () {
+ const { theme } = this.mergedConfig
+ return theme === 'custom' || theme === null
+ },
+ ...SharedComputedObject()
+ },
+ methods: {
+ updateFont (key, value) {
+ console.log(key, value)
+ this.$store.dispatch('setOption', {
+ name: 'theme3hacks',
+ value: {
+ ...this.mergedConfig.theme3hacks,
+ fonts: {
+ ...this.mergedConfig.theme3hacks.fonts,
+ [key]: value
+ }
+ }
+ })
+ },
+ isThemeActive (key) {
+ const { theme } = this.mergedConfig
+ return key === theme
+ },
+ setTheme (name) {
+ this.$store.dispatch('setTheme', { themeName: name, saveData: true, recompile: true })
+ },
+ previewTheme (key, input) {
+ const style = normalizeThemeData(input)
+ const x = 2
+ if (x === 1) return
+ const theme2 = convertTheme2To3(style)
+ const theme3 = init({
+ inputRuleset: theme2,
+ ultimateBackgroundColor: '#000000',
+ liteMode: true,
+ debug: true,
+ onlyNormalState: true
+ })
+
+ return getScopedVersion(
+ getCssRules(theme3.eager),
+ '#theme-preview-' + key
+ ).join('\n')
+ }
+ }
+}
+
+export default AppearanceTab
diff --git a/src/components/settings_modal/tabs/appearance_tab.vue b/src/components/settings_modal/tabs/appearance_tab.vue
new file mode 100644
index 00000000..de6eb8e7
--- /dev/null
+++ b/src/components/settings_modal/tabs/appearance_tab.vue
@@ -0,0 +1,313 @@
+<template>
+ <div class="appearance-tab" :label="$t('settings.general')">
+ <div class="setting-item">
+ <h2>{{ $t('settings.theme') }}</h2>
+ <ul
+ class="theme-list"
+ ref="themeList"
+ >
+ <button
+ v-if="isCustomThemeUsed"
+ disabled
+ class="button-default theme-preview"
+ >
+ <preview />
+ <h4 class="theme-name">{{ $t('settings.style.custom_theme_used') }}</h4>
+ </button>
+ <button
+ v-for="style in availableStyles"
+ :data-theme-key="style.key"
+ :key="style.key"
+ class="button-default theme-preview"
+ :class="{ toggled: isThemeActive(style.key) }"
+ @click="setTheme(style.key)"
+ >
+ <!-- eslint-disable vue/no-v-text-v-html-on-component -->
+ <component
+ :is="'style'"
+ v-if="style.ready || noIntersectionObserver"
+ v-html="previewTheme(style.key, style.data)"
+ />
+ <!-- eslint-enable vue/no-v-text-v-html-on-component -->
+ <preview :class="{ placeholder: ready }" :id="'theme-preview-' + style.key"/>
+ <h4 class="theme-name">{{ style.name }}</h4>
+ </button>
+ </ul>
+ </div>
+ <div class="alert neutral theme-notice">
+ {{ $t("settings.style.appearance_tab_note") }}
+ </div>
+ <div class="setting-item">
+ <h2>{{ $t('settings.scale_and_layout') }}</h2>
+ <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>
+ <h3>{{ $t('settings.style.interface_font_user_override') }}</h3>
+ <ul class="setting-list">
+ <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:modelValue="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:modelValue="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:modelValue="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:modelValue="v => updateFont('monospace', v)"
+ />
+ </li>
+ </ul>
+ </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.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>
+ </ul>
+ </div>
+ <div class="setting-item">
+ <h2>{{ $t('settings.visual_tweaks') }}</h2>
+ <ul class="setting-list">
+ <li>
+ <ChoiceSetting
+ id="forcedRoundness"
+ path="forcedRoundness"
+ :options="forcedRoundnessOptions"
+ >
+ {{ $t('settings.style.themes3.hacks.force_interface_roundness') }}
+ </ChoiceSetting>
+ </li>
+ <li>
+ <ChoiceSetting
+ id="underlayOverride"
+ path="theme3hacks.underlay"
+ :options="underlayOverrideModes"
+ >
+ {{ $t('settings.style.themes3.hacks.underlay_overrides') }}
+ </ChoiceSetting>
+ </li>
+ <li v-if="instanceWallpaperUsed">
+ <BooleanSetting path="hideInstanceWallpaper">
+ {{ $t('settings.hide_wallpaper') }}
+ </BooleanSetting>
+ </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>
+ </div>
+ </div>
+</template>
+
+<script src="./appearance_tab.js"></script>
+
+<style lang="scss">
+.appearance-tab {
+ .theme-notice {
+ padding: 0.5em;
+ margin: 1em;
+ }
+
+ .column-settings {
+ display: flex;
+ justify-content: space-evenly;
+ flex-wrap: wrap;
+ }
+
+ .column-settings .size-label {
+ display: block;
+ margin-bottom: 0.5em;
+ margin-top: 0.5em;
+ }
+
+ .theme-list {
+ list-style: none;
+ display: flex;
+ flex-wrap: wrap;
+ margin: -0.5em 0;
+ height: 25em;
+ overflow-x: hidden;
+ overflow-y: auto;
+ scrollbar-gutter: stable;
+ border-radius: var(--roundness);
+ border: 1px solid var(--border);
+ padding: 0;
+
+ .theme-preview {
+ font-size: 1rem; // fix for firefox
+ width: 19rem;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ margin: 0.5em;
+
+ &.placeholder {
+ opacity: 0.2;
+ }
+
+ .theme-preview-container {
+ pointer-events: none;
+ zoom: 0.5;
+ border: none;
+ border-radius: var(--roundness);
+ text-align: left;
+ }
+ }
+ }
+}
+</style>
diff --git a/src/components/settings_modal/tabs/filtering_tab.js b/src/components/settings_modal/tabs/filtering_tab.js
index 7c37f0bc..fbace15d 100644
--- a/src/components/settings_modal/tabs/filtering_tab.js
+++ b/src/components/settings_modal/tabs/filtering_tab.js
@@ -1,6 +1,7 @@
import { filter, trim, debounce } from 'lodash'
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 SharedComputedObject from '../helpers/shared_computed_object.js'
@@ -19,6 +20,7 @@ const FilteringTab = {
components: {
BooleanSetting,
ChoiceSetting,
+ UnitSetting,
IntegerSetting
},
computed: {
diff --git a/src/components/settings_modal/tabs/filtering_tab.vue b/src/components/settings_modal/tabs/filtering_tab.vue
index 97046ff0..c86810d5 100644
--- a/src/components/settings_modal/tabs/filtering_tab.vue
+++ b/src/components/settings_modal/tabs/filtering_tab.vue
@@ -7,13 +7,11 @@
<BooleanSetting path="hideFilteredStatuses">
{{ $t('settings.hide_filtered_statuses') }}
</BooleanSetting>
- <ul
- class="setting-list suboptions"
- :class="[{disabled: !streaming}]"
- >
+ <ul class="setting-list suboptions">
<li>
<BooleanSetting
- :disabled="hideFilteredStatuses"
+ parent-path="hideFilteredStatuses"
+ :parent-invert="true"
path="hideWordFilteredPosts"
>
{{ $t('settings.hide_wordfiltered_statuses') }}
@@ -22,7 +20,8 @@
<li>
<BooleanSetting
v-if="user"
- :disabled="hideFilteredStatuses"
+ parent-path="hideFilteredStatuses"
+ :parent-invert="true"
path="hideMutedThreads"
>
{{ $t('settings.hide_muted_threads') }}
@@ -31,7 +30,8 @@
<li>
<BooleanSetting
v-if="user"
- :disabled="hideFilteredStatuses"
+ parent-path="hideFilteredStatuses"
+ :parent-invert="true"
path="hideMutedPosts"
>
{{ $t('settings.hide_muted_posts') }}
@@ -45,13 +45,18 @@
</BooleanSetting>
</li>
<li>
+ <BooleanSetting path="muteSensitiveStatuses">
+ {{ $t('settings.mute_sensitive_posts') }}
+ </BooleanSetting>
+ </li>
+ <li>
<BooleanSetting path="hidePostStats">
{{ $t('settings.hide_post_stats') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="hideBotIndication">
- {{ $t('settings.hide_bot_indication') }}
+ {{ $t('settings.hide_actor_type_indication') }}
</BooleanSetting>
</li>
<ChoiceSetting
@@ -67,7 +72,7 @@
<textarea
id="muteWords"
v-model="muteWordsString"
- class="resize-height"
+ class="input resize-height"
/>
<div>{{ $t('settings.filtering_explanation') }}</div>
</li>
@@ -91,6 +96,22 @@
{{ $t('settings.hide_attachments_in_convo') }}
</BooleanSetting>
</li>
+ <li>
+ <BooleanSetting path="hideScrobbles">
+ {{ $t('settings.hide_scrobbles') }}
+ </BooleanSetting>
+ </li>
+ <li>
+ <UnitSetting
+ key="hideScrobblesAfter"
+ path="hideScrobblesAfter"
+ :units="['m', 'h', 'd']"
+ unitSet="time"
+ expert="1"
+ >
+ {{ $t('settings.hide_scrobbles_after') }}
+ </UnitSetting>
+ </li>
</ul>
</div>
<div
diff --git a/src/components/settings_modal/tabs/general_tab.js b/src/components/settings_modal/tabs/general_tab.js
index ea24d6ad..e9e8fa72 100644
--- a/src/components/settings_modal/tabs/general_tab.js
+++ b/src/components/settings_modal/tabs/general_tab.js
@@ -2,11 +2,12 @@ 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 SizeSetting, { defaultHorizontalUnits } from '../helpers/size_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 SharedComputedObject from '../helpers/shared_computed_object.js'
-import ServerSideIndicator from '../helpers/server_side_indicator.vue'
+import ProfileSettingIndicator from '../helpers/profile_setting_indicator.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faGlobe
@@ -39,11 +40,6 @@ const GeneralTab = {
value: mode,
label: this.$t(`settings.mention_link_display_${mode}`)
})),
- thirdColumnModeOptions: ['none', 'notifications', 'postform'].map(mode => ({
- key: mode,
- value: mode,
- label: this.$t(`settings.third_column_mode_${mode}`)
- })),
userPopoverAvatarActionOptions: ['close', 'zoom', 'open'].map(mode => ({
key: mode,
value: mode,
@@ -62,15 +58,13 @@ const GeneralTab = {
BooleanSetting,
ChoiceSetting,
IntegerSetting,
- SizeSetting,
+ FloatSetting,
+ UnitSetting,
InterfaceLanguageSwitcher,
ScopeSelector,
- ServerSideIndicator
+ ProfileSettingIndicator
},
computed: {
- horizontalUnits () {
- return defaultHorizontalUnits
- },
postFormats () {
return this.$store.state.instance.postFormats || []
},
@@ -81,23 +75,6 @@ const GeneralTab = {
label: this.$t(`post_status.content_type["${format}"]`)
}))
},
- 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]
- }
- },
- instanceSpecificPanelPresent () { return this.$store.state.instance.showInstanceSpecificPanel },
- instanceWallpaperUsed () {
- return this.$store.state.instance.background &&
- !this.$store.state.users.currentUser.background_image
- },
- instanceShoutboxPresent () { return this.$store.state.instance.shoutAvailable },
language: {
get: function () { return this.$store.getters.mergedConfig.interfaceLanguage },
set: function (val) {
@@ -108,7 +85,7 @@ const GeneralTab = {
},
methods: {
changeDefaultScope (value) {
- this.$store.dispatch('setServerSideOption', { name: 'defaultScope', value })
+ this.$store.dispatch('setProfileOption', { name: 'defaultScope', value })
}
}
}
diff --git a/src/components/settings_modal/tabs/general_tab.vue b/src/components/settings_modal/tabs/general_tab.vue
index 703e94a0..82d5da89 100644
--- a/src/components/settings_modal/tabs/general_tab.vue
+++ b/src/components/settings_modal/tabs/general_tab.vue
@@ -15,11 +15,6 @@
{{ $t('settings.hide_isp') }}
</BooleanSetting>
</li>
- <li v-if="instanceWallpaperUsed">
- <BooleanSetting path="hideInstanceWallpaper">
- {{ $t('settings.hide_wallpaper') }}
- </BooleanSetting>
- </li>
<li>
<BooleanSetting path="stopGifs">
{{ $t('settings.stop_gifs') }}
@@ -29,14 +24,11 @@
<BooleanSetting path="streaming">
{{ $t('settings.streaming') }}
</BooleanSetting>
- <ul
- class="setting-list suboptions"
- :class="[{disabled: !streaming}]"
- >
+ <ul class="setting-list suboptions">
<li>
<BooleanSetting
path="pauseOnUnfocused"
- :disabled="!streaming"
+ parent-path="streaming"
>
{{ $t('settings.pause_on_unfocused') }}
</BooleanSetting>
@@ -101,53 +93,6 @@
{{ $t('settings.hide_shoutbox') }}
</BooleanSetting>
</li>
- <li>
- <h3>{{ $t('settings.columns') }}</h3>
- </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="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">
- <SizeSetting
- v-for="column in columns"
- :key="column"
- :path="column + 'ColumnWidth'"
- :units="horizontalUnits"
- expert="1"
- >
- {{ $t('settings.column_sizes_' + column) }}
- </SizeSetting>
- </div>
- </li>
<li class="select-multiple">
<span class="label">{{ $t('settings.confirm_dialogs') }}</span>
<ul class="option-list">
@@ -213,7 +158,7 @@
</ChoiceSetting>
</li>
<ul
- v-if="conversationDisplay !== 'linear'"
+ v-if="mergedConfig.conversationDisplay !== 'linear'"
class="setting-list suboptions"
>
<li>
@@ -265,12 +210,36 @@
<li>
<BooleanSetting
v-if="user"
- path="serverSide_stripRichContent"
+ 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
+ class="setting-list suboptions"
+ v-if="mergedConfig.useAbsoluteTimeFormat"
+ >
+ <li>
+ <UnitSetting
+ path="absoluteTimeFormatMinAge"
+ unit-set="time"
+ :units="['s', 'm', 'h', 'd']"
+ :min="0"
+ >
+ {{ $t('settings.absolute_time_format_min_age') }}
+ </UnitSetting>
+ </li>
+ </ul>
<h3>{{ $t('settings.attachments') }}</h3>
<li>
<BooleanSetting
@@ -290,7 +259,7 @@
<BooleanSetting
path="preloadImage"
expert="1"
- :disabled="!hideNsfw"
+ parent-path="hideNsfw"
>
{{ $t('settings.preload_images') }}
</BooleanSetting>
@@ -299,7 +268,7 @@
<BooleanSetting
path="useOneClickNsfw"
expert="1"
- :disabled="!hideNsfw"
+ parent-path="hideNsfw"
>
{{ $t('settings.use_one_click_nsfw') }}
</BooleanSetting>
@@ -312,15 +281,13 @@
>
{{ $t('settings.loop_video') }}
</BooleanSetting>
- <ul
- class="setting-list suboptions"
- :class="[{disabled: !streaming}]"
- >
+ <ul class="setting-list suboptions">
<li>
<BooleanSetting
path="loopVideoSilentOnly"
expert="1"
- :disabled="!loopVideo || !loopSilentAvailable"
+ parent-path="loopVideo"
+ :disabled="!loopSilentAvailable"
>
{{ $t('settings.loop_video_silent_only') }}
</BooleanSetting>
@@ -418,18 +385,18 @@
<ul class="setting-list">
<li>
<label for="default-vis">
- {{ $t('settings.default_vis') }} <ServerSideIndicator :server-side="true" />
+ {{ $t('settings.default_vis') }} <ProfileSettingIndicator :is-profile="true" />
<ScopeSelector
class="scope-selector"
:show-all="true"
- :user-default="serverSide_defaultScope"
- :initial-scope="serverSide_defaultScope"
+ :user-default="$store.state.profileConfig.defaultScope"
+ :initial-scope="$store.state.profileConfig.defaultScope"
:on-scope-change="changeDefaultScope"
/>
</label>
</li>
<li>
- <!-- <BooleanSetting path="serverSide_defaultNSFW"> -->
+ <!-- <BooleanSetting source="profile" path="defaultNSFW"> -->
<BooleanSetting path="sensitiveByDefault">
{{ $t('settings.sensitive_by_default') }}
</BooleanSetting>
@@ -501,23 +468,17 @@
{{ $t('settings.pad_emoji') }}
</BooleanSetting>
</li>
+ <li>
+ <BooleanSetting
+ path="autocompleteSelect"
+ expert="1"
+ >
+ {{ $t('settings.autocomplete_select_first') }}
+ </BooleanSetting>
+ </li>
</ul>
</div>
</div>
</template>
<script src="./general_tab.js"></script>
-
-<style lang="scss">
-.column-settings {
- display: flex;
- justify-content: space-evenly;
- flex-wrap: wrap;
-}
-
-.column-settings .size-label {
- display: block;
- margin-bottom: 0.5em;
- margin-top: 0.5em;
-}
-</style>
diff --git a/src/components/settings_modal/tabs/mutes_and_blocks_tab.js b/src/components/settings_modal/tabs/mutes_and_blocks_tab.js
index 6cfeea35..51974f9f 100644
--- a/src/components/settings_modal/tabs/mutes_and_blocks_tab.js
+++ b/src/components/settings_modal/tabs/mutes_and_blocks_tab.js
@@ -9,17 +9,20 @@ import DomainMuteCard from 'src/components/domain_mute_card/domain_mute_card.vue
import SelectableList from 'src/components/selectable_list/selectable_list.vue'
import ProgressButton from 'src/components/progress_button/progress_button.vue'
import withSubscription from 'src/components/../hocs/with_subscription/with_subscription'
+import withLoadMore from 'src/components/../hocs/with_load_more/with_load_more'
import Checkbox from 'src/components/checkbox/checkbox.vue'
-const BlockList = withSubscription({
+const BlockList = withLoadMore({
fetch: (props, $store) => $store.dispatch('fetchBlocks'),
select: (props, $store) => get($store.state.users.currentUser, 'blockIds', []),
+ destroy: () => {},
childPropName: 'items'
})(SelectableList)
-const MuteList = withSubscription({
+const MuteList = withLoadMore({
fetch: (props, $store) => $store.dispatch('fetchMutes'),
select: (props, $store) => get($store.state.users.currentUser, 'muteIds', []),
+ destroy: () => {},
childPropName: 'items'
})(SelectableList)
diff --git a/src/components/settings_modal/tabs/notifications_tab.js b/src/components/settings_modal/tabs/notifications_tab.js
index 3c6ab87f..c53b5889 100644
--- a/src/components/settings_modal/tabs/notifications_tab.js
+++ b/src/components/settings_modal/tabs/notifications_tab.js
@@ -16,6 +16,10 @@ const NotificationsTab = {
user () {
return this.$store.state.users.currentUser
},
+ canReceiveReports () {
+ if (!this.user) { return false }
+ return this.user.privileges.includes('reports_manage_reports')
+ },
...SharedComputedObject()
},
methods: {
diff --git a/src/components/settings_modal/tabs/notifications_tab.vue b/src/components/settings_modal/tabs/notifications_tab.vue
index dd3806ed..10228888 100644
--- a/src/components/settings_modal/tabs/notifications_tab.vue
+++ b/src/components/settings_modal/tabs/notifications_tab.vue
@@ -1,49 +1,239 @@
<template>
<div :label="$t('settings.notifications')">
<div class="setting-item">
+ <h2>{{ $t('settings.notification_setting_annoyance') }}</h2>
+ <ul class="setting-list">
+ <li>
+ <BooleanSetting path="closingDrawerMarksAsSeen">
+ {{ $t('settings.notification_setting_drawer_marks_as_seen') }}
+ </BooleanSetting>
+ </li>
+ <li>
+ <BooleanSetting path="ignoreInactionableSeen">
+ {{ $t('settings.notification_setting_ignore_inactionable_seen') }}
+ </BooleanSetting>
+ <div>
+ <small>
+ {{ $t('settings.notification_setting_ignore_inactionable_seen_tip') }}
+ </small>
+ </div>
+ </li>
+ <li>
+ <BooleanSetting
+ path="unseenAtTop"
+ expert="1"
+ >
+ {{ $t('settings.notification_setting_unseen_at_top') }}
+ </BooleanSetting>
+ </li>
+ </ul>
+ </div>
+ <div class="setting-item">
<h2>{{ $t('settings.notification_setting_filters') }}</h2>
<ul class="setting-list">
<li>
- <BooleanSetting path="serverSide_blockNotificationsFromStrangers">
+ <BooleanSetting
+ source="profile"
+ path="blockNotificationsFromStrangers"
+ >
{{ $t('settings.notification_setting_block_from_strangers') }}
</BooleanSetting>
</li>
- <li class="select-multiple">
- <span class="label">{{ $t('settings.notification_visibility') }}</span>
- <ul class="option-list">
+ <li>
+ <h3> {{ $t('settings.notification_visibility') }}</h3>
+ <p v-if="expertLevel > 0">
+ {{ $t('settings.notification_setting_filters_chrome_push') }}
+ </p>
+ <ul class="setting-list two-column">
<li>
- <BooleanSetting path="notificationVisibility.likes">
- {{ $t('settings.notification_visibility_likes') }}
- </BooleanSetting>
+ <h4> {{ $t('settings.notification_visibility_mentions') }}</h4>
+ <ul class="setting-list">
+ <li>
+ <BooleanSetting path="notificationVisibility.mentions">
+ {{ $t('settings.notification_visibility_in_column') }}
+ </BooleanSetting>
+ </li>
+ <li>
+ <BooleanSetting path="notificationNative.mentions">
+ {{ $t('settings.notification_visibility_native_notifications') }}
+ </BooleanSetting>
+ </li>
+ </ul>
</li>
<li>
- <BooleanSetting path="notificationVisibility.repeats">
- {{ $t('settings.notification_visibility_repeats') }}
- </BooleanSetting>
+ <h4> {{ $t('settings.notification_visibility_statuses') }}</h4>
+ <ul class="setting-list">
+ <li>
+ <BooleanSetting path="notificationVisibility.statuses">
+ {{ $t('settings.notification_visibility_in_column') }}
+ </BooleanSetting>
+ </li>
+ <li>
+ <BooleanSetting path="notificationNative.statuses">
+ {{ $t('settings.notification_visibility_native_notifications') }}
+ </BooleanSetting>
+ </li>
+ </ul>
</li>
<li>
- <BooleanSetting path="notificationVisibility.follows">
- {{ $t('settings.notification_visibility_follows') }}
- </BooleanSetting>
+ <h4> {{ $t('settings.notification_visibility_likes') }}</h4>
+ <ul class="setting-list">
+ <li>
+ <BooleanSetting path="notificationVisibility.likes">
+ {{ $t('settings.notification_visibility_in_column') }}
+ </BooleanSetting>
+ </li>
+ <li>
+ <BooleanSetting path="notificationNative.likes">
+ {{ $t('settings.notification_visibility_native_notifications') }}
+ </BooleanSetting>
+ </li>
+ </ul>
</li>
<li>
- <BooleanSetting path="notificationVisibility.mentions">
- {{ $t('settings.notification_visibility_mentions') }}
+ <h4> {{ $t('settings.notification_visibility_repeats') }}</h4>
+ <ul class="setting-list">
+ <li>
+ <BooleanSetting path="notificationVisibility.repeats">
+ {{ $t('settings.notification_visibility_in_column') }}
+ </BooleanSetting>
+ </li>
+ <li>
+ <BooleanSetting path="notificationNative.repeats">
+ {{ $t('settings.notification_visibility_native_notifications') }}
+ </BooleanSetting>
+ </li>
+ </ul>
+ </li>
+ <li>
+ <h4> {{ $t('settings.notification_visibility_emoji_reactions') }}</h4>
+ <ul class="setting-list">
+ <li>
+ <BooleanSetting path="notificationVisibility.emojiReactions">
+ {{ $t('settings.notification_visibility_in_column') }}
+ </BooleanSetting>
+ </li>
+ <li>
+ <BooleanSetting path="notificationNative.emojiReactions">
+ {{ $t('settings.notification_visibility_native_notifications') }}
+ </BooleanSetting>
+ </li>
+ </ul>
+ </li>
+ <li>
+ <h4> {{ $t('settings.notification_visibility_follows') }}</h4>
+ <ul class="setting-list">
+ <li>
+ <BooleanSetting path="notificationVisibility.follows">
+ {{ $t('settings.notification_visibility_in_column') }}
+ </BooleanSetting>
+ </li>
+ <li>
+ <BooleanSetting path="notificationNative.follows">
+ {{ $t('settings.notification_visibility_native_notifications') }}
+ </BooleanSetting>
+ </li>
+ </ul>
+ </li>
+ <li>
+ <h4> {{ $t('settings.notification_visibility_follow_requests') }}</h4>
+ <ul class="setting-list">
+ <li>
+ <BooleanSetting path="notificationVisibility.followRequest">
+ {{ $t('settings.notification_visibility_in_column') }}
+ </BooleanSetting>
+ </li>
+ <li>
+ <BooleanSetting path="notificationNative.followRequest">
+ {{ $t('settings.notification_visibility_native_notifications') }}
+ </BooleanSetting>
+ </li>
+ </ul>
+ </li>
+ <li>
+ <h4> {{ $t('settings.notification_visibility_moves') }}</h4>
+ <ul class="setting-list">
+ <li>
+ <BooleanSetting path="notificationVisibility.moves">
+ {{ $t('settings.notification_visibility_in_column') }}
+ </BooleanSetting>
+ </li>
+ <li>
+ <BooleanSetting path="notificationNative.moves">
+ {{ $t('settings.notification_visibility_native_notifications') }}
+ </BooleanSetting>
+ </li>
+ </ul>
+ </li>
+ <li>
+ <h4> {{ $t('settings.notification_visibility_polls') }}</h4>
+ <ul class="setting-list">
+ <li>
+ <BooleanSetting path="notificationVisibility.polls">
+ {{ $t('settings.notification_visibility_in_column') }}
+ </BooleanSetting>
+ </li>
+ <li>
+ <BooleanSetting path="notificationNative.polls">
+ {{ $t('settings.notification_visibility_native_notifications') }}
+ </BooleanSetting>
+ </li>
+ </ul>
+ </li>
+ <li v-if="canReceiveReports">
+ <h4> {{ $t('settings.notification_visibility_reports') }}</h4>
+ <ul class="setting-list">
+ <li>
+ <BooleanSetting path="notificationVisibility.reports">
+ {{ $t('settings.notification_visibility_in_column') }}
+ </BooleanSetting>
+ </li>
+ <li>
+ <BooleanSetting path="notificationNative.reports">
+ {{ $t('settings.notification_visibility_native_notifications') }}
+ </BooleanSetting>
+ </li>
+ </ul>
+ </li>
+ </ul>
+ </li>
+ <li>
+ <BooleanSetting path="showExtraNotifications">
+ {{ $t('settings.notification_show_extra') }}
+ </BooleanSetting>
+ </li>
+ <li>
+ <ul class="setting-list suboptions">
+ <li>
+ <BooleanSetting
+ path="showChatsInExtraNotifications"
+ :disabled="!mergedConfig.showExtraNotifications"
+ >
+ {{ $t('settings.notification_extra_chats') }}
</BooleanSetting>
</li>
<li>
- <BooleanSetting path="notificationVisibility.moves">
- {{ $t('settings.notification_visibility_moves') }}
+ <BooleanSetting
+ path="showAnnouncementsInExtraNotifications"
+ :disabled="!mergedConfig.showExtraNotifications"
+ >
+ {{ $t('settings.notification_extra_announcements') }}
</BooleanSetting>
</li>
<li>
- <BooleanSetting path="notificationVisibility.emojiReactions">
- {{ $t('settings.notification_visibility_emoji_reactions') }}
+ <BooleanSetting
+ path="showFollowRequestsInExtraNotifications"
+ :disabled="!mergedConfig.showExtraNotifications"
+ >
+ {{ $t('settings.notification_extra_follow_requests') }}
</BooleanSetting>
</li>
<li>
- <BooleanSetting path="notificationVisibility.polls">
- {{ $t('settings.notification_visibility_polls') }}
+ <BooleanSetting
+ path="showExtraNotificationsTip"
+ :disabled="!mergedConfig.showExtraNotifications"
+ >
+ {{ $t('settings.notification_extra_tip') }}
</BooleanSetting>
</li>
</ul>
@@ -64,10 +254,26 @@
>
{{ $t('settings.enable_web_push_notifications') }}
</BooleanSetting>
+ <ul class="setting-list suboptions">
+ <li>
+ <BooleanSetting
+ path="webPushAlwaysShowNotifications"
+ :disabled="!mergedConfig.webPushNotifications"
+ >
+ {{ $t('settings.enable_web_push_always_show') }}
+ </BooleanSetting>
+ <div :class="{ faint: !mergedConfig.webPushNotifications }">
+ <small>
+ {{ $t('settings.enable_web_push_always_show_tip') }}
+ </small>
+ </div>
+ </li>
+ </ul>
</li>
<li>
<BooleanSetting
- path="serverSide_webPushHideContents"
+ source="profile"
+ path="webPushHideContents"
expert="1"
>
{{ $t('settings.notification_setting_hide_notification_contents') }}
diff --git a/src/components/settings_modal/tabs/profile_tab.js b/src/components/settings_modal/tabs/profile_tab.js
index eeacad48..dee17450 100644
--- a/src/components/settings_modal/tabs/profile_tab.js
+++ b/src/components/settings_modal/tabs/profile_tab.js
@@ -9,6 +9,7 @@ import suggestor from 'src/components/emoji_input/suggestor.js'
import Autosuggest from 'src/components/autosuggest/autosuggest.vue'
import Checkbox from 'src/components/checkbox/checkbox.vue'
import InterfaceLanguageSwitcher from 'src/components/interface_language_switcher/interface_language_switcher.vue'
+import Select from 'src/components/select/select.vue'
import BooleanSetting from '../helpers/boolean_setting.vue'
import SharedComputedObject from '../helpers/shared_computed_object.js'
import localeService from 'src/services/locale/locale.service.js'
@@ -39,6 +40,7 @@ const ProfileTab = {
showRole: this.$store.state.users.currentUser.show_role,
role: this.$store.state.users.currentUser.role,
bot: this.$store.state.users.currentUser.bot,
+ actorType: this.$store.state.users.currentUser.actor_type,
pickAvatarBtnVisible: true,
bannerUploading: false,
backgroundUploading: false,
@@ -57,7 +59,8 @@ const ProfileTab = {
ProgressButton,
Checkbox,
BooleanSetting,
- InterfaceLanguageSwitcher
+ InterfaceLanguageSwitcher,
+ Select
},
computed: {
user () {
@@ -116,6 +119,12 @@ const ProfileTab = {
bannerImgSrc () {
const src = this.$store.state.users.currentUser.cover_photo
return (!src) ? this.defaultBanner : src
+ },
+ groupActorAvailable () {
+ return this.$store.state.instance.groupActorAvailable
+ },
+ availableActorTypes () {
+ return this.groupActorAvailable ? ['Person', 'Service', 'Group'] : ['Person', 'Service']
}
},
methods: {
@@ -127,7 +136,7 @@ const ProfileTab = {
/* eslint-disable camelcase */
display_name: this.newName,
fields_attributes: this.newFields.filter(el => el != null),
- bot: this.bot,
+ actor_type: this.actorType,
show_role: this.showRole,
birthday: this.newBirthday || '',
show_birthday: this.showBirthday
diff --git a/src/components/settings_modal/tabs/profile_tab.scss b/src/components/settings_modal/tabs/profile_tab.scss
index ee253ffe..7eda943b 100644
--- a/src/components/settings_modal/tabs/profile_tab.scss
+++ b/src/components/settings_modal/tabs/profile_tab.scss
@@ -1,5 +1,3 @@
-@import "../../../variables";
-
.profile-tab {
.bio {
margin: 0;
@@ -43,16 +41,14 @@
display: block;
width: 100%;
height: 100%;
- border-radius: $fallback--avatarRadius;
- border-radius: var(--avatarRadius, $fallback--avatarRadius);
+ border-radius: var(--roundness);
}
.reset-button {
position: absolute;
top: 0.2em;
right: 0.2em;
- border-radius: $fallback--tooltipRadius;
- border-radius: var(--tooltipRadius, $fallback--tooltipRadius);
+ border-radius: var(--roundness);
background-color: rgb(0 0 0 / 60%);
opacity: 0.7;
width: 1.5em;
diff --git a/src/components/settings_modal/tabs/profile_tab.vue b/src/components/settings_modal/tabs/profile_tab.vue
index 6a5b478a..034034a1 100644
--- a/src/components/settings_modal/tabs/profile_tab.vue
+++ b/src/components/settings_modal/tabs/profile_tab.vue
@@ -12,7 +12,7 @@
<input
id="username"
v-model="newName"
- class="name-changer"
+ class="input name-changer"
v-bind="propsToNative(inputProps)"
>
</template>
@@ -26,7 +26,7 @@
<template #default="inputProps">
<textarea
v-model="newBio"
- class="bio resize-height"
+ class="input bio resize-height"
v-bind="propsToNative(inputProps)"
/>
</template>
@@ -47,7 +47,7 @@
id="birthday"
v-model="newBirthday"
type="date"
- class="birthday-input"
+ class="input birthday-input"
>
<Checkbox v-model="showBirthday">
{{ $t('settings.birthday.show_birthday') }}
@@ -71,6 +71,7 @@
v-model="newFields[i].name"
:placeholder="$t('settings.profile_fields.name')"
v-bind="propsToNative(inputProps)"
+ class="input"
>
</template>
</EmojiInput>
@@ -85,6 +86,7 @@
v-model="newFields[i].value"
:placeholder="$t('settings.profile_fields.value')"
v-bind="propsToNative(inputProps)"
+ class="input"
>
</template>
</EmojiInput>
@@ -109,10 +111,24 @@
</button>
</div>
<p>
- <Checkbox v-model="bot">
- {{ $t('settings.bot') }}
- </Checkbox>
+ <label>
+ {{ $t('settings.actor_type') }}
+ <Select v-model="actorType">
+ <option
+ v-for="option in availableActorTypes"
+ :key="option"
+ :value="option"
+ >
+ {{ $t('settings.actor_type_' + option) }}
+ </option>
+ </Select>
+ </label>
</p>
+ <div v-if="groupActorAvailable">
+ <small>
+ {{ $t('settings.actor_type_description') }}
+ </small>
+ </div>
<p>
<interface-language-switcher
:prompt-text="$t('settings.email_language')"
@@ -191,6 +207,7 @@
<div>
<input
type="file"
+ class="input"
@change="uploadFile('banner', $event)"
>
</div>
@@ -233,6 +250,7 @@
<div>
<input
type="file"
+ class="input"
@change="uploadFile('background', $event)"
>
</div>
@@ -254,37 +272,50 @@
<h2>{{ $t('settings.account_privacy') }}</h2>
<ul class="setting-list">
<li>
- <BooleanSetting path="serverSide_locked">
+ <BooleanSetting
+ source="profile"
+ path="locked"
+ >
{{ $t('settings.lock_account_description') }}
</BooleanSetting>
</li>
<li>
- <BooleanSetting path="serverSide_discoverable">
+ <BooleanSetting
+ source="profile"
+ path="discoverable"
+ >
{{ $t('settings.discoverable') }}
</BooleanSetting>
</li>
<li>
- <BooleanSetting path="serverSide_allowFollowingMove">
+ <BooleanSetting
+ source="profile"
+ path="allowFollowingMove"
+ >
{{ $t('settings.allow_following_move') }}
</BooleanSetting>
</li>
<li>
- <BooleanSetting path="serverSide_hideFavorites">
+ <BooleanSetting
+ source="profile"
+ path="hideFavorites"
+ >
{{ $t('settings.hide_favorites_description') }}
</BooleanSetting>
</li>
<li>
- <BooleanSetting path="serverSide_hideFollowers">
+ <BooleanSetting
+ source="profile"
+ path="hideFollowers"
+ >
{{ $t('settings.hide_followers_description') }}
</BooleanSetting>
- <ul
- class="setting-list suboptions"
- :class="[{disabled: !serverSide_hideFollowers}]"
- >
+ <ul class="setting-list suboptions">
<li>
<BooleanSetting
- path="serverSide_hideFollowersCount"
- :disabled="!serverSide_hideFollowers"
+ source="profile"
+ path="hideFollowersCount"
+ parent-path="hideFollowers"
>
{{ $t('settings.hide_followers_count_description') }}
</BooleanSetting>
@@ -292,17 +323,18 @@
</ul>
</li>
<li>
- <BooleanSetting path="serverSide_hideFollows">
+ <BooleanSetting
+ source="profile"
+ path="hideFollows"
+ >
{{ $t('settings.hide_follows_description') }}
</BooleanSetting>
- <ul
- class="setting-list suboptions"
- :class="[{disabled: !serverSide_hideFollows}]"
- >
+ <ul class="setting-list suboptions">
<li>
<BooleanSetting
- path="serverSide_hideFollowsCount"
- :disabled="!serverSide_hideFollows"
+ source="profile"
+ path="hideFollowsCount"
+ parent-path="hideFollows"
>
{{ $t('settings.hide_follows_count_description') }}
</BooleanSetting>
diff --git a/src/components/settings_modal/tabs/security_tab/mfa.vue b/src/components/settings_modal/tabs/security_tab/mfa.vue
index ee5b6b13..9421b16e 100644
--- a/src/components/settings_modal/tabs/security_tab/mfa.vue
+++ b/src/components/settings_modal/tabs/security_tab/mfa.vue
@@ -99,12 +99,14 @@
<input
v-model="otpConfirmToken"
type="text"
+ class="input"
>
<p>{{ $t('settings.enter_current_password_to_confirm') }}:</p>
<input
v-model="currentPassword"
type="password"
+ class="input"
>
<div class="confirm-otp-actions">
<button
@@ -137,8 +139,6 @@
<script src="./mfa.js"></script>
<style lang="scss">
-@import "../../../../variables";
-
.mfa-settings {
.mfa-heading,
.method-item {
@@ -149,8 +149,7 @@
}
.warning {
- color: $fallback--cOrange;
- color: var(--cOrange, $fallback--cOrange);
+ color: var(--cOrange);
}
.setup-otp {
diff --git a/src/components/settings_modal/tabs/security_tab/mfa_backup_codes.vue b/src/components/settings_modal/tabs/security_tab/mfa_backup_codes.vue
index 923161b2..32a8a759 100644
--- a/src/components/settings_modal/tabs/security_tab/mfa_backup_codes.vue
+++ b/src/components/settings_modal/tabs/security_tab/mfa_backup_codes.vue
@@ -21,16 +21,13 @@
</template>
<script src="./mfa_backup_codes.js"></script>
<style lang="scss">
-@import "../../../../variables";
-
.mfa-backup-codes {
.warning {
- color: $fallback--cOrange;
- color: var(--cOrange, $fallback--cOrange);
+ color: var(--cOrange);
}
.backup-codes {
- font-family: var(--postCodeFont, monospace);
+ font-family: var(--monoFont);
}
}
</style>
diff --git a/src/components/settings_modal/tabs/security_tab/mfa_totp.vue b/src/components/settings_modal/tabs/security_tab/mfa_totp.vue
index 8e767bd0..99b66818 100644
--- a/src/components/settings_modal/tabs/security_tab/mfa_totp.vue
+++ b/src/components/settings_modal/tabs/security_tab/mfa_totp.vue
@@ -30,6 +30,7 @@
<input
v-model="currentPassword"
type="password"
+ class="input"
>
</confirm>
<div
diff --git a/src/components/settings_modal/tabs/security_tab/security_tab.vue b/src/components/settings_modal/tabs/security_tab/security_tab.vue
index 6e03bef4..74103f6f 100644
--- a/src/components/settings_modal/tabs/security_tab/security_tab.vue
+++ b/src/components/settings_modal/tabs/security_tab/security_tab.vue
@@ -8,6 +8,7 @@
v-model="newEmail"
type="email"
autocomplete="email"
+ class="input"
>
</div>
<div>
@@ -16,6 +17,7 @@
v-model="changeEmailPassword"
type="password"
autocomplete="current-password"
+ class="input"
>
</div>
<button
@@ -40,6 +42,7 @@
<input
v-model="changePasswordInputs[0]"
type="password"
+ class="input"
>
</div>
<div>
@@ -47,6 +50,7 @@
<input
v-model="changePasswordInputs[1]"
type="password"
+ class="input"
>
</div>
<div>
@@ -54,6 +58,7 @@
<input
v-model="changePasswordInputs[2]"
type="password"
+ class="input"
>
</div>
<button
@@ -143,8 +148,8 @@
/>
</div>
<div>
- <i18n
- path="settings.new_alias_target"
+ <i18n-t
+ keypath="settings.new_alias_target"
tag="p"
>
<code
@@ -152,9 +157,10 @@
>
foo@example.org
</code>
- </i18n>
+ </i18n-t>
<input
v-model="addAliasTarget"
+ class="input"
>
</div>
<button
@@ -175,18 +181,19 @@
<h2>{{ $t('settings.move_account') }}</h2>
<p>{{ $t('settings.move_account_notes') }}</p>
<div>
- <i18n
- path="settings.move_account_target"
+ <i18n-t
+ keypath="settings.move_account_target"
tag="p"
>
- <code
- place="example"
- >
- foo@example.org
- </code>
- </i18n>
+ <template #example>
+ <code>
+ foo@example.org
+ </code>
+ </template>
+ </i18n-t>
<input
v-model="moveAccountTarget"
+ class="input"
>
</div>
<div>
@@ -195,6 +202,7 @@
v-model="moveAccountPassword"
type="password"
autocomplete="current-password"
+ class="input"
>
</div>
<button
@@ -222,6 +230,7 @@
<input
v-model="deleteAccountConfirmPasswordInput"
type="password"
+ class="input"
>
<button
class="btn button-default"
diff --git a/src/components/settings_modal/tabs/theme_tab/preview.vue b/src/components/settings_modal/tabs/theme_tab/theme_preview.vue
index d755279a..dbaecee4 100644
--- a/src/components/settings_modal/tabs/theme_tab/preview.vue
+++ b/src/components/settings_modal/tabs/theme_tab/theme_preview.vue
@@ -1,11 +1,11 @@
<template>
- <div class="preview-container">
+ <div class="theme-preview-container">
<div class="underlay underlay-preview" />
<div class="panel dummy">
<div class="panel-heading">
<div class="title">
{{ $t('settings.style.preview.header') }}
- <span class="badge badge-notification">
+ <span class="badge -notification">
99
</span>
</div>
@@ -81,7 +81,7 @@
class="faint"
scope="global"
>
- <a style="color: var(--faintLink);">
+ <a style="color: var(--linkFaint);">
{{ $t('settings.style.preview.faint_link') }}
</a>
</i18n-t>
@@ -95,17 +95,13 @@
<input
:value="$t('settings.style.preview.input')"
type="text"
+ class="input"
>
<div class="actions">
- <span class="checkbox">
- <input
- id="preview_checkbox"
- checked="very yes"
- type="checkbox"
- >
- <label for="preview_checkbox">{{ $t('settings.style.preview.checkbox') }}</label>
- </span>
+ <Checkbox>
+ {{ $t('settings.style.preview.checkbox') }}
+ </Checkbox>
<button class="btn button-default">
{{ $t('settings.style.preview.button') }}
</button>
@@ -116,6 +112,7 @@
</template>
<script>
+import Checkbox from 'src/components/checkbox/checkbox.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faTimes,
@@ -131,19 +128,123 @@ library.add(
faReply
)
-export default {}
+export default {
+ components: {
+ Checkbox
+ }
+}
</script>
<style lang="scss">
-.preview-container {
+.theme-preview-container {
position: relative;
-}
+ border-top: 1px dashed;
+ border-bottom: 1px dashed;
+ border-color: var(--border);
+ margin: 1em 0;
+ padding: 1em;
+ background-color: var(--wallpaper);
+ background-image: var(--body-background-image);
+ background-size: cover;
+ background-position: 50% 50%;
+
+ .theme-preview-content {
+ padding: 20px;
+ }
+
+ .dummy {
+ .post {
+ font-family: var(--postFont);
+ display: flex;
+
+ .content {
+ flex: 1;
+
+ h4 {
+ margin-bottom: 0.25em;
+ }
+
+ .icons {
+ margin-top: 0.5em;
+ display: flex;
+
+ i {
+ margin-right: 1em;
+ }
+ }
+ }
+ }
+
+ .after-post {
+ margin-top: 1em;
+ display: flex;
+ align-items: center;
+ }
+
+ .avatar,
+ .avatar-alt {
+ background:
+ linear-gradient(
+ 135deg,
+ #b8e1fc 0%,
+ #a9d2f3 10%,
+ #90bae4 25%,
+ #90bcea 37%,
+ #90bff0 50%,
+ #6ba8e5 51%,
+ #a2daf5 83%,
+ #bdf3fd 100%
+ );
+ color: black;
+ font-family: sans-serif;
+ text-align: center;
+ margin-right: 1em;
+ }
+
+ .avatar-alt {
+ flex: 0 auto;
+ margin-left: 28px;
+ font-size: 12px;
+ min-width: 20px;
+ min-height: 20px;
+ line-height: 20px;
+ }
+
+ .avatar {
+ flex: 0 auto;
+ width: 48px;
+ height: 48px;
+ font-size: 14px;
+ line-height: 48px;
+ }
+
+ .actions {
+ display: flex;
+ align-items: baseline;
+
+ .checkbox {
+ margin-right: 1em;
+ flex: 1;
+ }
+ }
+
+ .separator {
+ margin: 1em;
+ border-bottom: 1px solid;
+ border-color: var(--border);
+ }
+
+ .btn {
+ min-width: 3em;
+ }
+ }
-.underlay-preview {
- position: absolute;
- top: 0;
- bottom: 0;
- left: 10px;
- right: 10px;
+ .underlay-preview {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 10px;
+ right: 10px;
+ }
}
-</style>
+ </style>
diff --git a/src/components/settings_modal/tabs/theme_tab/theme_tab.js b/src/components/settings_modal/tabs/theme_tab/theme_tab.js
index 4a739f73..25836559 100644
--- a/src/components/settings_modal/tabs/theme_tab/theme_tab.js
+++ b/src/components/settings_modal/tabs/theme_tab/theme_tab.js
@@ -1,18 +1,11 @@
import {
rgb2hex,
hex2rgb,
- getContrastRatioLayers
+ getContrastRatioLayers,
+ relativeLuminance
} from 'src/services/color_convert/color_convert.js'
import {
- DEFAULT_SHADOWS,
- generateColors,
- generateShadows,
- generateRadii,
- generateFonts,
- composePreset,
- getThemes,
- shadows2to3,
- colors2to3
+ getThemes
} from 'src/services/style_setter/style_setter.js'
import {
newImporter,
@@ -25,8 +18,23 @@ import {
CURRENT_VERSION,
OPACITIES,
getLayers,
- getOpacitySlot
+ getOpacitySlot,
+ DEFAULT_SHADOWS,
+ generateColors,
+ generateShadows,
+ generateRadii,
+ generateFonts,
+ shadows2to3,
+ colors2to3
} from 'src/services/theme_data/theme_data.service.js'
+
+import { convertTheme2To3 } from 'src/services/theme_data/theme2_to_theme3.js'
+import { init } from 'src/services/theme_data/theme_data_3.service.js'
+import {
+ getCssRules,
+ getScopedVersion
+} from 'src/services/theme_data/css_utils.js'
+
import ColorInput from 'src/components/color_input/color_input.vue'
import RangeInput from 'src/components/range_input/range_input.vue'
import OpacityInput from 'src/components/opacity_input/opacity_input.vue'
@@ -37,7 +45,7 @@ import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'
import Checkbox from 'src/components/checkbox/checkbox.vue'
import Select from 'src/components/select/select.vue'
-import Preview from './preview.vue'
+import Preview from './theme_preview.vue'
// List of color values used in v1
const v1OnlyNames = [
@@ -62,6 +70,7 @@ const colorConvert = (color) => {
export default {
data () {
return {
+ themeV3Preview: [],
themeImporter: newImporter({
validator: this.importValidator,
onImport: this.onImport,
@@ -78,10 +87,7 @@ export default {
tempImportFile: undefined,
engineVersion: 0,
- previewShadows: {},
- previewColors: {},
- previewRadii: {},
- previewFonts: {},
+ previewTheme: {},
shadowsInvalid: true,
colorsInvalid: true,
@@ -232,13 +238,6 @@ export default {
chatMessage: this.chatMessageRadiusLocal
}
},
- preview () {
- return composePreset(this.previewColors, this.previewRadii, this.previewShadows, this.previewFonts)
- },
- previewTheme () {
- if (!this.preview.theme.colors) return { colors: {}, opacity: {}, radii: {}, shadows: {}, fonts: {} }
- return this.preview.theme
- },
// This needs optimization maybe
previewContrast () {
try {
@@ -306,14 +305,6 @@ export default {
return {}
}
},
- previewRules () {
- if (!this.preview.rules) return ''
- return [
- ...Object.values(this.preview.rules),
- 'color: var(--text)',
- 'font-family: var(--interfaceFont, sans-serif)'
- ].join(';')
- },
shadowsAvailable () {
return Object.keys(DEFAULT_SHADOWS).sort()
},
@@ -511,16 +502,15 @@ export default {
}
},
setCustomTheme () {
- this.$store.dispatch('setOption', {
- name: 'customTheme',
- value: {
+ this.$store.dispatch('setThemeV2', {
+ customTheme: {
+ ignore: true,
+ themeFileVersion: this.selectedVersion,
themeEngineVersion: CURRENT_VERSION,
...this.previewTheme
- }
- })
- this.$store.dispatch('setOption', {
- name: 'customThemeSource',
- value: {
+ },
+ customThemeSource: {
+ themeFileVersion: this.selectedVersion,
themeEngineVersion: CURRENT_VERSION,
shadows: this.shadowsLocal,
fonts: this.fontsLocal,
@@ -530,16 +520,24 @@ export default {
}
})
},
- updatePreviewColorsAndShadows () {
- this.previewColors = generateColors({
+ updatePreviewColors () {
+ const result = generateColors({
opacity: this.currentOpacity,
colors: this.currentColors
})
- this.previewShadows = generateShadows(
- { shadows: this.shadowsLocal, opacity: this.previewTheme.opacity, themeEngineVersion: this.engineVersion },
- this.previewColors.theme.colors,
- this.previewColors.mod
- )
+ this.previewTheme.colors = result.theme.colors
+ this.previewTheme.opacity = result.theme.opacity
+ },
+ updatePreviewShadows () {
+ this.previewTheme.shadows = generateShadows(
+ {
+ shadows: this.shadowsLocal,
+ opacity: this.previewTheme.opacity,
+ themeEngineVersion: this.engineVersion
+ },
+ this.previewTheme.colors,
+ relativeLuminance(this.previewTheme.colors.bg) < 0.5 ? 1 : -1
+ ).theme.shadows
},
importTheme () { this.themeImporter.importData() },
exportTheme () { this.themeExporter.exportData() },
@@ -608,7 +606,7 @@ export default {
normalizeLocalState (theme, version = 0, source, forceSource = false) {
let input
if (typeof source !== 'undefined') {
- if (forceSource || source.themeEngineVersion === CURRENT_VERSION) {
+ if (forceSource || source?.themeEngineVersion === CURRENT_VERSION) {
input = source
version = source.themeEngineVersion
} else {
@@ -690,6 +688,8 @@ export default {
} else {
this.shadowsLocal = shadows
}
+ this.updatePreviewColors()
+ this.updatePreviewShadows()
this.shadowSelected = this.shadowsAvailable[0]
}
@@ -697,12 +697,25 @@ export default {
this.clearFonts()
this.fontsLocal = fonts
}
+ },
+ updateTheme3Preview () {
+ const theme2 = convertTheme2To3(this.previewTheme)
+ const theme3 = init({
+ inputRuleset: theme2,
+ ultimateBackgroundColor: '#000000',
+ liteMode: true
+ })
+
+ this.themeV3Preview = getScopedVersion(
+ getCssRules(theme3.eager),
+ '#theme-preview'
+ ).join('\n')
}
},
watch: {
currentRadii () {
try {
- this.previewRadii = generateRadii({ radii: this.currentRadii })
+ this.previewTheme.radii = generateRadii({ radii: this.currentRadii }).theme.radii
this.radiiInvalid = false
} catch (e) {
this.radiiInvalid = true
@@ -711,9 +724,8 @@ export default {
},
shadowsLocal: {
handler () {
- if (Object.getOwnPropertyNames(this.previewColors).length === 1) return
try {
- this.updatePreviewColorsAndShadows()
+ this.updatePreviewShadows()
this.shadowsInvalid = false
} catch (e) {
this.shadowsInvalid = true
@@ -725,7 +737,7 @@ export default {
fontsLocal: {
handler () {
try {
- this.previewFonts = generateFonts({ fonts: this.fontsLocal })
+ this.previewTheme.fonts = generateFonts({ fonts: this.fontsLocal }).theme.fonts
this.fontsInvalid = false
} catch (e) {
this.fontsInvalid = true
@@ -736,18 +748,16 @@ export default {
},
currentColors () {
try {
- this.updatePreviewColorsAndShadows()
+ this.updatePreviewColors()
this.colorsInvalid = false
- this.shadowsInvalid = false
} catch (e) {
this.colorsInvalid = true
- this.shadowsInvalid = true
console.warn(e)
}
},
currentOpacity () {
try {
- this.updatePreviewColorsAndShadows()
+ this.updatePreviewColors()
} catch (e) {
console.warn(e)
}
@@ -755,7 +765,6 @@ export default {
selected () {
this.selectedTheme = Object.entries(this.availableStyles).find(([k, s]) => {
if (Array.isArray(s)) {
- console.log(s[0] === this.selected, this.selected)
return s[0] === this.selected
} else {
return s.name === this.selected
diff --git a/src/components/settings_modal/tabs/theme_tab/theme_tab.scss b/src/components/settings_modal/tabs/theme_tab/theme_tab.scss
index 9935c2e7..84933fb8 100644
--- a/src/components/settings_modal/tabs/theme_tab/theme_tab.scss
+++ b/src/components/settings_modal/tabs/theme_tab/theme_tab.scss
@@ -1,6 +1,9 @@
-@import "src/variables";
-
.theme-tab {
+ .deprecation-warning {
+ padding: 0.5em;
+ margin: 2em;
+ }
+
padding-bottom: 2em;
.preset-switcher {
@@ -12,6 +15,10 @@
margin-right: 0.25em;
}
+ .btn-group .btn {
+ margin: 0;
+ }
+
.style-control {
display: flex;
align-items: baseline;
@@ -159,111 +166,6 @@
}
}
- .preview-container {
- border-top: 1px dashed;
- border-bottom: 1px dashed;
- border-color: $fallback--border;
- border-color: var(--border, $fallback--border);
- margin: 1em 0;
- padding: 1em;
- background-color: var(--wallpaper);
- background-image: var(--body-background-image);
- background-size: cover;
- background-position: 50% 50%;
-
- .dummy {
- .post {
- font-family: var(--postFont);
- display: flex;
-
- .content {
- flex: 1;
-
- h4 {
- margin-bottom: 0.25em;
- }
-
- .icons {
- margin-top: 0.5em;
- display: flex;
-
- i {
- margin-right: 1em;
- }
- }
- }
- }
-
- .after-post {
- margin-top: 1em;
- display: flex;
- align-items: center;
- }
-
- .avatar,
- .avatar-alt {
- background:
- linear-gradient(
- 135deg,
- #b8e1fc 0%,
- #a9d2f3 10%,
- #90bae4 25%,
- #90bcea 37%,
- #90bff0 50%,
- #6ba8e5 51%,
- #a2daf5 83%,
- #bdf3fd 100%
- );
- color: black;
- font-family: sans-serif;
- text-align: center;
- margin-right: 1em;
- }
-
- .avatar-alt {
- flex: 0 auto;
- margin-left: 28px;
- font-size: 12px;
- min-width: 20px;
- min-height: 20px;
- line-height: 20px;
- border-radius: $fallback--avatarAltRadius;
- border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);
- }
-
- .avatar {
- flex: 0 auto;
- width: 48px;
- height: 48px;
- font-size: 14px;
- line-height: 48px;
- }
-
- .actions {
- display: flex;
- align-items: baseline;
-
- .checkbox {
- display: inline-flex;
- align-items: baseline;
- margin-right: 1em;
- flex: 1;
- }
- }
-
- .separator {
- margin: 1em;
- border-bottom: 1px solid;
- border-color: $fallback--border;
- border-color: var(--border, $fallback--border);
- }
-
- .btn {
- min-width: 3em;
- }
- }
- }
-
.radius-item {
flex-basis: auto;
}
@@ -296,7 +198,7 @@
border: 0;
box-shadow: none;
background: transparent;
- color: var(--faint, $fallback--faint);
+ color: var(--textFaint);
align-self: stretch;
}
@@ -316,10 +218,6 @@
max-width: 50em;
}
- .theme-preview-content {
- padding: 20px;
- }
-
.theme-warning {
display: flex;
align-items: baseline;
diff --git a/src/components/settings_modal/tabs/theme_tab/theme_tab.vue b/src/components/settings_modal/tabs/theme_tab/theme_tab.vue
index ff2fece9..4498c143 100644
--- a/src/components/settings_modal/tabs/theme_tab/theme_tab.vue
+++ b/src/components/settings_modal/tabs/theme_tab/theme_tab.vue
@@ -1,5 +1,8 @@
<template>
<div class="theme-tab">
+ <div class="alert warning deprecation-warning">
+ {{ $t("settings.style.themes2_outdated") }}
+ </div>
<div class="presets-container">
<div class="save-load">
<div
@@ -120,7 +123,19 @@
</div>
</div>
- <preview :style="previewRules" />
+ <!-- eslint-disable vue/no-v-text-v-html-on-component -->
+ <component :is="'style'" v-html="themeV3Preview"/>
+ <!-- eslint-enable vue/no-v-text-v-html-on-component -->
+ <preview id="theme-preview"/>
+
+ <div>
+ <button
+ class="btn button-default"
+ @click="updateTheme3Preview"
+ >
+ {{ $t("settings.style.update_preview") }}
+ </button>
+ </div>
<keep-alive>
<tab-switcher key="style-tweak">
@@ -156,7 +171,7 @@
<OpacityInput
v-model="bgOpacityLocal"
name="bgOpacity"
- :fallback="previewTheme.opacity.bg"
+ :fallback="previewTheme.opacity?.bg"
/>
<ColorInput
v-model="textColorLocal"
@@ -167,14 +182,14 @@
<ColorInput
v-model="accentColorLocal"
name="accentColor"
- :fallback="previewTheme.colors.link"
+ :fallback="previewTheme.colors?.link"
:label="$t('settings.accent')"
:show-optional-tickbox="typeof linkColorLocal !== 'undefined'"
/>
<ColorInput
v-model="linkColorLocal"
name="linkColor"
- :fallback="previewTheme.colors.accent"
+ :fallback="previewTheme.colors?.accent"
:label="$t('settings.links')"
:show-optional-tickbox="typeof accentColorLocal !== 'undefined'"
/>
@@ -190,13 +205,13 @@
v-model="fgTextColorLocal"
name="fgTextColor"
:label="$t('settings.text')"
- :fallback="previewTheme.colors.fgText"
+ :fallback="previewTheme.colors?.fgText"
/>
<ColorInput
v-model="fgLinkColorLocal"
name="fgLinkColor"
:label="$t('settings.links')"
- :fallback="previewTheme.colors.fgLink"
+ :fallback="previewTheme.colors?.fgLink"
/>
<p>{{ $t('settings.style.common_colors.foreground_hint') }}</p>
</div>
@@ -256,14 +271,14 @@
<ColorInput
v-model="postLinkColorLocal"
name="postLinkColor"
- :fallback="previewTheme.colors.accent"
+ :fallback="previewTheme.colors?.accent"
:label="$t('settings.links')"
/>
<ContrastRatio :contrast="previewContrast.postLink" />
<ColorInput
v-model="postGreentextColorLocal"
name="postGreentextColor"
- :fallback="previewTheme.colors.cGreen"
+ :fallback="previewTheme.colors?.cGreen"
:label="$t('settings.greentext')"
/>
<ContrastRatio :contrast="previewContrast.postGreentext" />
@@ -272,13 +287,13 @@
v-model="alertErrorColorLocal"
name="alertError"
:label="$t('settings.style.advanced_colors.alert_error')"
- :fallback="previewTheme.colors.alertError"
+ :fallback="previewTheme.colors?.alertError"
/>
<ColorInput
v-model="alertErrorTextColorLocal"
name="alertErrorText"
:label="$t('settings.text')"
- :fallback="previewTheme.colors.alertErrorText"
+ :fallback="previewTheme.colors?.alertErrorText"
/>
<ContrastRatio
:contrast="previewContrast.alertErrorText"
@@ -288,13 +303,13 @@
v-model="alertWarningColorLocal"
name="alertWarning"
:label="$t('settings.style.advanced_colors.alert_warning')"
- :fallback="previewTheme.colors.alertWarning"
+ :fallback="previewTheme.colors?.alertWarning"
/>
<ColorInput
v-model="alertWarningTextColorLocal"
name="alertWarningText"
:label="$t('settings.text')"
- :fallback="previewTheme.colors.alertWarningText"
+ :fallback="previewTheme.colors?.alertWarningText"
/>
<ContrastRatio
:contrast="previewContrast.alertWarningText"
@@ -304,13 +319,13 @@
v-model="alertNeutralColorLocal"
name="alertNeutral"
:label="$t('settings.style.advanced_colors.alert_neutral')"
- :fallback="previewTheme.colors.alertNeutral"
+ :fallback="previewTheme.colors?.alertNeutral"
/>
<ColorInput
v-model="alertNeutralTextColorLocal"
name="alertNeutralText"
:label="$t('settings.text')"
- :fallback="previewTheme.colors.alertNeutralText"
+ :fallback="previewTheme.colors?.alertNeutralText"
/>
<ContrastRatio
:contrast="previewContrast.alertNeutralText"
@@ -319,7 +334,7 @@
<OpacityInput
v-model="alertOpacityLocal"
name="alertOpacity"
- :fallback="previewTheme.opacity.alert"
+ :fallback="previewTheme.opacity?.alert"
/>
</div>
<div class="color-item">
@@ -328,13 +343,13 @@
v-model="badgeNotificationColorLocal"
name="badgeNotification"
:label="$t('settings.style.advanced_colors.badge_notification')"
- :fallback="previewTheme.colors.badgeNotification"
+ :fallback="previewTheme.colors?.badgeNotification"
/>
<ColorInput
v-model="badgeNotificationTextColorLocal"
name="badgeNotificationText"
:label="$t('settings.text')"
- :fallback="previewTheme.colors.badgeNotificationText"
+ :fallback="previewTheme.colors?.badgeNotificationText"
/>
<ContrastRatio
:contrast="previewContrast.badgeNotificationText"
@@ -346,19 +361,19 @@
<ColorInput
v-model="panelColorLocal"
name="panelColor"
- :fallback="previewTheme.colors.panel"
+ :fallback="previewTheme.colors?.panel"
:label="$t('settings.background')"
/>
<OpacityInput
v-model="panelOpacityLocal"
name="panelOpacity"
- :fallback="previewTheme.opacity.panel"
+ :fallback="previewTheme.opacity?.panel"
:disabled="panelColorLocal === 'transparent'"
/>
<ColorInput
v-model="panelTextColorLocal"
name="panelTextColor"
- :fallback="previewTheme.colors.panelText"
+ :fallback="previewTheme.colors?.panelText"
:label="$t('settings.text')"
/>
<ContrastRatio
@@ -368,7 +383,7 @@
<ColorInput
v-model="panelLinkColorLocal"
name="panelLinkColor"
- :fallback="previewTheme.colors.panelLink"
+ :fallback="previewTheme.colors?.panelLink"
:label="$t('settings.links')"
/>
<ContrastRatio
@@ -381,20 +396,20 @@
<ColorInput
v-model="topBarColorLocal"
name="topBarColor"
- :fallback="previewTheme.colors.topBar"
+ :fallback="previewTheme.colors?.topBar"
:label="$t('settings.background')"
/>
<ColorInput
v-model="topBarTextColorLocal"
name="topBarTextColor"
- :fallback="previewTheme.colors.topBarText"
+ :fallback="previewTheme.colors?.topBarText"
:label="$t('settings.text')"
/>
<ContrastRatio :contrast="previewContrast.topBarText" />
<ColorInput
v-model="topBarLinkColorLocal"
name="topBarLinkColor"
- :fallback="previewTheme.colors.topBarLink"
+ :fallback="previewTheme.colors?.topBarLink"
:label="$t('settings.links')"
/>
<ContrastRatio :contrast="previewContrast.topBarLink" />
@@ -404,19 +419,19 @@
<ColorInput
v-model="inputColorLocal"
name="inputColor"
- :fallback="previewTheme.colors.input"
+ :fallback="previewTheme.colors?.input"
:label="$t('settings.background')"
/>
<OpacityInput
v-model="inputOpacityLocal"
name="inputOpacity"
- :fallback="previewTheme.opacity.input"
+ :fallback="previewTheme.opacity?.input"
:disabled="inputColorLocal === 'transparent'"
/>
<ColorInput
v-model="inputTextColorLocal"
name="inputTextColor"
- :fallback="previewTheme.colors.inputText"
+ :fallback="previewTheme.colors?.inputText"
:label="$t('settings.text')"
/>
<ContrastRatio :contrast="previewContrast.inputText" />
@@ -426,33 +441,33 @@
<ColorInput
v-model="btnColorLocal"
name="btnColor"
- :fallback="previewTheme.colors.btn"
+ :fallback="previewTheme.colors?.btn"
:label="$t('settings.background')"
/>
<OpacityInput
v-model="btnOpacityLocal"
name="btnOpacity"
- :fallback="previewTheme.opacity.btn"
+ :fallback="previewTheme.opacity?.btn"
:disabled="btnColorLocal === 'transparent'"
/>
<ColorInput
v-model="btnTextColorLocal"
name="btnTextColor"
- :fallback="previewTheme.colors.btnText"
+ :fallback="previewTheme.colors?.btnText"
:label="$t('settings.text')"
/>
<ContrastRatio :contrast="previewContrast.btnText" />
<ColorInput
v-model="btnPanelTextColorLocal"
name="btnPanelTextColor"
- :fallback="previewTheme.colors.btnPanelText"
+ :fallback="previewTheme.colors?.btnPanelText"
:label="$t('settings.style.advanced_colors.panel_header')"
/>
<ContrastRatio :contrast="previewContrast.btnPanelText" />
<ColorInput
v-model="btnTopBarTextColorLocal"
name="btnTopBarTextColor"
- :fallback="previewTheme.colors.btnTopBarText"
+ :fallback="previewTheme.colors?.btnTopBarText"
:label="$t('settings.style.advanced_colors.top_bar')"
/>
<ContrastRatio :contrast="previewContrast.btnTopBarText" />
@@ -460,27 +475,27 @@
<ColorInput
v-model="btnPressedColorLocal"
name="btnPressedColor"
- :fallback="previewTheme.colors.btnPressed"
+ :fallback="previewTheme.colors?.btnPressed"
:label="$t('settings.background')"
/>
<ColorInput
v-model="btnPressedTextColorLocal"
name="btnPressedTextColor"
- :fallback="previewTheme.colors.btnPressedText"
+ :fallback="previewTheme.colors?.btnPressedText"
:label="$t('settings.text')"
/>
<ContrastRatio :contrast="previewContrast.btnPressedText" />
<ColorInput
v-model="btnPressedPanelTextColorLocal"
name="btnPressedPanelTextColor"
- :fallback="previewTheme.colors.btnPressedPanelText"
+ :fallback="previewTheme.colors?.btnPressedPanelText"
:label="$t('settings.style.advanced_colors.panel_header')"
/>
<ContrastRatio :contrast="previewContrast.btnPressedPanelText" />
<ColorInput
v-model="btnPressedTopBarTextColorLocal"
name="btnPressedTopBarTextColor"
- :fallback="previewTheme.colors.btnPressedTopBarText"
+ :fallback="previewTheme.colors?.btnPressedTopBarText"
:label="$t('settings.style.advanced_colors.top_bar')"
/>
<ContrastRatio :contrast="previewContrast.btnPressedTopBarText" />
@@ -488,52 +503,52 @@
<ColorInput
v-model="btnDisabledColorLocal"
name="btnDisabledColor"
- :fallback="previewTheme.colors.btnDisabled"
+ :fallback="previewTheme.colors?.btnDisabled"
:label="$t('settings.background')"
/>
<ColorInput
v-model="btnDisabledTextColorLocal"
name="btnDisabledTextColor"
- :fallback="previewTheme.colors.btnDisabledText"
+ :fallback="previewTheme.colors?.btnDisabledText"
:label="$t('settings.text')"
/>
<ColorInput
v-model="btnDisabledPanelTextColorLocal"
name="btnDisabledPanelTextColor"
- :fallback="previewTheme.colors.btnDisabledPanelText"
+ :fallback="previewTheme.colors?.btnDisabledPanelText"
:label="$t('settings.style.advanced_colors.panel_header')"
/>
<ColorInput
v-model="btnDisabledTopBarTextColorLocal"
name="btnDisabledTopBarTextColor"
- :fallback="previewTheme.colors.btnDisabledTopBarText"
+ :fallback="previewTheme.colors?.btnDisabledTopBarText"
:label="$t('settings.style.advanced_colors.top_bar')"
/>
<h5>{{ $t('settings.style.advanced_colors.toggled') }}</h5>
<ColorInput
v-model="btnToggledColorLocal"
name="btnToggledColor"
- :fallback="previewTheme.colors.btnToggled"
+ :fallback="previewTheme.colors?.btnToggled"
:label="$t('settings.background')"
/>
<ColorInput
v-model="btnToggledTextColorLocal"
name="btnToggledTextColor"
- :fallback="previewTheme.colors.btnToggledText"
+ :fallback="previewTheme.colors?.btnToggledText"
:label="$t('settings.text')"
/>
<ContrastRatio :contrast="previewContrast.btnToggledText" />
<ColorInput
v-model="btnToggledPanelTextColorLocal"
name="btnToggledPanelTextColor"
- :fallback="previewTheme.colors.btnToggledPanelText"
+ :fallback="previewTheme.colors?.btnToggledPanelText"
:label="$t('settings.style.advanced_colors.panel_header')"
/>
<ContrastRatio :contrast="previewContrast.btnToggledPanelText" />
<ColorInput
v-model="btnToggledTopBarTextColorLocal"
name="btnToggledTopBarTextColor"
- :fallback="previewTheme.colors.btnToggledTopBarText"
+ :fallback="previewTheme.colors?.btnToggledTopBarText"
:label="$t('settings.style.advanced_colors.top_bar')"
/>
<ContrastRatio :contrast="previewContrast.btnToggledTopBarText" />
@@ -543,20 +558,20 @@
<ColorInput
v-model="tabColorLocal"
name="tabColor"
- :fallback="previewTheme.colors.tab"
+ :fallback="previewTheme.colors?.tab"
:label="$t('settings.background')"
/>
<ColorInput
v-model="tabTextColorLocal"
name="tabTextColor"
- :fallback="previewTheme.colors.tabText"
+ :fallback="previewTheme.colors?.tabText"
:label="$t('settings.text')"
/>
<ContrastRatio :contrast="previewContrast.tabText" />
<ColorInput
v-model="tabActiveTextColorLocal"
name="tabActiveTextColor"
- :fallback="previewTheme.colors.tabActiveText"
+ :fallback="previewTheme.colors?.tabActiveText"
:label="$t('settings.text')"
/>
<ContrastRatio :contrast="previewContrast.tabActiveText" />
@@ -566,13 +581,13 @@
<ColorInput
v-model="borderColorLocal"
name="borderColor"
- :fallback="previewTheme.colors.border"
+ :fallback="previewTheme.colors?.border"
:label="$t('settings.style.common.color')"
/>
<OpacityInput
v-model="borderOpacityLocal"
name="borderOpacity"
- :fallback="previewTheme.opacity.border"
+ :fallback="previewTheme.opacity?.border"
:disabled="borderColorLocal === 'transparent'"
/>
</div>
@@ -581,25 +596,25 @@
<ColorInput
v-model="faintColorLocal"
name="faintColor"
- :fallback="previewTheme.colors.faint"
+ :fallback="previewTheme.colors?.faint"
:label="$t('settings.text')"
/>
<ColorInput
v-model="faintLinkColorLocal"
name="faintLinkColor"
- :fallback="previewTheme.colors.faintLink"
+ :fallback="previewTheme.colors?.faintLink"
:label="$t('settings.links')"
/>
<ColorInput
v-model="panelFaintColorLocal"
name="panelFaintColor"
- :fallback="previewTheme.colors.panelFaint"
+ :fallback="previewTheme.colors?.panelFaint"
:label="$t('settings.style.advanced_colors.panel_header')"
/>
<OpacityInput
v-model="faintOpacityLocal"
name="faintOpacity"
- :fallback="previewTheme.opacity.faint"
+ :fallback="previewTheme.opacity?.faint"
/>
</div>
<div class="color-item">
@@ -608,12 +623,12 @@
v-model="underlayColorLocal"
name="underlay"
:label="$t('settings.style.advanced_colors.underlay')"
- :fallback="previewTheme.colors.underlay"
+ :fallback="previewTheme.colors?.underlay"
/>
<OpacityInput
v-model="underlayOpacityLocal"
name="underlayOpacity"
- :fallback="previewTheme.opacity.underlay"
+ :fallback="previewTheme.opacity?.underlay"
:disabled="underlayOpacityLocal === 'transparent'"
/>
</div>
@@ -623,7 +638,7 @@
v-model="wallpaperColorLocal"
name="wallpaper"
:label="$t('settings.style.advanced_colors.wallpaper')"
- :fallback="previewTheme.colors.wallpaper"
+ :fallback="previewTheme.colors?.wallpaper"
/>
</div>
<div class="color-item">
@@ -632,13 +647,13 @@
v-model="pollColorLocal"
name="poll"
:label="$t('settings.background')"
- :fallback="previewTheme.colors.poll"
+ :fallback="previewTheme.colors?.poll"
/>
<ColorInput
v-model="pollTextColorLocal"
name="pollText"
:label="$t('settings.text')"
- :fallback="previewTheme.colors.pollText"
+ :fallback="previewTheme.colors?.pollText"
/>
</div>
<div class="color-item">
@@ -647,7 +662,7 @@
v-model="iconColorLocal"
name="icon"
:label="$t('settings.style.advanced_colors.icons')"
- :fallback="previewTheme.colors.icon"
+ :fallback="previewTheme.colors?.icon"
/>
</div>
<div class="color-item">
@@ -656,20 +671,20 @@
v-model="highlightColorLocal"
name="highlight"
:label="$t('settings.background')"
- :fallback="previewTheme.colors.highlight"
+ :fallback="previewTheme.colors?.highlight"
/>
<ColorInput
v-model="highlightTextColorLocal"
name="highlightText"
:label="$t('settings.text')"
- :fallback="previewTheme.colors.highlightText"
+ :fallback="previewTheme.colors?.highlightText"
/>
<ContrastRatio :contrast="previewContrast.highlightText" />
<ColorInput
v-model="highlightLinkColorLocal"
name="highlightLink"
:label="$t('settings.links')"
- :fallback="previewTheme.colors.highlightLink"
+ :fallback="previewTheme.colors?.highlightLink"
/>
<ContrastRatio :contrast="previewContrast.highlightLink" />
</div>
@@ -679,26 +694,26 @@
v-model="popoverColorLocal"
name="popover"
:label="$t('settings.background')"
- :fallback="previewTheme.colors.popover"
+ :fallback="previewTheme.colors?.popover"
/>
<OpacityInput
v-model="popoverOpacityLocal"
name="popoverOpacity"
- :fallback="previewTheme.opacity.popover"
+ :fallback="previewTheme.opacity?.popover"
:disabled="popoverOpacityLocal === 'transparent'"
/>
<ColorInput
v-model="popoverTextColorLocal"
name="popoverText"
:label="$t('settings.text')"
- :fallback="previewTheme.colors.popoverText"
+ :fallback="previewTheme.colors?.popoverText"
/>
<ContrastRatio :contrast="previewContrast.popoverText" />
<ColorInput
v-model="popoverLinkColorLocal"
name="popoverLink"
:label="$t('settings.links')"
- :fallback="previewTheme.colors.popoverLink"
+ :fallback="previewTheme.colors?.popoverLink"
/>
<ContrastRatio :contrast="previewContrast.popoverLink" />
</div>
@@ -708,20 +723,20 @@
v-model="selectedPostColorLocal"
name="selectedPost"
:label="$t('settings.background')"
- :fallback="previewTheme.colors.selectedPost"
+ :fallback="previewTheme.colors?.selectedPost"
/>
<ColorInput
v-model="selectedPostTextColorLocal"
name="selectedPostText"
:label="$t('settings.text')"
- :fallback="previewTheme.colors.selectedPostText"
+ :fallback="previewTheme.colors?.selectedPostText"
/>
<ContrastRatio :contrast="previewContrast.selectedPostText" />
<ColorInput
v-model="selectedPostLinkColorLocal"
name="selectedPostLink"
:label="$t('settings.links')"
- :fallback="previewTheme.colors.selectedPostLink"
+ :fallback="previewTheme.colors?.selectedPostLink"
/>
<ContrastRatio :contrast="previewContrast.selectedPostLink" />
</div>
@@ -731,20 +746,20 @@
v-model="selectedMenuColorLocal"
name="selectedMenu"
:label="$t('settings.background')"
- :fallback="previewTheme.colors.selectedMenu"
+ :fallback="previewTheme.colors?.selectedMenu"
/>
<ColorInput
v-model="selectedMenuTextColorLocal"
name="selectedMenuText"
:label="$t('settings.text')"
- :fallback="previewTheme.colors.selectedMenuText"
+ :fallback="previewTheme.colors?.selectedMenuText"
/>
<ContrastRatio :contrast="previewContrast.selectedMenuText" />
<ColorInput
v-model="selectedMenuLinkColorLocal"
name="selectedMenuLink"
:label="$t('settings.links')"
- :fallback="previewTheme.colors.selectedMenuLink"
+ :fallback="previewTheme.colors?.selectedMenuLink"
/>
<ContrastRatio :contrast="previewContrast.selectedMenuLink" />
</div>
@@ -753,57 +768,57 @@
<ColorInput
v-model="chatBgColorLocal"
name="chatBgColor"
- :fallback="previewTheme.colors.bg"
+ :fallback="previewTheme.colors?.bg"
:label="$t('settings.background')"
/>
<h5>{{ $t('settings.style.advanced_colors.chat.incoming') }}</h5>
<ColorInput
v-model="chatMessageIncomingBgColorLocal"
name="chatMessageIncomingBgColor"
- :fallback="previewTheme.colors.bg"
+ :fallback="previewTheme.colors?.bg"
:label="$t('settings.background')"
/>
<ColorInput
v-model="chatMessageIncomingTextColorLocal"
name="chatMessageIncomingTextColor"
- :fallback="previewTheme.colors.text"
+ :fallback="previewTheme.colors?.text"
:label="$t('settings.text')"
/>
<ColorInput
v-model="chatMessageIncomingLinkColorLocal"
name="chatMessageIncomingLinkColor"
- :fallback="previewTheme.colors.link"
+ :fallback="previewTheme.colors?.link"
:label="$t('settings.links')"
/>
<ColorInput
v-model="chatMessageIncomingBorderColorLocal"
name="chatMessageIncomingBorderLinkColor"
- :fallback="previewTheme.colors.fg"
+ :fallback="previewTheme.colors?.fg"
:label="$t('settings.style.advanced_colors.chat.border')"
/>
<h5>{{ $t('settings.style.advanced_colors.chat.outgoing') }}</h5>
<ColorInput
v-model="chatMessageOutgoingBgColorLocal"
name="chatMessageOutgoingBgColor"
- :fallback="previewTheme.colors.bg"
+ :fallback="previewTheme.colors?.bg"
:label="$t('settings.background')"
/>
<ColorInput
v-model="chatMessageOutgoingTextColorLocal"
name="chatMessageOutgoingTextColor"
- :fallback="previewTheme.colors.text"
+ :fallback="previewTheme.colors?.text"
:label="$t('settings.text')"
/>
<ColorInput
v-model="chatMessageOutgoingLinkColorLocal"
name="chatMessageOutgoingLinkColor"
- :fallback="previewTheme.colors.link"
+ :fallback="previewTheme.colors?.link"
:label="$t('settings.links')"
/>
<ColorInput
v-model="chatMessageOutgoingBorderColorLocal"
name="chatMessageOutgoingBorderLinkColor"
- :fallback="previewTheme.colors.bg"
+ :fallback="previewTheme.colors?.bg"
:label="$t('settings.style.advanced_colors.chat.border')"
/>
</div>
@@ -826,7 +841,7 @@
v-model="btnRadiusLocal"
name="btnRadius"
:label="$t('settings.btnRadius')"
- :fallback="previewTheme.radii.btn"
+ :fallback="previewTheme.radii?.btn"
max="16"
hard-min="0"
/>
@@ -834,7 +849,7 @@
v-model="inputRadiusLocal"
name="inputRadius"
:label="$t('settings.inputRadius')"
- :fallback="previewTheme.radii.input"
+ :fallback="previewTheme.radii?.input"
max="9"
hard-min="0"
/>
@@ -842,7 +857,7 @@
v-model="checkboxRadiusLocal"
name="checkboxRadius"
:label="$t('settings.checkboxRadius')"
- :fallback="previewTheme.radii.checkbox"
+ :fallback="previewTheme.radii?.checkbox"
max="16"
hard-min="0"
/>
@@ -850,7 +865,7 @@
v-model="panelRadiusLocal"
name="panelRadius"
:label="$t('settings.panelRadius')"
- :fallback="previewTheme.radii.panel"
+ :fallback="previewTheme.radii?.panel"
max="50"
hard-min="0"
/>
@@ -858,7 +873,7 @@
v-model="avatarRadiusLocal"
name="avatarRadius"
:label="$t('settings.avatarRadius')"
- :fallback="previewTheme.radii.avatar"
+ :fallback="previewTheme.radii?.avatar"
max="28"
hard-min="0"
/>
@@ -866,7 +881,7 @@
v-model="avatarAltRadiusLocal"
name="avatarAltRadius"
:label="$t('settings.avatarAltRadius')"
- :fallback="previewTheme.radii.avatarAlt"
+ :fallback="previewTheme.radii?.avatarAlt"
max="28"
hard-min="0"
/>
@@ -874,7 +889,7 @@
v-model="attachmentRadiusLocal"
name="attachmentRadius"
:label="$t('settings.attachmentRadius')"
- :fallback="previewTheme.radii.attachment"
+ :fallback="previewTheme.radii?.attachment"
max="50"
hard-min="0"
/>
@@ -882,7 +897,7 @@
v-model="tooltipRadiusLocal"
name="tooltipRadius"
:label="$t('settings.tooltipRadius')"
- :fallback="previewTheme.radii.tooltip"
+ :fallback="previewTheme.radii?.tooltip"
max="50"
hard-min="0"
/>
@@ -890,7 +905,7 @@
v-model="chatMessageRadiusLocal"
name="chatMessageRadius"
:label="$t('settings.chatMessageRadius')"
- :fallback="previewTheme.radii.chatMessage || 2"
+ :fallback="previewTheme.radii?.chatMessage || 2"
max="50"
hard-min="0"
/>
@@ -996,26 +1011,26 @@
v-model="fontsLocal.interface"
name="ui"
:label="$t('settings.style.fonts.components.interface')"
- :fallback="previewTheme.fonts.interface"
+ :fallback="previewTheme.fonts?.interface"
no-inherit="1"
/>
<FontControl
v-model="fontsLocal.input"
name="input"
:label="$t('settings.style.fonts.components.input')"
- :fallback="previewTheme.fonts.input"
+ :fallback="previewTheme.fonts?.input"
/>
<FontControl
v-model="fontsLocal.post"
name="post"
:label="$t('settings.style.fonts.components.post')"
- :fallback="previewTheme.fonts.post"
+ :fallback="previewTheme.fonts?.post"
/>
<FontControl
v-model="fontsLocal.postCode"
name="postCode"
:label="$t('settings.style.fonts.components.postCode')"
- :fallback="previewTheme.fonts.postCode"
+ :fallback="previewTheme.fonts?.postCode"
/>
</div>
</tab-switcher>