🤩 File Manager - Mr.X
PHP:
8.3.33
Server:
Apache
OS:
Linux 5.14.0-611.49.1.el9_7.x86_64
User:
websparkit
Navigate
Upload:
Upload
New File
New Folder
Editing:media.blade.php
<x-app-layout> <x-slot name="header"> <h2 class="font-semibold text-xl text-navy leading-tight"> {{ __('Media Uploads: ') }} {{ $project->name }} </h2> </x-slot> <div class="py-12"> <div class="max-w-7xl mx-auto sm:px-6 lg:px-8 space-y-8"> @if(session('success')) <div class="rounded-lg border border-green-200 bg-green-50 p-4"> <div class="flex items-center gap-2"> <svg class="h-5 w-5 text-green-500" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/></svg> <p class="text-sm font-medium text-green-800">{{ session('success') }}</p> </div> </div> @endif @if(session('error')) <div class="rounded-lg border border-red-200 bg-red-50 p-4"> <div class="flex items-center gap-2"> <svg class="h-5 w-5 text-red-500" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/></svg> <p class="text-sm font-medium text-red-800">{{ session('error') }}</p> </div> </div> @endif @php $usedGB = $totalUsed / (1024 * 1024 * 1024); $limitGB = 5; $percentUsed = min(100, ($totalUsed / $limit) * 100); @endphp <div class="bg-white overflow-hidden shadow-sm sm:rounded-lg"> <div class="border-b border-gray-100 bg-gradient-to-r from-navy to-navy-dark px-6 py-4 flex justify-between items-center"> <h3 class="text-base font-semibold text-white flex items-center gap-2"> <svg class="h-5 w-5" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m6.75 12H9m1.5-12H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z"/> </svg> Storage Usage </h3> <span class="text-sm text-white/70">{{ number_format($usedGB, 2) }} GB / {{ $limitGB }} GB</span> </div> <div class="p-6"> <div class="w-full bg-gray-200 rounded-full h-3"> <div class="bg-gradient-to-r from-orange to-orange-dark h-3 rounded-full transition-all duration-500" style="width: {{ $percentUsed }}%"></div> </div> <p class="text-xs text-gray-500 mt-2">{{ number_format($totalUsed / (1024 * 1024), 1) }} MB used of {{ $limitGB }} GB</p> </div> </div> <div class="grid grid-cols-1 lg:grid-cols-2 gap-8"> {{-- Upload Files (Images, PDFs, Videos, etc.) --}} <div class="bg-white overflow-hidden shadow-sm sm:rounded-lg"> <div class="border-b border-gray-100 bg-gradient-to-r from-orange to-orange-dark px-6 py-4"> <h3 class="text-base font-semibold text-white flex items-center gap-2"> <svg class="h-5 w-5" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" d="M3.75 9.776c.112-.017.227-.026.344-.026h15.812c.117 0 .232.009.344.026m-16.5 0a2.25 2.25 0 00-1.883 2.542l.857 6a2.25 2.25 0 002.227 1.932H19.05a2.25 2.25 0 002.227-1.932l.857-6a2.25 2.25 0 00-1.883-2.542m-16.5 0V6A2.25 2.25 0 016 3.75h3.879a1.5 1.5 0 011.06.44l2.122 2.12a1.5 1.5 0 001.06.44H18A2.25 2.25 0 0120.25 9v.776"/> </svg> Upload Files (Images, PDFs, Videos) </h3> </div> <div class="p-6"> <form action="{{ route('client.media.upload', $project) }}" method="POST" enctype="multipart/form-data" class="space-y-4"> @csrf <div> <label class="block text-sm font-medium text-gray-700">Select File</label> <input type="file" name="file" accept="image/*,video/*,audio/*,.pdf,.doc,.docx,.xls,.xlsx" class="mt-1 block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-semibold file:bg-navy file:text-white hover:file:bg-navy-dark" required> <p class="text-xs text-gray-400 mt-1">Max 5GB. Supports images, PDFs, documents, videos, and audio files.</p> </div> <button type="submit" class="w-full bg-navy hover:bg-navy-dark text-white font-semibold py-2 px-4 rounded-lg transition-colors"> Upload File </button> </form> </div> </div> {{-- Record Live Section --}} <div class="bg-white overflow-hidden shadow-sm sm:rounded-lg"> <div class="border-b border-gray-100 bg-gradient-to-r from-orange to-orange-dark px-6 py-4"> <h3 class="text-base font-semibold text-white flex items-center gap-2"> <svg class="h-5 w-5" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" d="M12 18.75a6 6 0 006-6v-1.5m-6 7.5a6 6 0 01-6-6v-1.5m6 7.5v3.75m-3.75 0h7.5M12 15.75a3 3 0 01-3-3V4.5a3 3 0 116 0v8.25a3 3 0 01-3 3z"/> </svg> Record Live </h3> </div> <div class="p-6 space-y-4"> <div class="flex flex-wrap gap-3"> <button type="button" onclick="startRecording('voice')" class="flex-1 bg-gray-100 hover:bg-gray-200 text-navy font-semibold py-3 px-4 rounded-lg border border-gray-300 hover:border-orange/40 transition-all text-sm flex items-center justify-center gap-2"> <svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M12 18.75a6 6 0 006-6v-1.5m-6 7.5a6 6 0 01-6-6v-1.5m6 7.5v3.75m-3.75 0h7.5M12 15.75a3 3 0 01-3-3V4.5a3 3 0 116 0v8.25a3 3 0 01-3 3z"/></svg> Voice Only </button> <button type="button" onclick="startRecording('camera')" class="flex-1 bg-gray-100 hover:bg-gray-200 text-navy font-semibold py-3 px-4 rounded-lg border border-gray-300 hover:border-orange/40 transition-all text-sm flex items-center justify-center gap-2"> <svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M6.827 6.175A2.31 2.31 0 015.186 7.23c-.38.054-.757.112-1.134.175C2.999 7.58 2.25 8.507 2.25 9.574V18a2.25 2.25 0 002.25 2.25h15A2.25 2.25 0 0021.75 18V9.574c0-1.067-.75-1.994-1.802-2.169a47.865 47.865 0 00-1.134-.175 2.31 2.31 0 01-1.64-1.055l-.822-1.316a2.192 2.192 0 00-1.736-1.039 48.774 48.774 0 00-5.232 0 2.192 2.192 0 00-1.736 1.039l-.821 1.316z"/><path stroke-linecap="round" stroke-linejoin="round" d="M16.5 12.75a4.5 4.5 0 11-9 0 4.5 4.5 0 019 0z"/></svg> Front Camera </button> <button type="button" onclick="startRecording('back_camera')" class="flex-1 bg-gray-100 hover:bg-gray-200 text-navy font-semibold py-3 px-4 rounded-lg border border-gray-300 hover:border-orange/40 transition-all text-sm flex items-center justify-center gap-2"> <svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M6.827 6.175A2.31 2.31 0 015.186 7.23c-.38.054-.757.112-1.134.175C2.999 7.58 2.25 8.507 2.25 9.574V18a2.25 2.25 0 002.25 2.25h15A2.25 2.25 0 0021.75 18V9.574c0-1.067-.75-1.994-1.802-2.169a47.865 47.865 0 00-1.134-.175 2.31 2.31 0 01-1.64-1.055l-.822-1.316a2.192 2.192 0 00-1.736-1.039 48.774 48.774 0 00-5.232 0 2.192 2.192 0 00-1.736 1.039l-.821 1.316z"/><path stroke-linecap="round" stroke-linejoin="round" d="M16.5 12.75a4.5 4.5 0 11-9 0 4.5 4.5 0 019 0z"/></svg> Back Camera </button> </div> {{-- Preview & Recording UI --}} <div id="recording-ui" class="hidden space-y-4"> <div class="relative bg-black rounded-lg overflow-hidden aspect-video"> <video id="preview" class="w-full h-full object-cover" autoplay muted playsinline></video> <div id="recording-indicator" class="hidden absolute top-3 left-3 flex items-center gap-2 bg-red-600 text-white text-xs font-bold px-3 py-1.5 rounded-full"> <span class="w-2 h-2 bg-white rounded-full animate-ping"></span> REC </div> <div id="timer" class="absolute top-3 right-3 bg-black/70 text-white text-sm font-mono px-3 py-1.5 rounded-full"> 00:00 </div> </div> <div class="flex justify-center gap-3"> <button id="stop-btn" onclick="stopRecording()" class="hidden bg-red-600 hover:bg-red-700 text-white font-bold py-2 px-6 rounded-lg transition-colors flex items-center gap-2"> <svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20"><path d="M5.5 3.5A1.5 1.5 0 004 5v10a1.5 1.5 0 001.5 1.5h9A1.5 1.5 0 0016 15V5a1.5 1.5 0 00-1.5-1.5h-9z"/></svg> Stop Recording </button> <button id="discard-btn" onclick="discardRecording()" class="hidden bg-gray-500 hover:bg-gray-600 text-white font-semibold py-2 px-6 rounded-lg transition-colors"> Discard </button> </div> </div> <div id="pre-recording-msg" class="text-center text-sm text-gray-400 py-8"> Click a button above to start recording. </div> </div> </div> </div> {{-- Uploaded Media List --}} <div class="bg-white overflow-hidden shadow-sm sm:rounded-lg"> <div class="border-b border-gray-100 bg-gradient-to-r from-navy to-navy-dark px-6 py-4"> <h3 class="text-base font-semibold text-white flex items-center gap-2"> <svg class="h-5 w-5" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" d="M3.75 9.776c.112-.017.227-.026.344-.026h15.812c.117 0 .232.009.344.026m-16.5 0a2.25 2.25 0 00-1.883 2.542l.857 6a2.25 2.25 0 002.227 1.932H19.05a2.25 2.25 0 002.227-1.932l.857-6a2.25 2.25 0 00-1.883-2.542m-16.5 0V6A2.25 2.25 0 016 3.75h3.879a1.5 1.5 0 011.06.44l2.122 2.12a1.5 1.5 0 001.06.44H18A2.25 2.25 0 0120.25 9v.776"/> </svg> Uploaded Media </h3> </div> <div class="p-6"> @if($project->media->isEmpty()) <div class="text-center py-8"> <svg class="mx-auto h-10 w-10 text-gray-300" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" d="M15.75 9V5.25A2.25 2.25 0 0013.5 3h-6a2.25 2.25 0 00-2.25 2.25v13.5A2.25 2.25 0 007.5 21h6a2.25 2.25 0 002.25-2.25V15m3 0l3-3m0 0l-3-3m3 3H9"/> </svg> <p class="mt-3 text-sm text-gray-500">No media uploaded yet.</p> </div> @else <div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4"> @foreach($project->media as $media) @php $isVideo = str_starts_with($media->mime_type ?? '', 'video/') || in_array($media->media_type, ['camera', 'back_camera']); $isAudio = str_starts_with($media->mime_type ?? '', 'audio/') || $media->media_type === 'voice'; $isImage = str_starts_with($media->mime_type ?? '', 'image/') || $media->media_type === 'image'; $isPdf = $media->mime_type === 'application/pdf' || $media->media_type === 'document'; @endphp <div class="border rounded-lg overflow-hidden bg-gray-50"> @if($isVideo) <video class="w-full aspect-video object-cover" controls preload="metadata"> <source src="{{ Storage::url($media->file_path) }}" type="{{ $media->mime_type ?? 'video/webm' }}"> </video> @elseif($isImage) <a href="{{ Storage::url($media->file_path) }}" target="_blank"> <img src="{{ Storage::url($media->file_path) }}" class="w-full aspect-video object-cover" alt="{{ $media->file_name }}"> </a> @elseif($isPdf) <div class="w-full aspect-video bg-red-50 flex items-center justify-center"> <svg class="w-16 h-16 text-red-500" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z"/> </svg> </div> <div class="px-3 py-2 bg-red-50"> <a href="{{ Storage::url($media->file_path) }}" target="_blank" class="text-sm font-medium text-red-600 hover:text-red-800 flex items-center gap-1"> <svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3"/></svg> Open PDF </a> </div> @elseif($isAudio) <div class="w-full aspect-video bg-gray-800 flex items-center justify-center"> <svg class="w-12 h-12 text-gray-400" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" d="M12 18.75a6 6 0 006-6v-1.5m-6 7.5a6 6 0 01-6-6v-1.5m6 7.5v3.75m-3.75 0h7.5M12 15.75a3 3 0 01-3-3V4.5a3 3 0 116 0v8.25a3 3 0 01-3 3z"/> </svg> </div> <div class="p-3"> <audio controls class="w-full"> <source src="{{ Storage::url($media->file_path) }}" type="{{ $media->mime_type ?? 'audio/webm' }}"> </audio> </div> @else <div class="w-full aspect-video bg-gray-100 flex items-center justify-center"> <svg class="w-16 h-16 text-gray-400" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z"/> </svg> </div> @endif <div class="p-3 border-t bg-white"> <div class="flex items-center justify-between"> <div class="min-w-0"> <p class="text-xs font-semibold text-gray-600 capitalize truncate">{{ $media->file_name }}</p> <p class="text-xs text-gray-400">{{ number_format($media->file_size / 1024, 1) }} KB</p> </div> <form action="{{ route('client.media.delete', $media) }}" method="POST" onsubmit="return confirm('Delete this media?')" class="flex-shrink-0 ml-2"> @csrf @method('DELETE') <button type="submit" class="text-red-500 hover:text-red-700"> <svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"/> </svg> </button> </form> </div> </div> </div> @endforeach </div> @endif </div> </div> </div> </div> <script> let mediaRecorder = null; let recordedChunks = []; let stream = null; let timerInterval = null; let seconds = 0; let currentType = ''; function startRecording(type) { currentType = type; recordedChunks = []; seconds = 0; const constraints = { audio: true, video: type !== 'voice', }; if (type === 'camera') { constraints.video = { facingMode: 'user' }; } else if (type === 'back_camera') { constraints.video = { facingMode: { ideal: 'environment' } }; } navigator.mediaDevices.getUserMedia(constraints) .then(s => { stream = s; const preview = document.getElementById('preview'); if (type !== 'voice') { preview.srcObject = s; } else { preview.srcObject = null; const canvas = document.createElement('canvas'); canvas.width = 640; canvas.height = 480; const ctx = canvas.getContext('2d'); ctx.fillStyle = '#1a1a2e'; ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.fillStyle = '#FF6D28'; ctx.font = '40px sans-serif'; ctx.textAlign = 'center'; ctx.fillText('Voice Recording', canvas.width/2, canvas.height/2 - 20); ctx.font = '20px sans-serif'; ctx.fillStyle = '#888'; ctx.fillText('Audio only', canvas.width/2, canvas.height/2 + 30); const audioCtx = new AudioContext(); const analyser = audioCtx.createAnalyser(); const source = audioCtx.createMediaStreamSource(s); source.connect(analyser); const bufferLength = analyser.frequencyBinCount; const dataArray = new Uint8Array(bufferLength); function draw() { requestAnimationFrame(draw); analyser.getByteFrequencyData(dataArray); ctx.fillStyle = '#1a1a2e'; ctx.fillRect(0, canvas.height - 60, canvas.width, 60); const barWidth = (canvas.width / bufferLength) * 2.5; let x = 0; for (let i = 0; i < bufferLength; i++) { const barHeight = (dataArray[i] / 255) * 50; ctx.fillStyle = `rgb(255, ${Math.max(109, 255 - dataArray[i])}, ${Math.max(40, 255 - dataArray[i] * 2)})`; ctx.fillRect(x, canvas.height - barHeight - 10, barWidth, barHeight); x += barWidth + 1; } } draw(); preview.srcObject = canvas.captureStream(30); } preview.play(); document.getElementById('recording-ui').classList.remove('hidden'); document.getElementById('pre-recording-msg').classList.add('hidden'); document.getElementById('recording-indicator').classList.remove('hidden'); document.getElementById('stop-btn').classList.remove('hidden'); document.getElementById('discard-btn').classList.remove('hidden'); const mimeType = type === 'voice' ? 'audio/webm;codecs=opus' : 'video/webm;codecs=vp9,opus'; mediaRecorder = new MediaRecorder(s, mimeType ? { mimeType } : {}); mediaRecorder.ondataavailable = e => { if (e.data.size > 0) recordedChunks.push(e.data); }; mediaRecorder.onstop = () => { const blob = new Blob(recordedChunks, { type: type === 'voice' ? 'audio/webm' : 'video/webm' }); uploadRecording(blob); }; mediaRecorder.start(); timerInterval = setInterval(() => { seconds++; const m = String(Math.floor(seconds / 60)).padStart(2, '0'); const s = String(seconds % 60).padStart(2, '0'); document.getElementById('timer').textContent = m + ':' + s; }, 1000); }) .catch(err => { alert('Could not access camera/microphone: ' + err.message); }); } function stopRecording() { if (mediaRecorder && mediaRecorder.state !== 'inactive') { mediaRecorder.stop(); } cleanupStream(); } function discardRecording() { recordedChunks = []; if (mediaRecorder && mediaRecorder.state !== 'inactive') { mediaRecorder.stop(); } cleanupStream(); document.getElementById('recording-ui').classList.add('hidden'); document.getElementById('pre-recording-msg').classList.remove('hidden'); document.getElementById('timer').textContent = '00:00'; } function cleanupStream() { if (stream) { stream.getTracks().forEach(t => t.stop()); stream = null; } if (timerInterval) { clearInterval(timerInterval); timerInterval = null; } document.getElementById('recording-indicator').classList.add('hidden'); document.getElementById('stop-btn').classList.add('hidden'); document.getElementById('discard-btn').classList.add('hidden'); } function uploadRecording(blob) { const formData = new FormData(); formData.append('blob', blob, currentType + '_' + Date.now() + '.webm'); formData.append('media_type', currentType); formData.append('duration', seconds); fetch('{{ route("client.media.record", $project) }}', { method: 'POST', headers: { 'X-CSRF-TOKEN': '{{ csrf_token() }}' }, body: formData }) .then(r => r.json()) .then(data => { if (data.success) { window.location.reload(); } else { alert('Upload failed: ' + (data.error || 'Unknown error')); } }) .catch(() => { alert('Upload failed. Please try again.'); }); } </script> </x-app-layout>
Save Changes
Cancel
Create New File
Create New Folder