tajik recitation player improvments
This commit is contained in:
parent
fb87847d65
commit
652c9ac43e
@ -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
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -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 () {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user