426 lines
13 KiB
TypeScript
426 lines
13 KiB
TypeScript
import React, { useState, useEffect } from "react";
|
||
import { View, Text, ScrollView, StyleSheet, Image, TouchableOpacity, Linking } from "react-native";
|
||
import { Switch } from "react-native-elements";
|
||
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";
|
||
|
||
const HUB_LOGO_PNG = require("@/assets/icons/hub_logo.png");
|
||
const ELESITE_LOGO_PNG = require("@/assets/icons/elesite_logo.png");
|
||
/* ------------------------------------------------------------------ */
|
||
/* 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;
|
||
};
|
||
|
||
const DataSourceAccordionCard: React.FC<DataSourceAccordionCardProps> = ({
|
||
logo,
|
||
accentColor,
|
||
title,
|
||
tagline,
|
||
enabled,
|
||
onToggle,
|
||
description,
|
||
features,
|
||
linkLabel,
|
||
linkUrl,
|
||
detailLabel,
|
||
}) => {
|
||
const [expanded, setExpanded] = useState(false);
|
||
|
||
return (
|
||
<View style={[styles.accordionCard, enabled && styles.accordionCardEnabled]}>
|
||
{/* ── ヘッダー行(常時表示) ── */}
|
||
<View style={styles.accordionHeader}>
|
||
{/* 左:ロゴ */}
|
||
<Image source={logo} style={styles.accordionLogo} />
|
||
|
||
{/* 中央:タイトル+タグライン */}
|
||
<View style={styles.accordionTitles}>
|
||
<Text style={styles.accordionTitle}>{title}</Text>
|
||
<Text style={styles.accordionTagline}>{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 : "#ccc" }]} />
|
||
<Text style={[styles.statusText, { color: enabled ? accentColor : "#aaa" }]}>
|
||
{enabled ? "有効 — 編成データを取得します" : "無効 — データを取得しません"}
|
||
</Text>
|
||
</View>
|
||
|
||
{/* ── 展開トリガー ── */}
|
||
<TouchableOpacity
|
||
style={styles.accordionToggleRow}
|
||
onPress={() => setExpanded((v) => !v)}
|
||
activeOpacity={0.6}
|
||
>
|
||
<Text style={styles.accordionToggleLabel}>
|
||
{expanded ? "詳細を閉じる" : (detailLabel ?? `${title} について`)}
|
||
</Text>
|
||
<MaterialCommunityIcons
|
||
name={expanded ? "chevron-up" : "chevron-down"}
|
||
size={16}
|
||
color="#888"
|
||
/>
|
||
</TouchableOpacity>
|
||
|
||
{/* ── 展開コンテンツ ── */}
|
||
{expanded && (
|
||
<View style={styles.accordionBody}>
|
||
{/* 説明文 */}
|
||
<Text style={styles.bodyDesc}>{description}</Text>
|
||
|
||
{/* 機能リスト */}
|
||
<View style={styles.bodyFeatures}>
|
||
{features.map((f) => (
|
||
<View key={f.icon} style={styles.featureRow}>
|
||
<View style={styles.featureIcon}>
|
||
<MaterialCommunityIcons name={f.icon as any} size={14} color="#444" />
|
||
</View>
|
||
<Text style={styles.featureLabel}>{f.label}</Text>
|
||
<Text style={styles.featureText}>{f.text}</Text>
|
||
</View>
|
||
))}
|
||
</View>
|
||
|
||
{/* リンク */}
|
||
<TouchableOpacity
|
||
style={styles.bodyLink}
|
||
onPress={() => Linking.openURL(linkUrl)}
|
||
activeOpacity={0.7}
|
||
>
|
||
<MaterialCommunityIcons name="open-in-new" size={13} color="#555" />
|
||
<Text style={styles.bodyLinkText}>{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: "plus", label: "追加機能", text: "前日、当日、翌日の運用の投稿が可能" },
|
||
];
|
||
|
||
const ELESITE_FEATURES: Feature[] = [
|
||
{ icon: "calendar-today", label: "運用データ", text: "当日に報告のあった運用情報のみ表示" },
|
||
{ icon: "map-outline", label: "対象エリア", text: "予讃線/瀬戸大橋線(なお直通している特急などの列番は含みます)" },
|
||
{ icon: "train", label: "対象運用", text: "JR四国管内営業列車対応、臨時列車/突発運用は非対応" },
|
||
];
|
||
|
||
/* ------------------------------------------------------------------ */
|
||
/* DataSourceSettings */
|
||
/* ------------------------------------------------------------------ */
|
||
export const DataSourceSettings = () => {
|
||
const navigation = useNavigation();
|
||
const { updatePermission, dataSourcePermission } = useTrainMenu();
|
||
const canAccess = updatePermission || Object.values(dataSourcePermission).some(Boolean);
|
||
const [useUnyohub, setUseUnyohub] = useState(false);
|
||
const [useElesite, setUseElesite] = useState(false);
|
||
|
||
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");
|
||
});
|
||
}, []);
|
||
|
||
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());
|
||
};
|
||
|
||
return (
|
||
<View style={styles.container}>
|
||
<SheetHeaderItem
|
||
title="情報ソース設定"
|
||
LeftItem={{
|
||
title: "戻る",
|
||
onPress: () => navigation.goBack(),
|
||
position: "left",
|
||
}}
|
||
/>
|
||
{!canAccess ? (
|
||
<View style={styles.noPermissionContainer}>
|
||
<Text style={styles.noPermissionText}>この設定にアクセスする権限がありません。</Text>
|
||
<Text style={styles.noPermissionSubText}>鉄道運用Hubまたはアプリ管理者の権限が必要です。</Text>
|
||
</View>
|
||
) : (
|
||
<ScrollView style={styles.content} contentContainerStyle={styles.contentInner}>
|
||
<Text style={styles.sectionTitle}>外部データソース</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データがある列車では地図上に緑色の「E」バッジが表示され、列車情報画面の編成表示も更新されます。"
|
||
}
|
||
features={ELESITE_FEATURES}
|
||
linkLabel="elesite-next.com を開く"
|
||
linkUrl="https://www.elesite-next.com/"
|
||
/>
|
||
|
||
<View style={styles.infoSection}>
|
||
<Text style={styles.infoText}>
|
||
外部のコミュニティデータソースとの連携を管理します。
|
||
{"\n\n"}
|
||
データの正確性は保証されません。また、これらの連携情報を利用する時点でそれぞれのサイトの利用規約に同意したものとします。{"\n\n"}外部ソースはJR四国非公式アプリが管理していないデータであるため、お問い合わせは各サービスの窓口までお願いいたします。
|
||
</Text>
|
||
</View>
|
||
</ScrollView>
|
||
)}
|
||
</View>
|
||
);
|
||
};
|
||
|
||
const styles = StyleSheet.create({
|
||
/* ── 権限なし ── */
|
||
noPermissionContainer: {
|
||
flex: 1,
|
||
backgroundColor: "#f8f8fc",
|
||
alignItems: "center",
|
||
justifyContent: "center",
|
||
padding: 30,
|
||
gap: 10,
|
||
},
|
||
noPermissionText: {
|
||
fontSize: 16,
|
||
fontWeight: "bold",
|
||
color: "#333",
|
||
textAlign: "center",
|
||
},
|
||
noPermissionSubText: {
|
||
fontSize: 13,
|
||
color: "#666",
|
||
textAlign: "center",
|
||
},
|
||
/* ── レイアウト ── */
|
||
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,
|
||
},
|
||
});
|