Merge branch 'develop' into sw-notification-action
This commit is contained in:
commit
524c213ba2
59 changed files with 2755 additions and 1892 deletions
|
@ -138,7 +138,7 @@ flagAsBotDescription: "このアカウントがプログラムによって運用
|
||||||
flagAsCat: "Catとして設定"
|
flagAsCat: "Catとして設定"
|
||||||
flagAsCatDescription: "このアカウントが猫であることを示す場合は、このフラグをオンにします。"
|
flagAsCatDescription: "このアカウントが猫であることを示す場合は、このフラグをオンにします。"
|
||||||
autoAcceptFollowed: "フォロー中ユーザーからのフォロリクを自動承認"
|
autoAcceptFollowed: "フォロー中ユーザーからのフォロリクを自動承認"
|
||||||
addAcount: "アカウント追加"
|
addAccount: "アカウントを追加"
|
||||||
loginFailed: "ログインに失敗しました"
|
loginFailed: "ログインに失敗しました"
|
||||||
showOnRemote: "リモートで表示"
|
showOnRemote: "リモートで表示"
|
||||||
general: "全般"
|
general: "全般"
|
||||||
|
@ -183,7 +183,7 @@ clearQueueConfirmTitle: "キューをクリアしますか?"
|
||||||
clearQueueConfirmText: "未配達の投稿は配送されなくなります。通常この操作を行う必要はありません。"
|
clearQueueConfirmText: "未配達の投稿は配送されなくなります。通常この操作を行う必要はありません。"
|
||||||
clearCachedFiles: "キャッシュをクリア"
|
clearCachedFiles: "キャッシュをクリア"
|
||||||
clearCachedFilesConfirm: "キャッシュされたリモートファイルをすべて削除しますか?"
|
clearCachedFilesConfirm: "キャッシュされたリモートファイルをすべて削除しますか?"
|
||||||
blockedInstances: "インスタンスブロック"
|
blockedInstances: "ブロックしたインスタンス"
|
||||||
blockedInstancesDescription: "ブロックしたいインスタンスのホストを改行で区切って設定します。ブロックされたインスタンスは、このインスタンスとやり取りできなくなります。"
|
blockedInstancesDescription: "ブロックしたいインスタンスのホストを改行で区切って設定します。ブロックされたインスタンスは、このインスタンスとやり取りできなくなります。"
|
||||||
muteAndBlock: "ミュートとブロック"
|
muteAndBlock: "ミュートとブロック"
|
||||||
mutedUsers: "ミュートしたユーザー"
|
mutedUsers: "ミュートしたユーザー"
|
||||||
|
@ -349,7 +349,6 @@ antennaExcludeKeywords: "除外キーワード"
|
||||||
antennaKeywordsDescription: "スペースで区切るとAND指定になり、改行で区切るとOR指定になります"
|
antennaKeywordsDescription: "スペースで区切るとAND指定になり、改行で区切るとOR指定になります"
|
||||||
notifyAntenna: "新しいノートを通知する"
|
notifyAntenna: "新しいノートを通知する"
|
||||||
withFileAntenna: "ファイルが添付されたノートのみ"
|
withFileAntenna: "ファイルが添付されたノートのみ"
|
||||||
serviceworker: "ServiceWorker"
|
|
||||||
enableServiceworker: "ServiceWorkerを有効にする"
|
enableServiceworker: "ServiceWorkerを有効にする"
|
||||||
antennaUsersDescription: "ユーザー名を改行で区切って指定します"
|
antennaUsersDescription: "ユーザー名を改行で区切って指定します"
|
||||||
caseSensitive: "大文字小文字を区別する"
|
caseSensitive: "大文字小文字を区別する"
|
||||||
|
@ -453,7 +452,7 @@ category: "カテゴリ"
|
||||||
tags: "タグ"
|
tags: "タグ"
|
||||||
docSource: "このドキュメントのソース"
|
docSource: "このドキュメントのソース"
|
||||||
createAccount: "アカウントを作成"
|
createAccount: "アカウントを作成"
|
||||||
existingAcount: "既存のアカウント"
|
existingAccount: "既存のアカウント"
|
||||||
regenerate: "再生成"
|
regenerate: "再生成"
|
||||||
fontSize: "フォントサイズ"
|
fontSize: "フォントサイズ"
|
||||||
noFollowRequests: "フォロー申請はありません"
|
noFollowRequests: "フォロー申請はありません"
|
||||||
|
@ -568,7 +567,7 @@ pluginTokenRequestedDescription: "このプラグインはここで設定した
|
||||||
notificationType: "通知の種類"
|
notificationType: "通知の種類"
|
||||||
edit: "編集"
|
edit: "編集"
|
||||||
useStarForReactionFallback: "リアクション絵文字が不明な場合、代わりに★を使う"
|
useStarForReactionFallback: "リアクション絵文字が不明な場合、代わりに★を使う"
|
||||||
emailConfig: "メールサーバー設定"
|
emailServer: "メールサーバー"
|
||||||
enableEmail: "メール配信機能を有効化する"
|
enableEmail: "メール配信機能を有効化する"
|
||||||
emailConfigInfo: "メールアドレスの確認やパスワードリセットの際に使います"
|
emailConfigInfo: "メールアドレスの確認やパスワードリセットの際に使います"
|
||||||
email: "メール"
|
email: "メール"
|
||||||
|
@ -728,6 +727,20 @@ hideOnlineStatusDescription: "オンライン状態を隠すと、検索など
|
||||||
online: "オンライン"
|
online: "オンライン"
|
||||||
active: "アクティブ"
|
active: "アクティブ"
|
||||||
offline: "オフライン"
|
offline: "オフライン"
|
||||||
|
notRecommended: "非推奨"
|
||||||
|
botProtection: "Bot防御"
|
||||||
|
instanceBlocking: "インスタンスブロック"
|
||||||
|
selectAccount: "アカウントを選択"
|
||||||
|
enabled: "有効"
|
||||||
|
disabled: "無効"
|
||||||
|
quickAction: "クイックアクション"
|
||||||
|
user: "ユーザー"
|
||||||
|
administration: "管理"
|
||||||
|
accounts: "アカウント"
|
||||||
|
switch: "切り替え"
|
||||||
|
noMaintainerInformationWarning: "管理者情報が設定されていません。"
|
||||||
|
noBotProtectionWarning: "Bot防御が設定されていません。"
|
||||||
|
configure: "設定する"
|
||||||
|
|
||||||
_email:
|
_email:
|
||||||
_follow:
|
_follow:
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "misskey",
|
"name": "misskey",
|
||||||
"author": "syuilo <syuilotan@yahoo.co.jp>",
|
"author": "syuilo <syuilotan@yahoo.co.jp>",
|
||||||
"version": "12.78.0-beta.2",
|
"version": "12.78.0",
|
||||||
"codename": "indigo",
|
"codename": "indigo",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
|
@ -18,7 +18,7 @@ type Captcha = {
|
||||||
getResponse(id: string): string;
|
getResponse(id: string): string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type CaptchaProvider = 'hcaptcha' | 'grecaptcha';
|
type CaptchaProvider = 'hcaptcha' | 'recaptcha';
|
||||||
|
|
||||||
type CaptchaContainer = {
|
type CaptchaContainer = {
|
||||||
readonly [_ in CaptchaProvider]?: Captcha;
|
readonly [_ in CaptchaProvider]?: Captcha;
|
||||||
|
@ -57,7 +57,7 @@ export default defineComponent({
|
||||||
src() {
|
src() {
|
||||||
const endpoint = ({
|
const endpoint = ({
|
||||||
hcaptcha: 'https://hcaptcha.com/1',
|
hcaptcha: 'https://hcaptcha.com/1',
|
||||||
grecaptcha: 'https://www.recaptcha.net/recaptcha',
|
recaptcha: 'https://www.recaptcha.net/recaptcha',
|
||||||
} as Record<PropertyKey, unknown>)[this.provider];
|
} as Record<PropertyKey, unknown>)[this.provider];
|
||||||
|
|
||||||
return `${typeof endpoint == 'string' ? endpoint : 'about:invalid'}/api.js?render=explicit`;
|
return `${typeof endpoint == 'string' ? endpoint : 'about:invalid'}/api.js?render=explicit`;
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
<i v-if="type === 'success'" class="fas fa-check"></i>
|
<i v-if="type === 'success'" class="fas fa-check"></i>
|
||||||
<i v-else-if="type === 'error'" class="fas fa-times-circle"></i>
|
<i v-else-if="type === 'error'" class="fas fa-times-circle"></i>
|
||||||
<i v-else-if="type === 'warning'" class="fas fa-exclamation-triangle"></i>
|
<i v-else-if="type === 'warning'" class="fas fa-exclamation-triangle"></i>
|
||||||
<i v-else-if="type === 'info'" class="fas fa-information-circle"></i>
|
<i v-else-if="type === 'info'" class="fas fa-info-circle"></i>
|
||||||
<i v-else-if="type === 'question'" class="fas fa-question-circle"></i>
|
<i v-else-if="type === 'question'" class="fas fa-question-circle"></i>
|
||||||
<i v-else-if="type === 'waiting'" class="fas fa-spinner fa-pulse"></i>
|
<i v-else-if="type === 'waiting'" class="fas fa-spinner fa-pulse"></i>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -24,6 +24,8 @@ export default defineComponent({
|
||||||
--formXPadding: 32px;
|
--formXPadding: 32px;
|
||||||
--formYPadding: 32px;
|
--formYPadding: 32px;
|
||||||
|
|
||||||
|
--formContentHMargin: 16px;
|
||||||
|
|
||||||
font-size: 95%;
|
font-size: 95%;
|
||||||
line-height: 1.3em;
|
line-height: 1.3em;
|
||||||
background: var(--bg);
|
background: var(--bg);
|
||||||
|
|
|
@ -30,7 +30,7 @@
|
||||||
top: var(--stickyTop, 0px);
|
top: var(--stickyTop, 0px);
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
margin: -8px calc(var(--formXPadding) * -1) 0 calc(var(--formXPadding) * -1);
|
margin: -8px calc(var(--formXPadding) * -1) 0 calc(var(--formXPadding) * -1);
|
||||||
padding: 8px calc(16px + var(--formXPadding)) 8px calc(16px + var(--formXPadding));
|
padding: 8px calc(var(--formContentHMargin) + var(--formXPadding)) 8px calc(var(--formContentHMargin) + var(--formXPadding));
|
||||||
background: var(--X17);
|
background: var(--X17);
|
||||||
-webkit-backdrop-filter: blur(10px);
|
-webkit-backdrop-filter: blur(10px);
|
||||||
backdrop-filter: blur(10px);
|
backdrop-filter: blur(10px);
|
||||||
|
@ -42,7 +42,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
._formCaption {
|
._formCaption {
|
||||||
padding: 8px 16px 0 16px;
|
padding: 8px var(--formContentHMargin) 0 var(--formContentHMargin);
|
||||||
}
|
}
|
||||||
|
|
||||||
._formItem {
|
._formItem {
|
||||||
|
|
|
@ -20,7 +20,7 @@ export default defineComponent({
|
||||||
.anocepby {
|
.anocepby {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 14px 16px;
|
padding: 14px var(--formContentHMargin);
|
||||||
|
|
||||||
> .key {
|
> .key {
|
||||||
margin-right: 12px;
|
margin-right: 12px;
|
||||||
|
|
|
@ -75,7 +75,7 @@ export default defineComponent({
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
min-height: 130px;
|
min-height: 130px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 16px;
|
padding: 16px var(--formContentHMargin);
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font: inherit;
|
font: inherit;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
|
|
|
@ -18,6 +18,9 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
modelValue() {
|
||||||
|
this.value = this.modelValue;
|
||||||
|
},
|
||||||
value() {
|
value() {
|
||||||
this.$emit('update:modelValue', this.value);
|
this.$emit('update:modelValue', this.value);
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,13 +5,13 @@
|
||||||
<MkLoading/>
|
<MkLoading/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<FormGroup v-else-if="resolved" class="_formItem">
|
<div v-else-if="resolved" class="_formItem">
|
||||||
<slot :result="result"></slot>
|
<slot :result="result"></slot>
|
||||||
</FormGroup>
|
</div>
|
||||||
<div class="_formItem" v-else>
|
<div class="_formItem" v-else>
|
||||||
<div class="_formPanel">
|
<div class="_formPanel eiurkvay">
|
||||||
error!
|
<div><i class="fas fa-exclamation-triangle"></i> {{ $ts.somethingHappened }}</div>
|
||||||
<button @click="retry">retry</button>
|
<MkButton inline @click="retry" class="retry"><i class="fas fa-redo-alt"></i> {{ $ts.retry }}</MkButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
|
@ -20,11 +20,11 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, PropType, ref, watch } from 'vue';
|
import { defineComponent, PropType, ref, watch } from 'vue';
|
||||||
import './form.scss';
|
import './form.scss';
|
||||||
import FormGroup from './group.vue';
|
import MkButton from '@client/components/ui/button.vue';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
FormGroup,
|
MkButton
|
||||||
},
|
},
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
|
@ -89,4 +89,13 @@ export default defineComponent({
|
||||||
.fade-leave-to {
|
.fade-leave-to {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.eiurkvay {
|
||||||
|
padding: 16px;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
> .retry {
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,91 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="zbcjwnqg" v-size="{ max: [550, 1000] }">
|
<div class="zbcjwnqg" style="margin-top: -8px;">
|
||||||
<div class="stats" v-if="info">
|
|
||||||
<div class="_panel">
|
|
||||||
<div>
|
|
||||||
<b><i class="fas fa-user"></i>{{ $ts.users }}</b>
|
|
||||||
<small>{{ $ts.local }}</small>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<dl class="total">
|
|
||||||
<dt>{{ $ts.total }}</dt>
|
|
||||||
<dd>{{ number(info.originalUsersCount) }}</dd>
|
|
||||||
</dl>
|
|
||||||
<dl class="diff" :class="{ inc: usersLocalDoD > 0 }">
|
|
||||||
<dt>{{ $ts.dayOverDayChanges }}</dt>
|
|
||||||
<dd>{{ number(usersLocalDoD) }}</dd>
|
|
||||||
</dl>
|
|
||||||
<dl class="diff" :class="{ inc: usersLocalWoW > 0 }">
|
|
||||||
<dt>{{ $ts.weekOverWeekChanges }}</dt>
|
|
||||||
<dd>{{ number(usersLocalWoW) }}</dd>
|
|
||||||
</dl>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="_panel">
|
|
||||||
<div>
|
|
||||||
<b><i class="fas fa-user"></i>{{ $ts.users }}</b>
|
|
||||||
<small>{{ $ts.remote }}</small>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<dl class="total">
|
|
||||||
<dt>{{ $ts.total }}</dt>
|
|
||||||
<dd>{{ number((info.usersCount - info.originalUsersCount)) }}</dd>
|
|
||||||
</dl>
|
|
||||||
<dl class="diff" :class="{ inc: usersRemoteDoD > 0 }">
|
|
||||||
<dt>{{ $ts.dayOverDayChanges }}</dt>
|
|
||||||
<dd>{{ number(usersRemoteDoD) }}</dd>
|
|
||||||
</dl>
|
|
||||||
<dl class="diff" :class="{ inc: usersRemoteWoW > 0 }">
|
|
||||||
<dt>{{ $ts.weekOverWeekChanges }}</dt>
|
|
||||||
<dd>{{ number(usersRemoteWoW) }}</dd>
|
|
||||||
</dl>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="_panel">
|
|
||||||
<div>
|
|
||||||
<b><i class="fas fa-pencil-alt"></i>{{ $ts.notes }}</b>
|
|
||||||
<small>{{ $ts.local }}</small>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<dl class="total">
|
|
||||||
<dt>{{ $ts.total }}</dt>
|
|
||||||
<dd>{{ number(info.originalNotesCount) }}</dd>
|
|
||||||
</dl>
|
|
||||||
<dl class="diff" :class="{ inc: notesLocalDoD > 0 }">
|
|
||||||
<dt>{{ $ts.dayOverDayChanges }}</dt>
|
|
||||||
<dd>{{ number(notesLocalDoD) }}</dd>
|
|
||||||
</dl>
|
|
||||||
<dl class="diff" :class="{ inc: notesLocalWoW > 0 }">
|
|
||||||
<dt>{{ $ts.weekOverWeekChanges }}</dt>
|
|
||||||
<dd>{{ number(notesLocalWoW) }}</dd>
|
|
||||||
</dl>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="_panel">
|
|
||||||
<div>
|
|
||||||
<b><i class="fas fa-pencil-alt"></i>{{ $ts.notes }}</b>
|
|
||||||
<small>{{ $ts.remote }}</small>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<dl class="total">
|
|
||||||
<dt>{{ $ts.total }}</dt>
|
|
||||||
<dd>{{ number((info.notesCount - info.originalNotesCount)) }}</dd>
|
|
||||||
</dl>
|
|
||||||
<dl class="diff" :class="{ inc: notesRemoteDoD > 0 }">
|
|
||||||
<dt>{{ $ts.dayOverDayChanges }}</dt>
|
|
||||||
<dd>{{ number(notesRemoteDoD) }}</dd>
|
|
||||||
</dl>
|
|
||||||
<dl class="diff" :class="{ inc: notesRemoteWoW > 0 }">
|
|
||||||
<dt>{{ $ts.weekOverWeekChanges }}</dt>
|
|
||||||
<dd>{{ number(notesRemoteWoW) }}</dd>
|
|
||||||
</dl>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<section class="_card">
|
|
||||||
<div class="_title" style="position: relative;"><i class="fas fa-chart-bar"></i> {{ $ts.statistics }}<button @click="fetchChart" class="_button" style="position: absolute; right: 0; bottom: 0; top: 0; padding: inherit;"><i class="fas fa-sync"></i></button></div>
|
|
||||||
<div class="_content" style="margin-top: -8px;">
|
|
||||||
<div class="selects" style="display: flex;">
|
<div class="selects" style="display: flex;">
|
||||||
<MkSelect v-model:value="chartSrc" style="margin: 0; flex: 1;">
|
<MkSelect v-model:value="chartSrc" style="margin: 0; flex: 1;">
|
||||||
<optgroup :label="$ts.federation">
|
<optgroup :label="$ts.federation">
|
||||||
|
@ -117,8 +31,6 @@
|
||||||
</div>
|
</div>
|
||||||
<canvas ref="chart"></canvas>
|
<canvas ref="chart"></canvas>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
@ -158,7 +70,6 @@ export default defineComponent({
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
info: null,
|
|
||||||
notesLocalWoW: 0,
|
notesLocalWoW: 0,
|
||||||
notesLocalDoD: 0,
|
notesLocalDoD: 0,
|
||||||
notesRemoteWoW: 0,
|
notesRemoteWoW: 0,
|
||||||
|
@ -216,8 +127,6 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
|
|
||||||
async created() {
|
async created() {
|
||||||
this.info = await os.api('stats');
|
|
||||||
|
|
||||||
this.now = new Date();
|
this.now = new Date();
|
||||||
|
|
||||||
this.fetchChart();
|
this.fetchChart();
|
||||||
|
@ -256,15 +165,6 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
this.notesLocalWoW = this.info.originalNotesCount - chart.perDay.notes.local.total[7];
|
|
||||||
this.notesLocalDoD = this.info.originalNotesCount - chart.perDay.notes.local.total[1];
|
|
||||||
this.notesRemoteWoW = (this.info.notesCount - this.info.originalNotesCount) - chart.perDay.notes.remote.total[7];
|
|
||||||
this.notesRemoteDoD = (this.info.notesCount - this.info.originalNotesCount) - chart.perDay.notes.remote.total[1];
|
|
||||||
this.usersLocalWoW = this.info.originalUsersCount - chart.perDay.users.local.total[7];
|
|
||||||
this.usersLocalDoD = this.info.originalUsersCount - chart.perDay.users.local.total[1];
|
|
||||||
this.usersRemoteWoW = (this.info.usersCount - this.info.originalUsersCount) - chart.perDay.users.remote.total[7];
|
|
||||||
this.usersRemoteDoD = (this.info.usersCount - this.info.originalUsersCount) - chart.perDay.users.remote.total[1];
|
|
||||||
|
|
||||||
this.chart = chart;
|
this.chart = chart;
|
||||||
|
|
||||||
this.renderChart();
|
this.renderChart();
|
||||||
|
@ -300,10 +200,10 @@ export default defineComponent({
|
||||||
aspectRatio: 2.5,
|
aspectRatio: 2.5,
|
||||||
layout: {
|
layout: {
|
||||||
padding: {
|
padding: {
|
||||||
left: 0,
|
left: 16,
|
||||||
right: 0,
|
right: 16,
|
||||||
top: 16,
|
top: 16,
|
||||||
bottom: 0
|
bottom: 8
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
legend: {
|
legend: {
|
||||||
|
@ -630,90 +530,8 @@ export default defineComponent({
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.zbcjwnqg {
|
.zbcjwnqg {
|
||||||
&.max-width_1000px {
|
> .selects {
|
||||||
> .stats {
|
padding: 8px 16px 0 16px;
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
grid-template-rows: 1fr 1fr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.max-width_550px {
|
|
||||||
> .stats {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
grid-template-rows: 1fr 1fr 1fr 1fr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> .stats {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr 1fr 1fr;
|
|
||||||
grid-template-rows: 1fr;
|
|
||||||
gap: var(--margin);
|
|
||||||
margin-bottom: var(--margin);
|
|
||||||
font-size: 90%;
|
|
||||||
|
|
||||||
> div {
|
|
||||||
display: flex;
|
|
||||||
box-sizing: border-box;
|
|
||||||
padding: 16px 20px;
|
|
||||||
|
|
||||||
> div {
|
|
||||||
width: 50%;
|
|
||||||
|
|
||||||
&:first-child {
|
|
||||||
> b {
|
|
||||||
display: block;
|
|
||||||
|
|
||||||
> i {
|
|
||||||
width: 16px;
|
|
||||||
margin-right: 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> small {
|
|
||||||
margin-left: 16px + 8px;
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
> dl {
|
|
||||||
display: flex;
|
|
||||||
margin: 0;
|
|
||||||
line-height: 1.5em;
|
|
||||||
|
|
||||||
> dt,
|
|
||||||
> dd {
|
|
||||||
width: 50%;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
> dd {
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
overflow: hidden;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.total {
|
|
||||||
> dt,
|
|
||||||
> dd {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.diff.inc {
|
|
||||||
> dd {
|
|
||||||
color: #82c11c;
|
|
||||||
|
|
||||||
&:before {
|
|
||||||
content: "+";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<MkModal ref="modal" @click="$emit('click')" @closed="$emit('closed')">
|
<MkModal ref="modal" @click="$emit('click')" @closed="$emit('closed')">
|
||||||
<div class="hrmcaedk _popup _narrow_" :style="{ width: `${width}px`, height: (height ? `min(${height}px, 100%)` : '100%') }">
|
<div class="hrmcaedk _popup _narrow_" :style="{ width: `${width}px`, height: (height ? `min(${height}px, 100%)` : '100%') }">
|
||||||
<div class="header">
|
<div class="header" @contextmenu="onContextmenu">
|
||||||
<button class="_button" @click="back()" v-if="history.length > 0"><i class="fas fa-chevron-left"></i></button>
|
<button class="_button" @click="back()" v-if="history.length > 0"><i class="fas fa-chevron-left"></i></button>
|
||||||
<button class="_button" style="pointer-events: none;" v-else><!-- マージンのバランスを取るためのダミー --></button>
|
<button class="_button" style="pointer-events: none;" v-else><!-- マージンのバランスを取るためのダミー --></button>
|
||||||
<span class="title">
|
<span class="title">
|
||||||
|
@ -27,6 +27,7 @@ import copyToClipboard from '@client/scripts/copy-to-clipboard';
|
||||||
import { resolve } from '@client/router';
|
import { resolve } from '@client/router';
|
||||||
import { url } from '@client/config';
|
import { url } from '@client/config';
|
||||||
import * as symbols from '@client/symbols';
|
import * as symbols from '@client/symbols';
|
||||||
|
import * as os from '@client/os';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
|
@ -148,6 +149,10 @@ export default defineComponent({
|
||||||
popout(this.path, this.$el);
|
popout(this.path, this.$el);
|
||||||
this.$refs.window.close();
|
this.$refs.window.close();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onContextmenu(e) {
|
||||||
|
os.contextMenu(this.contextmenu, e);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -45,7 +45,7 @@
|
||||||
</I18n>
|
</I18n>
|
||||||
</label>
|
</label>
|
||||||
<captcha v-if="meta.enableHcaptcha" class="captcha" provider="hcaptcha" ref="hcaptcha" v-model:value="hCaptchaResponse" :sitekey="meta.hcaptchaSiteKey"/>
|
<captcha v-if="meta.enableHcaptcha" class="captcha" provider="hcaptcha" ref="hcaptcha" v-model:value="hCaptchaResponse" :sitekey="meta.hcaptchaSiteKey"/>
|
||||||
<captcha v-if="meta.enableRecaptcha" class="captcha" provider="grecaptcha" ref="recaptcha" v-model:value="reCaptchaResponse" :sitekey="meta.recaptchaSiteKey"/>
|
<captcha v-if="meta.enableRecaptcha" class="captcha" provider="recaptcha" ref="recaptcha" v-model:value="reCaptchaResponse" :sitekey="meta.recaptchaSiteKey"/>
|
||||||
<MkButton type="submit" :disabled="shouldDisableSubmitting" primary>{{ $ts.start }}</MkButton>
|
<MkButton type="submit" :disabled="shouldDisableSubmitting" primary>{{ $ts.start }}</MkButton>
|
||||||
</template>
|
</template>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -29,6 +29,7 @@ export default defineComponent({
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.pxhvhrfw {
|
.pxhvhrfw {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
font-size: 90%;
|
||||||
|
|
||||||
> button {
|
> button {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|
|
@ -1,9 +1,15 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="cxiknjgy">
|
<transition name="fade" mode="out-in">
|
||||||
<slot :items="items"></slot>
|
<MkLoading v-if="fetching"/>
|
||||||
<div class="empty" v-if="empty" key="_empty_">
|
|
||||||
|
<MkError v-else-if="error" @retry="init()"/>
|
||||||
|
|
||||||
|
<div class="empty" v-else-if="empty" key="_empty_">
|
||||||
<slot name="empty"></slot>
|
<slot name="empty"></slot>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="cxiknjgy">
|
||||||
|
<slot :items="items"></slot>
|
||||||
<div class="more" v-show="more" key="_more_">
|
<div class="more" v-show="more" key="_more_">
|
||||||
<MkButton class="button" v-appear="$store.state.enableInfiniteScroll ? fetchMore : null" @click="fetchMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary>
|
<MkButton class="button" v-appear="$store.state.enableInfiniteScroll ? fetchMore : null" @click="fetchMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary>
|
||||||
<template v-if="!moreFetching">{{ $ts.loadMore }}</template>
|
<template v-if="!moreFetching">{{ $ts.loadMore }}</template>
|
||||||
|
@ -11,6 +17,7 @@
|
||||||
</MkButton>
|
</MkButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</transition>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
@ -36,6 +43,15 @@ export default defineComponent({
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
.fade-enter-active,
|
||||||
|
.fade-leave-active {
|
||||||
|
transition: opacity 0.125s ease;
|
||||||
|
}
|
||||||
|
.fade-enter-from,
|
||||||
|
.fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.cxiknjgy {
|
.cxiknjgy {
|
||||||
> .more > .button {
|
> .more > .button {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
|
|
|
@ -68,10 +68,9 @@ export default defineComponent({
|
||||||
padding: 0;
|
padding: 0;
|
||||||
width: 96px;
|
width: 96px;
|
||||||
height: 96px;
|
height: 96px;
|
||||||
|
display: flex;
|
||||||
> .icon {
|
align-items: center;
|
||||||
height: 100%;
|
justify-content: center;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
> .icon {
|
> .icon {
|
||||||
|
|
22
src/client/directives/click-anime.ts
Normal file
22
src/client/directives/click-anime.ts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import { Directive } from 'vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mounted(el, binding, vn) {
|
||||||
|
el.addEventListener('mousedown', () => {
|
||||||
|
el.classList.add('_anime_bounce_ready');
|
||||||
|
|
||||||
|
el.addEventListener('mouseleave', () => {
|
||||||
|
el.classList.remove('_anime_bounce_ready');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
el.addEventListener('click', () => {
|
||||||
|
el.classList.add('_anime_bounce');
|
||||||
|
});
|
||||||
|
|
||||||
|
el.addEventListener('animationend', () => {
|
||||||
|
el.classList.remove('_anime_bounce_ready');
|
||||||
|
el.classList.remove('_anime_bounce');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} as Directive;
|
|
@ -8,6 +8,7 @@ import hotkey from './hotkey';
|
||||||
import appear from './appear';
|
import appear from './appear';
|
||||||
import anim from './anim';
|
import anim from './anim';
|
||||||
import stickyContainer from './sticky-container';
|
import stickyContainer from './sticky-container';
|
||||||
|
import clickAnime from './click-anime';
|
||||||
|
|
||||||
export default function(app: App) {
|
export default function(app: App) {
|
||||||
app.directive('userPreview', userPreview);
|
app.directive('userPreview', userPreview);
|
||||||
|
@ -18,5 +19,6 @@ export default function(app: App) {
|
||||||
app.directive('hotkey', hotkey);
|
app.directive('hotkey', hotkey);
|
||||||
app.directive('appear', appear);
|
app.directive('appear', appear);
|
||||||
app.directive('anim', anim);
|
app.directive('anim', anim);
|
||||||
|
app.directive('click-anime', clickAnime);
|
||||||
app.directive('sticky-container', stickyContainer);
|
app.directive('sticky-container', stickyContainer);
|
||||||
}
|
}
|
||||||
|
|
|
@ -83,6 +83,11 @@ if ((typeof ColdDeviceStorage.get('lightTheme') === 'string') || (typeof ColdDev
|
||||||
ColdDeviceStorage.set('lightTheme', require('@client/themes/l-light.json5'));
|
ColdDeviceStorage.set('lightTheme', require('@client/themes/l-light.json5'));
|
||||||
ColdDeviceStorage.set('darkTheme', require('@client/themes/d-dark.json5'));
|
ColdDeviceStorage.set('darkTheme', require('@client/themes/d-dark.json5'));
|
||||||
}
|
}
|
||||||
|
const link = document.createElement('link');
|
||||||
|
link.rel = 'stylesheet';
|
||||||
|
link.href = 'https://use.fontawesome.com/releases/v5.15.3/css/all.css';
|
||||||
|
document.head.appendChild(link);
|
||||||
|
// TODOここまで
|
||||||
|
|
||||||
if (_DEV_) {
|
if (_DEV_) {
|
||||||
console.warn('Development mode!!!');
|
console.warn('Development mode!!!');
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="">
|
<div class="lcixvhis">
|
||||||
<div class="_section reports">
|
<div class="_section reports">
|
||||||
<div class="_content">
|
<div class="_content">
|
||||||
<div class="inputs" style="display: flex;">
|
<div class="inputs" style="display: flex;">
|
||||||
|
@ -80,6 +80,8 @@ export default defineComponent({
|
||||||
MkPagination,
|
MkPagination,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
emits: ['info'],
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
[symbols.PAGE_INFO]: {
|
[symbols.PAGE_INFO]: {
|
||||||
|
@ -117,6 +119,10 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.$emit('info', this[symbols.PAGE_INFO]);
|
||||||
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
acct,
|
acct,
|
||||||
|
|
||||||
|
@ -132,6 +138,10 @@ export default defineComponent({
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
.lcixvhis {
|
||||||
|
margin: var(--margin);
|
||||||
|
}
|
||||||
|
|
||||||
.bcekxzvu {
|
.bcekxzvu {
|
||||||
> .target {
|
> .target {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="ztgjmzrw">
|
<div class="ztgjmzrw">
|
||||||
<div class="_section">
|
|
||||||
<div class="_content">
|
|
||||||
<MkButton @click="add()" primary style="margin: 0 auto 16px auto;"><i class="fas fa-plus"></i> {{ $ts.add }}</MkButton>
|
<MkButton @click="add()" primary style="margin: 0 auto 16px auto;"><i class="fas fa-plus"></i> {{ $ts.add }}</MkButton>
|
||||||
<section class="_card _gap announcements" v-for="announcement in announcements">
|
<section class="_card _gap announcements" v-for="announcement in announcements">
|
||||||
<div class="_content announcement">
|
<div class="_content announcement">
|
||||||
|
@ -22,8 +20,6 @@
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
@ -41,6 +37,8 @@ export default defineComponent({
|
||||||
MkTextarea,
|
MkTextarea,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
emits: ['info'],
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
[symbols.PAGE_INFO]: {
|
[symbols.PAGE_INFO]: {
|
||||||
|
@ -57,6 +55,10 @@ export default defineComponent({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.$emit('info', this[symbols.PAGE_INFO]);
|
||||||
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
add() {
|
add() {
|
||||||
this.announcements.unshift({
|
this.announcements.unshift({
|
||||||
|
@ -109,3 +111,9 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.ztgjmzrw {
|
||||||
|
margin: var(--margin);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
138
src/client/pages/instance/bot-protection.vue
Normal file
138
src/client/pages/instance/bot-protection.vue
Normal file
|
@ -0,0 +1,138 @@
|
||||||
|
<template>
|
||||||
|
<FormBase>
|
||||||
|
<FormSuspense :p="init">
|
||||||
|
<FormRadios v-model="provider">
|
||||||
|
<template #desc><i class="fas fa-shield-alt"></i> {{ $ts.botProtection }}</template>
|
||||||
|
<option :value="null">{{ $ts.none }} ({{ $ts.notRecommended }})</option>
|
||||||
|
<option value="hcaptcha">hCaptcha</option>
|
||||||
|
<option value="recaptcha">reCAPTCHA</option>
|
||||||
|
</FormRadios>
|
||||||
|
|
||||||
|
<template v-if="provider === 'hcaptcha'">
|
||||||
|
<div class="_formItem _formNoConcat" v-sticky-container>
|
||||||
|
<div class="_formLabel">hCaptcha</div>
|
||||||
|
<div class="main">
|
||||||
|
<FormInput v-model:value="hcaptchaSiteKey">
|
||||||
|
<template #prefix><i class="fas fa-key"></i></template>
|
||||||
|
<span>{{ $ts.hcaptchaSiteKey }}</span>
|
||||||
|
</FormInput>
|
||||||
|
<FormInput v-model:value="hcaptchaSecretKey">
|
||||||
|
<template #prefix><i class="fas fa-key"></i></template>
|
||||||
|
<span>{{ $ts.hcaptchaSecretKey }}</span>
|
||||||
|
</FormInput>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="_formItem _formNoConcat" v-sticky-container>
|
||||||
|
<div class="_formLabel">{{ $ts.preview }}</div>
|
||||||
|
<div class="_formPanel" style="padding: var(--formContentHMargin);">
|
||||||
|
<MkCaptcha provider="hcaptcha" :sitekey="hcaptchaSiteKey || '10000000-ffff-ffff-ffff-000000000001'"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="provider === 'recaptcha'">
|
||||||
|
<div class="_formItem _formNoConcat" v-sticky-container>
|
||||||
|
<div class="_formLabel">reCAPTCHA</div>
|
||||||
|
<div class="main">
|
||||||
|
<FormInput v-model:value="recaptchaSiteKey">
|
||||||
|
<template #prefix><i class="fas fa-key"></i></template>
|
||||||
|
<span>{{ $ts.recaptchaSiteKey }}</span>
|
||||||
|
</FormInput>
|
||||||
|
<FormInput v-model:value="recaptchaSecretKey">
|
||||||
|
<template #prefix><i class="fas fa-key"></i></template>
|
||||||
|
<span>{{ $ts.recaptchaSecretKey }}</span>
|
||||||
|
</FormInput>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="recaptchaSiteKey" class="_formItem _formNoConcat" v-sticky-container>
|
||||||
|
<div class="_formLabel">{{ $ts.preview }}</div>
|
||||||
|
<div class="_formPanel" style="padding: var(--formContentHMargin);">
|
||||||
|
<MkCaptcha provider="recaptcha" :sitekey="recaptchaSiteKey"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
|
||||||
|
</FormSuspense>
|
||||||
|
</FormBase>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineAsyncComponent, defineComponent } from 'vue';
|
||||||
|
import FormRadios from '@client/components/form/radios.vue';
|
||||||
|
import FormInput from '@client/components/form/input.vue';
|
||||||
|
import FormButton from '@client/components/form/button.vue';
|
||||||
|
import FormBase from '@client/components/form/base.vue';
|
||||||
|
import FormGroup from '@client/components/form/group.vue';
|
||||||
|
import FormInfo from '@client/components/form/info.vue';
|
||||||
|
import FormSuspense from '@client/components/form/suspense.vue';
|
||||||
|
import * as os from '@client/os';
|
||||||
|
import * as symbols from '@client/symbols';
|
||||||
|
import { fetchInstance } from '@client/instance';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
components: {
|
||||||
|
FormRadios,
|
||||||
|
FormInput,
|
||||||
|
FormBase,
|
||||||
|
FormGroup,
|
||||||
|
FormButton,
|
||||||
|
FormInfo,
|
||||||
|
FormSuspense,
|
||||||
|
MkCaptcha: defineAsyncComponent(() => import('@client/components/captcha.vue')),
|
||||||
|
},
|
||||||
|
|
||||||
|
emits: ['info'],
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
[symbols.PAGE_INFO]: {
|
||||||
|
title: this.$ts.botProtection,
|
||||||
|
icon: 'fas fa-shield-alt'
|
||||||
|
},
|
||||||
|
provider: null,
|
||||||
|
enableHcaptcha: false,
|
||||||
|
hcaptchaSiteKey: null,
|
||||||
|
hcaptchaSecretKey: null,
|
||||||
|
enableRecaptcha: false,
|
||||||
|
recaptchaSiteKey: null,
|
||||||
|
recaptchaSecretKey: null,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async mounted() {
|
||||||
|
this.$emit('info', this[symbols.PAGE_INFO]);
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async init() {
|
||||||
|
const meta = await os.api('meta', { detail: true });
|
||||||
|
this.enableHcaptcha = meta.enableHcaptcha;
|
||||||
|
this.hcaptchaSiteKey = meta.hcaptchaSiteKey;
|
||||||
|
this.hcaptchaSecretKey = meta.hcaptchaSecretKey;
|
||||||
|
this.enableRecaptcha = meta.enableRecaptcha;
|
||||||
|
this.recaptchaSiteKey = meta.recaptchaSiteKey;
|
||||||
|
this.recaptchaSecretKey = meta.recaptchaSecretKey;
|
||||||
|
|
||||||
|
this.provider = this.enableHcaptcha ? 'hcaptcha' : this.enableRecaptcha ? 'recaptcha' : null;
|
||||||
|
|
||||||
|
this.$watch(() => this.provider, () => {
|
||||||
|
this.enableHcaptcha = this.provider === 'hcaptcha';
|
||||||
|
this.enableRecaptcha = this.provider === 'recaptcha';
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
save() {
|
||||||
|
os.apiWithDialog('admin/update-meta', {
|
||||||
|
enableHcaptcha: this.enableHcaptcha,
|
||||||
|
hcaptchaSiteKey: this.hcaptchaSiteKey,
|
||||||
|
hcaptchaSecretKey: this.hcaptchaSecretKey,
|
||||||
|
enableRecaptcha: this.enableRecaptcha,
|
||||||
|
recaptchaSiteKey: this.recaptchaSiteKey,
|
||||||
|
recaptchaSecretKey: this.recaptchaSecretKey,
|
||||||
|
}).then(() => {
|
||||||
|
fetchInstance();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
60
src/client/pages/instance/database.vue
Normal file
60
src/client/pages/instance/database.vue
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
<template>
|
||||||
|
<FormBase>
|
||||||
|
<FormSuspense :p="databasePromiseFactory" v-slot="{ result: database }">
|
||||||
|
<FormGroup v-for="table in database" :key="table[0]">
|
||||||
|
<template #label>{{ table[0] }}</template>
|
||||||
|
<FormKeyValueView>
|
||||||
|
<template #key>Size</template>
|
||||||
|
<template #value>{{ bytes(table[1].size) }}</template>
|
||||||
|
</FormKeyValueView>
|
||||||
|
<FormKeyValueView>
|
||||||
|
<template #key>Records</template>
|
||||||
|
<template #value>{{ number(table[1].count) }}</template>
|
||||||
|
</FormKeyValueView>
|
||||||
|
</FormGroup>
|
||||||
|
</FormSuspense>
|
||||||
|
</FormBase>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
import FormSuspense from '@client/components/form/suspense.vue';
|
||||||
|
import FormKeyValueView from '@client/components/form/key-value-view.vue';
|
||||||
|
import FormLink from '@client/components/form/link.vue';
|
||||||
|
import FormBase from '@client/components/form/base.vue';
|
||||||
|
import FormGroup from '@client/components/form/group.vue';
|
||||||
|
import * as os from '@client/os';
|
||||||
|
import * as symbols from '@client/symbols';
|
||||||
|
import bytes from '@client/filters/bytes';
|
||||||
|
import number from '@client/filters/number';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
components: {
|
||||||
|
FormSuspense,
|
||||||
|
FormKeyValueView,
|
||||||
|
FormBase,
|
||||||
|
FormGroup,
|
||||||
|
FormLink,
|
||||||
|
},
|
||||||
|
|
||||||
|
emits: ['info'],
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
[symbols.PAGE_INFO]: {
|
||||||
|
title: this.$ts.database,
|
||||||
|
icon: 'fas fa-database'
|
||||||
|
},
|
||||||
|
databasePromiseFactory: () => os.api('admin/get-table-stats', {}).then(res => Object.entries(res).sort((a, b) => b[1].size - a[1].size)),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.$emit('info', this[symbols.PAGE_INFO]);
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
bytes, number,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
127
src/client/pages/instance/email-settings.vue
Normal file
127
src/client/pages/instance/email-settings.vue
Normal file
|
@ -0,0 +1,127 @@
|
||||||
|
<template>
|
||||||
|
<FormBase>
|
||||||
|
<FormSuspense :p="init">
|
||||||
|
<FormSwitch v-model:value="enableEmail">{{ $ts.enableEmail }}<template #desc>{{ $ts.emailConfigInfo }}</template></FormSwitch>
|
||||||
|
|
||||||
|
<template v-if="enableEmail">
|
||||||
|
<FormInput v-model:value="email" type="email">
|
||||||
|
<span>{{ $ts.emailAddress }}</span>
|
||||||
|
</FormInput>
|
||||||
|
|
||||||
|
<div class="_formItem _formNoConcat" v-sticky-container>
|
||||||
|
<div class="_formLabel">{{ $ts.smtpConfig }}</div>
|
||||||
|
<div class="main">
|
||||||
|
<FormInput v-model:value="smtpHost">
|
||||||
|
<span>{{ $ts.smtpHost }}</span>
|
||||||
|
</FormInput>
|
||||||
|
<FormInput v-model:value="smtpPort" type="number">
|
||||||
|
<span>{{ $ts.smtpPort }}</span>
|
||||||
|
</FormInput>
|
||||||
|
<FormInput v-model:value="smtpUser">
|
||||||
|
<span>{{ $ts.smtpUser }}</span>
|
||||||
|
</FormInput>
|
||||||
|
<FormInput v-model:value="smtpPass" type="password">
|
||||||
|
<span>{{ $ts.smtpPass }}</span>
|
||||||
|
</FormInput>
|
||||||
|
<FormInfo>{{ $ts.emptyToDisableSmtpAuth }}</FormInfo>
|
||||||
|
<FormSwitch v-model:value="smtpSecure">{{ $ts.smtpSecure }}<template #desc>{{ $ts.smtpSecureInfo }}</template></FormSwitch>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormButton @click="testEmail">{{ $ts.testEmail }}</FormButton>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
|
||||||
|
</FormSuspense>
|
||||||
|
</FormBase>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
import FormSwitch from '@client/components/form/switch.vue';
|
||||||
|
import FormInput from '@client/components/form/input.vue';
|
||||||
|
import FormButton from '@client/components/form/button.vue';
|
||||||
|
import FormBase from '@client/components/form/base.vue';
|
||||||
|
import FormGroup from '@client/components/form/group.vue';
|
||||||
|
import FormInfo from '@client/components/form/info.vue';
|
||||||
|
import FormSuspense from '@client/components/form/suspense.vue';
|
||||||
|
import * as os from '@client/os';
|
||||||
|
import * as symbols from '@client/symbols';
|
||||||
|
import { fetchInstance } from '@client/instance';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
components: {
|
||||||
|
FormSwitch,
|
||||||
|
FormInput,
|
||||||
|
FormBase,
|
||||||
|
FormGroup,
|
||||||
|
FormButton,
|
||||||
|
FormInfo,
|
||||||
|
FormSuspense,
|
||||||
|
},
|
||||||
|
|
||||||
|
emits: ['info'],
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
[symbols.PAGE_INFO]: {
|
||||||
|
title: this.$ts.emailServer,
|
||||||
|
icon: 'fas fa-envelope'
|
||||||
|
},
|
||||||
|
enableEmail: false,
|
||||||
|
email: null,
|
||||||
|
smtpSecure: false,
|
||||||
|
smtpHost: '',
|
||||||
|
smtpPort: 0,
|
||||||
|
smtpUser: '',
|
||||||
|
smtpPass: '',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async mounted() {
|
||||||
|
this.$emit('info', this[symbols.PAGE_INFO]);
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async init() {
|
||||||
|
const meta = await os.api('meta', { detail: true });
|
||||||
|
this.enableEmail = meta.enableEmail;
|
||||||
|
this.email = meta.email;
|
||||||
|
this.smtpSecure = meta.smtpSecure;
|
||||||
|
this.smtpHost = meta.smtpHost;
|
||||||
|
this.smtpPort = meta.smtpPort;
|
||||||
|
this.smtpUser = meta.smtpUser;
|
||||||
|
this.smtpPass = meta.smtpPass;
|
||||||
|
},
|
||||||
|
|
||||||
|
async testEmail() {
|
||||||
|
const { canceled, result: destination } = await os.dialog({
|
||||||
|
title: this.$ts.destination,
|
||||||
|
input: {
|
||||||
|
placeholder: this.$instance.maintainerEmail
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (canceled) return;
|
||||||
|
os.apiWithDialog('admin/send-email', {
|
||||||
|
to: destination,
|
||||||
|
subject: 'Test email',
|
||||||
|
text: 'Yo'
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
save() {
|
||||||
|
os.apiWithDialog('admin/update-meta', {
|
||||||
|
enableEmail: this.enableEmail,
|
||||||
|
email: this.email,
|
||||||
|
smtpSecure: this.smtpSecure,
|
||||||
|
smtpHost: this.smtpHost,
|
||||||
|
smtpPort: this.smtpPort,
|
||||||
|
smtpUser: this.smtpUser,
|
||||||
|
smtpPass: this.smtpPass,
|
||||||
|
}).then(() => {
|
||||||
|
fetchInstance();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -1,24 +1,21 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="mk-instance-emojis">
|
<div class="ogwlenmc">
|
||||||
<div class="_section" style="padding: 0;">
|
|
||||||
<MkTab v-model:value="tab">
|
<MkTab v-model:value="tab">
|
||||||
<option value="local">{{ $ts.local }}</option>
|
<option value="local">{{ $ts.local }}</option>
|
||||||
<option value="remote">{{ $ts.remote }}</option>
|
<option value="remote">{{ $ts.remote }}</option>
|
||||||
</MkTab>
|
</MkTab>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="_section">
|
|
||||||
<div class="local" v-if="tab === 'local'">
|
<div class="local" v-if="tab === 'local'">
|
||||||
<MkButton primary @click="add" style="margin: 0 auto var(--margin) auto;"><i class="fas fa-plus"></i> {{ $ts.addEmoji }}</MkButton>
|
<MkButton primary @click="add" style="margin: var(--margin) auto;"><i class="fas fa-plus"></i> {{ $ts.addEmoji }}</MkButton>
|
||||||
<MkInput v-model:value="query" :debounce="true" type="search"><template #icon><i class="fas fa-search"></i></template><span>{{ $ts.search }}</span></MkInput>
|
<MkInput v-model:value="query" :debounce="true" type="search" style="margin: var(--margin);"><template #icon><i class="fas fa-search"></i></template><span>{{ $ts.search }}</span></MkInput>
|
||||||
<MkPagination :pagination="pagination" ref="emojis">
|
<MkPagination :pagination="pagination" ref="emojis">
|
||||||
<template #empty><span>{{ $ts.noCustomEmojis }}</span></template>
|
<template #empty><span>{{ $ts.noCustomEmojis }}</span></template>
|
||||||
<template #default="{items}">
|
<template #default="{items}">
|
||||||
<div class="emojis">
|
<div class="ldhfsamy">
|
||||||
<button class="emoji _panel _button" v-for="emoji in items" :key="emoji.id" @click="edit(emoji)">
|
<button class="emoji _panel _button" v-for="emoji in items" :key="emoji.id" @click="edit(emoji)">
|
||||||
<img :src="emoji.url" class="img" :alt="emoji.name"/>
|
<img :src="emoji.url" class="img" :alt="emoji.name"/>
|
||||||
<div class="body">
|
<div class="body">
|
||||||
<div class="name">{{ emoji.name }}</div>
|
<div class="name _monospace">{{ emoji.name }}</div>
|
||||||
<div class="info">{{ emoji.category }}</div>
|
<div class="info">{{ emoji.category }}</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
@ -28,16 +25,16 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="remote" v-else-if="tab === 'remote'">
|
<div class="remote" v-else-if="tab === 'remote'">
|
||||||
<MkInput v-model:value="queryRemote" :debounce="true" type="search"><template #icon><i class="fas fa-search"></i></template><span>{{ $ts.search }}</span></MkInput>
|
<MkInput v-model:value="queryRemote" :debounce="true" type="search" style="margin: var(--margin);"><template #icon><i class="fas fa-search"></i></template><span>{{ $ts.search }}</span></MkInput>
|
||||||
<MkInput v-model:value="host" :debounce="true"><span>{{ $ts.host }}</span></MkInput>
|
<MkInput v-model:value="host" :debounce="true" style="margin: var(--margin);"><span>{{ $ts.host }}</span></MkInput>
|
||||||
<MkPagination :pagination="remotePagination" ref="remoteEmojis">
|
<MkPagination :pagination="remotePagination" ref="remoteEmojis">
|
||||||
<template #empty><span>{{ $ts.noCustomEmojis }}</span></template>
|
<template #empty><span>{{ $ts.noCustomEmojis }}</span></template>
|
||||||
<template #default="{items}">
|
<template #default="{items}">
|
||||||
<div class="emojis">
|
<div class="ldhfsamy">
|
||||||
<div class="emoji _panel _button" v-for="emoji in items" :key="emoji.id" @click="remoteMenu(emoji, $event)">
|
<div class="emoji _panel _button" v-for="emoji in items" :key="emoji.id" @click="remoteMenu(emoji, $event)">
|
||||||
<img :src="emoji.url" class="img" :alt="emoji.name"/>
|
<img :src="emoji.url" class="img" :alt="emoji.name"/>
|
||||||
<div class="body">
|
<div class="body">
|
||||||
<div class="name">{{ emoji.name }}</div>
|
<div class="name _monospace">{{ emoji.name }}</div>
|
||||||
<div class="info">{{ emoji.host }}</div>
|
<div class="info">{{ emoji.host }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -46,7 +43,6 @@
|
||||||
</MkPagination>
|
</MkPagination>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
@ -67,6 +63,8 @@ export default defineComponent({
|
||||||
MkPagination,
|
MkPagination,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
emits: ['info'],
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
[symbols.PAGE_INFO]: {
|
[symbols.PAGE_INFO]: {
|
||||||
|
@ -99,6 +97,10 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async mounted() {
|
||||||
|
this.$emit('info', this[symbols.PAGE_INFO]);
|
||||||
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
async add(e) {
|
async add(e) {
|
||||||
const files = await selectFile(e.currentTarget || e.target, null, true);
|
const files = await selectFile(e.currentTarget || e.target, null, true);
|
||||||
|
@ -150,13 +152,13 @@ export default defineComponent({
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.mk-instance-emojis {
|
.ogwlenmc {
|
||||||
> ._section {
|
|
||||||
> .local {
|
> .local {
|
||||||
.emojis {
|
.ldhfsamy {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
|
||||||
grid-gap: var(--margin);
|
grid-gap: 12px;
|
||||||
|
margin: var(--margin);
|
||||||
|
|
||||||
> .emoji {
|
> .emoji {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -194,10 +196,11 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
|
|
||||||
> .remote {
|
> .remote {
|
||||||
.emojis {
|
.ldhfsamy {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
|
||||||
grid-gap: var(--margin);
|
grid-gap: 12px;
|
||||||
|
margin: var(--margin);
|
||||||
|
|
||||||
> .emoji {
|
> .emoji {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -226,6 +229,7 @@ export default defineComponent({
|
||||||
|
|
||||||
> .info {
|
> .info {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
|
font-size: 90%;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
@ -234,5 +238,4 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="enuoauvw">
|
||||||
<div class="_section">
|
<div class="query">
|
||||||
<div class="_content">
|
|
||||||
<MkInput v-model:value="host" :debounce="true"><span>{{ $ts.host }}</span></MkInput>
|
<MkInput v-model:value="host" :debounce="true"><span>{{ $ts.host }}</span></MkInput>
|
||||||
<div class="inputs" style="display: flex;">
|
<div class="inputs" style="display: flex;">
|
||||||
<MkSelect v-model:value="state" style="margin: 0; flex: 1;">
|
<MkSelect v-model:value="state" style="margin: 0; flex: 1;">
|
||||||
|
@ -37,11 +36,9 @@
|
||||||
</MkSelect>
|
</MkSelect>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div class="_section">
|
|
||||||
<div class="_content">
|
|
||||||
<MkPagination :pagination="pagination" #default="{items}" ref="instances" :key="host + state">
|
<MkPagination :pagination="pagination" #default="{items}" ref="instances" :key="host + state">
|
||||||
<div class="ppgwaixt _panel" v-for="instance in items" :key="instance.id" @click="info(instance)">
|
<div class="ppgwaixt _block" v-for="instance in items" :key="instance.id" @click="info(instance)">
|
||||||
<div class="host"><i class="fas fa-circle indicator" :class="getStatus(instance)"></i><b>{{ instance.host }}</b></div>
|
<div class="host"><i class="fas fa-circle indicator" :class="getStatus(instance)"></i><b>{{ instance.host }}</b></div>
|
||||||
<div class="status">
|
<div class="status">
|
||||||
<span class="sub" v-if="instance.followersCount > 0"><i class="fas fa-caret-down icon"></i>Sub</span>
|
<span class="sub" v-if="instance.followersCount > 0"><i class="fas fa-caret-down icon"></i>Sub</span>
|
||||||
|
@ -54,8 +51,6 @@
|
||||||
</div>
|
</div>
|
||||||
</MkPagination>
|
</MkPagination>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
@ -76,6 +71,8 @@ export default defineComponent({
|
||||||
MkPagination,
|
MkPagination,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
emits: ['info'],
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
[symbols.PAGE_INFO]: {
|
[symbols.PAGE_INFO]: {
|
||||||
|
@ -114,6 +111,10 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.$emit('info', this[symbols.PAGE_INFO]);
|
||||||
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
getStatus(instance) {
|
getStatus(instance) {
|
||||||
if (instance.isSuspended) return 'off';
|
if (instance.isSuspended) return 'off';
|
||||||
|
@ -131,6 +132,12 @@ export default defineComponent({
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
.enuoauvw {
|
||||||
|
> .query {
|
||||||
|
margin: var(--margin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.ppgwaixt {
|
.ppgwaixt {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
|
|
|
@ -82,9 +82,7 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
|
|
||||||
showUser() {
|
showUser() {
|
||||||
os.popup(import('./user-dialog.vue'), {
|
os.pageWindow(`/user-info/${this.file.userId}`);
|
||||||
userId: this.file.userId
|
|
||||||
}, {}, 'closed');
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async del() {
|
async del() {
|
||||||
|
|
92
src/client/pages/instance/files-settings.vue
Normal file
92
src/client/pages/instance/files-settings.vue
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
<template>
|
||||||
|
<FormBase>
|
||||||
|
<FormSuspense :p="init">
|
||||||
|
<FormSwitch v-model:value="cacheRemoteFiles">
|
||||||
|
{{ $ts.cacheRemoteFiles }}
|
||||||
|
<template #desc>{{ $ts.cacheRemoteFilesDescription }}</template>
|
||||||
|
</FormSwitch>
|
||||||
|
|
||||||
|
<FormSwitch v-model:value="proxyRemoteFiles">
|
||||||
|
{{ $ts.proxyRemoteFiles }}
|
||||||
|
<template #desc>{{ $ts.proxyRemoteFilesDescription }}</template>
|
||||||
|
</FormSwitch>
|
||||||
|
|
||||||
|
<FormInput v-model:value="localDriveCapacityMb" type="number">
|
||||||
|
<span>{{ $ts.driveCapacityPerLocalAccount }}</span>
|
||||||
|
<template #suffix>MB</template>
|
||||||
|
<template #desc>{{ $ts.inMb }}</template>
|
||||||
|
</FormInput>
|
||||||
|
|
||||||
|
<FormInput v-model:value="remoteDriveCapacityMb" type="number" :disabled="!cacheRemoteFiles">
|
||||||
|
<span>{{ $ts.driveCapacityPerRemoteAccount }}</span>
|
||||||
|
<template #suffix>MB</template>
|
||||||
|
<template #desc>{{ $ts.inMb }}</template>
|
||||||
|
</FormInput>
|
||||||
|
|
||||||
|
<FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
|
||||||
|
</FormSuspense>
|
||||||
|
</FormBase>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
import FormSwitch from '@client/components/form/switch.vue';
|
||||||
|
import FormInput from '@client/components/form/input.vue';
|
||||||
|
import FormButton from '@client/components/form/button.vue';
|
||||||
|
import FormBase from '@client/components/form/base.vue';
|
||||||
|
import FormGroup from '@client/components/form/group.vue';
|
||||||
|
import FormSuspense from '@client/components/form/suspense.vue';
|
||||||
|
import * as os from '@client/os';
|
||||||
|
import * as symbols from '@client/symbols';
|
||||||
|
import { fetchInstance } from '@client/instance';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
components: {
|
||||||
|
FormSwitch,
|
||||||
|
FormInput,
|
||||||
|
FormBase,
|
||||||
|
FormGroup,
|
||||||
|
FormButton,
|
||||||
|
FormSuspense,
|
||||||
|
},
|
||||||
|
|
||||||
|
emits: ['info'],
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
[symbols.PAGE_INFO]: {
|
||||||
|
title: this.$ts.files,
|
||||||
|
icon: 'fas fa-cloud'
|
||||||
|
},
|
||||||
|
cacheRemoteFiles: false,
|
||||||
|
proxyRemoteFiles: false,
|
||||||
|
localDriveCapacityMb: 0,
|
||||||
|
remoteDriveCapacityMb: 0,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async mounted() {
|
||||||
|
this.$emit('info', this[symbols.PAGE_INFO]);
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async init() {
|
||||||
|
const meta = await os.api('meta', { detail: true });
|
||||||
|
this.cacheRemoteFiles = meta.cacheRemoteFiles;
|
||||||
|
this.proxyRemoteFiles = meta.proxyRemoteFiles;
|
||||||
|
this.localDriveCapacityMb = meta.driveCapacityPerLocalUserMb;
|
||||||
|
this.remoteDriveCapacityMb = meta.driveCapacityPerRemoteUserMb;
|
||||||
|
},
|
||||||
|
save() {
|
||||||
|
os.apiWithDialog('admin/update-meta', {
|
||||||
|
cacheRemoteFiles: this.cacheRemoteFiles,
|
||||||
|
proxyRemoteFiles: this.proxyRemoteFiles,
|
||||||
|
localDriveCapacityMb: parseInt(this.localDriveCapacityMb, 10),
|
||||||
|
remoteDriveCapacityMb: parseInt(this.remoteDriveCapacityMb, 10),
|
||||||
|
}).then(() => {
|
||||||
|
fetchInstance();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -80,6 +80,8 @@ export default defineComponent({
|
||||||
MkDriveFileThumbnail,
|
MkDriveFileThumbnail,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
emits: ['info'],
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
[symbols.PAGE_INFO]: {
|
[symbols.PAGE_INFO]: {
|
||||||
|
@ -114,6 +116,10 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.$emit('info', this[symbols.PAGE_INFO]);
|
||||||
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
clear() {
|
clear() {
|
||||||
os.dialog({
|
os.dialog({
|
||||||
|
@ -153,6 +159,8 @@ export default defineComponent({
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.xrmjdkdw {
|
.xrmjdkdw {
|
||||||
|
margin: var(--margin);
|
||||||
|
|
||||||
.urempief {
|
.urempief {
|
||||||
margin-top: var(--margin);
|
margin-top: var(--margin);
|
||||||
|
|
||||||
|
|
|
@ -1,171 +1,243 @@
|
||||||
<template>
|
<template>
|
||||||
<div v-if="meta" v-show="page === 'index'" class="xhexznfu _section">
|
<div class="hiyeyicy" :class="{ wide: !narrow }" ref="el">
|
||||||
<MkFolder>
|
<div class="nav" v-if="!narrow || page == null">
|
||||||
<template #header><i class="fas fa-tachometer-alt"></i> {{ $ts.overview }}</template>
|
<FormBase>
|
||||||
|
<FormGroup>
|
||||||
<div class="sboqnrfi" :style="{ gridTemplateRows: overviewHeight }">
|
<div class="_formItem">
|
||||||
<MkInstanceStats :chart-limit="300" :detailed="true" class="_gap" ref="stats"/>
|
<div class="_formPanel lxpfedzu">
|
||||||
|
<img :src="$instance.iconUrl || '/favicon.ico'" alt="" class="icon"/>
|
||||||
<MkContainer :foldable="true" class="_gap">
|
|
||||||
<template #header><i class="fas fa-info-circle"></i>{{ $ts.instanceInfo }}</template>
|
|
||||||
|
|
||||||
<div class="_content">
|
|
||||||
<div class="_keyValue"><b>Misskey</b><span>v{{ version }}</span></div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="_content" v-if="serverInfo">
|
|
||||||
<div class="_keyValue"><b>Node.js</b><span>{{ serverInfo.node }}</span></div>
|
|
||||||
<div class="_keyValue"><b>PostgreSQL</b><span>v{{ serverInfo.psql }}</span></div>
|
|
||||||
<div class="_keyValue"><b>Redis</b><span>v{{ serverInfo.redis }}</span></div>
|
|
||||||
</div>
|
</div>
|
||||||
</MkContainer>
|
<FormLink :active="page === 'overview'" replace to="/instance/overview"><template #icon><i class="fas fa-tachometer-alt"></i></template>{{ $ts.overview }}</FormLink>
|
||||||
|
</FormGroup>
|
||||||
<MkContainer :foldable="true" :scrollable="true" class="_gap" style="height: 300px;">
|
<FormGroup>
|
||||||
<template #header><i class="fas fa-database"></i>{{ $ts.database }}</template>
|
<template #label>{{ $ts.quickAction }}</template>
|
||||||
|
<FormButton @click="lookup"><i class="fas fa-search"></i> {{ $ts.lookup }}</FormButton>
|
||||||
<div class="_content" v-if="dbInfo">
|
<FormButton v-if="$instance.disableRegistration" @click="invite"><i class="fas fa-user"></i> {{ $ts.invite }}</FormButton>
|
||||||
<table style="border-collapse: collapse; width: 100%;">
|
</FormGroup>
|
||||||
<tr style="opacity: 0.7;">
|
<FormGroup>
|
||||||
<th style="text-align: left; padding: 0 8px 8px 0;">Table</th>
|
<template #label>{{ $ts.administration }}</template>
|
||||||
<th style="text-align: left; padding: 0 8px 8px 0;">Records</th>
|
<FormLink :active="page === 'users'" replace to="/instance/users"><template #icon><i class="fas fa-users"></i></template>{{ $ts.users }}</FormLink>
|
||||||
<th style="text-align: left; padding: 0 0 8px 0;">Size</th>
|
<FormLink :active="page === 'emojis'" replace to="/instance/emojis"><template #icon><i class="fas fa-laugh"></i></template>{{ $ts.customEmojis }}</FormLink>
|
||||||
</tr>
|
<FormLink :active="page === 'federation'" replace to="/instance/federation"><template #icon><i class="fas fa-globe"></i></template>{{ $ts.federation }}</FormLink>
|
||||||
<tr v-for="table in dbInfo" :key="table[0]">
|
<FormLink :active="page === 'queue'" replace to="/instance/queue"><template #icon><i class="fas fa-clipboard-list"></i></template>{{ $ts.jobQueue }}</FormLink>
|
||||||
<th style="text-align: left; padding: 0 8px 0 0; word-break: break-all;">{{ table[0] }}</th>
|
<FormLink :active="page === 'files'" replace to="/instance/files"><template #icon><i class="fas fa-cloud"></i></template>{{ $ts.files }}</FormLink>
|
||||||
<td style="padding: 0 8px 0 0;">{{ number(table[1].count) }}</td>
|
<FormLink :active="page === 'announcements'" replace to="/instance/announcements"><template #icon><i class="fas fa-broadcast-tower"></i></template>{{ $ts.announcements }}</FormLink>
|
||||||
<td style="padding: 0; opacity: 0.7;">{{ bytes(table[1].size) }}</td>
|
<FormLink :active="page === 'abuses'" replace to="/instance/abuses"><template #icon><i class="fas fa-exclamation-circle"></i></template>{{ $ts.abuseReports }}</FormLink>
|
||||||
</tr>
|
</FormGroup>
|
||||||
</table>
|
<FormGroup>
|
||||||
|
<template #label>{{ $ts.settings }}</template>
|
||||||
|
<FormLink :active="page === 'settings'" replace to="/instance/settings"><template #icon><i class="fas fa-cog"></i></template>{{ $ts.general }}</FormLink>
|
||||||
|
<FormLink :active="page === 'files-settings'" replace to="/instance/files-settings"><template #icon><i class="fas fa-cloud"></i></template>{{ $ts.files }}</FormLink>
|
||||||
|
<FormLink :active="page === 'email-settings'" replace to="/instance/email-settings"><template #icon><i class="fas fa-envelope"></i></template>{{ $ts.emailServer }}</FormLink>
|
||||||
|
<FormLink :active="page === 'object-storage'" replace to="/instance/object-storage"><template #icon><i class="fas fa-cloud"></i></template>{{ $ts.objectStorage }}</FormLink>
|
||||||
|
<FormLink :active="page === 'security'" replace to="/instance/security"><template #icon><i class="fas fa-lock"></i></template>{{ $ts.security }}</FormLink>
|
||||||
|
<FormLink :active="page === 'service-worker'" replace to="/instance/service-worker"><template #icon><i class="fas fa-bolt"></i></template>ServiceWorker</FormLink>
|
||||||
|
<FormLink :active="page === 'relays'" replace to="/instance/relays"><template #icon><i class="fas fa-globe"></i></template>{{ $ts.relays }}</FormLink>
|
||||||
|
<FormLink :active="page === 'integrations'" replace to="/instance/integrations"><template #icon><i class="fas fa-share-alt"></i></template>{{ $ts.integration }}</FormLink>
|
||||||
|
<FormLink :active="page === 'instance-block'" replace to="/instance/instance-block"><template #icon><i class="fas fa-ban"></i></template>{{ $ts.instanceBlocking }}</FormLink>
|
||||||
|
<FormLink :active="page === 'proxy-account'" replace to="/instance/proxy-account"><template #icon><i class="fas fa-ghost"></i></template>{{ $ts.proxyAccount }}</FormLink>
|
||||||
|
<FormLink :active="page === 'other-settings'" replace to="/instance/other-settings"><template #icon><i class="fas fa-cogs"></i></template>{{ $ts.other }}</FormLink>
|
||||||
|
</FormGroup>
|
||||||
|
<FormGroup>
|
||||||
|
<template #label>{{ $ts.info }}</template>
|
||||||
|
<FormLink :active="page === 'database'" replace to="/instance/database"><template #icon><i class="fas fa-database"></i></template>{{ $ts.database }}</FormLink>
|
||||||
|
</FormGroup>
|
||||||
|
</FormBase>
|
||||||
</div>
|
</div>
|
||||||
</MkContainer>
|
<div class="main">
|
||||||
|
<component :is="component" :key="page" @info="onInfo" v-bind="pageProps"/>
|
||||||
</div>
|
</div>
|
||||||
</MkFolder>
|
|
||||||
</div>
|
|
||||||
<div v-if="page === 'logs'" class="_section">
|
|
||||||
<MkFolder>
|
|
||||||
<template #header><i class="fas fa-stream"></i> {{ $ts.logs }}</template>
|
|
||||||
|
|
||||||
<div class="_keyValue" v-for="log in modLogs">
|
|
||||||
<b>{{ log.type }}</b><span>by {{ log.user.username }}</span><MkTime :time="log.createdAt" style="opacity: 0.7;"/>
|
|
||||||
</div>
|
|
||||||
</MkFolder>
|
|
||||||
</div>
|
|
||||||
<div v-if="page === 'metrics'">
|
|
||||||
<XMetrics/>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { computed, defineComponent, markRaw } from 'vue';
|
import { computed, defineAsyncComponent, defineComponent, nextTick, onMounted, reactive, ref, watch } from 'vue';
|
||||||
import VueJsonPretty from 'vue-json-pretty';
|
import { i18n } from '@client/i18n';
|
||||||
import MkInstanceStats from '@client/components/instance-stats.vue';
|
import FormLink from '@client/components/form/link.vue';
|
||||||
import MkButton from '@client/components/ui/button.vue';
|
import FormGroup from '@client/components/form/group.vue';
|
||||||
import MkSelect from '@client/components/ui/select.vue';
|
import FormBase from '@client/components/form/base.vue';
|
||||||
import MkInput from '@client/components/ui/input.vue';
|
import FormButton from '@client/components/form/button.vue';
|
||||||
import MkContainer from '@client/components/ui/container.vue';
|
import { scroll } from '@client/scripts/scroll';
|
||||||
import MkFolder from '@client/components/ui/folder.vue';
|
|
||||||
import { version, url } from '@client/config';
|
|
||||||
import bytes from '../../filters/bytes';
|
|
||||||
import number from '../../filters/number';
|
|
||||||
import MkInstanceInfo from './instance.vue';
|
|
||||||
import XMetrics from './index.metrics.vue';
|
|
||||||
import * as os from '@client/os';
|
|
||||||
import * as symbols from '@client/symbols';
|
import * as symbols from '@client/symbols';
|
||||||
|
import * as os from '@client/os';
|
||||||
|
import { lookupUser } from '@client/scripts/lookup-user';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
MkInstanceStats,
|
FormBase,
|
||||||
MkButton,
|
FormLink,
|
||||||
MkSelect,
|
FormGroup,
|
||||||
MkInput,
|
FormButton,
|
||||||
MkContainer,
|
|
||||||
MkFolder,
|
|
||||||
XMetrics,
|
|
||||||
VueJsonPretty,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
props: {
|
||||||
|
initialPage: {
|
||||||
|
type: String,
|
||||||
|
required: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
setup(props, context) {
|
||||||
|
const indexInfo = {
|
||||||
|
title: i18n.locale.instance,
|
||||||
|
icon: 'fas fa-cog'
|
||||||
|
};
|
||||||
|
const INFO = ref(indexInfo);
|
||||||
|
const page = ref(props.initialPage);
|
||||||
|
const narrow = ref(false);
|
||||||
|
const view = ref(null);
|
||||||
|
const el = ref(null);
|
||||||
|
const onInfo = (viewInfo) => {
|
||||||
|
INFO.value = viewInfo;
|
||||||
|
};
|
||||||
|
const pageProps = ref({});
|
||||||
|
const component = computed(() => {
|
||||||
|
if (page.value == null) return null;
|
||||||
|
switch (page.value) {
|
||||||
|
case 'overview': return defineAsyncComponent(() => import('./overview.vue'));
|
||||||
|
case 'users': return defineAsyncComponent(() => import('./users.vue'));
|
||||||
|
case 'emojis': return defineAsyncComponent(() => import('./emojis.vue'));
|
||||||
|
case 'federation': return defineAsyncComponent(() => import('./federation.vue'));
|
||||||
|
case 'queue': return defineAsyncComponent(() => import('./queue.vue'));
|
||||||
|
case 'files': return defineAsyncComponent(() => import('./files.vue'));
|
||||||
|
case 'announcements': return defineAsyncComponent(() => import('./announcements.vue'));
|
||||||
|
case 'database': return defineAsyncComponent(() => import('./database.vue'));
|
||||||
|
case 'abuses': return defineAsyncComponent(() => import('./abuses.vue'));
|
||||||
|
case 'settings': return defineAsyncComponent(() => import('./settings.vue'));
|
||||||
|
case 'files-settings': return defineAsyncComponent(() => import('./files-settings.vue'));
|
||||||
|
case 'email-settings': return defineAsyncComponent(() => import('./email-settings.vue'));
|
||||||
|
case 'object-storage': return defineAsyncComponent(() => import('./object-storage.vue'));
|
||||||
|
case 'security': return defineAsyncComponent(() => import('./security.vue'));
|
||||||
|
case 'bot-protection': return defineAsyncComponent(() => import('./bot-protection.vue'));
|
||||||
|
case 'service-worker': return defineAsyncComponent(() => import('./service-worker.vue'));
|
||||||
|
case 'relays': return defineAsyncComponent(() => import('./relays.vue'));
|
||||||
|
case 'integrations': return defineAsyncComponent(() => import('./integrations.vue'));
|
||||||
|
case 'integrations/twitter': return defineAsyncComponent(() => import('./integrations-twitter.vue'));
|
||||||
|
case 'integrations/github': return defineAsyncComponent(() => import('./integrations-github.vue'));
|
||||||
|
case 'integrations/discord': return defineAsyncComponent(() => import('./integrations-discord.vue'));
|
||||||
|
case 'instance-block': return defineAsyncComponent(() => import('./instance-block.vue'));
|
||||||
|
case 'proxy-account': return defineAsyncComponent(() => import('./proxy-account.vue'));
|
||||||
|
case 'other-settings': return defineAsyncComponent(() => import('./other-settings.vue'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(component, () => {
|
||||||
|
pageProps.value = {};
|
||||||
|
|
||||||
|
nextTick(() => {
|
||||||
|
scroll(el.value, 0);
|
||||||
|
});
|
||||||
|
}, { immediate: true });
|
||||||
|
|
||||||
|
watch(() => props.initialPage, () => {
|
||||||
|
if (props.initialPage == null && !narrow.value) {
|
||||||
|
page.value = 'overview';
|
||||||
|
} else {
|
||||||
|
page.value = props.initialPage;
|
||||||
|
if (props.initialPage == null) {
|
||||||
|
INFO.value = indexInfo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
narrow.value = el.value.offsetWidth < 800;
|
||||||
|
if (!narrow.value) {
|
||||||
|
page.value = 'overview';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const invite = () => {
|
||||||
|
os.api('admin/invite').then(x => {
|
||||||
|
os.dialog({
|
||||||
|
type: 'info',
|
||||||
|
text: x.code
|
||||||
|
});
|
||||||
|
}).catch(e => {
|
||||||
|
os.dialog({
|
||||||
|
type: 'error',
|
||||||
|
text: e
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const lookup = (ev) => {
|
||||||
|
os.modalMenu([{
|
||||||
|
text: i18n.locale.user,
|
||||||
|
icon: 'fas fa-user',
|
||||||
|
action: () => {
|
||||||
|
lookupUser();
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
text: i18n.locale.note,
|
||||||
|
icon: 'fas fa-pencil-alt',
|
||||||
|
action: () => {
|
||||||
|
alert('TODO');
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
text: i18n.locale.file,
|
||||||
|
icon: 'fas fa-cloud',
|
||||||
|
action: () => {
|
||||||
|
alert('TODO');
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
text: i18n.locale.instance,
|
||||||
|
icon: 'fas fa-globe',
|
||||||
|
action: () => {
|
||||||
|
alert('TODO');
|
||||||
|
}
|
||||||
|
}], ev.currentTarget || ev.target);
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
[symbols.PAGE_INFO]: {
|
[symbols.PAGE_INFO]: INFO,
|
||||||
tabs: [{
|
page,
|
||||||
id: 'index',
|
narrow,
|
||||||
title: null,
|
view,
|
||||||
tooltip: this.$ts.instance,
|
el,
|
||||||
icon: 'fas fa-server',
|
onInfo,
|
||||||
onClick: () => { this.page = 'index'; },
|
pageProps,
|
||||||
selected: computed(() => this.page === 'index')
|
component,
|
||||||
}, {
|
invite,
|
||||||
id: 'metrics',
|
lookup,
|
||||||
title: null,
|
};
|
||||||
tooltip: this.$ts.metrics,
|
|
||||||
icon: 'fas fa-heartbeat',
|
|
||||||
onClick: () => { this.page = 'metrics'; },
|
|
||||||
selected: computed(() => this.page === 'metrics')
|
|
||||||
}, {
|
|
||||||
id: 'logs',
|
|
||||||
title: null,
|
|
||||||
tooltip: this.$ts.logs,
|
|
||||||
icon: 'fas fa-stream',
|
|
||||||
onClick: () => { this.page = 'logs'; },
|
|
||||||
selected: computed(() => this.page === 'logs')
|
|
||||||
}]
|
|
||||||
},
|
},
|
||||||
page: 'index',
|
|
||||||
version,
|
|
||||||
url,
|
|
||||||
stats: null,
|
|
||||||
serverInfo: null,
|
|
||||||
modLogs: [],
|
|
||||||
dbInfo: null,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
meta() {
|
|
||||||
return this.$instance;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
mounted() {
|
|
||||||
this.fetchJobs();
|
|
||||||
this.fetchModLogs();
|
|
||||||
|
|
||||||
os.api('admin/server-info', {}).then(res => {
|
|
||||||
this.serverInfo = res;
|
|
||||||
});
|
|
||||||
|
|
||||||
os.api('admin/get-table-stats', {}).then(res => {
|
|
||||||
this.dbInfo = Object.entries(res).sort((a, b) => b[1].size - a[1].size);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
async showInstanceInfo(q) {
|
|
||||||
let instance = q;
|
|
||||||
if (typeof q === 'string') {
|
|
||||||
instance = await os.api('federation/show-instance', {
|
|
||||||
host: q
|
|
||||||
});
|
|
||||||
}
|
|
||||||
os.popup(MkInstanceInfo, {
|
|
||||||
instance: instance
|
|
||||||
}, {}, 'closed');
|
|
||||||
},
|
|
||||||
|
|
||||||
fetchJobs() {
|
|
||||||
os.api('admin/queue/deliver-delayed', {}).then(jobs => {
|
|
||||||
this.jobs = jobs;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
fetchModLogs() {
|
|
||||||
os.api('admin/show-moderation-logs', {}).then(logs => {
|
|
||||||
this.modLogs = logs;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
bytes,
|
|
||||||
|
|
||||||
number,
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.hiyeyicy {
|
||||||
|
&.wide {
|
||||||
|
display: flex;
|
||||||
|
max-width: 1100px;
|
||||||
|
margin: 0 auto;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
> .nav {
|
||||||
|
width: 32%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-right: solid 0.5px var(--divider);
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .main {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: auto;
|
||||||
|
--baseContentWidth: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lxpfedzu {
|
||||||
|
padding: 16px;
|
||||||
|
|
||||||
|
> .icon {
|
||||||
|
display: block;
|
||||||
|
margin: auto;
|
||||||
|
height: 42px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
71
src/client/pages/instance/instance-block.vue
Normal file
71
src/client/pages/instance/instance-block.vue
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
<template>
|
||||||
|
<FormBase>
|
||||||
|
<FormSuspense :p="init">
|
||||||
|
<FormTextarea v-model:value="blockedHosts">
|
||||||
|
<span>{{ $ts.blockedInstances }}</span>
|
||||||
|
<template #desc>{{ $ts.blockedInstancesDescription }}</template>
|
||||||
|
</FormTextarea>
|
||||||
|
|
||||||
|
<FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
|
||||||
|
</FormSuspense>
|
||||||
|
</FormBase>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
import FormSwitch from '@client/components/form/switch.vue';
|
||||||
|
import FormInput from '@client/components/form/input.vue';
|
||||||
|
import FormButton from '@client/components/form/button.vue';
|
||||||
|
import FormBase from '@client/components/form/base.vue';
|
||||||
|
import FormGroup from '@client/components/form/group.vue';
|
||||||
|
import FormTextarea from '@client/components/form/textarea.vue';
|
||||||
|
import FormInfo from '@client/components/form/info.vue';
|
||||||
|
import FormSuspense from '@client/components/form/suspense.vue';
|
||||||
|
import * as os from '@client/os';
|
||||||
|
import * as symbols from '@client/symbols';
|
||||||
|
import { fetchInstance } from '@client/instance';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
components: {
|
||||||
|
FormSwitch,
|
||||||
|
FormInput,
|
||||||
|
FormBase,
|
||||||
|
FormGroup,
|
||||||
|
FormButton,
|
||||||
|
FormTextarea,
|
||||||
|
FormInfo,
|
||||||
|
FormSuspense,
|
||||||
|
},
|
||||||
|
|
||||||
|
emits: ['info'],
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
[symbols.PAGE_INFO]: {
|
||||||
|
title: this.$ts.instanceBlocking,
|
||||||
|
icon: 'fas fa-ban'
|
||||||
|
},
|
||||||
|
blockedHosts: '',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async mounted() {
|
||||||
|
this.$emit('info', this[symbols.PAGE_INFO]);
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async init() {
|
||||||
|
const meta = await os.api('meta', { detail: true });
|
||||||
|
this.blockedHosts = meta.blockedHosts.join('\n');
|
||||||
|
},
|
||||||
|
|
||||||
|
save() {
|
||||||
|
os.apiWithDialog('admin/update-meta', {
|
||||||
|
blockedHosts: this.blockedHosts.split('\n') || [],
|
||||||
|
}).then(() => {
|
||||||
|
fetchInstance();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
85
src/client/pages/instance/integrations-discord.vue
Normal file
85
src/client/pages/instance/integrations-discord.vue
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
<template>
|
||||||
|
<FormBase>
|
||||||
|
<FormSuspense :p="init">
|
||||||
|
<FormSwitch v-model:value="enableDiscordIntegration">
|
||||||
|
{{ $ts.enable }}
|
||||||
|
</FormSwitch>
|
||||||
|
|
||||||
|
<template v-if="enableDiscordIntegration">
|
||||||
|
<FormInfo>Callback URL: {{ `${url}/api/dc/cb` }}</FormInfo>
|
||||||
|
|
||||||
|
<FormInput v-model:value="discordClientId">
|
||||||
|
<template #prefix><i class="fas fa-key"></i></template>
|
||||||
|
Client ID
|
||||||
|
</FormInput>
|
||||||
|
|
||||||
|
<FormInput v-model:value="discordClientSecret">
|
||||||
|
<template #prefix><i class="fas fa-key"></i></template>
|
||||||
|
Client Secret
|
||||||
|
</FormInput>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
|
||||||
|
</FormSuspense>
|
||||||
|
</FormBase>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
import FormSwitch from '@client/components/form/switch.vue';
|
||||||
|
import FormInput from '@client/components/form/input.vue';
|
||||||
|
import FormButton from '@client/components/form/button.vue';
|
||||||
|
import FormBase from '@client/components/form/base.vue';
|
||||||
|
import FormInfo from '@client/components/form/info.vue';
|
||||||
|
import FormSuspense from '@client/components/form/suspense.vue';
|
||||||
|
import * as os from '@client/os';
|
||||||
|
import * as symbols from '@client/symbols';
|
||||||
|
import { fetchInstance } from '@client/instance';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
components: {
|
||||||
|
FormSwitch,
|
||||||
|
FormInput,
|
||||||
|
FormBase,
|
||||||
|
FormInfo,
|
||||||
|
FormButton,
|
||||||
|
FormSuspense,
|
||||||
|
},
|
||||||
|
|
||||||
|
emits: ['info'],
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
[symbols.PAGE_INFO]: {
|
||||||
|
title: 'Discord',
|
||||||
|
icon: 'fab fa-discord'
|
||||||
|
},
|
||||||
|
enableDiscordIntegration: false,
|
||||||
|
discordClientId: null,
|
||||||
|
discordClientSecret: null,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async mounted() {
|
||||||
|
this.$emit('info', this[symbols.PAGE_INFO]);
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async init() {
|
||||||
|
const meta = await os.api('meta', { detail: true });
|
||||||
|
this.enableDiscordIntegration = meta.enableDiscordIntegration;
|
||||||
|
this.discordClientId = meta.discordClientId;
|
||||||
|
this.discordClientSecret = meta.discordClientSecret;
|
||||||
|
},
|
||||||
|
save() {
|
||||||
|
os.apiWithDialog('admin/update-meta', {
|
||||||
|
enableDiscordIntegration: this.enableDiscordIntegration,
|
||||||
|
discordClientId: this.discordClientId,
|
||||||
|
discordClientSecret: this.discordClientSecret,
|
||||||
|
}).then(() => {
|
||||||
|
fetchInstance();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
85
src/client/pages/instance/integrations-github.vue
Normal file
85
src/client/pages/instance/integrations-github.vue
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
<template>
|
||||||
|
<FormBase>
|
||||||
|
<FormSuspense :p="init">
|
||||||
|
<FormSwitch v-model:value="enableGithubIntegration">
|
||||||
|
{{ $ts.enable }}
|
||||||
|
</FormSwitch>
|
||||||
|
|
||||||
|
<template v-if="enableGithubIntegration">
|
||||||
|
<FormInfo>Callback URL: {{ `${url}/api/gh/cb` }}</FormInfo>
|
||||||
|
|
||||||
|
<FormInput v-model:value="githubClientId">
|
||||||
|
<template #prefix><i class="fas fa-key"></i></template>
|
||||||
|
Client ID
|
||||||
|
</FormInput>
|
||||||
|
|
||||||
|
<FormInput v-model:value="githubClientSecret">
|
||||||
|
<template #prefix><i class="fas fa-key"></i></template>
|
||||||
|
Client Secret
|
||||||
|
</FormInput>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
|
||||||
|
</FormSuspense>
|
||||||
|
</FormBase>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
import FormSwitch from '@client/components/form/switch.vue';
|
||||||
|
import FormInput from '@client/components/form/input.vue';
|
||||||
|
import FormButton from '@client/components/form/button.vue';
|
||||||
|
import FormBase from '@client/components/form/base.vue';
|
||||||
|
import FormInfo from '@client/components/form/info.vue';
|
||||||
|
import FormSuspense from '@client/components/form/suspense.vue';
|
||||||
|
import * as os from '@client/os';
|
||||||
|
import * as symbols from '@client/symbols';
|
||||||
|
import { fetchInstance } from '@client/instance';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
components: {
|
||||||
|
FormSwitch,
|
||||||
|
FormInput,
|
||||||
|
FormBase,
|
||||||
|
FormInfo,
|
||||||
|
FormButton,
|
||||||
|
FormSuspense,
|
||||||
|
},
|
||||||
|
|
||||||
|
emits: ['info'],
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
[symbols.PAGE_INFO]: {
|
||||||
|
title: 'GitHub',
|
||||||
|
icon: 'fab fa-github'
|
||||||
|
},
|
||||||
|
enableGithubIntegration: false,
|
||||||
|
githubClientId: null,
|
||||||
|
githubClientSecret: null,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async mounted() {
|
||||||
|
this.$emit('info', this[symbols.PAGE_INFO]);
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async init() {
|
||||||
|
const meta = await os.api('meta', { detail: true });
|
||||||
|
this.enableGithubIntegration = meta.enableGithubIntegration;
|
||||||
|
this.githubClientId = meta.githubClientId;
|
||||||
|
this.githubClientSecret = meta.githubClientSecret;
|
||||||
|
},
|
||||||
|
save() {
|
||||||
|
os.apiWithDialog('admin/update-meta', {
|
||||||
|
enableGithubIntegration: this.enableGithubIntegration,
|
||||||
|
githubClientId: this.githubClientId,
|
||||||
|
githubClientSecret: this.githubClientSecret,
|
||||||
|
}).then(() => {
|
||||||
|
fetchInstance();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
85
src/client/pages/instance/integrations-twitter.vue
Normal file
85
src/client/pages/instance/integrations-twitter.vue
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
<template>
|
||||||
|
<FormBase>
|
||||||
|
<FormSuspense :p="init">
|
||||||
|
<FormSwitch v-model:value="enableTwitterIntegration">
|
||||||
|
{{ $ts.enable }}
|
||||||
|
</FormSwitch>
|
||||||
|
|
||||||
|
<template v-if="enableTwitterIntegration">
|
||||||
|
<FormInfo>Callback URL: {{ `${url}/api/tw/cb` }}</FormInfo>
|
||||||
|
|
||||||
|
<FormInput v-model:value="twitterConsumerKey">
|
||||||
|
<template #prefix><i class="fas fa-key"></i></template>
|
||||||
|
Consumer Key
|
||||||
|
</FormInput>
|
||||||
|
|
||||||
|
<FormInput v-model:value="twitterConsumerSecret">
|
||||||
|
<template #prefix><i class="fas fa-key"></i></template>
|
||||||
|
Consumer Secret
|
||||||
|
</FormInput>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
|
||||||
|
</FormSuspense>
|
||||||
|
</FormBase>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
import FormSwitch from '@client/components/form/switch.vue';
|
||||||
|
import FormInput from '@client/components/form/input.vue';
|
||||||
|
import FormButton from '@client/components/form/button.vue';
|
||||||
|
import FormBase from '@client/components/form/base.vue';
|
||||||
|
import FormInfo from '@client/components/form/info.vue';
|
||||||
|
import FormSuspense from '@client/components/form/suspense.vue';
|
||||||
|
import * as os from '@client/os';
|
||||||
|
import * as symbols from '@client/symbols';
|
||||||
|
import { fetchInstance } from '@client/instance';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
components: {
|
||||||
|
FormSwitch,
|
||||||
|
FormInput,
|
||||||
|
FormBase,
|
||||||
|
FormInfo,
|
||||||
|
FormButton,
|
||||||
|
FormSuspense,
|
||||||
|
},
|
||||||
|
|
||||||
|
emits: ['info'],
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
[symbols.PAGE_INFO]: {
|
||||||
|
title: 'Twitter',
|
||||||
|
icon: 'fab fa-twitter'
|
||||||
|
},
|
||||||
|
enableTwitterIntegration: false,
|
||||||
|
twitterConsumerKey: null,
|
||||||
|
twitterConsumerSecret: null,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async mounted() {
|
||||||
|
this.$emit('info', this[symbols.PAGE_INFO]);
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async init() {
|
||||||
|
const meta = await os.api('meta', { detail: true });
|
||||||
|
this.enableTwitterIntegration = meta.enableTwitterIntegration;
|
||||||
|
this.twitterConsumerKey = meta.twitterConsumerKey;
|
||||||
|
this.twitterConsumerSecret = meta.twitterConsumerSecret;
|
||||||
|
},
|
||||||
|
save() {
|
||||||
|
os.apiWithDialog('admin/update-meta', {
|
||||||
|
enableTwitterIntegration: this.enableTwitterIntegration,
|
||||||
|
twitterConsumerKey: this.twitterConsumerKey,
|
||||||
|
twitterConsumerSecret: this.twitterConsumerSecret,
|
||||||
|
}).then(() => {
|
||||||
|
fetchInstance();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
73
src/client/pages/instance/integrations.vue
Normal file
73
src/client/pages/instance/integrations.vue
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
<template>
|
||||||
|
<FormBase>
|
||||||
|
<FormSuspense :p="init">
|
||||||
|
<FormLink to="/instance/integrations/twitter">
|
||||||
|
<i class="fab fa-twitter"></i> Twitter
|
||||||
|
<template #suffix>{{ enableTwitterIntegration ? $ts.enabled : $ts.disabled }}</template>
|
||||||
|
</FormLink>
|
||||||
|
<FormLink to="/instance/integrations/github">
|
||||||
|
<i class="fab fa-github"></i> GitHub
|
||||||
|
<template #suffix>{{ enableGithubIntegration ? $ts.enabled : $ts.disabled }}</template>
|
||||||
|
</FormLink>
|
||||||
|
<FormLink to="/instance/integrations/discord">
|
||||||
|
<i class="fab fa-discord"></i> Discord
|
||||||
|
<template #suffix>{{ enableDiscordIntegration ? $ts.enabled : $ts.disabled }}</template>
|
||||||
|
</FormLink>
|
||||||
|
</FormSuspense>
|
||||||
|
</FormBase>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
import FormLink from '@client/components/form/link.vue';
|
||||||
|
import FormInput from '@client/components/form/input.vue';
|
||||||
|
import FormButton from '@client/components/form/button.vue';
|
||||||
|
import FormBase from '@client/components/form/base.vue';
|
||||||
|
import FormGroup from '@client/components/form/group.vue';
|
||||||
|
import FormTextarea from '@client/components/form/textarea.vue';
|
||||||
|
import FormInfo from '@client/components/form/info.vue';
|
||||||
|
import FormSuspense from '@client/components/form/suspense.vue';
|
||||||
|
import * as os from '@client/os';
|
||||||
|
import * as symbols from '@client/symbols';
|
||||||
|
import { fetchInstance } from '@client/instance';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
components: {
|
||||||
|
FormLink,
|
||||||
|
FormInput,
|
||||||
|
FormBase,
|
||||||
|
FormGroup,
|
||||||
|
FormButton,
|
||||||
|
FormTextarea,
|
||||||
|
FormInfo,
|
||||||
|
FormSuspense,
|
||||||
|
},
|
||||||
|
|
||||||
|
emits: ['info'],
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
[symbols.PAGE_INFO]: {
|
||||||
|
title: this.$ts.integration,
|
||||||
|
icon: 'fas fa-share-alt'
|
||||||
|
},
|
||||||
|
enableTwitterIntegration: false,
|
||||||
|
enableGithubIntegration: false,
|
||||||
|
enableDiscordIntegration: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async mounted() {
|
||||||
|
this.$emit('info', this[symbols.PAGE_INFO]);
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async init() {
|
||||||
|
const meta = await os.api('meta', { detail: true });
|
||||||
|
this.enableTwitterIntegration = meta.enableTwitterIntegration;
|
||||||
|
this.enableGithubIntegration = meta.enableGithubIntegration;
|
||||||
|
this.enableDiscordIntegration = meta.enableDiscordIntegration;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -1,22 +1,11 @@
|
||||||
<template>
|
<template>
|
||||||
|
<div class="_formItem">
|
||||||
|
<div class="_formLabel"><i class="fas fa-microchip"></i> {{ $ts.cpuAndMemory }}</div>
|
||||||
|
<div class="_formPanel xhexznfu">
|
||||||
<div>
|
<div>
|
||||||
<MkFolder>
|
|
||||||
<template #header><i class="fas fa-heartbeat"></i> {{ $ts.metrics }}</template>
|
|
||||||
<div class="_section" style="padding: 0 var(--margin);">
|
|
||||||
<div class="_content">
|
|
||||||
<MkContainer :foldable="false" class="_gap">
|
|
||||||
<template #header><i class="fas fa-microchip"></i>{{ $ts.cpuAndMemory }}</template>
|
|
||||||
<!--
|
|
||||||
<template #func>
|
|
||||||
<button class="_button" @click="resume" :disabled="!paused"><i class="fas fa-play"></i></button>
|
|
||||||
<button class="_button" @click="pause" :disabled="paused"><i class="fas fa-pause"></i></button>
|
|
||||||
</template>
|
|
||||||
-->
|
|
||||||
|
|
||||||
<div class="_content" style="margin-top: -8px; margin-bottom: -12px;">
|
|
||||||
<canvas :ref="cpumem"></canvas>
|
<canvas :ref="cpumem"></canvas>
|
||||||
</div>
|
</div>
|
||||||
<div class="_content" v-if="serverInfo">
|
<div v-if="serverInfo">
|
||||||
<div class="_table">
|
<div class="_table">
|
||||||
<div class="_row">
|
<div class="_row">
|
||||||
<div class="_cell"><div class="_label">MEM total</div>{{ bytes(serverInfo.mem.total) }}</div>
|
<div class="_cell"><div class="_label">MEM total</div>{{ bytes(serverInfo.mem.total) }}</div>
|
||||||
|
@ -25,21 +14,15 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</MkContainer>
|
</div>
|
||||||
|
</div>
|
||||||
<MkContainer :foldable="false" class="_gap">
|
<div class="_formItem">
|
||||||
<template #header><i class="fas fa-hdd"></i> {{ $ts.disk }}</template>
|
<div class="_formLabel"><i class="fas fa-hdd"></i> {{ $ts.disk }}</div>
|
||||||
<!--
|
<div class="_formPanel xhexznfu">
|
||||||
<template #func>
|
<div>
|
||||||
<button class="_button" @click="resume" :disabled="!paused"><i class="fas fa-play"></i></button>
|
|
||||||
<button class="_button" @click="pause" :disabled="paused"><i class="fas fa-pause"></i></button>
|
|
||||||
</template>
|
|
||||||
-->
|
|
||||||
|
|
||||||
<div class="_content" style="margin-top: -8px; margin-bottom: -12px;">
|
|
||||||
<canvas :ref="disk"></canvas>
|
<canvas :ref="disk"></canvas>
|
||||||
</div>
|
</div>
|
||||||
<div class="_content" v-if="serverInfo">
|
<div v-if="serverInfo">
|
||||||
<div class="_table">
|
<div class="_table">
|
||||||
<div class="_row">
|
<div class="_row">
|
||||||
<div class="_cell"><div class="_label">Disk total</div>{{ bytes(serverInfo.fs.total) }}</div>
|
<div class="_cell"><div class="_label">Disk total</div>{{ bytes(serverInfo.fs.total) }}</div>
|
||||||
|
@ -48,55 +31,23 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</MkContainer>
|
</div>
|
||||||
|
</div>
|
||||||
<MkContainer :foldable="false" class="_gap">
|
<div class="_formItem">
|
||||||
<template #header><i class="fas fa-exchange-alt"></i> {{ $ts.network }}</template>
|
<div class="_formLabel"><i class="fas fa-exchange-alt"></i> {{ $ts.network }}</div>
|
||||||
<!--
|
<div class="_formPanel xhexznfu">
|
||||||
<template #func>
|
<div>
|
||||||
<button class="_button" @click="resume" :disabled="!paused"><i class="fas fa-play"></i></button>
|
|
||||||
<button class="_button" @click="pause" :disabled="paused"><i class="fas fa-pause"></i></button>
|
|
||||||
</template>
|
|
||||||
-->
|
|
||||||
|
|
||||||
<div class="_content" style="margin-top: -8px; margin-bottom: -12px;">
|
|
||||||
<canvas :ref="net"></canvas>
|
<canvas :ref="net"></canvas>
|
||||||
</div>
|
</div>
|
||||||
<div class="_content" v-if="serverInfo">
|
<div v-if="serverInfo">
|
||||||
<div class="_table">
|
<div class="_table">
|
||||||
<div class="_row">
|
<div class="_row">
|
||||||
<div class="_cell"><div class="_label">Interface</div>{{ serverInfo.net.interface }}</div>
|
<div class="_cell"><div class="_label">Interface</div>{{ serverInfo.net.interface }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</MkContainer>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</MkFolder>
|
|
||||||
|
|
||||||
<MkFolder>
|
|
||||||
<template #header><i class="fas fa-clipboard-list"></i> {{ $ts.jobQueue }}</template>
|
|
||||||
|
|
||||||
<div class="vkyrmkwb" :style="{ gridTemplateRows: queueHeight }">
|
|
||||||
<MkContainer :foldable="false" :scrollable="true" :resize-base-el="() => $el">
|
|
||||||
<template #header><i class="fas fa-exclamation-triangle"></i> {{ $ts.delayed }}</template>
|
|
||||||
|
|
||||||
<div class="_content">
|
|
||||||
<div class="_keyValue" v-for="job in jobs" :key="job[0]">
|
|
||||||
<button class="_button" @click="showInstanceInfo(job[0])">{{ job[0] }}</button>
|
|
||||||
<div style="text-align: right;">{{ number(job[1]) }} jobs</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</MkContainer>
|
|
||||||
<XQueue :connection="queueConnection" domain="inbox" ref="queue" class="queue">
|
|
||||||
<template #title><i class="fas fa-exchange-alt"></i> In</template>
|
|
||||||
</XQueue>
|
|
||||||
<XQueue :connection="queueConnection" domain="deliver" class="queue">
|
|
||||||
<template #title><i class="fas fa-exchange-alt"></i> Out</template>
|
|
||||||
</XQueue>
|
|
||||||
</div>
|
|
||||||
</MkFolder>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
@ -188,9 +139,11 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
|
|
||||||
beforeUnmount() {
|
beforeUnmount() {
|
||||||
|
if (this.connection) {
|
||||||
this.connection.off('stats', this.onStats);
|
this.connection.off('stats', this.onStats);
|
||||||
this.connection.off('statsLog', this.onStatsLog);
|
this.connection.off('statsLog', this.onStatsLog);
|
||||||
this.connection.dispose();
|
this.connection.dispose();
|
||||||
|
}
|
||||||
this.queueConnection.dispose();
|
this.queueConnection.dispose();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -232,9 +185,9 @@ export default defineComponent({
|
||||||
aspectRatio: 3,
|
aspectRatio: 3,
|
||||||
layout: {
|
layout: {
|
||||||
padding: {
|
padding: {
|
||||||
left: 0,
|
left: 16,
|
||||||
right: 0,
|
right: 16,
|
||||||
top: 8,
|
top: 16,
|
||||||
bottom: 0
|
bottom: 0
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -304,9 +257,9 @@ export default defineComponent({
|
||||||
aspectRatio: 3,
|
aspectRatio: 3,
|
||||||
layout: {
|
layout: {
|
||||||
padding: {
|
padding: {
|
||||||
left: 0,
|
left: 16,
|
||||||
right: 0,
|
right: 16,
|
||||||
top: 8,
|
top: 16,
|
||||||
bottom: 0
|
bottom: 0
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -375,9 +328,9 @@ export default defineComponent({
|
||||||
aspectRatio: 3,
|
aspectRatio: 3,
|
||||||
layout: {
|
layout: {
|
||||||
padding: {
|
padding: {
|
||||||
left: 0,
|
left: 16,
|
||||||
right: 0,
|
right: 16,
|
||||||
top: 8,
|
top: 16,
|
||||||
bottom: 0
|
bottom: 0
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -494,81 +447,9 @@ export default defineComponent({
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.xhexznfu {
|
.xhexznfu {
|
||||||
&.min-width_1000px {
|
> div:nth-child(2) {
|
||||||
.sboqnrfi {
|
padding: 16px;
|
||||||
display: grid;
|
border-top: solid 0.5px var(--divider);
|
||||||
grid-template-columns: 3.2fr 1fr;
|
|
||||||
grid-template-rows: 1fr;
|
|
||||||
gap: 16px 16px;
|
|
||||||
|
|
||||||
> .stats {
|
|
||||||
height: min-content;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .column {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
> .info {
|
|
||||||
flex-shrink: 0;
|
|
||||||
flex-grow: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .db {
|
|
||||||
flex: 1;
|
|
||||||
flex-grow: 0;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .fed {
|
|
||||||
flex: 1;
|
|
||||||
flex-grow: 0;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
> *:not(:last-child) {
|
|
||||||
margin-bottom: var(--margin);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.segusily {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr 1fr;
|
|
||||||
grid-template-rows: 1fr;
|
|
||||||
gap: 16px 16px;
|
|
||||||
padding: 0 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vkyrmkwb {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 0.5fr 1fr 1fr;
|
|
||||||
grid-template-rows: 1fr;
|
|
||||||
gap: 16px 16px;
|
|
||||||
margin-bottom: var(--margin);
|
|
||||||
|
|
||||||
> .queue {
|
|
||||||
height: min-content;
|
|
||||||
}
|
|
||||||
|
|
||||||
> * {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.uwuemslx {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 2fr 3fr;
|
|
||||||
grid-template-rows: 1fr;
|
|
||||||
gap: 16px 16px;
|
|
||||||
height: 400px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.vkyrmkwb {
|
|
||||||
> * {
|
|
||||||
margin-bottom: var(--margin);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
154
src/client/pages/instance/object-storage.vue
Normal file
154
src/client/pages/instance/object-storage.vue
Normal file
|
@ -0,0 +1,154 @@
|
||||||
|
<template>
|
||||||
|
<FormBase>
|
||||||
|
<FormSuspense :p="init">
|
||||||
|
<FormSwitch v-model:value="useObjectStorage">{{ $ts.useObjectStorage }}</FormSwitch>
|
||||||
|
|
||||||
|
<template v-if="useObjectStorage">
|
||||||
|
<FormInput v-model:value="objectStorageBaseUrl">
|
||||||
|
<span>{{ $ts.objectStorageBaseUrl }}</span>
|
||||||
|
<template #desc>{{ $ts.objectStorageBaseUrlDesc }}</template>
|
||||||
|
</FormInput>
|
||||||
|
|
||||||
|
<FormInput v-model:value="objectStorageBucket">
|
||||||
|
<span>{{ $ts.objectStorageBucket }}</span>
|
||||||
|
<template #desc>{{ $ts.objectStorageBucketDesc }}</template>
|
||||||
|
</FormInput>
|
||||||
|
|
||||||
|
<FormInput v-model:value="objectStoragePrefix">
|
||||||
|
<span>{{ $ts.objectStoragePrefix }}</span>
|
||||||
|
<template #desc>{{ $ts.objectStoragePrefixDesc }}</template>
|
||||||
|
</FormInput>
|
||||||
|
|
||||||
|
<FormInput v-model:value="objectStorageEndpoint">
|
||||||
|
<span>{{ $ts.objectStorageEndpoint }}</span>
|
||||||
|
<template #desc>{{ $ts.objectStorageEndpointDesc }}</template>
|
||||||
|
</FormInput>
|
||||||
|
|
||||||
|
<FormInput v-model:value="objectStorageRegion">
|
||||||
|
<span>{{ $ts.objectStorageRegion }}</span>
|
||||||
|
<template #desc>{{ $ts.objectStorageRegionDesc }}</template>
|
||||||
|
</FormInput>
|
||||||
|
|
||||||
|
<FormInput v-model:value="objectStorageAccessKey">
|
||||||
|
<template #prefix><i class="fas fa-key"></i></template>
|
||||||
|
<span>Access key</span>
|
||||||
|
</FormInput>
|
||||||
|
|
||||||
|
<FormInput v-model:value="objectStorageSecretKey">
|
||||||
|
<template #prefix><i class="fas fa-key"></i></template>
|
||||||
|
<span>Secret key</span>
|
||||||
|
</FormInput>
|
||||||
|
|
||||||
|
<FormSwitch v-model:value="objectStorageUseSSL">
|
||||||
|
{{ $ts.objectStorageUseSSL }}
|
||||||
|
<template #desc>{{ $ts.objectStorageUseSSLDesc }}</template>
|
||||||
|
</FormSwitch>
|
||||||
|
|
||||||
|
<FormSwitch v-model:value="objectStorageUseProxy">
|
||||||
|
{{ $ts.objectStorageUseProxy }}
|
||||||
|
<template #desc>{{ $ts.objectStorageUseProxyDesc }}</template>
|
||||||
|
</FormSwitch>
|
||||||
|
|
||||||
|
<FormSwitch v-model:value="objectStorageSetPublicRead">
|
||||||
|
{{ $ts.objectStorageSetPublicRead }}
|
||||||
|
</FormSwitch>
|
||||||
|
|
||||||
|
<FormSwitch v-model:value="objectStorageS3ForcePathStyle">
|
||||||
|
s3ForcePathStyle
|
||||||
|
</FormSwitch>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
|
||||||
|
</FormSuspense>
|
||||||
|
</FormBase>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
import FormSwitch from '@client/components/form/switch.vue';
|
||||||
|
import FormInput from '@client/components/form/input.vue';
|
||||||
|
import FormButton from '@client/components/form/button.vue';
|
||||||
|
import FormBase from '@client/components/form/base.vue';
|
||||||
|
import FormGroup from '@client/components/form/group.vue';
|
||||||
|
import FormSuspense from '@client/components/form/suspense.vue';
|
||||||
|
import * as os from '@client/os';
|
||||||
|
import * as symbols from '@client/symbols';
|
||||||
|
import { fetchInstance } from '@client/instance';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
components: {
|
||||||
|
FormSwitch,
|
||||||
|
FormInput,
|
||||||
|
FormBase,
|
||||||
|
FormGroup,
|
||||||
|
FormButton,
|
||||||
|
FormSuspense,
|
||||||
|
},
|
||||||
|
|
||||||
|
emits: ['info'],
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
[symbols.PAGE_INFO]: {
|
||||||
|
title: this.$ts.objectStorage,
|
||||||
|
icon: 'fas fa-cloud'
|
||||||
|
},
|
||||||
|
useObjectStorage: false,
|
||||||
|
objectStorageBaseUrl: null,
|
||||||
|
objectStorageBucket: null,
|
||||||
|
objectStoragePrefix: null,
|
||||||
|
objectStorageEndpoint: null,
|
||||||
|
objectStorageRegion: null,
|
||||||
|
objectStoragePort: null,
|
||||||
|
objectStorageAccessKey: null,
|
||||||
|
objectStorageSecretKey: null,
|
||||||
|
objectStorageUseSSL: false,
|
||||||
|
objectStorageUseProxy: false,
|
||||||
|
objectStorageSetPublicRead: false,
|
||||||
|
objectStorageS3ForcePathStyle: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async mounted() {
|
||||||
|
this.$emit('info', this[symbols.PAGE_INFO]);
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async init() {
|
||||||
|
const meta = await os.api('meta', { detail: true });
|
||||||
|
this.useObjectStorage = meta.useObjectStorage;
|
||||||
|
this.objectStorageBaseUrl = meta.objectStorageBaseUrl;
|
||||||
|
this.objectStorageBucket = meta.objectStorageBucket;
|
||||||
|
this.objectStoragePrefix = meta.objectStoragePrefix;
|
||||||
|
this.objectStorageEndpoint = meta.objectStorageEndpoint;
|
||||||
|
this.objectStorageRegion = meta.objectStorageRegion;
|
||||||
|
this.objectStoragePort = meta.objectStoragePort;
|
||||||
|
this.objectStorageAccessKey = meta.objectStorageAccessKey;
|
||||||
|
this.objectStorageSecretKey = meta.objectStorageSecretKey;
|
||||||
|
this.objectStorageUseSSL = meta.objectStorageUseSSL;
|
||||||
|
this.objectStorageUseProxy = meta.objectStorageUseProxy;
|
||||||
|
this.objectStorageSetPublicRead = meta.objectStorageSetPublicRead;
|
||||||
|
this.objectStorageS3ForcePathStyle = meta.objectStorageS3ForcePathStyle;
|
||||||
|
},
|
||||||
|
save() {
|
||||||
|
os.apiWithDialog('admin/update-meta', {
|
||||||
|
useObjectStorage: this.useObjectStorage,
|
||||||
|
objectStorageBaseUrl: this.objectStorageBaseUrl ? this.objectStorageBaseUrl : null,
|
||||||
|
objectStorageBucket: this.objectStorageBucket ? this.objectStorageBucket : null,
|
||||||
|
objectStoragePrefix: this.objectStoragePrefix ? this.objectStoragePrefix : null,
|
||||||
|
objectStorageEndpoint: this.objectStorageEndpoint ? this.objectStorageEndpoint : null,
|
||||||
|
objectStorageRegion: this.objectStorageRegion ? this.objectStorageRegion : null,
|
||||||
|
objectStoragePort: this.objectStoragePort ? this.objectStoragePort : null,
|
||||||
|
objectStorageAccessKey: this.objectStorageAccessKey ? this.objectStorageAccessKey : null,
|
||||||
|
objectStorageSecretKey: this.objectStorageSecretKey ? this.objectStorageSecretKey : null,
|
||||||
|
objectStorageUseSSL: this.objectStorageUseSSL,
|
||||||
|
objectStorageUseProxy: this.objectStorageUseProxy,
|
||||||
|
objectStorageSetPublicRead: this.objectStorageSetPublicRead,
|
||||||
|
objectStorageS3ForcePathStyle: this.objectStorageS3ForcePathStyle,
|
||||||
|
}).then(() => {
|
||||||
|
fetchInstance();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
68
src/client/pages/instance/other-settings.vue
Normal file
68
src/client/pages/instance/other-settings.vue
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
<template>
|
||||||
|
<FormBase>
|
||||||
|
<FormSuspense :p="init">
|
||||||
|
<FormGroup>
|
||||||
|
<FormInput v-model:value="summalyProxy">
|
||||||
|
<template #prefix><i class="fas fa-link"></i></template>
|
||||||
|
Summaly Proxy URL
|
||||||
|
</FormInput>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
|
||||||
|
</FormSuspense>
|
||||||
|
</FormBase>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
import FormSwitch from '@client/components/form/switch.vue';
|
||||||
|
import FormInput from '@client/components/form/input.vue';
|
||||||
|
import FormButton from '@client/components/form/button.vue';
|
||||||
|
import FormBase from '@client/components/form/base.vue';
|
||||||
|
import FormGroup from '@client/components/form/group.vue';
|
||||||
|
import FormSuspense from '@client/components/form/suspense.vue';
|
||||||
|
import * as os from '@client/os';
|
||||||
|
import * as symbols from '@client/symbols';
|
||||||
|
import { fetchInstance } from '@client/instance';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
components: {
|
||||||
|
FormSwitch,
|
||||||
|
FormInput,
|
||||||
|
FormBase,
|
||||||
|
FormGroup,
|
||||||
|
FormButton,
|
||||||
|
FormSuspense,
|
||||||
|
},
|
||||||
|
|
||||||
|
emits: ['info'],
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
[symbols.PAGE_INFO]: {
|
||||||
|
title: this.$ts.other,
|
||||||
|
icon: 'fas fa-cogs'
|
||||||
|
},
|
||||||
|
summalyProxy: '',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async mounted() {
|
||||||
|
this.$emit('info', this[symbols.PAGE_INFO]);
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async init() {
|
||||||
|
const meta = await os.api('meta', { detail: true });
|
||||||
|
this.summalyProxy = meta.summalyProxy;
|
||||||
|
},
|
||||||
|
save() {
|
||||||
|
os.apiWithDialog('admin/update-meta', {
|
||||||
|
summalyProxy: this.summalyProxy,
|
||||||
|
}).then(() => {
|
||||||
|
fetchInstance();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
135
src/client/pages/instance/overview.vue
Normal file
135
src/client/pages/instance/overview.vue
Normal file
|
@ -0,0 +1,135 @@
|
||||||
|
<template>
|
||||||
|
<FormBase>
|
||||||
|
<FormSuspense :p="init">
|
||||||
|
<FormInfo v-if="noMaintainerInformation" warn>{{ $ts.noMaintainerInformationWarning }} <MkA to="/instance/settings" class="_link">{{ $ts.configure }}</MkA></FormInfo>
|
||||||
|
<FormInfo v-if="noBotProtection" warn>{{ $ts.noBotProtectionWarning }} <MkA to="/instance/bot-protection" class="_link">{{ $ts.configure }}</MkA></FormInfo>
|
||||||
|
|
||||||
|
<FormSuspense :p="fetchStats" v-slot="{ result: stats }">
|
||||||
|
<FormGroup>
|
||||||
|
<FormKeyValueView>
|
||||||
|
<template #key>Users</template>
|
||||||
|
<template #value>{{ number(stats.originalUsersCount) }}</template>
|
||||||
|
</FormKeyValueView>
|
||||||
|
<FormKeyValueView>
|
||||||
|
<template #key>Notes</template>
|
||||||
|
<template #value>{{ number(stats.originalNotesCount) }}</template>
|
||||||
|
</FormKeyValueView>
|
||||||
|
</FormGroup>
|
||||||
|
</FormSuspense>
|
||||||
|
|
||||||
|
<div class="_formItem">
|
||||||
|
<div class="_formPanel">
|
||||||
|
<MkInstanceStats :chart-limit="300" :detailed="true"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<XMetrics/>
|
||||||
|
|
||||||
|
<FormSuspense :p="fetchServerInfo" v-slot="{ result: serverInfo }">
|
||||||
|
<FormGroup>
|
||||||
|
<FormKeyValueView>
|
||||||
|
<template #key>Node.js</template>
|
||||||
|
<template #value>{{ serverInfo.node }}</template>
|
||||||
|
</FormKeyValueView>
|
||||||
|
<FormKeyValueView>
|
||||||
|
<template #key>PostgreSQL</template>
|
||||||
|
<template #value>{{ serverInfo.psql }}</template>
|
||||||
|
</FormKeyValueView>
|
||||||
|
<FormKeyValueView>
|
||||||
|
<template #key>Redis</template>
|
||||||
|
<template #value>{{ serverInfo.redis }}</template>
|
||||||
|
</FormKeyValueView>
|
||||||
|
</FormGroup>
|
||||||
|
</FormSuspense>
|
||||||
|
</FormSuspense>
|
||||||
|
</FormBase>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { computed, defineComponent, markRaw } from 'vue';
|
||||||
|
import FormKeyValueView from '@client/components/form/key-value-view.vue';
|
||||||
|
import FormInput from '@client/components/form/input.vue';
|
||||||
|
import FormButton from '@client/components/form/button.vue';
|
||||||
|
import FormBase from '@client/components/form/base.vue';
|
||||||
|
import FormGroup from '@client/components/form/group.vue';
|
||||||
|
import FormTextarea from '@client/components/form/textarea.vue';
|
||||||
|
import FormInfo from '@client/components/form/info.vue';
|
||||||
|
import FormSuspense from '@client/components/form/suspense.vue';
|
||||||
|
import MkInstanceStats from '@client/components/instance-stats.vue';
|
||||||
|
import MkButton from '@client/components/ui/button.vue';
|
||||||
|
import MkSelect from '@client/components/ui/select.vue';
|
||||||
|
import MkInput from '@client/components/ui/input.vue';
|
||||||
|
import MkContainer from '@client/components/ui/container.vue';
|
||||||
|
import MkFolder from '@client/components/ui/folder.vue';
|
||||||
|
import { version, url } from '@client/config';
|
||||||
|
import bytes from '../../filters/bytes';
|
||||||
|
import number from '../../filters/number';
|
||||||
|
import MkInstanceInfo from './instance.vue';
|
||||||
|
import XMetrics from './metrics.vue';
|
||||||
|
import * as os from '@client/os';
|
||||||
|
import * as symbols from '@client/symbols';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
components: {
|
||||||
|
FormBase,
|
||||||
|
FormSuspense,
|
||||||
|
FormGroup,
|
||||||
|
FormInfo,
|
||||||
|
FormKeyValueView,
|
||||||
|
MkInstanceStats,
|
||||||
|
XMetrics,
|
||||||
|
},
|
||||||
|
|
||||||
|
emits: ['info'],
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
[symbols.PAGE_INFO]: {
|
||||||
|
title: this.$ts.overview,
|
||||||
|
icon: 'fas fa-tachometer-alt'
|
||||||
|
},
|
||||||
|
page: 'index',
|
||||||
|
version,
|
||||||
|
url,
|
||||||
|
stats: null,
|
||||||
|
fetchStats: () => os.api('stats', {}),
|
||||||
|
fetchServerInfo: () => os.api('admin/server-info', {}),
|
||||||
|
fetchJobs: () => os.api('admin/queue/deliver-delayed', {}),
|
||||||
|
fetchModLogs: () => os.api('admin/show-moderation-logs', {}),
|
||||||
|
noMaintainerInformation: false,
|
||||||
|
noBotProtection: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async mounted() {
|
||||||
|
this.$emit('info', this[symbols.PAGE_INFO]);
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async init() {
|
||||||
|
this.meta = await os.api('meta', { detail: true });
|
||||||
|
|
||||||
|
const isEmpty = (x: any) => x == null || x == '';
|
||||||
|
|
||||||
|
this.noMaintainerInformation = isEmpty(this.meta.maintainerName) || isEmpty(this.meta.maintainerEmail);
|
||||||
|
this.noBotProtection = !this.meta.enableHcaptcha && !this.meta.enableRecaptcha;
|
||||||
|
},
|
||||||
|
|
||||||
|
async showInstanceInfo(q) {
|
||||||
|
let instance = q;
|
||||||
|
if (typeof q === 'string') {
|
||||||
|
instance = await os.api('federation/show-instance', {
|
||||||
|
host: q
|
||||||
|
});
|
||||||
|
}
|
||||||
|
os.popup(MkInstanceInfo, {
|
||||||
|
instance: instance
|
||||||
|
}, {}, 'closed');
|
||||||
|
},
|
||||||
|
|
||||||
|
bytes,
|
||||||
|
|
||||||
|
number,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
86
src/client/pages/instance/proxy-account.vue
Normal file
86
src/client/pages/instance/proxy-account.vue
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
<template>
|
||||||
|
<FormBase>
|
||||||
|
<FormSuspense :p="init">
|
||||||
|
<FormGroup>
|
||||||
|
<FormKeyValueView>
|
||||||
|
<template #key>{{ $ts.proxyAccount }}</template>
|
||||||
|
<template #value>{{ proxyAccount ? `@${proxyAccount.username}` : $ts.none }}</template>
|
||||||
|
</FormKeyValueView>
|
||||||
|
<template #caption>{{ $ts.proxyAccountDescription }}</template>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormButton @click="chooseProxyAccount" primary>{{ $ts.selectAccount }}</FormButton>
|
||||||
|
</FormSuspense>
|
||||||
|
</FormBase>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
import FormKeyValueView from '@client/components/form/key-value-view.vue';
|
||||||
|
import FormInput from '@client/components/form/input.vue';
|
||||||
|
import FormButton from '@client/components/form/button.vue';
|
||||||
|
import FormBase from '@client/components/form/base.vue';
|
||||||
|
import FormGroup from '@client/components/form/group.vue';
|
||||||
|
import FormTextarea from '@client/components/form/textarea.vue';
|
||||||
|
import FormInfo from '@client/components/form/info.vue';
|
||||||
|
import FormSuspense from '@client/components/form/suspense.vue';
|
||||||
|
import * as os from '@client/os';
|
||||||
|
import * as symbols from '@client/symbols';
|
||||||
|
import { fetchInstance } from '@client/instance';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
components: {
|
||||||
|
FormKeyValueView,
|
||||||
|
FormInput,
|
||||||
|
FormBase,
|
||||||
|
FormGroup,
|
||||||
|
FormButton,
|
||||||
|
FormTextarea,
|
||||||
|
FormInfo,
|
||||||
|
FormSuspense,
|
||||||
|
},
|
||||||
|
|
||||||
|
emits: ['info'],
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
[symbols.PAGE_INFO]: {
|
||||||
|
title: this.$ts.proxyAccount,
|
||||||
|
icon: 'fas fa-ghost'
|
||||||
|
},
|
||||||
|
proxyAccount: null,
|
||||||
|
proxyAccountId: null,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async mounted() {
|
||||||
|
this.$emit('info', this[symbols.PAGE_INFO]);
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async init() {
|
||||||
|
const meta = await os.api('meta', { detail: true });
|
||||||
|
this.proxyAccountId = meta.proxyAccountId;
|
||||||
|
if (this.proxyAccountId) {
|
||||||
|
this.proxyAccount = await os.api('users/show', { userId: this.proxyAccountId });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
chooseProxyAccount() {
|
||||||
|
os.selectUser().then(user => {
|
||||||
|
this.proxyAccount = user;
|
||||||
|
this.proxyAccountId = user.id;
|
||||||
|
this.save();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
save() {
|
||||||
|
os.apiWithDialog('admin/update-meta', {
|
||||||
|
proxyAccountId: this.proxyAccountId,
|
||||||
|
}).then(() => {
|
||||||
|
fetchInstance();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -1,7 +1,8 @@
|
||||||
<template>
|
<template>
|
||||||
<section class="_section">
|
<div class="_formItem">
|
||||||
<div class="_title"><slot name="title"></slot></div>
|
<div class="_formLabel"><slot name="title"></slot></div>
|
||||||
<div class="_content _table">
|
<div class="_formPanel pumxzjhg">
|
||||||
|
<div class="_table status">
|
||||||
<div class="_row">
|
<div class="_row">
|
||||||
<div class="_cell"><div class="_label">Process</div>{{ number(activeSincePrevTick) }}</div>
|
<div class="_cell"><div class="_label">Process</div>{{ number(activeSincePrevTick) }}</div>
|
||||||
<div class="_cell"><div class="_label">Active</div>{{ number(active) }}</div>
|
<div class="_cell"><div class="_label">Active</div>{{ number(active) }}</div>
|
||||||
|
@ -9,10 +10,10 @@
|
||||||
<div class="_cell"><div class="_label">Delayed</div>{{ number(delayed) }}</div>
|
<div class="_cell"><div class="_label">Delayed</div>{{ number(delayed) }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="_content" style="margin-bottom: -8px;">
|
<div class="">
|
||||||
<canvas ref="chart"></canvas>
|
<canvas ref="chart"></canvas>
|
||||||
</div>
|
</div>
|
||||||
<div class="_content" style="max-height: 180px; overflow: auto;">
|
<div class="jobs">
|
||||||
<div v-if="jobs.length > 0">
|
<div v-if="jobs.length > 0">
|
||||||
<div v-for="job in jobs" :key="job[0]">
|
<div v-for="job in jobs" :key="job[0]">
|
||||||
<span>{{ job[0] }}</span>
|
<span>{{ job[0] }}</span>
|
||||||
|
@ -21,7 +22,8 @@
|
||||||
</div>
|
</div>
|
||||||
<span v-else style="opacity: 0.5;">{{ $ts.noJobs }}</span>
|
<span v-else style="opacity: 0.5;">{{ $ts.noJobs }}</span>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
@ -110,10 +112,10 @@ export default defineComponent({
|
||||||
aspectRatio: 3,
|
aspectRatio: 3,
|
||||||
layout: {
|
layout: {
|
||||||
padding: {
|
padding: {
|
||||||
left: 0,
|
left: 16,
|
||||||
right: 0,
|
right: 16,
|
||||||
top: 8,
|
top: 16,
|
||||||
bottom: 0
|
bottom: 12
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
legend: {
|
legend: {
|
||||||
|
@ -198,3 +200,19 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.pumxzjhg {
|
||||||
|
> .status {
|
||||||
|
padding: 16px;
|
||||||
|
border-bottom: solid 0.5px var(--divider);
|
||||||
|
}
|
||||||
|
|
||||||
|
> .jobs {
|
||||||
|
padding: 16px;
|
||||||
|
border-top: solid 0.5px var(--divider);
|
||||||
|
max-height: 180px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -1,43 +1,47 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<FormBase>
|
||||||
<XQueue :connection="connection" domain="inbox">
|
<XQueue :connection="connection" domain="inbox">
|
||||||
<template #title><i class="fas fa-exchange-alt"></i> In</template>
|
<template #title>In</template>
|
||||||
</XQueue>
|
</XQueue>
|
||||||
<XQueue :connection="connection" domain="deliver">
|
<XQueue :connection="connection" domain="deliver">
|
||||||
<template #title><i class="fas fa-exchange-alt"></i> Out</template>
|
<template #title>Out</template>
|
||||||
</XQueue>
|
</XQueue>
|
||||||
<section class="_section">
|
<FormButton @click="clear()" danger><i class="fas fa-trash-alt"></i> {{ $ts.clearQueue }}</FormButton>
|
||||||
<div class="_content">
|
</FormBase>
|
||||||
<MkButton @click="clear()"><i class="fas fa-trash-alt"></i> {{ $ts.clearQueue }}</MkButton>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent } from 'vue';
|
import { defineComponent } from 'vue';
|
||||||
import MkButton from '@client/components/ui/button.vue';
|
import MkButton from '@client/components/ui/button.vue';
|
||||||
import XQueue from './queue.chart.vue';
|
import XQueue from './queue.chart.vue';
|
||||||
|
import FormBase from '@client/components/form/base.vue';
|
||||||
|
import FormButton from '@client/components/form/button.vue';
|
||||||
import * as os from '@client/os';
|
import * as os from '@client/os';
|
||||||
import * as symbols from '@client/symbols';
|
import * as symbols from '@client/symbols';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
|
FormBase,
|
||||||
|
FormButton,
|
||||||
MkButton,
|
MkButton,
|
||||||
XQueue,
|
XQueue,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
emits: ['info'],
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
[symbols.PAGE_INFO]: {
|
[symbols.PAGE_INFO]: {
|
||||||
title: this.$ts.jobQueue,
|
title: this.$ts.jobQueue,
|
||||||
icon: 'fas fa-exchange-alt',
|
icon: 'fas fa-clipboard-list',
|
||||||
},
|
},
|
||||||
connection: os.stream.useSharedConnection('queueStats'),
|
connection: os.stream.useSharedConnection('queueStats'),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
|
this.$emit('info', this[symbols.PAGE_INFO]);
|
||||||
|
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.connection.send('requestLog', {
|
this.connection.send('requestLog', {
|
||||||
id: Math.random().toString().substr(2, 8),
|
id: Math.random().toString().substr(2, 8),
|
||||||
|
|
|
@ -1,44 +1,41 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="relaycxt">
|
<FormBase class="relaycxt">
|
||||||
<section class="_section add">
|
<FormButton @click="addRelay" primary><i class="fas fa-plus"></i> {{ $ts.addRelay }}</FormButton>
|
||||||
<div class="_title"><i class="fas fa-plus"></i> {{ $ts.addRelay }}</div>
|
|
||||||
<div class="_content">
|
|
||||||
<MkInput v-model:value="inbox">
|
|
||||||
<span>{{ $ts.inboxUrl }}</span>
|
|
||||||
</MkInput>
|
|
||||||
<MkButton @click="add(inbox)" primary><i class="fas fa-plus"></i> {{ $ts.add }}</MkButton>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="_section relays">
|
<div class="_formItem" v-for="relay in relays" :key="relay.inbox">
|
||||||
<div class="_title"><i class="fas fa-project-diagram"></i> {{ $ts.addedRelays }}</div>
|
<div class="_formPanel" style="padding: 16px;">
|
||||||
<div class="_content relay" v-for="relay in relays" :key="relay.inbox">
|
|
||||||
<div>{{ relay.inbox }}</div>
|
<div>{{ relay.inbox }}</div>
|
||||||
<div>{{ $t(`_relayStatus.${relay.status}`) }}</div>
|
<div>{{ $t(`_relayStatus.${relay.status}`) }}</div>
|
||||||
<MkButton class="button" inline @click="remove(relay.inbox)"><i class="fas fa-trash-alt"></i> {{ $ts.remove }}</MkButton>
|
<MkButton class="button" inline danger @click="remove(relay.inbox)"><i class="fas fa-trash-alt"></i> {{ $ts.remove }}</MkButton>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
|
||||||
</div>
|
</div>
|
||||||
|
</FormBase>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent } from 'vue';
|
import { defineComponent } from 'vue';
|
||||||
import MkButton from '@client/components/ui/button.vue';
|
import MkButton from '@client/components/ui/button.vue';
|
||||||
import MkInput from '@client/components/ui/input.vue';
|
import MkInput from '@client/components/ui/input.vue';
|
||||||
|
import FormBase from '@client/components/form/base.vue';
|
||||||
|
import FormButton from '@client/components/form/button.vue';
|
||||||
import * as os from '@client/os';
|
import * as os from '@client/os';
|
||||||
import * as symbols from '@client/symbols';
|
import * as symbols from '@client/symbols';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
|
FormBase,
|
||||||
|
FormButton,
|
||||||
MkButton,
|
MkButton,
|
||||||
MkInput,
|
MkInput,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
emits: ['info'],
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
[symbols.PAGE_INFO]: {
|
[symbols.PAGE_INFO]: {
|
||||||
title: this.$ts.relays,
|
title: this.$ts.relays,
|
||||||
icon: 'fas fa-project-diagram',
|
icon: 'fas fa-globe',
|
||||||
},
|
},
|
||||||
relays: [],
|
relays: [],
|
||||||
inbox: '',
|
inbox: '',
|
||||||
|
@ -49,8 +46,19 @@ export default defineComponent({
|
||||||
this.refresh();
|
this.refresh();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.$emit('info', this[symbols.PAGE_INFO]);
|
||||||
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
add(inbox: string) {
|
async addRelay() {
|
||||||
|
const { canceled, result: inbox } = await os.dialog({
|
||||||
|
title: this.$ts.addRelay,
|
||||||
|
input: {
|
||||||
|
placeholder: this.$ts.inboxUrl
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (canceled) return;
|
||||||
os.api('admin/relays/add', {
|
os.api('admin/relays/add', {
|
||||||
inbox
|
inbox
|
||||||
}).then((relay: any) => {
|
}).then((relay: any) => {
|
||||||
|
@ -86,9 +94,5 @@ export default defineComponent({
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
._content.relay {
|
|
||||||
div {
|
|
||||||
margin: 0.5em 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
77
src/client/pages/instance/security.vue
Normal file
77
src/client/pages/instance/security.vue
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
<template>
|
||||||
|
<FormBase>
|
||||||
|
<FormSuspense :p="init">
|
||||||
|
<FormLink to="/instance/bot-protection">
|
||||||
|
<i class="fas fa-shield-alt"></i> {{ $ts.botProtection }}
|
||||||
|
<template #suffix v-if="enableHcaptcha">hCaptcha</template>
|
||||||
|
<template #suffix v-else-if="enableRecaptcha">reCAPTCHA</template>
|
||||||
|
<template #suffix v-else>{{ $ts.none }} ({{ $ts.notRecommended }})</template>
|
||||||
|
</FormLink>
|
||||||
|
|
||||||
|
<FormSwitch v-model:value="enableRegistration">{{ $ts.enableRegistration }}</FormSwitch>
|
||||||
|
|
||||||
|
<FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
|
||||||
|
</FormSuspense>
|
||||||
|
</FormBase>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineAsyncComponent, defineComponent } from 'vue';
|
||||||
|
import FormLink from '@client/components/form/link.vue';
|
||||||
|
import FormSwitch from '@client/components/form/switch.vue';
|
||||||
|
import FormButton from '@client/components/form/button.vue';
|
||||||
|
import FormBase from '@client/components/form/base.vue';
|
||||||
|
import FormGroup from '@client/components/form/group.vue';
|
||||||
|
import FormInfo from '@client/components/form/info.vue';
|
||||||
|
import FormSuspense from '@client/components/form/suspense.vue';
|
||||||
|
import * as os from '@client/os';
|
||||||
|
import * as symbols from '@client/symbols';
|
||||||
|
import { fetchInstance } from '@client/instance';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
components: {
|
||||||
|
FormLink,
|
||||||
|
FormSwitch,
|
||||||
|
FormBase,
|
||||||
|
FormGroup,
|
||||||
|
FormButton,
|
||||||
|
FormInfo,
|
||||||
|
FormSuspense,
|
||||||
|
},
|
||||||
|
|
||||||
|
emits: ['info'],
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
[symbols.PAGE_INFO]: {
|
||||||
|
title: this.$ts.security,
|
||||||
|
icon: 'fas fa-lock'
|
||||||
|
},
|
||||||
|
enableHcaptcha: false,
|
||||||
|
enableRecaptcha: false,
|
||||||
|
enableRegistration: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async mounted() {
|
||||||
|
this.$emit('info', this[symbols.PAGE_INFO]);
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async init() {
|
||||||
|
const meta = await os.api('meta', { detail: true });
|
||||||
|
this.enableHcaptcha = meta.enableHcaptcha;
|
||||||
|
this.enableRecaptcha = meta.enableRecaptcha;
|
||||||
|
this.enableRegistration = !meta.disableRegistration;
|
||||||
|
},
|
||||||
|
|
||||||
|
save() {
|
||||||
|
os.apiWithDialog('admin/update-meta', {
|
||||||
|
disableRegistration: !this.enableRegistration,
|
||||||
|
}).then(() => {
|
||||||
|
fetchInstance();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
84
src/client/pages/instance/service-worker.vue
Normal file
84
src/client/pages/instance/service-worker.vue
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
<template>
|
||||||
|
<FormBase>
|
||||||
|
<FormSuspense :p="init">
|
||||||
|
<FormSwitch v-model:value="enableServiceWorker">
|
||||||
|
{{ $ts.enableServiceworker }}
|
||||||
|
<template #desc>{{ $ts.serviceworkerInfo }}</template>
|
||||||
|
</FormSwitch>
|
||||||
|
|
||||||
|
<template v-if="enableServiceWorker">
|
||||||
|
<FormInput v-model:value="swPublicKey">
|
||||||
|
<template #prefix><i class="fas fa-key"></i></template>
|
||||||
|
Public key
|
||||||
|
</FormInput>
|
||||||
|
|
||||||
|
<FormInput v-model:value="swPrivateKey">
|
||||||
|
<template #prefix><i class="fas fa-key"></i></template>
|
||||||
|
Private key
|
||||||
|
</FormInput>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
|
||||||
|
</FormSuspense>
|
||||||
|
</FormBase>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
import FormSwitch from '@client/components/form/switch.vue';
|
||||||
|
import FormInput from '@client/components/form/input.vue';
|
||||||
|
import FormButton from '@client/components/form/button.vue';
|
||||||
|
import FormBase from '@client/components/form/base.vue';
|
||||||
|
import FormGroup from '@client/components/form/group.vue';
|
||||||
|
import FormSuspense from '@client/components/form/suspense.vue';
|
||||||
|
import * as os from '@client/os';
|
||||||
|
import * as symbols from '@client/symbols';
|
||||||
|
import { fetchInstance } from '@client/instance';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
components: {
|
||||||
|
FormSwitch,
|
||||||
|
FormInput,
|
||||||
|
FormBase,
|
||||||
|
FormGroup,
|
||||||
|
FormButton,
|
||||||
|
FormSuspense,
|
||||||
|
},
|
||||||
|
|
||||||
|
emits: ['info'],
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
[symbols.PAGE_INFO]: {
|
||||||
|
title: 'ServiceWorker',
|
||||||
|
icon: 'fas fa-bolt'
|
||||||
|
},
|
||||||
|
enableServiceWorker: false,
|
||||||
|
swPublicKey: null,
|
||||||
|
swPrivateKey: null,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async mounted() {
|
||||||
|
this.$emit('info', this[symbols.PAGE_INFO]);
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async init() {
|
||||||
|
const meta = await os.api('meta', { detail: true });
|
||||||
|
this.enableServiceWorker = meta.enableServiceWorker;
|
||||||
|
this.swPublicKey = meta.swPublickey;
|
||||||
|
this.swPrivateKey = meta.swPrivateKey;
|
||||||
|
},
|
||||||
|
save() {
|
||||||
|
os.apiWithDialog('admin/update-meta', {
|
||||||
|
enableServiceWorker: this.enableServiceWorker,
|
||||||
|
swPublicKey: this.swPublicKey,
|
||||||
|
swPrivateKey: this.swPrivateKey,
|
||||||
|
}).then(() => {
|
||||||
|
fetchInstance();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -1,581 +1,132 @@
|
||||||
<template>
|
<template>
|
||||||
<div v-if="meta" class="_section">
|
<FormBase>
|
||||||
<section class="_card _gap">
|
<FormSuspense :p="init">
|
||||||
<div class="_title"><i class="fas fa-info-circle"></i> {{ $ts.basicInfo }}</div>
|
<FormInput v-model:value="name">
|
||||||
<div class="_content">
|
<span>{{ $ts.instanceName }}</span>
|
||||||
<MkInput v-model:value="name">{{ $ts.instanceName }}</MkInput>
|
</FormInput>
|
||||||
<MkTextarea v-model:value="description">{{ $ts.instanceDescription }}</MkTextarea>
|
|
||||||
<MkInput v-model:value="iconUrl"><template #icon><i class="fas fa-link"></i></template>{{ $ts.iconUrl }}</MkInput>
|
|
||||||
<MkInput v-model:value="bannerUrl"><template #icon><i class="fas fa-link"></i></template>{{ $ts.bannerUrl }}</MkInput>
|
|
||||||
<MkInput v-model:value="backgroundImageUrl"><template #icon><i class="fas fa-link"></i></template>{{ $ts.backgroundImageUrl }}</MkInput>
|
|
||||||
<MkInput v-model:value="logoImageUrl"><template #icon><i class="fas fa-link"></i></template>{{ $ts.logoImageUrl }}</MkInput>
|
|
||||||
<MkInput v-model:value="tosUrl"><template #icon><i class="fas fa-link"></i></template>{{ $ts.tosUrl }}</MkInput>
|
|
||||||
<MkInput v-model:value="maintainerName">{{ $ts.maintainerName }}</MkInput>
|
|
||||||
<MkInput v-model:value="maintainerEmail" type="email"><template #icon><i class="fas fa-envelope"></i></template>{{ $ts.maintainerEmail }}</MkInput>
|
|
||||||
</div>
|
|
||||||
<div class="_footer">
|
|
||||||
<MkButton primary @click="save(true)"><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<MkInput v-model:value="pinnedClipId">{{ $ts.pinnedClipId }}</MkInput>
|
<FormTextarea v-model:value="description">
|
||||||
|
<span>{{ $ts.instanceDescription }}</span>
|
||||||
|
</FormTextarea>
|
||||||
|
|
||||||
<section class="_card _gap">
|
<FormInput v-model:value="iconUrl">
|
||||||
<div class="_content">
|
<template #prefix><i class="fas fa-link"></i></template>
|
||||||
<MkInput v-model:value="maxNoteTextLength" type="number" :save="() => save()"><template #icon><i class="fas fa-pencil-alt"></i></template>{{ $ts.maxNoteTextLength }}</MkInput>
|
<span>{{ $ts.iconUrl }}</span>
|
||||||
</div>
|
</FormInput>
|
||||||
<div class="_content">
|
|
||||||
<MkSwitch v-model:value="enableLocalTimeline" @update:value="save()">{{ $ts.enableLocalTimeline }}</MkSwitch>
|
|
||||||
<MkSwitch v-model:value="enableGlobalTimeline" @update:value="save()">{{ $ts.enableGlobalTimeline }}</MkSwitch>
|
|
||||||
<MkInfo>{{ $ts.disablingTimelinesInfo }}</MkInfo>
|
|
||||||
</div>
|
|
||||||
<div class="_content">
|
|
||||||
<MkSwitch v-model:value="useStarForReactionFallback" @update:value="save()">{{ $ts.useStarForReactionFallback }}</MkSwitch>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="_card _gap">
|
<FormInput v-model:value="bannerUrl">
|
||||||
<div class="_title"><i class="fas fa-user"></i> {{ $ts.registration }}</div>
|
<template #prefix><i class="fas fa-link"></i></template>
|
||||||
<div class="_content">
|
<span>{{ $ts.bannerUrl }}</span>
|
||||||
<MkSwitch v-model:value="enableRegistration" @update:value="save()">{{ $ts.enableRegistration }}</MkSwitch>
|
</FormInput>
|
||||||
<MkButton v-if="!enableRegistration" @click="invite">{{ $ts.invite }}</MkButton>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="_card _gap">
|
<FormInput v-model:value="tosUrl">
|
||||||
<div class="_title"><i class="fas fa-shield-alt"></i> {{ $ts.hcaptcha }}</div>
|
<template #prefix><i class="fas fa-link"></i></template>
|
||||||
<div class="_content">
|
<span>{{ $ts.tosUrl }}</span>
|
||||||
<MkSwitch v-model:value="enableHcaptcha">{{ $ts.enableHcaptcha }}</MkSwitch>
|
</FormInput>
|
||||||
<template v-if="enableHcaptcha">
|
|
||||||
<MkInput v-model:value="hcaptchaSiteKey" :disabled="!enableHcaptcha"><template #icon><i class="fas fa-key"></i></template>{{ $ts.hcaptchaSiteKey }}</MkInput>
|
|
||||||
<MkInput v-model:value="hcaptchaSecretKey" :disabled="!enableHcaptcha"><template #icon><i class="fas fa-key"></i></template>{{ $ts.hcaptchaSecretKey }}</MkInput>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
<div class="_content" v-if="enableHcaptcha">
|
|
||||||
<header>{{ $ts.preview }}</header>
|
|
||||||
<captcha v-if="enableHcaptcha" provider="hcaptcha" :sitekey="hcaptchaSiteKey || '10000000-ffff-ffff-ffff-000000000001'"/>
|
|
||||||
</div>
|
|
||||||
<div class="_footer">
|
|
||||||
<MkButton primary @click="save(true)"><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="_card _gap">
|
<FormInput v-model:value="maintainerName">
|
||||||
<div class="_title"><i class="fas fa-shield-alt"></i> {{ $ts.recaptcha }}</div>
|
<span>{{ $ts.maintainerName }}</span>
|
||||||
<div class="_content">
|
</FormInput>
|
||||||
<MkSwitch v-model:value="enableRecaptcha" ref="enableRecaptcha">{{ $ts.enableRecaptcha }}</MkSwitch>
|
|
||||||
<template v-if="enableRecaptcha">
|
|
||||||
<MkInput v-model:value="recaptchaSiteKey" :disabled="!enableRecaptcha"><template #icon><i class="fas fa-key"></i></template>{{ $ts.recaptchaSiteKey }}</MkInput>
|
|
||||||
<MkInput v-model:value="recaptchaSecretKey" :disabled="!enableRecaptcha"><template #icon><i class="fas fa-key"></i></template>{{ $ts.recaptchaSecretKey }}</MkInput>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
<div class="_content" v-if="enableRecaptcha && recaptchaSiteKey">
|
|
||||||
<header>{{ $ts.preview }}</header>
|
|
||||||
<captcha v-if="enableRecaptcha" provider="grecaptcha" :sitekey="recaptchaSiteKey"/>
|
|
||||||
</div>
|
|
||||||
<div class="_footer">
|
|
||||||
<MkButton primary @click="save(true)"><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="_card _gap">
|
<FormInput v-model:value="maintainerEmail" type="email">
|
||||||
<div class="_title"><i class="fas fa-envelope"></i> {{ $ts.emailConfig }}</div>
|
<template #prefix><i class="fas fa-envelope"></i></template>
|
||||||
<div class="_content">
|
<span>{{ $ts.maintainerEmail }}</span>
|
||||||
<MkSwitch v-model:value="enableEmail" @update:value="save()">{{ $ts.enableEmail }}<template #desc>{{ $ts.emailConfigInfo }}</template></MkSwitch>
|
</FormInput>
|
||||||
<MkInput v-model:value="email" type="email" :disabled="!enableEmail">{{ $ts.email }}</MkInput>
|
|
||||||
<div><b>{{ $ts.smtpConfig }}</b></div>
|
|
||||||
<div class="_inputs">
|
|
||||||
<MkInput v-model:value="smtpHost" :disabled="!enableEmail">{{ $ts.smtpHost }}</MkInput>
|
|
||||||
<MkInput v-model:value="smtpPort" type="number" :disabled="!enableEmail">{{ $ts.smtpPort }}</MkInput>
|
|
||||||
</div>
|
|
||||||
<div class="_inputs">
|
|
||||||
<MkInput v-model:value="smtpUser" :disabled="!enableEmail">{{ $ts.smtpUser }}</MkInput>
|
|
||||||
<MkInput v-model:value="smtpPass" type="password" :disabled="!enableEmail">{{ $ts.smtpPass }}</MkInput>
|
|
||||||
</div>
|
|
||||||
<MkInfo>{{ $ts.emptyToDisableSmtpAuth }}</MkInfo>
|
|
||||||
<MkSwitch v-model:value="smtpSecure" :disabled="!enableEmail">{{ $ts.smtpSecure }}<template #desc>{{ $ts.smtpSecureInfo }}</template></MkSwitch>
|
|
||||||
<div>
|
|
||||||
<MkButton :disabled="!enableEmail" primary inline @click="save(true)"><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
|
|
||||||
<MkButton :disabled="!enableEmail" inline @click="testEmail()">{{ $ts.testEmail }}</MkButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="_card _gap">
|
<FormInput v-model:value="maxNoteTextLength" type="number">
|
||||||
<div class="_title"><i class="fas fa-bolt"></i> {{ $ts.serviceworker }}</div>
|
<template #prefix><i class="fas fa-pencil-alt"></i></template>
|
||||||
<div class="_content">
|
<span>{{ $ts.maxNoteTextLength }}</span>
|
||||||
<MkSwitch v-model:value="enableServiceWorker">{{ $ts.enableServiceworker }}<template #desc>{{ $ts.serviceworkerInfo }}</template></MkSwitch>
|
</FormInput>
|
||||||
<template v-if="enableServiceWorker">
|
|
||||||
<div class="_inputs">
|
|
||||||
<MkInput v-model:value="swPublicKey" :disabled="!enableServiceWorker"><template #icon><i class="fas fa-key"></i></template>Public key</MkInput>
|
|
||||||
<MkInput v-model:value="swPrivateKey" :disabled="!enableServiceWorker"><template #icon><i class="fas fa-key"></i></template>Private key</MkInput>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
<div class="_footer">
|
|
||||||
<MkButton primary @click="save(true)"><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="_card _gap">
|
<FormSwitch v-model:value="enableLocalTimeline">{{ $ts.enableLocalTimeline }}</FormSwitch>
|
||||||
<div class="_title"><i class="fas fa-thumbtack"></i> {{ $ts.pinnedUsers }}</div>
|
<FormSwitch v-model:value="enableGlobalTimeline">{{ $ts.enableGlobalTimeline }}</FormSwitch>
|
||||||
<div class="_content">
|
<FormInfo>{{ $ts.disablingTimelinesInfo }}</FormInfo>
|
||||||
<MkTextarea v-model:value="pinnedUsers">
|
|
||||||
<template #desc>{{ $ts.pinnedUsersDescription }} <button class="_textButton" @click="addPinUser">{{ $ts.addUser }}</button></template>
|
|
||||||
</MkTextarea>
|
|
||||||
</div>
|
|
||||||
<div class="_footer">
|
|
||||||
<MkButton primary @click="save(true)"><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="_card _gap">
|
<FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
|
||||||
<div class="_title"><i class="fas fa-thumbtack"></i> {{ $ts.pinnedPages }}</div>
|
</FormSuspense>
|
||||||
<div class="_content">
|
</FormBase>
|
||||||
<MkTextarea v-model:value="pinnedPages">
|
|
||||||
<template #desc>{{ $ts.pinnedPagesDescription }}</template>
|
|
||||||
</MkTextarea>
|
|
||||||
</div>
|
|
||||||
<div class="_footer">
|
|
||||||
<MkButton primary @click="save(true)"><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="_card _gap">
|
|
||||||
<div class="_title"><i class="fas fa-cloud"></i> {{ $ts.files }}</div>
|
|
||||||
<div class="_content">
|
|
||||||
<MkSwitch v-model:value="cacheRemoteFiles">{{ $ts.cacheRemoteFiles }}<template #desc>{{ $ts.cacheRemoteFilesDescription }}</template></MkSwitch>
|
|
||||||
<MkSwitch v-model:value="proxyRemoteFiles">{{ $ts.proxyRemoteFiles }}<template #desc>{{ $ts.proxyRemoteFilesDescription }}</template></MkSwitch>
|
|
||||||
<MkInput v-model:value="localDriveCapacityMb" type="number">{{ $ts.driveCapacityPerLocalAccount }}<template #suffix>MB</template><template #desc>{{ $ts.inMb }}</template></MkInput>
|
|
||||||
<MkInput v-model:value="remoteDriveCapacityMb" type="number" :disabled="!cacheRemoteFiles">{{ $ts.driveCapacityPerRemoteAccount }}<template #suffix>MB</template><template #desc>{{ $ts.inMb }}</template></MkInput>
|
|
||||||
</div>
|
|
||||||
<div class="_footer">
|
|
||||||
<MkButton primary @click="save(true)"><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="_card _gap">
|
|
||||||
<div class="_title"><i class="fas fa-cloud"></i> {{ $ts.objectStorage }}</div>
|
|
||||||
<div class="_content">
|
|
||||||
<MkSwitch v-model:value="useObjectStorage">{{ $ts.useObjectStorage }}</MkSwitch>
|
|
||||||
<template v-if="useObjectStorage">
|
|
||||||
<MkInput v-model:value="objectStorageBaseUrl" :disabled="!useObjectStorage">{{ $ts.objectStorageBaseUrl }}<template #desc>{{ $ts.objectStorageBaseUrlDesc }}</template></MkInput>
|
|
||||||
<div class="_inputs">
|
|
||||||
<MkInput v-model:value="objectStorageBucket" :disabled="!useObjectStorage">{{ $ts.objectStorageBucket }}<template #desc>{{ $ts.objectStorageBucketDesc }}</template></MkInput>
|
|
||||||
<MkInput v-model:value="objectStoragePrefix" :disabled="!useObjectStorage">{{ $ts.objectStoragePrefix }}<template #desc>{{ $ts.objectStoragePrefixDesc }}</template></MkInput>
|
|
||||||
</div>
|
|
||||||
<MkInput v-model:value="objectStorageEndpoint" :disabled="!useObjectStorage">{{ $ts.objectStorageEndpoint }}<template #desc>{{ $ts.objectStorageEndpointDesc }}</template></MkInput>
|
|
||||||
<div class="_inputs">
|
|
||||||
<MkInput v-model:value="objectStorageRegion" :disabled="!useObjectStorage">{{ $ts.objectStorageRegion }}<template #desc>{{ $ts.objectStorageRegionDesc }}</template></MkInput>
|
|
||||||
</div>
|
|
||||||
<div class="_inputs">
|
|
||||||
<MkInput v-model:value="objectStorageAccessKey" :disabled="!useObjectStorage"><template #icon><i class="fas fa-key"></i></template>Access key</MkInput>
|
|
||||||
<MkInput v-model:value="objectStorageSecretKey" :disabled="!useObjectStorage"><template #icon><i class="fas fa-key"></i></template>Secret key</MkInput>
|
|
||||||
</div>
|
|
||||||
<MkSwitch v-model:value="objectStorageUseSSL" :disabled="!useObjectStorage">{{ $ts.objectStorageUseSSL }}<template #desc>{{ $ts.objectStorageUseSSLDesc }}</template></MkSwitch>
|
|
||||||
<MkSwitch v-model:value="objectStorageUseProxy" :disabled="!useObjectStorage">{{ $ts.objectStorageUseProxy }}<template #desc>{{ $ts.objectStorageUseProxyDesc }}</template></MkSwitch>
|
|
||||||
<MkSwitch v-model:value="objectStorageSetPublicRead" :disabled="!useObjectStorage">{{ $ts.objectStorageSetPublicRead }}</MkSwitch>
|
|
||||||
<MkSwitch v-model:value="objectStorageS3ForcePathStyle" :disabled="!useObjectStorage">s3ForcePathStyle</MkSwitch>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
<div class="_footer">
|
|
||||||
<MkButton primary @click="save(true)"><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="_card _gap">
|
|
||||||
<div class="_title"><i class="fas fa-ghost"></i> {{ $ts.proxyAccount }}</div>
|
|
||||||
<div class="_content">
|
|
||||||
<MkInput :value="proxyAccount ? proxyAccount.username : null" disabled><template #prefix>@</template>{{ $ts.proxyAccount }}<template #desc>{{ $ts.proxyAccountDescription }}</template></MkInput>
|
|
||||||
<MkButton primary @click="chooseProxyAccount">{{ $ts.chooseProxyAccount }}</MkButton>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="_card _gap">
|
|
||||||
<div class="_title"><i class="fas fa-ban"></i> {{ $ts.blockedInstances }}</div>
|
|
||||||
<div class="_content">
|
|
||||||
<MkTextarea v-model:value="blockedHosts">
|
|
||||||
<template #desc>{{ $ts.blockedInstancesDescription }}</template>
|
|
||||||
</MkTextarea>
|
|
||||||
</div>
|
|
||||||
<div class="_footer">
|
|
||||||
<MkButton primary @click="save(true)"><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="_card _gap">
|
|
||||||
<div class="_title"><i class="fas fa-share-alt"></i> {{ $ts.integration }}</div>
|
|
||||||
<div class="_content">
|
|
||||||
<header><i class="fab fa-twitter"></i> Twitter</header>
|
|
||||||
<MkSwitch v-model:value="enableTwitterIntegration">{{ $ts.enable }}</MkSwitch>
|
|
||||||
<template v-if="enableTwitterIntegration">
|
|
||||||
<MkInfo>Callback URL: {{ `${url}/api/tw/cb` }}</MkInfo>
|
|
||||||
<MkInput v-model:value="twitterConsumerKey" :disabled="!enableTwitterIntegration"><template #icon><i class="fas fa-key"></i></template>Consumer Key</MkInput>
|
|
||||||
<MkInput v-model:value="twitterConsumerSecret" :disabled="!enableTwitterIntegration"><template #icon><i class="fas fa-key"></i></template>Consumer Secret</MkInput>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
<div class="_content">
|
|
||||||
<header><i class="fas fa-github"></i> GitHub</header>
|
|
||||||
<MkSwitch v-model:value="enableGithubIntegration">{{ $ts.enable }}</MkSwitch>
|
|
||||||
<template v-if="enableGithubIntegration">
|
|
||||||
<MkInfo>Callback URL: {{ `${url}/api/gh/cb` }}</MkInfo>
|
|
||||||
<MkInput v-model:value="githubClientId" :disabled="!enableGithubIntegration"><template #icon><i class="fas fa-key"></i></template>Client ID</MkInput>
|
|
||||||
<MkInput v-model:value="githubClientSecret" :disabled="!enableGithubIntegration"><template #icon><i class="fas fa-key"></i></template>Client Secret</MkInput>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
<div class="_content">
|
|
||||||
<header><i class="fas fa-discord"></i> Discord</header>
|
|
||||||
<MkSwitch v-model:value="enableDiscordIntegration">{{ $ts.enable }}</MkSwitch>
|
|
||||||
<template v-if="enableDiscordIntegration">
|
|
||||||
<MkInfo>Callback URL: {{ `${url}/api/dc/cb` }}</MkInfo>
|
|
||||||
<MkInput v-model:value="discordClientId" :disabled="!enableDiscordIntegration"><template #icon><i class="fas fa-key"></i></template>Client ID</MkInput>
|
|
||||||
<MkInput v-model:value="discordClientSecret" :disabled="!enableDiscordIntegration"><template #icon><i class="fas fa-key"></i></template>Client Secret</MkInput>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
<div class="_footer">
|
|
||||||
<MkButton primary @click="save(true)"><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="_card _gap">
|
|
||||||
<div class="_title"><i class="fas fa-archway"></i> Summaly Proxy</div>
|
|
||||||
<div class="_content">
|
|
||||||
<MkInput v-model:value="summalyProxy">URL</MkInput>
|
|
||||||
<MkButton primary @click="save(true)"><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, defineAsyncComponent } from 'vue';
|
import { defineComponent } from 'vue';
|
||||||
import MkButton from '@client/components/ui/button.vue';
|
import FormSwitch from '@client/components/form/switch.vue';
|
||||||
import MkInput from '@client/components/ui/input.vue';
|
import FormInput from '@client/components/form/input.vue';
|
||||||
import MkTextarea from '@client/components/ui/textarea.vue';
|
import FormButton from '@client/components/form/button.vue';
|
||||||
import MkSwitch from '@client/components/ui/switch.vue';
|
import FormBase from '@client/components/form/base.vue';
|
||||||
import MkInfo from '@client/components/ui/info.vue';
|
import FormGroup from '@client/components/form/group.vue';
|
||||||
import { url } from '@client/config';
|
import FormTextarea from '@client/components/form/textarea.vue';
|
||||||
import getAcct from '@/misc/acct/render';
|
import FormInfo from '@client/components/form/info.vue';
|
||||||
|
import FormSuspense from '@client/components/form/suspense.vue';
|
||||||
import * as os from '@client/os';
|
import * as os from '@client/os';
|
||||||
import { fetchInstance } from '@client/instance';
|
|
||||||
import * as symbols from '@client/symbols';
|
import * as symbols from '@client/symbols';
|
||||||
|
import { fetchInstance } from '@client/instance';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
MkButton,
|
FormSwitch,
|
||||||
MkInput,
|
FormInput,
|
||||||
MkTextarea,
|
FormBase,
|
||||||
MkSwitch,
|
FormGroup,
|
||||||
MkInfo,
|
FormButton,
|
||||||
Captcha: defineAsyncComponent(() => import('@client/components/captcha.vue')),
|
FormTextarea,
|
||||||
|
FormInfo,
|
||||||
|
FormSuspense,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
emits: ['info'],
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
[symbols.PAGE_INFO]: {
|
[symbols.PAGE_INFO]: {
|
||||||
title: this.$ts.instance,
|
title: this.$ts.general,
|
||||||
icon: 'fas fa-cog',
|
icon: 'fas fa-cog'
|
||||||
},
|
},
|
||||||
meta: null,
|
|
||||||
url,
|
|
||||||
proxyAccount: null,
|
|
||||||
proxyAccountId: null,
|
|
||||||
cacheRemoteFiles: false,
|
|
||||||
proxyRemoteFiles: false,
|
|
||||||
localDriveCapacityMb: 0,
|
|
||||||
remoteDriveCapacityMb: 0,
|
|
||||||
blockedHosts: '',
|
|
||||||
pinnedUsers: '',
|
|
||||||
pinnedPages: '',
|
|
||||||
pinnedClipId: null,
|
|
||||||
maintainerName: null,
|
|
||||||
maintainerEmail: null,
|
|
||||||
name: null,
|
name: null,
|
||||||
description: null,
|
description: null,
|
||||||
tosUrl: null as string | null,
|
tosUrl: null as string | null,
|
||||||
enableEmail: false,
|
maintainerName: null,
|
||||||
email: null,
|
maintainerEmail: null,
|
||||||
bannerUrl: null,
|
|
||||||
iconUrl: null,
|
iconUrl: null,
|
||||||
logoImageUrl: null,
|
bannerUrl: null,
|
||||||
backgroundImageUrl: null,
|
|
||||||
maxNoteTextLength: 0,
|
maxNoteTextLength: 0,
|
||||||
enableRegistration: false,
|
|
||||||
enableLocalTimeline: false,
|
enableLocalTimeline: false,
|
||||||
enableGlobalTimeline: false,
|
enableGlobalTimeline: false,
|
||||||
enableHcaptcha: false,
|
|
||||||
hcaptchaSiteKey: null,
|
|
||||||
hcaptchaSecretKey: null,
|
|
||||||
enableRecaptcha: false,
|
|
||||||
recaptchaSiteKey: null,
|
|
||||||
recaptchaSecretKey: null,
|
|
||||||
enableServiceWorker: false,
|
|
||||||
swPublicKey: null,
|
|
||||||
swPrivateKey: null,
|
|
||||||
useObjectStorage: false,
|
|
||||||
objectStorageBaseUrl: null,
|
|
||||||
objectStorageBucket: null,
|
|
||||||
objectStoragePrefix: null,
|
|
||||||
objectStorageEndpoint: null,
|
|
||||||
objectStorageRegion: null,
|
|
||||||
objectStoragePort: null,
|
|
||||||
objectStorageAccessKey: null,
|
|
||||||
objectStorageSecretKey: null,
|
|
||||||
objectStorageUseSSL: false,
|
|
||||||
objectStorageUseProxy: false,
|
|
||||||
objectStorageSetPublicRead: false,
|
|
||||||
objectStorageS3ForcePathStyle: true,
|
|
||||||
enableTwitterIntegration: false,
|
|
||||||
twitterConsumerKey: null,
|
|
||||||
twitterConsumerSecret: null,
|
|
||||||
enableGithubIntegration: false,
|
|
||||||
githubClientId: null,
|
|
||||||
githubClientSecret: null,
|
|
||||||
enableDiscordIntegration: false,
|
|
||||||
discordClientId: null,
|
|
||||||
discordClientSecret: null,
|
|
||||||
useStarForReactionFallback: false,
|
|
||||||
smtpSecure: false,
|
|
||||||
smtpHost: '',
|
|
||||||
smtpPort: 0,
|
|
||||||
smtpUser: '',
|
|
||||||
smtpPass: '',
|
|
||||||
summalyProxy: '',
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async created() {
|
async mounted() {
|
||||||
this.meta = await os.api('meta', { detail: true });
|
this.$emit('info', this[symbols.PAGE_INFO]);
|
||||||
|
|
||||||
this.name = this.meta.name;
|
|
||||||
this.description = this.meta.description;
|
|
||||||
this.tosUrl = this.meta.tosUrl;
|
|
||||||
this.bannerUrl = this.meta.bannerUrl;
|
|
||||||
this.iconUrl = this.meta.iconUrl;
|
|
||||||
this.logoImageUrl = this.meta.logoImageUrl;
|
|
||||||
this.backgroundImageUrl = this.meta.backgroundImageUrl;
|
|
||||||
this.enableEmail = this.meta.enableEmail;
|
|
||||||
this.email = this.meta.email;
|
|
||||||
this.maintainerName = this.meta.maintainerName;
|
|
||||||
this.maintainerEmail = this.meta.maintainerEmail;
|
|
||||||
this.maxNoteTextLength = this.meta.maxNoteTextLength;
|
|
||||||
this.enableRegistration = !this.meta.disableRegistration;
|
|
||||||
this.enableLocalTimeline = !this.meta.disableLocalTimeline;
|
|
||||||
this.enableGlobalTimeline = !this.meta.disableGlobalTimeline;
|
|
||||||
this.enableHcaptcha = this.meta.enableHcaptcha;
|
|
||||||
this.hcaptchaSiteKey = this.meta.hcaptchaSiteKey;
|
|
||||||
this.hcaptchaSecretKey = this.meta.hcaptchaSecretKey;
|
|
||||||
this.enableRecaptcha = this.meta.enableRecaptcha;
|
|
||||||
this.recaptchaSiteKey = this.meta.recaptchaSiteKey;
|
|
||||||
this.recaptchaSecretKey = this.meta.recaptchaSecretKey;
|
|
||||||
this.proxyAccountId = this.meta.proxyAccountId;
|
|
||||||
this.cacheRemoteFiles = this.meta.cacheRemoteFiles;
|
|
||||||
this.proxyRemoteFiles = this.meta.proxyRemoteFiles;
|
|
||||||
this.localDriveCapacityMb = this.meta.driveCapacityPerLocalUserMb;
|
|
||||||
this.remoteDriveCapacityMb = this.meta.driveCapacityPerRemoteUserMb;
|
|
||||||
this.blockedHosts = this.meta.blockedHosts.join('\n');
|
|
||||||
this.pinnedUsers = this.meta.pinnedUsers.join('\n');
|
|
||||||
this.pinnedPages = this.meta.pinnedPages.join('\n');
|
|
||||||
this.pinnedClipId = this.meta.pinnedClipId;
|
|
||||||
this.enableServiceWorker = this.meta.enableServiceWorker;
|
|
||||||
this.swPublicKey = this.meta.swPublickey;
|
|
||||||
this.swPrivateKey = this.meta.swPrivateKey;
|
|
||||||
this.useObjectStorage = this.meta.useObjectStorage;
|
|
||||||
this.objectStorageBaseUrl = this.meta.objectStorageBaseUrl;
|
|
||||||
this.objectStorageBucket = this.meta.objectStorageBucket;
|
|
||||||
this.objectStoragePrefix = this.meta.objectStoragePrefix;
|
|
||||||
this.objectStorageEndpoint = this.meta.objectStorageEndpoint;
|
|
||||||
this.objectStorageRegion = this.meta.objectStorageRegion;
|
|
||||||
this.objectStoragePort = this.meta.objectStoragePort;
|
|
||||||
this.objectStorageAccessKey = this.meta.objectStorageAccessKey;
|
|
||||||
this.objectStorageSecretKey = this.meta.objectStorageSecretKey;
|
|
||||||
this.objectStorageUseSSL = this.meta.objectStorageUseSSL;
|
|
||||||
this.objectStorageUseProxy = this.meta.objectStorageUseProxy;
|
|
||||||
this.objectStorageSetPublicRead = this.meta.objectStorageSetPublicRead;
|
|
||||||
this.objectStorageS3ForcePathStyle = this.meta.objectStorageS3ForcePathStyle;
|
|
||||||
this.enableTwitterIntegration = this.meta.enableTwitterIntegration;
|
|
||||||
this.twitterConsumerKey = this.meta.twitterConsumerKey;
|
|
||||||
this.twitterConsumerSecret = this.meta.twitterConsumerSecret;
|
|
||||||
this.enableGithubIntegration = this.meta.enableGithubIntegration;
|
|
||||||
this.githubClientId = this.meta.githubClientId;
|
|
||||||
this.githubClientSecret = this.meta.githubClientSecret;
|
|
||||||
this.enableDiscordIntegration = this.meta.enableDiscordIntegration;
|
|
||||||
this.discordClientId = this.meta.discordClientId;
|
|
||||||
this.discordClientSecret = this.meta.discordClientSecret;
|
|
||||||
this.useStarForReactionFallback = this.meta.useStarForReactionFallback;
|
|
||||||
this.smtpSecure = this.meta.smtpSecure;
|
|
||||||
this.smtpHost = this.meta.smtpHost;
|
|
||||||
this.smtpPort = this.meta.smtpPort;
|
|
||||||
this.smtpUser = this.meta.smtpUser;
|
|
||||||
this.smtpPass = this.meta.smtpPass;
|
|
||||||
this.summalyProxy = this.meta.summalyProxy;
|
|
||||||
|
|
||||||
if (this.proxyAccountId) {
|
|
||||||
os.api('users/show', { userId: this.proxyAccountId }).then(proxyAccount => {
|
|
||||||
this.proxyAccount = proxyAccount;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
mounted() {
|
|
||||||
this.$watch('enableHcaptcha', () => {
|
|
||||||
if (this.enableHcaptcha && this.enableRecaptcha) {
|
|
||||||
os.dialog({
|
|
||||||
type: 'question', // warning だと間違って cancel するかもしれない
|
|
||||||
showCancelButton: true,
|
|
||||||
title: this.$ts.settingGuide,
|
|
||||||
text: this.$ts.avoidMultiCaptchaConfirm,
|
|
||||||
}).then(({ canceled }) => {
|
|
||||||
if (canceled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.enableRecaptcha = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.$watch('enableRecaptcha', () => {
|
|
||||||
if (this.enableRecaptcha && this.enableHcaptcha) {
|
|
||||||
os.dialog({
|
|
||||||
type: 'question', // warning だと間違って cancel するかもしれない
|
|
||||||
showCancelButton: true,
|
|
||||||
title: this.$ts.settingGuide,
|
|
||||||
text: this.$ts.avoidMultiCaptchaConfirm,
|
|
||||||
}).then(({ canceled }) => {
|
|
||||||
if (canceled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.enableHcaptcha = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
invite() {
|
async init() {
|
||||||
os.api('admin/invite').then(x => {
|
const meta = await os.api('meta', { detail: true });
|
||||||
os.dialog({
|
this.name = meta.name;
|
||||||
type: 'info',
|
this.description = meta.description;
|
||||||
text: x.code
|
this.tosUrl = meta.tosUrl;
|
||||||
});
|
this.iconUrl = meta.iconUrl;
|
||||||
}).catch(e => {
|
this.bannerUrl = meta.bannerUrl;
|
||||||
os.dialog({
|
this.maintainerName = meta.maintainerName;
|
||||||
type: 'error',
|
this.maintainerEmail = meta.maintainerEmail;
|
||||||
text: e
|
this.maxNoteTextLength = meta.maxNoteTextLength;
|
||||||
});
|
this.enableLocalTimeline = !meta.disableLocalTimeline;
|
||||||
});
|
this.enableGlobalTimeline = !meta.disableGlobalTimeline;
|
||||||
},
|
},
|
||||||
|
|
||||||
addPinUser() {
|
save() {
|
||||||
os.selectUser().then(user => {
|
os.apiWithDialog('admin/update-meta', {
|
||||||
this.pinnedUsers = this.pinnedUsers.trim();
|
|
||||||
this.pinnedUsers += '\n@' + getAcct(user);
|
|
||||||
this.pinnedUsers = this.pinnedUsers.trim();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
chooseProxyAccount() {
|
|
||||||
os.selectUser().then(user => {
|
|
||||||
this.proxyAccount = user;
|
|
||||||
this.proxyAccountId = user.id;
|
|
||||||
this.save(true);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
async testEmail() {
|
|
||||||
os.api('admin/send-email', {
|
|
||||||
to: this.maintainerEmail,
|
|
||||||
subject: 'Test email',
|
|
||||||
text: 'Yo'
|
|
||||||
}).then(x => {
|
|
||||||
os.dialog({
|
|
||||||
type: 'success',
|
|
||||||
splash: true
|
|
||||||
});
|
|
||||||
}).catch(e => {
|
|
||||||
os.dialog({
|
|
||||||
type: 'error',
|
|
||||||
text: e
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
save(withDialog = false) {
|
|
||||||
os.api('admin/update-meta', {
|
|
||||||
name: this.name,
|
name: this.name,
|
||||||
description: this.description,
|
description: this.description,
|
||||||
tosUrl: this.tosUrl,
|
tosUrl: this.tosUrl,
|
||||||
bannerUrl: this.bannerUrl,
|
|
||||||
iconUrl: this.iconUrl,
|
iconUrl: this.iconUrl,
|
||||||
logoImageUrl: this.logoImageUrl,
|
bannerUrl: this.bannerUrl,
|
||||||
backgroundImageUrl: this.backgroundImageUrl,
|
|
||||||
maintainerName: this.maintainerName,
|
maintainerName: this.maintainerName,
|
||||||
maintainerEmail: this.maintainerEmail,
|
maintainerEmail: this.maintainerEmail,
|
||||||
maxNoteTextLength: this.maxNoteTextLength,
|
maxNoteTextLength: this.maxNoteTextLength,
|
||||||
disableRegistration: !this.enableRegistration,
|
|
||||||
disableLocalTimeline: !this.enableLocalTimeline,
|
disableLocalTimeline: !this.enableLocalTimeline,
|
||||||
disableGlobalTimeline: !this.enableGlobalTimeline,
|
disableGlobalTimeline: !this.enableGlobalTimeline,
|
||||||
enableHcaptcha: this.enableHcaptcha,
|
|
||||||
hcaptchaSiteKey: this.hcaptchaSiteKey,
|
|
||||||
hcaptchaSecretKey: this.hcaptchaSecretKey,
|
|
||||||
enableRecaptcha: this.enableRecaptcha,
|
|
||||||
recaptchaSiteKey: this.recaptchaSiteKey,
|
|
||||||
recaptchaSecretKey: this.recaptchaSecretKey,
|
|
||||||
proxyAccountId: this.proxyAccountId,
|
|
||||||
cacheRemoteFiles: this.cacheRemoteFiles,
|
|
||||||
proxyRemoteFiles: this.proxyRemoteFiles,
|
|
||||||
localDriveCapacityMb: parseInt(this.localDriveCapacityMb, 10),
|
|
||||||
remoteDriveCapacityMb: parseInt(this.remoteDriveCapacityMb, 10),
|
|
||||||
blockedHosts: this.blockedHosts.split('\n') || [],
|
|
||||||
pinnedUsers: this.pinnedUsers ? this.pinnedUsers.split('\n') : [],
|
|
||||||
pinnedPages: this.pinnedPages ? this.pinnedPages.split('\n') : [],
|
|
||||||
pinnedClipId: (this.pinnedClipId && this.pinnedClipId) != '' ? this.pinnedClipId : null,
|
|
||||||
enableServiceWorker: this.enableServiceWorker,
|
|
||||||
swPublicKey: this.swPublicKey,
|
|
||||||
swPrivateKey: this.swPrivateKey,
|
|
||||||
useObjectStorage: this.useObjectStorage,
|
|
||||||
objectStorageBaseUrl: this.objectStorageBaseUrl ? this.objectStorageBaseUrl : null,
|
|
||||||
objectStorageBucket: this.objectStorageBucket ? this.objectStorageBucket : null,
|
|
||||||
objectStoragePrefix: this.objectStoragePrefix ? this.objectStoragePrefix : null,
|
|
||||||
objectStorageEndpoint: this.objectStorageEndpoint ? this.objectStorageEndpoint : null,
|
|
||||||
objectStorageRegion: this.objectStorageRegion ? this.objectStorageRegion : null,
|
|
||||||
objectStoragePort: this.objectStoragePort ? this.objectStoragePort : null,
|
|
||||||
objectStorageAccessKey: this.objectStorageAccessKey ? this.objectStorageAccessKey : null,
|
|
||||||
objectStorageSecretKey: this.objectStorageSecretKey ? this.objectStorageSecretKey : null,
|
|
||||||
objectStorageUseSSL: this.objectStorageUseSSL,
|
|
||||||
objectStorageUseProxy: this.objectStorageUseProxy,
|
|
||||||
objectStorageSetPublicRead: this.objectStorageSetPublicRead,
|
|
||||||
objectStorageS3ForcePathStyle: this.objectStorageS3ForcePathStyle,
|
|
||||||
enableTwitterIntegration: this.enableTwitterIntegration,
|
|
||||||
twitterConsumerKey: this.twitterConsumerKey,
|
|
||||||
twitterConsumerSecret: this.twitterConsumerSecret,
|
|
||||||
enableGithubIntegration: this.enableGithubIntegration,
|
|
||||||
githubClientId: this.githubClientId,
|
|
||||||
githubClientSecret: this.githubClientSecret,
|
|
||||||
enableDiscordIntegration: this.enableDiscordIntegration,
|
|
||||||
discordClientId: this.discordClientId,
|
|
||||||
discordClientSecret: this.discordClientSecret,
|
|
||||||
enableEmail: this.enableEmail,
|
|
||||||
email: this.email,
|
|
||||||
smtpSecure: this.smtpSecure,
|
|
||||||
smtpHost: this.smtpHost,
|
|
||||||
smtpPort: this.smtpPort,
|
|
||||||
smtpUser: this.smtpUser,
|
|
||||||
smtpPass: this.smtpPass,
|
|
||||||
summalyProxy: this.summalyProxy,
|
|
||||||
useStarForReactionFallback: this.useStarForReactionFallback,
|
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
fetchInstance();
|
fetchInstance();
|
||||||
if (withDialog) {
|
|
||||||
os.success();
|
|
||||||
}
|
|
||||||
}).catch(e => {
|
|
||||||
os.dialog({
|
|
||||||
type: 'error',
|
|
||||||
text: e
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,230 +0,0 @@
|
||||||
<template>
|
|
||||||
<XModalWindow ref="dialog"
|
|
||||||
:width="370"
|
|
||||||
@close="$refs.dialog.close()"
|
|
||||||
@closed="$emit('closed')"
|
|
||||||
>
|
|
||||||
<template #header v-if="user"><MkUserName class="name" :user="user"/></template>
|
|
||||||
<div class="vrcsvlkm" v-if="user && info">
|
|
||||||
<div class="_section">
|
|
||||||
<div class="banner" :style="bannerStyle">
|
|
||||||
<MkAvatar class="avatar" :user="user" :show-indicator="true"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="_section">
|
|
||||||
<div class="title">
|
|
||||||
<span class="acct">@{{ acct(user) }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="status">
|
|
||||||
<span class="staff" v-if="user.isAdmin"><i class="fas fa-bookmark"></i></span>
|
|
||||||
<span class="staff" v-if="user.isModerator"><i class="far fa-bookmark"></i></span>
|
|
||||||
<span class="punished" v-if="user.isSilenced"><i class="fas fa-microphone-slash"></i></span>
|
|
||||||
<span class="punished" v-if="user.isSuspended"><i class="fas fa-snowflake"></i></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="_section">
|
|
||||||
<div class="_content">
|
|
||||||
<MkSwitch v-if="user.host == null && $i.isAdmin && (moderator || !user.isAdmin)" @update:value="toggleModerator" v-model:value="moderator">{{ $ts.moderator }}</MkSwitch>
|
|
||||||
<MkSwitch @update:value="toggleSilence" v-model:value="silenced">{{ $ts.silence }}</MkSwitch>
|
|
||||||
<MkSwitch @update:value="toggleSuspend" v-model:value="suspended">{{ $ts.suspend }}</MkSwitch>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="_section">
|
|
||||||
<div class="_content">
|
|
||||||
<MkButton full @click="openProfile"><i class="fas fa-external-link-square-alt"></i> {{ $ts.profile }}</MkButton>
|
|
||||||
<MkButton full v-if="user.host != null" @click="updateRemoteUser"><i class="fas fa-sync"></i> {{ $ts.updateRemoteUser }}</MkButton>
|
|
||||||
<MkButton full @click="resetPassword"><i class="fas fa-key"></i> {{ $ts.resetPassword }}</MkButton>
|
|
||||||
<MkButton full @click="deleteAllFiles" danger><i class="fas fa-trash-alt"></i> {{ $ts.deleteAllFiles }}</MkButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="_section">
|
|
||||||
<details class="_content rawdata">
|
|
||||||
<pre><code>{{ JSON.stringify(info, null, 2) }}</code></pre>
|
|
||||||
</details>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</XModalWindow>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { computed, defineComponent } from 'vue';
|
|
||||||
import MkButton from '@client/components/ui/button.vue';
|
|
||||||
import MkSwitch from '@client/components/ui/switch.vue';
|
|
||||||
import XModalWindow from '@client/components/ui/modal-window.vue';
|
|
||||||
import Progress from '@client/scripts/loading';
|
|
||||||
import { acct, userPage } from '../../filters/user';
|
|
||||||
import * as os from '@client/os';
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
components: {
|
|
||||||
MkButton,
|
|
||||||
MkSwitch,
|
|
||||||
XModalWindow,
|
|
||||||
},
|
|
||||||
|
|
||||||
props: {
|
|
||||||
userId: {
|
|
||||||
required: true,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
emits: ['closed'],
|
|
||||||
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
user: null,
|
|
||||||
info: null,
|
|
||||||
moderator: false,
|
|
||||||
silenced: false,
|
|
||||||
suspended: false,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
bannerStyle(): any {
|
|
||||||
if (this.user.bannerUrl == null) return {};
|
|
||||||
return {
|
|
||||||
backgroundImage: `url(${ this.user.bannerUrl })`
|
|
||||||
};
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
created() {
|
|
||||||
this.fetch();
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
async fetch() {
|
|
||||||
Progress.start();
|
|
||||||
this.user = await os.api('users/show', { userId: this.userId });
|
|
||||||
this.info = await os.api('admin/show-user', { userId: this.userId });
|
|
||||||
this.moderator = this.info.isModerator;
|
|
||||||
this.silenced = this.info.isSilenced;
|
|
||||||
this.suspended = this.info.isSuspended;
|
|
||||||
Progress.done();
|
|
||||||
},
|
|
||||||
|
|
||||||
/** 処理対象ユーザーの情報を更新する */
|
|
||||||
async refreshUser() {
|
|
||||||
this.user = await os.api('users/show', { userId: this.user.id });
|
|
||||||
this.info = await os.api('admin/show-user', { userId: this.user.id });
|
|
||||||
},
|
|
||||||
|
|
||||||
openProfile() {
|
|
||||||
window.open(userPage(this.user, null, true), '_blank');
|
|
||||||
},
|
|
||||||
|
|
||||||
async updateRemoteUser() {
|
|
||||||
await os.api('admin/update-remote-user', { userId: this.user.id }).then(res => {
|
|
||||||
os.success();
|
|
||||||
});
|
|
||||||
await this.refreshUser();
|
|
||||||
},
|
|
||||||
|
|
||||||
async resetPassword() {
|
|
||||||
os.apiWithDialog('admin/reset-password', {
|
|
||||||
userId: this.user.id,
|
|
||||||
}, undefined, ({ password }) => {
|
|
||||||
os.dialog({
|
|
||||||
type: 'success',
|
|
||||||
text: this.$t('newPasswordIs', { password })
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
async toggleSilence(v) {
|
|
||||||
const confirm = await os.dialog({
|
|
||||||
type: 'warning',
|
|
||||||
showCancelButton: true,
|
|
||||||
text: v ? this.$ts.silenceConfirm : this.$ts.unsilenceConfirm,
|
|
||||||
});
|
|
||||||
if (confirm.canceled) {
|
|
||||||
this.silenced = !v;
|
|
||||||
} else {
|
|
||||||
await os.api(v ? 'admin/silence-user' : 'admin/unsilence-user', { userId: this.user.id });
|
|
||||||
await this.refreshUser();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async toggleSuspend(v) {
|
|
||||||
const confirm = await os.dialog({
|
|
||||||
type: 'warning',
|
|
||||||
showCancelButton: true,
|
|
||||||
text: v ? this.$ts.suspendConfirm : this.$ts.unsuspendConfirm,
|
|
||||||
});
|
|
||||||
if (confirm.canceled) {
|
|
||||||
this.suspended = !v;
|
|
||||||
} else {
|
|
||||||
await os.api(v ? 'admin/suspend-user' : 'admin/unsuspend-user', { userId: this.user.id });
|
|
||||||
await this.refreshUser();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async toggleModerator(v) {
|
|
||||||
await os.api(v ? 'admin/moderators/add' : 'admin/moderators/remove', { userId: this.user.id });
|
|
||||||
await this.refreshUser();
|
|
||||||
},
|
|
||||||
|
|
||||||
async deleteAllFiles() {
|
|
||||||
const confirm = await os.dialog({
|
|
||||||
type: 'warning',
|
|
||||||
showCancelButton: true,
|
|
||||||
text: this.$ts.deleteAllFilesConfirm,
|
|
||||||
});
|
|
||||||
if (confirm.canceled) return;
|
|
||||||
const process = async () => {
|
|
||||||
await os.api('admin/delete-all-files-of-a-user', { userId: this.user.id });
|
|
||||||
os.success();
|
|
||||||
};
|
|
||||||
await process().catch(e => {
|
|
||||||
os.dialog({
|
|
||||||
type: 'error',
|
|
||||||
text: e.toString()
|
|
||||||
});
|
|
||||||
});
|
|
||||||
await this.refreshUser();
|
|
||||||
},
|
|
||||||
|
|
||||||
acct
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.vrcsvlkm {
|
|
||||||
> ._section {
|
|
||||||
> .banner {
|
|
||||||
position: relative;
|
|
||||||
height: 100px;
|
|
||||||
background-color: #4c5e6d;
|
|
||||||
background-size: cover;
|
|
||||||
background-position: center;
|
|
||||||
border-radius: 8px;
|
|
||||||
|
|
||||||
> .avatar {
|
|
||||||
position: absolute;
|
|
||||||
top: 60px;
|
|
||||||
width: 64px;
|
|
||||||
height: 64px;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
margin: 0 auto;
|
|
||||||
border: solid 4px var(--panel);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> .title {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .status {
|
|
||||||
text-align: center;
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .rawdata {
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,24 +1,11 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="mk-instance-users">
|
<div class="lknzcolw">
|
||||||
<div class="_section">
|
<div class="actions">
|
||||||
<div class="_content">
|
|
||||||
<MkButton inline primary @click="addUser()"><i class="fas fa-plus"></i> {{ $ts.addUser }}</MkButton>
|
<MkButton inline primary @click="addUser()"><i class="fas fa-plus"></i> {{ $ts.addUser }}</MkButton>
|
||||||
</div>
|
<MkButton inline primary @click="lookupUser()"><i class="fas fa-search"></i> {{ $ts.lookup }}</MkButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="_section lookup">
|
<div class="users">
|
||||||
<div class="_title"><i class="fas fa-search"></i> {{ $ts.lookup }}</div>
|
|
||||||
<div class="_content">
|
|
||||||
<MkInput class="target" v-model:value="target" type="text" @enter="showUser()">
|
|
||||||
<span>{{ $ts.usernameOrUserId }}</span>
|
|
||||||
</MkInput>
|
|
||||||
<MkButton @click="showUser()" primary><i class="fas fa-search"></i> {{ $ts.lookup }}</MkButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="_section users">
|
|
||||||
<div class="_title"><i class="fas fa-users"></i> {{ $ts.users }}</div>
|
|
||||||
<div class="_content">
|
|
||||||
<div class="inputs" style="display: flex;">
|
<div class="inputs" style="display: flex;">
|
||||||
<MkSelect v-model:value="sort" style="margin: 0; flex: 1;">
|
<MkSelect v-model:value="sort" style="margin: 0; flex: 1;">
|
||||||
<template #label>{{ $ts.sort }}</template>
|
<template #label>{{ $ts.sort }}</template>
|
||||||
|
@ -75,12 +62,10 @@
|
||||||
</MkPagination>
|
</MkPagination>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent } from 'vue';
|
import { defineComponent } from 'vue';
|
||||||
import parseAcct from '@/misc/acct/parse';
|
|
||||||
import MkButton from '@client/components/ui/button.vue';
|
import MkButton from '@client/components/ui/button.vue';
|
||||||
import MkInput from '@client/components/ui/input.vue';
|
import MkInput from '@client/components/ui/input.vue';
|
||||||
import MkSelect from '@client/components/ui/select.vue';
|
import MkSelect from '@client/components/ui/select.vue';
|
||||||
|
@ -88,6 +73,7 @@ import MkPagination from '@client/components/ui/pagination.vue';
|
||||||
import { acct } from '../../filters/user';
|
import { acct } from '../../filters/user';
|
||||||
import * as os from '@client/os';
|
import * as os from '@client/os';
|
||||||
import * as symbols from '@client/symbols';
|
import * as symbols from '@client/symbols';
|
||||||
|
import { lookupUser } from '@client/scripts/lookup-user';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
|
@ -97,6 +83,8 @@ export default defineComponent({
|
||||||
MkPagination,
|
MkPagination,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
emits: ['info'],
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
[symbols.PAGE_INFO]: {
|
[symbols.PAGE_INFO]: {
|
||||||
|
@ -107,7 +95,6 @@ export default defineComponent({
|
||||||
handler: this.searchUser
|
handler: this.searchUser
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
target: '',
|
|
||||||
sort: '+createdAt',
|
sort: '+createdAt',
|
||||||
state: 'all',
|
state: 'all',
|
||||||
origin: 'local',
|
origin: 'local',
|
||||||
|
@ -140,40 +127,12 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
async mounted() {
|
||||||
/** テキストエリアのユーザーを解決する */
|
this.$emit('info', this[symbols.PAGE_INFO]);
|
||||||
fetchUser() {
|
|
||||||
return new Promise((res) => {
|
|
||||||
const usernamePromise = os.api('users/show', parseAcct(this.target));
|
|
||||||
const idPromise = os.api('users/show', { userId: this.target });
|
|
||||||
let _notFound = false;
|
|
||||||
const notFound = () => {
|
|
||||||
if (_notFound) {
|
|
||||||
os.dialog({
|
|
||||||
type: 'error',
|
|
||||||
text: this.$ts.noSuchUser
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
_notFound = true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
usernamePromise.then(res).catch(e => {
|
|
||||||
if (e.code === 'NO_SUCH_USER') {
|
|
||||||
notFound();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
idPromise.then(res).catch(e => {
|
|
||||||
notFound();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/** テキストエリアから処理対象ユーザーを設定する */
|
methods: {
|
||||||
async showUser() {
|
lookupUser,
|
||||||
const user = await this.fetchUser();
|
|
||||||
this.show(user);
|
|
||||||
this.target = '';
|
|
||||||
},
|
|
||||||
|
|
||||||
searchUser() {
|
searchUser() {
|
||||||
os.selectUser().then(user => {
|
os.selectUser().then(user => {
|
||||||
|
@ -203,9 +162,7 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
|
|
||||||
show(user) {
|
show(user) {
|
||||||
os.popup(import('./user-dialog.vue'), {
|
os.pageWindow(`/user-info/${user.id}`);
|
||||||
userId: user.id
|
|
||||||
}, {}, 'closed');
|
|
||||||
},
|
},
|
||||||
|
|
||||||
acct
|
acct
|
||||||
|
@ -214,9 +171,14 @@ export default defineComponent({
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.mk-instance-users {
|
.lknzcolw {
|
||||||
|
> .actions {
|
||||||
|
margin: var(--margin);
|
||||||
|
}
|
||||||
|
|
||||||
> .users {
|
> .users {
|
||||||
> ._content {
|
margin: var(--margin);
|
||||||
|
|
||||||
> .users {
|
> .users {
|
||||||
margin-top: var(--margin);
|
margin-top: var(--margin);
|
||||||
|
|
||||||
|
@ -271,5 +233,4 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
148
src/client/pages/settings/accounts.vue
Normal file
148
src/client/pages/settings/accounts.vue
Normal file
|
@ -0,0 +1,148 @@
|
||||||
|
<template>
|
||||||
|
<FormBase>
|
||||||
|
<FormSuspense :p="init">
|
||||||
|
<FormButton @click="addAccount" primary><i class="fas fa-plus"></i> {{ $ts.addAccount }}</FormButton>
|
||||||
|
|
||||||
|
<div class="_formItem _button" v-for="account in accounts" :key="account.id" @click="menu(account, $event)">
|
||||||
|
<div class="_formPanel 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 '@client/components/form/suspense.vue';
|
||||||
|
import FormLink from '@client/components/form/link.vue';
|
||||||
|
import FormBase from '@client/components/form/base.vue';
|
||||||
|
import FormGroup from '@client/components/form/group.vue';
|
||||||
|
import FormButton from '@client/components/form/button.vue';
|
||||||
|
import * as os from '@client/os';
|
||||||
|
import * as symbols from '@client/symbols';
|
||||||
|
import { getAccounts, addAccount, login } from '@client/account';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
components: {
|
||||||
|
FormBase,
|
||||||
|
FormSuspense,
|
||||||
|
FormButton,
|
||||||
|
},
|
||||||
|
|
||||||
|
emits: ['info'],
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
[symbols.PAGE_INFO]: {
|
||||||
|
title: this.$ts.accounts,
|
||||||
|
icon: 'fas fa-users',
|
||||||
|
},
|
||||||
|
storedAccounts: getAccounts().filter(x => x.id !== this.$i.id),
|
||||||
|
accounts: null,
|
||||||
|
init: () => os.api('users/show', {
|
||||||
|
userIds: this.storedAccounts.map(x => x.id)
|
||||||
|
}).then(accounts => {
|
||||||
|
this.accounts = accounts;
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.$emit('info', this[symbols.PAGE_INFO]);
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
menu(account, ev) {
|
||||||
|
os.modalMenu([{
|
||||||
|
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.modalMenu([{
|
||||||
|
text: this.$ts.existingAccount,
|
||||||
|
action: () => { this.addExistingAccount(); },
|
||||||
|
}, {
|
||||||
|
text: this.$ts.createAccount,
|
||||||
|
action: () => { this.createAccount(); },
|
||||||
|
}], ev.currentTarget || ev.target);
|
||||||
|
},
|
||||||
|
|
||||||
|
addExistingAccount() {
|
||||||
|
os.popup(import('@client/components/signin-dialog.vue'), {}, {
|
||||||
|
done: res => {
|
||||||
|
addAccount(res.id, res.i);
|
||||||
|
os.success();
|
||||||
|
},
|
||||||
|
}, 'closed');
|
||||||
|
},
|
||||||
|
|
||||||
|
createAccount() {
|
||||||
|
os.popup(import('@client/components/signup-dialog.vue'), {}, {
|
||||||
|
done: res => {
|
||||||
|
addAccount(res.id, res.i);
|
||||||
|
this.switchAccountWithToken(res.i);
|
||||||
|
},
|
||||||
|
}, 'closed');
|
||||||
|
},
|
||||||
|
|
||||||
|
switchAccount(account: any) {
|
||||||
|
const storedAccounts = 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>
|
|
@ -2,6 +2,14 @@
|
||||||
<div class="vvcocwet" :class="{ wide: !narrow }" ref="el">
|
<div class="vvcocwet" :class="{ wide: !narrow }" ref="el">
|
||||||
<div class="nav" v-if="!narrow || page == null">
|
<div class="nav" v-if="!narrow || page == null">
|
||||||
<FormBase>
|
<FormBase>
|
||||||
|
<FormGroup>
|
||||||
|
<div class="_formItem">
|
||||||
|
<div class="_formPanel lwjxoukj">
|
||||||
|
<MkAvatar :user="$i" class="avatar"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<FormLink :active="page === 'accounts'" replace to="/settings/accounts"><template #icon><i class="fas fa-users"></i></template>{{ $ts.accounts }}</FormLink>
|
||||||
|
</FormGroup>
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
<template #label>{{ $ts.basicSettings }}</template>
|
<template #label>{{ $ts.basicSettings }}</template>
|
||||||
<FormLink :active="page === 'profile'" replace to="/settings/profile"><template #icon><i class="fas fa-user"></i></template>{{ $ts.profile }}</FormLink>
|
<FormLink :active="page === 'profile'" replace to="/settings/profile"><template #icon><i class="fas fa-user"></i></template>{{ $ts.profile }}</FormLink>
|
||||||
|
@ -87,6 +95,7 @@ export default defineComponent({
|
||||||
const component = computed(() => {
|
const component = computed(() => {
|
||||||
if (page.value == null) return null;
|
if (page.value == null) return null;
|
||||||
switch (page.value) {
|
switch (page.value) {
|
||||||
|
case 'accounts': return defineAsyncComponent(() => import('./accounts.vue'));
|
||||||
case 'profile': return defineAsyncComponent(() => import('./profile.vue'));
|
case 'profile': return defineAsyncComponent(() => import('./profile.vue'));
|
||||||
case 'privacy': return defineAsyncComponent(() => import('./privacy.vue'));
|
case 'privacy': return defineAsyncComponent(() => import('./privacy.vue'));
|
||||||
case 'reaction': return defineAsyncComponent(() => import('./reaction.vue'));
|
case 'reaction': return defineAsyncComponent(() => import('./reaction.vue'));
|
||||||
|
@ -209,4 +218,15 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.lwjxoukj {
|
||||||
|
padding: 16px;
|
||||||
|
|
||||||
|
> .avatar {
|
||||||
|
display: block;
|
||||||
|
margin: auto;
|
||||||
|
width: 42px;
|
||||||
|
height: 42px;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,12 +1,34 @@
|
||||||
<template>
|
<template>
|
||||||
<FormBase>
|
<FormBase>
|
||||||
<FormGroup v-if="user">
|
<FormSuspense :p="init">
|
||||||
<template #label><MkAcct :user="user"/></template>
|
<div class="_formItem aeakzknw">
|
||||||
|
<MkAvatar class="avatar" :user="user" :show-indicator="true"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormLink :to="userPage(user)">Profile</FormLink>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormKeyValueView>
|
||||||
|
<template #key>Acct</template>
|
||||||
|
<template #value><span class="_monospace">{{ acct(user) }}</span></template>
|
||||||
|
</FormKeyValueView>
|
||||||
|
|
||||||
<FormKeyValueView>
|
<FormKeyValueView>
|
||||||
<template #key>ID</template>
|
<template #key>ID</template>
|
||||||
<template #value><span class="_monospace">{{ user.id }}</span></template>
|
<template #value><span class="_monospace">{{ user.id }}</span></template>
|
||||||
</FormKeyValueView>
|
</FormKeyValueView>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup v-if="iAmModerator">
|
||||||
|
<FormSwitch v-if="user.host == null && $i.isAdmin && (moderator || !user.isAdmin)" @update:value="toggleModerator" v-model:value="moderator">{{ $ts.moderator }}</FormSwitch>
|
||||||
|
<FormSwitch @update:value="toggleSilence" v-model:value="silenced">{{ $ts.silence }}</FormSwitch>
|
||||||
|
<FormSwitch @update:value="toggleSuspend" v-model:value="suspended">{{ $ts.suspend }}</FormSwitch>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormButton v-if="user.host != null" @click="updateRemoteUser"><i class="fas fa-sync"></i> {{ $ts.updateRemoteUser }}</FormButton>
|
||||||
|
<FormButton v-if="user.host == null && iAmModerator" @click="resetPassword"><i class="fas fa-key"></i> {{ $ts.resetPassword }}</FormButton>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
<FormLink :to="`/user-ap-info/${user.id}`">ActivityPub</FormLink>
|
<FormLink :to="`/user-ap-info/${user.id}`">ActivityPub</FormLink>
|
||||||
|
@ -28,7 +50,7 @@
|
||||||
<FormObjectView tall :value="user">
|
<FormObjectView tall :value="user">
|
||||||
<span>Raw</span>
|
<span>Raw</span>
|
||||||
</FormObjectView>
|
</FormObjectView>
|
||||||
</FormGroup>
|
</FormSuspense>
|
||||||
</FormBase>
|
</FormBase>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -36,6 +58,7 @@
|
||||||
import { computed, defineAsyncComponent, defineComponent } from 'vue';
|
import { computed, defineAsyncComponent, defineComponent } from 'vue';
|
||||||
import FormObjectView from '@client/components/form/object-view.vue';
|
import FormObjectView from '@client/components/form/object-view.vue';
|
||||||
import FormTextarea from '@client/components/form/textarea.vue';
|
import FormTextarea from '@client/components/form/textarea.vue';
|
||||||
|
import FormSwitch from '@client/components/form/switch.vue';
|
||||||
import FormLink from '@client/components/form/link.vue';
|
import FormLink from '@client/components/form/link.vue';
|
||||||
import FormBase from '@client/components/form/base.vue';
|
import FormBase from '@client/components/form/base.vue';
|
||||||
import FormGroup from '@client/components/form/group.vue';
|
import FormGroup from '@client/components/form/group.vue';
|
||||||
|
@ -47,11 +70,13 @@ import number from '@client/filters/number';
|
||||||
import bytes from '@client/filters/bytes';
|
import bytes from '@client/filters/bytes';
|
||||||
import * as symbols from '@client/symbols';
|
import * as symbols from '@client/symbols';
|
||||||
import { url } from '@client/config';
|
import { url } from '@client/config';
|
||||||
|
import { userPage, acct } from '@client/filters/user';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
FormBase,
|
FormBase,
|
||||||
FormTextarea,
|
FormTextarea,
|
||||||
|
FormSwitch,
|
||||||
FormObjectView,
|
FormObjectView,
|
||||||
FormButton,
|
FormButton,
|
||||||
FormLink,
|
FormLink,
|
||||||
|
@ -70,7 +95,7 @@ export default defineComponent({
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
[symbols.PAGE_INFO]: computed(() => ({
|
[symbols.PAGE_INFO]: computed(() => ({
|
||||||
title: this.$ts.userInfo,
|
title: this.user ? acct(this.user) : this.$ts.userInfo,
|
||||||
icon: 'fas fa-info-circle',
|
icon: 'fas fa-info-circle',
|
||||||
actions: this.user ? [this.user.url ? {
|
actions: this.user ? [this.user.url ? {
|
||||||
text: this.user.url,
|
text: this.user.url,
|
||||||
|
@ -80,23 +105,141 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
} : undefined].filter(x => x !== undefined) : [],
|
} : undefined].filter(x => x !== undefined) : [],
|
||||||
})),
|
})),
|
||||||
|
init: null,
|
||||||
user: null,
|
user: null,
|
||||||
|
info: null,
|
||||||
|
moderator: false,
|
||||||
|
silenced: false,
|
||||||
|
suspended: false,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
mounted() {
|
computed: {
|
||||||
this.fetch();
|
iAmModerator(): boolean {
|
||||||
|
return this.$i && (this.$i.isAdmin || this.$i.isModerator);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
userId: {
|
||||||
|
handler() {
|
||||||
|
this.init = this.createFetcher();
|
||||||
|
},
|
||||||
|
immediate: true
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
number,
|
number,
|
||||||
bytes,
|
bytes,
|
||||||
|
userPage,
|
||||||
|
acct,
|
||||||
|
|
||||||
async fetch() {
|
createFetcher() {
|
||||||
this.user = await os.api('users/show', {
|
if (this.iAmModerator) {
|
||||||
|
return () => Promise.all([os.api('users/show', {
|
||||||
userId: this.userId
|
userId: this.userId
|
||||||
|
}), os.api('admin/show-user', {
|
||||||
|
userId: this.userId
|
||||||
|
})]).then(([user, info]) => {
|
||||||
|
this.user = user;
|
||||||
|
this.info = info;
|
||||||
|
this.moderator = this.info.isModerator;
|
||||||
|
this.silenced = this.info.isSilenced;
|
||||||
|
this.suspended = this.info.isSuspended;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return () => os.api('users/show', {
|
||||||
|
userId: this.userId
|
||||||
|
}).then((user) => {
|
||||||
|
this.user = user;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
refreshUser() {
|
||||||
|
this.init = this.createFetcher();
|
||||||
|
},
|
||||||
|
|
||||||
|
async updateRemoteUser() {
|
||||||
|
await os.apiWithDialog('federation/update-remote-user', { userId: this.user.id });
|
||||||
|
this.refreshUser();
|
||||||
|
},
|
||||||
|
|
||||||
|
async resetPassword() {
|
||||||
|
os.apiWithDialog('admin/reset-password', {
|
||||||
|
userId: this.user.id,
|
||||||
|
}, undefined, ({ password }) => {
|
||||||
|
os.dialog({
|
||||||
|
type: 'success',
|
||||||
|
text: this.$t('newPasswordIs', { password })
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async toggleSilence(v) {
|
||||||
|
const confirm = await os.dialog({
|
||||||
|
type: 'warning',
|
||||||
|
showCancelButton: true,
|
||||||
|
text: v ? this.$ts.silenceConfirm : this.$ts.unsilenceConfirm,
|
||||||
|
});
|
||||||
|
if (confirm.canceled) {
|
||||||
|
this.silenced = !v;
|
||||||
|
} else {
|
||||||
|
await os.api(v ? 'admin/silence-user' : 'admin/unsilence-user', { userId: this.user.id });
|
||||||
|
await this.refreshUser();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async toggleSuspend(v) {
|
||||||
|
const confirm = await os.dialog({
|
||||||
|
type: 'warning',
|
||||||
|
showCancelButton: true,
|
||||||
|
text: v ? this.$ts.suspendConfirm : this.$ts.unsuspendConfirm,
|
||||||
|
});
|
||||||
|
if (confirm.canceled) {
|
||||||
|
this.suspended = !v;
|
||||||
|
} else {
|
||||||
|
await os.api(v ? 'admin/suspend-user' : 'admin/unsuspend-user', { userId: this.user.id });
|
||||||
|
await this.refreshUser();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async toggleModerator(v) {
|
||||||
|
await os.api(v ? 'admin/moderators/add' : 'admin/moderators/remove', { userId: this.user.id });
|
||||||
|
await this.refreshUser();
|
||||||
|
},
|
||||||
|
|
||||||
|
async deleteAllFiles() {
|
||||||
|
const confirm = await os.dialog({
|
||||||
|
type: 'warning',
|
||||||
|
showCancelButton: true,
|
||||||
|
text: this.$ts.deleteAllFilesConfirm,
|
||||||
|
});
|
||||||
|
if (confirm.canceled) return;
|
||||||
|
const process = async () => {
|
||||||
|
await os.api('admin/delete-all-files-of-a-user', { userId: this.user.id });
|
||||||
|
os.success();
|
||||||
|
};
|
||||||
|
await process().catch(e => {
|
||||||
|
os.dialog({
|
||||||
|
type: 'error',
|
||||||
|
text: e.toString()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
await this.refreshUser();
|
||||||
|
},
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.aeakzknw {
|
||||||
|
> .avatar {
|
||||||
|
display: block;
|
||||||
|
margin: 0 auto;
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -195,7 +195,7 @@
|
||||||
|
|
||||||
<template v-if="page === 'index'">
|
<template v-if="page === 'index'">
|
||||||
<div>
|
<div>
|
||||||
<div v-if="user.pinnedNotes.length > 0">
|
<div v-if="user.pinnedNotes.length > 0" class="_gap">
|
||||||
<XNote v-for="note in user.pinnedNotes" class="note _block" :note="note" @update:note="pinnedNoteUpdated(note, $event)" :key="note.id" :pinned="true"/>
|
<XNote v-for="note in user.pinnedNotes" class="note _block" :note="note" @update:note="pinnedNoteUpdated(note, $event)" :key="note.id" :pinned="true"/>
|
||||||
</div>
|
</div>
|
||||||
<MkInfo v-else-if="$i && $i.id === user.id">{{ $ts.userPagePinTip }}</MkInfo>
|
<MkInfo v-else-if="$i && $i.id === user.id">{{ $ts.userPagePinTip }}</MkInfo>
|
||||||
|
|
|
@ -59,17 +59,8 @@ export const router = createRouter({
|
||||||
{ path: '/my/antennas', component: page('my-antennas/index') },
|
{ path: '/my/antennas', component: page('my-antennas/index') },
|
||||||
{ path: '/my/clips', component: page('my-clips/index') },
|
{ path: '/my/clips', component: page('my-clips/index') },
|
||||||
{ path: '/scratchpad', component: page('scratchpad') },
|
{ path: '/scratchpad', component: page('scratchpad') },
|
||||||
|
{ path: '/instance/:page(.*)?', component: page('instance/index'), props: route => ({ initialPage: route.params.page || null }) },
|
||||||
{ path: '/instance', component: page('instance/index') },
|
{ path: '/instance', component: page('instance/index') },
|
||||||
{ path: '/instance/emojis', component: page('instance/emojis') },
|
|
||||||
{ path: '/instance/users', component: page('instance/users') },
|
|
||||||
{ path: '/instance/logs', component: page('instance/logs') },
|
|
||||||
{ path: '/instance/files', component: page('instance/files') },
|
|
||||||
{ path: '/instance/queue', component: page('instance/queue') },
|
|
||||||
{ path: '/instance/settings', component: page('instance/settings') },
|
|
||||||
{ path: '/instance/federation', component: page('instance/federation') },
|
|
||||||
{ path: '/instance/relays', component: page('instance/relays') },
|
|
||||||
{ path: '/instance/announcements', component: page('instance/announcements') },
|
|
||||||
{ path: '/instance/abuses', component: page('instance/abuses') },
|
|
||||||
{ path: '/notes/:note', name: 'note', component: page('note'), props: route => ({ noteId: route.params.note }) },
|
{ path: '/notes/:note', name: 'note', component: page('note'), props: route => ({ noteId: route.params.note }) },
|
||||||
{ path: '/tags/:tag', component: page('tag'), props: route => ({ tag: route.params.tag }) },
|
{ path: '/tags/:tag', component: page('tag'), props: route => ({ tag: route.params.tag }) },
|
||||||
{ path: '/user-info/:user', component: page('user-info'), props: route => ({ userId: route.params.user }) },
|
{ path: '/user-info/:user', component: page('user-info'), props: route => ({ userId: route.params.user }) },
|
||||||
|
|
37
src/client/scripts/lookup-user.ts
Normal file
37
src/client/scripts/lookup-user.ts
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import parseAcct from '@/misc/acct/parse';
|
||||||
|
import { i18n } from '@client/i18n';
|
||||||
|
import * as os from '@client/os';
|
||||||
|
|
||||||
|
export async function lookupUser() {
|
||||||
|
const { canceled, result } = await os.dialog({
|
||||||
|
title: i18n.locale.usernameOrUserId,
|
||||||
|
input: true
|
||||||
|
});
|
||||||
|
if (canceled) return;
|
||||||
|
|
||||||
|
const show = (user) => {
|
||||||
|
os.pageWindow(`/user-info/${user.id}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const usernamePromise = os.api('users/show', parseAcct(result));
|
||||||
|
const idPromise = os.api('users/show', { userId: result });
|
||||||
|
let _notFound = false;
|
||||||
|
const notFound = () => {
|
||||||
|
if (_notFound) {
|
||||||
|
os.dialog({
|
||||||
|
type: 'error',
|
||||||
|
text: i18n.locale.noSuchUser
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
_notFound = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
usernamePromise.then(show).catch(e => {
|
||||||
|
if (e.code === 'NO_SUCH_USER') {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
idPromise.then(show).catch(e => {
|
||||||
|
notFound();
|
||||||
|
});
|
||||||
|
}
|
|
@ -520,3 +520,27 @@ hr {
|
||||||
transform: scale3d(1, 1, 1);
|
transform: scale3d(1, 1, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
._anime_bounce {
|
||||||
|
animation: bounce ease 0.7s;
|
||||||
|
animation-iteration-count: 1;
|
||||||
|
transform-origin: 50% 50%;
|
||||||
|
}
|
||||||
|
._anime_bounce_ready {
|
||||||
|
transform: scaleX(0.90) scaleY(0.90) ;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes bounce{
|
||||||
|
0% {
|
||||||
|
transform: scaleX(0.90) scaleY(0.90) ;
|
||||||
|
}
|
||||||
|
19% {
|
||||||
|
transform: scaleX(1.10) scaleY(1.10) ;
|
||||||
|
}
|
||||||
|
48% {
|
||||||
|
transform: scaleX(0.95) scaleY(0.95) ;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scaleX(1.00) scaleY(1.00) ;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -25,9 +25,9 @@
|
||||||
</component>
|
</component>
|
||||||
</template>
|
</template>
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
<button class="item _button" :class="{ active: $route.path === '/instance' || $route.path.startsWith('/instance/') }" v-if="$i.isAdmin || $i.isModerator" @click="oepnInstanceMenu">
|
<MkA v-if="$i.isAdmin || $i.isModerator" class="item" active-class="active" to="/instance">
|
||||||
<i class="fas fa-server fa-fw"></i><span class="text">{{ $ts.instance }}</span>
|
<i class="fas fa-server fa-fw"></i><span class="text">{{ $ts.instance }}</span>
|
||||||
</button>
|
</MkA>
|
||||||
<button class="item _button" @click="more">
|
<button class="item _button" @click="more">
|
||||||
<i class="fa fa-ellipsis-h fa-fw"></i><span class="text">{{ $ts.more }}</span>
|
<i class="fa fa-ellipsis-h fa-fw"></i><span class="text">{{ $ts.more }}</span>
|
||||||
<span v-if="otherNavItemIndicated" class="indicator"><i class="fas fa-circle"></i></span>
|
<span v-if="otherNavItemIndicated" class="indicator"><i class="fas fa-circle"></i></span>
|
||||||
|
@ -157,11 +157,11 @@ export default defineComponent({
|
||||||
avatar: this.$i,
|
avatar: this.$i,
|
||||||
}, null, ...accountItemPromises, {
|
}, null, ...accountItemPromises, {
|
||||||
icon: 'fas fa-plus',
|
icon: 'fas fa-plus',
|
||||||
text: this.$ts.addAcount,
|
text: this.$ts.addAccount,
|
||||||
action: () => {
|
action: () => {
|
||||||
os.modalMenu([{
|
os.modalMenu([{
|
||||||
text: this.$ts.existingAcount,
|
text: this.$ts.existingAccount,
|
||||||
action: () => { this.addAcount(); },
|
action: () => { this.addAccount(); },
|
||||||
}, {
|
}, {
|
||||||
text: this.$ts.createAccount,
|
text: this.$ts.createAccount,
|
||||||
action: () => { this.createAccount(); },
|
action: () => { this.createAccount(); },
|
||||||
|
@ -172,71 +172,12 @@ export default defineComponent({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
oepnInstanceMenu(ev) {
|
|
||||||
os.modalMenu([{
|
|
||||||
type: 'link',
|
|
||||||
text: this.$ts.dashboard,
|
|
||||||
to: '/instance',
|
|
||||||
icon: 'fas fa-tachometer-alt',
|
|
||||||
}, null, this.$i.isAdmin ? {
|
|
||||||
type: 'link',
|
|
||||||
text: this.$ts.settings,
|
|
||||||
to: '/instance/settings',
|
|
||||||
icon: 'fas fa-cog',
|
|
||||||
} : undefined, {
|
|
||||||
type: 'link',
|
|
||||||
text: this.$ts.customEmojis,
|
|
||||||
to: '/instance/emojis',
|
|
||||||
icon: 'fas fa-laugh',
|
|
||||||
}, {
|
|
||||||
type: 'link',
|
|
||||||
text: this.$ts.users,
|
|
||||||
to: '/instance/users',
|
|
||||||
icon: 'fas fa-users',
|
|
||||||
}, {
|
|
||||||
type: 'link',
|
|
||||||
text: this.$ts.files,
|
|
||||||
to: '/instance/files',
|
|
||||||
icon: 'fas fa-cloud',
|
|
||||||
}, {
|
|
||||||
type: 'link',
|
|
||||||
text: this.$ts.jobQueue,
|
|
||||||
to: '/instance/queue',
|
|
||||||
icon: 'fas fa-exchange-alt',
|
|
||||||
}, {
|
|
||||||
type: 'link',
|
|
||||||
text: this.$ts.federation,
|
|
||||||
to: '/instance/federation',
|
|
||||||
icon: 'fas fa-globe',
|
|
||||||
}, {
|
|
||||||
type: 'link',
|
|
||||||
text: this.$ts.relays,
|
|
||||||
to: '/instance/relays',
|
|
||||||
icon: 'fas fa-project-diagram',
|
|
||||||
}, {
|
|
||||||
type: 'link',
|
|
||||||
text: this.$ts.announcements,
|
|
||||||
to: '/instance/announcements',
|
|
||||||
icon: 'fas fa-broadcast-tower',
|
|
||||||
}, {
|
|
||||||
type: 'link',
|
|
||||||
text: this.$ts.abuseReports,
|
|
||||||
to: '/instance/abuses',
|
|
||||||
icon: 'fas fa-exclamation-circle',
|
|
||||||
}, {
|
|
||||||
type: 'link',
|
|
||||||
text: this.$ts.logs,
|
|
||||||
to: '/instance/logs',
|
|
||||||
icon: 'fas fa-stream',
|
|
||||||
}], ev.currentTarget || ev.target);
|
|
||||||
},
|
|
||||||
|
|
||||||
more(ev) {
|
more(ev) {
|
||||||
os.popup(import('@client/components/launch-pad.vue'), {}, {
|
os.popup(import('@client/components/launch-pad.vue'), {}, {
|
||||||
}, 'closed');
|
}, 'closed');
|
||||||
},
|
},
|
||||||
|
|
||||||
addAcount() {
|
addAccount() {
|
||||||
os.popup(import('@client/components/signin-dialog.vue'), {}, {
|
os.popup(import('@client/components/signin-dialog.vue'), {}, {
|
||||||
done: async res => {
|
done: async res => {
|
||||||
await addAccount(res.id, res.i);
|
await addAccount(res.id, res.i);
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="npcljfve" :class="{ iconOnly }">
|
<div class="npcljfve" :class="{ iconOnly }">
|
||||||
<button class="item _button account" @click="openAccountMenu">
|
<button class="item _button account" @click="openAccountMenu" v-click-anime>
|
||||||
<MkAvatar :user="$i" class="avatar"/><MkAcct class="text" :user="$i"/>
|
<MkAvatar :user="$i" class="avatar"/><MkAcct class="text" :user="$i"/>
|
||||||
</button>
|
</button>
|
||||||
<div class="post" @click="post">
|
<div class="post" @click="post">
|
||||||
|
@ -9,25 +9,25 @@
|
||||||
</MkButton>
|
</MkButton>
|
||||||
</div>
|
</div>
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
<MkA class="item index" active-class="active" to="/" exact>
|
<MkA class="item index" active-class="active" to="/" exact v-click-anime>
|
||||||
<i class="fas fa-home fa-fw"></i><span class="text">{{ $ts.timeline }}</span>
|
<i class="fas fa-home fa-fw"></i><span class="text">{{ $ts.timeline }}</span>
|
||||||
</MkA>
|
</MkA>
|
||||||
<template v-for="item in menu">
|
<template v-for="item in menu">
|
||||||
<div v-if="item === '-'" class="divider"></div>
|
<div v-if="item === '-'" class="divider"></div>
|
||||||
<component v-else-if="menuDef[item] && (menuDef[item].show !== false)" :is="menuDef[item].to ? 'MkA' : 'button'" class="item _button" :class="item" active-class="active" v-on="menuDef[item].action ? { click: menuDef[item].action } : {}" :to="menuDef[item].to">
|
<component v-else-if="menuDef[item] && (menuDef[item].show !== false)" :is="menuDef[item].to ? 'MkA' : 'button'" class="item _button" :class="item" active-class="active" v-on="menuDef[item].action ? { click: menuDef[item].action } : {}" :to="menuDef[item].to" v-click-anime>
|
||||||
<i class="fa-fw" :class="menuDef[item].icon"></i><span class="text">{{ $ts[menuDef[item].title] }}</span>
|
<i class="fa-fw" :class="menuDef[item].icon"></i><span class="text">{{ $ts[menuDef[item].title] }}</span>
|
||||||
<span v-if="menuDef[item].indicated" class="indicator"><i class="fas fa-circle"></i></span>
|
<span v-if="menuDef[item].indicated" class="indicator"><i class="fas fa-circle"></i></span>
|
||||||
</component>
|
</component>
|
||||||
</template>
|
</template>
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
<button class="item _button" :class="{ active: $route.path === '/instance' || $route.path.startsWith('/instance/') }" v-if="$i.isAdmin || $i.isModerator" @click="oepnInstanceMenu">
|
<MkA v-if="$i.isAdmin || $i.isModerator" class="item" active-class="active" to="/instance" :behavior="settingsWindowed ? 'modalWindow' : null" v-click-anime>
|
||||||
<i class="fas fa-server fa-fw"></i><span class="text">{{ $ts.instance }}</span>
|
<i class="fas fa-server fa-fw"></i><span class="text">{{ $ts.instance }}</span>
|
||||||
</button>
|
</MkA>
|
||||||
<button class="item _button" @click="more">
|
<button class="item _button" @click="more" v-click-anime>
|
||||||
<i class="fas fa-ellipsis-h fa-fw"></i><span class="text">{{ $ts.more }}</span>
|
<i class="fas fa-ellipsis-h fa-fw"></i><span class="text">{{ $ts.more }}</span>
|
||||||
<span v-if="otherNavItemIndicated" class="indicator"><i class="fas fa-circle"></i></span>
|
<span v-if="otherNavItemIndicated" class="indicator"><i class="fas fa-circle"></i></span>
|
||||||
</button>
|
</button>
|
||||||
<MkA class="item" active-class="active" to="/settings" :behavior="settingsWindowed ? 'modalWindow' : null">
|
<MkA class="item" active-class="active" to="/settings" :behavior="settingsWindowed ? 'modalWindow' : null" v-click-anime>
|
||||||
<i class="fas fa-cog fa-fw"></i><span class="text">{{ $ts.settings }}</span>
|
<i class="fas fa-cog fa-fw"></i><span class="text">{{ $ts.settings }}</span>
|
||||||
</MkA>
|
</MkA>
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
|
@ -141,11 +141,11 @@ export default defineComponent({
|
||||||
avatar: this.$i,
|
avatar: this.$i,
|
||||||
}, null, ...accountItemPromises, {
|
}, null, ...accountItemPromises, {
|
||||||
icon: 'fas fa-plus',
|
icon: 'fas fa-plus',
|
||||||
text: this.$ts.addAcount,
|
text: this.$ts.addAccount,
|
||||||
action: () => {
|
action: () => {
|
||||||
os.modalMenu([{
|
os.modalMenu([{
|
||||||
text: this.$ts.existingAcount,
|
text: this.$ts.existingAccount,
|
||||||
action: () => { this.addAcount(); },
|
action: () => { this.addAccount(); },
|
||||||
}, {
|
}, {
|
||||||
text: this.$ts.createAccount,
|
text: this.$ts.createAccount,
|
||||||
action: () => { this.createAccount(); },
|
action: () => { this.createAccount(); },
|
||||||
|
@ -156,71 +156,12 @@ export default defineComponent({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
oepnInstanceMenu(ev) {
|
|
||||||
os.modalMenu([{
|
|
||||||
type: 'link',
|
|
||||||
text: this.$ts.dashboard,
|
|
||||||
to: '/instance',
|
|
||||||
icon: 'fas fa-tachometer-alt',
|
|
||||||
}, null, this.$i.isAdmin ? {
|
|
||||||
type: 'link',
|
|
||||||
text: this.$ts.settings,
|
|
||||||
to: '/instance/settings',
|
|
||||||
icon: 'fas fa-cog',
|
|
||||||
} : undefined, {
|
|
||||||
type: 'link',
|
|
||||||
text: this.$ts.customEmojis,
|
|
||||||
to: '/instance/emojis',
|
|
||||||
icon: 'fas fa-laugh',
|
|
||||||
}, {
|
|
||||||
type: 'link',
|
|
||||||
text: this.$ts.users,
|
|
||||||
to: '/instance/users',
|
|
||||||
icon: 'fas fa-users',
|
|
||||||
}, {
|
|
||||||
type: 'link',
|
|
||||||
text: this.$ts.files,
|
|
||||||
to: '/instance/files',
|
|
||||||
icon: 'fas fa-cloud',
|
|
||||||
}, {
|
|
||||||
type: 'link',
|
|
||||||
text: this.$ts.jobQueue,
|
|
||||||
to: '/instance/queue',
|
|
||||||
icon: 'fas fa-exchange-alt',
|
|
||||||
}, {
|
|
||||||
type: 'link',
|
|
||||||
text: this.$ts.federation,
|
|
||||||
to: '/instance/federation',
|
|
||||||
icon: 'fas fa-globe',
|
|
||||||
}, {
|
|
||||||
type: 'link',
|
|
||||||
text: this.$ts.relays,
|
|
||||||
to: '/instance/relays',
|
|
||||||
icon: 'fas fa-project-diagram',
|
|
||||||
}, {
|
|
||||||
type: 'link',
|
|
||||||
text: this.$ts.announcements,
|
|
||||||
to: '/instance/announcements',
|
|
||||||
icon: 'fas fa-broadcast-tower',
|
|
||||||
}, {
|
|
||||||
type: 'link',
|
|
||||||
text: this.$ts.abuseReports,
|
|
||||||
to: '/instance/abuses',
|
|
||||||
icon: 'fas fa-exclamation-circle',
|
|
||||||
}, {
|
|
||||||
type: 'link',
|
|
||||||
text: this.$ts.logs,
|
|
||||||
to: '/instance/logs',
|
|
||||||
icon: 'fas fa-stream',
|
|
||||||
}], ev.currentTarget || ev.target);
|
|
||||||
},
|
|
||||||
|
|
||||||
more(ev) {
|
more(ev) {
|
||||||
os.popup(import('@client/components/launch-pad.vue'), {}, {
|
os.popup(import('@client/components/launch-pad.vue'), {}, {
|
||||||
}, 'closed');
|
}, 'closed');
|
||||||
},
|
},
|
||||||
|
|
||||||
addAcount() {
|
addAccount() {
|
||||||
os.popup(import('@client/components/signin-dialog.vue'), {}, {
|
os.popup(import('@client/components/signin-dialog.vue'), {}, {
|
||||||
done: res => {
|
done: res => {
|
||||||
addAccount(res.id, res.i);
|
addAccount(res.id, res.i);
|
||||||
|
|
|
@ -218,6 +218,8 @@ export default defineComponent({
|
||||||
$widgets-hide-threshold: 1200px;
|
$widgets-hide-threshold: 1200px;
|
||||||
$nav-icon-only-width: 78px; // TODO: どこかに集約したい
|
$nav-icon-only-width: 78px; // TODO: どこかに集約したい
|
||||||
|
|
||||||
|
--panelShadow: none;
|
||||||
|
|
||||||
// ほんとは単に 100vh と書きたいところだが... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
|
// ほんとは単に 100vh と書きたいところだが... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
|
||||||
min-height: calc(var(--vh, 1vh) * 100);
|
min-height: calc(var(--vh, 1vh) * 100);
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
@ -250,7 +252,7 @@ export default defineComponent({
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
margin: 32px 0;
|
//margin: 32px 0;
|
||||||
|
|
||||||
&.fullView {
|
&.fullView {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
@ -276,6 +278,8 @@ export default defineComponent({
|
||||||
width: 750px;
|
width: 750px;
|
||||||
margin: 0 16px 0 0;
|
margin: 0 16px 0 0;
|
||||||
background: var(--bg);
|
background: var(--bg);
|
||||||
|
box-shadow: 0 0 0 1px var(--divider);
|
||||||
|
border-radius: 0;
|
||||||
--margin: 12px;
|
--margin: 12px;
|
||||||
|
|
||||||
> .header {
|
> .header {
|
||||||
|
@ -308,12 +312,17 @@ export default defineComponent({
|
||||||
> .widgets {
|
> .widgets {
|
||||||
//--panelShadow: none;
|
//--panelShadow: none;
|
||||||
width: 300px;
|
width: 300px;
|
||||||
|
margin-top: 16px;
|
||||||
|
|
||||||
@media (max-width: $widgets-hide-threshold) {
|
@media (max-width: $widgets-hide-threshold) {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
> .sidebar {
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 850px) {
|
@media (max-width: 850px) {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
||||||
|
|
|
@ -10,10 +10,9 @@ export const meta = {
|
||||||
'en-US': 'Update specified remote user information.'
|
'en-US': 'Update specified remote user information.'
|
||||||
},
|
},
|
||||||
|
|
||||||
tags: ['admin'],
|
tags: ['federation'],
|
||||||
|
|
||||||
requireCredential: true as const,
|
requireCredential: true as const,
|
||||||
requireModerator: true,
|
|
||||||
|
|
||||||
params: {
|
params: {
|
||||||
userId: {
|
userId: {
|
Loading…
Reference in a new issue