diff options
Diffstat (limited to 'src/components/settings_modal')
43 files changed, 2234 insertions, 549 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 index 8163af59..f57310ee 100644 --- a/src/components/settings_modal/admin_tabs/frontends_tab.js +++ b/src/components/settings_modal/admin_tabs/frontends_tab.js @@ -55,9 +55,13 @@ const FrontendsTab = { return fe.refs.includes(frontend.ref) }, getSuggestedRef (frontend) { - const defaultFe = this.adminDraft[':pleroma'][':frontends'][':primary'] - if (defaultFe?.name === frontend.name && this.canInstall(defaultFe)) { - return defaultFe.ref + 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] } diff --git a/src/components/settings_modal/admin_tabs/frontends_tab.vue b/src/components/settings_modal/admin_tabs/frontends_tab.vue index dd4c9790..8fb3d399 100644 --- a/src/components/settings_modal/admin_tabs/frontends_tab.vue +++ b/src/components/settings_modal/admin_tabs/frontends_tab.vue @@ -6,7 +6,10 @@ <div class="setting-item"> <h2>{{ $t('admin_dash.tabs.frontends') }}</h2> <p>{{ $t('admin_dash.frontend.wip_notice') }}</p> - <ul class="setting-list"> + <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> @@ -23,8 +26,18 @@ </ul> </li> </ul> + <div + v-else + class="setting-list" + > + {{ $t('admin_dash.frontend.default_frontend_unavail') }} + </div> + <div class="setting-list relative"> - <PanelLoading class="overlay" v-if="working"/> + <PanelLoading + v-if="working" + class="overlay" + /> <h3>{{ $t('admin_dash.frontend.available_frontends') }}</h3> <ul class="cards-list"> <li @@ -33,9 +46,9 @@ > <strong>{{ frontend.name }}</strong> {{ ' ' }} - <span v-if="adminDraft[':pleroma'][':frontends'][':primary']?.name === frontend.name"> + <span v-if="adminDraft && adminDraft[':pleroma'][':frontends'][':primary']?.name === frontend.name"> <i18n-t - v-if="adminDraft[':pleroma'][':frontends'][':primary']?.ref === frontend.refs[0]" + v-if="adminDraft && adminDraft[':pleroma'][':frontends'][':primary']?.ref === frontend.refs[0]" keypath="admin_dash.frontend.is_default" /> <i18n-t @@ -43,7 +56,7 @@ keypath="admin_dash.frontend.is_default_custom" > <template #version> - <code>{{ adminDraft[':pleroma'][':frontends'][':primary'].ref }}</code> + <code>{{ adminDraft && adminDraft[':pleroma'][':frontends'][':primary'].ref }}</code> </template> </i18n-t> </span> @@ -103,7 +116,7 @@ <button v-for="ref in frontend.refs" :key="ref" - class="button-default dropdown-item" + class="menu-item dropdown-item" @click.prevent="update(frontend, ref)" @click="close" > @@ -134,7 +147,7 @@ class="button button-default btn" type="button" :disabled=" - adminDraft[':pleroma'][':frontends'][':primary']?.name === frontend.name && + !adminDraft || adminDraft[':pleroma'][':frontends'][':primary']?.name === frontend.name && adminDraft[':pleroma'][':frontends'][':primary']?.ref === frontend.refs[0] " @click="setDefault(frontend)" @@ -160,7 +173,7 @@ <button v-for="ref in frontend.installedRefs || frontend.refs" :key="ref" - class="button-default dropdown-item" + class="menu-item dropdown-item" @click.prevent="setDefault(frontend, ref)" @click="close" > diff --git a/src/components/settings_modal/admin_tabs/instance_tab.vue b/src/components/settings_modal/admin_tabs/instance_tab.vue index a0e3351e..32e8df25 100644 --- a/src/components/settings_modal/admin_tabs/instance_tab.vue +++ b/src/components/settings_modal/admin_tabs/instance_tab.vue @@ -8,7 +8,10 @@ </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" /> + <AttachmentSetting + compact + path=":pleroma.:instance.:favicon" + /> </li> <li> <StringSetting path=":pleroma.:instance.:email" /> @@ -20,7 +23,10 @@ <StringSetting path=":pleroma.:instance.:short_description" /> </li> <li> - <AttachmentSetting compact path=":pleroma.:instance.:instance_thumbnail" /> + <AttachmentSetting + compact + path=":pleroma.:instance.:instance_thumbnail" + /> </li> <li> <AttachmentSetting path=":pleroma.:instance.:background_image" /> diff --git a/src/components/settings_modal/helpers/attachment_setting.vue b/src/components/settings_modal/helpers/attachment_setting.vue index b50231f2..96c80ab1 100644 --- a/src/components/settings_modal/helpers/attachment_setting.vue +++ b/src/components/settings_modal/helpers/attachment_setting.vue @@ -29,7 +29,7 @@ <label for="path">{{ $t('settings.url') }}</label> <input :id="path" - class="string-input" + class="input string-input" :disabled="shouldBeDisabled" :value="realDraftMode ? draft : state" @change="update" 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/modified_indicator.vue b/src/components/settings_modal/helpers/modified_indicator.vue index 45db3fc2..a747cebd 100644 --- a/src/components/settings_modal/helpers/modified_indicator.vue +++ b/src/components/settings_modal/helpers/modified_indicator.vue @@ -15,7 +15,7 @@ </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.vue b/src/components/settings_modal/helpers/number_setting.vue index 93f11331..32dc6f83 100644 --- a/src/components/settings_modal/helpers/number_setting.vue +++ b/src/components/settings_modal/helpers/number_setting.vue @@ -15,9 +15,10 @@ </template> <slot v-else /> </label> + {{ ' ' }} <input :id="path" - class="number-input" + class="input number-input" type="number" :step="step || 1" :disabled="shouldBeDisabled" diff --git a/src/components/settings_modal/helpers/setting.js b/src/components/settings_modal/helpers/setting.js index b3add346..3b3e6268 100644 --- a/src/components/settings_modal/helpers/setting.js +++ b/src/components/settings_modal/helpers/setting.js @@ -48,6 +48,10 @@ export default { draftMode: { type: Boolean, default: undefined + }, + timedApplyMode: { + type: Boolean, + default: false } }, inject: { @@ -161,7 +165,11 @@ export default { case 'admin': return (k, v) => this.$store.dispatch('pushAdminSetting', { path: k, value: v }) default: - return (k, v) => this.$store.dispatch('setOption', { name: k, value: v }) + 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 () { @@ -195,7 +203,8 @@ export default { } }, canHardReset () { - return this.realSource === 'admin' && this.$store.state.adminSettings.modifiedPaths.has(this.canonPath.join(' -> ')) + 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 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 12cef705..00000000 --- a/src/components/settings_modal/helpers/size_setting.js +++ /dev/null @@ -1,40 +0,0 @@ -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 - } - }, - computed: { - ...Setting.computed, - stateUnit () { - return this.state.replace(/\d+/, '') - }, - stateValue () { - return this.state.replace(/\D+/, '') - } - }, - methods: { - ...Setting.methods, - updateValue (e) { - this.configSink(this.path, parseInt(e.target.value) + this.stateUnit) - }, - updateUnit (e) { - this.configSink(this.path, this.stateValue + e.target.value) - } - } -} diff --git a/src/components/settings_modal/helpers/string_setting.vue b/src/components/settings_modal/helpers/string_setting.vue index 0cfa61ce..7b30d1b9 100644 --- a/src/components/settings_modal/helpers/string_setting.vue +++ b/src/components/settings_modal/helpers/string_setting.vue @@ -17,7 +17,7 @@ </label> <input :id="path" - class="string-input" + class="input string-input" :disabled="shouldBeDisabled" :value="realDraftMode ? draft : state" @change="update" 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 6c3fbaeb..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,20 +43,19 @@ </span> </template> -<script src="./size_setting.js"></script> +<script src="./unit_setting.js"></script> <style lang="scss"> -.SizeSetting { +.UnitSetting { .number-input { max-width: 6.5em; + text-align: right; } - .css-unit-input, - .css-unit-input select { - margin-left: 0.5em; - width: 4em; - max-width: 4em; + .unit-input, + .unit-input select { min-width: 4em; + width: auto; } } diff --git a/src/components/settings_modal/settings_modal.js b/src/components/settings_modal/settings_modal.js index ff58f2c3..63c9b24a 100644 --- a/src/components/settings_modal/settings_modal.js +++ b/src/components/settings_modal/settings_modal.js @@ -4,6 +4,7 @@ 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, isEqual } from 'lodash' import { @@ -53,6 +54,7 @@ const SettingsModal = { Modal, Popover, Checkbox, + ConfirmModal, SettingsModalUserContent: getResettableAsyncComponent( () => import('./settings_modal_user_content.vue'), { @@ -165,6 +167,7 @@ const SettingsModal = { }, computed: { currentSaveStateNotice () { + console.log(this.$store.state.interface.settings.currentSaveStateNotice) return this.$store.state.interface.settings.currentSaveStateNotice }, modalActivated () { diff --git a/src/components/settings_modal/settings_modal.scss b/src/components/settings_modal/settings_modal.scss index 49ef83e0..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,14 @@ .suboptions { margin-top: 0.3em; } + + &.two-column { + column-count: 2; + + > li { + break-inside: avoid; + } + } } .setting-description { diff --git a/src/components/settings_modal/settings_modal.vue b/src/components/settings_modal/settings_modal.vue index 4e7fd931..90dbbde0 100644 --- a/src/components/settings_modal/settings_modal.vue +++ b/src/components/settings_modal/settings_modal.vue @@ -14,7 +14,7 @@ <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') }} @@ -70,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" > @@ -80,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" > @@ -90,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" > @@ -147,6 +147,18 @@ </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 index f94721ec..ce835bf2 100644 --- a/src/components/settings_modal/settings_modal_admin_content.js +++ b/src/components/settings_modal/settings_modal_admin_content.js @@ -3,6 +3,7 @@ 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 { @@ -33,7 +34,8 @@ const SettingsModalAdminContent = { InstanceTab, LimitsTab, - FrontendsTab + FrontendsTab, + EmojiTab }, computed: { user () { diff --git a/src/components/settings_modal/settings_modal_admin_content.scss b/src/components/settings_modal/settings_modal_admin_content.scss index c984d703..a5314fe1 100644 --- a/src/components/settings_modal/settings_modal_admin_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,8 +39,7 @@ .unavailable, .unavailable svg { - color: var(--cRed, $fallback--cRed); - color: $fallback--cRed; + 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 index a7a2ac9a..65e23b7e 100644 --- a/src/components/settings_modal/settings_modal_admin_content.vue +++ b/src/components/settings_modal/settings_modal_admin_content.vue @@ -60,6 +60,14 @@ > <FrontendsTab /> </div> + + <div + :label="$t('admin_dash.tabs.emoji')" + icon="face-smile-beam" + data-tab-name="emoji" + > + <EmojiTab /> + </div> </tab-switcher> </template> diff --git a/src/components/settings_modal/settings_modal_user_content.js b/src/components/settings_modal/settings_modal_user_content.js index 9ac0301f..ebd5329f 100644 --- a/src/components/settings_modal/settings_modal_user_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 index c984d703..a5314fe1 100644 --- a/src/components/settings_modal/settings_modal_user_content.scss +++ b/src/components/settings_modal/settings_modal_user_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,8 +39,7 @@ .unavailable, .unavailable svg { - color: var(--cRed, $fallback--cRed); - color: $fallback--cRed; + color: var(--cRed); } } } diff --git a/src/components/settings_modal/settings_modal_user_content.vue b/src/components/settings_modal/settings_modal_user_content.vue index 0221cccb..1441d892 100644 --- a/src/components/settings_modal/settings_modal_user_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" 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 41d1b54f..c86810d5 100644 --- a/src/components/settings_modal/tabs/filtering_tab.vue +++ b/src/components/settings_modal/tabs/filtering_tab.vue @@ -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 3f2bcb13..e9e8fa72 100644 --- a/src/components/settings_modal/tabs/general_tab.js +++ b/src/components/settings_modal/tabs/general_tab.js @@ -3,7 +3,7 @@ import ChoiceSetting from '../helpers/choice_setting.vue' import ScopeSelector from 'src/components/scope_selector/scope_selector.vue' import IntegerSetting from '../helpers/integer_setting.vue' import FloatSetting from '../helpers/float_setting.vue' -import SizeSetting, { defaultHorizontalUnits } from '../helpers/size_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' @@ -40,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, @@ -64,15 +59,12 @@ const GeneralTab = { ChoiceSetting, IntegerSetting, FloatSetting, - SizeSetting, + UnitSetting, InterfaceLanguageSwitcher, ScopeSelector, ProfileSettingIndicator }, computed: { - horizontalUnits () { - return defaultHorizontalUnits - }, postFormats () { return this.$store.state.instance.postFormats || [] }, @@ -83,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) { diff --git a/src/components/settings_modal/tabs/general_tab.vue b/src/components/settings_modal/tabs/general_tab.vue index f56fa8e0..4ece6cf4 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') }} @@ -98,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"> @@ -269,15 +217,6 @@ {{ $t('settings.no_rich_text_description') }} </BooleanSetting> </li> - <li> - <FloatSetting - v-if="user" - path="emojiReactionsScale" - expert="1" - > - {{ $t('settings.emoji_reactions_scale') }} - </FloatSetting> - </li> <h3>{{ $t('settings.attachments') }}</h3> <li> <BooleanSetting @@ -520,17 +459,3 @@ </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/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 fcb92135..10228888 100644 --- a/src/components/settings_modal/tabs/notifications_tab.vue +++ b/src/components/settings_modal/tabs/notifications_tab.vue @@ -1,6 +1,34 @@ <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> @@ -11,42 +39,201 @@ {{ $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> + <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> - <BooleanSetting path="notificationVisibility.mentions"> - {{ $t('settings.notification_visibility_mentions') }} + <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> @@ -67,6 +254,21 @@ > {{ $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 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 1cc850cb..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> 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 d36d478f..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 @@ -155,6 +160,7 @@ </i18n-t> <input v-model="addAliasTarget" + class="input" > </div> <button @@ -187,6 +193,7 @@ </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> |
