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";
|
||||
|
||||
type Show = {
|
||||
@ -13,6 +14,7 @@ type Show = {
|
||||
|
||||
const GET_URL = API_ENDPOINT.v1.SHOWS;
|
||||
const POST_URL = API_ENDPOINT.v1.CURRENT;
|
||||
const REDIRECT_DELAY_S = 3;
|
||||
|
||||
const HHMM = /^(\d{1,2}):([0-5]\d)$/;
|
||||
|
||||
@ -37,11 +39,34 @@ export default function ShowsPage() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [posting, setPosting] = useState(false);
|
||||
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 [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(() => {
|
||||
let cancelled = false;
|
||||
@ -51,7 +76,8 @@ export default function ShowsPage() {
|
||||
const res = await fetch(GET_URL, { cache: "no-store" });
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
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) {
|
||||
if (!cancelled) setError(e.message || "番組一覧の取得に失敗しました。");
|
||||
} finally {
|
||||
@ -60,6 +86,13 @@ export default function ShowsPage() {
|
||||
})();
|
||||
return () => { cancelled = true; };
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (redirectTid.current) {
|
||||
window.clearInterval(redirectTid.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const current = useMemo(() => shows.find(s => s.id === selectedId) || null, [shows, selectedId]);
|
||||
|
||||
@ -67,12 +100,17 @@ export default function ShowsPage() {
|
||||
setError(null);
|
||||
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);
|
||||
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 {
|
||||
@ -83,7 +121,13 @@ export default function ShowsPage() {
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
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) {
|
||||
setError(e.message || "現在のエピソード設定に失敗しました。");
|
||||
} finally {
|
||||
@ -109,12 +153,15 @@ export default function ShowsPage() {
|
||||
<button
|
||||
key={s.id}
|
||||
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="season subtle">{s.season_name}</div>
|
||||
<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>
|
||||
</button>
|
||||
))}
|
||||
@ -139,11 +186,12 @@ export default function ShowsPage() {
|
||||
disabled={
|
||||
posting ||
|
||||
!selectedId ||
|
||||
isRedirecting ||
|
||||
(!!startTime && !toHHMMSS(startTime))
|
||||
}
|
||||
onClick={submit}
|
||||
>
|
||||
{posting ? "設定中…" : "現在のエピソードに設定"}
|
||||
{redirectIn !== null ? `設定しました(${redirectIn})` : posting ? "設定中…" : "現在のエピソードに設定"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user