Files
jrshikoku/lib/mockApi/trainRecorder.ts
harukin-expo-dev-env 4f4d3cad0a fix: crash on playback start + support multiple recordings
Crash fix (React Hooks violation):
- PlaybackTimeline.tsx: move ALL hooks (useRef for PanResponder, useCallback)
  to before the early return; use seekRef/totalRef to share values into
  PanResponder handlers without stale closure issues

Multiple recordings support:
- trainRecorder.ts: redesign to id-based multi-recording system
  - RecordingMeta type for lightweight list (no snapshots)
  - TrainRecording now includes id field
  - saveRecording/loadRecordingById/deleteRecordingById/loadRecordingList
  - migrateOldRecording() migrates old single MOCK_RECORDING key on first launch
- constants/storage.ts: add MOCK_RECORDINGS_INDEX + MOCK_RECORDING_DATA_PREFIX keys
- useTrainMenu: savedRecording → recordingList (RecordingMeta[]) + activeRecording (TrainRecording|null)
  - startPlayback(id) loads full recording on demand
  - deleteRecording(id) deletes by id and refreshes list
  - stopPlayback clears activeRecording
- DataSourceSettings: recording list UI
  - shows all recordings with date/time, snapshot count, duration
  - ▶ play and 削除 buttons per row
  - recording/playing status indicator

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-02 00:52:30 +00:00

95 lines
3.2 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { AS } from '../../storageControl';
import { STORAGE_KEYS } from '../../constants/storage';
import { TrainEntry } from './webviewXhrInterceptor';
export type TrainSnapshot = {
/** ms elapsed from recording start */
t: number;
trains: TrainEntry[];
};
/** 録画のメタ情報(一覧表示用・軽量) */
export type RecordingMeta = {
id: string;
recordedAt: string;
durationMs: number;
snapshotCount: number;
};
/** フル録画データ(再生時のみロード) */
export type TrainRecording = {
id: string;
recordedAt: string;
durationMs: number;
snapshots: TrainSnapshot[];
};
const parse = <T>(raw: string | null | boolean): T | null => {
if (!raw) return null;
try {
return JSON.parse(typeof raw === 'string' ? raw : JSON.stringify(raw)) as T;
} catch {
return null;
}
};
/** 録画IDを生成recordedAt ISO文字列からファイル名に使えるIDへ */
export const generateRecordingId = (recordedAt: string) =>
recordedAt.replace(/[:.]/g, '-');
/** 録画インデックス(メタ一覧)を読み込む */
export const loadRecordingList = async (): Promise<RecordingMeta[]> => {
const raw = await AS.getItem(STORAGE_KEYS.MOCK_RECORDINGS_INDEX).catch(() => null);
return parse<RecordingMeta[]>(raw) ?? [];
};
/** フル録画データをIDで読み込む */
export const loadRecordingById = async (id: string): Promise<TrainRecording | null> => {
const raw = await AS.getItem((STORAGE_KEYS.MOCK_RECORDING_DATA_PREFIX + id) as any).catch(() => null);
return parse<TrainRecording>(raw);
};
/** 録画を保存してインデックスに追加する */
export const saveRecording = async (recording: TrainRecording): Promise<void> => {
await AS.setItem(
(STORAGE_KEYS.MOCK_RECORDING_DATA_PREFIX + recording.id) as any,
JSON.stringify(recording),
);
const list = await loadRecordingList();
const meta: RecordingMeta = {
id: recording.id,
recordedAt: recording.recordedAt,
durationMs: recording.durationMs,
snapshotCount: recording.snapshots.length,
};
// 先頭に追加新しい順、同IDは重複排除
const newList = [meta, ...list.filter((m) => m.id !== recording.id)];
await AS.setItem(STORAGE_KEYS.MOCK_RECORDINGS_INDEX, JSON.stringify(newList));
};
/** 録画をIDで削除する */
export const deleteRecordingById = async (id: string): Promise<void> => {
await AS.removeItem((STORAGE_KEYS.MOCK_RECORDING_DATA_PREFIX + id) as any).catch(() => {});
const list = await loadRecordingList();
await AS.setItem(
STORAGE_KEYS.MOCK_RECORDINGS_INDEX,
JSON.stringify(list.filter((m) => m.id !== id)),
);
};
/**
* 旧フォーマットMOCK_RECORDINGからマイグレーション。
* 新インデックスが空の場合のみ実行する。
*/
export const migrateOldRecording = async (): Promise<void> => {
const list = await loadRecordingList();
if (list.length > 0) return; // 既に移行済み
const raw = await AS.getItem(STORAGE_KEYS.MOCK_RECORDING).catch(() => null);
const old = parse<Omit<TrainRecording, 'id'>>(raw);
if (!old) return;
const id = generateRecordingId(old.recordedAt);
await saveRecording({ ...old, id });
await AS.removeItem(STORAGE_KEYS.MOCK_RECORDING).catch(() => {});
};