@uimaxbai/am-lyrics 1.2.7 → 1.2.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/AmLyrics.d.ts +13 -0
- package/dist/src/AmLyrics.d.ts.map +1 -1
- package/dist/src/am-lyrics.js +306 -185
- package/dist/src/am-lyrics.js.map +1 -1
- package/dist/src/react.js +306 -185
- package/dist/src/react.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/AmLyrics.ts +367 -211
package/dist/src/am-lyrics.js
CHANGED
|
@@ -319,7 +319,7 @@ class GoogleService {
|
|
|
319
319
|
}
|
|
320
320
|
}
|
|
321
321
|
|
|
322
|
-
const VERSION = '1.2.
|
|
322
|
+
const VERSION = '1.2.9';
|
|
323
323
|
const INSTRUMENTAL_THRESHOLD_MS = 7000; // Show dots for gaps >= 7s
|
|
324
324
|
const FETCH_TIMEOUT_MS = 8000; // Timeout for all lyrics fetch requests
|
|
325
325
|
const SEEK_THRESHOLD_MS = 500;
|
|
@@ -392,6 +392,15 @@ class AmLyrics extends i {
|
|
|
392
392
|
this.isClickSeeking = false;
|
|
393
393
|
// Cached DOM elements for animation updates
|
|
394
394
|
this.cachedLyricsLines = [];
|
|
395
|
+
// Cached line and gap element maps for fast lookup
|
|
396
|
+
this.lineElementCache = new Map();
|
|
397
|
+
this.gapElementCache = new Map();
|
|
398
|
+
// Cached gap computation results
|
|
399
|
+
this.cachedAllGaps = [];
|
|
400
|
+
// Cached isUnsynced flag
|
|
401
|
+
this.cachedIsUnsynced = false;
|
|
402
|
+
// Cached pre-computed line data for render
|
|
403
|
+
this.cachedLineData = null;
|
|
395
404
|
// Active line tracking
|
|
396
405
|
this.activeLineIds = new Set();
|
|
397
406
|
this.currentPrimaryActiveLine = null;
|
|
@@ -483,6 +492,25 @@ class AmLyrics extends i {
|
|
|
483
492
|
get currentTime() {
|
|
484
493
|
return this._currentTime;
|
|
485
494
|
}
|
|
495
|
+
_updateFooter() {
|
|
496
|
+
const footer = this.shadowRoot?.querySelector('.lyrics-footer');
|
|
497
|
+
if (!footer)
|
|
498
|
+
return;
|
|
499
|
+
const switchBtn = footer.querySelector('.source-switch-btn');
|
|
500
|
+
const svgEl = footer.querySelector('.source-switch-svg');
|
|
501
|
+
const labelEl = footer.querySelector('.source-switch-label');
|
|
502
|
+
if (switchBtn) {
|
|
503
|
+
switchBtn.disabled = this.isFetchingAlternatives;
|
|
504
|
+
}
|
|
505
|
+
if (svgEl) {
|
|
506
|
+
svgEl.setAttribute('style', `margin-right: 4px; ${this.isFetchingAlternatives ? 'animation: spin 1s linear infinite;' : ''}`);
|
|
507
|
+
}
|
|
508
|
+
if (labelEl) {
|
|
509
|
+
labelEl.textContent = this.isFetchingAlternatives
|
|
510
|
+
? 'Switching...'
|
|
511
|
+
: 'Switch';
|
|
512
|
+
}
|
|
513
|
+
}
|
|
486
514
|
connectedCallback() {
|
|
487
515
|
super.connectedCallback();
|
|
488
516
|
this.fetchLyrics();
|
|
@@ -530,6 +558,7 @@ class AmLyrics extends i {
|
|
|
530
558
|
this.currentSourceIndex = 0;
|
|
531
559
|
this.isFetchingAlternatives = false;
|
|
532
560
|
this.hasFetchedAllProviders = false;
|
|
561
|
+
this._updateFooter();
|
|
533
562
|
try {
|
|
534
563
|
const resolvedMetadata = await this.resolveSongMetadata();
|
|
535
564
|
// If a newer fetch was triggered while we awaited, bail out
|
|
@@ -582,6 +611,7 @@ class AmLyrics extends i {
|
|
|
582
611
|
collectedSources.some(s => s.source === 'LRCLIB' ||
|
|
583
612
|
s.source === 'Tidal' ||
|
|
584
613
|
s.source === 'Genius');
|
|
614
|
+
this._updateFooter();
|
|
585
615
|
if (collectedSources.length > 0) {
|
|
586
616
|
this.availableSources = AmLyrics.mergeAndSortSources(collectedSources);
|
|
587
617
|
this.currentSourceIndex = 0;
|
|
@@ -692,6 +722,7 @@ class AmLyrics extends i {
|
|
|
692
722
|
return;
|
|
693
723
|
if (!this.hasFetchedAllProviders) {
|
|
694
724
|
this.isFetchingAlternatives = true;
|
|
725
|
+
this._updateFooter();
|
|
695
726
|
try {
|
|
696
727
|
const resolvedMetadata = await this.resolveSongMetadata();
|
|
697
728
|
if (resolvedMetadata?.metadata) {
|
|
@@ -731,6 +762,7 @@ class AmLyrics extends i {
|
|
|
731
762
|
finally {
|
|
732
763
|
this.hasFetchedAllProviders = true;
|
|
733
764
|
this.isFetchingAlternatives = false;
|
|
765
|
+
this._updateFooter();
|
|
734
766
|
}
|
|
735
767
|
}
|
|
736
768
|
if (this.availableSources.length > 1) {
|
|
@@ -1783,13 +1815,11 @@ class AmLyrics extends i {
|
|
|
1783
1815
|
// Reset animation if active lines change or if we skip time.
|
|
1784
1816
|
const linesChanged = !AmLyrics.arraysEqual(newActiveLines, oldActiveLines);
|
|
1785
1817
|
if (linesChanged || isSeek) {
|
|
1786
|
-
// Imperatively manage 'active' class so that scroll-animate and other
|
|
1787
|
-
// imperative classes are never clobbered.
|
|
1788
1818
|
if (this.lyricsContainer) {
|
|
1789
1819
|
// Remove 'active' from lines that are no longer active
|
|
1790
1820
|
for (const lineIndex of oldActiveLines) {
|
|
1791
1821
|
if (!newActiveLines.includes(lineIndex)) {
|
|
1792
|
-
const lineElement = this.
|
|
1822
|
+
const lineElement = this._getLineElement(lineIndex);
|
|
1793
1823
|
if (lineElement) {
|
|
1794
1824
|
lineElement.classList.remove('active');
|
|
1795
1825
|
AmLyrics.resetSyllables(lineElement);
|
|
@@ -1799,10 +1829,10 @@ class AmLyrics extends i {
|
|
|
1799
1829
|
// Add 'active' to newly active lines
|
|
1800
1830
|
for (const lineIndex of newActiveLines) {
|
|
1801
1831
|
if (!oldActiveLines.includes(lineIndex)) {
|
|
1802
|
-
const lineElement = this.
|
|
1832
|
+
const lineElement = this._getLineElement(lineIndex);
|
|
1803
1833
|
if (lineElement) {
|
|
1804
1834
|
lineElement.classList.add('active');
|
|
1805
|
-
lineElement.classList.remove('pre-active');
|
|
1835
|
+
lineElement.classList.remove('pre-active');
|
|
1806
1836
|
}
|
|
1807
1837
|
}
|
|
1808
1838
|
}
|
|
@@ -1811,14 +1841,12 @@ class AmLyrics extends i {
|
|
|
1811
1841
|
}
|
|
1812
1842
|
}
|
|
1813
1843
|
this.startAnimationFromTime(newTime);
|
|
1814
|
-
// Trigger scroll imperatively (was previously in updated() via @state)
|
|
1815
1844
|
this._handleActiveLineScroll(oldActiveLines, isSeek);
|
|
1816
1845
|
}
|
|
1817
|
-
// YouLyPlus-style syllable animation updates
|
|
1818
1846
|
if (this.lyricsContainer) {
|
|
1819
|
-
// Update syllables in active lines
|
|
1847
|
+
// Update syllables in active lines using cached elements
|
|
1820
1848
|
for (const lineIndex of this.activeLineIndices) {
|
|
1821
|
-
const lineElement = this.
|
|
1849
|
+
const lineElement = this._getLineElement(lineIndex);
|
|
1822
1850
|
if (lineElement) {
|
|
1823
1851
|
AmLyrics.updateSyllablesForLine(lineElement, newTime);
|
|
1824
1852
|
}
|
|
@@ -1828,60 +1856,81 @@ class AmLyrics extends i {
|
|
|
1828
1856
|
activeGaps.forEach(gapLine => {
|
|
1829
1857
|
AmLyrics.updateSyllablesForLine(gapLine, newTime);
|
|
1830
1858
|
});
|
|
1831
|
-
// Imperatively manage gap active state
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
// Also ensure the highlight + animation fired so CSS state is correct
|
|
1856
|
-
if (!dot.classList.contains('highlight')) {
|
|
1859
|
+
// Imperatively manage gap active state
|
|
1860
|
+
if (this.gapElementCache.size > 0) {
|
|
1861
|
+
for (const [, gap] of this.gapElementCache) {
|
|
1862
|
+
const gapStartTime = parseFloat(gap.getAttribute('data-start-time') || '0');
|
|
1863
|
+
const gapEndTime = parseFloat(gap.getAttribute('data-end-time') || '0');
|
|
1864
|
+
const shouldBeActive = newTime >= gapStartTime && newTime < gapEndTime;
|
|
1865
|
+
const isActive = gap.classList.contains('active');
|
|
1866
|
+
const isExiting = gap.classList.contains('gap-exiting');
|
|
1867
|
+
const exitLeadMs = GAP_EXIT_LEAD_MS;
|
|
1868
|
+
const shouldStartExiting = isActive && !isExiting && newTime >= gapEndTime - exitLeadMs;
|
|
1869
|
+
if (shouldBeActive && !isActive && !isExiting) {
|
|
1870
|
+
gap.classList.remove('gap-exiting');
|
|
1871
|
+
gap.classList.add('active');
|
|
1872
|
+
const dotSyllables = gap.querySelectorAll('.lyrics-syllable');
|
|
1873
|
+
dotSyllables.forEach(dot => {
|
|
1874
|
+
const dotStart = parseFloat(dot.getAttribute('data-start-time') || '0');
|
|
1875
|
+
const dotEnd = parseFloat(dot.getAttribute('data-end-time') || '0');
|
|
1876
|
+
if (newTime > dotEnd) {
|
|
1877
|
+
dot.classList.add('finished');
|
|
1878
|
+
if (!dot.classList.contains('highlight')) {
|
|
1879
|
+
AmLyrics.updateSyllableAnimation(dot);
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
else if (newTime >= dotStart && newTime <= dotEnd) {
|
|
1857
1883
|
AmLyrics.updateSyllableAnimation(dot);
|
|
1858
1884
|
}
|
|
1859
|
-
}
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
// After exit animation completes, remove gap-exiting to collapse
|
|
1871
|
-
setTimeout(() => {
|
|
1885
|
+
});
|
|
1886
|
+
}
|
|
1887
|
+
else if (shouldStartExiting) {
|
|
1888
|
+
gap.classList.add('gap-exiting');
|
|
1889
|
+
gap.classList.remove('active');
|
|
1890
|
+
setTimeout(() => {
|
|
1891
|
+
gap.classList.remove('gap-exiting');
|
|
1892
|
+
}, GAP_EXIT_LEAD_MS);
|
|
1893
|
+
}
|
|
1894
|
+
else if (isActive && !shouldBeActive) {
|
|
1895
|
+
gap.classList.remove('active');
|
|
1872
1896
|
gap.classList.remove('gap-exiting');
|
|
1873
|
-
}
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
gap.classList.remove('active');
|
|
1878
|
-
gap.classList.remove('gap-exiting');
|
|
1879
|
-
}
|
|
1880
|
-
else if (isExiting && newTime < gapEndTime - exitLeadMs) {
|
|
1881
|
-
// NEW: Cleanup exiting state if we seeked backwards before exit window
|
|
1882
|
-
gap.classList.remove('gap-exiting');
|
|
1897
|
+
}
|
|
1898
|
+
else if (isExiting && newTime < gapEndTime - exitLeadMs) {
|
|
1899
|
+
gap.classList.remove('gap-exiting');
|
|
1900
|
+
}
|
|
1883
1901
|
}
|
|
1884
|
-
}
|
|
1902
|
+
}
|
|
1903
|
+
else if (this.lyricsContainer) {
|
|
1904
|
+
// Fallback: no cache yet, use querySelectorAll
|
|
1905
|
+
const allGaps = this.lyricsContainer.querySelectorAll('.lyrics-gap');
|
|
1906
|
+
allGaps.forEach(gap => {
|
|
1907
|
+
const gapStartTime = parseFloat(gap.getAttribute('data-start-time') || '0');
|
|
1908
|
+
const gapEndTime = parseFloat(gap.getAttribute('data-end-time') || '0');
|
|
1909
|
+
const shouldBeActive = newTime >= gapStartTime && newTime < gapEndTime;
|
|
1910
|
+
const isActive = gap.classList.contains('active');
|
|
1911
|
+
const isExiting = gap.classList.contains('gap-exiting');
|
|
1912
|
+
const exitLeadMs = GAP_EXIT_LEAD_MS;
|
|
1913
|
+
const shouldStartExiting = isActive && !isExiting && newTime >= gapEndTime - exitLeadMs;
|
|
1914
|
+
if (shouldBeActive && !isActive && !isExiting) {
|
|
1915
|
+
gap.classList.remove('gap-exiting');
|
|
1916
|
+
gap.classList.add('active');
|
|
1917
|
+
}
|
|
1918
|
+
else if (shouldStartExiting) {
|
|
1919
|
+
gap.classList.add('gap-exiting');
|
|
1920
|
+
gap.classList.remove('active');
|
|
1921
|
+
setTimeout(() => {
|
|
1922
|
+
gap.classList.remove('gap-exiting');
|
|
1923
|
+
}, GAP_EXIT_LEAD_MS);
|
|
1924
|
+
}
|
|
1925
|
+
else if (isActive && !shouldBeActive) {
|
|
1926
|
+
gap.classList.remove('active');
|
|
1927
|
+
gap.classList.remove('gap-exiting');
|
|
1928
|
+
}
|
|
1929
|
+
else if (isExiting && newTime < gapEndTime - exitLeadMs) {
|
|
1930
|
+
gap.classList.remove('gap-exiting');
|
|
1931
|
+
}
|
|
1932
|
+
});
|
|
1933
|
+
}
|
|
1885
1934
|
// Track instrumental gap state
|
|
1886
1935
|
const currentGap = this.findInstrumentalGapAt(newTime);
|
|
1887
1936
|
if (currentGap) {
|
|
@@ -1899,20 +1948,18 @@ class AmLyrics extends i {
|
|
|
1899
1948
|
for (let i = 0; i < this.lyrics.length; i += 1) {
|
|
1900
1949
|
const line = this.lyrics[i];
|
|
1901
1950
|
const timeUntilStart = line.timestamp - newTime;
|
|
1902
|
-
const nextLineEl = this.
|
|
1951
|
+
const nextLineEl = this._getLineElement(i);
|
|
1903
1952
|
const isBackToBack = this.activeLineIndices.length > 0;
|
|
1904
1953
|
const leadTime = isBackToBack
|
|
1905
1954
|
? PRE_SCROLL_LEAD_SHORT_MS
|
|
1906
1955
|
: PRE_SCROLL_LEAD_MS;
|
|
1907
1956
|
if (timeUntilStart > leadTime) {
|
|
1908
|
-
break;
|
|
1957
|
+
break;
|
|
1909
1958
|
}
|
|
1910
1959
|
if (timeUntilStart > 0 && timeUntilStart <= leadTime) {
|
|
1911
|
-
// Time to pre-scroll and pre-activate!
|
|
1912
1960
|
if (nextLineEl) {
|
|
1913
1961
|
preActiveLineIndex = i;
|
|
1914
1962
|
if (!isBackToBack) {
|
|
1915
|
-
// Apply unblur & zoom effect ahead of lyric start only if no line is currently active
|
|
1916
1963
|
nextLineEl.classList.add('pre-active');
|
|
1917
1964
|
}
|
|
1918
1965
|
this.clearPreActiveClasses(i);
|
|
@@ -1928,6 +1975,9 @@ class AmLyrics extends i {
|
|
|
1928
1975
|
}
|
|
1929
1976
|
updated(changedProperties) {
|
|
1930
1977
|
if (changedProperties.has('lyrics')) {
|
|
1978
|
+
this._invalidateCaches();
|
|
1979
|
+
this._ensureLineDataCache();
|
|
1980
|
+
this._updateCachedIsUnsynced();
|
|
1931
1981
|
// Recalculate timing data for accurate animations whenever lyrics change
|
|
1932
1982
|
this._updateCharTimingData();
|
|
1933
1983
|
// Apply 'active' classes imperatively after lyrics first render,
|
|
@@ -1936,7 +1986,7 @@ class AmLyrics extends i {
|
|
|
1936
1986
|
if (this.lyricsContainer && this.lyrics) {
|
|
1937
1987
|
const activeLines = this.findActiveLineIndices(this.currentTime);
|
|
1938
1988
|
for (const lineIndex of activeLines) {
|
|
1939
|
-
const lineEl = this.
|
|
1989
|
+
const lineEl = this._getLineElement(lineIndex);
|
|
1940
1990
|
if (lineEl)
|
|
1941
1991
|
lineEl.classList.add('active');
|
|
1942
1992
|
}
|
|
@@ -1952,7 +2002,7 @@ class AmLyrics extends i {
|
|
|
1952
2002
|
this.backgroundWordProgress.clear();
|
|
1953
2003
|
this.mainWordAnimations.clear();
|
|
1954
2004
|
this.backgroundWordAnimations.clear();
|
|
1955
|
-
this.
|
|
2005
|
+
this.setUserScrolling(false);
|
|
1956
2006
|
// Cancel any running animations
|
|
1957
2007
|
if (this.animationFrameId) {
|
|
1958
2008
|
cancelAnimationFrame(this.animationFrameId);
|
|
@@ -1992,10 +2042,23 @@ class AmLyrics extends i {
|
|
|
1992
2042
|
const targetLineIndex = this.getPrimaryActiveLineIndex(this.activeLineIndices);
|
|
1993
2043
|
if (targetLineIndex === null)
|
|
1994
2044
|
return;
|
|
1995
|
-
const targetLine = this.
|
|
1996
|
-
if (targetLine)
|
|
1997
|
-
|
|
2045
|
+
const targetLine = this._getLineElement(targetLineIndex);
|
|
2046
|
+
if (!targetLine)
|
|
2047
|
+
return;
|
|
2048
|
+
// Only scroll snappily when lines are essentially back-to-back.
|
|
2049
|
+
// If there is any noticeable gap between them, scroll slower.
|
|
2050
|
+
let scrollDuration;
|
|
2051
|
+
const prevPrimaryIndex = AmLyrics.getLineIndexFromElement(this.currentPrimaryActiveLine);
|
|
2052
|
+
if (prevPrimaryIndex !== null &&
|
|
2053
|
+
targetLineIndex > prevPrimaryIndex &&
|
|
2054
|
+
this.lyrics) {
|
|
2055
|
+
const gap = this.lyrics[targetLineIndex].timestamp -
|
|
2056
|
+
this.lyrics[prevPrimaryIndex].endtime;
|
|
2057
|
+
if (gap > 200) {
|
|
2058
|
+
scrollDuration = Math.min(Math.max(gap * 0.6, SCROLL_ANIMATION_DURATION_MS), 2000);
|
|
2059
|
+
}
|
|
1998
2060
|
}
|
|
2061
|
+
this.focusLine(targetLine, forceScroll, scrollDuration);
|
|
1999
2062
|
}
|
|
2000
2063
|
_getTextWidth(text, font) {
|
|
2001
2064
|
if (!this._textWidthCanvas) {
|
|
@@ -2010,9 +2073,142 @@ class AmLyrics extends i {
|
|
|
2010
2073
|
}
|
|
2011
2074
|
return 0;
|
|
2012
2075
|
}
|
|
2076
|
+
_rebuildDomCache() {
|
|
2077
|
+
if (!this.lyricsContainer)
|
|
2078
|
+
return;
|
|
2079
|
+
this.lineElementCache.clear();
|
|
2080
|
+
this.gapElementCache.clear();
|
|
2081
|
+
if (!this.lyrics)
|
|
2082
|
+
return;
|
|
2083
|
+
for (let i = 0; i < this.lyrics.length; i += 1) {
|
|
2084
|
+
const lineEl = this.lyricsContainer.querySelector(`#lyrics-line-${i}`);
|
|
2085
|
+
if (lineEl)
|
|
2086
|
+
this.lineElementCache.set(i, lineEl);
|
|
2087
|
+
const gapEl = this.lyricsContainer.querySelector(`#gap-${i}`);
|
|
2088
|
+
if (gapEl)
|
|
2089
|
+
this.gapElementCache.set(i, gapEl);
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
_getLineElement(index) {
|
|
2093
|
+
const cached = this.lineElementCache.get(index);
|
|
2094
|
+
if (cached)
|
|
2095
|
+
return cached;
|
|
2096
|
+
if (!this.lyricsContainer)
|
|
2097
|
+
return null;
|
|
2098
|
+
const el = this.lyricsContainer.querySelector(`#lyrics-line-${index}`);
|
|
2099
|
+
if (el)
|
|
2100
|
+
this.lineElementCache.set(index, el);
|
|
2101
|
+
return el;
|
|
2102
|
+
}
|
|
2103
|
+
_getGapElement(index) {
|
|
2104
|
+
const cached = this.gapElementCache.get(index);
|
|
2105
|
+
if (cached)
|
|
2106
|
+
return cached;
|
|
2107
|
+
if (!this.lyricsContainer)
|
|
2108
|
+
return null;
|
|
2109
|
+
const el = this.lyricsContainer.querySelector(`#gap-${index}`);
|
|
2110
|
+
if (el)
|
|
2111
|
+
this.gapElementCache.set(index, el);
|
|
2112
|
+
return el;
|
|
2113
|
+
}
|
|
2114
|
+
_invalidateCaches() {
|
|
2115
|
+
this.cachedAllGaps = [];
|
|
2116
|
+
this.cachedIsUnsynced = false;
|
|
2117
|
+
this.cachedLineData = null;
|
|
2118
|
+
this.lineElementCache.clear();
|
|
2119
|
+
this.gapElementCache.clear();
|
|
2120
|
+
}
|
|
2121
|
+
_updateCachedIsUnsynced() {
|
|
2122
|
+
this.cachedIsUnsynced =
|
|
2123
|
+
this.lyrics && this.lyrics.length > 0
|
|
2124
|
+
? this.lyrics.every(l => l.timestamp === 0 && l.endtime === 0)
|
|
2125
|
+
: false;
|
|
2126
|
+
}
|
|
2127
|
+
_ensureLineDataCache() {
|
|
2128
|
+
if (this.cachedLineData || !this.lyrics)
|
|
2129
|
+
return;
|
|
2130
|
+
this.cachedLineData = this.lyrics.map(line => {
|
|
2131
|
+
const wordGroups = [];
|
|
2132
|
+
for (const syllable of line.text) {
|
|
2133
|
+
if (syllable.part && wordGroups.length > 0) {
|
|
2134
|
+
wordGroups[wordGroups.length - 1].push(syllable);
|
|
2135
|
+
}
|
|
2136
|
+
else {
|
|
2137
|
+
wordGroups.push([syllable]);
|
|
2138
|
+
}
|
|
2139
|
+
}
|
|
2140
|
+
const groupGrowable = new Array(wordGroups.length).fill(false);
|
|
2141
|
+
const groupGlowing = new Array(wordGroups.length).fill(false);
|
|
2142
|
+
const vwFullText = new Array(wordGroups.length).fill('');
|
|
2143
|
+
const vwFullDuration = new Array(wordGroups.length).fill(0);
|
|
2144
|
+
const vwCharOffset = new Array(wordGroups.length).fill(0);
|
|
2145
|
+
const vwStartMs = new Array(wordGroups.length).fill(0);
|
|
2146
|
+
const vwEndMs = new Array(wordGroups.length).fill(0);
|
|
2147
|
+
let vwStart = 0;
|
|
2148
|
+
while (vwStart < wordGroups.length) {
|
|
2149
|
+
let vwEnd = vwStart;
|
|
2150
|
+
while (vwEnd < wordGroups.length - 1) {
|
|
2151
|
+
const grp = wordGroups[vwEnd];
|
|
2152
|
+
const lastText = grp[grp.length - 1].text;
|
|
2153
|
+
if (/\s$/.test(lastText))
|
|
2154
|
+
break;
|
|
2155
|
+
vwEnd += 1;
|
|
2156
|
+
}
|
|
2157
|
+
const combinedText = wordGroups
|
|
2158
|
+
.slice(vwStart, vwEnd + 1)
|
|
2159
|
+
.flatMap(g => g.map(s => s.text))
|
|
2160
|
+
.join('')
|
|
2161
|
+
.trim();
|
|
2162
|
+
const combinedStart = wordGroups[vwStart][0].timestamp;
|
|
2163
|
+
const lastGrp = wordGroups[vwEnd];
|
|
2164
|
+
const combinedEnd = lastGrp[lastGrp.length - 1].endtime;
|
|
2165
|
+
const combinedDuration = combinedEnd - combinedStart;
|
|
2166
|
+
const isCJK = /[\u4e00-\u9fff\u3040-\u309f\u30a0-\u30ff\uac00-\ud7af]/.test(combinedText);
|
|
2167
|
+
const isRTL = /[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\u0590-\u05FF]/.test(combinedText);
|
|
2168
|
+
const hasHyphen = combinedText.includes('-');
|
|
2169
|
+
const wordLen = combinedText.length;
|
|
2170
|
+
let isGrowableVW = !isCJK && !isRTL && !hasHyphen && wordLen > 0 && wordLen <= 12;
|
|
2171
|
+
if (isGrowableVW) {
|
|
2172
|
+
if (wordLen < 3) {
|
|
2173
|
+
isGrowableVW =
|
|
2174
|
+
combinedDuration >= 1000 && combinedDuration >= wordLen * 500;
|
|
2175
|
+
}
|
|
2176
|
+
else {
|
|
2177
|
+
isGrowableVW =
|
|
2178
|
+
combinedDuration >= 800 && combinedDuration >= wordLen * 180;
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
const isGlowingVW = isGrowableVW;
|
|
2182
|
+
let charOff = 0;
|
|
2183
|
+
for (let gi = vwStart; gi <= vwEnd; gi += 1) {
|
|
2184
|
+
groupGrowable[gi] = isGrowableVW;
|
|
2185
|
+
groupGlowing[gi] = isGlowingVW;
|
|
2186
|
+
vwFullText[gi] = combinedText;
|
|
2187
|
+
vwFullDuration[gi] = combinedDuration;
|
|
2188
|
+
vwCharOffset[gi] = charOff;
|
|
2189
|
+
vwStartMs[gi] = combinedStart;
|
|
2190
|
+
vwEndMs[gi] = combinedEnd;
|
|
2191
|
+
const grpText = wordGroups[gi].map(s => s.text).join('');
|
|
2192
|
+
charOff += grpText.replace(/\s/g, '').length;
|
|
2193
|
+
}
|
|
2194
|
+
vwStart = vwEnd + 1;
|
|
2195
|
+
}
|
|
2196
|
+
return {
|
|
2197
|
+
wordGroups,
|
|
2198
|
+
groupGrowable,
|
|
2199
|
+
groupGlowing,
|
|
2200
|
+
vwFullText,
|
|
2201
|
+
vwFullDuration,
|
|
2202
|
+
vwCharOffset,
|
|
2203
|
+
vwStartMs,
|
|
2204
|
+
vwEndMs,
|
|
2205
|
+
};
|
|
2206
|
+
});
|
|
2207
|
+
}
|
|
2013
2208
|
_updateCharTimingData() {
|
|
2014
2209
|
if (!this.shadowRoot)
|
|
2015
2210
|
return;
|
|
2211
|
+
this._rebuildDomCache();
|
|
2016
2212
|
// Get the computed font from the first syllable to ensure accuracy
|
|
2017
2213
|
const referenceSyllable = this.shadowRoot.querySelector('.lyrics-syllable');
|
|
2018
2214
|
if (!referenceSyllable)
|
|
@@ -2125,21 +2321,29 @@ class AmLyrics extends i {
|
|
|
2125
2321
|
this.scrollToActiveLineYouLy(lineElement, forceScroll, scrollDuration);
|
|
2126
2322
|
}
|
|
2127
2323
|
}
|
|
2324
|
+
setUserScrolling(value) {
|
|
2325
|
+
this.isUserScrolling = value;
|
|
2326
|
+
if (value) {
|
|
2327
|
+
this.lyricsContainer?.classList.add('user-scrolling');
|
|
2328
|
+
}
|
|
2329
|
+
else {
|
|
2330
|
+
this.lyricsContainer?.classList.remove('user-scrolling');
|
|
2331
|
+
}
|
|
2332
|
+
}
|
|
2128
2333
|
handleUserScroll() {
|
|
2129
2334
|
// Ignore programmatic scrolls and click-seek scrolls
|
|
2130
2335
|
if (this.isProgrammaticScroll || this.isClickSeeking) {
|
|
2131
2336
|
return;
|
|
2132
2337
|
}
|
|
2133
2338
|
// Mark that user is currently scrolling
|
|
2134
|
-
this.
|
|
2135
|
-
this.lyricsContainer?.classList.add('user-scrolling');
|
|
2339
|
+
this.setUserScrolling(true);
|
|
2136
2340
|
// Clear any existing timeout
|
|
2137
2341
|
if (this.userScrollTimeoutId) {
|
|
2138
2342
|
clearTimeout(this.userScrollTimeoutId);
|
|
2139
2343
|
}
|
|
2140
2344
|
// Set timeout to re-enable auto-scroll after 2 seconds of no scrolling
|
|
2141
2345
|
this.userScrollTimeoutId = window.setTimeout(() => {
|
|
2142
|
-
this.
|
|
2346
|
+
this.setUserScrolling(false);
|
|
2143
2347
|
this.userScrollTimeoutId = undefined;
|
|
2144
2348
|
// Optionally scroll back to current active line when re-enabling auto-scroll
|
|
2145
2349
|
if (this.activeLineIndices.length > 0) {
|
|
@@ -2148,25 +2352,23 @@ class AmLyrics extends i {
|
|
|
2148
2352
|
}, 2000);
|
|
2149
2353
|
}
|
|
2150
2354
|
findActiveLineIndices(time) {
|
|
2151
|
-
if (!this.lyrics)
|
|
2355
|
+
if (!this.lyrics || this.lyrics.length === 0)
|
|
2152
2356
|
return [];
|
|
2153
2357
|
const activeLines = [];
|
|
2154
2358
|
for (let i = 0; i < this.lyrics.length; i += 1) {
|
|
2155
2359
|
const line = this.lyrics[i];
|
|
2156
2360
|
let effectiveEndTime = line.endtime;
|
|
2157
|
-
// Extend the "active" highlight window to abut the next line,
|
|
2158
|
-
// leaving a 500ms gap for breathing/scrolling
|
|
2159
2361
|
if (i < this.lyrics.length - 1) {
|
|
2160
2362
|
const nextLineStart = this.lyrics[i + 1].timestamp;
|
|
2161
2363
|
const gapDuration = nextLineStart - line.endtime;
|
|
2162
|
-
// If the gap is large enough to trigger the breathing dots,
|
|
2163
|
-
// DO NOT extend the highlight. The text should dim when the dots appear.
|
|
2164
2364
|
if (gapDuration < INSTRUMENTAL_THRESHOLD_MS) {
|
|
2165
2365
|
if (effectiveEndTime < nextLineStart) {
|
|
2166
2366
|
effectiveEndTime = Math.max(effectiveEndTime, nextLineStart - 500);
|
|
2167
2367
|
}
|
|
2168
2368
|
}
|
|
2169
2369
|
}
|
|
2370
|
+
if (line.timestamp > time)
|
|
2371
|
+
break;
|
|
2170
2372
|
if (time >= line.timestamp && time <= effectiveEndTime) {
|
|
2171
2373
|
activeLines.push(i);
|
|
2172
2374
|
}
|
|
@@ -2206,6 +2408,8 @@ class AmLyrics extends i {
|
|
|
2206
2408
|
* Used by the template to always render gap elements in the DOM.
|
|
2207
2409
|
*/
|
|
2208
2410
|
findAllInstrumentalGaps() {
|
|
2411
|
+
if (this.cachedAllGaps.length > 0)
|
|
2412
|
+
return this.cachedAllGaps;
|
|
2209
2413
|
if (!this.lyrics || this.lyrics.length === 0)
|
|
2210
2414
|
return [];
|
|
2211
2415
|
const gaps = [];
|
|
@@ -2224,6 +2428,7 @@ class AmLyrics extends i {
|
|
|
2224
2428
|
gaps.push({ insertBeforeIndex: i + 1, gapStart, gapEnd });
|
|
2225
2429
|
}
|
|
2226
2430
|
}
|
|
2431
|
+
this.cachedAllGaps = gaps;
|
|
2227
2432
|
return gaps;
|
|
2228
2433
|
}
|
|
2229
2434
|
startAnimationFromTime(time) {
|
|
@@ -2387,7 +2592,7 @@ class AmLyrics extends i {
|
|
|
2387
2592
|
clearTimeout(this.userScrollTimeoutId);
|
|
2388
2593
|
this.userScrollTimeoutId = undefined;
|
|
2389
2594
|
}
|
|
2390
|
-
this.
|
|
2595
|
+
this.setUserScrolling(false);
|
|
2391
2596
|
// Reset active line tracking to prevent scroll fighting
|
|
2392
2597
|
this.currentPrimaryActiveLine = null;
|
|
2393
2598
|
this.lastPrimaryActiveLine = null;
|
|
@@ -2686,7 +2891,7 @@ class AmLyrics extends i {
|
|
|
2686
2891
|
}
|
|
2687
2892
|
this.lyricsContainer.classList.remove('not-focused', 'user-scrolling');
|
|
2688
2893
|
this.isProgrammaticScroll = true;
|
|
2689
|
-
this.
|
|
2894
|
+
this.setUserScrolling(false);
|
|
2690
2895
|
if (this.userScrollTimeoutId) {
|
|
2691
2896
|
clearTimeout(this.userScrollTimeoutId);
|
|
2692
2897
|
this.userScrollTimeoutId = undefined;
|
|
@@ -3174,9 +3379,7 @@ class AmLyrics extends i {
|
|
|
3174
3379
|
this.style.setProperty('--hover-background-color', this.hoverBackgroundColor);
|
|
3175
3380
|
this.style.setProperty('--highlight-color', this.highlightColor);
|
|
3176
3381
|
const sourceLabel = this.lyricsSource ?? 'Unavailable';
|
|
3177
|
-
const isUnsynced = this.
|
|
3178
|
-
? this.lyrics.every(l => l.timestamp === 0 && l.endtime === 0)
|
|
3179
|
-
: false;
|
|
3382
|
+
const isUnsynced = this.cachedIsUnsynced;
|
|
3180
3383
|
const renderContent = () => {
|
|
3181
3384
|
if (this.isLoading) {
|
|
3182
3385
|
// Render stylized skeleton lines
|
|
@@ -3248,88 +3451,16 @@ class AmLyrics extends i {
|
|
|
3248
3451
|
// as the main vocal, so we intentionally do NOT render a separate
|
|
3249
3452
|
// translation/romanization block for background — it would just duplicate
|
|
3250
3453
|
// the main line's text.
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
}
|
|
3262
|
-
}
|
|
3263
|
-
// Pre-compute isGrowable per "visual word": adjacent groups whose text
|
|
3264
|
-
// doesn't end with whitespace form one visual word (e.g. "a"+"live" = "alive").
|
|
3265
|
-
// We evaluate growable on the combined text/duration, then propagate
|
|
3266
|
-
// the result to each individual group so it renders through the
|
|
3267
|
-
// single-syllable path (which supports char-level glow).
|
|
3268
|
-
const groupGrowable = new Array(wordGroups.length).fill(false);
|
|
3269
|
-
const groupGlowing = new Array(wordGroups.length).fill(false);
|
|
3270
|
-
// Visual word info for growable char-level glow:
|
|
3271
|
-
// Each group stores the combined visual word's text, duration, and
|
|
3272
|
-
// the char offset of this group within the visual word.
|
|
3273
|
-
const vwFullText = new Array(wordGroups.length).fill('');
|
|
3274
|
-
const vwFullDuration = new Array(wordGroups.length).fill(0);
|
|
3275
|
-
const vwCharOffset = new Array(wordGroups.length).fill(0);
|
|
3276
|
-
const vwStartMs = new Array(wordGroups.length).fill(0);
|
|
3277
|
-
const vwEndMs = new Array(wordGroups.length).fill(0);
|
|
3278
|
-
{
|
|
3279
|
-
let vwStart = 0;
|
|
3280
|
-
while (vwStart < wordGroups.length) {
|
|
3281
|
-
let vwEnd = vwStart;
|
|
3282
|
-
while (vwEnd < wordGroups.length - 1) {
|
|
3283
|
-
const grp = wordGroups[vwEnd];
|
|
3284
|
-
const lastText = grp[grp.length - 1].text;
|
|
3285
|
-
if (/\s$/.test(lastText))
|
|
3286
|
-
break;
|
|
3287
|
-
vwEnd += 1;
|
|
3288
|
-
}
|
|
3289
|
-
// Compute combined properties for this visual word
|
|
3290
|
-
const combinedText = wordGroups
|
|
3291
|
-
.slice(vwStart, vwEnd + 1)
|
|
3292
|
-
.flatMap(g => g.map(s => s.text))
|
|
3293
|
-
.join('')
|
|
3294
|
-
.trim();
|
|
3295
|
-
const combinedStart = wordGroups[vwStart][0].timestamp;
|
|
3296
|
-
const lastGrp = wordGroups[vwEnd];
|
|
3297
|
-
const combinedEnd = lastGrp[lastGrp.length - 1].endtime;
|
|
3298
|
-
const combinedDuration = combinedEnd - combinedStart;
|
|
3299
|
-
const isCJK = /[\u4e00-\u9fff\u3040-\u309f\u30a0-\u30ff\uac00-\ud7af]/.test(combinedText);
|
|
3300
|
-
const isRTL = /[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\u0590-\u05FF]/.test(combinedText);
|
|
3301
|
-
const hasHyphen = combinedText.includes('-');
|
|
3302
|
-
const wordLen = combinedText.length;
|
|
3303
|
-
let isGrowableVW = !isCJK && !isRTL && !hasHyphen && wordLen > 0 && wordLen <= 12;
|
|
3304
|
-
if (isGrowableVW) {
|
|
3305
|
-
if (wordLen < 3) {
|
|
3306
|
-
isGrowableVW =
|
|
3307
|
-
combinedDuration >= 1110 && combinedDuration >= wordLen * 550;
|
|
3308
|
-
}
|
|
3309
|
-
else {
|
|
3310
|
-
isGrowableVW =
|
|
3311
|
-
combinedDuration >= 850 && combinedDuration >= wordLen * 200;
|
|
3312
|
-
}
|
|
3313
|
-
}
|
|
3314
|
-
// Glow requirement (more strict)
|
|
3315
|
-
const isGlowingVW = isGrowableVW &&
|
|
3316
|
-
combinedDuration >= 1000 &&
|
|
3317
|
-
combinedDuration >= combinedText.length * 250;
|
|
3318
|
-
let charOff = 0;
|
|
3319
|
-
for (let gi = vwStart; gi <= vwEnd; gi += 1) {
|
|
3320
|
-
groupGrowable[gi] = isGrowableVW;
|
|
3321
|
-
groupGlowing[gi] = isGlowingVW;
|
|
3322
|
-
vwFullText[gi] = combinedText;
|
|
3323
|
-
vwFullDuration[gi] = combinedDuration;
|
|
3324
|
-
vwCharOffset[gi] = charOff;
|
|
3325
|
-
vwStartMs[gi] = combinedStart;
|
|
3326
|
-
vwEndMs[gi] = combinedEnd;
|
|
3327
|
-
const grpText = wordGroups[gi].map(s => s.text).join('');
|
|
3328
|
-
charOff += grpText.replace(/\s/g, '').length;
|
|
3329
|
-
}
|
|
3330
|
-
vwStart = vwEnd + 1;
|
|
3331
|
-
}
|
|
3332
|
-
}
|
|
3454
|
+
const bgPlacement = hasBackground
|
|
3455
|
+
? AmLyrics.getBackgroundTextPlacement(line)
|
|
3456
|
+
: 'after';
|
|
3457
|
+
const lineData = this.cachedLineData?.[lineIndex];
|
|
3458
|
+
const wordGroups = lineData?.wordGroups ?? [];
|
|
3459
|
+
const groupGrowable = lineData?.groupGrowable ?? [];
|
|
3460
|
+
const groupGlowing = lineData?.groupGlowing ?? [];
|
|
3461
|
+
const vwFullText = lineData?.vwFullText ?? [];
|
|
3462
|
+
const vwFullDuration = lineData?.vwFullDuration ?? [];
|
|
3463
|
+
const vwCharOffset = lineData?.vwCharOffset ?? [];
|
|
3333
3464
|
// Create main vocals using YouLyPlus syllable structure
|
|
3334
3465
|
const mainVocalElement = b `<p class="main-vocal-container">
|
|
3335
3466
|
${wordGroups.map((group, groupIdx) => {
|
|
@@ -3552,7 +3683,9 @@ class AmLyrics extends i {
|
|
|
3552
3683
|
}}
|
|
3553
3684
|
>
|
|
3554
3685
|
<div class="lyrics-line-container">
|
|
3555
|
-
${
|
|
3686
|
+
${bgPlacement === 'before' ? backgroundVocalElement : ''}
|
|
3687
|
+
${mainVocalElement}
|
|
3688
|
+
${bgPlacement === 'after' ? backgroundVocalElement : ''}
|
|
3556
3689
|
${translationElement} ${lineRomanizationElement}
|
|
3557
3690
|
</div>
|
|
3558
3691
|
</div>
|
|
@@ -3563,9 +3696,7 @@ class AmLyrics extends i {
|
|
|
3563
3696
|
<div
|
|
3564
3697
|
class="lyrics-container ${isUnsynced
|
|
3565
3698
|
? 'is-unsynced'
|
|
3566
|
-
: 'blur-inactive-enabled'}
|
|
3567
|
-
? 'user-scrolling'
|
|
3568
|
-
: ''}"
|
|
3699
|
+
: 'blur-inactive-enabled'}"
|
|
3569
3700
|
>
|
|
3570
3701
|
${!this.isLoading && this.lyrics && this.lyrics.length > 0
|
|
3571
3702
|
? b `
|
|
@@ -3680,17 +3811,15 @@ class AmLyrics extends i {
|
|
|
3680
3811
|
!this.hasFetchedAllProviders
|
|
3681
3812
|
? b `
|
|
3682
3813
|
<button
|
|
3683
|
-
class="download-button"
|
|
3814
|
+
class="download-button source-switch-btn"
|
|
3684
3815
|
title="Switch Lyrics Source"
|
|
3685
3816
|
style="font-family: inherit; font-size: 11px; padding: 2px 6px; border-radius: 4px; border: 1px solid rgba(255, 255, 255, 0.2); background: transparent; cursor: pointer; color: #aaa; display: inline-flex; align-items: center;"
|
|
3686
3817
|
@click=${this.switchSource}
|
|
3687
3818
|
?disabled=${this.isFetchingAlternatives}
|
|
3688
3819
|
>
|
|
3689
3820
|
<svg
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
? 'animation: spin 1s linear infinite;'
|
|
3693
|
-
: ''}"
|
|
3821
|
+
class="source-switch-svg lucide lucide-arrow-down-up-icon lucide-arrow-down-up"
|
|
3822
|
+
style="margin-right: 4px;"
|
|
3694
3823
|
xmlns="http://www.w3.org/2000/svg"
|
|
3695
3824
|
width="12"
|
|
3696
3825
|
height="12"
|
|
@@ -3700,7 +3829,6 @@ class AmLyrics extends i {
|
|
|
3700
3829
|
stroke-width="2"
|
|
3701
3830
|
stroke-linecap="round"
|
|
3702
3831
|
stroke-linejoin="round"
|
|
3703
|
-
class="lucide lucide-arrow-down-up-icon lucide-arrow-down-up"
|
|
3704
3832
|
>
|
|
3705
3833
|
${this.isFetchingAlternatives
|
|
3706
3834
|
? w `<path
|
|
@@ -3711,9 +3839,11 @@ class AmLyrics extends i {
|
|
|
3711
3839
|
><path d="m21 8-4-4-4 4"></path
|
|
3712
3840
|
><path d="M17 4v16"></path>`}
|
|
3713
3841
|
</svg>
|
|
3714
|
-
|
|
3842
|
+
<span class="source-switch-label"
|
|
3843
|
+
>${this.isFetchingAlternatives
|
|
3715
3844
|
? 'Switching...'
|
|
3716
|
-
: 'Switch'}
|
|
3845
|
+
: 'Switch'}</span
|
|
3846
|
+
>
|
|
3717
3847
|
</button>
|
|
3718
3848
|
`
|
|
3719
3849
|
: ''}
|
|
@@ -4998,18 +5128,9 @@ __decorate([
|
|
|
4998
5128
|
__decorate([
|
|
4999
5129
|
r()
|
|
5000
5130
|
], AmLyrics.prototype, "currentSourceIndex", void 0);
|
|
5001
|
-
__decorate([
|
|
5002
|
-
r()
|
|
5003
|
-
], AmLyrics.prototype, "isFetchingAlternatives", void 0);
|
|
5004
|
-
__decorate([
|
|
5005
|
-
r()
|
|
5006
|
-
], AmLyrics.prototype, "hasFetchedAllProviders", void 0);
|
|
5007
5131
|
__decorate([
|
|
5008
5132
|
e('.lyrics-container')
|
|
5009
5133
|
], AmLyrics.prototype, "lyricsContainer", void 0);
|
|
5010
|
-
__decorate([
|
|
5011
|
-
r()
|
|
5012
|
-
], AmLyrics.prototype, "isUserScrolling", void 0);
|
|
5013
5134
|
|
|
5014
5135
|
window.customElements.define('am-lyrics', AmLyrics);
|
|
5015
5136
|
//# sourceMappingURL=am-lyrics.js.map
|