Files
jrshikoku/components/Apps/FixedPositionBox/FixedStationBox.tsx
harukin-expo-dev-env 1d57f2a5c6 fix: 通知折りたたみ時も走行区間を表示
- contentTextにbody全行を全角スペース区切りで表示(折りたたみ時も見える)
- pollTrainPositionの変化なしスキップを除去(常に通知を更新)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-24 02:54:24 +00:00

532 lines
17 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 lineColorList from "@/assets/originData/lineColorList";
import { StationNumberMaker } from "@/components/駅名表/StationNumberMaker";
import { checkDuplicateTrainData } from "@/lib/checkDuplicateTrainData";
import {
CustomTrainData,
eachTrainDiagramType,
StationProps,
} from "@/lib/CommonTypes";
import { getCurrentTrainData } from "@/lib/getCurrentTrainData";
import { getTrainDelayStatus } from "@/lib/getTrainDelayStatus";
import { getTrainType } from "@/lib/getTrainType";
import { objectIsEmpty } from "@/lib/objectIsEmpty";
import { getTime, trainTimeFiltering } from "@/lib/trainTimeFiltering";
import { useAllTrainDiagram } from "@/stateBox/useAllTrainDiagram";
import { useAreaInfo } from "@/stateBox/useAreaInfo";
import { useCurrentTrain } from "@/stateBox/useCurrentTrain";
import { useStationList } from "@/stateBox/useStationList";
import { useTrainMenu } from "@/stateBox/useTrainMenu";
import { Ionicons } from "@expo/vector-icons";
import { useNavigation } from "@react-navigation/native";
import { FC, useCallback, useEffect, useRef, useState } from "react";
import { Animated, LayoutAnimation, PermissionsAndroid, Platform, Text, TouchableOpacity, View } from "react-native";
import { SheetManager } from "react-native-actions-sheet";
import { useThemeColors } from "@/lib/theme";
import {
startStationLockActivity,
updateStationLockActivity,
endStationLockActivity,
isAvailable as isLiveActivityAvailable,
StationTrainInfo,
} from "expo-live-activity";
type props = {
stationID: string;
};
export const FixedStation: FC<props> = ({ stationID }) => {
const { colors, fixed } = useThemeColors();
const { mapSwitch } = useTrainMenu();
const {
currentTrain,
fixedPosition,
setFixedPosition,
fixedPositionSize,
setFixedPositionSize,
liveNotificationActive,
setLiveNotificationActive,
} = useCurrentTrain();
const { getStationDataFromId } = useStationList();
const { stationList } = useStationList();
const { navigate } = useNavigation();
const [station, setStation] = useState<StationProps[]>([]);
// GPS追従中の点滅アニメーション
const pulseAnim = useRef(new Animated.Value(1)).current;
const isGpsFollowing = fixedPosition?.type === "nearestStation";
useEffect(() => {
if (!isGpsFollowing) {
pulseAnim.setValue(1);
return;
}
const loop = Animated.loop(
Animated.sequence([
Animated.timing(pulseAnim, { toValue: 0.4, duration: 600, useNativeDriver: true }),
Animated.timing(pulseAnim, { toValue: 1, duration: 600, useNativeDriver: true }),
])
);
loop.start();
return () => loop.stop();
}, [isGpsFollowing]);
useEffect(() => {
const data = getStationDataFromId(stationID);
setStation(data);
}, [stationID]);
const lineColor =
station.length > 0
? lineColorList[station[0]?.StationNumber.slice(0, 1)]
: "white";
////
const { allTrainDiagram, allCustomTrainData } = useAllTrainDiagram();
const { areaStationID } = useAreaInfo();
const [stationDiagram, setStationDiagram] = useState({}); //当該駅の全時刻表
const [isInfoArea, setIsInfoArea] = useState(false);
useEffect(() => {
// 現在の駅に停車するダイヤを作成する副作用[列車ダイヤと現在駅情報]
if (!allTrainDiagram) {
setStationDiagram({});
return;
}
if (station.length == 0) {
setStationDiagram({});
return;
}
let returnData = {};
Object.keys(allTrainDiagram).forEach((key) => {
if (allTrainDiagram[key].match(station[0].Station_JP + ",")) {
returnData[key] = allTrainDiagram[key];
}
});
setStationDiagram(returnData);
setIsInfoArea(station.some((s) => areaStationID.includes(s.StationNumber)));
}, [allTrainDiagram, station]);
const [trainTimeAndNumber, setTrainTimeAndNumber] = useState<
eachTrainDiagramType[]
>([]);
useEffect(() => {
//現在の駅に停車する列車から時刻を切り出してLEDベースにフォーマット
if (objectIsEmpty(stationDiagram)) return () => {};
const getTimeData = getTime(stationDiagram, station[0]);
setTrainTimeAndNumber(getTimeData);
}, [stationDiagram]);
const [selectedTrain, setSelectedTrain] = useState<eachTrainDiagramType[]>(
[]
);
useEffect(() => {
if (!trainTimeAndNumber) return () => {};
if (!currentTrain) return () => {};
const data = trainTimeAndNumber
.filter((d) => currentTrain.map((m) => m.num).includes(d.train)) //現在の列車に絞る[ToDo]
.filter((d) => trainTimeFiltering({ d, currentTrain, station })) //時間フィルター
.filter((d) => !d.isThrough)
.filter((d) => d.lastStation != station[0].Station_JP); //最終列車表示設定
setSelectedTrain(data);
}, [trainTimeAndNumber, currentTrain, tick /*liveActivity periodic refresh*/]);
// ── Live Notification ──
const [liveNotifyId, setLiveNotifyId] = useState<string | null>(null);
const liveNotifyIdRef = useRef<string | null>(null);
const hasStartedRef = useRef(false);
const [tick, setTick] = useState(0);
useEffect(() => {
liveNotifyIdRef.current = liveNotifyId;
}, [liveNotifyId]);
// Live Activity 起動中は 60 秒ごとに selectedTrain を強制再計算して時刻フィルタを再適用
useEffect(() => {
if (!liveNotifyId) return;
const interval = setInterval(() => setTick((t) => t + 1), 60000);
return () => clearInterval(interval);
}, [liveNotifyId]);
useEffect(() => {
return () => {
if (liveNotifyIdRef.current) {
endStationLockActivity(liveNotifyIdRef.current).catch(() => {});
setLiveNotificationActive(false);
}
};
}, []);
const buildTrainsInfo = useCallback((): StationTrainInfo[] => {
return selectedTrain.slice(0, 7).map((d) => {
const customData = getCurrentTrainData(
d.train,
currentTrain,
allCustomTrainData
);
const currentTrainDataForTrain = checkDuplicateTrainData(
currentTrain.filter((a) => a.num === d.train),
stationList
);
const { name, color: typeColor } = getTrainType({ type: customData.type, whiteMode: true });
const delayStatus = `${getTrainDelayStatus(
currentTrainDataForTrain,
station[0]?.Station_JP
)}`;
return {
time: d.time,
typeName: name,
trainName: customData.train_name || "",
destination: d.lastStation,
platform: "",
delayStatus: delayStatus || "定刻",
typeColor: typeColor || "",
trainNumber: d.train,
};
});
}, [selectedTrain, currentTrain, allCustomTrainData, stationList, station]);
useEffect(() => {
if (!liveNotifyId || station.length === 0) return;
const trains = buildTrainsInfo();
updateStationLockActivity(liveNotifyId, {
nextTrainTime: trains[0]?.time || "",
nextTrainDestination: trains[0]?.destination || "",
nextTrainPlatform: trains[0]?.platform || "",
followingTrainTime: trains[1]?.time || "",
followingTrainDestination: trains[1]?.destination || "",
stationName: station[0]?.Station_JP,
stationNumber: station[0]?.StationNumber,
lineColor,
trains,
}).catch(() => {});
}, [selectedTrain, currentTrain, liveNotifyId, buildTrainsInfo]);
// バナー表示と同時にLive Activityを自動開始selectedTrainが揃ってから
useEffect(() => {
if (station.length === 0 || hasStartedRef.current || liveNotifyId) return;
if (!isLiveActivityAvailable()) return;
hasStartedRef.current = true;
const startActivity = async () => {
if (Platform.OS === 'android' && Platform.Version >= 33) {
const granted = await PermissionsAndroid.request(
PermissionsAndroid.PERMISSIONS.POST_NOTIFICATIONS
);
if (granted !== PermissionsAndroid.RESULTS.GRANTED) return;
}
const trains = buildTrainsInfo();
try {
const id = await startStationLockActivity({
stationName: station[0]?.Station_JP || "",
nextTrainTime: trains[0]?.time || "",
nextTrainDestination: trains[0]?.destination || "",
nextTrainPlatform: trains[0]?.platform || "",
followingTrainTime: trains[1]?.time || "",
followingTrainDestination: trains[1]?.destination || "",
stationNumber: station[0]?.StationNumber,
lineColor,
trains,
});
setLiveNotifyId(id);
setLiveNotificationActive(true);
} catch (e) {
console.warn('[LiveNotify] start error:', e);
hasStartedRef.current = false;
}
};
startActivity();
}, [station, selectedTrain]);
return (
<View
style={{ display: "flex", flexDirection: "column", flex: 1 }}
pointerEvents="box-none"
>
<TouchableOpacity
style={{
flex: 1,
flexDirection: "row",
borderBottomColor: lineColor,
borderBottomWidth: 2,
position: "relative",
}}
activeOpacity={1}
onPress={() => {
const payload = {
currentStation: station,
navigate,
goTo: "menu",
onExit: () => SheetManager.hide("StationDetailView"),
};
//@ts-ignore
SheetManager.show("StationDetailView", { payload });
}}
>
<View
style={{
flex: 3,
flexDirection: "column",
alignContent: "center",
alignSelf: "center",
alignItems: "center",
height: "100%",
backgroundColor: colors.background,
}}
>
<View
style={{
backgroundColor: lineColor,
flexDirection: "row",
width: "100%",
alignContent: "center",
alignItems: "center",
height: 22,
overflow: "hidden",
paddingLeft: 5,
}}
>
<StationNumberMaker
currentStation={station}
singleSize={18}
useEach={true}
/>
<Text
style={{
fontSize: 14,
textAlignVertical: "center",
margin: 0,
padding: 0,
paddingLeft: 5,
flex: 1,
color: fixed.textOnPrimary,
}}
>
{station[0]?.Station_JP}
</Text>
<View
style={{
backgroundColor: colors.background,
width: 6,
borderLeftColor: lineColor,
borderTopColor: lineColor,
borderBottomColor: colors.background,
borderRightColor: colors.background,
borderBottomWidth: 18,
borderLeftWidth: 10,
borderRightWidth: 0,
borderTopWidth: 5,
height: 20,
}}
/>
</View>
<View
style={{
height: "100%",
backgroundColor: colors.background,
flex: 1,
}}
>
<Text style={{ fontSize: 18, color: colors.text }}></Text>
</View>
</View>
<View
style={{
flex: 5,
flexDirection: "column",
backgroundColor: colors.background,
borderTopWidth: 5,
borderTopColor: lineColor,
overflow: "hidden",
}}
>
{selectedTrain.length > 0 ? (
selectedTrain.map((d) => (
<FixedStationBoxEachTrain
d={d}
station={station[0]}
displaySize={fixedPositionSize}
key={d.train + "-fixedStationBox"}
/>
))
) : (
<View style={{ backgroundColor: colors.background, flex: 1 }}>
<Text style={{ fontSize: parseInt("11%"), color: colors.text }}>
</Text>
</View>
)}
</View>
</TouchableOpacity>
<View
style={{
flexDirection: "row",
justifyContent: "space-between",
borderTopColor: lineColor,
borderTopWidth: 2,
}}
pointerEvents="box-none"
>
<TouchableOpacity
style={{
flexDirection: "row",
alignItems: "center",
}}
onPress={() => {
setFixedPosition({ type: null, value: null });
}}
>
<View
style={{
flexDirection: "row",
alignItems: "center",
backgroundColor: lineColor,
paddingHorizontal: 5,
height: 26,
}}
>
{isGpsFollowing ? (
<Animated.View
style={{
flexDirection: "row",
alignItems: "center",
opacity: pulseAnim,
}}
>
<Ionicons name="navigate" size={15} color={fixed.textOnPrimary} />
<Text style={{ color: fixed.textOnPrimary, fontSize: 15, paddingRight: 5, paddingLeft: 3 }}>
GPS追従中
</Text>
</Animated.View>
) : (
<>
<Ionicons name="lock-closed" size={15} color={fixed.textOnPrimary} />
<Text style={{ color: fixed.textOnPrimary, fontSize: 15, paddingRight: 5 }}>
</Text>
</>
)}
<Ionicons name="close" size={15} color={fixed.textOnPrimary} />
</View>
<View
style={{
backgroundColor: "#0000",
width: 6,
borderLeftColor: lineColor,
borderTopColor: lineColor,
borderBottomColor: "#0000",
borderRightColor: "#0000",
borderBottomWidth: 26,
borderLeftWidth: 10,
borderRightWidth: 0,
borderTopWidth: 0,
height: 26,
}}
/>
</TouchableOpacity>
<TouchableOpacity
style={{
flexDirection: "row",
alignItems: "center",
}}
onPress={() => {
LayoutAnimation.configureNext({
duration: 500,
update: { type: "spring", springDamping: 0.7 },
});
if (fixedPositionSize === 226) {
setFixedPositionSize(mapSwitch == "true" ? 76 : 80);
} else {
setFixedPositionSize(226);
}
}}
>
<View
style={{
backgroundColor: "#0000",
width: 6,
borderLeftColor: "#0000",
borderTopColor: lineColor,
borderBottomColor: "#0000",
borderRightColor: lineColor,
borderBottomWidth: 26,
borderLeftWidth: 0,
borderRightWidth: 10,
borderTopWidth: 0,
height: 26,
}}
/>
<View
style={{
flexDirection: "row",
alignItems: "center",
backgroundColor: lineColor,
paddingHorizontal: 5,
height: 26,
}}
pointerEvents="none"
>
<Ionicons
name={fixedPositionSize == 226 ? "chevron-up" : "chevron-down"}
size={15}
color={fixed.textOnPrimary}
/>
<Text
style={{
color: fixed.textOnPrimary,
paddingRight: 5,
backgroundColor: lineColor,
fontSize: 15,
}}
>
{fixedPositionSize == 226
? "時刻表を縮小する"
: "時刻表を展開する"}
</Text>
</View>
</TouchableOpacity>
</View>
</View>
);
};
const FixedStationBoxEachTrain = ({ d, station, displaySize }) => {
const { colors, isDark } = useThemeColors();
const { currentTrain } = useCurrentTrain();
const { stationList } = useStationList();
const { allCustomTrainData } = useAllTrainDiagram();
const currentTrainData = checkDuplicateTrainData(
currentTrain.filter((a) => a.num == d.train),
stationList
);
const trainDelayStatus = `${getTrainDelayStatus(
currentTrainData,
station.Station_JP
)}`;
const [train, setTrain] = useState<CustomTrainData>(
getCurrentTrainData(d.train, currentTrain, allCustomTrainData)
);
useEffect(() => {
setTrain(getCurrentTrainData(d.train, currentTrain, allCustomTrainData));
}, [currentTrain, d.train]);
const { name, color } = getTrainType({ type: train.type, whiteMode: !isDark });
return (
<View
style={{
backgroundColor: colors.background,
flexDirection: "row",
height: displaySize == 226 ? "7.5%" : "33%",
overflow: "visible",
}}
>
<Text style={{ fontSize: parseInt("11%"), flex: 3, color: colors.text }}>{d.time}</Text>
<Text style={{ fontSize: parseInt("11%"), flex: 4, color }}>{name}</Text>
<Text style={{ fontSize: parseInt("11%"), flex: 4, color: colors.text }}>
{d.lastStation}
</Text>
<Text style={{ fontSize: parseInt("11%"), flex: 3, color: colors.text }}>
{trainDelayStatus}
</Text>
</View>
);
};