Compare commits
72 Commits
fix/some-r
...
experiment
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c1b4adc310 | ||
|
|
c425c0917b | ||
|
|
7b7ac73169 | ||
|
|
8594e79c25 | ||
|
|
6ce2cad4e5 | ||
|
|
75795f31e9 | ||
|
|
3554233994 | ||
|
|
d71cc373c9 | ||
|
|
3587f72434 | ||
|
|
4f4d3cad0a | ||
|
|
8144e8a48a | ||
|
|
37c08ad257 | ||
|
|
b5eb830734 | ||
|
|
92f4b37861 | ||
|
|
a35956848a | ||
|
|
a809d287a2 | ||
|
|
7566910821 | ||
|
|
2cf6b679c5 | ||
|
|
1dcc25dec0 | ||
|
|
24f0c82b54 | ||
|
|
8321a47cbb | ||
|
|
59821a40d2 | ||
|
|
42473189da | ||
|
|
71e1ad8d22 | ||
|
|
170fbf0a57 | ||
|
|
899c655d8c | ||
|
|
4c1a315f5d | ||
|
|
a8c785bf7f | ||
|
|
a9668e6d51 | ||
|
|
3a4e083b9d | ||
|
|
45cc68ae56 | ||
|
|
d2e2f1d05c | ||
|
|
43f8095912 | ||
|
|
14e8e3422c | ||
|
|
fac89f6f2a | ||
|
|
331b9d64d2 | ||
|
|
94eb84b6de | ||
|
|
9fb591d6c2 | ||
|
|
3ecb301e82 | ||
|
|
16e06573a1 | ||
|
|
5a1430d849 | ||
|
|
0fa3ba6029 | ||
|
|
ff90841411 | ||
|
|
2d14758e83 | ||
|
|
a09ba45699 | ||
|
|
b3cf1c8ca3 | ||
|
|
76a617cde6 | ||
|
|
07399f4b4e | ||
|
|
1b2ba087d5 | ||
|
|
374901c9fa | ||
|
|
36be7801f6 | ||
|
|
b87c6f8f71 | ||
|
|
4017f82b10 | ||
|
|
58ce5fa2b5 | ||
|
|
4809426632 | ||
|
|
8b42644548 | ||
|
|
5914646443 | ||
|
|
a54ef7ca13 | ||
|
|
6b46c7150c | ||
|
|
9e2abc96c7 | ||
|
|
ad5357ce7f | ||
|
|
045ed21cd7 | ||
|
|
a9fef20f78 | ||
|
|
0764c17d43 | ||
|
|
b12269d35d | ||
|
|
057e595220 | ||
|
|
2be3a5c481 | ||
|
|
a3873f55be | ||
|
|
dbfbc43316 | ||
|
|
69e61b401c | ||
|
|
bed4366654 | ||
|
|
0be77a56ae |
24
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);
|
||||
}
|
||||
};
|
||||
@@ -131,10 +135,10 @@ export default function App() {
|
||||
StationListProvider,
|
||||
FavoriteStationProvider,
|
||||
TrainDelayDataProvider,
|
||||
TrainMenuProvider, // CurrentTrainProvider より先に置くことで useTrainMenu が使える
|
||||
CurrentTrainProvider,
|
||||
AreaInfoProvider,
|
||||
BusAndTrainDataProvider,
|
||||
TrainMenuProvider,
|
||||
SheetProvider,
|
||||
]);
|
||||
return (
|
||||
|
||||
12
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";
|
||||
@@ -68,9 +68,7 @@ export function AppContainer() {
|
||||
config: {
|
||||
screens: {
|
||||
positions: {
|
||||
screens: {
|
||||
Apps: "positions/apps",
|
||||
},
|
||||
screens: {},
|
||||
},
|
||||
topMenu: {
|
||||
screens: {
|
||||
@@ -124,11 +122,11 @@ 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);
|
||||
}}
|
||||
>
|
||||
{/* @ts-expect-error - Tab.Navigator type definition issue */}
|
||||
<Tab.Navigator
|
||||
initialRouteName="topMenu"
|
||||
screenOptions={({ route }) => {
|
||||
@@ -138,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,
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
import React, { CSSProperties } from "react";
|
||||
import { Alert, BackHandler, View, ViewProps } from "react-native";
|
||||
import React from "react";
|
||||
import { Alert, ActivityIndicator, AppState, AppStateStatus, BackHandler, StyleSheet, Text, TouchableOpacity, View } from "react-native";
|
||||
import { WebView } from "react-native-webview";
|
||||
import { MaterialCommunityIcons } from "@expo/vector-icons";
|
||||
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,6 +22,88 @@ export default ({ route }) => {
|
||||
const { fixed } = useThemeColors();
|
||||
const webViewRef = React.useRef<WebView>(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);
|
||||
const [isLoading, setIsLoading] = React.useState(true);
|
||||
const [hasError, setHasError] = React.useState(false);
|
||||
const [errorMessage, setErrorMessage] = React.useState("");
|
||||
// WebViewをforce remountするためのkey
|
||||
const [webViewKey, setWebViewKey] = React.useState(0);
|
||||
// バックグラウンド移行時刻の記録
|
||||
const backgroundedAt = React.useRef<number | null>(null);
|
||||
// RN-side watchdog: WebViewプロセスの死活確認用
|
||||
const lastPongAt = React.useRef<number>(Date.now());
|
||||
const isLoadingRef = React.useRef(true);
|
||||
const hasErrorRef = React.useRef(false);
|
||||
// コンテンツ消失検知用: bodyLen の最大値と連続白画面カウント
|
||||
const maxBodyLenRef = React.useRef(0);
|
||||
const blankCountRef = React.useRef(0);
|
||||
|
||||
const remount = React.useCallback(() => {
|
||||
lastPongAt.current = Date.now(); // remount直後に誤検知しないようリセット
|
||||
maxBodyLenRef.current = 0;
|
||||
blankCountRef.current = 0;
|
||||
setHasError(false);
|
||||
hasErrorRef.current = false;
|
||||
setIsLoading(true);
|
||||
isLoadingRef.current = true;
|
||||
setWebViewKey((k) => k + 1);
|
||||
}, []);
|
||||
|
||||
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]);
|
||||
|
||||
const handleReload = () => {
|
||||
lastPongAt.current = Date.now();
|
||||
setHasError(false);
|
||||
hasErrorRef.current = false;
|
||||
setIsLoading(true);
|
||||
isLoadingRef.current = true;
|
||||
setWebViewKey((k) => k + 1);
|
||||
};
|
||||
|
||||
// AppState監視: バックグラウンド10秒超で復帰したらWebViewを再マウント
|
||||
React.useEffect(() => {
|
||||
const onAppStateChange = (nextState: AppStateStatus) => {
|
||||
if (nextState.match(/inactive|background/)) {
|
||||
backgroundedAt.current = Date.now();
|
||||
} else if (nextState === "active" && backgroundedAt.current !== null) {
|
||||
const elapsed = Date.now() - backgroundedAt.current;
|
||||
backgroundedAt.current = null;
|
||||
if (elapsed > 10_000) {
|
||||
remount();
|
||||
}
|
||||
}
|
||||
};
|
||||
const subscription = AppState.addEventListener("change", onAppStateChange);
|
||||
return () => subscription.remove();
|
||||
}, [remount]);
|
||||
|
||||
useFocusEffect(
|
||||
React.useCallback(() => {
|
||||
@@ -31,28 +122,171 @@ export default ({ route }) => {
|
||||
);
|
||||
return (
|
||||
<View style={{ height: "100%", backgroundColor: fixed.primary }}>
|
||||
<WebView
|
||||
source={{ uri }}
|
||||
contentMode="mobile"
|
||||
allowsBackForwardNavigationGestures
|
||||
ref={webViewRef}
|
||||
onNavigationStateChange={(navState) => {
|
||||
{isEnvironmentReady && (
|
||||
<WebView
|
||||
key={webViewKey}
|
||||
source={{ uri: resolvedUri }}
|
||||
contentMode="mobile"
|
||||
allowsBackForwardNavigationGestures
|
||||
ref={webViewRef}
|
||||
onLoadStart={() => {
|
||||
isLoadingRef.current = true;
|
||||
setHasError(false);
|
||||
hasErrorRef.current = false;
|
||||
maxBodyLenRef.current = 0;
|
||||
blankCountRef.current = 0;
|
||||
}}
|
||||
onLoadEnd={() => {
|
||||
setIsLoading(false);
|
||||
isLoadingRef.current = false;
|
||||
lastPongAt.current = Date.now();
|
||||
}}
|
||||
onError={(syntheticEvent) => {
|
||||
const { nativeEvent } = syntheticEvent;
|
||||
setIsLoading(false);
|
||||
isLoadingRef.current = false;
|
||||
setHasError(true);
|
||||
hasErrorRef.current = true;
|
||||
setErrorMessage(nativeEvent.description || "ページを読み込めませんでした");
|
||||
}}
|
||||
onHttpError={(syntheticEvent) => {
|
||||
const { nativeEvent } = syntheticEvent;
|
||||
if (nativeEvent.statusCode >= 500) {
|
||||
setIsLoading(false);
|
||||
isLoadingRef.current = false;
|
||||
setHasError(true);
|
||||
hasErrorRef.current = true;
|
||||
setErrorMessage(`サーバーエラー (${nativeEvent.statusCode})`);
|
||||
}
|
||||
}}
|
||||
onRenderProcessGone={() => {
|
||||
// クラッシュ・メモリ回収どちらも自動remount
|
||||
remount();
|
||||
}}
|
||||
// iOS: コンテンツプロセスがメモリ圧迫で終了した場合
|
||||
onContentProcessDidTerminate={() => remount()}
|
||||
onShouldStartLoadWithRequest={(request) => {
|
||||
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);
|
||||
// SPA内遷移中は白画面誤検知を防ぐためblankCountをリセット
|
||||
if (navState.loading) blankCountRef.current = 0;
|
||||
if (navState.url === "https://unyohub.2pd.jp/integration/succeeded.php") {
|
||||
goBack();
|
||||
Alert.alert("鉄道運用HUBへの投稿完了", "運用HUBからのこのアプリへのデータ反映には暫く時間がかかりますので、しばらくお待ちください。", [
|
||||
{ text: "完了" },
|
||||
]);
|
||||
webViewRef.current?.goBack();
|
||||
if (!hasAlerted.current) {
|
||||
hasAlerted.current = true;
|
||||
Alert.alert("鉄道運用HUBへの投稿完了", "運用HUBからのこのアプリへのデータ反映には暫く時間がかかりますので、しばらくお待ちください。", [
|
||||
{ text: "完了" },
|
||||
]);
|
||||
}
|
||||
}
|
||||
}}
|
||||
onMessage={(event) => {
|
||||
const { data } = event.nativeEvent;
|
||||
const { type } = JSON.parse(data);
|
||||
if (type === "back") return webViewRef.current?.goBack();
|
||||
if (type === "windowClose") return goBack();
|
||||
}}
|
||||
/>
|
||||
{useExitButton && <BigButton onPress={goBack} string="閉じる" />}
|
||||
onMessage={(event) => {
|
||||
const { data } = event.nativeEvent;
|
||||
const parsed = JSON.parse(data);
|
||||
const { type } = parsed;
|
||||
if (type === "pong") {
|
||||
lastPongAt.current = Date.now();
|
||||
const bodyLen: number = parsed.bodyLen ?? 0;
|
||||
// innerTextベース: 最大値を更新
|
||||
if (bodyLen > maxBodyLenRef.current) maxBodyLenRef.current = bodyLen;
|
||||
// 一度200文字超の表示テキストがあった後に20文字未満になったら白画面と判定
|
||||
// SPA遷移中の一時的な空白を避けるため3回連続(15秒)で発火
|
||||
if (maxBodyLenRef.current > 200 && bodyLen < 20) {
|
||||
blankCountRef.current += 1;
|
||||
if (blankCountRef.current >= 3) {
|
||||
blankCountRef.current = 0;
|
||||
maxBodyLenRef.current = 0;
|
||||
remount();
|
||||
}
|
||||
} else {
|
||||
blankCountRef.current = 0;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (type === "back") return webViewRef.current?.goBack();
|
||||
if (type === "windowClose") return goBack();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{isLoading && !hasError && (
|
||||
<View style={wvStyles.loadingOverlay} pointerEvents="none">
|
||||
<ActivityIndicator size="large" color="#fff" />
|
||||
</View>
|
||||
)}
|
||||
{hasError && (
|
||||
<View style={wvStyles.errorOverlay}>
|
||||
<MaterialCommunityIcons name="wifi-off" size={48} color="#ccc" />
|
||||
<Text style={wvStyles.errorText}>{errorMessage}</Text>
|
||||
<TouchableOpacity style={wvStyles.reloadButton} onPress={handleReload}>
|
||||
<MaterialCommunityIcons name="reload" size={18} color="#fff" />
|
||||
<Text style={wvStyles.reloadButtonText}>再読み込み</Text>
|
||||
</TouchableOpacity>
|
||||
{useExitButton && (
|
||||
<TouchableOpacity style={wvStyles.backButton} onPress={goBack}>
|
||||
<Text style={wvStyles.backButtonText}>閉じる</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
{useExitButton && !hasError && <BigButton onPress={goBack} string="閉じる" />}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const wvStyles = StyleSheet.create({
|
||||
loadingOverlay: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
backgroundColor: "rgba(0,0,0,0.25)",
|
||||
},
|
||||
errorOverlay: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
backgroundColor: "#1a1a2e",
|
||||
gap: 16,
|
||||
paddingHorizontal: 32,
|
||||
},
|
||||
errorText: {
|
||||
color: "#aaa",
|
||||
fontSize: 14,
|
||||
textAlign: "center",
|
||||
},
|
||||
reloadButton: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
backgroundColor: "#0099CC",
|
||||
borderRadius: 10,
|
||||
paddingHorizontal: 24,
|
||||
paddingVertical: 12,
|
||||
},
|
||||
reloadButtonText: {
|
||||
color: "#fff",
|
||||
fontSize: 15,
|
||||
fontWeight: "bold",
|
||||
},
|
||||
backButton: {
|
||||
paddingHorizontal: 24,
|
||||
paddingVertical: 10,
|
||||
},
|
||||
backButtonText: {
|
||||
color: "#888",
|
||||
fontSize: 14,
|
||||
},
|
||||
});
|
||||
|
||||
15
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<any>(null);
|
||||
|
||||
return (
|
||||
<Stack.Navigator
|
||||
id={null}
|
||||
screenOptions={{ cardStyle: { backgroundColor: bgColor } }}
|
||||
screenListeners={({ navigation: stackNav }) => {
|
||||
stackNavRef.current = stackNav;
|
||||
return {};
|
||||
}}
|
||||
>
|
||||
<Stack.Screen
|
||||
name="menu"
|
||||
|
||||
@@ -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 <StatusBar barStyle={barStyle} translucent backgroundColor="transparent" />;
|
||||
return <StatusBar barStyle="light-content" translucent backgroundColor="transparent" />;
|
||||
};
|
||||
|
||||
export default StatusbarDetect;
|
||||
|
||||
19
Top.tsx
@@ -16,6 +16,7 @@ import { news } from "./config/newsUpdate";
|
||||
import { Linking, Platform } from "react-native";
|
||||
import GeneralWebView from "./GeneralWebView";
|
||||
import { StationDiagramView } from "@/components/StationDiagram/StationDiagramView";
|
||||
import { positionsStackNavRef } from "./lib/rootNavigation";
|
||||
const Stack = createStackNavigator();
|
||||
export const Top = () => {
|
||||
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 = () => {
|
||||
<Stack.Navigator
|
||||
id={null}
|
||||
screenOptions={{ cardStyle: { backgroundColor: bgColor } }}
|
||||
screenListeners={({ navigation: stackNav }) => {
|
||||
stackNavRef.current = stackNav;
|
||||
return {};
|
||||
}}
|
||||
>
|
||||
<Stack.Screen
|
||||
name="Apps"
|
||||
|
||||
160
WINDOWS_PORT.md
Normal file
@@ -0,0 +1,160 @@
|
||||
# JR四国アプリ Windows版 (React Native for Windows) 引き継ぎ資料
|
||||
|
||||
## プロジェクト概要
|
||||
|
||||
JR四国の列車走行位置・運行情報アプリ。Expo Managed Workflow (React Native) で構築。
|
||||
Android/iOS向けに運用中。このタスクは **Windows PC版の追加** を目的とする。
|
||||
|
||||
- **リポジトリ**: `/home/ubuntu/jrshikoku` (Ubuntu開発サーバー) → Windows環境に clone/コピーして作業
|
||||
- **ブランチ**: `experiment/pc-version`(mainブランチ: `master`)
|
||||
- **React Native**: 0.83.2 / **Expo**: ^55.0.8
|
||||
|
||||
---
|
||||
|
||||
## これまでの経緯
|
||||
|
||||
### 試みたこと: Electron + Expo webビルド
|
||||
|
||||
`npx expo export -p web` でWebビルドし、ElectronでラップしてCORSをメインプロセスで回避する方法を試みた。
|
||||
|
||||
**結果**: 動作はしたが根本的な問題が多く断念。
|
||||
|
||||
| 問題 | 原因 |
|
||||
|------|------|
|
||||
| `injectJavaScript` が動かない | Electron `<webview>` はクロスオリジンにJS注入できない |
|
||||
| 列車描画が壊れる | `window.ReactNativeWebView` が未定義でスクリプトがクラッシュ |
|
||||
| 操作感が悪い | WebViewがiframe/Electron webviewタグになり、ネイティブのスクロール感が失われる |
|
||||
| `usage.htm`(利用規約)が毎回出る | `injectJavascript` が動かないため `goBack()` 処理が機能しない |
|
||||
|
||||
**結論**: `react-native-webview` をネイティブとして動かせないWeb系アプローチには限界がある。
|
||||
|
||||
---
|
||||
|
||||
## 推奨アプローチ: React Native for Windows (RNW)
|
||||
|
||||
### なぜRNWが良いか
|
||||
|
||||
- `react-native-webview` が **WebView2 (Chromium)** として動く → `injectJavaScript` がモバイルと同じ挙動
|
||||
- ネイティブアプリなので CORS 制限がない
|
||||
- スクロール・タッチ感がネイティブ
|
||||
- このアプリの主要機能(走行位置WebView、API通信)がそのまま動く可能性が高い
|
||||
|
||||
### バージョン対応
|
||||
|
||||
RN 0.83.2 に対応する RNW は **0.83.x**。
|
||||
|
||||
```
|
||||
yarn add react-native-windows@0.83.x
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## セットアップ手順 (Windows)
|
||||
|
||||
### 1. 前提環境のインストール
|
||||
|
||||
[Microsoft公式ドキュメント](https://microsoft.github.io/react-native-windows/docs/rnw-dependencies) に従う。
|
||||
|
||||
必要なもの:
|
||||
- **Visual Studio 2022** (Community可)
|
||||
- ワークロード: 「C++ によるデスクトップ開発」「ユニバーサル Windows プラットフォーム開発」
|
||||
- 個別コンポーネント: 「Windows 11 SDK (10.0.22621.0 以上)」「C++ CMake ツール」
|
||||
- **Node.js** LTS
|
||||
- **Git**
|
||||
|
||||
```powershell
|
||||
# winget で一括インストール (任意)
|
||||
winget install Microsoft.VisualStudio.2022.Community
|
||||
winget install OpenJS.NodeJS.LTS
|
||||
winget install Git.Git
|
||||
```
|
||||
|
||||
### 2. リポジトリをWindowsにクローン
|
||||
|
||||
```powershell
|
||||
git clone <リポジトリURL>
|
||||
cd jrshikoku
|
||||
git checkout experiment/pc-version
|
||||
yarn install
|
||||
```
|
||||
|
||||
### 3. RNW の初期化
|
||||
|
||||
```powershell
|
||||
yarn add react-native-windows@0.83.x
|
||||
npx react-native-windows-init --overwrite
|
||||
```
|
||||
|
||||
→ `windows/` ディレクトリが生成される。
|
||||
|
||||
### 4. ビルド & 実行
|
||||
|
||||
```powershell
|
||||
npx react-native run-windows
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 予想される問題と対処
|
||||
|
||||
### Expoモジュールの非対応
|
||||
|
||||
Expo Managed Workflow のモジュールはネイティブコードを自動生成するが、RNW は公式サポート外。
|
||||
以下のモジュールはWindowsで動かない可能性が高い(クラッシュしたらスタブに差し替える):
|
||||
|
||||
| モジュール | 状況 | 対処 |
|
||||
|-----------|------|------|
|
||||
| `expo-notifications` | Windowsプッシュ通知は別実装が必要 | Platform.OS === 'windows' でスキップ |
|
||||
| `expo-felica-reader` | ハードウェア依存、Windows非対応 | スタブに差し替え |
|
||||
| `expo-live-activity` | iOS専用 | すでにPlatform.OS === 'web' ガード済み |
|
||||
| `expo-linear-gradient` | RNW向けは要確認 | react-native-linear-gradient に差し替えも |
|
||||
| `react-native-maps` | Windows未対応 | metro.config.js でスタブを当てる |
|
||||
|
||||
### Platform.OS の値
|
||||
|
||||
RNW では `Platform.OS === 'windows'`(`'web'` でも `'ios'` でもない)。
|
||||
既存の `Platform.OS === 'android'` ガードがある箇所は意図通り動く(Windowsはスキップされる)。
|
||||
|
||||
### `react-native-webview` のCORS
|
||||
|
||||
ネイティブアプリなので基本的にCORSはない。ただし JR四国の `X-Frame-Options` は
|
||||
iframeではなく WebView2 で直接開くため無関係。
|
||||
|
||||
---
|
||||
|
||||
## アプリ構成の重要ポイント
|
||||
|
||||
### 走行位置タブ (`components/Apps/WebView.tsx`)
|
||||
|
||||
最重要コンポーネント。`react-native-webview` の WebView に `train.jr-shikoku.co.jp/sp.html` を読み込み、大量の JavaScript (`injectJavascript`) を注入して列車データを描画している。
|
||||
|
||||
- `injectJavascript`: `lib/webViewInjectjavascript.ts` で生成。ユーザー設定・ダークモード等を考慮した巨大スクリプト
|
||||
- `injectJavascriptBeforeContentLoaded`: モックAPI使用時のみ、XHRインターセプター
|
||||
- `window.ReactNativeWebView.postMessage` でアプリ側に列車クリック等のイベントを送る
|
||||
|
||||
### 状態管理
|
||||
|
||||
Context API (stateBox/) で全状態を管理。Redux等は使用していない。
|
||||
|
||||
### API
|
||||
|
||||
バックエンド: `jr-shikoku-backend-api-v1.haruk.in`(外部サーバー)
|
||||
列車走行位置: JR四国公式 `train.jr-shikoku.co.jp`(WebView内で直接アクセス)
|
||||
|
||||
---
|
||||
|
||||
## Claudeへの指示テンプレート
|
||||
|
||||
```
|
||||
このプロジェクトはExpo React NativeのJR四国アプリ(RN 0.83.2)。
|
||||
experiment/pc-versionブランチで作業中。
|
||||
React Native for Windows (RNW 0.83.x) を使ってWindows PC版を作りたい。
|
||||
|
||||
まず package.json と windows/ ディレクトリの存在を確認し、
|
||||
RNW の初期化が完了しているか確認してから作業を開始してほしい。
|
||||
|
||||
主要な懸念点:
|
||||
1. expo-notifications など Expo 系モジュールが Windows ビルドを壊す可能性
|
||||
2. react-native-maps は Windows 非対応(metro.config.js でスタブを当てる予定)
|
||||
3. Platform.OS === 'windows' の分岐が必要になる箇所を確認すること
|
||||
```
|
||||
10
app.json
@@ -24,7 +24,7 @@
|
||||
"**/*"
|
||||
],
|
||||
"ios": {
|
||||
"buildNumber": "61",
|
||||
"buildNumber": "63",
|
||||
"supportsTablet": true,
|
||||
"bundleIdentifier": "jrshikokuinfo.xprocess.hrkn",
|
||||
"appleTeamId": "54CRDT797G",
|
||||
@@ -144,7 +144,7 @@
|
||||
"minWidth": "70dp",
|
||||
"minHeight": "50dp",
|
||||
"description": "JR四国列車遅延速報EXのウィジェットです。30分ごとに自動更新します。タッチすると強制更新します。",
|
||||
"previewImage": "./assets/widgetResource/JR_shikoku_train_info.png",
|
||||
"previewImage": "./assets/widgetResource/JR_shikoku_train_info.jpg",
|
||||
"updatePeriodMillis": 1800000,
|
||||
"resizeMode": "horizontal|vertical"
|
||||
},
|
||||
@@ -154,7 +154,7 @@
|
||||
"minWidth": "70dp",
|
||||
"minHeight": "50dp",
|
||||
"description": "JR四国運行情報のウィジェットです。30分ごとに自動更新します。タッチすると強制更新します。",
|
||||
"previewImage": "./assets/widgetResource/JR_shikoku_info.png",
|
||||
"previewImage": "./assets/widgetResource/JR_shikoku_info.jpg",
|
||||
"updatePeriodMillis": 1800000,
|
||||
"resizeMode": "horizontal|vertical"
|
||||
},
|
||||
@@ -164,7 +164,7 @@
|
||||
"minWidth": "70dp",
|
||||
"minHeight": "50dp",
|
||||
"description": "JR四国非公式アプリの各種リンクを表示するウィジェットです。",
|
||||
"previewImage": "./assets/widgetResource/JR_shikoku_apps_shortcut.png",
|
||||
"previewImage": "./assets/widgetResource/JR_shikoku_apps_shortcut.jpg",
|
||||
"updatePeriodMillis": 1800000,
|
||||
"resizeMode": "horizontal|vertical"
|
||||
},
|
||||
@@ -174,7 +174,7 @@
|
||||
"minWidth": "70dp",
|
||||
"minHeight": "50dp",
|
||||
"description": "Felica対応ICカードの残高をホーム画面に表示するウィジェットです。タップでスキャン画面を開きます。",
|
||||
"previewImage": "./assets/widgetResource/JR_shikoku_felica_balance.png",
|
||||
"previewImage": "./assets/widgetResource/JR_shikoku_felica_balance.jpg",
|
||||
"updatePeriodMillis": 1800000,
|
||||
"resizeMode": "horizontal|vertical"
|
||||
}
|
||||
|
||||
BIN
assets/AppStoreAssets/05ABF10D-F2F4-4BB5-AFE3-162B3F977EAF.png
Normal file
|
After Width: | Height: | Size: 469 KiB |
BIN
assets/AppStoreAssets/87940F67-0514-4430-9F75-9608B1648EF6.png
Normal file
|
After Width: | Height: | Size: 334 KiB |
BIN
assets/AppStoreAssets/9CBA4B16-A402-48F7-BF98-03A66D8C5167.png
Normal file
|
After Width: | Height: | Size: 373 KiB |
BIN
assets/AppStoreAssets/D35EE2E6-D361-46A8-965D-8FFF59079827.png
Normal file
|
After Width: | Height: | Size: 274 KiB |
|
After Width: | Height: | Size: 913 KiB |
|
After Width: | Height: | Size: 616 KiB |
|
After Width: | Height: | Size: 638 KiB |
|
After Width: | Height: | Size: 392 KiB |
@@ -1,3 +1,26 @@
|
||||
export const series: { key: string; title: string; ids: string[] }[] = [
|
||||
{ key: "32", title: "キハ32形", ids: ["32ns", "32s", "32kpuy1", "32kpuy2", "32tht", "32thtk", "32oni1", "32oni1k", "32toai", "32at"] },
|
||||
{ key: "54", title: "キハ54形", ids: ["54s", "54st", "54nany1", "54nany2", "54smek1"] },
|
||||
{ key: "40", title: "キハ40", ids: ["40s", "40w"] },
|
||||
{ key: "185", title: "キハ185系", ids: ["185mrt", "185cm", "185g", "185tu", "185tu_uzu", "185iyor", "185iyoy", "185toai", "185mm1", "185mm2", "185ym1", "185ym2", "185ap1"] },
|
||||
{ key: "1000", title: "1000形", ids: ["1000"] },
|
||||
{ key: "1200", title: "1200形・1201形", ids: ["1200", "1201"] },
|
||||
{ key: "1500", title: "1500形", ids: ["1501", "1550", "1551"] },
|
||||
{ key: "2000", title: "2000系・N2000系", ids: ["2000asi", "2000uwa", "N2000", "2000nl", "2000-3", "2000ganp1", "2002a"] },
|
||||
{ key: "2600", title: "2600系", ids: ["2600", "2600apr", "2600apb"] },
|
||||
{ key: "2700", title: "2700系", ids: ["2700", "2700asi", "2700smn", "2700uzu", "2700apy", "2700apr", "2700apy1", "2700apr1"] },
|
||||
{ key: "3600", title: "3600系", ids: ["3600"] },
|
||||
{ key: "5000", title: "5000系", ids: ["5001", "5001k"] },
|
||||
{ key: "6000", title: "6000系", ids: ["6000f"] },
|
||||
{ key: "7000", title: "7000系", ids: ["7000"] },
|
||||
{ key: "7200", title: "7200系", ids: ["7200"] },
|
||||
{ key: "8000", title: "8000系", ids: ["8000no", "8000nr", "8001nr", "8000ap", "8000nn"] },
|
||||
{ key: "8600", title: "8600系", ids: ["8600"] },
|
||||
{ key: "9000", title: "9000系", ids: ["9000"] },
|
||||
{ key: "9640", title: "9640形", ids: ["9640", "9640ht", "9640mo1", "9640mo2", "9640tyg", "9640tyb", "9640jgr", "9640jbl"] },
|
||||
{ key: "other", title: "JR西日本・他", ids: ["285", "213w", "W741", "Wk141", "EF65", "EF210a", "EF210n", "EF210n1", "EF210l3", "T45000"] },
|
||||
];
|
||||
|
||||
export default () =>{
|
||||
return [
|
||||
//{ "id": "32", "name": "キハ32形", "icon": require("./32.png") },
|
||||
|
||||
BIN
assets/relationLogo/elesite_logo.jpg
Normal file
|
After Width: | Height: | Size: 16 KiB |
@@ -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 (
|
||||
<ActionSheet
|
||||
@@ -15,10 +25,11 @@ export const EachTrainInfo = ({ payload }) => {
|
||||
drawUnderStatusBar={false}
|
||||
isModal={Platform.OS === "ios" && !Platform.isPad}
|
||||
containerStyle={{ maxHeight }}
|
||||
|
||||
onOpen={handleOpen}
|
||||
onClose={handleClose}
|
||||
//useBottomSafeAreaPadding={Platform.OS == "android"}
|
||||
>
|
||||
<EachTrainInfoCore {...{ actionSheetRef, ...payload }} />
|
||||
<EachTrainInfoCore {...{ actionSheetRef, sheetOpened, ...payload }} />
|
||||
</ActionSheet>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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 = {
|
||||
@@ -163,16 +164,16 @@ export const TrainDataView:FC<props> = ({
|
||||
//disabled={!onLine}
|
||||
//onLongPress={openEditWindow}
|
||||
onLongPress={()=>{
|
||||
if (!onLine) return;
|
||||
if (!currentTrainData) 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");
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -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<any>(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
|
||||
|
||||
@@ -59,6 +59,7 @@ export const HeaderText: FC<Props> = ({
|
||||
getUnyohubByTrainNumber,
|
||||
getUnyohubEntriesByTrainNumber,
|
||||
useUnyohub: unyohubEnabled,
|
||||
unyohubData,
|
||||
} = useUnyohub();
|
||||
const { getElesiteEntriesByTrainNumber, useElesite: elesiteEnabled } =
|
||||
useElesite();
|
||||
@@ -160,9 +161,8 @@ export const HeaderText: FC<Props> = ({
|
||||
}
|
||||
}, [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;
|
||||
@@ -171,11 +171,34 @@ export const HeaderText: FC<Props> = ({
|
||||
}
|
||||
|
||||
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<string>();
|
||||
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 +311,7 @@ export const HeaderText: FC<Props> = ({
|
||||
(SheetManager.show as any)("TrainDataSources", {
|
||||
payload: {
|
||||
trainNum,
|
||||
unyohubTrainNum: unyohubLookupNum,
|
||||
unyohubTrainNum: unyohubTrainNumForSourceScreen,
|
||||
unyohubEntries,
|
||||
elesiteEntries,
|
||||
todayOperation,
|
||||
|
||||
@@ -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<any>,
|
||||
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]);
|
||||
};
|
||||
|
||||
@@ -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<string[][]>([]);
|
||||
|
||||
// 初回レンダリング時に同期的に計算することでActionSheetのアニメーション中の高さ変化を防ぐ
|
||||
const [stopStationIDList, setStopStationIDList] = useState<string[][]>(() =>
|
||||
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;
|
||||
|
||||
@@ -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<string, any[]>
|
||||
): { 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 };
|
||||
};
|
||||
|
||||
@@ -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<string, string>) => {
|
||||
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) => {
|
||||
|
||||
@@ -132,7 +132,7 @@ export const TrainIconStatus: FC<Props> = (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) => {
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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> = (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();
|
||||
}}
|
||||
|
||||
@@ -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 => {
|
||||
@@ -83,6 +83,152 @@ const formatDateHHMM = (datetime: string): string => {
|
||||
}
|
||||
};
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* FormationChips: "+"区切りの編成名をチップ形式で表示 */
|
||||
/* ------------------------------------------------------------------ */
|
||||
const FormationChips: FC<{ text: string; color: string }> = ({ text, color }) => {
|
||||
const parts = text.split("+").map((s) => s.trim()).filter(Boolean);
|
||||
if (parts.length === 0) return null;
|
||||
return (
|
||||
<View style={{ flexDirection: "row", alignItems: "center", flexWrap: "wrap", gap: 2 }}>
|
||||
{parts.map((part, i) => (
|
||||
<React.Fragment key={i}>
|
||||
{i > 0 && (
|
||||
<Text style={{ fontSize: 11, color, fontWeight: "bold" }}>+</Text>
|
||||
)}
|
||||
<View
|
||||
style={{
|
||||
borderWidth: 1,
|
||||
borderColor: color + "66",
|
||||
borderRadius: 4,
|
||||
paddingHorizontal: 5,
|
||||
paddingVertical: 1,
|
||||
backgroundColor: color + "12",
|
||||
}}
|
||||
>
|
||||
<Text style={{ fontSize: 12, fontWeight: "700", color }} numberOfLines={1}>
|
||||
{part}
|
||||
</Text>
|
||||
</View>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* FadingSubCycler */
|
||||
/* ------------------------------------------------------------------ */
|
||||
type FadingSubItem = { label: string; datetime: string | null };
|
||||
|
||||
const FadingSubCycler: FC<{ items: FadingSubItem[]; color: string }> = ({ items, color }) => {
|
||||
const { colors } = useThemeColors();
|
||||
const [index, setIndex] = useState(0);
|
||||
const opacity = useRef(new Animated.Value(1)).current;
|
||||
|
||||
useEffect(() => {
|
||||
if (items.length <= 1) return;
|
||||
const cycle = () => {
|
||||
Animated.timing(opacity, { toValue: 0, duration: 300, useNativeDriver: true }).start(() => {
|
||||
setIndex((i) => (i + 1) % items.length);
|
||||
Animated.timing(opacity, { toValue: 1, duration: 300, useNativeDriver: true }).start();
|
||||
});
|
||||
};
|
||||
const id = setInterval(cycle, 3000);
|
||||
return () => clearInterval(id);
|
||||
}, [items.length]);
|
||||
|
||||
const item = items[index];
|
||||
return (
|
||||
<Animated.View style={{ opacity, flexDirection: "row", alignItems: "center", gap: 4 }}>
|
||||
{item.datetime ? (
|
||||
<Text style={{ fontSize: 11, color: colors.textSecondary }} numberOfLines={1}>
|
||||
{`最終投稿: ${formatDateHHMM(item.datetime)}`}
|
||||
</Text>
|
||||
) : (
|
||||
<Text style={{ fontSize: 11, color: colors.textSecondary }} numberOfLines={1}>
|
||||
{`運用情報 ${index + 1}/${items.length}`}
|
||||
</Text>
|
||||
)}
|
||||
</Animated.View>
|
||||
);
|
||||
};
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* ActiveFormationChipsCycler: 全チップ常時表示、アクティブのみ枠アニメ */
|
||||
/* ------------------------------------------------------------------ */
|
||||
const ActiveFormationChipsCycler: FC<{ items: string[]; color: string }> = ({ items, color }) => {
|
||||
const [activeIndex, setActiveIndex] = useState(0);
|
||||
const borderAnim = useRef(new Animated.Value(items.length <= 1 ? 1.5 : 0)).current;
|
||||
|
||||
useEffect(() => {
|
||||
if (items.length <= 1) return;
|
||||
Animated.timing(borderAnim, { toValue: 1.5, duration: 200, useNativeDriver: false }).start();
|
||||
const id = setInterval(() => {
|
||||
Animated.timing(borderAnim, { toValue: 0, duration: 200, useNativeDriver: false }).start(() => {
|
||||
setActiveIndex((i) => (i + 1) % items.length);
|
||||
Animated.timing(borderAnim, { toValue: 1.5, duration: 200, useNativeDriver: false }).start();
|
||||
});
|
||||
}, 3000);
|
||||
return () => clearInterval(id);
|
||||
}, [items.length]);
|
||||
|
||||
return (
|
||||
<View style={{ flexDirection: "row", alignItems: "center", flexWrap: "wrap", gap: 4 }}>
|
||||
{items.map((text, i) => {
|
||||
const isActive = i === activeIndex;
|
||||
const parts = text.split("+").map((s) => s.trim()).filter(Boolean);
|
||||
const inner = (
|
||||
<View style={{ flexDirection: "row", alignItems: "center", flexWrap: "wrap", gap: 2 }}>
|
||||
{parts.map((part, j) => (
|
||||
<React.Fragment key={j}>
|
||||
{j > 0 && (
|
||||
<Text style={{ fontSize: 11, fontWeight: "bold", color: isActive ? color : color + "55" }}>+</Text>
|
||||
)}
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 12,
|
||||
fontWeight: isActive ? "700" : "500",
|
||||
color: isActive ? color : color + "55",
|
||||
paddingHorizontal: 4,
|
||||
paddingVertical: 1,
|
||||
}}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{part}
|
||||
</Text>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</View>
|
||||
);
|
||||
return (
|
||||
<React.Fragment key={i}>
|
||||
{i > 0 && (
|
||||
<Text style={{ fontSize: 11, color: color + "55" }}>・</Text>
|
||||
)}
|
||||
{isActive ? (
|
||||
<Animated.View
|
||||
style={{
|
||||
borderWidth: borderAnim,
|
||||
borderColor: color,
|
||||
borderRadius: 6,
|
||||
paddingHorizontal: 4,
|
||||
paddingVertical: 1,
|
||||
backgroundColor: color + "12",
|
||||
}}
|
||||
>
|
||||
{inner}
|
||||
</Animated.View>
|
||||
) : (
|
||||
inner
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export const TrainDataSources: FC<{ payload?: TrainDataSourcesPayload }> = ({
|
||||
payload,
|
||||
}) => {
|
||||
@@ -128,7 +274,45 @@ export const TrainDataSources: FC<{ payload?: TrainDataSourcesPayload }> = ({
|
||||
destinationStation,
|
||||
} = payload;
|
||||
|
||||
const hubTrainNum = unyohubTrainNumProp || trainNum;
|
||||
// __メモ書き サフィックスを除去して列番部分だけを返す
|
||||
const stripMemoSuffix = (value: string | null | undefined): string => {
|
||||
if (!value) return "";
|
||||
return value.split("__")[0].trim();
|
||||
};
|
||||
|
||||
const isFreightRetsuban = trainNum.includes("レ");
|
||||
const hubTrainNum = (unyohubTrainNumProp || trainNum).replace(/レ/g, "");
|
||||
const freightUnyohubCandidates = (() => {
|
||||
const digits = hubTrainNum.replace(/[^\d]/g, "");
|
||||
const candidates = new Set<string>();
|
||||
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;
|
||||
const normalized = stripMemoSuffix(candidate).replace(/レ/g, "");
|
||||
if (normalized === hubTrainNum) return true;
|
||||
if (!isFreightRetsuban) return false;
|
||||
return freightUnyohubCandidates.has(normalized);
|
||||
};
|
||||
|
||||
// APIデータ内の元の列番(__メモ付き)を取得して運用Hub連携 URL に使う
|
||||
const originalHubTrainNum = (() => {
|
||||
for (const entry of unyohubEntries) {
|
||||
const match = entry.trains?.find(
|
||||
(t) => stripMemoSuffix(t.train_number).replace(/レ/g, "") === hubTrainNum,
|
||||
);
|
||||
if (match?.train_number) return match.train_number;
|
||||
}
|
||||
return hubTrainNum;
|
||||
})();
|
||||
|
||||
// 進行方向の確定:
|
||||
// 1. payload.direction が明示されていればそれを使う
|
||||
@@ -226,7 +410,7 @@ export const TrainDataSources: FC<{ payload?: TrainDataSourcesPayload }> = ({
|
||||
</View>
|
||||
);
|
||||
|
||||
// 鉄道運用Hub: 車番(formations)が空でないエントリのみ抽出して判定
|
||||
// 鉄道運用Hub: 車番(formations) が空でないエントリのみ表示対象にする
|
||||
const hasNonEmptyFormations = unyohubEntries.some(
|
||||
(e) => !!e.formations && e.formations.trim() !== "",
|
||||
);
|
||||
@@ -242,6 +426,12 @@ export const TrainDataSources: FC<{ payload?: TrainDataSourcesPayload }> = ({
|
||||
.sort()
|
||||
.at(-1) ?? null;
|
||||
|
||||
// フェードサイクル用アイテム(編成あるエントリのみ)
|
||||
const unyohubSubItems: FadingSubItem[] = nonEmptyFormationEntries.map((e) => ({
|
||||
label: e.formations?.trim() || e.operation_id || "",
|
||||
datetime: e.last_posted_datetime ?? null,
|
||||
}));
|
||||
|
||||
// 投稿日時が今日でない場合はカードを薄く表示("YYYY-MM-DD HH:MM:SS" 形式)
|
||||
const todayDateStr = new Date().toLocaleDateString("sv"); // "YYYY-MM-DD"
|
||||
const isUnyohubStale =
|
||||
@@ -269,32 +459,36 @@ 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";
|
||||
|
||||
const formationNames =
|
||||
[...nonEmptyFormationEntries]
|
||||
.sort((a, b) => {
|
||||
const posA =
|
||||
a.trains?.find((t) => t.train_number === hubTrainNum)
|
||||
?.position_forward ?? 0;
|
||||
const posB =
|
||||
b.trains?.find((t) => t.train_number === hubTrainNum)
|
||||
?.position_forward ?? 0;
|
||||
return hubSortDescending ? posB - posA : posA - posB;
|
||||
})
|
||||
.slice(0, 4)
|
||||
.map((e) => e.formations)
|
||||
.join("・") +
|
||||
(nonEmptyFormationEntries.length > 4
|
||||
? ` 他${nonEmptyFormationEntries.length - 4}件`
|
||||
: "");
|
||||
const sortedFormationDisplay = [...nonEmptyFormationEntries]
|
||||
.sort((a, b) => {
|
||||
const posA =
|
||||
a.trains?.find((t) => matchesHubTrainNum(t.train_number))
|
||||
?.position_forward ?? 0;
|
||||
const posB =
|
||||
b.trains?.find((t) => matchesHubTrainNum(t.train_number))
|
||||
?.position_forward ?? 0;
|
||||
return hubSortDescending ? posB - posA : posA - posB;
|
||||
})
|
||||
.slice(0, 4);
|
||||
|
||||
const formationDetail = (
|
||||
<View style={styles.operationDetailBlock}>
|
||||
{hasNonEmptyFormations && (
|
||||
<Text style={[styles.unitIdText, { color: colors.textAccent, opacity: isUnyohubStale ? 0.4 : 1 }]}>{formationNames}</Text>
|
||||
<View style={{ opacity: isUnyohubStale ? 0.4 : 1 }}>
|
||||
<ActiveFormationChipsCycler
|
||||
items={sortedFormationDisplay.map((e) => e.formations || "")}
|
||||
color={colors.textAccent}
|
||||
/>
|
||||
{nonEmptyFormationEntries.length > 4 && (
|
||||
<Text style={{ fontSize: 11, color: colors.textTertiary, marginTop: 2 }}>
|
||||
他{nonEmptyFormationEntries.length - 4}件
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
{unyohubGroupNames !== "" && (
|
||||
<Text style={[styles.subText, { color: colors.textSecondary }]}>{unyohubGroupNames}</Text>
|
||||
@@ -351,8 +545,8 @@ export const TrainDataSources: FC<{ payload?: TrainDataSourcesPayload }> = ({
|
||||
.sort((a, b) => {
|
||||
// high松(left_station)側のユニットを先に表示
|
||||
// (heading_to === "left") === is_leading が true → high松(left)端のユニット
|
||||
const aNav = a.trains?.find((t) => t.train_number === trainNum)?.nav;
|
||||
const bNav = b.trains?.find((t) => t.train_number === trainNum)?.nav;
|
||||
const aNav = a.trains?.find((t) => t.train_number.trim() === trainNum)?.nav;
|
||||
const bNav = b.trains?.find((t) => t.train_number.trim() === trainNum)?.nav;
|
||||
const aIsLeft =
|
||||
(aNav?.heading_to === "left") === (aNav?.is_leading === true);
|
||||
const bIsLeft =
|
||||
@@ -483,9 +677,11 @@ export const TrainDataSources: FC<{ payload?: TrainDataSourcesPayload }> = ({
|
||||
label="外部コミュニティデータ"
|
||||
sub={
|
||||
hasNonEmptyFormations
|
||||
? unyohubLastPostedDatetime
|
||||
? `最終投稿: ${formatDateHHMM(unyohubLastPostedDatetime)}`
|
||||
: ""
|
||||
? unyohubSubItems.length > 1
|
||||
? <FadingSubCycler items={unyohubSubItems} color={colors.textSecondary} />
|
||||
: unyohubLastPostedDatetime
|
||||
? `最終投稿: ${formatDateHHMM(unyohubLastPostedDatetime)}`
|
||||
: ""
|
||||
: unyoCount > 0
|
||||
? "数日の運用報告なし"
|
||||
: "この列車の運用データはありません"
|
||||
@@ -496,7 +692,8 @@ export const TrainDataSources: FC<{ payload?: TrainDataSourcesPayload }> = ({
|
||||
disabled={unyoCount === 0}
|
||||
onPress={() =>
|
||||
openWebView(
|
||||
`https://jr-shikoku-data-system.pages.dev/unyohub-connection-train-data/${hubTrainNum}`,
|
||||
`https://jr-shikoku-data-system.pages.dev/unyohub-connection-train-data/${originalHubTrainNum}`,
|
||||
|
||||
true,
|
||||
)
|
||||
}
|
||||
@@ -527,7 +724,7 @@ export const TrainDataSources: FC<{ payload?: TrainDataSourcesPayload }> = ({
|
||||
const matchedEntry =
|
||||
elesiteNonEmptyFormationEntries[0] ?? elesiteEntries[0];
|
||||
const matchedTrain = matchedEntry?.trains?.find(
|
||||
(t) => t.train_number === trainNum,
|
||||
(t) => t.train_number.trim() === trainNum,
|
||||
);
|
||||
const url =
|
||||
matchedTrain?.timetable_url || "https://www.elesite-next.com/";
|
||||
@@ -942,15 +1139,28 @@ const TrainInfoDetail: FC<{
|
||||
|
||||
{/* うわさ / optional_text */}
|
||||
{(!!uwasa || !!optional_text) && (
|
||||
<View style={[styles.noteSection, { borderLeftColor: colors.borderSecondary }]}>
|
||||
<View style={styles.noteSection}>
|
||||
{!!uwasa && (
|
||||
<View style={styles.noteRow}>
|
||||
<View
|
||||
style={[
|
||||
styles.noteRow,
|
||||
styles.rumorRow,
|
||||
{
|
||||
backgroundColor: "rgba(245, 158, 11, 0.1)",
|
||||
borderColor: colors.textWarning,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<MaterialCommunityIcons
|
||||
name="message-text-outline"
|
||||
size={12}
|
||||
color={colors.iconSecondary}
|
||||
name="alert-circle-outline"
|
||||
size={14}
|
||||
color={colors.textWarning}
|
||||
style={styles.rumorIcon}
|
||||
/>
|
||||
<Text style={[styles.noteText, { color: colors.textSecondary }]}>{uwasa}</Text>
|
||||
<View style={styles.noteTextWrap}>
|
||||
<Text style={[styles.rumorLabel, { color: colors.textWarning }]}>噂情報</Text>
|
||||
<Text style={[styles.noteText, { color: colors.textSecondary }]}>{uwasa}</Text>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
{!!optional_text && (
|
||||
@@ -978,7 +1188,7 @@ type SourceCardProps = {
|
||||
color: string;
|
||||
title: string;
|
||||
label: string;
|
||||
sub?: string;
|
||||
sub?: string | React.ReactNode;
|
||||
badge: number | string | null;
|
||||
badgeColor: string;
|
||||
disabled?: boolean;
|
||||
@@ -1030,9 +1240,13 @@ const SourceCard: FC<SourceCardProps> = ({
|
||||
<Text style={[styles.labelText, { color: colors.textQuaternary }]}>{label}</Text>
|
||||
</View>
|
||||
{sub && (
|
||||
<Text style={[styles.subText, { color: colors.textSecondary }]} numberOfLines={1}>
|
||||
{sub}
|
||||
</Text>
|
||||
typeof sub === "string" ? (
|
||||
<Text style={[styles.subText, { color: colors.textSecondary }]} numberOfLines={1}>
|
||||
{sub}
|
||||
</Text>
|
||||
) : (
|
||||
<View style={styles.subNodeWrap}>{sub}</View>
|
||||
)
|
||||
)}
|
||||
{detail && <View style={styles.detailWrap}>{detail}</View>}
|
||||
</View>
|
||||
@@ -1195,6 +1409,9 @@ const styles = StyleSheet.create({
|
||||
subText: {
|
||||
fontSize: 12,
|
||||
},
|
||||
subNodeWrap: {
|
||||
marginTop: 2,
|
||||
},
|
||||
subTextDisabled: {
|
||||
color: "#bbb",
|
||||
},
|
||||
@@ -1302,15 +1519,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,
|
||||
|
||||
@@ -19,9 +19,30 @@ import { AS } from "@/storageControl";
|
||||
import { STORAGE_KEYS } from "@/constants";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
import { useSheetMaxHeight } from "./useSheetMaxHeight";
|
||||
|
||||
const toAndroidAliasIconName = (id: string) =>
|
||||
id
|
||||
.split(/[^a-zA-Z0-9]+/)
|
||||
.filter(Boolean)
|
||||
.map((part, index) =>
|
||||
index === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1)
|
||||
)
|
||||
.join("");
|
||||
|
||||
const findCurrentIconItem = (
|
||||
iconList: Array<{ id: string; name: string; icon: number }>,
|
||||
currentIcon: string | null
|
||||
) =>
|
||||
iconList.find(
|
||||
({ id }) =>
|
||||
id === currentIcon ||
|
||||
(Platform.OS === "android" && toAndroidAliasIconName(id) === currentIcon)
|
||||
) ?? null;
|
||||
|
||||
export const TrainIconUpdate = () => {
|
||||
const [iconList] = useState(icons());
|
||||
const [currentIcon] = useState(getAppIconName());
|
||||
const currentIconItem = findCurrentIconItem(iconList, currentIcon);
|
||||
const actionSheetRef = useRef(null);
|
||||
const insets = useSafeAreaInsets();
|
||||
const viewShot = useRef(null);
|
||||
@@ -93,7 +114,7 @@ export const TrainIconUpdate = () => {
|
||||
</Text>
|
||||
<View style={{ flex: 1 }} />
|
||||
</View>
|
||||
{currentIcon ? (
|
||||
{currentIconItem ? (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "column",
|
||||
@@ -114,7 +135,7 @@ export const TrainIconUpdate = () => {
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
source={iconList.filter(({ id }) => id == currentIcon)[0].icon}
|
||||
source={currentIconItem.icon}
|
||||
style={{ width: 80, height: 80, borderRadius: 8 }}
|
||||
resizeMode="contain"
|
||||
/>
|
||||
|
||||
@@ -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<View>(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 (
|
||||
<View ref={containerRef} style={{ flex: 1, backgroundColor: fixed.primary, paddingBottom: measuredPadding }}>
|
||||
<Animated.View ref={containerRef} style={{ flex: 1, backgroundColor: fixed.primary, paddingBottom: animatedOffset }}>
|
||||
<FlatList
|
||||
contentContainerStyle={{ justifyContent: "flex-end", flexGrow: 1 }}
|
||||
style={{ flex: 1 }}
|
||||
@@ -358,6 +359,6 @@ export const AllTrainDiagramView: FC = () => {
|
||||
string="閉じる"
|
||||
style={{ display: keyBoardVisible ? "none" : "flex" }}
|
||||
/>
|
||||
</View>
|
||||
</Animated.View>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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";
|
||||
@@ -24,6 +24,8 @@ import { MapsButton } from "./Apps/MapsButton";
|
||||
import { ReloadButton } from "./Apps/ReloadButton";
|
||||
import { useStationList } from "../stateBox/useStationList";
|
||||
import { FixedPositionBox } from "./Apps/FixedPositionBox";
|
||||
import { PlaybackTimeline } from "./Apps/PlaybackTimeline";
|
||||
import { RecordingStatusBar } from "./Apps/RecordingStatusBar";
|
||||
|
||||
export default function Apps() {
|
||||
const { webview, fixedPosition, setFixedPosition } = useCurrentTrain();
|
||||
@@ -35,6 +37,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 +86,7 @@ export default function Apps() {
|
||||
const bgColor = isDark ? "#1c1c1e" : "#ffffff";
|
||||
return (
|
||||
<View style={{ flex: 1, backgroundColor: bgColor }}>
|
||||
{lineColor && lineColorDark && (
|
||||
{isFocused && mapSwitch === "true" && lineColor && lineColorDark && (
|
||||
<LinearGradient
|
||||
colors={[lineColorDark, lineColor]}
|
||||
start={{ x: 0, y: 0 }}
|
||||
@@ -91,10 +94,11 @@ export default function Apps() {
|
||||
style={{ position: "absolute", top: 0, left: 0, right: 0, height: top }}
|
||||
/>
|
||||
)}
|
||||
{lineColor && (
|
||||
<StatusBar
|
||||
barStyle="light-content"
|
||||
/>
|
||||
{isFocused && mapSwitch !== "true" && (
|
||||
<View style={{ position: "absolute", top: 0, left: 0, right: 0, height: top, backgroundColor: "#0099CC" }} />
|
||||
)}
|
||||
{isFocused && (
|
||||
<StatusBar barStyle="light-content" />
|
||||
)}
|
||||
<View
|
||||
style={{
|
||||
@@ -117,12 +121,16 @@ export default function Apps() {
|
||||
/>
|
||||
|
||||
{fixedPosition.type && <FixedPositionBox />}
|
||||
<PlaybackTimeline />
|
||||
<RecordingStatusBar />
|
||||
|
||||
{mapSwitch == "true" ? (
|
||||
<ReloadButton
|
||||
onPress={() => Updates.reloadAsync()}
|
||||
right={isLandscape && trainInfo.trainNum ? (width / 100) * 40 : 0}
|
||||
/>
|
||||
<>
|
||||
<ReloadButton
|
||||
onPress={() => Updates.reloadAsync()}
|
||||
right={isLandscape && trainInfo.trainNum ? (width / 100) * 40 : 0}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<NewMenu />
|
||||
)}
|
||||
|
||||
@@ -332,9 +332,10 @@ export const FixedStation: FC<props> = ({ stationID }) => {
|
||||
}, [selectedTrain, currentTrain, liveNotifyId, buildTrainsInfo]);
|
||||
|
||||
// バナー表示と同時にLive Activityを自動開始(selectedTrainが揃ってから)
|
||||
// iOSのみ一時的に無効化中(Androidは有効)
|
||||
useEffect(() => {
|
||||
if (station.length === 0 || hasStartedRef.current || liveNotifyId) return;
|
||||
if (!isLiveActivityAvailable()) return;
|
||||
// iOSのLive Activityは無効化
|
||||
if (Platform.OS === 'ios') return;
|
||||
hasStartedRef.current = true;
|
||||
const startActivity = async () => {
|
||||
if (Platform.OS === 'android' && Platform.Version >= 33) {
|
||||
|
||||
@@ -471,9 +471,10 @@ export const FixedTrain: FC<props> = ({ trainID }) => {
|
||||
}, [train, nextStationData, liveNotifyId, stationStops, nextStationIndex, currentStationIndex]);
|
||||
|
||||
// バナー表示と同時にLive Activityを自動開始
|
||||
// iOSのみ一時的に無効化中(Androidは有効)
|
||||
useEffect(() => {
|
||||
if (!train || hasStartedRef.current || liveNotifyId) return;
|
||||
if (!isLiveActivityAvailable()) return;
|
||||
// iOSのLive Activityは無効化
|
||||
if (Platform.OS === 'ios') return;
|
||||
hasStartedRef.current = true;
|
||||
const startActivity = async () => {
|
||||
if (Platform.OS === 'android' && Platform.Version >= 33) {
|
||||
|
||||
244
components/Apps/PlaybackTimeline.tsx
Normal file
@@ -0,0 +1,244 @@
|
||||
import React, { FC, useRef, useCallback } from "react";
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
PanResponder,
|
||||
LayoutChangeEvent,
|
||||
} from "react-native";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import dayjs from "dayjs";
|
||||
import { useTrainMenu } from "../../stateBox/useTrainMenu";
|
||||
import { useThemeColors } from "../../lib/theme";
|
||||
import { useResponsive } from "../../lib/responsive";
|
||||
|
||||
/**
|
||||
* 再生中に走行位置WebViewの上部に表示するタイムラインコントローラー。
|
||||
* FixedPositionBox と同じ absolute 配置で zIndex を上に設定する。
|
||||
*/
|
||||
export const PlaybackTimeline: FC = () => {
|
||||
const { top } = useSafeAreaInsets();
|
||||
const { colors, fixed } = useThemeColors();
|
||||
const { moderateScale } = useResponsive();
|
||||
const {
|
||||
recorderState,
|
||||
activeRecording,
|
||||
playbackIndex,
|
||||
playbackPaused,
|
||||
stopPlayback,
|
||||
pausePlayback,
|
||||
resumePlayback,
|
||||
seekToSnapshot,
|
||||
} = useTrainMenu();
|
||||
|
||||
// ─── すべてのフックは早期returnより前に呼ぶ ───
|
||||
const trackWidthRef = useRef(1);
|
||||
|
||||
// PanResponder のハンドラ内で最新値を参照するために ref を使う
|
||||
const seekRef = useRef(seekToSnapshot);
|
||||
seekRef.current = seekToSnapshot;
|
||||
const totalRef = useRef(0);
|
||||
|
||||
const panResponder = useRef(
|
||||
PanResponder.create({
|
||||
onStartShouldSetPanResponder: () => true,
|
||||
onMoveShouldSetPanResponder: () => true,
|
||||
onPanResponderGrant: (evt) => {
|
||||
const x = evt.nativeEvent.locationX;
|
||||
const idx = Math.round((x / trackWidthRef.current) * (totalRef.current - 1));
|
||||
seekRef.current(Math.max(0, Math.min(idx, totalRef.current - 1)));
|
||||
},
|
||||
onPanResponderMove: (evt) => {
|
||||
const x = evt.nativeEvent.locationX;
|
||||
const idx = Math.round((x / trackWidthRef.current) * (totalRef.current - 1));
|
||||
seekRef.current(Math.max(0, Math.min(idx, totalRef.current - 1)));
|
||||
},
|
||||
})
|
||||
).current;
|
||||
|
||||
const onTrackLayout = useCallback((e: LayoutChangeEvent) => {
|
||||
trackWidthRef.current = e.nativeEvent.layout.width;
|
||||
}, []);
|
||||
|
||||
const btnSize = moderateScale(36);
|
||||
const iconSize = moderateScale(20);
|
||||
|
||||
// ─── 早期return ───
|
||||
if (recorderState !== "playing" || !activeRecording) return null;
|
||||
|
||||
const total = activeRecording.snapshots.length;
|
||||
totalRef.current = total; // PanResponder が参照する最新値を更新
|
||||
|
||||
const snap = activeRecording.snapshots[playbackIndex];
|
||||
|
||||
// スナップショット時刻 = 録画開始時刻 + elapsed
|
||||
const snapTime = dayjs(activeRecording.recordedAt).add(snap.t, "ms");
|
||||
const timeLabel = snapTime.format("HH:mm:ss");
|
||||
|
||||
// 録画の総時間をフォーマット
|
||||
const totalSec = Math.round(activeRecording.durationMs / 1000);
|
||||
const totalLabel =
|
||||
totalSec >= 60
|
||||
? `${Math.floor(totalSec / 60)}:${String(totalSec % 60).padStart(2, "0")}`
|
||||
: `${totalSec}s`;
|
||||
|
||||
const progress = total > 1 ? playbackIndex / (total - 1) : 0;
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
position: "absolute",
|
||||
top,
|
||||
left: 0,
|
||||
right: 0,
|
||||
zIndex: 2000,
|
||||
backgroundColor: colors.surface + "f2", // 少し透過
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.borderSecondary,
|
||||
paddingHorizontal: 12,
|
||||
paddingTop: 6,
|
||||
paddingBottom: 8,
|
||||
}}
|
||||
pointerEvents="box-none"
|
||||
>
|
||||
{/* 上段: ボタン + 時刻 + コマ数 */}
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 4,
|
||||
}}
|
||||
>
|
||||
{/* 先頭コマへ */}
|
||||
<TouchableOpacity
|
||||
onPress={() => seekToSnapshot(0)}
|
||||
style={[styles.btn(btnSize, colors.borderSecondary)]}
|
||||
hitSlop={{ top: 8, bottom: 8, left: 4, right: 4 }}
|
||||
>
|
||||
<Ionicons name="play-skip-back" size={iconSize} color={colors.textPrimary} />
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* 前のコマ */}
|
||||
<TouchableOpacity
|
||||
onPress={() => seekToSnapshot(playbackIndex - 1)}
|
||||
style={[styles.btn(btnSize, colors.borderSecondary)]}
|
||||
hitSlop={{ top: 8, bottom: 8, left: 4, right: 4 }}
|
||||
>
|
||||
<Ionicons name="play-back" size={iconSize} color={colors.textPrimary} />
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* 再生 / 一時停止 */}
|
||||
<TouchableOpacity
|
||||
onPress={playbackPaused ? resumePlayback : pausePlayback}
|
||||
style={[styles.btn(btnSize, fixed.primary)]}
|
||||
hitSlop={{ top: 8, bottom: 8, left: 4, right: 4 }}
|
||||
>
|
||||
<Ionicons
|
||||
name={playbackPaused ? "play" : "pause"}
|
||||
size={iconSize}
|
||||
color={fixed.textOnPrimary}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* 次のコマ */}
|
||||
<TouchableOpacity
|
||||
onPress={() => seekToSnapshot(playbackIndex + 1)}
|
||||
style={[styles.btn(btnSize, colors.borderSecondary)]}
|
||||
hitSlop={{ top: 8, bottom: 8, left: 4, right: 4 }}
|
||||
>
|
||||
<Ionicons name="play-forward" size={iconSize} color={colors.textPrimary} />
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* 末尾コマへ */}
|
||||
<TouchableOpacity
|
||||
onPress={() => seekToSnapshot(total - 1)}
|
||||
style={[styles.btn(btnSize, colors.borderSecondary)]}
|
||||
hitSlop={{ top: 8, bottom: 8, left: 4, right: 4 }}
|
||||
>
|
||||
<Ionicons name="play-skip-forward" size={iconSize} color={colors.textPrimary} />
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* スペーサー */}
|
||||
<View style={{ flex: 1 }} />
|
||||
|
||||
{/* 時刻表示 */}
|
||||
<View style={{ alignItems: "flex-end" }}>
|
||||
<Text style={{ fontSize: moderateScale(15), fontWeight: "bold", color: colors.textPrimary, fontVariant: ["tabular-nums"] }}>
|
||||
{timeLabel}
|
||||
</Text>
|
||||
<Text style={{ fontSize: moderateScale(10), color: colors.textSecondary, fontVariant: ["tabular-nums"] }}>
|
||||
{playbackIndex + 1}/{total} {totalLabel}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* 停止ボタン */}
|
||||
<TouchableOpacity
|
||||
onPress={stopPlayback}
|
||||
style={[styles.btn(btnSize, "#e53935"), { marginLeft: 6 }]}
|
||||
hitSlop={{ top: 8, bottom: 8, left: 4, right: 4 }}
|
||||
>
|
||||
<Ionicons name="stop" size={iconSize} color="#fff" />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* 下段: スクラバートラック */}
|
||||
<View
|
||||
style={{ marginTop: 6 }}
|
||||
onLayout={onTrackLayout}
|
||||
{...panResponder.panHandlers}
|
||||
>
|
||||
{/* トラック背景 */}
|
||||
<View
|
||||
style={{
|
||||
height: 6,
|
||||
borderRadius: 3,
|
||||
backgroundColor: colors.borderSecondary,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
{/* 進捗バー */}
|
||||
<View
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
width: `${progress * 100}%`,
|
||||
backgroundColor: fixed.primary,
|
||||
borderRadius: 3,
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
{/* ドラッグハンドル */}
|
||||
<View
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: -4,
|
||||
left: `${progress * 100}%`,
|
||||
marginLeft: -8,
|
||||
width: 14,
|
||||
height: 14,
|
||||
borderRadius: 7,
|
||||
backgroundColor: fixed.primary,
|
||||
borderWidth: 2,
|
||||
borderColor: fixed.textOnPrimary,
|
||||
elevation: 2,
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
// ヘルパー: ボタンスタイル生成
|
||||
const styles = {
|
||||
btn: (size: number, bg: string) => ({
|
||||
width: size,
|
||||
height: size,
|
||||
borderRadius: size / 2,
|
||||
backgroundColor: bg,
|
||||
alignItems: "center" as const,
|
||||
justifyContent: "center" as const,
|
||||
}),
|
||||
};
|
||||
163
components/Apps/RecordingStatusBar.tsx
Normal file
@@ -0,0 +1,163 @@
|
||||
import React, { FC, useEffect, useRef, useState } from "react";
|
||||
import { View, Text, AppState, InteractionManager } from "react-native";
|
||||
import { activateKeepAwakeAsync, deactivateKeepAwake } from "expo-keep-awake";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { useTrainMenu } from "../../stateBox/useTrainMenu";
|
||||
import { useThemeColors } from "../../lib/theme";
|
||||
import { useResponsive } from "../../lib/responsive";
|
||||
|
||||
const KEEP_AWAKE_TAG = "recording-status-bar";
|
||||
|
||||
const isActivityUnavailableError = (error: unknown) =>
|
||||
String(error).includes("The current activity is no longer available");
|
||||
|
||||
/**
|
||||
* 録画中に走行位置画面上部に表示するステータスバー。
|
||||
* PlaybackTimeline と同じ absolute 配置。
|
||||
* 録画中はスリープを抑制する。
|
||||
*/
|
||||
export const RecordingStatusBar: FC = () => {
|
||||
const { top } = useSafeAreaInsets();
|
||||
const { colors } = useThemeColors();
|
||||
const { moderateScale } = useResponsive();
|
||||
const { recorderState, recordingSnapshotCount } = useTrainMenu();
|
||||
|
||||
// 経過時間(秒)
|
||||
const [elapsedSec, setElapsedSec] = useState(0);
|
||||
const startTimeRef = useRef<number>(Date.now());
|
||||
|
||||
// 録画開始時にタイマーをリセットして1秒ごとに更新
|
||||
useEffect(() => {
|
||||
if (recorderState !== "recording") {
|
||||
setElapsedSec(0);
|
||||
return;
|
||||
}
|
||||
startTimeRef.current = Date.now();
|
||||
setElapsedSec(0);
|
||||
const timer = setInterval(() => {
|
||||
setElapsedSec(Math.floor((Date.now() - startTimeRef.current) / 1000));
|
||||
}, 1000);
|
||||
return () => clearInterval(timer);
|
||||
}, [recorderState]);
|
||||
|
||||
// 録画中はスリープ抑制
|
||||
useEffect(() => {
|
||||
if (recorderState !== "recording") return;
|
||||
if (__DEV__) return;
|
||||
|
||||
let mounted = true;
|
||||
|
||||
const activate = async () => {
|
||||
if (!mounted || AppState.currentState !== "active") return;
|
||||
try {
|
||||
await activateKeepAwakeAsync(KEEP_AWAKE_TAG);
|
||||
} catch (error) {
|
||||
if (!isActivityUnavailableError(error)) {
|
||||
console.warn("RecordingStatusBar: failed to activate keep awake", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const interactionHandle = InteractionManager.runAfterInteractions(() => {
|
||||
void activate();
|
||||
});
|
||||
|
||||
const subscription = AppState.addEventListener("change", (state) => {
|
||||
if (state === "active") {
|
||||
void activate();
|
||||
return;
|
||||
}
|
||||
deactivateKeepAwake(KEEP_AWAKE_TAG).catch(() => {});
|
||||
});
|
||||
|
||||
return () => {
|
||||
mounted = false;
|
||||
interactionHandle.cancel();
|
||||
subscription.remove();
|
||||
deactivateKeepAwake(KEEP_AWAKE_TAG).catch(() => {});
|
||||
};
|
||||
}, [recorderState]);
|
||||
|
||||
if (recorderState !== "recording") return null;
|
||||
|
||||
const minutes = Math.floor(elapsedSec / 60);
|
||||
const seconds = elapsedSec % 60;
|
||||
const timeLabel = `${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
position: "absolute",
|
||||
top,
|
||||
left: 0,
|
||||
right: 0,
|
||||
zIndex: 2000,
|
||||
backgroundColor: "rgba(229, 57, 53, 0.92)",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 14,
|
||||
paddingVertical: 7,
|
||||
gap: 10,
|
||||
}}
|
||||
pointerEvents="none"
|
||||
>
|
||||
{/* 点滅 REC ドット */}
|
||||
<BlinkDot />
|
||||
|
||||
<Text
|
||||
style={{
|
||||
color: "#fff",
|
||||
fontWeight: "bold",
|
||||
fontSize: moderateScale(13),
|
||||
letterSpacing: 1,
|
||||
}}
|
||||
>
|
||||
REC
|
||||
</Text>
|
||||
|
||||
<Text
|
||||
style={{
|
||||
color: "#fff",
|
||||
fontSize: moderateScale(15),
|
||||
fontWeight: "bold",
|
||||
fontVariant: ["tabular-nums"],
|
||||
letterSpacing: 1,
|
||||
}}
|
||||
>
|
||||
{timeLabel}
|
||||
</Text>
|
||||
|
||||
<View style={{ flex: 1 }} />
|
||||
|
||||
<Text
|
||||
style={{
|
||||
color: "rgba(255,255,255,0.85)",
|
||||
fontSize: moderateScale(11),
|
||||
}}
|
||||
>
|
||||
{recordingSnapshotCount} コマ
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
/** 1秒ごとに点滅する録画インジケータードット */
|
||||
const BlinkDot: FC = () => {
|
||||
const [visible, setVisible] = useState(true);
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => setVisible((v) => !v), 700);
|
||||
return () => clearInterval(timer);
|
||||
}, []);
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
width: 10,
|
||||
height: 10,
|
||||
borderRadius: 5,
|
||||
backgroundColor: visible ? "#fff" : "transparent",
|
||||
borderWidth: 1.5,
|
||||
borderColor: "#fff",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from "react";
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import { Platform } from "react-native";
|
||||
import { WebView } from "react-native-webview";
|
||||
|
||||
@@ -19,6 +19,8 @@ import { useNavigation } from "@react-navigation/native";
|
||||
import { useTrainMenu } from "../../stateBox/useTrainMenu";
|
||||
import { useStationList } from "../../stateBox/useStationList";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
import { useWebViewRemount } from "@/lib/useWebViewRemount";
|
||||
import { generateMockUpdateScript } from "../../lib/mockApi/webviewXhrInterceptor";
|
||||
export const AppsWebView = ({ openStationACFromEachTrainInfo }) => {
|
||||
const { webview, currentTrain } = useCurrentTrain();
|
||||
const { navigate } = useNavigation<any>();
|
||||
@@ -34,7 +36,24 @@ export const AppsWebView = ({ openStationACFromEachTrainInfo }) => {
|
||||
setLoadError,
|
||||
setTrainInfo,
|
||||
injectJavascript,
|
||||
injectJavascriptBeforeContentLoaded,
|
||||
mockApiFeatureEnabled,
|
||||
mockTrainPositions,
|
||||
} = useTrainMenu();
|
||||
const { remountKey, remount, processHandlers } = useWebViewRemount();
|
||||
|
||||
// コマが変化したとき(再生・シーク)に WebView 内の _MOCK_TRAIN を差し替えて再描画
|
||||
const mountedRef = useRef(false);
|
||||
useEffect(() => {
|
||||
// 初回マウント時はスキップ(beforeContentLoaded で既に正しいデータが入っている)
|
||||
if (!mountedRef.current) {
|
||||
mountedRef.current = true;
|
||||
return;
|
||||
}
|
||||
if (!mockApiFeatureEnabled || !mockTrainPositions) return;
|
||||
const script = generateMockUpdateScript(mockTrainPositions);
|
||||
webview?.current?.injectJavaScript(script);
|
||||
}, [mockTrainPositions]);
|
||||
var urlcache = "";
|
||||
let once = false;
|
||||
|
||||
@@ -151,7 +170,7 @@ export const AppsWebView = ({ openStationACFromEachTrainInfo }) => {
|
||||
|
||||
return (
|
||||
<WebView
|
||||
key={isDark ? 'dark' : 'light'}
|
||||
key={(isDark ? 'dark' : 'light') + (mockApiFeatureEnabled ? '-mock' : '-live') + '-' + remountKey}
|
||||
ref={webview}
|
||||
source={{ uri: "https://train.jr-shikoku.co.jp/sp.html" }}
|
||||
originWhitelist={[
|
||||
@@ -165,6 +184,8 @@ export const AppsWebView = ({ openStationACFromEachTrainInfo }) => {
|
||||
contentMode="mobile"
|
||||
style={{ backgroundColor: bgColor }}
|
||||
{...{ onMessage, onNavigationStateChange, onLoadEnd }}
|
||||
{...processHandlers}
|
||||
injectedJavaScriptBeforeContentLoaded={injectJavascriptBeforeContentLoaded}
|
||||
injectedJavaScript={injectJavascript}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -172,7 +172,7 @@ export const FixedContentBottom = (props) => {
|
||||
disabled={!felicaAvailable}
|
||||
>
|
||||
<Text style={{ color: "white", fontWeight: "bold", fontSize: fontScale(20) }}>
|
||||
IC残高・履歴
|
||||
IC残高・履歴(β)
|
||||
</Text>
|
||||
<MaterialCommunityIcons name="contactless-payment" color="white" size={moderateScale(50)} />
|
||||
<Text style={{ color: "white" }}>Felica対応ICカードの</Text>
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { View, Text, ScrollView, StyleSheet, Image, TouchableOpacity, Linking } from "react-native";
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
Image,
|
||||
TouchableOpacity,
|
||||
Linking,
|
||||
} from "react-native";
|
||||
import { Switch } from "@rneui/themed";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import { MaterialCommunityIcons } from "@expo/vector-icons";
|
||||
@@ -8,9 +16,20 @@ 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,
|
||||
getJrDataSystemTrack,
|
||||
getJrDataSystemUiVariant,
|
||||
JR_DATA_SYSTEM_ENV_OPTIONS,
|
||||
JrDataSystemTrack,
|
||||
JrDataSystemUiVariant,
|
||||
JrDataSystemEnvironmentKey,
|
||||
normalizeJrDataSystemEnvironment,
|
||||
resolveJrDataSystemEnvironment,
|
||||
} from "@/lib/jrDataSystemEnvironment";
|
||||
|
||||
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 */
|
||||
/* ------------------------------------------------------------------ */
|
||||
@@ -58,7 +77,16 @@ const DataSourceAccordionCard: React.FC<DataSourceAccordionCardProps> = ({
|
||||
const { colors } = useThemeColors();
|
||||
|
||||
return (
|
||||
<View style={[styles.accordionCard, { backgroundColor: colors.surface, borderColor: colors.borderSecondary }, enabled && styles.accordionCardEnabled]}>
|
||||
<View
|
||||
style={[
|
||||
styles.accordionCard,
|
||||
{
|
||||
backgroundColor: colors.surface,
|
||||
borderColor: colors.borderSecondary,
|
||||
},
|
||||
enabled && styles.accordionCardEnabled,
|
||||
]}
|
||||
>
|
||||
{/* ── ヘッダー行(常時表示) ── */}
|
||||
<View style={styles.accordionHeader}>
|
||||
{/* 左:ロゴ */}
|
||||
@@ -66,8 +94,14 @@ const DataSourceAccordionCard: React.FC<DataSourceAccordionCardProps> = ({
|
||||
|
||||
{/* 中央:タイトル+タグライン */}
|
||||
<View style={styles.accordionTitles}>
|
||||
<Text style={[styles.accordionTitle, { color: colors.textPrimary }]}>{title}</Text>
|
||||
<Text style={[styles.accordionTagline, { color: colors.textTertiary }]}>{tagline}</Text>
|
||||
<Text style={[styles.accordionTitle, { color: colors.textPrimary }]}>
|
||||
{title}
|
||||
</Text>
|
||||
<Text
|
||||
style={[styles.accordionTagline, { color: colors.textTertiary }]}
|
||||
>
|
||||
{tagline}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* 右:スイッチ */}
|
||||
@@ -81,19 +115,36 @@ const DataSourceAccordionCard: React.FC<DataSourceAccordionCardProps> = ({
|
||||
|
||||
{/* スイッチ状態テキスト */}
|
||||
<View style={styles.accordionStatusRow}>
|
||||
<View style={[styles.statusDot, { backgroundColor: enabled ? accentColor : colors.textDisabled }]} />
|
||||
<Text style={[styles.statusText, { color: enabled ? accentColor : colors.textQuaternary }]}>
|
||||
{enabled ? "有効 — 編成データを取得します" : "無効 — データを取得しません"}
|
||||
<View
|
||||
style={[
|
||||
styles.statusDot,
|
||||
{ backgroundColor: enabled ? accentColor : colors.textDisabled },
|
||||
]}
|
||||
/>
|
||||
<Text
|
||||
style={[
|
||||
styles.statusText,
|
||||
{ color: enabled ? accentColor : colors.textQuaternary },
|
||||
]}
|
||||
>
|
||||
{enabled
|
||||
? "有効 — 編成データを取得します"
|
||||
: "無効 — データを取得しません"}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* ── 展開トリガー ── */}
|
||||
<TouchableOpacity
|
||||
style={[styles.accordionToggleRow, { borderTopColor: colors.borderCard }]}
|
||||
style={[
|
||||
styles.accordionToggleRow,
|
||||
{ borderTopColor: colors.borderCard },
|
||||
]}
|
||||
onPress={() => setExpanded((v) => !v)}
|
||||
activeOpacity={0.6}
|
||||
>
|
||||
<Text style={[styles.accordionToggleLabel, { color: colors.textSecondary }]}>
|
||||
<Text
|
||||
style={[styles.accordionToggleLabel, { color: colors.textSecondary }]}
|
||||
>
|
||||
{expanded ? "詳細を閉じる" : (detailLabel ?? `${title} について`)}
|
||||
</Text>
|
||||
<MaterialCommunityIcons
|
||||
@@ -105,31 +156,69 @@ const DataSourceAccordionCard: React.FC<DataSourceAccordionCardProps> = ({
|
||||
|
||||
{/* ── 展開コンテンツ ── */}
|
||||
{expanded && (
|
||||
<View style={[styles.accordionBody, { borderTopColor: colors.borderCard, backgroundColor: colors.backgroundTertiary }]}>
|
||||
<View
|
||||
style={[
|
||||
styles.accordionBody,
|
||||
{
|
||||
borderTopColor: colors.borderCard,
|
||||
backgroundColor: colors.backgroundTertiary,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{/* 説明文 */}
|
||||
<Text style={[styles.bodyDesc, { color: colors.textSecondary }]}>{description}</Text>
|
||||
<Text style={[styles.bodyDesc, { color: colors.textSecondary }]}>
|
||||
{description}
|
||||
</Text>
|
||||
|
||||
{/* 機能リスト */}
|
||||
<View style={[styles.bodyFeatures, { borderTopColor: colors.borderSecondary }]}>
|
||||
<View
|
||||
style={[
|
||||
styles.bodyFeatures,
|
||||
{ borderTopColor: colors.borderSecondary },
|
||||
]}
|
||||
>
|
||||
{features.map((f) => (
|
||||
<View key={f.icon} style={styles.featureRow}>
|
||||
<View style={styles.featureIcon}>
|
||||
<MaterialCommunityIcons name={f.icon as any} size={14} color={colors.iconSecondary} />
|
||||
<MaterialCommunityIcons
|
||||
name={f.icon as any}
|
||||
size={14}
|
||||
color={colors.iconSecondary}
|
||||
/>
|
||||
</View>
|
||||
<Text style={[styles.featureLabel, { color: colors.textPrimary }]}>{f.label}</Text>
|
||||
<Text style={[styles.featureText, { color: colors.textSecondary }]}>{f.text}</Text>
|
||||
<Text
|
||||
style={[styles.featureLabel, { color: colors.textPrimary }]}
|
||||
>
|
||||
{f.label}
|
||||
</Text>
|
||||
<Text
|
||||
style={[styles.featureText, { color: colors.textSecondary }]}
|
||||
>
|
||||
{f.text}
|
||||
</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
|
||||
{/* リンク */}
|
||||
<TouchableOpacity
|
||||
style={[styles.bodyLink, { borderTopColor: colors.borderSecondary }]}
|
||||
style={[
|
||||
styles.bodyLink,
|
||||
{ borderTopColor: colors.borderSecondary },
|
||||
]}
|
||||
onPress={() => Linking.openURL(linkUrl)}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<MaterialCommunityIcons name="open-in-new" size={13} color={colors.iconSecondary} />
|
||||
<Text style={[styles.bodyLinkText, { color: colors.textSecondary }]}>{linkLabel}</Text>
|
||||
<MaterialCommunityIcons
|
||||
name="open-in-new"
|
||||
size={13}
|
||||
color={colors.iconSecondary}
|
||||
/>
|
||||
<Text
|
||||
style={[styles.bodyLinkText, { color: colors.textSecondary }]}
|
||||
>
|
||||
{linkLabel}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
)}
|
||||
@@ -141,16 +230,45 @@ const DataSourceAccordionCard: React.FC<DataSourceAccordionCardProps> = ({
|
||||
/* 定数 */
|
||||
/* ------------------------------------------------------------------ */
|
||||
const UNYOHUB_FEATURES: Feature[] = [
|
||||
{ icon: "calendar-today", label: "運用データ", text: "本日・過去数日から投稿があった運用の継続予測運用情報を表示" },
|
||||
{ icon: "map-outline", label: "対象エリア", text: "JR四国全線" },
|
||||
{ icon: "train", label: "対象運用", text: "JR四国管内営業列車及び貨物列車に対応、臨時列車/突発運用は非対応" },
|
||||
{ icon: "plus", label: "追加機能", text: "前日、当日、翌日の運用の投稿が可能" },
|
||||
{
|
||||
icon: "calendar-today",
|
||||
label: "運用データ",
|
||||
text: "当日・過去数日から投稿があった運用の継続予測運用情報を表示",
|
||||
},
|
||||
{ icon: "map-outline", label: "対象エリア", text: "JR四国全線" },
|
||||
{
|
||||
icon: "train",
|
||||
label: "対象運用",
|
||||
text: "JR四国管内営業列車及び貨物列車,定期回送列車に対応、臨時列車/突発運用は非対応",
|
||||
},
|
||||
{
|
||||
icon: "pencil",
|
||||
label: "入力方式",
|
||||
text: "アプリ内連携システムにて当日の運用の投稿が可能",
|
||||
},
|
||||
];
|
||||
|
||||
const ELESITE_FEATURES: Feature[] = [
|
||||
{ icon: "calendar-today", label: "運用データ", text: "当日に報告のあった運用情報のみ表示" },
|
||||
{ icon: "map-outline", label: "対象エリア", text: "予讃線/瀬戸大橋線(なお直通している特急などの列番は含みます)" },
|
||||
{ icon: "train", label: "対象運用", text: "JR四国管内営業列車対応、臨時列車/突発運用は非対応" },
|
||||
{
|
||||
icon: "calendar-today",
|
||||
label: "運用データ",
|
||||
text: "当日報告のあった運用情報のみ表示",
|
||||
},
|
||||
{
|
||||
icon: "map-outline",
|
||||
label: "対象エリア",
|
||||
text: "予讃線/瀬戸大橋線(直通している特急などの列番は含みます)",
|
||||
},
|
||||
{
|
||||
icon: "train",
|
||||
label: "対象運用",
|
||||
text: "JR四国管内営業列車対応、臨時列車/突発運用は非対応",
|
||||
},
|
||||
{
|
||||
icon: "pencil",
|
||||
label: "入力方式",
|
||||
text: "アプリ外リンク連携にて当日の運用の投稿が可能",
|
||||
},
|
||||
];
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
@@ -158,11 +276,39 @@ const ELESITE_FEATURES: Feature[] = [
|
||||
/* ------------------------------------------------------------------ */
|
||||
export const DataSourceSettings = () => {
|
||||
const navigation = useNavigation();
|
||||
const { dataSourcePermission, updatePermission } = useTrainMenu();
|
||||
const {
|
||||
updatePermission,
|
||||
mockApiFeatureEnabled,
|
||||
setMockApiFeatureEnabled,
|
||||
recorderState,
|
||||
recordingSnapshotCount,
|
||||
recordingList,
|
||||
startRecording,
|
||||
stopRecording,
|
||||
startPlayback,
|
||||
stopPlayback,
|
||||
deleteRecording,
|
||||
} = 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<JrDataSystemEnvironmentKey>(DEFAULT_JR_DATA_SYSTEM_ENV);
|
||||
const [jrDataSystemTrack, setJrDataSystemTrack] = useState<JrDataSystemTrack>(
|
||||
getJrDataSystemTrack(DEFAULT_JR_DATA_SYSTEM_ENV),
|
||||
);
|
||||
const [jrDataSystemUiVariant, setJrDataSystemUiVariant] =
|
||||
useState<JrDataSystemUiVariant>(
|
||||
getJrDataSystemUiVariant(DEFAULT_JR_DATA_SYSTEM_ENV),
|
||||
);
|
||||
|
||||
const applyJrDataSystemEnv = (env: JrDataSystemEnvironmentKey) => {
|
||||
setJrDataSystemEnv(env);
|
||||
setJrDataSystemTrack(getJrDataSystemTrack(env));
|
||||
setJrDataSystemUiVariant(getJrDataSystemUiVariant(env));
|
||||
AS.setItem(STORAGE_KEYS.JR_DATA_SYSTEM_ENV, env);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
AS.getItem(STORAGE_KEYS.USE_UNYOHUB).then((value) => {
|
||||
@@ -171,6 +317,20 @@ 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) => {
|
||||
const env = normalizeJrDataSystemEnvironment(value);
|
||||
setJrDataSystemEnv(env);
|
||||
setJrDataSystemTrack(getJrDataSystemTrack(env));
|
||||
setJrDataSystemUiVariant(getJrDataSystemUiVariant(env));
|
||||
})
|
||||
.catch(() => {
|
||||
setJrDataSystemEnv(DEFAULT_JR_DATA_SYSTEM_ENV);
|
||||
setJrDataSystemTrack(getJrDataSystemTrack(DEFAULT_JR_DATA_SYSTEM_ENV));
|
||||
setJrDataSystemUiVariant(
|
||||
getJrDataSystemUiVariant(DEFAULT_JR_DATA_SYSTEM_ENV),
|
||||
);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleToggleUnyohub = (value: boolean) => {
|
||||
@@ -179,64 +339,444 @@ export const DataSourceSettings = () => {
|
||||
};
|
||||
|
||||
const handleToggleElesite = (value: boolean) => {
|
||||
if (!canUseElesite) return;
|
||||
setUseElesite(value);
|
||||
AS.setItem(STORAGE_KEYS.USE_ELESITE, value.toString());
|
||||
};
|
||||
|
||||
const handleSelectJrDataSystemTrack = (value: JrDataSystemTrack) => {
|
||||
setJrDataSystemTrack(value);
|
||||
const normalizedVariant =
|
||||
value === "experimental" ? "release" : jrDataSystemUiVariant;
|
||||
setJrDataSystemUiVariant(normalizedVariant);
|
||||
const env = resolveJrDataSystemEnvironment(value, normalizedVariant);
|
||||
applyJrDataSystemEnv(env);
|
||||
};
|
||||
|
||||
const handleSelectJrDataSystemUiVariant = (value: JrDataSystemUiVariant) => {
|
||||
setJrDataSystemUiVariant(value);
|
||||
const env = resolveJrDataSystemEnvironment(jrDataSystemTrack, value);
|
||||
applyJrDataSystemEnv(env);
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={[styles.container, { backgroundColor: fixed.primary }]}>
|
||||
<SheetHeaderItem
|
||||
title="情報ソース設定"
|
||||
LeftItem={{
|
||||
title: "戻る",
|
||||
title: "< 戻る",
|
||||
onPress: () => navigation.goBack(),
|
||||
position: "left",
|
||||
}}
|
||||
/>
|
||||
<ScrollView style={[styles.content, { backgroundColor: colors.backgroundSecondary }]} contentContainerStyle={styles.contentInner}>
|
||||
<Text style={[styles.sectionTitle, { color: colors.textTertiary }]}>外部データソース</Text>
|
||||
<ScrollView
|
||||
style={[
|
||||
styles.content,
|
||||
{ backgroundColor: colors.backgroundSecondary },
|
||||
]}
|
||||
contentContainerStyle={styles.contentInner}
|
||||
>
|
||||
<Text style={[styles.sectionTitle, { color: colors.textTertiary }]}>
|
||||
外部データソース
|
||||
</Text>
|
||||
|
||||
<DataSourceAccordionCard
|
||||
logo={HUB_LOGO_PNG}
|
||||
accentColor="#0099CC"
|
||||
title="鉄道運用Hub"
|
||||
tagline="コミュニティによる列車運用情報サービス"
|
||||
enabled={useUnyohub}
|
||||
onToggle={handleToggleUnyohub}
|
||||
description={
|
||||
"鉄道運用Hubはオープンソースのユーザー投稿型鉄道運用情報データベースアプリケーションです。JR 四国をはじめ全国多数の路線系統に対応しています。\n\nデータがある列車では地図上にアイコンでマークが表示され、列車情報画面の編成表示も更新されます。"
|
||||
}
|
||||
features={UNYOHUB_FEATURES}
|
||||
linkLabel="unyohub.2pd.jp を開く(JR四国)"
|
||||
linkUrl="https://unyohub.2pd.jp/railroad_shikoku/"
|
||||
/>
|
||||
<DataSourceAccordionCard
|
||||
logo={HUB_LOGO_PNG}
|
||||
accentColor="#0099CC"
|
||||
title="鉄道運用Hub"
|
||||
tagline="コミュニティによる列車運用情報サービス"
|
||||
enabled={useUnyohub}
|
||||
onToggle={handleToggleUnyohub}
|
||||
description={
|
||||
"鉄道運用Hubはオープンソースのユーザー投稿型鉄道運用情報データベースアプリケーションです。JR 四国をはじめ全国多数の路線系統に対応しています。\n\nデータがある列車では地図上にアイコンでマークが表示され、列車情報画面の編成表示も更新されます。"
|
||||
}
|
||||
features={UNYOHUB_FEATURES}
|
||||
linkLabel="unyohub.2pd.jp を開く(JR四国)"
|
||||
linkUrl="https://unyohub.2pd.jp/railroad_shikoku/"
|
||||
/>
|
||||
|
||||
{canUseElesite && (
|
||||
<DataSourceAccordionCard
|
||||
logo={ELESITE_LOGO_PNG}
|
||||
accentColor="#44bb44"
|
||||
title="えれサイト"
|
||||
tagline="コミュニティによる列車運用情報サービス"
|
||||
enabled={useElesite}
|
||||
onToggle={handleToggleElesite}
|
||||
description={
|
||||
"えれサイトは、鉄道運用情報を共有するためのサイトです。皆様からの投稿を通じて、鉄道運行に関する情報を共有するサイトです。JR 四国の特急・普通列車を中心に対応しています。\n\nデータがある列車では地図上に緑色の「E」バッジが表示され、列車情報画面の編成表示も更新されます。"
|
||||
}
|
||||
features={ELESITE_FEATURES}
|
||||
linkLabel="elesite-next.com を開く"
|
||||
linkUrl="https://www.elesite-next.com/"
|
||||
/>
|
||||
)}
|
||||
<DataSourceAccordionCard
|
||||
logo={ELESITE_LOGO_PNG}
|
||||
accentColor="#44bb44"
|
||||
title="えれサイト"
|
||||
tagline="コミュニティによる列車運用情報サービス"
|
||||
enabled={useElesite}
|
||||
onToggle={handleToggleElesite}
|
||||
description={
|
||||
"えれサイトは、鉄道の運用情報を利用者同士で共有するサービスです。皆様からの投稿をもとに、列車のリアルタイムな動きを反映しています。JR四国の特急・普通列車をはじめ、現在は全国の路線に対応しています。\n\nデータがある列車では地図上にアイコンでマークが表示され、列車情報画面の編成表示も更新されます。"
|
||||
}
|
||||
features={ELESITE_FEATURES}
|
||||
linkLabel="elesite-next.com を開く"
|
||||
linkUrl="https://www.elesite-next.com/"
|
||||
/>
|
||||
|
||||
<View style={[styles.infoSection, { backgroundColor: colors.backgroundTertiary }]}>
|
||||
<Text style={[styles.infoText, { color: colors.textCaution }]}>
|
||||
外部のコミュニティデータソースとの連携を管理します。
|
||||
{"\n\n"}
|
||||
データの正確性は保証されません。また、これらの連携情報を利用する時点でそれぞれのサイトの利用規約に同意したものとします。{"\n\n"}外部ソースはJR四国非公式アプリが管理していないデータであるため、お問い合わせは各サービスの窓口までお願いいたします。
|
||||
</Text>
|
||||
</View>
|
||||
</ScrollView>
|
||||
<View
|
||||
style={[
|
||||
styles.infoSection,
|
||||
{ backgroundColor: colors.backgroundTertiary },
|
||||
]}
|
||||
>
|
||||
<Text style={[styles.infoText, { color: colors.textCaution }]}>
|
||||
外部のコミュニティデータソースとの連携を管理します。
|
||||
{"\n\n"}
|
||||
データの正確性は保証されません。また、これらの連携情報を利用する時点でそれぞれのサイトの利用規約に同意したものとします。
|
||||
{"\n\n"}
|
||||
外部ソースはJR四国非公式アプリが管理していないデータであるため、お問い合わせは各サービスの窓口までお願いいたします。
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{showDebugSelector && (
|
||||
<>
|
||||
<View
|
||||
style={[
|
||||
styles.debugSection,
|
||||
{
|
||||
backgroundColor: colors.surface,
|
||||
borderColor: colors.borderSecondary,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Text style={[styles.debugTitle, { color: colors.textPrimary }]}>
|
||||
デバッグ: モックAPI検証
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
styles.debugDescription,
|
||||
{ color: colors.textSecondary },
|
||||
]}
|
||||
>
|
||||
公式サイトの代わりにサンプルデータを流し込みます。
|
||||
</Text>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
marginTop: 8,
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.debugCurrentText,
|
||||
{ color: colors.textPrimary, fontSize: 14 },
|
||||
]}
|
||||
>
|
||||
モックAPI検証機能
|
||||
</Text>
|
||||
<Switch
|
||||
value={mockApiFeatureEnabled}
|
||||
onValueChange={setMockApiFeatureEnabled}
|
||||
color={fixed.primary}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 録画・再生 */}
|
||||
<View
|
||||
style={[
|
||||
styles.debugSection,
|
||||
{ backgroundColor: colors.surface, borderColor: colors.borderSecondary },
|
||||
]}
|
||||
>
|
||||
<Text style={[styles.debugTitle, { color: colors.textPrimary }]}>
|
||||
デバッグ: 走行位置録画
|
||||
</Text>
|
||||
<Text style={[styles.debugDescription, { color: colors.textSecondary }]}>
|
||||
ライブデータを録画してモックとして再生します。録画中はモックOFFになります。
|
||||
</Text>
|
||||
|
||||
{/* ステータス行 */}
|
||||
<View style={{ flexDirection: "row", alignItems: "center", marginTop: 8, gap: 8 }}>
|
||||
<View
|
||||
style={{
|
||||
width: 10, height: 10, borderRadius: 5,
|
||||
backgroundColor:
|
||||
recorderState === 'recording' ? '#e53935' :
|
||||
recorderState === 'playing' ? '#43a047' : colors.borderSecondary,
|
||||
}}
|
||||
/>
|
||||
<Text style={[styles.debugCurrentText, { color: colors.textSecondary, fontSize: 13 }]}>
|
||||
{recorderState === 'recording'
|
||||
? `録画中… ${recordingSnapshotCount} スナップショット`
|
||||
: recorderState === 'playing'
|
||||
? '再生中'
|
||||
: `${recordingList.length} 件の録画`}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* 録画開始 / 停止ボタン */}
|
||||
<View style={{ flexDirection: "row", gap: 8, marginTop: 10, flexWrap: "wrap" }}>
|
||||
{recorderState === 'idle' && (
|
||||
<TouchableOpacity
|
||||
onPress={startRecording}
|
||||
style={{
|
||||
backgroundColor: '#e53935', borderRadius: 8,
|
||||
paddingHorizontal: 14, paddingVertical: 8,
|
||||
}}
|
||||
>
|
||||
<Text style={{ color: '#fff', fontWeight: 'bold', fontSize: 13 }}>● 録画開始</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
{recorderState === 'recording' && (
|
||||
<TouchableOpacity
|
||||
onPress={stopRecording}
|
||||
style={{
|
||||
backgroundColor: colors.borderSecondary, borderRadius: 8,
|
||||
paddingHorizontal: 14, paddingVertical: 8,
|
||||
}}
|
||||
>
|
||||
<Text style={{ color: colors.textPrimary, fontWeight: 'bold', fontSize: 13 }}>■ 録画停止</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
{recorderState === 'playing' && (
|
||||
<TouchableOpacity
|
||||
onPress={stopPlayback}
|
||||
style={{
|
||||
backgroundColor: colors.borderSecondary, borderRadius: 8,
|
||||
paddingHorizontal: 14, paddingVertical: 8,
|
||||
}}
|
||||
>
|
||||
<Text style={{ color: colors.textPrimary, fontWeight: 'bold', fontSize: 13 }}>■ 再生停止</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* 録画一覧 */}
|
||||
{recordingList.length > 0 && recorderState !== 'recording' && (
|
||||
<View style={{ marginTop: 10, gap: 6 }}>
|
||||
{recordingList.map((rec) => {
|
||||
const isPlaying = recorderState === 'playing';
|
||||
const durationSec = Math.round(rec.durationMs / 1000);
|
||||
const durationLabel = durationSec >= 60
|
||||
? `${Math.floor(durationSec / 60)}分${durationSec % 60}秒`
|
||||
: `${durationSec}秒`;
|
||||
const dateLabel = new Date(rec.recordedAt).toLocaleString('ja-JP', {
|
||||
month: 'numeric', day: 'numeric',
|
||||
hour: '2-digit', minute: '2-digit',
|
||||
});
|
||||
return (
|
||||
<View
|
||||
key={rec.id}
|
||||
style={{
|
||||
flexDirection: 'row', alignItems: 'center',
|
||||
backgroundColor: colors.backgroundSecondary,
|
||||
borderRadius: 8, padding: 10, gap: 8,
|
||||
}}
|
||||
>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text style={{ color: colors.textPrimary, fontSize: 13, fontWeight: 'bold' }}>
|
||||
{dateLabel}
|
||||
</Text>
|
||||
<Text style={{ color: colors.textSecondary, fontSize: 11 }}>
|
||||
{rec.snapshotCount} コマ / {durationLabel}
|
||||
</Text>
|
||||
</View>
|
||||
{!isPlaying && (
|
||||
<TouchableOpacity
|
||||
onPress={() => startPlayback(rec.id)}
|
||||
style={{
|
||||
backgroundColor: '#43a047', borderRadius: 6,
|
||||
paddingHorizontal: 10, paddingVertical: 6,
|
||||
}}
|
||||
>
|
||||
<Text style={{ color: '#fff', fontWeight: 'bold', fontSize: 12 }}>▶</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
{!isPlaying && (
|
||||
<TouchableOpacity
|
||||
onPress={() => deleteRecording(rec.id)}
|
||||
style={{
|
||||
borderColor: '#e53935', borderWidth: 1, borderRadius: 6,
|
||||
paddingHorizontal: 10, paddingVertical: 6,
|
||||
}}
|
||||
>
|
||||
<Text style={{ color: '#e53935', fontSize: 12 }}>削除</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={[
|
||||
styles.debugSection,
|
||||
{
|
||||
backgroundColor: colors.surface,
|
||||
borderColor: colors.borderSecondary,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Text style={[styles.debugTitle, { color: colors.textPrimary }]}>
|
||||
投稿システム接続先
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
styles.debugDescription,
|
||||
{ color: colors.textSecondary },
|
||||
]}
|
||||
>
|
||||
本番運用と実験場、および本番のリリース版/ベータ版を切り替えます。
|
||||
</Text>
|
||||
|
||||
<Text
|
||||
style={[
|
||||
styles.debugCurrentText,
|
||||
{ color: colors.textTertiary },
|
||||
]}
|
||||
>
|
||||
系統
|
||||
</Text>
|
||||
<View style={styles.debugOptionRow}>
|
||||
{[
|
||||
{
|
||||
key: "production" as const,
|
||||
label: "本番",
|
||||
caption: "一般公開向け",
|
||||
},
|
||||
{
|
||||
key: "experimental" as const,
|
||||
label: "実験",
|
||||
caption: "毎日リセット",
|
||||
},
|
||||
].map((option) => {
|
||||
const selected = jrDataSystemTrack === option.key;
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={option.key}
|
||||
style={[
|
||||
styles.debugOptionButton,
|
||||
{
|
||||
backgroundColor: selected
|
||||
? fixed.primary
|
||||
: colors.backgroundTertiary,
|
||||
borderColor: selected
|
||||
? fixed.primary
|
||||
: colors.borderSecondary,
|
||||
},
|
||||
]}
|
||||
onPress={() => handleSelectJrDataSystemTrack(option.key)}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.debugOptionTitle,
|
||||
{
|
||||
color: selected
|
||||
? fixed.textOnPrimary
|
||||
: colors.textPrimary,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{option.label}
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
styles.debugOptionCaption,
|
||||
{
|
||||
color: selected
|
||||
? fixed.textOnPrimary
|
||||
: colors.textTertiary,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{option.caption}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
|
||||
<Text
|
||||
style={[
|
||||
styles.debugCurrentText,
|
||||
{ color: colors.textTertiary },
|
||||
]}
|
||||
>
|
||||
UIバージョン(本番のみ)
|
||||
</Text>
|
||||
<View style={styles.debugOptionRow}>
|
||||
{[
|
||||
{
|
||||
key: "release" as const,
|
||||
label: "リリース",
|
||||
caption: "安定版",
|
||||
},
|
||||
{
|
||||
key: "beta" as const,
|
||||
label: "ベータ",
|
||||
caption: "夜間ビルド",
|
||||
},
|
||||
].map((option) => {
|
||||
const selected = jrDataSystemUiVariant === option.key;
|
||||
const disabled = jrDataSystemTrack === "experimental";
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={option.key}
|
||||
style={[
|
||||
styles.debugOptionButton,
|
||||
{
|
||||
opacity: disabled ? 0.45 : 1,
|
||||
backgroundColor: selected
|
||||
? fixed.primary
|
||||
: colors.backgroundTertiary,
|
||||
borderColor: selected
|
||||
? fixed.primary
|
||||
: colors.borderSecondary,
|
||||
},
|
||||
]}
|
||||
onPress={() => {
|
||||
if (disabled) return;
|
||||
handleSelectJrDataSystemUiVariant(option.key);
|
||||
}}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.debugOptionTitle,
|
||||
{
|
||||
color: selected
|
||||
? fixed.textOnPrimary
|
||||
: colors.textPrimary,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{option.label}
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
styles.debugOptionCaption,
|
||||
{
|
||||
color: selected
|
||||
? fixed.textOnPrimary
|
||||
: colors.textTertiary,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{option.caption}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
<Text
|
||||
style={[
|
||||
styles.debugCurrentText,
|
||||
{ color: colors.textTertiary },
|
||||
]}
|
||||
>
|
||||
現在の接続先:{" "}
|
||||
{
|
||||
JR_DATA_SYSTEM_ENV_OPTIONS.find(
|
||||
(option) => option.key === jrDataSystemEnv,
|
||||
)?.baseUrl
|
||||
}
|
||||
</Text>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -403,4 +943,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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -261,7 +261,7 @@ export function FelicaHistoryPage() {
|
||||
|
||||
return (
|
||||
<View style={[styles.container, { backgroundColor: fixed.primary }]}>
|
||||
<SheetHeaderItem title="ICカード残高・履歴" />
|
||||
<SheetHeaderItem title="微妙におかしなICカード残高・履歴(β)" />
|
||||
<FlatList
|
||||
style={[styles.scroll, { backgroundColor: colors.background }]}
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
|
||||
@@ -11,10 +11,11 @@ import {
|
||||
} from "react-native";
|
||||
import LottieView from "lottie-react-native";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import { ListItem } from "@rneui/themed";
|
||||
import * as Updates from "expo-updates";
|
||||
import { AS } from "../../storageControl";
|
||||
import { STORAGE_KEYS } from "@/constants";
|
||||
import icons from "../../assets/icons/icons";
|
||||
import icons, { series } from "../../assets/icons/icons";
|
||||
import { setAlternateAppIcon, getAppIconName } from "expo-alternate-app-icons";
|
||||
import { SheetHeaderItem } from "../atom/SheetHeaderItem";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
@@ -24,6 +25,28 @@ const NUM_COLUMNS = Math.floor(Dimensions.get("window").width / ITEM_TARGET_WIDT
|
||||
|
||||
type IconEntry = { id: string; name: string; icon: number };
|
||||
type IconRow = IconEntry[];
|
||||
type IconSection = { key: string; title: string; items: IconEntry[]; rows: IconRow[] };
|
||||
|
||||
const toAndroidAliasIconName = (id: string) =>
|
||||
id
|
||||
.split(/[^a-zA-Z0-9]+/)
|
||||
.filter(Boolean)
|
||||
.map((part, index) =>
|
||||
index === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1)
|
||||
)
|
||||
.join("");
|
||||
|
||||
const matchesCurrentIcon = (id: string, currentIcon: string | null) =>
|
||||
id === currentIcon ||
|
||||
(Platform.OS === "android" && toAndroidAliasIconName(id) === currentIcon);
|
||||
|
||||
const chunkIcons = (items: IconEntry[]) => {
|
||||
const rows: IconRow[] = [];
|
||||
for (let i = 0; i < items.length; i += NUM_COLUMNS) {
|
||||
rows.push(items.slice(i, i + NUM_COLUMNS));
|
||||
}
|
||||
return rows;
|
||||
};
|
||||
|
||||
const IconItem = React.memo(({
|
||||
id, name, icon, isSelected, onPress, textColor,
|
||||
@@ -52,7 +75,8 @@ const IconItem = React.memo(({
|
||||
export const LauncherIconSettings = () => {
|
||||
const { goBack } = useNavigation();
|
||||
const [allIcons] = useState<IconEntry[]>(icons());
|
||||
const [currentIcon] = useState(getAppIconName());
|
||||
const [currentIcon, setCurrentIcon] = useState<string | null>(getAppIconName());
|
||||
const [expandedSections, setExpandedSections] = useState<string[]>([]);
|
||||
const [ready, setReady] = useState(false);
|
||||
const { colors, fixed } = useThemeColors();
|
||||
|
||||
@@ -67,21 +91,67 @@ export const LauncherIconSettings = () => {
|
||||
}, [goBack]);
|
||||
|
||||
const currentIconItem = useMemo(
|
||||
() => allIcons.find(({ id }) => id === currentIcon) ?? null,
|
||||
() => allIcons.find(({ id }) => matchesCurrentIcon(id, currentIcon)) ?? null,
|
||||
[allIcons, currentIcon]
|
||||
);
|
||||
|
||||
const rows = useMemo<IconRow[]>(() => {
|
||||
const result: IconRow[] = [];
|
||||
for (let i = 0; i < allIcons.length; i += NUM_COLUMNS) {
|
||||
result.push(allIcons.slice(i, i + NUM_COLUMNS));
|
||||
const sections = useMemo<IconSection[]>(() => {
|
||||
const iconMap = new Map(allIcons.map((icon) => [icon.id, icon] as const));
|
||||
const usedIds = new Set<string>();
|
||||
|
||||
const seriesSections = series
|
||||
.map((group) => {
|
||||
const items = group.ids
|
||||
.map((id) => iconMap.get(id))
|
||||
.filter((item): item is IconEntry => Boolean(item));
|
||||
|
||||
items.forEach((item) => usedIds.add(item.id));
|
||||
|
||||
return {
|
||||
key: group.key,
|
||||
title: group.title,
|
||||
items,
|
||||
rows: chunkIcons(items),
|
||||
};
|
||||
})
|
||||
.filter((section) => section.items.length > 0);
|
||||
|
||||
const ungroupedItems = allIcons.filter((item) => !usedIds.has(item.id));
|
||||
if (ungroupedItems.length === 0) {
|
||||
return seriesSections;
|
||||
}
|
||||
return result;
|
||||
|
||||
return [
|
||||
...seriesSections,
|
||||
{
|
||||
key: "ungrouped",
|
||||
title: "未分類",
|
||||
items: ungroupedItems,
|
||||
rows: chunkIcons(ungroupedItems),
|
||||
},
|
||||
];
|
||||
}, [allIcons]);
|
||||
|
||||
useEffect(() => {
|
||||
if (expandedSections.length > 0) return;
|
||||
const currentSection = sections.find((section) =>
|
||||
section.items.some(({ id }) => matchesCurrentIcon(id, currentIcon))
|
||||
);
|
||||
if (currentSection) {
|
||||
setExpandedSections([currentSection.key]);
|
||||
return;
|
||||
}
|
||||
if (sections[0]) {
|
||||
setExpandedSections([sections[0].key]);
|
||||
}
|
||||
}, [currentIcon, expandedSections.length, sections]);
|
||||
|
||||
const onPressIcon = useCallback((id: string) => {
|
||||
setAlternateAppIcon(id)
|
||||
const targetIcon = Platform.OS === "android" ? toAndroidAliasIconName(id) : id;
|
||||
|
||||
setAlternateAppIcon(targetIcon)
|
||||
.then(() => {
|
||||
setCurrentIcon(targetIcon);
|
||||
AS.setItem(STORAGE_KEYS.ICON_SETTING, "true");
|
||||
if (Platform.OS === "android") {
|
||||
ToastAndroid.show(
|
||||
@@ -97,23 +167,75 @@ export const LauncherIconSettings = () => {
|
||||
});
|
||||
}, []);
|
||||
|
||||
const renderRow = useCallback(
|
||||
({ item }: { item: IconRow }) => (
|
||||
<View style={{ flexDirection: "row", paddingHorizontal: 4 }}>
|
||||
{item.map(({ id, name, icon }) => (
|
||||
<IconItem
|
||||
key={id}
|
||||
id={id}
|
||||
name={name}
|
||||
icon={icon}
|
||||
isSelected={id === currentIcon}
|
||||
onPress={onPressIcon}
|
||||
textColor={colors.text}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
),
|
||||
[currentIcon, colors.text, onPressIcon]
|
||||
const toggleSection = useCallback((key: string) => {
|
||||
setExpandedSections((prev) => (prev.includes(key) ? [] : [key]));
|
||||
}, []);
|
||||
|
||||
const renderSection = useCallback(
|
||||
({ item }: { item: IconSection }) => {
|
||||
const expanded = expandedSections.includes(item.key);
|
||||
const hasCurrent = item.items.some(({ id }) => matchesCurrentIcon(id, currentIcon));
|
||||
|
||||
return (
|
||||
<ListItem.Accordion
|
||||
animation={{ type: "timing", duration: 180 }}
|
||||
isExpanded={expanded}
|
||||
onPress={() => toggleSection(item.key)}
|
||||
containerStyle={{
|
||||
marginHorizontal: 0,
|
||||
marginBottom: 0,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: colors.border,
|
||||
borderBottomWidth: expanded ? 0 : 1,
|
||||
borderBottomColor: colors.border,
|
||||
backgroundColor: colors.background,
|
||||
paddingHorizontal: 0,
|
||||
paddingVertical: 0,
|
||||
}}
|
||||
content={
|
||||
<>
|
||||
<ListItem.Content style={{ paddingHorizontal: 16, paddingVertical: 14 }}>
|
||||
<ListItem.Title style={{ color: colors.text, fontSize: 16, fontWeight: "500" }}>
|
||||
{item.title}
|
||||
</ListItem.Title>
|
||||
<ListItem.Subtitle style={{ color: hasCurrent ? fixed.primary : colors.textSecondary, fontSize: 12, marginTop: 3 }}>
|
||||
{item.items.length}種類{hasCurrent ? " • 現在選択中" : ""}
|
||||
</ListItem.Subtitle>
|
||||
</ListItem.Content>
|
||||
</>
|
||||
}
|
||||
noIcon={false}
|
||||
>
|
||||
<View style={{ backgroundColor: colors.backgroundSecondary, borderTopWidth: 1, borderTopColor: colors.border }}>
|
||||
{item.rows.map((row, index) => (
|
||||
<View
|
||||
key={`${item.key}-${index}`}
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 6,
|
||||
borderBottomWidth: index === item.rows.length - 1 ? 0 : 1,
|
||||
borderBottomColor: colors.border,
|
||||
}}
|
||||
>
|
||||
{row.map(({ id, name, icon }) => (
|
||||
<IconItem
|
||||
key={id}
|
||||
id={id}
|
||||
name={name}
|
||||
icon={icon}
|
||||
isSelected={matchesCurrentIcon(id, currentIcon)}
|
||||
onPress={onPressIcon}
|
||||
textColor={colors.text}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</ListItem.Accordion>
|
||||
);
|
||||
},
|
||||
[colors.backgroundSecondary, colors.backgroundTertiary, colors.border, colors.text, colors.textSecondary, currentIcon, expandedSections, fixed.primary, onPressIcon, toggleSection]
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -132,55 +254,50 @@ export const LauncherIconSettings = () => {
|
||||
) : (
|
||||
<FlatList
|
||||
style={{ flex: 1, backgroundColor: colors.background }}
|
||||
data={rows}
|
||||
keyExtractor={(_item, index) => String(index)}
|
||||
renderItem={renderRow}
|
||||
data={sections}
|
||||
keyExtractor={(item) => item.key}
|
||||
renderItem={renderSection}
|
||||
ItemSeparatorComponent={undefined}
|
||||
ListHeaderComponent={
|
||||
currentIconItem ? (
|
||||
<>
|
||||
<Text
|
||||
style={{
|
||||
backgroundColor: colors.backgroundTertiary,
|
||||
color: colors.text,
|
||||
}}
|
||||
>
|
||||
現在のアイコン: {currentIconItem.name}
|
||||
</Text>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
backgroundColor: colors.backgroundTertiary,
|
||||
padding: 10,
|
||||
}}
|
||||
>
|
||||
<View style={{ backgroundColor: colors.background, borderBottomWidth: 1, borderBottomColor: colors.border }}>
|
||||
<ListItem containerStyle={{ backgroundColor: colors.background, paddingVertical: 10 }}>
|
||||
<ListItem.Content>
|
||||
<ListItem.Title style={{ color: colors.text, fontSize: 14 }}>
|
||||
現在のアイコン
|
||||
</ListItem.Title>
|
||||
<ListItem.Subtitle style={{ color: fixed.primary, fontSize: 12, marginTop: 2 }}>
|
||||
{currentIconItem.id}
|
||||
</ListItem.Subtitle>
|
||||
</ListItem.Content>
|
||||
</ListItem>
|
||||
<View
|
||||
style={{
|
||||
borderWidth: 1,
|
||||
borderRadius: 10,
|
||||
borderColor: colors.border,
|
||||
margin: 10,
|
||||
padding: 10,
|
||||
backgroundColor: colors.background,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 16,
|
||||
paddingBottom: 14,
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
source={currentIconItem.icon}
|
||||
style={{ width: 80, height: 80, borderRadius: 8 }}
|
||||
style={{ width: 56, height: 56, borderRadius: 8 }}
|
||||
resizeMode="contain"
|
||||
/>
|
||||
<View style={{ marginLeft: 14, flex: 1 }}>
|
||||
<Text style={{ color: colors.text, fontSize: 15, fontWeight: "500" }}>
|
||||
JR四国非公式アプリ
|
||||
</Text>
|
||||
<Text style={{ color: colors.textSecondary, fontSize: 12, marginTop: 4 }}>
|
||||
系式をタップすると候補一覧を開けます
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<Text style={{ color: colors.text }}>JR四国非公式アプリ</Text>
|
||||
</View>
|
||||
</>
|
||||
) : null
|
||||
}
|
||||
getItemLayout={(_data, index) => ({
|
||||
length: 120,
|
||||
offset: 120 * index,
|
||||
index,
|
||||
})}
|
||||
initialNumToRender={5}
|
||||
maxToRenderPerBatch={5}
|
||||
windowSize={5}
|
||||
|
||||
@@ -17,7 +17,7 @@ import { useNotification } from "../../stateBox/useNotifications";
|
||||
import { SheetHeaderItem } from "@/components/atom/SheetHeaderItem";
|
||||
import { useThemeColors, type ColorThemePref } from "@/lib/theme/useThemeColors";
|
||||
|
||||
const versionCode = "7.0"; // Update this version code as needed
|
||||
const versionCode = "7.0.3"; // Update this version code as needed
|
||||
|
||||
export const SettingTopPage = ({
|
||||
testNFC,
|
||||
|
||||
@@ -19,6 +19,7 @@ type hoge = {
|
||||
name: string;
|
||||
timeType: string;
|
||||
time: string;
|
||||
platformNum: string | null;
|
||||
}[];
|
||||
export const ExGridSimpleView: FC<{
|
||||
data: hoge;
|
||||
@@ -63,6 +64,7 @@ export const ExGridSimpleView: FC<{
|
||||
timeType: string;
|
||||
time: string;
|
||||
isOperating: boolean;
|
||||
platformNum: string | null;
|
||||
}[];
|
||||
} = {
|
||||
"4": [], "5": [], "6": [], "7": [], "8": [], "9": [],
|
||||
|
||||
@@ -29,6 +29,7 @@ export const ExGridSimpleViewItem: FC<{
|
||||
timeType: string;
|
||||
time: string;
|
||||
isOperating: boolean;
|
||||
platformNum: string | null;
|
||||
};
|
||||
index: number;
|
||||
array: {
|
||||
@@ -233,6 +234,20 @@ export const ExGridSimpleViewItem: FC<{
|
||||
{d.timeType}
|
||||
</Text>
|
||||
)}
|
||||
{showLastStop && d.platformNum && (
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 10,
|
||||
position: "absolute",
|
||||
top: 36,
|
||||
left: 28,
|
||||
fontWeight: "bold",
|
||||
color: isCancelled ? "gray" : colors.text,
|
||||
}}
|
||||
>
|
||||
{d.platformNum}番
|
||||
</Text>
|
||||
)}
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 12,
|
||||
|
||||
@@ -308,8 +308,6 @@ export const ExGridView: FC<{
|
||||
<Animated.ScrollView
|
||||
style={[{ width: width }, animatedStyle]}
|
||||
pinchGestureEnabled={false}
|
||||
minimumZoomScale={0.5}
|
||||
maximumZoomScale={3.0}
|
||||
scrollEnabled={scrollEnabled}
|
||||
stickyHeaderIndices={
|
||||
groupKeys.at(0) ? groupKeys.map((_, i) => i * 2) : []
|
||||
|
||||
@@ -11,6 +11,7 @@ type hoge = {
|
||||
name: string;
|
||||
timeType: string;
|
||||
time: string;
|
||||
platformNum: string | null;
|
||||
};
|
||||
export const ListView: FC<{
|
||||
data: hoge[];
|
||||
|
||||
@@ -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,
|
||||
@@ -29,6 +29,7 @@ export const ListViewItem: FC<{
|
||||
name: string;
|
||||
timeType: string;
|
||||
time: string;
|
||||
platformNum: string | null;
|
||||
};
|
||||
showVehicle?: boolean;
|
||||
showAppSource?: boolean;
|
||||
@@ -240,12 +241,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 +258,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;
|
||||
@@ -376,6 +386,20 @@ export const ListViewItem: FC<{
|
||||
>
|
||||
{trainName}
|
||||
</Text>
|
||||
{d.platformNum ? (
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: isCancelled ? "gray" : colors.text,
|
||||
borderRadius: 10,
|
||||
paddingHorizontal: 6,
|
||||
paddingVertical: 1,
|
||||
}}
|
||||
>
|
||||
<Text style={{ fontSize: 11, color: colors.diagramBackground, fontWeight: "bold" }}>
|
||||
{d.platformNum}
|
||||
</Text>
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { FC, useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
Animated,
|
||||
View,
|
||||
Text,
|
||||
ScrollView,
|
||||
@@ -70,7 +71,7 @@ export const StationDiagramView: FC<props> = ({ route }) => {
|
||||
const { colors, fixed } = useThemeColors();
|
||||
const tabBarHeight = useBottomTabBarHeight();
|
||||
const containerRef = useRef<View>(null);
|
||||
const { keyboardVisible: keyBoardVisible, measuredOffset: keyboardOffset } =
|
||||
const { keyboardVisible: keyBoardVisible, animatedOffset: keyboardOffset } =
|
||||
useKeyboardAvoid({ measureRef: containerRef, tabBarHeight });
|
||||
const [input, setInput] = useState("");
|
||||
const [displayMode, setDisplayMode] = useState<
|
||||
@@ -90,6 +91,7 @@ export const StationDiagramView: FC<props> = ({ route }) => {
|
||||
name: string;
|
||||
timeType: string;
|
||||
time: string;
|
||||
platformNum: string | null;
|
||||
}[];
|
||||
const [showTypeFiltering, setShowTypeFiltering] = useState(false);
|
||||
const [showLastStop, setShowLastStop] = useState(false);
|
||||
@@ -159,7 +161,7 @@ export const StationDiagramView: FC<props> = ({ route }) => {
|
||||
return station === stationName;
|
||||
})
|
||||
.forEach((x) => {
|
||||
const [name, timeType, time] = x.split(",");
|
||||
const [name, timeType, time, platformNum] = x.split(",");
|
||||
if (!name || !timeType || !time) return;
|
||||
|
||||
const { type } = customTrainDataDetector(d, allCustomTrainData);
|
||||
@@ -169,6 +171,7 @@ export const StationDiagramView: FC<props> = ({ route }) => {
|
||||
name,
|
||||
timeType,
|
||||
time,
|
||||
platformNum: platformNum ?? null,
|
||||
};
|
||||
// //条件によってフィルタリング
|
||||
if (!threw && timeType && timeType.includes("通")) return;
|
||||
@@ -274,7 +277,7 @@ export const StationDiagramView: FC<props> = ({ route }) => {
|
||||
}, [currentStationDiagram, currentTrain]);
|
||||
|
||||
return (
|
||||
<View
|
||||
<Animated.View
|
||||
ref={containerRef}
|
||||
style={{ flex: 1, backgroundColor: fixed.primary, paddingBottom: keyboardOffset }}
|
||||
>
|
||||
@@ -613,7 +616,7 @@ export const StationDiagramView: FC<props> = ({ route }) => {
|
||||
{keyBoardVisible || (
|
||||
<BigButton onPress={() => goBack()} string="閉じる" />
|
||||
)}
|
||||
</View>
|
||||
</Animated.View>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { FC } from "react";
|
||||
import { Image, StyleSheet, View } from "react-native";
|
||||
import { Marker } from "react-native-maps";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import { useStationList } from "@/stateBox/useStationList";
|
||||
@@ -18,6 +19,7 @@ export const MapPin: FC<Props> = (props) => {
|
||||
const { index, indexBase, latlng, D, d, navigate, webview } = props;
|
||||
const { goBack } = useNavigation();
|
||||
const { getInjectJavascriptAddress } = useStationList();
|
||||
|
||||
return (
|
||||
<Marker
|
||||
key={index + indexBase}
|
||||
@@ -25,13 +27,37 @@ export const MapPin: FC<Props> = (props) => {
|
||||
latitude: parseFloat(latlng[0]),
|
||||
longitude: parseFloat(latlng[1]),
|
||||
}}
|
||||
anchor={{ x: 0.5, y: 1 }}
|
||||
tracksViewChanges={false}
|
||||
onPress={() => {
|
||||
const address = getInjectJavascriptAddress(D.StationNumber);
|
||||
if (!address) return;
|
||||
webview.current?.injectJavaScript(address);
|
||||
if (navigate) goBack();
|
||||
}}
|
||||
image={require("../../assets/reccha-small.png")}
|
||||
></Marker>
|
||||
>
|
||||
<View style={styles.markerWrap}>
|
||||
<Image
|
||||
source={require("../../assets/reccha-small.png")}
|
||||
style={styles.markerImage}
|
||||
resizeMode="contain"
|
||||
/>
|
||||
</View>
|
||||
</Marker>
|
||||
);
|
||||
};
|
||||
|
||||
const MAP_PIN_SIZE = Platform.OS === "android" ? 32 : 36;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
markerWrap: {
|
||||
width: MAP_PIN_SIZE,
|
||||
height: MAP_PIN_SIZE,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
markerImage: {
|
||||
width: MAP_PIN_SIZE,
|
||||
height: MAP_PIN_SIZE,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1 +1 @@
|
||||
export const news = "2026-02-01";
|
||||
export const news = "2026-04-01";
|
||||
|
||||
@@ -91,6 +91,9 @@ export const STORAGE_KEYS = {
|
||||
/** えれサイト使用設定 */
|
||||
USE_ELESITE: 'useElesite',
|
||||
|
||||
/** 投稿システム接続先 */
|
||||
JR_DATA_SYSTEM_ENV: 'jrDataSystemEnv',
|
||||
|
||||
/** えれサイトデータ */
|
||||
ELESITE_DATA: 'elesiteData',
|
||||
|
||||
@@ -108,6 +111,18 @@ export const STORAGE_KEYS = {
|
||||
|
||||
/** カラーテーマ設定 ("light" | "system" | "dark") */
|
||||
COLOR_THEME: 'colorTheme',
|
||||
|
||||
/** モックAPI検証機能の有効化スイッチ(admin専用) */
|
||||
MOCK_API_FEATURE_ENABLED: 'mockApiFeatureEnabled',
|
||||
|
||||
/** 走行位置録画インデックス(admin専用 / 複数録画のメタ情報一覧) */
|
||||
MOCK_RECORDINGS_INDEX: 'mockRecordingsIndex',
|
||||
|
||||
/** 走行位置録画データプレフィックス(admin専用 / + id でキーを構成) */
|
||||
MOCK_RECORDING_DATA_PREFIX: 'mockRecordingData_',
|
||||
|
||||
/** 走行位置録画データ(旧フォーマット / マイグレーション用) */
|
||||
MOCK_RECORDING: 'mockRecording',
|
||||
} as const;
|
||||
|
||||
/**
|
||||
|
||||
118
docs/actionsheet-animation-fix.md
Normal file
@@ -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<string[][]>(() =>
|
||||
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` フラグでゲートする。
|
||||
51
docs/changelog-6.2.1-to-6.2.1.1.md
Normal file
@@ -0,0 +1,51 @@
|
||||
## `6.2.1` → `6.2.1.1` の更新内容
|
||||
|
||||
### 1. 鉄道運用Hub(Unyohub)連携機能の新規追加・大幅強化
|
||||
|
||||
- **連携の基盤実装**: 列車運用Hub(外部データソース)との接続設定・データ取得ロジックを実装
|
||||
- **ON/OFF管理**: 追加ソースのON/OFF切り替え機能を追加。オフ時の挙動も修正
|
||||
- **アクセス権限管理**: データ編集権限の取得・判定ロジックを実装。情報ソース設定へのアクセスを権限ベースで制御
|
||||
- **UIコンポーネントの追加**:
|
||||
- `DataSourceAccordionCard` コンポーネントを追加(データソース設定画面)
|
||||
- 運用情報ソースの表示パネル(`TrainSourcesPanel` / `TrainDataSources`)を追加
|
||||
- 戻るボタンの位置設定と条件付きレンダリング構造を修正
|
||||
- HUBロゴの SVG・PNG ファイルを追加
|
||||
- **進行方向・路線表示の改善**:
|
||||
- 進行方向バナーと基準方向ラベルを追加
|
||||
- アニメーションで路線を切り替える機能を実装
|
||||
- **データ取得・ソートロジックの改善**:
|
||||
- `train_ids` / `related_train_ids` に基づいて `unit_ids` を収集する機能を追加
|
||||
- 方向に基づいて運用番号を正しい順序でソートする機能を追加
|
||||
- 車番が空でないエントリのみを対象とする判定ロジックに修正
|
||||
- 各コンポーネントにキャッシュバスティング(タイムスタンプ付きURL)を実装
|
||||
- **列車情報詳細のUI改善**:
|
||||
- タイプタグに色とフォントスタイルを追加
|
||||
- 列車情報表示をスクロール可能な形式に変更
|
||||
|
||||
### 2. WebView・トレインビジョン表示の改善
|
||||
|
||||
- **`ScrollingDescription` コンポーネント追加**: テキストを横スクロール表示する汎用コンポーネントを実装
|
||||
- **停止中の点滅アニメーション**: 停止中マークの点滅アニメーションをWebViewへ動的に注入する機能を追加
|
||||
- **Font Awesome 依存を削除**: インラインSVGに置き換え、全WebView対応のバッジ表示を実装。バンドルサイズ軽減
|
||||
- **lodash 依存を削除**: 軽量な変更検出ユーティリティを内製して置き換え
|
||||
- **データ取得の最適化**: `useInterval` と `webViewInjectjavascript` での取得処理を最適化し、`localStorage` キャッシュを実装。バックグラウンド復帰時にデータを即時再取得
|
||||
- **`onclick` 属性処理の改善**: 古いWebViewとの互換性向上のためイベント伝播を制御。`PopUpMenu` / `ShowTrainTimeInfo` を `Object.defineProperty` でロック
|
||||
- **アイコンの反転表示対応**
|
||||
- **`setReload` の遅延実行**: `setTimeout` でスクリプト実行完了後にリロードするよう変更
|
||||
- **`InjectJavascriptOptions` インターフェース追加**: `injectJavascriptData` 関数の引数をオブジェクト形式に変更
|
||||
|
||||
### 3. 駅・列車データの追加
|
||||
|
||||
- 駅データ、列車アイコンマッピング、列車タイプ設定を追加
|
||||
- 不要なコメント削除・列番データ処理の簡素化
|
||||
|
||||
### 4. 運休表示の改善
|
||||
|
||||
- LEDの行き先表示に運休表示を追加
|
||||
- 運休表示テキストの簡略化
|
||||
- 行き先名の取得ロジックを修正(列車名→行き先名に変更)
|
||||
- 時刻表テキストの結合条件を整理
|
||||
|
||||
### 5. バージョン
|
||||
|
||||
- **v6.2.1.1** にバージョンアップ
|
||||
127
docs/changelog-6.2.1.1-to-7.0.md
Normal file
@@ -0,0 +1,127 @@
|
||||
## `6.2.1.1` → `7.0` の更新内容
|
||||
|
||||
### 1. Expo SDK 52 → 55 メジャーアップグレード
|
||||
|
||||
- **Expo SDK を 52 → 53 → 54 → 55 に段階アップグレード**: React Native 0.76.6 → 0.83.2、React 18.3 → 19.2 に更新
|
||||
- **新アーキテクチャ(New Architecture)を有効化**: RN 0.83+ のデフォルト新アーキテクチャに対応。Kotlin 2.1.20 へ更新
|
||||
- **Reanimated 3.x → 4.x**: `react-native-worklets` を新規追加し、Reanimated 4.2.1 + Carousel 4.0.3 に移行
|
||||
- **ビルド構成の変更**: `expo start --android/--ios` から `expo run:android/run:ios`(ネイティブビルド)に変更
|
||||
- **babel.config.js**: `lazyImports` 有効化、`react-native-reanimated/plugin` 追加、`react-compiler` 無効化
|
||||
- **metro.config.js**: `modules/` ディレクトリを `watchFolders` に追加(ローカルExpoモジュール対応)
|
||||
- **不要パッケージの削除**: `react-native-router-flux`、`react-native-snap-carousel`、`react-native-svg-uri`、`react-native-auto-height-image`、`react-native-walkthrough-tooltip`、`react-native-remote-svg`、`pushy-react-native`、`firebase`、`native-base` を削除
|
||||
|
||||
### 2. ダークモード対応
|
||||
|
||||
- **テーマシステムの構築**: `lib/theme/colors.ts` に約50のセマンティックカラートークン(light/dark)を定義。`useThemeColors()` フックと `AppThemeProvider` で全体を制御
|
||||
- **WebView ダークモード**: JR四国列車位置ページにCSS暗色テーマを動的注入。HSLベースの `_darkenBg()` ヘルパーで個別要素の背景色も自動調整
|
||||
- **全コンポーネントへの適用**: ハードコードされた色(`#ffffff`、`#0099CC`、`"black"` 等)をテーマトークンに置き換え。設定画面、ActionSheet、駅詳細、お気に入り一覧、列車情報等に適用
|
||||
- **カラーテーマ設定**: 設定画面に `ColorThemePicker`(システム/ライト/ダーク選択)を追加
|
||||
|
||||
### 3. FeliCa(交通系ICカード)対応
|
||||
|
||||
- **残高読み取り**: Android(NfcF)/ iOS(CoreNFC)の両方でサービスコード `0x090F` に `ReadWithoutEncryption` コマンドを発行し、ICカード残高を取得
|
||||
- **乗降履歴取得**: 最大20件の履歴ブロックを解析。端末種別・処理種別・日時・入出場駅コード・残高・事業者コード等を構造化データとして取得
|
||||
- **FeliCa履歴ページ**: 347行の新規画面。カード種別表示・差額計算・駅名逆引き(5,931駅のマッピングデータ)・NFCスキャン・長押しコピーに対応
|
||||
- **`isAvailable()` 関数**: NFC機能の利用可否をプラットフォーム別にチェック
|
||||
- **iOS App Group連携**: `saveWidgetData()` でUserDefaultsに残高を書き込み、WidgetKitのタイムラインを自動更新
|
||||
|
||||
### 4. iOSウィジェット・Live Activity
|
||||
|
||||
- **FeliCaBalanceWidget**: ICカード残高をホーム画面に表示。ダーク/ライトモード対応、iPhone限定
|
||||
- **ShortcutWidget**: アプリ機能へのクイックアクセスタイル。遅延情報・運行情報の取得機能付き
|
||||
- **DelayInfoWidget**: JR四国の遅延・運休情報を表示
|
||||
- **OperationInfoWidget**: 列車運行状況を表示
|
||||
- **TrainFollow Live Activity**: ロック画面上でリアルタイムの列車追跡を表示
|
||||
- **StationLock Live Activity**: ロック画面上で駅固定の発車標を表示
|
||||
- **Live Activityの一時無効化**: インフラは実装済みだが、リリース時点では一時的に無効化
|
||||
|
||||
### 5. Android通知・ウィジェット
|
||||
|
||||
- **列車追跡フォアグラウンドサービス**: 15秒間隔でAPIポーリングし、通知を更新。「列車追跡」と「駅固定」の2モードに対応
|
||||
- **進捗バー付き通知**: 列車位置をルート上の進捗バーで表示。Android 16(Baklava)では `ProgressStyle` API で色分けセグメント・停車駅マーカーを表示
|
||||
- **駅固定通知**: Inbox形式で次発列車一覧を表示。列車種別ごとにバッジ色分け
|
||||
- **ShortcutWidget / FelicaQuickAccessWidget**: Androidウィジェットの追加・UIデザイン更新
|
||||
- **通知権限**: Android向け `POST_NOTIFICATIONS` 権限の明示的リクエストを追加
|
||||
|
||||
### 6. ディープリンク・通知ルーティング
|
||||
|
||||
- **URLスキーム `jrshikoku://` の実装**: `open/felica`、`open/traininfo`、`open/operation`、`open/settings`、`open/topmenu` へのルーティング対応
|
||||
- **通知タップルーティング**: チャンネルID・カテゴリ・コンテンツに基づき、`delay-ex`(遅延速報EX)、`strange-train`(怪レい列車)、`information`(運行情報)の各画面に遷移
|
||||
- **グローバルナビゲーション**: `rootNavigationRef` によりReactコンポーネントツリー外からのナビゲーションが可能に
|
||||
- **linking config**: `Apps.tsx` に `jrshikoku://` スキームの画面階層を定義
|
||||
|
||||
### 7. iPad・タブレット対応
|
||||
|
||||
- **iPad サポート有効化**: `app.json` の `supportsTablet` を `true` に変更
|
||||
- **ActionSheet改善**: iPadでは `isModal` を無効化し、`useSheetMaxHeight` フックで端末種別に応じた最大高さ制御(スマホのみ制限、タブレットは無制限)
|
||||
- **ヘッダー表示修正**: `ScrollView` → `View` + `width: 100%` に変更
|
||||
- **WebView最適化**: `contentMode="mobile"` を追加し、iPadでもモバイルビューポートを表示
|
||||
|
||||
### 8. Samsung DeX対応
|
||||
|
||||
- **低密度ディスプレイ検出**: `PixelRatio < 1.5` でDeX環境を自動検出
|
||||
- **Dimensionsモンキーパッチ**: `Dimensions.get('window')` をスケール済み仮想値に置き換え
|
||||
- **DensityScaleWrapper**: アプリ全体を `transform: [{ scale }]` でラップし、レイアウトの一貫性を維持
|
||||
- **フォントサイズ自動補正**: Babelプラグインで全 `Text`/`TextInput` のスタイルを `global.__scaleTextStyle()` でラップし、PixelRatioに応じたフォント補正を実行(最大4倍)
|
||||
|
||||
### 9. サウンド機能
|
||||
|
||||
- **遅延アナウンス機能**: 駅固定モードで遅延発生時に音声で通知する機能を追加
|
||||
- **SoundSettings画面**: サウンド設定のON/OFFトグルとプレビュー再生。`expo-audio` を使用
|
||||
- **プラットフォーム別URI処理**: Androidでは `file://` プレフィックスを除去
|
||||
|
||||
### 10. elesite連携
|
||||
|
||||
- **新データソースとして統合**: 列車編成・車両情報の取得用に `useElesite` フックを実装
|
||||
- **HeaderTextコンポーネント改善**: elesiteデータの表示・レイアウトを統合
|
||||
- **非空の編成ユニットを優先**: ソートロジックの改善
|
||||
- **権限ベースの設定制御**: 設定画面からelesiteのON/OFF切り替え
|
||||
|
||||
### 11. 駅固定・列車追跡の改善
|
||||
|
||||
- **バックグラウンド更新**: データ取得をバックグラウンドでも継続。列車追跡の終了条件はフォアグラウンド状態に依存
|
||||
- **進捗バー再設計**: 全駅対応の進捗表示に変更
|
||||
- **出発済みフィルタ**: 駅固定通知から出発済み列車を除外
|
||||
- **棒線駅接近判定の修正**: 通過駅での接近判定ロジックを改善
|
||||
- **通知書式改善**: 路線色の追加、進捗スタイルのセグメント対応
|
||||
- **APIタイムアウト追加**: API呼び出しにタイムアウトとエラーハンドリングを追加
|
||||
|
||||
### 12. UIコンポーネントの改善
|
||||
|
||||
- **カルーセル大幅改修**: グリッド表示モード追加(`GridMiniSign`、`SortGridCard`)、ソート機能実装、アニメーション追加
|
||||
- **キーボード回避ロジック共通化**: `useKeyboardAvoid` フックに統合。Androidの偽キーボードイベント対策、デバウンス、高さキャッシュを実装
|
||||
- **ActionSheet**: TypeScript型定義の整備(`SheetDefinition`)、スクロール処理の簡素化
|
||||
- **アニメーション最適化**: `useSharedValue`/`useAnimatedStyle` の導入、`LayoutAnimation` の除去によるパフォーマンス向上
|
||||
- **タブバーグラデーション**: 選択中の路線カラーに基づくグラデーション背景表示
|
||||
|
||||
### 13. 設定画面の再構成
|
||||
|
||||
- **セクション分け**: 「表示」「カスタマイズ」「通知・データ」「その他」にグループ化
|
||||
- **新規項目追加**: カラーテーマ選択、サウンド設定、レイアウト設定
|
||||
- **WidgetSettings削除**: ウィジェット専用設定画面を廃止
|
||||
- **テーマカラー適用**: 全テキスト・背景色にテーマトークンを使用
|
||||
|
||||
### 14. 観光スポット・地図機能
|
||||
|
||||
- **SpotSign コンポーネント**: 地図上の観光スポットマーカーを追加
|
||||
- **与島PA**: トップメニューに追加、座標を正確な位置に修正
|
||||
- **最寄駅追跡**: `UserPositionProvider` と `FixedNearestStationBox` でユーザー位置に基づく最寄駅表示
|
||||
|
||||
### 15. データ取得・キャッシュの最適化
|
||||
|
||||
- **キャッシュTTL変更**: 駅リスト(1時間→1週間)、運行情報(2分→1日)、列車データ(2分→1日)、Unyohub(10分→1時間)、障害情報(2分→1分)
|
||||
- **次駅表示修正**: Direction非依存に修正(JS/Kotlin両方)
|
||||
- **アンパンマン列車API**: 予讃線・土讃線のアンパンマン列車ごとにAPIエンドポイントを判定
|
||||
|
||||
### 16. その他
|
||||
|
||||
- **アプリアイコン53種追加**: 合計アイコン数が大幅増加
|
||||
- **アイコンID統一**: 大文字小文字の統一により可読性向上
|
||||
- **lodash依存の削除**: 内製の軽量変更検出ユーティリティに置き換え
|
||||
- **TypeScriptエラー削減**: 143件 → 47件に削減
|
||||
- **遅延による日付調整ロジック追加**
|
||||
- **スクリーンショット共有機能**: TrainDataSourcesコンポーネントに追加
|
||||
|
||||
### 17. バージョン
|
||||
|
||||
- **v7.0** にメジャーバージョンアップ(iOS buildNumber: 50→63、Android versionCode: 28→30)
|
||||
81
docs/changelog-7.0-to-7.0.2-features.md
Normal file
@@ -0,0 +1,81 @@
|
||||
# 064d81d4 -> a959cf39 更新ログ(機能説明フォーカス版)
|
||||
|
||||
## この差分で何が良くなったか
|
||||
この更新では、大きな新機能追加よりも、既存機能を日常利用で安定して使えるようにする改善が中心です。
|
||||
|
||||
特に「外部連携の扱いやすさ」「画面遷移の安定性」「キーボード/アニメーション時の操作感」に効く変更がまとまって入っています。
|
||||
|
||||
---
|
||||
|
||||
## 1. 運用Hub(elesite)連携の運用性向上
|
||||
### 実装/更新された機能
|
||||
- elesite連携の権限ハンドリングを簡素化し、設定分岐を整理。
|
||||
- 列車ペアマッピングと貨物列車の車番処理を改善。
|
||||
- elesiteロゴ追加など、情報ソース表示の視認性を改善。
|
||||
- 投稿システム接続先を切り替えて検証できるデバッグ導線を追加。
|
||||
|
||||
### ユーザーにとっての効果
|
||||
- 情報ソース連携時の挙動が安定し、設定後の迷いが減ります。
|
||||
- 表示データの整合性が上がり、参照時の信頼感が向上します。
|
||||
|
||||
## 2. ナビゲーション基盤の強化
|
||||
### 実装/更新された機能
|
||||
- stackAwareNavigate導入で、遷移時のスタック管理を改善。
|
||||
- ルート状態監視(onStateChange)を見直し、アクティブルート判定を精度向上。
|
||||
- StatusBar表示制御をフォーカス状態ベースに改善。
|
||||
- WebView内ナビゲーションの扱いを調整。
|
||||
|
||||
### ユーザーにとっての効果
|
||||
- 画面遷移での予期しない戻り/重複遷移が起きにくくなります。
|
||||
- 画面表示の一貫性が上がり、操作中の違和感が減ります。
|
||||
|
||||
## 3. キーボード回避とActionSheet操作感の改善
|
||||
### 実装/更新された機能
|
||||
- キーボード回避を Animated.timing / spring ベースに移行。
|
||||
- SearchUnitBoxでanimatedOffsetを使った追従へ改善。
|
||||
- EachTrainInfo ActionSheetのスプリング破綻を修正。
|
||||
- 調整内容を技術ドキュメントとして記録。
|
||||
|
||||
### ユーザーにとっての効果
|
||||
- 連続操作時でもUIの位置ずれが起きにくくなります。
|
||||
- シート表示や検索入力時の「引っかかり」が減り、操作が滑らかになります。
|
||||
|
||||
## 4. 列車情報画面の表示品質改善
|
||||
### 実装/更新された機能
|
||||
- HeaderTextのtodayOperation判定を改善し、不要な表示混入を抑制。
|
||||
- TrainDataViewの長押し条件を実データ基準へ調整。
|
||||
- ExGridViewの不要ズーム設定を除去。
|
||||
- ListViewItemの循環アニメーションを改善。
|
||||
|
||||
### ユーザーにとっての効果
|
||||
- 列車情報の表示が実態に近づき、誤認しにくくなります。
|
||||
- 長押しや一覧切替の体験が自然になり、ストレスが減ります。
|
||||
|
||||
## 5. 設定・内部運用の整備
|
||||
### 実装/更新された機能
|
||||
- 内部バージョン表記を7.0.2へ更新。
|
||||
- 情報ソース設定まわりを拡張し、運用切替時の扱いを改善。
|
||||
- タブバーアニメーション/キーボード非表示設定の整理を実施。
|
||||
|
||||
### ユーザーにとっての効果
|
||||
- 設定変更後の挙動がより予測しやすくなります。
|
||||
- 端末や利用状況の違いによる操作ブレが軽減されます。
|
||||
|
||||
## 6. ドキュメント整備(将来拡張と保守性)
|
||||
### 実装/更新された機能
|
||||
- チュートリアル機能の設計案を追加。
|
||||
- プライバシーポリシー関連ドキュメントを整備。
|
||||
- キーボード/ActionSheet調整の修正履歴を記録。
|
||||
|
||||
### ユーザーにとっての効果
|
||||
- 直接見える機能ではありませんが、今後の改善や不具合修正が速くなり、結果として体験品質の向上につながります。
|
||||
|
||||
---
|
||||
|
||||
## まとめ
|
||||
この更新は、外部連携・画面遷移・入力時アニメーションなど、日常操作で「気になりやすい部分」を重点的に磨いた差分です。
|
||||
|
||||
新機能を増やすよりも、既存機能を安定して快適に使えるようにすることに重きを置いた内容になっています。
|
||||
|
||||
## 参照
|
||||
- コミット差分精査版: docs/changelog-064d81d-to-a959cf3.md
|
||||
125
docs/changelog-7.0-to-7.0.2.md
Normal file
@@ -0,0 +1,125 @@
|
||||
# コミット差分ログ(064d81d4 -> a959cf39)
|
||||
|
||||
## 対象範囲
|
||||
- From: `064d81d468f21db4318a55f8fad4c432745d2441`
|
||||
- To: `a959cf3973eb9635c9e61133d81de5c1f9719c18`
|
||||
- 集計メモ:
|
||||
- レンジ内コミット(merge含む): 31
|
||||
- 非mergeコミット: 22
|
||||
- 変更ファイル数: 44
|
||||
- 変更行数: +1566 / -305
|
||||
|
||||
## サマリー
|
||||
このレンジでは、運用Hub(elesite/投稿系)連携の実運用改善、キーボード回避とActionSheetのアニメーション安定化、ナビゲーションのスタック制御改善、設定画面とWebView周辺の挙動調整が中心に進められました。
|
||||
|
||||
機能追加は大規模というより、既存機能の「誤動作しにくさ」「切替時の安定性」「運用しやすさ」を高める改善が多い構成です。
|
||||
|
||||
---
|
||||
|
||||
## 1. 運用Hub/外部連携(elesite・投稿系)の改善
|
||||
- elesite連携の権限ハンドリングを簡素化し、設定/運用時の分岐を整理。
|
||||
- 列車ペアマッピングや貨物列車の車番処理を改善し、データ整合性を向上。
|
||||
- elesiteロゴ資産を追加し、情報ソース表示の視認性を改善。
|
||||
- 投稿システム接続先のデバッグ機能を追加し、環境切替の検証性を向上。
|
||||
|
||||
主なコミット:
|
||||
- `fac89f6` fix: simplify elesite permission handling and update version code to 7.0.2
|
||||
- `94eb84b` fix: update train pair mapping in BusAndTrainDataProvider
|
||||
- `ad5357c` 運用Hub情報の取得ロジックを改善し、貨物列車の車番処理を追加
|
||||
- `1b2ba08` feat: 投稿システム接続先のデバッグ機能を追加
|
||||
- `ff90841` Add new logo image for Elesite to relationLogo assets
|
||||
|
||||
## 2. 画面遷移・ナビゲーション基盤の安定化
|
||||
- `stackAwareNavigate` を導入し、遷移時のスタック管理を強化。
|
||||
- App状態遷移時のアクティブルート判定を改善し、ルーティング誤判定を抑制。
|
||||
- StatusBar表示ロジックを見直し、フォーカス状態を踏まえた表示制御に更新。
|
||||
- WebViewナビゲーション挙動を調整し、画面内遷移の扱いを改善。
|
||||
|
||||
主なコミット:
|
||||
- `5914646` stackAwareNavigate関数を導入し、遷移時のナビゲーションロジックを改善
|
||||
- `a54ef7c` スタック管理強化 + 設計メモ追加
|
||||
- `6b46c71` AppContainerのonStateChangeロジックを改善
|
||||
- `9e2abc9` StatusBarの表示ロジックを改善
|
||||
- `3ecb301` fix: update WebView navigation
|
||||
|
||||
## 3. キーボード回避・ActionSheetアニメーション改善
|
||||
- キーボード回避を `Animated.timing/spring` ベースへ移行し、切替時の位置ずれを緩和。
|
||||
- SearchUnitBoxで `animatedOffset` を利用したスムーズな追従に変更。
|
||||
- ActionSheet(EachTrainInfo)で発生していたスプリング破綻を修正。
|
||||
- 調整内容をドキュメント化し、再発時のトラブルシュート性を向上。
|
||||
|
||||
主なコミット:
|
||||
- `4017f82` fix: キーボード回避をAnimated.timing/springに移行
|
||||
- `36be780` fix(SearchUnitBox): use animatedOffset with Animated.View
|
||||
- `8b42644` fix: EachTrainInfo ActionSheetのスプリングアニメーション破綻を修正
|
||||
- `b87c6f8` docs: キーボードアニメーション調整に関するドキュメントを追加
|
||||
- `4809426` docs: ActionSheetアニメーション破綻の修正記録を追加
|
||||
|
||||
## 4. 列車情報表示・操作の品質改善
|
||||
- HeaderTextの `todayOperation` 判定を調整し、完了済み運用の混入を抑制。
|
||||
- TrainDataViewのロングプレス判定条件を `onLine` 依存から実データ依存へ改善。
|
||||
- ExGridViewの不要ズームスケール設定を除去し、スクロール挙動の安定性を向上。
|
||||
- ListViewItemの循環アニメーションを改善し、表示切替の自然さを向上。
|
||||
|
||||
主なコミット:
|
||||
- `5a1430d` fix(HeaderText): filter out completed operations
|
||||
- `07399f4` fix(HeaderText) + fix(TrainIconStatus)
|
||||
- `76a617c` fix(TrainDataView): onLongPress condition update
|
||||
- `a09ba45` fix(ExGridView): remove zoom scale properties
|
||||
- `94eb84b` fix: enhance ListViewItem cycling animation
|
||||
|
||||
## 5. 設定・内部運用の整備
|
||||
- 設定画面側の内部バージョン更新(7.0.2)を反映。
|
||||
- データソース設定画面を拡張し、運用切替時の扱いを改善。
|
||||
- タブバーアニメーション/キーボード関連の設定を整理し、UI挙動を安定化。
|
||||
|
||||
主なコミット:
|
||||
- `fac89f6` fix: update version code to 7.0.2
|
||||
- `374901c` fix: タブバーアニメーションとキーボード非表示設定を削除
|
||||
|
||||
## 6. ドキュメント拡充(運用・設計)
|
||||
- チュートリアル機能の設計案を追加。
|
||||
- プライバシーポリシー文書(本番/将来案)を追加。
|
||||
- アニメーション調整や修正履歴の技術ドキュメントを追加。
|
||||
|
||||
追加ドキュメント:
|
||||
- `docs/tutorial-feature-plan.md`
|
||||
- `docs/privacy-policy.md`
|
||||
- `docs/privacy-policy-future.md`
|
||||
- `docs/actionsheet-animation-fix.md`
|
||||
- `docs/keyboard-animation-tuning-2026-04-08.md`
|
||||
- `docs/keyboard-animation-tuning-2026-04-09.md`
|
||||
|
||||
---
|
||||
|
||||
## 変更リスクと確認ポイント
|
||||
- 画面遷移系:
|
||||
- stackAwareNavigate導入に伴い、既存deep link/戻る挙動との整合確認が必要。
|
||||
- キーボード/アニメーション系:
|
||||
- 端末ごとのキーボード表示速度差で再ズレが起きないか継続確認が必要。
|
||||
- 外部連携系:
|
||||
- elesite権限状態の境界ケース(未許可/再許可/切替直後)で表示崩れがないか要確認。
|
||||
|
||||
## 付録: 非mergeコミット一覧(22件)
|
||||
- `fac89f6` fix: simplify elesite permission handling and update version code to 7.0.2
|
||||
- `94eb84b` fix: enhance ListViewItem cycling animation and update train pair mapping in BusAndTrainDataProvider
|
||||
- `3ecb301` fix: update WebView navigation and adjust interval timing in CurrentTrainProvider
|
||||
- `5a1430d` fix(HeaderText): update todayOperation prop to filter out completed operations
|
||||
- `ff90841` Add new logo image for Elesite to relationLogo assets
|
||||
- `a09ba45` fix(ExGridView): remove zoom scale properties from Animated.ScrollView
|
||||
- `76a617c` fix(TrainDataView): update onLongPress condition to check currentTrainData instead of onLine
|
||||
- `07399f4` fix(HeaderText): update todayOperation to use allTodayOperation for accurate state filtering
|
||||
- `1b2ba08` feat: 投稿システム接続先のデバッグ機能を追加し、環境設定を管理できるようにした
|
||||
- `374901c` fix: タブバーのアニメーションとキーボード非表示設定を削除
|
||||
- `36be780` fix(SearchUnitBox): use animatedOffset with Animated.View for smooth keyboard avoidance
|
||||
- `b87c6f8` docs: キーボードアニメーション調整に関するドキュメントを追加
|
||||
- `4017f82` fix: キーボード回避をAnimated.timing/springに移行し、高速切替時の位置ずれとアニメーション不動を解消
|
||||
- `58ce5fa` feat: チュートリアル機能の設計案を追加し、ユーザーの初回体験を改善
|
||||
- `4809426` docs: ActionSheetアニメーション破綻の修正記録を追加
|
||||
- `8b42644` fix: EachTrainInfo ActionSheetのスプリングアニメーション破綻を修正
|
||||
- `5914646` stackAwareNavigate関数を導入し、遷移時のナビゲーションロジックを改善
|
||||
- `a54ef7c` ナビゲーションロジックを改善し、stackAwareNavigate関数を導入して遷移時のスタック管理を強化。プライバシーポリシーと設計メモを追加。
|
||||
- `6b46c71` AppContainerのonStateChangeロジックを改善し、アクティブなルートの状態を正確にチェックするように修正
|
||||
- `9e2abc9` StatusBarの表示ロジックを改善し、Appsコンポーネントにフォーカス状態を追加
|
||||
- `ad5357c` 運用Hub情報の取得ロジックを改善し、貨物列車の車番処理を追加
|
||||
- `045ed21` 噂機能のスタイル強化
|
||||
82
docs/changelog-7.0.2-to-7.0.3-features.md
Normal file
@@ -0,0 +1,82 @@
|
||||
# a959cf39 -> 0b349148 更新ログ(機能説明フォーカス版)
|
||||
|
||||
## この差分で何が良くなったか
|
||||
この更新では、列車位置を「その場で見る」だけでなく、「記録してあとから再生する」ための機能が大きく強化されました。
|
||||
|
||||
同時に、mockデータを使った検証導線やWebView側の安定性も改善され、実データが不安定な状況でも挙動確認や表示調整を進めやすくなっています。
|
||||
|
||||
---
|
||||
|
||||
## 1. 列車位置の記録・再生機能を追加
|
||||
### 実装/更新された機能
|
||||
- 列車位置の記録・再生機能を追加。
|
||||
- 再生タイムラインUIを実装し、再生・一時停止・シーク操作に対応。
|
||||
- 経過時間を表示するステータスバーを追加。
|
||||
- 再生中はkeep-awakeで画面が落ちにくいよう調整。
|
||||
- 再生開始時クラッシュや複数録画時の扱いを修正。
|
||||
- 録画一覧の保存・再生・削除を設定画面から扱えるようにした。
|
||||
- 再生は記録時の時間差を反映して自動進行し、ループ末尾では待機を入れるようにした。
|
||||
|
||||
### ユーザーにとっての効果
|
||||
- 運行状況の変化をあとから振り返りやすくなります。
|
||||
- ある時点の列車位置を止めて確認できるため、検証や比較がしやすくなります。
|
||||
- 以前の録画形式が残っていても、継続して参照しやすくなります。
|
||||
|
||||
## 2. mock列車位置データでの検証がしやすくなった
|
||||
### 実装/更新された機能
|
||||
- WebViewへmock列車位置データを注入する仕組みを追加。
|
||||
- 設定画面に管理者向けのmock API切替を追加。
|
||||
- mock切替時にWebViewを再読み込みするよう改善。
|
||||
- app側の列車位置表示にもmockデータを反映するよう調整。
|
||||
- XHRインターセプタの初期化タイミングと互換性を改善。
|
||||
- 録画開始時はmockを自動でOFFにし、再生開始時は自動でONに切り替えるようにした。
|
||||
- WebView側では二重注入を避ける保護を入れ、mock適用の安定性を改善。
|
||||
|
||||
### ユーザーにとっての効果
|
||||
- 実データに依存せず、特定状況の表示確認や再現テストを行いやすくなります。
|
||||
- 検証時に「一部だけ反映されない」状態が起きにくくなります。
|
||||
|
||||
## 3. WebViewの読み込み失敗時の扱いを改善
|
||||
### 実装/更新された機能
|
||||
- ローディング表示とエラー表示を追加。
|
||||
- 再読み込み導線を追加。
|
||||
- アプリのバックグラウンド復帰時のリマウント制御を調整。
|
||||
- 注入JavaScript側のヘッダー配色を見直し、見やすさを改善。
|
||||
|
||||
### ユーザーにとっての効果
|
||||
- 読み込み失敗時に状況が分かりやすくなり、復旧操作もしやすくなります。
|
||||
- 復帰直後の表示崩れや古い状態の残留が起きにくくなります。
|
||||
|
||||
## 4. 列車情報の表示品質を改善
|
||||
### 実装/更新された機能
|
||||
- 編成情報表示用に、静的表示・フェード切替・アクティブ強調のチップUIを追加。
|
||||
- 列車番号の空白、メモ接尾辞、貨物列車記号を考慮した正規化処理を導入。
|
||||
- 番線表示用の `platformNum` をデータ構造へ追加。
|
||||
- 表示文字列やスタイル適用のタイミングを見直し、ちらつきや欠落を修正。
|
||||
|
||||
### ユーザーにとっての効果
|
||||
- 列車情報の見分けがしやすくなります。
|
||||
- データ照合が安定し、表示漏れや誤一致が起きにくくなります。
|
||||
|
||||
## 5. 設定・内部運用の整備
|
||||
### 実装/更新された機能
|
||||
- 内部バージョン表記を7.0.3へ更新。
|
||||
- 前バージョン差分ログをドキュメント化。
|
||||
- 検証用の列車データを更新。
|
||||
- 情報ソース設定画面を拡張し、外部データ連携の説明、録画管理、mock検証、投稿システム接続先の切替を一か所にまとめた。
|
||||
- 投稿システム接続先は、本番 / 実験 と リリース / ベータの切替を整理した。
|
||||
|
||||
### ユーザーにとっての効果
|
||||
- バージョン識別と運用確認がしやすくなります。
|
||||
- 今後の検証や保守時に、変更履歴を追いやすくなります。
|
||||
- 検証設定や接続先の切替が分かりやすくなり、試験時の操作ミスを減らしやすくなります。
|
||||
|
||||
---
|
||||
|
||||
## まとめ
|
||||
この更新は、列車位置を記録・再生して振り返るための新しい土台を追加しつつ、mock検証とWebView表示の安定性を同時に底上げした差分です。
|
||||
|
||||
日常利用の見た目改善だけでなく、再現確認や運用検証のしやすさまで含めて、一段階機能が広がった更新になっています。
|
||||
|
||||
## 参照
|
||||
- コミット差分精査版: docs/changelog-7.0.2-to-7.0.3.md
|
||||
142
docs/changelog-7.0.2-to-7.0.3.md
Normal file
@@ -0,0 +1,142 @@
|
||||
# コミット差分ログ(a959cf39 -> 0b349148)
|
||||
|
||||
## 対象範囲
|
||||
- From: `a959cf3973eb9635c9e61133d81de5c1f9719c18`
|
||||
- To: `0b349148d37802b6e86a09b7f34b718d622e92ca`
|
||||
- 集計メモ:
|
||||
- レンジ内コミット(merge含む): 29
|
||||
- 非mergeコミット: 27
|
||||
- 変更ファイル数: 50
|
||||
- 変更行数: +10785 / -185
|
||||
|
||||
## サマリー
|
||||
このレンジでは、列車位置の記録・再生機能の追加が中心です。
|
||||
|
||||
それを支える形で、mock列車位置データをWebViewへ注入するための検証基盤が大きく拡張され、あわせてWebViewの読み込み安定性、列車番号マッチング精度、表示コンポーネントの見やすさが改善されています。
|
||||
|
||||
前レンジが既存機能の安定化中心だったのに対して、このレンジは「記録して再生する」「mockで再現する」という検証・観測機能が大きく前進したのが特徴です。実装レベルでは、設定画面に録画管理や接続先切替UIが追加され、旧録画形式からの移行処理も入っています。
|
||||
|
||||
---
|
||||
|
||||
## 1. 列車位置の記録・再生機能を追加
|
||||
- 列車位置の記録と再生機能を新規追加。
|
||||
- 再生タイムラインUIを実装し、再生・一時停止・シーク操作に対応。
|
||||
- 経過時間表示とkeep-awakeを備えた `RecordingStatusBar` を追加。
|
||||
- 再生開始時クラッシュの修正と、複数録画データの取り扱いに対応。
|
||||
- 再生フレーム変更時にWebView上の列車表示が追従するよう改善。
|
||||
- 録画一覧の保存・削除・再生開始を設定画面から操作できるようにした。
|
||||
- 再生ループはスナップショット間の経過時間差を反映しつつ、通常時は最小3秒、ループ終端では15秒待機する制御を実装。
|
||||
- 旧単一録画フォーマットから複数録画フォーマットへの自動マイグレーション処理を追加。
|
||||
|
||||
主なコミット:
|
||||
- `37c08ad` feat: add train position record & playback feature
|
||||
- `8144e8a` feat: add playback timeline UI with pause/resume/seek controls
|
||||
- `3587f72` feat: add RecordingStatusBar with elapsed timer and keep-awake
|
||||
- `4f4d3ca` fix: crash on playback start + support multiple recordings
|
||||
- `d71cc37` fix: sync WebView train display when playback frame changes
|
||||
|
||||
## 2. mock列車位置データの注入基盤を強化
|
||||
- WebViewに対するXHRインターセプタを導入し、mock列車位置データの差し替えに対応。
|
||||
- 設定画面に管理者向けmock API切替を追加し、検証導線を整備。
|
||||
- 地図画面側の個別MOCKスイッチを整理し、設定側トグルに集約。
|
||||
- mock切替時にWebViewを再読み込みするよう改善し、反映漏れを抑制。
|
||||
- インターセプタ初期化タイミングを `injectedJavaScriptBeforeContentLoaded` へ移し、XHRフックの安定性を改善。
|
||||
- `setRequestHeader` と競合しないようXhr open処理を修正し、既存通信との互換性を改善。
|
||||
- app側の `currentTrain` にもmock列車位置を適用し、WebView外の表示も揃うよう調整。
|
||||
- 録画開始時はmockを自動OFF、録画再生時は自動ONに切り替える制御を追加。
|
||||
- XHRインターセプタに二重注入ガードを追加し、多重パッチによる不安定化を防止。
|
||||
|
||||
主なコミット:
|
||||
- `170fbf0` feat: add WebView XHR interceptor for mock train position injection
|
||||
- `71e1ad8` feat: add admin mock API toggle in settings and map screen switch
|
||||
- `4247318` fix: reload WebView on mock toggle and fix XHR callback timing bug
|
||||
- `59821a4` fix: rewrite XHR interceptor using prototype patching
|
||||
- `8321a47` fix: move XHR interceptor to injectedJavaScriptBeforeContentLoaded
|
||||
- `24f0c82` fix: call _origOpen even when intercepting to allow setRequestHeader
|
||||
- `a359568` refactor: remove map-screen MOCK switch; settings toggle controls mock directly
|
||||
- `92f4b37` feat: apply mock data to currentTrain (app-side train positions)
|
||||
|
||||
## 3. WebViewまわりの安定性と操作性を改善
|
||||
- `GeneralWebView` にローディング表示、エラー表示、再読み込み導線を追加。
|
||||
- `setReload` をグローバルへ公開し、外部からの再読み込み制御を改善。
|
||||
- `useWebViewRemount` に `backgroundThresholdMs` オプションを追加し、アプリ状態遷移時のリマウント制御を改善。
|
||||
- 注入JavaScript側のヘッダー色を見直し、視認性を改善。
|
||||
|
||||
主なコミット:
|
||||
- `4c1a315` fix: add loading and error handling in GeneralWebView component with reload functionality
|
||||
- `a8c785b` fix: expose setReload function to the global window object for accessibility
|
||||
- `b5eb830` fix: update useWebViewRemount to include backgroundThresholdMs option for better app state handling
|
||||
- `3554233` fix: update header colors in injected JavaScript for better visibility
|
||||
|
||||
## 4. 列車データの表示品質とマッチング精度を改善
|
||||
- 編成・車両情報の視認性向上のため、静的表示・フェード循環表示・アクティブ枠アニメ表示の3系統から成る `FormationChips` 系コンポーネントを追加。
|
||||
- train number の前後空白、メモ接尾辞、貨物列車の `レ` 記号を考慮した正規化処理を導入し、並び替え・抽出・照合の精度を改善。
|
||||
- `TrainDataSources` と `useUnyohub` で列車番号の比較ロジックを強化。
|
||||
- 貨物列車向けに 30xx / 90xx / 2桁番号の相互比較を行う補正ロジックを追加。
|
||||
- `platformNum` プロパティをデータ構造へ追加し、番線表示の精度を改善。
|
||||
- 表示文字列適用タイミングを見直し、observer反映直後のちらつきを軽減。
|
||||
- 非同期再描画やmock有効時に表示スタイルが欠落するケースを修正。
|
||||
|
||||
主なコミット:
|
||||
- `3a4e083` feat: add FormationChips, FadingSubCycler, and ActiveFormationChipsCycler components for enhanced train data visualization
|
||||
- `a9668e6` fix: trim train numbers in sorting and filtering logic for accurate matching
|
||||
- `45cc68a` fix: normalize train numbers by stripping suffixes in TrainDataSources and useUnyohub
|
||||
- `899c655` fix: add platformNum property to train data structures for improved display
|
||||
- `a809d28` fix: eliminate UX flash by calling setStrings() immediately in observer
|
||||
- `7566910` fix: re-apply Tokyo UX after async train re-renders
|
||||
- `2cf6b67` fix: Tokyo UX stripped when mock is active
|
||||
|
||||
## 5. バージョン更新と関連整備
|
||||
- 内部バージョン表記を7.0.3へ更新。
|
||||
- 前レンジの差分ログをドキュメントとして追加し、変更履歴の参照性を向上。
|
||||
- live disrupted data を含む `train.json` を更新し、検証データを最新化。
|
||||
- 情報ソース設定画面を拡張し、外部データソースの説明カード化、mock検証、録画管理、投稿システム接続先の系統/ UIバージョン切替を集約。
|
||||
- 接続先は 本番 / 実験 と リリース / ベータ の組み合わせで解決されるよう整理し、旧環境キーからの後方互換正規化も追加。
|
||||
|
||||
主なコミット:
|
||||
- `6ce2cad` fix: update version code to 7.0.3
|
||||
- `43f8095` feat: add changelog for version update from 064d81d4 to a959cf39, highlighting improvements in elesite integration, navigation stability, keyboard handling, and overall user experience
|
||||
- `1dcc25d` chore: update train.json with live disrupted data (2026-05-01 19:42)
|
||||
|
||||
---
|
||||
|
||||
## 変更リスクと確認ポイント
|
||||
- 記録/再生系:
|
||||
- 長時間記録データでタイムライン操作やシーク時の追従遅延、終端ループ待機が意図どおり動くか確認が必要。
|
||||
- mock注入系:
|
||||
- 実通信とmock通信の切替直後にキャッシュや古いXHRフックが残らないか、録画開始時OFF / 再生開始時ONの自動切替も含めて継続確認が必要。
|
||||
- WebView系:
|
||||
- 読み込み失敗後の再試行導線が端末差や回線差で安定して動くか確認が必要。
|
||||
- 表示整合性:
|
||||
- 列車番号正規化により、例外的な命名規則や貨物列車番号が意図せず同一扱いされないか確認が必要。
|
||||
- 設定/運用系:
|
||||
- 投稿システム接続先の切替で、実験系と本番系の URL 解決や旧設定値の移行が破綻しないか確認が必要。
|
||||
|
||||
## 付録: 非mergeコミット一覧(27件)
|
||||
- `6ce2cad` fix: update version code to 7.0.3
|
||||
- `3554233` fix: update header colors in injected JavaScript for better visibility
|
||||
- `d71cc37` fix: sync WebView train display when playback frame changes
|
||||
- `3587f72` feat: add RecordingStatusBar with elapsed timer and keep-awake
|
||||
- `4f4d3ca` fix: crash on playback start + support multiple recordings
|
||||
- `8144e8a` feat: add playback timeline UI with pause/resume/seek controls
|
||||
- `37c08ad` feat: add train position record & playback feature
|
||||
- `b5eb830` fix: update useWebViewRemount to include backgroundThresholdMs option for better app state handling
|
||||
- `92f4b37` feat: apply mock data to currentTrain (app-side train positions)
|
||||
- `a359568` refactor: remove map-screen MOCK switch; settings toggle controls mock directly
|
||||
- `a809d28` fix: eliminate UX flash by calling setStrings() immediately in observer
|
||||
- `7566910` fix: re-apply Tokyo UX after async train re-renders
|
||||
- `2cf6b67` fix: Tokyo UX stripped when mock is active
|
||||
- `1dcc25d` chore: update train.json with live disrupted data (2026-05-01 19:42)
|
||||
- `24f0c82` fix: call _origOpen even when intercepting to allow setRequestHeader
|
||||
- `8321a47` fix: move XHR interceptor to injectedJavaScriptBeforeContentLoaded
|
||||
- `59821a4` fix: rewrite XHR interceptor using prototype patching
|
||||
- `4247318` fix: reload WebView on mock toggle and fix XHR callback timing bug
|
||||
- `71e1ad8` feat: add admin mock API toggle in settings and map screen switch
|
||||
- `170fbf0` feat: add WebView XHR interceptor for mock train position injection
|
||||
- `899c655` fix: add platformNum property to train data structures for improved display
|
||||
- `4c1a315` fix: add loading and error handling in GeneralWebView component with reload functionality
|
||||
- `a8c785b` fix: expose setReload function to the global window object for accessibility
|
||||
- `a9668e6` fix: trim train numbers in sorting and filtering logic for accurate matching
|
||||
- `3a4e083` feat: add FormationChips, FadingSubCycler, and ActiveFormationChipsCycler components for enhanced train data visualization
|
||||
- `45cc68a` fix: normalize train numbers by stripping suffixes in TrainDataSources and useUnyohub
|
||||
- `43f8095` feat: add changelog for version update from 064d81d4 to a959cf39, highlighting improvements in elesite integration, navigation stability, keyboard handling, and overall user experience
|
||||
28
docs/keyboard-animation-tuning-2026-04-08.md
Normal file
@@ -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)`.
|
||||
138
docs/keyboard-animation-tuning-2026-04-09.md
Normal file
@@ -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<Animated.CompositeAnimation | null>(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 アニメーション完了(キーボード出現と同期)
|
||||
```
|
||||
84
docs/privacy-policy-future.md
Normal file
@@ -0,0 +1,84 @@
|
||||
# プライバシーポリシー 将来追加予定セクション
|
||||
|
||||
このファイルは、今後の機能実装時にプライバシーポリシーへ追加する内容の設計メモです。
|
||||
各機能のリリース時に privacy-policy.md へ統合してください。
|
||||
|
||||
---
|
||||
|
||||
## アカウント機能(OAuthログイン)リリース時に追加
|
||||
|
||||
### 1.1 アカウント情報(新規セクション)
|
||||
|
||||
本アプリでは、ソーシャルログイン(OAuth)によるアカウント機能を提供します。
|
||||
|
||||
- ログイン時に、利用する認証プロバイダ(Google、Apple等)から以下の情報を取得します。
|
||||
- 認証プロバイダが発行するユーザー識別子(ID)
|
||||
- 表示名(認証プロバイダが提供する場合)
|
||||
- メールアドレス(認証プロバイダが提供する場合)
|
||||
- これらの情報はアカウント管理および設定同期のために開発者のサーバーに保存します。
|
||||
- パスワードを開発者側で保持することはありません。認証は各プロバイダに委任されます。
|
||||
- アカウントの作成は任意であり、アカウントなしでも本アプリの基本機能は利用できます。
|
||||
|
||||
### 1.3 ユーザー設定の同期(新規セクション)
|
||||
|
||||
- アカウントにログインしている場合、以下の設定情報をサーバーに同期し、複数端末間で共有できます。
|
||||
- お気に入り駅
|
||||
- 表示設定(テーマ、レイアウト等)
|
||||
- 通知設定
|
||||
- カスタマイズ設定(アプリアイコン選択等)
|
||||
- 同期はアカウントにログインしている場合にのみ行われます。未ログイン時はすべて端末内にのみ保存されます。
|
||||
|
||||
### 3.1 第三者サービス表に追加
|
||||
|
||||
| OAuth認証プロバイダ(Google、Apple等) | ソーシャルログイン | 各プロバイダ |
|
||||
|
||||
### 6. データの保存期間に追加
|
||||
|
||||
- **アカウント情報・同期設定:** アカウントが削除されるまで保持します。
|
||||
- プッシュ通知トークンの記述に追記: 「またはアカウント削除時に削除されます。」
|
||||
|
||||
### 7. データの削除に追加
|
||||
|
||||
#### アカウントの削除
|
||||
- アプリ内の設定画面からアカウントを削除できます。アカウント削除時に、サーバーに保存されたアカウント情報、同期設定データ、およびプッシュ通知トークンを削除します。
|
||||
|
||||
### 9. 児童のプライバシーに追記
|
||||
|
||||
- 13歳未満の児童がアカウント作成等を通じて個人情報を提供したことが判明した場合、速やかに当該アカウントおよび関連情報を削除します。
|
||||
|
||||
---
|
||||
|
||||
## FeliCa OSSデータベース機能リリース時に追加
|
||||
|
||||
### 1.4 ユーザーが任意で提供する情報(FeliCaオープンデータ)(新規セクション)
|
||||
|
||||
本アプリでは、交通系ICカードの駅コード・改札コードに関するオープンデータベースの構築に参加する機能を提供します。
|
||||
|
||||
#### 提供されるデータの範囲
|
||||
- **提供対象:** 駅コード、改札コード、およびユーザーが入力する実績説明(駅名・改札名の対応情報等)
|
||||
- **提供対象外:** IDm(製造ID)、残高、個人の乗降履歴、その他個人を特定しうる情報
|
||||
|
||||
#### データの公開
|
||||
- 提供されたデータは、オープンソースのデータベースとして**一般に公開**されます。
|
||||
- 公開されるデータには提供者個人を特定する情報は含まれません。
|
||||
- 一度公開されたデータは、オープンソースとして第三者が複製・再配布する可能性があるため、**完全な削除を保証することはできません**。
|
||||
|
||||
#### 同意と任意性
|
||||
- データの提供は完全に任意であり、提供しなくても本アプリの機能に制限はありません。
|
||||
- データの提供時に、提供内容と公開範囲について個別に同意を求めます。
|
||||
|
||||
### 3.2 FeliCaオープンデータの公開(新規セクション)
|
||||
|
||||
ユーザーが任意で提供したFeliCa駅コード・改札コードのデータは、オープンソースデータベースとして一般に公開されます。公開データに個人を特定する情報は含まれません。
|
||||
|
||||
### 6. データの保存期間に追加
|
||||
|
||||
- **FeliCaオープンデータ:** オープンソースとして無期限に公開されます。
|
||||
|
||||
### 7. データの削除に追記
|
||||
|
||||
- アカウント削除後も、FeliCaオープンデータベースに提供済みのデータは、オープンソースデータとして公開が継続されます。
|
||||
|
||||
### 1.2 ICカード個人データの記述に追記
|
||||
|
||||
- ICカードのデータ提供機能(後述の1.4)とは明確に区別されます。
|
||||
151
docs/privacy-policy.md
Normal file
@@ -0,0 +1,151 @@
|
||||
# 『JR四国非公式アプリ』プライバシーポリシー
|
||||
|
||||
最終更新日: 2026年4月3日
|
||||
|
||||
本プライバシーポリシー(以下「本ポリシー」)は、harukin(以下「開発者」)が提供する「JR四国非公式アプリ」(以下「本アプリ」)における、ユーザー情報の取り扱いについて定めるものです。
|
||||
|
||||
本アプリはJR四国(四国旅客鉄道株式会社)とは一切関係のない非公式アプリです。
|
||||
|
||||
---
|
||||
|
||||
## 1. 収集する情報
|
||||
|
||||
### 1.1 端末から取得し、端末内のみで利用する情報
|
||||
|
||||
以下の情報は端末内での処理にのみ使用し、外部サーバーへの送信は行いません。
|
||||
|
||||
#### 位置情報
|
||||
- ユーザーの現在地を取得し、最寄り駅の情報表示や地図上での現在地表示に利用します。
|
||||
- 位置情報はアプリがフォアグラウンドで動作している間のみ取得し、アプリを終了すると取得を停止します。
|
||||
- 位置情報の利用は任意ですが、許可しない場合は最寄り駅の自動検出などの一部機能が制限されます。
|
||||
|
||||
#### ICカードデータ(NFC/FeliCa)
|
||||
- NFC対応端末において、交通系ICカードのIDm(製造ID)、残高、システムコード、利用履歴を読み取ります。
|
||||
- 読み取った情報は端末内(AsyncStorage)にのみ保存され、ウィジェット等での残高表示に使用します。
|
||||
- IDm、残高、個人の利用履歴等のデータは外部サーバーへ送信しません。
|
||||
|
||||
#### ユーザー設定・お気に入り
|
||||
- お気に入り駅、表示設定(テーマ・レイアウト等)、通知設定、カスタマイズ設定などのユーザー設定は、すべて端末内(AsyncStorage)にのみ保存されます。
|
||||
|
||||
### 1.2 外部サーバーに送信する情報
|
||||
|
||||
以下の情報は、機能の提供に必要な範囲で開発者が運営するサーバーに送信されます。
|
||||
|
||||
#### プッシュ通知トークン(Expo Push Token)
|
||||
- プッシュ通知の配信および一部機能の権限確認のために、端末固有のプッシュ通知トークンを開発者のサーバーに送信します。
|
||||
- このトークンはユーザーの識別にも使用されますが、氏名・メールアドレス等の個人を直接特定する情報とは紐づけません。
|
||||
|
||||
#### 通知設定情報
|
||||
- プッシュ通知機能を利用する場合、通知カテゴリの設定内容(遅延情報EX、運行情報、怪レい列車BOTの各ON/OFF)をプッシュ通知トークンとともにサーバーに送信・保存します。
|
||||
|
||||
---
|
||||
|
||||
## 2. 情報の利用目的
|
||||
|
||||
収集した情報は以下の目的でのみ利用します。
|
||||
|
||||
- 最寄り駅の検出および地図上での現在地表示
|
||||
- ICカード残高・利用履歴の端末内表示
|
||||
- プッシュ通知の配信(列車遅延情報、運行情報等)
|
||||
- 一部機能における権限確認
|
||||
- アプリの機能改善および不具合対応
|
||||
|
||||
---
|
||||
|
||||
## 3. 第三者への情報提供
|
||||
|
||||
開発者は、収集した情報を第三者に販売または貸与することはありません。
|
||||
|
||||
ただし、本アプリは以下の第三者サービスを利用しており、各サービスの利用規約およびプライバシーポリシーに基づき、当該サービス提供者が情報を取得する場合があります。
|
||||
|
||||
| サービス | 用途 | 提供者 |
|
||||
|---------|------|--------|
|
||||
| Google Maps SDK | 地図表示・駅マーカー表示 | Google LLC |
|
||||
| Firebase Cloud Messaging (FCM) | プッシュ通知基盤 | Google LLC |
|
||||
| Expo Push Notifications | プッシュ通知配信 | Expo |
|
||||
| Expo Updates | アプリのOTAアップデート配信 | Expo |
|
||||
|
||||
各サービスのプライバシーポリシーについては、以下をご参照ください。
|
||||
- Google: https://policies.google.com/privacy
|
||||
- Expo: https://expo.dev/privacy
|
||||
|
||||
---
|
||||
|
||||
## 4. WebViewを通じた第三者サイトへのアクセス
|
||||
|
||||
本アプリでは、以下のWebサイトをWebViewにより表示します。これらのサイトにおけるデータの取り扱いは各サイトのプライバシーポリシーに従います。
|
||||
|
||||
- JR四国 列車走行位置(https://train.jr-shikoku.co.jp/)
|
||||
- JR四国 運行情報(https://www.jr-shikoku.co.jp/info/)
|
||||
|
||||
---
|
||||
|
||||
## 5. 第三者データソースの利用
|
||||
|
||||
本アプリでは、設定画面から任意で以下の第三者データソースとの連携を有効にできます。
|
||||
|
||||
- 鉄道運用Hub(unyohub)
|
||||
- えれサイト(elesite)
|
||||
|
||||
これらのデータソースから取得したデータは表示目的でのみ使用し、ユーザーの情報をこれらのサービスに送信することはありません。連携機能を利用する場合は、各サービスの利用規約に同意したものとみなします。
|
||||
|
||||
一部の第三者データソース(鉄道運用Hub等)は独自のログイン機能を提供しています。ユーザーがWebView内でログインした場合、そのセッション情報(Cookie等)はアプリ内のWebViewストレージに保持されます。開発者がこれらの認証情報にアクセスすることはありません。セッション情報の取り扱いは各サービスのプライバシーポリシーに従います。
|
||||
|
||||
---
|
||||
|
||||
## 6. データの保存期間
|
||||
|
||||
### サーバー側データ
|
||||
- **プッシュ通知トークン・通知設定:** ユーザーが明示的に削除を要求するまで、またはサービス終了まで保持します。
|
||||
|
||||
### 端末内データ
|
||||
- ユーザー設定、お気に入り、ICカードデータ、キャッシュデータ等は、ユーザーがアプリをアンインストールするか、端末内のデータを消去するまで保持されます。
|
||||
|
||||
---
|
||||
|
||||
## 7. データの削除
|
||||
|
||||
### 端末内データの削除
|
||||
- アプリのアンインストール、または端末の設定からアプリのデータ消去を行うことで、端末内に保存されたすべてのデータを削除できます。
|
||||
|
||||
### サーバー側データの削除
|
||||
- サーバーに保存されたプッシュ通知トークンおよび通知設定の削除を希望する場合は、後述の問い合わせ先までご連絡ください。
|
||||
|
||||
---
|
||||
|
||||
## 8. セキュリティ
|
||||
|
||||
開発者は、収集した情報の漏洩、紛失、改ざんを防止するために合理的な技術的措置を講じます。ただし、インターネットを通じたデータ送信の安全性を完全に保証するものではありません。
|
||||
|
||||
---
|
||||
|
||||
## 9. 児童のプライバシー
|
||||
|
||||
本アプリは特定の年齢層を対象としたものではなく、13歳未満の児童から意図的に個人情報を収集することはありません。
|
||||
|
||||
---
|
||||
|
||||
## 10. アナリティクス・広告
|
||||
|
||||
本アプリはアクセス解析ツール(アナリティクス)および広告配信サービスを使用していません。
|
||||
|
||||
---
|
||||
|
||||
## 11. 本ポリシーの変更
|
||||
|
||||
開発者は、本ポリシーを変更する場合があります。重要な変更を行う場合は、アプリ内のお知らせまたは開発者のWebサイトにて通知します。変更後も本アプリを継続利用した場合、変更後のポリシーに同意したものとみなします。
|
||||
|
||||
---
|
||||
|
||||
## 12. お問い合わせ先
|
||||
|
||||
本ポリシーに関するお問い合わせは、以下の窓口までご連絡ください。
|
||||
|
||||
- **開発者:** harukin(Xprocess)
|
||||
- **Twitter/X:** [@xprocess_main](https://twitter.com/xprocess_main)
|
||||
- **マシュマロ(匿名質問箱):** https://marshmallow-qa.com/pag3sl0ju3g1jm7
|
||||
- **Webサイト:** https://haruk.in
|
||||
|
||||
---
|
||||
|
||||
© 2022 - 2026 harukin
|
||||
194
docs/tutorial-feature-plan.md
Normal file
@@ -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はこちら | コーチマーク実装時に検討 |
|
||||
129
lib/jrDataSystemEnvironment.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
export const BACKEND_API_BASE_URLS = {
|
||||
production: "https://jr-shikoku-backend-api-v1.haruk.in",
|
||||
experimental: "https://jr-shikoku-backend-api-v1-beta.haruk.in",
|
||||
} as const;
|
||||
|
||||
export const JR_DATA_SYSTEM_ENVS = {
|
||||
production_release: {
|
||||
label: "本番 / リリース",
|
||||
caption: "一般公開向け運用",
|
||||
baseUrl: "https://shikoku-railinfo.haruk.in",
|
||||
track: "production",
|
||||
uiVariant: "release",
|
||||
backendApiBaseUrl: BACKEND_API_BASE_URLS.production,
|
||||
},
|
||||
production_beta: {
|
||||
label: "本番 / ベータ",
|
||||
caption: "UI検証向け運用",
|
||||
baseUrl: "https://nightly.shikoku-railinfo.haruk.in",
|
||||
track: "production",
|
||||
uiVariant: "beta",
|
||||
backendApiBaseUrl: BACKEND_API_BASE_URLS.production,
|
||||
},
|
||||
experimental: {
|
||||
label: "実験 / 実験場",
|
||||
caption: "毎日リセットされる実験環境",
|
||||
baseUrl: "https://experimental.shikoku-railinfo.haruk.in",
|
||||
track: "experimental",
|
||||
uiVariant: "release",
|
||||
backendApiBaseUrl: BACKEND_API_BASE_URLS.experimental,
|
||||
},
|
||||
} as const;
|
||||
|
||||
export type JrDataSystemEnvironmentKey = keyof typeof JR_DATA_SYSTEM_ENVS;
|
||||
|
||||
export const DEFAULT_JR_DATA_SYSTEM_ENV: JrDataSystemEnvironmentKey =
|
||||
"production_release";
|
||||
|
||||
export type JrDataSystemTrack = "production" | "experimental";
|
||||
export type JrDataSystemUiVariant = "release" | "beta";
|
||||
|
||||
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 => {
|
||||
// Backward compatibility for legacy keys.
|
||||
if (value === "production") return "production_release";
|
||||
if (value === "chatgpt" || value === "claude") return "production_beta";
|
||||
|
||||
if (typeof value === "string" && value in JR_DATA_SYSTEM_ENVS) {
|
||||
return value as JrDataSystemEnvironmentKey;
|
||||
}
|
||||
return DEFAULT_JR_DATA_SYSTEM_ENV;
|
||||
};
|
||||
|
||||
export const resolveJrDataSystemEnvironment = (
|
||||
track: JrDataSystemTrack,
|
||||
uiVariant: JrDataSystemUiVariant,
|
||||
): JrDataSystemEnvironmentKey => {
|
||||
if (track === "experimental") {
|
||||
return "experimental";
|
||||
}
|
||||
return uiVariant === "beta" ? "production_beta" : "production_release";
|
||||
};
|
||||
|
||||
export const getJrDataSystemTrack = (
|
||||
environment: unknown,
|
||||
): JrDataSystemTrack => {
|
||||
const envKey = normalizeJrDataSystemEnvironment(environment);
|
||||
return JR_DATA_SYSTEM_ENVS[envKey].track;
|
||||
};
|
||||
|
||||
export const getJrDataSystemUiVariant = (
|
||||
environment: unknown,
|
||||
): JrDataSystemUiVariant => {
|
||||
const envKey = normalizeJrDataSystemEnvironment(environment);
|
||||
return JR_DATA_SYSTEM_ENVS[envKey].uiVariant;
|
||||
};
|
||||
|
||||
export const getBackendApiBaseUrl = (environment: unknown): string => {
|
||||
const envKey = normalizeJrDataSystemEnvironment(environment);
|
||||
return JR_DATA_SYSTEM_ENVS[envKey].backendApiBaseUrl;
|
||||
};
|
||||
|
||||
export const rewriteBackendApiUrl = (url: string, environment: unknown): string => {
|
||||
if (typeof url !== "string" || url.length === 0) return url;
|
||||
const target = getBackendApiBaseUrl(environment);
|
||||
for (const base of Object.values(BACKEND_API_BASE_URLS)) {
|
||||
if (url.startsWith(base)) {
|
||||
return url.replace(base, target);
|
||||
}
|
||||
}
|
||||
return url;
|
||||
};
|
||||
|
||||
export const rewriteJrDataSystemUrl = (
|
||||
uri: string,
|
||||
environment: unknown,
|
||||
): string => {
|
||||
if (typeof uri !== "string" || uri.length === 0) {
|
||||
return uri;
|
||||
}
|
||||
|
||||
const envKey = normalizeJrDataSystemEnvironment(environment);
|
||||
const targetBaseUrl = JR_DATA_SYSTEM_ENVS[envKey].baseUrl;
|
||||
|
||||
const knownBaseUrls = [
|
||||
"https://jr-shikoku-data-system.pages.dev",
|
||||
JR_DATA_SYSTEM_ENVS.production_release.baseUrl,
|
||||
JR_DATA_SYSTEM_ENVS.production_beta.baseUrl,
|
||||
JR_DATA_SYSTEM_ENVS.experimental.baseUrl,
|
||||
];
|
||||
|
||||
for (const baseUrl of knownBaseUrls) {
|
||||
if (uri.startsWith(baseUrl)) {
|
||||
return uri.replace(baseUrl, targetBaseUrl);
|
||||
}
|
||||
}
|
||||
|
||||
return uri;
|
||||
};
|
||||
23
lib/mockApi/index.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Mock API index
|
||||
*
|
||||
* Re-exports the XHR interceptor generator and pre-loaded sample data
|
||||
* captured from the official JR Shikoku train position site.
|
||||
*
|
||||
* Usage example:
|
||||
* import { MOCK_TRAIN_POSITIONS } from '@/lib/mockApi';
|
||||
* const { setMockApiEnabled, setMockTrainPositions } = useTrainMenu();
|
||||
*
|
||||
* // Enable mock mode with sample data
|
||||
* setMockTrainPositions(MOCK_TRAIN_POSITIONS);
|
||||
* setMockApiEnabled(true);
|
||||
*/
|
||||
|
||||
export { generateXhrInterceptorJs, MockApiConfig, TrainEntry } from './webviewXhrInterceptor';
|
||||
|
||||
// Pre-captured sample train position data from the official site
|
||||
import trainJson from './mockData/train.json';
|
||||
export const MOCK_TRAIN_POSITIONS = trainJson.filter(
|
||||
(entry): entry is import('./webviewXhrInterceptor').TrainEntry =>
|
||||
'TrainNum' in entry,
|
||||
);
|
||||
592
lib/mockApi/mockData/between.json
Normal file
@@ -0,0 +1,592 @@
|
||||
[
|
||||
{
|
||||
"BetweenStation": "高松~鬼無",
|
||||
"Datas": [
|
||||
{
|
||||
"StationName": "香西",
|
||||
"StationNumber": "Y01",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/kozai.pdf"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"BetweenStation": "端岡~鴨川",
|
||||
"Datas": [
|
||||
{
|
||||
"StationName": "国分",
|
||||
"StationNumber": "Y04",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/kokubu.pdf"
|
||||
},
|
||||
{
|
||||
"StationName": "讃岐府中",
|
||||
"StationNumber": "Y05",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/sanuki-fuchu.pdf"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"BetweenStation": "鴨川~坂出",
|
||||
"Datas": [
|
||||
{
|
||||
"StationName": "八十場",
|
||||
"StationNumber": "Y07",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/yasoba.pdf"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"BetweenStation": "丸亀~多度津",
|
||||
"Datas": [
|
||||
{
|
||||
"StationName": "讃岐塩屋",
|
||||
"StationNumber": "Y11",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/sanuki-shioya.pdf"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"BetweenStation": "詫間~高瀬",
|
||||
"Datas": [
|
||||
{
|
||||
"StationName": "みの",
|
||||
"StationNumber": "Y15",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/mino.pdf"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"BetweenStation": "高瀬~本山",
|
||||
"Datas": [
|
||||
{
|
||||
"StationName": "比地大",
|
||||
"StationNumber": "Y17",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/hijidai.pdf"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"BetweenStation": "伊予寒川~伊予土居",
|
||||
"Datas": [
|
||||
{
|
||||
"StationName": "赤星",
|
||||
"StationNumber": "Y25",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/akaboshi.pdf"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"BetweenStation": "石鎚山~伊予小松",
|
||||
"Datas": [
|
||||
{
|
||||
"StationName": "伊予氷見",
|
||||
"StationNumber": "Y33",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/iyo-himi.pdf"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"BetweenStation": "伊予小松~壬生川",
|
||||
"Datas": [
|
||||
{
|
||||
"StationName": "玉之江",
|
||||
"StationNumber": "Y35",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/tamanoe.pdf"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"BetweenStation": "伊予北条~粟井",
|
||||
"Datas": [
|
||||
{
|
||||
"StationName": "柳原",
|
||||
"StationNumber": "Y49",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/yanagihara.pdf"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"BetweenStation": "粟井~堀江",
|
||||
"Datas": [
|
||||
{
|
||||
"StationName": "光洋台",
|
||||
"StationNumber": "Y51",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/koyodai.pdf"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"BetweenStation": "高松~栗林",
|
||||
"Datas": [
|
||||
{
|
||||
"StationName": "昭和町",
|
||||
"StationNumber": "T27",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/showacho.pdf"
|
||||
},
|
||||
{
|
||||
"StationName": "栗林公園北口",
|
||||
"StationNumber": "T26",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/ritsurinkoen-kitaguchi.pdf"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"BetweenStation": "栗林~屋島",
|
||||
"Datas": [
|
||||
{
|
||||
"StationName": "木太町",
|
||||
"StationNumber": "T24",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/kitacho.pdf"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"BetweenStation": "屋島~八栗口",
|
||||
"Datas": [
|
||||
{
|
||||
"StationName": "古高松南",
|
||||
"StationNumber": "T22",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/furutakamatsu-minami.pdf"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"BetweenStation": "八栗口~志度",
|
||||
"Datas": [
|
||||
{
|
||||
"StationName": "讃岐牟礼",
|
||||
"StationNumber": "T20",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/sanuki-mure.pdf"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"BetweenStation": "造田~讃岐津田",
|
||||
"Datas": [
|
||||
{
|
||||
"StationName": "神前",
|
||||
"StationNumber": "T16",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/kanzaki.pdf"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"BetweenStation": "板野~板東",
|
||||
"Datas": [
|
||||
{
|
||||
"StationName": "阿波川端",
|
||||
"StationNumber": "T06",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/awa-kawabata.pdf"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"BetweenStation": "北伊予~伊予市",
|
||||
"Datas": [
|
||||
{
|
||||
"StationName": "南伊予",
|
||||
"StationNumber": "U02-1",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/minami-iyo.pdf"
|
||||
},
|
||||
{
|
||||
"StationName": "伊予横田",
|
||||
"StationNumber": "U03",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/iyo-yokota.pdf"
|
||||
},
|
||||
{
|
||||
"StationName": "鳥ノ木",
|
||||
"StationNumber": "U04",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/torinoki.pdf"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"BetweenStation": "向井原~伊予中山",
|
||||
"Datas": [
|
||||
{
|
||||
"StationName": "伊予大平",
|
||||
"StationNumber": "U07",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/iyo-ohira.pdf"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"BetweenStation": "内子~新谷",
|
||||
"Datas": [
|
||||
{
|
||||
"StationName": "五十崎",
|
||||
"StationNumber": "U11",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/ikazaki.pdf"
|
||||
},
|
||||
{
|
||||
"StationName": "喜多山",
|
||||
"StationNumber": "U12",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/kitayama.pdf"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"BetweenStation": "伊予大洲~伊予平野",
|
||||
"Datas": [
|
||||
{
|
||||
"StationName": "西大洲",
|
||||
"StationNumber": "U15",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/nishi-ozu.pdf"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"BetweenStation": "伊予石城~卯之町",
|
||||
"Datas": [
|
||||
{
|
||||
"StationName": "上宇和",
|
||||
"StationNumber": "U21",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/kami-uwa.pdf"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"BetweenStation": "伊予吉田~北宇和島",
|
||||
"Datas": [
|
||||
{
|
||||
"StationName": "高光",
|
||||
"StationNumber": "U26",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/takamitsu.pdf"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"BetweenStation": "向井原~伊予上灘",
|
||||
"Datas": [
|
||||
{
|
||||
"StationName": "高野川",
|
||||
"StationNumber": "S07",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/konokawa.pdf"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"BetweenStation": "伊予上灘~伊予長浜",
|
||||
"Datas": [
|
||||
{
|
||||
"StationName": "下灘",
|
||||
"StationNumber": "S09",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/shimonada.pdf"
|
||||
},
|
||||
{
|
||||
"StationName": "串",
|
||||
"StationNumber": "S10",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/kushi.pdf"
|
||||
},
|
||||
{
|
||||
"StationName": "喜多灘",
|
||||
"StationNumber": "S11",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/kitanada.pdf"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"BetweenStation": "伊予長浜~伊予白滝",
|
||||
"Datas": [
|
||||
{
|
||||
"StationName": "伊予出石",
|
||||
"StationNumber": "S13",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/iyo-izushi.pdf"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"BetweenStation": "伊予白滝~伊予大洲",
|
||||
"Datas": [
|
||||
{
|
||||
"StationName": "八多喜",
|
||||
"StationNumber": "S15",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/hataki.pdf"
|
||||
},
|
||||
{
|
||||
"StationName": "春賀",
|
||||
"StationNumber": "S16",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/haruka.pdf"
|
||||
},
|
||||
{
|
||||
"StationName": "五郎",
|
||||
"StationNumber": "S17",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/goro.pdf"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"BetweenStation": "塩入~讃岐財田",
|
||||
"Datas": [
|
||||
{
|
||||
"StationName": "黒川",
|
||||
"StationNumber": "D17",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/kurokawa.pdf"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"BetweenStation": "三縄~阿波川口",
|
||||
"Datas": [
|
||||
{
|
||||
"StationName": "祖谷口",
|
||||
"StationNumber": "D24",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/iyaguchi.pdf"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"BetweenStation": "大田口~大杉",
|
||||
"Datas": [
|
||||
{
|
||||
"StationName": "土佐穴内",
|
||||
"StationNumber": "D31",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/tosa-ananai.pdf"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"BetweenStation": "土佐北川~繁藤",
|
||||
"Datas": [
|
||||
{
|
||||
"StationName": "角茂谷",
|
||||
"StationNumber": "D34",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/kakumodani.pdf"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"BetweenStation": "土佐山田~後免",
|
||||
"Datas": [
|
||||
{
|
||||
"StationName": "山田西町",
|
||||
"StationNumber": "D38",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/yamadanishimachi.pdf"
|
||||
},
|
||||
{
|
||||
"StationName": "土佐長岡",
|
||||
"StationNumber": "D39",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/tosa-nagaoka.pdf"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"BetweenStation": "土佐大津~土佐一宮",
|
||||
"Datas": [
|
||||
{
|
||||
"StationName": "布師田",
|
||||
"StationNumber": "D42",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/nunoshida.pdf"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"BetweenStation": "高知~旭",
|
||||
"Datas": [
|
||||
{
|
||||
"StationName": "入明",
|
||||
"StationNumber": "K01",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/iriake.pdf"
|
||||
},
|
||||
{
|
||||
"StationName": "円行寺口",
|
||||
"StationNumber": "K02",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/engyojiguchi.pdf"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"BetweenStation": "旭~朝倉",
|
||||
"Datas": [
|
||||
{
|
||||
"StationName": "高知商業前",
|
||||
"StationNumber": "K04",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/kochi-shogyomae.pdf"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"BetweenStation": "朝倉~伊野",
|
||||
"Datas": [
|
||||
{
|
||||
"StationName": "枝川",
|
||||
"StationNumber": "K06",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/edagawa.pdf"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"BetweenStation": "伊野~日下",
|
||||
"Datas": [
|
||||
{
|
||||
"StationName": "波川",
|
||||
"StationNumber": "K08",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/hakawa.pdf"
|
||||
},
|
||||
{
|
||||
"StationName": "小村神社前",
|
||||
"StationNumber": "K08-1",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/omurajinjamae.pdf"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"BetweenStation": "日下~土佐加茂",
|
||||
"Datas": [
|
||||
{
|
||||
"StationName": "岡花",
|
||||
"StationNumber": "K10",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/okabana.pdf"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"BetweenStation": "佐川~斗賀野",
|
||||
"Datas": [
|
||||
{
|
||||
"StationName": "襟野々",
|
||||
"StationNumber": "K14",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/erinono.pdf"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"BetweenStation": "多ノ郷~須崎",
|
||||
"Datas": [
|
||||
{
|
||||
"StationName": "大間",
|
||||
"StationNumber": "K18",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/oma.pdf"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"BetweenStation": "須崎~土佐久礼",
|
||||
"Datas": [
|
||||
{
|
||||
"StationName": "土佐新荘",
|
||||
"StationNumber": "K20",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/tosa-shinjo.pdf"
|
||||
},
|
||||
{
|
||||
"StationName": "安和",
|
||||
"StationNumber": "K21",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/awa.pdf"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"BetweenStation": "影野~窪川",
|
||||
"Datas": [
|
||||
{
|
||||
"StationName": "六反地",
|
||||
"StationNumber": "K24",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/rokutanji.pdf"
|
||||
},
|
||||
{
|
||||
"StationName": "仁井田",
|
||||
"StationNumber": "K25",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/niida.pdf"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"BetweenStation": "蔵本~府中",
|
||||
"Datas": [
|
||||
{
|
||||
"StationName": "鮎喰",
|
||||
"StationNumber": "B03",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/akui.pdf"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"BetweenStation": "石井~牛島",
|
||||
"Datas": [
|
||||
{
|
||||
"StationName": "下浦",
|
||||
"StationNumber": "B06",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/shimoura.pdf"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"BetweenStation": "牛島~鴨島",
|
||||
"Datas": [
|
||||
{
|
||||
"StationName": "麻植塚",
|
||||
"StationNumber": "B08",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/oezuka.pdf"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"BetweenStation": "鴨島~阿波川島",
|
||||
"Datas": [
|
||||
{
|
||||
"StationName": "西麻植",
|
||||
"StationNumber": "B10",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/nishi-oe.pdf"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"BetweenStation": "山瀬~川田",
|
||||
"Datas": [
|
||||
{
|
||||
"StationName": "阿波山川",
|
||||
"StationNumber": "B14",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/awa-yamakawa.pdf"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"BetweenStation": "貞光~江口",
|
||||
"Datas": [
|
||||
{
|
||||
"StationName": "阿波半田",
|
||||
"StationNumber": "B19",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/awa-handa.pdf"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"BetweenStation": "江口~阿波加茂",
|
||||
"Datas": [
|
||||
{
|
||||
"StationName": "三加茂",
|
||||
"StationNumber": "B21",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/mikamo.pdf"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"BetweenStation": "鳴門~池谷",
|
||||
"Datas": [
|
||||
{
|
||||
"StationName": "撫養",
|
||||
"StationNumber": "N09",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/muya.pdf"
|
||||
},
|
||||
{
|
||||
"StationName": "金比羅前",
|
||||
"StationNumber": "N08",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/kompiramae.pdf"
|
||||
},
|
||||
{
|
||||
"StationName": "教会前",
|
||||
"StationNumber": "N07",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/kyokaimae.pdf"
|
||||
},
|
||||
{
|
||||
"StationName": "立道",
|
||||
"StationNumber": "N06",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/tatsumichi.pdf"
|
||||
},
|
||||
{
|
||||
"StationName": "阿波大谷",
|
||||
"StationNumber": "N05",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/awa-otani.pdf"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
74
lib/mockApi/mockData/cross.json
Normal file
@@ -0,0 +1,74 @@
|
||||
[
|
||||
{
|
||||
"StartStationName": "坂出",
|
||||
"EndStationName": "宇多津",
|
||||
"LineName": "seto",
|
||||
"Point": "Up",
|
||||
"Half": "F-End",
|
||||
"DispPos": "seto_0_児島"
|
||||
},
|
||||
{
|
||||
"StartStationName": "多度津",
|
||||
"EndStationName": "海岸寺",
|
||||
"LineName": "dosan",
|
||||
"Point": "Down",
|
||||
"Half": "Start",
|
||||
"DispPos": "dosan_0_多度津"
|
||||
},
|
||||
{
|
||||
"StartStationName": "向井原",
|
||||
"EndStationName": "伊予中山",
|
||||
"LineName": "uwajima2",
|
||||
"Point": "Up",
|
||||
"Half": "Start",
|
||||
"DispPos": "uwajima2_0_向井原"
|
||||
},
|
||||
{
|
||||
"StartStationName": "新谷",
|
||||
"EndStationName": "伊予大洲",
|
||||
"LineName": "uwajima2",
|
||||
"Point": "Up",
|
||||
"Half": "End",
|
||||
"DispPos": "uwajima2_3_伊予大洲"
|
||||
},
|
||||
{
|
||||
"StartStationName": "箸蔵",
|
||||
"EndStationName": "佃",
|
||||
"LineName": "tokushima",
|
||||
"Point": "Down",
|
||||
"Half": "End",
|
||||
"DispPos": "tokushima_16_佃"
|
||||
},
|
||||
{
|
||||
"StartStationName": "吉成",
|
||||
"EndStationName": "佐古",
|
||||
"LineName": "tokushima",
|
||||
"Point": "Up",
|
||||
"Half": "End",
|
||||
"DispPos": "tokushima_0_徳島"
|
||||
},
|
||||
{
|
||||
"StartStationName": "佐古",
|
||||
"EndStationName": "蔵本",
|
||||
"LineName": "koutoku",
|
||||
"Point": "Up",
|
||||
"Half": "Start",
|
||||
"DispPos": "koutoku_19_佐古"
|
||||
},
|
||||
{
|
||||
"StartStationName": "辻",
|
||||
"EndStationName": "佃",
|
||||
"LineName": "dosan",
|
||||
"Point": "Up",
|
||||
"Half": "End",
|
||||
"DispPos": "dosan_7_佃"
|
||||
},
|
||||
{
|
||||
"StartStationName": "板東",
|
||||
"EndStationName": "池谷",
|
||||
"LineName": "naruto",
|
||||
"Point": "Down",
|
||||
"Half": "End",
|
||||
"DispPos": "naruto_0_鳴門"
|
||||
}
|
||||
]
|
||||
0
lib/mockApi/mockData/current-trains.json
Normal file
10
lib/mockApi/mockData/holidaydisp.json
Normal file
@@ -0,0 +1,10 @@
|
||||
[
|
||||
{
|
||||
"TrainNum": "1219M",
|
||||
"String": "※土曜・休日は多度津-琴平間運休"
|
||||
},
|
||||
{
|
||||
"TrainNum": "5223M",
|
||||
"String": "※休日は高松-多度津間運休"
|
||||
}
|
||||
]
|
||||
20
lib/mockApi/mockData/ignore.json
Normal file
@@ -0,0 +1,20 @@
|
||||
[
|
||||
"70",
|
||||
"71",
|
||||
"73",
|
||||
"74",
|
||||
"75",
|
||||
"76",
|
||||
"3070",
|
||||
"3071",
|
||||
"3072",
|
||||
"3073",
|
||||
"3076",
|
||||
"3077",
|
||||
"3078",
|
||||
"3079",
|
||||
"8070",
|
||||
"8071",
|
||||
"8072",
|
||||
"8077"
|
||||
]
|
||||
1137
lib/mockApi/mockData/lang.json
Normal file
32
lib/mockApi/mockData/othercolor.json
Normal file
@@ -0,0 +1,32 @@
|
||||
[
|
||||
{
|
||||
"Line": "tokushimaa",
|
||||
"Datas": [
|
||||
{
|
||||
"Name": "徳島",
|
||||
"Color": "koutoku"
|
||||
},
|
||||
{
|
||||
"Name": "徳島~佐古",
|
||||
"Color": "koutoku"
|
||||
},
|
||||
{
|
||||
"Name": "佃~阿波池田",
|
||||
"Color": "dosan"
|
||||
},
|
||||
{
|
||||
"Name": "阿波池田",
|
||||
"Color": "dosan"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Line": "setoa",
|
||||
"Datas": [
|
||||
{
|
||||
"Name": "児島",
|
||||
"Color": "other"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
80
lib/mockApi/mockData/otherline.json
Normal file
@@ -0,0 +1,80 @@
|
||||
[
|
||||
{
|
||||
"LineName": "yosan",
|
||||
"Position": "end",
|
||||
"OtherLineName": "uwajima",
|
||||
"DispPos": "uwajima_0_松山"
|
||||
},
|
||||
{
|
||||
"LineName": "yosan",
|
||||
"Position": "start",
|
||||
"OtherLineName": "koutoku",
|
||||
"DispPos": "koutoku_0_高松"
|
||||
},
|
||||
{
|
||||
"LineName": "uwajima",
|
||||
"Position": "start",
|
||||
"OtherLineName": "yosan",
|
||||
"DispPos": "yosan_42_松山"
|
||||
},
|
||||
{
|
||||
"LineName": "uwajima2",
|
||||
"Position": "start",
|
||||
"OtherLineName": "uwajima",
|
||||
"DispPos": "uwajima_3_向井原"
|
||||
},
|
||||
{
|
||||
"LineName": "uwajima2",
|
||||
"Position": "end",
|
||||
"OtherLineName": "uwajima",
|
||||
"DispPos": "uwajima_8_伊予大洲"
|
||||
},
|
||||
{
|
||||
"LineName": "seto",
|
||||
"Position": "end",
|
||||
"OtherLineName": "yosan",
|
||||
"DispPos": "yosan_3_坂出"
|
||||
},
|
||||
{
|
||||
"LineName": "dosan",
|
||||
"Position": "start",
|
||||
"OtherLineName": "yosan",
|
||||
"DispPos": "yosan_6_多度津"
|
||||
},
|
||||
{
|
||||
"LineName": "dosan",
|
||||
"Position": "end",
|
||||
"OtherLineName": "dosan2",
|
||||
"DispPos": "dosan2_0_高知"
|
||||
},
|
||||
{
|
||||
"LineName": "tokushima",
|
||||
"Position": "start",
|
||||
"OtherLineName": "koutoku",
|
||||
"DispPos": "koutoku_20_徳島"
|
||||
},
|
||||
{
|
||||
"LineName": "koutoku",
|
||||
"Position": "start",
|
||||
"OtherLineName": "yosan",
|
||||
"DispPos": "yosan_0_高松"
|
||||
},
|
||||
{
|
||||
"LineName": "dosan2",
|
||||
"Position": "start",
|
||||
"OtherLineName": "dosan",
|
||||
"DispPos": "dosan_25_高知"
|
||||
},
|
||||
{
|
||||
"LineName": "tokushima",
|
||||
"Position": "end",
|
||||
"OtherLineName": "dosan",
|
||||
"DispPos": "dosan_8_阿波池田"
|
||||
},
|
||||
{
|
||||
"LineName": "naruto",
|
||||
"Position": "end",
|
||||
"OtherLineName": "koutoku",
|
||||
"DispPos": "koutoku_16_池谷"
|
||||
}
|
||||
]
|
||||
0
lib/mockApi/mockData/position-problems.json
Normal file
7
lib/mockApi/mockData/sightseeingtrainname.json
Normal file
@@ -0,0 +1,7 @@
|
||||
[
|
||||
"伊予灘ものがたり",
|
||||
"千年ものがたり",
|
||||
"アンパンマントロッコ",
|
||||
"夜明けのものがたり",
|
||||
"藍よしのがわトロッコ"
|
||||
]
|
||||
272
lib/mockApi/mockData/station-dosan.json
Normal file
@@ -0,0 +1,272 @@
|
||||
[
|
||||
{
|
||||
"Station_JP": "多度津",
|
||||
"Station_EN": "Tadotsu",
|
||||
"MyStation": "0",
|
||||
"StationNumber": "D12",
|
||||
"DispNum": "3",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/tadotsu.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/34.271088,133.756735",
|
||||
"JrHpUrl": "http://www.jr-shikoku.co.jp/01_trainbus/kakueki/tadotsu/"
|
||||
},
|
||||
{
|
||||
"Station_JP": "金蔵寺",
|
||||
"Station_EN": "Konzōji",
|
||||
"MyStation": "0",
|
||||
"StationNumber": "D13",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/konzoji.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/34.247861,133.777594",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "善通寺",
|
||||
"Station_EN": "Zentsūji",
|
||||
"MyStation": "1",
|
||||
"StationNumber": "D14",
|
||||
"DispNum": "3",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/zentsuji.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/34.229958,133.789141",
|
||||
"JrHpUrl": "http://www.jr-shikoku.co.jp/01_trainbus/kakueki/zentsuji/"
|
||||
},
|
||||
{
|
||||
"Station_JP": "琴平",
|
||||
"Station_EN": "Kotohira",
|
||||
"MyStation": "2",
|
||||
"StationNumber": "D15",
|
||||
"DispNum": "3",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/kotohira.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/34.191903,133.821295",
|
||||
"JrHpUrl": "http://www.jr-shikoku.co.jp/01_trainbus/kakueki/kotohira/"
|
||||
},
|
||||
{
|
||||
"Station_JP": "塩入",
|
||||
"Station_EN": "Shioiri",
|
||||
"MyStation": "3",
|
||||
"StationNumber": "D16",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/shioiri.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/34.156034,133.849974",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "讃岐財田",
|
||||
"Station_EN": "Sanuki-Saida",
|
||||
"MyStation": "4",
|
||||
"StationNumber": "D18",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/sanuki-saida.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/34.117586,133.814181",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "坪尻",
|
||||
"Station_EN": "Tsubojiri",
|
||||
"MyStation": "5",
|
||||
"StationNumber": "D19",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/tsubojiri.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/34.054035,133.823675",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "箸蔵",
|
||||
"Station_EN": "Hashikura",
|
||||
"MyStation": "6",
|
||||
"StationNumber": "D20",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/hashikura.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/34.040343,133.848761",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "佃",
|
||||
"Station_EN": "Tsukuda",
|
||||
"MyStation": "7",
|
||||
"StationNumber": "D21",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/tsukuda.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/34.032458,133.857363",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "阿波池田",
|
||||
"Station_EN": "Awa-Ikeda",
|
||||
"MyStation": "8",
|
||||
"StationNumber": "D22",
|
||||
"DispNum": "3",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/awa-ikeda.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/34.027177,133.804619",
|
||||
"JrHpUrl": "http://www.jr-shikoku.co.jp/01_trainbus/kakueki/awaikeda/"
|
||||
},
|
||||
{
|
||||
"Station_JP": "三縄",
|
||||
"Station_EN": "Minawa",
|
||||
"MyStation": "9",
|
||||
"StationNumber": "D23",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/minawa.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/34.006837,133.787427",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "阿波川口",
|
||||
"Station_EN": "Awa-Kawaguchi",
|
||||
"MyStation": "10",
|
||||
"StationNumber": "D25",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/awa-kawaguchi.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/33.963705,133.754617",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "小歩危",
|
||||
"Station_EN": "Koboke",
|
||||
"MyStation": "11",
|
||||
"StationNumber": "D26",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/koboke.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/33.923338,133.758747",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "大歩危",
|
||||
"Station_EN": "Ōboke",
|
||||
"MyStation": "12",
|
||||
"StationNumber": "D27",
|
||||
"DispNum": "3",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/oboke.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/33.876483,133.767298",
|
||||
"JrHpUrl": "http://www.jr-shikoku.co.jp/01_trainbus/kakueki/oboke/"
|
||||
},
|
||||
{
|
||||
"Station_JP": "土佐岩原",
|
||||
"Station_EN": "Tosa-Iwahara",
|
||||
"MyStation": "13",
|
||||
"StationNumber": "D28",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/tosa-iwahara.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/33.822635,133.788204",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "豊永",
|
||||
"Station_EN": "Toyonaga",
|
||||
"MyStation": "14",
|
||||
"StationNumber": "D29",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/toyonaga.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/33.796841,133.759716",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "大田口",
|
||||
"Station_EN": "Ōtaguchi",
|
||||
"MyStation": "15",
|
||||
"StationNumber": "D30",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/otaguchi.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/33.785517,133.726602",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "大杉",
|
||||
"Station_EN": "Ōsugi",
|
||||
"MyStation": "16",
|
||||
"StationNumber": "D32",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/osugi.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/33.760938,133.664483",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "土佐北川",
|
||||
"Station_EN": "Tosa-Kitagawa",
|
||||
"MyStation": "17",
|
||||
"StationNumber": "D33",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/tosa-kitagawa.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/33.714781,133.686312",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "繁藤",
|
||||
"Station_EN": "Shigetō",
|
||||
"MyStation": "18",
|
||||
"StationNumber": "D35",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/shigeto.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/33.680386,133.6902",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "新改",
|
||||
"Station_EN": "Shingai",
|
||||
"MyStation": "19",
|
||||
"StationNumber": "D36",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/shingai.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/33.649629,133.695788",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "土佐山田",
|
||||
"Station_EN": "Tosa-Yamada",
|
||||
"MyStation": "20",
|
||||
"StationNumber": "D37",
|
||||
"DispNum": "3",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/tosa-yamada.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/33.607099,133.684992",
|
||||
"JrHpUrl": "http://www.jr-shikoku.co.jp/01_trainbus/kakueki/tosayamada/"
|
||||
},
|
||||
{
|
||||
"Station_JP": "後免",
|
||||
"Station_EN": "Gomen",
|
||||
"MyStation": "21",
|
||||
"StationNumber": "D40",
|
||||
"DispNum": "3",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/gomen.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/33.579234,133.645357",
|
||||
"JrHpUrl": "http://www.jr-shikoku.co.jp/01_trainbus/kakueki/gomen/"
|
||||
},
|
||||
{
|
||||
"Station_JP": "土佐大津",
|
||||
"Station_EN": "Tosa-Ōtsu",
|
||||
"MyStation": "22",
|
||||
"StationNumber": "D41",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/tosa-otsu.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/33.576384,133.611446",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "土佐一宮",
|
||||
"Station_EN": "Tosa-Ikku",
|
||||
"MyStation": "23",
|
||||
"StationNumber": "D43",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/tosa-ikku.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/33.579247,133.576891",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "薊野",
|
||||
"Station_EN": "Azōno",
|
||||
"MyStation": "24",
|
||||
"StationNumber": "D44",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/azono.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/33.578616,133.560692",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "高知",
|
||||
"Station_EN": "Kōchi",
|
||||
"MyStation": "25",
|
||||
"StationNumber": "D45",
|
||||
"DispNum": "3",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/kochi.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/33.566525,133.543638",
|
||||
"JrHpUrl": "http://www.jr-shikoku.co.jp/01_trainbus/kakueki/kochi/"
|
||||
}
|
||||
]
|
||||
152
lib/mockApi/mockData/station-dosan2.json
Normal file
@@ -0,0 +1,152 @@
|
||||
[
|
||||
{
|
||||
"Station_JP": "高知",
|
||||
"Station_EN": "Kōchi",
|
||||
"MyStation": "0",
|
||||
"StationNumber": "K00",
|
||||
"DispNum": "3",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/kochi.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/33.566525,133.543638",
|
||||
"JrHpUrl": "http://www.jr-shikoku.co.jp/01_trainbus/kakueki/kochi/"
|
||||
},
|
||||
{
|
||||
"Station_JP": "旭",
|
||||
"Station_EN": "Asahi",
|
||||
"MyStation": "0",
|
||||
"StationNumber": "K03",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/asahi.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/33.558853,133.508817",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "朝倉",
|
||||
"Station_EN": "Asakura",
|
||||
"MyStation": "1",
|
||||
"StationNumber": "K05",
|
||||
"DispNum": "3",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/asakura.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/33.551389,133.485354",
|
||||
"JrHpUrl": "http://www.jr-shikoku.co.jp/01_trainbus/kakueki/asakura/"
|
||||
},
|
||||
{
|
||||
"Station_JP": "伊野",
|
||||
"Station_EN": "Ino",
|
||||
"MyStation": "2",
|
||||
"StationNumber": "K07",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/ino.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/33.547505,133.430131",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "日下",
|
||||
"Station_EN": "Kusaka",
|
||||
"MyStation": "3",
|
||||
"StationNumber": "K09",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/kusaka.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/33.53338,133.371236",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "土佐加茂",
|
||||
"Station_EN": "Tosa-Kamo",
|
||||
"MyStation": "4",
|
||||
"StationNumber": "K11",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/tosa-kamo.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/33.523569,133.321135",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "西佐川",
|
||||
"Station_EN": "Nishi-Sakawa",
|
||||
"MyStation": "5",
|
||||
"StationNumber": "K12",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/nishi-sakawa.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/33.512623,133.286508",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "佐川",
|
||||
"Station_EN": "Sakawa",
|
||||
"MyStation": "6",
|
||||
"StationNumber": "K13",
|
||||
"DispNum": "3",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/sakawa.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/33.500105,133.292438",
|
||||
"JrHpUrl": "http://www.jr-shikoku.co.jp/01_trainbus/kakueki/sakawa/"
|
||||
},
|
||||
{
|
||||
"Station_JP": "斗賀野",
|
||||
"Station_EN": "Togano",
|
||||
"MyStation": "7",
|
||||
"StationNumber": "K15",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/togano.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/33.475415,133.286372",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "吾桑",
|
||||
"Station_EN": "Asō",
|
||||
"MyStation": "8",
|
||||
"StationNumber": "K16",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/aso.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/33.430181,133.295638",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "多ノ郷",
|
||||
"Station_EN": "Ōnogō",
|
||||
"MyStation": "9",
|
||||
"StationNumber": "K17",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/onogo.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/33.410727,133.294630",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "須崎",
|
||||
"Station_EN": "Susaki",
|
||||
"MyStation": "10",
|
||||
"StationNumber": "K19",
|
||||
"DispNum": "3",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/susaki.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/33.392624,133.293189",
|
||||
"JrHpUrl": "http://www.jr-shikoku.co.jp/01_trainbus/kakueki/susaki/"
|
||||
},
|
||||
{
|
||||
"Station_JP": "土佐久礼",
|
||||
"Station_EN": "Tosa-Kure",
|
||||
"MyStation": "11",
|
||||
"StationNumber": "K22",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/tosa-kure.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/33.329028,133.226483",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "影野",
|
||||
"Station_EN": "Kageno",
|
||||
"MyStation": "12",
|
||||
"StationNumber": "K23",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/kageno.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/33.275882,133.17358",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "窪川",
|
||||
"Station_EN": "Kubokawa",
|
||||
"MyStation": "13",
|
||||
"StationNumber": "K26",
|
||||
"DispNum": "3",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/kubokawa.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/33.212436,133.13716",
|
||||
"JrHpUrl": "http://www.jr-shikoku.co.jp/01_trainbus/kakueki/kubokawa/"
|
||||
}
|
||||
]
|
||||
222
lib/mockApi/mockData/station-koutoku.json
Normal file
@@ -0,0 +1,222 @@
|
||||
[
|
||||
{
|
||||
"Station_JP": "高松",
|
||||
"Station_EN": "Takamatsu",
|
||||
"MyStation": "0",
|
||||
"StationNumber": "T28",
|
||||
"DispNum": "3",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/takamatsu.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/34.350682,134.046938",
|
||||
"JrHpUrl": "http://www.jr-shikoku.co.jp/01_trainbus/kakueki/takamatsu/"
|
||||
},
|
||||
{
|
||||
"Station_JP": "栗林",
|
||||
"Station_EN": "Ritsurin",
|
||||
"MyStation": "0",
|
||||
"StationNumber": "T25",
|
||||
"DispNum": "3",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/ritsurin.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/34.332203,134.053588",
|
||||
"JrHpUrl": "http://www.jr-shikoku.co.jp/01_trainbus/kakueki/ritsurin/"
|
||||
},
|
||||
{
|
||||
"Station_JP": "屋島",
|
||||
"Station_EN": "Yashima",
|
||||
"MyStation": "1",
|
||||
"StationNumber": "T23",
|
||||
"DispNum": "3",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/yashima.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/34.336655,134.109102",
|
||||
"JrHpUrl": "http://www.jr-shikoku.co.jp/01_trainbus/kakueki/yashima/"
|
||||
},
|
||||
{
|
||||
"Station_JP": "八栗口",
|
||||
"Station_EN": "Yakuriguchi",
|
||||
"MyStation": "2",
|
||||
"StationNumber": "T21",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/yakuriguchi.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/34.339097,134.136488",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "志度",
|
||||
"Station_EN": "Shido",
|
||||
"MyStation": "3",
|
||||
"StationNumber": "T19",
|
||||
"DispNum": "3",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/shido.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/34.321624,134.1728",
|
||||
"JrHpUrl": "http://www.jr-shikoku.co.jp/01_trainbus/kakueki/shido/"
|
||||
},
|
||||
{
|
||||
"Station_JP": "オレンジタウン",
|
||||
"Station_EN": "Orange-Town",
|
||||
"MyStation": "4",
|
||||
"StationNumber": "T18",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/orange-town.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/34.301827,134.180946",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "造田",
|
||||
"Station_EN": "Zōda",
|
||||
"MyStation": "5",
|
||||
"StationNumber": "T17",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/zoda.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/34.282822,134.185946",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "讃岐津田",
|
||||
"Station_EN": "Sanuki-Tsuda",
|
||||
"MyStation": "6",
|
||||
"StationNumber": "T15",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/sanuki-tsuda.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/34.288442,134.248154",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "鶴羽",
|
||||
"Station_EN": "Tsuruwa",
|
||||
"MyStation": "7",
|
||||
"StationNumber": "T14",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/tsuruwa.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/34.280008,134.273842",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "丹生",
|
||||
"Station_EN": "Nibu",
|
||||
"MyStation": "8",
|
||||
"StationNumber": "T13",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/nibu.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/34.254508,134.301487",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "三本松",
|
||||
"Station_EN": "Sambommatsu",
|
||||
"MyStation": "9",
|
||||
"StationNumber": "T12",
|
||||
"DispNum": "3",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/sambommatsu.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/34.251538,134.334473",
|
||||
"JrHpUrl": "http://www.jr-shikoku.co.jp/01_trainbus/kakueki/sanbonmatsu/"
|
||||
},
|
||||
{
|
||||
"Station_JP": "讃岐白鳥",
|
||||
"Station_EN": "Sanuki-Shirotori",
|
||||
"MyStation": "10",
|
||||
"StationNumber": "T11",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/sanuki-shirotori.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/34.243642,134.366203",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "引田",
|
||||
"Station_EN": "Hiketa",
|
||||
"MyStation": "11",
|
||||
"StationNumber": "T10",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/hiketa.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/34.224467,134.402083",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "讃岐相生",
|
||||
"Station_EN": "Sanuki-Aioi",
|
||||
"MyStation": "12",
|
||||
"StationNumber": "T09",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/sanuki-aioi.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/34.211994,134.424534",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "阿波大宮",
|
||||
"Station_EN": "Awa-Ōmiya",
|
||||
"MyStation": "13",
|
||||
"StationNumber": "T08",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/awa-omiya.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/34.17859,134.448947",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "板野",
|
||||
"Station_EN": "Itano",
|
||||
"MyStation": "14",
|
||||
"StationNumber": "T07",
|
||||
"DispNum": "3",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/itano.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/34.142193,134.46597",
|
||||
"JrHpUrl": "http://www.jr-shikoku.co.jp/01_trainbus/kakueki/itano/"
|
||||
},
|
||||
{
|
||||
"Station_JP": "板東",
|
||||
"Station_EN": "Bandō",
|
||||
"MyStation": "15",
|
||||
"StationNumber": "T05",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/bando.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/34.155432,134.506866",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "池谷",
|
||||
"Station_EN": "Ikenotani",
|
||||
"MyStation": "16",
|
||||
"StationNumber": "T04",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/ikenotani.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/34.15293,134.528905",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "勝瑞",
|
||||
"Station_EN": "Shōzui",
|
||||
"MyStation": "17",
|
||||
"StationNumber": "T03",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/shozui.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/34.128644,134.528267",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "吉成",
|
||||
"Station_EN": "Yoshinari",
|
||||
"MyStation": "18",
|
||||
"StationNumber": "T02",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/yoshinari.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/34.118511,134.530745",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "佐古",
|
||||
"Station_EN": "Sako",
|
||||
"MyStation": "19",
|
||||
"StationNumber": "T01",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/sako.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/34.080616,134.538576",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "徳島",
|
||||
"Station_EN": "Tokushima",
|
||||
"MyStation": "20",
|
||||
"StationNumber": "T00",
|
||||
"DispNum": "3",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/tokushima.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/34.074642,134.550764",
|
||||
"JrHpUrl": "http://www.jr-shikoku.co.jp/01_trainbus/kakueki/tokushima/"
|
||||
}
|
||||
]
|
||||
22
lib/mockApi/mockData/station-naruto.json
Normal file
@@ -0,0 +1,22 @@
|
||||
[
|
||||
{
|
||||
"Station_JP": "鳴門",
|
||||
"Station_EN": "Naruto",
|
||||
"MyStation": "0",
|
||||
"StationNumber": "N10",
|
||||
"DispNum": "3",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/naruto.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/34.17925,134.608536",
|
||||
"JrHpUrl": "http://www.jr-shikoku.co.jp/01_trainbus/kakueki/naruto/"
|
||||
},
|
||||
{
|
||||
"Station_JP": "池谷",
|
||||
"Station_EN": "Ikenotani",
|
||||
"MyStation": "0",
|
||||
"StationNumber": "N04",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/ikenotani.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/34.15293,134.528905",
|
||||
"JrHpUrl": ""
|
||||
}
|
||||
]
|
||||
192
lib/mockApi/mockData/station-tokushima.json
Normal file
@@ -0,0 +1,192 @@
|
||||
[
|
||||
{
|
||||
"Station_JP": "徳島",
|
||||
"Station_EN": "Tokushima",
|
||||
"MyStation": "0",
|
||||
"StationNumber": "T00",
|
||||
"DispNum": "3",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/tokushima.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/34.074642,134.550764",
|
||||
"JrHpUrl": "http://www.jr-shikoku.co.jp/01_trainbus/kakueki/tokushima/"
|
||||
},
|
||||
{
|
||||
"Station_JP": "佐古",
|
||||
"Station_EN": "Sako",
|
||||
"MyStation": "0",
|
||||
"StationNumber": "B01",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/sako.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/34.080616,134.538576",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "蔵本",
|
||||
"Station_EN": "Kuramoto",
|
||||
"MyStation": "1",
|
||||
"StationNumber": "B02",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/kuramoto.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/34.079332,134.518705",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "府中",
|
||||
"Station_EN": "Kō",
|
||||
"MyStation": "2",
|
||||
"StationNumber": "B04",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/ko.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/34.074134,134.482939",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "石井",
|
||||
"Station_EN": "Ishii",
|
||||
"MyStation": "3",
|
||||
"StationNumber": "B05",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/ishii.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/34.070188,134.444343",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "牛島",
|
||||
"Station_EN": "Ushinoshima",
|
||||
"MyStation": "4",
|
||||
"StationNumber": "B07",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/ushinoshima.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/34.076709,134.397553",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "鴨島",
|
||||
"Station_EN": "Kamojima",
|
||||
"MyStation": "5",
|
||||
"StationNumber": "B09",
|
||||
"DispNum": "3",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/kamojima.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/34.073615,134.356559",
|
||||
"JrHpUrl": "http://www.jr-shikoku.co.jp/01_trainbus/kakueki/kamojima/"
|
||||
},
|
||||
{
|
||||
"Station_JP": "阿波川島",
|
||||
"Station_EN": "Awa-Kawashima",
|
||||
"MyStation": "6",
|
||||
"StationNumber": "B11",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/awa-kawashima.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/34.061857,134.320768",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "学",
|
||||
"Station_EN": "Gaku",
|
||||
"MyStation": "7",
|
||||
"StationNumber": "B12",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/gaku.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/34.057766,134.286411",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "山瀬",
|
||||
"Station_EN": "Yamase",
|
||||
"MyStation": "8",
|
||||
"StationNumber": "B13",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/yamase.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/34.060226,134.256297",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "川田",
|
||||
"Station_EN": "Kawata",
|
||||
"MyStation": "9",
|
||||
"StationNumber": "B15",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/kawata.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/34.061533,134.204329",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "穴吹",
|
||||
"Station_EN": "Anabuki",
|
||||
"MyStation": "10",
|
||||
"StationNumber": "B16",
|
||||
"DispNum": "3",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/anabuki.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/34.05615,134.163064",
|
||||
"JrHpUrl": "http://www.jr-shikoku.co.jp/01_trainbus/kakueki/anabuki/"
|
||||
},
|
||||
{
|
||||
"Station_JP": "小島",
|
||||
"Station_EN": "Oshima",
|
||||
"MyStation": "11",
|
||||
"StationNumber": "B17",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/oshima.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/34.050665,134.106521",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "貞光",
|
||||
"Station_EN": "Sadamitsu",
|
||||
"MyStation": "12",
|
||||
"StationNumber": "B18",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/sadamitsu.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/34.039354,134.058774",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "江口",
|
||||
"Station_EN": "Eguchi",
|
||||
"MyStation": "13",
|
||||
"StationNumber": "B20",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/eguchi.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/34.037364,133.973229",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "阿波加茂",
|
||||
"Station_EN": "Awa-Kamo",
|
||||
"MyStation": "14",
|
||||
"StationNumber": "B22",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/awa-kamo.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/34.037063,133.926505",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "辻",
|
||||
"Station_EN": "Tsuji",
|
||||
"MyStation": "15",
|
||||
"StationNumber": "B23",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/tsuji.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/34.028992,133.873234",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "佃",
|
||||
"Station_EN": "Tsukuda",
|
||||
"MyStation": "16",
|
||||
"StationNumber": "B24",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/tsukuda.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/34.032458,133.857363",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "阿波池田",
|
||||
"Station_EN": "Awa-Ikeda",
|
||||
"MyStation": "17",
|
||||
"StationNumber": "B25",
|
||||
"DispNum": "3",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/awa-ikeda.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/34.027177,133.804619",
|
||||
"JrHpUrl": "http://www.jr-shikoku.co.jp/01_trainbus/kakueki/awaikeda/"
|
||||
}
|
||||
]
|
||||
212
lib/mockApi/mockData/station-uwajima.json
Normal file
@@ -0,0 +1,212 @@
|
||||
[
|
||||
{
|
||||
"Station_JP": "松山",
|
||||
"Station_EN": "Matsuyama",
|
||||
"MyStation": "0",
|
||||
"StationNumber": "U00",
|
||||
"DispNum": "3",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/matsuyama.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/33.84039,132.75139",
|
||||
"JrHpUrl": "http://www.jr-shikoku.co.jp/01_trainbus/kakueki/matsuyama/"
|
||||
},
|
||||
{
|
||||
"Station_JP": "市坪",
|
||||
"Station_EN": "Ichitsubo",
|
||||
"MyStation": "0",
|
||||
"StationNumber": "U01",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/ichitsubo.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/33.809441,132.749325",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "北伊予",
|
||||
"Station_EN": "Kita-Iyo",
|
||||
"MyStation": "1",
|
||||
"StationNumber": "U02",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/kita-iyo.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/33.787699,132.748963",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "伊予市",
|
||||
"Station_EN": "Iyoshi",
|
||||
"MyStation": "2",
|
||||
"StationNumber": "U05",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/iyoshi.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/33.755352,132.702327",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "向井原",
|
||||
"Station_EN": "Mukaibara",
|
||||
"MyStation": "3",
|
||||
"StationNumber": "U06",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/mukaibara.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/33.736043,132.695825",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "伊予中山",
|
||||
"Station_EN": "Iyo-Nakayama",
|
||||
"MyStation": "4",
|
||||
"StationNumber": "U08",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/iyo-nakayama.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/33.649292,132.711857",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "伊予立川",
|
||||
"Station_EN": "Iyo-Tachikawa",
|
||||
"MyStation": "5",
|
||||
"StationNumber": "U09",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/iyo-tachikawa.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/33.601197,132.677898",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "内子",
|
||||
"Station_EN": "Uchiko",
|
||||
"MyStation": "6",
|
||||
"StationNumber": "U10",
|
||||
"DispNum": "3",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/uchiko.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/33.549461,132.646304",
|
||||
"JrHpUrl": "http://www.jr-shikoku.co.jp/01_trainbus/kakueki/uchiko/"
|
||||
},
|
||||
{
|
||||
"Station_JP": "新谷",
|
||||
"Station_EN": "Niiya",
|
||||
"MyStation": "7",
|
||||
"StationNumber": "U13",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/niiya.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/33.534106,132.59904",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "伊予大洲",
|
||||
"Station_EN": "Iyo-Ōzu",
|
||||
"MyStation": "8",
|
||||
"StationNumber": "U14",
|
||||
"DispNum": "3",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/iyo-ozu.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/33.518495,132.544878",
|
||||
"JrHpUrl": "http://www.jr-shikoku.co.jp/01_trainbus/kakueki/iyozu/"
|
||||
},
|
||||
{
|
||||
"Station_JP": "伊予平野",
|
||||
"Station_EN": "Iyo-Hirano",
|
||||
"MyStation": "9",
|
||||
"StationNumber": "U16",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/iyo-hirano.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/33.495827,132.518108",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "千丈",
|
||||
"Station_EN": "Senjō",
|
||||
"MyStation": "10",
|
||||
"StationNumber": "U17",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/senjo.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/33.465646,132.457416",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "八幡浜",
|
||||
"Station_EN": "Yawatahama",
|
||||
"MyStation": "11",
|
||||
"StationNumber": "U18",
|
||||
"DispNum": "3",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/yawatahama.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/33.458146,132.436002",
|
||||
"JrHpUrl": "http://www.jr-shikoku.co.jp/01_trainbus/kakueki/yawatahama/"
|
||||
},
|
||||
{
|
||||
"Station_JP": "双岩",
|
||||
"Station_EN": "Futaiwa",
|
||||
"MyStation": "12",
|
||||
"StationNumber": "U19",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/futaiwa.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/33.424669,132.457934",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "伊予石城",
|
||||
"Station_EN": "Iyo-Iwaki",
|
||||
"MyStation": "13",
|
||||
"StationNumber": "U20",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/iyo-iwaki.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/33.391413,132.473259",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "卯之町",
|
||||
"Station_EN": "Unomachi",
|
||||
"MyStation": "14",
|
||||
"StationNumber": "U22",
|
||||
"DispNum": "3",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/unomachi.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/33.363175,132.509959",
|
||||
"JrHpUrl": "http://www.jr-shikoku.co.jp/01_trainbus/kakueki/unomachi/"
|
||||
},
|
||||
{
|
||||
"Station_JP": "下宇和",
|
||||
"Station_EN": "Shimo-Uwa",
|
||||
"MyStation": "15",
|
||||
"StationNumber": "U23",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/shimo-uwa.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/33.349786,132.531172",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "立間",
|
||||
"Station_EN": "Tachima",
|
||||
"MyStation": "16",
|
||||
"StationNumber": "U24",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/tachima.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/33.296721,132.539509",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "伊予吉田",
|
||||
"Station_EN": "Iyo-Yoshida",
|
||||
"MyStation": "17",
|
||||
"StationNumber": "U25",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/iyo-yoshida.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/33.273287,132.544098",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "北宇和島",
|
||||
"Station_EN": "Kita-Uwajima",
|
||||
"MyStation": "18",
|
||||
"StationNumber": "U27",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/kita-uwajima.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/33.238636,132.569909",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "宇和島",
|
||||
"Station_EN": "Uwajima",
|
||||
"MyStation": "19",
|
||||
"StationNumber": "U28",
|
||||
"DispNum": "3",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/uwajima.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/33.225793,132.567498",
|
||||
"JrHpUrl": "http://www.jr-shikoku.co.jp/01_trainbus/kakueki/uwajima/"
|
||||
}
|
||||
]
|
||||
442
lib/mockApi/mockData/station-yosan.json
Normal file
@@ -0,0 +1,442 @@
|
||||
[
|
||||
{
|
||||
"Station_JP": "高松",
|
||||
"Station_EN": "Takamatsu",
|
||||
"MyStation": "0",
|
||||
"StationNumber": "Y00",
|
||||
"DispNum": "3",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/takamatsu.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/34.350682,134.046938",
|
||||
"JrHpUrl": "http://www.jr-shikoku.co.jp/01_trainbus/kakueki/takamatsu/"
|
||||
},
|
||||
{
|
||||
"Station_JP": "鬼無",
|
||||
"Station_EN": "Kinashi",
|
||||
"MyStation": "0",
|
||||
"StationNumber": "Y02",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/kinashi.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/34.325494,133.993861",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "端岡",
|
||||
"Station_EN": "Hashioka",
|
||||
"MyStation": "1",
|
||||
"StationNumber": "Y03",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/hashioka.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/34.305027,133.967643",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "鴨川",
|
||||
"Station_EN": "Kamogawa",
|
||||
"MyStation": "2",
|
||||
"StationNumber": "Y06",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/kamogawa.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/34.306877,133.905229",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "坂出",
|
||||
"Station_EN": "Sakaide",
|
||||
"MyStation": "3",
|
||||
"StationNumber": "Y08",
|
||||
"DispNum": "3",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/sakaide.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/34.313222,133.856325",
|
||||
"JrHpUrl": "http://www.jr-shikoku.co.jp/01_trainbus/kakueki/sakaide/"
|
||||
},
|
||||
{
|
||||
"Station_JP": "宇多津",
|
||||
"Station_EN": "Utazu",
|
||||
"MyStation": "4",
|
||||
"StationNumber": "Y09",
|
||||
"DispNum": "3",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/utazu.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/34.306379,133.813784",
|
||||
"JrHpUrl": "http://www.jr-shikoku.co.jp/01_trainbus/kakueki/utazu/"
|
||||
},
|
||||
{
|
||||
"Station_JP": "丸亀",
|
||||
"Station_EN": "Marugame",
|
||||
"MyStation": "5",
|
||||
"StationNumber": "Y10",
|
||||
"DispNum": "3",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/marugame.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/34.292006,133.793175",
|
||||
"JrHpUrl": "http://www.jr-shikoku.co.jp/01_trainbus/kakueki/marugame/"
|
||||
},
|
||||
{
|
||||
"Station_JP": "多度津",
|
||||
"Station_EN": "Tadotsu",
|
||||
"MyStation": "6",
|
||||
"StationNumber": "Y12",
|
||||
"DispNum": "3",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/tadotsu.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/34.271088,133.756735",
|
||||
"JrHpUrl": "http://www.jr-shikoku.co.jp/01_trainbus/kakueki/tadotsu/"
|
||||
},
|
||||
{
|
||||
"Station_JP": "海岸寺",
|
||||
"Station_EN": "Kaiganji",
|
||||
"MyStation": "7",
|
||||
"StationNumber": "Y13",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/kaiganji.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/34.253932,133.729307",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "詫間",
|
||||
"Station_EN": "Takuma",
|
||||
"MyStation": "8",
|
||||
"StationNumber": "Y14",
|
||||
"DispNum": "3",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/takuma.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/34.220826,133.692737",
|
||||
"JrHpUrl": "http://www.jr-shikoku.co.jp/01_trainbus/kakueki/takuma/"
|
||||
},
|
||||
{
|
||||
"Station_JP": "高瀬",
|
||||
"Station_EN": "Takase",
|
||||
"MyStation": "9",
|
||||
"StationNumber": "Y16",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/takase.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/34.184083,133.711397",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "本山",
|
||||
"Station_EN": "Motoyama",
|
||||
"MyStation": "10",
|
||||
"StationNumber": "Y18",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/motoyama.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/34.146075,133.686473",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "観音寺",
|
||||
"Station_EN": "Kan-onji",
|
||||
"MyStation": "11",
|
||||
"StationNumber": "Y19",
|
||||
"DispNum": "3",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/kan-onji.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/34.124478,133.655709",
|
||||
"JrHpUrl": "http://www.jr-shikoku.co.jp/01_trainbus/kakueki/kanonji/"
|
||||
},
|
||||
{
|
||||
"Station_JP": "豊浜",
|
||||
"Station_EN": "Toyohama",
|
||||
"MyStation": "12",
|
||||
"StationNumber": "Y20",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/toyohama.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/34.079775,133.644206",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "箕浦",
|
||||
"Station_EN": "Minoura",
|
||||
"MyStation": "13",
|
||||
"StationNumber": "Y21",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/minoura.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/34.048043,133.618998",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "川之江",
|
||||
"Station_EN": "Kawanoe",
|
||||
"MyStation": "14",
|
||||
"StationNumber": "Y22",
|
||||
"DispNum": "3",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/kawanoe.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/34.014209,133.575856",
|
||||
"JrHpUrl": "http://www.jr-shikoku.co.jp/01_trainbus/kakueki/kawanoe/"
|
||||
},
|
||||
{
|
||||
"Station_JP": "伊予三島",
|
||||
"Station_EN": "Iyo-Mishima",
|
||||
"MyStation": "15",
|
||||
"StationNumber": "Y23",
|
||||
"DispNum": "3",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/iyo-mishima.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/33.979583,133.541984",
|
||||
"JrHpUrl": "http://www.jr-shikoku.co.jp/01_trainbus/kakueki/iyomishima/"
|
||||
},
|
||||
{
|
||||
"Station_JP": "伊予寒川",
|
||||
"Station_EN": "Iyo-Sangawa",
|
||||
"MyStation": "16",
|
||||
"StationNumber": "Y24",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/iyo-sangawa.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/33.967803,133.500153",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "伊予土居",
|
||||
"Station_EN": "Iyo-Doi",
|
||||
"MyStation": "17",
|
||||
"StationNumber": "Y26",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/iyo-doi.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/33.959024,133.428294",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "関川",
|
||||
"Station_EN": "Sekigawa",
|
||||
"MyStation": "18",
|
||||
"StationNumber": "Y27",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/sekigawa.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/33.955777,133.392567",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "多喜浜",
|
||||
"Station_EN": "Takihama",
|
||||
"MyStation": "19",
|
||||
"StationNumber": "Y28",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/takihama.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/33.970564,133.32415",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "新居浜",
|
||||
"Station_EN": "Niihama",
|
||||
"MyStation": "20",
|
||||
"StationNumber": "Y29",
|
||||
"DispNum": "3",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/niihama.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/33.947996,133.294341",
|
||||
"JrHpUrl": "http://www.jr-shikoku.co.jp/01_trainbus/kakueki/niihama/"
|
||||
},
|
||||
{
|
||||
"Station_JP": "中萩",
|
||||
"Station_EN": "Nakahagi",
|
||||
"MyStation": "21",
|
||||
"StationNumber": "Y30",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/nakahagi.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/33.926168,133.253391",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "伊予西条",
|
||||
"Station_EN": "Iyo-Saijo",
|
||||
"MyStation": "22",
|
||||
"StationNumber": "Y31",
|
||||
"DispNum": "3",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/iyo-saijo.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/33.912492,133.187578",
|
||||
"JrHpUrl": "http://www.jr-shikoku.co.jp/01_trainbus/kakueki/iyosaijo/index.html"
|
||||
},
|
||||
{
|
||||
"Station_JP": "石鎚山",
|
||||
"Station_EN": "Ishizuchiyama",
|
||||
"MyStation": "23",
|
||||
"StationNumber": "Y32",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/ishizuchiyama.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/33.895746,133.157257",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "伊予小松",
|
||||
"Station_EN": "Iyo-Komatsu",
|
||||
"MyStation": "24",
|
||||
"StationNumber": "Y34",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/iyo-komatsu.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/33.897567,133.116717",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "壬生川",
|
||||
"Station_EN": "Nyūgawa",
|
||||
"MyStation": "25",
|
||||
"StationNumber": "Y36",
|
||||
"DispNum": "3",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/nyugawa.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/33.929851,133.08552",
|
||||
"JrHpUrl": "http://www.jr-shikoku.co.jp/01_trainbus/kakueki/nyugawa/"
|
||||
},
|
||||
{
|
||||
"Station_JP": "伊予三芳",
|
||||
"Station_EN": "Iyo-Miyoshi",
|
||||
"MyStation": "26",
|
||||
"StationNumber": "Y37",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/iyo-miyoshi.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/33.954251,133.06422",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "伊予桜井",
|
||||
"Station_EN": "Iyo-Sakurai",
|
||||
"MyStation": "27",
|
||||
"StationNumber": "Y38",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/iyo-sakurai.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/34.012073,133.03593",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "伊予富田",
|
||||
"Station_EN": "Iyo-Tomita",
|
||||
"MyStation": "28",
|
||||
"StationNumber": "Y39",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/iyo-tomita.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/34.036447,133.008989",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "今治",
|
||||
"Station_EN": "Imabari",
|
||||
"MyStation": "29",
|
||||
"StationNumber": "Y40",
|
||||
"DispNum": "3",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/imabari.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/34.064167,132.993655",
|
||||
"JrHpUrl": "http://www.jr-shikoku.co.jp/01_trainbus/kakueki/imabari/"
|
||||
},
|
||||
{
|
||||
"Station_JP": "波止浜",
|
||||
"Station_EN": "Hashihama",
|
||||
"MyStation": "30",
|
||||
"StationNumber": "Y41",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/hashihama.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/34.098067,132.968786",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "波方",
|
||||
"Station_EN": "Namikata",
|
||||
"MyStation": "31",
|
||||
"StationNumber": "Y42",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/namikata.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/34.095134,132.941888",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "大西",
|
||||
"Station_EN": "Ōnishi",
|
||||
"MyStation": "32",
|
||||
"StationNumber": "Y43",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/onishi.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/34.066125,132.929367",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "伊予亀岡",
|
||||
"Station_EN": "Iyo-kameoka",
|
||||
"MyStation": "33",
|
||||
"StationNumber": "Y44",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/iyo-kameoka.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/34.053886,132.874104",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "菊間",
|
||||
"Station_EN": "Kikuma",
|
||||
"MyStation": "34",
|
||||
"StationNumber": "Y45",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/kikuma.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/34.033421,132.840883",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "浅海",
|
||||
"Station_EN": "Asanami",
|
||||
"MyStation": "35",
|
||||
"StationNumber": "Y46",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/asanami.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/34.010028,132.802691",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "大浦",
|
||||
"Station_EN": "Ōura",
|
||||
"MyStation": "36",
|
||||
"StationNumber": "Y47",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/oura.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/33.999461,132.77555",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "伊予北条",
|
||||
"Station_EN": "Iyo-Hōjō",
|
||||
"MyStation": "37",
|
||||
"StationNumber": "Y48",
|
||||
"DispNum": "3",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/iyo-hojo.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/33.972595,132.775097",
|
||||
"JrHpUrl": "http://www.jr-shikoku.co.jp/01_trainbus/kakueki/iyohojo/"
|
||||
},
|
||||
{
|
||||
"Station_JP": "粟井",
|
||||
"Station_EN": "Awai",
|
||||
"MyStation": "38",
|
||||
"StationNumber": "Y50",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/awai.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/33.942729,132.77044",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "堀江",
|
||||
"Station_EN": "Horie",
|
||||
"MyStation": "39",
|
||||
"StationNumber": "Y52",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/horie.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/33.905558,132.753212",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "伊予和気",
|
||||
"Station_EN": "Iyo-Wake",
|
||||
"MyStation": "40",
|
||||
"StationNumber": "Y53",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/iyo-wake.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/33.889798,132.740708",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "三津浜",
|
||||
"Station_EN": "Mitsuhama",
|
||||
"MyStation": "41",
|
||||
"StationNumber": "Y54",
|
||||
"DispNum": "2",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/mitsuhama.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/33.861165,132.728612",
|
||||
"JrHpUrl": ""
|
||||
},
|
||||
{
|
||||
"Station_JP": "松山",
|
||||
"Station_EN": "Matsuyama",
|
||||
"MyStation": "42",
|
||||
"StationNumber": "Y55",
|
||||
"DispNum": "3",
|
||||
"StationTimeTable": "http://www.jr-shikoku.co.jp/01_trainbus/jikoku/pdf/matsuyama.pdf",
|
||||
"StationMap": "https://www.google.co.jp/maps/place/33.84039,132.75139",
|
||||
"JrHpUrl": "http://www.jr-shikoku.co.jp/01_trainbus/kakueki/matsuyama/"
|
||||
}
|
||||
]
|
||||
945
lib/mockApi/mockData/train.json
Normal file
@@ -0,0 +1,945 @@
|
||||
[
|
||||
{
|
||||
"Index": 2,
|
||||
"TrainNum": "363D",
|
||||
"Pos": "高松",
|
||||
"PosNum": 279,
|
||||
"delay": "入線",
|
||||
"Direction": 1,
|
||||
"Type": "normal",
|
||||
"Line": "yosan"
|
||||
},
|
||||
{
|
||||
"Index": 8,
|
||||
"TrainNum": "5253M",
|
||||
"Pos": "高松",
|
||||
"PosNum": 277,
|
||||
"delay": "入線",
|
||||
"Direction": 1,
|
||||
"Type": "normal",
|
||||
"Line": "yosan"
|
||||
},
|
||||
{
|
||||
"Index": 23,
|
||||
"TrainNum": "3160M",
|
||||
"Pos": "高松~鬼無(下り)",
|
||||
"PosNum": 286,
|
||||
"delay": 0,
|
||||
"Direction": 1,
|
||||
"Type": "rapid:マリンライナー60号\r",
|
||||
"Line": "yosan"
|
||||
},
|
||||
{
|
||||
"Index": 68,
|
||||
"TrainNum": "152M",
|
||||
"Pos": "端岡",
|
||||
"PosNum": 9,
|
||||
"delay": 0,
|
||||
"Direction": 0,
|
||||
"Type": "normal",
|
||||
"Line": "yosan"
|
||||
},
|
||||
{
|
||||
"Index": 72,
|
||||
"TrainNum": "5145M",
|
||||
"Pos": "端岡~鴨川(下り)",
|
||||
"PosNum": 14,
|
||||
"delay": 0,
|
||||
"Direction": 1,
|
||||
"Type": "normal",
|
||||
"Line": "yosan"
|
||||
},
|
||||
{
|
||||
"Index": 86,
|
||||
"TrainNum": "3157M",
|
||||
"Pos": "児島予告窓",
|
||||
"PosNum": 246,
|
||||
"delay": "入線",
|
||||
"Direction": 1,
|
||||
"Type": "rapid:マリンライナー57号\r",
|
||||
"Line": "yosan"
|
||||
},
|
||||
{
|
||||
"Index": 87,
|
||||
"TrainNum": "25M",
|
||||
"Pos": "児島予告窓",
|
||||
"PosNum": 93,
|
||||
"delay": "入線",
|
||||
"Direction": 1,
|
||||
"Type": "express:しおかぜ25号\r",
|
||||
"Line": "yosan"
|
||||
},
|
||||
{
|
||||
"Index": 114,
|
||||
"TrainNum": "3158M",
|
||||
"Pos": "児島~宇多津(上り)",
|
||||
"PosNum": 236,
|
||||
"delay": 14,
|
||||
"Direction": 0,
|
||||
"Type": "rapid:マリンライナー58号\r",
|
||||
"Line": "yosan"
|
||||
},
|
||||
{
|
||||
"Index": 129,
|
||||
"TrainNum": "5251M",
|
||||
"Pos": "坂出~宇多津(下り)",
|
||||
"PosNum": 26,
|
||||
"delay": 7,
|
||||
"Direction": 1,
|
||||
"Type": "rapid:サンポート南風リレー\r",
|
||||
"Line": "yosan"
|
||||
},
|
||||
{
|
||||
"Index": 145,
|
||||
"TrainNum": "9028M",
|
||||
"Pos": "宇多津",
|
||||
"PosNum": 27,
|
||||
"delay": "入線",
|
||||
"Direction": 0,
|
||||
"Type": "express:いしづち28号\r",
|
||||
"Line": "yosan"
|
||||
},
|
||||
{
|
||||
"Index": 149,
|
||||
"TrainNum": "9196R",
|
||||
"Pos": "宇多津",
|
||||
"PosNum": 227,
|
||||
"delay": 0,
|
||||
"Direction": 0,
|
||||
"Type": "normal",
|
||||
"Line": "yosan"
|
||||
},
|
||||
{
|
||||
"Index": 274,
|
||||
"TrainNum": "141M",
|
||||
"Pos": "多度津",
|
||||
"PosNum": 41,
|
||||
"delay": 0,
|
||||
"Direction": 1,
|
||||
"Type": "normal",
|
||||
"Line": "yosan"
|
||||
},
|
||||
{
|
||||
"Index": 289,
|
||||
"TrainNum": "1250M",
|
||||
"Pos": "多度津予告窓",
|
||||
"PosNum": 45,
|
||||
"delay": "入線",
|
||||
"Direction": 0,
|
||||
"Type": "normal",
|
||||
"Line": "yosan"
|
||||
},
|
||||
{
|
||||
"Index": 291,
|
||||
"TrainNum": "56D",
|
||||
"Pos": "多度津予告窓",
|
||||
"PosNum": 303,
|
||||
"delay": "入線",
|
||||
"Direction": 0,
|
||||
"Type": "express:南風26号\r",
|
||||
"Line": "yosan"
|
||||
},
|
||||
{
|
||||
"Index": 297,
|
||||
"TrainNum": "23M",
|
||||
"Pos": "詫間",
|
||||
"PosNum": 53,
|
||||
"delay": 0,
|
||||
"Direction": 1,
|
||||
"Type": "express:しおかぜ23号\r",
|
||||
"Line": "yosan"
|
||||
},
|
||||
{
|
||||
"Index": 305,
|
||||
"TrainNum": "28M",
|
||||
"Pos": "詫間~高瀬",
|
||||
"PosNum": 55,
|
||||
"delay": 22,
|
||||
"Direction": 0,
|
||||
"Type": "express:しおかぜ28号\r",
|
||||
"Line": "yosan"
|
||||
},
|
||||
{
|
||||
"Index": 308,
|
||||
"TrainNum": "1606M",
|
||||
"Pos": "高瀬",
|
||||
"PosNum": 56,
|
||||
"delay": 0,
|
||||
"Direction": 0,
|
||||
"Type": "normal",
|
||||
"Line": "yosan"
|
||||
},
|
||||
{
|
||||
"Index": 321,
|
||||
"TrainNum": "143M",
|
||||
"Pos": "観音寺",
|
||||
"PosNum": 64,
|
||||
"delay": "入線",
|
||||
"Direction": 1,
|
||||
"Type": "normal",
|
||||
"Line": "yosan"
|
||||
},
|
||||
{
|
||||
"Index": 325,
|
||||
"TrainNum": "4608M",
|
||||
"Pos": "観音寺",
|
||||
"PosNum": 66,
|
||||
"delay": "入線",
|
||||
"Direction": 0,
|
||||
"Type": "normal",
|
||||
"Line": "yosan"
|
||||
},
|
||||
{
|
||||
"Index": 341,
|
||||
"TrainNum": "550M",
|
||||
"Pos": "箕浦~川之江",
|
||||
"PosNum": 100,
|
||||
"delay": 14,
|
||||
"Direction": 0,
|
||||
"Type": "normal",
|
||||
"Line": "yosan"
|
||||
},
|
||||
{
|
||||
"Index": 375,
|
||||
"TrainNum": "4563M",
|
||||
"Pos": "関川~多喜浜",
|
||||
"PosNum": 121,
|
||||
"delay": 13,
|
||||
"Direction": 1,
|
||||
"Type": "normal",
|
||||
"Line": "yosan"
|
||||
},
|
||||
{
|
||||
"Index": 385,
|
||||
"TrainNum": "5552M",
|
||||
"Pos": "多喜浜~新居浜",
|
||||
"PosNum": 125,
|
||||
"delay": 4,
|
||||
"Direction": 0,
|
||||
"Type": "normal",
|
||||
"Line": "yosan"
|
||||
},
|
||||
{
|
||||
"Index": 517,
|
||||
"TrainNum": "30M",
|
||||
"Pos": "伊予小松~壬生川",
|
||||
"PosNum": 148,
|
||||
"delay": 6,
|
||||
"Direction": 0,
|
||||
"Type": "express:しおかぜ30号\r",
|
||||
"Line": "yosan"
|
||||
},
|
||||
{
|
||||
"Index": 530,
|
||||
"TrainNum": "21M",
|
||||
"Pos": "伊予三芳",
|
||||
"PosNum": 166,
|
||||
"delay": 6,
|
||||
"Direction": 1,
|
||||
"Type": "express:しおかぜ21号\r",
|
||||
"Line": "yosan"
|
||||
},
|
||||
{
|
||||
"Index": 534,
|
||||
"TrainNum": "561M",
|
||||
"Pos": "伊予桜井",
|
||||
"PosNum": 171,
|
||||
"delay": 3,
|
||||
"Direction": 1,
|
||||
"Type": "normal",
|
||||
"Line": "yosan"
|
||||
},
|
||||
{
|
||||
"Index": 545,
|
||||
"TrainNum": "5556M",
|
||||
"Pos": "伊予富田~今治",
|
||||
"PosNum": 177,
|
||||
"delay": 2,
|
||||
"Direction": 0,
|
||||
"Type": "normal",
|
||||
"Line": "yosan"
|
||||
},
|
||||
{
|
||||
"Index": 585,
|
||||
"TrainNum": "559M",
|
||||
"Pos": "菊間~浅海",
|
||||
"PosNum": 199,
|
||||
"delay": 3,
|
||||
"Direction": 1,
|
||||
"Type": "normal",
|
||||
"Line": "yosan"
|
||||
},
|
||||
{
|
||||
"Index": 600,
|
||||
"TrainNum": "558M",
|
||||
"Pos": "伊予北条",
|
||||
"PosNum": 206,
|
||||
"delay": 3,
|
||||
"Direction": 0,
|
||||
"Type": "normal",
|
||||
"Line": "yosan"
|
||||
},
|
||||
{
|
||||
"Index": 614,
|
||||
"TrainNum": "1042M",
|
||||
"Pos": "粟井~堀江",
|
||||
"PosNum": 212,
|
||||
"delay": 1,
|
||||
"Direction": 0,
|
||||
"Type": "express:いしづち102号\r",
|
||||
"Line": "yosan"
|
||||
},
|
||||
{
|
||||
"Index": 3,
|
||||
"TrainNum": "363D",
|
||||
"Pos": "高松",
|
||||
"PosNum": 279,
|
||||
"delay": "入線",
|
||||
"Direction": 1,
|
||||
"Type": "normal",
|
||||
"Line": "koutoku"
|
||||
},
|
||||
{
|
||||
"Index": 5,
|
||||
"TrainNum": "5253M",
|
||||
"Pos": "高松",
|
||||
"PosNum": 277,
|
||||
"delay": "入線",
|
||||
"Direction": 1,
|
||||
"Type": "normal",
|
||||
"Line": "koutoku"
|
||||
},
|
||||
{
|
||||
"Index": 24,
|
||||
"TrainNum": "4360D",
|
||||
"Pos": "屋島~八栗口",
|
||||
"PosNum": 412,
|
||||
"delay": 2,
|
||||
"Direction": 0,
|
||||
"Type": "normal",
|
||||
"Line": "koutoku"
|
||||
},
|
||||
{
|
||||
"Index": 25,
|
||||
"TrainNum": "361D",
|
||||
"Pos": "八栗口",
|
||||
"PosNum": 514,
|
||||
"delay": 2,
|
||||
"Direction": 1,
|
||||
"Type": "normal",
|
||||
"Line": "koutoku"
|
||||
},
|
||||
{
|
||||
"Index": 55,
|
||||
"TrainNum": "5359D",
|
||||
"Pos": "鶴羽",
|
||||
"PosNum": 521,
|
||||
"delay": 2,
|
||||
"Direction": 1,
|
||||
"Type": "normal",
|
||||
"Line": "koutoku"
|
||||
},
|
||||
{
|
||||
"Index": 56,
|
||||
"TrainNum": "3027D",
|
||||
"Pos": "鶴羽~丹生",
|
||||
"PosNum": 522,
|
||||
"delay": 2,
|
||||
"Direction": 1,
|
||||
"Type": "express:うずしお27号\r",
|
||||
"Line": "koutoku"
|
||||
},
|
||||
{
|
||||
"Index": 68,
|
||||
"TrainNum": "366D",
|
||||
"Pos": "三本松",
|
||||
"PosNum": 435,
|
||||
"delay": 0,
|
||||
"Direction": 0,
|
||||
"Type": "normal",
|
||||
"Line": "koutoku"
|
||||
},
|
||||
{
|
||||
"Index": 96,
|
||||
"TrainNum": "3028D",
|
||||
"Pos": "阿波大宮~板野",
|
||||
"PosNum": 459,
|
||||
"delay": 0,
|
||||
"Direction": 0,
|
||||
"Type": "express:うずしお28号\r",
|
||||
"Line": "koutoku"
|
||||
},
|
||||
{
|
||||
"Index": 100,
|
||||
"TrainNum": "355D",
|
||||
"Pos": "板野~板東",
|
||||
"PosNum": 465,
|
||||
"delay": 0,
|
||||
"Direction": 1,
|
||||
"Type": "normal",
|
||||
"Line": "koutoku"
|
||||
},
|
||||
{
|
||||
"Index": 103,
|
||||
"TrainNum": "976D",
|
||||
"Pos": "鳴門",
|
||||
"PosNum": 472,
|
||||
"delay": 4,
|
||||
"Direction": 0,
|
||||
"Type": "normal",
|
||||
"Line": "koutoku"
|
||||
},
|
||||
{
|
||||
"Index": 117,
|
||||
"TrainNum": "364D",
|
||||
"Pos": "池谷~勝瑞",
|
||||
"PosNum": 480,
|
||||
"delay": 1,
|
||||
"Direction": 0,
|
||||
"Type": "normal",
|
||||
"Line": "koutoku"
|
||||
},
|
||||
{
|
||||
"Index": 130,
|
||||
"TrainNum": "480D",
|
||||
"Pos": "蔵本",
|
||||
"PosNum": 605,
|
||||
"delay": 0,
|
||||
"Direction": 0,
|
||||
"Type": "normal",
|
||||
"Line": "koutoku"
|
||||
},
|
||||
{
|
||||
"Index": 136,
|
||||
"TrainNum": "979D",
|
||||
"Pos": "佐古",
|
||||
"PosNum": 494,
|
||||
"delay": 0,
|
||||
"Direction": 1,
|
||||
"Type": "normal",
|
||||
"Line": "koutoku"
|
||||
},
|
||||
{
|
||||
"Index": 148,
|
||||
"TrainNum": "5481D",
|
||||
"Pos": "石井",
|
||||
"PosNum": 611,
|
||||
"delay": 0,
|
||||
"Direction": 1,
|
||||
"Type": "normal",
|
||||
"Line": "koutoku"
|
||||
},
|
||||
{
|
||||
"Index": 7,
|
||||
"TrainNum": "979D",
|
||||
"Pos": "佐古",
|
||||
"PosNum": 494,
|
||||
"delay": 0,
|
||||
"Direction": 1,
|
||||
"Type": "normal",
|
||||
"Line": "tokushima"
|
||||
},
|
||||
{
|
||||
"Index": 19,
|
||||
"TrainNum": "480D",
|
||||
"Pos": "蔵本",
|
||||
"PosNum": 605,
|
||||
"delay": 0,
|
||||
"Direction": 0,
|
||||
"Type": "normal",
|
||||
"Line": "tokushima"
|
||||
},
|
||||
{
|
||||
"Index": 25,
|
||||
"TrainNum": "5481D",
|
||||
"Pos": "石井",
|
||||
"PosNum": 611,
|
||||
"delay": 0,
|
||||
"Direction": 1,
|
||||
"Type": "normal",
|
||||
"Line": "tokushima"
|
||||
},
|
||||
{
|
||||
"Index": 37,
|
||||
"TrainNum": "4482D",
|
||||
"Pos": "鴨島",
|
||||
"PosNum": 623,
|
||||
"delay": 0,
|
||||
"Direction": 0,
|
||||
"Type": "normal",
|
||||
"Line": "tokushima"
|
||||
},
|
||||
{
|
||||
"Index": 49,
|
||||
"TrainNum": "5479D",
|
||||
"Pos": "学",
|
||||
"PosNum": 629,
|
||||
"delay": 0,
|
||||
"Direction": 1,
|
||||
"Type": "normal",
|
||||
"Line": "tokushima"
|
||||
},
|
||||
{
|
||||
"Index": 65,
|
||||
"TrainNum": "484D",
|
||||
"Pos": "穴吹",
|
||||
"PosNum": 645,
|
||||
"delay": "入線",
|
||||
"Direction": 0,
|
||||
"Type": "normal",
|
||||
"Line": "tokushima"
|
||||
},
|
||||
{
|
||||
"Index": 85,
|
||||
"TrainNum": "475D",
|
||||
"Pos": "江口~阿波加茂",
|
||||
"PosNum": 661,
|
||||
"delay": 0,
|
||||
"Direction": 1,
|
||||
"Type": "normal",
|
||||
"Line": "tokushima"
|
||||
},
|
||||
{
|
||||
"Index": 99,
|
||||
"TrainNum": "5486D",
|
||||
"Pos": "佃予告窓",
|
||||
"PosNum": 669,
|
||||
"delay": "入線",
|
||||
"Direction": 0,
|
||||
"Type": "normal",
|
||||
"Line": "tokushima"
|
||||
},
|
||||
{
|
||||
"Index": 100,
|
||||
"TrainNum": "488D",
|
||||
"Pos": "佃予告窓",
|
||||
"PosNum": 670,
|
||||
"delay": "入線",
|
||||
"Direction": 0,
|
||||
"Type": "normal",
|
||||
"Line": "tokushima"
|
||||
},
|
||||
{
|
||||
"Index": 0,
|
||||
"TrainNum": "21M",
|
||||
"Pos": "",
|
||||
"PosNum": 228,
|
||||
"delay": "入線",
|
||||
"Direction": 1,
|
||||
"Type": "express:しおかぜ21号\r",
|
||||
"Line": "uwajima"
|
||||
},
|
||||
{
|
||||
"Index": 1,
|
||||
"TrainNum": "559M",
|
||||
"Pos": "",
|
||||
"PosNum": 227,
|
||||
"delay": "入線",
|
||||
"Direction": 1,
|
||||
"Type": "normal",
|
||||
"Line": "uwajima"
|
||||
},
|
||||
{
|
||||
"Index": 10,
|
||||
"TrainNum": "4555M",
|
||||
"Pos": "松山",
|
||||
"PosNum": 243,
|
||||
"delay": 1,
|
||||
"Direction": 1,
|
||||
"Type": "normal",
|
||||
"Line": "uwajima"
|
||||
},
|
||||
{
|
||||
"Index": 12,
|
||||
"TrainNum": "925D",
|
||||
"Pos": "松山",
|
||||
"PosNum": 241,
|
||||
"delay": "入線",
|
||||
"Direction": 1,
|
||||
"Type": "normal",
|
||||
"Line": "uwajima"
|
||||
},
|
||||
{
|
||||
"Index": 22,
|
||||
"TrainNum": "19E",
|
||||
"Pos": "市坪",
|
||||
"PosNum": 18,
|
||||
"delay": 0,
|
||||
"Direction": 1,
|
||||
"Type": "normal",
|
||||
"Line": "uwajima"
|
||||
},
|
||||
{
|
||||
"Index": 37,
|
||||
"TrainNum": "9046E",
|
||||
"Pos": "",
|
||||
"PosNum": 35,
|
||||
"delay": "入線",
|
||||
"Direction": 0,
|
||||
"Type": "normal",
|
||||
"Line": "uwajima"
|
||||
},
|
||||
{
|
||||
"Index": 38,
|
||||
"TrainNum": "560E",
|
||||
"Pos": "",
|
||||
"PosNum": 34,
|
||||
"delay": "入線",
|
||||
"Direction": 0,
|
||||
"Type": "normal",
|
||||
"Line": "uwajima"
|
||||
},
|
||||
{
|
||||
"Index": 43,
|
||||
"TrainNum": "1077D",
|
||||
"Pos": "向井原~伊予中山",
|
||||
"PosNum": 49,
|
||||
"delay": 0,
|
||||
"Direction": 1,
|
||||
"Type": "express:宇和海27号\r",
|
||||
"Line": "uwajima"
|
||||
},
|
||||
{
|
||||
"Index": 51,
|
||||
"TrainNum": "4926D",
|
||||
"Pos": "伊予上灘~伊予長浜",
|
||||
"PosNum": 197,
|
||||
"delay": 0,
|
||||
"Direction": 0,
|
||||
"Type": "normal",
|
||||
"Line": "uwajima"
|
||||
},
|
||||
{
|
||||
"Index": 54,
|
||||
"TrainNum": "4655D",
|
||||
"Pos": "伊予立川~内子",
|
||||
"PosNum": 68,
|
||||
"delay": 0,
|
||||
"Direction": 1,
|
||||
"Type": "normal",
|
||||
"Line": "uwajima"
|
||||
},
|
||||
{
|
||||
"Index": 69,
|
||||
"TrainNum": "1078D",
|
||||
"Pos": "新谷~伊予大洲",
|
||||
"PosNum": 87,
|
||||
"delay": 4,
|
||||
"Direction": 0,
|
||||
"Type": "express:宇和海28号\r",
|
||||
"Line": "uwajima"
|
||||
},
|
||||
{
|
||||
"Index": 83,
|
||||
"TrainNum": "923D",
|
||||
"Pos": "千丈",
|
||||
"PosNum": 113,
|
||||
"delay": 4,
|
||||
"Direction": 1,
|
||||
"Type": "normal",
|
||||
"Line": "uwajima"
|
||||
},
|
||||
{
|
||||
"Index": 86,
|
||||
"TrainNum": "4928D",
|
||||
"Pos": "八幡浜",
|
||||
"PosNum": 123,
|
||||
"delay": "入線",
|
||||
"Direction": 0,
|
||||
"Type": "normal",
|
||||
"Line": "uwajima"
|
||||
},
|
||||
{
|
||||
"Index": 89,
|
||||
"TrainNum": "4659D",
|
||||
"Pos": "八幡浜",
|
||||
"PosNum": 124,
|
||||
"delay": "入線",
|
||||
"Direction": 1,
|
||||
"Type": "normal",
|
||||
"Line": "uwajima"
|
||||
},
|
||||
{
|
||||
"Index": 101,
|
||||
"TrainNum": "1075D",
|
||||
"Pos": "卯之町~下宇和",
|
||||
"PosNum": 150,
|
||||
"delay": 0,
|
||||
"Direction": 1,
|
||||
"Type": "express:宇和海25号\r",
|
||||
"Line": "uwajima"
|
||||
},
|
||||
{
|
||||
"Index": 110,
|
||||
"TrainNum": "4825D",
|
||||
"Pos": "北宇和島~宮野下方予告窓",
|
||||
"PosNum": 185,
|
||||
"delay": "入線",
|
||||
"Direction": 1,
|
||||
"Type": "normal",
|
||||
"Line": "uwajima"
|
||||
},
|
||||
{
|
||||
"Index": 116,
|
||||
"TrainNum": "1080D",
|
||||
"Pos": "宇和島",
|
||||
"PosNum": 188,
|
||||
"delay": "入線",
|
||||
"Direction": 0,
|
||||
"Type": "express:宇和海30号\r",
|
||||
"Line": "uwajima"
|
||||
},
|
||||
{
|
||||
"Index": 15,
|
||||
"TrainNum": "1250M",
|
||||
"Pos": "善通寺",
|
||||
"PosNum": 24,
|
||||
"delay": 1,
|
||||
"Direction": 0,
|
||||
"Type": "normal",
|
||||
"Line": "dosan"
|
||||
},
|
||||
{
|
||||
"Index": 18,
|
||||
"TrainNum": "5249M",
|
||||
"Pos": "善通寺~琴平",
|
||||
"PosNum": 29,
|
||||
"delay": 1,
|
||||
"Direction": 1,
|
||||
"Type": "normal",
|
||||
"Line": "dosan"
|
||||
},
|
||||
{
|
||||
"Index": 24,
|
||||
"TrainNum": "4255D",
|
||||
"Pos": "琴平",
|
||||
"PosNum": 34,
|
||||
"delay": 0,
|
||||
"Direction": 1,
|
||||
"Type": "normal",
|
||||
"Line": "dosan"
|
||||
},
|
||||
{
|
||||
"Index": 44,
|
||||
"TrainNum": "475D",
|
||||
"Pos": "佃予告窓",
|
||||
"PosNum": 68,
|
||||
"delay": "入線",
|
||||
"Direction": 1,
|
||||
"Type": "normal",
|
||||
"Line": "dosan"
|
||||
},
|
||||
{
|
||||
"Index": 45,
|
||||
"TrainNum": "5479D",
|
||||
"Pos": "佃予告窓",
|
||||
"PosNum": 69,
|
||||
"delay": "入線",
|
||||
"Direction": 1,
|
||||
"Type": "normal",
|
||||
"Line": "dosan"
|
||||
},
|
||||
{
|
||||
"Index": 55,
|
||||
"TrainNum": "5486D",
|
||||
"Pos": "阿波池田",
|
||||
"PosNum": 82,
|
||||
"delay": 0,
|
||||
"Direction": 0,
|
||||
"Type": "normal",
|
||||
"Line": "dosan"
|
||||
},
|
||||
{
|
||||
"Index": 68,
|
||||
"TrainNum": "51D",
|
||||
"Pos": "阿波川口",
|
||||
"PosNum": 98,
|
||||
"delay": 5,
|
||||
"Direction": 1,
|
||||
"Type": "express:南風21号\r",
|
||||
"Line": "dosan"
|
||||
},
|
||||
{
|
||||
"Index": 70,
|
||||
"TrainNum": "56D",
|
||||
"Pos": "阿波川口~小歩危",
|
||||
"PosNum": 101,
|
||||
"delay": 4,
|
||||
"Direction": 0,
|
||||
"Type": "express:南風26号\r",
|
||||
"Line": "dosan"
|
||||
},
|
||||
{
|
||||
"Index": 73,
|
||||
"TrainNum": "4253D",
|
||||
"Pos": "小歩危~大歩危",
|
||||
"PosNum": 107,
|
||||
"delay": 4,
|
||||
"Direction": 1,
|
||||
"Type": "normal",
|
||||
"Line": "dosan"
|
||||
},
|
||||
{
|
||||
"Index": 109,
|
||||
"TrainNum": "4256D",
|
||||
"Pos": "新改~土佐山田",
|
||||
"PosNum": 164,
|
||||
"delay": 7,
|
||||
"Direction": 0,
|
||||
"Type": "normal",
|
||||
"Line": "dosan"
|
||||
},
|
||||
{
|
||||
"Index": 113,
|
||||
"TrainNum": "4257D",
|
||||
"Pos": "土佐山田",
|
||||
"PosNum": 169,
|
||||
"delay": 0,
|
||||
"Direction": 1,
|
||||
"Type": "normal",
|
||||
"Line": "dosan"
|
||||
},
|
||||
{
|
||||
"Index": 117,
|
||||
"TrainNum": "5885D",
|
||||
"Pos": "後免予告窓",
|
||||
"PosNum": 176,
|
||||
"delay": "入線",
|
||||
"Direction": 1,
|
||||
"Type": "normal",
|
||||
"Line": "dosan"
|
||||
},
|
||||
{
|
||||
"Index": 118,
|
||||
"TrainNum": "5887D",
|
||||
"Pos": "後免予告窓",
|
||||
"PosNum": 177,
|
||||
"delay": "入線",
|
||||
"Direction": 1,
|
||||
"Type": "normal",
|
||||
"Line": "dosan"
|
||||
},
|
||||
{
|
||||
"Index": 119,
|
||||
"TrainNum": "5883D",
|
||||
"Pos": "なはり方~後免",
|
||||
"PosNum": 178,
|
||||
"delay": "入線",
|
||||
"Direction": 1,
|
||||
"Type": "normal",
|
||||
"Line": "dosan"
|
||||
},
|
||||
{
|
||||
"Index": 125,
|
||||
"TrainNum": "753D",
|
||||
"Pos": "後免",
|
||||
"PosNum": 179,
|
||||
"delay": 5,
|
||||
"Direction": 1,
|
||||
"Type": "normal",
|
||||
"Line": "dosan"
|
||||
},
|
||||
{
|
||||
"Index": 127,
|
||||
"TrainNum": "49D",
|
||||
"Pos": "後免~土佐大津",
|
||||
"PosNum": 189,
|
||||
"delay": 7,
|
||||
"Direction": 1,
|
||||
"Type": "express:南風19号\r",
|
||||
"Line": "dosan"
|
||||
},
|
||||
{
|
||||
"Index": 129,
|
||||
"TrainNum": "58D",
|
||||
"Pos": "土佐大津",
|
||||
"PosNum": 191,
|
||||
"delay": 4,
|
||||
"Direction": 0,
|
||||
"Type": "express:南風28号\r",
|
||||
"Line": "dosan"
|
||||
},
|
||||
{
|
||||
"Index": 132,
|
||||
"TrainNum": "3759A",
|
||||
"Pos": "土佐一宮予告窓",
|
||||
"PosNum": 197,
|
||||
"delay": "入線",
|
||||
"Direction": 1,
|
||||
"Type": "normal",
|
||||
"Line": "dosan"
|
||||
},
|
||||
{
|
||||
"Index": 133,
|
||||
"TrainNum": "289A",
|
||||
"Pos": "土佐一宮予告窓",
|
||||
"PosNum": 198,
|
||||
"delay": "入線",
|
||||
"Direction": 1,
|
||||
"Type": "normal",
|
||||
"Line": "dosan"
|
||||
},
|
||||
{
|
||||
"Index": 141,
|
||||
"TrainNum": "756D",
|
||||
"Pos": "薊野",
|
||||
"PosNum": 210,
|
||||
"delay": 1,
|
||||
"Direction": 0,
|
||||
"Type": "normal",
|
||||
"Line": "dosan"
|
||||
},
|
||||
{
|
||||
"Index": 2,
|
||||
"TrainNum": "4758D",
|
||||
"Pos": "高知~旭",
|
||||
"PosNum": 258,
|
||||
"delay": 0,
|
||||
"Direction": 0,
|
||||
"Type": "normal",
|
||||
"Line": "kubokawa"
|
||||
},
|
||||
{
|
||||
"Index": 12,
|
||||
"TrainNum": "751D",
|
||||
"Pos": "伊野~日下",
|
||||
"PosNum": 277,
|
||||
"delay": 0,
|
||||
"Direction": 1,
|
||||
"Type": "normal",
|
||||
"Line": "kubokawa"
|
||||
},
|
||||
{
|
||||
"Index": 27,
|
||||
"TrainNum": "2081D",
|
||||
"Pos": "斗賀野",
|
||||
"PosNum": 306,
|
||||
"delay": 0,
|
||||
"Direction": 1,
|
||||
"Type": "express:あしずり11号\r",
|
||||
"Line": "kubokawa"
|
||||
},
|
||||
{
|
||||
"Index": 31,
|
||||
"TrainNum": "4760D",
|
||||
"Pos": "吾桑~多ノ郷",
|
||||
"PosNum": 314,
|
||||
"delay": 0,
|
||||
"Direction": 0,
|
||||
"Type": "normal",
|
||||
"Line": "kubokawa"
|
||||
},
|
||||
{
|
||||
"Index": 38,
|
||||
"TrainNum": "2004D",
|
||||
"Pos": "須崎~土佐久礼",
|
||||
"PosNum": 327,
|
||||
"delay": 0,
|
||||
"Direction": 0,
|
||||
"Type": "express:しまんと4号\r",
|
||||
"Line": "kubokawa"
|
||||
},
|
||||
{
|
||||
"GetDateTime": "2026/05/01 19:42:21"
|
||||
}
|
||||
]
|
||||
946
lib/mockApi/mockData/train_lang.json
Normal file
@@ -0,0 +1,946 @@
|
||||
[
|
||||
{
|
||||
"Station_JP": "高知商業前",
|
||||
"Station_EN": "Kōchi-Shōgyōmae"
|
||||
},
|
||||
{
|
||||
"Station_JP": "古高松南",
|
||||
"Station_EN": "Furutakamatsu-Minami"
|
||||
},
|
||||
{
|
||||
"Station_JP": "高松",
|
||||
"Station_EN": "Takamatsu"
|
||||
},
|
||||
{
|
||||
"Station_JP": "香西",
|
||||
"Station_EN": "Kōzai"
|
||||
},
|
||||
{
|
||||
"Station_JP": "鬼無",
|
||||
"Station_EN": "Kinashi"
|
||||
},
|
||||
{
|
||||
"Station_JP": "端岡",
|
||||
"Station_EN": "Hashioka"
|
||||
},
|
||||
{
|
||||
"Station_JP": "国分",
|
||||
"Station_EN": "Kokubu"
|
||||
},
|
||||
{
|
||||
"Station_JP": "讃岐府中",
|
||||
"Station_EN": "Sanuki-Fuchū"
|
||||
},
|
||||
{
|
||||
"Station_JP": "鴨川",
|
||||
"Station_EN": "Kamogawa"
|
||||
},
|
||||
{
|
||||
"Station_JP": "八十場",
|
||||
"Station_EN": "Yasoba"
|
||||
},
|
||||
{
|
||||
"Station_JP": "坂出",
|
||||
"Station_EN": "Sakaide"
|
||||
},
|
||||
{
|
||||
"Station_JP": "宇多津",
|
||||
"Station_EN": "Utazu"
|
||||
},
|
||||
{
|
||||
"Station_JP": "丸亀",
|
||||
"Station_EN": "Marugame"
|
||||
},
|
||||
{
|
||||
"Station_JP": "讃岐塩屋",
|
||||
"Station_EN": "Sanuki-Shioya"
|
||||
},
|
||||
{
|
||||
"Station_JP": "多度津",
|
||||
"Station_EN": "Tadotsu"
|
||||
},
|
||||
{
|
||||
"Station_JP": "海岸寺",
|
||||
"Station_EN": "Kaiganji"
|
||||
},
|
||||
{
|
||||
"Station_JP": "詫間",
|
||||
"Station_EN": "Takuma"
|
||||
},
|
||||
{
|
||||
"Station_JP": "みの",
|
||||
"Station_EN": "Mino"
|
||||
},
|
||||
{
|
||||
"Station_JP": "高瀬",
|
||||
"Station_EN": "Takase"
|
||||
},
|
||||
{
|
||||
"Station_JP": "比地大",
|
||||
"Station_EN": "Hijidai"
|
||||
},
|
||||
{
|
||||
"Station_JP": "本山",
|
||||
"Station_EN": "Motoyama"
|
||||
},
|
||||
{
|
||||
"Station_JP": "観音寺",
|
||||
"Station_EN": "Kan-onji"
|
||||
},
|
||||
{
|
||||
"Station_JP": "豊浜",
|
||||
"Station_EN": "Toyohama"
|
||||
},
|
||||
{
|
||||
"Station_JP": "箕浦",
|
||||
"Station_EN": "Minoura"
|
||||
},
|
||||
{
|
||||
"Station_JP": "川之江",
|
||||
"Station_EN": "Kawanoe"
|
||||
},
|
||||
{
|
||||
"Station_JP": "伊予三島",
|
||||
"Station_EN": "Iyo-Mishima"
|
||||
},
|
||||
{
|
||||
"Station_JP": "伊予寒川",
|
||||
"Station_EN": "Iyo-Sangawa"
|
||||
},
|
||||
{
|
||||
"Station_JP": "赤星",
|
||||
"Station_EN": "Akaboshi"
|
||||
},
|
||||
{
|
||||
"Station_JP": "伊予土居",
|
||||
"Station_EN": "Iyo-Doi"
|
||||
},
|
||||
{
|
||||
"Station_JP": "関川",
|
||||
"Station_EN": "Sekigawa"
|
||||
},
|
||||
{
|
||||
"Station_JP": "多喜浜",
|
||||
"Station_EN": "Takihama"
|
||||
},
|
||||
{
|
||||
"Station_JP": "新居浜",
|
||||
"Station_EN": "Niihama"
|
||||
},
|
||||
{
|
||||
"Station_JP": "中萩",
|
||||
"Station_EN": "Nakahagi"
|
||||
},
|
||||
{
|
||||
"Station_JP": "伊予西条",
|
||||
"Station_EN": "Iyo-Saijo"
|
||||
},
|
||||
{
|
||||
"Station_JP": "石鎚山",
|
||||
"Station_EN": "Ishizuchiyama"
|
||||
},
|
||||
{
|
||||
"Station_JP": "伊予氷見",
|
||||
"Station_EN": "Iyo-Himi"
|
||||
},
|
||||
{
|
||||
"Station_JP": "伊予小松",
|
||||
"Station_EN": "Iyo-Komatsu"
|
||||
},
|
||||
{
|
||||
"Station_JP": "玉之江",
|
||||
"Station_EN": "Tamanoe"
|
||||
},
|
||||
{
|
||||
"Station_JP": "壬生川",
|
||||
"Station_EN": "Nyūgawa"
|
||||
},
|
||||
{
|
||||
"Station_JP": "伊予三芳",
|
||||
"Station_EN": "Iyo-Miyoshi"
|
||||
},
|
||||
{
|
||||
"Station_JP": "伊予桜井",
|
||||
"Station_EN": "Iyo-Sakurai"
|
||||
},
|
||||
{
|
||||
"Station_JP": "伊予富田",
|
||||
"Station_EN": "Iyo-Tomita"
|
||||
},
|
||||
{
|
||||
"Station_JP": "今治",
|
||||
"Station_EN": "Imabari"
|
||||
},
|
||||
{
|
||||
"Station_JP": "波止浜",
|
||||
"Station_EN": "Hashihama"
|
||||
},
|
||||
{
|
||||
"Station_JP": "波方",
|
||||
"Station_EN": "Namikata"
|
||||
},
|
||||
{
|
||||
"Station_JP": "大西",
|
||||
"Station_EN": "Ōnishi"
|
||||
},
|
||||
{
|
||||
"Station_JP": "伊予亀岡",
|
||||
"Station_EN": "Iyo-kameoka"
|
||||
},
|
||||
{
|
||||
"Station_JP": "菊間",
|
||||
"Station_EN": "Kikuma"
|
||||
},
|
||||
{
|
||||
"Station_JP": "浅海",
|
||||
"Station_EN": "Asanami"
|
||||
},
|
||||
{
|
||||
"Station_JP": "大浦",
|
||||
"Station_EN": "Ōura"
|
||||
},
|
||||
{
|
||||
"Station_JP": "伊予北条",
|
||||
"Station_EN": "Iyo-Hōjō"
|
||||
},
|
||||
{
|
||||
"Station_JP": "柳原",
|
||||
"Station_EN": "Yanagihara"
|
||||
},
|
||||
{
|
||||
"Station_JP": "粟井",
|
||||
"Station_EN": "Awai"
|
||||
},
|
||||
{
|
||||
"Station_JP": "光洋台",
|
||||
"Station_EN": "Kōyōdai"
|
||||
},
|
||||
{
|
||||
"Station_JP": "堀江",
|
||||
"Station_EN": "Horie"
|
||||
},
|
||||
{
|
||||
"Station_JP": "伊予和気",
|
||||
"Station_EN": "Iyo-Wake"
|
||||
},
|
||||
{
|
||||
"Station_JP": "三津浜",
|
||||
"Station_EN": "Mitsuhama"
|
||||
},
|
||||
{
|
||||
"Station_JP": "松山",
|
||||
"Station_EN": "Matsuyama"
|
||||
},
|
||||
{
|
||||
"Station_JP": "松山",
|
||||
"Station_EN": "Matsuyama"
|
||||
},
|
||||
{
|
||||
"Station_JP": "市坪",
|
||||
"Station_EN": "Ichitsubo"
|
||||
},
|
||||
{
|
||||
"Station_JP": "北伊予",
|
||||
"Station_EN": "Kita-Iyo"
|
||||
},
|
||||
{
|
||||
"Station_JP": "南伊予",
|
||||
"Station_EN": "Minami-Iyo"
|
||||
},
|
||||
{
|
||||
"Station_JP": "伊予横田",
|
||||
"Station_EN": "Iyo-Yokota"
|
||||
},
|
||||
{
|
||||
"Station_JP": "鳥ノ木",
|
||||
"Station_EN": "Torinoki"
|
||||
},
|
||||
{
|
||||
"Station_JP": "伊予市",
|
||||
"Station_EN": "Iyoshi"
|
||||
},
|
||||
{
|
||||
"Station_JP": "向井原",
|
||||
"Station_EN": "Mukaibara"
|
||||
},
|
||||
{
|
||||
"Station_JP": "高野川",
|
||||
"Station_EN": "Kōnokawa"
|
||||
},
|
||||
{
|
||||
"Station_JP": "伊予上灘",
|
||||
"Station_EN": "Iyo-Kaminada"
|
||||
},
|
||||
{
|
||||
"Station_JP": "下灘",
|
||||
"Station_EN": "Shimonada"
|
||||
},
|
||||
{
|
||||
"Station_JP": "串",
|
||||
"Station_EN": "Kushi"
|
||||
},
|
||||
{
|
||||
"Station_JP": "喜多灘",
|
||||
"Station_EN": "Kitanada"
|
||||
},
|
||||
{
|
||||
"Station_JP": "伊予長浜",
|
||||
"Station_EN": "Iyo-Nagahama"
|
||||
},
|
||||
{
|
||||
"Station_JP": "伊予出石",
|
||||
"Station_EN": "Iyo-Izushi"
|
||||
},
|
||||
{
|
||||
"Station_JP": "伊予白滝",
|
||||
"Station_EN": "Iyo-Shirataki"
|
||||
},
|
||||
{
|
||||
"Station_JP": "八多喜",
|
||||
"Station_EN": "Hataki"
|
||||
},
|
||||
{
|
||||
"Station_JP": "春賀",
|
||||
"Station_EN": "Haruka"
|
||||
},
|
||||
{
|
||||
"Station_JP": "五郎",
|
||||
"Station_EN": "Gorō"
|
||||
},
|
||||
{
|
||||
"Station_JP": "伊予大洲",
|
||||
"Station_EN": "Iyo-Ōzu"
|
||||
},
|
||||
{
|
||||
"Station_JP": "西大洲",
|
||||
"Station_EN": "Nishi-Ōzu"
|
||||
},
|
||||
{
|
||||
"Station_JP": "伊予平野",
|
||||
"Station_EN": "Iyo-Hirano"
|
||||
},
|
||||
{
|
||||
"Station_JP": "千丈",
|
||||
"Station_EN": "Senjō"
|
||||
},
|
||||
{
|
||||
"Station_JP": "八幡浜",
|
||||
"Station_EN": "Yawatahama"
|
||||
},
|
||||
{
|
||||
"Station_JP": "双岩",
|
||||
"Station_EN": "Futaiwa"
|
||||
},
|
||||
{
|
||||
"Station_JP": "伊予石城",
|
||||
"Station_EN": "Iyo-Iwaki"
|
||||
},
|
||||
{
|
||||
"Station_JP": "上宇和",
|
||||
"Station_EN": "Kami-Uwa"
|
||||
},
|
||||
{
|
||||
"Station_JP": "卯之町",
|
||||
"Station_EN": "Unomachi"
|
||||
},
|
||||
{
|
||||
"Station_JP": "下宇和",
|
||||
"Station_EN": "Shimo-Uwa"
|
||||
},
|
||||
{
|
||||
"Station_JP": "立間",
|
||||
"Station_EN": "Tachima"
|
||||
},
|
||||
{
|
||||
"Station_JP": "伊予吉田",
|
||||
"Station_EN": "Iyo-Yoshida"
|
||||
},
|
||||
{
|
||||
"Station_JP": "高光",
|
||||
"Station_EN": "Takamitsu"
|
||||
},
|
||||
{
|
||||
"Station_JP": "北宇和島",
|
||||
"Station_EN": "Kita-Uwajima"
|
||||
},
|
||||
{
|
||||
"Station_JP": "宇和島",
|
||||
"Station_EN": "Uwajima"
|
||||
},
|
||||
{
|
||||
"Station_JP": "佐古",
|
||||
"Station_EN": "Sako"
|
||||
},
|
||||
{
|
||||
"Station_JP": "蔵本",
|
||||
"Station_EN": "Kuramoto"
|
||||
},
|
||||
{
|
||||
"Station_JP": "鮎喰",
|
||||
"Station_EN": "Akui"
|
||||
},
|
||||
{
|
||||
"Station_JP": "府中",
|
||||
"Station_EN": "Kō"
|
||||
},
|
||||
{
|
||||
"Station_JP": "石井",
|
||||
"Station_EN": "Ishii"
|
||||
},
|
||||
{
|
||||
"Station_JP": "下浦",
|
||||
"Station_EN": "Shimoura"
|
||||
},
|
||||
{
|
||||
"Station_JP": "牛島",
|
||||
"Station_EN": "Ushinoshima"
|
||||
},
|
||||
{
|
||||
"Station_JP": "麻植塚",
|
||||
"Station_EN": "Oezuka"
|
||||
},
|
||||
{
|
||||
"Station_JP": "鴨島",
|
||||
"Station_EN": "Kamojima"
|
||||
},
|
||||
{
|
||||
"Station_JP": "西麻植",
|
||||
"Station_EN": "Nishi-Oe"
|
||||
},
|
||||
{
|
||||
"Station_JP": "阿波川島",
|
||||
"Station_EN": "Awa-Kawashima"
|
||||
},
|
||||
{
|
||||
"Station_JP": "学",
|
||||
"Station_EN": "Gaku"
|
||||
},
|
||||
{
|
||||
"Station_JP": "山瀬",
|
||||
"Station_EN": "Yamase"
|
||||
},
|
||||
{
|
||||
"Station_JP": "阿波山川",
|
||||
"Station_EN": "Awa-Yamakawa"
|
||||
},
|
||||
{
|
||||
"Station_JP": "川田",
|
||||
"Station_EN": "Kawata"
|
||||
},
|
||||
{
|
||||
"Station_JP": "穴吹",
|
||||
"Station_EN": "Anabuki"
|
||||
},
|
||||
{
|
||||
"Station_JP": "小島",
|
||||
"Station_EN": "Oshima"
|
||||
},
|
||||
{
|
||||
"Station_JP": "貞光",
|
||||
"Station_EN": "Sadamitsu"
|
||||
},
|
||||
{
|
||||
"Station_JP": "阿波半田",
|
||||
"Station_EN": "Awa-Handa"
|
||||
},
|
||||
{
|
||||
"Station_JP": "江口",
|
||||
"Station_EN": "Eguchi"
|
||||
},
|
||||
{
|
||||
"Station_JP": "三加茂",
|
||||
"Station_EN": "Mikamo"
|
||||
},
|
||||
{
|
||||
"Station_JP": "阿波加茂",
|
||||
"Station_EN": "Awa-Kamo"
|
||||
},
|
||||
{
|
||||
"Station_JP": "辻",
|
||||
"Station_EN": "Tsuji"
|
||||
},
|
||||
{
|
||||
"Station_JP": "高松",
|
||||
"Station_EN": "Takamatsu"
|
||||
},
|
||||
{
|
||||
"Station_JP": "昭和町",
|
||||
"Station_EN": "Shōwachō"
|
||||
},
|
||||
{
|
||||
"Station_JP": "栗林公園北口",
|
||||
"Station_EN": "Ritsurinkōen-Kitaguchi"
|
||||
},
|
||||
{
|
||||
"Station_JP": "栗林",
|
||||
"Station_EN": "Ritsurin"
|
||||
},
|
||||
{
|
||||
"Station_JP": "木太町",
|
||||
"Station_EN": "Kitachō"
|
||||
},
|
||||
{
|
||||
"Station_JP": "屋島",
|
||||
"Station_EN": "Yashima"
|
||||
},
|
||||
{
|
||||
"Station_JP": "八栗口",
|
||||
"Station_EN": "Yakuriguchi"
|
||||
},
|
||||
{
|
||||
"Station_JP": "讃岐牟礼",
|
||||
"Station_EN": "Sanuki-Mure"
|
||||
},
|
||||
{
|
||||
"Station_JP": "志度",
|
||||
"Station_EN": "Shido"
|
||||
},
|
||||
{
|
||||
"Station_JP": "オレンジタウン",
|
||||
"Station_EN": "Orange-Town"
|
||||
},
|
||||
{
|
||||
"Station_JP": "造田",
|
||||
"Station_EN": "Zōda"
|
||||
},
|
||||
{
|
||||
"Station_JP": "神前",
|
||||
"Station_EN": "Kanzaki"
|
||||
},
|
||||
{
|
||||
"Station_JP": "讃岐津田",
|
||||
"Station_EN": "Sanuki-Tsuda"
|
||||
},
|
||||
{
|
||||
"Station_JP": "鶴羽",
|
||||
"Station_EN": "Tsuruwa"
|
||||
},
|
||||
{
|
||||
"Station_JP": "丹生",
|
||||
"Station_EN": "Nibu"
|
||||
},
|
||||
{
|
||||
"Station_JP": "三本松",
|
||||
"Station_EN": "Sambommatsu"
|
||||
},
|
||||
{
|
||||
"Station_JP": "讃岐白鳥",
|
||||
"Station_EN": "Sanuki-Shirotori"
|
||||
},
|
||||
{
|
||||
"Station_JP": "引田",
|
||||
"Station_EN": "Hiketa"
|
||||
},
|
||||
{
|
||||
"Station_JP": "讃岐相生",
|
||||
"Station_EN": "Sanuki-Aioi"
|
||||
},
|
||||
{
|
||||
"Station_JP": "阿波大宮",
|
||||
"Station_EN": "Awa-Ōmiya"
|
||||
},
|
||||
{
|
||||
"Station_JP": "板野",
|
||||
"Station_EN": "Itano"
|
||||
},
|
||||
{
|
||||
"Station_JP": "阿波川端",
|
||||
"Station_EN": "Awa-Kawabata"
|
||||
},
|
||||
{
|
||||
"Station_JP": "板東",
|
||||
"Station_EN": "Bandō"
|
||||
},
|
||||
{
|
||||
"Station_JP": "池谷",
|
||||
"Station_EN": "Ikenotani"
|
||||
},
|
||||
{
|
||||
"Station_JP": "勝瑞",
|
||||
"Station_EN": "Shōzui"
|
||||
},
|
||||
{
|
||||
"Station_JP": "吉成",
|
||||
"Station_EN": "Yoshinari"
|
||||
},
|
||||
{
|
||||
"Station_JP": "佐古",
|
||||
"Station_EN": "Sako"
|
||||
},
|
||||
{
|
||||
"Station_JP": "徳島",
|
||||
"Station_EN": "Tokushima"
|
||||
},
|
||||
{
|
||||
"Station_JP": "多度津",
|
||||
"Station_EN": "Tadotsu"
|
||||
},
|
||||
{
|
||||
"Station_JP": "金蔵寺",
|
||||
"Station_EN": "Konzōji"
|
||||
},
|
||||
{
|
||||
"Station_JP": "善通寺",
|
||||
"Station_EN": "Zentsūji"
|
||||
},
|
||||
{
|
||||
"Station_JP": "琴平",
|
||||
"Station_EN": "Kotohira"
|
||||
},
|
||||
{
|
||||
"Station_JP": "塩入",
|
||||
"Station_EN": "Shioiri"
|
||||
},
|
||||
{
|
||||
"Station_JP": "黒川",
|
||||
"Station_EN": "Kurokawa"
|
||||
},
|
||||
{
|
||||
"Station_JP": "讃岐財田",
|
||||
"Station_EN": "Sanuki-Saida"
|
||||
},
|
||||
{
|
||||
"Station_JP": "坪尻",
|
||||
"Station_EN": "Tsubojiri"
|
||||
},
|
||||
{
|
||||
"Station_JP": "箸蔵",
|
||||
"Station_EN": "Hashikura"
|
||||
},
|
||||
{
|
||||
"Station_JP": "佃",
|
||||
"Station_EN": "Tsukuda"
|
||||
},
|
||||
{
|
||||
"Station_JP": "阿波池田",
|
||||
"Station_EN": "Awa-Ikeda"
|
||||
},
|
||||
{
|
||||
"Station_JP": "三縄",
|
||||
"Station_EN": "Minawa"
|
||||
},
|
||||
{
|
||||
"Station_JP": "祖谷口",
|
||||
"Station_EN": "Iyaguchi"
|
||||
},
|
||||
{
|
||||
"Station_JP": "阿波川口",
|
||||
"Station_EN": "Awa-Kawaguchi"
|
||||
},
|
||||
{
|
||||
"Station_JP": "小歩危",
|
||||
"Station_EN": "Koboke"
|
||||
},
|
||||
{
|
||||
"Station_JP": "大歩危",
|
||||
"Station_EN": "Ōboke"
|
||||
},
|
||||
{
|
||||
"Station_JP": "土佐岩原",
|
||||
"Station_EN": "Tosa-Iwahara"
|
||||
},
|
||||
{
|
||||
"Station_JP": "豊永",
|
||||
"Station_EN": "Toyonaga"
|
||||
},
|
||||
{
|
||||
"Station_JP": "大田口",
|
||||
"Station_EN": "Ōtaguchi"
|
||||
},
|
||||
{
|
||||
"Station_JP": "土佐穴内",
|
||||
"Station_EN": "Tosa-Ananai"
|
||||
},
|
||||
{
|
||||
"Station_JP": "大杉",
|
||||
"Station_EN": "Ōsugi"
|
||||
},
|
||||
{
|
||||
"Station_JP": "土佐北川",
|
||||
"Station_EN": "Tosa-Kitagawa"
|
||||
},
|
||||
{
|
||||
"Station_JP": "角茂谷",
|
||||
"Station_EN": "Kakumodani"
|
||||
},
|
||||
{
|
||||
"Station_JP": "繁藤",
|
||||
"Station_EN": "Shigetō"
|
||||
},
|
||||
{
|
||||
"Station_JP": "新改",
|
||||
"Station_EN": "Shingai"
|
||||
},
|
||||
{
|
||||
"Station_JP": "土佐山田",
|
||||
"Station_EN": "Tosa-Yamada"
|
||||
},
|
||||
{
|
||||
"Station_JP": "山田西町",
|
||||
"Station_EN": "Yamadanishimachi"
|
||||
},
|
||||
{
|
||||
"Station_JP": "土佐長岡",
|
||||
"Station_EN": "Tosa-Nagaoka"
|
||||
},
|
||||
{
|
||||
"Station_JP": "後免",
|
||||
"Station_EN": "Gomen"
|
||||
},
|
||||
{
|
||||
"Station_JP": "土佐大津",
|
||||
"Station_EN": "Tosa-Ōtsu"
|
||||
},
|
||||
{
|
||||
"Station_JP": "布師田",
|
||||
"Station_EN": "Nunoshida"
|
||||
},
|
||||
{
|
||||
"Station_JP": "土佐一宮",
|
||||
"Station_EN": "Tosa-Ikku"
|
||||
},
|
||||
{
|
||||
"Station_JP": "薊野",
|
||||
"Station_EN": "Azōno"
|
||||
},
|
||||
{
|
||||
"Station_JP": "高知",
|
||||
"Station_EN": "Kōchi"
|
||||
},
|
||||
{
|
||||
"Station_JP": "入明",
|
||||
"Station_EN": "Iriake"
|
||||
},
|
||||
{
|
||||
"Station_JP": "円行寺口",
|
||||
"Station_EN": "Engyōjiguchi"
|
||||
},
|
||||
{
|
||||
"Station_JP": "旭",
|
||||
"Station_EN": "Asahi"
|
||||
},
|
||||
{
|
||||
"Station_JP": "朝倉",
|
||||
"Station_EN": "Asakura"
|
||||
},
|
||||
{
|
||||
"Station_JP": "枝川",
|
||||
"Station_EN": "Edagawa"
|
||||
},
|
||||
{
|
||||
"Station_JP": "伊野",
|
||||
"Station_EN": "Ino"
|
||||
},
|
||||
{
|
||||
"Station_JP": "波川",
|
||||
"Station_EN": "Hakawa"
|
||||
},
|
||||
{
|
||||
"Station_JP": "小村神社前",
|
||||
"Station_EN": "Omurajinjamae"
|
||||
},
|
||||
{
|
||||
"Station_JP": "日下",
|
||||
"Station_EN": "Kusaka"
|
||||
},
|
||||
{
|
||||
"Station_JP": "岡花",
|
||||
"Station_EN": "Okabana"
|
||||
},
|
||||
{
|
||||
"Station_JP": "土佐加茂",
|
||||
"Station_EN": "Tosa-Kamo"
|
||||
},
|
||||
{
|
||||
"Station_JP": "西佐川",
|
||||
"Station_EN": "Nishi-Sakawa"
|
||||
},
|
||||
{
|
||||
"Station_JP": "佐川",
|
||||
"Station_EN": "Sakawa"
|
||||
},
|
||||
{
|
||||
"Station_JP": "襟野々",
|
||||
"Station_EN": "Erinono"
|
||||
},
|
||||
{
|
||||
"Station_JP": "斗賀野",
|
||||
"Station_EN": "Togano"
|
||||
},
|
||||
{
|
||||
"Station_JP": "吾桑",
|
||||
"Station_EN": "Asō"
|
||||
},
|
||||
{
|
||||
"Station_JP": "多ノ郷",
|
||||
"Station_EN": "Ōnogō"
|
||||
},
|
||||
{
|
||||
"Station_JP": "大間",
|
||||
"Station_EN": "Ōma"
|
||||
},
|
||||
{
|
||||
"Station_JP": "須崎",
|
||||
"Station_EN": "Susaki"
|
||||
},
|
||||
{
|
||||
"Station_JP": "土佐新荘",
|
||||
"Station_EN": "Tosa-Shinjō"
|
||||
},
|
||||
{
|
||||
"Station_JP": "安和",
|
||||
"Station_EN": "Awa"
|
||||
},
|
||||
{
|
||||
"Station_JP": "土佐久礼",
|
||||
"Station_EN": "Tosa-Kure"
|
||||
},
|
||||
{
|
||||
"Station_JP": "影野",
|
||||
"Station_EN": "Kageno"
|
||||
},
|
||||
{
|
||||
"Station_JP": "六反地",
|
||||
"Station_EN": "Rokutanji"
|
||||
},
|
||||
{
|
||||
"Station_JP": "仁井田",
|
||||
"Station_EN": "Niida"
|
||||
},
|
||||
{
|
||||
"Station_JP": "窪川",
|
||||
"Station_EN": "Kubokawa"
|
||||
},
|
||||
{
|
||||
"Station_JP": "児島",
|
||||
"Station_EN": "Kojima"
|
||||
},
|
||||
{
|
||||
"Station_JP": "上の町",
|
||||
"Station_EN": "KaminochŌ"
|
||||
},
|
||||
{
|
||||
"Station_JP": "木見",
|
||||
"Station_EN": "Kimi"
|
||||
},
|
||||
{
|
||||
"Station_JP": "植松",
|
||||
"Station_EN": "Uematsu"
|
||||
},
|
||||
{
|
||||
"Station_JP": "茶屋町",
|
||||
"Station_EN": "Chayamachi"
|
||||
},
|
||||
{
|
||||
"Station_JP": "久々原",
|
||||
"Station_EN": "Kuguhara"
|
||||
},
|
||||
{
|
||||
"Station_JP": "早島",
|
||||
"Station_EN": "kojima"
|
||||
},
|
||||
{
|
||||
"Station_JP": "備中箕島",
|
||||
"Station_EN": "Bitchū-Mishima"
|
||||
},
|
||||
{
|
||||
"Station_JP": "妹尾",
|
||||
"Station_EN": "Senoo"
|
||||
},
|
||||
{
|
||||
"Station_JP": "備前西市",
|
||||
"Station_EN": "Bizen-Nishiichi"
|
||||
},
|
||||
{
|
||||
"Station_JP": "大元",
|
||||
"Station_EN": "Ōmoto"
|
||||
},
|
||||
{
|
||||
"Station_JP": "岡山",
|
||||
"Station_EN": "Okayama"
|
||||
},
|
||||
{
|
||||
"Station_JP": "新谷",
|
||||
"Station_EN": "Niiya"
|
||||
},
|
||||
{
|
||||
"Station_JP": "伊予大洲",
|
||||
"Station_EN": "Iyo-Ōzu"
|
||||
},
|
||||
{
|
||||
"Station_JP": "伊予平野",
|
||||
"Station_EN": "Iyo-Hirano"
|
||||
},
|
||||
{
|
||||
"Station_JP": "内子",
|
||||
"Station_EN": "Uchiko"
|
||||
},
|
||||
{
|
||||
"Station_JP": "五十崎",
|
||||
"Station_EN": "Ikazaki"
|
||||
},
|
||||
{
|
||||
"Station_JP": "喜多山",
|
||||
"Station_EN": "Kitayama"
|
||||
},
|
||||
{
|
||||
"Station_JP": "伊予立川",
|
||||
"Station_EN": "Iyo-Tachikawa"
|
||||
},
|
||||
{
|
||||
"Station_JP": "伊予大平",
|
||||
"Station_EN": "Iyo-Ōhira"
|
||||
},
|
||||
{
|
||||
"Station_JP": "伊予中山",
|
||||
"Station_EN": "Iyo-Nakayama"
|
||||
},
|
||||
{
|
||||
"Station_JP": "中村",
|
||||
"Station_EN": "Nakamura"
|
||||
},
|
||||
{
|
||||
"Station_JP": "土佐入野",
|
||||
"Station_EN": "Tosa-irino"
|
||||
},
|
||||
{
|
||||
"Station_JP": "土佐上川口",
|
||||
"Station_EN": "Tosa-kamikawaguchi"
|
||||
},
|
||||
{
|
||||
"Station_JP": "土佐佐賀",
|
||||
"Station_EN": "Tosasaga"
|
||||
},
|
||||
{
|
||||
"Station_JP": "浮鞭",
|
||||
"Station_EN": "Ukibuchi"
|
||||
},
|
||||
{
|
||||
"Station_JP": "安芸",
|
||||
"Station_EN": "Aki"
|
||||
},
|
||||
{
|
||||
"Station_JP": "阿波大谷",
|
||||
"Station_EN": "Awa-Ōtani"
|
||||
},
|
||||
{
|
||||
"Station_JP": "立道",
|
||||
"Station_EN": "Tatsumichi"
|
||||
},
|
||||
{
|
||||
"Station_JP": "教会前",
|
||||
"Station_EN": "Kyōkaimae"
|
||||
},
|
||||
{
|
||||
"Station_JP": "金比羅前",
|
||||
"Station_EN": "Kompiramae"
|
||||
},
|
||||
{
|
||||
"Station_JP": "撫養",
|
||||
"Station_EN": "Muya"
|
||||
},
|
||||
{
|
||||
"Station_JP": "鳴門",
|
||||
"Station_EN": "Naruto"
|
||||
}
|
||||
]
|
||||
292
lib/mockApi/mockData/trainpare.json
Normal file
@@ -0,0 +1,292 @@
|
||||
[
|
||||
{
|
||||
"1M": {
|
||||
"1001M": "いしづち1号"
|
||||
}
|
||||
},
|
||||
{
|
||||
"1001M": {
|
||||
"1M": "しおかぜ1号"
|
||||
}
|
||||
},
|
||||
{
|
||||
"3M": {
|
||||
"1003M": "いしづち3号"
|
||||
}
|
||||
},
|
||||
{
|
||||
"1003M": {
|
||||
"3M": "しおかぜ3号"
|
||||
}
|
||||
},
|
||||
{
|
||||
"5M": {
|
||||
"1005M": "いしづち5号"
|
||||
}
|
||||
},
|
||||
{
|
||||
"1005M": {
|
||||
"5M": "しおかぜ5号"
|
||||
}
|
||||
},
|
||||
{
|
||||
"7M": {
|
||||
"1007M": "いしづち7号"
|
||||
}
|
||||
},
|
||||
{
|
||||
"1007M": {
|
||||
"7M": "しおかぜ7号"
|
||||
}
|
||||
},
|
||||
{
|
||||
"9M": {
|
||||
"1009M": "いしづち9号"
|
||||
}
|
||||
},
|
||||
{
|
||||
"1009M": {
|
||||
"9M": "しおかぜ9号"
|
||||
}
|
||||
},
|
||||
{
|
||||
"11M": {
|
||||
"1011M": "いしづち11号"
|
||||
}
|
||||
},
|
||||
{
|
||||
"1011M": {
|
||||
"11M": "しおかぜ11号"
|
||||
}
|
||||
},
|
||||
{
|
||||
"13M": {
|
||||
"1013M": "いしづち13号"
|
||||
}
|
||||
},
|
||||
{
|
||||
"1013M": {
|
||||
"13M": "しおかぜ13号"
|
||||
}
|
||||
},
|
||||
{
|
||||
"15M": {
|
||||
"1015M": "いしづち15号"
|
||||
}
|
||||
},
|
||||
{
|
||||
"1015M": {
|
||||
"15M": "しおかぜ15号"
|
||||
}
|
||||
},
|
||||
{
|
||||
"17M": {
|
||||
"1017M": "いしづち17号"
|
||||
}
|
||||
},
|
||||
{
|
||||
"1017M": {
|
||||
"17M": "しおかぜ17号"
|
||||
}
|
||||
},
|
||||
{
|
||||
"19M": {
|
||||
"1019M": "いしづち19号"
|
||||
}
|
||||
},
|
||||
{
|
||||
"1019M": {
|
||||
"19M": "しおかぜ19号"
|
||||
}
|
||||
},
|
||||
{
|
||||
"21M": {
|
||||
"1021M": "いしづち21号"
|
||||
}
|
||||
},
|
||||
{
|
||||
"1021M": {
|
||||
"21M": "しおかぜ21号"
|
||||
}
|
||||
},
|
||||
{
|
||||
"23M": {
|
||||
"1023M": "いしづち23号"
|
||||
}
|
||||
},
|
||||
{
|
||||
"1023M": {
|
||||
"23M": "しおかぜ23号"
|
||||
}
|
||||
},
|
||||
{
|
||||
"25M": {
|
||||
"1025M": "いしづち25号"
|
||||
}
|
||||
},
|
||||
{
|
||||
"1025M": {
|
||||
"25M": "しおかぜ25号"
|
||||
}
|
||||
},
|
||||
{
|
||||
"27M": {
|
||||
"1027M": "いしづち27号"
|
||||
}
|
||||
},
|
||||
{
|
||||
"1027M": {
|
||||
"27M": "しおかぜ27号"
|
||||
}
|
||||
},
|
||||
{
|
||||
"29M": {
|
||||
"1029M": "いしづち29号"
|
||||
}
|
||||
},
|
||||
{
|
||||
"1029M": {
|
||||
"29M": "しおかぜ29号"
|
||||
}
|
||||
},
|
||||
{
|
||||
"4M": {
|
||||
"1004M": "いしづち4号"
|
||||
}
|
||||
},
|
||||
{
|
||||
"1004M": {
|
||||
"4M": "しおかぜ4号"
|
||||
}
|
||||
},
|
||||
{
|
||||
"6M": {
|
||||
"1006M": "いしづち6号"
|
||||
}
|
||||
},
|
||||
{
|
||||
"1006M": {
|
||||
"6M": "しおかぜ6号"
|
||||
}
|
||||
},
|
||||
{
|
||||
"8M": {
|
||||
"1008M": "いしづち8号"
|
||||
}
|
||||
},
|
||||
{
|
||||
"1008M": {
|
||||
"8M": "しおかぜ8号"
|
||||
}
|
||||
},
|
||||
{
|
||||
"10M": {
|
||||
"1010M": "いしづち10号"
|
||||
}
|
||||
},
|
||||
{
|
||||
"1010M": {
|
||||
"10M": "しおかぜ10号"
|
||||
}
|
||||
},
|
||||
{
|
||||
"12M": {
|
||||
"1012M": "いしづち12号"
|
||||
}
|
||||
},
|
||||
{
|
||||
"1012M": {
|
||||
"12M": "しおかぜ12号"
|
||||
}
|
||||
},
|
||||
{
|
||||
"14M": {
|
||||
"1014M": "いしづち14号"
|
||||
}
|
||||
},
|
||||
{
|
||||
"1014M": {
|
||||
"14M": "しおかぜ14号"
|
||||
}
|
||||
},
|
||||
{
|
||||
"16M": {
|
||||
"1016M": "いしづち16号"
|
||||
}
|
||||
},
|
||||
{
|
||||
"1016M": {
|
||||
"16M": "しおかぜ16号"
|
||||
}
|
||||
},
|
||||
{
|
||||
"18M": {
|
||||
"1018M": "いしづち18号"
|
||||
}
|
||||
},
|
||||
{
|
||||
"1018M": {
|
||||
"18M": "しおかぜ18号"
|
||||
}
|
||||
},
|
||||
{
|
||||
"20M": {
|
||||
"1020M": "いしづち20号"
|
||||
}
|
||||
},
|
||||
{
|
||||
"1020M": {
|
||||
"20M": "しおかぜ20号"
|
||||
}
|
||||
},
|
||||
{
|
||||
"22M": {
|
||||
"1022M": "いしづち22号"
|
||||
}
|
||||
},
|
||||
{
|
||||
"1022M": {
|
||||
"22M": "しおかぜ22号"
|
||||
}
|
||||
},
|
||||
{
|
||||
"24M": {
|
||||
"1024M": "いしづち24号"
|
||||
}
|
||||
},
|
||||
{
|
||||
"1024M": {
|
||||
"24M": "しおかぜ24号"
|
||||
}
|
||||
},
|
||||
{
|
||||
"26M": {
|
||||
"1026M": "いしづち26号"
|
||||
}
|
||||
},
|
||||
{
|
||||
"1026M": {
|
||||
"26M": "しおかぜ26号"
|
||||
}
|
||||
},
|
||||
{
|
||||
"28M": {
|
||||
"1028M": "いしづち28号"
|
||||
}
|
||||
},
|
||||
{
|
||||
"1028M": {
|
||||
"28M": "しおかぜ28号"
|
||||
}
|
||||
},
|
||||
{
|
||||
"30M": {
|
||||
"1030M": "いしづち30号"
|
||||
}
|
||||
},
|
||||
{
|
||||
"1030M": {
|
||||
"30M": "しおかぜ30号"
|
||||
}
|
||||
}
|
||||
]
|
||||
2549
lib/mockApi/mockData/traintimeinfo.json
Normal file
94
lib/mockApi/trainRecorder.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { AS } from '../../storageControl';
|
||||
import { STORAGE_KEYS } from '../../constants/storage';
|
||||
import { TrainEntry } from './webviewXhrInterceptor';
|
||||
|
||||
export type TrainSnapshot = {
|
||||
/** ms elapsed from recording start */
|
||||
t: number;
|
||||
trains: TrainEntry[];
|
||||
};
|
||||
|
||||
/** 録画のメタ情報(一覧表示用・軽量) */
|
||||
export type RecordingMeta = {
|
||||
id: string;
|
||||
recordedAt: string;
|
||||
durationMs: number;
|
||||
snapshotCount: number;
|
||||
};
|
||||
|
||||
/** フル録画データ(再生時のみロード) */
|
||||
export type TrainRecording = {
|
||||
id: string;
|
||||
recordedAt: string;
|
||||
durationMs: number;
|
||||
snapshots: TrainSnapshot[];
|
||||
};
|
||||
|
||||
const parse = <T>(raw: string | null | boolean): T | null => {
|
||||
if (!raw) return null;
|
||||
try {
|
||||
return JSON.parse(typeof raw === 'string' ? raw : JSON.stringify(raw)) as T;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/** 録画IDを生成(recordedAt ISO文字列からファイル名に使えるIDへ) */
|
||||
export const generateRecordingId = (recordedAt: string) =>
|
||||
recordedAt.replace(/[:.]/g, '-');
|
||||
|
||||
/** 録画インデックス(メタ一覧)を読み込む */
|
||||
export const loadRecordingList = async (): Promise<RecordingMeta[]> => {
|
||||
const raw = await AS.getItem(STORAGE_KEYS.MOCK_RECORDINGS_INDEX).catch(() => null);
|
||||
return parse<RecordingMeta[]>(raw) ?? [];
|
||||
};
|
||||
|
||||
/** フル録画データをIDで読み込む */
|
||||
export const loadRecordingById = async (id: string): Promise<TrainRecording | null> => {
|
||||
const raw = await AS.getItem((STORAGE_KEYS.MOCK_RECORDING_DATA_PREFIX + id) as any).catch(() => null);
|
||||
return parse<TrainRecording>(raw);
|
||||
};
|
||||
|
||||
/** 録画を保存してインデックスに追加する */
|
||||
export const saveRecording = async (recording: TrainRecording): Promise<void> => {
|
||||
await AS.setItem(
|
||||
(STORAGE_KEYS.MOCK_RECORDING_DATA_PREFIX + recording.id) as any,
|
||||
JSON.stringify(recording),
|
||||
);
|
||||
const list = await loadRecordingList();
|
||||
const meta: RecordingMeta = {
|
||||
id: recording.id,
|
||||
recordedAt: recording.recordedAt,
|
||||
durationMs: recording.durationMs,
|
||||
snapshotCount: recording.snapshots.length,
|
||||
};
|
||||
// 先頭に追加(新しい順)、同IDは重複排除
|
||||
const newList = [meta, ...list.filter((m) => m.id !== recording.id)];
|
||||
await AS.setItem(STORAGE_KEYS.MOCK_RECORDINGS_INDEX, JSON.stringify(newList));
|
||||
};
|
||||
|
||||
/** 録画をIDで削除する */
|
||||
export const deleteRecordingById = async (id: string): Promise<void> => {
|
||||
await AS.removeItem((STORAGE_KEYS.MOCK_RECORDING_DATA_PREFIX + id) as any).catch(() => {});
|
||||
const list = await loadRecordingList();
|
||||
await AS.setItem(
|
||||
STORAGE_KEYS.MOCK_RECORDINGS_INDEX,
|
||||
JSON.stringify(list.filter((m) => m.id !== id)),
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 旧フォーマット(MOCK_RECORDING)からマイグレーション。
|
||||
* 新インデックスが空の場合のみ実行する。
|
||||
*/
|
||||
export const migrateOldRecording = async (): Promise<void> => {
|
||||
const list = await loadRecordingList();
|
||||
if (list.length > 0) return; // 既に移行済み
|
||||
const raw = await AS.getItem(STORAGE_KEYS.MOCK_RECORDING).catch(() => null);
|
||||
const old = parse<Omit<TrainRecording, 'id'>>(raw);
|
||||
if (!old) return;
|
||||
const id = generateRecordingId(old.recordedAt);
|
||||
await saveRecording({ ...old, id });
|
||||
await AS.removeItem(STORAGE_KEYS.MOCK_RECORDING).catch(() => {});
|
||||
};
|
||||
|
||||
218
lib/mockApi/webviewXhrInterceptor.ts
Normal file
@@ -0,0 +1,218 @@
|
||||
/**
|
||||
* WebView XHR Interceptor for JR Shikoku official train position site
|
||||
*
|
||||
* The official site (train.jr-shikoku.co.jp/sp.html) makes XMLHttpRequest calls
|
||||
* to its own /g?arg1=...&arg2=... API. This module generates a JavaScript string
|
||||
* that, when injected into the WebView before page load, overrides XMLHttpRequest
|
||||
* so those calls can be served with mock/alternative data.
|
||||
*
|
||||
* API endpoint reference:
|
||||
* Static (loaded once on startup):
|
||||
* GET /g?arg1=lang&arg2=lang - i18n strings
|
||||
* GET /g?arg1=station&arg2=traintimeinfo&arg3=dia - station timetable info
|
||||
* GET /g?arg1=train&arg2=trainpare - train pair info (Shinkansen links)
|
||||
* GET /g?arg1=station&arg2=between - between-station hidden stops
|
||||
* GET /g?arg1=station&arg2=othercolor - colour overrides for other lines
|
||||
* GET /g?arg1=train&arg2=holidaydisp - holiday train display rules
|
||||
* GET /g?arg1=train&arg2=sightseeingtrainname - sightseeing train names
|
||||
* GET /g?arg1=station&arg2=cross - cross-line station data
|
||||
* GET /g?arg1=station&arg2=otherline - other-line station data
|
||||
* GET /g?arg1=line&arg2=train_lang - line/train i18n names
|
||||
* GET /g?arg1=train&arg2=ignore - trains to hide from display
|
||||
* GET /g?arg1=station&arg2={line} - station list for a line
|
||||
* line values: yosan | koutoku | tokushima | dosan | dosan2 | uwajima | naruto
|
||||
*
|
||||
* Dynamic (polled ~every few seconds):
|
||||
* GET /g?arg1=train&arg2=train - live train positions ← main target
|
||||
*
|
||||
* Train position response schema:
|
||||
* Array of { Index, TrainNum, Pos, PosNum, delay, Direction, Type, Line }
|
||||
* with a trailing { GetDateTime: "YYYY/MM/DD HH:MM:SS" } element.
|
||||
*/
|
||||
|
||||
export interface TrainEntry {
|
||||
Index: number;
|
||||
TrainNum: string;
|
||||
/** 走行位置テキスト e.g. "高松〜鬼無(上り)" */
|
||||
Pos: string;
|
||||
PosNum: number;
|
||||
/** 遅延分数 or "入線" */
|
||||
delay: number | string;
|
||||
/** 0: 下り, 1: 上り */
|
||||
Direction: 0 | 1;
|
||||
/** "normal" | "rapid:..." | "express:..." | "ltd:..." */
|
||||
Type: string;
|
||||
/** "yosan" | "koutoku" | "tokushima" | "dosan" | "dosan2" | "uwajima" | "naruto" */
|
||||
Line: string;
|
||||
}
|
||||
|
||||
export interface MockApiConfig {
|
||||
/**
|
||||
* Mock train position data injected in place of the live /g?arg1=train&arg2=train API.
|
||||
* When provided, the WebView will use this data instead of polling the real server.
|
||||
*/
|
||||
trainPositions: TrainEntry[];
|
||||
|
||||
/**
|
||||
* When true, all /g? static API calls are also intercepted and served from
|
||||
* the supplied staticData map. When false (default), only the train position
|
||||
* polling API is intercepted; all other calls reach the real server.
|
||||
*/
|
||||
interceptStaticApis?: boolean;
|
||||
|
||||
/**
|
||||
* Optional map of query-string patterns to JSON response strings for the
|
||||
* static APIs. Only used when interceptStaticApis is true.
|
||||
* Keys are the full query string, e.g. "arg1=lang&arg2=lang".
|
||||
*/
|
||||
staticData?: Record<string, string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a JavaScript string that should be prepended to the WebView's
|
||||
* injectedJavaScript (runs before page scripts). It overrides XMLHttpRequest
|
||||
* so the specified /g?arg1=...&arg2=... calls are intercepted and served
|
||||
* from mock data.
|
||||
*/
|
||||
export const generateXhrInterceptorJs = (config: MockApiConfig): string => {
|
||||
const trainJson = JSON.stringify([
|
||||
...config.trainPositions,
|
||||
{ GetDateTime: new Date().toLocaleString("ja-JP", { timeZone: "Asia/Tokyo" }).replace(/\//g, "/") },
|
||||
]);
|
||||
|
||||
const staticEntries = config.interceptStaticApis && config.staticData
|
||||
? Object.entries(config.staticData)
|
||||
.map(([qs, data]) => `${JSON.stringify(qs)}: ${JSON.stringify(data)}`)
|
||||
.join(",\n ")
|
||||
: "";
|
||||
|
||||
return `
|
||||
(function() {
|
||||
// Double-injection guard: IJBCL and injectedJavaScript may both run this code.
|
||||
// The guard ensures the prototype is only patched once.
|
||||
if (window.__jrsMockActive) return;
|
||||
|
||||
'use strict';
|
||||
|
||||
// ── Mock data ──────────────────────────────────────────────────────────────
|
||||
var _MOCK_TRAIN = ${trainJson};
|
||||
var _INTERCEPT_STATIC = ${config.interceptStaticApis ? "true" : "false"};
|
||||
var _STATIC_MAP = {
|
||||
${staticEntries}
|
||||
};
|
||||
|
||||
// ── Prototype-patching approach ────────────────────────────────────────────
|
||||
// Instead of replacing window.XMLHttpRequest with a wrapper class,
|
||||
// we patch the prototype methods directly. This ensures:
|
||||
// • instanceof XMLHttpRequest still works
|
||||
// • All native properties (responseType, withCredentials, etc.) work
|
||||
// • 'this' context in callbacks is always the real XHR instance
|
||||
var _proto = window.XMLHttpRequest.prototype;
|
||||
var _origOpen = _proto.open;
|
||||
var _origSend = _proto.send;
|
||||
|
||||
_proto.open = function(method, url) {
|
||||
var qs = (url || '').replace(/^[^?]+\\?/, '');
|
||||
if (qs === 'arg1=train&arg2=train') {
|
||||
this.__jrsMockBody = JSON.stringify(_MOCK_TRAIN);
|
||||
// Still call origOpen so the XHR enters OPENED state.
|
||||
// This allows the page to call setRequestHeader() without errors.
|
||||
// The real request is suppressed in send() by not calling _origSend.
|
||||
_origOpen.apply(this, arguments);
|
||||
return;
|
||||
}
|
||||
if (_INTERCEPT_STATIC && _STATIC_MAP[qs] !== undefined) {
|
||||
this.__jrsMockBody = _STATIC_MAP[qs];
|
||||
_origOpen.apply(this, arguments);
|
||||
return;
|
||||
}
|
||||
this.__jrsMockBody = null;
|
||||
_origOpen.apply(this, arguments);
|
||||
};
|
||||
|
||||
_proto.send = function(body) {
|
||||
var self = this;
|
||||
if (self.__jrsMockBody != null) {
|
||||
var mockStr = self.__jrsMockBody;
|
||||
// Support responseType='json': return parsed object from .response
|
||||
var parsed = null;
|
||||
try { parsed = JSON.parse(mockStr); } catch(e) {}
|
||||
|
||||
// Override instance properties to serve mock data
|
||||
// (configurable:true allows re-override if needed)
|
||||
Object.defineProperties(self, {
|
||||
readyState: { get: function() { return 4; }, configurable: true, enumerable: true },
|
||||
status: { get: function() { return 200; }, configurable: true, enumerable: true },
|
||||
statusText: { get: function() { return 'OK'; }, configurable: true, enumerable: true },
|
||||
responseText: { get: function() { return mockStr; }, configurable: true, enumerable: true },
|
||||
response: {
|
||||
get: function() {
|
||||
var rt = self.responseType;
|
||||
if (rt === 'json') return parsed;
|
||||
if (rt === 'arraybuffer' || rt === 'blob') return mockStr; // best-effort
|
||||
return mockStr;
|
||||
},
|
||||
configurable: true, enumerable: true
|
||||
},
|
||||
});
|
||||
|
||||
setTimeout(function() {
|
||||
// Fire events via dispatchEvent only.
|
||||
// dispatchEvent fires both addEventListener callbacks AND onXxx property handlers,
|
||||
// so calling onreadystatechange.call() / onload.call() separately would double-fire.
|
||||
if (typeof self.dispatchEvent === 'function') {
|
||||
try { self.dispatchEvent(new Event('readystatechange')); } catch(e) {}
|
||||
try { self.dispatchEvent(new ProgressEvent('load')); } catch(e) {
|
||||
try { self.dispatchEvent(new Event('load')); } catch(e2) {}
|
||||
}
|
||||
try { self.dispatchEvent(new ProgressEvent('loadend')); } catch(e) {
|
||||
try { self.dispatchEvent(new Event('loadend')); } catch(e2) {}
|
||||
}
|
||||
} else {
|
||||
// Fallback for environments without dispatchEvent
|
||||
if (typeof self.onreadystatechange === 'function') {
|
||||
try { self.onreadystatechange.call(self); } catch(e) {}
|
||||
}
|
||||
if (typeof self.onload === 'function') {
|
||||
try { self.onload.call(self); } catch(e) {}
|
||||
}
|
||||
}
|
||||
}, 0);
|
||||
return;
|
||||
}
|
||||
_origSend.apply(this, arguments);
|
||||
};
|
||||
|
||||
// ── Live-update hook ───────────────────────────────────────────────────────
|
||||
// Called from React Native via injectJavaScript when the playback frame changes.
|
||||
window.__jrsMockUpdateTrain = function(newData) {
|
||||
_MOCK_TRAIN = newData;
|
||||
};
|
||||
|
||||
window.__jrsMockActive = true;
|
||||
console.log('[JRS Mock] XHR interceptor active (prototype-patch) – train position data is mocked');
|
||||
})();
|
||||
`;
|
||||
};
|
||||
|
||||
/**
|
||||
* 再生コマが変化したときに WebView へ injectJavaScript で流し込む更新スクリプトを生成する。
|
||||
* window.__jrsMockUpdateTrain で _MOCK_TRAIN を差し替え、window.setReload() で再描画を促す。
|
||||
*/
|
||||
export const generateMockUpdateScript = (trainPositions: TrainEntry[]): string => {
|
||||
const trainJson = JSON.stringify([
|
||||
...trainPositions,
|
||||
{ GetDateTime: new Date().toLocaleString("ja-JP", { timeZone: "Asia/Tokyo" }).replace(/\//g, "/") },
|
||||
]);
|
||||
return `
|
||||
(function() {
|
||||
if (typeof window.__jrsMockUpdateTrain === 'function') {
|
||||
window.__jrsMockUpdateTrain(${trainJson});
|
||||
}
|
||||
if (typeof window.setReload === 'function') {
|
||||
window.setReload();
|
||||
}
|
||||
})();
|
||||
true;
|
||||
`;
|
||||
};
|
||||
@@ -1,3 +1,47 @@
|
||||
import { createNavigationContainerRef } from "@react-navigation/native";
|
||||
import { createNavigationContainerRef, StackActions } from "@react-navigation/native";
|
||||
|
||||
export const rootNavigationRef = createNavigationContainerRef<any>();
|
||||
|
||||
/** 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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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<ReturnType<typeof setTimeout> | null>(null);
|
||||
const hideTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const retryTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const keyboardVisibleRef = useRef(false);
|
||||
const lastValidKbRef = useRef<{
|
||||
height: number;
|
||||
screenY: number;
|
||||
} | null>(null);
|
||||
// 実行中アニメーションの参照(高速切替時に前アニメをキャンセルするため)
|
||||
const currentAnimRef = useRef<Animated.CompositeAnimation | null>(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 };
|
||||
}
|
||||
|
||||
128
lib/useWebViewRemount.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { AppState, AppStateStatus } from "react-native";
|
||||
import WebView from "react-native-webview";
|
||||
|
||||
/**
|
||||
* WebView のメモリ解放・プロセス終了による白画面を自動復帰させるフック。
|
||||
*
|
||||
* 使い方:
|
||||
* const { remountKey, remount, processHandlers, pingHandlers, webViewRef } = useWebViewRemount();
|
||||
* <WebView key={remountKey} ref={webViewRef} {...processHandlers} {...pingHandlers} ... />
|
||||
*
|
||||
* 既存の ref がある場合は webViewRef を使わず processHandlers / pingHandlers だけ使ってもよい。
|
||||
* pingHandlers を使う場合は onMessage を上書きせず spread すること。
|
||||
* ping による白画面検知を有効にするには pingEnabled: true を渡す。
|
||||
*/
|
||||
export function useWebViewRemount(options?: { pingEnabled?: boolean; backgroundThresholdMs?: number }) {
|
||||
const pingEnabled = options?.pingEnabled ?? false;
|
||||
const backgroundThresholdMs = options?.backgroundThresholdMs ?? 300_000; // デフォルト5分
|
||||
const [remountKey, setRemountKey] = useState(0);
|
||||
const backgroundedAt = useRef<number | null>(null);
|
||||
const webViewRef = useRef<WebView>(null);
|
||||
|
||||
// ping watchdog 用
|
||||
const lastPongAt = useRef(Date.now());
|
||||
const isLoadingRef = useRef(true);
|
||||
|
||||
const remount = useCallback(() => {
|
||||
lastPongAt.current = Date.now();
|
||||
isLoadingRef.current = true;
|
||||
setRemountKey((k) => k + 1);
|
||||
}, []);
|
||||
|
||||
// バックグラウンドから復帰したら再マウント(デフォルト5分超)
|
||||
useEffect(() => {
|
||||
const onAppStateChange = (nextState: AppStateStatus) => {
|
||||
if (nextState.match(/inactive|background/)) {
|
||||
backgroundedAt.current = Date.now();
|
||||
} else if (nextState === "active" && backgroundedAt.current !== null) {
|
||||
const elapsed = Date.now() - backgroundedAt.current;
|
||||
backgroundedAt.current = null;
|
||||
if (elapsed > backgroundThresholdMs) {
|
||||
remount();
|
||||
}
|
||||
}
|
||||
};
|
||||
const subscription = AppState.addEventListener("change", onAppStateChange);
|
||||
return () => subscription.remove();
|
||||
}, [remount, backgroundThresholdMs]);
|
||||
|
||||
// ping watchdog: 5秒ごとに生存確認と白画面検知を行う
|
||||
// - ローディング中(isLoadingRef=true)でも45秒超なら remount(レンダラー死亡でonLoadEndが来ないケース)
|
||||
// - ロード完了後は30秒 pong 無応答で remount
|
||||
const maxTextLenRef = useRef(0);
|
||||
const blankCountRef = useRef(0);
|
||||
useEffect(() => {
|
||||
if (!pingEnabled) return;
|
||||
const id = setInterval(() => {
|
||||
const elapsed = Date.now() - lastPongAt.current;
|
||||
if (isLoadingRef.current) {
|
||||
// ローディング中でも45秒超はレンダラー死亡と判定
|
||||
if (elapsed > 45_000) remount();
|
||||
return;
|
||||
}
|
||||
// ロード完了後30秒 pong 無応答 → レンダラー死亡
|
||||
if (elapsed > 30_000) {
|
||||
remount();
|
||||
return;
|
||||
}
|
||||
webViewRef.current?.injectJavaScript(
|
||||
`(function(){var t=document.body?(document.body.innerText||'').replace(/\\s+/g,'').length:0;window.ReactNativeWebView&&window.ReactNativeWebView.postMessage(JSON.stringify({type:'__ping',len:t}));})();true;`
|
||||
);
|
||||
}, 5_000);
|
||||
return () => clearInterval(id);
|
||||
}, [pingEnabled, remount]);
|
||||
|
||||
const processHandlers = {
|
||||
onRenderProcessGone: () => remount(),
|
||||
onContentProcessDidTerminate: () => remount(),
|
||||
} as const;
|
||||
|
||||
// pingHandlers は onLoadEnd と onMessage を提供する
|
||||
// 既存の onMessage がある場合は手動でマージすること
|
||||
const pingHandlers = pingEnabled ? {
|
||||
onLoadEnd: () => {
|
||||
isLoadingRef.current = false;
|
||||
lastPongAt.current = Date.now();
|
||||
maxTextLenRef.current = 0;
|
||||
blankCountRef.current = 0;
|
||||
// ロード完了3秒後に初回コンテンツチェック
|
||||
setTimeout(() => {
|
||||
if (!isLoadingRef.current) {
|
||||
webViewRef.current?.injectJavaScript(
|
||||
`(function(){var t=document.body?(document.body.innerText||'').replace(/\\s+/g,'').length:0;window.ReactNativeWebView&&window.ReactNativeWebView.postMessage(JSON.stringify({type:'__ping',len:t}));})();true;`
|
||||
);
|
||||
}
|
||||
}, 3000);
|
||||
},
|
||||
onLoadStart: () => {
|
||||
isLoadingRef.current = true;
|
||||
lastPongAt.current = Date.now(); // ナビゲーション開始時にタイムアウトリセット
|
||||
maxTextLenRef.current = 0;
|
||||
blankCountRef.current = 0;
|
||||
},
|
||||
onMessage: (event: any) => {
|
||||
try {
|
||||
const parsed = JSON.parse(event.nativeEvent.data);
|
||||
if (parsed.type === "__ping") {
|
||||
lastPongAt.current = Date.now(); // 応答ごとにタイムアウトリセット
|
||||
const len: number = parsed.len ?? 0;
|
||||
if (len > maxTextLenRef.current) maxTextLenRef.current = len;
|
||||
// 一度でも20文字超になったページが5文字未満になったら白画面と判定
|
||||
if (maxTextLenRef.current > 20 && len < 5) {
|
||||
blankCountRef.current += 1;
|
||||
if (blankCountRef.current >= 3) {
|
||||
blankCountRef.current = 0;
|
||||
maxTextLenRef.current = 0;
|
||||
remount();
|
||||
}
|
||||
} else {
|
||||
blankCountRef.current = 0;
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
},
|
||||
} : {};
|
||||
|
||||
return { remountKey, remount, processHandlers, pingHandlers, webViewRef };
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { INTERVALS, API_ENDPOINTS } from '@/constants';
|
||||
import { TRAIN_TYPE_CONFIG } from './webview/trainTypeConfig';
|
||||
import { TRAIN_ICON_MAP, TRAIN_ICON_REGEX } from './webview/trainIconMap';
|
||||
import { STATION_DATA } from './webview/stationData';
|
||||
import { generateXhrInterceptorJs, MockApiConfig } from './mockApi/webviewXhrInterceptor';
|
||||
|
||||
export interface InjectJavascriptOptions {
|
||||
/** 地図スイッチ ("true" | "false") */
|
||||
@@ -20,8 +21,21 @@ export interface InjectJavascriptOptions {
|
||||
useElesite: string;
|
||||
/** ダークモードかどうか */
|
||||
isDark: boolean;
|
||||
/**
|
||||
* モックAPIの設定。指定した場合、WebView内の公式サイトAPIをインターセプトして
|
||||
* モックデータを返すXHRインターセプターが注入されます。
|
||||
* 未指定または null の場合、インターセプターは無効で通常通り公式APIに接続します。
|
||||
*/
|
||||
mockApiConfig?: MockApiConfig | null;
|
||||
/**
|
||||
* バックエンドAPIのベースURL。省略時は本番URL。
|
||||
* experimental 環境では https://jr-shikoku-backend-api-v1-beta.haruk.in を渡す。
|
||||
*/
|
||||
backendApiBaseUrl?: string;
|
||||
}
|
||||
|
||||
const PRODUCTION_BACKEND_BASE = 'https://jr-shikoku-backend-api-v1.haruk.in';
|
||||
|
||||
export const injectJavascriptData = ({
|
||||
mapSwitch,
|
||||
iconSetting,
|
||||
@@ -31,7 +45,10 @@ export const injectJavascriptData = ({
|
||||
useUnyohub,
|
||||
useElesite,
|
||||
isDark,
|
||||
backendApiBaseUrl,
|
||||
mockApiConfig,
|
||||
}: InjectJavascriptOptions): string => {
|
||||
|
||||
// 一番上のメニュー非表示 地図スイッチによって切り替え
|
||||
const topMenu =
|
||||
mapSwitch != "true"
|
||||
@@ -107,8 +124,8 @@ export const injectJavascriptData = ({
|
||||
+ '#header{background-color:#454b54!important;color:#e6edf3!important;border-bottom:1px solid #555d66!important;}'
|
||||
+ '#headerStr{color:#e6edf3!important;}'
|
||||
+ '#topHeader{background-color:#454b54!important;border-bottom:1px solid #555d66!important;}'
|
||||
+ '#topHeader div{color:#e6edf3!important;}'
|
||||
+ '#topHeader .accordion{background-color:#4d545d!important;color:#e6edf3!important;}'
|
||||
+ '#topHeader div{color:#000000!important;}'
|
||||
+ '#topHeader .accordion{background-color:#4d545d!important;color:#000000!important;}'
|
||||
+ '${uiSetting === "tokyo" ? '[id^="stationBlock"]{background-color:rgba(0,0,0,0.45)!important;}#disp>[id*="~"]{background-color:rgba(0,0,0,0.25)!important;}' : ''}'
|
||||
+ '#pMENU_2,#pMENU_3,#pMENU_k{background-color:#454b54!important;border-color:#555d66!important;}'
|
||||
+ '#pMENU_2 div,#pMENU_3 div,#pMENU_k div{color:#e6edf3!important;}'
|
||||
@@ -117,7 +134,10 @@ export const injectJavascriptData = ({
|
||||
+ '.accordionClass{background-color:#454b54!important;color:#e6edf3!important;}'
|
||||
+ 'select{background-color:#4d545d!important;color:#e6edf3!important;border-color:#555d66!important;}'
|
||||
+ '#upTrainCrossBar,#dwTrainCrossBar{opacity:0.85!important;}'
|
||||
) : '');
|
||||
) : (
|
||||
'#topHeader div{color:#000000!important;}'
|
||||
+ '#topHeader .accordion{color:#000000!important;}'
|
||||
));
|
||||
document.head.appendChild(s);
|
||||
})();
|
||||
|
||||
@@ -186,12 +206,18 @@ export const injectJavascriptData = ({
|
||||
}
|
||||
};
|
||||
|
||||
const setReload = () => {
|
||||
window.setReload = () => {
|
||||
try {
|
||||
document.getElementById('refreshIcon').click();
|
||||
setStrings();
|
||||
// Also run after async XHR responses settle.
|
||||
// When mock mode is active, the XHR interceptor responds via setTimeout(0),
|
||||
// which means the site re-renders trains AFTER this synchronous call.
|
||||
// The delayed call ensures Tokyo UX is re-applied after the DOM update.
|
||||
setTimeout(() => { try { setStrings(); } catch(e) {} }, 200);
|
||||
} catch(e) {}
|
||||
};
|
||||
const setReload = window.setReload;
|
||||
|
||||
// ポーリング処理 (Phase 3 以降)
|
||||
const startPolling = () => {
|
||||
@@ -859,7 +885,7 @@ export const injectJavascriptData = ({
|
||||
if(hasElesite) {
|
||||
const elesiteOffsetPx = _blueOffset + (hasUnyohub ? 20 : 0);
|
||||
const offsetStyle = badgeVerticalPos + ":" + elesiteOffsetPx + "px;";
|
||||
badgeHtml += "<div style='position:absolute;" + badgePosition + ":0;" + offsetStyle + "background-color:#44bb44;border-radius:50%;border:1px solid #228822;width:19px;height:19px;box-sizing:border-box;display:flex;align-items:center;justify-content:center;'><span style='color:white;font-size:10px;font-weight:bold;line-height:1;'>E</span></div>";
|
||||
badgeHtml += "<div style='position:absolute;" + badgePosition + ":0;" + offsetStyle + "background-color:#44bb44;border-radius:50%;border:1px solid #228822;width:19px;height:19px;box-sizing:border-box;overflow:hidden;display:flex;align-items:center;justify-content:center;'><img src='https://storage.haruk.in/elesite_logo.jpg' style='width:19px;height:19px;'/></div>";
|
||||
}
|
||||
|
||||
行き先情報.insertAdjacentHTML('beforebegin', "<div style='width:100%;display:flex;flex:1;flex-direction:"+(isLeft ? "column-reverse" : "column") + ";font-weight:bold;'>" + badgeHtml + "<p style='font-size:6px;padding:0;color:black;text-align:center;'>" + (TrainNumberOverride ? TrainNumberOverride : TrainNumber) + "</p><div style='flex:1;'></div><p style='font-size:8px;font-weight:bold;padding:0;color:black;text-align:center;'>" + (isWanman ? "ワンマン " : "") + "</p><p style='font-size:6px;font-weight:bold;padding:0;color:black;text-align:center;border-style:solid;border-width: "+(!!yosan2Color ? "2px" : "0px")+";border-color:" + yosan2Color + "'>" + viaData + "</p><p style='font-size:8px;font-weight:bold;padding:0;color: " + optionalTextColor + ";text-align:center;'>" + optionalText + "</p><p style='font-size:8px;font-weight:bold;padding:0;color:black;text-align:center;'>" + trainName + "</p><div style='width:100%;background:" + gradient + ";'><p style='font-size:10px;font-weight:bold;padding:0;margin:0;color:white;align-items:center;align-content:center;text-align:center;text-shadow:1px 1px 0px #00000030, -1px -1px 0px #00000030,-1px 1px 0px #00000030, 1px -1px 0px #00000030,1px 0px 0px #00000030, -1px 0px 0px #00000030,0px 1px 0px #00000030, 0px -1px 0px #00000030;'>" + (ToData ? ToData + "行" : ToData) + "</p></div><div style='width:100%;background:" + trainTypeColor + ";border-radius:"+(isLeft ? "4px 4px 0 0" : "0 0 4px 4px")+";'><p style='font-size:10px;font-weight:bold;font-style:italic;padding:0;color: white;text-align:center;'>" + trainType + "</p></div><p style='font-size:8px;font-weight:bold;padding:0;text-align:center;color: "+(hasProblem ? "red": "black")+"; "+(hasProblem ? "animation:_jrs_blink 3s linear infinite;" : "")+"'>" + (hasProblem ? "‼️停止中‼️" : "") + "</p></div>");
|
||||
@@ -1227,20 +1253,26 @@ const setStrings = () =>{
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
let _ssDebounce = null;
|
||||
const textInsert = new MutationObserver( (mutations) =>{
|
||||
setStrings();
|
||||
const currentLines = document.querySelector('#topHeader div').innerText;
|
||||
window.ReactNativeWebView.postMessage(JSON.stringify({type:"currentLines",currentLines}));
|
||||
// Only send currentLines when direct children of #disp change (line selection change)
|
||||
const disp = document.getElementById('disp');
|
||||
if (mutations.some(m => m.target === disp)) {
|
||||
const currentLines = document.querySelector('#topHeader div').innerText;
|
||||
window.ReactNativeWebView.postMessage(JSON.stringify({type:"currentLines",currentLines}));
|
||||
}
|
||||
// Apply Tokyo UX immediately (prevents visible flash on normal renders).
|
||||
try { setStrings(); } catch(e) {}
|
||||
// Also schedule a follow-up after 250ms to catch async re-renders
|
||||
// triggered by mock XHR setTimeout(0) responses that settle later.
|
||||
clearTimeout(_ssDebounce);
|
||||
_ssDebounce = setTimeout(() => { try { setStrings(); } catch(e) {} }, 250);
|
||||
});
|
||||
|
||||
// 監視を開始
|
||||
// 監視を開始 (subtree: true で深い子孫の変化も捕捉する)
|
||||
textInsert.observe(document.getElementById('disp'), {
|
||||
attributes: true, // 属性変化の監視
|
||||
//attributeOldValue: true, // 変化前の属性値を matation.oldValue に格納する
|
||||
//characterData: true, // テキストノードの変化を監視
|
||||
//characterDataOldValue: true, // 変化前のテキストを matation.oldValue に格納する
|
||||
childList: true, // 子ノードの変化を監視
|
||||
//subtree: true // 子孫ノードも監視対象に含める
|
||||
subtree: true, // 子孫ノードも監視対象に含める (train elements are 3 levels deep)
|
||||
});
|
||||
`;
|
||||
|
||||
@@ -1390,7 +1422,7 @@ setStationMenuDialog.observe(document.querySelector('#disp'), {
|
||||
});
|
||||
`
|
||||
: ``;
|
||||
return (
|
||||
let result =
|
||||
bootData +
|
||||
topMenu +
|
||||
trainIcon +
|
||||
@@ -1398,6 +1430,28 @@ setStationMenuDialog.observe(document.querySelector('#disp'), {
|
||||
makeTrainView +
|
||||
makeTrainMenu +
|
||||
textInsert +
|
||||
makeStationMenu
|
||||
);
|
||||
makeStationMenu;
|
||||
|
||||
if (backendApiBaseUrl && backendApiBaseUrl !== PRODUCTION_BACKEND_BASE) {
|
||||
result = result.split(PRODUCTION_BACKEND_BASE).join(backendApiBaseUrl);
|
||||
}
|
||||
|
||||
// Prepend XHR interceptor as a fallback for platforms where
|
||||
// injectedJavaScriptBeforeContentLoaded doesn't run reliably.
|
||||
// The __jrsMockActive guard in the interceptor prevents double-patching
|
||||
// when IJBCL already installed it.
|
||||
if (mockApiConfig) {
|
||||
result = generateXhrInterceptorJs(mockApiConfig) + '\n' + result;
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* XHRインターセプターJSを生成する(injectedJavaScriptBeforeContentLoaded用)
|
||||
* ページスクリプトより前に実行する必要があるため、別プロップで渡す。
|
||||
*/
|
||||
export const generateBeforeContentLoadedScript = (mockApiConfig: MockApiConfig | null): string => {
|
||||
if (!mockApiConfig) return '';
|
||||
return generateXhrInterceptorJs(mockApiConfig);
|
||||
};
|
||||
|
||||
26
menu.tsx
@@ -1,12 +1,11 @@
|
||||
import React, { useRef, useState, useEffect, useCallback, useMemo, FC } from "react";
|
||||
import { Platform, View, ScrollView, LayoutAnimation, Text, InteractionManager, useWindowDimensions } from "react-native";
|
||||
import { Platform, View, ScrollView, LayoutAnimation, Text, InteractionManager, useWindowDimensions, Image, StatusBar } from "react-native";
|
||||
import Constants from "expo-constants";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import {
|
||||
configureReanimatedLogger,
|
||||
ReanimatedLogLevel,
|
||||
} from "react-native-reanimated";
|
||||
import StatusbarDetect from "@/StatusbarDetect";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
import { useResponsive } from "@/lib/responsive";
|
||||
|
||||
@@ -16,7 +15,7 @@ import { FixedContentBottom } from "@/components/Menu/FixedContentBottom";
|
||||
|
||||
import { lineList, stationIDPair } from "@/lib/getStationList";
|
||||
import { useFavoriteStation } from "@/stateBox/useFavoriteStation";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import { useNavigation, useIsFocused } from "@react-navigation/native";
|
||||
import { useStationList } from "@/stateBox/useStationList";
|
||||
import { TopMenuButton } from "@/components/Menu/TopMenuButton";
|
||||
import { JRSTraInfoBox } from "@/components/Menu/JRSTraInfoBox";
|
||||
@@ -43,10 +42,13 @@ type props = {
|
||||
setMapMode: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
};
|
||||
|
||||
const MAP_PIN_SIZE = Platform.OS === "android" ? 32 : 36;
|
||||
|
||||
export const Menu: FC<props> = (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();
|
||||
@@ -287,7 +289,12 @@ export const Menu: FC<props> = (props) => {
|
||||
paddingTop: Platform.OS === "web" ? 0 : insets.top,
|
||||
}}
|
||||
>
|
||||
<StatusbarDetect />
|
||||
{isMenuFocused && (
|
||||
<StatusBar
|
||||
barStyle={isDark ? "light-content" : "dark-content"}
|
||||
translucent
|
||||
/>
|
||||
)}
|
||||
{!mapMode ? <TitleBar /> : <></>}
|
||||
<ScrollView
|
||||
ref={scrollRef}
|
||||
@@ -345,7 +352,8 @@ export const Menu: FC<props> = (props) => {
|
||||
latitude: lat,
|
||||
longitude: lng,
|
||||
}}
|
||||
image={require("@/assets/reccha-small.png")}
|
||||
anchor={{ x: 0.5, y: 1 }}
|
||||
tracksViewChanges={false}
|
||||
onPress={() => {
|
||||
setMapMode(false);
|
||||
setListIndex(index);
|
||||
@@ -373,7 +381,13 @@ export const Menu: FC<props> = (props) => {
|
||||
});
|
||||
returnToTop();
|
||||
}}
|
||||
/>
|
||||
>
|
||||
<Image
|
||||
source={require("@/assets/reccha-small.png")}
|
||||
style={{ width: MAP_PIN_SIZE, height: MAP_PIN_SIZE }}
|
||||
resizeMode="contain"
|
||||
/>
|
||||
</Marker>
|
||||
))}
|
||||
</MapView>
|
||||
{!mapMode && (
|
||||
|
||||
@@ -242,9 +242,12 @@ if (ExpoLiveActivityModule) {
|
||||
* このデバイスで Live Activity が使用可能かを返す。
|
||||
* iOS 16.2+ の実機かつユーザーが許可している場合のみ true。
|
||||
* Android では常に true。
|
||||
*
|
||||
* NOTE: 一時的に無効化中 — 常に false を返す
|
||||
*/
|
||||
export function isAvailable(): boolean {
|
||||
return ExpoLiveActivityModule?.isAvailable() ?? false;
|
||||
return false;
|
||||
// return ExpoLiveActivityModule?.isAvailable() ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
46
ndView.tsx
@@ -1,4 +1,4 @@
|
||||
import React, { Ref, useRef, useState, useEffect } from "react";
|
||||
import React, { Ref, useRef, useState, useEffect, useCallback } from "react";
|
||||
import {
|
||||
View,
|
||||
Platform,
|
||||
@@ -6,18 +6,44 @@ import {
|
||||
StyleProp,
|
||||
ViewStyle,
|
||||
Linking,
|
||||
AppState,
|
||||
} from "react-native";
|
||||
import { WebView } from "react-native-webview";
|
||||
import Constants from "expo-constants";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import { useNavigation, useFocusEffect } from "@react-navigation/native";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { useWebViewRemount } from "@/lib/useWebViewRemount";
|
||||
export default function tndView() {
|
||||
const webview = useRef<WebView>(null);
|
||||
const { remountKey, remount, processHandlers, pingHandlers, webViewRef } = useWebViewRemount({ pingEnabled: true, backgroundThresholdMs: 60_000 }); // 1分超でAppState remount
|
||||
const { navigate, addListener, isFocused } = useNavigation();
|
||||
const { fixed } = useThemeColors();
|
||||
const { top } = useSafeAreaInsets();
|
||||
|
||||
// タブにフォーカスが来たとき、バックグラウンドから戻ってきた場合 remount
|
||||
const bgAtRef = useRef<number | null>(null);
|
||||
useEffect(() => {
|
||||
const sub = AppState.addEventListener("change", (state) => {
|
||||
if (state.match(/inactive|background/)) {
|
||||
bgAtRef.current = Date.now();
|
||||
} else if (state === "active") {
|
||||
// useWebViewRemount側(1分超)とは独立: ここではフォーカス時判定用に保持するだけ
|
||||
}
|
||||
});
|
||||
return () => sub.remove();
|
||||
}, []);
|
||||
|
||||
// タブにフォーカスが来たとき、3秒以上バックグラウンドだった場合 remount
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
if (bgAtRef.current !== null) {
|
||||
const elapsed = Date.now() - bgAtRef.current;
|
||||
bgAtRef.current = null;
|
||||
if (elapsed > 3_000) remount();
|
||||
}
|
||||
}, [remount])
|
||||
);
|
||||
const jsa = `
|
||||
document.querySelector('.sitettl').style.display = 'none';
|
||||
document.querySelector('.attention').style.display = 'none';
|
||||
@@ -137,7 +163,8 @@ setInterval(() => {
|
||||
}}
|
||||
>
|
||||
<WebView
|
||||
ref={webview}
|
||||
key={remountKey}
|
||||
ref={webViewRef}
|
||||
source={{ uri: "https://www.jr-shikoku.co.jp/info/" }}
|
||||
originWhitelist={["https://www.jr-shikoku.co.jp"]}
|
||||
mixedContentMode={"compatibility"}
|
||||
@@ -145,13 +172,16 @@ setInterval(() => {
|
||||
javaScriptEnabled={true}
|
||||
injectedJavaScript={jsa}
|
||||
pullToRefreshEnabled
|
||||
onError={() => webview.current?.reload()}
|
||||
onMessage={() => {
|
||||
// 必要に応じてメッセージ処理を実装
|
||||
onError={() => remount()}
|
||||
{...processHandlers}
|
||||
{...pingHandlers}
|
||||
onMessage={(event) => {
|
||||
// pingHandlers の __ping メッセージを処理
|
||||
pingHandlers.onMessage?.(event);
|
||||
}}
|
||||
/>
|
||||
<ReloadButton
|
||||
onPress={() => webview.current.reload()}
|
||||
onPress={() => webViewRef.current?.reload()}
|
||||
top={top}
|
||||
/>
|
||||
</View>
|
||||
|
||||
@@ -10,6 +10,10 @@ import React, {
|
||||
useState,
|
||||
} from "react";
|
||||
import { API_ENDPOINTS, STORAGE_KEYS } from "@/constants";
|
||||
import {
|
||||
getBackendApiBaseUrl,
|
||||
BACKEND_API_BASE_URLS,
|
||||
} from "@/lib/jrDataSystemEnvironment";
|
||||
const initialState = {
|
||||
allTrainDiagram: {},
|
||||
setAllTrainDiagram: (e) => {},
|
||||
@@ -46,6 +50,16 @@ export const AllTrainDiagramProvider: FC<Props> = ({ children }) => {
|
||||
setKeyList(Object.keys(allTrainDiagram));
|
||||
else setKeyList([]);
|
||||
}, [allTrainDiagram]);
|
||||
const [backendApiBaseUrl, setBackendApiBaseUrl] = React.useState(
|
||||
BACKEND_API_BASE_URLS.production,
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
AS.getItem(STORAGE_KEYS.JR_DATA_SYSTEM_ENV)
|
||||
.then((value) => setBackendApiBaseUrl(getBackendApiBaseUrl(value)))
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
const getTrainDiagram = () => {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 15000);
|
||||
@@ -92,7 +106,7 @@ export const AllTrainDiagramProvider: FC<Props> = ({ children }) => {
|
||||
const getCustomTrainData = () => {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 15000);
|
||||
fetch("https://jr-shikoku-backend-api-v1.haruk.in/train-data", { signal: controller.signal })
|
||||
fetch(`${backendApiBaseUrl}/train-data`, { signal: controller.signal })
|
||||
.then((res) => res.json())
|
||||
.then((res) => {
|
||||
setAllCustomTrainData(res.data);
|
||||
@@ -111,7 +125,7 @@ export const AllTrainDiagramProvider: FC<Props> = ({ children }) => {
|
||||
const getTodayOperation = () => {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 15000);
|
||||
fetch(API_ENDPOINTS.OPERATION_LOGS, { signal: controller.signal })
|
||||
fetch(`${backendApiBaseUrl}/operation-logs`, { signal: controller.signal })
|
||||
.then((res) => res.json())
|
||||
.then((res) => {
|
||||
if (res.data === null) {
|
||||
|
||||
@@ -100,6 +100,12 @@ export const BusAndTrainDataProvider: FC<Props> = ({ children }) => {
|
||||
case "139M":
|
||||
returnArray.push("143M");
|
||||
break;
|
||||
case "4126M":
|
||||
returnArray.push("5126M");
|
||||
break;
|
||||
case "5126M":
|
||||
returnArray.push("4126M");
|
||||
break;
|
||||
// 土讃線琴平界隈
|
||||
case "1263M":
|
||||
returnArray.push("4263M");
|
||||
|
||||