121 Commits

Author SHA1 Message Date
harukin-expo-dev-env
7d454cf9d4 Merge commit '09c00202247c0c97f1d8c324c2cc49214eee1393' 2026-02-07 09:04:47 +00:00
harukin-expo-dev-env
699e3039b9 Merge commit '935b63f6cee9d84530a5e7f6e8f55bcc90f8a168' 2026-02-01 13:18:49 +00:00
harukin-expo-dev-env
5af3164f21 Merge commit 'ef81c1f4cdc325f66a55b9f06212190dbec5be24' 2025-12-31 16:08:13 +00:00
harukin-expo-dev-env
cd585b8206 Merge commit 'bb1ee2666e5c91819bc3330128a9636b5e2ad753' 2025-12-31 09:52:36 +00:00
harukin-expo-dev-env
927c73567a Merge commit '0bf345ff6ac492e66da91e71db0118adb58cb28a' 2025-12-20 10:14:56 +00:00
harukin-expo-dev-env
09170c6636 Merge commit '24f32335c48eda61d312eceaf6bdd1e442c63f60' 2025-12-12 19:27:28 +00:00
harukin-expo-dev-env
8705f69725 Merge commit '2975e9094e17963b726753d954ba347226af98e2' 2025-12-05 15:25:11 +00:00
harukin-expo-dev-env
9562e04ffe Merge commit '6f74f5dfa26369dc4ce15435eccb9af51b5842e3' 2025-11-06 01:41:13 +00:00
harukin-expo-dev-env
2ffb47b60c Merge commit '09f700fb667a9dc0b05488e05c3d9cd3c31ffaae' 2025-11-06 01:33:37 +00:00
harukin-expo-dev-env
1208a78831 Merge commit 'efd9b77cadfbff4205e17388e7b2a6cad637ba9f' 2025-09-29 18:43:34 +00:00
harukin-expo-dev-env
493ef92bd6 Merge commit 'dfaf5b05b93f8fdcddb17da2a4981faffc28e0ac' 2025-09-29 06:08:16 +00:00
harukin-expo-dev-env
a51ffe4a82 Merge commit 'fa1562f870e8f5fa26db931785dd297eeb834c56' 2025-09-28 19:50:05 +00:00
harukin-expo-dev-env
0ea25f1e97 Merge commit '19f9b58497df949ff20cbad00a344118ac5740cd' 2025-09-27 17:51:11 +00:00
harukin-expo-dev-env
16bd0fe192 Merge commit '59e7ba5290bd4f175c9dafbc0dd664b75417bf2f' 2025-09-25 03:00:42 +00:00
harukin-expo-dev-env
d75a495b19 Merge commit '0d9c1cdb186a2cecc2e0aec441d9af9ab6ff56a6' 2025-09-11 05:55:26 +00:00
harukin-expo-dev-env
a346a1477c Merge commit 'bc4cb450a3dfb0005d7e815019b6317953478a9f' 2025-09-09 16:16:57 +00:00
harukin-expo-dev-env
c93ca94a61 Merge commit '9b4c0735b0475659aa7fe1f688b8daef13b714f1' 2025-08-27 01:20:09 +00:00
harukin-expo-dev-env
4480c161d1 Merge commit '087f6c882968bdef782bfe2aa0005a67fe436c7c' 2025-08-24 11:30:06 +00:00
harukin-expo-dev-env
deb9b40949 Merge commit 'ddaad38ccc2d23121f7fb6e3678417e7ac9ed593' 2025-08-24 04:49:29 +00:00
harukin-expo-dev-env
fced009607 Merge commit 'eabb2499fa43ba6a5adb3ae71e18067d86d63e35' 2025-08-19 19:19:24 +00:00
harukin-expo-dev-env
0890c1b9ff Merge commit '7bbb5b972fdb2eb429e5f71d6dd30e279b47ad10' 2025-08-13 12:50:56 +00:00
harukin-expo-dev-env
4b3816940b Merge commit 'ff4eb2c95f72581771b09ea820e9413540684e7b' 2025-08-08 10:54:00 +00:00
harukin-expo-dev-env
9ea23d6eba Merge commit '3dbaa6bfbf2bf18d01464c5e367c95593250dc85' 2025-07-27 04:12:01 +00:00
harukin-expo-dev-env
31ea303b88 Merge commit '9b266c15f80301e0b7a47cfc702c52beec36f4c4' 2025-07-27 03:54:24 +00:00
harukin-expo-dev-env
7716cb516c Merge commit 'dc7cc555c990fd4a2cd5cf47089339c887eecc75' 2025-07-18 15:42:20 +00:00
harukin-expo-dev-env
70976c0554 Merge commit '8a7285bb20c76d6e99cafd96193957829906f9ac' 2025-07-08 16:44:25 +00:00
harukin-expo-dev-env
5616c7ed96 Merge commit '8212148fb298dc678b811aadf0344731b43e8cf7' 2025-05-04 12:56:37 +00:00
harukin-expo-dev-env
13580a57d4 Merge commit 'bd2248e1a943e574ccc44892122cd9da4df14b68' 2025-04-20 17:22:59 +00:00
harukin-expo-dev-env
c9481fb0c2 Merge commit 'b64138178c4ca99bfcbb4f115403a18674ce8136' 2025-04-14 19:10:10 +00:00
harukin-expo-dev-env
5cda45740c Merge commit '67f40b55c19ff2e94b1168865486bcde7d457cab' 2025-04-10 16:38:21 +00:00
harukin-expo-dev-env
84d1305796 Merge commit 'fdefbc82bd39c8cba29911273db2f96fa39c0e3f' 2025-04-10 15:55:13 +00:00
harukin-expo-dev-env
336e3510fa Merge commit 'af8f3333cd68ea507675ad02ec59ccf2f7959e00' 2025-03-30 02:52:30 +00:00
harukin-expo-dev-env
25780e1664 Merge commit '0a364021ce2fed98257e80cc780ac4020cecb9c1' 2025-03-25 07:27:06 +00:00
harukin-expo-dev-env
e6fab84393 Merge commit 'de2b94292dc97df6e290e91976d01cea24c98ced' 2025-03-22 12:33:34 +00:00
harukin-expo-dev-env
390acdeab7 Merge commit '8c75e06ac18e33588b5bf200d7fccf05d3c34c22' 2025-03-22 11:34:25 +00:00
harukin-expo-dev-env
ff7a5624e6 Merge commit '45500e7a4ae614debd93404339958465db88f71d' 2025-03-19 15:49:36 +00:00
harukin-expo-dev-env
9b79f224d3 Merge commit '2c5023568a0442679b476bd84651eca3fcc90ee1' 2025-03-16 05:56:14 +00:00
harukin-expo-dev-env
2f76e3776f Merge commit '8a48bc48e6eb82921fcda3a84e07db2eca51e614' 2025-03-15 04:23:16 +00:00
harukin-expo-dev-env
026da47d82 Merge commit '23fb2d715a461cb0ebfca1e1944757febd315fec' 2025-03-14 18:29:20 +00:00
harukin-expo-dev-env
29be052bf6 Merge commit 'ab883827e13b5c99556857395ac950ee00c53b83' 2025-03-11 05:26:18 +00:00
harukin-expo-dev-env
2fb7d97d33 Merge commit '31618aab496a7979a39e534d023cb69103b06721' 2025-03-10 15:51:25 +00:00
harukin-expo-dev-env
76ebf8055b Merge commit '26cf84705abd4c9a69ac56fcade483a373089bf8' 2025-03-06 03:52:42 +00:00
harukin-expo-dev-env
7b572cd657 Merge commit 'fa882223d49fec390177131371b99a3101976759' 2025-03-05 11:51:51 +00:00
harukin-expo-dev-env
61fa0f8484 Merge commit 'ede2884c4a7b8a508cfd9f4f7cc87da4efe3db08' 2025-03-04 16:17:50 +00:00
harukin-expo-dev-env
935aaf2610 Merge commit '098cae50533368eec9cfdb933900c503ae46e850' 2025-03-04 15:01:08 +00:00
harukin-expo-dev-env
8ec53d6e06 Merge commit '0aaf171477659e632a23aeb6af39e83d5acf70c2' 2025-02-09 03:21:16 +00:00
harukin-expo-dev-env
e8b1a21a3b Merge commit '4a01c529b47cd85420a4c1feeac87164730dee94' 2025-01-15 12:38:58 +00:00
harukin-expo-dev-env
e6b89842a3 Merge commit '2776f17681ac2bf76a70c268c4db8921c5888a4f' 2025-01-14 07:42:06 +00:00
harukin-expo-dev-env
1fc5220405 Merge commit '58d3eae1d7797a74febc752f4185337a6eb95dad' 2025-01-09 09:56:21 +00:00
harukin-expo-dev-env
5489406578 Merge commit 'a7c4f689bc0762a63b662901f943a62827b765eb' 2024-11-22 05:40:18 +00:00
harukin-expo-dev-env
2b217e98c7 Merge commit 'd8108e2c9c60a60ba7f933af12af996b3cc0491c' 2024-11-22 05:19:00 +00:00
harukin-expo-dev-env
2c2e61a2fa Merge commit '6d3e6623a0774cb489efa3b81335db319fad2b2d' 2024-10-31 12:27:16 +00:00
harukin-expo-dev-env
c222b303df Merge commit '1346909bb74c80bedf1841aefda66a67adb98443' 2024-09-30 13:00:12 +00:00
harukin-expo-dev-env
8f8c095ecd Merge commit 'ab2a18b562c5fe18087f3140449fc7764b969df6' 2024-09-28 20:45:33 +00:00
harukin-expo-dev-env
e31e84ff34 Merge commit '9b91c4a50e1df914f66c6c5b33007caf4b0c9c32' 2024-09-20 15:18:49 +00:00
harukin-expo-dev-env
2c2b355a3e Merge commit '63209ac88878f70d0896dca429277d09ef1bf492' 2024-09-16 16:34:11 +00:00
harukin-expo-dev-env
5461087ca0 Merge commit 'c9a90809c6b178d5b85d51d47521d2541bbed7f6' 2024-09-16 16:31:31 +00:00
harukin-expo-dev-env
57c7285b6e Merge commit '628d2a42c94801ea42c939dd702ebd448176e62f' 2024-09-16 16:10:05 +00:00
harukin-expo-dev-env
5c134c95cc Merge commit '801b3dc3b0dde2f8a46962cfbca1599fb31cb6a4' 2024-09-02 02:29:04 +00:00
harukin-expo-dev-env
8781653fe8 Merge commit '9b4b4bd0d6572d0d0ffdb35ec72559bf035d07a3' 2024-06-05 10:27:22 +00:00
harukin-expo-dev-env
67ccc37c17 Merge commit '35e05e92f50ebca5d4666b9a008915b7f437f86c' 2024-06-05 09:16:05 +00:00
harukin-expo-dev-env
92caab03f5 Merge commit '52c2da333a97234bb27a25baf8af8479d53c3422' 2024-06-03 12:40:26 +00:00
harukin-expo-dev-env
dd3a57b3ae Merge commit 'b9b983a177d37db717b551f8b18d5f6c87aa0e23' 2024-06-03 12:20:23 +00:00
harukin-expo-dev-env
8df32b9c1d Merge commit '7eea78027649dec4dd7492efd8edfb0e61df5eea' 2024-04-28 10:33:20 +00:00
harukin-expo-dev-env
4b901d5015 Merge commit '5d711d37550d288142e3e768a2a0f42a7279d434' 2024-04-18 03:32:42 +00:00
harukin-expo-dev-env
fc5c62685a Merge commit '85e2ad329d89e57be4c094067de48525fe7673e3' 2024-04-18 03:21:26 +00:00
harukin-expo-dev-env
fbc98b2ff7 Merge commit '05167c810a8b727ed6527dbb07c0b9c1de53c171' 2024-04-06 17:19:41 +00:00
harukin-expo-dev-env
3502043176 Merge commit '0efab93a1451818339bfbed97545e6782eafcb9c' 2024-04-06 17:19:32 +00:00
harukin-expo-dev-env
2d0ad8d59e Merge commit 'dd7da102c43348a5420a4b616509d37c8d03a65f' 2024-03-27 15:52:25 +00:00
harukin-expo-dev-env
b5172df7a9 Merge commit '548d5d3747e6491ae421a716e85f7d60be4e41e4' 2024-03-27 15:02:35 +00:00
harukin-expo-dev-env
740d414d2d Merge commit '7e59b8c7bd7d01b35b5b0d6c5be93baed222f660' 2024-03-26 12:59:20 +00:00
harukin-expo-dev-env
8db3e6c218 Merge commit '736f9a65e92257237f347bbe2d02fe88f0e409d6' 2024-03-26 12:54:31 +00:00
harukin-expo-dev-env
fb35b01d2e Merge commit '720b627011a108979ce07a103aff2d728e68cab0' 2024-03-18 12:50:34 +00:00
harukin-expo-dev-env
5fa6b1f73e Merge commit '481ca0158d93b90af0306ed3d54786d61d6281e2' 2024-03-18 04:17:25 +00:00
harukin-expo-dev-env
c16f7401ea Merge commit '5864e821120ec726c6e96fbc8edc6a7172d5bbd7' 2024-03-18 04:04:16 +00:00
harukin-expo-dev-env
f260c5d2dd Merge commit 'a769ccb9512d8c246be82cb1f16673b13b39d4e6' 2024-03-15 18:04:11 +00:00
harukin-expo-dev-env
80e9f1a869 Merge commit 'd8ce2a7f8aac23e1001bbe16690d37c57d1c704f' 2024-03-13 13:27:05 +00:00
harukin-expo-dev-env
26096ba244 Merge commit '5806e2a259e03182d082fe454f4a1932ad9c5e53' 2024-03-13 13:10:22 +00:00
harukin-expo-dev-env
435a910ef9 Merge commit 'f2aed4b945d3e86381ebb964787fcb5aff35fdbd' 2024-03-12 16:47:26 +00:00
harukin-expo-dev-env
68b9236d65 Merge commit '7bd7d951048357c4c0b81d9048906275066e8526' 2024-03-12 16:36:37 +00:00
harukin-expo-dev-env
0148c12e08 Merge commit '75952f4edea5d59554322a5c611483a0052e4e97' 2024-03-12 14:37:09 +00:00
harukin-expo-dev-env
34dc62aee6 Merge commit 'c0cdad36837f27dee7c22930834272052d53d090' 2024-03-09 15:48:41 +00:00
harukin-OneMix4
54ae681f00 Merge commit '8a94b81052a4cf6b833bf298abcd89ed93a0ada2' 2024-01-27 01:37:29 +09:00
harukin-OneMix4
db40351fec Merge commit '431c4c9c0b426250adefbb42db7893233fabb55b' 2024-01-27 01:04:19 +09:00
harukin-OneMix4
2dd8e62f85 Merge commit '485102b5917eaa819c615dbaf47ee27f81a5751f' 2024-01-12 14:04:37 +09:00
harukin-OneMix4
0937fbb619 Merge commit '65080e68f2197f31b8a4ed4bf94ad325aa77e923' 2024-01-09 18:31:30 +09:00
harukin-OneMix4
32edab3289 Merge commit '87129c6815233ea6e42575b83a26c0a2c34a6e5c' 2024-01-09 18:21:41 +09:00
harukin-OneMix4
fa96e68770 Merge commit 'b54f2f6f7e1478cee3f1f3cacf2bc3949d45583f' 2024-01-04 02:38:22 +09:00
harukin-OneMix4
1681be4437 Merge commit '0e06b6636862fa4728d36bb9b934490087cadf02' 2024-01-04 00:47:55 +09:00
harukin-OneMix4
7f96c44e88 Merge commit '0f0d69b22d4e8b3aea9bec06855d38751881055a' 2024-01-03 01:35:33 +09:00
harukin-OneMix4
7d485c466c Merge commit '29c84bcc1ce7bd1d2512b65ef812a313507d8650' 2024-01-03 01:07:31 +09:00
harukin-OneMix4
7869cbee6d Merge commit 'a4e85ff2e6bccb35ecf04f6bdf51ac193585b765' 2023-12-29 03:50:28 +09:00
harukin-OneMix4
c2ad681891 Merge commit '57459d975bbe3a1b4e145331b57a638b2c2fead5' 2023-12-29 02:09:44 +09:00
harukin-OneMix4
163f9ead18 Merge commit 'b23b59eab349139656b2d32c4b19f744a937aa59' 2023-12-25 17:09:41 +09:00
harukin-OneMix4
611fc9953f Merge commit 'e38f550b94a6c89fd2840ff95af392e8e9f49589' 2023-12-25 15:46:35 +09:00
harukin-OneMix4
fd95e99874 Merge commit '9b70843e9c6ed45ebce609952b0202f5ebf877d7' 2023-12-25 05:47:23 +09:00
harukin-OneMix4
f5d0f993db update three month update 2023-12-13 15:56:40 +09:00
harukin-OneMix4
b7e763d265 update three month update 2023-09-17 15:44:19 +09:00
harukin-OneMix4
0afa6f402b 4.5.4.1 2023-07-16 23:00:26 +09:00
harukin-OneMix4
eacce53775 Merge commit '84d36b88cc71ed591c9a50f086ab38cd08177758' 2023-07-16 23:00:05 +09:00
harukin-OneMix4
47b1eb325e なんかマージミスった? 2023-07-16 19:40:44 +09:00
harukin-OneMix4
a145e2c24f Merge commit '711f0bbe9f00200c02fb70757d7c0ca2d0060ed4' 2023-07-16 19:36:57 +09:00
harukin-OneMix4
862742cd3e update build number 2023-06-19 14:03:37 +09:00
harukin-OneMix4
d5f2d367dd Merge commit 'a3e8e3cbbe13e4fc137937493a91708b1dff7cae' 2023-06-18 21:05:04 +09:00
harukin-OneMix4
b1f72f18b5 Merge commit '81f394d6ca2ae418c89452acabe11196e4b7e940' 2023-05-10 17:19:18 +09:00
harukin-OneMix4
d4d86fb7eb 4.5.3 2023-05-10 17:13:10 +09:00
harukin-OneMix4
056a059a68 Merge commit 'bb115afe358eb7f2fb724beb1da13b2be24780b4' 2023-05-10 17:11:56 +09:00
harukin-DeskMini
12676d59cf 4.5.2リリース(本開放/ニュース更新) 2023-03-30 03:28:34 +09:00
harukin-DeskMini
bca2b300d3 4.5.2リリース(封印開放) 2023-03-30 03:25:28 +09:00
harukin-DeskMini
406808ee58 Merge commit '47d84777e2a26235d5442a68ae3b6bcb07f580cb' 2023-03-30 03:21:25 +09:00
harukin-DeskMini
a8aadb7f08 4.5.1 2023-03-27 07:01:47 +09:00
harukin-DeskMini
b810405573 Merge commit '65dd6dd0fdd41f06cfb686d578f9d604185510d3' 2023-03-27 07:01:20 +09:00
harukin-DeskMini
6f62418426 Merge commit '9eca67901906b8852f1f1f2dfa814ed658514b4b' 2023-03-27 01:06:04 +09:00
harukin-DeskMini
88621dd1c8 Merge commit '98b22db4dbea611907c4d7a6a60b7323dbec7a76' 2023-03-27 01:02:57 +09:00
harukin-DeskMini
12d9c1f49d 封印忘れ物 2023-03-26 21:45:04 +09:00
harukin-DeskMini
30c2b8a2f9 4.5.0-リリース用一部封印 2023-03-26 21:42:49 +09:00
harukin-DeskMini
8bc7069c4e Merge commit '816d96d37be6537c9ebcf0be30c74fd154b80dc5' 2023-03-26 21:09:24 +09:00
harukin-OneMix4
737cd25539 Merge commit '64538e33f317c6fe395859673922f63c2e3b0514' 2023-03-02 17:37:40 +09:00
harukin-DeskMini
2c50e5af67 Merge commit 'e93fe7095ef717bd0ddebeb18f28038a374d83f6' 2023-02-01 17:16:10 +09:00
harukin-DeskMini
ea677b4da5 Merge commit '40fb55c8cf5462a40a499ddcc69cb5313e559f84' 2023-01-29 22:10:09 +09:00
harukin-DeskMini
4525443e39 Merge commit 'e35ab09002641b3813d2055ff8107bd8088c4629' 2023-01-29 09:11:58 +09:00
213 changed files with 6925 additions and 21234 deletions

10
.gitignore vendored
View File

@@ -4,7 +4,7 @@ node_modules/**/*
.pnp.js
# Expo
.expo/
.expo/*
.expo-shared
# Build outputs
@@ -52,11 +52,3 @@ Thumbs.db
*.log
*.tmp
.cache/
android/
!modules/**/android/
ios/
!modules/**/ios/
*.ipa
*.apk
*.aab

107
App.tsx
View File

@@ -1,5 +1,5 @@
import React, { useEffect } from "react";
import { Linking, Platform, UIManager } from "react-native";
import { 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, SheetManager } from "react-native-actions-sheet";
import { SheetProvider } from "react-native-actions-sheet";
import "./components/ActionSheetComponents/sheets";
import { TrainDelayDataProvider } from "./stateBox/useTrainDelayData";
import { SafeAreaProvider } from "react-native-safe-area-context";
@@ -20,9 +20,6 @@ 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",
@@ -40,85 +37,6 @@ 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,
@@ -133,17 +51,14 @@ export default function App() {
SheetProvider,
]);
return (
<AppThemeProvider>
<DeviceOrientationChangeProvider>
<SafeAreaProvider>
<StatusbarDetect />
<GestureHandlerRootView style={{ flex: 1 }}>
<ProviderTree>
<AppContainer />
</ProviderTree>
</GestureHandlerRootView>
</SafeAreaProvider>
</DeviceOrientationChangeProvider>
</AppThemeProvider>
<DeviceOrientationChangeProvider>
<SafeAreaProvider>
<GestureHandlerRootView style={{ flex: 1 }}>
<ProviderTree>
<AppContainer />
</ProviderTree>
</GestureHandlerRootView>
</SafeAreaProvider>
</DeviceOrientationChangeProvider>
);
}

