Files
jrshikoku/components/Menu/Carousel/useSortMode.tsx

134 lines
4.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useCallback, useEffect, useRef, useState } from "react";
import { AS } from "@/storageControl";
import { STORAGE_KEYS } from "@/constants";
import { useFavoriteStation } from "@/stateBox/useFavoriteStation";
import { SortGridCard } from "./SortGridCard";
import { CarouselUIMode, StationSource } from "@/types";
type SortModeConfig = {
listUpStation: any[][];
setListIndex: (i: number) => void;
width: number;
origW: number;
origH: number;
cols: number;
gridPad: number;
gridGap: number;
cellW: number;
cellH: number;
carouselHeight: number;
stationSource: StationSource;
};
/** カルーセルの並び替えモードに関わる状態・ロジックをまとめたカスタムフック */
export function useSortMode({
listUpStation,
setListIndex,
width,
origW,
origH,
cols,
gridPad,
gridGap,
cellW,
cellH,
carouselHeight,
stationSource,
}: SortModeConfig) {
const { setFavoriteStation } = useFavoriteStation();
// "carousel" | "sort" | "sort-exiting" の 3 値で UI モードを管理
const [uiMode, setUiMode] = useState<CarouselUIMode>("carousel");
// ソート開始時のカルーセル位置を保存setListIndex(-1) される前の値)
const sortModeStartIndexRef = useRef(0);
// ソート終了後に移動するインデックス(タップで上書き可、デフォルト 0
const exitTargetIndexRef = useRef(0);
// carousel に戻ったら指定インデックスへ移動
useEffect(() => {
if (uiMode === "carousel") {
setListIndex(exitTargetIndexRef.current);
}
}, [uiMode]);
/** 並び替えモード開始(現在のカルーセル位置を渡す) */
const startSortMode = useCallback((currentIndex: number) => {
sortModeStartIndexRef.current = currentIndex;
exitTargetIndexRef.current = 0; // デフォルトは先頭
setListIndex(-1); // 未選択状態にして LED を非表示
setUiMode("sort");
}, [setListIndex]);
/** 退場アニメーション完了後にモードを終了 */
const exitSortMode = useCallback(() => {
setUiMode("sort-exiting");
// 退場スプリングが収束するまで待ってから carousel へ
setTimeout(() => {
setUiMode("carousel");
}, listUpStation.length * 40 + 500);
}, [listUpStation.length]);
/** Sortable.Grid の renderItemuseCallback でメモ化) */
const sortGridRenderItem = useCallback(
({ item, index }: { item: any; index: number }) => {
const col = index % cols;
const row = Math.floor(index / cols);
// カルーセルでの card 中心位置 → グリッドセルの中心位置 との差分を初期オフセットに
const carouselCardCenterX =
(index - sortModeStartIndexRef.current) * width + width / 2 - gridPad;
const carouselCardCenterY = carouselHeight / 2;
const cellCenterX = col * (cellW + gridGap) + cellW / 2;
const cellCenterY = row * (cellH + gridGap) + cellH / 2;
const startX = carouselCardCenterX - cellCenterX;
const startY = carouselCardCenterY - cellCenterY;
// 退場先: タップで選択されたカードが画面中央に来るカルーセル配置
const exitCarouselCardCenterX =
(index - exitTargetIndexRef.current) * width + width / 2 - gridPad;
const exitX = exitCarouselCardCenterX - cellCenterX;
const exitY = carouselCardCenterY - cellCenterY; // Y は変わらない
return (
<SortGridCard
key={item[0].StationNumber}
item={item}
cellW={cellW}
cellH={cellH}
startX={startX}
startY={startY}
exitX={exitX}
exitY={exitY}
startScale={origW / cellW}
isExiting={uiMode === "sort-exiting"}
exitDelay={Math.min(index * 40, 180)}
onPress={() => {
exitTargetIndexRef.current = index;
exitSortMode();
}}
/>
);
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[cellW, cellH, gridGap, gridPad, origW, origH, width, carouselHeight, uiMode]
);
/** Sortable.Grid の onDragEnd */
const onSortDragEnd = useCallback(
(newOrder: { indexToKey: string[] }) => {
// お気に入りモード以外はデータを書き換えない(安全策)
if (stationSource.type !== "favorite") return;
const newList = newOrder.indexToKey.map(
(key) => listUpStation.find((s) => s[0].StationNumber === key) ?? []
);
setFavoriteStation(newList);
AS.setItem(STORAGE_KEYS.FAVORITE_STATION, JSON.stringify(newList));
},
[listUpStation, setFavoriteStation, stationSource]
);
return {
uiMode,
startSortMode,
exitSortMode,
sortGridRenderItem,
onSortDragEnd,
};
}