Files
jrshikoku/components/Apps.tsx

380 lines
12 KiB
TypeScript

import React from "react";
import { InteractionManager } from "react-native";
import {
View,
Platform,
useWindowDimensions,
StatusBar,
useColorScheme,
Button,
} from "react-native";
import * as Updates from "expo-updates";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { LinearGradient } from "expo-linear-gradient";
import lineColorList from "../assets/originData/lineColorList";
import { lineList, stationIDPair } from "../lib/getStationList";
import { useCurrentTrain } from "../stateBox/useCurrentTrain";
import { useDeviceOrientationChange } from "../stateBox/useDeviceOrientationChange";
import { SheetManager } from "react-native-actions-sheet";
import { useNavigation, useIsFocused } from "@react-navigation/native";
import { useTrainMenu } from "../stateBox/useTrainMenu";
import { AppsWebView } from "./Apps/WebView";
import { NewMenu } from "./Apps/NewMenu";
import { MapsButton } from "./Apps/MapsButton";
import { ReloadButton } from "./Apps/ReloadButton";
import { useStationList } from "../stateBox/useStationList";
import { positionsLifecycleRef } from "../lib/rootNavigation";
import { FixedPositionBox } from "./Apps/FixedPositionBox";
import { PlaybackTimeline } from "./Apps/PlaybackTimeline";
import { RecordingStatusBar } from "./Apps/RecordingStatusBar";
import * as Sentry from "@sentry/react-native";
export default function Apps() {
const { webview, fixedPosition, setFixedPosition } = useCurrentTrain();
const { height, width } = useWindowDimensions();
const { navigate } = useNavigation<any>();
const { isLandscape } = useDeviceOrientationChange();
const { top } = useSafeAreaInsets();
const { originalStationList } = useStationList();
const { mapSwitch, trainInfo, setTrainInfo, selectedLine } = useTrainMenu();
const isDark = useColorScheme() === "dark";
const isFocused = useIsFocused();
const [hasActivatedScreen, setHasActivatedScreen] = React.useState(false);
const [hasActivatedWebView, setHasActivatedWebView] = React.useState(false);
const [hasStableWebViewSession, setHasStableWebViewSession] = React.useState(false);
const lastFocusSignatureRef = React.useRef("");
const lastLayoutSignatureRef = React.useRef("");
const lineColor = selectedLine && stationIDPair[selectedLine]
? lineColorList[stationIDPair[selectedLine]]
: null;
// 路線色の両端を暗くしたグラデーション用カラー
const darkenHex = (hex: string, factor: number) => {
const h = hex.replace("#", "");
const r = Math.round(parseInt(h.slice(0, 2), 16) * factor);
const g = Math.round(parseInt(h.slice(2, 4), 16) * factor);
const b = Math.round(parseInt(h.slice(4, 6), 16) * factor);
return `#${[r, g, b].map((v) => Math.min(255, v).toString(16).padStart(2, "0")).join("")}`;
};
const lineColorDark = lineColor ? darkenHex(lineColor, 0.55) : null;
const openStationACFromEachTrainInfo = async (stationName) => {
await SheetManager.hide("EachTrainInfo");
const findStationEachLine = (selectLine) => {
let NearStation = selectLine.filter((d) => d.Station_JP == stationName);
return NearStation;
};
let returnDataBase = lineList
.map((d) => findStationEachLine(originalStationList[d]))
.filter((d) => d.length > 0)
.reduce((pre, current) => {
pre.push(...current);
return pre;
}, []);
if (returnDataBase.length) {
const payload = {
currentStation: returnDataBase,
navigate,
goTo: "Apps",
useShow: () => {
SheetManager.show("StationDetailView", { payload });
},
onExit: () => SheetManager.hide("StationDetailView"),
};
setTimeout(() => {
SheetManager.show("StationDetailView", { payload });
}, 50);
} else {
SheetManager.hide("StationDetailView");
}
};
const bgColor = isDark ? "#1c1c1e" : "#ffffff";
const resetTransientPositionsSession = React.useCallback(() => {
Sentry.addBreadcrumb({
category: "positions.screen",
level: "info",
message: hasStableWebViewSession
? "positions stable session preserved before leave"
: "positions transient session reset before leave",
data: {
stableWebViewSession: hasStableWebViewSession,
},
});
positionsLifecycleRef.current = {
...positionsLifecycleRef.current,
isUnstable: false,
blockTabExit: false,
};
if (hasStableWebViewSession) {
return;
}
try {
webview.current?.stopLoading?.();
} catch {}
setHasActivatedWebView(false);
setHasActivatedScreen(false);
setHasStableWebViewSession(false);
}, [hasStableWebViewSession, webview]);
React.useEffect(() => {
Sentry.addBreadcrumb({
category: "positions.screen",
level: "info",
message: "positions screen mounted",
});
return () => {
Sentry.addBreadcrumb({
category: "positions.screen",
level: "info",
message: "positions screen unmounted",
});
};
}, []);
React.useEffect(() => {
if (!isFocused) {
if (!hasActivatedWebView && !hasStableWebViewSession) {
// allow one hidden startup prewarm so the positions session can begin before first tab focus
} else {
Sentry.addBreadcrumb({
category: "positions.screen",
level: "info",
message: "positions webview preserved on blur",
data: {
stableWebViewSession: hasStableWebViewSession,
activatedWebView: hasActivatedWebView,
},
});
return;
}
}
if (hasActivatedScreen && hasActivatedWebView) return;
const activationReason = isFocused
? (hasActivatedScreen ? "focus" : "first_focus")
: "background_prewarm";
Sentry.addBreadcrumb({
category: "positions.screen",
level: "info",
message: "positions screen activation scheduled",
data: {
reason: activationReason,
},
});
let cancelled = false;
const activate = () => {
if (cancelled) {
Sentry.addBreadcrumb({
category: "positions.screen",
level: "info",
message: "positions screen activation cancelled before webview mount",
data: {
reason: activationReason,
},
});
return;
}
Sentry.addBreadcrumb({
category: "positions.screen",
level: "info",
message: "positions screen activation committed",
data: {
reason: activationReason,
},
});
setHasActivatedScreen(true);
setHasActivatedWebView(true);
};
if (isFocused) {
const task = InteractionManager.runAfterInteractions(() => {
setTimeout(activate, 250);
});
return () => {
cancelled = true;
task.cancel?.();
};
}
const timer = setTimeout(activate, 350);
return () => {
cancelled = true;
clearTimeout(timer);
};
}, [hasActivatedScreen, hasActivatedWebView, hasStableWebViewSession, isFocused]);
React.useEffect(() => {
const shouldGuardTabExit = isFocused && hasActivatedWebView && !hasStableWebViewSession;
positionsLifecycleRef.current = {
isUnstable: shouldGuardTabExit,
blockTabExit: shouldGuardTabExit,
resetBeforeLeave: resetTransientPositionsSession,
deactivateStack: positionsLifecycleRef.current.deactivateStack,
};
return () => {
positionsLifecycleRef.current = {
isUnstable: false,
blockTabExit: false,
resetBeforeLeave: null,
deactivateStack: positionsLifecycleRef.current.deactivateStack,
};
};
}, [hasActivatedWebView, hasStableWebViewSession, isFocused, resetTransientPositionsSession]);
React.useEffect(() => {
Sentry.setContext("positions_screen", {
focused: isFocused,
activatedScreen: hasActivatedScreen,
activatedWebView: hasActivatedWebView,
stableWebViewSession: hasStableWebViewSession,
mapSwitch,
selectedLine: selectedLine ?? null,
trainNum: trainInfo?.trainNum ?? null,
landscape: isLandscape,
width,
height,
});
}, [hasActivatedScreen, hasActivatedWebView, hasStableWebViewSession, height, isFocused, isLandscape, mapSwitch, selectedLine, trainInfo?.trainNum, width]);
React.useEffect(() => {
const signature = JSON.stringify({
focused: isFocused,
mapSwitch,
selectedLine: selectedLine ?? null,
trainNum: trainInfo?.trainNum ?? null,
landscape: isLandscape,
});
if (lastFocusSignatureRef.current === signature) return;
lastFocusSignatureRef.current = signature;
Sentry.addBreadcrumb({
category: "positions.screen",
level: "info",
message: isFocused ? "positions focused" : "positions blurred",
data: {
mapSwitch,
selectedLine: selectedLine ?? null,
trainNum: trainInfo?.trainNum ?? null,
landscape: isLandscape,
},
});
}, [hasActivatedWebView, isFocused, isLandscape, mapSwitch, selectedLine, trainInfo?.trainNum]);
const handleLayout = React.useCallback((event) => {
const layout = event?.nativeEvent?.layout;
if (!layout) return;
const signature = [Math.round(layout.width), Math.round(layout.height), isLandscape ? "landscape" : "portrait"].join(":");
if (lastLayoutSignatureRef.current === signature) return;
lastLayoutSignatureRef.current = signature;
Sentry.addBreadcrumb({
category: "positions.screen",
level: "info",
message: "positions layout",
data: {
width: Math.round(layout.width),
height: Math.round(layout.height),
landscape: isLandscape,
},
});
}, [isLandscape]);
return (
<View style={{ flex: 1, backgroundColor: bgColor }}>
{isFocused && (
<View
pointerEvents="none"
style={{ position: "absolute", top: 0, left: 0, right: 0, height: top }}
>
{mapSwitch === "true" && lineColor && lineColorDark ? (
<LinearGradient
colors={[lineColorDark, lineColor]}
start={{ x: 0, y: 0 }}
end={{ x: 0, y: 1 }}
style={{ flex: 1 }}
/>
) : (
<View
style={{
flex: 1,
backgroundColor: "#0099CC",
}}
/>
)}
</View>
)}
{isFocused && hasActivatedScreen && (
<StatusBar barStyle="light-content" />
)}
<View
style={{
flex: 1,
paddingTop: top,
flexDirection: isLandscape ? "row" : "column",
}}
onLayout={handleLayout}
>
{hasActivatedScreen ? (
<>
{hasActivatedWebView ? (
<AppsWebView
openStationACFromEachTrainInfo={openStationACFromEachTrainInfo}
onInitialLoadReady={() => {
if (hasStableWebViewSession) return;
Sentry.addBreadcrumb({
category: "positions.screen",
level: "info",
message: "positions webview became stable",
});
positionsLifecycleRef.current = {
...positionsLifecycleRef.current,
isUnstable: false,
blockTabExit: false,
};
setHasStableWebViewSession(true);
}}
/>
) : (
<View style={{ flex: 1 }} />
)}
{isFocused && (
<MapsButton
onPress={() => {
Sentry.addBreadcrumb({
category: "positions.screen",
level: "info",
message: "maps button press",
data: {
mapSwitch,
selectedLine: selectedLine ?? null,
hasActivatedWebView,
},
});
navigate("trainMenu", { webview });
}}
/>
)}
{isFocused && fixedPosition.type && <FixedPositionBox />}
{isFocused && <PlaybackTimeline />}
{isFocused && <RecordingStatusBar />}
{isFocused &&
(mapSwitch == "true" ? (
<ReloadButton
onPress={() => Updates.reloadAsync()}
right={isLandscape && trainInfo.trainNum ? (width / 100) * 40 : 0}
/>
) : (
<NewMenu />
))}
</>
) : (
<View style={{ flex: 1 }} />
)}
</View>
</View>
);
}