127
Apps.tsx
View File

@@ -1,21 +1,14 @@
import React from "react";
import { NavigationContainer, DarkTheme, DefaultTheme } from "@react-navigation/native";
import { NavigationContainer, NavigationContainerRef } from "@react-navigation/native";
import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";
import { Animated, Platform, ActivityIndicator, View, StyleSheet, useColorScheme } from "react-native";
import { useNavigationState } from "@react-navigation/native";
import { Platform } from "react-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;
@@ -32,61 +25,10 @@ type TabProps = {
isInfo?: boolean;
};
const Tab = createBottomTabNavigator<RootTabParamList>();
export function AppContainer() {
const Tab = createBottomTabNavigator<RootTabParamList>();
const { areaInfo, areaIconBadgeText, isInfo } = useAreaInfo();
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 navigationRef = React.useRef<NavigationContainerRef<RootTabParamList>>(null);
const getTabProps = (
name: keyof RootTabParamList,
@@ -111,68 +53,19 @@ 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={rootNavigationRef}
linking={linking}
theme={isDark ? DarkTheme : DefaultTheme}
onStateChange={(state) => {
const hasExtra = state?.routes?.some((r) => (r.state?.index ?? 0) > 0) ?? false;
setIsExtraWindowOpen(hasExtra);
}}
>
<NavigationContainer ref={navigationRef}>
{/* @ts-expect-error - Tab.Navigator type definition issue */}
<Tab.Navigator
initialRouteName="topMenu"
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>
),
};
screenOptions={{
lazy: false,
tabBarHideOnKeyboard: Platform.OS === "android",
animation: "shift",
}}
>
<Tab.Screen
{...getTabProps("positions", "走行位置", "bar-chart", "AntDesign")}
{...getTabProps("positions", "走行位置", "barchart", "AntDesign")}
component={Top}
/>
<Tab.Screen

View File

@@ -1,16 +1,14 @@
import React, { CSSProperties } from "react";
import { Alert, BackHandler, View, ViewProps } from "react-native";
import { 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);
@@ -25,25 +23,17 @@ export default ({ route }) => {
return true;
};
const subscription = BackHandler.addEventListener("hardwareBackPress", onHardwareBack);
return () => subscription.remove();
BackHandler.addEventListener("hardwareBackPress", onHardwareBack);
return () => BackHandler.removeEventListener("hardwareBackPress", onHardwareBack);
}, [canGoBack, goBack])
);
return (
<View style={{ height: "100%", backgroundColor: fixed.primary }}>
<View style={styles}>
<WebView
source={{ uri }}
allowsBackForwardNavigationGestures
ref={webViewRef}
onNavigationStateChange={(navState) => {
setCanGoBack(navState.canGoBack);
if (navState.url === "https://unyohub.2pd.jp/integration/succeeded.php") {
goBack();
Alert.alert("鉄道運用HUBへの投稿完了", "運用HUBからのこのアプリへのデータ反映には暫く時間がかかりますので、しばらくお待ちください。", [
{ text: "完了" },
]);
}
}}
onNavigationStateChange={(navState) => setCanGoBack(navState.canGoBack)}
onMessage={(event) => {
const { data } = event.nativeEvent;
const { type } = JSON.parse(data);
@@ -55,3 +45,7 @@ export default ({ route }) => {
</View>
);
};
const styles: ViewProps["style"] = {
height: "100%",
backgroundColor: "#0099CC",
};

View File

@@ -1,6 +1,6 @@
import React, { useEffect, useRef, useState } from "react";
import { createStackNavigator } from "@react-navigation/stack";
import { useWindowDimensions, Platform, useColorScheme } from "react-native";
import { useWindowDimensions, Platform } from "react-native";
import Constants from "expo-constants";
import { Dimensions, StatusBar } from "react-native";
@@ -30,8 +30,6 @@ 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) => {
@@ -58,8 +56,6 @@ 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 -
@@ -68,11 +64,7 @@ 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 =
@@ -83,15 +75,15 @@ export function MenuPage() {
}, [height, tabBarHeight, width]);
useEffect(() => {
const unsubscribe = addListener("tabPress", (e) => {
scrollRef.current?.scrollTo({
y: mapHeightRef.current - 80,
scrollRef.current.scrollTo({
y: mapHeight - 80,
animated: true,
});
setMapMode(false);
AS.getItem(STORAGE_KEYS.FAVORITE_STATION)
.then((d) => {
const returnData = JSON.parse(d);
if (favoriteStationRef.current.toString() != d) {
if (favoriteStation.toString() != d) {
setFavoriteStation(returnData);
}
})
@@ -103,12 +95,9 @@ export function MenuPage() {
});
return unsubscribe;
}, [navigation]);
}, [navigation, mapHeight, favoriteStation, setFavoriteStation]);
return (
<Stack.Navigator
id={null}
screenOptions={{ contentStyle: { backgroundColor: bgColor } }}
>
<Stack.Navigator id={null}>
<Stack.Screen
name="menu"
options={{

View File

@@ -1,10 +1,12 @@
import React, { FC } from "react";
import { Platform, StatusBar, useColorScheme } from "react-native";
import { Platform, StatusBar, View } from "react-native";
const StatusbarDetect: FC = () => {
const isDark = useColorScheme() === "dark";
const barStyle = isDark ? "light-content" : "dark-content";
return <StatusBar barStyle={barStyle} translucent backgroundColor="transparent" />;
if (Platform.OS == "ios") {
return <StatusBar barStyle="dark-content" />;
} else if (Platform.OS == "android") {
return <View />;
}
};
export default StatusbarDetect;

22
Top.tsx
View File

@@ -1,7 +1,6 @@
import React, { useCallback, useEffect, useRef } from "react";
import React, { useEffect } 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";
@@ -20,15 +19,9 @@ 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" });
@@ -38,29 +31,26 @@ export const Top = () => {
return unsubscribe;
}, []);
const goToTrainMenu = useCallback(() => {
const goToTrainMenu = () => {
if (Platform.OS === "web") {
Linking.openURL("https://train.jr-shikoku.co.jp/");
setTimeout(() => navigate("topMenu", { screen: "menu" }), 100);
return;
}
if (!isFocused()) navigate("positions", { screen: "Apps" });
else if (mapSwitchRef.current == "true")
else if (mapSwitch == "true")
navigate("positions", { screen: "trainMenu" });
else webview.current?.injectJavaScript(`AccordionClassEvent()`);
return;
}, [isFocused, navigate, webview]);
};
useEffect(() => {
const unsubscribe = addListener("tabPress", goToTrainMenu);
return unsubscribe;
}, [addListener, goToTrainMenu]);
}, [addListener, mapSwitch]);
return (
<Stack.Navigator
id={null}
screenOptions={{ contentStyle: { backgroundColor: bgColor } }}
>
<Stack.Navigator id={null} detachInactiveScreens={false}>
<Stack.Screen
name="Apps"
options={{

View File

@@ -2,7 +2,6 @@ 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;
@@ -17,7 +16,7 @@ export const UpdateAsync = () => {
50
);
}
Updates.fetchUpdateAsync().then(() => Updates.reloadAsync());
Updates.fetchUpdateAsync().then(Updates.reloadAsync);
return;
})
.catch((e) => {

View File

@@ -2,14 +2,12 @@
"expo": {
"name": "JR四国非公式",
"slug": "jrshikoku",
"scheme": "jrshikoku",
"platforms": [
"ios",
"android",
"web"
],
"version": "7.0.0",
"userInterfaceStyle": "automatic",
"version": "6.0.4",
"orientation": "default",
"icon": "./assets/icons/s8600.png",
"splash": {
@@ -24,10 +22,9 @@
"**/*"
],
"ios": {
"buildNumber": "60",
"supportsTablet": true,
"buildNumber": "50",
"supportsTablet": false,
"bundleIdentifier": "jrshikokuinfo.xprocess.hrkn",
"appleTeamId": "54CRDT797G",
"config": {
"googleMapsApiKey": "AIzaSyAVGDTjBkR_0wkQiNkoo5WDLhqXCjrjk8Y"
},
@@ -37,54 +34,20 @@
"0003",
"FE00"
],
"ITSAppUsesNonExemptEncryption": false,
"NSSupportsLiveActivities": true,
"NSSupportsLiveActivitiesFrequentUpdates": true
"ITSAppUsesNonExemptEncryption": false
},
"entitlements": {
"com.apple.developer.nfc.readersession.formats": [
"TAG"
],
"com.apple.security.application-groups": [
"group.jrshikokuinfo.xprocess.hrkn"
]
}
},
"android": {
"package": "jrshikokuinfo.xprocess.hrkn",
"versionCode": 30,
"intentFilters": [
{
"action": "VIEW",
"data": [
{
"scheme": "jrshikoku"
}
],
"category": [
"BROWSABLE",
"DEFAULT"
]
},
{
"action": "VIEW",
"data": [
{
"scheme": "jrshikoku",
"host": "open",
"pathPrefix": "/felica"
}
],
"category": [
"BROWSABLE",
"DEFAULT"
]
}
],
"versionCode": 28,
"permissions": [
"ACCESS_FINE_LOCATION",
"NFC",
"POST_NOTIFICATIONS",
"android.permission.ACCESS_COARSE_LOCATION",
"android.permission.ACCESS_FINE_LOCATION"
],
@@ -104,19 +67,7 @@
"policy": "sdkVersion"
},
"plugins": [
"./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-font",
"expo-localization",
[
"expo-screen-orientation",
@@ -173,16 +124,6 @@
"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"
}
]
}
@@ -511,29 +452,6 @@
}
}
]
],
[
"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"
}
]
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

View File

