ユーザー設定とクライアント設定を分離

This commit is contained in:
syuilo
2020-02-17 03:10:51 +09:00
parent 88f5ec59d7
commit 93d25a2a34
17 changed files with 236 additions and 209 deletions

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>