634 lines
22 KiB
TypeScript
634 lines
22 KiB
TypeScript
import React, {
|
|
createContext,
|
|
useContext,
|
|
useState,
|
|
useEffect,
|
|
useRef,
|
|
useCallback,
|
|
FC,
|
|
} from "react";
|
|
import { Platform } from "react-native";
|
|
import * as DocumentPicker from "expo-document-picker";
|
|
import { File, Paths } from "expo-file-system";
|
|
import Share from "react-native-share";
|
|
|
|
import { ASCore } from "../storageControl";
|
|
import { AS } from "../storageControl";
|
|
|
|
import { getStationList2 } from "../lib/getStationList";
|
|
import { injectJavascriptData, generateBeforeContentLoadedScript } from "../lib/webViewInjectjavascript";
|
|
import { MockApiConfig, TrainEntry } from "../lib/mockApi/webviewXhrInterceptor";
|
|
import { MOCK_TRAIN_POSITIONS } from "../lib/mockApi";
|
|
import {
|
|
PositionMaster,
|
|
PositionLookup,
|
|
fetchPositionMasters,
|
|
fetchMockTrainPositions,
|
|
buildPosLookup,
|
|
lookupPos,
|
|
} from "../lib/mockApi/positionMasters";
|
|
import {
|
|
TrainRecording,
|
|
RecordingMeta,
|
|
RecordingImportResult,
|
|
saveRecording,
|
|
loadRecordingList,
|
|
loadRecordingById,
|
|
deleteRecordingById,
|
|
migrateOldRecording,
|
|
generateRecordingId,
|
|
buildRecordingExportText as buildRecordingExportTextCore,
|
|
buildAllRecordingsExportText as buildAllRecordingsExportTextCore,
|
|
importRecordingsFromText as importRecordingsFromTextCore,
|
|
} from "../lib/mockApi/trainRecorder";
|
|
|
|
import { useNotification } from "../stateBox/useNotifications";
|
|
import { useThemeColors } from "@/lib/theme";
|
|
import { STORAGE_KEYS } from "@/constants/storage";
|
|
import {
|
|
normalizeIconDisplayMode,
|
|
type IconDisplayMode,
|
|
} from "@/lib/iconDisplayMode";
|
|
import {
|
|
getBackendApiBaseUrl,
|
|
getDiagramTodayUrl,
|
|
BACKEND_API_BASE_URLS,
|
|
} from "@/lib/jrDataSystemEnvironment";
|
|
|
|
const initialState = {
|
|
selectedLine: undefined,
|
|
setSelectedLine: (e) => {},
|
|
mapsStationData: undefined,
|
|
setMapsStationData: (e) => {},
|
|
iconSetting: undefined,
|
|
setIconSetting: (e) => {},
|
|
mapSwitch: undefined,
|
|
setMapSwitch: (e) => {},
|
|
stationMenu: undefined,
|
|
setStationMenu: (e) => {},
|
|
uiSetting: undefined,
|
|
setUiSetting: (e) => {},
|
|
LoadError: false,
|
|
setLoadError: (e) => {},
|
|
trainInfo: {
|
|
trainNum: undefined,
|
|
limited: undefined,
|
|
trainData: undefined,
|
|
},
|
|
setTrainInfo: (e) => {},
|
|
trainMenu: "true",
|
|
setTrainMenu: (e) => {},
|
|
updatePermission: false,
|
|
setUpdatePermission: (e) => {},
|
|
/** バックエンドが返したユーザーロール */
|
|
userPermissionRole: "",
|
|
/** crew/administrator向け音声機能の表示・利用権限 */
|
|
restrictedSoundPermission: false,
|
|
/** 各情報ソースの利用権限 */
|
|
dataSourcePermission: { unyohub: false, elesite: false } as {
|
|
unyohub: boolean;
|
|
elesite: boolean;
|
|
},
|
|
injectJavascript: "",
|
|
/** injectedJavaScriptBeforeContentLoaded用(XHRインターセプター) */
|
|
injectJavascriptBeforeContentLoaded: "",
|
|
/** WebView内の公式サイトAPIに流し込むモック列車位置データ */
|
|
mockTrainPositions: null as TrainEntry[] | null,
|
|
setMockTrainPositions: (e: TrainEntry[] | null) => {},
|
|
/** モックAPI検証機能が設定で有効化されているか(admin専用) */
|
|
mockApiFeatureEnabled: false,
|
|
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',
|
|
recordingSnapshotCount: 0,
|
|
/** 録画一覧(メタ情報) */
|
|
recordingList: [] as RecordingMeta[],
|
|
/** 再生中のフル録画データ */
|
|
activeRecording: null as TrainRecording | null,
|
|
/** 現在再生中のスナップショットインデックス */
|
|
playbackIndex: 0,
|
|
/** 再生中スナップショットに対応する基準時刻(通常時は null) */
|
|
playbackCurrentTimeIso: null as string | null,
|
|
/** 再生一時停止中か */
|
|
playbackPaused: false,
|
|
startRecording: () => {},
|
|
stopRecording: (): Promise<void> => Promise.resolve(),
|
|
startPlayback: (_id: string): Promise<void> => Promise.resolve(),
|
|
stopPlayback: () => {},
|
|
pausePlayback: () => {},
|
|
resumePlayback: () => {},
|
|
/** 指定インデックスに直接ジャンプ(シーク) */
|
|
seekToSnapshot: (_index: number) => {},
|
|
addTrainSnapshot: (_trains: TrainEntry[]) => {},
|
|
deleteRecording: (_id: string): Promise<void> => Promise.resolve(),
|
|
exportRecordingFile: (_id: string): Promise<void> => Promise.resolve(),
|
|
exportAllRecordingsFile: (): Promise<void> => Promise.resolve(),
|
|
importRecordingFile: (): Promise<RecordingImportResult | null> => Promise.resolve(null),
|
|
importRecordingsFromText: (_content: string): Promise<RecordingImportResult> =>
|
|
Promise.resolve({ importedCount: 0, overwrittenCount: 0 }),
|
|
};
|
|
|
|
const TrainMenuContext = createContext(initialState);
|
|
|
|
export const useTrainMenu = () => {
|
|
return useContext(TrainMenuContext);
|
|
};
|
|
type props = { children: React.ReactNode };
|
|
export const TrainMenuProvider: FC<props> = ({ children }) => {
|
|
const { expoPushToken } = useNotification();
|
|
const [selectedLine, setSelectedLine] = useState(undefined);
|
|
const [mapsStationData, setMapsStationData] = useState(undefined);
|
|
useEffect(() => {
|
|
getStationList2().then(setMapsStationData);
|
|
}, []);
|
|
type boolType = "true" | "false" | undefined;
|
|
//画面表示関連
|
|
const [iconSetting, setIconSetting] = useState<IconDisplayMode | undefined>(
|
|
undefined,
|
|
);
|
|
const [mapSwitch, setMapSwitch] = useState<boolType>(undefined);
|
|
const [stationMenu, setStationMenu] = useState<boolType>(undefined);
|
|
const [LoadError, setLoadError] = useState(false);
|
|
|
|
// バックエンドAPIベースURL(環境設定から読み込み)
|
|
const [backendApiBaseUrl, setBackendApiBaseUrl] = useState<string>(
|
|
BACKEND_API_BASE_URLS.production,
|
|
);
|
|
const [diagramTodayUrl, setDiagramTodayUrl] = useState<string>(
|
|
"https://jr-shikoku-api-data-storage.haruk.in/tmp/diagram-today.json",
|
|
);
|
|
|
|
//更新権限所有確認・情報ソース別利用権限(将来ロールが増えたらここに足す)
|
|
const [updatePermission, setUpdatePermission] = useState(false);
|
|
const [userPermissionRole, setUserPermissionRole] = useState("");
|
|
const [restrictedSoundPermission, setRestrictedSoundPermission] =
|
|
useState(false);
|
|
const [dataSourcePermission, setDataSourcePermission] = useState<{
|
|
unyohub: boolean;
|
|
elesite: boolean;
|
|
}>({ unyohub: false, elesite: false });
|
|
useEffect(() => {
|
|
if (!expoPushToken) {
|
|
setUserPermissionRole("");
|
|
setUpdatePermission(false);
|
|
setRestrictedSoundPermission(false);
|
|
setDataSourcePermission({ unyohub: false, elesite: false });
|
|
return;
|
|
}
|
|
|
|
setUserPermissionRole("");
|
|
setUpdatePermission(false);
|
|
setRestrictedSoundPermission(false);
|
|
setDataSourcePermission({ unyohub: false, elesite: false });
|
|
|
|
const permissionController = new AbortController();
|
|
fetch(
|
|
`${backendApiBaseUrl}/check-permission?user_id=${expoPushToken}`,
|
|
{ signal: permissionController.signal },
|
|
)
|
|
.then((res) => res.json())
|
|
.then((res) => {
|
|
if (permissionController.signal.aborted) return;
|
|
const role: string = res.permission ?? "";
|
|
const normalizedRole = role.trim().toLowerCase();
|
|
const isAdministrator = normalizedRole === "administrator";
|
|
setUserPermissionRole(normalizedRole);
|
|
setUpdatePermission(isAdministrator);
|
|
setRestrictedSoundPermission(
|
|
normalizedRole === "crew" || isAdministrator,
|
|
);
|
|
setDataSourcePermission({
|
|
unyohub: isAdministrator || role === "unyoHubEditor",
|
|
elesite: isAdministrator || role === "eleSiteEditor",
|
|
});
|
|
})
|
|
.catch(() => {
|
|
if (permissionController.signal.aborted) return;
|
|
setUserPermissionRole("");
|
|
setUpdatePermission(false);
|
|
setRestrictedSoundPermission(false);
|
|
setDataSourcePermission({ unyohub: false, elesite: false });
|
|
});
|
|
|
|
return () => permissionController.abort();
|
|
}, [expoPushToken, backendApiBaseUrl]);
|
|
|
|
//列車情報表示関連
|
|
const [trainInfo, setTrainInfo] = useState({
|
|
trainNum: undefined,
|
|
limited: undefined,
|
|
trainData: undefined,
|
|
});
|
|
|
|
//駅情報画面用
|
|
const [trainMenu, setTrainMenu] = useState("true");
|
|
|
|
//GUIデザインベース
|
|
const [uiSetting, setUiSetting] = useState("tokyo");
|
|
|
|
// 鉄道運用Hub使用設定
|
|
const [useUnyohubSetting, setUseUnyohubSetting] = useState("false");
|
|
|
|
// えれサイト使用設定
|
|
const [useEleSiteSetting, setUseEleSiteSetting] = useState("false");
|
|
|
|
// モックAPI設定
|
|
const [mockTrainPositions, setMockTrainPositions] = useState<TrainEntry[] | null>(null);
|
|
// 位置マスター(mock API server から取得・キャッシュ)
|
|
const [positionMasters, setPositionMasters] = useState<PositionMaster[]>([]);
|
|
const [posLookup, setPosLookup] = useState<PositionLookup>(new Map());
|
|
// admin専用: モックAPI検証機能の有効化(永続化)
|
|
const [mockApiFeatureEnabled, setMockApiFeatureEnabledState] = useState(false);
|
|
|
|
const setMockApiFeatureEnabled = (value: boolean) => {
|
|
setMockApiFeatureEnabledState(value);
|
|
AS.setItem(STORAGE_KEYS.MOCK_API_FEATURE_ENABLED, value.toString());
|
|
// 機能をオフにしたらデータをリセット
|
|
if (!value) setMockTrainPositions(MOCK_TRAIN_POSITIONS);
|
|
// 機能をオンにしたら位置マスターを取得(未取得の場合)
|
|
if (value && positionMasters.length === 0) {
|
|
fetchPositionMasters()
|
|
.then((masters) => {
|
|
setPositionMasters(masters);
|
|
setPosLookup(buildPosLookup(masters));
|
|
})
|
|
.catch(() => {});
|
|
}
|
|
};
|
|
|
|
// --- 録画・再生 ---
|
|
type RecorderState = 'idle' | 'recording' | 'playing';
|
|
const [recorderState, setRecorderState] = useState<RecorderState>('idle');
|
|
const [recordingSnapshotCount, setRecordingSnapshotCount] = useState(0);
|
|
const [recordingList, setRecordingList] = useState<RecordingMeta[]>([]);
|
|
const [activeRecording, setActiveRecording] = useState<TrainRecording | null>(null);
|
|
const [playbackIndex, setPlaybackIndex] = useState(0);
|
|
const playbackCurrentTimeIso =
|
|
recorderState === 'playing' && activeRecording && activeRecording.snapshots.length > 0
|
|
? new Date(
|
|
new Date(activeRecording.recordedAt).getTime() +
|
|
(activeRecording.snapshots[playbackIndex]?.t ?? 0)
|
|
).toISOString()
|
|
: null;
|
|
const [playbackPaused, setPlaybackPaused] = useState(false);
|
|
const recordingStartTimeRef = useRef<number>(0);
|
|
const recordingSnapshotsRef = useRef<Array<{ t: number; trains: TrainEntry[] }>>([]);
|
|
|
|
const refreshRecordingList = async () => {
|
|
const list = await loadRecordingList();
|
|
setRecordingList(list);
|
|
};
|
|
|
|
// 起動時: 旧フォーマット移行 → 一覧読み込み
|
|
useEffect(() => {
|
|
migrateOldRecording().then(refreshRecordingList).catch(() => {});
|
|
}, []);
|
|
|
|
// 再生ループ: playbackIndex / recorderState / playbackPaused が変わるたびに次へ進める
|
|
useEffect(() => {
|
|
if (recorderState !== 'playing' || !activeRecording || activeRecording.snapshots.length === 0) return;
|
|
if (playbackPaused) return; // 一時停止中はタイマーを張らない
|
|
const snap = activeRecording.snapshots[playbackIndex];
|
|
setMockTrainPositions(snap.trains);
|
|
const nextIndex = (playbackIndex + 1) % activeRecording.snapshots.length;
|
|
const delay = nextIndex === 0
|
|
? 15000
|
|
: Math.max(activeRecording.snapshots[nextIndex].t - snap.t, 3000);
|
|
const timer = setTimeout(() => setPlaybackIndex(nextIndex), delay);
|
|
return () => clearTimeout(timer);
|
|
}, [recorderState, playbackIndex, activeRecording, playbackPaused]);
|
|
|
|
const startRecording = () => {
|
|
// 録画中はライブデータを取得するためモックをOFF
|
|
setMockApiFeatureEnabledState(false);
|
|
AS.setItem(STORAGE_KEYS.MOCK_API_FEATURE_ENABLED, 'false');
|
|
recordingStartTimeRef.current = Date.now();
|
|
recordingSnapshotsRef.current = [];
|
|
setRecordingSnapshotCount(0);
|
|
setRecorderState('recording');
|
|
};
|
|
|
|
const stopRecording = async () => {
|
|
const snaps = recordingSnapshotsRef.current;
|
|
if (snaps.length > 0) {
|
|
const recordedAt = new Date(recordingStartTimeRef.current).toISOString();
|
|
const recording: TrainRecording = {
|
|
id: generateRecordingId(recordedAt),
|
|
recordedAt,
|
|
durationMs: snaps[snaps.length - 1].t,
|
|
snapshots: snaps,
|
|
};
|
|
await saveRecording(recording);
|
|
await refreshRecordingList();
|
|
}
|
|
setRecorderState('idle');
|
|
};
|
|
|
|
const startPlayback = async (id: string) => {
|
|
const recording = await loadRecordingById(id);
|
|
if (!recording || recording.snapshots.length === 0) return;
|
|
setMockApiFeatureEnabledState(true);
|
|
AS.setItem(STORAGE_KEYS.MOCK_API_FEATURE_ENABLED, 'true');
|
|
setActiveRecording(recording);
|
|
setPlaybackIndex(0);
|
|
setPlaybackPaused(false);
|
|
setRecorderState('playing');
|
|
};
|
|
|
|
const stopPlayback = () => {
|
|
setRecorderState('idle');
|
|
setPlaybackPaused(false);
|
|
setActiveRecording(null);
|
|
};
|
|
|
|
const pausePlayback = () => setPlaybackPaused(true);
|
|
const resumePlayback = () => setPlaybackPaused(false);
|
|
|
|
const seekToSnapshot = (index: number) => {
|
|
if (!activeRecording) return;
|
|
const i = Math.max(0, Math.min(index, activeRecording.snapshots.length - 1));
|
|
setPlaybackIndex(i);
|
|
setPlaybackPaused(true);
|
|
setMockTrainPositions(activeRecording.snapshots[i].trains);
|
|
};
|
|
|
|
// useCurrentTrain から呼ばれる: ライブfetch成功時にスナップショットを追記
|
|
const addTrainSnapshot = (trains: TrainEntry[]) => {
|
|
if (recorderState !== 'recording') return;
|
|
recordingSnapshotsRef.current.push({
|
|
t: Date.now() - recordingStartTimeRef.current,
|
|
trains,
|
|
});
|
|
setRecordingSnapshotCount(recordingSnapshotsRef.current.length);
|
|
};
|
|
|
|
const deleteRecording = async (id: string) => {
|
|
await deleteRecordingById(id);
|
|
await refreshRecordingList();
|
|
};
|
|
|
|
const downloadTextFileOnWeb = (fileName: string, content: string) => {
|
|
const web = globalThis as any;
|
|
if (!web.document || !web.URL || !web.Blob) {
|
|
throw new Error('この環境ではファイルのダウンロードに対応していません。');
|
|
}
|
|
|
|
const blob = new web.Blob([content], { type: 'application/json' });
|
|
const url = web.URL.createObjectURL(blob);
|
|
const anchor = web.document.createElement('a');
|
|
anchor.href = url;
|
|
anchor.download = fileName;
|
|
web.document.body.appendChild(anchor);
|
|
anchor.click();
|
|
anchor.remove();
|
|
web.setTimeout(() => web.URL.revokeObjectURL(url), 0);
|
|
};
|
|
|
|
const shareJsonFile = async (fileName: string, content: string) => {
|
|
if (Platform.OS === 'web') {
|
|
downloadTextFileOnWeb(fileName, content);
|
|
return;
|
|
}
|
|
|
|
const file = new File(Paths.cache, fileName);
|
|
if (file.exists) {
|
|
file.delete();
|
|
}
|
|
file.create({ overwrite: true });
|
|
file.write(content);
|
|
|
|
await Share.open({
|
|
title: fileName,
|
|
subject: fileName,
|
|
url: file.uri,
|
|
type: 'application/json',
|
|
filename: fileName.replace(/\.json$/i, ''),
|
|
failOnCancel: false,
|
|
saveToFiles: true,
|
|
useInternalStorage: true,
|
|
});
|
|
};
|
|
|
|
const exportRecordingFile = async (id: string) => {
|
|
const content = await buildRecordingExportTextCore(id);
|
|
await shareJsonFile(`jrshikoku-recording-${id}.json`, content);
|
|
};
|
|
|
|
const exportAllRecordingsFile = async () => {
|
|
const content = await buildAllRecordingsExportTextCore();
|
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
await shareJsonFile(`jrshikoku-recordings-${timestamp}.json`, content);
|
|
};
|
|
|
|
const readPickedRecordingText = async (asset: DocumentPicker.DocumentPickerAsset) => {
|
|
const webFile = (asset as any).file;
|
|
if (Platform.OS === 'web' && webFile && typeof webFile.text === 'function') {
|
|
return webFile.text();
|
|
}
|
|
|
|
const file = new File(asset.uri);
|
|
return file.text();
|
|
};
|
|
|
|
const importRecordingFile = async (): Promise<RecordingImportResult | null> => {
|
|
const result = await DocumentPicker.getDocumentAsync({
|
|
type: ['application/json', 'text/json', 'text/plain', '*/*'],
|
|
copyToCacheDirectory: true,
|
|
multiple: false,
|
|
base64: false,
|
|
});
|
|
|
|
if (result.canceled || !result.assets?.[0]) return null;
|
|
|
|
const content = await readPickedRecordingText(result.assets[0]);
|
|
const importResult = await importRecordingsFromTextCore(content);
|
|
await refreshRecordingList();
|
|
return importResult;
|
|
};
|
|
|
|
const importRecordingsFromText = async (content: string): Promise<RecordingImportResult> => {
|
|
const importResult = await importRecordingsFromTextCore(content);
|
|
await refreshRecordingList();
|
|
return importResult;
|
|
};
|
|
|
|
/** PosNum + Line → Pos テキスト(位置マスターから) */
|
|
const lookupPosText = (posNum: number, line: string): string | undefined =>
|
|
lookupPos(posNum, line, posLookup);
|
|
|
|
const mockApiConfig: MockApiConfig | null =
|
|
mockApiFeatureEnabled && mockTrainPositions
|
|
? { trainPositions: mockTrainPositions, positionMasters }
|
|
: null;
|
|
|
|
//地図表示テキスト
|
|
const injectJavascript = injectJavascriptData({
|
|
mapSwitch,
|
|
iconSetting,
|
|
stationMenu,
|
|
trainMenu,
|
|
uiSetting,
|
|
useUnyohub: useUnyohubSetting,
|
|
useElesite: useEleSiteSetting,
|
|
isDark: useThemeColors().isDark,
|
|
backendApiBaseUrl,
|
|
diagramTodayUrl,
|
|
mockApiConfig,
|
|
});
|
|
|
|
// XHRインターセプターはページスクリプトより前に実行が必要
|
|
const injectJavascriptBeforeContentLoaded = generateBeforeContentLoadedScript(mockApiConfig);
|
|
|
|
useEffect(() => {
|
|
//列車アイコンスイッチ
|
|
ASCore({
|
|
k: STORAGE_KEYS.ICON_SWITCH,
|
|
s: (value) => setIconSetting(normalizeIconDisplayMode(value)),
|
|
d: "original",
|
|
u: true,
|
|
});
|
|
//地図スイッチ
|
|
ASCore({ k: STORAGE_KEYS.MAP_SWITCH, s: setMapSwitch, d: "true", u: true });
|
|
//駅メニュースイッチ
|
|
ASCore({
|
|
k: STORAGE_KEYS.STATION_SWITCH,
|
|
s: setStationMenu,
|
|
d: "true",
|
|
u: true,
|
|
});
|
|
//列車メニュースイッチ
|
|
ASCore({
|
|
k: STORAGE_KEYS.TRAIN_SWITCH,
|
|
s: setTrainMenu,
|
|
d: "true",
|
|
u: true,
|
|
});
|
|
//GUIデザインベーススイッチ
|
|
ASCore({
|
|
k: STORAGE_KEYS.UI_SETTING,
|
|
s: setUiSetting,
|
|
d: "tokyo",
|
|
u: true,
|
|
});
|
|
//鉄道運用Hubスイッチ
|
|
ASCore({
|
|
k: STORAGE_KEYS.USE_UNYOHUB,
|
|
s: setUseUnyohubSetting,
|
|
d: "false",
|
|
u: true,
|
|
});
|
|
//えれサイトスイッチ
|
|
ASCore({
|
|
k: STORAGE_KEYS.USE_ELESITE,
|
|
s: setUseEleSiteSetting,
|
|
d: "false",
|
|
u: true,
|
|
});
|
|
//モックAPI検証機能スイッチ(admin専用・再起動不要)
|
|
AS.getItem(STORAGE_KEYS.MOCK_API_FEATURE_ENABLED).then((value) => {
|
|
const enabled = value === "true" || value === true;
|
|
setMockApiFeatureEnabledState(enabled);
|
|
// 起動時に既に有効なら位置マスターを取得
|
|
if (enabled) {
|
|
fetchPositionMasters()
|
|
.then((masters) => {
|
|
setPositionMasters(masters);
|
|
setPosLookup(buildPosLookup(masters));
|
|
})
|
|
.catch(() => {});
|
|
}
|
|
}).catch(() => {});
|
|
// バックエンドAPIベースURL(環境設定から読み込み)
|
|
AS.getItem(STORAGE_KEYS.JR_DATA_SYSTEM_ENV).then((value) => {
|
|
setBackendApiBaseUrl(getBackendApiBaseUrl(value));
|
|
setDiagramTodayUrl(getDiagramTodayUrl(value));
|
|
}).catch(() => {});
|
|
// 静的サンプルデータで初期化(モックAPIが応答するまでのフォールバック)
|
|
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 (
|
|
<TrainMenuContext.Provider
|
|
value={{
|
|
selectedLine,
|
|
setSelectedLine,
|
|
mapsStationData,
|
|
setMapsStationData,
|
|
iconSetting,
|
|
setIconSetting,
|
|
mapSwitch,
|
|
setMapSwitch,
|
|
stationMenu,
|
|
setStationMenu,
|
|
uiSetting,
|
|
setUiSetting,
|
|
LoadError,
|
|
setLoadError,
|
|
trainInfo,
|
|
setTrainInfo,
|
|
trainMenu,
|
|
setTrainMenu,
|
|
updatePermission,
|
|
setUpdatePermission,
|
|
userPermissionRole,
|
|
restrictedSoundPermission,
|
|
dataSourcePermission,
|
|
injectJavascript,
|
|
injectJavascriptBeforeContentLoaded,
|
|
mockTrainPositions,
|
|
setMockTrainPositions,
|
|
mockApiFeatureEnabled,
|
|
setMockApiFeatureEnabled,
|
|
positionMasters,
|
|
lookupPosText,
|
|
recorderState,
|
|
recordingSnapshotCount,
|
|
recordingList,
|
|
activeRecording,
|
|
playbackIndex,
|
|
playbackCurrentTimeIso,
|
|
playbackPaused,
|
|
startRecording,
|
|
stopRecording,
|
|
startPlayback,
|
|
stopPlayback,
|
|
pausePlayback,
|
|
resumePlayback,
|
|
seekToSnapshot,
|
|
addTrainSnapshot,
|
|
deleteRecording,
|
|
exportRecordingFile,
|
|
exportAllRecordingsFile,
|
|
importRecordingFile,
|
|
importRecordingsFromText,
|
|
}}
|
|
>
|
|
{children}
|
|
</TrainMenuContext.Provider>
|
|
);
|
|
};
|