Compare commits
1 Commits
develop
...
experiment
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c1b4adc310 |
160
WINDOWS_PORT.md
Normal file
160
WINDOWS_PORT.md
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
# JR四国アプリ Windows版 (React Native for Windows) 引き継ぎ資料
|
||||||
|
|
||||||
|
## プロジェクト概要
|
||||||
|
|
||||||
|
JR四国の列車走行位置・運行情報アプリ。Expo Managed Workflow (React Native) で構築。
|
||||||
|
Android/iOS向けに運用中。このタスクは **Windows PC版の追加** を目的とする。
|
||||||
|
|
||||||
|
- **リポジトリ**: `/home/ubuntu/jrshikoku` (Ubuntu開発サーバー) → Windows環境に clone/コピーして作業
|
||||||
|
- **ブランチ**: `experiment/pc-version`(mainブランチ: `master`)
|
||||||
|
- **React Native**: 0.83.2 / **Expo**: ^55.0.8
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## これまでの経緯
|
||||||
|
|
||||||
|
### 試みたこと: Electron + Expo webビルド
|
||||||
|
|
||||||
|
`npx expo export -p web` でWebビルドし、ElectronでラップしてCORSをメインプロセスで回避する方法を試みた。
|
||||||
|
|
||||||
|
**結果**: 動作はしたが根本的な問題が多く断念。
|
||||||
|
|
||||||
|
| 問題 | 原因 |
|
||||||
|
|------|------|
|
||||||
|
| `injectJavaScript` が動かない | Electron `<webview>` はクロスオリジンにJS注入できない |
|
||||||
|
| 列車描画が壊れる | `window.ReactNativeWebView` が未定義でスクリプトがクラッシュ |
|
||||||
|
| 操作感が悪い | WebViewがiframe/Electron webviewタグになり、ネイティブのスクロール感が失われる |
|
||||||
|
| `usage.htm`(利用規約)が毎回出る | `injectJavascript` が動かないため `goBack()` 処理が機能しない |
|
||||||
|
|
||||||
|
**結論**: `react-native-webview` をネイティブとして動かせないWeb系アプローチには限界がある。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 推奨アプローチ: React Native for Windows (RNW)
|
||||||
|
|
||||||
|
### なぜRNWが良いか
|
||||||
|
|
||||||
|
- `react-native-webview` が **WebView2 (Chromium)** として動く → `injectJavaScript` がモバイルと同じ挙動
|
||||||
|
- ネイティブアプリなので CORS 制限がない
|
||||||
|
- スクロール・タッチ感がネイティブ
|
||||||
|
- このアプリの主要機能(走行位置WebView、API通信)がそのまま動く可能性が高い
|
||||||
|
|
||||||
|
### バージョン対応
|
||||||
|
|
||||||
|
RN 0.83.2 に対応する RNW は **0.83.x**。
|
||||||
|
|
||||||
|
```
|
||||||
|
yarn add react-native-windows@0.83.x
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## セットアップ手順 (Windows)
|
||||||
|
|
||||||
|
### 1. 前提環境のインストール
|
||||||
|
|
||||||
|
[Microsoft公式ドキュメント](https://microsoft.github.io/react-native-windows/docs/rnw-dependencies) に従う。
|
||||||
|
|
||||||
|
必要なもの:
|
||||||
|
- **Visual Studio 2022** (Community可)
|
||||||
|
- ワークロード: 「C++ によるデスクトップ開発」「ユニバーサル Windows プラットフォーム開発」
|
||||||
|
- 個別コンポーネント: 「Windows 11 SDK (10.0.22621.0 以上)」「C++ CMake ツール」
|
||||||
|
- **Node.js** LTS
|
||||||
|
- **Git**
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# winget で一括インストール (任意)
|
||||||
|
winget install Microsoft.VisualStudio.2022.Community
|
||||||
|
winget install OpenJS.NodeJS.LTS
|
||||||
|
winget install Git.Git
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. リポジトリをWindowsにクローン
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
git clone <リポジトリURL>
|
||||||
|
cd jrshikoku
|
||||||
|
git checkout experiment/pc-version
|
||||||
|
yarn install
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. RNW の初期化
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
yarn add react-native-windows@0.83.x
|
||||||
|
npx react-native-windows-init --overwrite
|
||||||
|
```
|
||||||
|
|
||||||
|
→ `windows/` ディレクトリが生成される。
|
||||||
|
|
||||||
|
### 4. ビルド & 実行
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
npx react-native run-windows
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 予想される問題と対処
|
||||||
|
|
||||||
|
### Expoモジュールの非対応
|
||||||
|
|
||||||
|
Expo Managed Workflow のモジュールはネイティブコードを自動生成するが、RNW は公式サポート外。
|
||||||
|
以下のモジュールはWindowsで動かない可能性が高い(クラッシュしたらスタブに差し替える):
|
||||||
|
|
||||||
|
| モジュール | 状況 | 対処 |
|
||||||
|
|-----------|------|------|
|
||||||
|
| `expo-notifications` | Windowsプッシュ通知は別実装が必要 | Platform.OS === 'windows' でスキップ |
|
||||||
|
| `expo-felica-reader` | ハードウェア依存、Windows非対応 | スタブに差し替え |
|
||||||
|
| `expo-live-activity` | iOS専用 | すでにPlatform.OS === 'web' ガード済み |
|
||||||
|
| `expo-linear-gradient` | RNW向けは要確認 | react-native-linear-gradient に差し替えも |
|
||||||
|
| `react-native-maps` | Windows未対応 | metro.config.js でスタブを当てる |
|
||||||
|
|
||||||
|
### Platform.OS の値
|
||||||
|
|
||||||
|
RNW では `Platform.OS === 'windows'`(`'web'` でも `'ios'` でもない)。
|
||||||
|
既存の `Platform.OS === 'android'` ガードがある箇所は意図通り動く(Windowsはスキップされる)。
|
||||||
|
|
||||||
|
### `react-native-webview` のCORS
|
||||||
|
|
||||||
|
ネイティブアプリなので基本的にCORSはない。ただし JR四国の `X-Frame-Options` は
|
||||||
|
iframeではなく WebView2 で直接開くため無関係。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## アプリ構成の重要ポイント
|
||||||
|
|
||||||
|
### 走行位置タブ (`components/Apps/WebView.tsx`)
|
||||||
|
|
||||||
|
最重要コンポーネント。`react-native-webview` の WebView に `train.jr-shikoku.co.jp/sp.html` を読み込み、大量の JavaScript (`injectJavascript`) を注入して列車データを描画している。
|
||||||
|
|
||||||
|
- `injectJavascript`: `lib/webViewInjectjavascript.ts` で生成。ユーザー設定・ダークモード等を考慮した巨大スクリプト
|
||||||
|
- `injectJavascriptBeforeContentLoaded`: モックAPI使用時のみ、XHRインターセプター
|
||||||
|
- `window.ReactNativeWebView.postMessage` でアプリ側に列車クリック等のイベントを送る
|
||||||
|
|
||||||
|
### 状態管理
|
||||||
|
|
||||||
|
Context API (stateBox/) で全状態を管理。Redux等は使用していない。
|
||||||
|
|
||||||
|
### API
|
||||||
|
|
||||||
|
バックエンド: `jr-shikoku-backend-api-v1.haruk.in`(外部サーバー)
|
||||||
|
列車走行位置: JR四国公式 `train.jr-shikoku.co.jp`(WebView内で直接アクセス)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Claudeへの指示テンプレート
|
||||||
|
|
||||||
|
```
|
||||||
|
このプロジェクトはExpo React NativeのJR四国アプリ(RN 0.83.2)。
|
||||||
|
experiment/pc-versionブランチで作業中。
|
||||||
|
React Native for Windows (RNW 0.83.x) を使ってWindows PC版を作りたい。
|
||||||
|
|
||||||
|
まず package.json と windows/ ディレクトリの存在を確認し、
|
||||||
|
RNW の初期化が完了しているか確認してから作業を開始してほしい。
|
||||||
|
|
||||||
|
主要な懸念点:
|
||||||
|
1. expo-notifications など Expo 系モジュールが Windows ビルドを壊す可能性
|
||||||
|
2. react-native-maps は Windows 非対応(metro.config.js でスタブを当てる予定)
|
||||||
|
3. Platform.OS === 'windows' の分岐が必要になる箇所を確認すること
|
||||||
|
```
|
||||||
@@ -18,18 +18,4 @@ export default [
|
|||||||
lng: 133.816444,
|
lng: 133.816444,
|
||||||
isSpot: true,
|
isSpot: true,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
Station_JP: ".小歩危展望台",
|
|
||||||
Station_EN: "Koboke Observatory",
|
|
||||||
MyStation: "0",
|
|
||||||
StationNumber: null,
|
|
||||||
DispNum: "3",
|
|
||||||
StationTimeTable: "",
|
|
||||||
StationMap: "https://maps.app.goo.gl/WBMN5R2tk2tusavk7",
|
|
||||||
JrHpUrl: "https://miyoshi-tourism.jp/spot/5438/",
|
|
||||||
jslodApi: "spot",
|
|
||||||
lat: 33.9372609,
|
|
||||||
lng: 133.753258,
|
|
||||||
isSpot: true,
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -22,11 +22,10 @@ type ReloadButton = {
|
|||||||
}
|
}
|
||||||
export const ReloadButton:FC<ReloadButton> = ({ onPress, right }) => {
|
export const ReloadButton:FC<ReloadButton> = ({ onPress, right }) => {
|
||||||
const { fixed } = useThemeColors();
|
const { fixed } = useThemeColors();
|
||||||
const { mapSwitch, LoadError = false, mockApiFeatureEnabled } = useTrainMenu();
|
const { mapSwitch, LoadError = false } = useTrainMenu();
|
||||||
const { top } = useSafeAreaInsets();
|
const { top } = useSafeAreaInsets();
|
||||||
const { moderateScale } = useResponsive();
|
const { moderateScale } = useResponsive();
|
||||||
const buttonSize = moderateScale(50);
|
const buttonSize = moderateScale(50);
|
||||||
const buttonColor = LoadError ? "red" : mockApiFeatureEnabled ? "#7c3aed" : fixed.primary;
|
|
||||||
const styles: stylesType = {
|
const styles: stylesType = {
|
||||||
touch: {
|
touch: {
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
@@ -34,7 +33,7 @@ export const ReloadButton:FC<ReloadButton> = ({ onPress, right }) => {
|
|||||||
right: 10 + right,
|
right: 10 + right,
|
||||||
width: buttonSize,
|
width: buttonSize,
|
||||||
height: buttonSize,
|
height: buttonSize,
|
||||||
backgroundColor: buttonColor,
|
backgroundColor: LoadError ? "red" : fixed.primary,
|
||||||
borderColor: fixed.textOnPrimary,
|
borderColor: fixed.textOnPrimary,
|
||||||
borderStyle: "solid",
|
borderStyle: "solid",
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import { useNotification } from "../../stateBox/useNotifications";
|
|||||||
import { SheetHeaderItem } from "@/components/atom/SheetHeaderItem";
|
import { SheetHeaderItem } from "@/components/atom/SheetHeaderItem";
|
||||||
import { useThemeColors, type ColorThemePref } from "@/lib/theme/useThemeColors";
|
import { useThemeColors, type ColorThemePref } from "@/lib/theme/useThemeColors";
|
||||||
|
|
||||||
const versionCode = "7.0.4"; // Update this version code as needed
|
const versionCode = "7.0.3"; // Update this version code as needed
|
||||||
|
|
||||||
export const SettingTopPage = ({
|
export const SettingTopPage = ({
|
||||||
testNFC,
|
testNFC,
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -14,7 +14,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export { generateXhrInterceptorJs, MockApiConfig, TrainEntry } from './webviewXhrInterceptor';
|
export { generateXhrInterceptorJs, MockApiConfig, TrainEntry } from './webviewXhrInterceptor';
|
||||||
export { PositionMaster, PositionLookup, fetchPositionMasters, fetchMockTrainPositions, buildPosLookup, lookupPos } from './positionMasters';
|
|
||||||
|
|
||||||
// Pre-captured sample train position data from the official site
|
// Pre-captured sample train position data from the official site
|
||||||
import trainJson from './mockData/train.json';
|
import trainJson from './mockData/train.json';
|
||||||
|
|||||||
@@ -1,101 +0,0 @@
|
|||||||
/**
|
|
||||||
* Position Masters – JR Shikoku mock API
|
|
||||||
*
|
|
||||||
* Fetches the position-master table from the mock API server and provides
|
|
||||||
* a lookup helper to convert (PosNum, Line) → Pos text.
|
|
||||||
*
|
|
||||||
* Used by:
|
|
||||||
* - useTrainMenu: fetches on mock-enable, stores in context
|
|
||||||
* - useCurrentTrain: fills Pos when mapping mock TrainEntry → trainDataType
|
|
||||||
* - webviewXhrInterceptor: bakes lookup into injected JS so WebView can
|
|
||||||
* also resolve Pos text client-side
|
|
||||||
*/
|
|
||||||
|
|
||||||
const POSITION_MASTERS_URL =
|
|
||||||
'https://jr-shikoku-backend-mock-api-v1.haruk.in/position-masters';
|
|
||||||
|
|
||||||
const MOCK_TRAIN_POSITIONS_URL =
|
|
||||||
'https://jr-shikoku-backend-mock-api-v1.haruk.in/train-positions/current';
|
|
||||||
|
|
||||||
export interface PositionMaster {
|
|
||||||
pos_num: number;
|
|
||||||
/** "yosan" | "koutoku" | "tokushima" | "dosan" | "uwajima" | "kubokawa" */
|
|
||||||
line: string;
|
|
||||||
/** 表示テキスト e.g. "高松", "高松~栗林" */
|
|
||||||
pos_text: string;
|
|
||||||
pos_type: 'station' | 'between' | 'approaching' | 'yard';
|
|
||||||
display_order: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** key: `${pos_num}:${line}` → pos_text */
|
|
||||||
export type PositionLookup = Map<string, string>;
|
|
||||||
|
|
||||||
/** Module-level cache (lives for the app session, not persisted). */
|
|
||||||
let _cache: PositionMaster[] | null = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch position masters from the remote API.
|
|
||||||
* Results are cached in memory for the session.
|
|
||||||
*/
|
|
||||||
export const fetchPositionMasters = async (): Promise<PositionMaster[]> => {
|
|
||||||
if (_cache) return _cache;
|
|
||||||
const res = await fetch(POSITION_MASTERS_URL);
|
|
||||||
if (!res.ok) throw new Error(`position-masters fetch failed: ${res.status}`);
|
|
||||||
const data: PositionMaster[] = await res.json();
|
|
||||||
_cache = data;
|
|
||||||
return data;
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Clear the in-memory cache (useful for testing / forced refresh). */
|
|
||||||
export const clearPositionMastersCache = () => {
|
|
||||||
_cache = null;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Build a fast Map from the masters array.
|
|
||||||
* When multiple records share the same (pos_num, line) pair (different
|
|
||||||
* display_order), the one with the lower display_order takes priority.
|
|
||||||
*/
|
|
||||||
export const buildPosLookup = (masters: PositionMaster[]): PositionLookup => {
|
|
||||||
const sorted = [...masters].sort((a, b) => a.display_order - b.display_order);
|
|
||||||
const map = new Map<string, string>();
|
|
||||||
for (const m of sorted) {
|
|
||||||
const key = `${m.pos_num}:${m.line}`;
|
|
||||||
if (!map.has(key)) {
|
|
||||||
map.set(key, m.pos_text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return map;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Look up the Pos text for a given (PosNum, Line) pair.
|
|
||||||
* Returns `undefined` when no match is found.
|
|
||||||
*/
|
|
||||||
export const lookupPos = (
|
|
||||||
posNum: number,
|
|
||||||
line: string,
|
|
||||||
lookup: PositionLookup,
|
|
||||||
): string | undefined => lookup.get(`${posNum}:${line}`);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Serialize the lookup as a plain JS object literal suitable for embedding
|
|
||||||
* into an injected JavaScript string.
|
|
||||||
*/
|
|
||||||
export const serializePosLookupForJs = (lookup: PositionLookup): string => {
|
|
||||||
const entries = Array.from(lookup.entries())
|
|
||||||
.map(([k, v]) => `${JSON.stringify(k)}:${JSON.stringify(v)}`)
|
|
||||||
.join(',');
|
|
||||||
return `{${entries}}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch the latest train positions from the mock API server.
|
|
||||||
* Returns an array of TrainEntry objects (GetDateTime sentinel included).
|
|
||||||
* Throws on network error or non-OK response.
|
|
||||||
*/
|
|
||||||
export const fetchMockTrainPositions = async (): Promise<any[]> => {
|
|
||||||
const res = await fetch(MOCK_TRAIN_POSITIONS_URL);
|
|
||||||
if (!res.ok) throw new Error(`mock train-positions fetch failed: ${res.status}`);
|
|
||||||
return res.json();
|
|
||||||
};
|
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
import { PositionMaster, buildPosLookup, serializePosLookupForJs } from './positionMasters';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* WebView XHR Interceptor for JR Shikoku official train position site
|
* WebView XHR Interceptor for JR Shikoku official train position site
|
||||||
*
|
*
|
||||||
@@ -55,13 +53,6 @@ export interface MockApiConfig {
|
|||||||
*/
|
*/
|
||||||
trainPositions: TrainEntry[];
|
trainPositions: TrainEntry[];
|
||||||
|
|
||||||
/**
|
|
||||||
* Position master data fetched from the mock API server.
|
|
||||||
* When provided, the interceptor script will use it to fill Pos text from PosNum
|
|
||||||
* for any train entry whose Pos field is absent.
|
|
||||||
*/
|
|
||||||
positionMasters?: PositionMaster[];
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When true, all /g? static API calls are also intercepted and served from
|
* When true, all /g? static API calls are also intercepted and served from
|
||||||
* the supplied staticData map. When false (default), only the train position
|
* the supplied staticData map. When false (default), only the train position
|
||||||
@@ -95,11 +86,6 @@ export const generateXhrInterceptorJs = (config: MockApiConfig): string => {
|
|||||||
.join(",\n ")
|
.join(",\n ")
|
||||||
: "";
|
: "";
|
||||||
|
|
||||||
// Bake position-masters lookup into the script so Pos can be resolved client-side
|
|
||||||
const posLookupJs = config.positionMasters && config.positionMasters.length > 0
|
|
||||||
? serializePosLookupForJs(buildPosLookup(config.positionMasters))
|
|
||||||
: '{}';
|
|
||||||
|
|
||||||
return `
|
return `
|
||||||
(function() {
|
(function() {
|
||||||
// Double-injection guard: IJBCL and injectedJavaScript may both run this code.
|
// Double-injection guard: IJBCL and injectedJavaScript may both run this code.
|
||||||
@@ -114,20 +100,6 @@ export const generateXhrInterceptorJs = (config: MockApiConfig): string => {
|
|||||||
var _STATIC_MAP = {
|
var _STATIC_MAP = {
|
||||||
${staticEntries}
|
${staticEntries}
|
||||||
};
|
};
|
||||||
// Position masters lookup: key = "posNum:line", value = Pos text
|
|
||||||
var _POS_LOOKUP = ${posLookupJs};
|
|
||||||
|
|
||||||
// Enrich a _MOCK_TRAIN array by filling Pos from _POS_LOOKUP when absent
|
|
||||||
function _enrichTrain(entries) {
|
|
||||||
return entries.map(function(entry) {
|
|
||||||
if (!entry.TrainNum) return entry; // GetDateTime sentinel
|
|
||||||
if (entry.Pos) return entry; // already has text
|
|
||||||
var key = entry.PosNum + ':' + entry.Line;
|
|
||||||
var text = _POS_LOOKUP[key];
|
|
||||||
if (!text) return entry;
|
|
||||||
return Object.assign({}, entry, { Pos: text });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Prototype-patching approach ────────────────────────────────────────────
|
// ── Prototype-patching approach ────────────────────────────────────────────
|
||||||
// Instead of replacing window.XMLHttpRequest with a wrapper class,
|
// Instead of replacing window.XMLHttpRequest with a wrapper class,
|
||||||
@@ -142,7 +114,10 @@ export const generateXhrInterceptorJs = (config: MockApiConfig): string => {
|
|||||||
_proto.open = function(method, url) {
|
_proto.open = function(method, url) {
|
||||||
var qs = (url || '').replace(/^[^?]+\\?/, '');
|
var qs = (url || '').replace(/^[^?]+\\?/, '');
|
||||||
if (qs === 'arg1=train&arg2=train') {
|
if (qs === 'arg1=train&arg2=train') {
|
||||||
this.__jrsMockBody = JSON.stringify(_enrichTrain(_MOCK_TRAIN));
|
this.__jrsMockBody = JSON.stringify(_MOCK_TRAIN);
|
||||||
|
// Still call origOpen so the XHR enters OPENED state.
|
||||||
|
// This allows the page to call setRequestHeader() without errors.
|
||||||
|
// The real request is suppressed in send() by not calling _origSend.
|
||||||
_origOpen.apply(this, arguments);
|
_origOpen.apply(this, arguments);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -212,7 +187,6 @@ export const generateXhrInterceptorJs = (config: MockApiConfig): string => {
|
|||||||
// Called from React Native via injectJavaScript when the playback frame changes.
|
// Called from React Native via injectJavaScript when the playback frame changes.
|
||||||
window.__jrsMockUpdateTrain = function(newData) {
|
window.__jrsMockUpdateTrain = function(newData) {
|
||||||
_MOCK_TRAIN = newData;
|
_MOCK_TRAIN = newData;
|
||||||
// Re-enrich on next open() call (data is enriched lazily in _proto.open)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
window.__jrsMockActive = true;
|
window.__jrsMockActive = true;
|
||||||
|
|||||||
@@ -604,6 +604,7 @@ export const injectJavascriptData = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if(new RegExp(/^4[1-9]\\d\\d[DM]$/).test(列番データ) || new RegExp(/^5[1-7]\\d\\d[DM]$/).test(列番データ) || new RegExp(/^3[2-9]\\d\\d[DM]$/).test(TrainNumber) ){
|
if(new RegExp(/^4[1-9]\\d\\d[DM]$/).test(列番データ) || new RegExp(/^5[1-7]\\d\\d[DM]$/).test(列番データ) || new RegExp(/^3[2-9]\\d\\d[DM]$/).test(TrainNumber) ){
|
||||||
|
flag=true;
|
||||||
isWanman = true;
|
isWanman = true;
|
||||||
}
|
}
|
||||||
if(new RegExp(/^49[0-4]\\dD$/).test(列番データ) || new RegExp(/^9[0-4]\\dD$/).test(列番データ)){
|
if(new RegExp(/^49[0-4]\\dD$/).test(列番データ) || new RegExp(/^9[0-4]\\dD$/).test(列番データ)){
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
{
|
{
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"compile-web-script": "npx tsx scripts/compile-web-script.ts",
|
|
||||||
"start": "expo start",
|
"start": "expo start",
|
||||||
"android": "expo run:android",
|
"android": "expo run:android",
|
||||||
"ios": "expo run:ios",
|
"ios": "expo run:ios",
|
||||||
|
|||||||
@@ -1,144 +0,0 @@
|
|||||||
#!/usr/bin/env tsx
|
|
||||||
/**
|
|
||||||
* compile-web-script.ts
|
|
||||||
*
|
|
||||||
* Usage:
|
|
||||||
* yarn compile-web-script
|
|
||||||
* yarn compile-web-script --no-obf # minifyのみ(難読化なし)
|
|
||||||
* yarn compile-web-script --no-userscript # .js のみ生成(userscript不要)
|
|
||||||
*
|
|
||||||
* Options (環境変数でも上書き可):
|
|
||||||
* UI=tokyo|default (default: tokyo)
|
|
||||||
* DARK=true|false (default: false)
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { execSync } from 'child_process';
|
|
||||||
import fs from 'fs';
|
|
||||||
import path from 'path';
|
|
||||||
import { injectJavascriptData } from '../lib/webViewInjectjavascript';
|
|
||||||
|
|
||||||
// ---- オプション解析 ----
|
|
||||||
const args = process.argv.slice(2);
|
|
||||||
const noObf = args.includes('--no-obf');
|
|
||||||
const noUserscript = args.includes('--no-userscript');
|
|
||||||
|
|
||||||
const options = {
|
|
||||||
mapSwitch: 'false',
|
|
||||||
iconSetting: 'true',
|
|
||||||
stationMenu: 'false',
|
|
||||||
trainMenu: 'false',
|
|
||||||
uiSetting: (process.env.UI ?? 'tokyo') as string,
|
|
||||||
useUnyohub: 'false',
|
|
||||||
useElesite: 'false',
|
|
||||||
isDark: (process.env.DARK ?? 'false') === 'true',
|
|
||||||
backendApiBaseUrl:'https://jr-shikoku-backend-api-v1.haruk.in',
|
|
||||||
mockApiConfig: null,
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
// ---- 出力ディレクトリ ----
|
|
||||||
const outDir = path.resolve(__dirname, '../docs/generated');
|
|
||||||
fs.mkdirSync(outDir, { recursive: true });
|
|
||||||
|
|
||||||
// const baseJs = path.join(outDir, 'webInjectJavascript.parsed.current.js'); // 中間: ベタJS
|
|
||||||
// const minJs = path.join(outDir, 'webInjectJavascript.parsed.current.min.js'); // 中間: minify済み
|
|
||||||
// const obfJs = path.join(outDir, 'webInjectJavascript.parsed.current.obf.js'); // 中間: 難読化済みJS
|
|
||||||
// const baseUser = path.join(outDir, 'webInjectJavascript.parsed.current.user.js'); // 中間: ベタuserscript
|
|
||||||
const obfUser = path.join(outDir, 'webInjectJavascript.parsed.current.obf.user.js');
|
|
||||||
|
|
||||||
// 中間ファイルを出力したい場合は上の const を有効化して各ステップの writeFileSync コメントを外す
|
|
||||||
const baseJs = '/tmp/webInjectJavascript.base.js';
|
|
||||||
const minJs = '/tmp/webInjectJavascript.min.js';
|
|
||||||
const obfJs = '/tmp/webInjectJavascript.obf.js';
|
|
||||||
|
|
||||||
// ---- userscript ヘッダ ----
|
|
||||||
const userscriptHeader = `\
|
|
||||||
// ==UserScript==
|
|
||||||
// @name JR Shikoku WebInject
|
|
||||||
// @namespace jrshikoku
|
|
||||||
// @version 1.0.0
|
|
||||||
// @description Generated inject script for train.jr-shikoku.co.jp
|
|
||||||
// @match https://train.jr-shikoku.co.jp/*
|
|
||||||
// @run-at document-end
|
|
||||||
// @grant none
|
|
||||||
// ==/UserScript==
|
|
||||||
(function(){
|
|
||||||
`;
|
|
||||||
const userscriptFooter = `\n})();\n`;
|
|
||||||
|
|
||||||
function wrapUserscript(src: string): string {
|
|
||||||
return userscriptHeader + src + userscriptFooter;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Step 1: ベタJS生成 ----
|
|
||||||
console.log('[1/3] Generating base JS...');
|
|
||||||
const shim = `\
|
|
||||||
/*
|
|
||||||
Generated from lib/webViewInjectjavascript.ts
|
|
||||||
Options: ${JSON.stringify(options)}
|
|
||||||
Date: ${new Date().toISOString()}
|
|
||||||
*/
|
|
||||||
if (!window.ReactNativeWebView) {
|
|
||||||
window.ReactNativeWebView = {
|
|
||||||
postMessage: (msg) => console.log('[ReactNativeWebView.postMessage]', msg),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
const raw = injectJavascriptData(options);
|
|
||||||
const baseContent = `${shim}\n${raw}`;
|
|
||||||
fs.writeFileSync(baseJs, baseContent, 'utf8');
|
|
||||||
// fs.writeFileSync(baseJs, baseContent, 'utf8'); // 中間ファイルとして保存する場合
|
|
||||||
// if (!noUserscript) {
|
|
||||||
// fs.writeFileSync(baseUser, wrapUserscript(baseContent), 'utf8'); // ベタuserscriptを保存する場合
|
|
||||||
// }
|
|
||||||
console.log(` -> base JS generated (${Math.round(Buffer.byteLength(baseContent) / 1024)}KB)`);
|
|
||||||
|
|
||||||
// ---- Step 2: minify (terser) ----
|
|
||||||
console.log('[2/3] Minifying with terser...');
|
|
||||||
const terserCmd = [
|
|
||||||
'npx', 'terser', JSON.stringify(baseJs),
|
|
||||||
'--compress', 'passes=3,drop_console=false,pure_getters=true',
|
|
||||||
'--mangle', 'toplevel=true',
|
|
||||||
'--output', JSON.stringify(minJs),
|
|
||||||
].join(' ');
|
|
||||||
execSync(terserCmd, { stdio: 'inherit' });
|
|
||||||
// fs.copyFileSync(minJs, path.join(outDir, 'webInjectJavascript.parsed.current.min.js')); // 中間ファイルとして保存する場合
|
|
||||||
console.log(` -> minified (${Math.round(fs.statSync(minJs).size / 1024)}KB)`);
|
|
||||||
|
|
||||||
if (noObf) {
|
|
||||||
console.log('[3/3] Skipped obfuscation (--no-obf)');
|
|
||||||
console.log('Done.');
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Step 3: 難読化 (javascript-obfuscator) ----
|
|
||||||
console.log('[3/3] Obfuscating with javascript-obfuscator...');
|
|
||||||
const obfCmd = [
|
|
||||||
'npx', 'javascript-obfuscator', JSON.stringify(minJs),
|
|
||||||
'--output', JSON.stringify(obfJs),
|
|
||||||
'--compact', 'true',
|
|
||||||
'--control-flow-flattening', 'false',
|
|
||||||
'--dead-code-injection', 'false',
|
|
||||||
'--string-array', 'true',
|
|
||||||
'--string-array-encoding', 'base64',
|
|
||||||
'--string-array-threshold', '0.75',
|
|
||||||
'--string-array-rotate', 'true',
|
|
||||||
'--string-array-shuffle', 'true',
|
|
||||||
'--string-array-calls-transform', 'true',
|
|
||||||
'--string-array-index-shift', 'true',
|
|
||||||
'--string-array-wrappers-count', '2',
|
|
||||||
'--string-array-wrappers-type', 'function',
|
|
||||||
'--string-array-wrappers-chained-calls', 'true',
|
|
||||||
'--split-strings', 'true',
|
|
||||||
'--split-strings-chunk-length', '8',
|
|
||||||
'--identifier-names-generator', 'hexadecimal',
|
|
||||||
'--rename-globals', 'false',
|
|
||||||
'--self-defending', 'false',
|
|
||||||
].join(' ');
|
|
||||||
execSync(obfCmd, { stdio: 'inherit' });
|
|
||||||
// fs.copyFileSync(obfJs, path.join(outDir, 'webInjectJavascript.parsed.current.obf.js')); // 難読化JSを保存する場合
|
|
||||||
|
|
||||||
const obfContent = fs.readFileSync(obfJs, 'utf8');
|
|
||||||
fs.writeFileSync(obfUser, wrapUserscript(obfContent), 'utf8');
|
|
||||||
|
|
||||||
console.log('\nDone. Generated:');
|
|
||||||
console.log(` ${path.relative(process.cwd(), obfUser)} (${Math.round(fs.statSync(obfUser).size / 1024)}KB)`);
|
|
||||||
@@ -16,7 +16,6 @@ import { checkDuplicateTrainData } from "@/lib/checkDuplicateTrainData";
|
|||||||
import { getStationID } from "@/lib/eachTrainInfoCoreLib/getStationData";
|
import { getStationID } from "@/lib/eachTrainInfoCoreLib/getStationData";
|
||||||
import { trainDataType } from "@/lib/trainPositionTextArray";
|
import { trainDataType } from "@/lib/trainPositionTextArray";
|
||||||
import { MOCK_TRAIN_POSITIONS } from "@/lib/mockApi";
|
import { MOCK_TRAIN_POSITIONS } from "@/lib/mockApi";
|
||||||
import { fetchMockTrainPositions } from "@/lib/mockApi/positionMasters";
|
|
||||||
import WebView from "react-native-webview";
|
import WebView from "react-native-webview";
|
||||||
import { StationProps } from "@/lib/CommonTypes";
|
import { StationProps } from "@/lib/CommonTypes";
|
||||||
type loading = "loading" | "success" | "error";
|
type loading = "loading" | "success" | "error";
|
||||||
@@ -78,7 +77,7 @@ export const CurrentTrainProvider: FC<props> = ({ children }) => {
|
|||||||
const [currentTrainLoading, setCurrentTrainLoading] =
|
const [currentTrainLoading, setCurrentTrainLoading] =
|
||||||
useState<loading>("loading");
|
useState<loading>("loading");
|
||||||
|
|
||||||
const { mockApiFeatureEnabled, mockTrainPositions, addTrainSnapshot, lookupPosText, recorderState } = useTrainMenu();
|
const { mockApiFeatureEnabled, mockTrainPositions, addTrainSnapshot } = useTrainMenu();
|
||||||
|
|
||||||
const { getInjectJavascriptAddress, stationList, originalStationList } =
|
const { getInjectJavascriptAddress, stationList, originalStationList } =
|
||||||
useStationList();
|
useStationList();
|
||||||
@@ -262,8 +261,8 @@ export const CurrentTrainProvider: FC<props> = ({ children }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
const getCurrentTrain = () => {
|
const getCurrentTrain = () => {
|
||||||
// 再生中: 録画データをそのまま使う(ポーリング不要)
|
// モックAPI機能が有効な場合はモックデータを使用
|
||||||
if (recorderState === 'playing') {
|
if (mockApiFeatureEnabled) {
|
||||||
const source = mockTrainPositions ?? MOCK_TRAIN_POSITIONS;
|
const source = mockTrainPositions ?? MOCK_TRAIN_POSITIONS;
|
||||||
const mapped = source
|
const mapped = source
|
||||||
.filter((x): x is import("@/lib/mockApi/webviewXhrInterceptor").TrainEntry =>
|
.filter((x): x is import("@/lib/mockApi/webviewXhrInterceptor").TrainEntry =>
|
||||||
@@ -272,8 +271,8 @@ export const CurrentTrainProvider: FC<props> = ({ children }) => {
|
|||||||
.map((x) => ({
|
.map((x) => ({
|
||||||
Index: x.Index,
|
Index: x.Index,
|
||||||
num: x.TrainNum,
|
num: x.TrainNum,
|
||||||
delay: x.delay as "入線" | number,
|
delay: x.delay,
|
||||||
Pos: x.Pos || lookupPosText(x.PosNum, x.Line) || '',
|
Pos: x.Pos,
|
||||||
PosNum: x.PosNum,
|
PosNum: x.PosNum,
|
||||||
Direction: x.Direction,
|
Direction: x.Direction,
|
||||||
Type: x.Type,
|
Type: x.Type,
|
||||||
@@ -283,33 +282,6 @@ export const CurrentTrainProvider: FC<props> = ({ children }) => {
|
|||||||
setCurrentTrainLoading("success");
|
setCurrentTrainLoading("success");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// モックON(通常): モックAPIサーバーをポーリング
|
|
||||||
if (mockApiFeatureEnabled) {
|
|
||||||
const controller = new AbortController();
|
|
||||||
const timeoutId = setTimeout(() => controller.abort(), 8000);
|
|
||||||
fetchMockTrainPositions()
|
|
||||||
.then((data) => {
|
|
||||||
const entries = data.filter((x: any) => "TrainNum" in x);
|
|
||||||
const mapped = entries.map((x: any) => ({
|
|
||||||
Index: x.Index as number,
|
|
||||||
num: x.TrainNum as string,
|
|
||||||
delay: x.delay as "入線" | number,
|
|
||||||
Pos: (x.Pos || lookupPosText(x.PosNum, x.Line) || '') as string,
|
|
||||||
PosNum: x.PosNum as number,
|
|
||||||
Direction: x.Direction as number,
|
|
||||||
Type: x.Type as string,
|
|
||||||
Line: x.Line as string,
|
|
||||||
}));
|
|
||||||
setCurrentTrain(mapped);
|
|
||||||
setCurrentTrainLoading("success");
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
setCurrentTrainLoading("error");
|
|
||||||
})
|
|
||||||
.finally(() => clearTimeout(timeoutId));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
const timeoutId = setTimeout(() => controller.abort(), 8000);
|
const timeoutId = setTimeout(() => controller.abort(), 8000);
|
||||||
fetch("https://n8n.haruk.in/webhook/c501550c-7d1b-4e50-927b-4429fe18931a", { signal: controller.signal })
|
fetch("https://n8n.haruk.in/webhook/c501550c-7d1b-4e50-927b-4429fe18931a", { signal: controller.signal })
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import React, {
|
|||||||
useState,
|
useState,
|
||||||
useEffect,
|
useEffect,
|
||||||
useRef,
|
useRef,
|
||||||
useCallback,
|
|
||||||
FC,
|
FC,
|
||||||
} from "react";
|
} from "react";
|
||||||
|
|
||||||
@@ -15,14 +14,6 @@ import { getStationList2 } from "../lib/getStationList";
|
|||||||
import { injectJavascriptData, generateBeforeContentLoadedScript } from "../lib/webViewInjectjavascript";
|
import { injectJavascriptData, generateBeforeContentLoadedScript } from "../lib/webViewInjectjavascript";
|
||||||
import { MockApiConfig, TrainEntry } from "../lib/mockApi/webviewXhrInterceptor";
|
import { MockApiConfig, TrainEntry } from "../lib/mockApi/webviewXhrInterceptor";
|
||||||
import { MOCK_TRAIN_POSITIONS } from "../lib/mockApi";
|
import { MOCK_TRAIN_POSITIONS } from "../lib/mockApi";
|
||||||
import {
|
|
||||||
PositionMaster,
|
|
||||||
PositionLookup,
|
|
||||||
fetchPositionMasters,
|
|
||||||
fetchMockTrainPositions,
|
|
||||||
buildPosLookup,
|
|
||||||
lookupPos,
|
|
||||||
} from "../lib/mockApi/positionMasters";
|
|
||||||
import {
|
import {
|
||||||
TrainRecording,
|
TrainRecording,
|
||||||
RecordingMeta,
|
RecordingMeta,
|
||||||
@@ -81,10 +72,6 @@ const initialState = {
|
|||||||
/** モックAPI検証機能が設定で有効化されているか(admin専用) */
|
/** モックAPI検証機能が設定で有効化されているか(admin専用) */
|
||||||
mockApiFeatureEnabled: false,
|
mockApiFeatureEnabled: false,
|
||||||
setMockApiFeatureEnabled: (e: boolean) => {},
|
setMockApiFeatureEnabled: (e: boolean) => {},
|
||||||
/** 位置マスターデータ(mock API server から取得) */
|
|
||||||
positionMasters: [] as PositionMaster[],
|
|
||||||
/** PosNum + Line → Pos テキスト変換 */
|
|
||||||
lookupPosText: (_posNum: number, _line: string): string | undefined => undefined,
|
|
||||||
// --- 録画・再生 ---
|
// --- 録画・再生 ---
|
||||||
recorderState: 'idle' as 'idle' | 'recording' | 'playing',
|
recorderState: 'idle' as 'idle' | 'recording' | 'playing',
|
||||||
recordingSnapshotCount: 0,
|
recordingSnapshotCount: 0,
|
||||||
@@ -177,9 +164,6 @@ export const TrainMenuProvider: FC<props> = ({ children }) => {
|
|||||||
|
|
||||||
// モックAPI設定
|
// モックAPI設定
|
||||||
const [mockTrainPositions, setMockTrainPositions] = useState<TrainEntry[] | null>(null);
|
const [mockTrainPositions, setMockTrainPositions] = useState<TrainEntry[] | null>(null);
|
||||||
// 位置マスター(mock API server から取得・キャッシュ)
|
|
||||||
const [positionMasters, setPositionMasters] = useState<PositionMaster[]>([]);
|
|
||||||
const [posLookup, setPosLookup] = useState<PositionLookup>(new Map());
|
|
||||||
// admin専用: モックAPI検証機能の有効化(永続化)
|
// admin専用: モックAPI検証機能の有効化(永続化)
|
||||||
const [mockApiFeatureEnabled, setMockApiFeatureEnabledState] = useState(false);
|
const [mockApiFeatureEnabled, setMockApiFeatureEnabledState] = useState(false);
|
||||||
|
|
||||||
@@ -188,15 +172,6 @@ export const TrainMenuProvider: FC<props> = ({ children }) => {
|
|||||||
AS.setItem(STORAGE_KEYS.MOCK_API_FEATURE_ENABLED, value.toString());
|
AS.setItem(STORAGE_KEYS.MOCK_API_FEATURE_ENABLED, value.toString());
|
||||||
// 機能をオフにしたらデータをリセット
|
// 機能をオフにしたらデータをリセット
|
||||||
if (!value) setMockTrainPositions(MOCK_TRAIN_POSITIONS);
|
if (!value) setMockTrainPositions(MOCK_TRAIN_POSITIONS);
|
||||||
// 機能をオンにしたら位置マスターを取得(未取得の場合)
|
|
||||||
if (value && positionMasters.length === 0) {
|
|
||||||
fetchPositionMasters()
|
|
||||||
.then((masters) => {
|
|
||||||
setPositionMasters(masters);
|
|
||||||
setPosLookup(buildPosLookup(masters));
|
|
||||||
})
|
|
||||||
.catch(() => {});
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- 録画・再生 ---
|
// --- 録画・再生 ---
|
||||||
@@ -300,13 +275,9 @@ export const TrainMenuProvider: FC<props> = ({ children }) => {
|
|||||||
setRecordingList(list);
|
setRecordingList(list);
|
||||||
};
|
};
|
||||||
|
|
||||||
/** PosNum + Line → Pos テキスト(位置マスターから) */
|
|
||||||
const lookupPosText = (posNum: number, line: string): string | undefined =>
|
|
||||||
lookupPos(posNum, line, posLookup);
|
|
||||||
|
|
||||||
const mockApiConfig: MockApiConfig | null =
|
const mockApiConfig: MockApiConfig | null =
|
||||||
mockApiFeatureEnabled && mockTrainPositions
|
mockApiFeatureEnabled && mockTrainPositions
|
||||||
? { trainPositions: mockTrainPositions, positionMasters }
|
? { trainPositions: mockTrainPositions }
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
//地図表示テキスト
|
//地図表示テキスト
|
||||||
@@ -373,46 +344,16 @@ export const TrainMenuProvider: FC<props> = ({ children }) => {
|
|||||||
});
|
});
|
||||||
//モックAPI検証機能スイッチ(admin専用・再起動不要)
|
//モックAPI検証機能スイッチ(admin専用・再起動不要)
|
||||||
AS.getItem(STORAGE_KEYS.MOCK_API_FEATURE_ENABLED).then((value) => {
|
AS.getItem(STORAGE_KEYS.MOCK_API_FEATURE_ENABLED).then((value) => {
|
||||||
const enabled = value === "true" || value === true;
|
setMockApiFeatureEnabledState(value === "true" || value === true);
|
||||||
setMockApiFeatureEnabledState(enabled);
|
|
||||||
// 起動時に既に有効なら位置マスターを取得
|
|
||||||
if (enabled) {
|
|
||||||
fetchPositionMasters()
|
|
||||||
.then((masters) => {
|
|
||||||
setPositionMasters(masters);
|
|
||||||
setPosLookup(buildPosLookup(masters));
|
|
||||||
})
|
|
||||||
.catch(() => {});
|
|
||||||
}
|
|
||||||
}).catch(() => {});
|
}).catch(() => {});
|
||||||
// バックエンドAPIベースURL(環境設定から読み込み)
|
// バックエンドAPIベースURL(環境設定から読み込み)
|
||||||
AS.getItem(STORAGE_KEYS.JR_DATA_SYSTEM_ENV).then((value) => {
|
AS.getItem(STORAGE_KEYS.JR_DATA_SYSTEM_ENV).then((value) => {
|
||||||
setBackendApiBaseUrl(getBackendApiBaseUrl(value));
|
setBackendApiBaseUrl(getBackendApiBaseUrl(value));
|
||||||
}).catch(() => {});
|
}).catch(() => {});
|
||||||
// 静的サンプルデータで初期化(モックAPIが応答するまでのフォールバック)
|
// サンプルデータで初期化
|
||||||
setMockTrainPositions(MOCK_TRAIN_POSITIONS);
|
setMockTrainPositions(MOCK_TRAIN_POSITIONS);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// モックAPIポーリング: モックON かつ 録画/再生中でない場合に15秒ごとに更新
|
|
||||||
const fetchAndSetMockPositions = useCallback(() => {
|
|
||||||
if (!mockApiFeatureEnabled || recorderState !== 'idle') return;
|
|
||||||
fetchMockTrainPositions()
|
|
||||||
.then((data) => {
|
|
||||||
const entries = data.filter((x: any) => "TrainNum" in x) as TrainEntry[];
|
|
||||||
setMockTrainPositions(entries);
|
|
||||||
})
|
|
||||||
.catch(() => {});
|
|
||||||
}, [mockApiFeatureEnabled, recorderState]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!mockApiFeatureEnabled || recorderState !== 'idle') return;
|
|
||||||
// 即時取得
|
|
||||||
fetchAndSetMockPositions();
|
|
||||||
// 15秒ごとにポーリング
|
|
||||||
const timer = setInterval(fetchAndSetMockPositions, 15000);
|
|
||||||
return () => clearInterval(timer);
|
|
||||||
}, [mockApiFeatureEnabled, recorderState]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TrainMenuContext.Provider
|
<TrainMenuContext.Provider
|
||||||
value={{
|
value={{
|
||||||
@@ -443,8 +384,6 @@ export const TrainMenuProvider: FC<props> = ({ children }) => {
|
|||||||
setMockTrainPositions,
|
setMockTrainPositions,
|
||||||
mockApiFeatureEnabled,
|
mockApiFeatureEnabled,
|
||||||
setMockApiFeatureEnabled,
|
setMockApiFeatureEnabled,
|
||||||
positionMasters,
|
|
||||||
lookupPosText,
|
|
||||||
recorderState,
|
recorderState,
|
||||||
recordingSnapshotCount,
|
recordingSnapshotCount,
|
||||||
recordingList,
|
recordingList,
|
||||||
|
|||||||
Reference in New Issue
Block a user