Merge remote-tracking branch 'origin/develop' into admin-users
This commit is contained in:
commit
43936a8725
628 changed files with 72639 additions and 24537 deletions
|
|
@ -1,12 +1,12 @@
|
|||
import BasicUserCard from '../../basic_user_card/basic_user_card.vue'
|
||||
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||
import PageList from 'src/components/page_list/page_list.vue'
|
||||
import AdminStatusCard from 'src/components/settings_modal/admin_tabs/admin_status_card.vue'
|
||||
import Modal from 'src/components/modal/modal.vue'
|
||||
import Popover from 'src/components/popover/popover.vue'
|
||||
import GenericConfirm from 'src/components/confirm_modal/generic_confirm.vue'
|
||||
import Select from 'src/components/select/select.vue'
|
||||
import TextConfirm from 'src/components/confirm_modal/text_confirm.vue'
|
||||
import Modal from 'src/components/modal/modal.vue'
|
||||
import PageList from 'src/components/page_list/page_list.vue'
|
||||
import Popover from 'src/components/popover/popover.vue'
|
||||
import Select from 'src/components/select/select.vue'
|
||||
import AdminStatusCard from 'src/components/settings_modal/admin_tabs/admin_status_card.vue'
|
||||
import BasicUserCard from '../../basic_user_card/basic_user_card.vue'
|
||||
|
||||
const AdminCard = {
|
||||
props: {
|
||||
|
|
@ -27,17 +27,17 @@ const AdminCard = {
|
|||
* @param {any} u
|
||||
* @returns {u is { id: string; _original: { is_approved; is_confirmed: boolean; } } }
|
||||
*/
|
||||
validator (u) {
|
||||
validator(u) {
|
||||
return (
|
||||
typeof(u.id) === 'string' &&
|
||||
typeof(u._original) === 'object' &&
|
||||
typeof(u._original.is_approved) === 'boolean' &&
|
||||
typeof(u._original.is_confirmed) === 'boolean'
|
||||
typeof u.id === 'string' &&
|
||||
typeof u._original === 'object' &&
|
||||
typeof u._original.is_approved === 'boolean' &&
|
||||
typeof u._original.is_confirmed === 'boolean'
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
progress: false,
|
||||
detailsExpanded: false,
|
||||
|
|
@ -49,7 +49,7 @@ const AdminCard = {
|
|||
justDeleted: false,
|
||||
showDirect: false,
|
||||
showReblogs: false,
|
||||
timelineSorting: "des"
|
||||
timelineSorting: 'des',
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -57,27 +57,27 @@ const AdminCard = {
|
|||
* checks if the user is defined
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isLoaded () {
|
||||
return typeof(this.user) !== 'undefined'
|
||||
isLoaded() {
|
||||
return typeof this.user !== 'undefined'
|
||||
},
|
||||
/**
|
||||
* @returns {object} user info
|
||||
*/
|
||||
user () {
|
||||
user() {
|
||||
return this.$store.getters.findUser(this.userDetails.id)
|
||||
},
|
||||
/**
|
||||
* @returns {object} user relationship
|
||||
*/
|
||||
relationship () {
|
||||
relationship() {
|
||||
return this.$store.getters.relationship(this.userDetails.id)
|
||||
},
|
||||
/**
|
||||
* @returns {boolean} is user local
|
||||
*/
|
||||
isLocal () {
|
||||
isLocal() {
|
||||
const u = this.$store.getters.findUser(this.userDetails.id)
|
||||
if (typeof(u) !== 'undefined') {
|
||||
if (typeof u !== 'undefined') {
|
||||
return u.is_local === true
|
||||
}
|
||||
return false
|
||||
|
|
@ -85,9 +85,9 @@ const AdminCard = {
|
|||
/**
|
||||
* @returns {boolean} is user admin
|
||||
*/
|
||||
isAdmin () {
|
||||
isAdmin() {
|
||||
const u = this.$store.getters.findUser(this.userDetails.id)
|
||||
if (typeof(u) !== 'undefined') {
|
||||
if (typeof u !== 'undefined') {
|
||||
return u.rights.admin === true
|
||||
}
|
||||
return false
|
||||
|
|
@ -95,9 +95,9 @@ const AdminCard = {
|
|||
/**
|
||||
* @returns {boolean} is user moderator
|
||||
*/
|
||||
isModerator () {
|
||||
isModerator() {
|
||||
const u = this.$store.getters.findUser(this.userDetails.id)
|
||||
if (typeof(u) !== 'undefined') {
|
||||
if (typeof u !== 'undefined') {
|
||||
return u.rights.moderator === true
|
||||
}
|
||||
return false
|
||||
|
|
@ -105,9 +105,9 @@ const AdminCard = {
|
|||
/**
|
||||
* @returns {boolean} is user active
|
||||
*/
|
||||
isActivated () {
|
||||
isActivated() {
|
||||
const u = this.$store.getters.findUser(this.userDetails.id)
|
||||
if (typeof(u) !== 'undefined') {
|
||||
if (typeof u !== 'undefined') {
|
||||
return u.deactivated === false
|
||||
}
|
||||
return false
|
||||
|
|
@ -115,16 +115,19 @@ const AdminCard = {
|
|||
/**
|
||||
* @returns {boolean} has this user been confirmed
|
||||
*/
|
||||
isConfirmed () {
|
||||
isConfirmed() {
|
||||
const u = this.$store.getters.findUser(this.userDetails.id)
|
||||
return (u._original.is_confirmed === true) || (this.justConfirmed === true)
|
||||
return u._original.is_confirmed === true || this.justConfirmed === true
|
||||
},
|
||||
/**
|
||||
* @returns {boolean} has this user been approved
|
||||
*/
|
||||
isApproved () {
|
||||
return (this.userDetails._original.is_approved === true) || (this.justApproved === true)
|
||||
}
|
||||
isApproved() {
|
||||
return (
|
||||
this.userDetails._original.is_approved === true ||
|
||||
this.justApproved === true
|
||||
)
|
||||
},
|
||||
},
|
||||
components: {
|
||||
BasicUserCard,
|
||||
|
|
@ -135,13 +138,13 @@ const AdminCard = {
|
|||
Popover,
|
||||
GenericConfirm,
|
||||
Select,
|
||||
TextConfirm
|
||||
TextConfirm,
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* @param {boolean} v set admin status
|
||||
*/
|
||||
setAdmin (v) {
|
||||
setAdmin(v) {
|
||||
const u = this.$store.getters.findUser(this.userDetails.id)
|
||||
if (v === true) {
|
||||
this.$store.dispatch('adminAddUserToAdminGroup', u)
|
||||
|
|
@ -152,7 +155,7 @@ const AdminCard = {
|
|||
/**
|
||||
* @param {boolean} v set moderator status
|
||||
*/
|
||||
setModerator (v) {
|
||||
setModerator(v) {
|
||||
const u = this.$store.getters.findUser(this.userDetails.id)
|
||||
if (v === true) {
|
||||
this.$store.dispatch('adminAddUserToModeratorGroup', u)
|
||||
|
|
@ -163,7 +166,7 @@ const AdminCard = {
|
|||
/**
|
||||
* @param {boolean} v set activation status
|
||||
*/
|
||||
setActivation (v) {
|
||||
setActivation(v) {
|
||||
const u = this.$store.getters.findUser(this.userDetails.id)
|
||||
if (v === true) {
|
||||
this.$store.dispatch('adminActivateUser', u)
|
||||
|
|
@ -174,7 +177,7 @@ const AdminCard = {
|
|||
/**
|
||||
* confirm this user
|
||||
*/
|
||||
confirmUser () {
|
||||
confirmUser() {
|
||||
const u = this.$store.getters.findUser(this.userDetails.id)
|
||||
this.$store.dispatch('adminConfirmUser', u)
|
||||
this.just_confirmed = true
|
||||
|
|
@ -182,37 +185,37 @@ const AdminCard = {
|
|||
/**
|
||||
* try resending the confirmation email
|
||||
*/
|
||||
resendConfirmationEmail () {
|
||||
resendConfirmationEmail() {
|
||||
const u = this.$store.getters.findUser(this.userDetails.id)
|
||||
this.$store.dispatch('adminResendConfirmationEmail', u)
|
||||
},
|
||||
/**
|
||||
* approve this user
|
||||
*/
|
||||
approveUser () {
|
||||
approveUser() {
|
||||
const u = this.$store.getters.findUser(this.userDetails.id)
|
||||
this.$store.dispatch('adminApproveUser', u)
|
||||
},
|
||||
/**
|
||||
* update user info from server
|
||||
*/
|
||||
forceUpdateUser () {
|
||||
forceUpdateUser() {
|
||||
this.$store.dispatch('fetchUser', this.userDetails.id)
|
||||
},
|
||||
/**
|
||||
* delete selected statuses
|
||||
*/
|
||||
deleteSelection () {
|
||||
deleteSelection() {
|
||||
const l = this.$refs.timelineList
|
||||
const s = l.getSelected()
|
||||
s.forEach(p => this.$store.dispatch('deleteStatus', p))
|
||||
s.forEach((p) => this.$store.dispatch('deleteStatus', p))
|
||||
l.reset()
|
||||
},
|
||||
/**
|
||||
* delete this user. keep in mind that user deletion is not intuitive in pleroma backend.
|
||||
* it actually deletes all content of a user. the user itself will keep showing up in search results.
|
||||
*/
|
||||
deleteUser () {
|
||||
deleteUser() {
|
||||
if (!this.justDeleted) {
|
||||
const u = this.$store.getters.findUser(this.userDetails.id)
|
||||
this.$store.dispatch('adminDeleteUser', u)
|
||||
|
|
@ -222,17 +225,19 @@ const AdminCard = {
|
|||
/**
|
||||
* @param {string} text name of tag to be added to user
|
||||
*/
|
||||
addUserTag (text) {
|
||||
addUserTag(text) {
|
||||
const u = this.$store.getters.findUser(this.userDetails.id)
|
||||
this.$store.dispatch('adminTagUser', { user: u, tag: text })
|
||||
this.$store
|
||||
.dispatch('adminTagUser', { user: u, tag: text })
|
||||
.then(() => this.$store.dispatch('fetchUser', this.userDetails.id))
|
||||
},
|
||||
/**
|
||||
* @param {string} text name of tag to be removed from user
|
||||
*/
|
||||
removeUserTag (text) {
|
||||
removeUserTag(text) {
|
||||
const u = this.$store.getters.findUser(this.userDetails.id)
|
||||
this.$store.dispatch('adminUntagUser', { user: u, tag: text })
|
||||
this.$store
|
||||
.dispatch('adminUntagUser', { user: u, tag: text })
|
||||
.then(() => this.$store.dispatch('fetchUser', this.userDetails.id))
|
||||
},
|
||||
/**
|
||||
|
|
@ -240,34 +245,48 @@ const AdminCard = {
|
|||
* @param {object} opts
|
||||
* @returns {Promise<Array<object>>} statuses
|
||||
*/
|
||||
async fetchStatuses (store, opts) {
|
||||
async fetchStatuses(store, opts) {
|
||||
const u = this.$store.getters.findUser(this.userDetails.id)
|
||||
const res = store.dispatch('adminListStatuses', { user: u, opts: { pageSize: opts.pageSize, godmode: this.showDirect, withReblogs: this.showReblogs}})
|
||||
return res.then(r => {
|
||||
const res = store.dispatch('adminListStatuses', {
|
||||
user: u,
|
||||
opts: {
|
||||
pageSize: opts.pageSize,
|
||||
godmode: this.showDirect,
|
||||
withReblogs: this.showReblogs,
|
||||
},
|
||||
})
|
||||
return res.then((r) => {
|
||||
const a = r.activities
|
||||
console.log(this.timelineSorting)
|
||||
if (this.timelineSorting === 'des') {
|
||||
return [...a].sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
|
||||
return [...a].sort(
|
||||
(a, b) => new Date(b.created_at) - new Date(a.created_at),
|
||||
)
|
||||
} else if (this.timelineSorting === 'asc') {
|
||||
return [...a].sort((a, b) => new Date(a.created_at) - new Date(b.created_at));
|
||||
return [...a].sort(
|
||||
(a, b) => new Date(a.created_at) - new Date(b.created_at),
|
||||
)
|
||||
} else return []
|
||||
})
|
||||
},
|
||||
confirmAction (box) {
|
||||
confirmAction(box) {
|
||||
this.$refs[box].show()
|
||||
this.$refs.dropdownuser.hidePopover()
|
||||
},
|
||||
userActionConfirmed (action) {
|
||||
this.$store.dispatch(action, this.$store.getters.findUser(this.userDetails.id))
|
||||
userActionConfirmed(action) {
|
||||
this.$store.dispatch(
|
||||
action,
|
||||
this.$store.getters.findUser(this.userDetails.id),
|
||||
)
|
||||
},
|
||||
statusActionConfirmed (action, opts) {
|
||||
statusActionConfirmed(action, opts) {
|
||||
const s = this.$refs.statusList.getSelected()
|
||||
s.forEach(p => {
|
||||
this.$store.dispatch(action, { id: p.id, ...(opts || {})})
|
||||
s.forEach((p) => {
|
||||
this.$store.dispatch(action, { id: p.id, ...(opts || {}) })
|
||||
})
|
||||
this.reset()
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default AdminCard
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||
import Select from 'src/components/select/select.vue'
|
||||
import Status from 'src/components/status/status.vue'
|
||||
|
||||
import { parseStatus } from 'src/services/entity_normalizer/entity_normalizer.service.js'
|
||||
|
||||
const AdminStatusCard = {
|
||||
|
|
@ -18,12 +19,12 @@ const AdminStatusCard = {
|
|||
* @param {any} u
|
||||
* @returns {u is { id: string }}
|
||||
*/
|
||||
validator (u) {
|
||||
return typeof(u.id) === 'string'
|
||||
}
|
||||
}
|
||||
validator(u) {
|
||||
return typeof u.id === 'string'
|
||||
},
|
||||
},
|
||||
},
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
jsonExpanded: false,
|
||||
statusCache: undefined,
|
||||
|
|
@ -33,28 +34,38 @@ const AdminStatusCard = {
|
|||
/**
|
||||
* @returns {boolean} is this status sensitive?
|
||||
*/
|
||||
isSensitive () {
|
||||
isSensitive() {
|
||||
return this.statusDetails.sensitive === true
|
||||
},
|
||||
/**
|
||||
* @returns {'public' | 'unlisted' | 'private' | 'direct'} status visibility
|
||||
*/
|
||||
visibility () {
|
||||
visibility() {
|
||||
return this.statusDetails.visibility
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* @param {boolean} v set sensitive
|
||||
*/
|
||||
changeSensitivity (v) {
|
||||
this.$store.dispatch('adminChangeStatusScope', { opts: { id: this.statusDetails.id, sensitive: v }}).then(res => parseStatus(res)).then(s => this.statusCache = s)
|
||||
changeSensitivity(v) {
|
||||
this.$store
|
||||
.dispatch('adminChangeStatusScope', {
|
||||
opts: { id: this.statusDetails.id, sensitive: v },
|
||||
})
|
||||
.then((res) => parseStatus(res))
|
||||
.then((s) => (this.statusCache = s))
|
||||
},
|
||||
/**
|
||||
* @param {boolean} v set visible
|
||||
*/
|
||||
changeVisibility (v) {
|
||||
this.$store.dispatch('adminChangeStatusScope', { opts: { id: this.statusDetails.id, visibility: v }}).then(res => parseStatus(res)).then(s => this.statusCache = s)
|
||||
changeVisibility(v) {
|
||||
this.$store
|
||||
.dispatch('adminChangeStatusScope', {
|
||||
opts: { id: this.statusDetails.id, visibility: v },
|
||||
})
|
||||
.then((res) => parseStatus(res))
|
||||
.then((s) => (this.statusCache = s))
|
||||
},
|
||||
/**
|
||||
* show the confirmation box for bulk actions.
|
||||
|
|
@ -71,13 +82,19 @@ const AdminStatusCard = {
|
|||
selectionConfirmed(action, opts) {
|
||||
const restricted = []
|
||||
const s = this.$refs.userList.getSelected()
|
||||
s.forEach(u => {
|
||||
if (restricted.includes(action) !== false || u.id !== this.$store.state.users.currentUser.id) {
|
||||
this.$store.dispatch(action, { id: this.statusDetails.id, ...(opts || {}) })
|
||||
s.forEach((u) => {
|
||||
if (
|
||||
restricted.includes(action) !== false ||
|
||||
u.id !== this.$store.state.users.currentUser.id
|
||||
) {
|
||||
this.$store.dispatch(action, {
|
||||
id: this.statusDetails.id,
|
||||
...(opts || {}),
|
||||
})
|
||||
}
|
||||
})
|
||||
this.reset()
|
||||
}
|
||||
},
|
||||
},
|
||||
components: {
|
||||
Checkbox,
|
||||
|
|
@ -87,9 +104,14 @@ const AdminStatusCard = {
|
|||
/**
|
||||
* fetch and cache status info
|
||||
*/
|
||||
mounted () {
|
||||
this.$store.dispatch('adminChangeStatusScope', { opts: { id: this.statusDetails.id }}).then(res => parseStatus(res)).then(s => this.statusCache = s)
|
||||
}
|
||||
mounted() {
|
||||
this.$store
|
||||
.dispatch('adminChangeStatusScope', {
|
||||
opts: { id: this.statusDetails.id },
|
||||
})
|
||||
.then((res) => parseStatus(res))
|
||||
.then((s) => (this.statusCache = s))
|
||||
},
|
||||
}
|
||||
|
||||
export default AdminStatusCard
|
||||
|
|
|
|||
40
src/components/settings_modal/admin_tabs/auth_tab.js
Normal file
40
src/components/settings_modal/admin_tabs/auth_tab.js
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import AttachmentSetting from '../helpers/attachment_setting.vue'
|
||||
import BooleanSetting from '../helpers/boolean_setting.vue'
|
||||
import ChoiceSetting from '../helpers/choice_setting.vue'
|
||||
import GroupSetting from '../helpers/group_setting.vue'
|
||||
import IntegerSetting from '../helpers/integer_setting.vue'
|
||||
import ListSetting from '../helpers/list_setting.vue'
|
||||
import MapSetting from '../helpers/map_setting.vue'
|
||||
import SharedComputedObject from '../helpers/shared_computed_object.js'
|
||||
import StringSetting from '../helpers/string_setting.vue'
|
||||
import TupleSetting from '../helpers/tuple_setting.vue'
|
||||
|
||||
const AuthTab = {
|
||||
provide() {
|
||||
return {
|
||||
defaultDraftMode: true,
|
||||
defaultSource: 'admin',
|
||||
}
|
||||
},
|
||||
components: {
|
||||
BooleanSetting,
|
||||
ChoiceSetting,
|
||||
IntegerSetting,
|
||||
StringSetting,
|
||||
TupleSetting,
|
||||
AttachmentSetting,
|
||||
GroupSetting,
|
||||
ListSetting,
|
||||
MapSetting,
|
||||
},
|
||||
computed: {
|
||||
...SharedComputedObject(),
|
||||
LDAPEnabled() {
|
||||
return this.$store.state.adminSettings.draft[':pleroma'][':ldap'][
|
||||
':enabled'
|
||||
]
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default AuthTab
|
||||
105
src/components/settings_modal/admin_tabs/auth_tab.vue
Normal file
105
src/components/settings_modal/admin_tabs/auth_tab.vue
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
<template>
|
||||
<div :label="$t('admin_dash.tabs.job_queues')">
|
||||
<div class="setting-section">
|
||||
<h3>{{ $t('admin_dash.auth.MFA') }}</h3>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<h4>{{ $t('admin_dash.auth.TOTP') }}</h4>
|
||||
<ul class="setting-list suboptions">
|
||||
<li>
|
||||
<IntegerSetting path=":pleroma.:instance.:multi_factor_authentication.:totp.:digits" />
|
||||
</li>
|
||||
<li>
|
||||
<IntegerSetting path=":pleroma.:instance.:multi_factor_authentication.:totp.:period" />
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<h4>{{ $t('admin_dash.auth.backup_codes') }}</h4>
|
||||
<ul class="setting-list suboptions">
|
||||
<li>
|
||||
<IntegerSetting path=":pleroma.:instance.:multi_factor_authentication.:backup_codes.:number" />
|
||||
</li>
|
||||
<li>
|
||||
<IntegerSetting path=":pleroma.:instance.:multi_factor_authentication.:backup_codes.:length" />
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<GroupSetting path=":pleroma.:instance.:multi_factor_authentication" />
|
||||
</ul>
|
||||
<h3>{{ $t('admin_dash.auth.OAuth') }}</h3>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<StringSetting path=":pleroma.:auth.:auth_template" />
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path=":pleroma.:auth.:enforce_oauth_admin_scope_usage" />
|
||||
</li>
|
||||
<li>
|
||||
<StringSetting path=":pleroma.:auth.:oauth_consumer_template" />
|
||||
</li>
|
||||
<li>
|
||||
<ListSetting path=":pleroma.:auth.:oauth_consumer_strategies" />
|
||||
</li>
|
||||
<li>
|
||||
<IntegerSetting path=":pleroma.:oauth2.:token_expires_in" />
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path=":pleroma.:oauth2.:issue_new_refresh_token" />
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path=":pleroma.:oauth2.:clean_expired_tokens" />
|
||||
</li>
|
||||
</ul>
|
||||
<h3>{{ $t('admin_dash.auth.LDAP') }}</h3>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<BooleanSetting path=":pleroma.:ldap.:enabled" />
|
||||
</li>
|
||||
<template v-if="LDAPEnabled">
|
||||
<li>
|
||||
<StringSetting path=":pleroma.:ldap.:host" />
|
||||
</li>
|
||||
<li>
|
||||
<IntegerSetting path=":pleroma.:ldap.:port" />
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path=":pleroma.:ldap.:tls" />
|
||||
</li>
|
||||
<li>
|
||||
<!-- CONFIRM old admin FE only supports ONE setting which is Verify, is that correct or should we allow more than one? -->
|
||||
<MapSetting
|
||||
:allow-new="false"
|
||||
path=":pleroma.:ldap.:tlsopts"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path=":pleroma.:ldap.:ssl" />
|
||||
</li>
|
||||
<li>
|
||||
<!-- CONFIRM old admin FE only supports ONE setting which is Verify, is that correct or should we allow more than one? -->
|
||||
<MapSetting
|
||||
:allow-new="false"
|
||||
path=":pleroma.:ldap.:sslopts"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<StringSetting path=":pleroma.:ldap.:base" />
|
||||
</li>
|
||||
<li>
|
||||
<StringSetting path=":pleroma.:ldap.:uid" />
|
||||
</li>
|
||||
<li>
|
||||
<StringSetting path=":pleroma.:ldap.:cacertfile" />
|
||||
</li>
|
||||
<li>
|
||||
<StringSetting path=":pleroma.:ldap.:mail" />
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
</div>
|
||||
<!-- CONFIRM admin token is present in AdminFE but missing in both data and descriptions?? -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./auth_tab.js"></script>
|
||||
|
|
@ -1,14 +1,29 @@
|
|||
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 Select from 'components/select/select.vue'
|
||||
import StillImage from 'components/still-image/still-image.vue'
|
||||
import { assign, clone } from 'lodash'
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
|
||||
import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'
|
||||
import EmojiEditingPopover from '../helpers/emoji_editing_popover.vue'
|
||||
import { useInterfaceStore } from 'src/stores/interface'
|
||||
import ModifiedIndicator from '../helpers/modified_indicator.vue'
|
||||
import SharedComputedObject from '../helpers/shared_computed_object.js'
|
||||
import StringSetting from '../helpers/string_setting.vue'
|
||||
|
||||
import { useEmojiStore } from 'src/stores/emoji.js'
|
||||
import { useInstanceStore } from 'src/stores/instance.js'
|
||||
import { useInterfaceStore } from 'src/stores/interface.js'
|
||||
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faArrowsRotate,
|
||||
faDownload,
|
||||
faFolderOpen,
|
||||
faServer,
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(faArrowsRotate, faFolderOpen, faDownload, faServer)
|
||||
|
||||
const EmojiTab = {
|
||||
components: {
|
||||
|
|
@ -18,16 +33,19 @@ const EmojiTab = {
|
|||
StillImage,
|
||||
Select,
|
||||
Popover,
|
||||
ConfirmModal,
|
||||
ConfirmModal: defineAsyncComponent(
|
||||
() => import('src/components/confirm_modal/confirm_modal.vue'),
|
||||
),
|
||||
|
||||
ModifiedIndicator,
|
||||
EmojiEditingPopover
|
||||
EmojiEditingPopover,
|
||||
},
|
||||
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
knownLocalPacks: { },
|
||||
knownRemotePacks: { },
|
||||
editedMetadata: { },
|
||||
knownLocalPacks: {},
|
||||
knownRemotePacks: {},
|
||||
editedMetadata: {},
|
||||
packName: '',
|
||||
newPackName: '',
|
||||
deleteModalVisible: false,
|
||||
|
|
@ -35,169 +53,162 @@ const EmojiTab = {
|
|||
remotePackDownloadAs: '',
|
||||
|
||||
remotePackURL: '',
|
||||
remotePackFile: null
|
||||
remotePackFile: null,
|
||||
}
|
||||
},
|
||||
|
||||
provide () {
|
||||
provide() {
|
||||
return { emojiAddr: this.emojiAddr }
|
||||
},
|
||||
|
||||
computed: {
|
||||
pack () {
|
||||
...SharedComputedObject(),
|
||||
pack() {
|
||||
return this.packName !== '' ? this.knownPacks[this.packName] : undefined
|
||||
},
|
||||
packMeta () {
|
||||
packMeta() {
|
||||
if (this.packName === '') return {}
|
||||
if (this.editedMetadata[this.packName] === undefined) {
|
||||
this.editedMetadata[this.packName] = clone(this.pack.pack)
|
||||
}
|
||||
|
||||
return this.editedMetadata[this.packName]
|
||||
},
|
||||
knownPacks () {
|
||||
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]
|
||||
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)
|
||||
}
|
||||
downloadWillReplaceLocal() {
|
||||
return (
|
||||
(this.remotePackDownloadAs.trim() === '' &&
|
||||
this.pack.remote &&
|
||||
this.pack.remote.baseName in this.knownLocalPacks) ||
|
||||
this.remotePackDownloadAs in this.knownLocalPacks
|
||||
)
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
reloadEmoji () {
|
||||
reloadEmoji() {
|
||||
this.$store.state.api.backendInteractor.reloadEmoji()
|
||||
},
|
||||
importFromFS () {
|
||||
importFromFS() {
|
||||
this.$store.state.api.backendInteractor.importEmojiFromFS()
|
||||
},
|
||||
emojiAddr (name) {
|
||||
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}`
|
||||
return `${useInstanceStore().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(() => {
|
||||
this.$refs.createPackPopover.hidePopover()
|
||||
|
||||
this.packName = this.newPackName
|
||||
this.newPackName = ''
|
||||
})
|
||||
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(() => {
|
||||
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(() => {
|
||||
delete this.editedMetadata[this.packName]
|
||||
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(() => {
|
||||
delete this.editedMetadata[this.packName]
|
||||
|
||||
this.deleteModalVisible = false
|
||||
this.packName = ''
|
||||
})
|
||||
this.deleteModalVisible = false
|
||||
this.packName = ''
|
||||
})
|
||||
},
|
||||
|
||||
metaEdited (prop) {
|
||||
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
|
||||
}
|
||||
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]
|
||||
})
|
||||
// Update actual pack data
|
||||
this.pack.pack = resp
|
||||
// Delete edited pack data, should auto-update itself
|
||||
delete this.editedMetadata[this.packName]
|
||||
})
|
||||
},
|
||||
|
||||
updatePackFiles (newFiles, packName) {
|
||||
updatePackFiles(newFiles, packName) {
|
||||
this.knownPacks[packName].files = newFiles
|
||||
this.sortPackFiles(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(() => allPacks)
|
||||
.catch(data => {
|
||||
this.displayError(data)
|
||||
})
|
||||
},
|
||||
|
||||
refreshPackList () {
|
||||
this.loadPacksPaginated(this.$store.state.api.backendInteractor.listEmojiPacks)
|
||||
.then(allPacks => {
|
||||
refreshPackList() {
|
||||
useEmojiStore()
|
||||
.getAdminPacks(
|
||||
this.remotePackInstance,
|
||||
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 => {
|
||||
listRemotePacks() {
|
||||
useEmojiStore()
|
||||
.getAdminPacks(
|
||||
this.remotePackInstance,
|
||||
this.$store.state.api.backendInteractor.listRemoteEmojiPacks,
|
||||
)
|
||||
.then((allPacks) => {
|
||||
let inst = this.remotePackInstance
|
||||
if (!inst.startsWith('http')) { inst = 'https://' + inst }
|
||||
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
|
||||
instance: instUrl.origin,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -205,98 +216,102 @@ const EmojiTab = {
|
|||
for (const pack in this.knownRemotePacks[inst]) {
|
||||
this.sortPackFiles(`${pack}@${inst}`)
|
||||
}
|
||||
|
||||
this.$refs.remotePackPopover.hidePopover()
|
||||
})
|
||||
.catch(data => {
|
||||
.catch((data) => {
|
||||
this.displayError(data)
|
||||
})
|
||||
},
|
||||
downloadRemotePack () {
|
||||
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 => {
|
||||
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.downloadPackPopover.hidePopover()
|
||||
|
||||
return this.refreshPackList()
|
||||
} else {
|
||||
this.displayError(resp.error)
|
||||
return Promise.reject(resp)
|
||||
}
|
||||
}).then(() => {
|
||||
})
|
||||
.then(() => {
|
||||
this.packName = this.remotePackDownloadAs
|
||||
this.remotePackDownloadAs = ''
|
||||
})
|
||||
},
|
||||
downloadRemoteURLPack () {
|
||||
this.$store.state.api.backendInteractor.downloadRemoteEmojiPackZIP({
|
||||
url: this.remotePackURL, packName: this.newPackName
|
||||
})
|
||||
.then(data => data.json())
|
||||
.then(resp => {
|
||||
downloadRemoteURLPack() {
|
||||
this.$store.state.api.backendInteractor
|
||||
.downloadRemoteEmojiPackZIP({
|
||||
url: this.remotePackURL,
|
||||
packName: this.newPackName,
|
||||
})
|
||||
.then((data) => data.json())
|
||||
.then((resp) => {
|
||||
if (resp === 'ok') {
|
||||
this.$refs.additionalRemotePopover.hidePopover()
|
||||
|
||||
return this.refreshPackList()
|
||||
} else {
|
||||
this.displayError(resp.error)
|
||||
return Promise.reject(resp)
|
||||
}
|
||||
}).then(() => {
|
||||
})
|
||||
.then(() => {
|
||||
this.packName = this.newPackName
|
||||
this.newPackName = ''
|
||||
this.remotePackURL = ''
|
||||
})
|
||||
},
|
||||
downloadRemoteFilePack () {
|
||||
this.$store.state.api.backendInteractor.downloadRemoteEmojiPackZIP({
|
||||
file: this.remotePackFile[0], packName: this.newPackName
|
||||
})
|
||||
.then(data => data.json())
|
||||
.then(resp => {
|
||||
downloadRemoteFilePack() {
|
||||
this.$store.state.api.backendInteractor
|
||||
.downloadRemoteEmojiPackZIP({
|
||||
file: this.remotePackFile[0],
|
||||
packName: this.newPackName,
|
||||
})
|
||||
.then((data) => data.json())
|
||||
.then((resp) => {
|
||||
if (resp === 'ok') {
|
||||
this.$refs.additionalRemotePopover.hidePopover()
|
||||
|
||||
return this.refreshPackList()
|
||||
} else {
|
||||
this.displayError(resp.error)
|
||||
return Promise.reject(resp)
|
||||
}
|
||||
}).then(() => {
|
||||
})
|
||||
.then(() => {
|
||||
this.packName = this.newPackName
|
||||
this.newPackName = ''
|
||||
this.remotePackURL = ''
|
||||
})
|
||||
},
|
||||
|
||||
displayError (msg) {
|
||||
displayError(msg) {
|
||||
useInterfaceStore().pushGlobalNotice({
|
||||
messageKey: 'admin_dash.emoji.error',
|
||||
messageArgs: [msg],
|
||||
level: 'error'
|
||||
level: 'error',
|
||||
})
|
||||
},
|
||||
sortPackFiles (nameOfPack) {
|
||||
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
|
||||
}, {})
|
||||
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 () {
|
||||
mounted() {
|
||||
this.refreshPackList()
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export default EmojiTab
|
||||
|
|
|
|||
|
|
@ -1,10 +1,54 @@
|
|||
.emoji-tab {
|
||||
.btn-group .btn:not(:first-child) {
|
||||
margin-left: 0.5em;
|
||||
.EmojiTab {
|
||||
.setting-list {
|
||||
margin: 0.5em 2em;
|
||||
}
|
||||
|
||||
.pack-info-wrapper {
|
||||
.toolbar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5em;
|
||||
|
||||
.header-buttons {
|
||||
display: flex;
|
||||
flex: 1 0 auto;
|
||||
justify-content: end;
|
||||
align-items: end;
|
||||
|
||||
&:not(.btn-group) {
|
||||
gap: 0.5em;
|
||||
}
|
||||
|
||||
.header-text {
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
.button-default {
|
||||
flex: 0 0 auto;
|
||||
padding: 0.5em;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.popover-wrapper {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.selector-buttons,
|
||||
.meta-buttons {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5em;
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
h5 {
|
||||
margin-top: 1em;
|
||||
margin-bottom: 0.25em;
|
||||
}
|
||||
|
||||
h3.toolbar {
|
||||
align-items: end;
|
||||
}
|
||||
|
||||
.emoji-info-input {
|
||||
|
|
@ -18,8 +62,8 @@
|
|||
}
|
||||
|
||||
.emoji {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
width: 2em;
|
||||
height: 2em;
|
||||
}
|
||||
|
||||
.emoji-unsaved {
|
||||
|
|
@ -30,6 +74,22 @@
|
|||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1em;
|
||||
|
||||
.button-unstyled {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.emoji-item,
|
||||
.placeholder {
|
||||
width: 2em;
|
||||
height: 2em;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
background: var(--textFaint);
|
||||
border-radius: 0.5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,168 +1,75 @@
|
|||
<template>
|
||||
<div
|
||||
class="emoji-tab"
|
||||
class="EmojiTab"
|
||||
:label="$t('admin_dash.tabs.emoji')"
|
||||
>
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('admin_dash.tabs.emoji') }}</h2>
|
||||
<div class="setting-section">
|
||||
<h3 class="toolbar">
|
||||
<span class="header-text">
|
||||
{{ $t('admin_dash.emoji.emoji_packs') }}
|
||||
</span>
|
||||
|
||||
<ul class="setting-list">
|
||||
<h3>{{ $t('admin_dash.emoji.global_actions') }}</h3>
|
||||
|
||||
<li class="btn-group setting-item">
|
||||
<span class="header-buttons btn-group">
|
||||
<button
|
||||
class="button button-default btn"
|
||||
class="button button-default"
|
||||
type="button"
|
||||
:title="$t('admin_dash.emoji.reload')"
|
||||
@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>
|
||||
<button
|
||||
class="button button-default emoji-panel-additional-actions"
|
||||
@click="$refs.additionalRemotePopover.showPopover"
|
||||
>
|
||||
<FAIcon
|
||||
icon="chevron-down"
|
||||
/>
|
||||
|
||||
<Popover
|
||||
ref="additionalRemotePopover"
|
||||
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"
|
||||
>
|
||||
<h3>Import pack from URL</h3>
|
||||
<input
|
||||
v-model="remotePackURL"
|
||||
class="input"
|
||||
placeholder="Pack .zip URL"
|
||||
>
|
||||
<button
|
||||
class="button button-default btn emoji-tab-popover-button"
|
||||
type="button"
|
||||
:disabled="newPackName.trim() === '' || remotePackURL.trim() === ''"
|
||||
@click="downloadRemoteURLPack"
|
||||
>
|
||||
Import
|
||||
</button>
|
||||
<h3>Import pack from a file</h3>
|
||||
<input
|
||||
type="file"
|
||||
accept="application/zip"
|
||||
class="emoji-tab-popover-file input"
|
||||
@change="remotePackFile = $event.target.files"
|
||||
>
|
||||
<button
|
||||
class="button button-default btn emoji-tab-popover-button"
|
||||
type="button"
|
||||
:disabled="newPackName.trim() === '' || remotePackFile === null || remotePackFile.length === 0"
|
||||
@click="downloadRemoteFilePack"
|
||||
>
|
||||
Import
|
||||
</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') }}
|
||||
<FAIcon icon="arrows-rotate" />
|
||||
{{ $t('admin_dash.emoji.reload_short') }}
|
||||
</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 #trigger>
|
||||
<button
|
||||
class="button button-default"
|
||||
type="button"
|
||||
:title="$t('admin_dash.emoji.remote_packs')"
|
||||
>
|
||||
<FAIcon icon="download" />
|
||||
{{ $t('admin_dash.emoji.remote_packs_short') }}
|
||||
</button>
|
||||
</template>
|
||||
<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 emoji-tab-popover-button"
|
||||
type="button"
|
||||
@click="listRemotePacks"
|
||||
>
|
||||
{{ $t('admin_dash.emoji.do_list') }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</Popover>
|
||||
|
||||
<Popover
|
||||
ref="additionalRemotePopover"
|
||||
popover-class="emoji-tab-edit-popover popover-default"
|
||||
trigger="click"
|
||||
placement="bottom"
|
||||
>
|
||||
<template #trigger>
|
||||
<button
|
||||
class="button button-default emoji-panel-additional-actions"
|
||||
:title="$t('admin_dash.emoji.import_pack')"
|
||||
@click="$refs.additionalRemotePopover.showPopover"
|
||||
>
|
||||
<FAIcon icon="folder-open" />
|
||||
{{ $t('admin_dash.emoji.import_pack_short') }}
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<template #content>
|
||||
<div class="emoji-tab-popover-input">
|
||||
<h3>{{ $t('admin_dash.emoji.new_pack_name') }}</h3>
|
||||
|
|
@ -171,94 +78,276 @@
|
|||
:placeholder="$t('admin_dash.emoji.new_pack_name')"
|
||||
class="input"
|
||||
>
|
||||
<h3>Import pack from URL</h3>
|
||||
<input
|
||||
v-model="remotePackURL"
|
||||
class="input"
|
||||
placeholder="Pack .zip URL"
|
||||
>
|
||||
<button
|
||||
class="button button-default btn emoji-tab-popover-button"
|
||||
type="button"
|
||||
@click="createEmojiPack"
|
||||
:disabled="newPackName.trim() === '' || remotePackURL.trim() === ''"
|
||||
@click="downloadRemoteURLPack"
|
||||
>
|
||||
{{ $t('admin_dash.emoji.create') }}
|
||||
Import
|
||||
</button>
|
||||
<h3>Import pack from a file</h3>
|
||||
<input
|
||||
type="file"
|
||||
accept="application/zip"
|
||||
class="emoji-tab-popover-file input"
|
||||
@change="remotePackFile = $event.target.files"
|
||||
>
|
||||
<button
|
||||
class="button button-default btn emoji-tab-popover-button"
|
||||
type="button"
|
||||
:disabled="newPackName.trim() === '' || remotePackFile === null || remotePackFile.length === 0"
|
||||
@click="downloadRemoteFilePack"
|
||||
>
|
||||
Import
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</Popover>
|
||||
</li>
|
||||
</ul>
|
||||
</span>
|
||||
</h3>
|
||||
<div class="setting-section">
|
||||
<h4 class="toolbar">
|
||||
{{ $t('admin_dash.emoji.edit_pack') }}
|
||||
</h4>
|
||||
<div class="setting-item selector-buttons">
|
||||
<button
|
||||
:disabled="!pack || pack.remote !== undefined"
|
||||
class="button button-default btn"
|
||||
type="button"
|
||||
@click="deleteModalVisible = true"
|
||||
>
|
||||
{{ $t('admin_dash.emoji.delete_pack') }}
|
||||
|
||||
<div v-if="pack">
|
||||
<div class="pack-info-wrapper">
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<label>
|
||||
{{ $t('admin_dash.emoji.description') }}
|
||||
<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
|
||||
:disabled="!pack || pack.remote === undefined"
|
||||
class="button button-default btn"
|
||||
type="button"
|
||||
@click="$refs.downloadPackPopover.showPopover"
|
||||
>
|
||||
{{ $t('admin_dash.emoji.download_pack') }}
|
||||
|
||||
<Popover
|
||||
ref="downloadPackPopover"
|
||||
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>
|
||||
|
||||
<span class="btn-group">
|
||||
<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>
|
||||
|
||||
<Popover
|
||||
ref="createPackPopover"
|
||||
popover-class="emoji-tab-edit-popover popover-default"
|
||||
trigger="click"
|
||||
placement="bottom"
|
||||
>
|
||||
<template #trigger>
|
||||
<button
|
||||
class="button button-default btn emoji-tab-popover-button"
|
||||
type="button"
|
||||
>
|
||||
{{ $t('admin_dash.emoji.create_pack') }}
|
||||
</button>
|
||||
</template>
|
||||
<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>
|
||||
</span>
|
||||
</div>
|
||||
<h5>
|
||||
{{ $t('admin_dash.emoji.metadata') }}
|
||||
|
||||
<ModifiedIndicator
|
||||
:changed="$refs.emojiPopovers && $refs.emojiPopovers.some(p => p.isEdited)"
|
||||
message-key="admin_dash.emoji.emoji_changed"
|
||||
/>
|
||||
</h5>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<label
|
||||
class="setting-item"
|
||||
:class="{ ['-disabled']: !pack || pack.remote !== undefined }"
|
||||
>
|
||||
<span class="setting-label">
|
||||
<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') }}
|
||||
{{ $t('admin_dash.emoji.description') }}
|
||||
</span>
|
||||
<textarea
|
||||
v-model="packMeta.description"
|
||||
:disabled="!pack || pack.remote !== undefined"
|
||||
height="4"
|
||||
class="bio resize-height input textarea setting-control"
|
||||
/>
|
||||
</label>
|
||||
</li>
|
||||
<li>
|
||||
<label
|
||||
class="setting-item"
|
||||
:class="{ ['-disabled']: !pack || pack.remote !== undefined }"
|
||||
>
|
||||
<span class="setting-label">
|
||||
<ModifiedIndicator
|
||||
:changed="metaEdited('homepage')"
|
||||
message-key="admin_dash.emoji.metadata_changed"
|
||||
/>
|
||||
{{ $t('admin_dash.emoji.homepage') }}
|
||||
</span>
|
||||
|
||||
<input
|
||||
v-model="packMeta.homepage"
|
||||
class="emoji-info-input input"
|
||||
:disabled="pack.remote !== undefined"
|
||||
>
|
||||
</label>
|
||||
</li>
|
||||
<li>
|
||||
<label>
|
||||
{{ $t('admin_dash.emoji.fallback_src') }}
|
||||
<input
|
||||
v-model="packMeta.homepage"
|
||||
class="emoji-info-input input setting-control"
|
||||
:disabled="!pack || pack.remote !== undefined"
|
||||
>
|
||||
</label>
|
||||
</li>
|
||||
<li>
|
||||
<label
|
||||
class="setting-item"
|
||||
:class="{ ['-disabled']: !pack || pack.remote !== undefined }"
|
||||
>
|
||||
<span class="setting-label">
|
||||
<ModifiedIndicator
|
||||
:changed="metaEdited('fallback-src')"
|
||||
message-key="admin_dash.emoji.metadata_changed"
|
||||
/>
|
||||
{{ $t('admin_dash.emoji.fallback_src') }}
|
||||
</span>
|
||||
|
||||
<input
|
||||
v-model="packMeta['fallback-src']"
|
||||
class="emoji-info-input input"
|
||||
:disabled="pack.remote !== undefined"
|
||||
>
|
||||
</label>
|
||||
</li>
|
||||
<li>
|
||||
<label>
|
||||
<input
|
||||
v-model="packMeta['fallback-src']"
|
||||
class="emoji-info-input input setting-control"
|
||||
:disabled="!pack || pack.remote !== undefined"
|
||||
>
|
||||
</label>
|
||||
</li>
|
||||
<li>
|
||||
<label
|
||||
class="setting-item"
|
||||
:class="{ ['-disabled']: !pack || pack.remote !== undefined }"
|
||||
>
|
||||
<span class="setting-label">
|
||||
{{ $t('admin_dash.emoji.fallback_sha256') }}
|
||||
</span>
|
||||
|
||||
<input
|
||||
v-model="packMeta['fallback-src-sha256']"
|
||||
:disabled="true"
|
||||
class="emoji-info-input input"
|
||||
>
|
||||
</label>
|
||||
</li>
|
||||
<li>
|
||||
<input
|
||||
v-model="packMeta['fallback-src-sha256']"
|
||||
:disabled="!pack || pack.remote !== undefined"
|
||||
class="emoji-info-input input setting-control"
|
||||
>
|
||||
</label>
|
||||
</li>
|
||||
<li>
|
||||
<div class="setting-item">
|
||||
<Checkbox
|
||||
v-model="packMeta['share-files']"
|
||||
:disabled="pack.remote !== undefined"
|
||||
:disabled="!pack || pack.remote !== undefined"
|
||||
class="setting-label setting-control"
|
||||
>
|
||||
<ModifiedIndicator
|
||||
:changed="metaEdited('share-files')"
|
||||
message-key="admin_dash.emoji.metadata_changed"
|
||||
/>
|
||||
{{ $t('admin_dash.emoji.share') }}
|
||||
</Checkbox>
|
||||
|
||||
<ModifiedIndicator
|
||||
:changed="metaEdited('share-files')"
|
||||
message-key="admin_dash.emoji.metadata_changed"
|
||||
/>
|
||||
</li>
|
||||
<li class="btn-group">
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div class="meta-buttons">
|
||||
<button
|
||||
v-if="pack.remote === undefined"
|
||||
v-if="pack && pack.remote === undefined"
|
||||
class="button button-default btn"
|
||||
type="button"
|
||||
@click="savePackMetadata"
|
||||
|
|
@ -266,148 +355,89 @@
|
|||
{{ $t('admin_dash.emoji.save_meta') }}
|
||||
</button>
|
||||
<button
|
||||
v-if="pack.remote === undefined"
|
||||
v-if="pack && 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.downloadPackPopover.showPopover"
|
||||
>
|
||||
{{ $t('admin_dash.emoji.download_pack') }}
|
||||
|
||||
<Popover
|
||||
ref="downloadPackPopover"
|
||||
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"
|
||||
@update-pack-files="updatePackFiles"
|
||||
@display-error="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.${pack.remote === undefined ? 'editing' : 'copying'}`, [shortcode])"
|
||||
:shortcode="shortcode"
|
||||
:file="file"
|
||||
:pack-name="packName"
|
||||
:remote="pack.remote"
|
||||
:known-local-packs="knownLocalPacks"
|
||||
@update-pack-files="updatePackFiles"
|
||||
@display-error="displayError"
|
||||
>
|
||||
<template #trigger>
|
||||
<StillImage
|
||||
class="emoji"
|
||||
:src="emojiAddr(file)"
|
||||
:title="`:${shortcode}:`"
|
||||
:alt="`:${shortcode}:`"
|
||||
/>
|
||||
</template>
|
||||
</EmojiEditingPopover>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<h5>
|
||||
{{ $t('admin_dash.emoji.files') }}
|
||||
|
||||
<ModifiedIndicator
|
||||
:changed="$refs.emojiPopovers && $refs.emojiPopovers.some(p => p.isEdited)"
|
||||
message-key="admin_dash.emoji.emoji_changed"
|
||||
/>
|
||||
</h5>
|
||||
|
||||
<div
|
||||
class="emoji-list setting-list"
|
||||
>
|
||||
<EmojiEditingPopover
|
||||
v-if="pack && pack.remote === undefined"
|
||||
class="emoji-item"
|
||||
placement="bottom"
|
||||
new-upload
|
||||
:title="$t('admin_dash.emoji.adding_new')"
|
||||
:pack-name="packName"
|
||||
@update-pack-files="updatePackFiles"
|
||||
@display-error="displayError"
|
||||
>
|
||||
<template #trigger>
|
||||
<FAIcon
|
||||
icon="plus"
|
||||
size="2x"
|
||||
:title="$t('admin_dash.emoji.add_file')"
|
||||
/>
|
||||
</template>
|
||||
</EmojiEditingPopover>
|
||||
<template v-if="!pack">
|
||||
<div
|
||||
v-for="(_, i) in new Array(20)"
|
||||
:key="i"
|
||||
class="placeholder"
|
||||
/>
|
||||
</template>
|
||||
|
||||
|
||||
<EmojiEditingPopover
|
||||
v-for="(file, shortcode) in (pack?.files || [])"
|
||||
ref="emojiPopovers"
|
||||
:key="shortcode"
|
||||
placement="top"
|
||||
:title="$t(`admin_dash.emoji.${pack?.remote === undefined ? 'editing' : 'copying'}`, [shortcode])"
|
||||
:shortcode="shortcode"
|
||||
:file="file"
|
||||
:pack-name="packName"
|
||||
:remote="pack?.remote"
|
||||
:known-local-packs="knownLocalPacks"
|
||||
@update-pack-files="updatePackFiles"
|
||||
@display-error="displayError"
|
||||
>
|
||||
<template #trigger>
|
||||
<StillImage
|
||||
class="emoji"
|
||||
:src="emojiAddr(file)"
|
||||
:title="`:${shortcode}:`"
|
||||
:alt="`:${shortcode}:`"
|
||||
/>
|
||||
</template>
|
||||
</EmojiEditingPopover>
|
||||
</div>
|
||||
</div>
|
||||
<h3>{{ $t('admin_dash.emoji.advanced') }}</h3>
|
||||
<button
|
||||
class="button button-default btn"
|
||||
type="button"
|
||||
@click="importFromFS"
|
||||
>
|
||||
<FAIcon icon="server" />
|
||||
{{ $t('admin_dash.emoji.importFS') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
33
src/components/settings_modal/admin_tabs/federation_tab.js
Normal file
33
src/components/settings_modal/admin_tabs/federation_tab.js
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import AttachmentSetting from '../helpers/attachment_setting.vue'
|
||||
import BooleanSetting from '../helpers/boolean_setting.vue'
|
||||
import GroupSetting from '../helpers/group_setting.vue'
|
||||
import IntegerSetting from '../helpers/integer_setting.vue'
|
||||
import ListSetting from '../helpers/list_setting.vue'
|
||||
import ListTupleSetting from '../helpers/list_tuple_setting.vue'
|
||||
import MapSetting from '../helpers/map_setting.vue'
|
||||
import SharedComputedObject from '../helpers/shared_computed_object.js'
|
||||
import StringSetting from '../helpers/string_setting.vue'
|
||||
|
||||
const FederationTab = {
|
||||
provide() {
|
||||
return {
|
||||
defaultDraftMode: true,
|
||||
defaultSource: 'admin',
|
||||
}
|
||||
},
|
||||
components: {
|
||||
BooleanSetting,
|
||||
IntegerSetting,
|
||||
StringSetting,
|
||||
AttachmentSetting,
|
||||
ListSetting,
|
||||
ListTupleSetting,
|
||||
GroupSetting,
|
||||
MapSetting,
|
||||
},
|
||||
computed: {
|
||||
...SharedComputedObject(),
|
||||
},
|
||||
}
|
||||
|
||||
export default FederationTab
|
||||
65
src/components/settings_modal/admin_tabs/federation_tab.vue
Normal file
65
src/components/settings_modal/admin_tabs/federation_tab.vue
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
<template>
|
||||
<div :label="$t('admin_dash.tabs.federation')">
|
||||
<div class="setting-section">
|
||||
<h3>{{ $t('admin_dash.federation.global') }}</h3>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<BooleanSetting path=":pleroma.:instance.:federating" />
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path=":pleroma.:instance.:skip_thread_containment" />
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path=":pleroma.:instance.:external_user_synchronization" />
|
||||
</li>
|
||||
</ul>
|
||||
<h3>{{ $t('admin_dash.federation.restrictions') }}</h3>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<MapSetting path=":pleroma.:instance.:quarantined_instances" />
|
||||
</li>
|
||||
<li>
|
||||
<MapSetting path=":pleroma.:instance.:rejected_instances" />
|
||||
</li>
|
||||
</ul>
|
||||
<h3>{{ $t('admin_dash.federation.activitypub') }}</h3>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<BooleanSetting path=":pleroma.:activitypub.:unfollow_blocked" />
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path=":pleroma.:activitypub.:outgoing_blocks" />
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path=":pleroma.:activitypub.:blockers_visible" />
|
||||
</li>
|
||||
<li>
|
||||
<IntegerSetting path=":pleroma.:activitypub.:follow_handshake_timeout" />
|
||||
</li>
|
||||
<li>
|
||||
<IntegerSetting path=":pleroma.:activitypub.:note_replies_output_limit" />
|
||||
</li>
|
||||
<li>
|
||||
<IntegerSetting path=":pleroma.:instance.:federation_incoming_replies_max_depth" />
|
||||
</li>
|
||||
<li>
|
||||
<IntegerSetting path=":pleroma.:instance.:federation_reachability_timeout_days" />
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path=":pleroma.:instance.:allow_relay" />
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path=":pleroma.:activitypub.:sign_object_fetches" />
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path=":pleroma.:activitypub.:authorized_fetch_mode" />
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path=":pleroma.:activitypub.:client_api_enabled" />
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./federation_tab.js"></script>
|
||||
|
|
@ -1,32 +1,29 @@
|
|||
import PanelLoading from 'src/components/panel_loading/panel_loading.vue'
|
||||
import Popover from 'src/components/popover/popover.vue'
|
||||
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 { useInterfaceStore } from 'src/stores/interface'
|
||||
|
||||
import IntegerSetting from '../helpers/integer_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'
|
||||
import StringSetting from '../helpers/string_setting.vue'
|
||||
|
||||
library.add(
|
||||
faGlobe
|
||||
)
|
||||
import { useInterfaceStore } from 'src/stores/interface.js'
|
||||
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { faGlobe } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(faGlobe)
|
||||
|
||||
const FrontendsTab = {
|
||||
provide () {
|
||||
provide() {
|
||||
return {
|
||||
defaultDraftMode: true,
|
||||
defaultSource: 'admin'
|
||||
defaultSource: 'admin',
|
||||
}
|
||||
},
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
working: false
|
||||
working: false,
|
||||
}
|
||||
},
|
||||
components: {
|
||||
|
|
@ -36,26 +33,26 @@ const FrontendsTab = {
|
|||
StringSetting,
|
||||
GroupSetting,
|
||||
PanelLoading,
|
||||
Popover
|
||||
Popover,
|
||||
},
|
||||
created () {
|
||||
created() {
|
||||
if (this.user.rights.admin) {
|
||||
this.$store.dispatch('loadFrontendsStuff')
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
frontends () {
|
||||
...SharedComputedObject(),
|
||||
frontends() {
|
||||
return this.$store.state.adminSettings.frontends
|
||||
},
|
||||
...SharedComputedObject()
|
||||
},
|
||||
methods: {
|
||||
canInstall (frontend) {
|
||||
const fe = this.frontends.find(f => f.name === frontend.name)
|
||||
canInstall(frontend) {
|
||||
const fe = this.frontends.find((f) => f.name === frontend.name)
|
||||
if (!fe) return false
|
||||
return fe.refs.includes(frontend.ref)
|
||||
},
|
||||
getSuggestedRef (frontend) {
|
||||
getSuggestedRef(frontend) {
|
||||
if (this.adminDraft) {
|
||||
const defaultFe = this.adminDraft[':pleroma'][':frontends'][':primary']
|
||||
if (defaultFe?.name === frontend.name && this.canInstall(defaultFe)) {
|
||||
|
|
@ -67,13 +64,14 @@ const FrontendsTab = {
|
|||
return frontend.refs[0]
|
||||
}
|
||||
},
|
||||
update (frontend, suggestRef) {
|
||||
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 })
|
||||
this.$store.state.api.backendInteractor
|
||||
.installFrontend({ payload })
|
||||
.finally(() => {
|
||||
this.working = false
|
||||
})
|
||||
|
|
@ -86,29 +84,32 @@ const FrontendsTab = {
|
|||
messageKey: 'admin_dash.frontend.failure_installing_frontend',
|
||||
messageArgs: {
|
||||
version: name + '/' + ref,
|
||||
reason: reason.error
|
||||
reason: reason.error,
|
||||
},
|
||||
timeout: 5000
|
||||
timeout: 5000,
|
||||
})
|
||||
} else {
|
||||
useInterfaceStore().pushGlobalNotice({
|
||||
level: 'success',
|
||||
messageKey: 'admin_dash.frontend.success_installing_frontend',
|
||||
messageArgs: {
|
||||
version: name + '/' + ref
|
||||
version: name + '/' + ref,
|
||||
},
|
||||
timeout: 2000
|
||||
timeout: 2000,
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
setDefault (frontend, suggestRef) {
|
||||
setDefault(frontend, suggestRef) {
|
||||
const ref = suggestRef || this.getSuggestedRef(frontend)
|
||||
const { name } = frontend
|
||||
|
||||
this.$store.commit('updateAdminDraft', { path: [':pleroma', ':frontends', ':primary'], value: { name, ref } })
|
||||
}
|
||||
}
|
||||
this.$store.commit('updateAdminDraft', {
|
||||
path: [':pleroma', ':frontends', ':primary'],
|
||||
value: { name, ref },
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default FrontendsTab
|
||||
|
|
|
|||
|
|
@ -1,8 +1,24 @@
|
|||
.frontends-tab {
|
||||
.FrontendsTab {
|
||||
.cards-list {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
li.frontend-card {
|
||||
display: flex;
|
||||
margin: 0;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.frontend-buttons {
|
||||
margin-top: 0.5em;
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
gap: 0.5em;
|
||||
flex-wrap: wrap;
|
||||
flex: 1 0 auto;
|
||||
align-items: end;
|
||||
}
|
||||
|
||||
.relative {
|
||||
position: relative;
|
||||
}
|
||||
|
|
@ -16,10 +32,26 @@
|
|||
inset: 0;
|
||||
}
|
||||
|
||||
h5 {
|
||||
margin: 0;
|
||||
font-size: 1.15em
|
||||
}
|
||||
|
||||
dl {
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
dt {
|
||||
margin-top: 0.5em;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
dd {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow-x: hidden;
|
||||
max-width: 10em;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
<template>
|
||||
<div
|
||||
class="frontends-tab"
|
||||
class="FrontendsTab"
|
||||
:label="$t('admin_dash.tabs.frontends')"
|
||||
>
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('admin_dash.tabs.frontends') }}</h2>
|
||||
<div class="setting-section">
|
||||
<h3>{{ $t('admin_dash.frontend.title') }}</h3>
|
||||
<p>{{ $t('admin_dash.frontend.wip_notice') }}</p>
|
||||
<ul
|
||||
v-if="adminDraft"
|
||||
class="setting-list"
|
||||
>
|
||||
<li>
|
||||
<h3>{{ $t('admin_dash.frontend.default_frontend') }}</h3>
|
||||
<h4>{{ $t('admin_dash.frontend.default_frontend') }}</h4>
|
||||
<p>{{ $t('admin_dash.frontend.default_frontend_tip') }}</p>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
|
|
@ -38,13 +38,14 @@
|
|||
v-if="working"
|
||||
class="overlay"
|
||||
/>
|
||||
<h3>{{ $t('admin_dash.frontend.available_frontends') }}</h3>
|
||||
<h4>{{ $t('admin_dash.frontend.available_frontends') }}</h4>
|
||||
<ul class="cards-list">
|
||||
<li
|
||||
v-for="frontend in frontends"
|
||||
:key="frontend.name"
|
||||
class="frontend-card"
|
||||
>
|
||||
<strong>{{ frontend.name }}</strong>
|
||||
<h5>{{ frontend.name }}</h5>
|
||||
{{ ' ' }}
|
||||
<span v-if="adminDraft && adminDraft[':pleroma'][':frontends'][':primary']?.name === frontend.name">
|
||||
<i18n-t
|
||||
|
|
@ -89,7 +90,7 @@
|
|||
>{{ frontend.build_url }}</a>
|
||||
</dd>
|
||||
</dl>
|
||||
<div>
|
||||
<div class="frontend-buttons">
|
||||
<span class="btn-group">
|
||||
<button
|
||||
class="button button-default btn"
|
||||
|
|
|
|||
51
src/components/settings_modal/admin_tabs/http_tab.js
Normal file
51
src/components/settings_modal/admin_tabs/http_tab.js
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import { get } from 'lodash'
|
||||
|
||||
import AttachmentSetting from '../helpers/attachment_setting.vue'
|
||||
import BooleanSetting from '../helpers/boolean_setting.vue'
|
||||
import ChoiceSetting from '../helpers/choice_setting.vue'
|
||||
import GroupSetting from '../helpers/group_setting.vue'
|
||||
import IntegerSetting from '../helpers/integer_setting.vue'
|
||||
import ListSetting from '../helpers/list_setting.vue'
|
||||
import MapSetting from '../helpers/map_setting.vue'
|
||||
import ProxySetting from '../helpers/proxy_setting.vue'
|
||||
import SharedComputedObject from '../helpers/shared_computed_object.js'
|
||||
import StringSetting from '../helpers/string_setting.vue'
|
||||
import TupleSetting from '../helpers/tuple_setting.vue'
|
||||
|
||||
const HTTPTab = {
|
||||
provide() {
|
||||
return {
|
||||
defaultDraftMode: true,
|
||||
defaultSource: 'admin',
|
||||
}
|
||||
},
|
||||
components: {
|
||||
BooleanSetting,
|
||||
ChoiceSetting,
|
||||
IntegerSetting,
|
||||
StringSetting,
|
||||
AttachmentSetting,
|
||||
MapSetting,
|
||||
GroupSetting,
|
||||
ListSetting,
|
||||
TupleSetting,
|
||||
ProxySetting,
|
||||
},
|
||||
computed: {
|
||||
...SharedComputedObject(),
|
||||
sslOptions() {
|
||||
const desc = get(
|
||||
this.$store.state.adminSettings.descriptions,
|
||||
':pleroma.:http.:adapter.:ssl_options.:versions',
|
||||
)
|
||||
return new Set(
|
||||
desc.suggestions.map((option) => ({
|
||||
label: option.replace(':tlsv', 'TLS v'),
|
||||
value: option,
|
||||
})),
|
||||
)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default HTTPTab
|
||||
86
src/components/settings_modal/admin_tabs/http_tab.vue
Normal file
86
src/components/settings_modal/admin_tabs/http_tab.vue
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
<template>
|
||||
<div
|
||||
class="LinksTab"
|
||||
:label="$t('admin_dash.tabs.http')"
|
||||
>
|
||||
<div class="setting-section">
|
||||
<h3>{{ $t('admin_dash.http.outbound') }}</h3>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<ProxySetting
|
||||
hide-description
|
||||
path=":pleroma.:http.:proxy_url"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path=":pleroma.:http.:send_user_agent" />
|
||||
</li>
|
||||
<li>
|
||||
<StringSetting path=":pleroma.:http.:user_agent" />
|
||||
</li>
|
||||
<li>
|
||||
<ListSetting
|
||||
override-available-options
|
||||
:options="sslOptions"
|
||||
path=":pleroma.:http.:adapter.:ssl_options.:versions"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<GroupSetting path=":pleroma.:http.:adapter" />
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="setting-section">
|
||||
<h3>{{ $t('admin_dash.http.incoming') }}</h3>
|
||||
<ul class="setting-list">
|
||||
<h4>{{ $t('admin_dash.http.security') }}</h4>
|
||||
<li>
|
||||
<BooleanSetting path=":pleroma.:http_security.:enabled" />
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path=":pleroma.:http_security.:sts" />
|
||||
</li>
|
||||
<li>
|
||||
<IntegerSetting path=":pleroma.:http_security.:sts_max_age" />
|
||||
</li>
|
||||
<li>
|
||||
<IntegerSetting path=":pleroma.:http_security.:ct_max_age" />
|
||||
</li>
|
||||
<li>
|
||||
<StringSetting path=":pleroma.:http_security.:referrer_policy" />
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path=":pleroma.:http_security.:allow_unsafe_eval" />
|
||||
</li>
|
||||
<li>
|
||||
<StringSetting path=":pleroma.:http_security.:report_url" />
|
||||
</li>
|
||||
</ul>
|
||||
<h3>{{ $t('admin_dash.http.web_push') }}</h3>
|
||||
<p>{{ $t('admin_dash.http.web_push_description') }}</p>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<StringSetting
|
||||
path=":web_push_encryption.:vapid_details.:subject"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<StringSetting
|
||||
path=":web_push_encryption.:vapid_details.:public_key"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<StringSetting
|
||||
path=":web_push_encryption.:vapid_details.:private_key"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
<!-- CONFIRM admin_token should go there but something is wrong with both data and description. -->
|
||||
<!-- given the nature of the setting it's probably better to not expose it and deprecate it on backend side -->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!--<style lang="scss" src="./http_tab.scss"></style>-->
|
||||
|
||||
<script src="./http_tab.js"></script>
|
||||
|
|
@ -1,25 +1,22 @@
|
|||
import { get } from 'lodash'
|
||||
|
||||
import AttachmentSetting from '../helpers/attachment_setting.vue'
|
||||
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 ColorSetting from '../helpers/color_setting.vue'
|
||||
import GroupSetting from '../helpers/group_setting.vue'
|
||||
import AttachmentSetting from '../helpers/attachment_setting.vue'
|
||||
|
||||
import IntegerSetting from '../helpers/integer_setting.vue'
|
||||
import ListSetting from '../helpers/list_setting.vue'
|
||||
import MapSetting from '../helpers/map_setting.vue'
|
||||
import PWAManifestIconsSetting from '../helpers/pwa_manifest_icons_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
|
||||
)
|
||||
import StringSetting from '../helpers/string_setting.vue'
|
||||
|
||||
const InstanceTab = {
|
||||
provide () {
|
||||
provide() {
|
||||
return {
|
||||
defaultDraftMode: true,
|
||||
defaultSource: 'admin'
|
||||
defaultSource: 'admin',
|
||||
}
|
||||
},
|
||||
components: {
|
||||
|
|
@ -27,12 +24,45 @@ const InstanceTab = {
|
|||
ChoiceSetting,
|
||||
IntegerSetting,
|
||||
StringSetting,
|
||||
ColorSetting,
|
||||
AttachmentSetting,
|
||||
GroupSetting
|
||||
ListSetting,
|
||||
PWAManifestIconsSetting,
|
||||
MapSetting,
|
||||
GroupSetting,
|
||||
},
|
||||
computed: {
|
||||
...SharedComputedObject()
|
||||
}
|
||||
...SharedComputedObject(),
|
||||
providersOptions() {
|
||||
const desc = get(this.$store.state.adminSettings.descriptions, [
|
||||
':pleroma',
|
||||
'Pleroma.Web.Metadata',
|
||||
':providers',
|
||||
])
|
||||
return new Set(
|
||||
desc.suggestions.map((option) => ({
|
||||
label: option.replace('Pleroma.Web.Metadata.Providers.', ''),
|
||||
value: option,
|
||||
})),
|
||||
)
|
||||
},
|
||||
limitLocalContentOptions() {
|
||||
const desc = get(this.$store.state.adminSettings.descriptions, [
|
||||
':pleroma',
|
||||
':instance',
|
||||
':limit_to_local_content',
|
||||
])
|
||||
return new Set(
|
||||
desc.suggestions.map((option) => ({
|
||||
label:
|
||||
option !== 'false'
|
||||
? this.$t('admin_dash.instance.' + option)
|
||||
: this.$t('general.no'),
|
||||
value: option,
|
||||
})),
|
||||
)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default InstanceTab
|
||||
|
|
|
|||
|
|
@ -1,17 +1,13 @@
|
|||
<template>
|
||||
<div :label="$t('admin_dash.tabs.instance')">
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('admin_dash.instance.instance') }}</h2>
|
||||
<div class="setting-section">
|
||||
<h3>{{ $t('admin_dash.instance.instance') }}</h3>
|
||||
<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>
|
||||
<StringSetting path=":pleroma.:instance.:contact_username" />
|
||||
</li>
|
||||
<li>
|
||||
<StringSetting path=":pleroma.:instance.:email" />
|
||||
|
|
@ -22,87 +18,75 @@
|
|||
<li>
|
||||
<StringSetting path=":pleroma.:instance.:short_description" />
|
||||
</li>
|
||||
<li>
|
||||
<ListSetting
|
||||
force-new
|
||||
ignore-suggestions
|
||||
path=":pleroma.:instance.:languages"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<StringSetting path=":pleroma.:instance.:status_page" />
|
||||
</li>
|
||||
</ul>
|
||||
<h3>{{ $t('admin_dash.instance.branding') }}</h3>
|
||||
<ul class="setting-list">
|
||||
<!-- 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>
|
||||
<AttachmentSetting
|
||||
compact
|
||||
path=":pleroma.:instance.:instance_thumbnail"
|
||||
/>
|
||||
</li>
|
||||
<h4>{{ $t('admin_dash.instance.pwa.manifest') }}</H4>
|
||||
<li>
|
||||
<PWAManifestIconsSetting path=":pleroma.:manifest.:icons" />
|
||||
</li>
|
||||
<li>
|
||||
<ColorSetting
|
||||
hide-draft-buttons
|
||||
label=""
|
||||
path=":pleroma.:manifest.:theme_color"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<ColorSetting
|
||||
hide-draft-buttons
|
||||
path=":pleroma.:manifest.:background_color"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<GroupSetting path=":pleroma.:manifest" />
|
||||
</li>
|
||||
<h4>{{ $t('admin_dash.instance.misc_brand') }}</H4>
|
||||
<li>
|
||||
<AttachmentSetting path=":pleroma.:instance.:background_image" />
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('admin_dash.instance.registrations') }}</h2>
|
||||
<h3>{{ $t('admin_dash.instance.rich_metadata') }}</h3>
|
||||
<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>
|
||||
<ListSetting
|
||||
override-available-options
|
||||
:options="providersOptions"
|
||||
:path="[':pleroma','Pleroma.Web.Metadata', ':providers']"
|
||||
/>
|
||||
</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>
|
||||
<BooleanSetting
|
||||
:path="[':pleroma','Pleroma.Web.Metadata', ':unfurl_nsfw']"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('admin_dash.instance.access') }}</h2>
|
||||
<div class="setting-section">
|
||||
<h3>{{ $t('admin_dash.instance.access') }}</h3>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<BooleanSetting
|
||||
|
|
@ -115,6 +99,8 @@
|
|||
<ChoiceSetting
|
||||
override-backend-description
|
||||
override-backend-description-label
|
||||
override-available-options
|
||||
:options="[...limitLocalContentOptions]"
|
||||
path=":pleroma.:instance.:limit_to_local_content"
|
||||
/>
|
||||
</li>
|
||||
|
|
|
|||
33
src/components/settings_modal/admin_tabs/job_queues_tab.js
Normal file
33
src/components/settings_modal/admin_tabs/job_queues_tab.js
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import AttachmentSetting from '../helpers/attachment_setting.vue'
|
||||
import BooleanSetting from '../helpers/boolean_setting.vue'
|
||||
import ChoiceSetting from '../helpers/choice_setting.vue'
|
||||
import GroupSetting from '../helpers/group_setting.vue'
|
||||
import IntegerSetting from '../helpers/integer_setting.vue'
|
||||
import ListSetting from '../helpers/list_setting.vue'
|
||||
import SharedComputedObject from '../helpers/shared_computed_object.js'
|
||||
import StringSetting from '../helpers/string_setting.vue'
|
||||
import TupleSetting from '../helpers/tuple_setting.vue'
|
||||
|
||||
const JobQueuesTab = {
|
||||
provide() {
|
||||
return {
|
||||
defaultDraftMode: true,
|
||||
defaultSource: 'admin',
|
||||
}
|
||||
},
|
||||
components: {
|
||||
BooleanSetting,
|
||||
ChoiceSetting,
|
||||
IntegerSetting,
|
||||
StringSetting,
|
||||
TupleSetting,
|
||||
AttachmentSetting,
|
||||
GroupSetting,
|
||||
ListSetting,
|
||||
},
|
||||
computed: {
|
||||
...SharedComputedObject(),
|
||||
},
|
||||
}
|
||||
|
||||
export default JobQueuesTab
|
||||
157
src/components/settings_modal/admin_tabs/job_queues_tab.vue
Normal file
157
src/components/settings_modal/admin_tabs/job_queues_tab.vue
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
<template>
|
||||
<div :label="$t('admin_dash.tabs.job_queues')">
|
||||
<div class="setting-section">
|
||||
<h3>{{ $t('admin_dash.job_queues.Gun.title') }}</h3>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<h4>{{ $t('admin_dash.job_queues.Gun.connections_pools') }}</h4>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<IntegerSetting path=":pleroma.:connections_pool.:connect_timeout" />
|
||||
</li>
|
||||
<li>
|
||||
<IntegerSetting path=":pleroma.:connections_pool.:connection_acquisition_retries" />
|
||||
</li>
|
||||
<li>
|
||||
<IntegerSetting path=":pleroma.:connections_pool.:connection_acquisition_wait" />
|
||||
</li>
|
||||
<li>
|
||||
<!-- CONFIRM what is this -->
|
||||
<IntegerSetting path=":pleroma.:connections_pool.:retry" />
|
||||
</li>
|
||||
<li>
|
||||
<IntegerSetting path=":pleroma.:connections_pool.:max_connections" />
|
||||
</li>
|
||||
<li>
|
||||
<!-- CONFIRM what is this -->
|
||||
<IntegerSetting path=":pleroma.:connections_pool.:max_idle_time" />
|
||||
</li>
|
||||
<li>
|
||||
<IntegerSetting path=":pleroma.:connections_pool.:reclaim_multiplier" />
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<h4>{{ $t('admin_dash.job_queues.Gun.pools.title') }}</h4>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<h5>{{ $t('admin_dash.job_queues.Gun.pools.default') }}</h5>
|
||||
<ul class="setting-list suboptions">
|
||||
<li>
|
||||
<IntegerSetting path=":pleroma.:pools.:default.:size" />
|
||||
</li>
|
||||
<li>
|
||||
<IntegerSetting path=":pleroma.:pools.:default.:max_waiting" />
|
||||
</li>
|
||||
<li>
|
||||
<IntegerSetting path=":pleroma.:pools.:default.:recv_timeout" />
|
||||
</li>
|
||||
</ul>
|
||||
<GroupSetting path=":pleroma.:pools.:default" />
|
||||
</li>
|
||||
<li>
|
||||
<h5>{{ $t('admin_dash.job_queues.Gun.pools.federation') }}</h5>
|
||||
<ul class="setting-list suboptions">
|
||||
<li>
|
||||
<IntegerSetting path=":pleroma.:pools.:federation.:size" />
|
||||
</li>
|
||||
<li>
|
||||
<IntegerSetting path=":pleroma.:pools.:federation.:max_waiting" />
|
||||
</li>
|
||||
<li>
|
||||
<IntegerSetting path=":pleroma.:pools.:federation.:recv_timeout" />
|
||||
</li>
|
||||
</ul>
|
||||
<GroupSetting path=":pleroma.:pools.:federation" />
|
||||
</li>
|
||||
<li>
|
||||
<!-- CONFIRM what is this? -->
|
||||
<h5>{{ $t('admin_dash.job_queues.Gun.pools.rich_media') }}</h5>
|
||||
<ul class="setting-list suboptions">
|
||||
<li>
|
||||
<IntegerSetting path=":pleroma.:pools.:rich_media.:size" />
|
||||
</li>
|
||||
<li>
|
||||
<IntegerSetting path=":pleroma.:pools.:rich_media.:max_waiting" />
|
||||
</li>
|
||||
<li>
|
||||
<IntegerSetting path=":pleroma.:pools.:rich_media.:recv_timeout" />
|
||||
</li>
|
||||
</ul>
|
||||
<GroupSetting path=":pleroma.:pools.:rich_media" />
|
||||
</li>
|
||||
<li>
|
||||
<h5>{{ $t('admin_dash.job_queues.Gun.pools.media') }}</h5>
|
||||
<ul class="setting-list suboptions">
|
||||
<li>
|
||||
<IntegerSetting path=":pleroma.:pools.:media.:size" />
|
||||
</li>
|
||||
<li>
|
||||
<IntegerSetting path=":pleroma.:pools.:media.:max_waiting" />
|
||||
</li>
|
||||
<li>
|
||||
<IntegerSetting path=":pleroma.:pools.:media.:recv_timeout" />
|
||||
</li>
|
||||
</ul>
|
||||
<GroupSetting path=":pleroma.:pools.:media" />
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<h3>{{ $t('admin_dash.job_queues.Hackney.title') }}</h3>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<h4>{{ $t('admin_dash.job_queues.Hackney.federation') }}</h4>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<IntegerSetting path=":pleroma.:hackney_pools.:federation.:max_connections" />
|
||||
</li>
|
||||
<li>
|
||||
<IntegerSetting path=":pleroma.:hackney_pools.:federation.:timeout" />
|
||||
</li>
|
||||
</ul>
|
||||
<GroupSetting path=":pleroma.:hackney_pools.:federation" />
|
||||
</li>
|
||||
<li>
|
||||
<h4>{{ $t('admin_dash.job_queues.Hackney.media') }}</h4>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<IntegerSetting path=":pleroma.:hackney_pools.:media.:max_connections" />
|
||||
</li>
|
||||
<li>
|
||||
<IntegerSetting path=":pleroma.:hackney_pools.:media.:timeout" />
|
||||
</li>
|
||||
</ul>
|
||||
<GroupSetting path=":pleroma.:hackney_pools.:media" />
|
||||
</li>
|
||||
<li>
|
||||
<!-- CONFIRM what is this -->
|
||||
<h4>{{ $t('admin_dash.job_queues.Hackney.rich_media') }}</h4>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<IntegerSetting path=":pleroma.:hackney_pools.:rich_media.:max_connections" />
|
||||
</li>
|
||||
<li>
|
||||
<IntegerSetting path=":pleroma.:hackney_pools.:rich_media.:timeout" />
|
||||
</li>
|
||||
</ul>
|
||||
<GroupSetting path=":pleroma.:hackney_pools.:rich_media" />
|
||||
</li>
|
||||
<li>
|
||||
<h4>{{ $t('admin_dash.job_queues.Hackney.upload') }}</h4>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<IntegerSetting path=":pleroma.:hackney_pools.:upload.:max_connections" />
|
||||
</li>
|
||||
<li>
|
||||
<IntegerSetting path=":pleroma.:hackney_pools.:upload.:timeout" />
|
||||
</li>
|
||||
</ul>
|
||||
<GroupSetting path=":pleroma.:hackney_pools.:upload" />
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./job_queues_tab.js"></script>
|
||||
|
|
@ -1,28 +1,19 @@
|
|||
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
|
||||
)
|
||||
import StringSetting from '../helpers/string_setting.vue'
|
||||
|
||||
const LimitsTab = {
|
||||
components: {
|
||||
BooleanSetting,
|
||||
ChoiceSetting,
|
||||
IntegerSetting,
|
||||
StringSetting
|
||||
StringSetting,
|
||||
},
|
||||
computed: {
|
||||
...SharedComputedObject()
|
||||
}
|
||||
...SharedComputedObject(),
|
||||
},
|
||||
}
|
||||
|
||||
export default LimitsTab
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
<template>
|
||||
<div :label="$t('admin_dash.tabs.limits')">
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('admin_dash.limits.arbitrary_limits') }}</h2>
|
||||
<div class="setting-section">
|
||||
<h3>{{ $t('admin_dash.limits.arbitrary_limits') }}</h3>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<h3>{{ $t('admin_dash.limits.posts') }}</h3>
|
||||
<h4>{{ $t('admin_dash.limits.posts') }}</h4>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<IntegerSetting
|
||||
|
|
@ -24,7 +24,7 @@
|
|||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<h3>{{ $t('admin_dash.limits.uploads') }}</h3>
|
||||
<h4>{{ $t('admin_dash.limits.uploads') }}</h4>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<IntegerSetting
|
||||
|
|
@ -50,7 +50,7 @@
|
|||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<h3>{{ $t('admin_dash.limits.users') }}</h3>
|
||||
<h4>{{ $t('admin_dash.limits.users') }}</h4>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<IntegerSetting
|
||||
|
|
@ -74,7 +74,7 @@
|
|||
/>
|
||||
</li>
|
||||
<li>
|
||||
<h4>{{ $t('admin_dash.limits.profile_fields') }}</h4>
|
||||
<h5>{{ $t('admin_dash.limits.profile_fields') }}</h5>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<IntegerSetting
|
||||
|
|
@ -108,7 +108,7 @@
|
|||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<h4>{{ $t('admin_dash.limits.user_uploads') }}</h4>
|
||||
<h5>{{ $t('admin_dash.limits.user_uploads') }}</h5>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<IntegerSetting
|
||||
|
|
@ -128,6 +128,25 @@
|
|||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<h4>{{ $t('admin_dash.limits.other') }}</h4>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<IntegerSetting
|
||||
source="admin"
|
||||
path=":pleroma.:instance.:max_report_comment_size"
|
||||
draft-mode
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<IntegerSetting
|
||||
source="admin"
|
||||
path=":pleroma.:instance.:max_endorsed_users"
|
||||
draft-mode
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
133
src/components/settings_modal/admin_tabs/links_tab.js
Normal file
133
src/components/settings_modal/admin_tabs/links_tab.js
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
import { get } from 'lodash'
|
||||
|
||||
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||
import AttachmentSetting from '../helpers/attachment_setting.vue'
|
||||
import BooleanSetting from '../helpers/boolean_setting.vue'
|
||||
import ChoiceSetting from '../helpers/choice_setting.vue'
|
||||
import GroupSetting from '../helpers/group_setting.vue'
|
||||
import IntegerSetting from '../helpers/integer_setting.vue'
|
||||
import ListSetting from '../helpers/list_setting.vue'
|
||||
import SharedComputedObject from '../helpers/shared_computed_object.js'
|
||||
import StringSetting from '../helpers/string_setting.vue'
|
||||
|
||||
const LinksTab = {
|
||||
provide() {
|
||||
return {
|
||||
defaultDraftMode: true,
|
||||
defaultSource: 'admin',
|
||||
}
|
||||
},
|
||||
components: {
|
||||
BooleanSetting,
|
||||
ChoiceSetting,
|
||||
IntegerSetting,
|
||||
StringSetting,
|
||||
AttachmentSetting,
|
||||
GroupSetting,
|
||||
ListSetting,
|
||||
Checkbox,
|
||||
},
|
||||
computed: {
|
||||
classIsPresent() {
|
||||
return (
|
||||
this.$store.state.adminSettings.draft[':pleroma']['Pleroma.Formatter'][
|
||||
':class'
|
||||
] !== false
|
||||
)
|
||||
},
|
||||
relIsPresent() {
|
||||
return (
|
||||
this.$store.state.adminSettings.draft[':pleroma']['Pleroma.Formatter'][
|
||||
':rel'
|
||||
] !== false
|
||||
)
|
||||
},
|
||||
truncateIsPresent() {
|
||||
return (
|
||||
this.$store.state.adminSettings.draft[':pleroma']['Pleroma.Formatter'][
|
||||
':truncate'
|
||||
] !== false
|
||||
)
|
||||
},
|
||||
truncateDescription() {
|
||||
return get(this.$store.state.adminSettings.descriptions, [
|
||||
':pleroma',
|
||||
'Pleroma.Formatter',
|
||||
':truncate',
|
||||
])
|
||||
},
|
||||
ttlSettersOptions() {
|
||||
const desc = get(
|
||||
this.$store.state.adminSettings.descriptions,
|
||||
':pleroma.:rich_media.:ttl_setters',
|
||||
)
|
||||
return new Set(
|
||||
desc.suggestions.map((option) => ({
|
||||
label: option.replace('Pleroma.Web.RichMedia.Parser.TTL.', ''),
|
||||
value: option,
|
||||
})),
|
||||
)
|
||||
},
|
||||
parsersOptions() {
|
||||
const desc = get(
|
||||
this.$store.state.adminSettings.descriptions,
|
||||
':pleroma.:rich_media.:parsers',
|
||||
)
|
||||
return new Set(
|
||||
desc.suggestions.map((option) => ({
|
||||
label: option.replace('Pleroma.Web.RichMedia.Parsers.', ''),
|
||||
value: option,
|
||||
})),
|
||||
)
|
||||
},
|
||||
validateTLDOptions() {
|
||||
return [
|
||||
{
|
||||
label: this.$t('general.yes'),
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
label: this.$t('general.no'),
|
||||
value: false,
|
||||
},
|
||||
{
|
||||
label: this.$t('admin_dash.links.no_scheme'),
|
||||
value: ':no_scheme',
|
||||
},
|
||||
]
|
||||
},
|
||||
mediaProxyEnabled() {
|
||||
return this.$store.state.adminSettings.draft[':pleroma'][':media_proxy'][
|
||||
':enabled'
|
||||
]
|
||||
},
|
||||
mediaInvalidationProvider() {
|
||||
return this.$store.state.adminSettings.draft[':pleroma'][':media_proxy'][
|
||||
':invalidation'
|
||||
][':provider']
|
||||
},
|
||||
...SharedComputedObject(),
|
||||
},
|
||||
methods: {
|
||||
checkRel(e) {
|
||||
this.$store.commit('updateAdminDraft', {
|
||||
path: [':pleroma', 'Pleroma.Formatter', ':rel'],
|
||||
value: e ? '' : false,
|
||||
})
|
||||
},
|
||||
checkClass(e) {
|
||||
this.$store.commit('updateAdminDraft', {
|
||||
path: [':pleroma', 'Pleroma.Formatter', ':class'],
|
||||
value: e ? '' : false,
|
||||
})
|
||||
},
|
||||
checkTruncate(e) {
|
||||
this.$store.commit('updateAdminDraft', {
|
||||
path: [':pleroma', 'Pleroma.Formatter', ':truncate'],
|
||||
value: e ? 20 : false,
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default LinksTab
|
||||
45
src/components/settings_modal/admin_tabs/links_tab.vue
Normal file
45
src/components/settings_modal/admin_tabs/links_tab.vue
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
<template>
|
||||
<div
|
||||
class="LinksTab"
|
||||
:label="$t('admin_dash.tabs.media_proxy')"
|
||||
>
|
||||
<div class="setting-section">
|
||||
<h3>{{ $t('admin_dash.links.link_previews') }}</h3>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<BooleanSetting path=":pleroma.:rich_media.:enabled" />
|
||||
</li>
|
||||
<li>
|
||||
<ListSetting
|
||||
override-available-options
|
||||
:options="parsersOptions"
|
||||
path=":pleroma.:rich_media.:parsers"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<IntegerSetting
|
||||
path=":pleroma.:rich_media.:timeout"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<ListSetting
|
||||
override-available-options
|
||||
:options="ttlSettersOptions"
|
||||
path=":pleroma.:rich_media.:ttl_setters"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<ListSetting
|
||||
path=":pleroma.:rich_media.:ignore_tld"
|
||||
ignore-suggestions
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<ListSetting path=":pleroma.:rich_media.:ignore_hosts" />
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./links_tab.js"></script>
|
||||
70
src/components/settings_modal/admin_tabs/mailer_tab.js
Normal file
70
src/components/settings_modal/admin_tabs/mailer_tab.js
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
import AttachmentSetting from '../helpers/attachment_setting.vue'
|
||||
import BooleanSetting from '../helpers/boolean_setting.vue'
|
||||
import ChoiceSetting from '../helpers/choice_setting.vue'
|
||||
import ColorSetting from '../helpers/color_setting.vue'
|
||||
import GroupSetting from '../helpers/group_setting.vue'
|
||||
import IntegerSetting from '../helpers/integer_setting.vue'
|
||||
import SharedComputedObject from '../helpers/shared_computed_object.js'
|
||||
import StringSetting from '../helpers/string_setting.vue'
|
||||
|
||||
const MailerTab = {
|
||||
provide() {
|
||||
return {
|
||||
defaultDraftMode: true,
|
||||
defaultSource: 'admin',
|
||||
}
|
||||
},
|
||||
components: {
|
||||
BooleanSetting,
|
||||
ChoiceSetting,
|
||||
IntegerSetting,
|
||||
StringSetting,
|
||||
AttachmentSetting,
|
||||
ColorSetting,
|
||||
GroupSetting,
|
||||
},
|
||||
computed: {
|
||||
adaptersLabels() {
|
||||
const prefix = 'Swoosh.Adapters.'
|
||||
const descriptions = this.$store.state.adminSettings.descriptions
|
||||
const options =
|
||||
descriptions[':pleroma']['Pleroma.Emails.Mailer'][':adapter']
|
||||
.suggestions
|
||||
|
||||
return Object.fromEntries(
|
||||
options.map((value) => [value, value.replace(prefix, '')]),
|
||||
)
|
||||
},
|
||||
startTLSLabels() {
|
||||
return {
|
||||
':always': this.$t('admin_dash.generic_enforcement.always'),
|
||||
':if_available': this.$t('admin_dash.generic_enforcement.if_available'),
|
||||
':never': this.$t('admin_dash.generic_enforcement.never'),
|
||||
}
|
||||
// return Object.fromEntries(options.map(value => [
|
||||
// value, value.replace(prefix, '')
|
||||
// ]))
|
||||
},
|
||||
adapter() {
|
||||
return this.$store.state.adminSettings.draft[':pleroma'][
|
||||
'Pleroma.Emails.Mailer'
|
||||
][':adapter']
|
||||
},
|
||||
mailerEnabled() {
|
||||
return this.$store.state.adminSettings.draft[':pleroma'][
|
||||
'Pleroma.Emails.Mailer'
|
||||
][':enabled']
|
||||
},
|
||||
...SharedComputedObject(),
|
||||
},
|
||||
methods: {
|
||||
adapterHasKey(key) {
|
||||
const descriptions = this.$store.state.adminSettings.descriptions
|
||||
const mailerStuff = descriptions[':pleroma']['Pleroma.Emails.Mailer']
|
||||
const adapterStuff = mailerStuff[':subgroup,' + this.adapter]
|
||||
return Object.hasOwn(adapterStuff, key)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default MailerTab
|
||||
179
src/components/settings_modal/admin_tabs/mailer_tab.vue
Normal file
179
src/components/settings_modal/admin_tabs/mailer_tab.vue
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
<template>
|
||||
<div :label="$t('admin_dash.tabs.mailer')">
|
||||
<div class="setting-section">
|
||||
<h3>{{ $t('admin_dash.mailer.styling') }}</h3>
|
||||
<ul class="setting-list">
|
||||
<h4>{{ $t('admin_dash.mailer.assets') }}</h4>
|
||||
<li>
|
||||
<StringSetting :path="[':pleroma','Pleroma.Emails.UserEmail', ':logo']" />
|
||||
</li>
|
||||
<h4>{{ $t('admin_dash.mailer.colors') }}</h4>
|
||||
<li>
|
||||
<ColorSetting :path="[':pleroma','Pleroma.Emails.UserEmail', ':styling', ':background_color']" />
|
||||
</li>
|
||||
<li>
|
||||
<ColorSetting :path="[':pleroma','Pleroma.Emails.UserEmail', ':styling', ':content_background_color']" />
|
||||
</li>
|
||||
<li>
|
||||
<ColorSetting :path="[':pleroma','Pleroma.Emails.UserEmail', ':styling', ':header_color']" />
|
||||
</li>
|
||||
<li>
|
||||
<ColorSetting :path="[':pleroma','Pleroma.Emails.UserEmail', ':styling', ':text_color']" />
|
||||
</li>
|
||||
<li>
|
||||
<ColorSetting :path="[':pleroma','Pleroma.Emails.UserEmail', ':styling', ':link_color']" />
|
||||
</li>
|
||||
<li>
|
||||
<ColorSetting :path="[':pleroma','Pleroma.Emails.UserEmail', ':styling', ':text_muted_color']" />
|
||||
</li>
|
||||
<li>
|
||||
<GroupSetting :path="[':pleroma','Pleroma.Emails.UserEmail', ':styling']" />
|
||||
</li>
|
||||
</ul>
|
||||
<h3>{{ $t('admin_dash.mailer.adapter') }}</h3>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<BooleanSetting :path="[':pleroma','Pleroma.Emails.Mailer',':enabled']" />
|
||||
</li>
|
||||
<template v-if="mailerEnabled">
|
||||
<li>
|
||||
<ChoiceSetting
|
||||
:path="[':pleroma','Pleroma.Emails.Mailer',':adapter']"
|
||||
:option-label-map="adaptersLabels"
|
||||
/>
|
||||
<h4>{{ $t('admin_dash.mailer.auth') }}</h4>
|
||||
<ul class="setting-list suboptions">
|
||||
<li v-if="adapterHasKey(':api_key')">
|
||||
<!-- authentication info -->
|
||||
<StringSetting
|
||||
:path="[':pleroma','Pleroma.Emails.Mailer',':api_key']"
|
||||
:password="true"
|
||||
:subgroup="adapter"
|
||||
/>
|
||||
</li>
|
||||
<li v-if="adapterHasKey(':access_key')">
|
||||
<StringSetting
|
||||
:path="[':pleroma','Pleroma.Emails.Mailer',':access_key']"
|
||||
:password="true"
|
||||
:subgroup="adapter"
|
||||
/>
|
||||
</li>
|
||||
<li v-if="adapterHasKey(':access_token')">
|
||||
<StringSetting
|
||||
:path="[':pleroma','Pleroma.Emails.Mailer',':access_token']"
|
||||
:password="true"
|
||||
:subgroup="adapter"
|
||||
/>
|
||||
</li>
|
||||
<li v-if="adapterHasKey(':username')">
|
||||
<StringSetting
|
||||
:path="[':pleroma','Pleroma.Emails.Mailer',':username']"
|
||||
:subgroup="adapter"
|
||||
/>
|
||||
</li>
|
||||
<li v-if="adapterHasKey(':password')">
|
||||
<StringSetting
|
||||
:path="[':pleroma','Pleroma.Emails.Mailer',':password']"
|
||||
:password="true"
|
||||
:subgroup="adapter"
|
||||
/>
|
||||
</li>
|
||||
<li v-if="adapterHasKey(':secret')">
|
||||
<StringSetting
|
||||
:path="[':pleroma','Pleroma.Emails.Mailer',':secret']"
|
||||
:subgroup="adapter"
|
||||
/>
|
||||
</li>
|
||||
<li v-if="adapterHasKey(':auth')">
|
||||
<ChoiceSetting
|
||||
:path="[':pleroma','Pleroma.Emails.Mailer',':auth']"
|
||||
:password="true"
|
||||
:subgroup="adapter"
|
||||
/>
|
||||
</li>
|
||||
|
||||
<!-- server info -->
|
||||
<li v-if="adapterHasKey(':relay')">
|
||||
<StringSetting
|
||||
:path="[':pleroma','Pleroma.Emails.Mailer',':relay']"
|
||||
:password="true"
|
||||
:subgroup="adapter"
|
||||
/>
|
||||
</li>
|
||||
<li v-if="adapterHasKey(':ssl')">
|
||||
<BooleanSetting
|
||||
:path="[':pleroma','Pleroma.Emails.Mailer',':ssl']"
|
||||
:password="true"
|
||||
:subgroup="adapter"
|
||||
/>
|
||||
</li>
|
||||
<li v-if="adapterHasKey(':tls')">
|
||||
<ChoiceSetting
|
||||
:path="[':pleroma','Pleroma.Emails.Mailer',':tls']"
|
||||
:option-label-map="startTLSLabels"
|
||||
:password="true"
|
||||
:subgroup="adapter"
|
||||
/>
|
||||
</li>
|
||||
<li v-if="adapterHasKey(':port')">
|
||||
<IntegerSetting
|
||||
:path="[':pleroma','Pleroma.Emails.Mailer',':port']"
|
||||
:subgroup="adapter"
|
||||
/>
|
||||
</li>
|
||||
<li v-if="adapterHasKey(':server_id')">
|
||||
<StringSetting
|
||||
:path="[':pleroma','Pleroma.Emails.Mailer',':server_id']"
|
||||
:subgroup="adapter"
|
||||
/>
|
||||
</li>
|
||||
<li v-if="adapterHasKey(':region')">
|
||||
<StringSetting
|
||||
:path="[':pleroma','Pleroma.Emails.Mailer',':region']"
|
||||
:subgroup="adapter"
|
||||
/>
|
||||
</li>
|
||||
<li v-if="adapterHasKey(':domain')">
|
||||
<StringSetting
|
||||
:path="[':pleroma','Pleroma.Emails.Mailer',':domain']"
|
||||
:subgroup="adapter"
|
||||
/>
|
||||
</li>
|
||||
|
||||
<!-- sendmail exclusive -->
|
||||
<li v-if="adapterHasKey(':cmd_path')">
|
||||
<StringSetting
|
||||
:path="[':pleroma','Pleroma.Emails.Mailer',':cmd_path']"
|
||||
:password="true"
|
||||
:subgroup="adapter"
|
||||
/>
|
||||
</li>
|
||||
<li v-if="adapterHasKey(':cmd_args')">
|
||||
<StringSetting
|
||||
:path="[':pleroma','Pleroma.Emails.Mailer',':cmd_args']"
|
||||
:password="true"
|
||||
:subgroup="adapter"
|
||||
/>
|
||||
</li>
|
||||
<li v-if="adapterHasKey(':qmail')">
|
||||
<BooleanSetting
|
||||
:path="[':pleroma','Pleroma.Emails.Mailer',':qmail']"
|
||||
:password="true"
|
||||
:subgroup="adapter"
|
||||
/>
|
||||
</li>
|
||||
<li v-if="adapterHasKey(':retries')">
|
||||
<IntegerSetting
|
||||
:path="[':pleroma','Pleroma.Emails.Mailer',':retries']"
|
||||
:subgroup="adapter"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./mailer_tab.js"></script>
|
||||
41
src/components/settings_modal/admin_tabs/media_proxy_tab.js
Normal file
41
src/components/settings_modal/admin_tabs/media_proxy_tab.js
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import AttachmentSetting from '../helpers/attachment_setting.vue'
|
||||
import BooleanSetting from '../helpers/boolean_setting.vue'
|
||||
import ChoiceSetting from '../helpers/choice_setting.vue'
|
||||
import GroupSetting from '../helpers/group_setting.vue'
|
||||
import IntegerSetting from '../helpers/integer_setting.vue'
|
||||
import ListSetting from '../helpers/list_setting.vue'
|
||||
import SharedComputedObject from '../helpers/shared_computed_object.js'
|
||||
import StringSetting from '../helpers/string_setting.vue'
|
||||
|
||||
const MediaProxyTab = {
|
||||
provide() {
|
||||
return {
|
||||
defaultDraftMode: true,
|
||||
defaultSource: 'admin',
|
||||
}
|
||||
},
|
||||
components: {
|
||||
BooleanSetting,
|
||||
ChoiceSetting,
|
||||
IntegerSetting,
|
||||
StringSetting,
|
||||
AttachmentSetting,
|
||||
GroupSetting,
|
||||
ListSetting,
|
||||
},
|
||||
computed: {
|
||||
mediaProxyEnabled() {
|
||||
return this.$store.state.adminSettings.draft[':pleroma'][':media_proxy'][
|
||||
':enabled'
|
||||
]
|
||||
},
|
||||
mediaInvalidationProvider() {
|
||||
return this.$store.state.adminSettings.draft[':pleroma'][':media_proxy'][
|
||||
':invalidation'
|
||||
][':provider']
|
||||
},
|
||||
...SharedComputedObject(),
|
||||
},
|
||||
}
|
||||
|
||||
export default MediaProxyTab
|
||||
135
src/components/settings_modal/admin_tabs/media_proxy_tab.vue
Normal file
135
src/components/settings_modal/admin_tabs/media_proxy_tab.vue
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
<template>
|
||||
<div :label="$t('admin_dash.tabs.media_proxy')">
|
||||
<div class="setting-section">
|
||||
<h3>{{ $t('admin_dash.media_proxy.basic') }}</h3>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<BooleanSetting path=":pleroma.:media_proxy.:enabled" />
|
||||
<ul
|
||||
v-if="mediaProxyEnabled"
|
||||
class="setting-list suboptions"
|
||||
>
|
||||
<li>
|
||||
<StringSetting path=":pleroma.:media_proxy.:base_url" />
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path=":pleroma.:media_proxy.:proxy_opts.:redirect_on_failure" />
|
||||
</li>
|
||||
<li>
|
||||
<ListSetting
|
||||
ignore-suggestions
|
||||
path=":pleroma.:media_proxy.:whitelist"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<template v-if="mediaProxyEnabled">
|
||||
<h3>{{ $t('admin_dash.media_proxy.invalidation') }}</h3>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<BooleanSetting path=":pleroma.:media_proxy.:invalidation.:enabled" />
|
||||
<ul class="setting-list suboptions">
|
||||
<li>
|
||||
<ChoiceSetting
|
||||
path=":pleroma.:media_proxy.:invalidation.:provider"
|
||||
parent-path=":pleroma.:media_proxy.:invalidation.:enabled"
|
||||
/>
|
||||
</li>
|
||||
<h4>{{ $t('admin_dash.media_proxy.invalidation_settings') }}</h4>
|
||||
<ul class="setting-list suboptions">
|
||||
<template v-if="mediaInvalidationProvider === 'Pleroma.Web.MediaProxy.Invalidation.Http'">
|
||||
<li>
|
||||
<StringSetting
|
||||
:path="[':pleroma', 'Pleroma.Web.MediaProxy.Invalidation.Http', ':method']"
|
||||
parent-path=":pleroma.:media_proxy.:invalidation.:enabled"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<ListSetting
|
||||
ignore-suggestions
|
||||
:path="[':pleroma', 'Pleroma.Web.MediaProxy.Invalidation.Http', ':headers']"
|
||||
parent-path=":pleroma.:media_proxy.:invalidation.:enabled"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<ListSetting
|
||||
:path="[':pleroma', 'Pleroma.Web.MediaProxy.Invalidation.Http', ':options']"
|
||||
parent-path=":pleroma.:media_proxy.:invalidation.:enabled"
|
||||
/>
|
||||
</li>
|
||||
</template>
|
||||
<template v-if="mediaInvalidationProvider === 'Pleroma.Web.MediaProxy.Invalidation.Script'">
|
||||
<!-- TODO: you know the drill by now - list component -->
|
||||
<li>
|
||||
<StringSetting
|
||||
:path="[':pleroma', 'Pleroma.Web.MediaProxy.Invalidation.Script', ':script_path']"
|
||||
parent-path=":pleroma.:media_proxy.:invalidation.:enabled"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<StringSetting
|
||||
:path="[':pleroma', 'Pleroma.Web.MediaProxy.Invalidation.Script', ':url_format']"
|
||||
parent-path=":pleroma.:media_proxy.:invalidation.:enabled"
|
||||
/>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<h3>{{ $t('admin_dash.media_proxy.limits') }}</h3>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<IntegerSetting
|
||||
path=":pleroma.:media_proxy.:proxy_opts.:max_body_length"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<IntegerSetting
|
||||
path=":pleroma.:media_proxy.:proxy_opts.:max_read_duration"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<GroupSetting path=":pleroma.:media_proxy.:proxy_opts" />
|
||||
</li>
|
||||
</ul>
|
||||
<!-- TODO: add whitelist when we have list component (hehe) -->
|
||||
<h3>{{ $t('admin_dash.media_proxy.thumbnails') }}</h3>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<BooleanSetting path=":pleroma.:media_preview_proxy.:enabled" />
|
||||
<ul class="setting-list suboptions">
|
||||
<li>
|
||||
<IntegerSetting
|
||||
parent-path=":pleroma.:media_preview_proxy.:enabled"
|
||||
path=":pleroma.:media_preview_proxy.:image_quality"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<IntegerSetting
|
||||
parent-path=":pleroma.:media_preview_proxy.:enabled"
|
||||
path=":pleroma.:media_preview_proxy.:min_content_length"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<IntegerSetting
|
||||
parent-path=":pleroma.:media_preview_proxy.:enabled"
|
||||
path=":pleroma.:media_preview_proxy.:thumbnail_max_width"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<IntegerSetting
|
||||
parent-path=":pleroma.:media_preview_proxy.:enabled"
|
||||
path=":pleroma.:media_preview_proxy.:thumbnail_max_height"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./media_proxy_tab.js"></script>
|
||||
37
src/components/settings_modal/admin_tabs/monitoring_tab.js
Normal file
37
src/components/settings_modal/admin_tabs/monitoring_tab.js
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import AttachmentSetting from '../helpers/attachment_setting.vue'
|
||||
import BooleanSetting from '../helpers/boolean_setting.vue'
|
||||
import ChoiceSetting from '../helpers/choice_setting.vue'
|
||||
import GroupSetting from '../helpers/group_setting.vue'
|
||||
import IntegerSetting from '../helpers/integer_setting.vue'
|
||||
import ListSetting from '../helpers/list_setting.vue'
|
||||
import SharedComputedObject from '../helpers/shared_computed_object.js'
|
||||
import StringSetting from '../helpers/string_setting.vue'
|
||||
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { faGlobe } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(faGlobe)
|
||||
|
||||
const MonitoringTab = {
|
||||
provide() {
|
||||
return {
|
||||
defaultDraftMode: true,
|
||||
defaultSource: 'admin',
|
||||
}
|
||||
},
|
||||
components: {
|
||||
BooleanSetting,
|
||||
ChoiceSetting,
|
||||
IntegerSetting,
|
||||
StringSetting,
|
||||
AttachmentSetting,
|
||||
GroupSetting,
|
||||
ListSetting,
|
||||
},
|
||||
computed: {
|
||||
...SharedComputedObject(),
|
||||
},
|
||||
methods: {},
|
||||
}
|
||||
|
||||
export default MonitoringTab
|
||||
47
src/components/settings_modal/admin_tabs/monitoring_tab.vue
Normal file
47
src/components/settings_modal/admin_tabs/monitoring_tab.vue
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
<template>
|
||||
<div :label="$t('admin_dash.tabs.monitoring')">
|
||||
<div class="setting-section">
|
||||
<h3>{{ $t('admin_dash.monitoring.builtins') }}</h3>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<BooleanSetting :path="[':pleroma','Pleroma.Emails.NewUsersDigestEmail',':enabled']" />
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path=":pleroma.:instance.:healthcheck" />
|
||||
</li>
|
||||
</ul>
|
||||
<h3>{{ $t('admin_dash.monitoring.prometheus') }}</h3>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<BooleanSetting :path="[':prometheus','Pleroma.Web.Endpoint.MetricsExporter',':enabled']" />
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting
|
||||
:parent-path="[':prometheus','Pleroma.Web.Endpoint.MetricsExporter',':enabled']"
|
||||
:path="[':prometheus','Pleroma.Web.Endpoint.MetricsExporter',':auth']"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<StringSetting
|
||||
:parent-path="[':prometheus','Pleroma.Web.Endpoint.MetricsExporter',':enabled']"
|
||||
:path="[':prometheus','Pleroma.Web.Endpoint.MetricsExporter',':path']"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<ChoiceSetting
|
||||
:parent-path="[':prometheus','Pleroma.Web.Endpoint.MetricsExporter',':enabled']"
|
||||
:path="[':prometheus','Pleroma.Web.Endpoint.MetricsExporter',':format']"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<ListSetting
|
||||
:parent-path="[':prometheus','Pleroma.Web.Endpoint.MetricsExporter',':enabled']"
|
||||
:path="[':prometheus','Pleroma.Web.Endpoint.MetricsExporter',':ip_whitelist']"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./monitoring_tab.js"></script>
|
||||
37
src/components/settings_modal/admin_tabs/other_tab.js
Normal file
37
src/components/settings_modal/admin_tabs/other_tab.js
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import AttachmentSetting from '../helpers/attachment_setting.vue'
|
||||
import BooleanSetting from '../helpers/boolean_setting.vue'
|
||||
import ChoiceSetting from '../helpers/choice_setting.vue'
|
||||
import ColorSetting from '../helpers/color_setting.vue'
|
||||
import GroupSetting from '../helpers/group_setting.vue'
|
||||
import IntegerSetting from '../helpers/integer_setting.vue'
|
||||
import ListSetting from '../helpers/list_setting.vue'
|
||||
import MapSetting from '../helpers/map_setting.vue'
|
||||
import PWAManifestIconsSetting from '../helpers/pwa_manifest_icons_setting.vue'
|
||||
import SharedComputedObject from '../helpers/shared_computed_object.js'
|
||||
import StringSetting from '../helpers/string_setting.vue'
|
||||
|
||||
const OtherTab = {
|
||||
provide() {
|
||||
return {
|
||||
defaultDraftMode: true,
|
||||
defaultSource: 'admin',
|
||||
}
|
||||
},
|
||||
components: {
|
||||
BooleanSetting,
|
||||
ChoiceSetting,
|
||||
IntegerSetting,
|
||||
StringSetting,
|
||||
ColorSetting,
|
||||
AttachmentSetting,
|
||||
ListSetting,
|
||||
PWAManifestIconsSetting,
|
||||
MapSetting,
|
||||
GroupSetting,
|
||||
},
|
||||
computed: {
|
||||
...SharedComputedObject(),
|
||||
},
|
||||
}
|
||||
|
||||
export default OtherTab
|
||||
52
src/components/settings_modal/admin_tabs/other_tab.vue
Normal file
52
src/components/settings_modal/admin_tabs/other_tab.vue
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
<template>
|
||||
<div :label="$t('admin_dash.tabs.other')">
|
||||
<div class="setting-section">
|
||||
<h3>{{ $t('admin_dash.other.uncategorized') }}</h3>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<StringSetting path=":pleroma.:instance.:static_dir" />
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path=":pleroma.:instance.:profile_directory" />
|
||||
</li>
|
||||
</ul>
|
||||
<h3>{{ $t('admin_dash.other.reports') }}</h3>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<BooleanSetting path=":pleroma.:instance.:report_strip_status" />
|
||||
</li>
|
||||
</ul>
|
||||
<h3>{{ $t('admin_dash.other.user_backup') }}</h3>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<IntegerSetting :path="[':pleroma','Pleroma.User.Backup',':purge_after_days']" />
|
||||
</li>
|
||||
<li>
|
||||
<IntegerSetting :path="[':pleroma','Pleroma.User.Backup',':limit_days']" />
|
||||
</li>
|
||||
<!-- CONFIRM what is this?
|
||||
<li>
|
||||
<StringSetting :path="[':pleroma','Pleroma.User.Backup',':dir']" />
|
||||
</li>
|
||||
-->
|
||||
<li>
|
||||
<IntegerSetting :path="[':pleroma','Pleroma.User.Backup',':process_chunk_size']" />
|
||||
</li>
|
||||
<li>
|
||||
<IntegerSetting :path="[':pleroma','Pleroma.User.Backup',':timeout']" />
|
||||
</li>
|
||||
</ul>
|
||||
<h3>{{ $t('admin_dash.other.privileges') }}</h3>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<ListSetting path=":pleroma.:instance.:admin_privileges" />
|
||||
</li>
|
||||
<li>
|
||||
<ListSetting path=":pleroma.:instance.:moderator_privileges" />
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./other_tab.js"></script>
|
||||
37
src/components/settings_modal/admin_tabs/posts_tab.js
Normal file
37
src/components/settings_modal/admin_tabs/posts_tab.js
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import AttachmentSetting from '../helpers/attachment_setting.vue'
|
||||
import BooleanSetting from '../helpers/boolean_setting.vue'
|
||||
import ChoiceSetting from '../helpers/choice_setting.vue'
|
||||
import ColorSetting from '../helpers/color_setting.vue'
|
||||
import GroupSetting from '../helpers/group_setting.vue'
|
||||
import IntegerSetting from '../helpers/integer_setting.vue'
|
||||
import ListSetting from '../helpers/list_setting.vue'
|
||||
import MapSetting from '../helpers/map_setting.vue'
|
||||
import PWAManifestIconsSetting from '../helpers/pwa_manifest_icons_setting.vue'
|
||||
import SharedComputedObject from '../helpers/shared_computed_object.js'
|
||||
import StringSetting from '../helpers/string_setting.vue'
|
||||
|
||||
const PostsTab = {
|
||||
provide() {
|
||||
return {
|
||||
defaultDraftMode: true,
|
||||
defaultSource: 'admin',
|
||||
}
|
||||
},
|
||||
components: {
|
||||
BooleanSetting,
|
||||
ChoiceSetting,
|
||||
IntegerSetting,
|
||||
StringSetting,
|
||||
ColorSetting,
|
||||
AttachmentSetting,
|
||||
ListSetting,
|
||||
PWAManifestIconsSetting,
|
||||
MapSetting,
|
||||
GroupSetting,
|
||||
},
|
||||
computed: {
|
||||
...SharedComputedObject(),
|
||||
},
|
||||
}
|
||||
|
||||
export default PostsTab
|
||||
29
src/components/settings_modal/admin_tabs/posts_tab.vue
Normal file
29
src/components/settings_modal/admin_tabs/posts_tab.vue
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
<template>
|
||||
<div :label="$t('admin_dash.tabs.posts')">
|
||||
<div class="setting-section">
|
||||
<h3>{{ $t('admin_dash.posts.global') }}</h3>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<BooleanSetting path=":pleroma.:instances_favicons.:enabled" />
|
||||
</li>
|
||||
</ul>
|
||||
<h3>{{ $t('admin_dash.posts.local') }}</h3>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<ListSetting path=":pleroma.:instance.:allowed_post_formats" />
|
||||
</li>
|
||||
</ul>
|
||||
<h3>{{ $t('admin_dash.posts.remote') }}</h3>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<IntegerSetting path=":pleroma.:instance.:remote_post_retention_days" />
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path=":pleroma.:instance.:skip_thread_containment" />
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./posts_tab.js"></script>
|
||||
19
src/components/settings_modal/admin_tabs/rates_tab.js
Normal file
19
src/components/settings_modal/admin_tabs/rates_tab.js
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import RateSetting from '../helpers/rate_setting.vue'
|
||||
import SharedComputedObject from '../helpers/shared_computed_object.js'
|
||||
|
||||
const RatesTab = {
|
||||
provide() {
|
||||
return {
|
||||
defaultDraftMode: true,
|
||||
defaultSource: 'admin',
|
||||
}
|
||||
},
|
||||
components: {
|
||||
RateSetting,
|
||||
},
|
||||
computed: {
|
||||
...SharedComputedObject(),
|
||||
},
|
||||
}
|
||||
|
||||
export default RatesTab
|
||||
41
src/components/settings_modal/admin_tabs/rates_tab.vue
Normal file
41
src/components/settings_modal/admin_tabs/rates_tab.vue
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
<template>
|
||||
<div :label="$t('admin_dash.tabs.instance')">
|
||||
<div class="setting-section">
|
||||
<h3>{{ $t('admin_dash.rate_limit.account_confirmation_resend') }}</h3>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<RateSetting path=":pleroma.:rate_limit.:account_confirmation_resend" />
|
||||
</li>
|
||||
<li>
|
||||
<RateSetting path=":pleroma.:rate_limit.:ap_routes" />
|
||||
</li>
|
||||
<li>
|
||||
<RateSetting path=":pleroma.:rate_limit.:app_account_creation" />
|
||||
</li>
|
||||
<li>
|
||||
<RateSetting path=":pleroma.:rate_limit.:authentication" />
|
||||
</li>
|
||||
<li>
|
||||
<RateSetting path=":pleroma.:rate_limit.:oauth_app_creation" />
|
||||
</li>
|
||||
<li>
|
||||
<RateSetting path=":pleroma.:rate_limit.:relation_id_action" />
|
||||
</li>
|
||||
<li>
|
||||
<RateSetting path=":pleroma.:rate_limit.:search" />
|
||||
</li>
|
||||
<li>
|
||||
<RateSetting path=":pleroma.:rate_limit.:status_id_action" />
|
||||
</li>
|
||||
<li>
|
||||
<RateSetting path=":pleroma.:rate_limit.:statuses_actions" />
|
||||
</li>
|
||||
<li>
|
||||
<RateSetting path=":pleroma.:rate_limit.:timeline" />
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./rates_tab.js"></script>
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
import AttachmentSetting from '../helpers/attachment_setting.vue'
|
||||
import BooleanSetting from '../helpers/boolean_setting.vue'
|
||||
import ChoiceSetting from '../helpers/choice_setting.vue'
|
||||
import GroupSetting from '../helpers/group_setting.vue'
|
||||
import IntegerSetting from '../helpers/integer_setting.vue'
|
||||
import ListSetting from '../helpers/list_setting.vue'
|
||||
import SharedComputedObject from '../helpers/shared_computed_object.js'
|
||||
import StringSetting from '../helpers/string_setting.vue'
|
||||
import TupleSetting from '../helpers/tuple_setting.vue'
|
||||
|
||||
const RegistrationsTab = {
|
||||
provide() {
|
||||
return {
|
||||
defaultDraftMode: true,
|
||||
defaultSource: 'admin',
|
||||
}
|
||||
},
|
||||
components: {
|
||||
BooleanSetting,
|
||||
ChoiceSetting,
|
||||
IntegerSetting,
|
||||
StringSetting,
|
||||
TupleSetting,
|
||||
AttachmentSetting,
|
||||
GroupSetting,
|
||||
ListSetting,
|
||||
},
|
||||
computed: {
|
||||
...SharedComputedObject(),
|
||||
},
|
||||
}
|
||||
|
||||
export default RegistrationsTab
|
||||
182
src/components/settings_modal/admin_tabs/registrations_tab.vue
Normal file
182
src/components/settings_modal/admin_tabs/registrations_tab.vue
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
<template>
|
||||
<div :label="$t('admin_dash.tabs.instance')">
|
||||
<div class="setting-section">
|
||||
<h3>{{ $t('admin_dash.instance.registrations') }}</h3>
|
||||
<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>
|
||||
<h4>{{ $t('admin_dash.instance.captcha_header') }}</h4>
|
||||
<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'"
|
||||
>
|
||||
<h5>{{ $t('admin_dash.instance.kocaptcha') }}</h5>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<StringSetting :path="[':pleroma', 'Pleroma.Captcha.Kocaptcha', ':endpoint']" />
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<h3>{{ $t('admin_dash.registrations.autofollow') }}</h3>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<ListSetting
|
||||
path=":pleroma.:instance.:autofollowed_nicknames"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<ListSetting
|
||||
path=":pleroma.:instance.:autofollowing_nicknames"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
<h3>{{ $t('admin_dash.registrations.welcome.title') }}</h3>
|
||||
<ul class="setting-list">
|
||||
<p>{{ $t('admin_dash.registrations.welcome.description') }}</p>
|
||||
<li>
|
||||
<h4>{{ $t('admin_dash.registrations.welcome.direct_message') }}</h4>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<BooleanSetting
|
||||
path=":pleroma.:welcome.:direct_message.:enabled"
|
||||
/>
|
||||
<ul class="setting-list suboptions">
|
||||
<li>
|
||||
<StringSetting
|
||||
path=":pleroma.:welcome.:direct_message.:sender_nickname"
|
||||
parent-path=":pleroma.:welcome.:direct_message.:enabled"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<StringSetting
|
||||
path=":pleroma.:welcome.:direct_message.:message"
|
||||
parent-path=":pleroma.:welcome.:direct_message.:enabled"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
<GroupSetting path=":pleroma.:welcome.:direct_message" />
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<h4>{{ $t('admin_dash.registrations.welcome.chat_message') }}</h4>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<BooleanSetting
|
||||
path=":pleroma.:welcome.:chat_message.:enabled"
|
||||
/>
|
||||
<ul class="setting-list suboptions">
|
||||
<li>
|
||||
<StringSetting
|
||||
tuple
|
||||
path=":pleroma.:welcome.:chat_message.:sender_nickname"
|
||||
parent-path=":pleroma.:welcome.:chat_message.:enabled"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<StringSetting
|
||||
path=":pleroma.:welcome.:chat_message.:message"
|
||||
parent-path=":pleroma.:welcome.:chat_message.:enabled"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
<GroupSetting path=":pleroma.:welcome.:chat_message" />
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<h4>{{ $t('admin_dash.registrations.welcome.email_message') }}</h4>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<BooleanSetting
|
||||
path=":pleroma.:welcome.:email.:enabled"
|
||||
/>
|
||||
<ul class="setting-list suboptions">
|
||||
<li>
|
||||
<TupleSetting
|
||||
path=":pleroma.:welcome.:email.:sender"
|
||||
parent-path=":pleroma.:welcome.:email.:enabled"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<StringSetting
|
||||
path=":pleroma.:welcome.:email.:subject"
|
||||
parent-path=":pleroma.:welcome.:email.:enabled"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<StringSetting
|
||||
path=":pleroma.:welcome.:email.:html"
|
||||
parent-path=":pleroma.:welcome.:email.:enabled"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
<GroupSetting path=":pleroma.:welcome.:email" />
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<h3>{{ $t('admin_dash.registrations.restrictions') }}</h3>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<ListSetting
|
||||
ignore-suggestions
|
||||
:path="[':pleroma', 'Pleroma.User', ':email_blacklist']"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./registrations_tab.js"></script>
|
||||
51
src/components/settings_modal/admin_tabs/uploads_tab.js
Normal file
51
src/components/settings_modal/admin_tabs/uploads_tab.js
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import BooleanSetting from '../helpers/boolean_setting.vue'
|
||||
import ChoiceSetting from '../helpers/choice_setting.vue'
|
||||
import IntegerSetting from '../helpers/integer_setting.vue'
|
||||
import SharedComputedObject from '../helpers/shared_computed_object.js'
|
||||
import StringSetting from '../helpers/string_setting.vue'
|
||||
|
||||
const UploadsTab = {
|
||||
provide() {
|
||||
return {
|
||||
defaultDraftMode: true,
|
||||
defaultSource: 'admin',
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
uploaders: [
|
||||
{
|
||||
key: 'Pleroma.Uploaders.Local',
|
||||
value: 'Pleroma.Uploaders.Local',
|
||||
label: this.$t('admin_dash.uploads.local_uploader'),
|
||||
},
|
||||
{
|
||||
key: 'Pleroma.Uploaders.IPFS',
|
||||
value: 'Pleroma.Uploaders.IPFS',
|
||||
label: 'IPFS',
|
||||
},
|
||||
{
|
||||
key: 'Pleroma.Uploaders.S3',
|
||||
value: 'Pleroma.Uploaders.S3',
|
||||
label: 'S3',
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
components: {
|
||||
BooleanSetting,
|
||||
ChoiceSetting,
|
||||
IntegerSetting,
|
||||
StringSetting,
|
||||
},
|
||||
computed: {
|
||||
uploader() {
|
||||
return this.$store.state.adminSettings.draft[':pleroma'][
|
||||
'Pleroma.Upload'
|
||||
][':uploader']
|
||||
},
|
||||
...SharedComputedObject(),
|
||||
},
|
||||
}
|
||||
|
||||
export default UploadsTab
|
||||
110
src/components/settings_modal/admin_tabs/uploads_tab.vue
Normal file
110
src/components/settings_modal/admin_tabs/uploads_tab.vue
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
<template>
|
||||
<div :label="$t('admin_dash.tabs.uploads')">
|
||||
<div class="setting-section">
|
||||
<h3>{{ $t('admin_dash.uploads.upload') }}</h3>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<ChoiceSetting
|
||||
:path="[':pleroma','Pleroma.Upload',':uploader']"
|
||||
:options="uploaders"
|
||||
/>
|
||||
<h4>{{ $t('admin_dash.uploads.uploader_settings') }}</h4>
|
||||
<ul class="setting-list suboptions">
|
||||
<template v-if="uploader === 'Pleroma.Uploaders.Local'">
|
||||
<li>
|
||||
<StringSetting
|
||||
:path="[':pleroma','Pleroma.Uploaders.Local',':uploads']"
|
||||
/>
|
||||
</li>
|
||||
</template>
|
||||
<template v-else-if="uploader === 'Pleroma.Uploaders.IPFS'">
|
||||
<li>
|
||||
<StringSetting
|
||||
:path="[':pleroma','Pleroma.Uploaders.IPFS',':get_gateway_url']"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<StringSetting
|
||||
:path="[':pleroma','Pleroma.Uploaders.IPFS',':post_gateway_url']"
|
||||
/>
|
||||
</li>
|
||||
</template>
|
||||
<template v-else-if="uploader === 'Pleroma.Uploaders.S3'">
|
||||
<li>
|
||||
<StringSetting
|
||||
:path="[':pleroma','Pleroma.Uploaders.S3',':bucket']"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<StringSetting
|
||||
:path="[':pleroma','Pleroma.Uploaders.S3',':bucket_namespace']"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting
|
||||
:path="[':pleroma','Pleroma.Uploaders.S3',':streaming_enabled']"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<StringSetting
|
||||
:path="[':pleroma','Pleroma.Uploaders.S3',':truncated_namespace']"
|
||||
/>
|
||||
</li>
|
||||
</template>
|
||||
<li>
|
||||
<IntegerSetting
|
||||
:path="[':pleroma','Pleroma.Uploaders.Uploader',':timeout']"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<h3>{{ $t('admin_dash.uploads.attachments') }}</h3>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<BooleanSetting
|
||||
path=":pleroma.:instance.:attachment_links"
|
||||
:options="uploaders"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting
|
||||
path=":pleroma.:instance.:cleanup_attachments"
|
||||
:options="uploaders"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
<!-- CONFIRM how filters work -->
|
||||
<h3>{{ $t('admin_dash.uploads.filenames') }}</h3>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<BooleanSetting
|
||||
:path="[':pleroma','Pleroma.Upload',':link_name']"
|
||||
:subgroup="adapter"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<IntegerSetting
|
||||
:path="[':pleroma','Pleroma.Upload',':filename_display_max_length']"
|
||||
:subgroup="adapter"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<StringSetting
|
||||
:path="[':pleroma','Pleroma.Upload',':default_description']"
|
||||
:subgroup="adapter"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<StringSetting
|
||||
:path="[':pleroma','Pleroma.Upload',':base_url']"
|
||||
:subgroup="adapter"
|
||||
/>
|
||||
</li>
|
||||
<!-- TODO: add mime-type when we have a dynamic list component -->
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./uploads_tab.js"></script>
|
||||
|
|
@ -1,18 +1,18 @@
|
|||
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||
import Select from 'src/components/select/select.vue'
|
||||
import BasicUserCard from 'src/components/basic_user_card/basic_user_card.vue'
|
||||
import ProgressButton from 'src/components/progress_button/progress_button.vue'
|
||||
import AdminCard from 'src/components/settings_modal/admin_tabs/admin_card.vue'
|
||||
import PageList from 'src/components/page_list/page_list.vue'
|
||||
import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'
|
||||
import Popover from 'src/components/popover/popover.vue'
|
||||
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||
import GenericConfirm from 'src/components/confirm_modal/generic_confirm.vue'
|
||||
import PageList from 'src/components/page_list/page_list.vue'
|
||||
import Popover from 'src/components/popover/popover.vue'
|
||||
import ProgressButton from 'src/components/progress_button/progress_button.vue'
|
||||
import Select from 'src/components/select/select.vue'
|
||||
import AdminCard from 'src/components/settings_modal/admin_tabs/admin_card.vue'
|
||||
import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'
|
||||
|
||||
const UsersTab = {
|
||||
provide () {
|
||||
provide() {
|
||||
return {
|
||||
defaultDraftMode: true,
|
||||
defaultSource: 'admin'
|
||||
defaultSource: 'admin',
|
||||
}
|
||||
},
|
||||
data() {
|
||||
|
|
@ -27,7 +27,7 @@ const UsersTab = {
|
|||
filtersName: '',
|
||||
filtersEmail: '',
|
||||
expandedUser: null,
|
||||
loading: false
|
||||
loading: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -35,44 +35,50 @@ const UsersTab = {
|
|||
* do we filter for admins?
|
||||
* @returns {boolean}
|
||||
*/
|
||||
filtersIsAdmin () {
|
||||
return this.filtersPrivileges === 'admin' || this.filtersPrivileges === 'modsnadmins'
|
||||
filtersIsAdmin() {
|
||||
return (
|
||||
this.filtersPrivileges === 'admin' ||
|
||||
this.filtersPrivileges === 'modsnadmins'
|
||||
)
|
||||
},
|
||||
/**
|
||||
* do we filter for moderators?
|
||||
* @returns {boolean}
|
||||
*/
|
||||
filtersIsModerator () {
|
||||
return this.filtersPrivileges === 'moderator' || this.filtersPrivileges === 'modsnadmins'
|
||||
filtersIsModerator() {
|
||||
return (
|
||||
this.filtersPrivileges === 'moderator' ||
|
||||
this.filtersPrivileges === 'modsnadmins'
|
||||
)
|
||||
},
|
||||
/**
|
||||
* do we filter for active users?
|
||||
* @returns {boolean}
|
||||
*/
|
||||
filtersActive () {
|
||||
filtersActive() {
|
||||
return this.filtersActivity === 'active'
|
||||
},
|
||||
/**
|
||||
* do we filter for deactivated users?
|
||||
* @returns {boolean}
|
||||
*/
|
||||
filtersDeactivated () {
|
||||
filtersDeactivated() {
|
||||
return this.filtersActivity === 'deactivated'
|
||||
},
|
||||
/**
|
||||
* do we filter for local users?
|
||||
* @returns {boolean}
|
||||
*/
|
||||
filtersLocal () {
|
||||
filtersLocal() {
|
||||
return this.filtersOrigin === 'local'
|
||||
},
|
||||
/**
|
||||
* do we filter for external users?
|
||||
* @return {boolean}
|
||||
*/
|
||||
filtersExternal () {
|
||||
filtersExternal() {
|
||||
return this.filtersOrigin === 'external'
|
||||
}
|
||||
},
|
||||
},
|
||||
components: {
|
||||
Checkbox,
|
||||
|
|
@ -83,7 +89,7 @@ const UsersTab = {
|
|||
AdminCard,
|
||||
TabSwitcher,
|
||||
Popover,
|
||||
GenericConfirm
|
||||
GenericConfirm,
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
|
|
@ -91,8 +97,8 @@ const UsersTab = {
|
|||
* @param {object} store
|
||||
* @param {object} opts
|
||||
*/
|
||||
fetchPage (store, opts) {
|
||||
if(!this.init) return new Promise(() => [])
|
||||
fetchPage(store, opts) {
|
||||
if (!this.init) return new Promise(() => [])
|
||||
const filters = {
|
||||
isAdmin: this.filtersIsAdmin,
|
||||
isModerator: this.filtersIsModerator,
|
||||
|
|
@ -101,20 +107,23 @@ const UsersTab = {
|
|||
local: this.filtersLocal,
|
||||
external: this.filtersExternal,
|
||||
needApproval: this.filtersNeedApproval,
|
||||
unconfirmed: this.filtersUnconfirmeUnconfirmed
|
||||
unconfirmed: this.filtersUnconfirmeUnconfirmed,
|
||||
}
|
||||
const nopts = {
|
||||
...opts,
|
||||
...{
|
||||
query: this.filtersQuery,
|
||||
filters,
|
||||
name: this.filtersName,
|
||||
email: this.filtersEmail,
|
||||
},
|
||||
}
|
||||
const nopts = { ...opts, ...{
|
||||
query: this.filtersQuery,
|
||||
filters,
|
||||
name: this.filtersName,
|
||||
email: this.filtersEmail
|
||||
}}
|
||||
return store.dispatch('fetchAdminUsers', nopts)
|
||||
},
|
||||
/**
|
||||
* reset the userlist explicitly
|
||||
*/
|
||||
reset () {
|
||||
reset() {
|
||||
this.$refs.userList.reset()
|
||||
},
|
||||
/**
|
||||
|
|
@ -132,23 +141,26 @@ const UsersTab = {
|
|||
selectionConfirmed(action) {
|
||||
const restricted = []
|
||||
const s = this.$refs.userList.getSelected()
|
||||
s.forEach(u => {
|
||||
if (restricted.includes(action) !== false || u.id !== this.$store.state.users.currentUser.id) {
|
||||
s.forEach((u) => {
|
||||
if (
|
||||
restricted.includes(action) !== false ||
|
||||
u.id !== this.$store.state.users.currentUser.id
|
||||
) {
|
||||
const uf = this.$store.getters.findUser(u.id)
|
||||
console.log('user: ', uf)
|
||||
this.$store.dispatch(action, this.$store.getters.findUser(u.id))
|
||||
}
|
||||
})
|
||||
this.reset()
|
||||
}
|
||||
},
|
||||
},
|
||||
/**
|
||||
* mark as initialized and reset user list
|
||||
*/
|
||||
mounted () {
|
||||
mounted() {
|
||||
this.init = true
|
||||
this.reset()
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export default UsersTab
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
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'
|
||||
import MediaUpload from 'src/components/media_upload/media_upload.vue'
|
||||
import Setting from './setting.js'
|
||||
|
||||
import { useInstanceStore } from 'src/stores/instance.js'
|
||||
|
||||
import { fileTypeExt } from 'src/services/file_type/file_type.service.js'
|
||||
|
||||
export default {
|
||||
...Setting,
|
||||
|
|
@ -11,34 +14,34 @@ export default {
|
|||
acceptTypes: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'image/*'
|
||||
}
|
||||
default: 'image/*',
|
||||
},
|
||||
},
|
||||
components: {
|
||||
...Setting.components,
|
||||
MediaUpload,
|
||||
Attachment
|
||||
Attachment,
|
||||
},
|
||||
computed: {
|
||||
...Setting.computed,
|
||||
attachment () {
|
||||
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
|
||||
const url = path.includes('://') ? path : useInstanceStore().server + path
|
||||
return {
|
||||
mimetype: fileTypeExt(url),
|
||||
url
|
||||
type: fileTypeExt(url),
|
||||
url,
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...Setting.methods,
|
||||
setMediaFile (fileInfo) {
|
||||
setMediaFile(fileInfo) {
|
||||
if (this.realDraftMode) {
|
||||
this.draft = fileInfo.url
|
||||
} else {
|
||||
this.configSink(this.path, fileInfo.url)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
<template>
|
||||
<span
|
||||
v-if="matchesExpertLevel"
|
||||
class="AttachmentSetting"
|
||||
class="AttachmentSetting setting-item"
|
||||
:class="{ '-compact': compact }"
|
||||
>
|
||||
<label
|
||||
class="setting-label"
|
||||
:for="path"
|
||||
:class="{ 'faint': shouldBeDisabled }"
|
||||
>
|
||||
|
|
@ -39,7 +40,7 @@
|
|||
:changed="isChanged"
|
||||
:onclick="reset"
|
||||
/>
|
||||
<ProfileSettingIndicator :is-profile="isProfileSetting" />
|
||||
<LocalSettingIndicator :is-local="isLocalSetting" />
|
||||
</div>
|
||||
<div v-if="!compact">{{ $t('settings.preview') }}</div>
|
||||
<Attachment
|
||||
|
|
|
|||
|
|
@ -5,27 +5,27 @@ export default {
|
|||
...Setting,
|
||||
props: {
|
||||
...Setting.props,
|
||||
indeterminateState: [String, Object]
|
||||
indeterminateState: [String, Object],
|
||||
},
|
||||
components: {
|
||||
...Setting.components,
|
||||
Checkbox
|
||||
Checkbox,
|
||||
},
|
||||
computed: {
|
||||
...Setting.computed,
|
||||
isIndeterminate () {
|
||||
isIndeterminate() {
|
||||
return this.visibleState === this.indeterminateState
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...Setting.methods,
|
||||
getValue (e) {
|
||||
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
|
||||
}
|
||||
return e
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
<template>
|
||||
<label
|
||||
v-if="matchesExpertLevel"
|
||||
class="BooleanSetting"
|
||||
class="BooleanSetting setting-item"
|
||||
>
|
||||
<Checkbox
|
||||
class="setting-control setting-label"
|
||||
:model-value="visibleState"
|
||||
:disabled="shouldBeDisabled"
|
||||
:indeterminate="isIndeterminate"
|
||||
|
|
@ -13,6 +14,12 @@
|
|||
class="label"
|
||||
:class="{ 'faint': shouldBeDisabled }"
|
||||
>
|
||||
<ModifiedIndicator
|
||||
:changed="isChanged"
|
||||
:onclick="reset"
|
||||
/>
|
||||
<LocalSettingIndicator :is-local="isLocalSetting" />
|
||||
{{ ' ' }}
|
||||
<template v-if="backendDescriptionLabel">
|
||||
{{ backendDescriptionLabel }}
|
||||
</template>
|
||||
|
|
@ -22,19 +29,16 @@
|
|||
<slot v-else />
|
||||
</span>
|
||||
</Checkbox>
|
||||
<ModifiedIndicator
|
||||
:changed="isChanged"
|
||||
:onclick="reset"
|
||||
/>
|
||||
<ProfileSettingIndicator :is-profile="isProfileSetting" />
|
||||
<DraftButtons />
|
||||
<p
|
||||
v-if="backendDescriptionDescription"
|
||||
v-if="backendDescriptionDescription || showDescription"
|
||||
class="setting-description"
|
||||
:class="{ 'faint': shouldBeDisabled }"
|
||||
>
|
||||
{{ backendDescriptionDescription + ' ' }}
|
||||
<slot name="description">
|
||||
{{ backendDescriptionDescription + ' ' }}
|
||||
</slot>
|
||||
</p>
|
||||
<DraftButtons />
|
||||
</label>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -5,37 +5,50 @@ export default {
|
|||
...Setting,
|
||||
components: {
|
||||
...Setting.components,
|
||||
Select
|
||||
Select,
|
||||
},
|
||||
props: {
|
||||
...Setting.props,
|
||||
overrideOptions: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
},
|
||||
options: {
|
||||
type: Array,
|
||||
required: false
|
||||
required: false,
|
||||
},
|
||||
optionLabelMap: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: {}
|
||||
}
|
||||
default: {},
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...Setting.computed,
|
||||
realOptions () {
|
||||
realOptions() {
|
||||
if (this.overrideOptions) {
|
||||
return this.options
|
||||
}
|
||||
if (this.realSource === 'admin') {
|
||||
return this.backendDescriptionSuggestions.map(x => ({
|
||||
if (
|
||||
!this.backendDescriptionSuggestions?.length ||
|
||||
this.backendDescriptionSuggestions?.length === 0
|
||||
) {
|
||||
return this.options
|
||||
}
|
||||
return this.backendDescriptionSuggestions.map((x) => ({
|
||||
key: x,
|
||||
value: x,
|
||||
label: this.optionLabelMap[x] || x
|
||||
label: this.optionLabelMap[x] || x,
|
||||
}))
|
||||
}
|
||||
return this.options
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...Setting.methods,
|
||||
getValue (e) {
|
||||
getValue(e) {
|
||||
return e
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,27 @@
|
|||
<template>
|
||||
<label
|
||||
v-if="matchesExpertLevel"
|
||||
class="ChoiceSetting"
|
||||
class="ChoiceSetting setting-item"
|
||||
:class="{ 'faint': shouldBeDisabled }"
|
||||
>
|
||||
<template v-if="backendDescriptionLabel">
|
||||
{{ backendDescriptionLabel }}
|
||||
</template>
|
||||
<template v-else>
|
||||
<slot />
|
||||
</template>
|
||||
{{ ' ' }}
|
||||
<span class="setting-label">
|
||||
<ModifiedIndicator
|
||||
:changed="isChanged"
|
||||
:onclick="reset"
|
||||
/>
|
||||
<LocalSettingIndicator :is-local="isLocalSetting" />
|
||||
{{ ' ' }}
|
||||
<template v-if="backendDescriptionLabel">
|
||||
{{ backendDescriptionLabel }}
|
||||
</template>
|
||||
<template v-else>
|
||||
<slot />
|
||||
</template>
|
||||
</span>
|
||||
<Select
|
||||
:model-value="realDraftMode ? draft :state"
|
||||
:disabled="disabled"
|
||||
class="setting-control"
|
||||
:model-value="realDraftMode ? draft : state"
|
||||
:disabled="shouldBeDisabled"
|
||||
@update:model-value="update"
|
||||
>
|
||||
<option
|
||||
|
|
@ -24,11 +33,6 @@
|
|||
{{ option.value === defaultState ? $t('settings.instance_default_simple') : '' }}
|
||||
</option>
|
||||
</Select>
|
||||
<ModifiedIndicator
|
||||
:changed="isChanged"
|
||||
:onclick="reset"
|
||||
/>
|
||||
<ProfileSettingIndicator :is-profile="isProfileSetting" />
|
||||
<DraftButtons />
|
||||
<p
|
||||
v-if="backendDescriptionDescription"
|
||||
|
|
@ -40,3 +44,16 @@
|
|||
</template>
|
||||
|
||||
<script src="./choice_setting.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
.ChoiceSetting.setting-item {
|
||||
.-mobile & {
|
||||
display: block;
|
||||
|
||||
.setting-label {
|
||||
display: block;
|
||||
margin-bottom: 0.5em
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
16
src/components/settings_modal/helpers/color_setting.js
Normal file
16
src/components/settings_modal/helpers/color_setting.js
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import ColorInput from 'src/components/color_input/color_input.vue'
|
||||
import Setting from './setting.js'
|
||||
|
||||
export default {
|
||||
...Setting,
|
||||
components: {
|
||||
...Setting.components,
|
||||
ColorInput,
|
||||
},
|
||||
methods: {
|
||||
...Setting.methods,
|
||||
getValue(e) {
|
||||
return e
|
||||
},
|
||||
},
|
||||
}
|
||||
72
src/components/settings_modal/helpers/color_setting.vue
Normal file
72
src/components/settings_modal/helpers/color_setting.vue
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
<template>
|
||||
<label
|
||||
v-if="matchesExpertLevel"
|
||||
class="ColorSetting setting-item"
|
||||
>
|
||||
<label
|
||||
v-if="!hideLabel"
|
||||
: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>
|
||||
{{ ' ' }}
|
||||
<ColorInput
|
||||
:id="path"
|
||||
:name="path"
|
||||
class="setting-control color-setting-input"
|
||||
:class="{ disabled: shouldBeDisabled }"
|
||||
:disabled="shouldBeDisabled"
|
||||
:placeholder="backendDescriptionSuggestions"
|
||||
:model-value="realDraftMode ? draft : state"
|
||||
@update:model-value="update"
|
||||
/>
|
||||
{{ ' ' }}
|
||||
<ModifiedIndicator
|
||||
:changed="isChanged"
|
||||
:onclick="reset"
|
||||
/>
|
||||
<LocalSettingIndicator :is-local="isLocalSetting" />
|
||||
<DraftButtons v-if="!hideDraftButtons" />
|
||||
<p
|
||||
v-if="backendDescriptionDescription"
|
||||
class="setting-description"
|
||||
:class="{ 'faint': shouldBeDisabled }"
|
||||
>
|
||||
{{ backendDescriptionDescription + ' ' }}
|
||||
</p>
|
||||
</label>
|
||||
</template>
|
||||
|
||||
<script src="./color_setting.js"></script>
|
||||
<style lang="scss">
|
||||
.ColorSetting {
|
||||
&.setting-item {
|
||||
display: grid;
|
||||
grid-template-areas:
|
||||
"label control"
|
||||
". desc"
|
||||
". draft";
|
||||
|
||||
.setting-label {
|
||||
text-align: right;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.setting-control {
|
||||
align-self: end;
|
||||
}
|
||||
}
|
||||
|
||||
.color-setting-input {
|
||||
align-self: baseline;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
<!-- TODO make it reusable -->
|
||||
<template>
|
||||
<span
|
||||
v-if="$parent.isDirty || $parent.canHardReset"
|
||||
class="DraftButtons"
|
||||
>
|
||||
<Popover
|
||||
|
|
@ -57,27 +58,24 @@
|
|||
|
||||
<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
|
||||
)
|
||||
library.add(faWrench)
|
||||
|
||||
export default {
|
||||
components: { Popover },
|
||||
props: ['changed']
|
||||
props: ['changed'],
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.DraftButtons {
|
||||
display: inline-block;
|
||||
display: inline-flex;
|
||||
position: relative;
|
||||
|
||||
.button-default {
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
gap: 0.5em;
|
||||
margin-top: 0.5em
|
||||
}
|
||||
|
||||
.draft-tooltip {
|
||||
|
|
|
|||
|
|
@ -150,63 +150,70 @@
|
|||
|
||||
<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'
|
||||
import SelectComponent from 'components/select/select.vue'
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
|
||||
export default {
|
||||
components: { Popover, ConfirmModal, StillImage, SelectComponent },
|
||||
components: {
|
||||
Popover,
|
||||
ConfirmModal: defineAsyncComponent(
|
||||
() => import('src/components/confirm_modal/confirm_modal.vue'),
|
||||
),
|
||||
|
||||
SelectComponent,
|
||||
},
|
||||
|
||||
inject: ['emojiAddr'],
|
||||
props: {
|
||||
placement: {
|
||||
type: String,
|
||||
required: true
|
||||
required: true,
|
||||
},
|
||||
|
||||
newUpload: Boolean,
|
||||
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
required: true,
|
||||
},
|
||||
packName: {
|
||||
type: String,
|
||||
required: true
|
||||
required: true,
|
||||
},
|
||||
shortcode: {
|
||||
type: String,
|
||||
// Only exists when this is not a new upload
|
||||
default: ''
|
||||
default: '',
|
||||
},
|
||||
file: {
|
||||
type: String,
|
||||
// Only exists when this is not a new upload
|
||||
default: ''
|
||||
default: '',
|
||||
},
|
||||
|
||||
// Only exists for emojis from remote packs
|
||||
remote: {
|
||||
type: Object,
|
||||
default: undefined
|
||||
default: undefined,
|
||||
},
|
||||
knownLocalPacks: {
|
||||
type: Object,
|
||||
default: undefined
|
||||
}
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
emits: ['updatePackFiles', 'displayError'],
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
uploadFile: [],
|
||||
uploadURL: '',
|
||||
editedShortcode: this.shortcode,
|
||||
editedFile: this.file,
|
||||
deleteModalVisible: false,
|
||||
copyToPack: ''
|
||||
copyToPack: '',
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
emojiPreview () {
|
||||
emojiPreview() {
|
||||
if (this.newUpload && this.uploadFile.length > 0) {
|
||||
return URL.createObjectURL(this.uploadFile[0])
|
||||
} else if (this.newUpload && this.uploadURL !== '') {
|
||||
|
|
@ -217,73 +224,92 @@ export default {
|
|||
|
||||
return null
|
||||
},
|
||||
isEdited () {
|
||||
return !this.newUpload && (this.editedShortcode !== this.shortcode || this.editedFile !== this.file)
|
||||
isEdited() {
|
||||
return (
|
||||
!this.newUpload &&
|
||||
(this.editedShortcode !== this.shortcode ||
|
||||
this.editedFile !== this.file)
|
||||
)
|
||||
},
|
||||
saveButtonDisabled() {
|
||||
if (this.remote === undefined)
|
||||
return this.newUpload ? (this.uploadURL === "" && this.uploadFile.length == 0) : !this.isEdited
|
||||
else
|
||||
return this.copyToPack === ""
|
||||
}
|
||||
return this.newUpload
|
||||
? this.uploadURL === '' && this.uploadFile.length == 0
|
||||
: !this.isEdited
|
||||
else return this.copyToPack === ''
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
saveEditedEmoji () {
|
||||
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)
|
||||
}
|
||||
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))
|
||||
return resp.json()
|
||||
})
|
||||
.then((resp) => this.$emit('updatePackFiles', resp))
|
||||
},
|
||||
uploadEmoji () {
|
||||
uploadEmoji() {
|
||||
let packName = this.remote === undefined ? this.packName : this.copyToPack
|
||||
this.$store.state.api.backendInteractor.addNewEmojiFile({
|
||||
packName: packName,
|
||||
file: this.remote === undefined
|
||||
? (this.uploadURL !== "" ? this.uploadURL : this.uploadFile[0])
|
||||
: this.emojiAddr(this.file),
|
||||
shortcode: this.editedShortcode,
|
||||
filename: this.editedFile
|
||||
}).then(resp => resp.json()).then(resp => {
|
||||
if (resp.error !== undefined) {
|
||||
this.$emit('displayError', resp.error)
|
||||
return
|
||||
}
|
||||
this.$store.state.api.backendInteractor
|
||||
.addNewEmojiFile({
|
||||
packName: packName,
|
||||
file:
|
||||
this.remote === undefined
|
||||
? this.uploadURL !== ''
|
||||
? this.uploadURL
|
||||
: this.uploadFile[0]
|
||||
: this.emojiAddr(this.file),
|
||||
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, packName)
|
||||
this.$refs.emojiPopover.hidePopover()
|
||||
this.$emit('updatePackFiles', resp, packName)
|
||||
this.$refs.emojiPopover.hidePopover()
|
||||
|
||||
this.editedFile = ''
|
||||
this.editedShortcode = ''
|
||||
this.uploadFile = []
|
||||
})
|
||||
this.editedFile = ''
|
||||
this.editedShortcode = ''
|
||||
this.uploadFile = []
|
||||
})
|
||||
},
|
||||
revertEmoji () {
|
||||
revertEmoji() {
|
||||
this.editedFile = this.file
|
||||
this.editedShortcode = this.shortcode
|
||||
},
|
||||
deleteEmoji () {
|
||||
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.$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, this.packName)
|
||||
})
|
||||
}
|
||||
}
|
||||
this.$emit('updatePackFiles', resp, this.packName)
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
import NumberSetting from './number_setting.vue'
|
||||
export default {
|
||||
components: {
|
||||
NumberSetting
|
||||
}
|
||||
NumberSetting,
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,13 +1,5 @@
|
|||
import { isEqual } from 'lodash'
|
||||
|
||||
import Setting from './setting.js'
|
||||
|
||||
export default {
|
||||
...Setting,
|
||||
computed: {
|
||||
...Setting.computed,
|
||||
isDirty () {
|
||||
return !isEqual(this.state, this.draft)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
<template>
|
||||
<span
|
||||
v-if="matchesExpertLevel"
|
||||
class="GroupSetting"
|
||||
class="GroupSetting setting-item"
|
||||
>
|
||||
<ModifiedIndicator
|
||||
:changed="isChanged"
|
||||
:onclick="reset"
|
||||
/>
|
||||
<ProfileSettingIndicator :is-profile="isProfileSetting" />
|
||||
<LocalSettingIndicator :is-local="isLocalSetting" />
|
||||
<DraftButtons />
|
||||
</span>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@
|
|||
:trigger-attrs="{ 'aria-label': $t('settings.setting_changed') }"
|
||||
>
|
||||
<template #trigger>
|
||||
|
||||
<FAIcon icon="circle-question" />
|
||||
</template>
|
||||
<template #content>
|
||||
|
|
@ -19,15 +18,14 @@
|
|||
|
||||
<script>
|
||||
import Popover from 'src/components/popover/popover.vue'
|
||||
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { faCircleQuestion } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(
|
||||
faCircleQuestion
|
||||
)
|
||||
library.add(faCircleQuestion)
|
||||
|
||||
export default {
|
||||
components: { Popover }
|
||||
components: { Popover },
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
import NumberSetting from './number_setting.vue'
|
||||
export default {
|
||||
components: {
|
||||
NumberSetting
|
||||
}
|
||||
NumberSetting,
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
135
src/components/settings_modal/helpers/list_setting.js
Normal file
135
src/components/settings_modal/helpers/list_setting.js
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||
import Setting from './setting.js'
|
||||
|
||||
import { useMergedConfigStore } from 'src/stores/merged_config.js'
|
||||
|
||||
export default {
|
||||
...Setting,
|
||||
data() {
|
||||
return {
|
||||
newValue: '',
|
||||
}
|
||||
},
|
||||
components: {
|
||||
...Setting.components,
|
||||
Checkbox,
|
||||
},
|
||||
props: {
|
||||
...Setting.props,
|
||||
ignoreSuggestions: {
|
||||
required: false,
|
||||
type: Boolean,
|
||||
},
|
||||
overrideAvailableOptions: {
|
||||
required: false,
|
||||
type: Boolean,
|
||||
},
|
||||
options: {
|
||||
required: false,
|
||||
type: Set,
|
||||
},
|
||||
allowNew: {
|
||||
required: false,
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
forceNew: {
|
||||
required: false,
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...Setting.computed,
|
||||
showNew() {
|
||||
if (this.forceNew) return true
|
||||
if (!this.allowNew) return false
|
||||
|
||||
const isExpert = useMergedConfigStore().mergedConfig.expertLevel > 0
|
||||
const hasBuiltins = this.builtinEntries.length > 0
|
||||
|
||||
if (hasBuiltins) {
|
||||
return isExpert
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
},
|
||||
valueSet() {
|
||||
return new Set(this.visibleState)
|
||||
},
|
||||
suggestionsSet() {
|
||||
const suggestions = this.backendDescriptionSuggestions
|
||||
if (suggestions) {
|
||||
return new Set(suggestions)
|
||||
} else {
|
||||
return new Set()
|
||||
}
|
||||
},
|
||||
extraEntries() {
|
||||
if (this.ignoreSuggestions) return [...this.valueSet.values()]
|
||||
if (!this.suggestionsSet) return []
|
||||
return [...this.valueSet.values()].filter((x) => {
|
||||
return !this.builtinEntriesValueSet.has(x)
|
||||
})
|
||||
},
|
||||
builtinEntries() {
|
||||
if (this.ignoreSuggestions) return []
|
||||
if (this.overrideAvailableOptions) {
|
||||
return [...this.options]
|
||||
}
|
||||
if (!this.suggestionsSet) return []
|
||||
|
||||
const builtins = [...this.suggestionsSet.values()]
|
||||
return builtins.map((option) => ({
|
||||
label: option,
|
||||
value: option,
|
||||
}))
|
||||
},
|
||||
builtinEntriesValueSet() {
|
||||
return new Set(this.builtinEntries.map((x) => x.value))
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...Setting.methods,
|
||||
optionPresent(option) {
|
||||
return this.valueSet.has(option)
|
||||
},
|
||||
getValue({ event, value, index, eventType }) {
|
||||
switch (eventType) {
|
||||
case 'toggle': {
|
||||
this.newValue = ''
|
||||
const newSet = new Set(this.valueSet.values())
|
||||
if (event) {
|
||||
newSet.add(value)
|
||||
} else {
|
||||
newSet.delete(value)
|
||||
}
|
||||
return [...newSet.values()]
|
||||
}
|
||||
|
||||
case 'add': {
|
||||
if (!this.newValue) return this.valueSet.values()
|
||||
const res = [...this.valueSet.values(), this.newValue]
|
||||
this.newValue = ''
|
||||
return res
|
||||
}
|
||||
|
||||
case 'remove': {
|
||||
const pre = [...this.valueSet.values()].slice(0, index)
|
||||
const post = [...this.valueSet.values()].slice(index + 1)
|
||||
|
||||
return [...pre, ...post]
|
||||
}
|
||||
|
||||
case 'edit': {
|
||||
const pre = [...this.valueSet.values()].slice(0, index)
|
||||
const post = [...this.valueSet.values()].slice(index + 1)
|
||||
const string = event.target.value
|
||||
if (!string) return [...this.valueSet.values()]
|
||||
|
||||
return [...pre, string, ...post]
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
92
src/components/settings_modal/helpers/list_setting.vue
Normal file
92
src/components/settings_modal/helpers/list_setting.vue
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
<template>
|
||||
<div
|
||||
v-if="matchesExpertLevel"
|
||||
class="ListSetting setting-item"
|
||||
>
|
||||
<label
|
||||
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>
|
||||
<p
|
||||
v-if="backendDescriptionDescription"
|
||||
class="setting-description"
|
||||
:class="{ 'faint': shouldBeDisabled }"
|
||||
>
|
||||
{{ backendDescriptionDescription + ' ' }}
|
||||
</p>
|
||||
<ul class="setting-list">
|
||||
<li
|
||||
v-for="(item, i) in builtinEntries"
|
||||
:key="i"
|
||||
>
|
||||
<Checkbox
|
||||
:disabled="shouldBeDisabled"
|
||||
:model-value="optionPresent(item.value)"
|
||||
@update:model-value="event => update({ event, value: item.value, eventType: 'toggle' })"
|
||||
>
|
||||
{{ item.label }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li
|
||||
v-for="(item, index) in extraEntries"
|
||||
:key="index"
|
||||
>
|
||||
<div class="btn-group">
|
||||
<input
|
||||
class="input string-input"
|
||||
:class="{ disabled: shouldBeDisabled }"
|
||||
:value="item"
|
||||
@change="e => update({ event: e, index, eventType: 'edit' })"
|
||||
>
|
||||
<button
|
||||
class="button-default"
|
||||
@click="e => update({ index, eventType: 'remove' })"
|
||||
>
|
||||
<FAIcon icon="times" />
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
<li v-if="showNew">
|
||||
<div class="btn-group">
|
||||
<input
|
||||
v-model="newValue"
|
||||
class="input string-input"
|
||||
:class="{ disabled: shouldBeDisabled }"
|
||||
:disabled="shouldBeDisabled"
|
||||
>
|
||||
<button
|
||||
class="button-default"
|
||||
@click="e => update({ eventType: 'add' })"
|
||||
>
|
||||
<FAIcon icon="plus" />
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<ModifiedIndicator
|
||||
:changed="isChanged"
|
||||
:onclick="reset"
|
||||
/>
|
||||
<LocalSettingIndicator :is-local="isLocalSetting" />
|
||||
<DraftButtons />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./list_setting.js"></script>
|
||||
<style lang="scss">
|
||||
.ListSetting {
|
||||
.setting-list {
|
||||
.checkbox {
|
||||
padding: 0.5em 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
44
src/components/settings_modal/helpers/list_tuple_setting.js
Normal file
44
src/components/settings_modal/helpers/list_tuple_setting.js
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import ListSetting from './list_setting.js'
|
||||
|
||||
export default {
|
||||
...ListSetting,
|
||||
data() {
|
||||
return {
|
||||
newValue: ['', ''],
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...ListSetting.methods,
|
||||
getValue({ event, index, eventType, tuple }) {
|
||||
switch (eventType) {
|
||||
case 'add': {
|
||||
if (!this.newValue[0] || !this.newValue[1]) return this.visibleState
|
||||
const res = [...this.visibleState, this.newValue]
|
||||
this.newValue = ['', '']
|
||||
return res
|
||||
}
|
||||
|
||||
case 'remove': {
|
||||
const pre = this.visibleState.slice(0, index)
|
||||
const post = this.visibleState.slice(index + 1)
|
||||
|
||||
return [...pre, ...post]
|
||||
}
|
||||
|
||||
case 'edit': {
|
||||
const pre = this.visibleState.slice(0, index)
|
||||
const post = this.visibleState.slice(index + 1)
|
||||
const item = this.visibleState[index]
|
||||
const string = event.target.value
|
||||
if (!string) return this.visibleState
|
||||
|
||||
if (tuple === 0) {
|
||||
return [...pre, [string, item[1]], ...post]
|
||||
} else {
|
||||
return [...pre, [item[0], string], ...post]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
112
src/components/settings_modal/helpers/list_tuple_setting.vue
Normal file
112
src/components/settings_modal/helpers/list_tuple_setting.vue
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
<template>
|
||||
<div
|
||||
v-if="matchesExpertLevel"
|
||||
class="ListTupleSetting"
|
||||
>
|
||||
<label
|
||||
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>
|
||||
<p
|
||||
v-if="backendDescriptionDescription"
|
||||
class="setting-description"
|
||||
:class="{ 'faint': shouldBeDisabled }"
|
||||
>
|
||||
{{ backendDescriptionDescription + ' ' }}
|
||||
</p>
|
||||
<ul class="setting-list">
|
||||
<li
|
||||
v-for="(item, index) in visibleState"
|
||||
:key="index"
|
||||
>
|
||||
<div class="btn-group">
|
||||
<input
|
||||
class="input string-input"
|
||||
:class="{ disabled: shouldBeDisabled }"
|
||||
:value="item.tuple[0]"
|
||||
@change="e => update({ event: e, index, eventType: 'edit', tuple: 0 })"
|
||||
>
|
||||
<input
|
||||
class="input string-input"
|
||||
:class="{ disabled: shouldBeDisabled }"
|
||||
:value="item.tuple[1]"
|
||||
@change="e => update({ event: e, index, eventType: 'edit', tuple: 1 })"
|
||||
>
|
||||
<button
|
||||
class="button-default"
|
||||
@click="e => update({ index, eventType: 'remove' })"
|
||||
>
|
||||
<FAIcon icon="times" />
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div class="btn-group">
|
||||
<input
|
||||
v-model="newValue[0]"
|
||||
class="input string-input"
|
||||
:class="{ disabled: shouldBeDisabled }"
|
||||
:disabled="shouldBeDisabled"
|
||||
:placeholder="backendDescriptionSuggestions[0][0]"
|
||||
>
|
||||
<input
|
||||
v-model="newValue[1]"
|
||||
class="input string-input"
|
||||
:class="{ disabled: shouldBeDisabled }"
|
||||
:disabled="shouldBeDisabled"
|
||||
:placeholder="backendDescriptionSuggestions[0][1]"
|
||||
>
|
||||
<button
|
||||
class="button-default"
|
||||
@click="e => update({ eventType: 'add' })"
|
||||
>
|
||||
<FAIcon icon="plus" />
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<ModifiedIndicator
|
||||
:changed="isChanged"
|
||||
:onclick="reset"
|
||||
/>
|
||||
<DraftButtons />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./list_tuple_setting.js"></script>
|
||||
<style lang="scss">
|
||||
.ListTupleSetting {
|
||||
.btn-group {
|
||||
display: flex
|
||||
}
|
||||
|
||||
dl {
|
||||
display: inline-grid;
|
||||
grid-template-columns: auto auto;
|
||||
gap: 0.5em;
|
||||
align-items: baseline;
|
||||
|
||||
dt {
|
||||
display: inline;
|
||||
font-weight: 800;
|
||||
|
||||
&::after {
|
||||
content: ':'
|
||||
}
|
||||
}
|
||||
|
||||
dd {
|
||||
display: inline;
|
||||
margin: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,21 +1,20 @@
|
|||
<template>
|
||||
<span
|
||||
v-if="isProfile"
|
||||
class="ProfileSettingIndicator"
|
||||
v-if="isLocal"
|
||||
class="LocalSettingIndicator"
|
||||
>
|
||||
<Popover
|
||||
trigger="hover"
|
||||
>
|
||||
<template #trigger>
|
||||
|
||||
<FAIcon
|
||||
icon="server"
|
||||
:aria-label="$t('settings.setting_server_side')"
|
||||
icon="desktop"
|
||||
:aria-label="$t('settings.setting_local_side')"
|
||||
/>
|
||||
</template>
|
||||
<template #content>
|
||||
<div class="profilesetting-tooltip">
|
||||
{{ $t('settings.setting_server_side') }}
|
||||
{{ $t('settings.setting_local_side') }}
|
||||
</div>
|
||||
</template>
|
||||
</Popover>
|
||||
|
|
@ -24,21 +23,20 @@
|
|||
|
||||
<script>
|
||||
import Popover from 'src/components/popover/popover.vue'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { faServer } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(
|
||||
faServer
|
||||
)
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { faDesktop } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(faDesktop)
|
||||
|
||||
export default {
|
||||
components: { Popover },
|
||||
props: ['isProfile']
|
||||
props: ['isLocal'],
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.ProfileSettingIndicator {
|
||||
.LocalSettingIndicator {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
}
|
||||
73
src/components/settings_modal/helpers/map_setting.js
Normal file
73
src/components/settings_modal/helpers/map_setting.js
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
import Setting from './setting.js'
|
||||
|
||||
export default {
|
||||
...Setting,
|
||||
props: {
|
||||
...Setting.props,
|
||||
allowNew: {
|
||||
required: false,
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
newValue: ['', ''], // avoiding extra complexity by just using an array instead of an object
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...Setting.computed,
|
||||
// state that we'll show in the UI, i.e. transforming map into list
|
||||
displayState() {
|
||||
return Object.entries(this.visibleState)
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...Setting.methods,
|
||||
getValue({ event, key, eventType, isKey }) {
|
||||
switch (eventType) {
|
||||
case 'add': {
|
||||
if (key === '') return this.visibleState
|
||||
const res = {
|
||||
...this.visibleState,
|
||||
...Object.fromEntries([this.newValue]),
|
||||
}
|
||||
this.newValue = ['', '']
|
||||
return res
|
||||
}
|
||||
|
||||
case 'remove': {
|
||||
// initial state for this type is empty array
|
||||
if (Array.isArray(this.visibleState)) return this.visibleState
|
||||
const newEntries = Object.entries(this.visibleState).filter(
|
||||
([k]) => k !== key,
|
||||
)
|
||||
|
||||
if (newEntries.length === 0) return []
|
||||
return Object.fromEntries(newEntries)
|
||||
}
|
||||
|
||||
case 'edit': {
|
||||
const string = event.target.value
|
||||
const newEntries = Object.entries(this.visibleState).map(([k, v]) => {
|
||||
if (isKey) {
|
||||
if (k === key) {
|
||||
return [string, v]
|
||||
} else {
|
||||
return [k, v]
|
||||
}
|
||||
} else {
|
||||
if (k === key) {
|
||||
return [k, string]
|
||||
} else {
|
||||
return [k, v]
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return Object.fromEntries(newEntries)
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
112
src/components/settings_modal/helpers/map_setting.vue
Normal file
112
src/components/settings_modal/helpers/map_setting.vue
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
<template>
|
||||
<div
|
||||
v-if="matchesExpertLevel"
|
||||
class="MapSetting setting-item"
|
||||
>
|
||||
<label
|
||||
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>
|
||||
<p
|
||||
v-if="backendDescriptionDescription"
|
||||
class="setting-description"
|
||||
:class="{ 'faint': shouldBeDisabled }"
|
||||
>
|
||||
{{ backendDescriptionDescription + ' ' }}
|
||||
</p>
|
||||
<ul class="setting-list">
|
||||
<li
|
||||
v-for="(item, i) in displayState"
|
||||
:key="i"
|
||||
>
|
||||
<div class="btn-group">
|
||||
<input
|
||||
class="input string-input"
|
||||
:class="{ disabled: shouldBeDisabled }"
|
||||
:value="item[0]"
|
||||
@change="e => update({ event: e, key: item[0], eventType: 'edit', isKey: true })"
|
||||
>
|
||||
<input
|
||||
class="input string-input"
|
||||
:class="{ disabled: shouldBeDisabled }"
|
||||
:value="item[1]"
|
||||
@change="e => update({ event: e, key: item[0], eventType: 'edit', isKey: false })"
|
||||
>
|
||||
<button
|
||||
class="button-default"
|
||||
@click="e => update({ key: item[0], eventType: 'remove' })"
|
||||
>
|
||||
<FAIcon icon="times" />
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
<li v-if="allowNew">
|
||||
<div class="btn-group">
|
||||
<input
|
||||
v-model="newValue[0]"
|
||||
class="input string-input"
|
||||
:class="{ disabled: shouldBeDisabled }"
|
||||
:disabled="shouldBeDisabled"
|
||||
:placeholder="backendDescriptionSuggestions[0][0]"
|
||||
>
|
||||
<input
|
||||
v-model="newValue[1]"
|
||||
class="input string-input"
|
||||
:class="{ disabled: shouldBeDisabled }"
|
||||
:disabled="shouldBeDisabled"
|
||||
:placeholder="backendDescriptionSuggestions[0][1]"
|
||||
>
|
||||
<button
|
||||
class="button-default"
|
||||
@click="e => update({ eventType: 'add' })"
|
||||
>
|
||||
<FAIcon icon="plus" />
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<ModifiedIndicator
|
||||
:changed="isChanged"
|
||||
:onclick="reset"
|
||||
/>
|
||||
<LocalSettingIndicator :is-local="isLocalSetting" />
|
||||
<DraftButtons />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./map_setting.js"></script>
|
||||
<style lang="scss">
|
||||
.MapSetting {
|
||||
.btn-group {
|
||||
display: flex
|
||||
}
|
||||
|
||||
dl {
|
||||
display: inline-grid;
|
||||
grid-template-columns: auto auto;
|
||||
gap: 0.5em;
|
||||
|
||||
dt {
|
||||
display: inline;
|
||||
font-weight: 800;
|
||||
|
||||
&::after {
|
||||
content: ':'
|
||||
}
|
||||
}
|
||||
|
||||
dd {
|
||||
display: inline;
|
||||
margin: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -8,7 +8,6 @@
|
|||
:trigger-attrs="{ 'aria-label': $t('settings.setting_changed') }"
|
||||
>
|
||||
<template #trigger>
|
||||
|
||||
<FAIcon
|
||||
icon="wrench"
|
||||
/>
|
||||
|
|
@ -24,12 +23,11 @@
|
|||
|
||||
<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
|
||||
)
|
||||
library.add(faWrench)
|
||||
|
||||
export default {
|
||||
components: { Popover },
|
||||
|
|
@ -37,9 +35,9 @@ export default {
|
|||
changed: Boolean,
|
||||
messageKey: {
|
||||
type: String,
|
||||
default: 'settings.setting_changed'
|
||||
}
|
||||
}
|
||||
default: 'settings.setting_changed',
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -7,33 +7,33 @@ export default {
|
|||
min: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 1
|
||||
default: 1,
|
||||
},
|
||||
max: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 1
|
||||
default: 1,
|
||||
},
|
||||
step: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 1
|
||||
default: 1,
|
||||
},
|
||||
truncate: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 1
|
||||
}
|
||||
default: 1,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...Setting.methods,
|
||||
getValue (e) {
|
||||
getValue(e) {
|
||||
if (!this.truncate === 1) {
|
||||
return parseInt(e.target.value)
|
||||
} else if (this.truncate > 1) {
|
||||
return Math.trunc(e.target.value / this.truncate) * this.truncate
|
||||
}
|
||||
return parseFloat(e.target.value)
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,21 @@
|
|||
<template>
|
||||
<span
|
||||
v-if="matchesExpertLevel"
|
||||
class="NumberSetting"
|
||||
class="NumberSetting setting-item"
|
||||
>
|
||||
<label
|
||||
v-if="!hideLabel"
|
||||
:for="path"
|
||||
class="setting-label"
|
||||
:class="{ 'faint': shouldBeDisabled }"
|
||||
>
|
||||
<ModifiedIndicator
|
||||
:changed="isChanged"
|
||||
:onclick="reset"
|
||||
/>
|
||||
<LocalSettingIndicator :is-local="isLocalSetting" />
|
||||
{{ ' ' }}
|
||||
<DraftButtons v-if="!hideDraftButtons" />
|
||||
<template v-if="backendDescriptionLabel">
|
||||
{{ backendDescriptionLabel + ' ' }}
|
||||
</template>
|
||||
|
|
@ -18,21 +27,15 @@
|
|||
{{ ' ' }}
|
||||
<input
|
||||
:id="path"
|
||||
class="input number-input"
|
||||
class="input number-input setting-control"
|
||||
type="number"
|
||||
:step="step || 1"
|
||||
:disabled="shouldBeDisabled"
|
||||
:placeholder="backendDescriptionSuggestions"
|
||||
:min="min || 0"
|
||||
:value="realDraftMode ? draft :state"
|
||||
@change="update"
|
||||
>
|
||||
{{ ' ' }}
|
||||
<ModifiedIndicator
|
||||
:changed="isChanged"
|
||||
:onclick="reset"
|
||||
/>
|
||||
<ProfileSettingIndicator :is-profile="isProfileSetting" />
|
||||
<DraftButtons />
|
||||
<p
|
||||
v-if="backendDescriptionDescription"
|
||||
class="setting-description"
|
||||
|
|
|
|||
54
src/components/settings_modal/helpers/proxy_setting.js
Normal file
54
src/components/settings_modal/helpers/proxy_setting.js
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||
import Setting from './setting.js'
|
||||
|
||||
const getUrl = (state) =>
|
||||
state?.tuple ? state.tuple[1] + ':' + state.tuple[2] : state
|
||||
const getSocks = (state) => state?.tuple
|
||||
|
||||
export default {
|
||||
...Setting,
|
||||
data() {
|
||||
return {
|
||||
urlField: '',
|
||||
socksField: false,
|
||||
}
|
||||
},
|
||||
created() {
|
||||
Setting.created()
|
||||
this.urlField = getUrl(this.realDraftMode ? this.draft : this.state)
|
||||
this.socksField = getSocks(this.realDraftMode ? this.draft : this.state)
|
||||
},
|
||||
computed: {
|
||||
...Setting.computed,
|
||||
// state that we'll show in the UI, i.e. transforming map into list
|
||||
displayState() {
|
||||
if (this.visibleState?.tuple) {
|
||||
return this.visibleState.tuple[1] + ':' + this.visibleState.tuple[2]
|
||||
}
|
||||
return this.visibleState
|
||||
},
|
||||
socksState() {
|
||||
return getSocks(this.visibleState)
|
||||
},
|
||||
},
|
||||
components: {
|
||||
...Setting.components,
|
||||
Checkbox,
|
||||
},
|
||||
methods: {
|
||||
...Setting.methods,
|
||||
getValue({ event, isProxy }) {
|
||||
if (isProxy) {
|
||||
this.socksField = event
|
||||
} else {
|
||||
this.urlField = event.target.value
|
||||
}
|
||||
|
||||
if (this.socksField) {
|
||||
return { tuple: [':socks5', ...this.urlField.split(':')] }
|
||||
} else {
|
||||
return this.urlField
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
58
src/components/settings_modal/helpers/proxy_setting.vue
Normal file
58
src/components/settings_modal/helpers/proxy_setting.vue
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
<template>
|
||||
<label
|
||||
v-if="matchesExpertLevel"
|
||||
class="ProxySetting setting-item"
|
||||
>
|
||||
<label
|
||||
v-if="!hideLabel"
|
||||
: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>
|
||||
{{ ' ' }}
|
||||
<div class="setting-control">
|
||||
<input
|
||||
:id="path"
|
||||
class="input string-input"
|
||||
:class="{ disabled: shouldBeDisabled }"
|
||||
:disabled="shouldBeDisabled"
|
||||
:placeholder="backendDescriptionSuggestions[0]"
|
||||
:value="displayState"
|
||||
@change="event => update({ event })"
|
||||
>
|
||||
{{ ' ' }}
|
||||
<Checkbox
|
||||
:model-value="socksState"
|
||||
:disabled="shouldBeDisabled"
|
||||
:indeterminate="isIndeterminate"
|
||||
@update:model-value="event => update({ event, isProxy: true})"
|
||||
>
|
||||
SOCKS5
|
||||
{{ ' ' }}
|
||||
</Checkbox>
|
||||
</div>
|
||||
{{ ' ' }}
|
||||
<ModifiedIndicator
|
||||
:changed="isChanged"
|
||||
:onclick="reset"
|
||||
/>
|
||||
<DraftButtons v-if="!hideDraftButtons" />
|
||||
<p
|
||||
v-if="backendDescriptionDescription"
|
||||
class="setting-description"
|
||||
:class="{ 'faint': shouldBeDisabled }"
|
||||
>
|
||||
{{ backendDescriptionDescription + ' ' }}
|
||||
</p>
|
||||
</label>
|
||||
</template>
|
||||
|
||||
<script src="./proxy_setting.js"></script>
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
import { clone } from 'lodash'
|
||||
|
||||
import Attachment from 'src/components/attachment/attachment.vue'
|
||||
import MediaUpload from 'src/components/media_upload/media_upload.vue'
|
||||
import Select from 'src/components/select/select.vue'
|
||||
import Setting from './setting.js'
|
||||
|
||||
import { useInstanceStore } from 'src/stores/instance.js'
|
||||
|
||||
import { fileTypeExt } from 'src/services/file_type/file_type.service.js'
|
||||
|
||||
export default {
|
||||
...Setting,
|
||||
components: {
|
||||
...Setting.components,
|
||||
Select,
|
||||
Attachment,
|
||||
MediaUpload,
|
||||
},
|
||||
computed: {
|
||||
...Setting.computed,
|
||||
purposeOptions() {
|
||||
return ['any', 'monochrome', 'maskable'].map((value) => ({
|
||||
value,
|
||||
key: value,
|
||||
label: this.$t('admin_dash.instance.pwa.icon.' + value),
|
||||
}))
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...Setting.methods,
|
||||
attachment(e) {
|
||||
const path = e[':src']
|
||||
if (!path) {
|
||||
return {
|
||||
type: '',
|
||||
url: '',
|
||||
}
|
||||
}
|
||||
const url = path.includes('://') ? path : useInstanceStore().server + path
|
||||
|
||||
return {
|
||||
type: fileTypeExt(url),
|
||||
url,
|
||||
}
|
||||
},
|
||||
setMediaFile({ event, index }) {
|
||||
this.update({
|
||||
event: {
|
||||
target: {
|
||||
value: event.url,
|
||||
},
|
||||
},
|
||||
index,
|
||||
eventType: 'edit',
|
||||
field: ':src',
|
||||
})
|
||||
},
|
||||
setPurpose({ event, index }) {
|
||||
this.update({
|
||||
event: {
|
||||
target: {
|
||||
value: event,
|
||||
},
|
||||
},
|
||||
index,
|
||||
eventType: 'edit',
|
||||
field: ':purpose',
|
||||
})
|
||||
},
|
||||
getValue({ event, field, index, eventType }) {
|
||||
switch (eventType) {
|
||||
case 'add': {
|
||||
const res = [...this.visibleState, {}]
|
||||
return res
|
||||
}
|
||||
|
||||
case 'remove': {
|
||||
const pre = this.visibleState.slice(0, index)
|
||||
const post = this.visibleState.slice(index + 1)
|
||||
|
||||
return [...pre, ...post]
|
||||
}
|
||||
|
||||
case 'edit': {
|
||||
const pre = this.visibleState.slice(0, index)
|
||||
const post = this.visibleState.slice(index + 1)
|
||||
const item = clone(this.visibleState[index])
|
||||
const string = event.target.value
|
||||
|
||||
if (!string) {
|
||||
delete item[field]
|
||||
} else {
|
||||
item[field] = string
|
||||
}
|
||||
|
||||
return [...pre, item, ...post]
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
@ -0,0 +1,225 @@
|
|||
<template>
|
||||
<div
|
||||
v-if="matchesExpertLevel"
|
||||
class="PWAManifestIconsSetting setting-item"
|
||||
>
|
||||
<label
|
||||
class="pwa-label setting-label"
|
||||
:class="{ 'faint': shouldBeDisabled }"
|
||||
>
|
||||
<ModifiedIndicator
|
||||
:changed="isChanged"
|
||||
:onclick="reset"
|
||||
/>
|
||||
{{ ' ' }}
|
||||
<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="setting-control">
|
||||
<ul class="item-list">
|
||||
<li
|
||||
v-if="visibleState.length === 0"
|
||||
class="no_items"
|
||||
>
|
||||
{{ $t('admin_dash.instance.pwa.no_icons') }}
|
||||
<button
|
||||
v-if="visibleState.length === 0"
|
||||
class="button-default add-button"
|
||||
@click="e => update({ eventType: 'add' })"
|
||||
>
|
||||
<FAIcon icon="plus" />
|
||||
</button>
|
||||
</li>
|
||||
<li
|
||||
v-for="(item, index) in visibleState"
|
||||
:key="index"
|
||||
>
|
||||
<div class="icon-element">
|
||||
<div class="src-field">
|
||||
<Attachment
|
||||
class="src-attachment"
|
||||
:compact="true"
|
||||
:attachment="attachment(item)"
|
||||
size="small"
|
||||
hide-description
|
||||
/>
|
||||
<div class="src-url">
|
||||
<label for="path">{{ $t('settings.url') }}</label>
|
||||
<input
|
||||
:id="path"
|
||||
class="input string-input"
|
||||
:disabled="shouldBeDisabled"
|
||||
:value="item[':src']"
|
||||
@change="event => update({ event, index, eventType: 'edit', field: ':src' })"
|
||||
>
|
||||
</div>
|
||||
<MediaUpload
|
||||
ref="mediaUpload"
|
||||
class="src-upload media-upload-icon"
|
||||
:class="{ disabled: shouldBeDisabled }"
|
||||
normal-button
|
||||
accept-types="image"
|
||||
@uploaded="event => setMediaFile({ event, index })"
|
||||
/>
|
||||
</div>
|
||||
<dl>
|
||||
<dt>{{ $t('admin_dash.instance.pwa.icon.purpose') }}</dt>
|
||||
<dd>
|
||||
<Select
|
||||
:class="{ disabled: shouldBeDisabled }"
|
||||
:disabled="shouldBeDisabled"
|
||||
:model-value="item[':purpose']"
|
||||
@update:model-value="event => setPurpose({ event, index })"
|
||||
>
|
||||
<option
|
||||
v-for="(purpose, index2) in purposeOptions"
|
||||
:key="index2"
|
||||
:value="purpose.value"
|
||||
>
|
||||
{{ purpose.label }}
|
||||
</option>
|
||||
</Select>
|
||||
</dd>
|
||||
<dt><code>sizes</code>{{ $t('admin_dash.instance.pwa.optional') }}</dt>
|
||||
<dd>
|
||||
<input
|
||||
class="input string-input"
|
||||
:class="{ disabled: shouldBeDisabled }"
|
||||
:value="item[':sizes']"
|
||||
@change="e => update({ event: e, index, eventType: 'edit', field: ':sizes' })"
|
||||
>
|
||||
</dd>
|
||||
<dt><code>type</code>{{ $t('admin_dash.instance.pwa.optional') }}</dt>
|
||||
<dd>
|
||||
<input
|
||||
class="input string-input"
|
||||
:class="{ disabled: shouldBeDisabled }"
|
||||
:value="item[':type']"
|
||||
@change="e => update({ event: e, index, eventType: 'edit', field: ':type' })"
|
||||
>
|
||||
</dd>
|
||||
</dl>
|
||||
<div class="buttons">
|
||||
<button
|
||||
v-if="index === visibleState.length - 1"
|
||||
class="button-default add-button"
|
||||
@click="e => update({ eventType: 'add' })"
|
||||
>
|
||||
<FAIcon icon="plus" />
|
||||
</button>
|
||||
<button
|
||||
class="button-default delete-button"
|
||||
@click="e => update({ index, eventType: 'remove' })"
|
||||
>
|
||||
<FAIcon icon="times" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./pwa_manifest_icons_setting.js"></script>
|
||||
<style lang="scss">
|
||||
div.PWAManifestIconsSetting {
|
||||
margin-left: 3em;
|
||||
|
||||
&.setting-item {
|
||||
display: grid;
|
||||
grid-template-areas:
|
||||
"label"
|
||||
"desc"
|
||||
"control"
|
||||
"draft";
|
||||
grid-template-rows: 2em auto 1fr;
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.pwa-label.setting-label {
|
||||
align-self: end;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
gap: 0.5em;
|
||||
justify-content: right;
|
||||
margin-top: 0.5em;
|
||||
|
||||
button {
|
||||
line-height: 2;
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, 12em);
|
||||
grid-gap: 2em;
|
||||
}
|
||||
|
||||
dl {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
margin-top: 0.5em;
|
||||
gap: 0.25em;
|
||||
align-items: baseline;
|
||||
|
||||
dt {
|
||||
font-weight: 800;
|
||||
|
||||
&::after {
|
||||
content: ':'
|
||||
}
|
||||
}
|
||||
|
||||
dd {
|
||||
margin: 0
|
||||
}
|
||||
}
|
||||
|
||||
.src-field {
|
||||
display: grid;
|
||||
grid-template-columns: auto;
|
||||
justify-items: center;
|
||||
gap: 0.5em;
|
||||
|
||||
.src-attachment {
|
||||
width: 10em;
|
||||
height: 10em;
|
||||
display: block;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.src-upload {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.src-url {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25em;
|
||||
width: 100%;
|
||||
|
||||
label {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
53
src/components/settings_modal/helpers/rate_setting.js
Normal file
53
src/components/settings_modal/helpers/rate_setting.js
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||
import Setting from './setting.js'
|
||||
|
||||
export default {
|
||||
...Setting,
|
||||
data() {
|
||||
return {
|
||||
newValue: '',
|
||||
}
|
||||
},
|
||||
components: {
|
||||
...Setting.components,
|
||||
Checkbox,
|
||||
},
|
||||
props: {
|
||||
...Setting.props,
|
||||
},
|
||||
computed: {
|
||||
...Setting.computed,
|
||||
isSeparate() {
|
||||
// [[a1, b1], [a2, b2]] vs [a, b]
|
||||
return Array.isArray(this.visibleState[0])
|
||||
},
|
||||
normalizedState() {
|
||||
if (this.isSeparate) {
|
||||
return this.visibleState.map((y) => y.map((x) => Number(x) || 0))
|
||||
} else {
|
||||
return [this.visibleState.map((x) => Number(x) || 0)]
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...Setting.methods,
|
||||
getValue({ event, side, index, eventType }) {
|
||||
if (eventType === 'edit') {
|
||||
const value = Number(event.target.value)
|
||||
if (Number.isNaN(value)) return this.visibleState
|
||||
|
||||
const newVal = [...this.normalizedState.map((x) => [...x])]
|
||||
newVal[side][index] = value
|
||||
return newVal
|
||||
}
|
||||
|
||||
if (eventType === 'toggleMode') {
|
||||
if (event === 'split') {
|
||||
return [this.normalizedState[0], this.normalizedState[0]]
|
||||
} else if (event === 'join') {
|
||||
return [this.normalizedState[0]]
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
140
src/components/settings_modal/helpers/rate_setting.vue
Normal file
140
src/components/settings_modal/helpers/rate_setting.vue
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
<template>
|
||||
<div
|
||||
v-if="matchesExpertLevel"
|
||||
class="RateSetting setting-item"
|
||||
>
|
||||
<label
|
||||
class="setting-label"
|
||||
:class="{ 'faint': shouldBeDisabled }"
|
||||
>
|
||||
<ModifiedIndicator
|
||||
:changed="isChanged"
|
||||
:onclick="reset"
|
||||
/>
|
||||
<LocalSettingIndicator :is-local="isLocalSetting" />
|
||||
{{ ' ' }}
|
||||
<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="setting-control">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th> </th>
|
||||
<th>
|
||||
{{ $t('admin_dash.rate_limit.period') }}
|
||||
</th>
|
||||
<th>
|
||||
{{ $t('admin_dash.rate_limit.amount') }}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
{{ isSeparate ? $t('admin_dash.rate_limit.unauthenticated') : $t('admin_dash.rate_limit.rate_limit') }}
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
class="input string-input"
|
||||
type="number"
|
||||
:value="normalizedState[0][0]"
|
||||
@change="e => update({ event: e, index: 0, side: 0, eventType: 'edit' })"
|
||||
>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
class="input string-input"
|
||||
type="number"
|
||||
:value="normalizedState[0][1]"
|
||||
@change="e => update({ event: e, index: 1, side: 0, eventType: 'edit' })"
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="isSeparate">
|
||||
<td>
|
||||
{{ $t('admin_dash.rate_limit.authenticated') }}
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
class="input string-input"
|
||||
type="number"
|
||||
:value="normalizedState[1][0]"
|
||||
@change="e => update({ event: e, index: 0, side: 1, eventType: 'edit' })"
|
||||
>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
class="input string-input"
|
||||
type="number"
|
||||
:value="normalizedState[1][1]"
|
||||
@change="e => update({ event: e, index: 1, side: 1, eventType: 'edit' })"
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<Checkbox
|
||||
:model-value="isSeparate"
|
||||
@update:model-value="event => update({ event: event ? 'join' : 'split', eventType: 'toggleMode' })"
|
||||
>
|
||||
{{ $t('admin_dash.rate_limit.separate') }}
|
||||
</Checkbox>
|
||||
</div>
|
||||
<DraftButtons />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./rate_setting.js"></script>
|
||||
<style lang="scss">
|
||||
.RateSetting {
|
||||
&.setting-item {
|
||||
display: grid;
|
||||
grid-template-areas:
|
||||
"label control"
|
||||
"desc control"
|
||||
". draft";
|
||||
|
||||
.setting-label {
|
||||
text-align: right;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.setting-description {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.setting-control {
|
||||
align-self: end;
|
||||
}
|
||||
}
|
||||
|
||||
table {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
th {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
td {
|
||||
input {
|
||||
width: 15ch;
|
||||
}
|
||||
}
|
||||
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,99 +1,147 @@
|
|||
import ModifiedIndicator from './modified_indicator.vue'
|
||||
import ProfileSettingIndicator from './profile_setting_indicator.vue'
|
||||
import { cloneDeep, get, isEqual, set } from 'lodash'
|
||||
|
||||
import DraftButtons from './draft_buttons.vue'
|
||||
import { get, set, cloneDeep } from 'lodash'
|
||||
import LocalSettingIndicator from './local_setting_indicator.vue'
|
||||
import ModifiedIndicator from './modified_indicator.vue'
|
||||
|
||||
import { useInstanceStore } from 'src/stores/instance.js'
|
||||
import { useInterfaceStore } from 'src/stores/interface.js'
|
||||
import { useLocalConfigStore } from 'src/stores/local_config.js'
|
||||
import { useMergedConfigStore } from 'src/stores/merged_config.js'
|
||||
import { useSyncConfigStore } from 'src/stores/sync_config.js'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ModifiedIndicator,
|
||||
DraftButtons,
|
||||
ProfileSettingIndicator
|
||||
LocalSettingIndicator,
|
||||
},
|
||||
props: {
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: null
|
||||
default: null,
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
path: {
|
||||
type: [String, Array],
|
||||
required: false
|
||||
required: false,
|
||||
},
|
||||
showDescription: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
},
|
||||
descriptionPathOverride: {
|
||||
type: [String, Array],
|
||||
required: false,
|
||||
},
|
||||
suggestions: {
|
||||
type: [String, Array],
|
||||
required: false,
|
||||
},
|
||||
subgroup: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
default: false,
|
||||
},
|
||||
local: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
parentPath: {
|
||||
type: [String, Array]
|
||||
type: [String, Array],
|
||||
},
|
||||
parentInvert: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
default: false,
|
||||
},
|
||||
expert: {
|
||||
type: [Number, String],
|
||||
default: 0
|
||||
default: 0,
|
||||
},
|
||||
source: {
|
||||
type: String,
|
||||
default: undefined
|
||||
default: undefined,
|
||||
},
|
||||
hideDraftButtons: {
|
||||
// this is for the weird backend hybrid (Boolean|String or Boolean|Number) settings
|
||||
required: false,
|
||||
type: Boolean,
|
||||
},
|
||||
hideLabel: {
|
||||
type: Boolean,
|
||||
},
|
||||
hideDescription: {
|
||||
type: Boolean
|
||||
type: Boolean,
|
||||
},
|
||||
swapDescriptionAndLabel: {
|
||||
type: Boolean
|
||||
type: Boolean,
|
||||
},
|
||||
backendDescriptionPath: {
|
||||
type: [String, Array],
|
||||
},
|
||||
overrideBackendDescription: {
|
||||
type: Boolean
|
||||
type: Boolean,
|
||||
},
|
||||
overrideBackendDescriptionLabel: {
|
||||
type: Boolean
|
||||
type: [Boolean, String],
|
||||
},
|
||||
draftMode: {
|
||||
type: Boolean,
|
||||
default: undefined
|
||||
default: undefined,
|
||||
},
|
||||
timedApplyMode: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
inject: {
|
||||
defaultSource: {
|
||||
default: 'default'
|
||||
default: 'default',
|
||||
},
|
||||
defaultDraftMode: {
|
||||
default: false
|
||||
}
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
localDraft: null
|
||||
localDraft: null,
|
||||
}
|
||||
},
|
||||
created () {
|
||||
if (this.realDraftMode && (this.realSource !== 'admin' || this.path == null)) {
|
||||
this.draft = this.state
|
||||
created() {
|
||||
if (
|
||||
this.realDraftMode &&
|
||||
(this.realSource !== 'admin' || this.path == null)
|
||||
) {
|
||||
this.draft = cloneDeep(this.state)
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
draft: {
|
||||
get () {
|
||||
get() {
|
||||
if (this.realSource === 'admin' || this.path == null) {
|
||||
return get(this.$store.state.adminSettings.draft, this.canonPath)
|
||||
} else {
|
||||
return this.localDraft
|
||||
}
|
||||
},
|
||||
set (value) {
|
||||
set(value) {
|
||||
if (this.realSource === 'admin' || this.path == null) {
|
||||
this.$store.commit('updateAdminDraft', { path: this.canonPath, value })
|
||||
this.$store.commit('updateAdminDraft', {
|
||||
path: this.canonPath,
|
||||
value,
|
||||
})
|
||||
} else {
|
||||
this.localDraft = value
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
state () {
|
||||
state() {
|
||||
if (this.path == null) {
|
||||
return this.modelValue
|
||||
}
|
||||
|
|
@ -104,98 +152,189 @@ export default {
|
|||
return value
|
||||
}
|
||||
},
|
||||
visibleState () {
|
||||
visibleState() {
|
||||
return this.realDraftMode ? this.draft : this.state
|
||||
},
|
||||
realSource () {
|
||||
realSource() {
|
||||
return this.source || this.defaultSource
|
||||
},
|
||||
realDraftMode () {
|
||||
return typeof this.draftMode === 'undefined' ? this.defaultDraftMode : this.draftMode
|
||||
realDraftMode() {
|
||||
return typeof this.draftMode === 'undefined'
|
||||
? this.defaultDraftMode
|
||||
: this.draftMode
|
||||
},
|
||||
backendDescription () {
|
||||
return get(this.$store.state.adminSettings.descriptions, this.path)
|
||||
backendDescription() {
|
||||
return get(
|
||||
this.$store.state.adminSettings.descriptions,
|
||||
this.descriptionPath,
|
||||
)
|
||||
},
|
||||
backendDescriptionLabel () {
|
||||
backendDescriptionLabel() {
|
||||
if (this.realSource !== 'admin') return ''
|
||||
if (
|
||||
this.overrideBackendDescriptionLabel !== '' &&
|
||||
typeof this.overrideBackendDescriptionLabel === 'string'
|
||||
) {
|
||||
return this.overrideBackendDescriptionLabel
|
||||
}
|
||||
if (!this.backendDescription || this.overrideBackendDescriptionLabel) {
|
||||
return this.$t([
|
||||
'admin_dash',
|
||||
'temp_overrides',
|
||||
...this.canonPath.map(p => p.replace(/\./g, '_DOT_')),
|
||||
'label'
|
||||
].join('.'))
|
||||
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 () {
|
||||
backendDescriptionDescription() {
|
||||
if (this.description) return this.description
|
||||
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('.'))
|
||||
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
|
||||
backendDescriptionSuggestions() {
|
||||
return this.backendDescription?.suggestions || this.suggestions
|
||||
},
|
||||
shouldBeDisabled () {
|
||||
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)
|
||||
let parentValue = null
|
||||
if (this.parentPath !== undefined && this.realSource === 'admin') {
|
||||
if (this.realDraftMode) {
|
||||
parentValue = get(
|
||||
this.$store.state.adminSettings.draft,
|
||||
this.parentPath,
|
||||
)
|
||||
} else {
|
||||
parentValue = get(this.configSource, this.parentPath)
|
||||
}
|
||||
}
|
||||
return (
|
||||
this.disabled ||
|
||||
(parentValue !== null
|
||||
? this.parentInvert
|
||||
? parentValue
|
||||
: !parentValue
|
||||
: false)
|
||||
)
|
||||
},
|
||||
configSource () {
|
||||
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
|
||||
return useMergedConfigStore().mergedConfig
|
||||
}
|
||||
},
|
||||
configSink () {
|
||||
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 })
|
||||
return (k, v) =>
|
||||
this.$store.dispatch('setProfileOption', { name: k, value: v })
|
||||
case 'admin':
|
||||
return (k, v) => this.$store.dispatch('pushAdminSetting', { path: k, value: v })
|
||||
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 })
|
||||
return (readPath, value) => {
|
||||
const writePath = `${readPath}`
|
||||
|
||||
if (!this.timedApplyMode) {
|
||||
if (this.local) {
|
||||
useLocalConfigStore().set({
|
||||
path: writePath,
|
||||
value,
|
||||
})
|
||||
} else {
|
||||
useSyncConfigStore().setSimplePrefAndSave({
|
||||
path: writePath,
|
||||
value,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
if (useInterfaceStore().temporaryChangesTimeoutId !== null) {
|
||||
console.error("Can't track more than one temporary change")
|
||||
return
|
||||
}
|
||||
|
||||
const oldValue = get(this.configSource, readPath)
|
||||
|
||||
if (this.local) {
|
||||
useLocalConfigStore().setTemporarily({ path: writePath, value })
|
||||
} else {
|
||||
useSyncConfigStore().setPreference({ path: writePath, value })
|
||||
}
|
||||
|
||||
const confirm = () => {
|
||||
if (this.local) {
|
||||
useLocalConfigStore().set({ path: writePath, value })
|
||||
} else {
|
||||
useSyncConfigStore().pushSyncConfig()
|
||||
}
|
||||
useInterfaceStore().clearTemporaryChanges()
|
||||
}
|
||||
|
||||
const revert = () => {
|
||||
if (this.local) {
|
||||
useLocalConfigStore().unsetTemporarily({
|
||||
path: writePath,
|
||||
value,
|
||||
})
|
||||
} else {
|
||||
useSyncConfigStore().setPreference({
|
||||
path: writePath,
|
||||
value: oldValue,
|
||||
})
|
||||
}
|
||||
useInterfaceStore().clearTemporaryChanges()
|
||||
}
|
||||
|
||||
useInterfaceStore().setTemporaryChanges({ confirm, revert })
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
defaultState () {
|
||||
defaultState() {
|
||||
switch (this.realSource) {
|
||||
case 'profile':
|
||||
return {}
|
||||
default:
|
||||
return get(this.$store.getters.defaultConfig, this.path)
|
||||
default: {
|
||||
return get(useMergedConfigStore().mergedConfigDefault, this.path)
|
||||
}
|
||||
}
|
||||
},
|
||||
isProfileSetting () {
|
||||
isProfileSetting() {
|
||||
return this.realSource === 'profile'
|
||||
},
|
||||
isChanged () {
|
||||
isLocalSetting() {
|
||||
return this.local
|
||||
},
|
||||
isChanged() {
|
||||
if (this.path == null) return false
|
||||
switch (this.realSource) {
|
||||
case 'profile':
|
||||
|
|
@ -205,57 +344,85 @@ export default {
|
|||
return this.state !== this.defaultState
|
||||
}
|
||||
},
|
||||
canonPath () {
|
||||
canonPath() {
|
||||
if (this.path == null) return null
|
||||
return Array.isArray(this.path) ? this.path : this.path.split('.')
|
||||
},
|
||||
isDirty () {
|
||||
descriptionPath() {
|
||||
if (this.path == null) return null
|
||||
if (this.descriptionPathOverride) return this.descriptionPathOverride
|
||||
const path = Array.isArray(this.path) ? this.path : this.path.split('.')
|
||||
if (this.subgroup) {
|
||||
return [
|
||||
...path.slice(0, path.length - 1),
|
||||
':subgroup,' + this.subgroup,
|
||||
...path.slice(path.length - 1),
|
||||
]
|
||||
}
|
||||
return path
|
||||
},
|
||||
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
|
||||
return this.realDraftMode && !isEqual(this.draft, this.state)
|
||||
}
|
||||
},
|
||||
canHardReset () {
|
||||
return this.realSource === 'admin' && this.$store.state.adminSettings.modifiedPaths &&
|
||||
this.$store.state.adminSettings.modifiedPaths.has(this.canonPath.join(' -> '))
|
||||
canHardReset() {
|
||||
return (
|
||||
this.realSource === 'admin' &&
|
||||
this.$store.state.adminSettings.modifiedPaths?.has(
|
||||
this.canonPath.join(' -> '),
|
||||
)
|
||||
)
|
||||
},
|
||||
matchesExpertLevel() {
|
||||
const settingExpertLevel = this.expert || 0
|
||||
const userToggleExpert =
|
||||
useMergedConfigStore().mergedConfig.expertLevel || 0
|
||||
|
||||
return settingExpertLevel <= userToggleExpert
|
||||
},
|
||||
matchesExpertLevel () {
|
||||
return (this.expert || 0) <= this.$store.state.config.expertLevel > 0
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getValue (e) {
|
||||
getValue(e) {
|
||||
return e.target.value
|
||||
},
|
||||
update (e) {
|
||||
update(e) {
|
||||
if (this.realDraftMode) {
|
||||
this.draft = this.getValue(e)
|
||||
} else {
|
||||
this.configSink(this.path, this.getValue(e))
|
||||
}
|
||||
},
|
||||
commitDraft () {
|
||||
commitDraft() {
|
||||
if (this.realDraftMode) {
|
||||
this.configSink(this.path, this.draft)
|
||||
}
|
||||
},
|
||||
reset () {
|
||||
reset() {
|
||||
if (this.realDraftMode) {
|
||||
this.draft = cloneDeep(this.state)
|
||||
} else {
|
||||
set(this.$store.getters.mergedConfig, this.path, cloneDeep(this.defaultState))
|
||||
set(
|
||||
useMergedConfigStore().mergedConfig,
|
||||
this.path,
|
||||
cloneDeep(this.defaultState),
|
||||
)
|
||||
}
|
||||
},
|
||||
hardReset () {
|
||||
hardReset() {
|
||||
switch (this.realSource) {
|
||||
case 'admin':
|
||||
return this.$store.dispatch('resetAdminSetting', { path: this.path })
|
||||
.then(() => { this.draft = this.state })
|
||||
return this.$store
|
||||
.dispatch('resetAdminSetting', { path: this.path })
|
||||
.then(() => {
|
||||
this.draft = this.state
|
||||
})
|
||||
default:
|
||||
console.warn('Hard reset not implemented yet!')
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,19 +1,18 @@
|
|||
import { mapState as mapPiniaState } from 'pinia'
|
||||
import { mapState } from 'vuex'
|
||||
|
||||
import { useMergedConfigStore } from 'src/stores/merged_config.js'
|
||||
|
||||
const SharedComputedObject = () => ({
|
||||
user () {
|
||||
return this.$store.state.users.currentUser
|
||||
},
|
||||
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
|
||||
}
|
||||
...mapPiniaState(useMergedConfigStore, ['mergedConfig']),
|
||||
...mapPiniaState(useMergedConfigStore, {
|
||||
expertLevel: (store) => store.mergedConfig.expertLevel,
|
||||
}),
|
||||
...mapState({
|
||||
adminConfig: (state) => state.adminSettings.config,
|
||||
adminDraft: (state) => state.adminSettings.draft,
|
||||
user: (state) => state.users.currentUser,
|
||||
}),
|
||||
})
|
||||
|
||||
export default SharedComputedObject
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import Setting from './setting.js'
|
||||
|
||||
export default {
|
||||
...Setting
|
||||
...Setting,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,20 @@
|
|||
<template>
|
||||
<label
|
||||
<span
|
||||
v-if="matchesExpertLevel"
|
||||
class="StringSetting"
|
||||
class="StringSetting setting-item"
|
||||
>
|
||||
<label
|
||||
v-if="!hideLabel"
|
||||
:for="path"
|
||||
class="setting-label"
|
||||
:class="{ 'faint': shouldBeDisabled }"
|
||||
>
|
||||
<ModifiedIndicator
|
||||
:changed="isChanged"
|
||||
:onclick="reset"
|
||||
/>
|
||||
<LocalSettingIndicator :is-local="isLocalSetting" />
|
||||
{{ ' ' }}
|
||||
<template v-if="backendDescriptionLabel">
|
||||
{{ backendDescriptionLabel + ' ' }}
|
||||
</template>
|
||||
|
|
@ -16,21 +23,17 @@
|
|||
</template>
|
||||
<slot v-else />
|
||||
</label>
|
||||
{{ ' ' }}
|
||||
<input
|
||||
:id="path"
|
||||
class="input string-input"
|
||||
class="setting-control input string-input"
|
||||
:class="{ disabled: shouldBeDisabled }"
|
||||
:disabled="shouldBeDisabled"
|
||||
:placeholder="backendDescriptionSuggestions"
|
||||
:value="realDraftMode ? draft : state"
|
||||
@change="update"
|
||||
>
|
||||
{{ ' ' }}
|
||||
<ModifiedIndicator
|
||||
:changed="isChanged"
|
||||
:onclick="reset"
|
||||
/>
|
||||
<ProfileSettingIndicator :is-profile="isProfileSetting" />
|
||||
<DraftButtons />
|
||||
<DraftButtons v-if="!hideDraftButtons" />
|
||||
<p
|
||||
v-if="backendDescriptionDescription"
|
||||
class="setting-description"
|
||||
|
|
@ -38,7 +41,7 @@
|
|||
>
|
||||
{{ backendDescriptionDescription + ' ' }}
|
||||
</p>
|
||||
</label>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script src="./string_setting.js"></script>
|
||||
|
|
|
|||
16
src/components/settings_modal/helpers/tuple_setting.js
Normal file
16
src/components/settings_modal/helpers/tuple_setting.js
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import Setting from './setting.js'
|
||||
|
||||
export default {
|
||||
...Setting,
|
||||
methods: {
|
||||
...Setting.methods,
|
||||
getValue({ e, side }) {
|
||||
const [a, b] = this.visibleState || []
|
||||
if (side === 0) {
|
||||
return { tuple: [e.target.value, b] }
|
||||
} else {
|
||||
return { tuple: [a, e.target.value] }
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
58
src/components/settings_modal/helpers/tuple_setting.vue
Normal file
58
src/components/settings_modal/helpers/tuple_setting.vue
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
<template>
|
||||
<span
|
||||
v-if="matchesExpertLevel"
|
||||
class="setting-item"
|
||||
>
|
||||
<label
|
||||
v-if="!hideLabel"
|
||||
:for="path"
|
||||
class="setting-label"
|
||||
:class="{ 'faint': shouldBeDisabled }"
|
||||
>
|
||||
<ModifiedIndicator
|
||||
:changed="isChanged"
|
||||
:onclick="reset"
|
||||
/>
|
||||
<LocalSettingIndicator :is-local="isLocalSetting" />
|
||||
{{ ' ' }}
|
||||
<template v-if="backendDescriptionLabel">
|
||||
{{ backendDescriptionLabel + ' ' }}
|
||||
</template>
|
||||
<template v-else-if="source === 'admin'">
|
||||
MISSING LABEL FOR {{ path }}
|
||||
</template>
|
||||
<slot v-else />
|
||||
</label>
|
||||
<span class="setting-control">
|
||||
<input
|
||||
:id="path"
|
||||
class="input string-input"
|
||||
:class="{ disabled: shouldBeDisabled }"
|
||||
:disabled="shouldBeDisabled"
|
||||
:placeholder="backendDescriptionSuggestions?.[0]?.[0]"
|
||||
:value="visibleState?.tuple?.[0]"
|
||||
@change="e => update({ e, side: 0 })"
|
||||
>
|
||||
{{ ' ' }}
|
||||
<input
|
||||
:id="path"
|
||||
class="input string-input"
|
||||
:class="{ disabled: shouldBeDisabled }"
|
||||
:disabled="shouldBeDisabled"
|
||||
:placeholder="backendDescriptionSuggestions?.[0]?.[1]"
|
||||
:value="visibleState?.tuple?.[1]"
|
||||
@change="e => update({ e, side: 1 })"
|
||||
>
|
||||
</span>
|
||||
<DraftButtons v-if="!hideDraftButtons" />
|
||||
<p
|
||||
v-if="backendDescriptionDescription"
|
||||
class="setting-description"
|
||||
:class="{ 'faint': shouldBeDisabled }"
|
||||
>
|
||||
{{ backendDescriptionDescription + ' ' }}
|
||||
</p>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script src="./tuple_setting.js"></script>
|
||||
|
|
@ -1,7 +1,23 @@
|
|||
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 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']
|
||||
|
||||
|
|
@ -9,47 +25,51 @@ export default {
|
|||
...Setting,
|
||||
components: {
|
||||
...Setting.components,
|
||||
Select
|
||||
Select,
|
||||
},
|
||||
props: {
|
||||
...Setting.props,
|
||||
min: Number,
|
||||
units: {
|
||||
type: Array,
|
||||
default: () => allCssUnits
|
||||
default: () => allCssUnits,
|
||||
},
|
||||
unitSet: {
|
||||
type: String,
|
||||
default: 'none'
|
||||
default: 'none',
|
||||
},
|
||||
step: {
|
||||
type: Number,
|
||||
default: 1
|
||||
default: 1,
|
||||
},
|
||||
resetDefault: {
|
||||
type: Object,
|
||||
default: null
|
||||
}
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...Setting.computed,
|
||||
stateUnit () {
|
||||
return typeof this.state === 'string' ? this.state.replace(/[0-9,.]+/, '') : ''
|
||||
stateUnit() {
|
||||
return typeof this.state === 'string'
|
||||
? this.state.replace(/[0-9,.]+/, '')
|
||||
: ''
|
||||
},
|
||||
stateValue() {
|
||||
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) {
|
||||
getUnitString(value) {
|
||||
if (this.unitSet === 'none') return value
|
||||
return this.$t(['settings', 'units', this.unitSet, value].join('.'))
|
||||
},
|
||||
updateValue (e) {
|
||||
updateValue(e) {
|
||||
this.configSink(this.path, parseFloat(e.target.value) + this.stateUnit)
|
||||
},
|
||||
updateUnit (e) {
|
||||
updateUnit(e) {
|
||||
let value = this.stateValue
|
||||
const newUnit = e.target.value
|
||||
if (this.resetDefault) {
|
||||
|
|
@ -59,6 +79,6 @@ export default {
|
|||
}
|
||||
}
|
||||
this.configSink(this.path, value + newUnit)
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,21 @@
|
|||
<template>
|
||||
<span
|
||||
v-if="matchesExpertLevel"
|
||||
class="UnitSetting"
|
||||
class="UnitSetting setting-item"
|
||||
>
|
||||
<label
|
||||
:for="path"
|
||||
class="size-label"
|
||||
class="setting-label size-label"
|
||||
>
|
||||
<LocalSettingIndicator :is-local="isLocalSetting" />
|
||||
<ModifiedIndicator
|
||||
:changed="isChanged"
|
||||
:onclick="reset"
|
||||
/>
|
||||
{{ ' ' }}
|
||||
<slot />
|
||||
</label>
|
||||
{{ ' ' }}
|
||||
<span class="no-break">
|
||||
<span class="no-break setting-control">
|
||||
<input
|
||||
:id="path"
|
||||
class="input number-input"
|
||||
|
|
@ -38,10 +43,6 @@
|
|||
</Select>
|
||||
</span>
|
||||
{{ ' ' }}
|
||||
<ModifiedIndicator
|
||||
:changed="isChanged"
|
||||
:onclick="reset"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
|
|
@ -50,7 +51,7 @@
|
|||
<style lang="scss">
|
||||
.UnitSetting {
|
||||
.no-break {
|
||||
display: inline-block;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.number-input {
|
||||
|
|
|
|||
209
src/components/settings_modal/helpers/vertical_tab_switcher.jsx
Normal file
209
src/components/settings_modal/helpers/vertical_tab_switcher.jsx
Normal file
|
|
@ -0,0 +1,209 @@
|
|||
// eslint-disable-next-line no-unused
|
||||
|
||||
import { throttle } from 'lodash'
|
||||
import { mapState as mapPiniaState, mapState } from 'pinia'
|
||||
import { Fragment, h } from 'vue'
|
||||
|
||||
import { FontAwesomeIcon as FAIcon } from '@fortawesome/vue-fontawesome'
|
||||
|
||||
import './vertical_tab_switcher.scss'
|
||||
|
||||
import { useInterfaceStore } from 'src/stores/interface.js'
|
||||
|
||||
const findFirstUsable = (slots) => slots.findIndex((_) => _.props)
|
||||
|
||||
export default {
|
||||
name: 'VerticalTabSwitcher',
|
||||
props: {
|
||||
renderOnlyFocused: {
|
||||
required: false,
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
onSwitch: {
|
||||
required: false,
|
||||
type: Function,
|
||||
default: undefined,
|
||||
},
|
||||
activeTab: {
|
||||
required: false,
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
bodyScrollLock: {
|
||||
required: false,
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
active: findFirstUsable(this.slots()),
|
||||
resizeHandler: null,
|
||||
navSide: 'tabs',
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
activeIndex() {
|
||||
// In case of controlled component
|
||||
if (this.activeTab) {
|
||||
return this.slots().findIndex(
|
||||
(slot) => slot && slot.props && this.activeTab === slot.props.key,
|
||||
)
|
||||
} else {
|
||||
return this.active
|
||||
}
|
||||
},
|
||||
isActive() {
|
||||
return (tabName) => {
|
||||
const isWanted = (slot) =>
|
||||
slot.props && slot.props['data-tab-name'] === tabName
|
||||
return this.$slots.default().findIndex(isWanted) === this.activeIndex
|
||||
}
|
||||
},
|
||||
...mapPiniaState(useInterfaceStore, {
|
||||
mobileLayout: (store) => store.layoutType === 'mobile',
|
||||
}),
|
||||
},
|
||||
beforeUpdate() {
|
||||
const currentSlot = this.slots()[this.active]
|
||||
if (!currentSlot.props) {
|
||||
this.active = findFirstUsable(this.slots())
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
clickTab(index) {
|
||||
return (e) => {
|
||||
e.preventDefault()
|
||||
this.setTab(index)
|
||||
}
|
||||
},
|
||||
setTab(index) {
|
||||
if (typeof this.onSwitch === 'function') {
|
||||
this.onSwitch.call(null, this.slots()[index].key)
|
||||
}
|
||||
this.active = index
|
||||
this.changeNavSide('content')
|
||||
},
|
||||
changeNavSide(side) {
|
||||
if (this.navSide !== side) {
|
||||
this.navSide = side
|
||||
}
|
||||
},
|
||||
// DO NOT put it to computed, it doesn't work (caching?)
|
||||
slots() {
|
||||
if (this.$slots.default()[0].type === Fragment) {
|
||||
return this.$slots.default()[0].children
|
||||
}
|
||||
return this.$slots.default()
|
||||
},
|
||||
},
|
||||
render() {
|
||||
const tabs = this.slots().map((slot, index) => {
|
||||
const props = slot.props
|
||||
if (!props) return
|
||||
const classesTab = ['vertical-tab', 'menu-item']
|
||||
if (
|
||||
this.activeIndex === index &&
|
||||
useInterfaceStore().layoutType !== 'mobile'
|
||||
) {
|
||||
classesTab.push('-active')
|
||||
}
|
||||
return (
|
||||
<button
|
||||
disabled={props.disabled}
|
||||
onClick={this.clickTab(index)}
|
||||
class={classesTab.join(' ')}
|
||||
type="button"
|
||||
role="tab"
|
||||
title={props.label}
|
||||
>
|
||||
{!props.icon ? (
|
||||
''
|
||||
) : (
|
||||
<FAIcon class="tab-icon" size="1x" fixed-width icon={props.icon} />
|
||||
)}
|
||||
<span class="text">{props.label}</span>
|
||||
</button>
|
||||
)
|
||||
})
|
||||
|
||||
const contents = this.slots().map((slot, index) => {
|
||||
const props = slot.props
|
||||
if (!props) return
|
||||
const active = this.activeIndex === index
|
||||
|
||||
let delayRender = slot.props['delay-render']
|
||||
if (delayRender && active) {
|
||||
slot.props['delay-render'] = false
|
||||
delayRender = false
|
||||
}
|
||||
const renderSlot =
|
||||
!delayRender && (!this.renderOnlyFocused || active) ? slot : ''
|
||||
|
||||
const headerClasses = ['tab-content-label']
|
||||
const header = (
|
||||
<h2 class={headerClasses}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => this.changeNavSide('tabs')}
|
||||
title={this.$t('nav.back')}
|
||||
class="button-unstyled"
|
||||
>
|
||||
<FAIcon size="lg" class="back-button-icon" icon="chevron-left" />
|
||||
</button>
|
||||
{props.label}
|
||||
</h2>
|
||||
)
|
||||
|
||||
const wrapperClasses = [
|
||||
'tab-content-wrapper',
|
||||
active ? '-active' : '-hidden',
|
||||
]
|
||||
const contentClasses = ['tab-content']
|
||||
if (props['full-width'] || props['full-width'] === '') {
|
||||
contentClasses.push('-full-width')
|
||||
wrapperClasses.push('-full-width')
|
||||
}
|
||||
if (props['full-height'] || props['full-width'] === '') {
|
||||
contentClasses.push('-full-height')
|
||||
wrapperClasses.push('-full-height')
|
||||
}
|
||||
return (
|
||||
<div class={wrapperClasses}>
|
||||
<div class="tab-mobile-header">{header}</div>
|
||||
<div class="tab-slot-wrapper">
|
||||
<div class={contentClasses}>{renderSlot}</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
const rootClasses = ['vertical-tab-switcher']
|
||||
if (useInterfaceStore().layoutType === 'mobile') {
|
||||
rootClasses.push('-mobile')
|
||||
}
|
||||
|
||||
if (this.navSide === 'tabs') {
|
||||
rootClasses.push('-nav-tabs')
|
||||
} else {
|
||||
rootClasses.push('-nav-contents')
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref="root" class={rootClasses.join(' ')}>
|
||||
<div class="tabs" role="tablist" ref="nav">
|
||||
{tabs}
|
||||
</div>
|
||||
<div
|
||||
role="tabpanel"
|
||||
class="contents"
|
||||
v-body-scroll-lock={this.bodyScrollLock}
|
||||
ref="contents"
|
||||
>
|
||||
{contents}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}
|
||||
184
src/components/settings_modal/helpers/vertical_tab_switcher.scss
Normal file
184
src/components/settings_modal/helpers/vertical_tab_switcher.scss
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
.vertical-tab-switcher {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
container-type: inline-size;
|
||||
|
||||
> .tabs {
|
||||
flex: 0 0 15em;
|
||||
flex-direction: column;
|
||||
overflow: hidden auto;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
width: 15em;
|
||||
min-width: 15em;
|
||||
border-right: 1px solid;
|
||||
border-right-color: var(--border);
|
||||
box-sizing: border-box;
|
||||
|
||||
> .menu-item {
|
||||
padding: 0.5em 1em;
|
||||
|
||||
.tab-icon {
|
||||
vertical-align: middle;
|
||||
margin-right: 0.75em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .contents {
|
||||
.tab-content-label {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
border-bottom: 1px solid var(--border);
|
||||
display: none;
|
||||
|
||||
button {
|
||||
box-sizing: border-box;
|
||||
padding: 0.5em;
|
||||
}
|
||||
}
|
||||
|
||||
.tab-slot-wrapper {
|
||||
flex: 1 1 auto;
|
||||
height: 100%;
|
||||
padding: 0 1em;
|
||||
overflow-y: auto;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(1em, 1fr) minmax(min-content, 45em) minmax(1em, 1fr);
|
||||
grid-template-areas: ". content .";
|
||||
flex-direction: column;
|
||||
|
||||
.tab-content {
|
||||
grid-area: content;
|
||||
|
||||
&.-full-width {
|
||||
grid-column: 1 / 4;
|
||||
}
|
||||
|
||||
&.-full-height {
|
||||
> * {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tab-content-wrapper {
|
||||
flex: 1 1 auto;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&.-hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.-mobile {
|
||||
> .contents {
|
||||
.tab-content-label {
|
||||
display: block
|
||||
}
|
||||
}
|
||||
|
||||
&.-nav-contents {
|
||||
> .contents {
|
||||
display: block;
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
}
|
||||
|
||||
> .tabs {
|
||||
display: none;
|
||||
flex-grow: 0;
|
||||
flex-shrink: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&.-nav-tabs {
|
||||
> .tabs {
|
||||
display: block;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
> .contents {
|
||||
display: none;
|
||||
flex-grow: 0;
|
||||
flex-shrink: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.tab-slot-wrapper {
|
||||
grid-template-columns: 0 minmax(min-content, 45em) 0;
|
||||
}
|
||||
}
|
||||
|
||||
@supports (container-type: inline-size) {
|
||||
&,
|
||||
&.-mobile {
|
||||
&.-nav-contents,
|
||||
&.-nav-tabs {
|
||||
/* I THINK it's a false positive and eslint doesn't understand the @-rule */
|
||||
/* stylelint-disable no-descending-specificity */
|
||||
> .contents {
|
||||
display: block;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
> .tabs {
|
||||
display: block;
|
||||
flex-grow: 0;
|
||||
}
|
||||
/* stylelint-enable no-descending-specificity */
|
||||
}
|
||||
}
|
||||
|
||||
@container (width < 50em) {
|
||||
> .contents {
|
||||
.tab-content-label {
|
||||
display: block
|
||||
}
|
||||
}
|
||||
|
||||
&.-mobile {
|
||||
> .contents {
|
||||
.tab-content-label {
|
||||
display: block
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&,
|
||||
&.-mobile {
|
||||
&.-nav-contents {
|
||||
> .contents {
|
||||
display: block;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
> .tabs {
|
||||
display: none;
|
||||
flex-grow: 0;
|
||||
flex-shrink: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&.-nav-tabs {
|
||||
/* stylelint-disable no-descending-specificity */
|
||||
> .tabs {
|
||||
display: block;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
> .contents {
|
||||
display: none;
|
||||
flex-grow: 0;
|
||||
flex-shrink: 1;
|
||||
}
|
||||
/* stylelint-enable no-descending-specificity */
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,27 +1,38 @@
|
|||
import { cloneDeep, isEqual } from 'lodash'
|
||||
import { mapActions, mapState } from 'pinia'
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
|
||||
import AsyncComponentError from 'src/components/async_component_error/async_component_error.vue'
|
||||
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||
import Modal from 'src/components/modal/modal.vue'
|
||||
import PanelLoading from 'src/components/panel_loading/panel_loading.vue'
|
||||
import AsyncComponentError from 'src/components/async_component_error/async_component_error.vue'
|
||||
import getResettableAsyncComponent from 'src/services/resettable_async_component.js'
|
||||
import Popover from '../popover/popover.vue'
|
||||
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||
import ConfirmModal from 'src/components/confirm_modal/confirm_modal.vue'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { cloneDeep, isEqual } from 'lodash'
|
||||
import { mapState, mapActions } from 'pinia'
|
||||
import Popover from 'src/components/popover/popover.vue'
|
||||
|
||||
import { useInterfaceStore } from 'src/stores/interface.js'
|
||||
import { useLocalConfigStore } from 'src/stores/local_config.js'
|
||||
import { useMergedConfigStore } from 'src/stores/merged_config.js'
|
||||
import { useSyncConfigStore } from 'src/stores/sync_config.js'
|
||||
|
||||
import {
|
||||
LOCAL_ONLY_KEYS,
|
||||
ROOT_CONFIG,
|
||||
ROOT_CONFIG_DEFINITIONS,
|
||||
validateSetting,
|
||||
} from 'src/modules/default_config_state.js'
|
||||
import {
|
||||
newExporter,
|
||||
newImporter,
|
||||
newExporter
|
||||
} from 'src/services/export_import/export_import.js'
|
||||
import getResettableAsyncComponent from 'src/services/resettable_async_component.js'
|
||||
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { faWindowMinimize } from '@fortawesome/free-regular-svg-icons'
|
||||
import {
|
||||
faTimes,
|
||||
faFileUpload,
|
||||
faChevronDown,
|
||||
faFileDownload,
|
||||
faChevronDown
|
||||
faFileUpload,
|
||||
faTimes,
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import {
|
||||
faWindowMinimize
|
||||
} from '@fortawesome/free-regular-svg-icons'
|
||||
import { useInterfaceStore } from 'src/stores/interface'
|
||||
|
||||
const PLEROMAFE_SETTINGS_MAJOR_VERSION = 1
|
||||
const PLEROMAFE_SETTINGS_MINOR_VERSION = 0
|
||||
|
|
@ -31,60 +42,63 @@ library.add(
|
|||
faWindowMinimize,
|
||||
faFileUpload,
|
||||
faFileDownload,
|
||||
faChevronDown
|
||||
faChevronDown,
|
||||
)
|
||||
|
||||
const SettingsModal = {
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
dataImporter: newImporter({
|
||||
validator: this.importValidator,
|
||||
onImport: this.onImport,
|
||||
onImportFailure: this.onImportFailure
|
||||
onImportFailure: this.onImportFailure,
|
||||
}),
|
||||
dataThemeExporter: newExporter({
|
||||
filename: 'pleromafe_settings.full',
|
||||
getExportedObject: () => this.generateExport(true)
|
||||
getExportedObject: () => this.generateExport(true),
|
||||
}),
|
||||
dataExporter: newExporter({
|
||||
filename: 'pleromafe_settings',
|
||||
getExportedObject: () => this.generateExport()
|
||||
})
|
||||
getExportedObject: () => this.generateExport(),
|
||||
}),
|
||||
}
|
||||
},
|
||||
components: {
|
||||
Modal,
|
||||
Popover,
|
||||
Checkbox,
|
||||
ConfirmModal,
|
||||
ConfirmModal: defineAsyncComponent(
|
||||
() => import('src/components/confirm_modal/confirm_modal.vue'),
|
||||
),
|
||||
|
||||
SettingsModalUserContent: getResettableAsyncComponent(
|
||||
() => import('./settings_modal_user_content.vue'),
|
||||
{
|
||||
loadingComponent: PanelLoading,
|
||||
errorComponent: AsyncComponentError,
|
||||
delay: 0
|
||||
}
|
||||
delay: 0,
|
||||
},
|
||||
),
|
||||
SettingsModalAdminContent: getResettableAsyncComponent(
|
||||
() => import('./settings_modal_admin_content.vue'),
|
||||
{
|
||||
loadingComponent: PanelLoading,
|
||||
errorComponent: AsyncComponentError,
|
||||
delay: 0
|
||||
}
|
||||
)
|
||||
delay: 0,
|
||||
},
|
||||
),
|
||||
},
|
||||
methods: {
|
||||
closeModal () {
|
||||
closeModal() {
|
||||
useInterfaceStore().closeSettingsModal()
|
||||
},
|
||||
peekModal () {
|
||||
peekModal() {
|
||||
useInterfaceStore().togglePeekSettingsModal()
|
||||
},
|
||||
importValidator (data) {
|
||||
importValidator(data) {
|
||||
if (!Array.isArray(data._pleroma_settings_version)) {
|
||||
return {
|
||||
messageKey: 'settings.file_import_export.invalid_file'
|
||||
messageKey: 'settings.file_import_export.invalid_file',
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -95,8 +109,8 @@ const SettingsModal = {
|
|||
messageKey: 'settings.file_export_import.errors.file_too_new',
|
||||
messageArgs: {
|
||||
fileMajor: major,
|
||||
feMajor: PLEROMAFE_SETTINGS_MAJOR_VERSION
|
||||
}
|
||||
feMajor: PLEROMAFE_SETTINGS_MAJOR_VERSION,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -105,94 +119,157 @@ const SettingsModal = {
|
|||
messageKey: 'settings.file_export_import.errors.file_too_old',
|
||||
messageArgs: {
|
||||
fileMajor: major,
|
||||
feMajor: PLEROMAFE_SETTINGS_MAJOR_VERSION
|
||||
}
|
||||
feMajor: PLEROMAFE_SETTINGS_MAJOR_VERSION,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if (minor > PLEROMAFE_SETTINGS_MINOR_VERSION) {
|
||||
useInterfaceStore().pushGlobalNotice({
|
||||
level: 'warning',
|
||||
messageKey: 'settings.file_export_import.errors.file_slightly_new'
|
||||
messageKey: 'settings.file_export_import.errors.file_slightly_new',
|
||||
})
|
||||
}
|
||||
|
||||
return true
|
||||
},
|
||||
onImportFailure (result) {
|
||||
onImportFailure(result) {
|
||||
if (result.error) {
|
||||
useInterfaceStore().pushGlobalNotice({ messageKey: 'settings.invalid_settings_imported', level: 'error' })
|
||||
useInterfaceStore().pushGlobalNotice({
|
||||
messageKey: 'settings.invalid_settings_imported',
|
||||
level: 'error',
|
||||
})
|
||||
} else {
|
||||
useInterfaceStore().pushGlobalNotice({ ...result.validationResult, level: 'error' })
|
||||
useInterfaceStore().pushGlobalNotice({
|
||||
...result.validationResult,
|
||||
level: 'error',
|
||||
})
|
||||
}
|
||||
},
|
||||
onImport (data) {
|
||||
if (data) { this.$store.dispatch('loadSettings', data) }
|
||||
onImport(input) {
|
||||
if (!input) return
|
||||
const { _pleroma_settings_version, ...data } = input
|
||||
|
||||
Object.entries(data).forEach(([path, value]) => {
|
||||
const definition = ROOT_CONFIG_DEFINITIONS[path]
|
||||
|
||||
const finalValue = validateSetting({
|
||||
path,
|
||||
value,
|
||||
definition,
|
||||
throwError: false,
|
||||
defaultState: ROOT_CONFIG,
|
||||
})
|
||||
|
||||
if (finalValue === undefined) return
|
||||
|
||||
if (LOCAL_ONLY_KEYS.has(path)) {
|
||||
useLocalConfigStore().set({ path, value: finalValue })
|
||||
} else {
|
||||
if (path.startsWith('muteFilters')) {
|
||||
Object.keys(
|
||||
useMergedConfigStore().mergedConfig.muteFilters,
|
||||
).forEach((key) => {
|
||||
useSyncConfigStore().unsetPreference({
|
||||
path: `simple.${path}.${key}`,
|
||||
})
|
||||
})
|
||||
|
||||
Object.entries(value).forEach(([key, filter]) => {
|
||||
useSyncConfigStore().setPreference({
|
||||
path: `simple.${path}.${key}`,
|
||||
value: filter,
|
||||
})
|
||||
})
|
||||
} else {
|
||||
if (finalValue !== undefined) {
|
||||
useSyncConfigStore().setPreference({
|
||||
path: `simple.${path}`,
|
||||
value: finalValue,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
useSyncConfigStore().pushSyncConfig()
|
||||
},
|
||||
restore () {
|
||||
restore() {
|
||||
this.dataImporter.importData()
|
||||
},
|
||||
backup () {
|
||||
backup() {
|
||||
this.dataExporter.exportData()
|
||||
},
|
||||
backupWithTheme () {
|
||||
backupWithTheme() {
|
||||
this.dataThemeExporter.exportData()
|
||||
},
|
||||
generateExport (theme = false) {
|
||||
const { config } = this.$store.state
|
||||
generateExport(theme = false) {
|
||||
const config = useMergedConfigStore().mergedConfigWithoutDefaults
|
||||
let sample = config
|
||||
if (!theme) {
|
||||
const ignoreList = new Set([
|
||||
'theme',
|
||||
'customTheme',
|
||||
'customThemeSource',
|
||||
'colors'
|
||||
'colors',
|
||||
'style',
|
||||
'styleCustomData',
|
||||
'palette',
|
||||
'paletteCustomData',
|
||||
'themeChecksum',
|
||||
])
|
||||
|
||||
sample = Object.fromEntries(
|
||||
Object
|
||||
.entries(sample)
|
||||
.filter(([key]) => !ignoreList.has(key))
|
||||
Object.entries(sample).filter(
|
||||
([key, value]) => !ignoreList.has(key) && value !== undefined,
|
||||
),
|
||||
)
|
||||
}
|
||||
const clone = cloneDeep(sample)
|
||||
clone._pleroma_settings_version = [
|
||||
PLEROMAFE_SETTINGS_MAJOR_VERSION,
|
||||
PLEROMAFE_SETTINGS_MINOR_VERSION
|
||||
PLEROMAFE_SETTINGS_MINOR_VERSION,
|
||||
]
|
||||
return clone
|
||||
},
|
||||
resetAdminDraft () {
|
||||
resetAdminDraft() {
|
||||
this.$store.commit('resetAdminDraft')
|
||||
},
|
||||
pushAdminDraft () {
|
||||
pushAdminDraft() {
|
||||
this.$store.dispatch('pushAdminDraft')
|
||||
},
|
||||
...mapActions(useInterfaceStore, ['temporaryChangesRevert', 'temporaryChangesConfirm'])
|
||||
...mapActions(useInterfaceStore, [
|
||||
'temporaryChangesRevert',
|
||||
'temporaryChangesConfirm',
|
||||
]),
|
||||
},
|
||||
computed: {
|
||||
...mapState(useInterfaceStore, {
|
||||
temporaryChangesTimeoutId: store => store.temporaryChangesTimeoutId,
|
||||
currentSaveStateNotice: store => store.settings.currentSaveStateNotice,
|
||||
modalActivated: store => store.settingsModalState !== 'hidden',
|
||||
modalMode: store => store.settingsModalMode,
|
||||
modalOpenedOnceUser: store => store.settingsModalLoadedUser,
|
||||
modalOpenedOnceAdmin: store => store.settingsModalLoadedAdmin,
|
||||
modalPeeked: store => store.settingsModalState === 'minimized'
|
||||
temporaryChangesCountdown: (store) => store.temporaryChangesCountdown,
|
||||
currentSaveStateNotice: (store) => store.settings.currentSaveStateNotice,
|
||||
modalActivated: (store) => store.settingsModalState !== 'hidden',
|
||||
modalMode: (store) => store.settingsModalMode,
|
||||
modalOpenedOnceUser: (store) => store.settingsModalLoadedUser,
|
||||
modalOpenedOnceAdmin: (store) => store.settingsModalLoadedAdmin,
|
||||
modalPeeked: (store) => store.settingsModalState === 'minimized',
|
||||
}),
|
||||
expertLevel: {
|
||||
get () {
|
||||
return this.$store.state.config.expertLevel > 0
|
||||
get() {
|
||||
return useMergedConfigStore().mergedConfig.expertLevel > 0
|
||||
},
|
||||
set(value) {
|
||||
useSyncConfigStore().setSimplePrefAndSave({
|
||||
path: 'expertLevel',
|
||||
value: value ? 1 : 0,
|
||||
})
|
||||
},
|
||||
set (value) {
|
||||
this.$store.dispatch('setOption', { name: 'expertLevel', value: value ? 1 : 0 })
|
||||
}
|
||||
},
|
||||
adminDraftAny () {
|
||||
adminDraftAny() {
|
||||
return !isEqual(
|
||||
this.$store.state.adminSettings.config,
|
||||
this.$store.state.adminSettings.draft
|
||||
this.$store.state.adminSettings.draft,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default SettingsModal
|
||||
|
|
|
|||
|
|
@ -1,48 +1,179 @@
|
|||
.settings-modal {
|
||||
overflow: hidden;
|
||||
|
||||
h4 {
|
||||
h2 {
|
||||
font-size: 1.3rem;
|
||||
font-weight: 500;
|
||||
margin-top: 1em;
|
||||
margin-bottom: 1em;
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 500;
|
||||
margin-top: 1em;
|
||||
margin-bottom: 0.5em;
|
||||
margin-right: 1em;
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding-bottom: 0.25em;
|
||||
box-sizing: border-box;
|
||||
padding-left: 0.5em;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 1.1rem;
|
||||
margin-top: 1em;
|
||||
margin-right: 1em;
|
||||
margin-bottom: 0.5em;
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 1rem;
|
||||
margin-left: 1em;
|
||||
margin-bottom: 0.25em;
|
||||
margin-top: 0.75em;
|
||||
}
|
||||
|
||||
p {
|
||||
line-height: 1.5;
|
||||
margin-left: 2em;
|
||||
}
|
||||
|
||||
.sidenote {
|
||||
margin-left: 5em;
|
||||
padding: 0.25em 1em;
|
||||
margin-top: 0.25em;
|
||||
}
|
||||
|
||||
.setting-description {
|
||||
margin-top: 0.2em;
|
||||
margin-bottom: 0;
|
||||
margin-left: 0;
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
.setting-item {
|
||||
display: grid;
|
||||
grid-template-areas:
|
||||
"label control"
|
||||
"label desc"
|
||||
". draft";
|
||||
grid-template-columns: 4fr 5fr;
|
||||
column-gap: 0.5em;
|
||||
align-items: baseline;
|
||||
padding: 0.5em 0;
|
||||
line-height: 1.5em;
|
||||
|
||||
.setting-label {
|
||||
grid-area: label;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.ModifiedIndicator,
|
||||
.LocalSettingIndicator {
|
||||
grid-area: indicator;
|
||||
}
|
||||
|
||||
.setting-control {
|
||||
grid-area: control;
|
||||
|
||||
&.textarea {
|
||||
align-self: baseline;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
&.checkbox {
|
||||
display: grid;
|
||||
grid-template-columns: subgrid;
|
||||
|
||||
.label {
|
||||
grid-area: label;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.checkbox-indicator {
|
||||
grid-area: control;
|
||||
height: 1.5em;
|
||||
line-height: 1.5em;
|
||||
align-self: baseline;
|
||||
}
|
||||
|
||||
.-mobile & {
|
||||
.label {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.setting-control.setting-label {
|
||||
grid-column: 1 / 3;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.setting-description {
|
||||
grid-area: desc;
|
||||
}
|
||||
|
||||
.DraftButtons {
|
||||
grid-area: draft;
|
||||
}
|
||||
}
|
||||
|
||||
.vertical-tab-switcher {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.setting-list,
|
||||
.option-list {
|
||||
list-style-type: none;
|
||||
padding-left: 2em;
|
||||
padding-left: 0;
|
||||
margin: 0;
|
||||
|
||||
&.suboptions {
|
||||
margin-left: 2em;
|
||||
border-top: 1px dotted var(--border);
|
||||
border-bottom: 1px dotted var(--border);
|
||||
}
|
||||
|
||||
|
||||
.btn:not(.dropdown-button) {
|
||||
padding: 0 2em;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.suboptions {
|
||||
margin-top: 0.3em;
|
||||
.btn-group {
|
||||
.button-default {
|
||||
flex: 0 1 auto;
|
||||
}
|
||||
}
|
||||
|
||||
&.two-column {
|
||||
column-count: 2;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
margin-left: 2em;
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding-bottom: 0.5em;
|
||||
margin-bottom: 1em;
|
||||
|
||||
.setting-item {
|
||||
grid-template-columns: 3fr 1fr;
|
||||
}
|
||||
|
||||
> li {
|
||||
margin: 0;
|
||||
break-inside: avoid;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.setting-description {
|
||||
margin-top: 0.2em;
|
||||
margin-bottom: 2em;
|
||||
font-size: 70%;
|
||||
}
|
||||
|
||||
.settings-modal-panel {
|
||||
overflow: hidden;
|
||||
transition: transform;
|
||||
transition-timing-function: ease-in-out;
|
||||
transition-duration: 300ms;
|
||||
width: 1000px;
|
||||
width: 70em;
|
||||
max-width: 90vw;
|
||||
height: 90vh;
|
||||
|
||||
|
|
@ -77,20 +208,64 @@
|
|||
}
|
||||
|
||||
&.-mobile {
|
||||
.setting-list,
|
||||
.tabs {
|
||||
.menu-item {
|
||||
font-size: 1.2em;
|
||||
padding-top: 0.75em;
|
||||
padding-bottom: 0.75em;
|
||||
}
|
||||
}
|
||||
|
||||
li {
|
||||
.sidenote {
|
||||
margin-left: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
/* stylelint-disable no-descending-specificity */
|
||||
.setting-item {
|
||||
grid-template-columns: 1fr min-content;
|
||||
column-gap: 0.5em;
|
||||
padding: 1em;
|
||||
align-items: center;
|
||||
|
||||
.setting-label {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
.label {
|
||||
text-align: left;
|
||||
order: 2;
|
||||
}
|
||||
|
||||
.checkbox-indicator {
|
||||
order: 1;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ul {
|
||||
padding: 0;
|
||||
|
||||
li:not(:first-child) {
|
||||
.setting-item {
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.setting-list:not(.suboptions),
|
||||
.option-list {
|
||||
padding-left: 0.25em;
|
||||
|
||||
> li {
|
||||
margin: 1em 0;
|
||||
line-height: 1.5em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
&.two-column {
|
||||
column-count: 1;
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.UnitSetting {
|
||||
padding-right: 0.5em;
|
||||
}
|
||||
}
|
||||
|
||||
&.peek {
|
||||
|
|
|
|||
|
|
@ -158,14 +158,14 @@
|
|||
</div>
|
||||
<teleport to="#modal">
|
||||
<ConfirmModal
|
||||
v-if="temporaryChangesTimeoutId"
|
||||
v-if="temporaryChangesCountdown > 0"
|
||||
:title="$t('settings.confirm_new_setting')"
|
||||
:cancel-text="$t('settings.revert')"
|
||||
:confirm-text="$t('settings.confirm')"
|
||||
@cancelled="temporaryChangesRevert"
|
||||
@accepted="temporaryChangesConfirm"
|
||||
>
|
||||
{{ $t('settings.confirm_new_question') }}
|
||||
{{ $t('settings.confirm_new_question_countdown', temporaryChangesCountdown) }}
|
||||
</ConfirmModal>
|
||||
</teleport>
|
||||
</Modal>
|
||||
|
|
|
|||
|
|
@ -1,83 +1,138 @@
|
|||
import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'
|
||||
|
||||
import InstanceTab from './admin_tabs/instance_tab.vue'
|
||||
import UsersTab from './admin_tabs/users_tab.vue'
|
||||
import LimitsTab from './admin_tabs/limits_tab.vue'
|
||||
import FrontendsTab from './admin_tabs/frontends_tab.vue'
|
||||
import AuthTab from './admin_tabs/auth_tab.vue'
|
||||
import EmojiTab from './admin_tabs/emoji_tab.vue'
|
||||
import { useInterfaceStore } from 'src/stores/interface'
|
||||
import FederationTab from './admin_tabs/federation_tab.vue'
|
||||
import FrontendsTab from './admin_tabs/frontends_tab.vue'
|
||||
import HTTPTab from './admin_tabs/http_tab.vue'
|
||||
import InstanceTab from './admin_tabs/instance_tab.vue'
|
||||
import JobQueuesTab from './admin_tabs/job_queues_tab.vue'
|
||||
import LimitsTab from './admin_tabs/limits_tab.vue'
|
||||
import LinksTab from './admin_tabs/links_tab.vue'
|
||||
import MailerTab from './admin_tabs/mailer_tab.vue'
|
||||
import MediaProxyTab from './admin_tabs/media_proxy_tab.vue'
|
||||
import MonitoringTab from './admin_tabs/monitoring_tab.vue'
|
||||
import OtherTab from './admin_tabs/other_tab.vue'
|
||||
import PostsTab from './admin_tabs/posts_tab.vue'
|
||||
import RatesTab from './admin_tabs/rates_tab.vue'
|
||||
import RegistrationsTab from './admin_tabs/registrations_tab.vue'
|
||||
import UploadsTab from './admin_tabs/uploads_tab.vue'
|
||||
import UsersTab from './admin_tabs/users_tab.vue'
|
||||
import VerticalTabSwitcher from './helpers/vertical_tab_switcher.jsx'
|
||||
|
||||
import { useInterfaceStore } from 'src/stores/interface.js'
|
||||
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faWrench,
|
||||
faHand,
|
||||
faLaptopCode,
|
||||
faPaintBrush,
|
||||
faBell,
|
||||
faChain,
|
||||
faChartLine,
|
||||
faCircleNodes,
|
||||
faDoorOpen,
|
||||
faDownload,
|
||||
faEllipsis,
|
||||
faEnvelope,
|
||||
faEyeSlash,
|
||||
faGauge,
|
||||
faGears,
|
||||
faGlobe,
|
||||
faHand,
|
||||
faInfo,
|
||||
faUser
|
||||
faKey,
|
||||
faLaptopCode,
|
||||
faMessage,
|
||||
faPaintBrush,
|
||||
faTowerBroadcast,
|
||||
faUpload,
|
||||
faUser,
|
||||
faWrench,
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(
|
||||
faWrench,
|
||||
faHand,
|
||||
faChain,
|
||||
faGlobe,
|
||||
faLaptopCode,
|
||||
faPaintBrush,
|
||||
faBell,
|
||||
faDownload,
|
||||
faEyeSlash,
|
||||
faInfo,
|
||||
faUser
|
||||
faUser,
|
||||
faTowerBroadcast,
|
||||
faEnvelope,
|
||||
faChartLine,
|
||||
faDoorOpen,
|
||||
faGears,
|
||||
faKey,
|
||||
faCircleNodes,
|
||||
faUpload,
|
||||
faMessage,
|
||||
faEllipsis,
|
||||
faGauge,
|
||||
)
|
||||
|
||||
const SettingsModalAdminContent = {
|
||||
components: {
|
||||
TabSwitcher,
|
||||
VerticalTabSwitcher,
|
||||
|
||||
InstanceTab,
|
||||
UsersTab,
|
||||
LimitsTab,
|
||||
RegistrationsTab,
|
||||
EmojiTab,
|
||||
FrontendsTab,
|
||||
EmojiTab
|
||||
FederationTab,
|
||||
MailerTab,
|
||||
UploadsTab,
|
||||
MediaProxyTab,
|
||||
LinksTab,
|
||||
JobQueuesTab,
|
||||
AuthTab,
|
||||
HTTPTab,
|
||||
MonitoringTab,
|
||||
RatesTab,
|
||||
OtherTab,
|
||||
PostsTab,
|
||||
},
|
||||
computed: {
|
||||
user () {
|
||||
user() {
|
||||
return this.$store.state.users.currentUser
|
||||
},
|
||||
isLoggedIn () {
|
||||
isLoggedIn() {
|
||||
return !!this.$store.state.users.currentUser
|
||||
},
|
||||
open () {
|
||||
open() {
|
||||
return useInterfaceStore().settingsModalState !== 'hidden'
|
||||
},
|
||||
bodyLock () {
|
||||
bodyLock() {
|
||||
return useInterfaceStore().settingsModalState === 'visible'
|
||||
},
|
||||
adminDbLoaded () {
|
||||
adminDbLoaded() {
|
||||
return this.$store.state.adminSettings.loaded
|
||||
},
|
||||
adminDescriptionsLoaded () {
|
||||
adminDescriptionsLoaded() {
|
||||
return this.$store.state.adminSettings.descriptions !== null
|
||||
},
|
||||
noDb () {
|
||||
noDb() {
|
||||
return this.$store.state.adminSettings.dbConfigEnabled === false
|
||||
}
|
||||
},
|
||||
},
|
||||
created () {
|
||||
created() {
|
||||
if (this.user.rights.admin) {
|
||||
this.$store.dispatch('loadAdminStuff')
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onOpen () {
|
||||
onOpen() {
|
||||
const targetTab = useInterfaceStore().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
|
||||
})
|
||||
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)
|
||||
}
|
||||
|
|
@ -85,16 +140,16 @@ const SettingsModalAdminContent = {
|
|||
// Clear the state of target tab, so that next time settings is opened
|
||||
// it doesn't force it.
|
||||
useInterfaceStore().clearSettingsModalTargetTab()
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
mounted() {
|
||||
this.onOpen()
|
||||
},
|
||||
watch: {
|
||||
open: function (value) {
|
||||
if (value) this.onOpen()
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default SettingsModalAdminContent
|
||||
|
|
|
|||
|
|
@ -1,48 +0,0 @@
|
|||
.settings_tab-switcher {
|
||||
height: 100%;
|
||||
|
||||
.setting-item {
|
||||
border-bottom: 2px solid var(--border);
|
||||
margin: 1em 1em 1.4em;
|
||||
padding-bottom: 1.4em;
|
||||
|
||||
> div,
|
||||
> label {
|
||||
display: block;
|
||||
margin-bottom: 0.5em;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.select-multiple {
|
||||
margin-top: 0.5em;
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
<template>
|
||||
<tab-switcher
|
||||
<vertical-tab-switcher
|
||||
v-if="adminDescriptionsLoaded && (noDb || adminDbLoaded)"
|
||||
ref="tabSwitcher"
|
||||
class="settings_tab-switcher"
|
||||
class="settings-admin-content settings_tab-switcher"
|
||||
:side-tab-bar="true"
|
||||
:scrollable-tabs="true"
|
||||
:render-only-focused="true"
|
||||
|
|
@ -15,7 +15,7 @@
|
|||
data-tab-name="nodb-notice"
|
||||
>
|
||||
<div :label="$t('admin_dash.tabs.nodb')">
|
||||
<div class="setting-item">
|
||||
<div class="setting-section">
|
||||
<h2>{{ $t('admin_dash.nodb.heading') }}</h2>
|
||||
<i18n-t
|
||||
scope="global"
|
||||
|
|
@ -48,6 +48,41 @@
|
|||
>
|
||||
<InstanceTab />
|
||||
</div>
|
||||
|
||||
<div
|
||||
:label="$t('admin_dash.tabs.registrations')"
|
||||
icon="door-open"
|
||||
data-tab-name="registrations"
|
||||
>
|
||||
<RegistrationsTab />
|
||||
</div>
|
||||
|
||||
<div
|
||||
:label="$t('admin_dash.tabs.auth')"
|
||||
icon="key"
|
||||
data-tab-name="monitoring"
|
||||
>
|
||||
<AuthTab />
|
||||
</div>
|
||||
|
||||
<div
|
||||
:label="$t('admin_dash.tabs.emoji')"
|
||||
icon="face-smile-beam"
|
||||
data-tab-name="emoji"
|
||||
full-width
|
||||
>
|
||||
<EmojiTab />
|
||||
</div>
|
||||
|
||||
<div
|
||||
:label="$t('admin_dash.tabs.frontends')"
|
||||
icon="laptop-code"
|
||||
data-tab-name="frontends"
|
||||
full-width
|
||||
>
|
||||
<FrontendsTab />
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="adminDbLoaded"
|
||||
:label="$t('admin_dash.tabs.users')"
|
||||
|
|
@ -64,24 +99,104 @@
|
|||
>
|
||||
<LimitsTab />
|
||||
</div>
|
||||
|
||||
<div
|
||||
:label="$t('admin_dash.tabs.frontends')"
|
||||
icon="laptop-code"
|
||||
data-tab-name="frontends"
|
||||
v-if="adminDbLoaded"
|
||||
:label="$t('admin_dash.tabs.rate_limit')"
|
||||
icon="gauge"
|
||||
data-tab-name="rate_limits"
|
||||
>
|
||||
<FrontendsTab />
|
||||
<RatesTab />
|
||||
</div>
|
||||
|
||||
<div
|
||||
:label="$t('admin_dash.tabs.emoji')"
|
||||
icon="face-smile-beam"
|
||||
data-tab-name="emoji"
|
||||
:label="$t('admin_dash.tabs.uploads')"
|
||||
icon="upload"
|
||||
data-tab-name="uploads"
|
||||
>
|
||||
<EmojiTab />
|
||||
<UploadsTab />
|
||||
</div>
|
||||
</tab-switcher>
|
||||
|
||||
<div
|
||||
:label="$t('admin_dash.tabs.media_proxy')"
|
||||
icon="tower-broadcast"
|
||||
data-tab-name="media_proxy"
|
||||
>
|
||||
<MediaProxyTab />
|
||||
</div>
|
||||
|
||||
<div
|
||||
:label="$t('admin_dash.tabs.posts')"
|
||||
icon="message"
|
||||
data-tab-name="other"
|
||||
>
|
||||
<PostsTab />
|
||||
</div>
|
||||
|
||||
<div
|
||||
:label="$t('admin_dash.tabs.links')"
|
||||
icon="chain"
|
||||
data-tab-name="links"
|
||||
>
|
||||
<LinksTab />
|
||||
</div>
|
||||
|
||||
<div
|
||||
:label="$t('admin_dash.tabs.mailer')"
|
||||
icon="envelope"
|
||||
data-tab-name="mailer"
|
||||
>
|
||||
<MailerTab />
|
||||
</div>
|
||||
|
||||
<div
|
||||
:label="$t('admin_dash.tabs.federation')"
|
||||
icon="circle-nodes"
|
||||
data-tab-name="monitoring"
|
||||
>
|
||||
<FederationTab />
|
||||
</div>
|
||||
|
||||
<div
|
||||
:label="$t('admin_dash.tabs.http')"
|
||||
icon="globe"
|
||||
data-tab-name="http"
|
||||
>
|
||||
<HTTPTab />
|
||||
</div>
|
||||
|
||||
<div
|
||||
:label="$t('admin_dash.tabs.job_queues')"
|
||||
icon="gears"
|
||||
data-tab-name="job_queues"
|
||||
>
|
||||
<JobQueuesTab />
|
||||
</div>
|
||||
|
||||
<div
|
||||
:label="$t('admin_dash.tabs.monitoring')"
|
||||
icon="chart-line"
|
||||
data-tab-name="monitoring"
|
||||
>
|
||||
<MonitoringTab />
|
||||
</div>
|
||||
|
||||
<div
|
||||
:label="$t('admin_dash.tabs.other')"
|
||||
icon="ellipsis"
|
||||
data-tab-name="other"
|
||||
>
|
||||
<OtherTab />
|
||||
</div>
|
||||
</vertical-tab-switcher>
|
||||
</template>
|
||||
|
||||
<script src="./settings_modal_admin_content.js"></script>
|
||||
|
||||
<style src="./settings_modal_admin_content.scss" lang="scss"></style>
|
||||
<style lang="scss">
|
||||
.settings-admin-content {
|
||||
.setting-item {
|
||||
grid-template-columns: 1fr 3fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,48 +1,61 @@
|
|||
import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'
|
||||
|
||||
import VerticalTabSwitcher from './helpers/vertical_tab_switcher.jsx'
|
||||
import AppearanceTab from './tabs/appearance_tab.vue'
|
||||
import ClutterTab from './tabs/clutter_tab.vue'
|
||||
import ComposingTab from './tabs/composing_tab.vue'
|
||||
import DataImportExportTab from './tabs/data_import_export_tab.vue'
|
||||
import DeveloperTab from './tabs/developer_tab.vue'
|
||||
import FilteringTab from './tabs/filtering_tab.vue'
|
||||
import GeneralTab from './tabs/general_tab.vue'
|
||||
import LayoutTab from './tabs/layout_tab.vue'
|
||||
import MutesAndBlocksTab from './tabs/mutes_and_blocks_tab.vue'
|
||||
import NotificationsTab from './tabs/notifications_tab.vue'
|
||||
import FilteringTab from './tabs/filtering_tab.vue'
|
||||
import SecurityTab from './tabs/security_tab/security_tab.vue'
|
||||
import OldThemeTab from './tabs/old_theme_tab/old_theme_tab.vue'
|
||||
import PostsTab from './tabs/posts_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 SecurityTab from './tabs/security_tab/security_tab.vue'
|
||||
import StyleTab from './tabs/style_tab/style_tab.vue'
|
||||
|
||||
import { useInterfaceStore } from 'src/stores/interface.js'
|
||||
import { useMergedConfigStore } from 'src/stores/merged_config.js'
|
||||
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faWrench,
|
||||
faUser,
|
||||
faFilter,
|
||||
faPaintBrush,
|
||||
faPalette,
|
||||
faBell,
|
||||
faBroom,
|
||||
faCode,
|
||||
faColumns,
|
||||
faDownload,
|
||||
faEyeSlash,
|
||||
faInfo,
|
||||
faWindowRestore
|
||||
faFilter,
|
||||
faLock,
|
||||
faMessage,
|
||||
faPaintBrush,
|
||||
faPalette,
|
||||
faUser,
|
||||
faWindowRestore,
|
||||
faWrench,
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { useInterfaceStore } from 'src/stores/interface'
|
||||
|
||||
library.add(
|
||||
faWrench,
|
||||
faUser,
|
||||
faFilter,
|
||||
faPaintBrush,
|
||||
faPalette,
|
||||
faMessage,
|
||||
faWindowRestore,
|
||||
faColumns,
|
||||
faBell,
|
||||
faDownload,
|
||||
faFilter,
|
||||
faEyeSlash,
|
||||
faInfo,
|
||||
faWindowRestore
|
||||
faBroom,
|
||||
faLock,
|
||||
faDownload,
|
||||
faPalette,
|
||||
faPaintBrush,
|
||||
faCode,
|
||||
)
|
||||
|
||||
const SettingsModalContent = {
|
||||
components: {
|
||||
TabSwitcher,
|
||||
VerticalTabSwitcher,
|
||||
|
||||
DataImportExportTab,
|
||||
MutesAndBlocksTab,
|
||||
|
|
@ -51,36 +64,45 @@ const SettingsModalContent = {
|
|||
SecurityTab,
|
||||
ProfileTab,
|
||||
GeneralTab,
|
||||
PostsTab,
|
||||
ComposingTab,
|
||||
ClutterTab,
|
||||
LayoutTab,
|
||||
AppearanceTab,
|
||||
StyleTab,
|
||||
VersionTab,
|
||||
ThemeTab
|
||||
DeveloperTab,
|
||||
OldThemeTab,
|
||||
},
|
||||
computed: {
|
||||
isLoggedIn () {
|
||||
isLoggedIn() {
|
||||
return !!this.$store.state.users.currentUser
|
||||
},
|
||||
open () {
|
||||
open() {
|
||||
return useInterfaceStore().settingsModalState !== 'hidden'
|
||||
},
|
||||
bodyLock () {
|
||||
bodyLock() {
|
||||
return useInterfaceStore().settingsModalState === 'visible'
|
||||
},
|
||||
expertLevel () {
|
||||
return this.$store.state.config.expertLevel
|
||||
expertLevel() {
|
||||
return useMergedConfigStore().mergedConfig.expertLevel
|
||||
},
|
||||
isMobileLayout () {
|
||||
return useInterfaceStore().layoutType === 'mobile'
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
navCollapsed: false,
|
||||
navHideHeader: false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onOpen () {
|
||||
onOpen() {
|
||||
const targetTab = useInterfaceStore().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
|
||||
})
|
||||
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)
|
||||
}
|
||||
|
|
@ -88,16 +110,16 @@ const SettingsModalContent = {
|
|||
// Clear the state of target tab, so that next time settings is opened
|
||||
// it doesn't force it.
|
||||
useInterfaceStore().clearSettingsModalTargetTab()
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
mounted() {
|
||||
this.onOpen()
|
||||
},
|
||||
watch: {
|
||||
open: function (value) {
|
||||
if (value) this.onOpen()
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default SettingsModalContent
|
||||
|
|
|
|||
|
|
@ -1,62 +0,0 @@
|
|||
.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
<template>
|
||||
<tab-switcher
|
||||
<vertical-tab-switcher
|
||||
ref="tabSwitcher"
|
||||
class="settings_tab-switcher"
|
||||
:side-tab-bar="true"
|
||||
:scrollable-tabs="true"
|
||||
:body-scroll-lock="bodyLock"
|
||||
:hide-header="navHideHeader"
|
||||
>
|
||||
<div
|
||||
:label="$t('settings.general')"
|
||||
|
|
@ -14,6 +14,33 @@
|
|||
<GeneralTab />
|
||||
</div>
|
||||
<div
|
||||
v-if="isLoggedIn"
|
||||
:label="$t('settings.profile_tab')"
|
||||
icon="user"
|
||||
data-tab-name="profile"
|
||||
:full-width="true"
|
||||
>
|
||||
<ProfileTab />
|
||||
</div>
|
||||
<div
|
||||
v-if="isLoggedIn"
|
||||
:label="$t('settings.composing')"
|
||||
icon="pen-alt"
|
||||
data-tab-name="composing"
|
||||
:delay-render="true"
|
||||
>
|
||||
<ComposingTab />
|
||||
</div>
|
||||
<div
|
||||
:label="$t('settings.posts')"
|
||||
icon="message"
|
||||
data-tab-name="posts"
|
||||
:delay-render="true"
|
||||
>
|
||||
<PostsTab />
|
||||
</div>
|
||||
<div
|
||||
:full-width="true"
|
||||
:label="$t('settings.appearance')"
|
||||
icon="window-restore"
|
||||
data-tab-name="appearance"
|
||||
|
|
@ -22,47 +49,22 @@
|
|||
<AppearanceTab />
|
||||
</div>
|
||||
<div
|
||||
v-if="expertLevel > 0"
|
||||
:label="$t('settings.style.themes3.editor.title')"
|
||||
icon="palette"
|
||||
data-tab-name="style"
|
||||
:label="$t('settings.layout')"
|
||||
icon="table-columns"
|
||||
data-tab-name="layout"
|
||||
:delay-render="true"
|
||||
>
|
||||
<StyleTab />
|
||||
</div>
|
||||
<div
|
||||
v-if="expertLevel > 0"
|
||||
:label="$t('settings.theme_old')"
|
||||
icon="paint-brush"
|
||||
data-tab-name="theme"
|
||||
:delay-render="true"
|
||||
>
|
||||
<ThemeTab />
|
||||
</div>
|
||||
<div
|
||||
v-if="isLoggedIn"
|
||||
:label="$t('settings.profile_tab')"
|
||||
icon="user"
|
||||
data-tab-name="profile"
|
||||
>
|
||||
<ProfileTab />
|
||||
<LayoutTab />
|
||||
</div>
|
||||
<div
|
||||
v-if="isLoggedIn"
|
||||
:full-width="true"
|
||||
:label="$t('settings.notifications')"
|
||||
icon="bell"
|
||||
data-tab-name="notifications"
|
||||
>
|
||||
<NotificationsTab />
|
||||
</div>
|
||||
<div
|
||||
v-if="isLoggedIn"
|
||||
:label="$t('settings.security_tab')"
|
||||
icon="lock"
|
||||
data-tab-name="security"
|
||||
>
|
||||
<SecurityTab />
|
||||
</div>
|
||||
<div
|
||||
:label="$t('settings.filtering')"
|
||||
icon="filter"
|
||||
|
|
@ -73,12 +75,28 @@
|
|||
<div
|
||||
v-if="isLoggedIn"
|
||||
:label="$t('settings.mutes_and_blocks')"
|
||||
:fullHeight="true"
|
||||
icon="eye-slash"
|
||||
data-tab-name="mutesAndBlocks"
|
||||
:full-width="true"
|
||||
:full-height="true"
|
||||
>
|
||||
<MutesAndBlocksTab />
|
||||
</div>
|
||||
<div
|
||||
:label="$t('settings.clutter')"
|
||||
icon="broom"
|
||||
data-tab-name="clutter"
|
||||
>
|
||||
<ClutterTab />
|
||||
</div>
|
||||
<div
|
||||
v-if="isLoggedIn"
|
||||
:label="$t('settings.security_tab')"
|
||||
icon="lock"
|
||||
data-tab-name="security"
|
||||
>
|
||||
<SecurityTab />
|
||||
</div>
|
||||
<div
|
||||
v-if="isLoggedIn"
|
||||
:label="$t('settings.data_import_export_tab')"
|
||||
|
|
@ -88,15 +106,34 @@
|
|||
<DataImportExportTab />
|
||||
</div>
|
||||
<div
|
||||
:label="$t('settings.version.title')"
|
||||
icon="info"
|
||||
data-tab-name="version"
|
||||
v-if="expertLevel > 0"
|
||||
:label="$t('settings.style.themes3.editor.title')"
|
||||
icon="palette"
|
||||
data-tab-name="style"
|
||||
:delay-render="true"
|
||||
:full-width="true"
|
||||
>
|
||||
<VersionTab />
|
||||
<StyleTab />
|
||||
</div>
|
||||
</tab-switcher>
|
||||
<div
|
||||
v-if="expertLevel > 0"
|
||||
:label="$t('settings.theme_old')"
|
||||
icon="paint-brush"
|
||||
data-tab-name="theme"
|
||||
:delay-render="true"
|
||||
:full-width="true"
|
||||
>
|
||||
<OldThemeTab />
|
||||
</div>
|
||||
<div
|
||||
v-if="expertLevel > 0"
|
||||
:label="$t('settings.developer')"
|
||||
icon="code"
|
||||
data-tab-name="developer"
|
||||
>
|
||||
<DeveloperTab />
|
||||
</div>
|
||||
</vertical-tab-switcher>
|
||||
</template>
|
||||
|
||||
<script src="./settings_modal_user_content.js"></script>
|
||||
|
||||
<style src="./settings_modal_user_content.scss" lang="scss"></style>
|
||||
|
|
|
|||
|
|
@ -1,40 +1,30 @@
|
|||
import { mapActions } from 'pinia'
|
||||
|
||||
import fileSizeFormatService from 'src/components/../services/file_size_format/file_size_format.js'
|
||||
import PaletteEditor from 'src/components/palette_editor/palette_editor.vue'
|
||||
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 IntegerSetting from '../helpers/integer_setting.vue'
|
||||
import SharedComputedObject from '../helpers/shared_computed_object.js'
|
||||
import UnitSetting from '../helpers/unit_setting.vue'
|
||||
import { defaultHorizontalUnits } from '../helpers/unit_setting.js'
|
||||
import PaletteEditor from 'src/components/palette_editor/palette_editor.vue'
|
||||
import Preview from './theme_tab/theme_preview.vue'
|
||||
import FontControl from 'src/components/font_control/font_control.vue'
|
||||
import Preview from './old_theme_tab/theme_preview.vue'
|
||||
|
||||
import { useInstanceStore } from 'src/stores/instance.js'
|
||||
import { normalizeThemeData, useInterfaceStore } from 'src/stores/interface.js'
|
||||
|
||||
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
|
||||
} from 'src/services/theme_data/css_utils.js'
|
||||
adoptStyleSheets,
|
||||
createStyleSheet,
|
||||
} from 'src/services/style_setter/style_setter.js'
|
||||
import { getCssRules } from 'src/services/theme_data/css_utils.js'
|
||||
import { deserialize } from 'src/services/theme_data/iss_deserializer.js'
|
||||
import { createStyleSheet, adoptStyleSheets } from 'src/services/style_setter/style_setter.js'
|
||||
import fileSizeFormatService from 'src/components/../services/file_size_format/file_size_format.js'
|
||||
|
||||
import SharedComputedObject from '../helpers/shared_computed_object.js'
|
||||
import ProfileSettingIndicator from '../helpers/profile_setting_indicator.vue'
|
||||
|
||||
import { mapActions } from 'pinia'
|
||||
import { useInterfaceStore, normalizeThemeData } from 'src/stores/interface'
|
||||
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faGlobe
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(
|
||||
faGlobe
|
||||
)
|
||||
import { init } from 'src/services/theme_data/theme_data_3.service.js'
|
||||
import { convertTheme2To3 } from 'src/services/theme_data/theme2_to_theme3.js'
|
||||
|
||||
const AppearanceTab = {
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
availableThemesV3: [],
|
||||
availableThemesV2: [],
|
||||
|
|
@ -45,7 +35,7 @@ const AppearanceTab = {
|
|||
validator: this.importValidator,
|
||||
onImport: this.onImport,
|
||||
parser: this.importParser,
|
||||
onImportFailure: this.onImportFailure
|
||||
onImportFailure: this.onImportFailure,
|
||||
}),
|
||||
palettesKeys: [
|
||||
'bg',
|
||||
|
|
@ -55,27 +45,29 @@ const AppearanceTab = {
|
|||
'cRed',
|
||||
'cGreen',
|
||||
'cBlue',
|
||||
'cOrange'
|
||||
'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}`)
|
||||
})),
|
||||
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) => ({
|
||||
key: mode,
|
||||
value: mode,
|
||||
label: this.$t(`settings.style.themes3.hacks.underlay_override_mode_${mode}`)
|
||||
label: this.$t(
|
||||
`settings.style.themes3.hacks.underlay_override_mode_${mode}`,
|
||||
),
|
||||
})),
|
||||
backgroundUploading: false,
|
||||
background: null,
|
||||
backgroundError: null,
|
||||
backgroundPreview: null,
|
||||
}
|
||||
},
|
||||
|
|
@ -85,17 +77,15 @@ const AppearanceTab = {
|
|||
IntegerSetting,
|
||||
FloatSetting,
|
||||
UnitSetting,
|
||||
ProfileSettingIndicator,
|
||||
FontControl,
|
||||
Preview,
|
||||
PaletteEditor
|
||||
PaletteEditor,
|
||||
},
|
||||
mounted () {
|
||||
mounted() {
|
||||
useInterfaceStore().getThemeData()
|
||||
|
||||
const updateIndex = (resource) => {
|
||||
const capitalizedResource = resource[0].toUpperCase() + resource.slice(1)
|
||||
const currentIndex = this.$store.state.instance[`${resource}sIndex`]
|
||||
const currentIndex = useInstanceStore()[`${resource}sIndex`]
|
||||
|
||||
let promise
|
||||
if (currentIndex) {
|
||||
|
|
@ -104,120 +94,151 @@ const AppearanceTab = {
|
|||
promise = useInterfaceStore()[`fetch${capitalizedResource}sIndex`]()
|
||||
}
|
||||
|
||||
return promise.then(index => {
|
||||
return Object
|
||||
.entries(index)
|
||||
.map(([k, func]) => [k, func()])
|
||||
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('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' })
|
||||
}
|
||||
}))
|
||||
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 = useInterfaceStore().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)
|
||||
}
|
||||
}))
|
||||
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)
|
||||
}
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
this.previewTheme('stock', 'v3')
|
||||
|
||||
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) this.previewTheme(theme.key, theme.version, theme.data)
|
||||
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) this.previewTheme(theme.key, theme.version, theme.data)
|
||||
})
|
||||
observer.unobserve(target)
|
||||
})
|
||||
observer.unobserve(target)
|
||||
})
|
||||
}, {
|
||||
root: this.$refs.themeList
|
||||
})
|
||||
},
|
||||
{
|
||||
root: this.$refs.themeList,
|
||||
},
|
||||
)
|
||||
} else {
|
||||
this.availableStyles.forEach(theme => this.previewTheme(theme.key, theme.version, theme.data))
|
||||
this.availableStyles.forEach((theme) =>
|
||||
this.previewTheme(theme.key, theme.version, theme.data),
|
||||
)
|
||||
}
|
||||
},
|
||||
updated () {
|
||||
updated() {
|
||||
this.$nextTick(() => {
|
||||
this.$refs.themeList.querySelectorAll('.theme-preview').forEach(node => {
|
||||
this.intersectionObserver.observe(node)
|
||||
})
|
||||
this.$refs.themeList
|
||||
.querySelectorAll('.theme-preview')
|
||||
.forEach((node) => {
|
||||
this.intersectionObserver.observe(node)
|
||||
})
|
||||
})
|
||||
},
|
||||
watch: {
|
||||
paletteDataUsed () {
|
||||
paletteDataUsed() {
|
||||
this.userPalette = this.paletteDataUsed || {}
|
||||
}
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
isDefaultBackground () {
|
||||
return !(this.$store.state.users.currentUser.background_image)
|
||||
isDefaultBackground() {
|
||||
return !this.$store.state.users.currentUser.background_image
|
||||
},
|
||||
switchInProgress () {
|
||||
switchInProgress() {
|
||||
return useInterfaceStore().themeChangeInProgress
|
||||
},
|
||||
paletteDataUsed () {
|
||||
paletteDataUsed() {
|
||||
return useInterfaceStore().paletteDataUsed
|
||||
},
|
||||
availableStyles () {
|
||||
return [
|
||||
...this.availableThemesV3,
|
||||
...this.availableThemesV2
|
||||
]
|
||||
availableStyles() {
|
||||
return [...this.availableThemesV3, ...this.availableThemesV2]
|
||||
},
|
||||
availablePalettes () {
|
||||
return [
|
||||
...this.bundledPalettes,
|
||||
...this.stylePalettes
|
||||
]
|
||||
availablePalettes() {
|
||||
return [...this.bundledPalettes, ...this.stylePalettes]
|
||||
},
|
||||
stylePalettes () {
|
||||
stylePalettes() {
|
||||
const ruleset = useInterfaceStore().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 meta = ruleset.find((x) => x.component === '@meta')
|
||||
const result = ruleset
|
||||
.filter((x) => x.component.startsWith('@palette'))
|
||||
.map((x) => {
|
||||
const { variant, directives } = x
|
||||
const {
|
||||
bg,
|
||||
|
|
@ -229,7 +250,7 @@ const AppearanceTab = {
|
|||
cBlue,
|
||||
cGreen,
|
||||
cOrange,
|
||||
wallpaper
|
||||
wallpaper,
|
||||
} = directives
|
||||
|
||||
const result = {
|
||||
|
|
@ -244,126 +265,106 @@ const AppearanceTab = {
|
|||
cBlue,
|
||||
cGreen,
|
||||
cOrange,
|
||||
wallpaper
|
||||
wallpaper,
|
||||
}
|
||||
return Object.fromEntries(Object.entries(result).filter(([, v]) => v))
|
||||
})
|
||||
return result
|
||||
},
|
||||
noIntersectionObserver () {
|
||||
noIntersectionObserver() {
|
||||
return !window.IntersectionObserver
|
||||
},
|
||||
horizontalUnits () {
|
||||
return defaultHorizontalUnits
|
||||
instanceWallpaper() {
|
||||
useInstanceStore().instanceIdentity.background
|
||||
},
|
||||
fontsOverride () {
|
||||
return this.$store.getters.mergedConfig.fontsOverride
|
||||
},
|
||||
columns () {
|
||||
const mode = this.$store.getters.mergedConfig.thirdColumnMode
|
||||
|
||||
const notif = mode === 'none' ? [] : ['notifs']
|
||||
|
||||
if (this.$store.getters.mergedConfig.sidebarRight || mode === 'postform') {
|
||||
return [...notif, 'content', 'sidebar']
|
||||
} else {
|
||||
return ['sidebar', 'content', ...notif]
|
||||
}
|
||||
},
|
||||
instanceWallpaperUsed () {
|
||||
return this.$store.state.instance.background &&
|
||||
instanceWallpaperUsed() {
|
||||
return (
|
||||
useInstanceStore().instanceIdentity.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 () {
|
||||
customThemeVersion() {
|
||||
const { themeVersion } = useInterfaceStore()
|
||||
return themeVersion
|
||||
},
|
||||
isCustomThemeUsed () {
|
||||
isCustomThemeUsed() {
|
||||
const { customTheme, customThemeSource } = this.mergedConfig
|
||||
return customTheme != null || customThemeSource != null
|
||||
},
|
||||
isCustomStyleUsed () {
|
||||
isCustomStyleUsed() {
|
||||
const { styleCustomData } = this.mergedConfig
|
||||
return styleCustomData != null
|
||||
},
|
||||
...SharedComputedObject()
|
||||
...SharedComputedObject(),
|
||||
},
|
||||
methods: {
|
||||
updateFont (key, value) {
|
||||
this.$store.dispatch('setOption', {
|
||||
name: 'theme3hacks',
|
||||
value: {
|
||||
...this.mergedConfig.theme3hacks,
|
||||
fonts: {
|
||||
...this.mergedConfig.theme3hacks.fonts,
|
||||
[key]: value
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
importFile () {
|
||||
importFile() {
|
||||
this.fileImporter.importData()
|
||||
},
|
||||
importParser (file, filename) {
|
||||
importParser(file, filename) {
|
||||
if (filename.endsWith('.json')) {
|
||||
return JSON.parse(file)
|
||||
} else if (filename.endsWith('.iss')) {
|
||||
return deserialize(file)
|
||||
}
|
||||
},
|
||||
onImport (parsed, filename) {
|
||||
onImport(parsed, filename) {
|
||||
if (filename.endsWith('.json')) {
|
||||
useInterfaceStore().setThemeCustom(parsed.source || parsed.theme)
|
||||
} else if (filename.endsWith('.iss')) {
|
||||
useInterfaceStore().setStyleCustom(parsed)
|
||||
}
|
||||
},
|
||||
onImportFailure (result) {
|
||||
onImportFailure(result) {
|
||||
console.error('Failure importing theme:', result)
|
||||
useInterfaceStore().pushGlobalNotice({ messageKey: 'settings.invalid_theme_imported', level: 'error' })
|
||||
useInterfaceStore().pushGlobalNotice({
|
||||
messageKey: 'settings.invalid_theme_imported',
|
||||
level: 'error',
|
||||
})
|
||||
},
|
||||
importValidator (parsed, filename) {
|
||||
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
|
||||
if (parsed.find((x) => x.component === '@meta') == null) return false
|
||||
return true
|
||||
}
|
||||
},
|
||||
isThemeActive (key) {
|
||||
return key === (this.mergedConfig.theme || this.$store.state.instance.theme)
|
||||
isThemeActive(key) {
|
||||
return (
|
||||
key ===
|
||||
(this.mergedConfig.theme || useInstanceStore().instanceIdentity.theme)
|
||||
)
|
||||
},
|
||||
isStyleActive (key) {
|
||||
return key === (this.mergedConfig.style || this.$store.state.instance.style)
|
||||
isStyleActive(key) {
|
||||
return (
|
||||
key ===
|
||||
(this.mergedConfig.style || useInstanceStore().instanceIdentity.style)
|
||||
)
|
||||
},
|
||||
isPaletteActive (key) {
|
||||
return key === (this.mergedConfig.palette || this.$store.state.instance.palette)
|
||||
isPaletteActive(key) {
|
||||
return (
|
||||
key ===
|
||||
(this.mergedConfig.palette ||
|
||||
useInstanceStore().instanceIdentity.palette)
|
||||
)
|
||||
},
|
||||
...mapActions(useInterfaceStore, [
|
||||
'setStyle',
|
||||
'setTheme'
|
||||
]),
|
||||
setPalette (name, data) {
|
||||
...mapActions(useInterfaceStore, ['setStyle', 'setTheme']),
|
||||
setPalette(name, data) {
|
||||
useInterfaceStore().setPalette(name)
|
||||
this.userPalette = data
|
||||
},
|
||||
setPaletteCustom (data) {
|
||||
setPaletteCustom(data) {
|
||||
useInterfaceStore().setPaletteCustom(data)
|
||||
this.userPalette = data
|
||||
},
|
||||
resetTheming () {
|
||||
resetTheming() {
|
||||
useInterfaceStore().setStyle('stock')
|
||||
},
|
||||
previewTheme (key, version, input) {
|
||||
previewTheme(key, version, input) {
|
||||
let theme3
|
||||
if (this.compilationCache[key]) {
|
||||
theme3 = this.compilationCache[key]
|
||||
|
|
@ -376,10 +377,10 @@ const AppearanceTab = {
|
|||
ultimateBackgroundColor: '#000000',
|
||||
liteMode: true,
|
||||
debug: true,
|
||||
onlyNormalState: true
|
||||
onlyNormalState: true,
|
||||
})
|
||||
} else if (version === 'v3') {
|
||||
const palette = input.find(x => x.component === '@palette')
|
||||
const palette = input.find((x) => x.component === '@palette')
|
||||
let paletteRule
|
||||
if (palette) {
|
||||
const { directives } = palette
|
||||
|
|
@ -388,21 +389,20 @@ const AppearanceTab = {
|
|||
paletteRule = {
|
||||
component: 'Root',
|
||||
directives: Object.fromEntries(
|
||||
Object
|
||||
.entries(directives)
|
||||
Object.entries(directives)
|
||||
.filter(([k]) => k && k !== 'name')
|
||||
.map(([k, v]) => ['--' + k, 'color | ' + v])
|
||||
)
|
||||
.map(([k, v]) => ['--' + k, 'color | ' + v]),
|
||||
),
|
||||
}
|
||||
} else {
|
||||
paletteRule = null
|
||||
}
|
||||
|
||||
theme3 = init({
|
||||
inputRuleset: [...input, paletteRule].filter(x => x),
|
||||
inputRuleset: [...input, paletteRule].filter((x) => x),
|
||||
ultimateBackgroundColor: '#000000',
|
||||
liteMode: true,
|
||||
onlyNormalState: true
|
||||
onlyNormalState: true,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
|
|
@ -410,7 +410,7 @@ const AppearanceTab = {
|
|||
inputRuleset: [],
|
||||
ultimateBackgroundColor: '#000000',
|
||||
liteMode: true,
|
||||
onlyNormalState: true
|
||||
onlyNormalState: true,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -418,22 +418,29 @@ const AppearanceTab = {
|
|||
this.compilationCache[key] = theme3
|
||||
}
|
||||
|
||||
|
||||
const sheet = createStyleSheet('appearance-tab-previews', 90)
|
||||
sheet.addRule([
|
||||
'#theme-preview-', key, ' {\n',
|
||||
getCssRules(theme3.eager).join('\n'),
|
||||
'\n}'
|
||||
].join(''))
|
||||
sheet.addRule(
|
||||
[
|
||||
'#theme-preview-',
|
||||
key,
|
||||
' {\n',
|
||||
getCssRules(theme3.eager).join('\n'),
|
||||
'\n}',
|
||||
].join(''),
|
||||
)
|
||||
sheet.ready = true
|
||||
adoptStyleSheets()
|
||||
},
|
||||
uploadFile (slot, e) {
|
||||
uploadFile(slot, e) {
|
||||
const file = e.target.files[0]
|
||||
if (!file) { return }
|
||||
if (file.size > this.$store.state.instance[slot + 'limit']) {
|
||||
if (!file) {
|
||||
return
|
||||
}
|
||||
if (file.size > useInstanceStore()[slot + 'limit']) {
|
||||
const filesize = fileSizeFormatService.fileSizeFormat(file.size)
|
||||
const allowedsize = fileSizeFormatService.fileSizeFormat(this.$store.state.instance[slot + 'limit'])
|
||||
const allowedsize = fileSizeFormatService.fileSizeFormat(
|
||||
useInstanceStore()[slot + 'limit'],
|
||||
)
|
||||
useInterfaceStore().pushGlobalNotice({
|
||||
messageKey: 'upload.error.message',
|
||||
messageArgs: [
|
||||
|
|
@ -441,10 +448,10 @@ const AppearanceTab = {
|
|||
filesize: filesize.num,
|
||||
filesizeunit: filesize.unit,
|
||||
allowedsize: allowedsize.num,
|
||||
allowedsizeunit: allowedsize.unit
|
||||
})
|
||||
allowedsizeunit: allowedsize.unit,
|
||||
}),
|
||||
],
|
||||
level: 'error'
|
||||
level: 'error',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
|
@ -457,26 +464,42 @@ const AppearanceTab = {
|
|||
}
|
||||
reader.readAsDataURL(file)
|
||||
},
|
||||
resetBackground () {
|
||||
const confirmed = window.confirm(this.$t('settings.reset_background_confirm'))
|
||||
resetBackground() {
|
||||
const confirmed = window.confirm(
|
||||
this.$t('settings.reset_background_confirm'),
|
||||
)
|
||||
if (confirmed) {
|
||||
this.submitBackground('')
|
||||
}
|
||||
},
|
||||
submitBackground (background) {
|
||||
if (!this.backgroundPreview && background !== '') { return }
|
||||
resetUploadedBackground() {
|
||||
this.backgroundPreview = null
|
||||
},
|
||||
clearBackgroundError() {
|
||||
this.backgroundError = null
|
||||
},
|
||||
submitBackground(background) {
|
||||
if (!this.backgroundPreview && background !== '') {
|
||||
return
|
||||
}
|
||||
|
||||
this.backgroundUploading = true
|
||||
this.$store.state.api.backendInteractor.updateProfileImages({ background })
|
||||
this.$store.state.api.backendInteractor
|
||||
.updateProfileImages({ background })
|
||||
.then((data) => {
|
||||
this.$store.commit('addNewUsers', [data])
|
||||
this.$store.commit('setCurrentUser', data)
|
||||
this.backgroundPreview = null
|
||||
this.backgroundError = null
|
||||
})
|
||||
.catch((e) => {
|
||||
this.backgroundError = e
|
||||
})
|
||||
.finally(() => {
|
||||
this.backgroundUploading = false
|
||||
})
|
||||
.catch(this.displayUploadError)
|
||||
.finally(() => { this.backgroundUploading = false })
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export default AppearanceTab
|
||||
|
|
|
|||
|
|
@ -1,27 +1,18 @@
|
|||
.appearance-tab {
|
||||
margin: 1em;
|
||||
|
||||
h3 {
|
||||
border: none
|
||||
}
|
||||
|
||||
.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;
|
||||
.theme-name {
|
||||
font-weight: 900;
|
||||
padding-bottom: 0.5em;
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
|
|
@ -29,7 +20,31 @@
|
|||
height: auto;
|
||||
}
|
||||
|
||||
.banner-background {
|
||||
display: flex;
|
||||
gap: 1em;
|
||||
flex-wrap: wrap;
|
||||
|
||||
h4 {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.banner-background-input {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5em;
|
||||
|
||||
.custom-bg-control {
|
||||
display: grid;
|
||||
gap: 0.5em;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.banner-background-preview {
|
||||
display: flex;
|
||||
max-width: 100%;
|
||||
width: 300px;
|
||||
position: relative;
|
||||
|
|
@ -37,40 +52,95 @@
|
|||
img {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.reset-button {
|
||||
position: absolute;
|
||||
top: 0.2em;
|
||||
right: 0.2em;
|
||||
border-radius: var(--roundness);
|
||||
background-color: rgb(0 0 0 / 60%);
|
||||
opacity: 0.7;
|
||||
width: 1.5em;
|
||||
height: 1.5em;
|
||||
text-align: center;
|
||||
line-height: 1.5em;
|
||||
font-size: 1.5em;
|
||||
cursor: pointer;
|
||||
.fun-monitor {
|
||||
position: relative;
|
||||
pointer-events: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
* {
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
svg {
|
||||
color: white;
|
||||
&-display-bezel,
|
||||
&-display-screen {
|
||||
aspect-ratio: 16 / 9;
|
||||
width: 16em;
|
||||
}
|
||||
|
||||
img {
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.wallpaper {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-color: var(--wallpaper);
|
||||
}
|
||||
|
||||
&-display-uploading {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
place-content: center;
|
||||
background-color: rgb(0 0 0 / 60%);
|
||||
font-size: 4em;
|
||||
}
|
||||
|
||||
&-display-screen {
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
|
||||
&-overlay {
|
||||
background: transparent;
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
&-image {
|
||||
aspect-ratio: 16 / 9
|
||||
}
|
||||
}
|
||||
|
||||
&-display-bezel {
|
||||
padding: 1em;
|
||||
margin: 0;
|
||||
order: 1;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
&-neck {
|
||||
width: 5em;
|
||||
height: 3em;
|
||||
margin-top: -1em;
|
||||
margin-bottom: -0.5em;
|
||||
order: 2
|
||||
}
|
||||
|
||||
&-stand {
|
||||
width: 8em;
|
||||
height: 1em;
|
||||
order: 3;
|
||||
z-index: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
.palettes-container {
|
||||
height: 15em;
|
||||
overflow: hidden auto;
|
||||
scrollbar-gutter: stable;
|
||||
border-radius: var(--roundness);
|
||||
border: 1px solid var(--border);
|
||||
margin: -0.5em;
|
||||
margin-bottom: 0.5em;
|
||||
margin-top: 0;
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
.palettes {
|
||||
|
|
@ -80,9 +150,9 @@
|
|||
padding: 0.5em;
|
||||
width: 100%;
|
||||
|
||||
h4 {
|
||||
margin: 0;
|
||||
h5 {
|
||||
grid-column: 1 / span 2;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -160,7 +230,7 @@
|
|||
|
||||
.theme-preview {
|
||||
font-size: 1rem; // fix for firefox
|
||||
width: 19rem;
|
||||
width: 14rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,15 @@
|
|||
<template>
|
||||
<div
|
||||
class="appearance-tab"
|
||||
:label="$t('settings.general')"
|
||||
:label="$t('settings.interface')"
|
||||
icon="table-columns"
|
||||
>
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.theme') }}</h2>
|
||||
<div
|
||||
class="setting-section"
|
||||
:label="$t('settings.theme')"
|
||||
icon="paintbrush"
|
||||
>
|
||||
<h3>{{ $t('settings.style.style_section') }}</h3>
|
||||
<ul
|
||||
ref="themeList"
|
||||
class="theme-list"
|
||||
|
|
@ -17,10 +22,10 @@
|
|||
@click="resetTheming"
|
||||
>
|
||||
<preview id="theme-preview-stock" />
|
||||
<h4 class="theme-name">
|
||||
<span class="theme-name">
|
||||
{{ $t('settings.style.stock_theme_used') }}
|
||||
<span class="alert neutral version">v3</span>
|
||||
</h4>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="isCustomThemeUsed"
|
||||
|
|
@ -28,10 +33,10 @@
|
|||
class="button-default theme-preview toggled"
|
||||
>
|
||||
<preview />
|
||||
<h4 class="theme-name">
|
||||
<span class="theme-name">
|
||||
{{ $t('settings.style.custom_theme_used') }}
|
||||
<span class="alert neutral version">v2</span>
|
||||
</h4>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="isCustomStyleUsed"
|
||||
|
|
@ -39,25 +44,25 @@
|
|||
class="button-default theme-preview toggled"
|
||||
>
|
||||
<preview />
|
||||
<h4 class="theme-name">
|
||||
<span class="theme-name">
|
||||
{{ $t('settings.style.custom_style_used') }}
|
||||
<span class="alert neutral version">v3</span>
|
||||
</h4>
|
||||
</span>
|
||||
</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 }"
|
||||
:class="{ toggled: isStyleActive(style.key), disabled: switchInProgress }"
|
||||
:disabled="switchInProgress"
|
||||
@click="style.version === 'v2' ? setTheme(style.key) : setStyle(style.key)"
|
||||
>
|
||||
<preview :id="'theme-preview-' + style.key" />
|
||||
<h4 class="theme-name">
|
||||
<span class="theme-name">
|
||||
{{ style.name }}
|
||||
<span class="alert neutral version">{{ style.version }}</span>
|
||||
</h4>
|
||||
</span>
|
||||
</button>
|
||||
</ul>
|
||||
<div class="import-file-container">
|
||||
|
|
@ -70,16 +75,14 @@
|
|||
<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>
|
||||
<h4>{{ $t('settings.style.themes3.palette.label') }}</h4>
|
||||
<div
|
||||
v-if="customThemeVersion === 'v3'"
|
||||
class="palettes-container"
|
||||
>
|
||||
<h4 v-if="stylePalettes?.length > 0">
|
||||
<h5 v-if="stylePalettes?.length > 0">
|
||||
{{ $t('settings.style.themes3.palette.style') }}
|
||||
</h4>
|
||||
</h5>
|
||||
<div class="palettes">
|
||||
<button
|
||||
v-for="p in stylePalettes || []"
|
||||
|
|
@ -103,7 +106,7 @@
|
|||
/>
|
||||
</div>
|
||||
</button>
|
||||
<h4>{{ $t('settings.style.themes3.palette.bundled') }}</h4>
|
||||
<h5>{{ $t('settings.style.themes3.palette.bundled') }}</h5>
|
||||
<button
|
||||
v-for="p in bundledPalettes"
|
||||
:key="p.name"
|
||||
|
|
@ -130,9 +133,9 @@
|
|||
</div>
|
||||
<div>
|
||||
<template v-if="customThemeVersion === 'v3'">
|
||||
<h4 v-if="expertLevel > 0">
|
||||
<h5 v-if="expertLevel > 0">
|
||||
{{ $t('settings.style.themes3.palette.user') }}
|
||||
</h4>
|
||||
</h5>
|
||||
<PaletteEditor
|
||||
v-if="expertLevel > 0"
|
||||
v-model="userPalette"
|
||||
|
|
@ -150,236 +153,93 @@
|
|||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.background') }}</h2>
|
||||
<div class="banner-background-preview">
|
||||
<img :src="user.background_image">
|
||||
<button
|
||||
v-if="!isDefaultBackground"
|
||||
class="button-unstyled reset-button"
|
||||
:title="$t('settings.reset_profile_background')"
|
||||
@click="resetBackground"
|
||||
>
|
||||
<FAIcon
|
||||
icon="times"
|
||||
type="button"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<p>{{ $t('settings.set_new_background') }}</p>
|
||||
<img
|
||||
v-if="backgroundPreview"
|
||||
class="banner-background-preview"
|
||||
:src="backgroundPreview"
|
||||
>
|
||||
<div>
|
||||
<input
|
||||
type="file"
|
||||
class="input"
|
||||
@change="uploadFile('background', $event)"
|
||||
>
|
||||
</div>
|
||||
<FAIcon
|
||||
v-if="backgroundUploading"
|
||||
class="uploading"
|
||||
spin
|
||||
icon="circle-notch"
|
||||
/>
|
||||
<button
|
||||
v-else-if="backgroundPreview"
|
||||
class="btn button-default"
|
||||
@click="submitBackground(background)"
|
||||
>
|
||||
{{ $t('settings.save') }}
|
||||
</button>
|
||||
</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>
|
||||
<h3>{{ $t('settings.background') }}</h3>
|
||||
<div class="banner-background">
|
||||
<div class="banner-background-preview">
|
||||
<div class="fun-monitor">
|
||||
<div class="fun-monitor-stand button-default" />
|
||||
<div class="fun-monitor-neck button-default" />
|
||||
<div class="fun-monitor-display-bezel button-default">
|
||||
<div class="fun-monitor-display-screen input">
|
||||
<img
|
||||
v-if="backgroundPreview || user.background_image || instanceWallpaper"
|
||||
class="fun-monitor-display-screen-image"
|
||||
:src="backgroundPreview || user.background_image || instanceWallpaper"
|
||||
>
|
||||
<div
|
||||
v-else
|
||||
class="wallpaper"
|
||||
/>
|
||||
<div class="fun-monitor-display-screen-overlay input" />
|
||||
<div
|
||||
v-if="backgroundUploading"
|
||||
class="fun-monitor-display-uploading"
|
||||
>
|
||||
<FAIcon
|
||||
class="fun-monitor-display-screen-uploading"
|
||||
spin
|
||||
icon="circle-notch"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<UnitSetting
|
||||
path="emojiSize"
|
||||
:step="0.1"
|
||||
:units="['px', 'rem']"
|
||||
:reset-default="{ 'px': 32, 'rem': 2.2 }"
|
||||
</div>
|
||||
<div class="banner-background-input">
|
||||
<h4>{{ $t('settings.set_new_background') }}</h4>
|
||||
<input
|
||||
type="file"
|
||||
class="input"
|
||||
@change="uploadFile('background', $event)"
|
||||
>
|
||||
{{ $t('settings.emoji_size') }}
|
||||
</UnitSetting>
|
||||
<ul
|
||||
class="setting-list suboptions"
|
||||
>
|
||||
<li>
|
||||
<FloatSetting
|
||||
v-if="user"
|
||||
path="emojiReactionsScale"
|
||||
expert="1"
|
||||
>
|
||||
{{ $t('settings.emoji_reactions_scale') }}
|
||||
</FloatSetting>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<UnitSetting
|
||||
path="navbarSize"
|
||||
:step="0.1"
|
||||
:units="['px', 'rem']"
|
||||
:reset-default="{ 'px': 55, 'rem': 3.5 }"
|
||||
>
|
||||
{{ $t('settings.navbar_size') }}
|
||||
</UnitSetting>
|
||||
</li>
|
||||
<h3>{{ $t('settings.style.interface_font_user_override') }}</h3>
|
||||
<li>
|
||||
<FontControl
|
||||
:model-value="mergedConfig.theme3hacks.fonts.interface"
|
||||
name="ui"
|
||||
:label="$t('settings.style.fonts.components.interface')"
|
||||
:fallback="{ family: 'sans-serif' }"
|
||||
no-inherit="1"
|
||||
@update:model-value="v => updateFont('interface', v)"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<FontControl
|
||||
v-if="expertLevel > 0"
|
||||
:model-value="mergedConfig.theme3hacks.fonts.input"
|
||||
name="input"
|
||||
:fallback="{ family: 'inherit' }"
|
||||
:label="$t('settings.style.fonts.components.input')"
|
||||
@update:model-value="v => updateFont('input', v)"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<FontControl
|
||||
v-if="expertLevel > 0"
|
||||
:model-value="mergedConfig.theme3hacks.fonts.post"
|
||||
name="post"
|
||||
:fallback="{ family: 'inherit' }"
|
||||
:label="$t('settings.style.fonts.components.post')"
|
||||
@update:model-value="v => updateFont('post', v)"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<FontControl
|
||||
v-if="expertLevel > 0"
|
||||
:model-value="mergedConfig.theme3hacks.fonts.monospace"
|
||||
name="postCode"
|
||||
:fallback="{ family: 'monospace' }"
|
||||
:label="$t('settings.style.fonts.components.monospace')"
|
||||
@update:model-value="v => updateFont('monospace', v)"
|
||||
/>
|
||||
</li>
|
||||
<h3>{{ $t('settings.columns') }}</h3>
|
||||
<li>
|
||||
<UnitSetting
|
||||
path="panelHeaderSize"
|
||||
:step="0.1"
|
||||
:units="['px', 'rem']"
|
||||
:reset-default="{ 'px': 52, 'rem': 3.2 }"
|
||||
timed-apply-mode
|
||||
>
|
||||
{{ $t('settings.panel_header_size') }}
|
||||
</UnitSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path="sidebarRight">
|
||||
{{ $t('settings.right_sidebar') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path="navbarColumnStretch">
|
||||
{{ $t('settings.navbar_column_stretch') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<ChoiceSetting
|
||||
v-if="user"
|
||||
id="thirdColumnMode"
|
||||
path="thirdColumnMode"
|
||||
:options="thirdColumnModeOptions"
|
||||
>
|
||||
{{ $t('settings.third_column_mode') }}
|
||||
</ChoiceSetting>
|
||||
</li>
|
||||
<li v-if="expertLevel > 0">
|
||||
{{ $t('settings.column_sizes') }}
|
||||
<div class="column-settings">
|
||||
<UnitSetting
|
||||
v-for="column in columns"
|
||||
:key="column"
|
||||
:path="column + 'ColumnWidth'"
|
||||
:units="horizontalUnits"
|
||||
expert="1"
|
||||
<div class="custom-bg-control">
|
||||
<button
|
||||
:disabled="!backgroundPreview"
|
||||
class="btn button-default"
|
||||
@click="submitBackground(background)"
|
||||
>
|
||||
{{ $t('settings.column_sizes_' + column) }}
|
||||
</UnitSetting>
|
||||
{{ $t('settings.save') }}
|
||||
</button>
|
||||
<button
|
||||
:disabled="!backgroundPreview"
|
||||
class="btn button-default"
|
||||
@click="resetUploadedBackground"
|
||||
>
|
||||
{{ $t('settings.reset') }}
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path="disableStickyHeaders">
|
||||
{{ $t('settings.disable_sticky_headers') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path="showScrollbars">
|
||||
{{ $t('settings.show_scrollbars') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<UnitSetting
|
||||
path="themeEditorMinWidth"
|
||||
:units="['px', 'rem']"
|
||||
expert="1"
|
||||
<div
|
||||
v-if="backgroundError"
|
||||
class="alert error -dismissible"
|
||||
>
|
||||
{{ $t('settings.theme_editor_min_width') }}
|
||||
</UnitSetting>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.visual_tweaks') }}</h2>
|
||||
<span>
|
||||
{{ backgroundError }}
|
||||
</span>
|
||||
<button
|
||||
class="button-unstyled"
|
||||
@click="clearBackgroundError"
|
||||
>
|
||||
<FAIcon
|
||||
class="fa-scale-110 fa-old-padding"
|
||||
icon="times"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
v-if="!isDefaultBackground"
|
||||
class="btn button-default reset-button"
|
||||
:title="$t('settings.reset_profile_background')"
|
||||
@click="resetBackground"
|
||||
>
|
||||
{{ $t('settings.reset_profile_background') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<h3>{{ $t('settings.visual_tweaks') }}</h3>
|
||||
<div class="alert neutral theme-notice">
|
||||
{{ $t("settings.style.visual_tweaks_section_note") }}
|
||||
</div>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<BooleanSetting path="modalMobileCenter">
|
||||
{{ $t('settings.mobile_center_dialog') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<ChoiceSetting
|
||||
id="forcedRoundness"
|
||||
|
|
@ -392,7 +252,7 @@
|
|||
<li>
|
||||
<ChoiceSetting
|
||||
id="underlayOverride"
|
||||
path="theme3hacks.underlay"
|
||||
path="underlay"
|
||||
:options="underlayOverrideModes"
|
||||
>
|
||||
{{ $t('settings.style.themes3.hacks.underlay_overrides') }}
|
||||
|
|
@ -404,19 +264,13 @@
|
|||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting
|
||||
path="forceThemeRecompilation"
|
||||
:expert="1"
|
||||
>
|
||||
{{ $t('settings.force_theme_recompilation_debug') }}
|
||||
<BooleanSetting path="allowForeignUserBackground">
|
||||
{{ $t('settings.foreign_user_background') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting
|
||||
path="themeDebug"
|
||||
:expert="1"
|
||||
>
|
||||
{{ $t('settings.theme_debug') }}
|
||||
<BooleanSetting path="compactProfiles">
|
||||
{{ $t('settings.compact_profiles') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
</ul>
|
||||
|
|
|
|||
159
src/components/settings_modal/tabs/clutter_tab.js
Normal file
159
src/components/settings_modal/tabs/clutter_tab.js
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
import { mapActions, mapState } from 'pinia'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||
import Select from 'src/components/select/select.vue'
|
||||
import BooleanSetting from '../helpers/boolean_setting.vue'
|
||||
import ChoiceSetting from '../helpers/choice_setting.vue'
|
||||
import HelpIndicator from '../helpers/help_indicator.vue'
|
||||
import IntegerSetting from '../helpers/integer_setting.vue'
|
||||
import SharedComputedObject from '../helpers/shared_computed_object.js'
|
||||
import UnitSetting from '../helpers/unit_setting.vue'
|
||||
|
||||
import { useInstanceStore } from 'src/stores/instance.js'
|
||||
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
|
||||
import { useSyncConfigStore } from 'src/stores/sync_config.js'
|
||||
|
||||
const ClutterTab = {
|
||||
components: {
|
||||
BooleanSetting,
|
||||
ChoiceSetting,
|
||||
UnitSetting,
|
||||
IntegerSetting,
|
||||
Checkbox,
|
||||
Select,
|
||||
HelpIndicator,
|
||||
},
|
||||
computed: {
|
||||
...SharedComputedObject(),
|
||||
...mapState(useInstanceCapabilitiesStore, ['shoutAvailable']),
|
||||
...mapState(useInstanceStore, {
|
||||
showFeaturesPanel: (store) => store.instanceIdentity.showFeaturesPanel,
|
||||
instanceSpecificPanelPresent: (store) =>
|
||||
store.instanceIdentity.showInstanceSpecificPanel &&
|
||||
store.instanceIdentity.instanceSpecificPanelContent,
|
||||
}),
|
||||
...mapState(useSyncConfigStore, {
|
||||
muteFilters: (store) =>
|
||||
Object.entries(store.prefsStorage.simple.muteFilters),
|
||||
muteFiltersObject: (store) => store.prefsStorage.simple.muteFilters,
|
||||
}),
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useSyncConfigStore, [
|
||||
'setSimplePrefAndSave',
|
||||
'unsetSimplePrefAndSave',
|
||||
'pushSyncConfig',
|
||||
]),
|
||||
getDatetimeLocal(timestamp) {
|
||||
const date = new Date(timestamp)
|
||||
const fmt = new Intl.NumberFormat('en-US', { minimumIntegerDigits: 2 })
|
||||
const datetime = [
|
||||
date.getFullYear(),
|
||||
'-',
|
||||
fmt.format(date.getMonth() + 1),
|
||||
'-',
|
||||
fmt.format(date.getDate()),
|
||||
'T',
|
||||
fmt.format(date.getHours()),
|
||||
':',
|
||||
fmt.format(date.getMinutes()),
|
||||
].join('')
|
||||
return datetime
|
||||
},
|
||||
checkRegexValid(id) {
|
||||
const filter = this.muteFiltersObject[id]
|
||||
if (filter.type !== 'regexp') return true
|
||||
if (filter.type !== 'user_regexp') return true
|
||||
const { value } = filter
|
||||
let valid = true
|
||||
try {
|
||||
new RegExp(value)
|
||||
} catch {
|
||||
valid = false
|
||||
console.error('Invalid RegExp: ' + value)
|
||||
}
|
||||
return valid
|
||||
},
|
||||
createFilter(
|
||||
filter = {
|
||||
type: 'word',
|
||||
value: '',
|
||||
name: 'New Filter',
|
||||
enabled: true,
|
||||
expires: null,
|
||||
hide: false,
|
||||
},
|
||||
) {
|
||||
const newId = uuidv4()
|
||||
|
||||
filter.order = this.muteFilters.length + 2
|
||||
this.muteFiltersDraftObject[newId] = filter
|
||||
this.setSimplePrefAndSave({ path: 'muteFilters.' + newId, value: filter })
|
||||
this.pushSyncConfig()
|
||||
},
|
||||
exportFilter(id) {
|
||||
this.exportedFilter = { ...this.muteFiltersDraftObject[id] }
|
||||
delete this.exportedFilter.order
|
||||
this.filterExporter.exportData()
|
||||
},
|
||||
importFilter() {
|
||||
this.filterImporter.importData()
|
||||
},
|
||||
copyFilter(id) {
|
||||
const filter = { ...this.muteFiltersDraftObject[id] }
|
||||
const newId = uuidv4()
|
||||
|
||||
this.muteFiltersDraftObject[newId] = filter
|
||||
this.setSimplePrefAndSave({ path: 'muteFilters.' + newId, value: filter })
|
||||
this.pushSyncConfig()
|
||||
},
|
||||
deleteFilter(id) {
|
||||
delete this.muteFiltersDraftObject[id]
|
||||
this.unsetSimplePrefAndSave({ path: 'muteFilters.' + id, value: null })
|
||||
this.pushSyncConfig()
|
||||
},
|
||||
purgeExpiredFilters() {
|
||||
this.muteFiltersExpired.forEach(([id]) => {
|
||||
delete this.muteFiltersDraftObject[id]
|
||||
this.unsetSimplePrefAndSave({ path: 'muteFilters.' + id, value: null })
|
||||
})
|
||||
this.pushSyncConfig()
|
||||
},
|
||||
updateFilter(id, field, value) {
|
||||
const filter = { ...this.muteFiltersDraftObject[id] }
|
||||
if (field === 'expires-never') {
|
||||
if (!value) {
|
||||
const offset = 1000 * 60 * 60 * 24 * 14 // 2 weeks
|
||||
const date = Date.now() + offset
|
||||
filter.expires = date
|
||||
} else {
|
||||
filter.expires = null
|
||||
}
|
||||
} else if (field === 'expires') {
|
||||
const parsed = Date.parse(value)
|
||||
filter.expires = parsed.valueOf()
|
||||
} else {
|
||||
filter[field] = value
|
||||
}
|
||||
this.muteFiltersDraftObject[id] = filter
|
||||
this.muteFiltersDraftDirty[id] = true
|
||||
},
|
||||
saveFilter(id) {
|
||||
this.setSimplePrefAndSave({
|
||||
path: 'muteFilters.' + id,
|
||||
value: this.muteFiltersDraftObject[id],
|
||||
})
|
||||
this.pushSyncConfig()
|
||||
this.muteFiltersDraftDirty[id] = false
|
||||
},
|
||||
},
|
||||
// Updating nested properties
|
||||
watch: {
|
||||
replyVisibility() {
|
||||
this.$store.dispatch('queueFlushAll')
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default ClutterTab
|
||||
95
src/components/settings_modal/tabs/clutter_tab.vue
Normal file
95
src/components/settings_modal/tabs/clutter_tab.vue
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
<template>
|
||||
<div class="clutter-tab">
|
||||
<div class="setting-section">
|
||||
<h3>{{ $t('settings.interface') }}</h3>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<BooleanSetting path="alwaysShowSubjectInput">
|
||||
{{ $t('settings.subject_input_always_show') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path="minimalScopesMode">
|
||||
{{ $t('settings.minimal_scopes_mode') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path="hidePostStats">
|
||||
{{ $t('settings.hide_post_stats') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path="hideUserStats">
|
||||
{{ $t('settings.hide_user_stats') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path="hideBotIndication">
|
||||
{{ $t('settings.hide_actor_type_indication') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path="hideScrobbles">
|
||||
{{ $t('settings.hide_scrobbles') }}
|
||||
</BooleanSetting>
|
||||
<ul class="setting-list suboptions">
|
||||
<li>
|
||||
<UnitSetting
|
||||
key="hideScrobblesAfter"
|
||||
path="hideScrobblesAfter"
|
||||
:units="['m', 'h', 'd']"
|
||||
unit-set="time"
|
||||
>
|
||||
{{ $t('settings.hide_scrobbles_after') }}
|
||||
</UnitSetting>
|
||||
</li>
|
||||
<li v-if="instanceSpecificPanelPresent">
|
||||
<BooleanSetting path="hideISP">
|
||||
{{ $t('settings.hide_isp') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path="userCardHidePersonalMarks">
|
||||
{{ $t('settings.user_card_hide_personal_marks') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li v-if="shoutAvailable">
|
||||
<BooleanSetting path="hideShoutbox">
|
||||
{{ $t('settings.hide_shoutbox') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
</ul>
|
||||
<h3>{{ $t('settings.attachments') }}</h3>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<IntegerSetting
|
||||
path="maxThumbnails"
|
||||
:min="0"
|
||||
>
|
||||
{{ $t('settings.max_thumbnails') }}
|
||||
</IntegerSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting
|
||||
:local="true"
|
||||
path="hideAttachments"
|
||||
>
|
||||
{{ $t('settings.hide_attachments_in_tl') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting
|
||||
:local="true"
|
||||
path="hideAttachmentsInConv"
|
||||
>
|
||||
{{ $t('settings.hide_attachments_in_convo') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./clutter_tab.js"></script>
|
||||
189
src/components/settings_modal/tabs/composing_tab.js
Normal file
189
src/components/settings_modal/tabs/composing_tab.js
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
import { mapState } from 'pinia'
|
||||
|
||||
import FontControl from 'src/components/font_control/font_control.vue'
|
||||
import InterfaceLanguageSwitcher from 'src/components/interface_language_switcher/interface_language_switcher.vue'
|
||||
import ScopeSelector from 'src/components/scope_selector/scope_selector.vue'
|
||||
import Select from 'src/components/select/select.vue'
|
||||
import BooleanSetting from '../helpers/boolean_setting.vue'
|
||||
import ChoiceSetting from '../helpers/choice_setting.vue'
|
||||
import FloatSetting from '../helpers/float_setting.vue'
|
||||
import IntegerSetting from '../helpers/integer_setting.vue'
|
||||
import SharedComputedObject from '../helpers/shared_computed_object.js'
|
||||
import UnitSetting from '../helpers/unit_setting.vue'
|
||||
|
||||
import { useInstanceStore } from 'src/stores/instance.js'
|
||||
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
|
||||
import { useMergedConfigStore } from 'src/stores/merged_config.js'
|
||||
import { useSyncConfigStore } from 'src/stores/sync_config.js'
|
||||
|
||||
import localeService from 'src/services/locale/locale.service.js'
|
||||
import { cacheKey, clearCache, emojiCacheKey } from 'src/services/sw/sw.js'
|
||||
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faDatabase,
|
||||
faGlobe,
|
||||
faMessage,
|
||||
faPenAlt,
|
||||
faSliders,
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(faGlobe, faMessage, faPenAlt, faDatabase, faSliders)
|
||||
|
||||
const ComposingTab = {
|
||||
data() {
|
||||
return {
|
||||
subjectLineOptions: ['email', 'noop', 'masto'].map((mode) => ({
|
||||
key: mode,
|
||||
value: mode,
|
||||
label: this.$t(
|
||||
`settings.subject_line_${mode === 'masto' ? 'mastodon' : mode}`,
|
||||
),
|
||||
})),
|
||||
conversationDisplayOptions: ['tree', 'linear'].map((mode) => ({
|
||||
key: mode,
|
||||
value: mode,
|
||||
label: this.$t(`settings.conversation_display_${mode}`),
|
||||
})),
|
||||
absoluteTime12hOptions: ['24h', '12h'].map((mode) => ({
|
||||
key: mode,
|
||||
value: mode,
|
||||
label: this.$t(`settings.absolute_time_format_12h_${mode}`),
|
||||
})),
|
||||
conversationOtherRepliesButtonOptions: ['below', 'inside'].map(
|
||||
(mode) => ({
|
||||
key: mode,
|
||||
value: mode,
|
||||
label: this.$t(`settings.conversation_other_replies_button_${mode}`),
|
||||
}),
|
||||
),
|
||||
mentionLinkDisplayOptions: ['short', 'full_for_remote', 'full'].map(
|
||||
(mode) => ({
|
||||
key: mode,
|
||||
value: mode,
|
||||
label: this.$t(`settings.mention_link_display_${mode}`),
|
||||
}),
|
||||
),
|
||||
userPopoverAvatarActionOptions: ['close', 'zoom', 'open'].map((mode) => ({
|
||||
key: mode,
|
||||
value: mode,
|
||||
label: this.$t(`settings.user_popover_avatar_action_${mode}`),
|
||||
})),
|
||||
unsavedPostActionOptions: ['save', 'discard', 'confirm'].map((mode) => ({
|
||||
key: mode,
|
||||
value: mode,
|
||||
label: this.$t(`settings.unsaved_post_action_${mode}`),
|
||||
})),
|
||||
loopSilentAvailable:
|
||||
// Firefox
|
||||
Object.getOwnPropertyDescriptor(
|
||||
HTMLVideoElement.prototype,
|
||||
'mozHasAudio',
|
||||
) ||
|
||||
// Chrome-likes
|
||||
Object.getOwnPropertyDescriptor(
|
||||
HTMLMediaElement.prototype,
|
||||
'webkitAudioDecodedByteCount',
|
||||
) ||
|
||||
// Future spec, still not supported in Nightly 63 as of 08/2018
|
||||
Object.getOwnPropertyDescriptor(
|
||||
HTMLMediaElement.prototype,
|
||||
'audioTracks',
|
||||
),
|
||||
emailLanguage: this.$store.state.users.currentUser.language || [''],
|
||||
}
|
||||
},
|
||||
components: {
|
||||
BooleanSetting,
|
||||
ChoiceSetting,
|
||||
IntegerSetting,
|
||||
FloatSetting,
|
||||
UnitSetting,
|
||||
InterfaceLanguageSwitcher,
|
||||
ScopeSelector,
|
||||
Select,
|
||||
FontControl,
|
||||
},
|
||||
computed: {
|
||||
postFormats() {
|
||||
return useInstanceCapabilitiesStore().postFormats
|
||||
},
|
||||
postContentOptions() {
|
||||
return this.postFormats.map((format) => ({
|
||||
key: format,
|
||||
value: format,
|
||||
label: this.$t(`post_status.content_type["${format}"]`),
|
||||
}))
|
||||
},
|
||||
language: {
|
||||
get: function () {
|
||||
return useMergedConfigStore().mergedConfig.interfaceLanguage
|
||||
},
|
||||
set: function (val) {
|
||||
useSyncConfigStore().setSimplePrefAndSave({
|
||||
path: 'interfaceLanguage',
|
||||
value: val,
|
||||
})
|
||||
},
|
||||
},
|
||||
...SharedComputedObject(),
|
||||
...mapState(useInstanceStore, ['blockExpiration']),
|
||||
},
|
||||
methods: {
|
||||
changeDefaultScope(value) {
|
||||
this.$store.dispatch('setProfileOption', { name: 'defaultScope', value })
|
||||
},
|
||||
clearCache(key) {
|
||||
clearCache(key)
|
||||
.then(() => {
|
||||
this.$store.dispatch('settingsSaved', { success: true })
|
||||
})
|
||||
.catch((error) => {
|
||||
this.$store.dispatch('settingsSaved', { error })
|
||||
})
|
||||
},
|
||||
tooSmall() {
|
||||
this.$emit('tooSmall')
|
||||
},
|
||||
tooBig() {
|
||||
this.$emit('tooBig')
|
||||
},
|
||||
getNavMode() {
|
||||
return this.$refs.tabSwitcher.getNavMode()
|
||||
},
|
||||
clearAssetCache() {
|
||||
this.clearCache(cacheKey)
|
||||
},
|
||||
clearEmojiCache() {
|
||||
this.clearCache(emojiCacheKey)
|
||||
},
|
||||
updateProfile() {
|
||||
const params = {
|
||||
language: localeService.internalToBackendLocaleMulti(
|
||||
this.emailLanguage,
|
||||
),
|
||||
}
|
||||
|
||||
this.$store.state.api.backendInteractor
|
||||
.updateProfile({ params })
|
||||
.then((user) => {
|
||||
this.$store.commit('addNewUsers', [user])
|
||||
this.$store.commit('setCurrentUser', user)
|
||||
})
|
||||
},
|
||||
updateFont(key, value) {
|
||||
useSyncConfigStore().setSimplePrefAndSave({
|
||||
path: 'theme3hacks',
|
||||
value: {
|
||||
...this.mergedConfig.theme3hacks,
|
||||
fonts: {
|
||||
...this.mergedConfig.theme3hacks.fonts,
|
||||
[key]: value,
|
||||
},
|
||||
},
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default ComposingTab
|
||||
117
src/components/settings_modal/tabs/composing_tab.vue
Normal file
117
src/components/settings_modal/tabs/composing_tab.vue
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
<template>
|
||||
<div :label="$t('settings.posts')">
|
||||
<div class="setting-section">
|
||||
<h3>{{ $t('settings.general') }}</h3>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<label
|
||||
class="setting-item "
|
||||
for="default-vis"
|
||||
>
|
||||
<ScopeSelector
|
||||
class="scope-selector setting-control"
|
||||
:show-all="true"
|
||||
:user-default="$store.state.profileConfig.defaultScope"
|
||||
:initial-scope="$store.state.profileConfig.defaultScope"
|
||||
:on-scope-change="changeDefaultScope"
|
||||
:unstyled="false"
|
||||
/>
|
||||
</label>
|
||||
</li>
|
||||
<li>
|
||||
<!-- <BooleanSetting source="profile" path="defaultNSFW"> -->
|
||||
<BooleanSetting path="sensitiveByDefault">
|
||||
{{ $t('settings.sensitive_by_default') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li v-if="postFormats.length > 0">
|
||||
<ChoiceSetting
|
||||
id="postContentType"
|
||||
path="postContentType"
|
||||
:options="postContentOptions"
|
||||
:local="true"
|
||||
>
|
||||
{{ $t('settings.default_post_status_content_type') }}
|
||||
</ChoiceSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path="padEmoji">
|
||||
{{ $t('settings.pad_emoji') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting
|
||||
path="autocompleteSelect"
|
||||
expert="1"
|
||||
>
|
||||
{{ $t('settings.autocomplete_select_first') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting
|
||||
path="autoSaveDraft"
|
||||
>
|
||||
{{ $t('settings.auto_save_draft') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li v-if="!mergedConfig.autoSaveDraft">
|
||||
<ChoiceSetting
|
||||
id="unsavedPostAction"
|
||||
path="unsavedPostAction"
|
||||
:options="unsavedPostActionOptions"
|
||||
expert="1"
|
||||
>
|
||||
{{ $t('settings.unsaved_post_action') }}
|
||||
</ChoiceSetting>
|
||||
</li>
|
||||
</ul>
|
||||
<h3>{{ $t('settings.replies') }}</h3>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<BooleanSetting
|
||||
path="scopeCopy"
|
||||
>
|
||||
{{ $t('settings.scope_copy') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<ChoiceSetting
|
||||
id="subjectLineBehavior"
|
||||
path="subjectLineBehavior"
|
||||
:options="subjectLineOptions"
|
||||
>
|
||||
{{ $t('settings.subject_line_behavior') }}
|
||||
</ChoiceSetting>
|
||||
</li>
|
||||
</ul>
|
||||
<h3 v-if="expertLevel > 0">
|
||||
{{ $t('settings.attachments') }}
|
||||
</h3>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<BooleanSetting
|
||||
path="imageCompression"
|
||||
:local="true"
|
||||
expert="1"
|
||||
>
|
||||
{{ $t('settings.image_compression') }}
|
||||
</BooleanSetting>
|
||||
<ul class="setting-list suboptions">
|
||||
<li>
|
||||
<BooleanSetting
|
||||
path="alwaysUseJpeg"
|
||||
:local="true"
|
||||
expert="1"
|
||||
parent-path="imageCompression"
|
||||
>
|
||||
{{ $t('settings.always_use_jpeg') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./composing_tab.js"></script>
|
||||
|
|
@ -1,85 +1,90 @@
|
|||
import Importer from 'src/components/importer/importer.vue'
|
||||
import Exporter from 'src/components/exporter/exporter.vue'
|
||||
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||
import { mapState } from 'vuex'
|
||||
import { useOAuthTokensStore } from 'src/stores/oauth_tokens'
|
||||
|
||||
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||
import Exporter from 'src/components/exporter/exporter.vue'
|
||||
import Importer from 'src/components/importer/importer.vue'
|
||||
|
||||
import { useOAuthTokensStore } from 'src/stores/oauth_tokens.js'
|
||||
|
||||
const DataImportExportTab = {
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
activeTab: 'profile',
|
||||
newDomainToMute: '',
|
||||
listBackupsError: false,
|
||||
addBackupError: false,
|
||||
addedBackup: false,
|
||||
backups: []
|
||||
backups: [],
|
||||
}
|
||||
},
|
||||
created () {
|
||||
created() {
|
||||
useOAuthTokensStore().fetchTokens()
|
||||
this.fetchBackups()
|
||||
},
|
||||
components: {
|
||||
Importer,
|
||||
Exporter,
|
||||
Checkbox
|
||||
Checkbox,
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
backendInteractor: (state) => state.api.backendInteractor,
|
||||
user: (state) => state.users.currentUser
|
||||
})
|
||||
user: (state) => state.users.currentUser,
|
||||
}),
|
||||
},
|
||||
methods: {
|
||||
getFollowsContent () {
|
||||
return this.backendInteractor.exportFriends({ id: this.user.id })
|
||||
getFollowsContent() {
|
||||
return this.backendInteractor
|
||||
.exportFriends({ id: this.user.id })
|
||||
.then(this.generateExportableUsersContent)
|
||||
},
|
||||
getBlocksContent () {
|
||||
return this.backendInteractor.fetchBlocks()
|
||||
getBlocksContent() {
|
||||
return this.backendInteractor
|
||||
.fetchBlocks()
|
||||
.then(this.generateExportableUsersContent)
|
||||
},
|
||||
getMutesContent () {
|
||||
return this.backendInteractor.fetchMutes()
|
||||
getMutesContent() {
|
||||
return this.backendInteractor
|
||||
.fetchMutes()
|
||||
.then(this.generateExportableUsersContent)
|
||||
},
|
||||
importFollows (file) {
|
||||
return this.backendInteractor.importFollows({ file })
|
||||
.then((status) => {
|
||||
if (!status) {
|
||||
throw new Error('failed')
|
||||
}
|
||||
})
|
||||
},
|
||||
importBlocks (file) {
|
||||
return this.backendInteractor.importBlocks({ file })
|
||||
.then((status) => {
|
||||
if (!status) {
|
||||
throw new Error('failed')
|
||||
}
|
||||
})
|
||||
},
|
||||
importMutes (file) {
|
||||
return this.backendInteractor.importMutes({ file })
|
||||
.then((status) => {
|
||||
if (!status) {
|
||||
throw new Error('failed')
|
||||
}
|
||||
})
|
||||
},
|
||||
generateExportableUsersContent (users) {
|
||||
// Get addresses
|
||||
return users.map((user) => {
|
||||
// check is it's a local user
|
||||
if (user && user.is_local) {
|
||||
// append the instance address
|
||||
return user.screen_name + '@' + location.hostname
|
||||
importFollows(file) {
|
||||
return this.backendInteractor.importFollows({ file }).then((status) => {
|
||||
if (!status) {
|
||||
throw new Error('failed')
|
||||
}
|
||||
return user.screen_name
|
||||
}).join('\n')
|
||||
})
|
||||
},
|
||||
addBackup () {
|
||||
this.$store.state.api.backendInteractor.addBackup()
|
||||
importBlocks(file) {
|
||||
return this.backendInteractor.importBlocks({ file }).then((status) => {
|
||||
if (!status) {
|
||||
throw new Error('failed')
|
||||
}
|
||||
})
|
||||
},
|
||||
importMutes(file) {
|
||||
return this.backendInteractor.importMutes({ file }).then((status) => {
|
||||
if (!status) {
|
||||
throw new Error('failed')
|
||||
}
|
||||
})
|
||||
},
|
||||
generateExportableUsersContent(users) {
|
||||
// Get addresses
|
||||
return users
|
||||
.map((user) => {
|
||||
// check is it's a local user
|
||||
if (user && user.is_local) {
|
||||
// append the instance address
|
||||
return user.screen_name + '@' + location.hostname
|
||||
}
|
||||
return user.screen_name
|
||||
})
|
||||
.join('\n')
|
||||
},
|
||||
addBackup() {
|
||||
this.$store.state.api.backendInteractor
|
||||
.addBackup()
|
||||
.then(() => {
|
||||
this.addedBackup = true
|
||||
this.addBackupError = false
|
||||
|
|
@ -90,8 +95,9 @@ const DataImportExportTab = {
|
|||
})
|
||||
.then(() => this.fetchBackups())
|
||||
},
|
||||
fetchBackups () {
|
||||
this.$store.state.api.backendInteractor.listBackups()
|
||||
fetchBackups() {
|
||||
this.$store.state.api.backendInteractor
|
||||
.listBackups()
|
||||
.then((res) => {
|
||||
this.backups = res
|
||||
this.listBackupsError = false
|
||||
|
|
@ -99,8 +105,8 @@ const DataImportExportTab = {
|
|||
.catch((error) => {
|
||||
this.listBackupsError = error.error
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default DataImportExportTab
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
.data-import-export-tab {
|
||||
.importer-exporter {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5em;
|
||||
}
|
||||
|
||||
table {
|
||||
td, th {
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
th {
|
||||
padding: 0 0.5em;
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 0.5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,62 +1,77 @@
|
|||
<template>
|
||||
<div
|
||||
class="data-import-export-tab"
|
||||
:label="$t('settings.data_import_export_tab')"
|
||||
>
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.follow_import') }}</h2>
|
||||
<p>{{ $t('settings.import_followers_from_a_csv_file') }}</p>
|
||||
<Importer
|
||||
:submit-handler="importFollows"
|
||||
:success-message="$t('settings.follows_imported')"
|
||||
:error-message="$t('settings.follow_import_error')"
|
||||
/>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.follow_export') }}</h2>
|
||||
<Exporter
|
||||
:get-content="getFollowsContent"
|
||||
filename="friends.csv"
|
||||
:export-button-label="$t('settings.follow_export_button')"
|
||||
/>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.block_import') }}</h2>
|
||||
<p>{{ $t('settings.import_blocks_from_a_csv_file') }}</p>
|
||||
<Importer
|
||||
:submit-handler="importBlocks"
|
||||
:success-message="$t('settings.blocks_imported')"
|
||||
:error-message="$t('settings.block_import_error')"
|
||||
/>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.block_export') }}</h2>
|
||||
<Exporter
|
||||
:get-content="getBlocksContent"
|
||||
filename="blocks.csv"
|
||||
:export-button-label="$t('settings.block_export_button')"
|
||||
/>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.mute_import') }}</h2>
|
||||
<p>{{ $t('settings.import_mutes_from_a_csv_file') }}</p>
|
||||
<Importer
|
||||
:submit-handler="importMutes"
|
||||
:success-message="$t('settings.mutes_imported')"
|
||||
:error-message="$t('settings.mute_import_error')"
|
||||
/>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.mute_export') }}</h2>
|
||||
<Exporter
|
||||
:get-content="getMutesContent"
|
||||
filename="mutes.csv"
|
||||
:export-button-label="$t('settings.mute_export_button')"
|
||||
/>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.account_backup') }}</h2>
|
||||
<p>{{ $t('settings.account_backup_description') }}</p>
|
||||
<table>
|
||||
<div class="setting-section">
|
||||
<h3>{{ $t('settings.import_export.title') }}</h3>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<h4>{{ $t('settings.import_export.follows') }}</h4>
|
||||
<p>{{ $t('settings.import_followers_from_a_csv_file') }}</p>
|
||||
<div class="importer-exporter">
|
||||
<Importer
|
||||
:submit-handler="importFollows"
|
||||
:success-message="$t('settings.follows_imported')"
|
||||
:error-message="$t('settings.follow_import_error')"
|
||||
/>
|
||||
<Exporter
|
||||
:get-content="getFollowsContent"
|
||||
filename="friends.csv"
|
||||
:export-button-label="$t('settings.follow_export_button')"
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<h4>{{ $t('settings.import_export.mutes') }}</h4>
|
||||
<p>{{ $t('settings.import_mutes_from_a_csv_file') }}</p>
|
||||
<div class="importer-exporter">
|
||||
<Importer
|
||||
:submit-handler="importMutes"
|
||||
:success-message="$t('settings.mutes_imported')"
|
||||
:error-message="$t('settings.mute_import_error')"
|
||||
/>
|
||||
<Exporter
|
||||
:get-content="getMutesContent"
|
||||
filename="friends.csv"
|
||||
:export-button-label="$t('settings.mute_export_button')"
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<h4>{{ $t('settings.import_export.blocks') }}</h4>
|
||||
<p>{{ $t('settings.import_blocks_from_a_csv_file') }}</p>
|
||||
<div class="importer-exporter">
|
||||
<Importer
|
||||
:submit-handler="importBlocks"
|
||||
:success-message="$t('settings.blocks_imported')"
|
||||
:error-message="$t('settings.block_import_error')"
|
||||
/>
|
||||
<Exporter
|
||||
:get-content="getBlocksContent"
|
||||
filename="friends.csv"
|
||||
:export-button-label="$t('settings.block_export_button')"
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<h3>{{ $t('settings.account_backup') }}</h3>
|
||||
<div class="setting-list">
|
||||
<p>{{ $t('settings.account_backup_description') }}</p>
|
||||
<button
|
||||
class="btn button-default"
|
||||
@click="addBackup"
|
||||
>
|
||||
{{ $t('settings.add_backup') }}
|
||||
</button>
|
||||
<p v-if="addedBackup">
|
||||
{{ $t('settings.added_backup') }}
|
||||
</p>
|
||||
<template v-if="addBackupError !== false">
|
||||
<p>{{ $t('settings.add_backup_error', { error: addBackupError }) }}</p>
|
||||
</template>
|
||||
</div>
|
||||
<table class="setting-list">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ $t('settings.account_backup_table_head') }}</th>
|
||||
|
|
@ -111,21 +126,9 @@
|
|||
/>
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
class="btn button-default"
|
||||
@click="addBackup"
|
||||
>
|
||||
{{ $t('settings.add_backup') }}
|
||||
</button>
|
||||
<p v-if="addedBackup">
|
||||
{{ $t('settings.added_backup') }}
|
||||
</p>
|
||||
<template v-if="addBackupError !== false">
|
||||
<p>{{ $t('settings.add_backup_error', { error: addBackupError }) }}</p>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./data_import_export_tab.js"></script>
|
||||
<!-- <style lang="scss" src="./profile.scss"></style> -->
|
||||
<style lang="scss" src="./data_import_export_tab.scss"></style>
|
||||
|
|
|
|||
47
src/components/settings_modal/tabs/developer_tab.js
Normal file
47
src/components/settings_modal/tabs/developer_tab.js
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import { mapState } from 'pinia'
|
||||
|
||||
import BooleanSetting from '../helpers/boolean_setting.vue'
|
||||
import SharedComputedObject from '../helpers/shared_computed_object.js'
|
||||
|
||||
import { useInstanceStore } from 'src/stores/instance.js'
|
||||
|
||||
import { cacheKey, clearCache, emojiCacheKey } from 'src/services/sw/sw.js'
|
||||
|
||||
const pleromaFeCommitUrl =
|
||||
'https://git.pleroma.social/pleroma/pleroma-fe/commit/'
|
||||
|
||||
const VersionTab = {
|
||||
components: {
|
||||
BooleanSetting,
|
||||
},
|
||||
computed: {
|
||||
frontendVersionLink() {
|
||||
return pleromaFeCommitUrl + this.frontendVersion
|
||||
},
|
||||
...mapState(useInstanceStore, [
|
||||
'backendVersion',
|
||||
'backendRepository',
|
||||
'frontendVersion',
|
||||
]),
|
||||
...SharedComputedObject(),
|
||||
},
|
||||
methods: {
|
||||
clearAssetCache() {
|
||||
this.clearCache(cacheKey)
|
||||
},
|
||||
clearEmojiCache() {
|
||||
this.clearCache(emojiCacheKey)
|
||||
},
|
||||
clearCache(key) {
|
||||
clearCache(key)
|
||||
.then(() => {
|
||||
this.$store.dispatch('settingsSaved', { success: true })
|
||||
})
|
||||
.catch((error) => {
|
||||
this.$store.dispatch('settingsSaved', { error })
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default VersionTab
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue