@uimaxbai/am-lyrics 1.1.2 → 1.1.5

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.
@@ -85,6 +85,8 @@ export declare class AmLyrics extends LitElement {
85
85
  */
86
86
  private static fetchLyricsFromLrclib;
87
87
  private static fetchLyricsFromGenius;
88
+ private static calculateLineAlignments;
89
+ private static parseTTML;
88
90
  private static convertKPoeLyrics;
89
91
  private static toMilliseconds;
90
92
  firstUpdated(): void;
@@ -1 +1 @@
1
- {"version":3,"file":"AmLyrics.d.ts","sourceRoot":"","sources":["../../src/AmLyrics.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"AmLyrics.d.ts","sourceRoot":"","sources":["../../src/AmLyrics.ts"],"names":[],"mappings":"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"}
@@ -309,7 +309,7 @@ class GoogleService {
309
309
  }
310
310
  }
311
311
 
312
- const VERSION = '1.1.2';
312
+ const VERSION = '1.1.5';
313
313
  const INSTRUMENTAL_THRESHOLD_MS = 7000; // Show dots for gaps >= 7s
314
314
  const KPOE_SERVERS = [
315
315
  'https://lyricsplus.binimum.org',
@@ -851,6 +851,82 @@ class AmLyrics extends i {
851
851
  return 10;
852
852
  };
853
853
  const allResults = [];
854
+ // Try cache API first
855
+ try {
856
+ const cacheParams = new URLSearchParams({
857
+ track: title,
858
+ artist,
859
+ });
860
+ if (metadata.album) {
861
+ cacheParams.append('album', metadata.album);
862
+ }
863
+ if (metadata.durationMs && metadata.durationMs > 0) {
864
+ cacheParams.append('duration', Math.round(metadata.durationMs / 1000).toString());
865
+ }
866
+ const cacheUrl = `https://lyrics-api.binimum.org/?${cacheParams.toString()}`;
867
+ const cacheRes = await fetch(cacheUrl);
868
+ if (cacheRes.ok) {
869
+ const cacheData = await cacheRes.json();
870
+ if (cacheData.results && cacheData.results.length > 0) {
871
+ const result = cacheData.results[0];
872
+ if (result.timing_type === 'word' && result.lyricsUrl) {
873
+ const ttmlRes = await fetch(result.lyricsUrl);
874
+ if (ttmlRes.ok) {
875
+ const ttmlText = await ttmlRes.text();
876
+ const lines = AmLyrics.parseTTML(ttmlText);
877
+ if (lines && lines.length > 0) {
878
+ allResults.push({ lines, source: 'BiniLyrics' });
879
+ return allResults;
880
+ }
881
+ }
882
+ }
883
+ else {
884
+ // Not word type, try fetching any word synced lyrics from lyricsplus
885
+ const fallbackParams = new URLSearchParams(params);
886
+ const fallbackUrl = `https://lyricsplus.binimum.org/v2/lyrics/get?${fallbackParams.toString()}`;
887
+ try {
888
+ const fallbackRes = await fetch(fallbackUrl);
889
+ if (fallbackRes.ok) {
890
+ const payload = await fallbackRes.json();
891
+ const lines = AmLyrics.convertKPoeLyrics(payload);
892
+ const hasWordSync = lines?.some((line) => line.text &&
893
+ Array.isArray(line.text) &&
894
+ line.text.length > 1);
895
+ if (lines && lines.length > 0 && hasWordSync) {
896
+ const sourceLabel = payload?.metadata?.source ||
897
+ payload?.metadata?.provider ||
898
+ 'LyricsPlus (KPoe)';
899
+ allResults.push({ lines, source: sourceLabel });
900
+ return allResults;
901
+ }
902
+ }
903
+ }
904
+ catch (fallbackError) {
905
+ // Ignore fallback fetch error
906
+ }
907
+ // If fallback fails or has no word sync, fall back to bini lyrics
908
+ if (result.lyricsUrl) {
909
+ const ttmlRes = await fetch(result.lyricsUrl);
910
+ if (ttmlRes.ok) {
911
+ const ttmlText = await ttmlRes.text();
912
+ const lines = AmLyrics.parseTTML(ttmlText);
913
+ if (lines && lines.length > 0) {
914
+ allResults.push({
915
+ lines,
916
+ source: 'BiniLyrics',
917
+ });
918
+ return allResults;
919
+ }
920
+ }
921
+ }
922
+ }
923
+ }
924
+ }
925
+ }
926
+ catch (e) {
927
+ // eslint-disable-next-line no-console
928
+ console.error('Cache API failed', e);
929
+ }
854
930
  // Shuffle servers so we pick a random one first, with all others as fallback
855
931
  // Limit to 2 servers to prevent unnecessary API spam when Apple lyrics are missing
856
932
  const shuffledServers = [...KPOE_SERVERS]
@@ -887,14 +963,13 @@ class AmLyrics extends i {
887
963
  }
888
964
  }
889
965
  }
890
- // If we haven't found a completely synced Apple/QQ result (rank 1 or 2) among the servers,
891
- // force an explicit query against lyricsplus.binimum.org looking for QQ
966
+ // If we haven't found a completely synced result (rank 1 or 2) among the servers,
967
+ // force an explicit query against lyricsplus.binimum.org looking for word lyrics
892
968
  const hasHighRankResult = allResults.some(r => getRank(r.source, r.lines) <= 2);
893
969
  if (!hasHighRankResult) {
894
970
  try {
895
- const qqParams = new URLSearchParams(params);
896
- qqParams.set('source', 'qq');
897
- const url = `https://lyricsplus.binimum.org/v2/lyrics/get?${qqParams.toString()}`;
971
+ const fallbackParams = new URLSearchParams(params);
972
+ const url = `https://lyricsplus.binimum.org/v2/lyrics/get?${fallbackParams.toString()}`;
898
973
  const response = await fetch(url);
899
974
  if (response.ok) {
900
975
  const payload = await response.json();
@@ -903,14 +978,15 @@ class AmLyrics extends i {
903
978
  const sourceLabel = payload?.metadata?.source ||
904
979
  payload?.metadata?.provider ||
905
980
  'LyricsPlus (KPoe)';
906
- if (lines && lines.length > 0) {
981
+ const hasWordSync = lines?.some((line) => line.text && Array.isArray(line.text) && line.text.length > 1);
982
+ if (lines && lines.length > 0 && hasWordSync) {
907
983
  allResults.push({ lines, source: sourceLabel });
908
984
  }
909
985
  }
910
986
  }
911
987
  }
912
988
  catch (error) {
913
- // Explicit QQ fallback failed, ignore
989
+ // Explicit fallback failed, ignore
914
990
  }
915
991
  }
916
992
  return allResults;
@@ -930,6 +1006,7 @@ class AmLyrics extends i {
930
1006
  if (!match) {
931
1007
  // Skip non-timestamped lines (headers like [ti:], [ar:], etc.)
932
1008
  // eslint-disable-next-line no-continue
1009
+ // eslint-disable-next-line no-continue
933
1010
  continue;
934
1011
  }
935
1012
  const minutes = parseInt(match[1], 10);
@@ -949,6 +1026,7 @@ class AmLyrics extends i {
949
1026
  const endtime = i + 1 < parsed.length ? parsed[i + 1].timestamp : timestamp + 5000;
950
1027
  // Skip empty lines (instrumental gaps)
951
1028
  if (!text.trim()) {
1029
+ // eslint-disable-next-line no-continue
952
1030
  // eslint-disable-next-line no-continue
953
1031
  continue;
954
1032
  }
@@ -992,6 +1070,7 @@ class AmLyrics extends i {
992
1070
  // eslint-disable-next-line no-await-in-loop
993
1071
  const searchResponse = await fetch(`${normalizedBase}/search/?${searchParams.toString()}`);
994
1072
  if (!searchResponse.ok) {
1073
+ // eslint-disable-next-line no-continue
995
1074
  // eslint-disable-next-line no-continue
996
1075
  continue;
997
1076
  }
@@ -999,6 +1078,7 @@ class AmLyrics extends i {
999
1078
  const searchData = await searchResponse.json();
1000
1079
  const items = searchData?.data?.items;
1001
1080
  if (!Array.isArray(items) || items.length === 0) {
1081
+ // eslint-disable-next-line no-continue
1002
1082
  // eslint-disable-next-line no-continue
1003
1083
  continue;
1004
1084
  }
@@ -1012,6 +1092,7 @@ class AmLyrics extends i {
1012
1092
  }
1013
1093
  const trackId = bestTrack?.id;
1014
1094
  if (!trackId) {
1095
+ // eslint-disable-next-line no-continue
1015
1096
  // eslint-disable-next-line no-continue
1016
1097
  continue;
1017
1098
  }
@@ -1019,6 +1100,7 @@ class AmLyrics extends i {
1019
1100
  // eslint-disable-next-line no-await-in-loop
1020
1101
  const lyricsResponse = await fetch(`${normalizedBase}/lyrics/?id=${trackId}`);
1021
1102
  if (!lyricsResponse.ok) {
1103
+ // eslint-disable-next-line no-continue
1022
1104
  // eslint-disable-next-line no-continue
1023
1105
  continue;
1024
1106
  }
@@ -1143,10 +1225,230 @@ class AmLyrics extends i {
1143
1225
  }
1144
1226
  }
1145
1227
  catch {
1228
+ // eslint-disable-next-line no-console
1146
1229
  console.error('No Genius lyrics found');
1147
1230
  }
1148
1231
  return null;
1149
1232
  }
1233
+ static calculateLineAlignments(lineSingers, agentTypes) {
1234
+ const lineSideAssignments = new Array(lineSingers.length).fill(undefined);
1235
+ let currentSideIsLeft = true;
1236
+ let lastPersonSingerId = null;
1237
+ let rightCount = 0;
1238
+ let totalCount = 0;
1239
+ lineSingers.forEach((singerId, index) => {
1240
+ let sideClass;
1241
+ if (singerId) {
1242
+ let type = agentTypes[singerId];
1243
+ if (!type) {
1244
+ if (singerId === 'v1000') {
1245
+ type = 'group';
1246
+ }
1247
+ else if (singerId === 'v2000') {
1248
+ type = 'other';
1249
+ }
1250
+ else {
1251
+ type = 'person';
1252
+ }
1253
+ }
1254
+ if (type === 'group') {
1255
+ sideClass = 'start';
1256
+ }
1257
+ else {
1258
+ if (lastPersonSingerId === null) {
1259
+ if (type === 'other') {
1260
+ currentSideIsLeft = false;
1261
+ }
1262
+ else {
1263
+ currentSideIsLeft = true;
1264
+ }
1265
+ }
1266
+ else if (singerId !== lastPersonSingerId) {
1267
+ currentSideIsLeft = !currentSideIsLeft;
1268
+ }
1269
+ sideClass = currentSideIsLeft ? 'start' : 'end';
1270
+ lastPersonSingerId = singerId;
1271
+ }
1272
+ }
1273
+ if (sideClass) {
1274
+ totalCount += 1;
1275
+ if (sideClass === 'end')
1276
+ rightCount += 1;
1277
+ }
1278
+ lineSideAssignments[index] = sideClass;
1279
+ });
1280
+ if (totalCount > 0 && Math.round((rightCount / totalCount) * 100) >= 85) {
1281
+ const flip = (s) => {
1282
+ if (s === 'start')
1283
+ return 'end';
1284
+ if (s === 'end')
1285
+ return 'start';
1286
+ return s;
1287
+ };
1288
+ for (let i = 0; i < lineSideAssignments.length; i += 1) {
1289
+ lineSideAssignments[i] = flip(lineSideAssignments[i]);
1290
+ }
1291
+ }
1292
+ return lineSideAssignments;
1293
+ }
1294
+ static parseTTML(ttmlString) {
1295
+ try {
1296
+ const parser = new DOMParser();
1297
+ const doc = parser.parseFromString(ttmlString, 'text/xml');
1298
+ const translations = {};
1299
+ const transliterations = {};
1300
+ const agentMap = {};
1301
+ const agents = doc.getElementsByTagName('ttm:agent');
1302
+ for (let i = 0; i < agents.length; i += 1) {
1303
+ const agent = agents[i];
1304
+ const id = agent.getAttribute('xml:id');
1305
+ const type = agent.getAttribute('type');
1306
+ if (id && type) {
1307
+ agentMap[id] = type;
1308
+ }
1309
+ }
1310
+ const translationNodes = doc.getElementsByTagName('translation');
1311
+ for (let i = 0; i < translationNodes.length; i += 1) {
1312
+ const texts = translationNodes[i].getElementsByTagName('text');
1313
+ for (let j = 0; j < texts.length; j += 1) {
1314
+ const textNode = texts[j];
1315
+ const key = textNode.getAttribute('for');
1316
+ if (key && textNode.textContent) {
1317
+ translations[key] = textNode.textContent;
1318
+ }
1319
+ }
1320
+ }
1321
+ const transliterationNodes = doc.getElementsByTagName('transliteration');
1322
+ for (let i = 0; i < transliterationNodes.length; i += 1) {
1323
+ const texts = transliterationNodes[i].getElementsByTagName('text');
1324
+ for (let j = 0; j < texts.length; j += 1) {
1325
+ const textNode = texts[j];
1326
+ const key = textNode.getAttribute('for');
1327
+ if (key && textNode.textContent) {
1328
+ transliterations[key] = textNode.textContent
1329
+ .trim()
1330
+ .replace(/\s+/g, ' ');
1331
+ }
1332
+ }
1333
+ }
1334
+ const timeToMs = (timeStr) => {
1335
+ if (!timeStr)
1336
+ return 0;
1337
+ const parts = timeStr.split(':');
1338
+ let seconds = 0;
1339
+ if (parts.length === 2) {
1340
+ seconds = parseInt(parts[0], 10) * 60 + parseFloat(parts[1]);
1341
+ }
1342
+ else if (parts.length === 3) {
1343
+ seconds =
1344
+ parseInt(parts[0], 10) * 3600 +
1345
+ parseInt(parts[1], 10) * 60 +
1346
+ parseFloat(parts[2]);
1347
+ }
1348
+ else {
1349
+ seconds = parseFloat(parts[0]);
1350
+ }
1351
+ return Math.round(seconds * 1000);
1352
+ };
1353
+ const lines = [];
1354
+ const pNodes = doc.getElementsByTagName('p');
1355
+ const lineSingers = [];
1356
+ for (let i = 0; i < pNodes.length; i += 1) {
1357
+ lineSingers.push(pNodes[i].getAttribute('ttm:agent') || undefined);
1358
+ }
1359
+ const alignments = AmLyrics.calculateLineAlignments(lineSingers, agentMap);
1360
+ for (let i = 0; i < pNodes.length; i += 1) {
1361
+ const p = pNodes[i];
1362
+ const key = p.getAttribute('itunes:key');
1363
+ const beginMs = timeToMs(p.getAttribute('begin'));
1364
+ const endMs = timeToMs(p.getAttribute('end'));
1365
+ let songPart;
1366
+ if (p.parentNode && p.parentNode.tagName === 'div') {
1367
+ songPart =
1368
+ p.parentNode.getAttribute('itunes:songPart') ||
1369
+ undefined;
1370
+ }
1371
+ const mainSyllables = [];
1372
+ const bgSyllables = [];
1373
+ const spans = p.getElementsByTagName('span');
1374
+ if (spans.length > 0) {
1375
+ for (let j = 0; j < spans.length; j += 1) {
1376
+ const span = spans[j];
1377
+ if (span.getAttribute('ttm:role') === 'x-bg') {
1378
+ const bgInnerSpans = span.getElementsByTagName('span');
1379
+ for (let k = 0; k < bgInnerSpans.length; k += 1) {
1380
+ const bgSpan = bgInnerSpans[k];
1381
+ let bgText = bgSpan.textContent || '';
1382
+ const nextNode = bgSpan.nextSibling;
1383
+ if (nextNode &&
1384
+ nextNode.nodeType === 3 &&
1385
+ /^\s/.test(nextNode.textContent || '') &&
1386
+ !bgText.endsWith(' ')) {
1387
+ bgText += ' ';
1388
+ }
1389
+ bgSyllables.push({
1390
+ text: bgText,
1391
+ timestamp: timeToMs(bgSpan.getAttribute('begin')),
1392
+ endtime: timeToMs(bgSpan.getAttribute('end')),
1393
+ part: false,
1394
+ });
1395
+ }
1396
+ // eslint-disable-next-line no-continue
1397
+ continue;
1398
+ }
1399
+ if (span.parentNode &&
1400
+ span.parentNode.getAttribute?.('ttm:role') === 'x-bg') {
1401
+ // eslint-disable-next-line no-continue
1402
+ continue;
1403
+ }
1404
+ let text = span.textContent || '';
1405
+ const nextNode = span.nextSibling;
1406
+ if (nextNode &&
1407
+ nextNode.nodeType === 3 &&
1408
+ /^\s/.test(nextNode.textContent || '') &&
1409
+ !text.endsWith(' ')) {
1410
+ text += ' ';
1411
+ }
1412
+ mainSyllables.push({
1413
+ text,
1414
+ timestamp: timeToMs(span.getAttribute('begin')),
1415
+ endtime: timeToMs(span.getAttribute('end')),
1416
+ part: false,
1417
+ });
1418
+ }
1419
+ }
1420
+ else {
1421
+ mainSyllables.push({
1422
+ text: p.textContent?.trim() || '',
1423
+ timestamp: beginMs,
1424
+ endtime: endMs,
1425
+ part: false,
1426
+ lineSynced: true,
1427
+ });
1428
+ }
1429
+ const alignment = alignments[i];
1430
+ lines.push({
1431
+ text: mainSyllables,
1432
+ background: bgSyllables.length > 0,
1433
+ backgroundText: bgSyllables,
1434
+ timestamp: beginMs,
1435
+ endtime: endMs,
1436
+ isWordSynced: spans.length > 0,
1437
+ alignment,
1438
+ songPart,
1439
+ translation: key ? translations[key] : undefined,
1440
+ romanizedText: key ? transliterations[key] : undefined,
1441
+ oppositeTurn: alignment === 'end',
1442
+ });
1443
+ }
1444
+ return lines;
1445
+ }
1446
+ catch (e) {
1447
+ // eslint-disable-next-line no-console
1448
+ console.error('Failed to parse TTML', e);
1449
+ return null;
1450
+ }
1451
+ }
1150
1452
  static convertKPoeLyrics(payload) {
1151
1453
  if (!payload) {
1152
1454
  return null;
@@ -1168,45 +1470,21 @@ class AmLyrics extends i {
1168
1470
  const lines = [];
1169
1471
  // If type is 'Line', we revert to line-by-line highlighting by skipping syllabus parsing
1170
1472
  const isLineType = payload.type === 'Line' || payload.type === 'line';
1171
- // Convert metadata.agents to alignment map
1172
- const agents = payload.metadata?.agents ?? {};
1173
- const agentEntries = Object.entries(agents);
1174
- const singerAlignmentMap = {};
1175
- if (agentEntries.length > 0) {
1176
- agentEntries.sort((a, b) => a[0].localeCompare(b[0]));
1177
- const personAgents = agentEntries.filter(
1178
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
1179
- ([_, agentData]) => agentData.type === 'person');
1180
- const personIndexMap = new Map();
1181
- personAgents.forEach(([agentKey], personIndex) => {
1182
- personIndexMap.set(agentKey, personIndex);
1183
- });
1184
- agentEntries.forEach(([agentKey, agentData]) => {
1185
- const mappedKey = agentData.alias || agentKey;
1186
- if (agentData.type === 'group') {
1187
- singerAlignmentMap[mappedKey] = 'start';
1188
- }
1189
- else if (agentData.type === 'other') {
1190
- singerAlignmentMap[mappedKey] = 'end';
1191
- }
1192
- else if (agentData.type === 'person') {
1193
- const personIndex = personIndexMap.get(agentKey);
1194
- if (personIndex !== undefined) {
1195
- singerAlignmentMap[mappedKey] =
1196
- personIndex % 2 === 0 ? 'start' : 'end';
1197
- }
1198
- }
1473
+ // Convert metadata.agents to type map
1474
+ const agentTypes = {};
1475
+ if (payload.metadata?.agents) {
1476
+ Object.entries(payload.metadata.agents).forEach(([key, agent]) => {
1477
+ const mappedKey = agent.alias || key;
1478
+ agentTypes[mappedKey] = agent.type;
1199
1479
  });
1200
1480
  }
1201
- for (const entry of sanitizedEntries) {
1481
+ const lineSingers = sanitizedEntries.map((entry) => entry.element?.singer);
1482
+ const alignments = AmLyrics.calculateLineAlignments(lineSingers, agentTypes);
1483
+ for (let i = 0; i < sanitizedEntries.length; i += 1) {
1484
+ const entry = sanitizedEntries[i];
1202
1485
  const start = AmLyrics.toMilliseconds(entry.time);
1203
1486
  const duration = AmLyrics.toMilliseconds(entry.duration);
1204
- // Determine alignment
1205
- let alignment;
1206
- const singerId = entry.element?.singer;
1207
- if (singerId && singerAlignmentMap[singerId]) {
1208
- alignment = singerAlignmentMap[singerId];
1209
- }
1487
+ const alignment = alignments[i];
1210
1488
  const lineText = typeof entry.text === 'string' ? entry.text : '';
1211
1489
  const lineStart = AmLyrics.toMilliseconds(entry.time);
1212
1490
  const lineDuration = AmLyrics.toMilliseconds(entry.duration);
@@ -1257,10 +1535,8 @@ class AmLyrics extends i {
1257
1535
  // If syllabus data matches, map it to main syllables
1258
1536
  if (Array.isArray(transliteration.syllabus) &&
1259
1537
  transliteration.syllabus.length === mainSyllables.length) {
1260
- transliteration.syllabus.forEach((s, i) => {
1261
- if (mainSyllables[i]) {
1262
- mainSyllables[i].romanizedText = s.text;
1263
- }
1538
+ transliteration.syllabus.forEach((s, idx) => {
1539
+ mainSyllables[idx].romanizedText = s.text;
1264
1540
  });
1265
1541
  }
1266
1542
  }
@@ -1270,10 +1546,11 @@ class AmLyrics extends i {
1270
1546
  text: mainSyllables,
1271
1547
  background: backgroundSyllables.length > 0,
1272
1548
  backgroundText: backgroundSyllables,
1273
- oppositeTurn: Array.isArray(entry.element)
1274
- ? entry.element.includes('opposite') ||
1275
- entry.element.includes('right')
1276
- : false,
1549
+ oppositeTurn: alignment === 'end' ||
1550
+ (Array.isArray(entry.element)
1551
+ ? entry.element.includes('opposite') ||
1552
+ entry.element.includes('right')
1553
+ : false),
1277
1554
  timestamp: lineStart,
1278
1555
  endtime: start + duration,
1279
1556
  isWordSynced: isLineType ? false : hasWordSync,
@@ -1386,15 +1663,32 @@ class AmLyrics extends i {
1386
1663
  // Entering gap: remove any leftover exit state, add active
1387
1664
  gap.classList.remove('gap-exiting');
1388
1665
  gap.classList.add('active');
1389
- // Mark any dots whose time has already passed as finished
1390
- // (prevents skipping the first dot when lyrics load mid-gap)
1666
+ // Mark dots whose time has already passed as finished, and
1667
+ // trigger highlight on the dot currently in its time window
1668
+ // so the first dot always lights up even on late load.
1391
1669
  const dotSyllables = gap.querySelectorAll('.lyrics-syllable');
1392
1670
  dotSyllables.forEach(dot => {
1671
+ const dotStart = parseFloat(dot.getAttribute('data-start-time') || '0');
1393
1672
  const dotEnd = parseFloat(dot.getAttribute('data-end-time') || '0');
1394
1673
  if (newTime > dotEnd) {
1395
1674
  dot.classList.add('finished');
1675
+ // Also ensure the highlight + animation fired so CSS state is correct
1676
+ if (!dot.classList.contains('highlight')) {
1677
+ AmLyrics.updateSyllableAnimation(dot);
1678
+ }
1679
+ }
1680
+ else if (newTime >= dotStart && newTime <= dotEnd) {
1681
+ // Currently within this dot's window — trigger its highlight
1682
+ AmLyrics.updateSyllableAnimation(dot);
1396
1683
  }
1397
1684
  });
1685
+ // Scroll to the gap element so dots animate in with
1686
+ // the staggered scroll rather than popping in.
1687
+ if (this.autoScroll &&
1688
+ !this.isUserScrolling &&
1689
+ !this.isClickSeeking) {
1690
+ this.scrollToActiveLineYouLy(gap);
1691
+ }
1398
1692
  }
1399
1693
  else if (shouldStartExiting) {
1400
1694
  // Exiting gap: keep visible while dots animate out