From 045ed21cd7b706957a323de02df3b4cdb2742bb3 Mon Sep 17 00:00:00 2001 From: harukin-expo-dev-env Date: Thu, 2 Apr 2026 15:25:22 +0000 Subject: [PATCH 01/21] =?UTF-8?q?=E5=99=82=E6=A9=9F=E8=83=BD=E3=81=AE?= =?UTF-8?q?=E3=82=B9=E3=82=BF=E3=82=A4=E3=83=AB=E5=BC=B7=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TrainDataSources.tsx | 51 +++++++++++++++---- 1 file changed, 42 insertions(+), 9 deletions(-) diff --git a/components/ActionSheetComponents/TrainDataSources.tsx b/components/ActionSheetComponents/TrainDataSources.tsx index 27f2885..453e44f 100644 --- a/components/ActionSheetComponents/TrainDataSources.tsx +++ b/components/ActionSheetComponents/TrainDataSources.tsx @@ -942,15 +942,28 @@ const TrainInfoDetail: FC<{ {/* うわさ / optional_text */} {(!!uwasa || !!optional_text) && ( - + {!!uwasa && ( - + - {uwasa} + + 噂情報 + {uwasa} + )} {!!optional_text && ( @@ -1302,15 +1315,35 @@ const styles = StyleSheet.create({ fontSize: 11, }, noteSection: { - gap: 4, - borderLeftWidth: 2, - paddingLeft: 8, + gap: 6, + marginTop: 4, + paddingRight: 10, }, noteRow: { flexDirection: "row", alignItems: "flex-start", gap: 4, }, + noteTextWrap: { + flex: 1, + gap: 2, + }, + rumorRow: { + borderWidth: 1, + borderRadius: 8, + paddingHorizontal: 10, + paddingVertical: 8, + gap: 6, + marginVertical: 2, + }, + rumorIcon: { + marginTop: 1, + }, + rumorLabel: { + fontSize: 10, + fontWeight: "700", + letterSpacing: 0.6, + }, noteText: { fontSize: 11, lineHeight: 16, From ad5357ce7f31294e8f54bf20caf81b1d01dedfbf Mon Sep 17 00:00:00 2001 From: harukin-expo-dev-env Date: Fri, 3 Apr 2026 02:07:26 +0000 Subject: [PATCH 02/21] =?UTF-8?q?=E9=81=8B=E7=94=A8Hub=E6=83=85=E5=A0=B1?= =?UTF-8?q?=E3=81=AE=E5=8F=96=E5=BE=97=E3=83=AD=E3=82=B8=E3=83=83=E3=82=AF?= =?UTF-8?q?=E3=82=92=E6=94=B9=E5=96=84=E3=81=97=E3=80=81=E8=B2=A8=E7=89=A9?= =?UTF-8?q?=E5=88=97=E8=BB=8A=E3=81=AE=E8=BB=8A=E7=95=AA=E5=87=A6=E7=90=86?= =?UTF-8?q?=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../EachTrainInfoCore/HeaderText.tsx | 30 +++++++++++++++++-- .../TrainDataSources.tsx | 30 +++++++++++++++---- 2 files changed, 52 insertions(+), 8 deletions(-) diff --git a/components/ActionSheetComponents/EachTrainInfoCore/HeaderText.tsx b/components/ActionSheetComponents/EachTrainInfoCore/HeaderText.tsx index 71532fe..72702f3 100644 --- a/components/ActionSheetComponents/EachTrainInfoCore/HeaderText.tsx +++ b/components/ActionSheetComponents/EachTrainInfoCore/HeaderText.tsx @@ -59,6 +59,7 @@ export const HeaderText: FC = ({ getUnyohubByTrainNumber, getUnyohubEntriesByTrainNumber, useUnyohub: unyohubEnabled, + unyohubData, } = useUnyohub(); const { getElesiteEntriesByTrainNumber, useElesite: elesiteEnabled } = useElesite(); @@ -171,11 +172,34 @@ export const HeaderText: FC = ({ } const unyohubLookupNum = customTrainData?.train_number_override || trainNum; + const isFreightRetsuban = unyohubLookupNum.includes("レ"); + const unyohubTrainNumForSourceScreen = isFreightRetsuban + ? unyohubLookupNum.replace(/レ/g, "") + : unyohubLookupNum; + const freightUnyohubCandidates = (() => { + const digits = unyohubTrainNumForSourceScreen.replace(/[^\d]/g, ""); + const candidates = new Set(); + if (!digits) return candidates; + candidates.add(digits); + if (/^\d{2}$/.test(digits)) { + candidates.add(`30${digits}`); + candidates.add(`90${digits}`); + } else if (/^(30|90)\d{2}$/.test(digits)) { + candidates.add(digits.slice(-2)); + } + return candidates; + })(); const unyohubFormation = getUnyohubByTrainNumber(unyohubLookupNum); - const unyohubEntries = getUnyohubEntriesByTrainNumber(unyohubLookupNum); + const unyohubEntries = isFreightRetsuban + ? unyohubData.filter((unyo) => + unyo.trains?.some( + (t) => !!t.train_number && freightUnyohubCandidates.has(t.train_number), + ), + ) + : getUnyohubEntriesByTrainNumber(unyohubTrainNumForSourceScreen); const elesiteEntries = getElesiteEntriesByTrainNumber(trainNum); - // 車番(formations)が空でないエントリが1件以上あれば「運用Hub情報あり」と判定 + // 車番(formations) がある場合のみ「運用Hub情報あり」と判定 const hasUnyohubFormation = unyohubEntries.some( (e) => !!e.formations && e.formations.trim() !== "", ); @@ -288,7 +312,7 @@ export const HeaderText: FC = ({ (SheetManager.show as any)("TrainDataSources", { payload: { trainNum, - unyohubTrainNum: unyohubLookupNum, + unyohubTrainNum: unyohubTrainNumForSourceScreen, unyohubEntries, elesiteEntries, todayOperation, diff --git a/components/ActionSheetComponents/TrainDataSources.tsx b/components/ActionSheetComponents/TrainDataSources.tsx index 453e44f..29ad41a 100644 --- a/components/ActionSheetComponents/TrainDataSources.tsx +++ b/components/ActionSheetComponents/TrainDataSources.tsx @@ -128,7 +128,27 @@ export const TrainDataSources: FC<{ payload?: TrainDataSourcesPayload }> = ({ destinationStation, } = payload; - const hubTrainNum = unyohubTrainNumProp || trainNum; + const isFreightRetsuban = trainNum.includes("レ"); + const hubTrainNum = (unyohubTrainNumProp || trainNum).replace(/レ/g, ""); + const freightUnyohubCandidates = (() => { + const digits = hubTrainNum.replace(/[^\d]/g, ""); + const candidates = new Set(); + if (!digits) return candidates; + candidates.add(digits); + if (/^\d{2}$/.test(digits)) { + candidates.add(`30${digits}`); + candidates.add(`90${digits}`); + } else if (/^(30|90)\d{2}$/.test(digits)) { + candidates.add(digits.slice(-2)); + } + return candidates; + })(); + const matchesHubTrainNum = (candidate?: string | null): boolean => { + if (!candidate) return false; + if (candidate === hubTrainNum) return true; + if (!isFreightRetsuban) return false; + return freightUnyohubCandidates.has(candidate); + }; // 進行方向の確定: // 1. payload.direction が明示されていればそれを使う @@ -226,7 +246,7 @@ export const TrainDataSources: FC<{ payload?: TrainDataSourcesPayload }> = ({ ); - // 鉄道運用Hub: 車番(formations)が空でないエントリのみ抽出して判定 + // 鉄道運用Hub: 車番(formations) が空でないエントリのみ表示対象にする const hasNonEmptyFormations = unyohubEntries.some( (e) => !!e.formations && e.formations.trim() !== "", ); @@ -269,7 +289,7 @@ export const TrainDataSources: FC<{ payload?: TrainDataSourcesPayload }> = ({ // outbound → position_forward 昇順 (pos=1 が宇和島/南端側) // inbound → position_forward 降順 (pos=MAX が宇和島/南端側) const matchedDirection = nonEmptyFormationEntries[0]?.trains?.find( - (t) => t.train_number === hubTrainNum, + (t) => matchesHubTrainNum(t.train_number), )?.direction; const hubSortDescending = matchedDirection === "inbound"; @@ -277,10 +297,10 @@ export const TrainDataSources: FC<{ payload?: TrainDataSourcesPayload }> = ({ [...nonEmptyFormationEntries] .sort((a, b) => { const posA = - a.trains?.find((t) => t.train_number === hubTrainNum) + a.trains?.find((t) => matchesHubTrainNum(t.train_number)) ?.position_forward ?? 0; const posB = - b.trains?.find((t) => t.train_number === hubTrainNum) + b.trains?.find((t) => matchesHubTrainNum(t.train_number)) ?.position_forward ?? 0; return hubSortDescending ? posB - posA : posA - posB; }) From 9e2abc96c7e70339534b53dc1d68678e86cf425d Mon Sep 17 00:00:00 2001 From: harukin-expo-dev-env Date: Sun, 5 Apr 2026 06:07:04 +0000 Subject: [PATCH 03/21] =?UTF-8?q?StatusBar=E3=81=AE=E8=A1=A8=E7=A4=BA?= =?UTF-8?q?=E3=83=AD=E3=82=B8=E3=83=83=E3=82=AF=E3=82=92=E6=94=B9=E5=96=84?= =?UTF-8?q?=E3=81=97=E3=80=81Apps=E3=82=B3=E3=83=B3=E3=83=9D=E3=83=BC?= =?UTF-8?q?=E3=83=8D=E3=83=B3=E3=83=88=E3=81=AB=E3=83=95=E3=82=A9=E3=83=BC?= =?UTF-8?q?=E3=82=AB=E3=82=B9=E7=8A=B6=E6=85=8B=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Apps.tsx | 3 +-- StatusbarDetect.tsx | 5 +---- components/Apps.tsx | 14 ++++++++------ menu.tsx | 13 +++++++++---- 4 files changed, 19 insertions(+), 16 deletions(-) diff --git a/Apps.tsx b/Apps.tsx index 600b009..5ddcd72 100644 --- a/Apps.tsx +++ b/Apps.tsx @@ -1,7 +1,7 @@ import React from "react"; import { NavigationContainer, DarkTheme, DefaultTheme } from "@react-navigation/native"; import { createBottomTabNavigator } from "@react-navigation/bottom-tabs"; -import { Animated, Platform, ActivityIndicator, View, StyleSheet } from "react-native"; +import { Animated, Platform, ActivityIndicator, View, StyleSheet, StatusBar } from "react-native"; import { useNavigationState } from "@react-navigation/native"; import { useFonts } from "expo-font"; import { LinearGradient } from "expo-linear-gradient"; @@ -128,7 +128,6 @@ export function AppContainer() { setIsExtraWindowOpen(hasExtra); }} > - {/* @ts-expect-error - Tab.Navigator type definition issue */} { diff --git a/StatusbarDetect.tsx b/StatusbarDetect.tsx index 28b973d..2912e78 100644 --- a/StatusbarDetect.tsx +++ b/StatusbarDetect.tsx @@ -1,11 +1,8 @@ import React, { FC } from "react"; import { Platform, StatusBar } from "react-native"; -import { useThemeColors } from "@/lib/theme"; const StatusbarDetect: FC = () => { - const { isDark } = useThemeColors(); - const barStyle = isDark ? "light-content" : "dark-content"; - return ; + return ; }; export default StatusbarDetect; diff --git a/components/Apps.tsx b/components/Apps.tsx index 09d5b73..2313e16 100644 --- a/components/Apps.tsx +++ b/components/Apps.tsx @@ -16,7 +16,7 @@ import { useCurrentTrain } from "../stateBox/useCurrentTrain"; import { useDeviceOrientationChange } from "../stateBox/useDeviceOrientationChange"; import { SheetManager } from "react-native-actions-sheet"; -import { useNavigation } from "@react-navigation/native"; +import { useNavigation, useIsFocused } from "@react-navigation/native"; import { useTrainMenu } from "../stateBox/useTrainMenu"; import { AppsWebView } from "./Apps/WebView"; import { NewMenu } from "./Apps/NewMenu"; @@ -35,6 +35,7 @@ export default function Apps() { const { originalStationList } = useStationList(); const { mapSwitch, trainInfo, setTrainInfo, selectedLine } = useTrainMenu(); const isDark = useColorScheme() === "dark"; + const isFocused = useIsFocused(); const lineColor = selectedLine && stationIDPair[selectedLine] ? lineColorList[stationIDPair[selectedLine]] @@ -83,7 +84,7 @@ export default function Apps() { const bgColor = isDark ? "#1c1c1e" : "#ffffff"; return ( - {lineColor && lineColorDark && ( + {isFocused && mapSwitch === "true" && lineColor && lineColorDark && ( )} - {lineColor && ( - + {isFocused && mapSwitch !== "true" && ( + + )} + {isFocused && ( + )} = (props) => { const { scrollRef, mapHeight, MapFullHeight, mapMode, setMapMode } = props; const { navigate } = useNavigation(); const { colors, isDark } = useThemeColors(); + const isMenuFocused = useIsFocused(); const { verticalScale } = useResponsive(); const insets = useSafeAreaInsets(); const { favoriteStation } = useFavoriteStation(); @@ -289,7 +289,12 @@ export const Menu: FC = (props) => { paddingTop: Platform.OS === "web" ? 0 : insets.top, }} > - + {isMenuFocused && ( + + )} {!mapMode ? : <>} Date: Sun, 5 Apr 2026 06:07:17 +0000 Subject: [PATCH 04/21] =?UTF-8?q?AppContainer=E3=81=AEonStateChange?= =?UTF-8?q?=E3=83=AD=E3=82=B8=E3=83=83=E3=82=AF=E3=82=92=E6=94=B9=E5=96=84?= =?UTF-8?q?=E3=81=97=E3=80=81=E3=82=A2=E3=82=AF=E3=83=86=E3=82=A3=E3=83=96?= =?UTF-8?q?=E3=81=AA=E3=83=AB=E3=83=BC=E3=83=88=E3=81=AE=E7=8A=B6=E6=85=8B?= =?UTF-8?q?=E3=82=92=E6=AD=A3=E7=A2=BA=E3=81=AB=E3=83=81=E3=82=A7=E3=83=83?= =?UTF-8?q?=E3=82=AF=E3=81=99=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Apps.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Apps.tsx b/Apps.tsx index 5ddcd72..0a4efa2 100644 --- a/Apps.tsx +++ b/Apps.tsx @@ -124,7 +124,8 @@ export function AppContainer() { linking={linking} theme={isDark ? DarkTheme : DefaultTheme} onStateChange={(state) => { - const hasExtra = state?.routes?.some((r) => (r.state?.index ?? 0) > 0) ?? false; + const activeRoute = state?.routes?.[state?.index ?? 0]; + const hasExtra = (activeRoute?.state?.index ?? 0) > 0; setIsExtraWindowOpen(hasExtra); }} > From a54ef7ca13bf1ea9466d3d9d37fa8ab3332f31c4 Mon Sep 17 00:00:00 2001 From: harukin-expo-dev-env Date: Wed, 8 Apr 2026 02:54:14 +0000 Subject: [PATCH 05/21] =?UTF-8?q?=E3=83=8A=E3=83=93=E3=82=B2=E3=83=BC?= =?UTF-8?q?=E3=82=B7=E3=83=A7=E3=83=B3=E3=83=AD=E3=82=B8=E3=83=83=E3=82=AF?= =?UTF-8?q?=E3=82=92=E6=94=B9=E5=96=84=E3=81=97=E3=80=81stackAwareNavigate?= =?UTF-8?q?=E9=96=A2=E6=95=B0=E3=82=92=E5=B0=8E=E5=85=A5=E3=81=97=E3=81=A6?= =?UTF-8?q?=E9=81=B7=E7=A7=BB=E6=99=82=E3=81=AE=E3=82=B9=E3=82=BF=E3=83=83?= =?UTF-8?q?=E3=82=AF=E7=AE=A1=E7=90=86=E3=82=92=E5=BC=B7=E5=8C=96=E3=80=82?= =?UTF-8?q?=E3=83=97=E3=83=A9=E3=82=A4=E3=83=90=E3=82=B7=E3=83=BC=E3=83=9D?= =?UTF-8?q?=E3=83=AA=E3=82=B7=E3=83=BC=E3=81=A8=E8=A8=AD=E8=A8=88=E3=83=A1?= =?UTF-8?q?=E3=83=A2=E3=82=92=E8=BF=BD=E5=8A=A0=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- App.tsx | 22 +++-- Apps.tsx | 4 +- MenuPage.tsx | 15 +++- Top.tsx | 19 ++++- docs/privacy-policy-future.md | 84 +++++++++++++++++++ docs/privacy-policy.md | 151 ++++++++++++++++++++++++++++++++++ lib/rootNavigation.ts | 46 ++++++++++- stateBox/useNotifications.tsx | 8 +- 8 files changed, 328 insertions(+), 21 deletions(-) create mode 100644 docs/privacy-policy-future.md create mode 100644 docs/privacy-policy.md diff --git a/App.tsx b/App.tsx index 5c1127a..c61cc83 100644 --- a/App.tsx +++ b/App.tsx @@ -25,7 +25,7 @@ import { buildProvidersTree } from "./lib/providerTreeProvider"; import { StationListProvider } from "./stateBox/useStationList"; import { NotificationProvider } from "./stateBox/useNotifications"; import { UserPositionProvider } from "./stateBox/useUserPosition"; -import { rootNavigationRef } from "./lib/rootNavigation"; +import { rootNavigationRef, stackAwareNavigate } from "./lib/rootNavigation"; import { AppThemeProvider } from "./lib/theme"; import StatusbarDetect from "./StatusbarDetect"; @@ -54,12 +54,12 @@ export default function App() { return; } - rootNavigationRef.navigate("topMenu", { + stackAwareNavigate("topMenu", { screen: "setting", params: { screen: "FelicaHistoryPage", }, - } as any); + }); }; const navigateWhenReady = ( @@ -87,26 +87,30 @@ export default function App() { navigateWhenReady(() => openFelicaPage(), url, retryCount); } else if (normalized.includes("open/traininfo")) { navigateWhenReady(() => { - rootNavigationRef.navigate("topMenu", { screen: "menu" } as any); + stackAwareNavigate("topMenu", { screen: "menu" }); setTimeout(() => { SheetManager.show("JRSTraInfo"); }, 450); }, url, retryCount); } else if (normalized.includes("open/operation")) { navigateWhenReady(() => { - rootNavigationRef.navigate("information" as any); + stackAwareNavigate("information"); }, url, retryCount); } else if (normalized.includes("open/settings")) { navigateWhenReady(() => { - rootNavigationRef.navigate("topMenu", { + stackAwareNavigate("topMenu", { screen: "setting", - } as any); + }); }, url, retryCount); } else if (normalized.includes("open/topmenu")) { navigateWhenReady(() => { - rootNavigationRef.navigate("topMenu", { + stackAwareNavigate("topMenu", { screen: "menu", - } as any); + }); + }, url, retryCount); + } else if (normalized.includes("positions/apps")) { + navigateWhenReady(() => { + stackAwareNavigate("positions"); }, url, retryCount); } }; diff --git a/Apps.tsx b/Apps.tsx index 0a4efa2..9c327f5 100644 --- a/Apps.tsx +++ b/Apps.tsx @@ -68,9 +68,7 @@ export function AppContainer() { config: { screens: { positions: { - screens: { - Apps: "positions/apps", - }, + screens: {}, }, topMenu: { screens: { diff --git a/MenuPage.tsx b/MenuPage.tsx index 1cb357f..b4b5f40 100644 --- a/MenuPage.tsx +++ b/MenuPage.tsx @@ -84,7 +84,14 @@ export function MenuPage() { setMapFullHeight(MapFullHeight); }, [height, tabBarHeight, width]); useEffect(() => { - const unsubscribe = addListener("tabPress", (e) => { + const unsubscribe = addListener("tabPress", (e: any) => { + if (navigation.isFocused() && stackNavRef.current) { + if (stackNavRef.current.getState()?.index > 0) { + e.preventDefault(); + stackNavRef.current.goBack(); + return; + } + } scrollRef.current?.scrollTo({ y: mapHeightRef.current - verticalScale(80), animated: true, @@ -106,10 +113,16 @@ export function MenuPage() { return unsubscribe; }, [navigation]); + const stackNavRef = useRef(null); + return ( { + stackNavRef.current = stackNav; + return {}; + }} > { const { webview } = useCurrentTrain(); @@ -38,14 +39,22 @@ export const Top = () => { return unsubscribe; }, []); - const goToTrainMenu = useCallback(() => { + const stackNavRef = positionsStackNavRef; + + const goToTrainMenu = useCallback((e: any) => { if (Platform.OS === "web") { Linking.openURL("https://train.jr-shikoku.co.jp/"); setTimeout(() => navigate("topMenu", { screen: "menu" }), 100); return; } - if (!isFocused()) navigate("positions", { screen: "Apps" }); - else if (mapSwitchRef.current == "true") + if (!isFocused()) return; + const stackNav = stackNavRef.current; + if (stackNav && stackNav.getState()?.index > 0) { + e.preventDefault(); + stackNav.goBack(); + return; + } + if (mapSwitchRef.current == "true") navigate("positions", { screen: "trainMenu" }); else webview.current?.injectJavaScript(`AccordionClassEvent()`); return; @@ -60,6 +69,10 @@ export const Top = () => { { + stackNavRef.current = stackNav; + return {}; + }} > (); + +/** positions タブの Stack.Navigator navigation を登録するグローバルref */ +export const positionsStackNavRef: { current: any } = { current: null }; + +/** + * 遷移先タブのネストスタックを一度 popToTop してからナビゲートする。 + * ウィジェットや外部リンクからの遷移時に、既存の開いている画面を閉じてから目的の画面へ移動するために使用する。 + */ +export function stackAwareNavigate(tabName: string, params?: any) { + if (!rootNavigationRef.isReady()) return; + + const doNavigate = () => { + if (params !== undefined) { + rootNavigationRef.navigate(tabName, params); + } else { + rootNavigationRef.navigate(tabName as any); + } + }; + + // positions タブは直接 stackNavRef を使って確実に popToTop する + if (tabName === "positions" && positionsStackNavRef.current) { + const stackNav = positionsStackNavRef.current; + if ((stackNav.getState()?.index ?? 0) > 0) { + stackNav.popToTop(); + setTimeout(doNavigate, 350); + } else { + doNavigate(); + } + return; + } + + const state = rootNavigationRef.getState(); + const tabRoute = state?.routes?.find((r: any) => r.name === tabName); + if (tabRoute?.state && (tabRoute.state.index ?? 0) > 0) { + rootNavigationRef.dispatch({ + ...StackActions.popToTop(), + target: tabRoute.state.key, + }); + // popToTop のアニメーション完了後に navigate を実行 + setTimeout(doNavigate, 350); + } else { + doNavigate(); + } +} diff --git a/stateBox/useNotifications.tsx b/stateBox/useNotifications.tsx index 1376433..7da9c05 100644 --- a/stateBox/useNotifications.tsx +++ b/stateBox/useNotifications.tsx @@ -11,7 +11,7 @@ import * as Notifications from "expo-notifications"; import * as Device from "expo-device"; import Constants from "expo-constants"; import { logger } from "@/utils/logger"; -import { rootNavigationRef } from "@/lib/rootNavigation"; +import { rootNavigationRef, stackAwareNavigate } from "@/lib/rootNavigation"; import { SheetManager } from "react-native-actions-sheet"; import { AS } from "@/storageControl"; import { STORAGE_KEYS } from "@/constants"; @@ -220,16 +220,16 @@ export const NotificationProvider: FC = ({ children }) => { AS.setItem(STORAGE_KEYS.LAST_HANDLED_NOTIFICATION, requestId).catch(() => {}); switch (action) { case "delay-ex": - rootNavigationRef.navigate("topMenu", { screen: "menu" }); + stackAwareNavigate("topMenu", { screen: "menu" }); setTimeout(() => { SheetManager.show("JRSTraInfo"); }, 450); break; case "strange-train": - rootNavigationRef.navigate("positions", { screen: "Apps" }); + stackAwareNavigate("positions"); break; case "information": - rootNavigationRef.navigate("information"); + stackAwareNavigate("information"); break; default: break; From 59146464431551db8f87139f7fc8f47670493ec8 Mon Sep 17 00:00:00 2001 From: harukin-expo-dev-env Date: Wed, 8 Apr 2026 02:54:30 +0000 Subject: [PATCH 06/21] =?UTF-8?q?stackAwareNavigate=E9=96=A2=E6=95=B0?= =?UTF-8?q?=E3=82=92=E5=B0=8E=E5=85=A5=E3=81=97=E3=80=81=E9=81=B7=E7=A7=BB?= =?UTF-8?q?=E6=99=82=E3=81=AE=E3=83=8A=E3=83=93=E3=82=B2=E3=83=BC=E3=82=B7?= =?UTF-8?q?=E3=83=A7=E3=83=B3=E3=83=AD=E3=82=B8=E3=83=83=E3=82=AF=E3=82=92?= =?UTF-8?q?=E6=94=B9=E5=96=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ActionSheetComponents/EachTrainInfo/TrainDataView.tsx | 5 +++-- .../StationDeteilView/StationTrainPositionButton.tsx | 5 +++-- components/Menu/Carousel/CarouselTypeChanger.tsx | 3 ++- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/components/ActionSheetComponents/EachTrainInfo/TrainDataView.tsx b/components/ActionSheetComponents/EachTrainInfo/TrainDataView.tsx index 34b6435..ae6e89c 100644 --- a/components/ActionSheetComponents/EachTrainInfo/TrainDataView.tsx +++ b/components/ActionSheetComponents/EachTrainInfo/TrainDataView.tsx @@ -13,6 +13,7 @@ import { useStationList } from "../../../stateBox/useStationList"; import { useAllTrainDiagram } from "@/stateBox/useAllTrainDiagram"; import { customTrainDataDetector } from "@/components/custom-train-data"; import type { NavigateFunction } from "@/types"; +import { stackAwareNavigate } from "@/lib/rootNavigation"; type props = { @@ -165,14 +166,14 @@ export const TrainDataView:FC = ({ onLongPress={()=>{ if (!onLine) return; setInjectData({ type:"train", value:currentTrainData?.num, fixed:true}); - navigate("positions", { screen: "Apps" }); + stackAwareNavigate("positions"); SheetManager.hide("EachTrainInfo"); }} onPress={() => { if (!onLine) return; setInjectData({ type: "station", value: currentPosition[0], fixed: false }); - navigate("positions", { screen: "Apps" }); + stackAwareNavigate("positions"); SheetManager.hide("EachTrainInfo"); }} > diff --git a/components/ActionSheetComponents/StationDeteilView/StationTrainPositionButton.tsx b/components/ActionSheetComponents/StationDeteilView/StationTrainPositionButton.tsx index b2c4f69..965b449 100644 --- a/components/ActionSheetComponents/StationDeteilView/StationTrainPositionButton.tsx +++ b/components/ActionSheetComponents/StationDeteilView/StationTrainPositionButton.tsx @@ -3,6 +3,7 @@ import { TouchableOpacity, View, Text, Linking } from "react-native"; import { useCurrentTrain } from "@/stateBox/useCurrentTrain"; import { useThemeColors } from "@/lib/theme"; import AntDesign from "react-native-vector-icons/AntDesign"; +import { stackAwareNavigate } from "@/lib/rootNavigation"; type Props = { stationNumber: string; onExit: () => void; @@ -24,12 +25,12 @@ export const StationTrainPositionButton: FC = (props) => { flex: 1, }} onLongPress={() => { - navigate("positions", { screen: "Apps" }); + stackAwareNavigate("positions"); setInjectData({ type: "station", value:stationNumber, fixed: true }); onExit(); }} onPress={() => { - navigate("positions", { screen: "Apps" }); + stackAwareNavigate("positions"); setInjectData({ type: "station", value: stationNumber, fixed: false }); onExit(); }} diff --git a/components/Menu/Carousel/CarouselTypeChanger.tsx b/components/Menu/Carousel/CarouselTypeChanger.tsx index 260f9c7..21818fe 100644 --- a/components/Menu/Carousel/CarouselTypeChanger.tsx +++ b/components/Menu/Carousel/CarouselTypeChanger.tsx @@ -8,6 +8,7 @@ import { Platform, } from "react-native"; import Ionicons from "react-native-vector-icons/Ionicons"; +import { stackAwareNavigate } from "@/lib/rootNavigation"; import { SearchUnitBox } from "@/components/Menu/RailScope/SearchUnitBox"; import { StationSource } from "@/types"; import { STORAGE_KEYS } from "@/constants"; @@ -47,7 +48,7 @@ export const CarouselTypeChanger = ({ if (isGpsFollowing) { setFixedPosition({ type: null, value: null }); } else { - navigate("positions", { screen: "Apps" } as any); + stackAwareNavigate("positions"); setFixedPosition({ type: "nearestStation", value: null }); } }; From 8b4264454853bc705b610fb7fc325bde33736cfd Mon Sep 17 00:00:00 2001 From: harukin-expo-dev-env Date: Wed, 8 Apr 2026 05:00:58 +0000 Subject: [PATCH 07/21] =?UTF-8?q?fix:=20EachTrainInfo=20ActionSheet?= =?UTF-8?q?=E3=81=AE=E3=82=B9=E3=83=97=E3=83=AA=E3=83=B3=E3=82=B0=E3=82=A2?= =?UTF-8?q?=E3=83=8B=E3=83=A1=E3=83=BC=E3=82=B7=E3=83=A7=E3=83=B3=E7=A0=B4?= =?UTF-8?q?=E7=B6=BB=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit iOS (isModal=true) でマリンライナー等の走行中列車を表示した際に ActionSheet のスライドアップアニメーションが瞬間表示になる問題を修正。 【根本原因】 1. iOS onOpen の発火タイミング問題(最重要) - ライブラリ内で onOpen が Modal.onShow にバインドされており、 スプリングアニメーション開始「前」に発火する - onOpen 後に showThrew=true になると通過駅が追加されて高さが増加し onSheetLayout が再発火 → スプリングがほぼ終点からリスタート 2. useEffect による非同期な高さ変化 - useThroughStations / useStopStationIDs / useTrainDiagramData が useState([]) で初期化し useEffect で計算していたため 空リスト → フルリストの高さ変化が onSheetLayout をトリガーしていた 3. useAutoScroll の InteractionManager が Reanimated アニメーションを認識しない 【修正内容】 - EachTrainInfoCore: showThrew の初期値を useState(() => !!getCurrentStationData(...)) に変更し、走行中なら最初から true にして高さ変化を防ぐ - useTrainDiagramData / useThroughStations / useStopStationIDs: 純粋計算関数を抽出し useState lazy initializer で初回レンダリング時から正確な高さを確保 - EachTrainInfo: onOpen/onClose で sheetOpened state を管理し EachTrainInfoCore に渡す - useAutoScroll: setShowThrew 引数を削除、sheetOpened フラグでスクロールをゲート --- .../ActionSheetComponents/EachTrainInfo.tsx | 17 +- .../EachTrainInfoCore.tsx | 9 +- .../EachTrainInfoCore/hooks/useAutoScroll.ts | 35 ++-- .../hooks/useStopStationIDs.ts | 30 +-- .../hooks/useThroughStations.ts | 179 +++++++++--------- .../hooks/useTrainDiagramData.ts | 34 ++-- 6 files changed, 164 insertions(+), 140 deletions(-) diff --git a/components/ActionSheetComponents/EachTrainInfo.tsx b/components/ActionSheetComponents/EachTrainInfo.tsx index 416f966..8aa50da 100644 --- a/components/ActionSheetComponents/EachTrainInfo.tsx +++ b/components/ActionSheetComponents/EachTrainInfo.tsx @@ -1,4 +1,4 @@ -import React, { useRef } from "react"; +import React, { useRef, useState } from "react"; import { Platform } from "react-native"; import ActionSheet from "react-native-actions-sheet"; import { EachTrainInfoCore } from "./EachTrainInfoCore"; @@ -6,6 +6,16 @@ import { useSheetMaxHeight } from "./useSheetMaxHeight"; export const EachTrainInfo = ({ payload }) => { const actionSheetRef = useRef(null); const maxHeight = useSheetMaxHeight(); + const [sheetOpened, setSheetOpened] = useState(false); + + const handleOpen = () => { + setSheetOpened(true); + }; + + const handleClose = () => { + setSheetOpened(false); + }; + if (!payload) return <>; return ( { drawUnderStatusBar={false} isModal={Platform.OS === "ios" && !Platform.isPad} containerStyle={{ maxHeight }} - + onOpen={handleOpen} + onClose={handleClose} //useBottomSafeAreaPadding={Platform.OS == "android"} > - + ); }; diff --git a/components/ActionSheetComponents/EachTrainInfoCore.tsx b/components/ActionSheetComponents/EachTrainInfoCore.tsx index 721b572..4e37c4f 100644 --- a/components/ActionSheetComponents/EachTrainInfoCore.tsx +++ b/components/ActionSheetComponents/EachTrainInfoCore.tsx @@ -22,6 +22,7 @@ import { ShowSpecialTrain } from "./EachTrainInfo/ShowSpecialTrain"; import { useTrainMenu } from "../../stateBox/useTrainMenu"; import { HeaderText } from "./EachTrainInfoCore/HeaderText"; import { useStationList } from "../../stateBox/useStationList"; +import { useCurrentTrain } from "../../stateBox/useCurrentTrain"; import { useThemeColors } from "@/lib/theme"; import { useAllTrainDiagram } from "@/stateBox/useAllTrainDiagram"; import { useResponsive } from "@/lib/responsive"; @@ -41,6 +42,7 @@ export const EachTrainInfoCore = ({ openStationACFromEachTrainInfo, from, navigate, + sheetOpened = false, }) => { const { stationList } = useStationList(); const { allCustomTrainData } = useAllTrainDiagram(); @@ -49,6 +51,7 @@ export const EachTrainInfoCore = ({ const { setTrainInfo } = useTrainMenu(); const { height } = useWindowDimensions(); const { isLandscape } = useDeviceOrientationChange(); + const { getCurrentStationData } = useCurrentTrain(); const scrollRef = useRef(null); // Custom hooks for data management @@ -72,7 +75,9 @@ export const EachTrainInfoCore = ({ } = useExtendedStations(trainData, setTrainData); // UI state - const [showThrew, setShowThrew] = useState(false); + // 走行中の列車は初期状態から通過駅を表示する(後から showThrew を true に変更すると + // ActionSheet の onSheetLayout が再発火してスプリングアニメーションが途中でリスタートするため) + const [showThrew, setShowThrew] = useState(() => !!getCurrentStationData(data.trainNum)); const [isJumped, setIsJumped] = useState(false); // Auto scroll to current position @@ -82,7 +87,7 @@ export const EachTrainInfoCore = ({ scrollRef, isJumped, setIsJumped, - setShowThrew + sheetOpened ); // Back button handler diff --git a/components/ActionSheetComponents/EachTrainInfoCore/hooks/useAutoScroll.ts b/components/ActionSheetComponents/EachTrainInfoCore/hooks/useAutoScroll.ts index e719341..5374558 100644 --- a/components/ActionSheetComponents/EachTrainInfoCore/hooks/useAutoScroll.ts +++ b/components/ActionSheetComponents/EachTrainInfoCore/hooks/useAutoScroll.ts @@ -1,5 +1,4 @@ import { useEffect, MutableRefObject } from 'react'; -import { InteractionManager } from 'react-native'; export const useAutoScroll = ( @@ -8,31 +7,27 @@ export const useAutoScroll = ( scrollRef: MutableRefObject, isJumped: boolean, setIsJumped: (value: boolean) => void, - setShowThrew: (value: boolean) => void + sheetOpened: boolean = false ) => { useEffect(() => { - if (isJumped || !points?.length || !scrollRef) return; + // ActionSheetのスプリングアニメーション完了後まで待機 + if (!sheetOpened || isJumped || !points?.length || !scrollRef) return; const currentPositionIndex = points.findIndex((d) => d === true); if (currentPositionIndex === -1) return; - // ActionSheetの開閉アニメーション完了後にレイアウト変更を行う - const handle = InteractionManager.runAfterInteractions(() => { - setShowThrew(true); + // 5駅以内の場合はスクロールしない + if (currentPositionIndex < 5) { + setIsJumped(true); + return; + } - // 5駅以内の場合はスクロールしない - if (currentPositionIndex < 5) { - setIsJumped(true); - return; - } + const scrollPosition = currentPositionIndex * 44 - 50; + const timer = setTimeout(() => { + scrollRef.current?.scrollTo({ y: scrollPosition, animated: true }); + setIsJumped(true); + }, 100); - const scrollPosition = currentPositionIndex * 44 - 50; - setTimeout(() => { - scrollRef.current?.scrollTo({ y: scrollPosition, animated: true }); - setIsJumped(true); - }, 100); - }); - - return () => handle.cancel(); - }, [points, trainDataWithThrough, scrollRef, isJumped, setIsJumped, setShowThrew]); + return () => clearTimeout(timer); + }, [sheetOpened, points, trainDataWithThrough, scrollRef, isJumped, setIsJumped]); }; diff --git a/components/ActionSheetComponents/EachTrainInfoCore/hooks/useStopStationIDs.ts b/components/ActionSheetComponents/EachTrainInfoCore/hooks/useStopStationIDs.ts index 30b4647..468a42f 100644 --- a/components/ActionSheetComponents/EachTrainInfoCore/hooks/useStopStationIDs.ts +++ b/components/ActionSheetComponents/EachTrainInfoCore/hooks/useStopStationIDs.ts @@ -1,25 +1,25 @@ import { useState, useEffect } from 'react'; import { useStationList } from '@/stateBox/useStationList'; +const computeStopStationIDs = (data: string[], stationList: any[][]): string[][] => + data.map((item) => { + const [stationName] = item.split(','); + return stationList + .map((lineStations) => lineStations.filter((s) => s.StationName === stationName)) + .reduce((acc, s) => acc.concat(s), []) + .map((s) => s.StationNumber); + }); + export const useStopStationIDs = (trainDataWithThrough: string[]) => { const { stationList } = useStationList(); - const [stopStationIDList, setStopStationIDList] = useState([]); + + // 初回レンダリング時に同期的に計算することでActionSheetのアニメーション中の高さ変化を防ぐ + const [stopStationIDList, setStopStationIDList] = useState(() => + computeStopStationIDs(trainDataWithThrough, stationList) + ); useEffect(() => { - const stationIDs = trainDataWithThrough.map((item) => { - const [stationName] = item.split(','); - - const matchingStations = stationList - .map((lineStations) => - lineStations.filter((station) => station.StationName === stationName) - ) - .reduce((acc, stations) => acc.concat(stations), []) - .map((station) => station.StationNumber); - - return matchingStations; - }); - - setStopStationIDList(stationIDs); + setStopStationIDList(computeStopStationIDs(trainDataWithThrough, stationList)); }, [trainDataWithThrough, stationList]); return stopStationIDList; diff --git a/components/ActionSheetComponents/EachTrainInfoCore/hooks/useThroughStations.ts b/components/ActionSheetComponents/EachTrainInfoCore/hooks/useThroughStations.ts index aed14c3..8fef145 100644 --- a/components/ActionSheetComponents/EachTrainInfoCore/hooks/useThroughStations.ts +++ b/components/ActionSheetComponents/EachTrainInfoCore/hooks/useThroughStations.ts @@ -2,107 +2,114 @@ import { useState, useEffect } from 'react'; import { lineListPair, stationIDPair } from '@/lib/getStationList'; import { useStationList } from '@/stateBox/useStationList'; -export const useThroughStations = (trainData) => { - const { originalStationList, stationList } = useStationList(); - const [trainDataWithThrough, setTrainDataWithThrough] = useState([]); - const [haveThrough, setHaveThrough] = useState(false); +const computeThroughStations = ( + trainData: string[], + stationList: any[][], + originalStationList: Record +): { trainDataWithThrough: string[]; haveThrough: boolean } => { + if (!trainData.length) return { trainDataWithThrough: [], haveThrough: false }; - useEffect(() => { - if (!trainData.length) { - setTrainDataWithThrough([]); - return; + let haveThrough = false; + const isCancel: boolean[] = []; + + const stopStationList = trainData.map((item, index, array) => { + const [station, se] = item.split(','); + const [, nextSe] = array[index + 1]?.split(',') || []; + + if (nextSe) { + // 運休判定ロジック: + // 1. 両方が休系(休編、休発、休着など)→ 運休区間 + // 2. 着/着編 → 休発/休発編:到着後に運休開始 → 通過駅は通常運行 + // 3. 休着/休着編 → 発/発編:運休終了後に出発 → 通過駅は通常運行 + // 4. その他の休の組み合わせ → 運休区間 + const bothCanceled = se.includes('休') && nextSe.includes('休'); + const normalArrivalToSuspendStart = + (se === '着' || se === '着編') && (nextSe.includes('休') && nextSe.includes('発')); + const suspendEndToNormalDeparture = + (se.includes('休') && se.includes('着')) && (nextSe === '発' || nextSe === '発編'); + + isCancel.push(bothCanceled && !normalArrivalToSuspendStart && !suspendEndToNormalDeparture); } - const isCancel = []; - const stopStationList = trainData.map((item, index, array) => { - const [station, se] = item.split(','); - const [, nextSe] = array[index + 1]?.split(',') || []; + if (se === '通編') haveThrough = true; - if (nextSe) { - // 運休判定ロジック: - // 1. 両方が休系(休編、休発、休着など)→ 運休区間 - // 2. 着/着編 → 休発/休発編:到着後に運休開始 → 通過駅は通常運行 - // 3. 休着/休着編 → 発/発編:運休終了後に出発 → 通過駅は通常運行 - // 4. その他の休の組み合わせ → 運休区間 - const bothCanceled = se.includes('休') && nextSe.includes('休'); - const normalArrivalToSuspendStart = - (se === '着' || se === '着編') && (nextSe.includes('休') && nextSe.includes('発')); - const suspendEndToNormalDeparture = - (se.includes('休') && se.includes('着')) && (nextSe === '発' || nextSe === '発編'); - - const isCanceled = bothCanceled && !normalArrivalToSuspendStart && !suspendEndToNormalDeparture; - isCancel.push(isCanceled); + return stationList.map((a) => a.filter((d) => d.StationName === station)); + }); + + const allThroughStationList = stopStationList.map((firstItem, index, array) => { + if (index === array.length - 1) return []; + + const secondItem = array[index + 1]; + let betweenStationLine = ''; + let baseStationNumberFirst = ''; + let baseStationNumberSecond = ''; + + Object.keys(stationIDPair).forEach((lineName, lineIndex) => { + if (!lineName) return; + const haveFirst = firstItem[lineIndex]; + const haveSecond = secondItem[lineIndex]; + + if (haveFirst?.length && haveSecond?.length) { + betweenStationLine = lineName; + baseStationNumberFirst = haveFirst[0].StationNumber; + baseStationNumberSecond = haveSecond[0].StationNumber; } - - if (se === '通編') setHaveThrough(true); - - return stationList.map((a) => a.filter((d) => d.StationName === station)); }); - const allThroughStationList = stopStationList.map((firstItem, index, array) => { - if (index === array.length - 1) return []; + if (!betweenStationLine) return []; - const secondItem = array[index + 1]; - let betweenStationLine = ''; - let baseStationNumberFirst = ''; - let baseStationNumberSecond = ''; + const allThroughStation: string[] = []; + let reverse = false; - Object.keys(stationIDPair).forEach((lineName, lineIndex) => { - if (!lineName) return; - const haveFirst = firstItem[lineIndex]; - const haveSecond = secondItem[lineIndex]; + originalStationList[lineListPair[stationIDPair[betweenStationLine]]]?.forEach((station) => { + const throughStatus = isCancel[index] ? '通休編' : '通過'; - if (haveFirst?.length && haveSecond?.length) { - betweenStationLine = lineName; - baseStationNumberFirst = haveFirst[0].StationNumber; - baseStationNumberSecond = haveSecond[0].StationNumber; - } - }); - - if (!betweenStationLine) return []; - - const allThroughStation = []; - let reverse = false; - - originalStationList[lineListPair[stationIDPair[betweenStationLine]]]?.forEach((station) => { - const throughStatus = isCancel[index] ? '通休編' : '通過'; - - if ( - station.StationNumber > baseStationNumberFirst && - station.StationNumber < baseStationNumberSecond - ) { - allThroughStation.push(`${station.Station_JP},${throughStatus},`); - setHaveThrough(true); - reverse = false; - } else if ( - station.StationNumber < baseStationNumberFirst && - station.StationNumber > baseStationNumberSecond - ) { - allThroughStation.push(`${station.Station_JP},${throughStatus},`); - setHaveThrough(true); - reverse = true; - } - }); - - if (reverse) allThroughStation.reverse(); - return allThroughStation; + if ( + station.StationNumber > baseStationNumberFirst && + station.StationNumber < baseStationNumberSecond + ) { + allThroughStation.push(`${station.Station_JP},${throughStatus},`); + haveThrough = true; + reverse = false; + } else if ( + station.StationNumber < baseStationNumberFirst && + station.StationNumber > baseStationNumberSecond + ) { + allThroughStation.push(`${station.Station_JP},${throughStatus},`); + haveThrough = true; + reverse = true; + } }); - let mainArray = [...trainData]; - let offset = 0; + if (reverse) allThroughStation.reverse(); + return allThroughStation; + }); - trainData.forEach((_, index) => { - offset += 1; - const throughStations = allThroughStationList[index]; - - if (!throughStations?.length) return; + let mainArray = [...trainData]; + let offset = 0; - mainArray.splice(offset, 0, ...throughStations); - offset += throughStations.length; - }); + trainData.forEach((_, index) => { + offset += 1; + const throughStations = allThroughStationList[index]; + if (!throughStations?.length) return; + mainArray.splice(offset, 0, ...throughStations); + offset += throughStations.length; + }); - setTrainDataWithThrough(mainArray); + return { trainDataWithThrough: mainArray, haveThrough }; +}; + +export const useThroughStations = (trainData) => { + const { originalStationList, stationList } = useStationList(); + + // 初回レンダリング時に同期的に計算することでActionSheetのアニメーション中の高さ変化を防ぐ + const [state, setState] = useState(() => + computeThroughStations(trainData, stationList, originalStationList) + ); + + useEffect(() => { + setState(computeThroughStations(trainData, stationList, originalStationList)); }, [trainData, stationList, originalStationList]); - return { trainDataWithThrough, haveThrough }; + return { trainDataWithThrough: state.trainDataWithThrough, haveThrough: state.haveThrough }; }; diff --git a/components/ActionSheetComponents/EachTrainInfoCore/hooks/useTrainDiagramData.ts b/components/ActionSheetComponents/EachTrainInfoCore/hooks/useTrainDiagramData.ts index 9e098cb..f94293a 100644 --- a/components/ActionSheetComponents/EachTrainInfoCore/hooks/useTrainDiagramData.ts +++ b/components/ActionSheetComponents/EachTrainInfoCore/hooks/useTrainDiagramData.ts @@ -2,28 +2,34 @@ import { useState, useEffect } from 'react'; import { useAllTrainDiagram } from '@/stateBox/useAllTrainDiagram'; import { searchSpecialTrain } from '@/lib/eachTrainInfoCoreLib/searchSpecialTrain'; +const parseTrainData = (trainNum: string, trainList: Record) => { + if (!trainNum) return { data: [], trueIDs: [] }; + const TD = trainList[trainNum]; + if (!TD) { + const specialTrainActualIDs = searchSpecialTrain(trainNum, trainList); + return { data: [], trueIDs: specialTrainActualIDs || [] }; + } + return { data: TD.split('#').filter((d) => d !== ''), trueIDs: [] }; +}; + export const useTrainDiagramData = (trainNum) => { const { allTrainDiagram: trainList } = useAllTrainDiagram(); - const [trainData, setTrainData] = useState([]); - const [trueTrainID, setTrueTrainID] = useState([]); const [isManuallyExtended, setIsManuallyExtended] = useState(false); + // 初回レンダリング時にコンテキストから同期的にデータを取得することで + // ActionSheetのアニメーション中に高さが変わるのを防ぐ + const [trainData, setTrainData] = useState(() => parseTrainData(trainNum, trainList).data); + const [trueTrainID, setTrueTrainID] = useState(() => parseTrainData(trainNum, trainList).trueIDs); + useEffect(() => { if (!trainNum) return; - + // 手動で拡張されている場合は上書きしない if (isManuallyExtended) return; - - const TD = trainList[trainNum]; - - if (!TD) { - const specialTrainActualIDs = searchSpecialTrain(trainNum, trainList); - setTrueTrainID(specialTrainActualIDs || []); - setTrainData([]); - return; - } - - setTrainData(TD.split('#').filter((d) => d !== '')); + + const { data, trueIDs } = parseTrainData(trainNum, trainList); + setTrueTrainID(trueIDs); + setTrainData(data); }, [trainNum, trainList, isManuallyExtended]); const setTrainDataExtended = (data) => { From 48094266323a96416f5de89f6ec1c0f95085f974 Mon Sep 17 00:00:00 2001 From: harukin-expo-dev-env Date: Wed, 8 Apr 2026 05:03:19 +0000 Subject: [PATCH 08/21] =?UTF-8?q?docs:=20ActionSheet=E3=82=A2=E3=83=8B?= =?UTF-8?q?=E3=83=A1=E3=83=BC=E3=82=B7=E3=83=A7=E3=83=B3=E7=A0=B4=E7=B6=BB?= =?UTF-8?q?=E3=81=AE=E4=BF=AE=E6=AD=A3=E8=A8=98=E9=8C=B2=E3=82=92=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/actionsheet-animation-fix.md | 118 ++++++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 docs/actionsheet-animation-fix.md diff --git a/docs/actionsheet-animation-fix.md b/docs/actionsheet-animation-fix.md new file mode 100644 index 0000000..ca6d7dd --- /dev/null +++ b/docs/actionsheet-animation-fix.md @@ -0,0 +1,118 @@ +# EachTrainInfo ActionSheet アニメーション破綻の修正記録 + +**日付:** 2026-04-08 +**ブランチ:** fix/April-Mid-Patch +**コミット:** 8b42644 + +--- + +## 症状 + +iOS (`isModal=true`) でマリンライナー等の**走行中の列車**を EachTrainInfo ActionSheet で表示したとき、スライドアップアニメーションが瞬間表示になる。 +Android では発生しない。 + +--- + +## 根本原因(3層) + +### 1. iOS `onOpen` の発火タイミング(最重要) + +ライブラリ `node_modules/react-native-actions-sheet/dist/src/index.js` line 962: +```js +onShow: props.onOpen, +``` + +iOS の `isModal=true` モードでは ActionSheet の `onOpen` prop が React Native の `Modal.onShow` にバインドされる。 +`Modal.onShow` はスプリングアニメーション**開始前**(モーダルが表示された直後)に発火する。 + +これにより以下の連鎖が起きていた: +1. Modal 表示 → `onOpen` → `setShowThrew(true)` 実行 +2. 通過駅が一気に追加されてシート高さが増加 +3. `onSheetLayout` が再発火してスプリングが「ほぼ終点位置」からリスタート +4. 結果:スライドアップに見えず瞬間表示になる + +### 2. `useEffect` による非同期な高さ変化 + +以下の hooks が `useState([])` (空) で初期化し `useEffect` で計算していた: +- `useTrainDiagramData` — 駅リスト本体 +- `useThroughStations` — 通過駅挿入後のリスト(実際にレンダリングされる) +- `useStopStationIDs` — 駅ID対応表 + +初回レンダリング(空・高さ小)→ `useEffect` 完了(フルリスト・高さ大)という変化が `onSheetLayout` を再トリガーしていた。 + +### 3. `useAutoScroll` の `InteractionManager` が非効果的だった + +`InteractionManager.runAfterInteractions()` は JS スレッドのインタラクション完了を待つが、ActionSheet の Reanimated スプリング(UI スレッド)完了は認識しないため、アニメーション途中でスクロールが実行されることがあった。 + +--- + +## 修正内容 + +### `showThrew` の初期値を同期的に決定 ← 最重要 + +```tsx +// EachTrainInfoCore.tsx(修正前) +const [showThrew, setShowThrew] = useState(false); + +// EachTrainInfoCore.tsx(修正後) +const [showThrew, setShowThrew] = useState(() => !!getCurrentStationData(data.trainNum)); +``` + +走行中の列車は最初から `true` にすることで、アニメーション中に通過駅の追加による高さ変化が起きなくなる。 + +### 各 hooks の lazy initializer 化 + +純粋計算関数を抽出して `useState` の初期化関数に渡すことで、初回レンダリング時から正確な高さを確保: + +```ts +// useThroughStations.ts +const [state, setState] = useState(() => + computeThroughStations(trainData, stationList, originalStationList) +); + +// useStopStationIDs.ts +const [stopStationIDList, setStopStationIDList] = useState(() => + computeStopStationIDs(trainDataWithThrough, stationList) +); + +// useTrainDiagramData.ts +const [trainData, setTrainData] = useState(() => parseTrainData(trainNum, trainList).data); +``` + +### `sheetOpened` フラグによるスクロールのゲート + +```tsx +// EachTrainInfo.tsx +const [sheetOpened, setSheetOpened] = useState(false); +// onOpen → setSheetOpened(true), onClose → setSheetOpened(false) +``` + +```ts +// useAutoScroll.ts — sheetOpened が true になるまでスクロールしない +if (!sheetOpened || isJumped || ...) return; +``` + +### `useAutoScroll` から `setShowThrew` 呼び出しを除去 + +スクロール位置制御と通過駅表示の責務を分離。`setShowThrew` は `EachTrainInfoCore` の初期化時のみで完結。 + +--- + +## 変更ファイル一覧 + +| ファイル | 変更内容 | +|---|---| +| `components/ActionSheetComponents/EachTrainInfo.tsx` | `sheetOpened` state 追加、`onOpen`/`onClose` ハンドラ実装 | +| `components/ActionSheetComponents/EachTrainInfoCore.tsx` | `showThrew` 同期初期化、`useCurrentTrain` import 追加、`setShowThrew` を `useAutoScroll` から除去 | +| `components/ActionSheetComponents/EachTrainInfoCore/hooks/useAutoScroll.ts` | `setShowThrew` 引数削除、`sheetOpened` ゲート追加、`InteractionManager` 廃止 | +| `components/ActionSheetComponents/EachTrainInfoCore/hooks/useTrainDiagramData.ts` | `parseTrainData` 純粋関数抽出、lazy initializer 化 | +| `components/ActionSheetComponents/EachTrainInfoCore/hooks/useThroughStations.ts` | `computeThroughStations` 純粋関数抽出、lazy initializer 化 | +| `components/ActionSheetComponents/EachTrainInfoCore/hooks/useStopStationIDs.ts` | `computeStopStationIDs` 純粋関数抽出、lazy initializer 化 | + +--- + +## 将来の注意点 + +- **ActionSheet に渡すコンテンツの高さはマウント時から固定すること。** `useEffect` で後から高さを変えると `onSheetLayout` が再発火してスプリングアニメーションがリスタートする。 +- **iOS で `isModal=true` の場合、`onOpen` はアニメーション完了前に発火する。** `onOpen` の中で state 変更を行うとアニメーションが破綻する可能性がある。 +- **Reanimated スプリングは UIスレッドで動くため `InteractionManager.runAfterInteractions()` では待てない。** 代わりに `onOpen` フラグでゲートする。 From 58ce5fa2b5df6a3294bf2edfb07b3681eb6c7f44 Mon Sep 17 00:00:00 2001 From: harukin-expo-dev-env Date: Wed, 8 Apr 2026 05:20:52 +0000 Subject: [PATCH 09/21] =?UTF-8?q?feat:=20=E3=83=81=E3=83=A5=E3=83=BC?= =?UTF-8?q?=E3=83=88=E3=83=AA=E3=82=A2=E3=83=AB=E6=A9=9F=E8=83=BD=E3=81=AE?= =?UTF-8?q?=E8=A8=AD=E8=A8=88=E6=A1=88=E3=82=92=E8=BF=BD=E5=8A=A0=E3=81=97?= =?UTF-8?q?=E3=80=81=E3=83=A6=E3=83=BC=E3=82=B6=E3=83=BC=E3=81=AE=E5=88=9D?= =?UTF-8?q?=E5=9B=9E=E4=BD=93=E9=A8=93=E3=82=92=E6=94=B9=E5=96=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/tutorial-feature-plan.md | 194 ++++++++++++++++++++++++++++++++++ 1 file changed, 194 insertions(+) create mode 100644 docs/tutorial-feature-plan.md diff --git a/docs/tutorial-feature-plan.md b/docs/tutorial-feature-plan.md new file mode 100644 index 0000000..f4acfab --- /dev/null +++ b/docs/tutorial-feature-plan.md @@ -0,0 +1,194 @@ +# チュートリアル機能 設計案 + +## 概要 + +JR四国非公式アプリにチュートリアル機能を追加し、ユーザーの初回体験を改善する。 + +--- + +## 案1: 初回起動ウォークスルー(オンボーディング) + +初回起動時に 3〜5 画面のスワイプ式ウォークスルーを表示。 + +| 画面 | 内容 | +|------|------| +| 1 | 「JR四国の列車位置をリアルタイムで確認できます」(マップ画面のスクリーンショット) | +| 2 | 「よく使う駅をお気に入り登録しよう」(お気に入り機能の紹介) | +| 3 | 「駅名標・発車時刻表も見られます」(駅ダイヤグラム紹介) | +| 4 | 「通知設定で遅延情報を受け取れます」(通知設定への誘導) | + +**表示制御**: `AsyncStorage` の `TUTORIAL_COMPLETED` キーで管理。 + +--- + +## 案2: コーチマーク(ツールチップ型ガイド) + +各画面で初めてアクセスした際に、UIパーツをハイライト+吹き出しで説明。 + +- **マップ画面**: 「ピンをタップすると列車情報が見られます」「このボタンで現在地に戻れます」 +- **メニュー画面**: 「横スクロールで路線を切り替えられます」「★で駅をお気に入りに追加」 +- **設定画面**: 「アプリアイコンを変更できます」 + +**表示制御**: `COACH_MARK_{SCREEN}_SHOWN` フラグで画面ごとに制御。 + +--- + +## 案3: コンテキスト依存ヒント + +特定の操作タイミングで自動表示。 + +| トリガー | ヒント内容 | +|----------|-----------| +| お気に入り 0 件でメニュー表示 | 「駅をお気に入りに追加すると、ここからすぐアクセスできます」 | +| FeliCa 対応端末で初回起動 | 「交通系ICカードの履歴を読み取れます」 | +| 列車遅延発生時 | 「通知設定で遅延情報を自動受信できます → 設定へ」 | +| 長押し操作が可能な箇所 | 「長押しで詳細メニューが開きます」 | + +--- + +## 案4: 「使い方」セクション刷新 + +現在の `howto.tsx`(WebView)を、アプリ内ネイティブ画面に置き換え。 + +- カテゴリ別に整理(「基本操作」「お気に入り」「ウィジェット」「FeliCa」など) +- GIF/Lottie アニメーションで操作手順を視覚的に表示(Lottie は導入済み) +- 設定画面からいつでもアクセス可能 + チュートリアルリセットボタン + +--- + +## 案5: 段階的機能開放(プログレッシブ・ディスクロージャー) + +使い込むにつれて高度な機能を提案。 + +``` +初回起動 → 基本操作ガイド +3回目起動 → 「お気に入り登録してみませんか?」 +1週間後 → 「ウィジェットを設定すると便利です」 +``` + +--- + +## 実装の優先順位 + +1. **初回ウォークスルー** — 最もインパクト大、実装も比較的シンプル +2. **コンテキスト依存ヒント** — お気に入り 0 件ヒントなど簡単なものから +3. **コーチマーク** — マップ画面の操作説明に効果的 +4. **使い方セクション刷新** — 既存の howto.tsx を段階的に改善 +5. **段階的機能開放** — 長期的な改善施策 + +--- + +## 実装パターン比較: OTA vs ライブラリ追加 + +以下で詳細に比較する。 + +### パターンA: OTA配信可能(既存依存のみ) + +既にインストール済みのライブラリのみで実装。`expo-updates` 経由の OTA で即座にユーザーへ配信可能。 + +#### 利用可能な既存ライブラリ + +- `react-native-reanimated` (v4.2.1) — アニメーション全般 +- `react-native-reanimated-carousel` (v4.0.3) — スワイプ式カルーセル(ウォークスルーに最適) +- `react-native-gesture-handler` (v2.30.0) — ジェスチャー制御 +- `@gorhom/bottom-sheet` (v5) — ボトムシート型UI +- `react-native-actions-sheet` (v10.1.2) — アクションシート +- `lottie-react-native` (v7.3.1) — アニメーション素材再生 +- `react-native-svg` (v15.15.3) — SVG描画 +- `@react-native-async-storage/async-storage` — 表示状態の永続化 +- `expo-haptics` — 触覚フィードバック + +#### 設計方針 + +| 機能 | 実装方法 | +|------|----------| +| ウォークスルー | `react-native-reanimated-carousel` でページスワイプ + `reanimated` でフェードアニメーション | +| コーチマーク | 自前実装: `react-native-svg` で穴あきオーバーレイ + `View.measure()` でターゲット位置取得 | +| ヒント表示 | `@gorhom/bottom-sheet` or `react-native-actions-sheet` でスナックバー風表示 | +| アニメーション | `lottie-react-native` で手順説明アニメ | +| 状態管理 | `AsyncStorage` でフラグ管理 | + +#### 難易度 + +| 機能 | 難易度 | 工数目安 | 備考 | +|------|--------|----------|------| +| ウォークスルー | ★★☆☆☆ | 小 | carousel がそのまま使える | +| コンテキストヒント | ★★☆☆☆ | 小 | BottomSheet/ActionSheet で簡単 | +| コーチマーク | ★★★★☆ | 大 | 穴あきオーバーレイの自前実装が必要。ターゲット要素の位置計測、スクロール追従、画面回転対応など | +| 使い方画面刷新 | ★★☆☆☆ | 中 | 通常の画面実装 | +| 段階的開放 | ★★☆☆☆ | 小 | AsyncStorage カウンタ + 条件分岐 | + +#### メリット・デメリット + +- ✅ OTA即時配信可能(ストア審査不要) +- ✅ 追加依存なし、バンドルサイズ増加なし +- ❌ コーチマーク(穴あきオーバーレイ)の自前実装コストが高い +- ❌ コーチマークのエッジケース対応(ScrollView内要素、モーダル上など)が大変 + +--- + +### パターンB: ライブラリ追加(ネイティブビルド必要) + +チュートリアル専用ライブラリを導入。次回のストアビルド&審査が必要。 + +#### 追加候補ライブラリ + +| ライブラリ | 用途 | ネイティブモジュール | +|-----------|------|---------------------| +| `react-native-copilot` | コーチマーク(ステップガイド) | なし(JS のみ) | +| `react-native-spotlight-tour` | スポットライト型ガイド | なし(JS のみ) | +| `@nickcarraway/react-native-tooltip-walkthrough` | ツールチップウォークスルー | なし(JS のみ) | + +> **重要**: 上記候補はいずれも **Pure JS ライブラリ**(ネイティブモジュールなし)のため、実際には OTA 配信可能。ただし `node_modules` の変更を含むため EAS Build が推奨される場合がある。 + +#### 設計方針 + +| 機能 | 実装方法 | +|------|----------| +| ウォークスルー | パターンA と同じ(既存 carousel で十分) | +| コーチマーク | `react-native-copilot` の `CopilotProvider` + `walkthroughable()` HOC | +| ヒント表示 | パターンA と同じ | +| アニメーション | パターンA と同じ | + +#### 難易度 + +| 機能 | 難易度 | 工数目安 | 備考 | +|------|--------|----------|------| +| ウォークスルー | ★★☆☆☆ | 小 | パターンAと同じ | +| コンテキストヒント | ★★☆☆☆ | 小 | パターンAと同じ | +| コーチマーク | ★★☆☆☆ | 小 | ライブラリが位置計測・オーバーレイを処理 | +| 使い方画面刷新 | ★★☆☆☆ | 中 | パターンAと同じ | +| 段階的開放 | ★★☆☆☆ | 小 | パターンAと同じ | + +#### メリット・デメリット + +- ✅ コーチマーク実装が大幅に楽(★★★★☆ → ★★☆☆☆) +- ✅ エッジケース(位置計測、スクロール追従)をライブラリが処理 +- ❌ 新規依存追加(バンドルサイズ微増) +- ❌ ライブラリのメンテナンス状況・Expo SDK互換性リスク +- ⚠️ Pure JS ライブラリなら実質 OTA 可能だが、検証が必要 + +--- + +## 結論・推奨アプローチ + +### フェーズ1(OTA配信)— すぐ着手可能 + +1. **ウォークスルー**: `react-native-reanimated-carousel` で実装 → OTA配信 +2. **コンテキストヒント**: `BottomSheet` / `ActionSheet` で実装 → OTA配信 +3. **段階的開放ロジック**: `AsyncStorage` カウンタ → OTA配信 + +### フェーズ2(次回ビルド時)— コーチマークの判断 + +- **コーチマークが必須なら**: `react-native-copilot`(Pure JS)を追加し、次回ビルドに含める +- **コーチマーク不要 or 後回しなら**: フェーズ1だけで十分な体験改善が可能 + +### 差分まとめ + +| 観点 | パターンA(OTA) | パターンB(ライブラリ追加) | +|------|------------------|---------------------------| +| 配信速度 | 即座 | 次回ビルド待ち | +| コーチマーク難易度 | ★★★★☆ | ★★☆☆☆ | +| それ以外の難易度 | 同等 | 同等 | +| 依存リスク | なし | 低(Pure JS) | +| 推奨 | フェーズ1はこちら | コーチマーク実装時に検討 | From 4017f82b101e855fd7b166ef942e42a49f8124d7 Mon Sep 17 00:00:00 2001 From: harukin-expo-dev-env Date: Thu, 9 Apr 2026 10:41:19 +0000 Subject: [PATCH 10/21] =?UTF-8?q?fix:=20=E3=82=AD=E3=83=BC=E3=83=9C?= =?UTF-8?q?=E3=83=BC=E3=83=89=E5=9B=9E=E9=81=BF=E3=82=92Animated.timing/sp?= =?UTF-8?q?ring=E3=81=AB=E7=A7=BB=E8=A1=8C=E3=81=97=E3=80=81=E9=AB=98?= =?UTF-8?q?=E9=80=9F=E5=88=87=E6=9B=BF=E6=99=82=E3=81=AE=E4=BD=8D=E7=BD=AE?= =?UTF-8?q?=E3=81=9A=E3=82=8C=E3=81=A8=E3=82=A2=E3=83=8B=E3=83=A1=E3=83=BC?= =?UTF-8?q?=E3=82=B7=E3=83=A7=E3=83=B3=E4=B8=8D=E5=8B=95=E3=82=92=E8=A7=A3?= =?UTF-8?q?=E6=B6=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - LayoutAnimation.configureNext → Animated.timing/spring に全面移行 - iOS: Animated.spring でキーボードアニメーションに追従 - Android: Animated.timing + Easing.out(cubic) で自然な減速カーブ - measureGenRef 世代カウンタで飛行中の古い measure() コールバックを無効化 - retryTimerRef (500ms) で adjustResize の中間座標を自動訂正 - currentAnimRef で高速切替時に前アニメをキャンセル - AllTrainDiagramView / StationDiagramView を Animated.View 化 - docs: 改修資料を追加 --- components/AllTrainDiagramView.tsx | 7 +- .../StationDiagram/StationDiagramView.tsx | 7 +- docs/keyboard-animation-tuning-2026-04-09.md | 138 +++++++++++++++ lib/useKeyboardAvoid.ts | 163 +++++++++++++++--- 4 files changed, 283 insertions(+), 32 deletions(-) create mode 100644 docs/keyboard-animation-tuning-2026-04-09.md diff --git a/components/AllTrainDiagramView.tsx b/components/AllTrainDiagramView.tsx index 75aebf6..2abbae8 100644 --- a/components/AllTrainDiagramView.tsx +++ b/components/AllTrainDiagramView.tsx @@ -1,5 +1,6 @@ import React, { useState, useRef, FC } from "react"; import { + Animated, View, Text, TouchableOpacity, @@ -38,7 +39,7 @@ export const AllTrainDiagramView: FC = () => { const [useStationName, setUseStationName] = useState(false); const [useRegex, setUseRegex] = useState(false); const containerRef = useRef(null); - const { keyboardVisible: keyBoardVisible, measuredOffset: measuredPadding } = + const { keyboardVisible: keyBoardVisible, animatedOffset } = useKeyboardAvoid({ measureRef: containerRef, tabBarHeight }); const regexTextStyle = { color: fixed.textOnPrimary, @@ -197,7 +198,7 @@ export const AllTrainDiagramView: FC = () => { ); }; return ( - + { string="閉じる" style={{ display: keyBoardVisible ? "none" : "flex" }} /> - + ); }; diff --git a/components/StationDiagram/StationDiagramView.tsx b/components/StationDiagram/StationDiagramView.tsx index 3e27156..e832bdf 100644 --- a/components/StationDiagram/StationDiagramView.tsx +++ b/components/StationDiagram/StationDiagramView.tsx @@ -1,5 +1,6 @@ import { FC, useEffect, useRef, useState } from "react"; import { + Animated, View, Text, ScrollView, @@ -70,7 +71,7 @@ export const StationDiagramView: FC = ({ route }) => { const { colors, fixed } = useThemeColors(); const tabBarHeight = useBottomTabBarHeight(); const containerRef = useRef(null); - const { keyboardVisible: keyBoardVisible, measuredOffset: keyboardOffset } = + const { keyboardVisible: keyBoardVisible, animatedOffset: keyboardOffset } = useKeyboardAvoid({ measureRef: containerRef, tabBarHeight }); const [input, setInput] = useState(""); const [displayMode, setDisplayMode] = useState< @@ -274,7 +275,7 @@ export const StationDiagramView: FC = ({ route }) => { }, [currentStationDiagram, currentTrain]); return ( - @@ -613,7 +614,7 @@ export const StationDiagramView: FC = ({ route }) => { {keyBoardVisible || ( goBack()} string="閉じる" /> )} - + ); }; diff --git a/docs/keyboard-animation-tuning-2026-04-09.md b/docs/keyboard-animation-tuning-2026-04-09.md new file mode 100644 index 0000000..ecb5825 --- /dev/null +++ b/docs/keyboard-animation-tuning-2026-04-09.md @@ -0,0 +1,138 @@ +# キーボード回避アニメーション改修 (2026-04-09) + +## 概要 + +`useKeyboardAvoid` hook を `LayoutAnimation` ベースから `Animated.timing` / `Animated.spring` ベースに全面的に書き換え、以下の問題を解決した。 + +## 解決した問題 + +| 問題 | 根本原因 | 対策 | +|---|---|---| +| キーボードの開閉アニメーションが動かない | `LayoutAnimation.configureNext` を `measure()` 非同期コールバック内で呼んでいたが、New Architecture (Fabric) では `adjustResize` による commit に消費/無効化されていた | `Animated.timing` / `Animated.spring` に移行。非同期コールバック内からでも確実にアニメーションが発動する | +| 閉じ→すぐ開きで位置が壊れる | 150ms timer 発火後の `measure()` が飛行中の状態で hide イベントが来てもキャンセル不可能。古い座標のコールバックが混入していた | `measureGenRef` 世代カウンタで古い `measure()` コールバックを無効化 | +| Android で 150ms 後の `measure()` が中間座標を返す | `adjustResize` のウィンドウリサイズは非同期で 250-300ms かかるため、150ms では中間状態を拾うことがある | `retryTimerRef` による 500ms リトライで自動訂正 | + +## 変更ファイル + +### `lib/useKeyboardAvoid.ts` + +#### LayoutAnimation → Animated.timing / Animated.spring + +```typescript +// 旧: LayoutAnimation.configureNext → setState (measure() コールバック内で動作しない) +LayoutAnimation.configureNext(LAYOUT_ANIM_CONFIG); +setMeasuredOffset(offset); + +// 新: Animated.timing / Animated.spring (どのコンテキストからでも動作する) +if (Platform.OS === "ios") { + Animated.spring(animatedOffset, { + toValue, damping: 500, stiffness: 1000, mass: 3, + useNativeDriver: false, + }).start(); +} else { + Animated.timing(animatedOffset, { + toValue, duration: 250, easing: Easing.out(Easing.cubic), + useNativeDriver: false, + }).start(); +} +``` + +- iOS: `Animated.spring` でキーボードの spring アニメーションに追従 +- Android: `Animated.timing` + `Easing.out(Easing.cubic)` で自然な減速カーブ + +#### measureGenRef (世代カウンタ) + +```typescript +const measureGenRef = useRef(0); + +// show イベント: 新しい世代を発行 +const gen = ++measureGenRef.current; +doMeasure(kbInfo.screenY, kbInfo.height, gen); + +// measure() コールバック内: 古い世代なら破棄 +if (gen !== measureGenRef.current) return; + +// hide イベント: 世代をインクリメントして飛行中コールバックを無効化 +measureGenRef.current++; +``` + +#### retryTimerRef (Android リトライ) + +```typescript +// 150ms: 初回 measure (adjustResize 途中の可能性あり) +showTimerRef.current = setTimeout(() => doMeasure(..., gen), 150); + +// 500ms: リトライ (adjustResize 完了後の確定座標で自動訂正) +retryTimerRef.current = setTimeout(() => doMeasure(..., gen), 500); +``` + +#### currentAnimRef (アニメーションキャンセル) + +```typescript +const currentAnimRef = useRef(null); + +const animateTo = (toValue: number) => { + if (currentAnimRef.current) { + currentAnimRef.current.stop(); // 前のアニメをキャンセル + } + const anim = Animated.timing(animatedOffset, { ... }); + currentAnimRef.current = anim; + anim.start(({ finished }) => { + if (finished) currentAnimRef.current = null; + }); +}; +``` + +#### hide debounce + +| プラットフォーム | delay | 理由 | +|---|---|---| +| Android | 300ms | IME 切替時の hide→show 連続発火対策 | +| iOS | 50ms | rapid close→open で `animateTo(0)` が先走るのを防止 | + +### `components/AllTrainDiagramView.tsx` + +- `View` → `Animated.View` に変更 +- `measuredOffset` (plain number) → `animatedOffset` (Animated.Value) に変更 + +### `components/StationDiagram/StationDiagramView.tsx` + +- 同上 + +### `components/Menu/RailScope/SearchUnitBox.tsx` + +- 変更なし(`measuredOffset` (plain number) を引き続き使用。`position: absolute` の `bottom` に Animated.Value は不要) + +## hook の返却値 + +```typescript +interface UseKeyboardAvoidResult { + keyboardVisible: boolean; // キーボードが表示中か + keyboardHeight: number; // キーボードの生の高さ + animatedOffset: Animated.Value; // Animated.View の paddingBottom/bottom 用 + measuredOffset: number; // plain number (SearchUnitBox 等向け後方互換) +} +``` + +## タイミングまとめ (Android) + +``` +t=0ms: keyboardDidShow +t=0ms: setKeyboardVisible(true), setKeyboardHeight(kbHeight) +t=0ms: gen = ++measureGenRef.current +t=150ms: doMeasure(gen) → measure() 開始 (adjustResize 途中の可能性) +t=~165ms: measure() callback → gen チェック → animateTo(offset) +t=500ms: doMeasure(gen) リトライ → measure() 開始 (adjustResize 完了済み) +t=~515ms: measure() callback → gen チェック → animateTo(確定offset) +``` + +## タイミングまとめ (iOS) + +``` +t=0ms: keyboardWillShow +t=0ms: setKeyboardVisible(true), setKeyboardHeight(kbHeight) +t=0ms: gen = ++measureGenRef.current +t=0ms: doMeasure(gen) → measure() 開始 +t=~5ms: measure() callback → gen チェック → Animated.spring 開始 +t=~250ms: spring アニメーション完了(キーボード出現と同期) +``` diff --git a/lib/useKeyboardAvoid.ts b/lib/useKeyboardAvoid.ts index 0f97f27..a518567 100644 --- a/lib/useKeyboardAvoid.ts +++ b/lib/useKeyboardAvoid.ts @@ -1,5 +1,5 @@ import { useEffect, useRef, useState } from "react"; -import { Keyboard, LayoutAnimation, Platform } from "react-native"; +import { Animated, Easing, Keyboard, Platform } from "react-native"; interface UseKeyboardAvoidOptions { /** measure()対象のViewのref。指定するとrefの画面座標からオフセットを精密計算する */ @@ -11,22 +11,29 @@ interface UseKeyboardAvoidOptions { interface UseKeyboardAvoidResult { /** キーボードが表示中か */ keyboardVisible: boolean; - /** キーボードの生の高さ(キャッシュ済み) */ + /** キーボードの生の高さ */ keyboardHeight: number; - /** measure()またはfallbackで計算されたオフセット値(paddingBottom/bottomに使う) */ + /** + * Animated.Value によるオフセット(Animated.View の paddingBottom/bottom に使う)。 + * measure() 非同期コールバック内から Animated.timing で駆動するため + * LayoutAnimation と異なりタイミングを問わず正しくアニメーションする。 + */ + animatedOffset: Animated.Value; + /** + * 現在のオフセット数値(Animated.Value を使えない箇所向け)。 + * SearchUnitBox など position:absolute で bottom を直接指定する場合に使う。 + */ measuredOffset: number; } -const LAYOUT_ANIM_CONFIG = { - duration: 250, - update: { type: LayoutAnimation.Types.easeInEaseOut }, -}; +const ANIM_DURATION = 250; /** * キーボード回避の共通hook。 - * - Androidの偽イベント(height<100)をガード+キャッシュで対応 - * - hide→show高速切替時のデバウンス(100ms) - * - Android measure()の150ms遅延 + * - height<=0 の偽イベントをガード、キャッシュで対応 + * - iOS: keyboardWillShow/Hide(アニメーション同期)+ keyboardWillChangeFrame + * - Android: hide→show 高速切替デバウンス(300ms)+ measure() 150ms 遅延 + * - Animated.timing で paddingBottom を駆動(LayoutAnimation 廃止) */ export function useKeyboardAvoid( options: UseKeyboardAvoidOptions = {} @@ -37,15 +44,58 @@ export function useKeyboardAvoid( const [keyboardHeight, setKeyboardHeight] = useState(0); const [measuredOffset, setMeasuredOffset] = useState(0); + // 再レンダーで Value が作り直されないよう useRef で保持 + const animatedOffset = useRef(new Animated.Value(0)).current; + const showTimerRef = useRef | null>(null); const hideTimerRef = useRef | null>(null); + const retryTimerRef = useRef | null>(null); + const keyboardVisibleRef = useRef(false); const lastValidKbRef = useRef<{ height: number; screenY: number; } | null>(null); + // 実行中アニメーションの参照(高速切替時に前アニメをキャンセルするため) + const currentAnimRef = useRef(null); + // 世代カウンタ: hide/show イベント毎にインクリメントし、 + // 飛行中の古い measure() コールバックを無効化する + const measureGenRef = useRef(0); useEffect(() => { - const doMeasure = (kbScreenY: number, kbHeight: number) => { + const animateTo = (toValue: number) => { + // 前のアニメーションを明示的にキャンセルしてから新しいものを開始 + if (currentAnimRef.current) { + currentAnimRef.current.stop(); + currentAnimRef.current = null; + } + setMeasuredOffset(toValue); // SearchUnitBox など plain number が必要な箇所向け + + let anim: Animated.CompositeAnimation; + if (Platform.OS === "ios") { + // iOS: キーボードの spring アニメーションに近い挙動 + anim = Animated.spring(animatedOffset, { + toValue, + damping: 500, + stiffness: 1000, + mass: 3, + useNativeDriver: false, + }); + } else { + // Android: easeOut で自然な減速カーブ + anim = Animated.timing(animatedOffset, { + toValue, + duration: ANIM_DURATION, + easing: Easing.out(Easing.cubic), + useNativeDriver: false, + }); + } + currentAnimRef.current = anim; + anim.start(({ finished }) => { + if (finished) currentAnimRef.current = null; + }); + }; + + const doMeasure = (kbScreenY: number, kbHeight: number, gen: number) => { if (measureRef?.current) { (measureRef.current as any).measure( ( @@ -56,28 +106,39 @@ export function useKeyboardAvoid( _pageX: number, pageY: number ) => { + // 世代が変わっていれば hide/show が割り込んだ証拠 → 破棄 + if (gen !== measureGenRef.current) return; const bottomY = pageY + h; const offset = Math.max(0, bottomY - kbScreenY); - LayoutAnimation.configureNext(LAYOUT_ANIM_CONFIG); - setMeasuredOffset(offset); + // measure() コールバック内から直接 Animated.timing を起動 → OK + animateTo(offset); } ); } else { - LayoutAnimation.configureNext(LAYOUT_ANIM_CONFIG); - setMeasuredOffset( - Platform.OS === "ios" ? kbHeight - tabBarHeight : kbHeight - ); + const offset = + Platform.OS === "ios" ? kbHeight - tabBarHeight : kbHeight; + animateTo(offset); } }; - const showSubscription = Keyboard.addListener("keyboardDidShow", (e) => { + const showEventName = + Platform.OS === "ios" ? "keyboardWillShow" : "keyboardDidShow"; + const hideEventName = + Platform.OS === "ios" ? "keyboardWillHide" : "keyboardDidHide"; + + const showSubscription = Keyboard.addListener(showEventName, (e) => { if (hideTimerRef.current) { clearTimeout(hideTimerRef.current); hideTimerRef.current = null; } if (showTimerRef.current) clearTimeout(showTimerRef.current); + if (retryTimerRef.current) { + clearTimeout(retryTimerRef.current); + retryTimerRef.current = null; + } - const isValid = e.endCoordinates.height >= 100; + // height <= 0 の偽イベントは無視してキャッシュを使う + const isValid = e.endCoordinates.height > 0; const kbInfo = isValid ? { height: e.endCoordinates.height, @@ -88,35 +149,85 @@ export function useKeyboardAvoid( if (isValid) lastValidKbRef.current = kbInfo; setKeyboardVisible(true); + keyboardVisibleRef.current = true; setKeyboardHeight(kbInfo.height); if (Platform.OS === "android") { + // Android: IME が完全に表示されてから measure() する + // 世代をインクリメントしてから timer に渡す + // → timer 発火後に飛行中の measure() callback を世代で識別できる + const gen = ++measureGenRef.current; showTimerRef.current = setTimeout( - () => doMeasure(kbInfo.screenY, kbInfo.height), + () => doMeasure(kbInfo.screenY, kbInfo.height, gen), 150 ); + // adjustResize のウィンドウリサイズは非同期で 250-300ms かかる。 + // 閉じ→すぐ開き の場合、150ms では中間座標を拾うことがあるため + // 500ms 後にリトライして自動訂正する。gen チェックで陳腐化コールバックは破棄される。 + retryTimerRef.current = setTimeout( + () => doMeasure(kbInfo.screenY, kbInfo.height, gen), + 500 + ); } else { - doMeasure(kbInfo.screenY, kbInfo.height); + // iOS: keyboardWillShow のタイミングで開始すればキーボード出現と同期する + const gen = ++measureGenRef.current; + doMeasure(kbInfo.screenY, kbInfo.height, gen); } }); - const hideSubscription = Keyboard.addListener("keyboardDidHide", () => { + const hideSubscription = Keyboard.addListener(hideEventName, () => { if (showTimerRef.current) clearTimeout(showTimerRef.current); + if (retryTimerRef.current) { + clearTimeout(retryTimerRef.current); + retryTimerRef.current = null; + } + // timer 発火済みで measure() が飛行中の場合はタイマークリアでは止められない。 + // 世代をインクリメントすることで、コールバックが返っても破棄させる。 + measureGenRef.current++; + // Android: IME切替時の hide→show 連続発火に備えて 300ms debounce + // iOS: 50ms のバッファを設ける(即 0ms だと rapid close→open で animateTo(0) が + // 先に走りパディングが一瞬ゼロになる cosmetic 問題を回避) + const delay = Platform.OS === "android" ? 300 : 50; hideTimerRef.current = setTimeout(() => { - LayoutAnimation.configureNext(LAYOUT_ANIM_CONFIG); setKeyboardVisible(false); + keyboardVisibleRef.current = false; setKeyboardHeight(0); - setMeasuredOffset(0); - }, 100); + animateTo(0); + }, delay); }); + // iOS のみ: キーボード表示中のサイズ変化(絵文字切替等)に追従 + let frameChangeSubscription: ReturnType< + typeof Keyboard.addListener + > | null = null; + if (Platform.OS === "ios") { + frameChangeSubscription = Keyboard.addListener( + "keyboardWillChangeFrame", + (e) => { + if (!keyboardVisibleRef.current) return; + const kbHeight = e.endCoordinates.height; + if (kbHeight <= 0) return; + lastValidKbRef.current = { + height: kbHeight, + screenY: e.endCoordinates.screenY, + }; + setKeyboardHeight(kbHeight); + const gen = ++measureGenRef.current; + doMeasure(e.endCoordinates.screenY, kbHeight, gen); + } + ); + } + return () => { if (showTimerRef.current) clearTimeout(showTimerRef.current); if (hideTimerRef.current) clearTimeout(hideTimerRef.current); + if (retryTimerRef.current) clearTimeout(retryTimerRef.current); + if (currentAnimRef.current) currentAnimRef.current.stop(); showSubscription.remove(); hideSubscription.remove(); + frameChangeSubscription?.remove(); }; }, [measureRef, tabBarHeight]); - return { keyboardVisible, keyboardHeight, measuredOffset }; + return { keyboardVisible, keyboardHeight, animatedOffset, measuredOffset }; } From b87c6f8f71a1754535630d628665254af47df055 Mon Sep 17 00:00:00 2001 From: harukin-expo-dev-env Date: Thu, 9 Apr 2026 10:46:54 +0000 Subject: [PATCH 11/21] =?UTF-8?q?docs:=20=E3=82=AD=E3=83=BC=E3=83=9C?= =?UTF-8?q?=E3=83=BC=E3=83=89=E3=82=A2=E3=83=8B=E3=83=A1=E3=83=BC=E3=82=B7?= =?UTF-8?q?=E3=83=A7=E3=83=B3=E8=AA=BF=E6=95=B4=E3=81=AB=E9=96=A2=E3=81=99?= =?UTF-8?q?=E3=82=8B=E3=83=89=E3=82=AD=E3=83=A5=E3=83=A1=E3=83=B3=E3=83=88?= =?UTF-8?q?=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/keyboard-animation-tuning-2026-04-08.md | 28 ++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 docs/keyboard-animation-tuning-2026-04-08.md diff --git a/docs/keyboard-animation-tuning-2026-04-08.md b/docs/keyboard-animation-tuning-2026-04-08.md new file mode 100644 index 0000000..bf1cfb0 --- /dev/null +++ b/docs/keyboard-animation-tuning-2026-04-08.md @@ -0,0 +1,28 @@ +# Keyboard Animation Tuning (2026-04-08) + +## Scope +- Map search: `components/Menu/RailScope/SearchUnitBox.tsx` +- Train number search: `components/AllTrainDiagramView.tsx` +- Station diagram search: `components/StationDiagram/StationDiagramView.tsx` + +## What Was Refactored +- Extracted repeated easing/duration values into local constants in each file. +- Extracted repeated `Animated.timing` options in map search into a small helper (`runTiming`). +- Consolidated repeated `LayoutAnimation.configureNext` payload in map search into `SEARCH_LAYOUT_ANIM`. +- Cleaned indentation/readability around map search input/header block. + +## Behavioral Notes +- No intended behavior change in this refactor pass. +- Existing keyboard/search animation behavior remains as tuned earlier. + +## Known Existing Type Warnings (pre-existing) +- `react-native-vector-icons/Ionicons` missing type declaration warning in `SearchUnitBox.tsx`. +- Index signature/implicit any warnings around line color key mapping in `SearchUnitBox.tsx`. + +## If Tuning Again +- Primary knobs: + - `KEYBOARD_BOTTOM_DURATION` + - `SEARCH_MORPH_DURATION` + - `PADDING_ANIM_DURATION` + - `CLOSE_BUTTON_ANIM_DURATION` +- Current easing is unified to `Easing.inOut(Easing.ease)`. From 36be7801f627f851501fad03831f1df0fe8785db Mon Sep 17 00:00:00 2001 From: harukin-expo-dev-env Date: Fri, 10 Apr 2026 02:28:27 +0000 Subject: [PATCH 12/21] fix(SearchUnitBox): use animatedOffset with Animated.View for smooth keyboard avoidance - Replace measuredOffset (plain number) with animatedOffset (Animated.Value) so the search bar smoothly follows the keyboard instead of jumping abruptly - Wrap position:absolute container in Animated.View to accept Animated.Value as bottom - Remove LayoutAnimation.configureNext calls that conflicted with Animated.timing from useKeyboardAvoid, causing layout animation races on Android - Drop unused keyboardHeight guard (keyboardHeight > 0 ? measuredBottom : 0); animatedOffset starts at 0 and is driven by the hook's timing, so no jump Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- components/Menu/RailScope/SearchUnitBox.tsx | 31 +++++++++------------ 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/components/Menu/RailScope/SearchUnitBox.tsx b/components/Menu/RailScope/SearchUnitBox.tsx index 057a45d..8dbcf36 100644 --- a/components/Menu/RailScope/SearchUnitBox.tsx +++ b/components/Menu/RailScope/SearchUnitBox.tsx @@ -1,9 +1,9 @@ -import React, { useState } from "react"; +import React from "react"; import { TouchableOpacity, Text, View, - LayoutAnimation, + Animated, TextInput, } from "react-native"; import Ionicons from "react-native-vector-icons/Ionicons"; @@ -34,31 +34,29 @@ export const SearchUnitBox = ({ const isSearch = stationSource.type === "search"; const query = isSearch ? stationSource.query : ""; const lineId = isSearch ? stationSource.lineId : undefined; - const { keyboardHeight, measuredOffset: measuredBottom } = + const { animatedOffset } = useKeyboardAvoid({ measureRef: parentRef, tabBarHeight }); return ( <> - 0 ? measuredBottom : 0) - : 60, + bottom: isSearch ? animatedOffset : 60, right: 0, - padding: isSearch ? 5 : 10, margin: isSearch ? 0 : 10, - backgroundColor: fixed.primary, - borderRadius: isSearch ? 5 : 50, width: isSearch ? width : 50, zIndex: 1000, }} + > + { - LayoutAnimation.configureNext({ - duration: 100, - update: { type: "easeInEaseOut", springDamping: 0.6 }, - }); setStationSource({ type: "search", query: "", lineId: undefined }); }} > @@ -77,10 +75,6 @@ export const SearchUnitBox = ({ { - LayoutAnimation.configureNext({ - duration: 100, - update: { type: "easeInEaseOut", springDamping: 0.6 }, - }); closeSearch(); }} > @@ -175,6 +169,7 @@ export const SearchUnitBox = ({ )} + ); }; From 374901c9fac83fd0efde8ea473ecac5ebfafc264 Mon Sep 17 00:00:00 2001 From: harukin-expo-dev-env Date: Fri, 10 Apr 2026 09:36:09 +0000 Subject: [PATCH 13/21] =?UTF-8?q?fix:=20=E3=82=BF=E3=83=96=E3=83=90?= =?UTF-8?q?=E3=83=BC=E3=81=AE=E3=82=A2=E3=83=8B=E3=83=A1=E3=83=BC=E3=82=B7?= =?UTF-8?q?=E3=83=A7=E3=83=B3=E3=81=A8=E3=82=AD=E3=83=BC=E3=83=9C=E3=83=BC?= =?UTF-8?q?=E3=83=89=E9=9D=9E=E8=A1=A8=E7=A4=BA=E8=A8=AD=E5=AE=9A=E3=82=92?= =?UTF-8?q?=E5=89=8A=E9=99=A4=20fix:=20JRSTraInfo=E3=82=B3=E3=83=B3?= =?UTF-8?q?=E3=83=9D=E3=83=BC=E3=83=8D=E3=83=B3=E3=83=88=E3=81=AE=E5=88=9D?= =?UTF-8?q?=E6=9C=9F=E3=83=87=E3=83=BC=E3=82=BF=E8=AA=AD=E3=81=BF=E8=BE=BC?= =?UTF-8?q?=E3=81=BF=E5=87=A6=E7=90=86=E3=82=92useEffect=E3=81=A7=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Apps.tsx | 2 -- components/ActionSheetComponents/JRSTraInfo.tsx | 4 ++++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Apps.tsx b/Apps.tsx index 9c327f5..f2322d5 100644 --- a/Apps.tsx +++ b/Apps.tsx @@ -136,8 +136,6 @@ export function AppContainer() { const defaultInactive = isDark ? "#8e8e93" : "#8e8e93"; return { lazy: false, - tabBarHideOnKeyboard: Platform.OS === "android", - animation: Platform.OS === "ios" ? "none" : "shift", sceneContainerStyle: { backgroundColor: defaultBg }, tabBarActiveTintColor: (showGradient || isExtraWindowOpen) ? "white" : defaultActive, tabBarInactiveTintColor: (showGradient || isExtraWindowOpen) ? "rgba(255,255,255,0.75)" : defaultInactive, diff --git a/components/ActionSheetComponents/JRSTraInfo.tsx b/components/ActionSheetComponents/JRSTraInfo.tsx index 606c2a4..4ebfd76 100644 --- a/components/ActionSheetComponents/JRSTraInfo.tsx +++ b/components/ActionSheetComponents/JRSTraInfo.tsx @@ -36,6 +36,10 @@ export const JRSTraInfo = () => { const maxHeight = useSheetMaxHeight(); const viewShot = useRef(null); + useEffect(() => { + setLoadingDelayData(true); + }, []); + const onCapture = async () => { const url = await viewShot.current.capture(); From 1b2ba087d520d3458fcb0f3f130dcf260d14d264 Mon Sep 17 00:00:00 2001 From: harukin-expo-dev-env Date: Sat, 11 Apr 2026 04:13:45 +0000 Subject: [PATCH 14/21] =?UTF-8?q?feat:=20=E6=8A=95=E7=A8=BF=E3=82=B7?= =?UTF-8?q?=E3=82=B9=E3=83=86=E3=83=A0=E6=8E=A5=E7=B6=9A=E5=85=88=E3=81=AE?= =?UTF-8?q?=E3=83=87=E3=83=90=E3=83=83=E3=82=B0=E6=A9=9F=E8=83=BD=E3=82=92?= =?UTF-8?q?=E8=BF=BD=E5=8A=A0=E3=81=97=E3=80=81=E7=92=B0=E5=A2=83=E8=A8=AD?= =?UTF-8?q?=E5=AE=9A=E3=82=92=E7=AE=A1=E7=90=86=E3=81=A7=E3=81=8D=E3=82=8B?= =?UTF-8?q?=E3=82=88=E3=81=86=E3=81=AB=E3=81=97=E3=81=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- GeneralWebView.tsx | 85 +++++++++++++--- components/Settings/DataSourceSettings.tsx | 109 +++++++++++++++++++++ constants/storage.ts | 3 + lib/jrDataSystemEnvironment.ts | 64 ++++++++++++ 4 files changed, 246 insertions(+), 15 deletions(-) create mode 100644 lib/jrDataSystemEnvironment.ts diff --git a/GeneralWebView.tsx b/GeneralWebView.tsx index a9a19d7..9e58f67 100644 --- a/GeneralWebView.tsx +++ b/GeneralWebView.tsx @@ -1,9 +1,17 @@ -import React, { CSSProperties } from "react"; -import { Alert, BackHandler, View, ViewProps } from "react-native"; +import React from "react"; +import { Alert, BackHandler, View } from "react-native"; import { WebView } from "react-native-webview"; import { BigButton } from "./components/atom/BigButton"; import { useFocusEffect, useNavigation } from "@react-navigation/native"; import { useThemeColors } from "@/lib/theme"; +import { AS } from "./storageControl"; +import { STORAGE_KEYS } from "@/constants"; +import { + DEFAULT_JR_DATA_SYSTEM_ENV, + normalizeJrDataSystemEnvironment, + rewriteJrDataSystemUrl, +} from "@/lib/jrDataSystemEnvironment"; + export default ({ route }) => { if (!route.params) { return null; @@ -13,8 +21,38 @@ export default ({ route }) => { const { fixed } = useThemeColors(); const webViewRef = React.useRef(null); const [canGoBack, setCanGoBack] = React.useState(false); + const [selectedEnvironment, setSelectedEnvironment] = React.useState( + DEFAULT_JR_DATA_SYSTEM_ENV, + ); + const [resolvedUri, setResolvedUri] = React.useState(""); + const [isEnvironmentReady, setIsEnvironmentReady] = React.useState(false); const hasAlerted = React.useRef(false); + React.useEffect(() => { + let isMounted = true; + + const applyEnvironment = (value: unknown) => { + if (!isMounted) return; + const nextEnvironment = normalizeJrDataSystemEnvironment(value); + setSelectedEnvironment(nextEnvironment); + setResolvedUri( + rewriteJrDataSystemUrl( + typeof uri === "string" ? uri : "", + nextEnvironment, + ), + ); + setIsEnvironmentReady(true); + }; + + AS.getItem(STORAGE_KEYS.JR_DATA_SYSTEM_ENV) + .then(applyEnvironment) + .catch(() => applyEnvironment(DEFAULT_JR_DATA_SYSTEM_ENV)); + + return () => { + isMounted = false; + }; + }, [uri]); + useFocusEffect( React.useCallback(() => { const onHardwareBack = () => { @@ -32,12 +70,28 @@ export default ({ route }) => { ); return ( - { + {isEnvironmentReady && ( + { + if (request.isTopFrame === false) { + return true; + } + + const rewrittenUrl = rewriteJrDataSystemUrl( + request.url, + selectedEnvironment, + ); + if (rewrittenUrl !== request.url) { + setResolvedUri(rewrittenUrl); + return false; + } + return true; + }} + onNavigationStateChange={(navState) => { setCanGoBack(navState.canGoBack); if (navState.url === "https://unyohub.2pd.jp/integration/succeeded.php") { goBack(); @@ -49,13 +103,14 @@ export default ({ route }) => { } } }} - onMessage={(event) => { - const { data } = event.nativeEvent; - const { type } = JSON.parse(data); - if (type === "back") return webViewRef.current?.goBack(); - if (type === "windowClose") return goBack(); - }} - /> + onMessage={(event) => { + const { data } = event.nativeEvent; + const { type } = JSON.parse(data); + if (type === "back") return webViewRef.current?.goBack(); + if (type === "windowClose") return goBack(); + }} + /> + )} {useExitButton && } ); diff --git a/components/Settings/DataSourceSettings.tsx b/components/Settings/DataSourceSettings.tsx index 88dc093..e827752 100644 --- a/components/Settings/DataSourceSettings.tsx +++ b/components/Settings/DataSourceSettings.tsx @@ -8,6 +8,12 @@ import { AS } from "../../storageControl"; import { STORAGE_KEYS } from "@/constants"; import { useTrainMenu } from "@/stateBox/useTrainMenu"; import { useThemeColors } from "@/lib/theme"; +import { + DEFAULT_JR_DATA_SYSTEM_ENV, + JR_DATA_SYSTEM_ENV_OPTIONS, + JrDataSystemEnvironmentKey, + normalizeJrDataSystemEnvironment, +} from "@/lib/jrDataSystemEnvironment"; const HUB_LOGO_PNG = require("@/assets/relationLogo/unyohub_logo.webp"); const ELESITE_LOGO_PNG = require("@/assets/relationLogo/elesite_logo.png"); @@ -161,8 +167,11 @@ export const DataSourceSettings = () => { const { dataSourcePermission, updatePermission } = useTrainMenu(); const { colors, fixed } = useThemeColors(); const canUseElesite = updatePermission || dataSourcePermission.elesite; + const showDebugSelector = __DEV__ || updatePermission; const [useUnyohub, setUseUnyohub] = useState(false); const [useElesite, setUseElesite] = useState(false); + const [jrDataSystemEnv, setJrDataSystemEnv] = + useState(DEFAULT_JR_DATA_SYSTEM_ENV); useEffect(() => { AS.getItem(STORAGE_KEYS.USE_UNYOHUB).then((value) => { @@ -171,6 +180,13 @@ export const DataSourceSettings = () => { AS.getItem(STORAGE_KEYS.USE_ELESITE).then((value) => { setUseElesite(value === true || value === "true"); }); + AS.getItem(STORAGE_KEYS.JR_DATA_SYSTEM_ENV) + .then((value) => { + setJrDataSystemEnv(normalizeJrDataSystemEnvironment(value)); + }) + .catch(() => { + setJrDataSystemEnv(DEFAULT_JR_DATA_SYSTEM_ENV); + }); }, []); const handleToggleUnyohub = (value: boolean) => { @@ -184,6 +200,11 @@ export const DataSourceSettings = () => { AS.setItem(STORAGE_KEYS.USE_ELESITE, value.toString()); }; + const handleSelectJrDataSystemEnv = (value: JrDataSystemEnvironmentKey) => { + setJrDataSystemEnv(value); + AS.setItem(STORAGE_KEYS.JR_DATA_SYSTEM_ENV, value); + }; + return ( { データの正確性は保証されません。また、これらの連携情報を利用する時点でそれぞれのサイトの利用規約に同意したものとします。{"\n\n"}外部ソースはJR四国非公式アプリが管理していないデータであるため、お問い合わせは各サービスの窓口までお願いいたします。 + + {showDebugSelector && ( + + デバッグ: 投稿システム接続先 + + 列車情報・編成投稿画面を、本番 / ChatGPT案 / Claude案で切り替えます。 + + + {JR_DATA_SYSTEM_ENV_OPTIONS.map((option) => { + const selected = jrDataSystemEnv === option.key; + return ( + handleSelectJrDataSystemEnv(option.key)} + activeOpacity={0.8} + > + + {option.label} + + + {option.caption} + + + ); + })} + + 現在の接続先: {JR_DATA_SYSTEM_ENV_OPTIONS.find((option) => option.key === jrDataSystemEnv)?.baseUrl} + + )} ); @@ -403,4 +474,42 @@ const styles = StyleSheet.create({ color: "#856404", lineHeight: 18, }, + debugSection: { + borderRadius: 12, + borderWidth: 1, + padding: 14, + gap: 10, + }, + debugTitle: { + fontSize: 15, + fontWeight: "bold", + }, + debugDescription: { + fontSize: 12, + lineHeight: 18, + }, + debugOptionRow: { + flexDirection: "row", + flexWrap: "wrap", + gap: 8, + }, + debugOptionButton: { + minWidth: 96, + borderRadius: 10, + borderWidth: 1, + paddingHorizontal: 12, + paddingVertical: 10, + gap: 2, + }, + debugOptionTitle: { + fontSize: 13, + fontWeight: "bold", + }, + debugOptionCaption: { + fontSize: 10, + }, + debugCurrentText: { + fontSize: 11, + lineHeight: 16, + }, }); diff --git a/constants/storage.ts b/constants/storage.ts index 8029e43..c1538b3 100644 --- a/constants/storage.ts +++ b/constants/storage.ts @@ -91,6 +91,9 @@ export const STORAGE_KEYS = { /** えれサイト使用設定 */ USE_ELESITE: 'useElesite', + /** 投稿システム接続先(デバッグ用) */ + JR_DATA_SYSTEM_ENV: 'jrDataSystemEnv', + /** えれサイトデータ */ ELESITE_DATA: 'elesiteData', diff --git a/lib/jrDataSystemEnvironment.ts b/lib/jrDataSystemEnvironment.ts new file mode 100644 index 0000000..3e8272b --- /dev/null +++ b/lib/jrDataSystemEnvironment.ts @@ -0,0 +1,64 @@ +export const JR_DATA_SYSTEM_ENVS = { + production: { + label: "本番", + caption: "現在の本番環境", + baseUrl: "https://jr-shikoku-data-system.pages.dev", + }, + chatgpt: { + label: "ChatGPT", + caption: "experiment-ux-refactoring-co-3crz", + baseUrl: + "https://experiment-ux-refactoring-co-3crz.jr-shikoku-data-system.pages.dev", + }, + claude: { + label: "Claude", + caption: "experiment-ux-refactoring-co-6cw7", + baseUrl: + "https://experiment-ux-refactoring-co-6cw7.jr-shikoku-data-system.pages.dev", + }, +} as const; + +export type JrDataSystemEnvironmentKey = keyof typeof JR_DATA_SYSTEM_ENVS; + +export const DEFAULT_JR_DATA_SYSTEM_ENV: JrDataSystemEnvironmentKey = + "production"; + +export const JR_DATA_SYSTEM_ENV_OPTIONS = ( + Object.entries(JR_DATA_SYSTEM_ENVS) as [ + JrDataSystemEnvironmentKey, + (typeof JR_DATA_SYSTEM_ENVS)[JrDataSystemEnvironmentKey], + ][] +).map(([key, value]) => ({ + key, + ...value, +})); + +export const normalizeJrDataSystemEnvironment = ( + value: unknown, +): JrDataSystemEnvironmentKey => { + if (typeof value === "string" && value in JR_DATA_SYSTEM_ENVS) { + return value as JrDataSystemEnvironmentKey; + } + return DEFAULT_JR_DATA_SYSTEM_ENV; +}; + +export const rewriteJrDataSystemUrl = ( + uri: string, + environment: unknown, +): string => { + if (typeof uri !== "string" || uri.length === 0) { + return uri; + } + + const envKey = normalizeJrDataSystemEnvironment(environment); + if (envKey === DEFAULT_JR_DATA_SYSTEM_ENV) { + return uri; + } + + const productionBaseUrl = JR_DATA_SYSTEM_ENVS.production.baseUrl; + const targetBaseUrl = JR_DATA_SYSTEM_ENVS[envKey].baseUrl; + + return uri.startsWith(productionBaseUrl) + ? uri.replace(productionBaseUrl, targetBaseUrl) + : uri; +}; From 07399f4b4ee1301955de7d71729dd676203de8de Mon Sep 17 00:00:00 2001 From: harukin-expo-dev-env Date: Sun, 12 Apr 2026 08:18:33 +0000 Subject: [PATCH 15/21] fix(HeaderText): update todayOperation to use allTodayOperation for accurate state filtering fix(TrainIconStatus): add cache option to fetch request for improved data handling --- .../ActionSheetComponents/EachTrainInfoCore/HeaderText.tsx | 7 +++---- .../EachTrainInfoCore/trainIconStatus.tsx | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/components/ActionSheetComponents/EachTrainInfoCore/HeaderText.tsx b/components/ActionSheetComponents/EachTrainInfoCore/HeaderText.tsx index 72702f3..cc15aec 100644 --- a/components/ActionSheetComponents/EachTrainInfoCore/HeaderText.tsx +++ b/components/ActionSheetComponents/EachTrainInfoCore/HeaderText.tsx @@ -161,9 +161,8 @@ export const HeaderText: FC = ({ } }, [trainData, trainNum, allCustomTrainData]); - const todayOperation = getTodayOperationByTrainId(trainNum).filter( - (d) => d.state !== 100, - ); + const allTodayOperation = getTodayOperationByTrainId(trainNum); + const todayOperation = allTodayOperation.filter((d) => d.state !== 100); let iconTrainDirection = parseInt(trainNum.replace(/[^\d]/g, "")) % 2 == 0 ? true : false; @@ -231,7 +230,7 @@ export const HeaderText: FC = ({ data={data} navigate={navigate} from={from} - todayOperation={todayOperation} + todayOperation={allTodayOperation} direction={iconTrainDirection} /> diff --git a/components/ActionSheetComponents/EachTrainInfoCore/trainIconStatus.tsx b/components/ActionSheetComponents/EachTrainInfoCore/trainIconStatus.tsx index c291c90..9dcbb5f 100644 --- a/components/ActionSheetComponents/EachTrainInfoCore/trainIconStatus.tsx +++ b/components/ActionSheetComponents/EachTrainInfoCore/trainIconStatus.tsx @@ -132,7 +132,7 @@ export const TrainIconStatus: FC = (props) => { fetch( `https://n8n.haruk.in/webhook/${anpanmanApiPath}?trainNum=${ data.trainNum - }&month=${dayjs().format("M")}&day=${dayjs().format("D")}` + }&month=${dayjs().format("M")}&day=${dayjs().format("D")}`,{ cache: "no-store" } ) .then((d) => d.json()) .then((d) => { From 76a617cde632ebca36da29a3a0f402541cc77191 Mon Sep 17 00:00:00 2001 From: harukin-expo-dev-env Date: Sun, 12 Apr 2026 08:18:59 +0000 Subject: [PATCH 16/21] fix(TrainDataView): update onLongPress condition to check currentTrainData instead of onLine --- .../ActionSheetComponents/EachTrainInfo/TrainDataView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/ActionSheetComponents/EachTrainInfo/TrainDataView.tsx b/components/ActionSheetComponents/EachTrainInfo/TrainDataView.tsx index ae6e89c..f5363d8 100644 --- a/components/ActionSheetComponents/EachTrainInfo/TrainDataView.tsx +++ b/components/ActionSheetComponents/EachTrainInfo/TrainDataView.tsx @@ -164,7 +164,7 @@ export const TrainDataView:FC = ({ //disabled={!onLine} //onLongPress={openEditWindow} onLongPress={()=>{ - if (!onLine) return; + if (!currentTrainData) return; setInjectData({ type:"train", value:currentTrainData?.num, fixed:true}); stackAwareNavigate("positions"); SheetManager.hide("EachTrainInfo"); From a09ba456993d9f0854aa48beb8805a6fff4c0f96 Mon Sep 17 00:00:00 2001 From: harukin-expo-dev-env Date: Sun, 12 Apr 2026 10:02:00 +0000 Subject: [PATCH 17/21] fix(ExGridView): remove zoom scale properties from Animated.ScrollView --- components/StationDiagram/ExGridView.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/components/StationDiagram/ExGridView.tsx b/components/StationDiagram/ExGridView.tsx index 9a1dc2f..2e4c692 100644 --- a/components/StationDiagram/ExGridView.tsx +++ b/components/StationDiagram/ExGridView.tsx @@ -308,8 +308,6 @@ export const ExGridView: FC<{ i * 2) : [] From ff908414116aeb75e5d735ca184c21c1a7d17ad3 Mon Sep 17 00:00:00 2001 From: harukin-expo-dev-env Date: Mon, 13 Apr 2026 07:47:25 +0000 Subject: [PATCH 18/21] Add new logo image for Elesite to relationLogo assets --- assets/relationLogo/elesite_logo.jpg | Bin 0 -> 15900 bytes .../TrainDataSources.tsx | 2 +- components/Menu/RailScope/SearchUnitBox.tsx | 31 ++++++++++-------- components/Settings/DataSourceSettings.tsx | 2 +- lib/webViewInjectjavascript.ts | 2 +- 5 files changed, 21 insertions(+), 16 deletions(-) create mode 100644 assets/relationLogo/elesite_logo.jpg diff --git a/assets/relationLogo/elesite_logo.jpg b/assets/relationLogo/elesite_logo.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c0fe942e5542beddd58268a55e00d7f3be2ef807 GIT binary patch literal 15900 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 literal 0 HcmV?d00001 diff --git a/components/ActionSheetComponents/TrainDataSources.tsx b/components/ActionSheetComponents/TrainDataSources.tsx index 29ad41a..30ceaa2 100644 --- a/components/ActionSheetComponents/TrainDataSources.tsx +++ b/components/ActionSheetComponents/TrainDataSources.tsx @@ -51,7 +51,7 @@ export type TrainDataSourcesPayload = { }; const HUB_LOGO_PNG = require("@/assets/relationLogo/unyohub_logo.webp"); -const ELESITE_LOGO_PNG = require("@/assets/relationLogo/elesite_logo.png"); +const ELESITE_LOGO_PNG = require("@/assets/relationLogo/elesite_logo.jpg"); /** ISO 8601 日時文字列を "HH:MM" 形式にフォーマット */ const formatHHMM = (iso: string): string => { diff --git a/components/Menu/RailScope/SearchUnitBox.tsx b/components/Menu/RailScope/SearchUnitBox.tsx index 8dbcf36..057a45d 100644 --- a/components/Menu/RailScope/SearchUnitBox.tsx +++ b/components/Menu/RailScope/SearchUnitBox.tsx @@ -1,9 +1,9 @@ -import React from "react"; +import React, { useState } from "react"; import { TouchableOpacity, Text, View, - Animated, + LayoutAnimation, TextInput, } from "react-native"; import Ionicons from "react-native-vector-icons/Ionicons"; @@ -34,29 +34,31 @@ export const SearchUnitBox = ({ const isSearch = stationSource.type === "search"; const query = isSearch ? stationSource.query : ""; const lineId = isSearch ? stationSource.lineId : undefined; - const { animatedOffset } = + const { keyboardHeight, measuredOffset: measuredBottom } = useKeyboardAvoid({ measureRef: parentRef, tabBarHeight }); return ( <> - 0 ? measuredBottom : 0) + : 60, right: 0, + padding: isSearch ? 5 : 10, margin: isSearch ? 0 : 10, + backgroundColor: fixed.primary, + borderRadius: isSearch ? 5 : 50, width: isSearch ? width : 50, zIndex: 1000, }} - > - { + LayoutAnimation.configureNext({ + duration: 100, + update: { type: "easeInEaseOut", springDamping: 0.6 }, + }); setStationSource({ type: "search", query: "", lineId: undefined }); }} > @@ -75,6 +77,10 @@ export const SearchUnitBox = ({ { + LayoutAnimation.configureNext({ + duration: 100, + update: { type: "easeInEaseOut", springDamping: 0.6 }, + }); closeSearch(); }} > @@ -169,7 +175,6 @@ export const SearchUnitBox = ({ )} - ); }; diff --git a/components/Settings/DataSourceSettings.tsx b/components/Settings/DataSourceSettings.tsx index 88dc093..cfaf226 100644 --- a/components/Settings/DataSourceSettings.tsx +++ b/components/Settings/DataSourceSettings.tsx @@ -10,7 +10,7 @@ import { useTrainMenu } from "@/stateBox/useTrainMenu"; import { useThemeColors } from "@/lib/theme"; const HUB_LOGO_PNG = require("@/assets/relationLogo/unyohub_logo.webp"); -const ELESITE_LOGO_PNG = require("@/assets/relationLogo/elesite_logo.png"); +const ELESITE_LOGO_PNG = require("@/assets/relationLogo/elesite_logo.jpg"); /* ------------------------------------------------------------------ */ /* DataSourceAccordionCard */ /* ------------------------------------------------------------------ */ diff --git a/lib/webViewInjectjavascript.ts b/lib/webViewInjectjavascript.ts index 2dbb2ef..0119594 100644 --- a/lib/webViewInjectjavascript.ts +++ b/lib/webViewInjectjavascript.ts @@ -859,7 +859,7 @@ export const injectJavascriptData = ({ if(hasElesite) { const elesiteOffsetPx = _blueOffset + (hasUnyohub ? 20 : 0); const offsetStyle = badgeVerticalPos + ":" + elesiteOffsetPx + "px;"; - badgeHtml += "
E
"; + badgeHtml += "
"; } 行き先情報.insertAdjacentHTML('beforebegin', "
" + badgeHtml + "

" + (TrainNumberOverride ? TrainNumberOverride : TrainNumber) + "

" + (isWanman ? "ワンマン " : "") + "

" + viaData + "

" + optionalText + "

" + trainName + "

" + (ToData ? ToData + "行" : ToData) + "

" + trainType + "

" + (hasProblem ? "‼️停止中‼️" : "") + "

"); From 5a1430d84960b4e35b562c06c07ed13d3be09c79 Mon Sep 17 00:00:00 2001 From: harukin-expo-dev-env Date: Tue, 14 Apr 2026 13:55:37 +0000 Subject: [PATCH 19/21] fix(HeaderText): update todayOperation prop to filter out completed operations --- .../ActionSheetComponents/EachTrainInfoCore/HeaderText.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/ActionSheetComponents/EachTrainInfoCore/HeaderText.tsx b/components/ActionSheetComponents/EachTrainInfoCore/HeaderText.tsx index cc15aec..3648fda 100644 --- a/components/ActionSheetComponents/EachTrainInfoCore/HeaderText.tsx +++ b/components/ActionSheetComponents/EachTrainInfoCore/HeaderText.tsx @@ -230,7 +230,7 @@ export const HeaderText: FC = ({ data={data} navigate={navigate} from={from} - todayOperation={allTodayOperation} + todayOperation={todayOperation} direction={iconTrainDirection} /> From 3ecb301e8276ca20ce9af849e5d808850653a0ab Mon Sep 17 00:00:00 2001 From: harukin-expo-dev-env Date: Sun, 19 Apr 2026 11:09:13 +0000 Subject: [PATCH 20/21] fix: update WebView navigation and adjust interval timing in CurrentTrainProvider --- GeneralWebView.tsx | 2 +- components/Settings/DataSourceSettings.tsx | 13 +++++---- lib/jrDataSystemEnvironment.ts | 12 ++++---- lib/stackOption.ts | 33 +++++++++++++++++++++- lib/webViewInjectjavascript.ts | 2 +- stateBox/useCurrentTrain.tsx | 11 ++------ 6 files changed, 49 insertions(+), 24 deletions(-) diff --git a/GeneralWebView.tsx b/GeneralWebView.tsx index 9e58f67..080c049 100644 --- a/GeneralWebView.tsx +++ b/GeneralWebView.tsx @@ -94,7 +94,7 @@ export default ({ route }) => { onNavigationStateChange={(navState) => { setCanGoBack(navState.canGoBack); if (navState.url === "https://unyohub.2pd.jp/integration/succeeded.php") { - goBack(); + webViewRef.current?.goBack(); if (!hasAlerted.current) { hasAlerted.current = true; Alert.alert("鉄道運用HUBへの投稿完了", "運用HUBからのこのアプリへのデータ反映には暫く時間がかかりますので、しばらくお待ちください。", [ diff --git a/components/Settings/DataSourceSettings.tsx b/components/Settings/DataSourceSettings.tsx index c929204..2b73843 100644 --- a/components/Settings/DataSourceSettings.tsx +++ b/components/Settings/DataSourceSettings.tsx @@ -147,16 +147,17 @@ const DataSourceAccordionCard: React.FC = ({ /* 定数 */ /* ------------------------------------------------------------------ */ const UNYOHUB_FEATURES: Feature[] = [ - { icon: "calendar-today", label: "運用データ", text: "本日・過去数日から投稿があった運用の継続予測運用情報を表示" }, + { icon: "calendar-today", label: "運用データ", text: "当日・過去数日から投稿があった運用の継続予測運用情報を表示" }, { icon: "map-outline", label: "対象エリア", text: "JR四国全線" }, - { icon: "train", label: "対象運用", text: "JR四国管内営業列車及び貨物列車に対応、臨時列車/突発運用は非対応" }, - { icon: "plus", label: "追加機能", text: "前日、当日、翌日の運用の投稿が可能" }, + { icon: "train", label: "対象運用", text: "JR四国管内営業列車及び貨物列車,定期回送列車に対応、臨時列車/突発運用は非対応" }, + { icon: "pencil", label: "入力方式", text: "アプリ内連携システムにて当日の運用の投稿が可能" }, ]; const ELESITE_FEATURES: Feature[] = [ - { icon: "calendar-today", label: "運用データ", text: "当日に報告のあった運用情報のみ表示" }, - { icon: "map-outline", label: "対象エリア", text: "予讃線/瀬戸大橋線(なお直通している特急などの列番は含みます)" }, + { icon: "calendar-today", label: "運用データ", text: "当日報告のあった運用情報のみ表示" }, + { icon: "map-outline", label: "対象エリア", text: "予讃線/瀬戸大橋線(直通している特急などの列番は含みます)" }, { icon: "train", label: "対象運用", text: "JR四国管内営業列車対応、臨時列車/突発運用は非対応" }, + { icon: "pencil", label: "入力方式", text: "アプリ外リンク連携にて当日の運用の投稿が可能" }, ]; /* ------------------------------------------------------------------ */ @@ -242,7 +243,7 @@ export const DataSourceSettings = () => { enabled={useElesite} onToggle={handleToggleElesite} description={ - "えれサイトは、鉄道運用情報を共有するためのサイトです。皆様からの投稿を通じて、鉄道運行に関する情報を共有するサイトです。JR 四国の特急・普通列車を中心に対応しています。\n\nデータがある列車では地図上に緑色の「E」バッジが表示され、列車情報画面の編成表示も更新されます。" + "えれサイトは、鉄道の運用情報を利用者同士で共有するサービスです。皆様からの投稿をもとに、列車のリアルタイムな動きを反映しています。JR四国の特急・普通列車をはじめ、現在は全国の路線に対応しています。\n\nデータがある列車では地図上にアイコンでマークが表示され、列車情報画面の編成表示も更新されます。" } features={ELESITE_FEATURES} linkLabel="elesite-next.com を開く" diff --git a/lib/jrDataSystemEnvironment.ts b/lib/jrDataSystemEnvironment.ts index 3e8272b..1a654d9 100644 --- a/lib/jrDataSystemEnvironment.ts +++ b/lib/jrDataSystemEnvironment.ts @@ -4,12 +4,12 @@ export const JR_DATA_SYSTEM_ENVS = { caption: "現在の本番環境", baseUrl: "https://jr-shikoku-data-system.pages.dev", }, - chatgpt: { - label: "ChatGPT", - caption: "experiment-ux-refactoring-co-3crz", - baseUrl: - "https://experiment-ux-refactoring-co-3crz.jr-shikoku-data-system.pages.dev", - }, + // chatgpt: { + // label: "ChatGPT", + // caption: "experiment-ux-refactoring-co-3crz", + // baseUrl: + // "https://experiment-ux-refactoring-co-3crz.jr-shikoku-data-system.pages.dev", + // }, claude: { label: "Claude", caption: "experiment-ux-refactoring-co-6cw7", diff --git a/lib/stackOption.ts b/lib/stackOption.ts index 2d570e7..ff79c39 100644 --- a/lib/stackOption.ts +++ b/lib/stackOption.ts @@ -1,8 +1,39 @@ -import { TransitionPresets } from "@react-navigation/stack"; +import { Platform } from "react-native"; +import { + CardStyleInterpolators, + TransitionPresets, +} from "@react-navigation/stack"; +import type { StackCardInterpolationProps } from "@react-navigation/stack"; + +/** + * Android用: モーダルのスライドアップはそのまま維持しつつ、 + * 背景カードへのアニメーション(scale, borderRadius等)を無効化する。 + * Android で背景カードのアニメーションが描画の乱れを引き起こすため。 + */ +const forModalPresentationAndroid = ( + props: StackCardInterpolationProps +) => { + const result = CardStyleInterpolators.forModalPresentationIOS(props); + + // 背景カード(next が存在する)にはスタイル変更を適用しない + if (props.next) { + return { + cardStyle: {}, + overlayStyle: result.overlayStyle, + }; + } + + return result; +}; + export const optionData = { gestureEnabled: true, ...TransitionPresets.ModalPresentationIOS, + ...(Platform.OS === "android" && { + cardStyleInterpolator: forModalPresentationAndroid, + }), cardOverlayEnabled: true, headerTransparent: true, headerShown: false, + detachPreviousScreen: false, }; diff --git a/lib/webViewInjectjavascript.ts b/lib/webViewInjectjavascript.ts index 0119594..db3248d 100644 --- a/lib/webViewInjectjavascript.ts +++ b/lib/webViewInjectjavascript.ts @@ -859,7 +859,7 @@ export const injectJavascriptData = ({ if(hasElesite) { const elesiteOffsetPx = _blueOffset + (hasUnyohub ? 20 : 0); const offsetStyle = badgeVerticalPos + ":" + elesiteOffsetPx + "px;"; - badgeHtml += "
"; + badgeHtml += "
"; } 行き先情報.insertAdjacentHTML('beforebegin', "
" + badgeHtml + "

" + (TrainNumberOverride ? TrainNumberOverride : TrainNumber) + "

" + (isWanman ? "ワンマン " : "") + "

" + viaData + "

" + optionalText + "

" + trainName + "

" + (ToData ? ToData + "行" : ToData) + "

" + trainType + "

" + (hasProblem ? "‼️停止中‼️" : "") + "

"); diff --git a/stateBox/useCurrentTrain.tsx b/stateBox/useCurrentTrain.tsx index 5b56be1..009a57b 100644 --- a/stateBox/useCurrentTrain.tsx +++ b/stateBox/useCurrentTrain.tsx @@ -141,14 +141,7 @@ export const CurrentTrainProvider: FC = ({ children }) => { } else { inject(`setReload()`); } - }, 15000, false, !!fixedPosition.type); - useEffect(() => { - if (fixedPosition?.type) { - setIntervalState.start(); - } else { - setIntervalState.stop(); - } - }, [fixedPosition]); + }, 15000, true, false); type getPositionFuncType = ( currentTrainData: trainDataType @@ -327,7 +320,7 @@ export const CurrentTrainProvider: FC = ({ children }) => { const [_0, _1] = useInterval(() => { getCurrentTrain(); - }, 10000, true, !!fixedPosition.type); //10秒毎に全在線列車取得 + }, 15000, true, false); //15秒毎に全在線列車取得 return ( Date: Sat, 25 Apr 2026 07:34:10 +0000 Subject: [PATCH 21/21] fix: enhance ListViewItem cycling animation and update train pair mapping in BusAndTrainDataProvider --- components/StationDiagram/ListViewItem.tsx | 13 +++++++++++-- stateBox/useBusAndTrainData.tsx | 6 ++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/components/StationDiagram/ListViewItem.tsx b/components/StationDiagram/ListViewItem.tsx index 9eb96ab..63957d0 100644 --- a/components/StationDiagram/ListViewItem.tsx +++ b/components/StationDiagram/ListViewItem.tsx @@ -2,7 +2,7 @@ import { migrateTrainName } from "@/lib/eachTrainInfoCoreLib/migrateTrainName"; import { getStringConfig } from "@/lib/getStringConfig"; import { getTrainType } from "@/lib/getTrainType"; import { useAllTrainDiagram } from "@/stateBox/useAllTrainDiagram"; -import { FC, useCallback, useEffect, useMemo, useState } from "react"; +import { FC, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { View, Text, TouchableOpacity } from "react-native"; import Animated, { useSharedValue, @@ -240,12 +240,14 @@ export const ListViewItem: FC<{ }, [showVehicle, showAppSource, d.trainNumber, getTodayOperationByTrainId, getUnyohubByTrainNumber, getElesiteByTrainNumber, isDark]); const [sourceIndex, setSourceIndex] = useState(0); + const isCyclingRef = useRef(false); const fadeAnim = useSharedValue(1); const fadeStyle = useAnimatedStyle(() => ({ opacity: fadeAnim.value, })); const advanceSource = useCallback(() => { + isCyclingRef.current = true; setSourceIndex((i) => (i + 1) % vehicleSources.length); }, [vehicleSources.length]); @@ -255,13 +257,20 @@ export const ListViewItem: FC<{ fadeAnim.value = withTiming(0, { duration: 300 }, (finished) => { if (finished) { runOnJS(advanceSource)(); - fadeAnim.value = withTiming(1, { duration: 300 }); } }); }, 3000); return () => clearInterval(cycle); }, [showVehicle, vehicleSources.length, advanceSource]); + // sourceIndex が変わった(= 新コンテンツが描画された)後にフェードインを開始 + useEffect(() => { + if (isCyclingRef.current) { + isCyclingRef.current = false; + fadeAnim.value = withTiming(1, { duration: 300 }); + } + }, [sourceIndex]); + useEffect(() => { setSourceIndex(0); fadeAnim.value = 1; diff --git a/stateBox/useBusAndTrainData.tsx b/stateBox/useBusAndTrainData.tsx index 49e8b2c..2f4aa8a 100644 --- a/stateBox/useBusAndTrainData.tsx +++ b/stateBox/useBusAndTrainData.tsx @@ -100,6 +100,12 @@ export const BusAndTrainDataProvider: FC = ({ children }) => { case "139M": returnArray.push("143M"); break; + case "4126M": + returnArray.push("5126M"); + break; + case "5126M": + returnArray.push("4126M"); + break; // 土讃線琴平界隈 case "1263M": returnArray.push("4263M");