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
@@ -46,6 +46,16 @@
|
||||
color: var(--brand-red) !important;
|
||||
}
|
||||
|
||||
.action-btn.subscribed {
|
||||
background: var(--brand-red);
|
||||
border-color: var(--brand-red);
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.mobile-action-dropdown .dropdown-item.subscribed {
|
||||
color: var(--brand-red) !important;
|
||||
}
|
||||
|
||||
.mobile-action-dropdown {
|
||||
display: none;
|
||||
position: relative;
|
||||
@@ -99,28 +109,36 @@
|
||||
<div class="video-actions" style="display: flex; align-items: center; gap: 8px; flex-wrap: wrap; overflow: visible;">
|
||||
@auth
|
||||
@if (Auth::id() === $video->user_id)
|
||||
<button class="action-btn desktop-action" onclick="openEditVideoModal({{ $video->id }})">
|
||||
<button class="action-btn desktop-action" onclick="openEditVideoModal('{{ $video->getRouteKey() }}')">
|
||||
<i class="bi bi-pencil"></i>
|
||||
<span>Edit</span>
|
||||
</button>
|
||||
@elseif (Auth::id() !== $video->user_id)
|
||||
<button class="action-btn desktop-action"><i class="bi bi-bell"></i><span>Subscribe</span></button>
|
||||
@php $isSubscribed = Auth::user()->isSubscribedTo($video->user); @endphp
|
||||
<button type="button"
|
||||
class="action-btn desktop-action subscribe-toggle-btn {{ $isSubscribed ? 'subscribed' : '' }}"
|
||||
data-channel-id="{{ $video->user_id }}"
|
||||
data-subscribe-url="{{ route('channel.subscribe', $video->user_id) }}"
|
||||
data-subscribed="{{ $isSubscribed ? 'true' : 'false' }}">
|
||||
<i class="bi {{ $isSubscribed ? 'bi-bell-fill' : 'bi-bell' }}"></i>
|
||||
<span class="subscribe-label">{{ $isSubscribed ? 'Subscribed' : 'Subscribe' }}</span>
|
||||
</button>
|
||||
@endif
|
||||
@else
|
||||
<button onclick="window.location.href='{{ route('login') }}'" class="action-btn desktop-action"><i
|
||||
class="bi bi-bell"></i><span>Subscribe</span></button>
|
||||
<button onclick="window.location.href='{{ route('login') }}'" class="action-btn desktop-action">
|
||||
<i class="bi bi-bell"></i><span>Subscribe</span>
|
||||
</button>
|
||||
@endauth
|
||||
|
||||
@auth
|
||||
<form method="POST"
|
||||
action="{{ $video->isLikedBy(Auth::user()) ? route('videos.unlike', $video->id) : route('videos.like', $video->id) }}"
|
||||
class="d-inline desktop-action">
|
||||
@csrf
|
||||
<button type="submit" class="action-btn {{ $video->isLikedBy(Auth::user()) ? 'liked' : '' }}">
|
||||
<i class="bi {{ $video->isLikedBy(Auth::user()) ? 'bi-hand-thumbs-up-fill' : 'bi-hand-thumbs-up' }}"></i>
|
||||
{{ $video->like_count > 0 ? number_format($video->like_count) : 'Like' }}
|
||||
</button>
|
||||
</form>
|
||||
<button type="button"
|
||||
class="action-btn desktop-action like-toggle-btn {{ $video->isLikedBy(Auth::user()) ? 'liked' : '' }}"
|
||||
data-video-id="{{ $video->id }}"
|
||||
data-toggle-url="{{ route('videos.toggleLike', $video) }}"
|
||||
data-liked="{{ $video->isLikedBy(Auth::user()) ? 'true' : 'false' }}">
|
||||
<i class="bi {{ $video->isLikedBy(Auth::user()) ? 'bi-hand-thumbs-up-fill' : 'bi-hand-thumbs-up' }}"></i>
|
||||
<span class="like-count-label">{{ $video->like_count > 0 ? number_format($video->like_count) : 'Like' }}</span>
|
||||
</button>
|
||||
@else
|
||||
<button onclick="window.location.href='{{ route('login') }}'" class="action-btn desktop-action">
|
||||
<i class="bi bi-hand-thumbs-up"></i>
|
||||
@@ -130,7 +148,7 @@
|
||||
|
||||
@if ($video->isShareable())
|
||||
<button class="action-btn desktop-action"
|
||||
onclick="openShareModal('{{ $video->share_url }}', '{{ addslashes($video->title) }}')">
|
||||
onclick="openShareModal('{{ $video->share_url }}', '{{ addslashes($video->title) }}', '{{ route('videos.recordShare', $video) }}')">
|
||||
<i class="bi bi-share"></i> Share
|
||||
</button>
|
||||
@endif
|
||||
@@ -144,6 +162,51 @@
|
||||
<span>Save</span>
|
||||
</button>
|
||||
|
||||
@php
|
||||
$dlAccess = $video->download_access ?? 'disabled';
|
||||
$dlUser = Auth::user();
|
||||
$showDl = match($dlAccess) {
|
||||
'everyone' => true,
|
||||
'registered' => (bool) $dlUser,
|
||||
'subscribers' => $dlUser && ($dlUser->id === $video->user_id || $dlUser->isSubscribedTo($video->user)),
|
||||
default => false,
|
||||
};
|
||||
@endphp
|
||||
@if($showDl)
|
||||
@php $isAudioDl = $video->isAudioOnly(); @endphp
|
||||
<!-- Download Dropdown -->
|
||||
<div class="dropdown desktop-action" style="display:inline-flex;">
|
||||
<button class="action-btn dropdown-toggle" type="button" id="downloadDropdown{{ $video->id }}"
|
||||
data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<i class="bi bi-download"></i>
|
||||
<span>Download</span>
|
||||
</button>
|
||||
<ul class="dropdown-menu" aria-labelledby="downloadDropdown{{ $video->id }}"
|
||||
style="background:var(--bg-secondary);border:1px solid var(--border-color);border-radius:10px;padding:6px 0;min-width:160px;">
|
||||
<li>
|
||||
@if($isAudioDl)
|
||||
<a class="dropdown-item" href="#"
|
||||
onclick="startSlideshowDownload('{{ $video->getRouteKey() }}'); return false;"
|
||||
style="color:var(--text-primary);display:flex;align-items:center;gap:8px;font-size:14px;padding:8px 14px;text-decoration:none;">
|
||||
<i class="bi bi-film"></i> Download Video
|
||||
</a>
|
||||
@else
|
||||
<a class="dropdown-item" href="{{ route('videos.download', $video) }}"
|
||||
style="color:var(--text-primary);display:flex;align-items:center;gap:8px;font-size:14px;padding:8px 14px;text-decoration:none;">
|
||||
<i class="bi bi-film"></i> Download Video
|
||||
</a>
|
||||
@endif
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item" href="{{ route('videos.downloadMp3', $video) }}"
|
||||
style="color:var(--text-primary);display:flex;align-items:center;gap:8px;font-size:14px;padding:8px 14px;text-decoration:none;">
|
||||
<i class="bi bi-music-note-beamed"></i> Download MP3
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="dropdown mobile-action-dropdown">
|
||||
<button class="action-btn dropdown-toggle" type="button" id="dropdownMenuLinkMusic{{ $video->id }}"
|
||||
data-bs-toggle="dropdown" aria-expanded="false">
|
||||
@@ -153,11 +216,17 @@
|
||||
<div class="dropdown-menu" aria-labelledby="dropdownMenuLinkMusic{{ $video->id }}">
|
||||
@auth
|
||||
@if (Auth::id() !== $video->user_id)
|
||||
<button type="button" class="dropdown-item">
|
||||
<i class="bi bi-bell"></i> Subscribe
|
||||
@php $isSubscribed = Auth::user()->isSubscribedTo($video->user); @endphp
|
||||
<button type="button"
|
||||
class="dropdown-item subscribe-toggle-btn {{ $isSubscribed ? 'subscribed' : '' }}"
|
||||
data-channel-id="{{ $video->user_id }}"
|
||||
data-subscribe-url="{{ route('channel.subscribe', $video->user_id) }}"
|
||||
data-subscribed="{{ $isSubscribed ? 'true' : 'false' }}">
|
||||
<i class="bi {{ $isSubscribed ? 'bi-bell-fill' : 'bi-bell' }}"></i>
|
||||
<span class="subscribe-label">{{ $isSubscribed ? 'Subscribed' : 'Subscribe' }}</span>
|
||||
</button>
|
||||
@else
|
||||
<button type="button" class="dropdown-item" onclick="openEditVideoModal({{ $video->id }})">
|
||||
<button type="button" class="dropdown-item" onclick="openEditVideoModal('{{ $video->getRouteKey() }}')">
|
||||
<i class="bi bi-pencil"></i> Edit
|
||||
</button>
|
||||
@endif
|
||||
@@ -168,16 +237,14 @@
|
||||
@endauth
|
||||
|
||||
@auth
|
||||
<form method="POST"
|
||||
action="{{ $video->isLikedBy(Auth::user()) ? route('videos.unlike', $video->id) : route('videos.like', $video->id) }}"
|
||||
class="d-inline w-100">
|
||||
@csrf
|
||||
<button type="submit" class="dropdown-item {{ $video->isLikedBy(Auth::user()) ? 'liked' : '' }}">
|
||||
<i
|
||||
class="bi {{ $video->isLikedBy(Auth::user()) ? 'bi-hand-thumbs-up-fill' : 'bi-hand-thumbs-up' }}"></i>
|
||||
{{ $video->like_count > 0 ? number_format($video->like_count) : 'Like' }}
|
||||
</button>
|
||||
</form>
|
||||
<button type="button"
|
||||
class="dropdown-item like-toggle-btn {{ $video->isLikedBy(Auth::user()) ? 'liked' : '' }}"
|
||||
data-video-id="{{ $video->id }}"
|
||||
data-toggle-url="{{ route('videos.toggleLike', $video) }}"
|
||||
data-liked="{{ $video->isLikedBy(Auth::user()) ? 'true' : 'false' }}">
|
||||
<i class="bi {{ $video->isLikedBy(Auth::user()) ? 'bi-hand-thumbs-up-fill' : 'bi-hand-thumbs-up' }}"></i>
|
||||
<span class="like-count-label">{{ $video->like_count > 0 ? number_format($video->like_count) : 'Like' }}</span>
|
||||
</button>
|
||||
@else
|
||||
<button class="dropdown-item" onclick="window.location.href='{{ route('login') }}'">
|
||||
<i class="bi bi-hand-thumbs-up"></i>
|
||||
@@ -187,7 +254,7 @@
|
||||
|
||||
@if ($video->isShareable())
|
||||
<button class="dropdown-item"
|
||||
onclick="openShareModal('{{ $video->share_url }}', '{{ addslashes($video->title) }}')">
|
||||
onclick="openShareModal('{{ $video->share_url }}', '{{ addslashes($video->title) }}', '{{ route('videos.recordShare', $video) }}')">
|
||||
<i class="bi bi-share"></i> Share
|
||||
</button>
|
||||
@endif
|
||||
@@ -195,11 +262,332 @@
|
||||
<button class="dropdown-item" onclick="openAddToPlaylistModal({{ $video->id }})">
|
||||
<i class="bi bi-bookmark"></i> Save
|
||||
</button>
|
||||
@if($showDl)
|
||||
@if($isAudioDl)
|
||||
<a class="dropdown-item" href="#"
|
||||
onclick="startSlideshowDownload('{{ $video->getRouteKey() }}'); return false;"
|
||||
style="color:var(--text-primary);display:flex;align-items:center;gap:8px;">
|
||||
<i class="bi bi-film"></i> Download Video
|
||||
</a>
|
||||
@else
|
||||
<a class="dropdown-item" href="{{ route('videos.download', $video) }}"
|
||||
style="color:var(--text-primary);display:flex;align-items:center;gap:8px;">
|
||||
<i class="bi bi-film"></i> Download Video
|
||||
</a>
|
||||
@endif
|
||||
<a class="dropdown-item" href="{{ route('videos.downloadMp3', $video) }}"
|
||||
style="color:var(--text-primary);display:flex;align-items:center;gap:8px;">
|
||||
<i class="bi bi-music-note-beamed"></i> Download MP3
|
||||
</a>
|
||||
@endif
|
||||
@if(Auth::check() && Auth::id() === $video->user_id)
|
||||
<button type="button" class="dropdown-item text-danger" onclick="showDeleteModal({{ $video->id }}, '{{ addslashes($video->title) }}')">
|
||||
<button type="button" class="dropdown-item text-danger" onclick="showDeleteModal('{{ $video->getRouteKey() }}', {{ json_encode($video->title) }})">
|
||||
<i class="bi bi-trash"></i> Delete
|
||||
</button>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
var csrfToken = '{{ csrf_token() }}';
|
||||
|
||||
// ── Subscribe toggle ─────────────────────────────────────────
|
||||
document.addEventListener('click', function (e) {
|
||||
var btn = e.target.closest('.subscribe-toggle-btn');
|
||||
if (!btn) return;
|
||||
|
||||
var url = btn.dataset.subscribeUrl;
|
||||
var subscribed = btn.dataset.subscribed === 'true';
|
||||
var channelId = btn.dataset.channelId;
|
||||
var nowSub = !subscribed;
|
||||
|
||||
// Optimistic update on all matching buttons (desktop + mobile)
|
||||
document.querySelectorAll('.subscribe-toggle-btn[data-channel-id="' + channelId + '"]').forEach(function (b) {
|
||||
b.dataset.subscribed = nowSub ? 'true' : 'false';
|
||||
b.classList.toggle('subscribed', nowSub);
|
||||
var icon = b.querySelector('i');
|
||||
var label = b.querySelector('.subscribe-label');
|
||||
if (icon) icon.className = 'bi ' + (nowSub ? 'bi-bell-fill' : 'bi-bell');
|
||||
if (label) label.textContent = nowSub ? 'Subscribed' : 'Subscribe';
|
||||
});
|
||||
|
||||
// Also update channel-row subscriber counts on the page
|
||||
document.querySelectorAll('.channel-subs[data-channel-id="' + channelId + '"]').forEach(function (el) {
|
||||
var n = parseInt(el.dataset.count || 0);
|
||||
el.dataset.count = nowSub ? n + 1 : Math.max(0, n - 1);
|
||||
el.textContent = Number(el.dataset.count).toLocaleString() + ' subscribers';
|
||||
});
|
||||
|
||||
fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'X-CSRF-TOKEN': csrfToken, 'Accept': 'application/json' },
|
||||
credentials: 'same-origin',
|
||||
})
|
||||
.then(function (r) {
|
||||
if (!r.ok) throw new Error(r.status);
|
||||
return r.json();
|
||||
})
|
||||
.then(function (data) {
|
||||
document.querySelectorAll('.subscribe-toggle-btn[data-channel-id="' + channelId + '"]').forEach(function (b) {
|
||||
b.dataset.subscribed = data.subscribed ? 'true' : 'false';
|
||||
b.classList.toggle('subscribed', data.subscribed);
|
||||
var icon = b.querySelector('i');
|
||||
var label = b.querySelector('.subscribe-label');
|
||||
if (icon) icon.className = 'bi ' + (data.subscribed ? 'bi-bell-fill' : 'bi-bell');
|
||||
if (label) label.textContent = data.subscribed ? 'Subscribed' : 'Subscribe';
|
||||
});
|
||||
document.querySelectorAll('.channel-subs[data-channel-id="' + channelId + '"]').forEach(function (el) {
|
||||
el.dataset.count = data.subscriber_count;
|
||||
el.textContent = Number(data.subscriber_count).toLocaleString() + ' subscribers';
|
||||
});
|
||||
})
|
||||
.catch(function (err) {
|
||||
// Revert optimistic update
|
||||
document.querySelectorAll('.subscribe-toggle-btn[data-channel-id="' + channelId + '"]').forEach(function (b) {
|
||||
b.dataset.subscribed = subscribed ? 'true' : 'false';
|
||||
b.classList.toggle('subscribed', subscribed);
|
||||
var icon = b.querySelector('i');
|
||||
var label = b.querySelector('.subscribe-label');
|
||||
if (icon) icon.className = 'bi ' + (subscribed ? 'bi-bell-fill' : 'bi-bell');
|
||||
if (label) label.textContent = subscribed ? 'Subscribed' : 'Subscribe';
|
||||
});
|
||||
if (typeof showToast === 'function') showToast('Could not update subscription. Please try again.', 'error');
|
||||
});
|
||||
});
|
||||
|
||||
document.addEventListener('click', function (e) {
|
||||
var btn = e.target.closest('.like-toggle-btn');
|
||||
if (!btn) return;
|
||||
|
||||
var url = btn.dataset.toggleUrl;
|
||||
var liked = btn.dataset.liked === 'true';
|
||||
|
||||
// Optimistic UI update
|
||||
var allBtns = document.querySelectorAll('.like-toggle-btn[data-video-id="' + btn.dataset.videoId + '"]');
|
||||
allBtns.forEach(function (b) {
|
||||
var icon = b.querySelector('i');
|
||||
var label = b.querySelector('.like-count-label');
|
||||
var nowLiked = !liked;
|
||||
b.dataset.liked = nowLiked ? 'true' : 'false';
|
||||
b.classList.toggle('liked', nowLiked);
|
||||
if (icon) {
|
||||
icon.className = 'bi ' + (nowLiked ? 'bi-hand-thumbs-up-fill' : 'bi-hand-thumbs-up');
|
||||
}
|
||||
if (label) {
|
||||
var current = parseInt(label.textContent.replace(/,/g, '')) || 0;
|
||||
var next = nowLiked ? current + 1 : Math.max(0, current - 1);
|
||||
label.textContent = next > 0 ? next.toLocaleString() : 'Like';
|
||||
}
|
||||
});
|
||||
|
||||
fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'X-CSRF-TOKEN': csrfToken, 'Accept': 'application/json' },
|
||||
})
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (data) {
|
||||
// Reconcile with server truth
|
||||
allBtns.forEach(function (b) {
|
||||
var icon = b.querySelector('i');
|
||||
var label = b.querySelector('.like-count-label');
|
||||
b.dataset.liked = data.liked ? 'true' : 'false';
|
||||
b.classList.toggle('liked', data.liked);
|
||||
if (icon) icon.className = 'bi ' + (data.liked ? 'bi-hand-thumbs-up-fill' : 'bi-hand-thumbs-up');
|
||||
if (label) label.textContent = data.like_count > 0 ? data.like_count.toLocaleString() : 'Like';
|
||||
});
|
||||
})
|
||||
.catch(function () {
|
||||
// Revert optimistic update on failure
|
||||
allBtns.forEach(function (b) {
|
||||
var icon = b.querySelector('i');
|
||||
b.dataset.liked = liked ? 'true' : 'false';
|
||||
b.classList.toggle('liked', liked);
|
||||
if (icon) icon.className = 'bi ' + (liked ? 'bi-hand-thumbs-up-fill' : 'bi-hand-thumbs-up');
|
||||
});
|
||||
if (typeof toast === 'function') toast('Could not update like. Try again.', 'error');
|
||||
});
|
||||
});
|
||||
})();
|
||||
|
||||
// ── Slideshow video generation progress ──────────────────────────────
|
||||
if (!window._slideshowDlInit) {
|
||||
window._slideshowDlInit = true;
|
||||
|
||||
// Inject progress overlay once
|
||||
var _overlay = document.createElement('div');
|
||||
_overlay.id = 'sl-dl-overlay';
|
||||
_overlay.innerHTML = `
|
||||
<div id="sl-dl-box">
|
||||
<div id="sl-dl-icon"><i class="bi bi-film"></i></div>
|
||||
<h5 id="sl-dl-title">Generating Video</h5>
|
||||
<p id="sl-dl-status">Starting...</p>
|
||||
<div id="sl-dl-track">
|
||||
<div id="sl-dl-bar"></div>
|
||||
</div>
|
||||
<div id="sl-dl-pct">0%</div>
|
||||
<p id="sl-dl-hint">This may take a minute for long tracks</p>
|
||||
<button id="sl-dl-cancel" onclick="window._slideshowDlCancel()">Cancel</button>
|
||||
</div>`;
|
||||
document.body.appendChild(_overlay);
|
||||
|
||||
// Styles
|
||||
var _style = document.createElement('style');
|
||||
_style.textContent = `
|
||||
#sl-dl-overlay {
|
||||
display: none; position: fixed; inset: 0; z-index: 99999;
|
||||
background: rgba(0,0,0,.85); align-items: center; justify-content: center;
|
||||
}
|
||||
#sl-dl-overlay.active { display: flex; }
|
||||
#sl-dl-box {
|
||||
background: #1e1e1e; border: 1px solid #333; border-radius: 18px;
|
||||
padding: 36px 32px; width: 90%; max-width: 400px; text-align: center;
|
||||
box-shadow: 0 24px 80px rgba(0,0,0,.7);
|
||||
}
|
||||
#sl-dl-icon { font-size: 44px; color: #e61e1e; margin-bottom: 14px; }
|
||||
#sl-dl-title { color: #fff; font-size: 18px; font-weight: 600; margin: 0 0 6px; }
|
||||
#sl-dl-status { color: #aaa; font-size: 13px; margin: 0 0 20px; min-height: 18px; }
|
||||
#sl-dl-track {
|
||||
background: #2a2a2a; border-radius: 6px; height: 10px;
|
||||
overflow: hidden; margin-bottom: 10px;
|
||||
}
|
||||
#sl-dl-bar {
|
||||
background: linear-gradient(90deg, #e61e1e, #ff4757);
|
||||
height: 100%; width: 0%; border-radius: 6px;
|
||||
transition: width .6s ease;
|
||||
}
|
||||
#sl-dl-pct { color: #e61e1e; font-size: 22px; font-weight: 700; margin-bottom: 4px; }
|
||||
#sl-dl-hint { color: #555; font-size: 11px; margin: 8px 0 16px; }
|
||||
#sl-dl-cancel {
|
||||
background: none; border: 1px solid #444; color: #888;
|
||||
border-radius: 8px; padding: 6px 18px; font-size: 13px; cursor: pointer;
|
||||
}
|
||||
#sl-dl-cancel:hover { border-color: #e61e1e; color: #e61e1e; }
|
||||
`;
|
||||
document.head.appendChild(_style);
|
||||
|
||||
var _pollTimer = null;
|
||||
var _currentKey = null;
|
||||
var _maxPct = 0; // progress only ever moves forward
|
||||
|
||||
window._slideshowDlCancel = function () {
|
||||
clearInterval(_pollTimer);
|
||||
_pollTimer = null;
|
||||
_currentKey = null;
|
||||
_maxPct = 0;
|
||||
document.getElementById('sl-dl-overlay').classList.remove('active');
|
||||
};
|
||||
|
||||
function _setProgress(pct, status) {
|
||||
// Never decrease the bar — only move forward (unless resetting to 0 on error/cancel)
|
||||
pct = Math.max(pct, _maxPct);
|
||||
_maxPct = pct;
|
||||
document.getElementById('sl-dl-bar').style.width = pct + '%';
|
||||
document.getElementById('sl-dl-pct').textContent = pct + '%';
|
||||
document.getElementById('sl-dl-status').textContent = status;
|
||||
}
|
||||
|
||||
function _setError(msg) {
|
||||
_maxPct = 0;
|
||||
document.getElementById('sl-dl-bar').style.width = '0%';
|
||||
document.getElementById('sl-dl-bar').style.background = '#e61e1e';
|
||||
document.getElementById('sl-dl-pct').textContent = '✕';
|
||||
document.getElementById('sl-dl-status').textContent = msg || 'Generation failed. Please try again.';
|
||||
document.getElementById('sl-dl-cancel').textContent = 'Close';
|
||||
}
|
||||
|
||||
window.startSlideshowDownload = function (routeKey) {
|
||||
if (_currentKey && _currentKey !== routeKey) {
|
||||
// Different video — reset
|
||||
clearInterval(_pollTimer);
|
||||
_pollTimer = null;
|
||||
}
|
||||
_currentKey = routeKey;
|
||||
_maxPct = 0;
|
||||
document.getElementById('sl-dl-bar').style.background = ''; // restore gradient
|
||||
document.getElementById('sl-dl-cancel').textContent = 'Cancel';
|
||||
_setProgress(0, 'Starting...');
|
||||
document.getElementById('sl-dl-overlay').classList.add('active');
|
||||
document.getElementById('sl-dl-cancel').style.display = '';
|
||||
|
||||
// Use the global csrf variable set by the layout, with meta tag as fallback
|
||||
var csrfMeta = document.querySelector('meta[name="csrf-token"]');
|
||||
var token = (typeof csrf !== 'undefined' ? csrf : '') || (csrfMeta ? csrfMeta.getAttribute('content') : '');
|
||||
|
||||
fetch('/videos/' + routeKey + '/slideshow/generate', {
|
||||
method: 'POST',
|
||||
headers: { 'X-CSRF-TOKEN': token, 'Accept': 'application/json' }
|
||||
})
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (data) {
|
||||
if (data.status === 'ready') {
|
||||
_maxPct = 100;
|
||||
document.getElementById('sl-dl-bar').style.width = '100%';
|
||||
document.getElementById('sl-dl-pct').textContent = '100%';
|
||||
document.getElementById('sl-dl-status').textContent = 'Ready! Starting download...';
|
||||
document.getElementById('sl-dl-cancel').style.display = 'none';
|
||||
setTimeout(function () {
|
||||
window._slideshowDlCancel();
|
||||
window.location.href = '/videos/' + routeKey + '/download';
|
||||
}, 600);
|
||||
return;
|
||||
}
|
||||
if (data.error) {
|
||||
_setError('Error: ' + data.error);
|
||||
return;
|
||||
}
|
||||
|
||||
var duration = data.duration || 0;
|
||||
_setProgress(2, 'Generating video...');
|
||||
|
||||
_pollTimer = setInterval(function () {
|
||||
fetch('/videos/' + routeKey + '/slideshow/progress?duration=' + duration, {
|
||||
headers: { 'Accept': 'application/json' }
|
||||
})
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (prog) {
|
||||
if (prog.status === 'error') {
|
||||
clearInterval(_pollTimer);
|
||||
_pollTimer = null;
|
||||
_setError(prog.message || null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (prog.status === 'ready') {
|
||||
clearInterval(_pollTimer);
|
||||
_pollTimer = null;
|
||||
_maxPct = 100;
|
||||
document.getElementById('sl-dl-bar').style.width = '100%';
|
||||
document.getElementById('sl-dl-pct').textContent = '100%';
|
||||
document.getElementById('sl-dl-status').textContent = 'Ready! Starting download...';
|
||||
document.getElementById('sl-dl-cancel').style.display = 'none';
|
||||
setTimeout(function () {
|
||||
window._slideshowDlCancel();
|
||||
window.location.href = '/videos/' + routeKey + '/download';
|
||||
}, 600);
|
||||
return;
|
||||
}
|
||||
|
||||
// 'waiting' means the process just started — keep the current bar position
|
||||
if (prog.status === 'waiting') return;
|
||||
|
||||
var pct = prog.percent || 0;
|
||||
var remaining = '';
|
||||
if (duration > 0 && pct > 2) {
|
||||
var secs = Math.round(duration * (100 - pct) / pct);
|
||||
remaining = secs > 60
|
||||
? ' (~' + Math.ceil(secs / 60) + ' min left)'
|
||||
: ' (~' + secs + 's left)';
|
||||
}
|
||||
_setProgress(pct, 'Generating video...' + remaining);
|
||||
})
|
||||
.catch(function () { /* ignore transient poll errors */ });
|
||||
}, 1500);
|
||||
})
|
||||
.catch(function () {
|
||||
_setProgress(0, 'Failed to start. Please try again.');
|
||||
});
|
||||
};
|
||||
}
|
||||
</script>
|
||||
Reference in new issue
Block a user