@@ -1,21 +0,0 @@
/**
* 観光スポットデータ
* 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,
},
];

Binary file not shown.

View File

@@ -1,15 +1,6 @@
module.exports = function(api) {
api.cache(true);
return {
presets: [
['babel-preset-expo', {
'react-compiler': { enabled: false },
lazyImports: true,
}],
],
plugins: [
'./plugins/babel-plugin-disable-font-scaling',
'react-native-reanimated/plugin',
],
presets: ['babel-preset-expo'],
};
};

View File

@@ -1,7 +1,6 @@
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;
@@ -9,7 +8,6 @@ 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);
@@ -32,13 +30,13 @@ export const DataConnectedButton: FC<{
}}
key={station + time}
>
<View style={{ flexDirection: "row", backgroundColor: colors.backgroundTertiary }}>
<View style={{ flexDirection: "row", backgroundColor: "#f5f5f5" }}>
<View
style={{
padding: 8,
flexDirection: "row",
borderBottomWidth: 1,
borderBottomColor: colors.borderLight,
borderBottomColor: "#f0f0f0",
flex: 1,
}}
>
@@ -51,12 +49,12 @@ export const DataConnectedButton: FC<{
height: "10%",
}}
/>
<Text style={{ fontSize:16, fontFamily: "DiaPro", color: colors.text }}>
<Text style={{ fontSize:16, fontFamily: "DiaPro" }}>
{se === "増" ? "⬐" : "↳"}
</Text>
<Text style={{ fontSize: 20, color: colors.textLink }}>{time}</Text>
<Text style={{ fontSize: 20, color: "#0000EE" }}>{time}</Text>
<View style={{ flex: 1 }} />
<Text style={{ fontSize: 18, width: 50, color: colors.text }}>
<Text style={{ fontSize: 18, width: 50 }}>
{se === "増" ? "増結" : "解結"}
</Text>
</View>

View File

@@ -11,7 +11,6 @@ import {
parseSeString,
} from "@/utils/seUtils";
import type { SeTypes } from "@/types";
import { useThemeColors } from "@/lib/theme";
type seTypes =
| "発編"
@@ -64,7 +63,6 @@ export const EachStopList: FC<props> = ({
array,
isNotService = false,
}) => {
const { colors: themeColors, isDark } = useThemeColors();
const [station, se, time, platformNum] = i.split(",") as [
string,
seTypes,
@@ -72,22 +70,22 @@ export const EachStopList: FC<props> = ({
string
]; // 阿波池田,発,6:21,1
let beforeSameStationData = null;
// 発(通常発・休発・休発編)の場合、前の着(通常着・休着・休着編)と統合する
if (se.includes("発")) {
// 運休系でない通常の発のみ、前の着を統合する
// 休編(非推奨)は発着が不明なため、次の発と統合する
if ((se.includes("発") && !se.includes("休")) || se === "休編") {
if (index > 0) {
const beforeData = array[index - 1].split(",") as [string, seTypes, string];
// 前が着(通常着でも休着でも)の場合は統合
if (beforeData[0] == station && beforeData[1].includes("着")) {
if (beforeData[0] == station) {
beforeSameStationData = beforeData;
}
}
}
let afterSameStationData = null;
// 着(通常着・休着・休着編)の場合、次の発(通常発・休発・休発編)と統合される(非表示)
if (se.includes("着")) {
// 運休系でない通常の着のみ、次の発と統合する
// 運休着(休着、休着編)は独立して表示する必要がある
if (se.includes("着") && !se.includes("休")) {
const afterData = array[index + 1]?.split(",") as [string, seTypes, string];
// 次が発(通常発でも休発でも)なら、この着を非表示にして次の発で両方表示
if (afterData && afterData[0] == station && afterData[1].includes("発")) {
if (afterData && afterData[0] == station) {
afterSameStationData = afterData;
return <></>;
}
@@ -137,8 +135,7 @@ export const EachStopList: FC<props> = ({
isCommunity,
isCanceled,
isDelayed,
isNotService,
isDark
isNotService
);
// 打ち消し線用の通常色(遅延していない時の色)
const normalColors = getStopListColors(
@@ -146,8 +143,7 @@ export const EachStopList: FC<props> = ({
isCommunity,
isCanceled,
false,
isNotService,
isDark
isNotService
);
// beforeSameStationData用の色設定
@@ -165,16 +161,14 @@ export const EachStopList: FC<props> = ({
beforeIsCommunity,
isCanceled,
isDelayed,
isNotService,
isDark
isNotService
);
const beforeNormalColors = getStopListColors(
beforeIsThrough,
beforeIsCommunity,
isCanceled,
false,
isNotService,
isDark
isNotService
);
beforeTimeTextColor = beforeColors.timeText;
beforeNormalTimeTextColor = beforeNormalColors.timeText;
@@ -239,7 +233,7 @@ export const EachStopList: FC<props> = ({
padding: 8,
flexDirection: "row",
borderBottomWidth: 1,
borderBottomColor: themeColors.borderLight,
borderBottomColor: "#f0f0f0",
flex: 1,
}}
>
@@ -355,7 +349,6 @@ 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 == "通過" ||
@@ -373,7 +366,7 @@ const StationNumbersBox: FC<{ stn: string; se: seTypes }> = (props) => {
<View style={{ flex: 1 }} />
<Text
style={{
color: fixed.textOnPrimary,
color: "white",
textAlign: "center",
fontSize: 10,
fontWeight: "bold",

View File

@@ -1,7 +1,6 @@
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;
@@ -22,7 +21,6 @@ export const PositionBox: FC<stateBox> = (props) => {
platformDescription,
lineNumber,
} = props;
const { colors } = useThemeColors();
let firstText = "";
let secondText = "";
let marginText = "";
@@ -48,22 +46,22 @@ export const PositionBox: FC<stateBox> = (props) => {
}
}
return (
<View style={{ ...(mode == 2 ? boxStyle2 : boxStyle), backgroundColor: colors.surface, ...style }}>
<Text style={{ fontSize: 12, color: colors.textAccent }}>{title}</Text>
<View style={{ ...(mode == 2 ? boxStyle2 : boxStyle), ...style }}>
<Text style={{ fontSize: 12, color: "#0099CC" }}>{title}</Text>
<View style={{ flex: 1 }} />
<View style={{ flexDirection: mode == 2 ? "row" : "column" }}>
{firstText && (
<Text style={[mode == 2 ? boxTextStyle2 : (isBetween ? boxTextStyle : boxTextStyleBig), { color: colors.textAccent }]}>
<Text style={mode == 2 ? boxTextStyle2 : (isBetween ? boxTextStyle : boxTextStyleBig)}>
{firstText}
</Text>
)}
{marginText && (
<Text style={{ color: colors.textAccent, textAlign: "right" }}>
<Text style={{ color: "#0099CC", textAlign: "right" }}>
{marginText}
</Text>
)}
{secondText && (
<Text style={[mode == 2 ? boxTextStyle2 :(isBetween ? boxTextStyle : boxTextStyleMini), { color: colors.textAccent }]}>
<Text style={mode == 2 ? boxTextStyle2 :(isBetween ? boxTextStyle : boxTextStyleMini)}>
{secondText}
</Text>
)}
@@ -74,7 +72,6 @@ export const PositionBox: FC<stateBox> = (props) => {
style={{
...{ ...(mode == 2 ? boxTextStyle2 : boxTextStyle) },
fontSize: 10,
color: colors.textAccent,
}}
>
{" " + externalText}
@@ -87,33 +84,39 @@ 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",
};

View File

@@ -1,15 +1,13 @@
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: colors.surface,
backgroundColor: "#ffffffc2",
flexDirection: "row",
}}
>
@@ -18,18 +16,18 @@ export const ScrollStickyContent = (props) => {
padding: 8,
flexDirection: "row",
borderBottomWidth: 1,
borderBottomColor: colors.border,
borderBottomColor: "#ffffffc2",
flex: 1,
}}
>
<Text style={{ fontSize: 20, color: colors.text }}></Text>
<Text style={{ fontSize: 20 }}></Text>
<View style={{ flex: 1 }} />
<View style={{ flexDirection: "row" }}>
{!isNaN(currentTrainData?.delay) && currentTrainData?.delay != 0 && (
<Text
style={{
fontSize: 15,
color: colors.text,
color: "black",
position: "absolute",
right: 110,
textAlign: "right",
@@ -43,10 +41,10 @@ export const ScrollStickyContent = (props) => {
style={{
fontSize: 20,
color: isNaN(currentTrainData?.delay)
? colors.text
? "black"
: currentTrainData?.delay == 0
? colors.text
: colors.textError,
? "black"
: "red",
width: 60,
}}
>
@@ -72,7 +70,6 @@ export const ScrollStickyContent = (props) => {
textAlign: "center",
textAlignVertical: "center",
opacity: haveThrough ? 1 : 0,
color: colors.text,
}}
>
({showThrew ? "▼" : "▶"})

View File

@@ -1,14 +1,12 @@
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;
@@ -24,7 +22,7 @@ export const ShowSpecialTrain = ({
style={{
padding: 10,
flexDirection: "row",
borderColor: colors.textAccent,
borderColor: "blue",
borderWidth: 1,
margin: 10,
borderRadius: 5,
@@ -32,7 +30,7 @@ export const ShowSpecialTrain = ({
}}
>
<Text
style={{ fontSize: 18, fontWeight: "bold", color: colors.text }}
style={{ fontSize: 18, fontWeight: "bold", color: "black" }}
>
:({ids})
</Text>

View File

@@ -1,6 +1,5 @@
import React, { CSSProperties, FC } from "react";
import { View, Text, StyleProp, TextStyle, ViewStyle } from "react-native";
import { useThemeColors } from "@/lib/theme";
type stateBox = {
text: string;
@@ -11,13 +10,12 @@ 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), backgroundColor: colors.surface, ...style }}>
<Text style={{ fontSize: 12, color: colors.textAccent }}>{title}</Text>
<View style={{ ...(mode == 2 ? boxStyle2 : boxStyle), ...style }}>
<Text style={{ fontSize: 12, color: "#0099CC" }}>{title}</Text>
<View style={{ flex: 1 }} />
<View style={{ flexDirection: mode == 2 ? "row" : "column" }}>
<Text style={[mode == 2 ? boxTextStyle2 : boxTextStyle, { color: colors.textAccent }]}>{text}</Text>
<Text style={mode == 2 ? boxTextStyle2 : boxTextStyle}>{text}</Text>
</View>
{endText && (
<View style={{ flexDirection: mode == 2 ? "row" : "column" }}>
@@ -25,7 +23,6 @@ export const StateBox: FC<stateBox> = (props) => {
style={{
...{ ...(mode == 2 ? boxTextStyle2 : boxTextStyle) },
fontSize: 10,
color: colors.textAccent,
}}
>
{endText}
@@ -37,22 +34,26 @@ 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",
};

View File

@@ -1,5 +1,5 @@
import React, { useState, useEffect, FC } from "react";
import { View, TouchableOpacity, useWindowDimensions, Text } from "react-native";
import { View, TouchableOpacity, useWindowDimensions } from "react-native";
import { StateBox } from "./StateBox";
import { PositionBox } from "./PositionBox";
import { useDeviceOrientationChange } from "../../../stateBox/useDeviceOrientationChange";
@@ -129,8 +129,6 @@ export const TrainDataView:FC<props> = ({
setDialog(true);
};
return (
<>
<StationPosPushDialog

View File

@@ -18,15 +18,13 @@ type ColorScheme = {
* @param isCanceled 運休かどうか
* @param isDelayed 遅延しているかどうか
* @param isNotService 回送列車かどうか
* @param isDark ダークモードかどうか
*/
export const getStopListColors = (
isThrough: boolean,
isCommunity: boolean,
isCanceled: boolean,
isDelayed: boolean,
isNotService: boolean = false,
isDark: boolean = false
isNotService: boolean = false
): ColorScheme => {
// 最優先: 回送列車の場合
if (isNotService) {
@@ -34,47 +32,45 @@ export const getStopListColors = (
// 回送 + 運休
if (isThrough) {
return {
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'),
background: '#1a1a1a', // 非常に濃いグレー
text: isCommunity ? '#8090c0' : '#909090', // 暗めの青灰色 or 暗めのグレー
timeText: isDelayed
? (isCommunity ? '#aa7799' : '#aa7777') // 遅延時: 暗めのピンク系の赤
: (isCommunity ? '#8090c0' : '#909090'),
seText: isCommunity ? '#8090c0' : '#909090',
opacity: '0.5',
};
} else {
return {
background: isDark ? '#2a2a2a' : '#3a3a3a',
text: isCommunity ? '#8090c0' : (isDark ? '#c0c0c0' : '#b0b0b0'),
background: '#3a3a3a', // 濃いグレー
text: isCommunity ? '#8090c0' : '#b0b0b0', // 暗めの青灰色 or 明るめのグレー
timeText: isDelayed
? (isCommunity ? '#bb8899' : '#bb8888')
: (isCommunity ? '#8090c0' : (isDark ? '#c0c0c0' : '#b0b0b0')),
seText: isCommunity ? '#8090c0' : (isDark ? '#c0c0c0' : '#b0b0b0'),
? (isCommunity ? '#bb8899' : '#bb8888') // 遅延時: 暗めのピンク系の赤
: (isCommunity ? '#8090c0' : '#b0b0b0'),
seText: isCommunity ? '#8090c0' : '#b0b0b0',
opacity: '0.8',
};
}
} else if (isThrough) {
// 回送 + 通過
return {
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'),
background: '#e8e8e8', // 薄いグレー
text: isCommunity ? '#6677cc' : '#777777', // 暗めの青 or グレー
timeText: isDelayed
? (isCommunity ? '#cc5577' : '#dd5555') // 遅延時: 暗めの赤
: (isCommunity ? '#6677cc' : '#777777'),
seText: isCommunity ? '#6677cc' : '#777777',
opacity: '0.6',
};
} else {
// 回送 + 通常停車
return {
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'),
background: isDelayed ? '#f5f0f0' : '#f5f5f5', // 遅延時は少し赤みのあるグレー
text: isCommunity ? '#4455aa' : '#555555', // 暗めの青 or ダークグレー
timeText: isDelayed
? (isCommunity ? '#bb3355' : '#cc0000') // 遅延時: 暗めの赤
: (isCommunity ? '#4455aa' : '#555555'),
seText: isCommunity ? '#4455aa' : '#555555',
opacity: '0.85',
};
}
@@ -85,10 +81,10 @@ export const getStopListColors = (
if (isThrough) {
// 通過系 + 運休
return {
background: isDark ? '#1e1e2e' : '#2a2a2a',
text: isCommunity ? '#a8b5ff' : '#c0c0c0',
timeText: isDelayed
? (isCommunity ? '#dd99bb' : '#dd9999')
background: '#2a2a2a', // 濃いグレー
text: isCommunity ? '#a8b5ff' : '#c0c0c0', // 薄い青 or 薄いグレー
timeText: isDelayed
? (isCommunity ? '#dd99bb' : '#dd9999') // 遅延時: 薄いピンク系の赤
: (isCommunity ? '#a8b5ff' : '#c0c0c0'),
seText: isCommunity ? '#a8b5ff' : '#c0c0c0',
opacity: '0.6',
@@ -96,10 +92,10 @@ export const getStopListColors = (
} else {
// 通常停車 + 運休
return {
background: isDark ? '#3a3a4a' : '#5a5a5a',
text: isCommunity ? '#a8b5ff' : '#ffffff',
background: '#5a5a5a', // 中程度のグレー
text: isCommunity ? '#a8b5ff' : '#ffffff', // 薄い青 or 白
timeText: isDelayed
? (isCommunity ? '#ffaacc' : '#ffaaaa')
? (isCommunity ? '#ffaacc' : '#ffaaaa') // 遅延時: 明るいピンク系の赤
: (isCommunity ? '#a8b5ff' : '#ffffff'),
seText: isCommunity ? '#a8b5ff' : '#ffffff',
opacity: '0.9',
@@ -110,14 +106,12 @@ export const getStopListColors = (
// 次: 通過系の場合
if (isThrough) {
return {
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'),
background: isCommunity ? '#f0f4ff' : '#fafafa', // 薄い青背景 or 薄いグレー
text: isCommunity ? '#5577ff' : '#888888', // 中程度の青 or グレー
timeText: isDelayed
? (isCommunity ? '#dd5588' : '#ff6666') // 遅延時: コミュニティは紫がかった赤、通常は明るい赤
: (isCommunity ? '#5577ff' : '#888888'),
seText: isCommunity ? '#5577ff' : '#888888',
opacity: '0.5',
};
}
@@ -126,27 +120,19 @@ export const getStopListColors = (
if (isCommunity) {
// コミュニティ投稿
return {
background: isDark
? (isDelayed ? '#2a1a2a' : '#1e1e2e')
: (isDelayed ? '#fff5f5' : '#ffffff'),
text: isDark ? '#8899ff' : '#3355dd',
timeText: isDelayed
? (isDark ? '#ee4488' : '#cc2266')
: (isDark ? '#8899ff' : '#3355dd'),
seText: isDark ? '#8899ff' : '#3355dd',
background: isDelayed ? '#fff5f5' : '#ffffff', // 遅延時は薄い赤背景
text: '#3355dd', // 明確な青
timeText: isDelayed ? '#cc2266' : '#3355dd', // 遅延時: 紫がかった赤
seText: '#3355dd',
opacity: '0.95',
};
} else {
// 公式データ
return {
background: isDark
? (isDelayed ? '#2a1a1a' : '#1e1e2e')
: (isDelayed ? '#fff5f5' : '#ffffff'),
text: isDark ? '#e0e0e0' : '#000000',
timeText: isDelayed
? (isDark ? '#ff5555' : '#dd0000')
: (isDark ? '#e0e0e0' : '#000000'),
seText: isDark ? '#e0e0e0' : '#000000',
background: isDelayed ? '#fff5f5' : '#ffffff', // 遅延時は薄い赤背景
text: '#000000', // 黒
timeText: isDelayed ? '#dd0000' : '#000000', // 遅延時: 標準的な赤
seText: '#000000',
opacity: '0.95',
};
}

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useRef, useState } from "react";
import React, { useEffect, useState } from "react";
import {
View,
Text,
@@ -8,7 +8,7 @@ import {
BackHandler,
Linking,
} from "react-native";
import { SheetManager } from "react-native-actions-sheet";
import { SheetManager, useScrollHandlers } from "react-native-actions-sheet";
import { getTrainType } from "../../lib/getTrainType";
import { customTrainDataDetector } from "../custom-train-data";
import { useDeviceOrientationChange } from "../../stateBox/useDeviceOrientationChange";
@@ -22,7 +22,6 @@ 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
@@ -43,12 +42,14 @@ export const EachTrainInfoCore = ({
}) => {
const { stationList } = useStationList();
const { allCustomTrainData } = useAllTrainDiagram();
const { colors, fixed } = useThemeColors();
const { setTrainInfo } = useTrainMenu();
const { height } = useWindowDimensions();
const { isLandscape } = useDeviceOrientationChange();
const scrollRef = useRef<any>(null);
const scrollHandlers = actionSheetRef
? //@ts-ignore
useScrollHandlers("scrollview-1", actionSheetRef)
: null;
// Custom hooks for data management
const { trainData, setTrainData, trueTrainID } = useTrainDiagramData(
data.trainNum
@@ -77,7 +78,7 @@ export const EachTrainInfoCore = ({
useAutoScroll(
points,
trainDataWithThrough,
scrollRef,
scrollHandlers,
isJumped,
setIsJumped,
setShowThrew
@@ -134,10 +135,10 @@ export const EachTrainInfoCore = ({
return (
<View
style={{
backgroundColor: fixed.primary,
backgroundColor: "#0099CC",
borderTopLeftRadius: 5,
borderTopRightRadius: 5,
borderColor: colors.border,
borderColor: "dark",
borderWidth: 1,
}}
>
@@ -147,7 +148,7 @@ export const EachTrainInfoCore = ({
height: 6,
width: 45,
borderRadius: 100,
backgroundColor: colors.borderLight,
backgroundColor: "#f0f0f0",
marginVertical: 10,
alignSelf: "center",
}}
@@ -163,18 +164,18 @@ export const EachTrainInfoCore = ({
navigate={navigate}
from={from}
fontLoaded={true}
scrollRef={scrollRef}
scrollHandlers={scrollHandlers}
/>
<DynamicHeaderScrollView
from={from}
styles={styles as any}
scrollRef={scrollRef}
scrollHandlers={scrollHandlers}
containerProps={{
style: {
maxHeight: isLandscape ? height - 94 : (height / 100) * 70,
backgroundColor:
customTrainType.data === "notService" ? "#777777ff" : colors.surface,
customTrainType.data === "notService" ? "#777777ff" : "white",
},
}}
shortHeader={
@@ -206,7 +207,7 @@ export const EachTrainInfoCore = ({
}
>
{customTrainType.data === "notService" && (
<Text style={{ backgroundColor: colors.surface, fontWeight: "bold" }}>
<Text style={{ backgroundColor: "#ffffffc2", fontWeight: "bold" }}>
</Text>
)}
@@ -218,10 +219,10 @@ export const EachTrainInfoCore = ({
onPress={() =>
extendToHeadStation(item.station, item.dia, index)
}
style={[styles.extendStationButton, { borderColor: colors.textAccent }]}
style={styles.extendStationButton}
key={`${item.station}-head${index}`}
>
<Text style={[styles.extendStationText, { color: colors.text }]}>
<Text style={styles.extendStationText}>
</Text>
</TouchableOpacity>
@@ -237,9 +238,9 @@ export const EachTrainInfoCore = ({
onPress={() =>
Linking.openURL(`https://twitter.com/search?q=${data.trainNum}`)
}
style={[styles.twitterSearchButton, { borderColor: colors.textAccent, backgroundColor: colors.backgroundOverlay }]}
style={styles.twitterSearchButton}
>
<Text style={[styles.extendStationText, { color: colors.text }]}>Twitterで検索</Text>
<Text style={styles.extendStationText}>Twitterで検索</Text>
</TouchableOpacity>
)}
{trainDataWithThrough.map((item, index, array) =>
@@ -250,7 +251,7 @@ export const EachTrainInfoCore = ({
openTrainInfo={openTrainInfo}
/>
) : item.split(",")[1].includes(".") ? (
<React.Fragment key={`${item}-skip`} />
<></>
) : (
<EachStopList
i={item}
@@ -266,7 +267,7 @@ export const EachTrainInfoCore = ({
/>
)
)}
<Text style={[styles.customDataNote, { color: colors.textSecondary }]}>
<Text style={styles.customDataNote}>
,
</Text>
{tailStation.length > 0 &&
@@ -275,17 +276,17 @@ export const EachTrainInfoCore = ({
!showTailStation.includes(index) && (
<TouchableOpacity
onPress={() => extendToTailStation(station, dia, index)}
style={[styles.extendStationButton, { borderColor: colors.textAccent }]}
style={styles.extendStationButton}
key={`${station}-tail${index}`}
>
<Text style={[styles.extendStationText, { color: colors.text }]}>
<Text style={styles.extendStationText}>
</Text>
</TouchableOpacity>
)
)}
<View style={[styles.bottomSpacer, { borderBottomColor: colors.borderLight }]} />
<View style={styles.bottomSpacer} />
</DynamicHeaderScrollView>
</View>
);
@@ -319,6 +320,7 @@ const styles = StyleSheet.create({
extendStationText: {
fontSize: 18,
fontWeight: "bold",
color: "black",
},
twitterSearchButton: {
padding: 10,
@@ -331,12 +333,14 @@ const styles = StyleSheet.create({
backgroundColor: "#ffffffc2",
},
customDataNote: {
backgroundColor: "#ffffffc2",
},
bottomSpacer: {
flexDirection: "row",
padding: 8,
borderBottomWidth: 1,
borderBottomColor: "#f0f0f0",
backgroundColor: "#ffffffc2",
flex: 1,
},
});

View File

@@ -1,5 +1,11 @@
import React, { FC, useMemo, useState } from "react";
import { Text, View, TextStyle, TouchableOpacity } from "react-native";
import React, { FC, useMemo } from "react";
import {
Text,
View,
TextStyle,
TouchableOpacity,
useWindowDimensions,
} from "react-native";
import { SheetManager } from "react-native-actions-sheet";
import { migrateTrainName } from "../../../lib/eachTrainInfoCoreLib/migrateTrainName";
import { TrainIconStatus } from "./trainIconStatus";
@@ -13,9 +19,6 @@ 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 };
@@ -27,12 +30,13 @@ type Props = {
navigate: NavigateFunction;
from: string;
fontLoaded: boolean;
scrollRef: any;
scrollHandlers: any;
};
const textConfig: TextStyle = {
fontSize: 17,
fontWeight: "bold",
color: "white",
};
export const HeaderText: FC<Props> = ({
@@ -44,29 +48,15 @@ export const HeaderText: FC<Props> = ({
tailStation,
navigate,
from,
scrollRef,
scrollHandlers,
}) => {
const { limited, trainNum } = data;
const { fixed } = useThemeColors();
const { height, width } = useWindowDimensions();
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 [
@@ -78,10 +68,7 @@ export const HeaderText: FC<Props> = ({
priority,
uwasa,
trainInfoUrl,
directions,
customTrainData,
] = useMemo(() => {
const result = customTrainDataDetector(trainNum, allCustomTrainData);
const {
type,
train_name,
@@ -91,14 +78,15 @@ export const HeaderText: FC<Props> = ({
uwasa,
train_info_url,
to_data,
directions,
} = result;
} = customTrainDataDetector(trainNum, allCustomTrainData);
const [typeString, fontAvailable, isOneMan] = getStringConfig(
type,
trainNum,
);
switch (true) {
case train_name !== "":
// 特急の場合は、列車名を取得
// 列番対称データがある場合はそれから列車番号を取得
return [
typeString,
train_name +
@@ -111,8 +99,6 @@ export const HeaderText: FC<Props> = ({
priority,
uwasa,
train_info_url,
directions,
result,
];
case trainData[trainData.length - 1] === undefined:
return [
@@ -124,10 +110,9 @@ export const HeaderText: FC<Props> = ({
priority,
uwasa,
train_info_url,
directions,
result,
];
case to_data && to_data !== "":
// 行先がある場合は、行先を取得
return [
typeString,
to_data + "行き",
@@ -137,10 +122,9 @@ export const HeaderText: FC<Props> = ({
priority,
uwasa,
train_info_url,
directions,
result,
];
default:
// 行先がある場合は、行先を取得
return [
typeString,
migrateTrainName(
@@ -152,8 +136,6 @@ export const HeaderText: FC<Props> = ({
priority,
uwasa,
train_info_url,
directions,
result,
];
}
}, [trainData]);
@@ -161,43 +143,11 @@ 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",
width: "100%",
}}
style={{ padding: 10, flexDirection: "row", alignItems: "center" }}
onTouchStart={() =>
scrollRef.current?.scrollTo({ y: 0, animated: true })
scrollHandlers.ref.current?.scrollTo({ y: 0, animated: true })
}
>
<TrainIconStatus
@@ -205,42 +155,18 @@ 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: fixed.textOnPrimary,
borderColor: "white",
}
: {}),
}}
@@ -256,56 +182,62 @@ export const HeaderText: FC<Props> = ({
>
<Text
style={{
...textConfig,
color: fixed.textOnPrimary,
...(trainName.length > 10 ? { fontSize: 16 } : {}),
flexShrink: 1,
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,
}}
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 }} />
<Text style={{ ...textConfig, color: fixed.textOnPrimary }}>
{showHeadStation.map((d) => `${headStation[d].id} + `)}
{trainNum}
{showTailStation.map((d) => ` + ${tailStation[d].id}`)}
</Text>
<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>
<MaterialCommunityIcons
name="database"
color={hasExtraInfo ? "yellow" : fixed.textOnPrimary}
color={
priority > 200 || todayOperation?.length > 0 ? "yellow" : "white"
}
size={30}
style={{ margin: 5 }}
onPress={() => {
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");
}
const uri = `https://jr-shikoku-data-system.pages.dev/trainData/${trainNum}?userID=${expoPushToken}&from=eachTrainInfo`;
navigate("generalWebView", { uri, useExitButton: false });
SheetManager.hide("EachTrainInfo");
}}
/>
</View>

View File

@@ -1,16 +1,14 @@
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: fixed.textOnPrimary,
color: "white",
fontFamily: "JNR-font",
}}
>

View File

@@ -1,12 +1,10 @@
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: fixed.textOnPrimary,
color: "white",
fontFamily: "Zou",
};
return (

View File

@@ -5,13 +5,13 @@ import { LayoutAnimation, ScrollView } from 'react-native';
export const useAutoScroll = (
points: boolean[] | undefined,
trainDataWithThrough: string[],
scrollRef: MutableRefObject<any>,
scrollHandlers: any,
isJumped: boolean,
setIsJumped: (value: boolean) => void,
setShowThrew: (value: boolean) => void
) => {
useEffect(() => {
if (isJumped || !points?.length || !scrollRef) return;
if (isJumped || !points?.length || !scrollHandlers) 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(() => {
scrollRef.current?.scrollTo({ y: scrollPosition, animated: true });
scrollHandlers.ref.current?.scrollTo({ y: scrollPosition, animated: true });
setIsJumped(true);
}, 400);
}, [points, trainDataWithThrough, scrollRef, isJumped, setIsJumped, setShowThrew]);
}, [points, trainDataWithThrough, scrollHandlers, isJumped, setIsJumped, setShowThrew]);
};

View File

@@ -3,6 +3,7 @@ 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";
@@ -16,18 +17,17 @@ 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, direction } = props;
const { data, navigate, from, todayOperation } = props;
const [anpanmanStatus, setAnpanmanStatus] = useState<apt>();
const { allCustomTrainData } = useAllTrainDiagram();
const [trainIconData, setTrainIcon] = useState<
{ vehicle_info_img: string;vehicle_info_right_img: string; vehicle_info_url: string }[]
{ vehicle_info_img: string; vehicle_info_url: string }[]
>([]);
useEffect(() => {
if (!data.trainNum) return;
@@ -79,12 +79,11 @@ 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_right_img: vehicle_info_img, vehicle_info_url }]);
setTrainIcon([{ vehicle_info_img, vehicle_info_url }]);
}
switch (data.trainNum) {
@@ -114,6 +113,30 @@ 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);
@@ -128,9 +151,8 @@ export const TrainIconStatus: FC<Props> = (props) => {
return (
<>
{trainIconData.map(
({ vehicle_info_img: trainIcon, vehicle_info_right_img: trainIconRight, vehicle_info_url: address }, index) => (
({ vehicle_info_img: trainIcon, vehicle_info_url: address }, index) => (
<TouchableOpacity
key={`${trainIcon}-${index}`}
onPress={() => {
navigate("howto", {
info: address,
@@ -142,7 +164,7 @@ export const TrainIconStatus: FC<Props> = (props) => {
>
{move ? (
<Image
source={{ uri: direction ? trainIcon : trainIconRight || trainIcon }}
source={{ uri: trainIcon }}
style={{
height: index > 0 ? 15 : 30,
width: index > 0 ? 12 : 24,

View File

@@ -3,7 +3,6 @@ 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 = {
@@ -12,7 +11,6 @@ type Props = {
from: string;
};
export const TrainViewIcon: FC<Props> = ({ data, navigate, from }) => {
const { fixed } = useThemeColors();
const [isTrainView, setIsTrainView] = useState(false);
//トレインビュー表示対象(特急、マリン)かを判定
useEffect(() => {
@@ -33,7 +31,7 @@ export const TrainViewIcon: FC<Props> = ({ data, navigate, from }) => {
return isTrainView ? (
<Ionicons
name="subway"
color={fixed.textOnPrimary}
color="white"
size={30}
style={{ margin: 5 }}
onPress={onPressTrainView}

View File

@@ -21,11 +21,9 @@ 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);
@@ -39,7 +37,7 @@ export const JRSTraInfo = () => {
if (ok) {
await Sharing.shareAsync(
"file://" + url,
{ mimeType: "image/jpeg", dialogTitle: "Share this image" }
(options = { mimeType: "image/jpeg", dialogTitle: "Share this image" })
);
}
};
@@ -58,22 +56,22 @@ export const JRSTraInfo = () => {
<Handler />
<View
style={{
backgroundColor: fixed.primary,
backgroundColor: "#0099CC",
borderTopRadius: 5,
borderColor: colors.border,
borderColor: "dark",
borderWidth: 1,
}}
>
<ViewShot ref={viewShot} options={{ format: "jpg" }}>
<View
style={{ height: 26, width: "100%", backgroundColor: fixed.primary }}
style={{ height: 26, width: "100%", backgroundColor: "#0099CC" }}
>
<View
style={{
height: 6,
width: 45,
borderRadius: 100,
backgroundColor: colors.borderLight,
backgroundColor: "#f0f0f0",
marginVertical: 10,
alignSelf: "center",
}}
@@ -84,19 +82,19 @@ export const JRSTraInfo = () => {
padding: 10,
flexDirection: "row",
alignItems: "center",
backgroundColor: fixed.primary,
backgroundColor: "#0099CC",
}}
>
<Text style={{ fontSize: 30, fontWeight: "bold", color: fixed.textOnPrimary }}>
<Text style={{ fontSize: 30, fontWeight: "bold", color: "white" }}>
EX
</Text>
<View style={{ flex: 1 }} />
<Text style={{ fontSize: 30, fontWeight: "bold", color: fixed.textOnPrimary }}>
<Text style={{ fontSize: 30, fontWeight: "bold", color: "white" }}>
{timeData}
</Text>
<Ionicons
name="reload"
color={fixed.textOnPrimary}
color="white"
size={30}
style={{ margin: 5 }}
onPress={() => {
@@ -108,7 +106,7 @@ export const JRSTraInfo = () => {
<View
style={{
padding: 10,
backgroundColor: colors.surface,
backgroundColor: "white",
}}
>
{loadingDelayData ? (
@@ -116,7 +114,7 @@ export const JRSTraInfo = () => {
<LottieView
autoPlay
loop
style={{ width: 150, height: 150, backgroundColor: colors.surface }}
style={{ width: 150, height: 150, backgroundColor: "#fff" }}
source={require("../../assets/51690-loading-diamonds.json")}
/>
</View>
@@ -125,26 +123,26 @@ export const JRSTraInfo = () => {
let data = d.split(" ");
return (
<View style={{ flexDirection: "row" }} key={data[1]}>
<Text style={{ flex: 15, fontSize: 18, color: colors.text }}>
<Text style={{ flex: 15, fontSize: 18 }}>
{data[0].replace("\n", "")}
</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>
<Text style={{ flex: 5, fontSize: 18 }}>{data[1]}</Text>
<Text style={{ flex: 6, fontSize: 18 }}>{data[3]}</Text>
</View>
);
})
) : (
<Text style={{ color: colors.text }}>5</Text>
<Text>5</Text>
)}
</View>
<View style={{ padding: 10, backgroundColor: fixed.primary }}>
<View style={{ padding: 10, backgroundColor: "#0099CC" }}>
<Text
style={{ fontSize: 20, fontWeight: "bold", color: fixed.textOnPrimary }}
style={{ fontSize: 20, fontWeight: "bold", color: "white" }}
>
EXについて
</Text>
<Text style={{ color: fixed.textOnPrimary }}>
<Text style={{ color: "white" }}>
JR四国公式列車運行情報より5分毎に取得しますTwitterにて投稿している内容と同一のものとなります
</Text>
</View>

View File

@@ -8,27 +8,23 @@ 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: fixed.primary,
backgroundColor: "#0099CC",
flexDirection: "row",
justifyContent: "space-between",
}}
@@ -37,16 +33,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={fixed.textOnPrimary} size={30} />
<MaterialCommunityIcons name="mastodon" color="white" size={30} />
<View style={{ flex: 1 }} />
<Text style={{ fontSize: 25, fontWeight: "bold", color: fixed.textOnPrimary }}>
<Text style={{ fontSize: 25, fontWeight: "bold", color: "white" }}>
MastodonBOT
</Text>
<View style={{ flex: 1 }} />
</TouchableOpacity>
<TouchableOpacity style={styles} onPress={onCapture}>
<MaterialCommunityIcons name="share-variant" color={fixed.textOnPrimary} size={30} />
<MaterialCommunityIcons name="share-variant" color="white" size={30} />
</TouchableOpacity>
</View>
);

View File

@@ -3,14 +3,12 @@ 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, Foundation } from "@expo/vector-icons";
import { MaterialCommunityIcons } 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
@@ -29,20 +27,20 @@ export const Social = () => {
>
<View
style={{
backgroundColor: fixed.primary,
backgroundColor: "#0099CC",
borderTopRadius: 5,
borderColor: "dark",
borderWidth: 1,
height: "100%",
}}
>
<View style={{ height: 26, width: "100%", backgroundColor: fixed.primary }}>
<View style={{ height: 26, width: "100%", backgroundColor: "#0099CC" }}>
<View
style={{
height: 6,
width: 45,
borderRadius: 100,
backgroundColor: colors.borderLight,
backgroundColor: "#f0f0f0",
marginVertical: 10,
alignSelf: "center",
}}
@@ -55,37 +53,17 @@ export const Social = () => {
<MaterialCommunityIcons
name="web"
style={{ padding: 5 }}
color={fixed.textOnPrimary}
color="white"
size={30}
/>
<Text style={{ fontSize: 30, fontWeight: "bold", color: fixed.textOnPrimary }}>
<Text style={{ fontSize: 30, fontWeight: "bold", color: "white" }}>
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-45928:0020:00
</ListItem.Subtitle>
</ListItem.Content>
<ListItem.Chevron color={fixed.textOnPrimary} />
</ListItem>
<ScrollView
style={{
padding: 10,
backgroundColor: colors.surface,
backgroundColor: "white",
borderBottomLeftRadius: 10,
borderBottomRightRadius: 10,
flex:1
@@ -154,10 +132,9 @@ 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 style={{ color: colors.text }}>{d.name}</ListItem.Title>
<ListItem.Title>{d.name}</ListItem.Title>
</ListItem.Content>
<ListItem.Chevron />
</ListItem>

View File

@@ -4,7 +4,6 @@ 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 };
@@ -13,7 +12,6 @@ export const SpecialTrainInfo: FC<props> = ({ payload }) => {
const { navigate } = payload;
const actionSheetRef = useRef(null);
const insets = useSafeAreaInsets();
const { colors, fixed } = useThemeColors();
return (
<ActionSheet
@@ -32,19 +30,19 @@ export const SpecialTrainInfo: FC<props> = ({ payload }) => {
>
<View
style={{
backgroundColor: fixed.primary,
backgroundColor: "#0099CC",
borderTopRadius: 5,
borderColor: "dark",
borderWidth: 1,
}}
>
<View style={{ height: 26, width: "100%", backgroundColor: fixed.primary }}>
<View style={{ height: 26, width: "100%", backgroundColor: "#0099CC" }}>
<View
style={{
height: 6,
width: 45,
borderRadius: 100,
backgroundColor: colors.borderLight,
backgroundColor: "#f0f0f0",
marginVertical: 10,
alignSelf: "center",
}}

View File

@@ -22,7 +22,6 @@ 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 <></>;
@@ -30,7 +29,6 @@ export const StationDeteilView = (props) => {
const { width } = useWindowDimensions();
const { busAndTrainData } = useBusAndTrainData();
const [trainBus, setTrainBus] = useState();
const { colors } = useThemeColors();
useEffect(() => {
if (!currentStation) return () => {};
@@ -71,7 +69,7 @@ export const StationDeteilView = (props) => {
<View
key={currentStation}
style={{
backgroundColor: colors.sheetBackground,
backgroundColor: "white",
borderTopRadius: 5,
borderColor: "dark",
borderWidth: 1,
@@ -83,7 +81,7 @@ export const StationDeteilView = (props) => {
height: 6,
width: 45,
borderRadius: 100,
backgroundColor: colors.borderLight,
backgroundColor: "#f0f0f0",
marginVertical: 10,
alignSelf: "center",
}}

View File

@@ -2,7 +2,6 @@ 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;
@@ -23,11 +22,10 @@ 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={fixed.textOnPrimary} size={50} />}
icon={<FontAwesome name="table" color="white" size={50} />}
flex={1}
onPressButton={() => {
navigate("stDiagram", {

View File

@@ -1,6 +1,5 @@
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;
@@ -10,7 +9,6 @@ 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
@@ -32,7 +30,7 @@ export const 駅構内図:FC<Props> = (props) => {
<View style={{ flex: 1 }} />
<Text
style={{
color: fixed.textOnPrimary,
color: "white",
textAlign: "center",
textAlignVertical: "center",
flex: 1,

View File

@@ -2,13 +2,11 @@ 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={fixed.textOnPrimary} size={50} />}
icon={<Ionicons name="map" color="white" size={50} />}
flex={1}
onPressButton={() => Linking.openURL(stationMap)}
>

View File

@@ -2,7 +2,6 @@ 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;
@@ -14,11 +13,10 @@ 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={fixed.textOnPrimary} size={50} />}
icon={<FontAwesome name="table" color="white" size={50} />}
flex={1}
onPressButton={() => {
usePDFView == "true"

View File

@@ -1,7 +1,6 @@
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;
@@ -11,12 +10,11 @@ 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: fixed.primary,
backgroundColor: "#0099CC",
flexDirection: "row",
alignContent: "center",
alignItems: "center",
@@ -36,14 +34,14 @@ export const StationTrainPositionButton: FC<Props> = (props) => {
>
<View style={{ flex: 1 }} />
<AntDesign
name={"bar-chart"}
name={"barchart"}
size={20}
color={fixed.textOnPrimary}
color={"white"}
style={{ marginHorizontal: 5, marginVertical: 5 }}
/>
<Text
style={{
color: fixed.textOnPrimary,
color: "white",
textAlign: "center",
textAlignVertical: "center",
}}

View File

@@ -2,17 +2,15 @@ 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={fixed.textOnPrimary} size={50} />}
icon={<Ionicons name="bus" color="white" size={50} />}
flex={1}
onPressButton={press}
onLongPressButton={() => Linking.openURL(address)}

View File

@@ -2,7 +2,6 @@ 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;
@@ -13,11 +12,10 @@ 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={fixed.textOnPrimary} size={50} />}
icon={<Foundation name="web" color="white" size={50} />}
flex={1}
onPressButton={() => {
navigate("howto", { info, goTo, useShow });

File diff suppressed because it is too large Load Diff

View File

@@ -17,14 +17,12 @@ 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();
@@ -33,7 +31,7 @@ export const TrainIconUpdate = () => {
if (ok) {
await Sharing.shareAsync(
"file://" + url,
{ mimeType: "image/jpeg", dialogTitle: "Share this image" }
(options = { mimeType: "image/jpeg", dialogTitle: "Share this image" })
);
}
};
@@ -59,19 +57,19 @@ export const TrainIconUpdate = () => {
<Handler />
<View
style={{
backgroundColor: fixed.primary,
backgroundColor: "#0099CC",
borderTopRadius: 5,
borderColor: "dark",
borderWidth: 1,
}}
>
<View style={{ height: 26, width: "100%", backgroundColor: fixed.primary }}>
<View style={{ height: 26, width: "100%", backgroundColor: "#0099CC" }}>
<View
style={{
height: 6,
width: 45,
borderRadius: 100,
backgroundColor: colors.borderLight,
backgroundColor: "#f0f0f0",
marginVertical: 10,
alignSelf: "center",
}}
@@ -83,10 +81,10 @@ export const TrainIconUpdate = () => {
padding: 10,
flexDirection: "row",
alignItems: "center",
backgroundColor: fixed.primary,
backgroundColor: "#0099CC",
}}
>
<Text style={{ fontSize: 30, fontWeight: "bold", color: fixed.textOnPrimary }}>
<Text style={{ fontSize: 30, fontWeight: "bold", color: "white" }}>
</Text>
<View style={{ flex: 1 }} />
@@ -97,26 +95,23 @@ export const TrainIconUpdate = () => {
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
backgroundColor: colors.surface,
backgroundColor: "#FFFFFFEE",
padding: 10,
}}
>
<View
<Image
source={iconList.filter(({ id }) => id == currentIcon)[0].icon}
style={{
width: 50,
height: 50,
padding: 30,
borderWidth: 1,
borderRadius: 10,
borderColor: colors.border,
borderColor: "white",
margin: 10,
padding: 10,
backgroundColor: colors.surface,
backgroundColor: "white",
}}
>
<Image
source={iconList.filter(({ id }) => id == currentIcon)[0].icon}
style={{ width: 80, height: 80, borderRadius: 8 }}
resizeMode="contain"
/>
</View>
/>
<Text>JR四国非公式アプリ</Text>
</View>
) : (
@@ -127,10 +122,10 @@ export const TrainIconUpdate = () => {
padding: 10,
flexDirection: "row",
alignItems: "center",
backgroundColor: fixed.primary,
backgroundColor: "#0099CC",
}}
>
<Text style={{ fontSize: 15, fontWeight: "bold", color: fixed.textOnPrimary }}>
<Text style={{ fontSize: 15, fontWeight: "bold", color: "white" }}>
JR四国非公式アプリを更新して好きなアイコンに変更してみよう
</Text>
</View>
@@ -138,7 +133,7 @@ export const TrainIconUpdate = () => {
<View
style={{
padding: 10,
backgroundColor: fixed.primary,
backgroundColor: "#0099CC",
flexDirection: "row",
justifyContent: "space-between",
}}
@@ -147,19 +142,19 @@ export const TrainIconUpdate = () => {
style={{
padding: 10,
flexDirection: "row",
borderColor: fixed.textOnPrimary,
borderColor: "white",
borderWidth: 1,
margin: 10,
borderRadius: 5,
alignItems: "center",
backgroundColor: fixed.primary,
backgroundColor: "#0099CC",
flex: 1,
}}
onPress={onCapture}
>
<MaterialCommunityIcons name="share" color={fixed.textOnPrimary} size={30} />
<MaterialCommunityIcons name="share" color="white" size={30} />
<View style={{ flex: 1 }} />
<Text style={{ fontSize: 25, fontWeight: "bold", color: fixed.textOnPrimary }}>
<Text style={{ fontSize: 25, fontWeight: "bold", color: "white" }}>
</Text>
<View style={{ flex: 1 }} />

View File

@@ -12,7 +12,6 @@ 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 {
@@ -24,7 +23,6 @@ export const TrainMenuLineSelector = () => {
const actionSheetRef = useRef(null);
const insets = useSafeAreaInsets();
const platformIs = Platform.OS == "android";
const { colors, fixed } = useThemeColors();
return (
<ActionSheet
gestureEnabled
@@ -35,13 +33,13 @@ export const TrainMenuLineSelector = () => {
useBottomSafeAreaPadding={platformIs}
>
<Handler />
<View style={{ height: 26, width: "100%", backgroundColor: colors.sheetBackground }}>
<View style={{ height: 26, width: "100%", backgroundColor: "white" }}>
<View
style={{
height: 6,
width: 45,
borderRadius: 100,
backgroundColor: colors.borderLight,
backgroundColor: "#f0f0f0",
marginVertical: 10,
alignSelf: "center",
}}
@@ -51,7 +49,7 @@ export const TrainMenuLineSelector = () => {
<TouchableOpacity
style={{
flexDirection: "row",
backgroundColor: selectedLine == d ? "#0099CC33" : colors.surface,
backgroundColor: selectedLine == d ? "#0099CC33" : "white",
}}
onPress={() => {
SheetManager.hide("TrainMenuLineSelector");
@@ -95,7 +93,7 @@ export const TrainMenuLineSelector = () => {
<View style={{ flex: 1 }} />
<Text
style={{
color: fixed.textOnPrimary,
color: "white",
textAlign: "center",
fontSize: 12,
fontWeight: "bold",
@@ -112,13 +110,13 @@ export const TrainMenuLineSelector = () => {
padding: 8,
flexDirection: "row",
borderBottomWidth: 1,
borderBottomColor: colors.borderLight,
borderBottomColor: "#f0f0f0",
flex: 1,
alignContent: "center",
alignItems: "center",
}}
>
<Text style={{ fontSize: 20, color: colors.text }}>
<Text style={{ fontSize: 20 }}>
{lineListPair[stationIDPair[d]]}
</Text>
<View style={{ flex: 1 }} />

View File

@@ -6,7 +6,6 @@ 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);
@@ -15,6 +14,5 @@ registerSheet("TrainMenuLineSelector", TrainMenuLineSelector);
registerSheet("TrainIconUpdate", TrainIconUpdate);
registerSheet("SpecialTrainInfo", SpecialTrainInfo);
registerSheet("Social", Social);
registerSheet("TrainDataSources", TrainDataSources);
export {};

View File

@@ -1,33 +1,31 @@
import React, { useState, useRef, FC } from "react";
import React, { useState, useEffect, 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 { useThemeColors } from "@/lib/theme";
import { Switch } from "@rneui/themed";
import { BigButton } from "./atom/BigButton";
import { Switch } from "react-native-elements";
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,
@@ -35,13 +33,11 @@ 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: fixed.textOnPrimary,
color: "white",
fontSize: 20,
margin: 3,
padding: 3,
@@ -49,10 +45,24 @@ export const AllTrainDiagramView: FC = () => {
const regexTextButtonStyle = {
...regexTextStyle,
borderWidth: 1,
borderColor: fixed.textOnPrimary,
borderColor: "white",
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 = "";
@@ -82,7 +92,6 @@ 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);
@@ -114,7 +123,7 @@ export const AllTrainDiagramView: FC = () => {
style={{
padding: 5,
flexDirection: "row",
borderColor: fixed.textOnPrimary,
borderColor: "white",
borderWidth: 1,
margin: 5,
borderRadius: 5,
@@ -148,56 +157,34 @@ export const AllTrainDiagramView: FC = () => {
)}
</View>
<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 && (
{typeString && (
<Text
style={{
fontSize: 20,
fontWeight: "bold",
color: fixed.textOnPrimary,
flexShrink: 1,
}}
onTextLayout={(e) => {
if (e.nativeEvent.lines.length > 1) {
setIsWrapped(true);
}
color: "white",
fontFamily: fontAvailable ? "JR-Nishi" : undefined,
fontWeight: !fontAvailable ? "bold" : undefined,
marginRight: 5,
}}
>
{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: fixed.textOnPrimary }}>
<Text style={{ fontSize: 20, fontWeight: "bold", color: "white" }}>
{id}
</Text>
</TouchableOpacity>
);
};
return (
<View ref={containerRef} style={{ flex: 1, backgroundColor: fixed.primary, paddingBottom: measuredPadding }}>
<View style={{ backgroundColor: "#0099CC", height: "100%" }}>
<FlatList
contentContainerStyle={{ justifyContent: "flex-end", flexGrow: 1 }}
style={{ flex: 1 }}
@@ -239,7 +226,7 @@ export const AllTrainDiagramView: FC = () => {
renderItem={({ item }) => <Item {...{ openTrainInfo, id: item }} />}
ListEmptyComponent={
<View style={{ flex: 1, alignItems: "center", marginTop: 50 }}>
<Text style={{ color: fixed.textOnPrimary, fontSize: 20 }}>
<Text style={{ color: "white", fontSize: 20 }}>
</Text>
</View>
@@ -247,7 +234,11 @@ export const AllTrainDiagramView: FC = () => {
keyExtractor={(item) => item}
//initialNumToRender={100}
/>
<View>
<KeyboardAvoidingView
behavior="padding"
keyboardVerticalOffset={80}
enabled={Platform.OS === "ios"}
>
<View style={{ height: 35, flexDirection: "row" }}>
<Switch
value={useRegex}
@@ -258,7 +249,7 @@ export const AllTrainDiagramView: FC = () => {
color="red"
style={{ margin: 5 }}
/>
<Text style={{ color: fixed.textOnPrimary, fontSize: 20, margin: 5 }}>
<Text style={{ color: "white", fontSize: 20, margin: 5 }}>
使
</Text>
<Switch
@@ -270,7 +261,7 @@ export const AllTrainDiagramView: FC = () => {
color="red"
style={{ margin: 5 }}
/>
<Text style={{ color: fixed.textOnPrimary, fontSize: 20, margin: 5 }}>
<Text style={{ color: "white", fontSize: 20, margin: 5 }}>
</Text>
</View>
@@ -278,7 +269,7 @@ export const AllTrainDiagramView: FC = () => {
style={{
height: 35,
flexDirection: "row",
backgroundColor: fixed.primary,
backgroundColor: "#0099CC",
margin: 5,
display: useRegex ? "flex" : "none",
}}
@@ -334,28 +325,31 @@ export const AllTrainDiagramView: FC = () => {
height: 35,
margin: 5,
alignItems: "center",
backgroundColor: colors.searchBackground,
backgroundColor: "#F4F4F4",
flexDirection: "row",
paddingLeft: 10,
paddingRight: 10,
borderRadius: 25,
borderColor: colors.searchBorder,
borderColor: "#F4F4F4",
}}
>
<TextInput
placeholder="列番・列車名を入力してフィルタリングします。"
onFocus={() => {}}
onFocus={() => setKeyBoardVisible(true)}
onEndEditing={() => {}}
onChange={(ret) => setInput(ret.nativeEvent.text)}
value={input}
style={{ flex: 1 }}
/>
</View>
</View>
</KeyboardAvoidingView>
<BigButton
onPress={goBack}
string="閉じる"
style={{ display: keyBoardVisible ? "none" : "flex" }}
style={{
display:
Platform.OS === "ios" ? "flex" : keyBoardVisible ? "none" : "flex",
}}
/>
</View>
);

View File

@@ -1,129 +0,0 @@
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>
);
}

View File

@@ -0,0 +1,25 @@
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",
},
});

View File

@@ -5,6 +5,7 @@ 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
@@ -34,8 +35,7 @@ export function InfoWidget({ time, text }) {
backgroundColor: "#ffffff",
borderRadius: 16,
}}
clickAction="OPEN_URI"
clickActionData={{ uri: "jrshikoku://open/operation" }}
clickAction="WIDGET_CLICK"
>
<FlexWidget
style={{
@@ -87,6 +87,7 @@ export function InfoWidget({ time, text }) {
fontSize: 20,
}}
clickAction="OPEN_APP"
text={text}
/>
) : (
@@ -95,6 +96,7 @@ export function InfoWidget({ time, text }) {
color: "#000000",
fontSize: 20,
}}
clickAction="WIDGET_CLICK"
text="通常運行中です。"
/>
)}
@@ -102,3 +104,9 @@ export function InfoWidget({ time, text }) {
</FlexWidget>
);
}
const FlexText = ({ flex, text }) => (
<FlexWidget style={{ flex }}>
<TextWidget style={{ fontSize: 20, color: "#000000" }} text={text} />
</FlexWidget>
);

View File

@@ -1,222 +0,0 @@
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>
);
}

View File

@@ -1,87 +0,0 @@
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>
);
}

View File

@@ -5,6 +5,7 @@ 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
@@ -33,8 +34,7 @@ export function TraInfoEXWidget({ time, delayString }) {
backgroundColor: "#ffffff",
borderRadius: 16,
}}
clickAction="OPEN_URI"
clickActionData={{ uri: "jrshikoku://open/traininfo" }}
clickAction="WIDGET_CLICK"
>
<FlexWidget
style={{
@@ -90,6 +90,7 @@ export function TraInfoEXWidget({ time, delayString }) {
backgroundColor: "#ffffff",
flex: 1,
}}
clickAction="WIDGET_CLICK"
key={data[1]}
>
<FlexText flex={3} text={data[0].replace("\n", "")} />
@@ -104,6 +105,7 @@ export function TraInfoEXWidget({ time, delayString }) {
color: "#000000",
fontSize: 20,
}}
clickAction="WIDGET_CLICK"
text="現在、5分以上の遅れはありません。"
/>
)}

View File

@@ -1,19 +1,12 @@
import React from "react";
import { TraInfoEXWidget, getDelayData } from "./TraInfoEXWidget";
import { ToastAndroid } from "react-native";
import { InfoWidget, getInfoString } from "./InfoWidget";
import {
FelicaQuickAccessWidget,
getFelicaQuickAccessData,
} from "./FelicaQuickAccessWidget";
import { ShortcutWidget, getShortcutData } from "./ShortcutWidget";
import { StrangeTrainWidget, getStrangeTrainData } from "./StrangeTrainWidget";
import { AS } from "../../storageControl";
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) {
@@ -24,51 +17,41 @@ 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": {
const name = widgetInfo.widgetName;
if (name === "JR_shikoku_felica_balance") {
const quickData = await getFelicaQuickAccessData();
renderWidget(<FelicaQuickAccessWidget {...quickData} />);
break;
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;
}
}
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;

View File

@@ -3,15 +3,11 @@ 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 lineColorList from "../assets/originData/lineColorList";
import { lineList, stationIDPair } from "../lib/getStationList";
import { lineList } from "../lib/getStationList";
import { useCurrentTrain } from "../stateBox/useCurrentTrain";
import { useDeviceOrientationChange } from "../stateBox/useDeviceOrientationChange";
import { SheetManager } from "react-native-actions-sheet";
@@ -24,31 +20,20 @@ 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, 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 { mapSwitch, trainInfo, setTrainInfo } = useTrainMenu();
const openStationACFromEachTrainInfo = async (stationName) => {
await SheetManager.hide("EachTrainInfo");
@@ -82,30 +67,16 @@ export default function Apps() {
SheetManager.hide("StationDetailView");
}
};
const bgColor = isDark ? "#1c1c1e" : "#ffffff";
return (
<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}
>
<View
style={{
height: "100%",
paddingTop: top,
flexDirection: isLandscape ? "row" : "column",
}}
onLayout={handleLayout}
>
{/* {Status} */}
<AppsWebView
{...{
openStationACFromEachTrainInfo,
@@ -128,7 +99,6 @@ export default function Apps() {
) : (
<NewMenu />
)}
</View>
</View>
);
}

View File

@@ -1,85 +1,26 @@
import { useCurrentTrain } from "@/stateBox/useCurrentTrain";
import { AppState, InteractionManager, View } from "react-native";
import {
activateKeepAwakeAsync,
deactivateKeepAwake,
} from "expo-keep-awake";
import { View, Platform } from "react-native";
import { useKeepAwake } from "expo-keep-awake";
import Constants from "expo-constants";
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]);
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(() => {});
};
}, []);
useKeepAwake();
return (
<View
style={{
position: "absolute",
top,
top: Platform.OS == "ios" ? Constants.statusBarHeight : 0,
borderRadius: 5,
zIndex: 1500,
width: "100%",
@@ -94,9 +35,6 @@ export const FixedPositionBox = () => {
{fixedPosition.type === "train" && (
<FixedTrain trainID={fixedPosition.value} />
)}
{fixedPosition.type === "nearestStation" && (
<FixedNearestStationBox />
)}
</View>
);
};

View File

@@ -1,34 +0,0 @@
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} />;
};

View File

@@ -19,56 +19,25 @@ import { useTrainMenu } from "@/stateBox/useTrainMenu";
import { Ionicons } from "@expo/vector-icons";
import { useNavigation } from "@react-navigation/native";
import { FC, useCallback, useEffect, useRef, useState } from "react";
import { Animated, LayoutAnimation, PermissionsAndroid, Platform, Text, TouchableOpacity, View } from "react-native";
import { FC, useEffect, useState } from "react";
import { LayoutAnimation, 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);
@@ -79,7 +48,7 @@ export const FixedStation: FC<props> = ({ stationID }) => {
: "white";
////
const { allTrainDiagram, allCustomTrainData } = useAllTrainDiagram();
const { allTrainDiagram } = useAllTrainDiagram();
const { areaStationID } = useAreaInfo();
const [stationDiagram, setStationDiagram] = useState({}); //当該駅の全時刻表
const [isInfoArea, setIsInfoArea] = useState(false);
@@ -126,113 +95,7 @@ export const FixedStation: FC<props> = ({ stationID }) => {
.filter((d) => !d.isThrough)
.filter((d) => d.lastStation != station[0].Station_JP); //最終列車表示設定
setSelectedTrain(data);
}, [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]);
}, [trainTimeAndNumber, currentTrain /*finalSwitch*/]);
return (
<View
@@ -267,7 +130,7 @@ export const FixedStation: FC<props> = ({ stationID }) => {
alignSelf: "center",
alignItems: "center",
height: "100%",
backgroundColor: colors.background,
backgroundColor: "white",
}}
>
<View
@@ -295,19 +158,19 @@ export const FixedStation: FC<props> = ({ stationID }) => {
padding: 0,
paddingLeft: 5,
flex: 1,
color: fixed.textOnPrimary,
color: "white",
}}
>
{station[0]?.Station_JP}
</Text>
<View
style={{
backgroundColor: colors.background,
backgroundColor: "white",
width: 6,
borderLeftColor: lineColor,
borderTopColor: lineColor,
borderBottomColor: colors.background,
borderRightColor: colors.background,
borderBottomColor: "white",
borderRightColor: "white",
borderBottomWidth: 18,
borderLeftWidth: 10,
borderRightWidth: 0,
@@ -319,11 +182,11 @@ export const FixedStation: FC<props> = ({ stationID }) => {
<View
style={{
height: "100%",
backgroundColor: colors.background,
backgroundColor: "white",
flex: 1,
}}
>
<Text style={{ fontSize: 18, color: colors.text }}></Text>
<Text style={{ fontSize: 18 }}></Text>
</View>
</View>
@@ -331,7 +194,7 @@ export const FixedStation: FC<props> = ({ stationID }) => {
style={{
flex: 5,
flexDirection: "column",
backgroundColor: colors.background,
backgroundColor: "white",
borderTopWidth: 5,
borderTopColor: lineColor,
overflow: "hidden",
@@ -347,8 +210,8 @@ export const FixedStation: FC<props> = ({ stationID }) => {
/>
))
) : (
<View style={{ backgroundColor: colors.background, flex: 1 }}>
<Text style={{ fontSize: parseInt("11%"), color: colors.text }}>
<View style={{ backgroundColor: "white", flex: 1 }}>
<Text style={{ fontSize: parseInt("11%") }}>
</Text>
</View>
@@ -382,28 +245,17 @@ export const FixedStation: FC<props> = ({ stationID }) => {
height: 26,
}}
>
{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} />
<Ionicons name="lock-closed" size={15} color="white" />
<Text
style={{
color: "white",
fontSize: 15,
paddingRight: 5,
}}
>
</Text>
<Ionicons name="close" size={15} color="white" />
</View>
<View
@@ -467,11 +319,11 @@ export const FixedStation: FC<props> = ({ stationID }) => {
<Ionicons
name={fixedPositionSize == 226 ? "chevron-up" : "chevron-down"}
size={15}
color={fixed.textOnPrimary}
color="white"
/>
<Text
style={{
color: fixed.textOnPrimary,
color: "white",
paddingRight: 5,
backgroundColor: lineColor,
fontSize: 15,
@@ -489,7 +341,6 @@ export const FixedStation: FC<props> = ({ stationID }) => {
};
const FixedStationBoxEachTrain = ({ d, station, displaySize }) => {
const { colors, isDark } = useThemeColors();
const { currentTrain } = useCurrentTrain();
const { stationList } = useStationList();
const { allCustomTrainData } = useAllTrainDiagram();
@@ -508,22 +359,22 @@ const FixedStationBoxEachTrain = ({ d, station, displaySize }) => {
useEffect(() => {
setTrain(getCurrentTrainData(d.train, currentTrain, allCustomTrainData));
}, [currentTrain, d.train]);
const { name, color } = getTrainType({ type: train.type, whiteMode: !isDark });
const { name, color } = getTrainType({ type: train.type, whiteMode: true });
return (
<View
style={{
backgroundColor: colors.background,
backgroundColor: "white",
flexDirection: "row",
height: displaySize == 226 ? "7.5%" : "33%",
overflow: "visible",
}}
>
<Text style={{ fontSize: parseInt("11%"), flex: 3, color: colors.text }}>{d.time}</Text>
<Text style={{ fontSize: parseInt("11%"), flex: 3 }}>{d.time}</Text>
<Text style={{ fontSize: parseInt("11%"), flex: 4, color }}>{name}</Text>
<Text style={{ fontSize: parseInt("11%"), flex: 4, color: colors.text }}>
<Text style={{ fontSize: parseInt("11%"), flex: 4 }}>
{d.lastStation}
</Text>
<Text style={{ fontSize: parseInt("11%"), flex: 3, color: colors.text }}>
<Text style={{ fontSize: parseInt("11%"), flex: 3 }}>
{trainDelayStatus}
</Text>
</View>

View File

@@ -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, useRef, useState } from "react";
import { FC, useEffect, useState } from "react";
import {
Text,
TouchableOpacity,
@@ -11,9 +11,6 @@ import {
Image,
LayoutAnimation,
ScrollView,
Platform,
PermissionsAndroid,
AppState,
} from "react-native";
import { getTrainType } from "@/lib/getTrainType";
import { trainDataType, trainPosition } from "@/lib/trainPositionTextArray";
@@ -25,20 +22,12 @@ 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,
@@ -46,17 +35,11 @@ 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)
@@ -71,11 +54,8 @@ export const FixedTrain: FC<props> = ({ trainID }) => {
if (stationData) {
setTrain(stationData);
} else {
// バックグラウンドでは一時的にデータが消えることがある→フォアグラウンド時のみ終了
if (AppState.currentState === "active") {
alert("追跡していた列車が消えました。追跡を終了します。");
setFixedPosition({ type: null, value: null });
}
alert("追跡していた列車が消えました。追跡を終了します。");
setFixedPosition({ type: null, value: null });
}
}, [trainID, currentTrain]);
@@ -216,25 +196,9 @@ 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 = searchStart;
searchCount < trainDataWidhThrough.length;
let searchCount = searchCountFirst;
searchCount < points.length;
searchCount++
) {
const nextPos = trainDataWidhThrough[searchCount];
@@ -242,18 +206,14 @@ 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();
@@ -269,8 +229,12 @@ export const FixedTrain: FC<props> = ({ trainID }) => {
}
}
if (distanceMinute >= 0) {
setNextStationData(getStationDataFromName(station));
break;
if (se.includes("通")) {
continue;
} else {
setNextStationData(getStationDataFromName(station));
break;
}
} else {
additionalSkipCount++;
continue;
@@ -312,7 +276,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({
@@ -324,178 +288,6 @@ 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 }}
@@ -514,7 +306,7 @@ export const FixedTrain: FC<props> = ({ trainID }) => {
style={{
flexDirection: fixedPositionSize === 226 ? "row" : "column",
flex: 1,
backgroundColor: colors.background,
backgroundColor: "white",
height: fixedPositionSize === 226 ? 200 : 50,
overflow: "hidden",
}}
@@ -560,7 +352,7 @@ export const FixedTrain: FC<props> = ({ trainID }) => {
? "bold"
: undefined,
marginTop: customTrainType.fontAvailable ? 3 : 0,
color: fixed.textOnPrimary,
color: "white",
textAlignVertical: "center",
textAlign: "left",
}}
@@ -571,7 +363,7 @@ export const FixedTrain: FC<props> = ({ trainID }) => {
<Text
style={{
fontSize: trainNameText.length > 4 ? 8 : 14,
color: fixed.textOnPrimary,
color: "white",
maxWidth: fixedPositionSize === 226 ? 200 : 60,
textAlignVertical: "center",
}}
@@ -622,7 +414,7 @@ export const FixedTrain: FC<props> = ({ trainID }) => {
<Text
style={{
fontSize: customData?.to_data?.length > 4 ? 9 : 12,
color: fixed.textOnPrimary,
color: "white",
fontWeight: "bold",
textAlignVertical: "center",
margin: 0,
@@ -638,11 +430,11 @@ export const FixedTrain: FC<props> = ({ trainID }) => {
{fixedPositionSize === 226 && (
<View
style={{
backgroundColor: colors.background,
backgroundColor: "white",
width: 10,
borderLeftColor: "black",
borderTopColor: lineColor,
borderBottomColor: colors.background,
borderBottomColor: "white",
borderRightColor: "black",
borderTopWidth: 50,
borderBottomWidth: 0,
@@ -707,12 +499,12 @@ export const FixedTrain: FC<props> = ({ trainID }) => {
{fixedPositionSize !== 226 && (
<View
style={{
backgroundColor: colors.background,
backgroundColor: "white",
width: 10,
borderLeftColor: "black",
borderTopColor: "black",
borderBottomColor: colors.background,
borderRightColor: colors.background,
borderBottomColor: "white",
borderRightColor: "white",
borderTopWidth: 21,
borderBottomWidth: 0,
borderLeftWidth: 0,
@@ -756,17 +548,17 @@ export const FixedTrain: FC<props> = ({ trainID }) => {
height: 26,
}}
>
<Ionicons name="lock-closed" size={15} color={fixed.textOnPrimary} />
<Ionicons name="lock-closed" size={15} color="white" />
<Text
style={{
color: fixed.textOnPrimary,
color: "white",
fontSize: 15,
paddingRight: 5,
}}
>
</Text>
<Ionicons name="close" size={15} color={fixed.textOnPrimary} />
<Ionicons name="close" size={15} color="white" />
</View>
<View
@@ -854,7 +646,6 @@ const CurrentPositionBox = ({
trainDataWithThrough,
isSmall,
}) => {
const { colors } = useThemeColors();
let firstText = "";
let secondText = "";
let marginText = "";
@@ -875,7 +666,7 @@ const CurrentPositionBox = ({
<View
style={{
flex: isSmall ? 1 : 3,
backgroundColor: colors.background,
backgroundColor: "white",
flexDirection: "row",
}}
>
@@ -883,12 +674,12 @@ const CurrentPositionBox = ({
<View style={{ flexDirection: "column" }}>
<View
style={{
backgroundColor: colors.background,
backgroundColor: "white",
width: 10,
borderLeftColor: lineColor,
borderTopColor: lineColor,
borderBottomColor: colors.background,
borderRightColor: colors.background,
borderBottomColor: "white",
borderRightColor: "white",
borderTopWidth: 28,
borderBottomWidth: 0,
borderLeftWidth: 0,
@@ -897,12 +688,12 @@ const CurrentPositionBox = ({
></View>
<View
style={{
backgroundColor: colors.background,
backgroundColor: "white",
width: 10,
borderLeftColor: colors.background,
borderTopColor: colors.background,
borderBottomColor: colors.background,
borderRightColor: colors.background,
borderLeftColor: "white",
borderTopColor: "white",
borderBottomColor: "white",
borderRightColor: "white",
borderTopWidth: 18,
borderBottomWidth: 0,
borderLeftWidth: 0,
@@ -917,47 +708,16 @@ const CurrentPositionBox = ({
overScrollMode="always"
>
{trainDataWithThrough.length > 0 &&
(() => {
// 着→発ペアを同一駅で統合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}
/>
));
})()}
trainDataWithThrough.map((d, index) => (
<EachStopData
d={d}
index={index}
key={d+"FixedTrainBoxEachStopData"}
delayTime={delayTime}
isSmall={isSmall}
secondText={secondText}
/>
))}
</ScrollView>
</View>
);
@@ -969,28 +729,27 @@ type eachStopType = {
isSmall: boolean;
index: number;
secondText: string;
arrivalTime?: string | null;
};
const EachStopData: FC<eachStopType> = (props) => {
const { colors } = useThemeColors();
const { d, delayTime, isSmall, index, secondText, arrivalTime } = props;
const { d, delayTime, isSmall, index, secondText } = props;
if (!d) return null;
if (d == "") return null;
const [station, se, time] = d.split(",");
const calcMinute = (t: string) => {
if (!t || t === "") return null;
let distanceMinute = 0;
if (time != "") {
const now = dayjs();
const hour = parseInt(t.split(":")[0]);
const dt = now
const hour = parseInt(time.split(":")[0]);
const distanceTime = now
.hour(hour < 4 ? hour + 24 : hour)
.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;
.minute(parseInt(time.split(":")[1]));
distanceMinute = distanceTime.diff(now, "minute") + delayTime;
if (now.hour() < 4) {
if (hour < 4) {
distanceMinute = distanceMinute - 1440;
}
}
}
return (
<>
<View
@@ -1025,25 +784,13 @@ 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: colors.text,
backgroundColor: colors.background,
color: "black",
backgroundColor: "white",
fontWeight: "bold",
}}
>
@@ -1088,7 +835,7 @@ const EachStopData: FC<eachStopType> = (props) => {
<Ionicons
name="arrow-forward"
size={isSmall ? 8 : 14}
color={colors.icon}
color="black"
style={{ marginTop: isSmall ? 0 : 3 }}
/>
</View>

View File

@@ -6,11 +6,9 @@ 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;
@@ -21,8 +19,8 @@ export const LandscapeBackButton: FC<{
left: 10,
width: 50,
height: 50,
backgroundColor: fixed.primary,
borderColor: fixed.textOnPrimary,
backgroundColor: "#0099CC",
borderColor: "white",
borderStyle: "solid",
borderWidth: 1,
borderRadius: 50,
@@ -37,13 +35,13 @@ export const LandscapeBackButton: FC<{
height: "auto",
textAlignVertical: "center",
fontWeight: "bold",
color: fixed.textOnPrimary,
color: "white",
},
};
return (
<TouchableOpacity onPress={onPress} style={styles.touch}>
<View style={{ flex: 1 }} />
<Ionicons name="arrow-back" color={fixed.textOnPrimary} size={30} />
<Ionicons name="arrow-back" color="white" size={30} />
<View style={{ flex: 1 }} />
</TouchableOpacity>
);

View File

@@ -3,13 +3,14 @@ import {
View,
Text,
TouchableOpacity,
Platform,
TouchableOpacityProps,
TextStyle,
} from "react-native";
import { useThemeColors } from "@/lib/theme";
import Constants from "expo-constants";
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;
};
@@ -19,9 +20,7 @@ type stylesType = {
};
export const MapsButton: FC<MapsButtonProps> = ({ onPress }) => {
const { fixed } = useThemeColors();
const { mapSwitch } = useTrainMenu();
const { top } = useSafeAreaInsets();
const styles: stylesType = {
touch: {
position: "absolute",
@@ -29,8 +28,8 @@ export const MapsButton: FC<MapsButtonProps> = ({ onPress }) => {
left: 10,
width: 50,
height: 50,
backgroundColor: fixed.primary,
borderColor: fixed.textOnPrimary,
backgroundColor: "#0099CC",
borderColor: "white",
borderStyle: "solid",
borderWidth: 1,
borderRadius: 50,
@@ -46,7 +45,7 @@ export const MapsButton: FC<MapsButtonProps> = ({ onPress }) => {
height: "auto",
textAlignVertical: "center",
fontWeight: "bold",
color: fixed.textOnPrimary,
color: "white",
fontSize: 20,
},
};

View File

@@ -1,19 +1,16 @@
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={{
@@ -21,8 +18,8 @@ export const NewMenu = () => {
top,
width,
height: 54,
backgroundColor: fixed.primary,
borderColor: fixed.textOnPrimary,
backgroundColor: "#0099CC",
borderColor: "white",
borderStyle: "solid",
borderWidth: 1,
alignContent: "center",
@@ -36,8 +33,8 @@ export const NewMenu = () => {
style={{
flex: 1,
height: 54,
backgroundColor: fixed.primary,
borderColor: fixed.textOnPrimary,
backgroundColor: "#0099CC",
borderColor: "white",
borderStyle: "solid",
borderWidth: 1,
borderRightWidth: 0,
@@ -55,8 +52,8 @@ export const NewMenu = () => {
style={{
width: 54,
height: 54,
backgroundColor: fixed.primary,
borderColor: fixed.textOnPrimary,
backgroundColor: "#0099CC",
borderColor: "white",
borderStyle: "solid",
borderWidth: 1,
alignContent: "center",
@@ -65,11 +62,11 @@ export const NewMenu = () => {
}}
>
<View style={{ flex: 1 }} />
<Ionicons name="menu" color={fixed.textOnPrimary} size={30} />
<Ionicons name="menu" color="white" size={30} />
<View style={{ flex: 1 }} />
</View>
<View style={{ flex: 1 }} />
<Text style={{ color: fixed.textOnPrimary, fontSize: 20 }}></Text>
<Text style={{ color: "white", fontSize: 20 }}></Text>
<View style={{ flex: 1 }}></View>
</>
</TouchableOpacity>
@@ -79,8 +76,8 @@ export const NewMenu = () => {
style={{
width: 54,
height: 54,
backgroundColor: LoadError ? "red" : fixed.primary,
borderColor: fixed.textOnPrimary,
backgroundColor: LoadError ? "red" : "#0099CC",
borderColor: "white",
borderStyle: "solid",
borderWidth: 1,
alignContent: "center",
@@ -89,7 +86,7 @@ export const NewMenu = () => {
}}
>
<View style={{ flex: 1 }} />
<Ionicons name="reload" color={fixed.textOnPrimary} size={30} />
<Ionicons name="reload" color="white" size={30} />
<View style={{ flex: 1 }} />
</TouchableOpacity>
</View>

View File

@@ -2,13 +2,14 @@ import React, { FC } from "react";
import {
View,
TouchableOpacity,
Platform,
TouchableOpacityProps,
TextStyle,
} from "react-native";
import { Ionicons } from "@expo/vector-icons";
import { useThemeColors } from "@/lib/theme";
import Constants from "expo-constants";
import { useTrainMenu } from "../../stateBox/useTrainMenu";
import { useSafeAreaInsets } from "react-native-safe-area-context";
const top = Platform.OS == "ios" ? Constants.statusBarHeight : 0;
type stylesType = {
touch: TouchableOpacityProps["style"];
@@ -20,9 +21,7 @@ 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",
@@ -30,8 +29,8 @@ export const ReloadButton:FC<ReloadButton> = ({ onPress, right }) => {
right: 10 + right,
width: 50,
height: 50,
backgroundColor: LoadError ? "red" : fixed.primary,
borderColor: fixed.textOnPrimary,
backgroundColor: LoadError ? "red" : "#0099CC",
borderColor: "white",
borderStyle: "solid",
borderWidth: 1,
borderRadius: 50,
@@ -47,13 +46,13 @@ export const ReloadButton:FC<ReloadButton> = ({ onPress, right }) => {
height: "auto",
textAlignVertical: "center",
fontWeight: "bold",
color: fixed.textOnPrimary,
color: "white",
},
};
return (
<TouchableOpacity onPress={onPress} style={styles.touch}>
<View style={{ flex: 1 }} />
<Ionicons name="reload" color={fixed.textOnPrimary} size={30} />
<Ionicons name="reload" color="white" size={30} />
<View style={{ flex: 1 }} />
</TouchableOpacity>
);

View File

@@ -1,5 +1,5 @@
import React from "react";
import { Platform, LayoutAnimation, useColorScheme } from "react-native";
import { Platform, LayoutAnimation } from "react-native";
import { WebView } from "react-native-webview";
import {
@@ -23,8 +23,6 @@ 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 {
@@ -160,7 +158,6 @@ export const AppsWebView = ({ openStationACFromEachTrainInfo }) => {
javaScriptEnabled
allowsBackForwardNavigationGestures
setSupportMultipleWindows
style={{ backgroundColor: bgColor }}
{...{ onMessage, onNavigationStateChange, onLoadEnd }}
injectedJavaScript={injectJavascript}
/>

View File

@@ -2,15 +2,13 @@ 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: fixed.primary }}>
{currentTrain && currentTrain.map((d) => <Text key={d.num}>{d.num}</Text>)}
<View style={{ height: "100%", backgroundColor: "#0099CC" }}>
{currentTrain && currentTrain.map((d) => <Text>{d.num}</Text>)}
<BigButton onPress={goBack} string="閉じる" />
</View>
);

View File

@@ -1,8 +1,10 @@
import {
ScrollView,
View,
Animated,
LayoutAnimation,
ViewStyle,
Platform,
} from "react-native";
import React, {
useEffect,
@@ -10,12 +12,11 @@ import React, {
useState,
useLayoutEffect,
ReactNode,
MutableRefObject,
useRef,
} from "react";
import { ScrollView } from "react-native-actions-sheet";
import { NativeViewGestureHandler } from "react-native-gesture-handler";
import { AS } from "../storageControl";
import { STORAGE_KEYS } from "@/constants";
import { useThemeColors } from "@/lib/theme";
type HeaderSize = "small" | "big" | "default";
@@ -27,7 +28,7 @@ type DynamicHeaderScrollViewProps = {
topStickyContent?: ReactNode;
styles?: { header: ViewStyle };
from?: string;
scrollRef?: MutableRefObject<any>;
scrollHandlers?: any;
};
export const DynamicHeaderScrollView: React.FC<DynamicHeaderScrollViewProps> = (
@@ -41,9 +42,8 @@ export const DynamicHeaderScrollView: React.FC<DynamicHeaderScrollViewProps> = (
topStickyContent,
styles,
from,
scrollRef,
scrollHandlers,
} = 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: fixed.primary,
backgroundColor: "#0099CC",
margin: 0,
top: 0,
opacity: 1,
},
off: {
height: Max_Header_Height,
backgroundColor: fixed.primary,
backgroundColor: "#0099CC",
margin: 0,
top: 0,
opacity: 0,
@@ -102,14 +102,14 @@ export const DynamicHeaderScrollView: React.FC<DynamicHeaderScrollViewProps> = (
const longHeaderStyle = {
on: {
height: Max_Header_Height,
backgroundColor: fixed.primary,
backgroundColor: "#0099CC",
margin: 0,
top: 0,
opacity: 1,
},
off: {
height: 0,
backgroundColor: fixed.primary,
backgroundColor: "#0099CC",
margin: 0,
top: 0,
opacity: 0,
@@ -135,6 +135,7 @@ export const DynamicHeaderScrollView: React.FC<DynamicHeaderScrollViewProps> = (
const [headerVisible, setHeaderVisible] = useState(false);
const onScroll = (event) => {
scrollHandlers.onScroll(event);
switch (headerSize) {
case "big":
setHeaderVisible(false);
@@ -178,27 +179,31 @@ export const DynamicHeaderScrollView: React.FC<DynamicHeaderScrollViewProps> = (
{topStickyContent}
</Animated.View>
</View>
<ScrollView
ref={scrollRef}
nestedScrollEnabled
bounces={false}
style={{ zIndex: 0 }}
stickyHeaderIndices={[1]}
scrollEventThrottle={1}
onScroll={onScroll}
<NativeViewGestureHandler
simultaneousHandlers={scrollHandlers.simultaneousHandlers}
>
<View style={{ height: Scroll_Distance, flexDirection: "column" }} />
{topStickyContent && (
<View
style={{
paddingTop: Min_Header_Height + 40,
flexDirection: "column",
}}
//index={1}
/>
)}
{children}
</ScrollView>
<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>
);
};

View File

@@ -8,10 +8,8 @@ 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();
@@ -28,19 +26,19 @@ export const FavoriteList: FC = () => {
goBack();
};
return (
<View style={{ height: "100%", backgroundColor: fixed.primary }}>
<View style={{ height: "100%", backgroundColor: "#0099CC" }}>
<Text
style={{
textAlign: "center",
fontSize: 20,
color: fixed.textOnPrimary,
color: "white",
fontWeight: "bold",
paddingVertical: 10,
}}
>
</Text>
<ScrollView style={{ height: "100%", backgroundColor: colors.background }}>
<ScrollView style={{ height: "100%", backgroundColor: "white" }}>
{favoriteStation
.map((currentStation) => {
return (
@@ -67,7 +65,7 @@ export const FavoriteList: FC = () => {
}}
>
<View style={{ flex: 1 }} />
<Text style={{ fontSize: 20, color: colors.text }}></Text>
<Text style={{ fontSize: 20 }}></Text>
<Icon name="chevron-right" size={20} />
</View>
</FavoriteListItem>
@@ -76,7 +74,7 @@ export const FavoriteList: FC = () => {
</ScrollView>
<Text
style={{
backgroundColor: colors.background,
backgroundColor: "white",
borderWidth: 1,
borderStyle: "solid",
}}

View File

@@ -1,6 +1,5 @@
import Sign from "@/components/駅名表/Sign";
import { SpotSign } from "@/components/観光スポット看板/SpotSign";
import React, { useCallback, useEffect, useRef, useState } from "react";
import React, { useEffect, useRef, useState } from "react";
import { AS } from "@/storageControl";
import {
useWindowDimensions,
@@ -14,19 +13,6 @@ 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,
@@ -34,127 +20,23 @@ export const CarouselBox = ({
setListIndex,
listIndex,
navigate,
stationSource,
}: {
originalStationList: any;
listUpStation: any[][];
nearPositionStation: any[][];
setListIndex: (i: number) => void;
listIndex: number;
navigate: any;
stationSource: StationSource;
stationListMode,
isSearchMode
}) => {
const carouselRef = useRef<ICarouselInstance>(null);
const { width } = useWindowDimensions();
const { colors, fixed } = useThemeColors();
const { height, width } = useWindowDimensions();
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 });
}, [listIndex, dotButton, width]);
// ドット表示設定の読み込み
useEffect(() => {
AS.getItem("CarouselSettings/activeDotSettings").then((data) => {
setDotButton(data === "true");
carouselBadgeScrollViewRef.current.scrollTo({
x: scrollToIndex,
animated: true,
});
}, []);
}, [listIndex, dotButton, width, carouselBadgeScrollViewRef]);
const oPSign = () => {
const payload = {
currentStation: listUpStation[listIndex],
@@ -167,38 +49,37 @@ 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);
};
const RenderItem = useCallback(
({ item }) => (
useEffect(() => {
AS.getItem("CarouselSettings/activeDotSettings").then((data) => {
setDotButton(data === "true");
});
}, []);
const RenderItem = ({ item, index }) => {
return (
<View
style={{ backgroundColor: "#0000", width, flexDirection: "row" }}
key={item[0].StationNumber ?? item[0].Station_JP}
style={{
backgroundColor: "#0000",
width,
flexDirection: "row",
marginLeft: 0,
marginRight: 0,
}}
key={item[0].StationNumber}
>
<View style={{ flex: 1 }} />
{item[0].isSpot ? (
<SpotSign
item={item}
isCurrentStation={item == nearPositionStation}
/>
) : item[0].StationNumber != "null" ? (
{item[0].StationNumber != "null" ? (
<Sign
stationID={item[0].StationNumber}
isCurrentStation={item == nearPositionStation}
@@ -210,18 +91,20 @@ export const CarouselBox = ({
style={{
width: width * 0.8,
height: ((width * 0.8) / 20) * 9,
borderColor: fixed.primary,
borderColor: "#0099CC",
borderWidth: 1,
backgroundColor: colors.background,
backgroundColor: "white",
}}
>
<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"
<View
style={{
flex: 1,
justifyContent: "center",
alignItems: "center",
}}
>
<Text style={{ color: "#0099CC", fontSize: 20 }}>
{!!isSearchMode ? "路線検索モードです。入力欄に駅名やナンバリングを入力したり、上に並んでいる路線を選んでみましょう!" :stationListMode == "position"
? "現在地の近くに駅がありません。"
: "お気に入りリストがありません。お気に入りの駅を追加しよう!"}
</Text>
@@ -230,141 +113,74 @@ 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 }}>
{/* カルーセル / グリッド(同じ高さ領域を共用・クロスフェード) */}
<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>
)}
<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>
</View>
);
};

View File

@@ -1,55 +1,28 @@
import { AS } from "@/storageControl";
import React, { useRef } from "react";
import React 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,
stationSource,
setStationSource,
closeSearch,
stationListMode,
setStationListMode,
setSelectedCurrentStation,
mapMode,
setMapMode,
}: {
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>>;
isSearchMode,
setisSearchMode,
input,
setInput,
}) => {
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,
@@ -65,8 +38,11 @@ export const CarouselTypeChanger = ({
setMapMode(false);
};
return (
<View
ref={containerRef}
<KeyboardAvoidingView
behavior="position"
contentContainerStyle={{ flex: 1, flexDirection: "row" }}
keyboardVerticalOffset={mapMode ? 0 : 45}
enabled={Platform.OS === "ios"}
style={{
width: "100%",
height: 40,
@@ -74,22 +50,21 @@ export const CarouselTypeChanger = ({
position: mapMode ? "absolute" : "relative",
bottom: mapMode ? 0 : undefined,
zIndex: 1000,
backgroundColor: colors.background,
backgroundColor: "white",
}}
key={"carouselTypeChanger"}
>
<SearchUnitBox
stationSource={stationSource}
setStationSource={setStationSource}
closeSearch={closeSearch}
mapMode={mapMode}
parentRef={containerRef}
isSearchMode={isSearchMode}
setisSearchMode={setisSearchMode}
input={input}
setInput={setInput}
/>
<TouchableOpacity
style={{
flex: 1,
backgroundColor:
stationSource.type === "position" ? fixed.primary : "#0099CC80",
stationListMode == "position" ? "#0099CC" : "#0099CC80",
padding: 5,
alignItems: "center",
flexDirection: "row",
@@ -103,21 +78,20 @@ export const CarouselTypeChanger = ({
onPressIn={() => {
if (!position) return;
returnToDefaultMode();
setStationSource({ type: "position" });
AS.setItem(STORAGE_KEYS.STATION_LIST_MODE, "position");
setStationListMode("position");
AS.setItem("stationListMode", "position");
setSelectedCurrentStation(0);
}}
onPress={() => {
if (!position) return;
returnToDefaultMode();
setStationSource({ type: "position" });
AS.setItem(STORAGE_KEYS.STATION_LIST_MODE, "position");
setStationListMode("position");
AS.setItem("stationListMode", "position");
setSelectedCurrentStation(0);
}}
onLongPress={handleGpsFollowLongPress}
>
<Ionicons
name={isGpsFollowing ? "navigate" : "locate-outline"}
name="locate-outline"
size={14}
color="white"
style={{ margin: 5 }}
@@ -147,7 +121,7 @@ export const CarouselTypeChanger = ({
<Ionicons
name={!mapMode ? "menu" : "chevron-up-outline"}
size={30}
color={colors.iconAccent}
color="#0099CC"
style={{ marginHorizontal: 5 }}
/>
</TouchableOpacity>
@@ -155,7 +129,7 @@ export const CarouselTypeChanger = ({
style={{
flex: 1,
backgroundColor:
stationSource.type === "favorite" ? fixed.primary : "#0099CC80",
stationListMode == "favorite" ? "#0099CC" : "#0099CC80",
padding: 5,
alignItems: "center",
flexDirection: "row",
@@ -167,14 +141,16 @@ export const CarouselTypeChanger = ({
}}
onPressIn={() => {
returnToDefaultMode();
setStationSource({ type: "favorite" });
AS.setItem(STORAGE_KEYS.STATION_LIST_MODE, "favorite");
// お気に入りリスト更新
setStationListMode("favorite");
AS.setItem("stationListMode", "favorite");
setSelectedCurrentStation(0);
}}
onPress={() => {
returnToDefaultMode();
setStationSource({ type: "favorite" });
AS.setItem(STORAGE_KEYS.STATION_LIST_MODE, "favorite");
// お気に入りリスト更新
setStationListMode("favorite");
AS.setItem("stationListMode", "favorite");
setSelectedCurrentStation(0);
}}
>
@@ -191,6 +167,6 @@ export const CarouselTypeChanger = ({
</Text>
</TouchableOpacity>
</View>
</KeyboardAvoidingView>
);
};

View File

@@ -1,125 +0,0 @@
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>
);
});

View File

@@ -1,115 +0,0 @@
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>
);
});

View File

@@ -1,132 +0,0 @@
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 の renderItemuseCallback でメモ化) */
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,
};
}

View File

@@ -14,13 +14,9 @@ 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}
@@ -74,7 +70,7 @@ export const FixedContentBottom = (props) => {
</Text>
</TextBox>
<TextBox
backgroundColor={fixed.primary}
backgroundColor="#0099CC"
flex={1}
onPressButton={() =>
SheetManager.show("SpecialTrainInfo", {
@@ -82,52 +78,52 @@ export const FixedContentBottom = (props) => {
})
}
>
<Text style={{ color: fixed.textOnPrimary, fontWeight: "bold", fontSize: 20 }}>
<Text style={{ color: "white", fontWeight: "bold", fontSize: 20 }}>
</Text>
<Text style={{ color: fixed.textOnPrimary, fontSize: 18 }}>
<Text style={{ color: "white", fontSize: 18 }}>
</Text>
</TextBox>
<TextBox
backgroundColor={fixed.primary}
backgroundColor="#0099CC"
flex={1}
onPressButton={() =>
Linking.openURL("https://www.jr-shikoku.co.jp/03_news/press/")
}
>
<Text style={{ color: fixed.textOnPrimary, fontWeight: "bold", fontSize: 20 }}>
<Text style={{ color: "white", fontWeight: "bold", fontSize: 20 }}>
</Text>
<Text style={{ color: fixed.textOnPrimary, fontSize: 18 }}>
<Text style={{ color: "white", fontSize: 18 }}>
</Text>
</TextBox>
<TextBox
backgroundColor={fixed.primary}
backgroundColor="#0099CC"
flex={1}
onPressButton={() =>
Linking.openURL("https://www.jr-shikoku.co.jp/teiki/")
}
>
<Text style={{ color: fixed.textOnPrimary, fontWeight: "bold", fontSize: 20 }}>
<Text style={{ color: "white", fontWeight: "bold", fontSize: 20 }}>
</Text>
<Text style={{ color: fixed.textOnPrimary, fontSize: 18 }}>
<Text style={{ color: "white", fontSize: 18 }}>
//
</Text>
</TextBox>
<TextBox
backgroundColor={fixed.primary}
backgroundColor="#0099CC"
flex={1}
onPressButton={() =>
Linking.openURL("https://www.jr-shikoku.co.jp/04_company/group/sp/")
}
>
<Text style={{ color: fixed.textOnPrimary, fontWeight: "bold", fontSize: 20 }}>
<Text style={{ color: "white", fontWeight: "bold", fontSize: 20 }}>
JR四国のお店
</Text>
<Text style={{ color: fixed.textOnPrimary, fontSize: 18 }}>
<Text style={{ color: "white", fontSize: 18 }}>
JR四国グループの施設をご案内
</Text>
</TextBox>
@@ -136,7 +132,7 @@ export const FixedContentBottom = (props) => {
style={{
flex: 1,
backgroundColor: "#729FCF",
borderColor: fixed.primary,
borderColor: "#0099CC",
padding: 10,
borderWidth: 1,
margin: 2,
@@ -158,43 +154,37 @@ export const FixedContentBottom = (props) => {
<TouchableOpacity
style={{
flex: 1,
backgroundColor: felicaAvailable ? "#00796B" : "#9E9E9E",
borderColor: fixed.primary,
backgroundColor: "#8AE234",
borderColor: "#0099CC",
padding: 10,
borderWidth: 1,
margin: 2,
alignItems: "center",
opacity: felicaAvailable ? 1 : 0.6,
}}
onPress={() => props.navigate("setting", { screen: "FelicaHistoryPage" })}
disabled={!felicaAvailable}
onPress={() => Linking.openURL("tel:0570-00-4592")}
>
<Text style={{ color: "white", fontWeight: "bold", fontSize: 20 }}>
IC残高
JR四国案内センター
</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>
)}
<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>
</TouchableOpacity>
</View>
<TextBox
backgroundColor={fixed.primary}
backgroundColor="#0099CC"
flex={1}
onPressButton={() => SheetManager.show("Social")}
>
<Text style={{ color: fixed.textOnPrimary, fontWeight: "bold", fontSize: 20 }}>
<Text style={{ color: "white", fontWeight: "bold", fontSize: 20 }}>
</Text>
<Text style={{ color: fixed.textOnPrimary, fontSize: 18 }}>
<Text style={{ color: "white", fontSize: 18 }}>
JR四国のSNS一覧です
</Text>
</TextBox>
<Text style={{ fontWeight: "bold", fontSize: 20, color: colors.text }}>
<Text style={{ fontWeight: "bold", fontSize: 20 }}>
JR四国非公式列車データベース(β)
</Text>
<View style={{ flexDirection: "row" }}>
@@ -237,37 +227,7 @@ export const FixedContentBottom = (props) => {
<Ionicons name="search" color="white" size={40} />
</TextBox>
</View>
<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>
<Text style={{ fontWeight: "bold", fontSize: 20 }}></Text>
<TextBox
backgroundColor="rgb(88, 101, 242)"
flex={1}
@@ -294,10 +254,10 @@ export const FixedContentBottom = (props) => {
})
}
>
<Text style={{ color: colors.text, fontWeight: "bold", fontSize: 20 }}>
<Text style={{ color: "black", fontWeight: "bold", fontSize: 20 }}>
</Text>
<Text style={{ color: colors.text, fontSize: 18 }}>
<Text style={{ color: "black", fontSize: 18 }}>
</Text>
</TextBox>

View File

@@ -11,15 +11,13 @@ 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: fixed.primary,
backgroundColor: "#0099CC",
borderRadius: 5,
margin: 10,
borderColor: "black",
@@ -27,7 +25,7 @@ export const JRSTraInfoBox = () => {
overflow: "hidden",
},
scroll: {
backgroundColor: fixed.primary,
backgroundColor: "#0099CC",
borderRadius: 5,
maxHeight: 300,
},
@@ -41,7 +39,7 @@ export const JRSTraInfoBox = () => {
},
box: {
padding: 10,
backgroundColor: colors.background,
backgroundColor: "white",
borderBottomLeftRadius: 5,
borderBottomRightRadius: 5,
},
@@ -55,16 +53,16 @@ export const JRSTraInfoBox = () => {
<View
style={{ padding: 10, flexDirection: "row", alignItems: "center" }}
>
<Text style={{ fontSize: 30, fontWeight: "bold", color: fixed.textOnPrimary }}>
<Text style={{ fontSize: 30, fontWeight: "bold", color: "white" }}>
EX
</Text>
<View style={{ flex: 1 }} />
<Text style={{ fontSize: 30, fontWeight: "bold", color: fixed.textOnPrimary }}>
<Text style={{ fontSize: 30, fontWeight: "bold", color: "white" }}>
{getTime ? dayjs(getTime).format("HH:mm") : NaN}
</Text>
<Ionicons
name="reload"
color={fixed.textOnPrimary}
color="white"
size={30}
style={{ margin: 5 }}
onPress={() => setLoadingDelayData(true)}
@@ -76,7 +74,7 @@ export const JRSTraInfoBox = () => {
<LottieView
autoPlay
loop
style={{ width: 150, height: 150, backgroundColor: colors.background }}
style={{ width: 150, height: 150, backgroundColor: "#fff" }}
source={require("@/assets/51690-loading-diamonds.json")}
/>
</View>
@@ -88,22 +86,22 @@ export const JRSTraInfoBox = () => {
style={{ flexDirection: "row" }}
key={data[1] + "key" + index}
>
<Text style={{ flex: 15, fontSize: 18, color: colors.text }}>
<Text style={{ flex: 15, fontSize: 18 }}>
{data[0].replace("\n", "")}
</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>
<Text style={{ flex: 5, fontSize: 18 }}>{data[1]}</Text>
<Text style={{ flex: 6, fontSize: 18 }}>{data[3]}</Text>
</View>
);
})
) : (
<Text style={{ color: colors.text }}>5</Text>
<Text>5</Text>
)}
</View>
</ScrollView>
<View style={styles.bottom}>
<View style={{ flex: 1 }} />
<Text style={{ color: fixed.textOnPrimary, fontWeight: "bold", fontSize: 20 }}>
<Text style={{ color: "white", fontWeight: "bold", fontSize: 20 }}>
</Text>
<View style={{ flex: 1 }} />

View File

@@ -1,69 +1,49 @@
import React, { useState } from "react";
import React 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 = ({
stationSource,
setStationSource,
closeSearch,
mapMode = false,
parentRef,
}: {
stationSource: StationSource;
setStationSource: (s: StationSource) => void;
closeSearch: () => void;
mapMode?: boolean;
parentRef?: React.RefObject<View>;
isSearchMode,
setisSearchMode,
input,
setInput,
}) => {
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 });
const { height, width } = useWindowDimensions();
return (
<>
<TouchableOpacity
style={{
position: "absolute",
bottom: isSearch
? (keyboardHeight > 0 ? measuredBottom : 0)
: 60,
bottom: !!isSearchMode ? 0 : 60,
right: 0,
padding: isSearch ? 5 : 10,
margin: isSearch ? 0 : 10,
backgroundColor: fixed.primary,
borderRadius: isSearch ? 5 : 50,
width: isSearch ? width : 50,
padding: !!isSearchMode ? 5 : 10,
margin: !!isSearchMode ? 0 : 10,
backgroundColor: "#0099CC",
borderRadius: !!isSearchMode ? 5 : 50,
width: !!isSearchMode ? width : 50,
zIndex: 1000,
}}
disabled={isSearch}
disabled={!!isSearchMode}
onPress={() => {
LayoutAnimation.configureNext({
duration: 100,
update: { type: "easeInEaseOut", springDamping: 0.6 },
});
setStationSource({ type: "search", query: "", lineId: undefined });
setisSearchMode(true);
}}
>
{!isSearch && <Ionicons name="search" size={30} color="white" />}
{isSearch && (
{!isSearchMode && <Ionicons name="search" size={30} color="white" />}
{!!isSearchMode && (
<View
style={{
backgroundColor: "#0099CC",
@@ -81,7 +61,7 @@ export const SearchUnitBox = ({
duration: 100,
update: { type: "easeInEaseOut", springDamping: 0.6 },
});
closeSearch();
setisSearchMode(false);
}}
>
<Ionicons
@@ -93,9 +73,9 @@ export const SearchUnitBox = ({
</TouchableOpacity>
<View
style={{
backgroundColor: colors.background,
backgroundColor: "white",
borderRadius: 25,
height: 40,
height: 30,
paddingRight: 10,
paddingLeft: 10,
flex: 1,
@@ -107,15 +87,13 @@ export const SearchUnitBox = ({
<TextInput
placeholder="駅名や駅ナンバリングを入力してフィルタリングします。"
onEndEditing={() => {}}
onChange={(ret) =>
setStationSource({ type: "search", query: ret.nativeEvent.text, lineId })
}
value={query}
style={{ flex: 1, height: "100%", paddingVertical: 0 }}
onChange={(ret) => setInput(ret.nativeEvent.text)}
value={input}
style={{ flex: 1 }}
/>
{query && (
{input && (
<TouchableOpacity
onPress={() => setStationSource({ type: "search", query: "", lineId })}
onPress={() => setInput("") }
style={{
padding: 3,
borderRadius: 15,
@@ -131,44 +109,48 @@ export const SearchUnitBox = ({
)}
</View>
</View>
{!query && (
{!input && (
<View style={{ flexDirection: "row", alignItems: "center" }}>
{Object.keys(lineList_LineWebID).map((d) => {
const buttonLineId = stationIDPair[lineList_LineWebID[d]];
return (
<TouchableOpacity
{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
style={{
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,
color: "white",
fontWeight: "bold",
fontSize: 20,
}}
onPress={() => {
// 同じ路線を再タップしても変化なし(元の挙動を維持)
if (lineId === buttonLineId) return;
setStationSource({ type: "search", query, lineId: buttonLineId });
}}
key={buttonLineId}
>
<Text
style={{
color: "white",
fontWeight: "bold",
fontSize: 20,
}}
>
{buttonLineId}
</Text>
</TouchableOpacity>
);
})}
{stationIDPair[lineList_LineWebID[d]]}
</Text>
</TouchableOpacity>
))}
</View>
)}
</View>

View File

@@ -1,6 +1,5 @@
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";
@@ -11,7 +10,6 @@ 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")
@@ -29,13 +27,13 @@ export const SpecialTrainInfoBox: FC<props> = ({ navigate }) => {
};
return (
<View style={{ backgroundColor: fixed.primary }}>
<View style={{ backgroundColor: "#0099CC" }}>
<View style={{ flexDirection: "row", alignItems: "center" }}>
<Text
style={{
fontSize: 30,
fontWeight: "bold",
color: fixed.textOnPrimary,
color: "white",
paddingHorizontal: 10,
paddingVertical: 5,
}}
@@ -43,7 +41,7 @@ export const SpecialTrainInfoBox: FC<props> = ({ navigate }) => {
</Text>
</View>
<ScrollView style={{ backgroundColor: colors.background }}>
<ScrollView style={{ backgroundColor: "white" }}>
{specialData.map((d) => (
<TouchableOpacity
onPress={() => onPressItem(d)}
@@ -52,12 +50,12 @@ export const SpecialTrainInfoBox: FC<props> = ({ navigate }) => {
style={{
padding: 10,
borderBottomWidth: 1,
borderBottomColor: colors.border,
borderBottomColor: "#ccc",
flexDirection: "row",
alignItems: "center",
}}
>
<Text style={{ color: colors.text, fontSize: 20 }}>{d.text}</Text>
<Text style={{ color: "black", fontSize: 20 }}>{d.text}</Text>
</TouchableOpacity>
))}
</ScrollView>

View File

@@ -1,6 +1,5 @@
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";
@@ -19,10 +18,9 @@ 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}
@@ -59,7 +57,7 @@ export const StationNumber: FC<StationNumberProps> = (props) => {
width: size,
height: size,
borderColor: lineColorList[lineID],
backgroundColor: lightColors.background,
backgroundColor: "white",
borderWidth: active ? 2 : 1,
borderRadius: 22,
}}
@@ -72,7 +70,7 @@ export const StationNumber: FC<StationNumberProps> = (props) => {
margin: 0,
padding: 0,
textAlign: "center",
color: lightColors.text,
color: "black",
fontWeight: active ? "bold" : "normal",
textAlignVertical: "center",
}}

View File

@@ -1,12 +1,9 @@
import { View, TouchableOpacity, Linking, Platform, Image, useWindowDimensions } from "react-native";
import { useThemeColors } from "@/lib/theme";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { View, TouchableOpacity, Linking,Platform, Image, useWindowDimensions } from "react-native";
import Constants from "expo-constants";
export const TitleBar = () => {
const { width } = useWindowDimensions();
const { colors } = useThemeColors();
const { top } = useSafeAreaInsets();
return (
<View
style={{
@@ -16,13 +13,13 @@ export const TitleBar = () => {
left: 0,
right: 0,
zIndex: 100,
paddingTop: top,
paddingTop: Platform.OS == "ios" ? Constants.statusBarHeight : 0,
}}
>
<TouchableOpacity
onPress={() => Linking.openURL("https://www.jr-shikoku.co.jp")}
>
<Image source={require("../../assets/Header.png")} style={{ width: width, resizeMode: "contain", backgroundColor: colors.background, height: 80 }} />
<Image source={require("../../assets/Header.png")} style={{ width: width, resizeMode: "contain", backgroundColor: "white", height: 80 }} />
</TouchableOpacity>
</View>
);

View File

@@ -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";
import MaterialCommunityIcons from "@expo/vector-icons/build/MaterialCommunityIcons";
export const TopMenuButton = () => {
const buttonList:{
backgroundColor: string;

View File

@@ -1,428 +0,0 @@
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,
},
});

View File

@@ -4,10 +4,8 @@ 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) => {
@@ -18,7 +16,7 @@ export const FavoriteSettingsItem = ({ currentStation }) => {
});
return (
<View style={{ flexDirection: "row", backgroundColor: colors.background }}>
<View style={{ flexDirection: "row", backgroundColor: "white" }}>
<View
style={{
width: 35,
@@ -39,7 +37,7 @@ export const FavoriteSettingsItem = ({ currentStation }) => {
<View style={{ flex: 1 }} />
<Text
style={{
color: fixed.textOnPrimary,
color: "white",
textAlign: "center",
fontSize: 12,
fontWeight: "bold",
@@ -59,13 +57,13 @@ export const FavoriteSettingsItem = ({ currentStation }) => {
padding: 8,
flexDirection: "row",
borderBottomWidth: 1,
borderBottomColor: colors.borderLight,
borderBottomColor: "#f0f0f0",
flex: 1,
alignContent: "center",
alignItems: "center",
}}
>
<Text style={{ fontSize: 20, color: colors.text }}>{currentStation[0].Station_JP}</Text>
<Text style={{ fontSize: 20 }}>{currentStation[0].Station_JP}</Text>
<View style={{ flex: 1 }} />
</View>
<View
@@ -75,7 +73,7 @@ export const FavoriteSettingsItem = ({ currentStation }) => {
alignSelf: "center",
}}
>
<Ionicons name={"reorder-two"} size={20} color={colors.text} style={{ marginHorizontal: 10 }} />
<Ionicons name={"reorder-two"} size={20} style={{ marginHorizontal: 10 }} />
</View>
</View>
);

View File

@@ -8,13 +8,11 @@ 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 (
@@ -22,13 +20,13 @@ export const FavoriteSettings = () => {
);
}, []);
return (
<View style={{ height: "100%", backgroundColor: fixed.primary }}>
<View style={{ height: "100%", backgroundColor: "#0099CC" }}>
<SheetHeaderItem
title="お気に入り設定"
LeftItem={{ title: " 設定", onPress: goBack }}
/>
<Animated.ScrollView
style={{ flex: 1, backgroundColor: colors.background }}
style={{ flex: 1, backgroundColor: "white" }}
contentContainerStyle={styles.contentContainer}
ref={scrollableRef}
>
@@ -58,7 +56,7 @@ export const FavoriteSettings = () => {
</Animated.ScrollView>
<Text
style={{
backgroundColor: colors.background,
backgroundColor: "white",
borderWidth: 1,
borderStyle: "solid",
}}

View File

@@ -1,325 +0,0 @@
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" },
});

View File

@@ -16,17 +16,15 @@ 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: fixed.primary }}>
<View style={{ height: "100%", backgroundColor: "#0099CC" }}>
<SheetHeaderItem title="アイコン設定" LeftItem={{ title: " 設定", onPress: goBack }} />
<ScrollView style={{ flex: 1, backgroundColor: colors.background }}>
<ScrollView style={{ flex: 1, backgroundColor: "white" }}>
{currentIcon ? (
<>
<Text
@@ -47,22 +45,19 @@ export const LauncherIconSettings = () => {
padding: 10,
}}
>
<View
<Image
source={iconList.filter(({ id }) => id == currentIcon)[0].icon}
style={{
width: 50,
height: 50,
padding: 30,
borderWidth: 1,
borderRadius: 10,
borderColor: colors.border,
borderColor: "white",
margin: 10,
padding: 10,
backgroundColor: colors.background,
backgroundColor: "white",
}}
>
<Image
source={iconList.filter(({ id }) => id == currentIcon)[0].icon}
style={{ width: 80, height: 80, borderRadius: 8 }}
resizeMode="contain"
/>
</View>
/>
<Text>JR四国非公式アプリ</Text>
</View>
</>

View File

@@ -2,10 +2,9 @@ import React from "react";
import { View, Text, TouchableOpacity, ScrollView } from "react-native";
import { useNavigation } from "@react-navigation/native";
import { SwitchArea } from "../atom/SwitchArea";
import { Button, CheckBox } from "@rneui/themed";
import { CheckBox } from "react-native-elements";
import { TripleSwitchArea } from "../atom/TripleSwitchArea";
import { SheetHeaderItem } from "../atom/SheetHeaderItem";
import { useThemeColors } from "@/lib/theme";
export const LayoutSettings = ({
iconSetting,
@@ -25,14 +24,12 @@ export const LayoutSettings = ({
headerSize,
setHeaderSize,
}) => {
const { goBack, navigate } = useNavigation() as any;
const { colors, fixed } = useThemeColors();
const { goBack } = useNavigation();
return (
<View style={{ height: "100%", backgroundColor: fixed.primary }}>
<View style={{ height: "100%", backgroundColor: "#0099CC" }}>
<SheetHeaderItem title="レイアウト設定" LeftItem={{ title: " 設定", onPress: goBack }} />
<ScrollView style={{ flex: 1, backgroundColor: colors.background }}>
<ScrollView style={{ flex: 1, backgroundColor: "white" }}>
<View style={{ flex: 1 }}>
<Button title="ICカード残高・履歴" onPress={() => navigate("FelicaHistoryPage")} />
<SwitchArea
str="列車アイコン表示"
bool={iconSetting}
@@ -120,13 +117,11 @@ export const LayoutSettings = ({
);
};
const SimpleSwitch = ({ bool, setBool, str }) => {
const { colors } = useThemeColors();
return (
const SimpleSwitch = ({ bool, setBool, str }) => (
<View style={{ flexDirection: "row" }}>
<CheckBox
checked={bool == "true" ? true : false}
checkedColor={colors.switchActive}
checkedColor="red"
onPress={() => setBool(bool == "true" ? "false" : "true")}
containerStyle={{
flex: 1,
@@ -134,9 +129,8 @@ const SimpleSwitch = ({ bool, setBool, str }) => {
borderColor: "white",
alignContent: "center",
}}
textStyle={{ fontSize: 20, fontWeight: "normal", color: colors.text }}
textStyle={{ fontSize: 20, fontWeight: "normal" }}
title={str}
/>
</View>
);
};
);

View File

@@ -2,18 +2,16 @@ import React, { useEffect, useState } from "react";
import { View, Text, TouchableOpacity, ScrollView } from "react-native";
import * as Clipboard from "expo-clipboard";
import { CheckBox } from "@rneui/themed";
import { CheckBox } from "react-native-elements";
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);
@@ -47,13 +45,13 @@ export const NotificationSettings = () => {
});
};
return (
<View style={{ height: "100%", backgroundColor: fixed.primary }}>
<View style={{ height: "100%", backgroundColor: "#0099CC" }}>
<SheetHeaderItem
title="通知設定(β)"
LeftItem={{ title: " 設定", onPress: goBack }}
RightItem={{ title: "登録実行", onPress: setRegister }}
/>
<ScrollView style={{ flex: 1, backgroundColor: colors.background }}>
<ScrollView style={{ flex: 1, backgroundColor: "white" }}>
<SimpleSwitch
bool={traInfoEX}
setBool={setTraInfoEX}
@@ -80,13 +78,11 @@ export const NotificationSettings = () => {
);
};
const SimpleSwitch = ({ bool, setBool, str }) => {
const { colors } = useThemeColors();
return (
const SimpleSwitch = ({ bool, setBool, str }) => (
<View style={{ flexDirection: "row" }}>
<CheckBox
checked={bool == "true" ? true : false}
checkedColor={colors.switchActive}
checkedColor="red"
onPress={() => setBool(bool == "true" ? "false" : "true")}
containerStyle={{
flex: 1,
@@ -94,9 +90,8 @@ const SimpleSwitch = ({ bool, setBool, str }) => {
borderColor: "white",
alignContent: "center",
}}
textStyle={{ fontSize: 20, fontWeight: "normal", color: colors.text }}
textStyle={{ fontSize: 20, fontWeight: "normal" }}
title={str}
/>
</View>
);
};
);

View File

@@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useState } from "react";
import React from "react";
import {
View,
Text,
@@ -16,17 +16,8 @@ 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.1"; // Update this version code as needed
const settingsPreviewSound = require("../../assets/sound/rikka-test.mp3");
const versionCode = "6.2.1"; // Update this version code as needed
export const SettingTopPage = ({
testNFC,
@@ -36,96 +27,33 @@ 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: fixed.primary }}>
<View style={{ height: "100%", backgroundColor: "#0099CC" }}>
<SheetHeaderItem title="アプリの設定画面" LeftItem={{
title: "閉じる",
onPress: () => navigation.goBack(),
}} />
<ScrollView style={{ flex: 1, backgroundColor: colors.backgroundSecondary }}>
<ScrollView style={{ flex: 1, backgroundColor: "#f8f8fc" }}>
<View style={{ height: 300, padding: 10 }}>
<View style={{ flex: 1 }} />
<TouchableOpacity activeOpacity={0.9} onPress={onPressHeaderImage}>
<Image
source={require("../../assets/Header.png")}
style={{
aspectRatio: 8.08,
height: undefined,
width: width - 20,
borderRadius: 5,
}}
/>
</TouchableOpacity>
<Image
source={require("../../assets/Header.png")}
style={{
aspectRatio: 8.08,
height: undefined,
width: width - 20,
borderRadius: 5,
}}
/>
<View style={{ flexDirection: "row", paddingTop: 10 }}>
<View style={{ flex: 1 }} />
<Text style={{ color: colors.text }}>: {versionCode}</Text>
<Text>: {versionCode}</Text>
<View style={{ flex: 1 }} />
</View>
<View style={{ flexDirection: "row", paddingBottom: 10 }}>
<View style={{ flex: 1 }} />
<Text style={{ color: colors.text }}>ReleaseChannel: {Updates.channel}</Text>
<Text>ReleaseChannel: {Updates.channel}</Text>
<View style={{ flex: 1 }} />
</View>
@@ -134,7 +62,6 @@ export const SettingTopPage = ({
style={{
fontSize: 14,
fontStyle: "italic",
color: colors.text,
}}
>
HARUKIN/Xprocessにより一部の機能を拡張したものです
@@ -143,7 +70,6 @@ export const SettingTopPage = ({
style={{
fontSize: 14,
fontStyle: "italic",
color: colors.text,
}}
>
HARUKIN/Xprocessにお願いします
@@ -182,15 +108,14 @@ export const SettingTopPage = ({
navigation.navigate("setting", { screen: "LayoutSettings" })
}
/>
{canAccessDataSourceSettings && (
{Platform.OS === "android" ? (
<SettingList
string="情報ソース設定"
string="ウィジェット設定"
onPress={() =>
navigation.navigate("setting", { screen: "DataSourceSettings" })
navigation.navigate("setting", { screen: "WidgetSettings" })
}
/>
)}
) : null}
<SettingList
string="アイコン設定"
onPress={() =>
@@ -238,7 +163,7 @@ export const SettingTopPage = ({
style={{
padding: 10,
flexDirection: "row",
borderColor: fixed.textOnPrimary,
borderColor: "white",
borderWidth: 1,
margin: 10,
borderRadius: 5,
@@ -247,7 +172,7 @@ export const SettingTopPage = ({
onPress={updateAndReload}
>
<View style={{ flex: 1 }} />
<Text style={{ fontSize: 25, fontWeight: "bold", color: fixed.textOnPrimary }}>
<Text style={{ fontSize: 25, fontWeight: "bold", color: "white" }}>
</Text>
<View style={{ flex: 1 }} />
@@ -257,7 +182,6 @@ export const SettingTopPage = ({
};
const SettingList = ({ string, onPress, disabled }) => {
const { colors } = useThemeColors();
return (
<ListItem
activeScale={0.95}
@@ -265,11 +189,9 @@ 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 style={{ color: colors.text }}>{string}</ListItem.Title>
<ListItem.Title>{string}</ListItem.Title>
</ListItem.Content>
<ListItem.Chevron />
</ListItem>

View File

@@ -0,0 +1,195 @@
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>
);
};

View File

@@ -8,21 +8,22 @@ 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 "@rneui/themed";
import { Switch } from "react-native-elements";
import AutoHeightImage from "react-native-auto-height-image";
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) {
@@ -50,26 +51,8 @@ export default function Setting(props) {
AS.getItem(STORAGE_KEYS.UI_SETTING).then(setUiSetting);
}, []);
const testNFC = async () => {
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 result = await ExpoFelicaReader.scan();
//alert(result);
};
const updateAndReload = () => {
Promise.all([
@@ -85,7 +68,7 @@ export default function Setting(props) {
]).then(() => Updates.reloadAsync());
};
return (
<Stack.Navigator id={null}>
<Stack.Navigator>
<Stack.Screen
name="settingTopPage"
options={{
@@ -133,6 +116,8 @@ export default function Setting(props) {
setTrainPosition={setTrainPosition}
uiSetting={uiSetting}
setUiSetting={setUiSetting}
testNFC={testNFC}
updateAndReload={updateAndReload}
headerSize={headerSize}
setHeaderSize={setHeaderSize}
/>
@@ -149,6 +134,17 @@ 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={{
@@ -171,28 +167,6 @@ 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>
);
}

View File

@@ -23,7 +23,6 @@ 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";
@@ -101,7 +100,6 @@ export const ExGridSimpleView: FC<{
];
const { currentTrain } = useCurrentTrain();
const { colors } = useThemeColors();
data.forEach((item) => {
let isOperating = false;
let [hour, minute] = dayjs()
@@ -131,39 +129,12 @@ 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: colors.diagramBackground }}
style={{ backgroundColor: "#fff" }}
>
{groupKeys.map((hour) => [
<View
@@ -171,10 +142,9 @@ export const ExGridSimpleView: FC<{
padding: 5,
borderBottomWidth: 0.5,
borderTopWidth: 0.5,
borderBottomColor: colors.diagramBorder,
backgroundColor: colors.diagramSectionHeader,
borderBottomColor: "#ccc",
backgroundColor: "#f0f0f0",
}}
onLayout={(e) => { yOffsets.current[hour] = e.nativeEvent.layout.y; }}
key={hour}
>
<Animated.Text
@@ -183,7 +153,6 @@ export const ExGridSimpleView: FC<{
fontSize: 15,
zIndex: 1,
marginLeft: 0,
color: colors.text,
},
stickyTextStyle,
]}
@@ -192,7 +161,6 @@ export const ExGridSimpleView: FC<{
</Animated.Text>
</View>,
<View
key={hour + "-items"}
style={{
flexDirection: "row",
position: "relative",

View File

@@ -19,7 +19,6 @@ 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: {
@@ -42,7 +41,6 @@ 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) {
@@ -55,10 +53,10 @@ export const ExGridSimpleViewItem: FC<{
}, []);
const { color, data } = getTrainType({
type: trainData?.type,
whiteMode: !isDark,
whiteMode: true,
});
// 行き先(駅名)の取得
const [destinationName] = useMemo(() => {
// 列車名、種別、フォントの取得
const [trainName] = useMemo(() => {
// to_dataが設定されていればそれを優先
if (trainData?.to_data) {
return [trainData.to_data];
@@ -77,9 +75,6 @@ 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])
@@ -151,7 +146,7 @@ export const ExGridSimpleViewItem: FC<{
// to_dataがある場合は、to_dataから駅名を抽出して色を判定
const stationNameForColor = trainData?.to_data
? trainData.to_data.replace(/行き$/, "") // 「行き」を除去
: destinationName;
: trainName;
const Stations = stationList
.map((a) => a.filter((d) => d.StationName == stationNameForColor))
@@ -166,7 +161,7 @@ export const ExGridSimpleViewItem: FC<{
);
setStationColor(stationLineColor || ["gray"]);
}
}, [stationList, destinationName, trainData]);
}, [stationList, trainName, trainData]);
// if(typeString == "回送"){
// return<></>;
// }
@@ -196,7 +191,7 @@ export const ExGridSimpleViewItem: FC<{
style={{
fontSize: 8,
fontWeight: "bold",
color: isCancelled ? "gray" : colors.text,
color: isCancelled ? "gray" : "black",
textAlign: "left",
textDecorationLine: isCancelled ? "line-through" : "none",
}}
@@ -226,7 +221,7 @@ export const ExGridSimpleViewItem: FC<{
top: 22,
left: 28,
fontWeight: "bold",
color: isCancelled ? "gray" : colors.text,
color: isCancelled ? "gray" : "black",
textDecorationLine: isCancelled ? "line-through" : "none",
}}
>
@@ -242,7 +237,7 @@ export const ExGridSimpleViewItem: FC<{
textDecorationLine: isCancelled ? "line-through" : "none",
}}
>
{destinationName}
{trainName}
</Text>
</View>
<View style={{ flex: 1 }} />

View File

@@ -23,7 +23,6 @@ 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 = {
@@ -100,7 +99,6 @@ export const ExGridView: FC<{
const { width } = useWindowDimensions();
const { currentTrain } = useCurrentTrain();
const { colors } = useThemeColors();
data.forEach((item) => {
let isOperating = false;
let [hour, minute] = dayjs()
@@ -185,7 +183,7 @@ export const ExGridView: FC<{
// アニメーションスタイル
const animatedStyle = useAnimatedStyle(() => ({
width: widthX.value,
backgroundColor: isChanging.value ? colors.diagramHighlight : colors.diagramBackground,
backgroundColor: isChanging.value ? "#8adeffff" : "white",
}));
// 時ヘッダーを横にスクロールしたときの処理
const scrollX = useSharedValue(0);
@@ -252,7 +250,7 @@ export const ExGridView: FC<{
ref={scrollRef}
contentContainerStyle={{
flexDirection: "column",
backgroundColor: colors.diagramBackground,
backgroundColor: "white",
}}
>
<Animated.View
@@ -273,10 +271,9 @@ export const ExGridView: FC<{
flex: 1,
textAlign: "left",
borderRightWidth: 0.5,
borderColor: colors.diagramBorder,
borderColor: "#ccc",
flexWrap: "nowrap",
fontSize: 12,
color: colors.text,
}}
>
{num - 5}
@@ -288,10 +285,9 @@ export const ExGridView: FC<{
style={{
textAlign: "right",
borderRightWidth: 0.5,
borderColor: colors.diagramBorder,
borderColor: "#ccc",
flexWrap: "nowrap",
fontSize: 12,
color: colors.text,
width: 50,
}}
key={"分LabelEnd"}
@@ -316,8 +312,8 @@ export const ExGridView: FC<{
padding: 5,
borderBottomWidth: 0.5,
borderTopWidth: 0.5,
borderBottomColor: colors.diagramBorder,
backgroundColor: colors.diagramSectionHeader,
borderBottomColor: "#ccc",
backgroundColor: "#f0f0f0",
}}
key={hour}
>
@@ -335,7 +331,6 @@ export const ExGridView: FC<{
</Animated.Text>
</View>,
<View
key={hour + "-items"}
style={{
flexDirection: "row",
position: "relative",

View File

@@ -19,7 +19,6 @@ 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: {
@@ -42,7 +41,6 @@ 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) {
@@ -53,7 +51,7 @@ export const ExGridViewItem: FC<{
});
}
}, []);
const { color, data } = getTrainType({ type: trainData?.type, whiteMode: !isDark });
const { color, data } = getTrainType({ type: trainData?.type, whiteMode: true });
// 列車名、種別、フォントの取得
const [
trainName,
@@ -218,7 +216,7 @@ export const ExGridViewItem: FC<{
bottom: 0,
right: 0,
fontWeight: "bold",
color: isCancelled ? "gray" : colors.text,
color: isCancelled ? "gray" : "black",
textDecorationLine: isCancelled ? "line-through" : "none",
}}
>

View File

@@ -1,10 +1,7 @@
import { FC, useRef, useEffect } from "react";
import { FC } 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;
@@ -14,14 +11,7 @@ type hoge = {
};
export const ListView: FC<{
data: hoge[];
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>>({});
}> = ({ data }) => {
const groupedData: Record<string, hoge[]> = {};
const groupKeys = [];
data.forEach((item) => {
@@ -32,56 +22,20 @@ 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
ref={scrollRef}
style={{ backgroundColor: colors.diagramBackground }}
style={{ backgroundColor: "white" }}
stickyHeaderIndices={
groupKeys.at(0) ? groupKeys.map((_, i) => i * 2) : []
}
>
{groupKeys.map((hour) => [
<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 style={{ backgroundColor: "white", padding: 5, borderBottomWidth: 0.5, borderTopWidth: 0.5, borderBottomColor: "#ccc" }} key={hour}>
<Text style={{ fontSize: 15 }}>{hour}</Text>
</View>,
<View key={hour + "-items"}>
<View>
{groupedData[hour].map((d, i) => (
<ListViewItem
key={d.trainNumber + i}
d={d}
showVehicle={showVehicle}
getUnyohubByTrainNumber={visibleSources.hub ? getUnyohubByTrainNumber : undefined}
getElesiteByTrainNumber={visibleSources.elesite ? getElesiteByTrainNumber : undefined}
showAppSource={visibleSources.app}
/>
<ListViewItem key={d.trainNumber + i} d={d} />
))}
</View>,
])}

View File

@@ -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, useRef, useState } from "react";
import { View, Text, TouchableOpacity, Animated } from "react-native";
import { FC, useEffect, useMemo, useState } from "react";
import { View, Text, TouchableOpacity } from "react-native";
import { customTrainDataDetector } from "../custom-train-data";
import dayjs from "dayjs";
import { SheetManager } from "react-native-actions-sheet";
@@ -14,7 +14,6 @@ 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: {
@@ -24,14 +23,9 @@ export const ListViewItem: FC<{
timeType: string;
time: string;
};
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();
}> = ({ d }) => {
const { allCustomTrainData } = useAllTrainDiagram();
const { navigate } = useNavigation();
const { colors, fixed, isDark } = useThemeColors();
const [trainData, setTrainData] = useState<CustomTrainData | undefined>();
useEffect(() => {
if (allCustomTrainData) {
@@ -44,7 +38,7 @@ export const ListViewItem: FC<{
}, []);
const { color, data } = getTrainType({
type: trainData?.type,
whiteMode: !isDark,
whiteMode: true,
});
// 列車名、種別、フォントの取得
const { getStationDataFromName, originalStationList } =
@@ -87,7 +81,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が設定されていればそれを最優先
@@ -205,47 +199,6 @@ 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
@@ -262,7 +215,7 @@ export const ListViewItem: FC<{
onPress={() => openTrainInfo()}
>
<View style={{ position: "relative", flex: 3 }}>
<Text style={{ fontSize: 30, fontFamily: "DiaPro", color: isCancelled ? "gray" : colors.text, textDecorationLine: isCancelled ? "line-through" : "none" }}>
<Text style={{ fontSize: 30, fontFamily: "DiaPro", color: isCancelled ? "gray" : "black", textDecorationLine: isCancelled ? "line-through" : "none" }}>
{formattedTime}
</Text>
<Text
@@ -272,7 +225,7 @@ export const ListViewItem: FC<{
bottom: -3,
right: 0,
fontWeight: "bold",
color: isCancelled ? "gray" : colors.text,
color: isCancelled ? "gray" : "black",
textDecorationLine: isCancelled ? "line-through" : "none",
}}
>
@@ -294,54 +247,31 @@ export const ListViewItem: FC<{
>
{typeString}
</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>
</>
}
<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>
</View>
<View style={{ flexDirection: "row", alignItems: "center", flex: 1 }}>
<Text

View File

@@ -8,7 +8,6 @@ import {
TouchableOpacity,
LayoutAnimation,
} from "react-native";
import { useThemeColors } from "@/lib/theme";
type hoge = {
trainNumber: string;
@@ -24,7 +23,6 @@ export const SearchInputSuggestBox: FC<{
currentStationDiagram: hoge;
}> = ({ input, setInput, currentStationDiagram }) => {
const { getStationDataFromName } = useStationList();
const { colors, fixed } = useThemeColors();
const [stationList, setStationList] = useState<
{
stationName: string;
@@ -85,7 +83,7 @@ export const SearchInputSuggestBox: FC<{
style={{
maxHeight: 200,
width: "100%",
backgroundColor: fixed.primary,
backgroundColor: "#0099CC",
zIndex: 100,
}}
>
@@ -97,15 +95,13 @@ export const SearchInputSuggestBox: FC<{
style={{
margin: 5,
padding: 5,
backgroundColor: colors.suggestBackground,
backgroundColor: "#eee",
borderRadius: 20,
}}
key={stationName + number.join(",")}
onPress={() => setInput(stationName)}
>
<Text style={{ color: colors.text }}>
{stationName.startsWith(".") ? stationName.slice(1) : stationName}
</Text>
<Text>{stationName}</Text>
</TouchableOpacity>
))}
</View>
@@ -114,7 +110,7 @@ export const SearchInputSuggestBox: FC<{
style={{
flexDirection: "row",
flexWrap: "wrap",
borderTopColor: colors.border,
borderTopColor: "#ccc",
borderTopWidth: 0.5,
paddingTop: 0,
marginTop: 10,
@@ -124,7 +120,7 @@ export const SearchInputSuggestBox: FC<{
style={{
margin: 5,
padding: 5,
backgroundColor: colors.suggestBackground,
backgroundColor: "#eee",
borderRadius: 5,
}}
key={"empty"}
@@ -136,7 +132,7 @@ export const SearchInputSuggestBox: FC<{
setListFiltered("");
}}
>
<Text style={{ color: colors.text }}></Text>
<Text></Text>
</TouchableOpacity>
{filteredStationLine.map((line) => (
<TouchableOpacity
@@ -145,7 +141,7 @@ export const SearchInputSuggestBox: FC<{
padding: 5,
backgroundColor: lineColorList[line]
? `${lineColorList[line]}`
: colors.suggestBackground,
: "#eee",
borderRadius: 5,
}}
key={line}
@@ -157,7 +153,7 @@ export const SearchInputSuggestBox: FC<{
setListFiltered(line);
}}
>
<Text style={{ color: lineColorList[line] ? `white` : colors.text }}>
<Text style={{ color: lineColorList[line] ? `white` : "black" }}>
{line}
</Text>
</TouchableOpacity>

View File

@@ -1,32 +1,27 @@
import { FC, useEffect, useRef, useState } from "react";
import { FC, useEffect, 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 "@rneui/themed";
import { Switch } from "react-native-elements";
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: {
@@ -57,16 +52,10 @@ export const StationDiagramView: FC<props> = ({ route }) => {
// 表示モード:縦並びリスト、横並びグリッド(時刻分割)、横並び単純左詰め
// フィルタリング:終点路線、種別、行先、関係停車駅
const { keyList, allTrainDiagram, allCustomTrainData, todayOperation } = useAllTrainDiagram();
const { useUnyohub: unyohubEnabled } = useUnyohub();
const { useElesite: elesiteEnabled } = useElesite();
const { keyList, allTrainDiagram, allCustomTrainData } = useAllTrainDiagram();
const { goBack } = useNavigation();
const { colors, fixed } = useThemeColors();
const tabBarHeight = useBottomTabBarHeight();
const containerRef = useRef<View>(null);
const { keyboardVisible: keyBoardVisible, measuredOffset: keyboardOffset } =
useKeyboardAvoid({ measureRef: containerRef, tabBarHeight });
const [keyBoardVisible, setKeyBoardVisible] = useState(false);
const [input, setInput] = useState("");
const [displayMode, setDisplayMode] = useState<
"list" | "grid" | "simpleGrid"
@@ -89,13 +78,6 @@ 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) {
@@ -196,106 +178,42 @@ export const StationDiagramView: FC<props> = ({ route }) => {
}
}, [currentStation, showLastStop, threw, input, selectedTypeList]);
// 時刻表データが更新されたら駅ロック Live Activity を自動更新
useEffect(() => {
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 ?? "",
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);
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentStationDiagram]);
return () => {
showSubscription.remove();
hideSubscription.remove();
};
}, []);
return (
<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>
<View style={{ height: "100%", backgroundColor: "#0099CC" }}>
<Text
style={{
textAlign: "center",
fontSize: 20,
color: "white",
fontWeight: "bold",
paddingVertical: 10,
}}
>
{currentStation[0].Station_JP}
</Text>
{displayMode === "list" ? (
<ListView data={currentStationDiagram} showVehicle={showVehicle} visibleSources={visibleSources} />
<ListView data={currentStationDiagram} />
) : displayMode === "simpleGrid" ? (
<ExGridSimpleView
data={currentStationDiagram}
@@ -313,7 +231,11 @@ export const StationDiagramView: FC<props> = ({ route }) => {
>
お気に入り登録した駅のうち、位置情報システムで移動可能な駅が表示されています。タップすることで位置情報システムの当該の駅に移動します。
</Text> */}
<View>
<KeyboardAvoidingView
behavior="padding"
keyboardVerticalOffset={80}
enabled={Platform.OS === "ios"}
>
{!keyBoardVisible ? (
<ScrollView
horizontal
@@ -327,9 +249,9 @@ export const StationDiagramView: FC<props> = ({ route }) => {
style={{
alignItems: "center",
marginHorizontal: 5,
backgroundColor: threw ? fixed.textOnPrimary : fixed.transparent,
backgroundColor: threw ? "white" : "#ffffff00",
alignSelf: "center",
borderColor: fixed.textOnPrimary,
borderColor: "white",
borderWidth: 1,
borderRadius: 100,
}}
@@ -339,7 +261,7 @@ export const StationDiagramView: FC<props> = ({ route }) => {
>
<Text
style={{
color: threw ? fixed.primary : fixed.textOnPrimary,
color: threw ? "#0099CC" : "white",
fontSize: 14,
margin: 5,
}}
@@ -351,9 +273,9 @@ export const StationDiagramView: FC<props> = ({ route }) => {
style={{
alignItems: "center",
marginHorizontal: 5,
backgroundColor: showLastStop ? fixed.textOnPrimary : fixed.transparent,
backgroundColor: showLastStop ? "white" : "#ffffff00",
alignSelf: "center",
borderColor: fixed.textOnPrimary,
borderColor: "white",
borderWidth: 1,
borderRadius: 100,
}}
@@ -363,7 +285,7 @@ export const StationDiagramView: FC<props> = ({ route }) => {
>
<Text
style={{
color: showLastStop ? fixed.primary : fixed.textOnPrimary,
color: showLastStop ? "#0099CC" : "white",
fontSize: 14,
margin: 5,
}}
@@ -376,7 +298,7 @@ export const StationDiagramView: FC<props> = ({ route }) => {
height: "auto",
borderLeftWidth: 1,
margin: 5,
borderColor: fixed.textOnPrimary,
borderColor: "white",
}}
/>
{showTypeFiltering ? (
@@ -425,9 +347,9 @@ export const StationDiagramView: FC<props> = ({ route }) => {
style={{
alignItems: "center",
marginHorizontal: 5,
backgroundColor: fixed.transparent,
backgroundColor: "#ffffff00",
alignSelf: "center",
borderColor: fixed.textOnPrimary,
borderColor: "white",
borderWidth: 1,
borderRadius: 100,
}}
@@ -440,7 +362,7 @@ export const StationDiagramView: FC<props> = ({ route }) => {
>
<Text
style={{
color: fixed.textOnPrimary,
color: "white",
fontSize: 14,
margin: 5,
}}
@@ -454,9 +376,9 @@ export const StationDiagramView: FC<props> = ({ route }) => {
style={{
alignItems: "center",
marginHorizontal: 5,
backgroundColor: fixed.transparent,
backgroundColor: "#ffffff00",
alignSelf: "center",
borderColor: fixed.textOnPrimary,
borderColor: "white",
borderWidth: 1,
borderRadius: 100,
}}
@@ -469,7 +391,7 @@ export const StationDiagramView: FC<props> = ({ route }) => {
>
<Text
style={{
color: fixed.textOnPrimary,
color: "white",
fontSize: 14,
margin: 5,
}}
@@ -483,9 +405,9 @@ export const StationDiagramView: FC<props> = ({ route }) => {
style={{
alignItems: "center",
marginHorizontal: 5,
backgroundColor: fixed.transparent,
backgroundColor: "#ffffff00",
alignSelf: "center",
borderColor: fixed.textOnPrimary,
borderColor: "white",
borderWidth: 1,
borderRadius: 100,
}}
@@ -501,7 +423,7 @@ export const StationDiagramView: FC<props> = ({ route }) => {
>
<Text
style={{
color: fixed.textOnPrimary,
color: "white",
fontSize: 14,
margin: 5,
}}
@@ -528,24 +450,24 @@ export const StationDiagramView: FC<props> = ({ route }) => {
height: 35,
margin: 5,
alignItems: "center",
backgroundColor: colors.searchBackground,
backgroundColor: "#F4F4F4",
flexDirection: "row",
paddingLeft: 10,
paddingRight: 10,
borderRadius: 25,
borderColor: colors.searchBorder,
borderColor: "#F4F4F4",
}}
>
<TextInput
placeholder="駅名を入力して停車駅でフィルタリングします。"
onFocus={() => {}}
onFocus={() => setKeyBoardVisible(true)}
onEndEditing={() => {}}
onChange={(ret) => setInput(ret.nativeEvent.text)}
value={input}
style={{ flex: 1, height: "100%", paddingVertical: 0 }}
style={{ flex: 1 }}
/>
</View>
</View>
</KeyboardAvoidingView>
{keyBoardVisible || (
<BigButton onPress={() => goBack()} string="閉じる" />
)}
@@ -560,7 +482,6 @@ 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 });
@@ -570,7 +491,7 @@ export const TypeSelectorBox: FC<{
alignItems: "center",
marginHorizontal: 5,
opacity: isSelected ? 1 : 0.8,
backgroundColor: isSelected ? fixed.textOnPrimary : color,
backgroundColor: isSelected ? "white" : color,
alignSelf: "center",
borderColor: color,
borderWidth: 1,
@@ -594,7 +515,7 @@ export const TypeSelectorBox: FC<{
>
<Text
style={{
color: isSelected ? color : fixed.textOnPrimary,
color: isSelected ? color : "white",
fontSize: 14,
margin: 5,
}}

View File

@@ -1,7 +1,6 @@
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;
@@ -10,15 +9,14 @@ 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: fixed.primary,
borderColor: fixed.textOnPrimary,
backgroundColor: "#0099CC",
borderColor: "white",
borderStyle: "solid",
borderWidth: 1,
borderRadius: 50,
@@ -30,7 +28,7 @@ export const MapsButton: FC<Props> = ({ onPress, top, mapSwitch }) => {
return (
<TouchableOpacity onPress={onPress} style={styles}>
<View style={{ flex: 1 }} />
<Ionicons name="close" color={fixed.textOnPrimary} size={30} />
<Ionicons name="close" color="white" size={30} />
<View style={{ flex: 1 }} />
</TouchableOpacity>
);

View File

@@ -1,7 +1,6 @@
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;
@@ -13,7 +12,6 @@ type Props = {
export const UsefulBox: FC<Props> = (props) => {
const { icon, backgroundColor, flex, onPressButton, children } = props;
const { fixed } = useThemeColors();
return (
<TouchableOpacity
style={{
@@ -25,8 +23,8 @@ export const UsefulBox: FC<Props> = (props) => {
}}
onPress={onPressButton}
>
<MaterialCommunityIcons name={icon} color={fixed.textOnPrimary} size={50} />
<Text style={{ color: fixed.textOnPrimary, fontWeight: "bold", fontSize: 16 }}>
<MaterialCommunityIcons name={icon} color="white" size={50} />
<Text style={{ color: "white", fontWeight: "bold", fontSize: 16 }}>
{children}
</Text>
</TouchableOpacity>

View File

@@ -1,5 +1,4 @@
import React, { FC } from "react";
import { useThemeColors } from "@/lib/theme";
import {
Text,
TextStyle,
@@ -16,14 +15,13 @@ 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: fixed.textOnPrimary,
borderColor: "white",
borderWidth: 1,
margin: 10,
borderRadius: 5,
@@ -34,7 +32,7 @@ export const BigButton: FC<Props> = (props) => {
>
<View style={{ flex: 1 }} />
{children}
<Text style={{ fontSize: 25, fontWeight: "bold", color: fixed.textOnPrimary, ...tS }}>
<Text style={{ fontSize: 25, fontWeight: "bold", color: "white", ...tS }}>
{string}
</Text>
<View style={{ flex: 1 }} />

Some files were not shown because too many files have changed in this diff Show More