// Main App — top nav layout + Google SSO auth gate const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{ "queryMode": 0, "defaultLang": "zh", "accentColor": "#6366f1" }/*EDITMODE-END*/; // ── Main App ── const App = () => { const [lang, setLang] = React.useState(TWEAK_DEFAULTS.defaultLang || 'zh'); const [page, setPage] = React.useState('select'); const [selectedShow, setSelectedShow] = React.useState(null); const [selectedEpisode, setSelectedEpisode] = React.useState(null); const [initSearch, setInitSearch] = React.useState(''); const [highlightTime, setHighlightTime] = React.useState(null); const [tweaksVisible, setTweaksVisible] = React.useState(false); const [tweaks, setTweaks] = React.useState(TWEAK_DEFAULTS); const [landingQuery, setLandingQuery] = React.useState(''); const { user, loading: userLoading, refresh: refreshUser, logout: doLogout } = useCurrentUser(); const isAdmin = user && user.role === 'admin' && user.status === 'active'; React.useEffect(() => { const handler = (e) => { if (e.data?.type === '__activate_edit_mode') setTweaksVisible(true); if (e.data?.type === '__deactivate_edit_mode') setTweaksVisible(false); }; window.addEventListener('message', handler); window.parent.postMessage({ type: '__edit_mode_available' }, '*'); return () => window.removeEventListener('message', handler); }, []); // Presentation routing via URL hash. `#presentation` shows fullscreen deck; // clearing the hash exits back to the previous page (state preserved). const [prevPage, setPrevPage] = React.useState('select'); React.useEffect(() => { const sync = () => { if (window.location.hash === '#presentation') { setPage(p => { if (p !== 'presentation') setPrevPage(p); return 'presentation'; }); } else { setPage(p => (p === 'presentation' ? prevPage : p)); } }; sync(); window.addEventListener('hashchange', sync); return () => window.removeEventListener('hashchange', sync); }, [prevPage]); const applyTweak = (key, val) => { setTweaks(t => ({ ...t, [key]: val })); window.parent.postMessage({ type: '__edit_mode_set_keys', edits: { [key]: val } }, '*'); }; const t = lang === 'zh'; const handleAdminClick = () => { if (!user) { window.location.href = googleLoginUrl(); return; } if (!isAdmin) { window.alert(t ? '此頁面僅限管理員使用' : 'Admin role required'); return; } setPage('admin-api'); }; const handleSignIn = () => { window.location.href = googleLoginUrl(); }; const handleSetPage = (p) => { if (p === 'select') { setSelectedShow(null); setSelectedEpisode(null); } if (p && p.startsWith('admin') && !isAdmin) { // Guard direct hash navigation to admin pages if (!user) { window.location.href = googleLoginUrl(); return; } window.alert(t ? '此頁面僅限管理員使用' : 'Admin role required'); return; } setPage(p); }; return (