Merge branch 'develop' into refactor/notification_settings
This commit is contained in:
commit
199fc9351d
121 changed files with 9554 additions and 7828 deletions
|
@ -0,0 +1,58 @@
|
|||
import {
|
||||
instanceDefaultProperties,
|
||||
multiChoiceProperties,
|
||||
defaultState as configDefaultState
|
||||
} from 'src/modules/config.js'
|
||||
|
||||
const SharedComputedObject = () => ({
|
||||
user () {
|
||||
return this.$store.state.users.currentUser
|
||||
},
|
||||
// Getting localized values for instance-default properties
|
||||
...instanceDefaultProperties
|
||||
.filter(key => multiChoiceProperties.includes(key))
|
||||
.map(key => [
|
||||
key + 'DefaultValue',
|
||||
function () {
|
||||
return this.$store.getters.instanceDefaultConfig[key]
|
||||
}
|
||||
])
|
||||
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}),
|
||||
...instanceDefaultProperties
|
||||
.filter(key => !multiChoiceProperties.includes(key))
|
||||
.map(key => [
|
||||
key + 'LocalizedValue',
|
||||
function () {
|
||||
return this.$t('settings.values.' + this.$store.getters.instanceDefaultConfig[key])
|
||||
}
|
||||
])
|
||||
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}),
|
||||
// Generating computed values for vuex properties
|
||||
...Object.keys(configDefaultState)
|
||||
.map(key => [key, {
|
||||
get () { return this.$store.getters.mergedConfig[key] },
|
||||
set (value) {
|
||||
this.$store.dispatch('setOption', { name: key, value })
|
||||
}
|
||||
}])
|
||||
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}),
|
||||
// Special cases (need to transform values or perform actions first)
|
||||
useStreamingApi: {
|
||||
get () { return this.$store.getters.mergedConfig.useStreamingApi },
|
||||
set (value) {
|
||||
const promise = value
|
||||
? this.$store.dispatch('enableMastoSockets')
|
||||
: this.$store.dispatch('disableMastoSockets')
|
||||
|
||||
promise.then(() => {
|
||||
this.$store.dispatch('setOption', { name: 'useStreamingApi', value })
|
||||
}).catch((e) => {
|
||||
console.error('Failed starting MastoAPI Streaming socket', e)
|
||||
this.$store.dispatch('disableMastoSockets')
|
||||
this.$store.dispatch('setOption', { name: 'useStreamingApi', value: false })
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export default SharedComputedObject
|
42
src/components/settings_modal/settings_modal.js
Normal file
42
src/components/settings_modal/settings_modal.js
Normal file
|
@ -0,0 +1,42 @@
|
|||
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'
|
||||
|
||||
const SettingsModal = {
|
||||
components: {
|
||||
Modal,
|
||||
SettingsModalContent: getResettableAsyncComponent(
|
||||
() => import('./settings_modal_content.vue'),
|
||||
{
|
||||
loading: PanelLoading,
|
||||
error: AsyncComponentError,
|
||||
delay: 0
|
||||
}
|
||||
)
|
||||
},
|
||||
methods: {
|
||||
closeModal () {
|
||||
this.$store.dispatch('closeSettingsModal')
|
||||
},
|
||||
peekModal () {
|
||||
this.$store.dispatch('togglePeekSettingsModal')
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
currentSaveStateNotice () {
|
||||
return this.$store.state.interface.settings.currentSaveStateNotice
|
||||
},
|
||||
modalActivated () {
|
||||
return this.$store.state.interface.settingsModalState !== 'hidden'
|
||||
},
|
||||
modalOpenedOnce () {
|
||||
return this.$store.state.interface.settingsModalLoaded
|
||||
},
|
||||
modalPeeked () {
|
||||
return this.$store.state.interface.settingsModalState === 'minimized'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default SettingsModal
|
44
src/components/settings_modal/settings_modal.scss
Normal file
44
src/components/settings_modal/settings_modal.scss
Normal file
|
@ -0,0 +1,44 @@
|
|||
@import 'src/_variables.scss';
|
||||
.settings-modal {
|
||||
overflow: hidden;
|
||||
|
||||
&.peek {
|
||||
.settings-modal-panel {
|
||||
/* Explanation:
|
||||
* Modal is positioned vertically centered.
|
||||
* 100vh - 100% = Distance between modal's top+bottom boundaries and screen
|
||||
* (100vh - 100%) / 2 = Distance between bottom (or top) boundary and screen
|
||||
* + 100% - we move modal completely off-screen, it's top boundary touches
|
||||
* bottom of the screen
|
||||
* - 50px - leaving tiny amount of space so that titlebar + tiny amount of modal is visible
|
||||
*/
|
||||
transform: translateY(calc(((100vh - 100%) / 2 + 100%) - 50px));
|
||||
}
|
||||
}
|
||||
|
||||
.settings-modal-panel {
|
||||
overflow: hidden;
|
||||
transition: transform;
|
||||
transition-timing-function: ease-in-out;
|
||||
transition-duration: 300ms;
|
||||
width: 1000px;
|
||||
max-width: 90vw;
|
||||
height: 90vh;
|
||||
|
||||
@media all and (max-width: 800px) {
|
||||
max-width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
height: 100%;
|
||||
overflow-y: hidden;
|
||||
|
||||
.btn {
|
||||
min-height: 28px;
|
||||
min-width: 10em;
|
||||
padding: 0 2em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
54
src/components/settings_modal/settings_modal.vue
Normal file
54
src/components/settings_modal/settings_modal.vue
Normal file
|
@ -0,0 +1,54 @@
|
|||
<template>
|
||||
<Modal
|
||||
:is-open="modalActivated"
|
||||
class="settings-modal"
|
||||
:class="{ peek: modalPeeked }"
|
||||
:no-background="modalPeeked"
|
||||
>
|
||||
<div class="settings-modal-panel panel">
|
||||
<div class="panel-heading">
|
||||
<span class="title">
|
||||
{{ $t('settings.settings') }}
|
||||
</span>
|
||||
<transition name="fade">
|
||||
<template v-if="currentSaveStateNotice">
|
||||
<div
|
||||
v-if="currentSaveStateNotice.error"
|
||||
class="alert error"
|
||||
@click.prevent
|
||||
>
|
||||
{{ $t('settings.saving_err') }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="!currentSaveStateNotice.error"
|
||||
class="alert transparent"
|
||||
@click.prevent
|
||||
>
|
||||
{{ $t('settings.saving_ok') }}
|
||||
</div>
|
||||
</template>
|
||||
</transition>
|
||||
<button
|
||||
class="btn"
|
||||
@click="peekModal"
|
||||
>
|
||||
{{ $t('general.peek') }}
|
||||
</button>
|
||||
<button
|
||||
class="btn"
|
||||
@click="closeModal"
|
||||
>
|
||||
{{ $t('general.close') }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<SettingsModalContent v-if="modalOpenedOnce" />
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script src="./settings_modal.js"></script>
|
||||
|
||||
<style src="./settings_modal.scss" lang="scss"></style>
|
34
src/components/settings_modal/settings_modal_content.js
Normal file
34
src/components/settings_modal/settings_modal_content.js
Normal file
|
@ -0,0 +1,34 @@
|
|||
import TabSwitcher from 'src/components/tab_switcher/tab_switcher.js'
|
||||
|
||||
import DataImportExportTab from './tabs/data_import_export_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 ProfileTab from './tabs/profile_tab.vue'
|
||||
import GeneralTab from './tabs/general_tab.vue'
|
||||
import VersionTab from './tabs/version_tab.vue'
|
||||
import ThemeTab from './tabs/theme_tab/theme_tab.vue'
|
||||
|
||||
const SettingsModalContent = {
|
||||
components: {
|
||||
TabSwitcher,
|
||||
|
||||
DataImportExportTab,
|
||||
MutesAndBlocksTab,
|
||||
NotificationsTab,
|
||||
FilteringTab,
|
||||
SecurityTab,
|
||||
ProfileTab,
|
||||
GeneralTab,
|
||||
VersionTab,
|
||||
ThemeTab
|
||||
},
|
||||
computed: {
|
||||
isLoggedIn () {
|
||||
return !!this.$store.state.users.currentUser
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default SettingsModalContent
|
43
src/components/settings_modal/settings_modal_content.scss
Normal file
43
src/components/settings_modal/settings_modal_content.scss
Normal file
|
@ -0,0 +1,43 @@
|
|||
@import 'src/_variables.scss';
|
||||
.settings_tab-switcher {
|
||||
height: 100%;
|
||||
|
||||
.setting-item {
|
||||
border-bottom: 2px solid var(--fg, $fallback--fg);
|
||||
margin: 1em 1em 1.4em;
|
||||
padding-bottom: 1.4em;
|
||||
|
||||
> div {
|
||||
margin-bottom: .5em;
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
select {
|
||||
min-width: 10em;
|
||||
}
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.unavailable,
|
||||
.unavailable i {
|
||||
color: var(--cRed, $fallback--cRed);
|
||||
color: $fallback--cRed;
|
||||
}
|
||||
|
||||
.number-input {
|
||||
max-width: 6em;
|
||||
}
|
||||
}
|
||||
}
|
73
src/components/settings_modal/settings_modal_content.vue
Normal file
73
src/components/settings_modal/settings_modal_content.vue
Normal file
|
@ -0,0 +1,73 @@
|
|||
<template>
|
||||
<tab-switcher
|
||||
ref="tabSwitcher"
|
||||
class="settings_tab-switcher"
|
||||
:side-tab-bar="true"
|
||||
:scrollable-tabs="true"
|
||||
>
|
||||
<div
|
||||
:label="$t('settings.general')"
|
||||
icon="wrench"
|
||||
>
|
||||
<GeneralTab />
|
||||
</div>
|
||||
<div
|
||||
v-if="isLoggedIn"
|
||||
:label="$t('settings.profile_tab')"
|
||||
icon="user"
|
||||
>
|
||||
<ProfileTab />
|
||||
</div>
|
||||
<div
|
||||
v-if="isLoggedIn"
|
||||
:label="$t('settings.security_tab')"
|
||||
icon="lock"
|
||||
>
|
||||
<SecurityTab />
|
||||
</div>
|
||||
<div
|
||||
:label="$t('settings.filtering')"
|
||||
icon="filter"
|
||||
>
|
||||
<FilteringTab />
|
||||
</div>
|
||||
<div
|
||||
:label="$t('settings.theme')"
|
||||
icon="brush"
|
||||
>
|
||||
<ThemeTab />
|
||||
</div>
|
||||
<div
|
||||
v-if="isLoggedIn"
|
||||
:label="$t('settings.notifications')"
|
||||
icon="bell-ringing-o"
|
||||
>
|
||||
<NotificationsTab />
|
||||
</div>
|
||||
<div
|
||||
v-if="isLoggedIn"
|
||||
:label="$t('settings.data_import_export_tab')"
|
||||
icon="download"
|
||||
>
|
||||
<DataImportExportTab />
|
||||
</div>
|
||||
<div
|
||||
v-if="isLoggedIn"
|
||||
:label="$t('settings.mutes_and_blocks')"
|
||||
:fullHeight="true"
|
||||
icon="eye-off"
|
||||
>
|
||||
<MutesAndBlocksTab />
|
||||
</div>
|
||||
<div
|
||||
:label="$t('settings.version.title')"
|
||||
icon="info-circled"
|
||||
>
|
||||
<VersionTab />
|
||||
</div>
|
||||
</tab-switcher>
|
||||
</template>
|
||||
|
||||
<script src="./settings_modal_content.js"></script>
|
||||
|
||||
<style src="./settings_modal_content.scss" lang="scss"></style>
|
65
src/components/settings_modal/tabs/data_import_export_tab.js
Normal file
65
src/components/settings_modal/tabs/data_import_export_tab.js
Normal file
|
@ -0,0 +1,65 @@
|
|||
import Importer from 'src/components/importer/importer.vue'
|
||||
import Exporter from 'src/components/exporter/exporter.vue'
|
||||
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||
|
||||
const DataImportExportTab = {
|
||||
data () {
|
||||
return {
|
||||
activeTab: 'profile',
|
||||
newDomainToMute: ''
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.$store.dispatch('fetchTokens')
|
||||
},
|
||||
components: {
|
||||
Importer,
|
||||
Exporter,
|
||||
Checkbox
|
||||
},
|
||||
computed: {
|
||||
user () {
|
||||
return this.$store.state.users.currentUser
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getFollowsContent () {
|
||||
return this.$store.state.api.backendInteractor.exportFriends({ id: this.$store.state.users.currentUser.id })
|
||||
.then(this.generateExportableUsersContent)
|
||||
},
|
||||
getBlocksContent () {
|
||||
return this.$store.state.api.backendInteractor.fetchBlocks()
|
||||
.then(this.generateExportableUsersContent)
|
||||
},
|
||||
importFollows (file) {
|
||||
return this.$store.state.api.backendInteractor.importFollows({ file })
|
||||
.then((status) => {
|
||||
if (!status) {
|
||||
throw new Error('failed')
|
||||
}
|
||||
})
|
||||
},
|
||||
importBlocks (file) {
|
||||
return this.$store.state.api.backendInteractor.importBlocks({ 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
|
||||
// eslint-disable-next-line no-undef
|
||||
return user.screen_name + '@' + location.hostname
|
||||
}
|
||||
return user.screen_name
|
||||
}).join('\n')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default DataImportExportTab
|
|
@ -0,0 +1,43 @@
|
|||
<template>
|
||||
<div
|
||||
: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>
|
||||
</template>
|
||||
|
||||
<script src="./data_import_export_tab.js"></script>
|
||||
<!-- <style lang="scss" src="./profile.scss"></style> -->
|
44
src/components/settings_modal/tabs/filtering_tab.js
Normal file
44
src/components/settings_modal/tabs/filtering_tab.js
Normal file
|
@ -0,0 +1,44 @@
|
|||
import { filter, trim } from 'lodash'
|
||||
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||
|
||||
import SharedComputedObject from '../helpers/shared_computed_object.js'
|
||||
|
||||
const FilteringTab = {
|
||||
data () {
|
||||
return {
|
||||
muteWordsStringLocal: this.$store.getters.mergedConfig.muteWords.join('\n')
|
||||
}
|
||||
},
|
||||
components: {
|
||||
Checkbox
|
||||
},
|
||||
computed: {
|
||||
...SharedComputedObject(),
|
||||
muteWordsString: {
|
||||
get () {
|
||||
return this.muteWordsStringLocal
|
||||
},
|
||||
set (value) {
|
||||
this.muteWordsStringLocal = value
|
||||
this.$store.dispatch('setOption', {
|
||||
name: 'muteWords',
|
||||
value: filter(value.split('\n'), (word) => trim(word).length > 0)
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
// Updating nested properties
|
||||
watch: {
|
||||
notificationVisibility: {
|
||||
handler (value) {
|
||||
this.$store.dispatch('setOption', {
|
||||
name: 'notificationVisibility',
|
||||
value: this.$store.getters.mergedConfig.notificationVisibility
|
||||
})
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default FilteringTab
|
86
src/components/settings_modal/tabs/filtering_tab.vue
Normal file
86
src/components/settings_modal/tabs/filtering_tab.vue
Normal file
|
@ -0,0 +1,86 @@
|
|||
<template>
|
||||
<div :label="$t('settings.filtering')">
|
||||
<div class="setting-item">
|
||||
<div class="select-multiple">
|
||||
<span class="label">{{ $t('settings.notification_visibility') }}</span>
|
||||
<ul class="option-list">
|
||||
<li>
|
||||
<Checkbox v-model="notificationVisibility.likes">
|
||||
{{ $t('settings.notification_visibility_likes') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="notificationVisibility.repeats">
|
||||
{{ $t('settings.notification_visibility_repeats') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="notificationVisibility.follows">
|
||||
{{ $t('settings.notification_visibility_follows') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="notificationVisibility.mentions">
|
||||
{{ $t('settings.notification_visibility_mentions') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="notificationVisibility.moves">
|
||||
{{ $t('settings.notification_visibility_moves') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="notificationVisibility.emojiReactions">
|
||||
{{ $t('settings.notification_visibility_emoji_reactions') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
{{ $t('settings.replies_in_timeline') }}
|
||||
<label
|
||||
for="replyVisibility"
|
||||
class="select"
|
||||
>
|
||||
<select
|
||||
id="replyVisibility"
|
||||
v-model="replyVisibility"
|
||||
>
|
||||
<option
|
||||
value="all"
|
||||
selected
|
||||
>{{ $t('settings.reply_visibility_all') }}</option>
|
||||
<option value="following">{{ $t('settings.reply_visibility_following') }}</option>
|
||||
<option value="self">{{ $t('settings.reply_visibility_self') }}</option>
|
||||
</select>
|
||||
<i class="icon-down-open" />
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<Checkbox v-model="hidePostStats">
|
||||
{{ $t('settings.hide_post_stats') }} {{ $t('settings.instance_default', { value: hidePostStatsLocalizedValue }) }}
|
||||
</Checkbox>
|
||||
</div>
|
||||
<div>
|
||||
<Checkbox v-model="hideUserStats">
|
||||
{{ $t('settings.hide_user_stats') }} {{ $t('settings.instance_default', { value: hideUserStatsLocalizedValue }) }}
|
||||
</Checkbox>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div>
|
||||
<p>{{ $t('settings.filtering_explanation') }}</p>
|
||||
<textarea
|
||||
id="muteWords"
|
||||
v-model="muteWordsString"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Checkbox v-model="hideFilteredStatuses">
|
||||
{{ $t('settings.hide_filtered_statuses') }} {{ $t('settings.instance_default', { value: hideFilteredStatusesLocalizedValue }) }}
|
||||
</Checkbox>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script src="./filtering_tab.js"></script>
|
31
src/components/settings_modal/tabs/general_tab.js
Normal file
31
src/components/settings_modal/tabs/general_tab.js
Normal file
|
@ -0,0 +1,31 @@
|
|||
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||
import InterfaceLanguageSwitcher from 'src/components/interface_language_switcher/interface_language_switcher.vue'
|
||||
|
||||
import SharedComputedObject from '../helpers/shared_computed_object.js'
|
||||
|
||||
const GeneralTab = {
|
||||
data () {
|
||||
return {
|
||||
loopSilentAvailable:
|
||||
// Firefox
|
||||
Object.getOwnPropertyDescriptor(HTMLVideoElement.prototype, 'mozHasAudio') ||
|
||||
// Chrome-likes
|
||||
Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'webkitAudioDecodedByteCount') ||
|
||||
// Future spec, still not supported in Nightly 63 as of 08/2018
|
||||
Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'audioTracks')
|
||||
}
|
||||
},
|
||||
components: {
|
||||
Checkbox,
|
||||
InterfaceLanguageSwitcher
|
||||
},
|
||||
computed: {
|
||||
postFormats () {
|
||||
return this.$store.state.instance.postFormats || []
|
||||
},
|
||||
instanceSpecificPanelPresent () { return this.$store.state.instance.showInstanceSpecificPanel },
|
||||
...SharedComputedObject()
|
||||
}
|
||||
}
|
||||
|
||||
export default GeneralTab
|
272
src/components/settings_modal/tabs/general_tab.vue
Normal file
272
src/components/settings_modal/tabs/general_tab.vue
Normal file
|
@ -0,0 +1,272 @@
|
|||
<template>
|
||||
<div :label="$t('settings.general')">
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.interface') }}</h2>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<interface-language-switcher />
|
||||
</li>
|
||||
<li v-if="instanceSpecificPanelPresent">
|
||||
<Checkbox v-model="hideISP">
|
||||
{{ $t('settings.hide_isp') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('nav.timeline') }}</h2>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<Checkbox v-model="hideMutedPosts">
|
||||
{{ $t('settings.hide_muted_posts') }} {{ $t('settings.instance_default', { value: hideMutedPostsLocalizedValue }) }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="collapseMessageWithSubject">
|
||||
{{ $t('settings.collapse_subject') }} {{ $t('settings.instance_default', { value: collapseMessageWithSubjectLocalizedValue }) }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="streaming">
|
||||
{{ $t('settings.streaming') }}
|
||||
</Checkbox>
|
||||
<ul
|
||||
class="setting-list suboptions"
|
||||
:class="[{disabled: !streaming}]"
|
||||
>
|
||||
<li>
|
||||
<Checkbox
|
||||
v-model="pauseOnUnfocused"
|
||||
:disabled="!streaming"
|
||||
>
|
||||
{{ $t('settings.pause_on_unfocused') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="useStreamingApi">
|
||||
{{ $t('settings.useStreamingApi') }}
|
||||
<br>
|
||||
<small>
|
||||
{{ $t('settings.useStreamingApiWarning') }}
|
||||
</small>
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="autoLoad">
|
||||
{{ $t('settings.autoload') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="hoverPreview">
|
||||
{{ $t('settings.reply_link_preview') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="emojiReactionsOnTimeline">
|
||||
{{ $t('settings.emoji_reactions_on_timeline') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.composing') }}</h2>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<Checkbox v-model="scopeCopy">
|
||||
{{ $t('settings.scope_copy') }} {{ $t('settings.instance_default', { value: scopeCopyLocalizedValue }) }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="alwaysShowSubjectInput">
|
||||
{{ $t('settings.subject_input_always_show') }} {{ $t('settings.instance_default', { value: alwaysShowSubjectInputLocalizedValue }) }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
{{ $t('settings.subject_line_behavior') }}
|
||||
<label
|
||||
for="subjectLineBehavior"
|
||||
class="select"
|
||||
>
|
||||
<select
|
||||
id="subjectLineBehavior"
|
||||
v-model="subjectLineBehavior"
|
||||
>
|
||||
<option value="email">
|
||||
{{ $t('settings.subject_line_email') }}
|
||||
{{ subjectLineBehaviorDefaultValue == 'email' ? $t('settings.instance_default_simple') : '' }}
|
||||
</option>
|
||||
<option value="masto">
|
||||
{{ $t('settings.subject_line_mastodon') }}
|
||||
{{ subjectLineBehaviorDefaultValue == 'mastodon' ? $t('settings.instance_default_simple') : '' }}
|
||||
</option>
|
||||
<option value="noop">
|
||||
{{ $t('settings.subject_line_noop') }}
|
||||
{{ subjectLineBehaviorDefaultValue == 'noop' ? $t('settings.instance_default_simple') : '' }}
|
||||
</option>
|
||||
</select>
|
||||
<i class="icon-down-open" />
|
||||
</label>
|
||||
</div>
|
||||
</li>
|
||||
<li v-if="postFormats.length > 0">
|
||||
<div>
|
||||
{{ $t('settings.post_status_content_type') }}
|
||||
<label
|
||||
for="postContentType"
|
||||
class="select"
|
||||
>
|
||||
<select
|
||||
id="postContentType"
|
||||
v-model="postContentType"
|
||||
>
|
||||
<option
|
||||
v-for="postFormat in postFormats"
|
||||
:key="postFormat"
|
||||
:value="postFormat"
|
||||
>
|
||||
{{ $t(`post_status.content_type["${postFormat}"]`) }}
|
||||
{{ postContentTypeDefaultValue === postFormat ? $t('settings.instance_default_simple') : '' }}
|
||||
</option>
|
||||
</select>
|
||||
<i class="icon-down-open" />
|
||||
</label>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="minimalScopesMode">
|
||||
{{ $t('settings.minimal_scopes_mode') }} {{ $t('settings.instance_default', { value: minimalScopesModeLocalizedValue }) }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="autohideFloatingPostButton">
|
||||
{{ $t('settings.autohide_floating_post_button') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="padEmoji">
|
||||
{{ $t('settings.pad_emoji') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.attachments') }}</h2>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<Checkbox v-model="hideAttachments">
|
||||
{{ $t('settings.hide_attachments_in_tl') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="hideAttachmentsInConv">
|
||||
{{ $t('settings.hide_attachments_in_convo') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<label for="maxThumbnails">
|
||||
{{ $t('settings.max_thumbnails') }}
|
||||
</label>
|
||||
<input
|
||||
id="maxThumbnails"
|
||||
v-model.number="maxThumbnails"
|
||||
class="number-input"
|
||||
type="number"
|
||||
min="0"
|
||||
step="1"
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="hideNsfw">
|
||||
{{ $t('settings.nsfw_clickthrough') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<ul class="setting-list suboptions">
|
||||
<li>
|
||||
<Checkbox
|
||||
v-model="preloadImage"
|
||||
:disabled="!hideNsfw"
|
||||
>
|
||||
{{ $t('settings.preload_images') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox
|
||||
v-model="useOneClickNsfw"
|
||||
:disabled="!hideNsfw"
|
||||
>
|
||||
{{ $t('settings.use_one_click_nsfw') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
</ul>
|
||||
<li>
|
||||
<Checkbox v-model="stopGifs">
|
||||
{{ $t('settings.stop_gifs') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="loopVideo">
|
||||
{{ $t('settings.loop_video') }}
|
||||
</Checkbox>
|
||||
<ul
|
||||
class="setting-list suboptions"
|
||||
:class="[{disabled: !streaming}]"
|
||||
>
|
||||
<li>
|
||||
<Checkbox
|
||||
v-model="loopVideoSilentOnly"
|
||||
:disabled="!loopVideo || !loopSilentAvailable"
|
||||
>
|
||||
{{ $t('settings.loop_video_silent_only') }}
|
||||
</Checkbox>
|
||||
<div
|
||||
v-if="!loopSilentAvailable"
|
||||
class="unavailable"
|
||||
>
|
||||
<i class="icon-globe" />! {{ $t('settings.limited_availability') }}
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="playVideosInModal">
|
||||
{{ $t('settings.play_videos_in_modal') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="useContainFit">
|
||||
{{ $t('settings.use_contain_fit') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.notifications') }}</h2>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<Checkbox v-model="webPushNotifications">
|
||||
{{ $t('settings.enable_web_push_notifications') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.fun') }}</h2>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<Checkbox v-model="greentext">
|
||||
{{ $t('settings.greentext') }} {{ $t('settings.instance_default', { value: greentextLocalizedValue }) }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./general_tab.js"></script>
|
136
src/components/settings_modal/tabs/mutes_and_blocks_tab.js
Normal file
136
src/components/settings_modal/tabs/mutes_and_blocks_tab.js
Normal file
|
@ -0,0 +1,136 @@
|
|||
import get from 'lodash/get'
|
||||
import map from 'lodash/map'
|
||||
import reject from 'lodash/reject'
|
||||
import Autosuggest from 'src/components/autosuggest/autosuggest.vue'
|
||||
import TabSwitcher from 'src/components/tab_switcher/tab_switcher.js'
|
||||
import BlockCard from 'src/components/block_card/block_card.vue'
|
||||
import MuteCard from 'src/components/mute_card/mute_card.vue'
|
||||
import DomainMuteCard from 'src/components/domain_mute_card/domain_mute_card.vue'
|
||||
import SelectableList from 'src/components/selectable_list/selectable_list.vue'
|
||||
import ProgressButton from 'src/components/progress_button/progress_button.vue'
|
||||
import withSubscription from 'src/components/../hocs/with_subscription/with_subscription'
|
||||
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||
|
||||
const BlockList = withSubscription({
|
||||
fetch: (props, $store) => $store.dispatch('fetchBlocks'),
|
||||
select: (props, $store) => get($store.state.users.currentUser, 'blockIds', []),
|
||||
childPropName: 'items'
|
||||
})(SelectableList)
|
||||
|
||||
const MuteList = withSubscription({
|
||||
fetch: (props, $store) => $store.dispatch('fetchMutes'),
|
||||
select: (props, $store) => get($store.state.users.currentUser, 'muteIds', []),
|
||||
childPropName: 'items'
|
||||
})(SelectableList)
|
||||
|
||||
const DomainMuteList = withSubscription({
|
||||
fetch: (props, $store) => $store.dispatch('fetchDomainMutes'),
|
||||
select: (props, $store) => get($store.state.users.currentUser, 'domainMutes', []),
|
||||
childPropName: 'items'
|
||||
})(SelectableList)
|
||||
|
||||
const MutesAndBlocks = {
|
||||
data () {
|
||||
return {
|
||||
activeTab: 'profile'
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.$store.dispatch('fetchTokens')
|
||||
this.$store.dispatch('getKnownDomains')
|
||||
},
|
||||
components: {
|
||||
TabSwitcher,
|
||||
BlockList,
|
||||
MuteList,
|
||||
DomainMuteList,
|
||||
BlockCard,
|
||||
MuteCard,
|
||||
DomainMuteCard,
|
||||
ProgressButton,
|
||||
Autosuggest,
|
||||
Checkbox
|
||||
},
|
||||
computed: {
|
||||
knownDomains () {
|
||||
return this.$store.state.instance.knownDomains
|
||||
},
|
||||
user () {
|
||||
return this.$store.state.users.currentUser
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
importFollows (file) {
|
||||
return this.$store.state.api.backendInteractor.importFollows({ file })
|
||||
.then((status) => {
|
||||
if (!status) {
|
||||
throw new Error('failed')
|
||||
}
|
||||
})
|
||||
},
|
||||
importBlocks (file) {
|
||||
return this.$store.state.api.backendInteractor.importBlocks({ 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
|
||||
// eslint-disable-next-line no-undef
|
||||
return user.screen_name + '@' + location.hostname
|
||||
}
|
||||
return user.screen_name
|
||||
}).join('\n')
|
||||
},
|
||||
activateTab (tabName) {
|
||||
this.activeTab = tabName
|
||||
},
|
||||
filterUnblockedUsers (userIds) {
|
||||
return reject(userIds, (userId) => {
|
||||
const relationship = this.$store.getters.relationship(this.userId)
|
||||
return relationship.blocking || userId === this.user.id
|
||||
})
|
||||
},
|
||||
filterUnMutedUsers (userIds) {
|
||||
return reject(userIds, (userId) => {
|
||||
const relationship = this.$store.getters.relationship(this.userId)
|
||||
return relationship.muting || userId === this.user.id
|
||||
})
|
||||
},
|
||||
queryUserIds (query) {
|
||||
return this.$store.dispatch('searchUsers', { query })
|
||||
.then((users) => map(users, 'id'))
|
||||
},
|
||||
blockUsers (ids) {
|
||||
return this.$store.dispatch('blockUsers', ids)
|
||||
},
|
||||
unblockUsers (ids) {
|
||||
return this.$store.dispatch('unblockUsers', ids)
|
||||
},
|
||||
muteUsers (ids) {
|
||||
return this.$store.dispatch('muteUsers', ids)
|
||||
},
|
||||
unmuteUsers (ids) {
|
||||
return this.$store.dispatch('unmuteUsers', ids)
|
||||
},
|
||||
filterUnMutedDomains (urls) {
|
||||
return urls.filter(url => !this.user.domainMutes.includes(url))
|
||||
},
|
||||
queryKnownDomains (query) {
|
||||
return new Promise((resolve, reject) => {
|
||||
resolve(this.knownDomains.filter(url => url.toLowerCase().includes(query)))
|
||||
})
|
||||
},
|
||||
unmuteDomains (domains) {
|
||||
return this.$store.dispatch('unmuteDomains', domains)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default MutesAndBlocks
|
29
src/components/settings_modal/tabs/mutes_and_blocks_tab.scss
Normal file
29
src/components/settings_modal/tabs/mutes_and_blocks_tab.scss
Normal file
|
@ -0,0 +1,29 @@
|
|||
.mutes-and-blocks-tab {
|
||||
height: 100%;
|
||||
|
||||
.usersearch-wrapper {
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
.bulk-actions {
|
||||
text-align: right;
|
||||
padding: 0 1em;
|
||||
min-height: 28px;
|
||||
}
|
||||
|
||||
.bulk-action-button {
|
||||
width: 10em
|
||||
}
|
||||
|
||||
.domain-mute-form {
|
||||
padding: 1em;
|
||||
display: flex;
|
||||
flex-direction: column
|
||||
}
|
||||
|
||||
.domain-mute-button {
|
||||
align-self: flex-end;
|
||||
margin-top: 1em;
|
||||
width: 10em
|
||||
}
|
||||
}
|
171
src/components/settings_modal/tabs/mutes_and_blocks_tab.vue
Normal file
171
src/components/settings_modal/tabs/mutes_and_blocks_tab.vue
Normal file
|
@ -0,0 +1,171 @@
|
|||
<template>
|
||||
<tab-switcher
|
||||
:scrollable-tabs="true"
|
||||
class="mutes-and-blocks-tab"
|
||||
>
|
||||
<div :label="$t('settings.blocks_tab')">
|
||||
<div class="usersearch-wrapper">
|
||||
<Autosuggest
|
||||
:filter="filterUnblockedUsers"
|
||||
:query="queryUserIds"
|
||||
:placeholder="$t('settings.search_user_to_block')"
|
||||
>
|
||||
<BlockCard
|
||||
slot-scope="row"
|
||||
:user-id="row.item"
|
||||
/>
|
||||
</Autosuggest>
|
||||
</div>
|
||||
<BlockList
|
||||
:refresh="true"
|
||||
:get-key="i => i"
|
||||
>
|
||||
<template
|
||||
slot="header"
|
||||
slot-scope="{selected}"
|
||||
>
|
||||
<div class="bulk-actions">
|
||||
<ProgressButton
|
||||
v-if="selected.length > 0"
|
||||
class="btn btn-default bulk-action-button"
|
||||
:click="() => blockUsers(selected)"
|
||||
>
|
||||
{{ $t('user_card.block') }}
|
||||
<template slot="progress">
|
||||
{{ $t('user_card.block_progress') }}
|
||||
</template>
|
||||
</ProgressButton>
|
||||
<ProgressButton
|
||||
v-if="selected.length > 0"
|
||||
class="btn btn-default"
|
||||
:click="() => unblockUsers(selected)"
|
||||
>
|
||||
{{ $t('user_card.unblock') }}
|
||||
<template slot="progress">
|
||||
{{ $t('user_card.unblock_progress') }}
|
||||
</template>
|
||||
</ProgressButton>
|
||||
</div>
|
||||
</template>
|
||||
<template
|
||||
slot="item"
|
||||
slot-scope="{item}"
|
||||
>
|
||||
<BlockCard :user-id="item" />
|
||||
</template>
|
||||
<template slot="empty">
|
||||
{{ $t('settings.no_blocks') }}
|
||||
</template>
|
||||
</BlockList>
|
||||
</div>
|
||||
|
||||
<div :label="$t('settings.mutes_tab')">
|
||||
<tab-switcher>
|
||||
<div label="Users">
|
||||
<div class="usersearch-wrapper">
|
||||
<Autosuggest
|
||||
:filter="filterUnMutedUsers"
|
||||
:query="queryUserIds"
|
||||
:placeholder="$t('settings.search_user_to_mute')"
|
||||
>
|
||||
<MuteCard
|
||||
slot-scope="row"
|
||||
:user-id="row.item"
|
||||
/>
|
||||
</Autosuggest>
|
||||
</div>
|
||||
<MuteList
|
||||
:refresh="true"
|
||||
:get-key="i => i"
|
||||
>
|
||||
<template
|
||||
slot="header"
|
||||
slot-scope="{selected}"
|
||||
>
|
||||
<div class="bulk-actions">
|
||||
<ProgressButton
|
||||
v-if="selected.length > 0"
|
||||
class="btn btn-default"
|
||||
:click="() => muteUsers(selected)"
|
||||
>
|
||||
{{ $t('user_card.mute') }}
|
||||
<template slot="progress">
|
||||
{{ $t('user_card.mute_progress') }}
|
||||
</template>
|
||||
</ProgressButton>
|
||||
<ProgressButton
|
||||
v-if="selected.length > 0"
|
||||
class="btn btn-default"
|
||||
:click="() => unmuteUsers(selected)"
|
||||
>
|
||||
{{ $t('user_card.unmute') }}
|
||||
<template slot="progress">
|
||||
{{ $t('user_card.unmute_progress') }}
|
||||
</template>
|
||||
</ProgressButton>
|
||||
</div>
|
||||
</template>
|
||||
<template
|
||||
slot="item"
|
||||
slot-scope="{item}"
|
||||
>
|
||||
<MuteCard :user-id="item" />
|
||||
</template>
|
||||
<template slot="empty">
|
||||
{{ $t('settings.no_mutes') }}
|
||||
</template>
|
||||
</MuteList>
|
||||
</div>
|
||||
|
||||
<div :label="$t('settings.domain_mutes')">
|
||||
<div class="domain-mute-form">
|
||||
<Autosuggest
|
||||
:filter="filterUnMutedDomains"
|
||||
:query="queryKnownDomains"
|
||||
:placeholder="$t('settings.type_domains_to_mute')"
|
||||
>
|
||||
<DomainMuteCard
|
||||
slot-scope="row"
|
||||
:domain="row.item"
|
||||
/>
|
||||
</Autosuggest>
|
||||
</div>
|
||||
<DomainMuteList
|
||||
:refresh="true"
|
||||
:get-key="i => i"
|
||||
>
|
||||
<template
|
||||
slot="header"
|
||||
slot-scope="{selected}"
|
||||
>
|
||||
<div class="bulk-actions">
|
||||
<ProgressButton
|
||||
v-if="selected.length > 0"
|
||||
class="btn btn-default"
|
||||
:click="() => unmuteDomains(selected)"
|
||||
>
|
||||
{{ $t('domain_mute_card.unmute') }}
|
||||
<template slot="progress">
|
||||
{{ $t('domain_mute_card.unmute_progress') }}
|
||||
</template>
|
||||
</ProgressButton>
|
||||
</div>
|
||||
</template>
|
||||
<template
|
||||
slot="item"
|
||||
slot-scope="{item}"
|
||||
>
|
||||
<DomainMuteCard :domain="item" />
|
||||
</template>
|
||||
<template slot="empty">
|
||||
{{ $t('settings.no_mutes') }}
|
||||
</template>
|
||||
</DomainMuteList>
|
||||
</div>
|
||||
</tab-switcher>
|
||||
</div>
|
||||
</tab-switcher>
|
||||
</template>
|
||||
|
||||
<script src="./mutes_and_blocks_tab.js"></script>
|
||||
<style lang="scss" src="./mutes_and_blocks_tab.scss"></style>
|
27
src/components/settings_modal/tabs/notifications_tab.js
Normal file
27
src/components/settings_modal/tabs/notifications_tab.js
Normal file
|
@ -0,0 +1,27 @@
|
|||
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||
|
||||
const NotificationsTab = {
|
||||
data () {
|
||||
return {
|
||||
activeTab: 'profile',
|
||||
notificationSettings: this.$store.state.users.currentUser.notification_settings,
|
||||
newDomainToMute: ''
|
||||
}
|
||||
},
|
||||
components: {
|
||||
Checkbox
|
||||
},
|
||||
computed: {
|
||||
user () {
|
||||
return this.$store.state.users.currentUser
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateNotificationSettings () {
|
||||
this.$store.state.api.backendInteractor
|
||||
.updateNotificationSettings({ settings: this.notificationSettings })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default NotificationsTab
|
49
src/components/settings_modal/tabs/notifications_tab.vue
Normal file
49
src/components/settings_modal/tabs/notifications_tab.vue
Normal file
|
@ -0,0 +1,49 @@
|
|||
<template>
|
||||
<div :label="$t('settings.notifications')">
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.notification_setting_filters') }}</h2>
|
||||
<div class="select-multiple">
|
||||
<span class="label">{{ $t('settings.notification_setting') }}</span>
|
||||
<ul class="option-list">
|
||||
<li>
|
||||
<Checkbox v-model="notificationSettings.from_following">
|
||||
{{ $t('settings.notification_setting_from_following') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="notificationSettings.from_followers">
|
||||
{{ $t('settings.notification_setting_from_followers') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="notificationSettings.from_strangers">
|
||||
{{ $t('settings.notification_setting_from_strangers') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.notification_setting_privacy') }}</h2>
|
||||
<p>
|
||||
<Checkbox v-model="notificationSettings.privacy_option">
|
||||
{{ $t('settings.notification_setting_privacy_option') }}
|
||||
</Checkbox>
|
||||
</p>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<p>{{ $t('settings.notification_mutes') }}</p>
|
||||
<p>{{ $t('settings.notification_blocks') }}</p>
|
||||
<button
|
||||
class="btn btn-default"
|
||||
@click="updateNotificationSettings"
|
||||
>
|
||||
{{ $t('general.submit') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./notifications_tab.js"></script>
|
||||
<!-- <style lang="scss" src="./profile.scss"></style> -->
|
181
src/components/settings_modal/tabs/profile_tab.js
Normal file
181
src/components/settings_modal/tabs/profile_tab.js
Normal file
|
@ -0,0 +1,181 @@
|
|||
import unescape from 'lodash/unescape'
|
||||
import ImageCropper from 'src/components/image_cropper/image_cropper.vue'
|
||||
import ScopeSelector from 'src/components/scope_selector/scope_selector.vue'
|
||||
import fileSizeFormatService from 'src/components/../services/file_size_format/file_size_format.js'
|
||||
import ProgressButton from 'src/components/progress_button/progress_button.vue'
|
||||
import EmojiInput from 'src/components/emoji_input/emoji_input.vue'
|
||||
import suggestor from 'src/components/emoji_input/suggestor.js'
|
||||
import Autosuggest from 'src/components/autosuggest/autosuggest.vue'
|
||||
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||
|
||||
const ProfileTab = {
|
||||
data () {
|
||||
return {
|
||||
newName: this.$store.state.users.currentUser.name,
|
||||
newBio: unescape(this.$store.state.users.currentUser.description),
|
||||
newLocked: this.$store.state.users.currentUser.locked,
|
||||
newNoRichText: this.$store.state.users.currentUser.no_rich_text,
|
||||
newDefaultScope: this.$store.state.users.currentUser.default_scope,
|
||||
hideFollows: this.$store.state.users.currentUser.hide_follows,
|
||||
hideFollowers: this.$store.state.users.currentUser.hide_followers,
|
||||
hideFollowsCount: this.$store.state.users.currentUser.hide_follows_count,
|
||||
hideFollowersCount: this.$store.state.users.currentUser.hide_followers_count,
|
||||
showRole: this.$store.state.users.currentUser.show_role,
|
||||
role: this.$store.state.users.currentUser.role,
|
||||
discoverable: this.$store.state.users.currentUser.discoverable,
|
||||
bot: this.$store.state.users.currentUser.bot,
|
||||
allowFollowingMove: this.$store.state.users.currentUser.allow_following_move,
|
||||
pickAvatarBtnVisible: true,
|
||||
bannerUploading: false,
|
||||
backgroundUploading: false,
|
||||
banner: null,
|
||||
bannerPreview: null,
|
||||
background: null,
|
||||
backgroundPreview: null,
|
||||
bannerUploadError: null,
|
||||
backgroundUploadError: null
|
||||
}
|
||||
},
|
||||
components: {
|
||||
ScopeSelector,
|
||||
ImageCropper,
|
||||
EmojiInput,
|
||||
Autosuggest,
|
||||
ProgressButton,
|
||||
Checkbox
|
||||
},
|
||||
computed: {
|
||||
user () {
|
||||
return this.$store.state.users.currentUser
|
||||
},
|
||||
emojiUserSuggestor () {
|
||||
return suggestor({
|
||||
emoji: [
|
||||
...this.$store.state.instance.emoji,
|
||||
...this.$store.state.instance.customEmoji
|
||||
],
|
||||
users: this.$store.state.users.users,
|
||||
updateUsersList: (query) => this.$store.dispatch('searchUsers', { query })
|
||||
})
|
||||
},
|
||||
emojiSuggestor () {
|
||||
return suggestor({ emoji: [
|
||||
...this.$store.state.instance.emoji,
|
||||
...this.$store.state.instance.customEmoji
|
||||
] })
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateProfile () {
|
||||
this.$store.state.api.backendInteractor
|
||||
.updateProfile({
|
||||
params: {
|
||||
note: this.newBio,
|
||||
locked: this.newLocked,
|
||||
// Backend notation.
|
||||
/* eslint-disable camelcase */
|
||||
display_name: this.newName,
|
||||
default_scope: this.newDefaultScope,
|
||||
no_rich_text: this.newNoRichText,
|
||||
hide_follows: this.hideFollows,
|
||||
hide_followers: this.hideFollowers,
|
||||
discoverable: this.discoverable,
|
||||
bot: this.bot,
|
||||
allow_following_move: this.allowFollowingMove,
|
||||
hide_follows_count: this.hideFollowsCount,
|
||||
hide_followers_count: this.hideFollowersCount,
|
||||
show_role: this.showRole
|
||||
/* eslint-enable camelcase */
|
||||
} }).then((user) => {
|
||||
this.$store.commit('addNewUsers', [user])
|
||||
this.$store.commit('setCurrentUser', user)
|
||||
})
|
||||
},
|
||||
changeVis (visibility) {
|
||||
this.newDefaultScope = visibility
|
||||
},
|
||||
uploadFile (slot, e) {
|
||||
const file = e.target.files[0]
|
||||
if (!file) { return }
|
||||
if (file.size > this.$store.state.instance[slot + 'limit']) {
|
||||
const filesize = fileSizeFormatService.fileSizeFormat(file.size)
|
||||
const allowedsize = fileSizeFormatService.fileSizeFormat(this.$store.state.instance[slot + 'limit'])
|
||||
this[slot + 'UploadError'] = [
|
||||
this.$t('upload.error.base'),
|
||||
this.$t(
|
||||
'upload.error.file_too_big',
|
||||
{
|
||||
filesize: filesize.num,
|
||||
filesizeunit: filesize.unit,
|
||||
allowedsize: allowedsize.num,
|
||||
allowedsizeunit: allowedsize.unit
|
||||
}
|
||||
)
|
||||
].join(' ')
|
||||
return
|
||||
}
|
||||
// eslint-disable-next-line no-undef
|
||||
const reader = new FileReader()
|
||||
reader.onload = ({ target }) => {
|
||||
const img = target.result
|
||||
this[slot + 'Preview'] = img
|
||||
this[slot] = file
|
||||
}
|
||||
reader.readAsDataURL(file)
|
||||
},
|
||||
submitAvatar (cropper, file) {
|
||||
const that = this
|
||||
return new Promise((resolve, reject) => {
|
||||
function updateAvatar (avatar) {
|
||||
that.$store.state.api.backendInteractor.updateAvatar({ avatar })
|
||||
.then((user) => {
|
||||
that.$store.commit('addNewUsers', [user])
|
||||
that.$store.commit('setCurrentUser', user)
|
||||
resolve()
|
||||
})
|
||||
.catch((err) => {
|
||||
reject(new Error(that.$t('upload.error.base') + ' ' + err.message))
|
||||
})
|
||||
}
|
||||
|
||||
if (cropper) {
|
||||
cropper.getCroppedCanvas().toBlob(updateAvatar, file.type)
|
||||
} else {
|
||||
updateAvatar(file)
|
||||
}
|
||||
})
|
||||
},
|
||||
submitBanner () {
|
||||
if (!this.bannerPreview) { return }
|
||||
|
||||
this.bannerUploading = true
|
||||
this.$store.state.api.backendInteractor.updateBanner({ banner: this.banner })
|
||||
.then((user) => {
|
||||
this.$store.commit('addNewUsers', [user])
|
||||
this.$store.commit('setCurrentUser', user)
|
||||
this.bannerPreview = null
|
||||
})
|
||||
.catch((err) => {
|
||||
this.bannerUploadError = this.$t('upload.error.base') + ' ' + err.message
|
||||
})
|
||||
.then(() => { this.bannerUploading = false })
|
||||
},
|
||||
submitBg () {
|
||||
if (!this.backgroundPreview) { return }
|
||||
let background = this.background
|
||||
this.backgroundUploading = true
|
||||
this.$store.state.api.backendInteractor.updateBg({ background }).then((data) => {
|
||||
if (!data.error) {
|
||||
this.$store.commit('addNewUsers', [data])
|
||||
this.$store.commit('setCurrentUser', data)
|
||||
this.backgroundPreview = null
|
||||
} else {
|
||||
this.backgroundUploadError = this.$t('upload.error.base') + data.error
|
||||
}
|
||||
this.backgroundUploading = false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default ProfileTab
|
82
src/components/settings_modal/tabs/profile_tab.scss
Normal file
82
src/components/settings_modal/tabs/profile_tab.scss
Normal file
|
@ -0,0 +1,82 @@
|
|||
@import '../../../_variables.scss';
|
||||
.profile-tab {
|
||||
.bio {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.visibility-tray {
|
||||
padding-top: 5px;
|
||||
}
|
||||
|
||||
input[type=file] {
|
||||
padding: 5px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.banner {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.uploading {
|
||||
font-size: 1.5em;
|
||||
margin: 0.25em;
|
||||
}
|
||||
|
||||
.name-changer {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.bg {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.current-avatar {
|
||||
display: block;
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
border-radius: $fallback--avatarRadius;
|
||||
border-radius: var(--avatarRadius, $fallback--avatarRadius);
|
||||
}
|
||||
|
||||
.oauth-tokens {
|
||||
width: 100%;
|
||||
|
||||
th {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.actions {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
&-usersearch-wrapper {
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
&-bulk-actions {
|
||||
text-align: right;
|
||||
padding: 0 1em;
|
||||
min-height: 28px;
|
||||
|
||||
button {
|
||||
width: 10em;
|
||||
}
|
||||
}
|
||||
|
||||
&-domain-mute-form {
|
||||
padding: 1em;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
button {
|
||||
align-self: flex-end;
|
||||
margin-top: 1em;
|
||||
width: 10em;
|
||||
}
|
||||
}
|
||||
|
||||
.setting-subitem {
|
||||
margin-left: 1.75em;
|
||||
}
|
||||
}
|
218
src/components/settings_modal/tabs/profile_tab.vue
Normal file
218
src/components/settings_modal/tabs/profile_tab.vue
Normal file
|
@ -0,0 +1,218 @@
|
|||
<template>
|
||||
<div class="profile-tab">
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.name_bio') }}</h2>
|
||||
<p>{{ $t('settings.name') }}</p>
|
||||
<EmojiInput
|
||||
v-model="newName"
|
||||
enable-emoji-picker
|
||||
:suggest="emojiSuggestor"
|
||||
>
|
||||
<input
|
||||
id="username"
|
||||
v-model="newName"
|
||||
classname="name-changer"
|
||||
>
|
||||
</EmojiInput>
|
||||
<p>{{ $t('settings.bio') }}</p>
|
||||
<EmojiInput
|
||||
v-model="newBio"
|
||||
enable-emoji-picker
|
||||
:suggest="emojiUserSuggestor"
|
||||
>
|
||||
<textarea
|
||||
v-model="newBio"
|
||||
classname="bio"
|
||||
/>
|
||||
</EmojiInput>
|
||||
<p>
|
||||
<Checkbox v-model="newLocked">
|
||||
{{ $t('settings.lock_account_description') }}
|
||||
</Checkbox>
|
||||
</p>
|
||||
<div>
|
||||
<label for="default-vis">{{ $t('settings.default_vis') }}</label>
|
||||
<div
|
||||
id="default-vis"
|
||||
class="visibility-tray"
|
||||
>
|
||||
<scope-selector
|
||||
:show-all="true"
|
||||
:user-default="newDefaultScope"
|
||||
:initial-scope="newDefaultScope"
|
||||
:on-scope-change="changeVis"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p>
|
||||
<Checkbox v-model="newNoRichText">
|
||||
{{ $t('settings.no_rich_text_description') }}
|
||||
</Checkbox>
|
||||
</p>
|
||||
<p>
|
||||
<Checkbox v-model="hideFollows">
|
||||
{{ $t('settings.hide_follows_description') }}
|
||||
</Checkbox>
|
||||
</p>
|
||||
<p class="setting-subitem">
|
||||
<Checkbox
|
||||
v-model="hideFollowsCount"
|
||||
:disabled="!hideFollows"
|
||||
>
|
||||
{{ $t('settings.hide_follows_count_description') }}
|
||||
</Checkbox>
|
||||
</p>
|
||||
<p>
|
||||
<Checkbox v-model="hideFollowers">
|
||||
{{ $t('settings.hide_followers_description') }}
|
||||
</Checkbox>
|
||||
</p>
|
||||
<p class="setting-subitem">
|
||||
<Checkbox
|
||||
v-model="hideFollowersCount"
|
||||
:disabled="!hideFollowers"
|
||||
>
|
||||
{{ $t('settings.hide_followers_count_description') }}
|
||||
</Checkbox>
|
||||
</p>
|
||||
<p>
|
||||
<Checkbox v-model="allowFollowingMove">
|
||||
{{ $t('settings.allow_following_move') }}
|
||||
</Checkbox>
|
||||
</p>
|
||||
<p v-if="role === 'admin' || role === 'moderator'">
|
||||
<Checkbox v-model="showRole">
|
||||
<template v-if="role === 'admin'">
|
||||
{{ $t('settings.show_admin_badge') }}
|
||||
</template>
|
||||
<template v-if="role === 'moderator'">
|
||||
{{ $t('settings.show_moderator_badge') }}
|
||||
</template>
|
||||
</Checkbox>
|
||||
</p>
|
||||
<p>
|
||||
<Checkbox v-model="discoverable">
|
||||
{{ $t('settings.discoverable') }}
|
||||
</Checkbox>
|
||||
</p>
|
||||
<p>
|
||||
<Checkbox v-model="bot">
|
||||
{{ $t('settings.bot') }}
|
||||
</Checkbox>
|
||||
</p>
|
||||
<button
|
||||
:disabled="newName && newName.length === 0"
|
||||
class="btn btn-default"
|
||||
@click="updateProfile"
|
||||
>
|
||||
{{ $t('general.submit') }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.avatar') }}</h2>
|
||||
<p class="visibility-notice">
|
||||
{{ $t('settings.avatar_size_instruction') }}
|
||||
</p>
|
||||
<p>{{ $t('settings.current_avatar') }}</p>
|
||||
<img
|
||||
:src="user.profile_image_url_original"
|
||||
class="current-avatar"
|
||||
>
|
||||
<p>{{ $t('settings.set_new_avatar') }}</p>
|
||||
<button
|
||||
v-show="pickAvatarBtnVisible"
|
||||
id="pick-avatar"
|
||||
class="btn"
|
||||
type="button"
|
||||
>
|
||||
{{ $t('settings.upload_a_photo') }}
|
||||
</button>
|
||||
<image-cropper
|
||||
trigger="#pick-avatar"
|
||||
:submit-handler="submitAvatar"
|
||||
@open="pickAvatarBtnVisible=false"
|
||||
@close="pickAvatarBtnVisible=true"
|
||||
/>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.profile_banner') }}</h2>
|
||||
<p>{{ $t('settings.current_profile_banner') }}</p>
|
||||
<img
|
||||
:src="user.cover_photo"
|
||||
class="banner"
|
||||
>
|
||||
<p>{{ $t('settings.set_new_profile_banner') }}</p>
|
||||
<img
|
||||
v-if="bannerPreview"
|
||||
class="banner"
|
||||
:src="bannerPreview"
|
||||
>
|
||||
<div>
|
||||
<input
|
||||
type="file"
|
||||
@change="uploadFile('banner', $event)"
|
||||
>
|
||||
</div>
|
||||
<i
|
||||
v-if="bannerUploading"
|
||||
class=" icon-spin4 animate-spin uploading"
|
||||
/>
|
||||
<button
|
||||
v-else-if="bannerPreview"
|
||||
class="btn btn-default"
|
||||
@click="submitBanner"
|
||||
>
|
||||
{{ $t('general.submit') }}
|
||||
</button>
|
||||
<div
|
||||
v-if="bannerUploadError"
|
||||
class="alert error"
|
||||
>
|
||||
Error: {{ bannerUploadError }}
|
||||
<i
|
||||
class="button-icon icon-cancel"
|
||||
@click="clearUploadError('banner')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.profile_background') }}</h2>
|
||||
<p>{{ $t('settings.set_new_profile_background') }}</p>
|
||||
<img
|
||||
v-if="backgroundPreview"
|
||||
class="bg"
|
||||
:src="backgroundPreview"
|
||||
>
|
||||
<div>
|
||||
<input
|
||||
type="file"
|
||||
@change="uploadFile('background', $event)"
|
||||
>
|
||||
</div>
|
||||
<i
|
||||
v-if="backgroundUploading"
|
||||
class=" icon-spin4 animate-spin uploading"
|
||||
/>
|
||||
<button
|
||||
v-else-if="backgroundPreview"
|
||||
class="btn btn-default"
|
||||
@click="submitBg"
|
||||
>
|
||||
{{ $t('general.submit') }}
|
||||
</button>
|
||||
<div
|
||||
v-if="backgroundUploadError"
|
||||
class="alert error"
|
||||
>
|
||||
Error: {{ backgroundUploadError }}
|
||||
<i
|
||||
class="button-icon icon-cancel"
|
||||
@click="clearUploadError('background')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./profile_tab.js"></script>
|
||||
<style lang="scss" src="./profile_tab.scss"></style>
|
|
@ -0,0 +1,9 @@
|
|||
const Confirm = {
|
||||
props: ['disabled'],
|
||||
data: () => ({}),
|
||||
methods: {
|
||||
confirm () { this.$emit('confirm') },
|
||||
cancel () { this.$emit('cancel') }
|
||||
}
|
||||
}
|
||||
export default Confirm
|
22
src/components/settings_modal/tabs/security_tab/confirm.vue
Normal file
22
src/components/settings_modal/tabs/security_tab/confirm.vue
Normal file
|
@ -0,0 +1,22 @@
|
|||
<template>
|
||||
<div>
|
||||
<slot />
|
||||
<button
|
||||
class="btn btn-default"
|
||||
:disabled="disabled"
|
||||
@click="confirm"
|
||||
>
|
||||
{{ $t('general.confirm') }}
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-default"
|
||||
:disabled="disabled"
|
||||
@click="cancel"
|
||||
>
|
||||
{{ $t('general.cancel') }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./confirm.js">
|
||||
</script>
|
155
src/components/settings_modal/tabs/security_tab/mfa.js
Normal file
155
src/components/settings_modal/tabs/security_tab/mfa.js
Normal file
|
@ -0,0 +1,155 @@
|
|||
import RecoveryCodes from './mfa_backup_codes.vue'
|
||||
import TOTP from './mfa_totp.vue'
|
||||
import Confirm from './confirm.vue'
|
||||
import VueQrcode from '@chenfengyuan/vue-qrcode'
|
||||
import { mapState } from 'vuex'
|
||||
|
||||
const Mfa = {
|
||||
data: () => ({
|
||||
settings: { // current settings of MFA
|
||||
available: false,
|
||||
enabled: false,
|
||||
totp: false
|
||||
},
|
||||
setupState: { // setup mfa
|
||||
state: '', // state of setup. '' -> 'getBackupCodes' -> 'setupOTP' -> 'complete'
|
||||
setupOTPState: '' // state of setup otp. '' -> 'prepare' -> 'confirm' -> 'complete'
|
||||
},
|
||||
backupCodes: {
|
||||
getNewCodes: false,
|
||||
inProgress: false, // progress of fetch codes
|
||||
codes: []
|
||||
},
|
||||
otpSettings: { // pre-setup setting of OTP. secret key, qrcode url.
|
||||
provisioning_uri: '',
|
||||
key: ''
|
||||
},
|
||||
currentPassword: null,
|
||||
otpConfirmToken: null,
|
||||
error: null,
|
||||
readyInit: false
|
||||
}),
|
||||
components: {
|
||||
'recovery-codes': RecoveryCodes,
|
||||
'totp-item': TOTP,
|
||||
'qrcode': VueQrcode,
|
||||
'confirm': Confirm
|
||||
},
|
||||
computed: {
|
||||
canSetupOTP () {
|
||||
return (
|
||||
(this.setupInProgress && this.backupCodesPrepared) ||
|
||||
this.settings.enabled
|
||||
) && !this.settings.totp && !this.setupOTPInProgress
|
||||
},
|
||||
setupInProgress () {
|
||||
return this.setupState.state !== '' && this.setupState.state !== 'complete'
|
||||
},
|
||||
setupOTPInProgress () {
|
||||
return this.setupState.state === 'setupOTP' && !this.completedOTP
|
||||
},
|
||||
prepareOTP () {
|
||||
return this.setupState.setupOTPState === 'prepare'
|
||||
},
|
||||
confirmOTP () {
|
||||
return this.setupState.setupOTPState === 'confirm'
|
||||
},
|
||||
completedOTP () {
|
||||
return this.setupState.setupOTPState === 'completed'
|
||||
},
|
||||
backupCodesPrepared () {
|
||||
return !this.backupCodes.inProgress && this.backupCodes.codes.length > 0
|
||||
},
|
||||
confirmNewBackupCodes () {
|
||||
return this.backupCodes.getNewCodes
|
||||
},
|
||||
...mapState({
|
||||
backendInteractor: (state) => state.api.backendInteractor
|
||||
})
|
||||
},
|
||||
|
||||
methods: {
|
||||
activateOTP () {
|
||||
if (!this.settings.enabled) {
|
||||
this.setupState.state = 'getBackupcodes'
|
||||
this.fetchBackupCodes()
|
||||
}
|
||||
},
|
||||
fetchBackupCodes () {
|
||||
this.backupCodes.inProgress = true
|
||||
this.backupCodes.codes = []
|
||||
|
||||
return this.backendInteractor.generateMfaBackupCodes()
|
||||
.then((res) => {
|
||||
this.backupCodes.codes = res.codes
|
||||
this.backupCodes.inProgress = false
|
||||
})
|
||||
},
|
||||
getBackupCodes () { // get a new backup codes
|
||||
this.backupCodes.getNewCodes = true
|
||||
},
|
||||
confirmBackupCodes () { // confirm getting new backup codes
|
||||
this.fetchBackupCodes().then((res) => {
|
||||
this.backupCodes.getNewCodes = false
|
||||
})
|
||||
},
|
||||
cancelBackupCodes () { // cancel confirm form of new backup codes
|
||||
this.backupCodes.getNewCodes = false
|
||||
},
|
||||
|
||||
// Setup OTP
|
||||
setupOTP () { // prepare setup OTP
|
||||
this.setupState.state = 'setupOTP'
|
||||
this.setupState.setupOTPState = 'prepare'
|
||||
this.backendInteractor.mfaSetupOTP()
|
||||
.then((res) => {
|
||||
this.otpSettings = res
|
||||
this.setupState.setupOTPState = 'confirm'
|
||||
})
|
||||
},
|
||||
doConfirmOTP () { // handler confirm enable OTP
|
||||
this.error = null
|
||||
this.backendInteractor.mfaConfirmOTP({
|
||||
token: this.otpConfirmToken,
|
||||
password: this.currentPassword
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.error) {
|
||||
this.error = res.error
|
||||
return
|
||||
}
|
||||
this.completeSetup()
|
||||
})
|
||||
},
|
||||
|
||||
completeSetup () {
|
||||
this.setupState.setupOTPState = 'complete'
|
||||
this.setupState.state = 'complete'
|
||||
this.currentPassword = null
|
||||
this.error = null
|
||||
this.fetchSettings()
|
||||
},
|
||||
cancelSetup () { // cancel setup
|
||||
this.setupState.setupOTPState = ''
|
||||
this.setupState.state = ''
|
||||
this.currentPassword = null
|
||||
this.error = null
|
||||
},
|
||||
// end Setup OTP
|
||||
|
||||
// fetch settings from server
|
||||
async fetchSettings () {
|
||||
let result = await this.backendInteractor.settingsMFA()
|
||||
if (result.error) return
|
||||
this.settings = result.settings
|
||||
this.settings.available = true
|
||||
return result
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.fetchSettings().then(() => {
|
||||
this.readyInit = true
|
||||
})
|
||||
}
|
||||
}
|
||||
export default Mfa
|
173
src/components/settings_modal/tabs/security_tab/mfa.vue
Normal file
173
src/components/settings_modal/tabs/security_tab/mfa.vue
Normal file
|
@ -0,0 +1,173 @@
|
|||
<template>
|
||||
<div
|
||||
v-if="readyInit && settings.available"
|
||||
class="setting-item mfa-settings"
|
||||
>
|
||||
<div class="mfa-heading">
|
||||
<h2>{{ $t('settings.mfa.title') }}</h2>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div
|
||||
v-if="!setupInProgress"
|
||||
class="setting-item"
|
||||
>
|
||||
<!-- Enabled methods -->
|
||||
<h3>{{ $t('settings.mfa.authentication_methods') }}</h3>
|
||||
<totp-item
|
||||
:settings="settings"
|
||||
@deactivate="fetchSettings"
|
||||
@activate="activateOTP"
|
||||
/>
|
||||
<br>
|
||||
|
||||
<div v-if="settings.enabled">
|
||||
<!-- backup codes block-->
|
||||
<recovery-codes
|
||||
v-if="!confirmNewBackupCodes"
|
||||
:backup-codes="backupCodes"
|
||||
/>
|
||||
<button
|
||||
v-if="!confirmNewBackupCodes"
|
||||
class="btn btn-default"
|
||||
@click="getBackupCodes"
|
||||
>
|
||||
{{ $t('settings.mfa.generate_new_recovery_codes') }}
|
||||
</button>
|
||||
|
||||
<div v-if="confirmNewBackupCodes">
|
||||
<confirm
|
||||
:disabled="backupCodes.inProgress"
|
||||
@confirm="confirmBackupCodes"
|
||||
@cancel="cancelBackupCodes"
|
||||
>
|
||||
<p class="warning">
|
||||
{{ $t('settings.mfa.warning_of_generate_new_codes') }}
|
||||
</p>
|
||||
</confirm>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="setupInProgress">
|
||||
<!-- setup block-->
|
||||
|
||||
<h3>{{ $t('settings.mfa.setup_otp') }}</h3>
|
||||
|
||||
<recovery-codes
|
||||
v-if="!setupOTPInProgress"
|
||||
:backup-codes="backupCodes"
|
||||
/>
|
||||
|
||||
<button
|
||||
v-if="canSetupOTP"
|
||||
class="btn btn-default"
|
||||
@click="cancelSetup"
|
||||
>
|
||||
{{ $t('general.cancel') }}
|
||||
</button>
|
||||
|
||||
<button
|
||||
v-if="canSetupOTP"
|
||||
class="btn btn-default"
|
||||
@click="setupOTP"
|
||||
>
|
||||
{{ $t('settings.mfa.setup_otp') }}
|
||||
</button>
|
||||
|
||||
<template v-if="setupOTPInProgress">
|
||||
<i v-if="prepareOTP">{{ $t('settings.mfa.wait_pre_setup_otp') }}</i>
|
||||
|
||||
<div v-if="confirmOTP">
|
||||
<div class="setup-otp">
|
||||
<div class="qr-code">
|
||||
<h4>{{ $t('settings.mfa.scan.title') }}</h4>
|
||||
<p>{{ $t('settings.mfa.scan.desc') }}</p>
|
||||
<qrcode
|
||||
:value="otpSettings.provisioning_uri"
|
||||
:options="{ width: 200 }"
|
||||
/>
|
||||
<p>
|
||||
{{ $t('settings.mfa.scan.secret_code') }}:
|
||||
{{ otpSettings.key }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="verify">
|
||||
<h4>{{ $t('general.verify') }}</h4>
|
||||
<p>{{ $t('settings.mfa.verify.desc') }}</p>
|
||||
<input
|
||||
v-model="otpConfirmToken"
|
||||
type="text"
|
||||
>
|
||||
|
||||
<p>{{ $t('settings.enter_current_password_to_confirm') }}:</p>
|
||||
<input
|
||||
v-model="currentPassword"
|
||||
type="password"
|
||||
>
|
||||
<div class="confirm-otp-actions">
|
||||
<button
|
||||
class="btn btn-default"
|
||||
@click="doConfirmOTP"
|
||||
>
|
||||
{{ $t('settings.mfa.confirm_and_enable') }}
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-default"
|
||||
@click="cancelSetup"
|
||||
>
|
||||
{{ $t('general.cancel') }}
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
v-if="error"
|
||||
class="alert error"
|
||||
>
|
||||
{{ error }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./mfa.js"></script>
|
||||
<style lang="scss">
|
||||
@import '../../../../_variables.scss';
|
||||
.mfa-settings {
|
||||
.mfa-heading, .method-item {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.warning {
|
||||
color: $fallback--cOrange;
|
||||
color: var(--cOrange, $fallback--cOrange);
|
||||
}
|
||||
|
||||
.setup-otp {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
.qr-code {
|
||||
flex: 1;
|
||||
padding-right: 10px;
|
||||
}
|
||||
.verify { flex: 1; }
|
||||
.error { margin: 4px 0 0 0; }
|
||||
.confirm-otp-actions {
|
||||
button {
|
||||
width: 15em;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,17 @@
|
|||
export default {
|
||||
props: {
|
||||
backupCodes: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
inProgress: false,
|
||||
codes: []
|
||||
})
|
||||
}
|
||||
},
|
||||
data: () => ({}),
|
||||
computed: {
|
||||
inProgress () { return this.backupCodes.inProgress },
|
||||
ready () { return this.backupCodes.codes.length > 0 },
|
||||
displayTitle () { return this.inProgress || this.ready }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
<template>
|
||||
<div class="mfa-backup-codes">
|
||||
<h4 v-if="displayTitle">
|
||||
{{ $t('settings.mfa.recovery_codes') }}
|
||||
</h4>
|
||||
<i v-if="inProgress">{{ $t('settings.mfa.waiting_a_recovery_codes') }}</i>
|
||||
<template v-if="ready">
|
||||
<p class="alert warning">
|
||||
{{ $t('settings.mfa.recovery_codes_warning') }}
|
||||
</p>
|
||||
<ul class="backup-codes">
|
||||
<li
|
||||
v-for="code in backupCodes.codes"
|
||||
:key="code"
|
||||
>
|
||||
{{ code }}
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<script src="./mfa_backup_codes.js"></script>
|
||||
<style lang="scss">
|
||||
@import '../../../../_variables.scss';
|
||||
|
||||
.mfa-backup-codes {
|
||||
.warning {
|
||||
color: $fallback--cOrange;
|
||||
color: var(--cOrange, $fallback--cOrange);
|
||||
}
|
||||
.backup-codes {
|
||||
font-family: var(--postCodeFont, monospace);
|
||||
}
|
||||
}
|
||||
</style>
|
49
src/components/settings_modal/tabs/security_tab/mfa_totp.js
Normal file
49
src/components/settings_modal/tabs/security_tab/mfa_totp.js
Normal file
|
@ -0,0 +1,49 @@
|
|||
import Confirm from './confirm.vue'
|
||||
import { mapState } from 'vuex'
|
||||
|
||||
export default {
|
||||
props: ['settings'],
|
||||
data: () => ({
|
||||
error: false,
|
||||
currentPassword: '',
|
||||
deactivate: false,
|
||||
inProgress: false // progress peform request to disable otp method
|
||||
}),
|
||||
components: {
|
||||
'confirm': Confirm
|
||||
},
|
||||
computed: {
|
||||
isActivated () {
|
||||
return this.settings.totp
|
||||
},
|
||||
...mapState({
|
||||
backendInteractor: (state) => state.api.backendInteractor
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
doActivate () {
|
||||
this.$emit('activate')
|
||||
},
|
||||
cancelDeactivate () { this.deactivate = false },
|
||||
doDeactivate () {
|
||||
this.error = null
|
||||
this.deactivate = true
|
||||
},
|
||||
confirmDeactivate () { // confirm deactivate TOTP method
|
||||
this.error = null
|
||||
this.inProgress = true
|
||||
this.backendInteractor.mfaDisableOTP({
|
||||
password: this.currentPassword
|
||||
})
|
||||
.then((res) => {
|
||||
this.inProgress = false
|
||||
if (res.error) {
|
||||
this.error = res.error
|
||||
return
|
||||
}
|
||||
this.deactivate = false
|
||||
this.$emit('deactivate')
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
43
src/components/settings_modal/tabs/security_tab/mfa_totp.vue
Normal file
43
src/components/settings_modal/tabs/security_tab/mfa_totp.vue
Normal file
|
@ -0,0 +1,43 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="method-item">
|
||||
<strong>{{ $t('settings.mfa.otp') }}</strong>
|
||||
<button
|
||||
v-if="!isActivated"
|
||||
class="btn btn-default"
|
||||
@click="doActivate"
|
||||
>
|
||||
{{ $t('general.enable') }}
|
||||
</button>
|
||||
|
||||
<button
|
||||
v-if="isActivated"
|
||||
class="btn btn-default"
|
||||
:disabled="deactivate"
|
||||
@click="doDeactivate"
|
||||
>
|
||||
{{ $t('general.disable') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<confirm
|
||||
v-if="deactivate"
|
||||
:disabled="inProgress"
|
||||
@confirm="confirmDeactivate"
|
||||
@cancel="cancelDeactivate"
|
||||
>
|
||||
{{ $t('settings.enter_current_password_to_confirm') }}:
|
||||
<input
|
||||
v-model="currentPassword"
|
||||
type="password"
|
||||
>
|
||||
</confirm>
|
||||
<div
|
||||
v-if="error"
|
||||
class="alert error"
|
||||
>
|
||||
{{ error }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script src="./mfa_totp.js"></script>
|
106
src/components/settings_modal/tabs/security_tab/security_tab.js
Normal file
106
src/components/settings_modal/tabs/security_tab/security_tab.js
Normal file
|
@ -0,0 +1,106 @@
|
|||
import ProgressButton from 'src/components/progress_button/progress_button.vue'
|
||||
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||
import Mfa from './mfa.vue'
|
||||
|
||||
const SecurityTab = {
|
||||
data () {
|
||||
return {
|
||||
newEmail: '',
|
||||
changeEmailError: false,
|
||||
changeEmailPassword: '',
|
||||
changedEmail: false,
|
||||
deletingAccount: false,
|
||||
deleteAccountConfirmPasswordInput: '',
|
||||
deleteAccountError: false,
|
||||
changePasswordInputs: [ '', '', '' ],
|
||||
changedPassword: false,
|
||||
changePasswordError: false
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.$store.dispatch('fetchTokens')
|
||||
},
|
||||
components: {
|
||||
ProgressButton,
|
||||
Mfa,
|
||||
Checkbox
|
||||
},
|
||||
computed: {
|
||||
user () {
|
||||
return this.$store.state.users.currentUser
|
||||
},
|
||||
pleromaBackend () {
|
||||
return this.$store.state.instance.pleromaBackend
|
||||
},
|
||||
oauthTokens () {
|
||||
return this.$store.state.oauthTokens.tokens.map(oauthToken => {
|
||||
return {
|
||||
id: oauthToken.id,
|
||||
appName: oauthToken.app_name,
|
||||
validUntil: new Date(oauthToken.valid_until).toLocaleDateString()
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
confirmDelete () {
|
||||
this.deletingAccount = true
|
||||
},
|
||||
deleteAccount () {
|
||||
this.$store.state.api.backendInteractor.deleteAccount({ password: this.deleteAccountConfirmPasswordInput })
|
||||
.then((res) => {
|
||||
if (res.status === 'success') {
|
||||
this.$store.dispatch('logout')
|
||||
this.$router.push({ name: 'root' })
|
||||
} else {
|
||||
this.deleteAccountError = res.error
|
||||
}
|
||||
})
|
||||
},
|
||||
changePassword () {
|
||||
const params = {
|
||||
password: this.changePasswordInputs[0],
|
||||
newPassword: this.changePasswordInputs[1],
|
||||
newPasswordConfirmation: this.changePasswordInputs[2]
|
||||
}
|
||||
this.$store.state.api.backendInteractor.changePassword(params)
|
||||
.then((res) => {
|
||||
if (res.status === 'success') {
|
||||
this.changedPassword = true
|
||||
this.changePasswordError = false
|
||||
this.logout()
|
||||
} else {
|
||||
this.changedPassword = false
|
||||
this.changePasswordError = res.error
|
||||
}
|
||||
})
|
||||
},
|
||||
changeEmail () {
|
||||
const params = {
|
||||
email: this.newEmail,
|
||||
password: this.changeEmailPassword
|
||||
}
|
||||
this.$store.state.api.backendInteractor.changeEmail(params)
|
||||
.then((res) => {
|
||||
if (res.status === 'success') {
|
||||
this.changedEmail = true
|
||||
this.changeEmailError = false
|
||||
} else {
|
||||
this.changedEmail = false
|
||||
this.changeEmailError = res.error
|
||||
}
|
||||
})
|
||||
},
|
||||
logout () {
|
||||
this.$store.dispatch('logout')
|
||||
this.$router.replace('/')
|
||||
},
|
||||
revokeToken (id) {
|
||||
if (window.confirm(`${this.$i18n.t('settings.revoke_token')}?`)) {
|
||||
this.$store.dispatch('revokeToken', id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default SecurityTab
|
143
src/components/settings_modal/tabs/security_tab/security_tab.vue
Normal file
143
src/components/settings_modal/tabs/security_tab/security_tab.vue
Normal file
|
@ -0,0 +1,143 @@
|
|||
<template>
|
||||
<div :label="$t('settings.security_tab')">
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.change_email') }}</h2>
|
||||
<div>
|
||||
<p>{{ $t('settings.new_email') }}</p>
|
||||
<input
|
||||
v-model="newEmail"
|
||||
type="email"
|
||||
autocomplete="email"
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<p>{{ $t('settings.current_password') }}</p>
|
||||
<input
|
||||
v-model="changeEmailPassword"
|
||||
type="password"
|
||||
autocomplete="current-password"
|
||||
>
|
||||
</div>
|
||||
<button
|
||||
class="btn btn-default"
|
||||
@click="changeEmail"
|
||||
>
|
||||
{{ $t('general.submit') }}
|
||||
</button>
|
||||
<p v-if="changedEmail">
|
||||
{{ $t('settings.changed_email') }}
|
||||
</p>
|
||||
<template v-if="changeEmailError !== false">
|
||||
<p>{{ $t('settings.change_email_error') }}</p>
|
||||
<p>{{ changeEmailError }}</p>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.change_password') }}</h2>
|
||||
<div>
|
||||
<p>{{ $t('settings.current_password') }}</p>
|
||||
<input
|
||||
v-model="changePasswordInputs[0]"
|
||||
type="password"
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<p>{{ $t('settings.new_password') }}</p>
|
||||
<input
|
||||
v-model="changePasswordInputs[1]"
|
||||
type="password"
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<p>{{ $t('settings.confirm_new_password') }}</p>
|
||||
<input
|
||||
v-model="changePasswordInputs[2]"
|
||||
type="password"
|
||||
>
|
||||
</div>
|
||||
<button
|
||||
class="btn btn-default"
|
||||
@click="changePassword"
|
||||
>
|
||||
{{ $t('general.submit') }}
|
||||
</button>
|
||||
<p v-if="changedPassword">
|
||||
{{ $t('settings.changed_password') }}
|
||||
</p>
|
||||
<p v-else-if="changePasswordError !== false">
|
||||
{{ $t('settings.change_password_error') }}
|
||||
</p>
|
||||
<p v-if="changePasswordError">
|
||||
{{ changePasswordError }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.oauth_tokens') }}</h2>
|
||||
<table class="oauth-tokens">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ $t('settings.app_name') }}</th>
|
||||
<th>{{ $t('settings.valid_until') }}</th>
|
||||
<th />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="oauthToken in oauthTokens"
|
||||
:key="oauthToken.id"
|
||||
>
|
||||
<td>{{ oauthToken.appName }}</td>
|
||||
<td>{{ oauthToken.validUntil }}</td>
|
||||
<td class="actions">
|
||||
<button
|
||||
class="btn btn-default"
|
||||
@click="revokeToken(oauthToken.id)"
|
||||
>
|
||||
{{ $t('settings.revoke_token') }}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<mfa />
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.delete_account') }}</h2>
|
||||
<p v-if="!deletingAccount">
|
||||
{{ $t('settings.delete_account_description') }}
|
||||
</p>
|
||||
<div v-if="deletingAccount">
|
||||
<p>{{ $t('settings.delete_account_instructions') }}</p>
|
||||
<p>{{ $t('login.password') }}</p>
|
||||
<input
|
||||
v-model="deleteAccountConfirmPasswordInput"
|
||||
type="password"
|
||||
>
|
||||
<button
|
||||
class="btn btn-default"
|
||||
@click="deleteAccount"
|
||||
>
|
||||
{{ $t('settings.delete_account') }}
|
||||
</button>
|
||||
</div>
|
||||
<p v-if="deleteAccountError !== false">
|
||||
{{ $t('settings.delete_account_error') }}
|
||||
</p>
|
||||
<p v-if="deleteAccountError">
|
||||
{{ deleteAccountError }}
|
||||
</p>
|
||||
<button
|
||||
v-if="!deletingAccount"
|
||||
class="btn btn-default"
|
||||
@click="confirmDelete"
|
||||
>
|
||||
{{ $t('general.submit') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./security_tab.js"></script>
|
||||
<!-- <style lang="scss" src="./profile.scss"></style> -->
|
117
src/components/settings_modal/tabs/theme_tab/preview.vue
Normal file
117
src/components/settings_modal/tabs/theme_tab/preview.vue
Normal file
|
@ -0,0 +1,117 @@
|
|||
<template>
|
||||
<div class="preview-container">
|
||||
<div class="underlay underlay-preview" />
|
||||
<div class="panel dummy">
|
||||
<div class="panel-heading">
|
||||
<div class="title">
|
||||
{{ $t('settings.style.preview.header') }}
|
||||
<span class="badge badge-notification">
|
||||
99
|
||||
</span>
|
||||
</div>
|
||||
<span class="faint">
|
||||
{{ $t('settings.style.preview.header_faint') }}
|
||||
</span>
|
||||
<span class="alert error">
|
||||
{{ $t('settings.style.preview.error') }}
|
||||
</span>
|
||||
<button class="btn">
|
||||
{{ $t('settings.style.preview.button') }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="panel-body theme-preview-content">
|
||||
<div class="post">
|
||||
<div class="avatar still-image">
|
||||
( ͡° ͜ʖ ͡°)
|
||||
</div>
|
||||
<div class="content">
|
||||
<h4>
|
||||
{{ $t('settings.style.preview.content') }}
|
||||
</h4>
|
||||
|
||||
<i18n path="settings.style.preview.text">
|
||||
<code style="font-family: var(--postCodeFont)">
|
||||
{{ $t('settings.style.preview.mono') }}
|
||||
</code>
|
||||
<a style="color: var(--link)">
|
||||
{{ $t('settings.style.preview.link') }}
|
||||
</a>
|
||||
</i18n>
|
||||
|
||||
<div class="icons">
|
||||
<i
|
||||
style="color: var(--cBlue)"
|
||||
class="button-icon icon-reply"
|
||||
/>
|
||||
<i
|
||||
style="color: var(--cGreen)"
|
||||
class="button-icon icon-retweet"
|
||||
/>
|
||||
<i
|
||||
style="color: var(--cOrange)"
|
||||
class="button-icon icon-star"
|
||||
/>
|
||||
<i
|
||||
style="color: var(--cRed)"
|
||||
class="button-icon icon-cancel"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="after-post">
|
||||
<div class="avatar-alt">
|
||||
:^)
|
||||
</div>
|
||||
<div class="content">
|
||||
<i18n
|
||||
path="settings.style.preview.fine_print"
|
||||
tag="span"
|
||||
class="faint"
|
||||
>
|
||||
<a style="color: var(--faintLink)">
|
||||
{{ $t('settings.style.preview.faint_link') }}
|
||||
</a>
|
||||
</i18n>
|
||||
</div>
|
||||
</div>
|
||||
<div class="separator" />
|
||||
|
||||
<span class="alert error">
|
||||
{{ $t('settings.style.preview.error') }}
|
||||
</span>
|
||||
<input
|
||||
:value="$t('settings.style.preview.input')"
|
||||
type="text"
|
||||
>
|
||||
|
||||
<div class="actions">
|
||||
<span class="checkbox">
|
||||
<input
|
||||
id="preview_checkbox"
|
||||
checked="very yes"
|
||||
type="checkbox"
|
||||
>
|
||||
<label for="preview_checkbox">{{ $t('settings.style.preview.checkbox') }}</label>
|
||||
</span>
|
||||
<button class="btn">
|
||||
{{ $t('settings.style.preview.button') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.preview-container {
|
||||
position: relative;
|
||||
}
|
||||
.underlay-preview {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 10px;
|
||||
right: 10px;
|
||||
}
|
||||
</style>
|
759
src/components/settings_modal/tabs/theme_tab/theme_tab.js
Normal file
759
src/components/settings_modal/tabs/theme_tab/theme_tab.js
Normal file
|
@ -0,0 +1,759 @@
|
|||
import { set, delete as del } from 'vue'
|
||||
import {
|
||||
rgb2hex,
|
||||
hex2rgb,
|
||||
getContrastRatioLayers
|
||||
} from 'src/services/color_convert/color_convert.js'
|
||||
import {
|
||||
DEFAULT_SHADOWS,
|
||||
generateColors,
|
||||
generateShadows,
|
||||
generateRadii,
|
||||
generateFonts,
|
||||
composePreset,
|
||||
getThemes,
|
||||
shadows2to3,
|
||||
colors2to3
|
||||
} from 'src/services/style_setter/style_setter.js'
|
||||
import {
|
||||
SLOT_INHERITANCE
|
||||
} from 'src/services/theme_data/pleromafe.js'
|
||||
import {
|
||||
CURRENT_VERSION,
|
||||
OPACITIES,
|
||||
getLayers,
|
||||
getOpacitySlot
|
||||
} from 'src/services/theme_data/theme_data.service.js'
|
||||
import ColorInput from 'src/components/color_input/color_input.vue'
|
||||
import RangeInput from 'src/components/range_input/range_input.vue'
|
||||
import OpacityInput from 'src/components/opacity_input/opacity_input.vue'
|
||||
import ShadowControl from 'src/components/shadow_control/shadow_control.vue'
|
||||
import FontControl from 'src/components/font_control/font_control.vue'
|
||||
import ContrastRatio from 'src/components/contrast_ratio/contrast_ratio.vue'
|
||||
import TabSwitcher from 'src/components/tab_switcher/tab_switcher.js'
|
||||
import ExportImport from 'src/components/export_import/export_import.vue'
|
||||
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||
|
||||
import Preview from './preview.vue'
|
||||
|
||||
// List of color values used in v1
|
||||
const v1OnlyNames = [
|
||||
'bg',
|
||||
'fg',
|
||||
'text',
|
||||
'link',
|
||||
'cRed',
|
||||
'cGreen',
|
||||
'cBlue',
|
||||
'cOrange'
|
||||
].map(_ => _ + 'ColorLocal')
|
||||
|
||||
const colorConvert = (color) => {
|
||||
if (color.startsWith('--') || color === 'transparent') {
|
||||
return color
|
||||
} else {
|
||||
return hex2rgb(color)
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
availableStyles: [],
|
||||
selected: this.$store.getters.mergedConfig.theme,
|
||||
themeWarning: undefined,
|
||||
tempImportFile: undefined,
|
||||
engineVersion: 0,
|
||||
|
||||
previewShadows: {},
|
||||
previewColors: {},
|
||||
previewRadii: {},
|
||||
previewFonts: {},
|
||||
|
||||
shadowsInvalid: true,
|
||||
colorsInvalid: true,
|
||||
radiiInvalid: true,
|
||||
|
||||
keepColor: false,
|
||||
keepShadows: false,
|
||||
keepOpacity: false,
|
||||
keepRoundness: false,
|
||||
keepFonts: false,
|
||||
|
||||
...Object.keys(SLOT_INHERITANCE)
|
||||
.map(key => [key, ''])
|
||||
.reduce((acc, [key, val]) => ({ ...acc, [ key + 'ColorLocal' ]: val }), {}),
|
||||
|
||||
...Object.keys(OPACITIES)
|
||||
.map(key => [key, ''])
|
||||
.reduce((acc, [key, val]) => ({ ...acc, [ key + 'OpacityLocal' ]: val }), {}),
|
||||
|
||||
shadowSelected: undefined,
|
||||
shadowsLocal: {},
|
||||
fontsLocal: {},
|
||||
|
||||
btnRadiusLocal: '',
|
||||
inputRadiusLocal: '',
|
||||
checkboxRadiusLocal: '',
|
||||
panelRadiusLocal: '',
|
||||
avatarRadiusLocal: '',
|
||||
avatarAltRadiusLocal: '',
|
||||
attachmentRadiusLocal: '',
|
||||
tooltipRadiusLocal: ''
|
||||
}
|
||||
},
|
||||
created () {
|
||||
const self = this
|
||||
|
||||
getThemes()
|
||||
.then((promises) => {
|
||||
return Promise.all(
|
||||
Object.entries(promises)
|
||||
.map(([k, v]) => v.then(res => [k, res]))
|
||||
)
|
||||
})
|
||||
.then(themes => themes.reduce((acc, [k, v]) => {
|
||||
if (v) {
|
||||
return {
|
||||
...acc,
|
||||
[k]: v
|
||||
}
|
||||
} else {
|
||||
return acc
|
||||
}
|
||||
}, {}))
|
||||
.then((themesComplete) => {
|
||||
self.availableStyles = themesComplete
|
||||
})
|
||||
},
|
||||
mounted () {
|
||||
this.loadThemeFromLocalStorage()
|
||||
if (typeof this.shadowSelected === 'undefined') {
|
||||
this.shadowSelected = this.shadowsAvailable[0]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
themeWarningHelp () {
|
||||
if (!this.themeWarning) return
|
||||
const t = this.$t
|
||||
const pre = 'settings.style.switcher.help.'
|
||||
const {
|
||||
origin,
|
||||
themeEngineVersion,
|
||||
type,
|
||||
noActionsPossible
|
||||
} = this.themeWarning
|
||||
if (origin === 'file') {
|
||||
// Loaded v2 theme from file
|
||||
if (themeEngineVersion === 2 && type === 'wrong_version') {
|
||||
return t(pre + 'v2_imported')
|
||||
}
|
||||
if (themeEngineVersion > CURRENT_VERSION) {
|
||||
return t(pre + 'future_version_imported') + ' ' +
|
||||
(
|
||||
noActionsPossible
|
||||
? t(pre + 'snapshot_missing')
|
||||
: t(pre + 'snapshot_present')
|
||||
)
|
||||
}
|
||||
if (themeEngineVersion < CURRENT_VERSION) {
|
||||
return t(pre + 'future_version_imported') + ' ' +
|
||||
(
|
||||
noActionsPossible
|
||||
? t(pre + 'snapshot_missing')
|
||||
: t(pre + 'snapshot_present')
|
||||
)
|
||||
}
|
||||
} else if (origin === 'localStorage') {
|
||||
if (type === 'snapshot_source_mismatch') {
|
||||
return t(pre + 'snapshot_source_mismatch')
|
||||
}
|
||||
// FE upgraded from v2
|
||||
if (themeEngineVersion === 2) {
|
||||
return t(pre + 'upgraded_from_v2')
|
||||
}
|
||||
// Admin downgraded FE
|
||||
if (themeEngineVersion > CURRENT_VERSION) {
|
||||
return t(pre + 'fe_downgraded') + ' ' +
|
||||
(
|
||||
noActionsPossible
|
||||
? t(pre + 'migration_snapshot_ok')
|
||||
: t(pre + 'migration_snapshot_gone')
|
||||
)
|
||||
}
|
||||
// Admin upgraded FE
|
||||
if (themeEngineVersion < CURRENT_VERSION) {
|
||||
return t(pre + 'fe_upgraded') + ' ' +
|
||||
(
|
||||
noActionsPossible
|
||||
? t(pre + 'migration_snapshot_ok')
|
||||
: t(pre + 'migration_snapshot_gone')
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
selectedVersion () {
|
||||
return Array.isArray(this.selected) ? 1 : 2
|
||||
},
|
||||
currentColors () {
|
||||
return Object.keys(SLOT_INHERITANCE)
|
||||
.map(key => [key, this[key + 'ColorLocal']])
|
||||
.reduce((acc, [key, val]) => ({ ...acc, [ key ]: val }), {})
|
||||
},
|
||||
currentOpacity () {
|
||||
return Object.keys(OPACITIES)
|
||||
.map(key => [key, this[key + 'OpacityLocal']])
|
||||
.reduce((acc, [key, val]) => ({ ...acc, [ key ]: val }), {})
|
||||
},
|
||||
currentRadii () {
|
||||
return {
|
||||
btn: this.btnRadiusLocal,
|
||||
input: this.inputRadiusLocal,
|
||||
checkbox: this.checkboxRadiusLocal,
|
||||
panel: this.panelRadiusLocal,
|
||||
avatar: this.avatarRadiusLocal,
|
||||
avatarAlt: this.avatarAltRadiusLocal,
|
||||
tooltip: this.tooltipRadiusLocal,
|
||||
attachment: this.attachmentRadiusLocal
|
||||
}
|
||||
},
|
||||
preview () {
|
||||
return composePreset(this.previewColors, this.previewRadii, this.previewShadows, this.previewFonts)
|
||||
},
|
||||
previewTheme () {
|
||||
if (!this.preview.theme.colors) return { colors: {}, opacity: {}, radii: {}, shadows: {}, fonts: {} }
|
||||
return this.preview.theme
|
||||
},
|
||||
// This needs optimization maybe
|
||||
previewContrast () {
|
||||
try {
|
||||
if (!this.previewTheme.colors.bg) return {}
|
||||
const colors = this.previewTheme.colors
|
||||
const opacity = this.previewTheme.opacity
|
||||
if (!colors.bg) return {}
|
||||
const hints = (ratio) => ({
|
||||
text: ratio.toPrecision(3) + ':1',
|
||||
// AA level, AAA level
|
||||
aa: ratio >= 4.5,
|
||||
aaa: ratio >= 7,
|
||||
// same but for 18pt+ texts
|
||||
laa: ratio >= 3,
|
||||
laaa: ratio >= 4.5
|
||||
})
|
||||
const colorsConverted = Object.entries(colors).reduce((acc, [key, value]) => ({ ...acc, [key]: colorConvert(value) }), {})
|
||||
|
||||
const ratios = Object.entries(SLOT_INHERITANCE).reduce((acc, [key, value]) => {
|
||||
const slotIsBaseText = key === 'text' || key === 'link'
|
||||
const slotIsText = slotIsBaseText || (
|
||||
typeof value === 'object' && value !== null && value.textColor
|
||||
)
|
||||
if (!slotIsText) return acc
|
||||
const { layer, variant } = slotIsBaseText ? { layer: 'bg' } : value
|
||||
const background = variant || layer
|
||||
const opacitySlot = getOpacitySlot(background)
|
||||
const textColors = [
|
||||
key,
|
||||
...(background === 'bg' ? ['cRed', 'cGreen', 'cBlue', 'cOrange'] : [])
|
||||
]
|
||||
|
||||
const layers = getLayers(
|
||||
layer,
|
||||
variant || layer,
|
||||
opacitySlot,
|
||||
colorsConverted,
|
||||
opacity
|
||||
)
|
||||
|
||||
return {
|
||||
...acc,
|
||||
...textColors.reduce((acc, textColorKey) => {
|
||||
const newKey = slotIsBaseText
|
||||
? 'bg' + textColorKey[0].toUpperCase() + textColorKey.slice(1)
|
||||
: textColorKey
|
||||
return {
|
||||
...acc,
|
||||
[newKey]: getContrastRatioLayers(
|
||||
colorsConverted[textColorKey],
|
||||
layers,
|
||||
colorsConverted[textColorKey]
|
||||
)
|
||||
}
|
||||
}, {})
|
||||
}
|
||||
}, {})
|
||||
|
||||
return Object.entries(ratios).reduce((acc, [k, v]) => { acc[k] = hints(v); return acc }, {})
|
||||
} catch (e) {
|
||||
console.warn('Failure computing contrasts', e)
|
||||
}
|
||||
},
|
||||
previewRules () {
|
||||
if (!this.preview.rules) return ''
|
||||
return [
|
||||
...Object.values(this.preview.rules),
|
||||
'color: var(--text)',
|
||||
'font-family: var(--interfaceFont, sans-serif)'
|
||||
].join(';')
|
||||
},
|
||||
shadowsAvailable () {
|
||||
return Object.keys(DEFAULT_SHADOWS).sort()
|
||||
},
|
||||
currentShadowOverriden: {
|
||||
get () {
|
||||
return !!this.currentShadow
|
||||
},
|
||||
set (val) {
|
||||
if (val) {
|
||||
set(this.shadowsLocal, this.shadowSelected, this.currentShadowFallback.map(_ => Object.assign({}, _)))
|
||||
} else {
|
||||
del(this.shadowsLocal, this.shadowSelected)
|
||||
}
|
||||
}
|
||||
},
|
||||
currentShadowFallback () {
|
||||
return (this.previewTheme.shadows || {})[this.shadowSelected]
|
||||
},
|
||||
currentShadow: {
|
||||
get () {
|
||||
return this.shadowsLocal[this.shadowSelected]
|
||||
},
|
||||
set (v) {
|
||||
set(this.shadowsLocal, this.shadowSelected, v)
|
||||
}
|
||||
},
|
||||
themeValid () {
|
||||
return !this.shadowsInvalid && !this.colorsInvalid && !this.radiiInvalid
|
||||
},
|
||||
exportedTheme () {
|
||||
const saveEverything = (
|
||||
!this.keepFonts &&
|
||||
!this.keepShadows &&
|
||||
!this.keepOpacity &&
|
||||
!this.keepRoundness &&
|
||||
!this.keepColor
|
||||
)
|
||||
|
||||
const source = {
|
||||
themeEngineVersion: CURRENT_VERSION
|
||||
}
|
||||
|
||||
if (this.keepFonts || saveEverything) {
|
||||
source.fonts = this.fontsLocal
|
||||
}
|
||||
if (this.keepShadows || saveEverything) {
|
||||
source.shadows = this.shadowsLocal
|
||||
}
|
||||
if (this.keepOpacity || saveEverything) {
|
||||
source.opacity = this.currentOpacity
|
||||
}
|
||||
if (this.keepColor || saveEverything) {
|
||||
source.colors = this.currentColors
|
||||
}
|
||||
if (this.keepRoundness || saveEverything) {
|
||||
source.radii = this.currentRadii
|
||||
}
|
||||
|
||||
const theme = {
|
||||
themeEngineVersion: CURRENT_VERSION,
|
||||
...this.previewTheme
|
||||
}
|
||||
|
||||
return {
|
||||
// To separate from other random JSON files and possible future source formats
|
||||
_pleroma_theme_version: 2, theme, source
|
||||
}
|
||||
}
|
||||
},
|
||||
components: {
|
||||
ColorInput,
|
||||
OpacityInput,
|
||||
RangeInput,
|
||||
ContrastRatio,
|
||||
ShadowControl,
|
||||
FontControl,
|
||||
TabSwitcher,
|
||||
Preview,
|
||||
ExportImport,
|
||||
Checkbox
|
||||
},
|
||||
methods: {
|
||||
loadTheme (
|
||||
{
|
||||
theme,
|
||||
source,
|
||||
_pleroma_theme_version: fileVersion
|
||||
},
|
||||
origin,
|
||||
forceUseSource = false
|
||||
) {
|
||||
this.dismissWarning()
|
||||
if (!source && !theme) {
|
||||
throw new Error('Can\'t load theme: empty')
|
||||
}
|
||||
const version = (origin === 'localStorage' && !theme.colors)
|
||||
? 'l1'
|
||||
: fileVersion
|
||||
const snapshotEngineVersion = (theme || {}).themeEngineVersion
|
||||
const themeEngineVersion = (source || {}).themeEngineVersion || 2
|
||||
const versionsMatch = themeEngineVersion === CURRENT_VERSION
|
||||
const sourceSnapshotMismatch = (
|
||||
theme !== undefined &&
|
||||
source !== undefined &&
|
||||
themeEngineVersion !== snapshotEngineVersion
|
||||
)
|
||||
// Force loading of source if user requested it or if snapshot
|
||||
// is unavailable
|
||||
const forcedSourceLoad = (source && forceUseSource) || !theme
|
||||
if (!(versionsMatch && !sourceSnapshotMismatch) &&
|
||||
!forcedSourceLoad &&
|
||||
version !== 'l1' &&
|
||||
origin !== 'defaults'
|
||||
) {
|
||||
if (sourceSnapshotMismatch && origin === 'localStorage') {
|
||||
this.themeWarning = {
|
||||
origin,
|
||||
themeEngineVersion,
|
||||
type: 'snapshot_source_mismatch'
|
||||
}
|
||||
} else if (!theme) {
|
||||
this.themeWarning = {
|
||||
origin,
|
||||
noActionsPossible: true,
|
||||
themeEngineVersion,
|
||||
type: 'no_snapshot_old_version'
|
||||
}
|
||||
} else if (!versionsMatch) {
|
||||
this.themeWarning = {
|
||||
origin,
|
||||
noActionsPossible: !source,
|
||||
themeEngineVersion,
|
||||
type: 'wrong_version'
|
||||
}
|
||||
}
|
||||
}
|
||||
this.normalizeLocalState(theme, version, source, forcedSourceLoad)
|
||||
},
|
||||
forceLoadLocalStorage () {
|
||||
this.loadThemeFromLocalStorage(true)
|
||||
},
|
||||
dismissWarning () {
|
||||
this.themeWarning = undefined
|
||||
this.tempImportFile = undefined
|
||||
},
|
||||
forceLoad () {
|
||||
const { origin } = this.themeWarning
|
||||
switch (origin) {
|
||||
case 'localStorage':
|
||||
this.loadThemeFromLocalStorage(true)
|
||||
break
|
||||
case 'file':
|
||||
this.onImport(this.tempImportFile, true)
|
||||
break
|
||||
}
|
||||
this.dismissWarning()
|
||||
},
|
||||
forceSnapshot () {
|
||||
const { origin } = this.themeWarning
|
||||
switch (origin) {
|
||||
case 'localStorage':
|
||||
this.loadThemeFromLocalStorage(false, true)
|
||||
break
|
||||
case 'file':
|
||||
console.err('Forcing snapshout from file is not supported yet')
|
||||
break
|
||||
}
|
||||
this.dismissWarning()
|
||||
},
|
||||
loadThemeFromLocalStorage (confirmLoadSource = false, forceSnapshot = false) {
|
||||
const {
|
||||
customTheme: theme,
|
||||
customThemeSource: source
|
||||
} = this.$store.getters.mergedConfig
|
||||
if (!theme && !source) {
|
||||
// Anon user or never touched themes
|
||||
this.loadTheme(
|
||||
this.$store.state.instance.themeData,
|
||||
'defaults',
|
||||
confirmLoadSource
|
||||
)
|
||||
} else {
|
||||
this.loadTheme(
|
||||
{
|
||||
theme,
|
||||
source: forceSnapshot ? theme : source
|
||||
},
|
||||
'localStorage',
|
||||
confirmLoadSource
|
||||
)
|
||||
}
|
||||
},
|
||||
setCustomTheme () {
|
||||
this.$store.dispatch('setOption', {
|
||||
name: 'customTheme',
|
||||
value: {
|
||||
themeEngineVersion: CURRENT_VERSION,
|
||||
...this.previewTheme
|
||||
}
|
||||
})
|
||||
this.$store.dispatch('setOption', {
|
||||
name: 'customThemeSource',
|
||||
value: {
|
||||
themeEngineVersion: CURRENT_VERSION,
|
||||
shadows: this.shadowsLocal,
|
||||
fonts: this.fontsLocal,
|
||||
opacity: this.currentOpacity,
|
||||
colors: this.currentColors,
|
||||
radii: this.currentRadii
|
||||
}
|
||||
})
|
||||
},
|
||||
updatePreviewColorsAndShadows () {
|
||||
this.previewColors = generateColors({
|
||||
opacity: this.currentOpacity,
|
||||
colors: this.currentColors
|
||||
})
|
||||
this.previewShadows = generateShadows(
|
||||
{ shadows: this.shadowsLocal, opacity: this.previewTheme.opacity, themeEngineVersion: this.engineVersion },
|
||||
this.previewColors.theme.colors,
|
||||
this.previewColors.mod
|
||||
)
|
||||
},
|
||||
onImport (parsed, forceSource = false) {
|
||||
this.tempImportFile = parsed
|
||||
this.loadTheme(parsed, 'file', forceSource)
|
||||
},
|
||||
importValidator (parsed) {
|
||||
const version = parsed._pleroma_theme_version
|
||||
return version >= 1 || version <= 2
|
||||
},
|
||||
clearAll () {
|
||||
this.loadThemeFromLocalStorage()
|
||||
},
|
||||
|
||||
// Clears all the extra stuff when loading V1 theme
|
||||
clearV1 () {
|
||||
Object.keys(this.$data)
|
||||
.filter(_ => _.endsWith('ColorLocal') || _.endsWith('OpacityLocal'))
|
||||
.filter(_ => !v1OnlyNames.includes(_))
|
||||
.forEach(key => {
|
||||
set(this.$data, key, undefined)
|
||||
})
|
||||
},
|
||||
|
||||
clearRoundness () {
|
||||
Object.keys(this.$data)
|
||||
.filter(_ => _.endsWith('RadiusLocal'))
|
||||
.forEach(key => {
|
||||
set(this.$data, key, undefined)
|
||||
})
|
||||
},
|
||||
|
||||
clearOpacity () {
|
||||
Object.keys(this.$data)
|
||||
.filter(_ => _.endsWith('OpacityLocal'))
|
||||
.forEach(key => {
|
||||
set(this.$data, key, undefined)
|
||||
})
|
||||
},
|
||||
|
||||
clearShadows () {
|
||||
this.shadowsLocal = {}
|
||||
},
|
||||
|
||||
clearFonts () {
|
||||
this.fontsLocal = {}
|
||||
},
|
||||
|
||||
/**
|
||||
* This applies stored theme data onto form. Supports three versions of data:
|
||||
* v3 (version >= 3) - newest version of themes which supports snapshots for better compatiblity
|
||||
* v2 (version = 2) - newer version of themes.
|
||||
* v1 (version = 1) - older version of themes (import from file)
|
||||
* v1l (version = l1) - older version of theme (load from local storage)
|
||||
* v1 and v1l differ because of way themes were stored/exported.
|
||||
* @param {Object} theme - theme data (snapshot)
|
||||
* @param {Number} version - version of data. 0 means try to guess based on data. "l1" means v1, locastorage type
|
||||
* @param {Object} source - theme source - this will be used if compatible
|
||||
* @param {Boolean} source - by default source won't be used if version doesn't match since it might render differently
|
||||
* this allows importing source anyway
|
||||
*/
|
||||
normalizeLocalState (theme, version = 0, source, forceSource = false) {
|
||||
let input
|
||||
if (typeof source !== 'undefined') {
|
||||
if (forceSource || source.themeEngineVersion === CURRENT_VERSION) {
|
||||
input = source
|
||||
version = source.themeEngineVersion
|
||||
} else {
|
||||
input = theme
|
||||
}
|
||||
} else {
|
||||
input = theme
|
||||
}
|
||||
|
||||
const radii = input.radii || input
|
||||
const opacity = input.opacity
|
||||
const shadows = input.shadows || {}
|
||||
const fonts = input.fonts || {}
|
||||
const colors = !input.themeEngineVersion
|
||||
? colors2to3(input.colors || input)
|
||||
: input.colors || input
|
||||
|
||||
if (version === 0) {
|
||||
if (input.version) version = input.version
|
||||
// Old v1 naming: fg is text, btn is foreground
|
||||
if (typeof colors.text === 'undefined' && typeof colors.fg !== 'undefined') {
|
||||
version = 1
|
||||
}
|
||||
// New v2 naming: text is text, fg is foreground
|
||||
if (typeof colors.text !== 'undefined' && typeof colors.fg !== 'undefined') {
|
||||
version = 2
|
||||
}
|
||||
}
|
||||
|
||||
this.engineVersion = version
|
||||
|
||||
// Stuff that differs between V1 and V2
|
||||
if (version === 1) {
|
||||
this.fgColorLocal = rgb2hex(colors.btn)
|
||||
this.textColorLocal = rgb2hex(colors.fg)
|
||||
}
|
||||
|
||||
if (!this.keepColor) {
|
||||
this.clearV1()
|
||||
const keys = new Set(version !== 1 ? Object.keys(SLOT_INHERITANCE) : [])
|
||||
if (version === 1 || version === 'l1') {
|
||||
keys
|
||||
.add('bg')
|
||||
.add('link')
|
||||
.add('cRed')
|
||||
.add('cBlue')
|
||||
.add('cGreen')
|
||||
.add('cOrange')
|
||||
}
|
||||
|
||||
keys.forEach(key => {
|
||||
const color = colors[key]
|
||||
const hex = rgb2hex(colors[key])
|
||||
this[key + 'ColorLocal'] = hex === '#aN' ? color : hex
|
||||
})
|
||||
}
|
||||
|
||||
if (opacity && !this.keepOpacity) {
|
||||
this.clearOpacity()
|
||||
Object.entries(opacity).forEach(([k, v]) => {
|
||||
if (typeof v === 'undefined' || v === null || Number.isNaN(v)) return
|
||||
this[k + 'OpacityLocal'] = v
|
||||
})
|
||||
}
|
||||
|
||||
if (!this.keepRoundness) {
|
||||
this.clearRoundness()
|
||||
Object.entries(radii).forEach(([k, v]) => {
|
||||
// 'Radius' is kept mostly for v1->v2 localstorage transition
|
||||
const key = k.endsWith('Radius') ? k.split('Radius')[0] : k
|
||||
this[key + 'RadiusLocal'] = v
|
||||
})
|
||||
}
|
||||
|
||||
if (!this.keepShadows) {
|
||||
this.clearShadows()
|
||||
if (version === 2) {
|
||||
this.shadowsLocal = shadows2to3(shadows, this.previewTheme.opacity)
|
||||
} else {
|
||||
this.shadowsLocal = shadows
|
||||
}
|
||||
this.shadowSelected = this.shadowsAvailable[0]
|
||||
}
|
||||
|
||||
if (!this.keepFonts) {
|
||||
this.clearFonts()
|
||||
this.fontsLocal = fonts
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
currentRadii () {
|
||||
try {
|
||||
this.previewRadii = generateRadii({ radii: this.currentRadii })
|
||||
this.radiiInvalid = false
|
||||
} catch (e) {
|
||||
this.radiiInvalid = true
|
||||
console.warn(e)
|
||||
}
|
||||
},
|
||||
shadowsLocal: {
|
||||
handler () {
|
||||
if (Object.getOwnPropertyNames(this.previewColors).length === 1) return
|
||||
try {
|
||||
this.updatePreviewColorsAndShadows()
|
||||
this.shadowsInvalid = false
|
||||
} catch (e) {
|
||||
this.shadowsInvalid = true
|
||||
console.warn(e)
|
||||
}
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
fontsLocal: {
|
||||
handler () {
|
||||
try {
|
||||
this.previewFonts = generateFonts({ fonts: this.fontsLocal })
|
||||
this.fontsInvalid = false
|
||||
} catch (e) {
|
||||
this.fontsInvalid = true
|
||||
console.warn(e)
|
||||
}
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
currentColors () {
|
||||
try {
|
||||
this.updatePreviewColorsAndShadows()
|
||||
this.colorsInvalid = false
|
||||
this.shadowsInvalid = false
|
||||
} catch (e) {
|
||||
this.colorsInvalid = true
|
||||
this.shadowsInvalid = true
|
||||
console.warn(e)
|
||||
}
|
||||
},
|
||||
currentOpacity () {
|
||||
try {
|
||||
this.updatePreviewColorsAndShadows()
|
||||
} catch (e) {
|
||||
console.warn(e)
|
||||
}
|
||||
},
|
||||
selected () {
|
||||
this.dismissWarning()
|
||||
if (this.selectedVersion === 1) {
|
||||
if (!this.keepRoundness) {
|
||||
this.clearRoundness()
|
||||
}
|
||||
|
||||
if (!this.keepShadows) {
|
||||
this.clearShadows()
|
||||
}
|
||||
|
||||
if (!this.keepOpacity) {
|
||||
this.clearOpacity()
|
||||
}
|
||||
|
||||
if (!this.keepColor) {
|
||||
this.clearV1()
|
||||
|
||||
this.bgColorLocal = this.selected[1]
|
||||
this.fgColorLocal = this.selected[2]
|
||||
this.textColorLocal = this.selected[3]
|
||||
this.linkColorLocal = this.selected[4]
|
||||
this.cRedColorLocal = this.selected[5]
|
||||
this.cGreenColorLocal = this.selected[6]
|
||||
this.cBlueColorLocal = this.selected[7]
|
||||
this.cOrangeColorLocal = this.selected[8]
|
||||
}
|
||||
} else if (this.selectedVersion >= 2) {
|
||||
this.normalizeLocalState(this.selected.theme, 2, this.selected.source)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
345
src/components/settings_modal/tabs/theme_tab/theme_tab.scss
Normal file
345
src/components/settings_modal/tabs/theme_tab/theme_tab.scss
Normal file
|
@ -0,0 +1,345 @@
|
|||
@import 'src/_variables.scss';
|
||||
.theme-tab {
|
||||
padding-bottom: 2em;
|
||||
.theme-warning {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
margin-bottom: .5em;
|
||||
.buttons {
|
||||
.btn {
|
||||
margin-bottom: .5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
.preset-switcher {
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
.style-control {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
margin-bottom: 5px;
|
||||
|
||||
.label {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
input, select {
|
||||
opacity: .5
|
||||
}
|
||||
}
|
||||
|
||||
.opt {
|
||||
margin: .5em;
|
||||
}
|
||||
|
||||
.color-input {
|
||||
flex: 0 0 0;
|
||||
}
|
||||
|
||||
input, select {
|
||||
min-width: 3em;
|
||||
margin: 0;
|
||||
flex: 0;
|
||||
|
||||
&[type=number] {
|
||||
min-width: 5em;
|
||||
}
|
||||
|
||||
&[type=range] {
|
||||
flex: 1;
|
||||
min-width: 3em;
|
||||
align-self: flex-start;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.reset-container {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.fonts-container,
|
||||
.reset-container,
|
||||
.apply-container,
|
||||
.radius-container,
|
||||
.color-container,
|
||||
{
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.fonts-container,
|
||||
.radius-container {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.color-container{
|
||||
> h4 {
|
||||
width: 99%;
|
||||
}
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.fonts-container,
|
||||
.color-container,
|
||||
.shadow-container,
|
||||
.radius-container,
|
||||
.presets-container {
|
||||
margin: 1em 1em 0;
|
||||
}
|
||||
|
||||
.tab-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
width: 100%;
|
||||
min-height: 30px;
|
||||
margin-bottom: 1em;
|
||||
|
||||
p {
|
||||
flex: 1;
|
||||
margin: 0;
|
||||
margin-right: .5em;
|
||||
}
|
||||
}
|
||||
|
||||
.tab-header-buttons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.btn {
|
||||
min-width: 1px;
|
||||
flex: 0 auto;
|
||||
padding: 0 1em;
|
||||
margin-bottom: .5em;
|
||||
}
|
||||
}
|
||||
|
||||
.shadow-selector {
|
||||
.override {
|
||||
flex: 1;
|
||||
margin-left: .5em;
|
||||
}
|
||||
.select-container {
|
||||
margin-top: -4px;
|
||||
margin-bottom: -3px;
|
||||
}
|
||||
}
|
||||
|
||||
.save-load,
|
||||
.save-load-options {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: baseline;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.presets,
|
||||
.import-export {
|
||||
margin-bottom: .5em;
|
||||
}
|
||||
|
||||
.import-export {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.override {
|
||||
margin-left: .5em;
|
||||
}
|
||||
}
|
||||
|
||||
.save-load-options {
|
||||
flex-wrap: wrap;
|
||||
margin-top: .5em;
|
||||
justify-content: center;
|
||||
.keep-option {
|
||||
margin: 0 .5em .5em;
|
||||
min-width: 25%;
|
||||
}
|
||||
}
|
||||
|
||||
.preview-container {
|
||||
border-top: 1px dashed;
|
||||
border-bottom: 1px dashed;
|
||||
border-color: $fallback--border;
|
||||
border-color: var(--border, $fallback--border);
|
||||
margin: 1em 0;
|
||||
padding: 1em;
|
||||
background: var(--body-background-image);
|
||||
background-size: cover;
|
||||
background-position: 50% 50%;
|
||||
|
||||
.dummy {
|
||||
.post {
|
||||
font-family: var(--postFont);
|
||||
display: flex;
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
|
||||
h4 {
|
||||
margin-bottom: .25em;
|
||||
}
|
||||
|
||||
.icons {
|
||||
margin-top: .5em;
|
||||
display: flex;
|
||||
|
||||
i {
|
||||
margin-right: 1em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.after-post {
|
||||
margin-top: 1em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.avatar, .avatar-alt{
|
||||
background: linear-gradient(135deg, #b8e1fc 0%,#a9d2f3 10%,#90bae4 25%,#90bcea 37%,#90bff0 50%,#6ba8e5 51%,#a2daf5 83%,#bdf3fd 100%);
|
||||
color: black;
|
||||
font-family: sans-serif;
|
||||
text-align: center;
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
.avatar-alt {
|
||||
flex: 0 auto;
|
||||
margin-left: 28px;
|
||||
font-size: 12px;
|
||||
min-width: 20px;
|
||||
min-height: 20px;
|
||||
line-height: 20px;
|
||||
border-radius: $fallback--avatarAltRadius;
|
||||
border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);
|
||||
}
|
||||
|
||||
.avatar {
|
||||
flex: 0 auto;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
font-size: 14px;
|
||||
line-height: 48px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
|
||||
.checkbox {
|
||||
display: inline-flex;
|
||||
align-items: baseline;
|
||||
margin-right: 1em;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.separator {
|
||||
margin: 1em;
|
||||
border-bottom: 1px solid;
|
||||
border-color: $fallback--border;
|
||||
border-color: var(--border, $fallback--border);
|
||||
}
|
||||
|
||||
.panel-heading {
|
||||
.badge, .alert, .btn, .faint {
|
||||
margin-left: 1em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.faint {
|
||||
text-overflow: ellipsis;
|
||||
min-width: 2em;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
.flex-spacer {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
.btn {
|
||||
margin-left: 0;
|
||||
padding: 0 1em;
|
||||
min-width: 3em;
|
||||
min-height: 30px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.apply-container {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.radius-item,
|
||||
.color-item {
|
||||
min-width: 20em;
|
||||
margin: 5px 6px 0 0;
|
||||
display:flex;
|
||||
flex-direction: column;
|
||||
flex: 1 1 0;
|
||||
|
||||
&.wide {
|
||||
min-width: 60%
|
||||
}
|
||||
|
||||
&:not(.wide):nth-child(2n+1) {
|
||||
margin-right: 7px;
|
||||
|
||||
}
|
||||
|
||||
.color, .opacity {
|
||||
display:flex;
|
||||
align-items: baseline;
|
||||
}
|
||||
}
|
||||
|
||||
.radius-item {
|
||||
flex-basis: auto;
|
||||
}
|
||||
|
||||
.theme-radius-rn,
|
||||
.theme-color-cl {
|
||||
border: 0;
|
||||
box-shadow: none;
|
||||
background: transparent;
|
||||
color: var(--faint, $fallback--faint);
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
.theme-color-cl,
|
||||
.theme-radius-in,
|
||||
.theme-color-in {
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.theme-radius-in {
|
||||
min-width: 1em;
|
||||
}
|
||||
|
||||
.theme-radius-in {
|
||||
max-width: 7em;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.theme-radius-lb{
|
||||
max-width: 50em;
|
||||
}
|
||||
|
||||
.theme-preview-content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.apply-container {
|
||||
.btn {
|
||||
min-height: 28px;
|
||||
min-width: 10em;
|
||||
padding: 0 2em;
|
||||
}
|
||||
}
|
||||
|
||||
.btn {
|
||||
margin-left: .25em;
|
||||
margin-right: .25em;
|
||||
}
|
||||
}
|
965
src/components/settings_modal/tabs/theme_tab/theme_tab.vue
Normal file
965
src/components/settings_modal/tabs/theme_tab/theme_tab.vue
Normal file
|
@ -0,0 +1,965 @@
|
|||
<template>
|
||||
<div class="theme-tab">
|
||||
<div class="presets-container">
|
||||
<div class="save-load">
|
||||
<div
|
||||
v-if="themeWarning"
|
||||
class="theme-warning"
|
||||
>
|
||||
<div class="alert warning">
|
||||
{{ themeWarningHelp }}
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<template v-if="themeWarning.type === 'snapshot_source_mismatch'">
|
||||
<button
|
||||
class="btn"
|
||||
@click="forceLoad"
|
||||
>
|
||||
{{ $t('settings.style.switcher.use_source') }}
|
||||
</button>
|
||||
<button
|
||||
class="btn"
|
||||
@click="forceSnapshot"
|
||||
>
|
||||
{{ $t('settings.style.switcher.use_snapshot') }}
|
||||
</button>
|
||||
</template>
|
||||
<template v-else-if="themeWarning.noActionsPossible">
|
||||
<button
|
||||
class="btn"
|
||||
@click="dismissWarning"
|
||||
>
|
||||
{{ $t('general.dismiss') }}
|
||||
</button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<button
|
||||
class="btn"
|
||||
@click="forceLoad"
|
||||
>
|
||||
{{ $t('settings.style.switcher.load_theme') }}
|
||||
</button>
|
||||
<button
|
||||
class="btn"
|
||||
@click="dismissWarning"
|
||||
>
|
||||
{{ $t('settings.style.switcher.keep_as_is') }}
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<ExportImport
|
||||
:export-object="exportedTheme"
|
||||
:export-label="$t("settings.export_theme")"
|
||||
:import-label="$t("settings.import_theme")"
|
||||
:import-failed-text="$t("settings.invalid_theme_imported")"
|
||||
:on-import="onImport"
|
||||
:validator="importValidator"
|
||||
>
|
||||
<template slot="before">
|
||||
<div class="presets">
|
||||
{{ $t('settings.presets') }}
|
||||
<label
|
||||
for="preset-switcher"
|
||||
class="select"
|
||||
>
|
||||
<select
|
||||
id="preset-switcher"
|
||||
v-model="selected"
|
||||
class="preset-switcher"
|
||||
>
|
||||
<option
|
||||
v-for="style in availableStyles"
|
||||
:key="style.name"
|
||||
:value="style"
|
||||
:style="{
|
||||
backgroundColor: style[1] || (style.theme || style.source).colors.bg,
|
||||
color: style[3] || (style.theme || style.source).colors.text
|
||||
}"
|
||||
>
|
||||
{{ style[0] || style.name }}
|
||||
</option>
|
||||
</select>
|
||||
<i class="icon-down-open" />
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
</ExportImport>
|
||||
</div>
|
||||
<div class="save-load-options">
|
||||
<span class="keep-option">
|
||||
<Checkbox v-model="keepColor">
|
||||
{{ $t('settings.style.switcher.keep_color') }}
|
||||
</Checkbox>
|
||||
</span>
|
||||
<span class="keep-option">
|
||||
<Checkbox v-model="keepShadows">
|
||||
{{ $t('settings.style.switcher.keep_shadows') }}
|
||||
</Checkbox>
|
||||
</span>
|
||||
<span class="keep-option">
|
||||
<Checkbox v-model="keepOpacity">
|
||||
{{ $t('settings.style.switcher.keep_opacity') }}
|
||||
</Checkbox>
|
||||
</span>
|
||||
<span class="keep-option">
|
||||
<Checkbox v-model="keepRoundness">
|
||||
{{ $t('settings.style.switcher.keep_roundness') }}
|
||||
</Checkbox>
|
||||
</span>
|
||||
<span class="keep-option">
|
||||
<Checkbox v-model="keepFonts">
|
||||
{{ $t('settings.style.switcher.keep_fonts') }}
|
||||
</Checkbox>
|
||||
</span>
|
||||
<p>{{ $t('settings.style.switcher.save_load_hint') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<preview :style="previewRules" />
|
||||
|
||||
<keep-alive>
|
||||
<tab-switcher key="style-tweak">
|
||||
<div
|
||||
:label="$t('settings.style.common_colors._tab_label')"
|
||||
class="color-container"
|
||||
>
|
||||
<div class="tab-header">
|
||||
<p>{{ $t('settings.theme_help') }}</p>
|
||||
<div class="tab-header-buttons">
|
||||
<button
|
||||
class="btn"
|
||||
@click="clearOpacity"
|
||||
>
|
||||
{{ $t('settings.style.switcher.clear_opacity') }}
|
||||
</button>
|
||||
<button
|
||||
class="btn"
|
||||
@click="clearV1"
|
||||
>
|
||||
{{ $t('settings.style.switcher.clear_all') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p>{{ $t('settings.theme_help_v2_1') }}</p>
|
||||
<h4>{{ $t('settings.style.common_colors.main') }}</h4>
|
||||
<div class="color-item">
|
||||
<ColorInput
|
||||
v-model="bgColorLocal"
|
||||
name="bgColor"
|
||||
:label="$t('settings.background')"
|
||||
/>
|
||||
<OpacityInput
|
||||
v-model="bgOpacityLocal"
|
||||
name="bgOpacity"
|
||||
:fallback="previewTheme.opacity.bg"
|
||||
/>
|
||||
<ColorInput
|
||||
v-model="textColorLocal"
|
||||
name="textColor"
|
||||
:label="$t('settings.text')"
|
||||
/>
|
||||
<ContrastRatio :contrast="previewContrast.bgText" />
|
||||
<ColorInput
|
||||
v-model="accentColorLocal"
|
||||
name="accentColor"
|
||||
:fallback="previewTheme.colors.link"
|
||||
:label="$t('settings.accent')"
|
||||
:show-optional-tickbox="typeof linkColorLocal !== 'undefined'"
|
||||
/>
|
||||
<ColorInput
|
||||
v-model="linkColorLocal"
|
||||
name="linkColor"
|
||||
:fallback="previewTheme.colors.accent"
|
||||
:label="$t('settings.links')"
|
||||
:show-optional-tickbox="typeof accentColorLocal !== 'undefined'"
|
||||
/>
|
||||
<ContrastRatio :contrast="previewContrast.bgLink" />
|
||||
</div>
|
||||
<div class="color-item">
|
||||
<ColorInput
|
||||
v-model="fgColorLocal"
|
||||
name="fgColor"
|
||||
:label="$t('settings.foreground')"
|
||||
/>
|
||||
<ColorInput
|
||||
v-model="fgTextColorLocal"
|
||||
name="fgTextColor"
|
||||
:label="$t('settings.text')"
|
||||
:fallback="previewTheme.colors.fgText"
|
||||
/>
|
||||
<ColorInput
|
||||
v-model="fgLinkColorLocal"
|
||||
name="fgLinkColor"
|
||||
:label="$t('settings.links')"
|
||||
:fallback="previewTheme.colors.fgLink"
|
||||
/>
|
||||
<p>{{ $t('settings.style.common_colors.foreground_hint') }}</p>
|
||||
</div>
|
||||
<h4>{{ $t('settings.style.common_colors.rgbo') }}</h4>
|
||||
<div class="color-item">
|
||||
<ColorInput
|
||||
v-model="cRedColorLocal"
|
||||
name="cRedColor"
|
||||
:label="$t('settings.cRed')"
|
||||
/>
|
||||
<ContrastRatio :contrast="previewContrast.bgCRed" />
|
||||
<ColorInput
|
||||
v-model="cBlueColorLocal"
|
||||
name="cBlueColor"
|
||||
:label="$t('settings.cBlue')"
|
||||
/>
|
||||
<ContrastRatio :contrast="previewContrast.bgCBlue" />
|
||||
</div>
|
||||
<div class="color-item">
|
||||
<ColorInput
|
||||
v-model="cGreenColorLocal"
|
||||
name="cGreenColor"
|
||||
:label="$t('settings.cGreen')"
|
||||
/>
|
||||
<ContrastRatio :contrast="previewContrast.bgCGreen" />
|
||||
<ColorInput
|
||||
v-model="cOrangeColorLocal"
|
||||
name="cOrangeColor"
|
||||
:label="$t('settings.cOrange')"
|
||||
/>
|
||||
<ContrastRatio :contrast="previewContrast.bgCOrange" />
|
||||
</div>
|
||||
<p>{{ $t('settings.theme_help_v2_2') }}</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
:label="$t('settings.style.advanced_colors._tab_label')"
|
||||
class="color-container"
|
||||
>
|
||||
<div class="tab-header">
|
||||
<p>{{ $t('settings.theme_help') }}</p>
|
||||
<button
|
||||
class="btn"
|
||||
@click="clearOpacity"
|
||||
>
|
||||
{{ $t('settings.style.switcher.clear_opacity') }}
|
||||
</button>
|
||||
<button
|
||||
class="btn"
|
||||
@click="clearV1"
|
||||
>
|
||||
{{ $t('settings.style.switcher.clear_all') }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="color-item">
|
||||
<h4>{{ $t('settings.style.advanced_colors.post') }}</h4>
|
||||
<ColorInput
|
||||
v-model="postLinkColorLocal"
|
||||
name="postLinkColor"
|
||||
:fallback="previewTheme.colors.accent"
|
||||
:label="$t('settings.links')"
|
||||
/>
|
||||
<ContrastRatio :contrast="previewContrast.postLink" />
|
||||
<ColorInput
|
||||
v-model="postGreentextColorLocal"
|
||||
name="postGreentextColor"
|
||||
:fallback="previewTheme.colors.cGreen"
|
||||
:label="$t('settings.greentext')"
|
||||
/>
|
||||
<ContrastRatio :contrast="previewContrast.postGreentext" />
|
||||
<h4>{{ $t('settings.style.advanced_colors.alert') }}</h4>
|
||||
<ColorInput
|
||||
v-model="alertErrorColorLocal"
|
||||
name="alertError"
|
||||
:label="$t('settings.style.advanced_colors.alert_error')"
|
||||
:fallback="previewTheme.colors.alertError"
|
||||
/>
|
||||
<ColorInput
|
||||
v-model="alertErrorTextColorLocal"
|
||||
name="alertErrorText"
|
||||
:label="$t('settings.text')"
|
||||
:fallback="previewTheme.colors.alertErrorText"
|
||||
/>
|
||||
<ContrastRatio
|
||||
:contrast="previewContrast.alertErrorText"
|
||||
large="true"
|
||||
/>
|
||||
<ColorInput
|
||||
v-model="alertWarningColorLocal"
|
||||
name="alertWarning"
|
||||
:label="$t('settings.style.advanced_colors.alert_warning')"
|
||||
:fallback="previewTheme.colors.alertWarning"
|
||||
/>
|
||||
<ColorInput
|
||||
v-model="alertWarningTextColorLocal"
|
||||
name="alertWarningText"
|
||||
:label="$t('settings.text')"
|
||||
:fallback="previewTheme.colors.alertWarningText"
|
||||
/>
|
||||
<ContrastRatio
|
||||
:contrast="previewContrast.alertWarningText"
|
||||
large="true"
|
||||
/>
|
||||
<ColorInput
|
||||
v-model="alertNeutralColorLocal"
|
||||
name="alertNeutral"
|
||||
:label="$t('settings.style.advanced_colors.alert_neutral')"
|
||||
:fallback="previewTheme.colors.alertNeutral"
|
||||
/>
|
||||
<ColorInput
|
||||
v-model="alertNeutralTextColorLocal"
|
||||
name="alertNeutralText"
|
||||
:label="$t('settings.text')"
|
||||
:fallback="previewTheme.colors.alertNeutralText"
|
||||
/>
|
||||
<ContrastRatio
|
||||
:contrast="previewContrast.alertNeutralText"
|
||||
large="true"
|
||||
/>
|
||||
<OpacityInput
|
||||
v-model="alertOpacityLocal"
|
||||
name="alertOpacity"
|
||||
:fallback="previewTheme.opacity.alert"
|
||||
/>
|
||||
</div>
|
||||
<div class="color-item">
|
||||
<h4>{{ $t('settings.style.advanced_colors.badge') }}</h4>
|
||||
<ColorInput
|
||||
v-model="badgeNotificationColorLocal"
|
||||
name="badgeNotification"
|
||||
:label="$t('settings.style.advanced_colors.badge_notification')"
|
||||
:fallback="previewTheme.colors.badgeNotification"
|
||||
/>
|
||||
<ColorInput
|
||||
v-model="badgeNotificationTextColorLocal"
|
||||
name="badgeNotificationText"
|
||||
:label="$t('settings.text')"
|
||||
:fallback="previewTheme.colors.badgeNotificationText"
|
||||
/>
|
||||
<ContrastRatio
|
||||
:contrast="previewContrast.badgeNotificationText"
|
||||
large="true"
|
||||
/>
|
||||
</div>
|
||||
<div class="color-item">
|
||||
<h4>{{ $t('settings.style.advanced_colors.panel_header') }}</h4>
|
||||
<ColorInput
|
||||
v-model="panelColorLocal"
|
||||
name="panelColor"
|
||||
:fallback="previewTheme.colors.panel"
|
||||
:label="$t('settings.background')"
|
||||
/>
|
||||
<OpacityInput
|
||||
v-model="panelOpacityLocal"
|
||||
name="panelOpacity"
|
||||
:fallback="previewTheme.opacity.panel"
|
||||
:disabled="panelColorLocal === 'transparent'"
|
||||
/>
|
||||
<ColorInput
|
||||
v-model="panelTextColorLocal"
|
||||
name="panelTextColor"
|
||||
:fallback="previewTheme.colors.panelText"
|
||||
:label="$t('settings.text')"
|
||||
/>
|
||||
<ContrastRatio
|
||||
:contrast="previewContrast.panelText"
|
||||
large="true"
|
||||
/>
|
||||
<ColorInput
|
||||
v-model="panelLinkColorLocal"
|
||||
name="panelLinkColor"
|
||||
:fallback="previewTheme.colors.panelLink"
|
||||
:label="$t('settings.links')"
|
||||
/>
|
||||
<ContrastRatio
|
||||
:contrast="previewContrast.panelLink"
|
||||
large="true"
|
||||
/>
|
||||
</div>
|
||||
<div class="color-item">
|
||||
<h4>{{ $t('settings.style.advanced_colors.top_bar') }}</h4>
|
||||
<ColorInput
|
||||
v-model="topBarColorLocal"
|
||||
name="topBarColor"
|
||||
:fallback="previewTheme.colors.topBar"
|
||||
:label="$t('settings.background')"
|
||||
/>
|
||||
<ColorInput
|
||||
v-model="topBarTextColorLocal"
|
||||
name="topBarTextColor"
|
||||
:fallback="previewTheme.colors.topBarText"
|
||||
:label="$t('settings.text')"
|
||||
/>
|
||||
<ContrastRatio :contrast="previewContrast.topBarText" />
|
||||
<ColorInput
|
||||
v-model="topBarLinkColorLocal"
|
||||
name="topBarLinkColor"
|
||||
:fallback="previewTheme.colors.topBarLink"
|
||||
:label="$t('settings.links')"
|
||||
/>
|
||||
<ContrastRatio :contrast="previewContrast.topBarLink" />
|
||||
</div>
|
||||
<div class="color-item">
|
||||
<h4>{{ $t('settings.style.advanced_colors.inputs') }}</h4>
|
||||
<ColorInput
|
||||
v-model="inputColorLocal"
|
||||
name="inputColor"
|
||||
:fallback="previewTheme.colors.input"
|
||||
:label="$t('settings.background')"
|
||||
/>
|
||||
<OpacityInput
|
||||
v-model="inputOpacityLocal"
|
||||
name="inputOpacity"
|
||||
:fallback="previewTheme.opacity.input"
|
||||
:disabled="inputColorLocal === 'transparent'"
|
||||
/>
|
||||
<ColorInput
|
||||
v-model="inputTextColorLocal"
|
||||
name="inputTextColor"
|
||||
:fallback="previewTheme.colors.inputText"
|
||||
:label="$t('settings.text')"
|
||||
/>
|
||||
<ContrastRatio :contrast="previewContrast.inputText" />
|
||||
</div>
|
||||
<div class="color-item">
|
||||
<h4>{{ $t('settings.style.advanced_colors.buttons') }}</h4>
|
||||
<ColorInput
|
||||
v-model="btnColorLocal"
|
||||
name="btnColor"
|
||||
:fallback="previewTheme.colors.btn"
|
||||
:label="$t('settings.background')"
|
||||
/>
|
||||
<OpacityInput
|
||||
v-model="btnOpacityLocal"
|
||||
name="btnOpacity"
|
||||
:fallback="previewTheme.opacity.btn"
|
||||
:disabled="btnColorLocal === 'transparent'"
|
||||
/>
|
||||
<ColorInput
|
||||
v-model="btnTextColorLocal"
|
||||
name="btnTextColor"
|
||||
:fallback="previewTheme.colors.btnText"
|
||||
:label="$t('settings.text')"
|
||||
/>
|
||||
<ContrastRatio :contrast="previewContrast.btnText" />
|
||||
<ColorInput
|
||||
v-model="btnPanelTextColorLocal"
|
||||
name="btnPanelTextColor"
|
||||
:fallback="previewTheme.colors.btnPanelText"
|
||||
:label="$t('settings.style.advanced_colors.panel_header')"
|
||||
/>
|
||||
<ContrastRatio :contrast="previewContrast.btnPanelText" />
|
||||
<ColorInput
|
||||
v-model="btnTopBarTextColorLocal"
|
||||
name="btnTopBarTextColor"
|
||||
:fallback="previewTheme.colors.btnTopBarText"
|
||||
:label="$t('settings.style.advanced_colors.top_bar')"
|
||||
/>
|
||||
<ContrastRatio :contrast="previewContrast.btnTopBarText" />
|
||||
<h5>{{ $t('settings.style.advanced_colors.pressed') }}</h5>
|
||||
<ColorInput
|
||||
v-model="btnPressedColorLocal"
|
||||
name="btnPressedColor"
|
||||
:fallback="previewTheme.colors.btnPressed"
|
||||
:label="$t('settings.background')"
|
||||
/>
|
||||
<ColorInput
|
||||
v-model="btnPressedTextColorLocal"
|
||||
name="btnPressedTextColor"
|
||||
:fallback="previewTheme.colors.btnPressedText"
|
||||
:label="$t('settings.text')"
|
||||
/>
|
||||
<ContrastRatio :contrast="previewContrast.btnPressedText" />
|
||||
<ColorInput
|
||||
v-model="btnPressedPanelTextColorLocal"
|
||||
name="btnPressedPanelTextColor"
|
||||
:fallback="previewTheme.colors.btnPressedPanelText"
|
||||
:label="$t('settings.style.advanced_colors.panel_header')"
|
||||
/>
|
||||
<ContrastRatio :contrast="previewContrast.btnPressedPanelText" />
|
||||
<ColorInput
|
||||
v-model="btnPressedTopBarTextColorLocal"
|
||||
name="btnPressedTopBarTextColor"
|
||||
:fallback="previewTheme.colors.btnPressedTopBarText"
|
||||
:label="$t('settings.style.advanced_colors.top_bar')"
|
||||
/>
|
||||
<ContrastRatio :contrast="previewContrast.btnPressedTopBarText" />
|
||||
<h5>{{ $t('settings.style.advanced_colors.disabled') }}</h5>
|
||||
<ColorInput
|
||||
v-model="btnDisabledColorLocal"
|
||||
name="btnDisabledColor"
|
||||
:fallback="previewTheme.colors.btnDisabled"
|
||||
:label="$t('settings.background')"
|
||||
/>
|
||||
<ColorInput
|
||||
v-model="btnDisabledTextColorLocal"
|
||||
name="btnDisabledTextColor"
|
||||
:fallback="previewTheme.colors.btnDisabledText"
|
||||
:label="$t('settings.text')"
|
||||
/>
|
||||
<ColorInput
|
||||
v-model="btnDisabledPanelTextColorLocal"
|
||||
name="btnDisabledPanelTextColor"
|
||||
:fallback="previewTheme.colors.btnDisabledPanelText"
|
||||
:label="$t('settings.style.advanced_colors.panel_header')"
|
||||
/>
|
||||
<ColorInput
|
||||
v-model="btnDisabledTopBarTextColorLocal"
|
||||
name="btnDisabledTopBarTextColor"
|
||||
:fallback="previewTheme.colors.btnDisabledTopBarText"
|
||||
:label="$t('settings.style.advanced_colors.top_bar')"
|
||||
/>
|
||||
<h5>{{ $t('settings.style.advanced_colors.toggled') }}</h5>
|
||||
<ColorInput
|
||||
v-model="btnToggledColorLocal"
|
||||
name="btnToggledColor"
|
||||
:fallback="previewTheme.colors.btnToggled"
|
||||
:label="$t('settings.background')"
|
||||
/>
|
||||
<ColorInput
|
||||
v-model="btnToggledTextColorLocal"
|
||||
name="btnToggledTextColor"
|
||||
:fallback="previewTheme.colors.btnToggledText"
|
||||
:label="$t('settings.text')"
|
||||
/>
|
||||
<ContrastRatio :contrast="previewContrast.btnToggledText" />
|
||||
<ColorInput
|
||||
v-model="btnToggledPanelTextColorLocal"
|
||||
name="btnToggledPanelTextColor"
|
||||
:fallback="previewTheme.colors.btnToggledPanelText"
|
||||
:label="$t('settings.style.advanced_colors.panel_header')"
|
||||
/>
|
||||
<ContrastRatio :contrast="previewContrast.btnToggledPanelText" />
|
||||
<ColorInput
|
||||
v-model="btnToggledTopBarTextColorLocal"
|
||||
name="btnToggledTopBarTextColor"
|
||||
:fallback="previewTheme.colors.btnToggledTopBarText"
|
||||
:label="$t('settings.style.advanced_colors.top_bar')"
|
||||
/>
|
||||
<ContrastRatio :contrast="previewContrast.btnToggledTopBarText" />
|
||||
</div>
|
||||
<div class="color-item">
|
||||
<h4>{{ $t('settings.style.advanced_colors.tabs') }}</h4>
|
||||
<ColorInput
|
||||
v-model="tabColorLocal"
|
||||
name="tabColor"
|
||||
:fallback="previewTheme.colors.tab"
|
||||
:label="$t('settings.background')"
|
||||
/>
|
||||
<ColorInput
|
||||
v-model="tabTextColorLocal"
|
||||
name="tabTextColor"
|
||||
:fallback="previewTheme.colors.tabText"
|
||||
:label="$t('settings.text')"
|
||||
/>
|
||||
<ContrastRatio :contrast="previewContrast.tabText" />
|
||||
<ColorInput
|
||||
v-model="tabActiveTextColorLocal"
|
||||
name="tabActiveTextColor"
|
||||
:fallback="previewTheme.colors.tabActiveText"
|
||||
:label="$t('settings.text')"
|
||||
/>
|
||||
<ContrastRatio :contrast="previewContrast.tabActiveText" />
|
||||
</div>
|
||||
<div class="color-item">
|
||||
<h4>{{ $t('settings.style.advanced_colors.borders') }}</h4>
|
||||
<ColorInput
|
||||
v-model="borderColorLocal"
|
||||
name="borderColor"
|
||||
:fallback="previewTheme.colors.border"
|
||||
:label="$t('settings.style.common.color')"
|
||||
/>
|
||||
<OpacityInput
|
||||
v-model="borderOpacityLocal"
|
||||
name="borderOpacity"
|
||||
:fallback="previewTheme.opacity.border"
|
||||
:disabled="borderColorLocal === 'transparent'"
|
||||
/>
|
||||
</div>
|
||||
<div class="color-item">
|
||||
<h4>{{ $t('settings.style.advanced_colors.faint_text') }}</h4>
|
||||
<ColorInput
|
||||
v-model="faintColorLocal"
|
||||
name="faintColor"
|
||||
:fallback="previewTheme.colors.faint"
|
||||
:label="$t('settings.text')"
|
||||
/>
|
||||
<ColorInput
|
||||
v-model="faintLinkColorLocal"
|
||||
name="faintLinkColor"
|
||||
:fallback="previewTheme.colors.faintLink"
|
||||
:label="$t('settings.links')"
|
||||
/>
|
||||
<ColorInput
|
||||
v-model="panelFaintColorLocal"
|
||||
name="panelFaintColor"
|
||||
:fallback="previewTheme.colors.panelFaint"
|
||||
:label="$t('settings.style.advanced_colors.panel_header')"
|
||||
/>
|
||||
<OpacityInput
|
||||
v-model="faintOpacityLocal"
|
||||
name="faintOpacity"
|
||||
:fallback="previewTheme.opacity.faint"
|
||||
/>
|
||||
</div>
|
||||
<div class="color-item">
|
||||
<h4>{{ $t('settings.style.advanced_colors.underlay') }}</h4>
|
||||
<ColorInput
|
||||
v-model="underlayColorLocal"
|
||||
name="underlay"
|
||||
:label="$t('settings.style.advanced_colors.underlay')"
|
||||
:fallback="previewTheme.colors.underlay"
|
||||
/>
|
||||
<OpacityInput
|
||||
v-model="underlayOpacityLocal"
|
||||
name="underlayOpacity"
|
||||
:fallback="previewTheme.opacity.underlay"
|
||||
:disabled="underlayOpacityLocal === 'transparent'"
|
||||
/>
|
||||
</div>
|
||||
<div class="color-item">
|
||||
<h4>{{ $t('settings.style.advanced_colors.poll') }}</h4>
|
||||
<ColorInput
|
||||
v-model="pollColorLocal"
|
||||
name="poll"
|
||||
:label="$t('settings.background')"
|
||||
:fallback="previewTheme.colors.poll"
|
||||
/>
|
||||
<ColorInput
|
||||
v-model="pollTextColorLocal"
|
||||
name="pollText"
|
||||
:label="$t('settings.text')"
|
||||
:fallback="previewTheme.colors.pollText"
|
||||
/>
|
||||
</div>
|
||||
<div class="color-item">
|
||||
<h4>{{ $t('settings.style.advanced_colors.icons') }}</h4>
|
||||
<ColorInput
|
||||
v-model="iconColorLocal"
|
||||
name="icon"
|
||||
:label="$t('settings.style.advanced_colors.icons')"
|
||||
:fallback="previewTheme.colors.icon"
|
||||
/>
|
||||
</div>
|
||||
<div class="color-item">
|
||||
<h4>{{ $t('settings.style.advanced_colors.highlight') }}</h4>
|
||||
<ColorInput
|
||||
v-model="highlightColorLocal"
|
||||
name="highlight"
|
||||
:label="$t('settings.background')"
|
||||
:fallback="previewTheme.colors.highlight"
|
||||
/>
|
||||
<ColorInput
|
||||
v-model="highlightTextColorLocal"
|
||||
name="highlightText"
|
||||
:label="$t('settings.text')"
|
||||
:fallback="previewTheme.colors.highlightText"
|
||||
/>
|
||||
<ContrastRatio :contrast="previewContrast.highlightText" />
|
||||
<ColorInput
|
||||
v-model="highlightLinkColorLocal"
|
||||
name="highlightLink"
|
||||
:label="$t('settings.links')"
|
||||
:fallback="previewTheme.colors.highlightLink"
|
||||
/>
|
||||
<ContrastRatio :contrast="previewContrast.highlightLink" />
|
||||
</div>
|
||||
<div class="color-item">
|
||||
<h4>{{ $t('settings.style.advanced_colors.popover') }}</h4>
|
||||
<ColorInput
|
||||
v-model="popoverColorLocal"
|
||||
name="popover"
|
||||
:label="$t('settings.background')"
|
||||
:fallback="previewTheme.colors.popover"
|
||||
/>
|
||||
<OpacityInput
|
||||
v-model="popoverOpacityLocal"
|
||||
name="popoverOpacity"
|
||||
:fallback="previewTheme.opacity.popover"
|
||||
:disabled="popoverOpacityLocal === 'transparent'"
|
||||
/>
|
||||
<ColorInput
|
||||
v-model="popoverTextColorLocal"
|
||||
name="popoverText"
|
||||
:label="$t('settings.text')"
|
||||
:fallback="previewTheme.colors.popoverText"
|
||||
/>
|
||||
<ContrastRatio :contrast="previewContrast.popoverText" />
|
||||
<ColorInput
|
||||
v-model="popoverLinkColorLocal"
|
||||
name="popoverLink"
|
||||
:label="$t('settings.links')"
|
||||
:fallback="previewTheme.colors.popoverLink"
|
||||
/>
|
||||
<ContrastRatio :contrast="previewContrast.popoverLink" />
|
||||
</div>
|
||||
<div class="color-item">
|
||||
<h4>{{ $t('settings.style.advanced_colors.selectedPost') }}</h4>
|
||||
<ColorInput
|
||||
v-model="selectedPostColorLocal"
|
||||
name="selectedPost"
|
||||
:label="$t('settings.background')"
|
||||
:fallback="previewTheme.colors.selectedPost"
|
||||
/>
|
||||
<ColorInput
|
||||
v-model="selectedPostTextColorLocal"
|
||||
name="selectedPostText"
|
||||
:label="$t('settings.text')"
|
||||
:fallback="previewTheme.colors.selectedPostText"
|
||||
/>
|
||||
<ContrastRatio :contrast="previewContrast.selectedPostText" />
|
||||
<ColorInput
|
||||
v-model="selectedPostLinkColorLocal"
|
||||
name="selectedPostLink"
|
||||
:label="$t('settings.links')"
|
||||
:fallback="previewTheme.colors.selectedPostLink"
|
||||
/>
|
||||
<ContrastRatio :contrast="previewContrast.selectedPostLink" />
|
||||
</div>
|
||||
<div class="color-item">
|
||||
<h4>{{ $t('settings.style.advanced_colors.selectedMenu') }}</h4>
|
||||
<ColorInput
|
||||
v-model="selectedMenuColorLocal"
|
||||
name="selectedMenu"
|
||||
:label="$t('settings.background')"
|
||||
:fallback="previewTheme.colors.selectedMenu"
|
||||
/>
|
||||
<ColorInput
|
||||
v-model="selectedMenuTextColorLocal"
|
||||
name="selectedMenuText"
|
||||
:label="$t('settings.text')"
|
||||
:fallback="previewTheme.colors.selectedMenuText"
|
||||
/>
|
||||
<ContrastRatio :contrast="previewContrast.selectedMenuText" />
|
||||
<ColorInput
|
||||
v-model="selectedMenuLinkColorLocal"
|
||||
name="selectedMenuLink"
|
||||
:label="$t('settings.links')"
|
||||
:fallback="previewTheme.colors.selectedMenuLink"
|
||||
/>
|
||||
<ContrastRatio :contrast="previewContrast.selectedMenuLink" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
:label="$t('settings.style.radii._tab_label')"
|
||||
class="radius-container"
|
||||
>
|
||||
<div class="tab-header">
|
||||
<p>{{ $t('settings.radii_help') }}</p>
|
||||
<button
|
||||
class="btn"
|
||||
@click="clearRoundness"
|
||||
>
|
||||
{{ $t('settings.style.switcher.clear_all') }}
|
||||
</button>
|
||||
</div>
|
||||
<RangeInput
|
||||
v-model="btnRadiusLocal"
|
||||
name="btnRadius"
|
||||
:label="$t('settings.btnRadius')"
|
||||
:fallback="previewTheme.radii.btn"
|
||||
max="16"
|
||||
hard-min="0"
|
||||
/>
|
||||
<RangeInput
|
||||
v-model="inputRadiusLocal"
|
||||
name="inputRadius"
|
||||
:label="$t('settings.inputRadius')"
|
||||
:fallback="previewTheme.radii.input"
|
||||
max="9"
|
||||
hard-min="0"
|
||||
/>
|
||||
<RangeInput
|
||||
v-model="checkboxRadiusLocal"
|
||||
name="checkboxRadius"
|
||||
:label="$t('settings.checkboxRadius')"
|
||||
:fallback="previewTheme.radii.checkbox"
|
||||
max="16"
|
||||
hard-min="0"
|
||||
/>
|
||||
<RangeInput
|
||||
v-model="panelRadiusLocal"
|
||||
name="panelRadius"
|
||||
:label="$t('settings.panelRadius')"
|
||||
:fallback="previewTheme.radii.panel"
|
||||
max="50"
|
||||
hard-min="0"
|
||||
/>
|
||||
<RangeInput
|
||||
v-model="avatarRadiusLocal"
|
||||
name="avatarRadius"
|
||||
:label="$t('settings.avatarRadius')"
|
||||
:fallback="previewTheme.radii.avatar"
|
||||
max="28"
|
||||
hard-min="0"
|
||||
/>
|
||||
<RangeInput
|
||||
v-model="avatarAltRadiusLocal"
|
||||
name="avatarAltRadius"
|
||||
:label="$t('settings.avatarAltRadius')"
|
||||
:fallback="previewTheme.radii.avatarAlt"
|
||||
max="28"
|
||||
hard-min="0"
|
||||
/>
|
||||
<RangeInput
|
||||
v-model="attachmentRadiusLocal"
|
||||
name="attachmentRadius"
|
||||
:label="$t('settings.attachmentRadius')"
|
||||
:fallback="previewTheme.radii.attachment"
|
||||
max="50"
|
||||
hard-min="0"
|
||||
/>
|
||||
<RangeInput
|
||||
v-model="tooltipRadiusLocal"
|
||||
name="tooltipRadius"
|
||||
:label="$t('settings.tooltipRadius')"
|
||||
:fallback="previewTheme.radii.tooltip"
|
||||
max="50"
|
||||
hard-min="0"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
:label="$t('settings.style.shadows._tab_label')"
|
||||
class="shadow-container"
|
||||
>
|
||||
<div class="tab-header shadow-selector">
|
||||
<div class="select-container">
|
||||
{{ $t('settings.style.shadows.component') }}
|
||||
<label
|
||||
for="shadow-switcher"
|
||||
class="select"
|
||||
>
|
||||
<select
|
||||
id="shadow-switcher"
|
||||
v-model="shadowSelected"
|
||||
class="shadow-switcher"
|
||||
>
|
||||
<option
|
||||
v-for="shadow in shadowsAvailable"
|
||||
:key="shadow"
|
||||
:value="shadow"
|
||||
>
|
||||
{{ $t('settings.style.shadows.components.' + shadow) }}
|
||||
</option>
|
||||
</select>
|
||||
<i class="icon-down-open" />
|
||||
</label>
|
||||
</div>
|
||||
<div class="override">
|
||||
<label
|
||||
for="override"
|
||||
class="label"
|
||||
>
|
||||
{{ $t('settings.style.shadows.override') }}
|
||||
</label>
|
||||
<input
|
||||
id="override"
|
||||
v-model="currentShadowOverriden"
|
||||
name="override"
|
||||
class="input-override"
|
||||
type="checkbox"
|
||||
>
|
||||
<label
|
||||
class="checkbox-label"
|
||||
for="override"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
class="btn"
|
||||
@click="clearShadows"
|
||||
>
|
||||
{{ $t('settings.style.switcher.clear_all') }}
|
||||
</button>
|
||||
</div>
|
||||
<ShadowControl
|
||||
v-model="currentShadow"
|
||||
:ready="!!currentShadowFallback"
|
||||
:fallback="currentShadowFallback"
|
||||
/>
|
||||
<div v-if="shadowSelected === 'avatar' || shadowSelected === 'avatarStatus'">
|
||||
<i18n
|
||||
path="settings.style.shadows.filter_hint.always_drop_shadow"
|
||||
tag="p"
|
||||
>
|
||||
<code>filter: drop-shadow()</code>
|
||||
</i18n>
|
||||
<p>{{ $t('settings.style.shadows.filter_hint.avatar_inset') }}</p>
|
||||
<i18n
|
||||
path="settings.style.shadows.filter_hint.drop_shadow_syntax"
|
||||
tag="p"
|
||||
>
|
||||
<code>drop-shadow</code>
|
||||
<code>spread-radius</code>
|
||||
<code>inset</code>
|
||||
</i18n>
|
||||
<i18n
|
||||
path="settings.style.shadows.filter_hint.inset_classic"
|
||||
tag="p"
|
||||
>
|
||||
<code>box-shadow</code>
|
||||
</i18n>
|
||||
<p>{{ $t('settings.style.shadows.filter_hint.spread_zero') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
:label="$t('settings.style.fonts._tab_label')"
|
||||
class="fonts-container"
|
||||
>
|
||||
<div class="tab-header">
|
||||
<p>{{ $t('settings.style.fonts.help') }}</p>
|
||||
<button
|
||||
class="btn"
|
||||
@click="clearFonts"
|
||||
>
|
||||
{{ $t('settings.style.switcher.clear_all') }}
|
||||
</button>
|
||||
</div>
|
||||
<FontControl
|
||||
v-model="fontsLocal.interface"
|
||||
name="ui"
|
||||
:label="$t('settings.style.fonts.components.interface')"
|
||||
:fallback="previewTheme.fonts.interface"
|
||||
no-inherit="1"
|
||||
/>
|
||||
<FontControl
|
||||
v-model="fontsLocal.input"
|
||||
name="input"
|
||||
:label="$t('settings.style.fonts.components.input')"
|
||||
:fallback="previewTheme.fonts.input"
|
||||
/>
|
||||
<FontControl
|
||||
v-model="fontsLocal.post"
|
||||
name="post"
|
||||
:label="$t('settings.style.fonts.components.post')"
|
||||
:fallback="previewTheme.fonts.post"
|
||||
/>
|
||||
<FontControl
|
||||
v-model="fontsLocal.postCode"
|
||||
name="postCode"
|
||||
:label="$t('settings.style.fonts.components.postCode')"
|
||||
:fallback="previewTheme.fonts.postCode"
|
||||
/>
|
||||
</div>
|
||||
</tab-switcher>
|
||||
</keep-alive>
|
||||
|
||||
<div class="apply-container">
|
||||
<button
|
||||
class="btn submit"
|
||||
:disabled="!themeValid"
|
||||
@click="setCustomTheme"
|
||||
>
|
||||
{{ $t('general.apply') }}
|
||||
</button>
|
||||
<button
|
||||
class="btn"
|
||||
@click="clearAll"
|
||||
>
|
||||
{{ $t('settings.style.switcher.reset') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./theme_tab.js"></script>
|
||||
|
||||
<style src="./theme_tab.scss" lang="scss"></style>
|
24
src/components/settings_modal/tabs/version_tab.js
Normal file
24
src/components/settings_modal/tabs/version_tab.js
Normal file
|
@ -0,0 +1,24 @@
|
|||
import { extractCommit } from 'src/services/version/version.service'
|
||||
|
||||
const pleromaFeCommitUrl = 'https://git.pleroma.social/pleroma/pleroma-fe/commit/'
|
||||
const pleromaBeCommitUrl = 'https://git.pleroma.social/pleroma/pleroma/commit/'
|
||||
|
||||
const VersionTab = {
|
||||
data () {
|
||||
const instance = this.$store.state.instance
|
||||
return {
|
||||
backendVersion: instance.backendVersion,
|
||||
frontendVersion: instance.frontendVersion
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
frontendVersionLink () {
|
||||
return pleromaFeCommitUrl + this.frontendVersion
|
||||
},
|
||||
backendVersionLink () {
|
||||
return pleromaBeCommitUrl + extractCommit(this.backendVersion)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default VersionTab
|
31
src/components/settings_modal/tabs/version_tab.vue
Normal file
31
src/components/settings_modal/tabs/version_tab.vue
Normal file
|
@ -0,0 +1,31 @@
|
|||
<template>
|
||||
<div :label="$t('settings.version.title')">
|
||||
<div class="setting-item">
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<p>{{ $t('settings.version.backend_version') }}</p>
|
||||
<ul class="option-list">
|
||||
<li>
|
||||
<a
|
||||
:href="backendVersionLink"
|
||||
target="_blank"
|
||||
>{{ backendVersion }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p>{{ $t('settings.version.frontend_version') }}</p>
|
||||
<ul class="option-list">
|
||||
<li>
|
||||
<a
|
||||
:href="frontendVersionLink"
|
||||
target="_blank"
|
||||
>{{ frontendVersion }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script src="./version_tab.js">
|
Loading…
Add table
Add a link
Reference in a new issue