From c1019a006bfa48ba7398daa02788c50be0bfb35a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?= <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Thu, 18 Jan 2024 18:21:33 +0900 Subject: [PATCH] =?UTF-8?q?feat(frontend):=20=E6=A8=AA=E3=82=B9=E3=83=AF?= =?UTF-8?q?=E3=82=A4=E3=83=97=E3=81=A7=E3=82=BF=E3=83=96=E3=82=92=E5=88=87?= =?UTF-8?q?=E3=82=8A=E6=9B=BF=E3=81=88=E3=82=8B=E6=A9=9F=E8=83=BD=20(#1301?= =?UTF-8?q?1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * (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> --- CHANGELOG.md | 1 + locales/index.d.ts | 1 + locales/ja-JP.yml | 1 + .../src/components/MkHorizontalSwipe.vue | 209 ++++++++++++++++++ packages/frontend/src/pages/about.vue | 171 +++++++------- packages/frontend/src/pages/announcements.vue | 57 ++--- packages/frontend/src/pages/channel.vue | 90 ++++---- packages/frontend/src/pages/channels.vue | 77 +++---- packages/frontend/src/pages/drive.file.vue | 15 +- packages/frontend/src/pages/explore.vue | 11 +- .../frontend/src/pages/flash/flash-index.vue | 41 ++-- packages/frontend/src/pages/gallery/index.vue | 11 +- packages/frontend/src/pages/instance-info.vue | 206 ++++++++--------- .../frontend/src/pages/my-clips/index.vue | 26 ++- packages/frontend/src/pages/notifications.vue | 28 ++- packages/frontend/src/pages/pages.vue | 47 ++-- packages/frontend/src/pages/search.vue | 25 ++- .../frontend/src/pages/settings/general.vue | 2 + packages/frontend/src/pages/timeline.vue | 46 ++-- packages/frontend/src/pages/user/index.vue | 30 +-- packages/frontend/src/store.ts | 4 + 21 files changed, 685 insertions(+), 414 deletions(-) create mode 100644 packages/frontend/src/components/MkHorizontalSwipe.vue diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b4331961..605b8af92 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ - Feat: 絵文字の詳細ダイアログを追加 - Feat: 枠線をつけるMFM`$[border.width=1,style=solid,color=fff,radius=0 ...]`を追加 - デフォルトで枠線からはみ出る部分が隠されるようにしました。初期と同じ挙動にするには`$[border.noclip`が必要です +- Feat: スワイプでタブを切り替えられるように - Enhance: MFM等のコードブロックに全文コピー用のボタンを追加 - Enhance: ハッシュタグ入力時に、本文の末尾の行に何も書かれていない場合は新たにスペースを追加しないように - Enhance: チャンネルノートのピン留めをノートのメニューからできるように diff --git a/locales/index.d.ts b/locales/index.d.ts index 71134544d..a659e790c 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -1204,6 +1204,7 @@ export interface Locale { "ranking": string; "lastNDays": string; "backToTitle": string; + "enableHorizontalSwipe": string; "_bubbleGame": { "howToPlay": string; "_howToPlay": { diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 743a3ca38..8749a5f49 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1201,6 +1201,7 @@ replaying: "リプレイ中" ranking: "ランキング" lastNDays: "直近{n}日" backToTitle: "タイトルへ" +enableHorizontalSwipe: "スワイプしてタブを切り替える" _bubbleGame: howToPlay: "遊び方" diff --git a/packages/frontend/src/components/MkHorizontalSwipe.vue b/packages/frontend/src/components/MkHorizontalSwipe.vue new file mode 100644 index 000000000..2c62aadbf --- /dev/null +++ b/packages/frontend/src/components/MkHorizontalSwipe.vue @@ -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> diff --git a/packages/frontend/src/pages/about.vue b/packages/frontend/src/pages/about.vue index f402b26ad..4ba1b6da7 100644 --- a/packages/frontend/src/pages/about.vue +++ b/packages/frontend/src/pages/about.vue @@ -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'; diff --git a/packages/frontend/src/pages/announcements.vue b/packages/frontend/src/pages/announcements.vue index 5632bf7ca..c31c6d090 100644 --- a/packages/frontend/src/pages/announcements.vue +++ b/packages/frontend/src/pages/announcements.vue @@ -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'; diff --git a/packages/frontend/src/pages/channel.vue b/packages/frontend/src/pages/channel.vue index 971eca8ca..4cdf2eea7 100644 --- a/packages/frontend/src/pages/channel.vue +++ b/packages/frontend/src/pages/channel.vue @@ -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(''); diff --git a/packages/frontend/src/pages/channels.vue b/packages/frontend/src/pages/channels.vue index 80a401eee..0b35ac0ec 100644 --- a/packages/frontend/src/pages/channels.vue +++ b/packages/frontend/src/pages/channels.vue @@ -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'; diff --git a/packages/frontend/src/pages/drive.file.vue b/packages/frontend/src/pages/drive.file.vue index 2c1e5d20a..6a9e90796 100644 --- a/packages/frontend/src/pages/drive.file.vue +++ b/packages/frontend/src/pages/drive.file.vue @@ -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; diff --git a/packages/frontend/src/pages/explore.vue b/packages/frontend/src/pages/explore.vue index f068de888..1b8001436 100644 --- a/packages/frontend/src/pages/explore.vue +++ b/packages/frontend/src/pages/explore.vue @@ -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'; diff --git a/packages/frontend/src/pages/flash/flash-index.vue b/packages/frontend/src/pages/flash/flash-index.vue index 785201889..53510ea23 100644 --- a/packages/frontend/src/pages/flash/flash-index.vue +++ b/packages/frontend/src/pages/flash/flash-index.vue @@ -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'; diff --git a/packages/frontend/src/pages/gallery/index.vue b/packages/frontend/src/pages/gallery/index.vue index 0198ab970..9749888fe 100644 --- a/packages/frontend/src/pages/gallery/index.vue +++ b/packages/frontend/src/pages/gallery/index.vue @@ -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'; diff --git a/packages/frontend/src/pages/instance-info.vue b/packages/frontend/src/pages/instance-info.vue index c8a0eeeea..4211dc0d8 100644 --- a/packages/frontend/src/pages/instance-info.vue +++ b/packages/frontend/src/pages/instance-info.vue @@ -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); diff --git a/packages/frontend/src/pages/my-clips/index.vue b/packages/frontend/src/pages/my-clips/index.vue index 850222708..468e46838 100644 --- a/packages/frontend/src/pages/my-clips/index.vue +++ b/packages/frontend/src/pages/my-clips/index.vue @@ -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>>(); diff --git a/packages/frontend/src/pages/notifications.vue b/packages/frontend/src/pages/notifications.vue index d57bda41b..8913a89ad 100644 --- a/packages/frontend/src/pages/notifications.vue +++ b/packages/frontend/src/pages/notifications.vue @@ -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> diff --git a/packages/frontend/src/pages/pages.vue b/packages/frontend/src/pages/pages.vue index 22ab9ced0..8b57b1af9 100644 --- a/packages/frontend/src/pages/pages.vue +++ b/packages/frontend/src/pages/pages.vue @@ -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'; diff --git a/packages/frontend/src/pages/search.vue b/packages/frontend/src/pages/search.vue index 9d5e5697c..b68de805c 100644 --- a/packages/frontend/src/pages/search.vue +++ b/packages/frontend/src/pages/search.vue @@ -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')); diff --git a/packages/frontend/src/pages/settings/general.vue b/packages/frontend/src/pages/settings/general.vue index 607aaec52..e52a5ee04 100644 --- a/packages/frontend/src/pages/settings/general.vue +++ b/packages/frontend/src/pages/settings/general.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); diff --git a/packages/frontend/src/pages/timeline.vue b/packages/frontend/src/pages/timeline.vue index 6fe8963f5..666a9968b 100644 --- a/packages/frontend/src/pages/timeline.vue +++ b/packages/frontend/src/pages/timeline.vue @@ -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); diff --git a/packages/frontend/src/pages/user/index.vue b/packages/frontend/src/pages/user/index.vue index 95869e7b8..603f1bef3 100644 --- a/packages/frontend/src/pages/user/index.vue +++ b/packages/frontend/src/pages/user/index.vue @@ -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); diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts index e3a85377d..21b796caa 100644 --- a/packages/frontend/src/store.ts +++ b/packages/frontend/src/store.ts @@ -427,6 +427,10 @@ export const defaultStore = markRaw(new Storage('base', { sfxVolume: 1, }, }, + enableHorizontalSwipe: { + where: 'device', + default: true, + }, sound_masterVolume: { where: 'device',