|
|
|
|
@ -83,19 +83,31 @@ export function MangaViewer({
|
|
|
|
|
const [dismissNavError, setDismissNavError] = useState(false);
|
|
|
|
|
const [boundaryWarning, setBoundaryWarning] = useState<'first' | 'last' | null>(null);
|
|
|
|
|
const [dimMap, setDimMap] = useState<Record<string, { width: number; height: number }>>({});
|
|
|
|
|
const [toast, setToast] = useState<{ title: string; episode: string | null } | null>(null);
|
|
|
|
|
const [toast, setToast] = useState<{ title?: string; episode?: string | null; text?: string; isWarning?: boolean } | null>(null);
|
|
|
|
|
const toolbarTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
|
|
|
const toastTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
|
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
|
|
|
|
|
|
|
|
const showNoticeToast = useCallback((text: string, isWarning = true) => {
|
|
|
|
|
setToast({ text, isWarning });
|
|
|
|
|
if (toastTimer.current) clearTimeout(toastTimer.current);
|
|
|
|
|
toastTimer.current = setTimeout(() => {
|
|
|
|
|
setToast(null);
|
|
|
|
|
}, 2800);
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
// Trigger Chapter Notification Toast on chapter load/change
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!chapter.title) return;
|
|
|
|
|
const parsed = parseTitleAndEpisode(chapter.title);
|
|
|
|
|
setToast(parsed);
|
|
|
|
|
const timer = setTimeout(() => {
|
|
|
|
|
setToast({ title: parsed.title, episode: parsed.episode, isWarning: false });
|
|
|
|
|
if (toastTimer.current) clearTimeout(toastTimer.current);
|
|
|
|
|
toastTimer.current = setTimeout(() => {
|
|
|
|
|
setToast(null);
|
|
|
|
|
}, 2800);
|
|
|
|
|
return () => clearTimeout(timer);
|
|
|
|
|
return () => {
|
|
|
|
|
if (toastTimer.current) clearTimeout(toastTimer.current);
|
|
|
|
|
};
|
|
|
|
|
}, [currentUrl, chapter.title]);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
@ -191,8 +203,10 @@ export function MangaViewer({
|
|
|
|
|
} else {
|
|
|
|
|
setBoundaryWarning('last');
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
showNoticeToast('마지막 화입니다.', true);
|
|
|
|
|
}
|
|
|
|
|
}, [slotIndex, totalSlots, chapter.next_chapter, boundaryWarning, onNavigate]);
|
|
|
|
|
}, [slotIndex, totalSlots, chapter.next_chapter, boundaryWarning, onNavigate, showNoticeToast]);
|
|
|
|
|
|
|
|
|
|
const goPrev = useCallback(() => {
|
|
|
|
|
if (slotIndex > 0) {
|
|
|
|
|
@ -204,8 +218,10 @@ export function MangaViewer({
|
|
|
|
|
} else {
|
|
|
|
|
setBoundaryWarning('first');
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
showNoticeToast('첫 번째 화입니다.', true);
|
|
|
|
|
}
|
|
|
|
|
}, [slotIndex, chapter.prev_chapter, boundaryWarning, onNavigate]);
|
|
|
|
|
}, [slotIndex, chapter.prev_chapter, boundaryWarning, onNavigate, showNoticeToast]);
|
|
|
|
|
|
|
|
|
|
const toggleFullscreen = useCallback(async () => {
|
|
|
|
|
try {
|
|
|
|
|
@ -340,11 +356,17 @@ export function MangaViewer({
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Chapter Transition Toast */}
|
|
|
|
|
{/* Chapter Transition / Warning Toast */}
|
|
|
|
|
{toast && (
|
|
|
|
|
<div className="chapter-toast">
|
|
|
|
|
{toast.episode && <span className="toast-badge">{toast.episode}</span>}
|
|
|
|
|
<span className="toast-title">{toast.title}</span>
|
|
|
|
|
<div className={`chapter-toast ${toast.isWarning ? 'warning-toast' : ''}`}>
|
|
|
|
|
{toast.isWarning ? (
|
|
|
|
|
<span className="toast-warning-text">⚠️ {toast.text}</span>
|
|
|
|
|
) : (
|
|
|
|
|
<>
|
|
|
|
|
{toast.episode && <span className="toast-badge">{toast.episode}</span>}
|
|
|
|
|
{toast.title && <span className="toast-title">{toast.title}</span>}
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|