Merge remote-tracking branch 'origin/develop' into migrate/vuex-to-pinia
This commit is contained in:
commit
58e18d48df
489 changed files with 31167 additions and 9871 deletions
257
src/components/settings_modal/admin_tabs/emoji_tab.js
Normal file
257
src/components/settings_modal/admin_tabs/emoji_tab.js
Normal file
|
|
@ -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
|
||||
59
src/components/settings_modal/admin_tabs/emoji_tab.scss
Normal file
59
src/components/settings_modal/admin_tabs/emoji_tab.scss
Normal file
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
358
src/components/settings_modal/admin_tabs/emoji_tab.vue
Normal file
358
src/components/settings_modal/admin_tabs/emoji_tab.vue
Normal file
|
|
@ -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>
|
||||
113
src/components/settings_modal/admin_tabs/frontends_tab.js
Normal file
113
src/components/settings_modal/admin_tabs/frontends_tab.js
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
import BooleanSetting from '../helpers/boolean_setting.vue'
|
||||
import ChoiceSetting from '../helpers/choice_setting.vue'
|
||||
import IntegerSetting from '../helpers/integer_setting.vue'
|
||||
import StringSetting from '../helpers/string_setting.vue'
|
||||
import GroupSetting from '../helpers/group_setting.vue'
|
||||
import Popover from 'src/components/popover/popover.vue'
|
||||
import PanelLoading from 'src/components/panel_loading/panel_loading.vue'
|
||||
|
||||
import SharedComputedObject from '../helpers/shared_computed_object.js'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faGlobe
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(
|
||||
faGlobe
|
||||
)
|
||||
|
||||
const FrontendsTab = {
|
||||
provide () {
|
||||
return {
|
||||
defaultDraftMode: true,
|
||||
defaultSource: 'admin'
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
working: false
|
||||
}
|
||||
},
|
||||
components: {
|
||||
BooleanSetting,
|
||||
ChoiceSetting,
|
||||
IntegerSetting,
|
||||
StringSetting,
|
||||
GroupSetting,
|
||||
PanelLoading,
|
||||
Popover
|
||||
},
|
||||
created () {
|
||||
if (this.user.rights.admin) {
|
||||
this.$store.dispatch('loadFrontendsStuff')
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
frontends () {
|
||||
return this.$store.state.adminSettings.frontends
|
||||
},
|
||||
...SharedComputedObject()
|
||||
},
|
||||
methods: {
|
||||
canInstall (frontend) {
|
||||
const fe = this.frontends.find(f => f.name === frontend.name)
|
||||
if (!fe) return false
|
||||
return fe.refs.includes(frontend.ref)
|
||||
},
|
||||
getSuggestedRef (frontend) {
|
||||
if (this.adminDraft) {
|
||||
const defaultFe = this.adminDraft[':pleroma'][':frontends'][':primary']
|
||||
if (defaultFe?.name === frontend.name && this.canInstall(defaultFe)) {
|
||||
return defaultFe.ref
|
||||
} else {
|
||||
return frontend.refs[0]
|
||||
}
|
||||
} else {
|
||||
return frontend.refs[0]
|
||||
}
|
||||
},
|
||||
update (frontend, suggestRef) {
|
||||
const ref = suggestRef || this.getSuggestedRef(frontend)
|
||||
const { name } = frontend
|
||||
const payload = { name, ref }
|
||||
|
||||
this.working = true
|
||||
this.$store.state.api.backendInteractor.installFrontend({ payload })
|
||||
.finally(() => {
|
||||
this.working = false
|
||||
})
|
||||
.then(async (response) => {
|
||||
this.$store.dispatch('loadFrontendsStuff')
|
||||
if (response.error) {
|
||||
const reason = await response.error.json()
|
||||
this.$store.dispatch('pushGlobalNotice', {
|
||||
level: 'error',
|
||||
messageKey: 'admin_dash.frontend.failure_installing_frontend',
|
||||
messageArgs: {
|
||||
version: name + '/' + ref,
|
||||
reason: reason.error
|
||||
},
|
||||
timeout: 5000
|
||||
})
|
||||
} else {
|
||||
this.$store.dispatch('pushGlobalNotice', {
|
||||
level: 'success',
|
||||
messageKey: 'admin_dash.frontend.success_installing_frontend',
|
||||
messageArgs: {
|
||||
version: name + '/' + ref
|
||||
},
|
||||
timeout: 2000
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
setDefault (frontend, suggestRef) {
|
||||
const ref = suggestRef || this.getSuggestedRef(frontend)
|
||||
const { name } = frontend
|
||||
|
||||
this.$store.commit('updateAdminDraft', { path: [':pleroma', ':frontends', ':primary'], value: { name, ref } })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default FrontendsTab
|
||||
29
src/components/settings_modal/admin_tabs/frontends_tab.scss
Normal file
29
src/components/settings_modal/admin_tabs/frontends_tab.scss
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
.frontends-tab {
|
||||
.cards-list {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.relative {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
position: absolute;
|
||||
background: var(--bg);
|
||||
// fix buttons showing through
|
||||
z-index: 2;
|
||||
opacity: 0.9;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
dd {
|
||||
text-overflow: ellipsis;
|
||||
word-wrap: nowrap;
|
||||
white-space: nowrap;
|
||||
overflow-x: hidden;
|
||||
max-width: 10em;
|
||||
}
|
||||
}
|
||||
229
src/components/settings_modal/admin_tabs/frontends_tab.vue
Normal file
229
src/components/settings_modal/admin_tabs/frontends_tab.vue
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
<template>
|
||||
<div
|
||||
class="frontends-tab"
|
||||
:label="$t('admin_dash.tabs.frontends')"
|
||||
>
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('admin_dash.tabs.frontends') }}</h2>
|
||||
<p>{{ $t('admin_dash.frontend.wip_notice') }}</p>
|
||||
<ul
|
||||
v-if="adminDraft"
|
||||
class="setting-list"
|
||||
>
|
||||
<li>
|
||||
<h3>{{ $t('admin_dash.frontend.default_frontend') }}</h3>
|
||||
<p>{{ $t('admin_dash.frontend.default_frontend_tip') }}</p>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<StringSetting path=":pleroma.:frontends.:primary.name" />
|
||||
</li>
|
||||
<li>
|
||||
<StringSetting path=":pleroma.:frontends.:primary.ref" />
|
||||
</li>
|
||||
<li>
|
||||
<GroupSetting path=":pleroma.:frontends.:primary" />
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<div
|
||||
v-else
|
||||
class="setting-list"
|
||||
>
|
||||
{{ $t('admin_dash.frontend.default_frontend_unavail') }}
|
||||
</div>
|
||||
|
||||
<div class="setting-list relative">
|
||||
<PanelLoading
|
||||
v-if="working"
|
||||
class="overlay"
|
||||
/>
|
||||
<h3>{{ $t('admin_dash.frontend.available_frontends') }}</h3>
|
||||
<ul class="cards-list">
|
||||
<li
|
||||
v-for="frontend in frontends"
|
||||
:key="frontend.name"
|
||||
>
|
||||
<strong>{{ frontend.name }}</strong>
|
||||
{{ ' ' }}
|
||||
<span v-if="adminDraft && adminDraft[':pleroma'][':frontends'][':primary']?.name === frontend.name">
|
||||
<i18n-t
|
||||
v-if="adminDraft && adminDraft[':pleroma'][':frontends'][':primary']?.ref === frontend.refs[0]"
|
||||
scope="global"
|
||||
keypath="admin_dash.frontend.is_default"
|
||||
/>
|
||||
<i18n-t
|
||||
v-else
|
||||
keypath="admin_dash.frontend.is_default_custom"
|
||||
scope="global"
|
||||
>
|
||||
<template #version>
|
||||
<code>{{ adminDraft && adminDraft[':pleroma'][':frontends'][':primary'].ref }}</code>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</span>
|
||||
<dl>
|
||||
<dt>{{ $t('admin_dash.frontend.repository') }}</dt>
|
||||
<dd>
|
||||
<a
|
||||
:href="frontend.git"
|
||||
target="_blank"
|
||||
>{{ frontend.git }}</a>
|
||||
</dd>
|
||||
<template v-if="expertLevel">
|
||||
<dt>{{ $t('admin_dash.frontend.versions') }}</dt>
|
||||
<dd
|
||||
v-for="ref in frontend.refs"
|
||||
:key="ref"
|
||||
>
|
||||
<code>{{ ref }}</code>
|
||||
</dd>
|
||||
</template>
|
||||
<dt v-if="expertLevel">
|
||||
{{ $t('admin_dash.frontend.build_url') }}
|
||||
</dt>
|
||||
<dd v-if="expertLevel">
|
||||
<a
|
||||
:href="frontend.build_url"
|
||||
target="_blank"
|
||||
>{{ frontend.build_url }}</a>
|
||||
</dd>
|
||||
</dl>
|
||||
<div>
|
||||
<span class="btn-group">
|
||||
<button
|
||||
class="button button-default btn"
|
||||
type="button"
|
||||
@click="update(frontend)"
|
||||
>
|
||||
{{
|
||||
frontend.installed
|
||||
? $t('admin_dash.frontend.reinstall')
|
||||
: $t('admin_dash.frontend.install')
|
||||
}}
|
||||
<code>
|
||||
{{
|
||||
getSuggestedRef(frontend)
|
||||
}}
|
||||
</code>
|
||||
</button>
|
||||
<Popover
|
||||
v-if="frontend.refs.length > 1"
|
||||
trigger="click"
|
||||
class="button-dropdown"
|
||||
placement="bottom"
|
||||
>
|
||||
<template #content="{close}">
|
||||
<div class="dropdown-menu">
|
||||
<div
|
||||
v-for="ref in frontend.refs"
|
||||
:key="ref"
|
||||
class="menu-item dropdown-item"
|
||||
>
|
||||
<button
|
||||
class="main-button"
|
||||
@click.prevent="update(frontend, ref)"
|
||||
@click="close"
|
||||
>
|
||||
<span>
|
||||
<i18n-t
|
||||
keypath="admin_dash.frontend.install_version"
|
||||
scope="global"
|
||||
>
|
||||
<template #version>
|
||||
<code>{{ ref }}</code>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #trigger>
|
||||
<button
|
||||
class="button button-default btn dropdown-button"
|
||||
type="button"
|
||||
:title="$t('admin_dash.frontend.more_install_options')"
|
||||
>
|
||||
<FAIcon icon="chevron-down" />
|
||||
</button>
|
||||
</template>
|
||||
</Popover>
|
||||
</span>
|
||||
<span
|
||||
v-if="frontend.installed && frontend.name !== 'admin-fe'"
|
||||
class="btn-group"
|
||||
>
|
||||
<button
|
||||
class="button button-default btn"
|
||||
type="button"
|
||||
:disabled="
|
||||
!adminDraft || adminDraft[':pleroma'][':frontends'][':primary']?.name === frontend.name &&
|
||||
adminDraft[':pleroma'][':frontends'][':primary']?.ref === frontend.refs[0]
|
||||
"
|
||||
@click="setDefault(frontend)"
|
||||
>
|
||||
{{
|
||||
$t('admin_dash.frontend.set_default')
|
||||
}}
|
||||
<code>
|
||||
{{
|
||||
getSuggestedRef(frontend)
|
||||
}}
|
||||
</code>
|
||||
</button>
|
||||
{{ ' ' }}
|
||||
<Popover
|
||||
v-if="frontend.refs.length > 1"
|
||||
trigger="click"
|
||||
class="button-dropdown"
|
||||
placement="bottom"
|
||||
>
|
||||
<template #content="{close}">
|
||||
<div class="dropdown-menu">
|
||||
<div
|
||||
v-for="ref in frontend.installedRefs || frontend.refs"
|
||||
:key="ref"
|
||||
class="menu-item dropdown-item"
|
||||
>
|
||||
<button
|
||||
class="main-button"
|
||||
@click.prevent="setDefault(frontend, ref)"
|
||||
@click="close"
|
||||
>
|
||||
<span>
|
||||
<i18n-t
|
||||
keypath="admin_dash.frontend.set_default_version"
|
||||
scope="global"
|
||||
>
|
||||
<template #version>
|
||||
<code>{{ ref }}</code>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #trigger>
|
||||
<button
|
||||
class="button button-default btn dropdown-button"
|
||||
type="button"
|
||||
:title="$t('admin_dash.frontend.more_default_options')"
|
||||
>
|
||||
<FAIcon icon="chevron-down" />
|
||||
</button>
|
||||
</template>
|
||||
</Popover>
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./frontends_tab.js"></script>
|
||||
|
||||
<style lang="scss" src="./frontends_tab.scss"></style>
|
||||
38
src/components/settings_modal/admin_tabs/instance_tab.js
Normal file
38
src/components/settings_modal/admin_tabs/instance_tab.js
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import BooleanSetting from '../helpers/boolean_setting.vue'
|
||||
import ChoiceSetting from '../helpers/choice_setting.vue'
|
||||
import IntegerSetting from '../helpers/integer_setting.vue'
|
||||
import StringSetting from '../helpers/string_setting.vue'
|
||||
import GroupSetting from '../helpers/group_setting.vue'
|
||||
import AttachmentSetting from '../helpers/attachment_setting.vue'
|
||||
|
||||
import SharedComputedObject from '../helpers/shared_computed_object.js'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faGlobe
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(
|
||||
faGlobe
|
||||
)
|
||||
|
||||
const InstanceTab = {
|
||||
provide () {
|
||||
return {
|
||||
defaultDraftMode: true,
|
||||
defaultSource: 'admin'
|
||||
}
|
||||
},
|
||||
components: {
|
||||
BooleanSetting,
|
||||
ChoiceSetting,
|
||||
IntegerSetting,
|
||||
StringSetting,
|
||||
AttachmentSetting,
|
||||
GroupSetting
|
||||
},
|
||||
computed: {
|
||||
...SharedComputedObject()
|
||||
}
|
||||
}
|
||||
|
||||
export default InstanceTab
|
||||
206
src/components/settings_modal/admin_tabs/instance_tab.vue
Normal file
206
src/components/settings_modal/admin_tabs/instance_tab.vue
Normal file
|
|
@ -0,0 +1,206 @@
|
|||
<template>
|
||||
<div :label="$t('admin_dash.tabs.instance')">
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('admin_dash.instance.instance') }}</h2>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<StringSetting path=":pleroma.:instance.:name" />
|
||||
</li>
|
||||
<!-- See https://git.pleroma.social/pleroma/pleroma/-/merge_requests/3963 -->
|
||||
<li v-if="adminDraft[':pleroma'][':instance'][':favicon'] !== undefined">
|
||||
<AttachmentSetting
|
||||
compact
|
||||
path=":pleroma.:instance.:favicon"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<StringSetting path=":pleroma.:instance.:email" />
|
||||
</li>
|
||||
<li>
|
||||
<StringSetting path=":pleroma.:instance.:description" />
|
||||
</li>
|
||||
<li>
|
||||
<StringSetting path=":pleroma.:instance.:short_description" />
|
||||
</li>
|
||||
<li>
|
||||
<AttachmentSetting
|
||||
compact
|
||||
path=":pleroma.:instance.:instance_thumbnail"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<AttachmentSetting path=":pleroma.:instance.:background_image" />
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('admin_dash.instance.registrations') }}</h2>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<BooleanSetting path=":pleroma.:instance.:registrations_open" />
|
||||
<ul class="setting-list suboptions">
|
||||
<li>
|
||||
<BooleanSetting
|
||||
path=":pleroma.:instance.:invites_enabled"
|
||||
parent-path=":pleroma.:instance.:registrations_open"
|
||||
parent-invert
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path=":pleroma.:instance.:birthday_required" />
|
||||
<ul class="setting-list suboptions">
|
||||
<li>
|
||||
<IntegerSetting
|
||||
path=":pleroma.:instance.:birthday_min_age"
|
||||
parent-path=":pleroma.:instance.:birthday_required"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path=":pleroma.:instance.:account_activation_required" />
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path=":pleroma.:instance.:account_approval_required" />
|
||||
</li>
|
||||
<li>
|
||||
<h3>{{ $t('admin_dash.instance.captcha_header') }}</h3>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<BooleanSetting :path="[':pleroma', 'Pleroma.Captcha', ':enabled']" />
|
||||
<ul class="setting-list suboptions">
|
||||
<li>
|
||||
<ChoiceSetting
|
||||
:path="[':pleroma', 'Pleroma.Captcha', ':method']"
|
||||
:parent-path="[':pleroma', 'Pleroma.Captcha', ':enabled']"
|
||||
:option-label-map="{
|
||||
'Pleroma.Captcha.Native': $t('admin_dash.captcha.native'),
|
||||
'Pleroma.Captcha.Kocaptcha': $t('admin_dash.captcha.kocaptcha')
|
||||
}"
|
||||
/>
|
||||
<IntegerSetting
|
||||
:path="[':pleroma', 'Pleroma.Captcha', ':seconds_valid']"
|
||||
:parent-path="[':pleroma', 'Pleroma.Captcha', ':enabled']"
|
||||
/>
|
||||
</li>
|
||||
<li
|
||||
v-if="adminDraft[':pleroma']['Pleroma.Captcha'][':enabled'] && adminDraft[':pleroma']['Pleroma.Captcha'][':method'] === 'Pleroma.Captcha.Kocaptcha'"
|
||||
>
|
||||
<h4>{{ $t('admin_dash.instance.kocaptcha') }}</h4>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<StringSetting :path="[':pleroma', 'Pleroma.Captcha.Kocaptcha', ':endpoint']" />
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('admin_dash.instance.access') }}</h2>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<BooleanSetting
|
||||
override-backend-description
|
||||
override-backend-description-label
|
||||
path=":pleroma.:instance.:public"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<ChoiceSetting
|
||||
override-backend-description
|
||||
override-backend-description-label
|
||||
path=":pleroma.:instance.:limit_to_local_content"
|
||||
/>
|
||||
</li>
|
||||
<li v-if="expertLevel">
|
||||
<h3>{{ $t('admin_dash.instance.restrict.header') }}</h3>
|
||||
<p>
|
||||
{{ $t('admin_dash.instance.restrict.description') }}
|
||||
</p>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<h4>{{ $t('admin_dash.instance.restrict.timelines') }}</h4>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<BooleanSetting
|
||||
path=":pleroma.:restrict_unauthenticated.:timelines.:local"
|
||||
indeterminate-state=":if_instance_is_private"
|
||||
swap-description-and-label
|
||||
hide-description
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting
|
||||
path=":pleroma.:restrict_unauthenticated.:timelines.:federated"
|
||||
indeterminate-state=":if_instance_is_private"
|
||||
swap-description-and-label
|
||||
hide-description
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<GroupSetting path=":pleroma.:restrict_unauthenticated.:timelines" />
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<h4>{{ $t('admin_dash.instance.restrict.profiles') }}</h4>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<BooleanSetting
|
||||
path=":pleroma.:restrict_unauthenticated.:profiles.:local"
|
||||
indeterminate-state=":if_instance_is_private"
|
||||
swap-description-and-label
|
||||
hide-description
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting
|
||||
path=":pleroma.:restrict_unauthenticated.:profiles.:remote"
|
||||
indeterminate-state=":if_instance_is_private"
|
||||
swap-description-and-label
|
||||
hide-description
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<GroupSetting path=":pleroma.:restrict_unauthenticated.:profiles" />
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<h4>{{ $t('admin_dash.instance.restrict.activities') }}</h4>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<BooleanSetting
|
||||
path=":pleroma.:restrict_unauthenticated.:activities.:local"
|
||||
indeterminate-state=":if_instance_is_private"
|
||||
swap-description-and-label
|
||||
hide-description
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting
|
||||
path=":pleroma.:restrict_unauthenticated.:activities.:remote"
|
||||
indeterminate-state=":if_instance_is_private"
|
||||
swap-description-and-label
|
||||
hide-description
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<GroupSetting path=":pleroma.:restrict_unauthenticated.:activities" />
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./instance_tab.js"></script>
|
||||
28
src/components/settings_modal/admin_tabs/limits_tab.js
Normal file
28
src/components/settings_modal/admin_tabs/limits_tab.js
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import BooleanSetting from '../helpers/boolean_setting.vue'
|
||||
import ChoiceSetting from '../helpers/choice_setting.vue'
|
||||
import IntegerSetting from '../helpers/integer_setting.vue'
|
||||
import StringSetting from '../helpers/string_setting.vue'
|
||||
|
||||
import SharedComputedObject from '../helpers/shared_computed_object.js'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faGlobe
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(
|
||||
faGlobe
|
||||
)
|
||||
|
||||
const LimitsTab = {
|
||||
components: {
|
||||
BooleanSetting,
|
||||
ChoiceSetting,
|
||||
IntegerSetting,
|
||||
StringSetting
|
||||
},
|
||||
computed: {
|
||||
...SharedComputedObject()
|
||||
}
|
||||
}
|
||||
|
||||
export default LimitsTab
|
||||
136
src/components/settings_modal/admin_tabs/limits_tab.vue
Normal file
136
src/components/settings_modal/admin_tabs/limits_tab.vue
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
<template>
|
||||
<div :label="$t('admin_dash.tabs.limits')">
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('admin_dash.limits.arbitrary_limits') }}</h2>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<h3>{{ $t('admin_dash.limits.posts') }}</h3>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<IntegerSetting
|
||||
source="admin"
|
||||
path=":pleroma.:instance.:limit"
|
||||
draft-mode
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<IntegerSetting
|
||||
source="admin"
|
||||
path=":pleroma.:instance.:remote_limit"
|
||||
expert="1"
|
||||
draft-mode
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<h3>{{ $t('admin_dash.limits.uploads') }}</h3>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<IntegerSetting
|
||||
source="admin"
|
||||
path=":pleroma.:instance.:description_limit"
|
||||
draft-mode
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<IntegerSetting
|
||||
source="admin"
|
||||
path=":pleroma.:instance.:upload_limit"
|
||||
draft-mode
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<IntegerSetting
|
||||
source="admin"
|
||||
path=":pleroma.:instance.:max_media_attachments"
|
||||
draft-mode
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<h3>{{ $t('admin_dash.limits.users') }}</h3>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<IntegerSetting
|
||||
source="admin"
|
||||
path=":pleroma.:instance.:max_pinned_statuses"
|
||||
draft-mode
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<IntegerSetting
|
||||
source="admin"
|
||||
path=":pleroma.:instance.:user_bio_length"
|
||||
draft-mode
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<IntegerSetting
|
||||
source="admin"
|
||||
path=":pleroma.:instance.:user_name_length"
|
||||
draft-mode
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<h4>{{ $t('admin_dash.limits.profile_fields') }}</h4>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<IntegerSetting
|
||||
source="admin"
|
||||
path=":pleroma.:instance.:max_account_fields"
|
||||
draft-mode
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<IntegerSetting
|
||||
source="admin"
|
||||
path=":pleroma.:instance.:max_remote_account_fields"
|
||||
draft-mode
|
||||
expert="1"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<IntegerSetting
|
||||
source="admin"
|
||||
path=":pleroma.:instance.:account_field_name_length"
|
||||
draft-mode
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<IntegerSetting
|
||||
source="admin"
|
||||
path=":pleroma.:instance.:account_field_value_length"
|
||||
draft-mode
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<h4>{{ $t('admin_dash.limits.user_uploads') }}</h4>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<IntegerSetting
|
||||
source="admin"
|
||||
path=":pleroma.:instance.:avatar_upload_limit"
|
||||
draft-mode
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<IntegerSetting
|
||||
source="admin"
|
||||
path=":pleroma.:instance.:banner_upload_limit"
|
||||
draft-mode
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./limits_tab.js"></script>
|
||||
44
src/components/settings_modal/helpers/attachment_setting.js
Normal file
44
src/components/settings_modal/helpers/attachment_setting.js
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import Setting from './setting.js'
|
||||
import { fileTypeExt } from 'src/services/file_type/file_type.service.js'
|
||||
import MediaUpload from 'src/components/media_upload/media_upload.vue'
|
||||
import Attachment from 'src/components/attachment/attachment.vue'
|
||||
|
||||
export default {
|
||||
...Setting,
|
||||
props: {
|
||||
...Setting.props,
|
||||
compact: Boolean,
|
||||
acceptTypes: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'image/*'
|
||||
}
|
||||
},
|
||||
components: {
|
||||
...Setting.components,
|
||||
MediaUpload,
|
||||
Attachment
|
||||
},
|
||||
computed: {
|
||||
...Setting.computed,
|
||||
attachment () {
|
||||
const path = this.realDraftMode ? this.draft : this.state
|
||||
// The "server" part is primarily for local dev, but could be useful for alt-domain or multiuser usage.
|
||||
const url = path.includes('://') ? path : this.$store.state.instance.server + path
|
||||
return {
|
||||
mimetype: fileTypeExt(url),
|
||||
url
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...Setting.methods,
|
||||
setMediaFile (fileInfo) {
|
||||
if (this.realDraftMode) {
|
||||
this.draft = fileInfo.url
|
||||
} else {
|
||||
this.configSink(this.path, fileInfo.url)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
122
src/components/settings_modal/helpers/attachment_setting.vue
Normal file
122
src/components/settings_modal/helpers/attachment_setting.vue
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
<template>
|
||||
<span
|
||||
v-if="matchesExpertLevel"
|
||||
class="AttachmentSetting"
|
||||
:class="{ '-compact': compact }"
|
||||
>
|
||||
<label
|
||||
:for="path"
|
||||
:class="{ 'faint': shouldBeDisabled }"
|
||||
>
|
||||
<template v-if="backendDescriptionLabel">
|
||||
{{ backendDescriptionLabel + ' ' }}
|
||||
</template>
|
||||
<template v-else-if="source === 'admin'">
|
||||
MISSING LABEL FOR {{ path }}
|
||||
</template>
|
||||
<slot v-else />
|
||||
|
||||
</label>
|
||||
<p
|
||||
v-if="backendDescriptionDescription"
|
||||
class="setting-description"
|
||||
:class="{ 'faint': shouldBeDisabled }"
|
||||
>
|
||||
{{ backendDescriptionDescription + ' ' }}
|
||||
</p>
|
||||
<div class="attachment-input">
|
||||
<div class="controls control-field">
|
||||
<label for="path">{{ $t('settings.url') }}</label>
|
||||
<input
|
||||
:id="path"
|
||||
class="input string-input"
|
||||
:disabled="shouldBeDisabled"
|
||||
:value="realDraftMode ? draft : state"
|
||||
@change="update"
|
||||
>
|
||||
{{ ' ' }}
|
||||
<ModifiedIndicator
|
||||
:changed="isChanged"
|
||||
:onclick="reset"
|
||||
/>
|
||||
<ProfileSettingIndicator :is-profile="isProfileSetting" />
|
||||
</div>
|
||||
<div v-if="!compact">{{ $t('settings.preview') }}</div>
|
||||
<Attachment
|
||||
class="attachment"
|
||||
:compact="compact"
|
||||
:attachment="attachment"
|
||||
size="small"
|
||||
hide-description
|
||||
/>
|
||||
<div class="controls control-upload">
|
||||
<MediaUpload
|
||||
ref="mediaUpload"
|
||||
class="media-upload-icon"
|
||||
normal-button
|
||||
:accept-types="acceptTypes"
|
||||
@uploaded="setMediaFile"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DraftButtons />
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script src="./attachment_setting.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
.AttachmentSetting {
|
||||
.attachment {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 15em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.attachment-input {
|
||||
margin-left: 1em;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 20em;
|
||||
}
|
||||
|
||||
&.-compact {
|
||||
.attachment-input {
|
||||
flex-direction: row;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.attachment {
|
||||
flex: 0;
|
||||
order: 0;
|
||||
display: block;
|
||||
min-width: 4em;
|
||||
height: 4em;
|
||||
align-self: center;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.control-field {
|
||||
order: 1;
|
||||
min-width: 12em;
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
|
||||
.control-upload {
|
||||
order: 2;
|
||||
min-width: 12em;
|
||||
padding: 0 0.5em;
|
||||
}
|
||||
}
|
||||
|
||||
.controls {
|
||||
margin-bottom: 0.5em;
|
||||
|
||||
input,
|
||||
button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,56 +1,31 @@
|
|||
import { get, set } from 'lodash'
|
||||
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||
import ModifiedIndicator from './modified_indicator.vue'
|
||||
import ServerSideIndicator from './server_side_indicator.vue'
|
||||
import Setting from './setting.js'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Checkbox,
|
||||
ModifiedIndicator,
|
||||
ServerSideIndicator
|
||||
...Setting,
|
||||
props: {
|
||||
...Setting.props,
|
||||
indeterminateState: [String, Object]
|
||||
},
|
||||
components: {
|
||||
...Setting.components,
|
||||
Checkbox
|
||||
},
|
||||
props: [
|
||||
'path',
|
||||
'disabled',
|
||||
'expert'
|
||||
],
|
||||
computed: {
|
||||
pathDefault () {
|
||||
const [firstSegment, ...rest] = this.path.split('.')
|
||||
return [firstSegment + 'DefaultValue', ...rest].join('.')
|
||||
},
|
||||
state () {
|
||||
const value = get(this.$parent, this.path)
|
||||
if (value === undefined) {
|
||||
return this.defaultState
|
||||
} else {
|
||||
return value
|
||||
}
|
||||
},
|
||||
defaultState () {
|
||||
return get(this.$parent, this.pathDefault)
|
||||
},
|
||||
isServerSide () {
|
||||
return this.path.startsWith('serverSide_')
|
||||
},
|
||||
isChanged () {
|
||||
return !this.path.startsWith('serverSide_') && this.state !== this.defaultState
|
||||
},
|
||||
matchesExpertLevel () {
|
||||
return (this.expert || 0) <= this.$parent.expertLevel
|
||||
...Setting.computed,
|
||||
isIndeterminate () {
|
||||
return this.visibleState === this.indeterminateState
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
update (e) {
|
||||
const [firstSegment, ...rest] = this.path.split('.')
|
||||
set(this.$parent, this.path, e)
|
||||
// Updating nested properties does not trigger update on its parent.
|
||||
// probably still not as reliable, but works for depth=1 at least
|
||||
if (rest.length > 0) {
|
||||
set(this.$parent, firstSegment, { ...get(this.$parent, firstSegment) })
|
||||
...Setting.methods,
|
||||
getValue (e) {
|
||||
// Basic tri-state toggle implementation
|
||||
if (!!this.indeterminateState && !e && this.visibleState === true) {
|
||||
// If we have indeterminate state, switching from true to false first goes through indeterminate
|
||||
return this.indeterminateState
|
||||
}
|
||||
},
|
||||
reset () {
|
||||
set(this.$parent, this.path, this.defaultState)
|
||||
return e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,23 +4,37 @@
|
|||
class="BooleanSetting"
|
||||
>
|
||||
<Checkbox
|
||||
:model-value="state"
|
||||
:disabled="disabled"
|
||||
:model-value="visibleState"
|
||||
:disabled="shouldBeDisabled"
|
||||
:indeterminate="isIndeterminate"
|
||||
@update:modelValue="update"
|
||||
>
|
||||
<span
|
||||
v-if="!!$slots.default"
|
||||
class="label"
|
||||
:class="{ 'faint': shouldBeDisabled }"
|
||||
>
|
||||
<slot />
|
||||
<template v-if="backendDescriptionLabel">
|
||||
{{ backendDescriptionLabel }}
|
||||
</template>
|
||||
<template v-else-if="source === 'admin'">
|
||||
MISSING LABEL FOR {{ path }}
|
||||
</template>
|
||||
<slot v-else />
|
||||
</span>
|
||||
{{ ' ' }}
|
||||
<ModifiedIndicator
|
||||
:changed="isChanged"
|
||||
:onclick="reset"
|
||||
/>
|
||||
<ServerSideIndicator :server-side="isServerSide" />
|
||||
</Checkbox>
|
||||
<ModifiedIndicator
|
||||
:changed="isChanged"
|
||||
:onclick="reset"
|
||||
/>
|
||||
<ProfileSettingIndicator :is-profile="isProfileSetting" />
|
||||
<DraftButtons />
|
||||
<p
|
||||
v-if="backendDescriptionDescription"
|
||||
class="setting-description"
|
||||
:class="{ 'faint': shouldBeDisabled }"
|
||||
>
|
||||
{{ backendDescriptionDescription + ' ' }}
|
||||
</p>
|
||||
</label>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,51 +1,41 @@
|
|||
import { get, set } from 'lodash'
|
||||
import Select from 'src/components/select/select.vue'
|
||||
import ModifiedIndicator from './modified_indicator.vue'
|
||||
import ServerSideIndicator from './server_side_indicator.vue'
|
||||
import Setting from './setting.js'
|
||||
|
||||
export default {
|
||||
...Setting,
|
||||
components: {
|
||||
Select,
|
||||
ModifiedIndicator,
|
||||
ServerSideIndicator
|
||||
...Setting.components,
|
||||
Select
|
||||
},
|
||||
props: {
|
||||
...Setting.props,
|
||||
options: {
|
||||
type: Array,
|
||||
required: false
|
||||
},
|
||||
optionLabelMap: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: {}
|
||||
}
|
||||
},
|
||||
props: [
|
||||
'path',
|
||||
'disabled',
|
||||
'options',
|
||||
'expert'
|
||||
],
|
||||
computed: {
|
||||
pathDefault () {
|
||||
const [firstSegment, ...rest] = this.path.split('.')
|
||||
return [firstSegment + 'DefaultValue', ...rest].join('.')
|
||||
},
|
||||
state () {
|
||||
const value = get(this.$parent, this.path)
|
||||
if (value === undefined) {
|
||||
return this.defaultState
|
||||
} else {
|
||||
return value
|
||||
...Setting.computed,
|
||||
realOptions () {
|
||||
if (this.realSource === 'admin') {
|
||||
return this.backendDescriptionSuggestions.map(x => ({
|
||||
key: x,
|
||||
value: x,
|
||||
label: this.optionLabelMap[x] || x
|
||||
}))
|
||||
}
|
||||
},
|
||||
defaultState () {
|
||||
return get(this.$parent, this.pathDefault)
|
||||
},
|
||||
isServerSide () {
|
||||
return this.path.startsWith('serverSide_')
|
||||
},
|
||||
isChanged () {
|
||||
return !this.path.startsWith('serverSide_') && this.state !== this.defaultState
|
||||
},
|
||||
matchesExpertLevel () {
|
||||
return (this.expert || 0) <= this.$parent.expertLevel
|
||||
return this.options
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
update (e) {
|
||||
set(this.$parent, this.path, e)
|
||||
},
|
||||
reset () {
|
||||
set(this.$parent, this.path, this.defaultState)
|
||||
...Setting.methods,
|
||||
getValue (e) {
|
||||
return e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,15 +3,20 @@
|
|||
v-if="matchesExpertLevel"
|
||||
class="ChoiceSetting"
|
||||
>
|
||||
<slot />
|
||||
<template v-if="backendDescriptionLabel">
|
||||
{{ backendDescriptionLabel }}
|
||||
</template>
|
||||
<template v-else>
|
||||
<slot />
|
||||
</template>
|
||||
{{ ' ' }}
|
||||
<Select
|
||||
:model-value="state"
|
||||
:model-value="realDraftMode ? draft :state"
|
||||
:disabled="disabled"
|
||||
@update:modelValue="update"
|
||||
>
|
||||
<option
|
||||
v-for="option in options"
|
||||
v-for="option in realOptions"
|
||||
:key="option.key"
|
||||
:value="option.value"
|
||||
>
|
||||
|
|
@ -23,7 +28,14 @@
|
|||
:changed="isChanged"
|
||||
:onclick="reset"
|
||||
/>
|
||||
<ServerSideIndicator :server-side="isServerSide" />
|
||||
<ProfileSettingIndicator :is-profile="isProfileSetting" />
|
||||
<DraftButtons />
|
||||
<p
|
||||
v-if="backendDescriptionDescription"
|
||||
class="setting-description"
|
||||
>
|
||||
{{ backendDescriptionDescription + ' ' }}
|
||||
</p>
|
||||
</label>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
88
src/components/settings_modal/helpers/draft_buttons.vue
Normal file
88
src/components/settings_modal/helpers/draft_buttons.vue
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
<!-- this is a helper exclusive to Setting components -->
|
||||
<!-- TODO make it reusable -->
|
||||
<template>
|
||||
<span
|
||||
class="DraftButtons"
|
||||
>
|
||||
<Popover
|
||||
v-if="$parent.isDirty"
|
||||
trigger="hover"
|
||||
normal-button
|
||||
:trigger-attrs="{ 'aria-label': $t('settings.commit_value_tooltip') }"
|
||||
@click="$parent.commitDraft"
|
||||
>
|
||||
<template #trigger>
|
||||
{{ $t('settings.commit_value') }}
|
||||
</template>
|
||||
<template #content>
|
||||
<div class="modified-tooltip">
|
||||
{{ $t('settings.commit_value_tooltip') }}
|
||||
</div>
|
||||
</template>
|
||||
</Popover>
|
||||
<Popover
|
||||
v-if="$parent.isDirty"
|
||||
trigger="hover"
|
||||
normal-button
|
||||
:trigger-attrs="{ 'aria-label': $t('settings.reset_value_tooltip') }"
|
||||
@click="$parent.reset"
|
||||
>
|
||||
<template #trigger>
|
||||
{{ $t('settings.reset_value') }}
|
||||
</template>
|
||||
<template #content>
|
||||
<div class="modified-tooltip">
|
||||
{{ $t('settings.reset_value_tooltip') }}
|
||||
</div>
|
||||
</template>
|
||||
</Popover>
|
||||
<Popover
|
||||
v-if="$parent.canHardReset"
|
||||
trigger="hover"
|
||||
normal-button
|
||||
:trigger-attrs="{ 'aria-label': $t('settings.hard_reset_value_tooltip') }"
|
||||
@click="$parent.hardReset"
|
||||
>
|
||||
<template #trigger>
|
||||
{{ $t('settings.hard_reset_value') }}
|
||||
</template>
|
||||
<template #content>
|
||||
<div class="modified-tooltip">
|
||||
{{ $t('settings.hard_reset_value_tooltip') }}
|
||||
</div>
|
||||
</template>
|
||||
</Popover>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Popover from 'src/components/popover/popover.vue'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { faWrench } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(
|
||||
faWrench
|
||||
)
|
||||
|
||||
export default {
|
||||
components: { Popover },
|
||||
props: ['changed']
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.DraftButtons {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
|
||||
.button-default {
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
}
|
||||
|
||||
.draft-tooltip {
|
||||
margin: 0.5em 1em;
|
||||
min-width: 10em;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
236
src/components/settings_modal/helpers/emoji_editing_popover.vue
Normal file
236
src/components/settings_modal/helpers/emoji_editing_popover.vue
Normal file
|
|
@ -0,0 +1,236 @@
|
|||
<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: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
|
||||
newUpload: Boolean,
|
||||
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
packName: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
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>
|
||||
13
src/components/settings_modal/helpers/group_setting.js
Normal file
13
src/components/settings_modal/helpers/group_setting.js
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { isEqual } from 'lodash'
|
||||
|
||||
import Setting from './setting.js'
|
||||
|
||||
export default {
|
||||
...Setting,
|
||||
computed: {
|
||||
...Setting.computed,
|
||||
isDirty () {
|
||||
return !isEqual(this.state, this.draft)
|
||||
}
|
||||
}
|
||||
}
|
||||
15
src/components/settings_modal/helpers/group_setting.vue
Normal file
15
src/components/settings_modal/helpers/group_setting.vue
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<template>
|
||||
<span
|
||||
v-if="matchesExpertLevel"
|
||||
class="GroupSetting"
|
||||
>
|
||||
<ModifiedIndicator
|
||||
:changed="isChanged"
|
||||
:onclick="reset"
|
||||
/>
|
||||
<ProfileSettingIndicator :is-profile="isProfileSetting" />
|
||||
<DraftButtons />
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script src="./group_setting.js"></script>
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<NumberSetting
|
||||
v-bind="$attrs"
|
||||
truncate="1"
|
||||
:truncate="1"
|
||||
>
|
||||
<slot />
|
||||
</NumberSetting>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,56 +1,39 @@
|
|||
import { get, set } from 'lodash'
|
||||
import ModifiedIndicator from './modified_indicator.vue'
|
||||
import Setting from './setting.js'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ModifiedIndicator
|
||||
},
|
||||
...Setting,
|
||||
props: {
|
||||
path: String,
|
||||
disabled: Boolean,
|
||||
min: Number,
|
||||
step: Number,
|
||||
truncate: Number,
|
||||
expert: [Number, String]
|
||||
},
|
||||
computed: {
|
||||
pathDefault () {
|
||||
const [firstSegment, ...rest] = this.path.split('.')
|
||||
return [firstSegment + 'DefaultValue', ...rest].join('.')
|
||||
...Setting.props,
|
||||
min: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 1
|
||||
},
|
||||
parent () {
|
||||
return this.$parent.$parent
|
||||
max: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 1
|
||||
},
|
||||
state () {
|
||||
const value = get(this.parent, this.path)
|
||||
if (value === undefined) {
|
||||
return this.defaultState
|
||||
} else {
|
||||
return value
|
||||
}
|
||||
step: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 1
|
||||
},
|
||||
defaultState () {
|
||||
return get(this.parent, this.pathDefault)
|
||||
},
|
||||
isChanged () {
|
||||
return this.state !== this.defaultState
|
||||
},
|
||||
matchesExpertLevel () {
|
||||
return (this.expert || 0) <= this.parent.expertLevel
|
||||
truncate: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 1
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
truncateValue (value) {
|
||||
if (!this.truncate) {
|
||||
return value
|
||||
...Setting.methods,
|
||||
getValue (e) {
|
||||
if (!this.truncate === 1) {
|
||||
return parseInt(e.target.value)
|
||||
} else if (this.truncate > 1) {
|
||||
return Math.trunc(e.target.value / this.truncate) * this.truncate
|
||||
}
|
||||
|
||||
return Math.trunc(value / this.truncate) * this.truncate
|
||||
},
|
||||
update (e) {
|
||||
set(this.parent, this.path, this.truncateValue(parseFloat(e.target.value)))
|
||||
},
|
||||
reset () {
|
||||
set(this.parent, this.path, this.defaultState)
|
||||
return parseFloat(e.target.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,17 +3,27 @@
|
|||
v-if="matchesExpertLevel"
|
||||
class="NumberSetting"
|
||||
>
|
||||
<label :for="path">
|
||||
<slot />
|
||||
<label
|
||||
:for="path"
|
||||
:class="{ 'faint': shouldBeDisabled }"
|
||||
>
|
||||
<template v-if="backendDescriptionLabel">
|
||||
{{ backendDescriptionLabel + ' ' }}
|
||||
</template>
|
||||
<template v-else-if="source === 'admin'">
|
||||
MISSING LABEL FOR {{ path }}
|
||||
</template>
|
||||
<slot v-else />
|
||||
</label>
|
||||
{{ ' ' }}
|
||||
<input
|
||||
:id="path"
|
||||
class="number-input"
|
||||
class="input number-input"
|
||||
type="number"
|
||||
:step="step || 1"
|
||||
:disabled="disabled"
|
||||
:disabled="shouldBeDisabled"
|
||||
:min="min || 0"
|
||||
:value="state"
|
||||
:value="realDraftMode ? draft :state"
|
||||
@change="update"
|
||||
>
|
||||
{{ ' ' }}
|
||||
|
|
@ -21,6 +31,15 @@
|
|||
:changed="isChanged"
|
||||
:onclick="reset"
|
||||
/>
|
||||
<ProfileSettingIndicator :is-profile="isProfileSetting" />
|
||||
<DraftButtons />
|
||||
<p
|
||||
v-if="backendDescriptionDescription"
|
||||
class="setting-description"
|
||||
:class="{ 'faint': shouldBeDisabled }"
|
||||
>
|
||||
{{ backendDescriptionDescription + ' ' }}
|
||||
</p>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<span
|
||||
v-if="serverSide"
|
||||
class="ServerSideIndicator"
|
||||
v-if="isProfile"
|
||||
class="ProfileSettingIndicator"
|
||||
>
|
||||
<Popover
|
||||
trigger="hover"
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
/>
|
||||
</template>
|
||||
<template #content>
|
||||
<div class="serverside-tooltip">
|
||||
<div class="profilesetting-tooltip">
|
||||
{{ $t('settings.setting_server_side') }}
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -33,17 +33,17 @@ library.add(
|
|||
|
||||
export default {
|
||||
components: { Popover },
|
||||
props: ['serverSide']
|
||||
props: ['isProfile']
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.ServerSideIndicator {
|
||||
.ProfileSettingIndicator {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.serverside-tooltip {
|
||||
.profilesetting-tooltip {
|
||||
margin: 0.5em 1em;
|
||||
min-width: 10em;
|
||||
text-align: center;
|
||||
262
src/components/settings_modal/helpers/setting.js
Normal file
262
src/components/settings_modal/helpers/setting.js
Normal file
|
|
@ -0,0 +1,262 @@
|
|||
import ModifiedIndicator from './modified_indicator.vue'
|
||||
import ProfileSettingIndicator from './profile_setting_indicator.vue'
|
||||
import DraftButtons from './draft_buttons.vue'
|
||||
import { get, set, cloneDeep } from 'lodash'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ModifiedIndicator,
|
||||
DraftButtons,
|
||||
ProfileSettingIndicator
|
||||
},
|
||||
props: {
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
path: {
|
||||
type: [String, Array],
|
||||
required: false
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
parentPath: {
|
||||
type: [String, Array]
|
||||
},
|
||||
parentInvert: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
expert: {
|
||||
type: [Number, String],
|
||||
default: 0
|
||||
},
|
||||
source: {
|
||||
type: String,
|
||||
default: undefined
|
||||
},
|
||||
hideDescription: {
|
||||
type: Boolean
|
||||
},
|
||||
swapDescriptionAndLabel: {
|
||||
type: Boolean
|
||||
},
|
||||
overrideBackendDescription: {
|
||||
type: Boolean
|
||||
},
|
||||
overrideBackendDescriptionLabel: {
|
||||
type: Boolean
|
||||
},
|
||||
draftMode: {
|
||||
type: Boolean,
|
||||
default: undefined
|
||||
},
|
||||
timedApplyMode: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
inject: {
|
||||
defaultSource: {
|
||||
default: 'default'
|
||||
},
|
||||
defaultDraftMode: {
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
localDraft: null
|
||||
}
|
||||
},
|
||||
created () {
|
||||
if (this.realDraftMode && (this.realSource !== 'admin' || this.path == null)) {
|
||||
this.draft = this.state
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
draft: {
|
||||
// TODO allow passing shared draft object?
|
||||
get () {
|
||||
if (this.realSource === 'admin' || this.path == null) {
|
||||
return get(this.$store.state.adminSettings.draft, this.canonPath)
|
||||
} else {
|
||||
return this.localDraft
|
||||
}
|
||||
},
|
||||
set (value) {
|
||||
if (this.realSource === 'admin' || this.path == null) {
|
||||
this.$store.commit('updateAdminDraft', { path: this.canonPath, value })
|
||||
} else {
|
||||
this.localDraft = value
|
||||
}
|
||||
}
|
||||
},
|
||||
state () {
|
||||
if (this.path == null) {
|
||||
return this.modelValue
|
||||
}
|
||||
const value = get(this.configSource, this.canonPath)
|
||||
if (value === undefined) {
|
||||
return this.defaultState
|
||||
} else {
|
||||
return value
|
||||
}
|
||||
},
|
||||
visibleState () {
|
||||
return this.realDraftMode ? this.draft : this.state
|
||||
},
|
||||
realSource () {
|
||||
return this.source || this.defaultSource
|
||||
},
|
||||
realDraftMode () {
|
||||
return typeof this.draftMode === 'undefined' ? this.defaultDraftMode : this.draftMode
|
||||
},
|
||||
backendDescription () {
|
||||
return get(this.$store.state.adminSettings.descriptions, this.path)
|
||||
},
|
||||
backendDescriptionLabel () {
|
||||
if (this.realSource !== 'admin') return ''
|
||||
if (!this.backendDescription || this.overrideBackendDescriptionLabel) {
|
||||
return this.$t([
|
||||
'admin_dash',
|
||||
'temp_overrides',
|
||||
...this.canonPath.map(p => p.replace(/\./g, '_DOT_')),
|
||||
'label'
|
||||
].join('.'))
|
||||
} else {
|
||||
return this.swapDescriptionAndLabel
|
||||
? this.backendDescription?.description
|
||||
: this.backendDescription?.label
|
||||
}
|
||||
},
|
||||
backendDescriptionDescription () {
|
||||
if (this.realSource !== 'admin') return ''
|
||||
if (this.hideDescription) return null
|
||||
if (!this.backendDescription || this.overrideBackendDescription) {
|
||||
return this.$t([
|
||||
'admin_dash',
|
||||
'temp_overrides',
|
||||
...this.canonPath.map(p => p.replace(/\./g, '_DOT_')),
|
||||
'description'
|
||||
].join('.'))
|
||||
} else {
|
||||
return this.swapDescriptionAndLabel
|
||||
? this.backendDescription?.label
|
||||
: this.backendDescription?.description
|
||||
}
|
||||
},
|
||||
backendDescriptionSuggestions () {
|
||||
return this.backendDescription?.suggestions
|
||||
},
|
||||
shouldBeDisabled () {
|
||||
if (this.path == null) {
|
||||
return this.disabled
|
||||
}
|
||||
const parentValue = this.parentPath !== undefined ? get(this.configSource, this.parentPath) : null
|
||||
return this.disabled || (parentValue !== null ? (this.parentInvert ? parentValue : !parentValue) : false)
|
||||
},
|
||||
configSource () {
|
||||
switch (this.realSource) {
|
||||
case 'profile':
|
||||
return this.$store.state.profileConfig
|
||||
case 'admin':
|
||||
return this.$store.state.adminSettings.config
|
||||
default:
|
||||
return this.$store.getters.mergedConfig
|
||||
}
|
||||
},
|
||||
configSink () {
|
||||
if (this.path == null) {
|
||||
return (k, v) => this.$emit('update:modelValue', v)
|
||||
}
|
||||
switch (this.realSource) {
|
||||
case 'profile':
|
||||
return (k, v) => this.$store.dispatch('setProfileOption', { name: k, value: v })
|
||||
case 'admin':
|
||||
return (k, v) => this.$store.dispatch('pushAdminSetting', { path: k, value: v })
|
||||
default:
|
||||
if (this.timedApplyMode) {
|
||||
return (k, v) => this.$store.dispatch('setOptionTemporarily', { name: k, value: v })
|
||||
} else {
|
||||
return (k, v) => this.$store.dispatch('setOption', { name: k, value: v })
|
||||
}
|
||||
}
|
||||
},
|
||||
defaultState () {
|
||||
switch (this.realSource) {
|
||||
case 'profile':
|
||||
return {}
|
||||
default:
|
||||
return get(this.$store.getters.defaultConfig, this.path)
|
||||
}
|
||||
},
|
||||
isProfileSetting () {
|
||||
return this.realSource === 'profile'
|
||||
},
|
||||
isChanged () {
|
||||
if (this.path == null) return false
|
||||
switch (this.realSource) {
|
||||
case 'profile':
|
||||
case 'admin':
|
||||
return false
|
||||
default:
|
||||
return this.state !== this.defaultState
|
||||
}
|
||||
},
|
||||
canonPath () {
|
||||
if (this.path == null) return null
|
||||
return Array.isArray(this.path) ? this.path : this.path.split('.')
|
||||
},
|
||||
isDirty () {
|
||||
if (this.path == null) return false
|
||||
if (this.realSource === 'admin' && this.canonPath.length > 3) {
|
||||
return false // should not show draft buttons for "grouped" values
|
||||
} else {
|
||||
return this.realDraftMode && this.draft !== this.state
|
||||
}
|
||||
},
|
||||
canHardReset () {
|
||||
return this.realSource === 'admin' && this.$store.state.adminSettings.modifiedPaths &&
|
||||
this.$store.state.adminSettings.modifiedPaths.has(this.canonPath.join(' -> '))
|
||||
},
|
||||
matchesExpertLevel () {
|
||||
return (this.expert || 0) <= this.$store.state.config.expertLevel > 0
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getValue (e) {
|
||||
return e.target.value
|
||||
},
|
||||
update (e) {
|
||||
if (this.realDraftMode) {
|
||||
this.draft = this.getValue(e)
|
||||
} else {
|
||||
this.configSink(this.path, this.getValue(e))
|
||||
}
|
||||
},
|
||||
commitDraft () {
|
||||
if (this.realDraftMode) {
|
||||
this.configSink(this.path, this.draft)
|
||||
}
|
||||
},
|
||||
reset () {
|
||||
if (this.realDraftMode) {
|
||||
this.draft = cloneDeep(this.state)
|
||||
} else {
|
||||
set(this.$store.getters.mergedConfig, this.path, cloneDeep(this.defaultState))
|
||||
}
|
||||
},
|
||||
hardReset () {
|
||||
switch (this.realSource) {
|
||||
case 'admin':
|
||||
return this.$store.dispatch('resetAdminSetting', { path: this.path })
|
||||
.then(() => { this.draft = this.state })
|
||||
default:
|
||||
console.warn('Hard reset not implemented yet!')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,52 +1,18 @@
|
|||
import { defaultState as configDefaultState } from 'src/modules/config.js'
|
||||
import { defaultState as serverSideConfigDefaultState } from 'src/modules/serverSideConfig.js'
|
||||
|
||||
const SharedComputedObject = () => ({
|
||||
user () {
|
||||
return this.$store.state.users.currentUser
|
||||
},
|
||||
// Getting values for default properties
|
||||
...Object.keys(configDefaultState)
|
||||
.map(key => [
|
||||
key + 'DefaultValue',
|
||||
function () {
|
||||
return this.$store.getters.defaultConfig[key]
|
||||
}
|
||||
])
|
||||
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}),
|
||||
// Generating computed values for vuex properties
|
||||
...Object.keys(configDefaultState)
|
||||
.map(key => [key, {
|
||||
get () { return this.$store.getters.mergedConfig[key] },
|
||||
set (value) {
|
||||
this.$store.dispatch('setOption', { name: key, value })
|
||||
}
|
||||
}])
|
||||
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}),
|
||||
...Object.keys(serverSideConfigDefaultState)
|
||||
.map(key => ['serverSide_' + key, {
|
||||
get () { return this.$store.state.serverSideConfig[key] },
|
||||
set (value) {
|
||||
this.$store.dispatch('setServerSideOption', { name: key, value })
|
||||
}
|
||||
}])
|
||||
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}),
|
||||
// Special cases (need to transform values or perform actions first)
|
||||
useStreamingApi: {
|
||||
get () { return this.$store.getters.mergedConfig.useStreamingApi },
|
||||
set (value) {
|
||||
const promise = value
|
||||
? this.$store.dispatch('enableMastoSockets')
|
||||
: this.$store.dispatch('disableMastoSockets')
|
||||
|
||||
promise.then(() => {
|
||||
this.$store.dispatch('setOption', { name: 'useStreamingApi', value })
|
||||
}).catch((e) => {
|
||||
console.error('Failed starting MastoAPI Streaming socket', e)
|
||||
this.$store.dispatch('disableMastoSockets')
|
||||
this.$store.dispatch('setOption', { name: 'useStreamingApi', value: false })
|
||||
})
|
||||
}
|
||||
expertLevel () {
|
||||
return this.$store.getters.mergedConfig.expertLevel > 0
|
||||
},
|
||||
mergedConfig () {
|
||||
return this.$store.getters.mergedConfig
|
||||
},
|
||||
adminConfig () {
|
||||
return this.$store.state.adminSettings.config
|
||||
},
|
||||
adminDraft () {
|
||||
return this.$store.state.adminSettings.draft
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -1,67 +0,0 @@
|
|||
import { get, set } from 'lodash'
|
||||
import ModifiedIndicator from './modified_indicator.vue'
|
||||
import Select from 'src/components/select/select.vue'
|
||||
|
||||
export const allCssUnits = ['cm', 'mm', 'in', 'px', 'pt', 'pc', 'em', 'ex', 'ch', 'rem', 'vw', 'vh', 'vmin', 'vmax', '%']
|
||||
export const defaultHorizontalUnits = ['px', 'rem', 'vw']
|
||||
export const defaultVerticalUnits = ['px', 'rem', 'vh']
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ModifiedIndicator,
|
||||
Select
|
||||
},
|
||||
props: {
|
||||
path: String,
|
||||
disabled: Boolean,
|
||||
min: Number,
|
||||
units: {
|
||||
type: [String],
|
||||
default: () => allCssUnits
|
||||
},
|
||||
expert: [Number, String]
|
||||
},
|
||||
computed: {
|
||||
pathDefault () {
|
||||
const [firstSegment, ...rest] = this.path.split('.')
|
||||
return [firstSegment + 'DefaultValue', ...rest].join('.')
|
||||
},
|
||||
stateUnit () {
|
||||
return (this.state || '').replace(/\d+/, '')
|
||||
},
|
||||
stateValue () {
|
||||
return (this.state || '').replace(/\D+/, '')
|
||||
},
|
||||
state () {
|
||||
const value = get(this.$parent, this.path)
|
||||
if (value === undefined) {
|
||||
return this.defaultState
|
||||
} else {
|
||||
return value
|
||||
}
|
||||
},
|
||||
defaultState () {
|
||||
return get(this.$parent, this.pathDefault)
|
||||
},
|
||||
isChanged () {
|
||||
return this.state !== this.defaultState
|
||||
},
|
||||
matchesExpertLevel () {
|
||||
return (this.expert || 0) <= this.$parent.expertLevel
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
update (e) {
|
||||
set(this.$parent, this.path, e)
|
||||
},
|
||||
reset () {
|
||||
set(this.$parent, this.path, this.defaultState)
|
||||
},
|
||||
updateValue (e) {
|
||||
set(this.$parent, this.path, parseInt(e.target.value) + this.stateUnit)
|
||||
},
|
||||
updateUnit (e) {
|
||||
set(this.$parent, this.path, this.stateValue + e.target.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
<template>
|
||||
<span
|
||||
v-if="matchesExpertLevel"
|
||||
class="SizeSetting"
|
||||
>
|
||||
<label
|
||||
:for="path"
|
||||
class="size-label"
|
||||
>
|
||||
<slot />
|
||||
</label>
|
||||
<input
|
||||
:id="path"
|
||||
class="number-input"
|
||||
type="number"
|
||||
step="1"
|
||||
:disabled="disabled"
|
||||
:min="min || 0"
|
||||
:value="stateValue"
|
||||
@change="updateValue"
|
||||
>
|
||||
<Select
|
||||
:id="path"
|
||||
:model-value="stateUnit"
|
||||
:disabled="disabled"
|
||||
class="css-unit-input"
|
||||
@change="updateUnit"
|
||||
>
|
||||
<option
|
||||
v-for="option in units"
|
||||
:key="option"
|
||||
:value="option"
|
||||
>
|
||||
{{ option }}
|
||||
</option>
|
||||
</Select>
|
||||
{{ ' ' }}
|
||||
<ModifiedIndicator
|
||||
:changed="isChanged"
|
||||
:onclick="reset"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script src="./size_setting.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
.css-unit-input,
|
||||
.css-unit-input select {
|
||||
margin-left: 0.5em;
|
||||
width: 4em;
|
||||
max-width: 4em;
|
||||
min-width: 4em;
|
||||
}
|
||||
</style>
|
||||
5
src/components/settings_modal/helpers/string_setting.js
Normal file
5
src/components/settings_modal/helpers/string_setting.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import Setting from './setting.js'
|
||||
|
||||
export default {
|
||||
...Setting
|
||||
}
|
||||
44
src/components/settings_modal/helpers/string_setting.vue
Normal file
44
src/components/settings_modal/helpers/string_setting.vue
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
<template>
|
||||
<label
|
||||
v-if="matchesExpertLevel"
|
||||
class="StringSetting"
|
||||
>
|
||||
<label
|
||||
:for="path"
|
||||
class="setting-label"
|
||||
:class="{ 'faint': shouldBeDisabled }"
|
||||
>
|
||||
<template v-if="backendDescriptionLabel">
|
||||
{{ backendDescriptionLabel + ' ' }}
|
||||
</template>
|
||||
<template v-else-if="source === 'admin'">
|
||||
MISSING LABEL FOR {{ path }}
|
||||
</template>
|
||||
<slot v-else />
|
||||
</label>
|
||||
{{ ' ' }}
|
||||
<input
|
||||
:id="path"
|
||||
class="input string-input"
|
||||
:disabled="shouldBeDisabled"
|
||||
:value="realDraftMode ? draft : state"
|
||||
@change="update"
|
||||
>
|
||||
{{ ' ' }}
|
||||
<ModifiedIndicator
|
||||
:changed="isChanged"
|
||||
:onclick="reset"
|
||||
/>
|
||||
<ProfileSettingIndicator :is-profile="isProfileSetting" />
|
||||
<DraftButtons />
|
||||
<p
|
||||
v-if="backendDescriptionDescription"
|
||||
class="setting-description"
|
||||
:class="{ 'faint': shouldBeDisabled }"
|
||||
>
|
||||
{{ backendDescriptionDescription + ' ' }}
|
||||
</p>
|
||||
</label>
|
||||
</template>
|
||||
|
||||
<script src="./string_setting.js"></script>
|
||||
64
src/components/settings_modal/helpers/unit_setting.js
Normal file
64
src/components/settings_modal/helpers/unit_setting.js
Normal file
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
68
src/components/settings_modal/helpers/unit_setting.vue
Normal file
68
src/components/settings_modal/helpers/unit_setting.vue
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
<template>
|
||||
<span
|
||||
v-if="matchesExpertLevel"
|
||||
class="UnitSetting"
|
||||
>
|
||||
<label
|
||||
:for="path"
|
||||
class="size-label"
|
||||
>
|
||||
<slot />
|
||||
</label>
|
||||
{{ ' ' }}
|
||||
<span class="no-break">
|
||||
<input
|
||||
:id="path"
|
||||
class="input number-input"
|
||||
type="number"
|
||||
:step="step"
|
||||
:disabled="disabled"
|
||||
:min="min || 0"
|
||||
:value="stateValue"
|
||||
@change="updateValue"
|
||||
>
|
||||
<Select
|
||||
:id="path"
|
||||
:model-value="stateUnit"
|
||||
:disabled="disabled"
|
||||
class="unit-input unstyled"
|
||||
@change="updateUnit"
|
||||
>
|
||||
<option
|
||||
v-for="option in units"
|
||||
:key="option"
|
||||
:value="option"
|
||||
>
|
||||
{{ getUnitString(option) }}
|
||||
</option>
|
||||
</Select>
|
||||
</span>
|
||||
{{ ' ' }}
|
||||
<ModifiedIndicator
|
||||
:changed="isChanged"
|
||||
:onclick="reset"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script src="./unit_setting.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
.UnitSetting {
|
||||
.no-break {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.number-input {
|
||||
max-width: 6.5em;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.unit-input,
|
||||
.unit-input select {
|
||||
min-width: 4em;
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
@ -4,8 +4,9 @@ import AsyncComponentError from 'src/components/async_component_error/async_comp
|
|||
import getResettableAsyncComponent from 'src/services/resettable_async_component.js'
|
||||
import Popover from '../popover/popover.vue'
|
||||
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||
import ConfirmModal from 'src/components/confirm_modal/confirm_modal.vue'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { cloneDeep } from 'lodash'
|
||||
import { cloneDeep, isEqual } from 'lodash'
|
||||
import {
|
||||
newImporter,
|
||||
newExporter
|
||||
|
|
@ -54,8 +55,17 @@ const SettingsModal = {
|
|||
Modal,
|
||||
Popover,
|
||||
Checkbox,
|
||||
SettingsModalContent: getResettableAsyncComponent(
|
||||
() => import('./settings_modal_content.vue'),
|
||||
ConfirmModal,
|
||||
SettingsModalUserContent: getResettableAsyncComponent(
|
||||
() => import('./settings_modal_user_content.vue'),
|
||||
{
|
||||
loadingComponent: PanelLoading,
|
||||
errorComponent: AsyncComponentError,
|
||||
delay: 0
|
||||
}
|
||||
),
|
||||
SettingsModalAdminContent: getResettableAsyncComponent(
|
||||
() => import('./settings_modal_admin_content.vue'),
|
||||
{
|
||||
loadingComponent: PanelLoading,
|
||||
errorComponent: AsyncComponentError,
|
||||
|
|
@ -148,6 +158,12 @@ const SettingsModal = {
|
|||
PLEROMAFE_SETTINGS_MINOR_VERSION
|
||||
]
|
||||
return clone
|
||||
},
|
||||
resetAdminDraft () {
|
||||
this.$store.commit('resetAdminDraft')
|
||||
},
|
||||
pushAdminDraft () {
|
||||
this.$store.dispatch('pushAdminDraft')
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -157,8 +173,14 @@ const SettingsModal = {
|
|||
modalActivated () {
|
||||
return useInterfaceStore().settingsModalState !== 'hidden'
|
||||
},
|
||||
modalOpenedOnce () {
|
||||
return useInterfaceStore().settingsModalLoaded
|
||||
modalMode () {
|
||||
return useInterfaceStore().settingsModalMode
|
||||
},
|
||||
modalOpenedOnceUser () {
|
||||
return useInterfaceStore().settingsModalLoadedUser
|
||||
},
|
||||
modalOpenedOnceAdmin () {
|
||||
return useInterfaceStore().settingsModalLoadedAdmin
|
||||
},
|
||||
modalPeeked () {
|
||||
return useInterfaceStore().settingsModalState === 'minimized'
|
||||
|
|
@ -168,9 +190,14 @@ const SettingsModal = {
|
|||
return this.$store.state.config.expertLevel > 0
|
||||
},
|
||||
set (value) {
|
||||
console.log(value)
|
||||
this.$store.dispatch('setOption', { name: 'expertLevel', value: value ? 1 : 0 })
|
||||
}
|
||||
},
|
||||
adminDraftAny () {
|
||||
return !isEqual(
|
||||
this.$store.state.adminSettings.config,
|
||||
this.$store.state.adminSettings.draft
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,19 @@
|
|||
@import "src/variables";
|
||||
|
||||
.settings-modal {
|
||||
overflow: hidden;
|
||||
|
||||
h4 {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.setting-list,
|
||||
.option-list {
|
||||
list-style-type: none;
|
||||
padding-left: 2em;
|
||||
|
||||
.btn:not(.dropdown-button) {
|
||||
padding: 0 2em;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
|
@ -15,6 +21,20 @@
|
|||
.suboptions {
|
||||
margin-top: 0.3em;
|
||||
}
|
||||
|
||||
&.two-column {
|
||||
column-count: 2;
|
||||
|
||||
> li {
|
||||
break-inside: avoid;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.setting-description {
|
||||
margin-top: 0.2em;
|
||||
margin-bottom: 2em;
|
||||
font-size: 70%;
|
||||
}
|
||||
|
||||
.settings-modal-panel {
|
||||
|
|
@ -37,14 +57,14 @@
|
|||
|
||||
.btn {
|
||||
min-height: 2em;
|
||||
min-width: 10em;
|
||||
padding: 0 2em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.settings-footer {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
line-height: 2;
|
||||
|
||||
>* {
|
||||
margin-right: 0.5em;
|
||||
|
|
@ -56,6 +76,23 @@
|
|||
}
|
||||
}
|
||||
|
||||
&.-mobile {
|
||||
.setting-list,
|
||||
.option-list {
|
||||
padding-left: 0.25em;
|
||||
|
||||
> li {
|
||||
margin: 1em 0;
|
||||
line-height: 1.5em;
|
||||
vertical-align: center;
|
||||
}
|
||||
|
||||
&.two-column {
|
||||
column-count: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.peek {
|
||||
.settings-modal-panel {
|
||||
/* Explanation:
|
||||
|
|
|
|||
|
|
@ -7,14 +7,14 @@
|
|||
>
|
||||
<div class="settings-modal-panel panel">
|
||||
<div class="panel-heading">
|
||||
<span class="title">
|
||||
{{ $t('settings.settings') }}
|
||||
</span>
|
||||
<h1 class="title">
|
||||
{{ modalMode === 'user' ? $t('settings.settings') : $t('admin_dash.window_title') }}
|
||||
</h1>
|
||||
<transition name="fade">
|
||||
<div
|
||||
v-if="currentSaveStateNotice"
|
||||
class="alert"
|
||||
:class="{ transparent: !currentSaveStateNotice.error, error: currentSaveStateNotice.error}"
|
||||
:class="{ success: !currentSaveStateNotice.error, error: currentSaveStateNotice.error}"
|
||||
@click.prevent
|
||||
>
|
||||
{{ currentSaveStateNotice.error ? $t('settings.saving_err') : $t('settings.saving_ok') }}
|
||||
|
|
@ -42,10 +42,12 @@
|
|||
</button>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<SettingsModalContent v-if="modalOpenedOnce" />
|
||||
<SettingsModalUserContent v-if="modalMode === 'user' && modalOpenedOnceUser" />
|
||||
<SettingsModalAdminContent v-if="modalMode === 'admin' && modalOpenedOnceAdmin" />
|
||||
</div>
|
||||
<div class="panel-footer settings-footer">
|
||||
<div class="panel-footer settings-footer -flexible-height">
|
||||
<Popover
|
||||
v-if="modalMode === 'user'"
|
||||
class="export"
|
||||
trigger="click"
|
||||
placement="top"
|
||||
|
|
@ -67,36 +69,42 @@
|
|||
</template>
|
||||
<template #content="{close}">
|
||||
<div class="dropdown-menu">
|
||||
<button
|
||||
class="button-default dropdown-item dropdown-item-icon"
|
||||
@click.prevent="backup"
|
||||
@click="close"
|
||||
>
|
||||
<FAIcon
|
||||
icon="file-download"
|
||||
fixed-width
|
||||
/><span>{{ $t("settings.file_export_import.backup_settings") }}</span>
|
||||
</button>
|
||||
<button
|
||||
class="button-default dropdown-item dropdown-item-icon"
|
||||
@click.prevent="backupWithTheme"
|
||||
@click="close"
|
||||
>
|
||||
<FAIcon
|
||||
icon="file-download"
|
||||
fixed-width
|
||||
/><span>{{ $t("settings.file_export_import.backup_settings_theme") }}</span>
|
||||
</button>
|
||||
<button
|
||||
class="button-default dropdown-item dropdown-item-icon"
|
||||
@click.prevent="restore"
|
||||
@click="close"
|
||||
>
|
||||
<FAIcon
|
||||
icon="file-upload"
|
||||
fixed-width
|
||||
/><span>{{ $t("settings.file_export_import.restore_settings") }}</span>
|
||||
</button>
|
||||
<div class="menu-item dropdown-item -icon">
|
||||
<button
|
||||
class="main-button"
|
||||
@click.prevent="backup"
|
||||
@click="close"
|
||||
>
|
||||
<FAIcon
|
||||
icon="file-download"
|
||||
fixed-width
|
||||
/><span>{{ $t("settings.file_export_import.backup_settings") }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="menu-item dropdown-item -icon">
|
||||
<button
|
||||
class="main-button"
|
||||
@click.prevent="backupWithTheme"
|
||||
@click="close"
|
||||
>
|
||||
<FAIcon
|
||||
icon="file-download"
|
||||
fixed-width
|
||||
/><span>{{ $t("settings.file_export_import.backup_settings_theme") }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="menu-item dropdown-item -icon">
|
||||
<button
|
||||
class="main-button"
|
||||
@click.prevent="restore"
|
||||
@click="close"
|
||||
>
|
||||
<FAIcon
|
||||
icon="file-upload"
|
||||
fixed-width
|
||||
/><span>{{ $t("settings.file_export_import.restore_settings") }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Popover>
|
||||
|
|
@ -107,12 +115,59 @@
|
|||
>
|
||||
{{ $t("settings.expert_mode") }}
|
||||
</Checkbox>
|
||||
<span v-if="modalMode === 'admin'">
|
||||
<i18n-t
|
||||
scope="global"
|
||||
keypath="admin_dash.wip_notice"
|
||||
>
|
||||
<template #adminFeLink>
|
||||
<a
|
||||
href="/pleroma/admin/#/login-pleroma"
|
||||
target="_blank"
|
||||
>
|
||||
{{ $t("admin_dash.old_ui_link") }}
|
||||
</a>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</span>
|
||||
<span
|
||||
id="unscrolled-content"
|
||||
class="extra-content"
|
||||
/>
|
||||
<span
|
||||
v-if="modalMode === 'admin'"
|
||||
class="admin-buttons"
|
||||
>
|
||||
<button
|
||||
class="button-default btn"
|
||||
:disabled="!adminDraftAny"
|
||||
@click="resetAdminDraft"
|
||||
>
|
||||
{{ $t("admin_dash.reset_all") }}
|
||||
</button>
|
||||
{{ ' ' }}
|
||||
<button
|
||||
class="button-default btn"
|
||||
:disabled="!adminDraftAny"
|
||||
@click="pushAdminDraft"
|
||||
>
|
||||
{{ $t("admin_dash.commit_all") }}
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<teleport to="#modal">
|
||||
<ConfirmModal
|
||||
v-if="$store.state.interface.temporaryChangesTimeoutId"
|
||||
:title="$t('settings.confirm_new_setting')"
|
||||
:cancel-text="$t('settings.revert')"
|
||||
:confirm-text="$t('settings.confirm')"
|
||||
@cancelled="$store.state.interface.temporaryChangesRevert"
|
||||
@accepted="$store.state.interface.temporaryChangesConfirm"
|
||||
>
|
||||
{{ $t('settings.confirm_new_question') }}
|
||||
</ConfirmModal>
|
||||
</teleport>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,95 @@
|
|||
import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'
|
||||
|
||||
import InstanceTab from './admin_tabs/instance_tab.vue'
|
||||
import LimitsTab from './admin_tabs/limits_tab.vue'
|
||||
import FrontendsTab from './admin_tabs/frontends_tab.vue'
|
||||
import EmojiTab from './admin_tabs/emoji_tab.vue'
|
||||
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faWrench,
|
||||
faHand,
|
||||
faLaptopCode,
|
||||
faPaintBrush,
|
||||
faBell,
|
||||
faDownload,
|
||||
faEyeSlash,
|
||||
faInfo
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(
|
||||
faWrench,
|
||||
faHand,
|
||||
faLaptopCode,
|
||||
faPaintBrush,
|
||||
faBell,
|
||||
faDownload,
|
||||
faEyeSlash,
|
||||
faInfo
|
||||
)
|
||||
|
||||
const SettingsModalAdminContent = {
|
||||
components: {
|
||||
TabSwitcher,
|
||||
|
||||
InstanceTab,
|
||||
LimitsTab,
|
||||
FrontendsTab,
|
||||
EmojiTab
|
||||
},
|
||||
computed: {
|
||||
user () {
|
||||
return this.$store.state.users.currentUser
|
||||
},
|
||||
isLoggedIn () {
|
||||
return !!this.$store.state.users.currentUser
|
||||
},
|
||||
open () {
|
||||
return this.$store.state.interface.settingsModalState !== 'hidden'
|
||||
},
|
||||
bodyLock () {
|
||||
return this.$store.state.interface.settingsModalState === 'visible'
|
||||
},
|
||||
adminDbLoaded () {
|
||||
return this.$store.state.adminSettings.loaded
|
||||
},
|
||||
adminDescriptionsLoaded () {
|
||||
return this.$store.state.adminSettings.descriptions !== null
|
||||
},
|
||||
noDb () {
|
||||
return this.$store.state.adminSettings.dbConfigEnabled === false
|
||||
}
|
||||
},
|
||||
created () {
|
||||
if (this.user.rights.admin) {
|
||||
this.$store.dispatch('loadAdminStuff')
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onOpen () {
|
||||
const targetTab = this.$store.state.interface.settingsModalTargetTab
|
||||
// We're being told to open in specific tab
|
||||
if (targetTab) {
|
||||
const tabIndex = this.$refs.tabSwitcher.$slots.default().findIndex(elm => {
|
||||
return elm.props && elm.props['data-tab-name'] === targetTab
|
||||
})
|
||||
if (tabIndex >= 0) {
|
||||
this.$refs.tabSwitcher.setTab(tabIndex)
|
||||
}
|
||||
}
|
||||
// Clear the state of target tab, so that next time settings is opened
|
||||
// it doesn't force it.
|
||||
this.$store.dispatch('clearSettingsModalTargetTab')
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.onOpen()
|
||||
},
|
||||
watch: {
|
||||
open: function (value) {
|
||||
if (value) this.onOpen()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default SettingsModalAdminContent
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
@ -19,10 +17,13 @@
|
|||
}
|
||||
|
||||
.select-multiple {
|
||||
margin-top: 0.5em;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.option-list {
|
||||
margin: 0;
|
||||
margin-top: 0.5em;
|
||||
padding-left: 0.5em;
|
||||
}
|
||||
}
|
||||
|
|
@ -33,10 +34,6 @@
|
|||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
select {
|
||||
min-width: 10em;
|
||||
}
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
|
|
@ -45,12 +42,7 @@
|
|||
|
||||
.unavailable,
|
||||
.unavailable svg {
|
||||
color: var(--cRed, $fallback--cRed);
|
||||
color: $fallback--cRed;
|
||||
}
|
||||
|
||||
.number-input {
|
||||
max-width: 6em;
|
||||
color: var(--cRed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
<template>
|
||||
<tab-switcher
|
||||
v-if="adminDescriptionsLoaded && (noDb || adminDbLoaded)"
|
||||
ref="tabSwitcher"
|
||||
class="settings_tab-switcher"
|
||||
:side-tab-bar="true"
|
||||
:scrollable-tabs="true"
|
||||
:render-only-focused="true"
|
||||
:body-scroll-lock="bodyLock"
|
||||
>
|
||||
<div
|
||||
v-if="noDb"
|
||||
:label="$t('admin_dash.tabs.nodb')"
|
||||
icon="exclamation-triangle"
|
||||
data-tab-name="nodb-notice"
|
||||
>
|
||||
<div :label="$t('admin_dash.tabs.nodb')">
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('admin_dash.nodb.heading') }}</h2>
|
||||
<i18n-t
|
||||
scope="global"
|
||||
keypath="admin_dash.nodb.text"
|
||||
>
|
||||
<template #documentation>
|
||||
<a
|
||||
href="https://docs-develop.pleroma.social/backend/configuration/howto_database_config/"
|
||||
target="_blank"
|
||||
>
|
||||
{{ $t("admin_dash.nodb.documentation") }}
|
||||
</a>
|
||||
</template>
|
||||
<template #property>
|
||||
<code>config :pleroma, configurable_from_database</code>
|
||||
</template>
|
||||
<template #value>
|
||||
<code>true</code>
|
||||
</template>
|
||||
</i18n-t>
|
||||
<p>{{ $t('admin_dash.nodb.text2') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="adminDbLoaded"
|
||||
:label="$t('admin_dash.tabs.instance')"
|
||||
icon="wrench"
|
||||
data-tab-name="general"
|
||||
>
|
||||
<InstanceTab />
|
||||
</div>
|
||||
<div
|
||||
v-if="adminDbLoaded"
|
||||
:label="$t('admin_dash.tabs.limits')"
|
||||
icon="hand"
|
||||
data-tab-name="limits"
|
||||
>
|
||||
<LimitsTab />
|
||||
</div>
|
||||
<div
|
||||
:label="$t('admin_dash.tabs.frontends')"
|
||||
icon="laptop-code"
|
||||
data-tab-name="frontends"
|
||||
>
|
||||
<FrontendsTab />
|
||||
</div>
|
||||
|
||||
<div
|
||||
:label="$t('admin_dash.tabs.emoji')"
|
||||
icon="face-smile-beam"
|
||||
data-tab-name="emoji"
|
||||
>
|
||||
<EmojiTab />
|
||||
</div>
|
||||
</tab-switcher>
|
||||
</template>
|
||||
|
||||
<script src="./settings_modal_admin_content.js"></script>
|
||||
|
||||
<style src="./settings_modal_admin_content.scss" lang="scss"></style>
|
||||
|
|
@ -7,8 +7,10 @@ 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'
|
||||
import StyleTab from './tabs/style_tab/style_tab.vue'
|
||||
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
|
|
@ -16,10 +18,12 @@ import {
|
|||
faUser,
|
||||
faFilter,
|
||||
faPaintBrush,
|
||||
faPalette,
|
||||
faBell,
|
||||
faDownload,
|
||||
faEyeSlash,
|
||||
faInfo
|
||||
faInfo,
|
||||
faWindowRestore
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { useInterfaceStore } from '../../stores/interface'
|
||||
|
||||
|
|
@ -28,10 +32,12 @@ library.add(
|
|||
faUser,
|
||||
faFilter,
|
||||
faPaintBrush,
|
||||
faPalette,
|
||||
faBell,
|
||||
faDownload,
|
||||
faEyeSlash,
|
||||
faInfo
|
||||
faInfo,
|
||||
faWindowRestore
|
||||
)
|
||||
|
||||
const SettingsModalContent = {
|
||||
|
|
@ -45,6 +51,8 @@ const SettingsModalContent = {
|
|||
SecurityTab,
|
||||
ProfileTab,
|
||||
GeneralTab,
|
||||
AppearanceTab,
|
||||
StyleTab,
|
||||
VersionTab,
|
||||
ThemeTab
|
||||
},
|
||||
|
|
@ -57,6 +65,12 @@ const SettingsModalContent = {
|
|||
},
|
||||
bodyLock () {
|
||||
return useInterfaceStore().settingsModalState === 'visible'
|
||||
},
|
||||
expertLevel () {
|
||||
return this.$store.state.config.expertLevel
|
||||
},
|
||||
isMobileLayout () {
|
||||
return this.$store.state.interface.layoutType === 'mobile'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
.settings_tab-switcher {
|
||||
height: 100%;
|
||||
|
||||
h1 {
|
||||
margin-bottom: 0.5em;
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin-bottom: 0;
|
||||
margin-top: 0.25em;
|
||||
}
|
||||
|
||||
h5 {
|
||||
margin-bottom: 0;
|
||||
margin-top: 0.25em;
|
||||
}
|
||||
|
||||
.setting-item {
|
||||
border-bottom: 2px solid var(--border);
|
||||
margin: 1em 1em 1.4em;
|
||||
padding-bottom: 1.4em;
|
||||
|
||||
> div,
|
||||
> label {
|
||||
margin-bottom: 0.5em;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.select-multiple {
|
||||
margin-top: 1em;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.option-list {
|
||||
margin: 0;
|
||||
margin-top: 0.5em;
|
||||
padding-left: 0.5em;
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.unavailable,
|
||||
.unavailable svg {
|
||||
color: var(--cRed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -13,6 +13,32 @@
|
|||
>
|
||||
<GeneralTab />
|
||||
</div>
|
||||
<div
|
||||
:label="$t('settings.appearance')"
|
||||
icon="window-restore"
|
||||
data-tab-name="appearance"
|
||||
:delay-render="true"
|
||||
>
|
||||
<AppearanceTab />
|
||||
</div>
|
||||
<div
|
||||
v-if="expertLevel > 0 && !isMobileLayout"
|
||||
:label="$t('settings.style.themes3.editor.title')"
|
||||
icon="palette"
|
||||
data-tab-name="style"
|
||||
:delay-render="true"
|
||||
>
|
||||
<StyleTab />
|
||||
</div>
|
||||
<div
|
||||
v-if="expertLevel > 0 && !isMobileLayout"
|
||||
:label="$t('settings.theme_old')"
|
||||
icon="paint-brush"
|
||||
data-tab-name="theme"
|
||||
:delay-render="true"
|
||||
>
|
||||
<ThemeTab />
|
||||
</div>
|
||||
<div
|
||||
v-if="isLoggedIn"
|
||||
:label="$t('settings.profile_tab')"
|
||||
|
|
@ -21,6 +47,14 @@
|
|||
>
|
||||
<ProfileTab />
|
||||
</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')"
|
||||
|
|
@ -36,20 +70,14 @@
|
|||
>
|
||||
<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"
|
||||
|
|
@ -59,15 +87,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"
|
||||
|
|
@ -78,6 +97,6 @@
|
|||
</tab-switcher>
|
||||
</template>
|
||||
|
||||
<script src="./settings_modal_content.js"></script>
|
||||
<script src="./settings_modal_user_content.js"></script>
|
||||
|
||||
<style src="./settings_modal_content.scss" lang="scss"></style>
|
||||
<style src="./settings_modal_user_content.scss" lang="scss"></style>
|
||||
425
src/components/settings_modal/tabs/appearance_tab.js
Normal file
425
src/components/settings_modal/tabs/appearance_tab.js
Normal file
|
|
@ -0,0 +1,425 @@
|
|||
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 PaletteEditor from 'src/components/palette_editor/palette_editor.vue'
|
||||
|
||||
import FontControl from 'src/components/font_control/font_control.vue'
|
||||
|
||||
import { normalizeThemeData } from 'src/modules/interface'
|
||||
|
||||
import { newImporter } from 'src/services/export_import/export_import.js'
|
||||
import { convertTheme2To3 } from 'src/services/theme_data/theme2_to_theme3.js'
|
||||
import { init } from 'src/services/theme_data/theme_data_3.service.js'
|
||||
import {
|
||||
getCssRules,
|
||||
getScopedVersion
|
||||
} from 'src/services/theme_data/css_utils.js'
|
||||
import { deserialize } from 'src/services/theme_data/iss_deserializer.js'
|
||||
|
||||
import SharedComputedObject from '../helpers/shared_computed_object.js'
|
||||
import ProfileSettingIndicator from '../helpers/profile_setting_indicator.vue'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faGlobe
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
import Preview from './theme_tab/theme_preview.vue'
|
||||
|
||||
// helper for debugging
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const toValue = (x) => JSON.parse(JSON.stringify(x === undefined ? 'null' : x))
|
||||
|
||||
library.add(
|
||||
faGlobe
|
||||
)
|
||||
|
||||
const AppearanceTab = {
|
||||
data () {
|
||||
return {
|
||||
availableThemesV3: [],
|
||||
availableThemesV2: [],
|
||||
bundledPalettes: [],
|
||||
compilationCache: {},
|
||||
fileImporter: newImporter({
|
||||
accept: '.json, .iss',
|
||||
validator: this.importValidator,
|
||||
onImport: this.onImport,
|
||||
parser: this.importParser,
|
||||
onImportFailure: this.onImportFailure
|
||||
}),
|
||||
palettesKeys: [
|
||||
'bg',
|
||||
'fg',
|
||||
'link',
|
||||
'text',
|
||||
'cRed',
|
||||
'cGreen',
|
||||
'cBlue',
|
||||
'cOrange'
|
||||
],
|
||||
userPalette: {},
|
||||
intersectionObserver: null,
|
||||
thirdColumnModeOptions: ['none', 'notifications', 'postform'].map(mode => ({
|
||||
key: mode,
|
||||
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,
|
||||
PaletteEditor
|
||||
},
|
||||
mounted () {
|
||||
this.$store.dispatch('getThemeData')
|
||||
|
||||
const updateIndex = (resource) => {
|
||||
const capitalizedResource = resource[0].toUpperCase() + resource.slice(1)
|
||||
const currentIndex = this.$store.state.instance[`${resource}sIndex`]
|
||||
|
||||
let promise
|
||||
if (currentIndex) {
|
||||
promise = Promise.resolve(currentIndex)
|
||||
} else {
|
||||
promise = this.$store.dispatch(`fetch${capitalizedResource}sIndex`)
|
||||
}
|
||||
|
||||
return promise.then(index => {
|
||||
return Object
|
||||
.entries(index)
|
||||
.map(([k, func]) => [k, func()])
|
||||
})
|
||||
}
|
||||
|
||||
updateIndex('style').then(styles => {
|
||||
styles.forEach(([key, stylePromise]) => stylePromise.then(data => {
|
||||
const meta = data.find(x => x.component === '@meta')
|
||||
this.availableThemesV3.push({ key, data, name: meta.directives.name, version: 'v3' })
|
||||
}))
|
||||
})
|
||||
|
||||
updateIndex('theme').then(themes => {
|
||||
themes.forEach(([key, themePromise]) => themePromise.then(data => {
|
||||
if (!data) {
|
||||
console.warn(`Theme with key ${key} is empty or malformed`)
|
||||
} else if (Array.isArray(data)) {
|
||||
console.warn(`Theme with key ${key} is a v1 theme and should be moved to static/palettes/index.json`)
|
||||
} else if (!data.source && !data.theme) {
|
||||
console.warn(`Theme with key ${key} is malformed`)
|
||||
} else {
|
||||
this.availableThemesV2.push({ key, data, name: data.name, version: 'v2' })
|
||||
}
|
||||
}))
|
||||
})
|
||||
|
||||
this.userPalette = this.$store.state.interface.paletteDataUsed || {}
|
||||
|
||||
updateIndex('palette').then(bundledPalettes => {
|
||||
bundledPalettes.forEach(([key, palettePromise]) => palettePromise.then(v => {
|
||||
let palette
|
||||
if (Array.isArray(v)) {
|
||||
const [
|
||||
name,
|
||||
bg,
|
||||
fg,
|
||||
text,
|
||||
link,
|
||||
cRed = '#FF0000',
|
||||
cGreen = '#00FF00',
|
||||
cBlue = '#0000FF',
|
||||
cOrange = '#E3FF00'
|
||||
] = v
|
||||
palette = { key, name, bg, fg, text, link, cRed, cBlue, cGreen, cOrange }
|
||||
} else {
|
||||
palette = { key, ...v }
|
||||
}
|
||||
if (!palette.key.startsWith('style.')) {
|
||||
this.bundledPalettes.push(palette)
|
||||
}
|
||||
}))
|
||||
})
|
||||
|
||||
if (window.IntersectionObserver) {
|
||||
this.intersectionObserver = new IntersectionObserver((entries, observer) => {
|
||||
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)
|
||||
})
|
||||
})
|
||||
},
|
||||
watch: {
|
||||
paletteDataUsed () {
|
||||
this.userPalette = this.paletteDataUsed || {}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
switchInProgress () {
|
||||
return this.$store.state.interface.themeChangeInProgress
|
||||
},
|
||||
paletteDataUsed () {
|
||||
return this.$store.state.interface.paletteDataUsed
|
||||
},
|
||||
availableStyles () {
|
||||
return [
|
||||
...this.availableThemesV3,
|
||||
...this.availableThemesV2
|
||||
]
|
||||
},
|
||||
availablePalettes () {
|
||||
return [
|
||||
...this.bundledPalettes,
|
||||
...this.stylePalettes
|
||||
]
|
||||
},
|
||||
stylePalettes () {
|
||||
const ruleset = this.$store.state.interface.styleDataUsed || []
|
||||
if (!ruleset && ruleset.length === 0) return
|
||||
const meta = ruleset.find(x => x.component === '@meta')
|
||||
const result = ruleset.filter(x => x.component.startsWith('@palette'))
|
||||
.map(x => {
|
||||
const { variant, directives } = x
|
||||
const {
|
||||
bg,
|
||||
fg,
|
||||
text,
|
||||
link,
|
||||
accent,
|
||||
cRed,
|
||||
cBlue,
|
||||
cGreen,
|
||||
cOrange,
|
||||
wallpaper
|
||||
} = directives
|
||||
|
||||
const result = {
|
||||
name: `${meta.directives.name || this.$t('settings.style.themes3.palette.imported')}: ${variant}`,
|
||||
key: `style.${variant.toLowerCase().replace(/ /g, '_')}`,
|
||||
bg,
|
||||
fg,
|
||||
text,
|
||||
link,
|
||||
accent,
|
||||
cRed,
|
||||
cBlue,
|
||||
cGreen,
|
||||
cOrange,
|
||||
wallpaper
|
||||
}
|
||||
return Object.fromEntries(Object.entries(result).filter(([k, v]) => v))
|
||||
})
|
||||
return result
|
||||
},
|
||||
noIntersectionObserver () {
|
||||
return !window.IntersectionObserver
|
||||
},
|
||||
horizontalUnits () {
|
||||
return defaultHorizontalUnits
|
||||
},
|
||||
fontsOverride () {
|
||||
return this.$store.getters.mergedConfig.fontsOverride
|
||||
},
|
||||
columns () {
|
||||
const mode = this.$store.getters.mergedConfig.thirdColumnMode
|
||||
|
||||
const notif = mode === 'none' ? [] : ['notifs']
|
||||
|
||||
if (this.$store.getters.mergedConfig.sidebarRight || mode === 'postform') {
|
||||
return [...notif, 'content', 'sidebar']
|
||||
} else {
|
||||
return ['sidebar', 'content', ...notif]
|
||||
}
|
||||
},
|
||||
instanceWallpaperUsed () {
|
||||
return this.$store.state.instance.background &&
|
||||
!this.$store.state.users.currentUser.background_image
|
||||
},
|
||||
language: {
|
||||
get: function () { return this.$store.getters.mergedConfig.interfaceLanguage },
|
||||
set: function (val) {
|
||||
this.$store.dispatch('setOption', { name: 'interfaceLanguage', value: val })
|
||||
}
|
||||
},
|
||||
customThemeVersion () {
|
||||
const { themeVersion } = this.$store.state.interface
|
||||
return themeVersion
|
||||
},
|
||||
isCustomThemeUsed () {
|
||||
const { customTheme, customThemeSource } = this.mergedConfig
|
||||
return customTheme != null || customThemeSource != null
|
||||
},
|
||||
isCustomStyleUsed (name) {
|
||||
const { styleCustomData } = this.mergedConfig
|
||||
return styleCustomData != null
|
||||
},
|
||||
...SharedComputedObject()
|
||||
},
|
||||
methods: {
|
||||
updateFont (key, value) {
|
||||
this.$store.dispatch('setOption', {
|
||||
name: 'theme3hacks',
|
||||
value: {
|
||||
...this.mergedConfig.theme3hacks,
|
||||
fonts: {
|
||||
...this.mergedConfig.theme3hacks.fonts,
|
||||
[key]: value
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
importFile () {
|
||||
this.fileImporter.importData()
|
||||
},
|
||||
importParser (file, filename) {
|
||||
if (filename.endsWith('.json')) {
|
||||
return JSON.parse(file)
|
||||
} else if (filename.endsWith('.iss')) {
|
||||
return deserialize(file)
|
||||
}
|
||||
},
|
||||
onImport (parsed, filename) {
|
||||
if (filename.endsWith('.json')) {
|
||||
this.$store.dispatch('setThemeCustom', parsed.source || parsed.theme)
|
||||
} else if (filename.endsWith('.iss')) {
|
||||
this.$store.dispatch('setStyleCustom', parsed)
|
||||
}
|
||||
},
|
||||
onImportFailure (result) {
|
||||
console.error('Failure importing theme:', result)
|
||||
this.$store.dispatch('pushGlobalNotice', { messageKey: 'settings.invalid_theme_imported', level: 'error' })
|
||||
},
|
||||
importValidator (parsed, filename) {
|
||||
if (filename.endsWith('.json')) {
|
||||
const version = parsed._pleroma_theme_version
|
||||
return version >= 1 || version <= 2
|
||||
} else if (filename.endsWith('.iss')) {
|
||||
if (!Array.isArray(parsed)) return false
|
||||
if (parsed.length < 1) return false
|
||||
if (parsed.find(x => x.component === '@meta') == null) return false
|
||||
return true
|
||||
}
|
||||
},
|
||||
isThemeActive (key) {
|
||||
return key === (this.mergedConfig.theme || this.$store.state.instance.theme)
|
||||
},
|
||||
isStyleActive (key) {
|
||||
return key === (this.mergedConfig.style || this.$store.state.instance.style)
|
||||
},
|
||||
isPaletteActive (key) {
|
||||
return key === (this.mergedConfig.palette || this.$store.state.instance.palette)
|
||||
},
|
||||
setStyle (name) {
|
||||
this.$store.dispatch('setStyle', name)
|
||||
},
|
||||
setTheme (name) {
|
||||
this.$store.dispatch('setTheme', name)
|
||||
},
|
||||
setPalette (name, data) {
|
||||
this.$store.dispatch('setPalette', name)
|
||||
this.userPalette = data
|
||||
},
|
||||
setPaletteCustom (data) {
|
||||
this.$store.dispatch('setPaletteCustom', data)
|
||||
this.userPalette = data
|
||||
},
|
||||
resetTheming (name) {
|
||||
this.$store.dispatch('setStyle', 'stock')
|
||||
},
|
||||
previewTheme (key, version, input) {
|
||||
let theme3
|
||||
if (this.compilationCache[key]) {
|
||||
theme3 = this.compilationCache[key]
|
||||
} else if (input) {
|
||||
if (version === 'v2') {
|
||||
const style = normalizeThemeData(input)
|
||||
const theme2 = convertTheme2To3(style)
|
||||
theme3 = init({
|
||||
inputRuleset: theme2,
|
||||
ultimateBackgroundColor: '#000000',
|
||||
liteMode: true,
|
||||
debug: true,
|
||||
onlyNormalState: true
|
||||
})
|
||||
} else if (version === 'v3') {
|
||||
const palette = input.find(x => x.component === '@palette')
|
||||
let paletteRule
|
||||
if (palette) {
|
||||
const { directives } = palette
|
||||
directives.link = directives.link || directives.accent
|
||||
directives.accent = directives.accent || directives.link
|
||||
paletteRule = {
|
||||
component: 'Root',
|
||||
directives: Object.fromEntries(
|
||||
Object
|
||||
.entries(directives)
|
||||
.filter(([k, v]) => k && k !== 'name')
|
||||
.map(([k, v]) => ['--' + k, 'color | ' + v])
|
||||
)
|
||||
}
|
||||
} else {
|
||||
paletteRule = null
|
||||
}
|
||||
|
||||
theme3 = init({
|
||||
inputRuleset: [...input, paletteRule].filter(x => x),
|
||||
ultimateBackgroundColor: '#000000',
|
||||
liteMode: true,
|
||||
debug: true,
|
||||
onlyNormalState: true
|
||||
})
|
||||
}
|
||||
} else {
|
||||
theme3 = init({
|
||||
inputRuleset: [],
|
||||
ultimateBackgroundColor: '#000000',
|
||||
liteMode: true,
|
||||
debug: true,
|
||||
onlyNormalState: true
|
||||
})
|
||||
}
|
||||
|
||||
if (!this.compilationCache[key]) {
|
||||
this.compilationCache[key] = theme3
|
||||
}
|
||||
|
||||
return getScopedVersion(
|
||||
getCssRules(theme3.eager),
|
||||
'#theme-preview-' + key
|
||||
).join('\n')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default AppearanceTab
|
||||
144
src/components/settings_modal/tabs/appearance_tab.scss
Normal file
144
src/components/settings_modal/tabs/appearance_tab.scss
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
.appearance-tab {
|
||||
.palette,
|
||||
.theme-notice {
|
||||
padding: 0.5em;
|
||||
margin: 1em;
|
||||
}
|
||||
|
||||
.setting-item {
|
||||
padding-bottom: 0;
|
||||
|
||||
&.heading {
|
||||
display: grid;
|
||||
align-items: baseline;
|
||||
grid-template-columns: 1fr auto auto auto;
|
||||
grid-gap: 0.5em;
|
||||
|
||||
h2 {
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin: 0.5em 0;
|
||||
}
|
||||
|
||||
.palettes-container {
|
||||
height: 15em;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
scrollbar-gutter: stable;
|
||||
border-radius: var(--roundness);
|
||||
border: 1px solid var(--border);
|
||||
margin: -0.5em;
|
||||
}
|
||||
|
||||
.palettes {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-gap: 0.5em;
|
||||
padding: 0.5em;
|
||||
width: 100%;
|
||||
|
||||
h4 {
|
||||
margin: 0;
|
||||
grid-column: 1 / span 2;
|
||||
}
|
||||
}
|
||||
|
||||
.palette-entry {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 0.5em;
|
||||
height: max-content;
|
||||
|
||||
.palette-label {
|
||||
height: auto;
|
||||
|
||||
label {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.palette-square {
|
||||
flex: 0 0 auto;
|
||||
display: inline-block;
|
||||
min-width: 1em;
|
||||
min-height: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
.column-settings {
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.column-settings .size-label {
|
||||
display: block;
|
||||
margin-bottom: 0.5em;
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
.modal-view.-mobile & {
|
||||
.palettes {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.palette-entry {
|
||||
grid-column: 1;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.palette-label {
|
||||
line-height: 1.5em;
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
}
|
||||
|
||||
.palette-preview {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr 1fr;
|
||||
grid-template-rows: 1em 1em;
|
||||
margin: 0.5em 0;
|
||||
}
|
||||
|
||||
.theme-list {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin: -0.5em 0;
|
||||
height: 25em;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
scrollbar-gutter: stable;
|
||||
border-radius: var(--roundness);
|
||||
border: 1px solid var(--border);
|
||||
padding: 0;
|
||||
margin-bottom: 1em;
|
||||
|
||||
.theme-preview {
|
||||
font-size: 1rem; // fix for firefox
|
||||
width: 19rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin: 0.5em;
|
||||
|
||||
&.placeholder {
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
.theme-preview-container {
|
||||
pointer-events: none;
|
||||
zoom: 0.5;
|
||||
border: none;
|
||||
border-radius: var(--roundness);
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
395
src/components/settings_modal/tabs/appearance_tab.vue
Normal file
395
src/components/settings_modal/tabs/appearance_tab.vue
Normal file
|
|
@ -0,0 +1,395 @@
|
|||
<template>
|
||||
<div
|
||||
class="appearance-tab"
|
||||
:label="$t('settings.general')"
|
||||
>
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.theme') }}</h2>
|
||||
<ul
|
||||
ref="themeList"
|
||||
class="theme-list"
|
||||
>
|
||||
<button
|
||||
class="button-default theme-preview"
|
||||
data-theme-key="stock"
|
||||
:class="{ toggled: isStyleActive('stock'), disabled: switchInProgress }"
|
||||
:disabled="switchInProgress"
|
||||
@click="resetTheming"
|
||||
>
|
||||
<!-- eslint-disable vue/no-v-text-v-html-on-component -->
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<component
|
||||
:is="'style'"
|
||||
v-html="previewTheme('stock', 'v3')"
|
||||
/>
|
||||
<!-- eslint-enable vue/no-v-html -->
|
||||
<!-- eslint-enable vue/no-v-text-v-html-on-component -->
|
||||
<preview id="theme-preview-stock" />
|
||||
<h4 class="theme-name">
|
||||
{{ $t('settings.style.stock_theme_used') }}
|
||||
<span class="alert neutral version">v3</span>
|
||||
</h4>
|
||||
</button>
|
||||
<button
|
||||
v-if="isCustomThemeUsed"
|
||||
disabled
|
||||
class="button-default theme-preview toggled"
|
||||
>
|
||||
<preview />
|
||||
<h4 class="theme-name">
|
||||
{{ $t('settings.style.custom_theme_used') }}
|
||||
<span class="alert neutral version">v2</span>
|
||||
</h4>
|
||||
</button>
|
||||
<button
|
||||
v-if="isCustomStyleUsed"
|
||||
disabled
|
||||
class="button-default theme-preview toggled"
|
||||
>
|
||||
<preview />
|
||||
<h4 class="theme-name">
|
||||
{{ $t('settings.style.custom_style_used') }}
|
||||
<span class="alert neutral version">v3</span>
|
||||
</h4>
|
||||
</button>
|
||||
<button
|
||||
v-for="style in availableStyles"
|
||||
:key="style.key"
|
||||
:data-theme-key="style.key"
|
||||
class="button-default theme-preview"
|
||||
:class="{ toggled: isThemeActive(style.key), disabled: switchInProgress }"
|
||||
:disabled="switchInProgress"
|
||||
@click="style.version === 'v2' ? setTheme(style.key) : setStyle(style.key)"
|
||||
>
|
||||
<!-- eslint-disable vue/no-v-text-v-html-on-component -->
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<div v-if="style.ready || noIntersectionObserver">
|
||||
<component
|
||||
:is="'style'"
|
||||
v-html="previewTheme(style.key, style.version, style.data)"
|
||||
/>
|
||||
</div>
|
||||
<!-- eslint-enable vue/no-v-html -->
|
||||
<!-- eslint-enable vue/no-v-text-v-html-on-component -->
|
||||
<preview :id="'theme-preview-' + style.key" />
|
||||
<h4 class="theme-name">
|
||||
{{ style.name }}
|
||||
<span class="alert neutral version">{{ style.version }}</span>
|
||||
</h4>
|
||||
</button>
|
||||
</ul>
|
||||
<div class="import-file-container">
|
||||
<button
|
||||
class="btn button-default"
|
||||
:class="{ disabled: switchInProgress }"
|
||||
:disabled="switchInProgress"
|
||||
@click="importFile"
|
||||
>
|
||||
<FAIcon icon="folder-open" />
|
||||
{{ $t('settings.style.themes3.editor.load_style') }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.style.themes3.palette.label') }}</h2>
|
||||
<div
|
||||
v-if="customThemeVersion === 'v3'"
|
||||
class="palettes-container"
|
||||
>
|
||||
<h4 v-if="stylePalettes?.length > 0">
|
||||
{{ $t('settings.style.themes3.palette.style') }}
|
||||
</h4>
|
||||
<div class="palettes">
|
||||
<button
|
||||
v-for="p in stylePalettes || []"
|
||||
:key="p.name"
|
||||
class="btn button-default palette-entry"
|
||||
:class="{ toggled: isPaletteActive(p.key), disabled: switchInProgress }"
|
||||
:disabled="switchInProgress"
|
||||
@click="() => setPalette(p.key, p)"
|
||||
>
|
||||
<div class="palette-label">
|
||||
<label>
|
||||
{{ p.name ?? $t('settings.style.themes3.palette.user') }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="palette-preview">
|
||||
<span
|
||||
v-for="c in palettesKeys"
|
||||
:key="c"
|
||||
class="palette-square"
|
||||
:style="{ backgroundColor: p[c], border: '1px solid ' + (p[c] ?? 'var(--text)') }"
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
<h4>{{ $t('settings.style.themes3.palette.bundled') }}</h4>
|
||||
<button
|
||||
v-for="p in bundledPalettes"
|
||||
:key="p.name"
|
||||
class="btn button-default palette-entry"
|
||||
:class="{ toggled: isPaletteActive(p.key), disabled: switchInProgress }"
|
||||
:disabled="switchInProgress"
|
||||
@click="() => setPalette(p.key, p)"
|
||||
>
|
||||
<div class="palette-label">
|
||||
<label>
|
||||
{{ p.name }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="palette-preview">
|
||||
<span
|
||||
v-for="c in palettesKeys"
|
||||
:key="c"
|
||||
class="palette-square"
|
||||
:style="{ backgroundColor: p[c], border: '1px solid ' + (p[c] ?? 'var(--text)') }"
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<template v-if="customThemeVersion === 'v3'">
|
||||
<h4 v-if="expertLevel > 0">
|
||||
{{ $t('settings.style.themes3.palette.user') }}
|
||||
</h4>
|
||||
<PaletteEditor
|
||||
v-if="expertLevel > 0"
|
||||
v-model="userPalette"
|
||||
class="userPalette"
|
||||
:compact="true"
|
||||
:apply="true"
|
||||
:disabled="switchInProgress"
|
||||
@applyPalette="data => setPaletteCustom(data)"
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="customThemeVersion === 'v2'">
|
||||
<div class="alert neutral theme-notice unsupported-theme-v2">
|
||||
{{ $t('settings.style.themes3.palette.v2_unsupported') }}
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.scale_and_layout') }}</h2>
|
||||
<div class="alert neutral theme-notice">
|
||||
{{ $t("settings.style.appearance_tab_note") }}
|
||||
</div>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<UnitSetting
|
||||
path="textSize"
|
||||
:step="0.1"
|
||||
:units="['px', 'rem']"
|
||||
:reset-default="{ 'px': 14, 'rem': 1 }"
|
||||
timed-apply-mode
|
||||
>
|
||||
{{ $t('settings.text_size') }}
|
||||
</UnitSetting>
|
||||
<div>
|
||||
<small>
|
||||
<i18n-t
|
||||
scope="global"
|
||||
keypath="settings.text_size_tip"
|
||||
tag="span"
|
||||
>
|
||||
<code>px</code>
|
||||
<code>rem</code>
|
||||
</i18n-t>
|
||||
<br>
|
||||
<i18n-t
|
||||
scope="global"
|
||||
keypath="settings.text_size_tip2"
|
||||
tag="span"
|
||||
>
|
||||
<code>14px</code>
|
||||
</i18n-t>
|
||||
</small>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<UnitSetting
|
||||
path="emojiSize"
|
||||
:step="0.1"
|
||||
:units="['px', 'rem']"
|
||||
:reset-default="{ 'px': 32, 'rem': 2.2 }"
|
||||
>
|
||||
{{ $t('settings.emoji_size') }}
|
||||
</UnitSetting>
|
||||
<ul
|
||||
class="setting-list suboptions"
|
||||
>
|
||||
<li>
|
||||
<FloatSetting
|
||||
v-if="user"
|
||||
path="emojiReactionsScale"
|
||||
expert="1"
|
||||
>
|
||||
{{ $t('settings.emoji_reactions_scale') }}
|
||||
</FloatSetting>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<UnitSetting
|
||||
path="navbarSize"
|
||||
:step="0.1"
|
||||
:units="['px', 'rem']"
|
||||
:reset-default="{ 'px': 55, 'rem': 3.5 }"
|
||||
>
|
||||
{{ $t('settings.navbar_size') }}
|
||||
</UnitSetting>
|
||||
</li>
|
||||
<h3>{{ $t('settings.style.interface_font_user_override') }}</h3>
|
||||
<li>
|
||||
<FontControl
|
||||
:model-value="mergedConfig.theme3hacks.fonts.interface"
|
||||
name="ui"
|
||||
:label="$t('settings.style.fonts.components.interface')"
|
||||
:fallback="{ family: 'sans-serif' }"
|
||||
no-inherit="1"
|
||||
@update: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>
|
||||
<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>
|
||||
<BooleanSetting path="modalMobileCenter">
|
||||
{{ $t('settings.mobile_center_dialog') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<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" src="./appearance_tab.scss"></style>
|
||||
|
|
@ -80,7 +80,7 @@
|
|||
<span
|
||||
v-else-if="backup.state === 'running'"
|
||||
>
|
||||
{{ $tc('settings.backup_running', backup.processed_number, { number: backup.processed_number }) }}
|
||||
{{ $t('settings.backup_running', { number: backup.processed_number }, backup.processed_number) }}
|
||||
</span>
|
||||
<span
|
||||
v-else-if="backup.state === 'failed'"
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -7,13 +7,11 @@
|
|||
<BooleanSetting path="hideFilteredStatuses">
|
||||
{{ $t('settings.hide_filtered_statuses') }}
|
||||
</BooleanSetting>
|
||||
<ul
|
||||
class="setting-list suboptions"
|
||||
:class="[{disabled: !streaming}]"
|
||||
>
|
||||
<ul class="setting-list suboptions">
|
||||
<li>
|
||||
<BooleanSetting
|
||||
:disabled="hideFilteredStatuses"
|
||||
parent-path="hideFilteredStatuses"
|
||||
:parent-invert="true"
|
||||
path="hideWordFilteredPosts"
|
||||
>
|
||||
{{ $t('settings.hide_wordfiltered_statuses') }}
|
||||
|
|
@ -22,7 +20,8 @@
|
|||
<li>
|
||||
<BooleanSetting
|
||||
v-if="user"
|
||||
:disabled="hideFilteredStatuses"
|
||||
parent-path="hideFilteredStatuses"
|
||||
:parent-invert="true"
|
||||
path="hideMutedThreads"
|
||||
>
|
||||
{{ $t('settings.hide_muted_threads') }}
|
||||
|
|
@ -31,7 +30,8 @@
|
|||
<li>
|
||||
<BooleanSetting
|
||||
v-if="user"
|
||||
:disabled="hideFilteredStatuses"
|
||||
parent-path="hideFilteredStatuses"
|
||||
:parent-invert="true"
|
||||
path="hideMutedPosts"
|
||||
>
|
||||
{{ $t('settings.hide_muted_posts') }}
|
||||
|
|
@ -44,6 +44,11 @@
|
|||
{{ $t('settings.mute_bot_posts') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path="muteSensitiveStatuses">
|
||||
{{ $t('settings.mute_sensitive_posts') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path="hidePostStats">
|
||||
{{ $t('settings.hide_post_stats') }}
|
||||
|
|
@ -51,7 +56,7 @@
|
|||
</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']"
|
||||
unit-set="time"
|
||||
expert="1"
|
||||
>
|
||||
{{ $t('settings.hide_scrobbles_after') }}
|
||||
</UnitSetting>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -3,11 +3,11 @@ 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'
|
||||
import ServerSideIndicator from '../helpers/server_side_indicator.vue'
|
||||
import ProfileSettingIndicator from '../helpers/profile_setting_indicator.vue'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faGlobe
|
||||
|
|
@ -30,6 +30,11 @@ const GeneralTab = {
|
|||
value: mode,
|
||||
label: this.$t(`settings.conversation_display_${mode}`)
|
||||
})),
|
||||
absoluteTime12hOptions: ['24h', '12h'].map(mode => ({
|
||||
key: mode,
|
||||
value: mode,
|
||||
label: this.$t(`settings.absolute_time_format_12h_${mode}`)
|
||||
})),
|
||||
conversationOtherRepliesButtonOptions: ['below', 'inside'].map(mode => ({
|
||||
key: mode,
|
||||
value: mode,
|
||||
|
|
@ -40,16 +45,16 @@ 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,
|
||||
label: this.$t(`settings.user_popover_avatar_action_${mode}`)
|
||||
})),
|
||||
unsavedPostActionOptions: ['save', 'discard', 'confirm'].map(mode => ({
|
||||
key: mode,
|
||||
value: mode,
|
||||
label: this.$t(`settings.unsaved_post_action_${mode}`)
|
||||
})),
|
||||
loopSilentAvailable:
|
||||
// Firefox
|
||||
Object.getOwnPropertyDescriptor(HTMLVideoElement.prototype, 'mozHasAudio') ||
|
||||
|
|
@ -64,15 +69,12 @@ const GeneralTab = {
|
|||
ChoiceSetting,
|
||||
IntegerSetting,
|
||||
FloatSetting,
|
||||
SizeSetting,
|
||||
UnitSetting,
|
||||
InterfaceLanguageSwitcher,
|
||||
ScopeSelector,
|
||||
ServerSideIndicator
|
||||
ProfileSettingIndicator
|
||||
},
|
||||
computed: {
|
||||
horizontalUnits () {
|
||||
return defaultHorizontalUnits
|
||||
},
|
||||
postFormats () {
|
||||
return this.$store.state.instance.postFormats || []
|
||||
},
|
||||
|
|
@ -83,34 +85,19 @@ 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) {
|
||||
this.$store.dispatch('setOption', { name: 'interfaceLanguage', value: val })
|
||||
}
|
||||
},
|
||||
instanceShoutboxPresent () { return this.$store.state.instance.shoutAvailable },
|
||||
instanceSpecificPanelPresent () { return this.$store.state.instance.showInstanceSpecificPanel },
|
||||
...SharedComputedObject()
|
||||
},
|
||||
methods: {
|
||||
changeDefaultScope (value) {
|
||||
this.$store.dispatch('setServerSideOption', { name: 'defaultScope', value })
|
||||
this.$store.dispatch('setProfileOption', { name: 'defaultScope', value })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,11 +15,6 @@
|
|||
{{ $t('settings.hide_isp') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li v-if="instanceWallpaperUsed">
|
||||
<BooleanSetting path="hideInstanceWallpaper">
|
||||
{{ $t('settings.hide_wallpaper') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path="stopGifs">
|
||||
{{ $t('settings.stop_gifs') }}
|
||||
|
|
@ -29,14 +24,11 @@
|
|||
<BooleanSetting path="streaming">
|
||||
{{ $t('settings.streaming') }}
|
||||
</BooleanSetting>
|
||||
<ul
|
||||
class="setting-list suboptions"
|
||||
:class="[{disabled: !streaming}]"
|
||||
>
|
||||
<ul class="setting-list suboptions">
|
||||
<li>
|
||||
<BooleanSetting
|
||||
path="pauseOnUnfocused"
|
||||
:disabled="!streaming"
|
||||
parent-path="streaming"
|
||||
>
|
||||
{{ $t('settings.pause_on_unfocused') }}
|
||||
</BooleanSetting>
|
||||
|
|
@ -101,53 +93,6 @@
|
|||
{{ $t('settings.hide_shoutbox') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<h3>{{ $t('settings.columns') }}</h3>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path="disableStickyHeaders">
|
||||
{{ $t('settings.disable_sticky_headers') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path="showScrollbars">
|
||||
{{ $t('settings.show_scrollbars') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path="sidebarRight">
|
||||
{{ $t('settings.right_sidebar') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path="navbarColumnStretch">
|
||||
{{ $t('settings.navbar_column_stretch') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<ChoiceSetting
|
||||
v-if="user"
|
||||
id="thirdColumnMode"
|
||||
path="thirdColumnMode"
|
||||
:options="thirdColumnModeOptions"
|
||||
>
|
||||
{{ $t('settings.third_column_mode') }}
|
||||
</ChoiceSetting>
|
||||
</li>
|
||||
<li v-if="expertLevel > 0">
|
||||
{{ $t('settings.column_sizes') }}
|
||||
<div class="column-settings">
|
||||
<SizeSetting
|
||||
v-for="column in columns"
|
||||
:key="column"
|
||||
:path="column + 'ColumnWidth'"
|
||||
:units="horizontalUnits"
|
||||
expert="1"
|
||||
>
|
||||
{{ $t('settings.column_sizes_' + column) }}
|
||||
</SizeSetting>
|
||||
</div>
|
||||
</li>
|
||||
<li class="select-multiple">
|
||||
<span class="label">{{ $t('settings.confirm_dialogs') }}</span>
|
||||
<ul class="option-list">
|
||||
|
|
@ -171,6 +116,16 @@
|
|||
{{ $t('settings.confirm_dialogs_mute') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path="modalOnMuteConversation">
|
||||
{{ $t('settings.confirm_dialogs_mute_conversation') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path="modalOnMuteDomain">
|
||||
{{ $t('settings.confirm_dialogs_mute_domain') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path="modalOnDelete">
|
||||
{{ $t('settings.confirm_dialogs_delete') }}
|
||||
|
|
@ -213,7 +168,7 @@
|
|||
</ChoiceSetting>
|
||||
</li>
|
||||
<ul
|
||||
v-if="conversationDisplay !== 'linear'"
|
||||
v-if="mergedConfig.conversationDisplay !== 'linear'"
|
||||
class="setting-list suboptions"
|
||||
>
|
||||
<li>
|
||||
|
|
@ -265,22 +220,55 @@
|
|||
<li>
|
||||
<BooleanSetting
|
||||
v-if="user"
|
||||
path="serverSide_stripRichContent"
|
||||
source="profile"
|
||||
path="stripRichContent"
|
||||
expert="1"
|
||||
>
|
||||
{{ $t('settings.no_rich_text_description') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<FloatSetting
|
||||
v-if="user"
|
||||
path="emojiReactionsScale"
|
||||
<BooleanSetting
|
||||
path="useAbsoluteTimeFormat"
|
||||
expert="1"
|
||||
>
|
||||
{{ $t('settings.emoji_reactions_scale') }}
|
||||
</FloatSetting>
|
||||
{{ $t('settings.absolute_time_format') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<ul
|
||||
v-if="mergedConfig.useAbsoluteTimeFormat"
|
||||
class="setting-list suboptions"
|
||||
>
|
||||
<li>
|
||||
<UnitSetting
|
||||
path="absoluteTimeFormatMinAge"
|
||||
unit-set="time"
|
||||
:units="['s', 'm', 'h', 'd']"
|
||||
:min="0"
|
||||
>
|
||||
{{ $t('settings.absolute_time_format_min_age') }}
|
||||
</UnitSetting>
|
||||
</li>
|
||||
<li>
|
||||
<ChoiceSetting
|
||||
id="absoluteTime12h"
|
||||
path="absoluteTime12h"
|
||||
:options="absoluteTime12hOptions"
|
||||
:expert="1"
|
||||
>
|
||||
{{ $t('settings.absolute_time_format_12h') }}
|
||||
</ChoiceSetting>
|
||||
</li>
|
||||
</ul>
|
||||
<h3>{{ $t('settings.attachments') }}</h3>
|
||||
<li>
|
||||
<BooleanSetting
|
||||
path="imageCompression"
|
||||
expert="1"
|
||||
>
|
||||
{{ $t('settings.image_compression') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting
|
||||
path="useContainFit"
|
||||
|
|
@ -299,7 +287,7 @@
|
|||
<BooleanSetting
|
||||
path="preloadImage"
|
||||
expert="1"
|
||||
:disabled="!hideNsfw"
|
||||
parent-path="hideNsfw"
|
||||
>
|
||||
{{ $t('settings.preload_images') }}
|
||||
</BooleanSetting>
|
||||
|
|
@ -308,7 +296,7 @@
|
|||
<BooleanSetting
|
||||
path="useOneClickNsfw"
|
||||
expert="1"
|
||||
:disabled="!hideNsfw"
|
||||
parent-path="hideNsfw"
|
||||
>
|
||||
{{ $t('settings.use_one_click_nsfw') }}
|
||||
</BooleanSetting>
|
||||
|
|
@ -321,15 +309,13 @@
|
|||
>
|
||||
{{ $t('settings.loop_video') }}
|
||||
</BooleanSetting>
|
||||
<ul
|
||||
class="setting-list suboptions"
|
||||
:class="[{disabled: !streaming}]"
|
||||
>
|
||||
<ul class="setting-list suboptions">
|
||||
<li>
|
||||
<BooleanSetting
|
||||
path="loopVideoSilentOnly"
|
||||
expert="1"
|
||||
:disabled="!loopVideo || !loopSilentAvailable"
|
||||
parent-path="loopVideo"
|
||||
:disabled="!loopSilentAvailable"
|
||||
>
|
||||
{{ $t('settings.loop_video_silent_only') }}
|
||||
</BooleanSetting>
|
||||
|
|
@ -427,18 +413,18 @@
|
|||
<ul class="setting-list">
|
||||
<li>
|
||||
<label for="default-vis">
|
||||
{{ $t('settings.default_vis') }} <ServerSideIndicator :server-side="true" />
|
||||
{{ $t('settings.default_vis') }} <ProfileSettingIndicator :is-profile="true" />
|
||||
<ScopeSelector
|
||||
class="scope-selector"
|
||||
:show-all="true"
|
||||
:user-default="serverSide_defaultScope"
|
||||
:initial-scope="serverSide_defaultScope"
|
||||
:user-default="$store.state.profileConfig.defaultScope"
|
||||
:initial-scope="$store.state.profileConfig.defaultScope"
|
||||
:on-scope-change="changeDefaultScope"
|
||||
/>
|
||||
</label>
|
||||
</li>
|
||||
<li>
|
||||
<!-- <BooleanSetting path="serverSide_defaultNSFW"> -->
|
||||
<!-- <BooleanSetting source="profile" path="defaultNSFW"> -->
|
||||
<BooleanSetting path="sensitiveByDefault">
|
||||
{{ $t('settings.sensitive_by_default') }}
|
||||
</BooleanSetting>
|
||||
|
|
@ -486,22 +472,6 @@
|
|||
{{ $t('settings.minimal_scopes_mode') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting
|
||||
path="alwaysShowNewPostButton"
|
||||
expert="1"
|
||||
>
|
||||
{{ $t('settings.always_show_post_button') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting
|
||||
path="autohideFloatingPostButton"
|
||||
expert="1"
|
||||
>
|
||||
{{ $t('settings.autohide_floating_post_button') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting
|
||||
path="padEmoji"
|
||||
|
|
@ -518,23 +488,25 @@
|
|||
{{ $t('settings.autocomplete_select_first') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting
|
||||
path="autoSaveDraft"
|
||||
>
|
||||
{{ $t('settings.auto_save_draft') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li v-if="!mergedConfig.autoSaveDraft">
|
||||
<ChoiceSetting
|
||||
id="unsavedPostAction"
|
||||
path="unsavedPostAction"
|
||||
:options="unsavedPostActionOptions"
|
||||
>
|
||||
{{ $t('settings.unsaved_post_action') }}
|
||||
</ChoiceSetting>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
|
|
|
|||
|
|
@ -9,17 +9,20 @@ import DomainMuteCard from 'src/components/domain_mute_card/domain_mute_card.vue
|
|||
import SelectableList from 'src/components/selectable_list/selectable_list.vue'
|
||||
import ProgressButton from 'src/components/progress_button/progress_button.vue'
|
||||
import withSubscription from 'src/components/../hocs/with_subscription/with_subscription'
|
||||
import withLoadMore from 'src/components/../hocs/with_load_more/with_load_more'
|
||||
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||
|
||||
const BlockList = withSubscription({
|
||||
const BlockList = withLoadMore({
|
||||
fetch: (props, $store) => $store.dispatch('fetchBlocks'),
|
||||
select: (props, $store) => get($store.state.users.currentUser, 'blockIds', []),
|
||||
destroy: () => {},
|
||||
childPropName: 'items'
|
||||
})(SelectableList)
|
||||
|
||||
const MuteList = withSubscription({
|
||||
const MuteList = withLoadMore({
|
||||
fetch: (props, $store) => $store.dispatch('fetchMutes'),
|
||||
select: (props, $store) => get($store.state.users.currentUser, 'muteIds', []),
|
||||
destroy: () => {},
|
||||
childPropName: 'items'
|
||||
})(SelectableList)
|
||||
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -1,49 +1,239 @@
|
|||
<template>
|
||||
<div :label="$t('settings.notifications')">
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.notification_setting_annoyance') }}</h2>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<BooleanSetting path="closingDrawerMarksAsSeen">
|
||||
{{ $t('settings.notification_setting_drawer_marks_as_seen') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path="ignoreInactionableSeen">
|
||||
{{ $t('settings.notification_setting_ignore_inactionable_seen') }}
|
||||
</BooleanSetting>
|
||||
<div>
|
||||
<small>
|
||||
{{ $t('settings.notification_setting_ignore_inactionable_seen_tip') }}
|
||||
</small>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting
|
||||
path="unseenAtTop"
|
||||
expert="1"
|
||||
>
|
||||
{{ $t('settings.notification_setting_unseen_at_top') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.notification_setting_filters') }}</h2>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<BooleanSetting path="serverSide_blockNotificationsFromStrangers">
|
||||
<BooleanSetting
|
||||
source="profile"
|
||||
path="blockNotificationsFromStrangers"
|
||||
>
|
||||
{{ $t('settings.notification_setting_block_from_strangers') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li class="select-multiple">
|
||||
<span class="label">{{ $t('settings.notification_visibility') }}</span>
|
||||
<ul class="option-list">
|
||||
<li>
|
||||
<h3> {{ $t('settings.notification_visibility') }}</h3>
|
||||
<p v-if="expertLevel > 0">
|
||||
{{ $t('settings.notification_setting_filters_chrome_push') }}
|
||||
</p>
|
||||
<ul class="setting-list two-column">
|
||||
<li>
|
||||
<BooleanSetting path="notificationVisibility.likes">
|
||||
{{ $t('settings.notification_visibility_likes') }}
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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.repeats">
|
||||
{{ $t('settings.notification_visibility_repeats') }}
|
||||
<BooleanSetting
|
||||
path="showAnnouncementsInExtraNotifications"
|
||||
:disabled="!mergedConfig.showExtraNotifications"
|
||||
>
|
||||
{{ $t('settings.notification_extra_announcements') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path="notificationVisibility.follows">
|
||||
{{ $t('settings.notification_visibility_follows') }}
|
||||
<BooleanSetting
|
||||
path="showFollowRequestsInExtraNotifications"
|
||||
:disabled="!mergedConfig.showExtraNotifications"
|
||||
>
|
||||
{{ $t('settings.notification_extra_follow_requests') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path="notificationVisibility.mentions">
|
||||
{{ $t('settings.notification_visibility_mentions') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path="notificationVisibility.moves">
|
||||
{{ $t('settings.notification_visibility_moves') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path="notificationVisibility.emojiReactions">
|
||||
{{ $t('settings.notification_visibility_emoji_reactions') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path="notificationVisibility.polls">
|
||||
{{ $t('settings.notification_visibility_polls') }}
|
||||
<BooleanSetting
|
||||
path="showExtraNotificationsTip"
|
||||
:disabled="!mergedConfig.showExtraNotifications"
|
||||
>
|
||||
{{ $t('settings.notification_extra_tip') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
</ul>
|
||||
|
|
@ -64,10 +254,26 @@
|
|||
>
|
||||
{{ $t('settings.enable_web_push_notifications') }}
|
||||
</BooleanSetting>
|
||||
<ul class="setting-list suboptions">
|
||||
<li>
|
||||
<BooleanSetting
|
||||
path="webPushAlwaysShowNotifications"
|
||||
:disabled="!mergedConfig.webPushNotifications"
|
||||
>
|
||||
{{ $t('settings.enable_web_push_always_show') }}
|
||||
</BooleanSetting>
|
||||
<div :class="{ faint: !mergedConfig.webPushNotifications }">
|
||||
<small>
|
||||
{{ $t('settings.enable_web_push_always_show_tip') }}
|
||||
</small>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting
|
||||
path="serverSide_webPushHideContents"
|
||||
source="profile"
|
||||
path="webPushHideContents"
|
||||
expert="1"
|
||||
>
|
||||
{{ $t('settings.notification_setting_hide_notification_contents') }}
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
@ -40,6 +41,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,
|
||||
|
|
@ -58,7 +60,8 @@ const ProfileTab = {
|
|||
ProgressButton,
|
||||
Checkbox,
|
||||
BooleanSetting,
|
||||
InterfaceLanguageSwitcher
|
||||
InterfaceLanguageSwitcher,
|
||||
Select
|
||||
},
|
||||
computed: {
|
||||
user () {
|
||||
|
|
@ -117,6 +120,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: {
|
||||
|
|
@ -128,7 +137,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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
<input
|
||||
id="username"
|
||||
v-model="newName"
|
||||
class="name-changer"
|
||||
class="input name-changer"
|
||||
v-bind="propsToNative(inputProps)"
|
||||
>
|
||||
</template>
|
||||
|
|
@ -26,7 +26,7 @@
|
|||
<template #default="inputProps">
|
||||
<textarea
|
||||
v-model="newBio"
|
||||
class="bio resize-height"
|
||||
class="input bio resize-height"
|
||||
v-bind="propsToNative(inputProps)"
|
||||
/>
|
||||
</template>
|
||||
|
|
@ -47,7 +47,7 @@
|
|||
id="birthday"
|
||||
v-model="newBirthday"
|
||||
type="date"
|
||||
class="birthday-input"
|
||||
class="input birthday-input"
|
||||
>
|
||||
<Checkbox v-model="showBirthday">
|
||||
{{ $t('settings.birthday.show_birthday') }}
|
||||
|
|
@ -71,6 +71,7 @@
|
|||
v-model="newFields[i].name"
|
||||
:placeholder="$t('settings.profile_fields.name')"
|
||||
v-bind="propsToNative(inputProps)"
|
||||
class="input"
|
||||
>
|
||||
</template>
|
||||
</EmojiInput>
|
||||
|
|
@ -85,6 +86,7 @@
|
|||
v-model="newFields[i].value"
|
||||
:placeholder="$t('settings.profile_fields.value')"
|
||||
v-bind="propsToNative(inputProps)"
|
||||
class="input"
|
||||
>
|
||||
</template>
|
||||
</EmojiInput>
|
||||
|
|
@ -109,10 +111,24 @@
|
|||
</button>
|
||||
</div>
|
||||
<p>
|
||||
<Checkbox v-model="bot">
|
||||
{{ $t('settings.bot') }}
|
||||
</Checkbox>
|
||||
<label>
|
||||
{{ $t('settings.actor_type') }}
|
||||
<Select v-model="actorType">
|
||||
<option
|
||||
v-for="option in availableActorTypes"
|
||||
:key="option"
|
||||
:value="option"
|
||||
>
|
||||
{{ $t('settings.actor_type_' + option) }}
|
||||
</option>
|
||||
</Select>
|
||||
</label>
|
||||
</p>
|
||||
<div v-if="groupActorAvailable">
|
||||
<small>
|
||||
{{ $t('settings.actor_type_description') }}
|
||||
</small>
|
||||
</div>
|
||||
<p>
|
||||
<interface-language-switcher
|
||||
:prompt-text="$t('settings.email_language')"
|
||||
|
|
@ -191,6 +207,7 @@
|
|||
<div>
|
||||
<input
|
||||
type="file"
|
||||
class="input"
|
||||
@change="uploadFile('banner', $event)"
|
||||
>
|
||||
</div>
|
||||
|
|
@ -233,6 +250,7 @@
|
|||
<div>
|
||||
<input
|
||||
type="file"
|
||||
class="input"
|
||||
@change="uploadFile('background', $event)"
|
||||
>
|
||||
</div>
|
||||
|
|
@ -254,37 +272,50 @@
|
|||
<h2>{{ $t('settings.account_privacy') }}</h2>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<BooleanSetting path="serverSide_locked">
|
||||
<BooleanSetting
|
||||
source="profile"
|
||||
path="locked"
|
||||
>
|
||||
{{ $t('settings.lock_account_description') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path="serverSide_discoverable">
|
||||
<BooleanSetting
|
||||
source="profile"
|
||||
path="discoverable"
|
||||
>
|
||||
{{ $t('settings.discoverable') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path="serverSide_allowFollowingMove">
|
||||
<BooleanSetting
|
||||
source="profile"
|
||||
path="allowFollowingMove"
|
||||
>
|
||||
{{ $t('settings.allow_following_move') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path="serverSide_hideFavorites">
|
||||
<BooleanSetting
|
||||
source="profile"
|
||||
path="hideFavorites"
|
||||
>
|
||||
{{ $t('settings.hide_favorites_description') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path="serverSide_hideFollowers">
|
||||
<BooleanSetting
|
||||
source="profile"
|
||||
path="hideFollowers"
|
||||
>
|
||||
{{ $t('settings.hide_followers_description') }}
|
||||
</BooleanSetting>
|
||||
<ul
|
||||
class="setting-list suboptions"
|
||||
:class="[{disabled: !serverSide_hideFollowers}]"
|
||||
>
|
||||
<ul class="setting-list suboptions">
|
||||
<li>
|
||||
<BooleanSetting
|
||||
path="serverSide_hideFollowersCount"
|
||||
:disabled="!serverSide_hideFollowers"
|
||||
source="profile"
|
||||
path="hideFollowersCount"
|
||||
parent-path="hideFollowers"
|
||||
>
|
||||
{{ $t('settings.hide_followers_count_description') }}
|
||||
</BooleanSetting>
|
||||
|
|
@ -292,17 +323,18 @@
|
|||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path="serverSide_hideFollows">
|
||||
<BooleanSetting
|
||||
source="profile"
|
||||
path="hideFollows"
|
||||
>
|
||||
{{ $t('settings.hide_follows_description') }}
|
||||
</BooleanSetting>
|
||||
<ul
|
||||
class="setting-list suboptions"
|
||||
:class="[{disabled: !serverSide_hideFollows}]"
|
||||
>
|
||||
<ul class="setting-list suboptions">
|
||||
<li>
|
||||
<BooleanSetting
|
||||
path="serverSide_hideFollowsCount"
|
||||
:disabled="!serverSide_hideFollows"
|
||||
source="profile"
|
||||
path="hideFollowsCount"
|
||||
parent-path="hideFollows"
|
||||
>
|
||||
{{ $t('settings.hide_follows_count_description') }}
|
||||
</BooleanSetting>
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@
|
|||
<input
|
||||
v-model="currentPassword"
|
||||
type="password"
|
||||
class="input"
|
||||
>
|
||||
</confirm>
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
v-model="newEmail"
|
||||
type="email"
|
||||
autocomplete="email"
|
||||
class="input"
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
|
|
@ -16,6 +17,7 @@
|
|||
v-model="changeEmailPassword"
|
||||
type="password"
|
||||
autocomplete="current-password"
|
||||
class="input"
|
||||
>
|
||||
</div>
|
||||
<button
|
||||
|
|
@ -40,6 +42,7 @@
|
|||
<input
|
||||
v-model="changePasswordInputs[0]"
|
||||
type="password"
|
||||
class="input"
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
|
|
@ -47,6 +50,7 @@
|
|||
<input
|
||||
v-model="changePasswordInputs[1]"
|
||||
type="password"
|
||||
class="input"
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
|
|
@ -54,6 +58,7 @@
|
|||
<input
|
||||
v-model="changePasswordInputs[2]"
|
||||
type="password"
|
||||
class="input"
|
||||
>
|
||||
</div>
|
||||
<button
|
||||
|
|
@ -143,8 +148,9 @@
|
|||
/>
|
||||
</div>
|
||||
<div>
|
||||
<i18n
|
||||
path="settings.new_alias_target"
|
||||
<i18n-t
|
||||
scope="global"
|
||||
keypath="settings.new_alias_target"
|
||||
tag="p"
|
||||
>
|
||||
<code
|
||||
|
|
@ -152,9 +158,10 @@
|
|||
>
|
||||
foo@example.org
|
||||
</code>
|
||||
</i18n>
|
||||
</i18n-t>
|
||||
<input
|
||||
v-model="addAliasTarget"
|
||||
class="input"
|
||||
>
|
||||
</div>
|
||||
<button
|
||||
|
|
@ -175,18 +182,20 @@
|
|||
<h2>{{ $t('settings.move_account') }}</h2>
|
||||
<p>{{ $t('settings.move_account_notes') }}</p>
|
||||
<div>
|
||||
<i18n
|
||||
path="settings.move_account_target"
|
||||
<i18n-t
|
||||
keypath="settings.move_account_target"
|
||||
tag="p"
|
||||
scope="global"
|
||||
>
|
||||
<code
|
||||
place="example"
|
||||
>
|
||||
foo@example.org
|
||||
</code>
|
||||
</i18n>
|
||||
<template #example>
|
||||
<code>
|
||||
foo@example.org
|
||||
</code>
|
||||
</template>
|
||||
</i18n-t>
|
||||
<input
|
||||
v-model="moveAccountTarget"
|
||||
class="input"
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
|
|
@ -195,6 +204,7 @@
|
|||
v-model="moveAccountPassword"
|
||||
type="password"
|
||||
autocomplete="current-password"
|
||||
class="input"
|
||||
>
|
||||
</div>
|
||||
<button
|
||||
|
|
@ -222,6 +232,7 @@
|
|||
<input
|
||||
v-model="deleteAccountConfirmPasswordInput"
|
||||
type="password"
|
||||
class="input"
|
||||
>
|
||||
<button
|
||||
class="btn button-default"
|
||||
|
|
|
|||
835
src/components/settings_modal/tabs/style_tab/style_tab.js
Normal file
835
src/components/settings_modal/tabs/style_tab/style_tab.js
Normal file
|
|
@ -0,0 +1,835 @@
|
|||
import { ref, reactive, computed, watch, watchEffect, provide, getCurrentInstance } from 'vue'
|
||||
import { useStore } from 'vuex'
|
||||
import { get, set, unset, throttle } from 'lodash'
|
||||
|
||||
import Select from 'src/components/select/select.vue'
|
||||
import SelectMotion from 'src/components/select/select_motion.vue'
|
||||
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||
import ComponentPreview from 'src/components/component_preview/component_preview.vue'
|
||||
import StringSetting from '../../helpers/string_setting.vue'
|
||||
import ShadowControl from 'src/components/shadow_control/shadow_control.vue'
|
||||
import ColorInput from 'src/components/color_input/color_input.vue'
|
||||
import PaletteEditor from 'src/components/palette_editor/palette_editor.vue'
|
||||
import OpacityInput from 'src/components/opacity_input/opacity_input.vue'
|
||||
import RoundnessInput from 'src/components/roundness_input/roundness_input.vue'
|
||||
import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'
|
||||
import Tooltip from 'src/components/tooltip/tooltip.vue'
|
||||
import ContrastRatio from 'src/components/contrast_ratio/contrast_ratio.vue'
|
||||
import Preview from '../theme_tab/theme_preview.vue'
|
||||
|
||||
import VirtualDirectivesTab from './virtual_directives_tab.vue'
|
||||
|
||||
import { init, findColor } from 'src/services/theme_data/theme_data_3.service.js'
|
||||
import {
|
||||
getCssRules,
|
||||
getScopedVersion
|
||||
} from 'src/services/theme_data/css_utils.js'
|
||||
import { serialize } from 'src/services/theme_data/iss_serializer.js'
|
||||
import { deserializeShadow, deserialize } from 'src/services/theme_data/iss_deserializer.js'
|
||||
import {
|
||||
rgb2hex,
|
||||
hex2rgb,
|
||||
getContrastRatio
|
||||
} from 'src/services/color_convert/color_convert.js'
|
||||
import {
|
||||
newImporter,
|
||||
newExporter
|
||||
} from 'src/services/export_import/export_import.js'
|
||||
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faFloppyDisk,
|
||||
faFolderOpen,
|
||||
faFile,
|
||||
faArrowsRotate,
|
||||
faCheck
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
// helper for debugging
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const toValue = (x) => JSON.parse(JSON.stringify(x === undefined ? 'null' : x))
|
||||
|
||||
// helper to make states comparable
|
||||
const normalizeStates = (states) => ['normal', ...(states?.filter(x => x !== 'normal') || [])].join(':')
|
||||
|
||||
library.add(
|
||||
faFile,
|
||||
faFloppyDisk,
|
||||
faFolderOpen,
|
||||
faArrowsRotate,
|
||||
faCheck
|
||||
)
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Select,
|
||||
SelectMotion,
|
||||
Checkbox,
|
||||
Tooltip,
|
||||
StringSetting,
|
||||
ComponentPreview,
|
||||
TabSwitcher,
|
||||
ShadowControl,
|
||||
ColorInput,
|
||||
PaletteEditor,
|
||||
OpacityInput,
|
||||
RoundnessInput,
|
||||
ContrastRatio,
|
||||
Preview,
|
||||
VirtualDirectivesTab
|
||||
},
|
||||
setup (props, context) {
|
||||
const exports = {}
|
||||
const store = useStore()
|
||||
// All rules that are made by editor
|
||||
const allEditedRules = ref(store.state.interface.styleDataUsed || {})
|
||||
const styleDataUsed = computed(() => store.state.interface.styleDataUsed)
|
||||
|
||||
watch([styleDataUsed], (value) => {
|
||||
onImport(store.state.interface.styleDataUsed)
|
||||
}, { once: true })
|
||||
|
||||
exports.isActive = computed(() => {
|
||||
const tabSwitcher = getCurrentInstance().parent.ctx
|
||||
return tabSwitcher ? tabSwitcher.isActive('style') : false
|
||||
})
|
||||
|
||||
// ## Meta stuff
|
||||
exports.name = ref('')
|
||||
exports.author = ref('')
|
||||
exports.license = ref('')
|
||||
exports.website = ref('')
|
||||
|
||||
const metaOut = computed(() => {
|
||||
return [
|
||||
'@meta {',
|
||||
` name: ${exports.name.value};`,
|
||||
` author: ${exports.author.value};`,
|
||||
` license: ${exports.license.value};`,
|
||||
` website: ${exports.website.value};`,
|
||||
'}'
|
||||
].join('\n')
|
||||
})
|
||||
|
||||
const metaRule = computed(() => ({
|
||||
component: '@meta',
|
||||
directives: {
|
||||
name: exports.name.value,
|
||||
author: exports.author.value,
|
||||
license: exports.license.value,
|
||||
website: exports.website.value
|
||||
}
|
||||
}))
|
||||
|
||||
// ## Palette stuff
|
||||
const palettes = reactive([
|
||||
{
|
||||
name: 'default',
|
||||
bg: '#121a24',
|
||||
fg: '#182230',
|
||||
text: '#b9b9ba',
|
||||
link: '#d8a070',
|
||||
accent: '#d8a070',
|
||||
cRed: '#FF0000',
|
||||
cBlue: '#0095ff',
|
||||
cGreen: '#0fa00f',
|
||||
cOrange: '#ffa500'
|
||||
},
|
||||
{
|
||||
name: 'light',
|
||||
bg: '#f2f6f9',
|
||||
fg: '#d6dfed',
|
||||
text: '#304055',
|
||||
underlay: '#5d6086',
|
||||
accent: '#f55b1b',
|
||||
cBlue: '#0095ff',
|
||||
cRed: '#d31014',
|
||||
cGreen: '#0fa00f',
|
||||
cOrange: '#ffa500',
|
||||
border: '#d8e6f9'
|
||||
}
|
||||
])
|
||||
exports.palettes = palettes
|
||||
|
||||
// This is kinda dumb but you cannot "replace" reactive() object
|
||||
// and so v-model simply fails when you try to chage (increase only?)
|
||||
// length of the array. Since linter complains about mutating modelValue
|
||||
// inside SelectMotion, the next best thing is to just wipe existing array
|
||||
// and replace it with new one.
|
||||
|
||||
const onPalettesUpdate = (e) => {
|
||||
palettes.splice(0, palettes.length)
|
||||
palettes.push(...e)
|
||||
}
|
||||
exports.onPalettesUpdate = onPalettesUpdate
|
||||
|
||||
const selectedPaletteId = ref(0)
|
||||
const selectedPalette = computed({
|
||||
get () {
|
||||
return palettes[selectedPaletteId.value]
|
||||
},
|
||||
set (newPalette) {
|
||||
palettes[selectedPaletteId.value] = newPalette
|
||||
}
|
||||
})
|
||||
exports.selectedPaletteId = selectedPaletteId
|
||||
exports.selectedPalette = selectedPalette
|
||||
provide('selectedPalette', selectedPalette)
|
||||
|
||||
watch([selectedPalette], () => updateOverallPreview())
|
||||
|
||||
exports.getNewPalette = () => ({
|
||||
name: 'new palette',
|
||||
bg: '#121a24',
|
||||
fg: '#182230',
|
||||
text: '#b9b9ba',
|
||||
link: '#d8a070',
|
||||
accent: '#d8a070',
|
||||
cRed: '#FF0000',
|
||||
cBlue: '#0095ff',
|
||||
cGreen: '#0fa00f',
|
||||
cOrange: '#ffa500'
|
||||
})
|
||||
|
||||
// Raw format
|
||||
const palettesRule = computed(() => {
|
||||
return palettes.map(palette => {
|
||||
const { name, ...rest } = palette
|
||||
return {
|
||||
component: '@palette',
|
||||
variant: name,
|
||||
directives: Object
|
||||
.entries(rest)
|
||||
.filter(([k, v]) => v && k)
|
||||
.reduce((acc, [k, v]) => ({ ...acc, [k]: v }), {})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// Text format
|
||||
const palettesOut = computed(() => {
|
||||
return palettes.map(({ name, ...palette }) => {
|
||||
const entries = Object
|
||||
.entries(palette)
|
||||
.filter(([k, v]) => v && k)
|
||||
.map(([slot, data]) => ` ${slot}: ${data};`)
|
||||
.join('\n')
|
||||
|
||||
return `@palette.${name} {\n${entries}\n}`
|
||||
}).join('\n\n')
|
||||
})
|
||||
|
||||
// ## Components stuff
|
||||
// Getting existing components
|
||||
const componentsContext = require.context('src', true, /\.style.js(on)?$/)
|
||||
const componentKeysAll = componentsContext.keys()
|
||||
const componentsMap = new Map(
|
||||
componentKeysAll
|
||||
.map(
|
||||
key => [key, componentsContext(key).default]
|
||||
).filter(([key, component]) => !component.virtual && !component.notEditable)
|
||||
)
|
||||
exports.componentsMap = componentsMap
|
||||
const componentKeys = [...componentsMap.keys()]
|
||||
exports.componentKeys = componentKeys
|
||||
|
||||
// Component list and selection
|
||||
const selectedComponentKey = ref(componentsMap.keys().next().value)
|
||||
exports.selectedComponentKey = selectedComponentKey
|
||||
|
||||
const selectedComponent = computed(() => componentsMap.get(selectedComponentKey.value))
|
||||
const selectedComponentName = computed(() => selectedComponent.value.name)
|
||||
|
||||
// Selection basis
|
||||
exports.selectedComponentVariants = computed(() => {
|
||||
return Object.keys({ normal: null, ...(selectedComponent.value.variants || {}) })
|
||||
})
|
||||
exports.selectedComponentStates = computed(() => {
|
||||
const all = Object.keys({ normal: null, ...(selectedComponent.value.states || {}) })
|
||||
return all.filter(x => x !== 'normal')
|
||||
})
|
||||
|
||||
// selection
|
||||
const selectedVariant = ref('normal')
|
||||
exports.selectedVariant = selectedVariant
|
||||
const selectedState = reactive(new Set())
|
||||
exports.selectedState = selectedState
|
||||
exports.updateSelectedStates = (state, v) => {
|
||||
if (v) {
|
||||
selectedState.add(state)
|
||||
} else {
|
||||
selectedState.delete(state)
|
||||
}
|
||||
}
|
||||
|
||||
// Reset variant and state on component change
|
||||
const updateSelectedComponent = () => {
|
||||
selectedVariant.value = 'normal'
|
||||
selectedState.clear()
|
||||
}
|
||||
|
||||
watch(
|
||||
selectedComponentName,
|
||||
updateSelectedComponent
|
||||
)
|
||||
|
||||
// ### Rules stuff aka meat and potatoes
|
||||
// The native structure of separate rules and the child -> parent
|
||||
// relation isn't very convenient for editor, we replace the array
|
||||
// and child -> parent structure with map and parent -> child structure
|
||||
const rulesToEditorFriendly = (rules, root = {}) => rules.reduce((acc, rule) => {
|
||||
const { parent: rParent, component: rComponent } = rule
|
||||
const parent = rParent ?? rule
|
||||
const hasChildren = !!rParent
|
||||
const child = hasChildren ? rule : null
|
||||
|
||||
const {
|
||||
component: pComponent,
|
||||
variant: pVariant = 'normal',
|
||||
state: pState = [] // no relation to Intel CPUs whatsoever
|
||||
} = parent
|
||||
|
||||
const pPath = `${hasChildren ? pComponent : rComponent}.${pVariant}.${normalizeStates(pState)}`
|
||||
|
||||
let output = get(acc, pPath)
|
||||
if (!output) {
|
||||
set(acc, pPath, {})
|
||||
output = get(acc, pPath)
|
||||
}
|
||||
|
||||
if (hasChildren) {
|
||||
output._children = output._children ?? {}
|
||||
const {
|
||||
component: cComponent,
|
||||
variant: cVariant = 'normal',
|
||||
state: cState = [],
|
||||
directives
|
||||
} = child
|
||||
|
||||
const cPath = `${cComponent}.${cVariant}.${normalizeStates(cState)}`
|
||||
set(output._children, cPath, { directives })
|
||||
} else {
|
||||
output.directives = parent.directives
|
||||
}
|
||||
return acc
|
||||
}, root)
|
||||
|
||||
const editorFriendlyFallbackStructure = computed(() => {
|
||||
const root = {}
|
||||
|
||||
componentKeys.forEach((componentKey) => {
|
||||
const componentValue = componentsMap.get(componentKey)
|
||||
const { defaultRules, name } = componentValue
|
||||
rulesToEditorFriendly(
|
||||
defaultRules.map((rule) => ({ ...rule, component: name })),
|
||||
root
|
||||
)
|
||||
})
|
||||
|
||||
return root
|
||||
})
|
||||
|
||||
// Checking whether component can support some "directives" which
|
||||
// are actually virtual subcomponents, i.e. Text, Link etc
|
||||
exports.componentHas = (subComponent) => {
|
||||
return !!selectedComponent.value.validInnerComponents?.find(x => x === subComponent)
|
||||
}
|
||||
|
||||
// Path for lodash's get and set
|
||||
const getPath = (component, directive) => {
|
||||
const pathSuffix = component ? `._children.${component}.normal.normal` : ''
|
||||
const path = `${selectedComponentName.value}.${selectedVariant.value}.${normalizeStates([...selectedState])}${pathSuffix}.directives.${directive}`
|
||||
return path
|
||||
}
|
||||
|
||||
// Templates for directives
|
||||
const isElementPresent = (component, directive, defaultValue = '') => computed({
|
||||
get () {
|
||||
return get(allEditedRules.value, getPath(component, directive)) != null
|
||||
},
|
||||
set (value) {
|
||||
if (value) {
|
||||
const fallback = get(
|
||||
editorFriendlyFallbackStructure.value,
|
||||
getPath(component, directive)
|
||||
)
|
||||
set(allEditedRules.value, getPath(component, directive), fallback ?? defaultValue)
|
||||
} else {
|
||||
unset(allEditedRules.value, getPath(component, directive))
|
||||
}
|
||||
exports.updateOverallPreview()
|
||||
}
|
||||
})
|
||||
|
||||
const getEditedElement = (component, directive, postProcess = x => x) => computed({
|
||||
get () {
|
||||
let usedRule
|
||||
const fallback = editorFriendlyFallbackStructure.value
|
||||
const real = allEditedRules.value
|
||||
const path = getPath(component, directive)
|
||||
|
||||
usedRule = get(real, path) // get real
|
||||
if (!usedRule) {
|
||||
usedRule = get(fallback, path)
|
||||
}
|
||||
|
||||
return postProcess(usedRule)
|
||||
},
|
||||
set (value) {
|
||||
if (value) {
|
||||
set(allEditedRules.value, getPath(component, directive), value)
|
||||
} else {
|
||||
unset(allEditedRules.value, getPath(component, directive))
|
||||
}
|
||||
exports.updateOverallPreview()
|
||||
}
|
||||
})
|
||||
|
||||
// All the editable stuff for the component
|
||||
exports.editedBackgroundColor = getEditedElement(null, 'background')
|
||||
exports.isBackgroundColorPresent = isElementPresent(null, 'background', '#FFFFFF')
|
||||
exports.editedOpacity = getEditedElement(null, 'opacity')
|
||||
exports.isOpacityPresent = isElementPresent(null, 'opacity', 1)
|
||||
exports.editedRoundness = getEditedElement(null, 'roundness')
|
||||
exports.isRoundnessPresent = isElementPresent(null, 'roundness', '0')
|
||||
exports.editedTextColor = getEditedElement('Text', 'textColor')
|
||||
exports.isTextColorPresent = isElementPresent('Text', 'textColor', '#000000')
|
||||
exports.editedTextAuto = getEditedElement('Text', 'textAuto')
|
||||
exports.isTextAutoPresent = isElementPresent('Text', 'textAuto', '#000000')
|
||||
exports.editedLinkColor = getEditedElement('Link', 'textColor')
|
||||
exports.isLinkColorPresent = isElementPresent('Link', 'textColor', '#000080')
|
||||
exports.editedIconColor = getEditedElement('Icon', 'textColor')
|
||||
exports.isIconColorPresent = isElementPresent('Icon', 'textColor', '#909090')
|
||||
exports.editedBorderColor = getEditedElement('Border', 'textColor')
|
||||
exports.isBorderColorPresent = isElementPresent('Border', 'textColor', '#909090')
|
||||
|
||||
const getContrast = (bg, text) => {
|
||||
try {
|
||||
const bgRgb = hex2rgb(bg)
|
||||
const textRgb = hex2rgb(text)
|
||||
|
||||
const ratio = getContrastRatio(bgRgb, textRgb)
|
||||
return {
|
||||
// TODO this ideally should be part of <ContractRatio />
|
||||
ratio,
|
||||
text: ratio.toPrecision(3) + ':1',
|
||||
// AA level, AAA level
|
||||
aa: ratio >= 4.5,
|
||||
aaa: ratio >= 7,
|
||||
// same but for 18pt+ texts
|
||||
laa: ratio >= 3,
|
||||
laaa: ratio >= 4.5
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failure computing contrast', e)
|
||||
return { error: e }
|
||||
}
|
||||
}
|
||||
|
||||
const normalizeShadows = (shadows) => {
|
||||
return shadows?.map(shadow => {
|
||||
if (typeof shadow === 'object') {
|
||||
return shadow
|
||||
}
|
||||
if (typeof shadow === 'string') {
|
||||
try {
|
||||
return deserializeShadow(shadow)
|
||||
} catch (e) {
|
||||
console.warn(e)
|
||||
return shadow
|
||||
}
|
||||
}
|
||||
return null
|
||||
})
|
||||
}
|
||||
provide('normalizeShadows', normalizeShadows)
|
||||
|
||||
// Shadow is partially edited outside the ShadowControl
|
||||
// for better space utilization
|
||||
const editedShadow = getEditedElement(null, 'shadow', normalizeShadows)
|
||||
exports.editedShadow = editedShadow
|
||||
const editedSubShadowId = ref(null)
|
||||
exports.editedSubShadowId = editedSubShadowId
|
||||
const editedSubShadow = computed(() => {
|
||||
if (editedShadow.value == null || editedSubShadowId.value == null) return null
|
||||
return editedShadow.value[editedSubShadowId.value]
|
||||
})
|
||||
exports.editedSubShadow = editedSubShadow
|
||||
exports.isShadowPresent = isElementPresent(null, 'shadow', [])
|
||||
exports.onSubShadow = (id) => {
|
||||
if (id != null) {
|
||||
editedSubShadowId.value = id
|
||||
} else {
|
||||
editedSubShadow.value = null
|
||||
}
|
||||
}
|
||||
exports.updateSubShadow = (axis, value) => {
|
||||
if (!editedSubShadow.value || editedSubShadowId.value == null) return
|
||||
const newEditedShadow = [...editedShadow.value]
|
||||
|
||||
newEditedShadow[editedSubShadowId.value] = {
|
||||
...newEditedShadow[editedSubShadowId.value],
|
||||
[axis]: value
|
||||
}
|
||||
|
||||
editedShadow.value = newEditedShadow
|
||||
}
|
||||
exports.isShadowTabOpen = ref(false)
|
||||
exports.onTabSwitch = (tab) => {
|
||||
exports.isShadowTabOpen.value = tab === 'shadow'
|
||||
}
|
||||
|
||||
// component preview
|
||||
exports.editorHintStyle = computed(() => {
|
||||
const editorHint = selectedComponent.value.editor
|
||||
const styles = []
|
||||
if (editorHint && Object.keys(editorHint).length > 0) {
|
||||
if (editorHint.aspect != null) {
|
||||
styles.push(`aspect-ratio: ${editorHint.aspect} !important;`)
|
||||
}
|
||||
if (editorHint.border != null) {
|
||||
styles.push(`border-width: ${editorHint.border}px !important;`)
|
||||
}
|
||||
}
|
||||
return styles.join('; ')
|
||||
})
|
||||
|
||||
const editorFriendlyToOriginal = computed(() => {
|
||||
const resultRules = []
|
||||
|
||||
const convert = (component, data = {}, parent) => {
|
||||
const variants = Object.entries(data || {})
|
||||
|
||||
variants.forEach(([variant, variantData]) => {
|
||||
const states = Object.entries(variantData)
|
||||
|
||||
states.forEach(([jointState, stateData]) => {
|
||||
const state = jointState.split(/:/g)
|
||||
const result = {
|
||||
component,
|
||||
variant,
|
||||
state,
|
||||
directives: stateData.directives || {}
|
||||
}
|
||||
|
||||
if (parent) {
|
||||
result.parent = {
|
||||
component: parent
|
||||
}
|
||||
}
|
||||
|
||||
resultRules.push(result)
|
||||
|
||||
// Currently we only support single depth for simplicity's sake
|
||||
if (!parent) {
|
||||
Object.entries(stateData._children || {}).forEach(([cName, child]) => convert(cName, child, component))
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
[...componentsMap.values()].forEach(({ name }) => {
|
||||
convert(name, allEditedRules.value[name])
|
||||
})
|
||||
|
||||
return resultRules
|
||||
})
|
||||
|
||||
const allCustomVirtualDirectives = [...componentsMap.values()]
|
||||
.map(c => {
|
||||
return c
|
||||
.defaultRules
|
||||
.filter(c => c.component === 'Root')
|
||||
.map(x => Object.entries(x.directives))
|
||||
.flat()
|
||||
})
|
||||
.filter(x => x)
|
||||
.flat()
|
||||
.map(([name, value]) => {
|
||||
const [valType, valVal] = value.split('|')
|
||||
return {
|
||||
name: name.substring(2),
|
||||
valType: valType?.trim(),
|
||||
value: valVal?.trim()
|
||||
}
|
||||
})
|
||||
|
||||
const virtualDirectives = ref(allCustomVirtualDirectives)
|
||||
exports.virtualDirectives = virtualDirectives
|
||||
exports.updateVirtualDirectives = (value) => {
|
||||
virtualDirectives.value = value
|
||||
}
|
||||
|
||||
// Raw format
|
||||
const virtualDirectivesRule = computed(() => ({
|
||||
component: 'Root',
|
||||
directives: Object.fromEntries(
|
||||
virtualDirectives.value.map(vd => [`--${vd.name}`, `${vd.valType} | ${vd.value}`])
|
||||
)
|
||||
}))
|
||||
|
||||
// Text format
|
||||
const virtualDirectivesOut = computed(() => {
|
||||
return [
|
||||
'Root {',
|
||||
...virtualDirectives.value
|
||||
.filter(vd => vd.name && vd.valType && vd.value)
|
||||
.map(vd => ` --${vd.name}: ${vd.valType} | ${vd.value};`),
|
||||
'}'
|
||||
].join('\n')
|
||||
})
|
||||
|
||||
exports.computeColor = (color) => {
|
||||
let computedColor
|
||||
try {
|
||||
computedColor = findColor(color, { dynamicVars: dynamicVars.value, staticVars: staticVars.value })
|
||||
if (computedColor) {
|
||||
return rgb2hex(computedColor)
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(e)
|
||||
}
|
||||
return null
|
||||
}
|
||||
provide('computeColor', exports.computeColor)
|
||||
|
||||
exports.contrast = computed(() => {
|
||||
return getContrast(
|
||||
exports.computeColor(previewColors.value.background),
|
||||
exports.computeColor(previewColors.value.text)
|
||||
)
|
||||
})
|
||||
|
||||
// ## Export and Import
|
||||
const styleExporter = newExporter({
|
||||
filename: () => exports.name.value ?? 'pleroma_theme',
|
||||
mime: 'text/plain',
|
||||
extension: 'iss',
|
||||
getExportedObject: () => exportStyleData.value
|
||||
})
|
||||
|
||||
const onImport = parsed => {
|
||||
const editorComponents = parsed.filter(x => x.component.startsWith('@'))
|
||||
const rootComponent = parsed.find(x => x.component === 'Root')
|
||||
const rules = parsed.filter(x => !x.component.startsWith('@') && x.component !== 'Root')
|
||||
const metaIn = editorComponents.find(x => x.component === '@meta').directives
|
||||
const palettesIn = editorComponents.filter(x => x.component === '@palette')
|
||||
|
||||
exports.name.value = metaIn.name
|
||||
exports.license.value = metaIn.license
|
||||
exports.author.value = metaIn.author
|
||||
exports.website.value = metaIn.website
|
||||
|
||||
const newVirtualDirectives = Object
|
||||
.entries(rootComponent.directives)
|
||||
.map(([name, value]) => {
|
||||
const [valType, valVal] = value.split('|').map(x => x.trim())
|
||||
return { name: name.substring(2), valType, value: valVal }
|
||||
})
|
||||
virtualDirectives.value = newVirtualDirectives
|
||||
|
||||
onPalettesUpdate(palettesIn.map(x => ({ name: x.variant, ...x.directives })))
|
||||
|
||||
allEditedRules.value = rulesToEditorFriendly(rules)
|
||||
|
||||
exports.updateOverallPreview()
|
||||
}
|
||||
|
||||
const styleImporter = newImporter({
|
||||
accept: '.iss',
|
||||
parser (string) { return deserialize(string) },
|
||||
onImportFailure (result) {
|
||||
console.error('Failure importing style:', result)
|
||||
this.$store.dispatch('pushGlobalNotice', { messageKey: 'settings.invalid_theme_imported', level: 'error' })
|
||||
},
|
||||
onImport
|
||||
})
|
||||
|
||||
// Raw format
|
||||
const exportRules = computed(() => [
|
||||
metaRule.value,
|
||||
...palettesRule.value,
|
||||
virtualDirectivesRule.value,
|
||||
...editorFriendlyToOriginal.value
|
||||
])
|
||||
|
||||
// Text format
|
||||
const exportStyleData = computed(() => {
|
||||
return [
|
||||
metaOut.value,
|
||||
palettesOut.value,
|
||||
virtualDirectivesOut.value,
|
||||
serialize(editorFriendlyToOriginal.value)
|
||||
].join('\n\n')
|
||||
})
|
||||
|
||||
exports.clearStyle = () => {
|
||||
onImport(store.state.interface.styleDataUsed)
|
||||
}
|
||||
|
||||
exports.exportStyle = () => {
|
||||
styleExporter.exportData()
|
||||
}
|
||||
|
||||
exports.importStyle = () => {
|
||||
styleImporter.importData()
|
||||
}
|
||||
|
||||
exports.applyStyle = () => {
|
||||
store.dispatch('setStyleCustom', exportRules.value)
|
||||
}
|
||||
|
||||
const overallPreviewRules = ref([])
|
||||
exports.overallPreviewRules = overallPreviewRules
|
||||
|
||||
const overallPreviewCssRules = ref([])
|
||||
watchEffect(throttle(() => {
|
||||
try {
|
||||
overallPreviewCssRules.value = getScopedVersion(
|
||||
getCssRules(overallPreviewRules.value),
|
||||
'#edited-style-preview'
|
||||
).join('\n')
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}, 500))
|
||||
|
||||
exports.overallPreviewCssRules = overallPreviewCssRules
|
||||
|
||||
const updateOverallPreview = throttle(() => {
|
||||
try {
|
||||
overallPreviewRules.value = init({
|
||||
inputRuleset: [
|
||||
...exportRules.value,
|
||||
{
|
||||
component: 'Root',
|
||||
directives: Object.fromEntries(
|
||||
Object
|
||||
.entries(selectedPalette.value)
|
||||
.filter(([k, v]) => k && v && k !== 'name')
|
||||
.map(([k, v]) => [`--${k}`, `color | ${v}`])
|
||||
)
|
||||
}
|
||||
],
|
||||
ultimateBackgroundColor: '#000000',
|
||||
debug: true
|
||||
}).eager
|
||||
} catch (e) {
|
||||
console.error('Could not compile preview theme', e)
|
||||
return null
|
||||
}
|
||||
}, 5000)
|
||||
//
|
||||
// Apart from "hover" we can't really show how component looks like in
|
||||
// certain states, so we have to fake them.
|
||||
const simulatePseudoSelectors = (css, prefix) => css
|
||||
.replace(prefix, '.component-preview .preview-block')
|
||||
.replace(':active', '.preview-active')
|
||||
.replace(':hover', '.preview-hover')
|
||||
.replace(':active', '.preview-active')
|
||||
.replace(':focus', '.preview-focus')
|
||||
.replace(':focus-within', '.preview-focus-within')
|
||||
.replace(':disabled', '.preview-disabled')
|
||||
|
||||
const previewRules = computed(() => {
|
||||
const filtered = overallPreviewRules.value.filter(r => {
|
||||
const componentMatch = r.component === selectedComponentName.value
|
||||
const parentComponentMatch = r.parent?.component === selectedComponentName.value
|
||||
if (!componentMatch && !parentComponentMatch) return false
|
||||
const rule = parentComponentMatch ? r.parent : r
|
||||
if (rule.component !== selectedComponentName.value) return false
|
||||
if (rule.variant !== selectedVariant.value) return false
|
||||
const ruleState = new Set(rule.state.filter(x => x !== 'normal'))
|
||||
const differenceA = [...ruleState].filter(x => !selectedState.has(x))
|
||||
const differenceB = [...selectedState].filter(x => !ruleState.has(x))
|
||||
return (differenceA.length + differenceB.length) === 0
|
||||
})
|
||||
const sorted = [...filtered]
|
||||
.filter(x => x.component === selectedComponentName.value)
|
||||
.sort((a, b) => {
|
||||
const aSelectorLength = a.selector.split(/ /g).length
|
||||
const bSelectorLength = b.selector.split(/ /g).length
|
||||
return aSelectorLength - bSelectorLength
|
||||
})
|
||||
|
||||
const prefix = sorted[0].selector
|
||||
|
||||
return filtered.filter(x => x.selector.startsWith(prefix))
|
||||
})
|
||||
|
||||
exports.previewClass = computed(() => {
|
||||
const selectors = []
|
||||
if (!!selectedComponent.value.variants?.normal || selectedVariant.value !== 'normal') {
|
||||
selectors.push(selectedComponent.value.variants[selectedVariant.value])
|
||||
}
|
||||
if (selectedState.size > 0) {
|
||||
selectedState.forEach(state => {
|
||||
const original = selectedComponent.value.states[state]
|
||||
selectors.push(simulatePseudoSelectors(original))
|
||||
})
|
||||
}
|
||||
return selectors.map(x => x.substring(1)).join('')
|
||||
})
|
||||
|
||||
exports.previewCss = computed(() => {
|
||||
try {
|
||||
const prefix = previewRules.value[0].selector
|
||||
const scoped = getCssRules(previewRules.value).map(x => simulatePseudoSelectors(x, prefix))
|
||||
return scoped.join('\n')
|
||||
} catch (e) {
|
||||
console.error('Invalid ruleset', e)
|
||||
return null
|
||||
}
|
||||
})
|
||||
|
||||
const dynamicVars = computed(() => {
|
||||
return previewRules.value[0].dynamicVars
|
||||
})
|
||||
|
||||
const staticVars = computed(() => {
|
||||
const rootComponent = overallPreviewRules.value.find(r => {
|
||||
return r.component === 'Root'
|
||||
})
|
||||
const rootDirectivesEntries = Object.entries(rootComponent.directives)
|
||||
const directives = {}
|
||||
rootDirectivesEntries
|
||||
.filter(([k, v]) => k.startsWith('--') && v.startsWith('color | '))
|
||||
.map(([k, v]) => [k.substring(2), v.substring('color | '.length)])
|
||||
.forEach(([k, v]) => {
|
||||
directives[k] = findColor(v, { dynamicVars: {}, staticVars: directives })
|
||||
})
|
||||
return directives
|
||||
})
|
||||
provide('staticVars', staticVars)
|
||||
exports.staticVars = staticVars
|
||||
|
||||
const previewColors = computed(() => {
|
||||
const stacked = dynamicVars.value.stacked
|
||||
const background = typeof stacked === 'string' ? stacked : rgb2hex(stacked)
|
||||
return {
|
||||
text: previewRules.value.find(r => r.component === 'Text')?.virtualDirectives['--text'],
|
||||
link: previewRules.value.find(r => r.component === 'Link')?.virtualDirectives['--link'],
|
||||
border: previewRules.value.find(r => r.component === 'Border')?.virtualDirectives['--border'],
|
||||
icon: previewRules.value.find(r => r.component === 'Icon')?.virtualDirectives['--icon'],
|
||||
background
|
||||
}
|
||||
})
|
||||
exports.previewColors = previewColors
|
||||
exports.updateOverallPreview = updateOverallPreview
|
||||
|
||||
updateOverallPreview()
|
||||
|
||||
watch(
|
||||
[
|
||||
allEditedRules.value,
|
||||
palettes,
|
||||
selectedPalette,
|
||||
selectedState,
|
||||
selectedVariant
|
||||
],
|
||||
updateOverallPreview
|
||||
)
|
||||
|
||||
return exports
|
||||
}
|
||||
}
|
||||
264
src/components/settings_modal/tabs/style_tab/style_tab.scss
Normal file
264
src/components/settings_modal/tabs/style_tab/style_tab.scss
Normal file
|
|
@ -0,0 +1,264 @@
|
|||
.StyleTab {
|
||||
.style-control {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: baseline;
|
||||
margin-bottom: 0.5em;
|
||||
|
||||
.label {
|
||||
margin-right: 0.5em;
|
||||
flex: 1 1 0;
|
||||
line-height: 2;
|
||||
min-height: 2em;
|
||||
}
|
||||
|
||||
&.suboption {
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
.color-input {
|
||||
flex: 0 0 0;
|
||||
}
|
||||
|
||||
input,
|
||||
select {
|
||||
min-width: 3em;
|
||||
margin: 0;
|
||||
flex: 0;
|
||||
|
||||
&[type="number"] {
|
||||
min-width: 9em;
|
||||
|
||||
&.-small {
|
||||
min-width: 5em;
|
||||
}
|
||||
}
|
||||
|
||||
&[type="range"] {
|
||||
flex: 1;
|
||||
min-width: 9em;
|
||||
align-self: center;
|
||||
margin: 0 0.25em;
|
||||
}
|
||||
|
||||
&[type="checkbox"] + i {
|
||||
height: 1.1em;
|
||||
align-self: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.meta-preview {
|
||||
display: grid;
|
||||
grid-template:
|
||||
"meta meta preview preview"
|
||||
"meta meta preview preview"
|
||||
"meta meta preview preview"
|
||||
"meta meta preview preview";
|
||||
grid-gap: 0.5em;
|
||||
grid-template-columns: min-content min-content 6fr max-content;
|
||||
|
||||
ul.setting-list {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: grid;
|
||||
grid-template-rows: subgrid;
|
||||
grid-area: meta;
|
||||
|
||||
> li {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.meta-field {
|
||||
margin: 0;
|
||||
|
||||
.setting-label {
|
||||
display: inline-block;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#edited-style-preview {
|
||||
grid-area: preview;
|
||||
}
|
||||
}
|
||||
|
||||
.setting-item {
|
||||
padding-bottom: 0;
|
||||
|
||||
.btn {
|
||||
padding: 0 0.5em;
|
||||
}
|
||||
|
||||
&:not(:first-child) {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
}
|
||||
|
||||
.list-editor {
|
||||
display: grid;
|
||||
grid-template-areas:
|
||||
"label editor"
|
||||
"selector editor"
|
||||
"movement editor";
|
||||
grid-template-columns: 10em 1fr;
|
||||
grid-template-rows: auto 1fr auto;
|
||||
grid-gap: 0.5em;
|
||||
|
||||
.list-edit-area {
|
||||
grid-area: editor;
|
||||
}
|
||||
|
||||
.list-select {
|
||||
grid-area: selector;
|
||||
margin: 0;
|
||||
|
||||
&-label {
|
||||
font-weight: bold;
|
||||
grid-area: label;
|
||||
margin: 0;
|
||||
align-self: baseline;
|
||||
}
|
||||
|
||||
&-movement {
|
||||
grid-area: movement;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.palette-editor {
|
||||
width: min-content;
|
||||
|
||||
.list-edit-area {
|
||||
display: grid;
|
||||
align-self: baseline;
|
||||
grid-template-rows: subgrid;
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.palette-editor-single {
|
||||
grid-row: 2 / span 2;
|
||||
}
|
||||
}
|
||||
|
||||
.variables-editor {
|
||||
.variable-selector {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto 10em;
|
||||
grid-template-rows: subgrid;
|
||||
align-items: baseline;
|
||||
grid-gap: 0 0.5em;
|
||||
}
|
||||
|
||||
.list-edit-area {
|
||||
display: grid;
|
||||
grid-template-rows: subgrid;
|
||||
}
|
||||
|
||||
.shadow-control {
|
||||
grid-row: 2 / span 2;
|
||||
}
|
||||
}
|
||||
|
||||
.component-editor {
|
||||
display: grid;
|
||||
grid-template-columns: 6fr 3fr 4fr;
|
||||
grid-template-rows: auto auto 1fr;
|
||||
grid-gap: 0.5em;
|
||||
grid-template-areas:
|
||||
"component component variant"
|
||||
"state state state"
|
||||
"preview settings settings";
|
||||
|
||||
.component-selector {
|
||||
grid-area: component;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.component-selector,
|
||||
.state-selector,
|
||||
.variant-selector {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr minmax(1fr, 10em);
|
||||
grid-template-rows: auto;
|
||||
grid-auto-flow: column;
|
||||
grid-gap: 0.5em;
|
||||
align-items: baseline;
|
||||
|
||||
> label:not(.Select) {
|
||||
font-weight: bold;
|
||||
justify-self: right;
|
||||
}
|
||||
}
|
||||
|
||||
.state-selector {
|
||||
grid-area: state;
|
||||
grid-template-columns: minmax(min-content, 7em) 1fr;
|
||||
}
|
||||
|
||||
.variant-selector {
|
||||
grid-area: variant;
|
||||
}
|
||||
|
||||
.state-selector-list {
|
||||
display: grid;
|
||||
list-style: none;
|
||||
grid-auto-flow: dense;
|
||||
grid-template-columns: repeat(5, minmax(min-content, 1fr));
|
||||
grid-auto-rows: 1fr;
|
||||
grid-gap: 0.5em;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.preview-container {
|
||||
--border: none;
|
||||
--shadow: none;
|
||||
--roundness: none;
|
||||
|
||||
grid-area: preview;
|
||||
}
|
||||
|
||||
.component-settings {
|
||||
grid-area: settings;
|
||||
}
|
||||
|
||||
.editor-tab {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 2em;
|
||||
grid-column-gap: 0.5em;
|
||||
align-items: center;
|
||||
grid-auto-rows: min-content;
|
||||
grid-auto-flow: dense;
|
||||
border-left: 1px solid var(--border);
|
||||
border-right: 1px solid var(--border);
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
.shadow-tab {
|
||||
grid-template-columns: 1fr;
|
||||
justify-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.extra-content {
|
||||
.style-actions-container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
|
||||
.style-actions {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(7em, 1fr));
|
||||
grid-gap: 0.25em;
|
||||
}
|
||||
}
|
||||
}
|
||||
402
src/components/settings_modal/tabs/style_tab/style_tab.vue
Normal file
402
src/components/settings_modal/tabs/style_tab/style_tab.vue
Normal file
|
|
@ -0,0 +1,402 @@
|
|||
<script src="./style_tab.js">
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="StyleTab">
|
||||
<div class="setting-item heading">
|
||||
<h2> {{ $t('settings.style.themes3.editor.title') }} </h2>
|
||||
<div class="meta-preview">
|
||||
<!-- eslint-disable vue/no-v-text-v-html-on-component -->
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<component
|
||||
:is="'style'"
|
||||
v-html="overallPreviewCssRules"
|
||||
/>
|
||||
<!-- eslint-enable vue/no-v-html -->
|
||||
<!-- eslint-enable vue/no-v-text-v-html-on-component -->
|
||||
<Preview id="edited-style-preview" />
|
||||
<teleport
|
||||
v-if="isActive"
|
||||
to="#unscrolled-content"
|
||||
>
|
||||
<div class="style-actions-container">
|
||||
<div class="style-actions">
|
||||
<button
|
||||
class="btn button-default button-new"
|
||||
@click="clearStyle"
|
||||
>
|
||||
<FAIcon icon="arrows-rotate" />
|
||||
{{ $t('settings.style.themes3.editor.reset_style') }}
|
||||
</button>
|
||||
<button
|
||||
class="btn button-default button-load"
|
||||
@click="importStyle"
|
||||
>
|
||||
<FAIcon icon="folder-open" />
|
||||
{{ $t('settings.style.themes3.editor.load_style') }}
|
||||
</button>
|
||||
<button
|
||||
class="btn button-default button-save"
|
||||
@click="exportStyle"
|
||||
>
|
||||
<FAIcon icon="floppy-disk" />
|
||||
{{ $t('settings.style.themes3.editor.save_style') }}
|
||||
</button>
|
||||
<button
|
||||
class="btn button-default button-apply"
|
||||
@click="applyStyle"
|
||||
>
|
||||
<FAIcon icon="check" />
|
||||
{{ $t('settings.style.themes3.editor.apply_preview') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</teleport>
|
||||
<ul class="setting-list style-metadata">
|
||||
<li>
|
||||
<StringSetting
|
||||
v-model="name"
|
||||
class="meta-field"
|
||||
>
|
||||
{{ $t('settings.style.themes3.editor.style_name') }}
|
||||
</StringSetting>
|
||||
</li>
|
||||
<li>
|
||||
<StringSetting
|
||||
v-model="author"
|
||||
class="meta-field"
|
||||
>
|
||||
{{ $t('settings.style.themes3.editor.style_author') }}
|
||||
</StringSetting>
|
||||
</li>
|
||||
<li>
|
||||
<StringSetting
|
||||
v-model="license"
|
||||
class="meta-field"
|
||||
>
|
||||
{{ $t('settings.style.themes3.editor.style_license') }}
|
||||
</StringSetting>
|
||||
</li>
|
||||
<li>
|
||||
<StringSetting
|
||||
v-model="website"
|
||||
class="meta-field"
|
||||
>
|
||||
{{ $t('settings.style.themes3.editor.style_website') }}
|
||||
</StringSetting>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<tab-switcher>
|
||||
<div
|
||||
key="component"
|
||||
class="setting-item component-editor"
|
||||
:label="$t('settings.style.themes3.editor.component_tab')"
|
||||
>
|
||||
<div class="component-selector">
|
||||
<label for="component-selector">
|
||||
{{ $t('settings.style.themes3.editor.component_selector') }}
|
||||
{{ ' ' }}
|
||||
</label>
|
||||
<Select
|
||||
id="component-selector"
|
||||
v-model="selectedComponentKey"
|
||||
>
|
||||
<option
|
||||
v-for="key in componentKeys"
|
||||
:key="'component-' + key"
|
||||
:value="key"
|
||||
>
|
||||
{{ componentsMap.get(key).name }}
|
||||
</option>
|
||||
</Select>
|
||||
</div>
|
||||
<div
|
||||
v-if="selectedComponentVariants.length > 1"
|
||||
class="variant-selector"
|
||||
>
|
||||
<label for="variant-selector">
|
||||
{{ $t('settings.style.themes3.editor.variant_selector') }}
|
||||
</label>
|
||||
<Select
|
||||
v-model="selectedVariant"
|
||||
>
|
||||
<option
|
||||
v-for="variant in selectedComponentVariants"
|
||||
:key="'component-variant-' + variant"
|
||||
:value="variant"
|
||||
>
|
||||
{{ variant }}
|
||||
</option>
|
||||
</Select>
|
||||
</div>
|
||||
<div
|
||||
v-if="selectedComponentStates.length > 0"
|
||||
class="state-selector"
|
||||
>
|
||||
<label>
|
||||
{{ $t('settings.style.themes3.editor.states_selector') }}
|
||||
</label>
|
||||
<ul
|
||||
class="state-selector-list"
|
||||
>
|
||||
<li
|
||||
v-for="state in selectedComponentStates"
|
||||
:key="'component-state-' + state"
|
||||
>
|
||||
<Checkbox
|
||||
:value="selectedState.has(state)"
|
||||
@update:modelValue="(v) => updateSelectedStates(state, v)"
|
||||
>
|
||||
{{ state }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="preview-container">
|
||||
<!-- eslint-disable vue/no-v-html vue/no-v-text-v-html-on-component -->
|
||||
<component
|
||||
:is="'style'"
|
||||
v-html="previewCss"
|
||||
/>
|
||||
<!-- eslint-enable vue/no-v-html vue/no-v-text-v-html-on-component -->
|
||||
<ComponentPreview
|
||||
class="component-preview"
|
||||
:show-text="componentHas('Text')"
|
||||
:shadow-control="isShadowTabOpen"
|
||||
:preview-class="previewClass"
|
||||
:preview-style="editorHintStyle"
|
||||
:preview-css="previewCss"
|
||||
:disabled="!editedSubShadow && typeof editedShadow !== 'string'"
|
||||
:shadow="editedSubShadow"
|
||||
:no-color-control="true"
|
||||
@update:shadow="({ axis, value }) => updateSubShadow(axis, value)"
|
||||
/>
|
||||
</div>
|
||||
<tab-switcher
|
||||
ref="tabSwitcher"
|
||||
class="component-settings"
|
||||
:on-switch="onTabSwitch"
|
||||
>
|
||||
<div
|
||||
key="main"
|
||||
class="editor-tab"
|
||||
:label="$t('settings.style.themes3.editor.main_tab')"
|
||||
>
|
||||
<ColorInput
|
||||
v-model="editedBackgroundColor"
|
||||
name="component-background-color"
|
||||
:fallback="computeColor(editedBackgroundColor) ?? previewColors.background"
|
||||
:disabled="!isBackgroundColorPresent"
|
||||
:label="$t('settings.style.themes3.editor.background')"
|
||||
:hide-optional-checkbox="true"
|
||||
/>
|
||||
<Tooltip :text="$t('settings.style.themes3.editor.include_in_rule')">
|
||||
<Checkbox v-model="isBackgroundColorPresent" />
|
||||
</Tooltip>
|
||||
<ColorInput
|
||||
v-if="componentHas('Text')"
|
||||
v-model="editedTextColor"
|
||||
name="component-text-color"
|
||||
:fallback="computeColor(editedTextColor) ?? previewColors.text"
|
||||
:label="$t('settings.style.themes3.editor.text_color')"
|
||||
:disabled="!isTextColorPresent"
|
||||
:hide-optional-checkbox="true"
|
||||
/>
|
||||
<Tooltip
|
||||
v-if="componentHas('Text')"
|
||||
:text="$t('settings.style.themes3.editor.include_in_rule')"
|
||||
>
|
||||
<Checkbox v-model="isTextColorPresent" />
|
||||
</Tooltip>
|
||||
<div
|
||||
v-if="componentHas('Text')"
|
||||
class="style-control suboption"
|
||||
>
|
||||
<label
|
||||
for="textAuto"
|
||||
class="label"
|
||||
:class="{ faint: !isTextAutoPresent }"
|
||||
>
|
||||
{{ $t('settings.style.themes3.editor.text_auto.label') }}
|
||||
</label>
|
||||
<Select
|
||||
id="textAuto"
|
||||
v-model="editedTextAuto"
|
||||
:disabled="!isTextAutoPresent"
|
||||
>
|
||||
<option value="no-preserve">
|
||||
{{ $t('settings.style.themes3.editor.text_auto.no-preserve') }}
|
||||
</option>
|
||||
<option value="no-auto">
|
||||
{{ $t('settings.style.themes3.editor.text_auto.no-auto') }}
|
||||
</option>
|
||||
<option value="preserve">
|
||||
{{ $t('settings.style.themes3.editor.text_auto.preserve') }}
|
||||
</option>
|
||||
</Select>
|
||||
</div>
|
||||
<Tooltip
|
||||
v-if="componentHas('Text')"
|
||||
:text="$t('settings.style.themes3.editor.include_in_rule')"
|
||||
>
|
||||
<Checkbox v-model="isTextAutoPresent" />
|
||||
</Tooltip>
|
||||
<div
|
||||
v-if="componentHas('Text')"
|
||||
class="style-control suboption"
|
||||
>
|
||||
<label class="label">
|
||||
{{ $t('settings.style.themes3.editor.contrast') }}
|
||||
</label>
|
||||
<ContrastRatio
|
||||
:show-ratio="true"
|
||||
:contrast="contrast"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="componentHas('Text')" />
|
||||
<ColorInput
|
||||
v-if="componentHas('Link')"
|
||||
v-model="editedLinkColor"
|
||||
name="component-link-color"
|
||||
:fallback="computeColor(editedLinkColor) ?? previewColors.link"
|
||||
:label="$t('settings.style.themes3.editor.link_color')"
|
||||
:disabled="!isLinkColorPresent"
|
||||
:hide-optional-checkbox="true"
|
||||
/>
|
||||
<Tooltip
|
||||
v-if="componentHas('Link')"
|
||||
:text="$t('settings.style.themes3.editor.include_in_rule')"
|
||||
>
|
||||
<Checkbox v-model="isLinkColorPresent" />
|
||||
</Tooltip>
|
||||
<ColorInput
|
||||
v-if="componentHas('Icon')"
|
||||
v-model="editedIconColor"
|
||||
name="component-icon-color"
|
||||
:fallback="computeColor(editedIconColor) ?? previewColors.icon"
|
||||
:label="$t('settings.style.themes3.editor.icon_color')"
|
||||
:disabled="!isIconColorPresent"
|
||||
:hide-optional-checkbox="true"
|
||||
/>
|
||||
<Tooltip
|
||||
v-if="componentHas('Icon')"
|
||||
:text="$t('settings.style.themes3.editor.include_in_rule')"
|
||||
>
|
||||
<Checkbox v-model="isIconColorPresent" />
|
||||
</Tooltip>
|
||||
<ColorInput
|
||||
v-if="componentHas('Border')"
|
||||
v-model="editedBorderColor"
|
||||
name="component-border-color"
|
||||
:fallback="computeColor(editedBorderColor) ?? previewColors.border"
|
||||
:label="$t('settings.style.themes3.editor.border_color')"
|
||||
:disabled="!isBorderColorPresent"
|
||||
:hide-optional-checkbox="true"
|
||||
/>
|
||||
<Tooltip
|
||||
v-if="componentHas('Border')"
|
||||
:text="$t('settings.style.themes3.editor.include_in_rule')"
|
||||
>
|
||||
<Checkbox v-model="isBorderColorPresent" />
|
||||
</Tooltip>
|
||||
<OpacityInput
|
||||
v-model="editedOpacity"
|
||||
name="component-opacity"
|
||||
:disabled="!isOpacityPresent"
|
||||
:label="$t('settings.style.themes3.editor.opacity')"
|
||||
/>
|
||||
<Tooltip :text="$t('settings.style.themes3.editor.include_in_rule')">
|
||||
<Checkbox v-model="isOpacityPresent" />
|
||||
</Tooltip>
|
||||
<RoundnessInput
|
||||
v-model="editedRoundness"
|
||||
name="component-roundness"
|
||||
:disabled="!isRoundnessPresent"
|
||||
:label="$t('settings.style.themes3.editor.roundness')"
|
||||
/>
|
||||
<Tooltip :text="$t('settings.style.themes3.editor.include_in_rule')">
|
||||
<Checkbox v-model="isRoundnessPresent" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div
|
||||
key="shadow"
|
||||
class="editor-tab shadow-tab"
|
||||
:label="$t('settings.style.themes3.editor.shadows_tab')"
|
||||
>
|
||||
<Checkbox
|
||||
v-model="isShadowPresent"
|
||||
class="style-control"
|
||||
>
|
||||
{{ $t('settings.style.themes3.editor.include_in_rule') }}
|
||||
</checkbox>
|
||||
<ShadowControl
|
||||
v-model="editedShadow"
|
||||
:disabled="!isShadowPresent"
|
||||
:no-preview="true"
|
||||
:compact="true"
|
||||
:static-vars="staticVars"
|
||||
@subShadowSelected="onSubShadow"
|
||||
/>
|
||||
</div>
|
||||
</tab-switcher>
|
||||
</div>
|
||||
<div
|
||||
key="palette"
|
||||
:label="$t('settings.style.themes3.editor.palette_tab')"
|
||||
class="setting-item list-editor palette-editor"
|
||||
>
|
||||
<label
|
||||
class="list-select-label"
|
||||
for="palette-selector"
|
||||
>
|
||||
{{ $t('settings.style.themes3.palette.label') }}
|
||||
{{ ' ' }}
|
||||
</label>
|
||||
<Select
|
||||
id="palette-selector"
|
||||
v-model="selectedPaletteId"
|
||||
class="list-select"
|
||||
size="4"
|
||||
>
|
||||
<option
|
||||
v-for="(p, index) in palettes"
|
||||
:key="p.name"
|
||||
:value="index"
|
||||
>
|
||||
{{ p.name }}
|
||||
</option>
|
||||
</Select>
|
||||
<SelectMotion
|
||||
class="list-select-movement"
|
||||
:model-value="palettes"
|
||||
:selected-id="selectedPaletteId"
|
||||
:get-add-value="getNewPalette"
|
||||
@update:modelValue="onPalettesUpdate"
|
||||
@update:selectedId="e => selectedPaletteId = e"
|
||||
/>
|
||||
<div class="list-edit-area">
|
||||
<StringSetting
|
||||
v-model="selectedPalette.name"
|
||||
class="palette-name-input"
|
||||
>
|
||||
{{ $t('settings.style.themes3.palette.name_label') }}
|
||||
</StringSetting>
|
||||
<PaletteEditor
|
||||
v-model="selectedPalette"
|
||||
class="palette-editor-single"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<VirtualDirectivesTab
|
||||
key="variables"
|
||||
:label="$t('settings.style.themes3.editor.variables_tab')"
|
||||
:model-value="virtualDirectives"
|
||||
@update:modelValue="updateVirtualDirectives"
|
||||
/>
|
||||
</tab-switcher>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style src="./style_tab.scss" lang="scss"></style>
|
||||
|
|
@ -0,0 +1,132 @@
|
|||
import { ref, computed, watch, inject } from 'vue'
|
||||
|
||||
import Select from 'src/components/select/select.vue'
|
||||
import SelectMotion from 'src/components/select/select_motion.vue'
|
||||
import ShadowControl from 'src/components/shadow_control/shadow_control.vue'
|
||||
import ColorInput from 'src/components/color_input/color_input.vue'
|
||||
|
||||
import { serializeShadow } from 'src/services/theme_data/iss_serializer.js'
|
||||
|
||||
// helper for debugging
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const toValue = (x) => JSON.parse(JSON.stringify(x === undefined ? 'null' : x))
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Select,
|
||||
SelectMotion,
|
||||
ShadowControl,
|
||||
ColorInput
|
||||
},
|
||||
props: ['modelValue'],
|
||||
emits: ['update:modelValue'],
|
||||
setup (props, context) {
|
||||
const exports = {}
|
||||
const emit = context.emit
|
||||
|
||||
exports.emit = emit
|
||||
exports.computeColor = inject('computeColor')
|
||||
exports.staticVars = inject('staticVars')
|
||||
|
||||
const selectedVirtualDirectiveId = ref(0)
|
||||
exports.selectedVirtualDirectiveId = selectedVirtualDirectiveId
|
||||
|
||||
const selectedVirtualDirective = computed({
|
||||
get () {
|
||||
return props.modelValue[selectedVirtualDirectiveId.value]
|
||||
},
|
||||
set (value) {
|
||||
const newVD = [...props.modelValue]
|
||||
newVD[selectedVirtualDirectiveId.value] = value
|
||||
|
||||
emit('update:modelValue', newVD)
|
||||
}
|
||||
})
|
||||
exports.selectedVirtualDirective = selectedVirtualDirective
|
||||
|
||||
exports.selectedVirtualDirectiveValType = computed({
|
||||
get () {
|
||||
return props.modelValue[selectedVirtualDirectiveId.value].valType
|
||||
},
|
||||
set (value) {
|
||||
const newValType = value
|
||||
let newValue
|
||||
switch (value) {
|
||||
case 'shadow':
|
||||
newValue = '0 0 0 #000000 / 1'
|
||||
break
|
||||
case 'color':
|
||||
newValue = '#000000'
|
||||
break
|
||||
default:
|
||||
newValue = 'none'
|
||||
}
|
||||
const newName = props.modelValue[selectedVirtualDirectiveId.value].name
|
||||
props.modelValue[selectedVirtualDirectiveId.value] = {
|
||||
name: newName,
|
||||
value: newValue,
|
||||
valType: newValType
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const draftVirtualDirectiveValid = ref(true)
|
||||
const draftVirtualDirective = ref({})
|
||||
exports.draftVirtualDirective = draftVirtualDirective
|
||||
const normalizeShadows = inject('normalizeShadows')
|
||||
|
||||
watch(
|
||||
selectedVirtualDirective,
|
||||
(directive) => {
|
||||
switch (directive.valType) {
|
||||
case 'shadow': {
|
||||
if (Array.isArray(directive.value)) {
|
||||
draftVirtualDirective.value = normalizeShadows(directive.value)
|
||||
} else {
|
||||
const splitShadow = directive.value.split(/,/g).map(x => x.trim())
|
||||
draftVirtualDirective.value = normalizeShadows(splitShadow)
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'color':
|
||||
draftVirtualDirective.value = directive.value
|
||||
break
|
||||
default:
|
||||
draftVirtualDirective.value = directive.value
|
||||
break
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
watch(
|
||||
draftVirtualDirective,
|
||||
(directive) => {
|
||||
try {
|
||||
switch (selectedVirtualDirective.value.valType) {
|
||||
case 'shadow': {
|
||||
props.modelValue[selectedVirtualDirectiveId.value].value =
|
||||
directive.map(x => serializeShadow(x)).join(', ')
|
||||
break
|
||||
}
|
||||
default:
|
||||
props.modelValue[selectedVirtualDirectiveId.value].value = directive
|
||||
}
|
||||
draftVirtualDirectiveValid.value = true
|
||||
} catch (e) {
|
||||
console.error('Invalid virtual directive value', e)
|
||||
draftVirtualDirectiveValid.value = false
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
exports.getNewVirtualDirective = () => ({
|
||||
name: 'newDirective',
|
||||
valType: 'generic',
|
||||
value: 'foobar'
|
||||
})
|
||||
|
||||
return exports
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
<script src="./virtual_directives_tab.js"></script>
|
||||
|
||||
<template>
|
||||
<div class="setting-item list-editor variables-editor">
|
||||
<label
|
||||
class="list-select-label"
|
||||
for="variables-selector"
|
||||
>
|
||||
{{ $t('settings.style.themes3.editor.variables.label') }}
|
||||
{{ ' ' }}
|
||||
</label>
|
||||
<Select
|
||||
id="variables-selector"
|
||||
v-model="selectedVirtualDirectiveId"
|
||||
class="list-select"
|
||||
size="20"
|
||||
>
|
||||
<option
|
||||
v-for="(p, index) in modelValue"
|
||||
:key="p.name"
|
||||
:value="index"
|
||||
>
|
||||
{{ p.name }}
|
||||
</option>
|
||||
</Select>
|
||||
<SelectMotion
|
||||
class="list-select-movement"
|
||||
:model-value="modelValue"
|
||||
:selected-id="selectedVirtualDirectiveId"
|
||||
:get-add-value="getNewVirtualDirective"
|
||||
@update:modelValue="e => emit('update:modelValue', e)"
|
||||
@update:selectedId="e => selectedVirtualDirectiveId = e"
|
||||
/>
|
||||
<div class="list-edit-area">
|
||||
<div class="variable-selector">
|
||||
<label
|
||||
class="variable-name-label"
|
||||
for="variables-selector"
|
||||
>
|
||||
{{ $t('settings.style.themes3.editor.variables.name_label') }}
|
||||
{{ ' ' }}
|
||||
</label>
|
||||
<input
|
||||
v-model="selectedVirtualDirective.name"
|
||||
class="input"
|
||||
>
|
||||
<label
|
||||
class="variable-type-label"
|
||||
for="variables-selector"
|
||||
>
|
||||
{{ $t('settings.style.themes3.editor.variables.type_label') }}
|
||||
{{ ' ' }}
|
||||
</label>
|
||||
<Select
|
||||
v-model="selectedVirtualDirectiveValType"
|
||||
>
|
||||
<option value="shadow">
|
||||
{{ $t('settings.style.themes3.editor.variables.type_shadow') }}
|
||||
</option>
|
||||
<option value="color">
|
||||
{{ $t('settings.style.themes3.editor.variables.type_color') }}
|
||||
</option>
|
||||
<option value="generic">
|
||||
{{ $t('settings.style.themes3.editor.variables.type_generic') }}
|
||||
</option>
|
||||
</Select>
|
||||
</div>
|
||||
<ShadowControl
|
||||
v-if="selectedVirtualDirectiveValType === 'shadow'"
|
||||
v-model="draftVirtualDirective"
|
||||
:static-vars="staticVars"
|
||||
:compact="true"
|
||||
/>
|
||||
<ColorInput
|
||||
v-if="selectedVirtualDirectiveValType === 'color'"
|
||||
v-model="draftVirtualDirective"
|
||||
name="virtual-directive-color"
|
||||
:fallback="computeColor(draftVirtualDirective)"
|
||||
:label="$t('settings.style.themes3.editor.variables.virtual_color')"
|
||||
:hide-optional-checkbox="true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -1,14 +1,14 @@
|
|||
<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">
|
||||
<h1 class="title">
|
||||
{{ $t('settings.style.preview.header') }}
|
||||
<span class="badge badge-notification">
|
||||
<span class="badge -notification">
|
||||
99
|
||||
</span>
|
||||
</div>
|
||||
</h1>
|
||||
<span class="faint">
|
||||
{{ $t('settings.style.preview.header_faint') }}
|
||||
</span>
|
||||
|
|
@ -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%;
|
||||
|
||||
.underlay-preview {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 10px;
|
||||
right: 10px;
|
||||
.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;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
|
@ -1,19 +1,9 @@
|
|||
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
|
||||
} from 'src/services/style_setter/style_setter.js'
|
||||
import {
|
||||
newImporter,
|
||||
newExporter
|
||||
|
|
@ -25,8 +15,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 +42,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'
|
||||
import { useInterfaceStore } from '../../../../stores/interface'
|
||||
|
||||
// List of color values used in v1
|
||||
|
|
@ -63,6 +68,7 @@ const colorConvert = (color) => {
|
|||
export default {
|
||||
data () {
|
||||
return {
|
||||
themeV3Preview: [],
|
||||
themeImporter: newImporter({
|
||||
validator: this.importValidator,
|
||||
onImport: this.onImport,
|
||||
|
|
@ -79,10 +85,7 @@ export default {
|
|||
tempImportFile: undefined,
|
||||
engineVersion: 0,
|
||||
|
||||
previewShadows: {},
|
||||
previewColors: {},
|
||||
previewRadii: {},
|
||||
previewFonts: {},
|
||||
previewTheme: {},
|
||||
|
||||
shadowsInvalid: true,
|
||||
colorsInvalid: true,
|
||||
|
|
@ -118,31 +121,24 @@ export default {
|
|||
}
|
||||
},
|
||||
created () {
|
||||
const self = this
|
||||
const currentIndex = this.$store.state.instance.themesIndex
|
||||
|
||||
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,
|
||||
[k]: v
|
||||
}
|
||||
} else {
|
||||
return acc
|
||||
}
|
||||
}, {}))
|
||||
.then((themesComplete) => {
|
||||
self.availableStyles = themesComplete
|
||||
})
|
||||
let promise
|
||||
if (currentIndex) {
|
||||
promise = Promise.resolve(currentIndex)
|
||||
} else {
|
||||
promise = this.$store.dispatch('fetchThemesIndex')
|
||||
}
|
||||
|
||||
promise.then(themesIndex => {
|
||||
Object
|
||||
.values(themesIndex)
|
||||
.forEach(themeFunc => {
|
||||
themeFunc().then(themeData => themeData && this.availableStyles.push(themeData))
|
||||
})
|
||||
})
|
||||
},
|
||||
mounted () {
|
||||
this.loadThemeFromLocalStorage()
|
||||
if (typeof this.shadowSelected === 'undefined') {
|
||||
this.shadowSelected = this.shadowsAvailable[0]
|
||||
}
|
||||
|
|
@ -233,13 +229,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 {
|
||||
|
|
@ -307,13 +296,8 @@ 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(';')
|
||||
themeDataUsed () {
|
||||
return this.$store.state.interface.themeDataUsed
|
||||
},
|
||||
shadowsAvailable () {
|
||||
return Object.keys(DEFAULT_SHADOWS).sort()
|
||||
|
|
@ -324,7 +308,18 @@ export default {
|
|||
},
|
||||
set (val) {
|
||||
if (val) {
|
||||
this.shadowsLocal[this.shadowSelected] = this.currentShadowFallback.map(_ => Object.assign({}, _))
|
||||
this.shadowsLocal[this.shadowSelected] = (this.currentShadowFallback || [])
|
||||
.map(s => ({
|
||||
name: null,
|
||||
x: 0,
|
||||
y: 0,
|
||||
blur: 0,
|
||||
spread: 0,
|
||||
inset: false,
|
||||
color: '#000000',
|
||||
alpha: 1,
|
||||
...s
|
||||
}))
|
||||
} else {
|
||||
delete this.shadowsLocal[this.shadowSelected]
|
||||
}
|
||||
|
|
@ -411,9 +406,6 @@ export default {
|
|||
forceUseSource = false
|
||||
) {
|
||||
this.dismissWarning()
|
||||
if (!source && !theme) {
|
||||
throw new Error('Can\'t load theme: empty')
|
||||
}
|
||||
const version = (origin === 'localStorage' && !theme.colors)
|
||||
? 'l1'
|
||||
: fileVersion
|
||||
|
|
@ -489,22 +481,11 @@ export default {
|
|||
this.dismissWarning()
|
||||
},
|
||||
loadThemeFromLocalStorage (confirmLoadSource = false, forceSnapshot = false) {
|
||||
const {
|
||||
customTheme: theme,
|
||||
customThemeSource: source
|
||||
} = this.$store.getters.mergedConfig
|
||||
if (!theme && !source) {
|
||||
// Anon user or never touched themes
|
||||
this.loadTheme(
|
||||
this.$store.state.instance.themeData,
|
||||
'defaults',
|
||||
confirmLoadSource
|
||||
)
|
||||
} else {
|
||||
const theme = this.themeDataUsed?.source
|
||||
if (theme) {
|
||||
this.loadTheme(
|
||||
{
|
||||
theme,
|
||||
source: forceSnapshot ? theme : source
|
||||
theme
|
||||
},
|
||||
'localStorage',
|
||||
confirmLoadSource
|
||||
|
|
@ -512,16 +493,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,
|
||||
|
|
@ -531,16 +511,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() },
|
||||
|
|
@ -609,7 +597,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 {
|
||||
|
|
@ -691,6 +679,8 @@ export default {
|
|||
} else {
|
||||
this.shadowsLocal = shadows
|
||||
}
|
||||
this.updatePreviewColors()
|
||||
this.updatePreviewShadows()
|
||||
this.shadowSelected = this.shadowsAvailable[0]
|
||||
}
|
||||
|
||||
|
|
@ -698,12 +688,28 @@ 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: {
|
||||
themeDataUsed () {
|
||||
this.loadThemeFromLocalStorage()
|
||||
},
|
||||
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
|
||||
|
|
@ -712,9 +718,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
|
||||
|
|
@ -726,7 +731,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
|
||||
|
|
@ -737,18 +742,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)
|
||||
}
|
||||
|
|
@ -756,7 +759,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
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
@import "src/variables";
|
||||
|
||||
.theme-tab {
|
||||
.deprecation-warning {
|
||||
padding: 0.5em;
|
||||
margin: 2em;
|
||||
}
|
||||
|
||||
padding-bottom: 2em;
|
||||
|
||||
.preset-switcher {
|
||||
|
|
@ -12,13 +15,19 @@
|
|||
margin-right: 0.25em;
|
||||
}
|
||||
|
||||
.btn-group .btn {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.style-control {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
margin-bottom: 5px;
|
||||
|
||||
.label {
|
||||
margin-right: 1em;
|
||||
flex: 1;
|
||||
line-height: 2;
|
||||
}
|
||||
|
||||
.opt {
|
||||
|
|
@ -36,20 +45,23 @@
|
|||
flex: 0;
|
||||
|
||||
&[type="number"] {
|
||||
min-width: 5em;
|
||||
min-width: 9em;
|
||||
|
||||
&.-small {
|
||||
min-width: 5em;
|
||||
}
|
||||
}
|
||||
|
||||
&[type="range"] {
|
||||
flex: 1;
|
||||
min-width: 3em;
|
||||
align-self: flex-start;
|
||||
min-width: 9em;
|
||||
align-self: center;
|
||||
margin: 0 0.5em;
|
||||
}
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
input,
|
||||
select {
|
||||
opacity: 0.5;
|
||||
&[type="checkbox"] + i {
|
||||
height: 1.1em;
|
||||
align-self: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -159,111 +171,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 +203,7 @@
|
|||
border: 0;
|
||||
box-shadow: none;
|
||||
background: transparent;
|
||||
color: var(--faint, $fallback--faint);
|
||||
color: var(--textFaint);
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
|
|
@ -316,10 +223,6 @@
|
|||
max-width: 50em;
|
||||
}
|
||||
|
||||
.theme-preview-content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.theme-warning {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
|
|
|
|||
|
|
@ -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,22 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<preview :style="previewRules" />
|
||||
<!-- eslint-disable vue/no-v-html vue/no-v-text-v-html-on-component -->
|
||||
<component
|
||||
:is="'style'"
|
||||
v-html="themeV3Preview"
|
||||
/>
|
||||
<!-- eslint-enable vue/no-v-html 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 +174,7 @@
|
|||
<OpacityInput
|
||||
v-model="bgOpacityLocal"
|
||||
name="bgOpacity"
|
||||
:fallback="previewTheme.opacity.bg"
|
||||
:fallback="previewTheme.opacity?.bg"
|
||||
/>
|
||||
<ColorInput
|
||||
v-model="textColorLocal"
|
||||
|
|
@ -167,16 +185,16 @@
|
|||
<ColorInput
|
||||
v-model="accentColorLocal"
|
||||
name="accentColor"
|
||||
:fallback="previewTheme.colors.link"
|
||||
:fallback="previewTheme.colors?.link"
|
||||
:label="$t('settings.accent')"
|
||||
:show-optional-tickbox="typeof linkColorLocal !== 'undefined'"
|
||||
:show-optional-checkbox="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'"
|
||||
:show-optional-checkbox="typeof accentColorLocal !== 'undefined'"
|
||||
/>
|
||||
<ContrastRatio :contrast="previewContrast.bgLink" />
|
||||
</div>
|
||||
|
|
@ -190,13 +208,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 +274,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 +290,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 +306,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 +322,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 +337,7 @@
|
|||
<OpacityInput
|
||||
v-model="alertOpacityLocal"
|
||||
name="alertOpacity"
|
||||
:fallback="previewTheme.opacity.alert"
|
||||
:fallback="previewTheme.opacity?.alert"
|
||||
/>
|
||||
</div>
|
||||
<div class="color-item">
|
||||
|
|
@ -328,13 +346,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 +364,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 +386,7 @@
|
|||
<ColorInput
|
||||
v-model="panelLinkColorLocal"
|
||||
name="panelLinkColor"
|
||||
:fallback="previewTheme.colors.panelLink"
|
||||
:fallback="previewTheme.colors?.panelLink"
|
||||
:label="$t('settings.links')"
|
||||
/>
|
||||
<ContrastRatio
|
||||
|
|
@ -381,20 +399,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 +422,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 +444,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 +478,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 +506,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 +561,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 +584,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 +599,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 +626,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 +641,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 +650,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 +665,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 +674,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 +697,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 +726,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 +749,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 +771,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 +844,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 +852,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 +860,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 +868,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 +876,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 +884,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 +892,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 +900,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 +908,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"
|
||||
/>
|
||||
|
|
@ -919,24 +937,14 @@
|
|||
</Select>
|
||||
</div>
|
||||
<div class="override">
|
||||
<label
|
||||
for="override"
|
||||
class="label"
|
||||
>
|
||||
{{ $t('settings.style.shadows.override') }}
|
||||
</label>
|
||||
{{ ' ' }}
|
||||
<input
|
||||
<Checkbox
|
||||
id="override"
|
||||
v-model="currentShadowOverriden"
|
||||
name="override"
|
||||
class="input-override"
|
||||
type="checkbox"
|
||||
>
|
||||
<label
|
||||
class="checkbox-label"
|
||||
for="override"
|
||||
/>
|
||||
{{ $t('settings.style.shadows.override') }}
|
||||
</Checkbox>
|
||||
</div>
|
||||
<button
|
||||
class="btn button-default"
|
||||
|
|
@ -947,38 +955,12 @@
|
|||
</div>
|
||||
<ShadowControl
|
||||
v-model="currentShadow"
|
||||
:ready="!!currentShadowFallback"
|
||||
:separate-inset="shadowSelected === 'avatar' || shadowSelected === 'avatarStatus'"
|
||||
:fallback="currentShadowFallback"
|
||||
:static-vars="previewTheme.colors"
|
||||
:compact="true"
|
||||
/>
|
||||
<div v-if="shadowSelected === 'avatar' || shadowSelected === 'avatarStatus'">
|
||||
<i18n-t
|
||||
scope="global"
|
||||
keypath="settings.style.shadows.filter_hint.always_drop_shadow"
|
||||
tag="p"
|
||||
>
|
||||
<code>filter: drop-shadow()</code>
|
||||
</i18n-t>
|
||||
<p>{{ $t('settings.style.shadows.filter_hint.avatar_inset') }}</p>
|
||||
<i18n-t
|
||||
scope="global"
|
||||
keypath="settings.style.shadows.filter_hint.drop_shadow_syntax"
|
||||
tag="p"
|
||||
>
|
||||
<code>drop-shadow</code>
|
||||
<code>spread-radius</code>
|
||||
<code>inset</code>
|
||||
</i18n-t>
|
||||
<i18n-t
|
||||
scope="global"
|
||||
keypath="settings.style.shadows.filter_hint.inset_classic"
|
||||
tag="p"
|
||||
>
|
||||
<code>box-shadow</code>
|
||||
</i18n-t>
|
||||
<p>{{ $t('settings.style.shadows.filter_hint.spread_zero') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
:label="$t('settings.style.fonts._tab_label')"
|
||||
class="fonts-container"
|
||||
|
|
@ -996,26 +978,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>
|
||||
|
|
|
|||
|
|
@ -1,22 +1,17 @@
|
|||
import { extractCommit } from 'src/services/version/version.service'
|
||||
|
||||
const pleromaFeCommitUrl = 'https://git.pleroma.social/pleroma/pleroma-fe/commit/'
|
||||
const pleromaBeCommitUrl = 'https://git.pleroma.social/pleroma/pleroma/commit/'
|
||||
|
||||
const VersionTab = {
|
||||
data () {
|
||||
const instance = this.$store.state.instance
|
||||
return {
|
||||
backendVersion: instance.backendVersion,
|
||||
backendRepository: instance.backendRepository,
|
||||
frontendVersion: instance.frontendVersion
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
frontendVersionLink () {
|
||||
return pleromaFeCommitUrl + this.frontendVersion
|
||||
},
|
||||
backendVersionLink () {
|
||||
return pleromaBeCommitUrl + extractCommit(this.backendVersion)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
<ul class="option-list">
|
||||
<li>
|
||||
<a
|
||||
:href="backendVersionLink"
|
||||
:href="backendRepository"
|
||||
target="_blank"
|
||||
>{{ backendVersion }}</a>
|
||||
</li>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue