diff --git a/src/App.css b/src/App.css index 3319e84..9057d28 100644 --- a/src/App.css +++ b/src/App.css @@ -484,6 +484,13 @@ kbd { min-width: 0; } +.history-title-row { + display: flex; + align-items: center; + gap: 6px; + overflow: hidden; +} + .history-manga-title { font-size: 13px; font-weight: 600; @@ -501,6 +508,93 @@ kbd { white-space: nowrap; } +/* Toolbar Title and Episode Badge */ +.toolbar-title-container { + display: flex; + align-items: center; + gap: 8px; + max-width: 360px; + min-width: 0; +} + +.toolbar-title-name { + font-size: 13px; + font-weight: 600; + color: var(--color-text); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.episode-badge { + background: linear-gradient(135deg, #7c3aed, #4f46e5); + color: #ffffff; + font-size: 11px; + font-weight: 700; + padding: 2px 7px; + border-radius: 12px; + white-space: nowrap; + flex-shrink: 0; + box-shadow: 0 2px 6px rgba(124, 58, 237, 0.4); +} + +.episode-badge.sm { + font-size: 10px; + padding: 1px 6px; +} + +/* Chapter Transition Toast */ +.chapter-toast { + position: absolute; + top: 24px; + left: 50%; + transform: translateX(-50%); + z-index: 100; + background: rgba(15, 15, 20, 0.88); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + border: 1px solid rgba(255, 255, 255, 0.15); + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5); + padding: 10px 18px; + border-radius: 30px; + display: flex; + align-items: center; + gap: 10px; + pointer-events: none; + animation: toastFadeIn 0.35s cubic-bezier(0.16, 1, 0.3, 1) forwards; +} + +.toast-badge { + background: linear-gradient(135deg, #7c3aed, #4f46e5); + color: #fff; + font-size: 12px; + font-weight: 700; + padding: 3px 9px; + border-radius: 12px; + white-space: nowrap; +} + +.toast-title { + color: #f3f4f6; + font-size: 14px; + font-weight: 600; + max-width: 320px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +@keyframes toastFadeIn { + from { + opacity: 0; + transform: translate(-50%, -12px) scale(0.95); + } + to { + opacity: 1; + transform: translate(-50%, 0) scale(1); + } +} + .history-url { flex: 1; font-size: 13px; diff --git a/src/components/MangaViewer.tsx b/src/components/MangaViewer.tsx index e8c0773..62e8245 100644 --- a/src/components/MangaViewer.tsx +++ b/src/components/MangaViewer.tsx @@ -2,6 +2,7 @@ import { invoke } from '@tauri-apps/api/core'; import { useState, useEffect, useCallback, useRef } from 'react'; import type { ViewerSettings } from '../types'; import type { ChapterData } from '../types'; +import { parseTitleAndEpisode } from '../utils/titleParser'; import { MangaPage } from './MangaPage'; import { ViewerToolbar } from './ViewerToolbar'; import { SettingsPanel } from './SettingsPanel'; @@ -82,9 +83,21 @@ export function MangaViewer({ const [dismissNavError, setDismissNavError] = useState(false); const [boundaryWarning, setBoundaryWarning] = useState<'first' | 'last' | null>(null); const [dimMap, setDimMap] = useState>({}); + const [toast, setToast] = useState<{ title: string; episode: string | null } | null>(null); const toolbarTimer = useRef | null>(null); const containerRef = useRef(null); + // Trigger Chapter Notification Toast on chapter load/change + useEffect(() => { + if (!chapter.title) return; + const parsed = parseTitleAndEpisode(chapter.title); + setToast(parsed); + const timer = setTimeout(() => { + setToast(null); + }, 2800); + return () => clearTimeout(timer); + }, [currentUrl, chapter.title]); + useEffect(() => { setDismissNavError(false); if (!navError) return; @@ -327,6 +340,14 @@ export function MangaViewer({ )} + {/* Chapter Transition Toast */} + {toast && ( +
+ {toast.episode && {toast.episode}} + {toast.title} +
+ )} + {/* Boundary Warning */} {boundaryWarning && (
diff --git a/src/components/UrlInput.tsx b/src/components/UrlInput.tsx index 255bc16..22cc8cd 100644 --- a/src/components/UrlInput.tsx +++ b/src/components/UrlInput.tsx @@ -1,5 +1,6 @@ import { useState, useRef, useEffect } from 'react'; import type { HistoryEntry } from '../types'; +import { parseTitleAndEpisode } from '../utils/titleParser'; interface UrlInputProps { onLoad: (url: string) => void; @@ -153,7 +154,7 @@ export function UrlInput({
{history.map((entry) => { - const displayTitle = entry.chapterTitle || (entry.title && entry.title !== entry.url ? entry.title : null); + const parsed = parseTitleAndEpisode(entry.chapterTitle || entry.title); return (