diff --git a/.env.example b/.env.example index cce781b..8a5b98b 100644 --- a/.env.example +++ b/.env.example @@ -8,18 +8,16 @@ # ─── Typische Werte PROD (docker-compose.yml) ───────────────────────────────── # DB_NAME=shinkan # DB_USER=shinkan_user -# DB_PASSWORD=… -# APP_URL=https://shinkan.jinkendo.de -# ALLOWED_ORIGINS=https://shinkan.jinkendo.de -# ENVIRONMENT=production +# SHINKAN_MEDIA_HOST=/shinkan-media +# MEDIA_ROOT=/app/media +# … # ─── Typische Werte DEV (docker-compose.dev-env.yml) ───────────────────────── # DB_NAME=shinkan_dev # DB_USER=shinkan_dev -# DB_PASSWORD=dev_password -# APP_URL=https://dev.shinkan.jinkendo.de -# ALLOWED_ORIGINS=https://dev.shinkan.jinkendo.de,http://192.168.2.49:3098 -# ENVIRONMENT=development +# SHINKAN_MEDIA_HOST=/shinkan-media/dev +# MEDIA_ROOT=/app/media +# … # ─── Ab hier: eine ausfüllbare Vorlage (bei uns meist Prod-Defaults) ─────────── DB_HOST=postgres @@ -46,7 +44,10 @@ APP_URL=https://shinkan.jinkendo.de ALLOWED_ORIGINS=https://shinkan.jinkendo.de ENVIRONMENT=production -MEDIA_DIR=/app/media +# Host-Pfad auf dem Rechner (Bind-Mount); im Container siehe MEDIA_ROOT. Pflicht für docker compose up. +SHINKAN_MEDIA_HOST=/shinkan-media +# Optional: Pfad im Container (FastAPI MEDIA_ROOT); Standard reicht fast immer. +MEDIA_ROOT=/app/media MEDIAWIKI_API_URL=https://karatetrainer.net/api.php MEDIAWIKI_USER=Jinkendo diff --git a/docker-compose.dev-env.yml b/docker-compose.dev-env.yml index 231da53..8441394 100644 --- a/docker-compose.dev-env.yml +++ b/docker-compose.dev-env.yml @@ -1,5 +1,4 @@ -# Keine festen container_name — Compose-Namen haben Projektprefix (-postgres-1). -# Gleiche Variablennamen wie docker-compose.yml; andere Werte in einer eigenen .env neben dieser Datei. +# Medien-Ablage: SHINKAN_MEDIA_HOST in der .env neben dieser Datei setzen (Host-Pfad, Bind-Mount). services: postgres: @@ -43,8 +42,9 @@ services: MEDIAWIKI_CATEGORY_SKILLS: "${MEDIAWIKI_CATEGORY_SKILLS:-Fähigkeitsbeschreibung}" MEDIAWIKI_CATEGORY_METHODS: "${MEDIAWIKI_CATEGORY_METHODS:-Methodenbeschreibung}" MEDIAWIKI_CATEGORY_MODELS: "${MEDIAWIKI_CATEGORY_MODELS:-Reifegradmodelle}" + MEDIA_ROOT: "${MEDIA_ROOT:-/app/media}" volumes: - - dev-shinkan-media:/app/media + - ${SHINKAN_MEDIA_HOST:?SHINKAN_MEDIA_HOST_missing_in_dotenv}:${MEDIA_ROOT:-/app/media} ports: - "8098:8000" depends_on: @@ -70,7 +70,6 @@ services: volumes: dev-shinkan-db-data: - dev-shinkan-media: networks: dev-shinkan-network: diff --git a/docker-compose.yml b/docker-compose.yml index b41c62e..cdc244f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -51,9 +51,10 @@ services: MEDIAWIKI_CATEGORY_EXERCISES: "${MEDIAWIKI_CATEGORY_EXERCISES:-Übungen}" MEDIAWIKI_CATEGORY_SKILLS: "${MEDIAWIKI_CATEGORY_SKILLS:-Fähigkeitsbeschreibung}" MEDIAWIKI_CATEGORY_METHODS: "${MEDIAWIKI_CATEGORY_METHODS:-Methodenbeschreibung}" - MEDIAWIKI_CATEGORY_MODELS: "${MEDIAWIKI_CATEGORY_MODELS:-Reifegradmodelle}" + # Medien: Pfad im Container (meist /app/media). Host-Pfad für Bind-Mount: SHINKAN_MEDIA_HOST in .env (obligatorisch). + MEDIA_ROOT: "${MEDIA_ROOT:-/app/media}" volumes: - - shinkan-media:/app/media + - ${SHINKAN_MEDIA_HOST:?SHINKAN_MEDIA_HOST_missing_in_dotenv}:${MEDIA_ROOT:-/app/media} ports: - "8003:8000" depends_on: @@ -79,7 +80,6 @@ services: volumes: shinkan-db-data: - shinkan-media: networks: shinkan-network: diff --git a/frontend/src/pages/ExerciseFormPage.jsx b/frontend/src/pages/ExerciseFormPage.jsx index c85fe02..24d7696 100644 --- a/frontend/src/pages/ExerciseFormPage.jsx +++ b/frontend/src/pages/ExerciseFormPage.jsx @@ -6,6 +6,10 @@ import RichTextEditor from '../components/RichTextEditor' import ExerciseProgressionGraphPanel from '../components/ExerciseProgressionGraphPanel' import { SKILL_LEVEL_OPTIONS, normalizeSkillLevelSlug } from '../constants/skillLevels' +/** Kein image/*/video/* — WebKit (iOS) ist damit oft kaputt. */ +const EXERCISE_MEDIA_UPLOAD_ACCEPT = + 'image/jpeg,image/png,image/gif,image/heic,image/heif,video/mp4,video/quicktime,application/pdf,.jpg,.jpeg,.png,.gif,.heic,.heif,.mp4,.mov,.pdf' + /** Kachelvorschau: Video nutzt ersten Frame (metadata), Bild = img. */ function ExerciseMediaThumbTile({ exerciseId, media, onOpenPreview }) { const src = !media.embed_url ? resolveExerciseMediaFileUrl(exerciseId, media) : null @@ -1460,7 +1464,7 @@ function ExerciseFormPage() { setMediaFile(e.target.files?.[0] || null)} />
diff --git a/frontend/src/pages/MediaLibraryPage.jsx b/frontend/src/pages/MediaLibraryPage.jsx index 3ff9883..b5b4c16 100644 --- a/frontend/src/pages/MediaLibraryPage.jsx +++ b/frontend/src/pages/MediaLibraryPage.jsx @@ -45,6 +45,10 @@ const MEDIA_KIND_OPTIONS = [ { value: 'other', label: 'Sonstiges' }, ] +/** Kein image/*/video/* — WebKit (iOS Safari) ist damit oft kaputt; alles explizit. */ +const MEDIA_UPLOAD_ACCEPT = + 'image/jpeg,image/png,image/gif,image/heic,image/heif,video/mp4,video/quicktime,application/pdf,.jpg,.jpeg,.png,.gif,.heic,.heif,.mp4,.mov,.pdf' + const LC_STATUS_LABELS = { active: 'Aktiv', trash_soft: 'Papierkorb (1)', @@ -474,12 +478,13 @@ export default function MediaLibraryPage() { const selCount = selected.size const onBulkArchiveFiles = async (e) => { - const fl = e.target.files + const input = e.target + const fl = input.files if (!fl?.length) return const list = Array.from(fl) - e.target.value = '' if (uploadVis === 'club' && !Number(uploadClubId)) { window.alert('Bitte einen Verein für die Sichtbarkeit „Verein“ wählen.') + input.value = '' return } setUploadBusy(true) @@ -502,6 +507,7 @@ export default function MediaLibraryPage() { } catch (err) { window.alert(err.message || String(err)) } finally { + input.value = '' setUploadBusy(false) } } @@ -624,7 +630,7 @@ export default function MediaLibraryPage() { ref={bulkFileInputRef} type="file" className="media-library__sr-file" - accept="image/*,video/*,application/pdf" + accept={MEDIA_UPLOAD_ACCEPT} multiple onChange={onBulkArchiveFiles} />