tajik recitation player improvments

This commit is contained in:
Hamid Reza Mohammadi 2026-09-12 13:24:45 +03:30
parent fb87847d65
commit 652c9ac43e
3 changed files with 178 additions and 7 deletions

View File

@ -183,13 +183,27 @@
<script> <script>
tgInitRecitationSync( tgInitRecitationSync(
document.getElementById('tg-audio-@recitation.Id'), document.getElementById('tg-audio-@recitation.Id'),
'@Html.Raw($"{TajikGanjoor.APIRoot.InternetUrl}/api/audio/file/{recitation.Id}.xml")' '@Html.Raw($"{TajikGanjoor.APIRoot.InternetUrl}/api/audio/file/{recitation.Id}.xml")',
'@Html.Raw(recitation.AudioArtist)',
@recitation.Id
); );
</script> </script>
} }
</div> </div>
} }
</div> </div>
<div id="tg-sticky-player" class="tg-sticky-player" style="display:none;">
<span id="tg-sticky-narrator" class="tg-sticky-player-narrator"></span>
<div id="tg-sticky-audio-slot" class="tg-sticky-player-audio-slot"></div>
<a role="button" id="tg-sticky-lock" class="tg-sticky-player-btn" onclick="tgSwitchScrollLock()" title="Қулфи ҳамоҳангӣ бо матн"><span id="tg-sticky-lock-icon"></span></a>
<a role="button" class="tg-sticky-player-btn" onclick="tgScrollToCurrentVerse()" title="Гузариш ба байти ҷорӣ">
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M12 8a4 4 0 100 8 4 4 0 000-8m8.94 3A9 9 0 0013 3.06V1h-2v2.06A9 9 0 003.06 11H1v2h2.06A9 9 0 0011 20.94V23h2v-2.06A9 9 0 0020.94 13H23v-2zM12 19a7 7 0 110-14 7 7 0 010 14z"/></svg>
</a>
<a role="button" class="tg-sticky-player-btn" onclick="tgCloseStickyPlayer()" title="Пӯшидан">
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>
</a>
</div>
} }
} }
else else

View File

@ -803,3 +803,57 @@ html, body {
border-radius: 4px; border-radius: 4px;
transition: background-color 0.2s ease; transition: background-color 0.2s ease;
} }
/* ---------- sticky mini audio-player (recitation seek controls stay
reachable while scrolling the poem text) ---------- */
.tg-sticky-player {
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 1000;
display: flex;
align-items: center;
gap: 10px;
padding: 8px 14px;
background: var(--tg-paper-edge);
border-top: 1px solid var(--tg-rule);
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.15);
}
.tg-sticky-player-narrator {
font-weight: 600;
white-space: nowrap;
color: var(--tg-ink);
flex-shrink: 0;
}
.tg-sticky-player-audio-slot {
flex: 1;
min-width: 0;
}
.tg-sticky-player-audio-slot audio {
width: 100%;
display: block;
}
.tg-sticky-player-btn {
display: inline-flex;
align-items: center;
cursor: pointer;
color: var(--tg-ink);
padding: 4px 6px;
flex-shrink: 0;
}
.tg-sticky-player-btn:hover {
color: var(--tg-accent);
}
@media (max-width: 600px) {
.tg-sticky-player-narrator {
display: none;
}
}

View File

