Compare commits
255 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 907ddf9dae | |||
| 929c444474 | |||
| b6800d6566 | |||
| b0a0d62344 | |||
| bfd90b50e4 | |||
| 553735ac1d | |||
| 5f1641ff90 | |||
| 351854f128 | |||
| e6f0391484 | |||
| b1cfad4528 | |||
| 76b13a70a9 | |||
| eec2fd2bb7 | |||
| 8c2930c814 | |||
| c7fdf6f0b1 | |||
| 9746a4d041 | |||
| 740c825122 | |||
| f6b25ce5d3 | |||
| 390ab3ac69 | |||
| 2eb926890c | |||
| bf2e68a3a5 | |||
| dd4892f6b7 | |||
| 61548ea3f9 | |||
| bf0c4d90d9 | |||
| 47d6f043b4 | |||
| aacae06743 | |||
| dab60c47fa | |||
| 39fe154b33 | |||
| a8582c9fd6 | |||
| 3133535606 | |||
| a5e1b7d297 | |||
| 021a9b0101 | |||
| 6cea1ed4e8 | |||
| 17f4db58bc | |||
| 9740a7639b | |||
| 191a536f5c | |||
| 07514bf92d | |||
| 997d910d1c | |||
| 9ea9d6764b | |||
| cf3995fcbe | |||
| 983c453322 | |||
| c69585b257 | |||
| c6d4fca881 | |||
| 9c67190509 | |||
| 956b660b4f | |||
| f056fe593a | |||
| ca8d3225d1 | |||
| 0ebc6bc153 | |||
| fbd342d640 | |||
| b6e6d2e809 | |||
| 9eff185351 | |||
| 10e9aece2b | |||
| a1b9144851 | |||
| 16a62aa3ba | |||
| f8395d7270 | |||
| 5a0fb9f43c | |||
| 9d188198a9 | |||
| cb1d2665d8 | |||
| 3bbdd9d0a6 | |||
| 159f91e55e | |||
| daf261b4f8 | |||
| 50fa4de919 | |||
| ab2061b2bd | |||
| 4dd0775640 | |||
| 4f81431ccd | |||
| 6371787b47 | |||
| 0ced766608 | |||
| 26b61cdbc9 | |||
| 0e6763d954 | |||
| 7ba9a750ea | |||
| f9b2786809 | |||
| 95f498e894 | |||
| a88f7bac09 | |||
| 23ef404f7b | |||
| 709cbda835 | |||
| f2bb72e40d | |||
| 6650a8c951 | |||
| d76daf8791 | |||
| 8d7005170c | |||
| 37e147e3be | |||
| 26aa71866a | |||
| 2274934731 | |||
| 6c674a3f38 | |||
| d72acabe9d | |||
| 3e0e4876bc | |||
| 3ab0b332ec | |||
| 5e6ff42fe6 | |||
| 4916edd498 | |||
| 0c582765db | |||
| ff6a8c9518 | |||
| 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 | |||
| 131bd30984 | |||
| af4e0d9438 | |||
| ec9b6dd1bc | |||
| e1c12c9dab | |||
| ad75dfe27c | |||
| 9c926362a9 | |||
| 0a1940f781 | |||
| f937bcc22d | |||
| 69f2d38a0a | |||
| 1d9a1d593b | |||
| b3d7ba448d | |||
| 3cb14405f6 | |||
| 33defa1182 | |||
| 1084b6b299 | |||
| fbb8580d28 | |||
| 5db74714db | |||
| 5a106a3ab7 | |||
| 0ef169518a | |||
| 9d76264a28 | |||
| 1181e488c1 | |||
| bbfd82aaea | |||
| bd10290bb3 | |||
| 63431adab1 | |||
| 393bcc4df3 | |||
| ec53d4fa2a | |||
| dd62ad8f73 | |||
| 25e13b9f41 | |||
| b9f8ed1ea8 | |||
| 2b4b237d1a | |||
| a69b59ed84 | |||
| 49c69ffe53 | |||
| 040ce9bce1 | |||
| 06b2e97392 | |||
| b8481e985e | |||
| 146602caa1 | |||
| c07f8f1677 | |||
| 8a318475bf | |||
| f5abf0d85a | |||
| e373ffdb76 | |||
| 61074d0bfe | |||
| 9a03143853 | |||
| 3ca109edee | |||
| 5420531c64 | |||
| 8116ecd197 | |||
| 1ecd67d8c5 | |||
| 6dd24b36ec | |||
| 84b043ff5f | |||
| 5cf864b9ab | |||
| 43758aa781 | |||
| 684a184d40 | |||
| 5e66fab175 | |||
| e1293d2500 | |||
| 8ce0244c4b | |||
| 50e514543b | |||
| e93cc93b77 | |||
| bf5be9bd57 | |||
| 9e45d592b1 | |||
| 834ad2bd41 | |||
| 83ca18f2c7 | |||
| baacfd5855 | |||
| 066317bbc8 | |||
| 0bcb03f833 | |||
| 9037d21237 | |||
| 4789543573 | |||
| 5345dca95f | |||
| b1a8a4c98f | |||
| 50fd2ece64 | |||
| f972d2b719 | |||
| deb24caaa2 | |||
| 5515f42415 | |||
| fbfb83fa34 | |||
| 8eb49f57d6 | |||
| c5d4dc3b65 | |||
| 7f2480bc01 | |||
| b8372e5087 | |||
| 75c07f013d | |||
| 46bfea4e13 | |||
| 5a2dc8c6a8 | |||
| 06650d014a | |||
| 45feeece58 | |||
| 385c2d8b86 | |||
| 41cc70086a | |||
| 98c71127aa | |||
| d049d3b07d | |||
| 8484f15092 | |||
| 1afa5e4377 | |||
| 9f6d86b8b6 | |||
| 72e7d63bd7 | |||
| b243439e78 | |||
| 65f3b2a877 | |||
| 925d162f26 | |||
| 731fe504c6 | |||
| 942ec395f1 | |||
| 306cf6882e | |||
| 468bb4633a |
@@ -59,4 +59,5 @@ ios/
|
||||
!modules/**/ios/
|
||||
*.ipa
|
||||
*.apk
|
||||
*.aab
|
||||
*.aab
|
||||
.env.local
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"Sentry": {
|
||||
"url": "https://mcp.sentry.dev/mcp/xprocess-m5/jr-shikoku-unofficial-apps"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,11 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { Linking, Platform, UIManager } from "react-native";
|
||||
import {
|
||||
IS_LOW_DENSITY,
|
||||
DEX_SCALE,
|
||||
useRealWindowDimensions,
|
||||
} from "./utils/dexDimensionOverride";
|
||||
import { Linking, Platform, UIManager, View } from "react-native";
|
||||
import { enableFreeze, enableScreens } from "react-native-screens";
|
||||
import { GestureHandlerRootView } from "react-native-gesture-handler";
|
||||
import "./utils/disableFontScaling"; // グローバルなフォントスケーリング無効化
|
||||
import { AppContainer } from "./Apps";
|
||||
@@ -20,22 +26,98 @@ 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 {
|
||||
markStartupExplicitTarget,
|
||||
resolveStartupNavigationSource,
|
||||
rootNavigationRef,
|
||||
stackAwareNavigate,
|
||||
} from "./lib/rootNavigation";
|
||||
import { AppThemeProvider } from "./lib/theme";
|
||||
import StatusbarDetect from "./StatusbarDetect";
|
||||
import * as Sentry from '@sentry/react-native';
|
||||
import {
|
||||
startAppLifecycleCrashSentinel,
|
||||
stopAppLifecycleCrashSentinel,
|
||||
} from "./lib/observability/appLifecycleCrashSentinel";
|
||||
|
||||
Sentry.init({
|
||||
dsn: 'https://1090312e4cf501f5a455d523eff2d538@o4511646874664960.ingest.us.sentry.io/4511646880432128',
|
||||
|
||||
// Adds more context data to events (IP address, cookies, user, etc.)
|
||||
// For more information, visit: https://docs.sentry.io/platforms/react-native/data-management/data-collected/
|
||||
sendDefaultPii: true,
|
||||
|
||||
// Enable Logs
|
||||
enableLogs: true,
|
||||
|
||||
// Configure Session Replay
|
||||
replaysSessionSampleRate: 0.1,
|
||||
replaysOnErrorSampleRate: 1,
|
||||
integrations: [Sentry.mobileReplayIntegration(), Sentry.feedbackIntegration()],
|
||||
|
||||
tracesSampleRate: __DEV__ ? 1.0 : 0.05,
|
||||
|
||||
beforeSend(event) {
|
||||
const dataFetch = event.contexts?.data_fetch as any;
|
||||
if (dataFetch?.responseHead && typeof dataFetch.responseHead === "string") {
|
||||
dataFetch.responseHead = dataFetch.responseHead.slice(0, 300);
|
||||
}
|
||||
return event;
|
||||
},
|
||||
|
||||
// uncomment the line below to enable Spotlight (https://spotlightjs.com)
|
||||
// spotlight: __DEV__,
|
||||
});
|
||||
|
||||
LogBox.ignoreLogs([
|
||||
"ViewPropTypes will be removed",
|
||||
"ColorPropType will be removed",
|
||||
]);
|
||||
|
||||
const nativeScreensDisabled =
|
||||
Platform.OS === "ios" || Platform.OS === "android";
|
||||
|
||||
if (nativeScreensDisabled) {
|
||||
enableFreeze(false);
|
||||
enableScreens(false);
|
||||
}
|
||||
|
||||
if (Platform.OS === "android") {
|
||||
if (UIManager.setLayoutAnimationEnabledExperimental) {
|
||||
UIManager.setLayoutAnimationEnabledExperimental(true);
|
||||
}
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
export default Sentry.wrap(function App() {
|
||||
useEffect(() => {
|
||||
const nativeScreensMode = nativeScreensDisabled
|
||||
? "screens-disabled"
|
||||
: "default";
|
||||
const rootTabSceneMode =
|
||||
Platform.OS === "android" ? "opacity-fixed-z-v1" : "default";
|
||||
Sentry.setTag("native_screens_mode", nativeScreensMode);
|
||||
Sentry.setTag("root_tab_scene_mode", rootTabSceneMode);
|
||||
Sentry.setContext("runtime_navigation_flags", {
|
||||
platform: Platform.OS,
|
||||
nativeScreensMode,
|
||||
rootTabSceneMode,
|
||||
});
|
||||
Sentry.addBreadcrumb({
|
||||
category: "runtime.flags",
|
||||
level: "info",
|
||||
message: "runtime navigation flags applied",
|
||||
data: {
|
||||
platform: Platform.OS,
|
||||
nativeScreensMode,
|
||||
rootTabSceneMode,
|
||||
},
|
||||
});
|
||||
void startAppLifecycleCrashSentinel({ nativeScreensMode });
|
||||
return () => {
|
||||
stopAppLifecycleCrashSentinel();
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
UpdateAsync();
|
||||
}, []);
|
||||
@@ -49,12 +131,12 @@ export default function App() {
|
||||
return;
|
||||
}
|
||||
|
||||
rootNavigationRef.navigate("topMenu", {
|
||||
stackAwareNavigate("topMenu", {
|
||||
screen: "setting",
|
||||
params: {
|
||||
screen: "FelicaHistoryPage",
|
||||
},
|
||||
} as any);
|
||||
});
|
||||
};
|
||||
|
||||
const navigateWhenReady = (
|
||||
@@ -73,42 +155,69 @@ export default function App() {
|
||||
|
||||
const routeFromUrl = (url: string, retryCount = 0) => {
|
||||
const normalized = (url || "").toLowerCase();
|
||||
if (!normalized) return;
|
||||
if (!normalized) return false;
|
||||
|
||||
if (
|
||||
normalized.includes("felicahistorypage") ||
|
||||
normalized.includes("open/felica")
|
||||
) {
|
||||
markStartupExplicitTarget();
|
||||
navigateWhenReady(() => openFelicaPage(), url, retryCount);
|
||||
} else if (normalized.includes("open/traininfo")) {
|
||||
return true;
|
||||
}
|
||||
if (normalized.includes("open/traininfo")) {
|
||||
markStartupExplicitTarget();
|
||||
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);
|
||||
}, url, retryCount);
|
||||
} else if (normalized.includes("open/settings")) {
|
||||
navigateWhenReady(() => {
|
||||
rootNavigationRef.navigate("topMenu", {
|
||||
screen: "setting",
|
||||
} as any);
|
||||
}, url, retryCount);
|
||||
} else if (normalized.includes("open/topmenu")) {
|
||||
navigateWhenReady(() => {
|
||||
rootNavigationRef.navigate("topMenu", {
|
||||
screen: "menu",
|
||||
} as any);
|
||||
}, url, retryCount);
|
||||
return true;
|
||||
}
|
||||
if (normalized.includes("open/operation")) {
|
||||
markStartupExplicitTarget();
|
||||
navigateWhenReady(() => {
|
||||
stackAwareNavigate("information");
|
||||
}, url, retryCount);
|
||||
return true;
|
||||
}
|
||||
if (normalized.includes("open/settings")) {
|
||||
markStartupExplicitTarget();
|
||||
navigateWhenReady(() => {
|
||||
stackAwareNavigate("topMenu", {
|
||||
screen: "setting",
|
||||
});
|
||||
}, url, retryCount);
|
||||
return true;
|
||||
}
|
||||
if (normalized.includes("open/topmenu")) {
|
||||
markStartupExplicitTarget();
|
||||
navigateWhenReady(() => {
|
||||
stackAwareNavigate("topMenu", {
|
||||
screen: "menu",
|
||||
});
|
||||
}, url, retryCount);
|
||||
return true;
|
||||
}
|
||||
if (normalized.includes("positions/apps")) {
|
||||
markStartupExplicitTarget();
|
||||
navigateWhenReady(() => {
|
||||
stackAwareNavigate("positions");
|
||||
}, url, retryCount);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
Linking.getInitialURL().then((url) => {
|
||||
if (url) routeFromUrl(url);
|
||||
});
|
||||
Linking.getInitialURL()
|
||||
.then((url) => {
|
||||
if (url) routeFromUrl(url);
|
||||
})
|
||||
.finally(() => {
|
||||
resolveStartupNavigationSource();
|
||||
});
|
||||
|
||||
const sub = Linking.addEventListener("url", ({ url }) => {
|
||||
routeFromUrl(url);
|
||||
@@ -126,10 +235,10 @@ export default function App() {
|
||||
StationListProvider,
|
||||
FavoriteStationProvider,
|
||||
TrainDelayDataProvider,
|
||||
TrainMenuProvider, // CurrentTrainProvider より先に置くことで useTrainMenu が使える
|
||||
CurrentTrainProvider,
|
||||
AreaInfoProvider,
|
||||
BusAndTrainDataProvider,
|
||||
TrainMenuProvider,
|
||||
SheetProvider,
|
||||
]);
|
||||
return (
|
||||
@@ -137,13 +246,42 @@ export default function App() {
|
||||
<DeviceOrientationChangeProvider>
|
||||
<SafeAreaProvider>
|
||||
<StatusbarDetect />
|
||||
<GestureHandlerRootView style={{ flex: 1 }}>
|
||||
<ProviderTree>
|
||||
<AppContainer />
|
||||
</ProviderTree>
|
||||
</GestureHandlerRootView>
|
||||
<DensityScaleWrapper>
|
||||
<GestureHandlerRootView style={{ flex: 1 }}>
|
||||
<ProviderTree>
|
||||
<AppContainer />
|
||||
</ProviderTree>
|
||||
</GestureHandlerRootView>
|
||||
</DensityScaleWrapper>
|
||||
</SafeAreaProvider>
|
||||
</DeviceOrientationChangeProvider>
|
||||
</AppThemeProvider>
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* 低密度ディスプレイ(DeX等)で全体を transform scale で拡大。
|
||||
* Dimensions.get をパッチ済みなので、コンポーネントは内部サイズに合わせてレイアウトする。
|
||||
*/
|
||||
function DensityScaleWrapper({ children }: { children: React.ReactNode }) {
|
||||
const { width, height } = useRealWindowDimensions();
|
||||
|
||||
if (!IS_LOW_DENSITY) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={{ width, height, overflow: "hidden" }}>
|
||||
<View
|
||||
style={{
|
||||
width: width / DEX_SCALE,
|
||||
height: height / DEX_SCALE,
|
||||
transform: [{ scale: DEX_SCALE }],
|
||||
transformOrigin: "top left",
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from "react";
|
||||
import { NavigationContainer, DarkTheme, DefaultTheme } from "@react-navigation/native";
|
||||
import { NavigationContainer, DarkTheme, DefaultTheme, type EventArg } from "@react-navigation/native";
|
||||
import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";
|
||||
import { Animated, Platform, ActivityIndicator, View, StyleSheet, useColorScheme } from "react-native";
|
||||
import { Animated, Platform, ActivityIndicator, View, StyleSheet, StatusBar, useWindowDimensions, InteractionManager } from "react-native";
|
||||
import { useNavigationState } from "@react-navigation/native";
|
||||
import { useFonts } from "expo-font";
|
||||
import { LinearGradient } from "expo-linear-gradient";
|
||||
@@ -14,8 +14,21 @@ import { useTrainMenu } from "./stateBox/useTrainMenu";
|
||||
import lineColorList from "./assets/originData/lineColorList";
|
||||
import { stationIDPair } from "./lib/getStationList";
|
||||
import "./components/ActionSheetComponents/sheets";
|
||||
import { rootNavigationRef } from "./lib/rootNavigation";
|
||||
import { lastObservedRootRouteRef, positionsLifecycleRef, rootNavigationRef } from "./lib/rootNavigation";
|
||||
import {
|
||||
startupExplicitTargetRef,
|
||||
waitForStartupNavigationResolution,
|
||||
} from "./lib/rootNavigation";
|
||||
import { fixedColors } from "./lib/theme/colors";
|
||||
import { useThemeColors } from "./lib/theme";
|
||||
import * as Sentry from "@sentry/react-native";
|
||||
import WebView from "react-native-webview";
|
||||
import { AS } from "./storageControl";
|
||||
import { STORAGE_KEYS } from "./constants/storage";
|
||||
import {
|
||||
recordAppLifecycleRootNavigation,
|
||||
setAppLifecycleWebViewActive,
|
||||
} from "./lib/observability/appLifecycleCrashSentinel";
|
||||
|
||||
type RootTabParamList = {
|
||||
positions: undefined;
|
||||
@@ -34,10 +47,219 @@ type TabProps = {
|
||||
|
||||
const Tab = createBottomTabNavigator<RootTabParamList>();
|
||||
|
||||
const HiddenStartupPreloadWebViews = ({
|
||||
shouldPreloadPositions,
|
||||
shouldPreloadInformation,
|
||||
}: {
|
||||
shouldPreloadPositions: boolean;
|
||||
shouldPreloadInformation: boolean;
|
||||
}) => {
|
||||
const [positionsLoaded, setPositionsLoaded] = React.useState(false);
|
||||
const [informationLoaded, setInformationLoaded] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
Sentry.setContext("startup_hidden_preload", {
|
||||
shouldPreloadPositions,
|
||||
shouldPreloadInformation,
|
||||
positionsLoaded,
|
||||
informationLoaded,
|
||||
});
|
||||
}, [informationLoaded, positionsLoaded, shouldPreloadInformation, shouldPreloadPositions]);
|
||||
|
||||
const hiddenStyle = React.useMemo(() => ({
|
||||
position: "absolute" as const,
|
||||
width: 1,
|
||||
height: 1,
|
||||
opacity: 0,
|
||||
left: -1000,
|
||||
top: -1000,
|
||||
pointerEvents: "none" as const,
|
||||
}), []);
|
||||
|
||||
const showPositions = shouldPreloadPositions && !positionsLoaded;
|
||||
const showInformation = shouldPreloadInformation && !informationLoaded;
|
||||
|
||||
if (!showPositions && !showInformation) return null;
|
||||
|
||||
return (
|
||||
<View pointerEvents="none" style={hiddenStyle}>
|
||||
{showPositions ? (
|
||||
<WebView
|
||||
source={{ uri: "https://train.jr-shikoku.co.jp/" }}
|
||||
originWhitelist={["https://train.jr-shikoku.co.jp"]}
|
||||
javaScriptEnabled
|
||||
setSupportMultipleWindows={false}
|
||||
onLoadStart={() => {
|
||||
setAppLifecycleWebViewActive("startup_hidden_positions", true);
|
||||
Sentry.addBreadcrumb({
|
||||
category: "startup.preload",
|
||||
level: "info",
|
||||
message: "positions hidden preload loadStart",
|
||||
});
|
||||
}}
|
||||
onLoadEnd={() => {
|
||||
setAppLifecycleWebViewActive("startup_hidden_positions", false);
|
||||
Sentry.addBreadcrumb({
|
||||
category: "startup.preload",
|
||||
level: "info",
|
||||
message: "positions hidden preload loadEnd",
|
||||
});
|
||||
setPositionsLoaded(true);
|
||||
}}
|
||||
onError={() => {
|
||||
setAppLifecycleWebViewActive("startup_hidden_positions", false);
|
||||
Sentry.addBreadcrumb({
|
||||
category: "startup.preload",
|
||||
level: "error",
|
||||
message: "positions hidden preload error",
|
||||
});
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
{showInformation ? (
|
||||
<WebView
|
||||
source={{ uri: "https://www.jr-shikoku.co.jp/info/" }}
|
||||
originWhitelist={["https://www.jr-shikoku.co.jp"]}
|
||||
javaScriptEnabled
|
||||
setSupportMultipleWindows={false}
|
||||
onLoadStart={() => {
|
||||
setAppLifecycleWebViewActive("startup_hidden_operation", true);
|
||||
Sentry.addBreadcrumb({
|
||||
category: "startup.preload",
|
||||
level: "info",
|
||||
message: "operation hidden preload loadStart",
|
||||
});
|
||||
}}
|
||||
onLoadEnd={() => {
|
||||
setAppLifecycleWebViewActive("startup_hidden_operation", false);
|
||||
Sentry.addBreadcrumb({
|
||||
category: "startup.preload",
|
||||
level: "info",
|
||||
message: "operation hidden preload loadEnd",
|
||||
});
|
||||
setInformationLoaded(true);
|
||||
}}
|
||||
onError={() => {
|
||||
setAppLifecycleWebViewActive("startup_hidden_operation", false);
|
||||
Sentry.addBreadcrumb({
|
||||
category: "startup.preload",
|
||||
level: "error",
|
||||
message: "operation hidden preload error",
|
||||
});
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const PositionsRootGateContext = React.createContext({
|
||||
shouldActivate: false,
|
||||
isRootFocused: false,
|
||||
});
|
||||
|
||||
const InformationRootGateContext = React.createContext({
|
||||
shouldActivate: false,
|
||||
isRootFocused: false,
|
||||
});
|
||||
|
||||
const DeferredPositionsRoot = ({ shouldActivate, isRootFocused }: { shouldActivate: boolean; isRootFocused: boolean }) => {
|
||||
const [hasActivated, setHasActivated] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
Sentry.setContext("positions_root_gate", {
|
||||
focused: isRootFocused,
|
||||
activated: hasActivated,
|
||||
shouldActivate,
|
||||
});
|
||||
}, [hasActivated, isRootFocused, shouldActivate]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!shouldActivate || hasActivated) return;
|
||||
Sentry.addBreadcrumb({
|
||||
category: "positions.root",
|
||||
level: "info",
|
||||
message: "positions root activation",
|
||||
data: {
|
||||
reason: "first_root_visit",
|
||||
},
|
||||
});
|
||||
setHasActivated(true);
|
||||
}, [hasActivated, shouldActivate]);
|
||||
|
||||
if (!hasActivated) {
|
||||
return <View style={{ flex: 1 }} />;
|
||||
}
|
||||
|
||||
return <Top />;
|
||||
};
|
||||
|
||||
const PositionsTabScreen = React.memo(() => {
|
||||
const { shouldActivate, isRootFocused } = React.useContext(PositionsRootGateContext);
|
||||
return (
|
||||
<DeferredPositionsRoot
|
||||
shouldActivate={shouldActivate}
|
||||
isRootFocused={isRootFocused}
|
||||
/>
|
||||
);
|
||||
});
|
||||
PositionsTabScreen.displayName = "PositionsTabScreen";
|
||||
|
||||
|
||||
const DeferredInformationRoot = ({ shouldActivate, isRootFocused }: { shouldActivate: boolean; isRootFocused: boolean }) => {
|
||||
const [hasActivated, setHasActivated] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
Sentry.setContext("operation_root_gate", {
|
||||
focused: isRootFocused,
|
||||
activated: hasActivated,
|
||||
shouldActivate,
|
||||
});
|
||||
}, [hasActivated, isRootFocused, shouldActivate]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!shouldActivate || hasActivated) return;
|
||||
Sentry.addBreadcrumb({
|
||||
category: "operation.root",
|
||||
level: "info",
|
||||
message: "operation root activation",
|
||||
data: {
|
||||
reason: isRootFocused ? "first_focus" : "background_prewarm",
|
||||
},
|
||||
});
|
||||
setHasActivated(true);
|
||||
}, [hasActivated, isRootFocused, shouldActivate]);
|
||||
|
||||
if (!hasActivated) {
|
||||
return <View style={{ flex: 1 }} />;
|
||||
}
|
||||
|
||||
return <TNDView />;
|
||||
};
|
||||
|
||||
const InformationTabScreen = React.memo(() => {
|
||||
const { shouldActivate, isRootFocused } = React.useContext(InformationRootGateContext);
|
||||
return (
|
||||
<DeferredInformationRoot
|
||||
shouldActivate={shouldActivate}
|
||||
isRootFocused={isRootFocused}
|
||||
/>
|
||||
);
|
||||
});
|
||||
InformationTabScreen.displayName = "InformationTabScreen";
|
||||
|
||||
export function AppContainer() {
|
||||
const { areaInfo, areaIconBadgeText, isInfo } = useAreaInfo();
|
||||
const { selectedLine } = useTrainMenu();
|
||||
const { width, height } = useWindowDimensions();
|
||||
const [isExtraWindowOpen, setIsExtraWindowOpen] = React.useState(false);
|
||||
const [startupInitialRoute, setStartupInitialRoute] = React.useState<keyof RootTabParamList | null>(null);
|
||||
const [hasVisitedPositions, setHasVisitedPositions] = React.useState(false);
|
||||
const [hasVisitedInformation, setHasVisitedInformation] = React.useState(false);
|
||||
const [currentRootRoute, setCurrentRootRoute] = React.useState<keyof RootTabParamList | null>(null);
|
||||
const [navigationReady, setNavigationReady] = React.useState(false);
|
||||
const startupPrewarmAttemptedRef = React.useRef(false);
|
||||
const informationStartupPrewarmAttemptedRef = React.useRef(false);
|
||||
|
||||
// フェードアニメーション用 (0=通常, 1=追加ウィンドウ青)
|
||||
const fadeAnim = React.useRef(new Animated.Value(0)).current;
|
||||
@@ -60,17 +282,54 @@ export function AppContainer() {
|
||||
const b = Math.round(parseInt(h.slice(4, 6), 16) * factor);
|
||||
return `#${[r, g, b].map((v) => Math.min(255, v).toString(16).padStart(2, "0")).join("")}`;
|
||||
};
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
const { isDark } = useThemeColors();
|
||||
const lastRootNavStateRef = React.useRef("");
|
||||
const lineColorDark = lineColor ? darkenHex(lineColor, 0.78) : null;
|
||||
|
||||
React.useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
void (async () => {
|
||||
await waitForStartupNavigationResolution();
|
||||
if (cancelled) return;
|
||||
|
||||
let nextInitialRoute: keyof RootTabParamList = "topMenu";
|
||||
if (!startupExplicitTargetRef.current) {
|
||||
try {
|
||||
const startPage = await AS.getItem(STORAGE_KEYS.START_PAGE);
|
||||
if (startPage === "true") {
|
||||
nextInitialRoute = "positions";
|
||||
}
|
||||
} catch {
|
||||
// Keep the default route when the setting is unset or unreadable.
|
||||
}
|
||||
}
|
||||
|
||||
if (cancelled) return;
|
||||
|
||||
setStartupInitialRoute(nextInitialRoute);
|
||||
setHasVisitedPositions(nextInitialRoute === "positions");
|
||||
Sentry.addBreadcrumb({
|
||||
category: "nav.root",
|
||||
level: "info",
|
||||
message: "startup initial root route resolved",
|
||||
data: {
|
||||
startupExplicitTarget: startupExplicitTargetRef.current,
|
||||
initialRoute: nextInitialRoute,
|
||||
},
|
||||
});
|
||||
})();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
const linking = {
|
||||
prefixes: ["jrshikoku://"],
|
||||
config: {
|
||||
screens: {
|
||||
positions: {
|
||||
screens: {
|
||||
Apps: "positions/apps",
|
||||
},
|
||||
screens: {},
|
||||
},
|
||||
topMenu: {
|
||||
screens: {
|
||||
@@ -101,17 +360,152 @@ export function AppContainer() {
|
||||
tabBarLabel: label,
|
||||
headerShown: false,
|
||||
gestureEnabled: true,
|
||||
unmountOnBlur: false,
|
||||
tabBarIcon: initIcon(icon as any, iconFamily, tabBarBadge, isInfo),
|
||||
|
||||
},
|
||||
});
|
||||
const applyRootNavigationState = React.useCallback((state: any, source: "ready" | "change") => {
|
||||
const activeRoute = state?.routes?.[state?.index ?? 0];
|
||||
const hasExtra = (activeRoute?.state?.index ?? 0) > 0;
|
||||
const nestedState = activeRoute?.state;
|
||||
const nestedRoute = nestedState?.routes?.[nestedState.index ?? 0]?.name ?? null;
|
||||
const signature = JSON.stringify({
|
||||
rootIndex: state?.index ?? 0,
|
||||
rootRoute: activeRoute?.name ?? null,
|
||||
nestedIndex: nestedState?.index ?? 0,
|
||||
nestedRoute,
|
||||
hasExtra,
|
||||
});
|
||||
if (lastRootNavStateRef.current !== signature) {
|
||||
lastRootNavStateRef.current = signature;
|
||||
Sentry.addBreadcrumb({
|
||||
category: "nav.root",
|
||||
level: "info",
|
||||
message: source === "ready" ? "root state initialized" : "root state change",
|
||||
data: {
|
||||
rootIndex: state?.index ?? 0,
|
||||
rootRoute: activeRoute?.name ?? null,
|
||||
nestedIndex: nestedState?.index ?? 0,
|
||||
nestedRoute,
|
||||
hasExtra,
|
||||
source,
|
||||
},
|
||||
});
|
||||
}
|
||||
const rootRouteName = (activeRoute?.name ?? null) as keyof RootTabParamList | null;
|
||||
if (rootRouteName) {
|
||||
lastObservedRootRouteRef.current = rootRouteName;
|
||||
}
|
||||
recordAppLifecycleRootNavigation({
|
||||
rootIndex: state?.index ?? 0,
|
||||
rootRoute: rootRouteName,
|
||||
nestedIndex: nestedState?.index ?? 0,
|
||||
nestedRoute,
|
||||
source,
|
||||
});
|
||||
Sentry.setTag("root_tab", rootRouteName ?? "unknown");
|
||||
Sentry.setContext("root_navigation", {
|
||||
rootIndex: state?.index ?? 0,
|
||||
rootRoute: rootRouteName,
|
||||
nestedIndex: nestedState?.index ?? 0,
|
||||
nestedRoute,
|
||||
hasExtra,
|
||||
source,
|
||||
operationOrientation: width > height ? "landscape" : "portrait",
|
||||
hasVisitedPositions,
|
||||
hasVisitedInformation,
|
||||
});
|
||||
setCurrentRootRoute(rootRouteName);
|
||||
setIsExtraWindowOpen(hasExtra);
|
||||
}, [hasVisitedInformation, hasVisitedPositions, width, height]);
|
||||
|
||||
const [fontLoaded, error] = useFonts({
|
||||
"JR-Nishi": require("./assets/fonts/jr-nishi.otf"),
|
||||
"JR-WEST-PLUS": require("./assets/fonts/JR-WEST-PLUS.otf"),
|
||||
Zou: require("./assets/fonts/DelaGothicOne-Regular.ttf"),
|
||||
"JNR-font": require("./assets/fonts/JNRfont_pict.ttf"),
|
||||
"DiaPro": require("./assets/fonts/DiaPro-Regular.otf"),
|
||||
});
|
||||
if (!fontLoaded) {
|
||||
React.useEffect(() => {
|
||||
if (!navigationReady || !fontLoaded) return;
|
||||
if (startupPrewarmAttemptedRef.current) return;
|
||||
if (hasVisitedPositions) {
|
||||
startupPrewarmAttemptedRef.current = true;
|
||||
return;
|
||||
}
|
||||
if (currentRootRoute === null || currentRootRoute === "positions") return;
|
||||
|
||||
startupPrewarmAttemptedRef.current = true;
|
||||
Sentry.addBreadcrumb({
|
||||
category: "positions.root",
|
||||
level: "info",
|
||||
message: "positions startup prewarm scheduled",
|
||||
data: {
|
||||
currentRootRoute,
|
||||
},
|
||||
});
|
||||
|
||||
let cancelled = false;
|
||||
const timer = setTimeout(() => {
|
||||
if (cancelled || hasVisitedPositions) return;
|
||||
Sentry.addBreadcrumb({
|
||||
category: "positions.root",
|
||||
level: "info",
|
||||
message: "positions startup prewarm activated",
|
||||
data: {
|
||||
currentRootRoute,
|
||||
},
|
||||
});
|
||||
setHasVisitedPositions(true);
|
||||
}, 500);
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
clearTimeout(timer);
|
||||
};
|
||||
}, [currentRootRoute, fontLoaded, hasVisitedPositions, navigationReady]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!navigationReady || !fontLoaded) return;
|
||||
if (informationStartupPrewarmAttemptedRef.current) return;
|
||||
if (hasVisitedInformation) {
|
||||
informationStartupPrewarmAttemptedRef.current = true;
|
||||
return;
|
||||
}
|
||||
if (currentRootRoute === null || currentRootRoute === "information") return;
|
||||
|
||||
informationStartupPrewarmAttemptedRef.current = true;
|
||||
Sentry.addBreadcrumb({
|
||||
category: "operation.root",
|
||||
level: "info",
|
||||
message: "operation startup prewarm scheduled",
|
||||
data: {
|
||||
currentRootRoute,
|
||||
},
|
||||
});
|
||||
|
||||
let cancelled = false;
|
||||
const timer = setTimeout(() => {
|
||||
if (cancelled || hasVisitedInformation) return;
|
||||
Sentry.addBreadcrumb({
|
||||
category: "operation.root",
|
||||
level: "info",
|
||||
message: "operation startup prewarm activated",
|
||||
data: {
|
||||
currentRootRoute,
|
||||
},
|
||||
});
|
||||
setHasVisitedInformation(true);
|
||||
}, 500);
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
clearTimeout(timer);
|
||||
};
|
||||
}, [currentRootRoute, fontLoaded, hasVisitedInformation, navigationReady]);
|
||||
|
||||
if (!fontLoaded || !startupInitialRoute) {
|
||||
return (
|
||||
<View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
|
||||
<ActivityIndicator size="large" />
|
||||
@@ -123,14 +517,123 @@ export function AppContainer() {
|
||||
ref={rootNavigationRef}
|
||||
linking={linking}
|
||||
theme={isDark ? DarkTheme : DefaultTheme}
|
||||
onReady={() => {
|
||||
Sentry.addBreadcrumb({
|
||||
category: "nav.root",
|
||||
level: "info",
|
||||
message: "navigation ready",
|
||||
});
|
||||
applyRootNavigationState(rootNavigationRef.getRootState?.() ?? rootNavigationRef.getState?.(), "ready");
|
||||
setNavigationReady(true);
|
||||
}}
|
||||
onStateChange={(state) => {
|
||||
const hasExtra = state?.routes?.some((r) => (r.state?.index ?? 0) > 0) ?? false;
|
||||
setIsExtraWindowOpen(hasExtra);
|
||||
applyRootNavigationState(state, "change");
|
||||
}}
|
||||
>
|
||||
{/* @ts-expect-error - Tab.Navigator type definition issue */}
|
||||
<PositionsRootGateContext.Provider
|
||||
value={{
|
||||
shouldActivate: hasVisitedPositions,
|
||||
isRootFocused: currentRootRoute === "positions",
|
||||
}}
|
||||
>
|
||||
<InformationRootGateContext.Provider
|
||||
value={{
|
||||
shouldActivate: hasVisitedInformation,
|
||||
isRootFocused: currentRootRoute === "information",
|
||||
}}
|
||||
>
|
||||
<Tab.Navigator
|
||||
initialRouteName="topMenu"
|
||||
id="rootTabs"
|
||||
detachInactiveScreens={false}
|
||||
initialRouteName={startupInitialRoute}
|
||||
screenListeners={({ route }) => ({
|
||||
tabPress: (event: EventArg<"tabPress", true>) => {
|
||||
const rootState = rootNavigationRef.getState();
|
||||
const activeRootRouteName =
|
||||
(rootState?.routes?.[rootState.index ?? 0]?.name as keyof RootTabParamList | undefined) ??
|
||||
currentRootRoute ??
|
||||
null;
|
||||
const leavingUnstablePositions =
|
||||
route.name !== "positions" &&
|
||||
positionsLifecycleRef.current.isUnstable;
|
||||
const blockingUnstableExit =
|
||||
route.name !== "positions" &&
|
||||
positionsLifecycleRef.current.blockTabExit;
|
||||
|
||||
Sentry.addBreadcrumb({
|
||||
category: "nav.tab",
|
||||
level: "info",
|
||||
message: blockingUnstableExit
|
||||
? "root tabPress blocked while positions unstable"
|
||||
: leavingUnstablePositions
|
||||
? "root tabPress intercepted for unstable positions"
|
||||
: "root tabPress",
|
||||
data: {
|
||||
route: route.name,
|
||||
currentRootRoute,
|
||||
activeRootRouteName,
|
||||
unstablePositions: positionsLifecycleRef.current.isUnstable,
|
||||
blockTabExit: positionsLifecycleRef.current.blockTabExit,
|
||||
},
|
||||
});
|
||||
|
||||
if (blockingUnstableExit) {
|
||||
event.preventDefault();
|
||||
positionsLifecycleRef.current.resetBeforeLeave?.();
|
||||
requestAnimationFrame(() => {
|
||||
setTimeout(() => {
|
||||
rootNavigationRef.navigate(route.name as never);
|
||||
}, 0);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (leavingUnstablePositions) {
|
||||
event.preventDefault();
|
||||
positionsLifecycleRef.current.resetBeforeLeave?.();
|
||||
requestAnimationFrame(() => {
|
||||
setTimeout(() => {
|
||||
rootNavigationRef.navigate(route.name as never);
|
||||
}, 0);
|
||||
});
|
||||
Sentry.addBreadcrumb({
|
||||
category: "positions.root",
|
||||
level: "info",
|
||||
message: "positions root exit reset and continued while session unstable",
|
||||
data: {
|
||||
route: route.name,
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
focus: () => {
|
||||
Sentry.addBreadcrumb({
|
||||
category: "nav.tab",
|
||||
level: "info",
|
||||
message: "root tab focus",
|
||||
data: { route: route.name },
|
||||
});
|
||||
},
|
||||
blur: () => {
|
||||
Sentry.addBreadcrumb({
|
||||
category: "nav.tab",
|
||||
level: "info",
|
||||
message: "root tab blur",
|
||||
data: { route: route.name },
|
||||
});
|
||||
},
|
||||
state: (event) => {
|
||||
Sentry.addBreadcrumb({
|
||||
category: "nav.tab",
|
||||
level: "info",
|
||||
message: "root tab state",
|
||||
data: {
|
||||
route: route.name,
|
||||
stateType: event.type,
|
||||
},
|
||||
});
|
||||
},
|
||||
})}
|
||||
screenOptions={({ route }) => {
|
||||
const showGradient = route.name === "positions" && !!lineColor && !!lineColorDark;
|
||||
const defaultBg = isDark ? "#1c1c1e" : "white";
|
||||
@@ -138,8 +641,7 @@ export function AppContainer() {
|
||||
const defaultInactive = isDark ? "#8e8e93" : "#8e8e93";
|
||||
return {
|
||||
lazy: false,
|
||||
tabBarHideOnKeyboard: Platform.OS === "android",
|
||||
animation: Platform.OS === "ios" ? "none" : "shift",
|
||||
freezeOnBlur: false,
|
||||
sceneContainerStyle: { backgroundColor: defaultBg },
|
||||
tabBarActiveTintColor: (showGradient || isExtraWindowOpen) ? "white" : defaultActive,
|
||||
tabBarInactiveTintColor: (showGradient || isExtraWindowOpen) ? "rgba(255,255,255,0.75)" : defaultInactive,
|
||||
@@ -173,7 +675,32 @@ export function AppContainer() {
|
||||
>
|
||||
<Tab.Screen
|
||||
{...getTabProps("positions", "走行位置", "bar-chart", "AntDesign")}
|
||||
component={Top}
|
||||
component={PositionsTabScreen}
|
||||
listeners={{
|
||||
tabPress: () => {
|
||||
Sentry.addBreadcrumb({
|
||||
category: "positions.root",
|
||||
level: "info",
|
||||
message: "positions root tabPress activation",
|
||||
});
|
||||
setHasVisitedPositions(true);
|
||||
},
|
||||
focus: () => {
|
||||
Sentry.addBreadcrumb({
|
||||
category: "positions.root",
|
||||
level: "info",
|
||||
message: "positions root focus activation",
|
||||
});
|
||||
setHasVisitedPositions(true);
|
||||
},
|
||||
blur: () => {
|
||||
Sentry.addBreadcrumb({
|
||||
category: "positions.root",
|
||||
level: "info",
|
||||
message: "positions root blur preserved",
|
||||
});
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Tab.Screen
|
||||
{...getTabProps("topMenu", "トップメニュー", "radio", "Ionicons")}
|
||||
@@ -189,9 +716,40 @@ export function AppContainer() {
|
||||
areaInfo ? areaIconBadgeText : undefined,
|
||||
isInfo
|
||||
)}
|
||||
children={TNDView}
|
||||
component={InformationTabScreen}
|
||||
listeners={{
|
||||
tabPress: () => {
|
||||
Sentry.addBreadcrumb({
|
||||
category: "operation.root",
|
||||
level: "info",
|
||||
message: "operation root tabPress activation",
|
||||
});
|
||||
setHasVisitedInformation(true);
|
||||
},
|
||||
focus: () => {
|
||||
Sentry.addBreadcrumb({
|
||||
category: "operation.root",
|
||||
level: "info",
|
||||
message: "operation root focus activation",
|
||||
});
|
||||
setHasVisitedInformation(true);
|
||||
},
|
||||
blur: () => {
|
||||
Sentry.addBreadcrumb({
|
||||
category: "operation.root",
|
||||
level: "info",
|
||||
message: "operation root blur preserved",
|
||||
});
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Tab.Navigator>
|
||||
</InformationRootGateContext.Provider>
|
||||
</PositionsRootGateContext.Provider>
|
||||
<HiddenStartupPreloadWebViews
|
||||
shouldPreloadPositions={navigationReady && fontLoaded && currentRootRoute === "topMenu" && !hasVisitedPositions}
|
||||
shouldPreloadInformation={navigationReady && fontLoaded && currentRootRoute === "topMenu" && !hasVisitedInformation}
|
||||
/>
|
||||
</NavigationContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,25 +1,277 @@
|
||||
import React, { CSSProperties } from "react";
|
||||
import { Alert, BackHandler, View, ViewProps } from "react-native";
|
||||
import React from "react";
|
||||
import { Alert, ActivityIndicator, BackHandler, StyleSheet, Text, TouchableOpacity, View } from "react-native";
|
||||
import * as FileSystem from "expo-file-system/legacy";
|
||||
import * as Sharing from "expo-sharing";
|
||||
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 { useTrainMenu } from "@/stateBox/useTrainMenu";
|
||||
import {
|
||||
DEFAULT_JR_DATA_SYSTEM_ENV,
|
||||
normalizeJrDataSystemEnvironment,
|
||||
rewriteJrDataSystemUrl,
|
||||
} from "@/lib/jrDataSystemEnvironment";
|
||||
|
||||
const RECORDING_DOWNLOAD_BRIDGE_SCRIPT = `
|
||||
(() => {
|
||||
if (window.__JRS_RECORDING_DOWNLOAD_BRIDGE__) return true;
|
||||
window.__JRS_RECORDING_DOWNLOAD_BRIDGE__ = true;
|
||||
|
||||
const blobUrls = new Map();
|
||||
const post = (payload) => {
|
||||
window.ReactNativeWebView?.postMessage(JSON.stringify(payload));
|
||||
};
|
||||
const isLikelyRecordingDownload = (url, anchor) => {
|
||||
if (!url) return false;
|
||||
const lowerUrl = String(url).toLowerCase();
|
||||
const downloadName = anchor?.getAttribute?.('download') || '';
|
||||
const lowerName = String(downloadName).toLowerCase();
|
||||
return Boolean(downloadName)
|
||||
|| lowerUrl.includes('recording')
|
||||
|| lowerUrl.includes('recordings')
|
||||
|| lowerUrl.includes('download')
|
||||
|| lowerUrl.includes('export')
|
||||
|| lowerUrl.includes('json')
|
||||
|| lowerName.includes('recording')
|
||||
|| lowerName.endsWith('.json');
|
||||
};
|
||||
const readUrlAsText = async (url) => {
|
||||
if (blobUrls.has(url)) {
|
||||
return blobUrls.get(url).text();
|
||||
}
|
||||
const response = await fetch(url, { credentials: 'include' });
|
||||
if (!response.ok) throw new Error('HTTP ' + response.status);
|
||||
return response.text();
|
||||
};
|
||||
const importFromUrl = async (url) => {
|
||||
try {
|
||||
const text = await readUrlAsText(url);
|
||||
post({ type: 'importRecordingDownload', text, sourceUrl: url });
|
||||
} catch (error) {
|
||||
post({
|
||||
type: 'importRecordingDownloadError',
|
||||
message: error?.message || String(error),
|
||||
sourceUrl: url,
|
||||
});
|
||||
}
|
||||
};
|
||||
const findAnchor = (target) => {
|
||||
let node = target;
|
||||
while (node && node !== document) {
|
||||
if (node.tagName === 'A' && node.href) return node;
|
||||
node = node.parentNode;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
if (window.URL?.createObjectURL) {
|
||||
const originalCreateObjectURL = window.URL.createObjectURL.bind(window.URL);
|
||||
window.URL.createObjectURL = (object) => {
|
||||
const url = originalCreateObjectURL(object);
|
||||
if (object instanceof Blob) blobUrls.set(url, object);
|
||||
return url;
|
||||
};
|
||||
}
|
||||
|
||||
document.addEventListener('click', (event) => {
|
||||
const anchor = findAnchor(event.target);
|
||||
if (!anchor || !isLikelyRecordingDownload(anchor.href, anchor)) return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
importFromUrl(anchor.href);
|
||||
}, true);
|
||||
|
||||
if (window.HTMLAnchorElement?.prototype?.click) {
|
||||
const originalClick = window.HTMLAnchorElement.prototype.click;
|
||||
window.HTMLAnchorElement.prototype.click = function patchedClick() {
|
||||
if (isLikelyRecordingDownload(this.href, this)) {
|
||||
importFromUrl(this.href);
|
||||
return;
|
||||
}
|
||||
return originalClick.apply(this, arguments);
|
||||
};
|
||||
}
|
||||
|
||||
return true;
|
||||
})();
|
||||
true;
|
||||
`;
|
||||
|
||||
const isLikelyRecordingDownloadUrl = (url: string) => {
|
||||
const lowerUrl = url.toLowerCase();
|
||||
return lowerUrl.includes('recording')
|
||||
|| lowerUrl.includes('recordings')
|
||||
|| lowerUrl.includes('download')
|
||||
|| lowerUrl.includes('export')
|
||||
|| lowerUrl.includes('json')
|
||||
|| lowerUrl.endsWith('.json');
|
||||
};
|
||||
|
||||
export default ({ route }) => {
|
||||
if (!route.params) {
|
||||
return null;
|
||||
}
|
||||
const { uri, useExitButton = true } = route.params;
|
||||
const {
|
||||
uri,
|
||||
useExitButton = true,
|
||||
importRecordingDownloads = false,
|
||||
} = route.params;
|
||||
const { goBack } = useNavigation();
|
||||
const { fixed } = useThemeColors();
|
||||
const { importRecordingsFromText } = useTrainMenu();
|
||||
const webViewRef = React.useRef<WebView>(null);
|
||||
const [canGoBack, setCanGoBack] = React.useState(false);
|
||||
const nativeCanGoBackRef = React.useRef(false);
|
||||
const historyStackRef = React.useRef<string[]>([]);
|
||||
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);
|
||||
// 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);
|
||||
const rawUri = typeof uri === "string" ? uri : "";
|
||||
historyStackRef.current = [];
|
||||
nativeCanGoBackRef.current = false;
|
||||
setCanGoBack(false);
|
||||
setSelectedEnvironment(nextEnvironment);
|
||||
setResolvedUri(
|
||||
importRecordingDownloads
|
||||
? rawUri
|
||||
: rewriteJrDataSystemUrl(rawUri, nextEnvironment),
|
||||
);
|
||||
setIsEnvironmentReady(true);
|
||||
};
|
||||
|
||||
AS.getItem(STORAGE_KEYS.JR_DATA_SYSTEM_ENV)
|
||||
.then(applyEnvironment)
|
||||
.catch(() => applyEnvironment(DEFAULT_JR_DATA_SYSTEM_ENV));
|
||||
|
||||
return () => {
|
||||
isMounted = false;
|
||||
};
|
||||
}, [uri, importRecordingDownloads]);
|
||||
|
||||
const handleReload = () => {
|
||||
lastPongAt.current = Date.now();
|
||||
setHasError(false);
|
||||
hasErrorRef.current = false;
|
||||
setIsLoading(true);
|
||||
isLoadingRef.current = true;
|
||||
setWebViewKey((k) => k + 1);
|
||||
};
|
||||
|
||||
const syncCanGoBack = React.useCallback((nativeCanGoBack: boolean) => {
|
||||
nativeCanGoBackRef.current = nativeCanGoBack;
|
||||
setCanGoBack(nativeCanGoBack || historyStackRef.current.length > 1);
|
||||
}, []);
|
||||
|
||||
const pushHistoryEntry = React.useCallback((url: string) => {
|
||||
if (!url) return;
|
||||
const stack = historyStackRef.current;
|
||||
if (stack[stack.length - 1] === url) return;
|
||||
stack.push(url);
|
||||
if (stack.length > 30) {
|
||||
stack.splice(0, stack.length - 30);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handlePseudoBack = React.useCallback(() => {
|
||||
const stack = historyStackRef.current;
|
||||
if (stack.length <= 1) return false;
|
||||
stack.pop();
|
||||
const previousUrl = stack[stack.length - 1];
|
||||
if (!previousUrl) return false;
|
||||
setResolvedUri(previousUrl);
|
||||
syncCanGoBack(false);
|
||||
remount();
|
||||
return true;
|
||||
}, [remount, syncCanGoBack]);
|
||||
|
||||
const handleWebViewBack = React.useCallback(() => {
|
||||
if (nativeCanGoBackRef.current) {
|
||||
webViewRef.current?.goBack();
|
||||
return true;
|
||||
}
|
||||
return handlePseudoBack();
|
||||
}, [handlePseudoBack]);
|
||||
|
||||
const handleImportRecordingText = React.useCallback(
|
||||
async (content: string) => {
|
||||
try {
|
||||
const result = await importRecordingsFromText(content);
|
||||
Alert.alert(
|
||||
"録画データを読み込みました",
|
||||
result.overwrittenCount > 0
|
||||
? `${result.importedCount}件を読み込みました。${result.overwrittenCount}件は同じIDのため上書きしました。`
|
||||
: `${result.importedCount}件を読み込みました。`,
|
||||
);
|
||||
} catch (error) {
|
||||
Alert.alert(
|
||||
"録画データを読み込めませんでした",
|
||||
(error as Error).message || "ファイル内容を確認してください。",
|
||||
);
|
||||
}
|
||||
},
|
||||
[importRecordingsFromText],
|
||||
);
|
||||
|
||||
const handleImportRecordingUrl = React.useCallback(
|
||||
async (url: string) => {
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
await handleImportRecordingText(await response.text());
|
||||
} catch (error) {
|
||||
Alert.alert(
|
||||
"録画データを取得できませんでした",
|
||||
(error as Error).message || "通信状態を確認してください。",
|
||||
);
|
||||
}
|
||||
},
|
||||
[handleImportRecordingText],
|
||||
);
|
||||
|
||||
useFocusEffect(
|
||||
React.useCallback(() => {
|
||||
const onHardwareBack = () => {
|
||||
if (canGoBack) {
|
||||
webViewRef.current?.goBack();
|
||||
return true;
|
||||
if (handleWebViewBack()) return true;
|
||||
}
|
||||
goBack();
|
||||
return true;
|
||||
@@ -27,31 +279,237 @@ export default ({ route }) => {
|
||||
|
||||
const subscription = BackHandler.addEventListener("hardwareBackPress", onHardwareBack);
|
||||
return () => subscription.remove();
|
||||
}, [canGoBack, goBack])
|
||||
}, [canGoBack, goBack, handleWebViewBack])
|
||||
);
|
||||
return (
|
||||
<View style={{ height: "100%", backgroundColor: fixed.primary }}>
|
||||
<WebView
|
||||
source={{ uri }}
|
||||
allowsBackForwardNavigationGestures
|
||||
ref={webViewRef}
|
||||
onNavigationStateChange={(navState) => {
|
||||
setCanGoBack(navState.canGoBack);
|
||||
if (navState.url === "https://unyohub.2pd.jp/integration/succeeded.php") {
|
||||
goBack();
|
||||
Alert.alert("鉄道運用HUBへの投稿完了", "運用HUBからのこのアプリへのデータ反映には暫く時間がかかりますので、しばらくお待ちください。", [
|
||||
{ text: "完了" },
|
||||
]);
|
||||
{isEnvironmentReady && (
|
||||
<WebView
|
||||
key={webViewKey}
|
||||
source={{ uri: resolvedUri }}
|
||||
contentMode="mobile"
|
||||
allowsBackForwardNavigationGestures
|
||||
setSupportMultipleWindows={false}
|
||||
ref={webViewRef}
|
||||
injectedJavaScriptBeforeContentLoaded={
|
||||
importRecordingDownloads ? RECORDING_DOWNLOAD_BRIDGE_SCRIPT : `true;`
|
||||
}
|
||||
}}
|
||||
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="閉じる" />}
|
||||
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;
|
||||
}
|
||||
|
||||
if (
|
||||
importRecordingDownloads &&
|
||||
isLikelyRecordingDownloadUrl(request.url) &&
|
||||
request.url !== resolvedUri
|
||||
) {
|
||||
void handleImportRecordingUrl(request.url);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!importRecordingDownloads) {
|
||||
const rewrittenUrl = rewriteJrDataSystemUrl(
|
||||
request.url,
|
||||
selectedEnvironment,
|
||||
);
|
||||
if (rewrittenUrl !== request.url) {
|
||||
setResolvedUri(rewrittenUrl);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}}
|
||||
onFileDownload={(event) => {
|
||||
if (!importRecordingDownloads) return;
|
||||
const downloadUrl = event.nativeEvent.downloadUrl;
|
||||
if (downloadUrl) {
|
||||
void handleImportRecordingUrl(downloadUrl);
|
||||
}
|
||||
}}
|
||||
onNavigationStateChange={(navState) => {
|
||||
if (navState.url) {
|
||||
const nextUrl = importRecordingDownloads
|
||||
? navState.url
|
||||
: rewriteJrDataSystemUrl(navState.url, selectedEnvironment);
|
||||
setResolvedUri((current) => (current === nextUrl ? current : nextUrl));
|
||||
if (nextUrl !== "https://unyohub.2pd.jp/integration/succeeded.php") {
|
||||
pushHistoryEntry(nextUrl);
|
||||
}
|
||||
}
|
||||
syncCanGoBack(navState.canGoBack);
|
||||
// SPA内遷移中は白画面誤検知を防ぐためblankCountをリセット
|
||||
if (navState.loading) blankCountRef.current = 0;
|
||||
if (navState.url === "https://unyohub.2pd.jp/integration/succeeded.php") {
|
||||
webViewRef.current?.goBack();
|
||||
if (!hasAlerted.current) {
|
||||
hasAlerted.current = true;
|
||||
Alert.alert("鉄道運用HUBへの投稿完了", "運用HUBからのこのアプリへのデータ反映には暫く時間がかかりますので、しばらくお待ちください。", [
|
||||
{ text: "完了" },
|
||||
]);
|
||||
}
|
||||
}
|
||||
}}
|
||||
onMessage={(event) => {
|
||||
const { data } = event.nativeEvent;
|
||||
let parsed: any;
|
||||
try {
|
||||
parsed = JSON.parse(data);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
const { type } = parsed;
|
||||
if (type === "importRecordingDownload") {
|
||||
void handleImportRecordingText(String(parsed.text ?? ""));
|
||||
return;
|
||||
}
|
||||
if (type === "importRecordingDownloadError") {
|
||||
Alert.alert(
|
||||
"録画データを取得できませんでした",
|
||||
parsed.message || "ダウンロード内容を読み取れませんでした。",
|
||||
);
|
||||
return;
|
||||
}
|
||||
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 === "printHtml") {
|
||||
(async () => {
|
||||
try {
|
||||
const path = FileSystem.cacheDirectory + "diagram.html";
|
||||
await FileSystem.writeAsStringAsync(path, parsed.html, { encoding: FileSystem.EncodingType.UTF8 });
|
||||
const ok = await Sharing.isAvailableAsync();
|
||||
if (ok) {
|
||||
await Sharing.shareAsync(path, { mimeType: "text/html", dialogTitle: "ダイヤグラムを共有" });
|
||||
}
|
||||
} catch (e) {
|
||||
Alert.alert("エラー", "PDF出力の準備に失敗しました。");
|
||||
}
|
||||
})();
|
||||
return;
|
||||
}
|
||||
if (type === "back") return handleWebViewBack();
|
||||
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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { useEffect, useRef, useState } from "react";
|
||||
import { createStackNavigator } from "@react-navigation/stack";
|
||||
import { useWindowDimensions, Platform, useColorScheme } from "react-native";
|
||||
import Constants from "expo-constants";
|
||||
import { useResponsive } from "@/lib/responsive";
|
||||
|
||||
import { Dimensions, StatusBar } from "react-native";
|
||||
|
||||
@@ -17,31 +18,53 @@ import Setting from "@/components/Settings/settings";
|
||||
import { useFavoriteStation } from "@/stateBox/useFavoriteStation";
|
||||
import { optionData } from "@/lib/stackOption";
|
||||
import { AllTrainDiagramView } from "@/components/AllTrainDiagramView";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import { useNavigation, useIsFocused } from "@react-navigation/native";
|
||||
import { news } from "@/config/newsUpdate";
|
||||
import { useBottomTabBarHeight } from "@react-navigation/bottom-tabs";
|
||||
import GeneralWebView from "@/GeneralWebView";
|
||||
import { StationDiagramView } from "@/components/StationDiagram/StationDiagramView";
|
||||
import * as Sentry from "@sentry/react-native";
|
||||
const Stack = createStackNavigator();
|
||||
|
||||
export function MenuPage() {
|
||||
const { favoriteStation, setFavoriteStation } = useFavoriteStation();
|
||||
const { height, width } = useWindowDimensions();
|
||||
const { verticalScale } = useResponsive();
|
||||
const tabBarHeight = useBottomTabBarHeight();
|
||||
const navigation = useNavigation<any>();
|
||||
const { addListener } = navigation;
|
||||
const isFocused = useIsFocused();
|
||||
const isDark = useColorScheme() === "dark";
|
||||
const bgColor = isDark ? "#1c1c1e" : "#ffffff";
|
||||
useEffect(() => {
|
||||
AS.getItem(STORAGE_KEYS.START_PAGE)
|
||||
.then((res) => {
|
||||
if (res == "true") navigation.navigate("positions");
|
||||
})
|
||||
.catch((e) => {
|
||||
//6.0以降false
|
||||
AS.setItem(STORAGE_KEYS.START_PAGE, "false");
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
Sentry.addBreadcrumb({
|
||||
category: "topMenu.screen",
|
||||
level: "info",
|
||||
message: "top menu mounted",
|
||||
});
|
||||
return () => {
|
||||
Sentry.addBreadcrumb({
|
||||
category: "topMenu.screen",
|
||||
level: "info",
|
||||
message: "top menu unmounted",
|
||||
});
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
Sentry.addBreadcrumb({
|
||||
category: "topMenu.screen",
|
||||
level: "info",
|
||||
message: isFocused ? "top menu focused" : "top menu blurred",
|
||||
data: {
|
||||
width,
|
||||
height,
|
||||
},
|
||||
});
|
||||
}, [height, isFocused, width]);
|
||||
|
||||
useEffect(() => {
|
||||
//ニュース表示
|
||||
AS.getItem(STORAGE_KEYS.NEWS_STATUS)
|
||||
.then((d) => {
|
||||
@@ -53,10 +76,20 @@ export function MenuPage() {
|
||||
if (isSetIcon == "true") SheetManager.show("TrainIconUpdate");
|
||||
})
|
||||
.catch((error) => logger.error("Error fetching icon setting:", error));
|
||||
|
||||
return undefined;
|
||||
}, []);
|
||||
|
||||
const scrollRef = useRef(null);
|
||||
const [mapMode, setMapMode] = useState(false);
|
||||
useEffect(() => {
|
||||
Sentry.setContext("top_menu_screen", {
|
||||
focused: isFocused,
|
||||
width,
|
||||
height,
|
||||
mapMode,
|
||||
});
|
||||
}, [height, isFocused, mapMode, width]);
|
||||
const [mapHeight, setMapHeight] = useState(0);
|
||||
const mapHeightRef = useRef(0);
|
||||
const favoriteStationRef = useRef(favoriteStation);
|
||||
@@ -65,8 +98,8 @@ export function MenuPage() {
|
||||
height -
|
||||
tabBarHeight +
|
||||
(Platform.OS == "android" ? Constants.statusBarHeight : 0) -
|
||||
100 -
|
||||
((((width / 100) * 80) / 20) * 9 + 10 + 30);
|
||||
verticalScale(100) -
|
||||
((((width / 100) * 80) / 20) * 9 + verticalScale(10) + verticalScale(30));
|
||||
setMapHeight(MapHeight);
|
||||
mapHeightRef.current = MapHeight;
|
||||
}, [height, tabBarHeight, width]);
|
||||
@@ -82,9 +115,26 @@ export function MenuPage() {
|
||||
setMapFullHeight(MapFullHeight);
|
||||
}, [height, tabBarHeight, width]);
|
||||
useEffect(() => {
|
||||
const unsubscribe = addListener("tabPress", (e) => {
|
||||
const unsubscribe = addListener("tabPress", (e: any) => {
|
||||
Sentry.addBreadcrumb({
|
||||
category: "topMenu.screen",
|
||||
level: "info",
|
||||
message: "top menu tabPress handler",
|
||||
data: {
|
||||
focused: navigation.isFocused(),
|
||||
stackIndex: stackNavRef.current?.getState()?.index ?? null,
|
||||
mapMode,
|
||||
},
|
||||
});
|
||||
if (navigation.isFocused() && stackNavRef.current) {
|
||||
if (stackNavRef.current.getState()?.index > 0) {
|
||||
e.preventDefault();
|
||||
stackNavRef.current.goBack();
|
||||
return;
|
||||
}
|
||||
}
|
||||
scrollRef.current?.scrollTo({
|
||||
y: mapHeightRef.current - 80,
|
||||
y: mapHeightRef.current - verticalScale(80),
|
||||
animated: true,
|
||||
});
|
||||
setMapMode(false);
|
||||
@@ -104,10 +154,45 @@ export function MenuPage() {
|
||||
|
||||
return unsubscribe;
|
||||
}, [navigation]);
|
||||
const stackNavRef = useRef<any>(null);
|
||||
|
||||
return (
|
||||
<Stack.Navigator
|
||||
id={null}
|
||||
screenOptions={{ contentStyle: { backgroundColor: bgColor } }}
|
||||
detachInactiveScreens={false}
|
||||
screenOptions={{ cardStyle: { backgroundColor: bgColor } }}
|
||||
screenListeners={({ route, navigation: stackNav }) => {
|
||||
stackNavRef.current = stackNav;
|
||||
return {
|
||||
focus: () => {
|
||||
Sentry.addBreadcrumb({
|
||||
category: "topMenu.screen",
|
||||
level: "info",
|
||||
message: "top menu stack focus",
|
||||
data: { route: route.name },
|
||||
});
|
||||
},
|
||||
blur: () => {
|
||||
Sentry.addBreadcrumb({
|
||||
category: "topMenu.screen",
|
||||
level: "info",
|
||||
message: "top menu stack blur",
|
||||
data: { route: route.name },
|
||||
});
|
||||
},
|
||||
state: () => {
|
||||
Sentry.addBreadcrumb({
|
||||
category: "topMenu.screen",
|
||||
level: "info",
|
||||
message: "top menu stack state",
|
||||
data: {
|
||||
route: route.name,
|
||||
stackIndex: stackNav.getState()?.index ?? null,
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
}}
|
||||
>
|
||||
<Stack.Screen
|
||||
name="menu"
|
||||
|
||||
@@ -175,7 +175,63 @@ export const API_ENDPOINTS = {
|
||||
};
|
||||
```
|
||||
|
||||
## 📱 ビルド
|
||||
## � 外部からのアプリ起動(ディープリンク)
|
||||
|
||||
本アプリは複数の方法で外部から起動・画面遷移できます。
|
||||
|
||||
### カスタムURLスキーム
|
||||
|
||||
スキーム `jrshikoku://` を使って特定の画面を直接開くことができます。
|
||||
|
||||
| URL | 遷移先 |
|
||||
|---|---|
|
||||
| `jrshikoku://open/felica` | FeliCa履歴ページ |
|
||||
| `jrshikoku://open/traininfo` | 遅延速報EX |
|
||||
| `jrshikoku://open/operation` | 運行情報 |
|
||||
| `jrshikoku://open/settings` | 設定 |
|
||||
| `jrshikoku://open/topmenu` | トップメニュー |
|
||||
| `jrshikoku://positions/apps` | 走行位置 |
|
||||
|
||||
URLの処理は `App.tsx` の `routeFromUrl` で実装されています。
|
||||
|
||||
### Androidウィジェット
|
||||
|
||||
ホーム画面に配置可能なウィジェットからアプリを起動できます。
|
||||
|
||||
| ウィジェット名 | 説明 |
|
||||
|---|---|
|
||||
| `JR_shikoku_train_info` | 遅延速報EX |
|
||||
| `JR_shikoku_apps_shortcut` | クイックアクセス(各機能へのショートカットタイル) |
|
||||
| `JR_shikoku_felica_balance` | ICカード残高表示 |
|
||||
| `JR_shikoku_info` | 運行情報 |
|
||||
| `JR_shikoku_train_strange` | 怪レい列車 |
|
||||
|
||||
ウィジェットのタップ時は `jrshikoku://` スキームでアプリ内画面へ遷移します。
|
||||
実装: `components/AndroidWidget/widget-task-handler.tsx`
|
||||
|
||||
### iOSウィジェット(WidgetKit)
|
||||
|
||||
WidgetKit拡張としてホーム画面ウィジェットを提供しています。Live Activities にも対応しています。
|
||||
設定: `targets/widget/Info.plist`
|
||||
|
||||
### プッシュ通知
|
||||
|
||||
通知タップ時に対応する画面へ遷移します。
|
||||
|
||||
| アクション | 遷移先 |
|
||||
|---|---|
|
||||
| `delay-ex` | 遅延速報EX |
|
||||
| `strange-train` | 走行位置(怪レい列車) |
|
||||
| `information` | 運行情報 |
|
||||
|
||||
実装: `stateBox/useNotifications.tsx`
|
||||
|
||||
### NFC(FeliCa)
|
||||
|
||||
NFC-Fを利用した交通系ICカードの読み取りに対応しています(Android)。アプリ内からスキャンを開始する形式です。
|
||||
実装: `modules/expo-felica-reader/`
|
||||
|
||||
## �📱 ビルド
|
||||
|
||||
```bash
|
||||
# APKビルド(Android)
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import React, { FC } from "react";
|
||||
import { Platform, StatusBar, useColorScheme } from "react-native";
|
||||
import { Platform, StatusBar } from "react-native";
|
||||
|
||||
const StatusbarDetect: FC = () => {
|
||||
const isDark = useColorScheme() === "dark";
|
||||
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;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useCallback, useEffect, useRef } from "react";
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { createStackNavigator } from "@react-navigation/stack";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import { useIsFocused, useNavigation } from "@react-navigation/native";
|
||||
import { useColorScheme } from "react-native";
|
||||
import Apps from "./components/Apps";
|
||||
import TrainBase from "./components/trainbaseview";
|
||||
@@ -13,15 +13,20 @@ import { useCurrentTrain } from "./stateBox/useCurrentTrain";
|
||||
import { useTrainMenu } from "./stateBox/useTrainMenu";
|
||||
import { AS } from "./storageControl";
|
||||
import { news } from "./config/newsUpdate";
|
||||
import { Linking, Platform } from "react-native";
|
||||
import { InteractionManager, Linking, Platform, View } from "react-native";
|
||||
import GeneralWebView from "./GeneralWebView";
|
||||
import { StationDiagramView } from "@/components/StationDiagram/StationDiagramView";
|
||||
import { positionsLifecycleRef, positionsStackNavRef } from "./lib/rootNavigation";
|
||||
import * as Sentry from "@sentry/react-native";
|
||||
const Stack = createStackNavigator();
|
||||
export const Top = () => {
|
||||
const { webview } = useCurrentTrain();
|
||||
const { navigate, addListener, isFocused } = useNavigation();
|
||||
const navigation = useNavigation<any>();
|
||||
const { navigate, addListener } = navigation;
|
||||
const isTabFocused = useIsFocused();
|
||||
const isDark = useColorScheme() === "dark";
|
||||
const bgColor = isDark ? "#1c1c1e" : "#ffffff";
|
||||
const [hasActivatedStack, setHasActivatedStack] = useState(false);
|
||||
|
||||
//地図用
|
||||
const { mapSwitch } = useTrainMenu();
|
||||
@@ -30,36 +35,215 @@ export const Top = () => {
|
||||
mapSwitchRef.current = mapSwitch;
|
||||
}, [mapSwitch]);
|
||||
|
||||
const goToFavoriteList = () =>
|
||||
const goToFavoriteList = (event?: any) => {
|
||||
if (!hasActivatedStack) {
|
||||
event?.preventDefault?.();
|
||||
Sentry.addBreadcrumb({
|
||||
category: "positions.stack",
|
||||
level: "info",
|
||||
message: "positions tabLongPress ignored before stack activation",
|
||||
});
|
||||
return;
|
||||
}
|
||||
navigate("positions", { screen: "favoriteList" });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = addListener("tabLongPress", goToFavoriteList);
|
||||
return unsubscribe;
|
||||
}, []);
|
||||
|
||||
const goToTrainMenu = useCallback(() => {
|
||||
useEffect(() => {
|
||||
Sentry.addBreadcrumb({
|
||||
category: "positions.stack",
|
||||
level: "info",
|
||||
message: "positions stack mounted",
|
||||
});
|
||||
return () => {
|
||||
Sentry.addBreadcrumb({
|
||||
category: "positions.stack",
|
||||
level: "info",
|
||||
message: "positions stack unmounted",
|
||||
});
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (hasActivatedStack) return;
|
||||
Sentry.addBreadcrumb({
|
||||
category: "positions.stack",
|
||||
level: "info",
|
||||
message: "positions stack activation scheduled",
|
||||
data: {
|
||||
reason: isTabFocused ? "first_focus" : "background_prewarm",
|
||||
},
|
||||
});
|
||||
let cancelled = false;
|
||||
const activate = () => {
|
||||
if (cancelled) return;
|
||||
Sentry.addBreadcrumb({
|
||||
category: "positions.stack",
|
||||
level: "info",
|
||||
message: "positions stack activation committed",
|
||||
data: {
|
||||
reason: isTabFocused ? "first_focus" : "background_prewarm",
|
||||
},
|
||||
});
|
||||
setHasActivatedStack(true);
|
||||
};
|
||||
|
||||
if (isTabFocused) {
|
||||
const task = InteractionManager.runAfterInteractions(() => {
|
||||
setTimeout(activate, 0);
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
task.cancel?.();
|
||||
};
|
||||
}
|
||||
|
||||
const timer = setTimeout(activate, 300);
|
||||
return () => {
|
||||
cancelled = true;
|
||||
clearTimeout(timer);
|
||||
};
|
||||
}, [hasActivatedStack, isTabFocused]);
|
||||
|
||||
useEffect(() => {
|
||||
Sentry.setContext("positions_stack", {
|
||||
focused: isTabFocused,
|
||||
activated: hasActivatedStack,
|
||||
mapSwitch,
|
||||
});
|
||||
}, [hasActivatedStack, isTabFocused, mapSwitch]);
|
||||
|
||||
const stackNavRef = positionsStackNavRef;
|
||||
|
||||
useEffect(() => {
|
||||
positionsLifecycleRef.current = {
|
||||
...positionsLifecycleRef.current,
|
||||
deactivateStack: null,
|
||||
};
|
||||
return () => {
|
||||
positionsLifecycleRef.current = {
|
||||
...positionsLifecycleRef.current,
|
||||
deactivateStack: null,
|
||||
};
|
||||
};
|
||||
}, []);
|
||||
|
||||
const goToTrainMenu = useCallback((e: any) => {
|
||||
const stackNav = stackNavRef.current;
|
||||
Sentry.addBreadcrumb({
|
||||
category: "positions.stack",
|
||||
level: "info",
|
||||
message: "positions tabPress handler",
|
||||
data: {
|
||||
platform: Platform.OS,
|
||||
focused: isTabFocused,
|
||||
mapSwitch: mapSwitchRef.current,
|
||||
stackIndex: stackNav?.getState()?.index ?? null,
|
||||
},
|
||||
});
|
||||
if (Platform.OS === "web") {
|
||||
Linking.openURL("https://train.jr-shikoku.co.jp/");
|
||||
setTimeout(() => navigate("topMenu", { screen: "menu" }), 100);
|
||||
return;
|
||||
}
|
||||
if (!isFocused()) navigate("positions", { screen: "Apps" });
|
||||
else if (mapSwitchRef.current == "true")
|
||||
if (!hasActivatedStack || !stackNav) {
|
||||
e?.preventDefault?.();
|
||||
Sentry.addBreadcrumb({
|
||||
category: "positions.stack",
|
||||
level: "info",
|
||||
message: "positions tabPress ignored before stack activation",
|
||||
data: {
|
||||
focused: isTabFocused,
|
||||
activated: hasActivatedStack,
|
||||
hasStackNav: !!stackNav,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!isTabFocused) return;
|
||||
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;
|
||||
}, [isFocused, navigate, webview]);
|
||||
}, [hasActivatedStack, isTabFocused, navigate, webview]);
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = addListener("tabPress", goToTrainMenu);
|
||||
return unsubscribe;
|
||||
}, [addListener, goToTrainMenu]);
|
||||
|
||||
if (!hasActivatedStack) {
|
||||
return <View style={{ flex: 1, backgroundColor: bgColor }} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack.Navigator
|
||||
id={null}
|
||||
screenOptions={{ contentStyle: { backgroundColor: bgColor } }}
|
||||
detachInactiveScreens={false}
|
||||
screenOptions={{ cardStyle: { backgroundColor: bgColor } }}
|
||||
screenListeners={({ route, navigation: stackNav }) => {
|
||||
stackNavRef.current = stackNav;
|
||||
return {
|
||||
focus: () => {
|
||||
Sentry.addBreadcrumb({
|
||||
category: "positions.stack",
|
||||
level: "info",
|
||||
message: "positions stack focus",
|
||||
data: { route: route.name },
|
||||
});
|
||||
},
|
||||
blur: () => {
|
||||
Sentry.addBreadcrumb({
|
||||
category: "positions.stack",
|
||||
level: "info",
|
||||
message: "positions stack blur",
|
||||
data: { route: route.name },
|
||||
});
|
||||
},
|
||||
transitionStart: () => {
|
||||
Sentry.addBreadcrumb({
|
||||
category: "positions.stack",
|
||||
level: "info",
|
||||
message: "positions stack transitionStart",
|
||||
data: {
|
||||
route: route.name,
|
||||
stackIndex: stackNav.getState()?.index ?? null,
|
||||
},
|
||||
});
|
||||
},
|
||||
transitionEnd: () => {
|
||||
Sentry.addBreadcrumb({
|
||||
category: "positions.stack",
|
||||
level: "info",
|
||||
message: "positions stack transitionEnd",
|
||||
data: {
|
||||
route: route.name,
|
||||
stackIndex: stackNav.getState()?.index ?? null,
|
||||
},
|
||||
});
|
||||
},
|
||||
state: () => {
|
||||
Sentry.addBreadcrumb({
|
||||
category: "positions.stack",
|
||||
level: "info",
|
||||
message: "positions stack state",
|
||||
data: {
|
||||
route: route.name,
|
||||
stackIndex: stackNav.getState()?.index ?? null,
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
}}
|
||||
>
|
||||
<Stack.Screen
|
||||
name="Apps"
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"android",
|
||||
"web"
|
||||
],
|
||||
"version": "7.0.0",
|
||||
"version": "7.1.0",
|
||||
"userInterfaceStyle": "automatic",
|
||||
"orientation": "default",
|
||||
"icon": "./assets/icons/s8600.png",
|
||||
@@ -24,7 +24,7 @@
|
||||
"**/*"
|
||||
],
|
||||
"ios": {
|
||||
"buildNumber": "60",
|
||||
"buildNumber": "66",
|
||||
"supportsTablet": true,
|
||||
"bundleIdentifier": "jrshikokuinfo.xprocess.hrkn",
|
||||
"appleTeamId": "54CRDT797G",
|
||||
@@ -33,13 +33,18 @@
|
||||
},
|
||||
"infoPlist": {
|
||||
"NFCReaderUsageDescription": "To read FeliCa card",
|
||||
"NSPhotoLibraryUsageDescription": "運行情報画像を共有または保存するために写真ライブラリへのアクセスを使用します。",
|
||||
"NSPhotoLibraryAddUsageDescription": "運行情報画像を写真ライブラリに保存するために使用します。",
|
||||
"com.apple.developer.nfc.readersession.felica.systemcodes": [
|
||||
"0003",
|
||||
"FE00"
|
||||
],
|
||||
"ITSAppUsesNonExemptEncryption": false,
|
||||
"NSSupportsLiveActivities": true,
|
||||
"NSSupportsLiveActivitiesFrequentUpdates": true
|
||||
"NSSupportsLiveActivitiesFrequentUpdates": true,
|
||||
"UIBackgroundModes": [
|
||||
"audio"
|
||||
]
|
||||
},
|
||||
"entitlements": {
|
||||
"com.apple.developer.nfc.readersession.formats": [
|
||||
@@ -52,7 +57,7 @@
|
||||
},
|
||||
"android": {
|
||||
"package": "jrshikokuinfo.xprocess.hrkn",
|
||||
"versionCode": 30,
|
||||
"versionCode": 32,
|
||||
"intentFilters": [
|
||||
{
|
||||
"action": "VIEW",
|
||||
@@ -105,12 +110,14 @@
|
||||
},
|
||||
"plugins": [
|
||||
"./plugins/with-android-local-properties",
|
||||
"./plugins/with-nfc-widget-guard",
|
||||
"@bacons/apple-targets",
|
||||
[
|
||||
"expo-font",
|
||||
{
|
||||
"fonts": [
|
||||
"./assets/fonts/jr-nishi.otf",
|
||||
"./assets/fonts/JR-WEST-PLUS.otf",
|
||||
"./assets/fonts/DelaGothicOne-Regular.ttf",
|
||||
"./assets/fonts/JNRfont_pict.ttf",
|
||||
"./assets/fonts/DiaPro-Regular.otf"
|
||||
@@ -140,17 +147,7 @@
|
||||
"minWidth": "70dp",
|
||||
"minHeight": "50dp",
|
||||
"description": "JR四国列車遅延速報EXのウィジェットです。30分ごとに自動更新します。タッチすると強制更新します。",
|
||||
"previewImage": "./assets/icon.png",
|
||||
"updatePeriodMillis": 1800000,
|
||||
"resizeMode": "horizontal|vertical"
|
||||
},
|
||||
{
|
||||
"name": "JR_shikoku_train_strange",
|
||||
"label": "怪レい列車",
|
||||
"minWidth": "70dp",
|
||||
"minHeight": "50dp",
|
||||
"description": "JR四国怪レい列車BOTのウィジェットです。30分ごとに自動更新します。タッチすると強制更新します。",
|
||||
"previewImage": "./assets/icon.png",
|
||||
"previewImage": "./assets/widgetResource/JR_shikoku_train_info.jpg",
|
||||
"updatePeriodMillis": 1800000,
|
||||
"resizeMode": "horizontal|vertical"
|
||||
},
|
||||
@@ -160,7 +157,7 @@
|
||||
"minWidth": "70dp",
|
||||
"minHeight": "50dp",
|
||||
"description": "JR四国運行情報のウィジェットです。30分ごとに自動更新します。タッチすると強制更新します。",
|
||||
"previewImage": "./assets/icon.png",
|
||||
"previewImage": "./assets/widgetResource/JR_shikoku_info.jpg",
|
||||
"updatePeriodMillis": 1800000,
|
||||
"resizeMode": "horizontal|vertical"
|
||||
},
|
||||
@@ -170,7 +167,7 @@
|
||||
"minWidth": "70dp",
|
||||
"minHeight": "50dp",
|
||||
"description": "JR四国非公式アプリの各種リンクを表示するウィジェットです。",
|
||||
"previewImage": "./assets/icon.png",
|
||||
"previewImage": "./assets/widgetResource/JR_shikoku_apps_shortcut.jpg",
|
||||
"updatePeriodMillis": 1800000,
|
||||
"resizeMode": "horizontal|vertical"
|
||||
},
|
||||
@@ -180,7 +177,7 @@
|
||||
"minWidth": "70dp",
|
||||
"minHeight": "50dp",
|
||||
"description": "Felica対応ICカードの残高をホーム画面に表示するウィジェットです。タップでスキャン画面を開きます。",
|
||||
"previewImage": "./assets/icon.png",
|
||||
"previewImage": "./assets/widgetResource/JR_shikoku_felica_balance.jpg",
|
||||
"updatePeriodMillis": 1800000,
|
||||
"resizeMode": "horizontal|vertical"
|
||||
}
|
||||
@@ -509,6 +506,454 @@
|
||||
"foregroundImage": "./assets/icons/ef210.png",
|
||||
"backgroundColor": "#001413"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "32ns",
|
||||
"ios": "./assets/icons/s32ns.png",
|
||||
"android": {
|
||||
"foregroundImage": "./assets/icons/s32ns.png",
|
||||
"backgroundColor": "#001413"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "32s",
|
||||
"ios": "./assets/icons/s32s.png",
|
||||
"android": {
|
||||
"foregroundImage": "./assets/icons/s32s.png",
|
||||
"backgroundColor": "#001413"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "32kpuy1",
|
||||
"ios": "./assets/icons/s32kpuy1.png",
|
||||
"android": {
|
||||
"foregroundImage": "./assets/icons/s32kpuy1.png",
|
||||
"backgroundColor": "#001413"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "32kpuy2",
|
||||
"ios": "./assets/icons/s32kpuy2.png",
|
||||
"android": {
|
||||
"foregroundImage": "./assets/icons/s32kpuy2.png",
|
||||
"backgroundColor": "#001413"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "32thtk",
|
||||
"ios": "./assets/icons/s32thtk.png",
|
||||
"android": {
|
||||
"foregroundImage": "./assets/icons/s32thtk.png",
|
||||
"backgroundColor": "#001413"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "32oni1",
|
||||
"ios": "./assets/icons/s32oni1.png",
|
||||
"android": {
|
||||
"foregroundImage": "./assets/icons/s32oni1.png",
|
||||
"backgroundColor": "#001413"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "32oni1k",
|
||||
"ios": "./assets/icons/s32oni1k.png",
|
||||
"android": {
|
||||
"foregroundImage": "./assets/icons/s32oni1k.png",
|
||||
"backgroundColor": "#001413"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "32to4",
|
||||
"ios": "./assets/icons/s32to4.png",
|
||||
"android": {
|
||||
"foregroundImage": "./assets/icons/s32to4.png",
|
||||
"backgroundColor": "#001413"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "32toai",
|
||||
"ios": "./assets/icons/s32to_ai.png",
|
||||
"android": {
|
||||
"foregroundImage": "./assets/icons/s32to_ai.png",
|
||||
"backgroundColor": "#001413"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "54s",
|
||||
"ios": "./assets/icons/s54s.png",
|
||||
"android": {
|
||||
"foregroundImage": "./assets/icons/s54s.png",
|
||||
"backgroundColor": "#001413"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "54to0ys",
|
||||
"ios": "./assets/icons/s54to0ys.png",
|
||||
"android": {
|
||||
"foregroundImage": "./assets/icons/s54to0ys.png",
|
||||
"backgroundColor": "#001413"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "54nany1",
|
||||
"ios": "./assets/icons/s54nany1.png",
|
||||
"android": {
|
||||
"foregroundImage": "./assets/icons/s54nany1.png",
|
||||
"backgroundColor": "#001413"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "54nany2",
|
||||
"ios": "./assets/icons/s54nany2.png",
|
||||
"android": {
|
||||
"foregroundImage": "./assets/icons/s54nany2.png",
|
||||
"backgroundColor": "#001413"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "54smek1",
|
||||
"ios": "./assets/icons/s32smek1.png",
|
||||
"android": {
|
||||
"foregroundImage": "./assets/icons/s32smek1.png",
|
||||
"backgroundColor": "#001413"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "40s",
|
||||
"ios": "./assets/icons/s40.png",
|
||||
"android": {
|
||||
"foregroundImage": "./assets/icons/s40.png",
|
||||
"backgroundColor": "#001413"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "40w",
|
||||
"ios": "./assets/icons/w40.png",
|
||||
"android": {
|
||||
"foregroundImage": "./assets/icons/w40.png",
|
||||
"backgroundColor": "#001413"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "185cm",
|
||||
"ios": "./assets/icons/s185cm.png",
|
||||
"android": {
|
||||
"foregroundImage": "./assets/icons/s185cm.png",
|
||||
"backgroundColor": "#001413"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "185g",
|
||||
"ios": "./assets/icons/s185g.png",
|
||||
"android": {
|
||||
"foregroundImage": "./assets/icons/s185g.png",
|
||||
"backgroundColor": "#001413"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "185tu_uzu",
|
||||
"ios": "./assets/icons/s185tu_uzu.png",
|
||||
"android": {
|
||||
"foregroundImage": "./assets/icons/s185tu_uzu.png",
|
||||
"backgroundColor": "#001413"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "185ap1",
|
||||
"ios": "./assets/icons/s185ap1.png",
|
||||
"android": {
|
||||
"foregroundImage": "./assets/icons/s185ap1.png",
|
||||
"backgroundColor": "#001413"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "185mm2",
|
||||
"ios": "./assets/icons/s185mm2.png",
|
||||
"android": {
|
||||
"foregroundImage": "./assets/icons/s185mm2.png",
|
||||
"backgroundColor": "#001413"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "185ym2",
|
||||
"ios": "./assets/icons/s185ym2.png",
|
||||
"android": {
|
||||
"foregroundImage": "./assets/icons/s185ym2.png",
|
||||
"backgroundColor": "#001413"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "1200",
|
||||
"ios": "./assets/icons/s1200.png",
|
||||
"android": {
|
||||
"foregroundImage": "./assets/icons/s1200.png",
|
||||
"backgroundColor": "#001413"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "1201",
|
||||
"ios": "./assets/icons/s1201.png",
|
||||
"android": {
|
||||
"foregroundImage": "./assets/icons/s1201.png",
|
||||
"backgroundColor": "#001413"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "1501",
|
||||
"ios": "./assets/icons/s1501.png",
|
||||
"android": {
|
||||
"foregroundImage": "./assets/icons/s1501.png",
|
||||
"backgroundColor": "#001413"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "1550",
|
||||
"ios": "./assets/icons/s1550.png",
|
||||
"android": {
|
||||
"foregroundImage": "./assets/icons/s1550.png",
|
||||
"backgroundColor": "#001413"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "1551",
|
||||
"ios": "./assets/icons/s1551.png",
|
||||
"android": {
|
||||
"foregroundImage": "./assets/icons/s1551.png",
|
||||
"backgroundColor": "#001413"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "2000uwa",
|
||||
"ios": "./assets/icons/s2000_uwa.png",
|
||||
"android": {
|
||||
"foregroundImage": "./assets/icons/s2000_uwa.png",
|
||||
"backgroundColor": "#001413"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "2000nl",
|
||||
"ios": "./assets/icons/s2000nl.png",
|
||||
"android": {
|
||||
"foregroundImage": "./assets/icons/s2000nl.png",
|
||||
"backgroundColor": "#001413"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "2000-3",
|
||||
"ios": "./assets/icons/s2000-3.png",
|
||||
"android": {
|
||||
"foregroundImage": "./assets/icons/s2000-3.png",
|
||||
"backgroundColor": "#001413"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "2000ganp1",
|
||||
"ios": "./assets/icons/s2000ganp1.png",
|
||||
"android": {
|
||||
"foregroundImage": "./assets/icons/s2000ganp1.png",
|
||||
"backgroundColor": "#001413"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "2600apr",
|
||||
"ios": "./assets/icons/s2600apr.png",
|
||||
"android": {
|
||||
"foregroundImage": "./assets/icons/s2600apr.png",
|
||||
"backgroundColor": "#001413"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "2600apb",
|
||||
"ios": "./assets/icons/s2600apb.png",
|
||||
"android": {
|
||||
"foregroundImage": "./assets/icons/s2600apb.png",
|
||||
"backgroundColor": "#001413"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "2700asi",
|
||||
"ios": "./assets/icons/s2700_asi.png",
|
||||
"android": {
|
||||
"foregroundImage": "./assets/icons/s2700_asi.png",
|
||||
"backgroundColor": "#001413"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "2700smn",
|
||||
"ios": "./assets/icons/s2700_smn.png",
|
||||
"android": {
|
||||
"foregroundImage": "./assets/icons/s2700_smn.png",
|
||||
"backgroundColor": "#001413"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "2700uzu",
|
||||
"ios": "./assets/icons/s2700_uzu.png",
|
||||
"android": {
|
||||
"foregroundImage": "./assets/icons/s2700_uzu.png",
|
||||
"backgroundColor": "#001413"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "2700apy1",
|
||||
"ios": "./assets/icons/s2700apy1.png",
|
||||
"android": {
|
||||
"foregroundImage": "./assets/icons/s2700apy1.png",
|
||||
"backgroundColor": "#001413"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "2700apr1",
|
||||
"ios": "./assets/icons/s2700apr1.png",
|
||||
"android": {
|
||||
"foregroundImage": "./assets/icons/s2700apr1.png",
|
||||
"backgroundColor": "#001413"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "3600",
|
||||
"ios": "./assets/icons/s3600.png",
|
||||
"android": {
|
||||
"foregroundImage": "./assets/icons/s3600.png",
|
||||
"backgroundColor": "#001413"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "6000f",
|
||||
"ios": "./assets/icons/s6000f.png",
|
||||
"android": {
|
||||
"foregroundImage": "./assets/icons/s6000f.png",
|
||||
"backgroundColor": "#001413"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "7001",
|
||||
"ios": "./assets/icons/s7001.png",
|
||||
"android": {
|
||||
"foregroundImage": "./assets/icons/s7001.png",
|
||||
"backgroundColor": "#001413"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "8001nr",
|
||||
"ios": "./assets/icons/s8001nr.png",
|
||||
"android": {
|
||||
"foregroundImage": "./assets/icons/s8001nr.png",
|
||||
"backgroundColor": "#001413"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "9000",
|
||||
"ios": "./assets/icons/s9000.png",
|
||||
"android": {
|
||||
"foregroundImage": "./assets/icons/s9000.png",
|
||||
"backgroundColor": "#001413"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "9640ht",
|
||||
"ios": "./assets/icons/tosa9640ht.png",
|
||||
"android": {
|
||||
"foregroundImage": "./assets/icons/tosa9640ht.png",
|
||||
"backgroundColor": "#001413"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "9640mo1",
|
||||
"ios": "./assets/icons/tosa9640mo1.png",
|
||||
"android": {
|
||||
"foregroundImage": "./assets/icons/tosa9640mo1.png",
|
||||
"backgroundColor": "#001413"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "9640mo2",
|
||||
"ios": "./assets/icons/tosa9640mo2.png",
|
||||
"android": {
|
||||
"foregroundImage": "./assets/icons/tosa9640mo2.png",
|
||||
"backgroundColor": "#001413"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "9640tyg",
|
||||
"ios": "./assets/icons/tosa9640tyg.png",
|
||||
"android": {
|
||||
"foregroundImage": "./assets/icons/tosa9640tyg.png",
|
||||
"backgroundColor": "#001413"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "9640tyb",
|
||||
"ios": "./assets/icons/tosa9640tyb.png",
|
||||
"android": {
|
||||
"foregroundImage": "./assets/icons/tosa9640tyb.png",
|
||||
"backgroundColor": "#001413"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "9640jbl",
|
||||
"ios": "./assets/icons/tosa9640jbl.png",
|
||||
"android": {
|
||||
"foregroundImage": "./assets/icons/tosa9640jbl.png",
|
||||
"backgroundColor": "#001413"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "W741",
|
||||
"ios": "./assets/icons/w741.png",
|
||||
"android": {
|
||||
"foregroundImage": "./assets/icons/w741.png",
|
||||
"backgroundColor": "#001413"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "EF210a",
|
||||
"ios": "./assets/icons/ef210a.png",
|
||||
"android": {
|
||||
"foregroundImage": "./assets/icons/ef210a.png",
|
||||
"backgroundColor": "#001413"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "EF210n",
|
||||
"ios": "./assets/icons/ef210n.png",
|
||||
"android": {
|
||||
"foregroundImage": "./assets/icons/ef210n.png",
|
||||
"backgroundColor": "#001413"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "EF210n1",
|
||||
"ios": "./assets/icons/ef210n1.png",
|
||||
"android": {
|
||||
"foregroundImage": "./assets/icons/ef210n1.png",
|
||||
"backgroundColor": "#001413"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "EF210l3",
|
||||
"ios": "./assets/icons/ef210l3.png",
|
||||
"android": {
|
||||
"foregroundImage": "./assets/icons/ef210l3.png",
|
||||
"backgroundColor": "#001413"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "T45000",
|
||||
"ios": "./assets/icons/s150001to.png",
|
||||
"android": {
|
||||
"foregroundImage": "./assets/icons/s150001to.png",
|
||||
"backgroundColor": "#001413"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Wk141",
|
||||
"ios": "./assets/icons/w141jg.png",
|
||||
"android": {
|
||||
"foregroundImage": "./assets/icons/w141jg.png",
|
||||
"backgroundColor": "#001413"
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
@@ -528,12 +973,28 @@
|
||||
"expo-web-browser",
|
||||
"expo-asset",
|
||||
"expo-sharing",
|
||||
[
|
||||
"react-native-share",
|
||||
{
|
||||
"ios": [],
|
||||
"android": [],
|
||||
"enableBase64ShareAndroid": false
|
||||
}
|
||||
],
|
||||
[
|
||||
"react-native-maps",
|
||||
{
|
||||
"iosGoogleMapsApiKey": "AIzaSyAVGDTjBkR_0wkQiNkoo5WDLhqXCjrjk8Y",
|
||||
"androidGoogleMapsApiKey": "AIzaSyAmFb-Yj033bXZWlSzNrfq_0jc1PgRrWcE"
|
||||
}
|
||||
],
|
||||
[
|
||||
"@sentry/react-native/expo",
|
||||
{
|
||||
"url": "https://sentry.io/",
|
||||
"project": "jr-shikoku-unofficial-apps",
|
||||
"organization": "xprocess-m5"
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 469 KiB |
|
After Width: | Height: | Size: 334 KiB |
|
After Width: | Height: | Size: 373 KiB |
|
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 |
@@ -0,0 +1,27 @@
|
||||
西日本方向ロゴ拡張「JR-WEST-PLUS」
|
||||
|
||||
2026/07/11 by Mameh1nata
|
||||
|
||||
「回送」とか専用の書体の方がかっこよくね?と思い制作したフォントです
|
||||
現時点で対応している種別は以下の通りです
|
||||
回送 試運転 貸切 修学旅行 救援 配給 訓練 検測 競技 検定 教習
|
||||
|
||||
ver1.0 プライベート公開
|
||||
ver1.01 修学旅行を追加
|
||||
ver1.1 救援 配給 訓練の追加
|
||||
ver1.2 競技 検定 教習の追加
|
||||
|
||||
-注意-
|
||||
・このフォントは西日本方向幕ロゴの制作者とは何の関係もありません
|
||||
・常識の範囲内で使うことをお勧めします(常識の範囲外でも自己責任でお願いします)
|
||||
・文字サイズが多少違ったりするかもしれませんがご了承ください
|
||||
・フォント初制作なのであまりにも致命的なミス以外は許してください
|
||||
|
||||
配布元: https://uu.getuploader.com/mameh1nata_dustbox/download/1
|
||||
|
||||
アプリ同梱版の調整:
|
||||
・JR-Nishi と字面を揃えるため、各グリフの表示領域を 1040 units に正規化
|
||||
・送り幅は 930 units とし、広げた字形の右側はアプリ側で小幅の DiaPro NBSP を付加して保護
|
||||
・縦の表示領域を 836 units、ベースラインを -86〜750 に正規化
|
||||
・フォント全体の ascender / descender を JR-Nishi と同じ 781 / -118 に調整
|
||||
・文字コードと内部フォント名は元ファイルから維持
|
||||
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 97 KiB |
|
After Width: | Height: | Size: 103 KiB |
|
After Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 3.7 KiB |
@@ -1,45 +1,124 @@
|
||||
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") },
|
||||
{ "id": "32kpuuy", "name": "キハ32形かっぱうようよ号", "icon": require("./32kpuuy.png") },
|
||||
//{ "id": "32", "name": "キハ32形", "icon": require("./32.png") },
|
||||
{ "id": "32ns", "name": "キハ32形(標準色)", "icon": require("./s32ns.png") },
|
||||
{ "id": "32s", "name": "キハ32形(新塗装)", "icon": require("./s32s.png") },
|
||||
//{ "id": "32kpuuy", "name": "キハ32形かっぱうようよ号", "icon": require("./32kpuuy.png") },
|
||||
{ "id": "32kpuy1", "name": "キハ32形かっぱうようよ号①", "icon": require("./s32kpuy1.png") },
|
||||
{ "id": "32kpuy2", "name": "キハ32形かっぱうようよ号②", "icon": require("./s32kpuy2.png") },
|
||||
{ "id": "32tht", "name": "キハ32形新幹線ホビートレイン", "icon": require("./32tht.png") },
|
||||
{ "id": "32thtk", "name": "キクハ32形新幹線ホビートレイン", "icon": require("./s32thtk.png") },
|
||||
{ "id": "32oni1", "name": "キハ32形特別塗装①", "icon": require("./s32oni1.png") },
|
||||
{ "id": "32oni1k", "name": "キハ32形特別塗装①(連結)", "icon": require("./s32oni1k.png") },
|
||||
//{ "id": "32to4", "name": "キハ32形(土讃色)", "icon": require("./s32to4.png") },
|
||||
{ "id": "32toai", "name": "キクハ32形藍よしのがわトロッコ", "icon": require("./s32to_ai.png") },
|
||||
{ "id": "32at", "name": "キクハ32形アンパンマントロッコ", "icon": require("./32at.png") },
|
||||
{ "id": "54", "name": "キハ54形", "icon": require("./54.png") },
|
||||
//{ "id": "54", "name": "キハ54形", "icon": require("./54.png") },
|
||||
{ "id": "54s", "name": "キハ54形(四国色)", "icon": require("./s54s.png") },
|
||||
{ "id": "54st", "name": "キハ54形しまんトロッコ", "icon": require("./54st.png") },
|
||||
{ "id": "40", "name": "キハ40", "icon": require("./40.png") },
|
||||
//{ "id": "54to0ys", "name": "キハ54形(ゆとりすと)", "icon": require("./s54to0ys.png") },
|
||||
{ "id": "54nany1", "name": "キハ54形なんよ号①", "icon": require("./s54nany1.png") },
|
||||
{ "id": "54nany2", "name": "キハ54形なんよ号②", "icon": require("./s54nany2.png") },
|
||||
{ "id": "54smek1", "name": "キハ54形しまんトロッコ(連結)", "icon": require("./s32smek1.png") },
|
||||
//{ "id": "40", "name": "キハ40", "icon": require("./40.png") },
|
||||
{ "id": "40s", "name": "キハ40(四国色)", "icon": require("./s40.png") },
|
||||
{ "id": "40w", "name": "キハ40(JR西日本色)", "icon": require("./w40.png") },
|
||||
{ "id": "185mrt", "name": "キハ185系四国色", "icon": require("./s185_mrt.png") },
|
||||
{ "id": "185cm", "name": "キハ185系普通列車色", "icon": require("./s185cm.png") },
|
||||
{ "id": "185g", "name": "キハ185系(緑帯)", "icon": require("./s185g.png") },
|
||||
{ "id": "185tu", "name": "キハ185系剣山色", "icon": require("./s185tu.png") },
|
||||
{ "id": "185tu_uzu", "name": "キハ185系剣山色(うずしお)", "icon": require("./s185tu_uzu.png") },
|
||||
{ "id": "185iyor", "name": "キハ185系伊予灘ものがたり(赤)", "icon": require("./s185iyor.png") },
|
||||
{ "id": "185iyoy", "name": "キハ185系伊予灘ものがたり(黄)", "icon": require("./s185iyoy.png") },
|
||||
{ "id": "185toai", "name": "キハ185系藍よしのがわトロッコ", "icon": require("./s185to_ai.png") },
|
||||
{ "id": "185mm1", "name": "キハ185系四国まんなか千年ものがたり(緑)", "icon": require("./s185mm1.png") },
|
||||
{ "id": "185mm2", "name": "キハ185系四国まんなか千年ものがたり(赤)", "icon": require("./s185mm2.png") },
|
||||
{ "id": "185ym1", "name": "キハ185系時代の夜明けのものがたり(茶)", "icon": require("./s185ym1.png") },
|
||||
{ "id": "185ym2", "name": "キハ185系時代の夜明けのものがたり(緑)", "icon": require("./s185ym2.png") },
|
||||
{ "id": "185ap1", "name": "キハ185系アンパンマン", "icon": require("./s185ap1.png") },
|
||||
{ "id": "1000", "name": "1000形", "icon": require("./s1000.png") },
|
||||
{ "id": "1200n", "name": "1200形", "icon": require("./s1200n.png") },
|
||||
{ "id": "1500", "name": "1500形", "icon": require("./s1500.png") },
|
||||
{ "id": "5001", "name": "5000系(二階建て)", "icon": require("./s5001.png") },
|
||||
{ "id": "5001k", "name": "5000系(平屋側)", "icon": require("./s5001k.png") },
|
||||
{ "id": "6000p", "name": "6000系", "icon": require("./s6000p.png") },
|
||||
{ "id": "7000", "name": "7000系", "icon": require("./s7000.png") },
|
||||
{ "id": "7200", "name": "7200系", "icon": require("./s7200.png") },
|
||||
//{ "id": "1200n", "name": "1200形", "icon": require("./s1200n.png") },
|
||||
{ "id": "1200", "name": "1200形(旧塗装)", "icon": require("./s1200.png") },
|
||||
{ "id": "1201", "name": "1201形", "icon": require("./s1201.png") },
|
||||
//{ "id": "1500", "name": "1500形", "icon": require("./s1500.png") },
|
||||
{ "id": "1501", "name": "1500形 1501", "icon": require("./s1501.png") },
|
||||
{ "id": "1550", "name": "1500形 1550", "icon": require("./s1550.png") },
|
||||
{ "id": "1551", "name": "1500形 1551", "icon": require("./s1551.png") },
|
||||
{ "id": "2000asi", "name": "2000系", "icon": require("./s2000_asi.png") },
|
||||
{ "id": "2000uwa", "name": "2000系(宇和海色)", "icon": require("./s2000_uwa.png") },
|
||||
{ "id": "N2000", "name": "N2000系", "icon": require("./s2000n.png") },
|
||||
{ "id": "2000nl", "name": "N2000系(南風色)", "icon": require("./s2000nl.png") },
|
||||
{ "id": "2000-3", "name": "N2000系(試作)", "icon": require("./s2000-3.png") },
|
||||
{ "id": "2000ganp1", "name": "2000系(復活塗装)", "icon": require("./s2000ganp1.png") },
|
||||
{ "id": "2002a", "name": "2000系アンパンマン", "icon": require("./s2002a.png") },
|
||||
{ "id": "2600", "name": "2600系" , "icon": require("./s2600.png")},
|
||||
{ "id": "2600apr", "name": "2600系アンパンマン(赤)", "icon": require("./s2600apr.png") },
|
||||
{ "id": "2600apb", "name": "2600系アンパンマン(青)", "icon": require("./s2600apb.png") },
|
||||
{ "id": "2700", "name": "2700系", "icon": require("./s2700.png") },
|
||||
{ "id": "2700asi", "name": "2700系(あしずり色)", "icon": require("./s2700_asi.png") },
|
||||
{ "id": "2700smn", "name": "2700系(しまんと色)", "icon": require("./s2700_smn.png") },
|
||||
{ "id": "2700uzu", "name": "2700系(うずしお色)", "icon": require("./s2700_uzu.png") },
|
||||
{ "id": "2700apy", "name": "2700系アンパンマン(黄)", "icon": require("./s2700apy.png") },
|
||||
{ "id": "2700apr", "name": "2700系アンパンマン(赤)", "icon": require("./s2700apr.png") },
|
||||
{ "id": "2700apy1", "name": "2700系アンパンマン(黄)①", "icon": require("./s2700apy1.png") },
|
||||
{ "id": "2700apr1", "name": "2700系アンパンマン(赤)①", "icon": require("./s2700apr1.png") },
|
||||
{ "id": "3600", "name": "3600系", "icon": require("./s3600.png") },
|
||||
{ "id": "5001", "name": "5000系(二階建て)", "icon": require("./s5001.png") },
|
||||
{ "id": "5001k", "name": "5000系(平屋側)", "icon": require("./s5001k.png") },
|
||||
//{ "id": "6000p", "name": "6000系", "icon": require("./s6000p.png") },
|
||||
{ "id": "6000f", "name": "6000系(Fライナー)", "icon": require("./s6000f.png") },
|
||||
{ "id": "7000", "name": "7000系", "icon": require("./s7000.png") },
|
||||
//{ "id": "7001", "name": "7000系 7001", "icon": require("./s7001.png") },
|
||||
{ "id": "7200", "name": "7200系", "icon": require("./s7200.png") },
|
||||
{ "id": "8000no", "name": "8000系(オレンジ)", "icon": require("./s8000no.png") },
|
||||
{ "id": "8000nr", "name": "8000系(赤)", "icon": require("./s8000nr.png") },
|
||||
{ "id": "8001nr", "name": "8000系(赤) 8001", "icon": require("./s8001nr.png") },
|
||||
{ "id": "8000ap", "name": "8000系アンパンマン", "icon": require("./s8000ap.png") },
|
||||
{ "id": "8000nn", "name": "8000系リニューアル改", "icon": require("./s8000nn.png") },
|
||||
{ "id": "8600", "name": "8600系", "icon": require("./s8600.png") },
|
||||
{ "id": "9000", "name": "9000系", "icon": require("./s9000.png") },
|
||||
{ "id": "9640", "name": "9640形(白)", "icon": require("./tosa9640.png") },
|
||||
{ "id": "9640ht", "name": "9640形(赤帯)", "icon": require("./tosa9640ht.png") },
|
||||
{ "id": "9640mo1", "name": "9640形もみじ号①", "icon": require("./tosa9640mo1.png") },
|
||||
{ "id": "9640mo2", "name": "9640形もみじ号②", "icon": require("./tosa9640mo2.png") },
|
||||
{ "id": "9640tyg", "name": "9640形(緑)", "icon": require("./tosa9640tyg.png") },
|
||||
{ "id": "9640tyb", "name": "9640形(青)", "icon": require("./tosa9640tyb.png") },
|
||||
{ "id": "9640jgr", "name": "9640形オープンデッキ(緑)", "icon": require("./tosa9640jgr.png") },
|
||||
{ "id": "9640jbl", "name": "9640形オープンデッキ(青)", "icon": require("./tosa9640jbl.png") },
|
||||
{ "id": "285", "name": "285系サンライズ瀬戸", "icon": require("./w285.png") },
|
||||
{ "id": "213w", "name": "ラ・マル・ド・ボァ", "icon": require("./w213w.png") },
|
||||
{ "id": "W741", "name": "DEC741系", "icon": require("./w741.png") },
|
||||
{ "id": "Wk141", "name": "キヤ141系", "icon": require("./w141jg.png") },
|
||||
{ "id": "EF65", "name": "EF65", "icon": require("./ef65.png") },
|
||||
{ "id": "EF210", "name": "EF210", "icon": require("./ef210.png") },
|
||||
//{ "id": "EF210", "name": "EF210", "icon": require("./ef210.png") },
|
||||
{ "id": "EF210a", "name": "EF210(300番代)", "icon": require("./ef210a.png") },
|
||||
{ "id": "EF210n", "name": "EF210(新塗装)", "icon": require("./ef210n.png") },
|
||||
{ "id": "EF210n1", "name": "EF210(新塗装①)", "icon": require("./ef210n1.png") },
|
||||
{ "id": "EF210l3", "name": "EF210(L3編成)", "icon": require("./ef210l3.png") },
|
||||
{ "id": "T45000", "name": "トラ45000", "icon": require("./s150001to.png") },
|
||||
]
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 111 KiB |
|
After Width: | Height: | Size: 112 KiB |
|
After Width: | Height: | Size: 175 KiB |
|
After Width: | Height: | Size: 158 KiB |
|
After Width: | Height: | Size: 151 KiB |
|
After Width: | Height: | Size: 155 KiB |
|
After Width: | Height: | Size: 201 KiB |
|
After Width: | Height: | Size: 140 KiB |
|
After Width: | Height: | Size: 151 KiB |
|
After Width: | Height: | Size: 210 KiB |
|
After Width: | Height: | Size: 102 KiB |
|
After Width: | Height: | Size: 215 KiB |
|
After Width: | Height: | Size: 119 KiB |
|
After Width: | Height: | Size: 103 KiB |
|
After Width: | Height: | Size: 190 KiB |
|
After Width: | Height: | Size: 133 KiB |
|
After Width: | Height: | Size: 196 KiB |
|
After Width: | Height: | Size: 198 KiB |
|
After Width: | Height: | Size: 133 KiB |
|
After Width: | Height: | Size: 133 KiB |
|
After Width: | Height: | Size: 132 KiB |
|
After Width: | Height: | Size: 181 KiB |
|
After Width: | Height: | Size: 190 KiB |
|
After Width: | Height: | Size: 190 KiB |
|
After Width: | Height: | Size: 191 KiB |
|
After Width: | Height: | Size: 141 KiB |
|
After Width: | Height: | Size: 213 KiB |
|
After Width: | Height: | Size: 196 KiB |
|
After Width: | Height: | Size: 146 KiB |
|
After Width: | Height: | Size: 270 KiB |
|
After Width: | Height: | Size: 126 KiB |
|
After Width: | Height: | Size: 196 KiB |
|
After Width: | Height: | Size: 153 KiB |
|
After Width: | Height: | Size: 149 KiB |
|
After Width: | Height: | Size: 149 KiB |
|
After Width: | Height: | Size: 235 KiB |
|
After Width: | Height: | Size: 202 KiB |
|
After Width: | Height: | Size: 156 KiB |
|
After Width: | Height: | Size: 158 KiB |
|
After Width: | Height: | Size: 116 KiB |
|
After Width: | Height: | Size: 107 KiB |
|
After Width: | Height: | Size: 114 KiB |
|
After Width: | Height: | Size: 153 KiB |
|
After Width: | Height: | Size: 156 KiB |
|
After Width: | Height: | Size: 138 KiB |
|
After Width: | Height: | Size: 148 KiB |
|
After Width: | Height: | Size: 138 KiB |
|
After Width: | Height: | Size: 118 KiB |
|
After Width: | Height: | Size: 124 KiB |
|
After Width: | Height: | Size: 120 KiB |
|
After Width: | Height: | Size: 139 KiB |
|
After Width: | Height: | Size: 136 KiB |
@@ -18,4 +18,18 @@ export default [
|
||||
lng: 133.816444,
|
||||
isSpot: true,
|
||||
},
|
||||
{
|
||||
Station_JP: ".小歩危展望台",
|
||||
Station_EN: "Koboke Observatory",
|
||||
MyStation: "0",
|
||||
StationNumber: null,
|
||||
DispNum: "3",
|
||||
StationTimeTable: "",
|
||||
StationMap: "https://maps.app.goo.gl/WBMN5R2tk2tusavk7",
|
||||
JrHpUrl: "https://miyoshi-tourism.jp/spot/5438/",
|
||||
jslodApi: "spot",
|
||||
lat: 33.9372609,
|
||||
lng: 133.753258,
|
||||
isSpot: true,
|
||||
},
|
||||
];
|
||||
|
||||
|
After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 146 KiB |
|
After Width: | Height: | Size: 75 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 62 KiB |
@@ -1,21 +1,35 @@
|
||||
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";
|
||||
import { useSheetMaxHeight } from "./useSheetMaxHeight";
|
||||
export const EachTrainInfo = ({ payload }) => {
|
||||
if (!payload) return <></>;
|
||||
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
|
||||
gestureEnabled={true}
|
||||
CustomHeaderComponent={<></>}
|
||||
ref={actionSheetRef}
|
||||
drawUnderStatusBar={false}
|
||||
isModal={Platform.OS == "ios"}
|
||||
|
||||
isModal={Platform.OS === "ios" && !Platform.isPad}
|
||||
containerStyle={{ maxHeight }}
|
||||
onOpen={handleOpen}
|
||||
onClose={handleClose}
|
||||
//useBottomSafeAreaPadding={Platform.OS == "android"}
|
||||
>
|
||||
<EachTrainInfoCore {...{ actionSheetRef, ...payload }} />
|
||||
<EachTrainInfoCore {...{ actionSheetRef, sheetOpened, ...payload }} />
|
||||
</ActionSheet>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { FC } from "react";
|
||||
import { View, Text, TouchableWithoutFeedback, Alert } from "react-native";
|
||||
import { useAllTrainDiagram } from "@/stateBox/useAllTrainDiagram";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
import { useResponsive } from "@/lib/responsive";
|
||||
|
||||
export const DataConnectedButton: FC<{
|
||||
i: string;
|
||||
@@ -10,6 +11,7 @@ export const DataConnectedButton: FC<{
|
||||
const [station, se, time] = i.split(",");
|
||||
const { keyList } = useAllTrainDiagram();
|
||||
const { colors } = useThemeColors();
|
||||
const { fontScale, moderateScale } = useResponsive();
|
||||
// 列番が有効かどうかをチェックする関数
|
||||
const isValidTrainNumber = (trainNum: string): boolean => {
|
||||
return keyList.includes(trainNum);
|
||||
@@ -44,19 +46,19 @@ export const DataConnectedButton: FC<{
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: 35,
|
||||
width: moderateScale(35),
|
||||
position: "relative",
|
||||
marginHorizontal: 15,
|
||||
flexDirection: "row",
|
||||
height: "10%",
|
||||
}}
|
||||
/>
|
||||
<Text style={{ fontSize:16, fontFamily: "DiaPro", color: colors.text }}>
|
||||
<Text style={{ fontSize: fontScale(16), fontFamily: "DiaPro", color: colors.text }}>
|
||||
{se === "増" ? "⬐" : "↳"}
|
||||
</Text>
|
||||
<Text style={{ fontSize: 20, color: colors.textLink }}>{time}</Text>
|
||||
<Text style={{ fontSize: fontScale(20), color: colors.textLink }}>{time}</Text>
|
||||
<View style={{ flex: 1 }} />
|
||||
<Text style={{ fontSize: 18, width: 50, color: colors.text }}>
|
||||
<Text style={{ fontSize: fontScale(18), width: moderateScale(50), color: colors.text }}>
|
||||
{se === "増" ? "増結" : "解結"}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
} from "@/utils/seUtils";
|
||||
import type { SeTypes } from "@/types";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
import { useResponsive } from "@/lib/responsive";
|
||||
|
||||
type seTypes =
|
||||
| "発編"
|
||||
@@ -65,6 +66,7 @@ export const EachStopList: FC<props> = ({
|
||||
isNotService = false,
|
||||
}) => {
|
||||
const { colors: themeColors, isDark } = useThemeColors();
|
||||
const { fontScale, moderateScale } = useResponsive();
|
||||
const [station, se, time, platformNum] = i.split(",") as [
|
||||
string,
|
||||
seTypes,
|
||||
@@ -120,6 +122,16 @@ export const EachStopList: FC<props> = ({
|
||||
const StationNumbers = Stations.filter((d) => d.StationNumber != null).map(
|
||||
(d) => d.StationNumber as string
|
||||
);
|
||||
const trimmedPlatformNum = platformNum?.trim();
|
||||
const hasPlatformNum = !!trimmedPlatformNum;
|
||||
const isPlatformNumNumeric = hasPlatformNum && /^\d+$/.test(trimmedPlatformNum);
|
||||
const platformDisplay = hasPlatformNum
|
||||
? isPlatformNumNumeric
|
||||
? parseInt(trimmedPlatformNum, 10) === 0
|
||||
? "⓪"
|
||||
: String.fromCharCode(0x2460 + parseInt(trimmedPlatformNum, 10) - 1)
|
||||
: `(${trimmedPlatformNum})`
|
||||
: "";
|
||||
// SE文字列を表示用に変換
|
||||
const [seString, seType] = parseSeString(se);
|
||||
|
||||
@@ -197,7 +209,7 @@ export const EachStopList: FC<props> = ({
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: 35,
|
||||
width: moderateScale(35),
|
||||
position: "relative",
|
||||
marginHorizontal: 15,
|
||||
flexDirection: "row",
|
||||
@@ -222,7 +234,7 @@ export const EachStopList: FC<props> = ({
|
||||
<View style={{ position: "relative", height: 25 }}>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 20,
|
||||
fontSize: fontScale(20),
|
||||
position: "absolute",
|
||||
textAlignVertical: "center",
|
||||
left: -15,
|
||||
@@ -247,16 +259,24 @@ export const EachStopList: FC<props> = ({
|
||||
<View style={{ flex: 1 }} />
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 20,
|
||||
fontSize: fontScale(20),
|
||||
color: colors.text,
|
||||
fontStyle: isCommunity ? "italic" : "normal",
|
||||
textAlignVertical: "center",
|
||||
}}
|
||||
>
|
||||
{station}{platformNum &&
|
||||
(parseInt(platformNum) === 0
|
||||
? "⓪"
|
||||
: String.fromCharCode(0x2460 + parseInt(platformNum) - 1))}
|
||||
{station}
|
||||
{hasPlatformNum && (
|
||||
<Text
|
||||
style={{
|
||||
fontSize: fontScale(isPlatformNumNumeric ? 20 : 14),
|
||||
color: colors.text,
|
||||
fontStyle: isCommunity ? "italic" : "normal",
|
||||
}}
|
||||
>
|
||||
{platformDisplay}
|
||||
</Text>
|
||||
)}
|
||||
</Text>
|
||||
<View style={{ flex: 1 }} />
|
||||
</View>
|
||||
@@ -308,20 +328,21 @@ const TimeText: FC<{
|
||||
normalTextColor,
|
||||
}) => {
|
||||
const [seString, seType] = parseSeString(se);
|
||||
const { fontScale, moderateScale } = useResponsive();
|
||||
|
||||
return (
|
||||
<View style={{ flexDirection: "row", alignItems: "center" }}>
|
||||
<View style={{ flexDirection: "row", alignItems: "center", opacity: isBefore ? 0.5 : 1 }}>
|
||||
{!!currentTrainData?.delay &&
|
||||
currentTrainData?.delay != "入線" &&
|
||||
currentTrainData?.delay != 0 && (
|
||||
<Text
|
||||
style={{
|
||||
fontSize: isBefore ? 14 : 15,
|
||||
fontSize: fontScale(isBefore ? 14 : 15),
|
||||
marginVertical: isDouble ? -2 : 0,
|
||||
color: normalTextColor,
|
||||
width: 60,
|
||||
width: moderateScale(60),
|
||||
position: "absolute",
|
||||
right: isBefore ? 125 : 120,
|
||||
right: isBefore ? moderateScale(125) : moderateScale(120),
|
||||
textAlign: "right",
|
||||
textDecorationLine: "line-through",
|
||||
fontStyle: seType == "community" ? "italic" : "normal",
|
||||
@@ -341,8 +362,8 @@ const TimeText: FC<{
|
||||
/>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: isBefore ? 14 : 18,
|
||||
width: 50,
|
||||
fontSize: fontScale(isBefore ? 14 : 18),
|
||||
width: moderateScale(50),
|
||||
color: textColor,
|
||||
marginVertical: isDouble ? -2 : 0,
|
||||
fontStyle: seType == "community" ? "italic" : "normal",
|
||||
@@ -356,6 +377,7 @@ const TimeText: FC<{
|
||||
const StationNumbersBox: FC<{ stn: string; se: seTypes }> = (props) => {
|
||||
const { stn, se } = props;
|
||||
const { fixed } = useThemeColors();
|
||||
const { fontScale } = useResponsive();
|
||||
const lineColor = lineColorList[stn.charAt(0)];
|
||||
const hasThrew =
|
||||
se == "通過" ||
|
||||
@@ -375,7 +397,7 @@ const StationNumbersBox: FC<{ stn: string; se: seTypes }> = (props) => {
|
||||
style={{
|
||||
color: fixed.textOnPrimary,
|
||||
textAlign: "center",
|
||||
fontSize: 10,
|
||||
fontSize: fontScale(10),
|
||||
fontWeight: "bold",
|
||||
}}
|
||||
>
|
||||
@@ -400,6 +422,7 @@ type StationTimeBoxType = {
|
||||
|
||||
const StationTimeBox: FC<StationTimeBoxType> = (props) => {
|
||||
const { delay, textColor, seType, se, time, isDouble, isBefore } = props;
|
||||
const { fontScale, moderateScale } = useResponsive();
|
||||
const dates = dayjs()
|
||||
.set("hour", parseInt(time.split(":")[0]))
|
||||
.set("minute", parseInt(time.split(":")[1]))
|
||||
@@ -408,10 +431,10 @@ const StationTimeBox: FC<StationTimeBoxType> = (props) => {
|
||||
return (
|
||||
<Text
|
||||
style={{
|
||||
fontSize: isBefore ? 14 : 20,
|
||||
fontSize: fontScale(isBefore ? 14 : 20),
|
||||
marginVertical: isDouble ? -2 : 0,
|
||||
color: textColor,
|
||||
width: 60,
|
||||
width: moderateScale(60),
|
||||
fontStyle: seType == "community" ? "italic" : "normal",
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
import React, { FC, useCallback, useEffect } from "react";
|
||||
import { Platform, TouchableOpacity, Text, StyleSheet } from "react-native";
|
||||
import { useTrainFollowActivity } from "@/lib/useTrainFollowActivity";
|
||||
import { useAllTrainDiagram } from "@/stateBox/useAllTrainDiagram";
|
||||
import { trainDataType } from "@/lib/trainPositionTextArray";
|
||||
|
||||
type Props = {
|
||||
currentTrainData: trainDataType;
|
||||
currentPosition: string[] | undefined;
|
||||
};
|
||||
|
||||
export const LiveActivityButton: FC<Props> = ({
|
||||
currentTrainData,
|
||||
currentPosition,
|
||||
}) => {
|
||||
const liveActivity = useTrainFollowActivity();
|
||||
const { allTrainDiagram } = useAllTrainDiagram();
|
||||
|
||||
const onLine = !!currentPosition?.toString().length;
|
||||
|
||||
const handleLiveActivity = useCallback(async () => {
|
||||
if (liveActivity.status === "active") {
|
||||
await liveActivity.end();
|
||||
return;
|
||||
}
|
||||
if (!onLine) return;
|
||||
|
||||
const diagramStr: string =
|
||||
(allTrainDiagram as Record<string, string>)[currentTrainData?.num] ?? "";
|
||||
const stations = diagramStr
|
||||
.split("#")
|
||||
.filter(Boolean)
|
||||
.map((s) => s.split(",")[0]);
|
||||
const destination =
|
||||
stations.length > 0 ? stations[stations.length - 1] : "";
|
||||
|
||||
const curSt = currentPosition?.[0] ?? currentTrainData?.Pos ?? "";
|
||||
const curIdx = stations.indexOf(curSt);
|
||||
const nextSt =
|
||||
curIdx >= 0 && curIdx + 1 < stations.length
|
||||
? stations[curIdx + 1]
|
||||
: currentPosition?.[1] ?? "";
|
||||
|
||||
await liveActivity.start({
|
||||
trainNumber: currentTrainData?.num ?? "",
|
||||
lineName: currentTrainData?.Line ?? "",
|
||||
destination,
|
||||
currentStation: curSt,
|
||||
nextStation: nextSt,
|
||||
delayMinutes:
|
||||
typeof currentTrainData?.delay === "number"
|
||||
? currentTrainData.delay
|
||||
: 0,
|
||||
scheduledArrival: "",
|
||||
});
|
||||
}, [
|
||||
liveActivity,
|
||||
onLine,
|
||||
allTrainDiagram,
|
||||
currentTrainData,
|
||||
currentPosition,
|
||||
]);
|
||||
|
||||
// 列車位置が変わったら Live Activity を自動更新
|
||||
useEffect(() => {
|
||||
if (liveActivity.status !== "active") return;
|
||||
if (!currentTrainData?.Pos) return;
|
||||
|
||||
const diagramStr: string =
|
||||
(allTrainDiagram as Record<string, string>)[currentTrainData?.num] ?? "";
|
||||
const stations = diagramStr
|
||||
.split("#")
|
||||
.filter(Boolean)
|
||||
.map((s) => s.split(",")[0]);
|
||||
|
||||
const curSt = currentPosition?.[0] ?? currentTrainData.Pos;
|
||||
const curIdx = stations.indexOf(curSt);
|
||||
const nextSt =
|
||||
curIdx >= 0 && curIdx + 1 < stations.length
|
||||
? stations[curIdx + 1]
|
||||
: currentPosition?.[1] ?? "";
|
||||
|
||||
liveActivity.update({
|
||||
currentStation: curSt,
|
||||
nextStation: nextSt,
|
||||
delayMinutes:
|
||||
typeof currentTrainData.delay === "number"
|
||||
? currentTrainData.delay
|
||||
: 0,
|
||||
scheduledArrival: "",
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
currentTrainData?.Pos,
|
||||
currentTrainData?.delay,
|
||||
currentPosition?.toString(),
|
||||
]);
|
||||
|
||||
if (Platform.OS !== "ios" || !liveActivity.available || !onLine) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
onPress={handleLiveActivity}
|
||||
disabled={liveActivity.status === "loading"}
|
||||
style={[
|
||||
styles.liveActivityButton,
|
||||
liveActivity.status === "active" && styles.liveActivityButtonActive,
|
||||
]}
|
||||
>
|
||||
<Text style={styles.liveActivityButtonText}>
|
||||
{liveActivity.status === "active"
|
||||
? "🔴 列車追従 Live Activity 終了"
|
||||
: liveActivity.status === "loading"
|
||||
? "⏳ 開始中..."
|
||||
: "🟢 列車追従 Live Activity 開始"}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
liveActivityButton: {
|
||||
marginHorizontal: 10,
|
||||
marginVertical: 6,
|
||||
paddingVertical: 10,
|
||||
paddingHorizontal: 16,
|
||||
borderRadius: 10,
|
||||
backgroundColor: "rgba(0, 153, 204, 0.12)",
|
||||
borderWidth: 1,
|
||||
borderColor: "rgba(0, 153, 204, 0.5)",
|
||||
alignItems: "center",
|
||||
},
|
||||
liveActivityButtonActive: {
|
||||
backgroundColor: "rgba(220, 50, 50, 0.1)",
|
||||
borderColor: "rgba(220, 50, 50, 0.5)",
|
||||
},
|
||||
liveActivityButtonText: {
|
||||
fontSize: 14,
|
||||
fontWeight: "600",
|
||||
color: "#0099cc",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { FC } from "react";
|
||||
import { ScrollView } from "react-native";
|
||||
import { View } from "react-native";
|
||||
import { TrainDataView } from "./TrainDataView";
|
||||
import { trainDataType } from "@/lib/trainPositionTextArray";
|
||||
import type { NavigateFunction } from "@/types";
|
||||
@@ -18,12 +18,13 @@ export const LongHeader:FC<props> = ({
|
||||
navigate,
|
||||
}) => {
|
||||
return (
|
||||
<ScrollView
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
flex: 1,
|
||||
width: "100%",
|
||||
alignSelf: "stretch",
|
||||
}}
|
||||
horizontal
|
||||
pagingEnabled
|
||||
>
|
||||
<TrainDataView
|
||||
currentTrainData={currentTrainData}
|
||||
@@ -33,6 +34,6 @@ export const LongHeader:FC<props> = ({
|
||||
navigate={navigate}
|
||||
key={"LongHeader"}
|
||||
/>
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { trainPosition, trainDataType } from "@/lib/trainPositionTextArray";
|
||||
import React, { FC } from "react";
|
||||
import { View, Text, TextStyle, ViewStyle } from "react-native";
|
||||
import { View, Text, ViewStyle } from "react-native";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
import { useResponsive } from "@/lib/responsive";
|
||||
|
||||
type stateBox = {
|
||||
currentTrainData: trainDataType | undefined;
|
||||
@@ -23,6 +24,7 @@ export const PositionBox: FC<stateBox> = (props) => {
|
||||
lineNumber,
|
||||
} = props;
|
||||
const { colors } = useThemeColors();
|
||||
const { fontScale } = useResponsive();
|
||||
let firstText = "";
|
||||
let secondText = "";
|
||||
let marginText = "";
|
||||
@@ -49,11 +51,11 @@ export const PositionBox: FC<stateBox> = (props) => {
|
||||
}
|
||||
return (
|
||||
<View style={{ ...(mode == 2 ? boxStyle2 : boxStyle), backgroundColor: colors.surface, ...style }}>
|
||||
<Text style={{ fontSize: 12, color: colors.textAccent }}>{title}</Text>
|
||||
<Text style={{ fontSize: fontScale(12), color: colors.textAccent }}>{title}</Text>
|
||||
<View style={{ flex: 1 }} />
|
||||
<View style={{ flexDirection: mode == 2 ? "row" : "column" }}>
|
||||
{firstText && (
|
||||
<Text style={[mode == 2 ? boxTextStyle2 : (isBetween ? boxTextStyle : boxTextStyleBig), { color: colors.textAccent }]}>
|
||||
<Text style={[mode == 2 ? { fontSize: fontScale(18), textAlign: "right" as const } : (isBetween ? { fontSize: fontScale(25), textAlign: "right" as const } : { fontSize: fontScale(28), textAlign: "right" as const }), { color: colors.textAccent }]}>
|
||||
{firstText}
|
||||
</Text>
|
||||
)}
|
||||
@@ -63,7 +65,7 @@ export const PositionBox: FC<stateBox> = (props) => {
|
||||
</Text>
|
||||
)}
|
||||
{secondText && (
|
||||
<Text style={[mode == 2 ? boxTextStyle2 :(isBetween ? boxTextStyle : boxTextStyleMini), { color: colors.textAccent }]}>
|
||||
<Text style={[mode == 2 ? { fontSize: fontScale(18), textAlign: "right" as const } :(isBetween ? { fontSize: fontScale(25), textAlign: "right" as const } : { fontSize: fontScale(16), textAlign: "right" as const }), { color: colors.textAccent }]}>
|
||||
{secondText}
|
||||
</Text>
|
||||
)}
|
||||
@@ -72,8 +74,8 @@ export const PositionBox: FC<stateBox> = (props) => {
|
||||
<View style={{ flexDirection: mode == 2 ? "row" : "column" }}>
|
||||
<Text
|
||||
style={{
|
||||
...{ ...(mode == 2 ? boxTextStyle2 : boxTextStyle) },
|
||||
fontSize: 10,
|
||||
...(mode == 2 ? { fontSize: fontScale(18), textAlign: "right" as const } : { fontSize: fontScale(25), textAlign: "right" as const }),
|
||||
fontSize: fontScale(10),
|
||||
color: colors.textAccent,
|
||||
}}
|
||||
>
|
||||
@@ -97,23 +99,3 @@ const boxStyle2: ViewStyle = {
|
||||
padding: 5,
|
||||
margin: 5,
|
||||
};
|
||||
const boxTextStyle2: TextStyle = {
|
||||
fontSize: 18,
|
||||
textAlign: "right",
|
||||
};
|
||||
const boxTextStyleBig: TextStyle = {
|
||||
fontSize: 28,
|
||||
textAlign: "right",
|
||||
};
|
||||
|
||||
|
||||
const boxTextStyleMini: TextStyle = {
|
||||
fontSize: 16,
|
||||
textAlign: "right",
|
||||
};
|
||||
|
||||
|
||||
const boxTextStyle: TextStyle = {
|
||||
fontSize: 25,
|
||||
textAlign: "right",
|
||||
};
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import React from "react";
|
||||
import { View, Text, LayoutAnimation, TouchableOpacity } from "react-native";
|
||||
import { View, Text, TouchableOpacity } from "react-native";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
import { useResponsive } from "@/lib/responsive";
|
||||
|
||||
export const ScrollStickyContent = (props) => {
|
||||
const { currentTrainData, showThrew, setShowThrew, haveThrough } = props;
|
||||
const { colors } = useThemeColors();
|
||||
const { fontScale } = useResponsive();
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
@@ -22,13 +24,13 @@ export const ScrollStickyContent = (props) => {
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
<Text style={{ fontSize: 20, color: colors.text }}>停車駅</Text>
|
||||
<Text style={{ fontSize: fontScale(20), color: colors.text }}>停車駅</Text>
|
||||
<View style={{ flex: 1 }} />
|
||||
<View style={{ flexDirection: "row" }}>
|
||||
{!isNaN(currentTrainData?.delay) && currentTrainData?.delay != 0 && (
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 15,
|
||||
fontSize: fontScale(15),
|
||||
color: colors.text,
|
||||
position: "absolute",
|
||||
right: 110,
|
||||
@@ -41,7 +43,7 @@ export const ScrollStickyContent = (props) => {
|
||||
)}
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 20,
|
||||
fontSize: fontScale(20),
|
||||
color: isNaN(currentTrainData?.delay)
|
||||
? colors.text
|
||||
: currentTrainData?.delay == 0
|
||||
@@ -55,17 +57,13 @@ export const ScrollStickyContent = (props) => {
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
if (!haveThrough) return;
|
||||
LayoutAnimation.configureNext({
|
||||
duration: 200,
|
||||
update: { type: "easeInEaseOut", springDamping: 0.6 },
|
||||
});
|
||||
setShowThrew(!showThrew);
|
||||
}}
|
||||
>
|
||||
<View style={{ flex: 1 }} />
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 12,
|
||||
fontSize: fontScale(12),
|
||||
width: 50,
|
||||
paddingBottom: 0,
|
||||
margin: "auto",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { FC } from "react";
|
||||
import { ScrollView } from "react-native";
|
||||
import { View } from "react-native";
|
||||
import { TrainDataView } from "./TrainDataView";
|
||||
import { trainDataType } from "@/lib/trainPositionTextArray";
|
||||
import type { NavigateFunction } from "@/types";
|
||||
@@ -18,13 +18,13 @@ export const ShortHeader:FC<props> = ({
|
||||
navigate,
|
||||
}) => {
|
||||
return (
|
||||
<ScrollView
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
flex: 1,
|
||||
width: "100%",
|
||||
alignSelf: "stretch",
|
||||
}}
|
||||
horizontal
|
||||
pagingEnabled
|
||||
>
|
||||
<TrainDataView
|
||||
mode={2}
|
||||
@@ -35,6 +35,6 @@ export const ShortHeader:FC<props> = ({
|
||||
navigate={navigate}
|
||||
key={"ShortHeader"}
|
||||
/>
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { CSSProperties, FC } from "react";
|
||||
import { View, Text, StyleProp, TextStyle, ViewStyle } from "react-native";
|
||||
import { View, Text, StyleProp, ViewStyle } from "react-native";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
import { useResponsive } from "@/lib/responsive";
|
||||
|
||||
type stateBox = {
|
||||
text: string;
|
||||
@@ -12,19 +13,20 @@ type stateBox = {
|
||||
export const StateBox: FC<stateBox> = (props) => {
|
||||
const { text, title, style, mode, endText } = props;
|
||||
const { colors } = useThemeColors();
|
||||
const { fontScale } = useResponsive();
|
||||
return (
|
||||
<View style={{ ...(mode == 2 ? boxStyle2 : boxStyle), backgroundColor: colors.surface, ...style }}>
|
||||
<Text style={{ fontSize: 12, color: colors.textAccent }}>{title}</Text>
|
||||
<Text style={{ fontSize: fontScale(12), color: colors.textAccent }}>{title}</Text>
|
||||
<View style={{ flex: 1 }} />
|
||||
<View style={{ flexDirection: mode == 2 ? "row" : "column" }}>
|
||||
<Text style={[mode == 2 ? boxTextStyle2 : boxTextStyle, { color: colors.textAccent }]}>{text}</Text>
|
||||
<Text style={[mode == 2 ? { fontSize: fontScale(18), textAlign: "right" as const } : { fontSize: fontScale(25), textAlign: "right" as const }, { color: colors.textAccent }]}>{text}</Text>
|
||||
</View>
|
||||
{endText && (
|
||||
<View style={{ flexDirection: mode == 2 ? "row" : "column" }}>
|
||||
<Text
|
||||
style={{
|
||||
...{ ...(mode == 2 ? boxTextStyle2 : boxTextStyle) },
|
||||
fontSize: 10,
|
||||
...(mode == 2 ? { fontSize: fontScale(18), textAlign: "right" as const } : { fontSize: fontScale(25), textAlign: "right" as const }),
|
||||
fontSize: fontScale(10),
|
||||
color: colors.textAccent,
|
||||
}}
|
||||
>
|
||||
@@ -47,12 +49,3 @@ const boxStyle2: ViewStyle = {
|
||||
padding: 5,
|
||||
margin: 5,
|
||||
};
|
||||
const boxTextStyle2: TextStyle = {
|
||||
fontSize: 18,
|
||||
textAlign: "right",
|
||||
};
|
||||
|
||||
const boxTextStyle: TextStyle = {
|
||||
fontSize: 25,
|
||||
textAlign: "right",
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useEffect, FC } from "react";
|
||||
import { View, TouchableOpacity, useWindowDimensions, Text } from "react-native";
|
||||
import { View, TouchableOpacity, Text } from "react-native";
|
||||
import { StateBox } from "./StateBox";
|
||||
import { PositionBox } from "./PositionBox";
|
||||
import { useDeviceOrientationChange } from "../../../stateBox/useDeviceOrientationChange";
|
||||
@@ -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 = {
|
||||
@@ -33,7 +34,6 @@ export const TrainDataView:FC<props> = ({
|
||||
}) => {
|
||||
const { stationList } = useStationList();
|
||||
|
||||
const { width, height } = useWindowDimensions();
|
||||
const { isLandscape } = useDeviceOrientationChange();
|
||||
const { setInjectData } = useCurrentTrain();
|
||||
|
||||
@@ -155,7 +155,7 @@ export const TrainDataView:FC<props> = ({
|
||||
flexDirection: "row",
|
||||
//minHeight: 200,
|
||||
//height: heightPercentageToDP("20%"),
|
||||
width: isLandscape ? (width / 100) * 40 : width,
|
||||
width: "100%",
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
@@ -164,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,8 +22,10 @@ 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";
|
||||
|
||||
// Custom hooks
|
||||
import { useTrainDiagramData } from "./EachTrainInfoCore/hooks/useTrainDiagramData";
|
||||
@@ -40,13 +42,16 @@ export const EachTrainInfoCore = ({
|
||||
openStationACFromEachTrainInfo,
|
||||
from,
|
||||
navigate,
|
||||
sheetOpened = false,
|
||||
}) => {
|
||||
const { stationList } = useStationList();
|
||||
const { allCustomTrainData } = useAllTrainDiagram();
|
||||
const { colors, fixed } = useThemeColors();
|
||||
const { verticalScale, moderateScale } = useResponsive();
|
||||
const { setTrainInfo } = useTrainMenu();
|
||||
const { height } = useWindowDimensions();
|
||||
const { isLandscape } = useDeviceOrientationChange();
|
||||
const { getCurrentStationData } = useCurrentTrain();
|
||||
|
||||
const scrollRef = useRef<any>(null);
|
||||
// Custom hooks for data management
|
||||
@@ -70,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
|
||||
@@ -80,7 +87,7 @@ export const EachTrainInfoCore = ({
|
||||
scrollRef,
|
||||
isJumped,
|
||||
setIsJumped,
|
||||
setShowThrew
|
||||
sheetOpened
|
||||
);
|
||||
|
||||
// Back button handler
|
||||
@@ -124,7 +131,6 @@ export const EachTrainInfoCore = ({
|
||||
} else {
|
||||
SheetManager.hide("EachTrainInfo").then(() => {
|
||||
setTimeout(() => {
|
||||
// @ts-expect-error - SheetManager payload type is too restrictive
|
||||
SheetManager.show("EachTrainInfo", { payload });
|
||||
}, 200);
|
||||
});
|
||||
@@ -141,11 +147,11 @@ export const EachTrainInfoCore = ({
|
||||
borderWidth: 1,
|
||||
}}
|
||||
>
|
||||
<View style={{ height: 26, width: "100%" }}>
|
||||
<View style={{ height: verticalScale(26), width: "100%" }}>
|
||||
<View
|
||||
style={{
|
||||
height: 6,
|
||||
width: 45,
|
||||
height: verticalScale(6),
|
||||
width: moderateScale(45),
|
||||
borderRadius: 100,
|
||||
backgroundColor: colors.borderLight,
|
||||
marginVertical: 10,
|
||||
@@ -172,7 +178,7 @@ export const EachTrainInfoCore = ({
|
||||
scrollRef={scrollRef}
|
||||
containerProps={{
|
||||
style: {
|
||||
maxHeight: isLandscape ? height - 94 : (height / 100) * 70,
|
||||
maxHeight: height * 0.7,
|
||||
backgroundColor:
|
||||
customTrainType.data === "notService" ? "#777777ff" : colors.surface,
|
||||
},
|
||||
@@ -206,7 +212,7 @@ export const EachTrainInfoCore = ({
|
||||
}
|
||||
>
|
||||
{customTrainType.data === "notService" && (
|
||||
<Text style={{ backgroundColor: colors.surface, fontWeight: "bold" }}>
|
||||
<Text style={{ backgroundColor: colors.surface, color: colors.text, fontWeight: "bold" }}>
|
||||
この列車には乗車できません。
|
||||
</Text>
|
||||
)}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { FC, useMemo, useState } from "react";
|
||||
import React, { FC, useMemo } from "react";
|
||||
import { Text, View, TextStyle, TouchableOpacity } from "react-native";
|
||||
import { SheetManager } from "react-native-actions-sheet";
|
||||
import { migrateTrainName } from "../../../lib/eachTrainInfoCoreLib/migrateTrainName";
|
||||
@@ -16,6 +16,9 @@ import type { NavigateFunction } from "@/types";
|
||||
import { useUnyohub } from "@/stateBox/useUnyohub";
|
||||
import { useElesite } from "@/stateBox/useElesite";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
import { formatWrappedTrainType, shouldWrapTrainType } from "@/lib/trainTypeWrap";
|
||||
import { useResponsive } from "@/lib/responsive";
|
||||
import { normalizeIconDisplayMode } from "@/lib/iconDisplayMode";
|
||||
|
||||
type Props = {
|
||||
data: { trainNum: string; limited: string };
|
||||
@@ -46,10 +49,11 @@ export const HeaderText: FC<Props> = ({
|
||||
from,
|
||||
scrollRef,
|
||||
}) => {
|
||||
const { limited, trainNum } = data;
|
||||
const { trainNum } = data;
|
||||
|
||||
const { fixed } = useThemeColors();
|
||||
const { updatePermission } = useTrainMenu();
|
||||
const { fontScale } = useResponsive();
|
||||
const { iconSetting } = useTrainMenu();
|
||||
const { allCustomTrainData, getTodayOperationByTrainId } =
|
||||
useAllTrainDiagram();
|
||||
const { expoPushToken } = useNotification();
|
||||
@@ -57,22 +61,22 @@ export const HeaderText: FC<Props> = ({
|
||||
getUnyohubByTrainNumber,
|
||||
getUnyohubEntriesByTrainNumber,
|
||||
useUnyohub: unyohubEnabled,
|
||||
unyohubData,
|
||||
} = useUnyohub();
|
||||
const { getElesiteEntriesByTrainNumber, useElesite: elesiteEnabled } =
|
||||
useElesite();
|
||||
const iconDisplayMode = normalizeIconDisplayMode(iconSetting);
|
||||
|
||||
// 追加ソースのON/OFFをここで管理(将来ソースが増えたらここに足す)
|
||||
const additionalSources = {
|
||||
unyohub: unyohubEnabled,
|
||||
elesite: elesiteEnabled,
|
||||
};
|
||||
const hasAdditionalSources = Object.values(additionalSources).some(Boolean);
|
||||
|
||||
// 列車名、種別、フォントの取得
|
||||
const [
|
||||
typeName,
|
||||
trainName,
|
||||
fontAvailable,
|
||||
fontFamily,
|
||||
isOneMan,
|
||||
infogram,
|
||||
priority,
|
||||
@@ -93,7 +97,7 @@ export const HeaderText: FC<Props> = ({
|
||||
to_data,
|
||||
directions,
|
||||
} = result;
|
||||
const [typeString, fontAvailable, isOneMan] = getStringConfig(
|
||||
const [typeString, fontFamily, isOneMan] = getStringConfig(
|
||||
type,
|
||||
trainNum,
|
||||
);
|
||||
@@ -105,7 +109,7 @@ export const HeaderText: FC<Props> = ({
|
||||
(train_num_distance !== "" && !isNaN(parseInt(train_num_distance))
|
||||
? ` ${parseInt(trainNum) - parseInt(train_num_distance)}号`
|
||||
: ""),
|
||||
fontAvailable,
|
||||
fontFamily,
|
||||
isOneMan,
|
||||
infogram,
|
||||
priority,
|
||||
@@ -118,7 +122,7 @@ export const HeaderText: FC<Props> = ({
|
||||
return [
|
||||
typeString,
|
||||
"",
|
||||
fontAvailable,
|
||||
fontFamily,
|
||||
isOneMan,
|
||||
infogram,
|
||||
priority,
|
||||
@@ -130,8 +134,8 @@ export const HeaderText: FC<Props> = ({
|
||||
case to_data && to_data !== "":
|
||||
return [
|
||||
typeString,
|
||||
to_data + "行き",
|
||||
fontAvailable,
|
||||
`${to_data}行き`,
|
||||
fontFamily,
|
||||
isOneMan,
|
||||
infogram,
|
||||
priority,
|
||||
@@ -144,9 +148,9 @@ export const HeaderText: FC<Props> = ({
|
||||
return [
|
||||
typeString,
|
||||
migrateTrainName(
|
||||
trainData[trainData.length - 1].split(",")[0] + "行き",
|
||||
`${trainData[trainData.length - 1].split(",")[0]}行き`,
|
||||
),
|
||||
fontAvailable,
|
||||
fontFamily,
|
||||
isOneMan,
|
||||
infogram,
|
||||
priority,
|
||||
@@ -156,11 +160,10 @@ export const HeaderText: FC<Props> = ({
|
||||
result,
|
||||
];
|
||||
}
|
||||
}, [trainData]);
|
||||
}, [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;
|
||||
@@ -168,11 +171,34 @@ export const HeaderText: FC<Props> = ({
|
||||
iconTrainDirection = directions ? true : false;
|
||||
}
|
||||
|
||||
const unyohubFormation = getUnyohubByTrainNumber(trainNum);
|
||||
const unyohubEntries = getUnyohubEntriesByTrainNumber(trainNum);
|
||||
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 = 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情報あり」と判定
|
||||
const hasUnyohubFormation = unyohubEntries.some(
|
||||
(e) => !!e.formations && e.formations.trim() !== "",
|
||||
);
|
||||
@@ -182,11 +208,20 @@ export const HeaderText: FC<Props> = ({
|
||||
|
||||
const hasExtraInfo =
|
||||
priority > 200 ||
|
||||
todayOperation?.length > 0 ||
|
||||
todayOperation.length > 0 ||
|
||||
hasUnyohubFormation ||
|
||||
hasElesiteFormation;
|
||||
|
||||
const [isWrapped, setIsWrapped] = useState(false);
|
||||
const wrapType = shouldWrapTrainType(typeName, trainName);
|
||||
|
||||
const openTrainInfoUrl = () => {
|
||||
if (!trainInfoUrl) return;
|
||||
const uri = trainInfoUrl.includes("pdf")
|
||||
? getPDFViewURL(trainInfoUrl)
|
||||
: trainInfoUrl;
|
||||
navigate("generalWebView", { uri, useExitButton: true });
|
||||
SheetManager.hide("EachTrainInfo");
|
||||
};
|
||||
|
||||
return (
|
||||
<View
|
||||
@@ -206,6 +241,7 @@ export const HeaderText: FC<Props> = ({
|
||||
from={from}
|
||||
todayOperation={todayOperation}
|
||||
direction={iconTrainDirection}
|
||||
iconDisplayMode={iconDisplayMode}
|
||||
/>
|
||||
|
||||
<View
|
||||
@@ -216,15 +252,23 @@ export const HeaderText: FC<Props> = ({
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
numberOfLines={wrapType ? 2 : 1}
|
||||
style={{
|
||||
fontSize: 20,
|
||||
fontSize: fontScale(20),
|
||||
color: fixed.textOnPrimary,
|
||||
fontFamily: fontAvailable ? "JR-Nishi" : undefined,
|
||||
fontWeight: !fontAvailable ? "bold" : undefined,
|
||||
fontFamily,
|
||||
fontWeight: fontFamily ? undefined : "bold",
|
||||
marginRight: 5,
|
||||
flexShrink: 0,
|
||||
...(wrapType
|
||||
? { width: fontScale(44), lineHeight: fontScale(22) }
|
||||
: {}),
|
||||
}}
|
||||
>
|
||||
{isWrapped ? typeName.replace(/(.{2})/g, "$1\n").trim() : typeName}
|
||||
{wrapType ? formatWrappedTrainType(typeName) : typeName}
|
||||
{fontFamily === "JR-WEST-PLUS" ? (
|
||||
<Text style={{ fontFamily: "DiaPro", fontSize: 7 }}>{"\u00A0"}</Text>
|
||||
) : null}
|
||||
</Text>
|
||||
{isOneMan && <OneManText />}
|
||||
</View>
|
||||
@@ -244,33 +288,25 @@ export const HeaderText: FC<Props> = ({
|
||||
}
|
||||
: {}),
|
||||
}}
|
||||
onPress={() => {
|
||||
if (!trainInfoUrl) return;
|
||||
const uri = trainInfoUrl.includes("pdf")
|
||||
? getPDFViewURL(trainInfoUrl)
|
||||
: trainInfoUrl;
|
||||
navigate("generalWebView", { uri, useExitButton: true });
|
||||
SheetManager.hide("EachTrainInfo");
|
||||
}}
|
||||
onPress={openTrainInfoUrl}
|
||||
disabled={!trainInfoUrl}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
...textConfig,
|
||||
color: fixed.textOnPrimary,
|
||||
...(trainName.length > 10 ? { fontSize: 16 } : {}),
|
||||
...(trainName.length > 10
|
||||
? { fontSize: fontScale(16) }
|
||||
: { fontSize: fontScale(17) }),
|
||||
flexShrink: 1,
|
||||
}}
|
||||
onTextLayout={(e) => {
|
||||
if (e.nativeEvent.lines.length > 1) setIsWrapped(true);
|
||||
}}
|
||||
>
|
||||
{trainName}
|
||||
</Text>
|
||||
<InfogramText infogram={infogram} />
|
||||
</TouchableOpacity>
|
||||
<View style={{ flex: 1 }} />
|
||||
<Text style={{ ...textConfig, color: fixed.textOnPrimary }}>
|
||||
<Text style={{ ...textConfig, fontSize: fontScale(17), color: fixed.textOnPrimary }}>
|
||||
{showHeadStation.map((d) => `${headStation[d].id} + `)}
|
||||
{trainNum}
|
||||
{showTailStation.map((d) => ` + ${tailStation[d].id}`)}
|
||||
@@ -285,6 +321,7 @@ export const HeaderText: FC<Props> = ({
|
||||
(SheetManager.show as any)("TrainDataSources", {
|
||||
payload: {
|
||||
trainNum,
|
||||
unyohubTrainNum: unyohubTrainNumForSourceScreen,
|
||||
unyohubEntries,
|
||||
elesiteEntries,
|
||||
todayOperation,
|
||||
@@ -301,7 +338,6 @@ export const HeaderText: FC<Props> = ({
|
||||
},
|
||||
});
|
||||
} else {
|
||||
// 追加ソースが全てオフ → 元の挙動(直接 DB ページを開く)
|
||||
const uri = `https://jr-shikoku-data-system.pages.dev/trainData/${trainNum}?userID=${expoPushToken}&from=eachTrainInfo`;
|
||||
navigate("generalWebView", { uri, useExitButton: false });
|
||||
SheetManager.hide("EachTrainInfo");
|
||||
|
||||
@@ -3,13 +3,14 @@ import { Text } from "react-native";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
type props = {
|
||||
infogram: string;
|
||||
fontSize?: number;
|
||||
}
|
||||
export const InfogramText: FC<props> = ({infogram}) => {
|
||||
export const InfogramText: FC<props> = ({infogram, fontSize = 20}) => {
|
||||
const { fixed } = useThemeColors();
|
||||
return (
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 20,
|
||||
fontSize,
|
||||
color: fixed.textOnPrimary,
|
||||
fontFamily: "JNR-font",
|
||||
}}
|
||||
|
||||