Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(frontend): 横スワイプでタブを切り替える機能 #13011

Merged
merged 20 commits into from
Jan 18, 2024
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
4a6d517
(add) 横スワイプでタブを切り替える機能
kakkokari-gtyih Jan 16, 2024
36de543
Change Changelog
kakkokari-gtyih Jan 16, 2024
69f6126
Merge branch 'develop' into feat-13003
kakkokari-gtyih Jan 17, 2024
9de8c11
Merge branch 'develop' into feat-13003
kakkokari-gtyih Jan 17, 2024
07c8ab4
Merge branch 'develop' into feat-13003
kakkokari-gtyih Jan 17, 2024
035397b
Merge branch 'develop' into feat-13003
kakkokari-gtyih Jan 17, 2024
e7939e0
y方向の移動が一定量を超えたらスワイプを中断するように
kakkokari-gtyih Jan 17, 2024
2f14801
Merge branch 'feat-13003' of https://github.com/kakkokari-gtyih/missk…
kakkokari-gtyih Jan 17, 2024
5ee3c92
Update swipe distance thresholds
kakkokari-gtyih Jan 17, 2024
ff41cbb
Remove console.log
kakkokari-gtyih Jan 17, 2024
cd2f378
Merge branch 'develop' into feat-13003
kakkokari-gtyih Jan 17, 2024
88e5ba8
adjust threshold
kakkokari-gtyih Jan 17, 2024
20aa287
Merge branch 'feat-13003' of https://github.com/kakkokari-gtyih/missk…
kakkokari-gtyih Jan 17, 2024
870da21
rename, use v-model
kakkokari-gtyih Jan 17, 2024
da0ea97
Merge branch 'develop' into feat-13003
kakkokari-gtyih Jan 17, 2024
03e58c4
fix
kakkokari-gtyih Jan 17, 2024
d881e2a
Merge branch 'feat-13003' of https://github.com/kakkokari-gtyih/missk…
kakkokari-gtyih Jan 17, 2024
7a3a1a0
Merge branch 'develop' into feat-13003
kakkokari-gtyih Jan 18, 2024
92eb539
Update MkHorizontalSwipe.vue
kakkokari-gtyih Jan 18, 2024
3b87bc8
use css module
kakkokari-gtyih Jan 18, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
- Feat: 絵文字の詳細ダイアログを追加
- Feat: 枠線をつけるMFM`$[border.width=1,style=solid,color=fff,radius=0 ...]`を追加
- デフォルトで枠線からはみ出る部分が隠されるようにしました。初期と同じ挙動にするには`$[border.noclip`が必要です
- Feat: スワイプでタブを切り替えられるように
- Enhance: MFM等のコードブロックに全文コピー用のボタンを追加
- Enhance: ハッシュタグ入力時に、本文の末尾の行に何も書かれていない場合は新たにスペースを追加しないように
- Enhance: チャンネルノートのピン留めをノートのメニューからできるように
Expand Down
1 change: 1 addition & 0 deletions locales/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1204,6 +1204,7 @@ export interface Locale {
"ranking": string;
"lastNDays": string;
"backToTitle": string;
"enableHorizontalSwipe": string;
"_bubbleGame": {
"howToPlay": string;
"_howToPlay": {
Expand Down
1 change: 1 addition & 0 deletions locales/ja-JP.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1201,6 +1201,7 @@ replaying: "リプレイ中"
ranking: "ランキング"
lastNDays: "直近{n}日"
backToTitle: "タイトルへ"
enableHorizontalSwipe: "スワイプしてタブを切り替える"

_bubbleGame:
howToPlay: "遊び方"
Expand Down
204 changes: 204 additions & 0 deletions packages/frontend/src/components/MkHorizontalSwipe.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
<!--
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 :name="transitionName" :class="[$style.transitionChildren, { [$style.swiping]: isSwipingForClass }]" :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));

&:global(.swipeAnimationLeft-enter-active),
kakkokari-gtyih marked this conversation as resolved.
Show resolved Hide resolved
&:global(.swipeAnimationRight-enter-active),
&:global(.swipeAnimationLeft-leave-active),
&:global(.swipeAnimationRight-leave-active) {
transition: transform .3s cubic-bezier(0.65, 0.05, 0.36, 1);
}

&:global(.swipeAnimationRight-leave-to),
&:global(.swipeAnimationLeft-enter-from) {
transform: translateX(calc(100% + 24px));
}

&:global(.swipeAnimationRight-enter-from),
&:global(.swipeAnimationLeft-leave-to) {
transform: translateX(calc(-100% - 24px));
}
}
}

.swiping {
transition: transform .2s ease-out;
}
</style>
Loading
Loading