Compare commits
46
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": {
|
"mcpServers": {
|
||||||
"Sentry": {
|
"Sentry": {
|
||||||
|
"type": "http",
|
||||||
"url": "https://mcp.sentry.dev/mcp/xprocess-m5/jr-shikoku-unofficial-apps"
|
"url": "https://mcp.sentry.dev/mcp/xprocess-m5/jr-shikoku-unofficial-apps"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -10,7 +10,8 @@
|
|||||||
"Read(//home/ubuntu/.qwen/debug/**)",
|
"Read(//home/ubuntu/.qwen/debug/**)",
|
||||||
"Bash(curl *)",
|
"Bash(curl *)",
|
||||||
"mcp__sentry__search_events",
|
"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ファイルだけです。
|
- 導入元は [emilkowalski/skills](https://github.com/emilkowalski/skills) の `animate-expo`、確認時点の固定コミットは `d23d7f88a2e21c9e4b1418c7abe420f5c1052ba7`(2026-08-29)です。今回導入するSkillは `animate-expo` の2ファイルだけです。
|
||||||
- `animate`、`ask-sonner`、`pick-ui-library` など他のSkillは、このリポジトリへ導入済みとは扱いません。
|
- `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は正本から確認)
|
## 現在の構成(current valuesは正本から確認)
|
||||||
|
|
||||||
- 現行の主要世代は Expo SDK 57、React Native 0.86系、React 19系です。patch/minorをこのファイルへ固定せず、正確な値は `package.json` と `yarn.lock` から確認してください。
|
- 現行の主要世代は 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 { useAppBootstrap } from "./lib/useAppBootstrap";
|
||||||
import { useStartupDeepLinkNavigation } from "./lib/useStartupDeepLinkNavigation";
|
import { useStartupDeepLinkNavigation } from "./lib/useStartupDeepLinkNavigation";
|
||||||
import { AppRootLayout } from "./components/AppRootLayout";
|
import { AppRootLayout } from "./components/AppRootLayout";
|
||||||
|
import { AppErrorBoundary } from "./components/AppErrorBoundary";
|
||||||
|
import {
|
||||||
|
markStartup,
|
||||||
|
markStartupOnce,
|
||||||
|
scheduleStartupReports,
|
||||||
|
} from "./lib/devStartupPerformance";
|
||||||
|
|
||||||
initializeSentry();
|
initializeSentry();
|
||||||
|
markStartupOnce("app-module-ready", "app module ready");
|
||||||
|
|
||||||
LogBox.ignoreLogs([
|
LogBox.ignoreLogs([
|
||||||
"ViewPropTypes will be removed",
|
"ViewPropTypes will be removed",
|
||||||
@@ -35,9 +42,19 @@ if (Platform.OS === "android") {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default wrapWithSentry(function App() {
|
export default wrapWithSentry(function App() {
|
||||||
|
markStartupOnce("app-render", "App render");
|
||||||
|
scheduleStartupReports();
|
||||||
useAppLifecycleObservability({ nativeScreensMode, rootTabSceneMode });
|
useAppLifecycleObservability({ nativeScreensMode, rootTabSceneMode });
|
||||||
useAppBootstrap();
|
useAppBootstrap();
|
||||||
useStartupDeepLinkNavigation();
|
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 { initIcon } from "./lib/initIcon";
|
||||||
import { Top } from "./Top";
|
import { Top } from "./Top";
|
||||||
import { MenuPage } from "./MenuPage";
|
import { MenuPage } from "./MenuPage";
|
||||||
|
import { AppErrorBoundary } from "./components/AppErrorBoundary";
|
||||||
import "./components/ActionSheetComponents/sheets";
|
import "./components/ActionSheetComponents/sheets";
|
||||||
import { rootNavigationRef } from "./lib/rootNavigation";
|
import { rootNavigationRef } from "./lib/rootNavigation";
|
||||||
import WebView from "react-native-webview";
|
import WebView from "react-native-webview";
|
||||||
@@ -26,10 +27,17 @@ import type { RootTabParamList } from "@/types/navigation";
|
|||||||
import { useThemeColors } from "./lib/theme";
|
import { useThemeColors } from "./lib/theme";
|
||||||
import { telemetry } from "./lib/observability/telemetry";
|
import { telemetry } from "./lib/observability/telemetry";
|
||||||
import { useAppContainerController } from "./lib/useAppContainerController";
|
import { useAppContainerController } from "./lib/useAppContainerController";
|
||||||
|
import { useCurrentTrainPolling } from "./stateBox/useCurrentTrain";
|
||||||
import {
|
import {
|
||||||
getTabBarAppearance,
|
getTabBarAppearance,
|
||||||
TabBarBackground,
|
TabBarBackground,
|
||||||
} from "./components/AppContainer/TabBarBackground";
|
} from "./components/AppContainer/TabBarBackground";
|
||||||
|
import {
|
||||||
|
markStartup,
|
||||||
|
markStartupOnce,
|
||||||
|
markStartupState,
|
||||||
|
} from "./lib/devStartupPerformance";
|
||||||
|
import { PositionsStartupReadyContext } from "./lib/positionsStartupContext";
|
||||||
|
|
||||||
const startupPreloadTelemetry = telemetry.scope("startup.preload");
|
const startupPreloadTelemetry = telemetry.scope("startup.preload");
|
||||||
const positionsRootTelemetry = telemetry.scope("positions.root");
|
const positionsRootTelemetry = telemetry.scope("positions.root");
|
||||||
@@ -38,27 +46,24 @@ const operationRootTelemetry = telemetry.scope("operation.root");
|
|||||||
const Tab = createBottomTabNavigator<RootTabParamList>();
|
const Tab = createBottomTabNavigator<RootTabParamList>();
|
||||||
|
|
||||||
const HiddenStartupPreloadWebViews = ({
|
const HiddenStartupPreloadWebViews = ({
|
||||||
shouldPreloadPositions,
|
|
||||||
shouldPreloadInformation,
|
shouldPreloadInformation,
|
||||||
|
onInformationLoadStart,
|
||||||
|
onInformationLoadSettled,
|
||||||
}: {
|
}: {
|
||||||
shouldPreloadPositions: boolean;
|
|
||||||
shouldPreloadInformation: boolean;
|
shouldPreloadInformation: boolean;
|
||||||
|
onInformationLoadStart: () => void;
|
||||||
|
onInformationLoadSettled: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
const [positionsLoaded, setPositionsLoaded] = useState(false);
|
|
||||||
const [informationLoaded, setInformationLoaded] = useState(false);
|
const [informationLoaded, setInformationLoaded] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
telemetry.context("startup_hidden_preload", {
|
telemetry.context("startup_hidden_preload", {
|
||||||
shouldPreloadPositions,
|
|
||||||
shouldPreloadInformation,
|
shouldPreloadInformation,
|
||||||
positionsLoaded,
|
|
||||||
informationLoaded,
|
informationLoaded,
|
||||||
});
|
});
|
||||||
}, [
|
}, [
|
||||||
informationLoaded,
|
informationLoaded,
|
||||||
positionsLoaded,
|
|
||||||
shouldPreloadInformation,
|
shouldPreloadInformation,
|
||||||
shouldPreloadPositions,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const hiddenStyle = useMemo(
|
const hiddenStyle = useMemo(
|
||||||
@@ -74,34 +79,26 @@ const HiddenStartupPreloadWebViews = ({
|
|||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
const showPositions = shouldPreloadPositions && !positionsLoaded;
|
|
||||||
const showInformation = shouldPreloadInformation && !informationLoaded;
|
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 (
|
return (
|
||||||
<View pointerEvents="none" style={hiddenStyle}>
|
<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 ? (
|
{showInformation ? (
|
||||||
<WebView
|
<WebView
|
||||||
source={{ uri: "https://www.jr-shikoku.co.jp/info/" }}
|
source={{ uri: "https://www.jr-shikoku.co.jp/info/" }}
|
||||||
@@ -109,15 +106,18 @@ const HiddenStartupPreloadWebViews = ({
|
|||||||
javaScriptEnabled
|
javaScriptEnabled
|
||||||
setSupportMultipleWindows={false}
|
setSupportMultipleWindows={false}
|
||||||
onLoadStart={() => {
|
onLoadStart={() => {
|
||||||
|
onInformationLoadStart();
|
||||||
setAppLifecycleWebViewActive("startup_hidden_operation", true);
|
setAppLifecycleWebViewActive("startup_hidden_operation", true);
|
||||||
startupPreloadTelemetry.info("operation hidden preload loadStart");
|
startupPreloadTelemetry.info("operation hidden preload loadStart");
|
||||||
}}
|
}}
|
||||||
onLoadEnd={() => {
|
onLoadEnd={() => {
|
||||||
|
onInformationLoadSettled();
|
||||||
setAppLifecycleWebViewActive("startup_hidden_operation", false);
|
setAppLifecycleWebViewActive("startup_hidden_operation", false);
|
||||||
startupPreloadTelemetry.info("operation hidden preload loadEnd");
|
startupPreloadTelemetry.info("operation hidden preload loadEnd");
|
||||||
setInformationLoaded(true);
|
setInformationLoaded(true);
|
||||||
}}
|
}}
|
||||||
onError={() => {
|
onError={() => {
|
||||||
|
onInformationLoadSettled();
|
||||||
setAppLifecycleWebViewActive("startup_hidden_operation", false);
|
setAppLifecycleWebViewActive("startup_hidden_operation", false);
|
||||||
startupPreloadTelemetry.error("operation hidden preload error");
|
startupPreloadTelemetry.error("operation hidden preload error");
|
||||||
}}
|
}}
|
||||||
@@ -143,6 +143,11 @@ const DeferredPositionsRoot = (props: DeferredRootProps) => {
|
|||||||
const [hasActivated, setHasActivated] = useState(false);
|
const [hasActivated, setHasActivated] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
markStartupState("positions-root-gate", "positions root gate state", {
|
||||||
|
focused: isRootFocused,
|
||||||
|
activated: hasActivated,
|
||||||
|
shouldActivate,
|
||||||
|
});
|
||||||
telemetry.context("positions_root_gate", {
|
telemetry.context("positions_root_gate", {
|
||||||
focused: isRootFocused,
|
focused: isRootFocused,
|
||||||
activated: hasActivated,
|
activated: hasActivated,
|
||||||
@@ -152,12 +157,19 @@ const DeferredPositionsRoot = (props: DeferredRootProps) => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!shouldActivate || hasActivated) return;
|
if (!shouldActivate || hasActivated) return;
|
||||||
|
markStartup("positions root activation scheduled", {
|
||||||
|
focused: isRootFocused,
|
||||||
|
});
|
||||||
positionsRootTelemetry.breadcrumb({
|
positionsRootTelemetry.breadcrumb({
|
||||||
message: "positions root activation",
|
message: "positions root activation",
|
||||||
data: {
|
data: {
|
||||||
reason: "first_root_visit",
|
reason: "first_root_visit",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
markStartupOnce(
|
||||||
|
"positions-root-activated",
|
||||||
|
"positions root activation committed",
|
||||||
|
);
|
||||||
setHasActivated(true);
|
setHasActivated(true);
|
||||||
}, [hasActivated, shouldActivate]);
|
}, [hasActivated, shouldActivate]);
|
||||||
|
|
||||||
@@ -165,7 +177,11 @@ const DeferredPositionsRoot = (props: DeferredRootProps) => {
|
|||||||
return <View style={{ flex: 1 }} />;
|
return <View style={{ flex: 1 }} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <Top />;
|
return (
|
||||||
|
<AppErrorBoundary scope="positions">
|
||||||
|
<Top />
|
||||||
|
</AppErrorBoundary>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const PositionsTabScreen = memo(() => {
|
const PositionsTabScreen = memo(() => {
|
||||||
@@ -186,6 +202,11 @@ const DeferredInformationRoot = (props: DeferredRootProps) => {
|
|||||||
const [hasActivated, setHasActivated] = useState(false);
|
const [hasActivated, setHasActivated] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
markStartupState("operation-root-gate", "operation root gate state", {
|
||||||
|
focused: isRootFocused,
|
||||||
|
activated: hasActivated,
|
||||||
|
shouldActivate,
|
||||||
|
});
|
||||||
telemetry.context("operation_root_gate", {
|
telemetry.context("operation_root_gate", {
|
||||||
focused: isRootFocused,
|
focused: isRootFocused,
|
||||||
activated: hasActivated,
|
activated: hasActivated,
|
||||||
@@ -195,12 +216,19 @@ const DeferredInformationRoot = (props: DeferredRootProps) => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!shouldActivate || hasActivated) return;
|
if (!shouldActivate || hasActivated) return;
|
||||||
|
markStartup("operation root activation scheduled", {
|
||||||
|
focused: isRootFocused,
|
||||||
|
});
|
||||||
operationRootTelemetry.breadcrumb({
|
operationRootTelemetry.breadcrumb({
|
||||||
message: "operation root activation",
|
message: "operation root activation",
|
||||||
data: {
|
data: {
|
||||||
reason: isRootFocused ? "first_focus" : "background_prewarm",
|
reason: isRootFocused ? "first_focus" : "background_prewarm",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
markStartupOnce(
|
||||||
|
"operation-root-activated",
|
||||||
|
"operation root activation committed",
|
||||||
|
);
|
||||||
setHasActivated(true);
|
setHasActivated(true);
|
||||||
}, [hasActivated, isRootFocused, shouldActivate]);
|
}, [hasActivated, isRootFocused, shouldActivate]);
|
||||||
|
|
||||||
@@ -208,9 +236,19 @@ const DeferredInformationRoot = (props: DeferredRootProps) => {
|
|||||||
return <View style={{ flex: 1 }} />;
|
return <View style={{ flex: 1 }} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <TNDView />;
|
return (
|
||||||
|
<AppErrorBoundary scope="information">
|
||||||
|
<TNDView />
|
||||||
|
</AppErrorBoundary>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const MenuTabScreen = () => (
|
||||||
|
<AppErrorBoundary scope="topMenu">
|
||||||
|
<MenuPage />
|
||||||
|
</AppErrorBoundary>
|
||||||
|
);
|
||||||
|
|
||||||
const InformationTabScreen = memo(() => {
|
const InformationTabScreen = memo(() => {
|
||||||
const { shouldActivate, isRootFocused } = useContext(
|
const { shouldActivate, isRootFocused } = useContext(
|
||||||
InformationRootGateContext,
|
InformationRootGateContext,
|
||||||
@@ -225,6 +263,7 @@ const InformationTabScreen = memo(() => {
|
|||||||
InformationTabScreen.displayName = "InformationTabScreen";
|
InformationTabScreen.displayName = "InformationTabScreen";
|
||||||
|
|
||||||
export function AppContainer() {
|
export function AppContainer() {
|
||||||
|
markStartupOnce("app-container-render", "AppContainer render");
|
||||||
const controller = useAppContainerController();
|
const controller = useAppContainerController();
|
||||||
const {
|
const {
|
||||||
areaInfo,
|
areaInfo,
|
||||||
@@ -235,14 +274,47 @@ export function AppContainer() {
|
|||||||
startupInitialRoute,
|
startupInitialRoute,
|
||||||
hasVisitedPositions,
|
hasVisitedPositions,
|
||||||
hasVisitedInformation,
|
hasVisitedInformation,
|
||||||
|
positionsInitialLoadReady,
|
||||||
currentRootRoute,
|
currentRootRoute,
|
||||||
fontLoaded,
|
fontLoaded,
|
||||||
startupIconFontsLoaded,
|
startupIconFontsLoaded,
|
||||||
shouldPreloadPositions,
|
|
||||||
shouldPreloadInformation,
|
shouldPreloadInformation,
|
||||||
|
handlePositionsInitialLoadReady,
|
||||||
|
handlePositionsStackReady,
|
||||||
|
handlePositionsWebViewLoadStarted,
|
||||||
|
handleOperationWebViewLoadStarted,
|
||||||
|
handleOperationWebViewLoadSettled,
|
||||||
|
positionsStackReady,
|
||||||
|
positionsWebViewLoadStarted,
|
||||||
|
operationWebViewPreloadActive,
|
||||||
|
operationWebViewLoadStarted,
|
||||||
|
operationWebViewLoadSettled,
|
||||||
} = controller;
|
} = controller;
|
||||||
|
|
||||||
|
// トップメニューの列車情報だけは、画面のmountや走行位置WebViewを待たず
|
||||||
|
// 既存のCurrentTrainProviderを購読して初回取得を開始する。
|
||||||
|
useCurrentTrainPolling(
|
||||||
|
startupInitialRoute === "topMenu" &&
|
||||||
|
(currentRootRoute === null || currentRootRoute === "topMenu"),
|
||||||
|
);
|
||||||
|
|
||||||
const { isDark } = useThemeColors();
|
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 = {
|
const linking = {
|
||||||
prefixes: ["jrshikoku://"],
|
prefixes: ["jrshikoku://"],
|
||||||
@@ -284,7 +356,7 @@ export function AppContainer() {
|
|||||||
tabBarIcon: initIcon(icon as any, iconFamily, tabBarBadge, isInfo),
|
tabBarIcon: initIcon(icon as any, iconFamily, tabBarBadge, isInfo),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (!fontLoaded || !startupIconFontsLoaded || !startupInitialRoute) {
|
if (!fontLoaded || !startupInitialRoute) {
|
||||||
return (
|
return (
|
||||||
<View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
|
<View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
|
||||||
<ActivityIndicator size="large" />
|
<ActivityIndicator size="large" />
|
||||||
@@ -299,102 +371,119 @@ export function AppContainer() {
|
|||||||
onReady={controller.handleNavigationReady}
|
onReady={controller.handleNavigationReady}
|
||||||
onStateChange={controller.handleNavigationStateChange}
|
onStateChange={controller.handleNavigationStateChange}
|
||||||
>
|
>
|
||||||
<PositionsRootGateContext.Provider
|
<PositionsStartupReadyContext.Provider
|
||||||
value={{
|
value={{
|
||||||
shouldActivate: hasVisitedPositions,
|
stackReady: positionsStackReady,
|
||||||
isRootFocused: currentRootRoute === "positions",
|
webViewLoadStarted: positionsWebViewLoadStarted,
|
||||||
|
initialLoadReady: positionsInitialLoadReady,
|
||||||
|
operationWebViewPreloadPending,
|
||||||
|
operationWebViewLoadStarted,
|
||||||
|
operationWebViewLoadSettled,
|
||||||
|
notifyInitialLoadReady: handlePositionsInitialLoadReady,
|
||||||
|
notifyStackReady: handlePositionsStackReady,
|
||||||
|
notifyWebViewLoadStarted: handlePositionsWebViewLoadStarted,
|
||||||
|
notifyOperationWebViewLoadStarted: handleOperationWebViewLoadStarted,
|
||||||
|
notifyOperationWebViewLoadSettled: handleOperationWebViewLoadSettled,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<InformationRootGateContext.Provider
|
<PositionsRootGateContext.Provider
|
||||||
value={{
|
value={{
|
||||||
shouldActivate: hasVisitedInformation,
|
shouldActivate: hasVisitedPositions,
|
||||||
isRootFocused: currentRootRoute === "information",
|
isRootFocused: currentRootRoute === "positions",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Tab.Navigator
|
<InformationRootGateContext.Provider
|
||||||
id="rootTabs"
|
value={{
|
||||||
detachInactiveScreens={false}
|
shouldActivate: hasVisitedInformation,
|
||||||
initialRouteName={startupInitialRoute}
|
isRootFocused: currentRootRoute === "information",
|
||||||
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
|
<Tab.Navigator
|
||||||
{...getTabProps(
|
id="rootTabs"
|
||||||
"positions",
|
detachInactiveScreens={false}
|
||||||
"走行位置",
|
initialRouteName={startupInitialRoute}
|
||||||
"bar-chart",
|
screenListeners={({ route }) => ({
|
||||||
"AntDesign",
|
tabPress: (event: EventArg<"tabPress", true>) => {
|
||||||
)}
|
controller.handleRootTabPress(route.name, event);
|
||||||
component={PositionsTabScreen}
|
},
|
||||||
listeners={{
|
focus: () => {
|
||||||
tabPress: controller.activatePositionsFromTabPress,
|
controller.handleRootTabFocus(route.name);
|
||||||
focus: controller.activatePositionsFromFocus,
|
},
|
||||||
blur: controller.preservePositionsOnBlur,
|
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
|
<Tab.Screen
|
||||||
{...getTabProps("topMenu", "トップメニュー", "radio", "Ionicons")}
|
{...getTabProps(
|
||||||
component={MenuPage}
|
"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
|
<Tab.Screen
|
||||||
{...getTabProps(
|
{...getTabProps(
|
||||||
"information",
|
"information",
|
||||||
"運行情報",
|
"運行情報",
|
||||||
"train",
|
"train",
|
||||||
"Ionicons",
|
"Ionicons",
|
||||||
areaInfo ? areaIconBadgeText : undefined,
|
areaInfo ? areaIconBadgeText : undefined,
|
||||||
isInfo,
|
isInfo,
|
||||||
)}
|
)}
|
||||||
component={InformationTabScreen}
|
component={InformationTabScreen}
|
||||||
listeners={{
|
listeners={{
|
||||||
tabPress: controller.activateInformationFromTabPress,
|
tabPress: controller.activateInformationFromTabPress,
|
||||||
focus: controller.activateInformationFromFocus,
|
focus: controller.activateInformationFromFocus,
|
||||||
blur: controller.preserveInformationOnBlur,
|
blur: controller.preserveInformationOnBlur,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Tab.Navigator>
|
</Tab.Navigator>
|
||||||
</InformationRootGateContext.Provider>
|
</InformationRootGateContext.Provider>
|
||||||
</PositionsRootGateContext.Provider>
|
</PositionsRootGateContext.Provider>
|
||||||
|
</PositionsStartupReadyContext.Provider>
|
||||||
<HiddenStartupPreloadWebViews
|
<HiddenStartupPreloadWebViews
|
||||||
shouldPreloadPositions={shouldPreloadPositions}
|
|
||||||
shouldPreloadInformation={shouldPreloadInformation}
|
shouldPreloadInformation={shouldPreloadInformation}
|
||||||
|
onInformationLoadStart={handleOperationWebViewLoadStarted}
|
||||||
|
onInformationLoadSettled={handleOperationWebViewLoadSettled}
|
||||||
/>
|
/>
|
||||||
</NavigationContainer>
|
</NavigationContainer>
|
||||||
);
|
);
|
||||||
+36
-11
@@ -119,6 +119,7 @@ export default ({ route }) => {
|
|||||||
uri,
|
uri,
|
||||||
useExitButton = true,
|
useExitButton = true,
|
||||||
importRecordingDownloads = false,
|
importRecordingDownloads = false,
|
||||||
|
onExit,
|
||||||
} = route.params;
|
} = route.params;
|
||||||
const { goBack } = useNavigation();
|
const { goBack } = useNavigation();
|
||||||
const { fixed } = useThemeColors();
|
const { fixed } = useThemeColors();
|
||||||
@@ -132,6 +133,8 @@ export default ({ route }) => {
|
|||||||
DEFAULT_JR_DATA_SYSTEM_ENV,
|
DEFAULT_JR_DATA_SYSTEM_ENV,
|
||||||
);
|
);
|
||||||
const [resolvedUri, setResolvedUri] = React.useState("");
|
const [resolvedUri, setResolvedUri] = React.useState("");
|
||||||
|
const [webViewSourceUri, setWebViewSourceUri] = React.useState("");
|
||||||
|
const resolvedUriRef = React.useRef("");
|
||||||
const [isEnvironmentReady, setIsEnvironmentReady] = React.useState(false);
|
const [isEnvironmentReady, setIsEnvironmentReady] = React.useState(false);
|
||||||
const hasAlerted = React.useRef(false);
|
const hasAlerted = React.useRef(false);
|
||||||
const [isLoading, setIsLoading] = React.useState(true);
|
const [isLoading, setIsLoading] = React.useState(true);
|
||||||
@@ -148,6 +151,11 @@ export default ({ route }) => {
|
|||||||
const blankCountRef = React.useRef(0);
|
const blankCountRef = React.useRef(0);
|
||||||
|
|
||||||
const remount = React.useCallback(() => {
|
const remount = React.useCallback(() => {
|
||||||
|
// 現在のURLは復元用に使うが、通常のナビゲーションではsourceへ戻さない。
|
||||||
|
// sourceの更新はWebViewの再読込になるため、フィルター操作中に行うと状態が消える。
|
||||||
|
if (resolvedUriRef.current) {
|
||||||
|
setWebViewSourceUri(resolvedUriRef.current);
|
||||||
|
}
|
||||||
lastPongAt.current = Date.now(); // remount直後に誤検知しないようリセット
|
lastPongAt.current = Date.now(); // remount直後に誤検知しないようリセット
|
||||||
maxBodyLenRef.current = 0;
|
maxBodyLenRef.current = 0;
|
||||||
blankCountRef.current = 0;
|
blankCountRef.current = 0;
|
||||||
@@ -172,11 +180,12 @@ export default ({ route }) => {
|
|||||||
const nextUri = importRecordingDownloads
|
const nextUri = importRecordingDownloads
|
||||||
? rawUri
|
? rawUri
|
||||||
: rewriteJrDataSystemUrl(rawUri, nextEnvironment);
|
: rewriteJrDataSystemUrl(rawUri, nextEnvironment);
|
||||||
setResolvedUri(
|
const initialUri = importRecordingDownloads
|
||||||
importRecordingDownloads
|
? nextUri
|
||||||
? nextUri
|
: withJrDataSystemIconQuery(nextUri, iconSetting);
|
||||||
: withJrDataSystemIconQuery(nextUri, iconSetting),
|
resolvedUriRef.current = initialUri;
|
||||||
);
|
setResolvedUri(initialUri);
|
||||||
|
setWebViewSourceUri(initialUri);
|
||||||
setIsEnvironmentReady(true);
|
setIsEnvironmentReady(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -190,6 +199,9 @@ export default ({ route }) => {
|
|||||||
}, [iconSetting, uri, importRecordingDownloads]);
|
}, [iconSetting, uri, importRecordingDownloads]);
|
||||||
|
|
||||||
const handleReload = () => {
|
const handleReload = () => {
|
||||||
|
if (resolvedUriRef.current) {
|
||||||
|
setWebViewSourceUri(resolvedUriRef.current);
|
||||||
|
}
|
||||||
lastPongAt.current = Date.now();
|
lastPongAt.current = Date.now();
|
||||||
setHasError(false);
|
setHasError(false);
|
||||||
hasErrorRef.current = false;
|
hasErrorRef.current = false;
|
||||||
@@ -219,7 +231,9 @@ export default ({ route }) => {
|
|||||||
stack.pop();
|
stack.pop();
|
||||||
const previousUrl = stack[stack.length - 1];
|
const previousUrl = stack[stack.length - 1];
|
||||||
if (!previousUrl) return false;
|
if (!previousUrl) return false;
|
||||||
|
resolvedUriRef.current = previousUrl;
|
||||||
setResolvedUri(previousUrl);
|
setResolvedUri(previousUrl);
|
||||||
|
setWebViewSourceUri(previousUrl);
|
||||||
syncCanGoBack(false);
|
syncCanGoBack(false);
|
||||||
remount();
|
remount();
|
||||||
return true;
|
return true;
|
||||||
@@ -233,6 +247,14 @@ export default ({ route }) => {
|
|||||||
return handlePseudoBack();
|
return handlePseudoBack();
|
||||||
}, [handlePseudoBack]);
|
}, [handlePseudoBack]);
|
||||||
|
|
||||||
|
const handleExit = React.useCallback(() => {
|
||||||
|
try {
|
||||||
|
void onExit?.();
|
||||||
|
} finally {
|
||||||
|
goBack();
|
||||||
|
}
|
||||||
|
}, [goBack, onExit]);
|
||||||
|
|
||||||
const handleImportRecordingText = React.useCallback(
|
const handleImportRecordingText = React.useCallback(
|
||||||
async (content: string) => {
|
async (content: string) => {
|
||||||
try {
|
try {
|
||||||
@@ -277,20 +299,20 @@ export default ({ route }) => {
|
|||||||
if (canGoBack) {
|
if (canGoBack) {
|
||||||
if (handleWebViewBack()) return true;
|
if (handleWebViewBack()) return true;
|
||||||
}
|
}
|
||||||
goBack();
|
handleExit();
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const subscription = BackHandler.addEventListener("hardwareBackPress", onHardwareBack);
|
const subscription = BackHandler.addEventListener("hardwareBackPress", onHardwareBack);
|
||||||
return () => subscription.remove();
|
return () => subscription.remove();
|
||||||
}, [canGoBack, goBack, handleWebViewBack])
|
}, [canGoBack, handleExit, handleWebViewBack])
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<View style={{ height: "100%", backgroundColor: fixed.primary }}>
|
<View style={{ height: "100%", backgroundColor: fixed.primary }}>
|
||||||
{isEnvironmentReady && (
|
{isEnvironmentReady && (
|
||||||
<WebView
|
<WebView
|
||||||
key={webViewKey}
|
key={webViewKey}
|
||||||
source={{ uri: resolvedUri }}
|
source={{ uri: webViewSourceUri }}
|
||||||
contentMode="mobile"
|
contentMode="mobile"
|
||||||
allowsBackForwardNavigationGestures
|
allowsBackForwardNavigationGestures
|
||||||
setSupportMultipleWindows={false}
|
setSupportMultipleWindows={false}
|
||||||
@@ -354,7 +376,9 @@ export default ({ route }) => {
|
|||||||
iconSetting,
|
iconSetting,
|
||||||
);
|
);
|
||||||
if (rewrittenUrl !== request.url) {
|
if (rewrittenUrl !== request.url) {
|
||||||
|
resolvedUriRef.current = rewrittenUrl;
|
||||||
setResolvedUri(rewrittenUrl);
|
setResolvedUri(rewrittenUrl);
|
||||||
|
setWebViewSourceUri(rewrittenUrl);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -375,6 +399,7 @@ export default ({ route }) => {
|
|||||||
rewriteJrDataSystemUrl(navState.url, selectedEnvironment),
|
rewriteJrDataSystemUrl(navState.url, selectedEnvironment),
|
||||||
iconSetting,
|
iconSetting,
|
||||||
);
|
);
|
||||||
|
resolvedUriRef.current = nextUrl;
|
||||||
setResolvedUri((current) => (current === nextUrl ? current : nextUrl));
|
setResolvedUri((current) => (current === nextUrl ? current : nextUrl));
|
||||||
if (nextUrl !== "https://unyohub.2pd.jp/integration/succeeded.php") {
|
if (nextUrl !== "https://unyohub.2pd.jp/integration/succeeded.php") {
|
||||||
pushHistoryEntry(nextUrl);
|
pushHistoryEntry(nextUrl);
|
||||||
@@ -448,7 +473,7 @@ export default ({ route }) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (type === "back") return handleWebViewBack();
|
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>
|
<Text style={wvStyles.reloadButtonText}>再読み込み</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
{useExitButton && (
|
{useExitButton && (
|
||||||
<TouchableOpacity style={wvStyles.backButton} onPress={goBack}>
|
<TouchableOpacity style={wvStyles.backButton} onPress={handleExit}>
|
||||||
<Text style={wvStyles.backButtonText}>閉じる</Text>
|
<Text style={wvStyles.backButtonText}>閉じる</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
{useExitButton && !hasError && <BigButton onPress={goBack} string="閉じる" />}
|
{useExitButton && !hasError && <BigButton onPress={handleExit} string="閉じる" />}
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+38
-6
@@ -8,7 +8,6 @@ import { AS } from "@/storageControl";
|
|||||||
import { STORAGE_KEYS } from "@/constants";
|
import { STORAGE_KEYS } from "@/constants";
|
||||||
import { logger } from "@/utils/logger";
|
import { logger } from "@/utils/logger";
|
||||||
import TrainBase from "@/components/trainbaseview";
|
import TrainBase from "@/components/trainbaseview";
|
||||||
import HowTo from "@/howto";
|
|
||||||
import { Menu } from "@/menu";
|
import { Menu } from "@/menu";
|
||||||
import News from "@/components/news";
|
import News from "@/components/news";
|
||||||
import Setting from "@/components/Settings/settings";
|
import Setting from "@/components/Settings/settings";
|
||||||
@@ -22,6 +21,11 @@ import { useBottomTabBarHeight } from "@react-navigation/bottom-tabs";
|
|||||||
import GeneralWebView from "@/GeneralWebView";
|
import GeneralWebView from "@/GeneralWebView";
|
||||||
import { StationDiagramView } from "@/components/StationDiagram/StationDiagramView";
|
import { StationDiagramView } from "@/components/StationDiagram/StationDiagramView";
|
||||||
import { telemetry } from "@/lib/observability/telemetry";
|
import { telemetry } from "@/lib/observability/telemetry";
|
||||||
|
import {
|
||||||
|
markStartup,
|
||||||
|
markStartupOnce,
|
||||||
|
markStartupState,
|
||||||
|
} from "@/lib/devStartupPerformance";
|
||||||
import type { StackNavigationProp } from "@react-navigation/stack";
|
import type { StackNavigationProp } from "@react-navigation/stack";
|
||||||
import type {
|
import type {
|
||||||
RootTabParamList,
|
RootTabParamList,
|
||||||
@@ -31,6 +35,7 @@ const Stack = createStackNavigator<TopMenuStackParamList>();
|
|||||||
const topMenuScreenTelemetry = telemetry.scope("topMenu.screen");
|
const topMenuScreenTelemetry = telemetry.scope("topMenu.screen");
|
||||||
|
|
||||||
export function MenuPage() {
|
export function MenuPage() {
|
||||||
|
markStartupOnce("top-menu-render", "top menu render");
|
||||||
const { height, width } = useWindowDimensions();
|
const { height, width } = useWindowDimensions();
|
||||||
const { verticalScale } = useResponsive();
|
const { verticalScale } = useResponsive();
|
||||||
const tabBarHeight = useBottomTabBarHeight();
|
const tabBarHeight = useBottomTabBarHeight();
|
||||||
@@ -42,6 +47,7 @@ export function MenuPage() {
|
|||||||
const bgColor = isDark ? "#1c1c1e" : "#ffffff";
|
const bgColor = isDark ? "#1c1c1e" : "#ffffff";
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
markStartupOnce("top-menu-mounted", "top menu mounted");
|
||||||
topMenuScreenTelemetry.info("top menu mounted");
|
topMenuScreenTelemetry.info("top menu mounted");
|
||||||
return () => {
|
return () => {
|
||||||
topMenuScreenTelemetry.info("top menu unmounted");
|
topMenuScreenTelemetry.info("top menu unmounted");
|
||||||
@@ -49,6 +55,9 @@ export function MenuPage() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
markStartupState("top-menu-focus", "top menu focus state", {
|
||||||
|
focused: isFocused,
|
||||||
|
});
|
||||||
topMenuScreenTelemetry.breadcrumb({
|
topMenuScreenTelemetry.breadcrumb({
|
||||||
message: isFocused ? "top menu focused" : "top menu blurred",
|
message: isFocused ? "top menu focused" : "top menu blurred",
|
||||||
data: {
|
data: {
|
||||||
@@ -60,16 +69,40 @@ export function MenuPage() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
//ニュース表示
|
//ニュース表示
|
||||||
|
markStartup("top menu startup storage checks start");
|
||||||
AS.getItem(STORAGE_KEYS.NEWS_STATUS)
|
AS.getItem(STORAGE_KEYS.NEWS_STATUS)
|
||||||
.then((d) => {
|
.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)
|
AS.getItem(STORAGE_KEYS.ICON_SETTING)
|
||||||
.then((isSetIcon) => {
|
.then((isSetIcon) => {
|
||||||
|
markStartup("top menu icon setting read end", {
|
||||||
|
hasValue: isSetIcon !== null && isSetIcon !== undefined,
|
||||||
|
});
|
||||||
if (isSetIcon === "true") SheetManager.show("TrainIconUpdate");
|
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;
|
return undefined;
|
||||||
}, []);
|
}, []);
|
||||||
@@ -122,7 +155,7 @@ export function MenuPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
scrollRef.current?.scrollTo({
|
scrollRef.current?.scrollTo({
|
||||||
y: mapHeightRef.current - verticalScale(80),
|
y: Math.max(0, mapHeightRef.current - verticalScale(80)),
|
||||||
animated: true,
|
animated: true,
|
||||||
});
|
});
|
||||||
setMapMode(false);
|
setMapMode(false);
|
||||||
@@ -209,7 +242,6 @@ export function MenuPage() {
|
|||||||
options={{ ...optionData, gestureEnabled: false }}
|
options={{ ...optionData, gestureEnabled: false }}
|
||||||
component={AllTrainDiagramView}
|
component={AllTrainDiagramView}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen name="howto" options={optionData} component={HowTo} />
|
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="generalWebView"
|
name="generalWebView"
|
||||||
options={optionData}
|
options={optionData}
|
||||||
|
|||||||
@@ -1,73 +1,109 @@
|
|||||||
# JR四国 列車位置情報アプリ
|
# 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
|
- **言語**: TypeScript / JavaScript
|
||||||
- **状態管理**: React Context API + カスタムフック
|
- **ナビゲーション**: React Navigation 7(bottom tabs + stack。Expo Routerは未使用)
|
||||||
- **ストレージ**: AsyncStorage
|
- **状態管理**: React Context API + カスタムフック(`stateBox/`)
|
||||||
- **地図表示**: WebView + JavaScript Injection
|
- **ストレージ**: AsyncStorage、expo-sqlite
|
||||||
|
- **地図表示**: WebView + JavaScript Injection、react-native-maps
|
||||||
|
- **シート**: react-native-actions-sheet、@gorhom/bottom-sheet
|
||||||
- **日付処理**: dayjs
|
- **日付処理**: dayjs
|
||||||
- **アニメーション**: react-native-reanimated
|
- **アニメーション / ジェスチャー**: react-native-reanimated、react-native-gesture-handler(v2系)
|
||||||
|
- **監視**: Sentry
|
||||||
|
- **ウィジェット**: react-native-android-widget(Android)、`@bacons/apple-targets` によるWidgetKit拡張(iOS)
|
||||||
|
|
||||||
## 📦 プロジェクト構造
|
## 📦 プロジェクト構造
|
||||||
|
|
||||||
```
|
```
|
||||||
jrshikoku/
|
jrshikoku/
|
||||||
├── components/ # Reactコンポーネント
|
├── components/ # Reactコンポーネント
|
||||||
│ ├── Apps/ # アプリケーション機能
|
│ ├── Apps/ # 走行位置画面
|
||||||
│ │ └── FixedPositionBox/
|
│ │ └── FixedPositionBox/
|
||||||
│ │ └── hooks/ # カスタムフック
|
│ │ └── hooks/ # カスタムフック
|
||||||
|
│ ├── AndroidWidget/ # Androidウィジェット
|
||||||
|
│ ├── ActionSheetComponents/
|
||||||
│ ├── atom/ # 基本UIコンポーネント
|
│ ├── atom/ # 基本UIコンポーネント
|
||||||
│ ├── Menu/ # メニュー関連
|
│ ├── Menu/ # メニュー関連
|
||||||
│ ├── Settings/ # 設定画面
|
│ ├── Settings/ # 設定画面
|
||||||
│ └── TrainMenu/ # 列車メニュー
|
│ └── TrainMenu/ # 列車メニュー
|
||||||
├── stateBox/ # グローバル状態管理
|
├── stateBox/ # グローバル状態管理(Context + hooks)
|
||||||
├── lib/ # ユーティリティライブラリ
|
├── lib/ # ユーティリティライブラリ
|
||||||
│ └── eachTrainInfoCoreLib/ # 列車情報処理
|
│ ├── domain/ # 駅・列車identityのdomain型
|
||||||
|
│ ├── eachTrainInfoCoreLib/ # 列車情報処理
|
||||||
|
│ ├── observability/ # Sentry・テレメトリ
|
||||||
|
│ ├── theme/ # カラーテーマ
|
||||||
|
│ └── webview/ # WebView注入用データ
|
||||||
├── constants/ # 定数定義
|
├── constants/ # 定数定義
|
||||||
│ ├── intervals.ts # 時間間隔定数
|
│ ├── intervals.ts # 時間間隔定数
|
||||||
│ ├── api.ts # APIエンドポイント
|
│ ├── api.ts # APIエンドポイント
|
||||||
│ ├── storage.ts # StorageKeys
|
│ ├── storage.ts # StorageKeys
|
||||||
│ └── index.ts
|
│ └── index.ts
|
||||||
|
├── modules/ # ローカルExpoモジュール
|
||||||
|
│ ├── expo-felica-reader/ # FeliCa読み取り(iOS/Android)
|
||||||
|
│ └── expo-live-activity/ # Live Activities(iOS)
|
||||||
|
├── targets/widget/ # iOS WidgetKit拡張
|
||||||
├── types/ # TypeScript型定義
|
├── types/ # TypeScript型定義
|
||||||
├── utils/ # ユーティリティ関数
|
├── utils/ # ユーティリティ関数
|
||||||
│ ├── logger.ts # ロギング
|
│ ├── logger.ts # ロギング
|
||||||
│ └── seUtils.ts # SE判定処理
|
│ └── seUtils.ts # SE判定処理
|
||||||
|
├── tests/ # pure関数のテスト(node:test)
|
||||||
|
├── docs/ # 設計・changelog・検証記録
|
||||||
|
│ └── architecture/ # domain契約・ADR
|
||||||
├── assets/ # 静的リソース
|
├── assets/ # 静的リソース
|
||||||
└── config/ # 設定ファイル
|
└── config/ # 設定ファイル
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`android/` と `ios/` はprebuildで生成されるため、gitでは管理していません。
|
||||||
|
|
||||||
## 🛠️ セットアップ
|
## 🛠️ セットアップ
|
||||||
|
|
||||||
### 必要環境
|
### 必要環境
|
||||||
- Node.js 18以上
|
- Node.js(Expo SDK 57が対応するLTS。`yarn expo:doctor` で確認)
|
||||||
- npm または yarn
|
- Yarn(lockfileは `yarn.lock` のみ)
|
||||||
- Expo CLI
|
- EAS CLI(ビルドする場合)
|
||||||
|
|
||||||
### インストール
|
### インストールと起動
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 依存関係のインストール
|
# 依存関係のインストール(postinstallでpatch-packageが実行されます)
|
||||||
npm install
|
yarn install
|
||||||
|
|
||||||
# 開発サーバー起動
|
# 開発サーバー起動(dev client)
|
||||||
npm start
|
yarn start
|
||||||
|
|
||||||
# iOS実行
|
# ネイティブビルドして実行
|
||||||
npm run ios
|
yarn ios
|
||||||
|
yarn android
|
||||||
|
```
|
||||||
|
|
||||||
# Android実行
|
### 検証
|
||||||
npm run android
|
|
||||||
|
|
||||||
# Web実行
|
```bash
|
||||||
npm run web
|
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
|
```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`: 通過駅を含む列車データ生成
|
- `useTrainDataWithThrough`: 通過駅を含む列車データ生成
|
||||||
- `useDestinationStation`: 行先駅データの管理
|
- `useDestinationStation`: 行先駅データの管理
|
||||||
|
|
||||||
### グローバル状態フック
|
### グローバル状態フック(`stateBox/`)
|
||||||
|
|
||||||
- `useAllTrainDiagram`: 全列車ダイヤデータ
|
- `useAllTrainDiagram`: 全列車ダイヤデータ
|
||||||
|
- `useAreaInfo`: 運行情報スナップショット(エリア情報)
|
||||||
- `useBusAndTrainData`: バス・列車データ
|
- `useBusAndTrainData`: バス・列車データ
|
||||||
- `useCurrentTrain`: 現在選択中の列車
|
- `useCurrentTrain`: 現在の列車位置データ
|
||||||
|
- `useDeviceOrientationChange`: 画面の向き
|
||||||
|
- `useElesite`: えれサイト運用データ
|
||||||
- `useFavoriteStation`: お気に入り駅
|
- `useFavoriteStation`: お気に入り駅
|
||||||
|
- `useNotifications`: プッシュ通知と通知タップ時のルーティング
|
||||||
- `useStationList`: 全駅リスト
|
- `useStationList`: 全駅リスト
|
||||||
|
- `useTopMenu`: トップメニュー状態
|
||||||
|
- `useTrainDelayData`: 遅延情報
|
||||||
- `useTrainMenu`: 列車メニュー状態
|
- `useTrainMenu`: 列車メニュー状態
|
||||||
|
- `useUnyohub`: 鉄道運用Hub運用データ
|
||||||
|
- `useUserPosition`: ユーザーの現在地
|
||||||
|
|
||||||
## 📊 リファクタリング実績
|
## 📊 リファクタリング実績
|
||||||
|
|
||||||
@@ -165,17 +222,24 @@ AsyncStorage.getItem(STORAGE_KEYS.BUS_AND_TRAIN);
|
|||||||
|
|
||||||
## 🚦 API エンドポイント
|
## 🚦 API エンドポイント
|
||||||
|
|
||||||
|
エンドポイントは `constants/api.ts` の `API_ENDPOINTS` に集約しています。一覧と各URLは同ファイルを参照してください。
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// constants/api.ts
|
// constants/api.ts(抜粋)
|
||||||
|
const BASE_URL = 'https://jr-shikoku-api-data-storage.haruk.in';
|
||||||
|
|
||||||
export const API_ENDPOINTS = {
|
export const API_ENDPOINTS = {
|
||||||
TRAIN_DATA_API: 'https://api.haruk.in/dev/jrshikoku/trainList',
|
DIAGRAM_TODAY: `${BASE_URL}/tmp/diagram-today.json`,
|
||||||
DIAGRAM_TODAY: 'https://api.haruk.in/dev/jrshikoku/diagram/today',
|
OPERATION_INFO: `${BASE_URL}/operation-info/jr-shikoku/latest.json`,
|
||||||
STATION_LIST: 'https://storage.haruk.in/s/station.json',
|
DELAY_INFO: `${BASE_URL}/derived/delays/latest.json`,
|
||||||
// ... 他8個
|
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 | 遷移先 |
|
| URL | 遷移先 |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `jrshikoku://open/felica` | FeliCa履歴ページ |
|
| `jrshikoku://open/felica`(`felicahistorypage` も可) | FeliCa履歴ページ |
|
||||||
| `jrshikoku://open/traininfo` | 遅延速報EX |
|
| `jrshikoku://open/traininfo` | 遅延速報EX |
|
||||||
| `jrshikoku://open/operation` | 運行情報 |
|
| `jrshikoku://open/operation` | 運行情報 |
|
||||||
| `jrshikoku://open/settings` | 設定 |
|
| `jrshikoku://open/settings` | 設定 |
|
||||||
| `jrshikoku://open/topmenu` | トップメニュー |
|
| `jrshikoku://open/topmenu` | トップメニュー |
|
||||||
| `jrshikoku://positions/apps` | 走行位置 |
|
| `jrshikoku://positions/apps` | 走行位置 |
|
||||||
|
|
||||||
URLの処理は `App.tsx` の `routeFromUrl` で実装されています。
|
URLパターンの定義は `lib/startupDeepLink.ts`、遷移処理は `lib/useStartupDeepLinkNavigation.ts` の `routeFromUrl` で実装されています。
|
||||||
|
|
||||||
### Androidウィジェット
|
### Androidウィジェット
|
||||||
|
|
||||||
@@ -204,15 +268,32 @@ URLの処理は `App.tsx` の `routeFromUrl` で実装されています。
|
|||||||
| `JR_shikoku_apps_shortcut` | クイックアクセス(各機能へのショートカットタイル) |
|
| `JR_shikoku_apps_shortcut` | クイックアクセス(各機能へのショートカットタイル) |
|
||||||
| `JR_shikoku_felica_balance` | ICカード残高表示 |
|
| `JR_shikoku_felica_balance` | ICカード残高表示 |
|
||||||
| `JR_shikoku_info` | 運行情報 |
|
| `JR_shikoku_info` | 運行情報 |
|
||||||
| `JR_shikoku_train_strange` | 怪レい列車 |
|
|
||||||
|
|
||||||
ウィジェットのタップ時は `jrshikoku://` スキームでアプリ内画面へ遷移します。
|
ウィジェットのタップ時は `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)
|
### iOSウィジェット(WidgetKit)
|
||||||
|
|
||||||
WidgetKit拡張としてホーム画面ウィジェットを提供しています。Live Activities にも対応しています。
|
WidgetKit拡張として、次のホーム画面ウィジェットを提供しています。
|
||||||
設定: `targets/widget/Info.plist`
|
|
||||||
|
- ショートカット
|
||||||
|
- ICカード残高
|
||||||
|
- 遅延速報
|
||||||
|
- 運行情報
|
||||||
|
|
||||||
|
Live Activitiesは、駅固定(StationLock)と列車追従(TrainFollow)に対応しています。
|
||||||
|
実装: `targets/widget/`
|
||||||
|
|
||||||
### プッシュ通知
|
### プッシュ通知
|
||||||
|
|
||||||
@@ -228,30 +309,40 @@ WidgetKit拡張としてホーム画面ウィジェットを提供していま
|
|||||||
|
|
||||||
### NFC(FeliCa)
|
### NFC(FeliCa)
|
||||||
|
|
||||||
NFC-Fを利用した交通系ICカードの読み取りに対応しています(Android)。アプリ内からスキャンを開始する形式です。
|
NFC-Fを利用した交通系ICカードの読み取りに、iOS・Androidとも対応しています。アプリ内からスキャンを開始する形式です。
|
||||||
実装: `modules/expo-felica-reader/`
|
実装: `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
|
```bash
|
||||||
# APKビルド(Android)
|
eas build --platform android --profile <profile>
|
||||||
eas build --platform android
|
eas build --platform ios --profile <profile>
|
||||||
|
|
||||||
# IPAビルド(iOS)
|
|
||||||
eas build --platform ios
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🤝 貢献
|
runtimeVersionの方針は [docs/architecture/adr/001-runtime-version-policy.md](./docs/architecture/adr/001-runtime-version-policy.md) を参照してください。
|
||||||
|
ネイティブ設定(`app.json` のプラグイン、permission、entitlement、ローカルモジュールなど)を変更した場合は、OTAではなく新しいビルドが必要です。
|
||||||
プルリクエストを歓迎します。大きな変更を行う場合は、まずissueを開いて変更内容を議論してください。
|
|
||||||
|
|
||||||
## 📄 ライセンス
|
## 📄 ライセンス
|
||||||
|
|
||||||
[ライセンス情報をここに記載]
|
本リポジトリは非公開です。無断での複製・再配布を禁止します(All Rights Reserved)。
|
||||||
|
|
||||||
|
- 同梱しているフォント・音声・ロゴなどの素材は、それぞれの権利者の規約に従います。
|
||||||
|
- 依存ライブラリのライセンスは、各パッケージの記載に従います。一覧はアプリの「設定 > オープンソースライセンス」で確認できます。
|
||||||
|
- `modules/expo-felica-reader/` は、Daiki Urata氏の [expo-felica-reader](https://github.com/7nohe/expo-felica-reader) 0.1.0(MIT License)をもとに改変したものです。ライセンス表記は `modules/expo-felica-reader/LICENSE` を参照してください。
|
||||||
|
|
||||||
|
本アプリはJR四国とは無関係の非公式アプリです。「JR四国」などの名称・商標は各権利者に帰属します。
|
||||||
|
|
||||||
## 👤 開発者
|
## 👤 開発者
|
||||||
|
|
||||||
[開発者情報をここに記載]
|
harukin/Xprocess
|
||||||
|
|
||||||
## 🔗 関連リンク
|
## 🔗 関連リンク
|
||||||
|
|
||||||
|
|||||||
@@ -5,33 +5,56 @@ import type { BottomTabNavigationProp } from "@react-navigation/bottom-tabs";
|
|||||||
import { useColorScheme } from "react-native";
|
import { useColorScheme } from "react-native";
|
||||||
import Apps from "./components/Apps";
|
import Apps from "./components/Apps";
|
||||||
import TrainBase from "./components/trainbaseview";
|
import TrainBase from "./components/trainbaseview";
|
||||||
import HowTo from "./howto";
|
|
||||||
import News from "./components/news";
|
import News from "./components/news";
|
||||||
import TrainMenu from "./components/trainMenu";
|
import TrainMenu from "./components/trainMenu";
|
||||||
import { FavoriteList } from "./components/FavoriteList";
|
import { FavoriteList } from "./components/FavoriteList";
|
||||||
import { optionData } from "./lib/stackOption";
|
import { optionData } from "./lib/stackOption";
|
||||||
import { useCurrentTrainPolling, useCurrentTrainView } from "./stateBox/useCurrentTrain";
|
import { useCurrentTrainPolling, useCurrentTrainView } from "./stateBox/useCurrentTrain";
|
||||||
|
import {
|
||||||
|
useCustomTrainData,
|
||||||
|
useOperationLogs,
|
||||||
|
} from "./stateBox/useAllTrainDiagram";
|
||||||
import { useTrainMenu } from "./stateBox/useTrainMenu";
|
import { useTrainMenu } from "./stateBox/useTrainMenu";
|
||||||
import { AS } from "./storageControl";
|
import { AS } from "./storageControl";
|
||||||
import { news } from "./config/newsUpdate";
|
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 GeneralWebView from "./GeneralWebView";
|
||||||
import { StationDiagramView } from "@/components/StationDiagram/StationDiagramView";
|
import { StationDiagramView } from "@/components/StationDiagram/StationDiagramView";
|
||||||
import { positionsLifecycleRef, positionsStackNavRef } from "./lib/rootNavigation";
|
import { positionsLifecycleRef, positionsStackNavRef } from "./lib/rootNavigation";
|
||||||
import { telemetry } from "./lib/observability/telemetry";
|
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";
|
import type { PositionsStackParamList, RootTabParamList } from "@/types/navigation";
|
||||||
const Stack = createStackNavigator<PositionsStackParamList>();
|
const Stack = createStackNavigator<PositionsStackParamList>();
|
||||||
const positionsStackTelemetry = telemetry.scope("positions.stack");
|
const positionsStackTelemetry = telemetry.scope("positions.stack");
|
||||||
export const Top = () => {
|
export const Top = () => {
|
||||||
|
markStartupOnce("positions-stack-render", "positions Top render");
|
||||||
const { webview } = useCurrentTrainView();
|
const { webview } = useCurrentTrainView();
|
||||||
const navigation = useNavigation<BottomTabNavigationProp<RootTabParamList>>();
|
const navigation = useNavigation<BottomTabNavigationProp<RootTabParamList>>();
|
||||||
const { navigate, addListener } = navigation;
|
const { navigate, addListener } = navigation;
|
||||||
const isTabFocused = useIsFocused();
|
const isTabFocused = useIsFocused();
|
||||||
|
const { notifyStackReady } = usePositionsStartupReady();
|
||||||
useCurrentTrainPolling(isTabFocused);
|
useCurrentTrainPolling(isTabFocused);
|
||||||
|
const { activateCustomTrainData } = useCustomTrainData();
|
||||||
|
const { activateOperationLogs } = useOperationLogs();
|
||||||
const isDark = useColorScheme() === "dark";
|
const isDark = useColorScheme() === "dark";
|
||||||
const bgColor = isDark ? "#1c1c1e" : "#ffffff";
|
const bgColor = isDark ? "#1c1c1e" : "#ffffff";
|
||||||
const [hasActivatedStack, setHasActivatedStack] = useState(false);
|
const [hasActivatedStack, setHasActivatedStack] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isTabFocused) return;
|
||||||
|
activateCustomTrainData();
|
||||||
|
activateOperationLogs();
|
||||||
|
}, [
|
||||||
|
activateCustomTrainData,
|
||||||
|
activateOperationLogs,
|
||||||
|
isTabFocused,
|
||||||
|
]);
|
||||||
|
|
||||||
//地図用
|
//地図用
|
||||||
const { mapSwitch } = useTrainMenu();
|
const { mapSwitch } = useTrainMenu();
|
||||||
const mapSwitchRef = useRef(mapSwitch);
|
const mapSwitchRef = useRef(mapSwitch);
|
||||||
@@ -54,6 +77,7 @@ export const Top = () => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
markStartupOnce("positions-stack-mounted", "positions stack mounted");
|
||||||
positionsStackTelemetry.info("positions stack mounted");
|
positionsStackTelemetry.info("positions stack mounted");
|
||||||
return () => {
|
return () => {
|
||||||
positionsStackTelemetry.info("positions stack unmounted");
|
positionsStackTelemetry.info("positions stack unmounted");
|
||||||
@@ -62,6 +86,9 @@ export const Top = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (hasActivatedStack) return;
|
if (hasActivatedStack) return;
|
||||||
|
markStartup("positions stack activation scheduled", {
|
||||||
|
focused: isTabFocused,
|
||||||
|
});
|
||||||
positionsStackTelemetry.breadcrumb({
|
positionsStackTelemetry.breadcrumb({
|
||||||
message: "positions stack activation scheduled",
|
message: "positions stack activation scheduled",
|
||||||
data: {
|
data: {
|
||||||
@@ -71,6 +98,10 @@ export const Top = () => {
|
|||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
const activate = () => {
|
const activate = () => {
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
|
markStartupOnce(
|
||||||
|
"positions-stack-activated",
|
||||||
|
"positions stack activation committed",
|
||||||
|
);
|
||||||
positionsStackTelemetry.breadcrumb({
|
positionsStackTelemetry.breadcrumb({
|
||||||
message: "positions stack activation committed",
|
message: "positions stack activation committed",
|
||||||
data: {
|
data: {
|
||||||
@@ -78,6 +109,7 @@ export const Top = () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
setHasActivatedStack(true);
|
setHasActivatedStack(true);
|
||||||
|
notifyStackReady();
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isTabFocused) {
|
if (isTabFocused) {
|
||||||
@@ -95,9 +127,13 @@ export const Top = () => {
|
|||||||
cancelled = true;
|
cancelled = true;
|
||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
};
|
};
|
||||||
}, [hasActivatedStack, isTabFocused]);
|
}, [hasActivatedStack, isTabFocused, notifyStackReady]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
markStartupState("positions-stack-gate", "positions stack gate state", {
|
||||||
|
focused: isTabFocused,
|
||||||
|
activated: hasActivatedStack,
|
||||||
|
});
|
||||||
telemetry.context("positions_stack", {
|
telemetry.context("positions_stack", {
|
||||||
focused: isTabFocused,
|
focused: isTabFocused,
|
||||||
activated: hasActivatedStack,
|
activated: hasActivatedStack,
|
||||||
@@ -131,11 +167,6 @@ export const Top = () => {
|
|||||||
stackIndex: stackNav?.getState()?.index ?? null,
|
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) {
|
if (!hasActivatedStack || !stackNav) {
|
||||||
e?.preventDefault?.();
|
e?.preventDefault?.();
|
||||||
positionsStackTelemetry.breadcrumb({
|
positionsStackTelemetry.breadcrumb({
|
||||||
@@ -239,7 +270,6 @@ export const Top = () => {
|
|||||||
options={{ ...optionData, gestureEnabled: false }}
|
options={{ ...optionData, gestureEnabled: false }}
|
||||||
component={StationDiagramView}
|
component={StationDiagramView}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen name="howto" options={optionData} component={HowTo} />
|
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="generalWebView"
|
name="generalWebView"
|
||||||
options={optionData}
|
options={optionData}
|
||||||
|
|||||||
@@ -5,8 +5,7 @@
|
|||||||
"scheme": "jrshikoku",
|
"scheme": "jrshikoku",
|
||||||
"platforms": [
|
"platforms": [
|
||||||
"ios",
|
"ios",
|
||||||
"android",
|
"android"
|
||||||
"web"
|
|
||||||
],
|
],
|
||||||
"version": "7.2",
|
"version": "7.2",
|
||||||
"userInterfaceStyle": "automatic",
|
"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 actionSheetRef = useRef(null);
|
||||||
const maxHeight = useSheetMaxHeight();
|
const maxHeight = useSheetMaxHeight();
|
||||||
const [sheetOpened, setSheetOpened] = useState(false);
|
const [sheetOpened, setSheetOpened] = useState(false);
|
||||||
|
|
||||||
useCurrentTrainPolling(sheetOpened);
|
useCurrentTrainPolling(sheetOpened);
|
||||||
|
|
||||||
const handleOpen = () => {
|
const handleOpen = () => {
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ export const DataConnectedButton: FC<{
|
|||||||
|
|
||||||
openTrainInfo(time);
|
openTrainInfo(time);
|
||||||
}}
|
}}
|
||||||
key={station + time}
|
|
||||||
>
|
>
|
||||||
<View style={{ flexDirection: "row", backgroundColor: colors.backgroundTertiary }}>
|
<View style={{ flexDirection: "row", backgroundColor: colors.backgroundTertiary }}>
|
||||||
<View
|
<View
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import React, { FC } from "react";
|
import React, { FC } from "react";
|
||||||
import { View, Text, TouchableWithoutFeedback } from "react-native";
|
import { Platform, View, Text, TouchableWithoutFeedback } from "react-native";
|
||||||
import dayjs from "dayjs";
|
import type { Dayjs } from "dayjs";
|
||||||
import { setClockTime } from "@/lib/timeUtils";
|
import { setClockTime } from "@/lib/timeUtils";
|
||||||
import lineColorList from "../../../assets/originData/lineColorList";
|
import lineColorList from "@/assets/originData/lineColorList";
|
||||||
import { trainDataType } from "@/lib/trainPositionTextArray";
|
import type { trainDataType } from "@/lib/trainPositionTextArray";
|
||||||
import { getStopListColors } from "./colorScheme";
|
import { getStopListColors } from "./colorScheme";
|
||||||
import {
|
import {
|
||||||
isCanceledSe,
|
isCanceledSe,
|
||||||
@@ -12,90 +12,89 @@ import {
|
|||||||
parseSeString,
|
parseSeString,
|
||||||
} from "@/utils/seUtils";
|
} from "@/utils/seUtils";
|
||||||
import type { SeTypes } from "@/types";
|
import type { SeTypes } from "@/types";
|
||||||
import { useThemeColors } from "@/lib/theme";
|
|
||||||
import { useResponsive } from "@/lib/responsive";
|
|
||||||
|
|
||||||
type seTypes =
|
type TrainDelay = trainDataType["delay"];
|
||||||
| "発編"
|
|
||||||
| "着編"
|
|
||||||
| "通編"
|
|
||||||
| "頃編"
|
|
||||||
| "発"
|
|
||||||
| "着"
|
|
||||||
| "休編"
|
|
||||||
| "休発"
|
|
||||||
| "休着"
|
|
||||||
| "休発編"
|
|
||||||
| "休着編"
|
|
||||||
| "通休編"
|
|
||||||
| "通発休編"
|
|
||||||
| "通着休編"
|
|
||||||
| string;
|
|
||||||
|
|
||||||
type currentTrainDataType = {
|
export type EachStopData = readonly [
|
||||||
Index: number;
|
station: string,
|
||||||
num: string;
|
se: SeTypes,
|
||||||
delay: "入線" | number | undefined;
|
time: string,
|
||||||
Pos: string;
|
platformNum?: string,
|
||||||
PosNum: string | number;
|
];
|
||||||
Direction: number;
|
|
||||||
Type: string;
|
type ResponsiveScale = {
|
||||||
Line: string;
|
fontScale: (size: number) => number;
|
||||||
|
moderateScale: (size: number, factor?: number) => number;
|
||||||
};
|
};
|
||||||
type StationInfo = { StationName: string; StationNumber: string | null };
|
|
||||||
|
|
||||||
type props = {
|
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;
|
index: number;
|
||||||
stationList: StationInfo[][];
|
|
||||||
points: boolean;
|
points: boolean;
|
||||||
currentTrainData?: currentTrainDataType;
|
|
||||||
openStationACFromEachTrainInfo?: (station: string) => void;
|
openStationACFromEachTrainInfo?: (station: string) => void;
|
||||||
showThrew: boolean;
|
showThrew: boolean;
|
||||||
array: string[];
|
|
||||||
isNotService?: boolean;
|
isNotService?: boolean;
|
||||||
};
|
};
|
||||||
export const EachStopList: FC<props> = ({
|
const EachStopListComponent: FC<props> = ({
|
||||||
i,
|
stationNumberMap,
|
||||||
|
parsedTrainData,
|
||||||
|
clockBase,
|
||||||
|
borderColor,
|
||||||
|
textOnPrimary,
|
||||||
|
isDark,
|
||||||
|
trainDelay,
|
||||||
|
fontScale,
|
||||||
|
moderateScale,
|
||||||
index,
|
index,
|
||||||
stationList,
|
|
||||||
points,
|
points,
|
||||||
currentTrainData,
|
|
||||||
openStationACFromEachTrainInfo,
|
openStationACFromEachTrainInfo,
|
||||||
showThrew,
|
showThrew,
|
||||||
array,
|
|
||||||
isNotService = false,
|
isNotService = false,
|
||||||
}) => {
|
}) => {
|
||||||
const { colors: themeColors, isDark } = useThemeColors();
|
// console.log({
|
||||||
const { fontScale, moderateScale } = useResponsive();
|
// trainNum,
|
||||||
const [station, se, time, platformNum] = i.split(",") as [
|
// index,
|
||||||
string,
|
// points,
|
||||||
seTypes,
|
// //openStationACFromEachTrainInfo,
|
||||||
string,
|
// showThrew,
|
||||||
string
|
// //array,
|
||||||
]; // 阿波池田,発,6:21,1
|
// isNotService,
|
||||||
let beforeSameStationData = null;
|
// });
|
||||||
|
|
||||||
|
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 (se.includes("発")) {
|
||||||
if (index > 0) {
|
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;
|
beforeSameStationData = beforeData;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let afterSameStationData = null;
|
let afterSameStationData: EachStopData | null = null;
|
||||||
// 着(通常着・休着・休着編)の場合、次の発(通常発・休発・休発編)と統合される(非表示)
|
// 着(通常着・休着・休着編)の場合、次の発(通常発・休発・休発編)と統合される(非表示)
|
||||||
if (se.includes("着")) {
|
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("発")) {
|
if (afterData && afterData[0] == station && afterData[1].includes("発")) {
|
||||||
afterSameStationData = afterData;
|
afterSameStationData = afterData;
|
||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if(station.includes(".")){
|
if (station.includes(".")) {
|
||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
if (!showThrew) {
|
if (!showThrew) {
|
||||||
@@ -107,25 +106,11 @@ export const EachStopList: FC<props> = ({
|
|||||||
if (se == "通発休編") return null;
|
if (se == "通発休編") return null;
|
||||||
if (se == "通着休編") return null;
|
if (se == "通着休編") return null;
|
||||||
}
|
}
|
||||||
const Stations = stationList
|
const StationNumbers = stationNumberMap.get(station) ?? [];
|
||||||
.map((a) => a.filter((d) => d.StationName == station))
|
|
||||||
.reduce((newArray, e) => newArray.concat(e), []);
|
|
||||||
/*Array [
|
|
||||||
Object {
|
|
||||||
"Station_JP": "佐古",
|
|
||||||
"StationNumber": "T01",
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"Station_JP": "佐古",
|
|
||||||
"StationNumber": "B01",
|
|
||||||
},
|
|
||||||
] */
|
|
||||||
const StationNumbers = Stations.filter((d) => d.StationNumber != null).map(
|
|
||||||
(d) => d.StationNumber as string
|
|
||||||
);
|
|
||||||
const trimmedPlatformNum = platformNum?.trim();
|
const trimmedPlatformNum = platformNum?.trim();
|
||||||
const hasPlatformNum = !!trimmedPlatformNum;
|
const hasPlatformNum = !!trimmedPlatformNum;
|
||||||
const isPlatformNumNumeric = hasPlatformNum && /^\d+$/.test(trimmedPlatformNum);
|
const isPlatformNumNumeric =
|
||||||
|
hasPlatformNum && /^\d+$/.test(trimmedPlatformNum);
|
||||||
const platformDisplay = hasPlatformNum
|
const platformDisplay = hasPlatformNum
|
||||||
? isPlatformNumNumeric
|
? isPlatformNumNumeric
|
||||||
? parseInt(trimmedPlatformNum, 10) === 0
|
? parseInt(trimmedPlatformNum, 10) === 0
|
||||||
@@ -141,9 +126,7 @@ export const EachStopList: FC<props> = ({
|
|||||||
const isCanceled = isCanceledSe(se);
|
const isCanceled = isCanceledSe(se);
|
||||||
const isCommunity = isCommunitySe(se);
|
const isCommunity = isCommunitySe(se);
|
||||||
const isDelayed =
|
const isDelayed =
|
||||||
currentTrainData?.delay !== undefined &&
|
trainDelay !== undefined && trainDelay !== "入線" && trainDelay > 0;
|
||||||
currentTrainData?.delay !== "入線" &&
|
|
||||||
currentTrainData?.delay > 0;
|
|
||||||
|
|
||||||
const colors = getStopListColors(
|
const colors = getStopListColors(
|
||||||
isThrough,
|
isThrough,
|
||||||
@@ -151,7 +134,7 @@ export const EachStopList: FC<props> = ({
|
|||||||
isCanceled,
|
isCanceled,
|
||||||
isDelayed,
|
isDelayed,
|
||||||
isNotService,
|
isNotService,
|
||||||
isDark
|
isDark,
|
||||||
);
|
);
|
||||||
// 打ち消し線用の通常色(遅延していない時の色)
|
// 打ち消し線用の通常色(遅延していない時の色)
|
||||||
const normalColors = getStopListColors(
|
const normalColors = getStopListColors(
|
||||||
@@ -160,7 +143,7 @@ export const EachStopList: FC<props> = ({
|
|||||||
isCanceled,
|
isCanceled,
|
||||||
false,
|
false,
|
||||||
isNotService,
|
isNotService,
|
||||||
isDark
|
isDark,
|
||||||
);
|
);
|
||||||
|
|
||||||
// beforeSameStationData用の色設定
|
// beforeSameStationData用の色設定
|
||||||
@@ -179,7 +162,7 @@ export const EachStopList: FC<props> = ({
|
|||||||
isCanceled,
|
isCanceled,
|
||||||
isDelayed,
|
isDelayed,
|
||||||
isNotService,
|
isNotService,
|
||||||
isDark
|
isDark,
|
||||||
);
|
);
|
||||||
const beforeNormalColors = getStopListColors(
|
const beforeNormalColors = getStopListColors(
|
||||||
beforeIsThrough,
|
beforeIsThrough,
|
||||||
@@ -187,19 +170,47 @@ export const EachStopList: FC<props> = ({
|
|||||||
isCanceled,
|
isCanceled,
|
||||||
false,
|
false,
|
||||||
isNotService,
|
isNotService,
|
||||||
isDark
|
isDark,
|
||||||
);
|
);
|
||||||
beforeTimeTextColor = beforeColors.timeText;
|
beforeTimeTextColor = beforeColors.timeText;
|
||||||
beforeNormalTimeTextColor = beforeNormalColors.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 (
|
return (
|
||||||
<TouchableWithoutFeedback
|
<TouchableWithoutFeedback
|
||||||
onPress={() =>
|
onPress={() =>
|
||||||
openStationACFromEachTrainInfo &&
|
openStationACFromEachTrainInfo &&
|
||||||
openStationACFromEachTrainInfo(station)
|
openStationACFromEachTrainInfo(station)
|
||||||
}
|
}
|
||||||
key={station + se + time}
|
|
||||||
>
|
>
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
@@ -214,50 +225,56 @@ export const EachStopList: FC<props> = ({
|
|||||||
position: "relative",
|
position: "relative",
|
||||||
marginHorizontal: 15,
|
marginHorizontal: 15,
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
height: "101%",
|
alignSelf: "stretch",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{StationNumbers.map((stn, index) => (
|
{StationNumbers.map((stn) => (
|
||||||
<StationNumbersBox stn={stn} se={se} key={index} />
|
<StationNumbersBox
|
||||||
))}
|
key={stn}
|
||||||
|
stn={stn}
|
||||||
|
se={se}
|
||||||
|
fontScale={fontScale}
|
||||||
|
textColor={textOnPrimary}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View
|
{points && (
|
||||||
style={{
|
<View
|
||||||
position: "relative",
|
style={{
|
||||||
flexDirection: "column",
|
position: "relative",
|
||||||
width: 0,
|
width: 0,
|
||||||
height: "100%",
|
alignSelf: "stretch",
|
||||||
alignItems: "flex-end",
|
alignItems: "flex-end",
|
||||||
}}
|
justifyContent: "center",
|
||||||
>
|
}}
|
||||||
<View style={{ flex: 1 }} />
|
>
|
||||||
<View style={{ position: "relative", height: 25 }}>
|
<View style={{ position: "relative", height: 25 }}>
|
||||||
<Text
|
<Text
|
||||||
style={{
|
style={{
|
||||||
fontSize: fontScale(20),
|
fontSize: fontScale(20),
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
textAlignVertical: "center",
|
textAlignVertical: "center",
|
||||||
left: -15,
|
left: -15,
|
||||||
top: "auto",
|
top: "auto",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{points && "🚊"}
|
🚊
|
||||||
</Text>
|
</Text>
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<View style={{ flex: 1 }} />
|
)}
|
||||||
</View>
|
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
padding: 8,
|
padding: 8,
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
borderBottomWidth: 1,
|
borderBottomWidth: 1,
|
||||||
borderBottomColor: themeColors.borderLight,
|
borderBottomColor: borderColor,
|
||||||
flex: 1,
|
flex: 1,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<View style={{ flexDirection: "column" }}>
|
|
||||||
<View style={{ flex: 1 }} />
|
|
||||||
<Text
|
<Text
|
||||||
style={{
|
style={{
|
||||||
fontSize: fontScale(20),
|
fontSize: fontScale(20),
|
||||||
@@ -278,64 +295,55 @@ export const EachStopList: FC<props> = ({
|
|||||||
{platformDisplay}
|
{platformDisplay}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</Text>
|
</Text>
|
||||||
<View style={{ flex: 1 }} />
|
{hasDoubleTime ? (
|
||||||
</View>
|
<View style={{ flexDirection: "column", alignItems: "flex-end" }}>
|
||||||
<View style={{ flex: 1 }} />
|
{beforeTimeText}
|
||||||
<View style={{ flexDirection: "column", alignItems: "flex-end" }}>
|
{mainTimeText}
|
||||||
{beforeSameStationData && (
|
</View>
|
||||||
<TimeText
|
) : (
|
||||||
isDouble={!!beforeSameStationData || !!afterSameStationData}
|
mainTimeText
|
||||||
isBefore={true}
|
)}
|
||||||
currentTrainData={currentTrainData}
|
|
||||||
se={beforeSameStationData[1]}
|
|
||||||
time={beforeSameStationData[2]}
|
|
||||||
key={"before" + beforeSameStationData[2]}
|
|
||||||
textColor={beforeTimeTextColor}
|
|
||||||
normalTextColor={beforeNormalTimeTextColor}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<TimeText
|
|
||||||
isDouble={!!beforeSameStationData || !!afterSameStationData}
|
|
||||||
currentTrainData={currentTrainData}
|
|
||||||
se={se}
|
|
||||||
time={time}
|
|
||||||
key={se + time}
|
|
||||||
textColor={colors.timeText}
|
|
||||||
normalTextColor={normalColors.timeText}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</TouchableWithoutFeedback>
|
</TouchableWithoutFeedback>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const TimeText: FC<{
|
export const EachStopList = React.memo(EachStopListComponent);
|
||||||
|
|
||||||
|
const TimeTextComponent: FC<{
|
||||||
isBefore?: boolean;
|
isBefore?: boolean;
|
||||||
isDouble: boolean;
|
isDouble: boolean;
|
||||||
currentTrainData: trainDataType;
|
delay: TrainDelay;
|
||||||
se: string;
|
se: string;
|
||||||
time: string;
|
time: string;
|
||||||
textColor: string;
|
textColor: string;
|
||||||
normalTextColor: string;
|
normalTextColor: string;
|
||||||
}> = ({
|
clockBase: Dayjs;
|
||||||
|
} & ResponsiveScale> = ({
|
||||||
isDouble,
|
isDouble,
|
||||||
currentTrainData,
|
delay,
|
||||||
se,
|
se,
|
||||||
time,
|
time,
|
||||||
isBefore = false,
|
isBefore = false,
|
||||||
textColor,
|
textColor,
|
||||||
normalTextColor,
|
normalTextColor,
|
||||||
|
clockBase,
|
||||||
|
fontScale,
|
||||||
|
moderateScale,
|
||||||
}) => {
|
}) => {
|
||||||
const [seString, seType] = parseSeString(se);
|
const [seString, seType] = parseSeString(se);
|
||||||
const { fontScale, moderateScale } = useResponsive();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={{ flexDirection: "row", alignItems: "center", opacity: isBefore ? 0.5 : 1 }}>
|
<View
|
||||||
{!!currentTrainData?.delay &&
|
style={{
|
||||||
currentTrainData?.delay != "入線" &&
|
flexDirection: "row",
|
||||||
currentTrainData?.delay != 0 && (
|
alignItems: "center",
|
||||||
|
opacity: isBefore ? 0.5 : 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{time !== "" && !!delay && delay != "入線" && delay != 0 && (
|
||||||
<Text
|
<Text
|
||||||
style={{
|
style={{
|
||||||
fontSize: fontScale(isBefore ? 14 : 15),
|
fontSize: fontScale(isBefore ? 14 : 15),
|
||||||
@@ -354,12 +362,15 @@ const TimeText: FC<{
|
|||||||
)}
|
)}
|
||||||
<StationTimeBox
|
<StationTimeBox
|
||||||
isBefore={isBefore}
|
isBefore={isBefore}
|
||||||
delay={currentTrainData?.delay}
|
delay={delay}
|
||||||
textColor={textColor}
|
textColor={textColor}
|
||||||
seType={seType}
|
seType={seType}
|
||||||
se={se}
|
se={se}
|
||||||
time={time}
|
time={time}
|
||||||
isDouble={isDouble}
|
isDouble={isDouble}
|
||||||
|
clockBase={clockBase}
|
||||||
|
fontScale={fontScale}
|
||||||
|
moderateScale={moderateScale}
|
||||||
/>
|
/>
|
||||||
<Text
|
<Text
|
||||||
style={{
|
style={{
|
||||||
@@ -375,10 +386,15 @@ const TimeText: FC<{
|
|||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
const StationNumbersBox: FC<{ stn: string; se: seTypes }> = (props) => {
|
const TimeText = React.memo(TimeTextComponent);
|
||||||
const { stn, se } = props;
|
|
||||||
const { fixed } = useThemeColors();
|
const StationNumbersBoxComponent: FC<{
|
||||||
const { fontScale } = useResponsive();
|
stn: string;
|
||||||
|
se: SeTypes;
|
||||||
|
textColor: string;
|
||||||
|
fontScale: ResponsiveScale["fontScale"];
|
||||||
|
}> = (props) => {
|
||||||
|
const { stn, se, textColor, fontScale } = props;
|
||||||
const lineColor = lineColorList[stn.charAt(0)];
|
const lineColor = lineColorList[stn.charAt(0)];
|
||||||
const hasThrew =
|
const hasThrew =
|
||||||
se == "通過" ||
|
se == "通過" ||
|
||||||
@@ -391,44 +407,74 @@ const StationNumbersBox: FC<{ stn: string; se: seTypes }> = (props) => {
|
|||||||
? "80"
|
? "80"
|
||||||
: "";
|
: "";
|
||||||
const backgroundColor = `${lineColor}${hasThrew}`;
|
const backgroundColor = `${lineColor}${hasThrew}`;
|
||||||
return (
|
|
||||||
<View style={{ backgroundColor, flex: 1 }} key={stn}>
|
const stationNumberLabel = `${stn.charAt(0)}\n${stn.slice(1)}`;
|
||||||
<View style={{ flex: 1 }} />
|
const stationNumberTextStyle = {
|
||||||
<Text
|
color: textColor,
|
||||||
|
textAlign: "center" as const,
|
||||||
|
fontSize: fontScale(10),
|
||||||
|
fontWeight: "bold" as const,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (Platform.OS === "ios") {
|
||||||
|
return (
|
||||||
|
<View
|
||||||
style={{
|
style={{
|
||||||
color: fixed.textOnPrimary,
|
backgroundColor,
|
||||||
textAlign: "center",
|
flex: 1,
|
||||||
fontSize: fontScale(10),
|
justifyContent: "center",
|
||||||
fontWeight: "bold",
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{stn.charAt(0)}
|
<Text style={stationNumberTextStyle}>{stationNumberLabel}</Text>
|
||||||
{"\n"}
|
</View>
|
||||||
{stn.slice(1)}
|
);
|
||||||
</Text>
|
}
|
||||||
<View style={{ flex: 1 }} />
|
|
||||||
</View>
|
return (
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
...stationNumberTextStyle,
|
||||||
|
backgroundColor,
|
||||||
|
flex: 1,
|
||||||
|
textAlignVertical: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{stationNumberLabel}
|
||||||
|
</Text>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
type StationTimeBoxType = {
|
type StationTimeBoxType = {
|
||||||
isBefore?: boolean;
|
isBefore?: boolean;
|
||||||
delay: "入線" | number | undefined;
|
delay: TrainDelay;
|
||||||
textColor: string;
|
textColor: string;
|
||||||
seType: seTypes;
|
seType: SeTypes;
|
||||||
se: string;
|
se: string;
|
||||||
time: string;
|
time: string;
|
||||||
isDouble: boolean;
|
isDouble: boolean;
|
||||||
};
|
clockBase: Dayjs;
|
||||||
|
} & ResponsiveScale;
|
||||||
|
|
||||||
const StationTimeBox: FC<StationTimeBoxType> = (props) => {
|
const StationNumbersBox = React.memo(StationNumbersBoxComponent);
|
||||||
const { delay, textColor, seType, se, time, isDouble, isBefore } = props;
|
|
||||||
const { fontScale, moderateScale } = useResponsive();
|
const StationTimeBoxComponent: FC<StationTimeBoxType> = (props) => {
|
||||||
const dates = setClockTime(
|
const {
|
||||||
dayjs(),
|
delay,
|
||||||
|
textColor,
|
||||||
|
seType,
|
||||||
|
se,
|
||||||
time,
|
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 (
|
return (
|
||||||
<Text
|
<Text
|
||||||
@@ -440,7 +486,9 @@ const StationTimeBox: FC<StationTimeBoxType> = (props) => {
|
|||||||
fontStyle: seType == "community" ? "italic" : "normal",
|
fontStyle: seType == "community" ? "italic" : "normal",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{se.includes("通") && time == "" ? "レ" : dates?.format("HH:mm") ?? ""}
|
{se.includes("通") && time == "" ? "レ" : (timeData ?? "")}
|
||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const StationTimeBox = React.memo(StationTimeBoxComponent);
|
||||||
|
|||||||
@@ -1,23 +1,14 @@
|
|||||||
import React, { FC } from "react";
|
import React, { FC } from "react";
|
||||||
import { View } from "react-native";
|
import { View } from "react-native";
|
||||||
import { TrainDataView } from "./TrainDataView";
|
import { TrainDataView } from "./TrainDataView";
|
||||||
import { trainDataType } from "@/lib/trainPositionTextArray";
|
import type { TrainDataViewHeaderProps } from "./TrainDataView";
|
||||||
import type { NavigateFunction } from "@/types";
|
type props = TrainDataViewHeaderProps;
|
||||||
type props = {
|
export const LongHeader: FC<props> = ({
|
||||||
currentTrainData: trainDataType;
|
|
||||||
currentTrainDataList?: trainDataType[];
|
|
||||||
currentPosition: string[] | undefined;
|
|
||||||
currentPositionList?: string[][];
|
|
||||||
nearTrainIDList: string[];
|
|
||||||
openTrainInfo: (f: string) => void;
|
|
||||||
navigate: NavigateFunction;
|
|
||||||
}
|
|
||||||
export const LongHeader:FC<props> = ({
|
|
||||||
currentTrainData,
|
currentTrainData,
|
||||||
currentTrainDataList,
|
currentTrainDataList,
|
||||||
|
nearTrainIDList,
|
||||||
currentPosition,
|
currentPosition,
|
||||||
currentPositionList,
|
currentPositionList,
|
||||||
nearTrainIDList,
|
|
||||||
openTrainInfo,
|
openTrainInfo,
|
||||||
navigate,
|
navigate,
|
||||||
}) => {
|
}) => {
|
||||||
@@ -31,14 +22,13 @@ export const LongHeader:FC<props> = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TrainDataView
|
<TrainDataView
|
||||||
|
nearTrainIDList={nearTrainIDList}
|
||||||
currentTrainData={currentTrainData}
|
currentTrainData={currentTrainData}
|
||||||
currentTrainDataList={currentTrainDataList}
|
currentTrainDataList={currentTrainDataList}
|
||||||
currentPosition={currentPosition}
|
currentPosition={currentPosition}
|
||||||
currentPositionList={currentPositionList}
|
currentPositionList={currentPositionList}
|
||||||
nearTrainIDList={nearTrainIDList}
|
|
||||||
openTrainInfo={openTrainInfo}
|
openTrainInfo={openTrainInfo}
|
||||||
navigate={navigate}
|
navigate={navigate}
|
||||||
key={"LongHeader"}
|
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,131 +8,194 @@ type stateBox = {
|
|||||||
currentTrainData: trainDataType | undefined;
|
currentTrainData: trainDataType | undefined;
|
||||||
title: string;
|
title: string;
|
||||||
style?: ViewStyle;
|
style?: ViewStyle;
|
||||||
mode?: number;
|
mode?: 0 | 1 | 2;
|
||||||
};
|
};
|
||||||
export const PositionBox: FC<stateBox> = (props) => {
|
export const PositionBox: FC<stateBox> = (props) => {
|
||||||
const {
|
const { currentTrainData, title, style, mode } = props;
|
||||||
currentTrainData,
|
|
||||||
title,
|
|
||||||
style,
|
|
||||||
mode,
|
|
||||||
} = props;
|
|
||||||
const { colors } = useThemeColors();
|
|
||||||
const { fontScale } = useResponsive();
|
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 isStationPosition = officialPosition?.positionType === "station";
|
||||||
const platformNumber = isStationPosition
|
const platformNumber = isStationPosition
|
||||||
? officialPosition?.platform
|
? officialPosition?.platform
|
||||||
: undefined;
|
: undefined;
|
||||||
const lineNumber = isStationPosition ? officialPosition?.track : undefined;
|
const lineNumber = isStationPosition ? officialPosition?.track : undefined;
|
||||||
|
const platformDescription = officialPosition?.note || ``;
|
||||||
// Notes describe the current position even when it is a prediction or a
|
// Notes describe the current position even when it is a prediction or a
|
||||||
// section between stations. Platform/track remain station-only fields, but
|
// section between stations. Platform/track remain station-only fields, but
|
||||||
// the note itself must not be hidden by position_type.
|
// 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);
|
const { isBetween, Pos: PosData } = trainPosition(currentTrainData);
|
||||||
if (isBetween === true) {
|
if (isBetween === true) {
|
||||||
const { from, to } = PosData;
|
const { from, to } = PosData;
|
||||||
firstText = from;
|
const firstText = from;
|
||||||
secondText = to;
|
const secondText = to;
|
||||||
marginText = mode == 2 ? "→" : "↓";
|
const marginText = mode == 2 ? "→" : "↓";
|
||||||
} else {
|
|
||||||
const { Pos } = PosData;
|
return (
|
||||||
if (Pos !== "") {
|
|
||||||
firstText = Pos;
|
|
||||||
if (platformNumber) {
|
|
||||||
secondText = `${platformNumber}番乗り場`;
|
|
||||||
if (lineNumber) {
|
|
||||||
externalText = `${lineNumber}番線`;
|
|
||||||
}
|
|
||||||
} else if (lineNumber) {
|
|
||||||
secondText = `${lineNumber}番線`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<View style={{ ...(mode == 2 ? boxStyle2 : boxStyle), backgroundColor: colors.surface, ...style }}>
|
|
||||||
<Text
|
|
||||||
numberOfLines={1}
|
|
||||||
ellipsizeMode="tail"
|
|
||||||
style={{ flexShrink: 1, fontSize: fontScale(12), color: colors.textAccent }}
|
|
||||||
>
|
|
||||||
{title}
|
|
||||||
</Text>
|
|
||||||
<View style={{ flex: 1 }} />
|
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
minWidth: 0,
|
...(mode == 2 ? boxStyle2 : boxStyle),
|
||||||
flexShrink: 1,
|
backgroundColor: colors.surface,
|
||||||
flexDirection: mode == 2 ? "row" : "column",
|
...style,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{firstText && (
|
<Text
|
||||||
<Text
|
numberOfLines={1}
|
||||||
numberOfLines={1}
|
ellipsizeMode="tail"
|
||||||
ellipsizeMode="tail"
|
style={{
|
||||||
style={[
|
flexShrink: 1,
|
||||||
{ flexShrink: 1, minWidth: 0 },
|
fontSize: fontScale(12),
|
||||||
mode == 2
|
color: colors.textAccent,
|
||||||
? { fontSize: fontScale(16), textAlign: "right" as const }
|
}}
|
||||||
: isBetween
|
>
|
||||||
? { fontSize: fontScale(23), textAlign: "right" as const }
|
{title}
|
||||||
: { fontSize: fontScale(23), textAlign: "right" as const },
|
</Text>
|
||||||
{ color: colors.textAccent },
|
<View style={{ flex: 1 }} />
|
||||||
]}
|
<View
|
||||||
>
|
style={{
|
||||||
{firstText}
|
minWidth: 0,
|
||||||
</Text>
|
flexShrink: 1,
|
||||||
)}
|
flexDirection: mode == 2 ? "row" : "column",
|
||||||
{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
|
<Text
|
||||||
numberOfLines={1}
|
numberOfLines={1}
|
||||||
ellipsizeMode="tail"
|
ellipsizeMode="tail"
|
||||||
style={{
|
style={{
|
||||||
flexShrink: 1,
|
flexShrink: 1,
|
||||||
minWidth: 0,
|
minWidth: 0,
|
||||||
...(mode == 2
|
textAlign: "right",
|
||||||
? { fontSize: fontScale(16), textAlign: "right" as const }
|
fontSize: mainFontSize,
|
||||||
: { fontSize: fontScale(23), textAlign: "right" as const }),
|
|
||||||
fontSize: fontScale(10),
|
|
||||||
color: colors.textAccent,
|
color: colors.textAccent,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{" " + externalText}
|
{firstText}
|
||||||
{" " + platformDescription}
|
</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>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
{platformDescription && (
|
||||||
</View>
|
<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 = {
|
const boxStyle: ViewStyle = {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
|
|||||||
@@ -1,12 +1,27 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { View, Text, TouchableOpacity } from "react-native";
|
import { View, Text, TouchableOpacity } from "react-native";
|
||||||
|
import type { trainDataType } from "@/lib/trainPositionTextArray";
|
||||||
import { useThemeColors } from "@/lib/theme";
|
import { useThemeColors } from "@/lib/theme";
|
||||||
import { useResponsive } from "@/lib/responsive";
|
import { useResponsive } from "@/lib/responsive";
|
||||||
|
|
||||||
export const ScrollStickyContent = (props) => {
|
type Props = {
|
||||||
const { currentTrainData, showThrew, setShowThrew, haveThrough } = 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 { colors } = useThemeColors();
|
||||||
const { fontScale } = useResponsive();
|
const { fontScale } = useResponsive();
|
||||||
|
const hasNumericDelay =
|
||||||
|
typeof trainDelay === "number" && !Number.isNaN(trainDelay);
|
||||||
|
const hasDelay = hasNumericDelay && trainDelay !== 0;
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
@@ -27,7 +42,7 @@ export const ScrollStickyContent = (props) => {
|
|||||||
<Text style={{ fontSize: fontScale(20), color: colors.text }}>停車駅</Text>
|
<Text style={{ fontSize: fontScale(20), color: colors.text }}>停車駅</Text>
|
||||||
<View style={{ flex: 1 }} />
|
<View style={{ flex: 1 }} />
|
||||||
<View style={{ flexDirection: "row" }}>
|
<View style={{ flexDirection: "row" }}>
|
||||||
{!isNaN(currentTrainData?.delay) && currentTrainData?.delay != 0 && (
|
{hasDelay && (
|
||||||
<Text
|
<Text
|
||||||
style={{
|
style={{
|
||||||
fontSize: fontScale(15),
|
fontSize: fontScale(15),
|
||||||
@@ -44,11 +59,7 @@ export const ScrollStickyContent = (props) => {
|
|||||||
<Text
|
<Text
|
||||||
style={{
|
style={{
|
||||||
fontSize: fontScale(20),
|
fontSize: fontScale(20),
|
||||||
color: isNaN(currentTrainData?.delay)
|
color: hasDelay ? colors.textError : colors.text,
|
||||||
? colors.text
|
|
||||||
: currentTrainData?.delay == 0
|
|
||||||
? colors.text
|
|
||||||
: colors.textError,
|
|
||||||
width: 60,
|
width: 60,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,23 +1,14 @@
|
|||||||
import React, { FC } from "react";
|
import React, { FC } from "react";
|
||||||
import { View } from "react-native";
|
import { View } from "react-native";
|
||||||
import { TrainDataView } from "./TrainDataView";
|
import { TrainDataView } from "./TrainDataView";
|
||||||
import { trainDataType } from "@/lib/trainPositionTextArray";
|
import type { TrainDataViewHeaderProps } from "./TrainDataView";
|
||||||
import type { NavigateFunction } from "@/types";
|
type props = TrainDataViewHeaderProps;
|
||||||
type props = {
|
export const ShortHeader: FC<props> = ({
|
||||||
currentTrainData: trainDataType;
|
|
||||||
currentTrainDataList?: trainDataType[];
|
|
||||||
currentPosition: string[] | undefined;
|
|
||||||
currentPositionList?: string[][];
|
|
||||||
nearTrainIDList: string[];
|
|
||||||
openTrainInfo: (f: string) => void;
|
|
||||||
navigate: NavigateFunction;
|
|
||||||
}
|
|
||||||
export const ShortHeader:FC<props> = ({
|
|
||||||
currentTrainData,
|
currentTrainData,
|
||||||
currentTrainDataList,
|
currentTrainDataList,
|
||||||
|
nearTrainIDList,
|
||||||
currentPosition,
|
currentPosition,
|
||||||
currentPositionList,
|
currentPositionList,
|
||||||
nearTrainIDList,
|
|
||||||
openTrainInfo,
|
openTrainInfo,
|
||||||
navigate,
|
navigate,
|
||||||
}) => {
|
}) => {
|
||||||
@@ -31,15 +22,14 @@ export const ShortHeader:FC<props> = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TrainDataView
|
<TrainDataView
|
||||||
mode={2}
|
nearTrainIDList={nearTrainIDList}
|
||||||
currentTrainData={currentTrainData}
|
currentTrainData={currentTrainData}
|
||||||
currentTrainDataList={currentTrainDataList}
|
currentTrainDataList={currentTrainDataList}
|
||||||
currentPosition={currentPosition}
|
currentPosition={currentPosition}
|
||||||
currentPositionList={currentPositionList}
|
currentPositionList={currentPositionList}
|
||||||
nearTrainIDList={nearTrainIDList}
|
mode={2}
|
||||||
openTrainInfo={openTrainInfo}
|
openTrainInfo={openTrainInfo}
|
||||||
navigate={navigate}
|
navigate={navigate}
|
||||||
key={"ShortHeader"}
|
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { CSSProperties, FC } from "react";
|
import React, { FC } from "react";
|
||||||
import { View, Text, StyleProp, ViewStyle } from "react-native";
|
import { View, Text, ViewStyle } from "react-native";
|
||||||
import { useThemeColors } from "@/lib/theme";
|
import { useThemeColors } from "@/lib/theme";
|
||||||
import { useResponsive } from "@/lib/responsive";
|
import { useResponsive } from "@/lib/responsive";
|
||||||
|
|
||||||
@@ -7,7 +7,7 @@ type stateBox = {
|
|||||||
text: string;
|
text: string;
|
||||||
title: string;
|
title: string;
|
||||||
style?: ViewStyle;
|
style?: ViewStyle;
|
||||||
mode?: number;
|
mode?: 0 | 1 | 2;
|
||||||
endText?: string;
|
endText?: string;
|
||||||
};
|
};
|
||||||
export const StateBox: FC<stateBox> = (props) => {
|
export const StateBox: FC<stateBox> = (props) => {
|
||||||
|
|||||||
@@ -1,63 +1,166 @@
|
|||||||
import React, { useState, useEffect, FC } from "react";
|
import React, { useState, FC } from "react";
|
||||||
import {
|
import {
|
||||||
ScrollView,
|
|
||||||
Text,
|
Text,
|
||||||
View,
|
View,
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
ViewStyle,
|
ViewStyle,
|
||||||
useWindowDimensions,
|
useWindowDimensions,
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
|
import { ScrollView } from "react-native-actions-sheet";
|
||||||
import { StateBox } from "./StateBox";
|
import { StateBox } from "./StateBox";
|
||||||
import { PositionBox } from "./PositionBox";
|
import { PositionBox } from "./PositionBox";
|
||||||
import { useCurrentTrainView } from "../../../stateBox/useCurrentTrain";
|
import { useCurrentTrainView } from "@/stateBox/useCurrentTrain";
|
||||||
import { SheetManager } from "react-native-actions-sheet";
|
import { SheetManager } from "react-native-actions-sheet";
|
||||||
import { trainDataType } from "@/lib/trainPositionTextArray";
|
import type { trainDataType } from "@/lib/trainPositionTextArray";
|
||||||
import { useCustomTrainData } from "@/stateBox/useAllTrainDiagram";
|
|
||||||
import { customTrainDataDetector } from "@/components/custom-train-data";
|
|
||||||
import type { NavigateFunction } from "@/types";
|
import type { NavigateFunction } from "@/types";
|
||||||
import { stackAwareNavigate } from "@/lib/rootNavigation";
|
import { stackAwareNavigate } from "@/lib/rootNavigation";
|
||||||
import { OfficialPositionEditButton } from "@/components/OfficialPositionEditButton";
|
import { OfficialPositionEditButton } from "@/components/OfficialPositionEditButton";
|
||||||
import { lineDisplayName } from "@/lib/lineDisplayName";
|
import { lineDisplayName } from "@/lib/lineDisplayName";
|
||||||
|
|
||||||
type props = {
|
export type TrainDataViewData = {
|
||||||
currentTrainData: trainDataType | undefined;
|
currentTrainData: trainDataType | undefined;
|
||||||
currentTrainDataList?: trainDataType[];
|
currentTrainDataList: trainDataType[];
|
||||||
currentPosition: string[] | undefined;
|
currentPosition: string[];
|
||||||
currentPositionList?: string[][];
|
currentPositionList: string[][];
|
||||||
showLineLabel?: boolean;
|
};
|
||||||
|
|
||||||
|
export type TrainDataViewMode = 0 | 1 | 2;
|
||||||
|
|
||||||
|
export type TrainDataViewControls = {
|
||||||
nearTrainIDList: string[];
|
nearTrainIDList: string[];
|
||||||
openTrainInfo: (f: string) => void;
|
openTrainInfo: (f: string) => void;
|
||||||
mode?: 0 | 1 | 2; //0:通常,1:コンパクト,2:横並び
|
|
||||||
navigate: NavigateFunction;
|
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,
|
currentTrainData,
|
||||||
|
currentTrainDataList,
|
||||||
currentPosition,
|
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,
|
nearTrainIDList,
|
||||||
|
position,
|
||||||
|
showLineLabel,
|
||||||
openTrainInfo,
|
openTrainInfo,
|
||||||
mode = 0,
|
mode = 0,
|
||||||
navigate,
|
navigate,
|
||||||
}) => {
|
}) => {
|
||||||
const { width} = useWindowDimensions();
|
const trainNum = train?.num ?? "";
|
||||||
|
const currentTrainData = train;
|
||||||
|
const currentPosition = position;
|
||||||
|
const { width } = useWindowDimensions();
|
||||||
const { setInjectData } = useCurrentTrainView();
|
const { setInjectData } = useCurrentTrainView();
|
||||||
|
|
||||||
const { allCustomTrainData } = useCustomTrainData();
|
|
||||||
const onLine = !!currentPosition?.toString().length;
|
const onLine = !!currentPosition?.toString().length;
|
||||||
const lineLabel = lineDisplayName(currentTrainData?.Line);
|
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 = {
|
const columnStyle = {
|
||||||
width: width / 2,
|
width: width / 2,
|
||||||
display: "flex",
|
display: "flex",
|
||||||
@@ -91,7 +194,7 @@ const TrainDataViewRow: FC<props> = ({
|
|||||||
display: "flex",
|
display: "flex",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<View style={{ flexDirection: "row",flex:1,...columnStyle }}>
|
<View style={{ flexDirection: "row", flex: 1, ...columnStyle }}>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={{ flex: 1, minWidth: 0, flexDirection: "row" }}
|
style={{ flex: 1, minWidth: 0, flexDirection: "row" }}
|
||||||
//disabled={!onLine}
|
//disabled={!onLine}
|
||||||
@@ -99,7 +202,7 @@ const TrainDataViewRow: FC<props> = ({
|
|||||||
if (!currentTrainData) return;
|
if (!currentTrainData) return;
|
||||||
setInjectData({
|
setInjectData({
|
||||||
type: "train",
|
type: "train",
|
||||||
value: currentTrainData?.num,
|
value: trainNum,
|
||||||
fixed: true,
|
fixed: true,
|
||||||
});
|
});
|
||||||
stackAwareNavigate("positions");
|
stackAwareNavigate("positions");
|
||||||
@@ -139,14 +242,11 @@ const TrainDataViewRow: FC<props> = ({
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View
|
<View
|
||||||
style={
|
style={{
|
||||||
{
|
flexDirection: mode == 2 ? "row" : "column",
|
||||||
flexDirection: mode == 2 ? "row" : "column",
|
flex: 1,
|
||||||
flex: 1,
|
...columnStyle,
|
||||||
...columnStyle
|
}}
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<View style={rightItemStyle}>
|
<View style={rightItemStyle}>
|
||||||
<StateBox
|
<StateBox
|
||||||
@@ -174,7 +274,7 @@ const TrainDataViewRow: FC<props> = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{nearTrainIDList.length == 0 ? (
|
{nearTrainIDList.length == 0 ? (
|
||||||
<StateBox mode={mode} title="列番" text={trainNumber} />
|
<StateBox mode={mode} title="列番" text={trainNum} />
|
||||||
) : (
|
) : (
|
||||||
<StateBox
|
<StateBox
|
||||||
mode={mode}
|
mode={mode}
|
||||||
@@ -192,82 +292,3 @@ const TrainDataViewRow: FC<props> = ({
|
|||||||
</View>
|
</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 {
|
import {
|
||||||
View,
|
View,
|
||||||
Text,
|
Text,
|
||||||
@@ -6,12 +7,11 @@ import {
|
|||||||
StyleSheet,
|
StyleSheet,
|
||||||
useWindowDimensions,
|
useWindowDimensions,
|
||||||
BackHandler,
|
BackHandler,
|
||||||
Linking,
|
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
import { SheetManager } from "react-native-actions-sheet";
|
import { SheetManager } from "react-native-actions-sheet";
|
||||||
import { getTrainType } from "../../lib/getTrainType";
|
import { getTrainType } from "@/lib/getTrainType";
|
||||||
import { customTrainDataDetector } from "../custom-train-data";
|
import { customTrainDataDetector } from "../custom-train-data";
|
||||||
import { EachStopList } from "./EachTrainInfo/EachStopList";
|
import { EachStopList, type EachStopData } from "./EachTrainInfo/EachStopList";
|
||||||
import { DataConnectedButton } from "./EachTrainInfo/DataConnectedButton";
|
import { DataConnectedButton } from "./EachTrainInfo/DataConnectedButton";
|
||||||
import { DynamicHeaderScrollView } from "../DynamicHeaderScrollView";
|
import { DynamicHeaderScrollView } from "../DynamicHeaderScrollView";
|
||||||
import { LongHeader } from "./EachTrainInfo/LongHeader";
|
import { LongHeader } from "./EachTrainInfo/LongHeader";
|
||||||
@@ -19,10 +19,13 @@ import { ShortHeader } from "./EachTrainInfo/ShortHeader";
|
|||||||
import { ScrollStickyContent } from "./EachTrainInfo/ScrollStickyContent";
|
import { ScrollStickyContent } from "./EachTrainInfo/ScrollStickyContent";
|
||||||
import { ShowSpecialTrain } from "./EachTrainInfo/ShowSpecialTrain";
|
import { ShowSpecialTrain } from "./EachTrainInfo/ShowSpecialTrain";
|
||||||
import { HeaderText } from "./EachTrainInfoCore/HeaderText";
|
import { HeaderText } from "./EachTrainInfoCore/HeaderText";
|
||||||
import { useStationList } from "../../stateBox/useStationList";
|
import { useStationList } from "@/stateBox/useStationList";
|
||||||
import { useCurrentTrainData } from "../../stateBox/useCurrentTrain";
|
import { useCurrentTrainData } from "@/stateBox/useCurrentTrain";
|
||||||
import { useThemeColors } from "@/lib/theme";
|
import { useThemeColors } from "@/lib/theme";
|
||||||
import { useCustomTrainData } from "@/stateBox/useAllTrainDiagram";
|
import {
|
||||||
|
useCustomTrainData,
|
||||||
|
useOperationLogs,
|
||||||
|
} from "@/stateBox/useAllTrainDiagram";
|
||||||
import { useResponsive } from "@/lib/responsive";
|
import { useResponsive } from "@/lib/responsive";
|
||||||
|
|
||||||
// Custom hooks
|
// Custom hooks
|
||||||
@@ -34,6 +37,7 @@ import { useTrainPosition } from "./EachTrainInfoCore/hooks/useTrainPosition";
|
|||||||
import { useAutoScroll } from "./EachTrainInfoCore/hooks/useAutoScroll";
|
import { useAutoScroll } from "./EachTrainInfoCore/hooks/useAutoScroll";
|
||||||
import { useExtendedStations } from "./EachTrainInfoCore/hooks/useExtendedStations";
|
import { useExtendedStations } from "./EachTrainInfoCore/hooks/useExtendedStations";
|
||||||
import type { EachTrainInfoPayload } from "@/types/actionSheets";
|
import type { EachTrainInfoPayload } from "@/types/actionSheets";
|
||||||
|
import { openExternalUrl } from "@/lib/openExternalUrl";
|
||||||
|
|
||||||
type EachTrainInfoCoreProps = {
|
type EachTrainInfoCoreProps = {
|
||||||
trainNum: EachTrainInfoPayload["trainNum"];
|
trainNum: EachTrainInfoPayload["trainNum"];
|
||||||
@@ -43,6 +47,11 @@ type EachTrainInfoCoreProps = {
|
|||||||
sheetOpened?: boolean;
|
sheetOpened?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const parseEachStopData = (data: string): EachStopData => {
|
||||||
|
const [station = "", se = "", time = "", platformNum] = data.split(",");
|
||||||
|
return [station, se, time, platformNum];
|
||||||
|
};
|
||||||
|
|
||||||
export const EachTrainInfoCore = ({
|
export const EachTrainInfoCore = ({
|
||||||
trainNum,
|
trainNum,
|
||||||
openStationACFromEachTrainInfo,
|
openStationACFromEachTrainInfo,
|
||||||
@@ -51,21 +60,56 @@ export const EachTrainInfoCore = ({
|
|||||||
sheetOpened = false,
|
sheetOpened = false,
|
||||||
}: EachTrainInfoCoreProps) => {
|
}: EachTrainInfoCoreProps) => {
|
||||||
const from = fromValue ?? "";
|
const from = fromValue ?? "";
|
||||||
const { stationList } = useStationList();
|
const { allCustomTrainData, activateCustomTrainData } = useCustomTrainData();
|
||||||
const { allCustomTrainData } = useCustomTrainData();
|
const { activateOperationLogs } = useOperationLogs();
|
||||||
const { colors, fixed } = useThemeColors();
|
const { colors, fixed, isDark } = useThemeColors();
|
||||||
const { verticalScale, moderateScale } = useResponsive();
|
const { verticalScale, moderateScale, fontScale } = useResponsive();
|
||||||
const { height } = useWindowDimensions();
|
const { height } = useWindowDimensions();
|
||||||
const { getCurrentStationData } = useCurrentTrainData();
|
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);
|
const scrollRef = useRef<any>(null);
|
||||||
// Custom hooks for data management
|
// Custom hooks for data management
|
||||||
const { trainData, setTrainData, trueTrainID } =
|
const { trainData, setTrainData, trueTrainID } =
|
||||||
useTrainDiagramData(trainNum);
|
useTrainDiagramData(trainNum);
|
||||||
const { trainDataWithThrough, haveThrough } = useThroughStations(trainData);
|
const { trainDataWithThrough, haveThrough } = useThroughStations(trainData);
|
||||||
|
const {
|
||||||
|
showHeadStation,
|
||||||
|
showTailStation,
|
||||||
|
includedTrainIDs,
|
||||||
|
extendToHeadStation,
|
||||||
|
extendToTailStation,
|
||||||
|
} = useExtendedStations(trainNum, trainData, setTrainData);
|
||||||
const { headStation, tailStation, nearTrainIDList } = useNearbyTrains(
|
const { headStation, tailStation, nearTrainIDList } = useNearbyTrains(
|
||||||
trainNum,
|
trainNum,
|
||||||
trainData,
|
trainData,
|
||||||
|
includedTrainIDs,
|
||||||
);
|
);
|
||||||
const stopStationIDList = useStopStationIDs(trainDataWithThrough);
|
const stopStationIDList = useStopStationIDs(trainDataWithThrough);
|
||||||
const {
|
const {
|
||||||
@@ -77,12 +121,15 @@ export const EachTrainInfoCore = ({
|
|||||||
pointsDisplay,
|
pointsDisplay,
|
||||||
} = useTrainPosition(trainNum, stopStationIDList);
|
} = useTrainPosition(trainNum, stopStationIDList);
|
||||||
|
|
||||||
const {
|
const parsedTrainData = useMemo<readonly EachStopData[]>(
|
||||||
showHeadStation,
|
() =>
|
||||||
showTailStation,
|
trainDataWithThrough.map(parseEachStopData),
|
||||||
extendToHeadStation,
|
[trainDataWithThrough],
|
||||||
extendToTailStation,
|
);
|
||||||
} = useExtendedStations(trainData, setTrainData);
|
const clockBase = useMemo(
|
||||||
|
() => dayjs(),
|
||||||
|
[trainDataWithThrough, currentTrainData?.delay],
|
||||||
|
);
|
||||||
|
|
||||||
// UI state
|
// UI state
|
||||||
// 走行中の列車は初期状態から通過駅を表示する(後から showThrew を true に変更すると
|
// 走行中の列車は初期状態から通過駅を表示する(後から showThrew を true に変更すると
|
||||||
@@ -90,15 +137,11 @@ export const EachTrainInfoCore = ({
|
|||||||
const [showThrew, setShowThrew] = useState(
|
const [showThrew, setShowThrew] = useState(
|
||||||
() => !!getCurrentStationData(trainNum),
|
() => !!getCurrentStationData(trainNum),
|
||||||
);
|
);
|
||||||
const [isJumped, setIsJumped] = useState(false);
|
|
||||||
|
|
||||||
// Auto scroll to current position
|
// Auto scroll to current position
|
||||||
useAutoScroll(
|
useAutoScroll(
|
||||||
points,
|
points,
|
||||||
trainDataWithThrough,
|
trainDataWithThrough,
|
||||||
scrollRef,
|
scrollRef,
|
||||||
isJumped,
|
|
||||||
setIsJumped,
|
|
||||||
sheetOpened,
|
sheetOpened,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -132,6 +175,133 @@ export const EachTrainInfoCore = ({
|
|||||||
}, 200);
|
}, 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 (
|
return (
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
@@ -159,14 +329,11 @@ export const EachTrainInfoCore = ({
|
|||||||
trainData={trainData}
|
trainData={trainData}
|
||||||
showHeadStation={showHeadStation}
|
showHeadStation={showHeadStation}
|
||||||
showTailStation={showTailStation}
|
showTailStation={showTailStation}
|
||||||
headStation={headStation}
|
|
||||||
tailStation={tailStation}
|
|
||||||
navigate={navigate}
|
navigate={navigate}
|
||||||
from={from}
|
from={from}
|
||||||
fontLoaded={true}
|
fontLoaded={true}
|
||||||
scrollRef={scrollRef}
|
scrollRef={scrollRef}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DynamicHeaderScrollView
|
<DynamicHeaderScrollView
|
||||||
from={from}
|
from={from}
|
||||||
styles={styles as any}
|
styles={styles as any}
|
||||||
@@ -183,145 +350,47 @@ export const EachTrainInfoCore = ({
|
|||||||
shortHeader={
|
shortHeader={
|
||||||
<ShortHeader
|
<ShortHeader
|
||||||
{...{
|
{...{
|
||||||
|
openTrainInfo,
|
||||||
|
navigate,
|
||||||
|
nearTrainIDList,
|
||||||
currentTrainData,
|
currentTrainData,
|
||||||
currentTrainDataList,
|
currentTrainDataList,
|
||||||
currentPosition,
|
currentPosition,
|
||||||
currentPositionList,
|
currentPositionList,
|
||||||
nearTrainIDList,
|
|
||||||
openTrainInfo,
|
|
||||||
navigate,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
longHeader={
|
longHeader={
|
||||||
<LongHeader
|
<LongHeader
|
||||||
{...{
|
{...{
|
||||||
|
openTrainInfo,
|
||||||
|
navigate,
|
||||||
currentTrainData,
|
currentTrainData,
|
||||||
|
nearTrainIDList,
|
||||||
currentTrainDataList,
|
currentTrainDataList,
|
||||||
currentPosition,
|
currentPosition,
|
||||||
currentPositionList,
|
currentPositionList,
|
||||||
nearTrainIDList,
|
|
||||||
openTrainInfo,
|
|
||||||
navigate,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
topStickyContent={
|
topStickyContent={
|
||||||
<ScrollStickyContent
|
<ScrollStickyContent
|
||||||
{...{ currentTrainData, showThrew, setShowThrew, haveThrough }}
|
{...{
|
||||||
|
trainDelay: currentTrainData?.delay,
|
||||||
|
showThrew,
|
||||||
|
setShowThrew,
|
||||||
|
haveThrough,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{customTrainType.data === "notService" && (
|
{listHeader}
|
||||||
<Text
|
{trainDataWithThrough.map((item, index) => (
|
||||||
style={{
|
<React.Fragment key={`${trainNum}-${index}`}>
|
||||||
backgroundColor: colors.surface,
|
{renderTrainDataItem(item, index)}
|
||||||
color: colors.text,
|
</React.Fragment>
|
||||||
fontWeight: "bold",
|
))}
|
||||||
}}
|
{listFooter}
|
||||||
>
|
|
||||||
この列車には乗車できません。
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
{headStation.length > 0 &&
|
|
||||||
headStation.map(
|
|
||||||
(item, index) =>
|
|
||||||
!showHeadStation.includes(index) && (
|
|
||||||
<TouchableOpacity
|
|
||||||
onPress={() =>
|
|
||||||
extendToHeadStation(item.station, item.dia, index)
|
|
||||||
}
|
|
||||||
style={[
|
|
||||||
styles.extendStationButton,
|
|
||||||
{ borderColor: colors.textAccent },
|
|
||||||
]}
|
|
||||||
key={`${item.station}-head${index}`}
|
|
||||||
>
|
|
||||||
<Text
|
|
||||||
style={[styles.extendStationText, { color: colors.text }]}
|
|
||||||
>
|
|
||||||
「本当の始発駅」を表示
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
),
|
|
||||||
)}
|
|
||||||
<ShowSpecialTrain
|
|
||||||
isTrainDataNothing={trainData.length === 0}
|
|
||||||
setTrainData={setTrainData}
|
|
||||||
trueTrainID={trueTrainID}
|
|
||||||
/>
|
|
||||||
{!trainData.length && (
|
|
||||||
<TouchableOpacity
|
|
||||||
onPress={() =>
|
|
||||||
Linking.openURL(`https://twitter.com/search?q=${trainNum}`)
|
|
||||||
}
|
|
||||||
style={[
|
|
||||||
styles.twitterSearchButton,
|
|
||||||
{
|
|
||||||
borderColor: colors.textAccent,
|
|
||||||
backgroundColor: colors.backgroundOverlay,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Text style={[styles.extendStationText, { color: colors.text }]}>
|
|
||||||
Twitterで検索
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
)}
|
|
||||||
{trainDataWithThrough.map((item, index, array) =>
|
|
||||||
item.split(",")[1] === "増" || item.split(",")[1] === "解" ? (
|
|
||||||
<DataConnectedButton
|
|
||||||
i={item}
|
|
||||||
key={`${item}-data`}
|
|
||||||
openTrainInfo={openTrainInfo}
|
|
||||||
/>
|
|
||||||
) : item.split(",")[1].includes(".") ? (
|
|
||||||
<React.Fragment key={`${item}-skip`} />
|
|
||||||
) : (
|
|
||||||
<EachStopList
|
|
||||||
i={item}
|
|
||||||
index={index}
|
|
||||||
stationList={stationList as any}
|
|
||||||
points={pointsDisplay?.[index] || false}
|
|
||||||
currentTrainData={currentTrainData as any}
|
|
||||||
openStationACFromEachTrainInfo={openStationACFromEachTrainInfo}
|
|
||||||
showThrew={showThrew}
|
|
||||||
array={array}
|
|
||||||
isNotService={customTrainType.data === "notService"}
|
|
||||||
key={`${item}-stop`}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
)}
|
|
||||||
<Text style={[styles.customDataNote, { color: colors.textSecondary }]}>
|
|
||||||
時刻が斜体,青色になっている時刻はコミュニティで追加されている独自データです。
|
|
||||||
</Text>
|
|
||||||
{tailStation.length > 0 &&
|
|
||||||
tailStation.map(
|
|
||||||
({ station, dia }, index) =>
|
|
||||||
!showTailStation.includes(index) && (
|
|
||||||
<TouchableOpacity
|
|
||||||
onPress={() => extendToTailStation(station, dia, index)}
|
|
||||||
style={[
|
|
||||||
styles.extendStationButton,
|
|
||||||
{ borderColor: colors.textAccent },
|
|
||||||
]}
|
|
||||||
key={`${station}-tail${index}`}
|
|
||||||
>
|
|
||||||
<Text
|
|
||||||
style={[styles.extendStationText, { color: colors.text }]}
|
|
||||||
>
|
|
||||||
「本当の終着駅」を表示
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
),
|
|
||||||
)}
|
|
||||||
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
styles.bottomSpacer,
|
|
||||||
{ borderBottomColor: colors.borderLight },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</DynamicHeaderScrollView>
|
</DynamicHeaderScrollView>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
|
import { useServiceStates } from "@/stateBox/useCurrentTrain";
|
||||||
|
import { formationText, serviceStateText } from "@/lib/serviceState/display";
|
||||||
import React, { FC, useMemo } from "react";
|
import React, { FC, useMemo } from "react";
|
||||||
import { Text, View, TextStyle, TouchableOpacity } from "react-native";
|
import { Text, View, TextStyle, TouchableOpacity } from "react-native";
|
||||||
import { SheetManager } from "react-native-actions-sheet";
|
import { SheetManager } from "react-native-actions-sheet";
|
||||||
import { migrateTrainName } from "../../../lib/eachTrainInfoCoreLib/migrateTrainName";
|
import { migrateTrainName } from "@/lib/eachTrainInfoCoreLib/migrateTrainName";
|
||||||
import { TrainIconStatus } from "./trainIconStatus";
|
import { TrainIconStatus } from "./trainIconStatus";
|
||||||
import { OneManText } from "./HeaderTextParts/OneManText";
|
import { OneManText } from "./HeaderTextParts/OneManText";
|
||||||
import { customTrainDataDetector } from "@/components/custom-train-data";
|
import { customTrainDataDetector } from "@/components/custom-train-data";
|
||||||
@@ -26,10 +28,8 @@ import { normalizeIconDisplayMode } from "@/lib/iconDisplayMode";
|
|||||||
type Props = {
|
type Props = {
|
||||||
trainNum: string;
|
trainNum: string;
|
||||||
trainData: string[];
|
trainData: string[];
|
||||||
showHeadStation: number[];
|
showHeadStation: string[];
|
||||||
showTailStation: number[];
|
showTailStation: string[];
|
||||||
headStation: { id: string }[];
|
|
||||||
tailStation: { id: string }[];
|
|
||||||
navigate: NavigateFunction;
|
navigate: NavigateFunction;
|
||||||
from: string;
|
from: string;
|
||||||
fontLoaded: boolean;
|
fontLoaded: boolean;
|
||||||
@@ -46,8 +46,6 @@ export const HeaderText: FC<Props> = ({
|
|||||||
trainData,
|
trainData,
|
||||||
showHeadStation,
|
showHeadStation,
|
||||||
showTailStation,
|
showTailStation,
|
||||||
headStation,
|
|
||||||
tailStation,
|
|
||||||
navigate,
|
navigate,
|
||||||
from,
|
from,
|
||||||
scrollRef,
|
scrollRef,
|
||||||
@@ -56,6 +54,9 @@ export const HeaderText: FC<Props> = ({
|
|||||||
const { fixed } = useThemeColors();
|
const { fixed } = useThemeColors();
|
||||||
const { fontScale } = useResponsive();
|
const { fontScale } = useResponsive();
|
||||||
const { iconSetting } = useTrainMenu();
|
const { iconSetting } = useTrainMenu();
|
||||||
|
const { serviceStates, available, getFormation } = useServiceStates();
|
||||||
|
const serviceState = serviceStates.find(state => state.train_id === trainNum);
|
||||||
|
const shadowText = available ? serviceState ? serviceStateText(serviceState) : formationText(getFormation(trainNum)) : null;
|
||||||
const { allCustomTrainData } = useCustomTrainData();
|
const { allCustomTrainData } = useCustomTrainData();
|
||||||
const { getTodayOperationByTrainId } = useOperationLogs();
|
const { getTodayOperationByTrainId } = useOperationLogs();
|
||||||
const { expoPushToken } = useNotification();
|
const { expoPushToken } = useNotification();
|
||||||
@@ -226,6 +227,7 @@ export const HeaderText: FC<Props> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
padding: 10,
|
padding: 10,
|
||||||
@@ -237,14 +239,14 @@ export const HeaderText: FC<Props> = ({
|
|||||||
scrollRef.current?.scrollTo({ y: 0, animated: true })
|
scrollRef.current?.scrollTo({ y: 0, animated: true })
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<TrainIconStatus
|
{!shadowText && <TrainIconStatus
|
||||||
trainNum={trainNum}
|
trainNum={trainNum}
|
||||||
navigate={navigate}
|
navigate={navigate}
|
||||||
from={from}
|
from={from}
|
||||||
todayOperation={todayOperation}
|
todayOperation={todayOperation}
|
||||||
direction={iconTrainDirection}
|
direction={iconTrainDirection}
|
||||||
iconDisplayMode={iconDisplayMode}
|
iconDisplayMode={iconDisplayMode}
|
||||||
/>
|
/>}
|
||||||
|
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
@@ -309,9 +311,9 @@ export const HeaderText: FC<Props> = ({
|
|||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<View style={{ flex: 1 }} />
|
<View style={{ flex: 1 }} />
|
||||||
<Text style={{ ...textConfig, fontSize: fontScale(17), color: fixed.textOnPrimary }}>
|
<Text style={{ ...textConfig, fontSize: fontScale(17), color: fixed.textOnPrimary }}>
|
||||||
{showHeadStation.map((d) => `${headStation[d].id} + `)}
|
{showHeadStation.map((trainID) => `${trainID} + `)}
|
||||||
{trainNum}
|
{trainNum}
|
||||||
{showTailStation.map((d) => ` + ${tailStation[d].id}`)}
|
{showTailStation.map((trainID) => ` + ${trainID}`)}
|
||||||
</Text>
|
</Text>
|
||||||
<MaterialCommunityIcons
|
<MaterialCommunityIcons
|
||||||
name="database"
|
name="database"
|
||||||
@@ -347,5 +349,9 @@ export const HeaderText: FC<Props> = ({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
{shadowText && <View style={{ paddingHorizontal: 10, paddingBottom: 10 }}>
|
||||||
|
<Text style={{ color: fixed.textOnPrimary, fontSize: fontScale(14) }}>{shadowText}</Text>
|
||||||
|
</View>}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React, { ComponentProps, FC } from "react";
|
|||||||
import { Image, TouchableOpacity, View } from "react-native";
|
import { Image, TouchableOpacity, View } from "react-native";
|
||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
import type { TrainIconEntry } from "@/lib/trainIconEntries";
|
import type { TrainIconEntry } from "@/lib/trainIconEntries";
|
||||||
|
import { normalizeVehicleInfoUrl } from "@/lib/vehicleInfoUrl";
|
||||||
|
|
||||||
type StackVariant = "header" | "list";
|
type StackVariant = "header" | "list";
|
||||||
type StatusIcon = ComponentProps<typeof Ionicons>;
|
type StatusIcon = ComponentProps<typeof Ionicons>;
|
||||||
@@ -87,7 +88,7 @@ export const TrainIconStack: FC<Props> = ({
|
|||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
key={`${trainIcon}-${index}`}
|
key={`${trainIcon}-${index}`}
|
||||||
onPress={() => onPressEntry(entry, index)}
|
onPress={() => onPressEntry(entry, index)}
|
||||||
disabled={!entry.vehicle_info_url}
|
disabled={!normalizeVehicleInfoUrl(entry.vehicle_info_url)}
|
||||||
>
|
>
|
||||||
{content}
|
{content}
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|||||||
@@ -1,33 +1,46 @@
|
|||||||
import { useEffect, MutableRefObject } from 'react';
|
import { useEffect, MutableRefObject, useRef } from 'react';
|
||||||
|
|
||||||
|
|
||||||
export const useAutoScroll = (
|
export const useAutoScroll = (
|
||||||
points: boolean[] | undefined,
|
points: boolean[] | undefined,
|
||||||
trainDataWithThrough: string[],
|
trainDataWithThrough: string[],
|
||||||
scrollRef: MutableRefObject<any>,
|
scrollRef: MutableRefObject<any>,
|
||||||
isJumped: boolean,
|
|
||||||
setIsJumped: (value: boolean) => void,
|
|
||||||
sheetOpened: boolean = false
|
sheetOpened: boolean = false
|
||||||
) => {
|
) => {
|
||||||
|
const jumpedRef = useRef(false);
|
||||||
|
const trainDataRef = useRef(trainDataWithThrough);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (trainDataRef.current !== trainDataWithThrough) {
|
||||||
|
trainDataRef.current = trainDataWithThrough;
|
||||||
|
jumpedRef.current = false;
|
||||||
|
}
|
||||||
|
|
||||||
// ActionSheetのスプリングアニメーション完了後まで待機
|
// ActionSheetのスプリングアニメーション完了後まで待機
|
||||||
if (!sheetOpened || isJumped || !points?.length || !scrollRef) return;
|
if (!sheetOpened || jumpedRef.current || !points?.length || !scrollRef) return;
|
||||||
|
|
||||||
const currentPositionIndex = points.findIndex((d) => d === true);
|
const currentPositionIndex = points.findIndex((d) => d === true);
|
||||||
if (currentPositionIndex === -1) return;
|
if (currentPositionIndex === -1) return;
|
||||||
|
|
||||||
// 5駅以内の場合はスクロールしない
|
// 5駅以内の場合はスクロールしない
|
||||||
if (currentPositionIndex < 5) {
|
if (currentPositionIndex < 5) {
|
||||||
setIsJumped(true);
|
jumpedRef.current = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const scrollPosition = currentPositionIndex * 44 - 50;
|
const scrollPosition = currentPositionIndex * 44 - 50;
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
scrollRef.current?.scrollTo({ y: scrollPosition, animated: true });
|
if (typeof scrollRef.current?.scrollToOffset === 'function') {
|
||||||
setIsJumped(true);
|
scrollRef.current.scrollToOffset({
|
||||||
|
offset: scrollPosition,
|
||||||
|
animated: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
scrollRef.current?.scrollTo({ y: scrollPosition, animated: true });
|
||||||
|
}
|
||||||
|
jumpedRef.current = true;
|
||||||
}, 100);
|
}, 100);
|
||||||
|
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}, [sheetOpened, points, trainDataWithThrough, scrollRef, isJumped, setIsJumped]);
|
}, [sheetOpened, points, trainDataWithThrough, scrollRef]);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,29 +1,85 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { openBackTrainInfo } from '@/lib/eachTrainInfoCoreLib/openBackTrainInfo';
|
import { openBackTrainInfo } from '@/lib/eachTrainInfoCoreLib/openBackTrainInfo';
|
||||||
|
|
||||||
export const useExtendedStations = (trainData, setTrainData) => {
|
type ExtendedState = {
|
||||||
const [showHeadStation, setShowHeadStation] = useState([]);
|
trainNum: string;
|
||||||
const [showTailStation, setShowTailStation] = useState([]);
|
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);
|
const array = openBackTrainInfo(station, trainData, dia);
|
||||||
if (!array) return;
|
if (!array) return;
|
||||||
|
|
||||||
setTrainData(array);
|
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);
|
const array = openBackTrainInfo(station, trainData, dia);
|
||||||
if (!array) return;
|
if (!array) return;
|
||||||
|
|
||||||
setTrainData(array);
|
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 {
|
return {
|
||||||
showHeadStation,
|
showHeadStation: currentState.headTrainIDs,
|
||||||
showTailStation,
|
showTailStation: currentState.tailTrainIDs,
|
||||||
|
includedTrainIDs: currentState.includedTrainIDs,
|
||||||
extendToHeadStation,
|
extendToHeadStation,
|
||||||
extendToTailStation,
|
extendToTailStation,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,27 +1,61 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useMemo } from "react";
|
||||||
import { useBusAndTrainData } from "@/stateBox/useBusAndTrainData";
|
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 { getInfluencedTrainData } = useBusAndTrainData();
|
||||||
const [headStation, setHeadStation] = useState([]);
|
|
||||||
const [tailStation, setTailStation] = useState([]);
|
|
||||||
const [nearTrainIDList, setNearTrainIDList] = useState([]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
return useMemo(() => {
|
||||||
if (!trainNum || !trainData.length) return;
|
if (!trainNum || !trainData.length) {
|
||||||
|
return { headStation: [], tailStation: [], nearTrainIDList: [] };
|
||||||
|
}
|
||||||
|
|
||||||
const nearTrainList = getInfluencedTrainData(trainNum);
|
const nearTrainList = getInfluencedTrainData(trainNum) ?? [];
|
||||||
if (!nearTrainList.length) return;
|
if (!nearTrainList.length) {
|
||||||
|
return { headStation: [], tailStation: [], nearTrainIDList: [] };
|
||||||
|
}
|
||||||
|
|
||||||
const trainDataArray = nearTrainList.map((d) => d.TrainData);
|
// Opening any train in a multi-train service should allow the whole
|
||||||
const trainIDs = nearTrainList.map((d) => d.id);
|
// connected chain to be extended. The direct list is still kept for the
|
||||||
setNearTrainIDList(trainIDs);
|
// 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 [firstStation] = trainData[0].split(",");
|
||||||
const [lastStation] = trainData[trainData.length - 1].split(",");
|
const [lastStation] = trainData[trainData.length - 1].split(",");
|
||||||
|
|
||||||
const head = [];
|
const head: BoundaryTrain[] = [];
|
||||||
const tail = [];
|
const tail: BoundaryTrain[] = [];
|
||||||
|
|
||||||
trainDataArray.forEach((data, i) => {
|
trainDataArray.forEach((data, i) => {
|
||||||
data.forEach((d) => {
|
data.forEach((d) => {
|
||||||
@@ -47,9 +81,10 @@ export const useNearbyTrains = (trainNum, trainData) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
setHeadStation(head);
|
return {
|
||||||
setTailStation(tail);
|
headStation: head,
|
||||||
}, [trainData, trainNum, getInfluencedTrainData]);
|
tailStation: tail,
|
||||||
|
nearTrainIDList: nearTrainList.map((d) => d.id),
|
||||||
return { headStation, tailStation, nearTrainIDList };
|
};
|
||||||
|
}, [getInfluencedTrainData, includedTrainIDs, trainData, trainNum]);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useStationList } from '@/stateBox/useStationList';
|
import { useStationList } from '@/stateBox/useStationList';
|
||||||
|
|
||||||
const computeStopStationIDs = (data: string[], stationList: any[][]): string[][] =>
|
const computeStopStationIDs = (data: string[], stationList: any[][]): string[][] =>
|
||||||
@@ -13,14 +13,9 @@ const computeStopStationIDs = (data: string[], stationList: any[][]): string[][]
|
|||||||
export const useStopStationIDs = (trainDataWithThrough: string[]) => {
|
export const useStopStationIDs = (trainDataWithThrough: string[]) => {
|
||||||
const { stationList } = useStationList();
|
const { stationList } = useStationList();
|
||||||
|
|
||||||
// 初回レンダリング時に同期的に計算することでActionSheetのアニメーション中の高さ変化を防ぐ
|
// station IDも表示データから直接導出し、effect由来の追加renderをなくす。
|
||||||
const [stopStationIDList, setStopStationIDList] = useState<string[][]>(() =>
|
return useMemo(
|
||||||
computeStopStationIDs(trainDataWithThrough, stationList)
|
() => 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 { lineListPair, stationIDPair } from '@/lib/getStationList';
|
||||||
import { useStationList } from '@/stateBox/useStationList';
|
import { useStationList } from '@/stateBox/useStationList';
|
||||||
import { OriginalStationList } from '@/lib/CommonTypes';
|
import { OriginalStationList } from '@/lib/CommonTypes';
|
||||||
|
|
||||||
const computeThroughStations = (
|
const computeThroughStationsUnmeasured = (
|
||||||
trainData: string[],
|
trainData: string[],
|
||||||
stationList: any[][],
|
stationList: any[][],
|
||||||
originalStationList: OriginalStationList
|
originalStationList: OriginalStationList
|
||||||
@@ -103,14 +103,20 @@ const computeThroughStations = (
|
|||||||
export const useThroughStations = (trainData) => {
|
export const useThroughStations = (trainData) => {
|
||||||
const { originalStationList, stationList } = useStationList();
|
const { originalStationList, stationList } = useStationList();
|
||||||
|
|
||||||
// 初回レンダリング時に同期的に計算することでActionSheetのアニメーション中の高さ変化を防ぐ
|
// 純粋な派生値なのでstate/effectで一度古いリストを描画してから更新しない。
|
||||||
const [state, setState] = useState(() =>
|
return useMemo(
|
||||||
computeThroughStations(trainData, stationList, originalStationList)
|
() => 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 { useTrainDiagram } from '@/stateBox/useAllTrainDiagram';
|
||||||
import { searchSpecialTrain } from '@/lib/eachTrainInfoCoreLib/searchSpecialTrain';
|
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: [] };
|
if (!trainNum) return { data: [], trueIDs: [] };
|
||||||
const TD = trainList[trainNum];
|
const TD = trainList[trainNum];
|
||||||
if (!TD) {
|
if (!TD) {
|
||||||
@@ -14,28 +17,30 @@ const parseTrainData = (trainNum: string, trainList: Record<string, string>) =>
|
|||||||
|
|
||||||
export const useTrainDiagramData = (trainNum) => {
|
export const useTrainDiagramData = (trainNum) => {
|
||||||
const { allTrainDiagram: trainList } = useTrainDiagram();
|
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のアニメーション中に高さが変わるのを防ぐ
|
// effectで新しい配列へ差し替える経路をなくし、リストの余分な再描画を防ぐ。
|
||||||
const [trainData, setTrainData] = useState(() => parseTrainData(trainNum, trainList).data);
|
const trainData =
|
||||||
const [trueTrainID, setTrueTrainID] = useState(() => parseTrainData(trainNum, trainList).trueIDs);
|
manualData?.trainNum === trainNum ? manualData.data : parsed.data;
|
||||||
|
|
||||||
useEffect(() => {
|
const setTrainDataExtended = useCallback(
|
||||||
if (!trainNum) return;
|
(data: string[]) => {
|
||||||
|
setManualData({ trainNum, data });
|
||||||
|
},
|
||||||
|
[trainNum],
|
||||||
|
);
|
||||||
|
|
||||||
// 手動で拡張されている場合は上書きしない
|
return {
|
||||||
if (isManuallyExtended) return;
|
trainData,
|
||||||
|
setTrainData: setTrainDataExtended,
|
||||||
const { data, trueIDs } = parseTrainData(trainNum, trainList);
|
trueTrainID: parsed.trueIDs,
|
||||||
setTrueTrainID(trueIDs);
|
|
||||||
setTrainData(data);
|
|
||||||
}, [trainNum, trainList, isManuallyExtended]);
|
|
||||||
|
|
||||||
const setTrainDataExtended = (data) => {
|
|
||||||
setTrainData(data);
|
|
||||||
setIsManuallyExtended(true);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
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 { AS } from '@/storageControl';
|
||||||
import { STORAGE_KEYS } from '@/constants';
|
import { STORAGE_KEYS } from '@/constants';
|
||||||
import { logger } from '@/utils/logger';
|
import { logger } from '@/utils/logger';
|
||||||
@@ -7,7 +7,6 @@ import { useTrainMenu } from '@/stateBox/useTrainMenu';
|
|||||||
import type { trainDataType } from '@/lib/trainPositionTextArray';
|
import type { trainDataType } from '@/lib/trainPositionTextArray';
|
||||||
import { lineIdFromStationNumber } from '@/lib/checkDuplicateTrainData';
|
import { lineIdFromStationNumber } from '@/lib/checkDuplicateTrainData';
|
||||||
import { findReversalPoints } from '@/lib/eachTrainInfoCoreLib/findReversalPoints';
|
import { findReversalPoints } from '@/lib/eachTrainInfoCoreLib/findReversalPoints';
|
||||||
|
|
||||||
export const useTrainPosition = (
|
export const useTrainPosition = (
|
||||||
trainNum: string,
|
trainNum: string,
|
||||||
stopStationIDList: string[][]
|
stopStationIDList: string[][]
|
||||||
@@ -18,13 +17,9 @@ export const useTrainPosition = (
|
|||||||
getPosition,
|
getPosition,
|
||||||
} = useCurrentTrainData();
|
} = useCurrentTrainData();
|
||||||
const { updatePermission } = useTrainMenu();
|
const { updatePermission } = useTrainMenu();
|
||||||
const [currentTrainData, setCurrentTrainData] = useState<trainDataType | undefined>();
|
|
||||||
const [currentTrainDataList, setCurrentTrainDataList] = useState<trainDataType[]>([]);
|
|
||||||
const [currentPosition, setCurrentPosition] = useState<string[]>([]);
|
|
||||||
const [currentPositionList, setCurrentPositionList] = useState<string[][]>([]);
|
|
||||||
const [trainPositionSwitch, setTrainPositionSwitch] = useState<string>('false');
|
const [trainPositionSwitch, setTrainPositionSwitch] = useState<string>('false');
|
||||||
|
|
||||||
useEffect(() => {
|
const currentTrainDataList = useMemo(() => {
|
||||||
const candidates = getCurrentStationDataList(trainNum);
|
const candidates = getCurrentStationDataList(trainNum);
|
||||||
const routeLineCounts = new Map<string, number>();
|
const routeLineCounts = new Map<string, number>();
|
||||||
for (const stationNumber of stopStationIDList.flat()) {
|
for (const stationNumber of stopStationIDList.flat()) {
|
||||||
@@ -35,14 +30,11 @@ export const useTrainPosition = (
|
|||||||
.sort(([, countA], [, countB]) => countB - countA)
|
.sort(([, countA], [, countB]) => countB - countA)
|
||||||
.map(([line]) => line);
|
.map(([line]) => line);
|
||||||
const selected = getCurrentStationData(trainNum, routeLineIds);
|
const selected = getCurrentStationData(trainNum, routeLineIds);
|
||||||
const visible = updatePermission
|
return updatePermission
|
||||||
? candidates
|
? candidates
|
||||||
: selected
|
: selected
|
||||||
? [selected]
|
? [selected]
|
||||||
: [];
|
: [];
|
||||||
logger.debug('Train position data:', visible);
|
|
||||||
setCurrentTrainDataList(visible);
|
|
||||||
setCurrentTrainData(visible[0]);
|
|
||||||
}, [
|
}, [
|
||||||
getCurrentStationData,
|
getCurrentStationData,
|
||||||
getCurrentStationDataList,
|
getCurrentStationDataList,
|
||||||
@@ -50,15 +42,16 @@ export const useTrainPosition = (
|
|||||||
trainNum,
|
trainNum,
|
||||||
updatePermission,
|
updatePermission,
|
||||||
]);
|
]);
|
||||||
|
const currentTrainData = currentTrainDataList[0];
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!stopStationIDList.length || !currentTrainDataList.length) {
|
logger.debug('Train position data:', currentTrainDataList);
|
||||||
setCurrentPositionList([]);
|
}, [currentTrainDataList]);
|
||||||
setCurrentPosition([]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const positions = currentTrainDataList.map((train) => {
|
const currentPositionList = useMemo(() => {
|
||||||
|
if (!stopStationIDList.length || !currentTrainDataList.length) return [];
|
||||||
|
|
||||||
|
return currentTrainDataList.map((train) => {
|
||||||
const position = getPosition(train);
|
const position = getPosition(train);
|
||||||
const next = position ? [...position] : [];
|
const next = position ? [...position] : [];
|
||||||
if (next.length > 1) {
|
if (next.length > 1) {
|
||||||
@@ -79,9 +72,8 @@ export const useTrainPosition = (
|
|||||||
}
|
}
|
||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
setCurrentPositionList(positions);
|
}, [currentTrainDataList, getPosition, stopStationIDList]);
|
||||||
setCurrentPosition(positions[0] ?? []);
|
const currentPosition = currentPositionList[0] ?? [];
|
||||||
}, [currentTrainDataList, stopStationIDList, getPosition]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
AS.getItem(STORAGE_KEYS.TRAIN_POSITION_SWITCH)
|
AS.getItem(STORAGE_KEYS.TRAIN_POSITION_SWITCH)
|
||||||
@@ -107,15 +99,21 @@ export const useTrainPosition = (
|
|||||||
return merged;
|
return merged;
|
||||||
};
|
};
|
||||||
|
|
||||||
const points =
|
const points = useMemo(
|
||||||
trainPositionSwitch === 'true'
|
() =>
|
||||||
? mergePoints(true)
|
trainPositionSwitch === 'true'
|
||||||
: stopStationIDList.map(() => false);
|
? mergePoints(true)
|
||||||
|
: stopStationIDList.map(() => false),
|
||||||
|
[currentPositionList, stopStationIDList, trainPositionSwitch],
|
||||||
|
);
|
||||||
|
|
||||||
const pointsDisplay =
|
const pointsDisplay = useMemo(
|
||||||
trainPositionSwitch === 'true'
|
() =>
|
||||||
? mergePoints(false)
|
trainPositionSwitch === 'true'
|
||||||
: stopStationIDList.map(() => false);
|
? mergePoints(false)
|
||||||
|
: stopStationIDList.map(() => false),
|
||||||
|
[currentPositionList, stopStationIDList, trainPositionSwitch],
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
currentTrainData,
|
currentTrainData,
|
||||||
|
|||||||
@@ -2,13 +2,14 @@ import React, { ComponentProps, FC, useEffect, useMemo, useState } from "react";
|
|||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { SheetManager } from "react-native-actions-sheet";
|
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 { useCustomTrainData } from "@/stateBox/useAllTrainDiagram";
|
||||||
import { useInterval } from "@/lib/useInterval";
|
import { useInterval } from "@/lib/useInterval";
|
||||||
import type { NavigateFunction } from "@/types";
|
import type { NavigateFunction } from "@/types";
|
||||||
import { OperationLogs } from "@/lib/CommonTypes";
|
import { OperationLogs } from "@/lib/CommonTypes";
|
||||||
import type { IconDisplayMode } from "@/lib/iconDisplayMode";
|
import type { IconDisplayMode } from "@/lib/iconDisplayMode";
|
||||||
import { resolveTrainIconEntries } from "@/lib/trainIconEntries";
|
import { resolveTrainIconEntries } from "@/lib/trainIconEntries";
|
||||||
|
import { normalizeVehicleInfoUrl } from "@/lib/vehicleInfoUrl";
|
||||||
import { TrainIconStack } from "./TrainIconStack";
|
import { TrainIconStack } from "./TrainIconStack";
|
||||||
|
|
||||||
type GlyphNames = ComponentProps<typeof Ionicons>["name"];
|
type GlyphNames = ComponentProps<typeof Ionicons>["name"];
|
||||||
@@ -122,11 +123,22 @@ export const TrainIconStatus: FC<Props> = (props) => {
|
|||||||
hidden={!!anpanmanStatus && showIcon}
|
hidden={!!anpanmanStatus && showIcon}
|
||||||
statusIcon={anpanmanStatus}
|
statusIcon={anpanmanStatus}
|
||||||
onPressEntry={(entry) => {
|
onPressEntry={(entry) => {
|
||||||
navigate("howto", {
|
const vehicleInfoUrl = normalizeVehicleInfoUrl(entry.vehicle_info_url);
|
||||||
info: entry.vehicle_info_url,
|
if (!vehicleInfoUrl) return;
|
||||||
goTo: from == "LED" ? "menu" : from,
|
|
||||||
});
|
void SheetManager.hide("EachTrainInfo")
|
||||||
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 React, { FC, useEffect, useState } from "react";
|
||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
import { SheetManager } from "react-native-actions-sheet";
|
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 { useThemeColors } from "@/lib/theme";
|
||||||
import type { NavigateFunction } from "@/types";
|
import type { NavigateFunction } from "@/types";
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import LottieView from "lottie-react-native";
|
|||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
import ViewShot from "react-native-view-shot";
|
import ViewShot from "react-native-view-shot";
|
||||||
import * as Sharing from "expo-sharing";
|
import * as Sharing from "expo-sharing";
|
||||||
import { useTrainDelayData } from "../../stateBox/useTrainDelayData";
|
import { useTrainDelayData } from "@/stateBox/useTrainDelayData";
|
||||||
import { BottomButtons } from "./JRSTraInfo/BottomButtons";
|
import { BottomButtons } from "./JRSTraInfo/BottomButtons";
|
||||||
import { useThemeColors } from "@/lib/theme";
|
import { useThemeColors } from "@/lib/theme";
|
||||||
import { useResponsive } from "@/lib/responsive";
|
import { useResponsive } from "@/lib/responsive";
|
||||||
@@ -127,7 +127,7 @@ export const JRSTraInfo = () => {
|
|||||||
autoPlay
|
autoPlay
|
||||||
loop
|
loop
|
||||||
style={{ width: 150, height: 150, backgroundColor: colors.surface }}
|
style={{ width: 150, height: 150, backgroundColor: colors.surface }}
|
||||||
source={require("../../assets/51690-loading-diamonds.json")}
|
source={require("@/assets/51690-loading-diamonds.json")}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
) : delayData ? (
|
) : delayData ? (
|
||||||
|
|||||||
@@ -3,12 +3,12 @@ import {
|
|||||||
View,
|
View,
|
||||||
Text,
|
Text,
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
Linking,
|
|
||||||
StyleProp,
|
StyleProp,
|
||||||
ViewStyle,
|
ViewStyle,
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
import { MaterialCommunityIcons } from "@expo/vector-icons";
|
import { MaterialCommunityIcons } from "@expo/vector-icons";
|
||||||
import { useThemeColors } from "@/lib/theme";
|
import { useThemeColors } from "@/lib/theme";
|
||||||
|
import { openExternalUrl } from "@/lib/openExternalUrl";
|
||||||
|
|
||||||
export const BottomButtons: FC<{ onCapture: () => void }> = ({ onCapture }) => {
|
export const BottomButtons: FC<{ onCapture: () => void }> = ({ onCapture }) => {
|
||||||
const { fixed } = useThemeColors();
|
const { fixed } = useThemeColors();
|
||||||
@@ -35,7 +35,7 @@ export const BottomButtons: FC<{ onCapture: () => void }> = ({ onCapture }) => {
|
|||||||
>
|
>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={{ ...styles, flex: 1 }}
|
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} />
|
<MaterialCommunityIcons name="mastodon" color={fixed.textOnPrimary} size={30} />
|
||||||
<View style={{ flex: 1 }} />
|
<View style={{ flex: 1 }} />
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import {
|
|||||||
Platform,
|
Platform,
|
||||||
Text,
|
Text,
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
Linking,
|
|
||||||
useWindowDimensions,
|
useWindowDimensions,
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
import ActionSheet from "react-native-actions-sheet";
|
import ActionSheet from "react-native-actions-sheet";
|
||||||
@@ -15,6 +14,7 @@ import { NewsReleaseInfoBox } from "../Menu/NewsReleaseInfoBox";
|
|||||||
import { useThemeColors } from "@/lib/theme";
|
import { useThemeColors } from "@/lib/theme";
|
||||||
import { useResponsive } from "@/lib/responsive";
|
import { useResponsive } from "@/lib/responsive";
|
||||||
import type { NewsReleaseInfoPayload } from "@/types/actionSheets";
|
import type { NewsReleaseInfoPayload } from "@/types/actionSheets";
|
||||||
|
import { openExternalUrl } from "@/lib/openExternalUrl";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
payload: NewsReleaseInfoPayload;
|
payload: NewsReleaseInfoPayload;
|
||||||
@@ -115,7 +115,7 @@ const NewsReleaseInfoContent: FC<ContentProps> = ({ payload: { navigate }, scrol
|
|||||||
{/* 下部固定ボタン(ScrollViewの外、画面の下固定) */}
|
{/* 下部固定ボタン(ScrollViewの外、画面の下固定) */}
|
||||||
<View style={{ padding: 10, backgroundColor: fixed.primary }}>
|
<View style={{ padding: 10, backgroundColor: fixed.primary }}>
|
||||||
<TouchableOpacity
|
<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={{
|
style={{
|
||||||
paddingVertical: 12,
|
paddingVertical: 12,
|
||||||
paddingLeft: 14,
|
paddingLeft: 14,
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ import ActionSheet ,{ ScrollView } from "react-native-actions-sheet";
|
|||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
import { ListItem } from "@rneui/themed";
|
import { ListItem } from "@rneui/themed";
|
||||||
import { MaterialCommunityIcons, Foundation } from "@expo/vector-icons";
|
import { MaterialCommunityIcons, Foundation } from "@expo/vector-icons";
|
||||||
import { Linking } from "react-native";
|
|
||||||
import TouchableScale from "react-native-touchable-scale";
|
import TouchableScale from "react-native-touchable-scale";
|
||||||
import { useThemeColors } from "@/lib/theme";
|
import { useThemeColors } from "@/lib/theme";
|
||||||
import { useSheetMaxHeight } from "./useSheetMaxHeight";
|
import { useSheetMaxHeight } from "./useSheetMaxHeight";
|
||||||
|
import { openExternalUrl } from "@/lib/openExternalUrl";
|
||||||
export const Social = () => {
|
export const Social = () => {
|
||||||
const actionSheetRef = useRef(null);
|
const actionSheetRef = useRef(null);
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
@@ -66,8 +66,8 @@ export const Social = () => {
|
|||||||
</View>
|
</View>
|
||||||
<ListItem
|
<ListItem
|
||||||
bottomDivider
|
bottomDivider
|
||||||
onPress={() => Linking.openURL("tel:0570-00-4592")}
|
onPress={() => openExternalUrl("tel:0570-00-4592")}
|
||||||
// @ts-ignore: TouchableScale props passed through Component
|
// @ts-expect-error RNEUIのListItem型はComponentに渡したTouchableScaleのpropsを含まない
|
||||||
friction={90}
|
friction={90}
|
||||||
tension={100}
|
tension={100}
|
||||||
activeScale={0.95}
|
activeScale={0.95}
|
||||||
@@ -83,7 +83,7 @@ export const Social = () => {
|
|||||||
0570-00-4592(8:00〜20:00 年中無休)
|
0570-00-4592(8:00〜20:00 年中無休)
|
||||||
</ListItem.Subtitle>
|
</ListItem.Subtitle>
|
||||||
</ListItem.Content>
|
</ListItem.Content>
|
||||||
{/* @ts-ignore: ListItem.Chevron doesn't expose color prop */}
|
{/* @ts-expect-error RNEUIのListItem.Chevron型はcolorを公開していないが、実装はIconへ渡す */}
|
||||||
<ListItem.Chevron color={"white"} />
|
<ListItem.Chevron color={"white"} />
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<ScrollView
|
<ScrollView
|
||||||
@@ -152,9 +152,9 @@ export const Social = () => {
|
|||||||
].map((d) => (
|
].map((d) => (
|
||||||
<ListItem
|
<ListItem
|
||||||
bottomDivider
|
bottomDivider
|
||||||
onPress={() => Linking.openURL(d.url)}
|
onPress={() => openExternalUrl(d.url)}
|
||||||
key={d.url}
|
key={d.url}
|
||||||
// @ts-ignore: TouchableScale props passed through Component
|
// @ts-expect-error RNEUIのListItem型はComponentに渡したTouchableScaleのpropsを含まない
|
||||||
friction={90} //
|
friction={90} //
|
||||||
tension={100} // These props are passed to the parent component (here TouchableScale)
|
tension={100} // These props are passed to the parent component (here TouchableScale)
|
||||||
activeScale={0.95} //
|
activeScale={0.95} //
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
Linking,
|
|
||||||
BackHandler,
|
BackHandler,
|
||||||
Platform,
|
Platform,
|
||||||
useWindowDimensions,
|
useWindowDimensions,
|
||||||
@@ -9,11 +8,11 @@ import {
|
|||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
import ActionSheet, { SheetManager } from "react-native-actions-sheet";
|
import ActionSheet, { SheetManager } from "react-native-actions-sheet";
|
||||||
import type { SheetProps } 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 { getPDFViewURL } from "@/lib/getPdfViewURL";
|
||||||
import { useBusAndTrainData } from "../../stateBox/useBusAndTrainData";
|
import { useBusAndTrainData } from "@/stateBox/useBusAndTrainData";
|
||||||
import { AS } from "../../storageControl";
|
import { AS } from "@/storageControl";
|
||||||
import { STORAGE_KEYS } from "@/constants";
|
import { STORAGE_KEYS } from "@/constants";
|
||||||
import { StationMapButton } from "./StationDeteilView/StationMapButton";
|
import { StationMapButton } from "./StationDeteilView/StationMapButton";
|
||||||
import { TrainBusButton } from "./StationDeteilView/TrainBusButton";
|
import { TrainBusButton } from "./StationDeteilView/TrainBusButton";
|
||||||
@@ -26,6 +25,7 @@ import { useTrainMenu } from "@/stateBox/useTrainMenu";
|
|||||||
import { useThemeColors } from "@/lib/theme";
|
import { useThemeColors } from "@/lib/theme";
|
||||||
import { useResponsive } from "@/lib/responsive";
|
import { useResponsive } from "@/lib/responsive";
|
||||||
import { useSheetMaxHeight } from "./useSheetMaxHeight";
|
import { useSheetMaxHeight } from "./useSheetMaxHeight";
|
||||||
|
import { openExternalUrl } from "@/lib/openExternalUrl";
|
||||||
|
|
||||||
export const StationDeteilView = ({ payload }: SheetProps<"StationDetailView">) => {
|
export const StationDeteilView = ({ payload }: SheetProps<"StationDetailView">) => {
|
||||||
if (!payload) return <></>;
|
if (!payload) return <></>;
|
||||||
@@ -102,15 +102,14 @@ export const StationDeteilView = ({ payload }: SheetProps<"StationDetailView">)
|
|||||||
stationID={station.StationNumber}
|
stationID={station.StationNumber}
|
||||||
oP={() => {
|
oP={() => {
|
||||||
usePDFView == "true"
|
usePDFView == "true"
|
||||||
? Linking.openURL(station.StationTimeTable)
|
? openExternalUrl(station.StationTimeTable)
|
||||||
: navigate("howto", {
|
: navigate("generalWebView", {
|
||||||
info,
|
uri: info,
|
||||||
goTo,
|
onExit: goTo != "NearTrainDiagramView" ? useShow : undefined,
|
||||||
useShow,
|
|
||||||
});
|
});
|
||||||
onExit();
|
onExit();
|
||||||
}}
|
}}
|
||||||
oLP={() => Linking.openURL(station.StationTimeTable)}
|
oLP={() => openExternalUrl(station.StationTimeTable)}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<View style={{ flexDirection: "row" }}>
|
<View style={{ flexDirection: "row" }}>
|
||||||
@@ -165,10 +164,9 @@ export const StationDeteilView = ({ payload }: SheetProps<"StationDetailView">)
|
|||||||
<TrainBusButton
|
<TrainBusButton
|
||||||
address={trainBus.address}
|
address={trainBus.address}
|
||||||
press={() => {
|
press={() => {
|
||||||
navigate("howto", {
|
navigate("generalWebView", {
|
||||||
info: trainBus.address,
|
uri: trainBus.address,
|
||||||
goTo,
|
onExit: goTo != "NearTrainDiagramView" ? useShow : undefined,
|
||||||
useShow,
|
|
||||||
});
|
});
|
||||||
onExit();
|
onExit();
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { FC } from "react";
|
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 { useThemeColors } from "@/lib/theme";
|
||||||
import type { NavigateFunction } from "@/types";
|
import type { NavigateFunction } from "@/types";
|
||||||
|
import { openExternalUrl } from "@/lib/openExternalUrl";
|
||||||
type Props = {
|
type Props = {
|
||||||
navigate: NavigateFunction;
|
navigate: NavigateFunction;
|
||||||
address: string;
|
address: string;
|
||||||
@@ -25,10 +26,13 @@ export const 駅構内図:FC<Props> = (props) => {
|
|||||||
flex: 1,
|
flex: 1,
|
||||||
}}
|
}}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
navigate("howto", { info, goTo, useShow });
|
navigate("generalWebView", {
|
||||||
|
uri: info,
|
||||||
|
onExit: goTo != "NearTrainDiagramView" ? useShow : undefined,
|
||||||
|
});
|
||||||
onExit();
|
onExit();
|
||||||
}}
|
}}
|
||||||
onLongPress={() => Linking.openURL(info)}
|
onLongPress={() => openExternalUrl(info)}
|
||||||
>
|
>
|
||||||
<View style={{ flex: 1 }} />
|
<View style={{ flex: 1 }} />
|
||||||
<Text
|
<Text
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Linking } from "react-native";
|
|
||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
import { TicketBox } from "@/components/atom/TicketBox";
|
import { TicketBox } from "@/components/atom/TicketBox";
|
||||||
import { useThemeColors } from "@/lib/theme";
|
import { useThemeColors } from "@/lib/theme";
|
||||||
|
import { openExternalUrl } from "@/lib/openExternalUrl";
|
||||||
export const StationMapButton = ({stationMap}) => {
|
export const StationMapButton = ({stationMap}) => {
|
||||||
const { fixed } = useThemeColors();
|
const { fixed } = useThemeColors();
|
||||||
return (
|
return (
|
||||||
@@ -10,7 +10,7 @@ export const StationMapButton = ({stationMap}) => {
|
|||||||
backgroundColor={"#888A85"}
|
backgroundColor={"#888A85"}
|
||||||
icon={<Ionicons name="map" color={fixed.textOnPrimary} size={50} />}
|
icon={<Ionicons name="map" color={fixed.textOnPrimary} size={50} />}
|
||||||
flex={1}
|
flex={1}
|
||||||
onPressButton={() => Linking.openURL(stationMap)}
|
onPressButton={() => openExternalUrl(stationMap)}
|
||||||
>
|
>
|
||||||
Map
|
Map
|
||||||
</TicketBox>
|
</TicketBox>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import React, { FC } from "react";
|
import React, { FC } from "react";
|
||||||
import { Linking } from "react-native";
|
|
||||||
import { FontAwesome } from "@expo/vector-icons";
|
import { FontAwesome } from "@expo/vector-icons";
|
||||||
import { TicketBox } from "@/components/atom/TicketBox";
|
import { TicketBox } from "@/components/atom/TicketBox";
|
||||||
import { useThemeColors } from "@/lib/theme";
|
import { useThemeColors } from "@/lib/theme";
|
||||||
import type { NavigateFunction } from "@/types";
|
import type { NavigateFunction } from "@/types";
|
||||||
|
import { openExternalUrl } from "@/lib/openExternalUrl";
|
||||||
type Props = {
|
type Props = {
|
||||||
info: string;
|
info: string;
|
||||||
address: string;
|
address: string;
|
||||||
@@ -23,11 +23,14 @@ export const StationTimeTableButton: FC<Props> = (props) => {
|
|||||||
flex={1}
|
flex={1}
|
||||||
onPressButton={() => {
|
onPressButton={() => {
|
||||||
usePDFView == "true"
|
usePDFView == "true"
|
||||||
? Linking.openURL(address)
|
? openExternalUrl(address)
|
||||||
: navigate("howto", { info, goTo, useShow });
|
: navigate("generalWebView", {
|
||||||
|
uri: info,
|
||||||
|
onExit: goTo != "NearTrainDiagramView" ? useShow : undefined,
|
||||||
|
});
|
||||||
onExit();
|
onExit();
|
||||||
}}
|
}}
|
||||||
onLongPressButton={() => Linking.openURL(address)}
|
onLongPressButton={() => openExternalUrl(address)}
|
||||||
>
|
>
|
||||||
時刻表
|
時刻表
|
||||||
</TicketBox>
|
</TicketBox>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import React, { FC } from "react";
|
import React, { FC } from "react";
|
||||||
import { Linking } from "react-native";
|
|
||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
import { TicketBox } from "@/components/atom/TicketBox";
|
import { TicketBox } from "@/components/atom/TicketBox";
|
||||||
import { useThemeColors } from "@/lib/theme";
|
import { useThemeColors } from "@/lib/theme";
|
||||||
|
import { openExternalUrl } from "@/lib/openExternalUrl";
|
||||||
type Props = {
|
type Props = {
|
||||||
address: string;
|
address: string;
|
||||||
press: () => void;
|
press: () => void;
|
||||||
@@ -15,7 +15,7 @@ export const TrainBusButton: FC<Props> = ({ address, press }) => {
|
|||||||
icon={<Ionicons name="bus" color={fixed.textOnPrimary} size={50} />}
|
icon={<Ionicons name="bus" color={fixed.textOnPrimary} size={50} />}
|
||||||
flex={1}
|
flex={1}
|
||||||
onPressButton={press}
|
onPressButton={press}
|
||||||
onLongPressButton={() => Linking.openURL(address)}
|
onLongPressButton={() => openExternalUrl(address)}
|
||||||
>
|
>
|
||||||
並行バス
|
並行バス
|
||||||
</TicketBox>
|
</TicketBox>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import React, { FC } from "react";
|
import React, { FC } from "react";
|
||||||
import { Linking } from "react-native";
|
|
||||||
import { Foundation } from "@expo/vector-icons";
|
import { Foundation } from "@expo/vector-icons";
|
||||||
import { TicketBox } from "@/components/atom/TicketBox";
|
import { TicketBox } from "@/components/atom/TicketBox";
|
||||||
import { useThemeColors } from "@/lib/theme";
|
import { useThemeColors } from "@/lib/theme";
|
||||||
import type { NavigateFunction } from "@/types";
|
import type { NavigateFunction } from "@/types";
|
||||||
|
import { openExternalUrl } from "@/lib/openExternalUrl";
|
||||||
type Props = {
|
type Props = {
|
||||||
navigate: NavigateFunction;
|
navigate: NavigateFunction;
|
||||||
info: string;
|
info: string;
|
||||||
@@ -20,10 +20,13 @@ export const WebSiteButton: FC<Props> = (Props) => {
|
|||||||
icon={<Foundation name="web" color={fixed.textOnPrimary} size={50} />}
|
icon={<Foundation name="web" color={fixed.textOnPrimary} size={50} />}
|
||||||
flex={1}
|
flex={1}
|
||||||
onPressButton={() => {
|
onPressButton={() => {
|
||||||
navigate("howto", { info, goTo, useShow });
|
navigate("generalWebView", {
|
||||||
|
uri: info,
|
||||||
|
onExit: goTo != "NearTrainDiagramView" ? useShow : undefined,
|
||||||
|
});
|
||||||
onExit();
|
onExit();
|
||||||
}}
|
}}
|
||||||
onLongPressButton={() => Linking.openURL(info)}
|
onLongPressButton={() => openExternalUrl(info)}
|
||||||
>
|
>
|
||||||
web
|
web
|
||||||
</TicketBox>
|
</TicketBox>
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import {
|
|||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
Platform,
|
Platform,
|
||||||
Linking,
|
|
||||||
Image,
|
Image,
|
||||||
Animated,
|
Animated,
|
||||||
useWindowDimensions,
|
useWindowDimensions,
|
||||||
@@ -27,6 +26,7 @@ import {
|
|||||||
import ViewShot from "react-native-view-shot";
|
import ViewShot from "react-native-view-shot";
|
||||||
import * as Sharing from "expo-sharing";
|
import * as Sharing from "expo-sharing";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
|
import { openExternalUrl } from "@/lib/openExternalUrl";
|
||||||
|
|
||||||
export type { TrainDataSourcesPayload } from "@/types/actionSheets";
|
export type { TrainDataSourcesPayload } from "@/types/actionSheets";
|
||||||
|
|
||||||
@@ -762,7 +762,7 @@ export const TrainDataSources: FC<{ payload?: TrainDataSourcesPayload }> = ({
|
|||||||
const url =
|
const url =
|
||||||
activeElesiteGroup?.timetableUrl || "https://www.elesite-next.com/";
|
activeElesiteGroup?.timetableUrl || "https://www.elesite-next.com/";
|
||||||
SheetManager.hide("TrainDataSources");
|
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 { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
import ViewShot from "react-native-view-shot";
|
import ViewShot from "react-native-view-shot";
|
||||||
import * as Sharing from "expo-sharing";
|
import * as Sharing from "expo-sharing";
|
||||||
import icons from "../../assets/icons/icons";
|
import icons from "@/assets/icons/icons";
|
||||||
import hubIcons from "../../assets/unyoHubIcons/icons";
|
import hubIcons from "@/assets/unyoHubIcons/icons";
|
||||||
|
|
||||||
import { getAppIconName } from "expo-alternate-app-icons";
|
import { getAppIconName } from "expo-alternate-app-icons";
|
||||||
import { AS } from "@/storageControl";
|
import { AS } from "@/storageControl";
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ import {
|
|||||||
} from "react-native";
|
} from "react-native";
|
||||||
import ActionSheet, { SheetManager } from "react-native-actions-sheet";
|
import ActionSheet, { SheetManager } from "react-native-actions-sheet";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
import { useTrainMenu } from "../../stateBox/useTrainMenu";
|
import { useTrainMenu } from "@/stateBox/useTrainMenu";
|
||||||
import { useCurrentTrainView } from "../../stateBox/useCurrentTrain";
|
import { useCurrentTrainView } from "@/stateBox/useCurrentTrain";
|
||||||
import lineColorList from "../../assets/originData/lineColorList";
|
import lineColorList from "@/assets/originData/lineColorList";
|
||||||
import { stationIDPair, lineListPair } from "../../lib/getStationList";
|
import { stationIDPair, lineListPair } from "@/lib/getStationList";
|
||||||
import { useThemeColors } from "@/lib/theme";
|
import { useThemeColors } from "@/lib/theme";
|
||||||
import { useSheetMaxHeight } from "./useSheetMaxHeight";
|
import { useSheetMaxHeight } from "./useSheetMaxHeight";
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useRef, FC } from "react";
|
import React, { useEffect, useState, useRef, FC } from "react";
|
||||||
import {
|
import {
|
||||||
Animated,
|
Animated,
|
||||||
View,
|
View,
|
||||||
@@ -7,7 +7,6 @@ import {
|
|||||||
FlatList,
|
FlatList,
|
||||||
TextInput,
|
TextInput,
|
||||||
ScrollView,
|
ScrollView,
|
||||||
Linking,
|
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
import {
|
import {
|
||||||
useCustomTrainData,
|
useCustomTrainData,
|
||||||
@@ -33,6 +32,7 @@ import { InfogramText } from "./ActionSheetComponents/EachTrainInfoCore/HeaderTe
|
|||||||
import { resolveTrainIconEntries } from "@/lib/trainIconEntries";
|
import { resolveTrainIconEntries } from "@/lib/trainIconEntries";
|
||||||
import { TrainIconStack } from "./ActionSheetComponents/EachTrainInfoCore/TrainIconStack";
|
import { TrainIconStack } from "./ActionSheetComponents/EachTrainInfoCore/TrainIconStack";
|
||||||
import { formatWrappedTrainType, shouldWrapTrainType } from "@/lib/trainTypeWrap";
|
import { formatWrappedTrainType, shouldWrapTrainType } from "@/lib/trainTypeWrap";
|
||||||
|
import { openExternalUrl } from "@/lib/openExternalUrl";
|
||||||
|
|
||||||
export const AllTrainDiagramView: FC = () => {
|
export const AllTrainDiagramView: FC = () => {
|
||||||
const { colors, fixed } = useThemeColors();
|
const { colors, fixed } = useThemeColors();
|
||||||
@@ -41,8 +41,16 @@ export const AllTrainDiagramView: FC = () => {
|
|||||||
const iconDisplayMode = normalizeIconDisplayMode(iconSetting);
|
const iconDisplayMode = normalizeIconDisplayMode(iconSetting);
|
||||||
const tabBarHeight = useBottomTabBarHeight();
|
const tabBarHeight = useBottomTabBarHeight();
|
||||||
const { keyList, allTrainDiagram } = useTrainDiagram();
|
const { keyList, allTrainDiagram } = useTrainDiagram();
|
||||||
const { allCustomTrainData } = useCustomTrainData();
|
const { allCustomTrainData, activateCustomTrainData } = useCustomTrainData();
|
||||||
const { getTodayOperationByTrainId } = useOperationLogs();
|
const {
|
||||||
|
getTodayOperationByTrainId,
|
||||||
|
activateOperationLogs,
|
||||||
|
} = useOperationLogs();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
activateCustomTrainData();
|
||||||
|
activateOperationLogs();
|
||||||
|
}, [activateCustomTrainData, activateOperationLogs]);
|
||||||
const [input, setInput] = useState(""); // 文字入力
|
const [input, setInput] = useState(""); // 文字入力
|
||||||
const [useStationName, setUseStationName] = useState(false);
|
const [useStationName, setUseStationName] = useState(false);
|
||||||
const [useRegex, setUseRegex] = useState(false);
|
const [useRegex, setUseRegex] = useState(false);
|
||||||
@@ -331,7 +339,7 @@ export const AllTrainDiagramView: FC = () => {
|
|||||||
<Text
|
<Text
|
||||||
style={{ ...regexTextButtonStyle, backgroundColor: "green" }}
|
style={{ ...regexTextButtonStyle, backgroundColor: "green" }}
|
||||||
onPress={() =>
|
onPress={() =>
|
||||||
Linking.openURL(
|
openExternalUrl(
|
||||||
"https://qiita.com/tossh/items/635aea9a529b9deb3038"
|
"https://qiita.com/tossh/items/635aea9a529b9deb3038"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { FlexWidget, OverlapWidget, SvgWidget, TextWidget } from "react-native-android-widget";
|
import { FlexWidget, OverlapWidget, SvgWidget, TextWidget } from "react-native-android-widget";
|
||||||
import { AS } from "../../storageControl";
|
import { AS } from "@/storageControl";
|
||||||
import { STORAGE_KEYS } from "../../constants";
|
import { STORAGE_KEYS } from "@/constants";
|
||||||
import { WidgetColors, widgetLightColors } from "./widget-theme";
|
import { WidgetColors, widgetLightColors } from "./widget-theme";
|
||||||
|
|
||||||
type LastFelicaSnapshot = {
|
type LastFelicaSnapshot = {
|
||||||
|
|||||||
@@ -11,23 +11,28 @@ import type { OperationInfoSnapshot } from "@/types";
|
|||||||
|
|
||||||
export const getInfoString = async () => {
|
export const getInfoString = async () => {
|
||||||
const time = dayjs().format("HH:mm");
|
const time = dayjs().format("HH:mm");
|
||||||
const response = await fetch(API_ENDPOINTS.OPERATION_INFO, {
|
try {
|
||||||
cache: "no-store",
|
const response = await fetch(API_ENDPOINTS.OPERATION_INFO, {
|
||||||
});
|
cache: "no-store",
|
||||||
if (!response.ok) {
|
});
|
||||||
throw new Error(
|
if (!response.ok) {
|
||||||
`Operation information request failed: ${response.status}`
|
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 (
|
return (
|
||||||
<FlexWidget
|
<FlexWidget
|
||||||
style={{
|
style={{
|
||||||
@@ -98,7 +103,7 @@ export function InfoWidget({ time, text, colors = widgetLightColors }: { time: s
|
|||||||
color: colors.text,
|
color: colors.text,
|
||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
}}
|
}}
|
||||||
text="通常運行中です。"
|
text={failed ? "運行情報を取得できませんでした。" : "通常運行中です。"}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</ListWidget>
|
</ListWidget>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { FlexWidget, TextWidget } from "react-native-android-widget";
|
|||||||
import { getDelayData } from "./TraInfoEXWidget";
|
import { getDelayData } from "./TraInfoEXWidget";
|
||||||
import { getInfoString } from "./InfoWidget";
|
import { getInfoString } from "./InfoWidget";
|
||||||
import { getFelicaQuickAccessData } from "./FelicaQuickAccessWidget";
|
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";
|
import { WidgetColors, widgetLightColors } from "./widget-theme";
|
||||||
|
|
||||||
export async function getShortcutData() {
|
export async function getShortcutData() {
|
||||||
|
|||||||
@@ -11,18 +11,28 @@ import { API_ENDPOINTS } from "@/constants";
|
|||||||
export const getDelayData = async () => {
|
export const getDelayData = async () => {
|
||||||
// Fetch data from the server
|
// Fetch data from the server
|
||||||
const time = dayjs().format("HH:mm");
|
const time = dayjs().format("HH:mm");
|
||||||
const delayString = await fetch(API_ENDPOINTS.DELAY_INFO_LEGACY)
|
try {
|
||||||
.then((response) => response.text())
|
const delayString = await fetch(API_ENDPOINTS.DELAY_INFO_LEGACY)
|
||||||
.then((data) => {
|
.then((response) => {
|
||||||
if (data !== "") {
|
if (!response.ok) {
|
||||||
return data.split("^");
|
throw new Error(`Delay information request failed: ${response.status}`);
|
||||||
}
|
}
|
||||||
return null;
|
return response.text();
|
||||||
});
|
})
|
||||||
//ToastAndroid.show(`${delayString}`, ToastAndroid.SHORT);
|
.then((data) => {
|
||||||
return { time, delayString };
|
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 (
|
return (
|
||||||
<FlexWidget
|
<FlexWidget
|
||||||
style={{
|
style={{
|
||||||
@@ -104,7 +114,7 @@ export function TraInfoEXWidget({ time, delayString, colors = widgetLightColors
|
|||||||
color: colors.text,
|
color: colors.text,
|
||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
}}
|
}}
|
||||||
text="現在、5分以上の遅れはありません。"
|
text={failed ? "遅延情報を取得できませんでした。" : "現在、5分以上の遅れはありません。"}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</ListWidget>
|
</ListWidget>
|
||||||
|
|||||||
@@ -8,16 +8,8 @@ import {
|
|||||||
import { ShortcutWidget, getShortcutData } from "./ShortcutWidget";
|
import { ShortcutWidget, getShortcutData } from "./ShortcutWidget";
|
||||||
import { StrangeTrainWidget, getStrangeTrainData } from "./StrangeTrainWidget";
|
import { StrangeTrainWidget, getStrangeTrainData } from "./StrangeTrainWidget";
|
||||||
import { widgetLightColors, widgetDarkColors } from "./widget-theme";
|
import { widgetLightColors, widgetDarkColors } from "./widget-theme";
|
||||||
import { AS } from "../../storageControl";
|
import { AS } from "@/storageControl";
|
||||||
import { STORAGE_KEYS } from "../../constants";
|
import { STORAGE_KEYS } from "@/constants";
|
||||||
|
|
||||||
export const nameToWidget = {
|
|
||||||
JR_shikoku_train_info: TraInfoEXWidget,
|
|
||||||
Info_Widget: InfoWidget,
|
|
||||||
JR_shikoku_apps_shortcut: ShortcutWidget,
|
|
||||||
JR_shikoku_felica_balance: FelicaQuickAccessWidget,
|
|
||||||
JR_shikoku_train_strange: StrangeTrainWidget,
|
|
||||||
};
|
|
||||||
|
|
||||||
type ColorThemePref = "light" | "system" | "dark";
|
type ColorThemePref = "light" | "system" | "dark";
|
||||||
|
|
||||||
@@ -95,18 +87,18 @@ export async function widgetTaskHandler(props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (name === "JR_shikoku_info") {
|
if (name === "JR_shikoku_info") {
|
||||||
const { time, text } = await getInfoString();
|
const { time, text, failed } = await getInfoString();
|
||||||
renderWithTheme(renderWidget, theme, (isDark) => (
|
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;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// JR_shikoku_train_info and default
|
// JR_shikoku_train_info and default
|
||||||
{
|
{
|
||||||
const { time, delayString } = await getDelayData();
|
const { time, delayString, failed } = await getDelayData();
|
||||||
renderWithTheme(renderWidget, theme, (isDark) => (
|
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;
|
break;
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import React, { useEffect, useRef } from "react";
|
import React, { useEffect, useRef } from "react";
|
||||||
import { Animated, StyleSheet, View } from "react-native";
|
import { Animated, StyleSheet, View } from "react-native";
|
||||||
import { LinearGradient } from "expo-linear-gradient";
|
import { LinearGradient } from "expo-linear-gradient";
|
||||||
import type { RootTabParamList } from "../../types/navigation";
|
import type { RootTabParamList } from "@/types/navigation";
|
||||||
import lineColorList from "../../assets/originData/lineColorList";
|
import lineColorList from "@/assets/originData/lineColorList";
|
||||||
import { stationIDPair } from "../../lib/getStationList";
|
import { stationIDPair } from "@/lib/getStationList";
|
||||||
import { fixedColors } from "../../lib/theme/colors";
|
import { fixedColors } from "@/lib/theme/colors";
|
||||||
|
|
||||||
export type TabBarBackgroundProps = {
|
export type TabBarBackgroundProps = {
|
||||||
routeName: keyof RootTabParamList;
|
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 { GestureHandlerRootView } from "react-native-gesture-handler";
|
||||||
import { SafeAreaProvider } from "react-native-safe-area-context";
|
import { SafeAreaProvider } from "react-native-safe-area-context";
|
||||||
import { SheetProvider } from "react-native-actions-sheet";
|
import { SheetProvider } from "react-native-actions-sheet";
|
||||||
import { AppContainer } from "../Apps";
|
import { AppContainer } from "@/AppContainer";
|
||||||
import { AllTrainDiagramProvider } from "../stateBox/useAllTrainDiagram";
|
import { AllTrainDiagramProvider } from "../stateBox/useAllTrainDiagram";
|
||||||
import { NotificationProvider } from "../stateBox/useNotifications";
|
import { NotificationProvider } from "../stateBox/useNotifications";
|
||||||
import { UserPositionProvider } from "../stateBox/useUserPosition";
|
import { UserPositionProvider } from "../stateBox/useUserPosition";
|
||||||
@@ -16,6 +16,7 @@ import { BusAndTrainDataProvider } from "../stateBox/useBusAndTrainData";
|
|||||||
import { DeviceOrientationChangeProvider } from "../stateBox/useDeviceOrientationChange";
|
import { DeviceOrientationChangeProvider } from "../stateBox/useDeviceOrientationChange";
|
||||||
import { buildProvidersTree } from "../lib/providerTreeProvider";
|
import { buildProvidersTree } from "../lib/providerTreeProvider";
|
||||||
import { AppThemeProvider } from "../lib/theme";
|
import { AppThemeProvider } from "../lib/theme";
|
||||||
|
import { markStartupOnce } from "../lib/devStartupPerformance";
|
||||||
import StatusbarDetect from "../StatusbarDetect";
|
import StatusbarDetect from "../StatusbarDetect";
|
||||||
import "./ActionSheetComponents/sheets";
|
import "./ActionSheetComponents/sheets";
|
||||||
|
|
||||||
@@ -34,6 +35,7 @@ const ProviderTree = buildProvidersTree([
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
export function AppRootLayout(): React.ReactElement {
|
export function AppRootLayout(): React.ReactElement {
|
||||||
|
markStartupOnce("root-layout-render", "AppRootLayout render");
|
||||||
return (
|
return (
|
||||||
<AppThemeProvider>
|
<AppThemeProvider>
|
||||||
<DeviceOrientationChangeProvider>
|
<DeviceOrientationChangeProvider>
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import { MapsButton } from "./Apps/MapsButton";
|
|||||||
import { ReloadButton } from "./Apps/ReloadButton";
|
import { ReloadButton } from "./Apps/ReloadButton";
|
||||||
import { useStationList } from "../stateBox/useStationList";
|
import { useStationList } from "../stateBox/useStationList";
|
||||||
import { positionsLifecycleRef } from "../lib/rootNavigation";
|
import { positionsLifecycleRef } from "../lib/rootNavigation";
|
||||||
|
import { usePositionsStartupReady } from "../lib/positionsStartupContext";
|
||||||
import { FixedPositionBox } from "./Apps/FixedPositionBox";
|
import { FixedPositionBox } from "./Apps/FixedPositionBox";
|
||||||
import { PlaybackTimeline } from "./Apps/PlaybackTimeline";
|
import { PlaybackTimeline } from "./Apps/PlaybackTimeline";
|
||||||
import { RecordingStatusBar } from "./Apps/RecordingStatusBar";
|
import { RecordingStatusBar } from "./Apps/RecordingStatusBar";
|
||||||
@@ -43,6 +44,7 @@ export default function Apps() {
|
|||||||
const { mapSwitch, selectedLine } = useTrainMenu();
|
const { mapSwitch, selectedLine } = useTrainMenu();
|
||||||
const isDark = useColorScheme() === "dark";
|
const isDark = useColorScheme() === "dark";
|
||||||
const isFocused = useIsFocused();
|
const isFocused = useIsFocused();
|
||||||
|
const { notifyInitialLoadReady } = usePositionsStartupReady();
|
||||||
const [hasActivatedScreen, setHasActivatedScreen] = React.useState(false);
|
const [hasActivatedScreen, setHasActivatedScreen] = React.useState(false);
|
||||||
const [hasActivatedWebView, setHasActivatedWebView] = React.useState(false);
|
const [hasActivatedWebView, setHasActivatedWebView] = React.useState(false);
|
||||||
const [hasStableWebViewSession, setHasStableWebViewSession] =
|
const [hasStableWebViewSession, setHasStableWebViewSession] =
|
||||||
@@ -332,6 +334,7 @@ export default function Apps() {
|
|||||||
<AppsWebView
|
<AppsWebView
|
||||||
openStationACFromEachTrainInfo={openStationACFromEachTrainInfo}
|
openStationACFromEachTrainInfo={openStationACFromEachTrainInfo}
|
||||||
onInitialLoadReady={() => {
|
onInitialLoadReady={() => {
|
||||||
|
notifyInitialLoadReady();
|
||||||
if (hasStableWebViewSession) return;
|
if (hasStableWebViewSession) return;
|
||||||
positionsScreenTelemetry.info(
|
positionsScreenTelemetry.info(
|
||||||
"positions webview became stable",
|
"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分ごと)
|
// 遅延案内の再生間隔: 120〜180 秒をランダムで選択(2〜3分ごと)
|
||||||
const getAnnouncementIntervalMs = () =>
|
const getAnnouncementIntervalMs = () =>
|
||||||
(Math.floor(Math.random() * 60) + 120) * 1000;
|
(Math.floor(Math.random() * 60) + 120) * 1000;
|
||||||
@@ -409,7 +409,6 @@ export const FixedStation: FC<props> = ({ stationID }) => {
|
|||||||
goTo: "menu",
|
goTo: "menu",
|
||||||
onExit: () => SheetManager.hide("StationDetailView"),
|
onExit: () => SheetManager.hide("StationDetailView"),
|
||||||
};
|
};
|
||||||
//@ts-ignore
|
|
||||||
SheetManager.show("StationDetailView", { payload });
|
SheetManager.show("StationDetailView", { payload });
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
TextStyle,
|
TextStyle,
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
import { useThemeColors } from "@/lib/theme";
|
import { useThemeColors } from "@/lib/theme";
|
||||||
import { useTrainMenu } from "../../stateBox/useTrainMenu";
|
import { useTrainMenu } from "@/stateBox/useTrainMenu";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
import { useResponsive } from "@/lib/responsive";
|
import { useResponsive } from "@/lib/responsive";
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import { Ionicons } from "@expo/vector-icons";
|
|||||||
import { useThemeColors } from "@/lib/theme";
|
import { useThemeColors } from "@/lib/theme";
|
||||||
import * as Updates from "expo-updates";
|
import * as Updates from "expo-updates";
|
||||||
import Constants from "expo-constants";
|
import Constants from "expo-constants";
|
||||||
import { useCurrentTrainView } from "../../stateBox/useCurrentTrain";
|
import { useCurrentTrainView } from "@/stateBox/useCurrentTrain";
|
||||||
import { useTrainMenu } from "../../stateBox/useTrainMenu";
|
import { useTrainMenu } from "@/stateBox/useTrainMenu";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
import { useResponsive } from "@/lib/responsive";
|
import { useResponsive } from "@/lib/responsive";
|
||||||
|
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ import {
|
|||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { useTrainRecording } from "../../stateBox/useTrainMenu";
|
import { useTrainRecording } from "@/stateBox/useTrainMenu";
|
||||||
import { useThemeColors } from "../../lib/theme";
|
import { useThemeColors } from "@/lib/theme";
|
||||||
import { useResponsive } from "../../lib/responsive";
|
import { useResponsive } from "@/lib/responsive";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 再生中に走行位置WebViewの上部に表示するタイムラインコントローラー。
|
* 再生中に走行位置WebViewの上部に表示するタイムラインコントローラー。
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ import React, { FC, useEffect, useRef, useState } from "react";
|
|||||||
import { View, Text, AppState, InteractionManager } from "react-native";
|
import { View, Text, AppState, InteractionManager } from "react-native";
|
||||||
import { activateKeepAwakeAsync, deactivateKeepAwake } from "expo-keep-awake";
|
import { activateKeepAwakeAsync, deactivateKeepAwake } from "expo-keep-awake";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
import { useTrainRecording } from "../../stateBox/useTrainMenu";
|
import { useTrainRecording } from "@/stateBox/useTrainMenu";
|
||||||
import { useThemeColors } from "../../lib/theme";
|
import { useThemeColors } from "@/lib/theme";
|
||||||
import { useResponsive } from "../../lib/responsive";
|
import { useResponsive } from "@/lib/responsive";
|
||||||
|
|
||||||
const KEEP_AWAKE_TAG = "recording-status-bar";
|
const KEEP_AWAKE_TAG = "recording-status-bar";
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
} from "react-native";
|
} from "react-native";
|
||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
import { useThemeColors } from "@/lib/theme";
|
import { useThemeColors } from "@/lib/theme";
|
||||||
import { useTrainMenu } from "../../stateBox/useTrainMenu";
|
import { useTrainMenu } from "@/stateBox/useTrainMenu";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
import { useResponsive } from "@/lib/responsive";
|
import { useResponsive } from "@/lib/responsive";
|
||||||
|
|
||||||
|
|||||||
+146
-45
@@ -1,4 +1,5 @@
|
|||||||
import React, { useCallback, useEffect, useRef } from "react";
|
import { serviceStateDecorationScript, serviceStatesMessage } from "@/lib/serviceState/webView";
|
||||||
|
import React, { useCallback, useEffect, useMemo, useRef } from "react";
|
||||||
import { Platform } from "react-native";
|
import { Platform } from "react-native";
|
||||||
import { WebView } from "react-native-webview";
|
import { WebView } from "react-native-webview";
|
||||||
|
|
||||||
@@ -8,35 +9,65 @@ import {
|
|||||||
lineListPair,
|
lineListPair,
|
||||||
stationIDPair,
|
stationIDPair,
|
||||||
stationNamePair,
|
stationNamePair,
|
||||||
} from "../../lib/getStationList";
|
} from "@/lib/getStationList";
|
||||||
import { checkDuplicateTrainData } from "../../lib/checkDuplicateTrainData";
|
import { checkDuplicateTrainData } from "@/lib/checkDuplicateTrainData";
|
||||||
import { useFavoriteStation } from "../../stateBox/useFavoriteStation";
|
import { useFavoriteStation } from "@/stateBox/useFavoriteStation";
|
||||||
import {
|
import {
|
||||||
useCurrentTrainData,
|
useCurrentTrainData,
|
||||||
useCurrentTrainView,
|
useCurrentTrainView,
|
||||||
} from "../../stateBox/useCurrentTrain";
|
useServiceStates,
|
||||||
import { useDeviceOrientationChange } from "../../stateBox/useDeviceOrientationChange";
|
} from "@/stateBox/useCurrentTrain";
|
||||||
|
import { useDeviceOrientationChange } from "@/stateBox/useDeviceOrientationChange";
|
||||||
import { SheetManager } from "react-native-actions-sheet";
|
import { SheetManager } from "react-native-actions-sheet";
|
||||||
|
|
||||||
import { useNavigation, useIsFocused } from "@react-navigation/native";
|
import { useNavigation, useIsFocused } from "@react-navigation/native";
|
||||||
import { useTrainMenu } from "../../stateBox/useTrainMenu";
|
import { useTrainMenu } from "@/stateBox/useTrainMenu";
|
||||||
import { useStationList } from "../../stateBox/useStationList";
|
import {
|
||||||
|
useCustomTrainData,
|
||||||
|
useOperationLogs,
|
||||||
|
useTrainDiagram,
|
||||||
|
} from "@/stateBox/useAllTrainDiagram";
|
||||||
|
import { useStationList } from "@/stateBox/useStationList";
|
||||||
import { useThemeColors } from "@/lib/theme";
|
import { useThemeColors } from "@/lib/theme";
|
||||||
import { useWebViewRemount } from "@/lib/useWebViewRemount";
|
import { useWebViewRemount } from "@/lib/useWebViewRemount";
|
||||||
import { generateMockUpdateScript } from "../../lib/mockApi/webviewXhrInterceptor";
|
import { generateMockUpdateScript } from "@/lib/mockApi/webviewXhrInterceptor";
|
||||||
import { telemetry } from "@/lib/observability/telemetry";
|
import { telemetry } from "@/lib/observability/telemetry";
|
||||||
import { setAppLifecycleWebViewActive } from "@/lib/observability/appLifecycleCrashSentinel";
|
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");
|
const positionsWebViewTelemetry = telemetry.scope("positions.webview");
|
||||||
export const AppsWebView = ({ openStationACFromEachTrainInfo, onInitialLoadReady }) => {
|
export const AppsWebView = ({ openStationACFromEachTrainInfo, onInitialLoadReady }) => {
|
||||||
|
markStartupOnce("positions-webview-render", "positions WebView render");
|
||||||
const initialPositionsUrl = "https://train.jr-shikoku.co.jp/sp.html";
|
const initialPositionsUrl = "https://train.jr-shikoku.co.jp/sp.html";
|
||||||
const { currentTrain } = useCurrentTrainData();
|
|
||||||
const { webview } = useCurrentTrainView();
|
const { webview } = useCurrentTrainView();
|
||||||
|
const { currentTrain } = useCurrentTrainData();
|
||||||
|
const { serviceStates } = useServiceStates();
|
||||||
|
const serviceMessage = useMemo(() => serviceStatesMessage(serviceStates), [serviceStates]);
|
||||||
|
const latestServiceMessageRef = useRef(serviceMessage);
|
||||||
|
latestServiceMessageRef.current = serviceMessage;
|
||||||
|
const serviceBridgeReadyRef = useRef(false);
|
||||||
|
const sendServiceStates = useCallback((reason: string) => {
|
||||||
|
if (!webview?.current || !serviceBridgeReadyRef.current) return;
|
||||||
|
const message = latestServiceMessageRef.current;
|
||||||
|
webview.current.injectJavaScript(message.script);
|
||||||
|
console.info("[service-state] WebView send", { reason, utf8Bytes: message.bytes });
|
||||||
|
}, [webview]);
|
||||||
const { navigate } = useNavigation();
|
const { navigate } = useNavigation();
|
||||||
const isFocused = useIsFocused();
|
const isFocused = useIsFocused();
|
||||||
const { favoriteStation } = useFavoriteStation();
|
const { favoriteStation } = useFavoriteStation();
|
||||||
const { isLandscape } = useDeviceOrientationChange();
|
const { isLandscape } = useDeviceOrientationChange();
|
||||||
const { isDark } = useThemeColors();
|
const { isDark } = useThemeColors();
|
||||||
|
const { notifyWebViewLoadStarted } = usePositionsStartupReady();
|
||||||
const bgColor = isDark ? "#1c1c1e" : "#ffffff";
|
const bgColor = isDark ? "#1c1c1e" : "#ffffff";
|
||||||
const { originalStationList, stationList, getInjectJavascriptAddress } =
|
const { originalStationList, stationList, getInjectJavascriptAddress } =
|
||||||
useStationList();
|
useStationList();
|
||||||
@@ -49,6 +80,64 @@ export const AppsWebView = ({ openStationACFromEachTrainInfo, onInitialLoadReady
|
|||||||
mockApiFeatureEnabled,
|
mockApiFeatureEnabled,
|
||||||
mockTrainPositions,
|
mockTrainPositions,
|
||||||
} = useTrainMenu();
|
} = 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(
|
||||||
|
() =>
|
||||||
|
[serviceStateDecorationScript, nativeBootstrapScriptRef.current, injectJavascriptBeforeContentLoaded]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join("\n"),
|
||||||
|
[injectJavascriptBeforeContentLoaded],
|
||||||
|
);
|
||||||
const addWebViewBreadcrumb = (
|
const addWebViewBreadcrumb = (
|
||||||
message: string,
|
message: string,
|
||||||
data?: Record<string, string | number | boolean | null | undefined>
|
data?: Record<string, string | number | boolean | null | undefined>
|
||||||
@@ -71,22 +160,13 @@ export const AppsWebView = ({ openStationACFromEachTrainInfo, onInitialLoadReady
|
|||||||
reason,
|
reason,
|
||||||
...(data ?? {}),
|
...(data ?? {}),
|
||||||
});
|
});
|
||||||
telemetry.message("positions.webview.remount_requested", {
|
telemetry.log("warn", "positions.webview.remount_requested", {
|
||||||
level: "warning",
|
area: "positions_webview",
|
||||||
tags: {
|
reason,
|
||||||
area: "positions_webview",
|
watchdogMode,
|
||||||
platform: Platform.OS,
|
focused: isFocused,
|
||||||
reason,
|
landscape: isLandscape,
|
||||||
watchdogMode,
|
...(data ?? {}),
|
||||||
},
|
|
||||||
contexts: {
|
|
||||||
positions_webview_remount: {
|
|
||||||
focused: isFocused,
|
|
||||||
landscape: isLandscape,
|
|
||||||
...(data ?? {}),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
fingerprint: ["positions_webview_remount", reason],
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onWatchdog: (reason, data) => {
|
onWatchdog: (reason, data) => {
|
||||||
@@ -95,28 +175,23 @@ export const AppsWebView = ({ openStationACFromEachTrainInfo, onInitialLoadReady
|
|||||||
watchdogMode,
|
watchdogMode,
|
||||||
...(data ?? {}),
|
...(data ?? {}),
|
||||||
});
|
});
|
||||||
telemetry.message("positions.webview.watchdog_detected", {
|
telemetry.log("warn", "positions.webview.watchdog_detected", {
|
||||||
level: "warning",
|
area: "positions_webview",
|
||||||
tags: {
|
reason,
|
||||||
area: "positions_webview",
|
watchdogMode,
|
||||||
platform: Platform.OS,
|
focused: isFocused,
|
||||||
reason,
|
landscape: isLandscape,
|
||||||
watchdogMode,
|
...(data ?? {}),
|
||||||
},
|
|
||||||
contexts: {
|
|
||||||
positions_webview_watchdog: {
|
|
||||||
focused: isFocused,
|
|
||||||
landscape: isLandscape,
|
|
||||||
...(data ?? {}),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
fingerprint: ["positions_webview_watchdog", reason],
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const lastRemountKeyRef = useRef<typeof remountKey | null>(null);
|
const lastRemountKeyRef = useRef<typeof remountKey | null>(null);
|
||||||
|
useEffect(() => {
|
||||||
|
sendServiceStates("state-update");
|
||||||
|
}, [serviceMessage.script, sendServiceStates]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
markStartupOnce("positions-webview-mounted", "positions WebView mounted");
|
||||||
setAppLifecycleWebViewActive("positions_main", true);
|
setAppLifecycleWebViewActive("positions_main", true);
|
||||||
return () => setAppLifecycleWebViewActive("positions_main", false);
|
return () => setAppLifecycleWebViewActive("positions_main", false);
|
||||||
}, []);
|
}, []);
|
||||||
@@ -145,6 +220,9 @@ export const AppsWebView = ({ openStationACFromEachTrainInfo, onInitialLoadReady
|
|||||||
}, [mockApiFeatureEnabled, remount]);
|
}, [mockApiFeatureEnabled, remount]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
markStartupState("positions-webview-focus", "positions WebView focus state", {
|
||||||
|
focused: isFocused,
|
||||||
|
});
|
||||||
focusedRef.current = isFocused;
|
focusedRef.current = isFocused;
|
||||||
addWebViewBreadcrumb(isFocused ? "webview focused" : "webview blurred", {
|
addWebViewBreadcrumb(isFocused ? "webview focused" : "webview blurred", {
|
||||||
landscape: isLandscape,
|
landscape: isLandscape,
|
||||||
@@ -162,6 +240,7 @@ export const AppsWebView = ({ openStationACFromEachTrainInfo, onInitialLoadReady
|
|||||||
if (lastRemountKeyRef.current !== remountKey) {
|
if (lastRemountKeyRef.current !== remountKey) {
|
||||||
addWebViewBreadcrumb("webview remount key", { remountKey });
|
addWebViewBreadcrumb("webview remount key", { remountKey });
|
||||||
lastRemountKeyRef.current = remountKey;
|
lastRemountKeyRef.current = remountKey;
|
||||||
|
serviceBridgeReadyRef.current = false;
|
||||||
initialInjectDoneRef.current = false;
|
initialInjectDoneRef.current = false;
|
||||||
pendingInitialInjectRef.current = null;
|
pendingInitialInjectRef.current = null;
|
||||||
initialLoadReadyNotifiedRef.current = false;
|
initialLoadReadyNotifiedRef.current = false;
|
||||||
@@ -225,9 +304,8 @@ export const AppsWebView = ({ openStationACFromEachTrainInfo, onInitialLoadReady
|
|||||||
addWebViewBreadcrumb("webview navigation", { url });
|
addWebViewBreadcrumb("webview navigation", { url });
|
||||||
switch (true) {
|
switch (true) {
|
||||||
case url.includes("https://train.jr-shikoku.co.jp/usage.htm"):
|
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();
|
webview?.current.goBack();
|
||||||
//Actions.howto();
|
|
||||||
break;
|
break;
|
||||||
case url.includes("https://train.jr-shikoku.co.jp/train.html"):
|
case url.includes("https://train.jr-shikoku.co.jp/train.html"):
|
||||||
//Actions.trainbase({info: url});
|
//Actions.trainbase({info: url});
|
||||||
@@ -332,6 +410,11 @@ export const AppsWebView = ({ openStationACFromEachTrainInfo, onInitialLoadReady
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onLoadStart = () => {
|
const onLoadStart = () => {
|
||||||
|
serviceBridgeReadyRef.current = false;
|
||||||
|
notifyWebViewLoadStarted();
|
||||||
|
markStartup("positions WebView loadStart", {
|
||||||
|
focused: focusedRef.current,
|
||||||
|
});
|
||||||
pingHandlers.onLoadStart?.();
|
pingHandlers.onLoadStart?.();
|
||||||
addWebViewBreadcrumb("webview loadStart", {
|
addWebViewBreadcrumb("webview loadStart", {
|
||||||
focused: focusedRef.current,
|
focused: focusedRef.current,
|
||||||
@@ -340,6 +423,11 @@ export const AppsWebView = ({ openStationACFromEachTrainInfo, onInitialLoadReady
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onLoadEnd = () => {
|
const onLoadEnd = () => {
|
||||||
|
serviceBridgeReadyRef.current = true;
|
||||||
|
sendServiceStates("load-end");
|
||||||
|
markStartup("positions WebView loadEnd", {
|
||||||
|
focused: focusedRef.current,
|
||||||
|
});
|
||||||
pingHandlers.onLoadEnd?.();
|
pingHandlers.onLoadEnd?.();
|
||||||
addWebViewBreadcrumb("webview loadEnd", {
|
addWebViewBreadcrumb("webview loadEnd", {
|
||||||
focused: focusedRef.current,
|
focused: focusedRef.current,
|
||||||
@@ -349,6 +437,11 @@ export const AppsWebView = ({ openStationACFromEachTrainInfo, onInitialLoadReady
|
|||||||
});
|
});
|
||||||
if (!initialLoadReadyNotifiedRef.current) {
|
if (!initialLoadReadyNotifiedRef.current) {
|
||||||
initialLoadReadyNotifiedRef.current = true;
|
initialLoadReadyNotifiedRef.current = true;
|
||||||
|
markStartupOnce(
|
||||||
|
"positions-webview-initial-ready",
|
||||||
|
"positions WebView initial load ready",
|
||||||
|
{ focused: focusedRef.current },
|
||||||
|
);
|
||||||
addWebViewBreadcrumb("webview initial load ready", {
|
addWebViewBreadcrumb("webview initial load ready", {
|
||||||
focused: focusedRef.current,
|
focused: focusedRef.current,
|
||||||
});
|
});
|
||||||
@@ -368,6 +461,10 @@ export const AppsWebView = ({ openStationACFromEachTrainInfo, onInitialLoadReady
|
|||||||
addWebViewBreadcrumb("webview initial inject scheduled", {
|
addWebViewBreadcrumb("webview initial inject scheduled", {
|
||||||
focused: focusedRef.current,
|
focused: focusedRef.current,
|
||||||
});
|
});
|
||||||
|
markStartup("positions WebView initial inject scheduled", {
|
||||||
|
focused: focusedRef.current,
|
||||||
|
favoriteCount: favoriteStation.length,
|
||||||
|
});
|
||||||
loadEndTimeoutRef.current = setTimeout(() => {
|
loadEndTimeoutRef.current = setTimeout(() => {
|
||||||
addWebViewBreadcrumb(
|
addWebViewBreadcrumb(
|
||||||
focusedRef.current
|
focusedRef.current
|
||||||
@@ -377,6 +474,10 @@ export const AppsWebView = ({ openStationACFromEachTrainInfo, onInitialLoadReady
|
|||||||
focused: focusedRef.current,
|
focused: focusedRef.current,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
markStartup("positions WebView initial inject run", {
|
||||||
|
focused: focusedRef.current,
|
||||||
|
favoriteCount: favoriteStation.length,
|
||||||
|
});
|
||||||
webview?.current?.injectJavaScript(string);
|
webview?.current?.injectJavaScript(string);
|
||||||
pendingInitialInjectRef.current = null;
|
pendingInitialInjectRef.current = null;
|
||||||
initialInjectDoneRef.current = true;
|
initialInjectDoneRef.current = true;
|
||||||
@@ -426,7 +527,7 @@ export const AppsWebView = ({ openStationACFromEachTrainInfo, onInitialLoadReady
|
|||||||
onRenderProcessGone={handleRenderProcessGone}
|
onRenderProcessGone={handleRenderProcessGone}
|
||||||
onContentProcessDidTerminate={handleContentProcessDidTerminate}
|
onContentProcessDidTerminate={handleContentProcessDidTerminate}
|
||||||
{...{ onMessage, onNavigationStateChange, onLoadEnd }}
|
{...{ onMessage, onNavigationStateChange, onLoadEnd }}
|
||||||
injectedJavaScriptBeforeContentLoaded={injectJavascriptBeforeContentLoaded}
|
injectedJavaScriptBeforeContentLoaded={webViewBeforeContentLoaded}
|
||||||
injectedJavaScript={injectJavascript}
|
injectedJavaScript={injectJavascript}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -156,6 +156,7 @@ export const DynamicHeaderScrollView: React.FC<DynamicHeaderScrollViewProps> = (
|
|||||||
<View {...containerProps}>
|
<View {...containerProps}>
|
||||||
<View style={{ zIndex: 1 }}>
|
<View style={{ zIndex: 1 }}>
|
||||||
<Animated.View
|
<Animated.View
|
||||||
|
pointerEvents={headerVisible ? "auto" : "none"}
|
||||||
style={[
|
style={[
|
||||||
styles.header,
|
styles.header,
|
||||||
headerVisible ? shotHeaderStyle.on : shotHeaderStyle.off,
|
headerVisible ? shotHeaderStyle.on : shotHeaderStyle.off,
|
||||||
@@ -164,6 +165,7 @@ export const DynamicHeaderScrollView: React.FC<DynamicHeaderScrollViewProps> = (
|
|||||||
{viewHeader ? <></> : shortHeader}
|
{viewHeader ? <></> : shortHeader}
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
<Animated.View
|
<Animated.View
|
||||||
|
pointerEvents={headerVisible ? "none" : "auto"}
|
||||||
style={[
|
style={[
|
||||||
styles.header,
|
styles.header,
|
||||||
headerVisible ? longHeaderStyle.off : longHeaderStyle.on,
|
headerVisible ? longHeaderStyle.off : longHeaderStyle.on,
|
||||||
@@ -175,27 +177,27 @@ export const DynamicHeaderScrollView: React.FC<DynamicHeaderScrollViewProps> = (
|
|||||||
{topStickyContent}
|
{topStickyContent}
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
</View>
|
</View>
|
||||||
<ScrollView
|
<ScrollView
|
||||||
ref={scrollRef}
|
ref={scrollRef}
|
||||||
nestedScrollEnabled
|
nestedScrollEnabled
|
||||||
bounces={false}
|
bounces={false}
|
||||||
style={{ zIndex: 0 }}
|
style={{ zIndex: 0 }}
|
||||||
stickyHeaderIndices={[1]}
|
stickyHeaderIndices={[1]}
|
||||||
scrollEventThrottle={1}
|
scrollEventThrottle={1}
|
||||||
onScroll={onScroll}
|
onScroll={onScroll}
|
||||||
>
|
>
|
||||||
<View style={{ height: Scroll_Distance, flexDirection: "column" }} />
|
<View style={{ height: Scroll_Distance, flexDirection: "column" }} />
|
||||||
{topStickyContent && (
|
{topStickyContent && (
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
paddingTop: Min_Header_Height + verticalScale(40),
|
paddingTop: Min_Header_Height + verticalScale(40),
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
}}
|
}}
|
||||||
//index={1}
|
//index={1}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{children}
|
{children}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import type { NavigateFunction } from "@/types";
|
|||||||
import { telemetry } from "@/lib/observability/telemetry";
|
import { telemetry } from "@/lib/observability/telemetry";
|
||||||
import { FavoriteEmptyState } from "./FavoriteEmptyState";
|
import { FavoriteEmptyState } from "./FavoriteEmptyState";
|
||||||
import { StationListLoadingState } from "./StationListLoadingState";
|
import { StationListLoadingState } from "./StationListLoadingState";
|
||||||
|
import { markStartupOnce } from "@/lib/devStartupPerformance";
|
||||||
|
|
||||||
const menuCarouselTelemetry = telemetry.scope("menu.carousel");
|
const menuCarouselTelemetry = telemetry.scope("menu.carousel");
|
||||||
|
|
||||||
@@ -78,6 +79,11 @@ export const CarouselBox = ({
|
|||||||
isPositionLoading: boolean;
|
isPositionLoading: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const isAndroid = Platform.OS === "android";
|
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 sortControlsEntering = isAndroid ? undefined : FadeIn.duration(200);
|
||||||
const sortControlsExiting = isAndroid ? undefined : FadeOut.duration(150);
|
const sortControlsExiting = isAndroid ? undefined : FadeOut.duration(150);
|
||||||
const carouselRef = useRef<ICarouselInstance>(null);
|
const carouselRef = useRef<ICarouselInstance>(null);
|
||||||
@@ -253,11 +259,9 @@ export const CarouselBox = ({
|
|||||||
currentStation: listUpStation[listIndex],
|
currentStation: listUpStation[listIndex],
|
||||||
navigate,
|
navigate,
|
||||||
goTo: "menu",
|
goTo: "menu",
|
||||||
//@ts-ignore
|
|
||||||
useShow: () => SheetManager.show("StationDetailView", { payload }),
|
useShow: () => SheetManager.show("StationDetailView", { payload }),
|
||||||
onExit: () => SheetManager.hide("StationDetailView"),
|
onExit: () => SheetManager.hide("StationDetailView"),
|
||||||
};
|
};
|
||||||
//@ts-ignore
|
|
||||||
SheetManager.show("StationDetailView", { payload });
|
SheetManager.show("StationDetailView", { payload });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Linking, Text, View, TouchableOpacity } from "react-native";
|
import { Text, View, TouchableOpacity } from "react-native";
|
||||||
import {
|
import {
|
||||||
Foundation,
|
Foundation,
|
||||||
FontAwesome,
|
FontAwesome,
|
||||||
@@ -17,6 +17,7 @@ import { useNotification } from "@/stateBox/useNotifications";
|
|||||||
import { useThemeColors } from "@/lib/theme";
|
import { useThemeColors } from "@/lib/theme";
|
||||||
import { useResponsive } from "@/lib/responsive";
|
import { useResponsive } from "@/lib/responsive";
|
||||||
import { isAvailable as isFelicaAvailable } from "@/modules/expo-felica-reader/src";
|
import { isAvailable as isFelicaAvailable } from "@/modules/expo-felica-reader/src";
|
||||||
|
import { openExternalUrl } from "@/lib/openExternalUrl";
|
||||||
|
|
||||||
export const FixedContentBottom = (props) => {
|
export const FixedContentBottom = (props) => {
|
||||||
const { expoPushToken } = useNotification();
|
const { expoPushToken } = useNotification();
|
||||||
@@ -33,7 +34,7 @@ export const FixedContentBottom = (props) => {
|
|||||||
icon={<Foundation name="ticket" color="white" size={moderateScale(50)} />}
|
icon={<Foundation name="ticket" color="white" size={moderateScale(50)} />}
|
||||||
flex={1}
|
flex={1}
|
||||||
onPressButton={() =>
|
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)} />}
|
icon={<FontAwesome name="first-order" color="white" size={moderateScale(50)} />}
|
||||||
flex={1}
|
flex={1}
|
||||||
onPressButton={() =>
|
onPressButton={() =>
|
||||||
Linking.openURL(
|
openExternalUrl(
|
||||||
"https://www.jr-shikoku.co.jp/01_trainbus/event_train/sp/",
|
"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)} />}
|
icon={<Ionicons name="flag" color="white" size={moderateScale(50)} />}
|
||||||
flex={1}
|
flex={1}
|
||||||
onPressButton={() =>
|
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"
|
backgroundColor="#ed86b5"
|
||||||
flex={1}
|
flex={1}
|
||||||
onPressButton={() =>
|
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) }}>
|
<Text style={{ color: "white", fontWeight: "bold", fontSize: fontScale(20) }}>
|
||||||
@@ -111,7 +112,7 @@ export const FixedContentBottom = (props) => {
|
|||||||
backgroundColor={fixed.primary}
|
backgroundColor={fixed.primary}
|
||||||
flex={1}
|
flex={1}
|
||||||
onPressButton={() =>
|
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) }}>
|
<Text style={{ color: fixed.textOnPrimary, fontWeight: "bold", fontSize: fontScale(20) }}>
|
||||||
@@ -125,7 +126,7 @@ export const FixedContentBottom = (props) => {
|
|||||||
backgroundColor={fixed.primary}
|
backgroundColor={fixed.primary}
|
||||||
flex={1}
|
flex={1}
|
||||||
onPressButton={() =>
|
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) }}>
|
<Text style={{ color: fixed.textOnPrimary, fontWeight: "bold", fontSize: fontScale(20) }}>
|
||||||
@@ -146,7 +147,7 @@ export const FixedContentBottom = (props) => {
|
|||||||
margin: 2,
|
margin: 2,
|
||||||
alignItems: "center",
|
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) }}>
|
<Text style={{ color: "white", fontWeight: "bold", fontSize: fontScale(20) }}>
|
||||||
時刻・運賃計算
|
時刻・運賃計算
|
||||||
@@ -294,7 +295,7 @@ export const FixedContentBottom = (props) => {
|
|||||||
backgroundColor="rgb(88, 101, 242)"
|
backgroundColor="rgb(88, 101, 242)"
|
||||||
flex={1}
|
flex={1}
|
||||||
onPressButton={() =>
|
onPressButton={() =>
|
||||||
Linking.openURL(
|
openExternalUrl(
|
||||||
"https://twitter.com/Xprocess_main/status/1955242437817012300",
|
"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%)"
|
backgroundColor="linear-gradient(120deg, rgba(247,135,54,0.208) 0%, rgba(54,125,247,0.208) 100%)"
|
||||||
flex={1}
|
flex={1}
|
||||||
onPressButton={() =>
|
onPressButton={() =>
|
||||||
props.navigate("howto", {
|
props.navigate("generalWebView", {
|
||||||
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/",
|
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/",
|
||||||
goTo: "menu",
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React from "react";
|
import React, { useEffect } from "react";
|
||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
Text,
|
Text,
|
||||||
@@ -19,6 +19,11 @@ export const JRSTraInfoBox = () => {
|
|||||||
useTrainDelayData();
|
useTrainDelayData();
|
||||||
const { colors, fixed } = useThemeColors();
|
const { colors, fixed } = useThemeColors();
|
||||||
const { fontScale, moderateScale, verticalScale } = useResponsive();
|
const { fontScale, moderateScale, verticalScale } = useResponsive();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void refreshDelayData();
|
||||||
|
}, [refreshDelayData]);
|
||||||
|
|
||||||
const styles: { [key: string]: StyleProp<ViewStyle> } = {
|
const styles: { [key: string]: StyleProp<ViewStyle> } = {
|
||||||
touch: {
|
touch: {
|
||||||
backgroundColor: fixed.primary,
|
backgroundColor: fixed.primary,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { useThemeColors } from "@/lib/theme";
|
|||||||
import type { StationProps } from "@/lib/CommonTypes";
|
import type { StationProps } from "@/lib/CommonTypes";
|
||||||
import { LED_vision } from "@/components/発車時刻表/LED_vidion";
|
import { LED_vision } from "@/components/発車時刻表/LED_vidion";
|
||||||
import { LEDFrame } from "@/components/発車時刻表/LED_Vision_Component/LEDFrame";
|
import { LEDFrame } from "@/components/発車時刻表/LED_Vision_Component/LEDFrame";
|
||||||
|
import { markStartupOnce } from "@/lib/devStartupPerformance";
|
||||||
|
|
||||||
type MenuLEDProps = {
|
type MenuLEDProps = {
|
||||||
station: StationProps[];
|
station: StationProps[];
|
||||||
@@ -39,6 +40,10 @@ const MenuLEDComponent = ({ station }: MenuLEDProps) => {
|
|||||||
const activeStation =
|
const activeStation =
|
||||||
preparedLED.key === stationKey ? preparedLED.station : null;
|
preparedLED.key === stationKey ? preparedLED.station : null;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
markStartupOnce("menu-led-mounted", "menu LED mounted");
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (preparedLED.key === stationKey) return;
|
if (preparedLED.key === stationKey) return;
|
||||||
|
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export const NewsReleaseInfoBox: FC<navigateProps> = ({ navigate }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onPressItem = (d: newsDataType) => {
|
const onPressItem = (d: newsDataType) => {
|
||||||
navigate("howto", { info: getPDFViewURL(d.url), goTo: "menu" });
|
navigate("generalWebView", { uri: getPDFViewURL(d.url) });
|
||||||
SheetManager.hide("NewsReleaseInfo");
|
SheetManager.hide("NewsReleaseInfo");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -29,9 +29,8 @@ export const SpecialTrainInfoBox: FC<props> = ({ navigate }) => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const onPressItem: (d: specialDataType) => void = (d) => {
|
const onPressItem: (d: specialDataType) => void = (d) => {
|
||||||
navigate("howto", {
|
navigate("generalWebView", {
|
||||||
info: getPDFViewURL("https://www.jr-shikoku.co.jp" + d.address),
|
uri: getPDFViewURL("https://www.jr-shikoku.co.jp" + d.address),
|
||||||
goTo: "menu",
|
|
||||||
});
|
});
|
||||||
SheetManager.hide("SpecialTrainInfo");
|
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 { useThemeColors } from "@/lib/theme";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
import { useResponsive } from "@/lib/responsive";
|
import { useResponsive } from "@/lib/responsive";
|
||||||
|
import { openExternalUrl } from "@/lib/openExternalUrl";
|
||||||
|
|
||||||
|
|
||||||
export const TitleBar = () => {
|
export const TitleBar = () => {
|
||||||
@@ -22,9 +23,9 @@ export const TitleBar = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TouchableOpacity
|
<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>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Linking, View } from "react-native";
|
import { View } from "react-native";
|
||||||
import { UsefulBox } from "@/components/TrainMenu/UsefulBox";
|
import { UsefulBox } from "@/components/TrainMenu/UsefulBox";
|
||||||
import { MaterialCommunityIcons } from "@expo/vector-icons";
|
import { MaterialCommunityIcons } from "@expo/vector-icons";
|
||||||
|
import { openExternalUrl } from "@/lib/openExternalUrl";
|
||||||
export const TopMenuButton = () => {
|
export const TopMenuButton = () => {
|
||||||
const buttonList:{
|
const buttonList:{
|
||||||
backgroundColor: string;
|
backgroundColor: string;
|
||||||
@@ -13,14 +14,14 @@ export const TopMenuButton = () => {
|
|||||||
backgroundColor: "#F89038",
|
backgroundColor: "#F89038",
|
||||||
icon: "train-car",
|
icon: "train-car",
|
||||||
onPress: () =>
|
onPress: () =>
|
||||||
Linking.openURL("https://www.jr-shikoku.co.jp/01_trainbus/sp/"),
|
openExternalUrl("https://www.jr-shikoku.co.jp/01_trainbus/sp/"),
|
||||||
title: "駅・鉄道情報",
|
title: "駅・鉄道情報",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
backgroundColor: "#EA4752",
|
backgroundColor: "#EA4752",
|
||||||
icon: "google-spreadsheet",
|
icon: "google-spreadsheet",
|
||||||
onPress: () =>
|
onPress: () =>
|
||||||
Linking.openURL(
|
openExternalUrl(
|
||||||
"https://www.jr-shikoku.co.jp/01_trainbus/jikoku/sp/#mainprice-box"
|
"https://www.jr-shikoku.co.jp/01_trainbus/jikoku/sp/#mainprice-box"
|
||||||
),
|
),
|
||||||
title: "運賃表",
|
title: "運賃表",
|
||||||
@@ -28,7 +29,7 @@ export const TopMenuButton = () => {
|
|||||||
{
|
{
|
||||||
backgroundColor: "#91C31F",
|
backgroundColor: "#91C31F",
|
||||||
icon: "clipboard-list-outline",
|
icon: "clipboard-list-outline",
|
||||||
onPress: () => Linking.openURL("https://www.jr-shikoku.co.jp/e5489/"),
|
onPress: () => openExternalUrl("https://www.jr-shikoku.co.jp/e5489/"),
|
||||||
title: "予約",
|
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,
|
StyleSheet,
|
||||||
Image,
|
Image,
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
Linking,
|
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
import { Switch } from "@rneui/themed";
|
import { Switch } from "@rneui/themed";
|
||||||
import { useNavigation } from "@react-navigation/native";
|
import { useNavigation } from "@react-navigation/native";
|
||||||
import { MaterialCommunityIcons } from "@expo/vector-icons";
|
import { MaterialCommunityIcons } from "@expo/vector-icons";
|
||||||
import { SheetHeaderItem } from "@/components/atom/SheetHeaderItem";
|
import { SheetHeaderItem } from "@/components/atom/SheetHeaderItem";
|
||||||
import { AS } from "../../storageControl";
|
import { AS } from "@/storageControl";
|
||||||
import { STORAGE_KEYS } from "@/constants";
|
import { STORAGE_KEYS } from "@/constants";
|
||||||
import { useTrainMenu } from "@/stateBox/useTrainMenu";
|
import { useTrainMenu } from "@/stateBox/useTrainMenu";
|
||||||
import { useThemeColors } from "@/lib/theme";
|
import { useThemeColors } from "@/lib/theme";
|
||||||
@@ -27,6 +26,7 @@ import {
|
|||||||
normalizeJrDataSystemEnvironment,
|
normalizeJrDataSystemEnvironment,
|
||||||
resolveJrDataSystemEnvironment,
|
resolveJrDataSystemEnvironment,
|
||||||
} from "@/lib/jrDataSystemEnvironment";
|
} from "@/lib/jrDataSystemEnvironment";
|
||||||
|
import { openExternalUrl } from "@/lib/openExternalUrl";
|
||||||
|
|
||||||
const HUB_LOGO_PNG = require("@/assets/relationLogo/unyohub_logo.webp");
|
const HUB_LOGO_PNG = require("@/assets/relationLogo/unyohub_logo.webp");
|
||||||
const ELESITE_LOGO_PNG = require("@/assets/relationLogo/elesite_logo.jpg");
|
const ELESITE_LOGO_PNG = require("@/assets/relationLogo/elesite_logo.jpg");
|
||||||
@@ -206,7 +206,7 @@ const DataSourceAccordionCard: React.FC<DataSourceAccordionCardProps> = ({
|
|||||||
styles.bodyLink,
|
styles.bodyLink,
|
||||||
{ borderTopColor: colors.borderSecondary },
|
{ borderTopColor: colors.borderSecondary },
|
||||||
]}
|
]}
|
||||||
onPress={() => Linking.openURL(linkUrl)}
|
onPress={() => openExternalUrl(linkUrl)}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
>
|
>
|
||||||
<MaterialCommunityIcons
|
<MaterialCommunityIcons
|
||||||
@@ -298,12 +298,16 @@ export const DataSourceSettings = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
AS.getItem(STORAGE_KEYS.USE_UNYOHUB).then((value) => {
|
AS.getItem(STORAGE_KEYS.USE_UNYOHUB)
|
||||||
setUseUnyohub(value === true || value === "true");
|
.then((value) => {
|
||||||
});
|
setUseUnyohub(value === true || value === "true");
|
||||||
AS.getItem(STORAGE_KEYS.USE_ELESITE).then((value) => {
|
})
|
||||||
setUseElesite(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)
|
AS.getItem(STORAGE_KEYS.JR_DATA_SYSTEM_ENV)
|
||||||
.then((value) => {
|
.then((value) => {
|
||||||
const env = normalizeJrDataSystemEnvironment(value);
|
const env = normalizeJrDataSystemEnvironment(value);
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import Icon from "react-native-vector-icons/Entypo";
|
import Icon from "react-native-vector-icons/Entypo";
|
||||||
import { View, Text, TouchableOpacity, LayoutAnimation } from "react-native";
|
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 Ionicons from "react-native-vector-icons/Ionicons";
|
||||||
import { AS } from "../../../storageControl";
|
import { AS } from "@/storageControl";
|
||||||
import { useThemeColors } from "@/lib/theme";
|
import { useThemeColors } from "@/lib/theme";
|
||||||
|
|
||||||
export const FavoriteSettingsItem = ({ currentStation }) => {
|
export const FavoriteSettingsItem = ({ currentStation }) => {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { View, Text, StyleSheet } from "react-native";
|
|||||||
import Animated, { useAnimatedRef } from "react-native-reanimated";
|
import Animated, { useAnimatedRef } from "react-native-reanimated";
|
||||||
import { useNavigation } from "@react-navigation/native";
|
import { useNavigation } from "@react-navigation/native";
|
||||||
import Sortable from "react-native-sortables";
|
import Sortable from "react-native-sortables";
|
||||||
import { useFavoriteStation } from "../../stateBox/useFavoriteStation";
|
import { useFavoriteStation } from "@/stateBox/useFavoriteStation";
|
||||||
import { FavoriteSettingsItem } from "./FavoliteSettings/FavoiliteSettingsItem";
|
import { FavoriteSettingsItem } from "./FavoliteSettings/FavoiliteSettingsItem";
|
||||||
import { SheetHeaderItem } from "@/components/atom/SheetHeaderItem";
|
import { SheetHeaderItem } from "@/components/atom/SheetHeaderItem";
|
||||||
import { getFavoriteStationKey } from "@/lib/favoriteStationUtils";
|
import { getFavoriteStationKey } from "@/lib/favoriteStationUtils";
|
||||||
|
|||||||
@@ -15,21 +15,21 @@ import { BigButton } from "../atom/BigButton";
|
|||||||
import { SheetHeaderItem } from "../atom/SheetHeaderItem";
|
import { SheetHeaderItem } from "../atom/SheetHeaderItem";
|
||||||
import { useThemeColors } from "@/lib/theme";
|
import { useThemeColors } from "@/lib/theme";
|
||||||
import dayjs from "dayjs";
|
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 { saveWidgetData } from "@/modules/expo-felica-reader/src";
|
||||||
import type { FelicaCardInfo, FelicaHistoryEntry } from "../../modules/expo-felica-reader/src";
|
import type { FelicaCardInfo, FelicaHistoryEntry } from "@/modules/expo-felica-reader/src";
|
||||||
import { AS } from "../../storageControl";
|
import { AS } from "@/storageControl";
|
||||||
|
|
||||||
// felicaStationMap は巨大 (5900行) なため、遅延ロード
|
// felicaStationMap は巨大 (5900行) なため、遅延ロード
|
||||||
let _lookupFelicaStation: typeof import("../../lib/felicaStationMap").lookupFelicaStation | null = null;
|
let _lookupFelicaStation: typeof import("@/lib/felicaStationMap").lookupFelicaStation | null = null;
|
||||||
async function getLookupFelicaStation() {
|
async function getLookupFelicaStation() {
|
||||||
if (!_lookupFelicaStation) {
|
if (!_lookupFelicaStation) {
|
||||||
const mod = await import("../../lib/felicaStationMap");
|
const mod = await import("@/lib/felicaStationMap");
|
||||||
_lookupFelicaStation = mod.lookupFelicaStation;
|
_lookupFelicaStation = mod.lookupFelicaStation;
|
||||||
}
|
}
|
||||||
return _lookupFelicaStation;
|
return _lookupFelicaStation;
|
||||||
}
|
}
|
||||||
import { STORAGE_KEYS } from "../../constants";
|
import { STORAGE_KEYS } from "@/constants";
|
||||||
import { requestWidgetUpdate } from "react-native-android-widget";
|
import { requestWidgetUpdate } from "react-native-android-widget";
|
||||||
import {
|
import {
|
||||||
FelicaQuickAccessWidget,
|
FelicaQuickAccessWidget,
|
||||||
|
|||||||
@@ -13,10 +13,10 @@ import LottieView from "lottie-react-native";
|
|||||||
import { useNavigation } from "@react-navigation/native";
|
import { useNavigation } from "@react-navigation/native";
|
||||||
import { ListItem } from "@rneui/themed";
|
import { ListItem } from "@rneui/themed";
|
||||||
import * as Updates from "expo-updates";
|
import * as Updates from "expo-updates";
|
||||||
import { AS } from "../../storageControl";
|
import { AS } from "@/storageControl";
|
||||||
import { STORAGE_KEYS } from "@/constants";
|
import { STORAGE_KEYS } from "@/constants";
|
||||||
import icons, { series as defaultSeries } from "../../assets/icons/icons";
|
import icons, { series as defaultSeries } from "@/assets/icons/icons";
|
||||||
import hubIcons, { series as hubSeries } from "../../assets/unyoHubIcons/icons";
|
import hubIcons, { series as hubSeries } from "@/assets/unyoHubIcons/icons";
|
||||||
import { setAlternateAppIcon, getAppIconName } from "expo-alternate-app-icons";
|
import { setAlternateAppIcon, getAppIconName } from "expo-alternate-app-icons";
|
||||||
import { SheetHeaderItem } from "../atom/SheetHeaderItem";
|
import { SheetHeaderItem } from "../atom/SheetHeaderItem";
|
||||||
import { useThemeColors } from "@/lib/theme";
|
import { useThemeColors } from "@/lib/theme";
|
||||||
@@ -362,7 +362,7 @@ export const LauncherIconSettings = () => {
|
|||||||
{!ready ? (
|
{!ready ? (
|
||||||
<View style={{ flex: 1, alignItems: "center", justifyContent: "center", backgroundColor: colors.background }}>
|
<View style={{ flex: 1, alignItems: "center", justifyContent: "center", backgroundColor: colors.background }}>
|
||||||
<LottieView
|
<LottieView
|
||||||
source={require("../../assets/51690-loading-diamonds.json")}
|
source={require("@/assets/51690-loading-diamonds.json")}
|
||||||
autoPlay
|
autoPlay
|
||||||
loop
|
loop
|
||||||
style={{ width: 120, height: 120 }}
|
style={{ width: 120, height: 120 }}
|
||||||
|
|||||||
@@ -47,17 +47,17 @@ export const LayoutSettings = ({
|
|||||||
bool={iconSetting}
|
bool={iconSetting}
|
||||||
setBool={setIconSetting}
|
setBool={setIconSetting}
|
||||||
firstItem={{
|
firstItem={{
|
||||||
firstImage: require("../../assets/configuration/icon_default.jpg"),
|
firstImage: require("@/assets/configuration/icon_default.jpg"),
|
||||||
firstText: "本家\n(文字アイコン)",
|
firstText: "本家\n(文字アイコン)",
|
||||||
firstValue: "default",
|
firstValue: "default",
|
||||||
}}
|
}}
|
||||||
secondItem={{
|
secondItem={{
|
||||||
secondImage: require("../../assets/configuration/icon_original.jpg"),
|
secondImage: require("@/assets/configuration/icon_original.jpg"),
|
||||||
secondText: "オリジナル\n(車種アイコン)",
|
secondText: "オリジナル\n(車種アイコン)",
|
||||||
secondValue: "original",
|
secondValue: "original",
|
||||||
}}
|
}}
|
||||||
thirdItem={{
|
thirdItem={{
|
||||||
thirdImage: require("../../assets/configuration/icon_hub.jpg"),
|
thirdImage: require("@/assets/configuration/icon_hub.jpg"),
|
||||||
thirdText: "鉄道運用Hub\n(Hubアイコン)",
|
thirdText: "鉄道運用Hub\n(Hubアイコン)",
|
||||||
thirdValue: "hub",
|
thirdValue: "hub",
|
||||||
}}
|
}}
|
||||||
@@ -67,8 +67,8 @@ export const LayoutSettings = ({
|
|||||||
str="列車表示"
|
str="列車表示"
|
||||||
bool={uiSetting}
|
bool={uiSetting}
|
||||||
setBool={setUiSetting}
|
setBool={setUiSetting}
|
||||||
falseImage={require("../../assets/configuration/layout_default.jpg")}
|
falseImage={require("@/assets/configuration/layout_default.jpg")}
|
||||||
trueImage={require("../../assets/configuration/layout_tokyo.jpg")}
|
trueImage={require("@/assets/configuration/layout_tokyo.jpg")}
|
||||||
falseText={"本家"}
|
falseText={"本家"}
|
||||||
trueText={"オリジナル"}
|
trueText={"オリジナル"}
|
||||||
falseValue="default"
|
falseValue="default"
|
||||||
@@ -78,8 +78,8 @@ export const LayoutSettings = ({
|
|||||||
str="トップメニュー表示"
|
str="トップメニュー表示"
|
||||||
bool={mapSwitch}
|
bool={mapSwitch}
|
||||||
setBool={setMapSwitch}
|
setBool={setMapSwitch}
|
||||||
falseImage={require("../../assets/configuration/menu_default.jpg")}
|
falseImage={require("@/assets/configuration/menu_default.jpg")}
|
||||||
trueImage={require("../../assets/configuration/menu_original.jpg")}
|
trueImage={require("@/assets/configuration/menu_original.jpg")}
|
||||||
falseText={"本家\n(文字&路線リスト)"}
|
falseText={"本家\n(文字&路線リスト)"}
|
||||||
trueText={"オリジナル\n(マップ&お気に入り)"}
|
trueText={"オリジナル\n(マップ&お気に入り)"}
|
||||||
/>
|
/>
|
||||||
@@ -89,8 +89,8 @@ export const LayoutSettings = ({
|
|||||||
setBool={setStationMenu}
|
setBool={setStationMenu}
|
||||||
falseText={"本家\n(文字)"}
|
falseText={"本家\n(文字)"}
|
||||||
trueText={"オリジナル\n(駅名標)"}
|
trueText={"オリジナル\n(駅名標)"}
|
||||||
falseImage={require("../../assets/configuration/station_default.jpg")}
|
falseImage={require("@/assets/configuration/station_default.jpg")}
|
||||||
trueImage={require("../../assets/configuration/station_original.jpg")}
|
trueImage={require("@/assets/configuration/station_original.jpg")}
|
||||||
>
|
>
|
||||||
<SimpleSwitch
|
<SimpleSwitch
|
||||||
bool={usePDFView}
|
bool={usePDFView}
|
||||||
@@ -104,25 +104,25 @@ export const LayoutSettings = ({
|
|||||||
setBool={setTrainMenu}
|
setBool={setTrainMenu}
|
||||||
falseText={"本家"}
|
falseText={"本家"}
|
||||||
trueText={"オリジナル"}
|
trueText={"オリジナル"}
|
||||||
falseImage={require("../../assets/configuration/train_default.jpg")}
|
falseImage={require("@/assets/configuration/train_default.jpg")}
|
||||||
trueImage={require("../../assets/configuration/train_original.jpg")}
|
trueImage={require("@/assets/configuration/train_original.jpg")}
|
||||||
>
|
>
|
||||||
<TripleSwitchArea
|
<TripleSwitchArea
|
||||||
str={"ヘッダーサイズ"}
|
str={"ヘッダーサイズ"}
|
||||||
bool={headerSize}
|
bool={headerSize}
|
||||||
setBool={setHeaderSize}
|
setBool={setHeaderSize}
|
||||||
firstItem={{
|
firstItem={{
|
||||||
firstImage: require("../../assets/configuration/train_original_small.jpg"),
|
firstImage: require("@/assets/configuration/train_original_small.jpg"),
|
||||||
firstText: "小固定",
|
firstText: "小固定",
|
||||||
firstValue: "small",
|
firstValue: "small",
|
||||||
}}
|
}}
|
||||||
secondItem={{
|
secondItem={{
|
||||||
secondImage: require("../../assets/configuration/train_original.jpg"),
|
secondImage: require("@/assets/configuration/train_original.jpg"),
|
||||||
secondText: "既定(可変)",
|
secondText: "既定(可変)",
|
||||||
secondValue: "default",
|
secondValue: "default",
|
||||||
}}
|
}}
|
||||||
thirdItem={{
|
thirdItem={{
|
||||||
thirdImage: require("../../assets/configuration/train_original.jpg"),
|
thirdImage: require("@/assets/configuration/train_original.jpg"),
|
||||||
thirdText: "大固定",
|
thirdText: "大固定",
|
||||||
thirdValue: "big",
|
thirdValue: "big",
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ import { View, Text, TouchableOpacity, ScrollView } from "react-native";
|
|||||||
import * as Clipboard from "expo-clipboard";
|
import * as Clipboard from "expo-clipboard";
|
||||||
|
|
||||||
import { CheckBox } from "@rneui/themed";
|
import { CheckBox } from "@rneui/themed";
|
||||||
import { AS } from "../../storageControl";
|
import { AS } from "@/storageControl";
|
||||||
import { STORAGE_KEYS } from "@/constants";
|
import { STORAGE_KEYS } from "@/constants";
|
||||||
import { useNotification } from "../../stateBox/useNotifications";
|
import { useNotification } from "@/stateBox/useNotifications";
|
||||||
import { useNavigation } from "@react-navigation/native";
|
import { useNavigation } from "@react-navigation/native";
|
||||||
import { SheetHeaderItem } from "../atom/SheetHeaderItem";
|
import { SheetHeaderItem } from "../atom/SheetHeaderItem";
|
||||||
import { useThemeColors } from "@/lib/theme";
|
import { useThemeColors } from "@/lib/theme";
|
||||||
@@ -18,9 +18,9 @@ export const NotificationSettings = () => {
|
|||||||
const [informations, setInformations] = useState(false);
|
const [informations, setInformations] = useState(false);
|
||||||
const [strangeTrain, setStrangeTrain] = useState(false);
|
const [strangeTrain, setStrangeTrain] = useState(false);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
AS.getItem(STORAGE_KEYS.TRA_INFO_EX).then(setTraInfoEX);
|
AS.getItem(STORAGE_KEYS.TRA_INFO_EX).then(setTraInfoEX).catch(() => {});
|
||||||
AS.getItem(STORAGE_KEYS.INFORMATIONS).then(setInformations);
|
AS.getItem(STORAGE_KEYS.INFORMATIONS).then(setInformations).catch(() => {});
|
||||||
AS.getItem(STORAGE_KEYS.STRANGE_TRAIN).then(setStrangeTrain);
|
AS.getItem(STORAGE_KEYS.STRANGE_TRAIN).then(setStrangeTrain).catch(() => {});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const setRegister = () => {
|
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";
|
} from "@/lib/jrDataSystemEnvironment";
|
||||||
import { STORAGE_KEYS } from "@/constants";
|
import { STORAGE_KEYS } from "@/constants";
|
||||||
import { AS } from "@/storageControl";
|
import { AS } from "@/storageControl";
|
||||||
|
import {
|
||||||
|
ERROR_BOUNDARY_TEST_SCOPES,
|
||||||
|
armErrorBoundaryTest,
|
||||||
|
} from "@/lib/errorBoundaryTest";
|
||||||
|
|
||||||
export const ResearchToolsSettings = () => {
|
export const ResearchToolsSettings = () => {
|
||||||
const navigation = useNavigation();
|
const navigation = useNavigation();
|
||||||
@@ -38,7 +42,7 @@ export const ResearchToolsSettings = () => {
|
|||||||
})
|
})
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
}, []);
|
}, []);
|
||||||
const { updatePermission, mockApiFeatureEnabled, setMockApiFeatureEnabled } =
|
const { updatePermission, mockApiFeatureEnabled, setMockApiFeatureEnabled, serviceStateShadowEnabled, setServiceStateShadowEnabled } =
|
||||||
useTrainMenu();
|
useTrainMenu();
|
||||||
const {
|
const {
|
||||||
recorderState,
|
recorderState,
|
||||||
@@ -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 openRecordingDatabase = () => {
|
||||||
const uri = `${backendApiBaseUrl}/recorder?from=eachTrainInfo&userID=${encodeURIComponent(expoPushToken || "")}`;
|
const uri = `${backendApiBaseUrl}/recorder?from=eachTrainInfo&userID=${encodeURIComponent(expoPushToken || "")}`;
|
||||||
const params = {
|
const params = {
|
||||||
@@ -185,6 +204,14 @@ export const ResearchToolsSettings = () => {
|
|||||||
</View>
|
</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.switchRow}>
|
||||||
|
<Text style={{ color: colors.textPrimary }}>区間ごとの編成表示</Text>
|
||||||
|
<Switch value={serviceStateShadowEnabled} onValueChange={setServiceStateShadowEnabled} color={fixed.primary} />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
styles.debugSection,
|
styles.debugSection,
|
||||||
@@ -580,6 +607,49 @@ export const ResearchToolsSettings = () => {
|
|||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</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>
|
</ScrollView>
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
ScrollView,
|
ScrollView,
|
||||||
Linking,
|
|
||||||
Image,
|
Image,
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
import * as Updates from "expo-updates";
|
import * as Updates from "expo-updates";
|
||||||
@@ -13,11 +12,13 @@ import { useNavigation } from "@react-navigation/native";
|
|||||||
import { ListItem } from "@rneui/themed";
|
import { ListItem } from "@rneui/themed";
|
||||||
import TouchableScale from "react-native-touchable-scale";
|
import TouchableScale from "react-native-touchable-scale";
|
||||||
import { SwitchArea } from "../atom/SwitchArea";
|
import { SwitchArea } from "../atom/SwitchArea";
|
||||||
import { useNotification } from "../../stateBox/useNotifications";
|
import { useNotification } from "@/stateBox/useNotifications";
|
||||||
import { SheetHeaderItem } from "@/components/atom/SheetHeaderItem";
|
import { SheetHeaderItem } from "@/components/atom/SheetHeaderItem";
|
||||||
import { useThemeColors, type ColorThemePref } from "@/lib/theme/useThemeColors";
|
import { useThemeColors, type ColorThemePref } from "@/lib/theme/useThemeColors";
|
||||||
import { useTrainMenu } from "@/stateBox/useTrainMenu";
|
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 = ({
|
export const SettingTopPage = ({
|
||||||
testNFC,
|
testNFC,
|
||||||
@@ -42,7 +43,7 @@ export const SettingTopPage = ({
|
|||||||
<View style={{ height: 300, padding: 10 }}>
|
<View style={{ height: 300, padding: 10 }}>
|
||||||
<View style={{ flex: 1 }} />
|
<View style={{ flex: 1 }} />
|
||||||
<Image
|
<Image
|
||||||
source={require("../../assets/Header.png")}
|
source={require("@/assets/Header.png")}
|
||||||
style={{
|
style={{
|
||||||
aspectRatio: 8.08,
|
aspectRatio: 8.08,
|
||||||
height: undefined,
|
height: undefined,
|
||||||
@@ -88,14 +89,15 @@ export const SettingTopPage = ({
|
|||||||
str="起動時に表示する画面"
|
str="起動時に表示する画面"
|
||||||
bool={startPage}
|
bool={startPage}
|
||||||
setBool={setStartPage}
|
setBool={setStartPage}
|
||||||
falseImage={require("../../assets/configuration/showSetting0.jpg")}
|
falseImage={require("@/assets/configuration/showSetting0.jpg")}
|
||||||
trueImage={require("../../assets/configuration/showSetting1.jpg")}
|
trueImage={require("@/assets/configuration/showSetting1.jpg")}
|
||||||
falseText={"リンクメニュー"}
|
falseText={"リンクメニュー"}
|
||||||
trueText={"列車位置情報"}
|
trueText={"列車位置情報"}
|
||||||
/>
|
/>
|
||||||
<ColorThemePicker value={colorTheme} onChange={setColorTheme} />
|
<ColorThemePicker value={colorTheme} onChange={setColorTheme} />
|
||||||
<SettingList
|
<SettingList
|
||||||
string="レイアウト設定"
|
string="レイアウト設定"
|
||||||
|
icon="view-dashboard-outline"
|
||||||
onPress={() =>
|
onPress={() =>
|
||||||
navigation.navigate("setting", { screen: "LayoutSettings" })
|
navigation.navigate("setting", { screen: "LayoutSettings" })
|
||||||
}
|
}
|
||||||
@@ -104,24 +106,28 @@ export const SettingTopPage = ({
|
|||||||
<SectionHeader title="カスタマイズ" />
|
<SectionHeader title="カスタマイズ" />
|
||||||
<SettingList
|
<SettingList
|
||||||
string="お気に入り登録の並び替え"
|
string="お気に入り登録の並び替え"
|
||||||
|
icon="sort-ascending"
|
||||||
onPress={() =>
|
onPress={() =>
|
||||||
navigation.navigate("setting", { screen: "FavoriteSettings" })
|
navigation.navigate("setting", { screen: "FavoriteSettings" })
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<SettingList
|
<SettingList
|
||||||
string="アイコン設定"
|
string="アイコン設定"
|
||||||
|
icon="image-multiple-outline"
|
||||||
onPress={() =>
|
onPress={() =>
|
||||||
navigation.navigate("setting", { screen: "LauncherIconSettings" })
|
navigation.navigate("setting", { screen: "LauncherIconSettings" })
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<SettingList
|
<SettingList
|
||||||
string="サウンド設定(β)"
|
string="サウンド設定(β)"
|
||||||
|
icon="volume-high"
|
||||||
onPress={() =>
|
onPress={() =>
|
||||||
navigation.navigate("setting", { screen: "SoundSettings" })
|
navigation.navigate("setting", { screen: "SoundSettings" })
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<SettingList
|
<SettingList
|
||||||
string="運行情報設定(β)"
|
string="運行情報設定(β)"
|
||||||
|
icon="alert-circle-outline"
|
||||||
onPress={() =>
|
onPress={() =>
|
||||||
navigation.navigate("setting", { screen: "OperationInfoSettings" })
|
navigation.navigate("setting", { screen: "OperationInfoSettings" })
|
||||||
}
|
}
|
||||||
@@ -132,6 +138,7 @@ export const SettingTopPage = ({
|
|||||||
string={`通知設定${
|
string={`通知設定${
|
||||||
expoPushToken == "" ? "(通知設定をオンにしてください)" : "(β)"
|
expoPushToken == "" ? "(通知設定をオンにしてください)" : "(β)"
|
||||||
}`}
|
}`}
|
||||||
|
icon="bell-outline"
|
||||||
onPress={() =>
|
onPress={() =>
|
||||||
navigation.navigate("setting", { screen: "NotificationSettings" })
|
navigation.navigate("setting", { screen: "NotificationSettings" })
|
||||||
}
|
}
|
||||||
@@ -139,13 +146,22 @@ export const SettingTopPage = ({
|
|||||||
/>
|
/>
|
||||||
<SettingList
|
<SettingList
|
||||||
string="情報ソース設定"
|
string="情報ソース設定"
|
||||||
|
icon="database-outline"
|
||||||
onPress={() =>
|
onPress={() =>
|
||||||
navigation.navigate("setting", { screen: "DataSourceSettings" })
|
navigation.navigate("setting", { screen: "DataSourceSettings" })
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<SettingList
|
||||||
|
string="接続ステータス"
|
||||||
|
icon="server-network"
|
||||||
|
onPress={() =>
|
||||||
|
navigation.navigate("setting", { screen: "ConnectionStatus" })
|
||||||
|
}
|
||||||
|
/>
|
||||||
{showResearchTools && (
|
{showResearchTools && (
|
||||||
<SettingList
|
<SettingList
|
||||||
string="調査ツール"
|
string="調査ツール"
|
||||||
|
icon="tools"
|
||||||
onPress={() =>
|
onPress={() =>
|
||||||
navigation.navigate("setting", { screen: "ResearchToolsSettings" })
|
navigation.navigate("setting", { screen: "ResearchToolsSettings" })
|
||||||
}
|
}
|
||||||
@@ -155,21 +171,24 @@ export const SettingTopPage = ({
|
|||||||
<SectionHeader title="その他" />
|
<SectionHeader title="その他" />
|
||||||
<SettingList
|
<SettingList
|
||||||
string="プライバシーポリシー"
|
string="プライバシーポリシー"
|
||||||
|
icon="shield-lock-outline"
|
||||||
onPress={() =>
|
onPress={() =>
|
||||||
Linking.openURL(
|
openExternalUrl(
|
||||||
"https://nexcloud.haruk.in/sites/press-harukin/JRShikokuApps/policy"
|
"https://xprocess.haruk.in/JR-shikoku-Apps-Common/NativeAppsPrivacyPolicy"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<SettingList
|
<SettingList
|
||||||
string="開発情報"
|
string="運営Twitter"
|
||||||
onPress={() =>
|
icon="twitter"
|
||||||
Linking.openURL("https://nexcloud.haruk.in/s/2WFEN5oLPGtrRqT")
|
onPress={() => openExternalUrl("https://twitter.com/Xprocess_main")}
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<SettingList
|
<SettingList
|
||||||
string="運営Twitter"
|
string="オープンソースライセンス"
|
||||||
onPress={() => Linking.openURL("https://twitter.com/Xprocess_main")}
|
icon="license"
|
||||||
|
onPress={() =>
|
||||||
|
navigation.navigate("setting", { screen: "OssLicenses" })
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</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();
|
const { colors } = useThemeColors();
|
||||||
return (
|
return (
|
||||||
<ListItem
|
<ListItem
|
||||||
// @ts-ignore: TouchableScale props passed through Component
|
// @ts-expect-error RNEUIのListItem型はComponentに渡したTouchableScaleのpropsを含まない
|
||||||
activeScale={0.95}
|
activeScale={0.95}
|
||||||
Component={TouchableScale}
|
Component={TouchableScale}
|
||||||
bottomDivider
|
bottomDivider
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
// @ts-ignore: containerStyle type mismatch with TouchableScale Component
|
|
||||||
containerStyle={{ backgroundColor: colors.surface }}
|
containerStyle={{ backgroundColor: colors.surface }}
|
||||||
>
|
>
|
||||||
|
{icon ? <MaterialCommunityIcons name={icon} size={24} color={colors.iconSecondary} /> : null}
|
||||||
<ListItem.Content>
|
<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.Content>
|
||||||
<ListItem.Chevron />
|
<ListItem.Chevron />
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
import { Switch } from "@rneui/themed";
|
import { Switch } from "@rneui/themed";
|
||||||
import { useNavigation } from "@react-navigation/native";
|
import { useNavigation } from "@react-navigation/native";
|
||||||
import { SheetHeaderItem } from "@/components/atom/SheetHeaderItem";
|
import { SheetHeaderItem } from "@/components/atom/SheetHeaderItem";
|
||||||
import { AS } from "../../storageControl";
|
import { AS } from "@/storageControl";
|
||||||
import { STORAGE_KEYS } from "@/constants";
|
import { STORAGE_KEYS } from "@/constants";
|
||||||
import { useThemeColors } from "@/lib/theme";
|
import { useThemeColors } from "@/lib/theme";
|
||||||
import { Asset } from "expo-asset";
|
import { Asset } from "expo-asset";
|
||||||
@@ -22,7 +22,7 @@ import {
|
|||||||
import { VoicepeakDebugLogSection } from "@/components/Settings/VoicepeakDebugLogSection";
|
import { VoicepeakDebugLogSection } from "@/components/Settings/VoicepeakDebugLogSection";
|
||||||
import { useTrainMenu } from "@/stateBox/useTrainMenu";
|
import { useTrainMenu } from "@/stateBox/useTrainMenu";
|
||||||
|
|
||||||
const previewSound = require("../../assets/sound/rikka-test.mp3");
|
const previewSound = require("@/assets/sound/rikka-test.mp3");
|
||||||
|
|
||||||
export const SoundSettings = () => {
|
export const SoundSettings = () => {
|
||||||
const { goBack } = useNavigation();
|
const { goBack } = useNavigation();
|
||||||
|
|||||||
@@ -12,9 +12,9 @@ import {
|
|||||||
import type { SettingsStackParamList } from "@/types/navigation";
|
import type { SettingsStackParamList } from "@/types/navigation";
|
||||||
import { createStackNavigator } from "@react-navigation/stack";
|
import { createStackNavigator } from "@react-navigation/stack";
|
||||||
import { pushTransitionOptions } from "@/lib/stackOption";
|
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 Updates from "expo-updates";
|
||||||
import { AS } from "../../storageControl";
|
import { AS } from "@/storageControl";
|
||||||
import { STORAGE_KEYS } from "@/constants";
|
import { STORAGE_KEYS } from "@/constants";
|
||||||
import { Switch } from "@rneui/themed";
|
import { Switch } from "@rneui/themed";
|
||||||
import { SettingTopPage } from "./SettingTopPage";
|
import { SettingTopPage } from "./SettingTopPage";
|
||||||
@@ -28,6 +28,8 @@ import { ResearchToolsSettings } from "./ResearchToolsSettings";
|
|||||||
import { FelicaHistoryPage } from "./FelicaHistoryPage";
|
import { FelicaHistoryPage } from "./FelicaHistoryPage";
|
||||||
import { SoundSettings } from "./SoundSettings";
|
import { SoundSettings } from "./SoundSettings";
|
||||||
import { OperationInfoSettings } from "./OperationInfoSettings";
|
import { OperationInfoSettings } from "./OperationInfoSettings";
|
||||||
|
import { ConnectionStatusSettings } from "./ConnectionStatusSettings";
|
||||||
|
import { OssLicenses } from "./OssLicenses";
|
||||||
import { useTrainMenu } from "@/stateBox/useTrainMenu";
|
import { useTrainMenu } from "@/stateBox/useTrainMenu";
|
||||||
import {
|
import {
|
||||||
normalizeIconDisplayMode,
|
normalizeIconDisplayMode,
|
||||||
@@ -298,6 +300,28 @@ export default function Setting(props) {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Stack.Screen>
|
</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>
|
</Stack.Navigator>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,12 +3,12 @@ import { lightColors } from "@/lib/theme";
|
|||||||
import { MaterialCommunityIcons, Ionicons, FontAwesome5 } from "@expo/vector-icons";
|
import { MaterialCommunityIcons, Ionicons, FontAwesome5 } from "@expo/vector-icons";
|
||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
import {
|
import {
|
||||||
Linking,
|
|
||||||
Text,
|
Text,
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
useWindowDimensions,
|
useWindowDimensions,
|
||||||
View,
|
View,
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
|
import { openExternalUrl } from "@/lib/openExternalUrl";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
item: StationProps[];
|
item: StationProps[];
|
||||||
@@ -129,14 +129,14 @@ export const SpotSign: FC<Props> = ({ item, isCurrentStation }) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TouchableOpacity
|
<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 }}
|
hitSlop={{ top: 8, bottom: 8, left: 12, right: 12 }}
|
||||||
>
|
>
|
||||||
<Ionicons name="map-outline" size={cardHeight * 0.15} color="white" />
|
<Ionicons name="map-outline" size={cardHeight * 0.15} color="white" />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
{item[0].JrHpUrl ? (
|
{item[0].JrHpUrl ? (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={() => Linking.openURL(item[0].JrHpUrl)}
|
onPress={() => openExternalUrl(item[0].JrHpUrl)}
|
||||||
hitSlop={{ top: 8, bottom: 8, left: 12, right: 12 }}
|
hitSlop={{ top: 8, bottom: 8, left: 12, right: 12 }}
|
||||||
>
|
>
|
||||||
<MaterialCommunityIcons name="web" size={cardHeight * 0.15} color="white" />
|
<MaterialCommunityIcons name="web" size={cardHeight * 0.15} color="white" />
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import * as SplashScreen from "expo-splash-screen";
|
import * as SplashScreen from "expo-splash-screen";
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { Platform, StyleSheet } from "react-native";
|
import { StyleSheet } from "react-native";
|
||||||
import Animated, {
|
import Animated, {
|
||||||
Easing,
|
Easing,
|
||||||
useAnimatedStyle,
|
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.
|
// 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.
|
// This must run at module scope so the native splash is not hidden too early.
|
||||||
if (Platform.OS !== "web") {
|
void SplashScreen.preventAutoHideAsync().catch(() => {
|
||||||
void SplashScreen.preventAutoHideAsync().catch(() => {
|
// The native splash may already be hidden during a reload.
|
||||||
// The native splash may already be hidden during a reload.
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function StartupOverlay() {
|
export default function StartupOverlay() {
|
||||||
const reducedMotion = useReducedMotion();
|
const reducedMotion = useReducedMotion();
|
||||||
const opacity = useSharedValue(1);
|
const opacity = useSharedValue(1);
|
||||||
const scale = useSharedValue(1);
|
const scale = useSharedValue(1);
|
||||||
const [visible, setVisible] = useState(Platform.OS !== "web");
|
const [visible, setVisible] = useState(true);
|
||||||
const hasStarted = useRef(false);
|
const hasStarted = useRef(false);
|
||||||
const fallbackTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
const fallbackTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
const animationTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
const animationTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
const animationFrame = useRef<number | null>(null);
|
const animationFrame = useRef<number | null>(null);
|
||||||
|
|
||||||
const finish = useCallback(() => {
|
const finish = useCallback(() => {
|
||||||
if (Platform.OS === "web" || hasStarted.current) return;
|
if (hasStarted.current) return;
|
||||||
|
|
||||||
hasStarted.current = true;
|
hasStarted.current = true;
|
||||||
if (fallbackTimer.current !== null) {
|
if (fallbackTimer.current !== null) {
|
||||||
@@ -80,8 +78,6 @@ export default function StartupOverlay() {
|
|||||||
}, [opacity, reducedMotion, scale]);
|
}, [opacity, reducedMotion, scale]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (Platform.OS === "web") return;
|
|
||||||
|
|
||||||
// A local bundled image normally fires onLoadEnd immediately. The
|
// A local bundled image normally fires onLoadEnd immediately. The
|
||||||
// fallback keeps a broken image from leaving the native splash forever.
|
// fallback keeps a broken image from leaving the native splash forever.
|
||||||
fallbackTimer.current = setTimeout(finish, FALLBACK_DELAY);
|
fallbackTimer.current = setTimeout(finish, FALLBACK_DELAY);
|
||||||
|
|||||||
@@ -101,10 +101,10 @@ export const ExGridSimpleViewItem: FC<{
|
|||||||
const payload = {
|
const payload = {
|
||||||
currentStation: returnDataBase,
|
currentStation: returnDataBase,
|
||||||
navigate,
|
navigate,
|
||||||
//@ts-ignore
|
goTo: "StationDiagram",
|
||||||
useShow: () => SheetManager.show("StationDetailView", { payload }),
|
useShow: () => SheetManager.show("StationDetailView", { payload }),
|
||||||
onExit: () => SheetManager.hide("StationDetailView"),
|
onExit: () => SheetManager.hide("StationDetailView"),
|
||||||
}; //@ts-ignore
|
};
|
||||||
setTimeout(() => SheetManager.show("StationDetailView", { payload }), 50);
|
setTimeout(() => SheetManager.show("StationDetailView", { payload }), 50);
|
||||||
} else {
|
} else {
|
||||||
SheetManager.hide("StationDetailView");
|
SheetManager.hide("StationDetailView");
|
||||||
@@ -128,7 +128,6 @@ export const ExGridSimpleViewItem: FC<{
|
|||||||
from: d.isOperating ? null : "AllTrainIDList",
|
from: d.isOperating ? null : "AllTrainIDList",
|
||||||
};
|
};
|
||||||
SheetManager.show("EachTrainInfo", {
|
SheetManager.show("EachTrainInfo", {
|
||||||
//@ts-ignore
|
|
||||||
payload,
|
payload,
|
||||||
onClose: () => {
|
onClose: () => {
|
||||||
//alert(data);
|
//alert(data);
|
||||||
|
|||||||
@@ -108,10 +108,10 @@ export const ExGridViewItem: FC<{
|
|||||||
const payload = {
|
const payload = {
|
||||||
currentStation: returnDataBase,
|
currentStation: returnDataBase,
|
||||||
navigate,
|
navigate,
|
||||||
//@ts-ignore
|
goTo: "StationDiagram",
|
||||||
useShow: () => SheetManager.show("StationDetailView", { payload }),
|
useShow: () => SheetManager.show("StationDetailView", { payload }),
|
||||||
onExit: () => SheetManager.hide("StationDetailView"),
|
onExit: () => SheetManager.hide("StationDetailView"),
|
||||||
}; //@ts-ignore
|
};
|
||||||
setTimeout(() => SheetManager.show("StationDetailView", { payload }), 50);
|
setTimeout(() => SheetManager.show("StationDetailView", { payload }), 50);
|
||||||
} else {
|
} else {
|
||||||
SheetManager.hide("StationDetailView");
|
SheetManager.hide("StationDetailView");
|
||||||
@@ -132,7 +132,6 @@ export const ExGridViewItem: FC<{
|
|||||||
from: d.isOperating ? null :"AllTrainIDList",
|
from: d.isOperating ? null :"AllTrainIDList",
|
||||||
};
|
};
|
||||||
SheetManager.show("EachTrainInfo", {
|
SheetManager.show("EachTrainInfo", {
|
||||||
//@ts-ignore
|
|
||||||
payload,
|
payload,
|
||||||
onClose: () => {
|
onClose: () => {
|
||||||
//alert(data);
|
//alert(data);
|
||||||
|
|||||||
@@ -181,10 +181,10 @@ export const ListViewItem: FC<{
|
|||||||
const payload = {
|
const payload = {
|
||||||
currentStation: returnDataBase,
|
currentStation: returnDataBase,
|
||||||
navigate,
|
navigate,
|
||||||
//@ts-ignore
|
goTo: "StationDiagram",
|
||||||
useShow: () => SheetManager.show("StationDetailView", { payload }),
|
useShow: () => SheetManager.show("StationDetailView", { payload }),
|
||||||
onExit: () => SheetManager.hide("StationDetailView"),
|
onExit: () => SheetManager.hide("StationDetailView"),
|
||||||
}; //@ts-ignore
|
};
|
||||||
setTimeout(() => SheetManager.show("StationDetailView", { payload }), 50);
|
setTimeout(() => SheetManager.show("StationDetailView", { payload }), 50);
|
||||||
} else {
|
} else {
|
||||||
SheetManager.hide("StationDetailView");
|
SheetManager.hide("StationDetailView");
|
||||||
@@ -205,7 +205,6 @@ export const ListViewItem: FC<{
|
|||||||
from: "AllTrainIDList",
|
from: "AllTrainIDList",
|
||||||
};
|
};
|
||||||
SheetManager.show("EachTrainInfo", {
|
SheetManager.show("EachTrainInfo", {
|
||||||
//@ts-ignore
|
|
||||||
payload,
|
payload,
|
||||||
onClose: () => {
|
onClose: () => {
|
||||||
//alert(data);
|
//alert(data);
|
||||||
|
|||||||
@@ -69,11 +69,16 @@ export const StationDiagramView: FC<props> = ({ route }) => {
|
|||||||
// フィルタリング:終点路線、種別、行先、関係停車駅
|
// フィルタリング:終点路線、種別、行先、関係停車駅
|
||||||
|
|
||||||
const { keyList, allTrainDiagram } = useTrainDiagram();
|
const { keyList, allTrainDiagram } = useTrainDiagram();
|
||||||
const { allCustomTrainData } = useCustomTrainData();
|
const { allCustomTrainData, activateCustomTrainData } = useCustomTrainData();
|
||||||
const { todayOperation } = useOperationLogs();
|
const { todayOperation, activateOperationLogs } = useOperationLogs();
|
||||||
const { useUnyohub: unyohubEnabled } = useUnyohub();
|
const { useUnyohub: unyohubEnabled } = useUnyohub();
|
||||||
const { useElesite: elesiteEnabled } = useElesite();
|
const { useElesite: elesiteEnabled } = useElesite();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
activateCustomTrainData();
|
||||||
|
activateOperationLogs();
|
||||||
|
}, [activateCustomTrainData, activateOperationLogs]);
|
||||||
|
|
||||||
const { goBack } = useNavigation();
|
const { goBack } = useNavigation();
|
||||||
const { colors, fixed } = useThemeColors();
|
const { colors, fixed } = useThemeColors();
|
||||||
const tabBarHeight = useBottomTabBarHeight();
|
const tabBarHeight = useBottomTabBarHeight();
|
||||||
@@ -361,7 +366,7 @@ export const StationDiagramView: FC<props> = ({ route }) => {
|
|||||||
autoPlay
|
autoPlay
|
||||||
loop
|
loop
|
||||||
style={{ width: 80, height: 80 }}
|
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>
|
<Text style={{ color: colors.text, marginTop: 12, fontSize: 14 }}>読み込み中...</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export const MapPin: FC<Props> = (props) => {
|
|||||||
>
|
>
|
||||||
<View style={styles.markerWrap}>
|
<View style={styles.markerWrap}>
|
||||||
<Image
|
<Image
|
||||||
source={require("../../assets/reccha-small.png")}
|
source={require("@/assets/reccha-small.png")}
|
||||||
style={styles.markerImage}
|
style={styles.markerImage}
|
||||||
resizeMode="contain"
|
resizeMode="contain"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { FC } from "react";
|
import React, { FC } from "react";
|
||||||
import { View, Text, TouchableOpacity } from "react-native";
|
import { View, Text, TouchableOpacity } from "react-native";
|
||||||
import { useThemeColors } from "@/lib/theme";
|
import { useThemeColors } from "@/lib/theme";
|
||||||
import lineColorList from "../../assets/originData/lineColorList";
|
import lineColorList from "@/assets/originData/lineColorList";
|
||||||
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export const SimpleSwitch = ({
|
|||||||
setBool,
|
setBool,
|
||||||
color,
|
color,
|
||||||
value,
|
value,
|
||||||
image = require("../../assets/icons.png"),
|
image = require("@/assets/icons.png"),
|
||||||
subText = "",
|
subText = "",
|
||||||
}) => {
|
}) => {
|
||||||
const { colors } = useThemeColors();
|
const { colors } = useThemeColors();
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useRef, useState, useEffect, useLayoutEffect } from "react";
|
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 MapView, { Marker } from "react-native-maps";
|
||||||
import { useCurrentTrainView } from "../stateBox/useCurrentTrain";
|
import { useCurrentTrainView } from "../stateBox/useCurrentTrain";
|
||||||
import { useNavigation } from "@react-navigation/native";
|
import { useNavigation } from "@react-navigation/native";
|
||||||
@@ -13,6 +13,7 @@ import { MapsButton } from "./TrainMenu/MapsButton";
|
|||||||
import { useStationList } from "@/stateBox/useStationList";
|
import { useStationList } from "@/stateBox/useStationList";
|
||||||
import { useThemeColors } from "@/lib/theme";
|
import { useThemeColors } from "@/lib/theme";
|
||||||
import { useUserPosition } from "@/stateBox/useUserPosition";
|
import { useUserPosition } from "@/stateBox/useUserPosition";
|
||||||
|
import { openExternalUrl } from "@/lib/openExternalUrl";
|
||||||
const ANDROID_USER_LOCATION_MARKER_SIZE = 18;
|
const ANDROID_USER_LOCATION_MARKER_SIZE = 18;
|
||||||
const ANDROID_USER_LOCATION_INNER_SIZE = 8;
|
const ANDROID_USER_LOCATION_INNER_SIZE = 8;
|
||||||
|
|
||||||
@@ -223,8 +224,8 @@ export default function TrainMenu({ style }) {
|
|||||||
icon="train-car"
|
icon="train-car"
|
||||||
flex={1}
|
flex={1}
|
||||||
onPressButton={() =>
|
onPressButton={() =>
|
||||||
navigate("howto", {
|
navigate("generalWebView", {
|
||||||
info: "https://xprocess.haruk.in/JR-shikoku-Apps-Common/info/train-position",
|
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"
|
icon="clipboard-list-outline"
|
||||||
flex={1}
|
flex={1}
|
||||||
onPressButton={() =>
|
onPressButton={() =>
|
||||||
Linking.openURL(
|
openExternalUrl(
|
||||||
"https://nexcloud.haruk.in/apps/forms/ZRHjWFF7znr5Xjr2"
|
"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 {
|
import {
|
||||||
useCurrentTrainData,
|
useCurrentTrainData,
|
||||||
useCurrentTrainView,
|
useCurrentTrainView,
|
||||||
} from "../../../stateBox/useCurrentTrain";
|
} from "@/stateBox/useCurrentTrain";
|
||||||
import LottieView from "lottie-react-native";
|
import LottieView from "lottie-react-native";
|
||||||
import { Ionicons, AntDesign } from "@expo/vector-icons";
|
import { Ionicons, AntDesign } from "@expo/vector-icons";
|
||||||
import { useStationList } from "@/stateBox/useStationList";
|
import { useStationList } from "@/stateBox/useStationList";
|
||||||
@@ -43,7 +43,7 @@ export const Header = ({ station }) => {
|
|||||||
autoPlay
|
autoPlay
|
||||||
loop
|
loop
|
||||||
style={{ width: 40, height: 40, marginRight: 30 }}
|
style={{ width: 40, height: 40, marginRight: 30 }}
|
||||||
source={require("../../../assets/51690-loading-diamonds.json")}
|
source={require("@/assets/51690-loading-diamonds.json")}
|
||||||
/>
|
/>
|
||||||
) : currentTrainLoading == "error" ? (
|
) : currentTrainLoading == "error" ? (
|
||||||
<Ionicons
|
<Ionicons
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user