feat: add opt-in service formation display to train details

This commit is contained in:
harukin-expo-dev-env
2026-10-07 01:00:49 +09:00
parent 564d7e5919
commit 810defe03e
3 changed files with 39 additions and 3 deletions
@@ -1,3 +1,5 @@
import { useServiceStates } from "@/stateBox/useCurrentTrain";
import { formationText, serviceStateText } from "@/lib/serviceState/display";
import React, { FC, useMemo } from "react";
import { Text, View, TextStyle, TouchableOpacity } from "react-native";
import { SheetManager } from "react-native-actions-sheet";
@@ -52,6 +54,9 @@ export const HeaderText: FC<Props> = ({
const { fixed } = useThemeColors();
const { fontScale } = useResponsive();
const { iconSetting } = useTrainMenu();
const { serviceStates, available, getFormation } = useServiceStates();
const serviceState = serviceStates.find(state => state.train_id === trainNum);
const shadowText = available ? serviceState ? serviceStateText(serviceState) : formationText(getFormation(trainNum)) : null;
const { allCustomTrainData } = useCustomTrainData();
const { getTodayOperationByTrainId } = useOperationLogs();
const { expoPushToken } = useNotification();
@@ -222,6 +227,7 @@ export const HeaderText: FC<Props> = ({
};
return (
<>
<View
style={{
padding: 10,
@@ -233,14 +239,14 @@ export const HeaderText: FC<Props> = ({
scrollRef.current?.scrollTo({ y: 0, animated: true })
}
>
<TrainIconStatus
{!shadowText && <TrainIconStatus
trainNum={trainNum}
navigate={navigate}
from={from}
todayOperation={todayOperation}
direction={iconTrainDirection}
iconDisplayMode={iconDisplayMode}
/>
/>}
<View
style={{
@@ -343,5 +349,9 @@ export const HeaderText: FC<Props> = ({
}}
/>
</View>
{shadowText && <View style={{ paddingHorizontal: 10, paddingBottom: 10 }}>
<Text style={{ color: fixed.textOnPrimary, fontSize: fontScale(14) }}>{shadowText}</Text>
</View>}
</>
);
};
@@ -42,7 +42,7 @@ export const ResearchToolsSettings = () => {
})
.catch(() => {});
}, []);
const { updatePermission, mockApiFeatureEnabled, setMockApiFeatureEnabled } =
const { updatePermission, mockApiFeatureEnabled, setMockApiFeatureEnabled, serviceStateShadowEnabled, setServiceStateShadowEnabled } =
useTrainMenu();
const {
recorderState,
@@ -204,6 +204,14 @@ export const ResearchToolsSettings = () => {
</View>
) : (
<>
<View style={[styles.debugSection, { backgroundColor: colors.surface, borderColor: colors.borderSecondary }]}>
<Text style={[styles.debugTitle, { color: colors.textPrimary }]}>区間ごとの編成表示(検証)</Text>
<Text style={[styles.debugDescription, { color: colors.textSecondary }]}>列車詳細と走行位置に、その区間の編成を表示します。</Text>
<View style={styles.switchRow}>
<Text style={{ color: colors.textPrimary }}>区間ごとの編成表示</Text>
<Switch value={serviceStateShadowEnabled} onValueChange={setServiceStateShadowEnabled} color={fixed.primary} />
</View>
</View>
<View
style={[
styles.debugSection,
+18
View File
@@ -0,0 +1,18 @@
import type { AssignedComponent, Formation, ServiceState } from "./types";
export function componentsText(components: readonly AssignedComponent[]): string {
return [...components].sort((a, b) => a.order - b.order)
.map(c => c.unit_ids?.length ? c.unit_ids.join(" + ") : "不明").join(" + ") || "不明";
}
export function serviceStateText(state: ServiceState): string | null {
if (state.status === "unsupported") return null;
const status = state.status === "partial" ? "一部不明" : state.status === "transition" ? "増結・分割・交換作業中の可能性" : "";
return [componentsText(state.components), status].filter(Boolean).join(" / ");
}
export function formationText(formation: Formation): string | null {
if (!formation.sections.length) return null;
const sections = [...new Set(formation.sections.map(s => componentsText(s.components)))];
return `本日の編成: ${sections.join(" → ")} / 現在位置: 取得できません`;
}