380 lines
12 KiB
TypeScript
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>
|
|
);
|
|
}
|