All checks were successful
Deploy Development / deploy (push) Successful in 40s
Test Suite / pytest-backend (push) Successful in 35s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 10s
Test Suite / playwright-tests (push) Successful in 1m1s
93 lines
3.8 KiB
JavaScript
93 lines
3.8 KiB
JavaScript
/**
|
|
* 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'
|
|
import MediaPreviewModal from './MediaPreviewModal'
|
|
import ReportContentModal from './ReportContentModal'
|
|
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)
|
|
const [reportTarget, setReportTarget] = useState(null)
|
|
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" />
|
|
</article>
|
|
)
|
|
})}
|
|
</div>
|
|
|
|
{preview && (
|
|
<MediaPreviewModal
|
|
title={(preview.title || '').trim() || preview.original_filename || `Medium #${preview.id}`}
|
|
media={preview}
|
|
fileUrl={preview.embed_url ? null : resolveExerciseMediaFileUrl(exerciseId, preview)}
|
|
onClose={() => setPreview(null)}
|
|
onReport={
|
|
(preview.asset_lifecycle_state || 'active') === 'active' && !preview.asset_legal_hold_active
|
|
? () => {
|
|
setReportTarget(preview)
|
|
setPreview(null)
|
|
}
|
|
: null
|
|
}
|
|
/>
|
|
)}
|
|
|
|
{reportTarget && (
|
|
<ReportContentModal
|
|
targetType="media_asset"
|
|
targetId={reportTarget.media_asset_id || reportTarget.id}
|
|
targetLabel={reportTarget.title || reportTarget.original_filename || `Medium #${reportTarget.id}`}
|
|
onClose={() => setReportTarget(null)}
|
|
/>
|
|
)}
|
|
</section>
|
|
)
|
|
}
|