Crash fix (React Hooks violation): - PlaybackTimeline.tsx: move ALL hooks (useRef for PanResponder, useCallback) to before the early return; use seekRef/totalRef to share values into PanResponder handlers without stale closure issues Multiple recordings support: - trainRecorder.ts: redesign to id-based multi-recording system - RecordingMeta type for lightweight list (no snapshots) - TrainRecording now includes id field - saveRecording/loadRecordingById/deleteRecordingById/loadRecordingList - migrateOldRecording() migrates old single MOCK_RECORDING key on first launch - constants/storage.ts: add MOCK_RECORDINGS_INDEX + MOCK_RECORDING_DATA_PREFIX keys - useTrainMenu: savedRecording → recordingList (RecordingMeta[]) + activeRecording (TrainRecording|null) - startPlayback(id) loads full recording on demand - deleteRecording(id) deletes by id and refreshes list - stopPlayback clears activeRecording - DataSourceSettings: recording list UI - shows all recordings with date/time, snapshot count, duration - ▶ play and 削除 buttons per row - recording/playing status indicator Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
985 lines
31 KiB
TypeScript
985 lines
31 KiB
TypeScript
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,
|
||
mockApiFeatureEnabled,
|
||
setMockApiFeatureEnabled,
|
||
recorderState,
|
||
recordingSnapshotCount,
|
||
recordingList,
|
||
startRecording,
|
||
stopRecording,
|
||
startPlayback,
|
||
stopPlayback,
|
||
deleteRecording,
|
||
} = 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 }]}>
|
||
デバッグ: モックAPI検証
|
||
</Text>
|
||
<Text
|
||
style={[
|
||
styles.debugDescription,
|
||
{ color: colors.textSecondary },
|
||
]}
|
||
>
|
||
公式サイトの代わりにサンプルデータを流し込みます。
|
||
</Text>
|
||
<View
|
||
style={{
|
||
flexDirection: "row",
|
||
alignItems: "center",
|
||
justifyContent: "space-between",
|
||
marginTop: 8,
|
||
}}
|
||
>
|
||
<Text
|
||
style={[
|
||
styles.debugCurrentText,
|
||
{ color: colors.textPrimary, fontSize: 14 },
|
||
]}
|
||
>
|
||
モックAPI検証機能
|
||
</Text>
|
||
<Switch
|
||
value={mockApiFeatureEnabled}
|
||
onValueChange={setMockApiFeatureEnabled}
|
||
color={fixed.primary}
|
||
/>
|
||
</View>
|
||
</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 }]}>
|
||
ライブデータを録画してモックとして再生します。録画中はモックOFFになります。
|
||
</Text>
|
||
|
||
{/* ステータス行 */}
|
||
<View style={{ flexDirection: "row", alignItems: "center", marginTop: 8, gap: 8 }}>
|
||
<View
|
||
style={{
|
||
width: 10, height: 10, borderRadius: 5,
|
||
backgroundColor:
|
||
recorderState === 'recording' ? '#e53935' :
|
||
recorderState === 'playing' ? '#43a047' : colors.borderSecondary,
|
||
}}
|
||
/>
|
||
<Text style={[styles.debugCurrentText, { color: colors.textSecondary, fontSize: 13 }]}>
|
||
{recorderState === 'recording'
|
||
? `録画中… ${recordingSnapshotCount} スナップショット`
|
||
: recorderState === 'playing'
|
||
? '再生中'
|
||
: `${recordingList.length} 件の録画`}
|
||
</Text>
|
||
</View>
|
||
|
||
{/* 録画開始 / 停止ボタン */}
|
||
<View style={{ flexDirection: "row", gap: 8, marginTop: 10, flexWrap: "wrap" }}>
|
||
{recorderState === 'idle' && (
|
||
<TouchableOpacity
|
||
onPress={startRecording}
|
||
style={{
|
||
backgroundColor: '#e53935', borderRadius: 8,
|
||
paddingHorizontal: 14, paddingVertical: 8,
|
||
}}
|
||
>
|
||
<Text style={{ color: '#fff', fontWeight: 'bold', fontSize: 13 }}>● 録画開始</Text>
|
||
</TouchableOpacity>
|
||
)}
|
||
{recorderState === 'recording' && (
|
||
<TouchableOpacity
|
||
onPress={stopRecording}
|
||
style={{
|
||
backgroundColor: colors.borderSecondary, borderRadius: 8,
|
||
paddingHorizontal: 14, paddingVertical: 8,
|
||
}}
|
||
>
|
||
<Text style={{ color: colors.textPrimary, fontWeight: 'bold', fontSize: 13 }}>■ 録画停止</Text>
|
||
</TouchableOpacity>
|
||
)}
|
||
{recorderState === 'playing' && (
|
||
<TouchableOpacity
|
||
onPress={stopPlayback}
|
||
style={{
|
||
backgroundColor: colors.borderSecondary, borderRadius: 8,
|
||
paddingHorizontal: 14, paddingVertical: 8,
|
||
}}
|
||
>
|
||
<Text style={{ color: colors.textPrimary, fontWeight: 'bold', fontSize: 13 }}>■ 再生停止</Text>
|
||
</TouchableOpacity>
|
||
)}
|
||
</View>
|
||
|
||
{/* 録画一覧 */}
|
||
{recordingList.length > 0 && recorderState !== 'recording' && (
|
||
<View style={{ marginTop: 10, gap: 6 }}>
|
||
{recordingList.map((rec) => {
|
||
const isPlaying = recorderState === 'playing';
|
||
const durationSec = Math.round(rec.durationMs / 1000);
|
||
const durationLabel = durationSec >= 60
|
||
? `${Math.floor(durationSec / 60)}分${durationSec % 60}秒`
|
||
: `${durationSec}秒`;
|
||
const dateLabel = new Date(rec.recordedAt).toLocaleString('ja-JP', {
|
||
month: 'numeric', day: 'numeric',
|
||
hour: '2-digit', minute: '2-digit',
|
||
});
|
||
return (
|
||
<View
|
||
key={rec.id}
|
||
style={{
|
||
flexDirection: 'row', alignItems: 'center',
|
||
backgroundColor: colors.backgroundSecondary,
|
||
borderRadius: 8, padding: 10, gap: 8,
|
||
}}
|
||
>
|
||
<View style={{ flex: 1 }}>
|
||
<Text style={{ color: colors.textPrimary, fontSize: 13, fontWeight: 'bold' }}>
|
||
{dateLabel}
|
||
</Text>
|
||
<Text style={{ color: colors.textSecondary, fontSize: 11 }}>
|
||
{rec.snapshotCount} コマ / {durationLabel}
|
||
</Text>
|
||
</View>
|
||
{!isPlaying && (
|
||
<TouchableOpacity
|
||
onPress={() => startPlayback(rec.id)}
|
||
style={{
|
||
backgroundColor: '#43a047', borderRadius: 6,
|
||
paddingHorizontal: 10, paddingVertical: 6,
|
||
}}
|
||
>
|
||
<Text style={{ color: '#fff', fontWeight: 'bold', fontSize: 12 }}>▶</Text>
|
||
</TouchableOpacity>
|
||
)}
|
||
{!isPlaying && (
|
||
<TouchableOpacity
|
||
onPress={() => deleteRecording(rec.id)}
|
||
style={{
|
||
borderColor: '#e53935', borderWidth: 1, borderRadius: 6,
|
||
paddingHorizontal: 10, paddingVertical: 6,
|
||
}}
|
||
>
|
||
<Text style={{ color: '#e53935', fontSize: 12 }}>削除</Text>
|
||
</TouchableOpacity>
|
||
)}
|
||
</View>
|
||
);
|
||
})}
|
||
</View>
|
||
)}
|
||
</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>
|
||
|
||
<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,
|
||
},
|
||
});
|