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