Files
jrshikoku/components/Settings/DataSourceSettings.tsx

799 lines
24 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 React, { useState, useEffect } from "react";
import {
View,
Text,
ScrollView,
StyleSheet,
Image,
TouchableOpacity,
Linking,
} from "react-native";
import { Switch } from "@rneui/themed";
import { useNavigation } from "@react-navigation/native";
import { MaterialCommunityIcons } from "@expo/vector-icons";
import { SheetHeaderItem } from "@/components/atom/SheetHeaderItem";
import { AS } from "../../storageControl";
import { STORAGE_KEYS } from "@/constants";
import { useTrainMenu } from "@/stateBox/useTrainMenu";
import { useThemeColors } from "@/lib/theme";
import {
DEFAULT_JR_DATA_SYSTEM_ENV,
getJrDataSystemTrack,
getJrDataSystemUiVariant,
JR_DATA_SYSTEM_ENV_OPTIONS,
JrDataSystemTrack,
JrDataSystemUiVariant,
JrDataSystemEnvironmentKey,
normalizeJrDataSystemEnvironment,
resolveJrDataSystemEnvironment,
} from "@/lib/jrDataSystemEnvironment";
const HUB_LOGO_PNG = require("@/assets/relationLogo/unyohub_logo.webp");
const ELESITE_LOGO_PNG = require("@/assets/relationLogo/elesite_logo.jpg");
/* ------------------------------------------------------------------ */
/* DataSourceAccordionCard */
/* ------------------------------------------------------------------ */
type Feature = { icon: string; label: string; text: string };
type DataSourceAccordionCardProps = {
/** ロゴ画像 (require) */
logo: any;
/** アクセントカラー */
accentColor: string;
/** データソース名 */
title: string;
/** 1行サブタイトル */
tagline: string;
/** スイッチの値 */
enabled: boolean;
/** スイッチ変更ハンドラ */
onToggle: (v: boolean) => void;
/** 説明文 */
description: string;
/** 機能リスト */
features: Feature[];
/** フッターリンクラベル */
linkLabel: string;
/** フッターリンク URL */
linkUrl: string;
/** 詳細ラベル */
detailLabel?: string;
};
const DataSourceAccordionCard: React.FC<DataSourceAccordionCardProps> = ({
logo,
accentColor,
title,
tagline,
enabled,
onToggle,
description,
features,
linkLabel,
linkUrl,
detailLabel,
}) => {
const [expanded, setExpanded] = useState(false);
const { colors } = useThemeColors();
return (
<View
style={[
styles.accordionCard,
{
backgroundColor: colors.surface,
borderColor: colors.borderSecondary,
},
enabled && styles.accordionCardEnabled,
]}
>
{/* ── ヘッダー行(常時表示) ── */}
<View style={styles.accordionHeader}>
{/* 左:ロゴ */}
<Image source={logo} style={styles.accordionLogo} />
{/* 中央:タイトル+タグライン */}
<View style={styles.accordionTitles}>
<Text style={[styles.accordionTitle, { color: colors.textPrimary }]}>
{title}
</Text>
<Text
style={[styles.accordionTagline, { color: colors.textTertiary }]}
>
{tagline}
</Text>
</View>
{/* 右:スイッチ */}
<Switch
value={enabled}
onValueChange={onToggle}
color={accentColor}
style={styles.accordionSwitch}
/>
</View>
{/* スイッチ状態テキスト */}
<View style={styles.accordionStatusRow}>
<View
style={[
styles.statusDot,
{ backgroundColor: enabled ? accentColor : colors.textDisabled },
]}
/>
<Text
style={[
styles.statusText,
{ color: enabled ? accentColor : colors.textQuaternary },
]}
>
{enabled
? "有効 — 編成データを取得します"
: "無効 — データを取得しません"}
</Text>
</View>
{/* ── 展開トリガー ── */}
<TouchableOpacity
style={[
styles.accordionToggleRow,
{ borderTopColor: colors.borderCard },
]}
onPress={() => setExpanded((v) => !v)}
activeOpacity={0.6}
>
<Text
style={[styles.accordionToggleLabel, { color: colors.textSecondary }]}
>
{expanded ? "詳細を閉じる" : (detailLabel ?? `${title} について`)}
</Text>
<MaterialCommunityIcons
name={expanded ? "chevron-up" : "chevron-down"}
size={16}
color={colors.iconSecondary}
/>
</TouchableOpacity>
{/* ── 展開コンテンツ ── */}
{expanded && (
<View
style={[
styles.accordionBody,
{
borderTopColor: colors.borderCard,
backgroundColor: colors.backgroundTertiary,
},
]}
>
{/* 説明文 */}
<Text style={[styles.bodyDesc, { color: colors.textSecondary }]}>
{description}
</Text>
{/* 機能リスト */}
<View
style={[
styles.bodyFeatures,
{ borderTopColor: colors.borderSecondary },
]}
>
{features.map((f) => (
<View key={f.icon} style={styles.featureRow}>
<View style={styles.featureIcon}>
<MaterialCommunityIcons
name={f.icon as any}
size={14}
color={colors.iconSecondary}
/>
</View>
<Text
style={[styles.featureLabel, { color: colors.textPrimary }]}
>
{f.label}
</Text>
<Text
style={[styles.featureText, { color: colors.textSecondary }]}
>
{f.text}
</Text>
</View>
))}
</View>
{/* リンク */}
<TouchableOpacity
style={[
styles.bodyLink,
{ borderTopColor: colors.borderSecondary },
]}
onPress={() => Linking.openURL(linkUrl)}
activeOpacity={0.7}
>
<MaterialCommunityIcons
name="open-in-new"
size={13}
color={colors.iconSecondary}
/>
<Text
style={[styles.bodyLinkText, { color: colors.textSecondary }]}
>
{linkLabel}
</Text>
</TouchableOpacity>
</View>
)}
</View>
);
};
/* ------------------------------------------------------------------ */
/* 定数 */
/* ------------------------------------------------------------------ */
const UNYOHUB_FEATURES: Feature[] = [
{
icon: "calendar-today",
label: "運用データ",
text: "当日・過去数日から投稿があった運用の継続予測運用情報を表示",
},
{ icon: "map-outline", label: "対象エリア", text: "JR四国全線" },
{
icon: "train",
label: "対象運用",
text: "JR四国管内営業列車及び貨物列車,定期回送列車に対応、臨時列車/突発運用は非対応",
},
{
icon: "pencil",
label: "入力方式",
text: "アプリ内連携システムにて当日の運用の投稿が可能",
},
];
const ELESITE_FEATURES: Feature[] = [
{
icon: "calendar-today",
label: "運用データ",
text: "当日報告のあった運用情報のみ表示",
},
{
icon: "map-outline",
label: "対象エリア",
text: "予讃線/瀬戸大橋線(直通している特急などの列番は含みます)",
},
{
icon: "train",
label: "対象運用",
text: "JR四国管内営業列車対応、臨時列車/突発運用は非対応",
},
{
icon: "pencil",
label: "入力方式",
text: "アプリ外リンク連携にて当日の運用の投稿が可能",
},
];
/* ------------------------------------------------------------------ */
/* DataSourceSettings */
/* ------------------------------------------------------------------ */
export const DataSourceSettings = () => {
const navigation = useNavigation();
const { updatePermission } = useTrainMenu();
const { colors, fixed } = useThemeColors();
const showDebugSelector = __DEV__ || updatePermission;
const [useUnyohub, setUseUnyohub] = useState(false);
const [useElesite, setUseElesite] = useState(false);
const [jrDataSystemEnv, setJrDataSystemEnv] =
useState<JrDataSystemEnvironmentKey>(DEFAULT_JR_DATA_SYSTEM_ENV);
const [jrDataSystemTrack, setJrDataSystemTrack] = useState<JrDataSystemTrack>(
getJrDataSystemTrack(DEFAULT_JR_DATA_SYSTEM_ENV),
);
const [jrDataSystemUiVariant, setJrDataSystemUiVariant] =
useState<JrDataSystemUiVariant>(
getJrDataSystemUiVariant(DEFAULT_JR_DATA_SYSTEM_ENV),
);
const applyJrDataSystemEnv = (env: JrDataSystemEnvironmentKey) => {
setJrDataSystemEnv(env);
setJrDataSystemTrack(getJrDataSystemTrack(env));
setJrDataSystemUiVariant(getJrDataSystemUiVariant(env));
AS.setItem(STORAGE_KEYS.JR_DATA_SYSTEM_ENV, env);
};
useEffect(() => {
AS.getItem(STORAGE_KEYS.USE_UNYOHUB).then((value) => {
setUseUnyohub(value === true || value === "true");
});
AS.getItem(STORAGE_KEYS.USE_ELESITE).then((value) => {
setUseElesite(value === true || value === "true");
});
AS.getItem(STORAGE_KEYS.JR_DATA_SYSTEM_ENV)
.then((value) => {
const env = normalizeJrDataSystemEnvironment(value);
setJrDataSystemEnv(env);
setJrDataSystemTrack(getJrDataSystemTrack(env));
setJrDataSystemUiVariant(getJrDataSystemUiVariant(env));
})
.catch(() => {
setJrDataSystemEnv(DEFAULT_JR_DATA_SYSTEM_ENV);
setJrDataSystemTrack(getJrDataSystemTrack(DEFAULT_JR_DATA_SYSTEM_ENV));
setJrDataSystemUiVariant(
getJrDataSystemUiVariant(DEFAULT_JR_DATA_SYSTEM_ENV),
);
});
}, []);
const handleToggleUnyohub = (value: boolean) => {
setUseUnyohub(value);
AS.setItem(STORAGE_KEYS.USE_UNYOHUB, value.toString());
};
const handleToggleElesite = (value: boolean) => {
setUseElesite(value);
AS.setItem(STORAGE_KEYS.USE_ELESITE, value.toString());
};
const handleSelectJrDataSystemTrack = (value: JrDataSystemTrack) => {
setJrDataSystemTrack(value);
const normalizedVariant =
value === "experimental" ? "release" : jrDataSystemUiVariant;
setJrDataSystemUiVariant(normalizedVariant);
const env = resolveJrDataSystemEnvironment(value, normalizedVariant);
applyJrDataSystemEnv(env);
};
const handleSelectJrDataSystemUiVariant = (value: JrDataSystemUiVariant) => {
setJrDataSystemUiVariant(value);
const env = resolveJrDataSystemEnvironment(jrDataSystemTrack, value);
applyJrDataSystemEnv(env);
};
return (
<View style={[styles.container, { backgroundColor: fixed.primary }]}>
<SheetHeaderItem
title="情報ソース設定"
LeftItem={{
title: " 戻る",
onPress: () => navigation.goBack(),
position: "left",
}}
/>
<ScrollView
style={[
styles.content,
{ backgroundColor: colors.backgroundSecondary },
]}
contentContainerStyle={styles.contentInner}
>
<Text style={[styles.sectionTitle, { color: colors.textTertiary }]}>
外部データソース
</Text>
<DataSourceAccordionCard
logo={HUB_LOGO_PNG}
accentColor="#0099CC"
title="鉄道運用Hub"
tagline="コミュニティによる列車運用情報サービス"
enabled={useUnyohub}
onToggle={handleToggleUnyohub}
description={
"鉄道運用Hubはオープンソースのユーザー投稿型鉄道運用情報データベースアプリケーションです。JR 四国をはじめ全国多数の路線系統に対応しています。\n\nデータがある列車では地図上にアイコンでマークが表示され、列車情報画面の編成表示も更新されます。"
}
features={UNYOHUB_FEATURES}
linkLabel="unyohub.2pd.jp を開く(JR四国)"
linkUrl="https://unyohub.2pd.jp/railroad_shikoku/"
/>
<DataSourceAccordionCard
logo={ELESITE_LOGO_PNG}
accentColor="#44bb44"
title="えれサイト"
tagline="コミュニティによる列車運用情報サービス"
enabled={useElesite}
onToggle={handleToggleElesite}
description={
"えれサイトは、鉄道の運用情報を利用者同士で共有するサービスです。皆様からの投稿をもとに、列車のリアルタイムな動きを反映しています。JR四国の特急・普通列車をはじめ、現在は全国の路線に対応しています。\n\nデータがある列車では地図上にアイコンでマークが表示され、列車情報画面の編成表示も更新されます。"
}
features={ELESITE_FEATURES}
linkLabel="elesite-next.com を開く"
linkUrl="https://www.elesite-next.com/"
/>
<View
style={[
styles.infoSection,
{ backgroundColor: colors.backgroundTertiary },
]}
>
<Text style={[styles.infoText, { color: colors.textCaution }]}>
外部のコミュニティデータソースとの連携を管理します。
{"\n\n"}
データの正確性は保証されません。また、これらの連携情報を利用する時点でそれぞれのサイトの利用規約に同意したものとします。
{"\n\n"}
外部ソースはJR四国非公式アプリが管理していないデータであるため、お問い合わせは各サービスの窓口までお願いいたします。
</Text>
</View>
{showDebugSelector && (
<>
<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>
<Text
style={[
styles.debugCurrentText,
{ color: colors.textTertiary },
]}
>
系統
</Text>
<View style={styles.debugOptionRow}>
{[
{
key: "production" as const,
label: "本番",
caption: "一般公開向け",
},
{
key: "experimental" as const,
label: "実験",
caption: "毎日リセット",
},
].map((option) => {
const selected = jrDataSystemTrack === option.key;
return (
<TouchableOpacity
key={option.key}
style={[
styles.debugOptionButton,
{
backgroundColor: selected
? fixed.primary
: colors.backgroundTertiary,
borderColor: selected
? fixed.primary
: colors.borderSecondary,
},
]}
onPress={() => handleSelectJrDataSystemTrack(option.key)}
activeOpacity={0.8}
>
<Text
style={[
styles.debugOptionTitle,
{
color: selected
? fixed.textOnPrimary
: colors.textPrimary,
},
]}
>
{option.label}
</Text>
<Text
style={[
styles.debugOptionCaption,
{
color: selected
? fixed.textOnPrimary
: colors.textTertiary,
},
]}
>
{option.caption}
</Text>
</TouchableOpacity>
);
})}
</View>
<Text
style={[
styles.debugCurrentText,
{ color: colors.textTertiary },
]}
>
UIバージョン(本番のみ)
</Text>
<View style={styles.debugOptionRow}>
{[
{
key: "release" as const,
label: "リリース",
caption: "安定版",
},
{
key: "beta" as const,
label: "ベータ",
caption: "夜間ビルド",
},
].map((option) => {
const selected = jrDataSystemUiVariant === option.key;
const disabled = jrDataSystemTrack === "experimental";
return (
<TouchableOpacity
key={option.key}
style={[
styles.debugOptionButton,
{
opacity: disabled ? 0.45 : 1,
backgroundColor: selected
? fixed.primary
: colors.backgroundTertiary,
borderColor: selected
? fixed.primary
: colors.borderSecondary,
},
]}
onPress={() => {
if (disabled) return;
handleSelectJrDataSystemUiVariant(option.key);
}}
activeOpacity={0.8}
>
<Text
style={[
styles.debugOptionTitle,
{
color: selected
? fixed.textOnPrimary
: colors.textPrimary,
},
]}
>
{option.label}
</Text>
<Text
style={[
styles.debugOptionCaption,
{
color: selected
? fixed.textOnPrimary
: colors.textTertiary,
},
]}
>
{option.caption}
</Text>
</TouchableOpacity>
);
})}
</View>
<Text
style={[
styles.debugCurrentText,
{ color: colors.textTertiary },
]}
>
現在の接続先:{" "}
{
JR_DATA_SYSTEM_ENV_OPTIONS.find(
(option) => option.key === jrDataSystemEnv,
)?.baseUrl
}
</Text>
</View>
</>
)}
</ScrollView>
</View>
);
};
const styles = StyleSheet.create({
/* ── レイアウト ── */
container: {
flex: 1,
backgroundColor: "#0099CC",
},
content: {
flex: 1,
backgroundColor: "#f8f8fc",
},
contentInner: {
paddingHorizontal: 14,
paddingBottom: 40,
gap: 12,
},
sectionTitle: {
fontSize: 13,
fontWeight: "600",
color: "#888",
letterSpacing: 0.5,
marginTop: 20,
marginLeft: 4,
},
/* ── アコーディオンカード ── */
accordionCard: {
backgroundColor: "#fff",
borderRadius: 14,
borderWidth: 1,
borderColor: "#e4e4e4",
overflow: "hidden",
},
accordionCardEnabled: {
borderColor: "#0099CC44",
},
accordionHeader: {
flexDirection: "row",
alignItems: "center",
paddingHorizontal: 14,
paddingTop: 14,
paddingBottom: 6,
gap: 10,
},
accordionLogo: {
width: 40,
height: 40,
borderRadius: 8,
flexShrink: 0,
},
accordionTitles: {
flex: 1,
gap: 2,
},
accordionTitle: {
fontSize: 15,
fontWeight: "bold",
color: "#111",
},
accordionTagline: {
fontSize: 11,
color: "#888",
},
accordionSwitch: {
flexShrink: 0,
},
accordionStatusRow: {
flexDirection: "row",
alignItems: "center",
gap: 6,
paddingHorizontal: 14,
paddingBottom: 10,
},
statusDot: {
width: 7,
height: 7,
borderRadius: 4,
},
statusText: {
fontSize: 12,
fontWeight: "500",
},
/* ── 展開トリガー ── */
accordionToggleRow: {
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
paddingHorizontal: 14,
paddingVertical: 10,
borderTopWidth: StyleSheet.hairlineWidth,
borderTopColor: "#ebebeb",
},
accordionToggleLabel: {
fontSize: 12,
color: "#666",
fontWeight: "500",
},
/* ── 展開コンテンツ ── */
accordionBody: {
borderTopWidth: StyleSheet.hairlineWidth,
borderTopColor: "#ebebeb",
padding: 14,
gap: 10,
backgroundColor: "#fafafa",
},
bodyDesc: {
fontSize: 12,
color: "#444",
lineHeight: 19,
},
bodyFeatures: {
gap: 7,
borderTopWidth: StyleSheet.hairlineWidth,
borderTopColor: "#e4e4e4",
paddingTop: 8,
},
featureRow: {
flexDirection: "row",
alignItems: "flex-start",
gap: 6,
},
featureIcon: {
width: 22,
alignItems: "center",
paddingTop: 1,
flexShrink: 0,
},
featureLabel: {
fontSize: 12,
fontWeight: "bold",
color: "#333",
width: 62,
flexShrink: 0,
},
featureText: {
fontSize: 12,
color: "#555",
flex: 1,
lineHeight: 17,
},
bodyLink: {
flexDirection: "row",
alignItems: "center",
gap: 5,
borderTopWidth: StyleSheet.hairlineWidth,
borderTopColor: "#e4e4e4",
paddingTop: 8,
marginTop: 2,
},
bodyLinkText: {
fontSize: 12,
color: "#555",
},
/* ── 注意書き ── */
infoSection: {
backgroundColor: "#fff3cd",
borderRadius: 10,
padding: 14,
},
infoText: {
fontSize: 13,
color: "#856404",
lineHeight: 18,
},
debugSection: {
borderRadius: 12,
borderWidth: 1,
padding: 14,
gap: 10,
},
debugTitle: {
fontSize: 15,
fontWeight: "bold",
},
debugDescription: {
fontSize: 12,
lineHeight: 18,
},
debugOptionRow: {
flexDirection: "row",
flexWrap: "wrap",
gap: 8,
},
debugOptionButton: {
minWidth: 96,
borderRadius: 10,
borderWidth: 1,
paddingHorizontal: 12,
paddingVertical: 10,
gap: 2,
},
debugOptionTitle: {
fontSize: 13,
fontWeight: "bold",
},
debugOptionCaption: {
fontSize: 10,
},
debugCurrentText: {
fontSize: 11,
lineHeight: 16,
},
});