refactoring

Resolve #7779
This commit is contained in:
syuilo
2021-11-12 02:02:25 +09:00
parent 037837b551
commit 0e4a111f81
1714 changed files with 20803 additions and 11751 deletions

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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