sonygod commited on
Commit
cac2dd6
·
1 Parent(s): a55955d
Files changed (1) hide show
  1. youtube_sub.js +160 -137
youtube_sub.js CHANGED
@@ -20,131 +20,153 @@ GM_addStyle(`
20
 
21
  // 增强的样式定义,添加过渡效果
22
  const styles = `
23
- .subtitle-manager {
24
- position: fixed;
25
- right: 0;
26
- top: 60px;
27
- width: 300px;
28
- height: calc(100vh - 60px);
29
- background: rgba(33, 33, 33, 0.95);
30
- color: white;
31
- z-index: 9999;
32
- font-family: Arial, sans-serif;
33
- display: flex;
34
- flex-direction: column;
35
- transition: transform 0.3s ease;
36
- }
37
 
38
- .subtitle-manager.collapsed {
39
- transform: translateX(290px);
40
- }
41
 
42
- .subtitle-toggle {
43
- position: absolute;
44
- left: -30px;
45
- top: 10px;
46
- width: 30px;
47
- height: 30px;
48
- background: rgba(33, 33, 33, 0.95);
49
- border: none;
50
- color: white;
51
- cursor: pointer;
52
- display: flex;
53
- align-items: center;
54
- justify-content: center;
55
- border-radius: 4px 0 0 4px;
56
- }
57
 
58
- .subtitle-tabs {
59
- display: flex;
60
- border-bottom: 1px solid #444;
61
- }
62
 
63
- .subtitle-tab {
64
- flex: 1;
65
- padding: 10px;
66
- text-align: center;
67
- cursor: pointer;
68
- background: transparent;
69
- border: none;
70
- color: white;
71
- transition: background-color 0.2s ease;
72
- }
 
73
 
74
- .subtitle-tab.active {
75
- background: #444;
76
- }
 
77
 
78
- .subtitle-content {
79
- flex: 1;
80
- overflow-y: auto;
81
- padding: 10px;
82
- transition: opacity 0.3s ease;
83
- }
84
 
85
- .subtitle-line {
86
- padding: 8px;
87
- margin: 4px 0;
88
- cursor: pointer;
89
- border-radius: 4px;
90
- transition: background-color 0.2s ease, transform 0.1s ease;
91
- position: relative;
92
- }
 
 
 
 
 
93
 
94
- .subtitle-line:hover {
95
- background: rgba(255, 255, 255, 0.1);
96
- transform: translateX(5px);
97
- }
98
 
99
- .subtitle-line.active {
100
- background: rgba(255, 255, 255, 0.2);
101
- }
102
 
103
- .mixed-line {
104
- display: flex;
105
- flex-direction: column;
106
- gap: 8px;
107
- padding: 12px 8px;
108
- }
109
 
110
- .en-text {
111
- color: #fff;
112
- font-size: 14px;
113
- }
 
 
 
114
 
115
- .zh-text {
116
- color: #aaa;
117
- font-size: 14px;
118
- }
 
 
 
 
 
 
119
 
120
- .timestamp {
121
- position: absolute;
122
- right: 8px;
123
- top: 4px;
124
- font-size: 10px;
125
- color: #888;
126
- opacity: 0;
127
- transition: opacity 0.2s ease;
128
- }
 
 
 
129
 
130
- .subtitle-line:hover .timestamp {
131
- opacity: 1;
132
- }
133
 
134
- /* 自定义滚动条样式 */
135
- .subtitle-content::-webkit-scrollbar {
136
- width: 8px;
137
- }
138
 
139
- .subtitle-content::-webkit-scrollbar-track {
140
- background: rgba(255, 255, 255, 0.1);
141
- }
142
 
143
- .subtitle-content::-webkit-scrollbar-thumb {
144
- background: rgba(255, 255, 255, 0.3);
145
- border-radius: 4px;
146
- }
147
- `;
 
 
 
 
148
 
149
  // Apply main styles
150
  GM_addStyle(styles);
@@ -280,7 +302,7 @@ GM_addStyle(`
280
  return tracks;
281
  }
282
 
283
- // Method 2: Get from player config
284
  if (player.getPlayerResponse) {
285
  const response = await player.getPlayerResponse();
286
  debug.log('Player response:', response);
@@ -320,26 +342,26 @@ GM_addStyle(`
320
  async loadSubtitles(track) {
321
  const MAX_RETRIES = 3;
322
  let retries = 0;
323
-
324
  while (retries < MAX_RETRIES) {
325
  try {
326
  // Use baseUrl instead of src
327
  const url = new URL(track.baseUrl);
328
  // Add required params for JSON format
329
  url.searchParams.set('fmt', 'json3');
330
-
331
  const response = await fetch(url.toString());
332
  if (!response.ok) {
333
  throw new Error(`HTTP error: ${response.status}`);
334
  }
335
-
336
  const data = await response.json();
337
  debug.log('Subtitle data:', data);
338
-
339
  if (!data?.events) {
340
  throw new Error('Invalid subtitle data format');
341
  }
342
-
343
  const subtitles = data.events
344
  .filter(event => event.segs && event.tStartMs !== undefined)
345
  .map(event => ({
@@ -348,10 +370,10 @@ GM_addStyle(`
348
  text: event.segs.map(seg => seg.utf8).join('').trim()
349
  }))
