Add NAS file manager integration and all pending platform changes

- Installed p7h/nas-file-manager package via private VCS repo
- Published config/nas-file-manager.php with super_admin middleware restriction
- Added NAS env vars to .env.example
- Created admin/nas-storage page with connection info panel and file browser widget
- Added NAS Storage link to admin sidebar (super_admin only)
- Added SuperAdminController@nasStorage method and admin.nas-storage route
- Includes all accumulated branch changes: profile wall, 2FA, audit logs,
  settings panel, country/phone/timezone components, posts, slideshow,
  playlist shares, video downloads/shares, comment likes, notifications,
  social links, and more

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
This commit is contained in:
ghassanandClaude Sonnet 4.6 committed 2026-05-13 13:24:32 +03:00
1 parent d44490dfe0
commit 0b2e95ea65
161 files changed
+27496 -7250

No files matched your search

File diff suppressed because it is too large. Load diff
+195
View File
@@ -0,0 +1,195 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>@yield('title', config('app.name'))</title>
<link rel="icon" type="image/png" href="{{ asset('storage/images/logo.png') }}">
<link rel="apple-touch-icon" href="{{ asset('storage/images/logo.png') }}">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/font/bootstrap-icons.css">
<style>
:root {
--brand-red: #e61e1e;
--bg-dark: #0f0f0f;
--bg-secondary: #1a1a1a;
--bg-card: #1e1e1e;
--border-color: #2e2e2e;
--text-primary: #f1f1f1;
--text-secondary: #888;
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
background: var(--bg-dark);
color: var(--text-primary);
font-family: "Inter", "Roboto", "Arial", sans-serif;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 24px 16px;
}
/* Subtle radial glow behind card */
body::before {
content: '';
position: fixed;
inset: 0;
background: radial-gradient(ellipse 80% 60% at 50% 0%, rgba(230,30,30,.07) 0%, transparent 70%);
pointer-events: none;
}
/* ── Auth card ─────────────────────────────────────────────────── */
.auth-wrap {
width: 100%;
max-width: 400px;
position: relative;
}
.auth-logo {
text-align: center;
margin-bottom: 32px;
}
.auth-logo a {
display: inline-block;
text-decoration: none;
}
.auth-logo img { height: 36px; }
.auth-card {
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: 16px;
padding: 36px 32px;
box-shadow: 0 24px 64px rgba(0,0,0,.5);
}
.auth-title {
font-size: 22px;
font-weight: 700;
letter-spacing: -.3px;
margin-bottom: 6px;
text-align: center;
}
.auth-subtitle {
font-size: 14px;
color: var(--text-secondary);
text-align: center;
margin-bottom: 28px;
}
/* ── Form elements ─────────────────────────────────────────────── */
.form-group { margin-bottom: 16px; }
.form-label {
display: block;
margin-bottom: 7px;
font-size: 13px;
font-weight: 500;
color: var(--text-primary);
}
.form-input {
width: 100%;
background: #121212;
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 11px 14px;
color: var(--text-primary);
font-size: 14px;
font-family: inherit;
transition: border-color .2s;
outline: none;
}
.form-input:focus { border-color: var(--brand-red); }
.form-input::placeholder { color: var(--text-secondary); }
.form-checkbox {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
font-size: 13px;
color: var(--text-secondary);
user-select: none;
}
.form-checkbox input[type="checkbox"] {
width: 16px; height: 16px;
cursor: pointer;
accent-color: var(--brand-red);
flex-shrink: 0;
}
/* ── Primary button ────────────────────────────────────────────── */
.btn-primary {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
width: 100%;
background: var(--brand-red);
color: #fff;
border: none;
padding: 12px 24px;
border-radius: 8px;
font-size: 15px;
font-weight: 600;
font-family: inherit;
cursor: pointer;
transition: background .2s;
margin-top: 8px;
}
.btn-primary:hover { background: #c91a1a; }
/* ── Error ─────────────────────────────────────────────────────── */
.auth-error {
display: flex;
align-items: flex-start;
gap: 10px;
background: rgba(230,30,30,.1);
border: 1px solid rgba(230,30,30,.35);
color: #f87171;
padding: 12px 14px;
border-radius: 8px;
margin-bottom: 20px;
font-size: 13px;
line-height: 1.5;
}
.auth-error i { flex-shrink: 0; margin-top: 1px; }
/* ── Footer link ───────────────────────────────────────────────── */
.auth-footer {
text-align: center;
margin-top: 24px;
font-size: 13px;
color: var(--text-secondary);
}
.auth-footer a { color: var(--brand-red); text-decoration: none; font-weight: 500; }
.auth-footer a:hover { text-decoration: underline; }
@media (max-width: 480px) {
.auth-card { padding: 28px 20px; border-radius: 12px; }
}
</style>
@yield('extra_styles')
</head>
<body>
<div class="auth-wrap">
<div class="auth-logo">
<a href="{{ route('videos.index') }}">
<img src="{{ asset('storage/images/fullLogo.png') }}" alt="{{ config('app.name') }}">
</a>
</div>
@yield('content')
<div class="auth-footer">@yield('footer_link')</div>
</div>
@yield('scripts')
</body>
</html>
@@ -34,10 +34,10 @@
<input type="text" id="newPlaylistName" placeholder="Playlist name"
style="width: 100%; padding: 12px 14px; margin-bottom: 12px; border: 1px solid #3f3f3f; border-radius: 8px; background: #121212; color: #fff; font-size: 14px; outline: none; transition: border-color 0.2s;">
<div style="display: flex; gap: 10px; justify-content: flex-end;">
<button onclick="hideCreatePlaylistInModal()"
style="padding: 8px 16px; background: #3f3f3f; color: #fff; border: none; border-radius: 18px; cursor: pointer; font-size: 14px; font-weight: 500; transition: background 0.2s;">Cancel</button>
<button onclick="createPlaylistFromModal()"
style="padding: 8px 16px; background: #e61e1e; color: #fff; border: none; border-radius: 18px; cursor: pointer; font-size: 14px; font-weight: 500; transition: background 0.2s;">Create</button>
<button onclick="hideCreatePlaylistInModal()" class="action-btn">Cancel</button>
<button onclick="createPlaylistFromModal()" class="action-btn action-btn-primary">
<i class="bi bi-plus-lg"></i> <span>Create</span>
</button>
</div>
</div>
@endauth
@@ -365,7 +365,7 @@
function createPlaylistFromModal() {
const name = document.getElementById('newPlaylistName').value.trim();
if (!name) {
alert('Please enter a playlist name');
showToast('Please enter a playlist name', 'error');
return;
}
@@ -17,11 +17,11 @@
</div>
</div>
<div class="modal-footer" style="border-top: 1px solid var(--border-color); padding: 16px 20px; gap: 8px;">
<button type="button" class="btn" style="background: var(--bg-primary); color: var(--text-primary); border: 1px solid var(--border-color); padding: 8px 16px; border-radius: 6px; font-weight: 500;" data-bs-dismiss="modal">
<button type="button" class="action-btn" data-bs-dismiss="modal">
Cancel
</button>
<button type="button" class="btn" style="background: var(--brand-red); color: white; padding: 8px 16px; border-radius: 6px; font-weight: 500; border: none;" onclick="deleteCommentWithModal({{ $commentId }})">
Delete
<button type="button" class="action-btn action-btn-primary" onclick="deleteCommentWithModal({{ $commentId }})">
<i class="bi bi-trash"></i> <span>Delete</span>
</button>
</div>
</div>
@@ -116,15 +116,17 @@
<div class="step-navigation">
<div></div>
<button type="button" class="btn-next" onclick="editNextStep(2)">
Next Step <i class="bi bi-arrow-right"></i>
<button type="button" class="action-btn action-btn-primary btn-next" onclick="editNextStep(2)">
<span>Next Step</span> <i class="bi bi-arrow-right"></i>
</button>
</div>
</div>
<!-- Step 2: Thumbnail -->
<!-- Step 2: Thumbnail / Slides -->
<div class="edit-step-content" data-step="2">
<div class="form-group">
<!-- Standard thumbnail (non-audio) -->
<div id="edit-thumb-section" class="form-group">
<label class="form-label">
<i class="bi bi-camera"></i> Change Thumbnail
</label>
@@ -135,7 +137,7 @@
<i class="bi bi-card-image"></i>
</div>
<p class="dropzone-title">Click to select or drag thumbnail</p>
<p class="dropzone-hint">JPG, PNG, WebP up to 5MB</p>
<p class="dropzone-hint">JPG, PNG, WebP up to 20MB</p>
</div>
<div id="edit-thumbnail-info" class="file-info-modal">
<div class="file-preview thumbnail-preview">
@@ -145,20 +147,42 @@
<p class="filename" id="edit-thumbnail-filename"></p>
<p id="edit-thumbnail-filesize"></p>
</div>
<button type="button" class="btn-remove-file"
onclick="removeEditThumbnail(event)">
<button type="button" class="btn-remove-file" onclick="removeEditThumbnail(event)">
<i class="bi bi-x-lg"></i>
</button>
</div>
</div>
</div>
<!-- Slides manager (audio tracks) -->
<div id="edit-slides-section" style="display:none;">
<label class="form-label"><i class="bi bi-images"></i> Cover Slides
<span style="font-weight:400;font-size:12px;color:var(--text-secondary);margin-left:6px;">Drag to reorder · click × to remove</span>
</label>
<!-- Hidden inputs carrying order + new files -->
<input type="hidden" name="slides_order" id="edit-slides-order" value="[]">
<input type="file" name="slides_add[]" id="edit-slides-add-input" accept="image/*" multiple style="display:none;">
<div id="edit-slides-strip" style="display:flex;flex-wrap:wrap;gap:8px;min-height:72px;padding:10px;background:rgba(255,255,255,.03);border:2px dashed rgba(255,255,255,.1);border-radius:10px;margin-bottom:8px;">
<!-- Slide items injected by JS -->
<div id="edit-slides-empty" style="display:flex;align-items:center;justify-content:center;width:100%;color:var(--text-secondary);font-size:13px;">No slides yet</div>
</div>
<div style="display:flex;gap:8px;align-items:center;">
<button type="button" class="action-btn" onclick="document.getElementById('edit-slides-add-input').click()">
<i class="bi bi-plus-lg"></i> <span>Add Images</span>
</button>
<span id="edit-slides-hint" style="font-size:12px;color:var(--text-secondary);"></span>
</div>
</div>
<div class="step-navigation">
<button type="button" class="btn-prev" onclick="editPrevStep(1)">
<i class="bi bi-arrow-left"></i> Back
<button type="button" class="action-btn btn-prev" onclick="editPrevStep(1)">
<i class="bi bi-arrow-left"></i> <span>Back</span>
</button>
<button type="button" class="btn-next" onclick="editNextStep(3)">
Next Step <i class="bi bi-arrow-right"></i>
<button type="button" class="action-btn action-btn-primary btn-next" onclick="editNextStep(3)">
<span>Next Step</span> <i class="bi bi-arrow-right"></i>
</button>
</div>
</div>
@@ -203,15 +227,52 @@
</div>
</div>
<!-- Download Access -->
<div class="form-group" style="margin: 16px 0 0;">
<label class="form-label">
<i class="bi bi-download"></i> Who can download?
</label>
<div class="options-grid" id="edit-download-options-modal">
<label class="option-item" data-download="disabled">
<input type="radio" name="download_access" value="disabled">
<div class="option-content">
<i class="bi bi-slash-circle"></i>
<span>Disabled</span>
</div>
</label>
<label class="option-item" data-download="everyone">
<input type="radio" name="download_access" value="everyone">
<div class="option-content">
<i class="bi bi-globe"></i>
<span>Everyone</span>
</div>
</label>
<label class="option-item" data-download="registered">
<input type="radio" name="download_access" value="registered">
<div class="option-content">
<i class="bi bi-person-check"></i>
<span>Members</span>
</div>
</label>
<label class="option-item" data-download="subscribers">
<input type="radio" name="download_access" value="subscribers">
<div class="option-content">
<i class="bi bi-star"></i>
<span>Subscribers</span>
</div>
</label>
</div>
</div>
<!-- Status Message -->
<div id="edit-status-message" class="status-message-modal"></div>
<div class="step-navigation">
<button type="button" class="btn-prev" onclick="editPrevStep(2)">
<i class="bi bi-arrow-left"></i> Back
<button type="button" class="action-btn btn-prev" onclick="editPrevStep(2)">
<i class="bi bi-arrow-left"></i> <span>Back</span>
</button>
<button type="submit" class="btn-save-changes" id="edit-submit-btn">
<i class="bi bi-check-lg"></i> Save Changes
<button type="submit" class="action-btn action-btn-primary btn-save-changes" id="edit-submit-btn">
<i class="bi bi-check-lg"></i> <span>Save Changes</span>
</button>
</div>
</div>
@@ -604,13 +665,7 @@
}
.dropzone-modal input[type="file"] {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0;
cursor: pointer;
display: none;
}
.dropzone-icon {
@@ -795,51 +850,14 @@
border-top: 1px solid #2a2a2a;
}
.btn-prev,
.btn-next,
.btn-save-changes {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 24px;
border-radius: 12px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
border: none;
}
.btn-prev {
background: transparent;
border: 1px solid #404040;
color: #aaa;
}
.btn-prev:hover {
border-color: #666;
color: #fff;
}
.btn-next,
.btn-save-changes {
background: linear-gradient(135deg, #e61e1e 0%, #ff4757 100%);
color: white;
box-shadow: 0 4px 15px rgba(230, 30, 30, 0.3);
}
.btn-next:hover,
.btn-save-changes:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(230, 30, 30, 0.4);
}
.btn-next:disabled,
.btn-save-changes:disabled {
background: #444;
background: #444 !important;
border-color: #444 !important;
cursor: not-allowed;
transform: none;
box-shadow: none;
opacity: 1;
}
/* Status Message */
@@ -869,6 +887,51 @@
border: 1px solid rgba(239, 68, 68, 0.3);
}
/* Slide items in edit modal */
.es-item {
position: relative;
width: 72px;
height: 72px;
border-radius: 8px;
overflow: hidden;
border: 2px solid rgba(255,255,255,.12);
cursor: grab;
flex-shrink: 0;
transition: border-color .2s, transform .15s;
}
.es-item:hover { border-color: #e61e1e; }
.es-item img {
width: 100%; height: 100%;
object-fit: cover;
display: block;
pointer-events: none;
}
.es-drag-handle {
position: absolute;
top: 2px; left: 2px;
width: 20px; height: 20px;
background: rgba(0,0,0,.55);
border-radius: 4px;
display: flex; align-items: center; justify-content: center;
font-size: 11px; color: #ccc;
cursor: grab;
}
.es-remove {
position: absolute;
top: 2px; right: 2px;
width: 20px; height: 20px;
background: rgba(0,0,0,.55);
border: none;
border-radius: 4px;
display: flex; align-items: center; justify-content: center;
font-size: 10px; color: #ccc;
cursor: pointer;
padding: 0;
transition: background .15s, color .15s;
}
.es-remove:hover { background: #e61e1e; color: #fff; }
.es-item.es-dragging { opacity: .35; outline: 2px dashed #e61e1e; }
/* Responsive */
@media (max-width: 576px) {
.edit-modal-body {
@@ -908,6 +971,12 @@
// Open modal and load video data
function openEditVideoModal(videoId) {
// On mobile redirect to the full edit page
if (window.innerWidth < 992) {
window.location.href = `/videos/${videoId}/edit`;
return;
}
window.currentVideoId = videoId;
// Fetch video data
@@ -960,11 +1029,31 @@
}
});
// Set download_access radio cards
const dlValue = video.download_access || 'disabled';
document.querySelectorAll('#edit-download-options-modal .option-item').forEach(opt => {
opt.classList.remove('active');
const radio = opt.querySelector('input[type="radio"]');
if (radio.value === dlValue) {
radio.checked = true;
opt.classList.add('active');
} else {
radio.checked = false;
}
});
// Reset file inputs
document.getElementById('edit-thumbnail-default').style.display = 'block';
document.getElementById('edit-thumbnail-info').classList.remove('active');
document.getElementById('edit-status-message').className = 'status-message-modal';
// Populate slides or thumbnail section
populateEditSlides(video.slides || [], video.is_audio || false);
// Update step 2 label dynamically
const step2Label = document.querySelector('.edit-step[data-step="2"] .step-label');
if (step2Label) step2Label.textContent = video.is_audio ? 'Slides' : 'Thumbnail';
// Reset step to 1
window.currentEditStep = 1;
updateEditStepDisplay();
@@ -980,12 +1069,12 @@
document.getElementById('editVideoModal').classList.add('show');
}, 10);
} else {
alert('Failed to load video data');
showToast('Failed to load video data', 'error');
}
})
.catch(error => {
console.error('Error:', error);
alert('Failed to load video data');
showToast('Failed to load video data', 'error');
});
}
@@ -1011,7 +1100,7 @@
if (step === 2) {
const title = document.getElementById('edit-video-title').value.trim();
if (!title) {
alert('Please enter a title for your video');
showToast('Please enter a title for your video', 'error');
return;
}
}
@@ -1066,6 +1155,15 @@
});
if (editThumbnailDropzone) {
editThumbnailDropzone.addEventListener('click', function(e) {
if (e.target.closest('.btn-remove-file')) return;
if (typeof window.openCropperModal_thumb_edit === 'function') {
window.openCropperModal_thumb_edit();
} else {
editThumbnailInput.click();
}
});
editThumbnailDropzone.addEventListener('dragover', (e) => {
e.preventDefault();
editThumbnailDropzone.classList.add('dragover');
@@ -1079,8 +1177,14 @@
e.preventDefault();
editThumbnailDropzone.classList.remove('dragover');
if (e.dataTransfer.files.length) {
editThumbnailInput.files = e.dataTransfer.files;
handleEditThumbnailSelect(editThumbnailInput);
const droppedFile = e.dataTransfer.files[0];
if (typeof window.tcPreload_thumb_edit === 'function') {
window.tcPreload_thumb_edit(droppedFile);
window.openCropperModal_thumb_edit();
} else {
editThumbnailInput.files = e.dataTransfer.files;
handleEditThumbnailSelect(editThumbnailInput);
}
}
});
}
@@ -1113,6 +1217,109 @@
document.getElementById('edit-thumbnail-info').classList.remove('active');
}
// ── Slides manager (audio tracks) ────────────────────────────
let editSlidesData = []; // [{id, url}] for existing; {file, url} for new additions
let editDragSrc = null;
function _esSyncOrder() {
const strip = document.getElementById('edit-slides-strip');
const keptIds = editSlidesData.filter(s => s.id).map(s => s.id);
document.getElementById('edit-slides-order').value = JSON.stringify(keptIds);
const count = editSlidesData.length;
document.getElementById('edit-slides-hint').textContent =
count === 1 ? '1 image — static cover' : count + ' images — will crossfade';
}
function editSlidesRefresh() {
const strip = document.getElementById('edit-slides-strip');
const empty = document.getElementById('edit-slides-empty');
strip.querySelectorAll('.es-item').forEach(el => el.remove());
empty.style.display = editSlidesData.length === 0 ? 'flex' : 'none';
editSlidesData.forEach((item, idx) => {
const div = document.createElement('div');
div.className = 'es-item';
div.draggable = true;
div._esData = item; // reference stored on element — avoids stale-index issues during drag
div.innerHTML = `
<div class="es-drag-handle" title="Drag to reorder"><i class="bi bi-grip-vertical"></i></div>
<img src="${item.url}" alt="">
<button type="button" class="es-remove" title="Remove"><i class="bi bi-x-lg"></i></button>
`;
div.querySelector('.es-remove').addEventListener('click', () => {
editSlidesData.splice(editSlidesData.indexOf(div._esData), 1);
editSlidesRefresh();
});
div.addEventListener('dragstart', e => {
editDragSrc = div;
e.dataTransfer.effectAllowed = 'move';
setTimeout(() => div.classList.add('es-dragging'), 0);
});
// On dragend: sync editSlidesData from current DOM order, then update inputs
div.addEventListener('dragend', () => {
div.classList.remove('es-dragging');
editDragSrc = null;
const newData = [];
strip.querySelectorAll('.es-item').forEach(el => newData.push(el._esData));
editSlidesData = newData;
_esSyncOrder();
});
// On dragover: just move the element in the DOM — never re-render
div.addEventListener('dragover', e => {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
if (!editDragSrc || editDragSrc === div) return;
const items = [...strip.querySelectorAll('.es-item')];
const srcPos = items.indexOf(editDragSrc);
const tgtPos = items.indexOf(div);
if (srcPos < tgtPos) div.after(editDragSrc);
else div.before(editDragSrc);
});
strip.appendChild(div);
});
_esSyncOrder();
}
// "Add Images" picker
document.getElementById('edit-slides-add-input').addEventListener('change', function() {
Array.from(this.files).forEach(file => {
const reader = new FileReader();
reader.onload = e => {
editSlidesData.push({ file, url: e.target.result });
editSlidesRefresh();
};
reader.readAsDataURL(file);
});
// Reset input so same files can be re-added after removal
this.value = '';
});
function populateEditSlides(slides, isAudio) {
document.getElementById('edit-thumb-section').style.display = isAudio ? 'none' : '';
document.getElementById('edit-slides-section').style.display = isAudio ? '' : 'none';
if (! isAudio) return;
editSlidesData = slides.map(s => ({ id: s.id, url: s.url }));
editSlidesRefresh();
}
// Inject new file inputs into the form on submit (FormData can't handle dynamic files otherwise)
function buildEditSlidesFormData(formData) {
// Remove existing slides_add from formData (the file input may have stale data)
// Instead append each new file individually
const newFiles = editSlidesData.filter(s => s.file);
newFiles.forEach(s => formData.append('slides_add[]', s.file, s.file.name));
return formData;
}
// Video type options
const videoTypeOptions = document.querySelectorAll('.video-type-option');
videoTypeOptions.forEach(option => {
@@ -1133,11 +1340,21 @@
});
});
// Download access options
document.querySelectorAll('#edit-download-options-modal .option-item').forEach(item => {
item.querySelector('input[type="radio"]').addEventListener('change', function() {
document.querySelectorAll('#edit-download-options-modal .option-item').forEach(o => o.classList.remove('active'));
item.classList.add('active');
});
});
// Form submission
document.getElementById('edit-video-form-modal').addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData(this);
let formData = new FormData(this);
// Append any newly added slide files (not captured by FormData from hidden input)
formData = buildEditSlidesFormData(formData);
const submitBtn = document.getElementById('edit-submit-btn');
// Disable button and show loading
@@ -1187,3 +1404,14 @@
});
});
</script>
<x-image-cropper
id="thumb_edit"
:width="448"
:height="252"
shape="square"
target-input="edit-thumbnail-input"
preview-img="edit-thumbnail-preview-img"
output-width="1280"
title="Crop Thumbnail"
/>
+75 -229
View File
@@ -1,256 +1,102 @@
<!-- Header -->
<header class="yt-header">
<style>
.yt-header {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 56px;
background: #0f0f0f;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 16px;
z-index: 1000;
border-bottom: 1px solid #303030;
}
.yt-header-left {
display: flex;
align-items: center;
gap: 16px;
}
.yt-menu-btn {
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: background 0.2s;
background: transparent;
border: none;
color: #f1f1f1;
}
.yt-menu-btn:hover { background: #303030; }
.yt-logo {
display: flex;
align-items: center;
text-decoration: none;
gap: 4px;
}
.yt-logo-text {
font-size: 1.4rem;
font-weight: 700;
color: #f1f1f1;
letter-spacing: -1px;
}
.yt-header-center {
flex: 1;
max-width: 640px;
margin: 0 40px;
display: flex;
align-items: center;
justify-content: center;
}
.yt-search {
flex: 1;
display: flex;
height: 40px;
}
.yt-search-input {
flex: 1;
background: #121212;
border: 1px solid #303030;
border-right: none;
border-radius: 20px 0 0 20px;
padding: 0 16px;
color: #f1f1f1;
font-size: 16px;
}
.yt-search-input:focus {
outline: none;
border-color: #1c62b9;
}
.yt-search-btn {
width: 64px;
background: #222;
border: 1px solid #303030;
border-radius: 0 20px 20px 0;
color: #f1f1f1;
cursor: pointer;
}
.yt-search-btn:hover { background: #303030; }
.yt-header-right {
display: flex;
align-items: center;
gap: 8px;
}
.yt-mobile-search-toggle {
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
background: transparent;
border: none;
color: #f1f1f1;
font-size: 1.2rem;
}
.yt-mobile-search-toggle:hover {
background: #303030;
}
.yt-upload-btn {
background: #e61e1e;
color: white;
border: none;
padding: 8px 16px;
border-radius: 20px;
font-weight: 500;
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
text-decoration: none;
}
.yt-upload-btn:hover { background: #cc1a1a; }
.yt-icon-btn {
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
background: transparent;
border: none;
color: #f1f1f1;
font-size: 1.2rem;
}
.yt-icon-btn:hover { background: #303030; }
.yt-user-avatar {
width: 32px;
height: 32px;
border-radius: 50%;
background: #555;
cursor: pointer;
}
@media (max-width: 576px) {
.yt-upload-btn {
width: 40px;
height: 40px;
padding: 0;
border-radius: 50%;
justify-content: center;
}
.yt-upload-btn span {
display: none;
}
@media (min-width: 400px) {
.yt-upload-btn {
width: auto;
border-radius: 20px;
padding: 8px 16px;
}
.yt-upload-btn span {
display: inline;
}
}
}
@media (max-width: 768px) {
.yt-header-center { display: none; }
}
</style>
<div class="yt-header-left">
<button class="yt-menu-btn" onclick="toggleSidebar()">
<i class="bi bi-list fs-5"></i>
</button>
<a href="{{ route('home') }}" class="yt-logo">
<!-- Mobile logo (visible only on mobile) -->
<img src="{{ asset('storage/images/logo.png') }}" alt="{{ config('app.name') }}" class="d-md-none" style="height: 30px;">
<!-- Desktop logo (visible only on desktop) -->
<img src="{{ asset('storage/images/fullLogo.png') }}" alt="{{ config('app.name') }}" class="d-none d-md-block" style="height: 30px;">
<img src="{{ asset('storage/images/logo.png') }}" alt="{{ config('app.name') }}" class="d-md-none" style="height:30px;">
<img src="{{ asset('storage/images/fullLogo.png') }}" alt="{{ config('app.name') }}" class="d-none d-md-block" style="height:30px;">
</a>
</div>
<div class="yt-header-center d-none d-md-flex">
<form action="{{ route('videos.search') }}" method="GET" class="yt-search">
<input type="text" name="q" class="yt-search-input" placeholder="Search" value="{{ request('q') }}">
<button type="submit" class="yt-search-btn">
<i class="bi bi-search"></i>
</button>
</form>
<div class="yt-header-center d-none d-md-flex">
<div class="yt-search">
<form action="{{ route('videos.search') }}" method="GET" class="yt-search-form">
<input type="text" name="q" class="yt-search-input" placeholder="Search" value="{{ request('q') }}">
<button type="submit" class="yt-search-btn"><i class="bi bi-search"></i></button>
</form>
</div>
</div>
<div class="yt-header-right">
<!-- Mobile Search Toggle Button -->
<button type="button" class="yt-mobile-search-toggle d-md-none" onclick="toggleMobileSearch()">
<!-- Mobile search -->
<button type="button" class="yt-icon-btn d-md-none" onclick="toggleMobileSearch()">
<i class="bi bi-search"></i>
</button>
@auth
<!-- Upload Button - Opens Modal -->
<button type="button" class="yt-upload-btn d-none d-md-flex" data-bs-toggle="modal" data-bs-target="#uploadModal">
<i class="bi bi-plus-lg"></i>
<span>Upload</span>
<!-- Create / Upload -->
<button type="button" class="yt-upload-btn d-none d-md-flex"
data-bs-toggle="modal" data-bs-target="#uploadModal">
<i class="bi bi-camera-video-fill"></i>
<span>Create</span>
</button>
<!-- User Dropdown -->
<div class="dropdown">
<button class="yt-icon-btn" data-bs-toggle="dropdown" aria-expanded="false">
@if(Auth::user()->avatar)
<img src="{{ asset('storage/avatars/' . Auth::user()->avatar) }}" class="yt-user-avatar" alt="User">
@else
<img src="https://i.pravatar.cc/150?u={{ Auth::user()->id }}" class="yt-user-avatar" alt="User">
@endif
<!-- Notifications -->
<div class="yt-notif-wrap" id="notifWrap">
<button type="button" class="yt-icon-btn" id="notifBtn"
onclick="toggleNotifPanel()" title="Notifications" aria-expanded="false">
<i class="bi bi-bell-fill"></i>
<span class="yt-notif-badge" id="notifBadge" style="display:none;"></span>
</button>
<ul class="dropdown-menu dropdown-menu-end dropdown-menu-dark">
@if(Auth::user()->isSuperAdmin())
<li><a class="dropdown-item" href="{{ route('admin.dashboard') }}"><i class="bi bi-speedometer2"></i> Admin Dashboard</a></li>
<li><hr class="dropdown-divider"></li>
@endif
<li><a class="dropdown-item" href="{{ route('profile') }}"><i class="bi bi-person"></i> Profile</a></li>
<li><a class="dropdown-item" href="{{ route('channel', Auth::user()->id) }}"><i class="bi bi-play-btn"></i> My Channel</a></li>
<li><a class="dropdown-item" href="{{ route('settings') }}"><i class="bi bi-gear"></i> Settings</a></li>
<li><hr class="dropdown-divider"></li>
<li>
<form method="POST" action="{{ route('logout') }}" class="m-0">
@csrf
<button type="submit" class="dropdown-item text-danger"><i class="bi bi-box-arrow-right"></i> Logout</button>
</form>
</li>
</ul>
<div class="yt-notif-panel" id="notifPanel" style="display:none;" role="menu">
<div class="yt-notif-header">
<span class="yt-notif-title">Notifications</span>
<button type="button" class="yt-notif-mark-all" id="notifMarkAll"
onclick="markAllRead()" style="display:none;">Mark all read</button>
</div>
<div class="yt-notif-list" id="notifList">
<div class="yt-notif-empty">Loading…</div>
</div>
</div>
</div>
<!-- User dropdown -->
<div class="yt-user-dropdown" id="headerUserDropdown">
<button class="yt-icon-btn" id="headerUserBtn"
onclick="toggleHeaderDropdown()" aria-expanded="false" aria-haspopup="true">
<img src="{{ Auth::user()->avatar_url }}"
class="yt-user-avatar" alt="{{ Auth::user()->name }}">
</button>
<div class="yt-user-panel" id="headerUserPanel" role="menu">
<div class="yt-panel-info">
<img src="{{ Auth::user()->avatar_url }}" alt="{{ Auth::user()->name }}">
<div style="min-width:0;">
<div class="yt-panel-name">{{ Auth::user()->name }}</div>
<div class="yt-panel-email">{{ Auth::user()->email }}</div>
</div>
</div>
<div class="yt-panel-links">
@if(Auth::user()->isSuperAdmin())
<a href="{{ route('admin.dashboard') }}" class="yt-drop-item">
<i class="bi bi-speedometer2"></i> Admin Dashboard
</a>
<div class="yt-panel-divider"></div>
@endif
<a href="{{ route('channel', Auth::user()->channel) }}" class="yt-drop-item">
<i class="bi bi-person-video3"></i> My Channel
</a>
<a href="{{ route('channel', Auth::user()->channel) }}#settings" class="yt-drop-item">
<i class="bi bi-gear"></i> Settings
</a>
<div class="yt-panel-divider"></div>
<form method="POST" action="{{ route('logout') }}" style="margin:0;">
@csrf
<button type="submit" class="yt-drop-item danger">
<i class="bi bi-box-arrow-right"></i> Sign Out
</button>
</form>
</div>
</div>
</div>
@else
<!-- Login/Register for visitors -->
<a href="{{ route('login') }}" class="yt-upload-btn">
<i class="bi bi-box-arrow-in-right"></i>
<span>Login</span>
<span>Sign In</span>
</a>
@endauth
</div>
@@ -4,19 +4,18 @@
<div class="modal-content" style="background: var(--bg-secondary); border: 1px solid var(--border-color); border-radius: 16px;">
<div class="modal-header" style="border-bottom: 1px solid var(--border-color); padding: 20px 24px;">
<h5 class="modal-title" id="shareModalLabel" style="font-weight: 600; color: var(--text-primary);">
<i class="bi bi-share me-2"></i>Share Video
<i class="bi bi-share me-2"></i>Share
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body" style="padding: 24px;">
<p style="color: var(--text-secondary); margin-bottom: 16px;">Share this video with your friends:</p>
<p style="color: var(--text-secondary); margin-bottom: 16px;">Share this link with your friends:</p>
<div class="share-link-container" style="display: flex; gap: 8px; align-items: center;">
<input type="text" id="shareLinkInput" class="form-control" readonly
style="background: var(--bg-primary); border: 1px solid var(--border-color); color: var(--text-primary); padding: 12px 16px; border-radius: 8px;">
<button type="button" id="copyLinkBtn" class="btn-copy"
style="background: var(--brand-red); color: white; border: none; padding: 12px 20px; border-radius: 8px; font-weight: 500; cursor: pointer; white-space: nowrap; transition: background 0.2s;">
<i class="bi bi-clipboard me-1"></i> Copy
<button type="button" id="copyLinkBtn" class="btn-copy action-btn action-btn-primary">
<i class="bi bi-clipboard"></i> <span>Copy</span>
</button>
</div>
@@ -58,67 +57,113 @@
transform: scale(1.1);
opacity: 0.9;
}
.btn-copy:hover {
background: #cc1a1a !important;
}
.btn-copy.copied {
background: #4caf50 !important;
border-color: #4caf50 !important;
}
</style>
<script>
function openShareModal(videoUrl, videoTitle) {
document.getElementById('shareLinkInput').value = videoUrl;
// Update social share links
var encodedUrl = encodeURIComponent(videoUrl);
var encodedTitle = encodeURIComponent(videoTitle);
document.getElementById('shareFacebook').href = 'https://www.facebook.com/sharer/sharer.php?u=' + encodedUrl;
document.getElementById('shareTwitter').href = 'https://twitter.com/intent/tweet?url=' + encodedUrl + '&text=' + encodedTitle;
document.getElementById('shareWhatsApp').href = 'https://wa.me/?text=' + encodedTitle + ' ' + encodedUrl;
document.getElementById('shareTelegram').href = 'https://t.me/share/url?url=' + encodedUrl + '&text=' + encodedTitle;
// Reset copy button state
var copyBtn = document.getElementById('copyLinkBtn');
copyBtn.innerHTML = '<i class="bi bi-clipboard me-1"></i> Copy';
copyBtn.classList.remove('copied');
document.getElementById('copySuccess').style.display = 'none';
// Show modal
var modal = new bootstrap.Modal(document.getElementById('shareModal'));
function _getLatestCsrf() {
var match = document.cookie.match(/(?:^|;\s*)XSRF-TOKEN=([^;]+)/);
if (match) return decodeURIComponent(match[1]);
return (typeof csrf !== 'undefined') ? csrf : '';
}
async function openShareModal(videoUrl, videoTitle, recordUrl) {
var csrfToken = _getLatestCsrf();
var shareUrl = videoUrl;
// Obtain a unique tracked share link from the server
if (recordUrl) {
try {
var res = await fetch(recordUrl, {
method: 'POST',
headers: {
'X-CSRF-TOKEN': csrfToken,
'Accept': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
},
});
if (res.ok) {
var data = await res.json();
if (data.url) shareUrl = data.url;
}
} catch (e) { /* fallback to plain URL */ }
}
// Mobile: use native share sheet with the unique link
if (window.innerWidth <= 768 && navigator.share) {
navigator.share({ title: videoTitle, url: shareUrl }).catch(function() {});
return;
}
// Desktop: show modal
_populateShareModal(shareUrl, videoTitle);
var modal = new bootstrap.Modal(document.getElementById('shareModal'), { backdrop: true, keyboard: true });
modal.show();
}
document.addEventListener('DOMContentLoaded', function() {
function _populateShareModal(shareUrl, videoTitle) {
document.getElementById('shareLinkInput').value = shareUrl;
var encodedUrl = encodeURIComponent(shareUrl);
var encodedTitle = encodeURIComponent(videoTitle);
var waText = encodeURIComponent(videoTitle + '\n' + shareUrl);
document.getElementById('shareFacebook').href = 'https://www.facebook.com/sharer/sharer.php?u=' + encodedUrl;
document.getElementById('shareTwitter').href = 'https://twitter.com/intent/tweet?url=' + encodedUrl + '&text=' + encodedTitle;
document.getElementById('shareWhatsApp').href = 'https://wa.me/?text=' + waText;
document.getElementById('shareTelegram').href = 'https://t.me/share/url?url=' + encodedUrl + '&text=' + encodedTitle;
var copyBtn = document.getElementById('copyLinkBtn');
var shareInput = document.getElementById('shareLinkInput');
var copySuccess = document.getElementById('copySuccess');
if (copyBtn && shareInput) {
copyBtn.addEventListener('click', function() {
shareInput.select();
shareInput.setSelectionRange(0, 99999);
navigator.clipboard.writeText(shareInput.value).then(function() {
copyBtn.innerHTML = '<i class="bi bi-check-lg me-1"></i> Copied!';
copyBtn.classList.add('copied');
copySuccess.style.display = 'block';
setTimeout(function() {
copyBtn.innerHTML = '<i class="bi bi-clipboard me-1"></i> Copy';
copyBtn.classList.remove('copied');
copySuccess.style.display = 'none';
}, 2000);
}).catch(function(err) {
console.error('Failed to copy: ', err);
// Fallback for older browsers
document.execCommand('copy');
copyBtn.innerHTML = '<i class="bi bi-check-lg me-1"></i> Copied!';
copySuccess.style.display = 'block';
});
});
copyBtn.innerHTML = '<i class="bi bi-clipboard"></i> <span>Copy</span>';
copyBtn.classList.remove('copied');
document.getElementById('copySuccess').style.display = 'none';
}
function _copyToClipboard(text) {
// Prefer modern clipboard API (requires HTTPS)
if (navigator.clipboard && window.isSecureContext) {
return navigator.clipboard.writeText(text);
}
// Textarea fallback — works even inside Bootstrap modals
return new Promise(function(resolve, reject) {
var ta = document.createElement('textarea');
ta.value = text;
ta.setAttribute('readonly', '');
ta.style.cssText = 'position:fixed;top:-9999px;left:-9999px;opacity:0;';
document.body.appendChild(ta);
ta.focus();
ta.select();
var ok = false;
try { ok = document.execCommand('copy'); } catch(e) {}
document.body.removeChild(ta);
ok ? resolve() : reject();
});
}
document.addEventListener('DOMContentLoaded', function() {
var copyBtn = document.getElementById('copyLinkBtn');
var shareInput = document.getElementById('shareLinkInput');
var copySuccess = document.getElementById('copySuccess');
if (!copyBtn || !shareInput) return;
copyBtn.addEventListener('click', function() {
_copyToClipboard(shareInput.value).then(function() {
copyBtn.innerHTML = '<i class="bi bi-check-lg"></i> <span>Copied!</span>';
copyBtn.classList.add('copied');
copySuccess.style.display = 'block';
setTimeout(function() {
copyBtn.innerHTML = '<i class="bi bi-clipboard"></i> <span>Copy</span>';
copyBtn.classList.remove('copied');
copySuccess.style.display = 'none';
}, 2500);
}).catch(function() {
showToast('Could not copy — please copy the link manually.', 'error');
});
});
});
</script>
@@ -1,12 +1,14 @@
<!-- Sidebar -->
<nav class="yt-sidebar" id="sidebar">
<!-- Primary section -->
<div class="yt-sidebar-section">
<a href="{{ route('home') }}" class="yt-sidebar-link {{ request()->is('/') || request()->is('videos') ? 'active' : '' }}">
<i class="bi bi-house-door-fill"></i>
<span>Home</span>
</a>
<a href="{{ route('videos.shorts') }}" class="yt-sidebar-link {{ request()->is('shorts') ? 'active' : '' }}">
<i class="bi bi-play-btn"></i>
<i class="bi bi-play-btn-fill"></i>
<span>Shorts</span>
</a>
<a href="{{ route('videos.trending') }}" class="yt-sidebar-link {{ request()->is('trending') ? 'active' : '' }}">
@@ -15,36 +17,94 @@
</a>
@auth
<a href="#" class="yt-sidebar-link">
<i class="bi bi-collection-play"></i>
<i class="bi bi-collection-play-fill"></i>
<span>Subscriptions</span>
</a>
@endauth
</div>
<!-- You section (auth only) -->
@auth
@php $channelBase = route('channel', Auth::user()->channel); @endphp
<div class="yt-sidebar-section">
<a href="{{ route('channel', Auth::user()->id) }}" class="yt-sidebar-link">
<i class="bi bi-person-video"></i>
<span>Your Channel</span>
<div class="yt-sidebar-section-header">You</div>
<a href="{{ $channelBase }}#wall"
class="yt-sidebar-link" id="sb-tab-wall">
<i class="bi bi-layout-text-sidebar-reverse"></i>
<span>My Wall</span>
</a>
<a href="{{ route('playlists.index') }}" class="yt-sidebar-link {{ request()->is('playlists*') ? 'active' : '' }}">
<i class="bi bi-collection-play"></i>
<span>Playlists</span>
<a href="{{ $channelBase }}#videos"
class="yt-sidebar-link" id="sb-tab-videos">
<i class="bi bi-play-circle-fill"></i>
<span>My Videos</span>
</a>
<a href="{{ route('history') }}" class="yt-sidebar-link">
<a href="{{ $channelBase }}#shorts"
class="yt-sidebar-link" id="sb-tab-shorts">
<i class="bi bi-phone-fill"></i>
<span>My Shorts</span>
</a>
<a href="{{ $channelBase }}#playlists"
class="yt-sidebar-link" id="sb-tab-playlists">
<i class="bi bi-collection-play-fill"></i>
<span>My Playlists</span>
</a>
<a href="{{ route('history') }}" class="yt-sidebar-link {{ request()->is('history') ? 'active' : '' }}">
<i class="bi bi-clock-history"></i>
<span>History</span>
</a>
<a href="{{ route('liked') }}" class="yt-sidebar-link">
<i class="bi bi-hand-thumbs-up"></i>
<a href="{{ route('liked') }}" class="yt-sidebar-link {{ request()->is('liked') ? 'active' : '' }}">
<i class="bi bi-hand-thumbs-up-fill"></i>
<span>Liked Videos</span>
</a>
@if(Auth::user()->isSuperAdmin())
<a href="{{ route('admin.dashboard') }}" class="yt-sidebar-link {{ request()->is('admin*') ? 'active' : '' }}">
<i class="bi bi-speedometer2"></i>
<span>Admin Panel</span>
</a>
@endif
</div>
@endauth
<!-- Explore section -->
<div class="yt-sidebar-section">
<div class="yt-sidebar-section-header">Explore</div>
<a href="{{ route('videos.index', ['filter' => 'music']) }}" class="yt-sidebar-link {{ request()->is('videos') && request('filter') === 'music' ? 'active' : '' }}">
<i class="bi bi-music-note-beamed"></i>
<span>Music</span>
</a>
<a href="{{ route('videos.index', ['filter' => 'match']) }}" class="yt-sidebar-link {{ request()->is('videos') && request('filter') === 'match' ? 'active' : '' }}">
<i class="bi bi-trophy-fill"></i>
<span>Sports</span>
</a>
</div>
<!-- Admin section (super_admin only) -->
@auth
@if(Auth::user()->isSuperAdmin())
<div class="yt-sidebar-section">
<div class="yt-sidebar-section-header">Admin</div>
<a href="{{ route('admin.dashboard') }}" class="yt-sidebar-link {{ request()->is('admin*') ? 'active' : '' }}">
<i class="bi bi-speedometer2"></i>
<span>Dashboard</span>
</a>
</div>
@endif
@endauth
</nav>
@auth
<script>
(function () {
var channelPath = @json(parse_url(route('channel', Auth::user()->channel), PHP_URL_PATH));
var onChannel = location.pathname === channelPath;
if (!onChannel) return;
var hash = location.hash.replace('#', '') || 'wall';
var el = document.getElementById('sb-tab-' + hash);
if (el) el.classList.add('active');
window.addEventListener('hashchange', function () {
document.querySelectorAll('#sb-tab-wall,#sb-tab-videos,#sb-tab-shorts,#sb-tab-playlists')
.forEach(function (a) { a.classList.remove('active'); });
var h = location.hash.replace('#', '') || 'wall';
var t = document.getElementById('sb-tab-' + h);
if (t) t.classList.add('active');
});
}());
</script>
@endauth
@@ -22,19 +22,20 @@
<form id="upload-form-modal" enctype="multipart/form-data">
@csrf
<!-- Video File -->
<!-- Video / Audio File -->
<div class="form-group">
<label class="form-label">
<i class="bi bi-camera-video"></i> Video File *
<label class="form-label" id="file-label-modal">
<i class="bi bi-camera-video" id="file-label-icon"></i>
<span id="file-label-text">Video or Music File *</span>
</label>
<div id="video-dropzone-modal" class="dropzone-modal">
<input type="file" name="video" id="video-modal" accept="video/*" required>
<input type="file" name="video" id="video-modal" accept="video/*,audio/mpeg,audio/mp4,audio/aac,audio/flac,audio/wav,.mp3,.m4a,.aac,.flac,.wav" required>
<div id="dropzone-default-modal">
<div class="dropzone-icon">
<i class="bi bi-cloud-arrow-up"></i>
</div>
<p class="dropzone-title">Click to select or drag video here</p>
<p class="dropzone-hint">MP4, MOV, AVI, WebM and more</p>
<p class="dropzone-title">Click to select or drag file here</p>
<p class="dropzone-hint">Video: MP4, MOV, AVI, WebM — Audio: MP3, M4A, AAC, FLAC</p>
</div>
<div id="file-info-modal" class="file-info-modal">
<div class="file-preview">
@@ -71,10 +72,10 @@
placeholder="Tell viewers about your video (Markdown supported)"></textarea>
</div>
<!-- Thumbnail -->
<div class="form-group">
<label class="form-label">
<i class="bi bi-image"></i> Thumbnail
<!-- Thumbnail (video) / Slides (audio) -->
<div class="form-group" id="thumbnail-section-modal">
<label class="form-label" id="thumbnail-label-modal">
<i class="bi bi-image"></i> <span id="thumbnail-label-text">Thumbnail</span>
</label>
<div id="thumbnail-dropzone-modal" class="dropzone-modal thumbnail-dropzone">
<input type="file" name="thumbnail" id="thumbnail-modal" accept="image/*">
@@ -82,7 +83,7 @@
<div class="dropzone-icon thumbnail-icon">
<i class="bi bi-card-image"></i>
</div>
<p class="dropzone-title">Click to select thumbnail (optional)</p>
<p class="dropzone-title" id="thumbnail-dropzone-hint">Click to select thumbnail (optional)</p>
</div>
<div id="thumbnail-info-modal" class="file-info-modal thumbnail-info">
<div class="file-preview thumbnail-preview">
@@ -99,6 +100,30 @@
</div>
</div>
<!-- Slides dropzone (audio only) -->
<div class="form-group" id="slides-section-modal" style="display:none;">
<label class="form-label">
<i class="bi bi-images"></i> Slides <span style="color:#f00;">*</span>
<span style="font-weight:400;font-size:12px;color:var(--text-secondary);margin-left:6px;">Select 1–10 images — multiple images will crossfade</span>
</label>
<div id="slides-dropzone-modal" class="dropzone-modal thumbnail-dropzone" style="min-height:80px;">
<input type="file" name="slides[]" id="slides-modal" accept="image/*" multiple style="display:none;">
<div id="slides-default-modal">
<div class="dropzone-icon thumbnail-icon"><i class="bi bi-images"></i></div>
<p class="dropzone-title">Click or drag to add cover images</p>
</div>
<div id="slides-preview-modal" style="display:none;padding:10px 12px;">
<div id="slides-strip-modal" style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:8px;"></div>
<div style="display:flex;align-items:center;justify-content:space-between;">
<span id="slides-count-modal" style="font-size:12px;color:var(--text-secondary);"></span>
<button type="button" onclick="removeSlidesModal(event)" style="background:none;border:none;color:var(--text-secondary);cursor:pointer;font-size:12px;padding:0;">
<i class="bi bi-x-lg"></i> Clear
</button>
</div>
</div>
</div>
</div>
<!-- Video Type -->
<div class="form-group">
<label class="form-label">
@@ -162,6 +187,43 @@
<!-- Hidden Shorts -->
<input type="hidden" name="is_shorts" id="is_shorts_modal" value="0">
<!-- Download Access -->
<div class="form-group" style="margin-bottom: 16px;">
<label class="form-label">
<i class="bi bi-download"></i> Who can download?
</label>
<div class="options-grid" id="download-options-modal">
<label class="option-item active" data-download="disabled">
<input type="radio" name="download_access" value="disabled" checked>
<div class="option-content">
<i class="bi bi-slash-circle"></i>
<span>Disabled</span>
</div>
</label>
<label class="option-item" data-download="everyone">
<input type="radio" name="download_access" value="everyone">
<div class="option-content">
<i class="bi bi-globe"></i>
<span>Everyone</span>
</div>
</label>
<label class="option-item" data-download="registered">
<input type="radio" name="download_access" value="registered">
<div class="option-content">
<i class="bi bi-person-check"></i>
<span>Members</span>
</div>
</label>
<label class="option-item" data-download="subscribers">
<input type="radio" name="download_access" value="subscribers">
<div class="option-content">
<i class="bi bi-star"></i>
<span>Subscribers</span>
</div>
</label>
</div>
</div>
<!-- Progress Bar -->
<div id="progress-container-modal" class="progress-container-modal">
<div class="progress-bar-wrapper">
@@ -174,8 +236,8 @@
<div id="status-message-modal" class="status-message-modal"></div>
<!-- Submit Button -->
<button type="submit" id="submit-btn-modal" class="btn-submit">
<i class="bi bi-cloud-arrow-up-fill"></i> Upload Video
<button type="submit" id="submit-btn-modal" class="action-btn action-btn-primary btn-submit">
<i class="bi bi-cloud-arrow-up-fill"></i> <span>Upload Video</span>
</button>
</form>
</div>
@@ -324,13 +386,7 @@
}
.dropzone-modal input[type="file"] {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0;
cursor: pointer;
display: none;
}
.dropzone-icon {
@@ -437,6 +493,7 @@
.btn-remove-file {
width: 28px;
height: 28px;
padding: 0;
border-radius: 50%;
background: #333;
border: none;
@@ -569,28 +626,16 @@
/* Submit Button */
.btn-submit {
width: 100%;
background: #e61e1e;
color: white;
border: none;
padding: 14px 24px;
border-radius: 8px;
justify-content: center;
font-size: 15px;
font-weight: 600;
cursor: pointer;
transition: background 0.2s;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.btn-submit:hover {
background: #cc1a1a;
}
.btn-submit:disabled {
background: #555;
background: #555 !important;
border-color: #555 !important;
cursor: not-allowed;
opacity: 1;
}
/* Responsive */
@@ -673,6 +718,15 @@ function resetUploadForm() {
item.classList.add('active');
}
});
document.querySelectorAll('#download-options-modal .option-item').forEach(item => {
item.classList.remove('active');
item.querySelector('input').checked = false;
if (item.dataset.download === 'disabled') {
item.classList.add('active');
item.querySelector('input').checked = true;
}
});
}
// Format file size
@@ -693,7 +747,8 @@ videoInputModal.addEventListener('change', function() {
});
videoDropzoneModal.addEventListener('click', function(e) {
if (e.target.classList.contains('btn-remove-file')) return;
if (e.target.closest('.btn-remove-file')) return;
videoInputModal.click();
});
videoDropzoneModal.addEventListener('dragover', (e) => {
@@ -714,23 +769,56 @@ videoDropzoneModal.addEventListener('drop', (e) => {
}
});
const AUDIO_EXTENSIONS = ['mp3', 'm4a', 'aac', 'flac', 'wav'];
function isAudioFile(file) {
const ext = file.name.split('.').pop().toLowerCase();
return AUDIO_EXTENSIONS.includes(ext) || file.type.startsWith('audio/');
}
function setAudioMode(audio) {
// Label
document.getElementById('file-label-icon').className = audio ? 'bi bi-music-note-beamed' : 'bi bi-camera-video';
document.getElementById('file-label-text').textContent = audio ? 'Music File *' : 'Video or Music File *';
// Toggle thumbnail vs slides section
document.getElementById('thumbnail-section-modal').style.display = audio ? 'none' : '';
document.getElementById('slides-section-modal').style.display = audio ? '' : 'none';
document.getElementById('slides-dropzone-modal').style.borderColor = audio ? '#e61e1e' : '';
// Auto-select Music type when audio
if (audio) {
document.querySelectorAll('#type-options-modal .option-item').forEach(o => o.classList.remove('active'));
const musicOption = document.querySelector('#type-options-modal .option-item[data-type="music"]');
if (musicOption) {
musicOption.classList.add('active');
musicOption.querySelector('input').checked = true;
}
}
// Update file preview icon
const previewIcon = document.querySelector('#file-info-modal .file-preview i');
if (previewIcon) previewIcon.className = audio ? 'bi bi-music-note-beamed' : 'bi bi-film';
// Update submit label
document.getElementById('submit-btn-modal').innerHTML =
'<i class="bi bi-cloud-arrow-up-fill"></i> <span>' + (audio ? 'Upload Track' : 'Upload Video') + '</span>';
}
function handleVideoSelectModal(input) {
if (input.files && input.files[0]) {
const file = input.files[0];
const maxSize = 512 * 1024 * 1024; // 512MB
// Validate file type
const validTypes = ['video/mp4', 'video/webm', 'video/ogg', 'video/quicktime', 'video/x-msvideo', 'video/x-flv', 'video/x-matroska'];
const validExtensions = ['.mp4', '.webm', '.ogg', '.mov', '.avi', '.wmv', '.flv', '.mkv'];
const validVideoTypes = ['video/mp4', 'video/webm', 'video/ogg', 'video/quicktime', 'video/x-msvideo', 'video/x-flv', 'video/x-matroska'];
const validVideoExtensions = ['.mp4', '.webm', '.ogg', '.mov', '.avi', '.wmv', '.flv', '.mkv'];
const ext = '.' + file.name.split('.').pop().toLowerCase();
let isValidType = validTypes.includes(file.type);
let fileExtension = '.' + file.name.split('.').pop().toLowerCase();
if (!isValidType) {
isValidType = validExtensions.includes(fileExtension);
}
const audio = isAudioFile(file);
const validVideo = validVideoTypes.includes(file.type) || validVideoExtensions.includes(ext);
if (!isValidType) {
alert('Invalid video format. Please select a valid video file (MP4, MOV, AVI, WebM, OGG, WMV, FLV, MKV).');
if (!audio && !validVideo) {
showToast('Invalid format. Accepted: MP4, MOV, AVI, WebM, MKV, MP3, M4A, AAC, FLAC, WAV.', 'error');
input.value = '';
document.getElementById('dropzone-default-modal').style.display = 'block';
document.getElementById('file-info-modal').classList.remove('active');
@@ -738,14 +826,16 @@ function handleVideoSelectModal(input) {
}
if (file.size > maxSize) {
alert('File size exceeds 512MB limit. Please select a smaller video file.');
showToast('File size exceeds 512MB limit.', 'error');
input.value = '';
document.getElementById('dropzone-default-modal').style.display = 'block';
document.getElementById('file-info-modal').classList.remove('active');
return;
}
// Auto-fill title from filename (like create page)
setAudioMode(audio);
// Auto-fill title from filename
const fileNameWithoutExt = file.name.replace(/\.[^/.]+$/, '').replace(/[-_]/g, ' ');
document.getElementById('upload-video-title').value = fileNameWithoutExt;
@@ -763,14 +853,26 @@ function removeVideoModal(e) {
document.getElementById('dropzone-default-modal').style.display = 'block';
document.getElementById('file-info-modal').classList.remove('active');
document.getElementById('upload-video-title').value = '';
setAudioMode(false);
}
// Thumbnail Dropzone
const thumbnailDropzoneModal = document.getElementById('thumbnail-dropzone-modal');
const thumbnailInputModal = document.getElementById('thumbnail-modal');
let thumbnailInputModal = document.getElementById('thumbnail-modal');
thumbnailInputModal.addEventListener('change', function() {
handleThumbnailSelectModal(this);
let _thumbClickTsModal = 0;
thumbnailInputModal.addEventListener('change', function() { _thumbClickTsModal = 0; handleThumbnailSelectModal(this); });
thumbnailDropzoneModal.addEventListener('click', function(e) {
if (e.target.closest('.btn-remove-file')) return;
const now = Date.now();
if (now - _thumbClickTsModal < 800) return;
_thumbClickTsModal = now;
if (typeof window.openCropperModal_thumb_upload === 'function') {
window.openCropperModal_thumb_upload();
} else {
thumbnailInputModal.click();
}
});
thumbnailDropzoneModal.addEventListener('dragover', (e) => {
@@ -786,8 +888,14 @@ thumbnailDropzoneModal.addEventListener('drop', (e) => {
e.preventDefault();
thumbnailDropzoneModal.classList.remove('dragover');
if (e.dataTransfer.files.length) {
thumbnailInputModal.files = e.dataTransfer.files;
handleThumbnailSelectModal(thumbnailInputModal);
const droppedFile = e.dataTransfer.files[0];
if (typeof window.tcPreload_thumb_upload === 'function') {
window.tcPreload_thumb_upload(droppedFile);
window.openCropperModal_thumb_upload();
} else {
thumbnailInputModal.files = e.dataTransfer.files;
handleThumbnailSelectModal(thumbnailInputModal);
}
}
});
@@ -812,11 +920,69 @@ function handleThumbnailSelectModal(input) {
function removeThumbnailModal(e) {
e.preventDefault();
e.stopPropagation();
thumbnailInputModal.value = '';
const fresh = thumbnailInputModal.cloneNode(false);
thumbnailInputModal.parentNode.replaceChild(fresh, thumbnailInputModal);
thumbnailInputModal = fresh;
thumbnailInputModal.addEventListener('change', function() { handleThumbnailSelectModal(this); });
document.getElementById('thumbnail-default-modal').style.display = 'block';
document.getElementById('thumbnail-info-modal').classList.remove('active');
}
// Slides dropzone (audio mode)
const slidesDropzoneModal = document.getElementById('slides-dropzone-modal');
let slidesInputModal = document.getElementById('slides-modal');
function handleSlidesSelectModal(input) {
if (!input.files || !input.files.length) return;
const files = Array.from(input.files);
const strip = document.getElementById('slides-strip-modal');
strip.innerHTML = '';
files.forEach(f => {
const reader = new FileReader();
reader.onload = e => {
const img = document.createElement('img');
img.src = e.target.result;
img.style.cssText = 'width:56px;height:56px;object-fit:cover;border-radius:6px;border:1px solid rgba(255,255,255,.15);';
strip.appendChild(img);
};
reader.readAsDataURL(f);
});
const count = files.length;
document.getElementById('slides-count-modal').textContent =
count === 1 ? '1 image — will show as static cover' : count + ' images — will crossfade during playback';
document.getElementById('slides-default-modal').style.display = 'none';
document.getElementById('slides-preview-modal').style.display = '';
slidesDropzoneModal.style.borderColor = '#22c55e';
}
slidesDropzoneModal.addEventListener('click', function(e) {
if (e.target.closest('button')) return;
slidesInputModal.click();
});
slidesInputModal.addEventListener('change', function() { handleSlidesSelectModal(this); });
slidesDropzoneModal.addEventListener('dragover', e => { e.preventDefault(); slidesDropzoneModal.classList.add('dragover'); });
slidesDropzoneModal.addEventListener('dragleave', () => slidesDropzoneModal.classList.remove('dragover'));
slidesDropzoneModal.addEventListener('drop', e => {
e.preventDefault();
slidesDropzoneModal.classList.remove('dragover');
if (e.dataTransfer.files.length) {
slidesInputModal.files = e.dataTransfer.files;
handleSlidesSelectModal(slidesInputModal);
}
});
function removeSlidesModal(e) {
e.preventDefault();
e.stopPropagation();
const fresh = slidesInputModal.cloneNode(false);
slidesInputModal.parentNode.replaceChild(fresh, slidesInputModal);
slidesInputModal = fresh;
slidesInputModal.addEventListener('change', function() { handleSlidesSelectModal(this); });
document.getElementById('slides-default-modal').style.display = 'block';
document.getElementById('slides-preview-modal').style.display = 'none';
slidesDropzoneModal.style.borderColor = '#e61e1e';
}
// Type options
document.querySelectorAll('#type-options-modal .option-item').forEach(item => {
item.querySelector('input').addEventListener('change', function() {
@@ -833,6 +999,14 @@ document.querySelectorAll('#visibility-options-modal .option-item').forEach(item
});
});
// Download options
document.querySelectorAll('#download-options-modal .option-item').forEach(item => {
item.querySelector('input').addEventListener('change', function() {
document.querySelectorAll('#download-options-modal .option-item').forEach(o => o.classList.remove('active'));
item.classList.add('active');
});
});
// Form submission
document.getElementById('upload-form-modal').addEventListener('submit', function(e) {
e.preventDefault();
@@ -847,10 +1021,19 @@ document.getElementById('upload-form-modal').addEventListener('submit', function
// Validate title
const title = document.getElementById('upload-video-title').value.trim();
if (!title) {
showErrorModal('Please enter a title for your video');
showErrorModal('Please enter a title');
return;
}
// At least one slide image is required for audio uploads
if (isAudioFile(videoInput.files[0])) {
if (!slidesInputModal.files || !slidesInputModal.files.length) {
showErrorModal('At least one cover image is required for audio tracks');
document.getElementById('slides-dropzone-modal').style.borderColor = '#e61e1e';
return;
}
}
const formData = new FormData(this);
const xhr = new XMLHttpRequest();
@@ -895,9 +1078,17 @@ document.getElementById('upload-form-modal').addEventListener('submit', function
} else {
try {
const response = JSON.parse(xhr.responseText);
showErrorModal(response.message || `Server error (${xhr.status})`);
// Laravel validation errors come back as { errors: { field: ['msg'] } }
if (response.errors) {
const msgs = Object.entries(response.errors)
.map(([field, errs]) => `<b>${field}:</b> ${errs.join(', ')}`)
.join('<br>');
showErrorModal(msgs);
} else {
showErrorModal(response.message || `Server error (${xhr.status})`);
}
} catch(e) {
showErrorModal(`Upload failed with status ${xhr.status}`);
showErrorModal(`Upload failed (${xhr.status}): ${xhr.responseText.slice(0, 300)}`);
}
}
});
@@ -927,3 +1118,14 @@ function showErrorModal(message) {
document.getElementById('submit-btn-modal').innerHTML = '<i class="bi bi-cloud-arrow-up-fill"></i> Upload Video';
}
</script>
<x-image-cropper
id="thumb_upload"
:width="448"
:height="252"
shape="square"
target-input="thumbnail-modal"
preview-img="thumbnail-preview-img"
output-width="1280"
title="Crop Thumbnail"
/>