feat(P-13): update app version to 0.8.89, implement MediaPreviewModal and reporting functionality across media components
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
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
This commit is contained in:
parent
0dbcd4175c
commit
24bf3f7035
|
|
@ -1,6 +1,6 @@
|
|||
# Shinkan Jinkendo Version Information
|
||||
|
||||
APP_VERSION = "0.8.88"
|
||||
APP_VERSION = "0.8.89"
|
||||
BUILD_DATE = "2026-05-11"
|
||||
DB_SCHEMA_VERSION = "20260511052"
|
||||
|
||||
|
|
@ -30,10 +30,19 @@ MODULE_VERSIONS = {
|
|||
"membership": "1.0.0",
|
||||
"catalogs": "1.5.0", # Updated: Trainer Contexts API (Migration 012)
|
||||
"maturity_models": "1.4.0", # matrix_stack_bundle: vollständiger Katalog+Modelle+Bindings Export/Import
|
||||
"content_reports": "1.1.0", # P-13: Melde-Button in Medienbibliothek + ExerciseAttachmentMediaStrip
|
||||
"content_reports": "1.2.0", # P-13: Viewer konsolidiert — MediaPreviewModal (geteilt); Melden in ExerciseFormPage-Viewer
|
||||
}
|
||||
|
||||
CHANGELOG = [
|
||||
{
|
||||
"version": "0.8.89",
|
||||
"date": "2026-05-11",
|
||||
"changes": [
|
||||
"Feat P-13: MediaPreviewModal — geteilter Medienvorschau-Dialog für alle Kontexte (Bibliothek, Übungsbearbeitung, angehängte Medien); optionale Melden- und Bearbeiten-Buttons im Header.",
|
||||
"Feat P-13: Melde-Button im ExerciseFormPage-Viewer (verlinktes Medium → Vorschau → Melden).",
|
||||
"Refactor: Inline-Vorschau-Blöcke in MediaLibraryPage, ExerciseFormPage und ExerciseAttachmentMediaStrip durch MediaPreviewModal ersetzt.",
|
||||
],
|
||||
},
|
||||
{
|
||||
"version": "0.8.88",
|
||||
"date": "2026-05-11",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
# Shinkan Jinkendo – Entwicklungsstand & Handover
|
||||
|
||||
**Stand:** 2026-05-07
|
||||
**App-Version / DB-Schema:** App **0.8.59**, DB-Schema siehe `backend/version.py` (`DB_SCHEMA_VERSION`)
|
||||
**Stand:** 2026-05-11
|
||||
**App-Version / DB-Schema:** App **0.8.89**, DB-Schema siehe `backend/version.py` (`DB_SCHEMA_VERSION`)
|
||||
|
||||
Diese Datei ist die **Einstiegs-Doku für neue Chat-Sessions**: Anforderungen im Detail stehen in `.claude/docs/` (siehe unten); hier der **implementierte Stand**, **Medien-Meilenstein** und **sinnvolle nächste Schritte**.
|
||||
|
||||
|
|
@ -119,13 +119,38 @@ Das Schema ist gegenüber dem Code zurück: Migration **`022_skills_schema_compl
|
|||
|
||||
---
|
||||
|
||||
## 5b. P-13: Content-Meldeverfahren (vollständig implementiert, 2026-05-11)
|
||||
|
||||
**DSA-konformes Meldeverfahren (KRIT-03) — App 0.8.87–0.8.89.**
|
||||
|
||||
**Backend (`backend/routers/content_reports.py`, Migration 052):**
|
||||
- `POST /api/content-reports` — optionale Auth; `official`-Medien ohne Login meldbar.
|
||||
- `GET /api/me/inbox/content-reports` — Plattform-Admin-Postfach.
|
||||
- `PATCH /api/content-reports/{id}` — Status-Übergang (submitted → in_review → resolved/dismissed).
|
||||
- `POST /api/content-reports/{id}/legal-hold` — Superadmin; integriert P-11 `set_legal_hold()`.
|
||||
- Automatische Priorisierung `high` für `minors`/`illegal_content`/`youth_protection`.
|
||||
- 15 Backend-Tests in `backend/tests/test_p13_content_reports.py`.
|
||||
|
||||
**Frontend:**
|
||||
- `ReportContentModal.jsx` — Melde-Formular (Grund, Beschreibung, Name, E-Mail, Gutglaubenserklärung).
|
||||
- `MediaPreviewModal.jsx` — geteilter Vorschau-Dialog; optionale Melden- und Bearbeiten-Buttons.
|
||||
- `InboxPage.jsx` — zweiter Abschnitt „Inhaltsmeldungen” für Plattform-Admins mit `ReportDetailModal`.
|
||||
- `OrgInboxContext.jsx` — liefert `contentReports`, `contentReportCount`, `canAccessContentReports`.
|
||||
- Melde-Button in **MediaLibraryPage** (Grid + Liste + Viewer), **ExerciseFormPage** (Viewer), **ExerciseAttachmentMediaStrip** (Viewer).
|
||||
|
||||
**Offen (explizit zurückgestellt):**
|
||||
- Melde-Einstieg im Coaching-Modus (Feedback-Schritt, nicht kritisch).
|
||||
|
||||
---
|
||||
|
||||
## 6. Nächste Session — sinnvolle Arbeitspakete
|
||||
|
||||
1. **Inline §11:** Syntax festlegen (z. B. `{{exerciseMedia:id}}` → kanonisches HTML), Server normalisieren bei Speichern, einen **`renderExerciseRichText()`**-Pfad im Frontend.
|
||||
2. **Tests:** pytest für `media_assets`-Router (Leserechte, Lifecycle, `from-asset`); ggf. Snapshot der Pfad-Umzug-Logik.
|
||||
3. **Retention:** Job-Dokumentation + Betrieb (ENV, Intervall); Dry-Run beschreiben.
|
||||
4. **S3/Adapter:** Speicher-Abstraktion §7 — wenn Produkt es verlangt.
|
||||
5. **Rahmen/UI:** Kalender „aus Rahmen übernehmen“ weiter anbinden (parallel, unabhängig von Medien).
|
||||
1. **Frontend testen:** P-13 Melde-Flow in Medienbibliothek und Übungsbearbeitung manuell durchspielen.
|
||||
2. **Inline §11:** Syntax festlegen (`{{exerciseMedia:id}}` → kanonisches HTML), Server normalisieren bei Speichern, einen `renderExerciseRichText()`-Pfad im Frontend.
|
||||
3. **Tests:** pytest für `media_assets`-Router (Leserechte, Lifecycle, `from-asset`); ggf. Snapshot der Pfad-Umzug-Logik.
|
||||
4. **Retention:** Job-Dokumentation + Betrieb (ENV, Intervall); Dry-Run beschreiben.
|
||||
5. **S3/Adapter:** Speicher-Abstraktion §7 — wenn Produkt es verlangt.
|
||||
6. **Rahmen/UI:** Kalender „aus Rahmen übernehmen“ weiter anbinden (parallel, unabhängig von Medien).
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -4,11 +4,10 @@
|
|||
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'
|
||||
import { collectInlineExerciseMediaIdsFromExercise } from '../utils/exerciseInlineMediaRefs'
|
||||
|
||||
function isTrashHidden(m) {
|
||||
return String(m?.asset_lifecycle_state || 'active').toLowerCase() === 'trash_hidden'
|
||||
|
|
@ -58,28 +57,28 @@ export default function ExerciseAttachmentMediaStrip({ exerciseId, exercise }) {
|
|||
</div>
|
||||
</div>
|
||||
<ExerciseMediaEmbed exerciseId={exerciseId} media={m} layoutSize="medium" />
|
||||
{(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>
|
||||
)}
|
||||
</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"
|
||||
|
|
@ -88,65 +87,6 @@ export default function ExerciseAttachmentMediaStrip({ exerciseId, exercise }) {
|
|||
onClose={() => setReportTarget(null)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
140
frontend/src/components/MediaPreviewModal.jsx
Normal file
140
frontend/src/components/MediaPreviewModal.jsx
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
/**
|
||||
* Gemeinsamer Medienvorschau-Dialog.
|
||||
*
|
||||
* Props:
|
||||
* title – Anzeigename des Mediums (Dateiname o.ä.)
|
||||
* media – Medienobjekt (mime_type, embed_url, asset_legal_hold_active, media_type)
|
||||
* fileUrl – Bereits aufgelöste Datei-URL (null für Embed-only-Medien)
|
||||
* onClose – Pflicht
|
||||
* onReport – optional; wenn gesetzt, erscheint "Melden"-Button
|
||||
* onEdit – optional; wenn gesetzt, erscheint "Bearbeiten"-Button
|
||||
*/
|
||||
import React from 'react'
|
||||
|
||||
export default function MediaPreviewModal({ title, media, fileUrl, onClose, onReport, onEdit }) {
|
||||
const mlow = (media?.mime_type || '').toLowerCase()
|
||||
const isHeic = mlow.includes('heic') || mlow.includes('heif')
|
||||
const isImage = !isHeic && (media?.mime_type?.startsWith('image/') || media?.media_type === 'image')
|
||||
const isVideo = media?.mime_type?.startsWith('video/') || media?.media_type === 'video'
|
||||
const isPdf = mlow.includes('pdf')
|
||||
const isLegalHold = !!media?.asset_legal_hold_active
|
||||
const canReport = onReport && !isLegalHold
|
||||
|
||||
function handleBackdrop(e) {
|
||||
if (e.target === e.currentTarget) onClose()
|
||||
}
|
||||
|
||||
function handleKeyDown(e) {
|
||||
if (e.key === 'Escape') onClose()
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="Medienvorschau"
|
||||
style={{
|
||||
position: 'fixed',
|
||||
inset: 0,
|
||||
background: 'rgba(0,0,0,0.55)',
|
||||
zIndex: 1100,
|
||||
overflow: 'auto',
|
||||
padding: '16px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
onClick={handleBackdrop}
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
<div
|
||||
className="card"
|
||||
style={{ maxWidth: 720, width: '100%', maxHeight: '90vh', overflow: 'auto', padding: '1.25rem' }}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* Header */}
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1rem', gap: '0.5rem' }}>
|
||||
<h3 style={{ margin: 0, fontSize: '1rem', fontWeight: 600, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||||
{title || 'Vorschau'}
|
||||
</h3>
|
||||
<div style={{ display: 'flex', gap: '0.4rem', flexShrink: 0 }}>
|
||||
{canReport && (
|
||||
<button type="button" className="btn btn-secondary" style={{ fontSize: '0.85rem', padding: '5px 10px' }} onClick={onReport}>
|
||||
Melden
|
||||
</button>
|
||||
)}
|
||||
{onEdit && (
|
||||
<button type="button" className="btn btn-secondary" style={{ fontSize: '0.85rem', padding: '5px 10px' }} onClick={onEdit}>
|
||||
Bearbeiten
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary"
|
||||
style={{ fontSize: '0.85rem', padding: '5px 10px' }}
|
||||
onClick={onClose}
|
||||
aria-label="Schließen"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Body */}
|
||||
{isLegalHold ? (
|
||||
<p style={{ color: 'var(--danger)', fontSize: '0.9rem' }}>
|
||||
Dieses Medium ist gesperrt und steht nicht zur Verfügung.
|
||||
</p>
|
||||
) : media?.embed_url ? (
|
||||
<p style={{ fontSize: '14px', wordBreak: 'break-all' }}>
|
||||
<a href={media.embed_url} target="_blank" rel="noreferrer">{media.embed_url}</a>
|
||||
</p>
|
||||
) : isHeic ? (
|
||||
<div>
|
||||
<p style={{ color: 'var(--text2)', fontSize: '0.9rem' }}>
|
||||
HEIC/HEIF — in diesem Browser oft keine eingebettete Vorschau. Zum Ansehen herunterladen oder im neuen Tab öffnen.
|
||||
</p>
|
||||
{fileUrl && (
|
||||
<a className="btn btn-secondary" href={fileUrl} target="_blank" rel="noopener noreferrer">
|
||||
Datei öffnen
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
) : isImage ? (
|
||||
<img
|
||||
key={media?.id}
|
||||
alt=""
|
||||
src={fileUrl}
|
||||
style={{ maxWidth: '100%', borderRadius: '8px', maxHeight: '70vh', objectFit: 'contain', display: 'block' }}
|
||||
/>
|
||||
) : isVideo ? (
|
||||
<video
|
||||
key={media?.id}
|
||||
src={fileUrl}
|
||||
controls
|
||||
playsInline
|
||||
preload="metadata"
|
||||
style={{ width: '100%', borderRadius: '8px', maxHeight: '70vh' }}
|
||||
>
|
||||
Wiedergabe nicht unterstützt.
|
||||
</video>
|
||||
) : isPdf ? (
|
||||
<div>
|
||||
<p style={{ color: 'var(--text2)', fontSize: '0.9rem' }}>PDF — zur Ansicht im neuen Tab öffnen.</p>
|
||||
{fileUrl && (
|
||||
<a className="btn btn-secondary" href={fileUrl} target="_blank" rel="noopener noreferrer">
|
||||
PDF öffnen
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
) : fileUrl ? (
|
||||
<p style={{ fontSize: '14px' }}>
|
||||
<a href={fileUrl} target="_blank" rel="noreferrer">Datei öffnen</a>
|
||||
</p>
|
||||
) : (
|
||||
<p style={{ color: 'var(--text2)', fontSize: '0.9rem' }}>Keine Vorschau verfügbar.</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -5,6 +5,8 @@ import { resolveExerciseMediaFileUrl, resolveMediaAssetFileUrl } from '../utils/
|
|||
import RichTextEditor from '../components/RichTextEditor'
|
||||
import ExerciseProgressionGraphPanel from '../components/ExerciseProgressionGraphPanel'
|
||||
import ExerciseMediaThumbTile from '../components/ExerciseMediaThumbTile'
|
||||
import MediaPreviewModal from '../components/MediaPreviewModal'
|
||||
import ReportContentModal from '../components/ReportContentModal'
|
||||
import {
|
||||
SHINKAN_EXERCISE_MEDIA_DRAG_MIME,
|
||||
buildExerciseMediaDragPayload,
|
||||
|
|
@ -391,6 +393,7 @@ function ExerciseFormPage() {
|
|||
const [archiveItems, setArchiveItems] = useState([])
|
||||
const [archiveError, setArchiveError] = useState(null)
|
||||
const [mediaPreview, setMediaPreview] = useState(null)
|
||||
const [reportTarget, setReportTarget] = useState(null)
|
||||
|
||||
useEffect(() => {
|
||||
const next = {}
|
||||
|
|
@ -1652,66 +1655,28 @@ function ExerciseFormPage() {
|
|||
</div>
|
||||
)}
|
||||
{mediaPreview && (
|
||||
<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={() => setMediaPreview(null)}
|
||||
onKeyDown={(e) => e.key === 'Escape' && setMediaPreview(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>
|
||||
{mediaPreview.asset_legal_hold_active ? (
|
||||
<p style={{ color: 'var(--danger)', fontSize: '0.9rem' }}>
|
||||
Dieses Medium ist gesperrt und steht nicht zur Verfügung.
|
||||
</p>
|
||||
) : mediaPreview.embed_url ? (
|
||||
<p style={{ fontSize: '14px', wordBreak: 'break-all' }}>
|
||||
<a href={mediaPreview.embed_url} target="_blank" rel="noreferrer">
|
||||
{mediaPreview.embed_url}
|
||||
</a>
|
||||
</p>
|
||||
) : mediaPreview.mime_type?.startsWith('video/') || mediaPreview.media_type === 'video' ? (
|
||||
<video
|
||||
src={resolveExerciseMediaFileUrl(exerciseId, mediaPreview)}
|
||||
controls
|
||||
style={{ width: '100%', borderRadius: '8px', maxHeight: '70vh' }}
|
||||
/>
|
||||
) : mediaPreview.mime_type?.startsWith('image/') || mediaPreview.media_type === 'image' ? (
|
||||
<img
|
||||
alt=""
|
||||
src={resolveExerciseMediaFileUrl(exerciseId, mediaPreview)}
|
||||
style={{ maxWidth: '100%', borderRadius: '8px', maxHeight: '70vh', objectFit: 'contain' }}
|
||||
/>
|
||||
) : (
|
||||
<p style={{ fontSize: '14px' }}>
|
||||
<a href={resolveExerciseMediaFileUrl(exerciseId, mediaPreview)} target="_blank" rel="noreferrer">
|
||||
Datei öffnen
|
||||
</a>
|
||||
</p>
|
||||
)}
|
||||
<div style={{ marginTop: '16px' }}>
|
||||
<button type="button" className="btn btn-secondary" onClick={() => setMediaPreview(null)}>
|
||||
Schließen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<MediaPreviewModal
|
||||
title={(mediaPreview.title || '').trim() || mediaPreview.original_filename || `Medium #${mediaPreview.id}`}
|
||||
media={mediaPreview}
|
||||
fileUrl={mediaPreview.embed_url ? null : resolveExerciseMediaFileUrl(exerciseId, mediaPreview)}
|
||||
onClose={() => setMediaPreview(null)}
|
||||
onReport={
|
||||
!mediaPreview.asset_legal_hold_active
|
||||
? () => {
|
||||
setReportTarget(mediaPreview)
|
||||
setMediaPreview(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)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import { activeClubMemberships } from '../utils/activeClub'
|
|||
import { resolveMediaAssetFileUrl } from '../utils/exerciseMediaUrl'
|
||||
import RightsDeclarationDialog from '../components/RightsDeclarationDialog'
|
||||
import ReportContentModal from '../components/ReportContentModal'
|
||||
import MediaPreviewModal from '../components/MediaPreviewModal'
|
||||
|
||||
const LC_OPTIONS = [
|
||||
{ value: 'active', label: 'Aktiv' },
|
||||
|
|
@ -1097,121 +1098,18 @@ export default function MediaLibraryPage() {
|
|||
</div>
|
||||
|
||||
{preview ? (
|
||||
<div
|
||||
className="media-library__overlay media-library__overlay--preview"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="preview-media-title"
|
||||
onClick={() => setPreview(null)}
|
||||
>
|
||||
<div
|
||||
className="media-library__modal media-library__modal--preview"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="media-library__modal-head">
|
||||
<h2 id="preview-media-title" className="media-library__preview-title">
|
||||
{preview.original_filename || `Medium #${preview.id}`}
|
||||
</h2>
|
||||
<div className="media-library__preview-head-actions">
|
||||
{(preview.lifecycle_state || 'active') === 'active' && !preview.legal_hold_active && (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary btn-small"
|
||||
onClick={() => {
|
||||
setReportTarget(preview)
|
||||
setPreview(null)
|
||||
}}
|
||||
>
|
||||
Melden
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary btn-small"
|
||||
onClick={() => {
|
||||
openEdit(preview)
|
||||
}}
|
||||
>
|
||||
Bearbeiten
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="media-library__icon-btn"
|
||||
onClick={() => setPreview(null)}
|
||||
aria-label="Vorschau schließen"
|
||||
>
|
||||
<X size={22} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="media-library__preview-body">
|
||||
{(() => {
|
||||
const url = resolveMediaAssetFileUrl(preview.id)
|
||||
const kind = previewDisplayKind(preview.mime_type)
|
||||
const mlow = (preview.mime_type || '').toLowerCase()
|
||||
if (!url) {
|
||||
return <p className="media-library__hint">Keine Datei-URL.</p>
|
||||
}
|
||||
if (mlow.includes('heic') || mlow.includes('heif')) {
|
||||
return (
|
||||
<div className="media-library__preview-fallback">
|
||||
<p className="media-library__hint">
|
||||
HEIC/HEIF — in diesem Browser oft keine eingebettete Vorschau. Zum Ansehen herunterladen oder im neuen Tab öffnen.
|
||||
</p>
|
||||
<a className="btn btn-secondary" href={url} target="_blank" rel="noopener noreferrer">
|
||||
Datei öffnen
|
||||
</a>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (kind === 'image') {
|
||||
return (
|
||||
<img
|
||||
key={preview.id}
|
||||
className="media-library__preview-img"
|
||||
src={url}
|
||||
alt=""
|
||||
/>
|
||||
)
|
||||
}
|
||||
if (kind === 'video') {
|
||||
return (
|
||||
<video
|
||||
key={preview.id}
|
||||
className="media-library__preview-video"
|
||||
src={url}
|
||||
controls
|
||||
playsInline
|
||||
preload="metadata"
|
||||
>
|
||||
Wiedergabe nicht unterstützt.
|
||||
</video>
|
||||
)
|
||||
}
|
||||
if (kind === 'pdf') {
|
||||
return (
|
||||
<div className="media-library__preview-fallback">
|
||||
<p className="media-library__hint">PDF — zur Ansicht im neuen Tab öffnen.</p>
|
||||
<a className="btn btn-secondary" href={url} target="_blank" rel="noopener noreferrer">
|
||||
PDF öffnen
|
||||
</a>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div className="media-library__preview-fallback">
|
||||
<p className="media-library__hint">
|
||||
Vorschau für diesen Typ nicht verfügbar ({preview.mime_type || 'unbekannt'}).
|
||||
</p>
|
||||
<a className="btn btn-secondary" href={url} target="_blank" rel="noopener noreferrer">
|
||||
Datei öffnen
|
||||
</a>
|
||||
</div>
|
||||
)
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<MediaPreviewModal
|
||||
title={preview.original_filename || `Medium #${preview.id}`}
|
||||
media={{ ...preview, asset_legal_hold_active: preview.legal_hold_active }}
|
||||
fileUrl={resolveMediaAssetFileUrl(preview.id)}
|
||||
onClose={() => setPreview(null)}
|
||||
onReport={
|
||||
(preview.lifecycle_state || 'active') === 'active' && !preview.legal_hold_active
|
||||
? () => { setReportTarget(preview); setPreview(null) }
|
||||
: null
|
||||
}
|
||||
onEdit={() => { openEdit(preview); setPreview(null) }}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{bulkOpen ? (
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// Shinkan Jinkendo Frontend Version
|
||||
|
||||
export const APP_VERSION = "0.8.88"
|
||||
export const APP_VERSION = "0.8.89"
|
||||
export const BUILD_DATE = "2026-05-11"
|
||||
|
||||
export const PAGE_VERSIONS = {
|
||||
|
|
@ -20,12 +20,14 @@ export const PAGE_VERSIONS = {
|
|||
TrainingCoachPage: "1.0.0",
|
||||
AdminCatalogsPage: "2.2.0",
|
||||
TrainerContextsPage: "1.0.0",
|
||||
MediaLibraryPage: "1.6.0", // P-11: Legal-Hold-Badge, Superadmin-Aktionen, Bestaetigungs-Dialog
|
||||
MediaLibraryPage: "1.8.0", // P-13: MediaPreviewModal (geteilt) + Melde-Button in Viewer
|
||||
ExerciseFormPage: "1.1.0", // P-13: MediaPreviewModal (geteilt) + Melden im Viewer
|
||||
ExerciseInlineFileMediaModal: "1.1.0", // P-06: RightsDeclarationDialog vor Upload
|
||||
ExerciseInlineEmbedModal: "1.1.0", // P-06: RightsDeclarationDialog vor Embed-Upload
|
||||
ExerciseMediaEmbed: "1.1.0", // P-11: Legal-Hold-Placeholder statt Datei
|
||||
ExerciseMediaThumbTile: "1.1.0", // P-11: Legal-Hold-Kachel statt Datei-Vorschau
|
||||
ExerciseAttachmentMediaStrip: "1.2.0", // P-13: MediaPreviewModal (geteilt) + Melden im Viewer
|
||||
InboxPage: "2.0.0", // P-13: Inhaltsmeldungen-Abschnitt integriert
|
||||
MediaLibraryPage: "1.7.0", // P-13: Melde-Button an Medienkacheln
|
||||
ExerciseAttachmentMediaStrip: "1.1.0", // P-13: Melde-Link an angehängten Medien
|
||||
MediaPreviewModal: "1.0.0", // P-13: geteilter Medienvorschau-Dialog (Melden + Bearbeiten optional)
|
||||
ReportContentModal: "1.0.0", // P-13: Melde-Formular (Grund, Beschreibung, Name, E-Mail)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user