350
  .filter(sub => sub.text);
351
-
352
  debug.log(`Loaded ${subtitles.length} subtitles`);
353
  return subtitles;
354
-
355
  } catch (error) {
356
  debug.error(`Subtitle load attempt ${retries + 1} failed:`, error);
357
  retries++;
@@ -613,24 +635,25 @@ GM_addStyle(`
613
  this.setupVideoTimeUpdate();
614
  }
615
 
 
616
  setupVideoTimeUpdate() {
617
  let rafId = null;
618
  const updateThreshold = 16; // ~60fps
619
  let lastUpdate = 0;
620
-
621
  const findSubtitleIndex = (time, subtitles) => {
622
  // Binary search for faster lookup
623
  let start = 0;
624
  let end = subtitles.length - 1;
625
-
626
  while (start <= end) {
627
  const mid = Math.floor((start + end) / 2);
628
  const sub = subtitles[mid];
629
-
630
  if (time >= sub.startTime && time <= sub.endTime) {
631
  return mid;
632
  }
633
-
634
  if (time < sub.startTime) {
635
  end = mid - 1;
636
  } else {
@@ -639,7 +662,7 @@ GM_addStyle(`
639
  }
640
  return -1;
641
  };
642
-
643
  const handleTimeUpdate = (video) => {
644
  const now = performance.now();
645
  if (now - lastUpdate < updateThreshold) {
@@ -647,24 +670,24 @@ GM_addStyle(`
647
  return;
648
  }
649
  lastUpdate = now;
650
-
651
  const currentTime = video.currentTime;
652
- const subtitles = this.currentTab === 'chinese' ?
653
  this.subtitles.chinese : this.subtitles.english;
654
-
655
  const newIndex = findSubtitleIndex(currentTime, subtitles);
656
  if (newIndex === this.subtitleIndex) {
657
  rafId = requestAnimationFrame(() => handleTimeUpdate(video));
658
  return;
659
  }
660
-
661
  this.subtitleIndex = newIndex;
662
  const lines = this.container.querySelectorAll('.subtitle-line');
663
-
664
  lines.forEach(line => {
665
  const lineIndex = parseInt(line.dataset.index);
666
  const isActive = lineIndex === newIndex;
667
-
668
  line.classList.toggle('active', isActive);
669
  if (isActive) {
670
  line.scrollIntoView({
@@ -673,26 +696,26 @@ GM_addStyle(`
673
  });
674
  }
675
  });
676
-
677
  rafId = requestAnimationFrame(() => handleTimeUpdate(video));
678
  };
679
-
680
  const setupVideoListener = async () => {
681
  const video = await this.waitForElement('video');
682
  if (video) {
683
  handleTimeUpdate(video);
684
-
685
  video.addEventListener('pause', () => {
686
  cancelAnimationFrame(rafId);
687
  });
688
-
689
  video.addEventListener('play', () => {
690
  lastUpdate = 0;
691
  handleTimeUpdate(video);
692
  });
693
  }
694
  };
695
-
696
  setupVideoListener();
697
  }
698
 