@ -1,10 +1,11 @@
/* Syncs poem text highlighting to an <audio> recitation's timing XML. /* Syncs poem text highlighting to an <audio> recitation's timing XML.
Simplified port of the equivalent logic in the main ganjoor.net site Simplified port of the equivalent logic in the main ganjoor.net site
(hilightverse/_setXml/_trackTimeChanged in wwwroot/js/bk.js and (hilightverse/_setXml/_trackTimeChanged in wwwroot/js/bk.js and
Pages/Partials/GanjoorPage/PageTypes/_PoemPagePartial.cshtml) - this Pages/Partials/GanjoorPage/PageTypes/_PoemPagePartial.cshtml) - this
version drops the inline pause/scroll-lock buttons injected into the version drops the inline pause button injected into the verse itself,
verse itself, since that needs UI TajikGanjoor doesn't have; it only since that needs UI TajikGanjoor doesn't have; it only does the
does the highlighting. highlighting, plus a shared sticky mini-player (also ported from the
main site) instead of the inline controls.
Verse elements are counted in document order across .m1, .m2 (the two Verse elements are counted in document order across .m1, .m2 (the two
halves of a couplet), .b2 > p (rubaʼi-style already-centered couplets, halves of a couplet), .b2 > p (rubaʼi-style already-centered couplets,
@ -13,6 +14,34 @@
produces for both the Persian and Tajik sites, so the sync XML's produces for both the Persian and Tajik sites, so the sync XML's
VerseOrder values line up the same way. */ VerseOrder values line up the same way. */
(function () { (function () {
var LOCK_CLOSED_SVG = '<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M12 17a2 2 0 002-2 2 2 0 00-2-2 2 2 0 00-2 2 2 2 0 002 2m6-9a2 2 0 012 2v10a2 2 0 01-2 2H6a2 2 0 01-2-2V10a2 2 0 012-2h1V6a5 5 0 0110 0v2zm-6-5a3 3 0 00-3 3v2h6V6a3 3 0 00-3-3z"/></svg>';
var LOCK_OPEN_SVG = '<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M12 17a2 2 0 002-2 2 2 0 00-2-2 2 2 0 00-2 2 2 2 0 002 2m6-9h-1V6a5 5 0 00-10 0h1.9a3.1 3.1 0 016.2 0v2H6a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V10a2 2 0 00-2-2z"/></svg>';
function getCookie(name) {
var match = document.cookie.match(new RegExp('(?:^|; )' + name + '=([^;]*)'));
return match ? decodeURIComponent(match[1]) : null;
}
function setCookie(name, value, days) {
var expires = new Date(Date.now() + days * 24 * 60 * 60 * 1000).toUTCString();
document.cookie = name + '=' + encodeURIComponent(value) + '; expires=' + expires + '; path=/';
}
// default: on, matching the previously-unconditional auto-scroll behavior
var scrollLock = getCookie('tgScrollLock') !== 'false';
function updateLockIcon() {
var icon = document.getElementById('tg-sticky-lock-icon');
if (!icon) return;
icon.innerHTML = scrollLock ? LOCK_CLOSED_SVG : LOCK_OPEN_SVG;
}
window.tgSwitchScrollLock = function () {
scrollLock = !scrollLock;
setCookie('tgScrollLock', scrollLock ? 'true' : 'false', 365);
updateLockIcon();
};
function getVerseElements() { function getVerseElements() {
return Array.prototype.slice.call( return Array.prototype.slice.call(
document.querySelectorAll('.m1, .m2, .n, .l, .b2 > p:not(.tg-fa)') document.querySelectorAll('.m1, .m2, .n, .l, .b2 > p:not(.tg-fa)')
@ -25,13 +54,86 @@
} }
if (index >= 0 && index < elements.length) { if (index >= 0 && index < elements.length) {
elements[index].classList.add('tg-verse-active'); elements[index].classList.add('tg-verse-active');
if (elements[index].scrollIntoView) { if (scrollLock && elements[index].scrollIntoView) {
elements[index].scrollIntoView({ block: 'center', behavior: 'smooth' }); elements[index].scrollIntoView({ block: 'center', behavior: 'smooth' });
} }
} }
} }
window.tgInitRecitationSync = function (audioEl, xmlUrl) { // Jumps to whichever verse is currently marked active, regardless of the
// scroll-lock setting - a one-shot jump distinct from the continuous
// auto-scroll that scrollLock controls.
window.tgScrollToCurrentVerse = function () {
var active = document.querySelector('.tg-verse-active');
if (active && active.scrollIntoView) {
active.scrollIntoView({ block: 'center', behavior: 'smooth' });
}
};
// ---------- sticky mini audio-player ----------
// Re-parents the actual playing <audio> element into a fixed bottom bar,
// same technique as the main site's showStickyPlayer/closeStickyPlayer in
// bk.js: moving an <audio> element via appendChild does not interrupt
// playback, so its native controls stay reachable while scrolling.
var stickyOriginalParent = null;
var stickyOriginalNextSibling = null;
var stickyDismissed = false;
var stickyLastAudioId = null;
function showStickyPlayer(audioEl, narratorName, audioId) {
if (audioId !== stickyLastAudioId) {
stickyDismissed = false;
stickyLastAudioId = audioId;
}
if (stickyDismissed) return;
var bar = document.getElementById('tg-sticky-player');
var slot = document.getElementById('tg-sticky-audio-slot');
if (!bar || !slot) return;
if (audioEl.parentElement !== slot) {
// if a different recitation is already sitting in the slot, put it
// back where it came from first so the slot never holds two
// <audio> elements at once
var existing = slot.firstElementChild;
if (existing && existing !== audioEl && stickyOriginalParent) {
if (stickyOriginalNextSibling) {
stickyOriginalParent.insertBefore(existing, stickyOriginalNextSibling);
} else {
stickyOriginalParent.appendChild(existing);
}
}
stickyOriginalParent = audioEl.parentElement;
stickyOriginalNextSibling = audioEl.nextSibling;
slot.appendChild(audioEl);
}
var narratorSpan = document.getElementById('tg-sticky-narrator');
if (narratorSpan) narratorSpan.textContent = narratorName || '';
updateLockIcon();
bar.style.display = 'flex';
}
window.tgCloseStickyPlayer = function () {
var slot = document.getElementById('tg-sticky-audio-slot');
var bar = document.getElementById('tg-sticky-player');
if (!slot || !bar) return;
var audioEl = slot.firstElementChild;
if (audioEl && stickyOriginalParent) {
if (stickyOriginalNextSibling) {
stickyOriginalParent.insertBefore(audioEl, stickyOriginalNextSibling);
} else {
stickyOriginalParent.appendChild(audioEl);
}
}
bar.style.display = 'none';
stickyDismissed = true;
};
window.tgInitRecitationSync = function (audioEl, xmlUrl, narratorName, audioId) {
var verseStart = []; var verseStart = [];
var verseEnd = []; var verseEnd = [];
var verseIndex = []; var verseIndex = [];
@ -83,6 +185,7 @@
document.querySelectorAll('audio').forEach(function (other) { document.querySelectorAll('audio').forEach(function (other) {
if (other !== audioEl) other.pause(); if (other !== audioEl) other.pause();
}); });
showStickyPlayer(audioEl, narratorName, audioId);
}); });
audioEl.addEventListener('ended', function () { audioEl.addEventListener('ended', function () {