parent
							
								
									ec75600e1c
								
							
						
					
					
						commit
						246693b848
					
				
					 50 changed files with 2588 additions and 1887 deletions
				
			
		|  | @ -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: "大文字小文字を区別する" | ||||||
|  | @ -568,7 +567,7 @@ pluginTokenRequestedDescription: "このプラグインはここで設定した | ||||||
| notificationType: "通知の種類" | notificationType: "通知の種類" | ||||||
| edit: "編集" | edit: "編集" | ||||||
| useStarForReactionFallback: "リアクション絵文字が不明な場合、代わりに★を使う" | useStarForReactionFallback: "リアクション絵文字が不明な場合、代わりに★を使う" | ||||||
| emailConfig: "メールサーバー設定" | emailServer: "メールサーバー" | ||||||
| enableEmail: "メール配信機能を有効化する" | enableEmail: "メール配信機能を有効化する" | ||||||
| emailConfigInfo: "メールアドレスの確認やパスワードリセットの際に使います" | emailConfigInfo: "メールアドレスの確認やパスワードリセットの際に使います" | ||||||
| email: "メール" | email: "メール" | ||||||
|  | @ -728,6 +727,14 @@ hideOnlineStatusDescription: "オンライン状態を隠すと、検索など | ||||||
| online: "オンライン" | online: "オンライン" | ||||||
| active: "アクティブ" | active: "アクティブ" | ||||||
| offline: "オフライン" | offline: "オフライン" | ||||||
|  | notRecommended: "非推奨" | ||||||
|  | botProtection: "Bot防御" | ||||||
|  | instanceBlocking: "インスタンスブロック" | ||||||
|  | selectAccount: "アカウントを選択" | ||||||
|  | enabled: "有効" | ||||||
|  | disabled: "無効" | ||||||
|  | quickAction: "クイックアクション" | ||||||
|  | user: "ユーザー" | ||||||
| 
 | 
 | ||||||
| _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-beta.3", | ||||||
| 	"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`; | ||||||
|  |  | ||||||
|  | @ -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,9 +5,9 @@ | ||||||
| 			<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"> | ||||||
| 			error! | 			error! | ||||||
|  | @ -20,13 +20,8 @@ | ||||||
| <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'; |  | ||||||
| 
 | 
 | ||||||
