199 lines
8.9 KiB
Plaintext
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>
|
|
}
|