ghassan a4384113c2 Audio songs: one-folder storage, version-aware download/share, GPU-checked renders
Storage structure
- All audio tracks (primary + per-language) now live in one folder per song with
  unique lowercase names ({slug}-{lang}-{id}); no more tracks/ subfolder.
- Generated renders (download video + HLS) moved into the song's local-only cache/
  subfolder, separated from source files (never synced to NAS, safe to wipe).
- tracks:reorganize artisan command (dry-run default) consolidates legacy files,
  updates DB paths, and deletes orphans + empty folders.
- CLAUDE.md documents the canonical layout as a global rule (identical local + NAS).

Version-aware download & share
- Download MP3/Video and Share now act on the version being played; ?track={id}
  is carried through share links and auto-selects audio + title + flag + about +
  OG/meta on open.

GPU + visualizer
- Setting::gpuUsable() runs a cached health probe (nvidia-smi + nvenc smoke test,
  256x144) before sending any encode to the GPU; falls back to CPU otherwise.
- Visualizer "Download Video" bakes in equal-width, cover-coloured, translucent
  frequency bars; loop-filter rebuild makes generation ~25x faster.

Image cropper
- result-callback mode + per-song cover-slide cropper in upload/edit (modal + mobile).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 14:03:43 +03:00

180 lines
8.5 KiB
PHP

<!-- Share Modal -->
<div class="modal fade" id="shareModal" tabindex="-1" aria-labelledby="shareModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<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
</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 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 action-btn action-btn-primary">
<i class="bi bi-clipboard"></i> <span>Copy</span>
</button>
</div>
<div id="copySuccess" class="copy-success" style="display: none; margin-top: 12px; color: #4caf50; font-size: 14px; text-align: center;">
<i class="bi bi-check-circle-fill me-1"></i> Link copied to clipboard!
</div>
<div style="margin-top: 24px; padding-top: 16px; border-top: 1px solid var(--border-color);">
<p style="color: var(--text-secondary); font-size: 13px; margin-bottom: 12px;">Share on social media:</p>
<div style="display: flex; gap: 12px;">
<a href="#" id="shareFacebook" class="social-share-btn" target="_blank"
style="display: flex; align-items: center; justify-content: center; width: 40px; height: 40px; border-radius: 50%; background: #1877f2; color: white; text-decoration: none;">
<i class="bi bi-facebook"></i>
</a>
<a href="#" id="shareTwitter" class="social-share-btn" target="_blank"
style="display: flex; align-items: center; justify-content: center; width: 40px; height: 40px; border-radius: 50%; background: #1da1f2; color: white; text-decoration: none;">
<i class="bi bi-twitter-x"></i>
</a>
<a href="#" id="shareWhatsApp" class="social-share-btn" target="_blank"
style="display: flex; align-items: center; justify-content: center; width: 40px; height: 40px; border-radius: 50%; background: #25d366; color: white; text-decoration: none;">
<i class="bi bi-whatsapp"></i>
</a>
<a href="#" id="shareTelegram" class="social-share-btn" target="_blank"
style="display: flex; align-items: center; justify-content: center; width: 40px; height: 40px; border-radius: 50%; background: #0088cc; color: white; text-decoration: none;">
<i class="bi bi-telegram"></i>
</a>
</div>
</div>
</div>
</div>
</div>
</div>
<style>
.social-share-btn {
transition: transform 0.2s, opacity 0.2s;
}
.social-share-btn:hover {
transform: scale(1.1);
opacity: 0.9;
}
.btn-copy.copied {
background: #4caf50 !important;
border-color: #4caf50 !important;
}
</style>
<script>
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;
// Preserve the version selector (?track=) from the requested URL — the server's tracked
// share link replaces shareUrl below, so we re-attach it afterwards.
var trackParam = '';
try { trackParam = (new URL(videoUrl, window.location.origin)).searchParams.get('track') || ''; } catch (e) {}
// 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 */ }
}
// Re-attach the version selector so the recipient opens the right language.
if (trackParam) {
shareUrl += (shareUrl.indexOf('?') === -1 ? '?' : '&') + 'track=' + encodeURIComponent(trackParam);
}
// 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();
}
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');
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>