Nick Beckley fb4e139d9f Save
2026-02-10 20:40:52 +00:00

199 lines
8.9 KiB
Plaintext

@model List<CatherineLynwood.Models.SoundtrackTrackModel>
@{
ViewData["Title"] = "Alpha Flame • Soundtrack";
}
<section class="container my-4" id="soundtrack">
<header class="mb-4">
<h1 class="h2">The Alpha Flame • Soundtrack</h1>
<p class="text-muted mb-0">Eight original tracks inspired by key chapters; listen while you read…</p>
</header>
<div class="row gy-4">
@if (Model != null && Model.Any())
{
var index = 0;
foreach (var track in Model)
{
var id = $"track-{index++}";
<div class="col-12">
<article class="card shadow-sm h-100">
<div class="row g-0 align-items-stretch">
<!-- Image + Play/Pause -->
<div class="col-12 col-md-5 col-lg-3">
<div class="position-relative h-100">
<responsive-image src="@track.ImageUrl" class="img-fluid w-100 h-100 object-fit-cover rounded-start" alt="@track.Title image" display-width-percentage="50"></responsive-image>
<button type="button"
class="btn btn-light btn-lg rounded-circle position-absolute top-50 start-50 translate-middle track-toggle"
aria-label="Play @track.Title"
data-audio-id="@id">
<i class="fad fa-play"></i>
</button>
</div>
</div>
<!-- Text -->
<div class="col-12 col-md-7 col-lg-9">
<div class="card-body d-flex flex-column">
<h2 class="h4 mb-2">@track.Title</h2>
@if (!string.IsNullOrWhiteSpace(track.Chapter) || !string.IsNullOrWhiteSpace(track.Description))
{
<p class="text-muted small mb-3">
@if (!string.IsNullOrWhiteSpace(track.Chapter))
{
<span><strong>Chapter:</strong> @track.Chapter</span>
}
@if (!string.IsNullOrWhiteSpace(track.Chapter) && !string.IsNullOrWhiteSpace(track.Description))
{
<span> • </span>
}
@if (!string.IsNullOrWhiteSpace(track.Description))
{
<span>@track.Description</span>
}
</p>
}
<div class="lyrics border rounded p-3 mb-3 overflow-auto"
style="max-height: 300px;">
@if (!string.IsNullOrWhiteSpace(track.LyricsHtml))
{
@Html.Raw(track.LyricsHtml)
}
</div>
<div class="mt-auto">
<button type="button"
class="btn btn-outline-dark me-2 track-toggle"
aria-label="Play @track.Title"
data-audio-id="@id">
<i class="fad fa-play me-1"></i> <span>Play</span>
</button>
<span class="text-muted small" data-duration-for="@id"></span>
</div>
<!-- Hidden audio element -->
<audio id="@id"
preload="metadata"
src="\audio\soundtrack\@track.AudioUrl"
data-title="@track.Title"></audio>
</div>
</div>
</div>
</article>
</div>
}
}
else
{
<div class="col-12">
<div class="alert alert-info">
Tracks will appear here soon.
</div>
</div>
}
</div>
<noscript>
<div class="alert alert-warning mt-4">Enable JavaScript to play the soundtrack.</div>
</noscript>
</section>
<style>
/* Keep images nicely cropped */
.object-fit-cover { object-fit: cover; }
/* Make the overlay button stand out on varied artwork */
.track-toggle.btn-light {
--bs-btn-bg: rgba(255,255,255,.9);
--bs-btn-border-color: rgba(0,0,0,.05);
box-shadow: 0 0.5rem 1rem rgba(0,0,0,.15);
width: 3.25rem;
height: 3.25rem;
display: grid;
place-items: center;
}
.track-toggle .fa-play, .track-toggle .fa-pause { font-size: 1.25rem; }
</style>
@section Scripts {
<script>
(function () {
const cards = document.querySelectorAll('#soundtrack article.card');
const toggles = document.querySelectorAll('.track-toggle');
const audios = Array.from(document.querySelectorAll('#soundtrack audio'));
function setAllToStopped(exceptId) {
audios.forEach(a => {
if (a.id !== exceptId) {
a.pause();
a.currentTime = a.currentTime; // stop updating without resetting
updateUI(a, false);
}
});
}
function formatTime(seconds) {
const s = Math.round(seconds);
const m = Math.floor(s / 60);
const r = s % 60;
return `${m}:${r.toString().padStart(2, '0')}`;
}
function updateUI(audio, isPlaying) {
// Update both toggles for this track
const buttons = document.querySelectorAll(`.track-toggle[data-audio-id="${audio.id}"]`);
buttons.forEach(btn => {
const icon = btn.querySelector('i');
const labelSpan = btn.querySelector('span');
if (isPlaying) {
btn.setAttribute('aria-label', `Pause ${audio.dataset.title}`);
if (icon) { icon.classList.remove('fa-play'); icon.classList.add('fa-pause'); }
if (labelSpan) { labelSpan.textContent = 'Pause'; }
} else {
btn.setAttribute('aria-label', `Play ${audio.dataset.title}`);
if (icon) { icon.classList.remove('fa-pause'); icon.classList.add('fa-play'); }
if (labelSpan) { labelSpan.textContent = 'Play'; }
}
});
}
// Wire up buttons
toggles.forEach(btn => {
btn.addEventListener('click', () => {
const id = btn.getAttribute('data-audio-id');
const audio = document.getElementById(id);
if (!audio) return;
if (audio.paused) {
setAllToStopped(id);
audio.play().then(() => updateUI(audio, true)).catch(() => { /* ignore */ });
} else {
audio.pause();
updateUI(audio, false);
}
});
});
// Keep UI in sync with media events
audios.forEach(audio => {
audio.addEventListener('play', () => {
setAllToStopped(audio.id);
updateUI(audio, true);
});
audio.addEventListener('pause', () => updateUI(audio, false));
audio.addEventListener('ended', () => {
audio.currentTime = 0;
updateUI(audio, false);
});
audio.addEventListener('loadedmetadata', () => {
const slot = document.querySelector(`[data-duration-for="${audio.id}"]`);
if (slot && isFinite(audio.duration)) {
slot.textContent = `Length: ${formatTime(audio.duration)}`;
}
});
});
})();
</script>
}