parent
							
								
									ec75600e1c
								
							
						
					
					
						commit
						246693b848
					
				
					 50 changed files with 2588 additions and 1887 deletions
				
			
		|  | @ -183,7 +183,7 @@ clearQueueConfirmTitle: "キューをクリアしますか?" | |||
| clearQueueConfirmText: "未配達の投稿は配送されなくなります。通常この操作を行う必要はありません。" | ||||
| clearCachedFiles: "キャッシュをクリア" | ||||
| clearCachedFilesConfirm: "キャッシュされたリモートファイルをすべて削除しますか?" | ||||
| blockedInstances: "インスタンスブロック" | ||||
| blockedInstances: "ブロックしたインスタンス" | ||||
| blockedInstancesDescription: "ブロックしたいインスタンスのホストを改行で区切って設定します。ブロックされたインスタンスは、このインスタンスとやり取りできなくなります。" | ||||
| muteAndBlock: "ミュートとブロック" | ||||
| mutedUsers: "ミュートしたユーザー" | ||||
|  | @ -349,7 +349,6 @@ antennaExcludeKeywords: "除外キーワード" | |||
| antennaKeywordsDescription: "スペースで区切るとAND指定になり、改行で区切るとOR指定になります" | ||||
| notifyAntenna: "新しいノートを通知する" | ||||
| withFileAntenna: "ファイルが添付されたノートのみ" | ||||
| serviceworker: "ServiceWorker" | ||||
| enableServiceworker: "ServiceWorkerを有効にする" | ||||
| antennaUsersDescription: "ユーザー名を改行で区切って指定します" | ||||
| caseSensitive: "大文字小文字を区別する" | ||||
|  | @ -568,7 +567,7 @@ pluginTokenRequestedDescription: "このプラグインはここで設定した | |||
| notificationType: "通知の種類" | ||||
| edit: "編集" | ||||
| useStarForReactionFallback: "リアクション絵文字が不明な場合、代わりに★を使う" | ||||
| emailConfig: "メールサーバー設定" | ||||
| emailServer: "メールサーバー" | ||||
| enableEmail: "メール配信機能を有効化する" | ||||
| emailConfigInfo: "メールアドレスの確認やパスワードリセットの際に使います" | ||||
| email: "メール" | ||||
|  | @ -728,6 +727,14 @@ hideOnlineStatusDescription: "オンライン状態を隠すと、検索など | |||
| online: "オンライン" | ||||
| active: "アクティブ" | ||||
| offline: "オフライン" | ||||
| notRecommended: "非推奨" | ||||
| botProtection: "Bot防御" | ||||
| instanceBlocking: "インスタンスブロック" | ||||
| selectAccount: "アカウントを選択" | ||||
| enabled: "有効" | ||||
| disabled: "無効" | ||||
| quickAction: "クイックアクション" | ||||
| user: "ユーザー" | ||||
| 
 | ||||
| _email: | ||||
|   _follow: | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| { | ||||
| 	"name": "misskey", | ||||
| 	"author": "syuilo <syuilotan@yahoo.co.jp>", | ||||
| 	"version": "12.78.0-beta.2", | ||||
| 	"version": "12.78.0-beta.3", | ||||
| 	"codename": "indigo", | ||||
| 	"repository": { | ||||
| 		"type": "git", | ||||
|  |  | |||
|  | @ -18,7 +18,7 @@ type Captcha = { | |||
| 	getResponse(id: string): string; | ||||
| }; | ||||
| 
 | ||||
| type CaptchaProvider = 'hcaptcha' | 'grecaptcha'; | ||||
| type CaptchaProvider = 'hcaptcha' | 'recaptcha'; | ||||
| 
 | ||||
| type CaptchaContainer = { | ||||
| 	readonly [_ in CaptchaProvider]?: Captcha; | ||||
|  | @ -57,7 +57,7 @@ export default defineComponent({ | |||
| 		src() { | ||||
| 			const endpoint = ({ | ||||
| 				hcaptcha: 'https://hcaptcha.com/1', | ||||
| 				grecaptcha: 'https://www.recaptcha.net/recaptcha', | ||||
| 				recaptcha: 'https://www.recaptcha.net/recaptcha', | ||||
| 			} as Record<PropertyKey, unknown>)[this.provider]; | ||||
| 
 | ||||
| 			return `${typeof endpoint == 'string' ? endpoint : 'about:invalid'}/api.js?render=explicit`; | ||||
|  |  | |||
|  | @ -24,6 +24,8 @@ export default defineComponent({ | |||
| 	--formXPadding: 32px; | ||||
| 	--formYPadding: 32px; | ||||
| 
 | ||||
| 	--formContentHMargin: 16px; | ||||
| 
 | ||||
| 	font-size: 95%; | ||||
| 	line-height: 1.3em; | ||||
| 	background: var(--bg); | ||||
|  |  | |||
|  | @ -30,7 +30,7 @@ | |||
| 	top: var(--stickyTop, 0px); | ||||
| 	z-index: 2; | ||||
| 	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); | ||||
| 	-webkit-backdrop-filter: blur(10px); | ||||
| 	backdrop-filter: blur(10px); | ||||
|  | @ -42,7 +42,7 @@ | |||
| } | ||||
| 
 | ||||
| ._formCaption { | ||||
| 	padding: 8px 16px 0 16px; | ||||
| 	padding: 8px var(--formContentHMargin) 0 var(--formContentHMargin); | ||||
| } | ||||
| 
 | ||||
| ._formItem { | ||||
|  |  | |||
|  | @ -20,7 +20,7 @@ export default defineComponent({ | |||
| .anocepby { | ||||
| 	display: flex; | ||||
| 	align-items: center; | ||||
| 	padding: 14px 16px; | ||||
| 	padding: 14px var(--formContentHMargin); | ||||
| 
 | ||||
| 	> .key { | ||||
| 		margin-right: 12px; | ||||
|  |  | |||
|  | @ -75,7 +75,7 @@ export default defineComponent({ | |||
| 			max-width: 100%; | ||||
| 			min-height: 130px; | ||||
| 			margin: 0; | ||||
| 			padding: 16px; | ||||
| 			padding: 16px var(--formContentHMargin); | ||||
| 			box-sizing: border-box; | ||||
| 			font: inherit; | ||||
| 			font-weight: normal; | ||||
|  |  | |||
|  | @ -18,6 +18,9 @@ export default defineComponent({ | |||
| 		} | ||||
| 	}, | ||||
| 	watch: { | ||||
| 		modelValue() { | ||||
| 			this.value = this.modelValue; | ||||
| 		}, | ||||
| 		value() { | ||||
| 			this.$emit('update:modelValue', this.value); | ||||
| 		} | ||||
|  |  | |||
|  | @ -5,9 +5,9 @@ | |||
| 			<MkLoading/> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	<FormGroup v-else-if="resolved" class="_formItem"> | ||||
| 	<div v-else-if="resolved" class="_formItem"> | ||||
| 		<slot :result="result"></slot> | ||||
| 	</FormGroup> | ||||
| 	</div> | ||||
| 	<div class="_formItem" v-else> | ||||
| 		<div class="_formPanel"> | ||||
| 			error! | ||||
|  | @ -20,13 +20,8 @@ | |||
| <script lang="ts"> | ||||
| import { defineComponent, PropType, ref, watch } from 'vue'; | ||||
| import './form.scss'; | ||||
| import FormGroup from './group.vue'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
| 		FormGroup, | ||||
| 	}, | ||||
| 
 | ||||
| 	props: { | ||||
| 		p: { | ||||
| 			type: Function as PropType<() => Promise<any>>, | ||||
|  |  | |||
|  | @ -1,123 +1,35 @@ | |||
| <template> | ||||
| <div class="zbcjwnqg" v-size="{ max: [550, 1000] }"> | ||||
| 	<div class="stats" v-if="info"> | ||||
| 		<div class="_panel"> | ||||
| 			<div> | ||||
| 				<b><i class="fas fa-user"></i>{{ $ts.users }}</b> | ||||
| 				<small>{{ $ts.local }}</small> | ||||
| 			</div> | ||||
| 			<div> | ||||
| 				<dl class="total"> | ||||
| 					<dt>{{ $ts.total }}</dt> | ||||
| 					<dd>{{ number(info.originalUsersCount) }}</dd> | ||||
| 				</dl> | ||||
| 				<dl class="diff" :class="{ inc: usersLocalDoD > 0 }"> | ||||
| 					<dt>{{ $ts.dayOverDayChanges }}</dt> | ||||
| 					<dd>{{ number(usersLocalDoD) }}</dd> | ||||
| 				</dl> | ||||
| 				<dl class="diff" :class="{ inc: usersLocalWoW > 0 }"> | ||||
| 					<dt>{{ $ts.weekOverWeekChanges }}</dt> | ||||
| 					<dd>{{ number(usersLocalWoW) }}</dd> | ||||
| 				</dl> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 		<div class="_panel"> | ||||
| 			<div> | ||||
| 				<b><i class="fas fa-user"></i>{{ $ts.users }}</b> | ||||
| 				<small>{{ $ts.remote }}</small> | ||||
| 			</div> | ||||
| 			<div> | ||||
| 				<dl class="total"> | ||||
| 					<dt>{{ $ts.total }}</dt> | ||||
| 					<dd>{{ number((info.usersCount - info.originalUsersCount)) }}</dd> | ||||
| 				</dl> | ||||
| 				<dl class="diff" :class="{ inc: usersRemoteDoD > 0 }"> | ||||
| 					<dt>{{ $ts.dayOverDayChanges }}</dt> | ||||
| 					<dd>{{ number(usersRemoteDoD) }}</dd> | ||||
| 				</dl> | ||||
| 				<dl class="diff" :class="{ inc: usersRemoteWoW > 0 }"> | ||||
| 					<dt>{{ $ts.weekOverWeekChanges }}</dt> | ||||
| 					<dd>{{ number(usersRemoteWoW) }}</dd> | ||||
| 				</dl> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 		<div class="_panel"> | ||||
| 			<div> | ||||
| 				<b><i class="fas fa-pencil-alt"></i>{{ $ts.notes }}</b> | ||||
| 				<small>{{ $ts.local }}</small> | ||||
| 			</div> | ||||
| 			<div> | ||||
| 				<dl class="total"> | ||||
| 					<dt>{{ $ts.total }}</dt> | ||||
| 					<dd>{{ number(info.originalNotesCount) }}</dd> | ||||
| 				</dl> | ||||
| 				<dl class="diff" :class="{ inc: notesLocalDoD > 0 }"> | ||||
| 					<dt>{{ $ts.dayOverDayChanges }}</dt> | ||||
| 					<dd>{{ number(notesLocalDoD) }}</dd> | ||||
| 				</dl> | ||||
| 				<dl class="diff" :class="{ inc: notesLocalWoW > 0 }"> | ||||
| 					<dt>{{ $ts.weekOverWeekChanges }}</dt> | ||||
| 					<dd>{{ number(notesLocalWoW) }}</dd> | ||||
| 				</dl> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 		<div class="_panel"> | ||||
| 			<div> | ||||
| 				<b><i class="fas fa-pencil-alt"></i>{{ $ts.notes }}</b> | ||||
| 				<small>{{ $ts.remote }}</small> | ||||
| 			</div> | ||||
| 			<div> | ||||
| 				<dl class="total"> | ||||
| 					<dt>{{ $ts.total }}</dt> | ||||
| 					<dd>{{ number((info.notesCount - info.originalNotesCount)) }}</dd> | ||||
| 				</dl> | ||||
| 				<dl class="diff" :class="{ inc: notesRemoteDoD > 0 }"> | ||||
| 					<dt>{{ $ts.dayOverDayChanges }}</dt> | ||||
| 					<dd>{{ number(notesRemoteDoD) }}</dd> | ||||
| 				</dl> | ||||
| 				<dl class="diff" :class="{ inc: notesRemoteWoW > 0 }"> | ||||
| 					<dt>{{ $ts.weekOverWeekChanges }}</dt> | ||||
| 					<dd>{{ number(notesRemoteWoW) }}</dd> | ||||
| 				</dl> | ||||
| 			</div> | ||||
| 		</div> | ||||
| <div class="zbcjwnqg" 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> | ||||
| 
 | ||||
| 	<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> | ||||
| 	<canvas ref="chart"></canvas> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
|  | @ -158,7 +70,6 @@ export default defineComponent({ | |||
| 
 | ||||
| 	data() { | ||||
| 		return { | ||||
| 			info: null, | ||||
| 			notesLocalWoW: 0, | ||||
| 			notesLocalDoD: 0, | ||||
| 			notesRemoteWoW: 0, | ||||
|  | @ -216,8 +127,6 @@ export default defineComponent({ | |||
| 	}, | ||||
| 
 | ||||
| 	async created() { | ||||
| 		this.info = await os.api('stats'); | ||||
| 
 | ||||
| 		this.now = new Date(); | ||||
| 
 | ||||
| 		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.renderChart(); | ||||
|  | @ -300,10 +200,10 @@ export default defineComponent({ | |||
| 					aspectRatio: 2.5, | ||||
| 					layout: { | ||||
| 						padding: { | ||||
| 							left: 0, | ||||
| 							right: 0, | ||||
| 							left: 16, | ||||
| 							right: 16, | ||||
| 							top: 16, | ||||
| 							bottom: 0 | ||||
| 							bottom: 8 | ||||
| 						} | ||||
| 					}, | ||||
| 					legend: { | ||||
|  | @ -630,90 +530,8 @@ export default defineComponent({ | |||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| .zbcjwnqg { | ||||
| 	&.max-width_1000px { | ||||
| 		> .stats { | ||||
| 			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: "+"; | ||||
| 								} | ||||
| 							} | ||||
| 						} | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	> .selects { | ||||
| 		padding: 8px 16px 0 16px; | ||||
| 	} | ||||
| } | ||||
| </style> | ||||
|  |  | |||
|  | @ -45,7 +45,7 @@ | |||
| 			</I18n> | ||||
| 		</label> | ||||
| 		<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> | ||||
| 	</template> | ||||
| </form> | ||||
|  |  | |||
|  | @ -29,6 +29,7 @@ export default defineComponent({ | |||
| <style lang="scss"> | ||||
| .pxhvhrfw { | ||||
| 	display: flex; | ||||
| 	font-size: 90%; | ||||
| 
 | ||||
| 	> button { | ||||
| 		flex: 1; | ||||
|  |  | |||
|  | @ -1,16 +1,23 @@ | |||
| <template> | ||||
| <div class="cxiknjgy"> | ||||
| 	<slot :items="items"></slot> | ||||
| 	<div class="empty" v-if="empty" key="_empty_"> | ||||
| <transition name="fade" mode="out-in"> | ||||
| 	<MkLoading v-if="fetching"/> | ||||
| 
 | ||||
| 	<MkError v-else-if="error" @retry="init()"/> | ||||
| 
 | ||||
| 	<div class="empty" v-else-if="empty" key="_empty_"> | ||||
| 		<slot name="empty"></slot> | ||||
| 	</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> | ||||
| 			<template v-if="!moreFetching">{{ $ts.loadMore }}</template> | ||||
| 			<template v-if="moreFetching"><MkLoading inline/></template> | ||||
| 		</MkButton> | ||||
| 
 | ||||
| 	<div v-else class="cxiknjgy"> | ||||
| 		<slot :items="items"></slot> | ||||
| 		<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> | ||||
| 				<template v-if="!moreFetching">{{ $ts.loadMore }}</template> | ||||
| 				<template v-if="moreFetching"><MkLoading inline/></template> | ||||
| 			</MkButton> | ||||
| 		</div> | ||||
| 	</div> | ||||
| </div> | ||||
| </transition> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
|  | @ -36,6 +43,15 @@ export default defineComponent({ | |||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| .fade-enter-active, | ||||
| .fade-leave-active { | ||||
| 	transition: opacity 0.125s ease; | ||||
| } | ||||
| .fade-enter-from, | ||||
| .fade-leave-to { | ||||
| 	opacity: 0; | ||||
| } | ||||
| 
 | ||||
| .cxiknjgy { | ||||
| 	> .more > .button { | ||||
| 		margin-left: auto; | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| <template> | ||||
| <div class=""> | ||||
| <div class="lcixvhis"> | ||||
| 	<div class="_section reports"> | ||||
| 		<div class="_content"> | ||||
| 			<div class="inputs" style="display: flex;"> | ||||
|  | @ -80,6 +80,8 @@ export default defineComponent({ | |||
| 		MkPagination, | ||||
| 	}, | ||||
| 
 | ||||
| 	emits: ['info'], | ||||
| 
 | ||||
| 	data() { | ||||
| 		return { | ||||
| 			[symbols.PAGE_INFO]: { | ||||
|  | @ -117,6 +119,10 @@ export default defineComponent({ | |||
| 		}, | ||||
| 	}, | ||||
| 
 | ||||
| 	mounted() { | ||||
| 		this.$emit('info', this[symbols.PAGE_INFO]); | ||||
| 	}, | ||||
| 
 | ||||
| 	methods: { | ||||
| 		acct, | ||||
| 
 | ||||
|  | @ -132,6 +138,10 @@ export default defineComponent({ | |||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| .lcixvhis { | ||||
| 	margin: var(--margin); | ||||
| } | ||||
| 
 | ||||
| .bcekxzvu { | ||||
| 	> .target { | ||||
| 		display: flex; | ||||
|  |  | |||
|  | @ -1,28 +1,24 @@ | |||
| <template> | ||||
| <div class="ztgjmzrw"> | ||||
| 	<div class="_section"> | ||||
| 		<div class="_content"> | ||||
| 			<MkButton @click="add()" primary style="margin: 0 auto 16px auto;"><i class="fas fa-plus"></i> {{ $ts.add }}</MkButton> | ||||
| 			<section class="_card _gap announcements" v-for="announcement in announcements"> | ||||
| 				<div class="_content announcement"> | ||||
| 					<MkInput v-model:value="announcement.title"> | ||||
| 						<span>{{ $ts.title }}</span> | ||||
| 					</MkInput> | ||||
| 					<MkTextarea v-model:value="announcement.text"> | ||||
| 						<span>{{ $ts.text }}</span> | ||||
| 					</MkTextarea> | ||||
| 					<MkInput v-model:value="announcement.imageUrl"> | ||||
| 						<span>{{ $ts.imageUrl }}</span> | ||||
| 					</MkInput> | ||||
| 					<p v-if="announcement.reads">{{ $t('nUsersRead', { n: announcement.reads }) }}</p> | ||||
| 					<div class="buttons"> | ||||
| 						<MkButton class="button" inline @click="save(announcement)" primary><i class="fas fa-save"></i> {{ $ts.save }}</MkButton> | ||||
| 						<MkButton class="button" inline @click="remove(announcement)"><i class="fas fa-trash-alt"></i> {{ $ts.remove }}</MkButton> | ||||
| 					</div> | ||||
| 				</div> | ||||
| 			</section> | ||||
| 	<MkButton @click="add()" primary style="margin: 0 auto 16px auto;"><i class="fas fa-plus"></i> {{ $ts.add }}</MkButton> | ||||
| 	<section class="_card _gap announcements" v-for="announcement in announcements"> | ||||
| 		<div class="_content announcement"> | ||||
| 			<MkInput v-model:value="announcement.title"> | ||||
| 				<span>{{ $ts.title }}</span> | ||||
| 			</MkInput> | ||||
| 			<MkTextarea v-model:value="announcement.text"> | ||||
| 				<span>{{ $ts.text }}</span> | ||||
| 			</MkTextarea> | ||||
| 			<MkInput v-model:value="announcement.imageUrl"> | ||||
| 				<span>{{ $ts.imageUrl }}</span> | ||||
| 			</MkInput> | ||||
| 			<p v-if="announcement.reads">{{ $t('nUsersRead', { n: announcement.reads }) }}</p> | ||||
| 			<div class="buttons"> | ||||
| 				<MkButton class="button" inline @click="save(announcement)" primary><i class="fas fa-save"></i> {{ $ts.save }}</MkButton> | ||||
| 				<MkButton class="button" inline @click="remove(announcement)"><i class="fas fa-trash-alt"></i> {{ $ts.remove }}</MkButton> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	</section> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
|  | @ -41,6 +37,8 @@ export default defineComponent({ | |||
| 		MkTextarea, | ||||
| 	}, | ||||
| 
 | ||||
| 	emits: ['info'], | ||||
| 
 | ||||
| 	data() { | ||||
| 		return { | ||||
| 			[symbols.PAGE_INFO]: { | ||||
|  | @ -57,6 +55,10 @@ export default defineComponent({ | |||
| 		}); | ||||
| 	}, | ||||
| 
 | ||||
| 	mounted() { | ||||
| 		this.$emit('info', this[symbols.PAGE_INFO]); | ||||
| 	}, | ||||
| 
 | ||||
| 	methods: { | ||||
| 		add() { | ||||
| 			this.announcements.unshift({ | ||||
|  | @ -109,3 +111,9 @@ export default defineComponent({ | |||
| 	} | ||||
| }); | ||||
| </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> | ||||
| <div class="mk-instance-emojis"> | ||||
| 	<div class="_section" style="padding: 0;"> | ||||
| 		<MkTab v-model:value="tab"> | ||||
| 			<option value="local">{{ $ts.local }}</option> | ||||
| 			<option value="remote">{{ $ts.remote }}</option> | ||||
| 		</MkTab> | ||||
| <div class="ogwlenmc"> | ||||
| 	<MkTab v-model:value="tab"> | ||||
| 		<option value="local">{{ $ts.local }}</option> | ||||
| 		<option value="remote">{{ $ts.remote }}</option> | ||||
| 	</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 class="_section"> | ||||
| 		<div class="local" v-if="tab === 'local'"> | ||||
| 			<MkButton primary @click="add" style="margin: 0 auto var(--margin) auto;"><i class="fas fa-plus"></i> {{ $ts.addEmoji }}</MkButton> | ||||
| 			<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="pagination" ref="emojis"> | ||||
| 				<template #empty><span>{{ $ts.noCustomEmojis }}</span></template> | ||||
| 				<template #default="{items}"> | ||||
| 					<div class="emojis"> | ||||
| 						<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">{{ emoji.name }}</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 class="remote" v-else-if="tab === 'remote'"> | ||||
| 		<MkInput v-model:value="queryRemote" :debounce="true" type="search" style="margin: var(--margin);"><template #icon><i class="fas fa-search"></i></template><span>{{ $ts.search }}</span></MkInput> | ||||
| 		<MkInput v-model:value="host" :debounce="true" style="margin: var(--margin);"><span>{{ $ts.host }}</span></MkInput> | ||||
| 		<MkPagination :pagination="remotePagination" ref="remoteEmojis"> | ||||
| 			<template #empty><span>{{ $ts.noCustomEmojis }}</span></template> | ||||
| 			<template #default="{items}"> | ||||
| 				<div class="ldhfsamy"> | ||||
| 					<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 _monospace">{{ emoji.name }}</div> | ||||
| 							<div class="info">{{ emoji.host }}</div> | ||||
| 						</div> | ||||
| 					</div> | ||||
| 				</template> | ||||
| 			</MkPagination> | ||||
| 		</div> | ||||
| 				</div> | ||||
| 			</template> | ||||
| 		</MkPagination> | ||||
| 	</div> | ||||
| </div> | ||||
| </template> | ||||
|  | @ -67,6 +63,8 @@ export default defineComponent({ | |||
| 		MkPagination, | ||||
| 	}, | ||||
| 
 | ||||
| 	emits: ['info'], | ||||
| 
 | ||||
| 	data() { | ||||
| 		return { | ||||
| 			[symbols.PAGE_INFO]: { | ||||
|  | @ -99,6 +97,10 @@ export default defineComponent({ | |||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	async mounted() { | ||||
| 		this.$emit('info', this[symbols.PAGE_INFO]); | ||||
| 	}, | ||||
| 
 | ||||
| 	methods: { | ||||
| 		async add(e) { | ||||
| 			const files = await selectFile(e.currentTarget || e.target, null, true); | ||||
|  | @ -150,85 +152,86 @@ export default defineComponent({ | |||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| .mk-instance-emojis { | ||||
| 	> ._section { | ||||
| 		> .local { | ||||
| 			.emojis { | ||||
| 				display: grid; | ||||
| 				grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); | ||||
| 				grid-gap: var(--margin); | ||||
| 		 | ||||
| 				> .emoji { | ||||
| 					display: flex; | ||||
| 					align-items: center; | ||||
| 					padding: 12px; | ||||
| 					text-align: left; | ||||
| .ogwlenmc { | ||||
| 	> .local { | ||||
| 		.ldhfsamy { | ||||
| 			display: grid; | ||||
| 			grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); | ||||
| 			grid-gap: 12px; | ||||
| 			margin: var(--margin); | ||||
| 	 | ||||
| 			> .emoji { | ||||
| 				display: flex; | ||||
| 				align-items: center; | ||||
| 				padding: 12px; | ||||
| 				text-align: left; | ||||
| 
 | ||||
| 					&:hover { | ||||
| 						color: var(--accent); | ||||
| 					} | ||||
| 				&:hover { | ||||
| 					color: var(--accent); | ||||
| 				} | ||||
| 
 | ||||
| 					> .img { | ||||
| 						width: 42px; | ||||
| 						height: 42px; | ||||
| 					} | ||||
| 				> .img { | ||||
| 					width: 42px; | ||||
| 					height: 42px; | ||||
| 				} | ||||
| 
 | ||||
| 					> .body { | ||||
| 						padding: 0 0 0 8px; | ||||
| 						white-space: nowrap; | ||||
| 				> .body { | ||||
| 					padding: 0 0 0 8px; | ||||
| 					white-space: nowrap; | ||||
| 					overflow: hidden; | ||||
| 
 | ||||
| 					> .name { | ||||
| 						text-overflow: ellipsis; | ||||
| 						overflow: hidden; | ||||
| 					} | ||||
| 
 | ||||
| 						> .name { | ||||
| 							text-overflow: ellipsis; | ||||
| 							overflow: hidden; | ||||
| 						} | ||||
| 
 | ||||
| 						> .info { | ||||
| 							opacity: 0.5; | ||||
| 							text-overflow: ellipsis; | ||||
| 							overflow: hidden; | ||||
| 						} | ||||
| 					> .info { | ||||
| 						opacity: 0.5; | ||||
| 						text-overflow: ellipsis; | ||||
| 						overflow: hidden; | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 		> .remote { | ||||
| 			.emojis { | ||||
| 				display: grid; | ||||
| 				grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); | ||||
| 				grid-gap: var(--margin); | ||||
| 	> .remote { | ||||
| 		.ldhfsamy { | ||||
| 			display: grid; | ||||
| 			grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); | ||||
| 			grid-gap: 12px; | ||||
| 			margin: var(--margin); | ||||
| 
 | ||||
| 				> .emoji { | ||||
| 					display: flex; | ||||
| 					align-items: center; | ||||
| 					padding: 12px; | ||||
| 					text-align: left; | ||||
| 			> .emoji { | ||||
| 				display: flex; | ||||
| 				align-items: center; | ||||
| 				padding: 12px; | ||||
| 				text-align: left; | ||||
| 
 | ||||
| 					&:hover { | ||||
| 						color: var(--accent); | ||||
| 					} | ||||
| 				&:hover { | ||||
| 					color: var(--accent); | ||||
| 				} | ||||
| 
 | ||||
| 					> .img { | ||||
| 						width: 32px; | ||||
| 						height: 32px; | ||||
| 					} | ||||
| 				> .img { | ||||
| 					width: 32px; | ||||
| 					height: 32px; | ||||
| 				} | ||||
| 
 | ||||
| 					> .body { | ||||
| 						padding: 0 0 0 8px; | ||||
| 						white-space: nowrap; | ||||
| 				> .body { | ||||
| 					padding: 0 0 0 8px; | ||||
| 					white-space: nowrap; | ||||
| 					overflow: hidden; | ||||
| 
 | ||||
| 					> .name { | ||||
| 						text-overflow: ellipsis; | ||||
| 						overflow: hidden; | ||||
| 					} | ||||
| 
 | ||||
| 						> .name { | ||||
| 							text-overflow: ellipsis; | ||||
| 							overflow: hidden; | ||||
| 						} | ||||
| 
 | ||||
| 						> .info { | ||||
| 							opacity: 0.5; | ||||
| 							text-overflow: ellipsis; | ||||
| 							overflow: hidden; | ||||
| 						} | ||||
| 					> .info { | ||||
| 						opacity: 0.5; | ||||
| 						font-size: 90%; | ||||
| 						text-overflow: ellipsis; | ||||
| 						overflow: hidden; | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
|  |  | |||
|  | @ -1,60 +1,55 @@ | |||
| <template> | ||||
| <div> | ||||
| 	<div class="_section"> | ||||
| 		<div class="_content"> | ||||
| 			<MkInput v-model:value="host" :debounce="true"><span>{{ $ts.host }}</span></MkInput> | ||||
| 			<div class="inputs" style="display: flex;"> | ||||
| 				<MkSelect v-model:value="state" style="margin: 0; flex: 1;"> | ||||
| 					<template #label>{{ $ts.state }}</template> | ||||
| 					<option value="all">{{ $ts.all }}</option> | ||||
| 					<option value="federating">{{ $ts.federating }}</option> | ||||
| 					<option value="subscribing">{{ $ts.subscribing }}</option> | ||||
| 					<option value="publishing">{{ $ts.publishing }}</option> | ||||
| 					<option value="suspended">{{ $ts.suspended }}</option> | ||||
| 					<option value="blocked">{{ $ts.blocked }}</option> | ||||
| 					<option value="notResponding">{{ $ts.notResponding }}</option> | ||||
| 				</MkSelect> | ||||
| 				<MkSelect v-model:value="sort" style="margin: 0; flex: 1;"> | ||||
| 					<template #label>{{ $ts.sort }}</template> | ||||
| 					<option value="+pubSub">{{ $ts.pubSub }} ({{ $ts.descendingOrder }})</option> | ||||
| 					<option value="-pubSub">{{ $ts.pubSub }} ({{ $ts.ascendingOrder }})</option> | ||||
| 					<option value="+notes">{{ $ts.notes }} ({{ $ts.descendingOrder }})</option> | ||||
| 					<option value="-notes">{{ $ts.notes }} ({{ $ts.ascendingOrder }})</option> | ||||
| 					<option value="+users">{{ $ts.users }} ({{ $ts.descendingOrder }})</option> | ||||
| 					<option value="-users">{{ $ts.users }} ({{ $ts.ascendingOrder }})</option> | ||||
| 					<option value="+following">{{ $ts.following }} ({{ $ts.descendingOrder }})</option> | ||||
| 					<option value="-following">{{ $ts.following }} ({{ $ts.ascendingOrder }})</option> | ||||
| 					<option value="+followers">{{ $ts.followers }} ({{ $ts.descendingOrder }})</option> | ||||
| 					<option value="-followers">{{ $ts.followers }} ({{ $ts.ascendingOrder }})</option> | ||||
| 					<option value="+caughtAt">{{ $ts.caughtAt }} ({{ $ts.descendingOrder }})</option> | ||||
| 					<option value="-caughtAt">{{ $ts.caughtAt }} ({{ $ts.ascendingOrder }})</option> | ||||
| 					<option value="+lastCommunicatedAt">{{ $ts.lastCommunicatedAt }} ({{ $ts.descendingOrder }})</option> | ||||
| 					<option value="-lastCommunicatedAt">{{ $ts.lastCommunicatedAt }} ({{ $ts.ascendingOrder }})</option> | ||||
| 					<option value="+driveUsage">{{ $ts.driveUsage }} ({{ $ts.descendingOrder }})</option> | ||||
| 					<option value="-driveUsage">{{ $ts.driveUsage }} ({{ $ts.ascendingOrder }})</option> | ||||
| 					<option value="+driveFiles">{{ $ts.driveFiles }} ({{ $ts.descendingOrder }})</option> | ||||
| 					<option value="-driveFiles">{{ $ts.driveFiles }} ({{ $ts.ascendingOrder }})</option> | ||||
| 				</MkSelect> | ||||
| <div class="enuoauvw"> | ||||
| 	<div class="query"> | ||||
| 		<MkInput v-model:value="host" :debounce="true"><span>{{ $ts.host }}</span></MkInput> | ||||
| 		<div class="inputs" style="display: flex;"> | ||||
| 			<MkSelect v-model:value="state" style="margin: 0; flex: 1;"> | ||||
| 				<template #label>{{ $ts.state }}</template> | ||||
| 				<option value="all">{{ $ts.all }}</option> | ||||
| 				<option value="federating">{{ $ts.federating }}</option> | ||||
| 				<option value="subscribing">{{ $ts.subscribing }}</option> | ||||
| 				<option value="publishing">{{ $ts.publishing }}</option> | ||||
| 				<option value="suspended">{{ $ts.suspended }}</option> | ||||
| 				<option value="blocked">{{ $ts.blocked }}</option> | ||||
| 				<option value="notResponding">{{ $ts.notResponding }}</option> | ||||
| 			</MkSelect> | ||||
| 			<MkSelect v-model:value="sort" style="margin: 0; flex: 1;"> | ||||
| 				<template #label>{{ $ts.sort }}</template> | ||||
| 				<option value="+pubSub">{{ $ts.pubSub }} ({{ $ts.descendingOrder }})</option> | ||||
| 				<option value="-pubSub">{{ $ts.pubSub }} ({{ $ts.ascendingOrder }})</option> | ||||
| 				<option value="+notes">{{ $ts.notes }} ({{ $ts.descendingOrder }})</option> | ||||
| 				<option value="-notes">{{ $ts.notes }} ({{ $ts.ascendingOrder }})</option> | ||||
| 				<option value="+users">{{ $ts.users }} ({{ $ts.descendingOrder }})</option> | ||||
| 				<option value="-users">{{ $ts.users }} ({{ $ts.ascendingOrder }})</option> | ||||
| 				<option value="+following">{{ $ts.following }} ({{ $ts.descendingOrder }})</option> | ||||
| 				<option value="-following">{{ $ts.following }} ({{ $ts.ascendingOrder }})</option> | ||||
| 				<option value="+followers">{{ $ts.followers }} ({{ $ts.descendingOrder }})</option> | ||||
| 				<option value="-followers">{{ $ts.followers }} ({{ $ts.ascendingOrder }})</option> | ||||
| 				<option value="+caughtAt">{{ $ts.caughtAt }} ({{ $ts.descendingOrder }})</option> | ||||
| 				<option value="-caughtAt">{{ $ts.caughtAt }} ({{ $ts.ascendingOrder }})</option> | ||||
| 				<option value="+lastCommunicatedAt">{{ $ts.lastCommunicatedAt }} ({{ $ts.descendingOrder }})</option> | ||||
| 				<option value="-lastCommunicatedAt">{{ $ts.lastCommunicatedAt }} ({{ $ts.ascendingOrder }})</option> | ||||
| 				<option value="+driveUsage">{{ $ts.driveUsage }} ({{ $ts.descendingOrder }})</option> | ||||
| 				<option value="-driveUsage">{{ $ts.driveUsage }} ({{ $ts.ascendingOrder }})</option> | ||||
| 				<option value="+driveFiles">{{ $ts.driveFiles }} ({{ $ts.descendingOrder }})</option> | ||||
| 				<option value="-driveFiles">{{ $ts.driveFiles }} ({{ $ts.ascendingOrder }})</option> | ||||
| 			</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 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> | ||||
| 	</MkPagination> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
|  | @ -76,6 +71,8 @@ export default defineComponent({ | |||
| 		MkPagination, | ||||
| 	}, | ||||
| 
 | ||||
| 	emits: ['info'], | ||||
| 
 | ||||
| 	data() { | ||||
| 		return { | ||||
| 			[symbols.PAGE_INFO]: { | ||||
|  | @ -114,6 +111,10 @@ export default defineComponent({ | |||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	mounted() { | ||||
| 		this.$emit('info', this[symbols.PAGE_INFO]); | ||||
| 	}, | ||||
| 
 | ||||
| 	methods: { | ||||
| 		getStatus(instance) { | ||||
| 			if (instance.isSuspended) return 'off'; | ||||
|  | @ -131,6 +132,12 @@ export default defineComponent({ | |||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| .enuoauvw { | ||||
| 	> .query { | ||||
| 		margin: var(--margin); | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| .ppgwaixt { | ||||
| 	cursor: pointer; | ||||
| 	padding: 16px; | ||||
|  |  | |||
|  | @ -82,9 +82,7 @@ export default defineComponent({ | |||
| 		}, | ||||
| 
 | ||||
| 		showUser() { | ||||
| 			os.popup(import('./user-dialog.vue'), { | ||||
| 				userId: this.file.userId | ||||
| 			}, {}, 'closed'); | ||||
| 			os.pageWindow(`/instance/user/${this.file.userId}`); | ||||
| 		}, | ||||
| 
 | ||||
| 		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, | ||||
| 	}, | ||||
| 
 | ||||
| 	emits: ['info'], | ||||
| 
 | ||||
| 	data() { | ||||
| 		return { | ||||
| 			[symbols.PAGE_INFO]: { | ||||
|  | @ -114,6 +116,10 @@ export default defineComponent({ | |||
| 		}, | ||||
| 	}, | ||||
| 
 | ||||
| 	mounted() { | ||||
| 		this.$emit('info', this[symbols.PAGE_INFO]); | ||||
| 	}, | ||||
| 
 | ||||
| 	methods: { | ||||
| 		clear() { | ||||
| 			os.dialog({ | ||||
|  | @ -153,6 +159,8 @@ export default defineComponent({ | |||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| .xrmjdkdw { | ||||
| 	margin: var(--margin); | ||||
| 
 | ||||
| 	.urempief { | ||||
| 		margin-top: var(--margin); | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,171 +1,239 @@ | |||
| <template> | ||||
| <div v-if="meta" v-show="page === 'index'" class="xhexznfu _section"> | ||||
| 	<MkFolder> | ||||
| 		<template #header><i class="fas fa-tachometer-alt"></i> {{ $ts.overview }}</template> | ||||
| 
 | ||||
| 		<div class="sboqnrfi" :style="{ gridTemplateRows: overviewHeight }"> | ||||
| 			<MkInstanceStats :chart-limit="300" :detailed="true" class="_gap" ref="stats"/> | ||||
| 
 | ||||
| 			<MkContainer :foldable="true" class="_gap"> | ||||
| 				<template #header><i class="fas fa-info-circle"></i>{{ $ts.instanceInfo }}</template> | ||||
| 
 | ||||
| 				<div class="_content"> | ||||
| 					<div class="_keyValue"><b>Misskey</b><span>v{{ version }}</span></div> | ||||
| <div class="hiyeyicy" :class="{ wide: !narrow }" ref="el"> | ||||
| 	<div class="nav" v-if="!narrow || page == null"> | ||||
| 		<FormBase> | ||||
| 			<FormGroup> | ||||
| 				<div class="_formItem"> | ||||
| 					<div class="_formPanel lxpfedzu"> | ||||
| 						<img :src="$instance.iconUrl || '/favicon.ico'" alt="" class="icon"/> | ||||
| 					</div> | ||||
| 				</div> | ||||
| 				<div class="_content" v-if="serverInfo"> | ||||
| 					<div class="_keyValue"><b>Node.js</b><span>{{ serverInfo.node }}</span></div> | ||||
| 					<div class="_keyValue"><b>PostgreSQL</b><span>v{{ serverInfo.psql }}</span></div> | ||||
| 					<div class="_keyValue"><b>Redis</b><span>v{{ serverInfo.redis }}</span></div> | ||||
| 				</div> | ||||
| 			</MkContainer> | ||||
| 			 | ||||
| 			<MkContainer :foldable="true" :scrollable="true" class="_gap" style="height: 300px;"> | ||||
| 				<template #header><i class="fas fa-database"></i>{{ $ts.database }}</template> | ||||
| 
 | ||||
| 				<div class="_content" v-if="dbInfo"> | ||||
| 					<table style="border-collapse: collapse; width: 100%;"> | ||||
| 						<tr style="opacity: 0.7;"> | ||||
| 							<th style="text-align: left; padding: 0 8px 8px 0;">Table</th> | ||||
| 							<th style="text-align: left; padding: 0 8px 8px 0;">Records</th> | ||||
| 							<th style="text-align: left; padding: 0 0 8px 0;">Size</th> | ||||
| 						</tr> | ||||
| 						<tr v-for="table in dbInfo" :key="table[0]"> | ||||
| 							<th style="text-align: left; padding: 0 8px 0 0; word-break: break-all;">{{ table[0] }}</th> | ||||
| 							<td style="padding: 0 8px 0 0;">{{ number(table[1].count) }}</td> | ||||
| 							<td style="padding: 0; opacity: 0.7;">{{ bytes(table[1].size) }}</td> | ||||
| 						</tr> | ||||
| 					</table> | ||||
| 				</div> | ||||
| 			</MkContainer> | ||||
| 		</div> | ||||
| 	</MkFolder> | ||||
| </div> | ||||
| <div v-if="page === 'logs'" class="_section"> | ||||
| 	<MkFolder> | ||||
| 		<template #header><i class="fas fa-stream"></i> {{ $ts.logs }}</template> | ||||
| 
 | ||||
| 		<div class="_keyValue" v-for="log in modLogs"> | ||||
| 			<b>{{ log.type }}</b><span>by {{ log.user.username }}</span><MkTime :time="log.createdAt" style="opacity: 0.7;"/> | ||||
| 		</div> | ||||
| 	</MkFolder> | ||||
| </div> | ||||
| <div v-if="page === 'metrics'"> | ||||
| 	<XMetrics/> | ||||
| 				<FormLink :active="page === 'overview'" replace to="/instance/overview"><template #icon><i class="fas fa-tachometer-alt"></i></template>{{ $ts.overview }}</FormLink> | ||||
| 			</FormGroup> | ||||
| 			<FormGroup> | ||||
| 				<template #label>{{ $ts.quickAction }}</template> | ||||
| 				<FormButton @click="lookup"><i class="fas fa-search"></i> {{ $ts.lookup }}</FormButton> | ||||
| 				<FormButton v-if="$instance.disableRegistration" @click="invite"><i class="fas fa-user"></i> {{ $ts.invite }}</FormButton> | ||||
| 			</FormGroup> | ||||
| 			<FormGroup> | ||||
| 				<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> | ||||
| 				<FormLink :active="page === 'federation'" replace to="/instance/federation"><template #icon><i class="fas fa-globe"></i></template>{{ $ts.federation }}</FormLink> | ||||
| 				<FormLink :active="page === 'queue'" replace to="/instance/queue"><template #icon><i class="fas fa-clipboard-list"></i></template>{{ $ts.jobQueue }}</FormLink> | ||||
| 				<FormLink :active="page === 'files'" replace to="/instance/files"><template #icon><i class="fas fa-cloud"></i></template>{{ $ts.files }}</FormLink> | ||||
| 				<FormLink :active="page === 'announcements'" replace to="/instance/announcements"><template #icon><i class="fas fa-broadcast-tower"></i></template>{{ $ts.announcements }}</FormLink> | ||||
| 				<FormLink :active="page === 'database'" replace to="/instance/database"><template #icon><i class="fas fa-database"></i></template>{{ $ts.database }}</FormLink> | ||||
| 				<FormLink :active="page === 'abuses'" replace to="/instance/abuses"><template #icon><i class="fas fa-exclamation-circle"></i></template>{{ $ts.abuseReports }}</FormLink> | ||||
| 			</FormGroup> | ||||
| 			<FormGroup> | ||||
| 				<template #label>{{ $ts.settings }}</template> | ||||
| 				<FormLink :active="page === 'settings'" replace to="/instance/settings"><template #icon><i class="fas fa-cog"></i></template>{{ $ts.general }}</FormLink> | ||||
| 				<FormLink :active="page === 'files-settings'" replace to="/instance/files-settings"><template #icon><i class="fas fa-cloud"></i></template>{{ $ts.files }}</FormLink> | ||||
| 				<FormLink :active="page === 'email-settings'" replace to="/instance/email-settings"><template #icon><i class="fas fa-envelope"></i></template>{{ $ts.emailServer }}</FormLink> | ||||
| 				<FormLink :active="page === 'object-storage'" replace to="/instance/object-storage"><template #icon><i class="fas fa-cloud"></i></template>{{ $ts.objectStorage }}</FormLink> | ||||
| 				<FormLink :active="page === 'security'" replace to="/instance/security"><template #icon><i class="fas fa-lock"></i></template>{{ $ts.security }}</FormLink> | ||||
| 				<FormLink :active="page === 'service-worker'" replace to="/instance/service-worker"><template #icon><i class="fas fa-bolt"></i></template>ServiceWorker</FormLink> | ||||
| 				<FormLink :active="page === 'relays'" replace to="/instance/relays"><template #icon><i class="fas fa-globe"></i></template>{{ $ts.relays }}</FormLink> | ||||
| 				<FormLink :active="page === 'integrations'" replace to="/instance/integrations"><template #icon><i class="fas fa-share-alt"></i></template>{{ $ts.integration }}</FormLink> | ||||
| 				<FormLink :active="page === 'instance-block'" replace to="/instance/instance-block"><template #icon><i class="fas fa-ban"></i></template>{{ $ts.instanceBlocking }}</FormLink> | ||||
| 				<FormLink :active="page === 'proxy-account'" replace to="/instance/proxy-account"><template #icon><i class="fas fa-ghost"></i></template>{{ $ts.proxyAccount }}</FormLink> | ||||
| 				<FormLink :active="page === 'other-settings'" replace to="/instance/other-settings"><template #icon><i class="fas fa-cogs"></i></template>{{ $ts.other }}</FormLink> | ||||
| 			</FormGroup> | ||||
| 		</FormBase> | ||||
| 	</div> | ||||
| 	<div class="main"> | ||||
| 		<component :is="component" :key="page" @info="onInfo" v-bind="pageProps"/> | ||||
| 	</div> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { computed, defineComponent, markRaw } from 'vue'; | ||||
| import VueJsonPretty from 'vue-json-pretty'; | ||||
| 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 './index.metrics.vue'; | ||||
| import * as os from '@client/os'; | ||||
| import { computed, defineAsyncComponent, defineComponent, nextTick, onMounted, reactive, ref, watch } from 'vue'; | ||||
| import { i18n } from '@client/i18n'; | ||||
| import FormLink from '@client/components/form/link.vue'; | ||||
| import FormGroup from '@client/components/form/group.vue'; | ||||
| import FormBase from '@client/components/form/base.vue'; | ||||
| import FormButton from '@client/components/form/button.vue'; | ||||
| import { scroll } from '@client/scripts/scroll'; | ||||
| import * as symbols from '@client/symbols'; | ||||
| import * as os from '@client/os'; | ||||
| import { lookupUser } from '@client/scripts/lookup-user'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
| 		MkInstanceStats, | ||||
| 		MkButton, | ||||
| 		MkSelect, | ||||
| 		MkInput, | ||||
| 		MkContainer, | ||||
| 		MkFolder, | ||||
| 		XMetrics, | ||||
| 		VueJsonPretty, | ||||
| 		FormBase, | ||||
| 		FormLink, | ||||
| 		FormGroup, | ||||
| 		FormButton, | ||||
| 	}, | ||||
| 
 | ||||
| 	data() { | ||||
| 		return { | ||||
| 			[symbols.PAGE_INFO]: { | ||||
| 				tabs: [{ | ||||
| 					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, | ||||
| 	props: { | ||||
| 		initialPage: { | ||||
| 			type: String, | ||||
| 			required: false | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	computed: { | ||||
| 		meta() { | ||||
| 			return this.$instance; | ||||
| 		}, | ||||
| 	}, | ||||
| 
 | ||||
| 	mounted() { | ||||
| 		this.fetchJobs(); | ||||
| 		this.fetchModLogs(); | ||||
| 
 | ||||
| 		os.api('admin/server-info', {}).then(res => { | ||||
| 			this.serverInfo = res; | ||||
| 		}); | ||||
| 
 | ||||
| 		os.api('admin/get-table-stats', {}).then(res => { | ||||
| 			this.dbInfo = Object.entries(res).sort((a, b) => b[1].size - a[1].size); | ||||
| 		}); | ||||
| 	}, | ||||
| 
 | ||||
| 	methods: { | ||||
| 		async showInstanceInfo(q) { | ||||
| 			let instance = q; | ||||
| 			if (typeof q === 'string') { | ||||
| 				instance = await os.api('federation/show-instance', { | ||||
| 					host: q | ||||
| 				}); | ||||
| 	setup(props, context) { | ||||
| 		const indexInfo = { | ||||
| 			title: i18n.locale.instance, | ||||
| 			icon: 'fas fa-cog' | ||||
| 		}; | ||||
| 		const INFO = ref(indexInfo); | ||||
| 		const page = ref(props.initialPage); | ||||
| 		const narrow = ref(false); | ||||
| 		const view = ref(null); | ||||
| 		const el = ref(null); | ||||
| 		const onInfo = (viewInfo) => { | ||||
| 			INFO.value = viewInfo; | ||||
| 		}; | ||||
| 		const pageProps = ref({}); | ||||
| 		const component = computed(() => { | ||||
| 			if (page.value == null) return null; | ||||
| 			switch (page.value) { | ||||
| 				case 'overview': return defineAsyncComponent(() => import('./overview.vue')); | ||||
| 				case 'users': return defineAsyncComponent(() => import('./users.vue')); | ||||
| 				case 'emojis': return defineAsyncComponent(() => import('./emojis.vue')); | ||||
| 				case 'federation': return defineAsyncComponent(() => import('./federation.vue')); | ||||
| 				case 'queue': return defineAsyncComponent(() => import('./queue.vue')); | ||||
| 				case 'files': return defineAsyncComponent(() => import('./files.vue')); | ||||
| 				case 'announcements': return defineAsyncComponent(() => import('./announcements.vue')); | ||||
| 				case 'database': return defineAsyncComponent(() => import('./database.vue')); | ||||
| 				case 'abuses': return defineAsyncComponent(() => import('./abuses.vue')); | ||||
| 				case 'settings': return defineAsyncComponent(() => import('./settings.vue')); | ||||
| 				case 'files-settings': return defineAsyncComponent(() => import('./files-settings.vue')); | ||||
| 				case 'email-settings': return defineAsyncComponent(() => import('./email-settings.vue')); | ||||
| 				case 'object-storage': return defineAsyncComponent(() => import('./object-storage.vue')); | ||||
| 				case 'security': return defineAsyncComponent(() => import('./security.vue')); | ||||
| 				case 'bot-protection': return defineAsyncComponent(() => import('./bot-protection.vue')); | ||||
| 				case 'service-worker': return defineAsyncComponent(() => import('./service-worker.vue')); | ||||
| 				case 'relays': return defineAsyncComponent(() => import('./relays.vue')); | ||||
| 				case 'integrations': return defineAsyncComponent(() => import('./integrations.vue')); | ||||
| 				case 'integrations/twitter': return defineAsyncComponent(() => import('./integrations-twitter.vue')); | ||||
| 				case 'integrations/github': return defineAsyncComponent(() => import('./integrations-github.vue')); | ||||
| 				case 'integrations/discord': return defineAsyncComponent(() => import('./integrations-discord.vue')); | ||||
| 				case 'instance-block': return defineAsyncComponent(() => import('./instance-block.vue')); | ||||
| 				case 'proxy-account': return defineAsyncComponent(() => import('./proxy-account.vue')); | ||||
| 				case 'other-settings': return defineAsyncComponent(() => import('./other-settings.vue')); | ||||
| 			} | ||||
| 			os.popup(MkInstanceInfo, { | ||||
| 				instance: instance | ||||
| 			}, {}, 'closed'); | ||||
| 		}, | ||||
| 		}); | ||||
| 
 | ||||
| 		fetchJobs() { | ||||
| 			os.api('admin/queue/deliver-delayed', {}).then(jobs => { | ||||
| 				this.jobs = jobs; | ||||
| 		watch(component, () => { | ||||
| 			pageProps.value = {}; | ||||
| 
 | ||||
| 			nextTick(() => { | ||||
| 				scroll(el.value, 0); | ||||
| 			}); | ||||
| 		}, | ||||
| 		}, { immediate: true }); | ||||
| 
 | ||||
| 		fetchModLogs() { | ||||
| 			os.api('admin/show-moderation-logs', {}).then(logs => { | ||||
| 				this.modLogs = logs; | ||||
| 		watch(() => props.initialPage, () => { | ||||
| 			if (props.initialPage == null && !narrow.value) { | ||||
| 				page.value = 'overview'; | ||||
| 			} else { | ||||
| 				page.value = props.initialPage; | ||||
| 				if (props.initialPage == null) { | ||||
| 					INFO.value = indexInfo; | ||||
| 				} | ||||
| 			} | ||||
| 		}); | ||||
| 
 | ||||
| 		onMounted(() => { | ||||
| 			narrow.value = el.value.offsetWidth < 800; | ||||
| 			if (!narrow.value) { | ||||
| 				page.value = 'overview'; | ||||
| 			} | ||||
| 		}); | ||||
| 
 | ||||
| 		const invite = () => { | ||||
| 			os.api('admin/invite').then(x => { | ||||
| 				os.dialog({ | ||||
| 					type: 'info', | ||||
| 					text: x.code | ||||
| 				}); | ||||
| 			}).catch(e => { | ||||
| 				os.dialog({ | ||||
| 					type: 'error', | ||||
| 					text: e | ||||
| 				}); | ||||
| 			}); | ||||
| 		}, | ||||
| 		}; | ||||
| 
 | ||||
| 		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> | ||||
| 
 | ||||
| <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> | ||||
| <div> | ||||
| 	<MkFolder> | ||||
| 		<template #header><i class="fas fa-heartbeat"></i> {{ $ts.metrics }}</template> | ||||
| 		<div class="_section" style="padding: 0 var(--margin);"> | ||||
| 			<div class="_content"> | ||||
| 				<MkContainer :foldable="false" class="_gap"> | ||||
| 					<template #header><i class="fas fa-microchip"></i>{{ $ts.cpuAndMemory }}</template> | ||||
| 					<!-- | ||||
| 					<template #func> | ||||
| 						<button class="_button" @click="resume" :disabled="!paused"><i class="fas fa-play"></i></button> | ||||
| 						<button class="_button" @click="pause" :disabled="paused"><i class="fas fa-pause"></i></button> | ||||
| 					</template> | ||||
| 					--> | ||||
| 
 | ||||
| 					<div class="_content" style="margin-top: -8px; margin-bottom: -12px;"> | ||||
| 						<canvas :ref="cpumem"></canvas> | ||||
| 					</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 class="_formItem"> | ||||
| 	<div class="_formLabel"><i class="fas fa-microchip"></i> {{ $ts.cpuAndMemory }}</div> | ||||
| 	<div class="_formPanel xhexznfu"> | ||||
| 		<div> | ||||
| 			<canvas :ref="cpumem"></canvas> | ||||
| 		</div> | ||||
| 		<div 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> | ||||
| 	</MkFolder> | ||||
| 
 | ||||
| 	<MkFolder> | ||||
| 		<template #header><i class="fas fa-clipboard-list"></i> {{ $ts.jobQueue }}</template> | ||||
| 
 | ||||
| 		<div class="vkyrmkwb" :style="{ gridTemplateRows: queueHeight }"> | ||||
| 			<MkContainer :foldable="false" :scrollable="true" :resize-base-el="() => $el"> | ||||
| 				<template #header><i class="fas fa-exclamation-triangle"></i> {{ $ts.delayed }}</template> | ||||
| 
 | ||||
| 				<div class="_content"> | ||||
| 					<div class="_keyValue" v-for="job in jobs" :key="job[0]"> | ||||
| 						<button class="_button" @click="showInstanceInfo(job[0])">{{ job[0] }}</button> | ||||
| 						<div style="text-align: right;">{{ number(job[1]) }} jobs</div> | ||||
| 					</div> | ||||
| 				</div> | ||||
| 			</MkContainer> | ||||
| 			<XQueue :connection="queueConnection" domain="inbox" ref="queue" class="queue"> | ||||
| 				<template #title><i class="fas fa-exchange-alt"></i> In</template> | ||||
| 			</XQueue> | ||||
| 			<XQueue :connection="queueConnection" domain="deliver" class="queue"> | ||||
| 				<template #title><i class="fas fa-exchange-alt"></i> Out</template> | ||||
| 			</XQueue> | ||||
| 	</div> | ||||
| </div> | ||||
| <div class="_formItem"> | ||||
| 	<div class="_formLabel"><i class="fas fa-hdd"></i> {{ $ts.disk }}</div> | ||||
| 	<div class="_formPanel xhexznfu"> | ||||
| 		<div> | ||||
| 			<canvas :ref="disk"></canvas> | ||||
| 		</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> | ||||
| </template> | ||||
| 
 | ||||
|  | @ -188,9 +139,11 @@ export default defineComponent({ | |||
| 	}, | ||||
| 
 | ||||
| 	beforeUnmount() { | ||||
| 		this.connection.off('stats', this.onStats); | ||||
| 		this.connection.off('statsLog', this.onStatsLog); | ||||
| 		this.connection.dispose(); | ||||
| 		if (this.connection) { | ||||
| 			this.connection.off('stats', this.onStats); | ||||
| 			this.connection.off('statsLog', this.onStatsLog); | ||||
| 			this.connection.dispose(); | ||||
| 		} | ||||
| 		this.queueConnection.dispose(); | ||||
| 	}, | ||||
| 
 | ||||
|  | @ -232,9 +185,9 @@ export default defineComponent({ | |||
| 					aspectRatio: 3, | ||||
| 					layout: { | ||||
| 						padding: { | ||||
| 							left: 0, | ||||
| 							right: 0, | ||||
| 							top: 8, | ||||
| 							left: 16, | ||||
| 							right: 16, | ||||
| 							top: 16, | ||||
| 							bottom: 0 | ||||
| 						} | ||||
| 					}, | ||||
|  | @ -304,9 +257,9 @@ export default defineComponent({ | |||
| 					aspectRatio: 3, | ||||
| 					layout: { | ||||
| 						padding: { | ||||
| 							left: 0, | ||||
| 							right: 0, | ||||
| 							top: 8, | ||||
| 							left: 16, | ||||
| 							right: 16, | ||||
| 							top: 16, | ||||
| 							bottom: 0 | ||||
| 						} | ||||
| 					}, | ||||
|  | @ -375,9 +328,9 @@ export default defineComponent({ | |||
| 					aspectRatio: 3, | ||||
| 					layout: { | ||||
| 						padding: { | ||||
| 							left: 0, | ||||
| 							right: 0, | ||||
| 							top: 8, | ||||
| 							left: 16, | ||||
| 							right: 16, | ||||
| 							top: 16, | ||||
| 							bottom: 0 | ||||
| 						} | ||||
| 					}, | ||||
|  | @ -494,81 +447,9 @@ export default defineComponent({ | |||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| .xhexznfu { | ||||
| 	&.min-width_1000px { | ||||
| 		.sboqnrfi { | ||||
| 			display: grid; | ||||
| 			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); | ||||
| 		} | ||||
| 	> div:nth-child(2) { | ||||
| 		padding: 16px; | ||||
| 		border-top: solid 0.5px var(--divider); | ||||
| 	} | ||||
| } | ||||
| </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> | ||||
| <section class="_section"> | ||||
| 	<div class="_title"><slot name="title"></slot></div> | ||||
| 	<div class="_content _table"> | ||||
| 		<div class="_row"> | ||||
| 			<div class="_cell"><div class="_label">Process</div>{{ number(activeSincePrevTick) }}</div> | ||||
| 			<div class="_cell"><div class="_label">Active</div>{{ number(active) }}</div> | ||||
| 			<div class="_cell"><div class="_label">Waiting</div>{{ number(waiting) }}</div> | ||||
| 			<div class="_cell"><div class="_label">Delayed</div>{{ number(delayed) }}</div> | ||||
| 		</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 class="_formItem"> | ||||
| 	<div class="_formLabel"><slot name="title"></slot></div> | ||||
| 	<div class="_formPanel pumxzjhg"> | ||||
| 		<div class="_table status"> | ||||
| 			<div class="_row"> | ||||
| 				<div class="_cell"><div class="_label">Process</div>{{ number(activeSincePrevTick) }}</div> | ||||
| 				<div class="_cell"><div class="_label">Active</div>{{ number(active) }}</div> | ||||
| 				<div class="_cell"><div class="_label">Waiting</div>{{ number(waiting) }}</div> | ||||
| 				<div class="_cell"><div class="_label">Delayed</div>{{ number(delayed) }}</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> | ||||
| </section> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
|  | @ -110,10 +112,10 @@ export default defineComponent({ | |||
| 				aspectRatio: 3, | ||||
| 				layout: { | ||||
| 					padding: { | ||||
| 						left: 0, | ||||
| 						right: 0, | ||||
| 						top: 8, | ||||
| 						bottom: 0 | ||||
| 						left: 16, | ||||
| 						right: 16, | ||||
| 						top: 16, | ||||
| 						bottom: 12 | ||||
| 					} | ||||
| 				}, | ||||
| 				legend: { | ||||
|  | @ -198,3 +200,19 @@ export default defineComponent({ | |||
| 	} | ||||
| }); | ||||
| </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> | ||||
| <div> | ||||
| <FormBase> | ||||
| 	<XQueue :connection="connection" domain="inbox"> | ||||
| 		<template #title><i class="fas fa-exchange-alt"></i> In</template> | ||||
| 		<template #title>In</template> | ||||
| 	</XQueue> | ||||
| 	<XQueue :connection="connection" domain="deliver"> | ||||
| 		<template #title><i class="fas fa-exchange-alt"></i> Out</template> | ||||
| 		<template #title>Out</template> | ||||
| 	</XQueue> | ||||
| 	<section class="_section"> | ||||
| 		<div class="_content"> | ||||
| 			<MkButton @click="clear()"><i class="fas fa-trash-alt"></i> {{ $ts.clearQueue }}</MkButton> | ||||
| 		</div> | ||||
| 	</section> | ||||
| </div> | ||||
| 	<FormButton @click="clear()" danger><i class="fas fa-trash-alt"></i> {{ $ts.clearQueue }}</FormButton> | ||||
| </FormBase> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent } from 'vue'; | ||||
| import MkButton from '@client/components/ui/button.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 symbols from '@client/symbols'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
| 		FormBase, | ||||
| 		FormButton, | ||||
| 		MkButton, | ||||
| 		XQueue, | ||||
| 	}, | ||||
| 
 | ||||
| 	emits: ['info'], | ||||
| 
 | ||||
| 	data() { | ||||
| 		return { | ||||
| 			[symbols.PAGE_INFO]: { | ||||
| 				title: this.$ts.jobQueue, | ||||
| 				icon: 'fas fa-exchange-alt', | ||||
| 				icon: 'fas fa-clipboard-list', | ||||
| 			}, | ||||
| 			connection: os.stream.useSharedConnection('queueStats'), | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	mounted() { | ||||
| 		this.$emit('info', this[symbols.PAGE_INFO]); | ||||
| 
 | ||||
| 		this.$nextTick(() => { | ||||
| 			this.connection.send('requestLog', { | ||||
| 				id: Math.random().toString().substr(2, 8), | ||||
|  |  | |||
|  | @ -1,44 +1,41 @@ | |||
| <template> | ||||
| <div class="relaycxt"> | ||||
| 	<section class="_section add"> | ||||
| 		<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> | ||||
| <FormBase class="relaycxt"> | ||||
| 	<FormButton @click="addRelay" primary><i class="fas fa-plus"></i> {{ $ts.addRelay }}</FormButton> | ||||
| 
 | ||||
| 	<section class="_section relays"> | ||||
| 		<div class="_title"><i class="fas fa-project-diagram"></i> {{ $ts.addedRelays }}</div> | ||||
| 		<div class="_content relay" v-for="relay in relays" :key="relay.inbox"> | ||||
| 	<div class="_formItem" v-for="relay in relays" :key="relay.inbox"> | ||||
| 		<div class="_formPanel" style="padding: 16px;"> | ||||
| 			<div>{{ relay.inbox }}</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> | ||||
| 	</section> | ||||
| </div> | ||||
| 	</div> | ||||
| </FormBase> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent } from 'vue'; | ||||
| import MkButton from '@client/components/ui/button.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 symbols from '@client/symbols'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
| 		FormBase, | ||||
| 		FormButton, | ||||
| 		MkButton, | ||||
| 		MkInput, | ||||
| 	}, | ||||
| 
 | ||||
| 	emits: ['info'], | ||||
| 
 | ||||
| 	data() { | ||||
| 		return { | ||||
| 			[symbols.PAGE_INFO]: { | ||||
| 				title: this.$ts.relays, | ||||
| 				icon: 'fas fa-project-diagram', | ||||
| 				icon: 'fas fa-globe', | ||||
| 			}, | ||||
| 			relays: [], | ||||
| 			inbox: '', | ||||
|  | @ -49,8 +46,19 @@ export default defineComponent({ | |||
| 		this.refresh(); | ||||
| 	}, | ||||
| 
 | ||||
| 	mounted() { | ||||
| 		this.$emit('info', this[symbols.PAGE_INFO]); | ||||
| 	}, | ||||
| 
 | ||||
| 	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', { | ||||
| 				inbox | ||||
| 			}).then((relay: any) => { | ||||
|  | @ -86,9 +94,5 @@ export default defineComponent({ | |||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| ._content.relay { | ||||
| 	div { | ||||
| 		margin: 0.5em 0; | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| </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> | ||||
| <div v-if="meta" class="_section"> | ||||
| 	<section class="_card _gap"> | ||||
| 		<div class="_title"><i class="fas fa-info-circle"></i> {{ $ts.basicInfo }}</div> | ||||
| 		<div class="_content"> | ||||
| 			<MkInput v-model:value="name">{{ $ts.instanceName }}</MkInput> | ||||
| 			<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> | ||||
| <FormBase> | ||||
| 	<FormSuspense :p="init"> | ||||
| 		<FormInput v-model:value="name"> | ||||
| 			<span>{{ $ts.instanceName }}</span> | ||||
| 		</FormInput> | ||||
| 
 | ||||
| 	<MkInput v-model:value="pinnedClipId">{{ $ts.pinnedClipId }}</MkInput> | ||||
| 		<FormTextarea v-model:value="description"> | ||||
| 			<span>{{ $ts.instanceDescription }}</span> | ||||
| 		</FormTextarea> | ||||
| 
 | ||||
| 	<section class="_card _gap"> | ||||
| 		<div class="_content"> | ||||
| 			<MkInput v-model:value="maxNoteTextLength" type="number" :save="() => save()"><template #icon><i class="fas fa-pencil-alt"></i></template>{{ $ts.maxNoteTextLength }}</MkInput> | ||||
| 		</div> | ||||
| 		<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> | ||||
| 		<FormInput v-model:value="iconUrl"> | ||||
| 			<template #prefix><i class="fas fa-link"></i></template> | ||||
| 			<span>{{ $ts.iconUrl }}</span> | ||||
| 		</FormInput> | ||||
| 
 | ||||
| 	<section class="_card _gap"> | ||||
| 		<div class="_title"><i class="fas fa-user"></i> {{ $ts.registration }}</div> | ||||
| 		<div class="_content"> | ||||
| 			<MkSwitch v-model:value="enableRegistration" @update:value="save()">{{ $ts.enableRegistration }}</MkSwitch> | ||||
| 			<MkButton v-if="!enableRegistration" @click="invite">{{ $ts.invite }}</MkButton> | ||||
| 		</div> | ||||
| 	</section> | ||||
| 		<FormInput v-model:value="bannerUrl"> | ||||
| 			<template #prefix><i class="fas fa-link"></i></template> | ||||
| 			<span>{{ $ts.bannerUrl }}</span> | ||||
| 		</FormInput> | ||||
| 
 | ||||
| 	<section class="_card _gap"> | ||||
| 		<div class="_title"><i class="fas fa-shield-alt"></i> {{ $ts.hcaptcha }}</div> | ||||
| 		<div class="_content"> | ||||
| 			<MkSwitch v-model:value="enableHcaptcha">{{ $ts.enableHcaptcha }}</MkSwitch> | ||||
| 			<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> | ||||
| 		<FormInput v-model:value="tosUrl"> | ||||
| 			<template #prefix><i class="fas fa-link"></i></template> | ||||
| 			<span>{{ $ts.tosUrl }}</span> | ||||
| 		</FormInput> | ||||
| 
 | ||||
| 	<section class="_card _gap"> | ||||
| 		<div class="_title"><i class="fas fa-shield-alt"></i> {{ $ts.recaptcha }}</div> | ||||
| 		<div class="_content"> | ||||
| 			<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> | ||||
| 		<FormInput v-model:value="maintainerName"> | ||||
| 			<span>{{ $ts.maintainerName }}</span> | ||||
| 		</FormInput> | ||||
| 
 | ||||
| 	<section class="_card _gap"> | ||||
| 		<div class="_title"><i class="fas fa-envelope"></i> {{ $ts.emailConfig }}</div> | ||||
| 		<div class="_content"> | ||||
| 			<MkSwitch v-model:value="enableEmail" @update:value="save()">{{ $ts.enableEmail }}<template #desc>{{ $ts.emailConfigInfo }}</template></MkSwitch> | ||||
| 			<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> | ||||
| 		<FormInput v-model:value="maintainerEmail" type="email"> | ||||
| 			<template #prefix><i class="fas fa-envelope"></i></template> | ||||
| 			<span>{{ $ts.maintainerEmail }}</span> | ||||
| 		</FormInput> | ||||
| 
 | ||||
| 	<section class="_card _gap"> | ||||
| 		<div class="_title"><i class="fas fa-bolt"></i> {{ $ts.serviceworker }}</div> | ||||
| 		<div class="_content"> | ||||
| 			<MkSwitch v-model:value="enableServiceWorker">{{ $ts.enableServiceworker }}<template #desc>{{ $ts.serviceworkerInfo }}</template></MkSwitch> | ||||
| 			<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> | ||||
| 		<FormInput v-model:value="maxNoteTextLength" type="number"> | ||||
| 			<template #prefix><i class="fas fa-pencil-alt"></i></template> | ||||
| 			<span>{{ $ts.maxNoteTextLength }}</span> | ||||
| 		</FormInput> | ||||
| 
 | ||||
| 	<section class="_card _gap"> | ||||
| 		<div class="_title"><i class="fas fa-thumbtack"></i> {{ $ts.pinnedUsers }}</div> | ||||
| 		<div class="_content"> | ||||
| 			<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> | ||||
| 		<FormSwitch v-model:value="enableLocalTimeline">{{ $ts.enableLocalTimeline }}</FormSwitch> | ||||
| 		<FormSwitch v-model:value="enableGlobalTimeline">{{ $ts.enableGlobalTimeline }}</FormSwitch> | ||||
| 		<FormInfo>{{ $ts.disablingTimelinesInfo }}</FormInfo> | ||||
| 
 | ||||
| 	<section class="_card _gap"> | ||||
| 		<div class="_title"><i class="fas fa-thumbtack"></i> {{ $ts.pinnedPages }}</div> | ||||
| 		<div class="_content"> | ||||
| 			<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> | ||||
| 		<FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton> | ||||
| 	</FormSuspense> | ||||
| </FormBase> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent, defineAsyncComponent } from 'vue'; | ||||
| import MkButton from '@client/components/ui/button.vue'; | ||||
| import MkInput from '@client/components/ui/input.vue'; | ||||
| import MkTextarea from '@client/components/ui/textarea.vue'; | ||||
| import MkSwitch from '@client/components/ui/switch.vue'; | ||||
| import MkInfo from '@client/components/ui/info.vue'; | ||||
| import { url } from '@client/config'; | ||||
| import getAcct from '@/misc/acct/render'; | ||||
| 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 { fetchInstance } from '@client/instance'; | ||||
| import * as symbols from '@client/symbols'; | ||||
| import { fetchInstance } from '@client/instance'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
| 		MkButton, | ||||
| 		MkInput, | ||||
| 		MkTextarea, | ||||
| 		MkSwitch, | ||||
| 		MkInfo, | ||||
| 		Captcha: defineAsyncComponent(() => import('@client/components/captcha.vue')), | ||||
| 		FormSwitch, | ||||
| 		FormInput, | ||||
| 		FormBase, | ||||
| 		FormGroup, | ||||
| 		FormButton, | ||||
| 		FormTextarea, | ||||
| 		FormInfo, | ||||
| 		FormSuspense, | ||||
| 	}, | ||||
| 
 | ||||
| 	emits: ['info'], | ||||
| 
 | ||||
| 	data() { | ||||
| 		return { | ||||
| 			[symbols.PAGE_INFO]: { | ||||
| 				title: this.$ts.instance, | ||||
| 				icon: 'fas fa-cog', | ||||
| 				title: this.$ts.general, | ||||
| 				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, | ||||
| 			description: null, | ||||
| 			tosUrl: null as string | null, | ||||
| 			enableEmail: false, | ||||
| 			email: null, | ||||
| 			bannerUrl: null, | ||||
| 			maintainerName: null, | ||||
| 			maintainerEmail: null, | ||||
| 			iconUrl: null, | ||||
| 			logoImageUrl: null, | ||||
| 			backgroundImageUrl: null, | ||||
| 			bannerUrl: null, | ||||
| 			maxNoteTextLength: 0, | ||||
| 			enableRegistration: false, | ||||
| 			enableLocalTimeline: 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() { | ||||
| 		this.meta = await os.api('meta', { detail: true }); | ||||
| 
 | ||||
| 		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; | ||||
| 				}); | ||||
| 			} | ||||
| 		}); | ||||
| 	async mounted() { | ||||
| 		this.$emit('info', this[symbols.PAGE_INFO]); | ||||
| 	}, | ||||
| 
 | ||||
| 	methods: { | ||||
| 		invite() { | ||||
| 			os.api('admin/invite').then(x => { | ||||
| 				os.dialog({ | ||||
| 					type: 'info', | ||||
| 					text: x.code | ||||
| 				}); | ||||
| 			}).catch(e => { | ||||
| 				os.dialog({ | ||||
| 					type: 'error', | ||||
| 					text: e | ||||
| 				}); | ||||
| 			}); | ||||
| 		async init() { | ||||
| 			const meta = await os.api('meta', { detail: true }); | ||||
| 			this.name = meta.name; | ||||
| 			this.description = meta.description; | ||||
| 			this.tosUrl = meta.tosUrl; | ||||
| 			this.iconUrl = meta.iconUrl; | ||||
| 			this.bannerUrl = meta.bannerUrl; | ||||
| 			this.maintainerName = meta.maintainerName; | ||||
| 			this.maintainerEmail = meta.maintainerEmail; | ||||
| 			this.maxNoteTextLength = meta.maxNoteTextLength; | ||||
| 			this.enableLocalTimeline = !meta.disableLocalTimeline; | ||||
| 			this.enableGlobalTimeline = !meta.disableGlobalTimeline; | ||||
| 		}, | ||||
| 
 | ||||
| 		addPinUser() { | ||||
| 			os.selectUser().then(user => { | ||||
| 				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', { | ||||
| 		save() { | ||||
| 			os.apiWithDialog('admin/update-meta', { | ||||
| 				name: this.name, | ||||
| 				description: this.description, | ||||
| 				tosUrl: this.tosUrl, | ||||
| 				bannerUrl: this.bannerUrl, | ||||
| 				iconUrl: this.iconUrl, | ||||
| 				logoImageUrl: this.logoImageUrl, | ||||
| 				backgroundImageUrl: this.backgroundImageUrl, | ||||
| 				bannerUrl: this.bannerUrl, | ||||
| 				maintainerName: this.maintainerName, | ||||
| 				maintainerEmail: this.maintainerEmail, | ||||
| 				maxNoteTextLength: this.maxNoteTextLength, | ||||
| 				disableRegistration: !this.enableRegistration, | ||||
| 				disableLocalTimeline: !this.enableLocalTimeline, | ||||
| 				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(() => { | ||||
| 				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> | ||||
| <div class="mk-instance-users"> | ||||
| 	<div class="_section"> | ||||
| 		<div class="_content"> | ||||
| 			<MkButton inline primary @click="addUser()"><i class="fas fa-plus"></i> {{ $ts.addUser }}</MkButton> | ||||
| 		</div> | ||||
| <div class="lknzcolw"> | ||||
| 	<div class="actions"> | ||||
| 		<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 class="_section lookup"> | ||||
| 		<div class="_title"><i class="fas fa-search"></i> {{ $ts.lookup }}</div> | ||||
| 		<div class="_content"> | ||||
| 			<MkInput class="target" v-model:value="target" type="text" @enter="showUser()"> | ||||
| 				<span>{{ $ts.usernameOrUserId }}</span> | ||||
| 	<div class="users"> | ||||
| 		<div class="inputs" style="display: flex;"> | ||||
| 			<MkSelect v-model:value="sort" style="margin: 0; flex: 1;"> | ||||
| 				<template #label>{{ $ts.sort }}</template> | ||||
| 				<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> | ||||
| 			<MkButton @click="showUser()" primary><i class="fas fa-search"></i> {{ $ts.lookup }}</MkButton> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 
 | ||||
| 	<div class="_section users"> | ||||
| 		<div class="_title"><i class="fas fa-users"></i> {{ $ts.users }}</div> | ||||
| 		<div class="_content"> | ||||
| 			<div class="inputs" style="display: flex;"> | ||||
| 				<MkSelect v-model:value="sort" style="margin: 0; flex: 1;"> | ||||
| 					<template #label>{{ $ts.sort }}</template> | ||||
| 					<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> | ||||
| 			</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> | ||||
| 		<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> | ||||
| 				</button> | ||||
| 			</MkPagination> | ||||
| 		</div> | ||||
| 					<div> | ||||
| 						<span>{{ $ts.registeredDate }}: <MkTime :time="user.createdAt" mode="detail"/></span> | ||||
| 					</div> | ||||
| 				</div> | ||||
| 			</button> | ||||
| 		</MkPagination> | ||||
| 	</div> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent } from 'vue'; | ||||
| import parseAcct from '@/misc/acct/parse'; | ||||
| import MkButton from '@client/components/ui/button.vue'; | ||||
| import MkInput from '@client/components/ui/input.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 * as os from '@client/os'; | ||||
| import * as symbols from '@client/symbols'; | ||||
| import { lookupUser } from '@client/scripts/lookup-user'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
|  | @ -97,6 +83,8 @@ export default defineComponent({ | |||
| 		MkPagination, | ||||
| 	}, | ||||
| 
 | ||||
| 	emits: ['info'], | ||||
| 
 | ||||
| 	data() { | ||||
| 		return { | ||||
| 			[symbols.PAGE_INFO]: { | ||||
|  | @ -107,7 +95,6 @@ export default defineComponent({ | |||
| 					handler: this.searchUser | ||||
| 				} | ||||
| 			}, | ||||
| 			target: '', | ||||
| 			sort: '+createdAt', | ||||
| 			state: 'all', | ||||
| 			origin: 'local', | ||||
|  | @ -140,40 +127,12 @@ export default defineComponent({ | |||
| 		}, | ||||
| 	}, | ||||
| 
 | ||||
| 	methods: { | ||||
| 		/** テキストエリアのユーザーを解決する */ | ||||
| 		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(); | ||||
| 				}); | ||||
| 			}); | ||||
| 		}, | ||||
| 	async mounted() { | ||||
| 		this.$emit('info', this[symbols.PAGE_INFO]); | ||||
| 	}, | ||||
| 
 | ||||
| 		/** テキストエリアから処理対象ユーザーを設定する */ | ||||
| 		async showUser() { | ||||
| 			const user = await this.fetchUser(); | ||||
| 			this.show(user); | ||||
| 			this.target = ''; | ||||
| 		}, | ||||
| 	methods: { | ||||
| 		lookupUser, | ||||
| 
 | ||||
| 		searchUser() { | ||||
| 			os.selectUser().then(user => { | ||||
|  | @ -203,9 +162,7 @@ export default defineComponent({ | |||
| 		}, | ||||
| 
 | ||||
| 		show(user) { | ||||
| 			os.popup(import('./user-dialog.vue'), { | ||||
| 				userId: user.id | ||||
| 			}, {}, 'closed'); | ||||
| 			os.pageWindow(`/instance/user/${user.id}`); | ||||
| 		}, | ||||
| 
 | ||||
| 		acct | ||||
|  | @ -214,57 +171,61 @@ export default defineComponent({ | |||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| .mk-instance-users { | ||||
| .lknzcolw { | ||||
| 	> .actions { | ||||
| 		margin: var(--margin); | ||||
| 	} | ||||
| 
 | ||||
| 	> .users { | ||||
| 		> ._content { | ||||
| 			> .users { | ||||
| 				margin-top: var(--margin); | ||||
| 		margin: var(--margin); | ||||
| 	 | ||||
| 		> .users { | ||||
| 			margin-top: var(--margin); | ||||
| 
 | ||||
| 				> .user { | ||||
| 					display: flex; | ||||
| 					width: 100%; | ||||
| 					box-sizing: border-box; | ||||
| 					text-align: left; | ||||
| 					align-items: center; | ||||
| 					padding: 16px; | ||||
| 			> .user { | ||||
| 				display: flex; | ||||
| 				width: 100%; | ||||
| 				box-sizing: border-box; | ||||
| 				text-align: left; | ||||
| 				align-items: center; | ||||
| 				padding: 16px; | ||||
| 
 | ||||
| 					&:hover { | ||||
| 						color: var(--accent); | ||||
| 				&: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 { | ||||
| 						width: 60px; | ||||
| 						height: 60px; | ||||
| 					} | ||||
| 
 | ||||
| 					> .body { | ||||
| 						margin-left: 0.3em; | ||||
| 						padding: 0 8px; | ||||
| 						flex: 1; | ||||
| 
 | ||||
| 						@media (max-width: 500px) { | ||||
| 							font-size: 14px; | ||||
| 					> header { | ||||
| 						> .name { | ||||
| 							font-weight: bold; | ||||
| 						} | ||||
| 
 | ||||
| 						> header { | ||||
| 							> .name { | ||||
| 								font-weight: bold; | ||||
| 							} | ||||
| 						> .acct { | ||||
| 							margin-left: 8px; | ||||
| 							opacity: 0.7; | ||||
| 						} | ||||
| 
 | ||||
| 							> .acct { | ||||
| 								margin-left: 8px; | ||||
| 								opacity: 0.7; | ||||
| 							} | ||||
| 						> .staff { | ||||
| 							margin-left: 0.5em; | ||||
| 							color: var(--badge); | ||||
| 						} | ||||
| 
 | ||||
| 							> .staff { | ||||
| 								margin-left: 0.5em; | ||||
| 								color: var(--badge); | ||||
| 							} | ||||
| 
 | ||||
| 							> .punished { | ||||
| 								margin-left: 0.5em; | ||||
| 								color: #4dabf7; | ||||
| 							} | ||||
| 						> .punished { | ||||
| 							margin-left: 0.5em; | ||||
| 							color: #4dabf7; | ||||
| 						} | ||||
| 					} | ||||
| 				} | ||||
|  |  | |||
|  | @ -1,34 +1,36 @@ | |||
| <template> | ||||
| <FormBase> | ||||
| 	<FormGroup v-if="user"> | ||||
| 		<template #label><MkAcct :user="user"/></template> | ||||
| 
 | ||||
| 		<FormKeyValueView> | ||||
| 			<template #key>ID</template> | ||||
| 			<template #value><span class="_monospace">{{ user.id }}</span></template> | ||||
| 		</FormKeyValueView> | ||||
| 
 | ||||
| 	<FormSuspense :p="init"> | ||||
| 		<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> | ||||
| 				<template #key>{{ $ts.updatedAt }}</template> | ||||
| 				<template #value><MkTime v-if="user.lastFetchedAt" mode="detail" :time="user.lastFetchedAt"/><span v-else>N/A</span></template> | ||||
| 				<template #key>ID</template> | ||||
| 				<template #value><span class="_monospace">{{ user.id }}</span></template> | ||||
| 			</FormKeyValueView> | ||||
| 		</FormGroup> | ||||
| 
 | ||||
| 		<FormObjectView tall :value="user"> | ||||
| 			<span>Raw</span> | ||||
| 		</FormObjectView> | ||||
| 	</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> | ||||
| 		</FormGroup> | ||||
| 	</FormSuspense> | ||||
| </FormBase> | ||||
| </template> | ||||
| 
 | ||||
|  | @ -80,23 +82,27 @@ export default defineComponent({ | |||
| 					} | ||||
| 				} : undefined].filter(x => x !== undefined) : [], | ||||
| 			})), | ||||
| 			init: null, | ||||
| 			user: null, | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	mounted() { | ||||
| 		this.fetch(); | ||||
| 	watch: { | ||||
| 		userId: { | ||||
| 			handler() { | ||||
| 				this.init = () => os.api('users/show', { | ||||
| 					userId: this.userId | ||||
| 				}).then(user => { | ||||
| 					this.user = user; | ||||
| 				}); | ||||
| 			}, | ||||
| 			immediate: true | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	methods: { | ||||
| 		number, | ||||
| 		bytes, | ||||
| 
 | ||||
| 		async fetch() { | ||||
| 			this.user = await os.api('users/show', { | ||||
| 				userId: this.userId | ||||
| 			}); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
|  |  | |||
|  | @ -195,7 +195,7 @@ | |||
| 
 | ||||
| 			<template v-if="page === 'index'"> | ||||
| 				<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"/> | ||||
| 					</div> | ||||
| 					<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/clips', component: page('my-clips/index') }, | ||||
| 		{ 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/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: '/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 }) }, | ||||
|  |  | |||
|  | @ -124,7 +124,13 @@ export function getUserMenu(user) { | |||
| 		action: () => { | ||||
| 			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', | ||||
| 		text: i18n.locale.info, | ||||
| 		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> | ||||
| 				</template> | ||||
| 				<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> | ||||
| 				</button> | ||||
| 				</MkA> | ||||
| 				<button class="item _button" @click="more"> | ||||
| 					<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> | ||||
|  | @ -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) { | ||||
| 			os.popup(import('@client/components/launch-pad.vue'), {}, { | ||||
| 			}, 'closed'); | ||||
|  |  | |||
|  | @ -20,9 +20,9 @@ | |||
| 		</component> | ||||
| 	</template> | ||||
| 	<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> | ||||
| 	</button> | ||||
| 	</MkA> | ||||
| 	<button class="item _button" @click="more"> | ||||
| 		<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> | ||||
|  | @ -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) { | ||||
| 			os.popup(import('@client/components/launch-pad.vue'), {}, { | ||||
| 			}, 'closed'); | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue