feat(frontend): 横スワイプでタブを切り替える機能 (#13011)
* (add) 横スワイプでタブを切り替える機能 * Change Changelog * y方向の移動が一定量を超えたらスワイプを中断するように * Update swipe distance thresholds * Remove console.log * adjust threshold * rename, use v-model * fix * Update MkHorizontalSwipe.vue Co-authored-by: syuilo <Syuilotan@yahoo.co.jp> * use css module --------- Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
This commit is contained in:
		
							parent
							
								
									67a41c09ae
								
							
						
					
					
						commit
						c1019a006b
					
				
					 21 changed files with 685 additions and 414 deletions
				
			
		| 
						 | 
				
			
			@ -24,6 +24,7 @@
 | 
			
		|||
- Feat: 絵文字の詳細ダイアログを追加
 | 
			
		||||
- Feat: 枠線をつけるMFM`$[border.width=1,style=solid,color=fff,radius=0 ...]`を追加
 | 
			
		||||
  - デフォルトで枠線からはみ出る部分が隠されるようにしました。初期と同じ挙動にするには`$[border.noclip`が必要です
 | 
			
		||||
- Feat: スワイプでタブを切り替えられるように
 | 
			
		||||
- Enhance: MFM等のコードブロックに全文コピー用のボタンを追加
 | 
			
		||||
- Enhance: ハッシュタグ入力時に、本文の末尾の行に何も書かれていない場合は新たにスペースを追加しないように
 | 
			
		||||
- Enhance: チャンネルノートのピン留めをノートのメニューからできるように
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										1
									
								
								locales/index.d.ts
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								locales/index.d.ts
									
										
									
									
										vendored
									
									
								
							| 
						 | 
				
			
			@ -1204,6 +1204,7 @@ export interface Locale {
 | 
			
		|||
    "ranking": string;
 | 
			
		||||
    "lastNDays": string;
 | 
			
		||||
    "backToTitle": string;
 | 
			
		||||
    "enableHorizontalSwipe": string;
 | 
			
		||||
    "_bubbleGame": {
 | 
			
		||||
        "howToPlay": string;
 | 
			
		||||
        "_howToPlay": {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1201,6 +1201,7 @@ replaying: "リプレイ中"
 | 
			
		|||
ranking: "ランキング"
 | 
			
		||||
lastNDays: "直近{n}日"
 | 
			
		||||
backToTitle: "タイトルへ"
 | 
			
		||||
enableHorizontalSwipe: "スワイプしてタブを切り替える"
 | 
			
		||||
 | 
			
		||||
_bubbleGame:
 | 
			
		||||
  howToPlay: "遊び方"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										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>
 | 
			
		||||
| 
						 | 
				
			
			@ -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>
 | 
			
		||||
						<span>{{ announcement.title }}</span>
 | 
			
		||||
					</div>
 | 
			
		||||
					<div :class="$style.content">
 | 
			
		||||
						<Mfm :text="announcement.text"/>
 | 
			
		||||
						<img v-if="announcement.imageUrl" :src="announcement.imageUrl"/>
 | 
			
		||||
						<div style="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>
 | 
			
		||||
							<span>{{ announcement.title }}</span>
 | 
			
		||||
						</div>
 | 
			
		||||
					</div>
 | 
			
		||||
					<div v-if="tab !== 'past' && $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="opacity: 0.7; font-size: 85%;">
 | 
			
		||||
								<MkTime :time="announcement.updatedAt ?? announcement.createdAt" mode="detail"/>
 | 
			
		||||
							</div>
 | 
			
		||||
						</div>
 | 
			
		||||
						<div v-if="tab !== 'past' && $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 } 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';
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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,111 +7,113 @@ 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>
 | 
			
		||||
					<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>
 | 
			
		||||
						<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>
 | 
			
		||||
| 
						 | 
				
			
			@ -136,6 +138,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';
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -144,6 +147,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'));
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -155,6 +155,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>
 | 
			
		||||
| 
						 | 
				
			
			@ -296,6 +297,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);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -427,6 +427,10 @@ export const defaultStore = markRaw(new Storage('base', {
 | 
			
		|||
			sfxVolume: 1,
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
	enableHorizontalSwipe: {
 | 
			
		||||
		where: 'device',
 | 
			
		||||
		default: true,
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	sound_masterVolume: {
 | 
			
		||||
		where: 'device',
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue