Compare commits

...

3 Commits

Author SHA1 Message Date
harukin-expo-dev-env daf261b4f8 Merge commit '50fa4de9196adbb86313acadbe403d9a64af585a' into develop 2026-06-23 17:12:31 +00:00
harukin-expo-dev-env 50fa4de919 アイコンステータスの表示改善 2026-06-23 17:11:28 +00:00
harukin-expo-dev-env ab2061b2bd unyohubアイコン対応 2026-06-23 16:40:44 +00:00
16 changed files with 517 additions and 217 deletions
@@ -17,6 +17,7 @@ import { useUnyohub } from "@/stateBox/useUnyohub";
import { useElesite } from "@/stateBox/useElesite";
import { useThemeColors } from "@/lib/theme";
import { useResponsive } from "@/lib/responsive";
import { normalizeIconDisplayMode } from "@/lib/iconDisplayMode";
type Props = {
data: { trainNum: string; limited: string };
@@ -47,11 +48,11 @@ export const HeaderText: FC<Props> = ({
from,
scrollRef,
}) => {
const { limited, trainNum } = data;
const { trainNum } = data;
const { fixed } = useThemeColors();
const { fontScale } = useResponsive();
const { updatePermission } = useTrainMenu();
const { iconSetting } = useTrainMenu();
const { allCustomTrainData, getTodayOperationByTrainId } =
useAllTrainDiagram();
const { expoPushToken } = useNotification();
@@ -63,15 +64,14 @@ export const HeaderText: FC<Props> = ({
} = useUnyohub();
const { getElesiteEntriesByTrainNumber, useElesite: elesiteEnabled } =
useElesite();
const iconDisplayMode = normalizeIconDisplayMode(iconSetting);
// 追加ソースのON/OFFをここで管理(将来ソースが増えたらここに足す)
const additionalSources = {
unyohub: unyohubEnabled,
elesite: elesiteEnabled,
};
const hasAdditionalSources = Object.values(additionalSources).some(Boolean);
// 列車名、種別、フォントの取得
const [
typeName,
trainName,
@@ -133,7 +133,7 @@ export const HeaderText: FC<Props> = ({
case to_data && to_data !== "":
return [
typeString,
to_data + "行き",
`${to_data}行き`,
fontAvailable,
isOneMan,
infogram,
@@ -147,7 +147,7 @@ export const HeaderText: FC<Props> = ({
return [
typeString,
migrateTrainName(
trainData[trainData.length - 1].split(",")[0] + "行き",
`${trainData[trainData.length - 1].split(",")[0]}行き`,
),
fontAvailable,
isOneMan,
@@ -161,7 +161,7 @@ export const HeaderText: FC<Props> = ({
}
}, [trainData, trainNum, allCustomTrainData]);
const allTodayOperation = getTodayOperationByTrainId(trainNum);
const allTodayOperation = getTodayOperationByTrainId(trainNum) ?? [];
const todayOperation = allTodayOperation.filter((d) => d.state !== 100);
let iconTrainDirection =
@@ -198,7 +198,6 @@ export const HeaderText: FC<Props> = ({
: getUnyohubEntriesByTrainNumber(unyohubTrainNumForSourceScreen);
const elesiteEntries = getElesiteEntriesByTrainNumber(trainNum);
// 車番(formations) がある場合のみ「運用Hub情報あり」と判定
const hasUnyohubFormation = unyohubEntries.some(
(e) => !!e.formations && e.formations.trim() !== "",
);
@@ -208,12 +207,21 @@ export const HeaderText: FC<Props> = ({
const hasExtraInfo =
priority > 200 ||
todayOperation?.length > 0 ||
todayOperation.length > 0 ||
hasUnyohubFormation ||
hasElesiteFormation;
const [isWrapped, setIsWrapped] = useState(false);
const openTrainInfoUrl = () => {
if (!trainInfoUrl) return;
const uri = trainInfoUrl.includes("pdf")
? getPDFViewURL(trainInfoUrl)
: trainInfoUrl;
navigate("generalWebView", { uri, useExitButton: true });
SheetManager.hide("EachTrainInfo");
};
return (
<View
style={{
@@ -232,6 +240,7 @@ export const HeaderText: FC<Props> = ({
from={from}
todayOperation={todayOperation}
direction={iconTrainDirection}
iconDisplayMode={iconDisplayMode}
/>
<View
@@ -270,26 +279,21 @@ export const HeaderText: FC<Props> = ({
}
: {}),
}}
onPress={() => {
if (!trainInfoUrl) return;
const uri = trainInfoUrl.includes("pdf")
? getPDFViewURL(trainInfoUrl)
: trainInfoUrl;
navigate("generalWebView", { uri, useExitButton: true });
SheetManager.hide("EachTrainInfo");
}}
onPress={openTrainInfoUrl}
disabled={!trainInfoUrl}
>
<Text
style={{
...textConfig,
color: fixed.textOnPrimary,
...(trainName.length > 10 ? { fontSize: fontScale(16) } : { fontSize: fontScale(17) }),
...(trainName.length > 10
? { fontSize: fontScale(16) }
: { fontSize: fontScale(17) }),
flexShrink: 1,
}}
onTextLayout={(e) => {
if (e.nativeEvent.lines.length > 1) setIsWrapped(true);
}}
if (e.nativeEvent.lines.length > 1) setIsWrapped(true);
}}
>
{trainName}
</Text>
@@ -328,7 +332,6 @@ export const HeaderText: FC<Props> = ({
},
});
} else {
// 追加ソースが全てオフ → 元の挙動(直接 DB ページを開く)
const uri = `https://jr-shikoku-data-system.pages.dev/trainData/${trainNum}?userID=${expoPushToken}&from=eachTrainInfo`;
navigate("generalWebView", { uri, useExitButton: false });
SheetManager.hide("EachTrainInfo");
@@ -3,13 +3,14 @@ import { Text } from "react-native";
import { useThemeColors } from "@/lib/theme";
type props = {
infogram: string;
fontSize?: number;
}
export const InfogramText: FC<props> = ({infogram}) => {
export const InfogramText: FC<props> = ({infogram, fontSize = 20}) => {
const { fixed } = useThemeColors();
return (
<Text
style={{
fontSize: 20,
fontSize,
color: fixed.textOnPrimary,
fontFamily: "JNR-font",
}}
@@ -0,0 +1,98 @@
import React, { ComponentProps, FC } from "react";
import { Image, TouchableOpacity, View } from "react-native";
import { Ionicons } from "@expo/vector-icons";
import type { TrainIconEntry } from "@/lib/trainIconEntries";
type StackVariant = "header" | "list";
type StatusIcon = ComponentProps<typeof Ionicons>;
type Props = {
entries: TrainIconEntry[];
direction?: boolean;
hidden?: boolean;
onPressEntry?: (entry: TrainIconEntry, index: number) => void;
statusIcon?: StatusIcon;
variant?: StackVariant;
};
const iconSize = {
header: {
width: 24,
height: 30,
stackedWidth: 12,
stackedHeight: 15,
marginRight: 5,
stackedMarginLeft: -10,
stackedMarginTop: 10,
statusSize: 24,
},
list: {
width: 20,
height: 22,
stackedWidth: 10,
stackedHeight: 12,
marginRight: 2,
stackedMarginLeft: -8,
stackedMarginTop: 8,
statusSize: 18,
},
} as const;
export const TrainIconStack: FC<Props> = ({
entries,
direction,
hidden = false,
onPressEntry,
statusIcon,
variant = "header",
}) => {
const size = iconSize[variant];
return (
<View style={{ flexDirection: "row", alignItems: "flex-start" }}>
{entries.map((entry, index) => {
const trainIcon = direction
? entry.vehicle_info_img
: entry.vehicle_info_right_img || entry.vehicle_info_img;
if (!trainIcon) return null;
const content = (
<View>
<View style={{ opacity: hidden ? 0 : 1 }}>
<Image
source={{ uri: trainIcon }}
style={{
height: index > 0 ? size.stackedHeight : size.height,
width: index > 0 ? size.stackedWidth : size.width,
marginRight: size.marginRight,
marginLeft: index > 0 ? size.stackedMarginLeft : 0,
marginTop: index > 0 ? size.stackedMarginTop : 0,
}}
resizeMethod="resize"
/>
</View>
{statusIcon && hidden && (
<View style={{ position: "absolute", top: 0, left: 0 }}>
<Ionicons {...statusIcon} size={size.statusSize} />
</View>
)}
</View>
);
if (!onPressEntry) {
return <View key={`${trainIcon}-${index}`}>{content}</View>;
}
return (
<TouchableOpacity
key={`${trainIcon}-${index}`}
onPress={() => onPressEntry(entry, index)}
disabled={!entry.vehicle_info_url}
>
{content}
</TouchableOpacity>
);
})}
</View>
);
};
@@ -1,5 +1,4 @@
import React, { ComponentProps, FC, useEffect, useState } from "react";
import { View, Image, TouchableOpacity } from "react-native";
import React, { ComponentProps, FC, useEffect, useMemo, useState } from "react";
import { Ionicons } from "@expo/vector-icons";
import dayjs from "dayjs";
import { SheetManager } from "react-native-actions-sheet";
@@ -8,6 +7,9 @@ import { useAllTrainDiagram } from "@/stateBox/useAllTrainDiagram";
import { useInterval } from "@/lib/useInterval";
import type { NavigateFunction } from "@/types";
import { OperationLogs } from "@/lib/CommonTypes";
import type { IconDisplayMode } from "@/lib/iconDisplayMode";
import { resolveTrainIconEntries } from "@/lib/trainIconEntries";
import { TrainIconStack } from "./TrainIconStack";
type GlyphNames = ComponentProps<typeof Ionicons>["name"];
@@ -17,94 +19,54 @@ type Props = {
from: string;
todayOperation: OperationLogs[];
direction?: boolean;
iconDisplayMode: IconDisplayMode;
};
type apt = {
name: GlyphNames;
color: string;
};
export const TrainIconStatus: FC<Props> = (props) => {
const { data, navigate, from, todayOperation, direction } = props;
const {
data,
navigate,
from,
todayOperation,
direction,
iconDisplayMode,
} = props;
const [anpanmanStatus, setAnpanmanStatus] = useState<apt>();
const { allCustomTrainData } = useAllTrainDiagram();
const [trainIconData, setTrainIcon] = useState<
{ vehicle_info_img: string;vehicle_info_right_img: string; vehicle_info_url: string }[]
>([]);
const customTrainData = useMemo(
() => customTrainDataDetector(data.trainNum, allCustomTrainData),
[data.trainNum, allCustomTrainData],
);
const trainIconData = useMemo(
() =>
data.trainNum
? resolveTrainIconEntries({
trainNum: data.trainNum,
customTrainData,
todayOperation,
iconDisplayMode,
})
: [],
[data.trainNum, customTrainData, todayOperation, iconDisplayMode],
);
useEffect(() => {
if (!data.trainNum) return;
const { train_info_img: vehicle_info_img, vehicle_info_url } =
customTrainDataDetector(data.trainNum, allCustomTrainData);
if (todayOperation.length !== 0) {
const returnData =
todayOperation
.sort((a, b) => {
// trainIdからカンマ以降の数字を抽出する関数
const extractOrderNumber = (trainId: string): number => {
const parts = trainId.split(',');
if (parts.length > 1) {
const num = parseInt(parts[1].trim(), 10);
return isNaN(num) ? Infinity : num;
}
return Infinity; // カンマなし = 末尾に移動
};
// data.trainNumと一致するtrainIdを探す関数
const findMatchingTrainId = (operation: OperationLogs): string | null => {
const allTrainIds = [
...(operation.train_ids || []),
...(operation.related_train_ids || []),
];
// data.trainNumの接頭辞と一致するものを探す
for (const trainId of allTrainIds) {
const prefix = trainId.split(',')[0]; // カンマ前の部分
if (prefix === data.trainNum) {
return trainId;
}
}
return null;
};
const aTrainId = findMatchingTrainId(a);
const bTrainId = findMatchingTrainId(b);
// マッチしたものがない場合は元の順序を保持
if (!aTrainId || !bTrainId) {
return aTrainId ? -1 : bTrainId ? 1 : 0;
}
const aOrder = extractOrderNumber(aTrainId);
const bOrder = extractOrderNumber(bTrainId);
return aOrder - bOrder;
})
.map((op) => ({
vehicle_info_img: op.vehicle_img || vehicle_info_img,
vehicle_info_right_img: op.vehicle_img_right || op.vehicle_img || vehicle_info_img,
vehicle_info_url: op.vehicle_info_url,
})) || [];
setTrainIcon(returnData);
} else if (vehicle_info_img) {
setTrainIcon([{ vehicle_info_img, vehicle_info_right_img: vehicle_info_img, vehicle_info_url }]);
}
// アンパンマンステータスAPIのエンドポイント判定
let anpanmanApiPath: string | null = null;
switch (data.trainNum) {
// 予讃線 → yosan-anpanman
// しおかぜ 8000 アンパン
case "10M":
case "22M":
case "9M":
case "21M":
// いしづち 8000 アンパン
case "1010M":
case "1022M":
case "1009M":
case "1021M":
// いしづち 三桁 アンパン
case "1041M":
case "1044M":
// 宇和海 2000 アンパン
case "1058D":
case "1066D":
case "1074D":
@@ -113,8 +75,6 @@ export const TrainIconStatus: FC<Props> = (props) => {
case "1067D":
anpanmanApiPath = "yosan-anpanman";
break;
// 土讃線 → dosan-anpanman
// 南風 2700 アンパン
case "32D":
case "36D":
case "44D":
@@ -132,12 +92,12 @@ export const TrainIconStatus: FC<Props> = (props) => {
fetch(
`https://n8n.haruk.in/webhook/${anpanmanApiPath}?trainNum=${
data.trainNum
}&month=${dayjs().format("M")}&day=${dayjs().format("D")}`,{ cache: "no-store" }
}&month=${dayjs().format("M")}&day=${dayjs().format("D")}`,
{ cache: "no-store" },
)
.then((d) => d.json())
.then((d) => {
if (d.trainStatus == "" || d.trainStatus == "○") {
//setAnpanmanStatus({name:"checkmark-circle-outline",color:"blue"});
} else if (d.trainStatus == "▲") {
setAnpanmanStatus({ name: "warning-outline", color: "yellow" });
} else if (d.trainStatus == "×") {
@@ -146,11 +106,8 @@ export const TrainIconStatus: FC<Props> = (props) => {
})
.catch(() => {});
}
}, [data.trainNum, allCustomTrainData, todayOperation]);
}, [data.trainNum, allCustomTrainData, todayOperation, iconDisplayMode]);
// JSスレッドでの点滅(useInterval + useState
// reanimated の withRepeat はUIスレッドで毎フレーム更新し続けるため
// ActionSheetのスプリングアニメーションと競合する
const [showIcon, setShowIcon] = useState(false);
useInterval(() => {
if (anpanmanStatus) {
@@ -159,47 +116,18 @@ export const TrainIconStatus: FC<Props> = (props) => {
}, 1000, !!anpanmanStatus);
return (
<>
{trainIconData.map(
({ vehicle_info_img: trainIcon, vehicle_info_right_img: trainIconRight, vehicle_info_url: address }, index) => (
<TouchableOpacity
key={`${trainIcon}-${index}`}
onPress={() => {
navigate("howto", {
info: address,
goTo: from == "LED" ? "menu" : from,
});
SheetManager.hide("EachTrainInfo");
}}
disabled={!address}
>
<View>
<View style={{ opacity: anpanmanStatus && showIcon ? 0 : 1 }}>
<Image
source={{ uri: direction ? trainIcon : trainIconRight || trainIcon }}
style={{
height: index > 0 ? 15 : 30,
width: index > 0 ? 12 : 24,
marginRight: 5,
marginLeft: index > 0 ? -10 : 0,
marginTop: index > 0 ? 10 : 0,
}}
resizeMethod="resize"
/>
</View>
{anpanmanStatus && showIcon && (
<View style={{ position: "absolute", top: 0, left: 0 }}>
<Ionicons
{...anpanmanStatus}
size={24}
style={{ marginRight: 5 }}
/>
</View>
)}
</View>
</TouchableOpacity>
)
)}
</>
<TrainIconStack
entries={trainIconData}
direction={direction}
hidden={!!anpanmanStatus && showIcon}
statusIcon={anpanmanStatus}
onPressEntry={(entry) => {
navigate("howto", {
info: entry.vehicle_info_url,
goTo: from == "LED" ? "menu" : from,
});
SheetManager.hide("EachTrainInfo");
}}
/>
);
};
+56 -39
View File
@@ -8,7 +8,6 @@ import {
TextInput,
ScrollView,
Linking,
Image,
} from "react-native";
import { useAllTrainDiagram } from "../stateBox/useAllTrainDiagram";
import { useBottomTabBarHeight } from "@react-navigation/bottom-tabs";
@@ -24,10 +23,17 @@ import { Switch } from "@rneui/themed";
import { migrateTrainName } from "@/lib/eachTrainInfoCoreLib/migrateTrainName";
import { OneManText } from "./ActionSheetComponents/EachTrainInfoCore/HeaderTextParts/OneManText";
import { getStringConfig } from "@/lib/getStringConfig";
import { useTrainMenu } from "@/stateBox/useTrainMenu";
import { normalizeIconDisplayMode } from "@/lib/iconDisplayMode";
import { InfogramText } from "./ActionSheetComponents/EachTrainInfoCore/HeaderTextParts/InfogramText";
import { resolveTrainIconEntries } from "@/lib/trainIconEntries";
import { TrainIconStack } from "./ActionSheetComponents/EachTrainInfoCore/TrainIconStack";
export const AllTrainDiagramView: FC = () => {
const { colors, fixed } = useThemeColors();
const { goBack, navigate } = useNavigation<any>();
const { iconSetting } = useTrainMenu();
const iconDisplayMode = normalizeIconDisplayMode(iconSetting);
const tabBarHeight = useBottomTabBarHeight();
const {
keyList,
@@ -80,9 +86,27 @@ export const AllTrainDiagramView: FC = () => {
openTrainInfo: (d: string) => void;
};
const Item: FC<ItemProps> = ({ id, openTrainInfo }) => {
const { train_info_img, train_name, type, train_num_distance, to_data } =
customTrainDataDetector(id, allCustomTrainData);
const todayOperation = getTodayOperationByTrainId(id).filter(d=> d.state !== 100);
const customTrainData = customTrainDataDetector(id, allCustomTrainData);
const {
train_name,
type,
train_num_distance,
to_data,
infogram,
directions,
} = customTrainData;
const todayOperation = (getTodayOperationByTrainId(id) ?? []).filter(d=> d.state !== 100);
const trainIconData = resolveTrainIconEntries({
trainNum: id,
customTrainData,
todayOperation,
iconDisplayMode,
});
let iconTrainDirection =
parseInt(id.replace(/[^\d]/g, "")) % 2 == 0 ? true : false;
if (directions != undefined) {
iconTrainDirection = directions ? true : false;
}
const [isWrapped, setIsWrapped] = useState(false);
const [typeString, fontAvailable, isOneMan] = getStringConfig(type, id);
@@ -124,29 +148,11 @@ export const AllTrainDiagramView: FC = () => {
onPress={() => openTrainInfo(id)}
>
<View style={{ marginHorizontal: 5, flexDirection: "row" }}>
{todayOperation.length > 0
? todayOperation.map((operation, index) => (
<Image
key={index}
source={{ uri: operation.vehicle_img || train_info_img }}
style={{
width: 20,
height: 22,
marginHorizontal: 2,
display: index == 0 ? "flex" : "none", //暫定対応:複数アイコンがある場合は最初のアイコンのみ表示
}}
/>
))
: train_info_img && (
<Image
source={{ uri: train_info_img }}
style={{
width: 20,
height: 22,
marginHorizontal: 2,
}}
/>
)}
<TrainIconStack
entries={trainIconData}
direction={iconTrainDirection}
variant="list"
/>
</View>
<View
@@ -173,22 +179,33 @@ export const AllTrainDiagramView: FC = () => {
)}
{isOneMan && <OneManText />}
</View>
{trainNameString && (
<Text
{(trainNameString || infogram) && (
<View
style={{
fontSize: 20,
fontWeight: "bold",
color: fixed.textOnPrimary,
flexDirection: "row",
alignItems: "center",
flexShrink: 1,
}}
onTextLayout={(e) => {
if (e.nativeEvent.lines.length > 1) {
setIsWrapped(true);
}
}}
>
{trainNameString}
</Text>
{trainNameString && (
<Text
style={{
fontSize: 20,
fontWeight: "bold",
color: fixed.textOnPrimary,
flexShrink: 1,
}}
onTextLayout={(e) => {
if (e.nativeEvent.lines.length > 1) {
setIsWrapped(true);
}
}}
>
{trainNameString}
</Text>
)}
{infogram ? <InfogramText infogram={infogram} fontSize={18} /> : null}
</View>
)}
<View style={{ flex: 1 }} />
<Text style={{ fontSize: 20, fontWeight: "bold", color: fixed.textOnPrimary }}>
@@ -26,6 +26,8 @@ import { Ionicons } from "@expo/vector-icons";
import dayjs from "dayjs";
import { useTrainMenu } from "@/stateBox/useTrainMenu";
import { useThemeColors } from "@/lib/theme";
import { normalizeIconDisplayMode } from "@/lib/iconDisplayMode";
import { resolveTrainDataIcon } from "@/lib/trainDataIcon";
import {
startTrainFollowActivity,
updateTrainFollowActivity,
@@ -48,8 +50,9 @@ export const FixedTrain: FC<props> = ({ trainID }) => {
setFixedPositionSize,
} = useCurrentTrain();
const { mapSwitch } = useTrainMenu();
const { mapSwitch, iconSetting } = useTrainMenu();
const { allCustomTrainData, allTrainDiagram } = useAllTrainDiagram();
const iconDisplayMode = normalizeIconDisplayMode(iconSetting);
const [liveNotifyId, setLiveNotifyId] = useState<string | null>(null);
const liveNotifyIdRef = useRef<string | null>(null);
@@ -59,11 +62,12 @@ export const FixedTrain: FC<props> = ({ trainID }) => {
const [customData, setCustomData] = useState<CustomTrainData>(
getCurrentTrainData(trainID, currentTrain, allCustomTrainData)
);
const customTrainIcon = resolveTrainDataIcon(customData, iconDisplayMode);
useEffect(() => {
setCustomData(
getCurrentTrainData(trainID, currentTrain, allCustomTrainData)
);
}, [currentTrain, trainID]);
}, [currentTrain, trainID, allCustomTrainData]);
useEffect(() => {
const stationData = getCurrentStationData(trainID);
if (stationData) {
@@ -555,7 +559,7 @@ export const FixedTrain: FC<props> = ({ trainID }) => {
}}
>
<Image
source={{ uri: customData.train_info_img || "" }}
source={{ uri: customTrainIcon }}
width={fixedPositionSize === 226 ? 23 : 14}
height={fixedPositionSize === 226 ? 26 : 17}
style={{ margin: 5 }}
+28 -9
View File
@@ -32,15 +32,34 @@ export const LayoutSettings = ({
<SheetHeaderItem title="レイアウト設定" LeftItem={{ title: " 設定", onPress: goBack }} />
<ScrollView style={{ flex: 1, backgroundColor: colors.background }}>
<View style={{ flex: 1 }}>
<SwitchArea
str="列車アイコン表示"
bool={iconSetting}
setBool={setIconSetting}
falseImage={require("../../assets/configuration/icon_default.jpg")}
trueImage={require("../../assets/configuration/icon_original.jpg")}
falseText={"本家\n(文字アイコン)"}
trueText={"オリジナル\n(車種アイコン)"}
/>
<View
style={{
backgroundColor: "#00000010",
borderRadius: 10,
margin: 5,
}}
>
<TripleSwitchArea
str="列車アイコン表示"
bool={iconSetting}
setBool={setIconSetting}
firstItem={{
firstImage: require("../../assets/configuration/icon_default.jpg"),
firstText: "本家\n(文字アイコン)",
firstValue: "default",
}}
secondItem={{
secondImage: require("../../assets/configuration/icon_original.jpg"),
secondText: "オリジナル\n(車種アイコン)",
secondValue: "original",
}}
thirdItem={{
thirdImage: require("../../assets/relationLogo/unyohub_logo.webp"),
thirdText: "鉄道運用Hub\n(Hubアイコン)",
thirdValue: "hub",
}}
/>
</View>
<SwitchArea
str="列車表示"
bool={uiSetting}
+8 -2
View File
@@ -24,13 +24,17 @@ import { LauncherIconSettings } from "./LauncherIconSettings";
import { DataSourceSettings } from "./DataSourceSettings";
import { FelicaHistoryPage } from "./FelicaHistoryPage";
import { SoundSettings } from "./SoundSettings";
import {
normalizeIconDisplayMode,
type IconDisplayMode,
} from "@/lib/iconDisplayMode";
const Stack = createStackNavigator();
export default function Setting(props) {
const {
navigation: { navigate },
} = props;
const [iconSetting, setIconSetting] = useState(false);
const [iconSetting, setIconSetting] = useState<IconDisplayMode>("original");
const [mapSwitch, setMapSwitch] = useState(false);
const [stationMenu, setStationMenu] = useState(false);
const [usePDFView, setUsePDFView] = useState(false);
@@ -40,7 +44,9 @@ export default function Setting(props) {
const [startPage, setStartPage] = useState(false);
const [uiSetting, setUiSetting] = useState("tokyo");
useLayoutEffect(() => {
AS.getItem(STORAGE_KEYS.ICON_SWITCH).then(setIconSetting);
AS.getItem(STORAGE_KEYS.ICON_SWITCH).then((value) =>
setIconSetting(normalizeIconDisplayMode(value)),
);
AS.getItem(STORAGE_KEYS.MAP_SWITCH).then(setMapSwitch);
AS.getItem(STORAGE_KEYS.STATION_SWITCH).then(setStationMenu);
AS.getItem(STORAGE_KEYS.USE_PDF_VIEW).then(setUsePDFView);
+3
View File
@@ -45,6 +45,7 @@ export type CustomTrainData = {
type: trainTypeID;
train_name: string;
train_info_img: string;
train_info_img_hub?: string | null;
train_info_url: string;
infogram: string;
via_data: string;
@@ -96,6 +97,8 @@ export type OperationLogs = {
unit_ids?: string[];
vehicle_img: string;
vehicle_img_right: string;
vehicle_img_hub?: string | null;
vehicle_img_right_hub?: string | null;
vehicle_info_url: string;
related_train_ids?: string[];
state: number | null;
+17
View File
@@ -0,0 +1,17 @@
export type IconDisplayMode = "default" | "original" | "hub";
export const normalizeIconDisplayMode = (
value: unknown,
): IconDisplayMode => {
if (value === "hub") return "hub";
if (value === "default" || value === "false" || value === false) {
return "default";
}
return "original";
};
export const isHubIconDisplayMode = (value: unknown): boolean =>
normalizeIconDisplayMode(value) === "hub";
export const usesCustomTrainIcons = (value: unknown): boolean =>
normalizeIconDisplayMode(value) !== "default";
+60
View File
@@ -0,0 +1,60 @@
import type { OperationLogs } from "@/lib/CommonTypes";
import {
normalizeIconDisplayMode,
type IconDisplayMode,
} from "@/lib/iconDisplayMode";
type OperationIconPair = {
forward: string;
rear: string;
};
export const resolveOperationIconPair = (
operation: Pick<
OperationLogs,
| "vehicle_img"
| "vehicle_img_right"
| "vehicle_img_hub"
| "vehicle_img_right_hub"
>,
iconDisplayMode: IconDisplayMode,
fallback = "",
): OperationIconPair => {
if (iconDisplayMode === "hub") {
return {
forward: operation.vehicle_img_hub || operation.vehicle_img || fallback,
rear:
operation.vehicle_img_right_hub ||
operation.vehicle_img_hub ||
operation.vehicle_img_right ||
operation.vehicle_img ||
fallback,
};
}
return {
forward: operation.vehicle_img || fallback,
rear: operation.vehicle_img_right || operation.vehicle_img || fallback,
};
};
export const resolveDirectionalOperationIcon = (
operation: Pick<
OperationLogs,
| "vehicle_img"
| "vehicle_img_right"
| "vehicle_img_hub"
| "vehicle_img_right_hub"
>,
iconDisplayModeValue: unknown,
direction: boolean,
fallback = "",
): string => {
const iconDisplayMode = normalizeIconDisplayMode(iconDisplayModeValue);
const { forward, rear } = resolveOperationIconPair(
operation,
iconDisplayMode,
fallback,
);
return direction ? forward || rear : rear || forward;
};
+28
View File
@@ -0,0 +1,28 @@
import type { CustomTrainData } from "@/lib/CommonTypes";
import {
normalizeIconDisplayMode,
type IconDisplayMode,
} from "@/lib/iconDisplayMode";
export const resolveTrainDataIcon = (
trainData: Pick<CustomTrainData, "train_info_img" | "train_info_img_hub">,
iconDisplayMode: IconDisplayMode,
fallback = "",
): string => {
if (iconDisplayMode === "hub") {
return trainData.train_info_img_hub || trainData.train_info_img || fallback;
}
return trainData.train_info_img || fallback;
};
export const resolveTrainDataIconFromValue = (
trainData: Pick<CustomTrainData, "train_info_img" | "train_info_img_hub">,
iconDisplayModeValue: unknown,
fallback = "",
): string =>
resolveTrainDataIcon(
trainData,
normalizeIconDisplayMode(iconDisplayModeValue),
fallback,
);
+97
View File
@@ -0,0 +1,97 @@
import type { CustomTrainData, OperationLogs } from "@/lib/CommonTypes";
import type { IconDisplayMode } from "@/lib/iconDisplayMode";
import { resolveOperationIconPair } from "@/lib/operationLogIcon";
import { resolveTrainDataIcon } from "@/lib/trainDataIcon";
export type TrainIconEntry = {
vehicle_info_img: string;
vehicle_info_right_img: string;
vehicle_info_url: string;
};
type TrainDataForIcon = Pick<
CustomTrainData,
"train_info_img" | "train_info_img_hub" | "vehicle_info_url"
>;
const extractOrderNumber = (trainId: string): number => {
const parts = trainId.split(",");
if (parts.length <= 1) return Infinity;
const num = parseInt(parts[1].trim(), 10);
return isNaN(num) ? Infinity : num;
};
const findMatchingTrainId = (
operation: OperationLogs,
trainNum: string,
): string | null => {
const allTrainIds = [
...(operation.train_ids || []),
...(operation.related_train_ids || []),
];
for (const trainId of allTrainIds) {
const prefix = trainId.split(",")[0];
if (prefix === trainNum) return trainId;
}
return null;
};
export const sortOperationsForTrainIcon = (
operations: OperationLogs[],
trainNum: string,
): OperationLogs[] =>
[...operations].sort((a, b) => {
const aTrainId = findMatchingTrainId(a, trainNum);
const bTrainId = findMatchingTrainId(b, trainNum);
if (!aTrainId || !bTrainId) {
return aTrainId ? -1 : bTrainId ? 1 : 0;
}
return extractOrderNumber(aTrainId) - extractOrderNumber(bTrainId);
});
export const resolveTrainIconEntries = ({
trainNum,
customTrainData,
todayOperation,
iconDisplayMode,
}: {
trainNum: string;
customTrainData: TrainDataForIcon;
todayOperation: OperationLogs[];
iconDisplayMode: IconDisplayMode;
}): TrainIconEntry[] => {
const fallbackIcon = resolveTrainDataIcon(customTrainData, iconDisplayMode);
if (todayOperation.length > 0) {
return sortOperationsForTrainIcon(todayOperation, trainNum)
.map((operation) => {
const { forward, rear } = resolveOperationIconPair(
operation,
iconDisplayMode,
fallbackIcon,
);
return {
vehicle_info_img: forward,
vehicle_info_right_img: rear,
vehicle_info_url: operation.vehicle_info_url,
};
})
.filter((entry) => entry.vehicle_info_img || entry.vehicle_info_right_img);
}
if (!fallbackIcon) return [];
return [
{
vehicle_info_img: fallbackIcon,
vehicle_info_right_img: fallbackIcon,
vehicle_info_url: customTrainData.vehicle_info_url || "",
},
];
};
+32 -19
View File
@@ -3,6 +3,11 @@ import { TRAIN_TYPE_CONFIG } from './webview/trainTypeConfig';
import { TRAIN_ICON_MAP, TRAIN_ICON_REGEX } from './webview/trainIconMap';
import { STATION_DATA } from './webview/stationData';
import { generateXhrInterceptorJs, MockApiConfig } from './mockApi/webviewXhrInterceptor';
import {
isHubIconDisplayMode,
normalizeIconDisplayMode,
usesCustomTrainIcons,
} from './iconDisplayMode';
import dosan from '@/assets/originData/dosan';
import dosan2 from '@/assets/originData/dosan2';
import koutoku from '@/assets/originData/koutoku';
@@ -16,7 +21,7 @@ import yosan from '@/assets/originData/yosan';
export interface InjectJavascriptOptions {
/** 地図スイッチ ("true" | "false") */
mapSwitch: string;
/** 列車アイコン表示スイッチ ("true" | "false") */
/** 列車アイコン表示モード ("default" | "original" | "hub") */
iconSetting: string;
/** 駅メニュースイッチ ("true" | "false") */
stationMenu: string;
@@ -63,6 +68,9 @@ export const injectJavascriptData = ({
diagramTodayUrl,
mockApiConfig,
}: InjectJavascriptOptions): string => {
const iconDisplayMode = normalizeIconDisplayMode(iconSetting);
const preferHubTrainIcon = isHubIconDisplayMode(iconDisplayMode);
const useVehicleIcons = usesCustomTrainIcons(iconDisplayMode);
const MARINE_STATION_SEQUENCE = [
"岡山", "大元", "備前西市", "妹尾", "早島", "茶屋町", "植松", "木見", "上の町",
"児島", "坂出", "鴨川", "国分", "端岡", "鬼無", "高松",
@@ -626,17 +634,22 @@ export const injectJavascriptData = ({
`;
// 左か右かを判定してアイコンを設置する
const trainIcon = `
const _preferHubTrainIcon = ${preferHubTrainIcon};
const setStationIcon = (setIconElem,img,hasProblem,backCount = 100) =>{
const position = setIconElem.getAttribute("style").includes("left");
const isHubDataset = _preferHubTrainIcon && typeof img === "string" && img.includes("unyohub-icon-dataset/");
let marginData = ${uiSetting === "tokyo" ? `"5px"`: `"2px"`};
let backgroundColor = "transparent";
let heightData = "22px";
let widthData = isHubDataset ? "18px" : "auto";
let transformData = isHubDataset ? "scale(1.15)" : "none";
if(backCount == 0){
marginData = position ? ${uiSetting === "tokyo" ? `"0px 0px -10px 0px" : "-10px 0px 0px 0px"`: `"0px 2px 0px 0px" : "0px 2px 0px 0px"`};
heightData = "16px";
widthData = isHubDataset ? "13px" : "auto";
}
setIconElem.insertAdjacentHTML('beforebegin', "<img src="+img+" style='float:"+(position ? 'left' : 'right')+";height:"+heightData+";margin: "+marginData+";background-color: "+backgroundColor+";'>");
setIconElem.insertAdjacentHTML('beforebegin', "<img src="+img+" style='float:"+(position ? 'left' : 'right')+";height:"+heightData+";width:"+widthData+";object-fit:contain;transform:"+transformData+";transform-origin:center center;margin: "+marginData+";background-color: "+backgroundColor+";'>");
if (backCount == 0 || backCount == 100) setIconElem.remove();
}
@@ -650,6 +663,16 @@ export const injectJavascriptData = ({
if (new RegExp(pattern).test(列番データ)) return url;
}
};
const pickOperationTrainIcon = (operation, direction) => {
const forward = _preferHubTrainIcon
? (operation.vehicle_img_hub || operation.vehicle_img)
: operation.vehicle_img;
const rear = _preferHubTrainIcon
? (operation.vehicle_img_right_hub || operation.vehicle_img_hub || operation.vehicle_img_right || operation.vehicle_img)
: (operation.vehicle_img_right || operation.vehicle_img);
return direction ? (forward || rear) : (rear || forward);
};
`;
const normal_train_name = `
@@ -1159,7 +1182,7 @@ const setStrings = () =>{
var TrainType = undefined;
setTrainMenuDialog(element)
${iconSetting == "true" ? `
${useVehicleIcons ? `
let trainIconUrl = [];
operationList
.sort((a,b)=>sortOperationalList(a,b,列番データ.toString()))
@@ -1173,14 +1196,8 @@ const setStrings = () =>{
if(directions != null && directions != undefined){
iconTrainDirection = directions ? true : false;
}
if(iconTrainDirection){
if(e.vehicle_img) trainIconUrl.push(e.vehicle_img);
else if(e.vehicle_img_right) trainIconUrl.push(e.vehicle_img_right);
}else{
if(e.vehicle_img_right) trainIconUrl.push(e.vehicle_img_right);
else if(e.vehicle_img) trainIconUrl.push(e.vehicle_img);
}
const iconUrl = pickOperationTrainIcon(e, iconTrainDirection);
if(iconUrl) trainIconUrl.push(iconUrl);
}
} else if (e.related_train_ids?.length > 0) {
const trainIds = e.related_train_ids.map(
@@ -1192,13 +1209,8 @@ const setStrings = () =>{
if(directions != null && directions != undefined){
iconTrainDirection = directions ? true : false;
}
if(iconTrainDirection){
if(e.vehicle_img) trainIconUrl.push(e.vehicle_img);
else if(e.vehicle_img_right) trainIconUrl.push(e.vehicle_img_right);
}else{
if(e.vehicle_img_right) trainIconUrl.push(e.vehicle_img_right);
else if(e.vehicle_img) trainIconUrl.push(e.vehicle_img);
}
const iconUrl = pickOperationTrainIcon(e, iconTrainDirection);
if(iconUrl) trainIconUrl.push(iconUrl);
}
}
});
@@ -1212,7 +1224,8 @@ const setStrings = () =>{
}
else{
if(trainDataList.find(e => e.train_id === 列番データ) !== undefined){
const trainIconUrl = [trainDataList.find(e => e.train_id === 列番データ).train_info_img];
const trainData = trainDataList.find(e => e.train_id === 列番データ);
const trainIconUrl = [_preferHubTrainIcon ? (trainData.train_info_img_hub || trainData.train_info_img) : trainData.train_info_img];
if(trainIconUrl.length > 0){
trainIconUrl.forEach((url,index,array) => {
if(url && url != ""){
+1 -1
View File
@@ -24,7 +24,7 @@ const noUserscript = args.includes('--no-userscript');
const options = {
mapSwitch: 'false',
iconSetting: 'true',
iconSetting: 'original',
stationMenu: 'false',
trainMenu: 'false',
uiSetting: (process.env.UI ?? 'tokyo') as string,
+9 -3
View File
@@ -37,6 +37,10 @@ import {
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,
@@ -124,7 +128,9 @@ export const TrainMenuProvider: FC<props> = ({ children }) => {
}, []);
type boolType = "true" | "false" | undefined;
//画面表示関連
const [iconSetting, setIconSetting] = useState<boolType>(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);
@@ -335,8 +341,8 @@ export const TrainMenuProvider: FC<props> = ({ children }) => {
//列車アイコンスイッチ
ASCore({
k: STORAGE_KEYS.ICON_SWITCH,
s: setIconSetting,
d: "true",
s: (value) => setIconSetting(normalizeIconDisplayMode(value)),
d: "original",
u: true,
});
//地図スイッチ