Compare commits
174 Commits
patch/6.x
...
feature/fi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
18f11c88b1 | ||
|
|
cee238d060 | ||
|
|
dad462ff45 | ||
|
|
26cde03d3e | ||
|
|
0230b56f8b | ||
|
|
a2912d77ae | ||
|
|
e80eeae211 | ||
|
|
33410fcd61 | ||
|
|
ff25363600 | ||
|
|
36cd7448a5 | ||
|
|
4a0e252366 | ||
|
|
1c96776f56 | ||
|
|
53b95a91d5 | ||
|
|
82fe8d1244 | ||
|
|
80eeb51134 | ||
|
|
2d96bdcad9 | ||
|
|
59653bbc16 | ||
|
|
f34d06192b | ||
|
|
16bf96faf7 | ||
|
|
3925370b97 | ||
|
|
dcd8de06f8 | ||
|
|
1d57f2a5c6 | ||
|
|
d3e4b173c7 | ||
|
|
5202f35702 | ||
|
|
dc3d250466 | ||
|
|
3f6b3cfcfb | ||
|
|
2cd5142262 | ||
|
|
4b518b848e | ||
|
|
fb89a2b334 | ||
|
|
a66af59438 | ||
|
|
04d0f50b99 | ||
|
|
f4b86f4e77 | ||
|
|
91470b5db8 | ||
|
|
ffcc6ff660 | ||
|
|
681b12b622 | ||
|
|
13f2c4de7a | ||
|
|
f19600a3af | ||
|
|
9a567d2486 | ||
|
|
9271629aa9 | ||
|
|
d79f5a07f8 | ||
|
|
86a4428861 | ||
|
|
960acdbb3f | ||
|
|
86123ecb81 | ||
|
|
1d14bcf91a | ||
|
|
c7b1501475 | ||
|
|
814de31418 | ||
|
|
ecc9ee313e | ||
|
|
0a2333a201 | ||
|
|
0d45732d66 | ||
|
|
38171574e8 | ||
|
|
ce4fb4d1da | ||
|
|
39a5b33e77 | ||
|
|
6829744fa4 | ||
|
|
777b5c8acb | ||
|
|
30e4e9780a | ||
|
|
d9574f991d | ||
|
|
a665bf3a74 | ||
|
|
2cdcb5176b | ||
|
|
44cb462595 | ||
|
|
9bf7a735c1 | ||
|
|
bdffce9e6a | ||
|
|
d4ad8c005e | ||
|
|
0d0b82eee1 | ||
|
|
3e26463354 | ||
|
|
cefca15de9 | ||
|
|
62c84f153e | ||
|
|
83741e32fc | ||
|
|
f262226d4c | ||
|
|
032bcf127e | ||
|
|
787718c36a | ||
|
|
5457ced33d | ||
|
|
91cad9c2c8 | ||
|
|
11cd8e0f40 | ||
|
|
7c84b037ac | ||
|
|
676460353f | ||
|
|
3f1da7272f | ||
|
|
ea261f4bbb | ||
|
|
e032dc9d70 | ||
|
|
2827fce560 | ||
|
|
bf4a59149a | ||
|
|
cf611c6c8d | ||
|
|
b7a09eda6e | ||
|
|
10df37d0a2 | ||
|
|
58b2049b24 | ||
|
|
29bc89f183 | ||
|
|
adfe69b72f | ||
|
|
f387479ff7 | ||
|
|
684aaeb92f | ||
|
|
0a8d5ca2b6 | ||
|
|
48b38a2fa3 | ||
|
|
aeb043cac5 | ||
|
|
2c6ceb73d8 | ||
|
|
f214f45fef | ||
|
|
616846e1cd | ||
|
|
be88a46df1 | ||
|
|
7386ec09fc | ||
|
|
a068dabc75 | ||
|
|
8fda56793a | ||
|
|
83c7dbde63 | ||
|
|
676fbf7b64 | ||
|
|
8bc726628a | ||
|
|
ee22d21862 | ||
|
|
3894694c9b | ||
|
|
ec4db3de9b | ||
|
|
6a66429431 | ||
|
|
c2d3645b86 | ||
|
|
ea94e4cf0d | ||
|
|
4a7e481bfd | ||
|
|
720123b1e5 | ||
|
|
faf452166c | ||
|
|
0eb7d70caa | ||
|
|
4296eada04 | ||
|
|
983d48a1fe | ||
|
|
556f98faac | ||
|
|
08e052f291 | ||
|
|
ab92cc7a85 | ||
|
|
d50d77aa44 | ||
|
|
3ea8300846 | ||
|
|
beb9b21e1c | ||
|
|
7d7b1849dd | ||
|
|
30d1111768 | ||
|
|
cc15e6a1ee | ||
|
|
66650764df | ||
|
|
191dd76627 | ||
|
|
affe907dfd | ||
|
|
50822c6c74 | ||
|
|
2142d90141 | ||
|
|
9cc7b0d4af | ||
|
|
0917bc0a74 | ||
|
|
0f52441b17 | ||
|
|
bf27904d7c | ||
|
|
3a182d4650 | ||
|
|
a16588c70f | ||
|
|
cf1b2f763e | ||
|
|
381873b926 | ||
|
|
9c14a871e8 | ||
|
|
d7f227d5e5 | ||
|
|
c1accbb204 | ||
|
|
ac2548e7b6 | ||
|
|
87f1cf2b1e | ||
|
|
c49aeeb331 | ||
|
|
506dc7157e | ||
|
|
66f5744d51 | ||
|
|
d4a9c4d7d8 | ||
|
|
f2d0b060b6 | ||
|
|
38191be0d3 | ||
|
|
df2e4145a2 | ||
|
|
d6ab19d4b1 | ||
|
|
7b7ec45bfa | ||
|
|
29941f515f | ||
|
|
625ee1d786 | ||
|
|
a9bb366308 | ||
|
|
657ee7494b | ||
|
|
b60a43f25c | ||
|
|
7004eeefad | ||
|
|
413ef4acb3 | ||
|
|
7f3a1493ef | ||
|
|
8e64932a01 | ||
|
|
9036e7a8c1 | ||
|
|
1bf4a6991d | ||
|
|
03b9080c06 | ||
|
|
4952e32e65 | ||
|
|
ff46c6ac8f | ||
|
|
70bbb4ed5a | ||
|
|
0a4c61071d | ||
|
|
7a58a9524a | ||
|
|
6bcb3fcaf1 | ||
|
|
d921d7f8b6 | ||
|
|
0a677c908d | ||
|
|
a42c0871bd | ||
|
|
4eea97ed1f | ||
|
|
5b0de88218 | ||
|
|
765b0d72b7 | ||
|
|
0e9b049707 |
10
.gitignore
vendored
10
.gitignore
vendored
@@ -4,7 +4,7 @@ node_modules/**/*
|
||||
.pnp.js
|
||||
|
||||
# Expo
|
||||
.expo/*
|
||||
.expo/
|
||||
.expo-shared
|
||||
|
||||
# Build outputs
|
||||
@@ -52,3 +52,11 @@ Thumbs.db
|
||||
*.log
|
||||
*.tmp
|
||||
.cache/
|
||||
|
||||
android/
|
||||
!modules/**/android/
|
||||
ios/
|
||||
!modules/**/ios/
|
||||
*.ipa
|
||||
*.apk
|
||||
*.aab
|
||||
107
App.tsx
107
App.tsx
@@ -1,5 +1,5 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { Platform, UIManager } from "react-native";
|
||||
import { Linking, Platform, UIManager } from "react-native";
|
||||
import { GestureHandlerRootView } from "react-native-gesture-handler";
|
||||
import "./utils/disableFontScaling"; // グローバルなフォントスケーリング無効化
|
||||
import { AppContainer } from "./Apps";
|
||||
@@ -10,7 +10,7 @@ import { CurrentTrainProvider } from "./stateBox/useCurrentTrain";
|
||||
import { AreaInfoProvider } from "./stateBox/useAreaInfo";
|
||||
import { BusAndTrainDataProvider } from "./stateBox/useBusAndTrainData";
|
||||
import { AllTrainDiagramProvider } from "./stateBox/useAllTrainDiagram";
|
||||
import { SheetProvider } from "react-native-actions-sheet";
|
||||
import { SheetProvider, SheetManager } from "react-native-actions-sheet";
|
||||
import "./components/ActionSheetComponents/sheets";
|
||||
import { TrainDelayDataProvider } from "./stateBox/useTrainDelayData";
|
||||
import { SafeAreaProvider } from "react-native-safe-area-context";
|
||||
@@ -20,6 +20,9 @@ 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 { AppThemeProvider } from "./lib/theme";
|
||||
import StatusbarDetect from "./StatusbarDetect";
|
||||
|
||||
LogBox.ignoreLogs([
|
||||
"ViewPropTypes will be removed",
|
||||
@@ -37,6 +40,85 @@ export default function App() {
|
||||
UpdateAsync();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const openFelicaPage = (retryCount = 0) => {
|
||||
if (!rootNavigationRef.isReady()) {
|
||||
if (retryCount < 8) {
|
||||
setTimeout(() => openFelicaPage(retryCount + 1), 250);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
rootNavigationRef.navigate("topMenu", {
|
||||
screen: "setting",
|
||||
params: {
|
||||
screen: "FelicaHistoryPage",
|
||||
},
|
||||
} as any);
|
||||
};
|
||||
|
||||
const navigateWhenReady = (
|
||||
callback: () => void,
|
||||
url: string,
|
||||
retryCount = 0
|
||||
) => {
|
||||
if (!rootNavigationRef.isReady()) {
|
||||
if (retryCount < 8) {
|
||||
setTimeout(() => navigateWhenReady(callback, url, retryCount + 1), 250);
|
||||
}
|
||||
return;
|
||||
}
|
||||
callback();
|
||||
};
|
||||
|
||||
const routeFromUrl = (url: string, retryCount = 0) => {
|
||||
const normalized = (url || "").toLowerCase();
|
||||
if (!normalized) return;
|
||||
|
||||
if (
|
||||
normalized.includes("felicahistorypage") ||
|
||||
normalized.includes("open/felica")
|
||||
) {
|
||||
navigateWhenReady(() => openFelicaPage(), url, retryCount);
|
||||
} else if (normalized.includes("open/traininfo")) {
|
||||
navigateWhenReady(() => {
|
||||
rootNavigationRef.navigate("topMenu", { screen: "menu" } as any);
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
Linking.getInitialURL().then((url) => {
|
||||
if (url) routeFromUrl(url);
|
||||
});
|
||||
|
||||
const sub = Linking.addEventListener("url", ({ url }) => {
|
||||
routeFromUrl(url);
|
||||
});
|
||||
|
||||
return () => {
|
||||
sub.remove();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const ProviderTree = buildProvidersTree([
|
||||
AllTrainDiagramProvider,
|
||||
NotificationProvider,
|
||||
@@ -51,14 +133,17 @@ export default function App() {
|
||||
SheetProvider,
|
||||
]);
|
||||
return (
|
||||
<DeviceOrientationChangeProvider>
|
||||
<SafeAreaProvider>
|
||||
<GestureHandlerRootView style={{ flex: 1 }}>
|
||||
<ProviderTree>
|
||||
<AppContainer />
|
||||
</ProviderTree>
|
||||
</GestureHandlerRootView>
|
||||
</SafeAreaProvider>
|
||||
</DeviceOrientationChangeProvider>
|
||||
<AppThemeProvider>
|
||||
<DeviceOrientationChangeProvider>
|
||||
<SafeAreaProvider>
|
||||
<StatusbarDetect />
|
||||
<GestureHandlerRootView style={{ flex: 1 }}>
|
||||
<ProviderTree>
|
||||
<AppContainer />
|
||||
</ProviderTree>
|
||||
</GestureHandlerRootView>
|
||||
</SafeAreaProvider>
|
||||
</DeviceOrientationChangeProvider>
|
||||
</AppThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
127
Apps.tsx
127
Apps.tsx
@@ -1,14 +1,21 @@
|
||||
import React from "react";
|
||||
import { NavigationContainer, NavigationContainerRef } from "@react-navigation/native";
|
||||
import { NavigationContainer, DarkTheme, DefaultTheme } from "@react-navigation/native";
|
||||
import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";
|
||||
import { Platform } from "react-native";
|
||||
import { Animated, Platform, ActivityIndicator, View, StyleSheet, useColorScheme } from "react-native";
|
||||
import { useNavigationState } from "@react-navigation/native";
|
||||
import { useFonts } from "expo-font";
|
||||
import { LinearGradient } from "expo-linear-gradient";
|
||||
import TNDView from "./ndView";
|
||||
import { initIcon } from "./lib/initIcon";
|
||||
import { Top } from "./Top";
|
||||
import { MenuPage } from "./MenuPage";
|
||||
import { useAreaInfo } from "./stateBox/useAreaInfo";
|
||||
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 { fixedColors } from "./lib/theme/colors";
|
||||
|
||||
type RootTabParamList = {
|
||||
positions: undefined;
|
||||
@@ -25,10 +32,61 @@ type TabProps = {
|
||||
isInfo?: boolean;
|
||||
};
|
||||
|
||||
const Tab = createBottomTabNavigator<RootTabParamList>();
|
||||
|
||||
export function AppContainer() {
|
||||
const Tab = createBottomTabNavigator<RootTabParamList>();
|
||||
const { areaInfo, areaIconBadgeText, isInfo } = useAreaInfo();
|
||||
const navigationRef = React.useRef<NavigationContainerRef<RootTabParamList>>(null);
|
||||
const { selectedLine } = useTrainMenu();
|
||||
const [isExtraWindowOpen, setIsExtraWindowOpen] = React.useState(false);
|
||||
|
||||
// フェードアニメーション用 (0=通常, 1=追加ウィンドウ青)
|
||||
const fadeAnim = React.useRef(new Animated.Value(0)).current;
|
||||
React.useEffect(() => {
|
||||
Animated.timing(fadeAnim, {
|
||||
toValue: isExtraWindowOpen ? 1 : 0,
|
||||
duration: 300,
|
||||
useNativeDriver: false,
|
||||
}).start();
|
||||
}, [isExtraWindowOpen]);
|
||||
|
||||
const lineColor = selectedLine && stationIDPair[selectedLine]
|
||||
? lineColorList[stationIDPair[selectedLine]]
|
||||
: null;
|
||||
|
||||
const darkenHex = (hex: string, factor: number) => {
|
||||
const h = hex.replace("#", "");
|
||||
const r = Math.round(parseInt(h.slice(0, 2), 16) * factor);
|
||||
const g = Math.round(parseInt(h.slice(2, 4), 16) * factor);
|
||||
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 lineColorDark = lineColor ? darkenHex(lineColor, 0.78) : null;
|
||||
const linking = {
|
||||
prefixes: ["jrshikoku://"],
|
||||
config: {
|
||||
screens: {
|
||||
positions: {
|
||||
screens: {
|
||||
Apps: "positions/apps",
|
||||
},
|
||||
},
|
||||
topMenu: {
|
||||
screens: {
|
||||
menu: "topMenu/menu",
|
||||
setting: {
|
||||
screens: {
|
||||
settingTopPage: "topMenu/setting",
|
||||
FelicaHistoryPage: "topMenu/setting/FelicaHistoryPage",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
information: "information",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const getTabProps = (
|
||||
name: keyof RootTabParamList,
|
||||
@@ -53,19 +111,68 @@ export function AppContainer() {
|
||||
"JNR-font": require("./assets/fonts/JNRfont_pict.ttf"),
|
||||
"DiaPro": require("./assets/fonts/DiaPro-Regular.otf"),
|
||||
});
|
||||
if (!fontLoaded) {
|
||||
return (
|
||||
<View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
|
||||
<ActivityIndicator size="large" />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<NavigationContainer ref={navigationRef}>
|
||||
<NavigationContainer
|
||||
ref={rootNavigationRef}
|
||||
linking={linking}
|
||||
theme={isDark ? DarkTheme : DefaultTheme}
|
||||
onStateChange={(state) => {
|
||||
const hasExtra = state?.routes?.some((r) => (r.state?.index ?? 0) > 0) ?? false;
|
||||
setIsExtraWindowOpen(hasExtra);
|
||||
}}
|
||||
>
|
||||
{/* @ts-expect-error - Tab.Navigator type definition issue */}
|
||||
<Tab.Navigator
|
||||
initialRouteName="topMenu"
|
||||
screenOptions={{
|
||||
lazy: false,
|
||||
tabBarHideOnKeyboard: Platform.OS === "android",
|
||||
animation: "shift",
|
||||
screenOptions={({ route }) => {
|
||||
const showGradient = route.name === "positions" && !!lineColor && !!lineColorDark;
|
||||
const defaultBg = isDark ? "#1c1c1e" : "white";
|
||||
const defaultActive = isDark ? "#ffffff" : "#007AFF";
|
||||
const defaultInactive = isDark ? "#8e8e93" : "#8e8e93";
|
||||
return {
|
||||
lazy: false,
|
||||
tabBarHideOnKeyboard: Platform.OS === "android",
|
||||
animation: Platform.OS === "ios" ? "none" : "shift",
|
||||
sceneContainerStyle: { backgroundColor: defaultBg },
|
||||
tabBarActiveTintColor: (showGradient || isExtraWindowOpen) ? "white" : defaultActive,
|
||||
tabBarInactiveTintColor: (showGradient || isExtraWindowOpen) ? "rgba(255,255,255,0.75)" : defaultInactive,
|
||||
tabBarStyle: { backgroundColor: "transparent" },
|
||||
tabBarBackground: () => (
|
||||
<View style={{ flex: 1 }}>
|
||||
{/* 路線カラー or デフォルト背景 */}
|
||||
{showGradient ? (
|
||||
<LinearGradient
|
||||
colors={[lineColor!, lineColorDark!]}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 0, y: 1 }}
|
||||
style={{ ...StyleSheet.absoluteFillObject }}
|
||||
/>
|
||||
) : (
|
||||
<View style={{ ...StyleSheet.absoluteFillObject, backgroundColor: defaultBg }} />
|
||||
)}
|
||||
{/* 追加ウィンドウ時の青グラデーション(フェードイン/アウト) */}
|
||||
<Animated.View style={{ ...StyleSheet.absoluteFillObject, opacity: fadeAnim }}>
|
||||
<LinearGradient
|
||||
colors={[fixedColors.primary, fixedColors.primaryDark]}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 0, y: 1 }}
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
</Animated.View>
|
||||
</View>
|
||||
),
|
||||
};
|
||||
}}
|
||||
>
|
||||
<Tab.Screen
|
||||
{...getTabProps("positions", "走行位置", "barchart", "AntDesign")}
|
||||
{...getTabProps("positions", "走行位置", "bar-chart", "AntDesign")}
|
||||
component={Top}
|
||||
/>
|
||||
<Tab.Screen
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import React, { CSSProperties } from "react";
|
||||
import { BackHandler, View, ViewProps } from "react-native";
|
||||
import { Alert, BackHandler, View, ViewProps } from "react-native";
|
||||
import { WebView } from "react-native-webview";
|
||||
import { BigButton } from "./components/atom/BigButton";
|
||||
import { useFocusEffect, useNavigation } from "@react-navigation/native";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
export default ({ route }) => {
|
||||
if (!route.params) {
|
||||
return null;
|
||||
}
|
||||
const { uri, useExitButton = true } = route.params;
|
||||
const { goBack } = useNavigation();
|
||||
const { fixed } = useThemeColors();
|
||||
const webViewRef = React.useRef<WebView>(null);
|
||||
const [canGoBack, setCanGoBack] = React.useState(false);
|
||||
|
||||
@@ -23,17 +25,25 @@ export default ({ route }) => {
|
||||
return true;
|
||||
};
|
||||
|
||||
BackHandler.addEventListener("hardwareBackPress", onHardwareBack);
|
||||
return () => BackHandler.removeEventListener("hardwareBackPress", onHardwareBack);
|
||||
const subscription = BackHandler.addEventListener("hardwareBackPress", onHardwareBack);
|
||||
return () => subscription.remove();
|
||||
}, [canGoBack, goBack])
|
||||
);
|
||||
return (
|
||||
<View style={styles}>
|
||||
<View style={{ height: "100%", backgroundColor: fixed.primary }}>
|
||||
<WebView
|
||||
source={{ uri }}
|
||||
allowsBackForwardNavigationGestures
|
||||
ref={webViewRef}
|
||||
onNavigationStateChange={(navState) => setCanGoBack(navState.canGoBack)}
|
||||
onNavigationStateChange={(navState) => {
|
||||
setCanGoBack(navState.canGoBack);
|
||||
if (navState.url === "https://unyohub.2pd.jp/integration/succeeded.php") {
|
||||
goBack();
|
||||
Alert.alert("鉄道運用HUBへの投稿完了", "運用HUBからのこのアプリへのデータ反映には暫く時間がかかりますので、しばらくお待ちください。", [
|
||||
{ text: "完了" },
|
||||
]);
|
||||
}
|
||||
}}
|
||||
onMessage={(event) => {
|
||||
const { data } = event.nativeEvent;
|
||||
const { type } = JSON.parse(data);
|
||||
@@ -45,7 +55,3 @@ export default ({ route }) => {
|
||||
</View>
|
||||
);
|
||||
};
|
||||
const styles: ViewProps["style"] = {
|
||||
height: "100%",
|
||||
backgroundColor: "#0099CC",
|
||||
};
|
||||
|
||||
23
MenuPage.tsx
23
MenuPage.tsx
@@ -1,6 +1,6 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { createStackNavigator } from "@react-navigation/stack";
|
||||
import { useWindowDimensions, Platform } from "react-native";
|
||||
import { useWindowDimensions, Platform, useColorScheme } from "react-native";
|
||||
import Constants from "expo-constants";
|
||||
|
||||
import { Dimensions, StatusBar } from "react-native";
|
||||
@@ -30,6 +30,8 @@ export function MenuPage() {
|
||||
const tabBarHeight = useBottomTabBarHeight();
|
||||
const navigation = useNavigation<any>();
|
||||
const { addListener } = navigation;
|
||||
const isDark = useColorScheme() === "dark";
|
||||
const bgColor = isDark ? "#1c1c1e" : "#ffffff";
|
||||
useEffect(() => {
|
||||
AS.getItem(STORAGE_KEYS.START_PAGE)
|
||||
.then((res) => {
|
||||
@@ -56,6 +58,8 @@ export function MenuPage() {
|
||||
const scrollRef = useRef(null);
|
||||
const [mapMode, setMapMode] = useState(false);
|
||||
const [mapHeight, setMapHeight] = useState(0);
|
||||
const mapHeightRef = useRef(0);
|
||||
const favoriteStationRef = useRef(favoriteStation);
|
||||
useEffect(() => {
|
||||
const MapHeight =
|
||||
height -
|
||||
@@ -64,7 +68,11 @@ export function MenuPage() {
|
||||
100 -
|
||||
((((width / 100) * 80) / 20) * 9 + 10 + 30);
|
||||
setMapHeight(MapHeight);
|
||||
mapHeightRef.current = MapHeight;
|
||||
}, [height, tabBarHeight, width]);
|
||||
useEffect(() => {
|
||||
favoriteStationRef.current = favoriteStation;
|
||||
}, [favoriteStation]);
|
||||
const [MapFullHeight, setMapFullHeight] = useState(0);
|
||||
useEffect(() => {
|
||||
const MapFullHeight =
|
||||
@@ -75,15 +83,15 @@ export function MenuPage() {
|
||||
}, [height, tabBarHeight, width]);
|
||||
useEffect(() => {
|
||||
const unsubscribe = addListener("tabPress", (e) => {
|
||||
scrollRef.current.scrollTo({
|
||||
y: mapHeight - 80,
|
||||
scrollRef.current?.scrollTo({
|
||||
y: mapHeightRef.current - 80,
|
||||
animated: true,
|
||||
});
|
||||
setMapMode(false);
|
||||
AS.getItem(STORAGE_KEYS.FAVORITE_STATION)
|
||||
.then((d) => {
|
||||
const returnData = JSON.parse(d);
|
||||
if (favoriteStation.toString() != d) {
|
||||
if (favoriteStationRef.current.toString() != d) {
|
||||
setFavoriteStation(returnData);
|
||||
}
|
||||
})
|
||||
@@ -95,9 +103,12 @@ export function MenuPage() {
|
||||
});
|
||||
|
||||
return unsubscribe;
|
||||
}, [navigation, mapHeight, favoriteStation, setFavoriteStation]);
|
||||
}, [navigation]);
|
||||
return (
|
||||
<Stack.Navigator id={null}>
|
||||
<Stack.Navigator
|
||||
id={null}
|
||||
screenOptions={{ contentStyle: { backgroundColor: bgColor } }}
|
||||
>
|
||||
<Stack.Screen
|
||||
name="menu"
|
||||
options={{
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import React, { FC } from "react";
|
||||
import { Platform, StatusBar, View } from "react-native";
|
||||
import { Platform, StatusBar, useColorScheme } from "react-native";
|
||||
|
||||
const StatusbarDetect: FC = () => {
|
||||
if (Platform.OS == "ios") {
|
||||
return <StatusBar barStyle="dark-content" />;
|
||||
} else if (Platform.OS == "android") {
|
||||
return <View />;
|
||||
}
|
||||
const isDark = useColorScheme() === "dark";
|
||||
const barStyle = isDark ? "light-content" : "dark-content";
|
||||
return <StatusBar barStyle={barStyle} translucent backgroundColor="transparent" />;
|
||||
};
|
||||
|
||||
export default StatusbarDetect;
|
||||
|
||||
22
Top.tsx
22
Top.tsx
@@ -1,6 +1,7 @@
|
||||
import React, { useEffect } from "react";
|
||||
import React, { useCallback, useEffect, useRef } from "react";
|
||||
import { createStackNavigator } from "@react-navigation/stack";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import { useColorScheme } from "react-native";
|
||||
import Apps from "./components/Apps";
|
||||
import TrainBase from "./components/trainbaseview";
|
||||
import HowTo from "./howto";
|
||||
@@ -19,9 +20,15 @@ const Stack = createStackNavigator();
|
||||
export const Top = () => {
|
||||
const { webview } = useCurrentTrain();
|
||||
const { navigate, addListener, isFocused } = useNavigation();
|
||||
const isDark = useColorScheme() === "dark";
|
||||
const bgColor = isDark ? "#1c1c1e" : "#ffffff";
|
||||
|
||||
//地図用
|
||||
const { mapSwitch } = useTrainMenu();
|
||||
const mapSwitchRef = useRef(mapSwitch);
|
||||
useEffect(() => {
|
||||
mapSwitchRef.current = mapSwitch;
|
||||
}, [mapSwitch]);
|
||||
|
||||
const goToFavoriteList = () =>
|
||||
navigate("positions", { screen: "favoriteList" });
|
||||
@@ -31,26 +38,29 @@ export const Top = () => {
|
||||
return unsubscribe;
|
||||
}, []);
|
||||
|
||||
const goToTrainMenu = () => {
|
||||
const goToTrainMenu = useCallback(() => {
|
||||
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 (mapSwitch == "true")
|
||||
else if (mapSwitchRef.current == "true")
|
||||
navigate("positions", { screen: "trainMenu" });
|
||||
else webview.current?.injectJavaScript(`AccordionClassEvent()`);
|
||||
return;
|
||||
};
|
||||
}, [isFocused, navigate, webview]);
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = addListener("tabPress", goToTrainMenu);
|
||||
return unsubscribe;
|
||||
}, [addListener, mapSwitch]);
|
||||
}, [addListener, goToTrainMenu]);
|
||||
|
||||
return (
|
||||
<Stack.Navigator id={null} detachInactiveScreens={false}>
|
||||
<Stack.Navigator
|
||||
id={null}
|
||||
screenOptions={{ contentStyle: { backgroundColor: bgColor } }}
|
||||
>
|
||||
<Stack.Screen
|
||||
name="Apps"
|
||||
options={{
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Platform, ToastAndroid } from "react-native";
|
||||
import * as Updates from "expo-updates";
|
||||
|
||||
export const UpdateAsync = () => {
|
||||
if (__DEV__) return; // dev client では expo-updates は無効
|
||||
Updates.checkForUpdateAsync()
|
||||
.then((update) => {
|
||||
if (!update.isAvailable) return;
|
||||
@@ -16,7 +17,7 @@ export const UpdateAsync = () => {
|
||||
50
|
||||
);
|
||||
}
|
||||
Updates.fetchUpdateAsync().then(Updates.reloadAsync);
|
||||
Updates.fetchUpdateAsync().then(() => Updates.reloadAsync());
|
||||
return;
|
||||
})
|
||||
.catch((e) => {
|
||||
|
||||
94
app.json
94
app.json
@@ -2,12 +2,14 @@
|
||||
"expo": {
|
||||
"name": "JR四国非公式",
|
||||
"slug": "jrshikoku",
|
||||
"scheme": "jrshikoku",
|
||||
"platforms": [
|
||||
"ios",
|
||||
"android",
|
||||
"web"
|
||||
],
|
||||
"version": "6.0.4",
|
||||
"version": "7.0.0",
|
||||
"userInterfaceStyle": "automatic",
|
||||
"orientation": "default",
|
||||
"icon": "./assets/icons/s8600.png",
|
||||
"splash": {
|
||||
@@ -22,9 +24,10 @@
|
||||
"**/*"
|
||||
],
|
||||
"ios": {
|
||||
"buildNumber": "50",
|
||||
"supportsTablet": false,
|
||||
"buildNumber": "60",
|
||||
"supportsTablet": true,
|
||||
"bundleIdentifier": "jrshikokuinfo.xprocess.hrkn",
|
||||
"appleTeamId": "54CRDT797G",
|
||||
"config": {
|
||||
"googleMapsApiKey": "AIzaSyAVGDTjBkR_0wkQiNkoo5WDLhqXCjrjk8Y"
|
||||
},
|
||||
@@ -34,20 +37,54 @@
|
||||
"0003",
|
||||
"FE00"
|
||||
],
|
||||
"ITSAppUsesNonExemptEncryption": false
|
||||
"ITSAppUsesNonExemptEncryption": false,
|
||||
"NSSupportsLiveActivities": true,
|
||||
"NSSupportsLiveActivitiesFrequentUpdates": true
|
||||
},
|
||||
"entitlements": {
|
||||
"com.apple.developer.nfc.readersession.formats": [
|
||||
"TAG"
|
||||
],
|
||||
"com.apple.security.application-groups": [
|
||||
"group.jrshikokuinfo.xprocess.hrkn"
|
||||
]
|
||||
}
|
||||
},
|
||||
"android": {
|
||||
"package": "jrshikokuinfo.xprocess.hrkn",
|
||||
"versionCode": 28,
|
||||
"versionCode": 30,
|
||||
"intentFilters": [
|
||||
{
|
||||
"action": "VIEW",
|
||||
"data": [
|
||||
{
|
||||
"scheme": "jrshikoku"
|
||||
}
|
||||
],
|
||||
"category": [
|
||||
"BROWSABLE",
|
||||
"DEFAULT"
|
||||
]
|
||||
},
|
||||
{
|
||||
"action": "VIEW",
|
||||
"data": [
|
||||
{
|
||||
"scheme": "jrshikoku",
|
||||
"host": "open",
|
||||
"pathPrefix": "/felica"
|
||||
}
|
||||
],
|
||||
"category": [
|
||||
"BROWSABLE",
|
||||
"DEFAULT"
|
||||
]
|
||||
}
|
||||
],
|
||||
"permissions": [
|
||||
"ACCESS_FINE_LOCATION",
|
||||
"NFC",
|
||||
"POST_NOTIFICATIONS",
|
||||
"android.permission.ACCESS_COARSE_LOCATION",
|
||||
"android.permission.ACCESS_FINE_LOCATION"
|
||||
],
|
||||
@@ -67,7 +104,19 @@
|
||||
"policy": "sdkVersion"
|
||||
},
|
||||
"plugins": [
|
||||
"expo-font",
|
||||
"./plugins/with-android-local-properties",
|
||||
"@bacons/apple-targets",
|
||||
[
|
||||
"expo-font",
|
||||
{
|
||||
"fonts": [
|
||||
"./assets/fonts/jr-nishi.otf",
|
||||
"./assets/fonts/DelaGothicOne-Regular.ttf",
|
||||
"./assets/fonts/JNRfont_pict.ttf",
|
||||
"./assets/fonts/DiaPro-Regular.otf"
|
||||
]
|
||||
}
|
||||
],
|
||||
"expo-localization",
|
||||
[
|
||||
"expo-screen-orientation",
|
||||
@@ -124,6 +173,16 @@
|
||||
"previewImage": "./assets/icon.png",
|
||||
"updatePeriodMillis": 1800000,
|
||||
"resizeMode": "horizontal|vertical"
|
||||
},
|
||||
{
|
||||
"name": "JR_shikoku_felica_balance",
|
||||
"label": "ICカード残高",
|
||||
"minWidth": "70dp",
|
||||
"minHeight": "50dp",
|
||||
"description": "Felica対応ICカードの残高をホーム画面に表示するウィジェットです。タップでスキャン画面を開きます。",
|
||||
"previewImage": "./assets/icon.png",
|
||||
"updatePeriodMillis": 1800000,
|
||||
"resizeMode": "horizontal|vertical"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -452,6 +511,29 @@
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
[
|
||||
"expo-build-properties",
|
||||
{
|
||||
"android": {
|
||||
"kotlinVersion": "2.1.20"
|
||||
},
|
||||
"ios": {
|
||||
"deploymentTarget": "16.2"
|
||||
}
|
||||
}
|
||||
],
|
||||
"expo-audio",
|
||||
"expo-video",
|
||||
"expo-web-browser",
|
||||
"expo-asset",
|
||||
"expo-sharing",
|
||||
[
|
||||
"react-native-maps",
|
||||
{
|
||||
"iosGoogleMapsApiKey": "AIzaSyAVGDTjBkR_0wkQiNkoo5WDLhqXCjrjk8Y",
|
||||
"androidGoogleMapsApiKey": "AIzaSyAmFb-Yj033bXZWlSzNrfq_0jc1PgRrWcE"
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
BIN
assets/icons/elesite_logo.png
Normal file
BIN
assets/icons/elesite_logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
BIN
assets/icons/hub_logo.png
Normal file
BIN
assets/icons/hub_logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.7 KiB |
21
assets/originData/spots.ts
Normal file
21
assets/originData/spots.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* 観光スポットデータ
|
||||
* StationNumber は "SP" プレフィックスで管理(路線とは別系統)
|
||||
* Station_JP の先頭ドット(.)はダイヤデータのキーと一致させるための命名規則
|
||||
*/
|
||||
export default [
|
||||
{
|
||||
Station_JP: ".与島",
|
||||
Station_EN: "Yoshima",
|
||||
MyStation: "0",
|
||||
StationNumber: null,
|
||||
DispNum: "3",
|
||||
StationTimeTable: "",
|
||||
StationMap: "https://www.google.co.jp/maps/place/34.389472,133.816444",
|
||||
JrHpUrl: "https://www.jb-honshi.co.jp/yoshimapa/",
|
||||
jslodApi: "spot",
|
||||
lat: 34.389472,
|
||||
lng: 133.816444,
|
||||
isSpot: true,
|
||||
},
|
||||
];
|
||||
BIN
assets/sound/rikka-test.mp3
Normal file
BIN
assets/sound/rikka-test.mp3
Normal file
Binary file not shown.
@@ -1,6 +1,15 @@
|
||||
module.exports = function(api) {
|
||||
api.cache(true);
|
||||
return {
|
||||
presets: ['babel-preset-expo'],
|
||||
presets: [
|
||||
['babel-preset-expo', {
|
||||
'react-compiler': { enabled: false },
|
||||
lazyImports: true,
|
||||
}],
|
||||
],
|
||||
plugins: [
|
||||
'./plugins/babel-plugin-disable-font-scaling',
|
||||
'react-native-reanimated/plugin',
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { FC } from "react";
|
||||
import { View, Text, TouchableWithoutFeedback, Alert } from "react-native";
|
||||
import { useAllTrainDiagram } from "@/stateBox/useAllTrainDiagram";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
|
||||
export const DataConnectedButton: FC<{
|
||||
i: string;
|
||||
@@ -8,6 +9,7 @@ export const DataConnectedButton: FC<{
|
||||
}> = ({ i, openTrainInfo }) => {
|
||||
const [station, se, time] = i.split(",");
|
||||
const { keyList } = useAllTrainDiagram();
|
||||
const { colors } = useThemeColors();
|
||||
// 列番が有効かどうかをチェックする関数
|
||||
const isValidTrainNumber = (trainNum: string): boolean => {
|
||||
return keyList.includes(trainNum);
|
||||
@@ -30,13 +32,13 @@ export const DataConnectedButton: FC<{
|
||||
}}
|
||||
key={station + time}
|
||||
>
|
||||
<View style={{ flexDirection: "row", backgroundColor: "#f5f5f5" }}>
|
||||
<View style={{ flexDirection: "row", backgroundColor: colors.backgroundTertiary }}>
|
||||
<View
|
||||
style={{
|
||||
padding: 8,
|
||||
flexDirection: "row",
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: "#f0f0f0",
|
||||
borderBottomColor: colors.borderLight,
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
@@ -49,12 +51,12 @@ export const DataConnectedButton: FC<{
|
||||
height: "10%",
|
||||
}}
|
||||
/>
|
||||
<Text style={{ fontSize:16, fontFamily: "DiaPro" }}>
|
||||
<Text style={{ fontSize:16, fontFamily: "DiaPro", color: colors.text }}>
|
||||
{se === "増" ? "⬐" : "↳"}
|
||||
</Text>
|
||||
<Text style={{ fontSize: 20, color: "#0000EE" }}>{time}</Text>
|
||||
<Text style={{ fontSize: 20, color: colors.textLink }}>{time}</Text>
|
||||
<View style={{ flex: 1 }} />
|
||||
<Text style={{ fontSize: 18, width: 50 }}>
|
||||
<Text style={{ fontSize: 18, width: 50, color: colors.text }}>
|
||||
{se === "増" ? "増結" : "解結"}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
parseSeString,
|
||||
} from "@/utils/seUtils";
|
||||
import type { SeTypes } from "@/types";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
|
||||
type seTypes =
|
||||
| "発編"
|
||||
@@ -63,6 +64,7 @@ export const EachStopList: FC<props> = ({
|
||||
array,
|
||||
isNotService = false,
|
||||
}) => {
|
||||
const { colors: themeColors, isDark } = useThemeColors();
|
||||
const [station, se, time, platformNum] = i.split(",") as [
|
||||
string,
|
||||
seTypes,
|
||||
@@ -70,22 +72,22 @@ export const EachStopList: FC<props> = ({
|
||||
string
|
||||
]; // 阿波池田,発,6:21,1
|
||||
let beforeSameStationData = null;
|
||||
// 運休系でない通常の発のみ、前の着を統合する
|
||||
// 休編(非推奨)は発着が不明なため、次の発と統合する
|
||||
if ((se.includes("発") && !se.includes("休")) || se === "休編") {
|
||||
// 発(通常発・休発・休発編)の場合、前の着(通常着・休着・休着編)と統合する
|
||||
if (se.includes("発")) {
|
||||
if (index > 0) {
|
||||
const beforeData = array[index - 1].split(",") as [string, seTypes, string];
|
||||
if (beforeData[0] == station) {
|
||||
// 前が着(通常着でも休着でも)の場合は統合
|
||||
if (beforeData[0] == station && beforeData[1].includes("着")) {
|
||||
beforeSameStationData = beforeData;
|
||||
}
|
||||
}
|
||||
}
|
||||
let afterSameStationData = null;
|
||||
// 運休系でない通常の着のみ、次の発と統合する
|
||||
// 運休着(休着、休着編)は独立して表示する必要がある
|
||||
if (se.includes("着") && !se.includes("休")) {
|
||||
// 着(通常着・休着・休着編)の場合、次の発(通常発・休発・休発編)と統合される(非表示)
|
||||
if (se.includes("着")) {
|
||||
const afterData = array[index + 1]?.split(",") as [string, seTypes, string];
|
||||
if (afterData && afterData[0] == station) {
|
||||
// 次が発(通常発でも休発でも)なら、この着を非表示にして次の発で両方表示
|
||||
if (afterData && afterData[0] == station && afterData[1].includes("発")) {
|
||||
afterSameStationData = afterData;
|
||||
return <></>;
|
||||
}
|
||||
@@ -135,7 +137,8 @@ export const EachStopList: FC<props> = ({
|
||||
isCommunity,
|
||||
isCanceled,
|
||||
isDelayed,
|
||||
isNotService
|
||||
isNotService,
|
||||
isDark
|
||||
);
|
||||
// 打ち消し線用の通常色(遅延していない時の色)
|
||||
const normalColors = getStopListColors(
|
||||
@@ -143,7 +146,8 @@ export const EachStopList: FC<props> = ({
|
||||
isCommunity,
|
||||
isCanceled,
|
||||
false,
|
||||
isNotService
|
||||
isNotService,
|
||||
isDark
|
||||
);
|
||||
|
||||
// beforeSameStationData用の色設定
|
||||
@@ -161,14 +165,16 @@ export const EachStopList: FC<props> = ({
|
||||
beforeIsCommunity,
|
||||
isCanceled,
|
||||
isDelayed,
|
||||
isNotService
|
||||
isNotService,
|
||||
isDark
|
||||
);
|
||||
const beforeNormalColors = getStopListColors(
|
||||
beforeIsThrough,
|
||||
beforeIsCommunity,
|
||||
isCanceled,
|
||||
false,
|
||||
isNotService
|
||||
isNotService,
|
||||
isDark
|
||||
);
|
||||
beforeTimeTextColor = beforeColors.timeText;
|
||||
beforeNormalTimeTextColor = beforeNormalColors.timeText;
|
||||
@@ -233,7 +239,7 @@ export const EachStopList: FC<props> = ({
|
||||
padding: 8,
|
||||
flexDirection: "row",
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: "#f0f0f0",
|
||||
borderBottomColor: themeColors.borderLight,
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
@@ -349,6 +355,7 @@ const TimeText: FC<{
|
||||
};
|
||||
const StationNumbersBox: FC<{ stn: string; se: seTypes }> = (props) => {
|
||||
const { stn, se } = props;
|
||||
const { fixed } = useThemeColors();
|
||||
const lineColor = lineColorList[stn.charAt(0)];
|
||||
const hasThrew =
|
||||
se == "通過" ||
|
||||
@@ -366,7 +373,7 @@ const StationNumbersBox: FC<{ stn: string; se: seTypes }> = (props) => {
|
||||
<View style={{ flex: 1 }} />
|
||||
<Text
|
||||
style={{
|
||||
color: "white",
|
||||
color: fixed.textOnPrimary,
|
||||
textAlign: "center",
|
||||
fontSize: 10,
|
||||
fontWeight: "bold",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { trainPosition, trainDataType } from "@/lib/trainPositionTextArray";
|
||||
import React, { FC } from "react";
|
||||
import { View, Text, TextStyle, ViewStyle } from "react-native";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
|
||||
type stateBox = {
|
||||
currentTrainData: trainDataType | undefined;
|
||||
@@ -21,6 +22,7 @@ export const PositionBox: FC<stateBox> = (props) => {
|
||||
platformDescription,
|
||||
lineNumber,
|
||||
} = props;
|
||||
const { colors } = useThemeColors();
|
||||
let firstText = "";
|
||||
let secondText = "";
|
||||
let marginText = "";
|
||||
@@ -46,22 +48,22 @@ export const PositionBox: FC<stateBox> = (props) => {
|
||||
}
|
||||
}
|
||||
return (
|
||||
<View style={{ ...(mode == 2 ? boxStyle2 : boxStyle), ...style }}>
|
||||
<Text style={{ fontSize: 12, color: "#0099CC" }}>{title}</Text>
|
||||
<View style={{ ...(mode == 2 ? boxStyle2 : boxStyle), backgroundColor: colors.surface, ...style }}>
|
||||
<Text style={{ fontSize: 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)}>
|
||||
<Text style={[mode == 2 ? boxTextStyle2 : (isBetween ? boxTextStyle : boxTextStyleBig), { color: colors.textAccent }]}>
|
||||
{firstText}
|
||||
</Text>
|
||||
)}
|
||||
{marginText && (
|
||||
<Text style={{ color: "#0099CC", textAlign: "right" }}>
|
||||
<Text style={{ color: colors.textAccent, textAlign: "right" }}>
|
||||
{marginText}
|
||||
</Text>
|
||||
)}
|
||||
{secondText && (
|
||||
<Text style={mode == 2 ? boxTextStyle2 :(isBetween ? boxTextStyle : boxTextStyleMini)}>
|
||||
<Text style={[mode == 2 ? boxTextStyle2 :(isBetween ? boxTextStyle : boxTextStyleMini), { color: colors.textAccent }]}>
|
||||
{secondText}
|
||||
</Text>
|
||||
)}
|
||||
@@ -72,6 +74,7 @@ export const PositionBox: FC<stateBox> = (props) => {
|
||||
style={{
|
||||
...{ ...(mode == 2 ? boxTextStyle2 : boxTextStyle) },
|
||||
fontSize: 10,
|
||||
color: colors.textAccent,
|
||||
}}
|
||||
>
|
||||
{" " + externalText}
|
||||
@@ -84,39 +87,33 @@ export const PositionBox: FC<stateBox> = (props) => {
|
||||
};
|
||||
const boxStyle: ViewStyle = {
|
||||
flex: 1,
|
||||
backgroundColor: "white",
|
||||
borderRadius: 10,
|
||||
padding: 10,
|
||||
margin: 10,
|
||||
};
|
||||
const boxStyle2: ViewStyle = {
|
||||
flex: 1,
|
||||
backgroundColor: "white",
|
||||
borderRadius: 10,
|
||||
padding: 5,
|
||||
margin: 5,
|
||||
};
|
||||
const boxTextStyle2: TextStyle = {
|
||||
fontSize: 18,
|
||||
color: "#0099CC",
|
||||
textAlign: "right",
|
||||
};
|
||||
const boxTextStyleBig: TextStyle = {
|
||||
fontSize: 28,
|
||||
color: "#0099CC",
|
||||
textAlign: "right",
|
||||
};
|
||||
|
||||
|
||||
const boxTextStyleMini: TextStyle = {
|
||||
fontSize: 16,
|
||||
color: "#0099CC",
|
||||
textAlign: "right",
|
||||
};
|
||||
|
||||
|
||||
const boxTextStyle: TextStyle = {
|
||||
fontSize: 25,
|
||||
color: "#0099CC",
|
||||
textAlign: "right",
|
||||
};
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import React from "react";
|
||||
import { View, Text, LayoutAnimation, TouchableOpacity } from "react-native";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
|
||||
export const ScrollStickyContent = (props) => {
|
||||
const { currentTrainData, showThrew, setShowThrew, haveThrough } = props;
|
||||
const { colors } = useThemeColors();
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
alignItems: "center",
|
||||
backgroundColor: "#ffffffc2",
|
||||
backgroundColor: colors.surface,
|
||||
flexDirection: "row",
|
||||
}}
|
||||
>
|
||||
@@ -16,18 +18,18 @@ export const ScrollStickyContent = (props) => {
|
||||
padding: 8,
|
||||
flexDirection: "row",
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: "#ffffffc2",
|
||||
borderBottomColor: colors.border,
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
<Text style={{ fontSize: 20 }}>停車駅</Text>
|
||||
<Text style={{ fontSize: 20, color: colors.text }}>停車駅</Text>
|
||||
<View style={{ flex: 1 }} />
|
||||
<View style={{ flexDirection: "row" }}>
|
||||
{!isNaN(currentTrainData?.delay) && currentTrainData?.delay != 0 && (
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 15,
|
||||
color: "black",
|
||||
color: colors.text,
|
||||
position: "absolute",
|
||||
right: 110,
|
||||
textAlign: "right",
|
||||
@@ -41,10 +43,10 @@ export const ScrollStickyContent = (props) => {
|
||||
style={{
|
||||
fontSize: 20,
|
||||
color: isNaN(currentTrainData?.delay)
|
||||
? "black"
|
||||
? colors.text
|
||||
: currentTrainData?.delay == 0
|
||||
? "black"
|
||||
: "red",
|
||||
? colors.text
|
||||
: colors.textError,
|
||||
width: 60,
|
||||
}}
|
||||
>
|
||||
@@ -70,6 +72,7 @@ export const ScrollStickyContent = (props) => {
|
||||
textAlign: "center",
|
||||
textAlignVertical: "center",
|
||||
opacity: haveThrough ? 1 : 0,
|
||||
color: colors.text,
|
||||
}}
|
||||
>
|
||||
(通過{showThrew ? "▼" : "▶"})
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { Text, TouchableOpacity } from "react-native";
|
||||
import React, { useState } from "react";
|
||||
import { useAllTrainDiagram } from "@/stateBox/useAllTrainDiagram";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
export const ShowSpecialTrain = ({
|
||||
isTrainDataNothing,
|
||||
setTrainData,
|
||||
trueTrainID,
|
||||
}) => {
|
||||
const { allTrainDiagram } = useAllTrainDiagram();
|
||||
const { colors } = useThemeColors();
|
||||
const replaceSpecialTrainDetail = (trainNum) => {
|
||||
let TD = allTrainDiagram[trainNum];
|
||||
if (!TD) return;
|
||||
@@ -22,7 +24,7 @@ export const ShowSpecialTrain = ({
|
||||
style={{
|
||||
padding: 10,
|
||||
flexDirection: "row",
|
||||
borderColor: "blue",
|
||||
borderColor: colors.textAccent,
|
||||
borderWidth: 1,
|
||||
margin: 10,
|
||||
borderRadius: 5,
|
||||
@@ -30,7 +32,7 @@ export const ShowSpecialTrain = ({
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{ fontSize: 18, fontWeight: "bold", color: "black" }}
|
||||
style={{ fontSize: 18, fontWeight: "bold", color: colors.text }}
|
||||
>
|
||||
本来の列車情報候補を表示:({ids})
|
||||
</Text>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { CSSProperties, FC } from "react";
|
||||
import { View, Text, StyleProp, TextStyle, ViewStyle } from "react-native";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
|
||||
type stateBox = {
|
||||
text: string;
|
||||
@@ -10,12 +11,13 @@ type stateBox = {
|
||||
};
|
||||
export const StateBox: FC<stateBox> = (props) => {
|
||||
const { text, title, style, mode, endText } = props;
|
||||
const { colors } = useThemeColors();
|
||||
return (
|
||||
<View style={{ ...(mode == 2 ? boxStyle2 : boxStyle), ...style }}>
|
||||
<Text style={{ fontSize: 12, color: "#0099CC" }}>{title}</Text>
|
||||
<View style={{ ...(mode == 2 ? boxStyle2 : boxStyle), backgroundColor: colors.surface, ...style }}>
|
||||
<Text style={{ fontSize: 12, color: colors.textAccent }}>{title}</Text>
|
||||
<View style={{ flex: 1 }} />
|
||||
<View style={{ flexDirection: mode == 2 ? "row" : "column" }}>
|
||||
<Text style={mode == 2 ? boxTextStyle2 : boxTextStyle}>{text}</Text>
|
||||
<Text style={[mode == 2 ? boxTextStyle2 : boxTextStyle, { color: colors.textAccent }]}>{text}</Text>
|
||||
</View>
|
||||
{endText && (
|
||||
<View style={{ flexDirection: mode == 2 ? "row" : "column" }}>
|
||||
@@ -23,6 +25,7 @@ export const StateBox: FC<stateBox> = (props) => {
|
||||
style={{
|
||||
...{ ...(mode == 2 ? boxTextStyle2 : boxTextStyle) },
|
||||
fontSize: 10,
|
||||
color: colors.textAccent,
|
||||
}}
|
||||
>
|
||||
{endText}
|
||||
@@ -34,26 +37,22 @@ export const StateBox: FC<stateBox> = (props) => {
|
||||
};
|
||||
const boxStyle: ViewStyle = {
|
||||
flex: 1,
|
||||
backgroundColor: "white",
|
||||
borderRadius: 10,
|
||||
padding: 10,
|
||||
margin: 10,
|
||||
};
|
||||
const boxStyle2: ViewStyle = {
|
||||
flex: 1,
|
||||
backgroundColor: "white",
|
||||
borderRadius: 10,
|
||||
padding: 5,
|
||||
margin: 5,
|
||||
};
|
||||
const boxTextStyle2: TextStyle = {
|
||||
fontSize: 18,
|
||||
color: "#0099CC",
|
||||
textAlign: "right",
|
||||
};
|
||||
|
||||
const boxTextStyle: TextStyle = {
|
||||
fontSize: 25,
|
||||
color: "#0099CC",
|
||||
textAlign: "right",
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useEffect, FC } from "react";
|
||||
import { View, TouchableOpacity, useWindowDimensions } from "react-native";
|
||||
import { View, TouchableOpacity, useWindowDimensions, Text } from "react-native";
|
||||
import { StateBox } from "./StateBox";
|
||||
import { PositionBox } from "./PositionBox";
|
||||
import { useDeviceOrientationChange } from "../../../stateBox/useDeviceOrientationChange";
|
||||
@@ -129,6 +129,8 @@ export const TrainDataView:FC<props> = ({
|
||||
setDialog(true);
|
||||
};
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<StationPosPushDialog
|
||||
|
||||
@@ -18,13 +18,15 @@ type ColorScheme = {
|
||||
* @param isCanceled 運休かどうか
|
||||
* @param isDelayed 遅延しているかどうか
|
||||
* @param isNotService 回送列車かどうか
|
||||
* @param isDark ダークモードかどうか
|
||||
*/
|
||||
export const getStopListColors = (
|
||||
isThrough: boolean,
|
||||
isCommunity: boolean,
|
||||
isCanceled: boolean,
|
||||
isDelayed: boolean,
|
||||
isNotService: boolean = false
|
||||
isNotService: boolean = false,
|
||||
isDark: boolean = false
|
||||
): ColorScheme => {
|
||||
// 最優先: 回送列車の場合
|
||||
if (isNotService) {
|
||||
@@ -32,45 +34,47 @@ export const getStopListColors = (
|
||||
// 回送 + 運休
|
||||
if (isThrough) {
|
||||
return {
|
||||
background: '#1a1a1a', // 非常に濃いグレー
|
||||
text: isCommunity ? '#8090c0' : '#909090', // 暗めの青灰色 or 暗めのグレー
|
||||
timeText: isDelayed
|
||||
? (isCommunity ? '#aa7799' : '#aa7777') // 遅延時: 暗めのピンク系の赤
|
||||
: (isCommunity ? '#8090c0' : '#909090'),
|
||||
seText: isCommunity ? '#8090c0' : '#909090',
|
||||
background: isDark ? '#111111' : '#1a1a1a',
|
||||
text: isCommunity ? '#8090c0' : (isDark ? '#707070' : '#909090'),
|
||||
timeText: isDelayed
|
||||
? (isCommunity ? '#aa7799' : '#aa7777')
|
||||
: (isCommunity ? '#8090c0' : (isDark ? '#707070' : '#909090')),
|
||||
seText: isCommunity ? '#8090c0' : (isDark ? '#707070' : '#909090'),
|
||||
opacity: '0.5',
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
background: '#3a3a3a', // 濃いグレー
|
||||
text: isCommunity ? '#8090c0' : '#b0b0b0', // 暗めの青灰色 or 明るめのグレー
|
||||
background: isDark ? '#2a2a2a' : '#3a3a3a',
|
||||
text: isCommunity ? '#8090c0' : (isDark ? '#c0c0c0' : '#b0b0b0'),
|
||||
timeText: isDelayed
|
||||
? (isCommunity ? '#bb8899' : '#bb8888') // 遅延時: 暗めのピンク系の赤
|
||||
: (isCommunity ? '#8090c0' : '#b0b0b0'),
|
||||
seText: isCommunity ? '#8090c0' : '#b0b0b0',
|
||||
? (isCommunity ? '#bb8899' : '#bb8888')
|
||||
: (isCommunity ? '#8090c0' : (isDark ? '#c0c0c0' : '#b0b0b0')),
|
||||
seText: isCommunity ? '#8090c0' : (isDark ? '#c0c0c0' : '#b0b0b0'),
|
||||
opacity: '0.8',
|
||||
};
|
||||
}
|
||||
} else if (isThrough) {
|
||||
// 回送 + 通過
|
||||
return {
|
||||
background: '#e8e8e8', // 薄いグレー
|
||||
text: isCommunity ? '#6677cc' : '#777777', // 暗めの青 or グレー
|
||||
timeText: isDelayed
|
||||
? (isCommunity ? '#cc5577' : '#dd5555') // 遅延時: 暗めの赤
|
||||
: (isCommunity ? '#6677cc' : '#777777'),
|
||||
seText: isCommunity ? '#6677cc' : '#777777',
|
||||
background: isDark ? '#2a2a2a' : '#e8e8e8',
|
||||
text: isCommunity ? (isDark ? '#8899ee' : '#6677cc') : (isDark ? '#aaaaaa' : '#777777'),
|
||||
timeText: isDelayed
|
||||
? (isCommunity ? '#cc5577' : '#dd5555')
|
||||
: (isCommunity ? (isDark ? '#8899ee' : '#6677cc') : (isDark ? '#aaaaaa' : '#777777')),
|
||||
seText: isCommunity ? (isDark ? '#8899ee' : '#6677cc') : (isDark ? '#aaaaaa' : '#777777'),
|
||||
opacity: '0.6',
|
||||
};
|
||||
} else {
|
||||
// 回送 + 通常停車
|
||||
return {
|
||||
background: isDelayed ? '#f5f0f0' : '#f5f5f5', // 遅延時は少し赤みのあるグレー
|
||||
text: isCommunity ? '#4455aa' : '#555555', // 暗めの青 or ダークグレー
|
||||
timeText: isDelayed
|
||||
? (isCommunity ? '#bb3355' : '#cc0000') // 遅延時: 暗めの赤
|
||||
: (isCommunity ? '#4455aa' : '#555555'),
|
||||
seText: isCommunity ? '#4455aa' : '#555555',
|
||||
background: isDark
|
||||
? (isDelayed ? '#2a1a1a' : '#222222')
|
||||
: (isDelayed ? '#f5f0f0' : '#f5f5f5'),
|
||||
text: isCommunity ? (isDark ? '#7788cc' : '#4455aa') : (isDark ? '#aaaaaa' : '#555555'),
|
||||
timeText: isDelayed
|
||||
? (isCommunity ? '#bb3355' : '#cc0000')
|
||||
: (isCommunity ? (isDark ? '#7788cc' : '#4455aa') : (isDark ? '#aaaaaa' : '#555555')),
|
||||
seText: isCommunity ? (isDark ? '#7788cc' : '#4455aa') : (isDark ? '#aaaaaa' : '#555555'),
|
||||
opacity: '0.85',
|
||||
};
|
||||
}
|
||||
@@ -81,10 +85,10 @@ export const getStopListColors = (
|
||||
if (isThrough) {
|
||||
// 通過系 + 運休
|
||||
return {
|
||||
background: '#2a2a2a', // 濃いグレー
|
||||
text: isCommunity ? '#a8b5ff' : '#c0c0c0', // 薄い青 or 薄いグレー
|
||||
timeText: isDelayed
|
||||
? (isCommunity ? '#dd99bb' : '#dd9999') // 遅延時: 薄いピンク系の赤
|
||||
background: isDark ? '#1e1e2e' : '#2a2a2a',
|
||||
text: isCommunity ? '#a8b5ff' : '#c0c0c0',
|
||||
timeText: isDelayed
|
||||
? (isCommunity ? '#dd99bb' : '#dd9999')
|
||||
: (isCommunity ? '#a8b5ff' : '#c0c0c0'),
|
||||
seText: isCommunity ? '#a8b5ff' : '#c0c0c0',
|
||||
opacity: '0.6',
|
||||
@@ -92,10 +96,10 @@ export const getStopListColors = (
|
||||
} else {
|
||||
// 通常停車 + 運休
|
||||
return {
|
||||
background: '#5a5a5a', // 中程度のグレー
|
||||
text: isCommunity ? '#a8b5ff' : '#ffffff', // 薄い青 or 白
|
||||
background: isDark ? '#3a3a4a' : '#5a5a5a',
|
||||
text: isCommunity ? '#a8b5ff' : '#ffffff',
|
||||
timeText: isDelayed
|
||||
? (isCommunity ? '#ffaacc' : '#ffaaaa') // 遅延時: 明るいピンク系の赤
|
||||
? (isCommunity ? '#ffaacc' : '#ffaaaa')
|
||||
: (isCommunity ? '#a8b5ff' : '#ffffff'),
|
||||
seText: isCommunity ? '#a8b5ff' : '#ffffff',
|
||||
opacity: '0.9',
|
||||
@@ -106,12 +110,14 @@ export const getStopListColors = (
|
||||
// 次: 通過系の場合
|
||||
if (isThrough) {
|
||||
return {
|
||||
background: isCommunity ? '#f0f4ff' : '#fafafa', // 薄い青背景 or 薄いグレー
|
||||
text: isCommunity ? '#5577ff' : '#888888', // 中程度の青 or グレー
|
||||
timeText: isDelayed
|
||||
? (isCommunity ? '#dd5588' : '#ff6666') // 遅延時: コミュニティは紫がかった赤、通常は明るい赤
|
||||
: (isCommunity ? '#5577ff' : '#888888'),
|
||||
seText: isCommunity ? '#5577ff' : '#888888',
|
||||
background: isDark
|
||||
? (isCommunity ? '#1a1e2e' : '#1e1e1e')
|
||||
: (isCommunity ? '#f0f4ff' : '#fafafa'),
|
||||
text: isCommunity ? (isDark ? '#7799ff' : '#5577ff') : (isDark ? '#666666' : '#888888'),
|
||||
timeText: isDelayed
|
||||
? (isCommunity ? '#dd5588' : '#ff6666')
|
||||
: (isCommunity ? (isDark ? '#7799ff' : '#5577ff') : (isDark ? '#666666' : '#888888')),
|
||||
seText: isCommunity ? (isDark ? '#7799ff' : '#5577ff') : (isDark ? '#666666' : '#888888'),
|
||||
opacity: '0.5',
|
||||
};
|
||||
}
|
||||
@@ -120,19 +126,27 @@ export const getStopListColors = (
|
||||
if (isCommunity) {
|
||||
// コミュニティ投稿
|
||||
return {
|
||||
background: isDelayed ? '#fff5f5' : '#ffffff', // 遅延時は薄い赤背景
|
||||
text: '#3355dd', // 明確な青
|
||||
timeText: isDelayed ? '#cc2266' : '#3355dd', // 遅延時: 紫がかった赤
|
||||
seText: '#3355dd',
|
||||
background: isDark
|
||||
? (isDelayed ? '#2a1a2a' : '#1e1e2e')
|
||||
: (isDelayed ? '#fff5f5' : '#ffffff'),
|
||||
text: isDark ? '#8899ff' : '#3355dd',
|
||||
timeText: isDelayed
|
||||
? (isDark ? '#ee4488' : '#cc2266')
|
||||
: (isDark ? '#8899ff' : '#3355dd'),
|
||||
seText: isDark ? '#8899ff' : '#3355dd',
|
||||
opacity: '0.95',
|
||||
};
|
||||
} else {
|
||||
// 公式データ
|
||||
return {
|
||||
background: isDelayed ? '#fff5f5' : '#ffffff', // 遅延時は薄い赤背景
|
||||
text: '#000000', // 黒
|
||||
timeText: isDelayed ? '#dd0000' : '#000000', // 遅延時: 標準的な赤
|
||||
seText: '#000000',
|
||||
background: isDark
|
||||
? (isDelayed ? '#2a1a1a' : '#1e1e2e')
|
||||
: (isDelayed ? '#fff5f5' : '#ffffff'),
|
||||
text: isDark ? '#e0e0e0' : '#000000',
|
||||
timeText: isDelayed
|
||||
? (isDark ? '#ff5555' : '#dd0000')
|
||||
: (isDark ? '#e0e0e0' : '#000000'),
|
||||
seText: isDark ? '#e0e0e0' : '#000000',
|
||||
opacity: '0.95',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
BackHandler,
|
||||
Linking,
|
||||
} from "react-native";
|
||||
import { SheetManager, useScrollHandlers } from "react-native-actions-sheet";
|
||||
import { SheetManager } from "react-native-actions-sheet";
|
||||
import { getTrainType } from "../../lib/getTrainType";
|
||||
import { customTrainDataDetector } from "../custom-train-data";
|
||||
import { useDeviceOrientationChange } from "../../stateBox/useDeviceOrientationChange";
|
||||
@@ -22,6 +22,7 @@ import { ShowSpecialTrain } from "./EachTrainInfo/ShowSpecialTrain";
|
||||
import { useTrainMenu } from "../../stateBox/useTrainMenu";
|
||||
import { HeaderText } from "./EachTrainInfoCore/HeaderText";
|
||||
import { useStationList } from "../../stateBox/useStationList";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
import { useAllTrainDiagram } from "@/stateBox/useAllTrainDiagram";
|
||||
|
||||
// Custom hooks
|
||||
@@ -42,14 +43,12 @@ export const EachTrainInfoCore = ({
|
||||
}) => {
|
||||
const { stationList } = useStationList();
|
||||
const { allCustomTrainData } = useAllTrainDiagram();
|
||||
const { colors, fixed } = useThemeColors();
|
||||
const { setTrainInfo } = useTrainMenu();
|
||||
const { height } = useWindowDimensions();
|
||||
const { isLandscape } = useDeviceOrientationChange();
|
||||
|
||||
const scrollHandlers = actionSheetRef
|
||||
? //@ts-ignore
|
||||
useScrollHandlers("scrollview-1", actionSheetRef)
|
||||
: null;
|
||||
const scrollRef = useRef<any>(null);
|
||||
// Custom hooks for data management
|
||||
const { trainData, setTrainData, trueTrainID } = useTrainDiagramData(
|
||||
data.trainNum
|
||||
@@ -78,7 +77,7 @@ export const EachTrainInfoCore = ({
|
||||
useAutoScroll(
|
||||
points,
|
||||
trainDataWithThrough,
|
||||
scrollHandlers,
|
||||
scrollRef,
|
||||
isJumped,
|
||||
setIsJumped,
|
||||
setShowThrew
|
||||
@@ -135,10 +134,10 @@ export const EachTrainInfoCore = ({
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: "#0099CC",
|
||||
backgroundColor: fixed.primary,
|
||||
borderTopLeftRadius: 5,
|
||||
borderTopRightRadius: 5,
|
||||
borderColor: "dark",
|
||||
borderColor: colors.border,
|
||||
borderWidth: 1,
|
||||
}}
|
||||
>
|
||||
@@ -148,7 +147,7 @@ export const EachTrainInfoCore = ({
|
||||
height: 6,
|
||||
width: 45,
|
||||
borderRadius: 100,
|
||||
backgroundColor: "#f0f0f0",
|
||||
backgroundColor: colors.borderLight,
|
||||
marginVertical: 10,
|
||||
alignSelf: "center",
|
||||
}}
|
||||
@@ -164,18 +163,18 @@ export const EachTrainInfoCore = ({
|
||||
navigate={navigate}
|
||||
from={from}
|
||||
fontLoaded={true}
|
||||
scrollHandlers={scrollHandlers}
|
||||
scrollRef={scrollRef}
|
||||
/>
|
||||
|
||||
<DynamicHeaderScrollView
|
||||
from={from}
|
||||
styles={styles as any}
|
||||
scrollHandlers={scrollHandlers}
|
||||
scrollRef={scrollRef}
|
||||
containerProps={{
|
||||
style: {
|
||||
maxHeight: isLandscape ? height - 94 : (height / 100) * 70,
|
||||
backgroundColor:
|
||||
customTrainType.data === "notService" ? "#777777ff" : "white",
|
||||
customTrainType.data === "notService" ? "#777777ff" : colors.surface,
|
||||
},
|
||||
}}
|
||||
shortHeader={
|
||||
@@ -207,7 +206,7 @@ export const EachTrainInfoCore = ({
|
||||
}
|
||||
>
|
||||
{customTrainType.data === "notService" && (
|
||||
<Text style={{ backgroundColor: "#ffffffc2", fontWeight: "bold" }}>
|
||||
<Text style={{ backgroundColor: colors.surface, fontWeight: "bold" }}>
|
||||
この列車には乗車できません。
|
||||
</Text>
|
||||
)}
|
||||
@@ -219,10 +218,10 @@ export const EachTrainInfoCore = ({
|
||||
onPress={() =>
|
||||
extendToHeadStation(item.station, item.dia, index)
|
||||
}
|
||||
style={styles.extendStationButton}
|
||||
style={[styles.extendStationButton, { borderColor: colors.textAccent }]}
|
||||
key={`${item.station}-head${index}`}
|
||||
>
|
||||
<Text style={styles.extendStationText}>
|
||||
<Text style={[styles.extendStationText, { color: colors.text }]}>
|
||||
「本当の始発駅」を表示
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
@@ -238,9 +237,9 @@ export const EachTrainInfoCore = ({
|
||||
onPress={() =>
|
||||
Linking.openURL(`https://twitter.com/search?q=${data.trainNum}`)
|
||||
}
|
||||
style={styles.twitterSearchButton}
|
||||
style={[styles.twitterSearchButton, { borderColor: colors.textAccent, backgroundColor: colors.backgroundOverlay }]}
|
||||
>
|
||||
<Text style={styles.extendStationText}>Twitterで検索</Text>
|
||||
<Text style={[styles.extendStationText, { color: colors.text }]}>Twitterで検索</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
{trainDataWithThrough.map((item, index, array) =>
|
||||
@@ -251,7 +250,7 @@ export const EachTrainInfoCore = ({
|
||||
openTrainInfo={openTrainInfo}
|
||||
/>
|
||||
) : item.split(",")[1].includes(".") ? (
|
||||
<></>
|
||||
<React.Fragment key={`${item}-skip`} />
|
||||
) : (
|
||||
<EachStopList
|
||||
i={item}
|
||||
@@ -267,7 +266,7 @@ export const EachTrainInfoCore = ({
|
||||
/>
|
||||
)
|
||||
)}
|
||||
<Text style={styles.customDataNote}>
|
||||
<Text style={[styles.customDataNote, { color: colors.textSecondary }]}>
|
||||
時刻が斜体,青色になっている時刻はコミュニティで追加されている独自データです。
|
||||
</Text>
|
||||
{tailStation.length > 0 &&
|
||||
@@ -276,17 +275,17 @@ export const EachTrainInfoCore = ({
|
||||
!showTailStation.includes(index) && (
|
||||
<TouchableOpacity
|
||||
onPress={() => extendToTailStation(station, dia, index)}
|
||||
style={styles.extendStationButton}
|
||||
style={[styles.extendStationButton, { borderColor: colors.textAccent }]}
|
||||
key={`${station}-tail${index}`}
|
||||
>
|
||||
<Text style={styles.extendStationText}>
|
||||
<Text style={[styles.extendStationText, { color: colors.text }]}>
|
||||
「本当の終着駅」を表示
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
)
|
||||
)}
|
||||
|
||||
<View style={styles.bottomSpacer} />
|
||||
<View style={[styles.bottomSpacer, { borderBottomColor: colors.borderLight }]} />
|
||||
</DynamicHeaderScrollView>
|
||||
</View>
|
||||
);
|
||||
@@ -320,7 +319,6 @@ const styles = StyleSheet.create({
|
||||
extendStationText: {
|
||||
fontSize: 18,
|
||||
fontWeight: "bold",
|
||||
color: "black",
|
||||
},
|
||||
twitterSearchButton: {
|
||||
padding: 10,
|
||||
@@ -333,14 +331,12 @@ const styles = StyleSheet.create({
|
||||
backgroundColor: "#ffffffc2",
|
||||
},
|
||||
customDataNote: {
|
||||
backgroundColor: "#ffffffc2",
|
||||
},
|
||||
bottomSpacer: {
|
||||
flexDirection: "row",
|
||||
padding: 8,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: "#f0f0f0",
|
||||
backgroundColor: "#ffffffc2",
|
||||
flex: 1,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
import React, { FC, useMemo } from "react";
|
||||
import {
|
||||
Text,
|
||||
View,
|
||||
TextStyle,
|
||||
TouchableOpacity,
|
||||
useWindowDimensions,
|
||||
} from "react-native";
|
||||
import React, { FC, useMemo, useState } from "react";
|
||||
import { Text, View, TextStyle, TouchableOpacity } from "react-native";
|
||||
import { SheetManager } from "react-native-actions-sheet";
|
||||
import { migrateTrainName } from "../../../lib/eachTrainInfoCoreLib/migrateTrainName";
|
||||
import { TrainIconStatus } from "./trainIconStatus";
|
||||
@@ -19,6 +13,9 @@ import { getStringConfig } from "@/lib/getStringConfig";
|
||||
import { MaterialCommunityIcons } from "@expo/vector-icons";
|
||||
import { getPDFViewURL } from "@/lib/getPdfViewURL";
|
||||
import type { NavigateFunction } from "@/types";
|
||||
import { useUnyohub } from "@/stateBox/useUnyohub";
|
||||
import { useElesite } from "@/stateBox/useElesite";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
|
||||
type Props = {
|
||||
data: { trainNum: string; limited: string };
|
||||
@@ -30,13 +27,12 @@ type Props = {
|
||||
navigate: NavigateFunction;
|
||||
from: string;
|
||||
fontLoaded: boolean;
|
||||
scrollHandlers: any;
|
||||
scrollRef: any;
|
||||
};
|
||||
|
||||
const textConfig: TextStyle = {
|
||||
fontSize: 17,
|
||||
fontWeight: "bold",
|
||||
color: "white",
|
||||
};
|
||||
|
||||
export const HeaderText: FC<Props> = ({
|
||||
@@ -48,15 +44,29 @@ export const HeaderText: FC<Props> = ({
|
||||
tailStation,
|
||||
navigate,
|
||||
from,
|
||||
scrollHandlers,
|
||||
scrollRef,
|
||||
}) => {
|
||||
const { limited, trainNum } = data;
|
||||
|
||||
const { height, width } = useWindowDimensions();
|
||||
const { fixed } = useThemeColors();
|
||||
const { updatePermission } = useTrainMenu();
|
||||
const { allCustomTrainData, getTodayOperationByTrainId } =
|
||||
useAllTrainDiagram();
|
||||
const { expoPushToken } = useNotification();
|
||||
const {
|
||||
getUnyohubByTrainNumber,
|
||||
getUnyohubEntriesByTrainNumber,
|
||||
useUnyohub: unyohubEnabled,
|
||||
} = useUnyohub();
|
||||
const { getElesiteEntriesByTrainNumber, useElesite: elesiteEnabled } =
|
||||
useElesite();
|
||||
|
||||
// 追加ソースのON/OFFをここで管理(将来ソースが増えたらここに足す)
|
||||
const additionalSources = {
|
||||
unyohub: unyohubEnabled,
|
||||
elesite: elesiteEnabled,
|
||||
};
|
||||
const hasAdditionalSources = Object.values(additionalSources).some(Boolean);
|
||||
|
||||
// 列車名、種別、フォントの取得
|
||||
const [
|
||||
@@ -68,7 +78,10 @@ export const HeaderText: FC<Props> = ({
|
||||
priority,
|
||||
uwasa,
|
||||
trainInfoUrl,
|
||||
directions,
|
||||
customTrainData,
|
||||
] = useMemo(() => {
|
||||
const result = customTrainDataDetector(trainNum, allCustomTrainData);
|
||||
const {
|
||||
type,
|
||||
train_name,
|
||||
@@ -78,15 +91,14 @@ export const HeaderText: FC<Props> = ({
|
||||
uwasa,
|
||||
train_info_url,
|
||||
to_data,
|
||||
} = customTrainDataDetector(trainNum, allCustomTrainData);
|
||||
directions,
|
||||
} = result;
|
||||
const [typeString, fontAvailable, isOneMan] = getStringConfig(
|
||||
type,
|
||||
trainNum,
|
||||
);
|
||||
switch (true) {
|
||||
case train_name !== "":
|
||||
// 特急の場合は、列車名を取得
|
||||
// 列番対称データがある場合はそれから列車番号を取得
|
||||
return [
|
||||
typeString,
|
||||
train_name +
|
||||
@@ -99,6 +111,8 @@ export const HeaderText: FC<Props> = ({
|
||||
priority,
|
||||
uwasa,
|
||||
train_info_url,
|
||||
directions,
|
||||
result,
|
||||
];
|
||||
case trainData[trainData.length - 1] === undefined:
|
||||
return [
|
||||
@@ -110,9 +124,10 @@ export const HeaderText: FC<Props> = ({
|
||||
priority,
|
||||
uwasa,
|
||||
train_info_url,
|
||||
directions,
|
||||
result,
|
||||
];
|
||||
case to_data && to_data !== "":
|
||||
// 行先がある場合は、行先を取得
|
||||
return [
|
||||
typeString,
|
||||
to_data + "行き",
|
||||
@@ -122,9 +137,10 @@ export const HeaderText: FC<Props> = ({
|
||||
priority,
|
||||
uwasa,
|
||||
train_info_url,
|
||||
directions,
|
||||
result,
|
||||
];
|
||||
default:
|
||||
// 行先がある場合は、行先を取得
|
||||
return [
|
||||
typeString,
|
||||
migrateTrainName(
|
||||
@@ -136,6 +152,8 @@ export const HeaderText: FC<Props> = ({
|
||||
priority,
|
||||
uwasa,
|
||||
train_info_url,
|
||||
directions,
|
||||
result,
|
||||
];
|
||||
}
|
||||
}, [trainData]);
|
||||
@@ -143,11 +161,43 @@ export const HeaderText: FC<Props> = ({
|
||||
const todayOperation = getTodayOperationByTrainId(trainNum).filter(
|
||||
(d) => d.state !== 100,
|
||||
);
|
||||
|
||||
let iconTrainDirection =
|
||||
parseInt(trainNum.replace(/[^\d]/g, "")) % 2 == 0 ? true : false;
|
||||
if (directions != undefined) {
|
||||
iconTrainDirection = directions ? true : false;
|
||||
}
|
||||
|
||||
const unyohubFormation = getUnyohubByTrainNumber(trainNum);
|
||||
const unyohubEntries = getUnyohubEntriesByTrainNumber(trainNum);
|
||||
const elesiteEntries = getElesiteEntriesByTrainNumber(trainNum);
|
||||
|
||||
// 車番(formations)が空でないエントリが1件以上あれば「運用Hub情報あり」と判定
|
||||
const hasUnyohubFormation = unyohubEntries.some(
|
||||
(e) => !!e.formations && e.formations.trim() !== "",
|
||||
);
|
||||
const hasElesiteFormation = elesiteEntries.some(
|
||||
(e) => (e.formation_config?.units?.length ?? 0) > 0,
|
||||
);
|
||||
|
||||
const hasExtraInfo =
|
||||
priority > 200 ||
|
||||
todayOperation?.length > 0 ||
|
||||
hasUnyohubFormation ||
|
||||
hasElesiteFormation;
|
||||
|
||||
const [isWrapped, setIsWrapped] = useState(false);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{ padding: 10, flexDirection: "row", alignItems: "center" }}
|
||||
style={{
|
||||
padding: 10,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
width: "100%",
|
||||
}}
|
||||
onTouchStart={() =>
|
||||
scrollHandlers.ref.current?.scrollTo({ y: 0, animated: true })
|
||||
scrollRef.current?.scrollTo({ y: 0, animated: true })
|
||||
}
|
||||
>
|
||||
<TrainIconStatus
|
||||
@@ -155,18 +205,42 @@ export const HeaderText: FC<Props> = ({
|
||||
navigate={navigate}
|
||||
from={from}
|
||||
todayOperation={todayOperation}
|
||||
direction={iconTrainDirection}
|
||||
/>
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
marginBottom: 2,
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 20,
|
||||
color: fixed.textOnPrimary,
|
||||
fontFamily: fontAvailable ? "JR-Nishi" : undefined,
|
||||
fontWeight: !fontAvailable ? "bold" : undefined,
|
||||
marginRight: 5,
|
||||
}}
|
||||
>
|
||||
{isWrapped ? typeName.replace(/(.{2})/g, "$1\n").trim() : typeName}
|
||||
</Text>
|
||||
{isOneMan && <OneManText />}
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
style={{
|
||||
borderRadius: 5,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
flexShrink: 1,
|
||||
flexWrap: "wrap",
|
||||
...(trainInfoUrl
|
||||
? {
|
||||
borderWidth: 0,
|
||||
borderBottomWidth: 1,
|
||||
borderStyle: "solid",
|
||||
borderColor: "white",
|
||||
borderColor: fixed.textOnPrimary,
|
||||
}
|
||||
: {}),
|
||||
}}
|
||||
@@ -180,64 +254,58 @@ export const HeaderText: FC<Props> = ({
|
||||
}}
|
||||
disabled={!trainInfoUrl}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 20,
|
||||
color: "white",
|
||||
fontFamily: fontAvailable ? "JR-Nishi" : undefined,
|
||||
fontWeight: !fontAvailable ? "bold" : undefined,
|
||||
marginRight: 5,
|
||||
}}
|
||||
>
|
||||
{typeName}
|
||||
</Text>
|
||||
{isOneMan && <OneManText />}
|
||||
<Text
|
||||
style={{
|
||||
...textConfig,
|
||||
...(trainName.length > 10 ? { fontSize: 14 } : {}),
|
||||
maxWidth: width * 0.6,
|
||||
color: fixed.textOnPrimary,
|
||||
...(trainName.length > 10 ? { fontSize: 16 } : {}),
|
||||
flexShrink: 1,
|
||||
}}
|
||||
onTextLayout={(e) => {
|
||||
if (e.nativeEvent.lines.length > 1) setIsWrapped(true);
|
||||
}}
|
||||
>
|
||||
{trainName}
|
||||
</Text>
|
||||
<InfogramText infogram={infogram} />
|
||||
{/* {trainInfoUrl && (
|
||||
<MaterialCommunityIcons
|
||||
name={"open-in-new"}
|
||||
color="white"
|
||||
size={15}
|
||||
/>
|
||||
)} */}
|
||||
</TouchableOpacity>
|
||||
|
||||
<View style={{ flex: 1 }} />
|
||||
<TouchableOpacity
|
||||
onLongPress={() => {
|
||||
if (!updatePermission) return;
|
||||
const uri = `https://jr-shikoku-data-system.pages.dev/trainData/${trainNum}?userID=${expoPushToken}&from=eachTrainInfo`;
|
||||
navigate("generalWebView", { uri, useExitButton: false });
|
||||
SheetManager.hide("EachTrainInfo");
|
||||
}}
|
||||
disabled={!updatePermission}
|
||||
>
|
||||
<Text style={textConfig}>
|
||||
{showHeadStation.map((d) => `${headStation[d].id} + `)}
|
||||
{trainNum}
|
||||
{showTailStation.map((d) => ` + ${tailStation[d].id}`)}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<Text style={{ ...textConfig, color: fixed.textOnPrimary }}>
|
||||
{showHeadStation.map((d) => `${headStation[d].id} + `)}
|
||||
{trainNum}
|
||||
{showTailStation.map((d) => ` + ${tailStation[d].id}`)}
|
||||
</Text>
|
||||
<MaterialCommunityIcons
|
||||
name="database"
|
||||
color={
|
||||
priority > 200 || todayOperation?.length > 0 ? "yellow" : "white"
|
||||
}
|
||||
color={hasExtraInfo ? "yellow" : fixed.textOnPrimary}
|
||||
size={30}
|
||||
style={{ margin: 5 }}
|
||||
onPress={() => {
|
||||
const uri = `https://jr-shikoku-data-system.pages.dev/trainData/${trainNum}?userID=${expoPushToken}&from=eachTrainInfo`;
|
||||
navigate("generalWebView", { uri, useExitButton: false });
|
||||
SheetManager.hide("EachTrainInfo");
|
||||
if (hasAdditionalSources) {
|
||||
(SheetManager.show as any)("TrainDataSources", {
|
||||
payload: {
|
||||
trainNum,
|
||||
unyohubEntries,
|
||||
elesiteEntries,
|
||||
todayOperation,
|
||||
navigate,
|
||||
expoPushToken,
|
||||
priority,
|
||||
direction: iconTrainDirection,
|
||||
customTrainData,
|
||||
typeName,
|
||||
trainName,
|
||||
departureStation: trainData[0]?.split(",")[0] ?? "",
|
||||
destinationStation:
|
||||
trainData[trainData.length - 1]?.split(",")[0] ?? "",
|
||||
},
|
||||
});
|
||||
} 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");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import React, { FC } from "react";
|
||||
import { Text } from "react-native";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
type props = {
|
||||
infogram: string;
|
||||
}
|
||||
export const InfogramText: FC<props> = ({infogram}) => {
|
||||
const { fixed } = useThemeColors();
|
||||
return (
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 20,
|
||||
color: "white",
|
||||
color: fixed.textOnPrimary,
|
||||
fontFamily: "JNR-font",
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import React, { FC } from "react";
|
||||
import { Text, View } from "react-native";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
export const OneManText: FC = () => {
|
||||
const { fixed } = useThemeColors();
|
||||
const styles = {
|
||||
fontSize: 12,
|
||||
margin: -2,
|
||||
color: "white",
|
||||
color: fixed.textOnPrimary,
|
||||
fontFamily: "Zou",
|
||||
};
|
||||
return (
|
||||
|
||||
@@ -5,13 +5,13 @@ import { LayoutAnimation, ScrollView } from 'react-native';
|
||||
export const useAutoScroll = (
|
||||
points: boolean[] | undefined,
|
||||
trainDataWithThrough: string[],
|
||||
scrollHandlers: any,
|
||||
scrollRef: MutableRefObject<any>,
|
||||
isJumped: boolean,
|
||||
setIsJumped: (value: boolean) => void,
|
||||
setShowThrew: (value: boolean) => void
|
||||
) => {
|
||||
useEffect(() => {
|
||||
if (isJumped || !points?.length || !scrollHandlers) return;
|
||||
if (isJumped || !points?.length || !scrollRef) return;
|
||||
|
||||
const currentPositionIndex = points.findIndex((d) => d === true);
|
||||
if (currentPositionIndex === -1) return;
|
||||
@@ -34,8 +34,8 @@ export const useAutoScroll = (
|
||||
|
||||
const scrollPosition = currentPositionIndex * 44 - 50;
|
||||
setTimeout(() => {
|
||||
scrollHandlers.ref.current?.scrollTo({ y: scrollPosition, animated: true });
|
||||
scrollRef.current?.scrollTo({ y: scrollPosition, animated: true });
|
||||
setIsJumped(true);
|
||||
}, 400);
|
||||
}, [points, trainDataWithThrough, scrollHandlers, isJumped, setIsJumped, setShowThrew]);
|
||||
}, [points, trainDataWithThrough, scrollRef, isJumped, setIsJumped, setShowThrew]);
|
||||
};
|
||||
|
||||
@@ -3,7 +3,6 @@ import { View, Image, TouchableOpacity } from "react-native";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import dayjs from "dayjs";
|
||||
import { useInterval } from "../../../lib/useInterval";
|
||||
import { Icon } from "@expo/vector-icons/build/createIconSet";
|
||||
import { SheetManager } from "react-native-actions-sheet";
|
||||
import { customTrainDataDetector } from "../../custom-train-data";
|
||||
import { useAllTrainDiagram } from "@/stateBox/useAllTrainDiagram";
|
||||
@@ -17,17 +16,18 @@ type Props = {
|
||||
navigate: NavigateFunction;
|
||||
from: string;
|
||||
todayOperation: OperationLogs[];
|
||||
direction?: boolean;
|
||||
};
|
||||
type apt = {
|
||||
name: GlyphNames;
|
||||
color: string;
|
||||
};
|
||||
export const TrainIconStatus: FC<Props> = (props) => {
|
||||
const { data, navigate, from, todayOperation } = props;
|
||||
const { data, navigate, from, todayOperation, direction } = props;
|
||||
const [anpanmanStatus, setAnpanmanStatus] = useState<apt>();
|
||||
const { allCustomTrainData } = useAllTrainDiagram();
|
||||
const [trainIconData, setTrainIcon] = useState<
|
||||
{ vehicle_info_img: string; vehicle_info_url: string }[]
|
||||
{ vehicle_info_img: string;vehicle_info_right_img: string; vehicle_info_url: string }[]
|
||||
>([]);
|
||||
useEffect(() => {
|
||||
if (!data.trainNum) return;
|
||||
@@ -79,11 +79,12 @@ export const TrainIconStatus: FC<Props> = (props) => {
|
||||
})
|
||||
.map((op) => ({
|
||||
vehicle_info_img: op.vehicle_img || vehicle_info_img,
|
||||
vehicle_info_right_img: op.vehicle_img_right || op.vehicle_img || vehicle_info_img,
|
||||
vehicle_info_url: op.vehicle_info_url,
|
||||
})) || [];
|
||||
setTrainIcon(returnData);
|
||||
} else if (vehicle_info_img) {
|
||||
setTrainIcon([{ vehicle_info_img, vehicle_info_url }]);
|
||||
setTrainIcon([{ vehicle_info_img, vehicle_info_right_img: vehicle_info_img, vehicle_info_url }]);
|
||||
}
|
||||
|
||||
switch (data.trainNum) {
|
||||
@@ -113,30 +114,6 @@ export const TrainIconStatus: FC<Props> = (props) => {
|
||||
}
|
||||
});
|
||||
break;
|
||||
case "2074D":
|
||||
case "2076D":
|
||||
case "2080D":
|
||||
case "2082D":
|
||||
case "2071D":
|
||||
case "2073D":
|
||||
case "2079D":
|
||||
case "2081D":
|
||||
fetch(
|
||||
`https://n8n.haruk.in/webhook/dosan-anpanman-first?trainNum=${
|
||||
data.trainNum
|
||||
}&month=${dayjs().format("M")}&day=${dayjs().format("D")}`
|
||||
)
|
||||
.then((d) => d.json())
|
||||
.then((d) => {
|
||||
if (d.trainStatus == "〇") {
|
||||
//setAnpanmanStatus({name:"checkmark-circle-outline",color:"blue"});
|
||||
} else if (d.trainStatus == "▲") {
|
||||
setAnpanmanStatus({ name: "warning-outline", color: "yellow" });
|
||||
} else if (d.trainStatus == "×") {
|
||||
//setAnpanmanStatus({ name: "close-circle-outline", color: "red" });
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
}, [data.trainNum, allCustomTrainData, todayOperation]);
|
||||
const [move, setMove] = useState(true);
|
||||
@@ -151,8 +128,9 @@ export const TrainIconStatus: FC<Props> = (props) => {
|
||||
return (
|
||||
<>
|
||||
{trainIconData.map(
|
||||
({ vehicle_info_img: trainIcon, vehicle_info_url: address }, index) => (
|
||||
({ vehicle_info_img: trainIcon, vehicle_info_right_img: trainIconRight, vehicle_info_url: address }, index) => (
|
||||
<TouchableOpacity
|
||||
key={`${trainIcon}-${index}`}
|
||||
onPress={() => {
|
||||
navigate("howto", {
|
||||
info: address,
|
||||
@@ -164,7 +142,7 @@ export const TrainIconStatus: FC<Props> = (props) => {
|
||||
>
|
||||
{move ? (
|
||||
<Image
|
||||
source={{ uri: trainIcon }}
|
||||
source={{ uri: direction ? trainIcon : trainIconRight || trainIcon }}
|
||||
style={{
|
||||
height: index > 0 ? 15 : 30,
|
||||
width: index > 0 ? 12 : 24,
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Ionicons } from "@expo/vector-icons";
|
||||
import { LayoutAnimation } from "react-native";
|
||||
import { SheetManager } from "react-native-actions-sheet";
|
||||
import { getType } from "../../../lib/eachTrainInfoCoreLib/getType";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
import type { NavigateFunction } from "@/types";
|
||||
|
||||
type Props = {
|
||||
@@ -11,6 +12,7 @@ type Props = {
|
||||
from: string;
|
||||
};
|
||||
export const TrainViewIcon: FC<Props> = ({ data, navigate, from }) => {
|
||||
const { fixed } = useThemeColors();
|
||||
const [isTrainView, setIsTrainView] = useState(false);
|
||||
//トレインビュー表示対象(特急、マリン)かを判定
|
||||
useEffect(() => {
|
||||
@@ -31,7 +33,7 @@ export const TrainViewIcon: FC<Props> = ({ data, navigate, from }) => {
|
||||
return isTrainView ? (
|
||||
<Ionicons
|
||||
name="subway"
|
||||
color="white"
|
||||
color={fixed.textOnPrimary}
|
||||
size={30}
|
||||
style={{ margin: 5 }}
|
||||
onPress={onPressTrainView}
|
||||
|
||||
@@ -21,9 +21,11 @@ import ViewShot from "react-native-view-shot";
|
||||
import * as Sharing from "expo-sharing";
|
||||
import { useTrainDelayData } from "../../stateBox/useTrainDelayData";
|
||||
import { BottomButtons } from "./JRSTraInfo/BottomButtons";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
export const JRSTraInfo = () => {
|
||||
const { getTime, delayData, loadingDelayData, setLoadingDelayData } =
|
||||
useTrainDelayData();
|
||||
const { colors, fixed } = useThemeColors();
|
||||
const timeData = dayjs(getTime).format("HH:mm");
|
||||
const actionSheetRef = useRef(null);
|
||||
const scrollHandlers = useScrollHandlers("scrollview-1", actionSheetRef);
|
||||
@@ -37,7 +39,7 @@ export const JRSTraInfo = () => {
|
||||
if (ok) {
|
||||
await Sharing.shareAsync(
|
||||
"file://" + url,
|
||||
(options = { mimeType: "image/jpeg", dialogTitle: "Share this image" })
|
||||
{ mimeType: "image/jpeg", dialogTitle: "Share this image" }
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -56,22 +58,22 @@ export const JRSTraInfo = () => {
|
||||
<Handler />
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: "#0099CC",
|
||||
backgroundColor: fixed.primary,
|
||||
borderTopRadius: 5,
|
||||
borderColor: "dark",
|
||||
borderColor: colors.border,
|
||||
borderWidth: 1,
|
||||
}}
|
||||
>
|
||||
<ViewShot ref={viewShot} options={{ format: "jpg" }}>
|
||||
<View
|
||||
style={{ height: 26, width: "100%", backgroundColor: "#0099CC" }}
|
||||
style={{ height: 26, width: "100%", backgroundColor: fixed.primary }}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
height: 6,
|
||||
width: 45,
|
||||
borderRadius: 100,
|
||||
backgroundColor: "#f0f0f0",
|
||||
backgroundColor: colors.borderLight,
|
||||
marginVertical: 10,
|
||||
alignSelf: "center",
|
||||
}}
|
||||
@@ -82,19 +84,19 @@ export const JRSTraInfo = () => {
|
||||
padding: 10,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
backgroundColor: "#0099CC",
|
||||
backgroundColor: fixed.primary,
|
||||
}}
|
||||
>
|
||||
<Text style={{ fontSize: 30, fontWeight: "bold", color: "white" }}>
|
||||
<Text style={{ fontSize: 30, fontWeight: "bold", color: fixed.textOnPrimary }}>
|
||||
列車遅延速報EX
|
||||
</Text>
|
||||
<View style={{ flex: 1 }} />
|
||||
<Text style={{ fontSize: 30, fontWeight: "bold", color: "white" }}>
|
||||
<Text style={{ fontSize: 30, fontWeight: "bold", color: fixed.textOnPrimary }}>
|
||||
{timeData}
|
||||
</Text>
|
||||
<Ionicons
|
||||
name="reload"
|
||||
color="white"
|
||||
color={fixed.textOnPrimary}
|
||||
size={30}
|
||||
style={{ margin: 5 }}
|
||||
onPress={() => {
|
||||
@@ -106,7 +108,7 @@ export const JRSTraInfo = () => {
|
||||
<View
|
||||
style={{
|
||||
padding: 10,
|
||||
backgroundColor: "white",
|
||||
backgroundColor: colors.surface,
|
||||
}}
|
||||
>
|
||||
{loadingDelayData ? (
|
||||
@@ -114,7 +116,7 @@ export const JRSTraInfo = () => {
|
||||
<LottieView
|
||||
autoPlay
|
||||
loop
|
||||
style={{ width: 150, height: 150, backgroundColor: "#fff" }}
|
||||
style={{ width: 150, height: 150, backgroundColor: colors.surface }}
|
||||
source={require("../../assets/51690-loading-diamonds.json")}
|
||||
/>
|
||||
</View>
|
||||
@@ -123,26 +125,26 @@ export const JRSTraInfo = () => {
|
||||
let data = d.split(" ");
|
||||
return (
|
||||
<View style={{ flexDirection: "row" }} key={data[1]}>
|
||||
<Text style={{ flex: 15, fontSize: 18 }}>
|
||||
<Text style={{ flex: 15, fontSize: 18, color: colors.text }}>
|
||||
{data[0].replace("\n", "")}
|
||||
</Text>
|
||||
<Text style={{ flex: 5, fontSize: 18 }}>{data[1]}</Text>
|
||||
<Text style={{ flex: 6, fontSize: 18 }}>{data[3]}</Text>
|
||||
<Text style={{ flex: 5, fontSize: 18, color: colors.text }}>{data[1]}</Text>
|
||||
<Text style={{ flex: 6, fontSize: 18, color: colors.text }}>{data[3]}</Text>
|
||||
</View>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<Text>現在、5分以上の遅れはありません。</Text>
|
||||
<Text style={{ color: colors.text }}>現在、5分以上の遅れはありません。</Text>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View style={{ padding: 10, backgroundColor: "#0099CC" }}>
|
||||
<View style={{ padding: 10, backgroundColor: fixed.primary }}>
|
||||
<Text
|
||||
style={{ fontSize: 20, fontWeight: "bold", color: "white" }}
|
||||
style={{ fontSize: 20, fontWeight: "bold", color: fixed.textOnPrimary }}
|
||||
>
|
||||
列車遅延情報EXについて
|
||||
</Text>
|
||||
<Text style={{ color: "white" }}>
|
||||
<Text style={{ color: fixed.textOnPrimary }}>
|
||||
列車遅延情報をJR四国公式列車運行情報より5分毎に取得します。Twitterにて投稿している内容と同一のものとなります。
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
@@ -8,23 +8,27 @@ import {
|
||||
ViewStyle,
|
||||
} from "react-native";
|
||||
import { MaterialCommunityIcons } from "@expo/vector-icons";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
|
||||
const styles: StyleProp<ViewStyle> = {
|
||||
padding: 10,
|
||||
flexDirection: "row",
|
||||
borderColor: "white",
|
||||
borderWidth: 1,
|
||||
margin: 10,
|
||||
borderRadius: 5,
|
||||
alignItems: "center",
|
||||
backgroundColor: "#0099CC",
|
||||
};
|
||||
export const BottomButtons: FC<{ onCapture: () => void }> = ({ onCapture }) => {
|
||||
const { fixed } = useThemeColors();
|
||||
|
||||
const styles: StyleProp<ViewStyle> = {
|
||||
padding: 10,
|
||||
flexDirection: "row",
|
||||
borderColor: fixed.textOnPrimary,
|
||||
borderWidth: 1,
|
||||
margin: 10,
|
||||
borderRadius: 5,
|
||||
alignItems: "center",
|
||||
backgroundColor: fixed.primary,
|
||||
};
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
padding: 10,
|
||||
backgroundColor: "#0099CC",
|
||||
backgroundColor: fixed.primary,
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
@@ -33,16 +37,16 @@ export const BottomButtons: FC<{ onCapture: () => void }> = ({ onCapture }) => {
|
||||
style={{ ...styles, flex: 1 }}
|
||||
onPress={() => Linking.openURL("https://mstdn.y-zu.org/@JRSTraInfoEX")}
|
||||
>
|
||||
<MaterialCommunityIcons name="mastodon" color="white" size={30} />
|
||||
<MaterialCommunityIcons name="mastodon" color={fixed.textOnPrimary} size={30} />
|
||||
<View style={{ flex: 1 }} />
|
||||
<Text style={{ fontSize: 25, fontWeight: "bold", color: "white" }}>
|
||||
<Text style={{ fontSize: 25, fontWeight: "bold", color: fixed.textOnPrimary }}>
|
||||
MastodonBOT
|
||||
</Text>
|
||||
<View style={{ flex: 1 }} />
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity style={styles} onPress={onCapture}>
|
||||
<MaterialCommunityIcons name="share-variant" color="white" size={30} />
|
||||
<MaterialCommunityIcons name="share-variant" color={fixed.textOnPrimary} size={30} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -3,12 +3,14 @@ import { View, Platform, Text } from "react-native";
|
||||
import ActionSheet ,{ ScrollView } from "react-native-actions-sheet";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { ListItem } from "@rneui/themed";
|
||||
import { MaterialCommunityIcons } from "@expo/vector-icons";
|
||||
import { MaterialCommunityIcons, Foundation } from "@expo/vector-icons";
|
||||
import { Linking } from "react-native";
|
||||
import TouchableScale from "react-native-touchable-scale";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
export const Social = () => {
|
||||
const actionSheetRef = useRef(null);
|
||||
const insets = useSafeAreaInsets();
|
||||
const { colors, fixed } = useThemeColors();
|
||||
|
||||
return (
|
||||
<ActionSheet
|
||||
@@ -27,20 +29,20 @@ export const Social = () => {
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: "#0099CC",
|
||||
backgroundColor: fixed.primary,
|
||||
borderTopRadius: 5,
|
||||
borderColor: "dark",
|
||||
borderWidth: 1,
|
||||
height: "100%",
|
||||
}}
|
||||
>
|
||||
<View style={{ height: 26, width: "100%", backgroundColor: "#0099CC" }}>
|
||||
<View style={{ height: 26, width: "100%", backgroundColor: fixed.primary }}>
|
||||
<View
|
||||
style={{
|
||||
height: 6,
|
||||
width: 45,
|
||||
borderRadius: 100,
|
||||
backgroundColor: "#f0f0f0",
|
||||
backgroundColor: colors.borderLight,
|
||||
marginVertical: 10,
|
||||
alignSelf: "center",
|
||||
}}
|
||||
@@ -53,17 +55,37 @@ export const Social = () => {
|
||||
<MaterialCommunityIcons
|
||||
name="web"
|
||||
style={{ padding: 5 }}
|
||||
color="white"
|
||||
color={fixed.textOnPrimary}
|
||||
size={30}
|
||||
/>
|
||||
<Text style={{ fontSize: 30, fontWeight: "bold", color: "white" }}>
|
||||
<Text style={{ fontSize: 30, fontWeight: "bold", color: fixed.textOnPrimary }}>
|
||||
JR四国公式SNS一族
|
||||
</Text>
|
||||
</View>
|
||||
<ListItem
|
||||
bottomDivider
|
||||
onPress={() => Linking.openURL("tel:0570-00-4592")}
|
||||
friction={90}
|
||||
tension={100}
|
||||
activeScale={0.95}
|
||||
Component={TouchableScale}
|
||||
containerStyle={{ backgroundColor: "#8AE234" }}
|
||||
>
|
||||
<Foundation name="telephone" color={fixed.textOnPrimary} size={30} />
|
||||
<ListItem.Content>
|
||||
<ListItem.Title style={{ color: fixed.textOnPrimary, fontWeight: "bold" }}>
|
||||
JR四国案内センター
|
||||
</ListItem.Title>
|
||||
<ListItem.Subtitle style={{ color: fixed.textOnPrimary }}>
|
||||
0570-00-4592(8:00〜20:00 年中無休)
|
||||
</ListItem.Subtitle>
|
||||
</ListItem.Content>
|
||||
<ListItem.Chevron color={fixed.textOnPrimary} />
|
||||
</ListItem>
|
||||
<ScrollView
|
||||
style={{
|
||||
padding: 10,
|
||||
backgroundColor: "white",
|
||||
backgroundColor: colors.surface,
|
||||
borderBottomLeftRadius: 10,
|
||||
borderBottomRightRadius: 10,
|
||||
flex:1
|
||||
@@ -132,9 +154,10 @@ export const Social = () => {
|
||||
tension={100} // These props are passed to the parent component (here TouchableScale)
|
||||
activeScale={0.95} //
|
||||
Component={TouchableScale}
|
||||
containerStyle={{ backgroundColor: colors.surface }}
|
||||
>
|
||||
<ListItem.Content>
|
||||
<ListItem.Title>{d.name}</ListItem.Title>
|
||||
<ListItem.Title style={{ color: colors.text }}>{d.name}</ListItem.Title>
|
||||
</ListItem.Content>
|
||||
<ListItem.Chevron />
|
||||
</ListItem>
|
||||
|
||||
@@ -4,6 +4,7 @@ import ActionSheet from "react-native-actions-sheet";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
|
||||
import { SpecialTrainInfoBox } from "../Menu/SpecialTrainInfoBox";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
|
||||
type props = {
|
||||
payload: { navigate: (screen: string, params?: object) => void };
|
||||
@@ -12,6 +13,7 @@ export const SpecialTrainInfo: FC<props> = ({ payload }) => {
|
||||
const { navigate } = payload;
|
||||
const actionSheetRef = useRef(null);
|
||||
const insets = useSafeAreaInsets();
|
||||
const { colors, fixed } = useThemeColors();
|
||||
|
||||
return (
|
||||
<ActionSheet
|
||||
@@ -30,19 +32,19 @@ export const SpecialTrainInfo: FC<props> = ({ payload }) => {
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: "#0099CC",
|
||||
backgroundColor: fixed.primary,
|
||||
borderTopRadius: 5,
|
||||
borderColor: "dark",
|
||||
borderWidth: 1,
|
||||
}}
|
||||
>
|
||||
<View style={{ height: 26, width: "100%", backgroundColor: "#0099CC" }}>
|
||||
<View style={{ height: 26, width: "100%", backgroundColor: fixed.primary }}>
|
||||
<View
|
||||
style={{
|
||||
height: 6,
|
||||
width: 45,
|
||||
borderRadius: 100,
|
||||
backgroundColor: "#f0f0f0",
|
||||
backgroundColor: colors.borderLight,
|
||||
marginVertical: 10,
|
||||
alignSelf: "center",
|
||||
}}
|
||||
|
||||
@@ -22,6 +22,7 @@ import { StationTimeTableButton } from "./StationDeteilView/StationTimeTableButt
|
||||
import { StationTrainPositionButton } from "./StationDeteilView/StationTrainPositionButton";
|
||||
import { StationDiagramButton } from "./StationDeteilView/StationDiagramButton";
|
||||
import { useTrainMenu } from "@/stateBox/useTrainMenu";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
|
||||
export const StationDeteilView = (props) => {
|
||||
if (!props.payload) return <></>;
|
||||
@@ -29,6 +30,7 @@ export const StationDeteilView = (props) => {
|
||||
const { width } = useWindowDimensions();
|
||||
const { busAndTrainData } = useBusAndTrainData();
|
||||
const [trainBus, setTrainBus] = useState();
|
||||
const { colors } = useThemeColors();
|
||||
|
||||
useEffect(() => {
|
||||
if (!currentStation) return () => {};
|
||||
@@ -69,7 +71,7 @@ export const StationDeteilView = (props) => {
|
||||
<View
|
||||
key={currentStation}
|
||||
style={{
|
||||
backgroundColor: "white",
|
||||
backgroundColor: colors.sheetBackground,
|
||||
borderTopRadius: 5,
|
||||
borderColor: "dark",
|
||||
borderWidth: 1,
|
||||
@@ -81,7 +83,7 @@ export const StationDeteilView = (props) => {
|
||||
height: 6,
|
||||
width: 45,
|
||||
borderRadius: 100,
|
||||
backgroundColor: "#f0f0f0",
|
||||
backgroundColor: colors.borderLight,
|
||||
marginVertical: 10,
|
||||
alignSelf: "center",
|
||||
}}
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { FC } from "react";
|
||||
import { Linking } from "react-native";
|
||||
import { FontAwesome } from "@expo/vector-icons";
|
||||
import { TicketBox } from "@/components/atom/TicketBox";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
type Props = {
|
||||
navigate: (screen: string, params?: object) => void;
|
||||
onExit: () => void;
|
||||
@@ -22,10 +23,11 @@ type Props = {
|
||||
};
|
||||
export const StationDiagramButton: FC<Props> = (props) => {
|
||||
const { navigate, onExit, currentStation } = props;
|
||||
const { fixed } = useThemeColors();
|
||||
return (
|
||||
<TicketBox
|
||||
backgroundColor={"#8F5902"}
|
||||
icon={<FontAwesome name="table" color="white" size={50} />}
|
||||
icon={<FontAwesome name="table" color={fixed.textOnPrimary} size={50} />}
|
||||
flex={1}
|
||||
onPressButton={() => {
|
||||
navigate("stDiagram", {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { FC } from "react";
|
||||
import { TouchableOpacity, View, Text, Linking } from "react-native";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
type Props = {
|
||||
navigate: (screen: string, params: { info: string; goTo: string; useShow: boolean }) => void;
|
||||
address: string;
|
||||
@@ -9,6 +10,7 @@ type Props = {
|
||||
};
|
||||
export const 駅構内図:FC<Props> = (props) => {
|
||||
const { navigate, address, goTo, useShow, onExit } = props;
|
||||
const { fixed } = useThemeColors();
|
||||
const info = address.replace("/index.html", "/") + "/kounai_map.html";
|
||||
return (
|
||||
<TouchableOpacity
|
||||
@@ -30,7 +32,7 @@ export const 駅構内図:FC<Props> = (props) => {
|
||||
<View style={{ flex: 1 }} />
|
||||
<Text
|
||||
style={{
|
||||
color: "white",
|
||||
color: fixed.textOnPrimary,
|
||||
textAlign: "center",
|
||||
textAlignVertical: "center",
|
||||
flex: 1,
|
||||
|
||||
@@ -2,11 +2,13 @@ import React from "react";
|
||||
import { Linking } from "react-native";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { TicketBox } from "@/components/atom/TicketBox";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
export const StationMapButton = ({stationMap}) => {
|
||||
const { fixed } = useThemeColors();
|
||||
return (
|
||||
<TicketBox
|
||||
backgroundColor={"#888A85"}
|
||||
icon={<Ionicons name="map" color="white" size={50} />}
|
||||
icon={<Ionicons name="map" color={fixed.textOnPrimary} size={50} />}
|
||||
flex={1}
|
||||
onPressButton={() => Linking.openURL(stationMap)}
|
||||
>
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { FC } from "react";
|
||||
import { Linking } from "react-native";
|
||||
import { FontAwesome } from "@expo/vector-icons";
|
||||
import { TicketBox } from "@/components/atom/TicketBox";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
type Props = {
|
||||
info: string;
|
||||
address: string;
|
||||
@@ -13,10 +14,11 @@ type Props = {
|
||||
};
|
||||
export const StationTimeTableButton: FC<Props> = (props) => {
|
||||
const { info, address, usePDFView, navigate, onExit, goTo, useShow } = props;
|
||||
const { fixed } = useThemeColors();
|
||||
return (
|
||||
<TicketBox
|
||||
backgroundColor={"#8F5902"}
|
||||
icon={<FontAwesome name="table" color="white" size={50} />}
|
||||
icon={<FontAwesome name="table" color={fixed.textOnPrimary} size={50} />}
|
||||
flex={1}
|
||||
onPressButton={() => {
|
||||
usePDFView == "true"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { FC } from "react";
|
||||
import { TouchableOpacity, View, Text, Linking } from "react-native";
|
||||
import { useCurrentTrain } from "@/stateBox/useCurrentTrain";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
import AntDesign from "react-native-vector-icons/AntDesign";
|
||||
type Props = {
|
||||
stationNumber: string;
|
||||
@@ -10,11 +11,12 @@ type Props = {
|
||||
export const StationTrainPositionButton: FC<Props> = (props) => {
|
||||
const { stationNumber, onExit, navigate } = props;
|
||||
const { setInjectData } = useCurrentTrain();
|
||||
const { fixed } = useThemeColors();
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={{
|
||||
height: 50,
|
||||
backgroundColor: "#0099CC",
|
||||
backgroundColor: fixed.primary,
|
||||
flexDirection: "row",
|
||||
alignContent: "center",
|
||||
alignItems: "center",
|
||||
@@ -34,14 +36,14 @@ export const StationTrainPositionButton: FC<Props> = (props) => {
|
||||
>
|
||||
<View style={{ flex: 1 }} />
|
||||
<AntDesign
|
||||
name={"barchart"}
|
||||
name={"bar-chart"}
|
||||
size={20}
|
||||
color={"white"}
|
||||
color={fixed.textOnPrimary}
|
||||
style={{ marginHorizontal: 5, marginVertical: 5 }}
|
||||
/>
|
||||
<Text
|
||||
style={{
|
||||
color: "white",
|
||||
color: fixed.textOnPrimary,
|
||||
textAlign: "center",
|
||||
textAlignVertical: "center",
|
||||
}}
|
||||
|
||||
@@ -2,15 +2,17 @@ import React, { FC } from "react";
|
||||
import { Linking } from "react-native";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { TicketBox } from "@/components/atom/TicketBox";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
type Props = {
|
||||
address: string;
|
||||
press: () => void;
|
||||
};
|
||||
export const TrainBusButton: FC<Props> = ({ address, press }) => {
|
||||
const { fixed } = useThemeColors();
|
||||
return (
|
||||
<TicketBox
|
||||
backgroundColor={"#CE5C00"}
|
||||
icon={<Ionicons name="bus" color="white" size={50} />}
|
||||
icon={<Ionicons name="bus" color={fixed.textOnPrimary} size={50} />}
|
||||
flex={1}
|
||||
onPressButton={press}
|
||||
onLongPressButton={() => Linking.openURL(address)}
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { FC } from "react";
|
||||
import { Linking } from "react-native";
|
||||
import { Foundation } from "@expo/vector-icons";
|
||||
import { TicketBox } from "@/components/atom/TicketBox";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
import type { NavigateFunction } from "@/types";
|
||||
type Props = {
|
||||
navigate: NavigateFunction;
|
||||
@@ -12,10 +13,11 @@ type Props = {
|
||||
};
|
||||
export const WebSiteButton: FC<Props> = (Props) => {
|
||||
const { navigate, info, goTo, useShow, onExit } = Props;
|
||||
const { fixed } = useThemeColors();
|
||||
return (
|
||||
<TicketBox
|
||||
backgroundColor={"#AD7FA8"}
|
||||
icon={<Foundation name="web" color="white" size={50} />}
|
||||
icon={<Foundation name="web" color={fixed.textOnPrimary} size={50} />}
|
||||
flex={1}
|
||||
onPressButton={() => {
|
||||
navigate("howto", { info, goTo, useShow });
|
||||
|
||||
1290
components/ActionSheetComponents/TrainDataSources.tsx
Normal file
1290
components/ActionSheetComponents/TrainDataSources.tsx
Normal file
File diff suppressed because it is too large
Load Diff
@@ -17,12 +17,14 @@ import icons from "../../assets/icons/icons";
|
||||
import { getAppIconName } from "expo-alternate-app-icons";
|
||||
import { AS } from "@/storageControl";
|
||||
import { STORAGE_KEYS } from "@/constants";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
export const TrainIconUpdate = () => {
|
||||
const [iconList] = useState(icons());
|
||||
const [currentIcon] = useState(getAppIconName());
|
||||
const actionSheetRef = useRef(null);
|
||||
const insets = useSafeAreaInsets();
|
||||
const viewShot = useRef(null);
|
||||
const { colors, fixed } = useThemeColors();
|
||||
|
||||
const onCapture = async () => {
|
||||
const url = await viewShot.current.capture();
|
||||
@@ -31,7 +33,7 @@ export const TrainIconUpdate = () => {
|
||||
if (ok) {
|
||||
await Sharing.shareAsync(
|
||||
"file://" + url,
|
||||
(options = { mimeType: "image/jpeg", dialogTitle: "Share this image" })
|
||||
{ mimeType: "image/jpeg", dialogTitle: "Share this image" }
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -57,19 +59,19 @@ export const TrainIconUpdate = () => {
|
||||
<Handler />
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: "#0099CC",
|
||||
backgroundColor: fixed.primary,
|
||||
borderTopRadius: 5,
|
||||
borderColor: "dark",
|
||||
borderWidth: 1,
|
||||
}}
|
||||
>
|
||||
<View style={{ height: 26, width: "100%", backgroundColor: "#0099CC" }}>
|
||||
<View style={{ height: 26, width: "100%", backgroundColor: fixed.primary }}>
|
||||
<View
|
||||
style={{
|
||||
height: 6,
|
||||
width: 45,
|
||||
borderRadius: 100,
|
||||
backgroundColor: "#f0f0f0",
|
||||
backgroundColor: colors.borderLight,
|
||||
marginVertical: 10,
|
||||
alignSelf: "center",
|
||||
}}
|
||||
@@ -81,10 +83,10 @@ export const TrainIconUpdate = () => {
|
||||
padding: 10,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
backgroundColor: "#0099CC",
|
||||
backgroundColor: fixed.primary,
|
||||
}}
|
||||
>
|
||||
<Text style={{ fontSize: 30, fontWeight: "bold", color: "white" }}>
|
||||
<Text style={{ fontSize: 30, fontWeight: "bold", color: fixed.textOnPrimary }}>
|
||||
アイコンを変更しました!
|
||||
</Text>
|
||||
<View style={{ flex: 1 }} />
|
||||
@@ -95,23 +97,26 @@ export const TrainIconUpdate = () => {
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
backgroundColor: "#FFFFFFEE",
|
||||
backgroundColor: colors.surface,
|
||||
padding: 10,
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
source={iconList.filter(({ id }) => id == currentIcon)[0].icon}
|
||||
<View
|
||||
style={{
|
||||
width: 50,
|
||||
height: 50,
|
||||
padding: 30,
|
||||
borderWidth: 1,
|
||||
borderRadius: 10,
|
||||
borderColor: "white",
|
||||
borderColor: colors.border,
|
||||
margin: 10,
|
||||
backgroundColor: "white",
|
||||
padding: 10,
|
||||
backgroundColor: colors.surface,
|
||||
}}
|
||||
/>
|
||||
>
|
||||
<Image
|
||||
source={iconList.filter(({ id }) => id == currentIcon)[0].icon}
|
||||
style={{ width: 80, height: 80, borderRadius: 8 }}
|
||||
resizeMode="contain"
|
||||
/>
|
||||
</View>
|
||||
<Text>JR四国非公式アプリ</Text>
|
||||
</View>
|
||||
) : (
|
||||
@@ -122,10 +127,10 @@ export const TrainIconUpdate = () => {
|
||||
padding: 10,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
backgroundColor: "#0099CC",
|
||||
backgroundColor: fixed.primary,
|
||||
}}
|
||||
>
|
||||
<Text style={{ fontSize: 15, fontWeight: "bold", color: "white" }}>
|
||||
<Text style={{ fontSize: 15, fontWeight: "bold", color: fixed.textOnPrimary }}>
|
||||
JR四国非公式アプリを更新して好きなアイコンに変更してみよう!
|
||||
</Text>
|
||||
</View>
|
||||
@@ -133,7 +138,7 @@ export const TrainIconUpdate = () => {
|
||||
<View
|
||||
style={{
|
||||
padding: 10,
|
||||
backgroundColor: "#0099CC",
|
||||
backgroundColor: fixed.primary,
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
@@ -142,19 +147,19 @@ export const TrainIconUpdate = () => {
|
||||
style={{
|
||||
padding: 10,
|
||||
flexDirection: "row",
|
||||
borderColor: "white",
|
||||
borderColor: fixed.textOnPrimary,
|
||||
borderWidth: 1,
|
||||
margin: 10,
|
||||
borderRadius: 5,
|
||||
alignItems: "center",
|
||||
backgroundColor: "#0099CC",
|
||||
backgroundColor: fixed.primary,
|
||||
flex: 1,
|
||||
}}
|
||||
onPress={onCapture}
|
||||
>
|
||||
<MaterialCommunityIcons name="share" color="white" size={30} />
|
||||
<MaterialCommunityIcons name="share" color={fixed.textOnPrimary} size={30} />
|
||||
<View style={{ flex: 1 }} />
|
||||
<Text style={{ fontSize: 25, fontWeight: "bold", color: "white" }}>
|
||||
<Text style={{ fontSize: 25, fontWeight: "bold", color: fixed.textOnPrimary }}>
|
||||
推しアイコンをシェア
|
||||
</Text>
|
||||
<View style={{ flex: 1 }} />
|
||||
|
||||
@@ -12,6 +12,7 @@ import { useTrainMenu } from "../../stateBox/useTrainMenu";
|
||||
import { useCurrentTrain } from "../../stateBox/useCurrentTrain";
|
||||
import lineColorList from "../../assets/originData/lineColorList";
|
||||
import { stationIDPair, lineListPair } from "../../lib/getStationList";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
|
||||
export const TrainMenuLineSelector = () => {
|
||||
const {
|
||||
@@ -23,6 +24,7 @@ export const TrainMenuLineSelector = () => {
|
||||
const actionSheetRef = useRef(null);
|
||||
const insets = useSafeAreaInsets();
|
||||
const platformIs = Platform.OS == "android";
|
||||
const { colors, fixed } = useThemeColors();
|
||||
return (
|
||||
<ActionSheet
|
||||
gestureEnabled
|
||||
@@ -33,13 +35,13 @@ export const TrainMenuLineSelector = () => {
|
||||
useBottomSafeAreaPadding={platformIs}
|
||||
>
|
||||
<Handler />
|
||||
<View style={{ height: 26, width: "100%", backgroundColor: "white" }}>
|
||||
<View style={{ height: 26, width: "100%", backgroundColor: colors.sheetBackground }}>
|
||||
<View
|
||||
style={{
|
||||
height: 6,
|
||||
width: 45,
|
||||
borderRadius: 100,
|
||||
backgroundColor: "#f0f0f0",
|
||||
backgroundColor: colors.borderLight,
|
||||
marginVertical: 10,
|
||||
alignSelf: "center",
|
||||
}}
|
||||
@@ -49,7 +51,7 @@ export const TrainMenuLineSelector = () => {
|
||||
<TouchableOpacity
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
backgroundColor: selectedLine == d ? "#0099CC33" : "white",
|
||||
backgroundColor: selectedLine == d ? "#0099CC33" : colors.surface,
|
||||
}}
|
||||
onPress={() => {
|
||||
SheetManager.hide("TrainMenuLineSelector");
|
||||
@@ -93,7 +95,7 @@ export const TrainMenuLineSelector = () => {
|
||||
<View style={{ flex: 1 }} />
|
||||
<Text
|
||||
style={{
|
||||
color: "white",
|
||||
color: fixed.textOnPrimary,
|
||||
textAlign: "center",
|
||||
fontSize: 12,
|
||||
fontWeight: "bold",
|
||||
@@ -110,13 +112,13 @@ export const TrainMenuLineSelector = () => {
|
||||
padding: 8,
|
||||
flexDirection: "row",
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: "#f0f0f0",
|
||||
borderBottomColor: colors.borderLight,
|
||||
flex: 1,
|
||||
alignContent: "center",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<Text style={{ fontSize: 20 }}>
|
||||
<Text style={{ fontSize: 20, color: colors.text }}>
|
||||
{lineListPair[stationIDPair[d]]}
|
||||
</Text>
|
||||
<View style={{ flex: 1 }} />
|
||||
|
||||
@@ -6,6 +6,7 @@ import { TrainMenuLineSelector } from "./TrainMenuLineSelector";
|
||||
import { TrainIconUpdate } from "./TrainIconUpdate";
|
||||
import { SpecialTrainInfo } from "./SpecialTrainInfo";
|
||||
import { Social } from "./SocialMenu";
|
||||
import { TrainDataSources } from "./TrainDataSources";
|
||||
|
||||
registerSheet("EachTrainInfo", EachTrainInfo);
|
||||
registerSheet("JRSTraInfo", JRSTraInfo);
|
||||
@@ -14,5 +15,6 @@ registerSheet("TrainMenuLineSelector", TrainMenuLineSelector);
|
||||
registerSheet("TrainIconUpdate", TrainIconUpdate);
|
||||
registerSheet("SpecialTrainInfo", SpecialTrainInfo);
|
||||
registerSheet("Social", Social);
|
||||
registerSheet("TrainDataSources", TrainDataSources);
|
||||
|
||||
export {};
|
||||
|
||||
@@ -1,31 +1,33 @@
|
||||
import React, { useState, useEffect, FC } from "react";
|
||||
import React, { useState, useRef, FC } from "react";
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
FlatList,
|
||||
KeyboardAvoidingView,
|
||||
TextInput,
|
||||
Platform,
|
||||
Keyboard,
|
||||
ScrollView,
|
||||
Linking,
|
||||
Image,
|
||||
} from "react-native";
|
||||
import { useAllTrainDiagram } from "../stateBox/useAllTrainDiagram";
|
||||
import { useBottomTabBarHeight } from "@react-navigation/bottom-tabs";
|
||||
import { BigButton } from "./atom/BigButton";
|
||||
import { useKeyboardAvoid } from "../lib/useKeyboardAvoid";
|
||||
|
||||
import { customTrainDataDetector } from "./custom-train-data";
|
||||
import { getTrainType } from "../lib/getTrainType";
|
||||
import { SheetManager } from "react-native-actions-sheet";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import { BigButton } from "./atom/BigButton";
|
||||
import { Switch } from "react-native-elements";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
import { Switch } from "@rneui/themed";
|
||||
import { migrateTrainName } from "@/lib/eachTrainInfoCoreLib/migrateTrainName";
|
||||
import { OneManText } from "./ActionSheetComponents/EachTrainInfoCore/HeaderTextParts/OneManText";
|
||||
import { getStringConfig } from "@/lib/getStringConfig";
|
||||
|
||||
export const AllTrainDiagramView: FC = () => {
|
||||
const { colors, fixed } = useThemeColors();
|
||||
const { goBack, navigate } = useNavigation();
|
||||
const tabBarHeight = useBottomTabBarHeight();
|
||||
const {
|
||||
keyList,
|
||||
allTrainDiagram,
|
||||
@@ -33,11 +35,13 @@ export const AllTrainDiagramView: FC = () => {
|
||||
getTodayOperationByTrainId,
|
||||
} = useAllTrainDiagram();
|
||||
const [input, setInput] = useState(""); // 文字入力
|
||||
const [keyBoardVisible, setKeyBoardVisible] = useState(false);
|
||||
const [useStationName, setUseStationName] = useState(false);
|
||||
const [useRegex, setUseRegex] = useState(false);
|
||||
const containerRef = useRef<View>(null);
|
||||
const { keyboardVisible: keyBoardVisible, measuredOffset: measuredPadding } =
|
||||
useKeyboardAvoid({ measureRef: containerRef, tabBarHeight });
|
||||
const regexTextStyle = {
|
||||
color: "white",
|
||||
color: fixed.textOnPrimary,
|
||||
fontSize: 20,
|
||||
margin: 3,
|
||||
padding: 3,
|
||||
@@ -45,24 +49,10 @@ export const AllTrainDiagramView: FC = () => {
|
||||
const regexTextButtonStyle = {
|
||||
...regexTextStyle,
|
||||
borderWidth: 1,
|
||||
borderColor: "white",
|
||||
borderColor: fixed.textOnPrimary,
|
||||
borderRadius: 3,
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const showSubscription = Keyboard.addListener("keyboardDidShow", () => {
|
||||
setKeyBoardVisible(true);
|
||||
});
|
||||
const hideSubscription = Keyboard.addListener("keyboardDidHide", () => {
|
||||
setKeyBoardVisible(false);
|
||||
});
|
||||
|
||||
return () => {
|
||||
showSubscription.remove();
|
||||
hideSubscription.remove();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const openTrainInfo = (d) => {
|
||||
const train = customTrainDataDetector(d, allCustomTrainData);
|
||||
let TrainNumber = "";
|
||||
@@ -92,6 +82,7 @@ export const AllTrainDiagramView: FC = () => {
|
||||
const { train_info_img, train_name, type, train_num_distance, to_data } =
|
||||
customTrainDataDetector(id, allCustomTrainData);
|
||||
const todayOperation = getTodayOperationByTrainId(id).filter(d=> d.state !== 100);
|
||||
const [isWrapped, setIsWrapped] = useState(false);
|
||||
|
||||
const [typeString, fontAvailable, isOneMan] = getStringConfig(type, id);
|
||||
|
||||
@@ -123,7 +114,7 @@ export const AllTrainDiagramView: FC = () => {
|
||||
style={{
|
||||
padding: 5,
|
||||
flexDirection: "row",
|
||||
borderColor: "white",
|
||||
borderColor: fixed.textOnPrimary,
|
||||
borderWidth: 1,
|
||||
margin: 5,
|
||||
borderRadius: 5,
|
||||
@@ -157,34 +148,56 @@ export const AllTrainDiagramView: FC = () => {
|
||||
)}
|
||||
</View>
|
||||
|
||||
{typeString && (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
marginBottom: 2,
|
||||
}}
|
||||
>
|
||||
{typeString && (
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 20,
|
||||
color: fixed.textOnPrimary,
|
||||
fontFamily: fontAvailable ? "JR-Nishi" : undefined,
|
||||
fontWeight: !fontAvailable ? "bold" : undefined,
|
||||
marginRight: 5,
|
||||
}}
|
||||
>
|
||||
{isWrapped
|
||||
? typeString.replace(/(.{2})/g, "$1\n").trim()
|
||||
: typeString}
|
||||
</Text>
|
||||
)}
|
||||
{isOneMan && <OneManText />}
|
||||
</View>
|
||||
{trainNameString && (
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 20,
|
||||
color: "white",
|
||||
fontFamily: fontAvailable ? "JR-Nishi" : undefined,
|
||||
fontWeight: !fontAvailable ? "bold" : undefined,
|
||||
marginRight: 5,
|
||||
fontWeight: "bold",
|
||||
color: fixed.textOnPrimary,
|
||||
flexShrink: 1,
|
||||
}}
|
||||
onTextLayout={(e) => {
|
||||
if (e.nativeEvent.lines.length > 1) {
|
||||
setIsWrapped(true);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{typeString}
|
||||
</Text>
|
||||
)}
|
||||
{isOneMan && <OneManText />}
|
||||
{trainNameString && (
|
||||
<Text style={{ fontSize: 20, fontWeight: "bold", color: "white" }}>
|
||||
{trainNameString}
|
||||
</Text>
|
||||
)}
|
||||
<View style={{ flex: 1 }} />
|
||||
<Text style={{ fontSize: 20, fontWeight: "bold", color: "white" }}>
|
||||
<Text style={{ fontSize: 20, fontWeight: "bold", color: fixed.textOnPrimary }}>
|
||||
{id}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
return (
|
||||
<View style={{ backgroundColor: "#0099CC", height: "100%" }}>
|
||||
<View ref={containerRef} style={{ flex: 1, backgroundColor: fixed.primary, paddingBottom: measuredPadding }}>
|
||||
<FlatList
|
||||
contentContainerStyle={{ justifyContent: "flex-end", flexGrow: 1 }}
|
||||
style={{ flex: 1 }}
|
||||
@@ -226,7 +239,7 @@ export const AllTrainDiagramView: FC = () => {
|
||||
renderItem={({ item }) => <Item {...{ openTrainInfo, id: item }} />}
|
||||
ListEmptyComponent={
|
||||
<View style={{ flex: 1, alignItems: "center", marginTop: 50 }}>
|
||||
<Text style={{ color: "white", fontSize: 20 }}>
|
||||
<Text style={{ color: fixed.textOnPrimary, fontSize: 20 }}>
|
||||
検索結果がありません。
|
||||
</Text>
|
||||
</View>
|
||||
@@ -234,11 +247,7 @@ export const AllTrainDiagramView: FC = () => {
|
||||
keyExtractor={(item) => item}
|
||||
//initialNumToRender={100}
|
||||
/>
|
||||
<KeyboardAvoidingView
|
||||
behavior="padding"
|
||||
keyboardVerticalOffset={80}
|
||||
enabled={Platform.OS === "ios"}
|
||||
>
|
||||
<View>
|
||||
<View style={{ height: 35, flexDirection: "row" }}>
|
||||
<Switch
|
||||
value={useRegex}
|
||||
@@ -249,7 +258,7 @@ export const AllTrainDiagramView: FC = () => {
|
||||
color="red"
|
||||
style={{ margin: 5 }}
|
||||
/>
|
||||
<Text style={{ color: "white", fontSize: 20, margin: 5 }}>
|
||||
<Text style={{ color: fixed.textOnPrimary, fontSize: 20, margin: 5 }}>
|
||||
正規表現を使用
|
||||
</Text>
|
||||
<Switch
|
||||
@@ -261,7 +270,7 @@ export const AllTrainDiagramView: FC = () => {
|
||||
color="red"
|
||||
style={{ margin: 5 }}
|
||||
/>
|
||||
<Text style={{ color: "white", fontSize: 20, margin: 5 }}>
|
||||
<Text style={{ color: fixed.textOnPrimary, fontSize: 20, margin: 5 }}>
|
||||
駅名で検索
|
||||
</Text>
|
||||
</View>
|
||||
@@ -269,7 +278,7 @@ export const AllTrainDiagramView: FC = () => {
|
||||
style={{
|
||||
height: 35,
|
||||
flexDirection: "row",
|
||||
backgroundColor: "#0099CC",
|
||||
backgroundColor: fixed.primary,
|
||||
margin: 5,
|
||||
display: useRegex ? "flex" : "none",
|
||||
}}
|
||||
@@ -325,31 +334,28 @@ export const AllTrainDiagramView: FC = () => {
|
||||
height: 35,
|
||||
margin: 5,
|
||||
alignItems: "center",
|
||||
backgroundColor: "#F4F4F4",
|
||||
backgroundColor: colors.searchBackground,
|
||||
flexDirection: "row",
|
||||
paddingLeft: 10,
|
||||
paddingRight: 10,
|
||||
borderRadius: 25,
|
||||
borderColor: "#F4F4F4",
|
||||
borderColor: colors.searchBorder,
|
||||
}}
|
||||
>
|
||||
<TextInput
|
||||
placeholder="列番・列車名を入力してフィルタリングします。"
|
||||
onFocus={() => setKeyBoardVisible(true)}
|
||||
onFocus={() => {}}
|
||||
onEndEditing={() => {}}
|
||||
onChange={(ret) => setInput(ret.nativeEvent.text)}
|
||||
value={input}
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
</View>
|
||||
</KeyboardAvoidingView>
|
||||
</View>
|
||||
<BigButton
|
||||
onPress={goBack}
|
||||
string="閉じる"
|
||||
style={{
|
||||
display:
|
||||
Platform.OS === "ios" ? "flex" : keyBoardVisible ? "none" : "flex",
|
||||
}}
|
||||
style={{ display: keyBoardVisible ? "none" : "flex" }}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
||||
129
components/AndroidWidget/FelicaQuickAccessWidget.tsx
Normal file
129
components/AndroidWidget/FelicaQuickAccessWidget.tsx
Normal file
@@ -0,0 +1,129 @@
|
||||
import React from "react";
|
||||
import dayjs from "dayjs";
|
||||
import { FlexWidget, OverlapWidget, SvgWidget, TextWidget } from "react-native-android-widget";
|
||||
import { AS } from "../../storageControl";
|
||||
import { STORAGE_KEYS } from "../../constants";
|
||||
|
||||
type LastFelicaSnapshot = {
|
||||
balance: number;
|
||||
idm: string;
|
||||
systemCode?: string;
|
||||
scannedAt: string;
|
||||
};
|
||||
|
||||
export async function getFelicaQuickAccessData() {
|
||||
const nowText = dayjs().format("HH:mm");
|
||||
const snapshot = (await AS.getItem(STORAGE_KEYS.FELICA_LAST_SNAPSHOT).catch(
|
||||
() => null
|
||||
)) as LastFelicaSnapshot | null;
|
||||
|
||||
const hasBalance =
|
||||
snapshot != null && typeof snapshot.balance === "number" && snapshot.balance >= 0;
|
||||
|
||||
return {
|
||||
nowText,
|
||||
amountText: hasBalance ? `\u00A5${snapshot.balance.toLocaleString()}` : "未読取",
|
||||
detailText:
|
||||
snapshot?.scannedAt != null
|
||||
? `最終読取: ${snapshot.scannedAt}`
|
||||
: "カードをタップして読取開始",
|
||||
};
|
||||
}
|
||||
|
||||
// IC card + NFC arcs, -22° rotated, as widget background
|
||||
const IC_CARD_BG_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 360 160">
|
||||
<g transform="translate(168,-32) rotate(-22)" opacity="0.24">
|
||||
<rect x="0" y="0" width="208" height="132" rx="14" fill="#0099CC"/>
|
||||
<rect x="0" y="28" width="208" height="32" fill="#007AAA"/>
|
||||
<rect x="16" y="74" width="38" height="26" rx="4" fill="#FFD966"/>
|
||||
<line x1="16" y1="87" x2="54" y2="87" stroke="#C8A800" stroke-width="1.5"/>
|
||||
<line x1="29" y1="74" x2="29" y2="100" stroke="#C8A800" stroke-width="1.5"/>
|
||||
<line x1="40" y1="74" x2="40" y2="100" stroke="#C8A800" stroke-width="1.5"/>
|
||||
<circle cx="68" cy="88" r="5" fill="white" opacity="0.55"/>
|
||||
<circle cx="82" cy="88" r="5" fill="white" opacity="0.55"/>
|
||||
<circle cx="96" cy="88" r="5" fill="white" opacity="0.55"/>
|
||||
<circle cx="110" cy="88" r="5" fill="white" opacity="0.55"/>
|
||||
<circle cx="154" cy="80" r="7" fill="white" opacity="0.65"/>
|
||||
<path d="M167 63 A20 20 0 0 1 167 97" fill="none" stroke="white" stroke-width="5" stroke-linecap="round" opacity="0.68"/>
|
||||
<path d="M178 52 A34 34 0 0 1 178 108" fill="none" stroke="white" stroke-width="5" stroke-linecap="round" opacity="0.50"/>
|
||||
<path d="M189 41 A48 48 0 0 1 189 119" fill="none" stroke="white" stroke-width="5" stroke-linecap="round" opacity="0.34"/>
|
||||
</g>
|
||||
<g transform="translate(4,80) rotate(-22)" stroke="#0A88CC" fill="none" stroke-linecap="round">
|
||||
<circle cx="16" cy="42" r="5" fill="#0A88CC" stroke="none" opacity="0.23"/>
|
||||
<path d="M28 30 A17 17 0 0 1 28 54" stroke-width="5" opacity="0.20"/>
|
||||
<path d="M38 22 A27 27 0 0 1 38 62" stroke-width="5" opacity="0.15"/>
|
||||
<path d="M48 14 A37 37 0 0 1 48 70" stroke-width="5" opacity="0.10"/>
|
||||
</g>
|
||||
</svg>`;
|
||||
|
||||
export function FelicaQuickAccessWidget({
|
||||
amountText,
|
||||
detailText,
|
||||
}: {
|
||||
amountText: string;
|
||||
nowText: string;
|
||||
detailText: string;
|
||||
}) {
|
||||
const hasValue = amountText !== "未読取";
|
||||
|
||||
return (
|
||||
<OverlapWidget
|
||||
style={{
|
||||
height: "match_parent",
|
||||
width: "match_parent",
|
||||
backgroundColor: "#DDF2FF",
|
||||
borderRadius: 20,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
clickAction="OPEN_URI"
|
||||
clickActionData={{ uri: "jrshikoku://open/felica" }}
|
||||
>
|
||||
{/* Background: IC card + NFC icons, rotated */}
|
||||
<SvgWidget
|
||||
style={{ height: "match_parent", width: "match_parent" }}
|
||||
svg={IC_CARD_BG_SVG}
|
||||
/>
|
||||
{/* Foreground: balance only */}
|
||||
<FlexWidget
|
||||
style={{
|
||||
height: "match_parent",
|
||||
width: "match_parent",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
paddingLeft: 16,
|
||||
paddingRight: 16,
|
||||
}}
|
||||
>
|
||||
<TextWidget
|
||||
text={amountText}
|
||||
style={{
|
||||
fontSize: 48,
|
||||
fontWeight: "bold",
|
||||
color: hasValue ? "#00527A" : "#699BB8",
|
||||
}}
|
||||
/>
|
||||
</FlexWidget>
|
||||
{/* Bottom-right: scan timestamp */}
|
||||
{hasValue && (
|
||||
<FlexWidget
|
||||
style={{
|
||||
height: "match_parent",
|
||||
width: "match_parent",
|
||||
justifyContent: "flex-end",
|
||||
alignItems: "flex-end",
|
||||
paddingRight: 10,
|
||||
paddingBottom: 8,
|
||||
}}
|
||||
>
|
||||
<TextWidget
|
||||
text={detailText}
|
||||
style={{
|
||||
fontSize: 11,
|
||||
color: "#3A7EA0",
|
||||
}}
|
||||
/>
|
||||
</FlexWidget>
|
||||
)}
|
||||
</OverlapWidget>
|
||||
);
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
import * as React from "react";
|
||||
import { StyleSheet, View } from "react-native";
|
||||
import { WidgetPreview } from "react-native-android-widget";
|
||||
|
||||
import { HelloWidget } from "./HelloWidget";
|
||||
|
||||
export function HelloWidgetPreviewScreen() {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<WidgetPreview
|
||||
renderWidget={() => <HelloWidget />}
|
||||
width={200}
|
||||
height={200}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
});
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
ListWidget,
|
||||
} from "react-native-android-widget";
|
||||
import dayjs from "dayjs";
|
||||
import { ToastAndroid } from "react-native";
|
||||
|
||||
export const getInfoString = async () => {
|
||||
// Fetch data from the server
|
||||
@@ -35,7 +34,8 @@ export function InfoWidget({ time, text }) {
|
||||
backgroundColor: "#ffffff",
|
||||
borderRadius: 16,
|
||||
}}
|
||||
clickAction="WIDGET_CLICK"
|
||||
clickAction="OPEN_URI"
|
||||
clickActionData={{ uri: "jrshikoku://open/operation" }}
|
||||
>
|
||||
<FlexWidget
|
||||
style={{
|
||||
@@ -87,7 +87,6 @@ export function InfoWidget({ time, text }) {
|
||||
|
||||
fontSize: 20,
|
||||
}}
|
||||
clickAction="OPEN_APP"
|
||||
text={text}
|
||||
/>
|
||||
) : (
|
||||
@@ -96,7 +95,6 @@ export function InfoWidget({ time, text }) {
|
||||
color: "#000000",
|
||||
fontSize: 20,
|
||||
}}
|
||||
clickAction="WIDGET_CLICK"
|
||||
text="通常運行中です。"
|
||||
/>
|
||||
)}
|
||||
@@ -104,9 +102,3 @@ export function InfoWidget({ time, text }) {
|
||||
</FlexWidget>
|
||||
);
|
||||
}
|
||||
|
||||
const FlexText = ({ flex, text }) => (
|
||||
<FlexWidget style={{ flex }}>
|
||||
<TextWidget style={{ fontSize: 20, color: "#000000" }} text={text} />
|
||||
</FlexWidget>
|
||||
);
|
||||
|
||||
222
components/AndroidWidget/ShortcutWidget.tsx
Normal file
222
components/AndroidWidget/ShortcutWidget.tsx
Normal file
@@ -0,0 +1,222 @@
|
||||
import React from "react";
|
||||
import { FlexWidget, TextWidget } from "react-native-android-widget";
|
||||
import { getDelayData } from "./TraInfoEXWidget";
|
||||
import { getInfoString } from "./InfoWidget";
|
||||
import { getFelicaQuickAccessData } from "./FelicaQuickAccessWidget";
|
||||
|
||||
export async function getShortcutData() {
|
||||
const [delayResult, infoResult, felicaResult] = await Promise.allSettled([
|
||||
getDelayData(),
|
||||
getInfoString(),
|
||||
getFelicaQuickAccessData(),
|
||||
]);
|
||||
|
||||
const delayCount =
|
||||
delayResult.status === "fulfilled" && delayResult.value.delayString
|
||||
? delayResult.value.delayString.length
|
||||
: 0;
|
||||
|
||||
const hasInfo =
|
||||
infoResult.status === "fulfilled" &&
|
||||
infoResult.value.text != null &&
|
||||
infoResult.value.text.length > 0;
|
||||
|
||||
const amountText =
|
||||
felicaResult.status === "fulfilled"
|
||||
? felicaResult.value.amountText
|
||||
: "未読取";
|
||||
|
||||
return { delayCount, hasInfo, amountText };
|
||||
}
|
||||
|
||||
export type ShortcutWidgetProps = {
|
||||
delayCount: number;
|
||||
hasInfo: boolean;
|
||||
amountText: string;
|
||||
};
|
||||
|
||||
const TILE_BG = "#E8F4FB";
|
||||
const SPACING = 6;
|
||||
|
||||
/** 汎用グリッドタイル */
|
||||
function GridTile({
|
||||
icon,
|
||||
label,
|
||||
sub,
|
||||
subColor,
|
||||
badgeText,
|
||||
badgeColor,
|
||||
bottomTrailing,
|
||||
uri,
|
||||
}: {
|
||||
icon: string;
|
||||
label: string;
|
||||
sub?: string;
|
||||
subColor?: string;
|
||||
badgeText?: string;
|
||||
badgeColor?: string;
|
||||
bottomTrailing?: string;
|
||||
uri: string;
|
||||
}) {
|
||||
return (
|
||||
<FlexWidget
|
||||
style={{
|
||||
flex: 1,
|
||||
backgroundColor: TILE_BG,
|
||||
borderRadius: 10,
|
||||
padding: 8,
|
||||
justifyContent: "center",
|
||||
}}
|
||||
clickAction="OPEN_URI"
|
||||
clickActionData={{ uri }}
|
||||
>
|
||||
<FlexWidget style={{ flexDirection: "row", alignItems: "center" }}>
|
||||
<TextWidget text={icon} style={{ fontSize: 22 }} />
|
||||
<FlexWidget style={{ marginLeft: 6, flex: 1 }}>
|
||||
{badgeText !== undefined && badgeColor !== undefined ? (
|
||||
<FlexWidget style={{ flexDirection: "row", alignItems: "center" }}>
|
||||
<TextWidget
|
||||
text={label}
|
||||
style={{ color: "#000000", fontSize: 12, fontWeight: "bold" }}
|
||||
/>
|
||||
<FlexWidget
|
||||
style={{
|
||||
backgroundColor: badgeColor as any,
|
||||
borderRadius: 6,
|
||||
paddingLeft: 4,
|
||||
paddingRight: 4,
|
||||
paddingTop: 1,
|
||||
paddingBottom: 1,
|
||||
marginLeft: 3,
|
||||
}}
|
||||
>
|
||||
<TextWidget
|
||||
text={badgeText}
|
||||
style={{ color: "#ffffff", fontSize: 9, fontWeight: "bold" }}
|
||||
/>
|
||||
</FlexWidget>
|
||||
</FlexWidget>
|
||||
) : (
|
||||
<TextWidget
|
||||
text={label}
|
||||
style={{ color: "#000000", fontSize: 12, fontWeight: "bold" }}
|
||||
/>
|
||||
)}
|
||||
{sub !== undefined ? (
|
||||
<TextWidget
|
||||
text={sub}
|
||||
style={{ color: subColor ?? "#555555", fontSize: 10, marginTop: 1 }}
|
||||
/>
|
||||
) : (
|
||||
<TextWidget text="" style={{ fontSize: 1 }} />
|
||||
)}
|
||||
</FlexWidget>
|
||||
</FlexWidget>
|
||||
{bottomTrailing !== undefined && (
|
||||
<FlexWidget
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "flex-end",
|
||||
width: "match_parent",
|
||||
}}
|
||||
>
|
||||
<TextWidget
|
||||
text={bottomTrailing}
|
||||
style={{ color: "#555555", fontSize: 8, fontWeight: "600" }}
|
||||
/>
|
||||
</FlexWidget>
|
||||
)}
|
||||
</FlexWidget>
|
||||
);
|
||||
}
|
||||
|
||||
export function ShortcutWidget({ delayCount, hasInfo, amountText }: ShortcutWidgetProps) {
|
||||
return (
|
||||
<FlexWidget
|
||||
style={{
|
||||
height: "match_parent",
|
||||
width: "match_parent",
|
||||
backgroundColor: "#ffffff",
|
||||
borderRadius: 16,
|
||||
padding: 8,
|
||||
}}
|
||||
>
|
||||
{/* Row 1: 走行位置 | 遅延速報EX */}
|
||||
<FlexWidget
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
width: "match_parent",
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
<GridTile icon="🚃" label="走行位置" sub="列車の現在位置" uri="jrshikoku://positions/apps" />
|
||||
<FlexWidget style={{ width: SPACING, height: "match_parent" }}>
|
||||
<TextWidget text="" style={{ fontSize: 1 }} />
|
||||
</FlexWidget>
|
||||
<GridTile
|
||||
icon="⚡"
|
||||
label="遅延速報EX"
|
||||
sub="列車の遅延情報"
|
||||
uri="jrshikoku://open/traininfo"
|
||||
badgeText={delayCount > 0 ? `${delayCount}件` : "なし"}
|
||||
badgeColor={delayCount > 0 ? "#E53935" : "#9E9E9E"}
|
||||
/>
|
||||
</FlexWidget>
|
||||
|
||||
{/* Spacer between rows */}
|
||||
<FlexWidget style={{ height: SPACING, width: "match_parent" }}>
|
||||
<TextWidget text="" style={{ fontSize: 1 }} />
|
||||
</FlexWidget>
|
||||
|
||||
{/* Row 2: 運行情報 | ICカード */}
|
||||
<FlexWidget
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
width: "match_parent",
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
<GridTile
|
||||
icon="📋"
|
||||
label="運行情報"
|
||||
sub="列車の運行状況"
|
||||
uri="jrshikoku://open/operation"
|
||||
badgeText={hasInfo ? "あり" : "なし"}
|
||||
badgeColor={hasInfo ? "#FF7043" : "#9E9E9E"}
|
||||
/>
|
||||
<FlexWidget style={{ width: SPACING, height: "match_parent" }}>
|
||||
<TextWidget text="" style={{ fontSize: 1 }} />
|
||||
</FlexWidget>
|
||||
<GridTile
|
||||
icon="💳"
|
||||
label="ICカード"
|
||||
sub={amountText}
|
||||
subColor="#0099CC"
|
||||
uri="jrshikoku://open/felica"
|
||||
/>
|
||||
</FlexWidget>
|
||||
|
||||
{/* Spacer between rows */}
|
||||
<FlexWidget style={{ height: SPACING, width: "match_parent" }}>
|
||||
<TextWidget text="" style={{ fontSize: 1 }} />
|
||||
</FlexWidget>
|
||||
|
||||
{/* Row 3: トップメニュー(全幅) */}
|
||||
<FlexWidget
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
width: "match_parent",
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
<GridTile
|
||||
icon="🏠"
|
||||
label="トップメニュー"
|
||||
sub="アプリのトップへ"
|
||||
bottomTrailing="クイックアクセス"
|
||||
uri="jrshikoku://open/topmenu"
|
||||
/>
|
||||
</FlexWidget>
|
||||
</FlexWidget>
|
||||
);
|
||||
}
|
||||
87
components/AndroidWidget/StrangeTrainWidget.tsx
Normal file
87
components/AndroidWidget/StrangeTrainWidget.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
import React from "react";
|
||||
import { FlexWidget, TextWidget } from "react-native-android-widget";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
export function getStrangeTrainData() {
|
||||
return { nowText: dayjs().format("HH:mm") };
|
||||
}
|
||||
|
||||
export function StrangeTrainWidget({ nowText }: { nowText: string }) {
|
||||
return (
|
||||
<FlexWidget
|
||||
style={{
|
||||
height: "match_parent",
|
||||
width: "match_parent",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
backgroundColor: "#ffffff",
|
||||
borderRadius: 16,
|
||||
}}
|
||||
clickAction="OPEN_URI"
|
||||
clickActionData={{ uri: "jrshikoku://open/traininfo" }}
|
||||
>
|
||||
<FlexWidget
|
||||
style={{
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
backgroundColor: "#0099CC",
|
||||
width: "match_parent",
|
||||
flexDirection: "row",
|
||||
paddingTop: 10,
|
||||
paddingBottom: 10,
|
||||
}}
|
||||
>
|
||||
<TextWidget
|
||||
text="怪レい列車BOT"
|
||||
style={{
|
||||
fontSize: 30,
|
||||
fontWeight: "bold",
|
||||
fontFamily: "Inter",
|
||||
color: "#fff",
|
||||
textAlign: "left",
|
||||
marginLeft: 10,
|
||||
}}
|
||||
/>
|
||||
<FlexWidget style={{ flex: 1 }} />
|
||||
<TextWidget
|
||||
text={nowText}
|
||||
style={{
|
||||
fontSize: 30,
|
||||
fontFamily: "Inter",
|
||||
color: "#fff",
|
||||
textAlign: "right",
|
||||
marginRight: 10,
|
||||
}}
|
||||
/>
|
||||
</FlexWidget>
|
||||
|
||||
<FlexWidget
|
||||
style={{
|
||||
flex: 1,
|
||||
backgroundColor: "#fff",
|
||||
width: "match_parent",
|
||||
padding: 10,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<TextWidget
|
||||
text="🚃"
|
||||
style={{
|
||||
fontSize: 36,
|
||||
textAlign: "center",
|
||||
marginBottom: 8,
|
||||
}}
|
||||
/>
|
||||
<TextWidget
|
||||
style={{
|
||||
color: "#000000",
|
||||
fontSize: 20,
|
||||
textAlign: "center",
|
||||
}}
|
||||
text="通知で怪レい列車をお知らせします"
|
||||
/>
|
||||
</FlexWidget>
|
||||
</FlexWidget>
|
||||
);
|
||||
}
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
ListWidget,
|
||||
} from "react-native-android-widget";
|
||||
import dayjs from "dayjs";
|
||||
import { ToastAndroid } from "react-native";
|
||||
|
||||
export const getDelayData = async () => {
|
||||
// Fetch data from the server
|
||||
@@ -34,7 +33,8 @@ export function TraInfoEXWidget({ time, delayString }) {
|
||||
backgroundColor: "#ffffff",
|
||||
borderRadius: 16,
|
||||
}}
|
||||
clickAction="WIDGET_CLICK"
|
||||
clickAction="OPEN_URI"
|
||||
clickActionData={{ uri: "jrshikoku://open/traininfo" }}
|
||||
>
|
||||
<FlexWidget
|
||||
style={{
|
||||
@@ -90,7 +90,6 @@ export function TraInfoEXWidget({ time, delayString }) {
|
||||
backgroundColor: "#ffffff",
|
||||
flex: 1,
|
||||
}}
|
||||
clickAction="WIDGET_CLICK"
|
||||
key={data[1]}
|
||||
>
|
||||
<FlexText flex={3} text={data[0].replace("\n", "")} />
|
||||
@@ -105,7 +104,6 @@ export function TraInfoEXWidget({ time, delayString }) {
|
||||
color: "#000000",
|
||||
fontSize: 20,
|
||||
}}
|
||||
clickAction="WIDGET_CLICK"
|
||||
text="現在、5分以上の遅れはありません。"
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
import React from "react";
|
||||
import { TraInfoEXWidget, getDelayData } from "./TraInfoEXWidget";
|
||||
import { ToastAndroid } from "react-native";
|
||||
import { InfoWidget, getInfoString } from "./InfoWidget";
|
||||
import { AS } from "../../storageControl";
|
||||
import {
|
||||
FelicaQuickAccessWidget,
|
||||
getFelicaQuickAccessData,
|
||||
} from "./FelicaQuickAccessWidget";
|
||||
import { ShortcutWidget, getShortcutData } from "./ShortcutWidget";
|
||||
import { StrangeTrainWidget, getStrangeTrainData } from "./StrangeTrainWidget";
|
||||
|
||||
export const nameToWidget = {
|
||||
JR_shikoku_train_info: TraInfoEXWidget,
|
||||
Info_Widget: InfoWidget,
|
||||
JR_shikoku_apps_shortcut: ShortcutWidget,
|
||||
JR_shikoku_felica_balance: FelicaQuickAccessWidget,
|
||||
JR_shikoku_train_strange: StrangeTrainWidget,
|
||||
};
|
||||
|
||||
export async function widgetTaskHandler(props) {
|
||||
@@ -17,41 +24,51 @@ export async function widgetTaskHandler(props) {
|
||||
clickAction,
|
||||
clickActionData,
|
||||
} = props;
|
||||
const WidgetName = await AS.getItem(
|
||||
`widgetType/${widgetInfo.widgetId}`
|
||||
).catch((e) => "JR_shikoku_train_info");
|
||||
// ToastAndroid.show(
|
||||
// `Widget Action: ${JSON.stringify(widgetInfo.widgetId)}`,
|
||||
// ToastAndroid.SHORT
|
||||
// );
|
||||
//ToastAndroid.show(`Widget Name: ${WidgetName}`, ToastAndroid.SHORT);
|
||||
|
||||
switch (widgetAction) {
|
||||
case "WIDGET_ADDED":
|
||||
case "WIDGET_UPDATE":
|
||||
case "WIDGET_CLICK":
|
||||
case "WIDGET_RESIZED":
|
||||
switch (WidgetName) {
|
||||
case "Info_Widget": {
|
||||
const { time, text } = await getInfoString();
|
||||
renderWidget(
|
||||
<InfoWidget time={time} text={text && text.toString()} />
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "JR_shikoku_train_info":
|
||||
default: {
|
||||
const { time, delayString } = await getDelayData();
|
||||
renderWidget(
|
||||
<TraInfoEXWidget time={time} delayString={delayString} />
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "WIDGET_RESIZED": {
|
||||
const name = widgetInfo.widgetName;
|
||||
|
||||
if (name === "JR_shikoku_felica_balance") {
|
||||
const quickData = await getFelicaQuickAccessData();
|
||||
renderWidget(<FelicaQuickAccessWidget {...quickData} />);
|
||||
break;
|
||||
}
|
||||
|
||||
if (name === "JR_shikoku_apps_shortcut") {
|
||||
const data = await getShortcutData();
|
||||
renderWidget(<ShortcutWidget {...data} />);
|
||||
break;
|
||||
}
|
||||
|
||||
if (name === "JR_shikoku_train_strange") {
|
||||
const data = getStrangeTrainData();
|
||||
renderWidget(<StrangeTrainWidget {...data} />);
|
||||
break;
|
||||
}
|
||||
|
||||
if (name === "JR_shikoku_info") {
|
||||
const { time, text } = await getInfoString();
|
||||
renderWidget(
|
||||
<InfoWidget time={time} text={text && text.toString()} />
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
// JR_shikoku_train_info and default
|
||||
{
|
||||
const { time, delayString } = await getDelayData();
|
||||
renderWidget(
|
||||
<TraInfoEXWidget time={time} delayString={delayString} />
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "WIDGET_DELETED":
|
||||
AS.removeItem(`widgetType/${widgetInfo.widgetId}`);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
|
||||
@@ -3,11 +3,15 @@ import {
|
||||
View,
|
||||
Platform,
|
||||
useWindowDimensions,
|
||||
StatusBar,
|
||||
useColorScheme,
|
||||
} from "react-native";
|
||||
import Constants from "expo-constants";
|
||||
import * as Updates from "expo-updates";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { LinearGradient } from "expo-linear-gradient";
|
||||
|
||||
import { lineList } from "../lib/getStationList";
|
||||
import lineColorList from "../assets/originData/lineColorList";
|
||||
import { lineList, stationIDPair } from "../lib/getStationList";
|
||||
import { useCurrentTrain } from "../stateBox/useCurrentTrain";
|
||||
import { useDeviceOrientationChange } from "../stateBox/useDeviceOrientationChange";
|
||||
import { SheetManager } from "react-native-actions-sheet";
|
||||
@@ -20,20 +24,31 @@ import { MapsButton } from "./Apps/MapsButton";
|
||||
import { ReloadButton } from "./Apps/ReloadButton";
|
||||
import { useStationList } from "../stateBox/useStationList";
|
||||
import { FixedPositionBox } from "./Apps/FixedPositionBox";
|
||||
/*
|
||||
import StatusbarDetect from '../StatusbarDetect';
|
||||
var Status = StatusbarDetect(); */
|
||||
|
||||
const top = Platform.OS == "ios" ? Constants.statusBarHeight : 0;
|
||||
|
||||
export default function Apps() {
|
||||
const { webview, fixedPosition, setFixedPosition } = useCurrentTrain();
|
||||
const { height, width } = useWindowDimensions();
|
||||
const { navigate } = useNavigation();
|
||||
const { isLandscape } = useDeviceOrientationChange();
|
||||
const { top } = useSafeAreaInsets();
|
||||
const handleLayout = () => {};
|
||||
const { originalStationList } = useStationList();
|
||||
const { mapSwitch, trainInfo, setTrainInfo } = useTrainMenu();
|
||||
const { mapSwitch, trainInfo, setTrainInfo, selectedLine } = useTrainMenu();
|
||||
const isDark = useColorScheme() === "dark";
|
||||
|
||||
const lineColor = selectedLine && stationIDPair[selectedLine]
|
||||
? lineColorList[stationIDPair[selectedLine]]
|
||||
: null;
|
||||
|
||||
// 路線色の両端を暗くしたグラデーション用カラー
|
||||
const darkenHex = (hex: string, factor: number) => {
|
||||
const h = hex.replace("#", "");
|
||||
const r = Math.round(parseInt(h.slice(0, 2), 16) * factor);
|
||||
const g = Math.round(parseInt(h.slice(2, 4), 16) * factor);
|
||||
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 lineColorDark = lineColor ? darkenHex(lineColor, 0.55) : null;
|
||||
|
||||
const openStationACFromEachTrainInfo = async (stationName) => {
|
||||
await SheetManager.hide("EachTrainInfo");
|
||||
@@ -67,16 +82,30 @@ export default function Apps() {
|
||||
SheetManager.hide("StationDetailView");
|
||||
}
|
||||
};
|
||||
const bgColor = isDark ? "#1c1c1e" : "#ffffff";
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
height: "100%",
|
||||
paddingTop: top,
|
||||
flexDirection: isLandscape ? "row" : "column",
|
||||
}}
|
||||
onLayout={handleLayout}
|
||||
>
|
||||
{/* {Status} */}
|
||||
<View style={{ flex: 1, backgroundColor: bgColor }}>
|
||||
{lineColor && lineColorDark && (
|
||||
<LinearGradient
|
||||
colors={[lineColorDark, lineColor]}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 0, y: 1 }}
|
||||
style={{ position: "absolute", top: 0, left: 0, right: 0, height: top }}
|
||||
/>
|
||||
)}
|
||||
{lineColor && (
|
||||
<StatusBar
|
||||
barStyle="light-content"
|
||||
/>
|
||||
)}
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
paddingTop: top,
|
||||
flexDirection: isLandscape ? "row" : "column",
|
||||
}}
|
||||
onLayout={handleLayout}
|
||||
>
|
||||
<AppsWebView
|
||||
{...{
|
||||
openStationACFromEachTrainInfo,
|
||||
@@ -99,6 +128,7 @@ export default function Apps() {
|
||||
) : (
|
||||
<NewMenu />
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,26 +1,85 @@
|
||||
import { useCurrentTrain } from "@/stateBox/useCurrentTrain";
|
||||
import { View, Platform } from "react-native";
|
||||
import { useKeepAwake } from "expo-keep-awake";
|
||||
import Constants from "expo-constants";
|
||||
import { AppState, InteractionManager, View } from "react-native";
|
||||
import {
|
||||
activateKeepAwakeAsync,
|
||||
deactivateKeepAwake,
|
||||
} from "expo-keep-awake";
|
||||
import { FixedTrain } from "./FixedPositionBox/FixedTrainBox";
|
||||
import { FixedStation } from "./FixedPositionBox/FixedStationBox";
|
||||
import { FixedNearestStationBox } from "./FixedPositionBox/FixedNearestStationBox";
|
||||
import { useEffect } from "react";
|
||||
import { useTrainMenu } from "@/stateBox/useTrainMenu";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
|
||||
const KEEP_AWAKE_TAG = "fixed-position-box";
|
||||
|
||||
const isActivityUnavailableError = (error: unknown) =>
|
||||
String(error).includes("The current activity is no longer available");
|
||||
|
||||
export const FixedPositionBox = () => {
|
||||
const { mapSwitch } = useTrainMenu();
|
||||
const { fixedPosition, fixedPositionSize, setFixedPositionSize } =
|
||||
useCurrentTrain();
|
||||
const { top } = useSafeAreaInsets();
|
||||
useEffect(() => {
|
||||
setFixedPositionSize(mapSwitch == "true" ? 76 : 80);
|
||||
}, [mapSwitch]);
|
||||
|
||||
useKeepAwake();
|
||||
useEffect(() => {
|
||||
if (__DEV__) {
|
||||
return;
|
||||
}
|
||||
|
||||
let mounted = true;
|
||||
let timerId: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
const activate = async () => {
|
||||
if (!mounted || AppState.currentState !== "active") {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await activateKeepAwakeAsync(KEEP_AWAKE_TAG);
|
||||
} catch (error) {
|
||||
if (!isActivityUnavailableError(error)) {
|
||||
console.warn("Failed to activate keep awake", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const interactionHandle = InteractionManager.runAfterInteractions(() => {
|
||||
timerId = setTimeout(() => {
|
||||
void activate();
|
||||
}, 250);
|
||||
});
|
||||
|
||||
const subscription = AppState.addEventListener("change", (state) => {
|
||||
if (state === "active") {
|
||||
timerId = setTimeout(() => {
|
||||
void activate();
|
||||
}, 250);
|
||||
return;
|
||||
}
|
||||
|
||||
deactivateKeepAwake(KEEP_AWAKE_TAG).catch(() => {});
|
||||
});
|
||||
|
||||
return () => {
|
||||
mounted = false;
|
||||
if (timerId) {
|
||||
clearTimeout(timerId);
|
||||
}
|
||||
interactionHandle.cancel();
|
||||
subscription.remove();
|
||||
deactivateKeepAwake(KEEP_AWAKE_TAG).catch(() => {});
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: Platform.OS == "ios" ? Constants.statusBarHeight : 0,
|
||||
top,
|
||||
borderRadius: 5,
|
||||
zIndex: 1500,
|
||||
width: "100%",
|
||||
@@ -35,6 +94,9 @@ export const FixedPositionBox = () => {
|
||||
{fixedPosition.type === "train" && (
|
||||
<FixedTrain trainID={fixedPosition.value} />
|
||||
)}
|
||||
{fixedPosition.type === "nearestStation" && (
|
||||
<FixedNearestStationBox />
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
34
components/Apps/FixedPositionBox/FixedNearestStationBox.tsx
Normal file
34
components/Apps/FixedPositionBox/FixedNearestStationBox.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import { useCurrentTrain } from "@/stateBox/useCurrentTrain";
|
||||
import { View, Text } from "react-native";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { FixedStation } from "./FixedStationBox";
|
||||
|
||||
export const FixedNearestStationBox = () => {
|
||||
const { colors } = useThemeColors();
|
||||
const { nearestStationID } = useCurrentTrain();
|
||||
|
||||
if (!nearestStationID) {
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
flexDirection: "row",
|
||||
backgroundColor: colors.background,
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 12,
|
||||
borderBottomWidth: 2,
|
||||
borderBottomColor: colors.textAccent,
|
||||
}}
|
||||
pointerEvents="box-none"
|
||||
>
|
||||
<Ionicons name="navigate-circle-outline" size={20} color={colors.textAccent} />
|
||||
<Text style={{ marginLeft: 6, color: colors.textAccent, fontSize: 14 }}>
|
||||
現在地を取得中...
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return <FixedStation stationID={nearestStationID} />;
|
||||
};
|
||||
@@ -19,25 +19,56 @@ import { useTrainMenu } from "@/stateBox/useTrainMenu";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
|
||||
import { FC, useEffect, useState } from "react";
|
||||
import { LayoutAnimation, Text, TouchableOpacity, View } from "react-native";
|
||||
import { FC, useCallback, useEffect, useRef, useState } from "react";
|
||||
import { Animated, LayoutAnimation, PermissionsAndroid, Platform, Text, TouchableOpacity, View } from "react-native";
|
||||
import { SheetManager } from "react-native-actions-sheet";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
import {
|
||||
startStationLockActivity,
|
||||
updateStationLockActivity,
|
||||
endStationLockActivity,
|
||||
isAvailable as isLiveActivityAvailable,
|
||||
StationTrainInfo,
|
||||
} from "expo-live-activity";
|
||||
|
||||
type props = {
|
||||
stationID: string;
|
||||
};
|
||||
|
||||
export const FixedStation: FC<props> = ({ stationID }) => {
|
||||
const { colors, fixed } = useThemeColors();
|
||||
const { mapSwitch } = useTrainMenu();
|
||||
const {
|
||||
currentTrain,
|
||||
fixedPosition,
|
||||
setFixedPosition,
|
||||
fixedPositionSize,
|
||||
setFixedPositionSize,
|
||||
liveNotificationActive,
|
||||
setLiveNotificationActive,
|
||||
} = useCurrentTrain();
|
||||
const { getStationDataFromId } = useStationList();
|
||||
const { stationList } = useStationList();
|
||||
const { navigate } = useNavigation();
|
||||
const [station, setStation] = useState<StationProps[]>([]);
|
||||
|
||||
// GPS追従中の点滅アニメーション
|
||||
const pulseAnim = useRef(new Animated.Value(1)).current;
|
||||
const isGpsFollowing = fixedPosition?.type === "nearestStation";
|
||||
useEffect(() => {
|
||||
if (!isGpsFollowing) {
|
||||
pulseAnim.setValue(1);
|
||||
return;
|
||||
}
|
||||
const loop = Animated.loop(
|
||||
Animated.sequence([
|
||||
Animated.timing(pulseAnim, { toValue: 0.4, duration: 600, useNativeDriver: true }),
|
||||
Animated.timing(pulseAnim, { toValue: 1, duration: 600, useNativeDriver: true }),
|
||||
])
|
||||
);
|
||||
loop.start();
|
||||
return () => loop.stop();
|
||||
}, [isGpsFollowing]);
|
||||
useEffect(() => {
|
||||
const data = getStationDataFromId(stationID);
|
||||
setStation(data);
|
||||
@@ -48,7 +79,7 @@ export const FixedStation: FC<props> = ({ stationID }) => {
|
||||
: "white";
|
||||
////
|
||||
|
||||
const { allTrainDiagram } = useAllTrainDiagram();
|
||||
const { allTrainDiagram, allCustomTrainData } = useAllTrainDiagram();
|
||||
const { areaStationID } = useAreaInfo();
|
||||
const [stationDiagram, setStationDiagram] = useState({}); //当該駅の全時刻表
|
||||
const [isInfoArea, setIsInfoArea] = useState(false);
|
||||
@@ -95,7 +126,113 @@ export const FixedStation: FC<props> = ({ stationID }) => {
|
||||
.filter((d) => !d.isThrough)
|
||||
.filter((d) => d.lastStation != station[0].Station_JP); //最終列車表示設定
|
||||
setSelectedTrain(data);
|
||||
}, [trainTimeAndNumber, currentTrain /*finalSwitch*/]);
|
||||
}, [trainTimeAndNumber, currentTrain, tick /*liveActivity periodic refresh*/]);
|
||||
|
||||
// ── Live Notification ──
|
||||
const [liveNotifyId, setLiveNotifyId] = useState<string | null>(null);
|
||||
const liveNotifyIdRef = useRef<string | null>(null);
|
||||
const hasStartedRef = useRef(false);
|
||||
const [tick, setTick] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
liveNotifyIdRef.current = liveNotifyId;
|
||||
}, [liveNotifyId]);
|
||||
|
||||
// Live Activity 起動中は 60 秒ごとに selectedTrain を強制再計算して時刻フィルタを再適用
|
||||
useEffect(() => {
|
||||
if (!liveNotifyId) return;
|
||||
const interval = setInterval(() => setTick((t) => t + 1), 60000);
|
||||
return () => clearInterval(interval);
|
||||
}, [liveNotifyId]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (liveNotifyIdRef.current) {
|
||||
endStationLockActivity(liveNotifyIdRef.current).catch(() => {});
|
||||
setLiveNotificationActive(false);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const buildTrainsInfo = useCallback((): StationTrainInfo[] => {
|
||||
return selectedTrain.slice(0, 7).map((d) => {
|
||||
const customData = getCurrentTrainData(
|
||||
d.train,
|
||||
currentTrain,
|
||||
allCustomTrainData
|
||||
);
|
||||
const currentTrainDataForTrain = checkDuplicateTrainData(
|
||||
currentTrain.filter((a) => a.num === d.train),
|
||||
stationList
|
||||
);
|
||||
const { name, color: typeColor } = getTrainType({ type: customData.type, whiteMode: true });
|
||||
const delayStatus = `${getTrainDelayStatus(
|
||||
currentTrainDataForTrain,
|
||||
station[0]?.Station_JP
|
||||
)}`;
|
||||
return {
|
||||
time: d.time,
|
||||
typeName: name,
|
||||
trainName: customData.train_name || "",
|
||||
destination: d.lastStation,
|
||||
platform: "",
|
||||
delayStatus: delayStatus || "定刻",
|
||||
typeColor: typeColor || "",
|
||||
trainNumber: d.train,
|
||||
};
|
||||
});
|
||||
}, [selectedTrain, currentTrain, allCustomTrainData, stationList, station]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!liveNotifyId || station.length === 0) return;
|
||||
const trains = buildTrainsInfo();
|
||||
updateStationLockActivity(liveNotifyId, {
|
||||
nextTrainTime: trains[0]?.time || "",
|
||||
nextTrainDestination: trains[0]?.destination || "",
|
||||
nextTrainPlatform: trains[0]?.platform || "",
|
||||
followingTrainTime: trains[1]?.time || "",
|
||||
followingTrainDestination: trains[1]?.destination || "",
|
||||
stationName: station[0]?.Station_JP,
|
||||
stationNumber: station[0]?.StationNumber,
|
||||
lineColor,
|
||||
trains,
|
||||
}).catch(() => {});
|
||||
}, [selectedTrain, currentTrain, liveNotifyId, buildTrainsInfo]);
|
||||
|
||||
// バナー表示と同時にLive Activityを自動開始(selectedTrainが揃ってから)
|
||||
useEffect(() => {
|
||||
if (station.length === 0 || hasStartedRef.current || liveNotifyId) return;
|
||||
if (!isLiveActivityAvailable()) return;
|
||||
hasStartedRef.current = true;
|
||||
const startActivity = async () => {
|
||||
if (Platform.OS === 'android' && Platform.Version >= 33) {
|
||||
const granted = await PermissionsAndroid.request(
|
||||
PermissionsAndroid.PERMISSIONS.POST_NOTIFICATIONS
|
||||
);
|
||||
if (granted !== PermissionsAndroid.RESULTS.GRANTED) return;
|
||||
}
|
||||
const trains = buildTrainsInfo();
|
||||
try {
|
||||
const id = await startStationLockActivity({
|
||||
stationName: station[0]?.Station_JP || "",
|
||||
nextTrainTime: trains[0]?.time || "",
|
||||
nextTrainDestination: trains[0]?.destination || "",
|
||||
nextTrainPlatform: trains[0]?.platform || "",
|
||||
followingTrainTime: trains[1]?.time || "",
|
||||
followingTrainDestination: trains[1]?.destination || "",
|
||||
stationNumber: station[0]?.StationNumber,
|
||||
lineColor,
|
||||
trains,
|
||||
});
|
||||
setLiveNotifyId(id);
|
||||
setLiveNotificationActive(true);
|
||||
} catch (e) {
|
||||
console.warn('[LiveNotify] start error:', e);
|
||||
hasStartedRef.current = false;
|
||||
}
|
||||
};
|
||||
startActivity();
|
||||
}, [station, selectedTrain]);
|
||||
|
||||
return (
|
||||
<View
|
||||
@@ -130,7 +267,7 @@ export const FixedStation: FC<props> = ({ stationID }) => {
|
||||
alignSelf: "center",
|
||||
alignItems: "center",
|
||||
height: "100%",
|
||||
backgroundColor: "white",
|
||||
backgroundColor: colors.background,
|
||||
}}
|
||||
>
|
||||
<View
|
||||
@@ -158,19 +295,19 @@ export const FixedStation: FC<props> = ({ stationID }) => {
|
||||
padding: 0,
|
||||
paddingLeft: 5,
|
||||
flex: 1,
|
||||
color: "white",
|
||||
color: fixed.textOnPrimary,
|
||||
}}
|
||||
>
|
||||
{station[0]?.Station_JP}
|
||||
</Text>
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: "white",
|
||||
backgroundColor: colors.background,
|
||||
width: 6,
|
||||
borderLeftColor: lineColor,
|
||||
borderTopColor: lineColor,
|
||||
borderBottomColor: "white",
|
||||
borderRightColor: "white",
|
||||
borderBottomColor: colors.background,
|
||||
borderRightColor: colors.background,
|
||||
borderBottomWidth: 18,
|
||||
borderLeftWidth: 10,
|
||||
borderRightWidth: 0,
|
||||
@@ -182,11 +319,11 @@ export const FixedStation: FC<props> = ({ stationID }) => {
|
||||
<View
|
||||
style={{
|
||||
height: "100%",
|
||||
backgroundColor: "white",
|
||||
backgroundColor: colors.background,
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
<Text style={{ fontSize: 18 }}>次の発車予定:</Text>
|
||||
<Text style={{ fontSize: 18, color: colors.text }}>次の発車予定:</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -194,7 +331,7 @@ export const FixedStation: FC<props> = ({ stationID }) => {
|
||||
style={{
|
||||
flex: 5,
|
||||
flexDirection: "column",
|
||||
backgroundColor: "white",
|
||||
backgroundColor: colors.background,
|
||||
borderTopWidth: 5,
|
||||
borderTopColor: lineColor,
|
||||
overflow: "hidden",
|
||||
@@ -210,8 +347,8 @@ export const FixedStation: FC<props> = ({ stationID }) => {
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<View style={{ backgroundColor: "white", flex: 1 }}>
|
||||
<Text style={{ fontSize: parseInt("11%") }}>
|
||||
<View style={{ backgroundColor: colors.background, flex: 1 }}>
|
||||
<Text style={{ fontSize: parseInt("11%"), color: colors.text }}>
|
||||
当駅を発着する走行中の列車はありません。
|
||||
</Text>
|
||||
</View>
|
||||
@@ -245,17 +382,28 @@ export const FixedStation: FC<props> = ({ stationID }) => {
|
||||
height: 26,
|
||||
}}
|
||||
>
|
||||
<Ionicons name="lock-closed" size={15} color="white" />
|
||||
<Text
|
||||
style={{
|
||||
color: "white",
|
||||
fontSize: 15,
|
||||
paddingRight: 5,
|
||||
}}
|
||||
>
|
||||
駅位置ロック中
|
||||
</Text>
|
||||
<Ionicons name="close" size={15} color="white" />
|
||||
{isGpsFollowing ? (
|
||||
<Animated.View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
opacity: pulseAnim,
|
||||
}}
|
||||
>
|
||||
<Ionicons name="navigate" size={15} color={fixed.textOnPrimary} />
|
||||
<Text style={{ color: fixed.textOnPrimary, fontSize: 15, paddingRight: 5, paddingLeft: 3 }}>
|
||||
GPS追従中
|
||||
</Text>
|
||||
</Animated.View>
|
||||
) : (
|
||||
<>
|
||||
<Ionicons name="lock-closed" size={15} color={fixed.textOnPrimary} />
|
||||
<Text style={{ color: fixed.textOnPrimary, fontSize: 15, paddingRight: 5 }}>
|
||||
駅位置ロック中
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
<Ionicons name="close" size={15} color={fixed.textOnPrimary} />
|
||||
</View>
|
||||
|
||||
<View
|
||||
@@ -319,11 +467,11 @@ export const FixedStation: FC<props> = ({ stationID }) => {
|
||||
<Ionicons
|
||||
name={fixedPositionSize == 226 ? "chevron-up" : "chevron-down"}
|
||||
size={15}
|
||||
color="white"
|
||||
color={fixed.textOnPrimary}
|
||||
/>
|
||||
<Text
|
||||
style={{
|
||||
color: "white",
|
||||
color: fixed.textOnPrimary,
|
||||
paddingRight: 5,
|
||||
backgroundColor: lineColor,
|
||||
fontSize: 15,
|
||||
@@ -341,6 +489,7 @@ export const FixedStation: FC<props> = ({ stationID }) => {
|
||||
};
|
||||
|
||||
const FixedStationBoxEachTrain = ({ d, station, displaySize }) => {
|
||||
const { colors, isDark } = useThemeColors();
|
||||
const { currentTrain } = useCurrentTrain();
|
||||
const { stationList } = useStationList();
|
||||
const { allCustomTrainData } = useAllTrainDiagram();
|
||||
@@ -359,22 +508,22 @@ const FixedStationBoxEachTrain = ({ d, station, displaySize }) => {
|
||||
useEffect(() => {
|
||||
setTrain(getCurrentTrainData(d.train, currentTrain, allCustomTrainData));
|
||||
}, [currentTrain, d.train]);
|
||||
const { name, color } = getTrainType({ type: train.type, whiteMode: true });
|
||||
const { name, color } = getTrainType({ type: train.type, whiteMode: !isDark });
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: "white",
|
||||
backgroundColor: colors.background,
|
||||
flexDirection: "row",
|
||||
height: displaySize == 226 ? "7.5%" : "33%",
|
||||
overflow: "visible",
|
||||
}}
|
||||
>
|
||||
<Text style={{ fontSize: parseInt("11%"), flex: 3 }}>{d.time}</Text>
|
||||
<Text style={{ fontSize: parseInt("11%"), flex: 3, color: colors.text }}>{d.time}</Text>
|
||||
<Text style={{ fontSize: parseInt("11%"), flex: 4, color }}>{name}</Text>
|
||||
<Text style={{ fontSize: parseInt("11%"), flex: 4 }}>
|
||||
<Text style={{ fontSize: parseInt("11%"), flex: 4, color: colors.text }}>
|
||||
{d.lastStation}行
|
||||
</Text>
|
||||
<Text style={{ fontSize: parseInt("11%"), flex: 3 }}>
|
||||
<Text style={{ fontSize: parseInt("11%"), flex: 3, color: colors.text }}>
|
||||
{trainDelayStatus}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useAllTrainDiagram } from "@/stateBox/useAllTrainDiagram";
|
||||
import { useCurrentTrain } from "@/stateBox/useCurrentTrain";
|
||||
import { useStationList } from "@/stateBox/useStationList";
|
||||
import { StationProps } from "@/lib/CommonTypes";
|
||||
import { FC, useEffect, useState } from "react";
|
||||
import { FC, useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
@@ -11,6 +11,9 @@ import {
|
||||
Image,
|
||||
LayoutAnimation,
|
||||
ScrollView,
|
||||
Platform,
|
||||
PermissionsAndroid,
|
||||
AppState,
|
||||
} from "react-native";
|
||||
import { getTrainType } from "@/lib/getTrainType";
|
||||
import { trainDataType, trainPosition } from "@/lib/trainPositionTextArray";
|
||||
@@ -22,12 +25,20 @@ import { getCurrentTrainData } from "@/lib/getCurrentTrainData";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import dayjs from "dayjs";
|
||||
import { useTrainMenu } from "@/stateBox/useTrainMenu";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
import {
|
||||
startTrainFollowActivity,
|
||||
updateTrainFollowActivity,
|
||||
endTrainFollowActivity,
|
||||
isAvailable as isLiveActivityAvailable,
|
||||
} from "expo-live-activity";
|
||||
|
||||
type props = {
|
||||
trainID: string;
|
||||
};
|
||||
|
||||
export const FixedTrain: FC<props> = ({ trainID }) => {
|
||||
const { colors, fixed } = useThemeColors();
|
||||
const {
|
||||
setFixedPosition,
|
||||
currentTrain,
|
||||
@@ -35,11 +46,17 @@ export const FixedTrain: FC<props> = ({ trainID }) => {
|
||||
getPosition,
|
||||
fixedPositionSize,
|
||||
setFixedPositionSize,
|
||||
liveNotificationActive,
|
||||
setLiveNotificationActive,
|
||||
} = useCurrentTrain();
|
||||
|
||||
const { mapSwitch } = useTrainMenu();
|
||||
const { allCustomTrainData, allTrainDiagram } = useAllTrainDiagram();
|
||||
|
||||
const [liveNotifyId, setLiveNotifyId] = useState<string | null>(null);
|
||||
const liveNotifyIdRef = useRef<string | null>(null);
|
||||
const hasStartedRef = useRef(false);
|
||||
|
||||
const [train, setTrain] = useState<trainDataType>(null);
|
||||
const [customData, setCustomData] = useState<CustomTrainData>(
|
||||
getCurrentTrainData(trainID, currentTrain, allCustomTrainData)
|
||||
@@ -54,8 +71,11 @@ export const FixedTrain: FC<props> = ({ trainID }) => {
|
||||
if (stationData) {
|
||||
setTrain(stationData);
|
||||
} else {
|
||||
alert("追跡していた列車が消えました。追跡を終了します。");
|
||||
setFixedPosition({ type: null, value: null });
|
||||
// バックグラウンドでは一時的にデータが消えることがある→フォアグラウンド時のみ終了
|
||||
if (AppState.currentState === "active") {
|
||||
alert("追跡していた列車が消えました。追跡を終了します。");
|
||||
setFixedPosition({ type: null, value: null });
|
||||
}
|
||||
}
|
||||
}, [trainID, currentTrain]);
|
||||
|
||||
@@ -196,9 +216,25 @@ export const FixedTrain: FC<props> = ({ trainID }) => {
|
||||
|
||||
const delayTime = train?.delay == "入線" ? 0 : train?.delay;
|
||||
let additionalSkipCount = 0;
|
||||
|
||||
// 2駅間走行中の場合: ダイヤ順で後ろのインデックスが進行方向の駅
|
||||
// Direction に関係なく、travel-order で大きい方が「向かう駅」
|
||||
let searchStart: number;
|
||||
if (currentPosition.length === 2) {
|
||||
const idx0 = stopStationIDList.findIndex(d => d.includes(currentPosition[0]));
|
||||
const idx1 = stopStationIDList.findIndex(d => d.includes(currentPosition[1]));
|
||||
const aheadIdx = Math.max(
|
||||
idx0 >= 0 ? idx0 : -1,
|
||||
idx1 >= 0 ? idx1 : -1
|
||||
);
|
||||
searchStart = aheadIdx >= 0 ? aheadIdx : searchCountLast;
|
||||
} else {
|
||||
searchStart = searchCountFirst;
|
||||
}
|
||||
|
||||
for (
|
||||
let searchCount = searchCountFirst;
|
||||
searchCount < points.length;
|
||||
let searchCount = searchStart;
|
||||
searchCount < trainDataWidhThrough.length;
|
||||
searchCount++
|
||||
) {
|
||||
const nextPos = trainDataWidhThrough[searchCount];
|
||||
@@ -206,14 +242,18 @@ export const FixedTrain: FC<props> = ({ trainID }) => {
|
||||
if (nextPos) {
|
||||
const [station, se, time] = nextPos.split(",");
|
||||
|
||||
// 通過駅はスキップ
|
||||
if (se.includes("通")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 駅に停車中(1点一致)の場合は時刻判定不要
|
||||
if (searchCountFirst == searchCountLast) {
|
||||
if (se.includes("通")) {
|
||||
continue;
|
||||
}
|
||||
setNextStationData(getStationDataFromName(station));
|
||||
break;
|
||||
}
|
||||
//棒線駅判定
|
||||
|
||||
// 2駅間走行中: 時刻で既に通過済みか判定
|
||||
let distanceMinute = 0;
|
||||
if (time != "") {
|
||||
const now = dayjs();
|
||||
@@ -229,12 +269,8 @@ export const FixedTrain: FC<props> = ({ trainID }) => {
|
||||
}
|
||||
}
|
||||
if (distanceMinute >= 0) {
|
||||
if (se.includes("通")) {
|
||||
continue;
|
||||
} else {
|
||||
setNextStationData(getStationDataFromName(station));
|
||||
break;
|
||||
}
|
||||
setNextStationData(getStationDataFromName(station));
|
||||
break;
|
||||
} else {
|
||||
additionalSkipCount++;
|
||||
continue;
|
||||
@@ -276,7 +312,7 @@ export const FixedTrain: FC<props> = ({ trainID }) => {
|
||||
}, [ToData]);
|
||||
const lineColor =
|
||||
station.length > 0
|
||||
? lineColorList[station[0]?.StationNumber.slice(0, 1)]
|
||||
? lineColorList[station[0]?.StationNumber?.slice(0, 1)]
|
||||
: "black";
|
||||
//const lineColor = "red";
|
||||
const customTrainType = getTrainType({
|
||||
@@ -288,6 +324,178 @@ export const FixedTrain: FC<props> = ({ trainID }) => {
|
||||
? ` ${parseInt(customData.train_id) - parseInt(customData.train_num_distance)}号`
|
||||
: ""
|
||||
}`;
|
||||
|
||||
// ── Station Progress for Live Notification ──
|
||||
// 着のみエントリを除外(終着駅は着を許可、発・通編・通発編は保持)
|
||||
const lastValidIdx = trainDataWidhThrough.reduce(
|
||||
(last: number, d: string, i: number) => (d ? i : last), -1
|
||||
);
|
||||
const filteredTrainData = trainDataWidhThrough.filter((d, idx) => {
|
||||
if (!d) return false;
|
||||
const [, se] = d.split(",");
|
||||
if (!se) return true;
|
||||
// 着を含み発を含まないエントリは終着駅のみ許可
|
||||
if (se.includes("着") && !se.includes("発")) {
|
||||
return idx === lastValidIdx;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
const stationStops = filteredTrainData
|
||||
.filter((d) => !d.split(",")[1]?.includes("通"))
|
||||
.map((d) => d.split(",")[0]);
|
||||
|
||||
// 全駅リスト(通過駅含む、停車/通過フラグ+乗換色付き)
|
||||
// 駅名→所属路線コードのマップ構築
|
||||
const stationToLineCodes: Record<string, string[]> = {};
|
||||
if (originalStationList) {
|
||||
Object.keys(lineListPair).forEach((lineCode: string) => {
|
||||
const lineName = lineListPair[lineCode];
|
||||
const stations = originalStationList[lineName];
|
||||
if (!stations) return;
|
||||
stations.forEach((s: StationProps) => {
|
||||
if (!stationToLineCodes[s.Station_JP]) stationToLineCodes[s.Station_JP] = [];
|
||||
if (!stationToLineCodes[s.Station_JP].includes(lineCode)) {
|
||||
stationToLineCodes[s.Station_JP].push(lineCode);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
// 現在走行中の路線コード
|
||||
const runningLineCode = station.length > 0
|
||||
? station[0]?.StationNumber?.slice(0, 1) || ""
|
||||
: "";
|
||||
|
||||
const allStations = filteredTrainData
|
||||
.map((d) => {
|
||||
const [name, se] = d.split(",");
|
||||
const isStop = !se?.includes("通");
|
||||
const lineCodes = stationToLineCodes[name] || [];
|
||||
// 乗換色: 走行路線以外の路線色
|
||||
const transferColors = lineCodes
|
||||
.filter((c) => c !== runningLineCode)
|
||||
.map((c) => lineColorList[c])
|
||||
.filter(Boolean);
|
||||
return {
|
||||
name,
|
||||
isStop,
|
||||
...(transferColors.length > 0 ? { transferColors } : {}),
|
||||
};
|
||||
});
|
||||
|
||||
// 全駅リスト中の現在地インデックス
|
||||
const currentStationIndex = (() => {
|
||||
const pos = train?.Pos || "";
|
||||
if (!pos) return 0;
|
||||
// Pos は "駅名" (駅にいる時) or "駅A~駅B" (走行中) の形式
|
||||
const posStations = pos.split("~").map((s: string) =>
|
||||
s.replace(/(下り)|(上り)|\(下り\)|\(上り\)/g, "").trim()
|
||||
);
|
||||
// 完全一致
|
||||
const firstIdx = allStations.findIndex((s) => s.name === posStations[0]);
|
||||
if (firstIdx >= 0) return firstIdx;
|
||||
// 部分一致フォールバック
|
||||
const partialIdx = allStations.findIndex((s) =>
|
||||
posStations[0].includes(s.name) || s.name.includes(posStations[0])
|
||||
);
|
||||
if (partialIdx >= 0) return partialIdx;
|
||||
return 0;
|
||||
})();
|
||||
|
||||
const nextStationIndex = (() => {
|
||||
const name = nextStationData[0]?.Station_JP;
|
||||
if (!name) return -1;
|
||||
const idx = stationStops.indexOf(name);
|
||||
if (idx >= 0) return idx;
|
||||
// 部分一致フォールバック
|
||||
return stationStops.findIndex((s) => s === name || name.includes(s) || s.includes(name));
|
||||
})();
|
||||
|
||||
// ── Live Notification ──
|
||||
useEffect(() => {
|
||||
liveNotifyIdRef.current = liveNotifyId;
|
||||
}, [liveNotifyId]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (liveNotifyIdRef.current) {
|
||||
endTrainFollowActivity(liveNotifyIdRef.current).catch(() => {});
|
||||
setLiveNotificationActive(false);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!liveNotifyId || !train) return;
|
||||
const delayNum = train.delay === "入線" ? 0 : parseInt(train.delay) || 0;
|
||||
const delayStatus = delayNum > 0 ? `${delayNum}分遅れ` : "定刻";
|
||||
const positionStatus =
|
||||
nextStationData[0]?.Station_JP === train.Pos ? "ただいま" : "次は";
|
||||
updateTrainFollowActivity(liveNotifyId, {
|
||||
currentStation: train.Pos || "",
|
||||
nextStation: nextStationData[0]?.Station_JP || "",
|
||||
delayMinutes: delayNum,
|
||||
scheduledArrival: "",
|
||||
trainNumber: trainID,
|
||||
trainType: customTrainType.shortName,
|
||||
trainName: trainNameText,
|
||||
trainTypeColor: customTrainType.color,
|
||||
lineColor,
|
||||
destination: ToData,
|
||||
positionStatus,
|
||||
delayStatus,
|
||||
stationStops,
|
||||
nextStationIndex: nextStationIndex >= 0 ? nextStationIndex : undefined,
|
||||
allStations,
|
||||
currentStationIndex,
|
||||
}).catch(() => {});
|
||||
}, [train, nextStationData, liveNotifyId, stationStops, nextStationIndex, currentStationIndex]);
|
||||
|
||||
// バナー表示と同時にLive Activityを自動開始
|
||||
useEffect(() => {
|
||||
if (!train || hasStartedRef.current || liveNotifyId) return;
|
||||
if (!isLiveActivityAvailable()) return;
|
||||
hasStartedRef.current = true;
|
||||
const startActivity = async () => {
|
||||
if (Platform.OS === 'android' && Platform.Version >= 33) {
|
||||
const granted = await PermissionsAndroid.request(
|
||||
PermissionsAndroid.PERMISSIONS.POST_NOTIFICATIONS
|
||||
);
|
||||
if (granted !== PermissionsAndroid.RESULTS.GRANTED) return;
|
||||
}
|
||||
const delayNum = train?.delay === "入線" ? 0 : parseInt(train?.delay) || 0;
|
||||
const delayStatus = delayNum > 0 ? `${delayNum}分遅れ` : "定刻";
|
||||
const positionStatus =
|
||||
nextStationData[0]?.Station_JP === train?.Pos ? "ただいま" : "次は";
|
||||
try {
|
||||
const id = await startTrainFollowActivity({
|
||||
trainNumber: trainID,
|
||||
lineName: "",
|
||||
destination: ToData,
|
||||
currentStation: train?.Pos || "",
|
||||
nextStation: nextStationData[0]?.Station_JP || "",
|
||||
delayMinutes: delayNum,
|
||||
scheduledArrival: "",
|
||||
trainType: customTrainType.shortName,
|
||||
trainName: trainNameText,
|
||||
trainTypeColor: customTrainType.color,
|
||||
lineColor,
|
||||
positionStatus,
|
||||
delayStatus,
|
||||
stationStops,
|
||||
nextStationIndex: nextStationIndex >= 0 ? nextStationIndex : undefined,
|
||||
allStations,
|
||||
currentStationIndex,
|
||||
});
|
||||
setLiveNotifyId(id);
|
||||
setLiveNotificationActive(true);
|
||||
} catch (e) {
|
||||
console.warn('[LiveNotify] start error:', e);
|
||||
}
|
||||
};
|
||||
startActivity();
|
||||
}, [train]);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{ display: "flex", flexDirection: "column", flex: 1 }}
|
||||
@@ -306,7 +514,7 @@ export const FixedTrain: FC<props> = ({ trainID }) => {
|
||||
style={{
|
||||
flexDirection: fixedPositionSize === 226 ? "row" : "column",
|
||||
flex: 1,
|
||||
backgroundColor: "white",
|
||||
backgroundColor: colors.background,
|
||||
height: fixedPositionSize === 226 ? 200 : 50,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
@@ -352,7 +560,7 @@ export const FixedTrain: FC<props> = ({ trainID }) => {
|
||||
? "bold"
|
||||
: undefined,
|
||||
marginTop: customTrainType.fontAvailable ? 3 : 0,
|
||||
color: "white",
|
||||
color: fixed.textOnPrimary,
|
||||
textAlignVertical: "center",
|
||||
textAlign: "left",
|
||||
}}
|
||||
@@ -363,7 +571,7 @@ export const FixedTrain: FC<props> = ({ trainID }) => {
|
||||
<Text
|
||||
style={{
|
||||
fontSize: trainNameText.length > 4 ? 8 : 14,
|
||||
color: "white",
|
||||
color: fixed.textOnPrimary,
|
||||
maxWidth: fixedPositionSize === 226 ? 200 : 60,
|
||||
textAlignVertical: "center",
|
||||
}}
|
||||
@@ -414,7 +622,7 @@ export const FixedTrain: FC<props> = ({ trainID }) => {
|
||||
<Text
|
||||
style={{
|
||||
fontSize: customData?.to_data?.length > 4 ? 9 : 12,
|
||||
color: "white",
|
||||
color: fixed.textOnPrimary,
|
||||
fontWeight: "bold",
|
||||
textAlignVertical: "center",
|
||||
margin: 0,
|
||||
@@ -430,11 +638,11 @@ export const FixedTrain: FC<props> = ({ trainID }) => {
|
||||
{fixedPositionSize === 226 && (
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: "white",
|
||||
backgroundColor: colors.background,
|
||||
width: 10,
|
||||
borderLeftColor: "black",
|
||||
borderTopColor: lineColor,
|
||||
borderBottomColor: "white",
|
||||
borderBottomColor: colors.background,
|
||||
borderRightColor: "black",
|
||||
borderTopWidth: 50,
|
||||
borderBottomWidth: 0,
|
||||
@@ -499,12 +707,12 @@ export const FixedTrain: FC<props> = ({ trainID }) => {
|
||||
{fixedPositionSize !== 226 && (
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: "white",
|
||||
backgroundColor: colors.background,
|
||||
width: 10,
|
||||
borderLeftColor: "black",
|
||||
borderTopColor: "black",
|
||||
borderBottomColor: "white",
|
||||
borderRightColor: "white",
|
||||
borderBottomColor: colors.background,
|
||||
borderRightColor: colors.background,
|
||||
borderTopWidth: 21,
|
||||
borderBottomWidth: 0,
|
||||
borderLeftWidth: 0,
|
||||
@@ -548,17 +756,17 @@ export const FixedTrain: FC<props> = ({ trainID }) => {
|
||||
height: 26,
|
||||
}}
|
||||
>
|
||||
<Ionicons name="lock-closed" size={15} color="white" />
|
||||
<Ionicons name="lock-closed" size={15} color={fixed.textOnPrimary} />
|
||||
<Text
|
||||
style={{
|
||||
color: "white",
|
||||
color: fixed.textOnPrimary,
|
||||
fontSize: 15,
|
||||
paddingRight: 5,
|
||||
}}
|
||||
>
|
||||
列車追跡中
|
||||
</Text>
|
||||
<Ionicons name="close" size={15} color="white" />
|
||||
<Ionicons name="close" size={15} color={fixed.textOnPrimary} />
|
||||
</View>
|
||||
|
||||
<View
|
||||
@@ -646,6 +854,7 @@ const CurrentPositionBox = ({
|
||||
trainDataWithThrough,
|
||||
isSmall,
|
||||
}) => {
|
||||
const { colors } = useThemeColors();
|
||||
let firstText = "";
|
||||
let secondText = "";
|
||||
let marginText = "";
|
||||
@@ -666,7 +875,7 @@ const CurrentPositionBox = ({
|
||||
<View
|
||||
style={{
|
||||
flex: isSmall ? 1 : 3,
|
||||
backgroundColor: "white",
|
||||
backgroundColor: colors.background,
|
||||
flexDirection: "row",
|
||||
}}
|
||||
>
|
||||
@@ -674,12 +883,12 @@ const CurrentPositionBox = ({
|
||||
<View style={{ flexDirection: "column" }}>
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: "white",
|
||||
backgroundColor: colors.background,
|
||||
width: 10,
|
||||
borderLeftColor: lineColor,
|
||||
borderTopColor: lineColor,
|
||||
borderBottomColor: "white",
|
||||
borderRightColor: "white",
|
||||
borderBottomColor: colors.background,
|
||||
borderRightColor: colors.background,
|
||||
borderTopWidth: 28,
|
||||
borderBottomWidth: 0,
|
||||
borderLeftWidth: 0,
|
||||
@@ -688,12 +897,12 @@ const CurrentPositionBox = ({
|
||||
></View>
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: "white",
|
||||
backgroundColor: colors.background,
|
||||
width: 10,
|
||||
borderLeftColor: "white",
|
||||
borderTopColor: "white",
|
||||
borderBottomColor: "white",
|
||||
borderRightColor: "white",
|
||||
borderLeftColor: colors.background,
|
||||
borderTopColor: colors.background,
|
||||
borderBottomColor: colors.background,
|
||||
borderRightColor: colors.background,
|
||||
borderTopWidth: 18,
|
||||
borderBottomWidth: 0,
|
||||
borderLeftWidth: 0,
|
||||
@@ -708,16 +917,47 @@ const CurrentPositionBox = ({
|
||||
overScrollMode="always"
|
||||
>
|
||||
{trainDataWithThrough.length > 0 &&
|
||||
trainDataWithThrough.map((d, index) => (
|
||||
<EachStopData
|
||||
d={d}
|
||||
index={index}
|
||||
key={d+"FixedTrainBoxEachStopData"}
|
||||
delayTime={delayTime}
|
||||
isSmall={isSmall}
|
||||
secondText={secondText}
|
||||
/>
|
||||
))}
|
||||
(() => {
|
||||
// 着→発ペアを同一駅で統合(EachStopListと同様)
|
||||
const merged: { d: string; arrivalTime: string | null }[] = [];
|
||||
for (let i = 0; i < trainDataWithThrough.length; i++) {
|
||||
const d = trainDataWithThrough[i];
|
||||
if (!d) continue;
|
||||
const [st, se] = d.split(",");
|
||||
if (se?.includes("着") && !se?.includes("発")) {
|
||||
const next = trainDataWithThrough[i + 1];
|
||||
if (next) {
|
||||
const [nextSt, nextSe] = next.split(",");
|
||||
if (nextSt === st && nextSe?.includes("発")) {
|
||||
// この着エントリは次の発エントリで統合するためスキップ
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (se?.includes("発") && i > 0) {
|
||||
const prev = trainDataWithThrough[i - 1];
|
||||
if (prev) {
|
||||
const [prevSt, prevSe, prevTime] = prev.split(",");
|
||||
if (prevSt === st && prevSe?.includes("着")) {
|
||||
merged.push({ d, arrivalTime: prevTime });
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
merged.push({ d, arrivalTime: null });
|
||||
}
|
||||
return merged.map(({ d, arrivalTime }, index) => (
|
||||
<EachStopData
|
||||
d={d}
|
||||
index={index}
|
||||
key={d + "FixedTrainBoxEachStopData"}
|
||||
delayTime={delayTime}
|
||||
isSmall={isSmall}
|
||||
secondText={secondText}
|
||||
arrivalTime={arrivalTime}
|
||||
/>
|
||||
));
|
||||
})()}
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
@@ -729,27 +969,28 @@ type eachStopType = {
|
||||
isSmall: boolean;
|
||||
index: number;
|
||||
secondText: string;
|
||||
arrivalTime?: string | null;
|
||||
};
|
||||
|
||||
const EachStopData: FC<eachStopType> = (props) => {
|
||||
const { d, delayTime, isSmall, index, secondText } = props;
|
||||
const { colors } = useThemeColors();
|
||||
const { d, delayTime, isSmall, index, secondText, arrivalTime } = props;
|
||||
if (!d) return null;
|
||||
if (d == "") return null;
|
||||
const [station, se, time] = d.split(",");
|
||||
let distanceMinute = 0;
|
||||
if (time != "") {
|
||||
const calcMinute = (t: string) => {
|
||||
if (!t || t === "") return null;
|
||||
const now = dayjs();
|
||||
const hour = parseInt(time.split(":")[0]);
|
||||
const distanceTime = now
|
||||
const hour = parseInt(t.split(":")[0]);
|
||||
const dt = now
|
||||
.hour(hour < 4 ? hour + 24 : hour)
|
||||
.minute(parseInt(time.split(":")[1]));
|
||||
distanceMinute = distanceTime.diff(now, "minute") + delayTime;
|
||||
if (now.hour() < 4) {
|
||||
if (hour < 4) {
|
||||
distanceMinute = distanceMinute - 1440;
|
||||
}
|
||||
}
|
||||
}
|
||||
.minute(parseInt(t.split(":")[1]));
|
||||
let diff = dt.diff(now, "minute") + delayTime;
|
||||
if (now.hour() < 4 && hour < 4) diff -= 1440;
|
||||
return diff;
|
||||
};
|
||||
const distanceMinute = calcMinute(time) ?? 0;
|
||||
const arrivalMinute = arrivalTime ? calcMinute(arrivalTime) : null;
|
||||
return (
|
||||
<>
|
||||
<View
|
||||
@@ -784,13 +1025,25 @@ const EachStopData: FC<eachStopType> = (props) => {
|
||||
);
|
||||
})}
|
||||
<View style={{ flex: 1 }} />
|
||||
{!isSmall && arrivalMinute != null && (
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 9,
|
||||
color: colors.text,
|
||||
backgroundColor: colors.background,
|
||||
fontWeight: "bold",
|
||||
}}
|
||||
>
|
||||
{arrivalMinute}
|
||||
</Text>
|
||||
)}
|
||||
{isSmall ||
|
||||
(time != "" && (
|
||||
<Text
|
||||
style={{
|
||||
fontSize: isSmall ? 8 : 12,
|
||||
color: "black",
|
||||
backgroundColor: "white",
|
||||
color: colors.text,
|
||||
backgroundColor: colors.background,
|
||||
fontWeight: "bold",
|
||||
}}
|
||||
>
|
||||
@@ -835,7 +1088,7 @@ const EachStopData: FC<eachStopType> = (props) => {
|
||||
<Ionicons
|
||||
name="arrow-forward"
|
||||
size={isSmall ? 8 : 14}
|
||||
color="black"
|
||||
color={colors.icon}
|
||||
style={{ marginTop: isSmall ? 0 : 3 }}
|
||||
/>
|
||||
</View>
|
||||
|
||||
@@ -6,9 +6,11 @@ import {
|
||||
TextStyle,
|
||||
} from "react-native";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
export const LandscapeBackButton: FC<{
|
||||
onPress: () => void;
|
||||
}> = ({ onPress }) => {
|
||||
const { fixed } = useThemeColors();
|
||||
type stylesType = {
|
||||
touch: TouchableOpacityProps["style"];
|
||||
text: TextStyle;
|
||||
@@ -19,8 +21,8 @@ export const LandscapeBackButton: FC<{
|
||||
left: 10,
|
||||
width: 50,
|
||||
height: 50,
|
||||
backgroundColor: "#0099CC",
|
||||
borderColor: "white",
|
||||
backgroundColor: fixed.primary,
|
||||
borderColor: fixed.textOnPrimary,
|
||||
borderStyle: "solid",
|
||||
borderWidth: 1,
|
||||
borderRadius: 50,
|
||||
@@ -35,13 +37,13 @@ export const LandscapeBackButton: FC<{
|
||||
height: "auto",
|
||||
textAlignVertical: "center",
|
||||
fontWeight: "bold",
|
||||
color: "white",
|
||||
color: fixed.textOnPrimary,
|
||||
},
|
||||
};
|
||||
return (
|
||||
<TouchableOpacity onPress={onPress} style={styles.touch}>
|
||||
<View style={{ flex: 1 }} />
|
||||
<Ionicons name="arrow-back" color="white" size={30} />
|
||||
<Ionicons name="arrow-back" color={fixed.textOnPrimary} size={30} />
|
||||
<View style={{ flex: 1 }} />
|
||||
</TouchableOpacity>
|
||||
);
|
||||
|
||||
@@ -3,14 +3,13 @@ import {
|
||||
View,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
Platform,
|
||||
TouchableOpacityProps,
|
||||
TextStyle,
|
||||
} from "react-native";
|
||||
import Constants from "expo-constants";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
import { useTrainMenu } from "../../stateBox/useTrainMenu";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
|
||||
const top = Platform.OS == "ios" ? Constants.statusBarHeight : 0;
|
||||
type MapsButtonProps = {
|
||||
onPress: () => void;
|
||||
};
|
||||
@@ -20,7 +19,9 @@ type stylesType = {
|
||||
};
|
||||
|
||||
export const MapsButton: FC<MapsButtonProps> = ({ onPress }) => {
|
||||
const { fixed } = useThemeColors();
|
||||
const { mapSwitch } = useTrainMenu();
|
||||
const { top } = useSafeAreaInsets();
|
||||
const styles: stylesType = {
|
||||
touch: {
|
||||
position: "absolute",
|
||||
@@ -28,8 +29,8 @@ export const MapsButton: FC<MapsButtonProps> = ({ onPress }) => {
|
||||
left: 10,
|
||||
width: 50,
|
||||
height: 50,
|
||||
backgroundColor: "#0099CC",
|
||||
borderColor: "white",
|
||||
backgroundColor: fixed.primary,
|
||||
borderColor: fixed.textOnPrimary,
|
||||
borderStyle: "solid",
|
||||
borderWidth: 1,
|
||||
borderRadius: 50,
|
||||
@@ -45,7 +46,7 @@ export const MapsButton: FC<MapsButtonProps> = ({ onPress }) => {
|
||||
height: "auto",
|
||||
textAlignVertical: "center",
|
||||
fontWeight: "bold",
|
||||
color: "white",
|
||||
color: fixed.textOnPrimary,
|
||||
fontSize: 20,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
import React from "react";
|
||||
import { View, Text, TouchableOpacity, useWindowDimensions, Platform } from "react-native";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
import * as Updates from "expo-updates";
|
||||
import Constants from "expo-constants";
|
||||
import { useCurrentTrain } from "../../stateBox/useCurrentTrain";
|
||||
import { useTrainMenu } from "../../stateBox/useTrainMenu";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
|
||||
const top = Platform.OS == "ios" ? Constants.statusBarHeight : 0;
|
||||
export const NewMenu = () => {
|
||||
const { fixed } = useThemeColors();
|
||||
const { webview } = useCurrentTrain();
|
||||
const { width } = useWindowDimensions();
|
||||
const { LoadError } = useTrainMenu();
|
||||
const { top } = useSafeAreaInsets();
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
@@ -18,8 +21,8 @@ export const NewMenu = () => {
|
||||
top,
|
||||
width,
|
||||
height: 54,
|
||||
backgroundColor: "#0099CC",
|
||||
borderColor: "white",
|
||||
backgroundColor: fixed.primary,
|
||||
borderColor: fixed.textOnPrimary,
|
||||
borderStyle: "solid",
|
||||
borderWidth: 1,
|
||||
alignContent: "center",
|
||||
@@ -33,8 +36,8 @@ export const NewMenu = () => {
|
||||
style={{
|
||||
flex: 1,
|
||||
height: 54,
|
||||
backgroundColor: "#0099CC",
|
||||
borderColor: "white",
|
||||
backgroundColor: fixed.primary,
|
||||
borderColor: fixed.textOnPrimary,
|
||||
borderStyle: "solid",
|
||||
borderWidth: 1,
|
||||
borderRightWidth: 0,
|
||||
@@ -52,8 +55,8 @@ export const NewMenu = () => {
|
||||
style={{
|
||||
width: 54,
|
||||
height: 54,
|
||||
backgroundColor: "#0099CC",
|
||||
borderColor: "white",
|
||||
backgroundColor: fixed.primary,
|
||||
borderColor: fixed.textOnPrimary,
|
||||
borderStyle: "solid",
|
||||
borderWidth: 1,
|
||||
alignContent: "center",
|
||||
@@ -62,11 +65,11 @@ export const NewMenu = () => {
|
||||
}}
|
||||
>
|
||||
<View style={{ flex: 1 }} />
|
||||
<Ionicons name="menu" color="white" size={30} />
|
||||
<Ionicons name="menu" color={fixed.textOnPrimary} size={30} />
|
||||
<View style={{ flex: 1 }} />
|
||||
</View>
|
||||
<View style={{ flex: 1 }} />
|
||||
<Text style={{ color: "white", fontSize: 20 }}>メニュー</Text>
|
||||
<Text style={{ color: fixed.textOnPrimary, fontSize: 20 }}>メニュー</Text>
|
||||
<View style={{ flex: 1 }}></View>
|
||||
</>
|
||||
</TouchableOpacity>
|
||||
@@ -76,8 +79,8 @@ export const NewMenu = () => {
|
||||
style={{
|
||||
width: 54,
|
||||
height: 54,
|
||||
backgroundColor: LoadError ? "red" : "#0099CC",
|
||||
borderColor: "white",
|
||||
backgroundColor: LoadError ? "red" : fixed.primary,
|
||||
borderColor: fixed.textOnPrimary,
|
||||
borderStyle: "solid",
|
||||
borderWidth: 1,
|
||||
alignContent: "center",
|
||||
@@ -86,7 +89,7 @@ export const NewMenu = () => {
|
||||
}}
|
||||
>
|
||||
<View style={{ flex: 1 }} />
|
||||
<Ionicons name="reload" color="white" size={30} />
|
||||
<Ionicons name="reload" color={fixed.textOnPrimary} size={30} />
|
||||
<View style={{ flex: 1 }} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
@@ -2,14 +2,13 @@ import React, { FC } from "react";
|
||||
import {
|
||||
View,
|
||||
TouchableOpacity,
|
||||
Platform,
|
||||
TouchableOpacityProps,
|
||||
TextStyle,
|
||||
} from "react-native";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import Constants from "expo-constants";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
import { useTrainMenu } from "../../stateBox/useTrainMenu";
|
||||
const top = Platform.OS == "ios" ? Constants.statusBarHeight : 0;
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
|
||||
type stylesType = {
|
||||
touch: TouchableOpacityProps["style"];
|
||||
@@ -21,7 +20,9 @@ type ReloadButton = {
|
||||
|
||||
}
|
||||
export const ReloadButton:FC<ReloadButton> = ({ onPress, right }) => {
|
||||
const { fixed } = useThemeColors();
|
||||
const { mapSwitch, LoadError = false } = useTrainMenu();
|
||||
const { top } = useSafeAreaInsets();
|
||||
const styles: stylesType = {
|
||||
touch: {
|
||||
position: "absolute",
|
||||
@@ -29,8 +30,8 @@ export const ReloadButton:FC<ReloadButton> = ({ onPress, right }) => {
|
||||
right: 10 + right,
|
||||
width: 50,
|
||||
height: 50,
|
||||
backgroundColor: LoadError ? "red" : "#0099CC",
|
||||
borderColor: "white",
|
||||
backgroundColor: LoadError ? "red" : fixed.primary,
|
||||
borderColor: fixed.textOnPrimary,
|
||||
borderStyle: "solid",
|
||||
borderWidth: 1,
|
||||
borderRadius: 50,
|
||||
@@ -46,13 +47,13 @@ export const ReloadButton:FC<ReloadButton> = ({ onPress, right }) => {
|
||||
height: "auto",
|
||||
textAlignVertical: "center",
|
||||
fontWeight: "bold",
|
||||
color: "white",
|
||||
color: fixed.textOnPrimary,
|
||||
},
|
||||
};
|
||||
return (
|
||||
<TouchableOpacity onPress={onPress} style={styles.touch}>
|
||||
<View style={{ flex: 1 }} />
|
||||
<Ionicons name="reload" color="white" size={30} />
|
||||
<Ionicons name="reload" color={fixed.textOnPrimary} size={30} />
|
||||
<View style={{ flex: 1 }} />
|
||||
</TouchableOpacity>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from "react";
|
||||
import { Platform, LayoutAnimation } from "react-native";
|
||||
import { Platform, LayoutAnimation, useColorScheme } from "react-native";
|
||||
import { WebView } from "react-native-webview";
|
||||
|
||||
import {
|
||||
@@ -23,6 +23,8 @@ export const AppsWebView = ({ openStationACFromEachTrainInfo }) => {
|
||||
const { navigate } = useNavigation();
|
||||
const { favoriteStation } = useFavoriteStation();
|
||||
const { isLandscape } = useDeviceOrientationChange();
|
||||
const isDark = useColorScheme() === "dark";
|
||||
const bgColor = isDark ? "#1c1c1e" : "#ffffff";
|
||||
const { originalStationList, stationList, getInjectJavascriptAddress } =
|
||||
useStationList();
|
||||
const {
|
||||
@@ -158,6 +160,7 @@ export const AppsWebView = ({ openStationACFromEachTrainInfo }) => {
|
||||
javaScriptEnabled
|
||||
allowsBackForwardNavigationGestures
|
||||
setSupportMultipleWindows
|
||||
style={{ backgroundColor: bgColor }}
|
||||
{...{ onMessage, onNavigationStateChange, onLoadEnd }}
|
||||
injectedJavaScript={injectJavascript}
|
||||
/>
|
||||
|
||||
@@ -2,13 +2,15 @@ import React from "react";
|
||||
import { View, Text } from "react-native";
|
||||
import { useCurrentTrain } from "../stateBox/useCurrentTrain";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
import { BigButton } from "./atom/BigButton";
|
||||
export default function CurrentTrainListView() {
|
||||
const { fixed } = useThemeColors();
|
||||
const { goBack } = useNavigation();
|
||||
const { currentTrain } = useCurrentTrain();
|
||||
return (
|
||||
<View style={{ height: "100%", backgroundColor: "#0099CC" }}>
|
||||
{currentTrain && currentTrain.map((d) => <Text>{d.num}</Text>)}
|
||||
<View style={{ height: "100%", backgroundColor: fixed.primary }}>
|
||||
{currentTrain && currentTrain.map((d) => <Text key={d.num}>{d.num}</Text>)}
|
||||
<BigButton onPress={goBack} string="閉じる" />
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import {
|
||||
ScrollView,
|
||||
View,
|
||||
Animated,
|
||||
LayoutAnimation,
|
||||
ViewStyle,
|
||||
Platform,
|
||||
} from "react-native";
|
||||
import React, {
|
||||
useEffect,
|
||||
@@ -12,11 +10,12 @@ import React, {
|
||||
useState,
|
||||
useLayoutEffect,
|
||||
ReactNode,
|
||||
useRef,
|
||||
MutableRefObject,
|
||||
} from "react";
|
||||
import { NativeViewGestureHandler } from "react-native-gesture-handler";
|
||||
import { ScrollView } from "react-native-actions-sheet";
|
||||
import { AS } from "../storageControl";
|
||||
import { STORAGE_KEYS } from "@/constants";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
|
||||
type HeaderSize = "small" | "big" | "default";
|
||||
|
||||
@@ -28,7 +27,7 @@ type DynamicHeaderScrollViewProps = {
|
||||
topStickyContent?: ReactNode;
|
||||
styles?: { header: ViewStyle };
|
||||
from?: string;
|
||||
scrollHandlers?: any;
|
||||
scrollRef?: MutableRefObject<any>;
|
||||
};
|
||||
|
||||
export const DynamicHeaderScrollView: React.FC<DynamicHeaderScrollViewProps> = (
|
||||
@@ -42,8 +41,9 @@ export const DynamicHeaderScrollView: React.FC<DynamicHeaderScrollViewProps> = (
|
||||
topStickyContent,
|
||||
styles,
|
||||
from,
|
||||
scrollHandlers,
|
||||
scrollRef,
|
||||
} = props;
|
||||
const { fixed } = useThemeColors();
|
||||
const [headerSize, setHeaderSize] = useState("default");
|
||||
useLayoutEffect(() => {
|
||||
AS.getItem(STORAGE_KEYS.HEADER_SIZE)
|
||||
@@ -85,14 +85,14 @@ export const DynamicHeaderScrollView: React.FC<DynamicHeaderScrollViewProps> = (
|
||||
const shotHeaderStyle = {
|
||||
on: {
|
||||
height: Min_Header_Height,
|
||||
backgroundColor: "#0099CC",
|
||||
backgroundColor: fixed.primary,
|
||||
margin: 0,
|
||||
top: 0,
|
||||
opacity: 1,
|
||||
},
|
||||
off: {
|
||||
height: Max_Header_Height,
|
||||
backgroundColor: "#0099CC",
|
||||
backgroundColor: fixed.primary,
|
||||
margin: 0,
|
||||
top: 0,
|
||||
opacity: 0,
|
||||
@@ -102,14 +102,14 @@ export const DynamicHeaderScrollView: React.FC<DynamicHeaderScrollViewProps> = (
|
||||
const longHeaderStyle = {
|
||||
on: {
|
||||
height: Max_Header_Height,
|
||||
backgroundColor: "#0099CC",
|
||||
backgroundColor: fixed.primary,
|
||||
margin: 0,
|
||||
top: 0,
|
||||
opacity: 1,
|
||||
},
|
||||
off: {
|
||||
height: 0,
|
||||
backgroundColor: "#0099CC",
|
||||
backgroundColor: fixed.primary,
|
||||
margin: 0,
|
||||
top: 0,
|
||||
opacity: 0,
|
||||
@@ -135,7 +135,6 @@ export const DynamicHeaderScrollView: React.FC<DynamicHeaderScrollViewProps> = (
|
||||
const [headerVisible, setHeaderVisible] = useState(false);
|
||||
|
||||
const onScroll = (event) => {
|
||||
scrollHandlers.onScroll(event);
|
||||
switch (headerSize) {
|
||||
case "big":
|
||||
setHeaderVisible(false);
|
||||
@@ -179,31 +178,27 @@ export const DynamicHeaderScrollView: React.FC<DynamicHeaderScrollViewProps> = (
|
||||
{topStickyContent}
|
||||
</Animated.View>
|
||||
</View>
|
||||
<NativeViewGestureHandler
|
||||
simultaneousHandlers={scrollHandlers.simultaneousHandlers}
|
||||
<ScrollView
|
||||
ref={scrollRef}
|
||||
nestedScrollEnabled
|
||||
bounces={false}
|
||||
style={{ zIndex: 0 }}
|
||||
stickyHeaderIndices={[1]}
|
||||
scrollEventThrottle={1}
|
||||
onScroll={onScroll}
|
||||
>
|
||||
<ScrollView
|
||||
nestedScrollEnabled
|
||||
ref={scrollHandlers.ref}
|
||||
onLayout={scrollHandlers.onLayout}
|
||||
scrollEventThrottle={scrollHandlers.scrollEventThrottle}
|
||||
style={{ zIndex: 0 }}
|
||||
stickyHeaderIndices={[1]}
|
||||
onScroll={onScroll}
|
||||
>
|
||||
<View style={{ height: Scroll_Distance, flexDirection: "column" }} />
|
||||
{topStickyContent && (
|
||||
<View
|
||||
style={{
|
||||
paddingTop: Min_Header_Height + 40,
|
||||
flexDirection: "column",
|
||||
}}
|
||||
//index={1}
|
||||
/>
|
||||
)}
|
||||
{children}
|
||||
</ScrollView>
|
||||
</NativeViewGestureHandler>
|
||||
<View style={{ height: Scroll_Distance, flexDirection: "column" }} />
|
||||
{topStickyContent && (
|
||||
<View
|
||||
style={{
|
||||
paddingTop: Min_Header_Height + 40,
|
||||
flexDirection: "column",
|
||||
}}
|
||||
//index={1}
|
||||
/>
|
||||
)}
|
||||
{children}
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -8,8 +8,10 @@ import { useNavigation } from "@react-navigation/native";
|
||||
import { useTrainMenu } from "../stateBox/useTrainMenu";
|
||||
import { FavoriteListItem } from "./atom/FavoriteListItem";
|
||||
import { BigButton } from "./atom/BigButton";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
import { useStationList } from "@/stateBox/useStationList";
|
||||
export const FavoriteList: FC = () => {
|
||||
const { colors, fixed } = useThemeColors();
|
||||
const { favoriteStation } = useFavoriteStation();
|
||||
const { webview } = useCurrentTrain();
|
||||
const { navigate, addListener, goBack, canGoBack } = useNavigation();
|
||||
@@ -26,19 +28,19 @@ export const FavoriteList: FC = () => {
|
||||
goBack();
|
||||
};
|
||||
return (
|
||||
<View style={{ height: "100%", backgroundColor: "#0099CC" }}>
|
||||
<View style={{ height: "100%", backgroundColor: fixed.primary }}>
|
||||
<Text
|
||||
style={{
|
||||
textAlign: "center",
|
||||
fontSize: 20,
|
||||
color: "white",
|
||||
color: fixed.textOnPrimary,
|
||||
fontWeight: "bold",
|
||||
paddingVertical: 10,
|
||||
}}
|
||||
>
|
||||
位置情報クイック移動メニュー
|
||||
</Text>
|
||||
<ScrollView style={{ height: "100%", backgroundColor: "white" }}>
|
||||
<ScrollView style={{ height: "100%", backgroundColor: colors.background }}>
|
||||
{favoriteStation
|
||||
.map((currentStation) => {
|
||||
return (
|
||||
@@ -65,7 +67,7 @@ export const FavoriteList: FC = () => {
|
||||
}}
|
||||
>
|
||||
<View style={{ flex: 1 }} />
|
||||
<Text style={{ fontSize: 20 }}>移動する</Text>
|
||||
<Text style={{ fontSize: 20, color: colors.text }}>移動する</Text>
|
||||
<Icon name="chevron-right" size={20} />
|
||||
</View>
|
||||
</FavoriteListItem>
|
||||
@@ -74,7 +76,7 @@ export const FavoriteList: FC = () => {
|
||||
</ScrollView>
|
||||
<Text
|
||||
style={{
|
||||
backgroundColor: "white",
|
||||
backgroundColor: colors.background,
|
||||
borderWidth: 1,
|
||||
borderStyle: "solid",
|
||||
}}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import Sign from "@/components/駅名表/Sign";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { SpotSign } from "@/components/観光スポット看板/SpotSign";
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { AS } from "@/storageControl";
|
||||
import {
|
||||
useWindowDimensions,
|
||||
@@ -13,6 +14,19 @@ import Carousel, { ICarouselInstance } from "react-native-reanimated-carousel";
|
||||
import { SheetManager } from "react-native-actions-sheet";
|
||||
import { StationNumber } from "../StationPagination";
|
||||
import { SimpleDot } from "../SimpleDot";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
import Sortable from "react-native-sortables";
|
||||
import Animated, {
|
||||
FadeIn,
|
||||
FadeOut,
|
||||
runOnJS,
|
||||
useAnimatedStyle,
|
||||
useSharedValue,
|
||||
withTiming,
|
||||
} from "react-native-reanimated";
|
||||
import { useSortMode } from "./useSortMode";
|
||||
import { StationSource } from "@/types";
|
||||
|
||||
export const CarouselBox = ({
|
||||
originalStationList,
|
||||
listUpStation,
|
||||
@@ -20,23 +34,127 @@ export const CarouselBox = ({
|
||||
setListIndex,
|
||||
listIndex,
|
||||
navigate,
|
||||
stationListMode,
|
||||
isSearchMode
|
||||
stationSource,
|
||||
}: {
|
||||
originalStationList: any;
|
||||
listUpStation: any[][];
|
||||
nearPositionStation: any[][];
|
||||
setListIndex: (i: number) => void;
|
||||
listIndex: number;
|
||||
navigate: any;
|
||||
stationSource: StationSource;
|
||||
}) => {
|
||||
const carouselRef = useRef<ICarouselInstance>(null);
|
||||
const { height, width } = useWindowDimensions();
|
||||
const { width } = useWindowDimensions();
|
||||
const { colors, fixed } = useThemeColors();
|
||||
const [dotButton, setDotButton] = useState(false);
|
||||
const carouselBadgeScrollViewRef = useRef<ScrollView>(null);
|
||||
// listIndex が -1 になってもカルーセルが表示中は直前の値を維持する
|
||||
const lastValidListIndexRef = useRef(0);
|
||||
if (listIndex >= 0) lastValidListIndexRef.current = listIndex;
|
||||
|
||||
// グリッド定数(ソートモードと座標計算で共用)
|
||||
const origW = width * 0.8;
|
||||
const origH = (origW / 20) * 9;
|
||||
const cols = 3;
|
||||
const gridPad = 8;
|
||||
const gridGap = 8;
|
||||
const cellW = (width - gridPad * 2 - gridGap * (cols - 1)) / cols;
|
||||
const cellH = (cellW / origW) * origH;
|
||||
const carouselHeight = origH + 10;
|
||||
const rows = Math.ceil(listUpStation.length / cols);
|
||||
const gridHeight = rows * cellH + Math.max(0, rows - 1) * gridGap + gridPad * 2;
|
||||
|
||||
const {
|
||||
uiMode,
|
||||
startSortMode,
|
||||
exitSortMode,
|
||||
sortGridRenderItem,
|
||||
onSortDragEnd,
|
||||
} = useSortMode({
|
||||
listUpStation,
|
||||
setListIndex,
|
||||
width,
|
||||
origW,
|
||||
origH,
|
||||
cols,
|
||||
gridPad,
|
||||
gridGap,
|
||||
cellW,
|
||||
cellH,
|
||||
carouselHeight,
|
||||
stationSource,
|
||||
});
|
||||
|
||||
// ソートモード中かどうか
|
||||
const isSortMode = uiMode !== "carousel";
|
||||
|
||||
// コンテナ高さ(カルーセル ↔ グリッドで可変)
|
||||
const containerHeight = useSharedValue(carouselHeight);
|
||||
const containerHeightStyle = useAnimatedStyle(() => ({ height: containerHeight.value }));
|
||||
|
||||
// ドットエリアのフェード
|
||||
const dotsOpacity = useSharedValue(1);
|
||||
const dotsAnimStyle = useAnimatedStyle(() => ({ opacity: dotsOpacity.value }));
|
||||
|
||||
// カルーセル ↔ グリッドのフェード
|
||||
const [isGridMounted, setIsGridMounted] = useState(false);
|
||||
const carouselOpacity = useSharedValue(1);
|
||||
const gridOpacity = useSharedValue(0);
|
||||
const carouselAnimStyle = useAnimatedStyle(() => ({ opacity: carouselOpacity.value }));
|
||||
const gridAnimStyle = useAnimatedStyle(() => ({ opacity: gridOpacity.value }));
|
||||
|
||||
useEffect(() => {
|
||||
const duration = 250;
|
||||
if (isSortMode) {
|
||||
setIsGridMounted(true); // フェードイン前にマウント
|
||||
dotsOpacity.value = withTiming(0, { duration });
|
||||
carouselOpacity.value = withTiming(0, { duration });
|
||||
gridOpacity.value = withTiming(1, { duration });
|
||||
containerHeight.value = withTiming(gridHeight, { duration });
|
||||
} else {
|
||||
dotsOpacity.value = withTiming(1, { duration });
|
||||
carouselOpacity.value = withTiming(1, { duration });
|
||||
containerHeight.value = withTiming(carouselHeight, { duration });
|
||||
gridOpacity.value = withTiming(0, { duration }, (finished) => {
|
||||
if (finished) runOnJS(setIsGridMounted)(false); // フェードアウト完了後にアンマウント
|
||||
});
|
||||
}
|
||||
}, [isSortMode]);
|
||||
|
||||
// ソートモード終了直後フラグ(次の listIndex 変更でアニメーションをスキップ)
|
||||
const justExitedSortRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isSortMode) {
|
||||
justExitedSortRef.current = true;
|
||||
}
|
||||
}, [isSortMode]);
|
||||
|
||||
// バッジからのインデックス変更をカルーセルに反映
|
||||
useEffect(() => {
|
||||
if (listIndex >= 0 && carouselRef.current) {
|
||||
const animated = !justExitedSortRef.current;
|
||||
justExitedSortRef.current = false;
|
||||
carouselRef.current.scrollTo({ index: listIndex, animated });
|
||||
}
|
||||
}, [listIndex]);
|
||||
|
||||
// ドットのスクロール追従
|
||||
useEffect(() => {
|
||||
if (!carouselBadgeScrollViewRef.current) return;
|
||||
const dotSize = dotButton ? 28 : 24;
|
||||
const scrollToIndex = dotSize * listIndex - width / 2 + dotSize - 5;
|
||||
carouselBadgeScrollViewRef.current.scrollTo({
|
||||
x: scrollToIndex,
|
||||
animated: true,
|
||||
carouselBadgeScrollViewRef.current.scrollTo({ x: scrollToIndex, animated: true });
|
||||
}, [listIndex, dotButton, width]);
|
||||
|
||||
// ドット表示設定の読み込み
|
||||
useEffect(() => {
|
||||
AS.getItem("CarouselSettings/activeDotSettings").then((data) => {
|
||||
setDotButton(data === "true");
|
||||
});
|
||||
}, [listIndex, dotButton, width, carouselBadgeScrollViewRef]);
|
||||
}, []);
|
||||
|
||||
const oPSign = () => {
|
||||
const payload = {
|
||||
currentStation: listUpStation[listIndex],
|
||||
@@ -49,37 +167,38 @@ export const CarouselBox = ({
|
||||
//@ts-ignore
|
||||
SheetManager.show("StationDetailView", { payload });
|
||||
};
|
||||
|
||||
const oLPSign = () => {
|
||||
// 駅がある場合はどのモードでもグリッドビューに切り替える
|
||||
if (
|
||||
listUpStation.length > 0 &&
|
||||
listUpStation[0][0].StationNumber !== "null"
|
||||
) {
|
||||
startSortMode(listIndex);
|
||||
return;
|
||||
}
|
||||
// 駅なし:長押しでドット表示切り替え
|
||||
LayoutAnimation.configureNext({
|
||||
duration: 600,
|
||||
update: { type: "spring", springDamping: 0.5 },
|
||||
});
|
||||
AS.setItem(
|
||||
"CarouselSettings/activeDotSettings",
|
||||
!dotButton ? "true" : "false"
|
||||
);
|
||||
AS.setItem("CarouselSettings/activeDotSettings", !dotButton ? "true" : "false");
|
||||
setDotButton(!dotButton);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
AS.getItem("CarouselSettings/activeDotSettings").then((data) => {
|
||||
setDotButton(data === "true");
|
||||
});
|
||||
}, []);
|
||||
const RenderItem = ({ item, index }) => {
|
||||
return (
|
||||
const RenderItem = useCallback(
|
||||
({ item }) => (
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: "#0000",
|
||||
width,
|
||||
flexDirection: "row",
|
||||
marginLeft: 0,
|
||||
marginRight: 0,
|
||||
}}
|
||||
key={item[0].StationNumber}
|
||||
style={{ backgroundColor: "#0000", width, flexDirection: "row" }}
|
||||
key={item[0].StationNumber ?? item[0].Station_JP}
|
||||
>
|
||||
<View style={{ flex: 1 }} />
|
||||
{item[0].StationNumber != "null" ? (
|
||||
{item[0].isSpot ? (
|
||||
<SpotSign
|
||||
item={item}
|
||||
isCurrentStation={item == nearPositionStation}
|
||||
/>
|
||||
) : item[0].StationNumber != "null" ? (
|
||||
<Sign
|
||||
stationID={item[0].StationNumber}
|
||||
isCurrentStation={item == nearPositionStation}
|
||||
@@ -91,20 +210,18 @@ export const CarouselBox = ({
|
||||
style={{
|
||||
width: width * 0.8,
|
||||
height: ((width * 0.8) / 20) * 9,
|
||||
borderColor: "#0099CC",
|
||||
borderColor: fixed.primary,
|
||||
borderWidth: 1,
|
||||
backgroundColor: "white",
|
||||
backgroundColor: colors.background,
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<Text style={{ color: "#0099CC", fontSize: 20 }}>
|
||||
{!!isSearchMode ? "路線検索モードです。入力欄に駅名やナンバリングを入力したり、上に並んでいる路線を選んでみましょう!" :stationListMode == "position"
|
||||
<View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
|
||||
<Text style={{ color: colors.textAccent, fontSize: 20 }}>
|
||||
{stationSource.type === "search"
|
||||
? (stationSource.query || stationSource.lineId)
|
||||
? "該当する駅が見つかりませんでした。"
|
||||
: "駅名・ナンバリングを入力するか、路線を選んでください。"
|
||||
: stationSource.type === "position"
|
||||
? "現在地の近くに駅がありません。"
|
||||
: "お気に入りリストがありません。お気に入りの駅を追加しよう!"}
|
||||
</Text>
|
||||
@@ -113,74 +230,141 @@ export const CarouselBox = ({
|
||||
)}
|
||||
<View style={{ flex: 1 }} />
|
||||
</View>
|
||||
);
|
||||
};
|
||||
),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[width, nearPositionStation, oPSign, oLPSign, stationSource]
|
||||
);
|
||||
|
||||
return (
|
||||
<View style={{ flex: 1, paddingTop: 10 }}>
|
||||
<Carousel
|
||||
ref={carouselRef}
|
||||
data={
|
||||
listUpStation.length > 0
|
||||
? listUpStation
|
||||
: [[{ StationNumber: "null" }]]
|
||||
}
|
||||
height={(((width / 100) * 80) / 20) * 9 + 10}
|
||||
pagingEnabled={true}
|
||||
snapEnabled={true}
|
||||
loop={false}
|
||||
width={width}
|
||||
style={{ width: width, alignContent: "center" }}
|
||||
mode="parallax"
|
||||
modeConfig={{
|
||||
parallaxScrollingScale: 1,
|
||||
parallaxScrollingOffset: 100,
|
||||
parallaxAdjacentItemScale: 0.8,
|
||||
}}
|
||||
scrollAnimationDuration={600}
|
||||
onSnapToItem={setListIndex}
|
||||
renderItem={RenderItem}
|
||||
overscrollEnabled={false}
|
||||
defaultIndex={listIndex >= listUpStation.length ? 0 : listIndex}
|
||||
/>
|
||||
<ScrollView
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
contentContainerStyle={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "center",
|
||||
alignContent: "center",
|
||||
alignItems: "center",
|
||||
paddingVertical: 2,
|
||||
paddingHorizontal: 10,
|
||||
minWidth: width,
|
||||
}}
|
||||
ref={(scrollViewRef) => {
|
||||
// ScrollViewのrefを保存
|
||||
if (scrollViewRef) {
|
||||
carouselBadgeScrollViewRef.current = scrollViewRef;
|
||||
}
|
||||
}}
|
||||
>
|
||||
{originalStationList &&
|
||||
listUpStation.map((d, index) => {
|
||||
const active = index == listIndex;
|
||||
const numberKey = d[0].StationNumber + index;
|
||||
return dotButton ? (
|
||||
<StationNumber
|
||||
onPress={() => setListIndex(index)}
|
||||
currentStation={d}
|
||||
active={active}
|
||||
key={numberKey}
|
||||
/>
|
||||
) : (
|
||||
<SimpleDot
|
||||
onPress={() => setListIndex(index)}
|
||||
active={active}
|
||||
key={numberKey}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</ScrollView>
|
||||
{/* カルーセル / グリッド(同じ高さ領域を共用・クロスフェード) */}
|
||||
<Animated.View style={[{ overflow: "visible" }, containerHeightStyle]}>
|
||||
{/* カルーセル */}
|
||||
<Animated.View
|
||||
style={[{ position: "absolute", width }, carouselAnimStyle]}
|
||||
pointerEvents={isSortMode ? "none" : "auto"}
|
||||
>
|
||||
<Carousel
|
||||
ref={carouselRef}
|
||||
data={listUpStation.length > 0 ? listUpStation : [[{ StationNumber: "null" }]]}
|
||||
height={carouselHeight}
|
||||
pagingEnabled={true}
|
||||
snapEnabled={true}
|
||||
loop={false}
|
||||
width={width}
|
||||
style={{ width, alignContent: "center" }}
|
||||
mode="parallax"
|
||||
modeConfig={{
|
||||
parallaxScrollingScale: 1,
|
||||
parallaxScrollingOffset: 100,
|
||||
parallaxAdjacentItemScale: 0.8,
|
||||
}}
|
||||
scrollAnimationDuration={600}
|
||||
onSnapToItem={setListIndex}
|
||||
renderItem={RenderItem}
|
||||
overscrollEnabled={false}
|
||||
defaultIndex={
|
||||
lastValidListIndexRef.current >= listUpStation.length
|
||||
? 0
|
||||
: lastValidListIndexRef.current
|
||||
}
|
||||
/>
|
||||
</Animated.View>
|
||||
|
||||
{/* グリッド:ソートモード中のみマウント */}
|
||||
{isGridMounted && (
|
||||
<Animated.View
|
||||
style={[
|
||||
{ position: "absolute", width, height: gridHeight, paddingHorizontal: gridPad, overflow: "visible" },
|
||||
gridAnimStyle,
|
||||
]}
|
||||
>
|
||||
<Sortable.Grid
|
||||
columns={cols}
|
||||
columnGap={gridGap}
|
||||
rowGap={gridGap}
|
||||
data={listUpStation}
|
||||
renderItem={sortGridRenderItem}
|
||||
keyExtractor={(item) => item[0].StationNumber ?? item[0].Station_JP}
|
||||
onDragEnd={onSortDragEnd}
|
||||
sortEnabled={stationSource.type === "favorite"}
|
||||
/>
|
||||
</Animated.View>
|
||||
)}
|
||||
</Animated.View>
|
||||
|
||||
{/* ドットエリア:ソートモード時はフェードアウト */}
|
||||
<Animated.View style={dotsAnimStyle} pointerEvents={isSortMode ? "none" : "auto"}>
|
||||
<ScrollView
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
contentContainerStyle={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "center",
|
||||
alignContent: "center",
|
||||
alignItems: "center",
|
||||
paddingVertical: 2,
|
||||
paddingHorizontal: 10,
|
||||
minWidth: width,
|
||||
}}
|
||||
ref={(scrollViewRef) => {
|
||||
if (scrollViewRef) {
|
||||
carouselBadgeScrollViewRef.current = scrollViewRef;
|
||||
}
|
||||
}}
|
||||
>
|
||||
{originalStationList &&
|
||||
listUpStation.map((d, index) => {
|
||||
const active = index == listIndex;
|
||||
const numberKey = d[0].StationNumber + index;
|
||||
return dotButton ? (
|
||||
<StationNumber
|
||||
onPress={() => setListIndex(index)}
|
||||
currentStation={d}
|
||||
active={active}
|
||||
key={numberKey}
|
||||
/>
|
||||
) : (
|
||||
<SimpleDot
|
||||
onPress={() => setListIndex(index)}
|
||||
active={active}
|
||||
key={numberKey}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</ScrollView>
|
||||
</Animated.View>
|
||||
|
||||
{/* 並び替えコントロール:ソートモード時に最下部からスライドイン */}
|
||||
{isSortMode && (
|
||||
<Animated.View
|
||||
entering={FadeIn.duration(200)}
|
||||
exiting={FadeOut.duration(150)}
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 16,
|
||||
paddingTop: 6,
|
||||
paddingBottom: 4,
|
||||
}}
|
||||
>
|
||||
<Text style={{ flex: 1, color: colors.textAccent, fontSize: 14 }}>
|
||||
{stationSource.type === "favorite" ? "長押しでドラッグして並び替え" : "タップして駅を選択"}
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
onPress={exitSortMode}
|
||||
disabled={uiMode === "sort-exiting"}
|
||||
style={{
|
||||
backgroundColor: uiMode === "sort-exiting" ? "#88c8e8" : fixed.primary,
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 6,
|
||||
borderRadius: 16,
|
||||
}}
|
||||
>
|
||||
<Text style={{ color: fixed.textOnPrimary, fontWeight: "bold" }}>完了</Text>
|
||||
</TouchableOpacity>
|
||||
</Animated.View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,28 +1,55 @@
|
||||
import { AS } from "@/storageControl";
|
||||
import React from "react";
|
||||
import React, { useRef } from "react";
|
||||
import {
|
||||
TouchableOpacity,
|
||||
Text,
|
||||
View,
|
||||
LayoutAnimation,
|
||||
KeyboardAvoidingView,
|
||||
Platform,
|
||||
} from "react-native";
|
||||
import Ionicons from "react-native-vector-icons/Ionicons";
|
||||
import { SearchUnitBox } from "@/components/Menu/RailScope/SearchUnitBox";
|
||||
import { StationSource } from "@/types";
|
||||
import { STORAGE_KEYS } from "@/constants";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
import { useCurrentTrain } from "@/stateBox/useCurrentTrain";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
|
||||
export const CarouselTypeChanger = ({
|
||||
locationStatus,
|
||||
position,
|
||||
stationListMode,
|
||||
setStationListMode,
|
||||
stationSource,
|
||||
setStationSource,
|
||||
closeSearch,
|
||||
setSelectedCurrentStation,
|
||||
mapMode,
|
||||
setMapMode,
|
||||
isSearchMode,
|
||||
setisSearchMode,
|
||||
input,
|
||||
setInput,
|
||||
}: {
|
||||
locationStatus: boolean | null;
|
||||
position: any;
|
||||
stationSource: StationSource;
|
||||
setStationSource: (s: StationSource) => void;
|
||||
closeSearch: () => void;
|
||||
setSelectedCurrentStation: (i: number) => void;
|
||||
mapMode: boolean;
|
||||
setMapMode: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}) => {
|
||||
const { fixedPosition, setFixedPosition } = useCurrentTrain();
|
||||
const { navigate } = useNavigation();
|
||||
const { colors, fixed } = useThemeColors();
|
||||
const isGpsFollowing = fixedPosition?.type === "nearestStation";
|
||||
const containerRef = useRef(null);
|
||||
|
||||
const handleGpsFollowLongPress = () => {
|
||||
returnToDefaultMode();
|
||||
if (isGpsFollowing) {
|
||||
setFixedPosition({ type: null, value: null });
|
||||
} else {
|
||||
navigate("positions", { screen: "Apps" } as any);
|
||||
setFixedPosition({ type: "nearestStation", value: null });
|
||||
}
|
||||
};
|
||||
|
||||
const returnToDefaultMode = () => {
|
||||
LayoutAnimation.configureNext({
|
||||
duration: 300,
|
||||
@@ -38,11 +65,8 @@ export const CarouselTypeChanger = ({
|
||||
setMapMode(false);
|
||||
};
|
||||
return (
|
||||
<KeyboardAvoidingView
|
||||
behavior="position"
|
||||
contentContainerStyle={{ flex: 1, flexDirection: "row" }}
|
||||
keyboardVerticalOffset={mapMode ? 0 : 45}
|
||||
enabled={Platform.OS === "ios"}
|
||||
<View
|
||||
ref={containerRef}
|
||||
style={{
|
||||
width: "100%",
|
||||
height: 40,
|
||||
@@ -50,21 +74,22 @@ export const CarouselTypeChanger = ({
|
||||
position: mapMode ? "absolute" : "relative",
|
||||
bottom: mapMode ? 0 : undefined,
|
||||
zIndex: 1000,
|
||||
backgroundColor: "white",
|
||||
backgroundColor: colors.background,
|
||||
}}
|
||||
key={"carouselTypeChanger"}
|
||||
>
|
||||
<SearchUnitBox
|
||||
isSearchMode={isSearchMode}
|
||||
setisSearchMode={setisSearchMode}
|
||||
input={input}
|
||||
setInput={setInput}
|
||||
stationSource={stationSource}
|
||||
setStationSource={setStationSource}
|
||||
closeSearch={closeSearch}
|
||||
mapMode={mapMode}
|
||||
parentRef={containerRef}
|
||||
/>
|
||||
<TouchableOpacity
|
||||
style={{
|
||||
flex: 1,
|
||||
backgroundColor:
|
||||
stationListMode == "position" ? "#0099CC" : "#0099CC80",
|
||||
stationSource.type === "position" ? fixed.primary : "#0099CC80",
|
||||
padding: 5,
|
||||
alignItems: "center",
|
||||
flexDirection: "row",
|
||||
@@ -78,20 +103,21 @@ export const CarouselTypeChanger = ({
|
||||
onPressIn={() => {
|
||||
if (!position) return;
|
||||
returnToDefaultMode();
|
||||
setStationListMode("position");
|
||||
AS.setItem("stationListMode", "position");
|
||||
setStationSource({ type: "position" });
|
||||
AS.setItem(STORAGE_KEYS.STATION_LIST_MODE, "position");
|
||||
setSelectedCurrentStation(0);
|
||||
}}
|
||||
onPress={() => {
|
||||
if (!position) return;
|
||||
returnToDefaultMode();
|
||||
setStationListMode("position");
|
||||
AS.setItem("stationListMode", "position");
|
||||
setStationSource({ type: "position" });
|
||||
AS.setItem(STORAGE_KEYS.STATION_LIST_MODE, "position");
|
||||
setSelectedCurrentStation(0);
|
||||
}}
|
||||
onLongPress={handleGpsFollowLongPress}
|
||||
>
|
||||
<Ionicons
|
||||
name="locate-outline"
|
||||
name={isGpsFollowing ? "navigate" : "locate-outline"}
|
||||
size={14}
|
||||
color="white"
|
||||
style={{ margin: 5 }}
|
||||
@@ -121,7 +147,7 @@ export const CarouselTypeChanger = ({
|
||||
<Ionicons
|
||||
name={!mapMode ? "menu" : "chevron-up-outline"}
|
||||
size={30}
|
||||
color="#0099CC"
|
||||
color={colors.iconAccent}
|
||||
style={{ marginHorizontal: 5 }}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
@@ -129,7 +155,7 @@ export const CarouselTypeChanger = ({
|
||||
style={{
|
||||
flex: 1,
|
||||
backgroundColor:
|
||||
stationListMode == "favorite" ? "#0099CC" : "#0099CC80",
|
||||
stationSource.type === "favorite" ? fixed.primary : "#0099CC80",
|
||||
padding: 5,
|
||||
alignItems: "center",
|
||||
flexDirection: "row",
|
||||
@@ -141,16 +167,14 @@ export const CarouselTypeChanger = ({
|
||||
}}
|
||||
onPressIn={() => {
|
||||
returnToDefaultMode();
|
||||
// お気に入りリスト更新
|
||||
setStationListMode("favorite");
|
||||
AS.setItem("stationListMode", "favorite");
|
||||
setStationSource({ type: "favorite" });
|
||||
AS.setItem(STORAGE_KEYS.STATION_LIST_MODE, "favorite");
|
||||
setSelectedCurrentStation(0);
|
||||
}}
|
||||
onPress={() => {
|
||||
returnToDefaultMode();
|
||||
// お気に入りリスト更新
|
||||
setStationListMode("favorite");
|
||||
AS.setItem("stationListMode", "favorite");
|
||||
setStationSource({ type: "favorite" });
|
||||
AS.setItem(STORAGE_KEYS.STATION_LIST_MODE, "favorite");
|
||||
setSelectedCurrentStation(0);
|
||||
}}
|
||||
>
|
||||
@@ -167,6 +191,6 @@ export const CarouselTypeChanger = ({
|
||||
お気に入りリスト
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</KeyboardAvoidingView>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
125
components/Menu/Carousel/GridMiniSign.tsx
Normal file
125
components/Menu/Carousel/GridMiniSign.tsx
Normal file
@@ -0,0 +1,125 @@
|
||||
import React, { FC } from "react";
|
||||
import { Text, TouchableOpacity, View } from "react-native";
|
||||
import lineColorList from "@/assets/originData/lineColorList";
|
||||
import { lightColors, useThemeColors } from "@/lib/theme";
|
||||
|
||||
type Props = {
|
||||
item: any[]; // StationProps[] (路線をまたぐ同名駅の配列)
|
||||
width: number;
|
||||
height: number;
|
||||
onPress?: () => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* グリッド表示専用の軽量駅カード。
|
||||
* Sign コンポーネントを使わず、駅番号・駅名をシンプルに描画するだけで
|
||||
* hooks・Lottie・前後駅計算などを一切持たない純粋な表示コンポーネント。
|
||||
*/
|
||||
export const GridMiniSign: FC<Props> = React.memo(({ item, width, height, onPress }) => {
|
||||
const { fixed } = useThemeColors();
|
||||
const station = item[0];
|
||||
const isSpot = !!station.isSpot;
|
||||
const lineId = isSpot ? "" : (station.StationNumber?.slice(0, 1) ?? "Y");
|
||||
const lineNum = isSpot ? "" : (station.StationNumber?.slice(1) ?? "");
|
||||
const lineColor = lineColorList[lineId] ?? fixed.primary;
|
||||
const rawName = station.Station_JP ?? "";
|
||||
const displayName = rawName.startsWith(".") ? rawName.slice(1) : rawName;
|
||||
const nameLen = displayName.length;
|
||||
const nameFontSize = nameLen <= 3 ? 22 : nameLen <= 5 ? 16 : 12;
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
onPress={onPress}
|
||||
activeOpacity={0.85}
|
||||
style={{
|
||||
width,
|
||||
height,
|
||||
borderColor: fixed.primary,
|
||||
borderWidth: 1,
|
||||
backgroundColor: lightColors.background,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
{/* 駅番号バッジ */}
|
||||
<View
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "8%",
|
||||
right: "8%",
|
||||
width: height * 0.28,
|
||||
height: height * 0.28,
|
||||
borderRadius: height * 0.14,
|
||||
borderColor: lineColor,
|
||||
borderWidth: 2,
|
||||
backgroundColor: lightColors.background,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: height * 0.1,
|
||||
fontWeight: "bold",
|
||||
color: lightColors.text,
|
||||
textAlign: "center",
|
||||
lineHeight: height * 0.12,
|
||||
}}
|
||||
numberOfLines={2}
|
||||
adjustsFontSizeToFit
|
||||
>
|
||||
{lineId + "\n" + lineNum}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* 駅名(日本語・英語) */}
|
||||
<View
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "10%",
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: "28%",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: nameFontSize,
|
||||
fontWeight: "bold",
|
||||
color: lightColors.textStationName,
|
||||
textAlign: "center",
|
||||
}}
|
||||
adjustsFontSizeToFit
|
||||
numberOfLines={1}
|
||||
>
|
||||
{displayName}
|
||||
</Text>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 8,
|
||||
color: lightColors.textStationName,
|
||||
textAlign: "center",
|
||||
marginTop: 2,
|
||||
}}
|
||||
numberOfLines={1}
|
||||
adjustsFontSizeToFit
|
||||
>
|
||||
{station.Station_EN}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* 下帯 */}
|
||||
<View
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: "26%",
|
||||
backgroundColor: fixed.primary,
|
||||
}}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
});
|
||||
115
components/Menu/Carousel/SortGridCard.tsx
Normal file
115
components/Menu/Carousel/SortGridCard.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { useItemContext } from "react-native-sortables";
|
||||
import Animated, {
|
||||
Easing,
|
||||
interpolate,
|
||||
useAnimatedStyle,
|
||||
useSharedValue,
|
||||
withDelay,
|
||||
withTiming,
|
||||
} from "react-native-reanimated";
|
||||
import { GridMiniSign } from "./GridMiniSign";
|
||||
|
||||
type Props = {
|
||||
item: any;
|
||||
cellW: number;
|
||||
cellH: number;
|
||||
startX: number;
|
||||
startY: number;
|
||||
exitX: number;
|
||||
exitY: number;
|
||||
startScale: number;
|
||||
isCurrentCard: boolean;
|
||||
isExiting: boolean;
|
||||
exitDelay: number;
|
||||
onPress?: () => void;
|
||||
};
|
||||
|
||||
const EASE_OUT = Easing.out(Easing.cubic);
|
||||
const DURATION = 320;
|
||||
|
||||
/** グリッドセルへのスライド+スケールアニメーション付きカード */
|
||||
export const SortGridCard = React.memo(function SortGridCard({
|
||||
item,
|
||||
cellW,
|
||||
cellH,
|
||||
startX,
|
||||
startY,
|
||||
exitX,
|
||||
exitY,
|
||||
startScale,
|
||||
isCurrentCard,
|
||||
isExiting,
|
||||
exitDelay,
|
||||
onPress,
|
||||
}: Props) {
|
||||
const { activationAnimationProgress } = useItemContext();
|
||||
|
||||
// 現在選択中のカードはカルーセル位置から、それ以外は近距離からスライド
|
||||
const initX = isCurrentCard ? startX : startX * 0.35;
|
||||
const initY = isCurrentCard ? startY : startY * 0.35;
|
||||
const initScale = isCurrentCard ? Math.min(startScale, 1.5) : Math.min(startScale, 1.15);
|
||||
|
||||
const tx = useSharedValue(initX);
|
||||
const ty = useSharedValue(initY);
|
||||
const sc = useSharedValue(initScale);
|
||||
const opacity = useSharedValue(0);
|
||||
|
||||
// 入場
|
||||
useEffect(() => {
|
||||
const cfg = { duration: DURATION, easing: EASE_OUT };
|
||||
tx.value = withTiming(0, cfg);
|
||||
ty.value = withTiming(0, cfg);
|
||||
sc.value = withTiming(1, cfg);
|
||||
opacity.value = withTiming(1, { duration: 180, easing: EASE_OUT });
|
||||
}, []);
|
||||
|
||||
// 退場
|
||||
useEffect(() => {
|
||||
if (!isExiting) return;
|
||||
const cfg = { duration: DURATION, easing: EASE_OUT };
|
||||
const toX = isCurrentCard ? exitX : exitX * 0.35;
|
||||
const toY = isCurrentCard ? exitY : exitY * 0.35;
|
||||
tx.value = withDelay(exitDelay, withTiming(toX, cfg));
|
||||
ty.value = withDelay(exitDelay, withTiming(toY, cfg));
|
||||
sc.value = withDelay(exitDelay, withTiming(initScale, cfg));
|
||||
opacity.value = withDelay(exitDelay, withTiming(0, { duration: 150, easing: EASE_OUT }));
|
||||
}, [isExiting]);
|
||||
|
||||
const animStyle = useAnimatedStyle(() => {
|
||||
const p = activationAnimationProgress.value;
|
||||
return {
|
||||
opacity: opacity.value * interpolate(p, [0, 1], [1, 0.85]),
|
||||
shadowOpacity: interpolate(p, [0, 1], [0, 0.4]),
|
||||
shadowRadius: interpolate(p, [0, 1], [0, 10]),
|
||||
elevation: interpolate(p, [0, 1], [1, 12]),
|
||||
transform: [
|
||||
{ translateX: tx.value },
|
||||
{ translateY: ty.value },
|
||||
{ scale: sc.value * interpolate(p, [0, 1], [1, 1.06]) },
|
||||
] as any,
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<Animated.View
|
||||
style={[
|
||||
{
|
||||
width: cellW,
|
||||
height: cellH,
|
||||
shadowColor: "#000",
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
},
|
||||
animStyle,
|
||||
]}
|
||||
>
|
||||
<GridMiniSign
|
||||
item={item}
|
||||
width={cellW}
|
||||
height={cellH}
|
||||
onPress={onPress}
|
||||
/>
|
||||
</Animated.View>
|
||||
);
|
||||
});
|
||||
|
||||
132
components/Menu/Carousel/useSortMode.tsx
Normal file
132
components/Menu/Carousel/useSortMode.tsx
Normal file
@@ -0,0 +1,132 @@
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { AS } from "@/storageControl";
|
||||
import { STORAGE_KEYS } from "@/constants";
|
||||
import { useFavoriteStation } from "@/stateBox/useFavoriteStation";
|
||||
import { SortGridCard } from "./SortGridCard";
|
||||
import { CarouselUIMode, StationSource } from "@/types";
|
||||
|
||||
type SortModeConfig = {
|
||||
listUpStation: any[][];
|
||||
setListIndex: (i: number) => void;
|
||||
width: number;
|
||||
origW: number;
|
||||
origH: number;
|
||||
cols: number;
|
||||
gridPad: number;
|
||||
gridGap: number;
|
||||
cellW: number;
|
||||
cellH: number;
|
||||
carouselHeight: number;
|
||||
stationSource: StationSource;
|
||||
};
|
||||
|
||||
/** カルーセルの並び替えモードに関わる状態・ロジックをまとめたカスタムフック */
|
||||
export function useSortMode({
|
||||
listUpStation,
|
||||
setListIndex,
|
||||
width,
|
||||
origW,
|
||||
origH,
|
||||
cols,
|
||||
gridPad,
|
||||
gridGap,
|
||||
cellW,
|
||||
cellH,
|
||||
carouselHeight,
|
||||
stationSource,
|
||||
}: SortModeConfig) {
|
||||
const { setFavoriteStation } = useFavoriteStation();
|
||||
// "carousel" | "sort" | "sort-exiting" の 3 値で UI モードを管理
|
||||
const [uiMode, setUiMode] = useState<CarouselUIMode>("carousel");
|
||||
// ソート開始時のカルーセル位置を保存(setListIndex(-1) される前の値)
|
||||
const sortModeStartIndexRef = useRef(0);
|
||||
// ソート終了後に移動するインデックス(タップで上書き可、デフォルト 0)
|
||||
const exitTargetIndexRef = useRef(0);
|
||||
|
||||
// carousel に戻ったら指定インデックスへ移動
|
||||
useEffect(() => {
|
||||
if (uiMode === "carousel") {
|
||||
setListIndex(exitTargetIndexRef.current);
|
||||
}
|
||||
}, [uiMode]);
|
||||
|
||||
/** 並び替えモード開始(現在のカルーセル位置を渡す) */
|
||||
const startSortMode = useCallback((currentIndex: number) => {
|
||||
sortModeStartIndexRef.current = currentIndex;
|
||||
exitTargetIndexRef.current = 0; // デフォルトは先頭
|
||||
setListIndex(-1); // 未選択状態にして LED を非表示
|
||||
setUiMode("sort");
|
||||
}, [setListIndex]);
|
||||
|
||||
/** 退場アニメーション完了後にモードを終了 */
|
||||
const exitSortMode = useCallback(() => {
|
||||
setUiMode("sort-exiting");
|
||||
// 退場スプリングが収束するまで待ってから carousel へ
|
||||
setTimeout(() => {
|
||||
setUiMode("carousel");
|
||||
}, listUpStation.length * 40 + 500);
|
||||
}, [listUpStation.length]);
|
||||
|
||||
/** Sortable.Grid の renderItem(useCallback でメモ化) */
|
||||
const sortGridRenderItem = useCallback(
|
||||
({ item, index }: { item: any; index: number }) => {
|
||||
const col = index % cols;
|
||||
const row = Math.floor(index / cols);
|
||||
const carouselCardCenterX =
|
||||
(index - sortModeStartIndexRef.current) * width + width / 2 - gridPad;
|
||||
const carouselCardCenterY = carouselHeight / 2;
|
||||
const cellCenterX = col * (cellW + gridGap) + cellW / 2;
|
||||
const cellCenterY = row * (cellH + gridGap) + cellH / 2;
|
||||
const startX = carouselCardCenterX - cellCenterX;
|
||||
const startY = carouselCardCenterY - cellCenterY;
|
||||
const exitCarouselCardCenterX =
|
||||
(index - exitTargetIndexRef.current) * width + width / 2 - gridPad;
|
||||
const exitX = exitCarouselCardCenterX - cellCenterX;
|
||||
const exitY = carouselCardCenterY - cellCenterY;
|
||||
return (
|
||||
<SortGridCard
|
||||
key={item[0].StationNumber}
|
||||
item={item}
|
||||
cellW={cellW}
|
||||
cellH={cellH}
|
||||
startX={startX}
|
||||
startY={startY}
|
||||
exitX={exitX}
|
||||
exitY={exitY}
|
||||
startScale={origW / cellW}
|
||||
isCurrentCard={index === sortModeStartIndexRef.current}
|
||||
isExiting={uiMode === "sort-exiting"}
|
||||
exitDelay={Math.min(index * 40, 180)}
|
||||
onPress={() => {
|
||||
exitTargetIndexRef.current = index;
|
||||
exitSortMode();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[cellW, cellH, gridGap, gridPad, origW, width, carouselHeight, uiMode]
|
||||
);
|
||||
|
||||
/** Sortable.Grid の onDragEnd */
|
||||
const onSortDragEnd = useCallback(
|
||||
(newOrder: { indexToKey: string[] }) => {
|
||||
// お気に入りモード以外はデータを書き換えない(安全策)
|
||||
if (stationSource.type !== "favorite") return;
|
||||
const newList = newOrder.indexToKey.map(
|
||||
(key) => listUpStation.find((s) => s[0].StationNumber === key) ?? []
|
||||
);
|
||||
setFavoriteStation(newList);
|
||||
AS.setItem(STORAGE_KEYS.FAVORITE_STATION, JSON.stringify(newList));
|
||||
},
|
||||
[listUpStation, setFavoriteStation, stationSource]
|
||||
);
|
||||
|
||||
return {
|
||||
uiMode,
|
||||
startSortMode,
|
||||
exitSortMode,
|
||||
sortGridRenderItem,
|
||||
onSortDragEnd,
|
||||
};
|
||||
}
|
||||
@@ -14,9 +14,13 @@ import { SpecialTrainInfoBox } from "./SpecialTrainInfoBox";
|
||||
import { SheetManager } from "react-native-actions-sheet";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import { useNotification } from "@/stateBox/useNotifications";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
import { isAvailable as isFelicaAvailable } from "@/modules/expo-felica-reader/src";
|
||||
|
||||
export const FixedContentBottom = (props) => {
|
||||
const { expoPushToken } = useNotification();
|
||||
const { colors, fixed } = useThemeColors();
|
||||
const felicaAvailable = isFelicaAvailable();
|
||||
return (
|
||||
<>
|
||||
{props.children}
|
||||
@@ -70,7 +74,7 @@ export const FixedContentBottom = (props) => {
|
||||
</Text>
|
||||
</TextBox>
|
||||
<TextBox
|
||||
backgroundColor="#0099CC"
|
||||
backgroundColor={fixed.primary}
|
||||
flex={1}
|
||||
onPressButton={() =>
|
||||
SheetManager.show("SpecialTrainInfo", {
|
||||
@@ -78,52 +82,52 @@ export const FixedContentBottom = (props) => {
|
||||
})
|
||||
}
|
||||
>
|
||||
<Text style={{ color: "white", fontWeight: "bold", fontSize: 20 }}>
|
||||
<Text style={{ color: fixed.textOnPrimary, fontWeight: "bold", fontSize: 20 }}>
|
||||
臨時列車などのお知らせ
|
||||
</Text>
|
||||
<Text style={{ color: "white", fontSize: 18 }}>
|
||||
<Text style={{ color: fixed.textOnPrimary, fontSize: 18 }}>
|
||||
区間縮小・計画運休・イベント・季節臨時列車など
|
||||
</Text>
|
||||
</TextBox>
|
||||
<TextBox
|
||||
backgroundColor="#0099CC"
|
||||
backgroundColor={fixed.primary}
|
||||
flex={1}
|
||||
onPressButton={() =>
|
||||
Linking.openURL("https://www.jr-shikoku.co.jp/03_news/press/")
|
||||
}
|
||||
>
|
||||
<Text style={{ color: "white", fontWeight: "bold", fontSize: 20 }}>
|
||||
<Text style={{ color: fixed.textOnPrimary, fontWeight: "bold", fontSize: 20 }}>
|
||||
ニュースリリース
|
||||
</Text>
|
||||
<Text style={{ color: "white", fontSize: 18 }}>
|
||||
<Text style={{ color: fixed.textOnPrimary, fontSize: 18 }}>
|
||||
公式プレス記事はこちら
|
||||
</Text>
|
||||
</TextBox>
|
||||
<TextBox
|
||||
backgroundColor="#0099CC"
|
||||
backgroundColor={fixed.primary}
|
||||
flex={1}
|
||||
onPressButton={() =>
|
||||
Linking.openURL("https://www.jr-shikoku.co.jp/teiki/")
|
||||
}
|
||||
>
|
||||
<Text style={{ color: "white", fontWeight: "bold", fontSize: 20 }}>
|
||||
<Text style={{ color: fixed.textOnPrimary, fontWeight: "bold", fontSize: 20 }}>
|
||||
定期運賃計算
|
||||
</Text>
|
||||
<Text style={{ color: "white", fontSize: 18 }}>
|
||||
<Text style={{ color: fixed.textOnPrimary, fontSize: 18 }}>
|
||||
通常/学生/快て〜き等はこちら
|
||||
</Text>
|
||||
</TextBox>
|
||||
<TextBox
|
||||
backgroundColor="#0099CC"
|
||||
backgroundColor={fixed.primary}
|
||||
flex={1}
|
||||
onPressButton={() =>
|
||||
Linking.openURL("https://www.jr-shikoku.co.jp/04_company/group/sp/")
|
||||
}
|
||||
>
|
||||
<Text style={{ color: "white", fontWeight: "bold", fontSize: 20 }}>
|
||||
<Text style={{ color: fixed.textOnPrimary, fontWeight: "bold", fontSize: 20 }}>
|
||||
JR四国のお店・サービス
|
||||
</Text>
|
||||
<Text style={{ color: "white", fontSize: 18 }}>
|
||||
<Text style={{ color: fixed.textOnPrimary, fontSize: 18 }}>
|
||||
JR四国グループの施設をご案内
|
||||
</Text>
|
||||
</TextBox>
|
||||
@@ -132,7 +136,7 @@ export const FixedContentBottom = (props) => {
|
||||
style={{
|
||||
flex: 1,
|
||||
backgroundColor: "#729FCF",
|
||||
borderColor: "#0099CC",
|
||||
borderColor: fixed.primary,
|
||||
padding: 10,
|
||||
borderWidth: 1,
|
||||
margin: 2,
|
||||
@@ -154,37 +158,43 @@ export const FixedContentBottom = (props) => {
|
||||
<TouchableOpacity
|
||||
style={{
|
||||
flex: 1,
|
||||
backgroundColor: "#8AE234",
|
||||
borderColor: "#0099CC",
|
||||
backgroundColor: felicaAvailable ? "#00796B" : "#9E9E9E",
|
||||
borderColor: fixed.primary,
|
||||
padding: 10,
|
||||
borderWidth: 1,
|
||||
margin: 2,
|
||||
alignItems: "center",
|
||||
opacity: felicaAvailable ? 1 : 0.6,
|
||||
}}
|
||||
onPress={() => Linking.openURL("tel:0570-00-4592")}
|
||||
onPress={() => props.navigate("setting", { screen: "FelicaHistoryPage" })}
|
||||
disabled={!felicaAvailable}
|
||||
>
|
||||
<Text style={{ color: "white", fontWeight: "bold", fontSize: 20 }}>
|
||||
JR四国案内センター
|
||||
IC残高・履歴
|
||||
</Text>
|
||||
<Foundation name="telephone" color="white" size={50} />
|
||||
<Text style={{ color: "white" }}>0570-00-4592</Text>
|
||||
<Text style={{ color: "white" }}>(8:00~20:00 年中無休)</Text>
|
||||
<Text style={{ color: "white" }}>(通話料がかかります)</Text>
|
||||
<MaterialCommunityIcons name="contactless-payment" color="white" size={50} />
|
||||
<Text style={{ color: "white" }}>Felica対応ICカードの</Text>
|
||||
<Text style={{ color: "white" }}>残高・利用履歴を表示</Text>
|
||||
{!felicaAvailable && (
|
||||
<Text style={{ color: "white", fontSize: 11, marginTop: 2 }}>
|
||||
NFC非対応端末
|
||||
</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<TextBox
|
||||
backgroundColor="#0099CC"
|
||||
backgroundColor={fixed.primary}
|
||||
flex={1}
|
||||
onPressButton={() => SheetManager.show("Social")}
|
||||
>
|
||||
<Text style={{ color: "white", fontWeight: "bold", fontSize: 20 }}>
|
||||
<Text style={{ color: fixed.textOnPrimary, fontWeight: "bold", fontSize: 20 }}>
|
||||
ソーシャルメディア
|
||||
</Text>
|
||||
<Text style={{ color: "white", fontSize: 18 }}>
|
||||
<Text style={{ color: fixed.textOnPrimary, fontSize: 18 }}>
|
||||
JR四国のSNS一覧です。
|
||||
</Text>
|
||||
</TextBox>
|
||||
<Text style={{ fontWeight: "bold", fontSize: 20 }}>
|
||||
<Text style={{ fontWeight: "bold", fontSize: 20, color: colors.text }}>
|
||||
JR四国非公式列車データベース(β)
|
||||
</Text>
|
||||
<View style={{ flexDirection: "row" }}>
|
||||
@@ -227,7 +237,37 @@ export const FixedContentBottom = (props) => {
|
||||
<Ionicons name="search" color="white" size={40} />
|
||||
</TextBox>
|
||||
</View>
|
||||
<Text style={{ fontWeight: "bold", fontSize: 20 }}>その他</Text>
|
||||
<View style={{ flexDirection: "row" }}>
|
||||
<TextBox
|
||||
backgroundColor="#2980B9"
|
||||
flex={1}
|
||||
onPressButton={() => {
|
||||
const uri = `https://jr-shikoku-data-system.pages.dev/diagram-graph?userID=${expoPushToken}&from=eachTrainInfo`;
|
||||
props.navigate("generalWebView", { uri, useExitButton: false });
|
||||
SheetManager.hide("EachTrainInfo");
|
||||
}}
|
||||
>
|
||||
<Text style={{ color: "white", fontWeight: "bold", fontSize: 20 }}>
|
||||
ダイヤグラフ
|
||||
</Text>
|
||||
<MaterialCommunityIcons name="chart-timeline" color="white" size={40} />
|
||||
</TextBox>
|
||||
<TextBox
|
||||
backgroundColor="#C0392B"
|
||||
flex={1}
|
||||
onPressButton={() => {
|
||||
const uri = `https://jr-shikoku-data-system.pages.dev/diagram-chart?userID=${expoPushToken}&from=eachTrainInfo`;
|
||||
props.navigate("generalWebView", { uri, useExitButton: false });
|
||||
SheetManager.hide("EachTrainInfo");
|
||||
}}
|
||||
>
|
||||
<Text style={{ color: "white", fontWeight: "bold", fontSize: 20 }}>
|
||||
運用チャート
|
||||
</Text>
|
||||
<MaterialCommunityIcons name="chart-gantt" color="white" size={40} />
|
||||
</TextBox>
|
||||
</View>
|
||||
<Text style={{ fontWeight: "bold", fontSize: 20, color: colors.text }}>その他</Text>
|
||||
<TextBox
|
||||
backgroundColor="rgb(88, 101, 242)"
|
||||
flex={1}
|
||||
@@ -254,10 +294,10 @@ export const FixedContentBottom = (props) => {
|
||||
})
|
||||
}
|
||||
>
|
||||
<Text style={{ color: "black", fontWeight: "bold", fontSize: 20 }}>
|
||||
<Text style={{ color: colors.text, fontWeight: "bold", fontSize: 20 }}>
|
||||
アプリの更新情報
|
||||
</Text>
|
||||
<Text style={{ color: "black", fontSize: 18 }}>
|
||||
<Text style={{ color: colors.text, fontSize: 18 }}>
|
||||
過去のアプリの更新履歴が表示できます。
|
||||
</Text>
|
||||
</TextBox>
|
||||
|
||||
@@ -11,13 +11,15 @@ import { Ionicons } from "@expo/vector-icons";
|
||||
import { SheetManager } from "react-native-actions-sheet";
|
||||
import LottieView from "lottie-react-native";
|
||||
import { useTrainDelayData } from "@/stateBox/useTrainDelayData";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
import dayjs from "dayjs";
|
||||
export const JRSTraInfoBox = () => {
|
||||
const { getTime, delayData, loadingDelayData, setLoadingDelayData } =
|
||||
useTrainDelayData();
|
||||
const { colors, fixed } = useThemeColors();
|
||||
const styles: { [key: string]: StyleProp<ViewStyle> } = {
|
||||
touch: {
|
||||
backgroundColor: "#0099CC",
|
||||
backgroundColor: fixed.primary,
|
||||
borderRadius: 5,
|
||||
margin: 10,
|
||||
borderColor: "black",
|
||||
@@ -25,7 +27,7 @@ export const JRSTraInfoBox = () => {
|
||||
overflow: "hidden",
|
||||
},
|
||||
scroll: {
|
||||
backgroundColor: "#0099CC",
|
||||
backgroundColor: fixed.primary,
|
||||
borderRadius: 5,
|
||||
maxHeight: 300,
|
||||
},
|
||||
@@ -39,7 +41,7 @@ export const JRSTraInfoBox = () => {
|
||||
},
|
||||
box: {
|
||||
padding: 10,
|
||||
backgroundColor: "white",
|
||||
backgroundColor: colors.background,
|
||||
borderBottomLeftRadius: 5,
|
||||
borderBottomRightRadius: 5,
|
||||
},
|
||||
@@ -53,16 +55,16 @@ export const JRSTraInfoBox = () => {
|
||||
<View
|
||||
style={{ padding: 10, flexDirection: "row", alignItems: "center" }}
|
||||
>
|
||||
<Text style={{ fontSize: 30, fontWeight: "bold", color: "white" }}>
|
||||
<Text style={{ fontSize: 30, fontWeight: "bold", color: fixed.textOnPrimary }}>
|
||||
列車遅延速報EX
|
||||
</Text>
|
||||
<View style={{ flex: 1 }} />
|
||||
<Text style={{ fontSize: 30, fontWeight: "bold", color: "white" }}>
|
||||
<Text style={{ fontSize: 30, fontWeight: "bold", color: fixed.textOnPrimary }}>
|
||||
{getTime ? dayjs(getTime).format("HH:mm") : NaN}
|
||||
</Text>
|
||||
<Ionicons
|
||||
name="reload"
|
||||
color="white"
|
||||
color={fixed.textOnPrimary}
|
||||
size={30}
|
||||
style={{ margin: 5 }}
|
||||
onPress={() => setLoadingDelayData(true)}
|
||||
@@ -74,7 +76,7 @@ export const JRSTraInfoBox = () => {
|
||||
<LottieView
|
||||
autoPlay
|
||||
loop
|
||||
style={{ width: 150, height: 150, backgroundColor: "#fff" }}
|
||||
style={{ width: 150, height: 150, backgroundColor: colors.background }}
|
||||
source={require("@/assets/51690-loading-diamonds.json")}
|
||||
/>
|
||||
</View>
|
||||
@@ -86,22 +88,22 @@ export const JRSTraInfoBox = () => {
|
||||
style={{ flexDirection: "row" }}
|
||||
key={data[1] + "key" + index}
|
||||
>
|
||||
<Text style={{ flex: 15, fontSize: 18 }}>
|
||||
<Text style={{ flex: 15, fontSize: 18, color: colors.text }}>
|
||||
{data[0].replace("\n", "")}
|
||||
</Text>
|
||||
<Text style={{ flex: 5, fontSize: 18 }}>{data[1]}</Text>
|
||||
<Text style={{ flex: 6, fontSize: 18 }}>{data[3]}</Text>
|
||||
<Text style={{ flex: 5, fontSize: 18, color: colors.text }}>{data[1]}</Text>
|
||||
<Text style={{ flex: 6, fontSize: 18, color: colors.text }}>{data[3]}</Text>
|
||||
</View>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<Text>現在、5分以上の遅れはありません。</Text>
|
||||
<Text style={{ color: colors.text }}>現在、5分以上の遅れはありません。</Text>
|
||||
)}
|
||||
</View>
|
||||
</ScrollView>
|
||||
<View style={styles.bottom}>
|
||||
<View style={{ flex: 1 }} />
|
||||
<Text style={{ color: "white", fontWeight: "bold", fontSize: 20 }}>
|
||||
<Text style={{ color: fixed.textOnPrimary, fontWeight: "bold", fontSize: 20 }}>
|
||||
詳細を見る
|
||||
</Text>
|
||||
<View style={{ flex: 1 }} />
|
||||
|
||||
@@ -1,49 +1,69 @@
|
||||
import React from "react";
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
TouchableOpacity,
|
||||
Text,
|
||||
View,
|
||||
LayoutAnimation,
|
||||
TextInput,
|
||||
KeyboardAvoidingView,
|
||||
Platform,
|
||||
} from "react-native";
|
||||
import Ionicons from "react-native-vector-icons/Ionicons";
|
||||
import { useWindowDimensions } from "react-native";
|
||||
import { useBottomTabBarHeight } from "@react-navigation/bottom-tabs";
|
||||
import lineColorList from "@/assets/originData/lineColorList";
|
||||
import { lineList_LineWebID, stationIDPair } from "@/lib/getStationList";
|
||||
import { StationSource } from "@/types";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
import { useKeyboardAvoid } from "@/lib/useKeyboardAvoid";
|
||||
|
||||
export const SearchUnitBox = ({
|
||||
isSearchMode,
|
||||
setisSearchMode,
|
||||
input,
|
||||
setInput,
|
||||
stationSource,
|
||||
setStationSource,
|
||||
closeSearch,
|
||||
mapMode = false,
|
||||
parentRef,
|
||||
}: {
|
||||
stationSource: StationSource;
|
||||
setStationSource: (s: StationSource) => void;
|
||||
closeSearch: () => void;
|
||||
mapMode?: boolean;
|
||||
parentRef?: React.RefObject<View>;
|
||||
}) => {
|
||||
const { height, width } = useWindowDimensions();
|
||||
const { width } = useWindowDimensions();
|
||||
const { colors, fixed } = useThemeColors();
|
||||
const tabBarHeight = useBottomTabBarHeight();
|
||||
const isSearch = stationSource.type === "search";
|
||||
const query = isSearch ? stationSource.query : "";
|
||||
const lineId = isSearch ? stationSource.lineId : undefined;
|
||||
const { keyboardHeight, measuredOffset: measuredBottom } =
|
||||
useKeyboardAvoid({ measureRef: parentRef, tabBarHeight });
|
||||
|
||||
return (
|
||||
<>
|
||||
<TouchableOpacity
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: !!isSearchMode ? 0 : 60,
|
||||
bottom: isSearch
|
||||
? (keyboardHeight > 0 ? measuredBottom : 0)
|
||||
: 60,
|
||||
right: 0,
|
||||
padding: !!isSearchMode ? 5 : 10,
|
||||
margin: !!isSearchMode ? 0 : 10,
|
||||
backgroundColor: "#0099CC",
|
||||
borderRadius: !!isSearchMode ? 5 : 50,
|
||||
width: !!isSearchMode ? width : 50,
|
||||
padding: isSearch ? 5 : 10,
|
||||
margin: isSearch ? 0 : 10,
|
||||
backgroundColor: fixed.primary,
|
||||
borderRadius: isSearch ? 5 : 50,
|
||||
width: isSearch ? width : 50,
|
||||
zIndex: 1000,
|
||||
}}
|
||||
disabled={!!isSearchMode}
|
||||
disabled={isSearch}
|
||||
onPress={() => {
|
||||
LayoutAnimation.configureNext({
|
||||
duration: 100,
|
||||
update: { type: "easeInEaseOut", springDamping: 0.6 },
|
||||
});
|
||||
setisSearchMode(true);
|
||||
setStationSource({ type: "search", query: "", lineId: undefined });
|
||||
}}
|
||||
>
|
||||
{!isSearchMode && <Ionicons name="search" size={30} color="white" />}
|
||||
{!!isSearchMode && (
|
||||
{!isSearch && <Ionicons name="search" size={30} color="white" />}
|
||||
{isSearch && (
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: "#0099CC",
|
||||
@@ -61,7 +81,7 @@ export const SearchUnitBox = ({
|
||||
duration: 100,
|
||||
update: { type: "easeInEaseOut", springDamping: 0.6 },
|
||||
});
|
||||
setisSearchMode(false);
|
||||
closeSearch();
|
||||
}}
|
||||
>
|
||||
<Ionicons
|
||||
@@ -73,9 +93,9 @@ export const SearchUnitBox = ({
|
||||
</TouchableOpacity>
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: "white",
|
||||
backgroundColor: colors.background,
|
||||
borderRadius: 25,
|
||||
height: 30,
|
||||
height: 40,
|
||||
paddingRight: 10,
|
||||
paddingLeft: 10,
|
||||
flex: 1,
|
||||
@@ -87,13 +107,15 @@ export const SearchUnitBox = ({
|
||||
<TextInput
|
||||
placeholder="駅名や駅ナンバリングを入力してフィルタリングします。"
|
||||
onEndEditing={() => {}}
|
||||
onChange={(ret) => setInput(ret.nativeEvent.text)}
|
||||
value={input}
|
||||
style={{ flex: 1 }}
|
||||
onChange={(ret) =>
|
||||
setStationSource({ type: "search", query: ret.nativeEvent.text, lineId })
|
||||
}
|
||||
value={query}
|
||||
style={{ flex: 1, height: "100%", paddingVertical: 0 }}
|
||||
/>
|
||||
{input && (
|
||||
{query && (
|
||||
<TouchableOpacity
|
||||
onPress={() => setInput("") }
|
||||
onPress={() => setStationSource({ type: "search", query: "", lineId })}
|
||||
style={{
|
||||
padding: 3,
|
||||
borderRadius: 15,
|
||||
@@ -109,48 +131,44 @@ export const SearchUnitBox = ({
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
{!input && (
|
||||
{!query && (
|
||||
<View style={{ flexDirection: "row", alignItems: "center" }}>
|
||||
{Object.keys(lineList_LineWebID).map((d) => (
|
||||
<TouchableOpacity
|
||||
style={{
|
||||
flex: 1,
|
||||
backgroundColor:
|
||||
lineColorList[stationIDPair[lineList_LineWebID[d]]],
|
||||
padding: 5,
|
||||
marginHorizontal: 2,
|
||||
borderRadius: 10,
|
||||
borderColor: "white",
|
||||
borderWidth: 1,
|
||||
borderStyle: "solid",
|
||||
alignItems: "center",
|
||||
opacity:
|
||||
isSearchMode == stationIDPair[lineList_LineWebID[d]]
|
||||
? 1
|
||||
: !isSearchMode
|
||||
? 1
|
||||
: 0.5,
|
||||
zIndex: 10,
|
||||
}}
|
||||
onPress={() => {
|
||||
const id = stationIDPair[lineList_LineWebID[d]];
|
||||
const s = isSearchMode == id ? undefined : id;
|
||||
if (!s) return;
|
||||
setisSearchMode(s);
|
||||
}}
|
||||
key={stationIDPair[lineList_LineWebID[d]]}
|
||||
>
|
||||
<Text
|
||||
{Object.keys(lineList_LineWebID).map((d) => {
|
||||
const buttonLineId = stationIDPair[lineList_LineWebID[d]];
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={{
|
||||
color: "white",
|
||||
fontWeight: "bold",
|
||||
fontSize: 20,
|
||||
flex: 1,
|
||||
backgroundColor: lineColorList[buttonLineId],
|
||||
padding: 5,
|
||||
marginHorizontal: 2,
|
||||
borderRadius: 10,
|
||||
borderColor: "white",
|
||||
borderWidth: 1,
|
||||
borderStyle: "solid",
|
||||
alignItems: "center",
|
||||
opacity: !lineId ? 1 : lineId === buttonLineId ? 1 : 0.5,
|
||||
zIndex: 10,
|
||||
}}
|
||||
onPress={() => {
|
||||
// 同じ路線を再タップしても変化なし(元の挙動を維持)
|
||||
if (lineId === buttonLineId) return;
|
||||
setStationSource({ type: "search", query, lineId: buttonLineId });
|
||||
}}
|
||||
key={buttonLineId}
|
||||
>
|
||||
{stationIDPair[lineList_LineWebID[d]]}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
<Text
|
||||
style={{
|
||||
color: "white",
|
||||
fontWeight: "bold",
|
||||
fontSize: 20,
|
||||
}}
|
||||
>
|
||||
{buttonLineId}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { FC, useLayoutEffect, useState } from "react";
|
||||
import { View, Text, TouchableOpacity } from "react-native";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
import { logger } from "@/utils/logger";
|
||||
import { getPDFViewURL } from "@/lib/getPdfViewURL";
|
||||
import { ScrollView, SheetManager } from "react-native-actions-sheet";
|
||||
@@ -10,6 +11,7 @@ type props = {
|
||||
type specialDataType = { address: string; text: string; description: string };
|
||||
|
||||
export const SpecialTrainInfoBox: FC<props> = ({ navigate }) => {
|
||||
const { colors, fixed } = useThemeColors();
|
||||
const [specialData, setSpecialData] = useState<specialDataType[]>([]);
|
||||
useLayoutEffect(() => {
|
||||
fetch("https://n8n.haruk.in/webhook/sptrainfo")
|
||||
@@ -27,13 +29,13 @@ export const SpecialTrainInfoBox: FC<props> = ({ navigate }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={{ backgroundColor: "#0099CC" }}>
|
||||
<View style={{ backgroundColor: fixed.primary }}>
|
||||
<View style={{ flexDirection: "row", alignItems: "center" }}>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 30,
|
||||
fontWeight: "bold",
|
||||
color: "white",
|
||||
color: fixed.textOnPrimary,
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 5,
|
||||
}}
|
||||
@@ -41,7 +43,7 @@ export const SpecialTrainInfoBox: FC<props> = ({ navigate }) => {
|
||||
臨時列車情報
|
||||
</Text>
|
||||
</View>
|
||||
<ScrollView style={{ backgroundColor: "white" }}>
|
||||
<ScrollView style={{ backgroundColor: colors.background }}>
|
||||
{specialData.map((d) => (
|
||||
<TouchableOpacity
|
||||
onPress={() => onPressItem(d)}
|
||||
@@ -50,12 +52,12 @@ export const SpecialTrainInfoBox: FC<props> = ({ navigate }) => {
|
||||
style={{
|
||||
padding: 10,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: "#ccc",
|
||||
borderBottomColor: colors.border,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<Text style={{ color: "black", fontSize: 20 }}>{d.text}</Text>
|
||||
<Text style={{ color: colors.text, fontSize: 20 }}>{d.text}</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</ScrollView>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { FC, useState } from "react";
|
||||
import { View, Text, TouchableOpacity } from "react-native";
|
||||
import { lightColors } from "@/lib/theme";
|
||||
import { useInterval } from "@/lib/useInterval";
|
||||
|
||||
import lineColorList from "@/assets/originData/lineColorList";
|
||||
@@ -18,9 +19,10 @@ export const StationNumber: FC<StationNumberProps> = (props) => {
|
||||
setAnimation(animation + 1 < data.length ? animation + 1 : 0);
|
||||
}, 2000);
|
||||
|
||||
const lineID = data[animation].StationNumber.slice(0, 1);
|
||||
const lineName = data[animation].StationNumber.slice(1);
|
||||
const lineID = data[animation]?.StationNumber?.slice(0, 1) ?? "";
|
||||
const lineName = data[animation]?.StationNumber?.slice(1) ?? "";
|
||||
const size = active ? 24 : 18;
|
||||
if (!data[animation]) return null;
|
||||
return (
|
||||
<TouchableOpacity
|
||||
onPress={onPress}
|
||||
@@ -57,7 +59,7 @@ export const StationNumber: FC<StationNumberProps> = (props) => {
|
||||
width: size,
|
||||
height: size,
|
||||
borderColor: lineColorList[lineID],
|
||||
backgroundColor: "white",
|
||||
backgroundColor: lightColors.background,
|
||||
borderWidth: active ? 2 : 1,
|
||||
borderRadius: 22,
|
||||
}}
|
||||
@@ -70,7 +72,7 @@ export const StationNumber: FC<StationNumberProps> = (props) => {
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
textAlign: "center",
|
||||
color: "black",
|
||||
color: lightColors.text,
|
||||
fontWeight: active ? "bold" : "normal",
|
||||
textAlignVertical: "center",
|
||||
}}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { View, TouchableOpacity, Linking,Platform, Image, useWindowDimensions } from "react-native";
|
||||
import Constants from "expo-constants";
|
||||
import { View, TouchableOpacity, Linking, Platform, Image, useWindowDimensions } from "react-native";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
|
||||
|
||||
export const TitleBar = () => {
|
||||
const { width } = useWindowDimensions();
|
||||
const { colors } = useThemeColors();
|
||||
const { top } = useSafeAreaInsets();
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
@@ -13,13 +16,13 @@ export const TitleBar = () => {
|
||||
left: 0,
|
||||
right: 0,
|
||||
zIndex: 100,
|
||||
paddingTop: Platform.OS == "ios" ? Constants.statusBarHeight : 0,
|
||||
paddingTop: top,
|
||||
}}
|
||||
>
|
||||
<TouchableOpacity
|
||||
onPress={() => Linking.openURL("https://www.jr-shikoku.co.jp")}
|
||||
>
|
||||
<Image source={require("../../assets/Header.png")} style={{ width: width, resizeMode: "contain", backgroundColor: "white", height: 80 }} />
|
||||
<Image source={require("../../assets/Header.png")} style={{ width: width, resizeMode: "contain", backgroundColor: colors.background, height: 80 }} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from "react";
|
||||
import { Linking, View } from "react-native";
|
||||
import { UsefulBox } from "@/components/TrainMenu/UsefulBox";
|
||||
import MaterialCommunityIcons from "@expo/vector-icons/build/MaterialCommunityIcons";
|
||||
import { MaterialCommunityIcons } from "@expo/vector-icons";
|
||||
export const TopMenuButton = () => {
|
||||
const buttonList:{
|
||||
backgroundColor: string;
|
||||
|
||||
428
components/Settings/DataSourceSettings.tsx
Normal file
428
components/Settings/DataSourceSettings.tsx
Normal file
@@ -0,0 +1,428 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { View, Text, ScrollView, StyleSheet, Image, TouchableOpacity, Linking } from "react-native";
|
||||
import { Switch } from "@rneui/themed";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import { MaterialCommunityIcons } from "@expo/vector-icons";
|
||||
import { SheetHeaderItem } from "@/components/atom/SheetHeaderItem";
|
||||
import { AS } from "../../storageControl";
|
||||
import { STORAGE_KEYS } from "@/constants";
|
||||
import { useTrainMenu } from "@/stateBox/useTrainMenu";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
|
||||
const HUB_LOGO_PNG = require("@/assets/icons/hub_logo.png");
|
||||
const ELESITE_LOGO_PNG = require("@/assets/icons/elesite_logo.png");
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* DataSourceAccordionCard */
|
||||
/* ------------------------------------------------------------------ */
|
||||
type Feature = { icon: string; label: string; text: string };
|
||||
|
||||
type DataSourceAccordionCardProps = {
|
||||
/** ロゴ画像 (require) */
|
||||
logo: any;
|
||||
/** アクセントカラー */
|
||||
accentColor: string;
|
||||
/** データソース名 */
|
||||
title: string;
|
||||
/** 1行サブタイトル */
|
||||
tagline: string;
|
||||
/** スイッチの値 */
|
||||
enabled: boolean;
|
||||
/** スイッチ変更ハンドラ */
|
||||
onToggle: (v: boolean) => void;
|
||||
/** 説明文 */
|
||||
description: string;
|
||||
/** 機能リスト */
|
||||
features: Feature[];
|
||||
/** フッターリンクラベル */
|
||||
linkLabel: string;
|
||||
/** フッターリンク URL */
|
||||
linkUrl: string;
|
||||
};
|
||||
|
||||
const DataSourceAccordionCard: React.FC<DataSourceAccordionCardProps> = ({
|
||||
logo,
|
||||
accentColor,
|
||||
title,
|
||||
tagline,
|
||||
enabled,
|
||||
onToggle,
|
||||
description,
|
||||
features,
|
||||
linkLabel,
|
||||
linkUrl,
|
||||
detailLabel,
|
||||
}) => {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const { colors } = useThemeColors();
|
||||
|
||||
return (
|
||||
<View style={[styles.accordionCard, { backgroundColor: colors.surface, borderColor: colors.borderSecondary }, enabled && styles.accordionCardEnabled]}>
|
||||
{/* ── ヘッダー行(常時表示) ── */}
|
||||
<View style={styles.accordionHeader}>
|
||||
{/* 左:ロゴ */}
|
||||
<Image source={logo} style={styles.accordionLogo} />
|
||||
|
||||
{/* 中央:タイトル+タグライン */}
|
||||
<View style={styles.accordionTitles}>
|
||||
<Text style={[styles.accordionTitle, { color: colors.textPrimary }]}>{title}</Text>
|
||||
<Text style={[styles.accordionTagline, { color: colors.textTertiary }]}>{tagline}</Text>
|
||||
</View>
|
||||
|
||||
{/* 右:スイッチ */}
|
||||
<Switch
|
||||
value={enabled}
|
||||
onValueChange={onToggle}
|
||||
color={accentColor}
|
||||
style={styles.accordionSwitch}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* スイッチ状態テキスト */}
|
||||
<View style={styles.accordionStatusRow}>
|
||||
<View style={[styles.statusDot, { backgroundColor: enabled ? accentColor : colors.textDisabled }]} />
|
||||
<Text style={[styles.statusText, { color: enabled ? accentColor : colors.textQuaternary }]}>
|
||||
{enabled ? "有効 — 編成データを取得します" : "無効 — データを取得しません"}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* ── 展開トリガー ── */}
|
||||
<TouchableOpacity
|
||||
style={[styles.accordionToggleRow, { borderTopColor: colors.borderCard }]}
|
||||
onPress={() => setExpanded((v) => !v)}
|
||||
activeOpacity={0.6}
|
||||
>
|
||||
<Text style={[styles.accordionToggleLabel, { color: colors.textSecondary }]}>
|
||||
{expanded ? "詳細を閉じる" : (detailLabel ?? `${title} について`)}
|
||||
</Text>
|
||||
<MaterialCommunityIcons
|
||||
name={expanded ? "chevron-up" : "chevron-down"}
|
||||
size={16}
|
||||
color={colors.iconSecondary}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* ── 展開コンテンツ ── */}
|
||||
{expanded && (
|
||||
<View style={[styles.accordionBody, { borderTopColor: colors.borderCard, backgroundColor: colors.backgroundTertiary }]}>
|
||||
{/* 説明文 */}
|
||||
<Text style={[styles.bodyDesc, { color: colors.textSecondary }]}>{description}</Text>
|
||||
|
||||
{/* 機能リスト */}
|
||||
<View style={[styles.bodyFeatures, { borderTopColor: colors.borderSecondary }]}>
|
||||
{features.map((f) => (
|
||||
<View key={f.icon} style={styles.featureRow}>
|
||||
<View style={styles.featureIcon}>
|
||||
<MaterialCommunityIcons name={f.icon as any} size={14} color={colors.iconSecondary} />
|
||||
</View>
|
||||
<Text style={[styles.featureLabel, { color: colors.textPrimary }]}>{f.label}</Text>
|
||||
<Text style={[styles.featureText, { color: colors.textSecondary }]}>{f.text}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
|
||||
{/* リンク */}
|
||||
<TouchableOpacity
|
||||
style={[styles.bodyLink, { borderTopColor: colors.borderSecondary }]}
|
||||
onPress={() => Linking.openURL(linkUrl)}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<MaterialCommunityIcons name="open-in-new" size={13} color={colors.iconSecondary} />
|
||||
<Text style={[styles.bodyLinkText, { color: colors.textSecondary }]}>{linkLabel}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* 定数 */
|
||||
/* ------------------------------------------------------------------ */
|
||||
const UNYOHUB_FEATURES: Feature[] = [
|
||||
{ icon: "calendar-today", label: "運用データ", text: "本日・過去数日から投稿があった運用の継続予測運用情報を表示" },
|
||||
{ icon: "map-outline", label: "対象エリア", text: "JR四国全線" },
|
||||
{ icon: "train", label: "対象運用", text: "JR四国管内営業列車及び貨物列車に対応、臨時列車/突発運用は非対応" },
|
||||
{ icon: "plus", label: "追加機能", text: "前日、当日、翌日の運用の投稿が可能" },
|
||||
];
|
||||
|
||||
const ELESITE_FEATURES: Feature[] = [
|
||||
{ icon: "calendar-today", label: "運用データ", text: "当日に報告のあった運用情報のみ表示" },
|
||||
{ icon: "map-outline", label: "対象エリア", text: "予讃線/瀬戸大橋線(なお直通している特急などの列番は含みます)" },
|
||||
{ icon: "train", label: "対象運用", text: "JR四国管内営業列車対応、臨時列車/突発運用は非対応" },
|
||||
];
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* DataSourceSettings */
|
||||
/* ------------------------------------------------------------------ */
|
||||
export const DataSourceSettings = () => {
|
||||
const navigation = useNavigation();
|
||||
const { updatePermission, dataSourcePermission } = useTrainMenu();
|
||||
const { colors, fixed } = useThemeColors();
|
||||
const canAccess = updatePermission || Object.values(dataSourcePermission).some(Boolean);
|
||||
const [useUnyohub, setUseUnyohub] = useState(false);
|
||||
const [useElesite, setUseElesite] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
AS.getItem(STORAGE_KEYS.USE_UNYOHUB).then((value) => {
|
||||
setUseUnyohub(value === true || value === "true");
|
||||
});
|
||||
AS.getItem(STORAGE_KEYS.USE_ELESITE).then((value) => {
|
||||
setUseElesite(value === true || value === "true");
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleToggleUnyohub = (value: boolean) => {
|
||||
setUseUnyohub(value);
|
||||
AS.setItem(STORAGE_KEYS.USE_UNYOHUB, value.toString());
|
||||
};
|
||||
|
||||
const handleToggleElesite = (value: boolean) => {
|
||||
setUseElesite(value);
|
||||
AS.setItem(STORAGE_KEYS.USE_ELESITE, value.toString());
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={[styles.container, { backgroundColor: fixed.primary }]}>
|
||||
<SheetHeaderItem
|
||||
title="情報ソース設定"
|
||||
LeftItem={{
|
||||
title: "戻る",
|
||||
onPress: () => navigation.goBack(),
|
||||
position: "left",
|
||||
}}
|
||||
/>
|
||||
{!canAccess ? (
|
||||
<View style={[styles.noPermissionContainer, { backgroundColor: colors.backgroundSecondary }]}>
|
||||
<Text style={[styles.noPermissionText, { color: colors.textPrimary }]}>この設定にアクセスする権限がありません。</Text>
|
||||
<Text style={[styles.noPermissionSubText, { color: colors.textSecondary }]}>鉄道運用Hubまたはアプリ管理者の権限が必要です。</Text>
|
||||
</View>
|
||||
) : (
|
||||
<ScrollView style={[styles.content, { backgroundColor: colors.backgroundSecondary }]} contentContainerStyle={styles.contentInner}>
|
||||
<Text style={[styles.sectionTitle, { color: colors.textTertiary }]}>外部データソース</Text>
|
||||
|
||||
<DataSourceAccordionCard
|
||||
logo={HUB_LOGO_PNG}
|
||||
accentColor="#0099CC"
|
||||
title="鉄道運用Hub"
|
||||
tagline="コミュニティによる列車運用情報サービス"
|
||||
enabled={useUnyohub}
|
||||
onToggle={handleToggleUnyohub}
|
||||
description={
|
||||
"鉄道運用Hubはオープンソースのユーザー投稿型鉄道運用情報データベースアプリケーションです。JR 四国をはじめ全国多数の路線系統に対応しています。\n\nデータがある列車では地図上にアイコンでマークが表示され、列車情報画面の編成表示も更新されます。"
|
||||
}
|
||||
features={UNYOHUB_FEATURES}
|
||||
linkLabel="unyohub.2pd.jp を開く(JR四国)"
|
||||
linkUrl="https://unyohub.2pd.jp/railroad_shikoku/"
|
||||
/>
|
||||
|
||||
<DataSourceAccordionCard
|
||||
logo={ELESITE_LOGO_PNG}
|
||||
accentColor="#44bb44"
|
||||
title="えれサイト"
|
||||
tagline="コミュニティによる列車運用情報サービス"
|
||||
enabled={useElesite}
|
||||
onToggle={handleToggleElesite}
|
||||
description={
|
||||
"えれサイトは、鉄道運用情報を共有するためのサイトです。皆様からの投稿を通じて、鉄道運行に関する情報を共有するサイトです。JR 四国の特急・普通列車を中心に対応しています。\n\nデータがある列車では地図上に緑色の「E」バッジが表示され、列車情報画面の編成表示も更新されます。"
|
||||
}
|
||||
features={ELESITE_FEATURES}
|
||||
linkLabel="elesite-next.com を開く"
|
||||
linkUrl="https://www.elesite-next.com/"
|
||||
/>
|
||||
|
||||
<View style={[styles.infoSection, { backgroundColor: colors.backgroundTertiary }]}>
|
||||
<Text style={[styles.infoText, { color: colors.textCaution }]}>
|
||||
外部のコミュニティデータソースとの連携を管理します。
|
||||
{"\n\n"}
|
||||
データの正確性は保証されません。また、これらの連携情報を利用する時点でそれぞれのサイトの利用規約に同意したものとします。{"\n\n"}外部ソースはJR四国非公式アプリが管理していないデータであるため、お問い合わせは各サービスの窓口までお願いいたします。
|
||||
</Text>
|
||||
</View>
|
||||
</ScrollView>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
/* ── 権限なし ── */
|
||||
noPermissionContainer: {
|
||||
flex: 1,
|
||||
backgroundColor: "#f8f8fc",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
padding: 30,
|
||||
gap: 10,
|
||||
},
|
||||
noPermissionText: {
|
||||
fontSize: 16,
|
||||
fontWeight: "bold",
|
||||
color: "#333",
|
||||
textAlign: "center",
|
||||
},
|
||||
noPermissionSubText: {
|
||||
fontSize: 13,
|
||||
color: "#666",
|
||||
textAlign: "center",
|
||||
},
|
||||
/* ── レイアウト ── */
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: "#0099CC",
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
backgroundColor: "#f8f8fc",
|
||||
},
|
||||
contentInner: {
|
||||
paddingHorizontal: 14,
|
||||
paddingBottom: 40,
|
||||
gap: 12,
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: 13,
|
||||
fontWeight: "600",
|
||||
color: "#888",
|
||||
letterSpacing: 0.5,
|
||||
marginTop: 20,
|
||||
marginLeft: 4,
|
||||
},
|
||||
/* ── アコーディオンカード ── */
|
||||
accordionCard: {
|
||||
backgroundColor: "#fff",
|
||||
borderRadius: 14,
|
||||
borderWidth: 1,
|
||||
borderColor: "#e4e4e4",
|
||||
overflow: "hidden",
|
||||
},
|
||||
accordionCardEnabled: {
|
||||
borderColor: "#0099CC44",
|
||||
},
|
||||
accordionHeader: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 14,
|
||||
paddingTop: 14,
|
||||
paddingBottom: 6,
|
||||
gap: 10,
|
||||
},
|
||||
accordionLogo: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 8,
|
||||
flexShrink: 0,
|
||||
},
|
||||
accordionTitles: {
|
||||
flex: 1,
|
||||
gap: 2,
|
||||
},
|
||||
accordionTitle: {
|
||||
fontSize: 15,
|
||||
fontWeight: "bold",
|
||||
color: "#111",
|
||||
},
|
||||
accordionTagline: {
|
||||
fontSize: 11,
|
||||
color: "#888",
|
||||
},
|
||||
accordionSwitch: {
|
||||
flexShrink: 0,
|
||||
},
|
||||
accordionStatusRow: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 6,
|
||||
paddingHorizontal: 14,
|
||||
paddingBottom: 10,
|
||||
},
|
||||
statusDot: {
|
||||
width: 7,
|
||||
height: 7,
|
||||
borderRadius: 4,
|
||||
},
|
||||
statusText: {
|
||||
fontSize: 12,
|
||||
fontWeight: "500",
|
||||
},
|
||||
/* ── 展開トリガー ── */
|
||||
accordionToggleRow: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
paddingHorizontal: 14,
|
||||
paddingVertical: 10,
|
||||
borderTopWidth: StyleSheet.hairlineWidth,
|
||||
borderTopColor: "#ebebeb",
|
||||
},
|
||||
accordionToggleLabel: {
|
||||
fontSize: 12,
|
||||
color: "#666",
|
||||
fontWeight: "500",
|
||||
},
|
||||
/* ── 展開コンテンツ ── */
|
||||
accordionBody: {
|
||||
borderTopWidth: StyleSheet.hairlineWidth,
|
||||
borderTopColor: "#ebebeb",
|
||||
padding: 14,
|
||||
gap: 10,
|
||||
backgroundColor: "#fafafa",
|
||||
},
|
||||
bodyDesc: {
|
||||
fontSize: 12,
|
||||
color: "#444",
|
||||
lineHeight: 19,
|
||||
},
|
||||
bodyFeatures: {
|
||||
gap: 7,
|
||||
borderTopWidth: StyleSheet.hairlineWidth,
|
||||
borderTopColor: "#e4e4e4",
|
||||
paddingTop: 8,
|
||||
},
|
||||
featureRow: {
|
||||
flexDirection: "row",
|
||||
alignItems: "flex-start",
|
||||
gap: 6,
|
||||
},
|
||||
featureIcon: {
|
||||
width: 22,
|
||||
alignItems: "center",
|
||||
paddingTop: 1,
|
||||
flexShrink: 0,
|
||||
},
|
||||
featureLabel: {
|
||||
fontSize: 12,
|
||||
fontWeight: "bold",
|
||||
color: "#333",
|
||||
width: 62,
|
||||
flexShrink: 0,
|
||||
},
|
||||
featureText: {
|
||||
fontSize: 12,
|
||||
color: "#555",
|
||||
flex: 1,
|
||||
lineHeight: 17,
|
||||
},
|
||||
bodyLink: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 5,
|
||||
borderTopWidth: StyleSheet.hairlineWidth,
|
||||
borderTopColor: "#e4e4e4",
|
||||
paddingTop: 8,
|
||||
marginTop: 2,
|
||||
},
|
||||
bodyLinkText: {
|
||||
fontSize: 12,
|
||||
color: "#555",
|
||||
},
|
||||
/* ── 注意書き ── */
|
||||
infoSection: {
|
||||
backgroundColor: "#fff3cd",
|
||||
borderRadius: 10,
|
||||
padding: 14,
|
||||
},
|
||||
infoText: {
|
||||
fontSize: 13,
|
||||
color: "#856404",
|
||||
lineHeight: 18,
|
||||
},
|
||||
});
|
||||
@@ -4,8 +4,10 @@ import { View, Text, TouchableOpacity, LayoutAnimation } from "react-native";
|
||||
import lineColorList from "../../../assets/originData/lineColorList";
|
||||
import Ionicons from "react-native-vector-icons/Ionicons";
|
||||
import { AS } from "../../../storageControl";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
|
||||
export const FavoriteSettingsItem = ({ currentStation }) => {
|
||||
const { colors, fixed } = useThemeColors();
|
||||
const lineIDs = [];
|
||||
const EachIDs = [];
|
||||
currentStation.forEach((d) => {
|
||||
@@ -16,7 +18,7 @@ export const FavoriteSettingsItem = ({ currentStation }) => {
|
||||
});
|
||||
|
||||
return (
|
||||
<View style={{ flexDirection: "row", backgroundColor: "white" }}>
|
||||
<View style={{ flexDirection: "row", backgroundColor: colors.background }}>
|
||||
<View
|
||||
style={{
|
||||
width: 35,
|
||||
@@ -37,7 +39,7 @@ export const FavoriteSettingsItem = ({ currentStation }) => {
|
||||
<View style={{ flex: 1 }} />
|
||||
<Text
|
||||
style={{
|
||||
color: "white",
|
||||
color: fixed.textOnPrimary,
|
||||
textAlign: "center",
|
||||
fontSize: 12,
|
||||
fontWeight: "bold",
|
||||
@@ -57,13 +59,13 @@ export const FavoriteSettingsItem = ({ currentStation }) => {
|
||||
padding: 8,
|
||||
flexDirection: "row",
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: "#f0f0f0",
|
||||
borderBottomColor: colors.borderLight,
|
||||
flex: 1,
|
||||
alignContent: "center",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<Text style={{ fontSize: 20 }}>{currentStation[0].Station_JP}</Text>
|
||||
<Text style={{ fontSize: 20, color: colors.text }}>{currentStation[0].Station_JP}</Text>
|
||||
<View style={{ flex: 1 }} />
|
||||
</View>
|
||||
<View
|
||||
@@ -73,7 +75,7 @@ export const FavoriteSettingsItem = ({ currentStation }) => {
|
||||
alignSelf: "center",
|
||||
}}
|
||||
>
|
||||
<Ionicons name={"reorder-two"} size={20} style={{ marginHorizontal: 10 }} />
|
||||
<Ionicons name={"reorder-two"} size={20} color={colors.text} style={{ marginHorizontal: 10 }} />
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -8,11 +8,13 @@ import { FavoriteSettingsItem } from "./FavoliteSettings/FavoiliteSettingsItem";
|
||||
import { SheetHeaderItem } from "@/components/atom/SheetHeaderItem";
|
||||
import { AS } from "@/storageControl";
|
||||
import { STORAGE_KEYS } from "@/constants";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
|
||||
export const FavoriteSettings = () => {
|
||||
const { favoriteStation, setFavoriteStation } = useFavoriteStation();
|
||||
const scrollableRef = useAnimatedRef();
|
||||
const { goBack } = useNavigation();
|
||||
const { colors, fixed } = useThemeColors();
|
||||
const renderItem = useCallback((props) => {
|
||||
const { item, index } = props;
|
||||
return (
|
||||
@@ -20,13 +22,13 @@ export const FavoriteSettings = () => {
|
||||
);
|
||||
}, []);
|
||||
return (
|
||||
<View style={{ height: "100%", backgroundColor: "#0099CC" }}>
|
||||
<View style={{ height: "100%", backgroundColor: fixed.primary }}>
|
||||
<SheetHeaderItem
|
||||
title="お気に入り設定"
|
||||
LeftItem={{ title: "< 設定", onPress: goBack }}
|
||||
/>
|
||||
<Animated.ScrollView
|
||||
style={{ flex: 1, backgroundColor: "white" }}
|
||||
style={{ flex: 1, backgroundColor: colors.background }}
|
||||
contentContainerStyle={styles.contentContainer}
|
||||
ref={scrollableRef}
|
||||
>
|
||||
@@ -56,7 +58,7 @@ export const FavoriteSettings = () => {
|
||||
</Animated.ScrollView>
|
||||
<Text
|
||||
style={{
|
||||
backgroundColor: "white",
|
||||
backgroundColor: colors.background,
|
||||
borderWidth: 1,
|
||||
borderStyle: "solid",
|
||||
}}
|
||||
|
||||
325
components/Settings/FelicaHistoryPage.tsx
Normal file
325
components/Settings/FelicaHistoryPage.tsx
Normal file
@@ -0,0 +1,325 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
ScrollView,
|
||||
ActivityIndicator,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
Platform,
|
||||
} from "react-native";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import { MaterialCommunityIcons } from "@expo/vector-icons";
|
||||
import * as Clipboard from "expo-clipboard";
|
||||
import { BigButton } from "../atom/BigButton";
|
||||
import { SheetHeaderItem } from "../atom/SheetHeaderItem";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
import * as ExpoFelicaReader from "../../modules/expo-felica-reader/src";
|
||||
import { saveWidgetData } from "@/modules/expo-felica-reader/src";
|
||||
import type { FelicaCardInfo, FelicaHistoryEntry } from "../../modules/expo-felica-reader/src";
|
||||
import { lookupFelicaStation } from "../../lib/felicaStationMap";
|
||||
import { AS } from "../../storageControl";
|
||||
import { STORAGE_KEYS } from "../../constants";
|
||||
import { requestWidgetUpdate } from "react-native-android-widget";
|
||||
import {
|
||||
FelicaQuickAccessWidget,
|
||||
getFelicaQuickAccessData,
|
||||
} from "../AndroidWidget/FelicaQuickAccessWidget";
|
||||
|
||||
// IDm先頭2バイト(4 hex chars)→ カード種別
|
||||
// FeliCa Networks が発行者ごとに Manufacture Code を割り当てている
|
||||
const IDM_PREFIX_LABEL: Record<string, string> = {
|
||||
"0308": "Suica",
|
||||
"0316": "Kitaca",
|
||||
"0311": "icsca",
|
||||
"0313": "nimoca",
|
||||
"0315": "はやかけん",
|
||||
"031d": "ICOCA",
|
||||
"0321": "SUGOCA",
|
||||
"0350": "TOICA",
|
||||
"030d": "manaca",
|
||||
"0520": "PASMO",
|
||||
"0b04": "PiTaPa",
|
||||
};
|
||||
|
||||
/**
|
||||
* IDmの先頭2バイト(発行者コード)からカード種別を返す。
|
||||
* 不明な場合は "交通系ICカード" を返す。
|
||||
*/
|
||||
function cardTypeLabel(idm: string): string {
|
||||
const prefix = idm.substring(0, 4).toLowerCase();
|
||||
return IDM_PREFIX_LABEL[prefix] ?? "交通系ICカード";
|
||||
}
|
||||
|
||||
// 処理種別コード → ラベル
|
||||
const PROCESS_TYPE_LABEL: Record<number, string> = {
|
||||
0x01: "改札入場",
|
||||
0x02: "改札出場",
|
||||
0x03: "乗継入場",
|
||||
0x04: "乗継出場",
|
||||
0x0f: "バス乗降",
|
||||
0x14: "タクシー",
|
||||
0x46: "物販",
|
||||
0x62: "チャージ",
|
||||
};
|
||||
|
||||
// byte[6-9] が駅コードではなく端末ID等として使われる processType
|
||||
// (metrodroid の isProductSale / CONSOLE_CHARGE 相当)
|
||||
// terminalType (byte[0]) が 0xC7(POS) / 0xC8(自販機) の場合も同様
|
||||
const NON_TRANSIT_PROCESS_TYPES = new Set([
|
||||
0x46, // 物販
|
||||
0x62, // チャージ
|
||||
0x14, // タクシー
|
||||
]);
|
||||
const NON_TRANSIT_TERMINAL_TYPES = new Set([
|
||||
0xc7, // POS端末
|
||||
0xc8, // 自動販売機
|
||||
]);
|
||||
|
||||
function processLabel(processType: number): string {
|
||||
return PROCESS_TYPE_LABEL[processType] ?? `0x${processType.toString(16).toUpperCase().padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
function stationLabel(regionCode: number, lineCode: number, stationCode: number): string {
|
||||
if (lineCode === 0 && stationCode === 0) return "";
|
||||
const entry = lookupFelicaStation(regionCode, lineCode, stationCode);
|
||||
if (entry) return entry.s;
|
||||
return `L${lineCode} S${stationCode}`;
|
||||
}
|
||||
|
||||
function HistoryRow({ entry, index, prevBalance }: { entry: FelicaHistoryEntry; index: number; prevBalance: number | null }) {
|
||||
const { colors } = useThemeColors();
|
||||
const dateStr = `${entry.year}/${String(entry.month).padStart(2, "0")}/${String(entry.day).padStart(2, "0")}`;
|
||||
const label = processLabel(entry.processType);
|
||||
const regionCode = entry.regionCode ?? 0;
|
||||
|
||||
// 物販・チャージ・POS端末などの場合は byte[6-9] が駅コードではないので表示しない
|
||||
const isTransit =
|
||||
!NON_TRANSIT_PROCESS_TYPES.has(entry.processType) &&
|
||||
!NON_TRANSIT_TERMINAL_TYPES.has(entry.terminalType);
|
||||
|
||||
const inStationLabel = isTransit ? stationLabel(regionCode, entry.inLineCode, entry.inStationCode) : "";
|
||||
const outStationLabel = isTransit ? stationLabel(regionCode, entry.outLineCode, entry.outStationCode) : "";
|
||||
const showStations = inStationLabel !== "" || outStationLabel !== "";
|
||||
|
||||
// 支払い金額 = 前の残高 - この取引後の残高(チャージは負→+表示)
|
||||
const amount = prevBalance != null ? prevBalance - entry.balance : null;
|
||||
const amountText = amount == null
|
||||
? `残高 ¥${entry.balance.toLocaleString()}`
|
||||
: amount > 0
|
||||
? `-¥${amount.toLocaleString()}`
|
||||
: amount < 0
|
||||
? `+¥${(-amount).toLocaleString()}`
|
||||
: "¥0";
|
||||
const amountColor = amount == null ? "#999" : amount < 0 ? "#00897B" : "#0099CC";
|
||||
|
||||
const debugText = JSON.stringify({ ...entry, inStationLabel, outStationLabel }, null, 2);
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
onLongPress={() => {
|
||||
Clipboard.setStringAsync(debugText);
|
||||
}}
|
||||
style={[styles.row, { borderBottomColor: colors.border, backgroundColor: index % 2 === 0 ? colors.background : colors.backgroundSecondary }]}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<View style={styles.rowLeft}>
|
||||
<Text style={[styles.dateText, { color: colors.textTertiary }]}>{dateStr}</Text>
|
||||
<Text style={[styles.labelText, { color: colors.textPrimary }]}>{label}</Text>
|
||||
{showStations ? (
|
||||
<Text style={[styles.stationText, { color: colors.textSecondary }]}>
|
||||
{outStationLabel
|
||||
? `${inStationLabel} → ${outStationLabel}`
|
||||
: inStationLabel}
|
||||
</Text>
|
||||
) : null}
|
||||
</View>
|
||||
<View style={styles.rowRight}>
|
||||
<Text style={[styles.amountText, { color: amountColor }]}>
|
||||
{amountText}
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
export function FelicaHistoryPage() {
|
||||
const { goBack } = useNavigation();
|
||||
const { colors, fixed } = useThemeColors();
|
||||
const [scanning, setScanning] = useState(false);
|
||||
const [result, setResult] = useState<FelicaCardInfo | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const handleScan = async () => {
|
||||
setScanning(true);
|
||||
setResult(null);
|
||||
setError(null);
|
||||
try {
|
||||
const data = await ExpoFelicaReader.scan();
|
||||
console.log("NFC Scan Result:", data);
|
||||
setResult(data);
|
||||
|
||||
if (data.balance >= 0) {
|
||||
await AS.setItem(STORAGE_KEYS.FELICA_LAST_SNAPSHOT, {
|
||||
balance: data.balance,
|
||||
idm: data.idm,
|
||||
systemCode: data.systemCode,
|
||||
scannedAt: new Date().toLocaleString("ja-JP"),
|
||||
});
|
||||
// iOS ウィジェットにも残高データを同期
|
||||
saveWidgetData("felicaLastSnapshot", {
|
||||
balance: data.balance,
|
||||
idm: data.idm,
|
||||
systemCode: data.systemCode,
|
||||
scannedAt: new Date().toLocaleString("ja-JP"),
|
||||
});
|
||||
}
|
||||
|
||||
if (Platform.OS === "android") {
|
||||
await requestWidgetUpdate({
|
||||
widgetName: "JR_shikoku_felica_balance",
|
||||
renderWidget: async () => {
|
||||
const quickData = await getFelicaQuickAccessData();
|
||||
return <FelicaQuickAccessWidget {...quickData} />;
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (e: any) {
|
||||
setError(e?.message ?? "読み取りに失敗しました");
|
||||
} finally {
|
||||
setScanning(false);
|
||||
}
|
||||
};
|
||||
|
||||
// ページを開いたら自動でスキャン待機を開始
|
||||
useEffect(() => {
|
||||
handleScan();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<View style={[styles.container, { backgroundColor: fixed.primary }]}>
|
||||
<SheetHeaderItem title="ICカード残高・履歴" />
|
||||
<ScrollView style={[styles.scroll, { backgroundColor: colors.background }]} contentContainerStyle={styles.scrollContent}>
|
||||
{/* スキャン中 */}
|
||||
{scanning && (
|
||||
<View style={styles.scanningBox}>
|
||||
<MaterialCommunityIcons name="nfc-search-variant" size={72} color="#0099CC" style={{ marginBottom: 12 }} />
|
||||
<ActivityIndicator color="#0099CC" size="small" style={{ marginBottom: 8 }} />
|
||||
<Text style={[styles.scanningText, { color: colors.textAccent }]}>ICカードをかざしてください</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* エラー */}
|
||||
{error && (
|
||||
<View style={[styles.errorBox, { backgroundColor: colors.backgroundSecondary }]}>
|
||||
<Text style={[styles.errorText, { color: colors.textError }]}>{error}</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 結果 */}
|
||||
{result && (
|
||||
<>
|
||||
{/* 残高カード */}
|
||||
<View style={[styles.balanceCard, { borderColor: fixed.primary, backgroundColor: colors.backgroundSecondary }]}>
|
||||
<Text style={[styles.cardTypeText, { color: colors.textAccent }]}>{cardTypeLabel(result.idm)}</Text>
|
||||
<Text style={[styles.balanceLabel, { color: colors.textSecondary }]}>残高</Text>
|
||||
<Text style={[styles.balanceAmount, { color: colors.textAccent }]}>
|
||||
{result.balance >= 0 ? `¥${result.balance.toLocaleString()}` : "読み取り失敗"}
|
||||
</Text>
|
||||
<Text style={[styles.idmText, { color: colors.textTertiary }]}>IDm: {result.idm}</Text>
|
||||
</View>
|
||||
|
||||
{/* 履歴リスト */}
|
||||
<Text style={[styles.sectionTitle, { color: colors.textPrimary }]}>
|
||||
利用履歴{result.history.length > 0 ? `(${result.history.length}件)` : ""}
|
||||
</Text>
|
||||
{result.history.length === 0 ? (
|
||||
<Text style={[styles.emptyText, { color: colors.textTertiary }]}>履歴を取得できませんでした</Text>
|
||||
) : (
|
||||
result.history.map((entry, i) => (
|
||||
<HistoryRow
|
||||
key={i}
|
||||
entry={entry}
|
||||
index={i}
|
||||
prevBalance={result.history[i + 1]?.balance ?? null}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</ScrollView>
|
||||
|
||||
{/* 下部ボタン */}
|
||||
<BigButton string={scanning ? "スキャン中…" : "再スキャン"} onPress={handleScan} style={{ opacity: scanning ? 0.5 : 1 }}>
|
||||
<MaterialCommunityIcons name="contactless-payment" color="white" size={28} style={{ marginRight: 8 }} />
|
||||
</BigButton>
|
||||
<BigButton string="閉じる" onPress={goBack} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: { height: "100%", backgroundColor: "#0099CC" },
|
||||
scroll: { flex: 1, backgroundColor: "white" },
|
||||
scrollContent: { paddingBottom: 32 },
|
||||
|
||||
scanningBox: {
|
||||
margin: 16,
|
||||
paddingVertical: 32,
|
||||
alignItems: "center",
|
||||
},
|
||||
scanningText: { fontSize: 18, fontWeight: "bold", color: "#0099CC" },
|
||||
|
||||
errorBox: {
|
||||
margin: 16,
|
||||
padding: 12,
|
||||
borderRadius: 8,
|
||||
backgroundColor: "#fdecea",
|
||||
borderWidth: 1,
|
||||
borderColor: "#f44336",
|
||||
},
|
||||
errorText: { color: "#c62828", fontSize: 14 },
|
||||
|
||||
balanceCard: {
|
||||
margin: 16,
|
||||
padding: 20,
|
||||
borderRadius: 12,
|
||||
backgroundColor: "#e3f2fd",
|
||||
borderWidth: 1,
|
||||
borderColor: "#0099CC",
|
||||
alignItems: "center",
|
||||
},
|
||||
cardTypeText: { fontSize: 13, color: "#0099CC", fontWeight: "bold", marginBottom: 8 },
|
||||
balanceLabel: { fontSize: 14, color: "#555", marginBottom: 4 },
|
||||
balanceAmount: { fontSize: 36, fontWeight: "bold", color: "#0099CC" },
|
||||
idmText: { fontSize: 11, color: "#888", marginTop: 6 },
|
||||
|
||||
sectionTitle: {
|
||||
fontSize: 16,
|
||||
fontWeight: "bold",
|
||||
color: "#333",
|
||||
marginHorizontal: 16,
|
||||
marginTop: 8,
|
||||
marginBottom: 4,
|
||||
},
|
||||
emptyText: { color: "#999", marginHorizontal: 16, marginTop: 8 },
|
||||
|
||||
row: {
|
||||
flexDirection: "row",
|
||||
paddingVertical: 10,
|
||||
paddingHorizontal: 16,
|
||||
alignItems: "center",
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
borderBottomColor: "#ddd",
|
||||
},
|
||||
rowEven: { backgroundColor: "white" },
|
||||
rowOdd: { backgroundColor: "#f5f9fc" },
|
||||
rowLeft: { flex: 1 },
|
||||
rowRight: { alignItems: "flex-end" },
|
||||
|
||||
dateText: { fontSize: 12, color: "#666" },
|
||||
labelText: { fontSize: 15, fontWeight: "bold", color: "#333", marginTop: 2 },
|
||||
stationText: { fontSize: 12, color: "#777", marginTop: 1 },
|
||||
timeText: { fontSize: 12, color: "#999", marginTop: 1 },
|
||||
amountText: { fontSize: 16, fontWeight: "bold" },
|
||||
});
|
||||
@@ -16,15 +16,17 @@ import icons from "../../assets/icons/icons";
|
||||
import { setAlternateAppIcon, getAppIconName } from "expo-alternate-app-icons";
|
||||
import { widthPercentageToDP } from "react-native-responsive-screen";
|
||||
import { SheetHeaderItem } from "../atom/SheetHeaderItem";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
|
||||
export const LauncherIconSettings = () => {
|
||||
const { goBack } = useNavigation();
|
||||
const [iconList] = useState(icons());
|
||||
const [currentIcon] = useState(getAppIconName());
|
||||
const { colors, fixed } = useThemeColors();
|
||||
return (
|
||||
<View style={{ height: "100%", backgroundColor: "#0099CC" }}>
|
||||
<View style={{ height: "100%", backgroundColor: fixed.primary }}>
|
||||
<SheetHeaderItem title="アイコン設定" LeftItem={{ title: "< 設定", onPress: goBack }} />
|
||||
<ScrollView style={{ flex: 1, backgroundColor: "white" }}>
|
||||
<ScrollView style={{ flex: 1, backgroundColor: colors.background }}>
|
||||
{currentIcon ? (
|
||||
<>
|
||||
<Text
|
||||
@@ -45,19 +47,22 @@ export const LauncherIconSettings = () => {
|
||||
padding: 10,
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
source={iconList.filter(({ id }) => id == currentIcon)[0].icon}
|
||||
<View
|
||||
style={{
|
||||
width: 50,
|
||||
height: 50,
|
||||
padding: 30,
|
||||
borderWidth: 1,
|
||||
borderRadius: 10,
|
||||
borderColor: "white",
|
||||
borderColor: colors.border,
|
||||
margin: 10,
|
||||
backgroundColor: "white",
|
||||
padding: 10,
|
||||
backgroundColor: colors.background,
|
||||
}}
|
||||
/>
|
||||
>
|
||||
<Image
|
||||
source={iconList.filter(({ id }) => id == currentIcon)[0].icon}
|
||||
style={{ width: 80, height: 80, borderRadius: 8 }}
|
||||
resizeMode="contain"
|
||||
/>
|
||||
</View>
|
||||
<Text>JR四国非公式アプリ</Text>
|
||||
</View>
|
||||
</>
|
||||
|
||||
@@ -2,9 +2,10 @@ import React from "react";
|
||||
import { View, Text, TouchableOpacity, ScrollView } from "react-native";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import { SwitchArea } from "../atom/SwitchArea";
|
||||
import { CheckBox } from "react-native-elements";
|
||||
import { Button, CheckBox } from "@rneui/themed";
|
||||
import { TripleSwitchArea } from "../atom/TripleSwitchArea";
|
||||
import { SheetHeaderItem } from "../atom/SheetHeaderItem";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
|
||||
export const LayoutSettings = ({
|
||||
iconSetting,
|
||||
@@ -24,12 +25,14 @@ export const LayoutSettings = ({
|
||||
headerSize,
|
||||
setHeaderSize,
|
||||
}) => {
|
||||
const { goBack } = useNavigation();
|
||||
const { goBack, navigate } = useNavigation() as any;
|
||||
const { colors, fixed } = useThemeColors();
|
||||
return (
|
||||
<View style={{ height: "100%", backgroundColor: "#0099CC" }}>
|
||||
<View style={{ height: "100%", backgroundColor: fixed.primary }}>
|
||||
<SheetHeaderItem title="レイアウト設定" LeftItem={{ title: "< 設定", onPress: goBack }} />
|
||||
<ScrollView style={{ flex: 1, backgroundColor: "white" }}>
|
||||
<ScrollView style={{ flex: 1, backgroundColor: colors.background }}>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Button title="ICカード残高・履歴" onPress={() => navigate("FelicaHistoryPage")} />
|
||||
<SwitchArea
|
||||
str="列車アイコン表示"
|
||||
bool={iconSetting}
|
||||
@@ -117,11 +120,13 @@ export const LayoutSettings = ({
|
||||
);
|
||||
};
|
||||
|
||||
const SimpleSwitch = ({ bool, setBool, str }) => (
|
||||
const SimpleSwitch = ({ bool, setBool, str }) => {
|
||||
const { colors } = useThemeColors();
|
||||
return (
|
||||
<View style={{ flexDirection: "row" }}>
|
||||
<CheckBox
|
||||
checked={bool == "true" ? true : false}
|
||||
checkedColor="red"
|
||||
checkedColor={colors.switchActive}
|
||||
onPress={() => setBool(bool == "true" ? "false" : "true")}
|
||||
containerStyle={{
|
||||
flex: 1,
|
||||
@@ -129,8 +134,9 @@ const SimpleSwitch = ({ bool, setBool, str }) => (
|
||||
borderColor: "white",
|
||||
alignContent: "center",
|
||||
}}
|
||||
textStyle={{ fontSize: 20, fontWeight: "normal" }}
|
||||
textStyle={{ fontSize: 20, fontWeight: "normal", color: colors.text }}
|
||||
title={str}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,16 +2,18 @@ import React, { useEffect, useState } from "react";
|
||||
import { View, Text, TouchableOpacity, ScrollView } from "react-native";
|
||||
import * as Clipboard from "expo-clipboard";
|
||||
|
||||
import { CheckBox } from "react-native-elements";
|
||||
import { CheckBox } from "@rneui/themed";
|
||||
import { AS } from "../../storageControl";
|
||||
import { STORAGE_KEYS } from "@/constants";
|
||||
import { useNotification } from "../../stateBox/useNotifications";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import { SheetHeaderItem } from "../atom/SheetHeaderItem";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
|
||||
export const NotificationSettings = () => {
|
||||
const { expoPushToken } = useNotification();
|
||||
const { goBack } = useNavigation();
|
||||
const { colors, fixed } = useThemeColors();
|
||||
const [traInfoEX, setTraInfoEX] = useState(false);
|
||||
const [informations, setInformations] = useState(false);
|
||||
const [strangeTrain, setStrangeTrain] = useState(false);
|
||||
@@ -45,13 +47,13 @@ export const NotificationSettings = () => {
|
||||
});
|
||||
};
|
||||
return (
|
||||
<View style={{ height: "100%", backgroundColor: "#0099CC" }}>
|
||||
<View style={{ height: "100%", backgroundColor: fixed.primary }}>
|
||||
<SheetHeaderItem
|
||||
title="通知設定(β)"
|
||||
LeftItem={{ title: "< 設定", onPress: goBack }}
|
||||
RightItem={{ title: "登録実行", onPress: setRegister }}
|
||||
/>
|
||||
<ScrollView style={{ flex: 1, backgroundColor: "white" }}>
|
||||
<ScrollView style={{ flex: 1, backgroundColor: colors.background }}>
|
||||
<SimpleSwitch
|
||||
bool={traInfoEX}
|
||||
setBool={setTraInfoEX}
|
||||
@@ -78,11 +80,13 @@ export const NotificationSettings = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const SimpleSwitch = ({ bool, setBool, str }) => (
|
||||
const SimpleSwitch = ({ bool, setBool, str }) => {
|
||||
const { colors } = useThemeColors();
|
||||
return (
|
||||
<View style={{ flexDirection: "row" }}>
|
||||
<CheckBox
|
||||
checked={bool == "true" ? true : false}
|
||||
checkedColor="red"
|
||||
checkedColor={colors.switchActive}
|
||||
onPress={() => setBool(bool == "true" ? "false" : "true")}
|
||||
containerStyle={{
|
||||
flex: 1,
|
||||
@@ -90,8 +94,9 @@ const SimpleSwitch = ({ bool, setBool, str }) => (
|
||||
borderColor: "white",
|
||||
alignContent: "center",
|
||||
}}
|
||||
textStyle={{ fontSize: 20, fontWeight: "normal" }}
|
||||
textStyle={{ fontSize: 20, fontWeight: "normal", color: colors.text }}
|
||||
title={str}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from "react";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
@@ -16,8 +16,17 @@ import TouchableScale from "react-native-touchable-scale";
|
||||
import { SwitchArea } from "../atom/SwitchArea";
|
||||
import { useNotification } from "../../stateBox/useNotifications";
|
||||
import { SheetHeaderItem } from "@/components/atom/SheetHeaderItem";
|
||||
import { useTrainMenu } from "../../stateBox/useTrainMenu";
|
||||
import { Asset } from "expo-asset";
|
||||
import {
|
||||
useAudioPlayer,
|
||||
setAudioModeAsync,
|
||||
} from "expo-audio";
|
||||
import type { AudioSource } from "expo-audio";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
|
||||
const versionCode = "6.2.1"; // Update this version code as needed
|
||||
const versionCode = "6.2.1.1"; // Update this version code as needed
|
||||
const settingsPreviewSound = require("../../assets/sound/rikka-test.mp3");
|
||||
|
||||
export const SettingTopPage = ({
|
||||
testNFC,
|
||||
@@ -27,33 +36,96 @@ export const SettingTopPage = ({
|
||||
}) => {
|
||||
const { width } = useWindowDimensions();
|
||||
const { expoPushToken } = useNotification();
|
||||
const { updatePermission, dataSourcePermission } = useTrainMenu();
|
||||
const { colors, fixed } = useThemeColors();
|
||||
const navigation = useNavigation();
|
||||
|
||||
// expo-asset でローカルパスを取得し、expo-audio に渡す
|
||||
// (SDK 52 の expo-audio は file:// URI を正しく処理できないため prefix を除去)
|
||||
const [resolvedSource, setResolvedSource] = useState<AudioSource>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
const resolve = async () => {
|
||||
try {
|
||||
const asset = Asset.fromModule(settingsPreviewSound);
|
||||
await asset.downloadAsync();
|
||||
const localUri = asset.localUri;
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
// file:// を剥がしてパスだけにする
|
||||
// (expo-audio ネイティブ AudioModule.kt の File(uri) バグ回避)
|
||||
const strippedPath = localUri
|
||||
? localUri.replace(/^file:\/\//, "")
|
||||
: null;
|
||||
|
||||
if (strippedPath) {
|
||||
setResolvedSource({ uri: strippedPath });
|
||||
}
|
||||
} catch (error) {
|
||||
if (!mounted) return;
|
||||
console.warn("Failed to resolve audio asset", error);
|
||||
}
|
||||
};
|
||||
|
||||
resolve();
|
||||
return () => {
|
||||
mounted = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const previewPlayer = useAudioPlayer(resolvedSource);
|
||||
|
||||
const onPressHeaderImage = useCallback(async () => {
|
||||
try {
|
||||
if (Platform.OS === "ios") {
|
||||
await setAudioModeAsync({
|
||||
playsInSilentMode: false,
|
||||
shouldPlayInBackground: false,
|
||||
interruptionMode: "mixWithOthers",
|
||||
});
|
||||
}
|
||||
if (previewPlayer.playing) previewPlayer.pause();
|
||||
previewPlayer.volume = 1;
|
||||
await previewPlayer.seekTo(0);
|
||||
previewPlayer.play();
|
||||
} catch (error) {
|
||||
console.warn("Failed to play preview sound", error);
|
||||
}
|
||||
}, [previewPlayer]);
|
||||
|
||||
// admin またはいずれかのソース権限を持つ場合のみ表示
|
||||
const canAccessDataSourceSettings =
|
||||
updatePermission || Object.values(dataSourcePermission).some(Boolean);
|
||||
return (
|
||||
<View style={{ height: "100%", backgroundColor: "#0099CC" }}>
|
||||
<View style={{ height: "100%", backgroundColor: fixed.primary }}>
|
||||
<SheetHeaderItem title="アプリの設定画面" LeftItem={{
|
||||
title: "閉じる",
|
||||
onPress: () => navigation.goBack(),
|
||||
}} />
|
||||
<ScrollView style={{ flex: 1, backgroundColor: "#f8f8fc" }}>
|
||||
<ScrollView style={{ flex: 1, backgroundColor: colors.backgroundSecondary }}>
|
||||
<View style={{ height: 300, padding: 10 }}>
|
||||
<View style={{ flex: 1 }} />
|
||||
<Image
|
||||
source={require("../../assets/Header.png")}
|
||||
style={{
|
||||
aspectRatio: 8.08,
|
||||
height: undefined,
|
||||
width: width - 20,
|
||||
borderRadius: 5,
|
||||
}}
|
||||
/>
|
||||
<TouchableOpacity activeOpacity={0.9} onPress={onPressHeaderImage}>
|
||||
<Image
|
||||
source={require("../../assets/Header.png")}
|
||||
style={{
|
||||
aspectRatio: 8.08,
|
||||
height: undefined,
|
||||
width: width - 20,
|
||||
borderRadius: 5,
|
||||
}}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
<View style={{ flexDirection: "row", paddingTop: 10 }}>
|
||||
<View style={{ flex: 1 }} />
|
||||
<Text>内部バージョン: {versionCode}</Text>
|
||||
<Text style={{ color: colors.text }}>内部バージョン: {versionCode}</Text>
|
||||
<View style={{ flex: 1 }} />
|
||||
</View>
|
||||
<View style={{ flexDirection: "row", paddingBottom: 10 }}>
|
||||
<View style={{ flex: 1 }} />
|
||||
<Text>ReleaseChannel: {Updates.channel}</Text>
|
||||
<Text style={{ color: colors.text }}>ReleaseChannel: {Updates.channel}</Text>
|
||||
<View style={{ flex: 1 }} />
|
||||
</View>
|
||||
|
||||
@@ -62,6 +134,7 @@ export const SettingTopPage = ({
|
||||
style={{
|
||||
fontSize: 14,
|
||||
fontStyle: "italic",
|
||||
color: colors.text,
|
||||
}}
|
||||
>
|
||||
このアプリは、四国旅客鉄道株式会社の提供する列車走行位置表示システムを利用し、HARUKIN/Xprocessにより一部の機能を拡張したものです。
|
||||
@@ -70,6 +143,7 @@ export const SettingTopPage = ({
|
||||
style={{
|
||||
fontSize: 14,
|
||||
fontStyle: "italic",
|
||||
color: colors.text,
|
||||
}}
|
||||
>
|
||||
このアプリに関するお問い合わせは、HARUKIN/Xprocessにお願いします。くれぐれも四国旅客鉄道株式会社にはお問い合わせしないようにお願いします。
|
||||
@@ -108,14 +182,15 @@ export const SettingTopPage = ({
|
||||
navigation.navigate("setting", { screen: "LayoutSettings" })
|
||||
}
|
||||
/>
|
||||
{Platform.OS === "android" ? (
|
||||
{canAccessDataSourceSettings && (
|
||||
<SettingList
|
||||
string="ウィジェット設定"
|
||||
string="情報ソース設定"
|
||||
onPress={() =>
|
||||
navigation.navigate("setting", { screen: "WidgetSettings" })
|
||||
navigation.navigate("setting", { screen: "DataSourceSettings" })
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
)}
|
||||
|
||||
<SettingList
|
||||
string="アイコン設定"
|
||||
onPress={() =>
|
||||
@@ -163,7 +238,7 @@ export const SettingTopPage = ({
|
||||
style={{
|
||||
padding: 10,
|
||||
flexDirection: "row",
|
||||
borderColor: "white",
|
||||
borderColor: fixed.textOnPrimary,
|
||||
borderWidth: 1,
|
||||
margin: 10,
|
||||
borderRadius: 5,
|
||||
@@ -172,7 +247,7 @@ export const SettingTopPage = ({
|
||||
onPress={updateAndReload}
|
||||
>
|
||||
<View style={{ flex: 1 }} />
|
||||
<Text style={{ fontSize: 25, fontWeight: "bold", color: "white" }}>
|
||||
<Text style={{ fontSize: 25, fontWeight: "bold", color: fixed.textOnPrimary }}>
|
||||
設定を保存して再読み込み
|
||||
</Text>
|
||||
<View style={{ flex: 1 }} />
|
||||
@@ -182,6 +257,7 @@ export const SettingTopPage = ({
|
||||
};
|
||||
|
||||
const SettingList = ({ string, onPress, disabled }) => {
|
||||
const { colors } = useThemeColors();
|
||||
return (
|
||||
<ListItem
|
||||
activeScale={0.95}
|
||||
@@ -189,9 +265,11 @@ const SettingList = ({ string, onPress, disabled }) => {
|
||||
bottomDivider
|
||||
onPress={onPress}
|
||||
disabled={disabled}
|
||||
// @ts-ignore: containerStyle type mismatch with TouchableScale Component
|
||||
containerStyle={{ backgroundColor: colors.surface }}
|
||||
>
|
||||
<ListItem.Content>
|
||||
<ListItem.Title>{string}</ListItem.Title>
|
||||
<ListItem.Title style={{ color: colors.text }}>{string}</ListItem.Title>
|
||||
</ListItem.Content>
|
||||
<ListItem.Chevron />
|
||||
</ListItem>
|
||||
|
||||
@@ -1,195 +0,0 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { View, Text, TouchableOpacity, ScrollView } from "react-native";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import { CheckBox } from "react-native-elements";
|
||||
import { getWidgetInfo, WidgetPreview } from "react-native-android-widget";
|
||||
import { getDelayData } from "../AndroidWidget/TraInfoEXWidget";
|
||||
import { getInfoString } from "../AndroidWidget/InfoWidget";
|
||||
import { AS } from "../../storageControl";
|
||||
import { nameToWidget } from "../AndroidWidget/widget-task-handler";
|
||||
import { ListItem } from "native-base";
|
||||
import { SheetHeaderItem } from "../atom/SheetHeaderItem";
|
||||
|
||||
export const WidgetSettings = () => {
|
||||
const { JR_shikoku_train_info, Info_Widget } = nameToWidget;
|
||||
const { goBack } = useNavigation();
|
||||
const [time, setTime] = useState();
|
||||
const [delayString, setDelayString] = useState();
|
||||
const [trainInfo, setTrainInfo] = useState();
|
||||
const [widgetList, setWidgetList] = useState([]);
|
||||
const reload = async () => {
|
||||
const d = [];
|
||||
const data = await getWidgetInfo("JR_shikoku_train_info");
|
||||
data.forEach((elem) => {
|
||||
d.push(elem.widgetId);
|
||||
});
|
||||
setWidgetList(d);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
reload();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
getDelayData().then(({ time, delayString }) => {
|
||||
setTime(time);
|
||||
setDelayString(delayString);
|
||||
});
|
||||
getInfoString().then(({ time, text }) => {
|
||||
setTime(time);
|
||||
setTrainInfo(text.toString());
|
||||
});
|
||||
}, []);
|
||||
return (
|
||||
<View style={{ height: "100%", backgroundColor: "#0099CC" }}>
|
||||
<SheetHeaderItem
|
||||
title="ウィジェット設定"
|
||||
LeftItem={{ title: "< 設定", onPress: goBack }}
|
||||
/>
|
||||
<ScrollView style={{ flex: 1, backgroundColor: "white" }}>
|
||||
<View style={{ alignContent: "center", alignItems: "center" }}>
|
||||
<View
|
||||
style={{
|
||||
borderRadius: 15,
|
||||
borderColor: "black",
|
||||
borderWidth: 5,
|
||||
borderStyle: "solid",
|
||||
overflow: "hidden",
|
||||
margin: 10,
|
||||
}}
|
||||
>
|
||||
<WidgetPreview
|
||||
renderWidget={() => (
|
||||
<JR_shikoku_train_info time={time} delayString={delayString} />
|
||||
)}
|
||||
width={400}
|
||||
height={250}
|
||||
/>
|
||||
</View>
|
||||
<View
|
||||
style={{
|
||||
borderRadius: 15,
|
||||
borderColor: "black",
|
||||
borderWidth: 5,
|
||||
borderStyle: "solid",
|
||||
overflow: "hidden",
|
||||
margin: 10,
|
||||
}}
|
||||
>
|
||||
<WidgetPreview
|
||||
renderWidget={() => <Info_Widget time={time} text={trainInfo} />}
|
||||
width={400}
|
||||
height={250}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
<ListItem key={"default"}>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 20,
|
||||
alignItems: "center",
|
||||
alignContent: "center",
|
||||
textAlign: "center",
|
||||
textAlignVertical: "center",
|
||||
marginRight: 10,
|
||||
}}
|
||||
>
|
||||
ID
|
||||
</Text>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 20,
|
||||
alignItems: "center",
|
||||
alignContent: "center",
|
||||
textAlign: "center",
|
||||
textAlignVertical: "center",
|
||||
}}
|
||||
>
|
||||
名前
|
||||
</Text>
|
||||
</ListItem>
|
||||
{widgetList.map((id) => (
|
||||
<WidgetList id={id} key={id} />
|
||||
))}
|
||||
</ScrollView>
|
||||
<Text
|
||||
style={{
|
||||
backgroundColor: "white",
|
||||
borderWidth: 1,
|
||||
borderStyle: "solid",
|
||||
}}
|
||||
>
|
||||
ホーム画面に追加したウィジェットをリストアップします。現状は数を表示するだけですが、ここに各種設定を追加していく予定です。
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const SimpleSwitch = ({ bool, setBool, str }) => (
|
||||
<View style={{ flexDirection: "row" }}>
|
||||
<CheckBox
|
||||
checked={bool == "true" ? true : false}
|
||||
checkedColor="red"
|
||||
onPress={() => setBool(bool == "true" ? "false" : "true")}
|
||||
containerStyle={{
|
||||
flex: 1,
|
||||
backgroundColor: "#00000000",
|
||||
borderColor: "white",
|
||||
alignContent: "center",
|
||||
}}
|
||||
textStyle={{ fontSize: 20, fontWeight: "normal" }}
|
||||
title={str}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
const WidgetList = ({ id }) => {
|
||||
const [widgetConfig, setWidgetConfig] = useState("");
|
||||
const reload = () => {
|
||||
AS.getItem(`widgetType/${id}`)
|
||||
.then((widgetType) => {
|
||||
setWidgetConfig(widgetType);
|
||||
})
|
||||
.catch((e) => {
|
||||
setWidgetConfig("JR_shikoku_train_info");
|
||||
});
|
||||
};
|
||||
useEffect(reload, [id]);
|
||||
return (
|
||||
<ListItem
|
||||
key={id}
|
||||
onPress={() => {
|
||||
//widget.widgetNameで定義されてないもう一つのウィジェットを選択する
|
||||
if (widgetConfig === "Info_Widget") {
|
||||
AS.setItem(`widgetType/${id}`, "JR_shikoku_train_info");
|
||||
} else {
|
||||
AS.setItem(`widgetType/${id}`, "Info_Widget");
|
||||
}
|
||||
reload();
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 20,
|
||||
alignItems: "center",
|
||||
alignContent: "center",
|
||||
textAlign: "center",
|
||||
textAlignVertical: "center",
|
||||
marginRight: 10,
|
||||
}}
|
||||
>
|
||||
{id}
|
||||
</Text>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 20,
|
||||
alignItems: "center",
|
||||
alignContent: "center",
|
||||
textAlign: "center",
|
||||
textAlignVertical: "center",
|
||||
}}
|
||||
>
|
||||
{widgetConfig}
|
||||
</Text>
|
||||
</ListItem>
|
||||
);
|
||||
};
|
||||
@@ -8,22 +8,21 @@ import {
|
||||
Image,
|
||||
useWindowDimensions,
|
||||
ToastAndroid,
|
||||
Platform
|
||||
} from "react-native";
|
||||
import { createStackNavigator } from "@react-navigation/stack";
|
||||
import { TransitionPresets } from "@react-navigation/stack";
|
||||
//import * as ExpoFelicaReader from "../../modules/expo-felica-reader/src";
|
||||
import * as ExpoFelicaReader from "../../modules/expo-felica-reader/src";
|
||||
import * as Updates from "expo-updates";
|
||||
import { AS } from "../../storageControl";
|
||||
import { STORAGE_KEYS } from "@/constants";
|
||||
import { Switch } from "react-native-elements";
|
||||
import AutoHeightImage from "react-native-auto-height-image";
|
||||
import { Switch } from "@rneui/themed";
|
||||
import { SettingTopPage } from "./SettingTopPage";
|
||||
import { LayoutSettings } from "./LayoutSettings";
|
||||
import { FavoriteSettings } from "./FavoriteSettings";
|
||||
import { WidgetSettings } from "./WidgetSettings";
|
||||
import { NotificationSettings } from "./NotificationSettings";
|
||||
import { LauncherIconSettings } from "./LauncherIconSettings";
|
||||
import { DataSourceSettings } from "./DataSourceSettings";
|
||||
import { FelicaHistoryPage } from "./FelicaHistoryPage";
|
||||
|
||||
const Stack = createStackNavigator();
|
||||
export default function Setting(props) {
|
||||
@@ -51,8 +50,26 @@ export default function Setting(props) {
|
||||
AS.getItem(STORAGE_KEYS.UI_SETTING).then(setUiSetting);
|
||||
}, []);
|
||||
const testNFC = async () => {
|
||||
//const result = await ExpoFelicaReader.scan();
|
||||
//alert(result);
|
||||
console.log("Testing NFC...");
|
||||
try {
|
||||
const result = await ExpoFelicaReader.scan();
|
||||
console.log("NFC Scan Result:", result);
|
||||
|
||||
if (result.balance >= 0) {
|
||||
await AS.setItem(STORAGE_KEYS.FELICA_LAST_SNAPSHOT, {
|
||||
balance: result.balance,
|
||||
idm: result.idm,
|
||||
systemCode: result.systemCode,
|
||||
scannedAt: new Date().toLocaleString("ja-JP"),
|
||||
});
|
||||
}
|
||||
|
||||
console.log("NFC Result:", result);
|
||||
alert(`IDm: ${result.idm}\n残高: ${result.balance}円\nシステムコード: ${result.systemCode}`);
|
||||
} catch (e) {
|
||||
console.error("NFC scan failed:", e);
|
||||
alert(`NFC読み取りに失敗しました: ${(e as Error).message}`);
|
||||
}
|
||||
};
|
||||
const updateAndReload = () => {
|
||||
Promise.all([
|
||||
@@ -68,7 +85,7 @@ export default function Setting(props) {
|
||||
]).then(() => Updates.reloadAsync());
|
||||
};
|
||||
return (
|
||||
<Stack.Navigator>
|
||||
<Stack.Navigator id={null}>
|
||||
<Stack.Screen
|
||||
name="settingTopPage"
|
||||
options={{
|
||||
@@ -116,8 +133,6 @@ export default function Setting(props) {
|
||||
setTrainPosition={setTrainPosition}
|
||||
uiSetting={uiSetting}
|
||||
setUiSetting={setUiSetting}
|
||||
testNFC={testNFC}
|
||||
updateAndReload={updateAndReload}
|
||||
headerSize={headerSize}
|
||||
setHeaderSize={setHeaderSize}
|
||||
/>
|
||||
@@ -134,17 +149,6 @@ export default function Setting(props) {
|
||||
}}
|
||||
component={NotificationSettings}
|
||||
/>
|
||||
{Platform.OS === 'android' && <Stack.Screen
|
||||
name="WidgetSettings"
|
||||
options={{
|
||||
gestureEnabled: true,
|
||||
...TransitionPresets.SlideFromRightIOS,
|
||||
cardOverlayEnabled: true,
|
||||
headerTransparent: true,
|
||||
headerShown: false,
|
||||
}}
|
||||
component={WidgetSettings}
|
||||
/>}
|
||||
<Stack.Screen
|
||||
name="LauncherIconSettings"
|
||||
options={{
|
||||
@@ -167,6 +171,28 @@ export default function Setting(props) {
|
||||
}}
|
||||
component={FavoriteSettings}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="DataSourceSettings"
|
||||
options={{
|
||||
gestureEnabled: true,
|
||||
...TransitionPresets.SlideFromRightIOS,
|
||||
cardOverlayEnabled: true,
|
||||
headerTransparent: true,
|
||||
headerShown: false,
|
||||
}}
|
||||
component={DataSourceSettings}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="FelicaHistoryPage"
|
||||
options={{
|
||||
gestureEnabled: true,
|
||||
...TransitionPresets.SlideFromRightIOS,
|
||||
cardOverlayEnabled: true,
|
||||
headerTransparent: true,
|
||||
headerShown: false,
|
||||
}}
|
||||
component={FelicaHistoryPage}
|
||||
/>
|
||||
</Stack.Navigator>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import Animated, {
|
||||
import { Gesture, GestureDetector } from "react-native-gesture-handler";
|
||||
import { ExGridViewTimePositionItem } from "./ExGridViewTimePositionItem";
|
||||
import { useCurrentTrain } from "@/stateBox/useCurrentTrain";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
import { logger } from "@/utils/logger";
|
||||
import dayjs from "dayjs";
|
||||
import { ExGridSimpleViewItem } from "./ExGridSimpleViewItem";
|
||||
@@ -100,6 +101,7 @@ export const ExGridSimpleView: FC<{
|
||||
];
|
||||
|
||||
const { currentTrain } = useCurrentTrain();
|
||||
const { colors } = useThemeColors();
|
||||
data.forEach((item) => {
|
||||
let isOperating = false;
|
||||
let [hour, minute] = dayjs()
|
||||
@@ -129,12 +131,39 @@ export const ExGridSimpleView: FC<{
|
||||
const stickyTextStyle = useAnimatedStyle(() => ({
|
||||
transform: [{ translateX: scrollX.value }],
|
||||
}));
|
||||
const scrollRef = useRef<ScrollView>(null);
|
||||
const yOffsets = useRef<Record<string, number>>({});
|
||||
|
||||
// データが揃ったら次の列車の時間帯へスクロール
|
||||
useEffect(() => {
|
||||
if (data.length === 0) return;
|
||||
const timer = setTimeout(() => {
|
||||
const now = dayjs();
|
||||
const nextTrain = data.find((d) => {
|
||||
const [h, m] = d.time.split(":").map(Number);
|
||||
const trainTime = h < 4
|
||||
? dayjs().add(1, "day").hour(h).minute(m)
|
||||
: dayjs().hour(h).minute(m);
|
||||
return trainTime.isAfter(now);
|
||||
});
|
||||
if (nextTrain) {
|
||||
const targetHour = String(parseInt(nextTrain.time.split(":")[0]));
|
||||
const y = yOffsets.current[targetHour];
|
||||
if (y !== undefined) {
|
||||
scrollRef.current?.scrollTo({ y: Math.max(0, y - 30), animated: true });
|
||||
}
|
||||
}
|
||||
}, 400);
|
||||
return () => clearTimeout(timer);
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
ref={scrollRef}
|
||||
stickyHeaderIndices={
|
||||
groupKeys.at(0) ? groupKeys.map((_, i) => i * 2) : []
|
||||
}
|
||||
style={{ backgroundColor: "#fff" }}
|
||||
style={{ backgroundColor: colors.diagramBackground }}
|
||||
>
|
||||
{groupKeys.map((hour) => [
|
||||
<View
|
||||
@@ -142,9 +171,10 @@ export const ExGridSimpleView: FC<{
|
||||
padding: 5,
|
||||
borderBottomWidth: 0.5,
|
||||
borderTopWidth: 0.5,
|
||||
borderBottomColor: "#ccc",
|
||||
backgroundColor: "#f0f0f0",
|
||||
borderBottomColor: colors.diagramBorder,
|
||||
backgroundColor: colors.diagramSectionHeader,
|
||||
}}
|
||||
onLayout={(e) => { yOffsets.current[hour] = e.nativeEvent.layout.y; }}
|
||||
key={hour}
|
||||
>
|
||||
<Animated.Text
|
||||
@@ -153,6 +183,7 @@ export const ExGridSimpleView: FC<{
|
||||
fontSize: 15,
|
||||
zIndex: 1,
|
||||
marginLeft: 0,
|
||||
color: colors.text,
|
||||
},
|
||||
stickyTextStyle,
|
||||
]}
|
||||
@@ -161,6 +192,7 @@ export const ExGridSimpleView: FC<{
|
||||
</Animated.Text>
|
||||
</View>,
|
||||
<View
|
||||
key={hour + "-items"}
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
position: "relative",
|
||||
|
||||
@@ -19,6 +19,7 @@ import { SharedValue, useAnimatedStyle } from "react-native-reanimated";
|
||||
import Animated from "react-native-reanimated";
|
||||
import lineColorList from "@/assets/originData/lineColorList";
|
||||
import { CustomTrainData, trainTypeID } from "@/lib/CommonTypes";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
|
||||
export const ExGridSimpleViewItem: FC<{
|
||||
d: {
|
||||
@@ -41,6 +42,7 @@ export const ExGridSimpleViewItem: FC<{
|
||||
const { allCustomTrainData } = useAllTrainDiagram();
|
||||
const { originalStationList, stationList } = useStationList();
|
||||
const { navigate } = useNavigation();
|
||||
const { colors, isDark } = useThemeColors();
|
||||
const [trainData, setTrainData] = useState<CustomTrainData>();
|
||||
useEffect(() => {
|
||||
if (allCustomTrainData) {
|
||||
@@ -53,10 +55,10 @@ export const ExGridSimpleViewItem: FC<{
|
||||
}, []);
|
||||
const { color, data } = getTrainType({
|
||||
type: trainData?.type,
|
||||
whiteMode: true,
|
||||
whiteMode: !isDark,
|
||||
});
|
||||
// 列車名、種別、フォントの取得
|
||||
const [trainName] = useMemo(() => {
|
||||
// 行き先(駅名)の取得
|
||||
const [destinationName] = useMemo(() => {
|
||||
// to_dataが設定されていればそれを優先
|
||||
if (trainData?.to_data) {
|
||||
return [trainData.to_data];
|
||||
@@ -75,6 +77,9 @@ export const ExGridSimpleViewItem: FC<{
|
||||
return [migrateTrainName(trainName)];
|
||||
}
|
||||
}, [d.array, trainData]);
|
||||
|
||||
// 列車名の取得(上部表示用)
|
||||
const trainName = trainData?.train_name || "";
|
||||
const timeArray = d.time.split(":").map((s) => parseInt(s));
|
||||
const formattedTime = dayjs()
|
||||
.set("hour", timeArray[0])
|
||||
@@ -146,7 +151,7 @@ export const ExGridSimpleViewItem: FC<{
|
||||
// to_dataがある場合は、to_dataから駅名を抽出して色を判定
|
||||
const stationNameForColor = trainData?.to_data
|
||||
? trainData.to_data.replace(/行き$/, "") // 「行き」を除去
|
||||
: trainName;
|
||||
: destinationName;
|
||||
|
||||
const Stations = stationList
|
||||
.map((a) => a.filter((d) => d.StationName == stationNameForColor))
|
||||
@@ -161,7 +166,7 @@ export const ExGridSimpleViewItem: FC<{
|
||||
);
|
||||
setStationColor(stationLineColor || ["gray"]);
|
||||
}
|
||||
}, [stationList, trainName, trainData]);
|
||||
}, [stationList, destinationName, trainData]);
|
||||
// if(typeString == "回送"){
|
||||
// return<></>;
|
||||
// }
|
||||
@@ -191,7 +196,7 @@ export const ExGridSimpleViewItem: FC<{
|
||||
style={{
|
||||
fontSize: 8,
|
||||
fontWeight: "bold",
|
||||
color: isCancelled ? "gray" : "black",
|
||||
color: isCancelled ? "gray" : colors.text,
|
||||
textAlign: "left",
|
||||
textDecorationLine: isCancelled ? "line-through" : "none",
|
||||
}}
|
||||
@@ -221,7 +226,7 @@ export const ExGridSimpleViewItem: FC<{
|
||||
top: 22,
|
||||
left: 28,
|
||||
fontWeight: "bold",
|
||||
color: isCancelled ? "gray" : "black",
|
||||
color: isCancelled ? "gray" : colors.text,
|
||||
textDecorationLine: isCancelled ? "line-through" : "none",
|
||||
}}
|
||||
>
|
||||
@@ -237,7 +242,7 @@ export const ExGridSimpleViewItem: FC<{
|
||||
textDecorationLine: isCancelled ? "line-through" : "none",
|
||||
}}
|
||||
>
|
||||
{trainName}
|
||||
{destinationName}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={{ flex: 1 }} />
|
||||
|
||||
@@ -23,6 +23,7 @@ import Animated, {
|
||||
import { Gesture, GestureDetector } from "react-native-gesture-handler";
|
||||
import { ExGridViewTimePositionItem } from "./ExGridViewTimePositionItem";
|
||||
import { useCurrentTrain } from "@/stateBox/useCurrentTrain";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
import { logger } from "@/utils/logger";
|
||||
import dayjs from "dayjs";
|
||||
type hoge = {
|
||||
@@ -99,6 +100,7 @@ export const ExGridView: FC<{
|
||||
|
||||
const { width } = useWindowDimensions();
|
||||
const { currentTrain } = useCurrentTrain();
|
||||
const { colors } = useThemeColors();
|
||||
data.forEach((item) => {
|
||||
let isOperating = false;
|
||||
let [hour, minute] = dayjs()
|
||||
@@ -183,7 +185,7 @@ export const ExGridView: FC<{
|
||||
// アニメーションスタイル
|
||||
const animatedStyle = useAnimatedStyle(() => ({
|
||||
width: widthX.value,
|
||||
backgroundColor: isChanging.value ? "#8adeffff" : "white",
|
||||
backgroundColor: isChanging.value ? colors.diagramHighlight : colors.diagramBackground,
|
||||
}));
|
||||
// 時ヘッダーを横にスクロールしたときの処理
|
||||
const scrollX = useSharedValue(0);
|
||||
@@ -250,7 +252,7 @@ export const ExGridView: FC<{
|
||||
ref={scrollRef}
|
||||
contentContainerStyle={{
|
||||
flexDirection: "column",
|
||||
backgroundColor: "white",
|
||||
backgroundColor: colors.diagramBackground,
|
||||
}}
|
||||
>
|
||||
<Animated.View
|
||||
@@ -271,9 +273,10 @@ export const ExGridView: FC<{
|
||||
flex: 1,
|
||||
textAlign: "left",
|
||||
borderRightWidth: 0.5,
|
||||
borderColor: "#ccc",
|
||||
borderColor: colors.diagramBorder,
|
||||
flexWrap: "nowrap",
|
||||
fontSize: 12,
|
||||
color: colors.text,
|
||||
}}
|
||||
>
|
||||
{num - 5}
|
||||
@@ -285,9 +288,10 @@ export const ExGridView: FC<{
|
||||
style={{
|
||||
textAlign: "right",
|
||||
borderRightWidth: 0.5,
|
||||
borderColor: "#ccc",
|
||||
borderColor: colors.diagramBorder,
|
||||
flexWrap: "nowrap",
|
||||
fontSize: 12,
|
||||
color: colors.text,
|
||||
width: 50,
|
||||
}}
|
||||
key={"分LabelEnd"}
|
||||
@@ -312,8 +316,8 @@ export const ExGridView: FC<{
|
||||
padding: 5,
|
||||
borderBottomWidth: 0.5,
|
||||
borderTopWidth: 0.5,
|
||||
borderBottomColor: "#ccc",
|
||||
backgroundColor: "#f0f0f0",
|
||||
borderBottomColor: colors.diagramBorder,
|
||||
backgroundColor: colors.diagramSectionHeader,
|
||||
}}
|
||||
key={hour}
|
||||
>
|
||||
@@ -331,6 +335,7 @@ export const ExGridView: FC<{
|
||||
</Animated.Text>
|
||||
</View>,
|
||||
<View
|
||||
key={hour + "-items"}
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
position: "relative",
|
||||
|
||||
@@ -19,6 +19,7 @@ import { SharedValue, useAnimatedStyle } from "react-native-reanimated";
|
||||
import Animated from "react-native-reanimated";
|
||||
import lineColorList from "@/assets/originData/lineColorList";
|
||||
import { CustomTrainData, trainTypeID } from "@/lib/CommonTypes";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
|
||||
export const ExGridViewItem: FC<{
|
||||
d: {
|
||||
@@ -41,6 +42,7 @@ export const ExGridViewItem: FC<{
|
||||
const { allCustomTrainData } = useAllTrainDiagram();
|
||||
const { originalStationList, stationList } = useStationList();
|
||||
const { navigate } = useNavigation();
|
||||
const { colors, isDark } = useThemeColors();
|
||||
const [trainData, setTrainData] = useState<CustomTrainData>();
|
||||
useEffect(() => {
|
||||
if (allCustomTrainData) {
|
||||
@@ -51,7 +53,7 @@ export const ExGridViewItem: FC<{
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
const { color, data } = getTrainType({ type: trainData?.type, whiteMode: true });
|
||||
const { color, data } = getTrainType({ type: trainData?.type, whiteMode: !isDark });
|
||||
// 列車名、種別、フォントの取得
|
||||
const [
|
||||
trainName,
|
||||
@@ -216,7 +218,7 @@ export const ExGridViewItem: FC<{
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
fontWeight: "bold",
|
||||
color: isCancelled ? "gray" : "black",
|
||||
color: isCancelled ? "gray" : colors.text,
|
||||
textDecorationLine: isCancelled ? "line-through" : "none",
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { FC } from "react";
|
||||
import { FC, useRef, useEffect } from "react";
|
||||
import { ListViewItem } from "@/components/StationDiagram/ListViewItem";
|
||||
import { View, Text, ScrollView } from "react-native";
|
||||
import dayjs from "dayjs";
|
||||
import { useUnyohub } from "@/stateBox/useUnyohub";
|
||||
import { useElesite } from "@/stateBox/useElesite";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
type hoge = {
|
||||
trainNumber: string;
|
||||
array: string;
|
||||
@@ -11,7 +14,14 @@ type hoge = {
|
||||
};
|
||||
export const ListView: FC<{
|
||||
data: hoge[];
|
||||
}> = ({ data }) => {
|
||||
showVehicle?: boolean;
|
||||
visibleSources?: { app: boolean; hub: boolean; elesite: boolean };
|
||||
}> = ({ data, showVehicle = false, visibleSources = { app: true, hub: true, elesite: true } }) => {
|
||||
const { getUnyohubByTrainNumber } = useUnyohub();
|
||||
const { getElesiteByTrainNumber } = useElesite();
|
||||
const { colors } = useThemeColors();
|
||||
const scrollRef = useRef<ScrollView>(null);
|
||||
const yOffsets = useRef<Record<string, number>>({});
|
||||
const groupedData: Record<string, hoge[]> = {};
|
||||
const groupKeys = [];
|
||||
data.forEach((item) => {
|
||||
@@ -22,20 +32,56 @@ export const ListView: FC<{
|
||||
}
|
||||
groupedData[hour].push(item);
|
||||
});
|
||||
|
||||
// データが揃ったら次の列車の時間帯へスクロール
|
||||
useEffect(() => {
|
||||
if (data.length === 0) return;
|
||||
const timer = setTimeout(() => {
|
||||
const now = dayjs();
|
||||
const nextTrain = data.find((d) => {
|
||||
const [h, m] = d.time.split(":").map(Number);
|
||||
const trainTime = h < 4
|
||||
? dayjs().add(1, "day").hour(h).minute(m)
|
||||
: dayjs().hour(h).minute(m);
|
||||
return trainTime.isAfter(now);
|
||||
});
|
||||
if (nextTrain) {
|
||||
const targetHour = String(parseInt(nextTrain.time.split(":")[0]));
|
||||
const y = yOffsets.current[targetHour];
|
||||
if (y !== undefined) {
|
||||
scrollRef.current?.scrollTo({ y: Math.max(0, y - 30), animated: true });
|
||||
}
|
||||
}
|
||||
}, 400);
|
||||
return () => clearTimeout(timer);
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
style={{ backgroundColor: "white" }}
|
||||
ref={scrollRef}
|
||||
style={{ backgroundColor: colors.diagramBackground }}
|
||||
stickyHeaderIndices={
|
||||
groupKeys.at(0) ? groupKeys.map((_, i) => i * 2) : []
|
||||
}
|
||||
>
|
||||
{groupKeys.map((hour) => [
|
||||
<View style={{ backgroundColor: "white", padding: 5, borderBottomWidth: 0.5, borderTopWidth: 0.5, borderBottomColor: "#ccc" }} key={hour}>
|
||||
<Text style={{ fontSize: 15 }}>{hour}時台</Text>
|
||||
<View
|
||||
style={{ backgroundColor: colors.diagramBackground, padding: 5, borderBottomWidth: 0.5, borderTopWidth: 0.5, borderBottomColor: colors.diagramBorder }}
|
||||
onLayout={(e) => { yOffsets.current[hour] = e.nativeEvent.layout.y; }}
|
||||
key={hour}
|
||||
>
|
||||
<Text style={{ fontSize: 15, color: colors.text }}>{hour}時台</Text>
|
||||
</View>,
|
||||
<View>
|
||||
<View key={hour + "-items"}>
|
||||
{groupedData[hour].map((d, i) => (
|
||||
<ListViewItem key={d.trainNumber + i} d={d} />
|
||||
<ListViewItem
|
||||
key={d.trainNumber + i}
|
||||
d={d}
|
||||
showVehicle={showVehicle}
|
||||
getUnyohubByTrainNumber={visibleSources.hub ? getUnyohubByTrainNumber : undefined}
|
||||
getElesiteByTrainNumber={visibleSources.elesite ? getElesiteByTrainNumber : undefined}
|
||||
showAppSource={visibleSources.app}
|
||||
/>
|
||||
))}
|
||||
</View>,
|
||||
])}
|
||||
|
||||
@@ -2,8 +2,8 @@ import { migrateTrainName } from "@/lib/eachTrainInfoCoreLib/migrateTrainName";
|
||||
import { getStringConfig } from "@/lib/getStringConfig";
|
||||
import { getTrainType } from "@/lib/getTrainType";
|
||||
import { useAllTrainDiagram } from "@/stateBox/useAllTrainDiagram";
|
||||
import { FC, useEffect, useMemo, useState } from "react";
|
||||
import { View, Text, TouchableOpacity } from "react-native";
|
||||
import { FC, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { View, Text, TouchableOpacity, Animated } from "react-native";
|
||||
import { customTrainDataDetector } from "../custom-train-data";
|
||||
import dayjs from "dayjs";
|
||||
import { SheetManager } from "react-native-actions-sheet";
|
||||
@@ -14,6 +14,7 @@ import { CustomTrainData, trainTypeID } from "@/lib/CommonTypes";
|
||||
import { StationNumberMaker } from "../駅名表/StationNumberMaker";
|
||||
import { getStationID } from "@/lib/eachTrainInfoCoreLib/getStationData";
|
||||
import lineColorList from "@/assets/originData/lineColorList";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
|
||||
export const ListViewItem: FC<{
|
||||
d: {
|
||||
@@ -23,9 +24,14 @@ export const ListViewItem: FC<{
|
||||
timeType: string;
|
||||
time: string;
|
||||
};
|
||||
}> = ({ d }) => {
|
||||
const { allCustomTrainData } = useAllTrainDiagram();
|
||||
showVehicle?: boolean;
|
||||
showAppSource?: boolean;
|
||||
getUnyohubByTrainNumber?: (trainNumber: string) => string | null;
|
||||
getElesiteByTrainNumber?: (trainNumber: string) => string | null;
|
||||
}> = ({ d, showVehicle = false, showAppSource = true, getUnyohubByTrainNumber, getElesiteByTrainNumber }) => {
|
||||
const { allCustomTrainData, getTodayOperationByTrainId } = useAllTrainDiagram();
|
||||
const { navigate } = useNavigation();
|
||||
const { colors, fixed, isDark } = useThemeColors();
|
||||
const [trainData, setTrainData] = useState<CustomTrainData | undefined>();
|
||||
useEffect(() => {
|
||||
if (allCustomTrainData) {
|
||||
@@ -38,7 +44,7 @@ export const ListViewItem: FC<{
|
||||
}, []);
|
||||
const { color, data } = getTrainType({
|
||||
type: trainData?.type,
|
||||
whiteMode: true,
|
||||
whiteMode: !isDark,
|
||||
});
|
||||
// 列車名、種別、フォントの取得
|
||||
const { getStationDataFromName, originalStationList } =
|
||||
@@ -81,7 +87,7 @@ export const ListViewItem: FC<{
|
||||
const station = getStationDataFromName(stationNameForColor);
|
||||
const defaultLineColor =
|
||||
station.length > 0
|
||||
? lineColorList[station[0]?.StationNumber.slice(0, 1)]
|
||||
? lineColorList[station[0]?.StationNumber?.slice(0, 1)]
|
||||
: "black";
|
||||
|
||||
// to_data_colorが設定されていればそれを最優先
|
||||
@@ -199,6 +205,47 @@ export const ListViewItem: FC<{
|
||||
|
||||
// 運休判定
|
||||
const isCancelled = d.timeType?.includes("休");
|
||||
|
||||
// 車番ソース一覧を構築(showVehicle=trueの時のみ計算)
|
||||
const vehicleSources = useMemo(() => {
|
||||
if (!showVehicle) return [];
|
||||
const sources: Array<{ label: string; text: string; badgeColor: string; textColor: string }> = [];
|
||||
if (showAppSource) {
|
||||
const ops = getTodayOperationByTrainId(d.trainNumber);
|
||||
const unitIds = [...new Set(ops.flatMap((op) => op.unit_ids ?? []))];
|
||||
if (unitIds.length > 0) {
|
||||
sources.push({ label: "運用", text: unitIds.join("・"), badgeColor: "#00BCD4", textColor: isDark ? "#4dd0e1" : "#006064" });
|
||||
}
|
||||
}
|
||||
const unyoText = getUnyohubByTrainNumber?.(d.trainNumber);
|
||||
if (unyoText) {
|
||||
sources.push({ label: "Hub", text: unyoText, badgeColor: "#9E9E9E", textColor: isDark ? "#cccccc" : "#424242" });
|
||||
}
|
||||
const elesiteText = getElesiteByTrainNumber?.(d.trainNumber);
|
||||
if (elesiteText) {
|
||||
sources.push({ label: "えれ", text: elesiteText, badgeColor: "#4CAF50", textColor: isDark ? "#81c784" : "#1B5E20" });
|
||||
}
|
||||
return sources;
|
||||
}, [showVehicle, showAppSource, d.trainNumber, getTodayOperationByTrainId, getUnyohubByTrainNumber, getElesiteByTrainNumber, isDark]);
|
||||
|
||||
const [sourceIndex, setSourceIndex] = useState(0);
|
||||
const fadeAnim = useRef(new Animated.Value(1)).current;
|
||||
|
||||
useEffect(() => {
|
||||
if (!showVehicle || vehicleSources.length <= 1) return;
|
||||
const cycle = setInterval(() => {
|
||||
Animated.timing(fadeAnim, { toValue: 0, duration: 300, useNativeDriver: true }).start(() => {
|
||||
setSourceIndex((i) => (i + 1) % vehicleSources.length);
|
||||
Animated.timing(fadeAnim, { toValue: 1, duration: 300, useNativeDriver: true }).start();
|
||||
});
|
||||
}, 3000);
|
||||
return () => clearInterval(cycle);
|
||||
}, [showVehicle, vehicleSources.length]);
|
||||
|
||||
useEffect(() => {
|
||||
setSourceIndex(0);
|
||||
fadeAnim.setValue(1);
|
||||
}, [showVehicle, vehicleSources.length]);
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
@@ -215,7 +262,7 @@ export const ListViewItem: FC<{
|
||||
onPress={() => openTrainInfo()}
|
||||
>
|
||||
<View style={{ position: "relative", flex: 3 }}>
|
||||
<Text style={{ fontSize: 30, fontFamily: "DiaPro", color: isCancelled ? "gray" : "black", textDecorationLine: isCancelled ? "line-through" : "none" }}>
|
||||
<Text style={{ fontSize: 30, fontFamily: "DiaPro", color: isCancelled ? "gray" : colors.text, textDecorationLine: isCancelled ? "line-through" : "none" }}>
|
||||
{formattedTime}
|
||||
</Text>
|
||||
<Text
|
||||
@@ -225,7 +272,7 @@ export const ListViewItem: FC<{
|
||||
bottom: -3,
|
||||
right: 0,
|
||||
fontWeight: "bold",
|
||||
color: isCancelled ? "gray" : "black",
|
||||
color: isCancelled ? "gray" : colors.text,
|
||||
textDecorationLine: isCancelled ? "line-through" : "none",
|
||||
}}
|
||||
>
|
||||
@@ -247,31 +294,54 @@ export const ListViewItem: FC<{
|
||||
>
|
||||
{typeString}
|
||||
</Text>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 15,
|
||||
fontWeight: "bold",
|
||||
flex: 1,
|
||||
paddingLeft: 2,
|
||||
color: isCancelled ? "gray" : color,
|
||||
textDecorationLine: isCancelled ? "line-through" : "none",
|
||||
}}
|
||||
>
|
||||
{(trainData?.train_name || "") +
|
||||
((trainData?.train_num_distance !== "" && !isNaN(parseInt(trainData?.train_num_distance)))
|
||||
? ` ${parseInt(d.trainNumber) - parseInt(trainData?.train_num_distance)}号`
|
||||
: "")}
|
||||
</Text>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 15,
|
||||
fontWeight: "bold",
|
||||
color: isCancelled ? "gray" : "black",
|
||||
textDecorationLine: isCancelled ? "line-through" : "none",
|
||||
}}
|
||||
>
|
||||
{trainData?.train_id}
|
||||
</Text>
|
||||
{showVehicle
|
||||
? (() => {
|
||||
if (vehicleSources.length === 0) {
|
||||
return <Text style={{ fontSize: 15, color: "gray", paddingLeft: 4, flex: 1 }}>—</Text>;
|
||||
}
|
||||
const src = vehicleSources[sourceIndex % vehicleSources.length];
|
||||
return (
|
||||
<Animated.View style={{ flex: 1, flexDirection: "row", alignItems: "center", paddingLeft: 4, gap: 4, opacity: fadeAnim }}>
|
||||
<View style={{ backgroundColor: src.badgeColor, borderRadius: 4, paddingHorizontal: 5, paddingVertical: 1 }}>
|
||||
<Text style={{ fontSize: 10, color: fixed.textOnPrimary, fontWeight: "bold" }}>{src.label}</Text>
|
||||
</View>
|
||||
<Text style={{ fontSize: 17, fontWeight: "bold", color: src.textColor, flex: 1 }} numberOfLines={1}>
|
||||
{src.text}
|
||||
</Text>
|
||||
{vehicleSources.length > 1 && (
|
||||
<Text style={{ fontSize: 10, color: colors.textQuaternary }}>{(sourceIndex % vehicleSources.length) + 1}/{vehicleSources.length}</Text>
|
||||
)}
|
||||
</Animated.View>
|
||||
);
|
||||
})()
|
||||
: <>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 15,
|
||||
fontWeight: "bold",
|
||||
flex: 1,
|
||||
paddingLeft: 2,
|
||||
color: isCancelled ? "gray" : color,
|
||||
textDecorationLine: isCancelled ? "line-through" : "none",
|
||||
}}
|
||||
>
|
||||
{(trainData?.train_name || "") +
|
||||
((trainData?.train_num_distance !== "" && !isNaN(parseInt(trainData?.train_num_distance)))
|
||||
? ` ${parseInt(d.trainNumber) - parseInt(trainData?.train_num_distance)}号`
|
||||
: "")}
|
||||
</Text>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 15,
|
||||
fontWeight: "bold",
|
||||
color: isCancelled ? "gray" : colors.text,
|
||||
textDecorationLine: isCancelled ? "line-through" : "none",
|
||||
}}
|
||||
>
|
||||
{trainData?.train_id}
|
||||
</Text>
|
||||
</>
|
||||
}
|
||||
</View>
|
||||
<View style={{ flexDirection: "row", alignItems: "center", flex: 1 }}>
|
||||
<Text
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
TouchableOpacity,
|
||||
LayoutAnimation,
|
||||
} from "react-native";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
|
||||
type hoge = {
|
||||
trainNumber: string;
|
||||
@@ -23,6 +24,7 @@ export const SearchInputSuggestBox: FC<{
|
||||
currentStationDiagram: hoge;
|
||||
}> = ({ input, setInput, currentStationDiagram }) => {
|
||||
const { getStationDataFromName } = useStationList();
|
||||
const { colors, fixed } = useThemeColors();
|
||||
const [stationList, setStationList] = useState<
|
||||
{
|
||||
stationName: string;
|
||||
@@ -83,7 +85,7 @@ export const SearchInputSuggestBox: FC<{
|
||||
style={{
|
||||
maxHeight: 200,
|
||||
width: "100%",
|
||||
backgroundColor: "#0099CC",
|
||||
backgroundColor: fixed.primary,
|
||||
zIndex: 100,
|
||||
}}
|
||||
>
|
||||
@@ -95,13 +97,15 @@ export const SearchInputSuggestBox: FC<{
|
||||
style={{
|
||||
margin: 5,
|
||||
padding: 5,
|
||||
backgroundColor: "#eee",
|
||||
backgroundColor: colors.suggestBackground,
|
||||
borderRadius: 20,
|
||||
}}
|
||||
key={stationName + number.join(",")}
|
||||
onPress={() => setInput(stationName)}
|
||||
>
|
||||
<Text>{stationName}</Text>
|
||||
<Text style={{ color: colors.text }}>
|
||||
{stationName.startsWith(".") ? stationName.slice(1) : stationName}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
@@ -110,7 +114,7 @@ export const SearchInputSuggestBox: FC<{
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
flexWrap: "wrap",
|
||||
borderTopColor: "#ccc",
|
||||
borderTopColor: colors.border,
|
||||
borderTopWidth: 0.5,
|
||||
paddingTop: 0,
|
||||
marginTop: 10,
|
||||
@@ -120,7 +124,7 @@ export const SearchInputSuggestBox: FC<{
|
||||
style={{
|
||||
margin: 5,
|
||||
padding: 5,
|
||||
backgroundColor: "#eee",
|
||||
backgroundColor: colors.suggestBackground,
|
||||
borderRadius: 5,
|
||||
}}
|
||||
key={"empty"}
|
||||
@@ -132,7 +136,7 @@ export const SearchInputSuggestBox: FC<{
|
||||
setListFiltered("");
|
||||
}}
|
||||
>
|
||||
<Text>全て</Text>
|
||||
<Text style={{ color: colors.text }}>全て</Text>
|
||||
</TouchableOpacity>
|
||||
{filteredStationLine.map((line) => (
|
||||
<TouchableOpacity
|
||||
@@ -141,7 +145,7 @@ export const SearchInputSuggestBox: FC<{
|
||||
padding: 5,
|
||||
backgroundColor: lineColorList[line]
|
||||
? `${lineColorList[line]}`
|
||||
: "#eee",
|
||||
: colors.suggestBackground,
|
||||
borderRadius: 5,
|
||||
}}
|
||||
key={line}
|
||||
@@ -153,7 +157,7 @@ export const SearchInputSuggestBox: FC<{
|
||||
setListFiltered(line);
|
||||
}}
|
||||
>
|
||||
<Text style={{ color: lineColorList[line] ? `white` : "black" }}>
|
||||
<Text style={{ color: lineColorList[line] ? `white` : colors.text }}>
|
||||
{line}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
@@ -1,27 +1,32 @@
|
||||
import { FC, useEffect, useState } from "react";
|
||||
import { FC, useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
ScrollView,
|
||||
TextInput,
|
||||
Keyboard,
|
||||
KeyboardAvoidingView,
|
||||
Platform,
|
||||
TouchableOpacity,
|
||||
LayoutAnimation,
|
||||
Modal,
|
||||
Pressable,
|
||||
} from "react-native";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import { BigButton } from "../atom/BigButton";
|
||||
import { useAllTrainDiagram } from "@/stateBox/useAllTrainDiagram";
|
||||
import { useUnyohub } from "@/stateBox/useUnyohub";
|
||||
import { useElesite } from "@/stateBox/useElesite";
|
||||
import { ListView } from "@/components/StationDiagram/ListView";
|
||||
import dayjs from "dayjs";
|
||||
import { ExGridView } from "./ExGridView";
|
||||
import { Switch } from "react-native-elements";
|
||||
import { Switch } from "@rneui/themed";
|
||||
import { customTrainDataDetector } from "../custom-train-data";
|
||||
import { getTrainType } from "@/lib/getTrainType";
|
||||
import { trainTypeID } from "@/lib/CommonTypes";
|
||||
import { SearchInputSuggestBox } from "./SearchBox/SearchInputSuggestBox";
|
||||
import { ExGridSimpleView } from "./ExGridSimpleView";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
import { useStationLockActivity } from "@/lib/useStationLockActivity";
|
||||
import { useBottomTabBarHeight } from "@react-navigation/bottom-tabs";
|
||||
import { useKeyboardAvoid } from "@/lib/useKeyboardAvoid";
|
||||
|
||||
type props = {
|
||||
route: {
|
||||
@@ -52,10 +57,16 @@ export const StationDiagramView: FC<props> = ({ route }) => {
|
||||
// 表示モード:縦並びリスト、横並びグリッド(時刻分割)、横並び単純左詰め
|
||||
// フィルタリング:終点路線、種別、行先、関係停車駅
|
||||
|
||||
const { keyList, allTrainDiagram, allCustomTrainData } = useAllTrainDiagram();
|
||||
const { keyList, allTrainDiagram, allCustomTrainData, todayOperation } = useAllTrainDiagram();
|
||||
const { useUnyohub: unyohubEnabled } = useUnyohub();
|
||||
const { useElesite: elesiteEnabled } = useElesite();
|
||||
|
||||
const { goBack } = useNavigation();
|
||||
const [keyBoardVisible, setKeyBoardVisible] = useState(false);
|
||||
const { colors, fixed } = useThemeColors();
|
||||
const tabBarHeight = useBottomTabBarHeight();
|
||||
const containerRef = useRef<View>(null);
|
||||
const { keyboardVisible: keyBoardVisible, measuredOffset: keyboardOffset } =
|
||||
useKeyboardAvoid({ measureRef: containerRef, tabBarHeight });
|
||||
const [input, setInput] = useState("");
|
||||
const [displayMode, setDisplayMode] = useState<
|
||||
"list" | "grid" | "simpleGrid"
|
||||
@@ -78,6 +89,13 @@ export const StationDiagramView: FC<props> = ({ route }) => {
|
||||
const [showTypeFiltering, setShowTypeFiltering] = useState(false);
|
||||
const [showLastStop, setShowLastStop] = useState(false);
|
||||
const [threw, setIsThrew] = useState(false);
|
||||
const [showVehicle, setShowVehicle] = useState(false);
|
||||
const [sourceModalVisible, setSourceModalVisible] = useState(false);
|
||||
// 有効なソースのうち表示するソース(初期=全て表示)
|
||||
const [visibleSources, setVisibleSources] = useState<{ app: boolean; hub: boolean; elesite: boolean }>({ app: true, hub: true, elesite: true });
|
||||
|
||||
// 駅ロック Live Activity (iOS only)
|
||||
const stationLock = useStationLockActivity();
|
||||
const [currentStationDiagram, setCurrentStationDiagram] = useState<hoge>([]);
|
||||
useEffect(() => {
|
||||
if (allTrainDiagram && currentStation.length > 0) {
|
||||
@@ -178,42 +196,106 @@ export const StationDiagramView: FC<props> = ({ route }) => {
|
||||
}
|
||||
}, [currentStation, showLastStop, threw, input, selectedTypeList]);
|
||||
|
||||
// 時刻表データが更新されたら駅ロック Live Activity を自動更新
|
||||
useEffect(() => {
|
||||
const showSubscription = Keyboard.addListener("keyboardDidShow", () => {
|
||||
LayoutAnimation.configureNext({
|
||||
duration: 600,
|
||||
update: { type: "spring", springDamping: 0.6 },
|
||||
});
|
||||
setKeyBoardVisible(true);
|
||||
});
|
||||
const hideSubscription = Keyboard.addListener("keyboardDidHide", () => {
|
||||
LayoutAnimation.configureNext({
|
||||
duration: 600,
|
||||
update: { type: "spring", springDamping: 0.6 },
|
||||
});
|
||||
setKeyBoardVisible(false);
|
||||
});
|
||||
if (stationLock.status !== "active") return;
|
||||
if (currentStationDiagram.length === 0) return;
|
||||
|
||||
const now = dayjs();
|
||||
const nextTrain = currentStationDiagram.find(
|
||||
(d) => dayjs(d.time, "HH:mm").isAfter(now)
|
||||
);
|
||||
const followingTrain = currentStationDiagram.find(
|
||||
(d) => dayjs(d.time, "HH:mm").isAfter(now) && d !== nextTrain
|
||||
);
|
||||
if (!nextTrain) return;
|
||||
|
||||
stationLock.update({
|
||||
nextTrainTime: nextTrain.time,
|
||||
nextTrainDestination: nextTrain.name,
|
||||
nextTrainPlatform: "",
|
||||
followingTrainTime: followingTrain?.time ?? "",
|
||||
followingTrainDestination: followingTrain?.name ?? "",
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [currentStationDiagram]);
|
||||
|
||||
return () => {
|
||||
showSubscription.remove();
|
||||
hideSubscription.remove();
|
||||
};
|
||||
}, []);
|
||||
return (
|
||||
<View style={{ height: "100%", backgroundColor: "#0099CC" }}>
|
||||
<Text
|
||||
style={{
|
||||
textAlign: "center",
|
||||
fontSize: 20,
|
||||
color: "white",
|
||||
fontWeight: "bold",
|
||||
paddingVertical: 10,
|
||||
}}
|
||||
>
|
||||
{currentStation[0].Station_JP}駅 時刻表
|
||||
</Text>
|
||||
<View
|
||||
ref={containerRef}
|
||||
style={{ flex: 1, backgroundColor: fixed.primary, paddingBottom: keyboardOffset }}
|
||||
>
|
||||
<View style={{ flexDirection: "row", alignItems: "center", paddingVertical: 10, paddingHorizontal: 10 }}>
|
||||
<Text
|
||||
style={{
|
||||
flex: 1,
|
||||
textAlign: "center",
|
||||
fontSize: 20,
|
||||
color: fixed.textOnPrimary,
|
||||
fontWeight: "bold",
|
||||
}}
|
||||
>
|
||||
{currentStation[0].Station_JP.startsWith(".")
|
||||
? currentStation[0].Station_JP.slice(1)
|
||||
: `${currentStation[0].Station_JP}駅`}{" "}
|
||||
時刻表
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
onPress={() => setShowVehicle((v) => !v)}
|
||||
onLongPress={() => setSourceModalVisible(true)}
|
||||
delayLongPress={400}
|
||||
style={{
|
||||
backgroundColor: showVehicle ? colors.background : "rgba(255,255,255,0.3)",
|
||||
borderRadius: 6,
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 4,
|
||||
}}
|
||||
>
|
||||
<Text style={{ fontSize: 18 }}>🚃</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
</View>
|
||||
|
||||
{/* ソース選択モーダル */}
|
||||
<Modal transparent animationType="fade" visible={sourceModalVisible} onRequestClose={() => setSourceModalVisible(false)}>
|
||||
<Pressable style={{ flex: 1, backgroundColor: colors.backgroundOverlay, justifyContent: "center", alignItems: "center" }} onPress={() => setSourceModalVisible(false)}>
|
||||
<Pressable style={{ backgroundColor: colors.background, borderRadius: 12, padding: 20, width: 280 }} onPress={() => {}}>
|
||||
<Text style={{ fontSize: 16, fontWeight: "bold", marginBottom: 14, textAlign: "center", color: colors.text }}>表示するソースを選択</Text>
|
||||
{[
|
||||
{ key: "app" as const, label: "🚃 アプリ運用情報", color: "#00BCD4", available: todayOperation.length > 0 },
|
||||
{ key: "hub" as const, label: "🚃 鉄道運用Hub", color: "#9E9E9E", available: unyohubEnabled },
|
||||
{ key: "elesite" as const, label: "🚃 えれサイト", color: "#4CAF50", available: elesiteEnabled },
|
||||
].map(({ key, label, color, available }) => (
|
||||
<TouchableOpacity
|
||||
key={key}
|
||||
onPress={() => available && setVisibleSources((s) => ({ ...s, [key]: !s[key] }))}
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
paddingVertical: 10,
|
||||
opacity: available ? 1 : 0.35,
|
||||
}}
|
||||
>
|
||||
<View style={{
|
||||
width: 22, height: 22, borderRadius: 4, borderWidth: 2,
|
||||
borderColor: available ? color : colors.border,
|
||||
backgroundColor: visibleSources[key] && available ? color : "transparent",
|
||||
marginRight: 12, alignItems: "center", justifyContent: "center",
|
||||
}}>
|
||||
{visibleSources[key] && available && <Text style={{ color: fixed.textOnPrimary, fontSize: 14, fontWeight: "bold" }}>✓</Text>}
|
||||
</View>
|
||||
<Text style={{ fontSize: 15, color: available ? colors.textPrimary : colors.textQuaternary, flex: 1 }}>{label}</Text>
|
||||
{!available && <Text style={{ fontSize: 11, color: colors.textQuaternary }}>未設定</Text>}
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
<TouchableOpacity onPress={() => setSourceModalVisible(false)} style={{ marginTop: 8, backgroundColor: fixed.primary, borderRadius: 8, paddingVertical: 10, alignItems: "center" }}>
|
||||
<Text style={{ color: fixed.textOnPrimary, fontWeight: "bold", fontSize: 15 }}>閉じる</Text>
|
||||
</TouchableOpacity>
|
||||
</Pressable>
|
||||
</Pressable>
|
||||
</Modal>
|
||||
{displayMode === "list" ? (
|
||||
<ListView data={currentStationDiagram} />
|
||||
<ListView data={currentStationDiagram} showVehicle={showVehicle} visibleSources={visibleSources} />
|
||||
) : displayMode === "simpleGrid" ? (
|
||||
<ExGridSimpleView
|
||||
data={currentStationDiagram}
|
||||
@@ -231,11 +313,7 @@ export const StationDiagramView: FC<props> = ({ route }) => {
|
||||
>
|
||||
お気に入り登録した駅のうち、位置情報システムで移動可能な駅が表示されています。タップすることで位置情報システムの当該の駅に移動します。
|
||||
</Text> */}
|
||||
<KeyboardAvoidingView
|
||||
behavior="padding"
|
||||
keyboardVerticalOffset={80}
|
||||
enabled={Platform.OS === "ios"}
|
||||
>
|
||||
<View>
|
||||
{!keyBoardVisible ? (
|
||||
<ScrollView
|
||||
horizontal
|
||||
@@ -249,9 +327,9 @@ export const StationDiagramView: FC<props> = ({ route }) => {
|
||||
style={{
|
||||
alignItems: "center",
|
||||
marginHorizontal: 5,
|
||||
backgroundColor: threw ? "white" : "#ffffff00",
|
||||
backgroundColor: threw ? fixed.textOnPrimary : fixed.transparent,
|
||||
alignSelf: "center",
|
||||
borderColor: "white",
|
||||
borderColor: fixed.textOnPrimary,
|
||||
borderWidth: 1,
|
||||
borderRadius: 100,
|
||||
}}
|
||||
@@ -261,7 +339,7 @@ export const StationDiagramView: FC<props> = ({ route }) => {
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
color: threw ? "#0099CC" : "white",
|
||||
color: threw ? fixed.primary : fixed.textOnPrimary,
|
||||
fontSize: 14,
|
||||
margin: 5,
|
||||
}}
|
||||
@@ -273,9 +351,9 @@ export const StationDiagramView: FC<props> = ({ route }) => {
|
||||
style={{
|
||||
alignItems: "center",
|
||||
marginHorizontal: 5,
|
||||
backgroundColor: showLastStop ? "white" : "#ffffff00",
|
||||
backgroundColor: showLastStop ? fixed.textOnPrimary : fixed.transparent,
|
||||
alignSelf: "center",
|
||||
borderColor: "white",
|
||||
borderColor: fixed.textOnPrimary,
|
||||
borderWidth: 1,
|
||||
borderRadius: 100,
|
||||
}}
|
||||
@@ -285,7 +363,7 @@ export const StationDiagramView: FC<props> = ({ route }) => {
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
color: showLastStop ? "#0099CC" : "white",
|
||||
color: showLastStop ? fixed.primary : fixed.textOnPrimary,
|
||||
fontSize: 14,
|
||||
margin: 5,
|
||||
}}
|
||||
@@ -298,7 +376,7 @@ export const StationDiagramView: FC<props> = ({ route }) => {
|
||||
height: "auto",
|
||||
borderLeftWidth: 1,
|
||||
margin: 5,
|
||||
borderColor: "white",
|
||||
borderColor: fixed.textOnPrimary,
|
||||
}}
|
||||
/>
|
||||
{showTypeFiltering ? (
|
||||
@@ -347,9 +425,9 @@ export const StationDiagramView: FC<props> = ({ route }) => {
|
||||
style={{
|
||||
alignItems: "center",
|
||||
marginHorizontal: 5,
|
||||
backgroundColor: "#ffffff00",
|
||||
backgroundColor: fixed.transparent,
|
||||
alignSelf: "center",
|
||||
borderColor: "white",
|
||||
borderColor: fixed.textOnPrimary,
|
||||
borderWidth: 1,
|
||||
borderRadius: 100,
|
||||
}}
|
||||
@@ -362,7 +440,7 @@ export const StationDiagramView: FC<props> = ({ route }) => {
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
color: "white",
|
||||
color: fixed.textOnPrimary,
|
||||
fontSize: 14,
|
||||
margin: 5,
|
||||
}}
|
||||
@@ -376,9 +454,9 @@ export const StationDiagramView: FC<props> = ({ route }) => {
|
||||
style={{
|
||||
alignItems: "center",
|
||||
marginHorizontal: 5,
|
||||
backgroundColor: "#ffffff00",
|
||||
backgroundColor: fixed.transparent,
|
||||
alignSelf: "center",
|
||||
borderColor: "white",
|
||||
borderColor: fixed.textOnPrimary,
|
||||
borderWidth: 1,
|
||||
borderRadius: 100,
|
||||
}}
|
||||
@@ -391,7 +469,7 @@ export const StationDiagramView: FC<props> = ({ route }) => {
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
color: "white",
|
||||
color: fixed.textOnPrimary,
|
||||
fontSize: 14,
|
||||
margin: 5,
|
||||
}}
|
||||
@@ -405,9 +483,9 @@ export const StationDiagramView: FC<props> = ({ route }) => {
|
||||
style={{
|
||||
alignItems: "center",
|
||||
marginHorizontal: 5,
|
||||
backgroundColor: "#ffffff00",
|
||||
backgroundColor: fixed.transparent,
|
||||
alignSelf: "center",
|
||||
borderColor: "white",
|
||||
borderColor: fixed.textOnPrimary,
|
||||
borderWidth: 1,
|
||||
borderRadius: 100,
|
||||
}}
|
||||
@@ -423,7 +501,7 @@ export const StationDiagramView: FC<props> = ({ route }) => {
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
color: "white",
|
||||
color: fixed.textOnPrimary,
|
||||
fontSize: 14,
|
||||
margin: 5,
|
||||
}}
|
||||
@@ -450,24 +528,24 @@ export const StationDiagramView: FC<props> = ({ route }) => {
|
||||
height: 35,
|
||||
margin: 5,
|
||||
alignItems: "center",
|
||||
backgroundColor: "#F4F4F4",
|
||||
backgroundColor: colors.searchBackground,
|
||||
flexDirection: "row",
|
||||
paddingLeft: 10,
|
||||
paddingRight: 10,
|
||||
borderRadius: 25,
|
||||
borderColor: "#F4F4F4",
|
||||
borderColor: colors.searchBorder,
|
||||
}}
|
||||
>
|
||||
<TextInput
|
||||
placeholder="駅名を入力して停車駅でフィルタリングします。"
|
||||
onFocus={() => setKeyBoardVisible(true)}
|
||||
onFocus={() => {}}
|
||||
onEndEditing={() => {}}
|
||||
onChange={(ret) => setInput(ret.nativeEvent.text)}
|
||||
value={input}
|
||||
style={{ flex: 1 }}
|
||||
style={{ flex: 1, height: "100%", paddingVertical: 0 }}
|
||||
/>
|
||||
</View>
|
||||
</KeyboardAvoidingView>
|
||||
</View>
|
||||
{keyBoardVisible || (
|
||||
<BigButton onPress={() => goBack()} string="閉じる" />
|
||||
)}
|
||||
@@ -482,6 +560,7 @@ export const TypeSelectorBox: FC<{
|
||||
relativeID?: trainTypeID[];
|
||||
}> = (props) => {
|
||||
const { selectedTypeList, setSelectedTypeList, typeID, relativeID } = props;
|
||||
const { fixed } = useThemeColors();
|
||||
const isSelected =
|
||||
selectedTypeList.findIndex((item) => item === typeID) !== -1;
|
||||
const { color, shortName } = getTrainType({ type: typeID, whiteMode: true });
|
||||
@@ -491,7 +570,7 @@ export const TypeSelectorBox: FC<{
|
||||
alignItems: "center",
|
||||
marginHorizontal: 5,
|
||||
opacity: isSelected ? 1 : 0.8,
|
||||
backgroundColor: isSelected ? "white" : color,
|
||||
backgroundColor: isSelected ? fixed.textOnPrimary : color,
|
||||
alignSelf: "center",
|
||||
borderColor: color,
|
||||
borderWidth: 1,
|
||||
@@ -515,7 +594,7 @@ export const TypeSelectorBox: FC<{
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
color: isSelected ? color : "white",
|
||||
color: isSelected ? color : fixed.textOnPrimary,
|
||||
fontSize: 14,
|
||||
margin: 5,
|
||||
}}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { FC } from "react";
|
||||
import { View, TouchableOpacity, TouchableOpacityProps } from "react-native";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
|
||||
type Props = {
|
||||
onPress: () => void;
|
||||
@@ -9,14 +10,15 @@ type Props = {
|
||||
};
|
||||
|
||||
export const MapsButton: FC<Props> = ({ onPress, top, mapSwitch }) => {
|
||||
const { fixed } = useThemeColors();
|
||||
const styles: TouchableOpacityProps["style"] = {
|
||||
position: "absolute",
|
||||
top,
|
||||
left: 10,
|
||||
width: 50,
|
||||
height: 50,
|
||||
backgroundColor: "#0099CC",
|
||||
borderColor: "white",
|
||||
backgroundColor: fixed.primary,
|
||||
borderColor: fixed.textOnPrimary,
|
||||
borderStyle: "solid",
|
||||
borderWidth: 1,
|
||||
borderRadius: 50,
|
||||
@@ -28,7 +30,7 @@ export const MapsButton: FC<Props> = ({ onPress, top, mapSwitch }) => {
|
||||
return (
|
||||
<TouchableOpacity onPress={onPress} style={styles}>
|
||||
<View style={{ flex: 1 }} />
|
||||
<Ionicons name="close" color="white" size={30} />
|
||||
<Ionicons name="close" color={fixed.textOnPrimary} size={30} />
|
||||
<View style={{ flex: 1 }} />
|
||||
</TouchableOpacity>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { FC } from "react";
|
||||
import { Text, TouchableOpacity } from "react-native";
|
||||
import { MaterialCommunityIcons } from "@expo/vector-icons";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
|
||||
type Props = {
|
||||
icon: keyof typeof MaterialCommunityIcons.glyphMap;
|
||||
@@ -12,6 +13,7 @@ type Props = {
|
||||
|
||||
export const UsefulBox: FC<Props> = (props) => {
|
||||
const { icon, backgroundColor, flex, onPressButton, children } = props;
|
||||
const { fixed } = useThemeColors();
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={{
|
||||
@@ -23,8 +25,8 @@ export const UsefulBox: FC<Props> = (props) => {
|
||||
}}
|
||||
onPress={onPressButton}
|
||||
>
|
||||
<MaterialCommunityIcons name={icon} color="white" size={50} />
|
||||
<Text style={{ color: "white", fontWeight: "bold", fontSize: 16 }}>
|
||||
<MaterialCommunityIcons name={icon} color={fixed.textOnPrimary} size={50} />
|
||||
<Text style={{ color: fixed.textOnPrimary, fontWeight: "bold", fontSize: 16 }}>
|
||||
{children}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { FC } from "react";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
import {
|
||||
Text,
|
||||
TextStyle,
|
||||
@@ -15,13 +16,14 @@ type Props = {
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
export const BigButton: FC<Props> = (props) => {
|
||||
const { fixed } = useThemeColors();
|
||||
const { onPress, string, style, tS, children } = props;
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={{
|
||||
padding: 10,
|
||||
flexDirection: "row",
|
||||
borderColor: "white",
|
||||
borderColor: fixed.textOnPrimary,
|
||||
borderWidth: 1,
|
||||
margin: 10,
|
||||
borderRadius: 5,
|
||||
@@ -32,7 +34,7 @@ export const BigButton: FC<Props> = (props) => {
|
||||
>
|
||||
<View style={{ flex: 1 }} />
|
||||
{children}
|
||||
<Text style={{ fontSize: 25, fontWeight: "bold", color: "white", ...tS }}>
|
||||
<Text style={{ fontSize: 25, fontWeight: "bold", color: fixed.textOnPrimary, ...tS }}>
|
||||
{string}
|
||||
</Text>
|
||||
<View style={{ flex: 1 }} />
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user