diff --git a/components/ActionSheetComponents/EachTrainInfoCore/HeaderText.tsx b/components/ActionSheetComponents/EachTrainInfoCore/HeaderText.tsx index a9eef33..48d974d 100644 --- a/components/ActionSheetComponents/EachTrainInfoCore/HeaderText.tsx +++ b/components/ActionSheetComponents/EachTrainInfoCore/HeaderText.tsx @@ -178,7 +178,7 @@ export const HeaderText: FC = ({ (e) => !!e.formations && e.formations.trim() !== "", ); const hasElesiteFormation = elesiteEntries.some( - (e) => !!e.formations && e.formations.trim() !== "", + (e) => (e.formation_config?.units?.length ?? 0) > 0, ); const hasExtraInfo = diff --git a/components/ActionSheetComponents/EachTrainInfoCore/TrainSourcesPanel.tsx b/components/ActionSheetComponents/EachTrainInfoCore/TrainSourcesPanel.tsx deleted file mode 100644 index 103ed96..0000000 --- a/components/ActionSheetComponents/EachTrainInfoCore/TrainSourcesPanel.tsx +++ /dev/null @@ -1,629 +0,0 @@ -import React, { FC, useMemo } from "react"; -import { - View, - Text, - TouchableOpacity, - StyleSheet, - ScrollView, - Image, -} from "react-native"; -import { MaterialCommunityIcons, Ionicons } from "@expo/vector-icons"; -import { SheetManager } from "react-native-actions-sheet"; -import type { NavigateFunction } from "@/types"; -import type { OperationLogs } from "@/lib/CommonTypes"; -import type { UnyohubData, ElesiteData } from "@/types/unyohub"; - -const HUB_LOGO_PNG = require("@/assets/icons/hub_logo.png"); -const ELESITE_LOGO_PNG = require("@/assets/icons/elesite_logo.png"); - -type Props = { - trainNum: string; - unyohubEntries: UnyohubData[]; - elesiteEntries: ElesiteData[]; - todayOperation: OperationLogs[]; - navigate: NavigateFunction; - expoPushToken: string; - onClose: () => void; - /** true = 上り (vehicle_img) / false = 下り (vehicle_img_right) */ - direction: boolean; -}; - -/** 情報ソース別カードを並べて表示するパネル */ -export const TrainSourcesPanel: FC = ({ - trainNum, - unyohubEntries, - elesiteEntries, - todayOperation, - navigate, - expoPushToken, - onClose, - direction, -}) => { - // 連結番号(train_ids のカンマ後数値)で昇順ソート - const sortedTodayOperation = useMemo(() => { - const extractOrder = (trainId: string): number => { - const parts = trainId.split(","); - if (parts.length > 1) { - const n = parseInt(parts[1].trim(), 10); - return isNaN(n) ? Infinity : n; - } - return Infinity; - }; - const findId = (op: OperationLogs): string | null => { - for (const id of [...(op.train_ids ?? []), ...(op.related_train_ids ?? [])]) { - if (id.split(",")[0] === trainNum) return id; - } - return null; - }; - return [...todayOperation].sort((a, b) => { - const aId = findId(a); - const bId = findId(b); - if (!aId || !bId) return aId ? -1 : bId ? 1 : 0; - return extractOrder(aId) - extractOrder(bId); - }); - }, [todayOperation, trainNum]); - - return ( - - {/* ヘッダー */} - - 運用情報ソース - - - - - - - {/* ──────────────────────────────────────── */} - {/* JR四国データベース(常に表示) */} - {/* ──────────────────────────────────────── */} - { - const uri = `https://jr-shikoku-data-system.pages.dev/trainData/${trainNum}?userID=${expoPushToken}&from=eachTrainInfo`; - navigate("generalWebView", { uri, useExitButton: false }); - SheetManager.hide("EachTrainInfo"); - onClose(); - }} - /> - - {/* ──────────────────────────────────────── */} - {/* 鉄道運用Hub */} - {/* ──────────────────────────────────────── */} - {unyohubEntries.length > 0 ? ( - <> - - {unyohubEntries.map((entry, i) => ( - - ))} - - ) : ( - - )} - - {/* ―――――――――――――――――――――――――――――――――――――――――― */} - {/* えれサイト */} - {/* ―――――――――――――――――――――――――――――――――――――――――― */} - {elesiteEntries.length > 0 ? ( - <> - - {elesiteEntries.map((entry, i) => ( - - ))} - - ) : ( - - )} - - {/* ──────────────────────────────────────── */} - {/* 今日の運用情報 */} - {/* ──────────────────────────────────────── */} - {sortedTodayOperation.length > 0 ? ( - <> - - - {sortedTodayOperation.map((op, i) => ( - - ))} - - ) : ( - - )} - - - ); -}; - -/* ------------------------------------------------------------------ */ -/* サブコンポーネント */ -/* ------------------------------------------------------------------ */ - -/** セクションラベル */ -const SectionLabel: FC<{ label: string; icon?: string; imagePng?: any }> = ({ label, icon, imagePng }) => ( - - {imagePng ? ( - - ) : ( - - )} - {label} - -); - -/** 汎用ソースカード */ -const SourceCard: FC<{ - iconName?: string; - iconImagePng?: any; - iconColor: string; - tag: string; - tagColor: string; - title: string; - description: string; - available: boolean; - onPress?: () => void; -}> = ({ iconName, iconImagePng, iconColor, tag, tagColor, title, description, available, onPress }) => ( - - - {iconImagePng ? ( - - ) : ( - - )} - - - - {title} - - {tag} - - - {description} - - {available && ( - - )} - -); - -/** 鉄道運用Hubカード(1運用分) */ -const UnyohubCard: FC<{ - entry: UnyohubData; - trainNum: string; - navigate: NavigateFunction; - onClose: () => void; -}> = ({ entry, trainNum, navigate, onClose }) => { - const matchedTrain = entry.trains?.find((t) => t.train_number === trainNum); - const timeInfo = - matchedTrain?.first_departure_time && matchedTrain?.final_arrival_time - ? `${matchedTrain.first_departure_time} → ${matchedTrain.final_arrival_time}` - : matchedTrain?.first_departure_time || null; - const routeInfo = - matchedTrain?.starting_station && matchedTrain?.terminal_station - ? `${matchedTrain.starting_station} → ${matchedTrain.terminal_station}` - : null; - const dirLabel = - matchedTrain?.direction === "inbound" ? "上り" : - matchedTrain?.direction === "outbound" ? "下り" : null; - - return ( - { - const uri = `https://jr-shikoku-data-system.pages.dev/unyohub-connection-train-data/${trainNum}`; - navigate("generalWebView", { uri, useExitButton: true }); - SheetManager.hide("EachTrainInfo"); - onClose(); - }} - > - {/* アイコン */} - - - - - - {entry.operation_id || "運用Hub"} - - 運用Hub - - {!!dirLabel && ( - - {dirLabel} - - )} - - - {/* 詳細行 */} - - {timeInfo && } - {routeInfo && } - - - - - ); -}; - -/** えれサイトカード(1運用分) */ -const ElesiteCard: FC<{ - entry: ElesiteData; - trainNum: string; - navigate: NavigateFunction; - onClose: () => void; -}> = ({ entry, trainNum, navigate, onClose }) => { - const matchedTrain = entry.trains?.find((t) => t.train_number === trainNum); - // 個別列車ページURL(なければトップページ) - const timetableUrl = matchedTrain?.timetable_url || "https://www.elesite-next.com/"; - // 進行方向: formation_config.left/right_station + nav.heading_to - const fc = entry.formation_config; - const headingTo = matchedTrain?.nav?.heading_to; - const dirLabel = - headingTo === "left" && fc?.left_station ? `← ${fc.left_station}` : - headingTo === "right" && fc?.right_station ? `${fc.right_station} →` : null; - // 編成名: units 優先 - const formationText = - fc?.units?.length ? fc.units.map(u => u.formation).join('+') : - (entry.formations || entry.operation_id || "えれサイト"); - - return ( - { - navigate("generalWebView", { uri: timetableUrl, useExitButton: true }); - SheetManager.hide("EachTrainInfo"); - onClose(); - }} - > - - - - - - {formationText} - - えれサイト - - {!!dirLabel && ( - - {dirLabel} - - )} - - - - {fc?.left_station && fc?.right_station && ( - - )} - {!!matchedTrain?.direction && ( - - )} - {entry.starting_location && ( - - )} - - - {entry.comment && ( - {entry.comment} - )} - - - - {entry.posts_count}件の投稿 - - - - - ); -}; - -/** 進行方向バナー */ -const DirectionBanner: FC<{ direction: boolean }> = ({ direction }) => ( - - {direction ? ( - // 上り:矢印は左向き(← 先頭) - <> - - 進行方向(上り) - - ) : ( - // 下り:矢印は右向き(先頭 →) - <> - 進行方向(下り) - - - )} - -); - -/** 今日の運用情報カード */ -const OperationCard: FC<{ - op: OperationLogs; - index: number; - direction: boolean; - navigate: NavigateFunction; - onClose: () => void; -}> = ({ op, index, direction, navigate, onClose }) => { - // 進行方向に応じて車両画像を選択(trainIconStatus.tsx と同ロジック) - const thumbUri = direction - ? (op.vehicle_img || op.vehicle_img_right) - : (op.vehicle_img_right || op.vehicle_img); - const hasUrl = !!op.vehicle_info_url; - const trainIds = [...(op.train_ids || []), ...(op.related_train_ids || [])]; - - return ( - { - navigate("howto", { info: op.vehicle_info_url, goTo: "menu" }); - SheetManager.hide("EachTrainInfo"); - onClose(); - } - : undefined - } - > - {/* 車両サムネイル or アイコン(進行方向考慮) */} - {thumbUri ? ( - - ) : ( - - - - )} - - - - 運用記録 #{index + 1} - - 記録 - - - {trainIds.length > 0 && ( - - 列番: {trainIds.slice(0, 4).join(" / ")} - {trainIds.length > 4 ? " ..." : ""} - - )} - {op.date && ( - 日付: {op.date} - )} - - {hasUrl && ( - - )} - - ); -}; - -/** 小さい詳細チップ */ -const DetailChip: FC<{ icon: string; text: string }> = ({ icon, text }) => ( - - - {text} - -); - -/* ------------------------------------------------------------------ */ -/* スタイル */ -/* ------------------------------------------------------------------ */ -const styles = StyleSheet.create({ - container: { - backgroundColor: "#fff", - borderTopWidth: 1, - borderTopColor: "#e0e0e0", - }, - panelHeader: { - flexDirection: "row", - alignItems: "center", - paddingHorizontal: 14, - paddingVertical: 10, - borderBottomWidth: 1, - borderBottomColor: "#f0f0f0", - }, - panelHeaderText: { - flex: 1, - fontSize: 14, - fontWeight: "bold", - color: "#333", - }, - closeButton: { - padding: 4, - }, - scroll: { - maxHeight: 360, - }, - scrollContent: { - paddingHorizontal: 12, - paddingVertical: 8, - paddingBottom: 16, - }, - sectionLabel: { - flexDirection: "row", - alignItems: "center", - marginTop: 10, - marginBottom: 4, - gap: 4, - }, - sectionLabelText: { - fontSize: 12, - color: "#888", - fontWeight: "600", - letterSpacing: 0.5, - }, - card: { - flexDirection: "row", - alignItems: "center", - backgroundColor: "#f9f9f9", - borderRadius: 10, - marginBottom: 8, - padding: 10, - borderWidth: 1, - borderColor: "#eeeeee", - gap: 10, - }, - cardDisabled: { - opacity: 0.5, - }, - cardNoArrow: { - // no change needed - }, - cardIconWrap: { - width: 40, - height: 40, - borderRadius: 10, - alignItems: "center", - justifyContent: "center", - flexShrink: 0, - }, - vehicleThumb: { - width: 50, - height: 40, - borderRadius: 6, - flexShrink: 0, - backgroundColor: "#eee", - }, - cardBody: { - flex: 1, - gap: 3, - }, - cardTitleRow: { - flexDirection: "row", - alignItems: "center", - gap: 6, - flexWrap: "wrap", - }, - cardTitle: { - fontSize: 14, - fontWeight: "bold", - color: "#222", - }, - cardTitleDisabled: { - color: "#aaa", - }, - cardDesc: { - fontSize: 12, - color: "#666", - }, - tag: { - borderRadius: 4, - paddingHorizontal: 5, - paddingVertical: 1, - }, - tagText: { - fontSize: 10, - fontWeight: "bold", - }, - unyoDetailRow: { - flexDirection: "row", - flexWrap: "wrap", - gap: 4, - marginTop: 2, - }, - unyoComment: { - fontSize: 11, - color: "#888", - fontStyle: "italic", - marginTop: 2, - }, - postsRow: { - flexDirection: "row", - alignItems: "center", - gap: 3, - marginTop: 2, - }, - postsText: { - fontSize: 11, - color: "#888", - }, - chip: { - flexDirection: "row", - alignItems: "center", - backgroundColor: "#f0f0f0", - borderRadius: 4, - paddingHorizontal: 5, - paddingVertical: 2, - gap: 3, - }, - chipText: { - fontSize: 11, - color: "#555", - }, - directionBanner: { - flexDirection: "row", - alignItems: "center", - gap: 4, - backgroundColor: "#E3F4FB", - borderRadius: 6, - paddingHorizontal: 8, - paddingVertical: 4, - marginBottom: 6, - alignSelf: "flex-start", - }, - directionText: { - fontSize: 11, - fontWeight: "600", - color: "#0099CC", - }, -}); diff --git a/components/ActionSheetComponents/TrainDataSources.tsx b/components/ActionSheetComponents/TrainDataSources.tsx index e77f006..2ec1516 100644 --- a/components/ActionSheetComponents/TrainDataSources.tsx +++ b/components/ActionSheetComponents/TrainDataSources.tsx @@ -15,6 +15,8 @@ import { getTrainType } from "@/lib/getTrainType"; import type { NavigateFunction } from "@/types"; import type { OperationLogs, CustomTrainData } from "@/lib/CommonTypes"; import type { UnyohubData, ElesiteData } from "@/types/unyohub"; +import { useUnyohub } from "@/stateBox/useUnyohub"; +import { useElesite } from "@/stateBox/useElesite"; export type TrainDataSourcesPayload = { trainNum: string; @@ -46,10 +48,27 @@ const ELESITE_LOGO_PNG = require("@/assets/icons/elesite_logo.png"); export const TrainDataSources: FC<{ payload?: TrainDataSourcesPayload }> = ({ payload, }) => { + // ── フックはすべて条件分岐より前に呼ぶ(Rules of Hooks)── + const { useUnyohub: unyohubEnabled } = useUnyohub(); + const { useElesite: elesiteEnabled } = useElesite(); + if (!payload) return null; - const { trainNum, unyohubEntries, elesiteEntries = [], todayOperation, navigate, expoPushToken, priority, direction: directionProp, customTrainData, typeName, trainName, departureStation, destinationStation } = - payload; + const { + trainNum, + unyohubEntries, + elesiteEntries = [], + todayOperation, + navigate, + expoPushToken, + priority, + direction: directionProp, + customTrainData, + typeName, + trainName, + departureStation, + destinationStation, + } = payload; // 進行方向の確定: // 1. payload.direction が明示されていればそれを使う @@ -57,7 +76,8 @@ export const TrainDataSources: FC<{ payload?: TrainDataSourcesPayload }> = ({ // 3. どちらもなければ列番の偶数 = 上り / 奇数 = 下り でフォールバック const resolvedDirection: boolean = (() => { if (directionProp !== undefined) return directionProp; - if (customTrainData?.directions !== undefined) return !!customTrainData.directions; + if (customTrainData?.directions !== undefined) + return !!customTrainData.directions; return parseInt(trainNum.replace(/[^\d]/g, ""), 10) % 2 === 0; })(); @@ -78,9 +98,9 @@ export const TrainDataSources: FC<{ payload?: TrainDataSourcesPayload }> = ({ // 運用情報: train_ids / related_train_ids の位置番号でソートして unit_ids を収集 // "4565M,2" のようなカンマ区切り位置番号を解析 // 上り(resolvedDirection=true)=ASC, 下り(false)=DESC - const positionOf = (op: typeof todayOperation[number]): number => { + const positionOf = (op: (typeof todayOperation)[number]): number => { const all = [...(op.train_ids ?? []), ...(op.related_train_ids ?? [])]; - const match = all.find(id => id.split(",")[0] === trainNum); + const match = all.find((id) => id.split(",")[0] === trainNum); const pos = match ? parseInt(match.split(",")[1] ?? "0", 10) : 0; return isNaN(pos) ? 0 : pos; }; @@ -91,32 +111,49 @@ export const TrainDataSources: FC<{ payload?: TrainDataSourcesPayload }> = ({ const allUnitIds = [ ...new Set(sortedOperation.flatMap((op) => op.unit_ids ?? [])), ]; - const fallbackIds = sortedOperation.flatMap((op) => op.train_ids ?? []).slice(0, 4); - const unitIdsSub = allUnitIds.length > 0 - ? null - : opCount > 0 - ? fallbackIds.join("・") || "運用記録あり" - : "本日の運用記録なし"; + const fallbackIds = sortedOperation + .flatMap((op) => op.train_ids ?? []) + .slice(0, 4); + const unitIdsSub = + allUnitIds.length > 0 + ? null + : opCount > 0 + ? fallbackIds.join("・") || "運用記録あり" + : "本日の運用記録なし"; const operationDetail = ( - {opCount > 0 && ( - allUnitIds.length > 0 ? ( + {opCount > 0 && + (allUnitIds.length > 0 ? ( - {allUnitIds.slice(0, 8).join("・")}{allUnitIds.length > 8 ? `他${allUnitIds.length - 8}件` : ""} + {allUnitIds.slice(0, 8).join("・")} + {allUnitIds.length > 8 ? `他${allUnitIds.length - 8}件` : ""} ) : ( - {fallbackIds.join("・") || "運用記録あり"} - ) - )} + + {fallbackIds.join("・") || "運用記録あり"} + + ))} {opCount > 0 && } @@ -124,26 +161,38 @@ export const TrainDataSources: FC<{ payload?: TrainDataSourcesPayload }> = ({ ); // 鉄道運用Hub: 車番(formations)が空でないエントリのみ抽出して判定 - const hasNonEmptyFormations = unyohubEntries.some(e => !!e.formations && e.formations.trim() !== ""); - const nonEmptyFormationEntries = unyohubEntries.filter(e => !!e.formations && e.formations.trim() !== ""); + const hasNonEmptyFormations = unyohubEntries.some( + (e) => !!e.formations && e.formations.trim() !== "", + ); + const nonEmptyFormationEntries = unyohubEntries.filter( + (e) => !!e.formations && e.formations.trim() !== "", + ); // 先頭エントリで direction を取得し、表示順を決定: // outbound → position_forward 昇順 (pos=1 が宇和島/南端側) // inbound → position_forward 降順 (pos=MAX が宇和島/南端側) - const matchedDirection = nonEmptyFormationEntries[0] - ?.trains?.find(t => t.train_number === trainNum)?.direction; + const matchedDirection = nonEmptyFormationEntries[0]?.trains?.find( + (t) => t.train_number === trainNum, + )?.direction; const hubSortDescending = matchedDirection === "inbound"; const formationNames = [...nonEmptyFormationEntries] .sort((a, b) => { - const posA = a.trains?.find(t => t.train_number === trainNum)?.position_forward ?? 0; - const posB = b.trains?.find(t => t.train_number === trainNum)?.position_forward ?? 0; + const posA = + a.trains?.find((t) => t.train_number === trainNum) + ?.position_forward ?? 0; + const posB = + b.trains?.find((t) => t.train_number === trainNum) + ?.position_forward ?? 0; return hubSortDescending ? posB - posA : posA - posB; }) .slice(0, 4) .map((e) => e.formations) - .join("・") + (nonEmptyFormationEntries.length > 4 ? ` 他${nonEmptyFormationEntries.length - 4}件` : ""); + .join("・") + + (nonEmptyFormationEntries.length > 4 + ? ` 他${nonEmptyFormationEntries.length - 4}件` + : ""); const formationDetail = ( @@ -157,18 +206,45 @@ export const TrainDataSources: FC<{ payload?: TrainDataSourcesPayload }> = ({ ); - // えれサイト: 車番(formations)が空でないエントリのみ抽出して判定 - const elesiteHasNonEmptyFormations = elesiteEntries.some(e => !!e.formations && e.formations.trim() !== ""); - const elesiteNonEmptyFormationEntries = elesiteEntries.filter(e => !!e.formations && e.formations.trim() !== ""); - // えれサイト: 投稿件数が1件以上あるか - const elesiteHasPosts = elesiteEntries.some(e => (e.posts_count ?? 0) > 0); + // えれサイト: units が1件以上あるエントリのみ「データあり」と判定 + const elesiteHasNonEmptyFormations = elesiteEntries.some( + (e) => (e.formation_config?.units?.length ?? 0) > 0, + ); + const elesiteNonEmptyFormationEntries = elesiteEntries + .filter((e) => (e.formation_config?.units?.length ?? 0) > 0) + .sort((a, b) => { + // high松(left_station)側のユニットを先に表示 + // (heading_to === "left") === is_leading が true → high松(left)端のユニット + const aNav = a.trains?.find((t) => t.train_number === trainNum)?.nav; + const bNav = b.trains?.find((t) => t.train_number === trainNum)?.nav; + const aIsLeft = + (aNav?.heading_to === "left") === (aNav?.is_leading === true); + const bIsLeft = + (bNav?.heading_to === "left") === (bNav?.is_leading === true); + if (aIsLeft === bIsLeft) return 0; + return aIsLeft ? -1 : 1; + }); + // えれサイト: 編成名テキスト(formation_config.units 優先) + const elesiteFormationNames = + elesiteNonEmptyFormationEntries + .slice(0, 4) + .map((e) => { + const units = e.formation_config?.units; + return units?.length + ? units.map((u) => u.formation).join("+") + : e.formations; + }) + .join("・") + + (elesiteNonEmptyFormationEntries.length > 4 + ? ` 他${elesiteNonEmptyFormationEntries.length - 4}件` + : ""); // 列車情報 subテキスト const trainInfoSub = customTrainData?.vehicle_formation ? customTrainData.vehicle_formation : hasTrainInfo - ? "臨時情報あり / コミュニティ樓所データを確認・編集" - : "編成データ・コミュニティ独自データを確認・編集"; + ? "臨時情報あり / コミュニティ樓所データを確認・編集" + : "編成データ・コミュニティ独自データを確認・編集"; const trainDetail = customTrainData ? ( = ({ /> ) : null; + const elesiteFormationDetail = ( + + {elesiteHasNonEmptyFormations && ( + {elesiteFormationNames} + )} + {elesiteCount > 0 + ? (() => { + const fc = (elesiteNonEmptyFormationEntries[0] ?? elesiteEntries[0]) + ?.formation_config; + return fc?.left_station && fc?.right_station ? ( + + ) : null; + })() + : undefined} + + ); return ( = ({ onPress={() => openWebView( `https://jr-shikoku-data-system.pages.dev/trainData/${trainNum}?userID=${expoPushToken}&from=eachTrainInfo`, - false + false, ) } /> {/* ─── 鉄道運用Hub ─────────────────────────── */} - 0 - ? "数日の運用報告なし" - : "この列車の運用データはありません" - } - badge={hasNonEmptyFormations ? unyoCount : null} - badgeColor="#333" - detail={formationDetail} - disabled={unyoCount === 0} - onPress={() => - openWebView( - `https://jr-shikoku-data-system.pages.dev/unyohub-connection-train-data/${trainNum}`, - true - ) - } - /> + {unyohubEnabled && ( + 0 + ? "数日の運用報告なし" + : "この列車の運用データはありません" + } + badge={hasNonEmptyFormations ? unyoCount : null} + badgeColor="#333" + detail={formationDetail} + disabled={unyoCount === 0} + onPress={() => + openWebView( + `https://jr-shikoku-data-system.pages.dev/unyohub-connection-train-data/${trainNum}`, + true, + ) + } + /> + )} {/* ─── えれサイト ─────────────────────────── */} - 0 ? (() => { - const fc = (elesiteNonEmptyFormationEntries[0] ?? elesiteEntries[0])?.formation_config; - return fc?.left_station && fc?.right_station ? ( - - ) : null; - })() : undefined - } - disabled={elesiteCount === 0} - onPress={() => { - const matchedEntry = elesiteNonEmptyFormationEntries[0] ?? elesiteEntries[0]; - const matchedTrain = matchedEntry?.trains?.find(t => t.train_number === trainNum); - const url = matchedTrain?.timetable_url || "https://www.elesite-next.com/"; - openWebView(url, true); - }} - /> - + {elesiteEnabled && ( + { + const matchedEntry = + elesiteNonEmptyFormationEntries[0] ?? elesiteEntries[0]; + const matchedTrain = matchedEntry?.trains?.find( + (t) => t.train_number === trainNum, + ); + const url = + matchedTrain?.timetable_url || "https://www.elesite-next.com/"; + openWebView(url, true); + }} + /> + )} @@ -337,13 +431,19 @@ const RefDirectionBanner: FC<{ rows: RefDirRow[]; color?: string }> = ({ )} {!!row.lineLabel && ( - {row.lineLabel} + + {row.lineLabel} + )} {!!row.rightLabel && ( <> {row.rightLabel} - + )} @@ -391,7 +491,9 @@ const CyclingRefDirectionBanner: FC<{ rows: RefDirRow[]; color?: string }> = ({ )} {!!row.lineLabel && ( - {row.lineLabel} + + {row.lineLabel} + )} {!!row.rightLabel && ( @@ -421,7 +523,11 @@ const CombinedCard: FC<{ label: string; onPress: () => void; }> = ({ rows, color, label, onPress }) => ( - + {/* 共通ヘッダー */} @@ -434,8 +540,17 @@ const CombinedCard: FC<{ {/* 左カラーバー分のスペーサー */} - - + + @@ -447,14 +562,19 @@ const CombinedCard: FC<{ )} {row.sub !== null && ( - {row.sub} - )} - {row.detail && ( - {row.detail} + + {row.sub} + )} + {row.detail && {row.detail}} {i === rows.length - 1 && ( - + )} @@ -472,12 +592,36 @@ const TrainInfoDetail: FC<{ departureStation?: string; destinationStation?: string; }> = ({ data, typeName, trainName, departureStation, destinationStation }) => { - const { infogram, train_info, vehicle_formation, via_data, start_date, end_date, uwasa, optional_text } = data; - const { color: typeColor, fontAvailable } = getTrainType({ type: data.type, whiteMode: false }); + const { + infogram, + train_info, + vehicle_formation, + via_data, + start_date, + end_date, + uwasa, + optional_text, + } = data; + const { color: typeColor, fontAvailable } = getTrainType({ + type: data.type, + whiteMode: false, + }); const resolvedTypeColor = typeColor === "white" ? "#333333ff" : typeColor; // to_data がなければダイヤの終着駅をフォールバック const destDisplay = data.to_data || destinationStation || ""; - const hasAny = typeName || trainName || departureStation || destDisplay || infogram || train_info || vehicle_formation || via_data || start_date || end_date || uwasa || optional_text; + const hasAny = + typeName || + trainName || + departureStation || + destDisplay || + infogram || + train_info || + vehicle_formation || + via_data || + start_date || + end_date || + uwasa || + optional_text; if (!hasAny) return null; return ( @@ -487,16 +631,27 @@ const TrainInfoDetail: FC<{ {!!typeName && ( - - {typeName} + + + {typeName} + )} {!!trainName && ( - {trainName} + + {trainName} + )} {!!(departureStation || destDisplay) && ( @@ -505,10 +660,16 @@ const TrainInfoDetail: FC<{ {departureStation} )} {!!(departureStation && destDisplay) && ( - + )} {!!destDisplay && ( - {destDisplay} + + {destDisplay} + )} )} @@ -543,13 +704,21 @@ const TrainInfoDetail: FC<{ )} {!!via_data && ( - + 経由: {via_data} )} {!!(start_date || end_date) && ( - + {start_date ?? ""}〜{end_date ?? ""} @@ -563,13 +732,21 @@ const TrainInfoDetail: FC<{ {!!uwasa && ( - + {uwasa} )} {!!optional_text && ( - + {optional_text} )} @@ -637,7 +814,11 @@ const SourceCard: FC = ({ {title} {label} - {sub && {sub}} + {sub && ( + + {sub} + + )} {detail && {detail}} diff --git a/components/Settings/DataSourceSettings.tsx b/components/Settings/DataSourceSettings.tsx index 2104144..17e403f 100644 --- a/components/Settings/DataSourceSettings.tsx +++ b/components/Settings/DataSourceSettings.tsx @@ -140,6 +140,7 @@ 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[] = [ diff --git a/lib/webViewInjectjavascript.ts b/lib/webViewInjectjavascript.ts index 322d41f..42e12c0 100644 --- a/lib/webViewInjectjavascript.ts +++ b/lib/webViewInjectjavascript.ts @@ -372,15 +372,27 @@ export const injectJavascriptData = ({ return null; } const results = []; - for (const entry of elesiteData) { + // 高松(left_station)側のユニットを先に表示 + // (heading_to === "left") === is_leading が true → 高松(left)端のユニット + const sortedElesiteData = elesiteData.slice().sort((a, b) => { + const aT = a.trains && a.trains.find(t => t.train_number === trainNumber); + const bT = b.trains && b.trains.find(t => t.train_number === trainNumber); + const aNav = aT && aT.nav; + const bNav = bT && bT.nav; + const aIsLeft = (aNav && aNav.heading_to === 'left') === (aNav && aNav.is_leading === true); + const bIsLeft = (bNav && bNav.heading_to === 'left') === (bNav && bNav.is_leading === true); + if (aIsLeft === bIsLeft) return 0; + return aIsLeft ? -1 : 1; + }); + for (const entry of sortedElesiteData) { if (!entry.trains) continue; const found = entry.trains.find(train => train.train_number === trainNumber); if (!found) continue; - // formation_config.units 優先、なければ formations + // units が1件以上ある場合のみ編成名を返す(空 units は報告なし扱い) const units = entry.formation_config && entry.formation_config.units; const formText = (units && units.length) ? units.map(u => u.formation).join('+') - : (entry.formations && entry.formations.trim() !== '' ? entry.formations : null); + : null; if (formText) results.push(formText); } return results.length > 0 ? results.join(', ') : null; diff --git a/stateBox/useElesite.tsx b/stateBox/useElesite.tsx index 9ed38dd..35120ee 100644 --- a/stateBox/useElesite.tsx +++ b/stateBox/useElesite.tsx @@ -68,15 +68,26 @@ export const useElesite = (): ElesiteHook => { const results: string[] = []; - for (const entry of elesiteData) { + // 高松(left_station)側のユニットを先に表示 + // (heading_to === "left") === is_leading が true → 高松(left)端のユニット + const sortedEntries = [...elesiteData].sort((a, b) => { + const aNav = a.trains?.find(t => t.train_number === trainNumber)?.nav; + const bNav = b.trains?.find(t => t.train_number === trainNumber)?.nav; + const aIsLeft = (aNav?.heading_to === "left") === (aNav?.is_leading === true); + const bIsLeft = (bNav?.heading_to === "left") === (bNav?.is_leading === true); + if (aIsLeft === bIsLeft) return 0; + return aIsLeft ? -1 : 1; + }); + + for (const entry of sortedEntries) { if (!entry.trains) continue; const found = entry.trains.find(train => train.train_number === trainNumber); if (!found) continue; - // formation_config.units があればそちらを優先 + // units が1件以上ある場合のみ編成名を返す(空 units は報告なし扱い) const units = entry.formation_config?.units; const formText = units?.length ? units.map(u => u.formation).join('+') - : (entry.formations?.trim() || null); + : null; if (formText) results.push(formText); }