@@ -737,7 +760,7 @@ GM_addStyle(`
737
 
738
  //enter debug mode
739
 
740
- //debugger;
741
 
742
  // 3. Load subtitle data
743
  for (const track of tracks) {
@@ -762,20 +785,20 @@ GM_addStyle(`
762
  updateSubtitleDisplay() {
763
  const content = this.container.querySelector('.subtitle-content');
764
  content.style.opacity = '0';
765
-
766
  setTimeout(() => {
767
  // Clear content safely
768
  while (content.firstChild) {
769
  content.removeChild(content.firstChild);
770
  }
771
-
772
  // Check for empty subtitles
773
  if (!this.subtitles.english.length && !this.subtitles.chinese.length) {
774
  content.appendChild(this.createMessageElement('No subtitles available'));
775
  content.style.opacity = '1';
776
  return;
777
  }
778
-
779
  // Display based on current tab
780
  switch (this.currentTab) {
781
  case 'english':
@@ -800,7 +823,7 @@ GM_addStyle(`
800
  }
801
  break;
802
  }
803
-
804
  // Fade in
805
  content.style.opacity = '1';
806
  }, 300);
 
20
 
21
  // 增强的样式定义,添加过渡效果
22
  const styles = `
23
+ .subtitle-manager {
24
+ position: fixed;
25
+ right: 0;
26
+ top: 60px;
27
+ width: 450px;
28
+ height: calc(100vh - 60px);
29
+ background: rgba(33, 33, 33, 0.95);
30
+ color: white;
31
+ z-index: 9999;
32
+ font-family: Arial, sans-serif;
33
+ display: flex;
34
+ flex-direction: column;
35
+ transition: transform 0.3s ease;
36
+ }
37
 
38
+ .subtitle-manager.collapsed {
39
+ transform: translateX(420px);
40
+ }
41
 
42
+ .subtitle-toggle {
43
+ position: absolute;
44
+ left: -30px;
45
+ top: 10px;
46
+ width: 30px;
47
+ height: 30px;
48
+ background: rgba(33, 33, 33, 0.95);
49
+ border: none;
50
+ color: white;
51
+ cursor: pointer;
52
+ display: flex;
53
+ align-items: center;
54
+ justify-content: center;
55
+ border-radius: 4px 0 0 4px;
56
+ }
57
 
58
+ .subtitle-tabs {
59
+ display: flex;
60
+ border-bottom: 1px solid #444;
61
+ }
62
 
63
+ .subtitle-tab {
64
+ flex: 1;
65
+ padding: 12px;
66
+ text-align: center;
67
+ cursor: pointer;
68
+ background: transparent;
69
+ border: none;
70
+ color: white;
71
+ transition: background-color 0.2s ease;
72
+ font-size: 14px;
73
+ }
74
 
75
+ .subtitle-tab.active {
76
+ background: #444;
77
+ font-weight: bold;
78
+ }
79
 
80
+ .subtitle-content {
81
+ flex: 1;
82
+ overflow-y: auto;
83
+ padding: 15px;
84
+ transition: opacity 0.3s ease;
85
+ }
86
 
87
+ .subtitle-line {
88
+ padding: 12px 15px;
89
+ margin: 6px 0;
90
+ cursor: pointer;
91
+ border-radius: 4px;
92
+ transition: background-color 0.2s ease, transform 0.1s ease;
93
+ position: relative;
94
+ white-space: pre-wrap;
95
+ word-wrap: break-word;
96
+ line-height: 1.5;
97
+ width: 410px;
98
+ font-size: 14px;
99
+ }
100
 
101
+ .subtitle-line:hover {
102
+ background: rgba(255, 255, 255, 0.1);
103
+ transform: translateX(5px);
104
+ }
105
 
106
+ .subtitle-line.active {
107
+ background: rgba(255, 255, 255, 0.2);
108
+ }
109
 
110
+ .mixed-line {
111
+ display: flex;
112
+ flex-direction: column;
113
+ gap: 12px;
114
+ padding: 15px;
115
+ }
116
 
117
+ .en-text {
118
+ color: #fff;
119
+ font-size: 14px;
120
+ line-height: 1.5;
121
+ white-space: pre-wrap;
122
+ word-wrap: break-word;
123
+ }
124
 
125
+ .zh-text {
126
+ color: #aaa;
127
+ font-size: 14px;
128
+ line-height: 1.5;
129
+ white-space: pre-wrap;
130
+ word-wrap: break-word;
131
+ border-top: 1px solid rgba(255,255,255,0.1);
132
+ margin-top: 8px;
133
+ padding-top: 8px;
134
+ }
135
 
136
+ .timestamp {
137
+ position: absolute;
138
+ right: 12px;
139
+ top: 8px;
140
+ font-size: 11px;
141
+ color: #888;
142
+ opacity: 0;
143
+ transition: opacity 0.2s ease;
144
+ background: rgba(0,0,0,0.5);
145
+ padding: 2px 6px;
146
+ border-radius: 3px;
147
+ }
148
 
149
+ .subtitle-line:hover .timestamp {
150
+ opacity: 1;
151
+ }
152
 
153
+ .subtitle-content::-webkit-scrollbar {
154
+ width: 8px;
155
+ }
 
156
 
157
+ .subtitle-content::-webkit-scrollbar-track {
158
+ background: rgba(255, 255, 255, 0.1);
159
+ }
160
 
161
+ .subtitle-content::-webkit-scrollbar-thumb {
162
+ background: rgba(255, 255, 255, 0.3);
163
+ border-radius: 4px;
164
+ }
165
+
166
+ .subtitle-content::-webkit-scrollbar-thumb:hover {
167
+ background: rgba(255, 255, 255, 0.4);
168
+ }
169
+ `;
170
 
171
  // Apply main styles
172
  GM_addStyle(styles);
 
302
  return tracks;
303
  }
304
 
305
+ // Method 2: Get from player config
306
  if (player.getPlayerResponse) {
307
  const response = await player.getPlayerResponse();
308
  debug.log('Player response:', response);
 
342
  async loadSubtitles(track) {
343
  const MAX_RETRIES = 3;
344
  let retries = 0;
345
+
346
  while (retries < MAX_RETRIES) {
347
  try {
348
  // Use baseUrl instead of src
349
  const url = new URL(track.baseUrl);
350
  // Add required params for JSON format
351
  url.searchParams.set('fmt', 'json3');
352
+
353
  const response = await fetch(url.toString());
354
  if (!response.ok) {
355
  throw new Error(`HTTP error: ${response.status}`);
356
  }
357
+
358
  const data = await response.json();
359
  debug.log('Subtitle data:', data);
360
+
361
  if (!data?.events) {
362
  throw new Error('Invalid subtitle data format');
363
  }
364
+ debugger;
365
  const subtitles = data.events
366
  .filter(event => event.segs && event.tStartMs !== undefined)
367
  .map(event => ({
 
370
  text: event.segs.map(seg => seg.utf8).join('').trim()
371
  }))
372
  .filter(sub => sub.text);
373
+
374
  debug.log(`Loaded ${subtitles.length} subtitles`);
375
  return subtitles;
376
+
377
  } catch (error) {
378
  debug.error(`Subtitle load attempt ${retries + 1} failed:`, error);
379
  retries++;
 
635
  this.setupVideoTimeUpdate();
636
  }
637
 
638
+
639
  setupVideoTimeUpdate() {
640
  let rafId = null;
641
  const updateThreshold = 16; // ~60fps
642
  let lastUpdate = 0;
643
+
644
  const findSubtitleIndex = (time, subtitles) => {
645
  // Binary search for faster lookup
646
  let start = 0;
647
  let end = subtitles.length - 1;
648
+
649
  while (start <= end) {
650
  const mid = Math.floor((start + end) / 2);
651
  const sub = subtitles[mid];
652
+
653
  if (time >= sub.startTime && time <= sub.endTime) {
654
  return mid;
655
  }
656
+
657
  if (time < sub.startTime) {
658
  end = mid - 1;
659
  } else {
 
662
  }
663
  return -1;
664
  };
665
+
666
  const handleTimeUpdate = (video) => {
667
  const now = performance.now();
668
  if (now - lastUpdate < updateThreshold) {
 
670
  return;
671
  }
672
  lastUpdate = now;
673
+
674
  const currentTime = video.currentTime;
675
+ const subtitles = this.currentTab === 'chinese' ?
676
  this.subtitles.chinese : this.subtitles.english;
677
+
678
  const newIndex = findSubtitleIndex(currentTime, subtitles);
679
  if (newIndex === this.subtitleIndex) {
680
  rafId = requestAnimationFrame(() => handleTimeUpdate(video));
681
  return;
682
  }
683
+
684
  this.subtitleIndex = newIndex;
685
  const lines = this.container.querySelectorAll('.subtitle-line');
686
+
687
  lines.forEach(line => {
688
  const lineIndex = parseInt(line.dataset.index);
689
  const isActive = lineIndex === newIndex;
690
+
691
  line.classList.toggle('active', isActive);
692
  if (isActive) {
693
  line.scrollIntoView({
 
696
  });
697
  }
698
  });
699
+
700
  rafId = requestAnimationFrame(() => handleTimeUpdate(video));
701
  };
702
+
703
  const setupVideoListener = async () => {
704
  const video = await this.waitForElement('video');
705
  if (video) {
706
  handleTimeUpdate(video);
707
+
708
  video.addEventListener('pause', () => {
709
  cancelAnimationFrame(rafId);
710
  });
711
+
712
  video.addEventListener('play', () => {
713
  lastUpdate = 0;
714
  handleTimeUpdate(video);
715
  });
716
  }
717
  };
718
+
719
  setupVideoListener();
720
  }
721
 
 
760
 
761
  //enter debug mode
762
 
763
+ debugger;
764
 
765
  // 3. Load subtitle data
766
  for (const track of tracks) {
 
785
  updateSubtitleDisplay() {
786
  const content = this.container.querySelector('.subtitle-content');
787
  content.style.opacity = '0';
788
+
789
  setTimeout(() => {
790
  // Clear content safely
791
  while (content.firstChild) {
792
  content.removeChild(content.firstChild);
793
  }
794
+
795
  // Check for empty subtitles
796
  if (!this.subtitles.english.length && !this.subtitles.chinese.length) {
797
  content.appendChild(this.createMessageElement('No subtitles available'));
798
  content.style.opacity = '1';
799
  return;
800
  }
801
+
802
  // Display based on current tab
803
  switch (this.currentTab) {
804
  case 'english':
 
823
  }
824
  break;
825
  }
826
+
827
  // Fade in
828
  content.style.opacity = '1';
829
  }, 300);