diff --git a/backend/version.py b/backend/version.py
index abd5b38..e26af3f 100644
--- a/backend/version.py
+++ b/backend/version.py
@@ -1,6 +1,6 @@
# Shinkan Jinkendo Version Information
-APP_VERSION = "0.8.62"
+APP_VERSION = "0.8.63"
BUILD_DATE = "2026-05-08"
DB_SCHEMA_VERSION = "20260508049"
@@ -29,6 +29,13 @@ MODULE_VERSIONS = {
}
CHANGELOG = [
+ {
+ "version": "0.8.63",
+ "date": "2026-05-08",
+ "changes": [
+ "RTE/Übung Medien: Picker-Thumbnails; Dateiauswahl-Anzeige; bereits verknüpfte Archive-Medien ins Fließtext einfügen; Platzhalter-Caption data-shinkan-exercise-media-caption + Caret nach ZWSP; Lesemodus: Medienliste nur für nicht eingebettete Anhänge; bearbeiten: kompakte Kacheln mit Drag-and-Drop in Textfelder, Upload unter Medien entfällt",
+ ],
+ },
{
"version": "0.8.62",
"date": "2026-05-08",
diff --git a/frontend/src/app.css b/frontend/src/app.css
index 320055f..aad7f2a 100644
--- a/frontend/src/app.css
+++ b/frontend/src/app.css
@@ -3914,6 +3914,9 @@ a.analysis-split__nav-item {
.rich-text-editor span.shinkan-inline-media::before {
content: '📎 #' attr(data-shinkan-exercise-media) ' · ' attr(data-shinkan-exercise-media-size);
}
+.rich-text-editor span.shinkan-inline-media[data-shinkan-exercise-media-caption]::before {
+ content: '📎 ' attr(data-shinkan-exercise-media-caption) ' · #' attr(data-shinkan-exercise-media) ' · ' attr(data-shinkan-exercise-media-size);
+}
/* Listen im Editor */
.rich-text-editor ul,
@@ -3998,6 +4001,192 @@ a.analysis-split__nav-item {
overflow: hidden;
}
+.rte-inline-asset-tile__thumb {
+ width: 100%;
+ aspect-ratio: 4 / 3;
+ border-radius: 6px;
+ overflow: hidden;
+ background: var(--surface2);
+ border: 1px solid rgba(127, 127, 127, 0.12);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-bottom: 2px;
+}
+.rte-inline-asset-tile__thumb-img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+.rte-inline-asset-tile__thumb-fallback {
+ font-size: 11px;
+ color: var(--text3);
+}
+.rte-inline-asset-tile__badge {
+ font-size: 10px;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.04em;
+ color: var(--accent-dark);
+}
+
+.rte-inline-file-row {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 10px;
+ position: relative;
+}
+.rte-inline-file-input-hidden {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ white-space: nowrap;
+ border: 0;
+}
+.rte-inline-file-pick-btn {
+ flex-shrink: 0;
+ cursor: pointer;
+ margin: 0;
+}
+.rte-inline-file-name {
+ flex: 1 1 160px;
+ font-size: 13px;
+ color: var(--text2);
+ line-height: 1.35;
+ min-width: 0;
+ word-break: break-word;
+}
+
+.exercise-edit-media-strip {
+ list-style: none;
+ padding: 0;
+ margin: 14px 0 0;
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+}
+.exercise-edit-media-strip__item {
+ display: flex;
+ gap: 12px;
+ align-items: stretch;
+ padding: 10px 12px;
+ border: 1px solid var(--border);
+ border-radius: 10px;
+ background: var(--surface);
+}
+.exercise-edit-media-strip__lead {
+ flex-shrink: 0;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 8px;
+ width: 96px;
+}
+.exercise-edit-media-strip__handle {
+ width: 100%;
+ text-align: center;
+ font-size: 14px;
+ line-height: 1.2;
+ padding: 6px 4px;
+ border-radius: 8px;
+ border: 1px dashed var(--accent);
+ background: rgba(29, 158, 117, 0.08);
+ color: var(--accent-dark);
+ cursor: grab;
+ user-select: none;
+}
+.exercise-edit-media-strip__handle:active {
+ cursor: grabbing;
+}
+.exercise-edit-media-strip__handle-text {
+ font-size: 11px;
+ font-weight: 600;
+}
+.exercise-edit-media-strip__embed-badge--solo {
+ width: 76px;
+ min-height: 76px;
+ border-radius: 8px;
+ border: 1px solid var(--border);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: var(--surface2);
+ font-size: 12px;
+ font-weight: 600;
+ color: var(--text2);
+ padding: 6px;
+ text-align: center;
+}
+
+.exercise-edit-media-strip__body {
+ flex: 1;
+ min-width: 0;
+}
+.exercise-edit-media-strip__toolbar {
+ display: grid;
+ grid-template-columns: 1fr minmax(120px, 160px);
+ gap: 8px;
+ margin-top: 8px;
+}
+@media (max-width: 520px) {
+ .exercise-edit-media-strip__toolbar {
+ grid-template-columns: 1fr;
+ }
+}
+.exercise-edit-media-strip__actions {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ margin-top: 10px;
+ align-items: center;
+}
+
+.exercise-orphan-media-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
+ gap: 12px;
+ margin-top: 12px;
+}
+.exercise-orphan-media-card {
+ border: 1px solid var(--border);
+ border-radius: 10px;
+ padding: 10px 12px;
+ background: var(--surface);
+}
+.exercise-orphan-media-card__head {
+ display: flex;
+ gap: 10px;
+ align-items: flex-start;
+ margin-bottom: 8px;
+}
+.exercise-orphan-media-card__meta {
+ flex: 1;
+ min-width: 0;
+}
+.exercise-orphan-media-card__title {
+ font-size: 14px;
+ display: block;
+ line-height: 1.3;
+ word-break: break-word;
+}
+.exercise-orphan-media-card__sub {
+ display: block;
+ font-size: 11px;
+ color: var(--text3);
+ margin-top: 4px;
+}
+.exercise-orphan-media-card__warn {
+ display: block;
+ font-size: 11px;
+ color: var(--danger);
+ margin-top: 4px;
+}
+
.rich-text-content {
font-size: 16px;
line-height: 1.55;
diff --git a/frontend/src/components/ExerciseAttachmentMediaStrip.jsx b/frontend/src/components/ExerciseAttachmentMediaStrip.jsx
new file mode 100644
index 0000000..503868a
--- /dev/null
+++ b/frontend/src/components/ExerciseAttachmentMediaStrip.jsx
@@ -0,0 +1,123 @@
+/**
+ * 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 { 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 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 (
+
+ Hier erscheinen nur Verknüpfungen, die noch nicht im Fließtext eingebettet sind (reine Material-Anhänge).
+
+
+ Datei öffnen
+
+ Angehängte Medien
+ Vorschau
+ {preview.embed_url ? (
+
+ ) : preview.mime_type?.startsWith('video/') || preview.media_type === 'video' ? (
+
+ ) : preview.mime_type?.startsWith('image/') || preview.media_type === 'image' ? (
+
+ ) : (
+
- Hinweis: Dieses Medium ist im Papierkorb und steht künftig nicht mehr zur Verfügung. -
- )} - {m.description &&{m.description}
} -{err}
+ ) : null} {tab === 'library' && ( <> @@ -219,14 +257,15 @@ export default function ExerciseInlineFileMediaModal({ disabled={busy} /> {loading ?Laden…
: null} - {err && tab === 'library' && !loading ? ( -{err}
- ) : null}- Datei oder Embed (YouTube, Vimeo, Instagram, TikTok). Max. 10 pro Übung. +
+ Neue Uploads oder Embeds über die Textfeld-Symbolleiste („Medien im Text“ / „Embed im Text“). Hier + verwaltest du Verknüpfungen — Kachel in ein Textfeld ziehen, um sie an der Cursorposition einzufügen + (mittlere Darstellung). +
++ Max. 10 Medien pro Übung.