Merge branch 'main' into develop
This commit is contained in:
commit
a24b72740b
@ -1,4 +1,5 @@
|
|||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState, useRef } from "react";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
import { API_ENDPOINT } from "../api/endpoint";
|
import { API_ENDPOINT } from "../api/endpoint";
|
||||||
|
|
||||||
type Show = {
|
type Show = {
|
||||||
@ -13,6 +14,7 @@ type Show = {
|
|||||||
|
|
||||||
const GET_URL = API_ENDPOINT.v1.SHOWS;
|
const GET_URL = API_ENDPOINT.v1.SHOWS;
|
||||||
const POST_URL = API_ENDPOINT.v1.CURRENT;
|
const POST_URL = API_ENDPOINT.v1.CURRENT;
|
||||||
|
const REDIRECT_DELAY_S = 3;
|
||||||
|
|
||||||
const HHMM = /^(\d{1,2}):([0-5]\d)$/;
|
const HHMM = /^(\d{1,2}):([0-5]\d)$/;
|
||||||
|
|
||||||
@ -37,11 +39,34 @@ export default function ShowsPage() {
|
|||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [posting, setPosting] = useState(false);
|
const [posting, setPosting] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [redirectIn, setRedirectIn] = useState<number | null>(null);
|
||||||
|
const redirectTid = useRef<number | null>(null);
|
||||||
|
const isRedirecting = redirectIn !== null;
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
// フォーム状態
|
// フォーム状態
|
||||||
const [selectedId, setSelectedId] = useState<number | null>(null);
|
const [selectedId, setSelectedId] = useState<number | null>(null);
|
||||||
const [startTime, setStartTime] = useState("");
|
const [startTime, setStartTime] = useState("");
|
||||||
|
|
||||||
|
function formatPlaybackLen(v: string): string {
|
||||||
|
const parts = v.split(":").map(Number);
|
||||||
|
if (parts.length === 2) {
|
||||||
|
// already MM:SS
|
||||||
|
const [mm, ss] = parts;
|
||||||
|
return `${String(mm).padStart(2, "0")}:${String(ss).padStart(2, "0")}`;
|
||||||
|
}
|
||||||
|
if (parts.length === 3) {
|
||||||
|
const [hh, mm, ss] = parts;
|
||||||
|
if (hh <= 0) {
|
||||||
|
// show MM:SS when hours are zero
|
||||||
|
return `${String(mm).padStart(2, "0")}:${String(ss).padStart(2, "0")}`;
|
||||||
|
}
|
||||||
|
// show HH:MM:SS when hours exist
|
||||||
|
return `${String(hh).padStart(2, "0")}:${String(mm).padStart(2, "0")}:${String(ss).padStart(2, "0")}`;
|
||||||
|
}
|
||||||
|
return v; // fallback (unexpected format)
|
||||||
|
}
|
||||||
|
|
||||||
// 一覧取得
|
// 一覧取得
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
@ -51,7 +76,8 @@ export default function ShowsPage() {
|
|||||||
const res = await fetch(GET_URL, { cache: "no-store" });
|
const res = await fetch(GET_URL, { cache: "no-store" });
|
||||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||||
const data = (await res.json()) as Show[];
|
const data = (await res.json()) as Show[];
|
||||||
if (!cancelled) setShows(data);
|
data.sort((a, b) => a.id - b.id); // ASC
|
||||||
|
setShows(data);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
if (!cancelled) setError(e.message || "番組一覧の取得に失敗しました。");
|
if (!cancelled) setError(e.message || "番組一覧の取得に失敗しました。");
|
||||||
} finally {
|
} finally {
|
||||||
@ -60,6 +86,13 @@ export default function ShowsPage() {
|
|||||||
})();
|
})();
|
||||||
return () => { cancelled = true; };
|
return () => { cancelled = true; };
|
||||||
}, []);
|
}, []);
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (redirectTid.current) {
|
||||||
|
window.clearInterval(redirectTid.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
const current = useMemo(() => shows.find(s => s.id === selectedId) || null, [shows, selectedId]);
|
const current = useMemo(() => shows.find(s => s.id === selectedId) || null, [shows, selectedId]);
|
||||||
|
|
||||||
@ -67,12 +100,17 @@ export default function ShowsPage() {
|
|||||||
setError(null);
|
setError(null);
|
||||||
if (!selectedId) { setError("エピソードを選択してください。"); return; }
|
if (!selectedId) { setError("エピソードを選択してください。"); return; }
|
||||||
|
|
||||||
let payload: any = { id: selectedId };
|
const selected = shows.find(s => s.id === selectedId);
|
||||||
|
if (!selected) { setError("選択中のエピソードが見つかりません。"); return; }
|
||||||
|
|
||||||
if (startTime) {
|
const payload: any = { id: selectedId };
|
||||||
|
|
||||||
|
if (startTime.trim()) {
|
||||||
const normalized = toHHMMSS(startTime);
|
const normalized = toHHMMSS(startTime);
|
||||||
if (!normalized) { setError("開始時刻は HH:MM の形式で入力してください。"); return; }
|
if (!normalized) { setError("開始時刻は HH:MM の形式で入力してください。"); return; }
|
||||||
payload.start_time = normalized; // API expects HH:MM:SS
|
payload.start_time = normalized;
|
||||||
|
} else {
|
||||||
|
payload.start_time = selected.start_time;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -83,7 +121,13 @@ export default function ShowsPage() {
|
|||||||
body: JSON.stringify(payload),
|
body: JSON.stringify(payload),
|
||||||
});
|
});
|
||||||
if (!res.ok) throw new Error(`POST 失敗 (${res.status})`);
|
if (!res.ok) throw new Error(`POST 失敗 (${res.status})`);
|
||||||
setStartTime("");
|
setRedirectIn(REDIRECT_DELAY_S);
|
||||||
|
const start = Date.now();
|
||||||
|
redirectTid.current = window.setInterval(() => {
|
||||||
|
const left = REDIRECT_DELAY_S - Math.floor((Date.now() - start) / 1000);
|
||||||
|
if (left <= 0) { window.clearInterval(redirectTid.current!); navigate("/", { replace: true }); }
|
||||||
|
else setRedirectIn(left);
|
||||||
|
}, 250);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
setError(e.message || "現在のエピソード設定に失敗しました。");
|
setError(e.message || "現在のエピソード設定に失敗しました。");
|
||||||
} finally {
|
} finally {
|
||||||
@ -109,12 +153,15 @@ export default function ShowsPage() {
|
|||||||
<button
|
<button
|
||||||
key={s.id}
|
key={s.id}
|
||||||
className={`show-card ${selectedId === s.id ? "selected" : ""}`}
|
className={`show-card ${selectedId === s.id ? "selected" : ""}`}
|
||||||
onClick={() => setSelectedId(s.id)}
|
onClick={() => {
|
||||||
|
setSelectedId(s.id);
|
||||||
|
setStartTime(s.start_time.slice(0, 5));
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div className="title">第{s.ep_num}話:{s.ep_title}</div>
|
<div className="title">第{s.ep_num}話:{s.ep_title}</div>
|
||||||
<div className="season subtle">{s.season_name}</div>
|
<div className="season subtle">{s.season_name}</div>
|
||||||
<div className="meta subtle">
|
<div className="meta subtle">
|
||||||
開始 {s.start_time.slice(0,5)}・長さ {s.playback_length.slice(0,5)}
|
開始 {s.start_time.slice(0, 5)}・長さ {formatPlaybackLen(s.playback_length)}
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
@ -139,11 +186,12 @@ export default function ShowsPage() {
|
|||||||
disabled={
|
disabled={
|
||||||
posting ||
|
posting ||
|
||||||
!selectedId ||
|
!selectedId ||
|
||||||
|
isRedirecting ||
|
||||||
(!!startTime && !toHHMMSS(startTime))
|
(!!startTime && !toHHMMSS(startTime))
|
||||||
}
|
}
|
||||||
onClick={submit}
|
onClick={submit}
|
||||||
>
|
>
|
||||||
{posting ? "設定中…" : "現在のエピソードに設定"}
|
{redirectIn !== null ? `設定しました(${redirectIn})` : posting ? "設定中…" : "現在のエピソードに設定"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user