251 lines
14 KiB
PHP
251 lines
14 KiB
PHP
|
|
<?php
|
||
|
|
declare(strict_types=1);
|
||
|
|
|
||
|
|
$pageTitle = $title ?? 'Release';
|
||
|
|
$release = $release ?? null;
|
||
|
|
$tracks = $tracks ?? [];
|
||
|
|
$releaseCover = (string)($release['cover_url'] ?? '');
|
||
|
|
ob_start();
|
||
|
|
?>
|
||
|
|
<section class="card" style="display:grid; gap:18px; padding-bottom:110px;">
|
||
|
|
<div class="badge">Release</div>
|
||
|
|
<?php if (!$release): ?>
|
||
|
|
<h1 style="margin:0; font-size:28px;">Release not found</h1>
|
||
|
|
<p style="color:var(--muted);">This release is unavailable.</p>
|
||
|
|
<?php else: ?>
|
||
|
|
<div class="release-wrap" style="display:grid; gap:18px;">
|
||
|
|
<div class="release-hero" style="display:grid; grid-template-columns:minmax(0,1fr) 360px; gap:22px; align-items:start;">
|
||
|
|
<div class="release-meta" style="display:grid; gap:14px;">
|
||
|
|
<h1 style="margin:0; font-size:46px; line-height:1.06;"><?= htmlspecialchars((string)$release['title'], ENT_QUOTES, 'UTF-8') ?></h1>
|
||
|
|
<div style="display:grid; grid-template-columns:repeat(2, minmax(0, 1fr)); gap:10px; max-width:640px;">
|
||
|
|
<?php if (!empty($release['catalog_no'])): ?>
|
||
|
|
<div style="padding:10px 12px; border-radius:12px; border:1px solid rgba(255,255,255,0.08); background:rgba(0,0,0,0.22);">
|
||
|
|
<div class="badge" style="font-size:9px;">Catalog</div>
|
||
|
|
<div style="margin-top:6px; font-size:14px;"><?= htmlspecialchars((string)$release['catalog_no'], ENT_QUOTES, 'UTF-8') ?></div>
|
||
|
|
</div>
|
||
|
|
<?php endif; ?>
|
||
|
|
<?php if (!empty($release['release_date'])): ?>
|
||
|
|
<div style="padding:10px 12px; border-radius:12px; border:1px solid rgba(255,255,255,0.08); background:rgba(0,0,0,0.22);">
|
||
|
|
<div class="badge" style="font-size:9px;">Release Date</div>
|
||
|
|
<div style="margin-top:6px; font-size:14px;"><?= htmlspecialchars((string)$release['release_date'], ENT_QUOTES, 'UTF-8') ?></div>
|
||
|
|
</div>
|
||
|
|
<?php endif; ?>
|
||
|
|
</div>
|
||
|
|
<?php if (!empty($release['description'])): ?>
|
||
|
|
<div style="color:var(--muted); line-height:1.75; max-width:760px;">
|
||
|
|
<?= nl2br(htmlspecialchars((string)$release['description'], ENT_QUOTES, 'UTF-8')) ?>
|
||
|
|
</div>
|
||
|
|
<?php endif; ?>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="release-cover-box" style="border-radius:20px; overflow:hidden; background:rgba(255,255,255,0.05); border:1px solid rgba(255,255,255,0.1); aspect-ratio:1/1;">
|
||
|
|
<?php if ($releaseCover !== ''): ?>
|
||
|
|
<img id="releaseCoverMain" src="<?= htmlspecialchars($releaseCover, ENT_QUOTES, 'UTF-8') ?>" alt="" style="width:100%; height:100%; object-fit:cover;">
|
||
|
|
<?php else: ?>
|
||
|
|
<div id="releaseCoverMain" style="height:100%; display:grid; place-items:center; color:var(--muted); letter-spacing:0.3em; font-size:12px;">AUDIOCORE</div>
|
||
|
|
<?php endif; ?>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<?php if (!empty($release['credits'])): ?>
|
||
|
|
<div style="padding:14px 16px; border-radius:16px; border:1px solid rgba(255,255,255,0.08); background:rgba(0,0,0,0.22);">
|
||
|
|
<div class="badge">Credits</div>
|
||
|
|
<div style="margin-top:8px; color:var(--muted); line-height:1.7;">
|
||
|
|
<?= nl2br(htmlspecialchars((string)$release['credits'], ENT_QUOTES, 'UTF-8')) ?>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<?php endif; ?>
|
||
|
|
|
||
|
|
<?php if ($tracks): ?>
|
||
|
|
<div style="padding:16px; border-radius:18px; border:1px solid rgba(255,255,255,0.08); background:rgba(0,0,0,0.22); display:grid; gap:10px;">
|
||
|
|
<div style="display:flex; align-items:center; justify-content:space-between; gap:12px;">
|
||
|
|
<div class="badge">Tracklist</div>
|
||
|
|
<div id="releasePlayerNow" style="font-size:12px; color:var(--muted);">Select a track to play sample</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div style="display:grid; gap:8px;">
|
||
|
|
<?php foreach ($tracks as $track): ?>
|
||
|
|
<?php
|
||
|
|
$sample = (string)($track['sample_url'] ?? '');
|
||
|
|
$trackTitle = (string)($track['title'] ?? 'Track');
|
||
|
|
$mix = (string)($track['mix_name'] ?? '');
|
||
|
|
$fullTitle = $mix !== '' ? ($trackTitle . ' (' . $mix . ')') : $trackTitle;
|
||
|
|
?>
|
||
|
|
<div class="track-row" style="display:grid; grid-template-columns:92px minmax(0,1fr) auto; gap:12px; align-items:center; padding:10px 12px; border-radius:12px; border:1px solid rgba(255,255,255,0.08); background:rgba(0,0,0,0.26);">
|
||
|
|
<button type="button"
|
||
|
|
class="track-play-btn"
|
||
|
|
data-src="<?= htmlspecialchars($sample, ENT_QUOTES, 'UTF-8') ?>"
|
||
|
|
data-title="<?= htmlspecialchars($fullTitle, ENT_QUOTES, 'UTF-8') ?>"
|
||
|
|
data-cover="<?= htmlspecialchars($releaseCover, ENT_QUOTES, 'UTF-8') ?>"
|
||
|
|
<?= $sample === '' ? 'disabled' : '' ?>>
|
||
|
|
<i class="fa-solid fa-play"></i> <span>Play</span>
|
||
|
|
</button>
|
||
|
|
<div style="min-width:0;">
|
||
|
|
<div style="font-weight:600; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;">
|
||
|
|
<?= htmlspecialchars($trackTitle, ENT_QUOTES, 'UTF-8') ?>
|
||
|
|
<?php if ($mix !== ''): ?>
|
||
|
|
<span style="color:var(--muted); font-weight:400;">(<?= htmlspecialchars($mix, ENT_QUOTES, 'UTF-8') ?>)</span>
|
||
|
|
<?php endif; ?>
|
||
|
|
</div>
|
||
|
|
<div style="font-size:12px; color:var(--muted); margin-top:4px;">
|
||
|
|
<?= (int)($track['track_no'] ?? 0) > 0 ? '#' . (int)$track['track_no'] : 'Track' ?>
|
||
|
|
<?= !empty($track['duration']) ? ' - ' . htmlspecialchars((string)$track['duration'], ENT_QUOTES, 'UTF-8') : '' ?>
|
||
|
|
<?= !empty($track['bpm']) ? ' - ' . htmlspecialchars((string)$track['bpm'], ENT_QUOTES, 'UTF-8') . ' BPM' : '' ?>
|
||
|
|
<?= !empty($track['key_signature']) ? ' - ' . htmlspecialchars((string)$track['key_signature'], ENT_QUOTES, 'UTF-8') : '' ?>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div style="font-size:11px; color:var(--muted); text-transform:uppercase; letter-spacing:0.18em;">
|
||
|
|
<?= $sample !== '' ? 'Sample' : 'No sample' ?>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<?php endforeach; ?>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<?php endif; ?>
|
||
|
|
</div>
|
||
|
|
<?php endif; ?>
|
||
|
|
</section>
|
||
|
|
|
||
|
|
<div id="acDock" class="ac-dock" hidden>
|
||
|
|
<div class="ac-dock-inner">
|
||
|
|
<div class="ac-dock-meta">
|
||
|
|
<div id="acDockArt" class="ac-dock-art">AC</div>
|
||
|
|
<div class="ac-dock-title-wrap">
|
||
|
|
<div class="badge" style="font-size:9px;">Now Playing</div>
|
||
|
|
<div id="acDockTitle" class="ac-dock-title">Track sample</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<button type="button" id="acDockToggle" class="ac-dock-toggle"><i class="fa-solid fa-play"></i> <span>Play</span></button>
|
||
|
|
<input id="acDockSeek" class="ac-seek" type="range" min="0" max="100" value="0" step="0.1">
|
||
|
|
<div class="ac-dock-time"><span id="acDockCurrent">0:00</span> / <span id="acDockDuration">0:00</span></div>
|
||
|
|
<input id="acDockVolume" class="ac-volume" type="range" min="0" max="1" value="1" step="0.01">
|
||
|
|
<button type="button" id="acDockClose" class="ac-dock-close" aria-label="Close player">X</button>
|
||
|
|
<audio id="acDockAudio" preload="none"></audio>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<style>
|
||
|
|
.release-wrap {
|
||
|
|
min-width: 0;
|
||
|
|
}
|
||
|
|
.track-row.is-active { border-color: rgba(34,242,165,.45)!important; background: rgba(34,242,165,.08)!important; }
|
||
|
|
.track-play-btn,.ac-dock-toggle{height:34px;border:1px solid rgba(34,242,165,.35);border-radius:999px;background:rgba(34,242,165,.12);color:#bffff0;font-weight:700;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:0 14px}
|
||
|
|
.track-play-btn[disabled]{border-color:rgba(255,255,255,.2);background:rgba(255,255,255,.08);color:var(--muted);cursor:not-allowed}
|
||
|
|
.ac-dock{position:fixed;left:14px;right:14px;bottom:14px;z-index:50}
|
||
|
|
.ac-dock-inner{display:grid;grid-template-columns:minmax(210px,280px) 96px minmax(160px,1fr) 110px 110px 44px;gap:10px;align-items:center;padding:12px;border-radius:14px;border:1px solid rgba(255,255,255,.14);background:rgba(10,11,15,.95)}
|
||
|
|
.ac-dock-meta{display:grid;grid-template-columns:44px minmax(0,1fr);align-items:center;gap:10px;min-width:0}
|
||
|
|
.ac-dock-art{width:44px;height:44px;border-radius:10px;border:1px solid rgba(255,255,255,.12);background:rgba(255,255,255,.06);overflow:hidden;display:grid;place-items:center;font-size:10px;color:var(--muted)}
|
||
|
|
.ac-dock-art img{width:100%;height:100%;object-fit:cover;display:block}
|
||
|
|
.ac-dock-title-wrap{min-width:0}.ac-dock-title{margin-top:4px;font-size:13px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||
|
|
.ac-dock-time{font-size:12px;color:var(--muted);font-family:'IBM Plex Mono',monospace}
|
||
|
|
.ac-dock-close{height:34px;border-radius:10px;border:1px solid rgba(255,255,255,.2);background:rgba(255,255,255,.05);color:var(--muted);cursor:pointer}
|
||
|
|
.ac-seek,.ac-volume{-webkit-appearance:none;appearance:none;height:6px;border-radius:999px;background:rgba(255,255,255,.2);outline:none}
|
||
|
|
.ac-seek::-webkit-slider-thumb,.ac-volume::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;width:14px;height:14px;border-radius:50%;background:#22f2a5;border:1px solid rgba(0,0,0,.45)}
|
||
|
|
@media (max-width: 980px) {
|
||
|
|
.release-hero {
|
||
|
|
grid-template-columns: 1fr !important;
|
||
|
|
gap: 16px !important;
|
||
|
|
}
|
||
|
|
.release-cover-box {
|
||
|
|
max-width: 420px;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
@media (max-width: 700px) {
|
||
|
|
.release-meta h1 {
|
||
|
|
font-size: 32px !important;
|
||
|
|
line-height: 1.12 !important;
|
||
|
|
}
|
||
|
|
.track-row {
|
||
|
|
grid-template-columns: 88px minmax(0,1fr) !important;
|
||
|
|
gap: 10px !important;
|
||
|
|
}
|
||
|
|
.track-row > :last-child {
|
||
|
|
display: none;
|
||
|
|
}
|
||
|
|
.ac-dock {
|
||
|
|
left: 8px;
|
||
|
|
right: 8px;
|
||
|
|
bottom: 8px;
|
||
|
|
}
|
||
|
|
.ac-dock-inner {
|
||
|
|
grid-template-columns: minmax(0,1fr) 90px !important;
|
||
|
|
grid-template-areas:
|
||
|
|
"meta close"
|
||
|
|
"toggle time"
|
||
|
|
"seek seek"
|
||
|
|
"volume volume";
|
||
|
|
row-gap: 8px;
|
||
|
|
}
|
||
|
|
.ac-dock-meta { grid-area: meta; }
|
||
|
|
.ac-dock-close { grid-area: close; }
|
||
|
|
.ac-dock-toggle { grid-area: toggle; }
|
||
|
|
.ac-dock-time { grid-area: time; text-align: right; }
|
||
|
|
.ac-seek { grid-area: seek; }
|
||
|
|
.ac-volume { grid-area: volume; }
|
||
|
|
}
|
||
|
|
</style>
|
||
|
|
|
||
|
|
<script>
|
||
|
|
(function () {
|
||
|
|
const dock = document.getElementById('acDock');
|
||
|
|
const audio = document.getElementById('acDockAudio');
|
||
|
|
const toggle = document.getElementById('acDockToggle');
|
||
|
|
const seek = document.getElementById('acDockSeek');
|
||
|
|
const volume = document.getElementById('acDockVolume');
|
||
|
|
const current = document.getElementById('acDockCurrent');
|
||
|
|
const duration = document.getElementById('acDockDuration');
|
||
|
|
const titleEl = document.getElementById('acDockTitle');
|
||
|
|
const dockArt = document.getElementById('acDockArt');
|
||
|
|
const closeBtn = document.getElementById('acDockClose');
|
||
|
|
const status = document.getElementById('releasePlayerNow');
|
||
|
|
const mainCover = document.getElementById('releaseCoverMain');
|
||
|
|
if (!dock || !audio || !toggle || !seek || !volume || !current || !duration || !titleEl || !closeBtn) return;
|
||
|
|
|
||
|
|
const mainCoverSrc = mainCover && mainCover.tagName === 'IMG' ? (mainCover.getAttribute('src') || '') : '';
|
||
|
|
const defaultCover = mainCoverSrc || <?= json_encode($releaseCover, JSON_UNESCAPED_SLASHES) ?> || '';
|
||
|
|
let activeBtn = null;
|
||
|
|
|
||
|
|
function fmt(sec){ if(!isFinite(sec)||sec<0) return '0:00'; const m=Math.floor(sec/60), s=Math.floor(sec%60); return m+':'+String(s).padStart(2,'0'); }
|
||
|
|
function setPlayState(btn,playing){ if(!btn) return; btn.innerHTML = playing ? '<i class="fa-solid fa-pause"></i> <span>Pause</span>' : '<i class="fa-solid fa-play"></i> <span>Play</span>'; }
|
||
|
|
function setDockArt(src){ if(!dockArt) return; const finalSrc = src || defaultCover; dockArt.innerHTML = finalSrc ? ('<img src="'+finalSrc.replace(/"/g,'"')+'" alt="">') : 'AC'; }
|
||
|
|
|
||
|
|
function setActive(btn,title){
|
||
|
|
document.querySelectorAll('.track-play-btn').forEach((b)=>{ setPlayState(b,false); const row=b.closest('.track-row'); if(row) row.classList.remove('is-active'); });
|
||
|
|
activeBtn = btn;
|
||
|
|
if(btn){ setPlayState(btn,true); const row=btn.closest('.track-row'); if(row) row.classList.add('is-active'); }
|
||
|
|
titleEl.textContent = title || 'Track sample';
|
||
|
|
if(status) status.textContent = title ? ('Now Playing: '+title) : 'Select a track to play sample';
|
||
|
|
}
|
||
|
|
|
||
|
|
function openAndPlay(src,title,btn,cover){
|
||
|
|
if(!src) return;
|
||
|
|
if(dock.hidden) dock.hidden = false;
|
||
|
|
setDockArt(cover || '');
|
||
|
|
if(audio.getAttribute('src') === src){ if(audio.paused) audio.play().catch(()=>{}); else audio.pause(); return; }
|
||
|
|
audio.setAttribute('src', src);
|
||
|
|
setActive(btn,title);
|
||
|
|
audio.play().catch(()=>{ if(btn) setPlayState(btn,false); });
|
||
|
|
}
|
||
|
|
|
||
|
|
document.querySelectorAll('.track-play-btn').forEach((btn)=>{
|
||
|
|
btn.addEventListener('click', ()=>{
|
||
|
|
openAndPlay(btn.getAttribute('data-src') || '', btn.getAttribute('data-title') || 'Track sample', btn, btn.getAttribute('data-cover') || '');
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
toggle.addEventListener('click', ()=>{ if(!audio.getAttribute('src')) return; if(audio.paused) audio.play().catch(()=>{}); else audio.pause(); });
|
||
|
|
closeBtn.addEventListener('click', ()=>{ audio.pause(); audio.removeAttribute('src'); seek.value='0'; current.textContent='0:00'; duration.textContent='0:00'; setPlayState(toggle,false); setActive(null,''); setDockArt(''); dock.hidden=true; });
|
||
|
|
volume.addEventListener('input', ()=>{ audio.volume = Number(volume.value); });
|
||
|
|
seek.addEventListener('input', ()=>{ if(!isFinite(audio.duration)||audio.duration<=0) return; audio.currentTime = (Number(seek.value)/100)*audio.duration; });
|
||
|
|
|
||
|
|
audio.addEventListener('loadedmetadata', ()=>{ duration.textContent = fmt(audio.duration); });
|
||
|
|
audio.addEventListener('timeupdate', ()=>{ current.textContent = fmt(audio.currentTime); if(isFinite(audio.duration)&&audio.duration>0){ seek.value = String((audio.currentTime/audio.duration)*100); } });
|
||
|
|
audio.addEventListener('play', ()=>{ setPlayState(toggle,true); if(activeBtn) setPlayState(activeBtn,true); });
|
||
|
|
audio.addEventListener('pause', ()=>{ setPlayState(toggle,false); if(activeBtn) setPlayState(activeBtn,false); });
|
||
|
|
audio.addEventListener('ended', ()=>{ setPlayState(toggle,false); if(activeBtn) setPlayState(activeBtn,false); seek.value='0'; current.textContent='0:00'; });
|
||
|
|
})();
|
||
|
|
</script>
|
||
|
|
<?php
|
||
|
|
$content = ob_get_clean();
|
||
|
|
require __DIR__ . '/../../../../views/site/layout.php';
|