forked from mirrors/misskey
Merge branch 'develop' into pizzax-indexeddb
This commit is contained in:
@@ -1,49 +1,49 @@
|
||||
<template>
|
||||
<div>
|
||||
<MkButton v-if="!data && !$i.twoFactorEnabled" @click="register">{{ $ts._2fa.registerDevice }}</MkButton>
|
||||
<MkButton v-if="!twoFactorData && !$i.twoFactorEnabled" @click="register">{{ i18n.ts._2fa.registerDevice }}</MkButton>
|
||||
<template v-if="$i.twoFactorEnabled">
|
||||
<p>{{ $ts._2fa.alreadyRegistered }}</p>
|
||||
<MkButton @click="unregister">{{ $ts.unregister }}</MkButton>
|
||||
<p>{{ i18n.ts._2fa.alreadyRegistered }}</p>
|
||||
<MkButton @click="unregister">{{ i18n.ts.unregister }}</MkButton>
|
||||
|
||||
<template v-if="supportsCredentials">
|
||||
<hr class="totp-method-sep">
|
||||
|
||||
<h2 class="heading">{{ $ts.securityKey }}</h2>
|
||||
<p>{{ $ts._2fa.securityKeyInfo }}</p>
|
||||
<h2 class="heading">{{ i18n.ts.securityKey }}</h2>
|
||||
<p>{{ i18n.ts._2fa.securityKeyInfo }}</p>
|
||||
<div class="key-list">
|
||||
<div v-for="key in $i.securityKeysList" class="key">
|
||||
<h3>{{ key.name }}</h3>
|
||||
<div class="last-used">{{ $ts.lastUsed }}<MkTime :time="key.lastUsed"/></div>
|
||||
<MkButton @click="unregisterKey(key)">{{ $ts.unregister }}</MkButton>
|
||||
<div class="last-used">{{ i18n.ts.lastUsed }}<MkTime :time="key.lastUsed"/></div>
|
||||
<MkButton @click="unregisterKey(key)">{{ i18n.ts.unregister }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<MkSwitch v-if="$i.securityKeysList.length > 0" v-model="usePasswordLessLogin" @update:modelValue="updatePasswordLessLogin">{{ $ts.passwordLessLogin }}</MkSwitch>
|
||||
<MkSwitch v-if="$i.securityKeysList.length > 0" v-model="usePasswordLessLogin" @update:modelValue="updatePasswordLessLogin">{{ i18n.ts.passwordLessLogin }}</MkSwitch>
|
||||
|
||||
<MkInfo v-if="registration && registration.error" warn>{{ $ts.error }} {{ registration.error }}</MkInfo>
|
||||
<MkButton v-if="!registration || registration.error" @click="addSecurityKey">{{ $ts._2fa.registerKey }}</MkButton>
|
||||
<MkInfo v-if="registration && registration.error" warn>{{ i18n.ts.error }} {{ registration.error }}</MkInfo>
|
||||
<MkButton v-if="!registration || registration.error" @click="addSecurityKey">{{ i18n.ts._2fa.registerKey }}</MkButton>
|
||||
|
||||
<ol v-if="registration && !registration.error">
|
||||
<li v-if="registration.stage >= 0">
|
||||
{{ $ts.tapSecurityKey }}
|
||||
{{ i18n.ts.tapSecurityKey }}
|
||||
<i v-if="registration.saving && registration.stage == 0" class="fas fa-spinner fa-pulse fa-fw"></i>
|
||||
</li>
|
||||
<li v-if="registration.stage >= 1">
|
||||
<MkForm :disabled="registration.stage != 1 || registration.saving">
|
||||
<MkInput v-model="keyName" :max="30">
|
||||
<template #label>{{ $ts.securityKeyName }}</template>
|
||||
<template #label>{{ i18n.ts.securityKeyName }}</template>
|
||||
</MkInput>
|
||||
<MkButton :disabled="keyName.length == 0" @click="registerKey">{{ $ts.registerSecurityKey }}</MkButton>
|
||||
<MkButton :disabled="keyName.length == 0" @click="registerKey">{{ i18n.ts.registerSecurityKey }}</MkButton>
|
||||
<i v-if="registration.saving && registration.stage == 1" class="fas fa-spinner fa-pulse fa-fw"></i>
|
||||
</MkForm>
|
||||
</li>
|
||||
</ol>
|
||||
</template>
|
||||
</template>
|
||||
<div v-if="data && !$i.twoFactorEnabled">
|
||||
<div v-if="twoFactorData && !$i.twoFactorEnabled">
|
||||
<ol style="margin: 0; padding: 0 0 0 1em;">
|
||||
<li>
|
||||
<I18n :src="$ts._2fa.step1" tag="span">
|
||||
<I18n :src="i18n.ts._2fa.step1" tag="span">
|
||||
<template #a>
|
||||
<a href="https://authy.com/" rel="noopener" target="_blank" class="_link">Authy</a>
|
||||
</template>
|
||||
@@ -52,19 +52,20 @@
|
||||
</template>
|
||||
</I18n>
|
||||
</li>
|
||||
<li>{{ $ts._2fa.step2 }}<br><img :src="data.qr"></li>
|
||||
<li>{{ $ts._2fa.step3 }}<br>
|
||||
<MkInput v-model="token" type="text" pattern="^[0-9]{6}$" autocomplete="off" spellcheck="false"><template #label>{{ $ts.token }}</template></MkInput>
|
||||
<MkButton primary @click="submit">{{ $ts.done }}</MkButton>
|
||||
<li>{{ i18n.ts._2fa.step2 }}<br><img :src="twoFactorData.qr"><p>{{ $ts._2fa.step2Url }}<br>{{ twoFactorData.url }}</p></li>
|
||||
<li>
|
||||
{{ i18n.ts._2fa.step3 }}<br>
|
||||
<MkInput v-model="token" type="text" pattern="^[0-9]{6}$" autocomplete="off" spellcheck="false"><template #label>{{ i18n.ts.token }}</template></MkInput>
|
||||
<MkButton primary @click="submit">{{ i18n.ts.done }}</MkButton>
|
||||
</li>
|
||||
</ol>
|
||||
<MkInfo>{{ $ts._2fa.step4 }}</MkInfo>
|
||||
<MkInfo>{{ i18n.ts._2fa.step4 }}</MkInfo>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { hostname } from '@/config';
|
||||
import { byteify, hexify, stringify } from '@/scripts/2fa';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
@@ -72,155 +73,144 @@ import MkInfo from '@/components/ui/info.vue';
|
||||
import MkInput from '@/components/form/input.vue';
|
||||
import MkSwitch from '@/components/form/switch.vue';
|
||||
import * as os from '@/os';
|
||||
import * as symbols from '@/symbols';
|
||||
import { $i } from '@/account';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkButton, MkInfo, MkInput, MkSwitch
|
||||
},
|
||||
const twoFactorData = ref<any>(null);
|
||||
const supportsCredentials = ref(!!navigator.credentials);
|
||||
const usePasswordLessLogin = ref($i!.usePasswordLessLogin);
|
||||
const registration = ref<any>(null);
|
||||
const keyName = ref('');
|
||||
const token = ref(null);
|
||||
|
||||
data() {
|
||||
return {
|
||||
data: null,
|
||||
supportsCredentials: !!navigator.credentials,
|
||||
usePasswordLessLogin: this.$i.usePasswordLessLogin,
|
||||
registration: null,
|
||||
keyName: '',
|
||||
token: null,
|
||||
};
|
||||
},
|
||||
function register() {
|
||||
os.inputText({
|
||||
title: i18n.ts.password,
|
||||
type: 'password'
|
||||
}).then(({ canceled, result: password }) => {
|
||||
if (canceled) return;
|
||||
os.api('i/2fa/register', {
|
||||
password: password
|
||||
}).then(data => {
|
||||
twoFactorData.value = data;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
methods: {
|
||||
register() {
|
||||
os.inputText({
|
||||
title: this.$ts.password,
|
||||
type: 'password'
|
||||
}).then(({ canceled, result: password }) => {
|
||||
if (canceled) return;
|
||||
os.api('i/2fa/register', {
|
||||
password: password
|
||||
}).then(data => {
|
||||
this.data = data;
|
||||
});
|
||||
function unregister() {
|
||||
os.inputText({
|
||||
title: i18n.ts.password,
|
||||
type: 'password'
|
||||
}).then(({ canceled, result: password }) => {
|
||||
if (canceled) return;
|
||||
os.api('i/2fa/unregister', {
|
||||
password: password
|
||||
}).then(() => {
|
||||
usePasswordLessLogin.value = false;
|
||||
updatePasswordLessLogin();
|
||||
}).then(() => {
|
||||
os.success();
|
||||
$i!.twoFactorEnabled = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function submit() {
|
||||
os.api('i/2fa/done', {
|
||||
token: token.value
|
||||
}).then(() => {
|
||||
os.success();
|
||||
$i!.twoFactorEnabled = true;
|
||||
}).catch(err => {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: err,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function registerKey() {
|
||||
registration.value.saving = true;
|
||||
os.api('i/2fa/key-done', {
|
||||
password: registration.value.password,
|
||||
name: keyName.value,
|
||||
challengeId: registration.value.challengeId,
|
||||
// we convert each 16 bits to a string to serialise
|
||||
clientDataJSON: stringify(registration.value.credential.response.clientDataJSON),
|
||||
attestationObject: hexify(registration.value.credential.response.attestationObject)
|
||||
}).then(key => {
|
||||
registration.value = null;
|
||||
key.lastUsed = new Date();
|
||||
os.success();
|
||||
})
|
||||
}
|
||||
|
||||
function unregisterKey(key) {
|
||||
os.inputText({
|
||||
title: i18n.ts.password,
|
||||
type: 'password'
|
||||
}).then(({ canceled, result: password }) => {
|
||||
if (canceled) return;
|
||||
return os.api('i/2fa/remove-key', {
|
||||
password,
|
||||
credentialId: key.id
|
||||
}).then(() => {
|
||||
usePasswordLessLogin.value = false;
|
||||
updatePasswordLessLogin();
|
||||
}).then(() => {
|
||||
os.success();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function addSecurityKey() {
|
||||
os.inputText({
|
||||
title: i18n.ts.password,
|
||||
type: 'password'
|
||||
}).then(({ canceled, result: password }) => {
|
||||
if (canceled) return;
|
||||
os.api('i/2fa/register-key', {
|
||||
password
|
||||
}).then(reg => {
|
||||
registration.value = {
|
||||
password,
|
||||
challengeId: reg!.challengeId,
|
||||
stage: 0,
|
||||
publicKeyOptions: {
|
||||
challenge: byteify(reg!.challenge, 'base64'),
|
||||
rp: {
|
||||
id: hostname,
|
||||
name: 'Misskey'
|
||||
},
|
||||
user: {
|
||||
id: byteify($i!.id, 'ascii'),
|
||||
name: $i!.username,
|
||||
displayName: $i!.name,
|
||||
},
|
||||
pubKeyCredParams: [{ alg: -7, type: 'public-key' }],
|
||||
timeout: 60000,
|
||||
attestation: 'direct'
|
||||
},
|
||||
saving: true
|
||||
};
|
||||
return navigator.credentials.create({
|
||||
publicKey: registration.value.publicKeyOptions
|
||||
});
|
||||
},
|
||||
}).then(credential => {
|
||||
registration.value.credential = credential;
|
||||
registration.value.saving = false;
|
||||
registration.value.stage = 1;
|
||||
}).catch(err => {
|
||||
console.warn('Error while registering?', err);
|
||||
registration.value.error = err.message;
|
||||
registration.value.stage = -1;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
unregister() {
|
||||
os.inputText({
|
||||
title: this.$ts.password,
|
||||
type: 'password'
|
||||
}).then(({ canceled, result: password }) => {
|
||||
if (canceled) return;
|
||||
os.api('i/2fa/unregister', {
|
||||
password: password
|
||||
}).then(() => {
|
||||
this.usePasswordLessLogin = false;
|
||||
this.updatePasswordLessLogin();
|
||||
}).then(() => {
|
||||
os.success();
|
||||
this.$i.twoFactorEnabled = false;
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
submit() {
|
||||
os.api('i/2fa/done', {
|
||||
token: this.token
|
||||
}).then(() => {
|
||||
os.success();
|
||||
this.$i.twoFactorEnabled = true;
|
||||
}).catch(e => {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: e
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
registerKey() {
|
||||
this.registration.saving = true;
|
||||
os.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: stringify(this.registration.credential.response.clientDataJSON),
|
||||
attestationObject: hexify(this.registration.credential.response.attestationObject)
|
||||
}).then(key => {
|
||||
this.registration = null;
|
||||
key.lastUsed = new Date();
|
||||
os.success();
|
||||
})
|
||||
},
|
||||
|
||||
unregisterKey(key) {
|
||||
os.inputText({
|
||||
title: this.$ts.password,
|
||||
type: 'password'
|
||||
}).then(({ canceled, result: password }) => {
|
||||
if (canceled) return;
|
||||
return os.api('i/2fa/remove-key', {
|
||||
password,
|
||||
credentialId: key.id
|
||||
}).then(() => {
|
||||
this.usePasswordLessLogin = false;
|
||||
this.updatePasswordLessLogin();
|
||||
}).then(() => {
|
||||
os.success();
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
addSecurityKey() {
|
||||
os.inputText({
|
||||
title: this.$ts.password,
|
||||
type: 'password'
|
||||
}).then(({ canceled, result: password }) => {
|
||||
if (canceled) return;
|
||||
os.api('i/2fa/register-key', {
|
||||
password
|
||||
}).then(registration => {
|
||||
this.registration = {
|
||||
password,
|
||||
challengeId: registration.challengeId,
|
||||
stage: 0,
|
||||
publicKeyOptions: {
|
||||
challenge: byteify(registration.challenge, 'base64'),
|
||||
rp: {
|
||||
id: hostname,
|
||||
name: 'Misskey'
|
||||
},
|
||||
user: {
|
||||
id: byteify(this.$i.id, 'ascii'),
|
||||
name: this.$i.username,
|
||||
displayName: this.$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() {
|
||||
os.api('i/2fa/password-less', {
|
||||
value: !!this.usePasswordLessLogin
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
async function updatePasswordLessLogin() {
|
||||
await os.api('i/2fa/password-less', {
|
||||
value: !!usePasswordLessLogin.value
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -7,163 +7,150 @@
|
||||
|
||||
<FormSection>
|
||||
<MkKeyValue>
|
||||
<template #key>{{ $ts.registeredDate }}</template>
|
||||
<template #key>{{ i18n.ts.registeredDate }}</template>
|
||||
<template #value><MkTime :time="$i.createdAt" mode="detail"/></template>
|
||||
</MkKeyValue>
|
||||
</FormSection>
|
||||
|
||||
<FormSection v-if="stats">
|
||||
<template #label>{{ $ts.statistics }}</template>
|
||||
<template #label>{{ i18n.ts.statistics }}</template>
|
||||
<MkKeyValue oneline style="margin: 1em 0;">
|
||||
<template #key>{{ $ts.notesCount }}</template>
|
||||
<template #key>{{ i18n.ts.notesCount }}</template>
|
||||
<template #value>{{ number(stats.notesCount) }}</template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue oneline style="margin: 1em 0;">
|
||||
<template #key>{{ $ts.repliesCount }}</template>
|
||||
<template #key>{{ i18n.ts.repliesCount }}</template>
|
||||
<template #value>{{ number(stats.repliesCount) }}</template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue oneline style="margin: 1em 0;">
|
||||
<template #key>{{ $ts.renotesCount }}</template>
|
||||
<template #key>{{ i18n.ts.renotesCount }}</template>
|
||||
<template #value>{{ number(stats.renotesCount) }}</template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue oneline style="margin: 1em 0;">
|
||||
<template #key>{{ $ts.repliedCount }}</template>
|
||||
<template #key>{{ i18n.ts.repliedCount }}</template>
|
||||
<template #value>{{ number(stats.repliedCount) }}</template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue oneline style="margin: 1em 0;">
|
||||
<template #key>{{ $ts.renotedCount }}</template>
|
||||
<template #key>{{ i18n.ts.renotedCount }}</template>
|
||||
<template #value>{{ number(stats.renotedCount) }}</template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue oneline style="margin: 1em 0;">
|
||||
<template #key>{{ $ts.pollVotesCount }}</template>
|
||||
<template #key>{{ i18n.ts.pollVotesCount }}</template>
|
||||
<template #value>{{ number(stats.pollVotesCount) }}</template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue oneline style="margin: 1em 0;">
|
||||
<template #key>{{ $ts.pollVotedCount }}</template>
|
||||
<template #key>{{ i18n.ts.pollVotedCount }}</template>
|
||||
<template #value>{{ number(stats.pollVotedCount) }}</template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue oneline style="margin: 1em 0;">
|
||||
<template #key>{{ $ts.sentReactionsCount }}</template>
|
||||
<template #key>{{ i18n.ts.sentReactionsCount }}</template>
|
||||
<template #value>{{ number(stats.sentReactionsCount) }}</template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue oneline style="margin: 1em 0;">
|
||||
<template #key>{{ $ts.receivedReactionsCount }}</template>
|
||||
<template #key>{{ i18n.ts.receivedReactionsCount }}</template>
|
||||
<template #value>{{ number(stats.receivedReactionsCount) }}</template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue oneline style="margin: 1em 0;">
|
||||
<template #key>{{ $ts.noteFavoritesCount }}</template>
|
||||
<template #key>{{ i18n.ts.noteFavoritesCount }}</template>
|
||||
<template #value>{{ number(stats.noteFavoritesCount) }}</template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue oneline style="margin: 1em 0;">
|
||||
<template #key>{{ $ts.followingCount }}</template>
|
||||
<template #key>{{ i18n.ts.followingCount }}</template>
|
||||
<template #value>{{ number(stats.followingCount) }}</template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue oneline style="margin: 1em 0;">
|
||||
<template #key>{{ $ts.followingCount }} ({{ $ts.local }})</template>
|
||||
<template #key>{{ i18n.ts.followingCount }} ({{ i18n.ts.local }})</template>
|
||||
<template #value>{{ number(stats.localFollowingCount) }}</template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue oneline style="margin: 1em 0;">
|
||||
<template #key>{{ $ts.followingCount }} ({{ $ts.remote }})</template>
|
||||
<template #key>{{ i18n.ts.followingCount }} ({{ i18n.ts.remote }})</template>
|
||||
<template #value>{{ number(stats.remoteFollowingCount) }}</template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue oneline style="margin: 1em 0;">
|
||||
<template #key>{{ $ts.followersCount }}</template>
|
||||
<template #key>{{ i18n.ts.followersCount }}</template>
|
||||
<template #value>{{ number(stats.followersCount) }}</template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue oneline style="margin: 1em 0;">
|
||||
<template #key>{{ $ts.followersCount }} ({{ $ts.local }})</template>
|
||||
<template #key>{{ i18n.ts.followersCount }} ({{ i18n.ts.local }})</template>
|
||||
<template #value>{{ number(stats.localFollowersCount) }}</template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue oneline style="margin: 1em 0;">
|
||||
<template #key>{{ $ts.followersCount }} ({{ $ts.remote }})</template>
|
||||
<template #key>{{ i18n.ts.followersCount }} ({{ i18n.ts.remote }})</template>
|
||||
<template #value>{{ number(stats.remoteFollowersCount) }}</template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue oneline style="margin: 1em 0;">
|
||||
<template #key>{{ $ts.pageLikesCount }}</template>
|
||||
<template #key>{{ i18n.ts.pageLikesCount }}</template>
|
||||
<template #value>{{ number(stats.pageLikesCount) }}</template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue oneline style="margin: 1em 0;">
|
||||
<template #key>{{ $ts.pageLikedCount }}</template>
|
||||
<template #key>{{ i18n.ts.pageLikedCount }}</template>
|
||||
<template #value>{{ number(stats.pageLikedCount) }}</template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue oneline style="margin: 1em 0;">
|
||||
<template #key>{{ $ts.driveFilesCount }}</template>
|
||||
<template #key>{{ i18n.ts.driveFilesCount }}</template>
|
||||
<template #value>{{ number(stats.driveFilesCount) }}</template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue oneline style="margin: 1em 0;">
|
||||
<template #key>{{ $ts.driveUsage }}</template>
|
||||
<template #key>{{ i18n.ts.driveUsage }}</template>
|
||||
<template #value>{{ bytes(stats.driveUsage) }}</template>
|
||||
</MkKeyValue>
|
||||
</FormSection>
|
||||
|
||||
<FormSection>
|
||||
<template #label>{{ $ts.other }}</template>
|
||||
<template #label>{{ i18n.ts.other }}</template>
|
||||
<MkKeyValue oneline style="margin: 1em 0;">
|
||||
<template #key>emailVerified</template>
|
||||
<template #value>{{ $i.emailVerified ? $ts.yes : $ts.no }}</template>
|
||||
<template #value>{{ $i.emailVerified ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue oneline style="margin: 1em 0;">
|
||||
<template #key>twoFactorEnabled</template>
|
||||
<template #value>{{ $i.twoFactorEnabled ? $ts.yes : $ts.no }}</template>
|
||||
<template #value>{{ $i.twoFactorEnabled ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue oneline style="margin: 1em 0;">
|
||||
<template #key>securityKeys</template>
|
||||
<template #value>{{ $i.securityKeys ? $ts.yes : $ts.no }}</template>
|
||||
<template #value>{{ $i.securityKeys ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue oneline style="margin: 1em 0;">
|
||||
<template #key>usePasswordLessLogin</template>
|
||||
<template #value>{{ $i.usePasswordLessLogin ? $ts.yes : $ts.no }}</template>
|
||||
<template #value>{{ $i.usePasswordLessLogin ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue oneline style="margin: 1em 0;">
|
||||
<template #key>isModerator</template>
|
||||
<template #value>{{ $i.isModerator ? $ts.yes : $ts.no }}</template>
|
||||
<template #value>{{ $i.isModerator ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue oneline style="margin: 1em 0;">
|
||||
<template #key>isAdmin</template>
|
||||
<template #value>{{ $i.isAdmin ? $ts.yes : $ts.no }}</template>
|
||||
<template #value>{{ $i.isAdmin ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
</MkKeyValue>
|
||||
</FormSection>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineAsyncComponent, defineComponent } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { defineExpose, onMounted, ref } from 'vue';
|
||||
import FormSection from '@/components/form/section.vue';
|
||||
import MkKeyValue from '@/components/key-value.vue';
|
||||
import * as os from '@/os';
|
||||
import number from '@/filters/number';
|
||||
import bytes from '@/filters/bytes';
|
||||
import * as symbols from '@/symbols';
|
||||
import { $i } from '@/account';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
FormSection,
|
||||
MkKeyValue,
|
||||
},
|
||||
const stats = ref<any>({});
|
||||
|
||||
emits: ['info'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: this.$ts.accountInfo,
|
||||
icon: 'fas fa-info-circle'
|
||||
},
|
||||
stats: null
|
||||
}
|
||||
},
|
||||
onMounted(() => {
|
||||
os.api('users/stats', {
|
||||
userId: $i!.id
|
||||
}).then(response => {
|
||||
stats.value = response;
|
||||
});
|
||||
});
|
||||
|
||||
mounted() {
|
||||
os.api('users/stats', {
|
||||
userId: this.$i.id
|
||||
}).then(stats => {
|
||||
this.stats = stats;
|
||||
});
|
||||
},
|
||||
|
||||
methods: {
|
||||
number,
|
||||
bytes,
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: i18n.ts.accountInfo,
|
||||
icon: 'fas fa-info-circle'
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="_formRoot">
|
||||
<FormSuspense :p="init">
|
||||
<FormButton primary @click="addAccount"><i class="fas fa-plus"></i> {{ $ts.addAccount }}</FormButton>
|
||||
<FormButton primary @click="addAccount"><i class="fas fa-plus"></i> {{ i18n.ts.addAccount }}</FormButton>
|
||||
|
||||
<div v-for="account in accounts" :key="account.id" class="_panel _button lcjjdxlm" @click="menu(account, $event)">
|
||||
<div class="avatar">
|
||||
@@ -20,90 +20,89 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { defineAsyncComponent, defineExpose, ref } from 'vue';
|
||||
import FormSuspense from '@/components/form/suspense.vue';
|
||||
import FormButton from '@/components/ui/button.vue';
|
||||
import * as os from '@/os';
|
||||
import * as symbols from '@/symbols';
|
||||
import { getAccounts, addAccount, login } from '@/account';
|
||||
import { getAccounts, addAccount as addAccounts, login, $i } from '@/account';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
FormSuspense,
|
||||
FormButton,
|
||||
},
|
||||
const storedAccounts = ref<any>(null);
|
||||
const accounts = ref<any>(null);
|
||||
|
||||
emits: ['info'],
|
||||
const init = async () => {
|
||||
getAccounts().then(accounts => {
|
||||
storedAccounts.value = accounts.filter(x => x.id !== $i!.id);
|
||||
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: this.$ts.accounts,
|
||||
icon: 'fas fa-users',
|
||||
bg: 'var(--bg)',
|
||||
},
|
||||
storedAccounts: getAccounts().then(accounts => accounts.filter(x => x.id !== this.$i.id)),
|
||||
accounts: null,
|
||||
init: async () => os.api('users/show', {
|
||||
userIds: (await this.storedAccounts).map(x => x.id)
|
||||
}).then(accounts => {
|
||||
this.accounts = accounts;
|
||||
}),
|
||||
};
|
||||
},
|
||||
console.log(storedAccounts.value);
|
||||
|
||||
methods: {
|
||||
menu(account, ev) {
|
||||
os.popupMenu([{
|
||||
text: this.$ts.switch,
|
||||
icon: 'fas fa-exchange-alt',
|
||||
action: () => this.switchAccount(account),
|
||||
}, {
|
||||
text: this.$ts.remove,
|
||||
icon: 'fas fa-trash-alt',
|
||||
danger: true,
|
||||
action: () => this.removeAccount(account),
|
||||
}], ev.currentTarget ?? ev.target);
|
||||
return os.api('users/show', {
|
||||
userIds: storedAccounts.value.map(x => x.id)
|
||||
});
|
||||
}).then(response => {
|
||||
accounts.value = response;
|
||||
console.log(accounts.value);
|
||||
});
|
||||
}
|
||||
|
||||
function menu(account, ev) {
|
||||
os.popupMenu([{
|
||||
text: i18n.ts.switch,
|
||||
icon: 'fas fa-exchange-alt',
|
||||
action: () => switchAccount(account),
|
||||
}, {
|
||||
text: i18n.ts.remove,
|
||||
icon: 'fas fa-trash-alt',
|
||||
danger: true,
|
||||
action: () => removeAccount(account),
|
||||
}], ev.currentTarget ?? ev.target);
|
||||
}
|
||||
|
||||
function addAccount(ev) {
|
||||
os.popupMenu([{
|
||||
text: i18n.ts.existingAccount,
|
||||
action: () => { addExistingAccount(); },
|
||||
}, {
|
||||
text: i18n.ts.createAccount,
|
||||
action: () => { createAccount(); },
|
||||
}], ev.currentTarget ?? ev.target);
|
||||
}
|
||||
|
||||
function addExistingAccount() {
|
||||
os.popup(defineAsyncComponent(() => import('@/components/signin-dialog.vue')), {}, {
|
||||
done: res => {
|
||||
addAccounts(res.id, res.i);
|
||||
os.success();
|
||||
},
|
||||
}, 'closed');
|
||||
}
|
||||
|
||||
addAccount(ev) {
|
||||
os.popupMenu([{
|
||||
text: this.$ts.existingAccount,
|
||||
action: () => { this.addExistingAccount(); },
|
||||
}, {
|
||||
text: this.$ts.createAccount,
|
||||
action: () => { this.createAccount(); },
|
||||
}], ev.currentTarget ?? ev.target);
|
||||
function createAccount() {
|
||||
os.popup(defineAsyncComponent(() => import('@/components/signup-dialog.vue')), {}, {
|
||||
done: res => {
|
||||
addAccounts(res.id, res.i);
|
||||
switchAccountWithToken(res.i);
|
||||
},
|
||||
}, 'closed');
|
||||
}
|
||||
|
||||
addExistingAccount() {
|
||||
os.popup(import('@/components/signin-dialog.vue'), {}, {
|
||||
done: res => {
|
||||
addAccount(res.id, res.i);
|
||||
os.success();
|
||||
},
|
||||
}, 'closed');
|
||||
},
|
||||
async function switchAccount(account: any) {
|
||||
const fetchedAccounts: any[] = await getAccounts();
|
||||
const token = fetchedAccounts.find(x => x.id === account.id).token;
|
||||
switchAccountWithToken(token);
|
||||
}
|
||||
|
||||
createAccount() {
|
||||
os.popup(import('@/components/signup-dialog.vue'), {}, {
|
||||
done: res => {
|
||||
addAccount(res.id, res.i);
|
||||
this.switchAccountWithToken(res.i);
|
||||
},
|
||||
}, 'closed');
|
||||
},
|
||||
function switchAccountWithToken(token: string) {
|
||||
login(token);
|
||||
}
|
||||
|
||||
async switchAccount(account: any) {
|
||||
const storedAccounts = await getAccounts();
|
||||
const token = storedAccounts.find(x => x.id === account.id).token;
|
||||
this.switchAccountWithToken(token);
|
||||
},
|
||||
|
||||
switchAccountWithToken(token: string) {
|
||||
login(token);
|
||||
},
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: i18n.ts.accounts,
|
||||
icon: 'fas fa-users',
|
||||
bg: 'var(--bg)',
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,56 +1,45 @@
|
||||
<template>
|
||||
<div class="_formRoot">
|
||||
<FormButton primary class="_formBlock" @click="generateToken">{{ $ts.generateAccessToken }}</FormButton>
|
||||
<FormLink to="/settings/apps" class="_formBlock">{{ $ts.manageAccessTokens }}</FormLink>
|
||||
<FormButton primary class="_formBlock" @click="generateToken">{{ i18n.ts.generateAccessToken }}</FormButton>
|
||||
<FormLink to="/settings/apps" class="_formBlock">{{ i18n.ts.manageAccessTokens }}</FormLink>
|
||||
<FormLink to="/api-console" :behavior="isDesktop ? 'window' : null" class="_formBlock">API console</FormLink>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { defineAsyncComponent, defineExpose, ref } from 'vue';
|
||||
import FormLink from '@/components/form/link.vue';
|
||||
import FormButton from '@/components/ui/button.vue';
|
||||
import * as os from '@/os';
|
||||
import * as symbols from '@/symbols';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
FormButton,
|
||||
FormLink,
|
||||
},
|
||||
const isDesktop = ref(window.innerWidth >= 1100);
|
||||
|
||||
emits: ['info'],
|
||||
function generateToken() {
|
||||
os.popup(defineAsyncComponent(() => import('@/components/token-generate-window.vue')), {}, {
|
||||
done: async result => {
|
||||
const { name, permissions } = result;
|
||||
const { token } = await os.api('miauth/gen-token', {
|
||||
session: null,
|
||||
name: name,
|
||||
permission: permissions,
|
||||
});
|
||||
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: 'API',
|
||||
icon: 'fas fa-key',
|
||||
bg: 'var(--bg)',
|
||||
},
|
||||
isDesktop: window.innerWidth >= 1100,
|
||||
};
|
||||
},
|
||||
|
||||
methods: {
|
||||
generateToken() {
|
||||
os.popup(import('@/components/token-generate-window.vue'), {}, {
|
||||
done: async result => {
|
||||
const { name, permissions } = result;
|
||||
const { token } = await os.api('miauth/gen-token', {
|
||||
session: null,
|
||||
name: name,
|
||||
permission: permissions,
|
||||
});
|
||||
|
||||
os.alert({
|
||||
type: 'success',
|
||||
title: this.$ts.token,
|
||||
text: token
|
||||
});
|
||||
},
|
||||
}, 'closed');
|
||||
os.alert({
|
||||
type: 'success',
|
||||
title: i18n.ts.token,
|
||||
text: token
|
||||
});
|
||||
},
|
||||
}, 'closed');
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: 'API',
|
||||
icon: 'fas fa-key',
|
||||
bg: 'var(--bg)',
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<template #empty>
|
||||
<div class="_fullinfo">
|
||||
<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
|
||||
<div>{{ $ts.nothing }}</div>
|
||||
<div>{{ i18n.ts.nothing }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot="{items}">
|
||||
@@ -14,18 +14,18 @@
|
||||
<div class="name">{{ token.name }}</div>
|
||||
<div class="description">{{ token.description }}</div>
|
||||
<div class="_keyValue">
|
||||
<div>{{ $ts.installedDate }}:</div>
|
||||
<div>{{ i18n.ts.installedDate }}:</div>
|
||||
<div><MkTime :time="token.createdAt"/></div>
|
||||
</div>
|
||||
<div class="_keyValue">
|
||||
<div>{{ $ts.lastUsedDate }}:</div>
|
||||
<div>{{ i18n.ts.lastUsedDate }}:</div>
|
||||
<div><MkTime :time="token.lastUsedAt"/></div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="_button" @click="revoke(token)"><i class="fas fa-trash-alt"></i></button>
|
||||
</div>
|
||||
<details>
|
||||
<summary>{{ $ts.details }}</summary>
|
||||
<summary>{{ i18n.ts.details }}</summary>
|
||||
<ul>
|
||||
<li v-for="p in token.permission" :key="p">{{ $t(`_permissions.${p}`) }}</li>
|
||||
</ul>
|
||||
@@ -37,42 +37,34 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { defineExpose, ref } from 'vue';
|
||||
import FormPagination from '@/components/ui/pagination.vue';
|
||||
import * as os from '@/os';
|
||||
import * as symbols from '@/symbols';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
FormPagination,
|
||||
},
|
||||
const list = ref<any>(null);
|
||||
|
||||
emits: ['info'],
|
||||
const pagination = {
|
||||
endpoint: 'i/apps' as const,
|
||||
limit: 100,
|
||||
params: {
|
||||
sort: '+lastUsedAt'
|
||||
}
|
||||
}
|
||||
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: this.$ts.installedApps,
|
||||
icon: 'fas fa-plug',
|
||||
bg: 'var(--bg)',
|
||||
},
|
||||
pagination: {
|
||||
endpoint: 'i/apps' as const,
|
||||
limit: 100,
|
||||
params: {
|
||||
sort: '+lastUsedAt'
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
function revoke(token) {
|
||||
os.api('i/revoke-token', { tokenId: token.id }).then(() => {
|
||||
list.value.reload();
|
||||
});
|
||||
}
|
||||
|
||||
methods: {
|
||||
revoke(token) {
|
||||
os.api('i/revoke-token', { tokenId: token.id }).then(() => {
|
||||
this.$refs.list.reload();
|
||||
});
|
||||
}
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: i18n.ts.installedApps,
|
||||
icon: 'fas fa-plug',
|
||||
bg: 'var(--bg)',
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="_formRoot">
|
||||
<FormInfo warn class="_formBlock">{{ $ts.customCssWarn }}</FormInfo>
|
||||
<FormInfo warn class="_formBlock">{{ i18n.ts.customCssWarn }}</FormInfo>
|
||||
|
||||
<FormTextarea v-model="localCustomCss" manual-save tall class="_monospace _formBlock" style="tab-size: 2;">
|
||||
<template #label>CSS</template>
|
||||
@@ -8,50 +8,38 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { defineExpose, ref, watch } from 'vue';
|
||||
import FormTextarea from '@/components/form/textarea.vue';
|
||||
import FormInfo from '@/components/ui/info.vue';
|
||||
import * as os from '@/os';
|
||||
import { unisonReload } from '@/scripts/unison-reload';
|
||||
import * as symbols from '@/symbols';
|
||||
import { defaultStore } from '@/store';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
FormTextarea,
|
||||
FormInfo,
|
||||
},
|
||||
const localCustomCss = ref(localStorage.getItem('customCss') ?? '');
|
||||
|
||||
emits: ['info'],
|
||||
async function apply() {
|
||||
localStorage.setItem('customCss', localCustomCss.value);
|
||||
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: this.$ts.customCss,
|
||||
icon: 'fas fa-code',
|
||||
bg: 'var(--bg)',
|
||||
},
|
||||
localCustomCss: localStorage.getItem('customCss')
|
||||
}
|
||||
},
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'info',
|
||||
text: i18n.ts.reloadToApplySetting,
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
mounted() {
|
||||
this.$watch('localCustomCss', this.apply);
|
||||
},
|
||||
unisonReload();
|
||||
}
|
||||
|
||||
methods: {
|
||||
async apply() {
|
||||
localStorage.setItem('customCss', this.localCustomCss);
|
||||
watch(localCustomCss, async () => {
|
||||
await apply();
|
||||
});
|
||||
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'info',
|
||||
text: this.$ts.reloadToApplySetting,
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
unisonReload();
|
||||
}
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: i18n.ts.customCss,
|
||||
icon: 'fas fa-code',
|
||||
bg: 'var(--bg)',
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,36 +1,36 @@
|
||||
<template>
|
||||
<div class="_formRoot">
|
||||
<FormGroup>
|
||||
<template #label>{{ $ts.defaultNavigationBehaviour }}</template>
|
||||
<FormSwitch v-model="navWindow">{{ $ts.openInWindow }}</FormSwitch>
|
||||
<template #label>{{ i18n.ts.defaultNavigationBehaviour }}</template>
|
||||
<FormSwitch v-model="navWindow">{{ i18n.ts.openInWindow }}</FormSwitch>
|
||||
</FormGroup>
|
||||
|
||||
<FormSwitch v-model="alwaysShowMainColumn" class="_formBlock">{{ $ts._deck.alwaysShowMainColumn }}</FormSwitch>
|
||||
<FormSwitch v-model="alwaysShowMainColumn" class="_formBlock">{{ i18n.ts._deck.alwaysShowMainColumn }}</FormSwitch>
|
||||
|
||||
<FormRadios v-model="columnAlign" class="_formBlock">
|
||||
<template #label>{{ $ts._deck.columnAlign }}</template>
|
||||
<option value="left">{{ $ts.left }}</option>
|
||||
<option value="center">{{ $ts.center }}</option>
|
||||
<template #label>{{ i18n.ts._deck.columnAlign }}</template>
|
||||
<option value="left">{{ i18n.ts.left }}</option>
|
||||
<option value="center">{{ i18n.ts.center }}</option>
|
||||
</FormRadios>
|
||||
|
||||
<FormRadios v-model="columnHeaderHeight" class="_formBlock">
|
||||
<template #label>{{ $ts._deck.columnHeaderHeight }}</template>
|
||||
<option :value="42">{{ $ts.narrow }}</option>
|
||||
<option :value="45">{{ $ts.medium }}</option>
|
||||
<option :value="48">{{ $ts.wide }}</option>
|
||||
<template #label>{{ i18n.ts._deck.columnHeaderHeight }}</template>
|
||||
<option :value="42">{{ i18n.ts.narrow }}</option>
|
||||
<option :value="45">{{ i18n.ts.medium }}</option>
|
||||
<option :value="48">{{ i18n.ts.wide }}</option>
|
||||
</FormRadios>
|
||||
|
||||
<FormInput v-model="columnMargin" type="number" class="_formBlock">
|
||||
<template #label>{{ $ts._deck.columnMargin }}</template>
|
||||
<template #label>{{ i18n.ts._deck.columnMargin }}</template>
|
||||
<template #suffix>px</template>
|
||||
</FormInput>
|
||||
|
||||
<FormLink class="_formBlock" @click="setProfile">{{ $ts._deck.profile }}<template #suffix>{{ profile }}</template></FormLink>
|
||||
<FormLink class="_formBlock" @click="setProfile">{{ i18n.ts._deck.profile }}<template #suffix>{{ profile }}</template></FormLink>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { computed, defineExpose, watch } from 'vue';
|
||||
import FormSwitch from '@/components/form/switch.vue';
|
||||
import FormLink from '@/components/form/link.vue';
|
||||
import FormRadios from '@/components/form/radios.vue';
|
||||
@@ -40,59 +40,41 @@ import { deckStore } from '@/ui/deck/deck-store';
|
||||
import * as os from '@/os';
|
||||
import { unisonReload } from '@/scripts/unison-reload';
|
||||
import * as symbols from '@/symbols';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
FormSwitch,
|
||||
FormLink,
|
||||
FormInput,
|
||||
FormRadios,
|
||||
FormGroup,
|
||||
},
|
||||
const navWindow = computed(deckStore.makeGetterSetter('navWindow'));
|
||||
const alwaysShowMainColumn = computed(deckStore.makeGetterSetter('alwaysShowMainColumn'));
|
||||
const columnAlign = computed(deckStore.makeGetterSetter('columnAlign'));
|
||||
const columnMargin = computed(deckStore.makeGetterSetter('columnMargin'));
|
||||
const columnHeaderHeight = computed(deckStore.makeGetterSetter('columnHeaderHeight'));
|
||||
const profile = computed(deckStore.makeGetterSetter('profile'));
|
||||
|
||||
emits: ['info'],
|
||||
watch(navWindow, async () => {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'info',
|
||||
text: i18n.ts.reloadToApplySetting,
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: this.$ts.deck,
|
||||
icon: 'fas fa-columns',
|
||||
bg: 'var(--bg)',
|
||||
},
|
||||
}
|
||||
},
|
||||
unisonReload();
|
||||
});
|
||||
|
||||
computed: {
|
||||
navWindow: deckStore.makeGetterSetter('navWindow'),
|
||||
alwaysShowMainColumn: deckStore.makeGetterSetter('alwaysShowMainColumn'),
|
||||
columnAlign: deckStore.makeGetterSetter('columnAlign'),
|
||||
columnMargin: deckStore.makeGetterSetter('columnMargin'),
|
||||
columnHeaderHeight: deckStore.makeGetterSetter('columnHeaderHeight'),
|
||||
profile: deckStore.makeGetterSetter('profile'),
|
||||
},
|
||||
async function setProfile() {
|
||||
const { canceled, result: name } = await os.inputText({
|
||||
title: i18n.ts._deck.profile,
|
||||
allowEmpty: false
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
profile.value = name;
|
||||
unisonReload();
|
||||
}
|
||||
|
||||
watch: {
|
||||
async navWindow() {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'info',
|
||||
text: this.$ts.reloadToApplySetting,
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
unisonReload();
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
async setProfile() {
|
||||
const { canceled, result: name } = await os.inputText({
|
||||
title: this.$ts._deck.profile,
|
||||
allowEmpty: false
|
||||
});
|
||||
if (canceled) return;
|
||||
this.profile = name;
|
||||
unisonReload();
|
||||
}
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: i18n.ts.deck,
|
||||
icon: 'fas fa-columns',
|
||||
bg: 'var(--bg)',
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,64 +1,52 @@
|
||||
<template>
|
||||
<div class="_formRoot">
|
||||
<FormInfo warn class="_formBlock">{{ $ts._accountDelete.mayTakeTime }}</FormInfo>
|
||||
<FormInfo class="_formBlock">{{ $ts._accountDelete.sendEmail }}</FormInfo>
|
||||
<FormButton v-if="!$i.isDeleted" danger class="_formBlock" @click="deleteAccount">{{ $ts._accountDelete.requestAccountDelete }}</FormButton>
|
||||
<FormButton v-else disabled>{{ $ts._accountDelete.inProgress }}</FormButton>
|
||||
<FormInfo warn class="_formBlock">{{ i18n.ts._accountDelete.mayTakeTime }}</FormInfo>
|
||||
<FormInfo class="_formBlock">{{ i18n.ts._accountDelete.sendEmail }}</FormInfo>
|
||||
<FormButton v-if="!$i.isDeleted" danger class="_formBlock" @click="deleteAccount">{{ i18n.ts._accountDelete.requestAccountDelete }}</FormButton>
|
||||
<FormButton v-else disabled>{{ i18n.ts._accountDelete.inProgress }}</FormButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineAsyncComponent, defineComponent } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { defineExpose } from 'vue';
|
||||
import FormInfo from '@/components/ui/info.vue';
|
||||
import FormButton from '@/components/ui/button.vue';
|
||||
import * as os from '@/os';
|
||||
import { signout } from '@/account';
|
||||
import * as symbols from '@/symbols';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
FormButton,
|
||||
FormInfo,
|
||||
},
|
||||
async function deleteAccount() {
|
||||
{
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.ts.deleteAccountConfirm,
|
||||
});
|
||||
if (canceled) return;
|
||||
}
|
||||
|
||||
emits: ['info'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: this.$ts._accountDelete.accountDelete,
|
||||
icon: 'fas fa-exclamation-triangle',
|
||||
bg: 'var(--bg)',
|
||||
},
|
||||
}
|
||||
},
|
||||
const { canceled, result: password } = await os.inputText({
|
||||
title: i18n.ts.password,
|
||||
type: 'password'
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
methods: {
|
||||
async deleteAccount() {
|
||||
{
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
text: this.$ts.deleteAccountConfirm,
|
||||
});
|
||||
if (canceled) return;
|
||||
}
|
||||
await os.apiWithDialog('i/delete-account', {
|
||||
password: password
|
||||
});
|
||||
|
||||
const { canceled, result: password } = await os.inputText({
|
||||
title: this.$ts.password,
|
||||
type: 'password'
|
||||
});
|
||||
if (canceled) return;
|
||||
await os.alert({
|
||||
title: i18n.ts._accountDelete.started,
|
||||
});
|
||||
|
||||
await os.apiWithDialog('i/delete-account', {
|
||||
password: password
|
||||
});
|
||||
await signout();
|
||||
}
|
||||
|
||||
await os.alert({
|
||||
title: this.$ts._accountDelete.started,
|
||||
});
|
||||
|
||||
signout();
|
||||
}
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: i18n.ts._accountDelete.accountDelete,
|
||||
icon: 'fas fa-exclamation-triangle',
|
||||
bg: 'var(--bg)',
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,40 +1,40 @@
|
||||
<template>
|
||||
<div class="_formRoot">
|
||||
<FormSection v-if="!fetching">
|
||||
<template #label>{{ $ts.usageAmount }}</template>
|
||||
<template #label>{{ i18n.ts.usageAmount }}</template>
|
||||
<div class="_formBlock uawsfosz">
|
||||
<div class="meter"><div :style="meterStyle"></div></div>
|
||||
</div>
|
||||
<FormSplit>
|
||||
<MkKeyValue class="_formBlock">
|
||||
<template #key>{{ $ts.capacity }}</template>
|
||||
<template #key>{{ i18n.ts.capacity }}</template>
|
||||
<template #value>{{ bytes(capacity, 1) }}</template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue class="_formBlock">
|
||||
<template #key>{{ $ts.inUse }}</template>
|
||||
<template #key>{{ i18n.ts.inUse }}</template>
|
||||
<template #value>{{ bytes(usage, 1) }}</template>
|
||||
</MkKeyValue>
|
||||
</FormSplit>
|
||||
</FormSection>
|
||||
|
||||
<FormSection>
|
||||
<template #label>{{ $ts.statistics }}</template>
|
||||
<template #label>{{ i18n.ts.statistics }}</template>
|
||||
<MkChart src="per-user-drive" :args="{ user: $i }" span="day" :limit="7 * 5" :bar="true" :stacked="true" :detailed="false" :aspect-ratio="6"/>
|
||||
</FormSection>
|
||||
|
||||
<FormSection>
|
||||
<FormLink @click="chooseUploadFolder()">
|
||||
{{ $ts.uploadFolder }}
|
||||
{{ i18n.ts.uploadFolder }}
|
||||
<template #suffix>{{ uploadFolder ? uploadFolder.name : '-' }}</template>
|
||||
<template #suffixIcon><i class="fas fa-folder-open"></i></template>
|
||||
</FormLink>
|
||||
<FormSwitch v-model="keepOriginalUploading" class="_formBlock">{{ $ts.keepOriginalUploading }}<template #caption>{{ $ts.keepOriginalUploadingDescription }}</template></FormSwitch>
|
||||
<FormSwitch v-model="keepOriginalUploading" class="_formBlock">{{ i18n.ts.keepOriginalUploading }}<template #caption>{{ i18n.ts.keepOriginalUploadingDescription }}</template></FormSwitch>
|
||||
</FormSection>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { computed, defineExpose, ref } from 'vue';
|
||||
import * as tinycolor from 'tinycolor2';
|
||||
import FormLink from '@/components/form/link.vue';
|
||||
import FormSwitch from '@/components/form/switch.vue';
|
||||
@@ -46,80 +46,59 @@ import bytes from '@/filters/bytes';
|
||||
import * as symbols from '@/symbols';
|
||||
import { defaultStore } from '@/store';
|
||||
import MkChart from '@/components/chart.vue';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
FormLink,
|
||||
FormSwitch,
|
||||
FormSection,
|
||||
MkKeyValue,
|
||||
FormSplit,
|
||||
MkChart,
|
||||
},
|
||||
const fetching = ref(true);
|
||||
const usage = ref<any>(null);
|
||||
const capacity = ref<any>(null);
|
||||
const uploadFolder = ref<any>(null);
|
||||
|
||||
emits: ['info'],
|
||||
const meterStyle = computed(() => {
|
||||
return {
|
||||
width: `${usage.value / capacity.value * 100}%`,
|
||||
background: tinycolor({
|
||||
h: 180 - (usage.value / capacity.value * 180),
|
||||
s: 0.7,
|
||||
l: 0.5
|
||||
})
|
||||
};
|
||||
});
|
||||
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: this.$ts.drive,
|
||||
icon: 'fas fa-cloud',
|
||||
bg: 'var(--bg)',
|
||||
},
|
||||
fetching: true,
|
||||
usage: null,
|
||||
capacity: null,
|
||||
uploadFolder: null,
|
||||
const keepOriginalUploading = computed(defaultStore.makeGetterSetter('keepOriginalUploading'));
|
||||
|
||||
os.api('drive').then(info => {
|
||||
capacity.value = info.capacity;
|
||||
usage.value = info.usage;
|
||||
fetching.value = false;
|
||||
});
|
||||
|
||||
if (defaultStore.state.uploadFolder) {
|
||||
os.api('drive/folders/show', {
|
||||
folderId: defaultStore.state.uploadFolder
|
||||
}).then(response => {
|
||||
uploadFolder.value = response;
|
||||
});
|
||||
}
|
||||
|
||||
function chooseUploadFolder() {
|
||||
os.selectDriveFolder(false).then(async folder => {
|
||||
defaultStore.set('uploadFolder', folder ? folder.id : null);
|
||||
os.success();
|
||||
if (defaultStore.state.uploadFolder) {
|
||||
uploadFolder.value = await os.api('drive/folders/show', {
|
||||
folderId: defaultStore.state.uploadFolder
|
||||
});
|
||||
} else {
|
||||
uploadFolder.value = null;
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
computed: {
|
||||
meterStyle(): any {
|
||||
return {
|
||||
width: `${this.usage / this.capacity * 100}%`,
|
||||
background: tinycolor({
|
||||
h: 180 - (this.usage / this.capacity * 180),
|
||||
s: 0.7,
|
||||
l: 0.5
|
||||
})
|
||||
};
|
||||
},
|
||||
keepOriginalUploading: defaultStore.makeGetterSetter('keepOriginalUploading'),
|
||||
},
|
||||
|
||||
async created() {
|
||||
os.api('drive').then(info => {
|
||||
this.capacity = info.capacity;
|
||||
this.usage = info.usage;
|
||||
this.fetching = false;
|
||||
this.$nextTick(() => {
|
||||
this.renderChart();
|
||||
});
|
||||
});
|
||||
|
||||
if (this.$store.state.uploadFolder) {
|
||||
this.uploadFolder = await os.api('drive/folders/show', {
|
||||
folderId: this.$store.state.uploadFolder
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
chooseUploadFolder() {
|
||||
os.selectDriveFolder(false).then(async folder => {
|
||||
this.$store.set('uploadFolder', folder ? folder.id : null);
|
||||
os.success();
|
||||
if (this.$store.state.uploadFolder) {
|
||||
this.uploadFolder = await os.api('drive/folders/show', {
|
||||
folderId: this.$store.state.uploadFolder
|
||||
});
|
||||
} else {
|
||||
this.uploadFolder = null;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
bytes
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: i18n.ts.drive,
|
||||
icon: 'fas fa-cloud',
|
||||
bg: 'var(--bg)',
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -39,8 +39,8 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, onMounted, ref, watch } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { defineExpose, onMounted, ref, watch } from 'vue';
|
||||
import FormSection from '@/components/form/section.vue';
|
||||
import FormInput from '@/components/form/input.vue';
|
||||
import FormSwitch from '@/components/form/switch.vue';
|
||||
@@ -49,79 +49,62 @@ import * as symbols from '@/symbols';
|
||||
import { $i } from '@/account';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
FormSection,
|
||||
FormSwitch,
|
||||
FormInput,
|
||||
},
|
||||
const emailAddress = ref($i!.email);
|
||||
|
||||
emits: ['info'],
|
||||
const onChangeReceiveAnnouncementEmail = (v) => {
|
||||
os.api('i/update', {
|
||||
receiveAnnouncementEmail: v
|
||||
});
|
||||
};
|
||||
|
||||
setup(props, context) {
|
||||
const emailAddress = ref($i.email);
|
||||
|
||||
const INFO = {
|
||||
title: i18n.ts.email,
|
||||
icon: 'fas fa-envelope',
|
||||
bg: 'var(--bg)',
|
||||
};
|
||||
|
||||
const onChangeReceiveAnnouncementEmail = (v) => {
|
||||
os.api('i/update', {
|
||||
receiveAnnouncementEmail: v
|
||||
});
|
||||
};
|
||||
|
||||
const saveEmailAddress = () => {
|
||||
os.inputText({
|
||||
title: i18n.ts.password,
|
||||
type: 'password'
|
||||
}).then(({ canceled, result: password }) => {
|
||||
if (canceled) return;
|
||||
os.apiWithDialog('i/update-email', {
|
||||
password: password,
|
||||
email: emailAddress.value,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const emailNotification_mention = ref($i.emailNotificationTypes.includes('mention'));
|
||||
const emailNotification_reply = ref($i.emailNotificationTypes.includes('reply'));
|
||||
const emailNotification_quote = ref($i.emailNotificationTypes.includes('quote'));
|
||||
const emailNotification_follow = ref($i.emailNotificationTypes.includes('follow'));
|
||||
const emailNotification_receiveFollowRequest = ref($i.emailNotificationTypes.includes('receiveFollowRequest'));
|
||||
const emailNotification_groupInvited = ref($i.emailNotificationTypes.includes('groupInvited'));
|
||||
|
||||
const saveNotificationSettings = () => {
|
||||
os.api('i/update', {
|
||||
emailNotificationTypes: [
|
||||
...[emailNotification_mention.value ? 'mention' : null],
|
||||
...[emailNotification_reply.value ? 'reply' : null],
|
||||
...[emailNotification_quote.value ? 'quote' : null],
|
||||
...[emailNotification_follow.value ? 'follow' : null],
|
||||
...[emailNotification_receiveFollowRequest.value ? 'receiveFollowRequest' : null],
|
||||
...[emailNotification_groupInvited.value ? 'groupInvited' : null],
|
||||
].filter(x => x != null)
|
||||
});
|
||||
};
|
||||
|
||||
watch([emailNotification_mention, emailNotification_reply, emailNotification_quote, emailNotification_follow, emailNotification_receiveFollowRequest, emailNotification_groupInvited], () => {
|
||||
saveNotificationSettings();
|
||||
const saveEmailAddress = () => {
|
||||
os.inputText({
|
||||
title: i18n.ts.password,
|
||||
type: 'password'
|
||||
}).then(({ canceled, result: password }) => {
|
||||
if (canceled) return;
|
||||
os.apiWithDialog('i/update-email', {
|
||||
password: password,
|
||||
email: emailAddress.value,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
watch(emailAddress, () => {
|
||||
saveEmailAddress();
|
||||
});
|
||||
});
|
||||
const emailNotification_mention = ref($i!.emailNotificationTypes.includes('mention'));
|
||||
const emailNotification_reply = ref($i!.emailNotificationTypes.includes('reply'));
|
||||
const emailNotification_quote = ref($i!.emailNotificationTypes.includes('quote'));
|
||||
const emailNotification_follow = ref($i!.emailNotificationTypes.includes('follow'));
|
||||
const emailNotification_receiveFollowRequest = ref($i!.emailNotificationTypes.includes('receiveFollowRequest'));
|
||||
const emailNotification_groupInvited = ref($i!.emailNotificationTypes.includes('groupInvited'));
|
||||
|
||||
return {
|
||||
[symbols.PAGE_INFO]: INFO,
|
||||
emailAddress,
|
||||
onChangeReceiveAnnouncementEmail,
|
||||
emailNotification_mention, emailNotification_reply, emailNotification_quote, emailNotification_follow, emailNotification_receiveFollowRequest, emailNotification_groupInvited,
|
||||
};
|
||||
},
|
||||
const saveNotificationSettings = () => {
|
||||
os.api('i/update', {
|
||||
emailNotificationTypes: [
|
||||
...[emailNotification_mention.value ? 'mention' : null],
|
||||
...[emailNotification_reply.value ? 'reply' : null],
|
||||
...[emailNotification_quote.value ? 'quote' : null],
|
||||
...[emailNotification_follow.value ? 'follow' : null],
|
||||
...[emailNotification_receiveFollowRequest.value ? 'receiveFollowRequest' : null],
|
||||
...[emailNotification_groupInvited.value ? 'groupInvited' : null],
|
||||
].filter(x => x != null)
|
||||
});
|
||||
};
|
||||
|
||||
watch([emailNotification_mention, emailNotification_reply, emailNotification_quote, emailNotification_follow, emailNotification_receiveFollowRequest, emailNotification_groupInvited], () => {
|
||||
saveNotificationSettings();
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
watch(emailAddress, () => {
|
||||
saveEmailAddress();
|
||||
});
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: i18n.ts.email,
|
||||
icon: 'fas fa-envelope',
|
||||
bg: 'var(--bg)',
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<div class="_formRoot">
|
||||
<FormSelect v-model="lang" class="_formBlock">
|
||||
<template #label>{{ $ts.uiLanguage }}</template>
|
||||
<template #label>{{ i18n.ts.uiLanguage }}</template>
|
||||
<option v-for="x in langs" :key="x[0]" :value="x[0]">{{ x[1] }}</option>
|
||||
<template #caption>
|
||||
<I18n :src="$ts.i18nInfo" tag="span">
|
||||
<I18n :src="i18n.ts.i18nInfo" tag="span">
|
||||
<template #link>
|
||||
<MkLink url="https://crowdin.com/project/misskey">Crowdin</MkLink>
|
||||
</template>
|
||||
@@ -13,48 +13,48 @@
|
||||
</FormSelect>
|
||||
|
||||
<FormRadios v-model="overridedDeviceKind" class="_formBlock">
|
||||
<template #label>{{ $ts.overridedDeviceKind }}</template>
|
||||
<option :value="null">{{ $ts.auto }}</option>
|
||||
<option value="smartphone"><i class="fas fa-mobile-alt"/> {{ $ts.smartphone }}</option>
|
||||
<option value="tablet"><i class="fas fa-tablet-alt"/> {{ $ts.tablet }}</option>
|
||||
<option value="desktop"><i class="fas fa-desktop"/> {{ $ts.desktop }}</option>
|
||||
<template #label>{{ i18n.ts.overridedDeviceKind }}</template>
|
||||
<option :value="null">{{ i18n.ts.auto }}</option>
|
||||
<option value="smartphone"><i class="fas fa-mobile-alt"/> {{ i18n.ts.smartphone }}</option>
|
||||
<option value="tablet"><i class="fas fa-tablet-alt"/> {{ i18n.ts.tablet }}</option>
|
||||
<option value="desktop"><i class="fas fa-desktop"/> {{ i18n.ts.desktop }}</option>
|
||||
</FormRadios>
|
||||
|
||||
<FormSwitch v-model="showFixedPostForm" class="_formBlock">{{ $ts.showFixedPostForm }}</FormSwitch>
|
||||
<FormSwitch v-model="showFixedPostForm" class="_formBlock">{{ i18n.ts.showFixedPostForm }}</FormSwitch>
|
||||
|
||||
<FormSection>
|
||||
<template #label>{{ $ts.behavior }}</template>
|
||||
<FormSwitch v-model="imageNewTab" class="_formBlock">{{ $ts.openImageInNewTab }}</FormSwitch>
|
||||
<FormSwitch v-model="enableInfiniteScroll" class="_formBlock">{{ $ts.enableInfiniteScroll }}</FormSwitch>
|
||||
<FormSwitch v-model="useReactionPickerForContextMenu" class="_formBlock">{{ $ts.useReactionPickerForContextMenu }}</FormSwitch>
|
||||
<FormSwitch v-model="disablePagesScript" class="_formBlock">{{ $ts.disablePagesScript }}</FormSwitch>
|
||||
<template #label>{{ i18n.ts.behavior }}</template>
|
||||
<FormSwitch v-model="imageNewTab" class="_formBlock">{{ i18n.ts.openImageInNewTab }}</FormSwitch>
|
||||
<FormSwitch v-model="enableInfiniteScroll" class="_formBlock">{{ i18n.ts.enableInfiniteScroll }}</FormSwitch>
|
||||
<FormSwitch v-model="useReactionPickerForContextMenu" class="_formBlock">{{ i18n.ts.useReactionPickerForContextMenu }}</FormSwitch>
|
||||
<FormSwitch v-model="disablePagesScript" class="_formBlock">{{ i18n.ts.disablePagesScript }}</FormSwitch>
|
||||
|
||||
<FormSelect v-model="serverDisconnectedBehavior" class="_formBlock">
|
||||
<template #label>{{ $ts.whenServerDisconnected }}</template>
|
||||
<option value="reload">{{ $ts._serverDisconnectedBehavior.reload }}</option>
|
||||
<option value="dialog">{{ $ts._serverDisconnectedBehavior.dialog }}</option>
|
||||
<option value="quiet">{{ $ts._serverDisconnectedBehavior.quiet }}</option>
|
||||
<template #label>{{ i18n.ts.whenServerDisconnected }}</template>
|
||||
<option value="reload">{{ i18n.ts._serverDisconnectedBehavior.reload }}</option>
|
||||
<option value="dialog">{{ i18n.ts._serverDisconnectedBehavior.dialog }}</option>
|
||||
<option value="quiet">{{ i18n.ts._serverDisconnectedBehavior.quiet }}</option>
|
||||
</FormSelect>
|
||||
</FormSection>
|
||||
|
||||
<FormSection>
|
||||
<template #label>{{ $ts.appearance }}</template>
|
||||
<FormSwitch v-model="disableAnimatedMfm" class="_formBlock">{{ $ts.disableAnimatedMfm }}</FormSwitch>
|
||||
<FormSwitch v-model="reduceAnimation" class="_formBlock">{{ $ts.reduceUiAnimation }}</FormSwitch>
|
||||
<FormSwitch v-model="useBlurEffect" class="_formBlock">{{ $ts.useBlurEffect }}</FormSwitch>
|
||||
<FormSwitch v-model="useBlurEffectForModal" class="_formBlock">{{ $ts.useBlurEffectForModal }}</FormSwitch>
|
||||
<FormSwitch v-model="showGapBetweenNotesInTimeline" class="_formBlock">{{ $ts.showGapBetweenNotesInTimeline }}</FormSwitch>
|
||||
<FormSwitch v-model="loadRawImages" class="_formBlock">{{ $ts.loadRawImages }}</FormSwitch>
|
||||
<FormSwitch v-model="disableShowingAnimatedImages" class="_formBlock">{{ $ts.disableShowingAnimatedImages }}</FormSwitch>
|
||||
<FormSwitch v-model="squareAvatars" class="_formBlock">{{ $ts.squareAvatars }}</FormSwitch>
|
||||
<FormSwitch v-model="useSystemFont" class="_formBlock">{{ $ts.useSystemFont }}</FormSwitch>
|
||||
<FormSwitch v-model="useOsNativeEmojis" class="_formBlock">{{ $ts.useOsNativeEmojis }}
|
||||
<template #label>{{ i18n.ts.appearance }}</template>
|
||||
<FormSwitch v-model="disableAnimatedMfm" class="_formBlock">{{ i18n.ts.disableAnimatedMfm }}</FormSwitch>
|
||||
<FormSwitch v-model="reduceAnimation" class="_formBlock">{{ i18n.ts.reduceUiAnimation }}</FormSwitch>
|
||||
<FormSwitch v-model="useBlurEffect" class="_formBlock">{{ i18n.ts.useBlurEffect }}</FormSwitch>
|
||||
<FormSwitch v-model="useBlurEffectForModal" class="_formBlock">{{ i18n.ts.useBlurEffectForModal }}</FormSwitch>
|
||||
<FormSwitch v-model="showGapBetweenNotesInTimeline" class="_formBlock">{{ i18n.ts.showGapBetweenNotesInTimeline }}</FormSwitch>
|
||||
<FormSwitch v-model="loadRawImages" class="_formBlock">{{ i18n.ts.loadRawImages }}</FormSwitch>
|
||||
<FormSwitch v-model="disableShowingAnimatedImages" class="_formBlock">{{ i18n.ts.disableShowingAnimatedImages }}</FormSwitch>
|
||||
<FormSwitch v-model="squareAvatars" class="_formBlock">{{ i18n.ts.squareAvatars }}</FormSwitch>
|
||||
<FormSwitch v-model="useSystemFont" class="_formBlock">{{ i18n.ts.useSystemFont }}</FormSwitch>
|
||||
<FormSwitch v-model="useOsNativeEmojis" class="_formBlock">{{ i18n.ts.useOsNativeEmojis }}
|
||||
<div><Mfm :key="useOsNativeEmojis" text="🍮🍦🍭🍩🍰🍫🍬🥞🍪"/></div>
|
||||
</FormSwitch>
|
||||
<FormSwitch v-model="disableDrawer" class="_formBlock">{{ $ts.disableDrawer }}</FormSwitch>
|
||||
<FormSwitch v-model="disableDrawer" class="_formBlock">{{ i18n.ts.disableDrawer }}</FormSwitch>
|
||||
|
||||
<FormRadios v-model="fontSize" class="_formBlock">
|
||||
<template #label>{{ $ts.fontSize }}</template>
|
||||
<template #label>{{ i18n.ts.fontSize }}</template>
|
||||
<option value="small"><span style="font-size: 14px;">Aa</span></option>
|
||||
<option :value="null"><span style="font-size: 16px;">Aa</span></option>
|
||||
<option value="large"><span style="font-size: 18px;">Aa</span></option>
|
||||
@@ -63,36 +63,36 @@
|
||||
</FormSection>
|
||||
|
||||
<FormSection>
|
||||
<FormSwitch v-model="aiChanMode">{{ $ts.aiChanMode }}</FormSwitch>
|
||||
<FormSwitch v-model="aiChanMode">{{ i18n.ts.aiChanMode }}</FormSwitch>
|
||||
</FormSection>
|
||||
|
||||
<FormSelect v-model="instanceTicker" class="_formBlock">
|
||||
<template #label>{{ $ts.instanceTicker }}</template>
|
||||
<option value="none">{{ $ts._instanceTicker.none }}</option>
|
||||
<option value="remote">{{ $ts._instanceTicker.remote }}</option>
|
||||
<option value="always">{{ $ts._instanceTicker.always }}</option>
|
||||
<template #label>{{ i18n.ts.instanceTicker }}</template>
|
||||
<option value="none">{{ i18n.ts._instanceTicker.none }}</option>
|
||||
<option value="remote">{{ i18n.ts._instanceTicker.remote }}</option>
|
||||
<option value="always">{{ i18n.ts._instanceTicker.always }}</option>
|
||||
</FormSelect>
|
||||
|
||||
<FormSelect v-model="nsfw" class="_formBlock">
|
||||
<template #label>{{ $ts.nsfw }}</template>
|
||||
<option value="respect">{{ $ts._nsfw.respect }}</option>
|
||||
<option value="ignore">{{ $ts._nsfw.ignore }}</option>
|
||||
<option value="force">{{ $ts._nsfw.force }}</option>
|
||||
<template #label>{{ i18n.ts.nsfw }}</template>
|
||||
<option value="respect">{{ i18n.ts._nsfw.respect }}</option>
|
||||
<option value="ignore">{{ i18n.ts._nsfw.ignore }}</option>
|
||||
<option value="force">{{ i18n.ts._nsfw.force }}</option>
|
||||
</FormSelect>
|
||||
|
||||
<FormGroup>
|
||||
<template #label>{{ $ts.defaultNavigationBehaviour }}</template>
|
||||
<FormSwitch v-model="defaultSideView">{{ $ts.openInSideView }}</FormSwitch>
|
||||
<template #label>{{ i18n.ts.defaultNavigationBehaviour }}</template>
|
||||
<FormSwitch v-model="defaultSideView">{{ i18n.ts.openInSideView }}</FormSwitch>
|
||||
</FormGroup>
|
||||
|
||||
<FormLink to="/settings/deck" class="_formBlock">{{ $ts.deck }}</FormLink>
|
||||
<FormLink to="/settings/deck" class="_formBlock">{{ i18n.ts.deck }}</FormLink>
|
||||
|
||||
<FormLink to="/settings/custom-css" class="_formBlock"><template #icon><i class="fas fa-code"></i></template>{{ $ts.customCss }}</FormLink>
|
||||
<FormLink to="/settings/custom-css" class="_formBlock"><template #icon><i class="fas fa-code"></i></template>{{ i18n.ts.customCss }}</FormLink>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { computed, defineExpose, ref, watch } from 'vue';
|
||||
import FormSwitch from '@/components/form/switch.vue';
|
||||
import FormSelect from '@/components/form/select.vue';
|
||||
import FormRadios from '@/components/form/radios.vue';
|
||||
@@ -102,122 +102,87 @@ import FormLink from '@/components/form/link.vue';
|
||||
import MkLink from '@/components/link.vue';
|
||||
import { langs } from '@/config';
|
||||
import { defaultStore } from '@/store';
|
||||
import { ColdDeviceStorage } from '@/store';
|
||||
import * as os from '@/os';
|
||||
import { unisonReload } from '@/scripts/unison-reload';
|
||||
import * as symbols from '@/symbols';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkLink,
|
||||
FormSwitch,
|
||||
FormSelect,
|
||||
FormRadios,
|
||||
FormGroup,
|
||||
FormLink,
|
||||
FormSection,
|
||||
},
|
||||
const lang = ref(localStorage.getItem('lang'));
|
||||
const fontSize = ref(localStorage.getItem('fontSize'));
|
||||
const useSystemFont = ref(localStorage.getItem('useSystemFont') != null);
|
||||
|
||||
emits: ['info'],
|
||||
async function reloadAsk() {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'info',
|
||||
text: i18n.ts.reloadToApplySetting,
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: this.$ts.general,
|
||||
icon: 'fas fa-cogs',
|
||||
bg: 'var(--bg)'
|
||||
},
|
||||
langs,
|
||||
lang: localStorage.getItem('lang'),
|
||||
fontSize: localStorage.getItem('fontSize'),
|
||||
useSystemFont: localStorage.getItem('useSystemFont') != null,
|
||||
}
|
||||
},
|
||||
unisonReload();
|
||||
}
|
||||
|
||||
computed: {
|
||||
overridedDeviceKind: defaultStore.makeGetterSetter('overridedDeviceKind'),
|
||||
serverDisconnectedBehavior: defaultStore.makeGetterSetter('serverDisconnectedBehavior'),
|
||||
reduceAnimation: defaultStore.makeGetterSetter('animation', v => !v, v => !v),
|
||||
useBlurEffectForModal: defaultStore.makeGetterSetter('useBlurEffectForModal'),
|
||||
useBlurEffect: defaultStore.makeGetterSetter('useBlurEffect'),
|
||||
showGapBetweenNotesInTimeline: defaultStore.makeGetterSetter('showGapBetweenNotesInTimeline'),
|
||||
disableAnimatedMfm: defaultStore.makeGetterSetter('animatedMfm', v => !v, v => !v),
|
||||
useOsNativeEmojis: defaultStore.makeGetterSetter('useOsNativeEmojis'),
|
||||
disableDrawer: defaultStore.makeGetterSetter('disableDrawer'),
|
||||
disableShowingAnimatedImages: defaultStore.makeGetterSetter('disableShowingAnimatedImages'),
|
||||
loadRawImages: defaultStore.makeGetterSetter('loadRawImages'),
|
||||
imageNewTab: defaultStore.makeGetterSetter('imageNewTab'),
|
||||
nsfw: defaultStore.makeGetterSetter('nsfw'),
|
||||
disablePagesScript: defaultStore.makeGetterSetter('disablePagesScript'),
|
||||
showFixedPostForm: defaultStore.makeGetterSetter('showFixedPostForm'),
|
||||
defaultSideView: defaultStore.makeGetterSetter('defaultSideView'),
|
||||
instanceTicker: defaultStore.makeGetterSetter('instanceTicker'),
|
||||
enableInfiniteScroll: defaultStore.makeGetterSetter('enableInfiniteScroll'),
|
||||
useReactionPickerForContextMenu: defaultStore.makeGetterSetter('useReactionPickerForContextMenu'),
|
||||
squareAvatars: defaultStore.makeGetterSetter('squareAvatars'),
|
||||
aiChanMode: defaultStore.makeGetterSetter('aiChanMode'),
|
||||
},
|
||||
const overridedDeviceKind = computed(defaultStore.makeGetterSetter('overridedDeviceKind'));
|
||||
const serverDisconnectedBehavior = computed(defaultStore.makeGetterSetter('serverDisconnectedBehavior'));
|
||||
const reduceAnimation = computed(defaultStore.makeGetterSetter('animation', v => !v, v => !v));
|
||||
const useBlurEffectForModal = computed(defaultStore.makeGetterSetter('useBlurEffectForModal'));
|
||||
const useBlurEffect = computed(defaultStore.makeGetterSetter('useBlurEffect'));
|
||||
const showGapBetweenNotesInTimeline = computed(defaultStore.makeGetterSetter('showGapBetweenNotesInTimeline'));
|
||||
const disableAnimatedMfm = computed(defaultStore.makeGetterSetter('animatedMfm', v => !v, v => !v));
|
||||
const useOsNativeEmojis = computed(defaultStore.makeGetterSetter('useOsNativeEmojis'));
|
||||
const disableDrawer = computed(defaultStore.makeGetterSetter('disableDrawer'));
|
||||
const disableShowingAnimatedImages = computed(defaultStore.makeGetterSetter('disableShowingAnimatedImages'));
|
||||
const loadRawImages = computed(defaultStore.makeGetterSetter('loadRawImages'));
|
||||
const imageNewTab = computed(defaultStore.makeGetterSetter('imageNewTab'));
|
||||
const nsfw = computed(defaultStore.makeGetterSetter('nsfw'));
|
||||
const disablePagesScript = computed(defaultStore.makeGetterSetter('disablePagesScript'));
|
||||
const showFixedPostForm = computed(defaultStore.makeGetterSetter('showFixedPostForm'));
|
||||
const defaultSideView = computed(defaultStore.makeGetterSetter('defaultSideView'));
|
||||
const instanceTicker = computed(defaultStore.makeGetterSetter('instanceTicker'));
|
||||
const enableInfiniteScroll = computed(defaultStore.makeGetterSetter('enableInfiniteScroll'));
|
||||
const useReactionPickerForContextMenu = computed(defaultStore.makeGetterSetter('useReactionPickerForContextMenu'));
|
||||
const squareAvatars = computed(defaultStore.makeGetterSetter('squareAvatars'));
|
||||
const aiChanMode = computed(defaultStore.makeGetterSetter('aiChanMode'));
|
||||
|
||||
watch: {
|
||||
lang() {
|
||||
localStorage.setItem('lang', this.lang);
|
||||
localStorage.removeItem('locale');
|
||||
this.reloadAsk();
|
||||
},
|
||||
watch(lang, () => {
|
||||
localStorage.setItem('lang', lang.value as string);
|
||||
localStorage.removeItem('locale');
|
||||
});
|
||||
|
||||
fontSize() {
|
||||
if (this.fontSize == null) {
|
||||
localStorage.removeItem('fontSize');
|
||||
} else {
|
||||
localStorage.setItem('fontSize', this.fontSize);
|
||||
}
|
||||
this.reloadAsk();
|
||||
},
|
||||
watch(fontSize, () => {
|
||||
if (fontSize.value == null) {
|
||||
localStorage.removeItem('fontSize');
|
||||
} else {
|
||||
localStorage.setItem('fontSize', fontSize.value);
|
||||
}
|
||||
});
|
||||
|
||||
useSystemFont() {
|
||||
if (this.useSystemFont) {
|
||||
localStorage.setItem('useSystemFont', 't');
|
||||
} else {
|
||||
localStorage.removeItem('useSystemFont');
|
||||
}
|
||||
this.reloadAsk();
|
||||
},
|
||||
watch(useSystemFont, () => {
|
||||
if (useSystemFont.value) {
|
||||
localStorage.setItem('useSystemFont', 't');
|
||||
} else {
|
||||
localStorage.removeItem('useSystemFont');
|
||||
}
|
||||
});
|
||||
|
||||
enableInfiniteScroll() {
|
||||
this.reloadAsk();
|
||||
},
|
||||
watch([
|
||||
lang,
|
||||
fontSize,
|
||||
useSystemFont,
|
||||
enableInfiniteScroll,
|
||||
squareAvatars,
|
||||
aiChanMode,
|
||||
showGapBetweenNotesInTimeline,
|
||||
instanceTicker,
|
||||
overridedDeviceKind
|
||||
], async () => {
|
||||
await reloadAsk();
|
||||
});
|
||||
|
||||
squareAvatars() {
|
||||
this.reloadAsk();
|
||||
},
|
||||
|
||||
aiChanMode() {
|
||||
this.reloadAsk();
|
||||
},
|
||||
|
||||
showGapBetweenNotesInTimeline() {
|
||||
this.reloadAsk();
|
||||
},
|
||||
|
||||
instanceTicker() {
|
||||
this.reloadAsk();
|
||||
},
|
||||
|
||||
overridedDeviceKind() {
|
||||
this.reloadAsk();
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
async reloadAsk() {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'info',
|
||||
text: this.$ts.reloadToApplySetting,
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
unisonReload();
|
||||
}
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: i18n.ts.general,
|
||||
icon: 'fas fa-cogs',
|
||||
bg: 'var(--bg)'
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
<template>
|
||||
<div class="_formRoot">
|
||||
<FormTextarea v-model="items" tall manual-save class="_formBlock">
|
||||
<template #label>{{ $ts.menu }}</template>
|
||||
<template #caption><button class="_textButton" @click="addItem">{{ $ts.addItem }}</button></template>
|
||||
<template #label>{{ i18n.ts.menu }}</template>
|
||||
<template #caption><button class="_textButton" @click="addItem">{{ i18n.ts.addItem }}</button></template>
|
||||
</FormTextarea>
|
||||
|
||||
<FormRadios v-model="menuDisplay" class="_formBlock">
|
||||
<template #label>{{ $ts.display }}</template>
|
||||
<option value="sideFull">{{ $ts._menuDisplay.sideFull }}</option>
|
||||
<option value="sideIcon">{{ $ts._menuDisplay.sideIcon }}</option>
|
||||
<option value="top">{{ $ts._menuDisplay.top }}</option>
|
||||
<!-- <MkRadio v-model="menuDisplay" value="hide" disabled>{{ $ts._menuDisplay.hide }}</MkRadio>--> <!-- TODO: サイドバーを完全に隠せるようにすると、別途ハンバーガーボタンのようなものをUIに表示する必要があり面倒 -->
|
||||
<template #label>{{ i18n.ts.display }}</template>
|
||||
<option value="sideFull">{{ i18n.ts._menuDisplay.sideFull }}</option>
|
||||
<option value="sideIcon">{{ i18n.ts._menuDisplay.sideIcon }}</option>
|
||||
<option value="top">{{ i18n.ts._menuDisplay.top }}</option>
|
||||
<!-- <MkRadio v-model="menuDisplay" value="hide" disabled>{{ i18n.ts._menuDisplay.hide }}</MkRadio>--> <!-- TODO: サイドバーを完全に隠せるようにすると、別途ハンバーガーボタンのようなものをUIに表示する必要があり面倒 -->
|
||||
</FormRadios>
|
||||
|
||||
<FormButton danger class="_formBlock" @click="reset()"><i class="fas fa-redo"></i> {{ $ts.default }}</FormButton>
|
||||
<FormButton danger class="_formBlock" @click="reset()"><i class="fas fa-redo"></i> {{ i18n.ts.default }}</FormButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { computed, defineExpose, ref, watch } from 'vue';
|
||||
import FormTextarea from '@/components/form/textarea.vue';
|
||||
import FormRadios from '@/components/form/radios.vue';
|
||||
import FormButton from '@/components/ui/button.vue';
|
||||
@@ -27,81 +27,60 @@ import { menuDef } from '@/menu';
|
||||
import { defaultStore } from '@/store';
|
||||
import * as symbols from '@/symbols';
|
||||
import { unisonReload } from '@/scripts/unison-reload';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
FormButton,
|
||||
FormTextarea,
|
||||
FormRadios,
|
||||
},
|
||||
const items = ref(defaultStore.state.menu.join('\n'));
|
||||
|
||||
emits: ['info'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: this.$ts.menu,
|
||||
icon: 'fas fa-list-ul',
|
||||
bg: 'var(--bg)',
|
||||
},
|
||||
menuDef: menuDef,
|
||||
items: defaultStore.state.menu.join('\n'),
|
||||
}
|
||||
},
|
||||
const split = computed(() => items.value.trim().split('\n').filter(x => x.trim() !== ''));
|
||||
const menuDisplay = computed(defaultStore.makeGetterSetter('menuDisplay'));
|
||||
|
||||
computed: {
|
||||
splited(): string[] {
|
||||
return this.items.trim().split('\n').filter(x => x.trim() !== '');
|
||||
},
|
||||
async function reloadAsk() {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'info',
|
||||
text: i18n.ts.reloadToApplySetting
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
menuDisplay: defaultStore.makeGetterSetter('menuDisplay')
|
||||
},
|
||||
unisonReload();
|
||||
}
|
||||
|
||||
watch: {
|
||||
menuDisplay() {
|
||||
this.reloadAsk();
|
||||
},
|
||||
async function addItem() {
|
||||
const menu = Object.keys(menuDef).filter(k => !defaultStore.state.menu.includes(k));
|
||||
const { canceled, result: item } = await os.select({
|
||||
title: i18n.ts.addItem,
|
||||
items: [...menu.map(k => ({
|
||||
value: k, text: i18n.ts[menuDef[k].title]
|
||||
})), {
|
||||
value: '-', text: i18n.ts.divider
|
||||
}]
|
||||
});
|
||||
if (canceled) return;
|
||||
items.value = [...split.value, item].join('\n');
|
||||
}
|
||||
|
||||
items() {
|
||||
this.save();
|
||||
},
|
||||
},
|
||||
async function save() {
|
||||
defaultStore.set('menu', split.value);
|
||||
await reloadAsk();
|
||||
}
|
||||
|
||||
methods: {
|
||||
async addItem() {
|
||||
const menu = Object.keys(this.menuDef).filter(k => !this.$store.state.menu.includes(k));
|
||||
const { canceled, result: item } = await os.select({
|
||||
title: this.$ts.addItem,
|
||||
items: [...menu.map(k => ({
|
||||
value: k, text: this.$ts[this.menuDef[k].title]
|
||||
})), ...[{
|
||||
value: '-', text: this.$ts.divider
|
||||
}]]
|
||||
});
|
||||
if (canceled) return;
|
||||
this.items = [...this.splited, item].join('\n');
|
||||
},
|
||||
function reset() {
|
||||
defaultStore.reset('menu');
|
||||
items.value = defaultStore.state.menu.join('\n');
|
||||
}
|
||||
|
||||
save() {
|
||||
this.$store.set('menu', this.splited);
|
||||
this.reloadAsk();
|
||||
},
|
||||
watch(items, async () => {
|
||||
await save();
|
||||
});
|
||||
|
||||
reset() {
|
||||
this.$store.reset('menu');
|
||||
this.items = this.$store.state.menu.join('\n');
|
||||
},
|
||||
watch(menuDisplay, async () => {
|
||||
await reloadAsk();
|
||||
});
|
||||
|
||||
async reloadAsk() {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'info',
|
||||
text: this.$ts.reloadToApplySetting,
|
||||
showCancelButton: true
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
unisonReload();
|
||||
}
|
||||
},
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: i18n.ts.menu,
|
||||
icon: 'fas fa-list-ul',
|
||||
bg: 'var(--bg)',
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,71 +1,59 @@
|
||||
<template>
|
||||
<div class="_formRoot">
|
||||
<FormLink class="_formBlock" @click="configure"><template #icon><i class="fas fa-cog"></i></template>{{ $ts.notificationSetting }}</FormLink>
|
||||
<FormLink class="_formBlock" @click="configure"><template #icon><i class="fas fa-cog"></i></template>{{ i18n.ts.notificationSetting }}</FormLink>
|
||||
<FormSection>
|
||||
<FormLink class="_formBlock" @click="readAllNotifications">{{ $ts.markAsReadAllNotifications }}</FormLink>
|
||||
<FormLink class="_formBlock" @click="readAllUnreadNotes">{{ $ts.markAsReadAllUnreadNotes }}</FormLink>
|
||||
<FormLink class="_formBlock" @click="readAllMessagingMessages">{{ $ts.markAsReadAllTalkMessages }}</FormLink>
|
||||
<FormLink class="_formBlock" @click="readAllNotifications">{{ i18n.ts.markAsReadAllNotifications }}</FormLink>
|
||||
<FormLink class="_formBlock" @click="readAllUnreadNotes">{{ i18n.ts.markAsReadAllUnreadNotes }}</FormLink>
|
||||
<FormLink class="_formBlock" @click="readAllMessagingMessages">{{ i18n.ts.markAsReadAllTalkMessages }}</FormLink>
|
||||
</FormSection>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { defineAsyncComponent, defineExpose } from 'vue';
|
||||
import FormButton from '@/components/ui/button.vue';
|
||||
import FormLink from '@/components/form/link.vue';
|
||||
import FormSection from '@/components/form/section.vue';
|
||||
import { notificationTypes } from 'misskey-js';
|
||||
import * as os from '@/os';
|
||||
import * as symbols from '@/symbols';
|
||||
import { $i } from '@/account';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
FormLink,
|
||||
FormButton,
|
||||
FormSection,
|
||||
},
|
||||
async function readAllUnreadNotes() {
|
||||
await os.api('i/read-all-unread-notes');
|
||||
}
|
||||
|
||||
emits: ['info'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: this.$ts.notifications,
|
||||
icon: 'fas fa-bell',
|
||||
bg: 'var(--bg)',
|
||||
},
|
||||
async function readAllMessagingMessages() {
|
||||
await os.api('i/read-all-messaging-messages');
|
||||
}
|
||||
|
||||
async function readAllNotifications() {
|
||||
await os.api('notifications/mark-all-as-read');
|
||||
}
|
||||
|
||||
function configure() {
|
||||
const includingTypes = notificationTypes.filter(x => !$i!.mutingNotificationTypes.includes(x));
|
||||
os.popup(defineAsyncComponent(() => import('@/components/notification-setting-window.vue')), {
|
||||
includingTypes,
|
||||
showGlobalToggle: false,
|
||||
}, {
|
||||
done: async (res) => {
|
||||
const { includingTypes: value } = res;
|
||||
await os.apiWithDialog('i/update', {
|
||||
mutingNotificationTypes: notificationTypes.filter(x => !value.includes(x)),
|
||||
}).then(i => {
|
||||
$i!.mutingNotificationTypes = i.mutingNotificationTypes;
|
||||
});
|
||||
}
|
||||
},
|
||||
}, 'closed');
|
||||
}
|
||||
|
||||
methods: {
|
||||
readAllUnreadNotes() {
|
||||
os.api('i/read-all-unread-notes');
|
||||
},
|
||||
|
||||
readAllMessagingMessages() {
|
||||
os.api('i/read-all-messaging-messages');
|
||||
},
|
||||
|
||||
readAllNotifications() {
|
||||
os.api('notifications/mark-all-as-read');
|
||||
},
|
||||
|
||||
configure() {
|
||||
const includingTypes = notificationTypes.filter(x => !this.$i.mutingNotificationTypes.includes(x));
|
||||
os.popup(import('@/components/notification-setting-window.vue'), {
|
||||
includingTypes,
|
||||
showGlobalToggle: false,
|
||||
}, {
|
||||
done: async (res) => {
|
||||
const { includingTypes: value } = res;
|
||||
await os.apiWithDialog('i/update', {
|
||||
mutingNotificationTypes: notificationTypes.filter(x => !value.includes(x)),
|
||||
}).then(i => {
|
||||
this.$i.mutingNotificationTypes = i.mutingNotificationTypes;
|
||||
});
|
||||
}
|
||||
}, 'closed');
|
||||
},
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: i18n.ts.notifications,
|
||||
icon: 'fas fa-bell',
|
||||
bg: 'var(--bg)',
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,66 +1,44 @@
|
||||
<template>
|
||||
<div class="_formRoot">
|
||||
<FormSwitch :value="$i.injectFeaturedNote" class="_formBlock" @update:modelValue="onChangeInjectFeaturedNote">
|
||||
{{ $ts.showFeaturedNotesInTimeline }}
|
||||
<FormSwitch v-model="$i.injectFeaturedNote" class="_formBlock" @update:modelValue="onChangeInjectFeaturedNote">
|
||||
{{ i18n.ts.showFeaturedNotesInTimeline }}
|
||||
</FormSwitch>
|
||||
|
||||
<!--
|
||||
<FormSwitch v-model="reportError" class="_formBlock">{{ $ts.sendErrorReports }}<template #caption>{{ $ts.sendErrorReportsDescription }}</template></FormSwitch>
|
||||
<FormSwitch v-model="reportError" class="_formBlock">{{ i18n.ts.sendErrorReports }}<template #caption>{{ i18n.ts.sendErrorReportsDescription }}</template></FormSwitch>
|
||||
-->
|
||||
|
||||
<FormLink to="/settings/account-info" class="_formBlock">{{ $ts.accountInfo }}</FormLink>
|
||||
<FormLink to="/settings/account-info" class="_formBlock">{{ i18n.ts.accountInfo }}</FormLink>
|
||||
|
||||
<FormLink to="/settings/delete-account" class="_formBlock"><template #icon><i class="fas fa-exclamation-triangle"></i></template>{{ $ts.closeAccount }}</FormLink>
|
||||
<FormLink to="/settings/delete-account" class="_formBlock"><template #icon><i class="fas fa-exclamation-triangle"></i></template>{{ i18n.ts.closeAccount }}</FormLink>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineAsyncComponent, defineComponent } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { computed, defineExpose } from 'vue';
|
||||
import FormSwitch from '@/components/form/switch.vue';
|
||||
import FormSection from '@/components/form/section.vue';
|
||||
import FormLink from '@/components/form/link.vue';
|
||||
import * as os from '@/os';
|
||||
import { debug } from '@/config';
|
||||
import { defaultStore } from '@/store';
|
||||
import { unisonReload } from '@/scripts/unison-reload';
|
||||
import * as symbols from '@/symbols';
|
||||
import { $i } from '@/account';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
FormSection,
|
||||
FormSwitch,
|
||||
FormLink,
|
||||
},
|
||||
const reportError = computed(defaultStore.makeGetterSetter('reportError'));
|
||||
|
||||
emits: ['info'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: this.$ts.other,
|
||||
icon: 'fas fa-ellipsis-h',
|
||||
bg: 'var(--bg)',
|
||||
},
|
||||
debug,
|
||||
}
|
||||
},
|
||||
function onChangeInjectFeaturedNote(v) {
|
||||
os.api('i/update', {
|
||||
injectFeaturedNote: v
|
||||
}).then((i) => {
|
||||
$i!.injectFeaturedNote = i.injectFeaturedNote;
|
||||
});
|
||||
}
|
||||
|
||||
computed: {
|
||||
reportError: defaultStore.makeGetterSetter('reportError'),
|
||||
},
|
||||
|
||||
methods: {
|
||||
changeDebug(v) {
|
||||
console.log(v);
|
||||
localStorage.setItem('debug', v.toString());
|
||||
unisonReload();
|
||||
},
|
||||
|
||||
onChangeInjectFeaturedNote(v) {
|
||||
os.api('i/update', {
|
||||
injectFeaturedNote: v
|
||||
});
|
||||
},
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: i18n.ts.other,
|
||||
icon: 'fas fa-ellipsis-h',
|
||||
bg: 'var(--bg)',
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
<template>
|
||||
<div class="_formRoot">
|
||||
<FormInfo warn class="_formBlock">{{ $ts._plugin.installWarn }}</FormInfo>
|
||||
<FormInfo warn class="_formBlock">{{ i18n.ts._plugin.installWarn }}</FormInfo>
|
||||
|
||||
<FormTextarea v-model="code" tall class="_formBlock">
|
||||
<template #label>{{ $ts.code }}</template>
|
||||
<template #label>{{ i18n.ts.code }}</template>
|
||||
</FormTextarea>
|
||||
|
||||
<div class="_formBlock">
|
||||
<FormButton :disabled="code == null" primary inline @click="install"><i class="fas fa-check"></i> {{ $ts.install }}</FormButton>
|
||||
<FormButton :disabled="code == null" primary inline @click="install"><i class="fas fa-check"></i> {{ i18n.ts.install }}</FormButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { defineExpose, defineAsyncComponent, nextTick, ref } from 'vue';
|
||||
import { AiScript, parse } from '@syuilo/aiscript';
|
||||
import { serialize } from '@syuilo/aiscript/built/serializer';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
@@ -23,111 +23,101 @@ import FormInfo from '@/components/ui/info.vue';
|
||||
import * as os from '@/os';
|
||||
import { ColdDeviceStorage } from '@/store';
|
||||
import { unisonReload } from '@/scripts/unison-reload';
|
||||
import { i18n } from '@/i18n';
|
||||
import * as symbols from '@/symbols';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
FormTextarea,
|
||||
FormButton,
|
||||
FormInfo,
|
||||
},
|
||||
const code = ref(null);
|
||||
|
||||
emits: ['info'],
|
||||
function installPlugin({ id, meta, ast, token }) {
|
||||
ColdDeviceStorage.set('plugins', ColdDeviceStorage.get('plugins').concat({
|
||||
...meta,
|
||||
id,
|
||||
active: true,
|
||||
configData: {},
|
||||
token: token,
|
||||
ast: ast
|
||||
}));
|
||||
}
|
||||
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: this.$ts._plugin.install,
|
||||
icon: 'fas fa-download',
|
||||
bg: 'var(--bg)',
|
||||
},
|
||||
code: null,
|
||||
}
|
||||
},
|
||||
async function install() {
|
||||
let ast;
|
||||
try {
|
||||
ast = parse(code.value);
|
||||
} catch (err) {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: 'Syntax error :('
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
methods: {
|
||||
installPlugin({ id, meta, ast, token }) {
|
||||
ColdDeviceStorage.set('plugins', ColdDeviceStorage.get('plugins').concat({
|
||||
...meta,
|
||||
id,
|
||||
active: true,
|
||||
configData: {},
|
||||
token: token,
|
||||
ast: ast
|
||||
}));
|
||||
const meta = AiScript.collectMetadata(ast);
|
||||
if (meta == null) {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: 'No metadata found :('
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const metadata = meta.get(null);
|
||||
if (metadata == null) {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: 'No metadata found :('
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const { name, version, author, description, permissions, config } = metadata;
|
||||
if (name == null || version == null || author == null) {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: 'Required property not found :('
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const token = permissions == null || permissions.length === 0 ? null : await new Promise((res, rej) => {
|
||||
os.popup(defineAsyncComponent(() => import('@/components/token-generate-window.vue')), {
|
||||
title: i18n.ts.tokenRequested,
|
||||
information: i18n.ts.pluginTokenRequestedDescription,
|
||||
initialName: name,
|
||||
initialPermissions: permissions
|
||||
}, {
|
||||
done: async result => {
|
||||
const { name, permissions } = result;
|
||||
const { token } = await os.api('miauth/gen-token', {
|
||||
session: null,
|
||||
name: name,
|
||||
permission: permissions,
|
||||
});
|
||||
res(token);
|
||||
}
|
||||
}, 'closed');
|
||||
});
|
||||
|
||||
installPlugin({
|
||||
id: uuid(),
|
||||
meta: {
|
||||
name, version, author, description, permissions, config
|
||||
},
|
||||
token,
|
||||
ast: serialize(ast)
|
||||
});
|
||||
|
||||
async install() {
|
||||
let ast;
|
||||
try {
|
||||
ast = parse(this.code);
|
||||
} catch (e) {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: 'Syntax error :('
|
||||
});
|
||||
return;
|
||||
}
|
||||
const meta = AiScript.collectMetadata(ast);
|
||||
if (meta == null) {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: 'No metadata found :('
|
||||
});
|
||||
return;
|
||||
}
|
||||
const data = meta.get(null);
|
||||
if (data == null) {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: 'No metadata found :('
|
||||
});
|
||||
return;
|
||||
}
|
||||
const { name, version, author, description, permissions, config } = data;
|
||||
if (name == null || version == null || author == null) {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: 'Required property not found :('
|
||||
});
|
||||
return;
|
||||
}
|
||||
os.success();
|
||||
|
||||
const token = permissions == null || permissions.length === 0 ? null : await new Promise((res, rej) => {
|
||||
os.popup(import('@/components/token-generate-window.vue'), {
|
||||
title: this.$ts.tokenRequested,
|
||||
information: this.$ts.pluginTokenRequestedDescription,
|
||||
initialName: name,
|
||||
initialPermissions: permissions
|
||||
}, {
|
||||
done: async result => {
|
||||
const { name, permissions } = result;
|
||||
const { token } = await os.api('miauth/gen-token', {
|
||||
session: null,
|
||||
name: name,
|
||||
permission: permissions,
|
||||
});
|
||||
nextTick(() => {
|
||||
unisonReload();
|
||||
});
|
||||
}
|
||||
|
||||
res(token);
|
||||
}
|
||||
}, 'closed');
|
||||
});
|
||||
|
||||
this.installPlugin({
|
||||
id: uuid(),
|
||||
meta: {
|
||||
name, version, author, description, permissions, config
|
||||
},
|
||||
token,
|
||||
ast: serialize(ast)
|
||||
});
|
||||
|
||||
os.success();
|
||||
|
||||
this.$nextTick(() => {
|
||||
unisonReload();
|
||||
});
|
||||
},
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: i18n.ts._plugin.install,
|
||||
icon: 'fas fa-download',
|
||||
bg: 'var(--bg)',
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,38 +1,38 @@
|
||||
<template>
|
||||
<div class="_formRoot">
|
||||
<FormLink to="/settings/plugin/install"><template #icon><i class="fas fa-download"></i></template>{{ $ts._plugin.install }}</FormLink>
|
||||
<FormLink to="/settings/plugin/install"><template #icon><i class="fas fa-download"></i></template>{{ i18n.ts._plugin.install }}</FormLink>
|
||||
|
||||
<FormSection>
|
||||
<template #label>{{ $ts.manage }}</template>
|
||||
<template #label>{{ i18n.ts.manage }}</template>
|
||||
<div v-for="plugin in plugins" :key="plugin.id" class="_formBlock _panel" style="padding: 20px;">
|
||||
<span style="display: flex;"><b>{{ plugin.name }}</b><span style="margin-left: auto;">v{{ plugin.version }}</span></span>
|
||||
|
||||
<FormSwitch class="_formBlock" :modelValue="plugin.active" @update:modelValue="changeActive(plugin, $event)">{{ $ts.makeActive }}</FormSwitch>
|
||||
<FormSwitch class="_formBlock" :modelValue="plugin.active" @update:modelValue="changeActive(plugin, $event)">{{ i18n.ts.makeActive }}</FormSwitch>
|
||||
|
||||
<MkKeyValue class="_formBlock">
|
||||
<template #key>{{ $ts.author }}</template>
|
||||
<template #key>{{ i18n.ts.author }}</template>
|
||||
<template #value>{{ plugin.author }}</template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue class="_formBlock">
|
||||
<template #key>{{ $ts.description }}</template>
|
||||
<template #key>{{ i18n.ts.description }}</template>
|
||||
<template #value>{{ plugin.description }}</template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue class="_formBlock">
|
||||
<template #key>{{ $ts.permission }}</template>
|
||||
<template #key>{{ i18n.ts.permission }}</template>
|
||||
<template #value>{{ plugin.permission }}</template>
|
||||
</MkKeyValue>
|
||||
|
||||
<div style="display: flex; gap: var(--margin); flex-wrap: wrap;">
|
||||
<MkButton v-if="plugin.config" inline @click="config(plugin)"><i class="fas fa-cog"></i> {{ $ts.settings }}</MkButton>
|
||||
<MkButton inline danger @click="uninstall(plugin)"><i class="fas fa-trash-alt"></i> {{ $ts.uninstall }}</MkButton>
|
||||
<MkButton v-if="plugin.config" inline @click="config(plugin)"><i class="fas fa-cog"></i> {{ i18n.ts.settings }}</MkButton>
|
||||
<MkButton inline danger @click="uninstall(plugin)"><i class="fas fa-trash-alt"></i> {{ i18n.ts.uninstall }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</FormSection>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { defineExpose, nextTick, ref } from 'vue';
|
||||
import FormLink from '@/components/form/link.vue';
|
||||
import FormSwitch from '@/components/form/switch.vue';
|
||||
import FormSection from '@/components/form/section.vue';
|
||||
@@ -41,67 +41,54 @@ import MkKeyValue from '@/components/key-value.vue';
|
||||
import * as os from '@/os';
|
||||
import { ColdDeviceStorage } from '@/store';
|
||||
import * as symbols from '@/symbols';
|
||||
import { unisonReload } from '@/scripts/unison-reload';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
FormLink,
|
||||
FormSwitch,
|
||||
FormSection,
|
||||
MkButton,
|
||||
MkKeyValue,
|
||||
},
|
||||
const plugins = ref(ColdDeviceStorage.get('plugins'));
|
||||
|
||||
emits: ['info'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: this.$ts.plugins,
|
||||
icon: 'fas fa-plug',
|
||||
bg: 'var(--bg)',
|
||||
},
|
||||
plugins: ColdDeviceStorage.get('plugins'),
|
||||
}
|
||||
},
|
||||
function uninstall(plugin) {
|
||||
ColdDeviceStorage.set('plugins', plugins.value.filter(x => x.id !== plugin.id));
|
||||
os.success();
|
||||
nextTick(() => {
|
||||
unisonReload();
|
||||
});
|
||||
}
|
||||
|
||||
methods: {
|
||||
uninstall(plugin) {
|
||||
ColdDeviceStorage.set('plugins', this.plugins.filter(x => x.id !== plugin.id));
|
||||
os.success();
|
||||
this.$nextTick(() => {
|
||||
unisonReload();
|
||||
});
|
||||
},
|
||||
// TODO: この処理をstore側にactionとして移動し、設定画面を開くAiScriptAPIを実装できるようにする
|
||||
async function config(plugin) {
|
||||
const config = plugin.config;
|
||||
for (const key in plugin.configData) {
|
||||
config[key].default = plugin.configData[key];
|
||||
}
|
||||
|
||||
// TODO: この処理をstore側にactionとして移動し、設定画面を開くAiScriptAPIを実装できるようにする
|
||||
async config(plugin) {
|
||||
const config = plugin.config;
|
||||
for (const key in plugin.configData) {
|
||||
config[key].default = plugin.configData[key];
|
||||
}
|
||||
const { canceled, result } = await os.form(plugin.name, config);
|
||||
if (canceled) return;
|
||||
|
||||
const { canceled, result } = await os.form(plugin.name, config);
|
||||
if (canceled) return;
|
||||
const coldPlugins = ColdDeviceStorage.get('plugins');
|
||||
coldPlugins.find(p => p.id === plugin.id)!.configData = result;
|
||||
ColdDeviceStorage.set('plugins', coldPlugins);
|
||||
|
||||
const plugins = ColdDeviceStorage.get('plugins');
|
||||
plugins.find(p => p.id === plugin.id).configData = result;
|
||||
ColdDeviceStorage.set('plugins', plugins);
|
||||
nextTick(() => {
|
||||
location.reload();
|
||||
});
|
||||
}
|
||||
|
||||
this.$nextTick(() => {
|
||||
location.reload();
|
||||
});
|
||||
},
|
||||
function changeActive(plugin, active) {
|
||||
const coldPlugins = ColdDeviceStorage.get('plugins');
|
||||
coldPlugins.find(p => p.id === plugin.id)!.active = active;
|
||||
ColdDeviceStorage.set('plugins', coldPlugins);
|
||||
|
||||
changeActive(plugin, active) {
|
||||
const plugins = ColdDeviceStorage.get('plugins');
|
||||
plugins.find(p => p.id === plugin.id).active = active;
|
||||
ColdDeviceStorage.set('plugins', plugins);
|
||||
nextTick(() => {
|
||||
location.reload();
|
||||
});
|
||||
}
|
||||
|
||||
this.$nextTick(() => {
|
||||
location.reload();
|
||||
});
|
||||
}
|
||||
},
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: i18n.ts.plugins,
|
||||
icon: 'fas fa-plug',
|
||||
bg: 'var(--bg)',
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { watch } from 'vue';
|
||||
import { defineAsyncComponent, watch } from 'vue';
|
||||
import XDraggable from 'vuedraggable';
|
||||
import FormInput from '@/components/form/input.vue';
|
||||
import FormRadios from '@/components/form/radios.vue';
|
||||
@@ -88,7 +88,7 @@ function remove(reaction, ev: MouseEvent) {
|
||||
}
|
||||
|
||||
function preview(ev: MouseEvent) {
|
||||
os.popup(import('@/components/emoji-picker-dialog.vue'), {
|
||||
os.popup(defineAsyncComponent(() => import('@/components/emoji-picker-dialog.vue')), {
|
||||
asReactionPicker: true,
|
||||
src: ev.currentTarget ?? ev.target,
|
||||
}, {}, 'closed');
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
<template>
|
||||
<div class="_formRoot">
|
||||
<FormSection>
|
||||
<template #label>{{ $ts.password }}</template>
|
||||
<FormButton primary @click="change()">{{ $ts.changePassword }}</FormButton>
|
||||
<template #label>{{ i18n.ts.password }}</template>
|
||||
<FormButton primary @click="change()">{{ i18n.ts.changePassword }}</FormButton>
|
||||
</FormSection>
|
||||
|
||||
<FormSection>
|
||||
<template #label>{{ $ts.twoStepAuthentication }}</template>
|
||||
<template #label>{{ i18n.ts.twoStepAuthentication }}</template>
|
||||
<X2fa/>
|
||||
</FormSection>
|
||||
|
||||
<FormSection>
|
||||
<template #label>{{ $ts.signinHistory }}</template>
|
||||
<template #label>{{ i18n.ts.signinHistory }}</template>
|
||||
<MkPagination :pagination="pagination">
|
||||
<template v-slot="{items}">
|
||||
<div>
|
||||
@@ -30,15 +30,15 @@
|
||||
|
||||
<FormSection>
|
||||
<FormSlot>
|
||||
<FormButton danger @click="regenerateToken"><i class="fas fa-sync-alt"></i> {{ $ts.regenerateLoginToken }}</FormButton>
|
||||
<template #caption>{{ $ts.regenerateLoginTokenDescription }}</template>
|
||||
<FormButton danger @click="regenerateToken"><i class="fas fa-sync-alt"></i> {{ i18n.ts.regenerateLoginToken }}</FormButton>
|
||||
<template #caption>{{ i18n.ts.regenerateLoginTokenDescription }}</template>
|
||||
</FormSlot>
|
||||
</FormSection>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { defineExpose } from 'vue';
|
||||
import FormSection from '@/components/form/section.vue';
|
||||
import FormSlot from '@/components/form/slot.vue';
|
||||
import FormButton from '@/components/ui/button.vue';
|
||||
@@ -46,77 +46,63 @@ import MkPagination from '@/components/ui/pagination.vue';
|
||||
import X2fa from './2fa.vue';
|
||||
import * as os from '@/os';
|
||||
import * as symbols from '@/symbols';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
FormSection,
|
||||
FormButton,
|
||||
MkPagination,
|
||||
FormSlot,
|
||||
X2fa,
|
||||
},
|
||||
const pagination = {
|
||||
endpoint: 'i/signin-history' as const,
|
||||
limit: 5,
|
||||
};
|
||||
|
||||
async function change() {
|
||||
const { canceled: canceled1, result: currentPassword } = await os.inputText({
|
||||
title: i18n.ts.currentPassword,
|
||||
type: 'password'
|
||||
});
|
||||
if (canceled1) return;
|
||||
|
||||
const { canceled: canceled2, result: newPassword } = await os.inputText({
|
||||
title: i18n.ts.newPassword,
|
||||
type: 'password'
|
||||
});
|
||||
if (canceled2) return;
|
||||
|
||||
const { canceled: canceled3, result: newPassword2 } = await os.inputText({
|
||||
title: i18n.ts.newPasswordRetype,
|
||||
type: 'password'
|
||||
});
|
||||
if (canceled3) return;
|
||||
|
||||
if (newPassword !== newPassword2) {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: i18n.ts.retypedNotMatch
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
emits: ['info'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: this.$ts.security,
|
||||
icon: 'fas fa-lock',
|
||||
bg: 'var(--bg)',
|
||||
},
|
||||
pagination: {
|
||||
endpoint: 'i/signin-history' as const,
|
||||
limit: 5,
|
||||
},
|
||||
}
|
||||
},
|
||||
os.apiWithDialog('i/change-password', {
|
||||
currentPassword,
|
||||
newPassword
|
||||
});
|
||||
}
|
||||
|
||||
methods: {
|
||||
async change() {
|
||||
const { canceled: canceled1, result: currentPassword } = await os.inputText({
|
||||
title: this.$ts.currentPassword,
|
||||
type: 'password'
|
||||
});
|
||||
if (canceled1) return;
|
||||
function regenerateToken() {
|
||||
os.inputText({
|
||||
title: i18n.ts.password,
|
||||
type: 'password'
|
||||
}).then(({ canceled, result: password }) => {
|
||||
if (canceled) return;
|
||||
os.api('i/regenerate_token', {
|
||||
password: password
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const { canceled: canceled2, result: newPassword } = await os.inputText({
|
||||
title: this.$ts.newPassword,
|
||||
type: 'password'
|
||||
});
|
||||
if (canceled2) return;
|
||||
|
||||
const { canceled: canceled3, result: newPassword2 } = await os.inputText({
|
||||
title: this.$ts.newPasswordRetype,
|
||||
type: 'password'
|
||||
});
|
||||
if (canceled3) return;
|
||||
|
||||
if (newPassword !== newPassword2) {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: this.$ts.retypedNotMatch
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
os.apiWithDialog('i/change-password', {
|
||||
currentPassword,
|
||||
newPassword
|
||||
});
|
||||
},
|
||||
|
||||
regenerateToken() {
|
||||
os.inputText({
|
||||
title: this.$ts.password,
|
||||
type: 'password'
|
||||
}).then(({ canceled, result: password }) => {
|
||||
if (canceled) return;
|
||||
os.api('i/regenerate_token', {
|
||||
password: password
|
||||
});
|
||||
});
|
||||
},
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: i18n.ts.security,
|
||||
icon: 'fas fa-lock',
|
||||
bg: 'var(--bg)',
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
<template>
|
||||
<div class="_formRoot">
|
||||
<FormRange v-model="masterVolume" :min="0" :max="1" :step="0.05" :text-converter="(v) => `${Math.floor(v * 100)}%`" class="_formBlock">
|
||||
<template #label>{{ $ts.masterVolume }}</template>
|
||||
<template #label>{{ i18n.ts.masterVolume }}</template>
|
||||
</FormRange>
|
||||
|
||||
<FormSection>
|
||||
<template #label>{{ $ts.sounds }}</template>
|
||||
<template #label>{{ i18n.ts.sounds }}</template>
|
||||
<FormLink v-for="type in Object.keys(sounds)" :key="type" style="margin-bottom: 8px;" @click="edit(type)">
|
||||
{{ $t('_sfx.' + type) }}
|
||||
<template #suffix>{{ sounds[type].type || $ts.none }}</template>
|
||||
<template #suffix>{{ sounds[type].type || i18n.ts.none }}</template>
|
||||
<template #suffixIcon><i class="fas fa-chevron-down"></i></template>
|
||||
</FormLink>
|
||||
</FormSection>
|
||||
|
||||
<FormButton danger class="_formBlock" @click="reset()"><i class="fas fa-redo"></i> {{ $ts.default }}</FormButton>
|
||||
<FormButton danger class="_formBlock" @click="reset()"><i class="fas fa-redo"></i> {{ i18n.ts.default }}</FormButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { computed, defineExpose, ref } from 'vue';
|
||||
import FormRange from '@/components/form/range.vue';
|
||||
import FormButton from '@/components/ui/button.vue';
|
||||
import FormLink from '@/components/form/link.vue';
|
||||
@@ -27,6 +27,28 @@ import * as os from '@/os';
|
||||
import { ColdDeviceStorage } from '@/store';
|
||||
import { playFile } from '@/scripts/sound';
|
||||
import * as symbols from '@/symbols';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
const masterVolume = computed({
|
||||
get: () => {
|
||||
return ColdDeviceStorage.get('sound_masterVolume');
|
||||
},
|
||||
set: (value) => {
|
||||
ColdDeviceStorage.set('sound_masterVolume', value);
|
||||
}
|
||||
});
|
||||
|
||||
const volumeIcon = computed(() => masterVolume.value === 0 ? 'fas fa-volume-mute' : 'fas fa-volume-up');
|
||||
|
||||
const sounds = ref({
|
||||
note: ColdDeviceStorage.get('sound_note'),
|
||||
noteMy: ColdDeviceStorage.get('sound_noteMy'),
|
||||
notification: ColdDeviceStorage.get('sound_notification'),
|
||||
chat: ColdDeviceStorage.get('sound_chat'),
|
||||
chatBg: ColdDeviceStorage.get('sound_chatBg'),
|
||||
antenna: ColdDeviceStorage.get('sound_antenna'),
|
||||
channel: ColdDeviceStorage.get('sound_channel'),
|
||||
});
|
||||
|
||||
const soundsTypes = [
|
||||
null,
|
||||
@@ -55,94 +77,58 @@ const soundsTypes = [
|
||||
'noizenecio/kick_gaba2',
|
||||
];
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
FormLink,
|
||||
FormButton,
|
||||
FormRange,
|
||||
FormSection,
|
||||
},
|
||||
|
||||
emits: ['info'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: this.$ts.sounds,
|
||||
icon: 'fas fa-music',
|
||||
bg: 'var(--bg)',
|
||||
},
|
||||
sounds: {},
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
masterVolume: { // TODO: (外部)関数にcomputedを使うのはアレなので直す
|
||||
get() { return ColdDeviceStorage.get('sound_masterVolume'); },
|
||||
set(value) { ColdDeviceStorage.set('sound_masterVolume', value); }
|
||||
async function edit(type) {
|
||||
const { canceled, result } = await os.form(i18n.t('_sfx.' + type), {
|
||||
type: {
|
||||
type: 'enum',
|
||||
enum: soundsTypes.map(x => ({
|
||||
value: x,
|
||||
label: x == null ? i18n.ts.none : x,
|
||||
})),
|
||||
label: i18n.ts.sound,
|
||||
default: sounds.value[type].type,
|
||||
},
|
||||
volumeIcon() {
|
||||
return this.masterVolume === 0 ? 'fas fa-volume-mute' : 'fas fa-volume-up';
|
||||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
this.sounds.note = ColdDeviceStorage.get('sound_note');
|
||||
this.sounds.noteMy = ColdDeviceStorage.get('sound_noteMy');
|
||||
this.sounds.notification = ColdDeviceStorage.get('sound_notification');
|
||||
this.sounds.chat = ColdDeviceStorage.get('sound_chat');
|
||||
this.sounds.chatBg = ColdDeviceStorage.get('sound_chatBg');
|
||||
this.sounds.antenna = ColdDeviceStorage.get('sound_antenna');
|
||||
this.sounds.channel = ColdDeviceStorage.get('sound_channel');
|
||||
},
|
||||
|
||||
methods: {
|
||||
async edit(type) {
|
||||
const { canceled, result } = await os.form(this.$t('_sfx.' + type), {
|
||||
type: {
|
||||
type: 'enum',
|
||||
enum: soundsTypes.map(x => ({
|
||||
value: x,
|
||||
label: x == null ? this.$ts.none : x,
|
||||
})),
|
||||
label: this.$ts.sound,
|
||||
default: this.sounds[type].type,
|
||||
},
|
||||
volume: {
|
||||
type: 'range',
|
||||
mim: 0,
|
||||
max: 1,
|
||||
step: 0.05,
|
||||
textConverter: (v) => `${Math.floor(v * 100)}%`,
|
||||
label: this.$ts.volume,
|
||||
default: this.sounds[type].volume
|
||||
},
|
||||
listen: {
|
||||
type: 'button',
|
||||
content: this.$ts.listen,
|
||||
action: (_, values) => {
|
||||
playFile(values.type, values.volume);
|
||||
}
|
||||
}
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
const v = {
|
||||
type: result.type,
|
||||
volume: result.volume,
|
||||
};
|
||||
|
||||
ColdDeviceStorage.set('sound_' + type, v);
|
||||
this.sounds[type] = v;
|
||||
volume: {
|
||||
type: 'range',
|
||||
mim: 0,
|
||||
max: 1,
|
||||
step: 0.05,
|
||||
textConverter: (v) => `${Math.floor(v * 100)}%`,
|
||||
label: i18n.ts.volume,
|
||||
default: sounds.value[type].volume
|
||||
},
|
||||
|
||||
reset() {
|
||||
for (const sound of Object.keys(this.sounds)) {
|
||||
const v = ColdDeviceStorage.default['sound_' + sound];
|
||||
ColdDeviceStorage.set('sound_' + sound, v);
|
||||
this.sounds[sound] = v;
|
||||
listen: {
|
||||
type: 'button',
|
||||
content: i18n.ts.listen,
|
||||
action: (_, values) => {
|
||||
playFile(values.type, values.volume);
|
||||
}
|
||||
}
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
const v = {
|
||||
type: result.type,
|
||||
volume: result.volume,
|
||||
};
|
||||
|
||||
ColdDeviceStorage.set('sound_' + type, v);
|
||||
sounds.value[type] = v;
|
||||
}
|
||||
|
||||
function reset() {
|
||||
for (const sound of Object.keys(sounds.value)) {
|
||||
const v = ColdDeviceStorage.default['sound_' + sound];
|
||||
ColdDeviceStorage.set('sound_' + sound, v);
|
||||
sounds.value[sound] = v;
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: i18n.ts.sounds,
|
||||
icon: 'fas fa-music',
|
||||
bg: 'var(--bg)',
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import * as JSON5 from 'json5';
|
||||
import JSON5 from 'json5';
|
||||
import FormTextarea from '@/components/form/textarea.vue';
|
||||
import FormButton from '@/components/ui/button.vue';
|
||||
import { applyTheme, validateTheme } from '@/scripts/theme';
|
||||
@@ -29,7 +29,7 @@ function parseThemeCode(code: string) {
|
||||
|
||||
try {
|
||||
theme = JSON5.parse(code);
|
||||
} catch (e) {
|
||||
} catch (err) {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: i18n.ts._theme.invalid
|
||||
|
||||
@@ -1,33 +1,33 @@
|
||||
<template>
|
||||
<div class="_formRoot">
|
||||
<FormSelect v-model="selectedThemeId" class="_formBlock">
|
||||
<template #label>{{ $ts.theme }}</template>
|
||||
<optgroup :label="$ts._theme.installedThemes">
|
||||
<template #label>{{ i18n.ts.theme }}</template>
|
||||
<optgroup :label="i18n.ts._theme.installedThemes">
|
||||
<option v-for="x in installedThemes" :key="x.id" :value="x.id">{{ x.name }}</option>
|
||||
</optgroup>
|
||||
<optgroup :label="$ts._theme.builtinThemes">
|
||||
<optgroup :label="i18n.ts._theme.builtinThemes">
|
||||
<option v-for="x in builtinThemes" :key="x.id" :value="x.id">{{ x.name }}</option>
|
||||
</optgroup>
|
||||
</FormSelect>
|
||||
<template v-if="selectedTheme">
|
||||
<FormInput readonly :modelValue="selectedTheme.author" class="_formBlock">
|
||||
<template #label>{{ $ts.author }}</template>
|
||||
<template #label>{{ i18n.ts.author }}</template>
|
||||
</FormInput>
|
||||
<FormTextarea v-if="selectedTheme.desc" readonly :modelValue="selectedTheme.desc" class="_formBlock">
|
||||
<template #label>{{ $ts._theme.description }}</template>
|
||||
<template #label>{{ i18n.ts._theme.description }}</template>
|
||||
</FormTextarea>
|
||||
<FormTextarea readonly tall :modelValue="selectedThemeCode" class="_formBlock">
|
||||
<template #label>{{ $ts._theme.code }}</template>
|
||||
<template #caption><button class="_textButton" @click="copyThemeCode()">{{ $ts.copy }}</button></template>
|
||||
<template #label>{{ i18n.ts._theme.code }}</template>
|
||||
<template #caption><button class="_textButton" @click="copyThemeCode()">{{ i18n.ts.copy }}</button></template>
|
||||
</FormTextarea>
|
||||
<FormButton v-if="!builtinThemes.some(t => t.id == selectedTheme.id)" class="_formBlock" danger @click="uninstall()"><i class="fas fa-trash-alt"></i> {{ $ts.uninstall }}</FormButton>
|
||||
<FormButton v-if="!builtinThemes.some(t => t.id == selectedTheme.id)" class="_formBlock" danger @click="uninstall()"><i class="fas fa-trash-alt"></i> {{ i18n.ts.uninstall }}</FormButton>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import * as JSON5 from 'json5';
|
||||
<script lang="ts" setup>
|
||||
import { computed, defineExpose, ref } from 'vue';
|
||||
import JSON5 from 'json5';
|
||||
import FormTextarea from '@/components/form/textarea.vue';
|
||||
import FormSelect from '@/components/form/select.vue';
|
||||
import FormInput from '@/components/form/input.vue';
|
||||
@@ -35,61 +35,42 @@ import FormButton from '@/components/ui/button.vue';
|
||||
import { Theme, builtinThemes } from '@/scripts/theme';
|
||||
import copyToClipboard from '@/scripts/copy-to-clipboard';
|
||||
import * as os from '@/os';
|
||||
import { ColdDeviceStorage } from '@/store';
|
||||
import { getThemes, removeTheme } from '@/theme-store';
|
||||
import * as symbols from '@/symbols';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
FormTextarea,
|
||||
FormSelect,
|
||||
FormInput,
|
||||
FormButton,
|
||||
},
|
||||
const installedThemes = ref(getThemes());
|
||||
const selectedThemeId = ref(null);
|
||||
|
||||
emits: ['info'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: this.$ts._theme.manage,
|
||||
icon: 'fas fa-folder-open',
|
||||
bg: 'var(--bg)',
|
||||
},
|
||||
installedThemes: getThemes(),
|
||||
builtinThemes,
|
||||
selectedThemeId: null,
|
||||
}
|
||||
},
|
||||
const themes = computed(() => builtinThemes.concat(installedThemes.value));
|
||||
|
||||
computed: {
|
||||
themes(): Theme[] {
|
||||
return this.builtinThemes.concat(this.installedThemes);
|
||||
},
|
||||
|
||||
selectedTheme() {
|
||||
if (this.selectedThemeId == null) return null;
|
||||
return this.themes.find(x => x.id === this.selectedThemeId);
|
||||
},
|
||||
const selectedTheme = computed(() => {
|
||||
if (selectedThemeId.value == null) return null;
|
||||
return themes.value.find(x => x.id === selectedThemeId.value);
|
||||
});
|
||||
|
||||
selectedThemeCode() {
|
||||
if (this.selectedTheme == null) return null;
|
||||
return JSON5.stringify(this.selectedTheme, null, '\t');
|
||||
},
|
||||
},
|
||||
const selectedThemeCode = computed(() => {
|
||||
if (selectedTheme.value == null) return null;
|
||||
return JSON5.stringify(selectedTheme.value, null, '\t');
|
||||
});
|
||||
|
||||
methods: {
|
||||
copyThemeCode() {
|
||||
copyToClipboard(this.selectedThemeCode);
|
||||
os.success();
|
||||
},
|
||||
function copyThemeCode() {
|
||||
copyToClipboard(selectedThemeCode.value);
|
||||
os.success();
|
||||
}
|
||||
|
||||
uninstall() {
|
||||
removeTheme(this.selectedTheme);
|
||||
this.installedThemes = this.installedThemes.filter(t => t.id !== this.selectedThemeId);
|
||||
this.selectedThemeId = null;
|
||||
os.success();
|
||||
},
|
||||
function uninstall() {
|
||||
removeTheme(selectedTheme.value as Theme);
|
||||
installedThemes.value = installedThemes.value.filter(t => t.id !== selectedThemeId.value);
|
||||
selectedThemeId.value = null;
|
||||
os.success();
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: i18n.ts._theme.manage,
|
||||
icon: 'fas fa-folder-open',
|
||||
bg: 'var(--bg)',
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -85,12 +85,11 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, onActivated, onMounted, ref, watch } from 'vue';
|
||||
import * as JSON5 from 'json5';
|
||||
<script lang="ts" setup>
|
||||
import { computed, onActivated, ref, watch } from 'vue';
|
||||
import JSON5 from 'json5';
|
||||
import FormSwitch from '@/components/form/switch.vue';
|
||||
import FormSelect from '@/components/form/select.vue';
|
||||
import FormGroup from '@/components/form/group.vue';
|
||||
import FormSection from '@/components/form/section.vue';
|
||||
import FormLink from '@/components/form/link.vue';
|
||||
import FormButton from '@/components/ui/button.vue';
|
||||
@@ -101,100 +100,78 @@ import { ColdDeviceStorage } from '@/store';
|
||||
import { i18n } from '@/i18n';
|
||||
import { defaultStore } from '@/store';
|
||||
import { instance } from '@/instance';
|
||||
import { concat, uniqueBy } from '@/scripts/array';
|
||||
import { uniqueBy } from '@/scripts/array';
|
||||
import { fetchThemes, getThemes } from '@/theme-store';
|
||||
import * as symbols from '@/symbols';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
FormSwitch,
|
||||
FormSelect,
|
||||
FormGroup,
|
||||
FormSection,
|
||||
FormLink,
|
||||
FormButton,
|
||||
const installedThemes = ref(getThemes());
|
||||
const instanceThemes = [];
|
||||
|
||||
if (instance.defaultLightTheme != null) instanceThemes.push(JSON5.parse(instance.defaultLightTheme));
|
||||
if (instance.defaultDarkTheme != null) instanceThemes.push(JSON5.parse(instance.defaultDarkTheme));
|
||||
|
||||
const themes = computed(() => uniqueBy(instanceThemes.concat(builtinThemes.concat(installedThemes.value)), theme => theme.id));
|
||||
const darkThemes = computed(() => themes.value.filter(t => t.base === 'dark' || t.kind === 'dark'));
|
||||
const lightThemes = computed(() => themes.value.filter(t => t.base === 'light' || t.kind === 'light'));
|
||||
const darkTheme = ColdDeviceStorage.ref('darkTheme');
|
||||
const darkThemeId = computed({
|
||||
get() {
|
||||
return darkTheme.value.id;
|
||||
},
|
||||
set(id) {
|
||||
ColdDeviceStorage.set('darkTheme', themes.value.find(x => x.id === id))
|
||||
}
|
||||
});
|
||||
const lightTheme = ColdDeviceStorage.ref('lightTheme');
|
||||
const lightThemeId = computed({
|
||||
get() {
|
||||
return lightTheme.value.id;
|
||||
},
|
||||
set(id) {
|
||||
ColdDeviceStorage.set('lightTheme', themes.value.find(x => x.id === id))
|
||||
}
|
||||
});
|
||||
const darkMode = computed(defaultStore.makeGetterSetter('darkMode'));
|
||||
const syncDeviceDarkMode = computed(ColdDeviceStorage.makeGetterSetter('syncDeviceDarkMode'));
|
||||
const wallpaper = ref(localStorage.getItem('wallpaper'));
|
||||
const themesCount = installedThemes.value.length;
|
||||
|
||||
emits: ['info'],
|
||||
watch(syncDeviceDarkMode, () => {
|
||||
if (syncDeviceDarkMode.value) {
|
||||
defaultStore.set('darkMode', isDeviceDarkmode());
|
||||
}
|
||||
});
|
||||
|
||||
setup(props, { emit }) {
|
||||
const INFO = {
|
||||
title: i18n.ts.theme,
|
||||
icon: 'fas fa-palette',
|
||||
bg: 'var(--bg)',
|
||||
};
|
||||
watch(wallpaper, () => {
|
||||
if (wallpaper.value == null) {
|
||||
localStorage.removeItem('wallpaper');
|
||||
} else {
|
||||
localStorage.setItem('wallpaper', wallpaper.value);
|
||||
}
|
||||
location.reload();
|
||||
});
|
||||
|
||||
const installedThemes = ref(getThemes());
|
||||
const instanceThemes = [];
|
||||
if (instance.defaultLightTheme != null) instanceThemes.push(JSON5.parse(instance.defaultLightTheme));
|
||||
if (instance.defaultDarkTheme != null) instanceThemes.push(JSON5.parse(instance.defaultDarkTheme));
|
||||
const themes = computed(() => uniqueBy(instanceThemes.concat(builtinThemes.concat(installedThemes.value)), theme => theme.id));
|
||||
const darkThemes = computed(() => themes.value.filter(t => t.base === 'dark' || t.kind === 'dark'));
|
||||
const lightThemes = computed(() => themes.value.filter(t => t.base === 'light' || t.kind === 'light'));
|
||||
const darkTheme = ColdDeviceStorage.ref('darkTheme');
|
||||
const darkThemeId = computed({
|
||||
get() {
|
||||
return darkTheme.value.id;
|
||||
},
|
||||
set(id) {
|
||||
ColdDeviceStorage.set('darkTheme', themes.value.find(x => x.id === id))
|
||||
}
|
||||
});
|
||||
const lightTheme = ColdDeviceStorage.ref('lightTheme');
|
||||
const lightThemeId = computed({
|
||||
get() {
|
||||
return lightTheme.value.id;
|
||||
},
|
||||
set(id) {
|
||||
ColdDeviceStorage.set('lightTheme', themes.value.find(x => x.id === id))
|
||||
}
|
||||
});
|
||||
const darkMode = computed(defaultStore.makeGetterSetter('darkMode'));
|
||||
const syncDeviceDarkMode = computed(ColdDeviceStorage.makeGetterSetter('syncDeviceDarkMode'));
|
||||
const wallpaper = ref(localStorage.getItem('wallpaper'));
|
||||
const themesCount = installedThemes.value.length;
|
||||
onActivated(() => {
|
||||
fetchThemes().then(() => {
|
||||
installedThemes.value = getThemes();
|
||||
});
|
||||
});
|
||||
|
||||
watch(syncDeviceDarkMode, () => {
|
||||
if (syncDeviceDarkMode.value) {
|
||||
defaultStore.set('darkMode', isDeviceDarkmode());
|
||||
}
|
||||
});
|
||||
fetchThemes().then(() => {
|
||||
installedThemes.value = getThemes();
|
||||
});
|
||||
|
||||
watch(wallpaper, () => {
|
||||
if (wallpaper.value == null) {
|
||||
localStorage.removeItem('wallpaper');
|
||||
} else {
|
||||
localStorage.setItem('wallpaper', wallpaper.value);
|
||||
}
|
||||
location.reload();
|
||||
});
|
||||
function setWallpaper(event) {
|
||||
selectFile(event.currentTarget ?? event.target, null).then(file => {
|
||||
wallpaper.value = file.url;
|
||||
});
|
||||
}
|
||||
|
||||
onActivated(() => {
|
||||
fetchThemes().then(() => {
|
||||
installedThemes.value = getThemes();
|
||||
});
|
||||
});
|
||||
|
||||
fetchThemes().then(() => {
|
||||
installedThemes.value = getThemes();
|
||||
});
|
||||
|
||||
return {
|
||||
[symbols.PAGE_INFO]: INFO,
|
||||
darkThemes,
|
||||
lightThemes,
|
||||
darkThemeId,
|
||||
lightThemeId,
|
||||
darkMode,
|
||||
syncDeviceDarkMode,
|
||||
themesCount,
|
||||
wallpaper,
|
||||
setWallpaper(e) {
|
||||
selectFile(e.currentTarget ?? e.target, null).then(file => {
|
||||
wallpaper.value = file.url;
|
||||
});
|
||||
},
|
||||
};
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: i18n.ts.theme,
|
||||
icon: 'fas fa-palette',
|
||||
bg: 'var(--bg)',
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -43,6 +43,14 @@ import * as os from '@/os';
|
||||
import * as symbols from '@/symbols';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: 'Edit webhook',
|
||||
icon: 'fas fa-bolt',
|
||||
bg: 'var(--bg)',
|
||||
},
|
||||
});
|
||||
|
||||
const webhook = await os.api('i/webhooks/show', {
|
||||
webhookId: new URLSearchParams(window.location.search).get('id')
|
||||
});
|
||||
@@ -78,12 +86,4 @@ async function save(): Promise<void> {
|
||||
active,
|
||||
});
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: 'Edit webhook',
|
||||
icon: 'fas fa-bolt',
|
||||
bg: 'var(--bg)',
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,35 +1,35 @@
|
||||
<template>
|
||||
<div class="_formRoot">
|
||||
<MkTab v-model="tab" class="_formBlock">
|
||||
<option value="soft">{{ $ts._wordMute.soft }}</option>
|
||||
<option value="hard">{{ $ts._wordMute.hard }}</option>
|
||||
<option value="soft">{{ i18n.ts._wordMute.soft }}</option>
|
||||
<option value="hard">{{ i18n.ts._wordMute.hard }}</option>
|
||||
</MkTab>
|
||||
<div class="_formBlock">
|
||||
<div v-show="tab === 'soft'">
|
||||
<MkInfo class="_formBlock">{{ $ts._wordMute.softDescription }}</MkInfo>
|
||||
<MkInfo class="_formBlock">{{ i18n.ts._wordMute.softDescription }}</MkInfo>
|
||||
<FormTextarea v-model="softMutedWords" class="_formBlock">
|
||||
<span>{{ $ts._wordMute.muteWords }}</span>
|
||||
<template #caption>{{ $ts._wordMute.muteWordsDescription }}<br>{{ $ts._wordMute.muteWordsDescription2 }}</template>
|
||||
<span>{{ i18n.ts._wordMute.muteWords }}</span>
|
||||
<template #caption>{{ i18n.ts._wordMute.muteWordsDescription }}<br>{{ i18n.ts._wordMute.muteWordsDescription2 }}</template>
|
||||
</FormTextarea>
|
||||
</div>
|
||||
<div v-show="tab === 'hard'">
|
||||
<MkInfo class="_formBlock">{{ $ts._wordMute.hardDescription }} {{ $ts.reflectMayTakeTime }}</MkInfo>
|
||||
<MkInfo class="_formBlock">{{ i18n.ts._wordMute.hardDescription }} {{ i18n.ts.reflectMayTakeTime }}</MkInfo>
|
||||
<FormTextarea v-model="hardMutedWords" class="_formBlock">
|
||||
<span>{{ $ts._wordMute.muteWords }}</span>
|
||||
<template #caption>{{ $ts._wordMute.muteWordsDescription }}<br>{{ $ts._wordMute.muteWordsDescription2 }}</template>
|
||||
<span>{{ i18n.ts._wordMute.muteWords }}</span>
|
||||
<template #caption>{{ i18n.ts._wordMute.muteWordsDescription }}<br>{{ i18n.ts._wordMute.muteWordsDescription2 }}</template>
|
||||
</FormTextarea>
|
||||
<MkKeyValue v-if="hardWordMutedNotesCount != null" class="_formBlock">
|
||||
<template #key>{{ $ts._wordMute.mutedNotes }}</template>
|
||||
<template #key>{{ i18n.ts._wordMute.mutedNotes }}</template>
|
||||
<template #value>{{ number(hardWordMutedNotesCount) }}</template>
|
||||
</MkKeyValue>
|
||||
</div>
|
||||
</div>
|
||||
<MkButton primary inline :disabled="!changed" @click="save()"><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
|
||||
<MkButton primary inline :disabled="!changed" @click="save()"><i class="fas fa-save"></i> {{ i18n.ts.save }}</MkButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { defineExpose, ref, watch } from 'vue';
|
||||
import FormTextarea from '@/components/form/textarea.vue';
|
||||
import MkKeyValue from '@/components/key-value.vue';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
@@ -38,114 +38,90 @@ import MkTab from '@/components/tab.vue';
|
||||
import * as os from '@/os';
|
||||
import number from '@/filters/number';
|
||||
import * as symbols from '@/symbols';
|
||||
import { defaultStore } from '@/store';
|
||||
import { $i } from '@/account';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkButton,
|
||||
FormTextarea,
|
||||
MkKeyValue,
|
||||
MkTab,
|
||||
MkInfo,
|
||||
},
|
||||
const render = (mutedWords) => mutedWords.map(x => {
|
||||
if (Array.isArray(x)) {
|
||||
return x.join(' ');
|
||||
} else {
|
||||
return x;
|
||||
}
|
||||
}).join('\n');
|
||||
|
||||
emits: ['info'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: this.$ts.wordMute,
|
||||
icon: 'fas fa-comment-slash',
|
||||
bg: 'var(--bg)',
|
||||
},
|
||||
tab: 'soft',
|
||||
softMutedWords: '',
|
||||
hardMutedWords: '',
|
||||
hardWordMutedNotesCount: null,
|
||||
changed: false,
|
||||
}
|
||||
},
|
||||
const tab = ref('soft');
|
||||
const softMutedWords = ref(render(defaultStore.state.mutedWords));
|
||||
const hardMutedWords = ref(render($i!.mutedWords));
|
||||
const hardWordMutedNotesCount = ref(null);
|
||||
const changed = ref(false);
|
||||
|
||||
watch: {
|
||||
softMutedWords: {
|
||||
handler() {
|
||||
this.changed = true;
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
hardMutedWords: {
|
||||
handler() {
|
||||
this.changed = true;
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
},
|
||||
os.api('i/get-word-muted-notes-count', {}).then(response => {
|
||||
hardWordMutedNotesCount.value = response?.count;
|
||||
});
|
||||
|
||||
async created() {
|
||||
const render = (mutedWords) => mutedWords.map(x => {
|
||||
if (Array.isArray(x)) {
|
||||
return x.join(' ');
|
||||
} else {
|
||||
return x;
|
||||
}
|
||||
}).join('\n');
|
||||
watch(softMutedWords, () => {
|
||||
changed.value = true;
|
||||
});
|
||||
|
||||
this.softMutedWords = render(this.$store.state.mutedWords);
|
||||
this.hardMutedWords = render(this.$i.mutedWords);
|
||||
watch(hardMutedWords, () => {
|
||||
changed.value = true;
|
||||
});
|
||||
|
||||
this.hardWordMutedNotesCount = (await os.api('i/get-word-muted-notes-count', {})).count;
|
||||
},
|
||||
async function save() {
|
||||
const parseMutes = (mutes, tab) => {
|
||||
// split into lines, remove empty lines and unnecessary whitespace
|
||||
let lines = mutes.trim().split('\n').map(line => line.trim()).filter(line => line !== '');
|
||||
|
||||
methods: {
|
||||
async save() {
|
||||
const parseMutes = (mutes, tab) => {
|
||||
// split into lines, remove empty lines and unnecessary whitespace
|
||||
let lines = mutes.trim().split('\n').map(line => line.trim()).filter(line => line != '');
|
||||
|
||||
// check each line if it is a RegExp or not
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i]
|
||||
const regexp = line.match(/^\/(.+)\/(.*)$/);
|
||||
if (regexp) {
|
||||
// check that the RegExp is valid
|
||||
try {
|
||||
new RegExp(regexp[1], regexp[2]);
|
||||
// note that regex lines will not be split by spaces!
|
||||
} catch (err) {
|
||||
// invalid syntax: do not save, do not reset changed flag
|
||||
os.alert({
|
||||
type: 'error',
|
||||
title: this.$ts.regexpError,
|
||||
text: this.$t('regexpErrorDescription', { tab, line: i + 1 }) + "\n" + err.toString()
|
||||
});
|
||||
// re-throw error so these invalid settings are not saved
|
||||
throw err;
|
||||
}
|
||||
} else {
|
||||
lines[i] = line.split(' ');
|
||||
}
|
||||
// check each line if it is a RegExp or not
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i]
|
||||
const regexp = line.match(/^\/(.+)\/(.*)$/);
|
||||
if (regexp) {
|
||||
// check that the RegExp is valid
|
||||
try {
|
||||
new RegExp(regexp[1], regexp[2]);
|
||||
// note that regex lines will not be split by spaces!
|
||||
} catch (err: any) {
|
||||
// invalid syntax: do not save, do not reset changed flag
|
||||
os.alert({
|
||||
type: 'error',
|
||||
title: i18n.ts.regexpError,
|
||||
text: i18n.t('regexpErrorDescription', { tab, line: i + 1 }) + "\n" + err.toString()
|
||||
});
|
||||
// re-throw error so these invalid settings are not saved
|
||||
throw err;
|
||||
}
|
||||
|
||||
return lines;
|
||||
};
|
||||
|
||||
let softMutes, hardMutes;
|
||||
try {
|
||||
softMutes = parseMutes(this.softMutedWords, this.$ts._wordMute.soft);
|
||||
hardMutes = parseMutes(this.hardMutedWords, this.$ts._wordMute.hard);
|
||||
} catch (err) {
|
||||
// already displayed error message in parseMutes
|
||||
return;
|
||||
} else {
|
||||
lines[i] = line.split(' ');
|
||||
}
|
||||
}
|
||||
|
||||
this.$store.set('mutedWords', softMutes);
|
||||
await os.api('i/update', {
|
||||
mutedWords: hardMutes,
|
||||
});
|
||||
return lines;
|
||||
};
|
||||
|
||||
this.changed = false;
|
||||
},
|
||||
let softMutes, hardMutes;
|
||||
try {
|
||||
softMutes = parseMutes(softMutedWords.value, i18n.ts._wordMute.soft);
|
||||
hardMutes = parseMutes(hardMutedWords.value, i18n.ts._wordMute.hard);
|
||||
} catch (err) {
|
||||
// already displayed error message in parseMutes
|
||||
return;
|
||||
}
|
||||
|
||||
number
|
||||
defaultStore.set('mutedWords', softMutes);
|
||||
await os.api('i/update', {
|
||||
mutedWords: hardMutes,
|
||||
});
|
||||
|
||||
changed.value = false;
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: i18n.ts.wordMute,
|
||||
icon: 'fas fa-comment-slash',
|
||||
bg: 'var(--bg)',
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user