247
packages/client/src/pages/settings/2fa.vue
Normal file
247
packages/client/src/pages/settings/2fa.vue
Normal file
@ -0,0 +1,247 @@
|
||||
<template>
|
||||
<section class="_card">
|
||||
<div class="_title"><i class="fas fa-lock"></i> {{ $ts.twoStepAuthentication }}</div>
|
||||
<div class="_content">
|
||||
<MkButton v-if="!data && !$i.twoFactorEnabled" @click="register">{{ $ts._2fa.registerDevice }}</MkButton>
|
||||
<template v-if="$i.twoFactorEnabled">
|
||||
<p>{{ $ts._2fa.alreadyRegistered }}</p>
|
||||
<MkButton @click="unregister">{{ $ts.unregister }}</MkButton>
|
||||
|
||||
<template v-if="supportsCredentials">
|
||||
<hr class="totp-method-sep">
|
||||
|
||||
<h2 class="heading">{{ $ts.securityKey }}</h2>
|
||||
<p>{{ $ts._2fa.securityKeyInfo }}</p>
|
||||
<div class="key-list">
|
||||
<div class="key" v-for="key in $i.securityKeysList">
|
||||
<h3>{{ key.name }}</h3>
|
||||
<div class="last-used">{{ $ts.lastUsed }}<MkTime :time="key.lastUsed"/></div>
|
||||
<MkButton @click="unregisterKey(key)">{{ $ts.unregister }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<MkSwitch v-model="usePasswordLessLogin" @update:modelValue="updatePasswordLessLogin" v-if="$i.securityKeysList.length > 0">{{ $ts.passwordLessLogin }}</MkSwitch>
|
||||
|
||||
<MkInfo warn v-if="registration && registration.error">{{ $ts.error }} {{ registration.error }}</MkInfo>
|
||||
<MkButton v-if="!registration || registration.error" @click="addSecurityKey">{{ $ts._2fa.registerKey }}</MkButton>
|
||||
|
||||
<ol v-if="registration && !registration.error">
|
||||
<li v-if="registration.stage >= 0">
|
||||
{{ $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>
|
||||
</MkInput>
|
||||
<MkButton @click="registerKey" :disabled="keyName.length == 0">{{ $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">
|
||||
<ol style="margin: 0; padding: 0 0 0 1em;">
|
||||
<li>
|
||||
<I18n :src="$ts._2fa.step1" tag="span">
|
||||
<template #a>
|
||||
<a href="https://authy.com/" rel="noopener" target="_blank" class="_link">Authy</a>
|
||||
</template>
|
||||
<template #b>
|
||||
<a href="https://support.google.com/accounts/answer/1066447" rel="noopener" target="_blank" class="_link">Google Authenticator</a>
|
||||
</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>
|
||||
</ol>
|
||||
<MkInfo>{{ $ts._2fa.step4 }}</MkInfo>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import { hostname } from '@/config';
|
||||
import { byteify, hexify, stringify } from '@/scripts/2fa';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import MkInfo from '@/components/ui/info.vue';
|
||||
import MkInput from '@/components/form/input.vue';
|
||||
import MkSwitch from '@/components/form/switch.vue';
|
||||
import FormBase from '@/components/debobigego/base.vue';
|
||||
import FormGroup from '@/components/debobigego/group.vue';
|
||||
import FormButton from '@/components/debobigego/button.vue';
|
||||
import * as os from '@/os';
|
||||
import * as symbols from '@/symbols';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
FormBase,
|
||||
MkButton, MkInfo, MkInput, MkSwitch
|
||||
},
|
||||
|
||||
emits: ['info'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: this.$ts.twoStepAuthentication,
|
||||
icon: 'fas fa-lock'
|
||||
},
|
||||
data: null,
|
||||
supportsCredentials: !!navigator.credentials,
|
||||
usePasswordLessLogin: this.$i.usePasswordLessLogin,
|
||||
registration: null,
|
||||
keyName: '',
|
||||
token: null,
|
||||
};
|
||||
},
|
||||
|
||||
methods: {
|
||||
register() {
|
||||
os.dialog({
|
||||
title: this.$ts.password,
|
||||
input: {
|
||||
type: 'password'
|
||||
}
|
||||
}).then(({ canceled, result: password }) => {
|
||||
if (canceled) return;
|
||||
os.api('i/2fa/register', {
|
||||
password: password
|
||||
}).then(data => {
|
||||
this.data = data;
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
unregister() {
|
||||
os.dialog({
|
||||
title: this.$ts.password,
|
||||
input: {
|
||||
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.dialog({
|
||||
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.dialog({
|
||||
title: this.$ts.password,
|
||||
input: {
|
||||
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.dialog({
|
||||
title: this.$ts.password,
|
||||
input: {
|
||||
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
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
185
packages/client/src/pages/settings/account-info.vue
Normal file
185
packages/client/src/pages/settings/account-info.vue
Normal file
@ -0,0 +1,185 @@
|
||||
<template>
|
||||
<FormBase>
|
||||
<FormKeyValueView>
|
||||
<template #key>ID</template>
|
||||
<template #value><span class="_monospace">{{ $i.id }}</span></template>
|
||||
</FormKeyValueView>
|
||||
|
||||
<FormGroup>
|
||||
<FormKeyValueView>
|
||||
<template #key>{{ $ts.registeredDate }}</template>
|
||||
<template #value><MkTime :time="$i.createdAt" mode="detail"/></template>
|
||||
</FormKeyValueView>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup v-if="stats">
|
||||
<template #label>{{ $ts.statistics }}</template>
|
||||
<FormKeyValueView>
|
||||
<template #key>{{ $ts.notesCount }}</template>
|
||||
<template #value>{{ number(stats.notesCount) }}</template>
|
||||
</FormKeyValueView>
|
||||
<FormKeyValueView>
|
||||
<template #key>{{ $ts.repliesCount }}</template>
|
||||
<template #value>{{ number(stats.repliesCount) }}</template>
|
||||
</FormKeyValueView>
|
||||
<FormKeyValueView>
|
||||
<template #key>{{ $ts.renotesCount }}</template>
|
||||
<template #value>{{ number(stats.renotesCount) }}</template>
|
||||
</FormKeyValueView>
|
||||
<FormKeyValueView>
|
||||
<template #key>{{ $ts.repliedCount }}</template>
|
||||
<template #value>{{ number(stats.repliedCount) }}</template>
|
||||
</FormKeyValueView>
|
||||
<FormKeyValueView>
|
||||
<template #key>{{ $ts.renotedCount }}</template>
|
||||
<template #value>{{ number(stats.renotedCount) }}</template>
|
||||
</FormKeyValueView>
|
||||
<FormKeyValueView>
|
||||
<template #key>{{ $ts.pollVotesCount }}</template>
|
||||
<template #value>{{ number(stats.pollVotesCount) }}</template>
|
||||
</FormKeyValueView>
|
||||
<FormKeyValueView>
|
||||
<template #key>{{ $ts.pollVotedCount }}</template>
|
||||
<template #value>{{ number(stats.pollVotedCount) }}</template>
|
||||
</FormKeyValueView>
|
||||
<FormKeyValueView>
|
||||
<template #key>{{ $ts.sentReactionsCount }}</template>
|
||||
<template #value>{{ number(stats.sentReactionsCount) }}</template>
|
||||
</FormKeyValueView>
|
||||
<FormKeyValueView>
|
||||
<template #key>{{ $ts.receivedReactionsCount }}</template>
|
||||
<template #value>{{ number(stats.receivedReactionsCount) }}</template>
|
||||
</FormKeyValueView>
|
||||
<FormKeyValueView>
|
||||
<template #key>{{ $ts.noteFavoritesCount }}</template>
|
||||
<template #value>{{ number(stats.noteFavoritesCount) }}</template>
|
||||
</FormKeyValueView>
|
||||
<FormKeyValueView>
|
||||
<template #key>{{ $ts.followingCount }}</template>
|
||||
<template #value>{{ number(stats.followingCount) }}</template>
|
||||
</FormKeyValueView>
|
||||
<FormKeyValueView>
|
||||
<template #key>{{ $ts.followingCount }} ({{ $ts.local }})</template>
|
||||
<template #value>{{ number(stats.localFollowingCount) }}</template>
|
||||
</FormKeyValueView>
|
||||
<FormKeyValueView>
|
||||
<template #key>{{ $ts.followingCount }} ({{ $ts.remote }})</template>
|
||||
<template #value>{{ number(stats.remoteFollowingCount) }}</template>
|
||||
</FormKeyValueView>
|
||||
<FormKeyValueView>
|
||||
<template #key>{{ $ts.followersCount }}</template>
|
||||
<template #value>{{ number(stats.followersCount) }}</template>
|
||||
</FormKeyValueView>
|
||||
<FormKeyValueView>
|
||||
<template #key>{{ $ts.followersCount }} ({{ $ts.local }})</template>
|
||||
<template #value>{{ number(stats.localFollowersCount) }}</template>
|
||||
</FormKeyValueView>
|
||||
<FormKeyValueView>
|
||||
<template #key>{{ $ts.followersCount }} ({{ $ts.remote }})</template>
|
||||
<template #value>{{ number(stats.remoteFollowersCount) }}</template>
|
||||
</FormKeyValueView>
|
||||
<FormKeyValueView>
|
||||
<template #key>{{ $ts.pageLikesCount }}</template>
|
||||
<template #value>{{ number(stats.pageLikesCount) }}</template>
|
||||
</FormKeyValueView>
|
||||
<FormKeyValueView>
|
||||
<template #key>{{ $ts.pageLikedCount }}</template>
|
||||
<template #value>{{ number(stats.pageLikedCount) }}</template>
|
||||
</FormKeyValueView>
|
||||
<FormKeyValueView>
|
||||
<template #key>{{ $ts.driveFilesCount }}</template>
|
||||
<template #value>{{ number(stats.driveFilesCount) }}</template>
|
||||
</FormKeyValueView>
|
||||
<FormKeyValueView>
|
||||
<template #key>{{ $ts.driveUsage }}</template>
|
||||
<template #value>{{ bytes(stats.driveUsage) }}</template>
|
||||
</FormKeyValueView>
|
||||
<FormKeyValueView>
|
||||
<template #key>{{ $ts.reversiCount }}</template>
|
||||
<template #value>{{ number(stats.reversiCount) }}</template>
|
||||
</FormKeyValueView>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<template #label>{{ $ts.other }}</template>
|
||||
<FormKeyValueView>
|
||||
<template #key>emailVerified</template>
|
||||
<template #value>{{ $i.emailVerified ? $ts.yes : $ts.no }}</template>
|
||||
</FormKeyValueView>
|
||||
<FormKeyValueView>
|
||||
<template #key>twoFactorEnabled</template>
|
||||
<template #value>{{ $i.twoFactorEnabled ? $ts.yes : $ts.no }}</template>
|
||||
</FormKeyValueView>
|
||||
<FormKeyValueView>
|
||||
<template #key>securityKeys</template>
|
||||
<template #value>{{ $i.securityKeys ? $ts.yes : $ts.no }}</template>
|
||||
</FormKeyValueView>
|
||||
<FormKeyValueView>
|
||||
<template #key>usePasswordLessLogin</template>
|
||||
<template #value>{{ $i.usePasswordLessLogin ? $ts.yes : $ts.no }}</template>
|
||||
</FormKeyValueView>
|
||||
<FormKeyValueView>
|
||||
<template #key>isModerator</template>
|
||||
<template #value>{{ $i.isModerator ? $ts.yes : $ts.no }}</template>
|
||||
</FormKeyValueView>
|
||||
<FormKeyValueView>
|
||||
<template #key>isAdmin</template>
|
||||
<template #value>{{ $i.isAdmin ? $ts.yes : $ts.no }}</template>
|
||||
</FormKeyValueView>
|
||||
</FormGroup>
|
||||
</FormBase>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineAsyncComponent, defineComponent } from 'vue';
|
||||
import FormSwitch from '@/components/form/switch.vue';
|
||||
import FormSelect from '@/components/form/select.vue';
|
||||
import FormLink from '@/components/debobigego/link.vue';
|
||||
import FormBase from '@/components/debobigego/base.vue';
|
||||
import FormGroup from '@/components/debobigego/group.vue';
|
||||
import FormButton from '@/components/debobigego/button.vue';
|
||||
import FormKeyValueView from '@/components/debobigego/key-value-view.vue';
|
||||
import * as os from '@/os';
|
||||
import number from '@/filters/number';
|
||||
import bytes from '@/filters/bytes';
|
||||
import * as symbols from '@/symbols';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
FormBase,
|
||||
FormSelect,
|
||||
FormSwitch,
|
||||
FormButton,
|
||||
FormLink,
|
||||
FormGroup,
|
||||
FormKeyValueView,
|
||||
},
|
||||
|
||||
emits: ['info'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: this.$ts.accountInfo,
|
||||
icon: 'fas fa-info-circle'
|
||||
},
|
||||
stats: null
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.$emit('info', this[symbols.PAGE_INFO]);
|
||||
|
||||
os.api('users/stats', {
|
||||
userId: this.$i.id
|
||||
}).then(stats => {
|
||||
this.stats = stats;
|
||||
});
|
||||
},
|
||||
|
||||
methods: {
|
||||
number,
|
||||
bytes,
|
||||
}
|
||||
});
|
||||
</script>
|
149
packages/client/src/pages/settings/accounts.vue
Normal file
149
packages/client/src/pages/settings/accounts.vue
Normal file
@ -0,0 +1,149 @@
|
||||
<template>
|
||||
<FormBase>
|
||||
<FormSuspense :p="init">
|
||||
<FormButton @click="addAccount" primary><i class="fas fa-plus"></i> {{ $ts.addAccount }}</FormButton>
|
||||
|
||||
<div class="_debobigegoItem _button" v-for="account in accounts" :key="account.id" @click="menu(account, $event)">
|
||||
<div class="_debobigegoPanel lcjjdxlm">
|
||||
<div class="avatar">
|
||||
<MkAvatar :user="account" class="avatar"/>
|
||||
</div>
|
||||
<div class="body">
|
||||
<div class="name">
|
||||
<MkUserName :user="account"/>
|
||||
</div>
|
||||
<div class="acct">
|
||||
<MkAcct :user="account"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</FormSuspense>
|
||||
</FormBase>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import FormSuspense from '@/components/debobigego/suspense.vue';
|
||||
import FormLink from '@/components/debobigego/link.vue';
|
||||
import FormBase from '@/components/debobigego/base.vue';
|
||||
import FormGroup from '@/components/debobigego/group.vue';
|
||||
import FormButton from '@/components/debobigego/button.vue';
|
||||
import * as os from '@/os';
|
||||
import * as symbols from '@/symbols';
|
||||
import { getAccounts, addAccount, login } from '@/account';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
FormBase,
|
||||
FormSuspense,
|
||||
FormButton,
|
||||
},
|
||||
|
||||
emits: ['info'],
|
||||
|
||||
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;
|
||||
}),
|
||||
};
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.$emit('info', this[symbols.PAGE_INFO]);
|
||||
},
|
||||
|
||||
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);
|
||||
},
|
||||
|
||||
addAccount(ev) {
|
||||
os.popupMenu([{
|
||||
text: this.$ts.existingAccount,
|
||||
action: () => { this.addExistingAccount(); },
|
||||
}, {
|
||||
text: this.$ts.createAccount,
|
||||
action: () => { this.createAccount(); },
|
||||
}], ev.currentTarget || ev.target);
|
||||
},
|
||||
|
||||
addExistingAccount() {
|
||||
os.popup(import('@/components/signin-dialog.vue'), {}, {
|
||||
done: res => {
|
||||
addAccount(res.id, res.i);
|
||||
os.success();
|
||||
},
|
||||
}, 'closed');
|
||||
},
|
||||
|
||||
createAccount() {
|
||||
os.popup(import('@/components/signup-dialog.vue'), {}, {
|
||||
done: res => {
|
||||
addAccount(res.id, res.i);
|
||||
this.switchAccountWithToken(res.i);
|
||||
},
|
||||
}, 'closed');
|
||||
},
|
||||
|
||||
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);
|
||||
},
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.lcjjdxlm {
|
||||
display: flex;
|
||||
padding: 16px;
|
||||
|
||||
> .avatar {
|
||||
display: block;
|
||||
flex-shrink: 0;
|
||||
margin: 0 12px 0 0;
|
||||
|
||||
> .avatar {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
}
|
||||
|
||||
> .body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
width: calc(100% - 62px);
|
||||
position: relative;
|
||||
|
||||
> .name {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
65
packages/client/src/pages/settings/api.vue
Normal file
65
packages/client/src/pages/settings/api.vue
Normal file
@ -0,0 +1,65 @@
|
||||
<template>
|
||||
<FormBase>
|
||||
<FormButton @click="generateToken" primary>{{ $ts.generateAccessToken }}</FormButton>
|
||||
<FormLink to="/settings/apps">{{ $ts.manageAccessTokens }}</FormLink>
|
||||
<FormLink to="/api-console" :behavior="isDesktop ? 'window' : null">API console</FormLink>
|
||||
</FormBase>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import FormSwitch from '@/components/form/switch.vue';
|
||||
import FormSelect from '@/components/form/select.vue';
|
||||
import FormLink from '@/components/debobigego/link.vue';
|
||||
import FormBase from '@/components/debobigego/base.vue';
|
||||
import FormGroup from '@/components/debobigego/group.vue';
|
||||
import FormButton from '@/components/debobigego/button.vue';
|
||||
import * as os from '@/os';
|
||||
import * as symbols from '@/symbols';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
FormBase,
|
||||
FormButton,
|
||||
FormLink,
|
||||
},
|
||||
|
||||
emits: ['info'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: 'API',
|
||||
icon: 'fas fa-key',
|
||||
bg: 'var(--bg)',
|
||||
},
|
||||
isDesktop: window.innerWidth >= 1100,
|
||||
};
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.$emit('info', this[symbols.PAGE_INFO]);
|
||||
},
|
||||
|
||||
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.dialog({
|
||||
type: 'success',
|
||||
title: this.$ts.token,
|
||||
text: token
|
||||
});
|
||||
},
|
||||
}, 'closed');
|
||||
},
|
||||
}
|
||||
});
|
||||
</script>
|
113
packages/client/src/pages/settings/apps.vue
Normal file
113
packages/client/src/pages/settings/apps.vue
Normal file
@ -0,0 +1,113 @@
|
||||
<template>
|
||||
<FormBase>
|
||||
<FormPagination :pagination="pagination" ref="list">
|
||||
<template #empty>
|
||||
<div class="_fullinfo">
|
||||
<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
|
||||
<div>{{ $ts.nothing }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #default="{items}">
|
||||
<div class="_debobigegoPanel bfomjevm" v-for="token in items" :key="token.id">
|
||||
<img class="icon" :src="token.iconUrl" alt="" v-if="token.iconUrl"/>
|
||||
<div class="body">
|
||||
<div class="name">{{ token.name }}</div>
|
||||
<div class="description">{{ token.description }}</div>
|
||||
<div class="_keyValue">
|
||||
<div>{{ $ts.installedDate }}:</div>
|
||||
<div><MkTime :time="token.createdAt"/></div>
|
||||
</div>
|
||||
<div class="_keyValue">
|
||||
<div>{{ $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>
|
||||
<ul>
|
||||
<li v-for="p in token.permission" :key="p">{{ $t(`_permissions.${p}`) }}</li>
|
||||
</ul>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</FormPagination>
|
||||
</FormBase>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import FormPagination from '@/components/debobigego/pagination.vue';
|
||||
import FormSelect from '@/components/form/select.vue';
|
||||
import FormLink from '@/components/debobigego/link.vue';
|
||||
import FormBase from '@/components/debobigego/base.vue';
|
||||
import FormGroup from '@/components/debobigego/group.vue';
|
||||
import FormButton from '@/components/debobigego/button.vue';
|
||||
import * as os from '@/os';
|
||||
import * as symbols from '@/symbols';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
FormBase,
|
||||
FormPagination,
|
||||
},
|
||||
|
||||
emits: ['info'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: this.$ts.installedApps,
|
||||
icon: 'fas fa-plug',
|
||||
bg: 'var(--bg)',
|
||||
},
|
||||
pagination: {
|
||||
endpoint: 'i/apps',
|
||||
limit: 100,
|
||||
params: {
|
||||
sort: '+lastUsedAt'
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.$emit('info', this[symbols.PAGE_INFO]);
|
||||
},
|
||||
|
||||
methods: {
|
||||
revoke(token) {
|
||||
os.api('i/revoke-token', { tokenId: token.id }).then(() => {
|
||||
this.$refs.list.reload();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.bfomjevm {
|
||||
display: flex;
|
||||
padding: 16px;
|
||||
|
||||
> .icon {
|
||||
display: block;
|
||||
flex-shrink: 0;
|
||||
margin: 0 12px 0 0;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
> .body {
|
||||
width: calc(100% - 62px);
|
||||
position: relative;
|
||||
|
||||
> .name {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
73
packages/client/src/pages/settings/custom-css.vue
Normal file
73
packages/client/src/pages/settings/custom-css.vue
Normal file
@ -0,0 +1,73 @@
|
||||
<template>
|
||||
<FormBase>
|
||||
<FormInfo warn>{{ $ts.customCssWarn }}</FormInfo>
|
||||
|
||||
<FormTextarea v-model="localCustomCss" manual-save tall class="_monospace" style="tab-size: 2;">
|
||||
<span>{{ $ts.local }}</span>
|
||||
</FormTextarea>
|
||||
</FormBase>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import FormTextarea from '@/components/form/textarea.vue';
|
||||
import FormSelect from '@/components/form/select.vue';
|
||||
import FormRadios from '@/components/form/radios.vue';
|
||||
import FormBase from '@/components/debobigego/base.vue';
|
||||
import FormGroup from '@/components/debobigego/group.vue';
|
||||
import FormLink from '@/components/debobigego/link.vue';
|
||||
import FormButton from '@/components/debobigego/button.vue';
|
||||
import FormInfo from '@/components/debobigego/info.vue';
|
||||
import * as os from '@/os';
|
||||
import { ColdDeviceStorage } from '@/store';
|
||||
import { unisonReload } from '@/scripts/unison-reload';
|
||||
import * as symbols from '@/symbols';
|
||||
import { defaultStore } from '@/store';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
FormTextarea,
|
||||
FormSelect,
|
||||
FormRadios,
|
||||
FormBase,
|
||||
FormGroup,
|
||||
FormLink,
|
||||
FormButton,
|
||||
FormInfo,
|
||||
},
|
||||
|
||||
emits: ['info'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: this.$ts.customCss,
|
||||
icon: 'fas fa-code',
|
||||
bg: 'var(--bg)',
|
||||
},
|
||||
localCustomCss: localStorage.getItem('customCss')
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.$emit('info', this[symbols.PAGE_INFO]);
|
||||
|
||||
this.$watch('localCustomCss', this.apply);
|
||||
},
|
||||
|
||||
methods: {
|
||||
async apply() {
|
||||
localStorage.setItem('customCss', this.localCustomCss);
|
||||
|
||||
const { canceled } = await os.dialog({
|
||||
type: 'info',
|
||||
text: this.$ts.reloadToApplySetting,
|
||||
showCancelButton: true
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
unisonReload();
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
107
packages/client/src/pages/settings/deck.vue
Normal file
107
packages/client/src/pages/settings/deck.vue
Normal file
@ -0,0 +1,107 @@
|
||||
<template>
|
||||
<FormBase>
|
||||
<FormGroup>
|
||||
<template #label>{{ $ts.defaultNavigationBehaviour }}</template>
|
||||
<FormSwitch v-model="navWindow">{{ $ts.openInWindow }}</FormSwitch>
|
||||
</FormGroup>
|
||||
|
||||
<FormSwitch v-model="alwaysShowMainColumn">{{ $ts._deck.alwaysShowMainColumn }}</FormSwitch>
|
||||
|
||||
<FormRadios v-model="columnAlign">
|
||||
<template #desc>{{ $ts._deck.columnAlign }}</template>
|
||||
<option value="left">{{ $ts.left }}</option>
|
||||
<option value="center">{{ $ts.center }}</option>
|
||||
</FormRadios>
|
||||
|
||||
<FormRadios v-model="columnHeaderHeight">
|
||||
<template #desc>{{ $ts._deck.columnHeaderHeight }}</template>
|
||||
<option :value="42">{{ $ts.narrow }}</option>
|
||||
<option :value="45">{{ $ts.medium }}</option>
|
||||
<option :value="48">{{ $ts.wide }}</option>
|
||||
</FormRadios>
|
||||
|
||||
<FormInput v-model="columnMargin" type="number">
|
||||
<span>{{ $ts._deck.columnMargin }}</span>
|
||||
<template #suffix>px</template>
|
||||
</FormInput>
|
||||
|
||||
<FormLink @click="setProfile">{{ $ts._deck.profile }}<template #suffix>{{ profile }}</template></FormLink>
|
||||
</FormBase>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import FormSwitch from '@/components/debobigego/switch.vue';
|
||||
import FormLink from '@/components/debobigego/link.vue';
|
||||
import FormRadios from '@/components/debobigego/radios.vue';
|
||||
import FormInput from '@/components/debobigego/input.vue';
|
||||
import FormBase from '@/components/debobigego/base.vue';
|
||||
import FormGroup from '@/components/debobigego/group.vue';
|
||||
import { deckStore } from '@/ui/deck/deck-store';
|
||||
import * as os from '@/os';
|
||||
import { unisonReload } from '@/scripts/unison-reload';
|
||||
import * as symbols from '@/symbols';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
FormSwitch,
|
||||
FormLink,
|
||||
FormInput,
|
||||
FormRadios,
|
||||
FormBase,
|
||||
FormGroup,
|
||||
},
|
||||
|
||||
emits: ['info'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: this.$ts.deck,
|
||||
icon: 'fas fa-columns',
|
||||
bg: 'var(--bg)',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
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'),
|
||||
},
|
||||
|
||||
watch: {
|
||||
async navWindow() {
|
||||
const { canceled } = await os.dialog({
|
||||
type: 'info',
|
||||
text: this.$ts.reloadToApplySetting,
|
||||
showCancelButton: true
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
unisonReload();
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.$emit('info', this[symbols.PAGE_INFO]);
|
||||
},
|
||||
|
||||
methods: {
|
||||
async setProfile() {
|
||||
const { canceled, result: name } = await os.dialog({
|
||||
title: this.$ts._deck.profile,
|
||||
input: {
|
||||
allowEmpty: false
|
||||
}
|
||||
});
|
||||
if (canceled) return;
|
||||
this.profile = name;
|
||||
unisonReload();
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
68
packages/client/src/pages/settings/delete-account.vue
Normal file
68
packages/client/src/pages/settings/delete-account.vue
Normal file
@ -0,0 +1,68 @@
|
||||
<template>
|
||||
<FormBase>
|
||||
<FormInfo warn>{{ $ts._accountDelete.mayTakeTime }}</FormInfo>
|
||||
<FormInfo>{{ $ts._accountDelete.sendEmail }}</FormInfo>
|
||||
<FormButton @click="deleteAccount" danger v-if="!$i.isDeleted">{{ $ts._accountDelete.requestAccountDelete }}</FormButton>
|
||||
<FormButton disabled v-else>{{ $ts._accountDelete.inProgress }}</FormButton>
|
||||
</FormBase>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineAsyncComponent, defineComponent } from 'vue';
|
||||
import FormInfo from '@/components/debobigego/info.vue';
|
||||
import FormBase from '@/components/debobigego/base.vue';
|
||||
import FormGroup from '@/components/debobigego/group.vue';
|
||||
import FormButton from '@/components/debobigego/button.vue';
|
||||
import * as os from '@/os';
|
||||
import { debug } from '@/config';
|
||||
import { signout } from '@/account';
|
||||
import * as symbols from '@/symbols';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
FormBase,
|
||||
FormButton,
|
||||
FormGroup,
|
||||
FormInfo,
|
||||
},
|
||||
|
||||
emits: ['info'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: this.$ts._accountDelete.accountDelete,
|
||||
icon: 'fas fa-exclamation-triangle',
|
||||
bg: 'var(--bg)',
|
||||
},
|
||||
debug,
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.$emit('info', this[symbols.PAGE_INFO]);
|
||||
},
|
||||
|
||||
methods: {
|
||||
async deleteAccount() {
|
||||
const { canceled, result: password } = await os.dialog({
|
||||
title: this.$ts.password,
|
||||
input: {
|
||||
type: 'password'
|
||||
}
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
await os.apiWithDialog('i/delete-account', {
|
||||
password: password
|
||||
});
|
||||
|
||||
await os.dialog({
|
||||
title: this.$ts._accountDelete.started,
|
||||
});
|
||||
|
||||
signout();
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
147
packages/client/src/pages/settings/drive.vue
Normal file
147
packages/client/src/pages/settings/drive.vue
Normal file
@ -0,0 +1,147 @@
|
||||
<template>
|
||||
<FormBase class="">
|
||||
<FormGroup v-if="!fetching">
|
||||
<template #label>{{ $ts.usageAmount }}</template>
|
||||
<div class="_debobigegoItem uawsfosz">
|
||||
<div class="_debobigegoPanel">
|
||||
<div class="meter"><div :style="meterStyle"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
<FormKeyValueView>
|
||||
<template #key>{{ $ts.capacity }}</template>
|
||||
<template #value>{{ bytes(capacity, 1) }}</template>
|
||||
</FormKeyValueView>
|
||||
<FormKeyValueView>
|
||||
<template #key>{{ $ts.inUse }}</template>
|
||||
<template #value>{{ bytes(usage, 1) }}</template>
|
||||
</FormKeyValueView>
|
||||
</FormGroup>
|
||||
|
||||
<div class="_debobigegoItem">
|
||||
<div class="_debobigegoLabel">{{ $ts.statistics }}</div>
|
||||
<div class="_debobigegoPanel">
|
||||
<div ref="chart"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FormButton :center="false" @click="chooseUploadFolder()" primary>
|
||||
{{ $ts.uploadFolder }}
|
||||
<template #suffix>{{ uploadFolder ? uploadFolder.name : '-' }}</template>
|
||||
<template #suffixIcon><i class="fas fa-folder-open"></i></template>
|
||||
</FormButton>
|
||||
</FormBase>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import * as tinycolor from 'tinycolor2';
|
||||
import FormButton from '@/components/debobigego/button.vue';
|
||||
import FormGroup from '@/components/debobigego/group.vue';
|
||||
import FormKeyValueView from '@/components/debobigego/key-value-view.vue';
|
||||
import FormBase from '@/components/debobigego/base.vue';
|
||||
import * as os from '@/os';
|
||||
import bytes from '@/filters/bytes';
|
||||
import * as symbols from '@/symbols';
|
||||
|
||||
// TODO: render chart
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
FormBase,
|
||||
FormButton,
|
||||
FormGroup,
|
||||
FormKeyValueView,
|
||||
},
|
||||
|
||||
emits: ['info'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: this.$ts.drive,
|
||||
icon: 'fas fa-cloud',
|
||||
bg: 'var(--bg)',
|
||||
},
|
||||
fetching: true,
|
||||
usage: null,
|
||||
capacity: null,
|
||||
uploadFolder: 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
|
||||
})
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
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
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.$emit('info', this[symbols.PAGE_INFO]);
|
||||
},
|
||||
|
||||
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
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@use "sass:math";
|
||||
|
||||
.uawsfosz {
|
||||
> div {
|
||||
padding: 24px;
|
||||
|
||||
> .meter {
|
||||
$size: 12px;
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
border-radius: math.div($size, 2);
|
||||
overflow: hidden;
|
||||
|
||||
> div {
|
||||
height: $size;
|
||||
border-radius: math.div($size, 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
70
packages/client/src/pages/settings/email-address.vue
Normal file
70
packages/client/src/pages/settings/email-address.vue
Normal file
@ -0,0 +1,70 @@
|
||||
<template>
|
||||
<FormBase>
|
||||
<FormGroup>
|
||||
<FormInput v-model="emailAddress" type="email">
|
||||
{{ $ts.emailAddress }}
|
||||
<template #desc v-if="$i.email && !$i.emailVerified">{{ $ts.verificationEmailSent }}</template>
|
||||
<template #desc v-else-if="emailAddress === $i.email && $i.emailVerified">{{ $ts.emailVerified }}</template>
|
||||
</FormInput>
|
||||
</FormGroup>
|
||||
<FormButton @click="save" primary>{{ $ts.save }}</FormButton>
|
||||
</FormBase>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import FormButton from '@/components/debobigego/button.vue';
|
||||
import FormInput from '@/components/form/input.vue';
|
||||
import FormBase from '@/components/debobigego/base.vue';
|
||||
import FormGroup from '@/components/debobigego/group.vue';
|
||||
import * as os from '@/os';
|
||||
import * as symbols from '@/symbols';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
FormBase,
|
||||
FormInput,
|
||||
FormButton,
|
||||
FormGroup,
|
||||
},
|
||||
|
||||
emits: ['info'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: this.$ts.emailAddress,
|
||||
icon: 'fas fa-envelope',
|
||||
bg: 'var(--bg)',
|
||||
},
|
||||
emailAddress: null,
|
||||
code: null,
|
||||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
this.emailAddress = this.$i.email;
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.$emit('info', this[symbols.PAGE_INFO]);
|
||||
},
|
||||
|
||||
methods: {
|
||||
save() {
|
||||
os.dialog({
|
||||
title: this.$ts.password,
|
||||
input: {
|
||||
type: 'password'
|
||||
}
|
||||
}).then(({ canceled, result: password }) => {
|
||||
if (canceled) return;
|
||||
os.apiWithDialog('i/update-email', {
|
||||
password: password,
|
||||
email: this.emailAddress,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
91
packages/client/src/pages/settings/email-notification.vue
Normal file
91
packages/client/src/pages/settings/email-notification.vue
Normal file
@ -0,0 +1,91 @@
|
||||
<template>
|
||||
<FormBase>
|
||||
<FormGroup>
|
||||
<FormSwitch v-model="mention">
|
||||
{{ $ts._notification._types.mention }}
|
||||
</FormSwitch>
|
||||
<FormSwitch v-model="reply">
|
||||
{{ $ts._notification._types.reply }}
|
||||
</FormSwitch>
|
||||
<FormSwitch v-model="quote">
|
||||
{{ $ts._notification._types.quote }}
|
||||
</FormSwitch>
|
||||
<FormSwitch v-model="follow">
|
||||
{{ $ts._notification._types.follow }}
|
||||
</FormSwitch>
|
||||
<FormSwitch v-model="receiveFollowRequest">
|
||||
{{ $ts._notification._types.receiveFollowRequest }}
|
||||
</FormSwitch>
|
||||
<FormSwitch v-model="groupInvited">
|
||||
{{ $ts._notification._types.groupInvited }}
|
||||
</FormSwitch>
|
||||
</FormGroup>
|
||||
</FormBase>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import FormButton from '@/components/debobigego/button.vue';
|
||||
import FormSwitch from '@/components/form/switch.vue';
|
||||
import FormBase from '@/components/debobigego/base.vue';
|
||||
import FormGroup from '@/components/debobigego/group.vue';
|
||||
import * as os from '@/os';
|
||||
import * as symbols from '@/symbols';
|
||||
import * as symbols from '@/symbols';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
FormBase,
|
||||
FormSwitch,
|
||||
FormButton,
|
||||
FormGroup,
|
||||
},
|
||||
|
||||
emits: ['info'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: this.$ts.emailNotification,
|
||||
icon: 'fas fa-envelope',
|
||||
bg: 'var(--bg)',
|
||||
},
|
||||
|
||||
mention: this.$i.emailNotificationTypes.includes('mention'),
|
||||
reply: this.$i.emailNotificationTypes.includes('reply'),
|
||||
quote: this.$i.emailNotificationTypes.includes('quote'),
|
||||
follow: this.$i.emailNotificationTypes.includes('follow'),
|
||||
receiveFollowRequest: this.$i.emailNotificationTypes.includes('receiveFollowRequest'),
|
||||
groupInvited: this.$i.emailNotificationTypes.includes('groupInvited'),
|
||||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
this.$watch('mention', this.save);
|
||||
this.$watch('reply', this.save);
|
||||
this.$watch('quote', this.save);
|
||||
this.$watch('follow', this.save);
|
||||
this.$watch('receiveFollowRequest', this.save);
|
||||
this.$watch('groupInvited', this.save);
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.$emit('info', this[symbols.PAGE_INFO]);
|
||||
},
|
||||
|
||||
methods: {
|
||||
save() {
|
||||
os.api('i/update', {
|
||||
emailNotificationTypes: [
|
||||
...[this.mention ? 'mention' : null],
|
||||
...[this.reply ? 'reply' : null],
|
||||
...[this.quote ? 'quote' : null],
|
||||
...[this.follow ? 'follow' : null],
|
||||
...[this.receiveFollowRequest ? 'receiveFollowRequest' : null],
|
||||
...[this.groupInvited ? 'groupInvited' : null],
|
||||
].filter(x => x != null)
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
66
packages/client/src/pages/settings/email.vue
Normal file
66
packages/client/src/pages/settings/email.vue
Normal file
@ -0,0 +1,66 @@
|
||||
<template>
|
||||
<FormBase>
|
||||
<FormGroup>
|
||||
<template #label>{{ $ts.emailAddress }}</template>
|
||||
<FormLink to="/settings/email/address">
|
||||
<template v-if="$i.email && !$i.emailVerified" #icon><i class="fas fa-exclamation-triangle" style="color: var(--warn);"></i></template>
|
||||
<template v-else-if="$i.email && $i.emailVerified" #icon><i class="fas fa-check" style="color: var(--success);"></i></template>
|
||||
{{ $i.email || $ts.notSet }}
|
||||
</FormLink>
|
||||
</FormGroup>
|
||||
|
||||
<FormLink to="/settings/email/notification">
|
||||
<template #icon><i class="fas fa-bell"></i></template>
|
||||
{{ $ts.emailNotification }}
|
||||
</FormLink>
|
||||
|
||||
<FormSwitch :value="$i.receiveAnnouncementEmail" @update:modelValue="onChangeReceiveAnnouncementEmail">
|
||||
{{ $ts.receiveAnnouncementFromInstance }}
|
||||
</FormSwitch>
|
||||
</FormBase>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import FormButton from '@/components/debobigego/button.vue';
|
||||
import FormLink from '@/components/debobigego/link.vue';
|
||||
import FormBase from '@/components/debobigego/base.vue';
|
||||
import FormGroup from '@/components/debobigego/group.vue';
|
||||
import FormSwitch from '@/components/debobigego/switch.vue';
|
||||
import * as os from '@/os';
|
||||
import * as symbols from '@/symbols';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
FormBase,
|
||||
FormLink,
|
||||
FormButton,
|
||||
FormSwitch,
|
||||
FormGroup,
|
||||
},
|
||||
|
||||
emits: ['info'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: this.$ts.email,
|
||||
icon: 'fas fa-envelope',
|
||||
bg: 'var(--bg)',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.$emit('info', this[symbols.PAGE_INFO]);
|
||||
},
|
||||
|
||||
methods: {
|
||||
onChangeReceiveAnnouncementEmail(v) {
|
||||
os.api('i/update', {
|
||||
receiveAnnouncementEmail: v
|
||||
});
|
||||
},
|
||||
}
|
||||
});
|
||||
</script>
|
52
packages/client/src/pages/settings/experimental-features.vue
Normal file
52
packages/client/src/pages/settings/experimental-features.vue
Normal file
@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<FormBase>
|
||||
<FormButton @click="error()">error test</FormButton>
|
||||
</FormBase>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineAsyncComponent, defineComponent } from 'vue';
|
||||
import FormSwitch from '@/components/form/switch.vue';
|
||||
import FormSelect from '@/components/form/select.vue';
|
||||
import FormLink from '@/components/debobigego/link.vue';
|
||||
import FormBase from '@/components/debobigego/base.vue';
|
||||
import FormGroup from '@/components/debobigego/group.vue';
|
||||
import FormButton from '@/components/debobigego/button.vue';
|
||||
import FormKeyValueView from '@/components/debobigego/key-value-view.vue';
|
||||
import * as os from '@/os';
|
||||
import * as symbols from '@/symbols';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
FormBase,
|
||||
FormSelect,
|
||||
FormSwitch,
|
||||
FormButton,
|
||||
FormLink,
|
||||
FormGroup,
|
||||
FormKeyValueView,
|
||||
},
|
||||
|
||||
emits: ['info'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: this.$ts.experimentalFeatures,
|
||||
icon: 'fas fa-flask'
|
||||
},
|
||||
stats: null
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.$emit('info', this[symbols.PAGE_INFO]);
|
||||
},
|
||||
|
||||
methods: {
|
||||
error() {
|
||||
throw new Error('Test error');
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
223
packages/client/src/pages/settings/general.vue
Normal file
223
packages/client/src/pages/settings/general.vue
Normal file
@ -0,0 +1,223 @@
|
||||
<template>
|
||||
<FormBase>
|
||||
<FormSwitch v-model="showFixedPostForm">{{ $ts.showFixedPostForm }}</FormSwitch>
|
||||
|
||||
<FormSelect v-model="lang">
|
||||
<template #label>{{ $ts.uiLanguage }}</template>
|
||||
<option v-for="x in langs" :value="x[0]" :key="x[0]">{{ x[1] }}</option>
|
||||
<template #caption>
|
||||
<I18n :src="$ts.i18nInfo" tag="span">
|
||||
<template #link>
|
||||
<MkLink url="https://crowdin.com/project/misskey">Crowdin</MkLink>
|
||||
</template>
|
||||
</I18n>
|
||||
</template>
|
||||
</FormSelect>
|
||||
|
||||
<FormGroup>
|
||||
<template #label>{{ $ts.behavior }}</template>
|
||||
<FormSwitch v-model="imageNewTab">{{ $ts.openImageInNewTab }}</FormSwitch>
|
||||
<FormSwitch v-model="enableInfiniteScroll">{{ $ts.enableInfiniteScroll }}</FormSwitch>
|
||||
<FormSwitch v-model="useReactionPickerForContextMenu">{{ $ts.useReactionPickerForContextMenu }}</FormSwitch>
|
||||
<FormSwitch v-model="disablePagesScript">{{ $ts.disablePagesScript }}</FormSwitch>
|
||||
</FormGroup>
|
||||
|
||||
<FormSelect v-model="serverDisconnectedBehavior">
|
||||
<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>
|
||||
</FormSelect>
|
||||
|
||||
<FormGroup>
|
||||
<template #label>{{ $ts.appearance }}</template>
|
||||
<FormSwitch v-model="disableAnimatedMfm">{{ $ts.disableAnimatedMfm }}</FormSwitch>
|
||||
<FormSwitch v-model="reduceAnimation">{{ $ts.reduceUiAnimation }}</FormSwitch>
|
||||
<FormSwitch v-model="useBlurEffect">{{ $ts.useBlurEffect }}</FormSwitch>
|
||||
<FormSwitch v-model="useBlurEffectForModal">{{ $ts.useBlurEffectForModal }}</FormSwitch>
|
||||
<FormSwitch v-model="showGapBetweenNotesInTimeline">{{ $ts.showGapBetweenNotesInTimeline }}</FormSwitch>
|
||||
<FormSwitch v-model="loadRawImages">{{ $ts.loadRawImages }}</FormSwitch>
|
||||
<FormSwitch v-model="disableShowingAnimatedImages">{{ $ts.disableShowingAnimatedImages }}</FormSwitch>
|
||||
<FormSwitch v-model="squareAvatars">{{ $ts.squareAvatars }}</FormSwitch>
|
||||
<FormSwitch v-model="useSystemFont">{{ $ts.useSystemFont }}</FormSwitch>
|
||||
<FormSwitch v-model="useOsNativeEmojis">{{ $ts.useOsNativeEmojis }}
|
||||
<div><Mfm text="🍮🍦🍭🍩🍰🍫🍬🥞🍪" :key="useOsNativeEmojis"/></div>
|
||||
</FormSwitch>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormSwitch v-model="aiChanMode">{{ $ts.aiChanMode }}</FormSwitch>
|
||||
</FormGroup>
|
||||
|
||||
<FormRadios v-model="fontSize">
|
||||
<template #desc>{{ $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>
|
||||
<option value="veryLarge"><span style="font-size: 20px;">Aa</span></option>
|
||||
</FormRadios>
|
||||
|
||||
<FormSelect v-model="instanceTicker">
|
||||
<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>
|
||||
</FormSelect>
|
||||
|
||||
<FormSelect v-model="nsfw">
|
||||
<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>
|
||||
</FormSelect>
|
||||
|
||||
<FormGroup>
|
||||
<template #label>{{ $ts.defaultNavigationBehaviour }}</template>
|
||||
<FormSwitch v-model="defaultSideView">{{ $ts.openInSideView }}</FormSwitch>
|
||||
</FormGroup>
|
||||
|
||||
<FormSelect v-model="chatOpenBehavior">
|
||||
<template #label>{{ $ts.chatOpenBehavior }}</template>
|
||||
<option value="page">{{ $ts.showInPage }}</option>
|
||||
<option value="window">{{ $ts.openInWindow }}</option>
|
||||
<option value="popout">{{ $ts.popout }}</option>
|
||||
</FormSelect>
|
||||
|
||||
<FormLink to="/settings/deck">{{ $ts.deck }}</FormLink>
|
||||
|
||||
<FormLink to="/settings/custom-css"><template #icon><i class="fas fa-code"></i></template>{{ $ts.customCss }}</FormLink>
|
||||
</FormBase>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import FormSwitch from '@/components/debobigego/switch.vue';
|
||||
import FormSelect from '@/components/debobigego/select.vue';
|
||||
import FormRadios from '@/components/debobigego/radios.vue';
|
||||
import FormBase from '@/components/debobigego/base.vue';
|
||||
import FormGroup from '@/components/debobigego/group.vue';
|
||||
import FormLink from '@/components/debobigego/link.vue';
|
||||
import FormButton from '@/components/debobigego/button.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';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkLink,
|
||||
FormSwitch,
|
||||
FormSelect,
|
||||
FormRadios,
|
||||
FormBase,
|
||||
FormGroup,
|
||||
FormLink,
|
||||
FormButton,
|
||||
},
|
||||
|
||||
emits: ['info'],
|
||||
|
||||
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,
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
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'),
|
||||
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'),
|
||||
chatOpenBehavior: ColdDeviceStorage.makeGetterSetter('chatOpenBehavior'),
|
||||
instanceTicker: defaultStore.makeGetterSetter('instanceTicker'),
|
||||
enableInfiniteScroll: defaultStore.makeGetterSetter('enableInfiniteScroll'),
|
||||
useReactionPickerForContextMenu: defaultStore.makeGetterSetter('useReactionPickerForContextMenu'),
|
||||
squareAvatars: defaultStore.makeGetterSetter('squareAvatars'),
|
||||
aiChanMode: defaultStore.makeGetterSetter('aiChanMode'),
|
||||
},
|
||||
|
||||
watch: {
|
||||
lang() {
|
||||
localStorage.setItem('lang', this.lang);
|
||||
localStorage.removeItem('locale');
|
||||
this.reloadAsk();
|
||||
},
|
||||
|
||||
fontSize() {
|
||||
if (this.fontSize == null) {
|
||||
localStorage.removeItem('fontSize');
|
||||
} else {
|
||||
localStorage.setItem('fontSize', this.fontSize);
|
||||
}
|
||||
this.reloadAsk();
|
||||
},
|
||||
|
||||
useSystemFont() {
|
||||
if (this.useSystemFont) {
|
||||
localStorage.setItem('useSystemFont', 't');
|
||||
} else {
|
||||
localStorage.removeItem('useSystemFont');
|
||||
}
|
||||
this.reloadAsk();
|
||||
},
|
||||
|
||||
enableInfiniteScroll() {
|
||||
this.reloadAsk();
|
||||
},
|
||||
|
||||
squareAvatars() {
|
||||
this.reloadAsk();
|
||||
},
|
||||
|
||||
aiChanMode() {
|
||||
this.reloadAsk();
|
||||
},
|
||||
|
||||
showGapBetweenNotesInTimeline() {
|
||||
this.reloadAsk();
|
||||
},
|
||||
|
||||
instanceTicker() {
|
||||
this.reloadAsk();
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.$emit('info', this[symbols.PAGE_INFO]);
|
||||
},
|
||||
|
||||
methods: {
|
||||
async reloadAsk() {
|
||||
const { canceled } = await os.dialog({
|
||||
type: 'info',
|
||||
text: this.$ts.reloadToApplySetting,
|
||||
showCancelButton: true
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
unisonReload();
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
112
packages/client/src/pages/settings/import-export.vue
Normal file
112
packages/client/src/pages/settings/import-export.vue
Normal file
@ -0,0 +1,112 @@
|
||||
<template>
|
||||
<div style="margin: 16px;">
|
||||
<FormSection>
|
||||
<template #label>{{ $ts._exportOrImport.allNotes }}</template>
|
||||
<MkButton :class="$style.button" inline @click="doExport('notes')"><i class="fas fa-download"></i> {{ $ts.export }}</MkButton>
|
||||
</FormSection>
|
||||
<FormSection>
|
||||
<template #label>{{ $ts._exportOrImport.followingList }}</template>
|
||||
<MkButton :class="$style.button" inline @click="doExport('following')"><i class="fas fa-download"></i> {{ $ts.export }}</MkButton>
|
||||
<MkButton :class="$style.button" inline @click="doImport('following', $event)"><i class="fas fa-upload"></i> {{ $ts.import }}</MkButton>
|
||||
</FormSection>
|
||||
<FormSection>
|
||||
<template #label>{{ $ts._exportOrImport.userLists }}</template>
|
||||
<MkButton :class="$style.button" inline @click="doExport('user-lists')"><i class="fas fa-download"></i> {{ $ts.export }}</MkButton>
|
||||
<MkButton :class="$style.button" inline @click="doImport('user-lists', $event)"><i class="fas fa-upload"></i> {{ $ts.import }}</MkButton>
|
||||
</FormSection>
|
||||
<FormSection>
|
||||
<template #label>{{ $ts._exportOrImport.muteList }}</template>
|
||||
<MkButton :class="$style.button" inline @click="doExport('muting')"><i class="fas fa-download"></i> {{ $ts.export }}</MkButton>
|
||||
<MkButton :class="$style.button" inline @click="doImport('muting', $event)"><i class="fas fa-upload"></i> {{ $ts.import }}</MkButton>
|
||||
</FormSection>
|
||||
<FormSection>
|
||||
<template #label>{{ $ts._exportOrImport.blockingList }}</template>
|
||||
<MkButton :class="$style.button" inline @click="doExport('blocking')"><i class="fas fa-download"></i> {{ $ts.export }}</MkButton>
|
||||
<MkButton :class="$style.button" inline @click="doImport('blocking', $event)"><i class="fas fa-upload"></i> {{ $ts.import }}</MkButton>
|
||||
</FormSection>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import FormSection from '@/components/form/section.vue';
|
||||
import * as os from '@/os';
|
||||
import { selectFile } from '@/scripts/select-file';
|
||||
import * as symbols from '@/symbols';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
FormSection,
|
||||
MkButton,
|
||||
},
|
||||
|
||||
emits: ['info'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: this.$ts.importAndExport,
|
||||
icon: 'fas fa-boxes',
|
||||
bg: 'var(--bg)',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.$emit('info', this[symbols.PAGE_INFO]);
|
||||
},
|
||||
|
||||
methods: {
|
||||
doExport(target) {
|
||||
os.api(
|
||||
target === 'notes' ? 'i/export-notes' :
|
||||
target === 'following' ? 'i/export-following' :
|
||||
target === 'blocking' ? 'i/export-blocking' :
|
||||
target === 'user-lists' ? 'i/export-user-lists' :
|
||||
target === 'muting' ? 'i/export-mute' :
|
||||
null, {})
|
||||
.then(() => {
|
||||
os.dialog({
|
||||
type: 'info',
|
||||
text: this.$ts.exportRequested
|
||||
});
|
||||
}).catch((e: any) => {
|
||||
os.dialog({
|
||||
type: 'error',
|
||||
text: e.message
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
async doImport(target, e) {
|
||||
const file = await selectFile(e.currentTarget || e.target);
|
||||
|
||||
os.api(
|
||||
target === 'following' ? 'i/import-following' :
|
||||
target === 'user-lists' ? 'i/import-user-lists' :
|
||||
target === 'muting' ? 'i/import-muting' :
|
||||
target === 'blocking' ? 'i/import-blocking' :
|
||||
null, {
|
||||
fileId: file.id
|
||||
}).then(() => {
|
||||
os.dialog({
|
||||
type: 'info',
|
||||
text: this.$ts.importRequested
|
||||
});
|
||||
}).catch((e: any) => {
|
||||
os.dialog({
|
||||
type: 'error',
|
||||
text: e.message
|
||||
});
|
||||
});
|
||||
},
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style module>
|
||||
.button {
|
||||
margin-right: 16px;
|
||||
}
|
||||
</style>
|
326
packages/client/src/pages/settings/index.vue
Normal file
326
packages/client/src/pages/settings/index.vue
Normal file
@ -0,0 +1,326 @@
|
||||
<template>
|
||||
<div class="vvcocwet" :class="{ wide: !narrow }" ref="el">
|
||||
<div class="nav" v-if="!narrow || page == null">
|
||||
<MkSpacer :content-max="700">
|
||||
<div class="baaadecd">
|
||||
<div class="title">{{ $ts.settings }}</div>
|
||||
<MkInfo v-if="emailNotConfigured" warn class="info">{{ $ts.emailNotConfiguredWarning }} <MkA to="/settings/email" class="_link">{{ $ts.configure }}</MkA></MkInfo>
|
||||
<MkSuperMenu :def="menuDef" :grid="page == null"></MkSuperMenu>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
<div class="main">
|
||||
<component :is="component" :key="page" v-bind="pageProps"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineAsyncComponent, defineComponent, nextTick, onMounted, reactive, ref, watch } from 'vue';
|
||||
import { i18n } from '@/i18n';
|
||||
import MkInfo from '@/components/ui/info.vue';
|
||||
import MkSuperMenu from '@/components/ui/super-menu.vue';
|
||||
import { scroll } from '@/scripts/scroll';
|
||||
import { signout } from '@/account';
|
||||
import { unisonReload } from '@/scripts/unison-reload';
|
||||
import * as symbols from '@/symbols';
|
||||
import { instance } from '@/instance';
|
||||
import { $i } from '@/account';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkInfo,
|
||||
MkSuperMenu,
|
||||
},
|
||||
|
||||
props: {
|
||||
initialPage: {
|
||||
type: String,
|
||||
required: false
|
||||
}
|
||||
},
|
||||
|
||||
setup(props, context) {
|
||||
const indexInfo = {
|
||||
title: i18n.locale.settings,
|
||||
icon: 'fas fa-cog',
|
||||
bg: 'var(--bg)',
|
||||
hideHeader: true,
|
||||
};
|
||||
const INFO = ref(indexInfo);
|
||||
const page = ref(props.initialPage);
|
||||
const narrow = ref(false);
|
||||
const view = ref(null);
|
||||
const el = ref(null);
|
||||
const menuDef = computed(() => [{
|
||||
title: i18n.locale.basicSettings,
|
||||
items: [{
|
||||
icon: 'fas fa-user',
|
||||
text: i18n.locale.profile,
|
||||
to: '/settings/profile',
|
||||
active: page.value === 'profile',
|
||||
}, {
|
||||
icon: 'fas fa-lock-open',
|
||||
text: i18n.locale.privacy,
|
||||
to: '/settings/privacy',
|
||||
active: page.value === 'privacy',
|
||||
}, {
|
||||
icon: 'fas fa-laugh',
|
||||
text: i18n.locale.reaction,
|
||||
to: '/settings/reaction',
|
||||
active: page.value === 'reaction',
|
||||
}, {
|
||||
icon: 'fas fa-cloud',
|
||||
text: i18n.locale.drive,
|
||||
to: '/settings/drive',
|
||||
active: page.value === 'drive',
|
||||
}, {
|
||||
icon: 'fas fa-bell',
|
||||
text: i18n.locale.notifications,
|
||||
to: '/settings/notifications',
|
||||
active: page.value === 'notifications',
|
||||
}, {
|
||||
icon: 'fas fa-envelope',
|
||||
text: i18n.locale.email,
|
||||
to: '/settings/email',
|
||||
active: page.value === 'email',
|
||||
}, {
|
||||
icon: 'fas fa-share-alt',
|
||||
text: i18n.locale.integration,
|
||||
to: '/settings/integration',
|
||||
active: page.value === 'integration',
|
||||
}, {
|
||||
icon: 'fas fa-lock',
|
||||
text: i18n.locale.security,
|
||||
to: '/settings/security',
|
||||
active: page.value === 'security',
|
||||
}],
|
||||
}, {
|
||||
title: i18n.locale.clientSettings,
|
||||
items: [{
|
||||
icon: 'fas fa-cogs',
|
||||
text: i18n.locale.general,
|
||||
to: '/settings/general',
|
||||
active: page.value === 'general',
|
||||
}, {
|
||||
icon: 'fas fa-palette',
|
||||
text: i18n.locale.theme,
|
||||
to: '/settings/theme',
|
||||
active: page.value === 'theme',
|
||||
}, {
|
||||
icon: 'fas fa-list-ul',
|
||||
text: i18n.locale.menu,
|
||||
to: '/settings/menu',
|
||||
active: page.value === 'menu',
|
||||
}, {
|
||||
icon: 'fas fa-music',
|
||||
text: i18n.locale.sounds,
|
||||
to: '/settings/sounds',
|
||||
active: page.value === 'sounds',
|
||||
}, {
|
||||
icon: 'fas fa-plug',
|
||||
text: i18n.locale.plugins,
|
||||
to: '/settings/plugin',
|
||||
active: page.value === 'plugin',
|
||||
}],
|
||||
}, {
|
||||
title: i18n.locale.otherSettings,
|
||||
items: [{
|
||||
icon: 'fas fa-boxes',
|
||||
text: i18n.locale.importAndExport,
|
||||
to: '/settings/import-export',
|
||||
active: page.value === 'import-export',
|
||||
}, {
|
||||
icon: 'fas fa-ban',
|
||||
text: i18n.locale.muteAndBlock,
|
||||
to: '/settings/mute-block',
|
||||
active: page.value === 'mute-block',
|
||||
}, {
|
||||
icon: 'fas fa-comment-slash',
|
||||
text: i18n.locale.wordMute,
|
||||
to: '/settings/word-mute',
|
||||
active: page.value === 'word-mute',
|
||||
}, {
|
||||
icon: 'fas fa-key',
|
||||
text: 'API',
|
||||
to: '/settings/api',
|
||||
active: page.value === 'api',
|
||||
}, {
|
||||
icon: 'fas fa-ellipsis-h',
|
||||
text: i18n.locale.other,
|
||||
to: '/settings/other',
|
||||
active: page.value === 'other',
|
||||
}],
|
||||
}, {
|
||||
items: [{
|
||||
type: 'button',
|
||||
icon: 'fas fa-trash',
|
||||
text: i18n.locale.clearCache,
|
||||
action: () => {
|
||||
localStorage.removeItem('locale');
|
||||
localStorage.removeItem('theme');
|
||||
unisonReload();
|
||||
},
|
||||
}, {
|
||||
type: 'button',
|
||||
icon: 'fas fa-sign-in-alt fa-flip-horizontal',
|
||||
text: i18n.locale.logout,
|
||||
action: () => {
|
||||
signout();
|
||||
},
|
||||
danger: true,
|
||||
},],
|
||||
}]);
|
||||
|
||||
const pageProps = ref({});
|
||||
const component = computed(() => {
|
||||
if (page.value == null) return null;
|
||||
switch (page.value) {
|
||||
case 'accounts': return defineAsyncComponent(() => import('./accounts.vue'));
|
||||
case 'profile': return defineAsyncComponent(() => import('./profile.vue'));
|
||||
case 'privacy': return defineAsyncComponent(() => import('./privacy.vue'));
|
||||
case 'reaction': return defineAsyncComponent(() => import('./reaction.vue'));
|
||||
case 'drive': return defineAsyncComponent(() => import('./drive.vue'));
|
||||
case 'notifications': return defineAsyncComponent(() => import('./notifications.vue'));
|
||||
case 'mute-block': return defineAsyncComponent(() => import('./mute-block.vue'));
|
||||
case 'word-mute': return defineAsyncComponent(() => import('./word-mute.vue'));
|
||||
case 'integration': return defineAsyncComponent(() => import('./integration.vue'));
|
||||
case 'security': return defineAsyncComponent(() => import('./security.vue'));
|
||||
case '2fa': return defineAsyncComponent(() => import('./2fa.vue'));
|
||||
case 'api': return defineAsyncComponent(() => import('./api.vue'));
|
||||
case 'apps': return defineAsyncComponent(() => import('./apps.vue'));
|
||||
case 'other': return defineAsyncComponent(() => import('./other.vue'));
|
||||
case 'general': return defineAsyncComponent(() => import('./general.vue'));
|
||||
case 'email': return defineAsyncComponent(() => import('./email.vue'));
|
||||
case 'email/address': return defineAsyncComponent(() => import('./email-address.vue'));
|
||||
case 'email/notification': return defineAsyncComponent(() => import('./email-notification.vue'));
|
||||
case 'theme': return defineAsyncComponent(() => import('./theme.vue'));
|
||||
case 'theme/install': return defineAsyncComponent(() => import('./theme.install.vue'));
|
||||
case 'theme/manage': return defineAsyncComponent(() => import('./theme.manage.vue'));
|
||||
case 'menu': return defineAsyncComponent(() => import('./menu.vue'));
|
||||
case 'sounds': return defineAsyncComponent(() => import('./sounds.vue'));
|
||||
case 'custom-css': return defineAsyncComponent(() => import('./custom-css.vue'));
|
||||
case 'deck': return defineAsyncComponent(() => import('./deck.vue'));
|
||||
case 'plugin': return defineAsyncComponent(() => import('./plugin.vue'));
|
||||
case 'plugin/install': return defineAsyncComponent(() => import('./plugin.install.vue'));
|
||||
case 'plugin/manage': return defineAsyncComponent(() => import('./plugin.manage.vue'));
|
||||
case 'import-export': return defineAsyncComponent(() => import('./import-export.vue'));
|
||||
case 'account-info': return defineAsyncComponent(() => import('./account-info.vue'));
|
||||
case 'update': return defineAsyncComponent(() => import('./update.vue'));
|
||||
case 'registry': return defineAsyncComponent(() => import('./registry.vue'));
|
||||
case 'delete-account': return defineAsyncComponent(() => import('./delete-account.vue'));
|
||||
case 'experimental-features': return defineAsyncComponent(() => import('./experimental-features.vue'));
|
||||
}
|
||||
if (page.value.startsWith('registry/keys/system/')) {
|
||||
return defineAsyncComponent(() => import('./registry.keys.vue'));
|
||||
}
|
||||
if (page.value.startsWith('registry/value/system/')) {
|
||||
return defineAsyncComponent(() => import('./registry.value.vue'));
|
||||
}
|
||||
});
|
||||
|
||||
watch(component, () => {
|
||||
pageProps.value = {};
|
||||
|
||||
if (page.value) {
|
||||
if (page.value.startsWith('registry/keys/system/')) {
|
||||
pageProps.value.scope = page.value.replace('registry/keys/system/', '').split('/');
|
||||
}
|
||||
if (page.value.startsWith('registry/value/system/')) {
|
||||
const path = page.value.replace('registry/value/system/', '').split('/');
|
||||
pageProps.value.xKey = path.pop();
|
||||
pageProps.value.scope = path;
|
||||
}
|
||||
}
|
||||
|
||||
nextTick(() => {
|
||||
scroll(el.value, { top: 0 });
|
||||
});
|
||||
}, { immediate: true });
|
||||
|
||||
watch(() => props.initialPage, () => {
|
||||
if (props.initialPage == null && !narrow.value) {
|
||||
page.value = 'profile';
|
||||
} else {
|
||||
page.value = props.initialPage;
|
||||
if (props.initialPage == null) {
|
||||
INFO.value = indexInfo;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
narrow.value = el.value.offsetWidth < 800;
|
||||
if (!narrow.value) {
|
||||
page.value = 'profile';
|
||||
}
|
||||
});
|
||||
|
||||
const emailNotConfigured = computed(() => instance.enableEmail && ($i.email == null || !$i.emailVerified));
|
||||
|
||||
return {
|
||||
[symbols.PAGE_INFO]: INFO,
|
||||
page,
|
||||
menuDef,
|
||||
narrow,
|
||||
view,
|
||||
el,
|
||||
pageProps,
|
||||
component,
|
||||
emailNotConfigured,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.vvcocwet {
|
||||
> .nav {
|
||||
.baaadecd {
|
||||
> .title {
|
||||
margin: 16px;
|
||||
font-size: 1.5em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
> .info {
|
||||
margin: 0 16px;
|
||||
}
|
||||
|
||||
> .accounts {
|
||||
> .avatar {
|
||||
display: block;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
margin: 8px auto 16px auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.wide {
|
||||
display: flex;
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
height: 100%;
|
||||
|
||||
> .nav {
|
||||
width: 32%;
|
||||
box-sizing: border-box;
|
||||
overflow: auto;
|
||||
|
||||
.baaadecd {
|
||||
> .title {
|
||||
margin: 24px 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
141
packages/client/src/pages/settings/integration.vue
Normal file
141
packages/client/src/pages/settings/integration.vue
Normal file
@ -0,0 +1,141 @@
|
||||
<template>
|
||||
<FormBase>
|
||||
<div class="_debobigegoItem" v-if="enableTwitterIntegration">
|
||||
<div class="_debobigegoLabel"><i class="fab fa-twitter"></i> Twitter</div>
|
||||
<div class="_debobigegoPanel" style="padding: 16px;">
|
||||
<p v-if="integrations.twitter">{{ $ts.connectedTo }}: <a :href="`https://twitter.com/${integrations.twitter.screenName}`" rel="nofollow noopener" target="_blank">@{{ integrations.twitter.screenName }}</a></p>
|
||||
<MkButton v-if="integrations.twitter" @click="disconnectTwitter" danger>{{ $ts.disconnectService }}</MkButton>
|
||||
<MkButton v-else @click="connectTwitter" primary>{{ $ts.connectService }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="_debobigegoItem" v-if="enableDiscordIntegration">
|
||||
<div class="_debobigegoLabel"><i class="fab fa-discord"></i> Discord</div>
|
||||
<div class="_debobigegoPanel" style="padding: 16px;">
|
||||
<p v-if="integrations.discord">{{ $ts.connectedTo }}: <a :href="`https://discord.com/users/${integrations.discord.id}`" rel="nofollow noopener" target="_blank">@{{ integrations.discord.username }}#{{ integrations.discord.discriminator }}</a></p>
|
||||
<MkButton v-if="integrations.discord" @click="disconnectDiscord" danger>{{ $ts.disconnectService }}</MkButton>
|
||||
<MkButton v-else @click="connectDiscord" primary>{{ $ts.connectService }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="_debobigegoItem" v-if="enableGithubIntegration">
|
||||
<div class="_debobigegoLabel"><i class="fab fa-github"></i> GitHub</div>
|
||||
<div class="_debobigegoPanel" style="padding: 16px;">
|
||||
<p v-if="integrations.github">{{ $ts.connectedTo }}: <a :href="`https://github.com/${integrations.github.login}`" rel="nofollow noopener" target="_blank">@{{ integrations.github.login }}</a></p>
|
||||
<MkButton v-if="integrations.github" @click="disconnectGithub" danger>{{ $ts.disconnectService }}</MkButton>
|
||||
<MkButton v-else @click="connectGithub" primary>{{ $ts.connectService }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</FormBase>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import { apiUrl } from '@/config';
|
||||
import FormBase from '@/components/debobigego/base.vue';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import * as os from '@/os';
|
||||
import * as symbols from '@/symbols';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
FormBase,
|
||||
MkButton
|
||||
},
|
||||
|
||||
emits: ['info'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: this.$ts.integration,
|
||||
icon: 'fas fa-share-alt',
|
||||
bg: 'var(--bg)',
|
||||
},
|
||||
apiUrl,
|
||||
twitterForm: null,
|
||||
discordForm: null,
|
||||
githubForm: null,
|
||||
enableTwitterIntegration: false,
|
||||
enableDiscordIntegration: false,
|
||||
enableGithubIntegration: false,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
integrations() {
|
||||
return this.$i.integrations;
|
||||
},
|
||||
|
||||
meta() {
|
||||
return this.$instance;
|
||||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
this.enableTwitterIntegration = this.meta.enableTwitterIntegration;
|
||||
this.enableDiscordIntegration = this.meta.enableDiscordIntegration;
|
||||
this.enableGithubIntegration = this.meta.enableGithubIntegration;
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.$emit('info', this[symbols.PAGE_INFO]);
|
||||
|
||||
document.cookie = `igi=${this.$i.token}; path=/;` +
|
||||
` max-age=31536000;` +
|
||||
(document.location.protocol.startsWith('https') ? ' secure' : '');
|
||||
|
||||
this.$watch('integrations', () => {
|
||||
if (this.integrations.twitter) {
|
||||
if (this.twitterForm) this.twitterForm.close();
|
||||
}
|
||||
if (this.integrations.discord) {
|
||||
if (this.discordForm) this.discordForm.close();
|
||||
}
|
||||
if (this.integrations.github) {
|
||||
if (this.githubForm) this.githubForm.close();
|
||||
}
|
||||
}, {
|
||||
deep: true
|
||||
});
|
||||
},
|
||||
|
||||
methods: {
|
||||
connectTwitter() {
|
||||
this.twitterForm = window.open(apiUrl + '/connect/twitter',
|
||||
'twitter_connect_window',
|
||||
'height=570, width=520');
|
||||
},
|
||||
|
||||
disconnectTwitter() {
|
||||
window.open(apiUrl + '/disconnect/twitter',
|
||||
'twitter_disconnect_window',
|
||||
'height=570, width=520');
|
||||
},
|
||||
|
||||
connectDiscord() {
|
||||
this.discordForm = window.open(apiUrl + '/connect/discord',
|
||||
'discord_connect_window',
|
||||
'height=570, width=520');
|
||||
},
|
||||
|
||||
disconnectDiscord() {
|
||||
window.open(apiUrl + '/disconnect/discord',
|
||||
'discord_disconnect_window',
|
||||
'height=570, width=520');
|
||||
},
|
||||
|
||||
connectGithub() {
|
||||
this.githubForm = window.open(apiUrl + '/connect/github',
|
||||
'github_connect_window',
|
||||
'height=570, width=520');
|
||||
},
|
||||
|
||||
disconnectGithub() {
|
||||
window.open(apiUrl + '/disconnect/github',
|
||||
'github_disconnect_window',
|
||||
'height=570, width=520');
|
||||
},
|
||||
}
|
||||
});
|
||||
</script>
|
117
packages/client/src/pages/settings/menu.vue
Normal file
117
packages/client/src/pages/settings/menu.vue
Normal file
@ -0,0 +1,117 @@
|
||||
<template>
|
||||
<FormBase>
|
||||
<FormTextarea v-model="items" tall manual-save>
|
||||
<span>{{ $ts.menu }}</span>
|
||||
<template #desc><button class="_textButton" @click="addItem">{{ $ts.addItem }}</button></template>
|
||||
</FormTextarea>
|
||||
|
||||
<FormRadios v-model="menuDisplay">
|
||||
<template #desc>{{ $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に表示する必要があり面倒 -->
|
||||
</FormRadios>
|
||||
|
||||
<FormButton @click="reset()" danger><i class="fas fa-redo"></i> {{ $ts.default }}</FormButton>
|
||||
</FormBase>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import FormTextarea from '@/components/debobigego/textarea.vue';
|
||||
import FormRadios from '@/components/debobigego/radios.vue';
|
||||
import FormBase from '@/components/debobigego/base.vue';
|
||||
import FormButton from '@/components/debobigego/button.vue';
|
||||
import * as os from '@/os';
|
||||
import { menuDef } from '@/menu';
|
||||
import { defaultStore } from '@/store';
|
||||
import * as symbols from '@/symbols';
|
||||
import { unisonReload } from '@/scripts/unison-reload';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
FormBase,
|
||||
FormButton,
|
||||
FormTextarea,
|
||||
FormRadios,
|
||||
},
|
||||
|
||||
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'),
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
splited(): string[] {
|
||||
return this.items.trim().split('\n').filter(x => x.trim() !== '');
|
||||
},
|
||||
|
||||
menuDisplay: defaultStore.makeGetterSetter('menuDisplay')
|
||||
},
|
||||
|
||||
watch: {
|
||||
menuDisplay() {
|
||||
this.reloadAsk();
|
||||
},
|
||||
|
||||
items() {
|
||||
this.save();
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.$emit('info', this[symbols.PAGE_INFO]);
|
||||
},
|
||||
|
||||
methods: {
|
||||
async addItem() {
|
||||
const menu = Object.keys(this.menuDef).filter(k => !this.$store.state.menu.includes(k));
|
||||
const { canceled, result: item } = await os.dialog({
|
||||
type: null,
|
||||
title: this.$ts.addItem,
|
||||
select: {
|
||||
items: [...menu.map(k => ({
|
||||
value: k, text: this.$ts[this.menuDef[k].title]
|
||||
})), ...[{
|
||||
value: '-', text: this.$ts.divider
|
||||
}]]
|
||||
},
|
||||
showCancelButton: true
|
||||
});
|
||||
if (canceled) return;
|
||||
this.items = [...this.splited, item].join('\n');
|
||||
},
|
||||
|
||||
save() {
|
||||
this.$store.set('menu', this.splited);
|
||||
this.reloadAsk();
|
||||
},
|
||||
|
||||
reset() {
|
||||
this.$store.reset('menu');
|
||||
this.items = this.$store.state.menu.join('\n');
|
||||
},
|
||||
|
||||
async reloadAsk() {
|
||||
const { canceled } = await os.dialog({
|
||||
type: 'info',
|
||||
text: this.$ts.reloadToApplySetting,
|
||||
showCancelButton: true
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
unisonReload();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
85
packages/client/src/pages/settings/mute-block.vue
Normal file
85
packages/client/src/pages/settings/mute-block.vue
Normal file
@ -0,0 +1,85 @@
|
||||
<template>
|
||||
<FormBase>
|
||||
<MkTab v-model="tab" style="margin-bottom: var(--margin);">
|
||||
<option value="mute">{{ $ts.mutedUsers }}</option>
|
||||
<option value="block">{{ $ts.blockedUsers }}</option>
|
||||
</MkTab>
|
||||
<div v-if="tab === 'mute'">
|
||||
<MkPagination :pagination="mutingPagination" class="muting">
|
||||
<template #empty><FormInfo>{{ $ts.noUsers }}</FormInfo></template>
|
||||
<template #default="{items}">
|
||||
<FormGroup>
|
||||
<FormLink v-for="mute in items" :key="mute.id" :to="userPage(mute.mutee)">
|
||||
<MkAcct :user="mute.mutee"/>
|
||||
</FormLink>
|
||||
</FormGroup>
|
||||
</template>
|
||||
</MkPagination>
|
||||
</div>
|
||||
<div v-if="tab === 'block'">
|
||||
<MkPagination :pagination="blockingPagination" class="blocking">
|
||||
<template #empty><FormInfo>{{ $ts.noUsers }}</FormInfo></template>
|
||||
<template #default="{items}">
|
||||
<FormGroup>
|
||||
<FormLink v-for="block in items" :key="block.id" :to="userPage(block.blockee)">
|
||||
<MkAcct :user="block.blockee"/>
|
||||
</FormLink>
|
||||
</FormGroup>
|
||||
</template>
|
||||
</MkPagination>
|
||||
</div>
|
||||
</FormBase>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import MkPagination from '@/components/ui/pagination.vue';
|
||||
import MkTab from '@/components/tab.vue';
|
||||
import FormInfo from '@/components/debobigego/info.vue';
|
||||
import FormLink from '@/components/debobigego/link.vue';
|
||||
import FormBase from '@/components/debobigego/base.vue';
|
||||
import FormGroup from '@/components/debobigego/group.vue';
|
||||
import { userPage } from '@/filters/user';
|
||||
import * as os from '@/os';
|
||||
import * as symbols from '@/symbols';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkPagination,
|
||||
MkTab,
|
||||
FormInfo,
|
||||
FormBase,
|
||||
FormGroup,
|
||||
FormLink,
|
||||
},
|
||||
|
||||
emits: ['info'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: this.$ts.muteAndBlock,
|
||||
icon: 'fas fa-ban',
|
||||
bg: 'var(--bg)',
|
||||
},
|
||||
tab: 'mute',
|
||||
mutingPagination: {
|
||||
endpoint: 'mute/list',
|
||||
limit: 10,
|
||||
},
|
||||
blockingPagination: {
|
||||
endpoint: 'blocking/list',
|
||||
limit: 10,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.$emit('info', this[symbols.PAGE_INFO]);
|
||||
},
|
||||
|
||||
methods: {
|
||||
userPage
|
||||
}
|
||||
});
|
||||
</script>
|
77
packages/client/src/pages/settings/notifications.vue
Normal file
77
packages/client/src/pages/settings/notifications.vue
Normal file
@ -0,0 +1,77 @@
|
||||
<template>
|
||||
<FormBase>
|
||||
<FormLink @click="configure">{{ $ts.notificationSetting }}</FormLink>
|
||||
<FormGroup>
|
||||
<FormButton @click="readAllNotifications">{{ $ts.markAsReadAllNotifications }}</FormButton>
|
||||
<FormButton @click="readAllUnreadNotes">{{ $ts.markAsReadAllUnreadNotes }}</FormButton>
|
||||
<FormButton @click="readAllMessagingMessages">{{ $ts.markAsReadAllTalkMessages }}</FormButton>
|
||||
</FormGroup>
|
||||
</FormBase>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import FormButton from '@/components/debobigego/button.vue';
|
||||
import FormLink from '@/components/debobigego/link.vue';
|
||||
import FormBase from '@/components/debobigego/base.vue';
|
||||
import FormGroup from '@/components/debobigego/group.vue';
|
||||
import { notificationTypes } from 'misskey-js';
|
||||
import * as os from '@/os';
|
||||
import * as symbols from '@/symbols';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
FormBase,
|
||||
FormLink,
|
||||
FormButton,
|
||||
FormGroup,
|
||||
},
|
||||
|
||||
emits: ['info'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: this.$ts.notifications,
|
||||
icon: 'fas fa-bell',
|
||||
bg: 'var(--bg)',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.$emit('info', this[symbols.PAGE_INFO]);
|
||||
},
|
||||
|
||||
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');
|
||||
},
|
||||
}
|
||||
});
|
||||
</script>
|
97
packages/client/src/pages/settings/other.vue
Normal file
97
packages/client/src/pages/settings/other.vue
Normal file
@ -0,0 +1,97 @@
|
||||
<template>
|
||||
<FormBase>
|
||||
<FormLink to="/settings/update">Misskey Update</FormLink>
|
||||
|
||||
<FormSwitch :value="$i.injectFeaturedNote" @update:modelValue="onChangeInjectFeaturedNote">
|
||||
{{ $ts.showFeaturedNotesInTimeline }}
|
||||
</FormSwitch>
|
||||
|
||||
<FormSwitch v-model="reportError">{{ $ts.sendErrorReports }}<template #desc>{{ $ts.sendErrorReportsDescription }}</template></FormSwitch>
|
||||
|
||||
<FormLink to="/settings/account-info">{{ $ts.accountInfo }}</FormLink>
|
||||
<FormLink to="/settings/experimental-features">{{ $ts.experimentalFeatures }}</FormLink>
|
||||
|
||||
<FormGroup>
|
||||
<template #label>{{ $ts.developer }}</template>
|
||||
<FormSwitch v-model="debug" @update:modelValue="changeDebug">
|
||||
DEBUG MODE
|
||||
</FormSwitch>
|
||||
<template v-if="debug">
|
||||
<FormButton @click="taskmanager">Task Manager</FormButton>
|
||||
</template>
|
||||
</FormGroup>
|
||||
|
||||
<FormLink to="/settings/registry"><template #icon><i class="fas fa-cogs"></i></template>{{ $ts.registry }}</FormLink>
|
||||
|
||||
<FormLink to="/bios" behavior="browser"><template #icon><i class="fas fa-door-open"></i></template>BIOS</FormLink>
|
||||
<FormLink to="/cli" behavior="browser"><template #icon><i class="fas fa-door-open"></i></template>CLI</FormLink>
|
||||
|
||||
<FormLink to="/settings/delete-account"><template #icon><i class="fas fa-exclamation-triangle"></i></template>{{ $ts.closeAccount }}</FormLink>
|
||||
</FormBase>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineAsyncComponent, defineComponent } from 'vue';
|
||||
import FormSwitch from '@/components/form/switch.vue';
|
||||
import FormSelect from '@/components/form/select.vue';
|
||||
import FormLink from '@/components/debobigego/link.vue';
|
||||
import FormBase from '@/components/debobigego/base.vue';
|
||||
import FormGroup from '@/components/debobigego/group.vue';
|
||||
import FormButton from '@/components/debobigego/button.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';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
FormBase,
|
||||
FormSelect,
|
||||
FormSwitch,
|
||||
FormButton,
|
||||
FormLink,
|
||||
FormGroup,
|
||||
},
|
||||
|
||||
emits: ['info'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: this.$ts.other,
|
||||
icon: 'fas fa-ellipsis-h',
|
||||
bg: 'var(--bg)',
|
||||
},
|
||||
debug,
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
reportError: defaultStore.makeGetterSetter('reportError'),
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.$emit('info', this[symbols.PAGE_INFO]);
|
||||
},
|
||||
|
||||
methods: {
|
||||
changeDebug(v) {
|
||||
console.log(v);
|
||||
localStorage.setItem('debug', v.toString());
|
||||
unisonReload();
|
||||
},
|
||||
|
||||
onChangeInjectFeaturedNote(v) {
|
||||
os.api('i/update', {
|
||||
injectFeaturedNote: v
|
||||
});
|
||||
},
|
||||
|
||||
taskmanager() {
|
||||
os.popup(import('@/components/taskmanager.vue'), {
|
||||
}, {}, 'closed');
|
||||
},
|
||||
}
|
||||
});
|
||||
</script>
|
147
packages/client/src/pages/settings/plugin.install.vue
Normal file
147
packages/client/src/pages/settings/plugin.install.vue
Normal file
@ -0,0 +1,147 @@
|
||||
<template>
|
||||
<FormBase>
|
||||
<FormInfo warn>{{ $ts._plugin.installWarn }}</FormInfo>
|
||||
|
||||
<FormGroup>
|
||||
<FormTextarea v-model="code" tall>
|
||||
<span>{{ $ts.code }}</span>
|
||||
</FormTextarea>
|
||||
</FormGroup>
|
||||
|
||||
<FormButton @click="install" :disabled="code == null" primary inline><i class="fas fa-check"></i> {{ $ts.install }}</FormButton>
|
||||
</FormBase>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import { AiScript, parse } from '@syuilo/aiscript';
|
||||
import { serialize } from '@syuilo/aiscript/built/serializer';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import FormTextarea from '@/components/form/textarea.vue';
|
||||
import FormSelect from '@/components/form/select.vue';
|
||||
import FormRadios from '@/components/form/radios.vue';
|
||||
import FormBase from '@/components/debobigego/base.vue';
|
||||
import FormGroup from '@/components/debobigego/group.vue';
|
||||
import FormLink from '@/components/debobigego/link.vue';
|
||||
import FormButton from '@/components/debobigego/button.vue';
|
||||
import FormInfo from '@/components/debobigego/info.vue';
|
||||
import * as os from '@/os';
|
||||
import { ColdDeviceStorage } from '@/store';
|
||||
import { unisonReload } from '@/scripts/unison-reload';
|
||||
import * as symbols from '@/symbols';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
FormTextarea,
|
||||
FormSelect,
|
||||
FormRadios,
|
||||
FormBase,
|
||||
FormGroup,
|
||||
FormLink,
|
||||
FormButton,
|
||||
FormInfo,
|
||||
},
|
||||
|
||||
emits: ['info'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: this.$ts._plugin.install,
|
||||
icon: 'fas fa-download',
|
||||
bg: 'var(--bg)',
|
||||
},
|
||||
code: null,
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.$emit('info', this[symbols.PAGE_INFO]);
|
||||
},
|
||||
|
||||
methods: {
|
||||
installPlugin({ id, meta, ast, token }) {
|
||||
ColdDeviceStorage.set('plugins', ColdDeviceStorage.get('plugins').concat({
|
||||
...meta,
|
||||
id,
|
||||
active: true,
|
||||
configData: {},
|
||||
token: token,
|
||||
ast: ast
|
||||
}));
|
||||
},
|
||||
|
||||
async install() {
|
||||
let ast;
|
||||
try {
|
||||
ast = parse(this.code);
|
||||
} catch (e) {
|
||||
os.dialog({
|
||||
type: 'error',
|
||||
text: 'Syntax error :('
|
||||
});
|
||||
return;
|
||||
}
|
||||
const meta = AiScript.collectMetadata(ast);
|
||||
if (meta == null) {
|
||||
os.dialog({
|
||||
type: 'error',
|
||||
text: 'No metadata found :('
|
||||
});
|
||||
return;
|
||||
}
|
||||
const data = meta.get(null);
|
||||
if (data == null) {
|
||||
os.dialog({
|
||||
type: 'error',
|
||||
text: 'No metadata found :('
|
||||
});
|
||||
return;
|
||||
}
|
||||
const { name, version, author, description, permissions, config } = data;
|
||||
if (name == null || version == null || author == null) {
|
||||
os.dialog({
|
||||
type: 'error',
|
||||
text: 'Required property not found :('
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
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,
|
||||
});
|
||||
|
||||
res(token);
|
||||
}
|
||||
}, 'closed');
|
||||
});
|
||||
|
||||
this.installPlugin({
|
||||
id: uuid(),
|
||||
meta: {
|
||||
name, version, author, description, permissions, config
|
||||
},
|
||||
token,
|
||||
ast: serialize(ast)
|
||||
});
|
||||
|
||||
os.success();
|
||||
|
||||
this.$nextTick(() => {
|
||||
unisonReload();
|
||||
});
|
||||
},
|
||||
}
|
||||
});
|
||||
</script>
|
115
packages/client/src/pages/settings/plugin.manage.vue
Normal file
115
packages/client/src/pages/settings/plugin.manage.vue
Normal file
@ -0,0 +1,115 @@
|
||||
<template>
|
||||
<FormBase>
|
||||
<FormGroup v-for="plugin in plugins" :key="plugin.id">
|
||||
<template #label><span style="display: flex;"><b>{{ plugin.name }}</b><span style="margin-left: auto;">v{{ plugin.version }}</span></span></template>
|
||||
|
||||
<FormSwitch :value="plugin.active" @update:modelValue="changeActive(plugin, $event)">{{ $ts.makeActive }}</FormSwitch>
|
||||
<div class="_debobigegoItem">
|
||||
<div class="_debobigegoPanel" style="padding: 16px;">
|
||||
<div class="_keyValue">
|
||||
<div>{{ $ts.author }}:</div>
|
||||
<div>{{ plugin.author }}</div>
|
||||
</div>
|
||||
<div class="_keyValue">
|
||||
<div>{{ $ts.description }}:</div>
|
||||
<div>{{ plugin.description }}</div>
|
||||
</div>
|
||||
<div class="_keyValue">
|
||||
<div>{{ $ts.permission }}:</div>
|
||||
<div>{{ plugin.permissions }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="_debobigegoItem">
|
||||
<div class="_debobigegoPanel" style="padding: 16px;">
|
||||
<MkButton @click="config(plugin)" inline v-if="plugin.config"><i class="fas fa-cog"></i> {{ $ts.settings }}</MkButton>
|
||||
<MkButton @click="uninstall(plugin)" inline danger><i class="fas fa-trash-alt"></i> {{ $ts.uninstall }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</FormGroup>
|
||||
</FormBase>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import MkTextarea from '@/components/form/textarea.vue';
|
||||
import MkSelect from '@/components/form/select.vue';
|
||||
import FormSwitch from '@/components/form/switch.vue';
|
||||
import FormBase from '@/components/debobigego/base.vue';
|
||||
import FormGroup from '@/components/debobigego/group.vue';
|
||||
import * as os from '@/os';
|
||||
import { ColdDeviceStorage } from '@/store';
|
||||
import * as symbols from '@/symbols';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkButton,
|
||||
MkTextarea,
|
||||
MkSelect,
|
||||
FormSwitch,
|
||||
FormBase,
|
||||
FormGroup,
|
||||
},
|
||||
|
||||
emits: ['info'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: this.$ts._plugin.manage,
|
||||
icon: 'fas fa-plug',
|
||||
bg: 'var(--bg)',
|
||||
},
|
||||
plugins: ColdDeviceStorage.get('plugins'),
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.$emit('info', this[symbols.PAGE_INFO]);
|
||||
},
|
||||
|
||||
methods: {
|
||||
uninstall(plugin) {
|
||||
ColdDeviceStorage.set('plugins', this.plugins.filter(x => x.id !== plugin.id));
|
||||
os.success();
|
||||
this.$nextTick(() => {
|
||||
unisonReload();
|
||||
});
|
||||
},
|
||||
|
||||
// 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 plugins = ColdDeviceStorage.get('plugins');
|
||||
plugins.find(p => p.id === plugin.id).configData = result;
|
||||
ColdDeviceStorage.set('plugins', plugins);
|
||||
|
||||
this.$nextTick(() => {
|
||||
location.reload();
|
||||
});
|
||||
},
|
||||
|
||||
changeActive(plugin, active) {
|
||||
const plugins = ColdDeviceStorage.get('plugins');
|
||||
plugins.find(p => p.id === plugin.id).active = active;
|
||||
ColdDeviceStorage.set('plugins', plugins);
|
||||
|
||||
this.$nextTick(() => {
|
||||
location.reload();
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
44
packages/client/src/pages/settings/plugin.vue
Normal file
44
packages/client/src/pages/settings/plugin.vue
Normal file
@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<FormBase>
|
||||
<FormLink to="/settings/plugin/install"><template #icon><i class="fas fa-download"></i></template>{{ $ts._plugin.install }}</FormLink>
|
||||
<FormLink to="/settings/plugin/manage"><template #icon><i class="fas fa-folder-open"></i></template>{{ $ts._plugin.manage }}<template #suffix>{{ plugins }}</template></FormLink>
|
||||
</FormBase>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import FormBase from '@/components/debobigego/base.vue';
|
||||
import FormGroup from '@/components/debobigego/group.vue';
|
||||
import FormLink from '@/components/debobigego/link.vue';
|
||||
import * as os from '@/os';
|
||||
import { ColdDeviceStorage } from '@/store';
|
||||
import * as symbols from '@/symbols';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
FormBase,
|
||||
FormLink,
|
||||
},
|
||||
|
||||
emits: ['info'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: this.$ts.plugins,
|
||||
icon: 'fas fa-plug',
|
||||
bg: 'var(--bg)',
|
||||
},
|
||||
plugins: ColdDeviceStorage.get('plugins').length,
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.$emit('info', this[symbols.PAGE_INFO]);
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
120
packages/client/src/pages/settings/privacy.vue
Normal file
120
packages/client/src/pages/settings/privacy.vue
Normal file
@ -0,0 +1,120 @@
|
||||
<template>
|
||||
<FormBase>
|
||||
<FormGroup>
|
||||
<FormSwitch v-model="isLocked" @update:modelValue="save()">{{ $ts.makeFollowManuallyApprove }}</FormSwitch>
|
||||
<FormSwitch v-model="autoAcceptFollowed" :disabled="!isLocked" @update:modelValue="save()">{{ $ts.autoAcceptFollowed }}</FormSwitch>
|
||||
<template #caption>{{ $ts.lockedAccountInfo }}</template>
|
||||
</FormGroup>
|
||||
<FormSwitch v-model="publicReactions" @update:modelValue="save()">
|
||||
{{ $ts.makeReactionsPublic }}
|
||||
<template #desc>{{ $ts.makeReactionsPublicDescription }}</template>
|
||||
</FormSwitch>
|
||||
<FormGroup>
|
||||
<template #label>{{ $ts.ffVisibility }}</template>
|
||||
<FormSelect v-model="ffVisibility">
|
||||
<option value="public">{{ $ts._ffVisibility.public }}</option>
|
||||
<option value="followers">{{ $ts._ffVisibility.followers }}</option>
|
||||
<option value="private">{{ $ts._ffVisibility.private }}</option>
|
||||
</FormSelect>
|
||||
<template #caption>{{ $ts.ffVisibilityDescription }}</template>
|
||||
</FormGroup>
|
||||
<FormSwitch v-model="hideOnlineStatus" @update:modelValue="save()">
|
||||
{{ $ts.hideOnlineStatus }}
|
||||
<template #desc>{{ $ts.hideOnlineStatusDescription }}</template>
|
||||
</FormSwitch>
|
||||
<FormSwitch v-model="noCrawle" @update:modelValue="save()">
|
||||
{{ $ts.noCrawle }}
|
||||
<template #desc>{{ $ts.noCrawleDescription }}</template>
|
||||
</FormSwitch>
|
||||
<FormSwitch v-model="isExplorable" @update:modelValue="save()">
|
||||
{{ $ts.makeExplorable }}
|
||||
<template #desc>{{ $ts.makeExplorableDescription }}</template>
|
||||
</FormSwitch>
|
||||
<FormSwitch v-model="rememberNoteVisibility" @update:modelValue="save()">{{ $ts.rememberNoteVisibility }}</FormSwitch>
|
||||
<FormGroup v-if="!rememberNoteVisibility">
|
||||
<template #label>{{ $ts.defaultNoteVisibility }}</template>
|
||||
<FormSelect v-model="defaultNoteVisibility">
|
||||
<option value="public">{{ $ts._visibility.public }}</option>
|
||||
<option value="home">{{ $ts._visibility.home }}</option>
|
||||
<option value="followers">{{ $ts._visibility.followers }}</option>
|
||||
<option value="specified">{{ $ts._visibility.specified }}</option>
|
||||
</FormSelect>
|
||||
<FormSwitch v-model="defaultNoteLocalOnly">{{ $ts._visibility.localOnly }}</FormSwitch>
|
||||
</FormGroup>
|
||||
<FormSwitch v-model="keepCw" @update:modelValue="save()">{{ $ts.keepCw }}</FormSwitch>
|
||||
</FormBase>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import FormSwitch from '@/components/debobigego/switch.vue';
|
||||
import FormSelect from '@/components/debobigego/select.vue';
|
||||
import FormBase from '@/components/debobigego/base.vue';
|
||||
import FormGroup from '@/components/debobigego/group.vue';
|
||||
import * as os from '@/os';
|
||||
import { defaultStore } from '@/store';
|
||||
import * as symbols from '@/symbols';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
FormBase,
|
||||
FormSelect,
|
||||
FormGroup,
|
||||
FormSwitch,
|
||||
},
|
||||
|
||||
emits: ['info'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: this.$ts.privacy,
|
||||
icon: 'fas fa-lock-open',
|
||||
bg: 'var(--bg)',
|
||||
},
|
||||
isLocked: false,
|
||||
autoAcceptFollowed: false,
|
||||
noCrawle: false,
|
||||
isExplorable: false,
|
||||
hideOnlineStatus: false,
|
||||
publicReactions: false,
|
||||
ffVisibility: 'public',
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
defaultNoteVisibility: defaultStore.makeGetterSetter('defaultNoteVisibility'),
|
||||
defaultNoteLocalOnly: defaultStore.makeGetterSetter('defaultNoteLocalOnly'),
|
||||
rememberNoteVisibility: defaultStore.makeGetterSetter('rememberNoteVisibility'),
|
||||
keepCw: defaultStore.makeGetterSetter('keepCw'),
|
||||
},
|
||||
|
||||
created() {
|
||||
this.isLocked = this.$i.isLocked;
|
||||
this.autoAcceptFollowed = this.$i.autoAcceptFollowed;
|
||||
this.noCrawle = this.$i.noCrawle;
|
||||
this.isExplorable = this.$i.isExplorable;
|
||||
this.hideOnlineStatus = this.$i.hideOnlineStatus;
|
||||
this.publicReactions = this.$i.publicReactions;
|
||||
this.ffVisibility = this.$i.ffVisibility;
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.$emit('info', this[symbols.PAGE_INFO]);
|
||||
},
|
||||
|
||||
methods: {
|
||||
save() {
|
||||
os.api('i/update', {
|
||||
isLocked: !!this.isLocked,
|
||||
autoAcceptFollowed: !!this.autoAcceptFollowed,
|
||||
noCrawle: !!this.noCrawle,
|
||||
isExplorable: !!this.isExplorable,
|
||||
hideOnlineStatus: !!this.hideOnlineStatus,
|
||||
publicReactions: !!this.publicReactions,
|
||||
ffVisibility: this.ffVisibility,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
281
packages/client/src/pages/settings/profile.vue
Normal file
281
packages/client/src/pages/settings/profile.vue
Normal file
@ -0,0 +1,281 @@
|
||||
<template>
|
||||
<FormBase>
|
||||
<FormGroup>
|
||||
<div class="_debobigegoItem _debobigegoPanel llvierxe" :style="{ backgroundImage: $i.bannerUrl ? `url(${ $i.bannerUrl })` : null }">
|
||||
<MkAvatar class="avatar" :user="$i"/>
|
||||
</div>
|
||||
<FormButton @click="changeAvatar" primary>{{ $ts._profile.changeAvatar }}</FormButton>
|
||||
<FormButton @click="changeBanner" primary>{{ $ts._profile.changeBanner }}</FormButton>
|
||||
</FormGroup>
|
||||
|
||||
<FormInput v-model="name" :max="30" manual-save>
|
||||
<span>{{ $ts._profile.name }}</span>
|
||||
</FormInput>
|
||||
|
||||
<FormTextarea v-model="description" :max="500" tall manual-save>
|
||||
<span>{{ $ts._profile.description }}</span>
|
||||
<template #desc>{{ $ts._profile.youCanIncludeHashtags }}</template>
|
||||
</FormTextarea>
|
||||
|
||||
<FormInput v-model="location" manual-save>
|
||||
<span>{{ $ts.location }}</span>
|
||||
<template #prefix><i class="fas fa-map-marker-alt"></i></template>
|
||||
</FormInput>
|
||||
|
||||
<FormInput v-model="birthday" type="date" manual-save>
|
||||
<span>{{ $ts.birthday }}</span>
|
||||
<template #prefix><i class="fas fa-birthday-cake"></i></template>
|
||||
</FormInput>
|
||||
|
||||
<FormSelect v-model="lang">
|
||||
<template #label>{{ $ts.language }}</template>
|
||||
<option v-for="x in langs" :value="x[0]" :key="x[0]">{{ x[1] }}</option>
|
||||
</FormSelect>
|
||||
|
||||
<FormGroup>
|
||||
<FormButton @click="editMetadata" primary>{{ $ts._profile.metadataEdit }}</FormButton>
|
||||
<template #caption>{{ $ts._profile.metadataDescription }}</template>
|
||||
</FormGroup>
|
||||
|
||||
<FormSwitch v-model="isCat">{{ $ts.flagAsCat }}<template #desc>{{ $ts.flagAsCatDescription }}</template></FormSwitch>
|
||||
|
||||
<FormSwitch v-model="isBot">{{ $ts.flagAsBot }}<template #desc>{{ $ts.flagAsBotDescription }}</template></FormSwitch>
|
||||
|
||||
<FormSwitch v-model="alwaysMarkNsfw">{{ $ts.alwaysMarkSensitive }}</FormSwitch>
|
||||
</FormBase>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import FormButton from '@/components/debobigego/button.vue';
|
||||
import FormInput from '@/components/debobigego/input.vue';
|
||||
import FormTextarea from '@/components/debobigego/textarea.vue';
|
||||
import FormSwitch from '@/components/debobigego/switch.vue';
|
||||
import FormSelect from '@/components/debobigego/select.vue';
|
||||
import FormBase from '@/components/debobigego/base.vue';
|
||||
import FormGroup from '@/components/debobigego/group.vue';
|
||||
import { host, langs } from '@/config';
|
||||
import { selectFile } from '@/scripts/select-file';
|
||||
import * as os from '@/os';
|
||||
import * as symbols from '@/symbols';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
FormButton,
|
||||
FormInput,
|
||||
FormTextarea,
|
||||
FormSwitch,
|
||||
FormSelect,
|
||||
FormBase,
|
||||
FormGroup,
|
||||
},
|
||||
|
||||
emits: ['info'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: this.$ts.profile,
|
||||
icon: 'fas fa-user',
|
||||
bg: 'var(--bg)',
|
||||
},
|
||||
host,
|
||||
langs,
|
||||
name: null,
|
||||
description: null,
|
||||
birthday: null,
|
||||
lang: null,
|
||||
location: null,
|
||||
fieldName0: null,
|
||||
fieldValue0: null,
|
||||
fieldName1: null,
|
||||
fieldValue1: null,
|
||||
fieldName2: null,
|
||||
fieldValue2: null,
|
||||
fieldName3: null,
|
||||
fieldValue3: null,
|
||||
avatarId: null,
|
||||
bannerId: null,
|
||||
isBot: false,
|
||||
isCat: false,
|
||||
alwaysMarkNsfw: false,
|
||||
saving: false,
|
||||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
this.name = this.$i.name;
|
||||
this.description = this.$i.description;
|
||||
this.location = this.$i.location;
|
||||
this.birthday = this.$i.birthday;
|
||||
this.lang = this.$i.lang;
|
||||
this.avatarId = this.$i.avatarId;
|
||||
this.bannerId = this.$i.bannerId;
|
||||
this.isBot = this.$i.isBot;
|
||||
this.isCat = this.$i.isCat;
|
||||
this.alwaysMarkNsfw = this.$i.alwaysMarkNsfw;
|
||||
|
||||
this.fieldName0 = this.$i.fields[0] ? this.$i.fields[0].name : null;
|
||||
this.fieldValue0 = this.$i.fields[0] ? this.$i.fields[0].value : null;
|
||||
this.fieldName1 = this.$i.fields[1] ? this.$i.fields[1].name : null;
|
||||
this.fieldValue1 = this.$i.fields[1] ? this.$i.fields[1].value : null;
|
||||
this.fieldName2 = this.$i.fields[2] ? this.$i.fields[2].name : null;
|
||||
this.fieldValue2 = this.$i.fields[2] ? this.$i.fields[2].value : null;
|
||||
this.fieldName3 = this.$i.fields[3] ? this.$i.fields[3].name : null;
|
||||
this.fieldValue3 = this.$i.fields[3] ? this.$i.fields[3].value : null;
|
||||
|
||||
this.$watch('name', this.save);
|
||||
this.$watch('description', this.save);
|
||||
this.$watch('location', this.save);
|
||||
this.$watch('birthday', this.save);
|
||||
this.$watch('lang', this.save);
|
||||
this.$watch('isBot', this.save);
|
||||
this.$watch('isCat', this.save);
|
||||
this.$watch('alwaysMarkNsfw', this.save);
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.$emit('info', this[symbols.PAGE_INFO]);
|
||||
},
|
||||
|
||||
methods: {
|
||||
changeAvatar(e) {
|
||||
selectFile(e.currentTarget || e.target, this.$ts.avatar).then(file => {
|
||||
os.api('i/update', {
|
||||
avatarId: file.id,
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
changeBanner(e) {
|
||||
selectFile(e.currentTarget || e.target, this.$ts.banner).then(file => {
|
||||
os.api('i/update', {
|
||||
bannerId: file.id,
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
async editMetadata() {
|
||||
const { canceled, result } = await os.form(this.$ts._profile.metadata, {
|
||||
fieldName0: {
|
||||
type: 'string',
|
||||
label: this.$ts._profile.metadataLabel + ' 1',
|
||||
default: this.fieldName0,
|
||||
},
|
||||
fieldValue0: {
|
||||
type: 'string',
|
||||
label: this.$ts._profile.metadataContent + ' 1',
|
||||
default: this.fieldValue0,
|
||||
},
|
||||
fieldName1: {
|
||||
type: 'string',
|
||||
label: this.$ts._profile.metadataLabel + ' 2',
|
||||
default: this.fieldName1,
|
||||
},
|
||||
fieldValue1: {
|
||||
type: 'string',
|
||||
label: this.$ts._profile.metadataContent + ' 2',
|
||||
default: this.fieldValue1,
|
||||
},
|
||||
fieldName2: {
|
||||
type: 'string',
|
||||
label: this.$ts._profile.metadataLabel + ' 3',
|
||||
default: this.fieldName2,
|
||||
},
|
||||
fieldValue2: {
|
||||
type: 'string',
|
||||
label: this.$ts._profile.metadataContent + ' 3',
|
||||
default: this.fieldValue2,
|
||||
},
|
||||
fieldName3: {
|
||||
type: 'string',
|
||||
label: this.$ts._profile.metadataLabel + ' 4',
|
||||
default: this.fieldName3,
|
||||
},
|
||||
fieldValue3: {
|
||||
type: 'string',
|
||||
label: this.$ts._profile.metadataContent + ' 4',
|
||||
default: this.fieldValue3,
|
||||
},
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
this.fieldName0 = result.fieldName0;
|
||||
this.fieldValue0 = result.fieldValue0;
|
||||
this.fieldName1 = result.fieldName1;
|
||||
this.fieldValue1 = result.fieldValue1;
|
||||
this.fieldName2 = result.fieldName2;
|
||||
this.fieldValue2 = result.fieldValue2;
|
||||
this.fieldName3 = result.fieldName3;
|
||||
this.fieldValue3 = result.fieldValue3;
|
||||
|
||||
const fields = [
|
||||
{ name: this.fieldName0, value: this.fieldValue0 },
|
||||
{ name: this.fieldName1, value: this.fieldValue1 },
|
||||
{ name: this.fieldName2, value: this.fieldValue2 },
|
||||
{ name: this.fieldName3, value: this.fieldValue3 },
|
||||
];
|
||||
|
||||
os.api('i/update', {
|
||||
fields,
|
||||
}).then(i => {
|
||||
os.success();
|
||||
}).catch(err => {
|
||||
os.dialog({
|
||||
type: 'error',
|
||||
text: err.id
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
save() {
|
||||
this.saving = true;
|
||||
|
||||
os.apiWithDialog('i/update', {
|
||||
name: this.name || null,
|
||||
description: this.description || null,
|
||||
location: this.location || null,
|
||||
birthday: this.birthday || null,
|
||||
lang: this.lang || null,
|
||||
isBot: !!this.isBot,
|
||||
isCat: !!this.isCat,
|
||||
alwaysMarkNsfw: !!this.alwaysMarkNsfw,
|
||||
}).then(i => {
|
||||
this.saving = false;
|
||||
this.$i.avatarId = i.avatarId;
|
||||
this.$i.avatarUrl = i.avatarUrl;
|
||||
this.$i.bannerId = i.bannerId;
|
||||
this.$i.bannerUrl = i.bannerUrl;
|
||||
}).catch(err => {
|
||||
this.saving = false;
|
||||
});
|
||||
},
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.llvierxe {
|
||||
position: relative;
|
||||
height: 150px;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
|
||||
> * {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
> .avatar {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: block;
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
margin: auto;
|
||||
box-shadow: 0 0 0 6px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
}
|
||||
</style>
|
152
packages/client/src/pages/settings/reaction.vue
Normal file
152
packages/client/src/pages/settings/reaction.vue
Normal file
@ -0,0 +1,152 @@
|
||||
<template>
|
||||
<FormBase>
|
||||
<div class="_debobigegoItem">
|
||||
<div class="_debobigegoLabel">{{ $ts.reactionSettingDescription }}</div>
|
||||
<div class="_debobigegoPanel">
|
||||
<XDraggable class="zoaiodol" v-model="reactions" :item-key="item => item" animation="150" delay="100" delay-on-touch-only="true">
|
||||
<template #item="{element}">
|
||||
<button class="_button item" @click="remove(element, $event)">
|
||||
<MkEmoji :emoji="element" :normal="true"/>
|
||||
</button>
|
||||
</template>
|
||||
<template #footer>
|
||||
<button class="_button add" @click="chooseEmoji"><i class="fas fa-plus"></i></button>
|
||||
</template>
|
||||
</XDraggable>
|
||||
</div>
|
||||
<div class="_debobigegoCaption">{{ $ts.reactionSettingDescription2 }} <button class="_textButton" @click="preview">{{ $ts.preview }}</button></div>
|
||||
</div>
|
||||
|
||||
<FormRadios v-model="reactionPickerWidth">
|
||||
<template #desc>{{ $ts.width }}</template>
|
||||
<option :value="1">{{ $ts.small }}</option>
|
||||
<option :value="2">{{ $ts.medium }}</option>
|
||||
<option :value="3">{{ $ts.large }}</option>
|
||||
</FormRadios>
|
||||
<FormRadios v-model="reactionPickerHeight">
|
||||
<template #desc>{{ $ts.height }}</template>
|
||||
<option :value="1">{{ $ts.small }}</option>
|
||||
<option :value="2">{{ $ts.medium }}</option>
|
||||
<option :value="3">{{ $ts.large }}</option>
|
||||
</FormRadios>
|
||||
<FormButton @click="preview"><i class="fas fa-eye"></i> {{ $ts.preview }}</FormButton>
|
||||
<FormButton danger @click="setDefault"><i class="fas fa-undo"></i> {{ $ts.default }}</FormButton>
|
||||
</FormBase>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import XDraggable from 'vuedraggable';
|
||||
import FormInput from '@/components/debobigego/input.vue';
|
||||
import FormRadios from '@/components/debobigego/radios.vue';
|
||||
import FormBase from '@/components/debobigego/base.vue';
|
||||
import FormButton from '@/components/debobigego/button.vue';
|
||||
import * as os from '@/os';
|
||||
import { defaultStore } from '@/store';
|
||||
import * as symbols from '@/symbols';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
FormInput,
|
||||
FormButton,
|
||||
FormBase,
|
||||
FormRadios,
|
||||
XDraggable,
|
||||
},
|
||||
|
||||
emits: ['info'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: this.$ts.reaction,
|
||||
icon: 'fas fa-laugh',
|
||||
action: {
|
||||
icon: 'fas fa-eye',
|
||||
handler: this.preview
|
||||
},
|
||||
bg: 'var(--bg)',
|
||||
},
|
||||
reactions: JSON.parse(JSON.stringify(this.$store.state.reactions)),
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
reactionPickerWidth: defaultStore.makeGetterSetter('reactionPickerWidth'),
|
||||
reactionPickerHeight: defaultStore.makeGetterSetter('reactionPickerHeight'),
|
||||
},
|
||||
|
||||
watch: {
|
||||
reactions: {
|
||||
handler() {
|
||||
this.save();
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.$emit('info', this[symbols.PAGE_INFO]);
|
||||
},
|
||||
|
||||
methods: {
|
||||
save() {
|
||||
this.$store.set('reactions', this.reactions);
|
||||
},
|
||||
|
||||
remove(reaction, ev) {
|
||||
os.popupMenu([{
|
||||
text: this.$ts.remove,
|
||||
action: () => {
|
||||
this.reactions = this.reactions.filter(x => x !== reaction)
|
||||
}
|
||||
}], ev.currentTarget || ev.target);
|
||||
},
|
||||
|
||||
preview(ev) {
|
||||
os.popup(import('@/components/emoji-picker-dialog.vue'), {
|
||||
asReactionPicker: true,
|
||||
src: ev.currentTarget || ev.target,
|
||||
}, {}, 'closed');
|
||||
},
|
||||
|
||||
async setDefault() {
|
||||
const { canceled } = await os.dialog({
|
||||
type: 'warning',
|
||||
text: this.$ts.resetAreYouSure,
|
||||
showCancelButton: true
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
this.reactions = JSON.parse(JSON.stringify(this.$store.def.reactions.default));
|
||||
},
|
||||
|
||||
chooseEmoji(ev) {
|
||||
os.pickEmoji(ev.currentTarget || ev.target, {
|
||||
showPinned: false
|
||||
}).then(emoji => {
|
||||
if (!this.reactions.includes(emoji)) {
|
||||
this.reactions.push(emoji);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.zoaiodol {
|
||||
padding: 16px;
|
||||
|
||||
> .item {
|
||||
display: inline-block;
|
||||
padding: 8px;
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
> .add {
|
||||
display: inline-block;
|
||||
padding: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
114
packages/client/src/pages/settings/registry.keys.vue
Normal file
114
packages/client/src/pages/settings/registry.keys.vue
Normal file
@ -0,0 +1,114 @@
|
||||
<template>
|
||||
<FormBase>
|
||||
<FormGroup>
|
||||
<FormKeyValueView>
|
||||
<template #key>{{ $ts._registry.domain }}</template>
|
||||
<template #value>{{ $ts.system }}</template>
|
||||
</FormKeyValueView>
|
||||
<FormKeyValueView>
|
||||
<template #key>{{ $ts._registry.scope }}</template>
|
||||
<template #value>{{ scope.join('/') }}</template>
|
||||
</FormKeyValueView>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup v-if="keys">
|
||||
<template #label>{{ $ts._registry.keys }}</template>
|
||||
<FormLink v-for="key in keys" :to="`/settings/registry/value/system/${scope.join('/')}/${key[0]}`" class="_monospace">{{ key[0] }}<template #suffix>{{ key[1].toUpperCase() }}</template></FormLink>
|
||||
</FormGroup>
|
||||
|
||||
<FormButton @click="createKey" primary>{{ $ts._registry.createKey }}</FormButton>
|
||||
</FormBase>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineAsyncComponent, defineComponent } from 'vue';
|
||||
import * as JSON5 from 'json5';
|
||||
import FormSwitch from '@/components/form/switch.vue';
|
||||
import FormSelect from '@/components/form/select.vue';
|
||||
import FormLink from '@/components/debobigego/link.vue';
|
||||
import FormBase from '@/components/debobigego/base.vue';
|
||||
import FormGroup from '@/components/debobigego/group.vue';
|
||||
import FormButton from '@/components/debobigego/button.vue';
|
||||
import FormKeyValueView from '@/components/debobigego/key-value-view.vue';
|
||||
import * as os from '@/os';
|
||||
import * as symbols from '@/symbols';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
FormBase,
|
||||
FormSelect,
|
||||
FormSwitch,
|
||||
FormButton,
|
||||
FormLink,
|
||||
FormGroup,
|
||||
FormKeyValueView,
|
||||
},
|
||||
|
||||
props: {
|
||||
scope: {
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
emits: ['info'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: this.$ts.registry,
|
||||
icon: 'fas fa-cogs',
|
||||
bg: 'var(--bg)',
|
||||
},
|
||||
keys: null,
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
scope() {
|
||||
this.fetch();
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.$emit('info', this[symbols.PAGE_INFO]);
|
||||
this.fetch();
|
||||
},
|
||||
|
||||
methods: {
|
||||
fetch() {
|
||||
os.api('i/registry/keys-with-type', {
|
||||
scope: this.scope
|
||||
}).then(keys => {
|
||||
this.keys = Object.entries(keys).sort((a, b) => a[0].localeCompare(b[0]));
|
||||
});
|
||||
},
|
||||
|
||||
async createKey() {
|
||||
const { canceled, result } = await os.form(this.$ts._registry.createKey, {
|
||||
key: {
|
||||
type: 'string',
|
||||
label: this.$ts._registry.key,
|
||||
},
|
||||
value: {
|
||||
type: 'string',
|
||||
multiline: true,
|
||||
label: this.$ts.value,
|
||||
},
|
||||
scope: {
|
||||
type: 'string',
|
||||
label: this.$ts._registry.scope,
|
||||
default: this.scope.join('/')
|
||||
}
|
||||
});
|
||||
if (canceled) return;
|
||||
os.apiWithDialog('i/registry/set', {
|
||||
scope: result.scope.split('/'),
|
||||
key: result.key,
|
||||
value: JSON5.parse(result.value),
|
||||
}).then(() => {
|
||||
this.fetch();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
149
packages/client/src/pages/settings/registry.value.vue
Normal file
149
packages/client/src/pages/settings/registry.value.vue
Normal file
@ -0,0 +1,149 @@
|
||||
<template>
|
||||
<FormBase>
|
||||
<FormInfo warn>{{ $ts.editTheseSettingsMayBreakAccount }}</FormInfo>
|
||||
|
||||
<template v-if="value">
|
||||
<FormGroup>
|
||||
<FormKeyValueView>
|
||||
<template #key>{{ $ts._registry.domain }}</template>
|
||||
<template #value>{{ $ts.system }}</template>
|
||||
</FormKeyValueView>
|
||||
<FormKeyValueView>
|
||||
<template #key>{{ $ts._registry.scope }}</template>
|
||||
<template #value>{{ scope.join('/') }}</template>
|
||||
</FormKeyValueView>
|
||||
<FormKeyValueView>
|
||||
<template #key>{{ $ts._registry.key }}</template>
|
||||
<template #value>{{ xKey }}</template>
|
||||
</FormKeyValueView>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormTextarea tall v-model="valueForEditor" class="_monospace" style="tab-size: 2;">
|
||||
<span>{{ $ts.value }} (JSON)</span>
|
||||
</FormTextarea>
|
||||
<FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
|
||||
</FormGroup>
|
||||
|
||||
<FormKeyValueView>
|
||||
<template #key>{{ $ts.updatedAt }}</template>
|
||||
<template #value><MkTime :time="value.updatedAt" mode="detail"/></template>
|
||||
</FormKeyValueView>
|
||||
|
||||
<FormButton danger @click="del"><i class="fas fa-trash"></i> {{ $ts.delete }}</FormButton>
|
||||
</template>
|
||||
</FormBase>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineAsyncComponent, defineComponent } from 'vue';
|
||||
import * as JSON5 from 'json5';
|
||||
import FormInfo from '@/components/debobigego/info.vue';
|
||||
import FormSwitch from '@/components/form/switch.vue';
|
||||
import FormSelect from '@/components/form/select.vue';
|
||||
import FormTextarea from '@/components/form/textarea.vue';
|
||||
import FormBase from '@/components/debobigego/base.vue';
|
||||
import FormGroup from '@/components/debobigego/group.vue';
|
||||
import FormButton from '@/components/debobigego/button.vue';
|
||||
import FormKeyValueView from '@/components/debobigego/key-value-view.vue';
|
||||
import * as os from '@/os';
|
||||
import * as symbols from '@/symbols';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
FormInfo,
|
||||
FormBase,
|
||||
FormSelect,
|
||||
FormSwitch,
|
||||
FormButton,
|
||||
FormTextarea,
|
||||
FormGroup,
|
||||
FormKeyValueView,
|
||||
},
|
||||
|
||||
props: {
|
||||
scope: {
|
||||
required: true
|
||||
},
|
||||
xKey: {
|
||||
required: true
|
||||
},
|
||||
},
|
||||
|
||||
emits: ['info'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: this.$ts.registry,
|
||||
icon: 'fas fa-cogs',
|
||||
bg: 'var(--bg)',
|
||||
},
|
||||
value: null,
|
||||
valueForEditor: null,
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
key() {
|
||||
this.fetch();
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.$emit('info', this[symbols.PAGE_INFO]);
|
||||
this.fetch();
|
||||
},
|
||||
|
||||
methods: {
|
||||
fetch() {
|
||||
os.api('i/registry/get-detail', {
|
||||
scope: this.scope,
|
||||
key: this.xKey
|
||||
}).then(value => {
|
||||
this.value = value;
|
||||
this.valueForEditor = JSON5.stringify(this.value.value, null, '\t');
|
||||
});
|
||||
},
|
||||
|
||||
save() {
|
||||
try {
|
||||
JSON5.parse(this.valueForEditor);
|
||||
} catch (e) {
|
||||
os.dialog({
|
||||
type: 'error',
|
||||
text: this.$ts.invalidValue
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
os.dialog({
|
||||
type: 'warning',
|
||||
text: this.$ts.saveConfirm,
|
||||
showCancelButton: true
|
||||
}).then(({ canceled }) => {
|
||||
if (canceled) return;
|
||||
os.apiWithDialog('i/registry/set', {
|
||||
scope: this.scope,
|
||||
key: this.xKey,
|
||||
value: JSON5.parse(this.valueForEditor)
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
del() {
|
||||
os.dialog({
|
||||
type: 'warning',
|
||||
text: this.$ts.deleteConfirm,
|
||||
showCancelButton: true
|
||||
}).then(({ canceled }) => {
|
||||
if (canceled) return;
|
||||
os.apiWithDialog('i/registry/remove', {
|
||||
scope: this.scope,
|
||||
key: this.xKey
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
90
packages/client/src/pages/settings/registry.vue
Normal file
90
packages/client/src/pages/settings/registry.vue
Normal file
@ -0,0 +1,90 @@
|
||||
<template>
|
||||
<FormBase>
|
||||
<FormGroup v-if="scopes">
|
||||
<template #label>{{ $ts.system }}</template>
|
||||
<FormLink v-for="scope in scopes" :to="`/settings/registry/keys/system/${scope.join('/')}`" class="_monospace">{{ scope.join('/') }}</FormLink>
|
||||
</FormGroup>
|
||||
<FormButton @click="createKey" primary>{{ $ts._registry.createKey }}</FormButton>
|
||||
</FormBase>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineAsyncComponent, defineComponent } from 'vue';
|
||||
import * as JSON5 from 'json5';
|
||||
import FormSwitch from '@/components/form/switch.vue';
|
||||
import FormSelect from '@/components/form/select.vue';
|
||||
import FormLink from '@/components/debobigego/link.vue';
|
||||
import FormBase from '@/components/debobigego/base.vue';
|
||||
import FormGroup from '@/components/debobigego/group.vue';
|
||||
import FormButton from '@/components/debobigego/button.vue';
|
||||
import FormKeyValueView from '@/components/debobigego/key-value-view.vue';
|
||||
import * as os from '@/os';
|
||||
import * as symbols from '@/symbols';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
FormBase,
|
||||
FormSelect,
|
||||
FormSwitch,
|
||||
FormButton,
|
||||
FormLink,
|
||||
FormGroup,
|
||||
FormKeyValueView,
|
||||
},
|
||||
|
||||
emits: ['info'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: this.$ts.registry,
|
||||
icon: 'fas fa-cogs',
|
||||
bg: 'var(--bg)',
|
||||
},
|
||||
scopes: null,
|
||||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
this.fetch();
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.$emit('info', this[symbols.PAGE_INFO]);
|
||||
},
|
||||
|
||||
methods: {
|
||||
fetch() {
|
||||
os.api('i/registry/scopes').then(scopes => {
|
||||
this.scopes = scopes.slice().sort((a, b) => a.join('/').localeCompare(b.join('/')));
|
||||
});
|
||||
},
|
||||
|
||||
async createKey() {
|
||||
const { canceled, result } = await os.form(this.$ts._registry.createKey, {
|
||||
key: {
|
||||
type: 'string',
|
||||
label: this.$ts._registry.key,
|
||||
},
|
||||
value: {
|
||||
type: 'string',
|
||||
multiline: true,
|
||||
label: this.$ts.value,
|
||||
},
|
||||
scope: {
|
||||
type: 'string',
|
||||
label: this.$ts._registry.scope,
|
||||
}
|
||||
});
|
||||
if (canceled) return;
|
||||
os.apiWithDialog('i/registry/set', {
|
||||
scope: result.scope.split('/'),
|
||||
key: result.key,
|
||||
value: JSON5.parse(result.value),
|
||||
}).then(() => {
|
||||
this.fetch();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
158
packages/client/src/pages/settings/security.vue
Normal file
158
packages/client/src/pages/settings/security.vue
Normal file
@ -0,0 +1,158 @@
|
||||
<template>
|
||||
<FormBase>
|
||||
<X2fa/>
|
||||
<FormLink to="/settings/2fa"><template #icon><i class="fas fa-mobile-alt"></i></template>{{ $ts.twoStepAuthentication }}</FormLink>
|
||||
<FormButton primary @click="change()">{{ $ts.changePassword }}</FormButton>
|
||||
<FormPagination :pagination="pagination">
|
||||
<template #label>{{ $ts.signinHistory }}</template>
|
||||
<template #default="{items}">
|
||||
<div class="_debobigegoPanel timnmucd" v-for="item in items" :key="item.id">
|
||||
<header>
|
||||
<i v-if="item.success" class="fas fa-check icon succ"></i>
|
||||
<i v-else class="fas fa-times-circle icon fail"></i>
|
||||
<code class="ip _monospace">{{ item.ip }}</code>
|
||||
<MkTime :time="item.createdAt" class="time"/>
|
||||
</header>
|
||||
</div>
|
||||
</template>
|
||||
</FormPagination>
|
||||
<FormGroup>
|
||||
<FormButton danger @click="regenerateToken"><i class="fas fa-sync-alt"></i> {{ $ts.regenerateLoginToken }}</FormButton>
|
||||
<template #caption>{{ $ts.regenerateLoginTokenDescription }}</template>
|
||||
</FormGroup>
|
||||
</FormBase>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import FormBase from '@/components/debobigego/base.vue';
|
||||
import FormLink from '@/components/debobigego/link.vue';
|
||||
import FormGroup from '@/components/debobigego/group.vue';
|
||||
import FormButton from '@/components/debobigego/button.vue';
|
||||
import FormPagination from '@/components/debobigego/pagination.vue';
|
||||
import * as os from '@/os';
|
||||
import * as symbols from '@/symbols';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
FormBase,
|
||||
FormLink,
|
||||
FormButton,
|
||||
FormPagination,
|
||||
FormGroup,
|
||||
},
|
||||
|
||||
emits: ['info'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: this.$ts.security,
|
||||
icon: 'fas fa-lock',
|
||||
bg: 'var(--bg)',
|
||||
},
|
||||
pagination: {
|
||||
endpoint: 'i/signin-history',
|
||||
limit: 5,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.$emit('info', this[symbols.PAGE_INFO]);
|
||||
},
|
||||
|
||||
methods: {
|
||||
async change() {
|
||||
const { canceled: canceled1, result: currentPassword } = await os.dialog({
|
||||
title: this.$ts.currentPassword,
|
||||
input: {
|
||||
type: 'password'
|
||||
}
|
||||
});
|
||||
if (canceled1) return;
|
||||
|
||||
const { canceled: canceled2, result: newPassword } = await os.dialog({
|
||||
title: this.$ts.newPassword,
|
||||
input: {
|
||||
type: 'password'
|
||||
}
|
||||
});
|
||||
if (canceled2) return;
|
||||
|
||||
const { canceled: canceled3, result: newPassword2 } = await os.dialog({
|
||||
title: this.$ts.newPasswordRetype,
|
||||
input: {
|
||||
type: 'password'
|
||||
}
|
||||
});
|
||||
if (canceled3) return;
|
||||
|
||||
if (newPassword !== newPassword2) {
|
||||
os.dialog({
|
||||
type: 'error',
|
||||
text: this.$ts.retypedNotMatch
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
os.apiWithDialog('i/change-password', {
|
||||
currentPassword,
|
||||
newPassword
|
||||
});
|
||||
},
|
||||
|
||||
regenerateToken() {
|
||||
os.dialog({
|
||||
title: this.$ts.password,
|
||||
input: {
|
||||
type: 'password'
|
||||
}
|
||||
}).then(({ canceled, result: password }) => {
|
||||
if (canceled) return;
|
||||
os.api('i/regenerate_token', {
|
||||
password: password
|
||||
});
|
||||
});
|
||||
},
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.timnmucd {
|
||||
padding: 16px;
|
||||
|
||||
> header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
> .icon {
|
||||
width: 1em;
|
||||
margin-right: 0.75em;
|
||||
|
||||
&.succ {
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
&.fail {
|
||||
color: var(--error);
|
||||
}
|
||||
}
|
||||
|
||||
> .ip {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
> .time {
|
||||
margin-left: auto;
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
155
packages/client/src/pages/settings/sounds.vue
Normal file
155
packages/client/src/pages/settings/sounds.vue
Normal file
@ -0,0 +1,155 @@
|
||||
<template>
|
||||
<FormBase>
|
||||
<FormRange v-model="masterVolume" :min="0" :max="1" :step="0.05">
|
||||
<template #label><i class="fas fa-volume-icon"></i> {{ $ts.masterVolume }}</template>
|
||||
</FormRange>
|
||||
|
||||
<FormGroup>
|
||||
<template #label>{{ $ts.sounds }}</template>
|
||||
<FormButton v-for="type in Object.keys(sounds)" :key="type" :center="false" @click="edit(type)">
|
||||
{{ $t('_sfx.' + type) }}
|
||||
<template #suffix>{{ sounds[type].type || $ts.none }}</template>
|
||||
<template #suffixIcon><i class="fas fa-chevron-down"></i></template>
|
||||
</FormButton>
|
||||
</FormGroup>
|
||||
|
||||
<FormButton @click="reset()" danger><i class="fas fa-redo"></i> {{ $ts.default }}</FormButton>
|
||||
</FormBase>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import FormRange from '@/components/debobigego/range.vue';
|
||||
import FormSelect from '@/components/debobigego/select.vue';
|
||||
import FormBase from '@/components/debobigego/base.vue';
|
||||
import FormButton from '@/components/debobigego/button.vue';
|
||||
import FormGroup from '@/components/debobigego/group.vue';
|
||||
import * as os from '@/os';
|
||||
import { ColdDeviceStorage } from '@/store';
|
||||
import { playFile } from '@/scripts/sound';
|
||||
import * as symbols from '@/symbols';
|
||||
|
||||
const soundsTypes = [
|
||||
null,
|
||||
'syuilo/up',
|
||||
'syuilo/down',
|
||||
'syuilo/pope1',
|
||||
'syuilo/pope2',
|
||||
'syuilo/waon',
|
||||
'syuilo/popo',
|
||||
'syuilo/triple',
|
||||
'syuilo/poi1',
|
||||
'syuilo/poi2',
|
||||
'syuilo/pirori',
|
||||
'syuilo/pirori-wet',
|
||||
'syuilo/pirori-square-wet',
|
||||
'syuilo/square-pico',
|
||||
'syuilo/reverved',
|
||||
'syuilo/ryukyu',
|
||||
'syuilo/kick',
|
||||
'syuilo/snare',
|
||||
'syuilo/queue-jammed',
|
||||
'aisha/1',
|
||||
'aisha/2',
|
||||
'aisha/3',
|
||||
'noizenecio/kick_gaba',
|
||||
'noizenecio/kick_gaba2',
|
||||
];
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
FormSelect,
|
||||
FormButton,
|
||||
FormBase,
|
||||
FormRange,
|
||||
FormGroup,
|
||||
},
|
||||
|
||||
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); }
|
||||
},
|
||||
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');
|
||||
this.sounds.reversiPutBlack = ColdDeviceStorage.get('sound_reversiPutBlack');
|
||||
this.sounds.reversiPutWhite = ColdDeviceStorage.get('sound_reversiPutWhite');
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.$emit('info', this[symbols.PAGE_INFO]);
|
||||
},
|
||||
|
||||
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,
|
||||
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;
|
||||
},
|
||||
|
||||
reset() {
|
||||
for (const sound of Object.keys(this.sounds)) {
|
||||
const v = ColdDeviceStorage.default['sound_' + sound];
|
||||
ColdDeviceStorage.set('sound_' + sound, v);
|
||||
this.sounds[sound] = v;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
105
packages/client/src/pages/settings/theme.install.vue
Normal file
105
packages/client/src/pages/settings/theme.install.vue
Normal file
@ -0,0 +1,105 @@
|
||||
<template>
|
||||
<FormBase>
|
||||
<FormGroup>
|
||||
<FormTextarea v-model="installThemeCode">
|
||||
<span>{{ $ts._theme.code }}</span>
|
||||
</FormTextarea>
|
||||
<FormButton @click="() => preview(installThemeCode)" :disabled="installThemeCode == null" inline><i class="fas fa-eye"></i> {{ $ts.preview }}</FormButton>
|
||||
</FormGroup>
|
||||
|
||||
<FormButton @click="() => install(installThemeCode)" :disabled="installThemeCode == null" primary inline><i class="fas fa-check"></i> {{ $ts.install }}</FormButton>
|
||||
</FormBase>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import * as JSON5 from 'json5';
|
||||
import FormTextarea from '@/components/form/textarea.vue';
|
||||
import FormSelect from '@/components/form/select.vue';
|
||||
import FormRadios from '@/components/form/radios.vue';
|
||||
import FormBase from '@/components/debobigego/base.vue';
|
||||
import FormGroup from '@/components/debobigego/group.vue';
|
||||
import FormLink from '@/components/debobigego/link.vue';
|
||||
import FormButton from '@/components/debobigego/button.vue';
|
||||
import { applyTheme, validateTheme } from '@/scripts/theme';
|
||||
import * as os from '@/os';
|
||||
import { ColdDeviceStorage } from '@/store';
|
||||
import { addTheme, getThemes } from '@/theme-store';
|
||||
import * as symbols from '@/symbols';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
FormTextarea,
|
||||
FormSelect,
|
||||
FormRadios,
|
||||
FormBase,
|
||||
FormGroup,
|
||||
FormLink,
|
||||
FormButton,
|
||||
},
|
||||
|
||||
emits: ['info'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: this.$ts._theme.install,
|
||||
icon: 'fas fa-download',
|
||||
bg: 'var(--bg)',
|
||||
},
|
||||
installThemeCode: null,
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.$emit('info', this[symbols.PAGE_INFO]);
|
||||
},
|
||||
|
||||
methods: {
|
||||
parseThemeCode(code) {
|
||||
let theme;
|
||||
|
||||
try {
|
||||
theme = JSON5.parse(code);
|
||||
} catch (e) {
|
||||
os.dialog({
|
||||
type: 'error',
|
||||
text: this.$ts._theme.invalid
|
||||
});
|
||||
return false;
|
||||
}
|
||||
if (!validateTheme(theme)) {
|
||||
os.dialog({
|
||||
type: 'error',
|
||||
text: this.$ts._theme.invalid
|
||||
});
|
||||
return false;
|
||||
}
|
||||
if (getThemes().some(t => t.id === theme.id)) {
|
||||
os.dialog({
|
||||
type: 'info',
|
||||
text: this.$ts._theme.alreadyInstalled
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
return theme;
|
||||
},
|
||||
|
||||
preview(code) {
|
||||
const theme = this.parseThemeCode(code);
|
||||
if (theme) applyTheme(theme, false);
|
||||
},
|
||||
|
||||
async install(code) {
|
||||
const theme = this.parseThemeCode(code);
|
||||
if (!theme) return;
|
||||
await addTheme(theme);
|
||||
os.dialog({
|
||||
type: 'success',
|
||||
text: this.$t('_theme.installed', { name: theme.name })
|
||||
});
|
||||
},
|
||||
}
|
||||
});
|
||||
</script>
|
105
packages/client/src/pages/settings/theme.manage.vue
Normal file
105
packages/client/src/pages/settings/theme.manage.vue
Normal file
@ -0,0 +1,105 @@
|
||||
<template>
|
||||
<FormBase>
|
||||
<FormSelect v-model="selectedThemeId">
|
||||
<template #label>{{ $ts.theme }}</template>
|
||||
<optgroup :label="$ts._theme.installedThemes">
|
||||
<option v-for="x in installedThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
|
||||
</optgroup>
|
||||
<optgroup :label="$ts._theme.builtinThemes">
|
||||
<option v-for="x in builtinThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
|
||||
</optgroup>
|
||||
</FormSelect>
|
||||
<template v-if="selectedTheme">
|
||||
<FormInput readonly :modelValue="selectedTheme.author">
|
||||
<span>{{ $ts.author }}</span>
|
||||
</FormInput>
|
||||
<FormTextarea readonly :modelValue="selectedTheme.desc" v-if="selectedTheme.desc">
|
||||
<span>{{ $ts._theme.description }}</span>
|
||||
</FormTextarea>
|
||||
<FormTextarea readonly tall :modelValue="selectedThemeCode">
|
||||
<span>{{ $ts._theme.code }}</span>
|
||||
<template #desc><button @click="copyThemeCode()" class="_textButton">{{ $ts.copy }}</button></template>
|
||||
</FormTextarea>
|
||||
<FormButton @click="uninstall()" danger v-if="!builtinThemes.some(t => t.id == selectedTheme.id)"><i class="fas fa-trash-alt"></i> {{ $ts.uninstall }}</FormButton>
|
||||
</template>
|
||||
</FormBase>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import * as JSON5 from 'json5';
|
||||
import FormTextarea from '@/components/debobigego/textarea.vue';
|
||||
import FormSelect from '@/components/debobigego/select.vue';
|
||||
import FormRadios from '@/components/debobigego/radios.vue';
|
||||
import FormBase from '@/components/debobigego/base.vue';
|
||||
import FormGroup from '@/components/debobigego/group.vue';
|
||||
import FormInput from '@/components/debobigego/input.vue';
|
||||
import FormButton from '@/components/debobigego/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';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
FormTextarea,
|
||||
FormSelect,
|
||||
FormRadios,
|
||||
FormBase,
|
||||
FormGroup,
|
||||
FormInput,
|
||||
FormButton,
|
||||
},
|
||||
|
||||
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,
|
||||
}
|
||||
},
|
||||
|
||||
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);
|
||||
},
|
||||
|
||||
selectedThemeCode() {
|
||||
if (this.selectedTheme == null) return null;
|
||||
return JSON5.stringify(this.selectedTheme, null, '\t');
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.$emit('info', this[symbols.PAGE_INFO]);
|
||||
},
|
||||
|
||||
methods: {
|
||||
copyThemeCode() {
|
||||
copyToClipboard(this.selectedThemeCode);
|
||||
os.success();
|
||||
},
|
||||
|
||||
uninstall() {
|
||||
removeTheme(this.selectedTheme);
|
||||
this.installedThemes = this.installedThemes.filter(t => t.id !== this.selectedThemeId);
|
||||
this.selectedThemeId = null;
|
||||
os.success();
|
||||
},
|
||||
}
|
||||
});
|
||||
</script>
|
424
packages/client/src/pages/settings/theme.vue
Normal file
424
packages/client/src/pages/settings/theme.vue
Normal file
@ -0,0 +1,424 @@
|
||||
<template>
|
||||
<FormBase>
|
||||
<FormGroup>
|
||||
<div class="rfqxtzch _debobigegoItem _debobigegoPanel">
|
||||
<div class="darkMode">
|
||||
<div class="toggleWrapper">
|
||||
<input type="checkbox" class="dn" id="dn" v-model="darkMode"/>
|
||||
<label for="dn" class="toggle">
|
||||
<span class="before">{{ $ts.light }}</span>
|
||||
<span class="after">{{ $ts.dark }}</span>
|
||||
<span class="toggle__handler">
|
||||
<span class="crater crater--1"></span>
|
||||
<span class="crater crater--2"></span>
|
||||
<span class="crater crater--3"></span>
|
||||
</span>
|
||||
<span class="star star--1"></span>
|
||||
<span class="star star--2"></span>
|
||||
<span class="star star--3"></span>
|
||||
<span class="star star--4"></span>
|
||||
<span class="star star--5"></span>
|
||||
<span class="star star--6"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<FormSwitch v-model="syncDeviceDarkMode">{{ $ts.syncDeviceDarkMode }}</FormSwitch>
|
||||
</FormGroup>
|
||||
|
||||
<template v-if="darkMode">
|
||||
<FormSelect v-model="darkThemeId">
|
||||
<template #label>{{ $ts.themeForDarkMode }}</template>
|
||||
<optgroup :label="$ts.darkThemes">
|
||||
<option v-for="x in darkThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
|
||||
</optgroup>
|
||||
<optgroup :label="$ts.lightThemes">
|
||||
<option v-for="x in lightThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
|
||||
</optgroup>
|
||||
</FormSelect>
|
||||
<FormSelect v-model="lightThemeId">
|
||||
<template #label>{{ $ts.themeForLightMode }}</template>
|
||||
<optgroup :label="$ts.lightThemes">
|
||||
<option v-for="x in lightThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
|
||||
</optgroup>
|
||||
<optgroup :label="$ts.darkThemes">
|
||||
<option v-for="x in darkThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
|
||||
</optgroup>
|
||||
</FormSelect>
|
||||
</template>
|
||||
<template v-else>
|
||||
<FormSelect v-model="lightThemeId">
|
||||
<template #label>{{ $ts.themeForLightMode }}</template>
|
||||
<optgroup :label="$ts.lightThemes">
|
||||
<option v-for="x in lightThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
|
||||
</optgroup>
|
||||
<optgroup :label="$ts.darkThemes">
|
||||
<option v-for="x in darkThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
|
||||
</optgroup>
|
||||
</FormSelect>
|
||||
<FormSelect v-model="darkThemeId">
|
||||
<template #label>{{ $ts.themeForDarkMode }}</template>
|
||||
<optgroup :label="$ts.darkThemes">
|
||||
<option v-for="x in darkThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
|
||||
</optgroup>
|
||||
<optgroup :label="$ts.lightThemes">
|
||||
<option v-for="x in lightThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
|
||||
</optgroup>
|
||||
</FormSelect>
|
||||
</template>
|
||||
|
||||
<FormButton primary v-if="wallpaper == null" @click="setWallpaper">{{ $ts.setWallpaper }}</FormButton>
|
||||
<FormButton primary v-else @click="wallpaper = null">{{ $ts.removeWallpaper }}</FormButton>
|
||||
|
||||
<FormGroup>
|
||||
<FormLink to="https://assets.misskey.io/theme/list" external><template #icon><i class="fas fa-globe"></i></template>{{ $ts._theme.explore }}</FormLink>
|
||||
<FormLink to="/settings/theme/install"><template #icon><i class="fas fa-download"></i></template>{{ $ts._theme.install }}</FormLink>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLink to="/theme-editor"><template #icon><i class="fas fa-paint-roller"></i></template>{{ $ts._theme.make }}</FormLink>
|
||||
<!--<FormLink to="/advanced-theme-editor"><template #icon><i class="fas fa-paint-roller"></i></template>{{ $ts._theme.make }} ({{ $ts.advanced }})</FormLink>-->
|
||||
</FormGroup>
|
||||
|
||||
<FormLink to="/settings/theme/manage"><template #icon><i class="fas fa-folder-open"></i></template>{{ $ts._theme.manage }}<template #suffix>{{ themesCount }}</template></FormLink>
|
||||
</FormBase>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, onActivated, onMounted, ref, watch } from 'vue';
|
||||
import FormSwitch from '@/components/debobigego/switch.vue';
|
||||
import FormSelect from '@/components/debobigego/select.vue';
|
||||
import FormBase from '@/components/debobigego/base.vue';
|
||||
import FormGroup from '@/components/debobigego/group.vue';
|
||||
import FormLink from '@/components/debobigego/link.vue';
|
||||
import FormButton from '@/components/debobigego/button.vue';
|
||||
import { builtinThemes } from '@/scripts/theme';
|
||||
import { selectFile } from '@/scripts/select-file';
|
||||
import { isDeviceDarkmode } from '@/scripts/is-device-darkmode';
|
||||
import { ColdDeviceStorage } from '@/store';
|
||||
import { i18n } from '@/i18n';
|
||||
import { defaultStore } from '@/store';
|
||||
import { fetchThemes, getThemes } from '@/theme-store';
|
||||
import * as symbols from '@/symbols';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
FormSwitch,
|
||||
FormSelect,
|
||||
FormBase,
|
||||
FormGroup,
|
||||
FormLink,
|
||||
FormButton,
|
||||
},
|
||||
|
||||
emits: ['info'],
|
||||
|
||||
setup(props, { emit }) {
|
||||
const INFO = {
|
||||
title: i18n.locale.theme,
|
||||
icon: 'fas fa-palette',
|
||||
bg: 'var(--bg)',
|
||||
};
|
||||
|
||||
const installedThemes = ref(getThemes());
|
||||
const themes = computed(() => builtinThemes.concat(installedThemes.value));
|
||||
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;
|
||||
|
||||
watch(syncDeviceDarkMode, () => {
|
||||
if (syncDeviceDarkMode) {
|
||||
defaultStore.set('darkMode', isDeviceDarkmode());
|
||||
}
|
||||
});
|
||||
|
||||
watch(wallpaper, () => {
|
||||
if (wallpaper.value == null) {
|
||||
localStorage.removeItem('wallpaper');
|
||||
} else {
|
||||
localStorage.setItem('wallpaper', wallpaper.value);
|
||||
}
|
||||
location.reload();
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
emit('info', INFO);
|
||||
});
|
||||
|
||||
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, false).then(file => {
|
||||
wallpaper.value = file.url;
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.rfqxtzch {
|
||||
padding: 16px;
|
||||
|
||||
> .darkMode {
|
||||
position: relative;
|
||||
padding: 32px 0;
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.7;
|
||||
|
||||
&, * {
|
||||
cursor: not-allowed !important;
|
||||
}
|
||||
}
|
||||
|
||||
.toggleWrapper {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
overflow: hidden;
|
||||
padding: 0 100px;
|
||||
transform: translate3d(-50%, -50%, 0);
|
||||
|
||||
input {
|
||||
position: absolute;
|
||||
left: -99em;
|
||||
}
|
||||
}
|
||||
|
||||
.toggle {
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: 90px;
|
||||
height: 50px;
|
||||
background-color: #83D8FF;
|
||||
border-radius: 90px - 6;
|
||||
transition: background-color 200ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important;
|
||||
|
||||
> .before, > .after {
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
font-size: 18px;
|
||||
transition: color 1s ease;
|
||||
}
|
||||
|
||||
> .before {
|
||||
left: -70px;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
> .after {
|
||||
right: -68px;
|
||||
color: var(--fg);
|
||||
}
|
||||
}
|
||||
|
||||
.toggle__handler {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
top: 3px;
|
||||
left: 3px;
|
||||
width: 50px - 6;
|
||||
height: 50px - 6;
|
||||
background-color: #FFCF96;
|
||||
border-radius: 50px;
|
||||
box-shadow: 0 2px 6px rgba(0,0,0,.3);
|
||||
transition: all 400ms cubic-bezier(0.68, -0.55, 0.265, 1.55) !important;
|
||||
transform: rotate(-45deg);
|
||||
|
||||
.crater {
|
||||
position: absolute;
|
||||
background-color: #E8CDA5;
|
||||
opacity: 0;
|
||||
transition: opacity 200ms ease-in-out !important;
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
.crater--1 {
|
||||
top: 18px;
|
||||
left: 10px;
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
}
|
||||
|
||||
.crater--2 {
|
||||
top: 28px;
|
||||
left: 22px;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
.crater--3 {
|
||||
top: 10px;
|
||||
left: 25px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.star {
|
||||
position: absolute;
|
||||
background-color: #ffffff;
|
||||
transition: all 300ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.star--1 {
|
||||
top: 10px;
|
||||
left: 35px;
|
||||
z-index: 0;
|
||||
width: 30px;
|
||||
height: 3px;
|
||||
}
|
||||
|
||||
.star--2 {
|
||||
top: 18px;
|
||||
left: 28px;
|
||||
z-index: 1;
|
||||
width: 30px;
|
||||
height: 3px;
|
||||
}
|
||||
|
||||
.star--3 {
|
||||
top: 27px;
|
||||
left: 40px;
|
||||
z-index: 0;
|
||||
width: 30px;
|
||||
height: 3px;
|
||||
}
|
||||
|
||||
.star--4,
|
||||
.star--5,
|
||||
.star--6 {
|
||||
opacity: 0;
|
||||
transition: all 300ms 0 cubic-bezier(0.445, 0.05, 0.55, 0.95) !important;
|
||||
}
|
||||
|
||||
.star--4 {
|
||||
top: 16px;
|
||||
left: 11px;
|
||||
z-index: 0;
|
||||
width: 2px;
|
||||
height: 2px;
|
||||
transform: translate3d(3px,0,0);
|
||||
}
|
||||
|
||||
.star--5 {
|
||||
top: 32px;
|
||||
left: 17px;
|
||||
z-index: 0;
|
||||
width: 3px;
|
||||
height: 3px;
|
||||
transform: translate3d(3px,0,0);
|
||||
}
|
||||
|
||||
.star--6 {
|
||||
top: 36px;
|
||||
left: 28px;
|
||||
z-index: 0;
|
||||
width: 2px;
|
||||
height: 2px;
|
||||
transform: translate3d(3px,0,0);
|
||||
}
|
||||
|
||||
input:checked {
|
||||
+ .toggle {
|
||||
background-color: #749DD6;
|
||||
|
||||
> .before {
|
||||
color: var(--fg);
|
||||
}
|
||||
|
||||
> .after {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.toggle__handler {
|
||||
background-color: #FFE5B5;
|
||||
transform: translate3d(40px, 0, 0) rotate(0);
|
||||
|
||||
.crater { opacity: 1; }
|
||||
}
|
||||
|
||||
.star--1 {
|
||||
width: 2px;
|
||||
height: 2px;
|
||||
}
|
||||
|
||||
.star--2 {
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
transform: translate3d(-5px, 0, 0);
|
||||
}
|
||||
|
||||
.star--3 {
|
||||
width: 2px;
|
||||
height: 2px;
|
||||
transform: translate3d(-7px, 0, 0);
|
||||
}
|
||||
|
||||
.star--4,
|
||||
.star--5,
|
||||
.star--6 {
|
||||
opacity: 1;
|
||||
transform: translate3d(0,0,0);
|
||||
}
|
||||
|
||||
.star--4 {
|
||||
transition: all 300ms 200ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important;
|
||||
}
|
||||
|
||||
.star--5 {
|
||||
transition: all 300ms 300ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important;
|
||||
}
|
||||
|
||||
.star--6 {
|
||||
transition: all 300ms 400ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
95
packages/client/src/pages/settings/update.vue
Normal file
95
packages/client/src/pages/settings/update.vue
Normal file
@ -0,0 +1,95 @@
|
||||
<template>
|
||||
<FormBase>
|
||||
<template v-if="meta">
|
||||
<FormInfo v-if="version === meta.version">{{ $ts.youAreRunningUpToDateClient }}</FormInfo>
|
||||
<FormInfo v-else warn>{{ $ts.newVersionOfClientAvailable }}</FormInfo>
|
||||
</template>
|
||||
<FormGroup>
|
||||
<template #label>{{ instanceName }}</template>
|
||||
<FormKeyValueView>
|
||||
<template #key>{{ $ts.currentVersion }}</template>
|
||||
<template #value>{{ version }}</template>
|
||||
</FormKeyValueView>
|
||||
<FormKeyValueView>
|
||||
<template #key>{{ $ts.latestVersion }}</template>
|
||||
<template #value v-if="meta">{{ meta.version }}</template>
|
||||
<template #value v-else><MkEllipsis/></template>
|
||||
</FormKeyValueView>
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<template #label>Misskey</template>
|
||||
<FormKeyValueView>
|
||||
<template #key>{{ $ts.latestVersion }}</template>
|
||||
<template #value v-if="releases">{{ releases[0].tag_name }}</template>
|
||||
<template #value v-else><MkEllipsis/></template>
|
||||
</FormKeyValueView>
|
||||
<template #caption v-if="releases"><MkTime :time="releases[0].published_at" mode="detail"/></template>
|
||||
</FormGroup>
|
||||
</FormBase>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineAsyncComponent, defineComponent } from 'vue';
|
||||
import FormSwitch from '@/components/form/switch.vue';
|
||||
import FormSelect from '@/components/form/select.vue';
|
||||
import FormLink from '@/components/debobigego/link.vue';
|
||||
import FormBase from '@/components/debobigego/base.vue';
|
||||
import FormGroup from '@/components/debobigego/group.vue';
|
||||
import FormButton from '@/components/debobigego/button.vue';
|
||||
import FormKeyValueView from '@/components/debobigego/key-value-view.vue';
|
||||
import FormInfo from '@/components/debobigego/info.vue';
|
||||
import * as os from '@/os';
|
||||
import { version, instanceName } from '@/config';
|
||||
import * as symbols from '@/symbols';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
FormBase,
|
||||
FormSelect,
|
||||
FormSwitch,
|
||||
FormButton,
|
||||
FormLink,
|
||||
FormGroup,
|
||||
FormKeyValueView,
|
||||
FormInfo,
|
||||
},
|
||||
|
||||
emits: ['info'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: 'Misskey Update',
|
||||
icon: 'fas fa-sync-alt',
|
||||
bg: 'var(--bg)',
|
||||
},
|
||||
version,
|
||||
instanceName,
|
||||
releases: null,
|
||||
meta: null
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.$emit('info', this[symbols.PAGE_INFO]);
|
||||
|
||||
os.api('meta', {
|
||||
detail: false
|
||||
}).then(meta => {
|
||||
this.meta = meta;
|
||||
localStorage.setItem('v', meta.version);
|
||||
});
|
||||
|
||||
fetch('https://api.github.com/repos/misskey-dev/misskey/releases', {
|
||||
method: 'GET',
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(res => {
|
||||
this.releases = res;
|
||||
});
|
||||
},
|
||||
|
||||
methods: {
|
||||
}
|
||||
});
|
||||
</script>
|
110
packages/client/src/pages/settings/word-mute.vue
Normal file
110
packages/client/src/pages/settings/word-mute.vue
Normal file
@ -0,0 +1,110 @@
|
||||
<template>
|
||||
<div>
|
||||
<MkTab v-model="tab">
|
||||
<option value="soft">{{ $ts._wordMute.soft }}</option>
|
||||
<option value="hard">{{ $ts._wordMute.hard }}</option>
|
||||
</MkTab>
|
||||
<FormBase>
|
||||
<div class="_debobigegoItem">
|
||||
<div v-show="tab === 'soft'">
|
||||
<FormInfo>{{ $ts._wordMute.softDescription }}</FormInfo>
|
||||
<FormTextarea v-model="softMutedWords">
|
||||
<span>{{ $ts._wordMute.muteWords }}</span>
|
||||
<template #desc>{{ $ts._wordMute.muteWordsDescription }}<br>{{ $ts._wordMute.muteWordsDescription2 }}</template>
|
||||
</FormTextarea>
|
||||
</div>
|
||||
<div v-show="tab === 'hard'">
|
||||
<FormInfo>{{ $ts._wordMute.hardDescription }}</FormInfo>
|
||||
<FormTextarea v-model="hardMutedWords">
|
||||
<span>{{ $ts._wordMute.muteWords }}</span>
|
||||
<template #desc>{{ $ts._wordMute.muteWordsDescription }}<br>{{ $ts._wordMute.muteWordsDescription2 }}</template>
|
||||
</FormTextarea>
|
||||
<FormKeyValueView v-if="hardWordMutedNotesCount != null">
|
||||
<template #key>{{ $ts._wordMute.mutedNotes }}</template>
|
||||
<template #value>{{ number(hardWordMutedNotesCount) }}</template>
|
||||
</FormKeyValueView>
|
||||
</div>
|
||||
</div>
|
||||
<FormButton @click="save()" primary inline :disabled="!changed"><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
|
||||
</FormBase>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import FormTextarea from '@/components/form/textarea.vue';
|
||||
import FormBase from '@/components/debobigego/base.vue';
|
||||
import FormKeyValueView from '@/components/debobigego/key-value-view.vue';
|
||||
import FormButton from '@/components/debobigego/button.vue';
|
||||
import FormInfo from '@/components/debobigego/info.vue';
|
||||
import MkTab from '@/components/tab.vue';
|
||||
import * as os from '@/os';
|
||||
import number from '@/filters/number';
|
||||
import * as symbols from '@/symbols';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
FormBase,
|
||||
FormButton,
|
||||
FormTextarea,
|
||||
FormKeyValueView,
|
||||
MkTab,
|
||||
FormInfo,
|
||||
},
|
||||
|
||||
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,
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
softMutedWords: {
|
||||
handler() {
|
||||
this.changed = true;
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
hardMutedWords: {
|
||||
handler() {
|
||||
this.changed = true;
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
},
|
||||
|
||||
async created() {
|
||||
this.softMutedWords = this.$store.state.mutedWords.map(x => x.join(' ')).join('\n');
|
||||
this.hardMutedWords = this.$i.mutedWords.map(x => x.join(' ')).join('\n');
|
||||
|
||||
this.hardWordMutedNotesCount = (await os.api('i/get-word-muted-notes-count', {})).count;
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.$emit('info', this[symbols.PAGE_INFO]);
|
||||
},
|
||||
|
||||
methods: {
|
||||
async save() {
|
||||
this.$store.set('mutedWords', this.softMutedWords.trim().split('\n').map(x => x.trim().split(' ')));
|
||||
await os.api('i/update', {
|
||||
mutedWords: this.hardMutedWords.trim().split('\n').map(x => x.trim().split(' ')),
|
||||
});
|
||||
this.changed = false;
|
||||
},
|
||||
|
||||
number
|
||||
}
|
||||
});
|
||||
</script>
|
Reference in New Issue
Block a user