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>
tgInitRecitationSync(
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>
}
</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

View File

@ -803,3 +803,57 @@ html, body {
border-radius: 4px;
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
(hilightverse/_setXml/_trackTimeChanged in wwwroot/js/bk.js and
Pages/Partials/GanjoorPage/PageTypes/_PoemPagePartial.cshtml) - this
version drops the inline pause/scroll-lock buttons injected into the
verse itself, since that needs UI TajikGanjoor doesn't have; it only
does the highlighting.
version drops the inline pause button injected into the verse itself,
since that needs UI TajikGanjoor doesn't have; it only does the
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
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
VerseOrder values line up the same way. */
(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() {
return Array.prototype.slice.call(
document.querySelectorAll('.m1, .m2, .n, .l, .b2 > p:not(.tg-fa)')
@ -25,13 +54,86 @@
}
if (index >= 0 && index < elements.length) {
elements[index].classList.add('tg-verse-active');
if (elements[index].scrollIntoView) {
if (scrollLock && elements[index].scrollIntoView) {
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 verseEnd = [];
var verseIndex = [];
@ -83,6 +185,7 @@
document.querySelectorAll('audio').forEach(function (other) {
if (other !== audioEl) other.pause();
});
showStickyPlayer(audioEl, narratorName, audioId);
});
audioEl.addEventListener('ended', function () {