2026-05-08 12:20:24 +02:00
|
|
|
/**
|
|
|
|
|
* Nur Medien, die noch nicht im Fließtext eingebettet sind — ohne Doppel-Darstellung.
|
|
|
|
|
*/
|
|
|
|
|
import React, { useMemo, useState } from 'react'
|
|
|
|
|
import ExerciseMediaEmbed from './ExerciseMediaEmbed'
|
|
|
|
|
import ExerciseMediaThumbTile from './ExerciseMediaThumbTile'
|
2026-05-11 18:08:57 +02:00
|
|
|
import ReportContentModal from './ReportContentModal'
|
2026-05-08 12:20:24 +02:00
|
|
|
import { resolveExerciseMediaFileUrl } from '../utils/exerciseMediaUrl'
|
|
|
|
|
import {
|
|
|
|
|
collectInlineExerciseMediaIdsFromExercise,
|
|
|
|
|
} from '../utils/exerciseInlineMediaRefs'
|
|
|
|
|
|
|
|
|
|
function isTrashHidden(m) {
|
|
|
|
|
return String(m?.asset_lifecycle_state || 'active').toLowerCase() === 'trash_hidden'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default function ExerciseAttachmentMediaStrip({ exerciseId, exercise }) {
|
|
|
|
|
const [preview, setPreview] = useState(null)
|
2026-05-11 18:08:57 +02:00
|
|
|
const [reportTarget, setReportTarget] = useState(null)
|
2026-05-08 12:20:24 +02:00
|
|
|
const inlineIds = useMemo(() => collectInlineExerciseMediaIdsFromExercise(exercise), [exercise])
|
|
|
|
|
|
|
|
|
|
const orphans = useMemo(() => {
|
|
|
|
|
const list = (exercise?.media || []).filter((m) => m && !isTrashHidden(m))
|
|
|
|
|
return list.filter((m) => !inlineIds.has(Number(m.id)))
|
|
|
|
|
}, [exercise, inlineIds])
|
|
|
|
|
|
|
|
|
|
if (!orphans.length || exerciseId == null) return null
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<section className="card exercise-detail-section exercise-attachment-media-strip">
|
|
|
|
|
<h2>Angehängte Medien</h2>
|
|
|
|
|
<p style={{ marginTop: '6px', color: 'var(--text2)', fontSize: '0.88rem' }}>
|
|
|
|
|
Hier erscheinen nur Verknüpfungen, die noch nicht im Fließtext eingebettet sind (reine Material-Anhänge).
|
|
|
|
|
</p>
|
|
|
|
|
<div className="exercise-orphan-media-grid">
|
|
|
|
|
{orphans.map((m) => {
|
|
|
|
|
const lc = String(m.asset_lifecycle_state || 'active').toLowerCase()
|
|
|
|
|
const caption = (m.title || '').trim() || (m.original_filename || '').trim() || `Medium #${m.id}`
|
|
|
|
|
return (
|
|
|
|
|
<article key={m.id} className="exercise-orphan-media-card">
|
|
|
|
|
<div className="exercise-orphan-media-card__head">
|
|
|
|
|
<ExerciseMediaThumbTile
|
|
|
|
|
exerciseId={exerciseId}
|
|
|
|
|
media={m}
|
|
|
|
|
onOpenPreview={setPreview}
|
|
|
|
|
size={88}
|
|
|
|
|
/>
|
|
|
|
|
<div className="exercise-orphan-media-card__meta">
|
|
|
|
|
<strong className="exercise-orphan-media-card__title">{caption}</strong>
|
|
|
|
|
<span className="exercise-orphan-media-card__sub">
|
|
|
|
|
#{m.id}
|
|
|
|
|
{m.embed_platform ? ` · ${m.embed_platform}` : ''}
|
|
|
|
|
{m.media_type ? ` · ${m.media_type}` : ''}
|
|
|
|
|
</span>
|
|
|
|
|
{lc === 'trash_soft' && (
|
|
|
|
|
<span className="exercise-orphan-media-card__warn">Papierkorb (Stufe 1)</span>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<ExerciseMediaEmbed exerciseId={exerciseId} media={m} layoutSize="medium" />
|
2026-05-11 18:08:57 +02:00
|
|
|
{(m.asset_lifecycle_state || 'active') === 'active' && !m.asset_legal_hold_active && (
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => setReportTarget(m)}
|
|
|
|
|
style={{
|
|
|
|
|
background: 'none',
|
|
|
|
|
border: 'none',
|
|
|
|
|
padding: '4px 0 0',
|
|
|
|
|
cursor: 'pointer',
|
|
|
|
|
fontSize: '0.72rem',
|
|
|
|
|
color: 'var(--text3)',
|
|
|
|
|
textDecoration: 'underline',
|
|
|
|
|
textAlign: 'left',
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
Inhalt melden
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
2026-05-08 12:20:24 +02:00
|
|
|
</article>
|
|
|
|
|
)
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
2026-05-11 18:08:57 +02:00
|
|
|
{reportTarget && (
|
|
|
|
|
<ReportContentModal
|
|
|
|
|
targetType="media_asset"
|
|
|
|
|
targetId={reportTarget.media_asset_id || reportTarget.id}
|
|
|
|
|
targetLabel={reportTarget.title || reportTarget.original_filename || `Medium #${reportTarget.id}`}
|
|
|
|
|
onClose={() => setReportTarget(null)}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
|
2026-05-08 12:20:24 +02:00
|
|
|
{preview && (
|
|
|
|
|
<div
|
|
|
|
|
role="dialog"
|
|
|
|
|
aria-modal="true"
|
|
|
|
|
aria-label="Medienvorschau"
|
|
|
|
|
style={{
|
|
|
|
|
position: 'fixed',
|
|
|
|
|
inset: 0,
|
|
|
|
|
background: 'rgba(0,0,0,0.55)',
|
|
|
|
|
zIndex: 1001,
|
|
|
|
|
overflow: 'auto',
|
|
|
|
|
padding: '16px',
|
|
|
|
|
display: 'flex',
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
justifyContent: 'center',
|
|
|
|
|
}}
|
|
|
|
|
onClick={() => setPreview(null)}
|
|
|
|
|
onKeyDown={(e) => e.key === 'Escape' && setPreview(null)}
|
|
|
|
|
>
|
|
|
|
|
<div
|
|
|
|
|
className="card"
|
|
|
|
|
style={{ maxWidth: 720, width: '100%', maxHeight: '90vh', overflow: 'auto' }}
|
|
|
|
|
onClick={(e) => e.stopPropagation()}
|
|
|
|
|
>
|
|
|
|
|
<h3 style={{ marginTop: 0, fontSize: '1.05rem' }}>Vorschau</h3>
|
|
|
|
|
{preview.embed_url ? (
|
|
|
|
|
<p style={{ fontSize: '14px', wordBreak: 'break-all' }}>
|
|
|
|
|
<a href={preview.embed_url} target="_blank" rel="noreferrer">
|
|
|
|
|
{preview.embed_url}
|
|
|
|
|
</a>
|
|
|
|
|
</p>
|
|
|
|
|
) : preview.mime_type?.startsWith('video/') || preview.media_type === 'video' ? (
|
|
|
|
|
<video
|
|
|
|
|
src={resolveExerciseMediaFileUrl(exerciseId, preview)}
|
|
|
|
|
controls
|
|
|
|
|
style={{ width: '100%', borderRadius: '8px', maxHeight: '70vh' }}
|
|
|
|
|
/>
|
|
|
|
|
) : preview.mime_type?.startsWith('image/') || preview.media_type === 'image' ? (
|
|
|
|
|
<img
|
|
|
|
|
alt=""
|
|
|
|
|
src={resolveExerciseMediaFileUrl(exerciseId, preview)}
|
|
|
|
|
style={{ maxWidth: '100%', borderRadius: '8px', maxHeight: '70vh', objectFit: 'contain' }}
|
|
|
|
|
/>
|
|
|
|
|
) : (
|
|
|
|
|
<p style={{ fontSize: '14px' }}>
|
|
|
|
|
<a href={resolveExerciseMediaFileUrl(exerciseId, preview)} target="_blank" rel="noreferrer">
|
|
|
|
|
Datei öffnen
|
|
|
|
|
</a>
|
|
|
|
|
</p>
|
|
|
|
|
)}
|
|
|
|
|
<div style={{ marginTop: '16px' }}>
|
|
|
|
|
<button type="button" className="btn btn-secondary" onClick={() => setPreview(null)}>
|
|
|
|
|
Schließen
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</section>
|
|
|
|
|
)
|
|
|
|
|
}
|