Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c678700436 | ||
|
|
e6f9268535 | ||
|
|
d31b6cf721 | ||
|
|
c2c20a6a83 |
@@ -0,0 +1,28 @@
|
||||
name: Verify
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
verify:
|
||||
name: Typecheck, lint, and unit tests
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Use the project Node version
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24.9.0
|
||||
cache: yarn
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
|
||||
- name: Required verification
|
||||
run: yarn verify
|
||||
+1
-2
@@ -10,8 +10,7 @@
|
||||
"Read(//home/ubuntu/.qwen/debug/**)",
|
||||
"Bash(curl *)",
|
||||
"mcp__sentry__search_events",
|
||||
"mcp__sentry__find_organizations",
|
||||
"Skill(jr-shikoku-architecture)"
|
||||
"mcp__sentry__find_organizations"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -7,15 +7,8 @@ import { useAppLifecycleObservability } from "./lib/observability/useAppLifecycl
|
||||
import { useAppBootstrap } from "./lib/useAppBootstrap";
|
||||
import { useStartupDeepLinkNavigation } from "./lib/useStartupDeepLinkNavigation";
|
||||
import { AppRootLayout } from "./components/AppRootLayout";
|
||||
import { AppErrorBoundary } from "./components/AppErrorBoundary";
|
||||
import {
|
||||
markStartup,
|
||||
markStartupOnce,
|
||||
scheduleStartupReports,
|
||||
} from "./lib/devStartupPerformance";
|
||||
|
||||
initializeSentry();
|
||||
markStartupOnce("app-module-ready", "app module ready");
|
||||
|
||||
LogBox.ignoreLogs([
|
||||
"ViewPropTypes will be removed",
|
||||
@@ -42,19 +35,9 @@ if (Platform.OS === "android") {
|
||||
}
|
||||
|
||||
export default wrapWithSentry(function App() {
|
||||
markStartupOnce("app-render", "App render");
|
||||
scheduleStartupReports();
|
||||
useAppLifecycleObservability({ nativeScreensMode, rootTabSceneMode });
|
||||
useAppBootstrap();
|
||||
useStartupDeepLinkNavigation();
|
||||
|
||||
React.useEffect(() => {
|
||||
markStartup("App effect committed");
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<AppErrorBoundary scope="root">
|
||||
<AppRootLayout />
|
||||
</AppErrorBoundary>
|
||||
);
|
||||
return <AppRootLayout />;
|
||||
});
|
||||
|
||||
+118
-207
@@ -18,7 +18,6 @@ import TNDView from "./ndView";
|
||||
import { initIcon } from "./lib/initIcon";
|
||||
import { Top } from "./Top";
|
||||
import { MenuPage } from "./MenuPage";
|
||||
import { AppErrorBoundary } from "./components/AppErrorBoundary";
|
||||
import "./components/ActionSheetComponents/sheets";
|
||||
import { rootNavigationRef } from "./lib/rootNavigation";
|
||||
import WebView from "react-native-webview";
|
||||
@@ -27,17 +26,10 @@ import type { RootTabParamList } from "@/types/navigation";
|
||||
import { useThemeColors } from "./lib/theme";
|
||||
import { telemetry } from "./lib/observability/telemetry";
|
||||
import { useAppContainerController } from "./lib/useAppContainerController";
|
||||
import { useCurrentTrainPolling } from "./stateBox/useCurrentTrain";
|
||||
import {
|
||||
getTabBarAppearance,
|
||||
TabBarBackground,
|
||||
} from "./components/AppContainer/TabBarBackground";
|
||||
import {
|
||||
markStartup,
|
||||
markStartupOnce,
|
||||
markStartupState,
|
||||
} from "./lib/devStartupPerformance";
|
||||
import { PositionsStartupReadyContext } from "./lib/positionsStartupContext";
|
||||
|
||||
const startupPreloadTelemetry = telemetry.scope("startup.preload");
|
||||
const positionsRootTelemetry = telemetry.scope("positions.root");
|
||||
@@ -46,24 +38,27 @@ const operationRootTelemetry = telemetry.scope("operation.root");
|
||||
const Tab = createBottomTabNavigator<RootTabParamList>();
|
||||
|
||||
const HiddenStartupPreloadWebViews = ({
|
||||
shouldPreloadPositions,
|
||||
shouldPreloadInformation,
|
||||
onInformationLoadStart,
|
||||
onInformationLoadSettled,
|
||||
}: {
|
||||
shouldPreloadPositions: boolean;
|
||||
shouldPreloadInformation: boolean;
|
||||
onInformationLoadStart: () => void;
|
||||
onInformationLoadSettled: () => void;
|
||||
}) => {
|
||||
const [positionsLoaded, setPositionsLoaded] = useState(false);
|
||||
const [informationLoaded, setInformationLoaded] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
telemetry.context("startup_hidden_preload", {
|
||||
shouldPreloadPositions,
|
||||
shouldPreloadInformation,
|
||||
positionsLoaded,
|
||||
informationLoaded,
|
||||
});
|
||||
}, [
|
||||
informationLoaded,
|
||||
positionsLoaded,
|
||||
shouldPreloadInformation,
|
||||
shouldPreloadPositions,
|
||||
]);
|
||||
|
||||
const hiddenStyle = useMemo(
|
||||
@@ -79,26 +74,34 @@ const HiddenStartupPreloadWebViews = ({
|
||||
[],
|
||||
);
|
||||
|
||||
const showPositions = shouldPreloadPositions && !positionsLoaded;
|
||||
const showInformation = shouldPreloadInformation && !informationLoaded;
|
||||
|
||||
useEffect(() => {
|
||||
markStartupState(
|
||||
"hidden-startup-preload-state",
|
||||
"hidden startup preload state",
|
||||
{
|
||||
showInformation,
|
||||
informationLoaded,
|
||||
},
|
||||
);
|
||||
}, [
|
||||
informationLoaded,
|
||||
showInformation,
|
||||
]);
|
||||
|
||||
if (!showInformation) return null;
|
||||
if (!showPositions && !showInformation) return null;
|
||||
|
||||
return (
|
||||
<View pointerEvents="none" style={hiddenStyle}>
|
||||
{showPositions ? (
|
||||
<WebView
|
||||
source={{ uri: "https://train.jr-shikoku.co.jp/" }}
|
||||
originWhitelist={["https://train.jr-shikoku.co.jp"]}
|
||||
javaScriptEnabled
|
||||
setSupportMultipleWindows={false}
|
||||
onLoadStart={() => {
|
||||
setAppLifecycleWebViewActive("startup_hidden_positions", true);
|
||||
startupPreloadTelemetry.info("positions hidden preload loadStart");
|
||||
}}
|
||||
onLoadEnd={() => {
|
||||
setAppLifecycleWebViewActive("startup_hidden_positions", false);
|
||||
startupPreloadTelemetry.info("positions hidden preload loadEnd");
|
||||
setPositionsLoaded(true);
|
||||
}}
|
||||
onError={() => {
|
||||
setAppLifecycleWebViewActive("startup_hidden_positions", false);
|
||||
startupPreloadTelemetry.error("positions hidden preload error");
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
{showInformation ? (
|
||||
<WebView
|
||||
source={{ uri: "https://www.jr-shikoku.co.jp/info/" }}
|
||||
@@ -106,18 +109,15 @@ const HiddenStartupPreloadWebViews = ({
|
||||
javaScriptEnabled
|
||||
setSupportMultipleWindows={false}
|
||||
onLoadStart={() => {
|
||||
onInformationLoadStart();
|
||||
setAppLifecycleWebViewActive("startup_hidden_operation", true);
|
||||
startupPreloadTelemetry.info("operation hidden preload loadStart");
|
||||
}}
|
||||
onLoadEnd={() => {
|
||||
onInformationLoadSettled();
|
||||
setAppLifecycleWebViewActive("startup_hidden_operation", false);
|
||||
startupPreloadTelemetry.info("operation hidden preload loadEnd");
|
||||
setInformationLoaded(true);
|
||||
}}
|
||||
onError={() => {
|
||||
onInformationLoadSettled();
|
||||
setAppLifecycleWebViewActive("startup_hidden_operation", false);
|
||||
startupPreloadTelemetry.error("operation hidden preload error");
|
||||
}}
|
||||
@@ -143,11 +143,6 @@ const DeferredPositionsRoot = (props: DeferredRootProps) => {
|
||||
const [hasActivated, setHasActivated] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
markStartupState("positions-root-gate", "positions root gate state", {
|
||||
focused: isRootFocused,
|
||||
activated: hasActivated,
|
||||
shouldActivate,
|
||||
});
|
||||
telemetry.context("positions_root_gate", {
|
||||
focused: isRootFocused,
|
||||
activated: hasActivated,
|
||||
@@ -157,19 +152,12 @@ const DeferredPositionsRoot = (props: DeferredRootProps) => {
|
||||
|
||||
useEffect(() => {
|
||||
if (!shouldActivate || hasActivated) return;
|
||||
markStartup("positions root activation scheduled", {
|
||||
focused: isRootFocused,
|
||||
});
|
||||
positionsRootTelemetry.breadcrumb({
|
||||
message: "positions root activation",
|
||||
data: {
|
||||
reason: "first_root_visit",
|
||||
},
|
||||
});
|
||||
markStartupOnce(
|
||||
"positions-root-activated",
|
||||
"positions root activation committed",
|
||||
);
|
||||
setHasActivated(true);
|
||||
}, [hasActivated, shouldActivate]);
|
||||
|
||||
@@ -177,11 +165,7 @@ const DeferredPositionsRoot = (props: DeferredRootProps) => {
|
||||
return <View style={{ flex: 1 }} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<AppErrorBoundary scope="positions">
|
||||
<Top />
|
||||
</AppErrorBoundary>
|
||||
);
|
||||
return <Top />;
|
||||
};
|
||||
|
||||
const PositionsTabScreen = memo(() => {
|
||||
@@ -202,11 +186,6 @@ const DeferredInformationRoot = (props: DeferredRootProps) => {
|
||||
const [hasActivated, setHasActivated] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
markStartupState("operation-root-gate", "operation root gate state", {
|
||||
focused: isRootFocused,
|
||||
activated: hasActivated,
|
||||
shouldActivate,
|
||||
});
|
||||
telemetry.context("operation_root_gate", {
|
||||
focused: isRootFocused,
|
||||
activated: hasActivated,
|
||||
@@ -216,19 +195,12 @@ const DeferredInformationRoot = (props: DeferredRootProps) => {
|
||||
|
||||
useEffect(() => {
|
||||
if (!shouldActivate || hasActivated) return;
|
||||
markStartup("operation root activation scheduled", {
|
||||
focused: isRootFocused,
|
||||
});
|
||||
operationRootTelemetry.breadcrumb({
|
||||
message: "operation root activation",
|
||||
data: {
|
||||
reason: isRootFocused ? "first_focus" : "background_prewarm",
|
||||
},
|
||||
});
|
||||
markStartupOnce(
|
||||
"operation-root-activated",
|
||||
"operation root activation committed",
|
||||
);
|
||||
setHasActivated(true);
|
||||
}, [hasActivated, isRootFocused, shouldActivate]);
|
||||
|
||||
@@ -236,19 +208,9 @@ const DeferredInformationRoot = (props: DeferredRootProps) => {
|
||||
return <View style={{ flex: 1 }} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<AppErrorBoundary scope="information">
|
||||
<TNDView />
|
||||
</AppErrorBoundary>
|
||||
);
|
||||
return <TNDView />;
|
||||
};
|
||||
|
||||
const MenuTabScreen = () => (
|
||||
<AppErrorBoundary scope="topMenu">
|
||||
<MenuPage />
|
||||
</AppErrorBoundary>
|
||||
);
|
||||
|
||||
const InformationTabScreen = memo(() => {
|
||||
const { shouldActivate, isRootFocused } = useContext(
|
||||
InformationRootGateContext,
|
||||
@@ -263,7 +225,6 @@ const InformationTabScreen = memo(() => {
|
||||
InformationTabScreen.displayName = "InformationTabScreen";
|
||||
|
||||
export function AppContainer() {
|
||||
markStartupOnce("app-container-render", "AppContainer render");
|
||||
const controller = useAppContainerController();
|
||||
const {
|
||||
areaInfo,
|
||||
@@ -274,47 +235,14 @@ export function AppContainer() {
|
||||
startupInitialRoute,
|
||||
hasVisitedPositions,
|
||||
hasVisitedInformation,
|
||||
positionsInitialLoadReady,
|
||||
currentRootRoute,
|
||||
fontLoaded,
|
||||
startupIconFontsLoaded,
|
||||
shouldPreloadPositions,
|
||||
shouldPreloadInformation,
|
||||
handlePositionsInitialLoadReady,
|
||||
handlePositionsStackReady,
|
||||
handlePositionsWebViewLoadStarted,
|
||||
handleOperationWebViewLoadStarted,
|
||||
handleOperationWebViewLoadSettled,
|
||||
positionsStackReady,
|
||||
positionsWebViewLoadStarted,
|
||||
operationWebViewPreloadActive,
|
||||
operationWebViewLoadStarted,
|
||||
operationWebViewLoadSettled,
|
||||
} = controller;
|
||||
|
||||
// トップメニューの列車情報だけは、画面のmountや走行位置WebViewを待たず
|
||||
// 既存のCurrentTrainProviderを購読して初回取得を開始する。
|
||||
useCurrentTrainPolling(
|
||||
startupInitialRoute === "topMenu" &&
|
||||
(currentRootRoute === null || currentRootRoute === "topMenu"),
|
||||
);
|
||||
|
||||
const { isDark } = useThemeColors();
|
||||
const operationWebViewPreloadPending =
|
||||
operationWebViewPreloadActive;
|
||||
|
||||
useEffect(() => {
|
||||
markStartupState("app-container-gate", "AppContainer gate state", {
|
||||
fontLoaded,
|
||||
startupIconFontsLoaded,
|
||||
startupInitialRoute,
|
||||
navigationReady: controller.navigationReady,
|
||||
});
|
||||
}, [
|
||||
controller.navigationReady,
|
||||
fontLoaded,
|
||||
startupIconFontsLoaded,
|
||||
startupInitialRoute,
|
||||
]);
|
||||
|
||||
const linking = {
|
||||
prefixes: ["jrshikoku://"],
|
||||
@@ -356,7 +284,7 @@ export function AppContainer() {
|
||||
tabBarIcon: initIcon(icon as any, iconFamily, tabBarBadge, isInfo),
|
||||
},
|
||||
});
|
||||
if (!fontLoaded || !startupInitialRoute) {
|
||||
if (!fontLoaded || !startupIconFontsLoaded || !startupInitialRoute) {
|
||||
return (
|
||||
<View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
|
||||
<ActivityIndicator size="large" />
|
||||
@@ -371,119 +299,102 @@ export function AppContainer() {
|
||||
onReady={controller.handleNavigationReady}
|
||||
onStateChange={controller.handleNavigationStateChange}
|
||||
>
|
||||
<PositionsStartupReadyContext.Provider
|
||||
<PositionsRootGateContext.Provider
|
||||
value={{
|
||||
stackReady: positionsStackReady,
|
||||
webViewLoadStarted: positionsWebViewLoadStarted,
|
||||
initialLoadReady: positionsInitialLoadReady,
|
||||
operationWebViewPreloadPending,
|
||||
operationWebViewLoadStarted,
|
||||
operationWebViewLoadSettled,
|
||||
notifyInitialLoadReady: handlePositionsInitialLoadReady,
|
||||
notifyStackReady: handlePositionsStackReady,
|
||||
notifyWebViewLoadStarted: handlePositionsWebViewLoadStarted,
|
||||
notifyOperationWebViewLoadStarted: handleOperationWebViewLoadStarted,
|
||||
notifyOperationWebViewLoadSettled: handleOperationWebViewLoadSettled,
|
||||
shouldActivate: hasVisitedPositions,
|
||||
isRootFocused: currentRootRoute === "positions",
|
||||
}}
|
||||
>
|
||||
<PositionsRootGateContext.Provider
|
||||
<InformationRootGateContext.Provider
|
||||
value={{
|
||||
shouldActivate: hasVisitedPositions,
|
||||
isRootFocused: currentRootRoute === "positions",
|
||||
shouldActivate: hasVisitedInformation,
|
||||
isRootFocused: currentRootRoute === "information",
|
||||
}}
|
||||
>
|
||||
<InformationRootGateContext.Provider
|
||||
value={{
|
||||
shouldActivate: hasVisitedInformation,
|
||||
isRootFocused: currentRootRoute === "information",
|
||||
<Tab.Navigator
|
||||
id="rootTabs"
|
||||
detachInactiveScreens={false}
|
||||
initialRouteName={startupInitialRoute}
|
||||
screenListeners={({ route }) => ({
|
||||
tabPress: (event: EventArg<"tabPress", true>) => {
|
||||
controller.handleRootTabPress(route.name, event);
|
||||
},
|
||||
focus: () => {
|
||||
controller.handleRootTabFocus(route.name);
|
||||
},
|
||||
blur: () => {
|
||||
controller.handleRootTabBlur(route.name);
|
||||
},
|
||||
state: (event) => {
|
||||
controller.handleRootTabState(route.name, event.type);
|
||||
},
|
||||
})}
|
||||
screenOptions={({ route }) => {
|
||||
const tabBarBackgroundProps = {
|
||||
routeName: route.name,
|
||||
selectedLine,
|
||||
isDark,
|
||||
isExtraWindowOpen,
|
||||
};
|
||||
const {
|
||||
defaultBg,
|
||||
activeTintColor,
|
||||
inactiveTintColor,
|
||||
} = getTabBarAppearance(tabBarBackgroundProps);
|
||||
return {
|
||||
lazy: false,
|
||||
freezeOnBlur: false,
|
||||
sceneContainerStyle: { backgroundColor: defaultBg },
|
||||
tabBarActiveTintColor: activeTintColor,
|
||||
tabBarInactiveTintColor: inactiveTintColor,
|
||||
tabBarStyle: { backgroundColor: "transparent" },
|
||||
tabBarBackground: () => (
|
||||
<TabBarBackground {...tabBarBackgroundProps} />
|
||||
),
|
||||
};
|
||||
}}
|
||||
>
|
||||
<Tab.Navigator
|
||||
id="rootTabs"
|
||||
detachInactiveScreens={false}
|
||||
initialRouteName={startupInitialRoute}
|
||||
screenListeners={({ route }) => ({
|
||||
tabPress: (event: EventArg<"tabPress", true>) => {
|
||||
controller.handleRootTabPress(route.name, event);
|
||||
},
|
||||
focus: () => {
|
||||
controller.handleRootTabFocus(route.name);
|
||||
},
|
||||
blur: () => {
|
||||
controller.handleRootTabBlur(route.name);
|
||||
},
|
||||
state: (event) => {
|
||||
controller.handleRootTabState(route.name, event.type);
|
||||
},
|
||||
})}
|
||||
screenOptions={({ route }) => {
|
||||
const tabBarBackgroundProps = {
|
||||
routeName: route.name,
|
||||
selectedLine,
|
||||
isDark,
|
||||
isExtraWindowOpen,
|
||||
};
|
||||
const {
|
||||
defaultBg,
|
||||
activeTintColor,
|
||||
inactiveTintColor,
|
||||
} = getTabBarAppearance(tabBarBackgroundProps);
|
||||
return {
|
||||
lazy: false,
|
||||
freezeOnBlur: false,
|
||||
sceneContainerStyle: { backgroundColor: defaultBg },
|
||||
tabBarActiveTintColor: activeTintColor,
|
||||
tabBarInactiveTintColor: inactiveTintColor,
|
||||
tabBarStyle: { backgroundColor: "transparent" },
|
||||
tabBarBackground: () => (
|
||||
<TabBarBackground {...tabBarBackgroundProps} />
|
||||
),
|
||||
};
|
||||
<Tab.Screen
|
||||
{...getTabProps(
|
||||
"positions",
|
||||
"走行位置",
|
||||
"bar-chart",
|
||||
"AntDesign",
|
||||
)}
|
||||
component={PositionsTabScreen}
|
||||
listeners={{
|
||||
tabPress: controller.activatePositionsFromTabPress,
|
||||
focus: controller.activatePositionsFromFocus,
|
||||
blur: controller.preservePositionsOnBlur,
|
||||
}}
|
||||
>
|
||||
<Tab.Screen
|
||||
{...getTabProps(
|
||||
"positions",
|
||||
"走行位置",
|
||||
"bar-chart",
|
||||
"AntDesign",
|
||||
)}
|
||||
component={PositionsTabScreen}
|
||||
listeners={{
|
||||
tabPress: controller.activatePositionsFromTabPress,
|
||||
focus: controller.activatePositionsFromFocus,
|
||||
blur: controller.preservePositionsOnBlur,
|
||||
}}
|
||||
/>
|
||||
<Tab.Screen
|
||||
{...getTabProps("topMenu", "トップメニュー", "radio", "Ionicons")}
|
||||
component={MenuTabScreen}
|
||||
/>
|
||||
/>
|
||||
<Tab.Screen
|
||||
{...getTabProps("topMenu", "トップメニュー", "radio", "Ionicons")}
|
||||
component={MenuPage}
|
||||
/>
|
||||
|
||||
<Tab.Screen
|
||||
{...getTabProps(
|
||||
"information",
|
||||
"運行情報",
|
||||
"train",
|
||||
"Ionicons",
|
||||
areaInfo ? areaIconBadgeText : undefined,
|
||||
isInfo,
|
||||
)}
|
||||
component={InformationTabScreen}
|
||||
listeners={{
|
||||
tabPress: controller.activateInformationFromTabPress,
|
||||
focus: controller.activateInformationFromFocus,
|
||||
blur: controller.preserveInformationOnBlur,
|
||||
}}
|
||||
/>
|
||||
</Tab.Navigator>
|
||||
</InformationRootGateContext.Provider>
|
||||
</PositionsRootGateContext.Provider>
|
||||
</PositionsStartupReadyContext.Provider>
|
||||
<Tab.Screen
|
||||
{...getTabProps(
|
||||
"information",
|
||||
"運行情報",
|
||||
"train",
|
||||
"Ionicons",
|
||||
areaInfo ? areaIconBadgeText : undefined,
|
||||
isInfo,
|
||||
)}
|
||||
component={InformationTabScreen}
|
||||
listeners={{
|
||||
tabPress: controller.activateInformationFromTabPress,
|
||||
focus: controller.activateInformationFromFocus,
|
||||
blur: controller.preserveInformationOnBlur,
|
||||
}}
|
||||
/>
|
||||
</Tab.Navigator>
|
||||
</InformationRootGateContext.Provider>
|
||||
</PositionsRootGateContext.Provider>
|
||||
<HiddenStartupPreloadWebViews
|
||||
shouldPreloadPositions={shouldPreloadPositions}
|
||||
shouldPreloadInformation={shouldPreloadInformation}
|
||||
onInformationLoadStart={handleOperationWebViewLoadStarted}
|
||||
onInformationLoadSettled={handleOperationWebViewLoadSettled}
|
||||
/>
|
||||
</NavigationContainer>
|
||||
);
|
||||
+11
-36
@@ -119,7 +119,6 @@ export default ({ route }) => {
|
||||
uri,
|
||||
useExitButton = true,
|
||||
importRecordingDownloads = false,
|
||||
onExit,
|
||||
} = route.params;
|
||||
const { goBack } = useNavigation();
|
||||
const { fixed } = useThemeColors();
|
||||
@@ -133,8 +132,6 @@ export default ({ route }) => {
|
||||
DEFAULT_JR_DATA_SYSTEM_ENV,
|
||||
);
|
||||
const [resolvedUri, setResolvedUri] = React.useState("");
|
||||
const [webViewSourceUri, setWebViewSourceUri] = React.useState("");
|
||||
const resolvedUriRef = React.useRef("");
|
||||
const [isEnvironmentReady, setIsEnvironmentReady] = React.useState(false);
|
||||
const hasAlerted = React.useRef(false);
|
||||
const [isLoading, setIsLoading] = React.useState(true);
|
||||
@@ -151,11 +148,6 @@ export default ({ route }) => {
|
||||
const blankCountRef = React.useRef(0);
|
||||
|
||||
const remount = React.useCallback(() => {
|
||||
// 現在のURLは復元用に使うが、通常のナビゲーションではsourceへ戻さない。
|
||||
// sourceの更新はWebViewの再読込になるため、フィルター操作中に行うと状態が消える。
|
||||
if (resolvedUriRef.current) {
|
||||
setWebViewSourceUri(resolvedUriRef.current);
|
||||
}
|
||||
lastPongAt.current = Date.now(); // remount直後に誤検知しないようリセット
|
||||
maxBodyLenRef.current = 0;
|
||||
blankCountRef.current = 0;
|
||||
@@ -180,12 +172,11 @@ export default ({ route }) => {
|
||||
const nextUri = importRecordingDownloads
|
||||
? rawUri
|
||||
: rewriteJrDataSystemUrl(rawUri, nextEnvironment);
|
||||
const initialUri = importRecordingDownloads
|
||||
? nextUri
|
||||
: withJrDataSystemIconQuery(nextUri, iconSetting);
|
||||
resolvedUriRef.current = initialUri;
|
||||
setResolvedUri(initialUri);
|
||||
setWebViewSourceUri(initialUri);
|
||||
setResolvedUri(
|
||||
importRecordingDownloads
|
||||
? nextUri
|
||||
: withJrDataSystemIconQuery(nextUri, iconSetting),
|
||||
);
|
||||
setIsEnvironmentReady(true);
|
||||
};
|
||||
|
||||
@@ -199,9 +190,6 @@ export default ({ route }) => {
|
||||
}, [iconSetting, uri, importRecordingDownloads]);
|
||||
|
||||
const handleReload = () => {
|
||||
if (resolvedUriRef.current) {
|
||||
setWebViewSourceUri(resolvedUriRef.current);
|
||||
}
|
||||
lastPongAt.current = Date.now();
|
||||
setHasError(false);
|
||||
hasErrorRef.current = false;
|
||||
@@ -231,9 +219,7 @@ export default ({ route }) => {
|
||||
stack.pop();
|
||||
const previousUrl = stack[stack.length - 1];
|
||||
if (!previousUrl) return false;
|
||||
resolvedUriRef.current = previousUrl;
|
||||
setResolvedUri(previousUrl);
|
||||
setWebViewSourceUri(previousUrl);
|
||||
syncCanGoBack(false);
|
||||
remount();
|
||||
return true;
|
||||
@@ -247,14 +233,6 @@ export default ({ route }) => {
|
||||
return handlePseudoBack();
|
||||
}, [handlePseudoBack]);
|
||||
|
||||
const handleExit = React.useCallback(() => {
|
||||
try {
|
||||
void onExit?.();
|
||||
} finally {
|
||||
goBack();
|
||||
}
|
||||
}, [goBack, onExit]);
|
||||
|
||||
const handleImportRecordingText = React.useCallback(
|
||||
async (content: string) => {
|
||||
try {
|
||||
@@ -299,20 +277,20 @@ export default ({ route }) => {
|
||||
if (canGoBack) {
|
||||
if (handleWebViewBack()) return true;
|
||||
}
|
||||
handleExit();
|
||||
goBack();
|
||||
return true;
|
||||
};
|
||||
|
||||
const subscription = BackHandler.addEventListener("hardwareBackPress", onHardwareBack);
|
||||
return () => subscription.remove();
|
||||
}, [canGoBack, handleExit, handleWebViewBack])
|
||||
}, [canGoBack, goBack, handleWebViewBack])
|
||||
);
|
||||
return (
|
||||
<View style={{ height: "100%", backgroundColor: fixed.primary }}>
|
||||
{isEnvironmentReady && (
|
||||
<WebView
|
||||
key={webViewKey}
|
||||
source={{ uri: webViewSourceUri }}
|
||||
source={{ uri: resolvedUri }}
|
||||
contentMode="mobile"
|
||||
allowsBackForwardNavigationGestures
|
||||
setSupportMultipleWindows={false}
|
||||
@@ -376,9 +354,7 @@ export default ({ route }) => {
|
||||
iconSetting,
|
||||
);
|
||||
if (rewrittenUrl !== request.url) {
|
||||
resolvedUriRef.current = rewrittenUrl;
|
||||
setResolvedUri(rewrittenUrl);
|
||||
setWebViewSourceUri(rewrittenUrl);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -399,7 +375,6 @@ export default ({ route }) => {
|
||||
rewriteJrDataSystemUrl(navState.url, selectedEnvironment),
|
||||
iconSetting,
|
||||
);
|
||||
resolvedUriRef.current = nextUrl;
|
||||
setResolvedUri((current) => (current === nextUrl ? current : nextUrl));
|
||||
if (nextUrl !== "https://unyohub.2pd.jp/integration/succeeded.php") {
|
||||
pushHistoryEntry(nextUrl);
|
||||
@@ -473,7 +448,7 @@ export default ({ route }) => {
|
||||
return;
|
||||
}
|
||||
if (type === "back") return handleWebViewBack();
|
||||
if (type === "windowClose") return handleExit();
|
||||
if (type === "windowClose") return goBack();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
@@ -491,13 +466,13 @@ export default ({ route }) => {
|
||||
<Text style={wvStyles.reloadButtonText}>再読み込み</Text>
|
||||
</TouchableOpacity>
|
||||
{useExitButton && (
|
||||
<TouchableOpacity style={wvStyles.backButton} onPress={handleExit}>
|
||||
<TouchableOpacity style={wvStyles.backButton} onPress={goBack}>
|
||||
<Text style={wvStyles.backButtonText}>閉じる</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
{useExitButton && !hasError && <BigButton onPress={handleExit} string="閉じる" />}
|
||||
{useExitButton && !hasError && <BigButton onPress={goBack} string="閉じる" />}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
+6
-38
@@ -8,6 +8,7 @@ import { AS } from "@/storageControl";
|
||||
import { STORAGE_KEYS } from "@/constants";
|
||||
import { logger } from "@/utils/logger";
|
||||
import TrainBase from "@/components/trainbaseview";
|
||||
import HowTo from "@/howto";
|
||||
import { Menu } from "@/menu";
|
||||
import News from "@/components/news";
|
||||
import Setting from "@/components/Settings/settings";
|
||||
@@ -21,11 +22,6 @@ import { useBottomTabBarHeight } from "@react-navigation/bottom-tabs";
|
||||
import GeneralWebView from "@/GeneralWebView";
|
||||
import { StationDiagramView } from "@/components/StationDiagram/StationDiagramView";
|
||||
import { telemetry } from "@/lib/observability/telemetry";
|
||||
import {
|
||||
markStartup,
|
||||
markStartupOnce,
|
||||
markStartupState,
|
||||
} from "@/lib/devStartupPerformance";
|
||||
import type { StackNavigationProp } from "@react-navigation/stack";
|
||||
import type {
|
||||
RootTabParamList,
|
||||
@@ -35,7 +31,6 @@ const Stack = createStackNavigator<TopMenuStackParamList>();
|
||||
const topMenuScreenTelemetry = telemetry.scope("topMenu.screen");
|
||||
|
||||
export function MenuPage() {
|
||||
markStartupOnce("top-menu-render", "top menu render");
|
||||
const { height, width } = useWindowDimensions();
|
||||
const { verticalScale } = useResponsive();
|
||||
const tabBarHeight = useBottomTabBarHeight();
|
||||
@@ -47,7 +42,6 @@ export function MenuPage() {
|
||||
const bgColor = isDark ? "#1c1c1e" : "#ffffff";
|
||||
|
||||
useEffect(() => {
|
||||
markStartupOnce("top-menu-mounted", "top menu mounted");
|
||||
topMenuScreenTelemetry.info("top menu mounted");
|
||||
return () => {
|
||||
topMenuScreenTelemetry.info("top menu unmounted");
|
||||
@@ -55,9 +49,6 @@ export function MenuPage() {
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
markStartupState("top-menu-focus", "top menu focus state", {
|
||||
focused: isFocused,
|
||||
});
|
||||
topMenuScreenTelemetry.breadcrumb({
|
||||
message: isFocused ? "top menu focused" : "top menu blurred",
|
||||
data: {
|
||||
@@ -69,40 +60,16 @@ export function MenuPage() {
|
||||
|
||||
useEffect(() => {
|
||||
//ニュース表示
|
||||
markStartup("top menu startup storage checks start");
|
||||
AS.getItem(STORAGE_KEYS.NEWS_STATUS)
|
||||
.then((d) => {
|
||||
markStartup("top menu news status read end", {
|
||||
hasValue: d !== null && d !== undefined,
|
||||
isCurrent: d === news,
|
||||
});
|
||||
if (d !== news) {
|
||||
markStartup("top menu startup navigation attempt", {
|
||||
target: "news",
|
||||
reason: "news_status",
|
||||
});
|
||||
navigation.navigate("topMenu", { screen: "news" });
|
||||
}
|
||||
if (d !== news) navigation.navigate("topMenu", { screen: "news" });
|
||||
})
|
||||
.catch(() => {
|
||||
markStartup("top menu news status read error");
|
||||
markStartup("top menu startup navigation attempt", {
|
||||
target: "news",
|
||||
reason: "news_status_error",
|
||||
});
|
||||
navigation.navigate("topMenu", { screen: "news" });
|
||||
});
|
||||
.catch(() => navigation.navigate("topMenu", { screen: "news" }));
|
||||
AS.getItem(STORAGE_KEYS.ICON_SETTING)
|
||||
.then((isSetIcon) => {
|
||||
markStartup("top menu icon setting read end", {
|
||||
hasValue: isSetIcon !== null && isSetIcon !== undefined,
|
||||
});
|
||||
if (isSetIcon === "true") SheetManager.show("TrainIconUpdate");
|
||||
})
|
||||
.catch((error) => {
|
||||
markStartup("top menu icon setting read error");
|
||||
logger.error("Error fetching icon setting:", error);
|
||||
});
|
||||
.catch((error) => logger.error("Error fetching icon setting:", error));
|
||||
|
||||
return undefined;
|
||||
}, []);
|
||||
@@ -155,7 +122,7 @@ export function MenuPage() {
|
||||
}
|
||||
}
|
||||
scrollRef.current?.scrollTo({
|
||||
y: Math.max(0, mapHeightRef.current - verticalScale(80)),
|
||||
y: mapHeightRef.current - verticalScale(80),
|
||||
animated: true,
|
||||
});
|
||||
setMapMode(false);
|
||||
@@ -242,6 +209,7 @@ export function MenuPage() {
|
||||
options={{ ...optionData, gestureEnabled: false }}
|
||||
component={AllTrainDiagramView}
|
||||
/>
|
||||
<Stack.Screen name="howto" options={optionData} component={HowTo} />
|
||||
<Stack.Screen
|
||||
name="generalWebView"
|
||||
options={optionData}
|
||||
|
||||
@@ -1,109 +1,73 @@
|
||||
# JR四国 列車位置情報アプリ
|
||||
|
||||
JR四国の列車リアルタイム位置情報を表示するReact Nativeアプリケーション(非公式)。
|
||||
JR四国の列車リアルタイム位置情報を表示するReact Nativeアプリケーション。
|
||||
|
||||
## 🚀 技術スタック
|
||||
|
||||
> 正確なバージョンは宣言値を `package.json`、解決値を `yarn.lock` で確認してください。
|
||||
|
||||
- **フレームワーク**: Expo SDK 57 / React Native 0.86系 / React 19系(New Architecture)
|
||||
- **フレームワーク**: React Native (Expo SDK 52)
|
||||
- **言語**: TypeScript / JavaScript
|
||||
- **ナビゲーション**: React Navigation 7(bottom tabs + stack。Expo Routerは未使用)
|
||||
- **状態管理**: React Context API + カスタムフック(`stateBox/`)
|
||||
- **ストレージ**: AsyncStorage、expo-sqlite
|
||||
- **地図表示**: WebView + JavaScript Injection、react-native-maps
|
||||
- **シート**: react-native-actions-sheet、@gorhom/bottom-sheet
|
||||
- **状態管理**: React Context API + カスタムフック
|
||||
- **ストレージ**: AsyncStorage
|
||||
- **地図表示**: WebView + JavaScript Injection
|
||||
- **日付処理**: dayjs
|
||||
- **アニメーション / ジェスチャー**: react-native-reanimated、react-native-gesture-handler(v2系)
|
||||
- **監視**: Sentry
|
||||
- **ウィジェット**: react-native-android-widget(Android)、`@bacons/apple-targets` によるWidgetKit拡張(iOS)
|
||||
- **アニメーション**: react-native-reanimated
|
||||
|
||||
## 📦 プロジェクト構造
|
||||
|
||||
```
|
||||
jrshikoku/
|
||||
├── components/ # Reactコンポーネント
|
||||
│ ├── Apps/ # 走行位置画面
|
||||
│ ├── Apps/ # アプリケーション機能
|
||||
│ │ └── FixedPositionBox/
|
||||
│ │ └── hooks/ # カスタムフック
|
||||
│ ├── AndroidWidget/ # Androidウィジェット
|
||||
│ ├── ActionSheetComponents/
|
||||
│ ├── atom/ # 基本UIコンポーネント
|
||||
│ ├── Menu/ # メニュー関連
|
||||
│ ├── Settings/ # 設定画面
|
||||
│ └── TrainMenu/ # 列車メニュー
|
||||
├── stateBox/ # グローバル状態管理(Context + hooks)
|
||||
├── stateBox/ # グローバル状態管理
|
||||
├── lib/ # ユーティリティライブラリ
|
||||
│ ├── domain/ # 駅・列車identityのdomain型
|
||||
│ ├── eachTrainInfoCoreLib/ # 列車情報処理
|
||||
│ ├── observability/ # Sentry・テレメトリ
|
||||
│ ├── theme/ # カラーテーマ
|
||||
│ └── webview/ # WebView注入用データ
|
||||
│ └── eachTrainInfoCoreLib/ # 列車情報処理
|
||||
├── constants/ # 定数定義
|
||||
│ ├── intervals.ts # 時間間隔定数
|
||||
│ ├── api.ts # APIエンドポイント
|
||||
│ ├── storage.ts # StorageKeys
|
||||
│ └── index.ts
|
||||
├── modules/ # ローカルExpoモジュール
|
||||
│ ├── expo-felica-reader/ # FeliCa読み取り(iOS/Android)
|
||||
│ └── expo-live-activity/ # Live Activities(iOS)
|
||||
├── targets/widget/ # iOS WidgetKit拡張
|
||||
├── types/ # TypeScript型定義
|
||||
├── utils/ # ユーティリティ関数
|
||||
│ ├── logger.ts # ロギング
|
||||
│ └── seUtils.ts # SE判定処理
|
||||
├── tests/ # pure関数のテスト(node:test)
|
||||
├── docs/ # 設計・changelog・検証記録
|
||||
│ └── architecture/ # domain契約・ADR
|
||||
├── assets/ # 静的リソース
|
||||
└── config/ # 設定ファイル
|
||||
```
|
||||
|
||||
`android/` と `ios/` はprebuildで生成されるため、gitでは管理していません。
|
||||
|
||||
## 🛠️ セットアップ
|
||||
|
||||
### 必要環境
|
||||
- Node.js(Expo SDK 57が対応するLTS。`yarn expo:doctor` で確認)
|
||||
- Yarn(lockfileは `yarn.lock` のみ)
|
||||
- EAS CLI(ビルドする場合)
|
||||
- Node.js 18以上
|
||||
- npm または yarn
|
||||
- Expo CLI
|
||||
|
||||
### インストールと起動
|
||||
### インストール
|
||||
|
||||
```bash
|
||||
# 依存関係のインストール(postinstallでpatch-packageが実行されます)
|
||||
yarn install
|
||||
# 依存関係のインストール
|
||||
npm install
|
||||
|
||||
# 開発サーバー起動(dev client)
|
||||
yarn start
|
||||
# 開発サーバー起動
|
||||
npm start
|
||||
|
||||
# ネイティブビルドして実行
|
||||
yarn ios
|
||||
yarn android
|
||||
```
|
||||
# iOS実行
|
||||
npm run ios
|
||||
|
||||
### 検証
|
||||
# Android実行
|
||||
npm run android
|
||||
|
||||
```bash
|
||||
yarn typecheck # tsc --noEmit
|
||||
yarn lint # expo lint
|
||||
yarn test # tests/*.test.ts
|
||||
yarn verify # 上記3つをまとめて実行
|
||||
yarn expo:doctor # Expo依存関係の整合性チェック
|
||||
```
|
||||
|
||||
### 依存ライブラリのライセンス一覧
|
||||
|
||||
`dependencies` を追加・更新したら、アプリ内の「オープンソースライセンス」画面で使う一覧を再生成してください。再生成を忘れると `yarn test` が失敗します。
|
||||
|
||||
```bash
|
||||
yarn licenses:generate # assets/licenses/oss-licenses.json を更新
|
||||
# Web実行
|
||||
npm run web
|
||||
```
|
||||
|
||||
## 📝 開発ガイドライン
|
||||
|
||||
開発エージェント向けの詳細なルールは [AGENTS.md](./AGENTS.md)、domain契約は [docs/architecture/](./docs/architecture/) を参照してください。
|
||||
|
||||
### 定数の使用
|
||||
|
||||
```typescript
|
||||
@@ -156,31 +120,18 @@ AsyncStorage.getItem(STORAGE_KEYS.BUS_AND_TRAIN);
|
||||
|
||||
### 列車位置追跡
|
||||
- リアルタイム列車位置表示
|
||||
- 遅延情報の表示(遅延速報EX)
|
||||
- 遅延情報の表示
|
||||
- 次駅・着駅の予測
|
||||
- 列車追従・駅固定モード(iOSではLive Activitiesに対応)
|
||||
- 走行位置の記録と再生
|
||||
|
||||
### 駅情報
|
||||
- 時刻表表示
|
||||
- 駅詳細情報
|
||||
- お気に入り駅登録・並び替え
|
||||
- お気に入り駅登録
|
||||
|
||||
### 運行情報・通知
|
||||
- JR四国の運行情報表示
|
||||
- プッシュ通知(遅延速報EX、運行情報など)
|
||||
- サウンドによる案内(β)
|
||||
|
||||
### ICカード
|
||||
- FeliCa対応ICカードの残高・履歴読み取り
|
||||
|
||||
### カスタマイズ(設定画面)
|
||||
- 起動時に表示する画面、レイアウト設定
|
||||
- カラーテーマ(ライト / 端末依存 / ダーク)
|
||||
- アイコン設定、ランチャーアイコン変更
|
||||
- サウンド設定(β)、運行情報設定(β)
|
||||
- 通知設定、情報ソース設定、接続ステータス
|
||||
- ウィジェット設定(Android)
|
||||
### カスタマイズ
|
||||
- アイコン設定
|
||||
- レイアウト設定
|
||||
- 通知設定
|
||||
|
||||
## 🔧 カスタムフック
|
||||
|
||||
@@ -193,22 +144,14 @@ AsyncStorage.getItem(STORAGE_KEYS.BUS_AND_TRAIN);
|
||||
- `useTrainDataWithThrough`: 通過駅を含む列車データ生成
|
||||
- `useDestinationStation`: 行先駅データの管理
|
||||
|
||||
### グローバル状態フック(`stateBox/`)
|
||||
### グローバル状態フック
|
||||
|
||||
- `useAllTrainDiagram`: 全列車ダイヤデータ
|
||||
- `useAreaInfo`: 運行情報スナップショット(エリア情報)
|
||||
- `useBusAndTrainData`: バス・列車データ
|
||||
- `useCurrentTrain`: 現在の列車位置データ
|
||||
- `useDeviceOrientationChange`: 画面の向き
|
||||
- `useElesite`: えれサイト運用データ
|
||||
- `useCurrentTrain`: 現在選択中の列車
|
||||
- `useFavoriteStation`: お気に入り駅
|
||||
- `useNotifications`: プッシュ通知と通知タップ時のルーティング
|
||||
- `useStationList`: 全駅リスト
|
||||
- `useTopMenu`: トップメニュー状態
|
||||
- `useTrainDelayData`: 遅延情報
|
||||
- `useTrainMenu`: 列車メニュー状態
|
||||
- `useUnyohub`: 鉄道運用Hub運用データ
|
||||
- `useUserPosition`: ユーザーの現在地
|
||||
|
||||
## 📊 リファクタリング実績
|
||||
|
||||
@@ -222,24 +165,17 @@ AsyncStorage.getItem(STORAGE_KEYS.BUS_AND_TRAIN);
|
||||
|
||||
## 🚦 API エンドポイント
|
||||
|
||||
エンドポイントは `constants/api.ts` の `API_ENDPOINTS` に集約しています。一覧と各URLは同ファイルを参照してください。
|
||||
|
||||
```typescript
|
||||
// constants/api.ts(抜粋)
|
||||
const BASE_URL = 'https://jr-shikoku-api-data-storage.haruk.in';
|
||||
|
||||
// constants/api.ts
|
||||
export const API_ENDPOINTS = {
|
||||
DIAGRAM_TODAY: `${BASE_URL}/tmp/diagram-today.json`,
|
||||
OPERATION_INFO: `${BASE_URL}/operation-info/jr-shikoku/latest.json`,
|
||||
DELAY_INFO: `${BASE_URL}/derived/delays/latest.json`,
|
||||
CURRENT_POSITIONS: `${BASE_URL}/tmp/currentPositions.json`,
|
||||
// ...
|
||||
TRAIN_DATA_API: 'https://api.haruk.in/dev/jrshikoku/trainList',
|
||||
DIAGRAM_TODAY: 'https://api.haruk.in/dev/jrshikoku/diagram/today',
|
||||
STATION_LIST: 'https://storage.haruk.in/s/station.json',
|
||||
// ... 他8個
|
||||
};
|
||||
```
|
||||
|
||||
データ取得の全体像は [docs/data-fetch-map.md](./docs/data-fetch-map.md) を参照してください。
|
||||
|
||||
## 🔗 外部からのアプリ起動(ディープリンク)
|
||||
## � 外部からのアプリ起動(ディープリンク)
|
||||
|
||||
本アプリは複数の方法で外部から起動・画面遷移できます。
|
||||
|
||||
@@ -249,14 +185,14 @@ export const API_ENDPOINTS = {
|
||||
|
||||
| URL | 遷移先 |
|
||||
|---|---|
|
||||
| `jrshikoku://open/felica`(`felicahistorypage` も可) | FeliCa履歴ページ |
|
||||
| `jrshikoku://open/felica` | FeliCa履歴ページ |
|
||||
| `jrshikoku://open/traininfo` | 遅延速報EX |
|
||||
| `jrshikoku://open/operation` | 運行情報 |
|
||||
| `jrshikoku://open/settings` | 設定 |
|
||||
| `jrshikoku://open/topmenu` | トップメニュー |
|
||||
| `jrshikoku://positions/apps` | 走行位置 |
|
||||
|
||||
URLパターンの定義は `lib/startupDeepLink.ts`、遷移処理は `lib/useStartupDeepLinkNavigation.ts` の `routeFromUrl` で実装されています。
|
||||
URLの処理は `App.tsx` の `routeFromUrl` で実装されています。
|
||||
|
||||
### Androidウィジェット
|
||||
|
||||
@@ -268,32 +204,15 @@ URLパターンの定義は `lib/startupDeepLink.ts`、遷移処理は `lib/useS
|
||||
| `JR_shikoku_apps_shortcut` | クイックアクセス(各機能へのショートカットタイル) |
|
||||
| `JR_shikoku_felica_balance` | ICカード残高表示 |
|
||||
| `JR_shikoku_info` | 運行情報 |
|
||||
| `JR_shikoku_train_strange` | 怪レい列車 |
|
||||
|
||||
ウィジェットのタップ時は `jrshikoku://` スキームでアプリ内画面へ遷移します。
|
||||
配置可能なウィジェットは `app.json` の `react-native-android-widget` プラグイン設定で登録し、描画は `components/AndroidWidget/widget-task-handler.tsx` で行います。
|
||||
|
||||
#### 未公開: 怪レい列車ウィジェット(`JR_shikoku_train_strange`)
|
||||
|
||||
公開する予定はありますが、現在は未完成のため `app.json` に登録しておらず、ホーム画面には配置できません。
|
||||
|
||||
- `components/AndroidWidget/StrangeTrainWidget.tsx` と `widget-task-handler.tsx` に描画処理はありますが、表示内容は固定文の仮実装です。
|
||||
- ウィジェットに表示するデータの取得先(エンドポイント)がまだ用意されていません。
|
||||
- タップ先は仮で `jrshikoku://open/traininfo`(遅延速報EX)になっています。
|
||||
- 公開時は、データ取得の実装、プレビュー画像(`assets/widgetResource/`)の追加、`app.json` への登録が必要です。登録はネイティブ設定の変更になるため、OTAではなく新しいビルドで配布する必要があります。
|
||||
|
||||
怪レい列車の通知(通知設定、通知アクション `strange-train`)は、ウィジェットとは別に実装されています。
|
||||
実装: `components/AndroidWidget/widget-task-handler.tsx`
|
||||
|
||||
### iOSウィジェット(WidgetKit)
|
||||
|
||||
WidgetKit拡張として、次のホーム画面ウィジェットを提供しています。
|
||||
|
||||
- ショートカット
|
||||
- ICカード残高
|
||||
- 遅延速報
|
||||
- 運行情報
|
||||
|
||||
Live Activitiesは、駅固定(StationLock)と列車追従(TrainFollow)に対応しています。
|
||||
実装: `targets/widget/`
|
||||
WidgetKit拡張としてホーム画面ウィジェットを提供しています。Live Activities にも対応しています。
|
||||
設定: `targets/widget/Info.plist`
|
||||
|
||||
### プッシュ通知
|
||||
|
||||
@@ -309,40 +228,30 @@ Live Activitiesは、駅固定(StationLock)と列車追従(TrainFollow)
|
||||
|
||||
### NFC(FeliCa)
|
||||
|
||||
NFC-Fを利用した交通系ICカードの読み取りに、iOS・Androidとも対応しています。アプリ内からスキャンを開始する形式です。
|
||||
実装: `modules/expo-felica-reader/`([7nohe/expo-felica-reader](https://github.com/7nohe/expo-felica-reader) をもとに改変、MIT)
|
||||
NFC-Fを利用した交通系ICカードの読み取りに対応しています(Android)。アプリ内からスキャンを開始する形式です。
|
||||
実装: `modules/expo-felica-reader/`
|
||||
|
||||
## 📱 ビルド
|
||||
|
||||
EASのprofileとchannelは `eas.json` で管理しています。主なprofileは次のとおりです。
|
||||
|
||||
| profile | 用途 |
|
||||
|---|---|
|
||||
| `development` | dev client(internal配布) |
|
||||
| `preview` | 内部配布 |
|
||||
| `production<version>` / `beta<version>` | バージョン別の本番・ベータ(例: `production7.2`、`beta7.2`) |
|
||||
## �📱 ビルド
|
||||
|
||||
```bash
|
||||
eas build --platform android --profile <profile>
|
||||
eas build --platform ios --profile <profile>
|
||||
# APKビルド(Android)
|
||||
eas build --platform android
|
||||
|
||||
# IPAビルド(iOS)
|
||||
eas build --platform ios
|
||||
```
|
||||
|
||||
runtimeVersionの方針は [docs/architecture/adr/001-runtime-version-policy.md](./docs/architecture/adr/001-runtime-version-policy.md) を参照してください。
|
||||
ネイティブ設定(`app.json` のプラグイン、permission、entitlement、ローカルモジュールなど)を変更した場合は、OTAではなく新しいビルドが必要です。
|
||||
## 🤝 貢献
|
||||
|
||||
プルリクエストを歓迎します。大きな変更を行う場合は、まずissueを開いて変更内容を議論してください。
|
||||
|
||||
## 📄 ライセンス
|
||||
|
||||
本リポジトリは非公開です。無断での複製・再配布を禁止します(All Rights Reserved)。
|
||||
|
||||
- 同梱しているフォント・音声・ロゴなどの素材は、それぞれの権利者の規約に従います。
|
||||
- 依存ライブラリのライセンスは、各パッケージの記載に従います。一覧はアプリの「設定 > オープンソースライセンス」で確認できます。
|
||||
- `modules/expo-felica-reader/` は、Daiki Urata氏の [expo-felica-reader](https://github.com/7nohe/expo-felica-reader) 0.1.0(MIT License)をもとに改変したものです。ライセンス表記は `modules/expo-felica-reader/LICENSE` を参照してください。
|
||||
|
||||
本アプリはJR四国とは無関係の非公式アプリです。「JR四国」などの名称・商標は各権利者に帰属します。
|
||||
[ライセンス情報をここに記載]
|
||||
|
||||
## 👤 開発者
|
||||
|
||||
harukin/Xprocess
|
||||
[開発者情報をここに記載]
|
||||
|
||||
## 🔗 関連リンク
|
||||
|
||||
|
||||
@@ -5,56 +5,33 @@ import type { BottomTabNavigationProp } from "@react-navigation/bottom-tabs";
|
||||
import { useColorScheme } from "react-native";
|
||||
import Apps from "./components/Apps";
|
||||
import TrainBase from "./components/trainbaseview";
|
||||
import HowTo from "./howto";
|
||||
import News from "./components/news";
|
||||
import TrainMenu from "./components/trainMenu";
|
||||
import { FavoriteList } from "./components/FavoriteList";
|
||||
import { optionData } from "./lib/stackOption";
|
||||
import { useCurrentTrainPolling, useCurrentTrainView } from "./stateBox/useCurrentTrain";
|
||||
import {
|
||||
useCustomTrainData,
|
||||
useOperationLogs,
|
||||
} from "./stateBox/useAllTrainDiagram";
|
||||
import { useTrainMenu } from "./stateBox/useTrainMenu";
|
||||
import { AS } from "./storageControl";
|
||||
import { news } from "./config/newsUpdate";
|
||||
import { InteractionManager, Platform, View } from "react-native";
|
||||
import { InteractionManager, Linking, Platform, View } from "react-native";
|
||||
import GeneralWebView from "./GeneralWebView";
|
||||
import { StationDiagramView } from "@/components/StationDiagram/StationDiagramView";
|
||||
import { positionsLifecycleRef, positionsStackNavRef } from "./lib/rootNavigation";
|
||||
import { telemetry } from "./lib/observability/telemetry";
|
||||
import {
|
||||
markStartup,
|
||||
markStartupOnce,
|
||||
markStartupState,
|
||||
} from "./lib/devStartupPerformance";
|
||||
import { usePositionsStartupReady } from "./lib/positionsStartupContext";
|
||||
import type { PositionsStackParamList, RootTabParamList } from "@/types/navigation";
|
||||
const Stack = createStackNavigator<PositionsStackParamList>();
|
||||
const positionsStackTelemetry = telemetry.scope("positions.stack");
|
||||
export const Top = () => {
|
||||
markStartupOnce("positions-stack-render", "positions Top render");
|
||||
const { webview } = useCurrentTrainView();
|
||||
const navigation = useNavigation<BottomTabNavigationProp<RootTabParamList>>();
|
||||
const { navigate, addListener } = navigation;
|
||||
const isTabFocused = useIsFocused();
|
||||
const { notifyStackReady } = usePositionsStartupReady();
|
||||
useCurrentTrainPolling(isTabFocused);
|
||||
const { activateCustomTrainData } = useCustomTrainData();
|
||||
const { activateOperationLogs } = useOperationLogs();
|
||||
const isDark = useColorScheme() === "dark";
|
||||
const bgColor = isDark ? "#1c1c1e" : "#ffffff";
|
||||
const [hasActivatedStack, setHasActivatedStack] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isTabFocused) return;
|
||||
activateCustomTrainData();
|
||||
activateOperationLogs();
|
||||
}, [
|
||||
activateCustomTrainData,
|
||||
activateOperationLogs,
|
||||
isTabFocused,
|
||||
]);
|
||||
|
||||
//地図用
|
||||
const { mapSwitch } = useTrainMenu();
|
||||
const mapSwitchRef = useRef(mapSwitch);
|
||||
@@ -77,7 +54,6 @@ export const Top = () => {
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
markStartupOnce("positions-stack-mounted", "positions stack mounted");
|
||||
positionsStackTelemetry.info("positions stack mounted");
|
||||
return () => {
|
||||
positionsStackTelemetry.info("positions stack unmounted");
|
||||
@@ -86,9 +62,6 @@ export const Top = () => {
|
||||
|
||||
useEffect(() => {
|
||||
if (hasActivatedStack) return;
|
||||
markStartup("positions stack activation scheduled", {
|
||||
focused: isTabFocused,
|
||||
});
|
||||
positionsStackTelemetry.breadcrumb({
|
||||
message: "positions stack activation scheduled",
|
||||
data: {
|
||||
@@ -98,10 +71,6 @@ export const Top = () => {
|
||||
let cancelled = false;
|
||||
const activate = () => {
|
||||
if (cancelled) return;
|
||||
markStartupOnce(
|
||||
"positions-stack-activated",
|
||||
"positions stack activation committed",
|
||||
);
|
||||
positionsStackTelemetry.breadcrumb({
|
||||
message: "positions stack activation committed",
|
||||
data: {
|
||||
@@ -109,7 +78,6 @@ export const Top = () => {
|
||||
},
|
||||
});
|
||||
setHasActivatedStack(true);
|
||||
notifyStackReady();
|
||||
};
|
||||
|
||||
if (isTabFocused) {
|
||||
@@ -127,13 +95,9 @@ export const Top = () => {
|
||||
cancelled = true;
|
||||
clearTimeout(timer);
|
||||
};
|
||||
}, [hasActivatedStack, isTabFocused, notifyStackReady]);
|
||||
}, [hasActivatedStack, isTabFocused]);
|
||||
|
||||
useEffect(() => {
|
||||
markStartupState("positions-stack-gate", "positions stack gate state", {
|
||||
focused: isTabFocused,
|
||||
activated: hasActivatedStack,
|
||||
});
|
||||
telemetry.context("positions_stack", {
|
||||
focused: isTabFocused,
|
||||
activated: hasActivatedStack,
|
||||
@@ -167,6 +131,11 @@ export const Top = () => {
|
||||
stackIndex: stackNav?.getState()?.index ?? null,
|
||||
},
|
||||
});
|
||||
if (Platform.OS === "web") {
|
||||
Linking.openURL("https://train.jr-shikoku.co.jp/");
|
||||
setTimeout(() => navigate("topMenu", { screen: "menu" }), 100);
|
||||
return;
|
||||
}
|
||||
if (!hasActivatedStack || !stackNav) {
|
||||
e?.preventDefault?.();
|
||||
positionsStackTelemetry.breadcrumb({
|
||||
@@ -270,6 +239,7 @@ export const Top = () => {
|
||||
options={{ ...optionData, gestureEnabled: false }}
|
||||
component={StationDiagramView}
|
||||
/>
|
||||
<Stack.Screen name="howto" options={optionData} component={HowTo} />
|
||||
<Stack.Screen
|
||||
name="generalWebView"
|
||||
options={optionData}
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
"scheme": "jrshikoku",
|
||||
"platforms": [
|
||||
"ios",
|
||||
"android"
|
||||
"android",
|
||||
"web"
|
||||
],
|
||||
"version": "7.2",
|
||||
"userInterfaceStyle": "automatic",
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -9,7 +9,6 @@ export const EachTrainInfo = ({ payload }: SheetProps<"EachTrainInfo">) => {
|
||||
const actionSheetRef = useRef(null);
|
||||
const maxHeight = useSheetMaxHeight();
|
||||
const [sheetOpened, setSheetOpened] = useState(false);
|
||||
|
||||
useCurrentTrainPolling(sheetOpened);
|
||||
|
||||
const handleOpen = () => {
|
||||
|
||||
@@ -32,6 +32,7 @@ export const DataConnectedButton: FC<{
|
||||
|
||||
openTrainInfo(time);
|
||||
}}
|
||||
key={station + time}
|
||||
>
|
||||
<View style={{ flexDirection: "row", backgroundColor: colors.backgroundTertiary }}>
|
||||
<View
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React, { FC } from "react";
|
||||
import { Platform, View, Text, TouchableWithoutFeedback } from "react-native";
|
||||
import type { Dayjs } from "dayjs";
|
||||
import { View, Text, TouchableWithoutFeedback } from "react-native";
|
||||
import dayjs from "dayjs";
|
||||
import { setClockTime } from "@/lib/timeUtils";
|
||||
import lineColorList from "@/assets/originData/lineColorList";
|
||||
import type { trainDataType } from "@/lib/trainPositionTextArray";
|
||||
import lineColorList from "../../../assets/originData/lineColorList";
|
||||
import { trainDataType } from "@/lib/trainPositionTextArray";
|
||||
import { getStopListColors } from "./colorScheme";
|
||||
import {
|
||||
isCanceledSe,
|
||||
@@ -12,89 +12,90 @@ import {
|
||||
parseSeString,
|
||||
} from "@/utils/seUtils";
|
||||
import type { SeTypes } from "@/types";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
import { useResponsive } from "@/lib/responsive";
|
||||
|
||||
type TrainDelay = trainDataType["delay"];
|
||||
type seTypes =
|
||||
| "発編"
|
||||
| "着編"
|
||||
| "通編"
|
||||
| "頃編"
|
||||
| "発"
|
||||
| "着"
|
||||
| "休編"
|
||||
| "休発"
|
||||
| "休着"
|
||||
| "休発編"
|
||||
| "休着編"
|
||||
| "通休編"
|
||||
| "通発休編"
|
||||
| "通着休編"
|
||||
| string;
|
||||
|
||||
export type EachStopData = readonly [
|
||||
station: string,
|
||||
se: SeTypes,
|
||||
time: string,
|
||||
platformNum?: string,
|
||||
];
|
||||
|
||||
type ResponsiveScale = {
|
||||
fontScale: (size: number) => number;
|
||||
moderateScale: (size: number, factor?: number) => number;
|
||||
type currentTrainDataType = {
|
||||
Index: number;
|
||||
num: string;
|
||||
delay: "入線" | number | undefined;
|
||||
Pos: string;
|
||||
PosNum: string | number;
|
||||
Direction: number;
|
||||
Type: string;
|
||||
Line: string;
|
||||
};
|
||||
type StationInfo = { StationName: string; StationNumber: string | null };
|
||||
|
||||
type props = {
|
||||
stationNumberMap: ReadonlyMap<string, readonly string[]>;
|
||||
parsedTrainData: readonly EachStopData[];
|
||||
clockBase: Dayjs;
|
||||
borderColor: string;
|
||||
textOnPrimary: string;
|
||||
isDark: boolean;
|
||||
trainDelay: TrainDelay;
|
||||
fontScale: ResponsiveScale["fontScale"];
|
||||
moderateScale: ResponsiveScale["moderateScale"];
|
||||
i: string;
|
||||
index: number;
|
||||
stationList: StationInfo[][];
|
||||
points: boolean;
|
||||
currentTrainData?: currentTrainDataType;
|
||||
openStationACFromEachTrainInfo?: (station: string) => void;
|
||||
showThrew: boolean;
|
||||
array: string[];
|
||||
isNotService?: boolean;
|
||||
};
|
||||
const EachStopListComponent: FC<props> = ({
|
||||
stationNumberMap,
|
||||
parsedTrainData,
|
||||
clockBase,
|
||||
borderColor,
|
||||
textOnPrimary,
|
||||
isDark,
|
||||
trainDelay,
|
||||
fontScale,
|
||||
moderateScale,
|
||||
export const EachStopList: FC<props> = ({
|
||||
i,
|
||||
index,
|
||||
stationList,
|
||||
points,
|
||||
currentTrainData,
|
||||
openStationACFromEachTrainInfo,
|
||||
showThrew,
|
||||
array,
|
||||
isNotService = false,
|
||||
}) => {
|
||||
// console.log({
|
||||
// trainNum,
|
||||
// index,
|
||||
// points,
|
||||
// //openStationACFromEachTrainInfo,
|
||||
// showThrew,
|
||||
// //array,
|
||||
// isNotService,
|
||||
// });
|
||||
|
||||
const currentData = parsedTrainData[index];
|
||||
if (!currentData) return null;
|
||||
|
||||
const [station, se, time, platformNum] = currentData; // 阿波池田,発,6:21,1
|
||||
let beforeSameStationData: EachStopData | null = null;
|
||||
const { colors: themeColors, isDark } = useThemeColors();
|
||||
const { fontScale, moderateScale } = useResponsive();
|
||||
const [station, se, time, platformNum] = i.split(",") as [
|
||||
string,
|
||||
seTypes,
|
||||
string,
|
||||
string
|
||||
]; // 阿波池田,発,6:21,1
|
||||
let beforeSameStationData = null;
|
||||
// 発(通常発・休発・休発編)の場合、前の着(通常着・休着・休着編)と統合する
|
||||
if (se.includes("発")) {
|
||||
if (index > 0) {
|
||||
const beforeData = parsedTrainData[index - 1];
|
||||
const beforeData = array[index - 1].split(",") as [string, seTypes, string];
|
||||
// 前が着(通常着でも休着でも)の場合は統合
|
||||
if (beforeData && beforeData[0] == station && beforeData[1].includes("着")) {
|
||||
if (beforeData[0] == station && beforeData[1].includes("着")) {
|
||||
beforeSameStationData = beforeData;
|
||||
}
|
||||
}
|
||||
}
|
||||
let afterSameStationData: EachStopData | null = null;
|
||||
let afterSameStationData = null;
|
||||
// 着(通常着・休着・休着編)の場合、次の発(通常発・休発・休発編)と統合される(非表示)
|
||||
if (se.includes("着")) {
|
||||
const afterData = parsedTrainData[index + 1];
|
||||
const afterData = array[index + 1]?.split(",") as [string, seTypes, string];
|
||||
// 次が発(通常発でも休発でも)なら、この着を非表示にして次の発で両方表示
|
||||
if (afterData && afterData[0] == station && afterData[1].includes("発")) {
|
||||
afterSameStationData = afterData;
|
||||
return <></>;
|
||||
}
|
||||
}
|
||||
if (station.includes(".")) {
|
||||
if(station.includes(".")){
|
||||
return <></>;
|
||||
}
|
||||
if (!showThrew) {
|
||||
@@ -106,11 +107,25 @@ const EachStopListComponent: FC<props> = ({
|
||||
if (se == "通発休編") return null;
|
||||
if (se == "通着休編") return null;
|
||||
}
|
||||
const StationNumbers = stationNumberMap.get(station) ?? [];
|
||||
const Stations = stationList
|
||||
.map((a) => a.filter((d) => d.StationName == station))
|
||||
.reduce((newArray, e) => newArray.concat(e), []);
|
||||
/*Array [
|
||||
Object {
|
||||
"Station_JP": "佐古",
|
||||
"StationNumber": "T01",
|
||||
},
|
||||
Object {
|
||||
"Station_JP": "佐古",
|
||||
"StationNumber": "B01",
|
||||
},
|
||||
] */
|
||||
const StationNumbers = Stations.filter((d) => d.StationNumber != null).map(
|
||||
(d) => d.StationNumber as string
|
||||
);
|
||||
const trimmedPlatformNum = platformNum?.trim();
|
||||
const hasPlatformNum = !!trimmedPlatformNum;
|
||||
const isPlatformNumNumeric =
|
||||
hasPlatformNum && /^\d+$/.test(trimmedPlatformNum);
|
||||
const isPlatformNumNumeric = hasPlatformNum && /^\d+$/.test(trimmedPlatformNum);
|
||||
const platformDisplay = hasPlatformNum
|
||||
? isPlatformNumNumeric
|
||||
? parseInt(trimmedPlatformNum, 10) === 0
|
||||
@@ -126,7 +141,9 @@ const EachStopListComponent: FC<props> = ({
|
||||
const isCanceled = isCanceledSe(se);
|
||||
const isCommunity = isCommunitySe(se);
|
||||
const isDelayed =
|
||||
trainDelay !== undefined && trainDelay !== "入線" && trainDelay > 0;
|
||||
currentTrainData?.delay !== undefined &&
|
||||
currentTrainData?.delay !== "入線" &&
|
||||
currentTrainData?.delay > 0;
|
||||
|
||||
const colors = getStopListColors(
|
||||
isThrough,
|
||||
@@ -134,7 +151,7 @@ const EachStopListComponent: FC<props> = ({
|
||||
isCanceled,
|
||||
isDelayed,
|
||||
isNotService,
|
||||
isDark,
|
||||
isDark
|
||||
);
|
||||
// 打ち消し線用の通常色(遅延していない時の色)
|
||||
const normalColors = getStopListColors(
|
||||
@@ -143,7 +160,7 @@ const EachStopListComponent: FC<props> = ({
|
||||
isCanceled,
|
||||
false,
|
||||
isNotService,
|
||||
isDark,
|
||||
isDark
|
||||
);
|
||||
|
||||
// beforeSameStationData用の色設定
|
||||
@@ -162,7 +179,7 @@ const EachStopListComponent: FC<props> = ({
|
||||
isCanceled,
|
||||
isDelayed,
|
||||
isNotService,
|
||||
isDark,
|
||||
isDark
|
||||
);
|
||||
const beforeNormalColors = getStopListColors(
|
||||
beforeIsThrough,
|
||||
@@ -170,47 +187,19 @@ const EachStopListComponent: FC<props> = ({
|
||||
isCanceled,
|
||||
false,
|
||||
isNotService,
|
||||
isDark,
|
||||
isDark
|
||||
);
|
||||
beforeTimeTextColor = beforeColors.timeText;
|
||||
beforeNormalTimeTextColor = beforeNormalColors.timeText;
|
||||
}
|
||||
|
||||
const hasDoubleTime = !!beforeSameStationData || !!afterSameStationData;
|
||||
const beforeTimeText = beforeSameStationData ? (
|
||||
<TimeText
|
||||
isDouble={hasDoubleTime}
|
||||
isBefore={true}
|
||||
delay={trainDelay}
|
||||
se={beforeSameStationData[1]}
|
||||
time={beforeSameStationData[2]}
|
||||
textColor={beforeTimeTextColor}
|
||||
normalTextColor={beforeNormalTimeTextColor}
|
||||
clockBase={clockBase}
|
||||
fontScale={fontScale}
|
||||
moderateScale={moderateScale}
|
||||
/>
|
||||
) : null;
|
||||
const mainTimeText = (
|
||||
<TimeText
|
||||
isDouble={hasDoubleTime}
|
||||
delay={trainDelay}
|
||||
se={se}
|
||||
time={time}
|
||||
textColor={colors.timeText}
|
||||
normalTextColor={normalColors.timeText}
|
||||
clockBase={clockBase}
|
||||
fontScale={fontScale}
|
||||
moderateScale={moderateScale}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<TouchableWithoutFeedback
|
||||
onPress={() =>
|
||||
openStationACFromEachTrainInfo &&
|
||||
openStationACFromEachTrainInfo(station)
|
||||
}
|
||||
key={station + se + time}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
@@ -225,56 +214,50 @@ const EachStopListComponent: FC<props> = ({
|
||||
position: "relative",
|
||||
marginHorizontal: 15,
|
||||
flexDirection: "row",
|
||||
alignSelf: "stretch",
|
||||
height: "101%",
|
||||
}}
|
||||
>
|
||||
{StationNumbers.map((stn) => (
|
||||
<StationNumbersBox
|
||||
key={stn}
|
||||
stn={stn}
|
||||
se={se}
|
||||
fontScale={fontScale}
|
||||
textColor={textOnPrimary}
|
||||
/>
|
||||
))}
|
||||
>
|
||||
{StationNumbers.map((stn, index) => (
|
||||
<StationNumbersBox stn={stn} se={se} key={index} />
|
||||
))}
|
||||
</View>
|
||||
|
||||
{points && (
|
||||
<View
|
||||
style={{
|
||||
position: "relative",
|
||||
width: 0,
|
||||
alignSelf: "stretch",
|
||||
alignItems: "flex-end",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<View style={{ position: "relative", height: 25 }}>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: fontScale(20),
|
||||
position: "absolute",
|
||||
textAlignVertical: "center",
|
||||
left: -15,
|
||||
top: "auto",
|
||||
}}
|
||||
>
|
||||
🚊
|
||||
</Text>
|
||||
</View>
|
||||
<View
|
||||
style={{
|
||||
position: "relative",
|
||||
flexDirection: "column",
|
||||
width: 0,
|
||||
height: "100%",
|
||||
alignItems: "flex-end",
|
||||
}}
|
||||
>
|
||||
<View style={{ flex: 1 }} />
|
||||
<View style={{ position: "relative", height: 25 }}>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: fontScale(20),
|
||||
position: "absolute",
|
||||
textAlignVertical: "center",
|
||||
left: -15,
|
||||
top: "auto",
|
||||
}}
|
||||
>
|
||||
{points && "🚊"}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
<View style={{ flex: 1 }} />
|
||||
</View>
|
||||
<View
|
||||
style={{
|
||||
padding: 8,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: borderColor,
|
||||
borderBottomColor: themeColors.borderLight,
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
<View style={{ flexDirection: "column" }}>
|
||||
<View style={{ flex: 1 }} />
|
||||
<Text
|
||||
style={{
|
||||
fontSize: fontScale(20),
|
||||
@@ -295,55 +278,64 @@ const EachStopListComponent: FC<props> = ({
|
||||
{platformDisplay}
|
||||
</Text>
|
||||
)}
|
||||
</Text>
|
||||
{hasDoubleTime ? (
|
||||
<View style={{ flexDirection: "column", alignItems: "flex-end" }}>
|
||||
{beforeTimeText}
|
||||
{mainTimeText}
|
||||
</View>
|
||||
) : (
|
||||
mainTimeText
|
||||
)}
|
||||
</Text>
|
||||
<View style={{ flex: 1 }} />
|
||||
</View>
|
||||
<View style={{ flex: 1 }} />
|
||||
<View style={{ flexDirection: "column", alignItems: "flex-end" }}>
|
||||
{beforeSameStationData && (
|
||||
<TimeText
|
||||
isDouble={!!beforeSameStationData || !!afterSameStationData}
|
||||
isBefore={true}
|
||||
currentTrainData={currentTrainData}
|
||||
se={beforeSameStationData[1]}
|
||||
time={beforeSameStationData[2]}
|
||||
key={"before" + beforeSameStationData[2]}
|
||||
textColor={beforeTimeTextColor}
|
||||
normalTextColor={beforeNormalTimeTextColor}
|
||||
/>
|
||||
)}
|
||||
<TimeText
|
||||
isDouble={!!beforeSameStationData || !!afterSameStationData}
|
||||
currentTrainData={currentTrainData}
|
||||
se={se}
|
||||
time={time}
|
||||
key={se + time}
|
||||
textColor={colors.timeText}
|
||||
normalTextColor={normalColors.timeText}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
);
|
||||
};
|
||||
|
||||
export const EachStopList = React.memo(EachStopListComponent);
|
||||
|
||||
const TimeTextComponent: FC<{
|
||||
const TimeText: FC<{
|
||||
isBefore?: boolean;
|
||||
isDouble: boolean;
|
||||
delay: TrainDelay;
|
||||
currentTrainData: trainDataType;
|
||||
se: string;
|
||||
time: string;
|
||||
textColor: string;
|
||||
normalTextColor: string;
|
||||
clockBase: Dayjs;
|
||||
} & ResponsiveScale> = ({
|
||||
}> = ({
|
||||
isDouble,
|
||||
delay,
|
||||
currentTrainData,
|
||||
se,
|
||||
time,
|
||||
isBefore = false,
|
||||
textColor,
|
||||
normalTextColor,
|
||||
clockBase,
|
||||
fontScale,
|
||||
moderateScale,
|
||||
}) => {
|
||||
const [seString, seType] = parseSeString(se);
|
||||
const { fontScale, moderateScale } = useResponsive();
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
opacity: isBefore ? 0.5 : 1,
|
||||
}}
|
||||
>
|
||||
{time !== "" && !!delay && delay != "入線" && delay != 0 && (
|
||||
<View style={{ flexDirection: "row", alignItems: "center", opacity: isBefore ? 0.5 : 1 }}>
|
||||
{!!currentTrainData?.delay &&
|
||||
currentTrainData?.delay != "入線" &&
|
||||
currentTrainData?.delay != 0 && (
|
||||
<Text
|
||||
style={{
|
||||
fontSize: fontScale(isBefore ? 14 : 15),
|
||||
@@ -362,15 +354,12 @@ const TimeTextComponent: FC<{
|
||||
)}
|
||||
<StationTimeBox
|
||||
isBefore={isBefore}
|
||||
delay={delay}
|
||||
delay={currentTrainData?.delay}
|
||||
textColor={textColor}
|
||||
seType={seType}
|
||||
se={se}
|
||||
time={time}
|
||||
isDouble={isDouble}
|
||||
clockBase={clockBase}
|
||||
fontScale={fontScale}
|
||||
moderateScale={moderateScale}
|
||||
/>
|
||||
<Text
|
||||
style={{
|
||||
@@ -386,15 +375,10 @@ const TimeTextComponent: FC<{
|
||||
</View>
|
||||
);
|
||||
};
|
||||
const TimeText = React.memo(TimeTextComponent);
|
||||
|
||||
const StationNumbersBoxComponent: FC<{
|
||||
stn: string;
|
||||
se: SeTypes;
|
||||
textColor: string;
|
||||
fontScale: ResponsiveScale["fontScale"];
|
||||
}> = (props) => {
|
||||
const { stn, se, textColor, fontScale } = props;
|
||||
const StationNumbersBox: FC<{ stn: string; se: seTypes }> = (props) => {
|
||||
const { stn, se } = props;
|
||||
const { fixed } = useThemeColors();
|
||||
const { fontScale } = useResponsive();
|
||||
const lineColor = lineColorList[stn.charAt(0)];
|
||||
const hasThrew =
|
||||
se == "通過" ||
|
||||
@@ -407,74 +391,44 @@ const StationNumbersBoxComponent: FC<{
|
||||
? "80"
|
||||
: "";
|
||||
const backgroundColor = `${lineColor}${hasThrew}`;
|
||||
|
||||
const stationNumberLabel = `${stn.charAt(0)}\n${stn.slice(1)}`;
|
||||
const stationNumberTextStyle = {
|
||||
color: textColor,
|
||||
textAlign: "center" as const,
|
||||
fontSize: fontScale(10),
|
||||
fontWeight: "bold" as const,
|
||||
};
|
||||
|
||||
if (Platform.OS === "ios") {
|
||||
return (
|
||||
<View
|
||||
return (
|
||||
<View style={{ backgroundColor, flex: 1 }} key={stn}>
|
||||
<View style={{ flex: 1 }} />
|
||||
<Text
|
||||
style={{
|
||||
backgroundColor,
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
color: fixed.textOnPrimary,
|
||||
textAlign: "center",
|
||||
fontSize: fontScale(10),
|
||||
fontWeight: "bold",
|
||||
}}
|
||||
>
|
||||
<Text style={stationNumberTextStyle}>{stationNumberLabel}</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Text
|
||||
style={{
|
||||
...stationNumberTextStyle,
|
||||
backgroundColor,
|
||||
flex: 1,
|
||||
textAlignVertical: "center",
|
||||
}}
|
||||
>
|
||||
{stationNumberLabel}
|
||||
</Text>
|
||||
{stn.charAt(0)}
|
||||
{"\n"}
|
||||
{stn.slice(1)}
|
||||
</Text>
|
||||
<View style={{ flex: 1 }} />
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
type StationTimeBoxType = {
|
||||
isBefore?: boolean;
|
||||
delay: TrainDelay;
|
||||
delay: "入線" | number | undefined;
|
||||
textColor: string;
|
||||
seType: SeTypes;
|
||||
seType: seTypes;
|
||||
se: string;
|
||||
time: string;
|
||||
isDouble: boolean;
|
||||
clockBase: Dayjs;
|
||||
} & ResponsiveScale;
|
||||
};
|
||||
|
||||
const StationNumbersBox = React.memo(StationNumbersBoxComponent);
|
||||
|
||||
const StationTimeBoxComponent: FC<StationTimeBoxType> = (props) => {
|
||||
const {
|
||||
delay,
|
||||
textColor,
|
||||
seType,
|
||||
se,
|
||||
const StationTimeBox: FC<StationTimeBoxType> = (props) => {
|
||||
const { delay, textColor, seType, se, time, isDouble, isBefore } = props;
|
||||
const { fontScale, moderateScale } = useResponsive();
|
||||
const dates = setClockTime(
|
||||
dayjs(),
|
||||
time,
|
||||
isDouble,
|
||||
isBefore,
|
||||
clockBase,
|
||||
fontScale,
|
||||
moderateScale,
|
||||
} = props;
|
||||
const timeData = setClockTime(
|
||||
clockBase,
|
||||
time,
|
||||
delay == "入線" || delay == undefined ? 0 : delay,
|
||||
)?.format("HH:mm") ?? null;
|
||||
delay == "入線" || delay == undefined ? 0 : delay
|
||||
);
|
||||
|
||||
return (
|
||||
<Text
|
||||
@@ -486,9 +440,7 @@ const StationTimeBoxComponent: FC<StationTimeBoxType> = (props) => {
|
||||
fontStyle: seType == "community" ? "italic" : "normal",
|
||||
}}
|
||||
>
|
||||
{se.includes("通") && time == "" ? "レ" : (timeData ?? "")}
|
||||
{se.includes("通") && time == "" ? "レ" : dates?.format("HH:mm") ?? ""}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
const StationTimeBox = React.memo(StationTimeBoxComponent);
|
||||
|
||||
@@ -1,14 +1,23 @@
|
||||
import React, { FC } from "react";
|
||||
import { View } from "react-native";
|
||||
import { TrainDataView } from "./TrainDataView";
|
||||
import type { TrainDataViewHeaderProps } from "./TrainDataView";
|
||||
type props = TrainDataViewHeaderProps;
|
||||
export const LongHeader: FC<props> = ({
|
||||
import { trainDataType } from "@/lib/trainPositionTextArray";
|
||||
import type { NavigateFunction } from "@/types";
|
||||
type props = {
|
||||
currentTrainData: trainDataType;
|
||||
currentTrainDataList?: trainDataType[];
|
||||
currentPosition: string[] | undefined;
|
||||
currentPositionList?: string[][];
|
||||
nearTrainIDList: string[];
|
||||
openTrainInfo: (f: string) => void;
|
||||
navigate: NavigateFunction;
|
||||
}
|
||||
export const LongHeader:FC<props> = ({
|
||||
currentTrainData,
|
||||
currentTrainDataList,
|
||||
nearTrainIDList,
|
||||
currentPosition,
|
||||
currentPositionList,
|
||||
nearTrainIDList,
|
||||
openTrainInfo,
|
||||
navigate,
|
||||
}) => {
|
||||
@@ -22,13 +31,14 @@ export const LongHeader: FC<props> = ({
|
||||
}}
|
||||
>
|
||||
<TrainDataView
|
||||
nearTrainIDList={nearTrainIDList}
|
||||
currentTrainData={currentTrainData}
|
||||
currentTrainDataList={currentTrainDataList}
|
||||
currentPosition={currentPosition}
|
||||
currentPositionList={currentPositionList}
|
||||
nearTrainIDList={nearTrainIDList}
|
||||
openTrainInfo={openTrainInfo}
|
||||
navigate={navigate}
|
||||
key={"LongHeader"}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -8,194 +8,131 @@ type stateBox = {
|
||||
currentTrainData: trainDataType | undefined;
|
||||
title: string;
|
||||
style?: ViewStyle;
|
||||
mode?: 0 | 1 | 2;
|
||||
mode?: number;
|
||||
};
|
||||
export const PositionBox: FC<stateBox> = (props) => {
|
||||
const { currentTrainData, title, style, mode } = props;
|
||||
const { fontScale } = useResponsive();
|
||||
const mainFontSize = mode == 2 ? fontScale(16) : fontScale(23);
|
||||
const {
|
||||
currentTrainData,
|
||||
title,
|
||||
style,
|
||||
mode,
|
||||
} = props;
|
||||
const { colors } = useThemeColors();
|
||||
const { officialPosition } = currentTrainData || {};
|
||||
const { fontScale } = useResponsive();
|
||||
const {officialPosition} = currentTrainData || {};
|
||||
|
||||
const isStationPosition = officialPosition?.positionType === "station";
|
||||
const platformNumber = isStationPosition
|
||||
? officialPosition?.platform
|
||||
: undefined;
|
||||
const lineNumber = isStationPosition ? officialPosition?.track : undefined;
|
||||
const platformDescription = officialPosition?.note || ``;
|
||||
// Notes describe the current position even when it is a prediction or a
|
||||
// section between stations. Platform/track remain station-only fields, but
|
||||
// the note itself must not be hidden by position_type.
|
||||
const platformDescription = officialPosition?.note;
|
||||
let firstText = "";
|
||||
let secondText = "";
|
||||
let marginText = "";
|
||||
let externalText = "";
|
||||
const { isBetween, Pos: PosData } = trainPosition(currentTrainData);
|
||||
if (isBetween === true) {
|
||||
const { from, to } = PosData;
|
||||
const firstText = from;
|
||||
const secondText = to;
|
||||
const marginText = mode == 2 ? "→" : "↓";
|
||||
|
||||
return (
|
||||
firstText = from;
|
||||
secondText = to;
|
||||
marginText = mode == 2 ? "→" : "↓";
|
||||
} else {
|
||||
const { Pos } = PosData;
|
||||
if (Pos !== "") {
|
||||
firstText = Pos;
|
||||
if (platformNumber) {
|
||||
secondText = `${platformNumber}番乗り場`;
|
||||
if (lineNumber) {
|
||||
externalText = `${lineNumber}番線`;
|
||||
}
|
||||
} else if (lineNumber) {
|
||||
secondText = `${lineNumber}番線`;
|
||||
}
|
||||
}
|
||||
}
|
||||
return (
|
||||
<View style={{ ...(mode == 2 ? boxStyle2 : boxStyle), backgroundColor: colors.surface, ...style }}>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
ellipsizeMode="tail"
|
||||
style={{ flexShrink: 1, fontSize: fontScale(12), color: colors.textAccent }}
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
<View style={{ flex: 1 }} />
|
||||
<View
|
||||
style={{
|
||||
...(mode == 2 ? boxStyle2 : boxStyle),
|
||||
backgroundColor: colors.surface,
|
||||
...style,
|
||||
minWidth: 0,
|
||||
flexShrink: 1,
|
||||
flexDirection: mode == 2 ? "row" : "column",
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
ellipsizeMode="tail"
|
||||
style={{
|
||||
flexShrink: 1,
|
||||
fontSize: fontScale(12),
|
||||
color: colors.textAccent,
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
<View style={{ flex: 1 }} />
|
||||
<View
|
||||
style={{
|
||||
minWidth: 0,
|
||||
flexShrink: 1,
|
||||
flexDirection: mode == 2 ? "row" : "column",
|
||||
}}
|
||||
>
|
||||
{firstText && (
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
ellipsizeMode="tail"
|
||||
style={{
|
||||
flexShrink: 1,
|
||||
minWidth: 0,
|
||||
textAlign: "right",
|
||||
fontSize: mainFontSize,
|
||||
color: colors.textAccent,
|
||||
}}
|
||||
style={[
|
||||
{ flexShrink: 1, minWidth: 0 },
|
||||
mode == 2
|
||||
? { fontSize: fontScale(16), textAlign: "right" as const }
|
||||
: isBetween
|
||||
? { fontSize: fontScale(23), textAlign: "right" as const }
|
||||
: { fontSize: fontScale(23), textAlign: "right" as const },
|
||||
{ color: colors.textAccent },
|
||||
]}
|
||||
>
|
||||
{firstText}
|
||||
</Text>
|
||||
)}
|
||||
{marginText && (
|
||||
<Text style={{ color: colors.textAccent, textAlign: "right" }}>
|
||||
{marginText}
|
||||
</Text>
|
||||
)}
|
||||
{secondText && (
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
ellipsizeMode="tail"
|
||||
style={{
|
||||
flexShrink: 1,
|
||||
minWidth: 0,
|
||||
textAlign: "right",
|
||||
fontSize: mainFontSize,
|
||||
color: colors.textAccent,
|
||||
}}
|
||||
style={[
|
||||
{ flexShrink: 1, minWidth: 0 },
|
||||
mode == 2
|
||||
? { fontSize: fontScale(16), textAlign: "right" as const }
|
||||
: isBetween
|
||||
? { fontSize: fontScale(23), textAlign: "right" as const }
|
||||
: { fontSize: fontScale(14), textAlign: "right" as const },
|
||||
{ color: colors.textAccent },
|
||||
]}
|
||||
>
|
||||
{secondText}
|
||||
</Text>
|
||||
</View>
|
||||
{platformDescription && (
|
||||
<View style={{ flexDirection: mode == 2 ? "row" : "column" }}>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
ellipsizeMode="tail"
|
||||
style={{
|
||||
flexShrink: 1,
|
||||
minWidth: 0,
|
||||
textAlign: "right",
|
||||
fontSize: fontScale(10),
|
||||
color: colors.textAccent,
|
||||
}}
|
||||
>
|
||||
{" " + platformDescription}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
} else {
|
||||
const { Pos } = PosData;
|
||||
const subFontSize = mode == 2 ? fontScale(16) : fontScale(14);
|
||||
const firstText = Pos !== "" ? Pos : "";
|
||||
let secondText = "";
|
||||
let externalText = "";
|
||||
if (platformNumber) {
|
||||
secondText = `${platformNumber}番乗り場`;
|
||||
externalText = lineNumber ? `${lineNumber}番線` : "";
|
||||
} else if (lineNumber) {
|
||||
secondText = `${lineNumber}番線`;
|
||||
}
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
...(mode == 2 ? boxStyle2 : boxStyle),
|
||||
backgroundColor: colors.surface,
|
||||
...style,
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
ellipsizeMode="tail"
|
||||
style={{
|
||||
flexShrink: 1,
|
||||
fontSize: fontScale(12),
|
||||
color: colors.textAccent,
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
<View style={{ flex: 1 }} />
|
||||
<View
|
||||
style={{
|
||||
minWidth: 0,
|
||||
flexShrink: 1,
|
||||
flexDirection: mode == 2 ? "row" : "column",
|
||||
}}
|
||||
>
|
||||
{(platformDescription || externalText) && (
|
||||
<View style={{ flexDirection: mode == 2 ? "row" : "column" }}>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
ellipsizeMode="tail"
|
||||
style={{
|
||||
flexShrink: 1,
|
||||
minWidth: 0,
|
||||
fontSize: mainFontSize,
|
||||
textAlign: "right",
|
||||
...(mode == 2
|
||||
? { fontSize: fontScale(16), textAlign: "right" as const }
|
||||
: { fontSize: fontScale(23), textAlign: "right" as const }),
|
||||
fontSize: fontScale(10),
|
||||
color: colors.textAccent,
|
||||
}}
|
||||
>
|
||||
{firstText}
|
||||
</Text>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
ellipsizeMode="tail"
|
||||
style={{
|
||||
flexShrink: 1,
|
||||
minWidth: 0,
|
||||
fontSize: subFontSize,
|
||||
textAlign: "right",
|
||||
color: colors.textAccent,
|
||||
}}
|
||||
>
|
||||
{secondText}
|
||||
{" " + externalText}
|
||||
{" " + platformDescription}
|
||||
</Text>
|
||||
</View>
|
||||
{(platformDescription || externalText) && (
|
||||
<View style={{ flexDirection: mode == 2 ? "row" : "column" }}>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
ellipsizeMode="tail"
|
||||
style={{
|
||||
flexShrink: 1,
|
||||
minWidth: 0,
|
||||
textAlign: "right",
|
||||
fontSize: fontScale(10),
|
||||
color: colors.textAccent,
|
||||
}}
|
||||
>
|
||||
{" " + externalText}
|
||||
{" " + platformDescription}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
const boxStyle: ViewStyle = {
|
||||
flex: 1,
|
||||
|
||||
@@ -1,27 +1,12 @@
|
||||
import React from "react";
|
||||
import { View, Text, TouchableOpacity } from "react-native";
|
||||
import type { trainDataType } from "@/lib/trainPositionTextArray";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
import { useResponsive } from "@/lib/responsive";
|
||||
|
||||
type Props = {
|
||||
trainDelay: trainDataType["delay"];
|
||||
showThrew: boolean;
|
||||
setShowThrew: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
haveThrough: boolean;
|
||||
};
|
||||
|
||||
export const ScrollStickyContent = ({
|
||||
trainDelay,
|
||||
showThrew,
|
||||
setShowThrew,
|
||||
haveThrough,
|
||||
}: Props) => {
|
||||
export const ScrollStickyContent = (props) => {
|
||||
const { currentTrainData, showThrew, setShowThrew, haveThrough } = props;
|
||||
const { colors } = useThemeColors();
|
||||
const { fontScale } = useResponsive();
|
||||
const hasNumericDelay =
|
||||
typeof trainDelay === "number" && !Number.isNaN(trainDelay);
|
||||
const hasDelay = hasNumericDelay && trainDelay !== 0;
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
@@ -42,7 +27,7 @@ export const ScrollStickyContent = ({
|
||||
<Text style={{ fontSize: fontScale(20), color: colors.text }}>停車駅</Text>
|
||||
<View style={{ flex: 1 }} />
|
||||
<View style={{ flexDirection: "row" }}>
|
||||
{hasDelay && (
|
||||
{!isNaN(currentTrainData?.delay) && currentTrainData?.delay != 0 && (
|
||||
<Text
|
||||
style={{
|
||||
fontSize: fontScale(15),
|
||||
@@ -59,7 +44,11 @@ export const ScrollStickyContent = ({
|
||||
<Text
|
||||
style={{
|
||||
fontSize: fontScale(20),
|
||||
color: hasDelay ? colors.textError : colors.text,
|
||||
color: isNaN(currentTrainData?.delay)
|
||||
? colors.text
|
||||
: currentTrainData?.delay == 0
|
||||
? colors.text
|
||||
: colors.textError,
|
||||
width: 60,
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -1,14 +1,23 @@
|
||||
import React, { FC } from "react";
|
||||
import { View } from "react-native";
|
||||
import { TrainDataView } from "./TrainDataView";
|
||||
import type { TrainDataViewHeaderProps } from "./TrainDataView";
|
||||
type props = TrainDataViewHeaderProps;
|
||||
export const ShortHeader: FC<props> = ({
|
||||
import { trainDataType } from "@/lib/trainPositionTextArray";
|
||||
import type { NavigateFunction } from "@/types";
|
||||
type props = {
|
||||
currentTrainData: trainDataType;
|
||||
currentTrainDataList?: trainDataType[];
|
||||
currentPosition: string[] | undefined;
|
||||
currentPositionList?: string[][];
|
||||
nearTrainIDList: string[];
|
||||
openTrainInfo: (f: string) => void;
|
||||
navigate: NavigateFunction;
|
||||
}
|
||||
export const ShortHeader:FC<props> = ({
|
||||
currentTrainData,
|
||||
currentTrainDataList,
|
||||
nearTrainIDList,
|
||||
currentPosition,
|
||||
currentPositionList,
|
||||
nearTrainIDList,
|
||||
openTrainInfo,
|
||||
navigate,
|
||||
}) => {
|
||||
@@ -22,14 +31,15 @@ export const ShortHeader: FC<props> = ({
|
||||
}}
|
||||
>
|
||||
<TrainDataView
|
||||
nearTrainIDList={nearTrainIDList}
|
||||
mode={2}
|
||||
currentTrainData={currentTrainData}
|
||||
currentTrainDataList={currentTrainDataList}
|
||||
currentPosition={currentPosition}
|
||||
currentPositionList={currentPositionList}
|
||||
mode={2}
|
||||
nearTrainIDList={nearTrainIDList}
|
||||
openTrainInfo={openTrainInfo}
|
||||
navigate={navigate}
|
||||
key={"ShortHeader"}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { FC } from "react";
|
||||
import { View, Text, ViewStyle } from "react-native";
|
||||
import React, { CSSProperties, FC } from "react";
|
||||
import { View, Text, StyleProp, ViewStyle } from "react-native";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
import { useResponsive } from "@/lib/responsive";
|
||||
|
||||
@@ -7,7 +7,7 @@ type stateBox = {
|
||||
text: string;
|
||||
title: string;
|
||||
style?: ViewStyle;
|
||||
mode?: 0 | 1 | 2;
|
||||
mode?: number;
|
||||
endText?: string;
|
||||
};
|
||||
export const StateBox: FC<stateBox> = (props) => {
|
||||
|
||||
@@ -1,166 +1,63 @@
|
||||
import React, { useState, FC } from "react";
|
||||
import React, { useState, useEffect, FC } from "react";
|
||||
import {
|
||||
ScrollView,
|
||||
Text,
|
||||
View,
|
||||
TouchableOpacity,
|
||||
ViewStyle,
|
||||
useWindowDimensions,
|
||||
} from "react-native";
|
||||
import { ScrollView } from "react-native-actions-sheet";
|
||||
import { StateBox } from "./StateBox";
|
||||
import { PositionBox } from "./PositionBox";
|
||||
import { useCurrentTrainView } from "@/stateBox/useCurrentTrain";
|
||||
import { useCurrentTrainView } from "../../../stateBox/useCurrentTrain";
|
||||
import { SheetManager } from "react-native-actions-sheet";
|
||||
import type { trainDataType } from "@/lib/trainPositionTextArray";
|
||||
import { trainDataType } from "@/lib/trainPositionTextArray";
|
||||
import { useCustomTrainData } from "@/stateBox/useAllTrainDiagram";
|
||||
import { customTrainDataDetector } from "@/components/custom-train-data";
|
||||
import type { NavigateFunction } from "@/types";
|
||||
import { stackAwareNavigate } from "@/lib/rootNavigation";
|
||||
import { OfficialPositionEditButton } from "@/components/OfficialPositionEditButton";
|
||||
import { lineDisplayName } from "@/lib/lineDisplayName";
|
||||
|
||||
export type TrainDataViewData = {
|
||||
type props = {
|
||||
currentTrainData: trainDataType | undefined;
|
||||
currentTrainDataList: trainDataType[];
|
||||
currentPosition: string[];
|
||||
currentPositionList: string[][];
|
||||
};
|
||||
|
||||
export type TrainDataViewMode = 0 | 1 | 2;
|
||||
|
||||
export type TrainDataViewControls = {
|
||||
currentTrainDataList?: trainDataType[];
|
||||
currentPosition: string[] | undefined;
|
||||
currentPositionList?: string[][];
|
||||
showLineLabel?: boolean;
|
||||
nearTrainIDList: string[];
|
||||
openTrainInfo: (f: string) => void;
|
||||
mode?: 0 | 1 | 2; //0:通常,1:コンパクト,2:横並び
|
||||
navigate: NavigateFunction;
|
||||
};
|
||||
|
||||
export type TrainDataViewHeaderProps = TrainDataViewData &
|
||||
TrainDataViewControls;
|
||||
|
||||
type TrainDataViewProps = TrainDataViewHeaderProps & {
|
||||
mode?: TrainDataViewMode; // 0:通常, 1:コンパクト, 2:横並び
|
||||
};
|
||||
|
||||
export const TrainDataView: FC<TrainDataViewProps> = ({
|
||||
const TrainDataViewRow: FC<props> = ({
|
||||
currentTrainData,
|
||||
currentTrainDataList,
|
||||
currentPosition,
|
||||
currentPositionList,
|
||||
...controls
|
||||
}) => {
|
||||
const { width: windowWidth } = useWindowDimensions();
|
||||
const rows = currentTrainDataList?.length
|
||||
? currentTrainDataList
|
||||
: [currentTrainData];
|
||||
const positions = currentPositionList?.length
|
||||
? currentPositionList
|
||||
: [currentPosition ?? []];
|
||||
const [activeIndex, setActiveIndex] = useState(0);
|
||||
const [pageWidth, setPageWidth] = useState(windowWidth);
|
||||
|
||||
const hasMultipleRows = rows.length > 1;
|
||||
const visibleActiveIndex = Math.min(
|
||||
activeIndex,
|
||||
Math.max(rows.length - 1, 0),
|
||||
);
|
||||
|
||||
return (
|
||||
<View
|
||||
onLayout={(event) => {
|
||||
const nextWidth = event.nativeEvent.layout.width;
|
||||
setPageWidth((previous) =>
|
||||
previous === nextWidth ? previous : nextWidth,
|
||||
);
|
||||
}}
|
||||
style={{ flex: 1, width: "100%" }}
|
||||
>
|
||||
<ScrollView
|
||||
horizontal
|
||||
pagingEnabled={hasMultipleRows}
|
||||
scrollEnabled={hasMultipleRows}
|
||||
nestedScrollEnabled
|
||||
directionalLockEnabled
|
||||
showsHorizontalScrollIndicator={false}
|
||||
style={{ flex: 1, width: "100%" }}
|
||||
contentContainerStyle={{ flexGrow: 1, alignItems: "stretch" }}
|
||||
onMomentumScrollEnd={(event) => {
|
||||
const width = event.nativeEvent.layoutMeasurement.width || pageWidth;
|
||||
if (!width) return;
|
||||
setActiveIndex(
|
||||
Math.max(
|
||||
0,
|
||||
Math.min(
|
||||
rows.length - 1,
|
||||
Math.round(event.nativeEvent.contentOffset.x / width),
|
||||
),
|
||||
),
|
||||
);
|
||||
}}
|
||||
>
|
||||
{rows.map((d, index) => (
|
||||
<RenderRow
|
||||
{...controls}
|
||||
train={d}
|
||||
position={positions[index] ?? []}
|
||||
pageWidth={pageWidth}
|
||||
showLineLabel={rows.length > 1}
|
||||
index={index}
|
||||
key={index}
|
||||
/>
|
||||
))}
|
||||
</ScrollView>
|
||||
{hasMultipleRows && (
|
||||
<Text style={{ alignSelf: "center", fontSize: 11 }}>
|
||||
{visibleActiveIndex + 1} / {rows.length}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
type RenderRowProps = TrainDataViewRowProps & {
|
||||
pageWidth: number;
|
||||
index: number;
|
||||
};
|
||||
|
||||
const RenderRow = (props: RenderRowProps) => {
|
||||
const { pageWidth, index, ...rowProps } = props;
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
width: pageWidth || "100%",
|
||||
flexGrow: 0,
|
||||
flexShrink: 0,
|
||||
alignSelf: "stretch",
|
||||
}}
|
||||
>
|
||||
<TrainDataViewRow {...rowProps} />
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
type TrainDataViewRowProps = TrainDataViewControls & {
|
||||
train: trainDataType | undefined;
|
||||
position: string[];
|
||||
showLineLabel: boolean;
|
||||
mode?: TrainDataViewMode;
|
||||
};
|
||||
|
||||
const TrainDataViewRow: FC<TrainDataViewRowProps> = ({
|
||||
train,
|
||||
nearTrainIDList,
|
||||
position,
|
||||
showLineLabel,
|
||||
nearTrainIDList,
|
||||
openTrainInfo,
|
||||
mode = 0,
|
||||
navigate,
|
||||
}) => {
|
||||
const trainNum = train?.num ?? "";
|
||||
const currentTrainData = train;
|
||||
const currentPosition = position;
|
||||
const { width } = useWindowDimensions();
|
||||
const { width} = useWindowDimensions();
|
||||
const { setInjectData } = useCurrentTrainView();
|
||||
|
||||
const { allCustomTrainData } = useCustomTrainData();
|
||||
const onLine = !!currentPosition?.toString().length;
|
||||
const lineLabel = lineDisplayName(currentTrainData?.Line);
|
||||
|
||||
const [trainNumber, setTrainNumber] = useState(currentTrainData?.num);
|
||||
useEffect(() => {
|
||||
const { train_number_override } = customTrainDataDetector(
|
||||
currentTrainData?.num,
|
||||
allCustomTrainData,
|
||||
);
|
||||
if (train_number_override) {
|
||||
setTrainNumber(train_number_override);
|
||||
} else {
|
||||
setTrainNumber(currentTrainData?.num);
|
||||
}
|
||||
}, [currentTrainData?.num, allCustomTrainData]);
|
||||
const columnStyle = {
|
||||
width: width / 2,
|
||||
display: "flex",
|
||||
@@ -194,7 +91,7 @@ const TrainDataViewRow: FC<TrainDataViewRowProps> = ({
|
||||
display: "flex",
|
||||
}}
|
||||
>
|
||||
<View style={{ flexDirection: "row", flex: 1, ...columnStyle }}>
|
||||
<View style={{ flexDirection: "row",flex:1,...columnStyle }}>
|
||||
<TouchableOpacity
|
||||
style={{ flex: 1, minWidth: 0, flexDirection: "row" }}
|
||||
//disabled={!onLine}
|
||||
@@ -202,7 +99,7 @@ const TrainDataViewRow: FC<TrainDataViewRowProps> = ({
|
||||
if (!currentTrainData) return;
|
||||
setInjectData({
|
||||
type: "train",
|
||||
value: trainNum,
|
||||
value: currentTrainData?.num,
|
||||
fixed: true,
|
||||
});
|
||||
stackAwareNavigate("positions");
|
||||
@@ -242,11 +139,14 @@ const TrainDataViewRow: FC<TrainDataViewRowProps> = ({
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexDirection: mode == 2 ? "row" : "column",
|
||||
flex: 1,
|
||||
...columnStyle,
|
||||
}}
|
||||
style={
|
||||
{
|
||||
flexDirection: mode == 2 ? "row" : "column",
|
||||
flex: 1,
|
||||
...columnStyle
|
||||
}
|
||||
|
||||
}
|
||||
>
|
||||
<View style={rightItemStyle}>
|
||||
<StateBox
|
||||
@@ -274,7 +174,7 @@ const TrainDataViewRow: FC<TrainDataViewRowProps> = ({
|
||||
}}
|
||||
>
|
||||
{nearTrainIDList.length == 0 ? (
|
||||
<StateBox mode={mode} title="列番" text={trainNum} />
|
||||
<StateBox mode={mode} title="列番" text={trainNumber} />
|
||||
) : (
|
||||
<StateBox
|
||||
mode={mode}
|
||||
@@ -292,3 +192,82 @@ const TrainDataViewRow: FC<TrainDataViewRowProps> = ({
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export const TrainDataView: FC<props> = ({
|
||||
currentTrainData,
|
||||
currentTrainDataList,
|
||||
currentPosition,
|
||||
currentPositionList,
|
||||
...rest
|
||||
}) => {
|
||||
const rows = currentTrainDataList?.length
|
||||
? currentTrainDataList
|
||||
: [currentTrainData];
|
||||
const positions = currentPositionList?.length
|
||||
? currentPositionList
|
||||
: [currentPosition ?? []];
|
||||
const [activeIndex, setActiveIndex] = useState(0);
|
||||
const [pageWidth, setPageWidth] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
setActiveIndex((index) => Math.min(index, Math.max(rows.length - 1, 0)));
|
||||
}, [rows.length]);
|
||||
|
||||
const renderRow = (train: trainDataType | undefined, index: number) => (
|
||||
<View
|
||||
key={`${train?.Line ?? "unknown"}-${String(train?.PosNum ?? "")}-${index}`}
|
||||
style={{
|
||||
width: pageWidth || "100%",
|
||||
flexGrow: 0,
|
||||
flexShrink: 0,
|
||||
alignSelf: "stretch",
|
||||
}}
|
||||
>
|
||||
<TrainDataViewRow
|
||||
{...rest}
|
||||
currentTrainData={train}
|
||||
currentPosition={positions[index] ?? []}
|
||||
showLineLabel={rows.length > 1}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
||||
const hasMultipleRows = rows.length > 1;
|
||||
|
||||
return (
|
||||
<View
|
||||
onLayout={(event) => setPageWidth(event.nativeEvent.layout.width)}
|
||||
style={{ flex: 1, width: "100%" }}
|
||||
>
|
||||
<ScrollView
|
||||
horizontal
|
||||
pagingEnabled={hasMultipleRows}
|
||||
scrollEnabled={hasMultipleRows}
|
||||
nestedScrollEnabled
|
||||
showsHorizontalScrollIndicator={false}
|
||||
style={{ flex: 1, width: "100%" }}
|
||||
contentContainerStyle={{ flexGrow: 1, alignItems: "stretch" }}
|
||||
onMomentumScrollEnd={(event) => {
|
||||
const width = event.nativeEvent.layoutMeasurement.width || pageWidth;
|
||||
if (!width) return;
|
||||
setActiveIndex(
|
||||
Math.max(
|
||||
0,
|
||||
Math.min(
|
||||
rows.length - 1,
|
||||
Math.round(event.nativeEvent.contentOffset.x / width),
|
||||
),
|
||||
),
|
||||
);
|
||||
}}
|
||||
>
|
||||
{rows.map(renderRow)}
|
||||
</ScrollView>
|
||||
{hasMultipleRows && (
|
||||
<Text style={{ alignSelf: "center", fontSize: 11 }}>
|
||||
{activeIndex + 1} / {rows.length}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||
import dayjs from "dayjs";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
@@ -10,9 +9,9 @@ import {
|
||||
Linking,
|
||||
} from "react-native";
|
||||
import { SheetManager } from "react-native-actions-sheet";
|
||||
import { getTrainType } from "@/lib/getTrainType";
|
||||
import { getTrainType } from "../../lib/getTrainType";
|
||||
import { customTrainDataDetector } from "../custom-train-data";
|
||||
import { EachStopList, type EachStopData } from "./EachTrainInfo/EachStopList";
|
||||
import { EachStopList } from "./EachTrainInfo/EachStopList";
|
||||
import { DataConnectedButton } from "./EachTrainInfo/DataConnectedButton";
|
||||
import { DynamicHeaderScrollView } from "../DynamicHeaderScrollView";
|
||||
import { LongHeader } from "./EachTrainInfo/LongHeader";
|
||||
@@ -20,13 +19,10 @@ import { ShortHeader } from "./EachTrainInfo/ShortHeader";
|
||||
import { ScrollStickyContent } from "./EachTrainInfo/ScrollStickyContent";
|
||||
import { ShowSpecialTrain } from "./EachTrainInfo/ShowSpecialTrain";
|
||||
import { HeaderText } from "./EachTrainInfoCore/HeaderText";
|
||||
import { useStationList } from "@/stateBox/useStationList";
|
||||
import { useCurrentTrainData } from "@/stateBox/useCurrentTrain";
|
||||
import { useStationList } from "../../stateBox/useStationList";
|
||||
import { useCurrentTrainData } from "../../stateBox/useCurrentTrain";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
import {
|
||||
useCustomTrainData,
|
||||
useOperationLogs,
|
||||
} from "@/stateBox/useAllTrainDiagram";
|
||||
import { useCustomTrainData } from "@/stateBox/useAllTrainDiagram";
|
||||
import { useResponsive } from "@/lib/responsive";
|
||||
|
||||
// Custom hooks
|
||||
@@ -47,11 +43,6 @@ type EachTrainInfoCoreProps = {
|
||||
sheetOpened?: boolean;
|
||||
};
|
||||
|
||||
const parseEachStopData = (data: string): EachStopData => {
|
||||
const [station = "", se = "", time = "", platformNum] = data.split(",");
|
||||
return [station, se, time, platformNum];
|
||||
};
|
||||
|
||||
export const EachTrainInfoCore = ({
|
||||
trainNum,
|
||||
openStationACFromEachTrainInfo,
|
||||
@@ -60,56 +51,21 @@ export const EachTrainInfoCore = ({
|
||||
sheetOpened = false,
|
||||
}: EachTrainInfoCoreProps) => {
|
||||
const from = fromValue ?? "";
|
||||
const { allCustomTrainData, activateCustomTrainData } = useCustomTrainData();
|
||||
const { activateOperationLogs } = useOperationLogs();
|
||||
const { colors, fixed, isDark } = useThemeColors();
|
||||
const { verticalScale, moderateScale, fontScale } = useResponsive();
|
||||
const { stationList } = useStationList();
|
||||
const { allCustomTrainData } = useCustomTrainData();
|
||||
const { colors, fixed } = useThemeColors();
|
||||
const { verticalScale, moderateScale } = useResponsive();
|
||||
const { height } = useWindowDimensions();
|
||||
const { getCurrentStationData } = useCurrentTrainData();
|
||||
const { stationList } = useStationList();
|
||||
|
||||
useEffect(() => {
|
||||
if (!sheetOpened) return;
|
||||
activateCustomTrainData();
|
||||
activateOperationLogs();
|
||||
}, [
|
||||
activateCustomTrainData,
|
||||
activateOperationLogs,
|
||||
sheetOpened,
|
||||
]);
|
||||
|
||||
const stationNumberMap = useMemo(() => {
|
||||
const map = new Map<string, string[]>();
|
||||
for (const lineStations of stationList) {
|
||||
for (const station of lineStations) {
|
||||
if (station.StationNumber == null) continue;
|
||||
const numbers = map.get(station.StationName);
|
||||
if (numbers) {
|
||||
numbers.push(station.StationNumber);
|
||||
} else {
|
||||
map.set(station.StationName, [station.StationNumber]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}, [stationList]);
|
||||
|
||||
const scrollRef = useRef<any>(null);
|
||||
// Custom hooks for data management
|
||||
const { trainData, setTrainData, trueTrainID } =
|
||||
useTrainDiagramData(trainNum);
|
||||
const { trainDataWithThrough, haveThrough } = useThroughStations(trainData);
|
||||
const {
|
||||
showHeadStation,
|
||||
showTailStation,
|
||||
includedTrainIDs,
|
||||
extendToHeadStation,
|
||||
extendToTailStation,
|
||||
} = useExtendedStations(trainNum, trainData, setTrainData);
|
||||
const { headStation, tailStation, nearTrainIDList } = useNearbyTrains(
|
||||
trainNum,
|
||||
trainData,
|
||||
includedTrainIDs,
|
||||
);
|
||||
const stopStationIDList = useStopStationIDs(trainDataWithThrough);
|
||||
const {
|
||||
@@ -121,15 +77,12 @@ export const EachTrainInfoCore = ({
|
||||
pointsDisplay,
|
||||
} = useTrainPosition(trainNum, stopStationIDList);
|
||||
|
||||
const parsedTrainData = useMemo<readonly EachStopData[]>(
|
||||
() =>
|
||||
trainDataWithThrough.map(parseEachStopData),
|
||||
[trainDataWithThrough],
|
||||
);
|
||||
const clockBase = useMemo(
|
||||
() => dayjs(),
|
||||
[trainDataWithThrough, currentTrainData?.delay],
|
||||
);
|
||||
const {
|
||||
showHeadStation,
|
||||
showTailStation,
|
||||
extendToHeadStation,
|
||||
extendToTailStation,
|
||||
} = useExtendedStations(trainData, setTrainData);
|
||||
|
||||
// UI state
|
||||
// 走行中の列車は初期状態から通過駅を表示する(後から showThrew を true に変更すると
|
||||
@@ -137,11 +90,15 @@ export const EachTrainInfoCore = ({
|
||||
const [showThrew, setShowThrew] = useState(
|
||||
() => !!getCurrentStationData(trainNum),
|
||||
);
|
||||
const [isJumped, setIsJumped] = useState(false);
|
||||
|
||||
// Auto scroll to current position
|
||||
useAutoScroll(
|
||||
points,
|
||||
trainDataWithThrough,
|
||||
scrollRef,
|
||||
isJumped,
|
||||
setIsJumped,
|
||||
sheetOpened,
|
||||
);
|
||||
|
||||
@@ -175,133 +132,6 @@ export const EachTrainInfoCore = ({
|
||||
}, 200);
|
||||
});
|
||||
};
|
||||
const renderTrainDataItem = (item: string, index: number) => {
|
||||
const stopType = parsedTrainData[index]?.[1] ?? "";
|
||||
|
||||
if (stopType === "増" || stopType === "解") {
|
||||
return (
|
||||
<DataConnectedButton
|
||||
i={item}
|
||||
openTrainInfo={openTrainInfo}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (stopType.includes(".")) return null;
|
||||
|
||||
return (
|
||||
<EachStopList
|
||||
stationNumberMap={stationNumberMap}
|
||||
parsedTrainData={parsedTrainData}
|
||||
clockBase={clockBase}
|
||||
borderColor={colors.borderLight}
|
||||
textOnPrimary={fixed.textOnPrimary}
|
||||
isDark={isDark}
|
||||
trainDelay={currentTrainData?.delay}
|
||||
fontScale={fontScale}
|
||||
moderateScale={moderateScale}
|
||||
index={index}
|
||||
points={pointsDisplay?.[index] || false}
|
||||
openStationACFromEachTrainInfo={openStationACFromEachTrainInfo}
|
||||
showThrew={showThrew}
|
||||
isNotService={customTrainType.data === "notService"}
|
||||
/>
|
||||
);
|
||||
};
|
||||
const listHeader = (
|
||||
<>
|
||||
{customTrainType.data === "notService" && (
|
||||
<Text
|
||||
style={{
|
||||
backgroundColor: colors.surface,
|
||||
color: colors.text,
|
||||
fontWeight: "bold",
|
||||
}}
|
||||
>
|
||||
この列車には乗車できません。
|
||||
</Text>
|
||||
)}
|
||||
{headStation.length > 0 &&
|
||||
headStation.map(
|
||||
(item) =>
|
||||
!showHeadStation.includes(item.id) && (
|
||||
<TouchableOpacity
|
||||
onPress={() =>
|
||||
extendToHeadStation(item.station, item.dia, item.id)
|
||||
}
|
||||
style={[
|
||||
styles.extendStationButton,
|
||||
{ borderColor: colors.textAccent },
|
||||
]}
|
||||
key={item.id + "-head"}
|
||||
>
|
||||
<Text
|
||||
style={[styles.extendStationText, { color: colors.text }]}
|
||||
>
|
||||
「本当の始発駅」を表示
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
),
|
||||
)}
|
||||
<ShowSpecialTrain
|
||||
isTrainDataNothing={trainData.length === 0}
|
||||
setTrainData={setTrainData}
|
||||
trueTrainID={trueTrainID}
|
||||
/>
|
||||
{!trainData.length && (
|
||||
<TouchableOpacity
|
||||
onPress={() =>
|
||||
Linking.openURL("https://twitter.com/search?q=" + trainNum)
|
||||
}
|
||||
style={[
|
||||
styles.twitterSearchButton,
|
||||
{
|
||||
borderColor: colors.textAccent,
|
||||
backgroundColor: colors.backgroundOverlay,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Text style={[styles.extendStationText, { color: colors.text }]}>
|
||||
Twitterで検索
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
const listFooter = (
|
||||
<>
|
||||
<Text style={[styles.customDataNote, { color: colors.textSecondary }]}>
|
||||
時刻が斜体,青色になっている時刻はコミュニティで追加されている独自データです。
|
||||
</Text>
|
||||
{tailStation.length > 0 &&
|
||||
tailStation.map(
|
||||
({ station, dia, id }) =>
|
||||
!showTailStation.includes(id) && (
|
||||
<TouchableOpacity
|
||||
onPress={() => extendToTailStation(station, dia, id)}
|
||||
style={[
|
||||
styles.extendStationButton,
|
||||
{ borderColor: colors.textAccent },
|
||||
]}
|
||||
key={id + "-tail"}
|
||||
>
|
||||
<Text
|
||||
style={[styles.extendStationText, { color: colors.text }]}
|
||||
>
|
||||
「本当の終着駅」を表示
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
),
|
||||
)}
|
||||
<View
|
||||
style={[
|
||||
styles.bottomSpacer,
|
||||
{ borderBottomColor: colors.borderLight },
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
@@ -329,11 +159,14 @@ export const EachTrainInfoCore = ({
|
||||
trainData={trainData}
|
||||
showHeadStation={showHeadStation}
|
||||
showTailStation={showTailStation}
|
||||
headStation={headStation}
|
||||
tailStation={tailStation}
|
||||
navigate={navigate}
|
||||
from={from}
|
||||
fontLoaded={true}
|
||||
scrollRef={scrollRef}
|
||||
/>
|
||||
|
||||
<DynamicHeaderScrollView
|
||||
from={from}
|
||||
styles={styles as any}
|
||||
@@ -350,47 +183,145 @@ export const EachTrainInfoCore = ({
|
||||
shortHeader={
|
||||
<ShortHeader
|
||||
{...{
|
||||
openTrainInfo,
|
||||
navigate,
|
||||
nearTrainIDList,
|
||||
currentTrainData,
|
||||
currentTrainDataList,
|
||||
currentPosition,
|
||||
currentPositionList,
|
||||
nearTrainIDList,
|
||||
openTrainInfo,
|
||||
navigate,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
longHeader={
|
||||
<LongHeader
|
||||
{...{
|
||||
openTrainInfo,
|
||||
navigate,
|
||||
currentTrainData,
|
||||
nearTrainIDList,
|
||||
currentTrainDataList,
|
||||
currentPosition,
|
||||
currentPositionList,
|
||||
nearTrainIDList,
|
||||
openTrainInfo,
|
||||
navigate,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
topStickyContent={
|
||||
<ScrollStickyContent
|
||||
{...{
|
||||
trainDelay: currentTrainData?.delay,
|
||||
showThrew,
|
||||
setShowThrew,
|
||||
haveThrough,
|
||||
}}
|
||||
{...{ currentTrainData, showThrew, setShowThrew, haveThrough }}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{listHeader}
|
||||
{trainDataWithThrough.map((item, index) => (
|
||||
<React.Fragment key={`${trainNum}-${index}`}>
|
||||
{renderTrainDataItem(item, index)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
{listFooter}
|
||||
{customTrainType.data === "notService" && (
|
||||
<Text
|
||||
style={{
|
||||
backgroundColor: colors.surface,
|
||||
color: colors.text,
|
||||
fontWeight: "bold",
|
||||
}}
|
||||
>
|
||||
この列車には乗車できません。
|
||||
</Text>
|
||||
)}
|
||||
{headStation.length > 0 &&
|
||||
headStation.map(
|
||||
(item, index) =>
|
||||
!showHeadStation.includes(index) && (
|
||||
<TouchableOpacity
|
||||
onPress={() =>
|
||||
extendToHeadStation(item.station, item.dia, index)
|
||||
}
|
||||
style={[
|
||||
styles.extendStationButton,
|
||||
{ borderColor: colors.textAccent },
|
||||
]}
|
||||
key={`${item.station}-head${index}`}
|
||||
>
|
||||
<Text
|
||||
style={[styles.extendStationText, { color: colors.text }]}
|
||||
>
|
||||
「本当の始発駅」を表示
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
),
|
||||
)}
|
||||
<ShowSpecialTrain
|
||||
isTrainDataNothing={trainData.length === 0}
|
||||
setTrainData={setTrainData}
|
||||
trueTrainID={trueTrainID}
|
||||
/>
|
||||
{!trainData.length && (
|
||||
<TouchableOpacity
|
||||
onPress={() =>
|
||||
Linking.openURL(`https://twitter.com/search?q=${trainNum}`)
|
||||
}
|
||||
style={[
|
||||
styles.twitterSearchButton,
|
||||
{
|
||||
borderColor: colors.textAccent,
|
||||
backgroundColor: colors.backgroundOverlay,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Text style={[styles.extendStationText, { color: colors.text }]}>
|
||||
Twitterで検索
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
{trainDataWithThrough.map((item, index, array) =>
|
||||
item.split(",")[1] === "増" || item.split(",")[1] === "解" ? (
|
||||
<DataConnectedButton
|
||||
i={item}
|
||||
key={`${item}-data`}
|
||||
openTrainInfo={openTrainInfo}
|
||||
/>
|
||||
) : item.split(",")[1].includes(".") ? (
|
||||
<React.Fragment key={`${item}-skip`} />
|
||||
) : (
|
||||
<EachStopList
|
||||
i={item}
|
||||
index={index}
|
||||
stationList={stationList as any}
|
||||
points={pointsDisplay?.[index] || false}
|
||||
currentTrainData={currentTrainData as any}
|
||||
openStationACFromEachTrainInfo={openStationACFromEachTrainInfo}
|
||||
showThrew={showThrew}
|
||||
array={array}
|
||||
isNotService={customTrainType.data === "notService"}
|
||||
key={`${item}-stop`}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
<Text style={[styles.customDataNote, { color: colors.textSecondary }]}>
|
||||
時刻が斜体,青色になっている時刻はコミュニティで追加されている独自データです。
|
||||
</Text>
|
||||
{tailStation.length > 0 &&
|
||||
tailStation.map(
|
||||
({ station, dia }, index) =>
|
||||
!showTailStation.includes(index) && (
|
||||
<TouchableOpacity
|
||||
onPress={() => extendToTailStation(station, dia, index)}
|
||||
style={[
|
||||
styles.extendStationButton,
|
||||
{ borderColor: colors.textAccent },
|
||||
]}
|
||||
key={`${station}-tail${index}`}
|
||||
>
|
||||
<Text
|
||||
style={[styles.extendStationText, { color: colors.text }]}
|
||||
>
|
||||
「本当の終着駅」を表示
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
),
|
||||
)}
|
||||
|
||||
<View
|
||||
style={[
|
||||
styles.bottomSpacer,
|
||||
{ borderBottomColor: colors.borderLight },
|
||||
]}
|
||||
/>
|
||||
</DynamicHeaderScrollView>
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { FC, useMemo } from "react";
|
||||
import { Text, View, TextStyle, TouchableOpacity } from "react-native";
|
||||
import { SheetManager } from "react-native-actions-sheet";
|
||||
import { migrateTrainName } from "@/lib/eachTrainInfoCoreLib/migrateTrainName";
|
||||
import { migrateTrainName } from "../../../lib/eachTrainInfoCoreLib/migrateTrainName";
|
||||
import { TrainIconStatus } from "./trainIconStatus";
|
||||
import { OneManText } from "./HeaderTextParts/OneManText";
|
||||
import { customTrainDataDetector } from "@/components/custom-train-data";
|
||||
@@ -26,8 +26,10 @@ import { normalizeIconDisplayMode } from "@/lib/iconDisplayMode";
|
||||
type Props = {
|
||||
trainNum: string;
|
||||
trainData: string[];
|
||||
showHeadStation: string[];
|
||||
showTailStation: string[];
|
||||
showHeadStation: number[];
|
||||
showTailStation: number[];
|
||||
headStation: { id: string }[];
|
||||
tailStation: { id: string }[];
|
||||
navigate: NavigateFunction;
|
||||
from: string;
|
||||
fontLoaded: boolean;
|
||||
@@ -44,6 +46,8 @@ export const HeaderText: FC<Props> = ({
|
||||
trainData,
|
||||
showHeadStation,
|
||||
showTailStation,
|
||||
headStation,
|
||||
tailStation,
|
||||
navigate,
|
||||
from,
|
||||
scrollRef,
|
||||
@@ -305,9 +309,9 @@ export const HeaderText: FC<Props> = ({
|
||||
</TouchableOpacity>
|
||||
<View style={{ flex: 1 }} />
|
||||
<Text style={{ ...textConfig, fontSize: fontScale(17), color: fixed.textOnPrimary }}>
|
||||
{showHeadStation.map((trainID) => `${trainID} + `)}
|
||||
{showHeadStation.map((d) => `${headStation[d].id} + `)}
|
||||
{trainNum}
|
||||
{showTailStation.map((trainID) => ` + ${trainID}`)}
|
||||
{showTailStation.map((d) => ` + ${tailStation[d].id}`)}
|
||||
</Text>
|
||||
<MaterialCommunityIcons
|
||||
name="database"
|
||||
|
||||
@@ -2,7 +2,6 @@ import React, { ComponentProps, FC } from "react";
|
||||
import { Image, TouchableOpacity, View } from "react-native";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import type { TrainIconEntry } from "@/lib/trainIconEntries";
|
||||
import { normalizeVehicleInfoUrl } from "@/lib/vehicleInfoUrl";
|
||||
|
||||
type StackVariant = "header" | "list";
|
||||
type StatusIcon = ComponentProps<typeof Ionicons>;
|
||||
@@ -88,7 +87,7 @@ export const TrainIconStack: FC<Props> = ({
|
||||
<TouchableOpacity
|
||||
key={`${trainIcon}-${index}`}
|
||||
onPress={() => onPressEntry(entry, index)}
|
||||
disabled={!normalizeVehicleInfoUrl(entry.vehicle_info_url)}
|
||||
disabled={!entry.vehicle_info_url}
|
||||
>
|
||||
{content}
|
||||
</TouchableOpacity>
|
||||
|
||||
@@ -1,46 +1,33 @@
|
||||
import { useEffect, MutableRefObject, useRef } from 'react';
|
||||
import { useEffect, MutableRefObject } from 'react';
|
||||
|
||||
|
||||
export const useAutoScroll = (
|
||||
points: boolean[] | undefined,
|
||||
trainDataWithThrough: string[],
|
||||
scrollRef: MutableRefObject<any>,
|
||||
isJumped: boolean,
|
||||
setIsJumped: (value: boolean) => void,
|
||||
sheetOpened: boolean = false
|
||||
) => {
|
||||
const jumpedRef = useRef(false);
|
||||
const trainDataRef = useRef(trainDataWithThrough);
|
||||
|
||||
useEffect(() => {
|
||||
if (trainDataRef.current !== trainDataWithThrough) {
|
||||
trainDataRef.current = trainDataWithThrough;
|
||||
jumpedRef.current = false;
|
||||
}
|
||||
|
||||
// ActionSheetのスプリングアニメーション完了後まで待機
|
||||
if (!sheetOpened || jumpedRef.current || !points?.length || !scrollRef) return;
|
||||
if (!sheetOpened || isJumped || !points?.length || !scrollRef) return;
|
||||
|
||||
const currentPositionIndex = points.findIndex((d) => d === true);
|
||||
if (currentPositionIndex === -1) return;
|
||||
|
||||
// 5駅以内の場合はスクロールしない
|
||||
if (currentPositionIndex < 5) {
|
||||
jumpedRef.current = true;
|
||||
setIsJumped(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const scrollPosition = currentPositionIndex * 44 - 50;
|
||||
const timer = setTimeout(() => {
|
||||
if (typeof scrollRef.current?.scrollToOffset === 'function') {
|
||||
scrollRef.current.scrollToOffset({
|
||||
offset: scrollPosition,
|
||||
animated: true,
|
||||
});
|
||||
} else {
|
||||
scrollRef.current?.scrollTo({ y: scrollPosition, animated: true });
|
||||
}
|
||||
jumpedRef.current = true;
|
||||
scrollRef.current?.scrollTo({ y: scrollPosition, animated: true });
|
||||
setIsJumped(true);
|
||||
}, 100);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [sheetOpened, points, trainDataWithThrough, scrollRef]);
|
||||
}, [sheetOpened, points, trainDataWithThrough, scrollRef, isJumped, setIsJumped]);
|
||||
};
|
||||
|
||||
@@ -1,85 +1,29 @@
|
||||
import { useState } from 'react';
|
||||
import { openBackTrainInfo } from '@/lib/eachTrainInfoCoreLib/openBackTrainInfo';
|
||||
|
||||
type ExtendedState = {
|
||||
trainNum: string;
|
||||
headTrainIDs: string[];
|
||||
tailTrainIDs: string[];
|
||||
includedTrainIDs: string[];
|
||||
};
|
||||
export const useExtendedStations = (trainData, setTrainData) => {
|
||||
const [showHeadStation, setShowHeadStation] = useState([]);
|
||||
const [showTailStation, setShowTailStation] = useState([]);
|
||||
|
||||
const initialExtendedState = (trainNum: string): ExtendedState => ({
|
||||
trainNum,
|
||||
headTrainIDs: [],
|
||||
tailTrainIDs: [],
|
||||
includedTrainIDs: trainNum ? [trainNum] : [],
|
||||
});
|
||||
|
||||
export const useExtendedStations = (
|
||||
trainNum: string,
|
||||
trainData: string[],
|
||||
setTrainData: (data: string[]) => void,
|
||||
) => {
|
||||
const [extendedState, setExtendedState] = useState(() =>
|
||||
initialExtendedState(trainNum),
|
||||
);
|
||||
const currentState =
|
||||
extendedState.trainNum === trainNum
|
||||
? extendedState
|
||||
: initialExtendedState(trainNum);
|
||||
|
||||
const extendToHeadStation = (
|
||||
station: string,
|
||||
dia: string[],
|
||||
trainID: string,
|
||||
) => {
|
||||
const extendToHeadStation = (station, dia, index) => {
|
||||
const array = openBackTrainInfo(station, trainData, dia);
|
||||
if (!array) return;
|
||||
|
||||
|
||||
setTrainData(array);
|
||||
setExtendedState((previous) => {
|
||||
const state =
|
||||
previous.trainNum === trainNum
|
||||
? previous
|
||||
: initialExtendedState(trainNum);
|
||||
return {
|
||||
...state,
|
||||
headTrainIDs: [trainID, ...state.headTrainIDs],
|
||||
includedTrainIDs: state.includedTrainIDs.includes(trainID)
|
||||
? state.includedTrainIDs
|
||||
: [...state.includedTrainIDs, trainID],
|
||||
};
|
||||
});
|
||||
setShowHeadStation((prev) => [...prev, index]);
|
||||
};
|
||||
|
||||
const extendToTailStation = (
|
||||
station: string,
|
||||
dia: string[],
|
||||
trainID: string,
|
||||
) => {
|
||||
const extendToTailStation = (station, dia, index) => {
|
||||
const array = openBackTrainInfo(station, trainData, dia);
|
||||
if (!array) return;
|
||||
|
||||
|
||||
setTrainData(array);
|
||||
setExtendedState((previous) => {
|
||||
const state =
|
||||
previous.trainNum === trainNum
|
||||
? previous
|
||||
: initialExtendedState(trainNum);
|
||||
return {
|
||||
...state,
|
||||
tailTrainIDs: [...state.tailTrainIDs, trainID],
|
||||
includedTrainIDs: state.includedTrainIDs.includes(trainID)
|
||||
? state.includedTrainIDs
|
||||
: [...state.includedTrainIDs, trainID],
|
||||
};
|
||||
});
|
||||
setShowTailStation((prev) => [...prev, index]);
|
||||
};
|
||||
|
||||
return {
|
||||
showHeadStation: currentState.headTrainIDs,
|
||||
showTailStation: currentState.tailTrainIDs,
|
||||
includedTrainIDs: currentState.includedTrainIDs,
|
||||
showHeadStation,
|
||||
showTailStation,
|
||||
extendToHeadStation,
|
||||
extendToTailStation,
|
||||
};
|
||||
|
||||
@@ -1,61 +1,27 @@
|
||||
import { useMemo } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useBusAndTrainData } from "@/stateBox/useBusAndTrainData";
|
||||
|
||||
type InfluencedTrain = { id: string; TrainData: string[] };
|
||||
type BoundaryTrain = { station: string; dia: string[]; id: string };
|
||||
|
||||
export const useNearbyTrains = (
|
||||
trainNum: string,
|
||||
trainData: string[],
|
||||
includedTrainIDs: string[],
|
||||
) => {
|
||||
export const useNearbyTrains = (trainNum, trainData) => {
|
||||
const { getInfluencedTrainData } = useBusAndTrainData();
|
||||
const [headStation, setHeadStation] = useState([]);
|
||||
const [tailStation, setTailStation] = useState([]);
|
||||
const [nearTrainIDList, setNearTrainIDList] = useState([]);
|
||||
|
||||
return useMemo(() => {
|
||||
if (!trainNum || !trainData.length) {
|
||||
return { headStation: [], tailStation: [], nearTrainIDList: [] };
|
||||
}
|
||||
useEffect(() => {
|
||||
if (!trainNum || !trainData.length) return;
|
||||
|
||||
const nearTrainList = getInfluencedTrainData(trainNum) ?? [];
|
||||
if (!nearTrainList.length) {
|
||||
return { headStation: [], tailStation: [], nearTrainIDList: [] };
|
||||
}
|
||||
const nearTrainList = getInfluencedTrainData(trainNum);
|
||||
if (!nearTrainList.length) return;
|
||||
|
||||
// Opening any train in a multi-train service should allow the whole
|
||||
// connected chain to be extended. The direct list is still kept for the
|
||||
// existing header action; boundary candidates come from its transitive
|
||||
// connections.
|
||||
const connectedTrainByID = new Map<string, InfluencedTrain>();
|
||||
const visitedTrainIDs = new Set<string>([trainNum]);
|
||||
const pendingTrainIDs = [trainNum];
|
||||
const trainDataArray = nearTrainList.map((d) => d.TrainData);
|
||||
const trainIDs = nearTrainList.map((d) => d.id);
|
||||
setNearTrainIDList(trainIDs);
|
||||
|
||||
while (pendingTrainIDs.length > 0) {
|
||||
const currentTrainID = pendingTrainIDs.shift();
|
||||
if (!currentTrainID) continue;
|
||||
const connectedTrains = getInfluencedTrainData(currentTrainID) ?? [];
|
||||
|
||||
connectedTrains.forEach((connectedTrain) => {
|
||||
if (!connectedTrain?.id || visitedTrainIDs.has(connectedTrain.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
visitedTrainIDs.add(connectedTrain.id);
|
||||
connectedTrainByID.set(connectedTrain.id, connectedTrain);
|
||||
pendingTrainIDs.push(connectedTrain.id);
|
||||
});
|
||||
}
|
||||
|
||||
const includedTrainIDSet = new Set(includedTrainIDs);
|
||||
const candidateTrains = Array.from(connectedTrainByID.values()).filter(
|
||||
(train) => !includedTrainIDSet.has(train.id),
|
||||
);
|
||||
const trainDataArray = candidateTrains.map((d) => d.TrainData);
|
||||
const trainIDs = candidateTrains.map((d) => d.id);
|
||||
const [firstStation] = trainData[0].split(",");
|
||||
const [lastStation] = trainData[trainData.length - 1].split(",");
|
||||
|
||||
const head: BoundaryTrain[] = [];
|
||||
const tail: BoundaryTrain[] = [];
|
||||
const head = [];
|
||||
const tail = [];
|
||||
|
||||
trainDataArray.forEach((data, i) => {
|
||||
data.forEach((d) => {
|
||||
@@ -81,10 +47,9 @@ export const useNearbyTrains = (
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
headStation: head,
|
||||
tailStation: tail,
|
||||
nearTrainIDList: nearTrainList.map((d) => d.id),
|
||||
};
|
||||
}, [getInfluencedTrainData, includedTrainIDs, trainData, trainNum]);
|
||||
setHeadStation(head);
|
||||
setTailStation(tail);
|
||||
}, [trainData, trainNum, getInfluencedTrainData]);
|
||||
|
||||
return { headStation, tailStation, nearTrainIDList };
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useStationList } from '@/stateBox/useStationList';
|
||||
|
||||
const computeStopStationIDs = (data: string[], stationList: any[][]): string[][] =>
|
||||
@@ -13,9 +13,14 @@ const computeStopStationIDs = (data: string[], stationList: any[][]): string[][]
|
||||
export const useStopStationIDs = (trainDataWithThrough: string[]) => {
|
||||
const { stationList } = useStationList();
|
||||
|
||||
// station IDも表示データから直接導出し、effect由来の追加renderをなくす。
|
||||
return useMemo(
|
||||
() => computeStopStationIDs(trainDataWithThrough, stationList),
|
||||
[trainDataWithThrough, stationList],
|
||||
// 初回レンダリング時に同期的に計算することでActionSheetのアニメーション中の高さ変化を防ぐ
|
||||
const [stopStationIDList, setStopStationIDList] = useState<string[][]>(() =>
|
||||
computeStopStationIDs(trainDataWithThrough, stationList)
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setStopStationIDList(computeStopStationIDs(trainDataWithThrough, stationList));
|
||||
}, [trainDataWithThrough, stationList]);
|
||||
|
||||
return stopStationIDList;
|
||||
};
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { lineListPair, stationIDPair } from '@/lib/getStationList';
|
||||
import { useStationList } from '@/stateBox/useStationList';
|
||||
import { OriginalStationList } from '@/lib/CommonTypes';
|
||||
|
||||
const computeThroughStationsUnmeasured = (
|
||||
const computeThroughStations = (
|
||||
trainData: string[],
|
||||
stationList: any[][],
|
||||
originalStationList: OriginalStationList
|
||||
@@ -103,20 +103,14 @@ const computeThroughStationsUnmeasured = (
|
||||
export const useThroughStations = (trainData) => {
|
||||
const { originalStationList, stationList } = useStationList();
|
||||
|
||||
// 純粋な派生値なのでstate/effectで一度古いリストを描画してから更新しない。
|
||||
return useMemo(
|
||||
() => computeThroughStations(trainData, stationList, originalStationList),
|
||||
[trainData, stationList, originalStationList],
|
||||
// 初回レンダリング時に同期的に計算することでActionSheetのアニメーション中の高さ変化を防ぐ
|
||||
const [state, setState] = useState(() =>
|
||||
computeThroughStations(trainData, stationList, originalStationList)
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setState(computeThroughStations(trainData, stationList, originalStationList));
|
||||
}, [trainData, stationList, originalStationList]);
|
||||
|
||||
return { trainDataWithThrough: state.trainDataWithThrough, haveThrough: state.haveThrough };
|
||||
};
|
||||
function computeThroughStations(
|
||||
trainData: string[],
|
||||
stationList: any[][],
|
||||
originalStationList: OriginalStationList,
|
||||
): { trainDataWithThrough: string[]; haveThrough: boolean } {
|
||||
return computeThroughStationsUnmeasured(
|
||||
trainData,
|
||||
stationList,
|
||||
originalStationList,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useTrainDiagram } from '@/stateBox/useAllTrainDiagram';
|
||||
import { searchSpecialTrain } from '@/lib/eachTrainInfoCoreLib/searchSpecialTrain';
|
||||
|
||||
const parseTrainData = (
|
||||
trainNum: string,
|
||||
trainList: Record<string, string>,
|
||||
) => {
|
||||
const parseTrainData = (trainNum: string, trainList: Record<string, string>) => {
|
||||
if (!trainNum) return { data: [], trueIDs: [] };
|
||||
const TD = trainList[trainNum];
|
||||
if (!TD) {
|
||||
@@ -17,30 +14,28 @@ const parseTrainData = (
|
||||
|
||||
export const useTrainDiagramData = (trainNum) => {
|
||||
const { allTrainDiagram: trainList } = useTrainDiagram();
|
||||
const parsed = useMemo(
|
||||
() => parseTrainData(trainNum, trainList),
|
||||
[trainNum, trainList],
|
||||
);
|
||||
const [manualData, setManualData] = useState<{
|
||||
trainNum: string;
|
||||
data: string[];
|
||||
} | null>(null);
|
||||
const [isManuallyExtended, setIsManuallyExtended] = useState(false);
|
||||
|
||||
// ダイヤ本体は入力から直接導出する。列車切替時に前の列車を一度描画して
|
||||
// effectで新しい配列へ差し替える経路をなくし、リストの余分な再描画を防ぐ。
|
||||
const trainData =
|
||||
manualData?.trainNum === trainNum ? manualData.data : parsed.data;
|
||||
// 初回レンダリング時にコンテキストから同期的にデータを取得することで
|
||||
// ActionSheetのアニメーション中に高さが変わるのを防ぐ
|
||||
const [trainData, setTrainData] = useState(() => parseTrainData(trainNum, trainList).data);
|
||||
const [trueTrainID, setTrueTrainID] = useState(() => parseTrainData(trainNum, trainList).trueIDs);
|
||||
|
||||
const setTrainDataExtended = useCallback(
|
||||
(data: string[]) => {
|
||||
setManualData({ trainNum, data });
|
||||
},
|
||||
[trainNum],
|
||||
);
|
||||
useEffect(() => {
|
||||
if (!trainNum) return;
|
||||
|
||||
return {
|
||||
trainData,
|
||||
setTrainData: setTrainDataExtended,
|
||||
trueTrainID: parsed.trueIDs,
|
||||
// 手動で拡張されている場合は上書きしない
|
||||
if (isManuallyExtended) return;
|
||||
|
||||
const { data, trueIDs } = parseTrainData(trainNum, trainList);
|
||||
setTrueTrainID(trueIDs);
|
||||
setTrainData(data);
|
||||
}, [trainNum, trainList, isManuallyExtended]);
|
||||
|
||||
const setTrainDataExtended = (data) => {
|
||||
setTrainData(data);
|
||||
setIsManuallyExtended(true);
|
||||
};
|
||||
|
||||
return { trainData, setTrainData: setTrainDataExtended, trueTrainID };
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { AS } from '@/storageControl';
|
||||
import { STORAGE_KEYS } from '@/constants';
|
||||
import { logger } from '@/utils/logger';
|
||||
@@ -7,6 +7,7 @@ import { useTrainMenu } from '@/stateBox/useTrainMenu';
|
||||
import type { trainDataType } from '@/lib/trainPositionTextArray';
|
||||
import { lineIdFromStationNumber } from '@/lib/checkDuplicateTrainData';
|
||||
import { findReversalPoints } from '@/lib/eachTrainInfoCoreLib/findReversalPoints';
|
||||
|
||||
export const useTrainPosition = (
|
||||
trainNum: string,
|
||||
stopStationIDList: string[][]
|
||||
@@ -17,9 +18,13 @@ export const useTrainPosition = (
|
||||
getPosition,
|
||||
} = useCurrentTrainData();
|
||||
const { updatePermission } = useTrainMenu();
|
||||
const [currentTrainData, setCurrentTrainData] = useState<trainDataType | undefined>();
|
||||
const [currentTrainDataList, setCurrentTrainDataList] = useState<trainDataType[]>([]);
|
||||
const [currentPosition, setCurrentPosition] = useState<string[]>([]);
|
||||
const [currentPositionList, setCurrentPositionList] = useState<string[][]>([]);
|
||||
const [trainPositionSwitch, setTrainPositionSwitch] = useState<string>('false');
|
||||
|
||||
const currentTrainDataList = useMemo(() => {
|
||||
useEffect(() => {
|
||||
const candidates = getCurrentStationDataList(trainNum);
|
||||
const routeLineCounts = new Map<string, number>();
|
||||
for (const stationNumber of stopStationIDList.flat()) {
|
||||
@@ -30,11 +35,14 @@ export const useTrainPosition = (
|
||||
.sort(([, countA], [, countB]) => countB - countA)
|
||||
.map(([line]) => line);
|
||||
const selected = getCurrentStationData(trainNum, routeLineIds);
|
||||
return updatePermission
|
||||
const visible = updatePermission
|
||||
? candidates
|
||||
: selected
|
||||
? [selected]
|
||||
: [];
|
||||
logger.debug('Train position data:', visible);
|
||||
setCurrentTrainDataList(visible);
|
||||
setCurrentTrainData(visible[0]);
|
||||
}, [
|
||||
getCurrentStationData,
|
||||
getCurrentStationDataList,
|
||||
@@ -42,16 +50,15 @@ export const useTrainPosition = (
|
||||
trainNum,
|
||||
updatePermission,
|
||||
]);
|
||||
const currentTrainData = currentTrainDataList[0];
|
||||
|
||||
useEffect(() => {
|
||||
logger.debug('Train position data:', currentTrainDataList);
|
||||
}, [currentTrainDataList]);
|
||||
if (!stopStationIDList.length || !currentTrainDataList.length) {
|
||||
setCurrentPositionList([]);
|
||||
setCurrentPosition([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const currentPositionList = useMemo(() => {
|
||||
if (!stopStationIDList.length || !currentTrainDataList.length) return [];
|
||||
|
||||
return currentTrainDataList.map((train) => {
|
||||
const positions = currentTrainDataList.map((train) => {
|
||||
const position = getPosition(train);
|
||||
const next = position ? [...position] : [];
|
||||
if (next.length > 1) {
|
||||
@@ -72,8 +79,9 @@ export const useTrainPosition = (
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}, [currentTrainDataList, getPosition, stopStationIDList]);
|
||||
const currentPosition = currentPositionList[0] ?? [];
|
||||
setCurrentPositionList(positions);
|
||||
setCurrentPosition(positions[0] ?? []);
|
||||
}, [currentTrainDataList, stopStationIDList, getPosition]);
|
||||
|
||||
useEffect(() => {
|
||||
AS.getItem(STORAGE_KEYS.TRAIN_POSITION_SWITCH)
|
||||
@@ -99,21 +107,15 @@ export const useTrainPosition = (
|
||||
return merged;
|
||||
};
|
||||
|
||||
const points = useMemo(
|
||||
() =>
|
||||
trainPositionSwitch === 'true'
|
||||
? mergePoints(true)
|
||||
: stopStationIDList.map(() => false),
|
||||
[currentPositionList, stopStationIDList, trainPositionSwitch],
|
||||
);
|
||||
const points =
|
||||
trainPositionSwitch === 'true'
|
||||
? mergePoints(true)
|
||||
: stopStationIDList.map(() => false);
|
||||
|
||||
const pointsDisplay = useMemo(
|
||||
() =>
|
||||
trainPositionSwitch === 'true'
|
||||
? mergePoints(false)
|
||||
: stopStationIDList.map(() => false),
|
||||
[currentPositionList, stopStationIDList, trainPositionSwitch],
|
||||
);
|
||||
const pointsDisplay =
|
||||
trainPositionSwitch === 'true'
|
||||
? mergePoints(false)
|
||||
: stopStationIDList.map(() => false);
|
||||
|
||||
return {
|
||||
currentTrainData,
|
||||
|
||||
@@ -2,14 +2,13 @@ import React, { ComponentProps, FC, useEffect, useMemo, useState } from "react";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import dayjs from "dayjs";
|
||||
import { SheetManager } from "react-native-actions-sheet";
|
||||
import { customTrainDataDetector } from "@/components/custom-train-data";
|
||||
import { customTrainDataDetector } from "../../custom-train-data";
|
||||
import { useCustomTrainData } from "@/stateBox/useAllTrainDiagram";
|
||||
import { useInterval } from "@/lib/useInterval";
|
||||
import type { NavigateFunction } from "@/types";
|
||||
import { OperationLogs } from "@/lib/CommonTypes";
|
||||
import type { IconDisplayMode } from "@/lib/iconDisplayMode";
|
||||
import { resolveTrainIconEntries } from "@/lib/trainIconEntries";
|
||||
import { normalizeVehicleInfoUrl } from "@/lib/vehicleInfoUrl";
|
||||
import { TrainIconStack } from "./TrainIconStack";
|
||||
|
||||
type GlyphNames = ComponentProps<typeof Ionicons>["name"];
|
||||
@@ -123,22 +122,11 @@ export const TrainIconStatus: FC<Props> = (props) => {
|
||||
hidden={!!anpanmanStatus && showIcon}
|
||||
statusIcon={anpanmanStatus}
|
||||
onPressEntry={(entry) => {
|
||||
const vehicleInfoUrl = normalizeVehicleInfoUrl(entry.vehicle_info_url);
|
||||
if (!vehicleInfoUrl) return;
|
||||
|
||||
void SheetManager.hide("EachTrainInfo")
|
||||
.then(() => {
|
||||
navigate("generalWebView", {
|
||||
uri: vehicleInfoUrl,
|
||||
useExitButton: true,
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
navigate("generalWebView", {
|
||||
uri: vehicleInfoUrl,
|
||||
useExitButton: true,
|
||||
});
|
||||
});
|
||||
navigate("howto", {
|
||||
info: entry.vehicle_info_url,
|
||||
goTo: from == "LED" ? "menu" : from,
|
||||
});
|
||||
SheetManager.hide("EachTrainInfo");
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { FC, useEffect, useState } from "react";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { SheetManager } from "react-native-actions-sheet";
|
||||
import { getType } from "@/lib/eachTrainInfoCoreLib/getType";
|
||||
import { getType } from "../../../lib/eachTrainInfoCoreLib/getType";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
import type { NavigateFunction } from "@/types";
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ import LottieView from "lottie-react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import ViewShot from "react-native-view-shot";
|
||||
import * as Sharing from "expo-sharing";
|
||||
import { useTrainDelayData } from "@/stateBox/useTrainDelayData";
|
||||
import { useTrainDelayData } from "../../stateBox/useTrainDelayData";
|
||||
import { BottomButtons } from "./JRSTraInfo/BottomButtons";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
import { useResponsive } from "@/lib/responsive";
|
||||
@@ -127,7 +127,7 @@ export const JRSTraInfo = () => {
|
||||
autoPlay
|
||||
loop
|
||||
style={{ width: 150, height: 150, backgroundColor: colors.surface }}
|
||||
source={require("@/assets/51690-loading-diamonds.json")}
|
||||
source={require("../../assets/51690-loading-diamonds.json")}
|
||||
/>
|
||||
</View>
|
||||
) : delayData ? (
|
||||
|
||||
@@ -67,7 +67,7 @@ export const Social = () => {
|
||||
<ListItem
|
||||
bottomDivider
|
||||
onPress={() => Linking.openURL("tel:0570-00-4592")}
|
||||
// @ts-expect-error RNEUIのListItem型はComponentに渡したTouchableScaleのpropsを含まない
|
||||
// @ts-ignore: TouchableScale props passed through Component
|
||||
friction={90}
|
||||
tension={100}
|
||||
activeScale={0.95}
|
||||
@@ -83,7 +83,7 @@ export const Social = () => {
|
||||
0570-00-4592(8:00〜20:00 年中無休)
|
||||
</ListItem.Subtitle>
|
||||
</ListItem.Content>
|
||||
{/* @ts-expect-error RNEUIのListItem.Chevron型はcolorを公開していないが、実装はIconへ渡す */}
|
||||
{/* @ts-ignore: ListItem.Chevron doesn't expose color prop */}
|
||||
<ListItem.Chevron color={"white"} />
|
||||
</ListItem>
|
||||
<ScrollView
|
||||
@@ -154,7 +154,7 @@ export const Social = () => {
|
||||
bottomDivider
|
||||
onPress={() => Linking.openURL(d.url)}
|
||||
key={d.url}
|
||||
// @ts-expect-error RNEUIのListItem型はComponentに渡したTouchableScaleのpropsを含まない
|
||||
// @ts-ignore: TouchableScale props passed through Component
|
||||
friction={90} //
|
||||
tension={100} // These props are passed to the parent component (here TouchableScale)
|
||||
activeScale={0.95} //
|
||||
|
||||
@@ -9,11 +9,11 @@ import {
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import ActionSheet, { SheetManager } from "react-native-actions-sheet";
|
||||
import type { SheetProps } from "react-native-actions-sheet";
|
||||
import Sign from "@/components/駅名表/Sign";
|
||||
import Sign from "../../components/駅名表/Sign";
|
||||
|
||||
import { getPDFViewURL } from "@/lib/getPdfViewURL";
|
||||
import { useBusAndTrainData } from "@/stateBox/useBusAndTrainData";
|
||||
import { AS } from "@/storageControl";
|
||||
import { getPDFViewURL } from "../../lib/getPdfViewURL";
|
||||
import { useBusAndTrainData } from "../../stateBox/useBusAndTrainData";
|
||||
import { AS } from "../../storageControl";
|
||||
import { STORAGE_KEYS } from "@/constants";
|
||||
import { StationMapButton } from "./StationDeteilView/StationMapButton";
|
||||
import { TrainBusButton } from "./StationDeteilView/TrainBusButton";
|
||||
@@ -103,9 +103,10 @@ export const StationDeteilView = ({ payload }: SheetProps<"StationDetailView">)
|
||||
oP={() => {
|
||||
usePDFView == "true"
|
||||
? Linking.openURL(station.StationTimeTable)
|
||||
: navigate("generalWebView", {
|
||||
uri: info,
|
||||
onExit: goTo != "NearTrainDiagramView" ? useShow : undefined,
|
||||
: navigate("howto", {
|
||||
info,
|
||||
goTo,
|
||||
useShow,
|
||||
});
|
||||
onExit();
|
||||
}}
|
||||
@@ -164,9 +165,10 @@ export const StationDeteilView = ({ payload }: SheetProps<"StationDetailView">)
|
||||
<TrainBusButton
|
||||
address={trainBus.address}
|
||||
press={() => {
|
||||
navigate("generalWebView", {
|
||||
uri: trainBus.address,
|
||||
onExit: goTo != "NearTrainDiagramView" ? useShow : undefined,
|
||||
navigate("howto", {
|
||||
info: trainBus.address,
|
||||
goTo,
|
||||
useShow,
|
||||
});
|
||||
onExit();
|
||||
}}
|
||||
|
||||
@@ -25,10 +25,7 @@ export const 駅構内図:FC<Props> = (props) => {
|
||||
flex: 1,
|
||||
}}
|
||||
onPress={() => {
|
||||
navigate("generalWebView", {
|
||||
uri: info,
|
||||
onExit: goTo != "NearTrainDiagramView" ? useShow : undefined,
|
||||
});
|
||||
navigate("howto", { info, goTo, useShow });
|
||||
onExit();
|
||||
}}
|
||||
onLongPress={() => Linking.openURL(info)}
|
||||
|
||||
@@ -24,10 +24,7 @@ export const StationTimeTableButton: FC<Props> = (props) => {
|
||||
onPressButton={() => {
|
||||
usePDFView == "true"
|
||||
? Linking.openURL(address)
|
||||
: navigate("generalWebView", {
|
||||
uri: info,
|
||||
onExit: goTo != "NearTrainDiagramView" ? useShow : undefined,
|
||||
});
|
||||
: navigate("howto", { info, goTo, useShow });
|
||||
onExit();
|
||||
}}
|
||||
onLongPressButton={() => Linking.openURL(address)}
|
||||
|
||||
@@ -20,10 +20,7 @@ export const WebSiteButton: FC<Props> = (Props) => {
|
||||
icon={<Foundation name="web" color={fixed.textOnPrimary} size={50} />}
|
||||
flex={1}
|
||||
onPressButton={() => {
|
||||
navigate("generalWebView", {
|
||||
uri: info,
|
||||
onExit: goTo != "NearTrainDiagramView" ? useShow : undefined,
|
||||
});
|
||||
navigate("howto", { info, goTo, useShow });
|
||||
onExit();
|
||||
}}
|
||||
onLongPressButton={() => Linking.openURL(info)}
|
||||
|
||||
@@ -12,8 +12,8 @@ import ActionSheet, { SheetManager } from "react-native-actions-sheet";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import ViewShot from "react-native-view-shot";
|
||||
import * as Sharing from "expo-sharing";
|
||||
import icons from "@/assets/icons/icons";
|
||||
import hubIcons from "@/assets/unyoHubIcons/icons";
|
||||
import icons from "../../assets/icons/icons";
|
||||
import hubIcons from "../../assets/unyoHubIcons/icons";
|
||||
|
||||
import { getAppIconName } from "expo-alternate-app-icons";
|
||||
import { AS } from "@/storageControl";
|
||||
|
||||
@@ -8,10 +8,10 @@ import {
|
||||
} from "react-native";
|
||||
import ActionSheet, { SheetManager } from "react-native-actions-sheet";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { useTrainMenu } from "@/stateBox/useTrainMenu";
|
||||
import { useCurrentTrainView } from "@/stateBox/useCurrentTrain";
|
||||
import lineColorList from "@/assets/originData/lineColorList";
|
||||
import { stationIDPair, lineListPair } from "@/lib/getStationList";
|
||||
import { useTrainMenu } from "../../stateBox/useTrainMenu";
|
||||
import { useCurrentTrainView } from "../../stateBox/useCurrentTrain";
|
||||
import lineColorList from "../../assets/originData/lineColorList";
|
||||
import { stationIDPair, lineListPair } from "../../lib/getStationList";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
import { useSheetMaxHeight } from "./useSheetMaxHeight";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState, useRef, FC } from "react";
|
||||
import React, { useState, useRef, FC } from "react";
|
||||
import {
|
||||
Animated,
|
||||
View,
|
||||
@@ -41,16 +41,8 @@ export const AllTrainDiagramView: FC = () => {
|
||||
const iconDisplayMode = normalizeIconDisplayMode(iconSetting);
|
||||
const tabBarHeight = useBottomTabBarHeight();
|
||||
const { keyList, allTrainDiagram } = useTrainDiagram();
|
||||
const { allCustomTrainData, activateCustomTrainData } = useCustomTrainData();
|
||||
const {
|
||||
getTodayOperationByTrainId,
|
||||
activateOperationLogs,
|
||||
} = useOperationLogs();
|
||||
|
||||
useEffect(() => {
|
||||
activateCustomTrainData();
|
||||
activateOperationLogs();
|
||||
}, [activateCustomTrainData, activateOperationLogs]);
|
||||
const { allCustomTrainData } = useCustomTrainData();
|
||||
const { getTodayOperationByTrainId } = useOperationLogs();
|
||||
const [input, setInput] = useState(""); // 文字入力
|
||||
const [useStationName, setUseStationName] = useState(false);
|
||||
const [useRegex, setUseRegex] = useState(false);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React from "react";
|
||||
import dayjs from "dayjs";
|
||||
import { FlexWidget, OverlapWidget, SvgWidget, TextWidget } from "react-native-android-widget";
|
||||
import { AS } from "@/storageControl";
|
||||
import { STORAGE_KEYS } from "@/constants";
|
||||
import { AS } from "../../storageControl";
|
||||
import { STORAGE_KEYS } from "../../constants";
|
||||
import { WidgetColors, widgetLightColors } from "./widget-theme";
|
||||
|
||||
type LastFelicaSnapshot = {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { FlexWidget, TextWidget } from "react-native-android-widget";
|
||||
import { getDelayData } from "./TraInfoEXWidget";
|
||||
import { getInfoString } from "./InfoWidget";
|
||||
import { getFelicaQuickAccessData } from "./FelicaQuickAccessWidget";
|
||||
import { isAvailable as isNfcAvailable } from "@/modules/expo-felica-reader/src";
|
||||
import { isAvailable as isNfcAvailable } from "../../modules/expo-felica-reader/src";
|
||||
import { WidgetColors, widgetLightColors } from "./widget-theme";
|
||||
|
||||
export async function getShortcutData() {
|
||||
|
||||
@@ -8,8 +8,16 @@ import {
|
||||
import { ShortcutWidget, getShortcutData } from "./ShortcutWidget";
|
||||
import { StrangeTrainWidget, getStrangeTrainData } from "./StrangeTrainWidget";
|
||||
import { widgetLightColors, widgetDarkColors } from "./widget-theme";
|
||||
import { AS } from "@/storageControl";
|
||||
import { STORAGE_KEYS } from "@/constants";
|
||||
import { AS } from "../../storageControl";
|
||||
import { STORAGE_KEYS } from "../../constants";
|
||||
|
||||
export const nameToWidget = {
|
||||
JR_shikoku_train_info: TraInfoEXWidget,
|
||||
Info_Widget: InfoWidget,
|
||||
JR_shikoku_apps_shortcut: ShortcutWidget,
|
||||
JR_shikoku_felica_balance: FelicaQuickAccessWidget,
|
||||
JR_shikoku_train_strange: StrangeTrainWidget,
|
||||
};
|
||||
|
||||
type ColorThemePref = "light" | "system" | "dark";
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import { Animated, StyleSheet, View } from "react-native";
|
||||
import { LinearGradient } from "expo-linear-gradient";
|
||||
import type { RootTabParamList } from "@/types/navigation";
|
||||
import lineColorList from "@/assets/originData/lineColorList";
|
||||
import { stationIDPair } from "@/lib/getStationList";
|
||||
import { fixedColors } from "@/lib/theme/colors";
|
||||
import type { RootTabParamList } from "../../types/navigation";
|
||||
import lineColorList from "../../assets/originData/lineColorList";
|
||||
import { stationIDPair } from "../../lib/getStationList";
|
||||
import { fixedColors } from "../../lib/theme/colors";
|
||||
|
||||
export type TabBarBackgroundProps = {
|
||||
routeName: keyof RootTabParamList;
|
||||
|
||||
@@ -1,250 +0,0 @@
|
||||
import {
|
||||
Component,
|
||||
useContext,
|
||||
useState,
|
||||
useSyncExternalStore,
|
||||
type ErrorInfo,
|
||||
type ReactNode,
|
||||
} from "react";
|
||||
import { Platform, ScrollView, Text, TouchableOpacity, View } from "react-native";
|
||||
import * as Clipboard from "expo-clipboard";
|
||||
import * as Updates from "expo-updates";
|
||||
import Constants from "expo-constants";
|
||||
import {
|
||||
SafeAreaInsetsContext,
|
||||
initialWindowMetrics,
|
||||
} from "react-native-safe-area-context";
|
||||
import { captureTelemetryException } from "@/lib/observability/sentry";
|
||||
import { formatErrorReport } from "@/lib/errorReport";
|
||||
import {
|
||||
ErrorBoundaryTestError,
|
||||
disarmErrorBoundaryTest,
|
||||
getArmedErrorBoundaryTestScope,
|
||||
subscribeErrorBoundaryTest,
|
||||
} from "@/lib/errorBoundaryTest";
|
||||
|
||||
type Props = {
|
||||
/** 捕捉位置。Sentryのtagとエラー報告に出る("root"、タブ名など) */
|
||||
scope: string;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
type State = {
|
||||
error: unknown;
|
||||
hasError: boolean;
|
||||
componentStack: string | null;
|
||||
sentryEventId: string | null;
|
||||
occurredAt: Date | null;
|
||||
};
|
||||
|
||||
const initialState: State = {
|
||||
error: null,
|
||||
hasError: false,
|
||||
componentStack: null,
|
||||
sentryEventId: null,
|
||||
occurredAt: null,
|
||||
};
|
||||
|
||||
/**
|
||||
* 子の描画中に起きた例外を捕捉し、白画面の代わりにエラー画面を出す。
|
||||
*
|
||||
* イベントハンドラや非同期処理、ネイティブのクラッシュは捕捉できない。
|
||||
* エラー画面はテーマContextなど壊れた可能性のある仕組みに依存せず、固定色で描く。
|
||||
*/
|
||||
export class AppErrorBoundary extends Component<Props, State> {
|
||||
state: State = initialState;
|
||||
|
||||
static getDerivedStateFromError(error: unknown): Partial<State> {
|
||||
return { error, hasError: true, occurredAt: new Date() };
|
||||
}
|
||||
|
||||
componentDidCatch(error: unknown, info: ErrorInfo) {
|
||||
const componentStack = info.componentStack ?? null;
|
||||
const isTest = error instanceof ErrorBoundaryTestError;
|
||||
// 再表示で同じテストエラーが再発しないよう、捕捉した時点で解除する
|
||||
if (isTest) disarmErrorBoundaryTest();
|
||||
let sentryEventId: string | null = null;
|
||||
try {
|
||||
sentryEventId = captureTelemetryException(error, {
|
||||
tags: {
|
||||
error_boundary: this.props.scope,
|
||||
error_boundary_test: String(isTest),
|
||||
},
|
||||
contexts: { react: { componentStack } },
|
||||
});
|
||||
} catch {
|
||||
// Sentry送信の失敗でエラー画面の表示まで失敗させない
|
||||
}
|
||||
this.setState({ componentStack, sentryEventId });
|
||||
}
|
||||
|
||||
private reset = () => this.setState(initialState);
|
||||
|
||||
render() {
|
||||
if (!this.state.hasError) {
|
||||
return (
|
||||
<>
|
||||
<ErrorBoundaryTestTrigger scope={this.props.scope} />
|
||||
{this.props.children}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const report = formatErrorReport({
|
||||
scope: this.props.scope,
|
||||
error: this.state.error,
|
||||
componentStack: this.state.componentStack,
|
||||
sentryEventId: this.state.sentryEventId,
|
||||
occurredAt: this.state.occurredAt ?? new Date(),
|
||||
environment: {
|
||||
platform: Platform.OS,
|
||||
platformVersion: String(Platform.Version),
|
||||
appVersion: Constants.expoConfig?.version,
|
||||
runtimeVersion: Updates.runtimeVersion,
|
||||
updateId: Updates.updateId,
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<ErrorFallback
|
||||
isRoot={this.props.scope === "root"}
|
||||
report={report}
|
||||
onRetry={this.reset}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/** 研究ツールから指定された scope のときだけ、描画中に例外を投げる。 */
|
||||
const ErrorBoundaryTestTrigger = ({ scope }: { scope: string }) => {
|
||||
const armedScope = useSyncExternalStore(
|
||||
subscribeErrorBoundaryTest,
|
||||
getArmedErrorBoundaryTestScope,
|
||||
);
|
||||
if (armedScope === scope) throw new ErrorBoundaryTestError(scope);
|
||||
return null;
|
||||
};
|
||||
|
||||
const colors = {
|
||||
background: "#f5f5f5",
|
||||
text: "#222222",
|
||||
subText: "#555555",
|
||||
reportBackground: "#ffffff",
|
||||
border: "#dddddd",
|
||||
primary: "#0099cc",
|
||||
onPrimary: "#ffffff",
|
||||
};
|
||||
|
||||
const ErrorFallback = ({
|
||||
isRoot,
|
||||
report,
|
||||
onRetry,
|
||||
}: {
|
||||
isRoot: boolean;
|
||||
report: string;
|
||||
onRetry: () => void;
|
||||
}) => {
|
||||
const [copied, setCopied] = useState(false);
|
||||
// ルートはSafeAreaProviderの外側なので、起動時の値で代替する
|
||||
const insets =
|
||||
useContext(SafeAreaInsetsContext) ?? initialWindowMetrics?.insets;
|
||||
|
||||
const copyReport = () => {
|
||||
Clipboard.setStringAsync(report)
|
||||
.then(() => setCopied(true))
|
||||
.catch(() => setCopied(false));
|
||||
};
|
||||
|
||||
const reloadApp = () => {
|
||||
// 開発ビルドなどでreloadできない場合は、再表示で代替する
|
||||
Updates.reloadAsync().catch(onRetry);
|
||||
};
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
backgroundColor: colors.background,
|
||||
paddingTop: (insets?.top ?? 0) + 16,
|
||||
paddingHorizontal: 16,
|
||||
// タブ内はタブバーの上に描かれるので、下の余白はルートだけ確保する
|
||||
paddingBottom: (isRoot ? insets?.bottom ?? 0 : 0) + 16,
|
||||
}}
|
||||
>
|
||||
<Text style={{ color: colors.text, fontSize: 18, fontWeight: "bold" }}>
|
||||
{isRoot ? "アプリでエラーが発生しました" : "この画面でエラーが発生しました"}
|
||||
</Text>
|
||||
<Text style={{ color: colors.subText, fontSize: 14, marginTop: 8 }}>
|
||||
{isRoot
|
||||
? "再読み込みで復帰できる場合があります。"
|
||||
: "再表示で復帰できる場合があります。他のタブは引き続き使えます。"}
|
||||
{"\n"}
|
||||
不具合の報告には、下の「エラー内容をコピー」を使ってください。
|
||||
</Text>
|
||||
|
||||
<View style={{ flexDirection: "row", marginTop: 16, gap: 8 }}>
|
||||
<FallbackButton
|
||||
label={isRoot ? "再読み込み" : "再表示"}
|
||||
onPress={isRoot ? reloadApp : onRetry}
|
||||
primary
|
||||
/>
|
||||
<FallbackButton
|
||||
label={copied ? "コピーしました" : "エラー内容をコピー"}
|
||||
onPress={copyReport}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<ScrollView
|
||||
style={{
|
||||
flex: 1,
|
||||
marginTop: 16,
|
||||
backgroundColor: colors.reportBackground,
|
||||
borderColor: colors.border,
|
||||
borderWidth: 1,
|
||||
borderRadius: 6,
|
||||
}}
|
||||
contentContainerStyle={{ padding: 12 }}
|
||||
>
|
||||
<Text selectable style={{ color: colors.text, fontSize: 12 }}>
|
||||
{report}
|
||||
</Text>
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const FallbackButton = ({
|
||||
label,
|
||||
onPress,
|
||||
primary = false,
|
||||
}: {
|
||||
label: string;
|
||||
onPress: () => void;
|
||||
primary?: boolean;
|
||||
}) => (
|
||||
<TouchableOpacity
|
||||
accessibilityRole="button"
|
||||
onPress={onPress}
|
||||
style={{
|
||||
flex: 1,
|
||||
minHeight: 44,
|
||||
borderRadius: 6,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
paddingHorizontal: 12,
|
||||
backgroundColor: primary ? colors.primary : colors.reportBackground,
|
||||
borderColor: colors.primary,
|
||||
borderWidth: 1,
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
color: primary ? colors.onPrimary : colors.primary,
|
||||
fontSize: 14,
|
||||
fontWeight: "bold",
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
@@ -2,7 +2,7 @@ import React from "react";
|
||||
import { GestureHandlerRootView } from "react-native-gesture-handler";
|
||||
import { SafeAreaProvider } from "react-native-safe-area-context";
|
||||
import { SheetProvider } from "react-native-actions-sheet";
|
||||
import { AppContainer } from "@/AppContainer";
|
||||
import { AppContainer } from "../Apps";
|
||||
import { AllTrainDiagramProvider } from "../stateBox/useAllTrainDiagram";
|
||||
import { NotificationProvider } from "../stateBox/useNotifications";
|
||||
import { UserPositionProvider } from "../stateBox/useUserPosition";
|
||||
@@ -16,7 +16,6 @@ import { BusAndTrainDataProvider } from "../stateBox/useBusAndTrainData";
|
||||
import { DeviceOrientationChangeProvider } from "../stateBox/useDeviceOrientationChange";
|
||||
import { buildProvidersTree } from "../lib/providerTreeProvider";
|
||||
import { AppThemeProvider } from "../lib/theme";
|
||||
import { markStartupOnce } from "../lib/devStartupPerformance";
|
||||
import StatusbarDetect from "../StatusbarDetect";
|
||||
import "./ActionSheetComponents/sheets";
|
||||
|
||||
@@ -35,7 +34,6 @@ const ProviderTree = buildProvidersTree([
|
||||
]);
|
||||
|
||||
export function AppRootLayout(): React.ReactElement {
|
||||
markStartupOnce("root-layout-render", "AppRootLayout render");
|
||||
return (
|
||||
<AppThemeProvider>
|
||||
<DeviceOrientationChangeProvider>
|
||||
|
||||
@@ -26,7 +26,6 @@ import { MapsButton } from "./Apps/MapsButton";
|
||||
import { ReloadButton } from "./Apps/ReloadButton";
|
||||
import { useStationList } from "../stateBox/useStationList";
|
||||
import { positionsLifecycleRef } from "../lib/rootNavigation";
|
||||
import { usePositionsStartupReady } from "../lib/positionsStartupContext";
|
||||
import { FixedPositionBox } from "./Apps/FixedPositionBox";
|
||||
import { PlaybackTimeline } from "./Apps/PlaybackTimeline";
|
||||
import { RecordingStatusBar } from "./Apps/RecordingStatusBar";
|
||||
@@ -44,7 +43,6 @@ export default function Apps() {
|
||||
const { mapSwitch, selectedLine } = useTrainMenu();
|
||||
const isDark = useColorScheme() === "dark";
|
||||
const isFocused = useIsFocused();
|
||||
const { notifyInitialLoadReady } = usePositionsStartupReady();
|
||||
const [hasActivatedScreen, setHasActivatedScreen] = React.useState(false);
|
||||
const [hasActivatedWebView, setHasActivatedWebView] = React.useState(false);
|
||||
const [hasStableWebViewSession, setHasStableWebViewSession] =
|
||||
@@ -334,7 +332,6 @@ export default function Apps() {
|
||||
<AppsWebView
|
||||
openStationACFromEachTrainInfo={openStationACFromEachTrainInfo}
|
||||
onInitialLoadReady={() => {
|
||||
notifyInitialLoadReady();
|
||||
if (hasStableWebViewSession) return;
|
||||
positionsScreenTelemetry.info(
|
||||
"positions webview became stable",
|
||||
|
||||
@@ -57,7 +57,7 @@ import { STORAGE_KEYS } from "@/constants";
|
||||
|
||||
// ── 遅延速報サウンド設定 ──
|
||||
// 将来のファイル差し替えに備えて定数で管理
|
||||
const DELAY_ANNOUNCEMENT_SOUND = require("@/assets/sound/delay-support.wav");
|
||||
const DELAY_ANNOUNCEMENT_SOUND = require("../../../assets/sound/delay-support.wav");
|
||||
// 遅延案内の再生間隔: 120〜180 秒をランダムで選択(2〜3分ごと)
|
||||
const getAnnouncementIntervalMs = () =>
|
||||
(Math.floor(Math.random() * 60) + 120) * 1000;
|
||||
@@ -409,6 +409,7 @@ export const FixedStation: FC<props> = ({ stationID }) => {
|
||||
goTo: "menu",
|
||||
onExit: () => SheetManager.hide("StationDetailView"),
|
||||
};
|
||||
//@ts-ignore
|
||||
SheetManager.show("StationDetailView", { payload });
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
TextStyle,
|
||||
} from "react-native";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
import { useTrainMenu } from "@/stateBox/useTrainMenu";
|
||||
import { useTrainMenu } from "../../stateBox/useTrainMenu";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { useResponsive } from "@/lib/responsive";
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@ import { Ionicons } from "@expo/vector-icons";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
import * as Updates from "expo-updates";
|
||||
import Constants from "expo-constants";
|
||||
import { useCurrentTrainView } from "@/stateBox/useCurrentTrain";
|
||||
import { useTrainMenu } from "@/stateBox/useTrainMenu";
|
||||
import { useCurrentTrainView } from "../../stateBox/useCurrentTrain";
|
||||
import { useTrainMenu } from "../../stateBox/useTrainMenu";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { useResponsive } from "@/lib/responsive";
|
||||
|
||||
|
||||
@@ -9,9 +9,9 @@ import {
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import dayjs from "dayjs";
|
||||
import { useTrainRecording } from "@/stateBox/useTrainMenu";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
import { useResponsive } from "@/lib/responsive";
|
||||
import { useTrainRecording } from "../../stateBox/useTrainMenu";
|
||||
import { useThemeColors } from "../../lib/theme";
|
||||
import { useResponsive } from "../../lib/responsive";
|
||||
|
||||
/**
|
||||
* 再生中に走行位置WebViewの上部に表示するタイムラインコントローラー。
|
||||
|
||||
@@ -2,9 +2,9 @@ import React, { FC, useEffect, useRef, useState } from "react";
|
||||
import { View, Text, AppState, InteractionManager } from "react-native";
|
||||
import { activateKeepAwakeAsync, deactivateKeepAwake } from "expo-keep-awake";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { useTrainRecording } from "@/stateBox/useTrainMenu";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
import { useResponsive } from "@/lib/responsive";
|
||||
import { useTrainRecording } from "../../stateBox/useTrainMenu";
|
||||
import { useThemeColors } from "../../lib/theme";
|
||||
import { useResponsive } from "../../lib/responsive";
|
||||
|
||||
const KEEP_AWAKE_TAG = "recording-status-bar";
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
} from "react-native";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
import { useTrainMenu } from "@/stateBox/useTrainMenu";
|
||||
import { useTrainMenu } from "../../stateBox/useTrainMenu";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { useResponsive } from "@/lib/responsive";
|
||||
|
||||
|
||||
+12
-105
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useEffect, useMemo, useRef } from "react";
|
||||
import React, { useCallback, useEffect, useRef } from "react";
|
||||
import { Platform } from "react-native";
|
||||
import { WebView } from "react-native-webview";
|
||||
|
||||
@@ -8,44 +8,27 @@ import {
|
||||
lineListPair,
|
||||
stationIDPair,
|
||||
stationNamePair,
|
||||
} from "@/lib/getStationList";
|
||||
import { checkDuplicateTrainData } from "@/lib/checkDuplicateTrainData";
|
||||
import { useFavoriteStation } from "@/stateBox/useFavoriteStation";
|
||||
} from "../../lib/getStationList";
|
||||
import { checkDuplicateTrainData } from "../../lib/checkDuplicateTrainData";
|
||||
import { useFavoriteStation } from "../../stateBox/useFavoriteStation";
|
||||
import {
|
||||
useCurrentTrainData,
|
||||
useCurrentTrainView,
|
||||
} from "@/stateBox/useCurrentTrain";
|
||||
import { useDeviceOrientationChange } from "@/stateBox/useDeviceOrientationChange";
|
||||
} 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 {
|
||||
useCustomTrainData,
|
||||
useOperationLogs,
|
||||
useTrainDiagram,
|
||||
} from "@/stateBox/useAllTrainDiagram";
|
||||
import { useStationList } from "@/stateBox/useStationList";
|
||||
import { useTrainMenu } from "../../stateBox/useTrainMenu";
|
||||
import { useStationList } from "../../stateBox/useStationList";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
import { useWebViewRemount } from "@/lib/useWebViewRemount";
|
||||
import { generateMockUpdateScript } from "@/lib/mockApi/webviewXhrInterceptor";
|
||||
import { generateMockUpdateScript } from "../../lib/mockApi/webviewXhrInterceptor";
|
||||
import { telemetry } from "@/lib/observability/telemetry";
|
||||
import { setAppLifecycleWebViewActive } from "@/lib/observability/appLifecycleCrashSentinel";
|
||||
import {
|
||||
markStartup,
|
||||
markStartupOnce,
|
||||
markStartupState,
|
||||
} from "@/lib/devStartupPerformance";
|
||||
import { usePositionsStartupReady } from "@/lib/positionsStartupContext";
|
||||
import {
|
||||
generateNativeWebViewDataBootstrapScript,
|
||||
NATIVE_WEBVIEW_BOOTSTRAP_MAX_AGE_MS,
|
||||
type NativeWebViewDataBootstrap,
|
||||
} from "@/lib/webViewInjectjavascript";
|
||||
|
||||
const positionsWebViewTelemetry = telemetry.scope("positions.webview");
|
||||
export const AppsWebView = ({ openStationACFromEachTrainInfo, onInitialLoadReady }) => {
|
||||
markStartupOnce("positions-webview-render", "positions WebView render");
|
||||
const initialPositionsUrl = "https://train.jr-shikoku.co.jp/sp.html";
|
||||
const { currentTrain } = useCurrentTrainData();
|
||||
const { webview } = useCurrentTrainView();
|
||||
@@ -54,7 +37,6 @@ export const AppsWebView = ({ openStationACFromEachTrainInfo, onInitialLoadReady
|
||||
const { favoriteStation } = useFavoriteStation();
|
||||
const { isLandscape } = useDeviceOrientationChange();
|
||||
const { isDark } = useThemeColors();
|
||||
const { notifyWebViewLoadStarted } = usePositionsStartupReady();
|
||||
const bgColor = isDark ? "#1c1c1e" : "#ffffff";
|
||||
const { originalStationList, stationList, getInjectJavascriptAddress } =
|
||||
useStationList();
|
||||
@@ -67,58 +49,6 @@ export const AppsWebView = ({ openStationACFromEachTrainInfo, onInitialLoadReady
|
||||
mockApiFeatureEnabled,
|
||||
mockTrainPositions,
|
||||
} = useTrainMenu();
|
||||
const { allCustomTrainData, dataReadyAt: customTrainDataReadyAt } =
|
||||
useCustomTrainData();
|
||||
const { todayOperation, dataReadyAt: operationLogsReadyAt } =
|
||||
useOperationLogs();
|
||||
const { allTrainDiagram, dataReadyAt: trainDiagramReadyAt } =
|
||||
useTrainDiagram();
|
||||
|
||||
// Capture one native snapshot for this WebView instance. Keeping the large
|
||||
// train payload in a ref avoids serializing it again on each 30-second poll.
|
||||
const nativeBootstrapScriptRef = useRef<string | null>(null);
|
||||
if (nativeBootstrapScriptRef.current === null) {
|
||||
const now = Date.now();
|
||||
const isFresh = (readyAt: number | null): readyAt is number =>
|
||||
typeof readyAt === "number" &&
|
||||
now >= readyAt &&
|
||||
now - readyAt <= NATIVE_WEBVIEW_BOOTSTRAP_MAX_AGE_MS;
|
||||
const nativeBootstrap: NativeWebViewDataBootstrap = { version: 1 };
|
||||
let hasFreshData = false;
|
||||
|
||||
if (isFresh(customTrainDataReadyAt)) {
|
||||
nativeBootstrap.trainDataList = {
|
||||
data: allCustomTrainData,
|
||||
updatedAt: customTrainDataReadyAt,
|
||||
};
|
||||
hasFreshData = true;
|
||||
}
|
||||
if (isFresh(operationLogsReadyAt)) {
|
||||
nativeBootstrap.operationList = {
|
||||
data: todayOperation,
|
||||
updatedAt: operationLogsReadyAt,
|
||||
};
|
||||
hasFreshData = true;
|
||||
}
|
||||
if (isFresh(trainDiagramReadyAt)) {
|
||||
nativeBootstrap.trainDiagramData2 = {
|
||||
data: allTrainDiagram,
|
||||
updatedAt: trainDiagramReadyAt,
|
||||
};
|
||||
hasFreshData = true;
|
||||
}
|
||||
|
||||
nativeBootstrapScriptRef.current = generateNativeWebViewDataBootstrapScript(
|
||||
hasFreshData ? nativeBootstrap : null,
|
||||
);
|
||||
}
|
||||
const webViewBeforeContentLoaded = useMemo(
|
||||
() =>
|
||||
[nativeBootstrapScriptRef.current, injectJavascriptBeforeContentLoaded]
|
||||
.filter(Boolean)
|
||||
.join("\n"),
|
||||
[injectJavascriptBeforeContentLoaded],
|
||||
);
|
||||
const addWebViewBreadcrumb = (
|
||||
message: string,
|
||||
data?: Record<string, string | number | boolean | null | undefined>
|
||||
@@ -187,7 +117,6 @@ export const AppsWebView = ({ openStationACFromEachTrainInfo, onInitialLoadReady
|
||||
const lastRemountKeyRef = useRef<typeof remountKey | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
markStartupOnce("positions-webview-mounted", "positions WebView mounted");
|
||||
setAppLifecycleWebViewActive("positions_main", true);
|
||||
return () => setAppLifecycleWebViewActive("positions_main", false);
|
||||
}, []);
|
||||
@@ -216,9 +145,6 @@ export const AppsWebView = ({ openStationACFromEachTrainInfo, onInitialLoadReady
|
||||
}, [mockApiFeatureEnabled, remount]);
|
||||
|
||||
useEffect(() => {
|
||||
markStartupState("positions-webview-focus", "positions WebView focus state", {
|
||||
focused: isFocused,
|
||||
});
|
||||
focusedRef.current = isFocused;
|
||||
addWebViewBreadcrumb(isFocused ? "webview focused" : "webview blurred", {
|
||||
landscape: isLandscape,
|
||||
@@ -299,8 +225,9 @@ export const AppsWebView = ({ openStationACFromEachTrainInfo, onInitialLoadReady
|
||||
addWebViewBreadcrumb("webview navigation", { url });
|
||||
switch (true) {
|
||||
case url.includes("https://train.jr-shikoku.co.jp/usage.htm"):
|
||||
if (Platform.OS === "android") navigate("generalWebView", { uri: url });
|
||||
if (Platform.OS === "android") navigate("howto", { info: url });
|
||||
webview?.current.goBack();
|
||||
//Actions.howto();
|
||||
break;
|
||||
case url.includes("https://train.jr-shikoku.co.jp/train.html"):
|
||||
//Actions.trainbase({info: url});
|
||||
@@ -405,10 +332,6 @@ export const AppsWebView = ({ openStationACFromEachTrainInfo, onInitialLoadReady
|
||||
};
|
||||
|
||||
const onLoadStart = () => {
|
||||
notifyWebViewLoadStarted();
|
||||
markStartup("positions WebView loadStart", {
|
||||
focused: focusedRef.current,
|
||||
});
|
||||
pingHandlers.onLoadStart?.();
|
||||
addWebViewBreadcrumb("webview loadStart", {
|
||||
focused: focusedRef.current,
|
||||
@@ -417,9 +340,6 @@ export const AppsWebView = ({ openStationACFromEachTrainInfo, onInitialLoadReady
|
||||
};
|
||||
|
||||
const onLoadEnd = () => {
|
||||
markStartup("positions WebView loadEnd", {
|
||||
focused: focusedRef.current,
|
||||
});
|
||||
pingHandlers.onLoadEnd?.();
|
||||
addWebViewBreadcrumb("webview loadEnd", {
|
||||
focused: focusedRef.current,
|
||||
@@ -429,11 +349,6 @@ export const AppsWebView = ({ openStationACFromEachTrainInfo, onInitialLoadReady
|
||||
});
|
||||
if (!initialLoadReadyNotifiedRef.current) {
|
||||
initialLoadReadyNotifiedRef.current = true;
|
||||
markStartupOnce(
|
||||
"positions-webview-initial-ready",
|
||||
"positions WebView initial load ready",
|
||||
{ focused: focusedRef.current },
|
||||
);
|
||||
addWebViewBreadcrumb("webview initial load ready", {
|
||||
focused: focusedRef.current,
|
||||
});
|
||||
@@ -453,10 +368,6 @@ export const AppsWebView = ({ openStationACFromEachTrainInfo, onInitialLoadReady
|
||||
addWebViewBreadcrumb("webview initial inject scheduled", {
|
||||
focused: focusedRef.current,
|
||||
});
|
||||
markStartup("positions WebView initial inject scheduled", {
|
||||
focused: focusedRef.current,
|
||||
favoriteCount: favoriteStation.length,
|
||||
});
|
||||
loadEndTimeoutRef.current = setTimeout(() => {
|
||||
addWebViewBreadcrumb(
|
||||
focusedRef.current
|
||||
@@ -466,10 +377,6 @@ export const AppsWebView = ({ openStationACFromEachTrainInfo, onInitialLoadReady
|
||||
focused: focusedRef.current,
|
||||
}
|
||||
);
|
||||
markStartup("positions WebView initial inject run", {
|
||||
focused: focusedRef.current,
|
||||
favoriteCount: favoriteStation.length,
|
||||
});
|
||||
webview?.current?.injectJavaScript(string);
|
||||
pendingInitialInjectRef.current = null;
|
||||
initialInjectDoneRef.current = true;
|
||||
@@ -519,7 +426,7 @@ export const AppsWebView = ({ openStationACFromEachTrainInfo, onInitialLoadReady
|
||||
onRenderProcessGone={handleRenderProcessGone}
|
||||
onContentProcessDidTerminate={handleContentProcessDidTerminate}
|
||||
{...{ onMessage, onNavigationStateChange, onLoadEnd }}
|
||||
injectedJavaScriptBeforeContentLoaded={webViewBeforeContentLoaded}
|
||||
injectedJavaScriptBeforeContentLoaded={injectJavascriptBeforeContentLoaded}
|
||||
injectedJavaScript={injectJavascript}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -156,7 +156,6 @@ export const DynamicHeaderScrollView: React.FC<DynamicHeaderScrollViewProps> = (
|
||||
<View {...containerProps}>
|
||||
<View style={{ zIndex: 1 }}>
|
||||
<Animated.View
|
||||
pointerEvents={headerVisible ? "auto" : "none"}
|
||||
style={[
|
||||
styles.header,
|
||||
headerVisible ? shotHeaderStyle.on : shotHeaderStyle.off,
|
||||
@@ -165,7 +164,6 @@ export const DynamicHeaderScrollView: React.FC<DynamicHeaderScrollViewProps> = (
|
||||
{viewHeader ? <></> : shortHeader}
|
||||
</Animated.View>
|
||||
<Animated.View
|
||||
pointerEvents={headerVisible ? "none" : "auto"}
|
||||
style={[
|
||||
styles.header,
|
||||
headerVisible ? longHeaderStyle.off : longHeaderStyle.on,
|
||||
@@ -177,27 +175,27 @@ export const DynamicHeaderScrollView: React.FC<DynamicHeaderScrollViewProps> = (
|
||||
{topStickyContent}
|
||||
</Animated.View>
|
||||
</View>
|
||||
<ScrollView
|
||||
ref={scrollRef}
|
||||
nestedScrollEnabled
|
||||
bounces={false}
|
||||
style={{ zIndex: 0 }}
|
||||
stickyHeaderIndices={[1]}
|
||||
scrollEventThrottle={1}
|
||||
onScroll={onScroll}
|
||||
>
|
||||
<View style={{ height: Scroll_Distance, flexDirection: "column" }} />
|
||||
{topStickyContent && (
|
||||
<View
|
||||
style={{
|
||||
paddingTop: Min_Header_Height + verticalScale(40),
|
||||
flexDirection: "column",
|
||||
}}
|
||||
//index={1}
|
||||
/>
|
||||
)}
|
||||
{children}
|
||||
</ScrollView>
|
||||
<ScrollView
|
||||
ref={scrollRef}
|
||||
nestedScrollEnabled
|
||||
bounces={false}
|
||||
style={{ zIndex: 0 }}
|
||||
stickyHeaderIndices={[1]}
|
||||
scrollEventThrottle={1}
|
||||
onScroll={onScroll}
|
||||
>
|
||||
<View style={{ height: Scroll_Distance, flexDirection: "column" }} />
|
||||
{topStickyContent && (
|
||||
<View
|
||||
style={{
|
||||
paddingTop: Min_Header_Height + verticalScale(40),
|
||||
flexDirection: "column",
|
||||
}}
|
||||
//index={1}
|
||||
/>
|
||||
)}
|
||||
{children}
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -34,7 +34,6 @@ import type { NavigateFunction } from "@/types";
|
||||
import { telemetry } from "@/lib/observability/telemetry";
|
||||
import { FavoriteEmptyState } from "./FavoriteEmptyState";
|
||||
import { StationListLoadingState } from "./StationListLoadingState";
|
||||
import { markStartupOnce } from "@/lib/devStartupPerformance";
|
||||
|
||||
const menuCarouselTelemetry = telemetry.scope("menu.carousel");
|
||||
|
||||
@@ -79,11 +78,6 @@ export const CarouselBox = ({
|
||||
isPositionLoading: boolean;
|
||||
}) => {
|
||||
const isAndroid = Platform.OS === "android";
|
||||
useEffect(() => {
|
||||
markStartupOnce("menu-carousel-mounted", "menu carousel mounted", {
|
||||
stationGroupCount: listUpStation.length,
|
||||
});
|
||||
}, [listUpStation.length]);
|
||||
const sortControlsEntering = isAndroid ? undefined : FadeIn.duration(200);
|
||||
const sortControlsExiting = isAndroid ? undefined : FadeOut.duration(150);
|
||||
const carouselRef = useRef<ICarouselInstance>(null);
|
||||
@@ -259,9 +253,11 @@ export const CarouselBox = ({
|
||||
currentStation: listUpStation[listIndex],
|
||||
navigate,
|
||||
goTo: "menu",
|
||||
//@ts-ignore
|
||||
useShow: () => SheetManager.show("StationDetailView", { payload }),
|
||||
onExit: () => SheetManager.hide("StationDetailView"),
|
||||
};
|
||||
//@ts-ignore
|
||||
SheetManager.show("StationDetailView", { payload });
|
||||
};
|
||||
|
||||
|
||||
@@ -310,8 +310,9 @@ export const FixedContentBottom = (props) => {
|
||||
backgroundColor="linear-gradient(120deg, rgba(247,135,54,0.208) 0%, rgba(54,125,247,0.208) 100%)"
|
||||
flex={1}
|
||||
onPressButton={() =>
|
||||
props.navigate("generalWebView", {
|
||||
uri: "https://xprocess.haruk.in/tags/JR%E5%9B%9B%E5%9B%BD%E5%88%97%E8%BB%8A%E4%BD%8D%E7%BD%AE%E6%83%85%E5%A0%B1%E3%82%A2%E3%83%97%E3%83%AA/",
|
||||
props.navigate("howto", {
|
||||
info: "https://xprocess.haruk.in/tags/JR%E5%9B%9B%E5%9B%BD%E5%88%97%E8%BB%8A%E4%BD%8D%E7%BD%AE%E6%83%85%E5%A0%B1%E3%82%A2%E3%83%97%E3%83%AA/",
|
||||
goTo: "menu",
|
||||
})
|
||||
}
|
||||
>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect } from "react";
|
||||
import React from "react";
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
@@ -19,11 +19,6 @@ export const JRSTraInfoBox = () => {
|
||||
useTrainDelayData();
|
||||
const { colors, fixed } = useThemeColors();
|
||||
const { fontScale, moderateScale, verticalScale } = useResponsive();
|
||||
|
||||
useEffect(() => {
|
||||
void refreshDelayData();
|
||||
}, [refreshDelayData]);
|
||||
|
||||
const styles: { [key: string]: StyleProp<ViewStyle> } = {
|
||||
touch: {
|
||||
backgroundColor: fixed.primary,
|
||||
|
||||
@@ -6,7 +6,6 @@ import { useThemeColors } from "@/lib/theme";
|
||||
import type { StationProps } from "@/lib/CommonTypes";
|
||||
import { LED_vision } from "@/components/発車時刻表/LED_vidion";
|
||||
import { LEDFrame } from "@/components/発車時刻表/LED_Vision_Component/LEDFrame";
|
||||
import { markStartupOnce } from "@/lib/devStartupPerformance";
|
||||
|
||||
type MenuLEDProps = {
|
||||
station: StationProps[];
|
||||
@@ -40,10 +39,6 @@ const MenuLEDComponent = ({ station }: MenuLEDProps) => {
|
||||
const activeStation =
|
||||
preparedLED.key === stationKey ? preparedLED.station : null;
|
||||
|
||||
useEffect(() => {
|
||||
markStartupOnce("menu-led-mounted", "menu LED mounted");
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (preparedLED.key === stationKey) return;
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ export const NewsReleaseInfoBox: FC<navigateProps> = ({ navigate }) => {
|
||||
};
|
||||
|
||||
const onPressItem = (d: newsDataType) => {
|
||||
navigate("generalWebView", { uri: getPDFViewURL(d.url) });
|
||||
navigate("howto", { info: getPDFViewURL(d.url), goTo: "menu" });
|
||||
SheetManager.hide("NewsReleaseInfo");
|
||||
};
|
||||
|
||||
|
||||
@@ -29,8 +29,9 @@ export const SpecialTrainInfoBox: FC<props> = ({ navigate }) => {
|
||||
}, []);
|
||||
|
||||
const onPressItem: (d: specialDataType) => void = (d) => {
|
||||
navigate("generalWebView", {
|
||||
uri: getPDFViewURL("https://www.jr-shikoku.co.jp" + d.address),
|
||||
navigate("howto", {
|
||||
info: getPDFViewURL("https://www.jr-shikoku.co.jp" + d.address),
|
||||
goTo: "menu",
|
||||
});
|
||||
SheetManager.hide("SpecialTrainInfo");
|
||||
};
|
||||
|
||||
@@ -24,7 +24,7 @@ export const TitleBar = () => {
|
||||
<TouchableOpacity
|
||||
onPress={() => Linking.openURL("https://www.jr-shikoku.co.jp")}
|
||||
>
|
||||
<Image source={require("@/assets/Header.png")} style={{ width: width, resizeMode: "contain", backgroundColor: colors.background, height: verticalScale(80) }} />
|
||||
<Image source={require("../../assets/Header.png")} style={{ width: width, resizeMode: "contain", backgroundColor: colors.background, height: verticalScale(80) }} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -1,519 +0,0 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
ActivityIndicator,
|
||||
RefreshControl,
|
||||
ScrollView,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from "react-native";
|
||||
import { useFocusEffect, useNavigation } from "@react-navigation/native";
|
||||
import { MaterialCommunityIcons } from "@expo/vector-icons";
|
||||
import dayjs from "dayjs";
|
||||
import { SheetHeaderItem } from "@/components/atom/SheetHeaderItem";
|
||||
import { AS } from "@/storageControl";
|
||||
import { STORAGE_KEYS } from "@/constants";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
import { connectionStatusClient } from "@/lib/connectionStatusClient";
|
||||
import {
|
||||
getInitialServiceHealthResults,
|
||||
isDeviceOffline,
|
||||
runConnectionStatusChecks,
|
||||
type ConnectionStatusFeatureFlags,
|
||||
type ServiceHealthResult,
|
||||
type ServiceHealthStatus,
|
||||
} from "@/lib/connectionStatus";
|
||||
import {
|
||||
DEFAULT_JR_DATA_SYSTEM_ENV,
|
||||
normalizeJrDataSystemEnvironment,
|
||||
} from "@/lib/jrDataSystemEnvironment";
|
||||
|
||||
const STATUS_TEXT: Record<ServiceHealthStatus, { label: string }> = {
|
||||
checking: { label: "確認中" },
|
||||
ok: { label: "正常" },
|
||||
degraded: { label: "更新遅延" },
|
||||
error: { label: "接続できません" },
|
||||
unknown: { label: "判定不能" },
|
||||
};
|
||||
|
||||
const formatTime = (value: string | null | undefined): string => {
|
||||
if (!value || !Number.isFinite(Date.parse(value))) return "—";
|
||||
return dayjs(value).format("HH:mm:ss");
|
||||
};
|
||||
|
||||
const formatAge = (value: number | null | undefined): string => {
|
||||
if (value === null || value === undefined || !Number.isFinite(value)) return "—";
|
||||
if (value < 60) return Math.max(0, Math.floor(value)) + "秒";
|
||||
return Math.floor(value / 60) + "分" + Math.floor(value % 60) + "秒";
|
||||
};
|
||||
|
||||
const readStoredBoolean = async (key: string): Promise<boolean> => {
|
||||
try {
|
||||
const value = await AS.getItem(key);
|
||||
return value === true || value === "true";
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const loadConnectionStatusFeatureFlags = async (): Promise<ConnectionStatusFeatureFlags> => {
|
||||
const [voicepeakEnabled, unyohubEnabled, elesiteEnabled] = await Promise.all([
|
||||
readStoredBoolean(STORAGE_KEYS.VOICEPEAK_ENABLED),
|
||||
readStoredBoolean(STORAGE_KEYS.USE_UNYOHUB),
|
||||
readStoredBoolean(STORAGE_KEYS.USE_ELESITE),
|
||||
]);
|
||||
return { voicepeakEnabled, unyohubEnabled, elesiteEnabled };
|
||||
};
|
||||
|
||||
const getStatusColor = (
|
||||
status: ServiceHealthStatus,
|
||||
colors: ReturnType<typeof useThemeColors>["colors"],
|
||||
fixed: ReturnType<typeof useThemeColors>["fixed"],
|
||||
) => {
|
||||
switch (status) {
|
||||
case "ok":
|
||||
return fixed.buttonGreen;
|
||||
case "degraded":
|
||||
return colors.textCaution;
|
||||
case "error":
|
||||
return colors.textError;
|
||||
case "unknown":
|
||||
return colors.textTertiary;
|
||||
case "checking":
|
||||
default:
|
||||
return colors.textAccent;
|
||||
}
|
||||
};
|
||||
|
||||
export const ConnectionStatusSettings = () => {
|
||||
const navigation = useNavigation();
|
||||
const { colors, fixed } = useThemeColors();
|
||||
const [services, setServices] = useState<ServiceHealthResult[]>(() =>
|
||||
getInitialServiceHealthResults(DEFAULT_JR_DATA_SYSTEM_ENV),
|
||||
);
|
||||
const [isChecking, setIsChecking] = useState(false);
|
||||
const [lastCheckedAt, setLastCheckedAt] = useState<string | null>(null);
|
||||
const [isOffline, setIsOffline] = useState(false);
|
||||
const [expandedServiceId, setExpandedServiceId] = useState<string | null>(null);
|
||||
const isMountedRef = useRef(true);
|
||||
const isCheckingRef = useRef(false);
|
||||
const runIdRef = useRef(0);
|
||||
const abortControllerRef = useRef<AbortController | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
isMountedRef.current = false;
|
||||
abortControllerRef.current?.abort();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const runChecks = useCallback(async () => {
|
||||
if (isCheckingRef.current) return;
|
||||
isCheckingRef.current = true;
|
||||
const runId = ++runIdRef.current;
|
||||
const controller = new AbortController();
|
||||
abortControllerRef.current = controller;
|
||||
const offline = isDeviceOffline();
|
||||
|
||||
if (isMountedRef.current) {
|
||||
setIsChecking(true);
|
||||
setIsOffline(offline);
|
||||
setLastCheckedAt(null);
|
||||
setExpandedServiceId(null);
|
||||
setServices(getInitialServiceHealthResults(DEFAULT_JR_DATA_SYSTEM_ENV));
|
||||
}
|
||||
|
||||
try {
|
||||
let nextEnvironment = DEFAULT_JR_DATA_SYSTEM_ENV;
|
||||
try {
|
||||
nextEnvironment = normalizeJrDataSystemEnvironment(
|
||||
await AS.getItem(STORAGE_KEYS.JR_DATA_SYSTEM_ENV),
|
||||
);
|
||||
} catch {
|
||||
// The default release environment is the same fallback used elsewhere.
|
||||
}
|
||||
const featureFlags = await loadConnectionStatusFeatureFlags();
|
||||
if (!isMountedRef.current || runId !== runIdRef.current) return;
|
||||
setServices(getInitialServiceHealthResults(nextEnvironment, featureFlags));
|
||||
|
||||
await runConnectionStatusChecks({
|
||||
client: connectionStatusClient,
|
||||
environment: nextEnvironment,
|
||||
featureFlags,
|
||||
signal: controller.signal,
|
||||
isOffline: () => offline,
|
||||
onServiceResult: (result) => {
|
||||
if (!isMountedRef.current || runId !== runIdRef.current) return;
|
||||
setServices((current) =>
|
||||
current.map((service) => (service.id === result.id ? result : service)),
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
if (
|
||||
isMountedRef.current &&
|
||||
runId === runIdRef.current &&
|
||||
!controller.signal.aborted
|
||||
) {
|
||||
setLastCheckedAt(new Date().toISOString());
|
||||
}
|
||||
} finally {
|
||||
if (runId === runIdRef.current) {
|
||||
isCheckingRef.current = false;
|
||||
abortControllerRef.current = null;
|
||||
if (isMountedRef.current) setIsChecking(false);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
void runChecks();
|
||||
}, [runChecks]),
|
||||
);
|
||||
|
||||
const hasBlockingProblem = services.some(
|
||||
(service) =>
|
||||
service.enabled !== false &&
|
||||
service.checks.some(
|
||||
(check) =>
|
||||
check.role !== "optional" &&
|
||||
["degraded", "error", "unknown"].includes(check.status),
|
||||
),
|
||||
);
|
||||
const hasOptionalProblem = services.some(
|
||||
(service) =>
|
||||
service.enabled !== false &&
|
||||
service.checks.some(
|
||||
(check) =>
|
||||
check.role === "optional" &&
|
||||
["degraded", "error", "unknown"].includes(check.status),
|
||||
),
|
||||
);
|
||||
const hasProblem = hasBlockingProblem || hasOptionalProblem;
|
||||
const hasChecking = services.some(
|
||||
(service) => service.enabled !== false && service.status === "checking",
|
||||
);
|
||||
const summary = hasChecking
|
||||
? "サービスを確認しています"
|
||||
: hasBlockingProblem
|
||||
? "一部のサービスで問題が発生しています"
|
||||
: hasOptionalProblem
|
||||
? "一部の任意サービスを確認できません"
|
||||
: "すべてのサービスは正常です";
|
||||
|
||||
return (
|
||||
<View style={{ flex: 1, backgroundColor: fixed.primary }}>
|
||||
<SheetHeaderItem
|
||||
title="接続ステータス"
|
||||
LeftItem={{ title: "< 設定", onPress: () => navigation.goBack() }}
|
||||
/>
|
||||
<ScrollView
|
||||
style={{ flex: 1, backgroundColor: colors.backgroundSecondary }}
|
||||
contentContainerStyle={{ paddingBottom: 24 }}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={isChecking}
|
||||
onRefresh={() => void runChecks()}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
margin: 12,
|
||||
padding: 16,
|
||||
borderRadius: 10,
|
||||
backgroundColor: colors.surface,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.borderCard,
|
||||
}}
|
||||
>
|
||||
<View style={{ flexDirection: "row", alignItems: "center" }}>
|
||||
<MaterialCommunityIcons
|
||||
name={
|
||||
hasChecking && isChecking
|
||||
? "loading"
|
||||
: hasProblem
|
||||
? "alert-circle-outline"
|
||||
: "check-circle-outline"
|
||||
}
|
||||
size={28}
|
||||
color={
|
||||
hasChecking && isChecking
|
||||
? colors.textAccent
|
||||
: hasProblem
|
||||
? colors.textCaution
|
||||
: fixed.buttonGreen
|
||||
}
|
||||
/>
|
||||
<Text
|
||||
style={{
|
||||
flex: 1,
|
||||
marginLeft: 10,
|
||||
color: colors.text,
|
||||
fontSize: 17,
|
||||
fontWeight: "700",
|
||||
}}
|
||||
>
|
||||
{summary}
|
||||
</Text>
|
||||
</View>
|
||||
{isOffline ? (
|
||||
<View
|
||||
style={{
|
||||
marginTop: 12,
|
||||
padding: 10,
|
||||
borderRadius: 6,
|
||||
backgroundColor: colors.backgroundTertiary,
|
||||
}}
|
||||
>
|
||||
<Text style={{ color: colors.textError, fontWeight: "600" }}>
|
||||
インターネットに接続されていません
|
||||
</Text>
|
||||
</View>
|
||||
) : null}
|
||||
<Text style={{ marginTop: 12, color: colors.textSecondary, fontSize: 13 }}>
|
||||
最終確認: {lastCheckedAt ? formatTime(lastCheckedAt) : "—"}
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="接続ステータスを再確認"
|
||||
disabled={isChecking}
|
||||
onPress={() => void runChecks()}
|
||||
style={{
|
||||
marginTop: 12,
|
||||
minHeight: 42,
|
||||
borderRadius: 7,
|
||||
backgroundColor: isChecking
|
||||
? colors.backgroundTertiary
|
||||
: fixed.primary,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
paddingHorizontal: 14,
|
||||
}}
|
||||
>
|
||||
{isChecking ? (
|
||||
<ActivityIndicator color={fixed.textOnPrimary} size="small" />
|
||||
) : (
|
||||
<MaterialCommunityIcons
|
||||
name="refresh"
|
||||
size={18}
|
||||
color={fixed.textOnPrimary}
|
||||
/>
|
||||
)}
|
||||
<Text
|
||||
style={{
|
||||
marginLeft: 6,
|
||||
color: isChecking ? colors.textTertiary : fixed.textOnPrimary,
|
||||
fontWeight: "700",
|
||||
}}
|
||||
>
|
||||
{isChecking ? "確認中…" : "再確認"}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<Text
|
||||
style={{
|
||||
marginHorizontal: 16,
|
||||
marginTop: 4,
|
||||
marginBottom: 6,
|
||||
color: colors.textTertiary,
|
||||
fontSize: 13,
|
||||
}}
|
||||
>
|
||||
アプリが利用する公開データを、画面を開いた時と再確認時だけ確認します。
|
||||
</Text>
|
||||
|
||||
<View
|
||||
style={{
|
||||
marginHorizontal: 12,
|
||||
borderRadius: 10,
|
||||
overflow: "hidden",
|
||||
backgroundColor: colors.surface,
|
||||
}}
|
||||
>
|
||||
{services.map((service) => (
|
||||
<ServiceStatusRow
|
||||
key={service.id}
|
||||
service={service}
|
||||
expanded={expandedServiceId === service.id}
|
||||
onPress={() =>
|
||||
setExpandedServiceId((current) =>
|
||||
current === service.id ? null : service.id,
|
||||
)
|
||||
}
|
||||
colors={colors}
|
||||
fixed={fixed}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
<Text
|
||||
style={{
|
||||
marginHorizontal: 16,
|
||||
marginTop: 12,
|
||||
color: colors.textTertiary,
|
||||
fontSize: 12,
|
||||
lineHeight: 18,
|
||||
}}
|
||||
>
|
||||
判定不能は、接続先の応答形式を確認できない状態です。列車の遅延や運休そのものは、API障害として判定しません。
|
||||
</Text>
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const ServiceStatusRow = ({
|
||||
service,
|
||||
expanded,
|
||||
onPress,
|
||||
colors,
|
||||
fixed,
|
||||
}: {
|
||||
service: ServiceHealthResult;
|
||||
expanded: boolean;
|
||||
onPress: () => void;
|
||||
colors: ReturnType<typeof useThemeColors>["colors"];
|
||||
fixed: ReturnType<typeof useThemeColors>["fixed"];
|
||||
}) => {
|
||||
const status = STATUS_TEXT[service.status];
|
||||
const isDisabled = service.enabled === false;
|
||||
const displayStatus = isDisabled ? { label: "未使用" } : status;
|
||||
const statusColor = isDisabled
|
||||
? colors.textTertiary
|
||||
: getStatusColor(service.status, colors, fixed);
|
||||
const latestSuccess = service.checks.find((check) => check.lastSuccessAt);
|
||||
const latestAge = service.checks.reduce<number | null>((worst, check) => {
|
||||
if (check.ageSeconds === null || check.ageSeconds === undefined) return worst;
|
||||
return worst === null ? check.ageSeconds : Math.max(worst, check.ageSeconds);
|
||||
}, null);
|
||||
const maxLatency = service.checks.reduce<number | null>((max, check) => {
|
||||
if (check.latencyMs === undefined) return max;
|
||||
return max === null ? check.latencyMs : Math.max(max, check.latencyMs);
|
||||
}, null);
|
||||
const detailMessage =
|
||||
service.message ??
|
||||
service.checks.find(
|
||||
(check) => check.message && check.status !== "ok",
|
||||
)?.message ??
|
||||
"正常";
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={service.label + ": " + displayStatus.label}
|
||||
onPress={onPress}
|
||||
style={{
|
||||
paddingHorizontal: 14,
|
||||
paddingVertical: 13,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.borderSecondary,
|
||||
}}
|
||||
>
|
||||
<View style={{ flexDirection: "row", alignItems: "center" }}>
|
||||
<MaterialCommunityIcons
|
||||
name={service.icon as any}
|
||||
size={22}
|
||||
color={colors.iconSecondary}
|
||||
/>
|
||||
<Text
|
||||
style={{
|
||||
flex: 1,
|
||||
marginLeft: 10,
|
||||
color: colors.text,
|
||||
fontSize: 16,
|
||||
fontWeight: "600",
|
||||
}}
|
||||
>
|
||||
{service.label}
|
||||
</Text>
|
||||
<View style={{ flexDirection: "row", alignItems: "center" }}>
|
||||
{!isDisabled && service.status === "checking" ? (
|
||||
<ActivityIndicator
|
||||
size="small"
|
||||
color={statusColor}
|
||||
style={{ marginRight: 6 }}
|
||||
/>
|
||||
) : (
|
||||
<View
|
||||
style={{
|
||||
width: 9,
|
||||
height: 9,
|
||||
borderRadius: 5,
|
||||
backgroundColor: statusColor,
|
||||
marginRight: 6,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Text style={{ color: statusColor, fontWeight: "700" }}>
|
||||
{displayStatus.label}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
{expanded ? (
|
||||
<View
|
||||
style={{
|
||||
marginTop: 12,
|
||||
marginLeft: 32,
|
||||
paddingTop: 10,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: colors.borderSecondary,
|
||||
}}
|
||||
>
|
||||
<DetailLine
|
||||
label="状態"
|
||||
value={displayStatus.label}
|
||||
valueColor={statusColor}
|
||||
colors={colors}
|
||||
/>
|
||||
<DetailLine
|
||||
label="最終データ更新"
|
||||
value={formatTime(latestSuccess?.lastSuccessAt)}
|
||||
colors={colors}
|
||||
/>
|
||||
<DetailLine
|
||||
label="データ経過時間"
|
||||
value={formatAge(latestAge)}
|
||||
colors={colors}
|
||||
/>
|
||||
<DetailLine
|
||||
label="API応答時間"
|
||||
value={maxLatency === null ? "—" : maxLatency + " ms"}
|
||||
colors={colors}
|
||||
/>
|
||||
<DetailLine
|
||||
label="確認時刻"
|
||||
value={formatTime(service.checks[0]?.checkedAt)}
|
||||
colors={colors}
|
||||
/>
|
||||
<Text style={{ marginTop: 6, color: colors.textSecondary, fontSize: 12 }}>
|
||||
{detailMessage}
|
||||
</Text>
|
||||
</View>
|
||||
) : null}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
const DetailLine = ({
|
||||
label,
|
||||
value,
|
||||
valueColor,
|
||||
colors,
|
||||
}: {
|
||||
label: string;
|
||||
value: string;
|
||||
valueColor?: string;
|
||||
colors: ReturnType<typeof useThemeColors>["colors"];
|
||||
}) => (
|
||||
<View style={{ flexDirection: "row", marginBottom: 5 }}>
|
||||
<Text style={{ width: 125, color: colors.textTertiary, fontSize: 12 }}>
|
||||
{label}
|
||||
</Text>
|
||||
<Text style={{ flex: 1, color: valueColor ?? colors.textSecondary, fontSize: 12 }}>
|
||||
{value}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
@@ -12,7 +12,7 @@ import { Switch } from "@rneui/themed";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import { MaterialCommunityIcons } from "@expo/vector-icons";
|
||||
import { SheetHeaderItem } from "@/components/atom/SheetHeaderItem";
|
||||
import { AS } from "@/storageControl";
|
||||
import { AS } from "../../storageControl";
|
||||
import { STORAGE_KEYS } from "@/constants";
|
||||
import { useTrainMenu } from "@/stateBox/useTrainMenu";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import Icon from "react-native-vector-icons/Entypo";
|
||||
import { View, Text, TouchableOpacity, LayoutAnimation } from "react-native";
|
||||
import lineColorList from "@/assets/originData/lineColorList";
|
||||
import lineColorList from "../../../assets/originData/lineColorList";
|
||||
import Ionicons from "react-native-vector-icons/Ionicons";
|
||||
import { AS } from "@/storageControl";
|
||||
import { AS } from "../../../storageControl";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
|
||||
export const FavoriteSettingsItem = ({ currentStation }) => {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { View, Text, StyleSheet } from "react-native";
|
||||
import Animated, { useAnimatedRef } from "react-native-reanimated";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import Sortable from "react-native-sortables";
|
||||
import { useFavoriteStation } from "@/stateBox/useFavoriteStation";
|
||||
import { useFavoriteStation } from "../../stateBox/useFavoriteStation";
|
||||
import { FavoriteSettingsItem } from "./FavoliteSettings/FavoiliteSettingsItem";
|
||||
import { SheetHeaderItem } from "@/components/atom/SheetHeaderItem";
|
||||
import { getFavoriteStationKey } from "@/lib/favoriteStationUtils";
|
||||
|
||||
@@ -15,21 +15,21 @@ import { BigButton } from "../atom/BigButton";
|
||||
import { SheetHeaderItem } from "../atom/SheetHeaderItem";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
import dayjs from "dayjs";
|
||||
import * as ExpoFelicaReader from "@/modules/expo-felica-reader/src";
|
||||
import * as ExpoFelicaReader from "../../modules/expo-felica-reader/src";
|
||||
import { saveWidgetData } from "@/modules/expo-felica-reader/src";
|
||||
import type { FelicaCardInfo, FelicaHistoryEntry } from "@/modules/expo-felica-reader/src";
|
||||
import { AS } from "@/storageControl";
|
||||
import type { FelicaCardInfo, FelicaHistoryEntry } from "../../modules/expo-felica-reader/src";
|
||||
import { AS } from "../../storageControl";
|
||||
|
||||
// felicaStationMap は巨大 (5900行) なため、遅延ロード
|
||||
let _lookupFelicaStation: typeof import("@/lib/felicaStationMap").lookupFelicaStation | null = null;
|
||||
let _lookupFelicaStation: typeof import("../../lib/felicaStationMap").lookupFelicaStation | null = null;
|
||||
async function getLookupFelicaStation() {
|
||||
if (!_lookupFelicaStation) {
|
||||
const mod = await import("@/lib/felicaStationMap");
|
||||
const mod = await import("../../lib/felicaStationMap");
|
||||
_lookupFelicaStation = mod.lookupFelicaStation;
|
||||
}
|
||||
return _lookupFelicaStation;
|
||||
}
|
||||
import { STORAGE_KEYS } from "@/constants";
|
||||
import { STORAGE_KEYS } from "../../constants";
|
||||
import { requestWidgetUpdate } from "react-native-android-widget";
|
||||
import {
|
||||
FelicaQuickAccessWidget,
|
||||
|
||||
@@ -13,10 +13,10 @@ import LottieView from "lottie-react-native";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import { ListItem } from "@rneui/themed";
|
||||
import * as Updates from "expo-updates";
|
||||
import { AS } from "@/storageControl";
|
||||
import { AS } from "../../storageControl";
|
||||
import { STORAGE_KEYS } from "@/constants";
|
||||
import icons, { series as defaultSeries } from "@/assets/icons/icons";
|
||||
import hubIcons, { series as hubSeries } from "@/assets/unyoHubIcons/icons";
|
||||
import icons, { series as defaultSeries } from "../../assets/icons/icons";
|
||||
import hubIcons, { series as hubSeries } from "../../assets/unyoHubIcons/icons";
|
||||
import { setAlternateAppIcon, getAppIconName } from "expo-alternate-app-icons";
|
||||
import { SheetHeaderItem } from "../atom/SheetHeaderItem";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
@@ -362,7 +362,7 @@ export const LauncherIconSettings = () => {
|
||||
{!ready ? (
|
||||
<View style={{ flex: 1, alignItems: "center", justifyContent: "center", backgroundColor: colors.background }}>
|
||||
<LottieView
|
||||
source={require("@/assets/51690-loading-diamonds.json")}
|
||||
source={require("../../assets/51690-loading-diamonds.json")}
|
||||
autoPlay
|
||||
loop
|
||||
style={{ width: 120, height: 120 }}
|
||||
|
||||
@@ -47,17 +47,17 @@ export const LayoutSettings = ({
|
||||
bool={iconSetting}
|
||||
setBool={setIconSetting}
|
||||
firstItem={{
|
||||
firstImage: require("@/assets/configuration/icon_default.jpg"),
|
||||
firstImage: require("../../assets/configuration/icon_default.jpg"),
|
||||
firstText: "本家\n(文字アイコン)",
|
||||
firstValue: "default",
|
||||
}}
|
||||
secondItem={{
|
||||
secondImage: require("@/assets/configuration/icon_original.jpg"),
|
||||
secondImage: require("../../assets/configuration/icon_original.jpg"),
|
||||
secondText: "オリジナル\n(車種アイコン)",
|
||||
secondValue: "original",
|
||||
}}
|
||||
thirdItem={{
|
||||
thirdImage: require("@/assets/configuration/icon_hub.jpg"),
|
||||
thirdImage: require("../../assets/configuration/icon_hub.jpg"),
|
||||
thirdText: "鉄道運用Hub\n(Hubアイコン)",
|
||||
thirdValue: "hub",
|
||||
}}
|
||||
@@ -67,8 +67,8 @@ export const LayoutSettings = ({
|
||||
str="列車表示"
|
||||
bool={uiSetting}
|
||||
setBool={setUiSetting}
|
||||
falseImage={require("@/assets/configuration/layout_default.jpg")}
|
||||
trueImage={require("@/assets/configuration/layout_tokyo.jpg")}
|
||||
falseImage={require("../../assets/configuration/layout_default.jpg")}
|
||||
trueImage={require("../../assets/configuration/layout_tokyo.jpg")}
|
||||
falseText={"本家"}
|
||||
trueText={"オリジナル"}
|
||||
falseValue="default"
|
||||
@@ -78,8 +78,8 @@ export const LayoutSettings = ({
|
||||
str="トップメニュー表示"
|
||||
bool={mapSwitch}
|
||||
setBool={setMapSwitch}
|
||||
falseImage={require("@/assets/configuration/menu_default.jpg")}
|
||||
trueImage={require("@/assets/configuration/menu_original.jpg")}
|
||||
falseImage={require("../../assets/configuration/menu_default.jpg")}
|
||||
trueImage={require("../../assets/configuration/menu_original.jpg")}
|
||||
falseText={"本家\n(文字&路線リスト)"}
|
||||
trueText={"オリジナル\n(マップ&お気に入り)"}
|
||||
/>
|
||||
@@ -89,8 +89,8 @@ export const LayoutSettings = ({
|
||||
setBool={setStationMenu}
|
||||
falseText={"本家\n(文字)"}
|
||||
trueText={"オリジナル\n(駅名標)"}
|
||||
falseImage={require("@/assets/configuration/station_default.jpg")}
|
||||
trueImage={require("@/assets/configuration/station_original.jpg")}
|
||||
falseImage={require("../../assets/configuration/station_default.jpg")}
|
||||
trueImage={require("../../assets/configuration/station_original.jpg")}
|
||||
>
|
||||
<SimpleSwitch
|
||||
bool={usePDFView}
|
||||
@@ -104,25 +104,25 @@ export const LayoutSettings = ({
|
||||
setBool={setTrainMenu}
|
||||
falseText={"本家"}
|
||||
trueText={"オリジナル"}
|
||||
falseImage={require("@/assets/configuration/train_default.jpg")}
|
||||
trueImage={require("@/assets/configuration/train_original.jpg")}
|
||||
falseImage={require("../../assets/configuration/train_default.jpg")}
|
||||
trueImage={require("../../assets/configuration/train_original.jpg")}
|
||||
>
|
||||
<TripleSwitchArea
|
||||
str={"ヘッダーサイズ"}
|
||||
bool={headerSize}
|
||||
setBool={setHeaderSize}
|
||||
firstItem={{
|
||||
firstImage: require("@/assets/configuration/train_original_small.jpg"),
|
||||
firstImage: require("../../assets/configuration/train_original_small.jpg"),
|
||||
firstText: "小固定",
|
||||
firstValue: "small",
|
||||
}}
|
||||
secondItem={{
|
||||
secondImage: require("@/assets/configuration/train_original.jpg"),
|
||||
secondImage: require("../../assets/configuration/train_original.jpg"),
|
||||
secondText: "既定(可変)",
|
||||
secondValue: "default",
|
||||
}}
|
||||
thirdItem={{
|
||||
thirdImage: require("@/assets/configuration/train_original.jpg"),
|
||||
thirdImage: require("../../assets/configuration/train_original.jpg"),
|
||||
thirdText: "大固定",
|
||||
thirdValue: "big",
|
||||
}}
|
||||
|
||||
@@ -3,9 +3,9 @@ import { View, Text, TouchableOpacity, ScrollView } from "react-native";
|
||||
import * as Clipboard from "expo-clipboard";
|
||||
|
||||
import { CheckBox } from "@rneui/themed";
|
||||
import { AS } from "@/storageControl";
|
||||
import { AS } from "../../storageControl";
|
||||
import { STORAGE_KEYS } from "@/constants";
|
||||
import { useNotification } from "@/stateBox/useNotifications";
|
||||
import { useNotification } from "../../stateBox/useNotifications";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import { SheetHeaderItem } from "../atom/SheetHeaderItem";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
|
||||
@@ -1,208 +0,0 @@
|
||||
import React, { useCallback, useMemo, useState } from "react";
|
||||
import {
|
||||
FlatList,
|
||||
Linking,
|
||||
Platform,
|
||||
Text,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from "react-native";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import { MaterialCommunityIcons } from "@expo/vector-icons";
|
||||
import { SheetHeaderItem } from "@/components/atom/SheetHeaderItem";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
import {
|
||||
filterOssLicensePackages,
|
||||
resolveOssLicenseText,
|
||||
type OssLicenseData,
|
||||
type OssLicensePackage,
|
||||
} from "@/lib/ossLicenses";
|
||||
import licenseData from "@/assets/licenses/oss-licenses.json";
|
||||
|
||||
const data = licenseData as OssLicenseData;
|
||||
|
||||
const keyOf = (pkg: OssLicensePackage) => `${pkg.name}@${pkg.version}`;
|
||||
|
||||
export const OssLicenses = () => {
|
||||
const navigation = useNavigation();
|
||||
const { colors, fixed } = useThemeColors();
|
||||
const [query, setQuery] = useState("");
|
||||
const [expandedKey, setExpandedKey] = useState<string | null>(null);
|
||||
|
||||
const packages = useMemo(
|
||||
() => filterOssLicensePackages(data.packages, query),
|
||||
[query],
|
||||
);
|
||||
|
||||
const toggle = useCallback((key: string) => {
|
||||
setExpandedKey((current) => (current === key ? null : key));
|
||||
}, []);
|
||||
|
||||
const renderItem = useCallback(
|
||||
({ item }: { item: OssLicensePackage }) => (
|
||||
<LicenseRow
|
||||
pkg={item}
|
||||
expanded={expandedKey === keyOf(item)}
|
||||
onPress={toggle}
|
||||
/>
|
||||
),
|
||||
[expandedKey, toggle],
|
||||
);
|
||||
|
||||
const header = (
|
||||
<View>
|
||||
<Text style={{ color: colors.textSecondary, fontSize: 13, padding: 16, lineHeight: 20 }}>
|
||||
本アプリは以下のオープンソースソフトウェアを利用しています。項目をタップするとライセンス全文を表示します。
|
||||
</Text>
|
||||
<SectionHeader title="改変して同梱しているソフトウェア" />
|
||||
{data.vendored.map((pkg) => (
|
||||
<LicenseRow
|
||||
key={keyOf(pkg)}
|
||||
pkg={pkg}
|
||||
expanded={expandedKey === keyOf(pkg)}
|
||||
onPress={toggle}
|
||||
/>
|
||||
))}
|
||||
<SectionHeader title={`依存ライブラリ(${packages.length}件)`} />
|
||||
<View style={{ paddingHorizontal: 16, paddingBottom: 8 }}>
|
||||
<TextInput
|
||||
value={query}
|
||||
onChangeText={setQuery}
|
||||
placeholder="パッケージ名・ライセンス名で検索"
|
||||
placeholderTextColor={colors.textTertiary}
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
clearButtonMode="while-editing"
|
||||
style={{
|
||||
backgroundColor: colors.backgroundInput,
|
||||
color: colors.text,
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 8,
|
||||
fontSize: 15,
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
||||
return (
|
||||
<View style={{ flex: 1, backgroundColor: fixed.primary }}>
|
||||
<SheetHeaderItem
|
||||
title="オープンソースライセンス"
|
||||
LeftItem={{ title: "< 設定", onPress: () => navigation.goBack() }}
|
||||
/>
|
||||
<FlatList
|
||||
style={{ flex: 1, backgroundColor: colors.backgroundSecondary }}
|
||||
data={packages}
|
||||
keyExtractor={keyOf}
|
||||
renderItem={renderItem}
|
||||
extraData={expandedKey}
|
||||
ListHeaderComponent={header}
|
||||
ListEmptyComponent={
|
||||
<Text style={{ color: colors.textTertiary, textAlign: "center", padding: 20 }}>
|
||||
該当するパッケージはありません
|
||||
</Text>
|
||||
}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
keyboardDismissMode="on-drag"
|
||||
initialNumToRender={20}
|
||||
windowSize={11}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const LicenseRow = React.memo(
|
||||
({
|
||||
pkg,
|
||||
expanded,
|
||||
onPress,
|
||||
}: {
|
||||
pkg: OssLicensePackage;
|
||||
expanded: boolean;
|
||||
onPress: (key: string) => void;
|
||||
}) => {
|
||||
const { colors } = useThemeColors();
|
||||
const text = expanded ? resolveOssLicenseText(data, pkg) : null;
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: colors.surface,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.borderLight,
|
||||
}}
|
||||
>
|
||||
<TouchableOpacity
|
||||
onPress={() => onPress(keyOf(pkg))}
|
||||
style={{ flexDirection: "row", alignItems: "center", padding: 14 }}
|
||||
accessibilityRole="button"
|
||||
accessibilityState={{ expanded }}
|
||||
>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text style={{ color: colors.text, fontSize: 15 }} numberOfLines={1}>
|
||||
{pkg.name}
|
||||
</Text>
|
||||
<Text style={{ color: colors.textTertiary, fontSize: 12, marginTop: 2 }}>
|
||||
{pkg.version} ・ {pkg.license}
|
||||
{pkg.note ? ` ・ ${pkg.note}` : ""}
|
||||
</Text>
|
||||
</View>
|
||||
<MaterialCommunityIcons
|
||||
name={expanded ? "chevron-up" : "chevron-down"}
|
||||
size={22}
|
||||
color={colors.iconSecondary}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
{expanded && (
|
||||
<View style={{ paddingHorizontal: 14, paddingBottom: 14 }}>
|
||||
{pkg.repository && (
|
||||
<TouchableOpacity
|
||||
onPress={() => Linking.openURL(pkg.repository!)}
|
||||
style={{ marginBottom: 10 }}
|
||||
accessibilityRole="link"
|
||||
>
|
||||
<Text style={{ color: colors.textAccent, fontSize: 13 }}>
|
||||
{pkg.repository}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
<Text
|
||||
selectable
|
||||
style={{
|
||||
color: colors.textSecondary,
|
||||
fontSize: 11,
|
||||
lineHeight: 16,
|
||||
fontFamily: Platform.select({ ios: "Menlo", default: "monospace" }),
|
||||
}}
|
||||
>
|
||||
{text ?? `このパッケージにはライセンスファイルが同梱されていません(${pkg.license})。`}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
LicenseRow.displayName = "LicenseRow";
|
||||
|
||||
const SectionHeader = ({ title }: { title: string }) => {
|
||||
const { colors } = useThemeColors();
|
||||
return (
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 13,
|
||||
fontWeight: "600",
|
||||
color: colors.textTertiary,
|
||||
letterSpacing: 0.5,
|
||||
marginTop: 12,
|
||||
marginLeft: 16,
|
||||
marginBottom: 6,
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
@@ -22,10 +22,6 @@ import {
|
||||
} from "@/lib/jrDataSystemEnvironment";
|
||||
import { STORAGE_KEYS } from "@/constants";
|
||||
import { AS } from "@/storageControl";
|
||||
import {
|
||||
ERROR_BOUNDARY_TEST_SCOPES,
|
||||
armErrorBoundaryTest,
|
||||
} from "@/lib/errorBoundaryTest";
|
||||
|
||||
export const ResearchToolsSettings = () => {
|
||||
const navigation = useNavigation();
|
||||
@@ -136,21 +132,6 @@ export const ResearchToolsSettings = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const confirmErrorBoundaryTest = (scope: string, label: string) => {
|
||||
Alert.alert(
|
||||
"エラー画面テスト",
|
||||
`${label}でエラー画面を表示します。Sentryにテスト用のイベントが1件送信されます。`,
|
||||
[
|
||||
{ text: "キャンセル", style: "cancel" },
|
||||
{
|
||||
text: "表示する",
|
||||
style: "destructive",
|
||||
onPress: () => armErrorBoundaryTest(scope),
|
||||
},
|
||||
],
|
||||
);
|
||||
};
|
||||
|
||||
const openRecordingDatabase = () => {
|
||||
const uri = `${backendApiBaseUrl}/recorder?from=eachTrainInfo&userID=${encodeURIComponent(expoPushToken || "")}`;
|
||||
const params = {
|
||||
@@ -599,49 +580,6 @@ export const ResearchToolsSettings = () => {
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={[
|
||||
styles.debugSection,
|
||||
{
|
||||
backgroundColor: colors.surface,
|
||||
borderColor: colors.borderSecondary,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Text style={[styles.debugTitle, { color: colors.textPrimary }]}>
|
||||
デバッグ: エラー画面テスト
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
styles.debugDescription,
|
||||
{ color: colors.textSecondary },
|
||||
]}
|
||||
>
|
||||
選んだ範囲で意図的に描画エラーを起こし、エラー画面を確認します。タブを選んだ場合は、そのタブを開くとエラー画面が表示されます。
|
||||
</Text>
|
||||
<View style={styles.buttonRow}>
|
||||
{ERROR_BOUNDARY_TEST_SCOPES.map(({ scope, label }) => (
|
||||
<TouchableOpacity
|
||||
key={scope}
|
||||
onPress={() => confirmErrorBoundaryTest(scope, label)}
|
||||
style={[
|
||||
styles.neutralButton,
|
||||
{ backgroundColor: colors.borderSecondary },
|
||||
]}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.neutralButtonText,
|
||||
{ color: colors.textPrimary },
|
||||
]}
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
</ScrollView>
|
||||
|
||||
@@ -13,12 +13,11 @@ import { useNavigation } from "@react-navigation/native";
|
||||
import { ListItem } from "@rneui/themed";
|
||||
import TouchableScale from "react-native-touchable-scale";
|
||||
import { SwitchArea } from "../atom/SwitchArea";
|
||||
import { useNotification } from "@/stateBox/useNotifications";
|
||||
import { useNotification } from "../../stateBox/useNotifications";
|
||||
import { SheetHeaderItem } from "@/components/atom/SheetHeaderItem";
|
||||
import { useThemeColors, type ColorThemePref } from "@/lib/theme/useThemeColors";
|
||||
import { useTrainMenu } from "@/stateBox/useTrainMenu";
|
||||
import { MaterialCommunityIcons } from "@expo/vector-icons";
|
||||
const versionCode = "7.2.2";
|
||||
const versionCode = "7.2.1";
|
||||
|
||||
export const SettingTopPage = ({
|
||||
testNFC,
|
||||
@@ -43,7 +42,7 @@ export const SettingTopPage = ({
|
||||
<View style={{ height: 300, padding: 10 }}>
|
||||
<View style={{ flex: 1 }} />
|
||||
<Image
|
||||
source={require("@/assets/Header.png")}
|
||||
source={require("../../assets/Header.png")}
|
||||
style={{
|
||||
aspectRatio: 8.08,
|
||||
height: undefined,
|
||||
@@ -89,15 +88,14 @@ export const SettingTopPage = ({
|
||||
str="起動時に表示する画面"
|
||||
bool={startPage}
|
||||
setBool={setStartPage}
|
||||
falseImage={require("@/assets/configuration/showSetting0.jpg")}
|
||||
trueImage={require("@/assets/configuration/showSetting1.jpg")}
|
||||
falseImage={require("../../assets/configuration/showSetting0.jpg")}
|
||||
trueImage={require("../../assets/configuration/showSetting1.jpg")}
|
||||
falseText={"リンクメニュー"}
|
||||
trueText={"列車位置情報"}
|
||||
/>
|
||||
<ColorThemePicker value={colorTheme} onChange={setColorTheme} />
|
||||
<SettingList
|
||||
string="レイアウト設定"
|
||||
icon="view-dashboard-outline"
|
||||
onPress={() =>
|
||||
navigation.navigate("setting", { screen: "LayoutSettings" })
|
||||
}
|
||||
@@ -106,28 +104,24 @@ export const SettingTopPage = ({
|
||||
<SectionHeader title="カスタマイズ" />
|
||||
<SettingList
|
||||
string="お気に入り登録の並び替え"
|
||||
icon="sort-ascending"
|
||||
onPress={() =>
|
||||
navigation.navigate("setting", { screen: "FavoriteSettings" })
|
||||
}
|
||||
/>
|
||||
<SettingList
|
||||
string="アイコン設定"
|
||||
icon="image-multiple-outline"
|
||||
onPress={() =>
|
||||
navigation.navigate("setting", { screen: "LauncherIconSettings" })
|
||||
}
|
||||
/>
|
||||
<SettingList
|
||||
string="サウンド設定(β)"
|
||||
icon="volume-high"
|
||||
onPress={() =>
|
||||
navigation.navigate("setting", { screen: "SoundSettings" })
|
||||
}
|
||||
/>
|
||||
<SettingList
|
||||
string="運行情報設定(β)"
|
||||
icon="alert-circle-outline"
|
||||
onPress={() =>
|
||||
navigation.navigate("setting", { screen: "OperationInfoSettings" })
|
||||
}
|
||||
@@ -138,7 +132,6 @@ export const SettingTopPage = ({
|
||||
string={`通知設定${
|
||||
expoPushToken == "" ? "(通知設定をオンにしてください)" : "(β)"
|
||||
}`}
|
||||
icon="bell-outline"
|
||||
onPress={() =>
|
||||
navigation.navigate("setting", { screen: "NotificationSettings" })
|
||||
}
|
||||
@@ -146,22 +139,13 @@ export const SettingTopPage = ({
|
||||
/>
|
||||
<SettingList
|
||||
string="情報ソース設定"
|
||||
icon="database-outline"
|
||||
onPress={() =>
|
||||
navigation.navigate("setting", { screen: "DataSourceSettings" })
|
||||
}
|
||||
/>
|
||||
<SettingList
|
||||
string="接続ステータス"
|
||||
icon="server-network"
|
||||
onPress={() =>
|
||||
navigation.navigate("setting", { screen: "ConnectionStatus" })
|
||||
}
|
||||
/>
|
||||
{showResearchTools && (
|
||||
<SettingList
|
||||
string="調査ツール"
|
||||
icon="tools"
|
||||
onPress={() =>
|
||||
navigation.navigate("setting", { screen: "ResearchToolsSettings" })
|
||||
}
|
||||
@@ -171,24 +155,21 @@ export const SettingTopPage = ({
|
||||
<SectionHeader title="その他" />
|
||||
<SettingList
|
||||
string="プライバシーポリシー"
|
||||
icon="shield-lock-outline"
|
||||
onPress={() =>
|
||||
Linking.openURL(
|
||||
"https://xprocess.haruk.in/JR-shikoku-Apps-Common/NativeAppsPrivacyPolicy"
|
||||
"https://nexcloud.haruk.in/sites/press-harukin/JRShikokuApps/policy"
|
||||
)
|
||||
}
|
||||
/>
|
||||
<SettingList
|
||||
string="運営Twitter"
|
||||
icon="twitter"
|
||||
onPress={() => Linking.openURL("https://twitter.com/Xprocess_main")}
|
||||
string="開発情報"
|
||||
onPress={() =>
|
||||
Linking.openURL("https://nexcloud.haruk.in/s/2WFEN5oLPGtrRqT")
|
||||
}
|
||||
/>
|
||||
<SettingList
|
||||
string="オープンソースライセンス"
|
||||
icon="license"
|
||||
onPress={() =>
|
||||
navigation.navigate("setting", { screen: "OssLicenses" })
|
||||
}
|
||||
string="運営Twitter"
|
||||
onPress={() => Linking.openURL("https://twitter.com/Xprocess_main")}
|
||||
/>
|
||||
</View>
|
||||
</ScrollView>
|
||||
@@ -214,31 +195,21 @@ export const SettingTopPage = ({
|
||||
);
|
||||
};
|
||||
|
||||
const SettingList = ({
|
||||
string,
|
||||
icon,
|
||||
onPress,
|
||||
disabled = false,
|
||||
}: {
|
||||
string: string;
|
||||
icon?: React.ComponentProps<typeof MaterialCommunityIcons>["name"];
|
||||
onPress: () => void;
|
||||
disabled?: boolean;
|
||||
}) => {
|
||||
const SettingList = ({ string, onPress, disabled = false }: { string: string; onPress: () => void; disabled?: boolean }) => {
|
||||
const { colors } = useThemeColors();
|
||||
return (
|
||||
<ListItem
|
||||
// @ts-expect-error RNEUIのListItem型はComponentに渡したTouchableScaleのpropsを含まない
|
||||
// @ts-ignore: TouchableScale props passed through Component
|
||||
activeScale={0.95}
|
||||
Component={TouchableScale}
|
||||
bottomDivider
|
||||
onPress={onPress}
|
||||
disabled={disabled}
|
||||
// @ts-ignore: containerStyle type mismatch with TouchableScale Component
|
||||
containerStyle={{ backgroundColor: colors.surface }}
|
||||
>
|
||||
{icon ? <MaterialCommunityIcons name={icon} size={24} color={colors.iconSecondary} /> : null}
|
||||
<ListItem.Content>
|
||||
<ListItem.Title numberOfLines={1} style={{ color: colors.text }}>{string}</ListItem.Title>
|
||||
<ListItem.Title style={{ color: colors.text }}>{string}</ListItem.Title>
|
||||
</ListItem.Content>
|
||||
<ListItem.Chevron />
|
||||
</ListItem>
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
import { Switch } from "@rneui/themed";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import { SheetHeaderItem } from "@/components/atom/SheetHeaderItem";
|
||||
import { AS } from "@/storageControl";
|
||||
import { AS } from "../../storageControl";
|
||||
import { STORAGE_KEYS } from "@/constants";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
import { Asset } from "expo-asset";
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
import { VoicepeakDebugLogSection } from "@/components/Settings/VoicepeakDebugLogSection";
|
||||
import { useTrainMenu } from "@/stateBox/useTrainMenu";
|
||||
|
||||
const previewSound = require("@/assets/sound/rikka-test.mp3");
|
||||
const previewSound = require("../../assets/sound/rikka-test.mp3");
|
||||
|
||||
export const SoundSettings = () => {
|
||||
const { goBack } = useNavigation();
|
||||
|
||||
@@ -12,9 +12,9 @@ import {
|
||||
import type { SettingsStackParamList } from "@/types/navigation";
|
||||
import { createStackNavigator } from "@react-navigation/stack";
|
||||
import { pushTransitionOptions } from "@/lib/stackOption";
|
||||
import * as ExpoFelicaReader from "@/modules/expo-felica-reader/src";
|
||||
import * as ExpoFelicaReader from "../../modules/expo-felica-reader/src";
|
||||
import * as Updates from "expo-updates";
|
||||
import { AS } from "@/storageControl";
|
||||
import { AS } from "../../storageControl";
|
||||
import { STORAGE_KEYS } from "@/constants";
|
||||
import { Switch } from "@rneui/themed";
|
||||
import { SettingTopPage } from "./SettingTopPage";
|
||||
@@ -28,8 +28,6 @@ import { ResearchToolsSettings } from "./ResearchToolsSettings";
|
||||
import { FelicaHistoryPage } from "./FelicaHistoryPage";
|
||||
import { SoundSettings } from "./SoundSettings";
|
||||
import { OperationInfoSettings } from "./OperationInfoSettings";
|
||||
import { ConnectionStatusSettings } from "./ConnectionStatusSettings";
|
||||
import { OssLicenses } from "./OssLicenses";
|
||||
import { useTrainMenu } from "@/stateBox/useTrainMenu";
|
||||
import {
|
||||
normalizeIconDisplayMode,
|
||||
@@ -300,28 +298,6 @@ export default function Setting(props) {
|
||||
/>
|
||||
)}
|
||||
</Stack.Screen>
|
||||
<Stack.Screen
|
||||
name="ConnectionStatus"
|
||||
options={{
|
||||
gestureEnabled: true,
|
||||
...pushTransitionOptions,
|
||||
cardOverlayEnabled: true,
|
||||
headerTransparent: true,
|
||||
headerShown: false,
|
||||
}}
|
||||
component={ConnectionStatusSettings}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="OssLicenses"
|
||||
options={{
|
||||
gestureEnabled: true,
|
||||
...pushTransitionOptions,
|
||||
cardOverlayEnabled: true,
|
||||
headerTransparent: true,
|
||||
headerShown: false,
|
||||
}}
|
||||
component={OssLicenses}
|
||||
/>
|
||||
</Stack.Navigator>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as SplashScreen from "expo-splash-screen";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { StyleSheet } from "react-native";
|
||||
import { Platform, StyleSheet } from "react-native";
|
||||
import Animated, {
|
||||
Easing,
|
||||
useAnimatedStyle,
|
||||
@@ -19,22 +19,24 @@ const EASE_OUT = Easing.bezier(0.23, 1, 0.32, 1);
|
||||
|
||||
// Keep the native splash visible until the React overlay has been mounted.
|
||||
// This must run at module scope so the native splash is not hidden too early.
|
||||
void SplashScreen.preventAutoHideAsync().catch(() => {
|
||||
// The native splash may already be hidden during a reload.
|
||||
});
|
||||
if (Platform.OS !== "web") {
|
||||
void SplashScreen.preventAutoHideAsync().catch(() => {
|
||||
// The native splash may already be hidden during a reload.
|
||||
});
|
||||
}
|
||||
|
||||
export default function StartupOverlay() {
|
||||
const reducedMotion = useReducedMotion();
|
||||
const opacity = useSharedValue(1);
|
||||
const scale = useSharedValue(1);
|
||||
const [visible, setVisible] = useState(true);
|
||||
const [visible, setVisible] = useState(Platform.OS !== "web");
|
||||
const hasStarted = useRef(false);
|
||||
const fallbackTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const animationTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const animationFrame = useRef<number | null>(null);
|
||||
|
||||
const finish = useCallback(() => {
|
||||
if (hasStarted.current) return;
|
||||
if (Platform.OS === "web" || hasStarted.current) return;
|
||||
|
||||
hasStarted.current = true;
|
||||
if (fallbackTimer.current !== null) {
|
||||
@@ -78,6 +80,8 @@ export default function StartupOverlay() {
|
||||
}, [opacity, reducedMotion, scale]);
|
||||
|
||||
useEffect(() => {
|
||||
if (Platform.OS === "web") return;
|
||||
|
||||
// A local bundled image normally fires onLoadEnd immediately. The
|
||||
// fallback keeps a broken image from leaving the native splash forever.
|
||||
fallbackTimer.current = setTimeout(finish, FALLBACK_DELAY);
|
||||
|
||||
@@ -101,10 +101,10 @@ export const ExGridSimpleViewItem: FC<{
|
||||
const payload = {
|
||||
currentStation: returnDataBase,
|
||||
navigate,
|
||||
goTo: "StationDiagram",
|
||||
//@ts-ignore
|
||||
useShow: () => SheetManager.show("StationDetailView", { payload }),
|
||||
onExit: () => SheetManager.hide("StationDetailView"),
|
||||
};
|
||||
}; //@ts-ignore
|
||||
setTimeout(() => SheetManager.show("StationDetailView", { payload }), 50);
|
||||
} else {
|
||||
SheetManager.hide("StationDetailView");
|
||||
@@ -128,6 +128,7 @@ export const ExGridSimpleViewItem: FC<{
|
||||
from: d.isOperating ? null : "AllTrainIDList",
|
||||
};
|
||||
SheetManager.show("EachTrainInfo", {
|
||||
//@ts-ignore
|
||||
payload,
|
||||
onClose: () => {
|
||||
//alert(data);
|
||||
|
||||
@@ -108,10 +108,10 @@ export const ExGridViewItem: FC<{
|
||||
const payload = {
|
||||
currentStation: returnDataBase,
|
||||
navigate,
|
||||
goTo: "StationDiagram",
|
||||
//@ts-ignore
|
||||
useShow: () => SheetManager.show("StationDetailView", { payload }),
|
||||
onExit: () => SheetManager.hide("StationDetailView"),
|
||||
};
|
||||
}; //@ts-ignore
|
||||
setTimeout(() => SheetManager.show("StationDetailView", { payload }), 50);
|
||||
} else {
|
||||
SheetManager.hide("StationDetailView");
|
||||
@@ -132,6 +132,7 @@ export const ExGridViewItem: FC<{
|
||||
from: d.isOperating ? null :"AllTrainIDList",
|
||||
};
|
||||
SheetManager.show("EachTrainInfo", {
|
||||
//@ts-ignore
|
||||
payload,
|
||||
onClose: () => {
|
||||
//alert(data);
|
||||
|
||||
@@ -181,10 +181,10 @@ export const ListViewItem: FC<{
|
||||
const payload = {
|
||||
currentStation: returnDataBase,
|
||||
navigate,
|
||||
goTo: "StationDiagram",
|
||||
//@ts-ignore
|
||||
useShow: () => SheetManager.show("StationDetailView", { payload }),
|
||||
onExit: () => SheetManager.hide("StationDetailView"),
|
||||
};
|
||||
}; //@ts-ignore
|
||||
setTimeout(() => SheetManager.show("StationDetailView", { payload }), 50);
|
||||
} else {
|
||||
SheetManager.hide("StationDetailView");
|
||||
@@ -205,6 +205,7 @@ export const ListViewItem: FC<{
|
||||
from: "AllTrainIDList",
|
||||
};
|
||||
SheetManager.show("EachTrainInfo", {
|
||||
//@ts-ignore
|
||||
payload,
|
||||
onClose: () => {
|
||||
//alert(data);
|
||||
|
||||
@@ -69,16 +69,11 @@ export const StationDiagramView: FC<props> = ({ route }) => {
|
||||
// フィルタリング:終点路線、種別、行先、関係停車駅
|
||||
|
||||
const { keyList, allTrainDiagram } = useTrainDiagram();
|
||||
const { allCustomTrainData, activateCustomTrainData } = useCustomTrainData();
|
||||
const { todayOperation, activateOperationLogs } = useOperationLogs();
|
||||
const { allCustomTrainData } = useCustomTrainData();
|
||||
const { todayOperation } = useOperationLogs();
|
||||
const { useUnyohub: unyohubEnabled } = useUnyohub();
|
||||
const { useElesite: elesiteEnabled } = useElesite();
|
||||
|
||||
useEffect(() => {
|
||||
activateCustomTrainData();
|
||||
activateOperationLogs();
|
||||
}, [activateCustomTrainData, activateOperationLogs]);
|
||||
|
||||
const { goBack } = useNavigation();
|
||||
const { colors, fixed } = useThemeColors();
|
||||
const tabBarHeight = useBottomTabBarHeight();
|
||||
@@ -366,7 +361,7 @@ export const StationDiagramView: FC<props> = ({ route }) => {
|
||||
autoPlay
|
||||
loop
|
||||
style={{ width: 80, height: 80 }}
|
||||
source={require("@/assets/51690-loading-diamonds.json")}
|
||||
source={require("../../assets/51690-loading-diamonds.json")}
|
||||
/>
|
||||
<Text style={{ color: colors.text, marginTop: 12, fontSize: 14 }}>読み込み中...</Text>
|
||||
</View>
|
||||
|
||||
@@ -38,7 +38,7 @@ export const MapPin: FC<Props> = (props) => {
|
||||
>
|
||||
<View style={styles.markerWrap}>
|
||||
<Image
|
||||
source={require("@/assets/reccha-small.png")}
|
||||
source={require("../../assets/reccha-small.png")}
|
||||
style={styles.markerImage}
|
||||
resizeMode="contain"
|
||||
/>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { FC } from "react";
|
||||
import { View, Text, TouchableOpacity } from "react-native";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
import lineColorList from "@/assets/originData/lineColorList";
|
||||
import lineColorList from "../../assets/originData/lineColorList";
|
||||
|
||||
|
||||
type Props = {
|
||||
|
||||
@@ -12,7 +12,7 @@ export const SimpleSwitch = ({
|
||||
setBool,
|
||||
color,
|
||||
value,
|
||||
image = require("@/assets/icons.png"),
|
||||
image = require("../../assets/icons.png"),
|
||||
subText = "",
|
||||
}) => {
|
||||
const { colors } = useThemeColors();
|
||||
|
||||
@@ -223,8 +223,8 @@ export default function TrainMenu({ style }) {
|
||||
icon="train-car"
|
||||
flex={1}
|
||||
onPressButton={() =>
|
||||
navigate("generalWebView", {
|
||||
uri: "https://xprocess.haruk.in/JR-shikoku-Apps-Common/info/train-position",
|
||||
navigate("howto", {
|
||||
info: "https://xprocess.haruk.in/JR-shikoku-Apps-Common/info/train-position",
|
||||
})
|
||||
}
|
||||
>
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { StyleProp, ViewStyle } from "react-native";
|
||||
|
||||
type TrainMenuProps = {
|
||||
style?: StyleProp<ViewStyle>;
|
||||
};
|
||||
|
||||
export default function TrainMenu(_props: TrainMenuProps) {
|
||||
return null;
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import { View, Text } from "react-native";
|
||||
import {
|
||||
useCurrentTrainData,
|
||||
useCurrentTrainView,
|
||||
} from "@/stateBox/useCurrentTrain";
|
||||
} from "../../../stateBox/useCurrentTrain";
|
||||
import LottieView from "lottie-react-native";
|
||||
import { Ionicons, AntDesign } from "@expo/vector-icons";
|
||||
import { useStationList } from "@/stateBox/useStationList";
|
||||
@@ -43,7 +43,7 @@ export const Header = ({ station }) => {
|
||||
autoPlay
|
||||
loop
|
||||
style={{ width: 40, height: 40, marginRight: 30 }}
|
||||
source={require("@/assets/51690-loading-diamonds.json")}
|
||||
source={require("../../../assets/51690-loading-diamonds.json")}
|
||||
/>
|
||||
) : currentTrainLoading == "error" ? (
|
||||
<Ionicons
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Text } from "react-native";
|
||||
import { Switch } from "@rneui/themed";
|
||||
import { AS } from "@/storageControl";
|
||||
import { AS } from "../../../storageControl";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
export const SwitchBox = (props) => {
|
||||
const { value, setValue, setKey, title } = props;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { FC, useState, useRef, useEffect } from "react";
|
||||
import { Text, TextStyle, View, TouchableOpacity } from "react-native";
|
||||
import { useInterval } from "@/lib/useInterval";
|
||||
import { useInterval } from "../../../lib/useInterval";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
|
||||
const descriptionStyle: TextStyle = {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { FC } from "react";
|
||||
import { Text, View } from "react-native";
|
||||
import { getTrainType } from "@/lib/getTrainType";
|
||||
import { getTrainType } from "../../../lib/getTrainType";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
import { trainTypeID } from "@/lib/CommonTypes";
|
||||
type Props = {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Text, TextStyle, View } from "react-native";
|
||||
import {
|
||||
trainDataType,
|
||||
trainPosition,
|
||||
} from "@/lib/trainPositionTextArray";
|
||||
} from "../../../lib/trainPositionTextArray";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
import { OfficialPositionEditButton } from "@/components/OfficialPositionEditButton";
|
||||
import type { NavigateFunction } from "@/types";
|
||||
|
||||
@@ -46,7 +46,6 @@ import {
|
||||
} from "@/lib/checkDuplicateTrainData";
|
||||
import { trainPosition } from "@/lib/trainPositionTextArray";
|
||||
import { LEDFrame } from "@/components/発車時刻表/LED_Vision_Component/LEDFrame";
|
||||
import { markStartupOnce } from "@/lib/devStartupPerformance";
|
||||
|
||||
const readBooleanSetting = async (key: string) => {
|
||||
try {
|
||||
@@ -120,8 +119,7 @@ export const LED_vision: FC<props> = (props) => {
|
||||
const [isInfoArea, setIsInfoArea] = useState(false);
|
||||
const { areaInfo, areaStationID } = useAreaInfo();
|
||||
const { allTrainDiagram } = useTrainDiagram();
|
||||
const { allCustomTrainData, activateCustomTrainData } =
|
||||
useCustomTrainData();
|
||||
const { allCustomTrainData } = useCustomTrainData();
|
||||
const { fixed } = useThemeColors();
|
||||
const [voicepeakSettings, setVoicepeakSettings] =
|
||||
useState<VoicepeakSettings | null>(null);
|
||||
@@ -148,12 +146,6 @@ export const LED_vision: FC<props> = (props) => {
|
||||
);
|
||||
const [nativeVoicepeakPlaybackKey, setNativeVoicepeakPlaybackKey] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
// LEDが実際に表示される段階で列車補助情報を再検証する。
|
||||
// キャッシュまたはcustomTrainDataDetectorのfallbackは先に利用できる。
|
||||
activateCustomTrainData();
|
||||
}, [activateCustomTrainData]);
|
||||
|
||||
const refreshVoicepeakSettings = useCallback(() => {
|
||||
loadVoicepeakSettings()
|
||||
.then((settings) => {
|
||||
@@ -541,13 +533,12 @@ export const LED_vision: FC<props> = (props) => {
|
||||
}, 1000);
|
||||
|
||||
useEffect(() => {
|
||||
markStartupOnce("led-content-ready", "LED content ready");
|
||||
onContentReady?.();
|
||||
}, [onContentReady, station]);
|
||||
|
||||
const content = (
|
||||
<>
|
||||
{nativeVoicepeakHtml !== EMPTY_NATIVE_VOICEPEAK_HTML && (
|
||||
{Platform.OS !== "web" && nativeVoicepeakHtml !== EMPTY_NATIVE_VOICEPEAK_HTML && (
|
||||
<WebView
|
||||
key={nativeVoicepeakPlaybackKey}
|
||||
source={{ html: nativeVoicepeakHtml }}
|
||||
|
||||
@@ -20,7 +20,7 @@ export const LottieDelayView: FC<Props> = ({ progress }) => {
|
||||
speed={1.4}
|
||||
style={{ width: 80, height: 80 }}
|
||||
loop={false}
|
||||
source={require("@/assets/939-star.json")}
|
||||
source={require("../../assets/939-star.json")}
|
||||
ref={lottieRef}
|
||||
onAnimationFinish={(isCanceled) => {
|
||||
setProgressState(progress);
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
ViewStyle,
|
||||
} from "react-native";
|
||||
import { StationName } from "./StationName";
|
||||
import lineColorList from "@/assets/originData/lineColorList";
|
||||
import lineColorList from "../../assets/originData/lineColorList";
|
||||
import { useThemeColors, lightColors } from "@/lib/theme";
|
||||
export const NextPreStationLine = ({ nexStation, preStation, isMatsuyama }) => {
|
||||
const 下枠フレーム: ViewStyle = {
|
||||
|
||||
@@ -7,23 +7,20 @@ import {
|
||||
} from "react-native";
|
||||
import { MaterialCommunityIcons } from "@expo/vector-icons";
|
||||
import LottieView from "lottie-react-native";
|
||||
import { useInterval } from "@/lib/useInterval";
|
||||
import { useFavoriteStation } from "@/stateBox/useFavoriteStation";
|
||||
import { useInterval } from "../../lib/useInterval";
|
||||
import { useFavoriteStation } from "../../stateBox/useFavoriteStation";
|
||||
|
||||
import { StationNameArea } from "./StationNameArea";
|
||||
import { StationNumberMaker } from "./StationNumberMaker";
|
||||
import { NextPreStationLine } from "./NextPreStationLine";
|
||||
import { LottieDelayView } from "./LottieDelayView";
|
||||
import { AddressText } from "./AddressText";
|
||||
import { useStationList } from "@/stateBox/useStationList";
|
||||
import { useStationList } from "../../stateBox/useStationList";
|
||||
import { useThemeColors, lightColors } from "@/lib/theme";
|
||||
import { markStartupOnce } from "@/lib/devStartupPerformance";
|
||||
|
||||
export default function Sign(props) {
|
||||
const { oP, oLP, isCurrentStation = false, stationID } = props;
|
||||
|
||||
markStartupOnce("menu-station-sign-render", "menu station sign render");
|
||||
|
||||
const { width, height } = useWindowDimensions();
|
||||
const { getStationDataFromId, originalStationList } = useStationList();
|
||||
const { fixed } = useThemeColors();
|
||||
@@ -176,7 +173,7 @@ export default function Sign(props) {
|
||||
height: ((width * 0.8) / 20) * 9,
|
||||
backgroundColor: lightColors.surface,
|
||||
}}
|
||||
source={require("@/assets/StationSign.json")}
|
||||
source={require("../../assets/StationSign.json")}
|
||||
/>
|
||||
)}
|
||||
<StationNumberMaker {...{ currentStation: currentStationData }} />
|
||||
|
||||
@@ -48,26 +48,11 @@ export const API_ENDPOINTS = {
|
||||
/** 本番列車位置情報 */
|
||||
CURRENT_POSITIONS: `${BASE_URL}/tmp/currentPositions.json`,
|
||||
|
||||
/** 列車位置R2障害時のGAS fallback */
|
||||
CURRENT_POSITIONS_FALLBACK: 'https://script.google.com/macros/s/AKfycby9Y2-Bm75J_WkbZimi7iS8v5r9wMa9wtzpdwES9sOGF4i6HIYEJOM60W6gM1gXzt1o/exec',
|
||||
|
||||
/** 臨時列車情報 */
|
||||
SPECIAL_TRAIN_INFO_STORAGE: `${BASE_URL}/sptrainfo`,
|
||||
|
||||
/** ニュースリリース情報 */
|
||||
NEWS_RELEASES_STORAGE: `${BASE_URL}/news-releases/latest.json`,
|
||||
|
||||
/** VOICEPEAK公開APIのベースURL(音声生成は通常機能からのみPOST) */
|
||||
VOICEPEAK_PUBLIC_BASE_URL: 'https://voicepeak-api.haruk.in',
|
||||
} as const;
|
||||
|
||||
/** 接続ステータス画面で利用する公開health endpoint */
|
||||
export const HEALTH_ENDPOINTS = {
|
||||
/** operation-monitor の公開health */
|
||||
OPERATION_MONITOR: 'https://jrs-operation-info.haruk.in/health',
|
||||
|
||||
/** VOICEPEAK公開APIの公開health(音声生成は実行しない) */
|
||||
VOICEPEAK: `${API_ENDPOINTS.VOICEPEAK_PUBLIC_BASE_URL}/healthz`,
|
||||
} as const;
|
||||
|
||||
/**
|
||||
|
||||
@@ -18,9 +18,6 @@ export const STORAGE_KEYS = {
|
||||
|
||||
/** 全列車ダイアグラム */
|
||||
ALL_TRAIN_DIAGRAM: 'allTrainDiagram',
|
||||
|
||||
/** 列車補助情報(列車名・種別・編成など) */
|
||||
CUSTOM_TRAIN_DATA: 'customTrainData',
|
||||
|
||||
/** 遅延データ */
|
||||
DELAY_DATA: 'delayData',
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
# 接続ステータス health contract
|
||||
|
||||
## 境界
|
||||
|
||||
接続先のJSON/textは各adapterが解釈し、画面は正規化されたHealthCheckResultとServiceHealthResultだけを購読する。
|
||||
|
||||
API response → adapter/parser → HealthCheckResult → role-aware service aggregation → UI
|
||||
|
||||
画面から個別URL、認証情報、raw exceptionを参照しない。将来のsystem-health aggregatorはこのadapter境界へ追加する。
|
||||
|
||||
## 状態
|
||||
|
||||
- checking: 実行中
|
||||
- ok: 到達可能でschema/freshnessが正常
|
||||
- degraded: stale、更新遅延、diagnostic障害、fallbackなど
|
||||
- error: timeout、network error、HTTP 5xx、primaryの明示的error
|
||||
- unknown: healthなし、malformed、未知status、初期化中など
|
||||
|
||||
## role
|
||||
|
||||
- primary: ユーザー機能を成立させる公開データ。error/degraded/unknownをサービス状態へ反映する。
|
||||
- diagnostic: 生成Workerや補助API。primaryがokでも障害時はdegradedへするがerrorへ昇格しない。
|
||||
- optional: 設定ON時の補助機能またはfallback。optionalだけの障害で機能をerrorにしない。
|
||||
|
||||
primary > diagnostic > optionalという重要度であり、単純な全checkの最悪値集約は使用しない。primaryがokでdiagnosticが問題ならdegraded、primaryがerrorならerrorとする。
|
||||
|
||||
## 実行制約
|
||||
|
||||
- 画面focus時、手動再確認、pull-to-refreshだけで実行する。
|
||||
- 1サービスの失敗はPromise.allSettledで他サービスを止めない。
|
||||
- 7秒timeout、AbortController、unmount後commit防止を維持する。
|
||||
- POST、token付きGET、admin endpoint、VOICEPEAK音声生成はstatus確認に使用しない。
|
||||
- status画面へ追加するのは公開GETと、ユーザー機能を成立させる安全なデータ確認だけとする。
|
||||
@@ -1,134 +0,0 @@
|
||||
# 接続ステータス外部依存監査
|
||||
|
||||
監査日: 2026-09-18
|
||||
|
||||
## 集計
|
||||
|
||||
同じURLをRN、WebView、Widget、Live Activityから呼ぶケースは1つの論理依存へまとめて集計した。
|
||||
|
||||
- 接続ステータスへ追加・維持: 18論理依存(VOICEPEAKは音声生成runtimeとsafe healthを1機能として集計)
|
||||
- 現在利用中だがユーザー向け接続ステータス対象外: 9論理依存
|
||||
- 管理者・開発用または観測基盤: 2論理依存
|
||||
- 定数だけ残るlegacy/未使用、または他repo情報でURLが確定した未使用依存: 4系統
|
||||
- routeは確定したがproduction hostが未確定: 2 Worker(3 health route)
|
||||
- 将来のdeep health追加TODO: 1系統(Position API WorkerのD1確認)
|
||||
|
||||
「現在利用中」は、画面・hook・WebView注入・Widget・Live Activity・設定ON・明示操作からruntime到達できるものとして判定した。定数や古いドキュメントにしか登場しないURLは含めていない。
|
||||
|
||||
## 1. 接続ステータスへ追加・維持したもの
|
||||
|
||||
| 機能 | 内部サービス | URL / method | 呼び出し元 | 現在利用中 | role | 確認方法 | healthの深さ |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| 列車走行位置 | R2公開snapshot | https://jr-shikoku-api-data-storage.haruk.in/tmp/currentPositions.json / GET | stateBox/useCurrentTrain.tsx | yes | primary | data配列、fetchedAt、180秒超をdegraded | 公開データschema + freshness |
|
||||
| 列車走行位置 | GAS fallback | https://script.google.com/macros/s/AKfycby9Y2-Bm75J_WkbZimi7iS8v5r9wMa9wtzpdwES9sOGF4i6HIYEJOM60W6gM1gXzt1o/exec / GET | stateBox/useCurrentTrain.tsx | yes | optional | 配列JSONを検証。状態・freshnessは返らない | fallbackの軽量schema確認のみ |
|
||||
| 列車走行位置 | position problems n8n | https://n8n.haruk.in/webhook/jrshikoku-position-problems / GET | lib/webViewInjectjavascript.ts | yes | diagnostic | dataキーを持つJSONを検証 | 生成元の死活・payload envelopeのみ |
|
||||
| 運行情報 | operation-monitor | https://jrs-operation-info.haruk.in/health / GET | lib/connectionStatus.ts、R2運行情報の生成系 | yes | diagnostic | status: ok/stale/error/initializingを正規化 | Worker health。DB deep healthではない |
|
||||
| 運行情報 | 公開運行情報snapshot | https://jr-shikoku-api-data-storage.haruk.in/operation-info/jr-shikoku/latest.json / GET | stateBox/useAreaInfo.tsx、Widget | yes | primary | fetchedAt、compatibility.areaInfo、operationInfoText | アプリが読む公開JSONのschema |
|
||||
| 運行情報 | Go Backend operation logs | https://jr-shikoku-backend-api-v1.haruk.in/operation-logs(experimentalはhttps://jr-shikoku-backend-api-v1-beta.haruk.in/operation-logs) / GET | stateBox/useAllTrainDiagram.tsx、WebView注入 | yes | diagnostic | data: nullまたは配列 | Backend endpoint到達 + envelope |
|
||||
| 時刻表・ダイヤ | R2 diagram | production /tmp/diagram-today.json、experimental /tmp/diagram-today-beta.json / GET | stateBox/useAllTrainDiagram.tsx | yes | primary | 配列JSON | 配信データ取得・schema。位置情報と同じfreshness閾値は使わない |
|
||||
| 列車・駅データ | Go Backend | https://jr-shikoku-backend-api-v1.haruk.in/ping(experimentalはhttps://jr-shikoku-backend-api-v1-beta.haruk.in/ping) / GET | connection status | yes | diagnostic | HTTP 2xxかつ {message: pong pong} | HTTP process healthのみ。DB正常とは表示しない |
|
||||
| 列車・駅データ | Go Backend train-data | https://jr-shikoku-backend-api-v1.haruk.in/train-data(experimentalはhttps://jr-shikoku-backend-api-v1-beta.haruk.in/train-data) / GET | stateBox/useAllTrainDiagram.tsx、WebView注入 | yes | primary | data配列 | 公開API schema |
|
||||
| 列車・駅データ | n8n station list | https://n8n.haruk.in/webhook/jr-shikoku-station-list / GET | lib/webViewInjectjavascript.ts | yes | diagnostic | 配列または非空object | WebView補助データの到達確認 |
|
||||
| 列車・駅データ | バス・列車補助GAS | https://script.google.com/macros/s/AKfycbw0UW6ZeCDgUYFRP0zxpc_Oqfy-91dBdbWv-cM8n3narKp14IyCd2wy5HW7taXcW7E/exec / GET | stateBox/useBusAndTrainData.tsx(AsyncStorage miss時) | yes(条件付き) | optional | 配列JSON | 補助配信のschemaのみ |
|
||||
| 遅延速報EX | canonical derived data | https://jr-shikoku-api-data-storage.haruk.in/derived/delays/latest.json / GET | 定数・status check | yes(現行画面の主経路ではない) | diagnostic | stale、sourceFetchedAt/observedAt | 公開derived JSON。遅延列車数は障害判定に使わない |
|
||||
| 遅延速報EX | 現行legacy text | https://jr-shikoku-api-data-storage.haruk.in/legacy/trainfo-ex.txt / GET | stateBox/useTrainDelayData.tsx、Android Widget | yes | primary | textとして取得できること | HTTP/text到達のみ。text内部のfreshnessは判定しない |
|
||||
| 臨時列車 | R2 sptrainfo | https://jr-shikoku-api-data-storage.haruk.in/sptrainfo / GET | components/Menu/SpecialTrainInfoBox.tsx | yes | primary | {data: []}を含む配列envelope。0件は正常 | 公開JSON。special-train-dbの /health はroute確定、production host待ち |
|
||||
| ニュース | R2 news releases | https://jr-shikoku-api-data-storage.haruk.in/news-releases/latest.json / GET | components/Menu/NewsReleaseInfoBox.tsx | yes | primary | {items: []}を含むitems配列。0件は正常 | 公開JSON。/news-releases/health はroute確定、production host待ち |
|
||||
| 音声読み上げ | VOICEPEAK health | https://voicepeak-api.haruk.in/healthz / GET | lib/connectionStatus.ts | yes(VOICEPEAK_ENABLED=true時) | optional | ok:true、workerHealthy:false、publicSpeech.enabledを検証。enabled=falseはdegraded、malformedはunknown、503はerror | 公開health。内部workerも確認するが、アプリからDB等のdeep healthは表示しない |
|
||||
| 音声読み上げ | VOICEPEAK public speech | https://voicepeak-api.haruk.in/v1/public/speech / POST | lib/voicepeak.ts、components/発車時刻表/LED_vidion.tsx | yes(VOICEPEAK_ENABLED=true時) | runtimeのみ | 接続ステータスからは一切POSTしない | 音声生成endpoint。生成・CPU負荷・コストを伴うため死活確認には使わない |
|
||||
| 鉄道運用Hub | R2 thirdparty | https://jr-shikoku-api-data-storage.haruk.in/thirdparty/unyohub-unyo.json / GET | stateBox/useUnyohub.tsx、WebView注入 | yes(設定ON時) | optional | 配列JSON | 補助データschemaのみ |
|
||||
| えれサイト | R2 thirdparty | https://jr-shikoku-api-data-storage.haruk.in/thirdparty/elesite-unyo.json / GET | stateBox/useElesite.tsx、WebView注入 | yes(設定ON時) | optional | 配列JSON | 補助データschemaのみ |
|
||||
|
||||
VOICEPEAKは、通常機能では /v1/public/speech をPOSTするが、接続ステータスは認証不要の /healthz だけをGETする。ok:falseまたは内部worker異常はerror、publicSpeech.enabled:falseはdegraded、想定外schemaはunknownとする。設定OFF時は一覧から非表示にし、RECORDER_ADMIN_TOKENやVOICEPEAK secretは追加しない。
|
||||
|
||||
## 2. 利用中だがユーザー向け接続ステータス対象外
|
||||
|
||||
| 機能 / 内部名称 | URL / 呼び出し元 | 現在利用中 | 対象外理由 |
|
||||
| --- | --- | --- | --- |
|
||||
| 公式在線WebView | https://train.jr-shikoku.co.jp/sp.html、ページ内公式XHR / components/Apps/WebView.tsx、Apps.tsx | yes | 主要機能だが相手サイトの軽量healthがなく、status画面のためにHTML/XHRを追加取得すると負荷と二重取得になる。R2位置データをprimaryとする |
|
||||
| 公式運行情報WebView | https://www.jr-shikoku.co.jp/info/ / ndView.tsx、hidden preload | yes | 公式ページ全体の取得を診断目的に行わない。R2 snapshot + operation-monitor healthを対象とする |
|
||||
| 公式在線位置補完Backend | 環境Backendの /api/v1/official-positions/resolve?line=...&pos=...&pos_num=... / lib/officialPositionService.ts | yes | 現在位置から有効なtupleが必要。status画面のために架空IDを送らず、位置snapshot取得後の補完処理で個別に失敗を吸収する |
|
||||
| アンパンマン列車状態 | https://n8n.haruk.in/webhook/yosan-anpanman?trainNum=...、dosan-anpanman?trainNum=... / trainIconStatus.tsx | yes(対象列車だけ) | 固定の安全なmetadata/healthがなく、任意列車の日付queryをstatus確認のために送らない。表示用の任意補助 |
|
||||
| 通知設定登録 | https://n8n.haruk.in/webhook/jr-shikoku-notification-configurations / NotificationSettings.tsx | yes(登録操作時) | Push tokenを含む副作用POST。status画面から実行しない |
|
||||
| 権限確認 | 環境Backendの /check-permission?user_id=<push-token> / useTrainMenu.tsx | yes(token取得後) | 識別子を含むユーザー権限API。health checkのためにtokenを流用しない |
|
||||
| 駅住所LOD | 駅データのjslodApi + .json、主に https://uedayou.net/jrslod/... / components/駅名表/AddressText.tsx | yes(駅名標表示時) | 駅ごとの動的URLで、主要鉄道機能に必須でない外部LOD。全駅分を取得しない |
|
||||
| jr-shikoku-data-system画面 | https://jr-shikoku-data-system.pages.dev/...、shikoku-railinfo.haruk.in、push token query / 各Menu・TrainDataSources | yes(明示操作時) | ユーザーが開く外部Web画面。識別子を含むためstatus用probeを追加しない |
|
||||
| 録画import / GeneralWebView | routeで渡された任意URL、blob、recording download / GeneralWebView.tsx | yes(明示操作時) | 任意URLを扱う機能。監視対象を固定できず、recording backendのadmin endpointも呼ばない |
|
||||
| 画像・CDN・公式リンク | storage.haruk.in、n8n画像、fossil.2pd.jp、JR四国/JR駅公式URL | yes(表示・明示操作時) | 装飾asset、車両画像、PDF、外部リンク。主要データサービスの接続状態ではない |
|
||||
|
||||
## 3. 管理者・開発用または観測基盤
|
||||
|
||||
| 名称 | URL / 呼び出し元 | 判定 |
|
||||
| --- | --- | --- |
|
||||
| Position mock API | https://jr-shikoku-backend-mock-api-v1.haruk.in/position-masters、/train-positions/current / lib/mockApi/positionMasters.ts | admin専用のmock設定時だけ。一般ユーザー向け一覧には出さない |
|
||||
| Position API Worker root | https://jr-shikoku-backend-mock-api-v1.haruk.in/ / GET | 他repoでproduction URLとroot応答を確認済み。現行アプリの通常runtime callはなく、一覧には出さない。rootはWorker aliveのみでD1 deep healthではない |
|
||||
| Sentry / telemetry / Expo Updates | lib/observability/sentry.ts、lib/observability/telemetry.ts、UpdateAsync.ts、expo-updates | 観測・配布基盤。鉄道機能のユーザー向けhealthではない。token/DSNやupdate通信を画面へ表示しない |
|
||||
|
||||
HTTP transportはRN側が標準fetchと既存observedFetchJson/observedFetchTextであり、axios、Nitro Fetch、ky、superagentはruntime採用されていない。WebView内部はWebView側のfetch/XHRでRN wrapperの観測範囲外である。
|
||||
|
||||
## 4. legacy / 未使用
|
||||
|
||||
| 名称 | 根拠 | 扱い |
|
||||
| --- | --- | --- |
|
||||
| API_ENDPOINTS.CUSTOM_TRAIN_DATA | constants/api.tsにのみ存在し、runtime call siteなし | 未使用。現行列車データはBackend /train-data |
|
||||
| API_ENDPOINTS.SPECIAL_TRAIN_INFO | constants/api.tsにのみ存在し、runtime call siteなし | 未使用。現行臨時列車はR2 /sptrainfo |
|
||||
| recording-backend public health | https://rec-jr-s.haruk.in/healthz / GET(READMEでproduction URLを確認) | URLは確定したが、現行走行履歴は端末local storageで完結するためstatus一覧には出さない。/recorder/statusはadmin token必須なのでアプリから呼ばない |
|
||||
| README・旧data-fetch-mapの旧n8n/GAS経路 | 現行sourceのcall siteと一致しない記述が残る | 削除せず、今回のruntime監査では現行sourceを正本とした |
|
||||
|
||||
### 候補名はあるが現行runtime call siteが見つからなかったもの
|
||||
|
||||
- JR西日本運行情報の直接API
|
||||
- 怪レい列車のデータAPI(通知設定キーはあるが、データ取得APIはない)
|
||||
- position-collector、special-train-db、news-releases Workerのproduction host(routeは確定、Cloudflare実環境のhost待ち)
|
||||
- Position API Workerの通常ユーザー向けendpoint(root URLは確定したが現行runtime callなし)
|
||||
- recording-backend。走行履歴は現行sourceでは端末local storage(public /healthz URLは確定)
|
||||
- アプリ独自のremote config/version JSON。Expo Updatesは配布基盤として別分類
|
||||
- font/assetのremote取得。主要フォント・音声・画像はbundleまたは機能画面で個別取得
|
||||
|
||||
駅情報の基本リストはlib/getStationList.tsとassetsの静的データで、n8n station-listはWebView補助取得として扱った。
|
||||
|
||||
## 5. 判断不能 / backend改修TODO
|
||||
|
||||
以下はrouteまたはproduction hostが未確定のため、URLを推測してアプリから呼んでいない。確定済みのVOICEPEAK、Main Backend、Position API root、recording /healthzはこの一覧から除外した。
|
||||
|
||||
1. **position-collector /health**
|
||||
- Worker名はjr-shikoku-position-collector、公開routeは/health、status値はok/stale/error/initializingまで確定している。
|
||||
- production Custom Domainがrepo configにないため、Cloudflare実環境でworkers.dev hostを確認するまで列車走行位置のdiagnosticへ追加しない。hostを推測・ハードコードしない。
|
||||
|
||||
2. **special-train-db /health**
|
||||
- Worker名はjr-shikoku-special-train-db、公開routeは/health、D1 stateを読む正規healthである。
|
||||
- production hostがrepo configにないため、Cloudflare実環境で確認するまで臨時列車のdiagnosticへ追加しない。
|
||||
|
||||
3. **special-train-db /news-releases/health**
|
||||
- 上記Workerの同一hostに/news-releases/healthが存在し、ニュース用の別監視項目として利用できる。
|
||||
- production hostが未確定のため、ニュースのdiagnosticへ仮URLを追加しない。
|
||||
|
||||
4. **Position API Workerのdeep health**
|
||||
- root https://jr-shikoku-backend-mock-api-v1.haruk.in/ と応答schemaは確定しているが、現行アプリの通常runtime callはないため一覧へ追加しない。
|
||||
- rootはWorker aliveのみ。将来利用する場合はD1 SELECT 1等を行う/healthを別途追加する。
|
||||
|
||||
5. **recording-backendのdeep status**
|
||||
- public https://rec-jr-s.haruk.in/healthz は確定しているが、現行走行履歴は端末local storageで完結するため一覧へ追加しない。
|
||||
- /recorder/statusはadmin token必須。アプリから呼ばず、RECORDER_ADMIN_TOKENも追加しない。
|
||||
|
||||
怪レい列車API、special-train-db、news-releases Worker、Position API Worker、recording-backendのstatus画面用POST/管理APIは、確認のために呼び出していない。現行アプリsourceでユーザー機能のruntime callがないものは、監視対象へ捏造追加していない。
|
||||
|
||||
## 6. role集約の契約
|
||||
|
||||
- primaryのerror/degraded/unknownは機能状態へ反映する。
|
||||
- primaryがokでdiagnosticがerror/degraded/unknownなら機能はdegradedとする。diagnostic errorを機能全体のerrorへ昇格しない。
|
||||
- optionalのerror/degraded/unknownだけでは機能状態をerrorにしない。画面上部には任意サービスの問題を別文言で示す。
|
||||
- VOICEPEAK設定がOFFの場合はservice定義自体を作らず、一覧から非表示にする。
|
||||
- VOICEPEAKの /healthz はoptional checkとして扱い、失敗しても鉄道機能全体をerrorにしない。
|
||||
- HTTP 200だけではokにせず、health status・JSON schema・公開データのfreshnessをadapterで判定する。
|
||||
|
||||
## 7. 将来system-health aggregatorへ移行する場合
|
||||
|
||||
画面はServiceHealthResultだけを購読し、URLや個別JSONを解釈しない。将来はlib/connectionStatus.tsのservice definitions/clientを/system-health adapterへ差し替え、次を維持すればよい。
|
||||
|
||||
- HealthCheckResultのid/status/checkedAt/latencyMs/lastSuccessAt/ageSeconds
|
||||
- primary/diagnostic/optionalのrole
|
||||
- disabled/optional機能の表示ルール
|
||||
- incremental callback、AbortController、7秒timeout、Promise.allSettled
|
||||
|
||||
backend側のhealth追加は別repo・別変更として行い、アプリへsecret、admin token、Worker secret、n8n secretを追加しない。
|
||||
+10
-14
@@ -33,8 +33,8 @@ Sentryで観測している `endpoint=positions / operation_info / train_operati
|
||||
| positions n8n | `stateBox/useCurrentTrain.tsx` | `getCurrentTrain` | `n8n.haruk.in` | `/webhook/c501550c-7d1b-4e50-927b-4429fe18931a` | positions | 起動時、mock切替時、interval | 15秒 | 8000ms | 最大1回 | なし | Google Apps Script fallback | `positions_current` / source=`n8n` / user_visible=`true` / preload=`false` / fetch_priority=`high` | mock API無効時の主経路。成功時は録画snapshotにも使う。 |
|
||||
| positions GAS fallback | `stateBox/useCurrentTrain.tsx` | `getCurrentTrain` catch内 | `script.google.com` | `/macros/s/AKfyc.../exec` | positions | n8n走行位置取得失敗時のみ | n8n失敗時のみ | 8000ms | 最大1回 | 既存データとmerge | 前回表示データ維持 | `positions_gas_fallback` / source=`gas` / user_visible=`true` / preload=`false` / fetch_priority=`high` | Direction等が欠けるため既存データとmerge。 |
|
||||
| timetable today | `stateBox/useAllTrainDiagram.tsx` | `getTrainDiagram` | `jr-shikoku-api-data-storage.haruk.in` | `/tmp/diagram-today.json` または `/tmp/diagram-today-beta.json` | provider全体 / timetable consumers | provider mount直後 | 30秒、background継続指定あり。ただしobservedFetch側はbackground開始を抑制 | 15000ms | 最大1回 | AsyncStorage `ALL_TRAIN_DIAGRAM` | cache失敗時はalert | `timetable_today` / source=`static_storage` / user_visible=`false` / preload=`true` / fetch_priority=`medium` | ダイヤ・発車標・列車詳細で広く使用。 |
|
||||
| train operation data | `stateBox/useAllTrainDiagram.tsx` | `getCustomTrainData` | `jr-shikoku-backend-api-v1.haruk.in` または beta | `/train-data` | topMenu LED / positions / train detail / diagram | 必要画面のactivation後。起動時はAsyncStorageの`CUSTOM_TRAIN_DATA`を先に表示し、バックグラウンドで再検証 | activation後30秒、background継続指定あり。ただしobservedFetch側はbackground開始を抑制 | 15000ms | 最大1回 | AsyncStorage `CUSTOM_TRAIN_DATA`(有効期限24時間) | LED/列車情報の既存fallback | `train_operation_data` / source=`backend_api` / user_visible=`false` / preload=`false` / fetch_priority=`medium` | 約1MB級。positions WebViewには別途1日キャッシュがある。 |
|
||||
| operation logs | `stateBox/useAllTrainDiagram.tsx` | `getTodayOperation` | `jr-shikoku-backend-api-v1.haruk.in` または beta | `/operation-logs` | positions / train detail / diagram | 必要画面のactivation後。topMenu単独では取得しない | activation後30秒、background継続指定あり。ただしobservedFetch側はbackground開始を抑制 | 15000ms | 最大1回 | なし | 失敗は握りつぶし | `operation_logs` / source=`backend_api` / user_visible=`false` / preload=`false` / fetch_priority=`medium` | 列車別運行ログ。LED表示には使用しない。WebView内では別途取得される。 |
|
||||
| train operation data | `stateBox/useAllTrainDiagram.tsx` | `getCustomTrainData` | `jr-shikoku-backend-api-v1.haruk.in` または beta | `/train-data` | provider全体 / train detail / diagram | provider mount直後 | 30秒、background継続指定あり。ただしobservedFetch側はbackground開始を抑制 | 15000ms | 最大1回 | なし | 失敗は握りつぶし | `train_operation_data` / source=`backend_api` / user_visible=`false` / preload=`true` / fetch_priority=`medium` | 約1MB級。WebView内でも別途取得される。 |
|
||||
| operation logs | `stateBox/useAllTrainDiagram.tsx` | `getTodayOperation` | `jr-shikoku-backend-api-v1.haruk.in` または beta | `/operation-logs` | provider全体 / topMenu / train detail | provider mount直後 | 30秒、background継続指定あり。ただしobservedFetch側はbackground開始を抑制 | 15000ms | 最大1回 | なし | 失敗は握りつぶし | `operation_logs` / source=`backend_api` / user_visible=`false` / preload=`true` / fetch_priority=`medium` | 列車別運行ログ。WebView内でも別途取得される。 |
|
||||
| operation flag | `stateBox/useAreaInfo.tsx` | `getAreaData` | `n8n.haruk.in` | `/webhook/jr-shikoku-trainfo-flag` | topMenu / information / provider全体 | mount後、InteractionManager後に1200ms遅延 | 60秒 | 10000ms | 最大1回 | なし | 失敗は握りつぶし | `operation_info_flag` / source=`n8n` / user_visible=`true` / preload=`true` / fetch_priority=`medium` | topMenuのバッジ・対象エリア判定。 |
|
||||
| operation text GAS | `stateBox/useAreaInfo.tsx` | `fetchAreaDescription` | `script.google.com` | `/macros/s/AKfycbz80.../exec` | topMenu / information | operation flagで対象駅ありの場合だけ800ms遅延 | flag結果依存 | 15000ms | なし | なし | 失敗は握りつぶし | `operation_info_text` / source=`gas` / user_visible=`true` / preload=`false` / fetch_priority=`medium` | 第1回観測でtimeout。現在は通常時の実行を抑制。 |
|
||||
|
||||
@@ -53,8 +53,8 @@ Sentryで観測している `endpoint=positions / operation_info / train_operati
|
||||
| positions WebView unyohub | `lib/webViewInjectjavascript.ts` | `unyohubDataUpdate` | `jr-shikoku-api-data-storage.haruk.in` | `/thirdparty/unyohub-unyo.json` | positions WebView | 設定ON時、Phase 3 | 30秒 | なし | なし | WebView localStorage 1時間 | 失敗はconsole | 未計測 / 候補 `thirdparty_unyohub` | 鉄道運用Hub由来の集約JSON。 |
|
||||
| positions WebView elesite | `lib/webViewInjectjavascript.ts` | `elesiteDataUpdate` | `jr-shikoku-api-data-storage.haruk.in` | `/thirdparty/elesite-unyo.json` | positions WebView | 設定ON時、Phase 3 | 30秒 | なし | なし | WebView localStorage 1時間 | 失敗はconsole | 未計測 / 候補 `thirdparty_elesite` | えれサイト由来の集約JSON。 |
|
||||
| operation official page | `ndView.tsx` | operation WebView | `www.jr-shikoku.co.jp` | `/info/` | information | 運行情報root初回activate時 | WebView lifecycle依存 | WebView標準 | WebView標準 | OS/WebView依存 | remount watchdog | 未計測 | injected scriptでページ内容を加工し、画像生成結果をpostMessage。 |
|
||||
| operation hidden preload | `AppContainer.tsx` | `HiddenStartupPreloadWebViews` | `www.jr-shikoku.co.jp` | `/info/` | startup hidden preload | 起動時preload条件成立時 | 1回 | WebView標準 | なし | OS/WebView依存 | なし | 未計測 | Sentry breadcrumb/contextのみ。 |
|
||||
| positions hidden preload | `AppContainer.tsx` | `HiddenStartupPreloadWebViews` | `train.jr-shikoku.co.jp` | `/` | startup hidden preload | 起動時preload条件成立時 | 1回 | WebView標準 | なし | OS/WebView依存 | なし | 未計測 | 走行位置WebViewの事前ウォームアップ。 |
|
||||
| operation hidden preload | `Apps.tsx` | `HiddenStartupPreloadWebViews` | `www.jr-shikoku.co.jp` | `/info/` | startup hidden preload | 起動時preload条件成立時 | 1回 | WebView標準 | なし | OS/WebView依存 | なし | 未計測 | Sentry breadcrumb/contextのみ。 |
|
||||
| positions hidden preload | `Apps.tsx` | `HiddenStartupPreloadWebViews` | `train.jr-shikoku.co.jp` | `/` | startup hidden preload | 起動時preload条件成立時 | 1回 | WebView標準 | なし | OS/WebView依存 | なし | 未計測 | 走行位置WebViewの事前ウォームアップ。 |
|
||||
|
||||
## Sentry endpoint対象外だが存在するRN fetch
|
||||
|
||||
@@ -174,14 +174,14 @@ timetable / station diagram / train detail
|
||||
|
||||
### アプリ起動直後
|
||||
|
||||
`useAllTrainDiagram` のproviderはダイヤ本体だけを起動時に取得する。大きい列車補助情報と運行ログは、実際に必要な画面がactivationした後に取得する。`useCurrentTrain` のpollingはroot screenのfocus leaseで制御する。
|
||||
Providerがmountされるため、`useAllTrainDiagram` などのprovider-owned fetchは表示タブに関係なく走り得る。`useCurrentTrain` のpollingだけはroot screenのfocus leaseで制御する。
|
||||
|
||||
| 取得 | 起動時挙動 | 備考 |
|
||||
|---|---|---|
|
||||
| `useCurrentTrain.getCurrentTrain` | positions/topMenuのフォーカス時に即時1回、その後15秒interval | mock ON/OFFで経路が変わる。blur/unmount時にintervalとin-flight requestを停止し、再フォーカス時に再開する。 |
|
||||
| `useAllTrainDiagram.getTrainDiagram` | 即時1回、その後30秒interval | timetable。AsyncStorage fallbackあり。 |
|
||||
| `useAllTrainDiagram.getCustomTrainData` | 起動時にAsyncStorage cacheを読み、必要画面のactivation後に即時1回、その後30秒interval | train_operation。cacheは24時間有効、成功レスポンスを再保存する。 |
|
||||
| `useAllTrainDiagram.getTodayOperation` | positions / train detail / diagramのactivation後に即時1回、その後30秒interval | operation_logs。topMenu単独では取得しない。 |
|
||||
| `useAllTrainDiagram.getCustomTrainData` | 即時1回、その後30秒interval | train_operation。 |
|
||||
| `useAllTrainDiagram.getTodayOperation` | 即時1回、その後30秒interval | operation_info。 |
|
||||
| `useAreaInfo.getAreaData` | InteractionManager後 + 1200ms遅延。その後60秒interval | operation_info flag。対象ありならGAS textも800ms遅延。 |
|
||||
| `useTrainMenu.check-permission` | push token取得後 | Sentry未計測。 |
|
||||
| hidden preload WebViews | preload条件成立時 | positions公式/operation公式を不可視WebViewでロード。 |
|
||||
@@ -191,9 +191,7 @@ timetable / station diagram / train detail
|
||||
| 取得 | 発生源 | 備考 |
|
||||
|---|---|---|
|
||||
| positions RN polling | `useCurrentTrain` provider | positions/topMenuのフォーカス中だけ15秒。blur/unmount時にintervalとin-flight requestを停止する。 |
|
||||
| timetable | `useAllTrainDiagram` provider | 起動時に取得し、30秒ごとに更新。 |
|
||||
| train_operation | `useAllTrainDiagram` provider + 各利用画面 | topMenu LED、positions、列車詳細、ダイヤ画面のactivation後だけ取得し、30秒ごとに更新。 |
|
||||
| operation_logs | `useAllTrainDiagram` provider + 各利用画面 | positions、列車詳細、ダイヤ画面のactivation後だけ取得し、topMenu単独では取得しない。 |
|
||||
| timetable / train_operation / operation_logs | `useAllTrainDiagram` provider | 表示タブに関係なく30秒ごと。 |
|
||||
| operation flag / text | `useAreaInfo` | topMenu表示内容に直結。 |
|
||||
| special train info | `SpecialTrainInfoBox` | 該当Sheet表示時のみ。 |
|
||||
|
||||
@@ -229,12 +227,10 @@ timetable / station diagram / train detail
|
||||
T+0ms app providers mount
|
||||
├─ useCurrentTrain -> positions current fetch
|
||||
├─ useAllTrainDiagram -> timetable fetch
|
||||
├─ useAllTrainDiagram -> train_operation fetch
|
||||
├─ useAllTrainDiagram -> operation_logs fetch
|
||||
└─ useTrainMenu -> settings/env load, maybe check-permission after token
|
||||
|
||||
必要画面のactivation後
|
||||
├─ topMenu LED / positions / train detail / diagram -> train_operation fetch
|
||||
└─ positions / train detail / diagram -> operation_logs fetch
|
||||
|
||||
T+after interactions + 1200ms
|
||||
└─ useAreaInfo -> operation_info_flag
|
||||
└─ if active area exists: +800ms -> operation_info_text GAS
|
||||
|
||||
@@ -16,7 +16,8 @@ NavigationContainer (Apps.tsx)
|
||||
│ ├─ Apps
|
||||
│ ├─ trainbase { info, from? }
|
||||
│ ├─ stDiagram { currentStation }
|
||||
│ ├─ generalWebView { uri, useExitButton?, importRecordingDownloads?, onExit? }
|
||||
│ ├─ howto { info, goTo?, useShow? }
|
||||
│ ├─ generalWebView { uri, useExitButton?, importRecordingDownloads? }
|
||||
│ ├─ news
|
||||
│ ├─ trainMenu
|
||||
│ └─ favoriteList
|
||||
@@ -33,6 +34,7 @@ NavigationContainer (Apps.tsx)
|
||||
│ │ └─ FelicaHistoryPage
|
||||
│ ├─ trainbase
|
||||
│ ├─ AllTrainIDList
|
||||
│ ├─ howto
|
||||
│ └─ generalWebView
|
||||
└─ information
|
||||
└─ TNDView (ndView.tsx)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user