feat: Serien-Suche, Admin-Favoriten, Pushover, Dashboard-Badge

This commit is contained in:
2026-06-18 02:21:06 +02:00
parent f77c3c60d4
commit 7f1ba07500
5 changed files with 219 additions and 23 deletions

View File

@@ -1903,7 +1903,7 @@ function GlobalSearch({ setActive, setDevToolsNav, mobile }) {
}
function Dashboard({ toast, mobile, setActive, setDevToolsNav, unreadMsgs=0, unreadBoard=0, unreadPromoted=0, onBoardRead, unreadChangelog=0, onChangelogRead, user }) {
function Dashboard({ toast, mobile, setActive, setDevToolsNav, unreadMsgs=0, unreadBoard=0, unreadPromoted=0, onBoardRead, unreadChangelog=0, onChangelogRead, unackedFavs=0, onAckFavs, user }) {
const [now, setNow] = useState(new Date());
const [showBoard, setShowBoard] = useState(false);
const [showChangelog, setShowChangelog] = useState(false);
@@ -1981,6 +1981,16 @@ function Dashboard({ toast, mobile, setActive, setDevToolsNav, unreadMsgs=0, unr
borderRadius:'50%', background:'#ffe66d', boxShadow:'0 0 5px #ffe66d' }}/>
)}
</button>
{/* Admin: unquittierte Favoriten */}
{user?.role==='admin' && unackedFavs > 0 && (
<button onClick={()=>{ setActive('media'); onAckFavs(); }} style={{
background:'rgba(245,158,11,0.12)', border:'1px solid rgba(245,158,11,0.35)',
borderRadius:7, color:'#f59e0b', cursor:'pointer',
padding:'4px 10px', fontSize:9, fontFamily:'monospace', letterSpacing:1,
}}>
⭐ {unackedFavs} neue Favorit{unackedFavs!==1?'en':''}
</button>
)}
</div>
</div>
);
@@ -3608,6 +3618,7 @@ export default function App() {
const [unreadBoard, setUnreadBoard] = useState(0);
const [unreadPromoted, setUnreadPromoted] = useState(0);
const [unreadChangelog,setUnreadChangelog]= useState(0);
const [unackedFavs, setUnackedFavs] = useState(0);
const [authChecked, setAuthChecked] = useState(false);
useEffect(()=>{

View File

@@ -165,6 +165,14 @@ function MovieCard({ movie, favIds, hasXrel, onToggleFav, onOpenModal }) {
: <div style={{ width:'100%', height:'100%', display:'flex', alignItems:'center',
justifyContent:'center', fontSize:36, color:'rgba(255,255,255,0.1)' }}>🎬</div>
}
{/* TV-Badge */}
{movie.media_type === 'tv' && (
<div style={{ position:'absolute', top:6, left:6, background:'rgba(96,165,250,0.85)',
borderRadius:5, padding:'2px 7px', fontSize:9, fontWeight:700,
fontFamily:'monospace', color:'#fff', letterSpacing:1 }}>
SERIE
</div>
)}
{/* Top-Badge */}
{isTop && (
<div style={{ position:'absolute', bottom:6, left:6, background:'rgba(0,0,0,0.85)',
@@ -257,6 +265,7 @@ function useFavIds() {
release_date: movie.release_date || '',
release_date_de: fmtDE(movie.release_date),
genres: movie.genres || [], fsk: movie.fsk || '',
media_type: movie.media_type || 'movie',
}});
setFavIds(prev => new Set([...prev, movie.id]));
}
@@ -422,7 +431,7 @@ function SucheGrid({ onOpenModal, xrelIds, onXrelLoaded }) {
value={query}
onChange={e => setQuery(e.target.value)}
onKeyDown={onKey}
placeholder="Filmtitel eingeben…"
placeholder="Film- oder Serientitel eingeben…"
style={{
flex:1, background:'rgba(255,255,255,0.06)', border:'1px solid rgba(255,255,255,0.15)',
borderRadius:8, padding:'9px 14px', color:'#fff', fontFamily:"'Space Mono',monospace",
@@ -467,15 +476,29 @@ function SucheGrid({ onOpenModal, xrelIds, onXrelLoaded }) {
// ── Favoriten Kalender ────────────────────────────────────────────────────────
function FavoritenKalender({ onOpenModal }) {
const [favs, setFavs] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [favs, setFavs] = useState([]);
const [allFavs, setAllFavs] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [adminTab, setAdminTab]= useState(false);
const [isAdmin, setIsAdmin] = useState(false);
useEffect(() => {
setLoading(true); setError('');
api('/tools/media/favorites').then(setFavs).catch(e => setError(e.message)).finally(() => setLoading(false));
Promise.all([
api('/tools/media/favorites'),
api('/tools/media/favorites/all').catch(() => null),
]).then(([mine, all]) => {
setFavs(mine);
if (all) { setAllFavs(all); setIsAdmin(true); }
}).catch(e => setError(e.message)).finally(() => setLoading(false));
}, []);
const ackAll = async () => {
await api('/tools/media/favorites/acknowledge-all', { method:'POST', body:{} });
setAllFavs(prev => prev.map(f => ({ ...f, acknowledged: 1 })));
};
const removeFav = async (tmdbId) => {
try {
await api(`/tools/media/favorites/${tmdbId}`, { method:'DELETE' });
@@ -485,7 +508,79 @@ function FavoritenKalender({ onOpenModal }) {
if (loading) return <StatusBox>⏳ Lädt…</StatusBox>;
if (error) return <ErrorBox msg={error} />;
if (!favs.length) return <StatusBox>Noch keine Favoriten markiere Filme mit ❤</StatusBox>;
// Admin-Ansicht: alle Favoriten aller User
if (isAdmin && adminTab) {
const unacked = allFavs.filter(f => !f.acknowledged).length;
return (
<div>
<div style={{display:'flex',gap:8,marginBottom:16,alignItems:'center'}}>
<button onClick={()=>setAdminTab(false)}
style={{background:'rgba(255,255,255,0.06)',border:'1px solid rgba(255,255,255,0.12)',
borderRadius:7,padding:'5px 12px',color:'rgba(255,255,255,0.6)',fontFamily:'monospace',fontSize:11,cursor:'pointer'}}>
← Meine Favoriten
</button>
{unacked > 0 && (
<button onClick={ackAll}
style={{background:'rgba(245,158,11,0.12)',border:'1px solid rgba(245,158,11,0.35)',
borderRadius:7,padding:'5px 12px',color:'#f59e0b',fontFamily:'monospace',fontSize:11,cursor:'pointer'}}>
✓ Alle {unacked} quittieren
</button>
)}
</div>
<div style={{display:'flex',flexDirection:'column',gap:6}}>
{allFavs.map(f => (
<div key={f.id} style={{display:'flex',alignItems:'center',gap:10,padding:'8px 12px',
background: !f.acknowledged ? 'rgba(245,158,11,0.06)' : 'rgba(255,255,255,0.02)',
border:`1px solid ${!f.acknowledged ? 'rgba(245,158,11,0.25)' : 'rgba(255,255,255,0.06)'}`,
borderRadius:8}}>
{f.poster_path
? <img src={POSTER+f.poster_path} alt={f.title} style={{width:32,height:48,objectFit:'cover',borderRadius:4,flexShrink:0}}/>
: <div style={{width:32,height:48,background:'#111',borderRadius:4,flexShrink:0,display:'flex',alignItems:'center',justifyContent:'center',fontSize:14}}>
{f.media_type==='tv'?'📺':'🎬'}
</div>
}
<div style={{flex:1,minWidth:0}}>
<div style={{color:'rgba(255,255,255,0.85)',fontFamily:'monospace',fontSize:12,
overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap'}}>
{f.title}
{f.media_type==='tv' && <span style={{marginLeft:6,color:'#60a5fa',fontSize:9}}>SERIE</span>}
</div>
<div style={{color:'rgba(255,255,255,0.35)',fontFamily:'monospace',fontSize:10,marginTop:2}}>
👤 {f.username} · ⭐ {f.added_at ? new Date(f.added_at).toLocaleDateString('de-DE') : '—'}
{!!f.acknowledged && <span style={{marginLeft:6,color:'rgba(255,255,255,0.2)'}}>✓ quittiert</span>}
</div>
</div>
{!f.acknowledged && (
<button onClick={async(e)=>{e.stopPropagation();
await api(`/tools/media/favorites/${f.id}/acknowledge`,{method:'POST',body:{}});
setAllFavs(prev=>prev.map(x=>x.id===f.id?{...x,acknowledged:1}:x));
}}
style={{background:'rgba(245,158,11,0.1)',border:'1px solid rgba(245,158,11,0.3)',
borderRadius:6,padding:'3px 10px',color:'#f59e0b',fontFamily:'monospace',fontSize:9,cursor:'pointer',flexShrink:0}}>
</button>
)}
</div>
))}
{!allFavs.length && <StatusBox>Noch keine Favoriten von irgendjemandem.</StatusBox>}
</div>
</div>
);
}
if (!favs.length) return (
<div>
{isAdmin && (
<button onClick={()=>setAdminTab(true)}
style={{marginBottom:12,background:'rgba(255,255,255,0.05)',border:'1px solid rgba(255,255,255,0.1)',
borderRadius:7,padding:'5px 12px',color:'rgba(255,255,255,0.5)',fontFamily:'monospace',fontSize:11,cursor:'pointer'}}>
👥 Alle Favoriten ({allFavs.length})
</button>
)}
<StatusBox>Noch keine Favoriten markiere Filme mit ❤</StatusBox>
</div>
);
const groups = {};
for (const f of favs) {
@@ -498,6 +593,20 @@ function FavoritenKalender({ onOpenModal }) {
return (
<div style={{ display:'flex', flexDirection:'column', gap:24 }}>
{isAdmin && (
<button onClick={()=>setAdminTab(true)}
style={{marginBottom:4,background:'rgba(255,255,255,0.05)',border:'1px solid rgba(255,255,255,0.1)',
borderRadius:7,padding:'5px 12px',color:'rgba(255,255,255,0.5)',fontFamily:'monospace',fontSize:11,cursor:'pointer',
display:'flex',alignItems:'center',gap:6,width:'fit-content'}}>
👥 Alle Favoriten ({allFavs.length})
{allFavs.filter(f=>!f.acknowledged).length > 0 && (
<span style={{background:'rgba(245,158,11,0.2)',border:'1px solid rgba(245,158,11,0.4)',
borderRadius:10,padding:'1px 7px',color:'#f59e0b',fontSize:9}}>
{allFavs.filter(f=>!f.acknowledged).length} neu
</span>
)}
</button>
)}
{Object.entries(groups).sort(([a],[b]) => a.localeCompare(b)).map(([key, group]) => (
<div key={key}>
<div style={{ ...S.head, marginBottom:10, display:'flex', alignItems:'center', gap:8 }}>