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:
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
@@ -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"
|
||||
/>
|
||||
@@ -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"
|
||||
/>
|
||||
Reference in new issue
Block a user