| export default defineComponent({ | export default defineComponent({ | ||||||
| 	components: { |  | ||||||
| 		FormGroup, |  | ||||||
| 	}, |  | ||||||
| 
 |  | ||||||
| 	props: { | 	props: { | ||||||
| 		p: { | 		p: { | ||||||
| 			type: Function as PropType<() => Promise<any>>, | 			type: Function as PropType<() => Promise<any>>, | ||||||
|  |  | ||||||
|  | @ -1,123 +1,35 @@ | ||||||
| <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="selects" style="display: flex;"> | ||||||
| 		<div class="_panel"> | 		<MkSelect v-model:value="chartSrc" style="margin: 0; flex: 1;"> | ||||||
| 			<div> | 			<optgroup :label="$ts.federation"> | ||||||
| 				<b><i class="fas fa-user"></i>{{ $ts.users }}</b> | 				<option value="federation-instances">{{ $ts._charts.federationInstancesIncDec }}</option> | ||||||
| 				<small>{{ $ts.local }}</small> | 				<option value="federation-instances-total">{{ $ts._charts.federationInstancesTotal }}</option> | ||||||
| 			</div> | 			</optgroup> | ||||||
| 			<div> | 			<optgroup :label="$ts.users"> | ||||||
| 				<dl class="total"> | 				<option value="users">{{ $ts._charts.usersIncDec }}</option> | ||||||
| 					<dt>{{ $ts.total }}</dt> | 				<option value="users-total">{{ $ts._charts.usersTotal }}</option> | ||||||
| 					<dd>{{ number(info.originalUsersCount) }}</dd> | 				<option value="active-users">{{ $ts._charts.activeUsers }}</option> | ||||||
| 				</dl> | 			</optgroup> | ||||||
| 				<dl class="diff" :class="{ inc: usersLocalDoD > 0 }"> | 			<optgroup :label="$ts.notes"> | ||||||
| 					<dt>{{ $ts.dayOverDayChanges }}</dt> | 				<option value="notes">{{ $ts._charts.notesIncDec }}</option> | ||||||
| 					<dd>{{ number(usersLocalDoD) }}</dd> | 				<option value="local-notes">{{ $ts._charts.localNotesIncDec }}</option> | ||||||
| 				</dl> | 				<option value="remote-notes">{{ $ts._charts.remoteNotesIncDec }}</option> | ||||||
| 				<dl class="diff" :class="{ inc: usersLocalWoW > 0 }"> | 				<option value="notes-total">{{ $ts._charts.notesTotal }}</option> | ||||||
| 					<dt>{{ $ts.weekOverWeekChanges }}</dt> | 			</optgroup> | ||||||
| 					<dd>{{ number(usersLocalWoW) }}</dd> | 			<optgroup :label="$ts.drive"> | ||||||
| 				</dl> | 				<option value="drive-files">{{ $ts._charts.filesIncDec }}</option> | ||||||
| 			</div> | 				<option value="drive-files-total">{{ $ts._charts.filesTotal }}</option> | ||||||
| 		</div> | 				<option value="drive">{{ $ts._charts.storageUsageIncDec }}</option> | ||||||
| 		<div class="_panel"> | 				<option value="drive-total">{{ $ts._charts.storageUsageTotal }}</option> | ||||||
| 			<div> | 			</optgroup> | ||||||
| 				<b><i class="fas fa-user"></i>{{ $ts.users }}</b> | 		</MkSelect> | ||||||
| 				<small>{{ $ts.remote }}</small> | 		<MkSelect v-model:value="chartSpan" style="margin: 0;"> | ||||||
| 			</div> | 			<option value="hour">{{ $ts.perHour }}</option> | ||||||
| 			<div> | 			<option value="day">{{ $ts.perDay }}</option> | ||||||
| 				<dl class="total"> | 		</MkSelect> | ||||||
| 					<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> | 	</div> | ||||||
| 
 | 	<canvas ref="chart"></canvas> | ||||||
| 	<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;"> |  | ||||||
| 				<MkSelect v-model:value="chartSrc" style="margin: 0; flex: 1;"> |  | ||||||
| 					<optgroup :label="$ts.federation"> |  | ||||||
| 						<option value="federation-instances">{{ $ts._charts.federationInstancesIncDec }}</option> |  | ||||||
| 						<option value="federation-instances-total">{{ $ts._charts.federationInstancesTotal }}</option> |  | ||||||
| 					</optgroup> |  | ||||||
| 					<optgroup :label="$ts.users"> |  | ||||||
| 						<option value="users">{{ $ts._charts.usersIncDec }}</option> |  | ||||||
| 						<option value="users-total">{{ $ts._charts.usersTotal }}</option> |  | ||||||
| 						<option value="active-users">{{ $ts._charts.activeUsers }}</option> |  | ||||||
| 					</optgroup> |  | ||||||
| 					<optgroup :label="$ts.notes"> |  | ||||||
| 						<option value="notes">{{ $ts._charts.notesIncDec }}</option> |  | ||||||
| 						<option value="local-notes">{{ $ts._charts.localNotesIncDec }}</option> |  | ||||||
| 						<option value="remote-notes">{{ $ts._charts.remoteNotesIncDec }}</option> |  | ||||||
| 						<option value="notes-total">{{ $ts._charts.notesTotal }}</option> |  | ||||||
| 					</optgroup> |  | ||||||
| 					<optgroup :label="$ts.drive"> |  | ||||||
| 						<option value="drive-files">{{ $ts._charts.filesIncDec }}</option> |  | ||||||
| 						<option value="drive-files-total">{{ $ts._charts.filesTotal }}</option> |  | ||||||
| 						<option value="drive">{{ $ts._charts.storageUsageIncDec }}</option> |  | ||||||
| 						<option value="drive-total">{{ $ts._charts.storageUsageTotal }}</option> |  | ||||||
| 					</optgroup> |  | ||||||
| 				</MkSelect> |  | ||||||
| 				<MkSelect v-model:value="chartSpan" style="margin: 0;"> |  | ||||||
| 					<option value="hour">{{ $ts.perHour }}</option> |  | ||||||
| 					<option value="day">{{ $ts.perDay }}</option> |  | ||||||
| 				</MkSelect> |  | ||||||
| 			</div> |  | ||||||
| 			<canvas ref="chart"></canvas> |  | ||||||
| 		</div> |  | ||||||
| 	</section> |  | ||||||
| </div> | </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
|  | @ -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> | ||||||
|  |  | ||||||
|  | @ -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,16 +1,23 @@ | ||||||
| <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 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> | 	<div v-else class="cxiknjgy"> | ||||||
| 			<template v-if="!moreFetching">{{ $ts.loadMore }}</template> | 		<slot :items="items"></slot> | ||||||
| 			<template v-if="moreFetching"><MkLoading inline/></template> | 		<div class="more" v-show="more" key="_more_"> | ||||||
| 		</MkButton> | 			<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"><MkLoading inline/></template> | ||||||
|  | 			</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; | ||||||
|  |  | ||||||
|  | @ -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,28 +1,24 @@ | ||||||
| <template> | <template> | ||||||
| <div class="ztgjmzrw"> | <div class="ztgjmzrw"> | ||||||
| 	<div class="_section"> | 	<MkButton @click="add()" primary style="margin: 0 auto 16px auto;"><i class="fas fa-plus"></i> {{ $ts.add }}</MkButton> | ||||||
| 		<div class="_content"> | 	<section class="_card _gap announcements" v-for="announcement in announcements"> | ||||||
| 			<MkButton @click="add()" primary style="margin: 0 auto 16px auto;"><i class="fas fa-plus"></i> {{ $ts.add }}</MkButton> | 		<div class="_content announcement"> | ||||||
| 			<section class="_card _gap announcements" v-for="announcement in announcements"> | 			<MkInput v-model:value="announcement.title"> | ||||||
| 				<div class="_content announcement"> | 				<span>{{ $ts.title }}</span> | ||||||
| 					<MkInput v-model:value="announcement.title"> | 			</MkInput> | ||||||
| 						<span>{{ $ts.title }}</span> | 			<MkTextarea v-model:value="announcement.text"> | ||||||
| 					</MkInput> | 				<span>{{ $ts.text }}</span> | ||||||
| 					<MkTextarea v-model:value="announcement.text"> | 			</MkTextarea> | ||||||
| 						<span>{{ $ts.text }}</span> | 			<MkInput v-model:value="announcement.imageUrl"> | ||||||
| 					</MkTextarea> | 				<span>{{ $ts.imageUrl }}</span> | ||||||
| 					<MkInput v-model:value="announcement.imageUrl"> | 			</MkInput> | ||||||
| 						<span>{{ $ts.imageUrl }}</span> | 			<p v-if="announcement.reads">{{ $t('nUsersRead', { n: announcement.reads }) }}</p> | ||||||
| 					</MkInput> | 			<div class="buttons"> | ||||||
| 					<p v-if="announcement.reads">{{ $t('nUsersRead', { n: announcement.reads }) }}</p> | 				<MkButton class="button" inline @click="save(announcement)" primary><i class="fas fa-save"></i> {{ $ts.save }}</MkButton> | ||||||
| 					<div class="buttons"> | 				<MkButton class="button" inline @click="remove(announcement)"><i class="fas fa-trash-alt"></i> {{ $ts.remove }}</MkButton> | ||||||
| 						<MkButton class="button" inline @click="save(announcement)" primary><i class="fas fa-save"></i> {{ $ts.save }}</MkButton> | 			</div> | ||||||
| 						<MkButton class="button" inline @click="remove(announcement)"><i class="fas fa-trash-alt"></i> {{ $ts.remove }}</MkButton> |  | ||||||
| 					</div> |  | ||||||
| 				</div> |  | ||||||
| 			</section> |  | ||||||
| 		</div> | 		</div> | ||||||
| 	</div> | 	</section> | ||||||
| </div> | </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
|  | @ -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,50 +1,46 @@ | ||||||
| <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 class="local" v-if="tab === 'local'"> | ||||||
|  | 		<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" style="margin: var(--margin);"><template #icon><i class="fas fa-search"></i></template><span>{{ $ts.search }}</span></MkInput> | ||||||
|  | 		<MkPagination :pagination="pagination" ref="emojis"> | ||||||
|  | 			<template #empty><span>{{ $ts.noCustomEmojis }}</span></template> | ||||||
|  | 			<template #default="{items}"> | ||||||
|  | 				<div class="ldhfsamy"> | ||||||
|  | 					<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"/> | ||||||
|  | 						<div class="body"> | ||||||
|  | 							<div class="name _monospace">{{ emoji.name }}</div> | ||||||
|  | 							<div class="info">{{ emoji.category }}</div> | ||||||
|  | 						</div> | ||||||
|  | 					</button> | ||||||
|  | 				</div> | ||||||
|  | 			</template> | ||||||
|  | 		</MkPagination> | ||||||
| 	</div> | 	</div> | ||||||
| 
 | 
 | ||||||
| 	<div class="_section"> | 	<div class="remote" v-else-if="tab === 'remote'"> | ||||||
| 		<div class="local" v-if="tab === 'local'"> | 		<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> | ||||||
| 			<MkButton primary @click="add" style="margin: 0 auto var(--margin) auto;"><i class="fas fa-plus"></i> {{ $ts.addEmoji }}</MkButton> | 		<MkInput v-model:value="host" :debounce="true" style="margin: var(--margin);"><span>{{ $ts.host }}</span></MkInput> | ||||||
| 			<MkInput v-model:value="query" :debounce="true" type="search"><template #icon><i class="fas fa-search"></i></template><span>{{ $ts.search }}</span></MkInput> | 		<MkPagination :pagination="remotePagination" ref="remoteEmojis"> | ||||||
| 			<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="ldhfsamy"> | ||||||
| 					<div class="emojis"> | 					<div class="emoji _panel _button" v-for="emoji in items" :key="emoji.id" @click="remoteMenu(emoji, $event)"> | ||||||
| 						<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 _monospace">{{ emoji.name }}</div> | ||||||
| 								<div class="name">{{ emoji.name }}</div> | 							<div class="info">{{ emoji.host }}</div> | ||||||
| 								<div class="info">{{ emoji.category }}</div> |  | ||||||
| 							</div> |  | ||||||
| 						</button> |  | ||||||
| 					</div> |  | ||||||
| 				</template> |  | ||||||
| 			</MkPagination> |  | ||||||
| 		</div> |  | ||||||
| 
 |  | ||||||
| 		<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="host" :debounce="true"><span>{{ $ts.host }}</span></MkInput> |  | ||||||
| 			<MkPagination :pagination="remotePagination" ref="remoteEmojis"> |  | ||||||
| 				<template #empty><span>{{ $ts.noCustomEmojis }}</span></template> |  | ||||||
| 				<template #default="{items}"> |  | ||||||
| 					<div class="emojis"> |  | ||||||
| 						<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"/> |  | ||||||
| 							<div class="body"> |  | ||||||
| 								<div class="name">{{ emoji.name }}</div> |  | ||||||
| 								<div class="info">{{ emoji.host }}</div> |  | ||||||
| 							</div> |  | ||||||
| 						</div> | 						</div> | ||||||
| 					</div> | 					</div> | ||||||
| 				</template> | 				</div> | ||||||
| 			</MkPagination> | 			</template> | ||||||
| 		</div> | 		</MkPagination> | ||||||
| 	</div> | 	</div> | ||||||
| </div> | </div> | ||||||
| </template> | </template> | ||||||
|  | @ -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,85 +152,86 @@ export default defineComponent({ | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
| .mk-instance-emojis { | .ogwlenmc { | ||||||
| 	> ._section { | 	> .local { | ||||||
| 		> .local { | 		.ldhfsamy { | ||||||
| 			.emojis { | 			display: grid; | ||||||
| 				display: grid; | 			grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); | ||||||
| 				grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); | 			grid-gap: 12px; | ||||||
| 				grid-gap: var(--margin); | 			margin: var(--margin); | ||||||
| 	 | 	 | ||||||
| 				> .emoji { | 			> .emoji { | ||||||
| 					display: flex; | 				display: flex; | ||||||
| 					align-items: center; | 				align-items: center; | ||||||
| 					padding: 12px; | 				padding: 12px; | ||||||
| 					text-align: left; | 				text-align: left; | ||||||
| 
 | 
 | ||||||
| 					&:hover { | 				&:hover { | ||||||
| 						color: var(--accent); | 					color: var(--accent); | ||||||
| 					} | 				} | ||||||
| 
 | 
 | ||||||
| 					> .img { | 				> .img { | ||||||
| 						width: 42px; | 					width: 42px; | ||||||
| 						height: 42px; | 					height: 42px; | ||||||
| 					} | 				} | ||||||
| 
 | 
 | ||||||
| 					> .body { | 				> .body { | ||||||
| 						padding: 0 0 0 8px; | 					padding: 0 0 0 8px; | ||||||
| 						white-space: nowrap; | 					white-space: nowrap; | ||||||
|  | 					overflow: hidden; | ||||||
|  | 
 | ||||||
|  | 					> .name { | ||||||
|  | 						text-overflow: ellipsis; | ||||||
| 						overflow: hidden; | 						overflow: hidden; | ||||||
|  | 					} | ||||||
| 
 | 
 | ||||||
| 						> .name { | 					> .info { | ||||||
| 							text-overflow: ellipsis; | 						opacity: 0.5; | ||||||
| 							overflow: hidden; | 						text-overflow: ellipsis; | ||||||
| 						} | 						overflow: hidden; | ||||||
| 
 |  | ||||||
| 						> .info { |  | ||||||
| 							opacity: 0.5; |  | ||||||
| 							text-overflow: ellipsis; |  | ||||||
| 							overflow: hidden; |  | ||||||
| 						} |  | ||||||
| 					} | 					} | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
|  | 	} | ||||||
| 
 | 
 | ||||||
| 		> .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; | ||||||
| 					align-items: center; | 				align-items: center; | ||||||
| 					padding: 12px; | 				padding: 12px; | ||||||
| 					text-align: left; | 				text-align: left; | ||||||
| 
 | 
 | ||||||
| 					&:hover { | 				&:hover { | ||||||
| 						color: var(--accent); | 					color: var(--accent); | ||||||
| 					} | 				} | ||||||
| 
 | 
 | ||||||
| 					> .img { | 				> .img { | ||||||
| 						width: 32px; | 					width: 32px; | ||||||
| 						height: 32px; | 					height: 32px; | ||||||
| 					} | 				} | ||||||
| 
 | 
 | ||||||
| 					> .body { | 				> .body { | ||||||
| 						padding: 0 0 0 8px; | 					padding: 0 0 0 8px; | ||||||
| 						white-space: nowrap; | 					white-space: nowrap; | ||||||
|  | 					overflow: hidden; | ||||||
|  | 
 | ||||||
|  | 					> .name { | ||||||
|  | 						text-overflow: ellipsis; | ||||||
| 						overflow: hidden; | 						overflow: hidden; | ||||||
|  | 					} | ||||||
| 
 | 
 | ||||||
| 						> .name { | 					> .info { | ||||||
| 							text-overflow: ellipsis; | 						opacity: 0.5; | ||||||
| 							overflow: hidden; | 						font-size: 90%; | ||||||
| 						} | 						text-overflow: ellipsis; | ||||||
| 
 | 						overflow: hidden; | ||||||
| 						> .info { |  | ||||||
| 							opacity: 0.5; |  | ||||||
| 							text-overflow: ellipsis; |  | ||||||
| 							overflow: hidden; |  | ||||||
| 						} |  | ||||||
| 					} | 					} | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
|  | @ -1,60 +1,55 @@ | ||||||
| <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;"> | 				<template #label>{{ $ts.state }}</template> | ||||||
| 					<template #label>{{ $ts.state }}</template> | 				<option value="all">{{ $ts.all }}</option> | ||||||
| 					<option value="all">{{ $ts.all }}</option> | 				<option value="federating">{{ $ts.federating }}</option> | ||||||
| 					<option value="federating">{{ $ts.federating }}</option> | 				<option value="subscribing">{{ $ts.subscribing }}</option> | ||||||
| 					<option value="subscribing">{{ $ts.subscribing }}</option> | 				<option value="publishing">{{ $ts.publishing }}</option> | ||||||
| 					<option value="publishing">{{ $ts.publishing }}</option> | 				<option value="suspended">{{ $ts.suspended }}</option> | ||||||
| 					<option value="suspended">{{ $ts.suspended }}</option> | 				<option value="blocked">{{ $ts.blocked }}</option> | ||||||
| 					<option value="blocked">{{ $ts.blocked }}</option> | 				<option value="notResponding">{{ $ts.notResponding }}</option> | ||||||
| 					<option value="notResponding">{{ $ts.notResponding }}</option> | 			</MkSelect> | ||||||
| 				</MkSelect> | 			<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> | 				<option value="+pubSub">{{ $ts.pubSub }} ({{ $ts.descendingOrder }})</option> | ||||||
| 					<option value="+pubSub">{{ $ts.pubSub }} ({{ $ts.descendingOrder }})</option> | 				<option value="-pubSub">{{ $ts.pubSub }} ({{ $ts.ascendingOrder }})</option> | ||||||
| 					<option value="-pubSub">{{ $ts.pubSub }} ({{ $ts.ascendingOrder }})</option> | 				<option value="+notes">{{ $ts.notes }} ({{ $ts.descendingOrder }})</option> | ||||||
| 					<option value="+notes">{{ $ts.notes }} ({{ $ts.descendingOrder }})</option> | 				<option value="-notes">{{ $ts.notes }} ({{ $ts.ascendingOrder }})</option> | ||||||
| 					<option value="-notes">{{ $ts.notes }} ({{ $ts.ascendingOrder }})</option> | 				<option value="+users">{{ $ts.users }} ({{ $ts.descendingOrder }})</option> | ||||||
| 					<option value="+users">{{ $ts.users }} ({{ $ts.descendingOrder }})</option> | 				<option value="-users">{{ $ts.users }} ({{ $ts.ascendingOrder }})</option> | ||||||
| 					<option value="-users">{{ $ts.users }} ({{ $ts.ascendingOrder }})</option> | 				<option value="+following">{{ $ts.following }} ({{ $ts.descendingOrder }})</option> | ||||||
| 					<option value="+following">{{ $ts.following }} ({{ $ts.descendingOrder }})</option> | 				<option value="-following">{{ $ts.following }} ({{ $ts.ascendingOrder }})</option> | ||||||
| 					<option value="-following">{{ $ts.following }} ({{ $ts.ascendingOrder }})</option> | 				<option value="+followers">{{ $ts.followers }} ({{ $ts.descendingOrder }})</option> | ||||||
| 					<option value="+followers">{{ $ts.followers }} ({{ $ts.descendingOrder }})</option> | 				<option value="-followers">{{ $ts.followers }} ({{ $ts.ascendingOrder }})</option> | ||||||
| 					<option value="-followers">{{ $ts.followers }} ({{ $ts.ascendingOrder }})</option> | 				<option value="+caughtAt">{{ $ts.caughtAt }} ({{ $ts.descendingOrder }})</option> | ||||||
| 					<option value="+caughtAt">{{ $ts.caughtAt }} ({{ $ts.descendingOrder }})</option> | 				<option value="-caughtAt">{{ $ts.caughtAt }} ({{ $ts.ascendingOrder }})</option> | ||||||
| 					<option value="-caughtAt">{{ $ts.caughtAt }} ({{ $ts.ascendingOrder }})</option> | 				<option value="+lastCommunicatedAt">{{ $ts.lastCommunicatedAt }} ({{ $ts.descendingOrder }})</option> | ||||||
| 					<option value="+lastCommunicatedAt">{{ $ts.lastCommunicatedAt }} ({{ $ts.descendingOrder }})</option> | 				<option value="-lastCommunicatedAt">{{ $ts.lastCommunicatedAt }} ({{ $ts.ascendingOrder }})</option> | ||||||
| 					<option value="-lastCommunicatedAt">{{ $ts.lastCommunicatedAt }} ({{ $ts.ascendingOrder }})</option> | 				<option value="+driveUsage">{{ $ts.driveUsage }} ({{ $ts.descendingOrder }})</option> | ||||||
| 					<option value="+driveUsage">{{ $ts.driveUsage }} ({{ $ts.descendingOrder }})</option> | 				<option value="-driveUsage">{{ $ts.driveUsage }} ({{ $ts.ascendingOrder }})</option> | ||||||
| 					<option value="-driveUsage">{{ $ts.driveUsage }} ({{ $ts.ascendingOrder }})</option> | 				<option value="+driveFiles">{{ $ts.driveFiles }} ({{ $ts.descendingOrder }})</option> | ||||||
| 					<option value="+driveFiles">{{ $ts.driveFiles }} ({{ $ts.descendingOrder }})</option> | 				<option value="-driveFiles">{{ $ts.driveFiles }} ({{ $ts.ascendingOrder }})</option> | ||||||
| 					<option value="-driveFiles">{{ $ts.driveFiles }} ({{ $ts.ascendingOrder }})</option> | 			</MkSelect> | ||||||
| 				</MkSelect> | 		</div> | ||||||
|  | 	</div> | ||||||
|  | 
 | ||||||
|  | 	<MkPagination :pagination="pagination" #default="{items}" ref="instances" :key="host + state"> | ||||||
|  | 		<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="status"> | ||||||
|  | 				<span class="sub" v-if="instance.followersCount > 0"><i class="fas fa-caret-down icon"></i>Sub</span> | ||||||
|  | 				<span class="sub" v-else><i class="fas fa-caret-down icon"></i>-</span> | ||||||
|  | 				<span class="pub" v-if="instance.followingCount > 0"><i class="fas fa-caret-up icon"></i>Pub</span> | ||||||
|  | 				<span class="pub" v-else><i class="fas fa-caret-up icon"></i>-</span> | ||||||
|  | 				<span class="lastCommunicatedAt"><i class="fas fa-exchange-alt icon"></i><MkTime :time="instance.lastCommunicatedAt"/></span> | ||||||
|  | 				<span class="latestStatus"><i class="fas fa-traffic-light icon"></i>{{ instance.latestStatus || '-' }}</span> | ||||||
| 			</div> | 			</div> | ||||||
| 		</div> | 		</div> | ||||||
| 	</div> | 	</MkPagination> | ||||||
| 	<div class="_section"> |  | ||||||
| 		<div class="_content"> |  | ||||||
| 			<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="host"><i class="fas fa-circle indicator" :class="getStatus(instance)"></i><b>{{ instance.host }}</b></div> |  | ||||||
| 					<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-else><i class="fas fa-caret-down icon"></i>-</span> |  | ||||||
| 						<span class="pub" v-if="instance.followingCount > 0"><i class="fas fa-caret-up icon"></i>Pub</span> |  | ||||||
| 						<span class="pub" v-else><i class="fas fa-caret-up icon"></i>-</span> |  | ||||||
| 						<span class="lastCommunicatedAt"><i class="fas fa-exchange-alt icon"></i><MkTime :time="instance.lastCommunicatedAt"/></span> |  | ||||||
| 						<span class="latestStatus"><i class="fas fa-traffic-light icon"></i>{{ instance.latestStatus || '-' }}</span> |  | ||||||
| 					</div> |  | ||||||
| 				</div> |  | ||||||
| 			</MkPagination> |  | ||||||
| 		</div> |  | ||||||
| 	</div> |  | ||||||
| </div> | </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
|  | @ -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(`/instance/user/${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,239 @@ | ||||||
| <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"> | 					</div> | ||||||
| 				<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"> | 				<FormLink :active="page === 'overview'" replace to="/instance/overview"><template #icon><i class="fas fa-tachometer-alt"></i></template>{{ $ts.overview }}</FormLink> | ||||||
| 					<div class="_keyValue"><b>Node.js</b><span>{{ serverInfo.node }}</span></div> | 			</FormGroup> | ||||||
| 					<div class="_keyValue"><b>PostgreSQL</b><span>v{{ serverInfo.psql }}</span></div> | 			<FormGroup> | ||||||
| 					<div class="_keyValue"><b>Redis</b><span>v{{ serverInfo.redis }}</span></div> | 				<template #label>{{ $ts.quickAction }}</template> | ||||||
| 				</div> | 				<FormButton @click="lookup"><i class="fas fa-search"></i> {{ $ts.lookup }}</FormButton> | ||||||
| 			</MkContainer> | 				<FormButton v-if="$instance.disableRegistration" @click="invite"><i class="fas fa-user"></i> {{ $ts.invite }}</FormButton> | ||||||
| 			 | 			</FormGroup> | ||||||
| 			<MkContainer :foldable="true" :scrollable="true" class="_gap" style="height: 300px;"> | 			<FormGroup> | ||||||
| 				<template #header><i class="fas fa-database"></i>{{ $ts.database }}</template> | 				<FormLink :active="page === 'users'" replace to="/instance/users"><template #icon><i class="fas fa-users"></i></template>{{ $ts.users }}</FormLink> | ||||||
| 
 | 				<FormLink :active="page === 'emojis'" replace to="/instance/emojis"><template #icon><i class="fas fa-laugh"></i></template>{{ $ts.customEmojis }}</FormLink> | ||||||
| 				<div class="_content" v-if="dbInfo"> | 				<FormLink :active="page === 'federation'" replace to="/instance/federation"><template #icon><i class="fas fa-globe"></i></template>{{ $ts.federation }}</FormLink> | ||||||
| 					<table style="border-collapse: collapse; width: 100%;"> | 				<FormLink :active="page === 'queue'" replace to="/instance/queue"><template #icon><i class="fas fa-clipboard-list"></i></template>{{ $ts.jobQueue }}</FormLink> | ||||||
| 						<tr style="opacity: 0.7;"> | 				<FormLink :active="page === 'files'" replace to="/instance/files"><template #icon><i class="fas fa-cloud"></i></template>{{ $ts.files }}</FormLink> | ||||||
| 							<th style="text-align: left; padding: 0 8px 8px 0;">Table</th> | 				<FormLink :active="page === 'announcements'" replace to="/instance/announcements"><template #icon><i class="fas fa-broadcast-tower"></i></template>{{ $ts.announcements }}</FormLink> | ||||||
| 							<th style="text-align: left; padding: 0 8px 8px 0;">Records</th> | 				<FormLink :active="page === 'database'" replace to="/instance/database"><template #icon><i class="fas fa-database"></i></template>{{ $ts.database }}</FormLink> | ||||||
| 							<th style="text-align: left; padding: 0 0 8px 0;">Size</th> | 				<FormLink :active="page === 'abuses'" replace to="/instance/abuses"><template #icon><i class="fas fa-exclamation-circle"></i></template>{{ $ts.abuseReports }}</FormLink> | ||||||
| 						</tr> | 			</FormGroup> | ||||||
| 						<tr v-for="table in dbInfo" :key="table[0]"> | 			<FormGroup> | ||||||
| 							<th style="text-align: left; padding: 0 8px 0 0; word-break: break-all;">{{ table[0] }}</th> | 				<template #label>{{ $ts.settings }}</template> | ||||||
| 							<td style="padding: 0 8px 0 0;">{{ number(table[1].count) }}</td> | 				<FormLink :active="page === 'settings'" replace to="/instance/settings"><template #icon><i class="fas fa-cog"></i></template>{{ $ts.general }}</FormLink> | ||||||
| 							<td style="padding: 0; opacity: 0.7;">{{ bytes(table[1].size) }}</td> | 				<FormLink :active="page === 'files-settings'" replace to="/instance/files-settings"><template #icon><i class="fas fa-cloud"></i></template>{{ $ts.files }}</FormLink> | ||||||
| 						</tr> | 				<FormLink :active="page === 'email-settings'" replace to="/instance/email-settings"><template #icon><i class="fas fa-envelope"></i></template>{{ $ts.emailServer }}</FormLink> | ||||||
| 					</table> | 				<FormLink :active="page === 'object-storage'" replace to="/instance/object-storage"><template #icon><i class="fas fa-cloud"></i></template>{{ $ts.objectStorage }}</FormLink> | ||||||
| 				</div> | 				<FormLink :active="page === 'security'" replace to="/instance/security"><template #icon><i class="fas fa-lock"></i></template>{{ $ts.security }}</FormLink> | ||||||
| 			</MkContainer> | 				<FormLink :active="page === 'service-worker'" replace to="/instance/service-worker"><template #icon><i class="fas fa-bolt"></i></template>ServiceWorker</FormLink> | ||||||
| 		</div> | 				<FormLink :active="page === 'relays'" replace to="/instance/relays"><template #icon><i class="fas fa-globe"></i></template>{{ $ts.relays }}</FormLink> | ||||||
| 	</MkFolder> | 				<FormLink :active="page === 'integrations'" replace to="/instance/integrations"><template #icon><i class="fas fa-share-alt"></i></template>{{ $ts.integration }}</FormLink> | ||||||
| </div> | 				<FormLink :active="page === 'instance-block'" replace to="/instance/instance-block"><template #icon><i class="fas fa-ban"></i></template>{{ $ts.instanceBlocking }}</FormLink> | ||||||
| <div v-if="page === 'logs'" class="_section"> | 				<FormLink :active="page === 'proxy-account'" replace to="/instance/proxy-account"><template #icon><i class="fas fa-ghost"></i></template>{{ $ts.proxyAccount }}</FormLink> | ||||||
| 	<MkFolder> | 				<FormLink :active="page === 'other-settings'" replace to="/instance/other-settings"><template #icon><i class="fas fa-cogs"></i></template>{{ $ts.other }}</FormLink> | ||||||
| 		<template #header><i class="fas fa-stream"></i> {{ $ts.logs }}</template> | 			</FormGroup> | ||||||
| 
 | 		</FormBase> | ||||||
| 		<div class="_keyValue" v-for="log in modLogs"> | 	</div> | ||||||
| 			<b>{{ log.type }}</b><span>by {{ log.user.username }}</span><MkTime :time="log.createdAt" style="opacity: 0.7;"/> | 	<div class="main"> | ||||||
| 		</div> | 		<component :is="component" :key="page" @info="onInfo" v-bind="pageProps"/> | ||||||
| 	</MkFolder> | 	</div> | ||||||
| </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: { | ||||||
| 		return { | 		initialPage: { | ||||||
| 			[symbols.PAGE_INFO]: { | 			type: String, | ||||||
| 				tabs: [{ | 			required: false | ||||||
| 					id: 'index', |  | ||||||
| 					title: null, |  | ||||||
| 					tooltip: this.$ts.instance, |  | ||||||
| 					icon: 'fas fa-server', |  | ||||||
| 					onClick: () => { this.page = 'index'; }, |  | ||||||
| 					selected: computed(() => this.page === 'index') |  | ||||||
| 				}, { |  | ||||||
| 					id: 'metrics', |  | ||||||
| 					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: { | 	setup(props, context) { | ||||||
| 		meta() { | 		const indexInfo = { | ||||||
| 			return this.$instance; | 			title: i18n.locale.instance, | ||||||
| 		}, | 			icon: 'fas fa-cog' | ||||||
| 	}, | 		}; | ||||||
| 
 | 		const INFO = ref(indexInfo); | ||||||
| 	mounted() { | 		const page = ref(props.initialPage); | ||||||
| 		this.fetchJobs(); | 		const narrow = ref(false); | ||||||
| 		this.fetchModLogs(); | 		const view = ref(null); | ||||||
| 
 | 		const el = ref(null); | ||||||
| 		os.api('admin/server-info', {}).then(res => { | 		const onInfo = (viewInfo) => { | ||||||
| 			this.serverInfo = res; | 			INFO.value = viewInfo; | ||||||
| 		}); | 		}; | ||||||
| 
 | 		const pageProps = ref({}); | ||||||
| 		os.api('admin/get-table-stats', {}).then(res => { | 		const component = computed(() => { | ||||||
| 			this.dbInfo = Object.entries(res).sort((a, b) => b[1].size - a[1].size); | 			if (page.value == null) return null; | ||||||
| 		}); | 			switch (page.value) { | ||||||
| 	}, | 				case 'overview': return defineAsyncComponent(() => import('./overview.vue')); | ||||||
| 
 | 				case 'users': return defineAsyncComponent(() => import('./users.vue')); | ||||||
| 	methods: { | 				case 'emojis': return defineAsyncComponent(() => import('./emojis.vue')); | ||||||
| 		async showInstanceInfo(q) { | 				case 'federation': return defineAsyncComponent(() => import('./federation.vue')); | ||||||
| 			let instance = q; | 				case 'queue': return defineAsyncComponent(() => import('./queue.vue')); | ||||||
| 			if (typeof q === 'string') { | 				case 'files': return defineAsyncComponent(() => import('./files.vue')); | ||||||
| 				instance = await os.api('federation/show-instance', { | 				case 'announcements': return defineAsyncComponent(() => import('./announcements.vue')); | ||||||
| 					host: q | 				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')); | ||||||
| 			} | 			} | ||||||
| 			os.popup(MkInstanceInfo, { | 		}); | ||||||
| 				instance: instance |  | ||||||
| 			}, {}, 'closed'); |  | ||||||
| 		}, |  | ||||||
| 
 | 
 | ||||||
| 		fetchJobs() { | 		watch(component, () => { | ||||||
| 			os.api('admin/queue/deliver-delayed', {}).then(jobs => { | 			pageProps.value = {}; | ||||||
| 				this.jobs = jobs; | 
 | ||||||
|  | 			nextTick(() => { | ||||||
|  | 				scroll(el.value, 0); | ||||||
| 			}); | 			}); | ||||||
| 		}, | 		}, { immediate: true }); | ||||||
| 
 | 
 | ||||||
| 		fetchModLogs() { | 		watch(() => props.initialPage, () => { | ||||||
| 			os.api('admin/show-moderation-logs', {}).then(logs => { | 			if (props.initialPage == null && !narrow.value) { | ||||||
| 				this.modLogs = logs; | 				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 | ||||||
|  | 				}); | ||||||
| 			}); | 			}); | ||||||
| 		}, | 		}; | ||||||
| 
 | 
 | ||||||
| 		bytes, | 		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); | ||||||
|  | 		}; | ||||||
| 
 | 
 | ||||||
| 		number, | 		return { | ||||||
| 	} | 			[symbols.PAGE_INFO]: INFO, | ||||||
|  | 			page, | ||||||
|  | 			narrow, | ||||||
|  | 			view, | ||||||
|  | 			el, | ||||||
|  | 			onInfo, | ||||||
|  | 			pageProps, | ||||||
|  | 			component, | ||||||
|  | 			invite, | ||||||
|  | 			lookup, | ||||||
|  | 		}; | ||||||
|  | 	}, | ||||||
| }); | }); | ||||||
| </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; | ||||||
|  | 
 | ||||||
|  | 	> img { | ||||||
|  | 		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,101 +1,52 @@ | ||||||
| <template> | <template> | ||||||
| <div> | <div class="_formItem"> | ||||||
| 	<MkFolder> | 	<div class="_formLabel"><i class="fas fa-microchip"></i> {{ $ts.cpuAndMemory }}</div> | ||||||
| 		<template #header><i class="fas fa-heartbeat"></i> {{ $ts.metrics }}</template> | 	<div class="_formPanel xhexznfu"> | ||||||
| 		<div class="_section" style="padding: 0 var(--margin);"> | 		<div> | ||||||
| 			<div class="_content"> | 			<canvas :ref="cpumem"></canvas> | ||||||
| 				<MkContainer :foldable="false" class="_gap"> | 		</div> | ||||||
| 					<template #header><i class="fas fa-microchip"></i>{{ $ts.cpuAndMemory }}</template> | 		<div v-if="serverInfo"> | ||||||
| 					<!-- | 			<div class="_table"> | ||||||
| 					<template #func> | 				<div class="_row"> | ||||||
| 						<button class="_button" @click="resume" :disabled="!paused"><i class="fas fa-play"></i></button> | 					<div class="_cell"><div class="_label">MEM total</div>{{ bytes(serverInfo.mem.total) }}</div> | ||||||
| 						<button class="_button" @click="pause" :disabled="paused"><i class="fas fa-pause"></i></button> | 					<div class="_cell"><div class="_label">MEM used</div>{{ bytes(memUsage) }} ({{ (memUsage / serverInfo.mem.total * 100).toFixed(0) }}%)</div> | ||||||
| 					</template> | 					<div class="_cell"><div class="_label">MEM free</div>{{ bytes(serverInfo.mem.total - memUsage) }} ({{ ((serverInfo.mem.total - memUsage) / serverInfo.mem.total * 100).toFixed(0) }}%)</div> | ||||||
| 					--> | 				</div> | ||||||
| 
 |  | ||||||
| 					<div class="_content" style="margin-top: -8px; margin-bottom: -12px;"> |  | ||||||
| 						<canvas :ref="cpumem"></canvas> |  | ||||||
| 					</div> |  | ||||||
| 					<div class="_content" v-if="serverInfo"> |  | ||||||
| 						<div class="_table"> |  | ||||||
| 							<div class="_row"> |  | ||||||
| 								<div class="_cell"><div class="_label">MEM total</div>{{ bytes(serverInfo.mem.total) }}</div> |  | ||||||
| 								<div class="_cell"><div class="_label">MEM used</div>{{ bytes(memUsage) }} ({{ (memUsage / serverInfo.mem.total * 100).toFixed(0) }}%)</div> |  | ||||||
| 								<div class="_cell"><div class="_label">MEM free</div>{{ bytes(serverInfo.mem.total - memUsage) }} ({{ ((serverInfo.mem.total - memUsage) / serverInfo.mem.total * 100).toFixed(0) }}%)</div> |  | ||||||
| 							</div> |  | ||||||
| 						</div> |  | ||||||
| 					</div> |  | ||||||
| 				</MkContainer> |  | ||||||
| 
 |  | ||||||
| 				<MkContainer :foldable="false" class="_gap"> |  | ||||||
| 					<template #header><i class="fas fa-hdd"></i> {{ $ts.disk }}</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="disk"></canvas> |  | ||||||
| 					</div> |  | ||||||
| 					<div class="_content" v-if="serverInfo"> |  | ||||||
| 						<div class="_table"> |  | ||||||
| 							<div class="_row"> |  | ||||||
| 								<div class="_cell"><div class="_label">Disk total</div>{{ bytes(serverInfo.fs.total) }}</div> |  | ||||||
| 								<div class="_cell"><div class="_label">Disk used</div>{{ bytes(serverInfo.fs.used) }} ({{ (serverInfo.fs.used / serverInfo.fs.total * 100).toFixed(0) }}%)</div> |  | ||||||
| 								<div class="_cell"><div class="_label">Disk free</div>{{ bytes(serverInfo.fs.total - serverInfo.fs.used) }} ({{ ((serverInfo.fs.total - serverInfo.fs.used) / serverInfo.fs.total * 100).toFixed(0) }}%)</div> |  | ||||||
| 							</div> |  | ||||||
| 						</div> |  | ||||||
| 					</div> |  | ||||||
| 				</MkContainer> |  | ||||||
| 
 |  | ||||||
| 				<MkContainer :foldable="false" class="_gap"> |  | ||||||
| 					<template #header><i class="fas fa-exchange-alt"></i> {{ $ts.network }}</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="net"></canvas> |  | ||||||
| 					</div> |  | ||||||
| 					<div class="_content" v-if="serverInfo"> |  | ||||||
| 						<div class="_table"> |  | ||||||
| 							<div class="_row"> |  | ||||||
| 								<div class="_cell"><div class="_label">Interface</div>{{ serverInfo.net.interface }}</div> |  | ||||||
| 							</div> |  | ||||||
| 						</div> |  | ||||||
| 					</div> |  | ||||||
| 				</MkContainer> |  | ||||||
| 			</div> | 			</div> | ||||||
| 		</div> | 		</div> | ||||||
| 	</MkFolder> | 	</div> | ||||||
| 
 | </div> | ||||||
| 	<MkFolder> | <div class="_formItem"> | ||||||
| 		<template #header><i class="fas fa-clipboard-list"></i> {{ $ts.jobQueue }}</template> | 	<div class="_formLabel"><i class="fas fa-hdd"></i> {{ $ts.disk }}</div> | ||||||
| 
 | 	<div class="_formPanel xhexznfu"> | ||||||
| 		<div class="vkyrmkwb" :style="{ gridTemplateRows: queueHeight }"> | 		<div> | ||||||
| 			<MkContainer :foldable="false" :scrollable="true" :resize-base-el="() => $el"> | 			<canvas :ref="disk"></canvas> | ||||||
| 				<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> | 		</div> | ||||||
| 	</MkFolder> | 		<div v-if="serverInfo"> | ||||||
|  | 			<div class="_table"> | ||||||
|  | 				<div class="_row"> | ||||||
|  | 					<div class="_cell"><div class="_label">Disk total</div>{{ bytes(serverInfo.fs.total) }}</div> | ||||||
|  | 					<div class="_cell"><div class="_label">Disk used</div>{{ bytes(serverInfo.fs.used) }} ({{ (serverInfo.fs.used / serverInfo.fs.total * 100).toFixed(0) }}%)</div> | ||||||
|  | 					<div class="_cell"><div class="_label">Disk free</div>{{ bytes(serverInfo.fs.total - serverInfo.fs.used) }} ({{ ((serverInfo.fs.total - serverInfo.fs.used) / serverInfo.fs.total * 100).toFixed(0) }}%)</div> | ||||||
|  | 				</div> | ||||||
|  | 			</div> | ||||||
|  | 		</div> | ||||||
|  | 	</div> | ||||||
|  | </div> | ||||||
|  | <div class="_formItem"> | ||||||
|  | 	<div class="_formLabel"><i class="fas fa-exchange-alt"></i> {{ $ts.network }}</div> | ||||||
|  | 	<div class="_formPanel xhexznfu"> | ||||||
|  | 		<div> | ||||||
|  | 			<canvas :ref="net"></canvas> | ||||||
|  | 		</div> | ||||||
|  | 		<div v-if="serverInfo"> | ||||||
|  | 			<div class="_table"> | ||||||
|  | 				<div class="_row"> | ||||||
|  | 					<div class="_cell"><div class="_label">Interface</div>{{ serverInfo.net.interface }}</div> | ||||||
|  | 				</div> | ||||||
|  | 			</div> | ||||||
|  | 		</div> | ||||||
|  | 	</div> | ||||||
| </div> | </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
|  | @ -188,9 +139,11 @@ export default defineComponent({ | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	beforeUnmount() { | 	beforeUnmount() { | ||||||
| 		this.connection.off('stats', this.onStats); | 		if (this.connection) { | ||||||
| 		this.connection.off('statsLog', this.onStatsLog); | 			this.connection.off('stats', this.onStats); | ||||||
| 		this.connection.dispose(); | 			this.connection.off('statsLog', this.onStatsLog); | ||||||
|  | 			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> | ||||||
							
								
								
									
										131
									
								
								src/client/pages/instance/overview.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										131
									
								
								src/client/pages/instance/overview.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,131 @@ | ||||||
|  | <template> | ||||||
|  | <FormBase> | ||||||
|  | 	<FormSuspense :p="init"> | ||||||
|  | 		<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 VueJsonPretty from 'vue-json-pretty'; | ||||||
|  | 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, | ||||||
|  | 		FormKeyValueView, | ||||||
|  | 		MkInstanceStats, | ||||||
|  | 		MkButton, | ||||||
|  | 		MkSelect, | ||||||
|  | 		MkInput, | ||||||
|  | 		MkContainer, | ||||||
|  | 		MkFolder, | ||||||
|  | 		XMetrics, | ||||||
|  | 		VueJsonPretty, | ||||||
|  | 	}, | ||||||
|  | 
 | ||||||
|  | 	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', {}), | ||||||
|  | 		} | ||||||
|  | 	}, | ||||||
|  | 
 | ||||||
|  | 	async mounted() { | ||||||
|  | 		this.$emit('info', this[symbols.PAGE_INFO]); | ||||||
|  | 	}, | ||||||
|  | 
 | ||||||
|  | 	methods: { | ||||||
|  | 		async init() { | ||||||
|  | 			this.meta = await os.api('meta', { detail: true }); | ||||||
|  | 		}, | ||||||
|  | 	 | ||||||
|  | 		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,27 +1,29 @@ | ||||||
| <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="_row"> | 		<div class="_table status"> | ||||||
| 			<div class="_cell"><div class="_label">Process</div>{{ number(activeSincePrevTick) }}</div> | 			<div class="_row"> | ||||||
| 			<div class="_cell"><div class="_label">Active</div>{{ number(active) }}</div> | 				<div class="_cell"><div class="_label">Process</div>{{ number(activeSincePrevTick) }}</div> | ||||||
| 			<div class="_cell"><div class="_label">Waiting</div>{{ number(waiting) }}</div> | 				<div class="_cell"><div class="_label">Active</div>{{ number(active) }}</div> | ||||||
| 			<div class="_cell"><div class="_label">Delayed</div>{{ number(delayed) }}</div> | 				<div class="_cell"><div class="_label">Waiting</div>{{ number(waiting) }}</div> | ||||||
| 		</div> | 				<div class="_cell"><div class="_label">Delayed</div>{{ number(delayed) }}</div> | ||||||
| 	</div> |  | ||||||
| 	<div class="_content" style="margin-bottom: -8px;"> |  | ||||||
| 		<canvas ref="chart"></canvas> |  | ||||||
| 	</div> |  | ||||||
| 	<div class="_content" style="max-height: 180px; overflow: auto;"> |  | ||||||
| 		<div v-if="jobs.length > 0"> |  | ||||||
| 			<div v-for="job in jobs" :key="job[0]"> |  | ||||||
| 				<span>{{ job[0] }}</span> |  | ||||||
| 				<span style="margin-left: 8px; opacity: 0.7;">({{ number(job[1]) }} jobs)</span> |  | ||||||
| 			</div> | 			</div> | ||||||
| 		</div> | 		</div> | ||||||
| 		<span v-else style="opacity: 0.5;">{{ $ts.noJobs }}</span> | 		<div class=""> | ||||||
|  | 			<canvas ref="chart"></canvas> | ||||||
|  | 		</div> | ||||||
|  | 		<div class="jobs"> | ||||||
|  | 			<div v-if="jobs.length > 0"> | ||||||
|  | 				<div v-for="job in jobs" :key="job[0]"> | ||||||
|  | 					<span>{{ job[0] }}</span> | ||||||
|  | 					<span style="margin-left: 8px; opacity: 0.7;">({{ number(job[1]) }} jobs)</span> | ||||||
|  | 				</div> | ||||||
|  | 			</div> | ||||||
|  | 			<span v-else style="opacity: 0.5;">{{ $ts.noJobs }}</span> | ||||||
|  | 		</div> | ||||||
| 	</div> | 	</div> | ||||||
| </section> | </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> |  | ||||||
							
								
								
									
										229
									
								
								src/client/pages/instance/user.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										229
									
								
								src/client/pages/instance/user.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,229 @@ | ||||||
|  | <template> | ||||||
|  | <FormBase> | ||||||
|  | 	<FormSuspense :p="init"> | ||||||
|  | 		<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> | ||||||
|  | 				<template #key>ID</template> | ||||||
|  | 				<template #value><span class="_monospace">{{ user.id }}</span></template> | ||||||
|  | 			</FormKeyValueView> | ||||||
|  | 		</FormGroup> | ||||||
|  | 
 | ||||||
|  | 		<FormGroup> | ||||||
|  | 			<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" @click="resetPassword"><i class="fas fa-key"></i> {{ $ts.resetPassword }}</FormButton> | ||||||
|  | 		</FormGroup> | ||||||
|  | 
 | ||||||
|  | 		<FormGroup> | ||||||
|  | 			<FormLink :to="`/user-ap-info/${user.id}`">ActivityPub</FormLink> | ||||||
|  | 
 | ||||||
|  | 			<FormLink v-if="user.host" :to="`/instance-info/${user.host}`">{{ $ts.instanceInfo }}<template #suffix>{{ user.host }}</template></FormLink> | ||||||
|  | 			<FormKeyValueView v-else> | ||||||
|  | 				<template #key>{{ $ts.instanceInfo }}</template> | ||||||
|  | 				<template #value>(Local user)</template> | ||||||
|  | 			</FormKeyValueView> | ||||||
|  | 		</FormGroup> | ||||||
|  | 
 | ||||||
|  | 		<FormGroup> | ||||||
|  | 			<FormKeyValueView> | ||||||
|  | 				<template #key>{{ $ts.updatedAt }}</template> | ||||||
|  | 				<template #value><MkTime v-if="user.lastFetchedAt" mode="detail" :time="user.lastFetchedAt"/><span v-else>N/A</span></template> | ||||||
|  | 			</FormKeyValueView> | ||||||
|  | 		</FormGroup> | ||||||
|  | 
 | ||||||
|  | 		<FormObjectView tall :value="user"> | ||||||
|  | 			<span>Raw</span> | ||||||
|  | 		</FormObjectView> | ||||||
|  | 	</FormSuspense> | ||||||
|  | </FormBase> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script lang="ts"> | ||||||
|  | import { computed, defineAsyncComponent, defineComponent } from 'vue'; | ||||||
|  | import FormObjectView from '@client/components/form/object-view.vue'; | ||||||
|  | import FormSwitch from '@client/components/form/switch.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 FormKeyValueView from '@client/components/form/key-value-view.vue'; | ||||||
|  | import FormSuspense from '@client/components/form/suspense.vue'; | ||||||
|  | import * as os from '@client/os'; | ||||||
|  | import number from '@client/filters/number'; | ||||||
|  | import bytes from '@client/filters/bytes'; | ||||||
|  | import * as symbols from '@client/symbols'; | ||||||
|  | import { url } from '@client/config'; | ||||||
|  | import { userPage, acct } from '@client/filters/user'; | ||||||
|  | 
 | ||||||
|  | export default defineComponent({ | ||||||
|  | 	components: { | ||||||
|  | 		FormBase, | ||||||
|  | 		FormSwitch, | ||||||
|  | 		FormObjectView, | ||||||
|  | 		FormButton, | ||||||
|  | 		FormLink, | ||||||
|  | 		FormGroup, | ||||||
|  | 		FormKeyValueView, | ||||||
|  | 		FormSuspense, | ||||||
|  | 	}, | ||||||
|  | 
 | ||||||
|  | 	props: { | ||||||
|  | 		userId: { | ||||||
|  | 			type: String, | ||||||
|  | 			required: true | ||||||
|  | 		} | ||||||
|  | 	}, | ||||||
|  | 
 | ||||||
|  | 	data() { | ||||||
|  | 		return { | ||||||
|  | 			[symbols.PAGE_INFO]: computed(() => ({ | ||||||
|  | 				title: this.$ts.userInfo, | ||||||
|  | 				icon: 'fas fa-info-circle', | ||||||
|  | 				actions: this.user ? [this.user.url ? { | ||||||
|  | 					text: this.user.url, | ||||||
|  | 					icon: 'fas fa-external-link-alt', | ||||||
|  | 					handler: () => { | ||||||
|  | 						window.open(this.user.url, '_blank'); | ||||||
|  | 					} | ||||||
|  | 				} : undefined].filter(x => x !== undefined) : [], | ||||||
|  | 			})), | ||||||
|  | 			init: null, | ||||||
|  | 			user: null, | ||||||
|  | 			info: null, | ||||||
|  | 			moderator: false, | ||||||
|  | 			silenced: false, | ||||||
|  | 			suspended: false, | ||||||
|  | 		} | ||||||
|  | 	}, | ||||||
|  | 
 | ||||||
|  | 	watch: { | ||||||
|  | 		userId: { | ||||||
|  | 			handler() { | ||||||
|  | 				this.init = this.createFetcher(); | ||||||
|  | 			}, | ||||||
|  | 			immediate: true | ||||||
|  | 		} | ||||||
|  | 	}, | ||||||
|  | 
 | ||||||
|  | 	methods: { | ||||||
|  | 		number, | ||||||
|  | 		bytes, | ||||||
|  | 		userPage, | ||||||
|  | 		acct, | ||||||
|  | 
 | ||||||
|  | 		createFetcher() { | ||||||
|  | 			return () => Promise.all([os.api('users/show', { | ||||||
|  | 				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; | ||||||
|  | 			}); | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
|  | 		refreshUser() { | ||||||
|  | 			this.init = this.createFetcher(); | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
|  | 		async updateRemoteUser() { | ||||||
|  | 			await os.apiWithDialog('admin/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> | ||||||
|  | 
 | ||||||
|  | <style lang="scss" scoped> | ||||||
|  | .aeakzknw { | ||||||
|  | 	> .avatar { | ||||||
|  | 		display: block; | ||||||
|  | 		margin: 0 auto; | ||||||
|  | 		width: 64px; | ||||||
|  | 		height: 64px; | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | </style> | ||||||
|  | @ -1,86 +1,71 @@ | ||||||
| <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> | 		<MkButton inline primary @click="lookupUser()"><i class="fas fa-search"></i> {{ $ts.lookup }}</MkButton> | ||||||
| 		</div> |  | ||||||
| 	</div> | 	</div> | ||||||
| 
 | 
 | ||||||
| 	<div class="_section lookup"> | 	<div class="users"> | ||||||
| 		<div class="_title"><i class="fas fa-search"></i> {{ $ts.lookup }}</div> | 		<div class="inputs" style="display: flex;"> | ||||||
| 		<div class="_content"> | 			<MkSelect v-model:value="sort" style="margin: 0; flex: 1;"> | ||||||
| 			<MkInput class="target" v-model:value="target" type="text" @enter="showUser()"> | 				<template #label>{{ $ts.sort }}</template> | ||||||
| 				<span>{{ $ts.usernameOrUserId }}</span> | 				<option value="-createdAt">{{ $ts.registeredDate }} ({{ $ts.ascendingOrder }})</option> | ||||||
|  | 				<option value="+createdAt">{{ $ts.registeredDate }} ({{ $ts.descendingOrder }})</option> | ||||||
|  | 				<option value="-updatedAt">{{ $ts.lastUsed }} ({{ $ts.ascendingOrder }})</option> | ||||||
|  | 				<option value="+updatedAt">{{ $ts.lastUsed }} ({{ $ts.descendingOrder }})</option> | ||||||
|  | 			</MkSelect> | ||||||
|  | 			<MkSelect v-model:value="state" style="margin: 0; flex: 1;"> | ||||||
|  | 				<template #label>{{ $ts.state }}</template> | ||||||
|  | 				<option value="all">{{ $ts.all }}</option> | ||||||
|  | 				<option value="available">{{ $ts.normal }}</option> | ||||||
|  | 				<option value="admin">{{ $ts.administrator }}</option> | ||||||
|  | 				<option value="moderator">{{ $ts.moderator }}</option> | ||||||
|  | 				<option value="silenced">{{ $ts.silence }}</option> | ||||||
|  | 				<option value="suspended">{{ $ts.suspend }}</option> | ||||||
|  | 			</MkSelect> | ||||||
|  | 			<MkSelect v-model:value="origin" style="margin: 0; flex: 1;"> | ||||||
|  | 				<template #label>{{ $ts.instance }}</template> | ||||||
|  | 				<option value="combined">{{ $ts.all }}</option> | ||||||
|  | 				<option value="local">{{ $ts.local }}</option> | ||||||
|  | 				<option value="remote">{{ $ts.remote }}</option> | ||||||
|  | 			</MkSelect> | ||||||
|  | 		</div> | ||||||
|  | 		<div class="inputs" style="display: flex; padding-top: 1.2em;"> | ||||||
|  | 			<MkInput v-model:value="searchUsername" style="margin: 0; flex: 1;" type="text" spellcheck="false" @update:value="$refs.users.reload()"> | ||||||
|  | 				<span>{{ $ts.username }}</span> | ||||||
|  | 			</MkInput> | ||||||
|  | 			<MkInput v-model:value="searchHost" style="margin: 0; flex: 1;" type="text" spellcheck="false" @update:value="$refs.users.reload()" :disabled="pagination.params().origin === 'local'"> | ||||||
|  | 				<span>{{ $ts.host }}</span> | ||||||
| 			</MkInput> | 			</MkInput> | ||||||
| 			<MkButton @click="showUser()" primary><i class="fas fa-search"></i> {{ $ts.lookup }}</MkButton> |  | ||||||
| 		</div> | 		</div> | ||||||
| 	</div> |  | ||||||
| 
 | 
 | ||||||
| 	<div class="_section users"> | 		<MkPagination :pagination="pagination" #default="{items}" class="users" ref="users"> | ||||||
| 		<div class="_title"><i class="fas fa-users"></i> {{ $ts.users }}</div> | 			<button class="user _panel _button _gap" v-for="user in items" :key="user.id" @click="show(user)"> | ||||||
| 		<div class="_content"> | 				<MkAvatar class="avatar" :user="user" :disable-link="true" :show-indicator="true"/> | ||||||
| 			<div class="inputs" style="display: flex;"> | 				<div class="body"> | ||||||
| 				<MkSelect v-model:value="sort" style="margin: 0; flex: 1;"> | 					<header> | ||||||
| 					<template #label>{{ $ts.sort }}</template> | 						<MkUserName class="name" :user="user"/> | ||||||
| 					<option value="-createdAt">{{ $ts.registeredDate }} ({{ $ts.ascendingOrder }})</option> | 						<span class="acct">@{{ acct(user) }}</span> | ||||||
| 					<option value="+createdAt">{{ $ts.registeredDate }} ({{ $ts.descendingOrder }})</option> | 						<span class="staff" v-if="user.isAdmin"><i class="fas fa-bookmark"></i></span> | ||||||
| 					<option value="-updatedAt">{{ $ts.lastUsed }} ({{ $ts.ascendingOrder }})</option> | 						<span class="staff" v-if="user.isModerator"><i class="far fa-bookmark"></i></span> | ||||||
| 					<option value="+updatedAt">{{ $ts.lastUsed }} ({{ $ts.descendingOrder }})</option> | 						<span class="punished" v-if="user.isSilenced"><i class="fas fa-microphone-slash"></i></span> | ||||||
| 				</MkSelect> | 						<span class="punished" v-if="user.isSuspended"><i class="fas fa-snowflake"></i></span> | ||||||
| 				<MkSelect v-model:value="state" style="margin: 0; flex: 1;"> | 					</header> | ||||||
| 					<template #label>{{ $ts.state }}</template> | 					<div> | ||||||
| 					<option value="all">{{ $ts.all }}</option> | 						<span>{{ $ts.lastUsed }}: <MkTime v-if="user.updatedAt" :time="user.updatedAt" mode="detail"/></span> | ||||||
| 					<option value="available">{{ $ts.normal }}</option> |  | ||||||
| 					<option value="admin">{{ $ts.administrator }}</option> |  | ||||||
| 					<option value="moderator">{{ $ts.moderator }}</option> |  | ||||||
| 					<option value="silenced">{{ $ts.silence }}</option> |  | ||||||
| 					<option value="suspended">{{ $ts.suspend }}</option> |  | ||||||
| 				</MkSelect> |  | ||||||
| 				<MkSelect v-model:value="origin" style="margin: 0; flex: 1;"> |  | ||||||
| 					<template #label>{{ $ts.instance }}</template> |  | ||||||
| 					<option value="combined">{{ $ts.all }}</option> |  | ||||||
| 					<option value="local">{{ $ts.local }}</option> |  | ||||||
| 					<option value="remote">{{ $ts.remote }}</option> |  | ||||||
| 				</MkSelect> |  | ||||||
| 			</div> |  | ||||||
| 			<div class="inputs" style="display: flex; padding-top: 1.2em;"> |  | ||||||
| 				<MkInput v-model:value="searchUsername" style="margin: 0; flex: 1;" type="text" spellcheck="false" @update:value="$refs.users.reload()"> |  | ||||||
| 					<span>{{ $ts.username }}</span> |  | ||||||
| 				</MkInput> |  | ||||||
| 				<MkInput v-model:value="searchHost" style="margin: 0; flex: 1;" type="text" spellcheck="false" @update:value="$refs.users.reload()" :disabled="pagination.params().origin === 'local'"> |  | ||||||
| 					<span>{{ $ts.host }}</span> |  | ||||||
| 				</MkInput> |  | ||||||
| 			</div> |  | ||||||
| 
 |  | ||||||
| 			<MkPagination :pagination="pagination" #default="{items}" class="users" ref="users"> |  | ||||||
| 				<button class="user _panel _button _gap" v-for="user in items" :key="user.id" @click="show(user)"> |  | ||||||
| 					<MkAvatar class="avatar" :user="user" :disable-link="true" :show-indicator="true"/> |  | ||||||
| 					<div class="body"> |  | ||||||
| 						<header> |  | ||||||
| 							<MkUserName class="name" :user="user"/> |  | ||||||
| 							<span class="acct">@{{ acct(user) }}</span> |  | ||||||
| 							<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> |  | ||||||
| 						</header> |  | ||||||
| 						<div> |  | ||||||
| 							<span>{{ $ts.lastUsed }}: <MkTime v-if="user.updatedAt" :time="user.updatedAt" mode="detail"/></span> |  | ||||||
| 						</div> |  | ||||||
| 						<div> |  | ||||||
| 							<span>{{ $ts.registeredDate }}: <MkTime :time="user.createdAt" mode="detail"/></span> |  | ||||||
| 						</div> |  | ||||||
| 					</div> | 					</div> | ||||||
| 				</button> | 					<div> | ||||||
| 			</MkPagination> | 						<span>{{ $ts.registeredDate }}: <MkTime :time="user.createdAt" mode="detail"/></span> | ||||||
| 		</div> | 					</div> | ||||||
|  | 				</div> | ||||||
|  | 			</button> | ||||||
|  | 		</MkPagination> | ||||||
| 	</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(`/instance/user/${user.id}`); | ||||||
| 				userId: user.id |  | ||||||
| 			}, {}, 'closed'); |  | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
| 		acct | 		acct | ||||||
|  | @ -214,57 +171,61 @@ 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 { |  | ||||||
| 				margin-top: var(--margin); |  | ||||||
| 	 | 	 | ||||||
| 				> .user { | 		> .users { | ||||||
| 					display: flex; | 			margin-top: var(--margin); | ||||||
| 					width: 100%; |  | ||||||
| 					box-sizing: border-box; |  | ||||||
| 					text-align: left; |  | ||||||
| 					align-items: center; |  | ||||||
| 					padding: 16px; |  | ||||||
| 
 | 
 | ||||||
| 					&:hover { | 			> .user { | ||||||
| 						color: var(--accent); | 				display: flex; | ||||||
|  | 				width: 100%; | ||||||
|  | 				box-sizing: border-box; | ||||||
|  | 				text-align: left; | ||||||
|  | 				align-items: center; | ||||||
|  | 				padding: 16px; | ||||||
|  | 
 | ||||||
|  | 				&:hover { | ||||||
|  | 					color: var(--accent); | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				> .avatar { | ||||||
|  | 					width: 60px; | ||||||
|  | 					height: 60px; | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				> .body { | ||||||
|  | 					margin-left: 0.3em; | ||||||
|  | 					padding: 0 8px; | ||||||
|  | 					flex: 1; | ||||||
|  | 
 | ||||||
|  | 					@media (max-width: 500px) { | ||||||
|  | 						font-size: 14px; | ||||||
| 					} | 					} | ||||||
| 
 | 
 | ||||||
| 					> .avatar { | 					> header { | ||||||
| 						width: 60px; | 						> .name { | ||||||
| 						height: 60px; | 							font-weight: bold; | ||||||
| 					} |  | ||||||
| 
 |  | ||||||
| 					> .body { |  | ||||||
| 						margin-left: 0.3em; |  | ||||||
| 						padding: 0 8px; |  | ||||||
| 						flex: 1; |  | ||||||
| 
 |  | ||||||
| 						@media (max-width: 500px) { |  | ||||||
| 							font-size: 14px; |  | ||||||
| 						} | 						} | ||||||
| 
 | 
 | ||||||
| 						> header { | 						> .acct { | ||||||
| 							> .name { | 							margin-left: 8px; | ||||||
| 								font-weight: bold; | 							opacity: 0.7; | ||||||
| 							} | 						} | ||||||
| 
 | 
 | ||||||
| 							> .acct { | 						> .staff { | ||||||
| 								margin-left: 8px; | 							margin-left: 0.5em; | ||||||
| 								opacity: 0.7; | 							color: var(--badge); | ||||||
| 							} | 						} | ||||||
| 
 | 
 | ||||||
| 							> .staff { | 						> .punished { | ||||||
| 								margin-left: 0.5em; | 							margin-left: 0.5em; | ||||||
| 								color: var(--badge); | 							color: #4dabf7; | ||||||
| 							} |  | ||||||
| 
 |  | ||||||
| 							> .punished { |  | ||||||
| 								margin-left: 0.5em; |  | ||||||
| 								color: #4dabf7; |  | ||||||
| 							} |  | ||||||
| 						} | 						} | ||||||
| 					} | 					} | ||||||
| 				} | 				} | ||||||
|  |  | ||||||
|  | @ -1,34 +1,36 @@ | ||||||
| <template> | <template> | ||||||
| <FormBase> | <FormBase> | ||||||
| 	<FormGroup v-if="user"> | 	<FormSuspense :p="init"> | ||||||
| 		<template #label><MkAcct :user="user"/></template> |  | ||||||
| 
 |  | ||||||
| 		<FormKeyValueView> |  | ||||||
| 			<template #key>ID</template> |  | ||||||
| 			<template #value><span class="_monospace">{{ user.id }}</span></template> |  | ||||||
| 		</FormKeyValueView> |  | ||||||
| 
 |  | ||||||
| 		<FormGroup> | 		<FormGroup> | ||||||
| 			<FormLink :to="`/user-ap-info/${user.id}`">ActivityPub</FormLink> | 			<template #label><MkAcct :user="user"/></template> | ||||||
| 
 | 
 | ||||||
| 			<FormLink v-if="user.host" :to="`/instance-info/${user.host}`">{{ $ts.instanceInfo }}<template #suffix>{{ user.host }}</template></FormLink> |  | ||||||
| 			<FormKeyValueView v-else> |  | ||||||
| 				<template #key>{{ $ts.instanceInfo }}</template> |  | ||||||
| 				<template #value>(Local user)</template> |  | ||||||
| 			</FormKeyValueView> |  | ||||||
| 		</FormGroup> |  | ||||||
| 
 |  | ||||||
| 		<FormGroup> |  | ||||||
| 			<FormKeyValueView> | 			<FormKeyValueView> | ||||||
| 				<template #key>{{ $ts.updatedAt }}</template> | 				<template #key>ID</template> | ||||||
| 				<template #value><MkTime v-if="user.lastFetchedAt" mode="detail" :time="user.lastFetchedAt"/><span v-else>N/A</span></template> | 				<template #value><span class="_monospace">{{ user.id }}</span></template> | ||||||
| 			</FormKeyValueView> | 			</FormKeyValueView> | ||||||
| 		</FormGroup> |  | ||||||
| 
 | 
 | ||||||
| 		<FormObjectView tall :value="user"> | 			<FormGroup> | ||||||
| 			<span>Raw</span> | 				<FormLink :to="`/user-ap-info/${user.id}`">ActivityPub</FormLink> | ||||||
| 		</FormObjectView> | 
 | ||||||
| 	</FormGroup> | 				<FormLink v-if="user.host" :to="`/instance-info/${user.host}`">{{ $ts.instanceInfo }}<template #suffix>{{ user.host }}</template></FormLink> | ||||||
|  | 				<FormKeyValueView v-else> | ||||||
|  | 					<template #key>{{ $ts.instanceInfo }}</template> | ||||||
|  | 					<template #value>(Local user)</template> | ||||||
|  | 				</FormKeyValueView> | ||||||
|  | 			</FormGroup> | ||||||
|  | 
 | ||||||
|  | 			<FormGroup> | ||||||
|  | 				<FormKeyValueView> | ||||||
|  | 					<template #key>{{ $ts.updatedAt }}</template> | ||||||
|  | 					<template #value><MkTime v-if="user.lastFetchedAt" mode="detail" :time="user.lastFetchedAt"/><span v-else>N/A</span></template> | ||||||
|  | 				</FormKeyValueView> | ||||||
|  | 			</FormGroup> | ||||||
|  | 
 | ||||||
|  | 			<FormObjectView tall :value="user"> | ||||||
|  | 				<span>Raw</span> | ||||||
|  | 			</FormObjectView> | ||||||
|  | 		</FormGroup> | ||||||
|  | 	</FormSuspense> | ||||||
| </FormBase> | </FormBase> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
|  | @ -80,23 +82,27 @@ export default defineComponent({ | ||||||
| 					} | 					} | ||||||
| 				} : undefined].filter(x => x !== undefined) : [], | 				} : undefined].filter(x => x !== undefined) : [], | ||||||
| 			})), | 			})), | ||||||
|  | 			init: null, | ||||||
| 			user: null, | 			user: null, | ||||||
| 		} | 		} | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	mounted() { | 	watch: { | ||||||
| 		this.fetch(); | 		userId: { | ||||||
|  | 			handler() { | ||||||
|  | 				this.init = () => os.api('users/show', { | ||||||
|  | 					userId: this.userId | ||||||
|  | 				}).then(user => { | ||||||
|  | 					this.user = user; | ||||||
|  | 				}); | ||||||
|  | 			}, | ||||||
|  | 			immediate: true | ||||||
|  | 		} | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	methods: { | 	methods: { | ||||||
| 		number, | 		number, | ||||||
| 		bytes, | 		bytes, | ||||||
| 
 |  | ||||||
| 		async fetch() { |  | ||||||
| 			this.user = await os.api('users/show', { |  | ||||||
| 				userId: this.userId |  | ||||||
| 			}); |  | ||||||
| 		} |  | ||||||
| 	} | 	} | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|  | @ -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,9 @@ 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/user/:user', component: page('instance/user'), props: route => ({ userId: route.params.user }) }, | ||||||
|  | 		{ 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 }) }, | ||||||
|  |  | ||||||
|  | @ -124,7 +124,13 @@ export function getUserMenu(user) { | ||||||
| 		action: () => { | 		action: () => { | ||||||
| 			copyToClipboard(`@${user.username}@${user.host || host}`); | 			copyToClipboard(`@${user.username}@${user.host || host}`); | ||||||
| 		} | 		} | ||||||
| 	}, { | 	}, ($i && ($i.isAdmin || $i.isModerator)) ? { | ||||||
|  | 		icon: 'fas fa-info-circle', | ||||||
|  | 		text: i18n.locale.info, | ||||||
|  | 		action: () => { | ||||||
|  | 			os.pageWindow(`/instance/user/${user.id}`); | ||||||
|  | 		} | ||||||
|  | 	} : { | ||||||
| 		icon: 'fas fa-info-circle', | 		icon: 'fas fa-info-circle', | ||||||
| 		text: i18n.locale.info, | 		text: i18n.locale.info, | ||||||
| 		action: () => { | 		action: () => { | ||||||
|  |  | ||||||
							
								
								
									
										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(`/instance/user/${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(); | ||||||
|  | 	}); | ||||||
|  | } | ||||||
|  | @ -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> | ||||||
|  | @ -172,65 +172,6 @@ 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'); | ||||||
|  |  | ||||||
|  | @ -20,9 +20,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" :behavior="settingsWindowed ? 'modalWindow' : null"> | ||||||
| 		<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="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> | ||||||
|  | @ -156,65 +156,6 @@ 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'); | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue