Merge remote-tracking branch 'misskey-dev/develop' into io
This commit is contained in:
		| @@ -4,48 +4,64 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| --> | ||||
|  | ||||
| <template> | ||||
| <Suspense> | ||||
| 	<template #fallback> | ||||
| 		<MkLoading v-if="!inline ?? true"/> | ||||
| 	</template> | ||||
| 	<code v-if="inline" :class="$style.codeInlineRoot">{{ code }}</code> | ||||
| 	<XCode v-else-if="show && lang" :code="code" :lang="lang"/> | ||||
| 	<pre v-else-if="show" :class="$style.codeBlockFallbackRoot"><code :class="$style.codeBlockFallbackCode">{{ code }}</code></pre> | ||||
| 	<button v-else :class="$style.codePlaceholderRoot" @click="show = true"> | ||||
| 		<div :class="$style.codePlaceholderContainer"> | ||||
| 			<div><i class="ti ti-code"></i> {{ i18n.ts.code }}</div> | ||||
| 			<div>{{ i18n.ts.clickToShow }}</div> | ||||
| 		</div> | ||||
| <div :class="$style.codeBlockRoot"> | ||||
| 	<button :class="$style.codeBlockCopyButton" class="_button" @click="copy"> | ||||
| 		<i class="ti ti-copy"></i> | ||||
| 	</button> | ||||
| </Suspense> | ||||
| 	<Suspense> | ||||
| 		<template #fallback> | ||||
| 			<MkLoading /> | ||||
| 		</template> | ||||
| 		<XCode v-if="show && lang" :code="code" :lang="lang"/> | ||||
| 		<pre v-else-if="show" :class="$style.codeBlockFallbackRoot"><code :class="$style.codeBlockFallbackCode">{{ code }}</code></pre> | ||||
| 		<button v-else :class="$style.codePlaceholderRoot" @click="show = true"> | ||||
| 			<div :class="$style.codePlaceholderContainer"> | ||||
| 				<div><i class="ti ti-code"></i> {{ i18n.ts.code }}</div> | ||||
| 				<div>{{ i18n.ts.clickToShow }}</div> | ||||
| 			</div> | ||||
| 		</button> | ||||
| 	</Suspense> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts" setup> | ||||
| import { defineAsyncComponent, ref } from 'vue'; | ||||
| import * as os from '@/os.js'; | ||||
| import MkLoading from '@/components/global/MkLoading.vue'; | ||||
| import { defaultStore } from '@/store.js'; | ||||
| import { i18n } from '@/i18n.js'; | ||||
| import copyToClipboard from '@/scripts/copy-to-clipboard.js'; | ||||
|  | ||||
| defineProps<{ | ||||
| const props = defineProps<{ | ||||
| 	code: string; | ||||
| 	lang?: string; | ||||
| 	inline?: boolean; | ||||
| }>(); | ||||
|  | ||||
| const show = ref(!defaultStore.state.dataSaver.code); | ||||
|  | ||||
| const XCode = defineAsyncComponent(() => import('@/components/MkCode.core.vue')); | ||||
|  | ||||
| function copy() { | ||||
| 	copyToClipboard(props.code); | ||||
| 	os.success(); | ||||
| } | ||||
| </script> | ||||
|  | ||||
| <style module lang="scss"> | ||||
| .codeInlineRoot { | ||||
| 	display: inline-block; | ||||
| 	font-family: Consolas, Monaco, Andale Mono, Ubuntu Mono, monospace; | ||||
| 	overflow-wrap: anywhere; | ||||
| .codeBlockRoot { | ||||
| 	position: relative; | ||||
| } | ||||
|  | ||||
| .codeBlockCopyButton { | ||||
| 	color: #D4D4D4; | ||||
| 	background: #1E1E1E; | ||||
| 	padding: .1em; | ||||
| 	border-radius: .3em; | ||||
| 	position: absolute; | ||||
| 	top: 8px; | ||||
| 	right: 8px; | ||||
| 	opacity: 0.5; | ||||
|  | ||||
| 	&:hover { | ||||
| 		opacity: 0.8; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| .codeBlockFallbackRoot { | ||||
|   | ||||
							
								
								
									
										26
									
								
								packages/frontend/src/components/MkCodeInline.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								packages/frontend/src/components/MkCodeInline.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | ||||
| <!-- | ||||
| SPDX-FileCopyrightText: syuilo and other misskey contributors | ||||
| SPDX-License-Identifier: AGPL-3.0-only | ||||
| --> | ||||
|  | ||||
| <template> | ||||
| <code :class="$style.root">{{ code }}</code> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts" setup> | ||||
| const props = defineProps<{ | ||||
| 	code: string; | ||||
| }>(); | ||||
| </script> | ||||
|  | ||||
| <style module lang="scss"> | ||||
| .root { | ||||
| 	display: inline-block; | ||||
| 	font-family: Consolas, Monaco, Andale Mono, Ubuntu Mono, monospace; | ||||
| 	overflow-wrap: anywhere; | ||||
| 	color: #D4D4D4; | ||||
| 	background: #1E1E1E; | ||||
| 	padding: .1em; | ||||
| 	border-radius: .3em; | ||||
| } | ||||
| </style> | ||||
							
								
								
									
										209
									
								
								packages/frontend/src/components/MkHorizontalSwipe.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										209
									
								
								packages/frontend/src/components/MkHorizontalSwipe.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,209 @@ | ||||
| <!-- | ||||
| SPDX-FileCopyrightText: syuilo and other misskey contributors | ||||
| SPDX-License-Identifier: AGPL-3.0-only | ||||
| --> | ||||
|  | ||||
| <template> | ||||
| <div | ||||
| 	ref="rootEl" | ||||
| 	:class="[$style.transitionRoot, (defaultStore.state.animation && $style.enableAnimation)]" | ||||
| 	@touchstart="touchStart" | ||||
| 	@touchmove="touchMove" | ||||
| 	@touchend="touchEnd" | ||||
| > | ||||
| 	<Transition | ||||
| 		:class="[$style.transitionChildren, { [$style.swiping]: isSwipingForClass }]" | ||||
| 		:enterActiveClass="$style.swipeAnimation_enterActive" | ||||
| 		:leaveActiveClass="$style.swipeAnimation_leaveActive" | ||||
| 		:enterFromClass="transitionName === 'swipeAnimationLeft' ? $style.swipeAnimationLeft_enterFrom : $style.swipeAnimationRight_enterFrom" | ||||
| 		:leaveToClass="transitionName === 'swipeAnimationLeft' ? $style.swipeAnimationLeft_leaveTo : $style.swipeAnimationRight_leaveTo" | ||||
| 		:style="`--swipe: ${pullDistance}px;`" | ||||
| 	> | ||||
| 		<!-- 【注意】slot内の最上位要素に動的にkeyを設定すること --> | ||||
| 		<!-- 各最上位要素にユニークなkeyの指定がないとTransitionがうまく動きません --> | ||||
| 		<slot></slot> | ||||
| 	</Transition> | ||||
| </div> | ||||
| </template> | ||||
| <script lang="ts" setup> | ||||
| import { ref, shallowRef, computed, nextTick, watch } from 'vue'; | ||||
| import type { Tab } from '@/components/global/MkPageHeader.tabs.vue'; | ||||
| import { defaultStore } from '@/store.js'; | ||||
|  | ||||
| const rootEl = shallowRef<HTMLDivElement>(); | ||||
|  | ||||
| // eslint-disable-next-line no-undef | ||||
| const tabModel = defineModel<string>('tab'); | ||||
|  | ||||
| const props = defineProps<{ | ||||
| 	tabs: Tab[]; | ||||
| }>(); | ||||
|  | ||||
| const emit = defineEmits<{ | ||||
| 	(ev: 'swiped', newKey: string, direction: 'left' | 'right'): void; | ||||
| }>(); | ||||
|  | ||||
| // ▼ しきい値 ▼ // | ||||
|  | ||||
| // スワイプと判定される最小の距離 | ||||
| const MIN_SWIPE_DISTANCE = 50; | ||||
|  | ||||
| // スワイプ時の動作を発火する最小の距離 | ||||
| const SWIPE_DISTANCE_THRESHOLD = 125; | ||||
|  | ||||
| // スワイプを中断するY方向の移動距離 | ||||
| const SWIPE_ABORT_Y_THRESHOLD = 75; | ||||
|  | ||||
| // スワイプできる最大の距離 | ||||
| const MAX_SWIPE_DISTANCE = 150; | ||||
|  | ||||
| // ▲ しきい値 ▲ // | ||||
|  | ||||
| let startScreenX: number | null = null; | ||||
| let startScreenY: number | null = null; | ||||
|  | ||||
| const currentTabIndex = computed(() => props.tabs.findIndex(tab => tab.key === tabModel.value)); | ||||
|  | ||||
| const pullDistance = ref(0); | ||||
| const isSwiping = ref(false); | ||||
| const isSwipingForClass = ref(false); | ||||
| let swipeAborted = false; | ||||
|  | ||||
| function touchStart(event: TouchEvent) { | ||||
| 	if (!defaultStore.reactiveState.enableHorizontalSwipe.value) return; | ||||
|  | ||||
| 	if (event.touches.length !== 1) return; | ||||
|  | ||||
| 	startScreenX = event.touches[0].screenX; | ||||
| 	startScreenY = event.touches[0].screenY; | ||||
| } | ||||
|  | ||||
| function touchMove(event: TouchEvent) { | ||||
| 	if (!defaultStore.reactiveState.enableHorizontalSwipe.value) return; | ||||
|  | ||||
| 	if (event.touches.length !== 1) return; | ||||
|  | ||||
| 	if (startScreenX == null || startScreenY == null) return; | ||||
|  | ||||
| 	if (swipeAborted) return; | ||||
|  | ||||
| 	let distanceX = event.touches[0].screenX - startScreenX; | ||||
| 	let distanceY = event.touches[0].screenY - startScreenY; | ||||
|  | ||||
| 	if (Math.abs(distanceY) > SWIPE_ABORT_Y_THRESHOLD) { | ||||
| 		swipeAborted = true; | ||||
|  | ||||
| 		pullDistance.value = 0; | ||||
| 		isSwiping.value = false; | ||||
| 		setTimeout(() => { | ||||
| 			isSwipingForClass.value = false; | ||||
| 		}, 400); | ||||
|  | ||||
| 		return; | ||||
| 	} | ||||
|  | ||||
| 	if (Math.abs(distanceX) < MIN_SWIPE_DISTANCE) return; | ||||
| 	if (Math.abs(distanceX) > MAX_SWIPE_DISTANCE) return; | ||||
|  | ||||
| 	if (currentTabIndex.value === 0 || props.tabs[currentTabIndex.value - 1].onClick) { | ||||
| 		distanceX = Math.min(distanceX, 0); | ||||
| 	} | ||||
| 	if (currentTabIndex.value === props.tabs.length - 1 || props.tabs[currentTabIndex.value + 1].onClick) { | ||||
| 		distanceX = Math.max(distanceX, 0); | ||||
| 	} | ||||
| 	if (distanceX === 0) return; | ||||
|  | ||||
| 	isSwiping.value = true; | ||||
| 	isSwipingForClass.value = true; | ||||
| 	nextTick(() => { | ||||
| 		// グリッチを控えるため、1.5px以上の差がないと更新しない | ||||
| 		if (Math.abs(distanceX - pullDistance.value) < 1.5) return; | ||||
| 		pullDistance.value = distanceX; | ||||
| 	}); | ||||
| } | ||||
|  | ||||
| function touchEnd(event: TouchEvent) { | ||||
| 	if (swipeAborted) { | ||||
| 		swipeAborted = false; | ||||
| 		return; | ||||
| 	} | ||||
|  | ||||
| 	if (!defaultStore.reactiveState.enableHorizontalSwipe.value) return; | ||||
|  | ||||
| 	if (event.touches.length !== 0) return; | ||||
|  | ||||
| 	if (startScreenX == null) return; | ||||
|  | ||||
| 	if (!isSwiping.value) return; | ||||
|  | ||||
| 	const distance = event.changedTouches[0].screenX - startScreenX; | ||||
|  | ||||
| 	if (Math.abs(distance) > SWIPE_DISTANCE_THRESHOLD) { | ||||
| 		if (distance > 0) { | ||||
| 			if (props.tabs[currentTabIndex.value - 1] && !props.tabs[currentTabIndex.value - 1].onClick) { | ||||
| 				tabModel.value = props.tabs[currentTabIndex.value - 1].key; | ||||
| 				emit('swiped', props.tabs[currentTabIndex.value - 1].key, 'right'); | ||||
| 			} | ||||
| 		} else { | ||||
| 			if (props.tabs[currentTabIndex.value + 1] && !props.tabs[currentTabIndex.value + 1].onClick) { | ||||
| 				tabModel.value = props.tabs[currentTabIndex.value + 1].key; | ||||
| 				emit('swiped', props.tabs[currentTabIndex.value + 1].key, 'left'); | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	pullDistance.value = 0; | ||||
| 	isSwiping.value = false; | ||||
| 	setTimeout(() => { | ||||
| 		isSwipingForClass.value = false; | ||||
| 	}, 400); | ||||
| } | ||||
|  | ||||
| const transitionName = ref<'swipeAnimationLeft' | 'swipeAnimationRight' | undefined>(undefined); | ||||
|  | ||||
| watch(tabModel, (newTab, oldTab) => { | ||||
| 	const newIndex = props.tabs.findIndex(tab => tab.key === newTab); | ||||
| 	const oldIndex = props.tabs.findIndex(tab => tab.key === oldTab); | ||||
|  | ||||
| 	if (oldIndex >= 0 && newIndex && oldIndex < newIndex) { | ||||
| 		transitionName.value = 'swipeAnimationLeft'; | ||||
| 	} else { | ||||
| 		transitionName.value = 'swipeAnimationRight'; | ||||
| 	} | ||||
|  | ||||
| 	window.setTimeout(() => { | ||||
| 		transitionName.value = undefined; | ||||
| 	}, 400); | ||||
| }); | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" module> | ||||
| .transitionRoot.enableAnimation { | ||||
| 	display: grid; | ||||
| 	overflow: clip; | ||||
|  | ||||
| 	.transitionChildren { | ||||
| 		grid-area: 1 / 1 / 2 / 2; | ||||
| 		transform: translateX(var(--swipe)); | ||||
|  | ||||
| 		&.swipeAnimation_enterActive, | ||||
| 		&.swipeAnimation_leaveActive { | ||||
| 			transition: transform .3s cubic-bezier(0.65, 0.05, 0.36, 1); | ||||
| 		} | ||||
|  | ||||
| 		&.swipeAnimationRight_leaveTo, | ||||
| 		&.swipeAnimationLeft_enterFrom { | ||||
| 			transform: translateX(calc(100% + 24px)); | ||||
| 		} | ||||
|  | ||||
| 		&.swipeAnimationRight_enterFrom, | ||||
| 		&.swipeAnimationLeft_leaveTo { | ||||
| 			transform: translateX(calc(-100% - 24px)); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| .swiping { | ||||
| 	transition: transform .2s ease-out; | ||||
| } | ||||
| </style> | ||||
| @@ -138,7 +138,7 @@ const rangePercent = computed({ | ||||
| 		audioEl.value.currentTime = to * durationMs.value / 1000; | ||||
| 	}, | ||||
| }); | ||||
| const volume = ref(.5); | ||||
| const volume = ref(.25); | ||||
| const bufferedEnd = ref(0); | ||||
| const bufferedDataRatio = computed(() => { | ||||
| 	if (!audioEl.value) return 0; | ||||
| @@ -161,7 +161,7 @@ function togglePlayPause() { | ||||
|  | ||||
| function toggleMute() { | ||||
| 	if (volume.value === 0) { | ||||
| 		volume.value = .5; | ||||
| 		volume.value = .25; | ||||
| 	} else { | ||||
| 		volume.value = 0; | ||||
| 	} | ||||
|   | ||||
| @@ -5,9 +5,11 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
|  | ||||
| <!-- Media系専用のinput range --> | ||||
| <template> | ||||
| <div :class="$style.controlsSeekbar" :style="sliderBgWhite ? '--sliderBg: rgba(255,255,255,.25);' : '--sliderBg: var(--scrollbarHandle);'"> | ||||
| 	<progress v-if="buffer !== undefined" :class="$style.buffer" :value="isNaN(buffer) ? 0 : buffer" min="0" max="1">{{ Math.round(buffer * 100) }}% buffered</progress> | ||||
| 	<input v-model="model" :class="$style.seek" :style="`--value: ${modelValue * 100}%;`" type="range" min="0" max="1" step="any" @change="emit('dragEnded', modelValue)"/> | ||||
| <div :style="sliderBgWhite ? '--sliderBg: rgba(255,255,255,.25);' : '--sliderBg: var(--scrollbarHandle);'"> | ||||
| 	<div :class="$style.controlsSeekbar"> | ||||
| 		<progress v-if="buffer !== undefined" :class="$style.buffer" :value="isNaN(buffer) ? 0 : buffer" min="0" max="1">{{ Math.round(buffer * 100) }}% buffered</progress> | ||||
| 		<input v-model="model" :class="$style.seek" :style="`--value: ${modelValue * 100}%;`" type="range" min="0" max="1" step="any" @change="emit('dragEnded', modelValue)"/> | ||||
| 	</div> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
|   | ||||
| @@ -176,7 +176,7 @@ const rangePercent = computed({ | ||||
| 		videoEl.value.currentTime = to * durationMs.value / 1000; | ||||
| 	}, | ||||
| }); | ||||
| const volume = ref(.5); | ||||
| const volume = ref(.25); | ||||
| const bufferedEnd = ref(0); | ||||
| const bufferedDataRatio = computed(() => { | ||||
| 	if (!videoEl.value) return 0; | ||||
| @@ -236,7 +236,7 @@ function toggleFullscreen() { | ||||
|  | ||||
| function toggleMute() { | ||||
| 	if (volume.value === 0) { | ||||
| 		volume.value = .5; | ||||
| 		volume.value = .25; | ||||
| 	} else { | ||||
| 		volume.value = 0; | ||||
| 	} | ||||
| @@ -533,6 +533,9 @@ onDeactivated(() => { | ||||
|  | ||||
| .seekbarRoot { | ||||
| 	grid-area: seekbar; | ||||
| 	/* ▼シークバー操作をやりやすくするためにクリックイベントが伝播されないエリアを拡張する */ | ||||
| 	margin: -10px; | ||||
| 	padding: 10px; | ||||
| } | ||||
|  | ||||
| @container (min-width: 500px) { | ||||
|   | ||||
| @@ -13,6 +13,7 @@ import MkMention from '@/components/MkMention.vue'; | ||||
| import MkEmoji from '@/components/global/MkEmoji.vue'; | ||||
| import MkCustomEmoji from '@/components/global/MkCustomEmoji.vue'; | ||||
| import MkCode from '@/components/MkCode.vue'; | ||||
| import MkCodeInline from '@/components/MkCodeInline.vue'; | ||||
| import MkGoogle from '@/components/MkGoogle.vue'; | ||||
| import MkSparkle from '@/components/MkSparkle.vue'; | ||||
| import MkA from '@/components/global/MkA.vue'; | ||||
| @@ -373,10 +374,9 @@ export default function(props: MfmProps, context: SetupContext<MfmEvents>) { | ||||
| 			} | ||||
|  | ||||
| 			case 'inlineCode': { | ||||
| 				return [h(MkCode, { | ||||
| 				return [h(MkCodeInline, { | ||||
| 					key: Math.random(), | ||||
| 					code: token.props.code, | ||||
| 					inline: true, | ||||
| 				})]; | ||||
| 			} | ||||
|  | ||||
|   | ||||
| @@ -5,7 +5,7 @@ | ||||
|  | ||||
| import { i18n } from '@/i18n.js'; | ||||
|  | ||||
| export function hms(ms: number, options: { | ||||
| export function hms(ms: number, options?: { | ||||
| 	textFormat?: 'colon' | 'locale'; | ||||
| 	enableSeconds?: boolean; | ||||
| 	enableMs?: boolean; | ||||
|   | ||||
| @@ -6,98 +6,100 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| <template> | ||||
| <MkStickyContainer> | ||||
| 	<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template> | ||||
| 	<MkSpacer v-if="tab === 'overview'" :contentMax="600" :marginMin="20"> | ||||
| 		<div class="_gaps_m"> | ||||
| 			<div :class="$style.banner" :style="{ backgroundImage: `url(${ instance.bannerUrl })` }"> | ||||
| 				<div style="overflow: clip;"> | ||||
| 					<img :src="instance.iconUrl ?? instance.faviconUrl ?? '/favicon.ico'" alt="" :class="$style.bannerIcon"/> | ||||
| 					<div :class="$style.bannerName"> | ||||
| 						<b>{{ instance.name ?? host }}</b> | ||||
| 	<MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs"> | ||||
| 		<MkSpacer v-if="tab === 'overview'" :contentMax="600" :marginMin="20"> | ||||
| 			<div class="_gaps_m"> | ||||
| 				<div :class="$style.banner" :style="{ backgroundImage: `url(${ instance.bannerUrl })` }"> | ||||
| 					<div style="overflow: clip;"> | ||||
| 						<img :src="instance.iconUrl ?? instance.faviconUrl ?? '/favicon.ico'" alt="" :class="$style.bannerIcon"/> | ||||
| 						<div :class="$style.bannerName"> | ||||
| 							<b>{{ instance.name ?? host }}</b> | ||||
| 						</div> | ||||
| 					</div> | ||||
| 				</div> | ||||
| 			</div> | ||||
|  | ||||
| 			<MkKeyValue> | ||||
| 				<template #key>{{ i18n.ts.description }}</template> | ||||
| 				<template #value><div v-html="instance.description"></div></template> | ||||
| 			</MkKeyValue> | ||||
| 				<MkKeyValue> | ||||
| 					<template #key>{{ i18n.ts.description }}</template> | ||||
| 					<template #value><div v-html="instance.description"></div></template> | ||||
| 				</MkKeyValue> | ||||
|  | ||||
| 			<FormSection> | ||||
| 				<div class="_gaps_m"> | ||||
| 					<MkKeyValue :copy="version"> | ||||
| 						<template #key>Misskey</template> | ||||
| 						<template #value>{{ version }}</template> | ||||
| 					</MkKeyValue> | ||||
| 					<div v-html="i18n.t('poweredByMisskeyDescription', { name: instance.name ?? host })"> | ||||
| 					</div> | ||||
| 					<FormLink to="/about-misskey">{{ i18n.ts.aboutMisskey }}</FormLink> | ||||
| 				</div> | ||||
| 			</FormSection> | ||||
|  | ||||
| 			<FormSection> | ||||
| 				<div class="_gaps_m"> | ||||
| 					<FormSplit> | ||||
| 						<MkKeyValue> | ||||
| 							<template #key>{{ i18n.ts.administrator }}</template> | ||||
| 							<template #value>{{ instance.maintainerName }}</template> | ||||
| 						</MkKeyValue> | ||||
| 						<MkKeyValue> | ||||
| 							<template #key>{{ i18n.ts.contact }}</template> | ||||
| 							<template #value>{{ instance.maintainerEmail }}</template> | ||||
| 						</MkKeyValue> | ||||
| 					</FormSplit> | ||||
| 					<FormLink v-if="instance.impressumUrl" :to="instance.impressumUrl" external>{{ i18n.ts.impressum }}</FormLink> | ||||
| 					<div class="_gaps_s"> | ||||
| 						<MkFolder v-if="instance.serverRules.length > 0"> | ||||
| 							<template #label>{{ i18n.ts.serverRules }}</template> | ||||
|  | ||||
| 							<ol class="_gaps_s" :class="$style.rules"> | ||||
| 								<li v-for="item, index in instance.serverRules" :key="index" :class="$style.rule"><div :class="$style.ruleText" v-html="item"></div></li> | ||||
| 							</ol> | ||||
| 						</MkFolder> | ||||
| 						<FormLink v-if="instance.tosUrl" :to="instance.tosUrl" external>{{ i18n.ts.termsOfService }}</FormLink> | ||||
| 						<FormLink v-if="instance.privacyPolicyUrl" :to="instance.privacyPolicyUrl" external>{{ i18n.ts.privacyPolicy }}</FormLink> | ||||
| 					</div> | ||||
| 				</div> | ||||
| 			</FormSection> | ||||
|  | ||||
| 			<FormSuspense :p="initStats"> | ||||
| 				<FormSection> | ||||
| 					<template #label>{{ i18n.ts.statistics }}</template> | ||||
| 					<FormSplit> | ||||
| 						<MkKeyValue> | ||||
| 							<template #key>{{ i18n.ts.users }}</template> | ||||
| 							<template #value>{{ number(stats.originalUsersCount) }}</template> | ||||
| 					<div class="_gaps_m"> | ||||
| 						<MkKeyValue :copy="version"> | ||||
| 							<template #key>Misskey</template> | ||||
| 							<template #value>{{ version }}</template> | ||||
| 						</MkKeyValue> | ||||
| 						<MkKeyValue> | ||||
| 							<template #key>{{ i18n.ts.notes }}</template> | ||||
| 							<template #value>{{ number(stats.originalNotesCount) }}</template> | ||||
| 						</MkKeyValue> | ||||
| 					</FormSplit> | ||||
| 						<div v-html="i18n.t('poweredByMisskeyDescription', { name: instance.name ?? host })"> | ||||
| 						</div> | ||||
| 						<FormLink to="/about-misskey">{{ i18n.ts.aboutMisskey }}</FormLink> | ||||
| 					</div> | ||||
| 				</FormSection> | ||||
| 			</FormSuspense> | ||||
|  | ||||
| 			<FormSection> | ||||
| 				<template #label>Well-known resources</template> | ||||
| 				<div class="_gaps_s"> | ||||
| 					<FormLink :to="`/.well-known/host-meta`" external>host-meta</FormLink> | ||||
| 					<FormLink :to="`/.well-known/host-meta.json`" external>host-meta.json</FormLink> | ||||
| 					<FormLink :to="`/.well-known/nodeinfo`" external>nodeinfo</FormLink> | ||||
| 					<FormLink :to="`/robots.txt`" external>robots.txt</FormLink> | ||||
| 					<FormLink :to="`/manifest.json`" external>manifest.json</FormLink> | ||||
| 				</div> | ||||
| 			</FormSection> | ||||
| 		</div> | ||||
| 	</MkSpacer> | ||||
| 	<MkSpacer v-else-if="tab === 'emojis'" :contentMax="1000" :marginMin="20"> | ||||
| 		<XEmojis/> | ||||
| 	</MkSpacer> | ||||
| 	<MkSpacer v-else-if="tab === 'federation'" :contentMax="1000" :marginMin="20"> | ||||
| 		<XFederation/> | ||||
| 	</MkSpacer> | ||||
| 	<MkSpacer v-else-if="tab === 'charts'" :contentMax="1000" :marginMin="20"> | ||||
| 		<MkInstanceStats/> | ||||
| 	</MkSpacer> | ||||
| 				<FormSection> | ||||
| 					<div class="_gaps_m"> | ||||
| 						<FormSplit> | ||||
| 							<MkKeyValue> | ||||
| 								<template #key>{{ i18n.ts.administrator }}</template> | ||||
| 								<template #value>{{ instance.maintainerName }}</template> | ||||
| 							</MkKeyValue> | ||||
| 							<MkKeyValue> | ||||
| 								<template #key>{{ i18n.ts.contact }}</template> | ||||
| 								<template #value>{{ instance.maintainerEmail }}</template> | ||||
| 							</MkKeyValue> | ||||
| 						</FormSplit> | ||||
| 						<FormLink v-if="instance.impressumUrl" :to="instance.impressumUrl" external>{{ i18n.ts.impressum }}</FormLink> | ||||
| 						<div class="_gaps_s"> | ||||
| 							<MkFolder v-if="instance.serverRules.length > 0"> | ||||
| 								<template #label>{{ i18n.ts.serverRules }}</template> | ||||
|  | ||||
| 								<ol class="_gaps_s" :class="$style.rules"> | ||||
| 									<li v-for="item, index in instance.serverRules" :key="index" :class="$style.rule"><div :class="$style.ruleText" v-html="item"></div></li> | ||||
| 								</ol> | ||||
| 							</MkFolder> | ||||
| 							<FormLink v-if="instance.tosUrl" :to="instance.tosUrl" external>{{ i18n.ts.termsOfService }}</FormLink> | ||||
| 							<FormLink v-if="instance.privacyPolicyUrl" :to="instance.privacyPolicyUrl" external>{{ i18n.ts.privacyPolicy }}</FormLink> | ||||
| 						</div> | ||||
| 					</div> | ||||
| 				</FormSection> | ||||
|  | ||||
| 				<FormSuspense :p="initStats"> | ||||
| 					<FormSection> | ||||
| 						<template #label>{{ i18n.ts.statistics }}</template> | ||||
| 						<FormSplit> | ||||
| 							<MkKeyValue> | ||||
| 								<template #key>{{ i18n.ts.users }}</template> | ||||
| 								<template #value>{{ number(stats.originalUsersCount) }}</template> | ||||
| 							</MkKeyValue> | ||||
| 							<MkKeyValue> | ||||
| 								<template #key>{{ i18n.ts.notes }}</template> | ||||
| 								<template #value>{{ number(stats.originalNotesCount) }}</template> | ||||
| 							</MkKeyValue> | ||||
| 						</FormSplit> | ||||
| 					</FormSection> | ||||
| 				</FormSuspense> | ||||
|  | ||||
| 				<FormSection> | ||||
| 					<template #label>Well-known resources</template> | ||||
| 					<div class="_gaps_s"> | ||||
| 						<FormLink :to="`/.well-known/host-meta`" external>host-meta</FormLink> | ||||
| 						<FormLink :to="`/.well-known/host-meta.json`" external>host-meta.json</FormLink> | ||||
| 						<FormLink :to="`/.well-known/nodeinfo`" external>nodeinfo</FormLink> | ||||
| 						<FormLink :to="`/robots.txt`" external>robots.txt</FormLink> | ||||
| 						<FormLink :to="`/manifest.json`" external>manifest.json</FormLink> | ||||
| 					</div> | ||||
| 				</FormSection> | ||||
| 			</div> | ||||
| 		</MkSpacer> | ||||
| 		<MkSpacer v-else-if="tab === 'emojis'" :contentMax="1000" :marginMin="20"> | ||||
| 			<XEmojis/> | ||||
| 		</MkSpacer> | ||||
| 		<MkSpacer v-else-if="tab === 'federation'" :contentMax="1000" :marginMin="20"> | ||||
| 			<XFederation/> | ||||
| 		</MkSpacer> | ||||
| 		<MkSpacer v-else-if="tab === 'charts'" :contentMax="1000" :marginMin="20"> | ||||
| 			<MkInstanceStats/> | ||||
| 		</MkSpacer> | ||||
| 	</MkHorizontalSwipe> | ||||
| </MkStickyContainer> | ||||
| </template> | ||||
|  | ||||
| @@ -114,6 +116,7 @@ import FormSplit from '@/components/form/split.vue'; | ||||
| import MkFolder from '@/components/MkFolder.vue'; | ||||
| import MkKeyValue from '@/components/MkKeyValue.vue'; | ||||
| import MkInstanceStats from '@/components/MkInstanceStats.vue'; | ||||
| import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue'; | ||||
| import { misskeyApi } from '@/scripts/misskey-api.js'; | ||||
| import number from '@/filters/number.js'; | ||||
| import { i18n } from '@/i18n.js'; | ||||
|   | ||||
| @@ -7,34 +7,36 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| <MkStickyContainer> | ||||
| 	<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template> | ||||
| 	<MkSpacer :contentMax="800"> | ||||
| 		<div class="_gaps"> | ||||
| 			<MkInfo v-if="$i && $i.hasUnreadAnnouncement && tab === 'current'" warn>{{ i18n.ts.youHaveUnreadAnnouncements }}</MkInfo> | ||||
| 			<MkPagination ref="paginationEl" :key="tab" v-slot="{items}" :pagination="tab === 'current' ? paginationCurrent : paginationPast" class="_gaps"> | ||||
| 				<section v-for="announcement in items" :key="announcement.id" class="_panel" :class="$style.announcement"> | ||||
| 					<div v-if="announcement.forYou" :class="$style.forYou"><i class="ti ti-pin"></i> {{ i18n.ts.forYou }}</div> | ||||
| 					<div :class="$style.header"> | ||||
| 						<span v-if="$i && !announcement.silence && !announcement.isRead" style="margin-right: 0.5em;">🆕</span> | ||||
| 						<span style="margin-right: 0.5em;"> | ||||
| 							<i v-if="announcement.icon === 'info'" class="ti ti-info-circle"></i> | ||||
| 							<i v-else-if="announcement.icon === 'warning'" class="ti ti-alert-triangle" style="color: var(--warn);"></i> | ||||
| 							<i v-else-if="announcement.icon === 'error'" class="ti ti-circle-x" style="color: var(--error);"></i> | ||||
| 							<i v-else-if="announcement.icon === 'success'" class="ti ti-check" style="color: var(--success);"></i> | ||||
| 						</span> | ||||
| 						<Mfm :text="announcement.title"/> | ||||
| 					</div> | ||||
| 					<div :class="$style.content"> | ||||
| 						<Mfm :text="announcement.text"/> | ||||
| 						<img v-if="announcement.imageUrl" :src="announcement.imageUrl"/> | ||||
| 						<div style="margin-top: 8px; opacity: 0.7; font-size: 85%;"> | ||||
| 							<MkTime :time="announcement.updatedAt ?? announcement.createdAt" mode="detail"/> | ||||
| 		<MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs"> | ||||
| 			<div :key="tab" class="_gaps"> | ||||
| 				<MkInfo v-if="$i && $i.hasUnreadAnnouncement && tab === 'current'" warn>{{ i18n.ts.youHaveUnreadAnnouncements }}</MkInfo> | ||||
| 				<MkPagination ref="paginationEl" :key="tab" v-slot="{items}" :pagination="tab === 'current' ? paginationCurrent : paginationPast" class="_gaps"> | ||||
| 					<section v-for="announcement in items" :key="announcement.id" class="_panel" :class="$style.announcement"> | ||||
| 						<div v-if="announcement.forYou" :class="$style.forYou"><i class="ti ti-pin"></i> {{ i18n.ts.forYou }}</div> | ||||
| 						<div :class="$style.header"> | ||||
| 							<span v-if="$i && !announcement.silence && !announcement.isRead" style="margin-right: 0.5em;">🆕</span> | ||||
| 							<span style="margin-right: 0.5em;"> | ||||
| 								<i v-if="announcement.icon === 'info'" class="ti ti-info-circle"></i> | ||||
| 								<i v-else-if="announcement.icon === 'warning'" class="ti ti-alert-triangle" style="color: var(--warn);"></i> | ||||
| 								<i v-else-if="announcement.icon === 'error'" class="ti ti-circle-x" style="color: var(--error);"></i> | ||||
| 								<i v-else-if="announcement.icon === 'success'" class="ti ti-check" style="color: var(--success);"></i> | ||||
| 							</span> | ||||
| 							<Mfm :text="announcement.title"/> | ||||
| 						</div> | ||||
| 					</div> | ||||
| 					<div v-if="$i && !announcement.silence && !announcement.isRead" :class="$style.footer"> | ||||
| 						<MkButton primary @click="read(announcement)"><i class="ti ti-check"></i> {{ i18n.ts.gotIt }}</MkButton> | ||||
| 					</div> | ||||
| 				</section> | ||||
| 			</MkPagination> | ||||
| 		</div> | ||||
| 						<div :class="$style.content"> | ||||
| 							<Mfm :text="announcement.text"/> | ||||
| 							<img v-if="announcement.imageUrl" :src="announcement.imageUrl"/> | ||||
| 							<div style="margin-top: 8px; opacity: 0.7; font-size: 85%;"> | ||||
| 								<MkTime :time="announcement.updatedAt ?? announcement.createdAt" mode="detail"/> | ||||
| 							</div> | ||||
| 						</div> | ||||
| 						<div v-if="$i && !announcement.silence && !announcement.isRead" :class="$style.footer"> | ||||
| 							<MkButton primary @click="read(announcement)"><i class="ti ti-check"></i> {{ i18n.ts.gotIt }}</MkButton> | ||||
| 						</div> | ||||
| 					</section> | ||||
| 				</MkPagination> | ||||
| 			</div> | ||||
| 		</MkHorizontalSwipe> | ||||
| 	</MkSpacer> | ||||
| </MkStickyContainer> | ||||
| </template> | ||||
| @@ -44,6 +46,7 @@ import { ref, computed, watch } from 'vue'; | ||||
| import MkPagination from '@/components/MkPagination.vue'; | ||||
| import MkButton from '@/components/MkButton.vue'; | ||||
| import MkInfo from '@/components/MkInfo.vue'; | ||||
| import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue'; | ||||
| import * as os from '@/os.js'; | ||||
| import { misskeyApi } from '@/scripts/misskey-api.js'; | ||||
| import { i18n } from '@/i18n.js'; | ||||
|   | ||||
| @@ -7,53 +7,55 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| <MkStickyContainer> | ||||
| 	<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template> | ||||
| 	<MkSpacer :contentMax="700" :class="$style.main"> | ||||
| 		<div v-if="channel && tab === 'overview'" class="_gaps"> | ||||
| 			<div class="_panel" :class="$style.bannerContainer"> | ||||
| 				<XChannelFollowButton :channel="channel" :full="true" :class="$style.subscribe"/> | ||||
| 				<MkButton v-if="favorited" v-tooltip="i18n.ts.unfavorite" asLike class="button" rounded primary :class="$style.favorite" @click="unfavorite()"><i class="ti ti-star"></i></MkButton> | ||||
| 				<MkButton v-else v-tooltip="i18n.ts.favorite" asLike class="button" rounded :class="$style.favorite" @click="favorite()"><i class="ti ti-star"></i></MkButton> | ||||
| 				<div :style="{ backgroundImage: channel.bannerUrl ? `url(${channel.bannerUrl})` : undefined }" :class="$style.banner"> | ||||
| 					<div :class="$style.bannerStatus"> | ||||
| 						<div><i class="ti ti-users ti-fw"></i><I18n :src="i18n.ts._channel.usersCount" tag="span" style="margin-left: 4px;"><template #n><b>{{ channel.usersCount }}</b></template></I18n></div> | ||||
| 						<div><i class="ti ti-pencil ti-fw"></i><I18n :src="i18n.ts._channel.notesCount" tag="span" style="margin-left: 4px;"><template #n><b>{{ channel.notesCount }}</b></template></I18n></div> | ||||
| 		<MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs"> | ||||
| 			<div v-if="channel && tab === 'overview'" key="overview" class="_gaps"> | ||||
| 				<div class="_panel" :class="$style.bannerContainer"> | ||||
| 					<XChannelFollowButton :channel="channel" :full="true" :class="$style.subscribe"/> | ||||
| 					<MkButton v-if="favorited" v-tooltip="i18n.ts.unfavorite" asLike class="button" rounded primary :class="$style.favorite" @click="unfavorite()"><i class="ti ti-star"></i></MkButton> | ||||
| 					<MkButton v-else v-tooltip="i18n.ts.favorite" asLike class="button" rounded :class="$style.favorite" @click="favorite()"><i class="ti ti-star"></i></MkButton> | ||||
| 					<div :style="{ backgroundImage: channel.bannerUrl ? `url(${channel.bannerUrl})` : undefined }" :class="$style.banner"> | ||||
| 						<div :class="$style.bannerStatus"> | ||||
| 							<div><i class="ti ti-users ti-fw"></i><I18n :src="i18n.ts._channel.usersCount" tag="span" style="margin-left: 4px;"><template #n><b>{{ channel.usersCount }}</b></template></I18n></div> | ||||
| 							<div><i class="ti ti-pencil ti-fw"></i><I18n :src="i18n.ts._channel.notesCount" tag="span" style="margin-left: 4px;"><template #n><b>{{ channel.notesCount }}</b></template></I18n></div> | ||||
| 						</div> | ||||
| 						<div v-if="channel.isSensitive" :class="$style.sensitiveIndicator">{{ i18n.ts.sensitive }}</div> | ||||
| 						<div :class="$style.bannerFade"></div> | ||||
| 					</div> | ||||
| 					<div v-if="channel.description" :class="$style.description"> | ||||
| 						<Mfm :text="channel.description" :isNote="false"/> | ||||
| 					</div> | ||||
| 					<div v-if="channel.isSensitive" :class="$style.sensitiveIndicator">{{ i18n.ts.sensitive }}</div> | ||||
| 					<div :class="$style.bannerFade"></div> | ||||
| 				</div> | ||||
| 				<div v-if="channel.description" :class="$style.description"> | ||||
| 					<Mfm :text="channel.description" :isNote="false"/> | ||||
|  | ||||
| 				<MkFoldableSection> | ||||
| 					<template #header><i class="ti ti-pin ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.pinnedNotes }}</template> | ||||
| 					<div v-if="channel.pinnedNotes && channel.pinnedNotes.length > 0" class="_gaps"> | ||||
| 						<MkNote v-for="note in channel.pinnedNotes" :key="note.id" class="_panel" :note="note"/> | ||||
| 					</div> | ||||
| 				</MkFoldableSection> | ||||
| 			</div> | ||||
| 			<div v-if="channel && tab === 'timeline'" key="timeline" class="_gaps"> | ||||
| 				<MkInfo v-if="channel.isArchived" warn>{{ i18n.ts.thisChannelArchived }}</MkInfo> | ||||
|  | ||||
| 				<!-- スマホ・タブレットの場合、キーボードが表示されると投稿が見づらくなるので、デスクトップ場合のみ自動でフォーカスを当てる --> | ||||
| 				<MkPostForm v-if="$i && defaultStore.reactiveState.showFixedPostFormInChannel.value" :channel="channel" class="post-form _panel" fixed :autofocus="deviceKind === 'desktop'"/> | ||||
|  | ||||
| 				<MkTimeline :key="channelId" src="channel" :channel="channelId" @before="before" @after="after" @note="miLocalStorage.setItemAsJson(`channelLastReadedAt:${channel.id}`, Date.now())"/> | ||||
| 			</div> | ||||
| 			<div v-else-if="tab === 'featured'" key="featured"> | ||||
| 				<MkNotes :pagination="featuredPagination"/> | ||||
| 			</div> | ||||
| 			<div v-else-if="tab === 'search'" key="search"> | ||||
| 				<div class="_gaps"> | ||||
| 					<div> | ||||
| 						<MkInput v-model="searchQuery" @enter="search()"> | ||||
| 							<template #prefix><i class="ti ti-search"></i></template> | ||||
| 						</MkInput> | ||||
| 						<MkButton primary rounded style="margin-top: 8px;" @click="search()">{{ i18n.ts.search }}</MkButton> | ||||
| 					</div> | ||||
| 					<MkNotes v-if="searchPagination" :key="searchKey" :pagination="searchPagination"/> | ||||
| 				</div> | ||||
| 			</div> | ||||
|  | ||||
| 			<MkFoldableSection> | ||||
| 				<template #header><i class="ti ti-pin ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.pinnedNotes }}</template> | ||||
| 				<div v-if="channel.pinnedNotes && channel.pinnedNotes.length > 0" class="_gaps"> | ||||
| 					<MkNote v-for="note in channel.pinnedNotes" :key="note.id" class="_panel" :note="note"/> | ||||
| 				</div> | ||||
| 			</MkFoldableSection> | ||||
| 		</div> | ||||
| 		<div v-if="channel && tab === 'timeline'" class="_gaps"> | ||||
| 			<MkInfo v-if="channel.isArchived" warn>{{ i18n.ts.thisChannelArchived }}</MkInfo> | ||||
|  | ||||
| 			<!-- スマホ・タブレットの場合、キーボードが表示されると投稿が見づらくなるので、デスクトップ場合のみ自動でフォーカスを当てる --> | ||||
| 			<MkPostForm v-if="$i && defaultStore.reactiveState.showFixedPostFormInChannel.value" :channel="channel" class="post-form _panel" fixed :autofocus="deviceKind === 'desktop'"/> | ||||
|  | ||||
| 			<MkTimeline :key="channelId" src="channel" :channel="channelId" @before="before" @after="after" @note="miLocalStorage.setItemAsJson(`channelLastReadedAt:${channel.id}`, Date.now())"/> | ||||
| 		</div> | ||||
| 		<div v-else-if="tab === 'featured'"> | ||||
| 			<MkNotes :pagination="featuredPagination"/> | ||||
| 		</div> | ||||
| 		<div v-else-if="tab === 'search'"> | ||||
| 			<div class="_gaps"> | ||||
| 				<div> | ||||
| 					<MkInput v-model="searchQuery" @enter="search()"> | ||||
| 						<template #prefix><i class="ti ti-search"></i></template> | ||||
| 					</MkInput> | ||||
| 					<MkButton primary rounded style="margin-top: 8px;" @click="search()">{{ i18n.ts.search }}</MkButton> | ||||
| 				</div> | ||||
| 				<MkNotes v-if="searchPagination" :key="searchKey" :pagination="searchPagination"/> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 		</MkHorizontalSwipe> | ||||
| 	</MkSpacer> | ||||
| 	<template #footer> | ||||
| 		<div :class="$style.footer"> | ||||
| @@ -87,6 +89,7 @@ import { defaultStore } from '@/store.js'; | ||||
| import MkNote from '@/components/MkNote.vue'; | ||||
| import MkInfo from '@/components/MkInfo.vue'; | ||||
| import MkFoldableSection from '@/components/MkFoldableSection.vue'; | ||||
| import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue'; | ||||
| import { PageHeaderItem } from '@/types/page-header.js'; | ||||
| import { isSupportShare } from '@/scripts/navigator.js'; | ||||
| import copyToClipboard from '@/scripts/copy-to-clipboard.js'; | ||||
| @@ -100,6 +103,7 @@ const props = defineProps<{ | ||||
| }>(); | ||||
|  | ||||
| const tab = ref('overview'); | ||||
|  | ||||
| const channel = ref<Misskey.entities.Channel | null>(null); | ||||
| const favorited = ref(false); | ||||
| const searchQuery = ref(''); | ||||
|   | ||||
| @@ -7,44 +7,46 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| <MkStickyContainer> | ||||
| 	<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template> | ||||
| 	<MkSpacer :contentMax="700"> | ||||
| 		<div v-if="tab === 'search'"> | ||||
| 			<div class="_gaps"> | ||||
| 				<MkInput v-model="searchQuery" :large="true" :autofocus="true" type="search" @enter="search"> | ||||
| 					<template #prefix><i class="ti ti-search"></i></template> | ||||
| 				</MkInput> | ||||
| 				<MkRadios v-model="searchType" @update:modelValue="search()"> | ||||
| 					<option value="nameAndDescription">{{ i18n.ts._channel.nameAndDescription }}</option> | ||||
| 					<option value="nameOnly">{{ i18n.ts._channel.nameOnly }}</option> | ||||
| 				</MkRadios> | ||||
| 				<MkButton large primary gradate rounded @click="search">{{ i18n.ts.search }}</MkButton> | ||||
| 			</div> | ||||
| 		<MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs"> | ||||
| 			<div v-if="tab === 'search'" key="search"> | ||||
| 				<div class="_gaps"> | ||||
| 					<MkInput v-model="searchQuery" :large="true" :autofocus="true" type="search" @enter="search"> | ||||
| 						<template #prefix><i class="ti ti-search"></i></template> | ||||
| 					</MkInput> | ||||
| 					<MkRadios v-model="searchType" @update:modelValue="search()"> | ||||
| 						<option value="nameAndDescription">{{ i18n.ts._channel.nameAndDescription }}</option> | ||||
| 						<option value="nameOnly">{{ i18n.ts._channel.nameOnly }}</option> | ||||
| 					</MkRadios> | ||||
| 					<MkButton large primary gradate rounded @click="search">{{ i18n.ts.search }}</MkButton> | ||||
| 				</div> | ||||
|  | ||||
| 			<MkFoldableSection v-if="channelPagination"> | ||||
| 				<template #header>{{ i18n.ts.searchResult }}</template> | ||||
| 				<MkChannelList :key="key" :pagination="channelPagination"/> | ||||
| 			</MkFoldableSection> | ||||
| 		</div> | ||||
| 		<div v-if="tab === 'featured'"> | ||||
| 			<MkPagination v-slot="{items}" :pagination="featuredPagination"> | ||||
| 				<MkChannelPreview v-for="channel in items" :key="channel.id" class="_margin" :channel="channel"/> | ||||
| 			</MkPagination> | ||||
| 		</div> | ||||
| 		<div v-else-if="tab === 'favorites'"> | ||||
| 			<MkPagination v-slot="{items}" :pagination="favoritesPagination"> | ||||
| 				<MkChannelPreview v-for="channel in items" :key="channel.id" class="_margin" :channel="channel"/> | ||||
| 			</MkPagination> | ||||
| 		</div> | ||||
| 		<div v-else-if="tab === 'following'"> | ||||
| 			<MkPagination v-slot="{items}" :pagination="followingPagination"> | ||||
| 				<MkChannelPreview v-for="channel in items" :key="channel.id" class="_margin" :channel="channel"/> | ||||
| 			</MkPagination> | ||||
| 		</div> | ||||
| 		<div v-else-if="tab === 'owned'"> | ||||
| 			<MkButton class="new" @click="create()"><i class="ti ti-plus"></i></MkButton> | ||||
| 			<MkPagination v-slot="{items}" :pagination="ownedPagination"> | ||||
| 				<MkChannelPreview v-for="channel in items" :key="channel.id" class="_margin" :channel="channel"/> | ||||
| 			</MkPagination> | ||||
| 		</div> | ||||
| 				<MkFoldableSection v-if="channelPagination"> | ||||
| 					<template #header>{{ i18n.ts.searchResult }}</template> | ||||
| 					<MkChannelList :key="key" :pagination="channelPagination"/> | ||||
| 				</MkFoldableSection> | ||||
| 			</div> | ||||
| 			<div v-if="tab === 'featured'" key="featured"> | ||||
| 				<MkPagination v-slot="{items}" :pagination="featuredPagination"> | ||||
| 					<MkChannelPreview v-for="channel in items" :key="channel.id" class="_margin" :channel="channel"/> | ||||
| 				</MkPagination> | ||||
| 			</div> | ||||
| 			<div v-else-if="tab === 'favorites'" key="favorites"> | ||||
| 				<MkPagination v-slot="{items}" :pagination="favoritesPagination"> | ||||
| 					<MkChannelPreview v-for="channel in items" :key="channel.id" class="_margin" :channel="channel"/> | ||||
| 				</MkPagination> | ||||
| 			</div> | ||||
| 			<div v-else-if="tab === 'following'" key="following"> | ||||
| 				<MkPagination v-slot="{items}" :pagination="followingPagination"> | ||||
| 					<MkChannelPreview v-for="channel in items" :key="channel.id" class="_margin" :channel="channel"/> | ||||
| 				</MkPagination> | ||||
| 			</div> | ||||
| 			<div v-else-if="tab === 'owned'" key="owned"> | ||||
| 				<MkButton class="new" @click="create()"><i class="ti ti-plus"></i></MkButton> | ||||
| 				<MkPagination v-slot="{items}" :pagination="ownedPagination"> | ||||
| 					<MkChannelPreview v-for="channel in items" :key="channel.id" class="_margin" :channel="channel"/> | ||||
| 				</MkPagination> | ||||
| 			</div> | ||||
| 		</MkHorizontalSwipe> | ||||
| 	</MkSpacer> | ||||
| </MkStickyContainer> | ||||
| </template> | ||||
| @@ -58,6 +60,7 @@ import MkInput from '@/components/MkInput.vue'; | ||||
| import MkRadios from '@/components/MkRadios.vue'; | ||||
| import MkButton from '@/components/MkButton.vue'; | ||||
| import MkFoldableSection from '@/components/MkFoldableSection.vue'; | ||||
| import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata.js'; | ||||
| import { i18n } from '@/i18n.js'; | ||||
| import { useRouter } from '@/global/router/supplier.js'; | ||||
|   | ||||
| @@ -9,13 +9,15 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| 		<MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/> | ||||
| 	</template> | ||||
|  | ||||
| 	<MkSpacer v-if="tab === 'info'" :contentMax="800"> | ||||
| 		<XFileInfo :fileId="fileId"/> | ||||
| 	</MkSpacer> | ||||
| 	<MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs"> | ||||
| 		<MkSpacer v-if="tab === 'info'" key="info" :contentMax="800"> | ||||
| 			<XFileInfo :fileId="fileId"/> | ||||
| 		</MkSpacer> | ||||
|  | ||||
| 	<MkSpacer v-else-if="tab === 'notes'" :contentMax="800"> | ||||
| 		<XNotes :fileId="fileId"/> | ||||
| 	</MkSpacer> | ||||
| 		<MkSpacer v-else-if="tab === 'notes'" key="notes" :contentMax="800"> | ||||
| 			<XNotes :fileId="fileId"/> | ||||
| 		</MkSpacer> | ||||
| 	</MkHorizontalSwipe> | ||||
| </MkStickyContainer> | ||||
| </template> | ||||
|  | ||||
| @@ -23,6 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| import { computed, ref, defineAsyncComponent } from 'vue'; | ||||
| import { i18n } from '@/i18n.js'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata.js'; | ||||
| import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue'; | ||||
|  | ||||
| const props = defineProps<{ | ||||
| 	fileId: string; | ||||
|   | ||||
| @@ -6,17 +6,17 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| <template> | ||||
| <MkStickyContainer> | ||||
| 	<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template> | ||||
| 	<div> | ||||
| 		<div v-if="tab === 'featured'"> | ||||
| 	<MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs"> | ||||
| 		<div v-if="tab === 'featured'" key="featured"> | ||||
| 			<XFeatured/> | ||||
| 		</div> | ||||
| 		<div v-else-if="tab === 'users'"> | ||||
| 		<div v-else-if="tab === 'users'" key="users"> | ||||
| 			<XUsers/> | ||||
| 		</div> | ||||
| 		<div v-else-if="tab === 'roles'"> | ||||
| 		<div v-else-if="tab === 'roles'" key="roles"> | ||||
| 			<XRoles/> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	</MkHorizontalSwipe> | ||||
| </MkStickyContainer> | ||||
| </template> | ||||
|  | ||||
| @@ -26,6 +26,7 @@ import XFeatured from './explore.featured.vue'; | ||||
| import XUsers from './explore.users.vue'; | ||||
| import XRoles from './explore.roles.vue'; | ||||
| import MkFoldableSection from '@/components/MkFoldableSection.vue'; | ||||
| import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata.js'; | ||||
| import { i18n } from '@/i18n.js'; | ||||
|  | ||||
|   | ||||
| @@ -7,32 +7,34 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| <MkStickyContainer> | ||||
| 	<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template> | ||||
| 	<MkSpacer :contentMax="700"> | ||||
| 		<div v-if="tab === 'featured'"> | ||||
| 			<MkPagination v-slot="{items}" :pagination="featuredFlashsPagination"> | ||||
| 				<div class="_gaps_s"> | ||||
| 					<MkFlashPreview v-for="flash in items" :key="flash.id" :flash="flash"/> | ||||
| 				</div> | ||||
| 			</MkPagination> | ||||
| 		</div> | ||||
|  | ||||
| 		<div v-else-if="tab === 'my'"> | ||||
| 			<div class="_gaps"> | ||||
| 				<MkButton gradate rounded style="margin: 0 auto;" @click="create()"><i class="ti ti-plus"></i></MkButton> | ||||
| 				<MkPagination v-slot="{items}" :pagination="myFlashsPagination"> | ||||
| 		<MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs"> | ||||
| 			<div v-if="tab === 'featured'" key="featured"> | ||||
| 				<MkPagination v-slot="{items}" :pagination="featuredFlashsPagination"> | ||||
| 					<div class="_gaps_s"> | ||||
| 						<MkFlashPreview v-for="flash in items" :key="flash.id" :flash="flash"/> | ||||
| 					</div> | ||||
| 				</MkPagination> | ||||
| 			</div> | ||||
| 		</div> | ||||
|  | ||||
| 		<div v-else-if="tab === 'liked'"> | ||||
| 			<MkPagination v-slot="{items}" :pagination="likedFlashsPagination"> | ||||
| 				<div class="_gaps_s"> | ||||
| 					<MkFlashPreview v-for="like in items" :key="like.flash.id" :flash="like.flash"/> | ||||
| 			<div v-else-if="tab === 'my'" key="my"> | ||||
| 				<div class="_gaps"> | ||||
| 					<MkButton gradate rounded style="margin: 0 auto;" @click="create()"><i class="ti ti-plus"></i></MkButton> | ||||
| 					<MkPagination v-slot="{items}" :pagination="myFlashsPagination"> | ||||
| 						<div class="_gaps_s"> | ||||
| 							<MkFlashPreview v-for="flash in items" :key="flash.id" :flash="flash"/> | ||||
| 						</div> | ||||
| 					</MkPagination> | ||||
| 				</div> | ||||
| 			</MkPagination> | ||||
| 		</div> | ||||
| 			</div> | ||||
|  | ||||
| 			<div v-else-if="tab === 'liked'" key="liked"> | ||||
| 				<MkPagination v-slot="{items}" :pagination="likedFlashsPagination"> | ||||
| 					<div class="_gaps_s"> | ||||
| 						<MkFlashPreview v-for="like in items" :key="like.flash.id" :flash="like.flash"/> | ||||
| 					</div> | ||||
| 				</MkPagination> | ||||
| 			</div> | ||||
| 		</MkHorizontalSwipe> | ||||
| 	</MkSpacer> | ||||
| </MkStickyContainer> | ||||
| </template> | ||||
| @@ -42,6 +44,7 @@ import { computed, ref } from 'vue'; | ||||
| import MkFlashPreview from '@/components/MkFlashPreview.vue'; | ||||
| import MkPagination from '@/components/MkPagination.vue'; | ||||
| import MkButton from '@/components/MkButton.vue'; | ||||
| import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue'; | ||||
| import { i18n } from '@/i18n.js'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata.js'; | ||||
| import { useRouter } from '@/global/router/supplier.js'; | ||||
|   | ||||
| @@ -37,7 +37,7 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| 					<template #icon><i class="ti ti-code"></i></template> | ||||
| 					<template #label>{{ i18n.ts._play.viewSource }}</template> | ||||
|  | ||||
| 					<MkCode :code="flash.script" lang="is" :inline="false" class="_monospace"/> | ||||
| 					<MkCode :code="flash.script" lang="is" class="_monospace"/> | ||||
| 				</MkFolder> | ||||
| 				<div :class="$style.footer"> | ||||
| 					<Mfm :text="`By @${flash.user.username}`"/> | ||||
|   | ||||
| @@ -7,8 +7,8 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| <MkStickyContainer> | ||||
| 	<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template> | ||||
| 	<MkSpacer :contentMax="1400"> | ||||
| 		<div class="_root"> | ||||
| 			<div v-if="tab === 'explore'"> | ||||
| 		<MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs"> | ||||
| 			<div v-if="tab === 'explore'" key="explore"> | ||||
| 				<MkFoldableSection class="_margin"> | ||||
| 					<template #header><i class="ti ti-clock"></i>{{ i18n.ts.recentPosts }}</template> | ||||
| 					<MkPagination v-slot="{items}" :pagination="recentPostsPagination" :disableAutoLoad="true"> | ||||
| @@ -26,14 +26,14 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| 					</MkPagination> | ||||
| 				</MkFoldableSection> | ||||
| 			</div> | ||||
| 			<div v-else-if="tab === 'liked'"> | ||||
| 			<div v-else-if="tab === 'liked'" key="liked"> | ||||
| 				<MkPagination v-slot="{items}" :pagination="likedPostsPagination"> | ||||
| 					<div :class="$style.items"> | ||||
| 						<MkGalleryPostPreview v-for="like in items" :key="like.id" :post="like.post" class="post"/> | ||||
| 					</div> | ||||
| 				</MkPagination> | ||||
| 			</div> | ||||
| 			<div v-else-if="tab === 'my'"> | ||||
| 			<div v-else-if="tab === 'my'" key="my"> | ||||
| 				<MkA to="/gallery/new" class="_link" style="margin: 16px;"><i class="ti ti-plus"></i> {{ i18n.ts.postToGallery }}</MkA> | ||||
| 				<MkPagination v-slot="{items}" :pagination="myPostsPagination"> | ||||
| 					<div :class="$style.items"> | ||||
| @@ -41,7 +41,7 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| 					</div> | ||||
| 				</MkPagination> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 		</MkHorizontalSwipe> | ||||
| 	</MkSpacer> | ||||
| </MkStickyContainer> | ||||
| </template> | ||||
| @@ -51,6 +51,7 @@ import { watch, ref, computed } from 'vue'; | ||||
| import MkFoldableSection from '@/components/MkFoldableSection.vue'; | ||||
| import MkPagination from '@/components/MkPagination.vue'; | ||||
| import MkGalleryPostPreview from '@/components/MkGalleryPostPreview.vue'; | ||||
| import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata.js'; | ||||
| import { i18n } from '@/i18n.js'; | ||||
| import { useRouter } from '@/global/router/supplier.js'; | ||||
|   | ||||
| @@ -7,112 +7,114 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| <MkStickyContainer> | ||||
| 	<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template> | ||||
| 	<MkSpacer v-if="instance" :contentMax="600" :marginMin="16" :marginMax="32"> | ||||
| 		<div v-if="tab === 'overview'" class="_gaps_m"> | ||||
| 			<div class="fnfelxur"> | ||||
| 				<img :src="faviconUrl" alt="" class="icon"/> | ||||
| 				<span class="name">{{ instance.name || `(${i18n.ts.unknown})` }}</span> | ||||
| 			</div> | ||||
| 			<div style="display: flex; flex-direction: column; gap: 1em;"> | ||||
| 				<MkKeyValue :copy="host" oneline> | ||||
| 					<template #key>Host</template> | ||||
| 					<template #value><span class="_monospace"><MkLink :url="`https://${host}`">{{ host }}</MkLink></span></template> | ||||
| 				</MkKeyValue> | ||||
| 				<MkKeyValue oneline> | ||||
| 					<template #key>{{ i18n.ts.software }}</template> | ||||
| 					<template #value><span class="_monospace">{{ instance.softwareName || `(${i18n.ts.unknown})` }} / {{ instance.softwareVersion || `(${i18n.ts.unknown})` }}</span></template> | ||||
| 				</MkKeyValue> | ||||
| 				<MkKeyValue oneline> | ||||
| 					<template #key>{{ i18n.ts.administrator }}</template> | ||||
| 					<template #value>{{ instance.maintainerName || `(${i18n.ts.unknown})` }} ({{ instance.maintainerEmail || `(${i18n.ts.unknown})` }})</template> | ||||
| 				</MkKeyValue> | ||||
| 			</div> | ||||
| 			<MkKeyValue> | ||||
| 				<template #key>{{ i18n.ts.description }}</template> | ||||
| 				<template #value>{{ instance.description }}</template> | ||||
| 			</MkKeyValue> | ||||
|  | ||||
| 			<FormSection v-if="iAmModerator"> | ||||
| 				<template #label>Moderation</template> | ||||
| 				<div class="_gaps_s"> | ||||
| 					<MkSwitch v-model="suspended" :disabled="!instance" @update:modelValue="toggleSuspend">{{ i18n.ts.stopActivityDelivery }}</MkSwitch> | ||||
| 					<MkSwitch v-model="isBlocked" :disabled="!meta || !instance" @update:modelValue="toggleBlock">{{ i18n.ts.blockThisInstance }}</MkSwitch> | ||||
| 					<MkSwitch v-model="isSilenced" :disabled="!meta || !instance" @update:modelValue="toggleSilenced">{{ i18n.ts.silenceThisInstance }}</MkSwitch> | ||||
| 					<MkSwitch v-model="isSensitiveMedia" :disabled="!meta || !instance" @update:modelValue="toggleSensitiveMedia">{{ i18n.ts.sensitiveMediaThisInstance }}</MkSwitch> | ||||
| 					<MkButton @click="refreshMetadata"><i class="ti ti-refresh"></i> Refresh metadata</MkButton> | ||||
| 		<MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs"> | ||||
| 			<div v-if="tab === 'overview'" key="overview" class="_gaps_m"> | ||||
| 				<div class="fnfelxur"> | ||||
| 					<img :src="faviconUrl" alt="" class="icon"/> | ||||
| 					<span class="name">{{ instance.name || `(${i18n.ts.unknown})` }}</span> | ||||
| 				</div> | ||||
| 			</FormSection> | ||||
|  | ||||
| 			<FormSection> | ||||
| 				<MkKeyValue oneline style="margin: 1em 0;"> | ||||
| 					<template #key>{{ i18n.ts.registeredAt }}</template> | ||||
| 					<template #value><MkTime mode="detail" :time="instance.firstRetrievedAt"/></template> | ||||
| 				</MkKeyValue> | ||||
| 				<MkKeyValue oneline style="margin: 1em 0;"> | ||||
| 					<template #key>{{ i18n.ts.updatedAt }}</template> | ||||
| 					<template #value><MkTime mode="detail" :time="instance.infoUpdatedAt"/></template> | ||||
| 				</MkKeyValue> | ||||
| 				<MkKeyValue oneline style="margin: 1em 0;"> | ||||
| 					<template #key>{{ i18n.ts.latestRequestReceivedAt }}</template> | ||||
| 					<template #value><MkTime v-if="instance.latestRequestReceivedAt" :time="instance.latestRequestReceivedAt"/><span v-else>N/A</span></template> | ||||
| 				</MkKeyValue> | ||||
| 			</FormSection> | ||||
|  | ||||
| 			<FormSection> | ||||
| 				<MkKeyValue oneline style="margin: 1em 0;"> | ||||
| 					<template #key>Following (Pub)</template> | ||||
| 					<template #value>{{ number(instance.followingCount) }}</template> | ||||
| 				</MkKeyValue> | ||||
| 				<MkKeyValue oneline style="margin: 1em 0;"> | ||||
| 					<template #key>Followers (Sub)</template> | ||||
| 					<template #value>{{ number(instance.followersCount) }}</template> | ||||
| 				</MkKeyValue> | ||||
| 			</FormSection> | ||||
|  | ||||
| 			<FormSection> | ||||
| 				<template #label>Well-known resources</template> | ||||
| 				<FormLink :to="`https://${host}/.well-known/host-meta`" external style="margin-bottom: 8px;">host-meta</FormLink> | ||||
| 				<FormLink :to="`https://${host}/.well-known/host-meta.json`" external style="margin-bottom: 8px;">host-meta.json</FormLink> | ||||
| 				<FormLink :to="`https://${host}/.well-known/nodeinfo`" external style="margin-bottom: 8px;">nodeinfo</FormLink> | ||||
| 				<FormLink :to="`https://${host}/robots.txt`" external style="margin-bottom: 8px;">robots.txt</FormLink> | ||||
| 				<FormLink :to="`https://${host}/manifest.json`" external style="margin-bottom: 8px;">manifest.json</FormLink> | ||||
| 			</FormSection> | ||||
| 		</div> | ||||
| 		<div v-else-if="tab === 'chart'" class="_gaps_m"> | ||||
| 			<div class="cmhjzshl"> | ||||
| 				<div class="selects"> | ||||
| 					<MkSelect v-model="chartSrc" style="margin: 0 10px 0 0; flex: 1;"> | ||||
| 						<option value="instance-requests">{{ i18n.ts._instanceCharts.requests }}</option> | ||||
| 						<option value="instance-users">{{ i18n.ts._instanceCharts.users }}</option> | ||||
| 						<option value="instance-users-total">{{ i18n.ts._instanceCharts.usersTotal }}</option> | ||||
| 						<option value="instance-notes">{{ i18n.ts._instanceCharts.notes }}</option> | ||||
| 						<option value="instance-notes-total">{{ i18n.ts._instanceCharts.notesTotal }}</option> | ||||
| 						<option value="instance-ff">{{ i18n.ts._instanceCharts.ff }}</option> | ||||
| 						<option value="instance-ff-total">{{ i18n.ts._instanceCharts.ffTotal }}</option> | ||||
| 						<option value="instance-drive-usage">{{ i18n.ts._instanceCharts.cacheSize }}</option> | ||||
| 						<option value="instance-drive-usage-total">{{ i18n.ts._instanceCharts.cacheSizeTotal }}</option> | ||||
| 						<option value="instance-drive-files">{{ i18n.ts._instanceCharts.files }}</option> | ||||
| 						<option value="instance-drive-files-total">{{ i18n.ts._instanceCharts.filesTotal }}</option> | ||||
| 					</MkSelect> | ||||
| 				<div style="display: flex; flex-direction: column; gap: 1em;"> | ||||
| 					<MkKeyValue :copy="host" oneline> | ||||
| 						<template #key>Host</template> | ||||
| 						<template #value><span class="_monospace"><MkLink :url="`https://${host}`">{{ host }}</MkLink></span></template> | ||||
| 					</MkKeyValue> | ||||
| 					<MkKeyValue oneline> | ||||
| 						<template #key>{{ i18n.ts.software }}</template> | ||||
| 						<template #value><span class="_monospace">{{ instance.softwareName || `(${i18n.ts.unknown})` }} / {{ instance.softwareVersion || `(${i18n.ts.unknown})` }}</span></template> | ||||
| 					</MkKeyValue> | ||||
| 					<MkKeyValue oneline> | ||||
| 						<template #key>{{ i18n.ts.administrator }}</template> | ||||
| 						<template #value>{{ instance.maintainerName || `(${i18n.ts.unknown})` }} ({{ instance.maintainerEmail || `(${i18n.ts.unknown})` }})</template> | ||||
| 					</MkKeyValue> | ||||
| 				</div> | ||||
| 				<div class="charts"> | ||||
| 					<div class="label">{{ i18n.t('recentNHours', { n: 90 }) }}</div> | ||||
| 					<MkChart class="chart" :src="chartSrc" span="hour" :limit="90" :args="{ host: host }" :detailed="true"></MkChart> | ||||
| 					<div class="label">{{ i18n.t('recentNDays', { n: 90 }) }}</div> | ||||
| 					<MkChart class="chart" :src="chartSrc" span="day" :limit="90" :args="{ host: host }" :detailed="true"></MkChart> | ||||
| 				<MkKeyValue> | ||||
| 					<template #key>{{ i18n.ts.description }}</template> | ||||
| 					<template #value>{{ instance.description }}</template> | ||||
| 				</MkKeyValue> | ||||
|  | ||||
| 				<FormSection v-if="iAmModerator"> | ||||
| 					<template #label>Moderation</template> | ||||
| 					<div class="_gaps_s"> | ||||
| 						<MkSwitch v-model="suspended" :disabled="!instance" @update:modelValue="toggleSuspend">{{ i18n.ts.stopActivityDelivery }}</MkSwitch> | ||||
| 						<MkSwitch v-model="isBlocked" :disabled="!meta || !instance" @update:modelValue="toggleBlock">{{ i18n.ts.blockThisInstance }}</MkSwitch> | ||||
| 						<MkSwitch v-model="isSilenced" :disabled="!meta || !instance" @update:modelValue="toggleSilenced">{{ i18n.ts.silenceThisInstance }}</MkSwitch> | ||||
| 						<MkSwitch v-model="isSensitiveMedia" :disabled="!meta || !instance" @update:modelValue="toggleSensitiveMedia">{{ i18n.ts.sensitiveMediaThisInstance }}</MkSwitch> | ||||
| 						<MkButton @click="refreshMetadata"><i class="ti ti-refresh"></i> Refresh metadata</MkButton> | ||||
| 					</div> | ||||
| 				</FormSection> | ||||
|  | ||||
| 				<FormSection> | ||||
| 					<MkKeyValue oneline style="margin: 1em 0;"> | ||||
| 						<template #key>{{ i18n.ts.registeredAt }}</template> | ||||
| 						<template #value><MkTime mode="detail" :time="instance.firstRetrievedAt"/></template> | ||||
| 					</MkKeyValue> | ||||
| 					<MkKeyValue oneline style="margin: 1em 0;"> | ||||
| 						<template #key>{{ i18n.ts.updatedAt }}</template> | ||||
| 						<template #value><MkTime mode="detail" :time="instance.infoUpdatedAt"/></template> | ||||
| 					</MkKeyValue> | ||||
| 					<MkKeyValue oneline style="margin: 1em 0;"> | ||||
| 						<template #key>{{ i18n.ts.latestRequestReceivedAt }}</template> | ||||
| 						<template #value><MkTime v-if="instance.latestRequestReceivedAt" :time="instance.latestRequestReceivedAt"/><span v-else>N/A</span></template> | ||||
| 					</MkKeyValue> | ||||
| 				</FormSection> | ||||
|  | ||||
| 				<FormSection> | ||||
| 					<MkKeyValue oneline style="margin: 1em 0;"> | ||||
| 						<template #key>Following (Pub)</template> | ||||
| 						<template #value>{{ number(instance.followingCount) }}</template> | ||||
| 					</MkKeyValue> | ||||
| 					<MkKeyValue oneline style="margin: 1em 0;"> | ||||
| 						<template #key>Followers (Sub)</template> | ||||
| 						<template #value>{{ number(instance.followersCount) }}</template> | ||||
| 					</MkKeyValue> | ||||
| 				</FormSection> | ||||
|  | ||||
| 				<FormSection> | ||||
| 					<template #label>Well-known resources</template> | ||||
| 					<FormLink :to="`https://${host}/.well-known/host-meta`" external style="margin-bottom: 8px;">host-meta</FormLink> | ||||
| 					<FormLink :to="`https://${host}/.well-known/host-meta.json`" external style="margin-bottom: 8px;">host-meta.json</FormLink> | ||||
| 					<FormLink :to="`https://${host}/.well-known/nodeinfo`" external style="margin-bottom: 8px;">nodeinfo</FormLink> | ||||
| 					<FormLink :to="`https://${host}/robots.txt`" external style="margin-bottom: 8px;">robots.txt</FormLink> | ||||
| 					<FormLink :to="`https://${host}/manifest.json`" external style="margin-bottom: 8px;">manifest.json</FormLink> | ||||
| 				</FormSection> | ||||
| 			</div> | ||||
| 			<div v-else-if="tab === 'chart'" key="chart" class="_gaps_m"> | ||||
| 				<div class="cmhjzshl"> | ||||
| 					<div class="selects"> | ||||
| 						<MkSelect v-model="chartSrc" style="margin: 0 10px 0 0; flex: 1;"> | ||||
| 							<option value="instance-requests">{{ i18n.ts._instanceCharts.requests }}</option> | ||||
| 							<option value="instance-users">{{ i18n.ts._instanceCharts.users }}</option> | ||||
| 							<option value="instance-users-total">{{ i18n.ts._instanceCharts.usersTotal }}</option> | ||||
| 							<option value="instance-notes">{{ i18n.ts._instanceCharts.notes }}</option> | ||||
| 							<option value="instance-notes-total">{{ i18n.ts._instanceCharts.notesTotal }}</option> | ||||
| 							<option value="instance-ff">{{ i18n.ts._instanceCharts.ff }}</option> | ||||
| 							<option value="instance-ff-total">{{ i18n.ts._instanceCharts.ffTotal }}</option> | ||||
| 							<option value="instance-drive-usage">{{ i18n.ts._instanceCharts.cacheSize }}</option> | ||||
| 							<option value="instance-drive-usage-total">{{ i18n.ts._instanceCharts.cacheSizeTotal }}</option> | ||||
| 							<option value="instance-drive-files">{{ i18n.ts._instanceCharts.files }}</option> | ||||
| 							<option value="instance-drive-files-total">{{ i18n.ts._instanceCharts.filesTotal }}</option> | ||||
| 						</MkSelect> | ||||
| 					</div> | ||||
| 					<div class="charts"> | ||||
| 						<div class="label">{{ i18n.t('recentNHours', { n: 90 }) }}</div> | ||||
| 						<MkChart class="chart" :src="chartSrc" span="hour" :limit="90" :args="{ host: host }" :detailed="true"></MkChart> | ||||
| 						<div class="label">{{ i18n.t('recentNDays', { n: 90 }) }}</div> | ||||
| 						<MkChart class="chart" :src="chartSrc" span="day" :limit="90" :args="{ host: host }" :detailed="true"></MkChart> | ||||
| 					</div> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 		<div v-else-if="tab === 'users'" class="_gaps_m"> | ||||
| 			<MkPagination v-slot="{items}" :pagination="usersPagination" style="display: grid; grid-template-columns: repeat(auto-fill,minmax(270px,1fr)); grid-gap: 12px;"> | ||||
| 				<MkA v-for="user in items" :key="user.id" v-tooltip.mfm="`Last posted: ${dateString(user.updatedAt)}`" class="user" :to="`/admin/user/${user.id}`"> | ||||
| 					<MkUserCardMini :user="user"/> | ||||
| 				</MkA> | ||||
| 			</MkPagination> | ||||
| 		</div> | ||||
| 		<div v-else-if="tab === 'raw'" class="_gaps_m"> | ||||
| 			<MkObjectView tall :value="instance"> | ||||
| 			</MkObjectView> | ||||
| 		</div> | ||||
| 			<div v-else-if="tab === 'users'" key="users" class="_gaps_m"> | ||||
| 				<MkPagination v-slot="{items}" :pagination="usersPagination" style="display: grid; grid-template-columns: repeat(auto-fill,minmax(270px,1fr)); grid-gap: 12px;"> | ||||
| 					<MkA v-for="user in items" :key="user.id" v-tooltip.mfm="`Last posted: ${dateString(user.updatedAt)}`" class="user" :to="`/admin/user/${user.id}`"> | ||||
| 						<MkUserCardMini :user="user"/> | ||||
| 					</MkA> | ||||
| 				</MkPagination> | ||||
| 			</div> | ||||
| 			<div v-else-if="tab === 'raw'" key="raw" class="_gaps_m"> | ||||
| 				<MkObjectView tall :value="instance"> | ||||
| 				</MkObjectView> | ||||
| 			</div> | ||||
| 		</MkHorizontalSwipe> | ||||
| 	</MkSpacer> | ||||
| </MkStickyContainer> | ||||
| </template> | ||||
| @@ -137,6 +139,7 @@ import { definePageMetadata } from '@/scripts/page-metadata.js'; | ||||
| import { i18n } from '@/i18n.js'; | ||||
| import MkUserCardMini from '@/components/MkUserCardMini.vue'; | ||||
| import MkPagination from '@/components/MkPagination.vue'; | ||||
| import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue'; | ||||
| import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js'; | ||||
| import { dateString } from '@/filters/date.js'; | ||||
|  | ||||
| @@ -145,6 +148,7 @@ const props = defineProps<{ | ||||
| }>(); | ||||
|  | ||||
| const tab = ref('overview'); | ||||
|  | ||||
| const chartSrc = ref('instance-requests'); | ||||
| const meta = ref<Misskey.entities.AdminMetaResponse | null>(null); | ||||
| const instance = ref<Misskey.entities.FederationInstance | null>(null); | ||||
|   | ||||
| @@ -7,20 +7,22 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| <MkStickyContainer> | ||||
| 	<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template> | ||||
| 	<MkSpacer :contentMax="700"> | ||||
| 		<div v-if="tab === 'my'" class="_gaps"> | ||||
| 			<MkButton primary rounded class="add" @click="create"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton> | ||||
| 		<MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs"> | ||||
| 			<div v-if="tab === 'my'" key="my" class="_gaps"> | ||||
| 				<MkButton primary rounded class="add" @click="create"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton> | ||||
|  | ||||
| 			<MkPagination v-slot="{items}" ref="pagingComponent" :pagination="pagination" class="_gaps"> | ||||
| 				<MkA v-for="item in items" :key="item.id" :to="`/clips/${item.id}`"> | ||||
| 				<MkPagination v-slot="{items}" ref="pagingComponent" :pagination="pagination" class="_gaps"> | ||||
| 					<MkA v-for="item in items" :key="item.id" :to="`/clips/${item.id}`"> | ||||
| 						<MkClipPreview :clip="item"/> | ||||
| 					</MkA> | ||||
| 				</MkPagination> | ||||
| 			</div> | ||||
| 			<div v-else-if="tab === 'favorites'" key="favorites" class="_gaps"> | ||||
| 				<MkA v-for="item in favorites" :key="item.id" :to="`/clips/${item.id}`"> | ||||
| 					<MkClipPreview :clip="item"/> | ||||
| 				</MkA> | ||||
| 			</MkPagination> | ||||
| 		</div> | ||||
| 		<div v-else-if="tab === 'favorites'" class="_gaps"> | ||||
| 			<MkA v-for="item in favorites" :key="item.id" :to="`/clips/${item.id}`"> | ||||
| 				<MkClipPreview :clip="item"/> | ||||
| 			</MkA> | ||||
| 		</div> | ||||
| 			</div> | ||||
| 		</MkHorizontalSwipe> | ||||
| 	</MkSpacer> | ||||
| </MkStickyContainer> | ||||
| </template> | ||||
| @@ -36,6 +38,7 @@ import { misskeyApi } from '@/scripts/misskey-api.js'; | ||||
| import { i18n } from '@/i18n.js'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata.js'; | ||||
| import { clipsCache } from '@/cache.js'; | ||||
| import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue'; | ||||
|  | ||||
| const pagination = { | ||||
| 	endpoint: 'clips/list' as const, | ||||
| @@ -44,6 +47,7 @@ const pagination = { | ||||
| }; | ||||
|  | ||||
| const tab = ref('my'); | ||||
|  | ||||
| const favorites = ref<Misskey.entities.Clip[] | null>(null); | ||||
|  | ||||
| const pagingComponent = shallowRef<InstanceType<typeof MkPagination>>(); | ||||
|   | ||||
| @@ -7,15 +7,17 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| <MkStickyContainer> | ||||
| 	<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template> | ||||
| 	<MkSpacer :contentMax="800"> | ||||
| 		<div v-if="tab === 'all'"> | ||||
| 			<XNotifications class="notifications" :excludeTypes="excludeTypes"/> | ||||
| 		</div> | ||||
| 		<div v-else-if="tab === 'mentions'"> | ||||
| 			<MkNotes :pagination="mentionsPagination"/> | ||||
| 		</div> | ||||
| 		<div v-else-if="tab === 'directNotes'"> | ||||
| 			<MkNotes :pagination="directNotesPagination"/> | ||||
| 		</div> | ||||
| 		<MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs"> | ||||
| 			<div v-if="tab === 'all'" key="all"> | ||||
| 				<XNotifications :class="$style.notifications" :excludeTypes="excludeTypes"/> | ||||
| 			</div> | ||||
| 			<div v-else-if="tab === 'mentions'" key="mention"> | ||||
| 				<MkNotes :pagination="mentionsPagination"/> | ||||
| 			</div> | ||||
| 			<div v-else-if="tab === 'directNotes'" key="directNotes"> | ||||
| 				<MkNotes :pagination="directNotesPagination"/> | ||||
| 			</div> | ||||
| 		</MkHorizontalSwipe> | ||||
| 	</MkSpacer> | ||||
| </MkStickyContainer> | ||||
| </template> | ||||
| @@ -24,6 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| import { computed, ref } from 'vue'; | ||||
| import XNotifications from '@/components/MkNotifications.vue'; | ||||
| import MkNotes from '@/components/MkNotes.vue'; | ||||
| import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue'; | ||||
| import * as os from '@/os.js'; | ||||
| import { i18n } from '@/i18n.js'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata.js'; | ||||
| @@ -96,3 +99,10 @@ definePageMetadata(computed(() => ({ | ||||
| 	icon: 'ti ti-bell', | ||||
| }))); | ||||
| </script> | ||||
|  | ||||
| <style module lang="scss"> | ||||
| .notifications { | ||||
| 	border-radius: var(--radius); | ||||
| 	overflow: clip; | ||||
| } | ||||
| </style> | ||||
|   | ||||
| @@ -7,30 +7,32 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| <MkStickyContainer> | ||||
| 	<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template> | ||||
| 	<MkSpacer :contentMax="700"> | ||||
| 		<div v-if="tab === 'featured'"> | ||||
| 			<MkPagination v-slot="{items}" :pagination="featuredPagesPagination"> | ||||
| 				<div class="_gaps"> | ||||
| 					<MkPagePreview v-for="page in items" :key="page.id" :page="page"/> | ||||
| 				</div> | ||||
| 			</MkPagination> | ||||
| 		</div> | ||||
| 		<MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs"> | ||||
| 			<div v-if="tab === 'featured'" key="featured"> | ||||
| 				<MkPagination v-slot="{items}" :pagination="featuredPagesPagination"> | ||||
| 					<div class="_gaps"> | ||||
| 						<MkPagePreview v-for="page in items" :key="page.id" :page="page"/> | ||||
| 					</div> | ||||
| 				</MkPagination> | ||||
| 			</div> | ||||
|  | ||||
| 		<div v-else-if="tab === 'my'" class="_gaps"> | ||||
| 			<MkButton class="new" @click="create()"><i class="ti ti-plus"></i></MkButton> | ||||
| 			<MkPagination v-slot="{items}" :pagination="myPagesPagination"> | ||||
| 				<div class="_gaps"> | ||||
| 					<MkPagePreview v-for="page in items" :key="page.id" :page="page"/> | ||||
| 				</div> | ||||
| 			</MkPagination> | ||||
| 		</div> | ||||
| 			<div v-else-if="tab === 'my'" key="my" class="_gaps"> | ||||
| 				<MkButton class="new" @click="create()"><i class="ti ti-plus"></i></MkButton> | ||||
| 				<MkPagination v-slot="{items}" :pagination="myPagesPagination"> | ||||
| 					<div class="_gaps"> | ||||
| 						<MkPagePreview v-for="page in items" :key="page.id" :page="page"/> | ||||
| 					</div> | ||||
| 				</MkPagination> | ||||
| 			</div> | ||||
|  | ||||
| 		<div v-else-if="tab === 'liked'"> | ||||
| 			<MkPagination v-slot="{items}" :pagination="likedPagesPagination"> | ||||
| 				<div class="_gaps"> | ||||
| 					<MkPagePreview v-for="like in items" :key="like.page.id" :page="like.page"/> | ||||
| 				</div> | ||||
| 			</MkPagination> | ||||
| 		</div> | ||||
| 			<div v-else-if="tab === 'liked'" key="liked"> | ||||
| 				<MkPagination v-slot="{items}" :pagination="likedPagesPagination"> | ||||
| 					<div class="_gaps"> | ||||
| 						<MkPagePreview v-for="like in items" :key="like.page.id" :page="like.page"/> | ||||
| 					</div> | ||||
| 				</MkPagination> | ||||
| 			</div> | ||||
| 		</MkHorizontalSwipe> | ||||
| 	</MkSpacer> | ||||
| </MkStickyContainer> | ||||
| </template> | ||||
| @@ -40,6 +42,7 @@ import { computed, ref } from 'vue'; | ||||
| import MkPagePreview from '@/components/MkPagePreview.vue'; | ||||
| import MkPagination from '@/components/MkPagination.vue'; | ||||
| import MkButton from '@/components/MkButton.vue'; | ||||
| import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue'; | ||||
| import { i18n } from '@/i18n.js'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata.js'; | ||||
| import { useRouter } from '@/global/router/supplier.js'; | ||||
|   | ||||
| @@ -7,18 +7,20 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| <MkStickyContainer> | ||||
| 	<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template> | ||||
|  | ||||
| 	<MkSpacer v-if="tab === 'note'" :contentMax="800"> | ||||
| 		<div v-if="notesSearchAvailable"> | ||||
| 			<XNote/> | ||||
| 		</div> | ||||
| 		<div v-else> | ||||
| 			<MkInfo warn>{{ i18n.ts.notesSearchNotAvailable }}</MkInfo> | ||||
| 		</div> | ||||
| 	</MkSpacer> | ||||
| 	<MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs"> | ||||
| 		<MkSpacer v-if="tab === 'note'" key="note" :contentMax="800"> | ||||
| 			<div v-if="notesSearchAvailable"> | ||||
| 				<XNote/> | ||||
| 			</div> | ||||
| 			<div v-else> | ||||
| 				<MkInfo warn>{{ i18n.ts.notesSearchNotAvailable }}</MkInfo> | ||||
| 			</div> | ||||
| 		</MkSpacer> | ||||
|  | ||||
| 	<MkSpacer v-else-if="tab === 'user'" :contentMax="800"> | ||||
| 		<XUser/> | ||||
| 	</MkSpacer> | ||||
| 		<MkSpacer v-else-if="tab === 'user'" key="user" :contentMax="800"> | ||||
| 			<XUser/> | ||||
| 		</MkSpacer> | ||||
| 	</MkHorizontalSwipe> | ||||
| </MkStickyContainer> | ||||
| </template> | ||||
|  | ||||
| @@ -29,6 +31,7 @@ import { definePageMetadata } from '@/scripts/page-metadata.js'; | ||||
| import { $i } from '@/account.js'; | ||||
| import { instance } from '@/instance.js'; | ||||
| import MkInfo from '@/components/MkInfo.vue'; | ||||
| import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue'; | ||||
|  | ||||
| const XNote = defineAsyncComponent(() => import('./search.note.vue')); | ||||
| const XUser = defineAsyncComponent(() => import('./search.user.vue')); | ||||
|   | ||||
| @@ -156,6 +156,7 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| 				<MkSwitch v-model="enableInfiniteScroll">{{ i18n.ts.enableInfiniteScroll }}</MkSwitch> | ||||
| 				<MkSwitch v-model="keepScreenOn">{{ i18n.ts.keepScreenOn }}</MkSwitch> | ||||
| 				<MkSwitch v-model="disableStreamingTimeline">{{ i18n.ts.disableStreamingTimeline }}</MkSwitch> | ||||
| 				<MkSwitch v-model="enableHorizontalSwipe">{{ i18n.ts.enableHorizontalSwipe }}</MkSwitch> | ||||
| 			</div> | ||||
| 			<MkSelect v-model="serverDisconnectedBehavior"> | ||||
| 				<template #label>{{ i18n.ts.whenServerDisconnected }}</template> | ||||
| @@ -298,6 +299,7 @@ const keepScreenOn = computed(defaultStore.makeGetterSetter('keepScreenOn')); | ||||
| const disableStreamingTimeline = computed(defaultStore.makeGetterSetter('disableStreamingTimeline')); | ||||
| const useGroupedNotifications = computed(defaultStore.makeGetterSetter('useGroupedNotifications')); | ||||
| const enableSeasonalScreenEffect = computed(defaultStore.makeGetterSetter('enableSeasonalScreenEffect')); | ||||
| const enableHorizontalSwipe = computed(defaultStore.makeGetterSetter('enableHorizontalSwipe')); | ||||
|  | ||||
| watch(lang, () => { | ||||
| 	miLocalStorage.setItem('lang', lang.value as string); | ||||
|   | ||||
| @@ -7,27 +7,28 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| <MkStickyContainer> | ||||
| 	<template #header><MkPageHeader v-model:tab="src" :actions="headerActions" :tabs="$i ? headerTabs : headerTabsWhenNotLogin" :displayMyAvatar="true"/></template> | ||||
| 	<MkSpacer :contentMax="800"> | ||||
| 		<div ref="rootEl" v-hotkey.global="keymap"> | ||||
| 			<MkInfo v-if="['home', 'local', 'social', 'global'].includes(src) && !defaultStore.reactiveState.timelineTutorials.value[src]" style="margin-bottom: var(--margin);" closable @close="closeTutorial()"> | ||||
| 				{{ i18n.ts._timelineDescription[src] }} | ||||
| 			</MkInfo> | ||||
| 			<MkPostForm v-if="defaultStore.reactiveState.showFixedPostForm.value" :class="$style.postForm" class="post-form _panel" fixed style="margin-bottom: var(--margin);"/> | ||||
|  | ||||
| 			<div v-if="queue > 0" :class="$style.new"><button class="_buttonPrimary" :class="$style.newButton" @click="top()">{{ i18n.ts.newNoteRecived }}</button></div> | ||||
| 			<div :class="$style.tl"> | ||||
| 				<MkTimeline | ||||
| 					ref="tlComponent" | ||||
| 					:key="src + withRenotes + withReplies + onlyFiles" | ||||
| 					:src="src.split(':')[0]" | ||||
| 					:list="src.split(':')[1]" | ||||
| 					:withRenotes="withRenotes" | ||||
| 					:withReplies="withReplies" | ||||
| 					:onlyFiles="onlyFiles" | ||||
| 					:sound="true" | ||||
| 					@queue="queueUpdated" | ||||
| 				/> | ||||
| 		<MkHorizontalSwipe v-model:tab="src" :tabs="$i ? headerTabs : headerTabsWhenNotLogin"> | ||||
| 			<div :key="src + withRenotes + withReplies + onlyFiles" ref="rootEl" v-hotkey.global="keymap"> | ||||
| 				<MkInfo v-if="['home', 'local', 'social', 'global'].includes(src) && !defaultStore.reactiveState.timelineTutorials.value[src]" style="margin-bottom: var(--margin);" closable @close="closeTutorial()"> | ||||
| 					{{ i18n.ts._timelineDescription[src] }} | ||||
| 				</MkInfo> | ||||
| 				<MkPostForm v-if="defaultStore.reactiveState.showFixedPostForm.value" :class="$style.postForm" class="post-form _panel" fixed style="margin-bottom: var(--margin);"/> | ||||
| 				<div v-if="queue > 0" :class="$style.new"><button class="_buttonPrimary" :class="$style.newButton" @click="top()">{{ i18n.ts.newNoteRecived }}</button></div> | ||||
| 				<div :class="$style.tl"> | ||||
| 					<MkTimeline | ||||
| 						ref="tlComponent" | ||||
| 						:key="src + withRenotes + withReplies + onlyFiles" | ||||
| 						:src="src.split(':')[0]" | ||||
| 						:list="src.split(':')[1]" | ||||
| 						:withRenotes="withRenotes" | ||||
| 						:withReplies="withReplies" | ||||
| 						:onlyFiles="onlyFiles" | ||||
| 						:sound="true" | ||||
| 						@queue="queueUpdated" | ||||
| 					/> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 		</MkHorizontalSwipe> | ||||
| 	</MkSpacer> | ||||
| </MkStickyContainer> | ||||
| </template> | ||||
| @@ -38,6 +39,7 @@ import type { Tab } from '@/components/global/MkPageHeader.tabs.vue'; | ||||
| import MkTimeline from '@/components/MkTimeline.vue'; | ||||
| import MkInfo from '@/components/MkInfo.vue'; | ||||
| import MkPostForm from '@/components/MkPostForm.vue'; | ||||
| import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue'; | ||||
| import { scroll } from '@/scripts/scroll.js'; | ||||
| import * as os from '@/os.js'; | ||||
| import { misskeyApi } from '@/scripts/misskey-api.js'; | ||||
| @@ -69,7 +71,9 @@ const withRenotes = ref(true); | ||||
| const withReplies = ref($i ? defaultStore.state.tlWithReplies : false); | ||||
| const onlyFiles = ref(false); | ||||
|  | ||||
| watch(src, () => queue.value = 0); | ||||
| watch(src, () => { | ||||
| 	queue.value = 0; | ||||
| }); | ||||
|  | ||||
| watch(withReplies, (x) => { | ||||
| 	if ($i) defaultStore.set('tlWithReplies', x); | ||||
|   | ||||
| @@ -8,19 +8,21 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| 	<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template> | ||||
| 	<div> | ||||
| 		<div v-if="user"> | ||||
| 			<XHome v-if="tab === 'home'" :user="user"/> | ||||
| 			<MkSpacer v-else-if="tab === 'notes'" :contentMax="800" style="padding-top: 0"> | ||||
| 				<XTimeline :user="user"/> | ||||
| 			</MkSpacer> | ||||
| 			<XActivity v-else-if="tab === 'activity'" :user="user"/> | ||||
| 			<XAchievements v-else-if="tab === 'achievements'" :user="user"/> | ||||
| 			<XReactions v-else-if="tab === 'reactions'" :user="user"/> | ||||
| 			<XClips v-else-if="tab === 'clips'" :user="user"/> | ||||
| 			<XLists v-else-if="tab === 'lists'" :user="user"/> | ||||
| 			<XPages v-else-if="tab === 'pages'" :user="user"/> | ||||
| 			<XFlashs v-else-if="tab === 'flashs'" :user="user"/> | ||||
| 			<XGallery v-else-if="tab === 'gallery'" :user="user"/> | ||||
| 			<XRaw v-else-if="tab === 'raw'" :user="user"/> | ||||
| 			<MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs"> | ||||
| 				<XHome v-if="tab === 'home'" key="home" :user="user"/> | ||||
| 				<MkSpacer v-else-if="tab === 'notes'" key="notes" :contentMax="800" style="padding-top: 0"> | ||||
| 					<XTimeline :user="user"/> | ||||
| 				</MkSpacer> | ||||
| 				<XActivity v-else-if="tab === 'activity'" key="activity" :user="user"/> | ||||
| 				<XAchievements v-else-if="tab === 'achievements'" key="achievements" :user="user"/> | ||||
| 				<XReactions v-else-if="tab === 'reactions'" key="reactions" :user="user"/> | ||||
| 				<XClips v-else-if="tab === 'clips'" key="clips" :user="user"/> | ||||
| 				<XLists v-else-if="tab === 'lists'" key="lists" :user="user"/> | ||||
| 				<XPages v-else-if="tab === 'pages'" key="pages" :user="user"/> | ||||
| 				<XFlashs v-else-if="tab === 'flashs'" key="flashs" :user="user"/> | ||||
| 				<XGallery v-else-if="tab === 'gallery'" key="gallery" :user="user"/> | ||||
| 				<XRaw v-else-if="tab === 'raw'" key="raw" :user="user"/> | ||||
| 			</MkHorizontalSwipe> | ||||
| 		</div> | ||||
| 		<MkError v-else-if="error" @retry="fetchUser()"/> | ||||
| 		<MkLoading v-else/> | ||||
| @@ -36,6 +38,7 @@ import { misskeyApi } from '@/scripts/misskey-api.js'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata.js'; | ||||
| import { i18n } from '@/i18n.js'; | ||||
| import { $i } from '@/account.js'; | ||||
| import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue'; | ||||
|  | ||||
| const XHome = defineAsyncComponent(() => import('./home.vue')); | ||||
| const XTimeline = defineAsyncComponent(() => import('./index.timeline.vue')); | ||||
| @@ -57,6 +60,7 @@ const props = withDefaults(defineProps<{ | ||||
| }); | ||||
|  | ||||
| const tab = ref(props.page); | ||||
|  | ||||
| const user = ref<null | Misskey.entities.UserDetailed>(null); | ||||
| const error = ref<any>(null); | ||||
|  | ||||
|   | ||||
| @@ -990,7 +990,7 @@ export class DropAndFusionGame extends EventEmitter<{ | ||||
| }> { | ||||
| 	private PHYSICS_QUALITY_FACTOR = 16; // 低いほどパフォーマンスが高いがガタガタして安定しなくなる、逆に高すぎても何故か不安定になる | ||||
| 	private COMBO_INTERVAL = 60; // frame | ||||
| 	public readonly GAME_VERSION = 2; | ||||
| 	public readonly GAME_VERSION = 3; | ||||
| 	public readonly GAME_WIDTH = 450; | ||||
| 	public readonly GAME_HEIGHT = 600; | ||||
| 	public readonly DROP_COOLTIME = 30; // frame | ||||
|   | ||||
| @@ -431,6 +431,10 @@ export const defaultStore = markRaw(new Storage('base', { | ||||
| 			sfxVolume: 1, | ||||
| 		}, | ||||
| 	}, | ||||
| 	enableHorizontalSwipe: { | ||||
| 		where: 'device', | ||||
| 		default: true, | ||||
| 	}, | ||||
|  | ||||
| 	sound_masterVolume: { | ||||
| 		where: 'device', | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 まっちゃとーにゅ
					まっちゃとーにゅ