ユーザー設定とクライアント設定を分離
This commit is contained in:
@ -1,253 +0,0 @@
|
||||
<template>
|
||||
<section class="_card">
|
||||
<div class="_title"><fa :icon="faLock"/> {{ $t('twoStepAuthentication') }}</div>
|
||||
<div class="_content">
|
||||
<p v-if="!data && !$store.state.i.twoFactorEnabled"><mk-button @click="register">{{ $t('_2fa.registerDevice') }}</mk-button></p>
|
||||
<template v-if="$store.state.i.twoFactorEnabled">
|
||||
<p>{{ $t('_2fa.alreadyRegistered') }}</p>
|
||||
<mk-button @click="unregister">{{ $t('unregister') }}</mk-button>
|
||||
|
||||
<template v-if="supportsCredentials">
|
||||
<hr class="totp-method-sep">
|
||||
|
||||
<h2 class="heading">{{ $t('securityKey') }}</h2>
|
||||
<p>{{ $t('_2fa.securityKeyInfo') }}</p>
|
||||
<div class="key-list">
|
||||
<div class="key" v-for="key in $store.state.i.securityKeysList">
|
||||
<h3>{{ key.name }}</h3>
|
||||
<div class="last-used">{{ $t('lastUsed') }}<mk-time :time="key.lastUsed"/></div>
|
||||
<mk-button @click="unregisterKey(key)">{{ $t('unregister') }}</mk-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<mk-switch v-model="usePasswordLessLogin" @change="updatePasswordLessLogin" v-if="$store.state.i.securityKeysList.length > 0">{{ $t('passwordLessLogin') }}</mk-switch>
|
||||
|
||||
<mk-info warn v-if="registration && registration.error">{{ $t('something-went-wrong') }} {{ registration.error }}</mk-info>
|
||||
<mk-button v-if="!registration || registration.error" @click="addSecurityKey">{{ $t('_2fa.registerKey') }}</mk-button>
|
||||
|
||||
<ol v-if="registration && !registration.error">
|
||||
<li v-if="registration.stage >= 0">
|
||||
{{ $t('activate-key') }}
|
||||
<fa icon="spinner" pulse fixed-width v-if="registration.saving && registration.stage == 0" />
|
||||
</li>
|
||||
<li v-if="registration.stage >= 1">
|
||||
<mk-form :disabled="registration.stage != 1 || registration.saving">
|
||||
<mk-input v-model="keyName" :max="30">
|
||||
<span>{{ $t('securityKeyName') }}</span>
|
||||
</mk-input>
|
||||
<mk-button @click="registerKey" :disabled="keyName.length == 0">{{ $t('registerSecurityKey') }}</mk-button>
|
||||
<fa icon="spinner" pulse fixed-width v-if="registration.saving && registration.stage == 1" />
|
||||
</mk-form>
|
||||
</li>
|
||||
</ol>
|
||||
</template>
|
||||
</template>
|
||||
<div v-if="data && !$store.state.i.twoFactorEnabled">
|
||||
<ol style="margin: 0; padding: 0 0 0 1em;">
|
||||
<li>
|
||||
<i18n path="_2fa.step1" tag="span">
|
||||
<a href="https://authy.com/" rel="noopener" target="_blank" place="a" class="_link">Authy</a>
|
||||
<a href="https://support.google.com/accounts/answer/1066447" rel="noopener" target="_blank" place="b" class="_link">Google Authenticator</a>
|
||||
</i18n>
|
||||
</li>
|
||||
<li>{{ $t('_2fa.step2') }}<br><img :src="data.qr"></li>
|
||||
<li>{{ $t('_2fa.step3') }}<br>
|
||||
<mk-input v-model="token" type="text" pattern="^[0-9]{6}$" autocomplete="off" spellcheck="false">{{ $t('token') }}</mk-input>
|
||||
<mk-button primary @click="submit">{{ $t('done') }}</mk-button>
|
||||
</li>
|
||||
</ol>
|
||||
<mk-info>{{ $t('_2fa.step4') }}</mk-info>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { faLock } from '@fortawesome/free-solid-svg-icons';
|
||||
import i18n from '../../i18n';
|
||||
import { hostname } from '../../config';
|
||||
import { hexifyAB } from '../../scripts/2fa';
|
||||
import MkButton from '../../components/ui/button.vue';
|
||||
import MkInfo from '../../components/ui/info.vue';
|
||||
import MkInput from '../../components/ui/input.vue';
|
||||
import MkSwitch from '../../components/ui/switch.vue';
|
||||
|
||||
function stringifyAB(buffer) {
|
||||
return String.fromCharCode.apply(null, new Uint8Array(buffer));
|
||||
}
|
||||
|
||||
export default Vue.extend({
|
||||
i18n,
|
||||
components: {
|
||||
MkButton, MkInfo, MkInput, MkSwitch
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
data: null,
|
||||
supportsCredentials: !!navigator.credentials,
|
||||
usePasswordLessLogin: this.$store.state.i.usePasswordLessLogin,
|
||||
registration: null,
|
||||
keyName: '',
|
||||
token: null,
|
||||
faLock
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
register() {
|
||||
this.$root.dialog({
|
||||
title: this.$t('password'),
|
||||
input: {
|
||||
type: 'password'
|
||||
}
|
||||
}).then(({ canceled, result: password }) => {
|
||||
if (canceled) return;
|
||||
this.$root.api('i/2fa/register', {
|
||||
password: password
|
||||
}).then(data => {
|
||||
this.data = data;
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
unregister() {
|
||||
this.$root.dialog({
|
||||
title: this.$t('password'),
|
||||
input: {
|
||||
type: 'password'
|
||||
}
|
||||
}).then(({ canceled, result: password }) => {
|
||||
if (canceled) return;
|
||||
this.$root.api('i/2fa/unregister', {
|
||||
password: password
|
||||
}).then(() => {
|
||||
this.usePasswordLessLogin = false;
|
||||
this.updatePasswordLessLogin();
|
||||
}).then(() => {
|
||||
this.$root.dialog({
|
||||
type: 'success',
|
||||
iconOnly: true, autoClose: true
|
||||
});
|
||||
this.$store.state.i.twoFactorEnabled = false;
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
submit() {
|
||||
this.$root.api('i/2fa/done', {
|
||||
token: this.token
|
||||
}).then(() => {
|
||||
this.$root.dialog({
|
||||
type: 'success',
|
||||
iconOnly: true, autoClose: true
|
||||
});
|
||||
this.$store.state.i.twoFactorEnabled = true;
|
||||
}).catch(e => {
|
||||
this.$root.dialog({
|
||||
type: 'error',
|
||||
iconOnly: true, autoClose: true
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
registerKey() {
|
||||
this.registration.saving = true;
|
||||
this.$root.api('i/2fa/key-done', {
|
||||
password: this.registration.password,
|
||||
name: this.keyName,
|
||||
challengeId: this.registration.challengeId,
|
||||
// we convert each 16 bits to a string to serialise
|
||||
clientDataJSON: stringifyAB(this.registration.credential.response.clientDataJSON),
|
||||
attestationObject: hexifyAB(this.registration.credential.response.attestationObject)
|
||||
}).then(key => {
|
||||
this.registration = null;
|
||||
key.lastUsed = new Date();
|
||||
this.$root.dialog({
|
||||
type: 'success',
|
||||
iconOnly: true, autoClose: true
|
||||
});
|
||||
})
|
||||
},
|
||||
|
||||
unregisterKey(key) {
|
||||
this.$root.dialog({
|
||||
title: this.$t('password'),
|
||||
input: {
|
||||
type: 'password'
|
||||
}
|
||||
}).then(({ canceled, result: password }) => {
|
||||
if (canceled) return;
|
||||
return this.$root.api('i/2fa/remove-key', {
|
||||
password,
|
||||
credentialId: key.id
|
||||
}).then(() => {
|
||||
this.usePasswordLessLogin = false;
|
||||
this.updatePasswordLessLogin();
|
||||
}).then(() => {
|
||||
this.$root.dialog({
|
||||
type: 'success',
|
||||
iconOnly: true, autoClose: true
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
addSecurityKey() {
|
||||
this.$root.dialog({
|
||||
title: this.$t('password'),
|
||||
input: {
|
||||
type: 'password'
|
||||
}
|
||||
}).then(({ canceled, result: password }) => {
|
||||
if (canceled) return;
|
||||
this.$root.api('i/2fa/register-key', {
|
||||
password
|
||||
}).then(registration => {
|
||||
this.registration = {
|
||||
password,
|
||||
challengeId: registration.challengeId,
|
||||
stage: 0,
|
||||
publicKeyOptions: {
|
||||
challenge: Buffer.from(
|
||||
registration.challenge
|
||||
.replace(/\-/g, "+")
|
||||
.replace(/_/g, "/"),
|
||||
'base64'
|
||||
),
|
||||
rp: {
|
||||
id: hostname,
|
||||
name: 'Misskey'
|
||||
},
|
||||
user: {
|
||||
id: Uint8Array.from(this.$store.state.i.id, c => c.charCodeAt(0)),
|
||||
name: this.$store.state.i.username,
|
||||
displayName: this.$store.state.i.name,
|
||||
},
|
||||
pubKeyCredParams: [{alg: -7, type: 'public-key'}],
|
||||
timeout: 60000,
|
||||
attestation: 'direct'
|
||||
},
|
||||
saving: true
|
||||
};
|
||||
return navigator.credentials.create({
|
||||
publicKey: this.registration.publicKeyOptions
|
||||
});
|
||||
}).then(credential => {
|
||||
this.registration.credential = credential;
|
||||
this.registration.saving = false;
|
||||
this.registration.stage = 1;
|
||||
}).catch(err => {
|
||||
console.warn('Error while registering?', err);
|
||||
this.registration.error = err.message;
|
||||
this.registration.stage = -1;
|
||||
});
|
||||
});
|
||||
},
|
||||
updatePasswordLessLogin() {
|
||||
this.$root.api('i/2fa/password-less', {
|
||||
value: !!this.usePasswordLessLogin
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
@ -1,46 +0,0 @@
|
||||
<template>
|
||||
<section class="_card">
|
||||
<div class="_title"><fa :icon="faKey"/> API</div>
|
||||
<div class="_content">
|
||||
<mk-input :value="$store.state.i.token" readonly>
|
||||
<span>{{ $t('token') }}</span>
|
||||
</mk-input>
|
||||
<mk-button @click="regenerateToken"><fa :icon="faSyncAlt"/> {{ $t('regenerate') }}</mk-button>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { faKey, faSyncAlt } from '@fortawesome/free-solid-svg-icons';
|
||||
import i18n from '../../i18n';
|
||||
import MkButton from '../../components/ui/button.vue';
|
||||
import MkInput from '../../components/ui/input.vue';
|
||||
|
||||
export default Vue.extend({
|
||||
i18n,
|
||||
components: {
|
||||
MkButton, MkInput
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
faKey, faSyncAlt
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
regenerateToken() {
|
||||
this.$root.dialog({
|
||||
title: this.$t('password'),
|
||||
input: {
|
||||
type: 'password'
|
||||
}
|
||||
}).then(({ canceled, result: password }) => {
|
||||
if (canceled) return;
|
||||
this.$root.api('i/regenerate_token', {
|
||||
password: password
|
||||
});
|
||||
});
|
||||
},
|
||||
}
|
||||
});
|
||||
</script>
|
@ -1,212 +0,0 @@
|
||||
<template>
|
||||
<section class="uawsfosz _card">
|
||||
<div class="_title"><fa :icon="faCloud"/> {{ $t('drive') }}</div>
|
||||
<div class="_content">
|
||||
<mk-pagination :pagination="drivePagination" #default="{items}" class="drive" ref="drive">
|
||||
<div class="file" v-for="(file, i) in items" :key="file.id" @click="selected = file" :class="{ selected: selected && (selected.id === file.id) }">
|
||||
<x-file-thumbnail class="thumbnail" :file="file" fit="cover"/>
|
||||
<div class="body">
|
||||
<p class="name">
|
||||
<span>{{ file.name.lastIndexOf('.') != -1 ? file.name.substr(0, file.name.lastIndexOf('.')) : file.name }}</span>
|
||||
<span class="ext" v-if="file.name.lastIndexOf('.') != -1">{{ file.name.substr(file.name.lastIndexOf('.')) }}</span>
|
||||
</p>
|
||||
<footer>
|
||||
<span class="type"><x-file-type-icon :type="file.type" class="icon"/>{{ file.type }}</span>
|
||||
<span class="separator"></span>
|
||||
<span class="data-size">{{ file.size | bytes }}</span>
|
||||
<span class="separator"></span>
|
||||
<span class="created-at"><fa :icon="faClock"/><mk-time :time="file.createdAt"/></span>
|
||||
<template v-if="file.isSensitive">
|
||||
<span class="separator"></span>
|
||||
<span class="nsfw"><fa :icon="faEyeSlash"/> {{ $t('nsfw') }}</span>
|
||||
</template>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</mk-pagination>
|
||||
</div>
|
||||
<div class="_footer">
|
||||
<mk-button primary inline :disabled="selected == null" @click="download()"><fa :icon="faDownload"/> {{ $t('download') }}</mk-button>
|
||||
<mk-button inline :disabled="selected == null" @click="del()"><fa :icon="faTrashAlt"/> {{ $t('delete') }}</mk-button>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { faCloud, faDownload } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faClock, faEyeSlash, faTrashAlt } from '@fortawesome/free-regular-svg-icons';
|
||||
import XFileTypeIcon from '../../components/file-type-icon.vue';
|
||||
import XFileThumbnail from '../../components/drive-file-thumbnail.vue';
|
||||
import MkButton from '../../components/ui/button.vue';
|
||||
import MkPagination from '../../components/ui/pagination.vue';
|
||||
import i18n from '../../i18n';
|
||||
|
||||
export default Vue.extend({
|
||||
i18n,
|
||||
|
||||
components: {
|
||||
XFileTypeIcon,
|
||||
XFileThumbnail,
|
||||
MkPagination,
|
||||
MkButton,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
selected: null,
|
||||
connection: null,
|
||||
drivePagination: {
|
||||
endpoint: 'drive/files',
|
||||
limit: 10,
|
||||
},
|
||||
faCloud, faClock, faEyeSlash, faDownload, faTrashAlt
|
||||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
this.connection = this.$root.stream.useSharedConnection('drive');
|
||||
|
||||
this.connection.on('fileCreated', this.onStreamDriveFileCreated);
|
||||
this.connection.on('fileUpdated', this.onStreamDriveFileUpdated);
|
||||
this.connection.on('fileDeleted', this.onStreamDriveFileDeleted);
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
this.connection.dispose();
|
||||
},
|
||||
|
||||
methods: {
|
||||
onStreamDriveFileCreated(file) {
|
||||
this.$refs.drive.prepend(file);
|
||||
},
|
||||
|
||||
onStreamDriveFileUpdated(file) {
|
||||
// TODO
|
||||
},
|
||||
|
||||
onStreamDriveFileDeleted(fileId) {
|
||||
this.$refs.drive.remove(x => x.id === fileId);
|
||||
},
|
||||
|
||||
download() {
|
||||
window.open(this.selected.url, '_blank');
|
||||
},
|
||||
|
||||
async del() {
|
||||
const { canceled } = await this.$root.dialog({
|
||||
type: 'warning',
|
||||
text: this.$t('driveFileDeleteConfirm', { name: this.selected.name }),
|
||||
showCancelButton: true
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
this.$root.api('drive/files/delete', {
|
||||
fileId: this.selected.id
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.uawsfosz {
|
||||
> ._content {
|
||||
max-height: 350px;
|
||||
overflow: auto;
|
||||
|
||||
> .drive {
|
||||
> .file {
|
||||
display: grid;
|
||||
margin: 0 auto;
|
||||
grid-template-columns: 64px 1fr;
|
||||
grid-column-gap: 10px;
|
||||
cursor: pointer;
|
||||
|
||||
&.selected {
|
||||
background: var(--accent);
|
||||
box-shadow: 0 0 0 8px var(--accent);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
> .thumbnail {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
}
|
||||
|
||||
> .body {
|
||||
display: block;
|
||||
word-break: break-all;
|
||||
padding-top: 4px;
|
||||
|
||||
> .name {
|
||||
display: block;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-size: 0.9em;
|
||||
font-weight: bold;
|
||||
word-break: break-word;
|
||||
|
||||
> .ext {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
> .tags {
|
||||
display: block;
|
||||
margin: 4px 0 0 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
font-size: 0.5em;
|
||||
|
||||
> .tag {
|
||||
display: inline-block;
|
||||
margin: 0 5px 0 0;
|
||||
padding: 1px 5px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
> footer {
|
||||
display: block;
|
||||
margin: 4px 0 0 0;
|
||||
font-size: 0.7em;
|
||||
|
||||
> .separator {
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
> .type {
|
||||
opacity: 0.7;
|
||||
|
||||
> .icon {
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
> .data-size {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
> .created-at {
|
||||
opacity: 0.7;
|
||||
|
||||
> [data-icon] {
|
||||
margin-right: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
> .nsfw {
|
||||
color: #bf4633;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,159 +0,0 @@
|
||||
<template>
|
||||
<section class="_card">
|
||||
<div class="_title"><fa :icon="faCog"/> {{ $t('general') }}</div>
|
||||
<div class="_content">
|
||||
<mk-button primary v-if="wallpaper == null" @click="setWallpaper">{{ $t('setWallpaper') }}</mk-button>
|
||||
<mk-button primary v-else @click="wallpaper = null">{{ $t('removeWallpaper') }}</mk-button>
|
||||
</div>
|
||||
<div class="_content">
|
||||
<mk-switch v-model="autoReload">
|
||||
{{ $t('autoReloadWhenDisconnected') }}
|
||||
</mk-switch>
|
||||
<mk-switch v-model="$store.state.i.autoWatch" @change="onChangeAutoWatch">
|
||||
{{ $t('autoNoteWatch') }}<template #desc>{{ $t('autoNoteWatchDescription') }}</template>
|
||||
</mk-switch>
|
||||
</div>
|
||||
<div class="_content">
|
||||
<mk-button @click="readAllNotifications">{{ $t('markAsReadAllNotifications') }}</mk-button>
|
||||
<mk-button @click="readAllUnreadNotes">{{ $t('markAsReadAllUnreadNotes') }}</mk-button>
|
||||
<mk-button @click="readAllMessagingMessages">{{ $t('markAsReadAllTalkMessages') }}</mk-button>
|
||||
</div>
|
||||
<div class="_content">
|
||||
<mk-switch v-model="imageNewTab">{{ $t('openImageInNewTab') }}</mk-switch>
|
||||
<mk-switch v-model="disableAnimatedMfm">{{ $t('disableAnimatedMfm') }}</mk-switch>
|
||||
<mk-switch v-model="reduceAnimation">{{ $t('reduceUiAnimation') }}</mk-switch>
|
||||
<mk-switch v-model="useOsNativeEmojis">
|
||||
{{ $t('useOsNativeEmojis') }}
|
||||
<template #desc><mfm text="🍮🍦🍭🍩🍰🍫🍬🥞🍪"/></template>
|
||||
</mk-switch>
|
||||
</div>
|
||||
<div class="_content">
|
||||
<mk-select v-model="lang">
|
||||
<template #label>{{ $t('uiLanguage') }}</template>
|
||||
|
||||
<option v-for="x in langs" :value="x[0]" :key="x[0]">{{ x[1] }}</option>
|
||||
</mk-select>
|
||||
</div>
|
||||
<div class="_content">
|
||||
<div>{{ $t('fontSize') }}</div>
|
||||
<mk-radio v-model="fontSize" value="small"><span style="font-size: 14px;">Aa</span></mk-radio>
|
||||
<mk-radio v-model="fontSize" :value="null"><span style="font-size: 16px;">Aa</span></mk-radio>
|
||||
<mk-radio v-model="fontSize" value="large"><span style="font-size: 18px;">Aa</span></mk-radio>
|
||||
<mk-radio v-model="fontSize" value="veryLarge"><span style="font-size: 20px;">Aa</span></mk-radio>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { faImage, faCog } from '@fortawesome/free-solid-svg-icons';
|
||||
import MkInput from '../../components/ui/input.vue';
|
||||
import MkButton from '../../components/ui/button.vue';
|
||||
import MkSwitch from '../../components/ui/switch.vue';
|
||||
import MkSelect from '../../components/ui/select.vue';
|
||||
import MkRadio from '../../components/ui/radio.vue';
|
||||
import i18n from '../../i18n';
|
||||
import { langs } from '../../config';
|
||||
import { selectFile } from '../../scripts/select-file';
|
||||
|
||||
export default Vue.extend({
|
||||
i18n,
|
||||
|
||||
components: {
|
||||
MkInput,
|
||||
MkButton,
|
||||
MkSwitch,
|
||||
MkSelect,
|
||||
MkRadio,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
langs,
|
||||
lang: localStorage.getItem('lang'),
|
||||
fontSize: localStorage.getItem('fontSize'),
|
||||
wallpaper: localStorage.getItem('wallpaper'),
|
||||
faImage, faCog
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
autoReload: {
|
||||
get() { return this.$store.state.device.autoReload; },
|
||||
set(value) { this.$store.commit('device/set', { key: 'autoReload', value }); }
|
||||
},
|
||||
|
||||
reduceAnimation: {
|
||||
get() { return !this.$store.state.device.animation; },
|
||||
set(value) { this.$store.commit('device/set', { key: 'animation', value: !value }); }
|
||||
},
|
||||
|
||||
disableAnimatedMfm: {
|
||||
get() { return !this.$store.state.device.animatedMfm; },
|
||||
set(value) { this.$store.commit('device/set', { key: 'animatedMfm', value: !value }); }
|
||||
},
|
||||
|
||||
useOsNativeEmojis: {
|
||||
get() { return this.$store.state.device.useOsNativeEmojis; },
|
||||
set(value) { this.$store.commit('device/set', { key: 'useOsNativeEmojis', value }); }
|
||||
},
|
||||
|
||||
imageNewTab: {
|
||||
get() { return this.$store.state.device.imageNewTab; },
|
||||
set(value) { this.$store.commit('device/set', { key: 'imageNewTab', value }); }
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
lang() {
|
||||
localStorage.setItem('lang', this.lang);
|
||||
localStorage.removeItem('locale');
|
||||
location.reload();
|
||||
},
|
||||
|
||||
fontSize() {
|
||||
if (this.fontSize == null) {
|
||||
localStorage.removeItem('fontSize');
|
||||
} else {
|
||||
localStorage.setItem('fontSize', this.fontSize);
|
||||
}
|
||||
location.reload();
|
||||
},
|
||||
|
||||
wallpaper() {
|
||||
if (this.wallpaper == null) {
|
||||
localStorage.removeItem('wallpaper');
|
||||
} else {
|
||||
localStorage.setItem('wallpaper', this.wallpaper);
|
||||
}
|
||||
location.reload();
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
setWallpaper(e) {
|
||||
selectFile(this, e.currentTarget || e.target, null, false).then(file => {
|
||||
this.wallpaper = file.url;
|
||||
});
|
||||
},
|
||||
|
||||
onChangeAutoWatch(v) {
|
||||
this.$root.api('i/update', {
|
||||
autoWatch: v
|
||||
});
|
||||
},
|
||||
|
||||
readAllUnreadNotes() {
|
||||
this.$root.api('i/read_all_unread_notes');
|
||||
},
|
||||
|
||||
readAllMessagingMessages() {
|
||||
this.$root.api('i/read_all_messaging_messages');
|
||||
},
|
||||
|
||||
readAllNotifications() {
|
||||
this.$root.api('notifications/mark_all_as_read');
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
@ -1,121 +0,0 @@
|
||||
<template>
|
||||
<section class="_card">
|
||||
<div class="_title"><fa :icon="faBoxes"/> {{ $t('importAndExport') }}</div>
|
||||
<div class="_content">
|
||||
<mk-select v-model="exportTarget">
|
||||
<option value="notes">{{ $t('_exportOrImport.allNotes') }}</option>
|
||||
<option value="following">{{ $t('_exportOrImport.followingList') }}</option>
|
||||
<option value="user-lists">{{ $t('_exportOrImport.userLists') }}</option>
|
||||
<option value="mute">{{ $t('_exportOrImport.muteList') }}</option>
|
||||
<option value="blocking">{{ $t('_exportOrImport.blockingList') }}</option>
|
||||
</mk-select>
|
||||
<mk-button inline @click="doExport()"><fa :icon="faDownload"/> {{ $t('export') }}</mk-button>
|
||||
<mk-button inline @click="doImport()" :disabled="!['following', 'user-lists'].includes(exportTarget)"><fa :icon="faUpload"/> {{ $t('import') }}</mk-button>
|
||||
</div>
|
||||
<input ref="file" type="file" style="display: none;" @change="onChangeFile"/>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { faDownload, faUpload, faBoxes } from '@fortawesome/free-solid-svg-icons';
|
||||
import MkButton from '../../components/ui/button.vue';
|
||||
import MkSelect from '../../components/ui/select.vue';
|
||||
import i18n from '../../i18n';
|
||||
import { apiUrl } from '../../config';
|
||||
|
||||
export default Vue.extend({
|
||||
i18n,
|
||||
|
||||
components: {
|
||||
MkButton,
|
||||
MkSelect,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
exportTarget: 'notes',
|
||||
faDownload, faUpload, faBoxes
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
doExport() {
|
||||
this.$root.api(
|
||||
this.exportTarget == 'notes' ? 'i/export-notes' :
|
||||
this.exportTarget == 'following' ? 'i/export-following' :
|
||||
this.exportTarget == 'blocking' ? 'i/export-blocking' :
|
||||
this.exportTarget == 'user-lists' ? 'i/export-user-lists' :
|
||||
null, {})
|
||||
.then(() => {
|
||||
this.$root.dialog({
|
||||
type: 'info',
|
||||
text: this.$t('exportRequested')
|
||||
});
|
||||
}).catch((e: any) => {
|
||||
this.$root.dialog({
|
||||
type: 'error',
|
||||
text: e.message
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
doImport() {
|
||||
(this.$refs.file as any).click();
|
||||
},
|
||||
|
||||
onChangeFile() {
|
||||
const [file] = Array.from((this.$refs.file as any).files);
|
||||
|
||||
const data = new FormData();
|
||||
data.append('file', file);
|
||||
data.append('i', this.$store.state.i.token);
|
||||
|
||||
const dialog = this.$root.dialog({
|
||||
type: 'waiting',
|
||||
text: this.$t('uploading') + '...',
|
||||
showOkButton: false,
|
||||
showCancelButton: false,
|
||||
cancelableByBgClick: false
|
||||
});
|
||||
|
||||
fetch(apiUrl + '/drive/files/create', {
|
||||
method: 'POST',
|
||||
body: data
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(f => {
|
||||
this.reqImport(f);
|
||||
})
|
||||
.catch(e => {
|
||||
this.$root.dialog({
|
||||
type: 'error',
|
||||
text: e
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
dialog.close();
|
||||
});
|
||||
},
|
||||
|
||||
reqImport(file) {
|
||||
this.$root.api(
|
||||
this.exportTarget == 'following' ? 'i/import-following' :
|
||||
this.exportTarget == 'user-lists' ? 'i/import-user-lists' :
|
||||
null, {
|
||||
fileId: file.id
|
||||
}).then(() => {
|
||||
this.$root.dialog({
|
||||
type: 'info',
|
||||
text: this.$t('importRequested')
|
||||
});
|
||||
}).catch((e: any) => {
|
||||
this.$root.dialog({
|
||||
type: 'error',
|
||||
text: e.message
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
@ -1,44 +1,61 @@
|
||||
<template>
|
||||
<div class="mk-settings-page">
|
||||
<div>
|
||||
<portal to="icon"><fa :icon="faCog"/></portal>
|
||||
<portal to="title">{{ $t('settings') }}</portal>
|
||||
|
||||
<x-profile-setting/>
|
||||
<x-privacy-setting/>
|
||||
<x-reaction-setting/>
|
||||
<x-theme/>
|
||||
<x-import-export/>
|
||||
<x-drive/>
|
||||
<x-general/>
|
||||
<x-mute-block/>
|
||||
<x-security/>
|
||||
<x-2fa/>
|
||||
<x-integration/>
|
||||
<x-api/>
|
||||
|
||||
<mk-button @click="cacheClear()" primary class="cacheClear">{{ $t('cacheClear') }}</mk-button>
|
||||
<mk-button @click="$root.signout()" primary class="logout">{{ $t('logout') }}</mk-button>
|
||||
<section class="_card">
|
||||
<div class="_title"><fa :icon="faCog"/> {{ $t('accessibility') }}</div>
|
||||
<div class="_content">
|
||||
<mk-switch v-model="autoReload">
|
||||
{{ $t('autoReloadWhenDisconnected') }}
|
||||
</mk-switch>
|
||||
</div>
|
||||
<div class="_content">
|
||||
<mk-switch v-model="imageNewTab">{{ $t('openImageInNewTab') }}</mk-switch>
|
||||
<mk-switch v-model="disableAnimatedMfm">{{ $t('disableAnimatedMfm') }}</mk-switch>
|
||||
<mk-switch v-model="reduceAnimation">{{ $t('reduceUiAnimation') }}</mk-switch>
|
||||
<mk-switch v-model="useOsNativeEmojis">
|
||||
{{ $t('useOsNativeEmojis') }}
|
||||
<template #desc><mfm text="🍮🍦🍭🍩🍰🍫🍬🥞🍪"/></template>
|
||||
</mk-switch>
|
||||
</div>
|
||||
<div class="_content">
|
||||
<mk-select v-model="lang">
|
||||
<template #label>{{ $t('uiLanguage') }}</template>
|
||||
|
||||
<option v-for="x in langs" :value="x[0]" :key="x[0]">{{ x[1] }}</option>
|
||||
</mk-select>
|
||||
</div>
|
||||
<div class="_content">
|
||||
<div>{{ $t('fontSize') }}</div>
|
||||
<mk-radio v-model="fontSize" value="small"><span style="font-size: 14px;">Aa</span></mk-radio>
|
||||
<mk-radio v-model="fontSize" :value="null"><span style="font-size: 16px;">Aa</span></mk-radio>
|
||||
<mk-radio v-model="fontSize" value="large"><span style="font-size: 18px;">Aa</span></mk-radio>
|
||||
<mk-radio v-model="fontSize" value="veryLarge"><span style="font-size: 20px;">Aa</span></mk-radio>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<mk-button @click="cacheClear()" primary style="margin: var(--margin) auto;">{{ $t('cacheClear') }}</mk-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { faCog } from '@fortawesome/free-solid-svg-icons';
|
||||
import XProfileSetting from './profile.vue';
|
||||
import XPrivacySetting from './privacy.vue';
|
||||
import XImportExport from './import-export.vue';
|
||||
import XDrive from './drive.vue';
|
||||
import XGeneral from './general.vue';
|
||||
import XReactionSetting from './reaction.vue';
|
||||
import XMuteBlock from './mute-block.vue';
|
||||
import XSecurity from './security.vue';
|
||||
import XTheme from './theme.vue';
|
||||
import X2fa from './2fa.vue';
|
||||
import XIntegration from './integration.vue';
|
||||
import XApi from './api.vue';
|
||||
import { faImage, faCog } from '@fortawesome/free-solid-svg-icons';
|
||||
import MkInput from '../../components/ui/input.vue';
|
||||
import MkButton from '../../components/ui/button.vue';
|
||||
import MkSwitch from '../../components/ui/switch.vue';
|
||||
import MkSelect from '../../components/ui/select.vue';
|
||||
import MkRadio from '../../components/ui/radio.vue';
|
||||
import XTheme from './theme.vue';
|
||||
import i18n from '../../i18n';
|
||||
import { langs } from '../../config';
|
||||
|
||||
export default Vue.extend({
|
||||
i18n,
|
||||
|
||||
metaInfo() {
|
||||
return {
|
||||
title: this.$t('settings') as string
|
||||
@ -46,27 +63,67 @@ export default Vue.extend({
|
||||
},
|
||||
|
||||
components: {
|
||||
XProfileSetting,
|
||||
XPrivacySetting,
|
||||
XImportExport,
|
||||
XDrive,
|
||||
XGeneral,
|
||||
XReactionSetting,
|
||||
XMuteBlock,
|
||||
XSecurity,
|
||||
XTheme,
|
||||
X2fa,
|
||||
XIntegration,
|
||||
XApi,
|
||||
MkInput,
|
||||
MkButton,
|
||||
MkSwitch,
|
||||
MkSelect,
|
||||
MkRadio,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
faCog
|
||||
langs,
|
||||
lang: localStorage.getItem('lang'),
|
||||
fontSize: localStorage.getItem('fontSize'),
|
||||
faImage, faCog
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
autoReload: {
|
||||
get() { return this.$store.state.device.autoReload; },
|
||||
set(value) { this.$store.commit('device/set', { key: 'autoReload', value }); }
|
||||
},
|
||||
|
||||
reduceAnimation: {
|
||||
get() { return !this.$store.state.device.animation; },
|
||||
set(value) { this.$store.commit('device/set', { key: 'animation', value: !value }); }
|
||||
},
|
||||
|
||||
disableAnimatedMfm: {
|
||||
get() { return !this.$store.state.device.animatedMfm; },
|
||||
set(value) { this.$store.commit('device/set', { key: 'animatedMfm', value: !value }); }
|
||||
},
|
||||
|
||||
useOsNativeEmojis: {
|
||||
get() { return this.$store.state.device.useOsNativeEmojis; },
|
||||
set(value) { this.$store.commit('device/set', { key: 'useOsNativeEmojis', value }); }
|
||||
},
|
||||
|
||||
imageNewTab: {
|
||||
get() { return this.$store.state.device.imageNewTab; },
|
||||
set(value) { this.$store.commit('device/set', { key: 'imageNewTab', value }); }
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
lang() {
|
||||
localStorage.setItem('lang', this.lang);
|
||||
localStorage.removeItem('locale');
|
||||
location.reload();
|
||||
},
|
||||
|
||||
fontSize() {
|
||||
if (this.fontSize == null) {
|
||||
localStorage.removeItem('fontSize');
|
||||
} else {
|
||||
localStorage.setItem('fontSize', this.fontSize);
|
||||
}
|
||||
location.reload();
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
cacheClear() {
|
||||
// Clear cache (service worker)
|
||||
@ -86,12 +143,3 @@ export default Vue.extend({
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.mk-settings-page {
|
||||
> .logout,
|
||||
> .cacheClear {
|
||||
margin: 8px auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -1,131 +0,0 @@
|
||||
<template>
|
||||
<section class="_card" v-if="enableTwitterIntegration || enableDiscordIntegration || enableGithubIntegration">
|
||||
<div class="_title"><fa :icon="faShareAlt"/> {{ $t('integration') }}</div>
|
||||
|
||||
<div class="_content" v-if="enableTwitterIntegration">
|
||||
<header><fa :icon="faTwitter"/> Twitter</header>
|
||||
<p v-if="integrations.twitter">{{ $t('connectedTo') }}: <a :href="`https://twitter.com/${integrations.twitter.screenName}`" rel="nofollow noopener" target="_blank">@{{ integrations.twitter.screenName }}</a></p>
|
||||
<mk-button v-if="integrations.twitter" @click="disconnectTwitter">{{ $t('disconnectSerice') }}</mk-button>
|
||||
<mk-button v-else @click="connectTwitter">{{ $t('connectSerice') }}</mk-button>
|
||||
</div>
|
||||
|
||||
<div class="_content" v-if="enableDiscordIntegration">
|
||||
<header><fa :icon="faDiscord"/> Discord</header>
|
||||
<p v-if="integrations.discord">{{ $t('connectedTo') }}: <a :href="`https://discordapp.com/users/${integrations.discord.id}`" rel="nofollow noopener" target="_blank">@{{ integrations.discord.username }}#{{ integrations.discord.discriminator }}</a></p>
|
||||
<mk-button v-if="integrations.discord" @click="disconnectDiscord">{{ $t('disconnectSerice') }}</mk-button>
|
||||
<mk-button v-else @click="connectDiscord">{{ $t('connectSerice') }}</mk-button>
|
||||
</div>
|
||||
|
||||
<div class="_content" v-if="enableGithubIntegration">
|
||||
<header><fa :icon="faGithub"/> GitHub</header>
|
||||
<p v-if="integrations.github">{{ $t('connectedTo') }}: <a :href="`https://github.com/${integrations.github.login}`" rel="nofollow noopener" target="_blank">@{{ integrations.github.login }}</a></p>
|
||||
<mk-button v-if="integrations.github" @click="disconnectGithub">{{ $t('disconnectSerice') }}</mk-button>
|
||||
<mk-button v-else @click="connectGithub">{{ $t('connectSerice') }}</mk-button>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { faShareAlt } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faTwitter, faDiscord, faGithub } from '@fortawesome/free-brands-svg-icons';
|
||||
import i18n from '../../i18n';
|
||||
import { apiUrl } from '../../config';
|
||||
import MkButton from '../../components/ui/button.vue';
|
||||
|
||||
export default Vue.extend({
|
||||
i18n,
|
||||
|
||||
components: {
|
||||
MkButton
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
apiUrl,
|
||||
twitterForm: null,
|
||||
discordForm: null,
|
||||
githubForm: null,
|
||||
enableTwitterIntegration: false,
|
||||
enableDiscordIntegration: false,
|
||||
enableGithubIntegration: false,
|
||||
faShareAlt, faTwitter, faDiscord, faGithub
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
integrations() {
|
||||
return this.$store.state.i.integrations;
|
||||
},
|
||||
|
||||
meta() {
|
||||
return this.$store.state.instance.meta;
|
||||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
this.enableTwitterIntegration = this.meta.enableTwitterIntegration;
|
||||
this.enableDiscordIntegration = this.meta.enableDiscordIntegration;
|
||||
this.enableGithubIntegration = this.meta.enableGithubIntegration;
|
||||
},
|
||||
|
||||
mounted() {
|
||||
if (!document.cookie.match(/i=(\w+)/)) {
|
||||
document.cookie = `i=${this.$store.state.i.token}; path=/;` +
|
||||
` domain=${document.location.hostname}; max-age=31536000;` +
|
||||
(document.location.protocol.startsWith('https') ? ' secure' : '');
|
||||
}
|
||||
this.$watch('integrations', () => {
|
||||
if (this.integrations.twitter) {
|
||||
if (this.twitterForm) this.twitterForm.close();
|
||||
}
|
||||
if (this.integrations.discord) {
|
||||
if (this.discordForm) this.discordForm.close();
|
||||
}
|
||||
if (this.integrations.github) {
|
||||
if (this.githubForm) this.githubForm.close();
|
||||
}
|
||||
}, {
|
||||
deep: true
|
||||
});
|
||||
},
|
||||
|
||||
methods: {
|
||||
connectTwitter() {
|
||||
this.twitterForm = window.open(apiUrl + '/connect/twitter',
|
||||
'twitter_connect_window',
|
||||
'height=570, width=520');
|
||||
},
|
||||
|
||||
disconnectTwitter() {
|
||||
window.open(apiUrl + '/disconnect/twitter',
|
||||
'twitter_disconnect_window',
|
||||
'height=570, width=520');
|
||||
},
|
||||
|
||||
connectDiscord() {
|
||||
this.discordForm = window.open(apiUrl + '/connect/discord',
|
||||
'discord_connect_window',
|
||||
'height=570, width=520');
|
||||
},
|
||||
|
||||
disconnectDiscord() {
|
||||
window.open(apiUrl + '/disconnect/discord',
|
||||
'discord_disconnect_window',
|
||||
'height=570, width=520');
|
||||
},
|
||||
|
||||
connectGithub() {
|
||||
this.githubForm = window.open(apiUrl + '/connect/github',
|
||||
'github_connect_window',
|
||||
'height=570, width=520');
|
||||
},
|
||||
|
||||
disconnectGithub() {
|
||||
window.open(apiUrl + '/disconnect/github',
|
||||
'github_disconnect_window',
|
||||
'height=570, width=520');
|
||||
},
|
||||
}
|
||||
});
|
||||
</script>
|
@ -1,76 +0,0 @@
|
||||
<template>
|
||||
<section class="rrfwjxfl _card">
|
||||
<div class="_title"><fa :icon="faBan"/> {{ $t('muteAndBlock') }}</div>
|
||||
<div class="_content">
|
||||
<span>{{ $t('mutedUsers') }}</span>
|
||||
<mk-pagination :pagination="mutingPagination" class="muting">
|
||||
<template #empty><span>{{ $t('noUsers') }}</span></template>
|
||||
<template #default="{items}">
|
||||
<div class="user" v-for="(mute, i) in items" :key="mute.id">
|
||||
<router-link class="name" :to="mute.mutee | userPage">
|
||||
<mk-acct :user="mute.mutee"/>
|
||||
</router-link>
|
||||
</div>
|
||||
</template>
|
||||
</mk-pagination>
|
||||
</div>
|
||||
<div class="_content">
|
||||
<span>{{ $t('blockedUsers') }}</span>
|
||||
<mk-pagination :pagination="blockingPagination" class="blocking">
|
||||
<template #empty><span>{{ $t('noUsers') }}</span></template>
|
||||
<template #default="{items}">
|
||||
<div class="user" v-for="(block, i) in items" :key="block.id">
|
||||
<router-link class="name" :to="block.blockee | userPage">
|
||||
<mk-acct :user="block.blockee"/>
|
||||
</router-link>
|
||||
</div>
|
||||
</template>
|
||||
</mk-pagination>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { faBan } from '@fortawesome/free-solid-svg-icons';
|
||||
import MkPagination from '../../components/ui/pagination.vue';
|
||||
import i18n from '../../i18n';
|
||||
|
||||
export default Vue.extend({
|
||||
i18n,
|
||||
|
||||
components: {
|
||||
MkPagination,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
mutingPagination: {
|
||||
endpoint: 'mute/list',
|
||||
limit: 10,
|
||||
},
|
||||
blockingPagination: {
|
||||
endpoint: 'blocking/list',
|
||||
limit: 10,
|
||||
},
|
||||
faBan
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.rrfwjxfl {
|
||||
> ._content {
|
||||
max-height: 350px;
|
||||
overflow: auto;
|
||||
|
||||
> .muting,
|
||||
> .blocking {
|
||||
> .empty {
|
||||
opacity: 0.5 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,76 +0,0 @@
|
||||
<template>
|
||||
<section class="_card">
|
||||
<div class="_title"><fa :icon="faLock"/> {{ $t('privacy') }}</div>
|
||||
<div class="_content">
|
||||
<mk-switch v-model="isLocked" @change="save()">{{ $t('makeFollowManuallyApprove') }}</mk-switch>
|
||||
<mk-switch v-model="autoAcceptFollowed" v-if="isLocked" @change="save()">{{ $t('autoAcceptFollowed') }}</mk-switch>
|
||||
</div>
|
||||
<div class="_content">
|
||||
<mk-switch v-model="rememberNoteVisibility" @change="save()">{{ $t('rememberNoteVisibility') }}</mk-switch>
|
||||
<mk-select v-model="defaultNoteVisibility" style="margin-bottom: 8px;" v-if="!rememberNoteVisibility">
|
||||
<template #label>{{ $t('defaultNoteVisibility') }}</template>
|
||||
<option value="public">{{ $t('_visibility.public') }}</option>
|
||||
<option value="home">{{ $t('_visibility.home') }}</option>
|
||||
<option value="followers">{{ $t('_visibility.followers') }}</option>
|
||||
<option value="specified">{{ $t('_visibility.specified') }}</option>
|
||||
</mk-select>
|
||||
<mk-switch v-model="defaultNoteLocalOnly" v-if="!rememberNoteVisibility">{{ $t('_visibility.localOnly') }}</mk-switch>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { faLock } from '@fortawesome/free-solid-svg-icons';
|
||||
import MkSelect from '../../components/ui/select.vue';
|
||||
import MkSwitch from '../../components/ui/switch.vue';
|
||||
import i18n from '../../i18n';
|
||||
|
||||
export default Vue.extend({
|
||||
i18n,
|
||||
|
||||
components: {
|
||||
MkSelect,
|
||||
MkSwitch,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
isLocked: false,
|
||||
autoAcceptFollowed: false,
|
||||
faLock
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
defaultNoteVisibility: {
|
||||
get() { return this.$store.state.settings.defaultNoteVisibility; },
|
||||
set(value) { this.$store.dispatch('settings/set', { key: 'defaultNoteVisibility', value }); }
|
||||
},
|
||||
|
||||
defaultNoteLocalOnly: {
|
||||
get() { return this.$store.state.settings.defaultNoteLocalOnly; },
|
||||
set(value) { this.$store.dispatch('settings/set', { key: 'defaultNoteLocalOnly', value }); }
|
||||
},
|
||||
|
||||
rememberNoteVisibility: {
|
||||
get() { return this.$store.state.settings.rememberNoteVisibility; },
|
||||
set(value) { this.$store.dispatch('settings/set', { key: 'rememberNoteVisibility', value }); }
|
||||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
this.isLocked = this.$store.state.i.isLocked;
|
||||
this.autoAcceptFollowed = this.$store.state.i.autoAcceptFollowed;
|
||||
},
|
||||
|
||||
methods: {
|
||||
save() {
|
||||
this.$root.api('i/update', {
|
||||
isLocked: !!this.isLocked,
|
||||
autoAcceptFollowed: !!this.autoAcceptFollowed,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
@ -1,223 +0,0 @@
|
||||
<template>
|
||||
<section class="llvierxe _card">
|
||||
<div class="_title"><fa :icon="faUser"/> {{ $t('profile') }}<small style="display: block; font-weight: normal; opacity: 0.6;">@{{ $store.state.i.username }}@{{ host }}</small></div>
|
||||
<div class="_content">
|
||||
<div class="header" :style="{ backgroundImage: $store.state.i.bannerUrl ? `url(${ $store.state.i.bannerUrl })` : null }" @click="changeBanner">
|
||||
<mk-avatar class="avatar" :user="$store.state.i" :disable-preview="true" :disable-link="true" @click.stop="changeAvatar"/>
|
||||
</div>
|
||||
|
||||
<mk-input v-model="name" :max="30">
|
||||
<span>{{ $t('_profile.name') }}</span>
|
||||
</mk-input>
|
||||
|
||||
<mk-textarea v-model="description" :max="500">
|
||||
<span>{{ $t('_profile.description') }}</span>
|
||||
<template #desc>{{ $t('_profile.youCanIncludeHashtags') }}</template>
|
||||
</mk-textarea>
|
||||
|
||||
<mk-input v-model="location">
|
||||
<span>{{ $t('location') }}</span>
|
||||
<template #prefix><fa :icon="faMapMarkerAlt"/></template>
|
||||
</mk-input>
|
||||
|
||||
<mk-input v-model="birthday" type="date">
|
||||
<template #title>{{ $t('birthday') }}</template>
|
||||
<template #prefix><fa :icon="faBirthdayCake"/></template>
|
||||
</mk-input>
|
||||
|
||||
<details class="fields">
|
||||
<summary>{{ $t('_profile.metadata') }}</summary>
|
||||
<div class="row">
|
||||
<mk-input v-model="fieldName0">{{ $t('_profile.metadataLabel') }}</mk-input>
|
||||
<mk-input v-model="fieldValue0">{{ $t('_profile.metadataContent') }}</mk-input>
|
||||
</div>
|
||||
<div class="row">
|
||||
<mk-input v-model="fieldName1">{{ $t('_profile.metadataLabel') }}</mk-input>
|
||||
<mk-input v-model="fieldValue1">{{ $t('_profile.metadataContent') }}</mk-input>
|
||||
</div>
|
||||
<div class="row">
|
||||
<mk-input v-model="fieldName2">{{ $t('_profile.metadataLabel') }}</mk-input>
|
||||
<mk-input v-model="fieldValue2">{{ $t('_profile.metadataContent') }}</mk-input>
|
||||
</div>
|
||||
<div class="row">
|
||||
<mk-input v-model="fieldName3">{{ $t('_profile.metadataLabel') }}</mk-input>
|
||||
<mk-input v-model="fieldValue3">{{ $t('_profile.metadataContent') }}</mk-input>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<mk-switch v-model="isBot">{{ $t('flagAsBot') }}</mk-switch>
|
||||
<mk-switch v-model="isCat">{{ $t('flagAsCat') }}</mk-switch>
|
||||
</div>
|
||||
<div class="_footer">
|
||||
<mk-button @click="save(true)" primary><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { faUnlockAlt, faCogs, faUser, faMapMarkerAlt, faBirthdayCake } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faSave } from '@fortawesome/free-regular-svg-icons';
|
||||
import MkButton from '../../components/ui/button.vue';
|
||||
import MkInput from '../../components/ui/input.vue';
|
||||
import MkTextarea from '../../components/ui/textarea.vue';
|
||||
import MkSwitch from '../../components/ui/switch.vue';
|
||||
import i18n from '../../i18n';
|
||||
import { host } from '../../config';
|
||||
import { selectFile } from '../../scripts/select-file';
|
||||
|
||||
export default Vue.extend({
|
||||
i18n,
|
||||
|
||||
components: {
|
||||
MkButton,
|
||||
MkInput,
|
||||
MkTextarea,
|
||||
MkSwitch,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
host,
|
||||
name: null,
|
||||
description: null,
|
||||
birthday: null,
|
||||
location: null,
|
||||
fieldName0: null,
|
||||
fieldValue0: null,
|
||||
fieldName1: null,
|
||||
fieldValue1: null,
|
||||
fieldName2: null,
|
||||
fieldValue2: null,
|
||||
fieldName3: null,
|
||||
fieldValue3: null,
|
||||
avatarId: null,
|
||||
bannerId: null,
|
||||
isBot: false,
|
||||
isCat: false,
|
||||
saving: false,
|
||||
faSave, faUnlockAlt, faCogs, faUser, faMapMarkerAlt, faBirthdayCake
|
||||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
this.name = this.$store.state.i.name;
|
||||
this.description = this.$store.state.i.description;
|
||||
this.location = this.$store.state.i.location;
|
||||
this.birthday = this.$store.state.i.birthday;
|
||||
this.avatarId = this.$store.state.i.avatarId;
|
||||
this.bannerId = this.$store.state.i.bannerId;
|
||||
this.isBot = this.$store.state.i.isBot;
|
||||
this.isCat = this.$store.state.i.isCat;
|
||||
|
||||
this.fieldName0 = this.$store.state.i.fields[0] ? this.$store.state.i.fields[0].name : null;
|
||||
this.fieldValue0 = this.$store.state.i.fields[0] ? this.$store.state.i.fields[0].value : null;
|
||||
this.fieldName1 = this.$store.state.i.fields[1] ? this.$store.state.i.fields[1].name : null;
|
||||
this.fieldValue1 = this.$store.state.i.fields[1] ? this.$store.state.i.fields[1].value : null;
|
||||
this.fieldName2 = this.$store.state.i.fields[2] ? this.$store.state.i.fields[2].name : null;
|
||||
this.fieldValue2 = this.$store.state.i.fields[2] ? this.$store.state.i.fields[2].value : null;
|
||||
this.fieldName3 = this.$store.state.i.fields[3] ? this.$store.state.i.fields[3].name : null;
|
||||
this.fieldValue3 = this.$store.state.i.fields[3] ? this.$store.state.i.fields[3].value : null;
|
||||
},
|
||||
|
||||
methods: {
|
||||
changeAvatar(e) {
|
||||
selectFile(this, e.currentTarget || e.target, this.$t('avatar')).then(file => {
|
||||
this.$root.api('i/update', {
|
||||
avatarId: file.id,
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
changeBanner(e) {
|
||||
selectFile(this, e.currentTarget || e.target, this.$t('banner')).then(file => {
|
||||
this.$root.api('i/update', {
|
||||
bannerId: file.id,
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
save(notify) {
|
||||
const fields = [
|
||||
{ name: this.fieldName0, value: this.fieldValue0 },
|
||||
{ name: this.fieldName1, value: this.fieldValue1 },
|
||||
{ name: this.fieldName2, value: this.fieldValue2 },
|
||||
{ name: this.fieldName3, value: this.fieldValue3 },
|
||||
];
|
||||
|
||||
this.saving = true;
|
||||
|
||||
this.$root.api('i/update', {
|
||||
name: this.name || null,
|
||||
description: this.description || null,
|
||||
location: this.location || null,
|
||||
birthday: this.birthday || null,
|
||||
fields,
|
||||
isBot: !!this.isBot,
|
||||
isCat: !!this.isCat,
|
||||
}).then(i => {
|
||||
this.saving = false;
|
||||
this.$store.state.i.avatarId = i.avatarId;
|
||||
this.$store.state.i.avatarUrl = i.avatarUrl;
|
||||
this.$store.state.i.bannerId = i.bannerId;
|
||||
this.$store.state.i.bannerUrl = i.bannerUrl;
|
||||
|
||||
if (notify) {
|
||||
this.$root.dialog({
|
||||
type: 'success',
|
||||
iconOnly: true, autoClose: true
|
||||
});
|
||||
}
|
||||
}).catch(err => {
|
||||
this.saving = false;
|
||||
this.$root.dialog({
|
||||
type: 'error',
|
||||
text: err.id
|
||||
});
|
||||
});
|
||||
},
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.llvierxe {
|
||||
> ._content {
|
||||
> .header {
|
||||
position: relative;
|
||||
height: 150px;
|
||||
overflow: hidden;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
border-radius: 5px;
|
||||
border: solid 1px var(--divider);
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
|
||||
> .avatar {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: block;
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
margin: auto;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 0 0 6px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
> .fields {
|
||||
> .row {
|
||||
> * {
|
||||
display: inline-block;
|
||||
width: 50%;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,62 +0,0 @@
|
||||
<template>
|
||||
<section class="_card">
|
||||
<div class="_title"><fa :icon="faLaugh"/> {{ $t('reaction') }}</div>
|
||||
<div class="_content">
|
||||
<mk-textarea v-model="reactions">{{ $t('reaction') }}<template #desc>{{ $t('reactionSettingDescription') }}</template></mk-textarea>
|
||||
</div>
|
||||
<div class="_footer">
|
||||
<mk-button @click="save()" primary inline :disabled="!changed"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
|
||||
<mk-button inline @click="preview"><fa :icon="faEye"/> {{ $t('preview') }}</mk-button>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { faLaugh, faSave, faEye } from '@fortawesome/free-regular-svg-icons';
|
||||
import MkTextarea from '../../components/ui/textarea.vue';
|
||||
import MkButton from '../../components/ui/button.vue';
|
||||
import MkReactionPicker from '../../components/reaction-picker.vue';
|
||||
import i18n from '../../i18n';
|
||||
|
||||
export default Vue.extend({
|
||||
i18n,
|
||||
|
||||
components: {
|
||||
MkTextarea,
|
||||
MkButton,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
reactions: this.$store.state.settings.reactions.join('\n'),
|
||||
changed: false,
|
||||
faLaugh, faSave, faEye
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
reactions() {
|
||||
this.changed = true;
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
save() {
|
||||
this.$store.dispatch('settings/set', { key: 'reactions', value: this.reactions.trim().split('\n') });
|
||||
this.changed = false;
|
||||
},
|
||||
|
||||
preview(ev) {
|
||||
const picker = this.$root.new(MkReactionPicker, {
|
||||
source: ev.currentTarget || ev.target,
|
||||
reactions: this.reactions.trim().split('\n'),
|
||||
showFocus: false,
|
||||
});
|
||||
picker.$once('chosen', reaction => {
|
||||
picker.close();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
@ -1,87 +0,0 @@
|
||||
<template>
|
||||
<section class="_card">
|
||||
<div class="_title"><fa :icon="faLock"/> {{ $t('password') }}</div>
|
||||
<div class="_content">
|
||||
<mk-button primary @click="change()">{{ $t('changePassword') }}</mk-button>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { faLock } from '@fortawesome/free-solid-svg-icons';
|
||||
import MkButton from '../../components/ui/button.vue';
|
||||
import i18n from '../../i18n';
|
||||
|
||||
export default Vue.extend({
|
||||
i18n,
|
||||
|
||||
components: {
|
||||
MkButton,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
faLock
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
async change() {
|
||||
const { canceled: canceled1, result: currentPassword } = await this.$root.dialog({
|
||||
title: this.$t('currentPassword'),
|
||||
input: {
|
||||
type: 'password'
|
||||
}
|
||||
});
|
||||
if (canceled1) return;
|
||||
|
||||
const { canceled: canceled2, result: newPassword } = await this.$root.dialog({
|
||||
title: this.$t('newPassword'),
|
||||
input: {
|
||||
type: 'password'
|
||||
}
|
||||
});
|
||||
if (canceled2) return;
|
||||
|
||||
const { canceled: canceled3, result: newPassword2 } = await this.$root.dialog({
|
||||
title: this.$t('newPasswordRetype'),
|
||||
input: {
|
||||
type: 'password'
|
||||
}
|
||||
});
|
||||
if (canceled3) return;
|
||||
|
||||
if (newPassword !== newPassword2) {
|
||||
this.$root.dialog({
|
||||
type: 'error',
|
||||
text: this.$t('retypedNotMatch')
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const dialog = this.$root.dialog({
|
||||
type: 'waiting',
|
||||
iconOnly: true
|
||||
});
|
||||
|
||||
this.$root.api('i/change-password', {
|
||||
currentPassword,
|
||||
newPassword
|
||||
}).then(() => {
|
||||
this.$root.dialog({
|
||||
type: 'success',
|
||||
iconOnly: true, autoClose: true
|
||||
});
|
||||
}).catch(e => {
|
||||
this.$root.dialog({
|
||||
type: 'error',
|
||||
text: e
|
||||
});
|
||||
}).finally(() => {
|
||||
dialog.close();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
@ -12,6 +12,10 @@
|
||||
</optgroup>
|
||||
</mk-select>
|
||||
</div>
|
||||
<div class="_content">
|
||||
<mk-button primary v-if="wallpaper == null" @click="setWallpaper">{{ $t('setWallpaper') }}</mk-button>
|
||||
<mk-button primary v-else @click="wallpaper = null">{{ $t('removeWallpaper') }}</mk-button>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@ -23,6 +27,7 @@ import MkButton from '../../components/ui/button.vue';
|
||||
import MkSelect from '../../components/ui/select.vue';
|
||||
import i18n from '../../i18n';
|
||||
import { Theme, builtinThemes, applyTheme } from '../../theme';
|
||||
import { selectFile } from '../../scripts/select-file';
|
||||
|
||||
export default Vue.extend({
|
||||
i18n,
|
||||
@ -35,6 +40,7 @@ export default Vue.extend({
|
||||
|
||||
data() {
|
||||
return {
|
||||
wallpaper: localStorage.getItem('wallpaper'),
|
||||
faPalette
|
||||
}
|
||||
},
|
||||
@ -65,11 +71,25 @@ export default Vue.extend({
|
||||
watch: {
|
||||
theme() {
|
||||
applyTheme(this.themes.find(x => x.id === this.theme));
|
||||
},
|
||||
|
||||
|
||||
wallpaper() {
|
||||
if (this.wallpaper == null) {
|
||||
localStorage.removeItem('wallpaper');
|
||||
} else {
|
||||
localStorage.setItem('wallpaper', this.wallpaper);
|
||||
}
|
||||
location.reload();
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
setWallpaper(e) {
|
||||
selectFile(this, e.currentTarget || e.target, null, false).then(file => {
|
||||
this.wallpaper = file.url;
|
||||
});
|
||||
},
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
Reference in New Issue
Block a user