WIP: storage-fix-local-nas work before playlist controls feature
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
This commit is contained in:
1 parent
6b3ab5b65e
commit
c160242dbc
46 files changed
+5365
-815
No files matched your search
@@ -1,7 +1,24 @@
|
||||
@props(['playlist', 'showTypeBadge' => false])
|
||||
@props(['playlist'])
|
||||
|
||||
@once
|
||||
<style>
|
||||
.pl-count-badge {
|
||||
position: absolute;
|
||||
inset: 0 0 0 auto;
|
||||
width: 72px;
|
||||
background: rgba(0,0,0,.78);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
pointer-events: none;
|
||||
z-index: 3;
|
||||
}
|
||||
.pl-count-badge i { font-size: 20px; }
|
||||
.pl-visibility-badge {
|
||||
position: absolute;
|
||||
bottom: 8px;
|
||||
@@ -18,6 +35,26 @@
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .4px;
|
||||
pointer-events: none;
|
||||
z-index: 3;
|
||||
}
|
||||
.pl-type-badge {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 8px;
|
||||
background: rgba(0,0,0,.82);
|
||||
color: #a78bfa;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
padding: 3px 7px;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .4px;
|
||||
pointer-events: none;
|
||||
white-space: nowrap;
|
||||
z-index: 3;
|
||||
}
|
||||
</style>
|
||||
@endonce
|
||||
@@ -27,16 +64,14 @@
|
||||
<div class="yt-video-card">
|
||||
<a href="{{ route('playlists.show', $pl->id) }}">
|
||||
<div class="yt-video-thumb">
|
||||
<img src="{{ $pl->thumbnail_url }}" alt="{{ $pl->name }}" loading="lazy">
|
||||
<img src="{{ $pl->thumbnail_url }}" alt="{{ $pl->name }}" loading="lazy" decoding="async" onload="this.classList.add('loaded');this.closest('.yt-video-thumb').classList.add('loaded')">
|
||||
<div class="pl-count-badge">
|
||||
<i class="bi bi-collection-play-fill"></i>
|
||||
{{ $pl->videos_count }}
|
||||
</div>
|
||||
@if($showTypeBadge)
|
||||
<span class="feed-pl-type-badge">
|
||||
<i class="bi bi-collection-play-fill"></i> PLAYLIST
|
||||
</span>
|
||||
@endif
|
||||
<span class="pl-type-badge">
|
||||
<i class="bi bi-collection-play-fill"></i> Playlist
|
||||
</span>
|
||||
@if($pl->visibility === 'private')
|
||||
<span class="pl-visibility-badge"><i class="bi bi-lock-fill"></i> Private</span>
|
||||
@elseif($pl->visibility === 'unlisted')
|
||||
|
||||
@@ -4,18 +4,33 @@
|
||||
.action-btn {
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 8px 14px;
|
||||
padding: 0 14px;
|
||||
height: 36px;
|
||||
font-size: 0.82rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
color: var(--text-primary);
|
||||
display: flex;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
transition: all 0.2s ease;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
line-height: 1;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.action-btn i,
|
||||
.action-btn svg {
|
||||
font-size: 14px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
flex-shrink: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
@@ -113,6 +128,11 @@
|
||||
<i class="bi bi-pencil"></i>
|
||||
<span>Edit</span>
|
||||
</button>
|
||||
<button class="action-btn desktop-action" onclick="showDeleteModal('{{ $video->getRouteKey() }}', {{ json_encode($video->title) }})"
|
||||
style="color:#ef4444;border-color:rgba(239,68,68,.35);">
|
||||
<i class="bi bi-trash"></i>
|
||||
<span>Delete</span>
|
||||
</button>
|
||||
@elseif (Auth::id() !== $video->user_id)
|
||||
@php $isSubscribed = Auth::user()->isSubscribedTo($video->user); @endphp
|
||||
<button type="button"
|
||||
@@ -149,16 +169,13 @@
|
||||
@if ($video->isShareable())
|
||||
<button class="action-btn desktop-action"
|
||||
onclick="openShareModal('{{ $video->share_url }}', '{{ addslashes($video->title) }}', '{{ route('videos.recordShare', $video) }}')">
|
||||
<i class="bi bi-share"></i> Share
|
||||
<i class="bi bi-share"></i><span>Share</span>
|
||||
</button>
|
||||
@endif
|
||||
|
||||
<!-- Save to Playlist Button -->
|
||||
<button class="action-btn desktop-action" onclick="openAddToPlaylistModal({{ $video->id }})">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
||||
stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"></path>
|
||||
</svg>
|
||||
<i class="bi bi-bookmark"></i>
|
||||
<span>Save</span>
|
||||
</button>
|
||||
|
||||
@@ -590,4 +607,110 @@ if (!window._slideshowDlInit) {
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
// ── Video delete dialog ──────────────────────────────────────────────
|
||||
@auth
|
||||
@if(Auth::id() === $video->user_id)
|
||||
(function () {
|
||||
var _videoDeleteUrl = '{{ route('videos.destroy', $video) }}';
|
||||
var _videoCsrf = '{{ csrf_token() }}';
|
||||
var _owner2fa = {{ (Auth::user()->two_factor_enabled && Auth::user()->two_factor_secret) ? 'true' : 'false' }};
|
||||
var _dlgId = 'vaDlg_{{ $video->id }}';
|
||||
var _dlgHtml = '<div id="' + _dlgId + '" style="display:none;position:fixed;inset:0;z-index:9999;background:rgba(0,0,0,.75);backdrop-filter:blur(4px);align-items:center;justify-content:center;padding:16px;" onclick="if(event.target===this)_closeVaDlg()">'
|
||||
+ '<div style="background:var(--bg-secondary);border:1px solid var(--border-color);border-radius:16px;width:100%;max-width:420px;overflow:hidden;box-shadow:0 24px 60px rgba(0,0,0,.6);">'
|
||||
+ '<div style="padding:20px 24px 16px;border-bottom:1px solid var(--border-color);display:flex;align-items:center;justify-content:space-between;">'
|
||||
+ '<div style="display:flex;align-items:center;gap:10px;font-size:16px;font-weight:600;color:#ef4444;"><i class="bi bi-trash"></i> Delete Video</div>'
|
||||
+ '<button onclick="_closeVaDlg()" style="background:none;border:none;color:var(--text-secondary);cursor:pointer;font-size:18px;line-height:1;padding:4px;"><i class="bi bi-x-lg"></i></button>'
|
||||
+ '</div>'
|
||||
+ '<div style="padding:20px 24px;">'
|
||||
+ '<p id="' + _dlgId + '_title" style="margin:0 0 12px;font-size:14px;color:var(--text-primary);"></p>'
|
||||
+ '<div style="background:rgba(239,68,68,.1);border:1px solid rgba(239,68,68,.25);border-radius:8px;padding:10px 14px;font-size:13px;color:#fca5a5;display:flex;gap:8px;align-items:flex-start;">'
|
||||
+ '<i class="bi bi-exclamation-triangle-fill" style="flex-shrink:0;margin-top:1px;"></i>'
|
||||
+ '<span>All views, likes, comments and HLS files will be deleted. This cannot be undone.</span>'
|
||||
+ '</div>'
|
||||
+ (_owner2fa ? '<div style="margin-top:16px;"><label style="font-size:13px;color:var(--text-secondary);display:flex;align-items:center;gap:6px;margin-bottom:8px;"><i class="bi bi-shield-lock-fill" style="color:#a78bfa;"></i>Enter your <strong style=\'color:var(--text-primary);\'>2FA code</strong> to confirm</label>'
|
||||
+ '<input type="text" id="' + _dlgId + '_otp" inputmode="numeric" pattern="[0-9]*" maxlength="6" autocomplete="one-time-code" placeholder="000000"'
|
||||
+ ' style="width:100%;background:var(--bg-primary);border:1px solid var(--border-color);border-radius:8px;padding:10px 14px;color:#fff;font-size:22px;letter-spacing:.3em;text-align:center;box-sizing:border-box;"></div>' : '')
|
||||
+ '<div id="' + _dlgId + '_err" style="display:none;margin-top:12px;padding:8px 12px;background:rgba(239,68,68,.12);border:1px solid rgba(239,68,68,.3);border-radius:6px;color:#f87171;font-size:13px;"></div>'
|
||||
+ '</div>'
|
||||
+ '<div style="padding:12px 24px 20px;display:flex;gap:10px;justify-content:flex-end;">'
|
||||
+ '<button onclick="_closeVaDlg()" class="action-btn">Cancel</button>'
|
||||
+ '<button id="' + _dlgId + '_btn" onclick="_confirmVaDlg()" class="action-btn" style="background:#ef4444;color:#fff;border-color:#ef4444;"><i class="bi bi-trash"></i> Delete</button>'
|
||||
+ '</div>'
|
||||
+ '</div></div>';
|
||||
|
||||
var _dlgInserted = false;
|
||||
function _ensureDlg() {
|
||||
if (_dlgInserted) return;
|
||||
document.body.insertAdjacentHTML('beforeend', _dlgHtml);
|
||||
_dlgInserted = true;
|
||||
}
|
||||
|
||||
window.showDeleteModal = function (routeKey, title) {
|
||||
_ensureDlg();
|
||||
var dlg = document.getElementById(_dlgId);
|
||||
document.getElementById(_dlgId + '_title').textContent = 'You are about to permanently delete "' + title + '".';
|
||||
document.getElementById(_dlgId + '_err').style.display = 'none';
|
||||
if (_owner2fa) { document.getElementById(_dlgId + '_otp').value = ''; }
|
||||
dlg.style.display = 'flex';
|
||||
dlg.style.alignItems = 'center';
|
||||
dlg.style.justifyContent = 'center';
|
||||
if (_owner2fa) setTimeout(function () { document.getElementById(_dlgId + '_otp').focus(); }, 100);
|
||||
document.addEventListener('keydown', _escHandler);
|
||||
};
|
||||
|
||||
window._closeVaDlg = function () {
|
||||
var dlg = document.getElementById(_dlgId);
|
||||
if (dlg) dlg.style.display = 'none';
|
||||
document.removeEventListener('keydown', _escHandler);
|
||||
};
|
||||
|
||||
function _escHandler(e) { if (e.key === 'Escape') _closeVaDlg(); }
|
||||
|
||||
window._confirmVaDlg = function () {
|
||||
var btn = document.getElementById(_dlgId + '_btn');
|
||||
var errEl = document.getElementById(_dlgId + '_err');
|
||||
var otp = _owner2fa ? document.getElementById(_dlgId + '_otp').value.replace(/\s/g, '') : '';
|
||||
|
||||
if (_owner2fa && otp.length !== 6) {
|
||||
errEl.textContent = 'Please enter your 6-digit 2FA code.';
|
||||
errEl.style.display = 'block';
|
||||
document.getElementById(_dlgId + '_otp').focus();
|
||||
return;
|
||||
}
|
||||
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<i class="bi bi-hourglass-split"></i> Deleting…';
|
||||
errEl.style.display = 'none';
|
||||
|
||||
var body = new URLSearchParams({ '_token': _videoCsrf, '_method': 'DELETE' });
|
||||
if (_owner2fa) body.append('otp_code', otp);
|
||||
|
||||
fetch(_videoDeleteUrl, {
|
||||
method: 'POST',
|
||||
headers: { 'Accept': 'application/json', 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: body.toString()
|
||||
})
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (data) {
|
||||
if (data.success) {
|
||||
window.location.href = '/';
|
||||
} else {
|
||||
errEl.textContent = data.message || 'Delete failed.';
|
||||
errEl.style.display = 'block';
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="bi bi-trash"></i> Delete';
|
||||
if (_owner2fa) { document.getElementById(_dlgId + '_otp').value = ''; document.getElementById(_dlgId + '_otp').focus(); }
|
||||
}
|
||||
})
|
||||
.catch(function () {
|
||||
errEl.textContent = 'An error occurred. Please try again.';
|
||||
errEl.style.display = 'block';
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="bi bi-trash"></i> Delete';
|
||||
});
|
||||
};
|
||||
})();
|
||||
@endif
|
||||
@endauth
|
||||
</script>
|
||||
@@ -3,7 +3,7 @@
|
||||
@php
|
||||
$videoUrl = $video ? asset('storage/videos/' . $video->filename) : null;
|
||||
$thumbnailUrl = $video && $video->thumbnail
|
||||
? asset('storage/thumbnails/' . $video->thumbnail)
|
||||
? route('media.thumbnail', $video->thumbnail)
|
||||
: ($video ? 'https://picsum.photos/seed/' . $video->id . '/640/360' : 'https://picsum.photos/seed/random/640/360');
|
||||
|
||||
$typeIcon = $video ? match($video->type) {
|
||||
@@ -29,7 +29,7 @@ $sizeClasses = match($size) {
|
||||
<a href="{{ $video ? route('videos.show', $video) : '#' }}">
|
||||
<div class="yt-video-thumb" onmouseenter="playVideo(this)" onmouseleave="stopVideo(this)"
|
||||
data-audio="{{ $video && $video->isAudioOnly() ? 'true' : 'false' }}">
|
||||
<img src="{{ $thumbnailUrl }}" alt="{{ $video->title ?? 'Video' }}">
|
||||
<img src="{{ $thumbnailUrl }}" alt="{{ $video->title ?? 'Video' }}" loading="lazy" decoding="async" onload="this.classList.add('loaded');this.closest('.yt-video-thumb').classList.add('loaded')">
|
||||
@if($videoUrl)
|
||||
<video preload="none">
|
||||
<source src="{{ $videoUrl }}" type="{{ $video->mime_type ?? 'video/mp4' }}">
|
||||
@@ -64,7 +64,7 @@ $sizeClasses = match($size) {
|
||||
<a href="{{ $video && $video->user ? route('channel', $video->user->channel) : '#' }}"
|
||||
class="yt-channel-icon" onclick="event.stopPropagation();">
|
||||
@if($video && $video->user && $video->user->avatar_url)
|
||||
<img src="{{ $video->user->avatar_url }}" alt="{{ $video->user->name }}" style="width: 100%; height: 100%; object-fit: cover; border-radius: 50%;">
|
||||
<img src="{{ $video->user->avatar_url }}" alt="{{ $video->user->name }}" loading="lazy" decoding="async" onload="this.classList.add('loaded')" style="width: 100%; height: 100%; object-fit: cover; border-radius: 50%; opacity: 0; transition: opacity 0.25s ease;">
|
||||
@endif
|
||||
</a>
|
||||
<div class="yt-video-details">
|
||||
@@ -275,6 +275,28 @@ $sizeClasses = match($size) {
|
||||
background: #1a1a1a;
|
||||
}
|
||||
|
||||
/* Skeleton shimmer while thumbnail loads */
|
||||
.yt-video-card .yt-video-thumb::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(90deg, #1a1a1a 25%, #2a2a2a 50%, #1a1a1a 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: thumb-shimmer 1.4s ease infinite;
|
||||
z-index: 0;
|
||||
border-radius: inherit;
|
||||
}
|
||||
|
||||
@keyframes thumb-shimmer {
|
||||
0% { background-position: 200% 0; }
|
||||
100% { background-position: -200% 0; }
|
||||
}
|
||||
|
||||
.yt-video-card .yt-video-thumb.loaded::before {
|
||||
animation: none;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.yt-video-card .yt-video-thumb img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@@ -282,9 +304,16 @@ $sizeClasses = match($size) {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
transition: transform 0.2s ease;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease, transform 0.2s ease;
|
||||
z-index: 1;
|
||||
}
|
||||
.yt-video-card:hover .yt-video-thumb img {
|
||||
|
||||
.yt-video-card .yt-video-thumb img.loaded {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.yt-video-card:hover .yt-video-thumb img.loaded {
|
||||
transform: scale(1.03);
|
||||
}
|
||||
|
||||
@@ -298,6 +327,7 @@ $sizeClasses = match($size) {
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
background: #000;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.yt-video-card .yt-video-thumb video.active {
|
||||
@@ -315,6 +345,7 @@ $sizeClasses = match($size) {
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
pointer-events: none;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.yt-video-thumb.audio-playing .audio-preview-overlay {
|
||||
@@ -360,6 +391,7 @@ $sizeClasses = match($size) {
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.yt-video-card .yt-shorts-badge {
|
||||
@@ -377,6 +409,7 @@ $sizeClasses = match($size) {
|
||||
gap: 4px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.yt-video-card .yt-shorts-badge i {
|
||||
@@ -395,6 +428,7 @@ $sizeClasses = match($size) {
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
pointer-events: none;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.yt-video-card .yt-visibility-private {
|
||||
@@ -423,6 +457,10 @@ $sizeClasses = match($size) {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.yt-video-card .yt-channel-icon img.loaded {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.yt-video-card .yt-video-details {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
@@ -79,6 +79,18 @@
|
||||
.ins-demo-pct { font-size:12px; font-weight:700; color:var(--text-secondary); min-width:34px; text-align:right; }
|
||||
.ins-demo-cnt { font-size:11px; color:var(--text-secondary); min-width:30px; text-align:right; }
|
||||
|
||||
/* ── Two-column grid (collapses to 1 col on mobile) ─── */
|
||||
.ins-two-col { display:grid; grid-template-columns:1fr 1fr; gap:20px; }
|
||||
@media(max-width:600px){ .ins-two-col { grid-template-columns:1fr; } }
|
||||
|
||||
/* ── Who Liked rows ──────────────────────────────────── */
|
||||
.ins-liker-row { display:flex; align-items:center; gap:10px; padding:7px 6px; border-bottom:1px solid rgba(255,255,255,.04); cursor:pointer; border-radius:8px; transition:background .12s; }
|
||||
.ins-liker-row:last-child { border-bottom:none; }
|
||||
.ins-liker-row:hover { background:rgba(255,255,255,.05); }
|
||||
.ins-liker-avatar { width:34px; height:34px; border-radius:50%; object-fit:cover; flex-shrink:0; background:rgba(255,255,255,.08); }
|
||||
.ins-liker-name { flex:1; font-size:13px; font-weight:600; color:var(--text-primary); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
||||
.ins-liker-time { font-size:11px; color:var(--text-secondary); flex-shrink:0; }
|
||||
|
||||
/* ── Skeleton ─────────────────────────────────────────── */
|
||||
.ins-skeleton { display:flex; flex-direction:column; gap:12px; padding:4px 0; }
|
||||
.ins-skel-row { height:14px; border-radius:6px; background:rgba(255,255,255,.06); animation:skelPulse 1.4s ease-in-out infinite; }
|
||||
@@ -367,7 +379,7 @@ function renderInsights(d) {
|
||||
<div class="ins-dl-header">
|
||||
<div class="ins-section-title" style="margin:0;"><i class="bi bi-people-fill"></i> Viewers — ${_fmt(d.unique_viewers)} registered · ${_fmt(d.guest_views||0)} guest</div>
|
||||
</div>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;">
|
||||
<div class="ins-two-col">
|
||||
<div>
|
||||
<div style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:var(--text-secondary);margin-bottom:8px;">Top Viewers</div>
|
||||
${topViewerRows || '<p style="font-size:13px;color:var(--text-secondary);margin:0;">No registered viewers yet.</p>'}
|
||||
@@ -379,6 +391,30 @@ function renderInsights(d) {
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
// ── Who Liked section ───────────────────────────────
|
||||
let likersHtml = '';
|
||||
if (d.likes > 0 && d.likers && d.likers.length) {
|
||||
const likerRows = d.likers.map(u => `
|
||||
<div class="ins-liker-row" onclick="window.location.href='/channel/${u.channel||u.id}'" title="View ${u.name}'s profile">
|
||||
<img src="${u.avatar}" alt="${u.name}" class="ins-liker-avatar">
|
||||
<div class="ins-liker-name">${u.name}</div>
|
||||
<div class="ins-liker-time">${_ago(u.liked_at)}</div>
|
||||
<i class="bi bi-chevron-right" style="font-size:10px;color:var(--text-secondary);flex-shrink:0;margin-left:4px;"></i>
|
||||
</div>`).join('');
|
||||
|
||||
likersHtml = `
|
||||
<div style="margin-top:20px;border-top:1px solid var(--border-color);padding-top:18px;">
|
||||
<div class="ins-section-title"><i class="bi bi-heart-fill" style="color:#ef4444;"></i> Liked by — ${_fmt(d.likes)} ${d.likes===1?'person':'people'}</div>
|
||||
${likerRows}
|
||||
</div>`;
|
||||
} else if (d.likes === 0) {
|
||||
likersHtml = `
|
||||
<div style="margin-top:20px;border-top:1px solid var(--border-color);padding-top:18px;">
|
||||
<div class="ins-section-title"><i class="bi bi-heart" style="color:var(--text-secondary);"></i> Likes</div>
|
||||
<p style="font-size:13px;color:var(--text-secondary);margin:0;">No likes yet.</p>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// ── Downloads section ───────────────────────────────
|
||||
let dlHtml = '';
|
||||
if (d.downloads===0) {
|
||||
@@ -422,7 +458,7 @@ function renderInsights(d) {
|
||||
<div class="ins-section-title" style="margin:0;"><i class="bi bi-download"></i> Downloads — ${_fmt(d.downloads)} total</div>
|
||||
<div class="ins-dl-type-pills">${pills}</div>
|
||||
</div>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;">
|
||||
<div class="ins-two-col">
|
||||
<div>
|
||||
<div style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:var(--text-secondary);margin-bottom:8px;">
|
||||
Top Downloaders <span style="font-weight:400;opacity:.5;">(tap to see history)</span>
|
||||
@@ -523,7 +559,7 @@ function renderInsights(d) {
|
||||
<div>
|
||||
<div class="ins-section-title"><i class="bi bi-globe2"></i> Audience by Country <span style="font-size:10px;opacity:.5;font-weight:400;text-transform:none;">(tap for viewer breakdown)</span></div>
|
||||
${countriesHtml}
|
||||
</div></div>` + viewersHtml + dlHtml + shareHtml + demographicsHtml;
|
||||
</div></div>` + viewersHtml + likersHtml + dlHtml + shareHtml + demographicsHtml;
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════
|
||||
|
||||
Reference in new issue
Block a user