@php $rawResources = collect($lesson->resource_links ?? []); $buildAudioResource = static function (string $url, ?string $label = null, string $streamUrlOverride = ''): array { $path = parse_url($url, PHP_URL_PATH) ?: $url; // For /media/audio?path=course-media/.../normal.mp3, extract real filename from query parse_str(parse_url($url, PHP_URL_QUERY) ?? '', $qp); $filePath = $qp['path'] ?? ''; $basename = $filePath !== '' ? pathinfo($filePath, PATHINFO_FILENAME) : pathinfo($path, PATHINFO_FILENAME); $normalizedBase = \Illuminate\Support\Str::lower($basename); $normalizedLabel = strtolower($label ?? ''); $dedupeKey = preg_replace('/(?:[-_ ]\(?1\)?)+$/', '', $normalizedBase) ?: $normalizedBase; $readableName = (string) \Illuminate\Support\Str::of(rawurldecode($label ?: $basename)) ->replace('.mp3', '') ->replace(['+', '_', '-'], ' ') ->replaceMatches('/\s+/', ' ') ->trim() ->title(); $variant = 'practice'; $title = $readableName !== '' ? $readableName : 'Audio MP3'; $description = 'Reproduce el audio y ajusta la velocidad para estudiar mejor.'; if (str_contains($normalizedBase, 'normal') || str_contains($normalizedLabel, 'normal')) { $variant = 'translation'; $title = 'Normal version'; $description = 'Usa este audio mientras lees y cambia la velocidad a 0.5x, 1x, 1.5x o 2x.'; } elseif (str_contains($normalizedBase, 'interactive') || str_contains($normalizedLabel, 'interactive')) { $variant = 'interactive'; $title = 'Interactive version'; $description = 'Audio para practicar preguntas y respuestas con control de velocidad.'; } $streamUrl = $streamUrlOverride ?: (str_starts_with(ltrim($path, '/'), 'media/imported/') ? route('media.audio', ['path' => ltrim($path, '/')]) : $url); return [ 'url' => $url, 'stream_url' => $streamUrl, 'title' => $title, 'description' => $description, 'variant' => $variant, 'download_name' => $filePath !== '' ? basename($filePath) : basename($path), 'dedupe_key' => $dedupeKey, 'sort_order' => match ($variant) { 'translation' => 0, 'interactive' => 1, default => 2, }, ]; }; $isAudioResource = static function (array $resource): bool { $url = strtolower((string) ($resource['url'] ?? '')); return str_ends_with(parse_url($url, PHP_URL_PATH) ?? $url, '.mp3') || str_contains($url, '.mp3'); }; $audioResources = $rawResources ->filter($isAudioResource) ->map(fn (array $resource): array => $buildAudioResource( (string) ($resource['url'] ?? '#'), (string) ($resource['label'] ?? ''), (string) ($resource['stream_url'] ?? ''), )) ->sortBy(fn (array $audio): string => sprintf('%02d-%s', $audio['sort_order'], $audio['title'])) ->unique('dedupe_key') ->values(); $zipResources = $rawResources ->reject($isAudioResource) ->values(); $inlineAudioPlayers = []; $notesWithAudioPlaceholders = $lesson->notes_markdown ? preg_replace_callback( '/^\s*\[([^\]]+)\]\(([^)]+\.mp3(?:\?[^)]*)?)\)\s*$/imu', static function (array $matches) use (&$inlineAudioPlayers, $buildAudioResource): string { $index = count($inlineAudioPlayers); $placeholder = "@@INLINE_AUDIO_{$index}@@"; $inlineAudioPlayers[$placeholder] = $buildAudioResource($matches[2], $matches[1]); return $placeholder; }, $lesson->notes_markdown, ) : null; $notesWithAudioPlaceholders = $notesWithAudioPlaceholders ? preg_replace("/\n{3,}/", "\n\n", trim($notesWithAudioPlaceholders)) : null; $markdown = $notesWithAudioPlaceholders ? \Illuminate\Support\Str::markdown($notesWithAudioPlaceholders, ['html_input' => 'strip', 'allow_unsafe_links' => false]) : null; foreach ($inlineAudioPlayers as $placeholder => $audio) { $playerHtml = <<

{$audio['title']}

{$audio['description']}

Descargar MP3
HTML; $markdown = str_replace('

' . $placeholder . '

', $playerHtml, $markdown); $markdown = str_replace($placeholder, $playerHtml, $markdown); } $availableLessons = $course->lessons->where('is_available', true)->count(); $nextLesson = $course->lessons ->where('is_available', true) ->where('position', '>', $lesson->position) ->sortBy('position') ->first(); @endphp
{{-- Video player --}}
@if ($lesson->video_path) {{-- Next lesson overlay (shown by JS when video ends) --}} @if ($nextLesson) @endif @else

Lección sin video

Esta parte del curso solo tiene apuntes o recursos adjuntos.

Puedes leer las notas y archivos disponibles más abajo.

@endif
{{ $lesson->video_path && $lesson->duration_seconds ? 'Duración '.$lesson->formatted_duration : ($lesson->video_path ? 'Video local' : 'Solo apuntes y recursos') }} @if ($lesson->subtitle_es_path || $lesson->subtitle_en_path) Subtítulos {{ $lesson->subtitle_es_path ? 'ES' : '' }}{{ $lesson->subtitle_es_path && $lesson->subtitle_en_path ? ' / ' : '' }}{{ $lesson->subtitle_en_path ? 'EN' : '' }} @endif
@if ($lesson->video_path)
@endif
{{-- Lesson title + back link --}}
Volver a mis cursos

{{ $lesson->section_title }}

{{ $lesson->title }}

{{-- Lesson content: audio players + text + downloads --}}
{{-- Audio players (Normal version first, then Interactive) --}} @if ($audioResources->isNotEmpty())
@foreach ($audioResources as $audio) @endforeach
@endif {{-- Story text / notes --}} @if ($markdown)
{!! $markdown !!}
@elseif ($audioResources->isEmpty() && $zipResources->isEmpty())

Esta lección todavía no tiene contenido cargado.

@endif {{-- ZIP / document downloads --}} @if ($zipResources->isNotEmpty())
@foreach ($zipResources as $resource)

{{ $resource['label'] }}

{{ $resource['description'] }}

@endforeach
@endif
{{-- Sidebar: accordion curriculum --}}