Compare commits
41
Commits
Symlink
+1
@@ -0,0 +1 @@
|
||||
../../.agents/skills/animate-expo
|
||||
+1
@@ -0,0 +1 @@
|
||||
../../.agents/skills/jr-shikoku-architecture
|
||||
+1
@@ -0,0 +1 @@
|
||||
../../.agents/skills/jr-shikoku-data-model
|
||||
@@ -1,28 +0,0 @@
|
||||
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,7 +1,8 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"Sentry": {
|
||||
"type": "http",
|
||||
"url": "https://mcp.sentry.dev/mcp/xprocess-m5/jr-shikoku-unofficial-apps"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -10,7 +10,8 @@
|
||||
"Read(//home/ubuntu/.qwen/debug/**)",
|
||||
"Bash(curl *)",
|
||||
"mcp__sentry__search_events",
|
||||
"mcp__sentry__find_organizations"
|
||||
"mcp__sentry__find_organizations",
|
||||
"Skill(jr-shikoku-architecture)"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,11 @@ Skillの例や推奨と実際のプロジェクト構成が衝突する場合は
|
||||
- 導入元は [emilkowalski/skills](https://github.com/emilkowalski/skills) の `animate-expo`、確認時点の固定コミットは `d23d7f88a2e21c9e4b1418c7abe420f5c1052ba7`(2026-08-29)です。今回導入するSkillは `animate-expo` の2ファイルだけです。
|
||||
- `animate`、`ask-sonner`、`pick-ui-library` など他のSkillは、このリポジトリへ導入済みとは扱いません。
|
||||
|
||||
## Skillの置き場所
|
||||
|
||||
- リポジトリのSkillの正本は `.agents/skills/` です(Codexが読む場所)。
|
||||
- Claude Code向けに、`.claude/skills/` に `.agents/skills/` の各Skillへの相対symlinkを置いています。Skillを編集するときは `.agents/skills/` 側を変更し、Skillを追加したときは `.claude/skills/` にもsymlinkを追加してください。
|
||||
|
||||
## 現在の構成(current valuesは正本から確認)
|
||||
|
||||
- 現行の主要世代は Expo SDK 57、React Native 0.86系、React 19系です。patch/minorをこのファイルへ固定せず、正確な値は `package.json` と `yarn.lock` から確認してください。
|
||||
|
||||
@@ -7,8 +7,15 @@ 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",
|
||||
@@ -35,9 +42,19 @@ if (Platform.OS === "android") {
|
||||
}
|
||||
|
||||
export default wrapWithSentry(function App() {
|
||||
markStartupOnce("app-render", "App render");
|
||||
scheduleStartupReports();
|
||||
useAppLifecycleObservability({ nativeScreensMode, rootTabSceneMode });
|
||||
useAppBootstrap();
|
||||
useStartupDeepLinkNavigation();
|
||||
|
||||
return <AppRootLayout />;
|
||||
React.useEffect(() => {
|
||||
markStartup("App effect committed");
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<AppErrorBoundary scope="root">
|
||||
<AppRootLayout />
|
||||
</AppErrorBoundary>
|
||||
);
|
||||
});
|
||||
|
||||
+207
-118
@@ -18,6 +18,7 @@ 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";
|
||||
@@ -26,10 +27,17 @@ 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");
|
||||
@@ -38,27 +46,24 @@ 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(
|
||||
@@ -74,34 +79,26 @@ const HiddenStartupPreloadWebViews = ({
|
||||
[],
|
||||
);
|
||||
|
||||
const showPositions = shouldPreloadPositions && !positionsLoaded;
|
||||
const showInformation = shouldPreloadInformation && !informationLoaded;
|
||||
|
||||
if (!showPositions && !showInformation) return null;
|
||||
useEffect(() => {
|
||||
markStartupState(
|
||||
"hidden-startup-preload-state",
|
||||
"hidden startup preload state",
|
||||
{
|
||||
showInformation,
|
||||
informationLoaded,
|
||||
},
|
||||
);
|
||||
}, [
|
||||
informationLoaded,
|
||||
showInformation,
|
||||
]);
|
||||
|
||||
if (!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/" }}
|
||||
@@ -109,15 +106,18 @@ 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,6 +143,11 @@ 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,
|
||||
@@ -152,12 +157,19 @@ 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]);
|
||||
|
||||
@@ -165,7 +177,11 @@ const DeferredPositionsRoot = (props: DeferredRootProps) => {
|
||||
return <View style={{ flex: 1 }} />;
|
||||
}
|
||||
|
||||
return <Top />;
|
||||
return (
|
||||
<AppErrorBoundary scope="positions">
|
||||
<Top />
|
||||
</AppErrorBoundary>
|
||||
);
|
||||
};
|
||||
|
||||
const PositionsTabScreen = memo(() => {
|
||||
@@ -186,6 +202,11 @@ 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,
|
||||
@@ -195,12 +216,19 @@ 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]);
|
||||
|
||||
@@ -208,9 +236,19 @@ const DeferredInformationRoot = (props: DeferredRootProps) => {
|
||||
return <View style={{ flex: 1 }} />;
|
||||
}
|
||||
|
||||
return <TNDView />;
|
||||
return (
|
||||
<AppErrorBoundary scope="information">
|
||||
<TNDView />
|
||||
</AppErrorBoundary>
|
||||
);
|
||||
};
|
||||
|
||||
const MenuTabScreen = () => (
|
||||
<AppErrorBoundary scope="topMenu">
|
||||
<MenuPage />
|
||||
</AppErrorBoundary>
|
||||
);
|
||||
|
||||
const InformationTabScreen = memo(() => {
|
||||
const { shouldActivate, isRootFocused } = useContext(
|
||||
InformationRootGateContext,
|
||||
@@ -225,6 +263,7 @@ const InformationTabScreen = memo(() => {
|
||||
InformationTabScreen.displayName = "InformationTabScreen";
|
||||
|
||||
export function AppContainer() {
|
||||
markStartupOnce("app-container-render", "AppContainer render");
|
||||
const controller = useAppContainerController();
|
||||
const {
|
||||
areaInfo,
|
||||
@@ -235,14 +274,47 @@ 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://"],
|
||||
@@ -284,7 +356,7 @@ export function AppContainer() {
|
||||
tabBarIcon: initIcon(icon as any, iconFamily, tabBarBadge, isInfo),
|
||||
},
|
||||
});
|
||||
if (!fontLoaded || !startupIconFontsLoaded || !startupInitialRoute) {
|
||||
if (!fontLoaded || !startupInitialRoute) {
|
||||
return (
|
||||
<View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
|
||||
<ActivityIndicator size="large" />
|
||||
@@ -299,102 +371,119 @@ export function AppContainer() {
|
||||
onReady={controller.handleNavigationReady}
|
||||
onStateChange={controller.handleNavigationStateChange}
|
||||
>
|
||||
<PositionsRootGateContext.Provider
|
||||
<PositionsStartupReadyContext.Provider
|
||||
value={{
|
||||
shouldActivate: hasVisitedPositions,
|
||||
isRootFocused: currentRootRoute === "positions",
|
||||
stackReady: positionsStackReady,
|
||||
webViewLoadStarted: positionsWebViewLoadStarted,
|
||||
initialLoadReady: positionsInitialLoadReady,
|
||||
operationWebViewPreloadPending,
|
||||
operationWebViewLoadStarted,
|
||||
operationWebViewLoadSettled,
|
||||
notifyInitialLoadReady: handlePositionsInitialLoadReady,
|
||||
notifyStackReady: handlePositionsStackReady,
|
||||
notifyWebViewLoadStarted: handlePositionsWebViewLoadStarted,
|
||||
notifyOperationWebViewLoadStarted: handleOperationWebViewLoadStarted,
|
||||
notifyOperationWebViewLoadSettled: handleOperationWebViewLoadSettled,
|
||||
}}
|
||||
>
|
||||
<InformationRootGateContext.Provider
|
||||
<PositionsRootGateContext.Provider
|
||||
value={{
|
||||
shouldActivate: hasVisitedInformation,
|
||||
isRootFocused: currentRootRoute === "information",
|
||||
shouldActivate: hasVisitedPositions,
|
||||
isRootFocused: currentRootRoute === "positions",
|
||||
}}
|
||||
>
|
||||
<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} />
|
||||
),
|
||||
};
|
||||
<InformationRootGateContext.Provider
|
||||
value={{
|
||||
shouldActivate: hasVisitedInformation,
|
||||
isRootFocused: currentRootRoute === "information",
|
||||
}}
|
||||
>
|
||||
<Tab.Screen
|
||||
{...getTabProps(
|
||||
"positions",
|
||||
"走行位置",
|
||||
"bar-chart",
|
||||
"AntDesign",
|
||||
)}
|
||||
component={PositionsTabScreen}
|
||||
listeners={{
|
||||
tabPress: controller.activatePositionsFromTabPress,
|
||||
focus: controller.activatePositionsFromFocus,
|
||||
blur: controller.preservePositionsOnBlur,
|
||||
<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("topMenu", "トップメニュー", "radio", "Ionicons")}
|
||||
component={MenuPage}
|
||||
/>
|
||||
>
|
||||
<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(
|
||||
"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>
|
||||
<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>
|
||||
<HiddenStartupPreloadWebViews
|
||||
shouldPreloadPositions={shouldPreloadPositions}
|
||||
shouldPreloadInformation={shouldPreloadInformation}
|
||||
onInformationLoadStart={handleOperationWebViewLoadStarted}
|
||||
onInformationLoadSettled={handleOperationWebViewLoadSettled}
|
||||
/>
|
||||
</NavigationContainer>
|
||||
);
|
||||
+36
-11
@@ -119,6 +119,7 @@ export default ({ route }) => {
|
||||
uri,
|
||||
useExitButton = true,
|
||||
importRecordingDownloads = false,
|
||||
onExit,
|
||||
} = route.params;
|
||||
const { goBack } = useNavigation();
|
||||
const { fixed } = useThemeColors();
|
||||
@@ -132,6 +133,8 @@ 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);
|
||||
@@ -148,6 +151,11 @@ 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;
|
||||
@@ -172,11 +180,12 @@ export default ({ route }) => {
|
||||
const nextUri = importRecordingDownloads
|
||||
? rawUri
|
||||
: rewriteJrDataSystemUrl(rawUri, nextEnvironment);
|
||||
setResolvedUri(
|
||||
importRecordingDownloads
|
||||
? nextUri
|
||||
: withJrDataSystemIconQuery(nextUri, iconSetting),
|
||||
);
|
||||
const initialUri = importRecordingDownloads
|
||||
? nextUri
|
||||
: withJrDataSystemIconQuery(nextUri, iconSetting);
|
||||
resolvedUriRef.current = initialUri;
|
||||
setResolvedUri(initialUri);
|
||||
setWebViewSourceUri(initialUri);
|
||||
setIsEnvironmentReady(true);
|
||||
};
|
||||
|
||||
@@ -190,6 +199,9 @@ export default ({ route }) => {
|
||||
}, [iconSetting, uri, importRecordingDownloads]);
|
||||
|
||||
const handleReload = () => {
|
||||
if (resolvedUriRef.current) {
|
||||
setWebViewSourceUri(resolvedUriRef.current);
|
||||
}
|
||||
lastPongAt.current = Date.now();
|
||||
setHasError(false);
|
||||
hasErrorRef.current = false;
|
||||
@@ -219,7 +231,9 @@ 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;
|
||||
@@ -233,6 +247,14 @@ 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 {
|
||||
@@ -277,20 +299,20 @@ export default ({ route }) => {
|
||||
if (canGoBack) {
|
||||
if (handleWebViewBack()) return true;
|
||||
}
|
||||
goBack();
|
||||
handleExit();
|
||||
return true;
|
||||
};
|
||||
|
||||
const subscription = BackHandler.addEventListener("hardwareBackPress", onHardwareBack);
|
||||
return () => subscription.remove();
|
||||
}, [canGoBack, goBack, handleWebViewBack])
|
||||
}, [canGoBack, handleExit, handleWebViewBack])
|
||||
);
|
||||
return (
|
||||
<View style={{ height: "100%", backgroundColor: fixed.primary }}>
|
||||
{isEnvironmentReady && (
|
||||
<WebView
|
||||
key={webViewKey}
|
||||
source={{ uri: resolvedUri }}
|
||||
source={{ uri: webViewSourceUri }}
|
||||
contentMode="mobile"
|
||||
allowsBackForwardNavigationGestures
|
||||
setSupportMultipleWindows={false}
|
||||
@@ -354,7 +376,9 @@ export default ({ route }) => {
|
||||
iconSetting,
|
||||
);
|
||||
if (rewrittenUrl !== request.url) {
|
||||
resolvedUriRef.current = rewrittenUrl;
|
||||
setResolvedUri(rewrittenUrl);
|
||||
setWebViewSourceUri(rewrittenUrl);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -375,6 +399,7 @@ 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);
|
||||
@@ -448,7 +473,7 @@ export default ({ route }) => {
|
||||
return;
|
||||
}
|
||||
if (type === "back") return handleWebViewBack();
|
||||
if (type === "windowClose") return goBack();
|
||||
if (type === "windowClose") return handleExit();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
@@ -466,13 +491,13 @@ export default ({ route }) => {
|
||||
<Text style={wvStyles.reloadButtonText}>再読み込み</Text>
|
||||
</TouchableOpacity>
|
||||
{useExitButton && (
|
||||
<TouchableOpacity style={wvStyles.backButton} onPress={goBack}>
|
||||
<TouchableOpacity style={wvStyles.backButton} onPress={handleExit}>
|
||||
<Text style={wvStyles.backButtonText}>閉じる</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
{useExitButton && !hasError && <BigButton onPress={goBack} string="閉じる" />}
|
||||
{useExitButton && !hasError && <BigButton onPress={handleExit} string="閉じる" />}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
+38
-6
@@ -8,7 +8,6 @@ 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";
|
||||
@@ -22,6 +21,11 @@ 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,
|
||||
@@ -31,6 +35,7 @@ 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();
|
||||
@@ -42,6 +47,7 @@ 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");
|
||||
@@ -49,6 +55,9 @@ 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: {
|
||||
@@ -60,16 +69,40 @@ export function MenuPage() {
|
||||
|
||||
useEffect(() => {
|
||||
//ニュース表示
|
||||
markStartup("top menu startup storage checks start");
|
||||
AS.getItem(STORAGE_KEYS.NEWS_STATUS)
|
||||
.then((d) => {
|
||||
if (d !== news) navigation.navigate("topMenu", { screen: "news" });
|
||||
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" });
|
||||
}
|
||||
})
|
||||
.catch(() => 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" });
|
||||
});
|
||||
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) => logger.error("Error fetching icon setting:", error));
|
||||
.catch((error) => {
|
||||
markStartup("top menu icon setting read error");
|
||||
logger.error("Error fetching icon setting:", error);
|
||||
});
|
||||
|
||||
return undefined;
|
||||
}, []);
|
||||
@@ -122,7 +155,7 @@ export function MenuPage() {
|
||||
}
|
||||
}
|
||||
scrollRef.current?.scrollTo({
|
||||
y: mapHeightRef.current - verticalScale(80),
|
||||
y: Math.max(0, mapHeightRef.current - verticalScale(80)),
|
||||
animated: true,
|
||||
});
|
||||
setMapMode(false);
|
||||
@@ -209,7 +242,6 @@ export function MenuPage() {
|
||||
options={{ ...optionData, gestureEnabled: false }}
|
||||
component={AllTrainDiagramView}
|
||||
/>
|
||||
<Stack.Screen name="howto" options={optionData} component={HowTo} />
|
||||
<Stack.Screen
|
||||
name="generalWebView"
|
||||
options={optionData}
|
||||
|
||||
@@ -1,73 +1,109 @@
|
||||
# JR四国 列車位置情報アプリ
|
||||
|
||||
JR四国の列車リアルタイム位置情報を表示するReact Nativeアプリケーション。
|
||||
JR四国の列車リアルタイム位置情報を表示するReact Nativeアプリケーション(非公式)。
|
||||
|
||||
## 🚀 技術スタック
|
||||
|
||||
- **フレームワーク**: React Native (Expo SDK 52)
|
||||
> 正確なバージョンは宣言値を `package.json`、解決値を `yarn.lock` で確認してください。
|
||||
|
||||
- **フレームワーク**: Expo SDK 57 / React Native 0.86系 / React 19系(New Architecture)
|
||||
- **言語**: TypeScript / JavaScript
|
||||
- **状態管理**: React Context API + カスタムフック
|
||||
- **ストレージ**: AsyncStorage
|
||||
- **地図表示**: WebView + JavaScript Injection
|
||||
- **ナビゲーション**: 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
|
||||
- **日付処理**: dayjs
|
||||
- **アニメーション**: react-native-reanimated
|
||||
- **アニメーション / ジェスチャー**: react-native-reanimated、react-native-gesture-handler(v2系)
|
||||
- **監視**: Sentry
|
||||
- **ウィジェット**: react-native-android-widget(Android)、`@bacons/apple-targets` によるWidgetKit拡張(iOS)
|
||||
|
||||
## 📦 プロジェクト構造
|
||||
|
||||
```
|
||||
jrshikoku/
|
||||
├── components/ # Reactコンポーネント
|
||||
│ ├── Apps/ # アプリケーション機能
|
||||
│ ├── Apps/ # 走行位置画面
|
||||
│ │ └── FixedPositionBox/
|
||||
│ │ └── hooks/ # カスタムフック
|
||||
│ ├── AndroidWidget/ # Androidウィジェット
|
||||
│ ├── ActionSheetComponents/
|
||||
│ ├── atom/ # 基本UIコンポーネント
|
||||
│ ├── Menu/ # メニュー関連
|
||||
│ ├── Settings/ # 設定画面
|
||||
│ └── TrainMenu/ # 列車メニュー
|
||||
├── stateBox/ # グローバル状態管理
|
||||
├── stateBox/ # グローバル状態管理(Context + hooks)
|
||||
├── lib/ # ユーティリティライブラリ
|
||||
│ └── eachTrainInfoCoreLib/ # 列車情報処理
|
||||
│ ├── domain/ # 駅・列車identityのdomain型
|
||||
│ ├── eachTrainInfoCoreLib/ # 列車情報処理
|
||||
│ ├── observability/ # Sentry・テレメトリ
|
||||
│ ├── theme/ # カラーテーマ
|
||||
│ └── webview/ # WebView注入用データ
|
||||
├── 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 18以上
|
||||
- npm または yarn
|
||||
- Expo CLI
|
||||
- Node.js(Expo SDK 57が対応するLTS。`yarn expo:doctor` で確認)
|
||||
- Yarn(lockfileは `yarn.lock` のみ)
|
||||
- EAS CLI(ビルドする場合)
|
||||
|
||||
### インストール
|
||||
### インストールと起動
|
||||
|
||||
```bash
|
||||
# 依存関係のインストール
|
||||
npm install
|
||||
# 依存関係のインストール(postinstallでpatch-packageが実行されます)
|
||||
yarn install
|
||||
|
||||
# 開発サーバー起動
|
||||
npm start
|
||||
# 開発サーバー起動(dev client)
|
||||
yarn start
|
||||
|
||||
# iOS実行
|
||||
npm run ios
|
||||
# ネイティブビルドして実行
|
||||
yarn ios
|
||||
yarn android
|
||||
```
|
||||
|
||||
# Android実行
|
||||
npm run android
|
||||
### 検証
|
||||
|
||||
# Web実行
|
||||
npm run web
|
||||
```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 を更新
|
||||
```
|
||||
|
||||
## 📝 開発ガイドライン
|
||||
|
||||
開発エージェント向けの詳細なルールは [AGENTS.md](./AGENTS.md)、domain契約は [docs/architecture/](./docs/architecture/) を参照してください。
|
||||
|
||||
### 定数の使用
|
||||
|
||||
```typescript
|
||||
@@ -120,18 +156,31 @@ AsyncStorage.getItem(STORAGE_KEYS.BUS_AND_TRAIN);
|
||||
|
||||
### 列車位置追跡
|
||||
- リアルタイム列車位置表示
|
||||
- 遅延情報の表示
|
||||
- 遅延情報の表示(遅延速報EX)
|
||||
- 次駅・着駅の予測
|
||||
- 列車追従・駅固定モード(iOSではLive Activitiesに対応)
|
||||
- 走行位置の記録と再生
|
||||
|
||||
### 駅情報
|
||||
- 時刻表表示
|
||||
- 駅詳細情報
|
||||
- お気に入り駅登録
|
||||
- お気に入り駅登録・並び替え
|
||||
|
||||
### カスタマイズ
|
||||
- アイコン設定
|
||||
- レイアウト設定
|
||||
- 通知設定
|
||||
### 運行情報・通知
|
||||
- JR四国の運行情報表示
|
||||
- プッシュ通知(遅延速報EX、運行情報など)
|
||||
- サウンドによる案内(β)
|
||||
|
||||
### ICカード
|
||||
- FeliCa対応ICカードの残高・履歴読み取り
|
||||
|
||||
### カスタマイズ(設定画面)
|
||||
- 起動時に表示する画面、レイアウト設定
|
||||
- カラーテーマ(ライト / 端末依存 / ダーク)
|
||||
- アイコン設定、ランチャーアイコン変更
|
||||
- サウンド設定(β)、運行情報設定(β)
|
||||
- 通知設定、情報ソース設定、接続ステータス
|
||||
- ウィジェット設定(Android)
|
||||
|
||||
## 🔧 カスタムフック
|
||||
|
||||
@@ -144,14 +193,22 @@ AsyncStorage.getItem(STORAGE_KEYS.BUS_AND_TRAIN);
|
||||
- `useTrainDataWithThrough`: 通過駅を含む列車データ生成
|
||||
- `useDestinationStation`: 行先駅データの管理
|
||||
|
||||
### グローバル状態フック
|
||||
### グローバル状態フック(`stateBox/`)
|
||||
|
||||
- `useAllTrainDiagram`: 全列車ダイヤデータ
|
||||
- `useAreaInfo`: 運行情報スナップショット(エリア情報)
|
||||
- `useBusAndTrainData`: バス・列車データ
|
||||
- `useCurrentTrain`: 現在選択中の列車
|
||||
- `useCurrentTrain`: 現在の列車位置データ
|
||||
- `useDeviceOrientationChange`: 画面の向き
|
||||
- `useElesite`: えれサイト運用データ
|
||||
- `useFavoriteStation`: お気に入り駅
|
||||
- `useNotifications`: プッシュ通知と通知タップ時のルーティング
|
||||
- `useStationList`: 全駅リスト
|
||||
- `useTopMenu`: トップメニュー状態
|
||||
- `useTrainDelayData`: 遅延情報
|
||||
- `useTrainMenu`: 列車メニュー状態
|
||||
- `useUnyohub`: 鉄道運用Hub運用データ
|
||||
- `useUserPosition`: ユーザーの現在地
|
||||
|
||||
## 📊 リファクタリング実績
|
||||
|
||||
@@ -165,17 +222,24 @@ AsyncStorage.getItem(STORAGE_KEYS.BUS_AND_TRAIN);
|
||||
|
||||
## 🚦 API エンドポイント
|
||||
|
||||
エンドポイントは `constants/api.ts` の `API_ENDPOINTS` に集約しています。一覧と各URLは同ファイルを参照してください。
|
||||
|
||||
```typescript
|
||||
// constants/api.ts
|
||||
// constants/api.ts(抜粋)
|
||||
const BASE_URL = 'https://jr-shikoku-api-data-storage.haruk.in';
|
||||
|
||||
export const API_ENDPOINTS = {
|
||||
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個
|
||||
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`,
|
||||
// ...
|
||||
};
|
||||
```
|
||||
|
||||
## � 外部からのアプリ起動(ディープリンク)
|
||||
データ取得の全体像は [docs/data-fetch-map.md](./docs/data-fetch-map.md) を参照してください。
|
||||
|
||||
## 🔗 外部からのアプリ起動(ディープリンク)
|
||||
|
||||
本アプリは複数の方法で外部から起動・画面遷移できます。
|
||||
|
||||
@@ -185,14 +249,14 @@ export const API_ENDPOINTS = {
|
||||
|
||||
| URL | 遷移先 |
|
||||
|---|---|
|
||||
| `jrshikoku://open/felica` | FeliCa履歴ページ |
|
||||
| `jrshikoku://open/felica`(`felicahistorypage` も可) | FeliCa履歴ページ |
|
||||
| `jrshikoku://open/traininfo` | 遅延速報EX |
|
||||
| `jrshikoku://open/operation` | 運行情報 |
|
||||
| `jrshikoku://open/settings` | 設定 |
|
||||
| `jrshikoku://open/topmenu` | トップメニュー |
|
||||
| `jrshikoku://positions/apps` | 走行位置 |
|
||||
|
||||
URLの処理は `App.tsx` の `routeFromUrl` で実装されています。
|
||||
URLパターンの定義は `lib/startupDeepLink.ts`、遷移処理は `lib/useStartupDeepLinkNavigation.ts` の `routeFromUrl` で実装されています。
|
||||
|
||||
### Androidウィジェット
|
||||
|
||||
@@ -204,15 +268,32 @@ URLの処理は `App.tsx` の `routeFromUrl` で実装されています。
|
||||
| `JR_shikoku_apps_shortcut` | クイックアクセス(各機能へのショートカットタイル) |
|
||||
| `JR_shikoku_felica_balance` | ICカード残高表示 |
|
||||
| `JR_shikoku_info` | 運行情報 |
|
||||
| `JR_shikoku_train_strange` | 怪レい列車 |
|
||||
|
||||
ウィジェットのタップ時は `jrshikoku://` スキームでアプリ内画面へ遷移します。
|
||||
実装: `components/AndroidWidget/widget-task-handler.tsx`
|
||||
配置可能なウィジェットは `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`)は、ウィジェットとは別に実装されています。
|
||||
|
||||
### iOSウィジェット(WidgetKit)
|
||||
|
||||
WidgetKit拡張としてホーム画面ウィジェットを提供しています。Live Activities にも対応しています。
|
||||
設定: `targets/widget/Info.plist`
|
||||
WidgetKit拡張として、次のホーム画面ウィジェットを提供しています。
|
||||
|
||||
- ショートカット
|
||||
- ICカード残高
|
||||
- 遅延速報
|
||||
- 運行情報
|
||||
|
||||
Live Activitiesは、駅固定(StationLock)と列車追従(TrainFollow)に対応しています。
|
||||
実装: `targets/widget/`
|
||||
|
||||
### プッシュ通知
|
||||
|
||||
@@ -228,30 +309,40 @@ WidgetKit拡張としてホーム画面ウィジェットを提供していま
|
||||
|
||||
### NFC(FeliCa)
|
||||
|
||||
NFC-Fを利用した交通系ICカードの読み取りに対応しています(Android)。アプリ内からスキャンを開始する形式です。
|
||||
実装: `modules/expo-felica-reader/`
|
||||
NFC-Fを利用した交通系ICカードの読み取りに、iOS・Androidとも対応しています。アプリ内からスキャンを開始する形式です。
|
||||
実装: `modules/expo-felica-reader/`([7nohe/expo-felica-reader](https://github.com/7nohe/expo-felica-reader) をもとに改変、MIT)
|
||||
|
||||
## �📱 ビルド
|
||||
## 📱 ビルド
|
||||
|
||||
EASのprofileとchannelは `eas.json` で管理しています。主なprofileは次のとおりです。
|
||||
|
||||
| profile | 用途 |
|
||||
|---|---|
|
||||
| `development` | dev client(internal配布) |
|
||||
| `preview` | 内部配布 |
|
||||
| `production<version>` / `beta<version>` | バージョン別の本番・ベータ(例: `production7.2`、`beta7.2`) |
|
||||
|
||||
```bash
|
||||
# APKビルド(Android)
|
||||
eas build --platform android
|
||||
|
||||
# IPAビルド(iOS)
|
||||
eas build --platform ios
|
||||
eas build --platform android --profile <profile>
|
||||
eas build --platform ios --profile <profile>
|
||||
```
|
||||
|
||||
## 🤝 貢献
|
||||
|
||||
プルリクエストを歓迎します。大きな変更を行う場合は、まずissueを開いて変更内容を議論してください。
|
||||
runtimeVersionの方針は [docs/architecture/adr/001-runtime-version-policy.md](./docs/architecture/adr/001-runtime-version-policy.md) を参照してください。
|
||||
ネイティブ設定(`app.json` のプラグイン、permission、entitlement、ローカルモジュールなど)を変更した場合は、OTAではなく新しいビルドが必要です。
|
||||
|
||||
## 📄 ライセンス
|
||||
|
||||
[ライセンス情報をここに記載]
|
||||
本リポジトリは非公開です。無断での複製・再配布を禁止します(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,33 +5,56 @@ 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, Linking, Platform, View } from "react-native";
|
||||
import { InteractionManager, 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);
|
||||
@@ -54,6 +77,7 @@ export const Top = () => {
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
markStartupOnce("positions-stack-mounted", "positions stack mounted");
|
||||
positionsStackTelemetry.info("positions stack mounted");
|
||||
return () => {
|
||||
positionsStackTelemetry.info("positions stack unmounted");
|
||||
@@ -62,6 +86,9 @@ export const Top = () => {
|
||||
|
||||
useEffect(() => {
|
||||
if (hasActivatedStack) return;
|
||||
markStartup("positions stack activation scheduled", {
|
||||
focused: isTabFocused,
|
||||
});
|
||||
positionsStackTelemetry.breadcrumb({
|
||||
message: "positions stack activation scheduled",
|
||||
data: {
|
||||
@@ -71,6 +98,10 @@ 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: {
|
||||
@@ -78,6 +109,7 @@ export const Top = () => {
|
||||
},
|
||||
});
|
||||
setHasActivatedStack(true);
|
||||
notifyStackReady();
|
||||
};
|
||||
|
||||
if (isTabFocused) {
|
||||
@@ -95,9 +127,13 @@ export const Top = () => {
|
||||
cancelled = true;
|
||||
clearTimeout(timer);
|
||||
};
|
||||
}, [hasActivatedStack, isTabFocused]);
|
||||
}, [hasActivatedStack, isTabFocused, notifyStackReady]);
|
||||
|
||||
useEffect(() => {
|
||||
markStartupState("positions-stack-gate", "positions stack gate state", {
|
||||
focused: isTabFocused,
|
||||
activated: hasActivatedStack,
|
||||
});
|
||||
telemetry.context("positions_stack", {
|
||||
focused: isTabFocused,
|
||||
activated: hasActivatedStack,
|
||||
@@ -131,11 +167,6 @@ 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({
|
||||
@@ -239,7 +270,6 @@ export const Top = () => {
|
||||
options={{ ...optionData, gestureEnabled: false }}
|
||||
component={StationDiagramView}
|
||||
/>
|
||||
<Stack.Screen name="howto" options={optionData} component={HowTo} />
|
||||
<Stack.Screen
|
||||
name="generalWebView"
|
||||
options={optionData}
|
||||
|
||||
@@ -5,8 +5,7 @@
|
||||
"scheme": "jrshikoku",
|
||||
"platforms": [
|
||||
"ios",
|
||||
"android",
|
||||
"web"
|
||||
"android"
|
||||
],
|
||||
"version": "7.2",
|
||||
"userInterfaceStyle": "automatic",
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -9,6 +9,7 @@ export const EachTrainInfo = ({ payload }: SheetProps<"EachTrainInfo">) => {
|
||||
const actionSheetRef = useRef(null);
|
||||
const maxHeight = useSheetMaxHeight();
|
||||
const [sheetOpened, setSheetOpened] = useState(false);
|
||||
|
||||
useCurrentTrainPolling(sheetOpened);
|
||||
|
||||
const handleOpen = () => {
|
||||
|
||||
@@ -32,7 +32,6 @@ 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 { View, Text, TouchableWithoutFeedback } from "react-native";
|
||||
import dayjs from "dayjs";
|
||||
import { Platform, View, Text, TouchableWithoutFeedback } from "react-native";
|
||||
import type { Dayjs } from "dayjs";
|
||||
import { setClockTime } from "@/lib/timeUtils";
|
||||
import lineColorList from "../../../assets/originData/lineColorList";
|
||||
import { trainDataType } from "@/lib/trainPositionTextArray";
|
||||
import lineColorList from "@/assets/originData/lineColorList";
|
||||
import type { trainDataType } from "@/lib/trainPositionTextArray";
|
||||
import { getStopListColors } from "./colorScheme";
|
||||
import {
|
||||
isCanceledSe,
|
||||
@@ -12,90 +12,89 @@ import {
|
||||
parseSeString,
|
||||
} from "@/utils/seUtils";
|
||||
import type { SeTypes } from "@/types";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
import { useResponsive } from "@/lib/responsive";
|
||||
|
||||
type seTypes =
|
||||
| "発編"
|
||||
| "着編"
|
||||
| "通編"
|
||||
| "頃編"
|
||||
| "発"
|
||||
| "着"
|
||||
| "休編"
|
||||
| "休発"
|
||||
| "休着"
|
||||
| "休発編"
|
||||
| "休着編"
|
||||
| "通休編"
|
||||
| "通発休編"
|
||||
| "通着休編"
|
||||
| string;
|
||||
type TrainDelay = trainDataType["delay"];
|
||||
|
||||
type currentTrainDataType = {
|
||||
Index: number;
|
||||
num: string;
|
||||
delay: "入線" | number | undefined;
|
||||
Pos: string;
|
||||
PosNum: string | number;
|
||||
Direction: number;
|
||||
Type: string;
|
||||
Line: 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 StationInfo = { StationName: string; StationNumber: string | null };
|
||||
|
||||
type props = {
|
||||
i: string;
|
||||
stationNumberMap: ReadonlyMap<string, readonly string[]>;
|
||||
parsedTrainData: readonly EachStopData[];
|
||||
clockBase: Dayjs;
|
||||
borderColor: string;
|
||||
textOnPrimary: string;
|
||||
isDark: boolean;
|
||||
trainDelay: TrainDelay;
|
||||
fontScale: ResponsiveScale["fontScale"];
|
||||
moderateScale: ResponsiveScale["moderateScale"];
|
||||
index: number;
|
||||
stationList: StationInfo[][];
|
||||
points: boolean;
|
||||
currentTrainData?: currentTrainDataType;
|
||||
openStationACFromEachTrainInfo?: (station: string) => void;
|
||||
showThrew: boolean;
|
||||
array: string[];
|
||||
isNotService?: boolean;
|
||||
};
|
||||
export const EachStopList: FC<props> = ({
|
||||
i,
|
||||
const EachStopListComponent: FC<props> = ({
|
||||
stationNumberMap,
|
||||
parsedTrainData,
|
||||
clockBase,
|
||||
borderColor,
|
||||
textOnPrimary,
|
||||
isDark,
|
||||
trainDelay,
|
||||
fontScale,
|
||||
moderateScale,
|
||||
index,
|
||||
stationList,
|
||||
points,
|
||||
currentTrainData,
|
||||
openStationACFromEachTrainInfo,
|
||||
showThrew,
|
||||
array,
|
||||
isNotService = false,
|
||||
}) => {
|
||||
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;
|
||||
// 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;
|
||||
// 発(通常発・休発・休発編)の場合、前の着(通常着・休着・休着編)と統合する
|
||||
if (se.includes("発")) {
|
||||
if (index > 0) {
|
||||
const beforeData = array[index - 1].split(",") as [string, seTypes, string];
|
||||
const beforeData = parsedTrainData[index - 1];
|
||||
// 前が着(通常着でも休着でも)の場合は統合
|
||||
if (beforeData[0] == station && beforeData[1].includes("着")) {
|
||||
if (beforeData && beforeData[0] == station && beforeData[1].includes("着")) {
|
||||
beforeSameStationData = beforeData;
|
||||
}
|
||||
}
|
||||
}
|
||||
let afterSameStationData = null;
|
||||
let afterSameStationData: EachStopData | null = null;
|
||||
// 着(通常着・休着・休着編)の場合、次の発(通常発・休発・休発編)と統合される(非表示)
|
||||
if (se.includes("着")) {
|
||||
const afterData = array[index + 1]?.split(",") as [string, seTypes, string];
|
||||
const afterData = parsedTrainData[index + 1];
|
||||
// 次が発(通常発でも休発でも)なら、この着を非表示にして次の発で両方表示
|
||||
if (afterData && afterData[0] == station && afterData[1].includes("発")) {
|
||||
afterSameStationData = afterData;
|
||||
return <></>;
|
||||
}
|
||||
}
|
||||
if(station.includes(".")){
|
||||
if (station.includes(".")) {
|
||||
return <></>;
|
||||
}
|
||||
if (!showThrew) {
|
||||
@@ -107,25 +106,11 @@ export const EachStopList: FC<props> = ({
|
||||
if (se == "通発休編") return null;
|
||||
if (se == "通着休編") return null;
|
||||
}
|
||||
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 StationNumbers = stationNumberMap.get(station) ?? [];
|
||||
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
|
||||
@@ -141,9 +126,7 @@ export const EachStopList: FC<props> = ({
|
||||
const isCanceled = isCanceledSe(se);
|
||||
const isCommunity = isCommunitySe(se);
|
||||
const isDelayed =
|
||||
currentTrainData?.delay !== undefined &&
|
||||
currentTrainData?.delay !== "入線" &&
|
||||
currentTrainData?.delay > 0;
|
||||
trainDelay !== undefined && trainDelay !== "入線" && trainDelay > 0;
|
||||
|
||||
const colors = getStopListColors(
|
||||
isThrough,
|
||||
@@ -151,7 +134,7 @@ export const EachStopList: FC<props> = ({
|
||||
isCanceled,
|
||||
isDelayed,
|
||||
isNotService,
|
||||
isDark
|
||||
isDark,
|
||||
);
|
||||
// 打ち消し線用の通常色(遅延していない時の色)
|
||||
const normalColors = getStopListColors(
|
||||
@@ -160,7 +143,7 @@ export const EachStopList: FC<props> = ({
|
||||
isCanceled,
|
||||
false,
|
||||
isNotService,
|
||||
isDark
|
||||
isDark,
|
||||
);
|
||||
|
||||
// beforeSameStationData用の色設定
|
||||
@@ -179,7 +162,7 @@ export const EachStopList: FC<props> = ({
|
||||
isCanceled,
|
||||
isDelayed,
|
||||
isNotService,
|
||||
isDark
|
||||
isDark,
|
||||
);
|
||||
const beforeNormalColors = getStopListColors(
|
||||
beforeIsThrough,
|
||||
@@ -187,19 +170,47 @@ export const EachStopList: 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={{
|
||||
@@ -214,50 +225,56 @@ export const EachStopList: FC<props> = ({
|
||||
position: "relative",
|
||||
marginHorizontal: 15,
|
||||
flexDirection: "row",
|
||||
height: "101%",
|
||||
alignSelf: "stretch",
|
||||
}}
|
||||
>
|
||||
{StationNumbers.map((stn, index) => (
|
||||
<StationNumbersBox stn={stn} se={se} key={index} />
|
||||
))}
|
||||
>
|
||||
{StationNumbers.map((stn) => (
|
||||
<StationNumbersBox
|
||||
key={stn}
|
||||
stn={stn}
|
||||
se={se}
|
||||
fontScale={fontScale}
|
||||
textColor={textOnPrimary}
|
||||
/>
|
||||
))}
|
||||
</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>
|
||||
{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>
|
||||
<View style={{ flex: 1 }} />
|
||||
</View>
|
||||
)}
|
||||
<View
|
||||
style={{
|
||||
padding: 8,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: themeColors.borderLight,
|
||||
borderBottomColor: borderColor,
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
<View style={{ flexDirection: "column" }}>
|
||||
<View style={{ flex: 1 }} />
|
||||
<Text
|
||||
style={{
|
||||
fontSize: fontScale(20),
|
||||
@@ -278,64 +295,55 @@ export const EachStopList: FC<props> = ({
|
||||
{platformDisplay}
|
||||
</Text>
|
||||
)}
|
||||
</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>
|
||||
</Text>
|
||||
{hasDoubleTime ? (
|
||||
<View style={{ flexDirection: "column", alignItems: "flex-end" }}>
|
||||
{beforeTimeText}
|
||||
{mainTimeText}
|
||||
</View>
|
||||
) : (
|
||||
mainTimeText
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
);
|
||||
};
|
||||
|
||||
const TimeText: FC<{
|
||||
export const EachStopList = React.memo(EachStopListComponent);
|
||||
|
||||
const TimeTextComponent: FC<{
|
||||
isBefore?: boolean;
|
||||
isDouble: boolean;
|
||||
currentTrainData: trainDataType;
|
||||
delay: TrainDelay;
|
||||
se: string;
|
||||
time: string;
|
||||
textColor: string;
|
||||
normalTextColor: string;
|
||||
}> = ({
|
||||
clockBase: Dayjs;
|
||||
} & ResponsiveScale> = ({
|
||||
isDouble,
|
||||
currentTrainData,
|
||||
delay,
|
||||
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 }}>
|
||||
{!!currentTrainData?.delay &&
|
||||
currentTrainData?.delay != "入線" &&
|
||||
currentTrainData?.delay != 0 && (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
opacity: isBefore ? 0.5 : 1,
|
||||
}}
|
||||
>
|
||||
{time !== "" && !!delay && delay != "入線" && delay != 0 && (
|
||||
<Text
|
||||
style={{
|
||||
fontSize: fontScale(isBefore ? 14 : 15),
|
||||
@@ -354,12 +362,15 @@ const TimeText: FC<{
|
||||
)}
|
||||
<StationTimeBox
|
||||
isBefore={isBefore}
|
||||
delay={currentTrainData?.delay}
|
||||
delay={delay}
|
||||
textColor={textColor}
|
||||
seType={seType}
|
||||
se={se}
|
||||
time={time}
|
||||
isDouble={isDouble}
|
||||
clockBase={clockBase}
|
||||
fontScale={fontScale}
|
||||
moderateScale={moderateScale}
|
||||
/>
|
||||
<Text
|
||||
style={{
|
||||
@@ -375,10 +386,15 @@ const TimeText: FC<{
|
||||
</View>
|
||||
);
|
||||
};
|
||||
const StationNumbersBox: FC<{ stn: string; se: seTypes }> = (props) => {
|
||||
const { stn, se } = props;
|
||||
const { fixed } = useThemeColors();
|
||||
const { fontScale } = useResponsive();
|
||||
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 lineColor = lineColorList[stn.charAt(0)];
|
||||
const hasThrew =
|
||||
se == "通過" ||
|
||||
@@ -391,44 +407,74 @@ const StationNumbersBox: FC<{ stn: string; se: seTypes }> = (props) => {
|
||||
? "80"
|
||||
: "";
|
||||
const backgroundColor = `${lineColor}${hasThrew}`;
|
||||
return (
|
||||
<View style={{ backgroundColor, flex: 1 }} key={stn}>
|
||||
<View style={{ flex: 1 }} />
|
||||
<Text
|
||||
|
||||
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
|
||||
style={{
|
||||
color: fixed.textOnPrimary,
|
||||
textAlign: "center",
|
||||
fontSize: fontScale(10),
|
||||
fontWeight: "bold",
|
||||
backgroundColor,
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
{stn.charAt(0)}
|
||||
{"\n"}
|
||||
{stn.slice(1)}
|
||||
</Text>
|
||||
<View style={{ flex: 1 }} />
|
||||
</View>
|
||||
<Text style={stationNumberTextStyle}>{stationNumberLabel}</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Text
|
||||
style={{
|
||||
...stationNumberTextStyle,
|
||||
backgroundColor,
|
||||
flex: 1,
|
||||
textAlignVertical: "center",
|
||||
}}
|
||||
>
|
||||
{stationNumberLabel}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
type StationTimeBoxType = {
|
||||
isBefore?: boolean;
|
||||
delay: "入線" | number | undefined;
|
||||
delay: TrainDelay;
|
||||
textColor: string;
|
||||
seType: seTypes;
|
||||
seType: SeTypes;
|
||||
se: string;
|
||||
time: string;
|
||||
isDouble: boolean;
|
||||
};
|
||||
clockBase: Dayjs;
|
||||
} & ResponsiveScale;
|
||||
|
||||
const StationTimeBox: FC<StationTimeBoxType> = (props) => {
|
||||
const { delay, textColor, seType, se, time, isDouble, isBefore } = props;
|
||||
const { fontScale, moderateScale } = useResponsive();
|
||||
const dates = setClockTime(
|
||||
dayjs(),
|
||||
const StationNumbersBox = React.memo(StationNumbersBoxComponent);
|
||||
|
||||
const StationTimeBoxComponent: FC<StationTimeBoxType> = (props) => {
|
||||
const {
|
||||
delay,
|
||||
textColor,
|
||||
seType,
|
||||
se,
|
||||
time,
|
||||
delay == "入線" || delay == undefined ? 0 : delay
|
||||
);
|
||||
isDouble,
|
||||
isBefore,
|
||||
clockBase,
|
||||
fontScale,
|
||||
moderateScale,
|
||||
} = props;
|
||||
const timeData = setClockTime(
|
||||
clockBase,
|
||||
time,
|
||||
delay == "入線" || delay == undefined ? 0 : delay,
|
||||
)?.format("HH:mm") ?? null;
|
||||
|
||||
return (
|
||||
<Text
|
||||
@@ -440,7 +486,9 @@ const StationTimeBox: FC<StationTimeBoxType> = (props) => {
|
||||
fontStyle: seType == "community" ? "italic" : "normal",
|
||||
}}
|
||||
>
|
||||
{se.includes("通") && time == "" ? "レ" : dates?.format("HH:mm") ?? ""}
|
||||
{se.includes("通") && time == "" ? "レ" : (timeData ?? "")}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
const StationTimeBox = React.memo(StationTimeBoxComponent);
|
||||
|
||||
@@ -1,23 +1,14 @@
|
||||
import React, { FC } from "react";
|
||||
import { View } from "react-native";
|
||||
import { TrainDataView } from "./TrainDataView";
|
||||
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> = ({
|
||||
import type { TrainDataViewHeaderProps } from "./TrainDataView";
|
||||
type props = TrainDataViewHeaderProps;
|
||||
export const LongHeader: FC<props> = ({
|
||||
currentTrainData,
|
||||
currentTrainDataList,
|
||||
nearTrainIDList,
|
||||
currentPosition,
|
||||
currentPositionList,
|
||||
nearTrainIDList,
|
||||
openTrainInfo,
|
||||
navigate,
|
||||
}) => {
|
||||
@@ -31,14 +22,13 @@ 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,131 +8,194 @@ type stateBox = {
|
||||
currentTrainData: trainDataType | undefined;
|
||||
title: string;
|
||||
style?: ViewStyle;
|
||||
mode?: number;
|
||||
mode?: 0 | 1 | 2;
|
||||
};
|
||||
export const PositionBox: FC<stateBox> = (props) => {
|
||||
const {
|
||||
currentTrainData,
|
||||
title,
|
||||
style,
|
||||
mode,
|
||||
} = props;
|
||||
const { colors } = useThemeColors();
|
||||
const { currentTrainData, title, style, mode } = props;
|
||||
const { fontScale } = useResponsive();
|
||||
const {officialPosition} = currentTrainData || {};
|
||||
const mainFontSize = mode == 2 ? fontScale(16) : fontScale(23);
|
||||
const { colors } = useThemeColors();
|
||||
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;
|
||||
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 }} />
|
||||
const firstText = from;
|
||||
const secondText = to;
|
||||
const marginText = mode == 2 ? "→" : "↓";
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
minWidth: 0,
|
||||
flexShrink: 1,
|
||||
flexDirection: mode == 2 ? "row" : "column",
|
||||
...(mode == 2 ? boxStyle2 : boxStyle),
|
||||
backgroundColor: colors.surface,
|
||||
...style,
|
||||
}}
|
||||
>
|
||||
{firstText && (
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
ellipsizeMode="tail"
|
||||
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 },
|
||||
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 || externalText) && (
|
||||
<View style={{ 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",
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
ellipsizeMode="tail"
|
||||
style={{
|
||||
flexShrink: 1,
|
||||
minWidth: 0,
|
||||
...(mode == 2
|
||||
? { fontSize: fontScale(16), textAlign: "right" as const }
|
||||
: { fontSize: fontScale(23), textAlign: "right" as const }),
|
||||
fontSize: fontScale(10),
|
||||
textAlign: "right",
|
||||
fontSize: mainFontSize,
|
||||
color: colors.textAccent,
|
||||
}}
|
||||
>
|
||||
{" " + externalText}
|
||||
{" " + platformDescription}
|
||||
{firstText}
|
||||
</Text>
|
||||
<Text style={{ color: colors.textAccent, textAlign: "right" }}>
|
||||
{marginText}
|
||||
</Text>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
ellipsizeMode="tail"
|
||||
style={{
|
||||
flexShrink: 1,
|
||||
minWidth: 0,
|
||||
textAlign: "right",
|
||||
fontSize: mainFontSize,
|
||||
color: colors.textAccent,
|
||||
}}
|
||||
>
|
||||
{secondText}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</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",
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
ellipsizeMode="tail"
|
||||
style={{
|
||||
flexShrink: 1,
|
||||
minWidth: 0,
|
||||
fontSize: mainFontSize,
|
||||
textAlign: "right",
|
||||
color: colors.textAccent,
|
||||
}}
|
||||
>
|
||||
{firstText}
|
||||
</Text>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
ellipsizeMode="tail"
|
||||
style={{
|
||||
flexShrink: 1,
|
||||
minWidth: 0,
|
||||
fontSize: subFontSize,
|
||||
textAlign: "right",
|
||||
color: colors.textAccent,
|
||||
}}
|
||||
>
|
||||
{secondText}
|
||||
</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>
|
||||
);
|
||||
}
|
||||
};
|
||||
const boxStyle: ViewStyle = {
|
||||
flex: 1,
|
||||
|
||||
@@ -1,12 +1,27 @@
|
||||
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";
|
||||
|
||||
export const ScrollStickyContent = (props) => {
|
||||
const { currentTrainData, showThrew, setShowThrew, haveThrough } = props;
|
||||
type Props = {
|
||||
trainDelay: trainDataType["delay"];
|
||||
showThrew: boolean;
|
||||
setShowThrew: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
haveThrough: boolean;
|
||||
};
|
||||
|
||||
export const ScrollStickyContent = ({
|
||||
trainDelay,
|
||||
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={{
|
||||
@@ -27,7 +42,7 @@ export const ScrollStickyContent = (props) => {
|
||||
<Text style={{ fontSize: fontScale(20), color: colors.text }}>停車駅</Text>
|
||||
<View style={{ flex: 1 }} />
|
||||
<View style={{ flexDirection: "row" }}>
|
||||
{!isNaN(currentTrainData?.delay) && currentTrainData?.delay != 0 && (
|
||||
{hasDelay && (
|
||||
<Text
|
||||
style={{
|
||||
fontSize: fontScale(15),
|
||||
@@ -44,11 +59,7 @@ export const ScrollStickyContent = (props) => {
|
||||
<Text
|
||||
style={{
|
||||
fontSize: fontScale(20),
|
||||
color: isNaN(currentTrainData?.delay)
|
||||
? colors.text
|
||||
: currentTrainData?.delay == 0
|
||||
? colors.text
|
||||
: colors.textError,
|
||||
color: hasDelay ? colors.textError : colors.text,
|
||||
width: 60,
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -1,23 +1,14 @@
|
||||
import React, { FC } from "react";
|
||||
import { View } from "react-native";
|
||||
import { TrainDataView } from "./TrainDataView";
|
||||
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> = ({
|
||||
import type { TrainDataViewHeaderProps } from "./TrainDataView";
|
||||
type props = TrainDataViewHeaderProps;
|
||||
export const ShortHeader: FC<props> = ({
|
||||
currentTrainData,
|
||||
currentTrainDataList,
|
||||
nearTrainIDList,
|
||||
currentPosition,
|
||||
currentPositionList,
|
||||
nearTrainIDList,
|
||||
openTrainInfo,
|
||||
navigate,
|
||||
}) => {
|
||||
@@ -31,15 +22,14 @@ export const ShortHeader:FC<props> = ({
|
||||
}}
|
||||
>
|
||||
<TrainDataView
|
||||
mode={2}
|
||||
nearTrainIDList={nearTrainIDList}
|
||||
currentTrainData={currentTrainData}
|
||||
currentTrainDataList={currentTrainDataList}
|
||||
currentPosition={currentPosition}
|
||||
currentPositionList={currentPositionList}
|
||||
nearTrainIDList={nearTrainIDList}
|
||||
mode={2}
|
||||
openTrainInfo={openTrainInfo}
|
||||
navigate={navigate}
|
||||
key={"ShortHeader"}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { CSSProperties, FC } from "react";
|
||||
import { View, Text, StyleProp, ViewStyle } from "react-native";
|
||||
import React, { FC } from "react";
|
||||
import { View, Text, 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?: number;
|
||||
mode?: 0 | 1 | 2;
|
||||
endText?: string;
|
||||
};
|
||||
export const StateBox: FC<stateBox> = (props) => {
|
||||
|
||||
@@ -1,63 +1,166 @@
|
||||
import React, { useState, useEffect, FC } from "react";
|
||||
import React, { useState, 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 { trainDataType } from "@/lib/trainPositionTextArray";
|
||||
import { useCustomTrainData } from "@/stateBox/useAllTrainDiagram";
|
||||
import { customTrainDataDetector } from "@/components/custom-train-data";
|
||||
import type { trainDataType } from "@/lib/trainPositionTextArray";
|
||||
import type { NavigateFunction } from "@/types";
|
||||
import { stackAwareNavigate } from "@/lib/rootNavigation";
|
||||
import { OfficialPositionEditButton } from "@/components/OfficialPositionEditButton";
|
||||
import { lineDisplayName } from "@/lib/lineDisplayName";
|
||||
|
||||
type props = {
|
||||
export type TrainDataViewData = {
|
||||
currentTrainData: trainDataType | undefined;
|
||||
currentTrainDataList?: trainDataType[];
|
||||
currentPosition: string[] | undefined;
|
||||
currentPositionList?: string[][];
|
||||
showLineLabel?: boolean;
|
||||
currentTrainDataList: trainDataType[];
|
||||
currentPosition: string[];
|
||||
currentPositionList: string[][];
|
||||
};
|
||||
|
||||
export type TrainDataViewMode = 0 | 1 | 2;
|
||||
|
||||
export type TrainDataViewControls = {
|
||||
nearTrainIDList: string[];
|
||||
openTrainInfo: (f: string) => void;
|
||||
mode?: 0 | 1 | 2; //0:通常,1:コンパクト,2:横並び
|
||||
navigate: NavigateFunction;
|
||||
};
|
||||
const TrainDataViewRow: FC<props> = ({
|
||||
|
||||
export type TrainDataViewHeaderProps = TrainDataViewData &
|
||||
TrainDataViewControls;
|
||||
|
||||
type TrainDataViewProps = TrainDataViewHeaderProps & {
|
||||
mode?: TrainDataViewMode; // 0:通常, 1:コンパクト, 2:横並び
|
||||
};
|
||||
|
||||
export const TrainDataView: FC<TrainDataViewProps> = ({
|
||||
currentTrainData,
|
||||
currentTrainDataList,
|
||||
currentPosition,
|
||||
showLineLabel,
|
||||
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,
|
||||
openTrainInfo,
|
||||
mode = 0,
|
||||
navigate,
|
||||
}) => {
|
||||
const { width} = useWindowDimensions();
|
||||
const trainNum = train?.num ?? "";
|
||||
const currentTrainData = train;
|
||||
const currentPosition = position;
|
||||
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",
|
||||
@@ -91,7 +194,7 @@ const TrainDataViewRow: FC<props> = ({
|
||||
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}
|
||||
@@ -99,7 +202,7 @@ const TrainDataViewRow: FC<props> = ({
|
||||
if (!currentTrainData) return;
|
||||
setInjectData({
|
||||
type: "train",
|
||||
value: currentTrainData?.num,
|
||||
value: trainNum,
|
||||
fixed: true,
|
||||
});
|
||||
stackAwareNavigate("positions");
|
||||
@@ -139,14 +242,11 @@ const TrainDataViewRow: FC<props> = ({
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={
|
||||
{
|
||||
flexDirection: mode == 2 ? "row" : "column",
|
||||
flex: 1,
|
||||
...columnStyle
|
||||
}
|
||||
|
||||
}
|
||||
style={{
|
||||
flexDirection: mode == 2 ? "row" : "column",
|
||||
flex: 1,
|
||||
...columnStyle,
|
||||
}}
|
||||
>
|
||||
<View style={rightItemStyle}>
|
||||
<StateBox
|
||||
@@ -174,7 +274,7 @@ const TrainDataViewRow: FC<props> = ({
|
||||
}}
|
||||
>
|
||||
{nearTrainIDList.length == 0 ? (
|
||||
<StateBox mode={mode} title="列番" text={trainNumber} />
|
||||
<StateBox mode={mode} title="列番" text={trainNum} />
|
||||
) : (
|
||||
<StateBox
|
||||
mode={mode}
|
||||
@@ -192,82 +292,3 @@ const TrainDataViewRow: FC<props> = ({
|
||||
</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,4 +1,5 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||
import dayjs from "dayjs";
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
@@ -6,12 +7,11 @@ import {
|
||||
StyleSheet,
|
||||
useWindowDimensions,
|
||||
BackHandler,
|
||||
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 } from "./EachTrainInfo/EachStopList";
|
||||
import { EachStopList, type EachStopData } from "./EachTrainInfo/EachStopList";
|
||||
import { DataConnectedButton } from "./EachTrainInfo/DataConnectedButton";
|
||||
import { DynamicHeaderScrollView } from "../DynamicHeaderScrollView";
|
||||
import { LongHeader } from "./EachTrainInfo/LongHeader";
|
||||
@@ -19,10 +19,13 @@ 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 } from "@/stateBox/useAllTrainDiagram";
|
||||
import {
|
||||
useCustomTrainData,
|
||||
useOperationLogs,
|
||||
} from "@/stateBox/useAllTrainDiagram";
|
||||
import { useResponsive } from "@/lib/responsive";
|
||||
|
||||
// Custom hooks
|
||||
@@ -34,6 +37,7 @@ import { useTrainPosition } from "./EachTrainInfoCore/hooks/useTrainPosition";
|
||||
import { useAutoScroll } from "./EachTrainInfoCore/hooks/useAutoScroll";
|
||||
import { useExtendedStations } from "./EachTrainInfoCore/hooks/useExtendedStations";
|
||||
import type { EachTrainInfoPayload } from "@/types/actionSheets";
|
||||
import { openExternalUrl } from "@/lib/openExternalUrl";
|
||||
|
||||
type EachTrainInfoCoreProps = {
|
||||
trainNum: EachTrainInfoPayload["trainNum"];
|
||||
@@ -43,6 +47,11 @@ 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,
|
||||
@@ -51,21 +60,56 @@ export const EachTrainInfoCore = ({
|
||||
sheetOpened = false,
|
||||
}: EachTrainInfoCoreProps) => {
|
||||
const from = fromValue ?? "";
|
||||
const { stationList } = useStationList();
|
||||
const { allCustomTrainData } = useCustomTrainData();
|
||||
const { colors, fixed } = useThemeColors();
|
||||
const { verticalScale, moderateScale } = useResponsive();
|
||||
const { allCustomTrainData, activateCustomTrainData } = useCustomTrainData();
|
||||
const { activateOperationLogs } = useOperationLogs();
|
||||
const { colors, fixed, isDark } = useThemeColors();
|
||||
const { verticalScale, moderateScale, fontScale } = 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 {
|
||||
@@ -77,12 +121,15 @@ export const EachTrainInfoCore = ({
|
||||
pointsDisplay,
|
||||
} = useTrainPosition(trainNum, stopStationIDList);
|
||||
|
||||
const {
|
||||
showHeadStation,
|
||||
showTailStation,
|
||||
extendToHeadStation,
|
||||
extendToTailStation,
|
||||
} = useExtendedStations(trainData, setTrainData);
|
||||
const parsedTrainData = useMemo<readonly EachStopData[]>(
|
||||
() =>
|
||||
trainDataWithThrough.map(parseEachStopData),
|
||||
[trainDataWithThrough],
|
||||
);
|
||||
const clockBase = useMemo(
|
||||
() => dayjs(),
|
||||
[trainDataWithThrough, currentTrainData?.delay],
|
||||
);
|
||||
|
||||
// UI state
|
||||
// 走行中の列車は初期状態から通過駅を表示する(後から showThrew を true に変更すると
|
||||
@@ -90,15 +137,11 @@ 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,
|
||||
);
|
||||
|
||||
@@ -132,6 +175,133 @@ 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={() =>
|
||||
openExternalUrl("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={{
|
||||
@@ -159,14 +329,11 @@ 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}
|
||||
@@ -183,145 +350,47 @@ 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
|
||||
{...{ currentTrainData, showThrew, setShowThrew, haveThrough }}
|
||||
{...{
|
||||
trainDelay: currentTrainData?.delay,
|
||||
showThrew,
|
||||
setShowThrew,
|
||||
haveThrough,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{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 },
|
||||
]}
|
||||
/>
|
||||
{listHeader}
|
||||
{trainDataWithThrough.map((item, index) => (
|
||||
<React.Fragment key={`${trainNum}-${index}`}>
|
||||
{renderTrainDataItem(item, index)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
{listFooter}
|
||||
</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,10 +26,8 @@ import { normalizeIconDisplayMode } from "@/lib/iconDisplayMode";
|
||||
type Props = {
|
||||
trainNum: string;
|
||||
trainData: string[];
|
||||
showHeadStation: number[];
|
||||
showTailStation: number[];
|
||||
headStation: { id: string }[];
|
||||
tailStation: { id: string }[];
|
||||
showHeadStation: string[];
|
||||
showTailStation: string[];
|
||||
navigate: NavigateFunction;
|
||||
from: string;
|
||||
fontLoaded: boolean;
|
||||
@@ -46,8 +44,6 @@ export const HeaderText: FC<Props> = ({
|
||||
trainData,
|
||||
showHeadStation,
|
||||
showTailStation,
|
||||
headStation,
|
||||
tailStation,
|
||||
navigate,
|
||||
from,
|
||||
scrollRef,
|
||||
@@ -309,9 +305,9 @@ export const HeaderText: FC<Props> = ({
|
||||
</TouchableOpacity>
|
||||
<View style={{ flex: 1 }} />
|
||||
<Text style={{ ...textConfig, fontSize: fontScale(17), color: fixed.textOnPrimary }}>
|
||||
{showHeadStation.map((d) => `${headStation[d].id} + `)}
|
||||
{showHeadStation.map((trainID) => `${trainID} + `)}
|
||||
{trainNum}
|
||||
{showTailStation.map((d) => ` + ${tailStation[d].id}`)}
|
||||
{showTailStation.map((trainID) => ` + ${trainID}`)}
|
||||
</Text>
|
||||
<MaterialCommunityIcons
|
||||
name="database"
|
||||
|
||||
@@ -2,6 +2,7 @@ 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>;
|
||||
@@ -87,7 +88,7 @@ export const TrainIconStack: FC<Props> = ({
|
||||
<TouchableOpacity
|
||||
key={`${trainIcon}-${index}`}
|
||||
onPress={() => onPressEntry(entry, index)}
|
||||
disabled={!entry.vehicle_info_url}
|
||||
disabled={!normalizeVehicleInfoUrl(entry.vehicle_info_url)}
|
||||
>
|
||||
{content}
|
||||
</TouchableOpacity>
|
||||
|
||||
@@ -1,33 +1,46 @@
|
||||
import { useEffect, MutableRefObject } from 'react';
|
||||
import { useEffect, MutableRefObject, useRef } 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 || isJumped || !points?.length || !scrollRef) return;
|
||||
if (!sheetOpened || jumpedRef.current || !points?.length || !scrollRef) return;
|
||||
|
||||
const currentPositionIndex = points.findIndex((d) => d === true);
|
||||
if (currentPositionIndex === -1) return;
|
||||
|
||||
// 5駅以内の場合はスクロールしない
|
||||
if (currentPositionIndex < 5) {
|
||||
setIsJumped(true);
|
||||
jumpedRef.current = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const scrollPosition = currentPositionIndex * 44 - 50;
|
||||
const timer = setTimeout(() => {
|
||||
scrollRef.current?.scrollTo({ y: scrollPosition, animated: true });
|
||||
setIsJumped(true);
|
||||
if (typeof scrollRef.current?.scrollToOffset === 'function') {
|
||||
scrollRef.current.scrollToOffset({
|
||||
offset: scrollPosition,
|
||||
animated: true,
|
||||
});
|
||||
} else {
|
||||
scrollRef.current?.scrollTo({ y: scrollPosition, animated: true });
|
||||
}
|
||||
jumpedRef.current = true;
|
||||
}, 100);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [sheetOpened, points, trainDataWithThrough, scrollRef, isJumped, setIsJumped]);
|
||||
}, [sheetOpened, points, trainDataWithThrough, scrollRef]);
|
||||
};
|
||||
|
||||
@@ -1,29 +1,85 @@
|
||||
import { useState } from 'react';
|
||||
import { openBackTrainInfo } from '@/lib/eachTrainInfoCoreLib/openBackTrainInfo';
|
||||
|
||||
export const useExtendedStations = (trainData, setTrainData) => {
|
||||
const [showHeadStation, setShowHeadStation] = useState([]);
|
||||
const [showTailStation, setShowTailStation] = useState([]);
|
||||
type ExtendedState = {
|
||||
trainNum: string;
|
||||
headTrainIDs: string[];
|
||||
tailTrainIDs: string[];
|
||||
includedTrainIDs: string[];
|
||||
};
|
||||
|
||||
const extendToHeadStation = (station, dia, index) => {
|
||||
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 array = openBackTrainInfo(station, trainData, dia);
|
||||
if (!array) return;
|
||||
|
||||
|
||||
setTrainData(array);
|
||||
setShowHeadStation((prev) => [...prev, index]);
|
||||
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],
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const extendToTailStation = (station, dia, index) => {
|
||||
const extendToTailStation = (
|
||||
station: string,
|
||||
dia: string[],
|
||||
trainID: string,
|
||||
) => {
|
||||
const array = openBackTrainInfo(station, trainData, dia);
|
||||
if (!array) return;
|
||||
|
||||
|
||||
setTrainData(array);
|
||||
setShowTailStation((prev) => [...prev, index]);
|
||||
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],
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
showHeadStation,
|
||||
showTailStation,
|
||||
showHeadStation: currentState.headTrainIDs,
|
||||
showTailStation: currentState.tailTrainIDs,
|
||||
includedTrainIDs: currentState.includedTrainIDs,
|
||||
extendToHeadStation,
|
||||
extendToTailStation,
|
||||
};
|
||||
|
||||
@@ -1,27 +1,61 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useMemo } from "react";
|
||||
import { useBusAndTrainData } from "@/stateBox/useBusAndTrainData";
|
||||
|
||||
export const useNearbyTrains = (trainNum, trainData) => {
|
||||
type InfluencedTrain = { id: string; TrainData: string[] };
|
||||
type BoundaryTrain = { station: string; dia: string[]; id: string };
|
||||
|
||||
export const useNearbyTrains = (
|
||||
trainNum: string,
|
||||
trainData: string[],
|
||||
includedTrainIDs: string[],
|
||||
) => {
|
||||
const { getInfluencedTrainData } = useBusAndTrainData();
|
||||
const [headStation, setHeadStation] = useState([]);
|
||||
const [tailStation, setTailStation] = useState([]);
|
||||
const [nearTrainIDList, setNearTrainIDList] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!trainNum || !trainData.length) return;
|
||||
return useMemo(() => {
|
||||
if (!trainNum || !trainData.length) {
|
||||
return { headStation: [], tailStation: [], nearTrainIDList: [] };
|
||||
}
|
||||
|
||||
const nearTrainList = getInfluencedTrainData(trainNum);
|
||||
if (!nearTrainList.length) return;
|
||||
const nearTrainList = getInfluencedTrainData(trainNum) ?? [];
|
||||
if (!nearTrainList.length) {
|
||||
return { headStation: [], tailStation: [], nearTrainIDList: [] };
|
||||
}
|
||||
|
||||
const trainDataArray = nearTrainList.map((d) => d.TrainData);
|
||||
const trainIDs = nearTrainList.map((d) => d.id);
|
||||
setNearTrainIDList(trainIDs);
|
||||
// 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];
|
||||
|
||||
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 = [];
|
||||
const tail = [];
|
||||
const head: BoundaryTrain[] = [];
|
||||
const tail: BoundaryTrain[] = [];
|
||||
|
||||
trainDataArray.forEach((data, i) => {
|
||||
data.forEach((d) => {
|
||||
@@ -47,9 +81,10 @@ export const useNearbyTrains = (trainNum, trainData) => {
|
||||
});
|
||||
});
|
||||
|
||||
setHeadStation(head);
|
||||
setTailStation(tail);
|
||||
}, [trainData, trainNum, getInfluencedTrainData]);
|
||||
|
||||
return { headStation, tailStation, nearTrainIDList };
|
||||
return {
|
||||
headStation: head,
|
||||
tailStation: tail,
|
||||
nearTrainIDList: nearTrainList.map((d) => d.id),
|
||||
};
|
||||
}, [getInfluencedTrainData, includedTrainIDs, trainData, trainNum]);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { useStationList } from '@/stateBox/useStationList';
|
||||
|
||||
const computeStopStationIDs = (data: string[], stationList: any[][]): string[][] =>
|
||||
@@ -13,14 +13,9 @@ const computeStopStationIDs = (data: string[], stationList: any[][]): string[][]
|
||||
export const useStopStationIDs = (trainDataWithThrough: string[]) => {
|
||||
const { stationList } = useStationList();
|
||||
|
||||
// 初回レンダリング時に同期的に計算することでActionSheetのアニメーション中の高さ変化を防ぐ
|
||||
const [stopStationIDList, setStopStationIDList] = useState<string[][]>(() =>
|
||||
computeStopStationIDs(trainDataWithThrough, stationList)
|
||||
// station IDも表示データから直接導出し、effect由来の追加renderをなくす。
|
||||
return useMemo(
|
||||
() => computeStopStationIDs(trainDataWithThrough, stationList),
|
||||
[trainDataWithThrough, stationList],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setStopStationIDList(computeStopStationIDs(trainDataWithThrough, stationList));
|
||||
}, [trainDataWithThrough, stationList]);
|
||||
|
||||
return stopStationIDList;
|
||||
};
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { lineListPair, stationIDPair } from '@/lib/getStationList';
|
||||
import { useStationList } from '@/stateBox/useStationList';
|
||||
import { OriginalStationList } from '@/lib/CommonTypes';
|
||||
|
||||
const computeThroughStations = (
|
||||
const computeThroughStationsUnmeasured = (
|
||||
trainData: string[],
|
||||
stationList: any[][],
|
||||
originalStationList: OriginalStationList
|
||||
@@ -103,14 +103,20 @@ const computeThroughStations = (
|
||||
export const useThroughStations = (trainData) => {
|
||||
const { originalStationList, stationList } = useStationList();
|
||||
|
||||
// 初回レンダリング時に同期的に計算することでActionSheetのアニメーション中の高さ変化を防ぐ
|
||||
const [state, setState] = useState(() =>
|
||||
computeThroughStations(trainData, stationList, originalStationList)
|
||||
// 純粋な派生値なのでstate/effectで一度古いリストを描画してから更新しない。
|
||||
return useMemo(
|
||||
() => computeThroughStations(trainData, stationList, originalStationList),
|
||||
[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,8 +1,11 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useCallback, useMemo, useState } 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) {
|
||||
@@ -14,28 +17,30 @@ const parseTrainData = (trainNum: string, trainList: Record<string, string>) =>
|
||||
|
||||
export const useTrainDiagramData = (trainNum) => {
|
||||
const { allTrainDiagram: trainList } = useTrainDiagram();
|
||||
const [isManuallyExtended, setIsManuallyExtended] = useState(false);
|
||||
const parsed = useMemo(
|
||||
() => parseTrainData(trainNum, trainList),
|
||||
[trainNum, trainList],
|
||||
);
|
||||
const [manualData, setManualData] = useState<{
|
||||
trainNum: string;
|
||||
data: string[];
|
||||
} | null>(null);
|
||||
|
||||
// 初回レンダリング時にコンテキストから同期的にデータを取得することで
|
||||
// ActionSheetのアニメーション中に高さが変わるのを防ぐ
|
||||
const [trainData, setTrainData] = useState(() => parseTrainData(trainNum, trainList).data);
|
||||
const [trueTrainID, setTrueTrainID] = useState(() => parseTrainData(trainNum, trainList).trueIDs);
|
||||
// ダイヤ本体は入力から直接導出する。列車切替時に前の列車を一度描画して
|
||||
// effectで新しい配列へ差し替える経路をなくし、リストの余分な再描画を防ぐ。
|
||||
const trainData =
|
||||
manualData?.trainNum === trainNum ? manualData.data : parsed.data;
|
||||
|
||||
useEffect(() => {
|
||||
if (!trainNum) return;
|
||||
const setTrainDataExtended = useCallback(
|
||||
(data: string[]) => {
|
||||
setManualData({ trainNum, data });
|
||||
},
|
||||
[trainNum],
|
||||
);
|
||||
|
||||
// 手動で拡張されている場合は上書きしない
|
||||
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: parsed.trueIDs,
|
||||
};
|
||||
|
||||
return { trainData, setTrainData: setTrainDataExtended, trueTrainID };
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { AS } from '@/storageControl';
|
||||
import { STORAGE_KEYS } from '@/constants';
|
||||
import { logger } from '@/utils/logger';
|
||||
@@ -7,7 +7,6 @@ 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[][]
|
||||
@@ -18,13 +17,9 @@ 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');
|
||||
|
||||
useEffect(() => {
|
||||
const currentTrainDataList = useMemo(() => {
|
||||
const candidates = getCurrentStationDataList(trainNum);
|
||||
const routeLineCounts = new Map<string, number>();
|
||||
for (const stationNumber of stopStationIDList.flat()) {
|
||||
@@ -35,14 +30,11 @@ export const useTrainPosition = (
|
||||
.sort(([, countA], [, countB]) => countB - countA)
|
||||
.map(([line]) => line);
|
||||
const selected = getCurrentStationData(trainNum, routeLineIds);
|
||||
const visible = updatePermission
|
||||
return updatePermission
|
||||
? candidates
|
||||
: selected
|
||||
? [selected]
|
||||
: [];
|
||||
logger.debug('Train position data:', visible);
|
||||
setCurrentTrainDataList(visible);
|
||||
setCurrentTrainData(visible[0]);
|
||||
}, [
|
||||
getCurrentStationData,
|
||||
getCurrentStationDataList,
|
||||
@@ -50,15 +42,16 @@ export const useTrainPosition = (
|
||||
trainNum,
|
||||
updatePermission,
|
||||
]);
|
||||
const currentTrainData = currentTrainDataList[0];
|
||||
|
||||
useEffect(() => {
|
||||
if (!stopStationIDList.length || !currentTrainDataList.length) {
|
||||
setCurrentPositionList([]);
|
||||
setCurrentPosition([]);
|
||||
return;
|
||||
}
|
||||
logger.debug('Train position data:', currentTrainDataList);
|
||||
}, [currentTrainDataList]);
|
||||
|
||||
const positions = currentTrainDataList.map((train) => {
|
||||
const currentPositionList = useMemo(() => {
|
||||
if (!stopStationIDList.length || !currentTrainDataList.length) return [];
|
||||
|
||||
return currentTrainDataList.map((train) => {
|
||||
const position = getPosition(train);
|
||||
const next = position ? [...position] : [];
|
||||
if (next.length > 1) {
|
||||
@@ -79,9 +72,8 @@ export const useTrainPosition = (
|
||||
}
|
||||
return next;
|
||||
});
|
||||
setCurrentPositionList(positions);
|
||||
setCurrentPosition(positions[0] ?? []);
|
||||
}, [currentTrainDataList, stopStationIDList, getPosition]);
|
||||
}, [currentTrainDataList, getPosition, stopStationIDList]);
|
||||
const currentPosition = currentPositionList[0] ?? [];
|
||||
|
||||
useEffect(() => {
|
||||
AS.getItem(STORAGE_KEYS.TRAIN_POSITION_SWITCH)
|
||||
@@ -107,15 +99,21 @@ export const useTrainPosition = (
|
||||
return merged;
|
||||
};
|
||||
|
||||
const points =
|
||||
trainPositionSwitch === 'true'
|
||||
? mergePoints(true)
|
||||
: stopStationIDList.map(() => false);
|
||||
const points = useMemo(
|
||||
() =>
|
||||
trainPositionSwitch === 'true'
|
||||
? mergePoints(true)
|
||||
: stopStationIDList.map(() => false),
|
||||
[currentPositionList, stopStationIDList, trainPositionSwitch],
|
||||
);
|
||||
|
||||
const pointsDisplay =
|
||||
trainPositionSwitch === 'true'
|
||||
? mergePoints(false)
|
||||
: stopStationIDList.map(() => false);
|
||||
const pointsDisplay = useMemo(
|
||||
() =>
|
||||
trainPositionSwitch === 'true'
|
||||
? mergePoints(false)
|
||||
: stopStationIDList.map(() => false),
|
||||
[currentPositionList, stopStationIDList, trainPositionSwitch],
|
||||
);
|
||||
|
||||
return {
|
||||
currentTrainData,
|
||||
|
||||
@@ -2,13 +2,14 @@ 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 "../../custom-train-data";
|
||||
import { customTrainDataDetector } from "@/components/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"];
|
||||
@@ -122,11 +123,22 @@ export const TrainIconStatus: FC<Props> = (props) => {
|
||||
hidden={!!anpanmanStatus && showIcon}
|
||||
statusIcon={anpanmanStatus}
|
||||
onPressEntry={(entry) => {
|
||||
navigate("howto", {
|
||||
info: entry.vehicle_info_url,
|
||||
goTo: from == "LED" ? "menu" : from,
|
||||
});
|
||||
SheetManager.hide("EachTrainInfo");
|
||||
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,
|
||||
});
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -3,12 +3,12 @@ import {
|
||||
View,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
Linking,
|
||||
StyleProp,
|
||||
ViewStyle,
|
||||
} from "react-native";
|
||||
import { MaterialCommunityIcons } from "@expo/vector-icons";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
import { openExternalUrl } from "@/lib/openExternalUrl";
|
||||
|
||||
export const BottomButtons: FC<{ onCapture: () => void }> = ({ onCapture }) => {
|
||||
const { fixed } = useThemeColors();
|
||||
@@ -35,7 +35,7 @@ export const BottomButtons: FC<{ onCapture: () => void }> = ({ onCapture }) => {
|
||||
>
|
||||
<TouchableOpacity
|
||||
style={{ ...styles, flex: 1 }}
|
||||
onPress={() => Linking.openURL("https://mstdn.y-zu.org/@JRSTraInfoEX")}
|
||||
onPress={() => openExternalUrl("https://mstdn.y-zu.org/@JRSTraInfoEX")}
|
||||
>
|
||||
<MaterialCommunityIcons name="mastodon" color={fixed.textOnPrimary} size={30} />
|
||||
<View style={{ flex: 1 }} />
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
Platform,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
Linking,
|
||||
useWindowDimensions,
|
||||
} from "react-native";
|
||||
import ActionSheet from "react-native-actions-sheet";
|
||||
@@ -15,6 +14,7 @@ import { NewsReleaseInfoBox } from "../Menu/NewsReleaseInfoBox";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
import { useResponsive } from "@/lib/responsive";
|
||||
import type { NewsReleaseInfoPayload } from "@/types/actionSheets";
|
||||
import { openExternalUrl } from "@/lib/openExternalUrl";
|
||||
|
||||
type Props = {
|
||||
payload: NewsReleaseInfoPayload;
|
||||
@@ -115,7 +115,7 @@ const NewsReleaseInfoContent: FC<ContentProps> = ({ payload: { navigate }, scrol
|
||||
{/* 下部固定ボタン(ScrollViewの外、画面の下固定) */}
|
||||
<View style={{ padding: 10, backgroundColor: fixed.primary }}>
|
||||
<TouchableOpacity
|
||||
onPress={() => Linking.openURL("https://www.jr-shikoku.co.jp/03_news/press/")}
|
||||
onPress={() => openExternalUrl("https://www.jr-shikoku.co.jp/03_news/press/")}
|
||||
style={{
|
||||
paddingVertical: 12,
|
||||
paddingLeft: 14,
|
||||
|
||||
@@ -4,10 +4,10 @@ import ActionSheet ,{ ScrollView } from "react-native-actions-sheet";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { ListItem } from "@rneui/themed";
|
||||
import { MaterialCommunityIcons, Foundation } from "@expo/vector-icons";
|
||||
import { Linking } from "react-native";
|
||||
import TouchableScale from "react-native-touchable-scale";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
import { useSheetMaxHeight } from "./useSheetMaxHeight";
|
||||
import { openExternalUrl } from "@/lib/openExternalUrl";
|
||||
export const Social = () => {
|
||||
const actionSheetRef = useRef(null);
|
||||
const insets = useSafeAreaInsets();
|
||||
@@ -66,8 +66,8 @@ export const Social = () => {
|
||||
</View>
|
||||
<ListItem
|
||||
bottomDivider
|
||||
onPress={() => Linking.openURL("tel:0570-00-4592")}
|
||||
// @ts-ignore: TouchableScale props passed through Component
|
||||
onPress={() => openExternalUrl("tel:0570-00-4592")}
|
||||
// @ts-expect-error RNEUIのListItem型はComponentに渡したTouchableScaleのpropsを含まない
|
||||
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-ignore: ListItem.Chevron doesn't expose color prop */}
|
||||
{/* @ts-expect-error RNEUIのListItem.Chevron型はcolorを公開していないが、実装はIconへ渡す */}
|
||||
<ListItem.Chevron color={"white"} />
|
||||
</ListItem>
|
||||
<ScrollView
|
||||
@@ -152,9 +152,9 @@ export const Social = () => {
|
||||
].map((d) => (
|
||||
<ListItem
|
||||
bottomDivider
|
||||
onPress={() => Linking.openURL(d.url)}
|
||||
onPress={() => openExternalUrl(d.url)}
|
||||
key={d.url}
|
||||
// @ts-ignore: TouchableScale props passed through Component
|
||||
// @ts-expect-error RNEUIのListItem型はComponentに渡したTouchableScaleのpropsを含まない
|
||||
friction={90} //
|
||||
tension={100} // These props are passed to the parent component (here TouchableScale)
|
||||
activeScale={0.95} //
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import {
|
||||
View,
|
||||
Linking,
|
||||
BackHandler,
|
||||
Platform,
|
||||
useWindowDimensions,
|
||||
@@ -9,11 +8,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";
|
||||
@@ -26,6 +25,7 @@ import { useTrainMenu } from "@/stateBox/useTrainMenu";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
import { useResponsive } from "@/lib/responsive";
|
||||
import { useSheetMaxHeight } from "./useSheetMaxHeight";
|
||||
import { openExternalUrl } from "@/lib/openExternalUrl";
|
||||
|
||||
export const StationDeteilView = ({ payload }: SheetProps<"StationDetailView">) => {
|
||||
if (!payload) return <></>;
|
||||
@@ -102,15 +102,14 @@ export const StationDeteilView = ({ payload }: SheetProps<"StationDetailView">)
|
||||
stationID={station.StationNumber}
|
||||
oP={() => {
|
||||
usePDFView == "true"
|
||||
? Linking.openURL(station.StationTimeTable)
|
||||
: navigate("howto", {
|
||||
info,
|
||||
goTo,
|
||||
useShow,
|
||||
? openExternalUrl(station.StationTimeTable)
|
||||
: navigate("generalWebView", {
|
||||
uri: info,
|
||||
onExit: goTo != "NearTrainDiagramView" ? useShow : undefined,
|
||||
});
|
||||
onExit();
|
||||
}}
|
||||
oLP={() => Linking.openURL(station.StationTimeTable)}
|
||||
oLP={() => openExternalUrl(station.StationTimeTable)}
|
||||
/>
|
||||
</View>
|
||||
<View style={{ flexDirection: "row" }}>
|
||||
@@ -165,10 +164,9 @@ export const StationDeteilView = ({ payload }: SheetProps<"StationDetailView">)
|
||||
<TrainBusButton
|
||||
address={trainBus.address}
|
||||
press={() => {
|
||||
navigate("howto", {
|
||||
info: trainBus.address,
|
||||
goTo,
|
||||
useShow,
|
||||
navigate("generalWebView", {
|
||||
uri: trainBus.address,
|
||||
onExit: goTo != "NearTrainDiagramView" ? useShow : undefined,
|
||||
});
|
||||
onExit();
|
||||
}}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { FC } from "react";
|
||||
import { TouchableOpacity, View, Text, Linking } from "react-native";
|
||||
import { TouchableOpacity, View, Text } from "react-native";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
import type { NavigateFunction } from "@/types";
|
||||
import { openExternalUrl } from "@/lib/openExternalUrl";
|
||||
type Props = {
|
||||
navigate: NavigateFunction;
|
||||
address: string;
|
||||
@@ -25,10 +26,13 @@ export const 駅構内図:FC<Props> = (props) => {
|
||||
flex: 1,
|
||||
}}
|
||||
onPress={() => {
|
||||
navigate("howto", { info, goTo, useShow });
|
||||
navigate("generalWebView", {
|
||||
uri: info,
|
||||
onExit: goTo != "NearTrainDiagramView" ? useShow : undefined,
|
||||
});
|
||||
onExit();
|
||||
}}
|
||||
onLongPress={() => Linking.openURL(info)}
|
||||
onLongPress={() => openExternalUrl(info)}
|
||||
>
|
||||
<View style={{ flex: 1 }} />
|
||||
<Text
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React from "react";
|
||||
import { Linking } from "react-native";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { TicketBox } from "@/components/atom/TicketBox";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
import { openExternalUrl } from "@/lib/openExternalUrl";
|
||||
export const StationMapButton = ({stationMap}) => {
|
||||
const { fixed } = useThemeColors();
|
||||
return (
|
||||
@@ -10,7 +10,7 @@ export const StationMapButton = ({stationMap}) => {
|
||||
backgroundColor={"#888A85"}
|
||||
icon={<Ionicons name="map" color={fixed.textOnPrimary} size={50} />}
|
||||
flex={1}
|
||||
onPressButton={() => Linking.openURL(stationMap)}
|
||||
onPressButton={() => openExternalUrl(stationMap)}
|
||||
>
|
||||
Map
|
||||
</TicketBox>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React, { FC } from "react";
|
||||
import { Linking } from "react-native";
|
||||
import { FontAwesome } from "@expo/vector-icons";
|
||||
import { TicketBox } from "@/components/atom/TicketBox";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
import type { NavigateFunction } from "@/types";
|
||||
import { openExternalUrl } from "@/lib/openExternalUrl";
|
||||
type Props = {
|
||||
info: string;
|
||||
address: string;
|
||||
@@ -23,11 +23,14 @@ export const StationTimeTableButton: FC<Props> = (props) => {
|
||||
flex={1}
|
||||
onPressButton={() => {
|
||||
usePDFView == "true"
|
||||
? Linking.openURL(address)
|
||||
: navigate("howto", { info, goTo, useShow });
|
||||
? openExternalUrl(address)
|
||||
: navigate("generalWebView", {
|
||||
uri: info,
|
||||
onExit: goTo != "NearTrainDiagramView" ? useShow : undefined,
|
||||
});
|
||||
onExit();
|
||||
}}
|
||||
onLongPressButton={() => Linking.openURL(address)}
|
||||
onLongPressButton={() => openExternalUrl(address)}
|
||||
>
|
||||
時刻表
|
||||
</TicketBox>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React, { FC } from "react";
|
||||
import { Linking } from "react-native";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { TicketBox } from "@/components/atom/TicketBox";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
import { openExternalUrl } from "@/lib/openExternalUrl";
|
||||
type Props = {
|
||||
address: string;
|
||||
press: () => void;
|
||||
@@ -15,7 +15,7 @@ export const TrainBusButton: FC<Props> = ({ address, press }) => {
|
||||
icon={<Ionicons name="bus" color={fixed.textOnPrimary} size={50} />}
|
||||
flex={1}
|
||||
onPressButton={press}
|
||||
onLongPressButton={() => Linking.openURL(address)}
|
||||
onLongPressButton={() => openExternalUrl(address)}
|
||||
>
|
||||
並行バス
|
||||
</TicketBox>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React, { FC } from "react";
|
||||
import { Linking } from "react-native";
|
||||
import { Foundation } from "@expo/vector-icons";
|
||||
import { TicketBox } from "@/components/atom/TicketBox";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
import type { NavigateFunction } from "@/types";
|
||||
import { openExternalUrl } from "@/lib/openExternalUrl";
|
||||
type Props = {
|
||||
navigate: NavigateFunction;
|
||||
info: string;
|
||||
@@ -20,10 +20,13 @@ export const WebSiteButton: FC<Props> = (Props) => {
|
||||
icon={<Foundation name="web" color={fixed.textOnPrimary} size={50} />}
|
||||
flex={1}
|
||||
onPressButton={() => {
|
||||
navigate("howto", { info, goTo, useShow });
|
||||
navigate("generalWebView", {
|
||||
uri: info,
|
||||
onExit: goTo != "NearTrainDiagramView" ? useShow : undefined,
|
||||
});
|
||||
onExit();
|
||||
}}
|
||||
onLongPressButton={() => Linking.openURL(info)}
|
||||
onLongPressButton={() => openExternalUrl(info)}
|
||||
>
|
||||
web
|
||||
</TicketBox>
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
TouchableOpacity,
|
||||
StyleSheet,
|
||||
Platform,
|
||||
Linking,
|
||||
Image,
|
||||
Animated,
|
||||
useWindowDimensions,
|
||||
@@ -27,6 +26,7 @@ import {
|
||||
import ViewShot from "react-native-view-shot";
|
||||
import * as Sharing from "expo-sharing";
|
||||
import dayjs from "dayjs";
|
||||
import { openExternalUrl } from "@/lib/openExternalUrl";
|
||||
|
||||
export type { TrainDataSourcesPayload } from "@/types/actionSheets";
|
||||
|
||||
@@ -762,7 +762,7 @@ export const TrainDataSources: FC<{ payload?: TrainDataSourcesPayload }> = ({
|
||||
const url =
|
||||
activeElesiteGroup?.timetableUrl || "https://www.elesite-next.com/";
|
||||
SheetManager.hide("TrainDataSources");
|
||||
Linking.openURL(url);
|
||||
openExternalUrl(url);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -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, { useState, useRef, FC } from "react";
|
||||
import React, { useEffect, useState, useRef, FC } from "react";
|
||||
import {
|
||||
Animated,
|
||||
View,
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
FlatList,
|
||||
TextInput,
|
||||
ScrollView,
|
||||
Linking,
|
||||
} from "react-native";
|
||||
import {
|
||||
useCustomTrainData,
|
||||
@@ -33,6 +32,7 @@ import { InfogramText } from "./ActionSheetComponents/EachTrainInfoCore/HeaderTe
|
||||
import { resolveTrainIconEntries } from "@/lib/trainIconEntries";
|
||||
import { TrainIconStack } from "./ActionSheetComponents/EachTrainInfoCore/TrainIconStack";
|
||||
import { formatWrappedTrainType, shouldWrapTrainType } from "@/lib/trainTypeWrap";
|
||||
import { openExternalUrl } from "@/lib/openExternalUrl";
|
||||
|
||||
export const AllTrainDiagramView: FC = () => {
|
||||
const { colors, fixed } = useThemeColors();
|
||||
@@ -41,8 +41,16 @@ export const AllTrainDiagramView: FC = () => {
|
||||
const iconDisplayMode = normalizeIconDisplayMode(iconSetting);
|
||||
const tabBarHeight = useBottomTabBarHeight();
|
||||
const { keyList, allTrainDiagram } = useTrainDiagram();
|
||||
const { allCustomTrainData } = useCustomTrainData();
|
||||
const { getTodayOperationByTrainId } = useOperationLogs();
|
||||
const { allCustomTrainData, activateCustomTrainData } = useCustomTrainData();
|
||||
const {
|
||||
getTodayOperationByTrainId,
|
||||
activateOperationLogs,
|
||||
} = useOperationLogs();
|
||||
|
||||
useEffect(() => {
|
||||
activateCustomTrainData();
|
||||
activateOperationLogs();
|
||||
}, [activateCustomTrainData, activateOperationLogs]);
|
||||
const [input, setInput] = useState(""); // 文字入力
|
||||
const [useStationName, setUseStationName] = useState(false);
|
||||
const [useRegex, setUseRegex] = useState(false);
|
||||
@@ -331,7 +339,7 @@ export const AllTrainDiagramView: FC = () => {
|
||||
<Text
|
||||
style={{ ...regexTextButtonStyle, backgroundColor: "green" }}
|
||||
onPress={() =>
|
||||
Linking.openURL(
|
||||
openExternalUrl(
|
||||
"https://qiita.com/tossh/items/635aea9a529b9deb3038"
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -11,23 +11,28 @@ import type { OperationInfoSnapshot } from "@/types";
|
||||
|
||||
export const getInfoString = async () => {
|
||||
const time = dayjs().format("HH:mm");
|
||||
const response = await fetch(API_ENDPOINTS.OPERATION_INFO, {
|
||||
cache: "no-store",
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Operation information request failed: ${response.status}`
|
||||
);
|
||||
try {
|
||||
const response = await fetch(API_ENDPOINTS.OPERATION_INFO, {
|
||||
cache: "no-store",
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Operation information request failed: ${response.status}`
|
||||
);
|
||||
}
|
||||
|
||||
const snapshot = (await response.json()) as OperationInfoSnapshot;
|
||||
const operationInfoText = snapshot.compatibility?.operationInfoText ?? "";
|
||||
const text = operationInfoText === "" ? null : operationInfoText.split("^");
|
||||
|
||||
return { time, text, failed: false };
|
||||
} catch {
|
||||
// nullだと「通常運行中です。」と表示されるため、取得失敗は別に扱う
|
||||
return { time, text: null, failed: true };
|
||||
}
|
||||
|
||||
const snapshot = (await response.json()) as OperationInfoSnapshot;
|
||||
const operationInfoText = snapshot.compatibility?.operationInfoText ?? "";
|
||||
const text = operationInfoText === "" ? null : operationInfoText.split("^");
|
||||
|
||||
return { time, text };
|
||||
};
|
||||
|
||||
export function InfoWidget({ time, text, colors = widgetLightColors }: { time: string; text: string[] | string | null; colors?: WidgetColors }) {
|
||||
export function InfoWidget({ time, text, failed = false, colors = widgetLightColors }: { time: string; text: string[] | string | null; failed?: boolean; colors?: WidgetColors }) {
|
||||
return (
|
||||
<FlexWidget
|
||||
style={{
|
||||
@@ -98,7 +103,7 @@ export function InfoWidget({ time, text, colors = widgetLightColors }: { time: s
|
||||
color: colors.text,
|
||||
fontSize: 20,
|
||||
}}
|
||||
text="通常運行中です。"
|
||||
text={failed ? "運行情報を取得できませんでした。" : "通常運行中です。"}
|
||||
/>
|
||||
)}
|
||||
</ListWidget>
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -11,18 +11,28 @@ import { API_ENDPOINTS } from "@/constants";
|
||||
export const getDelayData = async () => {
|
||||
// Fetch data from the server
|
||||
const time = dayjs().format("HH:mm");
|
||||
const delayString = await fetch(API_ENDPOINTS.DELAY_INFO_LEGACY)
|
||||
.then((response) => response.text())
|
||||
.then((data) => {
|
||||
if (data !== "") {
|
||||
return data.split("^");
|
||||
}
|
||||
return null;
|
||||
});
|
||||
//ToastAndroid.show(`${delayString}`, ToastAndroid.SHORT);
|
||||
return { time, delayString };
|
||||
try {
|
||||
const delayString = await fetch(API_ENDPOINTS.DELAY_INFO_LEGACY)
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`Delay information request failed: ${response.status}`);
|
||||
}
|
||||
return response.text();
|
||||
})
|
||||
.then((data) => {
|
||||
if (data !== "") {
|
||||
return data.split("^");
|
||||
}
|
||||
return null;
|
||||
});
|
||||
//ToastAndroid.show(`${delayString}`, ToastAndroid.SHORT);
|
||||
return { time, delayString, failed: false };
|
||||
} catch {
|
||||
// nullだと「遅れはありません」と表示されるため、取得失敗は別に扱う
|
||||
return { time, delayString: null, failed: true };
|
||||
}
|
||||
};
|
||||
export function TraInfoEXWidget({ time, delayString, colors = widgetLightColors }: { time: string; delayString: string[] | null; colors?: WidgetColors }) {
|
||||
export function TraInfoEXWidget({ time, delayString, failed = false, colors = widgetLightColors }: { time: string; delayString: string[] | null; failed?: boolean; colors?: WidgetColors }) {
|
||||
return (
|
||||
<FlexWidget
|
||||
style={{
|
||||
@@ -104,7 +114,7 @@ export function TraInfoEXWidget({ time, delayString, colors = widgetLightColors
|
||||
color: colors.text,
|
||||
fontSize: 20,
|
||||
}}
|
||||
text="現在、5分以上の遅れはありません。"
|
||||
text={failed ? "遅延情報を取得できませんでした。" : "現在、5分以上の遅れはありません。"}
|
||||
/>
|
||||
)}
|
||||
</ListWidget>
|
||||
|
||||
@@ -8,16 +8,8 @@ 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";
|
||||
|
||||
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,
|
||||
};
|
||||
import { AS } from "@/storageControl";
|
||||
import { STORAGE_KEYS } from "@/constants";
|
||||
|
||||
type ColorThemePref = "light" | "system" | "dark";
|
||||
|
||||
@@ -95,18 +87,18 @@ export async function widgetTaskHandler(props) {
|
||||
}
|
||||
|
||||
if (name === "JR_shikoku_info") {
|
||||
const { time, text } = await getInfoString();
|
||||
const { time, text, failed } = await getInfoString();
|
||||
renderWithTheme(renderWidget, theme, (isDark) => (
|
||||
<InfoWidget time={time} text={text && text.toString()} colors={isDark ? widgetDarkColors : widgetLightColors} />
|
||||
<InfoWidget time={time} text={text && text.toString()} failed={failed} colors={isDark ? widgetDarkColors : widgetLightColors} />
|
||||
));
|
||||
break;
|
||||
}
|
||||
|
||||
// JR_shikoku_train_info and default
|
||||
{
|
||||
const { time, delayString } = await getDelayData();
|
||||
const { time, delayString, failed } = await getDelayData();
|
||||
renderWithTheme(renderWidget, theme, (isDark) => (
|
||||
<TraInfoEXWidget time={time} delayString={delayString} colors={isDark ? widgetDarkColors : widgetLightColors} />
|
||||
<TraInfoEXWidget time={time} delayString={delayString} failed={failed} colors={isDark ? widgetDarkColors : widgetLightColors} />
|
||||
));
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
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 "../Apps";
|
||||
import { AppContainer } from "@/AppContainer";
|
||||
import { AllTrainDiagramProvider } from "../stateBox/useAllTrainDiagram";
|
||||
import { NotificationProvider } from "../stateBox/useNotifications";
|
||||
import { UserPositionProvider } from "../stateBox/useUserPosition";
|
||||
@@ -16,6 +16,7 @@ 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";
|
||||
|
||||
@@ -34,6 +35,7 @@ const ProviderTree = buildProvidersTree([
|
||||
]);
|
||||
|
||||
export function AppRootLayout(): React.ReactElement {
|
||||
markStartupOnce("root-layout-render", "AppRootLayout render");
|
||||
return (
|
||||
<AppThemeProvider>
|
||||
<DeviceOrientationChangeProvider>
|
||||
|
||||
@@ -26,6 +26,7 @@ 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";
|
||||
@@ -43,6 +44,7 @@ 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] =
|
||||
@@ -332,6 +334,7 @@ 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,7 +409,6 @@ 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";
|
||||
|
||||
|
||||
+125
-44
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useEffect, useRef } from "react";
|
||||
import React, { useCallback, useEffect, useMemo, useRef } from "react";
|
||||
import { Platform } from "react-native";
|
||||
import { WebView } from "react-native-webview";
|
||||
|
||||
@@ -8,27 +8,44 @@ 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 { useStationList } from "../../stateBox/useStationList";
|
||||
import { useTrainMenu } from "@/stateBox/useTrainMenu";
|
||||
import {
|
||||
useCustomTrainData,
|
||||
useOperationLogs,
|
||||
useTrainDiagram,
|
||||
} from "@/stateBox/useAllTrainDiagram";
|
||||
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();
|
||||
@@ -37,6 +54,7 @@ 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();
|
||||
@@ -49,6 +67,64 @@ export const AppsWebView = ({ openStationACFromEachTrainInfo, onInitialLoadReady
|
||||
mockApiFeatureEnabled,
|
||||
mockTrainPositions,
|
||||
} = useTrainMenu();
|
||||
const { allCustomTrainData, dataReadyAt: customTrainDataReadyAt } =
|
||||
useCustomTrainData();
|
||||
const { todayOperation, dataReadyAt: operationLogsReadyAt } =
|
||||
useOperationLogs();
|
||||
const {
|
||||
allTrainDiagram,
|
||||
dataReadyAt: trainDiagramChangedAt,
|
||||
getDataVerifiedAt: getTrainDiagramVerifiedAt,
|
||||
} = useTrainDiagram();
|
||||
// 304で内容未変更を確認した時刻も鮮度として扱う。
|
||||
const trainDiagramReadyAt =
|
||||
getTrainDiagramVerifiedAt() ?? trainDiagramChangedAt;
|
||||
|
||||
// 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>
|
||||
@@ -71,22 +147,13 @@ export const AppsWebView = ({ openStationACFromEachTrainInfo, onInitialLoadReady
|
||||
reason,
|
||||
...(data ?? {}),
|
||||
});
|
||||
telemetry.message("positions.webview.remount_requested", {
|
||||
level: "warning",
|
||||
tags: {
|
||||
area: "positions_webview",
|
||||
platform: Platform.OS,
|
||||
reason,
|
||||
watchdogMode,
|
||||
},
|
||||
contexts: {
|
||||
positions_webview_remount: {
|
||||
focused: isFocused,
|
||||
landscape: isLandscape,
|
||||
...(data ?? {}),
|
||||
},
|
||||
},
|
||||
fingerprint: ["positions_webview_remount", reason],
|
||||
telemetry.log("warn", "positions.webview.remount_requested", {
|
||||
area: "positions_webview",
|
||||
reason,
|
||||
watchdogMode,
|
||||
focused: isFocused,
|
||||
landscape: isLandscape,
|
||||
...(data ?? {}),
|
||||
});
|
||||
},
|
||||
onWatchdog: (reason, data) => {
|
||||
@@ -95,28 +162,20 @@ export const AppsWebView = ({ openStationACFromEachTrainInfo, onInitialLoadReady
|
||||
watchdogMode,
|
||||
...(data ?? {}),
|
||||
});
|
||||
telemetry.message("positions.webview.watchdog_detected", {
|
||||
level: "warning",
|
||||
tags: {
|
||||
area: "positions_webview",
|
||||
platform: Platform.OS,
|
||||
reason,
|
||||
watchdogMode,
|
||||
},
|
||||
contexts: {
|
||||
positions_webview_watchdog: {
|
||||
focused: isFocused,
|
||||
landscape: isLandscape,
|
||||
...(data ?? {}),
|
||||
},
|
||||
},
|
||||
fingerprint: ["positions_webview_watchdog", reason],
|
||||
telemetry.log("warn", "positions.webview.watchdog_detected", {
|
||||
area: "positions_webview",
|
||||
reason,
|
||||
watchdogMode,
|
||||
focused: isFocused,
|
||||
landscape: isLandscape,
|
||||
...(data ?? {}),
|
||||
});
|
||||
},
|
||||
});
|
||||
const lastRemountKeyRef = useRef<typeof remountKey | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
markStartupOnce("positions-webview-mounted", "positions WebView mounted");
|
||||
setAppLifecycleWebViewActive("positions_main", true);
|
||||
return () => setAppLifecycleWebViewActive("positions_main", false);
|
||||
}, []);
|
||||
@@ -145,6 +204,9 @@ 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,
|
||||
@@ -225,9 +287,8 @@ 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("howto", { info: url });
|
||||
if (Platform.OS === "android") navigate("generalWebView", { uri: url });
|
||||
webview?.current.goBack();
|
||||
//Actions.howto();
|
||||
break;
|
||||
case url.includes("https://train.jr-shikoku.co.jp/train.html"):
|
||||
//Actions.trainbase({info: url});
|
||||
@@ -332,6 +393,10 @@ export const AppsWebView = ({ openStationACFromEachTrainInfo, onInitialLoadReady
|
||||
};
|
||||
|
||||
const onLoadStart = () => {
|
||||
notifyWebViewLoadStarted();
|
||||
markStartup("positions WebView loadStart", {
|
||||
focused: focusedRef.current,
|
||||
});
|
||||
pingHandlers.onLoadStart?.();
|
||||
addWebViewBreadcrumb("webview loadStart", {
|
||||
focused: focusedRef.current,
|
||||
@@ -340,6 +405,9 @@ export const AppsWebView = ({ openStationACFromEachTrainInfo, onInitialLoadReady
|
||||
};
|
||||
|
||||
const onLoadEnd = () => {
|
||||
markStartup("positions WebView loadEnd", {
|
||||
focused: focusedRef.current,
|
||||
});
|
||||
pingHandlers.onLoadEnd?.();
|
||||
addWebViewBreadcrumb("webview loadEnd", {
|
||||
focused: focusedRef.current,
|
||||
@@ -349,6 +417,11 @@ 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,
|
||||
});
|
||||
@@ -368,6 +441,10 @@ 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
|
||||
@@ -377,6 +454,10 @@ 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;
|
||||
@@ -426,7 +507,7 @@ export const AppsWebView = ({ openStationACFromEachTrainInfo, onInitialLoadReady
|
||||
onRenderProcessGone={handleRenderProcessGone}
|
||||
onContentProcessDidTerminate={handleContentProcessDidTerminate}
|
||||
{...{ onMessage, onNavigationStateChange, onLoadEnd }}
|
||||
injectedJavaScriptBeforeContentLoaded={injectJavascriptBeforeContentLoaded}
|
||||
injectedJavaScriptBeforeContentLoaded={webViewBeforeContentLoaded}
|
||||
injectedJavaScript={injectJavascript}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -156,6 +156,7 @@ 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,
|
||||
@@ -164,6 +165,7 @@ export const DynamicHeaderScrollView: React.FC<DynamicHeaderScrollViewProps> = (
|
||||
{viewHeader ? <></> : shortHeader}
|
||||
</Animated.View>
|
||||
<Animated.View
|
||||
pointerEvents={headerVisible ? "none" : "auto"}
|
||||
style={[
|
||||
styles.header,
|
||||
headerVisible ? longHeaderStyle.off : longHeaderStyle.on,
|
||||
@@ -175,27 +177,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,6 +34,7 @@ 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");
|
||||
|
||||
@@ -78,6 +79,11 @@ 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);
|
||||
@@ -253,11 +259,9 @@ 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 });
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Linking, Text, View, TouchableOpacity } from "react-native";
|
||||
import { Text, View, TouchableOpacity } from "react-native";
|
||||
import {
|
||||
Foundation,
|
||||
FontAwesome,
|
||||
@@ -17,6 +17,7 @@ import { useNotification } from "@/stateBox/useNotifications";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
import { useResponsive } from "@/lib/responsive";
|
||||
import { isAvailable as isFelicaAvailable } from "@/modules/expo-felica-reader/src";
|
||||
import { openExternalUrl } from "@/lib/openExternalUrl";
|
||||
|
||||
export const FixedContentBottom = (props) => {
|
||||
const { expoPushToken } = useNotification();
|
||||
@@ -33,7 +34,7 @@ export const FixedContentBottom = (props) => {
|
||||
icon={<Foundation name="ticket" color="white" size={moderateScale(50)} />}
|
||||
flex={1}
|
||||
onPressButton={() =>
|
||||
Linking.openURL("https://www.jr-eki.com/ticket/brand")
|
||||
openExternalUrl("https://www.jr-eki.com/ticket/brand")
|
||||
}
|
||||
>
|
||||
トクトク切符
|
||||
@@ -43,7 +44,7 @@ export const FixedContentBottom = (props) => {
|
||||
icon={<FontAwesome name="first-order" color="white" size={moderateScale(50)} />}
|
||||
flex={1}
|
||||
onPressButton={() =>
|
||||
Linking.openURL(
|
||||
openExternalUrl(
|
||||
"https://www.jr-shikoku.co.jp/01_trainbus/event_train/sp/",
|
||||
)
|
||||
}
|
||||
@@ -55,7 +56,7 @@ export const FixedContentBottom = (props) => {
|
||||
icon={<Ionicons name="flag" color="white" size={moderateScale(50)} />}
|
||||
flex={1}
|
||||
onPressButton={() =>
|
||||
Linking.openURL("https://www.jr-eki.com/tour/brand")
|
||||
openExternalUrl("https://www.jr-eki.com/tour/brand")
|
||||
}
|
||||
>
|
||||
旅行ツアー
|
||||
@@ -65,7 +66,7 @@ export const FixedContentBottom = (props) => {
|
||||
backgroundColor="#ed86b5"
|
||||
flex={1}
|
||||
onPressButton={() =>
|
||||
Linking.openURL("https://www.jr-eki.com/smart-eki/index.html")
|
||||
openExternalUrl("https://www.jr-eki.com/smart-eki/index.html")
|
||||
}
|
||||
>
|
||||
<Text style={{ color: "white", fontWeight: "bold", fontSize: fontScale(20) }}>
|
||||
@@ -111,7 +112,7 @@ export const FixedContentBottom = (props) => {
|
||||
backgroundColor={fixed.primary}
|
||||
flex={1}
|
||||
onPressButton={() =>
|
||||
Linking.openURL("https://www.jr-shikoku.co.jp/teiki/")
|
||||
openExternalUrl("https://www.jr-shikoku.co.jp/teiki/")
|
||||
}
|
||||
>
|
||||
<Text style={{ color: fixed.textOnPrimary, fontWeight: "bold", fontSize: fontScale(20) }}>
|
||||
@@ -125,7 +126,7 @@ export const FixedContentBottom = (props) => {
|
||||
backgroundColor={fixed.primary}
|
||||
flex={1}
|
||||
onPressButton={() =>
|
||||
Linking.openURL("https://www.jr-shikoku.co.jp/04_company/group/sp/")
|
||||
openExternalUrl("https://www.jr-shikoku.co.jp/04_company/group/sp/")
|
||||
}
|
||||
>
|
||||
<Text style={{ color: fixed.textOnPrimary, fontWeight: "bold", fontSize: fontScale(20) }}>
|
||||
@@ -146,7 +147,7 @@ export const FixedContentBottom = (props) => {
|
||||
margin: 2,
|
||||
alignItems: "center",
|
||||
}}
|
||||
onPress={() => Linking.openURL("https://www.jr-odekake.net/smt/")}
|
||||
onPress={() => openExternalUrl("https://www.jr-odekake.net/smt/")}
|
||||
>
|
||||
<Text style={{ color: "white", fontWeight: "bold", fontSize: fontScale(20) }}>
|
||||
時刻・運賃計算
|
||||
@@ -294,7 +295,7 @@ export const FixedContentBottom = (props) => {
|
||||
backgroundColor="rgb(88, 101, 242)"
|
||||
flex={1}
|
||||
onPressButton={() =>
|
||||
Linking.openURL(
|
||||
openExternalUrl(
|
||||
"https://twitter.com/Xprocess_main/status/1955242437817012300",
|
||||
)
|
||||
}
|
||||
@@ -310,9 +311,8 @@ 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("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",
|
||||
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/",
|
||||
})
|
||||
}
|
||||
>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from "react";
|
||||
import React, { useEffect } from "react";
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
@@ -19,6 +19,11 @@ 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,6 +6,7 @@ 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[];
|
||||
@@ -39,6 +40,10 @@ 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("howto", { info: getPDFViewURL(d.url), goTo: "menu" });
|
||||
navigate("generalWebView", { uri: getPDFViewURL(d.url) });
|
||||
SheetManager.hide("NewsReleaseInfo");
|
||||
};
|
||||
|
||||
|
||||
@@ -29,9 +29,8 @@ export const SpecialTrainInfoBox: FC<props> = ({ navigate }) => {
|
||||
}, []);
|
||||
|
||||
const onPressItem: (d: specialDataType) => void = (d) => {
|
||||
navigate("howto", {
|
||||
info: getPDFViewURL("https://www.jr-shikoku.co.jp" + d.address),
|
||||
goTo: "menu",
|
||||
navigate("generalWebView", {
|
||||
uri: getPDFViewURL("https://www.jr-shikoku.co.jp" + d.address),
|
||||
});
|
||||
SheetManager.hide("SpecialTrainInfo");
|
||||
};
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { View, TouchableOpacity, Linking, Platform, Image, useWindowDimensions } from "react-native";
|
||||
import { View, TouchableOpacity, Platform, Image, useWindowDimensions } from "react-native";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { useResponsive } from "@/lib/responsive";
|
||||
import { openExternalUrl } from "@/lib/openExternalUrl";
|
||||
|
||||
|
||||
export const TitleBar = () => {
|
||||
@@ -22,9 +23,9 @@ export const TitleBar = () => {
|
||||
}}
|
||||
>
|
||||
<TouchableOpacity
|
||||
onPress={() => Linking.openURL("https://www.jr-shikoku.co.jp")}
|
||||
onPress={() => openExternalUrl("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,7 +1,8 @@
|
||||
import React from "react";
|
||||
import { Linking, View } from "react-native";
|
||||
import { View } from "react-native";
|
||||
import { UsefulBox } from "@/components/TrainMenu/UsefulBox";
|
||||
import { MaterialCommunityIcons } from "@expo/vector-icons";
|
||||
import { openExternalUrl } from "@/lib/openExternalUrl";
|
||||
export const TopMenuButton = () => {
|
||||
const buttonList:{
|
||||
backgroundColor: string;
|
||||
@@ -13,14 +14,14 @@ export const TopMenuButton = () => {
|
||||
backgroundColor: "#F89038",
|
||||
icon: "train-car",
|
||||
onPress: () =>
|
||||
Linking.openURL("https://www.jr-shikoku.co.jp/01_trainbus/sp/"),
|
||||
openExternalUrl("https://www.jr-shikoku.co.jp/01_trainbus/sp/"),
|
||||
title: "駅・鉄道情報",
|
||||
},
|
||||
{
|
||||
backgroundColor: "#EA4752",
|
||||
icon: "google-spreadsheet",
|
||||
onPress: () =>
|
||||
Linking.openURL(
|
||||
openExternalUrl(
|
||||
"https://www.jr-shikoku.co.jp/01_trainbus/jikoku/sp/#mainprice-box"
|
||||
),
|
||||
title: "運賃表",
|
||||
@@ -28,7 +29,7 @@ export const TopMenuButton = () => {
|
||||
{
|
||||
backgroundColor: "#91C31F",
|
||||
icon: "clipboard-list-outline",
|
||||
onPress: () => Linking.openURL("https://www.jr-shikoku.co.jp/e5489/"),
|
||||
onPress: () => openExternalUrl("https://www.jr-shikoku.co.jp/e5489/"),
|
||||
title: "予約",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -0,0 +1,519 @@
|
||||
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>
|
||||
);
|
||||
@@ -6,13 +6,12 @@ import {
|
||||
StyleSheet,
|
||||
Image,
|
||||
TouchableOpacity,
|
||||
Linking,
|
||||
} from "react-native";
|
||||
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";
|
||||
@@ -27,6 +26,7 @@ import {
|
||||
normalizeJrDataSystemEnvironment,
|
||||
resolveJrDataSystemEnvironment,
|
||||
} from "@/lib/jrDataSystemEnvironment";
|
||||
import { openExternalUrl } from "@/lib/openExternalUrl";
|
||||
|
||||
const HUB_LOGO_PNG = require("@/assets/relationLogo/unyohub_logo.webp");
|
||||
const ELESITE_LOGO_PNG = require("@/assets/relationLogo/elesite_logo.jpg");
|
||||
@@ -206,7 +206,7 @@ const DataSourceAccordionCard: React.FC<DataSourceAccordionCardProps> = ({
|
||||
styles.bodyLink,
|
||||
{ borderTopColor: colors.borderSecondary },
|
||||
]}
|
||||
onPress={() => Linking.openURL(linkUrl)}
|
||||
onPress={() => openExternalUrl(linkUrl)}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<MaterialCommunityIcons
|
||||
@@ -298,12 +298,16 @@ export const DataSourceSettings = () => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
AS.getItem(STORAGE_KEYS.USE_UNYOHUB).then((value) => {
|
||||
setUseUnyohub(value === true || value === "true");
|
||||
});
|
||||
AS.getItem(STORAGE_KEYS.USE_ELESITE).then((value) => {
|
||||
setUseElesite(value === true || value === "true");
|
||||
});
|
||||
AS.getItem(STORAGE_KEYS.USE_UNYOHUB)
|
||||
.then((value) => {
|
||||
setUseUnyohub(value === true || value === "true");
|
||||
})
|
||||
.catch(() => setUseUnyohub(false));
|
||||
AS.getItem(STORAGE_KEYS.USE_ELESITE)
|
||||
.then((value) => {
|
||||
setUseElesite(value === true || value === "true");
|
||||
})
|
||||
.catch(() => setUseElesite(false));
|
||||
AS.getItem(STORAGE_KEYS.JR_DATA_SYSTEM_ENV)
|
||||
.then((value) => {
|
||||
const env = normalizeJrDataSystemEnvironment(value);
|
||||
|
||||
@@ -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";
|
||||
@@ -18,9 +18,9 @@ export const NotificationSettings = () => {
|
||||
const [informations, setInformations] = useState(false);
|
||||
const [strangeTrain, setStrangeTrain] = useState(false);
|
||||
useEffect(() => {
|
||||
AS.getItem(STORAGE_KEYS.TRA_INFO_EX).then(setTraInfoEX);
|
||||
AS.getItem(STORAGE_KEYS.INFORMATIONS).then(setInformations);
|
||||
AS.getItem(STORAGE_KEYS.STRANGE_TRAIN).then(setStrangeTrain);
|
||||
AS.getItem(STORAGE_KEYS.TRA_INFO_EX).then(setTraInfoEX).catch(() => {});
|
||||
AS.getItem(STORAGE_KEYS.INFORMATIONS).then(setInformations).catch(() => {});
|
||||
AS.getItem(STORAGE_KEYS.STRANGE_TRAIN).then(setStrangeTrain).catch(() => {});
|
||||
}, []);
|
||||
|
||||
const setRegister = () => {
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
import React, { useCallback, useMemo, useState } from "react";
|
||||
import {
|
||||
FlatList,
|
||||
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";
|
||||
import { openExternalUrl } from "@/lib/openExternalUrl";
|
||||
|
||||
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={() => openExternalUrl(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,6 +22,10 @@ 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();
|
||||
@@ -132,6 +136,21 @@ 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 = {
|
||||
@@ -580,6 +599,49 @@ 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>
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
ScrollView,
|
||||
Linking,
|
||||
Image,
|
||||
} from "react-native";
|
||||
import * as Updates from "expo-updates";
|
||||
@@ -13,11 +12,13 @@ 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";
|
||||
const versionCode = "7.2.1";
|
||||
import { MaterialCommunityIcons } from "@expo/vector-icons";
|
||||
import { openExternalUrl } from "@/lib/openExternalUrl";
|
||||
const versionCode = "7.2.2";
|
||||
|
||||
export const SettingTopPage = ({
|
||||
testNFC,
|
||||
@@ -42,7 +43,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,
|
||||
@@ -88,14 +89,15 @@ 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" })
|
||||
}
|
||||
@@ -104,24 +106,28 @@ 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" })
|
||||
}
|
||||
@@ -132,6 +138,7 @@ export const SettingTopPage = ({
|
||||
string={`通知設定${
|
||||
expoPushToken == "" ? "(通知設定をオンにしてください)" : "(β)"
|
||||
}`}
|
||||
icon="bell-outline"
|
||||
onPress={() =>
|
||||
navigation.navigate("setting", { screen: "NotificationSettings" })
|
||||
}
|
||||
@@ -139,13 +146,22 @@ 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" })
|
||||
}
|
||||
@@ -155,21 +171,24 @@ export const SettingTopPage = ({
|
||||
<SectionHeader title="その他" />
|
||||
<SettingList
|
||||
string="プライバシーポリシー"
|
||||
icon="shield-lock-outline"
|
||||
onPress={() =>
|
||||
Linking.openURL(
|
||||
"https://nexcloud.haruk.in/sites/press-harukin/JRShikokuApps/policy"
|
||||
openExternalUrl(
|
||||
"https://xprocess.haruk.in/JR-shikoku-Apps-Common/NativeAppsPrivacyPolicy"
|
||||
)
|
||||
}
|
||||
/>
|
||||
<SettingList
|
||||
string="開発情報"
|
||||
onPress={() =>
|
||||
Linking.openURL("https://nexcloud.haruk.in/s/2WFEN5oLPGtrRqT")
|
||||
}
|
||||
string="運営Twitter"
|
||||
icon="twitter"
|
||||
onPress={() => openExternalUrl("https://twitter.com/Xprocess_main")}
|
||||
/>
|
||||
<SettingList
|
||||
string="運営Twitter"
|
||||
onPress={() => Linking.openURL("https://twitter.com/Xprocess_main")}
|
||||
string="オープンソースライセンス"
|
||||
icon="license"
|
||||
onPress={() =>
|
||||
navigation.navigate("setting", { screen: "OssLicenses" })
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
</ScrollView>
|
||||
@@ -195,21 +214,31 @@ export const SettingTopPage = ({
|
||||
);
|
||||
};
|
||||
|
||||
const SettingList = ({ string, onPress, disabled = false }: { string: string; onPress: () => void; disabled?: boolean }) => {
|
||||
const SettingList = ({
|
||||
string,
|
||||
icon,
|
||||
onPress,
|
||||
disabled = false,
|
||||
}: {
|
||||
string: string;
|
||||
icon?: React.ComponentProps<typeof MaterialCommunityIcons>["name"];
|
||||
onPress: () => void;
|
||||
disabled?: boolean;
|
||||
}) => {
|
||||
const { colors } = useThemeColors();
|
||||
return (
|
||||
<ListItem
|
||||
// @ts-ignore: TouchableScale props passed through Component
|
||||
// @ts-expect-error RNEUIのListItem型はComponentに渡したTouchableScaleのpropsを含まない
|
||||
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 style={{ color: colors.text }}>{string}</ListItem.Title>
|
||||
<ListItem.Title numberOfLines={1} 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,6 +28,8 @@ 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,
|
||||
@@ -298,6 +300,28 @@ 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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,12 +3,12 @@ import { lightColors } from "@/lib/theme";
|
||||
import { MaterialCommunityIcons, Ionicons, FontAwesome5 } from "@expo/vector-icons";
|
||||
import { FC } from "react";
|
||||
import {
|
||||
Linking,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
useWindowDimensions,
|
||||
View,
|
||||
} from "react-native";
|
||||
import { openExternalUrl } from "@/lib/openExternalUrl";
|
||||
|
||||
type Props = {
|
||||
item: StationProps[];
|
||||
@@ -129,14 +129,14 @@ export const SpotSign: FC<Props> = ({ item, isCurrentStation }) => {
|
||||
}}
|
||||
>
|
||||
<TouchableOpacity
|
||||
onPress={() => item[0].StationMap && Linking.openURL(item[0].StationMap)}
|
||||
onPress={() => item[0].StationMap && openExternalUrl(item[0].StationMap)}
|
||||
hitSlop={{ top: 8, bottom: 8, left: 12, right: 12 }}
|
||||
>
|
||||
<Ionicons name="map-outline" size={cardHeight * 0.15} color="white" />
|
||||
</TouchableOpacity>
|
||||
{item[0].JrHpUrl ? (
|
||||
<TouchableOpacity
|
||||
onPress={() => Linking.openURL(item[0].JrHpUrl)}
|
||||
onPress={() => openExternalUrl(item[0].JrHpUrl)}
|
||||
hitSlop={{ top: 8, bottom: 8, left: 12, right: 12 }}
|
||||
>
|
||||
<MaterialCommunityIcons name="web" size={cardHeight * 0.15} color="white" />
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as SplashScreen from "expo-splash-screen";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { Platform, StyleSheet } from "react-native";
|
||||
import { StyleSheet } from "react-native";
|
||||
import Animated, {
|
||||
Easing,
|
||||
useAnimatedStyle,
|
||||
@@ -19,24 +19,22 @@ 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.
|
||||
if (Platform.OS !== "web") {
|
||||
void SplashScreen.preventAutoHideAsync().catch(() => {
|
||||
// The native splash may already be hidden during a reload.
|
||||
});
|
||||
}
|
||||
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(Platform.OS !== "web");
|
||||
const [visible, setVisible] = useState(true);
|
||||
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 (Platform.OS === "web" || hasStarted.current) return;
|
||||
if (hasStarted.current) return;
|
||||
|
||||
hasStarted.current = true;
|
||||
if (fallbackTimer.current !== null) {
|
||||
@@ -80,8 +78,6 @@ 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,
|
||||
//@ts-ignore
|
||||
goTo: "StationDiagram",
|
||||
useShow: () => SheetManager.show("StationDetailView", { payload }),
|
||||
onExit: () => SheetManager.hide("StationDetailView"),
|
||||
}; //@ts-ignore
|
||||
};
|
||||
setTimeout(() => SheetManager.show("StationDetailView", { payload }), 50);
|
||||
} else {
|
||||
SheetManager.hide("StationDetailView");
|
||||
@@ -128,7 +128,6 @@ 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,
|
||||
//@ts-ignore
|
||||
goTo: "StationDiagram",
|
||||
useShow: () => SheetManager.show("StationDetailView", { payload }),
|
||||
onExit: () => SheetManager.hide("StationDetailView"),
|
||||
}; //@ts-ignore
|
||||
};
|
||||
setTimeout(() => SheetManager.show("StationDetailView", { payload }), 50);
|
||||
} else {
|
||||
SheetManager.hide("StationDetailView");
|
||||
@@ -132,7 +132,6 @@ 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,
|
||||
//@ts-ignore
|
||||
goTo: "StationDiagram",
|
||||
useShow: () => SheetManager.show("StationDetailView", { payload }),
|
||||
onExit: () => SheetManager.hide("StationDetailView"),
|
||||
}; //@ts-ignore
|
||||
};
|
||||
setTimeout(() => SheetManager.show("StationDetailView", { payload }), 50);
|
||||
} else {
|
||||
SheetManager.hide("StationDetailView");
|
||||
@@ -205,7 +205,6 @@ export const ListViewItem: FC<{
|
||||
from: "AllTrainIDList",
|
||||
};
|
||||
SheetManager.show("EachTrainInfo", {
|
||||
//@ts-ignore
|
||||
payload,
|
||||
onClose: () => {
|
||||
//alert(data);
|
||||
|
||||
@@ -69,11 +69,16 @@ export const StationDiagramView: FC<props> = ({ route }) => {
|
||||
// フィルタリング:終点路線、種別、行先、関係停車駅
|
||||
|
||||
const { keyList, allTrainDiagram } = useTrainDiagram();
|
||||
const { allCustomTrainData } = useCustomTrainData();
|
||||
const { todayOperation } = useOperationLogs();
|
||||
const { allCustomTrainData, activateCustomTrainData } = useCustomTrainData();
|
||||
const { todayOperation, activateOperationLogs } = useOperationLogs();
|
||||
const { useUnyohub: unyohubEnabled } = useUnyohub();
|
||||
const { useElesite: elesiteEnabled } = useElesite();
|
||||
|
||||
useEffect(() => {
|
||||
activateCustomTrainData();
|
||||
activateOperationLogs();
|
||||
}, [activateCustomTrainData, activateOperationLogs]);
|
||||
|
||||
const { goBack } = useNavigation();
|
||||
const { colors, fixed } = useThemeColors();
|
||||
const tabBarHeight = useBottomTabBarHeight();
|
||||
@@ -361,7 +366,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();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useRef, useState, useEffect, useLayoutEffect } from "react";
|
||||
import { View, Text, TouchableOpacity, Linking, Platform } from "react-native";
|
||||
import { View, Text, TouchableOpacity, Platform } from "react-native";
|
||||
import MapView, { Marker } from "react-native-maps";
|
||||
import { useCurrentTrainView } from "../stateBox/useCurrentTrain";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
@@ -13,6 +13,7 @@ import { MapsButton } from "./TrainMenu/MapsButton";
|
||||
import { useStationList } from "@/stateBox/useStationList";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
import { useUserPosition } from "@/stateBox/useUserPosition";
|
||||
import { openExternalUrl } from "@/lib/openExternalUrl";
|
||||
const ANDROID_USER_LOCATION_MARKER_SIZE = 18;
|
||||
const ANDROID_USER_LOCATION_INNER_SIZE = 8;
|
||||
|
||||
@@ -223,8 +224,8 @@ export default function TrainMenu({ style }) {
|
||||
icon="train-car"
|
||||
flex={1}
|
||||
onPressButton={() =>
|
||||
navigate("howto", {
|
||||
info: "https://xprocess.haruk.in/JR-shikoku-Apps-Common/info/train-position",
|
||||
navigate("generalWebView", {
|
||||
uri: "https://xprocess.haruk.in/JR-shikoku-Apps-Common/info/train-position",
|
||||
})
|
||||
}
|
||||
>
|
||||
@@ -243,7 +244,7 @@ export default function TrainMenu({ style }) {
|
||||
icon="clipboard-list-outline"
|
||||
flex={1}
|
||||
onPressButton={() =>
|
||||
Linking.openURL(
|
||||
openExternalUrl(
|
||||
"https://nexcloud.haruk.in/apps/forms/ZRHjWFF7znr5Xjr2"
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
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
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user