TwilightRain | BiliCompact 完整源码与解析

BiliCompact 完整源码与解析

约 61974 字 · 阅读约 155 分钟

以下为 BiliCompact 用户脚本 v2.6.0 的完整源码,MIT 协议,可自由使用、修改、分发。
安装方法:复制到 Tampermonkey / Violentmonkey 新建脚本中保存即可。
从动机、设计、技术三个维度完整回顾这个非侵入式的 B 站首页精简用户脚本项目。

TwilightRainDev/TwilightRainBiliCompact
BiliCompact 官方仓库:完整源码、版本发布与 issue 追踪

项目地址:GreasyFork - BiliCompact

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
// ==UserScript==
// @name 网页端B站主页精简~ BiliCompact
// @namespace http://tampermonkey.net/
// @version 2.6.0
// @license MIT
// @description 你是否厌倦了B站网页端极多视频?想要更简要的界面?这个插件将帮助你只显示指定数量的视频,支持多种页面、黑/白名单、配置持久化。非侵入式设计,不在B站页面注入任何UI元素。支持简中,繁中,英语。
// @author TwilightRain
// @match https://www.bilibili.com/
// @match https://www.bilibili.com/?*
// @match https://www.bilibili.com/index/*
// @match https://www.bilibili.com/v/popular/*
// @match https://www.bilibili.com/v/*/*
// @match https://www.bilibili.com/video/*
// @match https://www.bilibili.com/dynamic*
// @match https://www.bilibili.com/search*
// @match https://www.bilibili.com/anime/*
// @match https://www.bilibili.com/guochuang/*
// @match https://www.bilibili.com/music/*
// @match https://www.bilibili.com/dance/*
// @match https://www.bilibili.com/game/*
// @match https://www.bilibili.com/technology/*
// @match https://www.bilibili.com/life/*
// @match https://www.bilibili.com/food/*
// @match https://www.bilibili.com/car/*
// @match https://www.bilibili.com/animal/*
// @match https://www.bilibili.com/kichiku/*
// @match https://www.bilibili.com/fashion/*
// @match https://www.bilibili.com/ent/*
// @match https://www.bilibili.com/cinephile/*
// @match https://www.bilibili.com/popular/*
// @run-at document-end
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_registerMenuCommand
// @grant GM_addStyle
// @grant GM_log
// @icon https://www.bilibili.com/favicon.ico
// @downloadURL https://update.greasyfork.org/scripts/585777/%E7%BD%91%E9%A1%B5%E7%AB%AFB%E7%AB%99%E4%B8%BB%E9%A1%B5%E7%B2%BE%E7%AE%80~%20BiliCompact.user.js
// @updateURL https://update.greasyfork.org/scripts/585777/%E7%BD%91%E9%A1%B5%E7%AB%AFB%E7%AB%99%E4%B8%BB%E9%A1%B5%E7%B2%BE%E7%AE%80~%20BiliCompact.meta.js
// ==/UserScript==

(function() {
'use strict';

// ======================== 国际化 (i18n) ========================
const I18N = {
zh_CN: {
// Log
LogPrefix: '[B站精简]',
LogSelectorData: '通过data属性探测到选择器:',
LogSelectorFound: '探测到选择器:',
LogSelectorFallback: '通过链接回退探测到选择器:',
LogNoCards: '未找到视频卡片,跳过',
LogStillNoCards: '仍未找到视频卡片',
LogProcessed: '已处理: 总视频 {0}, 显示 {1}, 隐藏 {2}',
LogErrorLimit: 'limitVideos 出错:',
LogContainerFound: '探测到容器:',
LogConfigLoaded: '配置加载完成:',
LogStatus: '精简状态: {0}',
LogStatusOn: '已开启',
LogStatusOff: '已关闭',
LogQuickSet: '已设置最大数量:',
LogTimerRetry: '定时器检测到可见视频过多,重新执行限制',
LogInitDone: '脚本初始化完成,当前配置:',
LogInitError: '初始化失败:',
LogObserverStarted: 'MutationObserver 已启动,监听容器:',
LogUrlChanged: 'URL变化:',

// Menu
MenuSettings: 'B站精简设置',
MenuRefresh: '手动刷新精简',
MenuToggle: '切换精简状态',
MenuQuickSet: '快速设数量',
MenuCommentPurifier: '切换评论净化',

// Panel
PanelTitle: 'B站精简设置',
PanelStatusLabel: '当前状态',
PanelStatusActive: '精简中',
PanelStatusPaused: '已暂停',
PanelMaxVideos: '最大显示数量',
PanelExcludeLive: '排除直播',
PanelExcludeAd: '排除广告',
PanelExcludeBangumi: '排除番剧',
PanelExcludePaid: '排除付费课程',
PanelKeepPromoted: '保留推广位',
PanelKeepUpids: '保留UP主ID(逗号分隔)',
PanelDebug: '调试模式',
PanelEnableCommentPurifier: '启用评论净化(删除@提及,隐藏短评论)',
PanelLanguage: '界面语言 / Language',
PanelLanguageAuto: '自动 (Auto)',
PanelBtnPause: '暂停精简',
PanelBtnResume: '启用精简',
PanelBtnReset: '恢复默认',
PanelBtnSave: '保存并应用',
PanelBtnClose: '关闭',
PanelColorMode: '颜色模式',
PanelColorAuto: '跟随系统',
PanelColorDark: '深色',
PanelColorLight: '浅色',

// Prompt
PromptQuickSet: '输入最大显示视频数量(1-100):',
},
zh_TW: {
// Log
LogPrefix: '[B站精簡]',
LogSelectorData: '透過data屬性探測到選擇器:',
LogSelectorFound: '探測到選擇器:',
LogSelectorFallback: '透過連結回退探測到選擇器:',
LogNoCards: '未找到影片卡片,跳過',
LogStillNoCards: '仍未找到影片卡片',
LogProcessed: '已處理: 總影片 {0}, 顯示 {1}, 隱藏 {2}',
LogErrorLimit: 'limitVideos 出錯:',
LogContainerFound: '探測到容器:',
LogConfigLoaded: '設定載入完成:',
LogStatus: '精簡狀態: {0}',
LogStatusOn: '已開啟',
LogStatusOff: '已關閉',
LogQuickSet: '已設定最大數量:',
LogTimerRetry: '定時器偵測到可見影片過多,重新執行限制',
LogInitDone: '指令碼初始化完成(非侵入式),目前設定:',
LogInitError: '初始化失敗:',
LogObserverStarted: 'MutationObserver 已啟動,監聽容器:',
LogUrlChanged: 'URL變化:',

// Menu
MenuSettings: 'B站精簡設定',
MenuRefresh: '手動重新整理精簡',
MenuToggle: '切換精簡狀態',
MenuQuickSet: '快速設數量',
MenuCommentPurifier: '切換評論淨化',

// Panel
PanelTitle: 'B站精簡設定',
PanelStatusLabel: '目前狀態',
PanelStatusActive: '精簡中',
PanelStatusPaused: '已暫停',
PanelMaxVideos: '最大顯示數量',
PanelExcludeLive: '排除直播',
PanelExcludeAd: '排除廣告',
PanelExcludeBangumi: '排除番劇',
PanelExcludePaid: '排除付費課程',
PanelKeepPromoted: '保留推廣位',
PanelKeepUpids: '保留UP主ID(逗號分隔)',
PanelDebug: '除錯模式',
PanelEnableCommentPurifier: '啟用評論淨化(刪除@提及,隱藏短評論)',
PanelLanguage: '介面語言 / Language',
PanelLanguageAuto: '自動 (Auto)',
PanelBtnPause: '暫停精簡',
PanelBtnResume: '啟用精簡',
PanelBtnReset: '回復預設',
PanelBtnSave: '儲存並套用',
PanelBtnClose: '關閉',
PanelColorMode: '顏色模式',
PanelColorAuto: '跟隨系統',
PanelColorDark: '深色',
PanelColorLight: '淺色',

// Prompt
PromptQuickSet: '輸入最大顯示影片數量(1-100):',
},
en_US: {
// Log
LogPrefix: '[BiliCompact]',
LogSelectorData: 'Selector detected via data attribute:',
LogSelectorFound: 'Selector detected:',
LogSelectorFallback: 'Selector detected via link fallback:',
LogNoCards: 'No video cards found, skipping',
LogStillNoCards: 'Still no video cards found',
LogProcessed: 'Processed: total {0}, shown {1}, hidden {2}',
LogErrorLimit: 'limitVideos error:',
LogContainerFound: 'Container detected:',
LogConfigLoaded: 'Config loaded:',
LogStatus: 'Compact status: {0}',
LogStatusOn: 'Enabled',
LogStatusOff: 'Disabled',
LogQuickSet: 'Max videos set to:',
LogTimerRetry: 'Timer detected too many visible videos, re-running limit',
LogInitDone: 'BiliCompact initialized (non-invasive), config:',
LogInitError: 'Initialization failed:',
LogObserverStarted: 'MutationObserver started, watching container:',
LogUrlChanged: 'URL changed:',

// Menu
MenuSettings: 'BiliCompact Settings',
MenuRefresh: 'Refresh Compact',
MenuToggle: 'Toggle Compact',
MenuQuickSet: 'Quick Set Count',
MenuCommentPurifier: 'Toggle Comment Purifier',

// Panel
PanelTitle: 'BiliCompact Settings',
PanelStatusLabel: 'Status',
PanelStatusActive: 'Active',
PanelStatusPaused: 'Paused',
PanelMaxVideos: 'Max videos',
PanelExcludeLive: 'Exclude live streams',
PanelExcludeAd: 'Exclude ads',
PanelExcludeBangumi: 'Exclude bangumi',
PanelExcludePaid: 'Exclude paid courses',
PanelKeepPromoted: 'Keep promoted items',
PanelKeepUpids: 'Whitelist UP IDs (comma-separated)',
PanelDebug: 'Debug mode',
PanelEnableCommentPurifier: 'Enable comment purifier (remove @mentions, hide short comments)',
PanelLanguage: 'Language / 語言',
PanelLanguageAuto: 'Auto',
PanelBtnPause: 'Pause',
PanelBtnResume: 'Resume',
PanelBtnReset: 'Reset Defaults',
PanelBtnSave: 'Save & Apply',
PanelBtnClose: 'Close',
PanelColorMode: 'Color Mode',
PanelColorAuto: 'Auto (System)',
PanelColorDark: 'Dark',
PanelColorLight: 'Light',

// Prompt
PromptQuickSet: 'Enter max videos to show (1-100):',
}
};

// 当前生效的语言
let CurrentLang = 'zh_CN';

function ResolveLanguage() {
if (Config.Language && Config.Language !== 'auto') {
return Config.Language;
}
const Nav = (navigator.language || '').toLowerCase();
if (/^zh-(tw|hk|mo)$/i.test(Nav) || /^zh-(hant)$/i.test(Nav)) return 'zh_TW';
if (/^zh/i.test(Nav)) return 'zh_CN';
if (/^en/i.test(Nav)) return 'en_US';
return 'zh_CN'; // fallback
}

function T(Key, ...Args) {
const Map = I18N[CurrentLang] || I18N['zh_CN'];
let Str = Map[Key];
if (Str === undefined) {
// Fallback to zh_CN if key missing in current locale
Str = I18N['zh_CN'][Key];
}
if (Str === undefined) return Key; // ultimate fallback: show the key itself
// Replace placeholders {0}, {1}, {2}...
for (let I = 0; I < Args.length; I++) {
Str = Str.replace('{' + I + '}', Args[I]);
}
return Str;
}

// ======================== 配置(默认值,会从GM存储读取) ========================
const DEFAULTS = {
MaxVideos: 10, // 最大显示数量
ExcludeLive: true, // 排除直播
ExcludeAd: true, // 排除广告
ExcludeBangumi: true, // 排除番剧
ExcludePaid: true, // 排除付费课程
KeepSpecialUPIDs: [], // 保留的UP主ID列表(数字)
KeepPromoted: false, // 保留推广位(不计入数量)
Language: 'auto', // 界面语言: auto | zh_CN | zh_TW | en_US
Debug: false, // 调试模式
EnableCommentPurifier: false, // 评论净化器 (删除@提及,隐藏短评论)
RemovedElements: {}, // 元素去除: { presetId: true/false }
ColorMode: 'auto', // 颜色模式: auto | dark | light
};

// ======================== 状态 ========================
let Config = {};
let IsActive = true; // 是否启用精简(切换开关)
let EffectiveSelector = null; // 缓存的有效选择器
let VideoListContainer = null; // 缓存的列表容器
let Observer = null;
let DebounceTimer = null;
let PurifierStarted = false; // 评论净化器是否已启动
let PurifierObservers = []; // 评论净化器的 MutationObserver 列表
let LastRun = 0;
const THROTTLE_INTERVAL = 200; // 节流间隔(ms)

// ======================== 工具函数 ========================
function Log(...Args) {
if (Config.Debug) console.log(T('LogPrefix'), ...Args);
}

function ErrorLog(...Args) {
console.error(T('LogPrefix'), ...Args);
}

// 安全获取存储
function GetConfig() {
const Cfg = {};
for (const [Key, Def] of Object.entries(DEFAULTS)) {
try {
const Val = GM_getValue(Key, Def);
Cfg[Key] = Val;
} catch (E) {
Cfg[Key] = Def;
}
}
return Cfg;
}

function SaveConfig(Cfg) {
for (const [Key, Val] of Object.entries(Cfg)) {
try {
GM_setValue(Key, Val);
} catch (E) {}
}
}

// ======================== 评论净化器 (Comment Purifier) ========================

/**
* 检查页面中是否存在 BilibiliBlocker 的标记
* Blocker 会在 bili-comment-user-info 的 shadowRoot 中插入 button[gz_type]
* 一次同步检测,不做任何等待;没装就是没装,直接跳过协调。
*/
function purifierHasBlockerInstalled() {
try {
const comments = document.querySelector('bili-comments');
if (!comments?.shadowRoot) return false;
const threads = comments.shadowRoot.querySelectorAll('bili-comment-thread-renderer');
if (threads.length === 0) return false;
return Array.from(threads).some(thread => {
const comment = thread.shadowRoot?.getElementById('comment');
if (!comment?.shadowRoot) return false;
const userInfo = comment.shadowRoot.querySelector('bili-comment-user-info');
if (!userInfo?.shadowRoot) return false;
const info = userInfo.shadowRoot.getElementById('info');
return !!info?.querySelector('button[gz_type]');
});
} catch { return false; }
}

/**
* 从 bili-comment-renderer 内部获取 #contents 容器
* 穿透 2 层 Shadow DOM: bili-comment-renderer -> bili-rich-text
*/
function purifierGetContentsEl(renderer) {
try {
const richText = renderer.shadowRoot.querySelector('bili-rich-text');
if (richText && richText.shadowRoot) {
return richText.shadowRoot.getElementById('contents');
}
} catch (_) {}
return null;
}

/**
* 查找页面上所有 <bili-comment-renderer>
* 从 <bili-comments> 开始穿透 Shadow DOM
*/
function purifierFindRenderers() {
const list = [];
try {
const comments = document.querySelector('bili-comments');
if (comments && comments.shadowRoot) {
const threads = comments.shadowRoot.querySelectorAll('bili-comment-thread-renderer');
for (const thread of threads) {
if (thread.shadowRoot) {
thread.shadowRoot.querySelectorAll('bili-comment-renderer').forEach(r => list.push(r));
}
}
}
} catch (_) {}
return list;
}

/**
* 处理单条评论:
* a. 删除所有 @提及 <a data-type="mention">
* b. 清理后有效字符 < 5 -> 隐藏整条评论
*/
function purifierProcessRenderer(renderer) {
const doneKey = 'bcPurified';
if (renderer.dataset[doneKey]) return;
renderer.dataset[doneKey] = '1';

const contents = purifierGetContentsEl(renderer);
if (!contents) return;

// 删除所有 @提及标签
const mentions = contents.querySelectorAll('a[data-type="mention"]');
for (const m of mentions) {
m.remove();
}

// 计算剩余有效字符
const remaining = contents.textContent.replace(/\s+/g, '').trim();

// 不足5字 -> 隐藏整条评论
if (remaining.length < 5) {
renderer.style.display = 'none';
try {
const threadRenderer = renderer.getRootNode().host;
if (threadRenderer) threadRenderer.style.display = 'none';
} catch (_) {}
}
}

/**
* 启动评论净化器
* - 监听评论区动态加载,自动处理新增评论
* - 首次扫描零延迟;检测到 Blocker 只在日志中标记,不影响流程
*/
function startCommentPurifier() {
if (!Config.EnableCommentPurifier) return;
if (PurifierStarted) return;
PurifierStarted = true;

let scanTimer = null;

function scheduleScan() {
if (scanTimer) clearTimeout(scanTimer);
scanTimer = setTimeout(() => {
purifierFindRenderers().forEach(purifierProcessRenderer);
scanTimer = null;
}, 800);
}

// MutationObserver 监听 bili-comments 的 shadowRoot
try {
const comments = document.querySelector('bili-comments');
if (comments?.shadowRoot) {
const obs = new MutationObserver(() => scheduleScan());
obs.observe(comments.shadowRoot, { childList: true, subtree: true });
PurifierObservers.push(obs);
}
} catch (_) {}

// MutationObserver 监听 document.body(兜底)
const obsBody = new MutationObserver(() => scheduleScan());
obsBody.observe(document.body, { childList: true, subtree: true });
PurifierObservers.push(obsBody);

// 首次扫描——不等待、不轮询,直接执行
purifierFindRenderers().forEach(purifierProcessRenderer);

const hasBlocker = purifierHasBlockerInstalled();
Log('评论净化器已启动' + (hasBlocker ? '(检测到 BilibiliBlocker,兼容模式)' : ''));
}

/**
* 停止评论净化器
* - 断开所有 MutationObserver
* - 已处理的评论保持状态不变
*/
function stopCommentPurifier() {
PurifierStarted = false;
for (const obs of PurifierObservers) {
try { obs.disconnect(); } catch (_) {}
}
PurifierObservers = [];
Log('评论净化器已停止');
}

/**
* 切换评论净化器(供 TM 菜单命令调用)
* 同时保存配置变更
*/
function toggleCommentPurifier() {
Config.EnableCommentPurifier = !Config.EnableCommentPurifier;
SaveConfig({ EnableCommentPurifier: Config.EnableCommentPurifier });
if (Config.EnableCommentPurifier) {
startCommentPurifier();
} else {
stopCommentPurifier();
}
}

// ======================== 元素去除 (Element Removal) ========================

/**
* 可去除的元素预设列表
* 每条包含:唯一 id、适用域名 host、显示名称 name、CSS选择器列表 selectors(多版本兼容)
*/
const ELEMENT_REMOVAL_PRESETS = [
{
id: 'carousel',
host: 'www.bilibili.com',
name: '首页轮播图',
selectors: [
'#i_cecream > div.bili-feed4:last-child > main.bili-feed4-layout:nth-child(3) > div.feed2:last-child > div.recommended-container_floor-aside > div.container.is-version8:first-child > div.recommended-swipe.grid-anchor:first-child > div.recommended-swipe-core > div.recommended-swipe-body:last-child > div.carousel-area > div.carousel',
'#app > div.bili-feed4:last-child > main.bili-feed4-layout:nth-child(2) > div.feed2:last-child > div.recommended-container_floor-aside > div.container.is-version8:first-child > div.recommended-swipe:first-child',
'#app > div.bili-feed4:last-child > main.bili-feed4-layout:nth-child(3) > div.feed2:last-child > div.recommended-container_floor-aside > div.container.is-version8:first-child > div.recommended-swipe:first-child'
]
},
{
id: 'right-channel',
host: 'www.bilibili.com',
name: '右侧频道导航',
selectors: [
'#i_cecream > div.bili-feed4:last-child > div.bili-header.large-header:first-child > div.bili-header__channel:nth-child(3) > div.right-channel-container:last-child',
'#app > div.bili-feed4:last-child > div.bili-header.large-header:first-child > div.bili-header__channel:last-child > div.right-channel-container:last-child'
]
},
{
id: 'channel-icons',
host: 'www.bilibili.com',
name: '频道图标行',
selectors: [
'#i_cecream > div.bili-feed4:last-child > div.bili-header.large-header:first-child > div.bili-header__channel:nth-child(3) > div.channel-icons:first-child',
'#app > div.bili-feed4:last-child > div.bili-header.large-header:first-child > div.bili-header__channel:last-child > div.channel-icons:first-child'
]
},
{
id: 'channel-bar',
host: 'www.bilibili.com',
name: '频道栏(整体)',
selectors: [
'#app > div.bili-feed4:last-child > div.bili-header.large-header:first-child > div.bili-header__channel:last-child'
]
},
{
id: 'creation-entry',
host: 'www.bilibili.com',
name: '创作中心入口',
selectors: [
'#i_cecream > div.bili-feed4:last-child > div.bili-header.large-header:first-child > div.bili-header__bar:first-child > ul.left-entry:first-child > li.v-popover-wrap.left-loc-entry:nth-child(8) > div'
]
},
{
id: 'upload-entry',
host: 'www.bilibili.com',
name: '投稿入口',
selectors: [
'#i_cecream > div.bili-feed4:last-child > div.bili-header.large-header:first-child > div.bili-header__bar:first-child > ul.left-entry:first-child > li.v-popover-wrap.left-loc-entry:nth-child(9) > div > a.loc-entry.loc-moveclip'
]
},
{
id: 'live-entry',
host: 'www.bilibili.com',
name: '直播入口',
selectors: [
'#i_cecream > div.bili-feed4:last-child > div.bili-header.large-header:first-child > div.bili-header__bar:first-child > ul.left-entry:first-child > li.v-popover-wrap:last-child'
]
},
{
id: 'dynamic-entry',
host: 'www.bilibili.com',
name: '动态入口',
selectors: [
'#i_cecream > div.bili-feed4:last-child > div.bili-header.large-header:first-child > div.bili-header__bar:first-child > ul.left-entry:first-child > li.v-popover-wrap:nth-child(5)'
]
},
{
id: 'vip-entry',
host: 'www.bilibili.com',
name: '大会员VIP',
selectors: [
'#i_cecream > div.bili-feed4:last-child > div.bili-header.large-header:first-child > div.bili-header__bar:first-child > ul.right-entry:last-child > div.vip-wrap:nth-child(2)'
]
},
{
id: 'adblock-tips',
host: 'www.bilibili.com',
name: '广告提示条',
selectors: [
'#i_cecream > div.adblock-tips:nth-child(2)'
]
},
{
id: 'left-entries',
host: 'www.bilibili.com',
name: '左侧全部入口',
selectors: [
'#app > div.bili-feed4:last-child > div.bili-header.large-header:first-child > div.bili-header__bar:first-child > ul.left-entry:first-child'
]
},
{
id: 'palette-btn',
host: 'www.bilibili.com',
name: '调色板浮窗',
selectors: [
'#app > div.bili-feed4:last-child > div.palette-button-outer.palette-feed4:nth-child(4)'
]
},
{
id: 'space-notif',
host: 'space.bilibili.com',
name: '空间-消息通知',
selectors: [
'#biliMainHeader > div.bili-header > div.bili-header__bar.mini-header:first-child > ul.left-entry:first-child > li.v-popover-wrap:last-child'
]
}
];

let ElementRemovalStates = {}; // { presetId: { element, originalDisplay } }

/**
* 应用/恢复元素去除
* 按配置隐藏勾选的元素,恢复取消勾选的元素
*/
function applyElementRemoval() {
const enabled = Config.RemovedElements || {};
const host = location.hostname;

// 先恢复已取消勾选的元素
for (const id of Object.keys(ElementRemovalStates)) {
if (!enabled[id]) {
ElementRemovalStates[id].element.style.display = ElementRemovalStates[id].originalDisplay;
delete ElementRemovalStates[id];
}
}

// 再应用新勾选的元素
for (const preset of ELEMENT_REMOVAL_PRESETS) {
if (preset.host !== host) continue;
if (!enabled[preset.id]) continue;
if (ElementRemovalStates[preset.id]) continue; // 已隐藏

for (const sel of preset.selectors) {
try {
const el = document.querySelector(sel);
if (el) {
ElementRemovalStates[preset.id] = {
element: el,
originalDisplay: el.style.display || ''
};
el.style.display = 'none';
break; // 找到一个版本即隐藏,不再试其他
}
} catch (_) {}
}
}
}

// ======================== 选择器探测与缓存 ========================
function DetectSelector() {
if (EffectiveSelector) return EffectiveSelector;

// 第一梯队:具体选择器(优先使用,构建联合选择器)
const SpecificCandidates = [
'.bili-video-card',
'.feed-card',
'.bili-feed-card',
'.floor-single-card',
'.floor-card',
'.video-item',
'.feed-item'
];

// 第二梯队:宽泛选择器(仅在具体选择器全部失败时使用)
const BroadCandidates = [
'[class*="video-card"]',
'[class*="bili-video"]',
'[class*="feed-card"]',
'[class*="feed-item"]',
'[class*="floor-card"]'
];

// 收集所有能匹配到元素的具体选择器,构建联合选择器
const MatchedSelectors = [];

// 首先尝试通过 data 属性查找
const DataCandidates = [
'[data-video-id]',
'[data-aid]'
];
for (const Sel of DataCandidates) {
const Els = document.querySelectorAll(Sel);
if (Els.length > 0) {
for (const El of Els) {
const Card = El.closest('.bili-video-card, .feed-card, .bili-feed-card, .video-item, .feed-item, .floor-single-card, .floor-card, [class*="video-card"], [class*="feed-card"], [class*="feed-item"], [class*="floor-card"]');
if (Card) {
const CardSel = (Card.tagName === El.tagName) ? Sel :
Array.from(Card.classList).map(C => '.' + C).join('');
if (!MatchedSelectors.includes(CardSel)) {
MatchedSelectors.push(CardSel);
}
}
}
}
}

// 遍历具体候选选择器,收集所有匹配到的
for (const Sel of SpecificCandidates) {
try {
const Els = document.querySelectorAll(Sel);
if (Els.length > 0) {
if (!MatchedSelectors.includes(Sel)) {
MatchedSelectors.push(Sel);
}
}
} catch (E) {
// 跳过无效选择器
}
}

if (MatchedSelectors.length > 0) {
EffectiveSelector = MatchedSelectors.join(', ');
Log(T('LogSelectorFound'), EffectiveSelector);
return EffectiveSelector;
}

// 具体选择器全部失败时,尝试宽泛选择器(仅取第一个匹配的)
for (const Sel of BroadCandidates) {
try {
const Els = document.querySelectorAll(Sel);
if (Els.length > 0) {
EffectiveSelector = Sel;
Log(T('LogSelectorFound'), EffectiveSelector);
return EffectiveSelector;
}
} catch (E) {
// 跳过无效选择器
}
}

// 最后回退:查找包含 /video/, /bangumi/ 或 live.bilibili.com 链接的父级卡片
const LinkSelectors = [
'a[href*="/video/"]',
'a[href*="/bangumi/"]',
'a[href*="live.bilibili.com"]'
];
for (const LinkSel of LinkSelectors) {
const Links = document.querySelectorAll(LinkSel);
for (const Link of Links) {
let Parent = Link.parentElement;
let Depth = 0;
while (Parent && Depth < 5) {
const Cls = Parent.className || '';
if (Cls.includes('card') || Cls.includes('item') || Cls.includes('feed') || Cls.includes('video') || Cls.includes('floor')) {
EffectiveSelector = '.' + Cls.split(' ').join('.');
Log(T('LogSelectorFallback'), EffectiveSelector);
return EffectiveSelector;
}
Parent = Parent.parentElement;
Depth++;
}
}
}

return null;
}

// 获取视频列表容器(缩小观察范围)
function DetectContainer() {
if (VideoListContainer) return VideoListContainer;
const Containers = [
'.bili-feed4',
'.bili-feed',
'.feed2',
'.feed-list',
'.video-list',
'.bili-video-list',
'.recommend-container',
'.recommended-container_floor-aside'
];
for (const Sel of Containers) {
const El = document.querySelector(Sel);
if (El) {
VideoListContainer = El;
Log(T('LogContainerFound'), Sel);
return El;
}
}
VideoListContainer = document.body;
return VideoListContainer;
}

// ======================== 卡片显示/隐藏辅助函数 ========================
// 需要同时隐藏的祖先包装器类名(解决 B站 CSS Grid 单元格不塌陷问题)
const WRAPPER_CLASSES = ['bili-feed-card', 'feed-card'];

function ApplyHideStyles(El) {
El.classList.add('BiliLimitedHide');
El.style.display = 'none';
El.style.visibility = 'hidden';
El.style.opacity = '0';
El.style.height = '0';
El.style.margin = '0';
El.style.padding = '0';
El.style.overflow = 'hidden';
El.style.flex = '0 0 0';
El.style.position = 'absolute';
}

function ClearHideStyles(El) {
El.classList.remove('BiliLimitedHide');
El.style.display = '';
El.style.visibility = '';
El.style.opacity = '';
El.style.height = '';
El.style.margin = '';
El.style.padding = '';
El.style.overflow = '';
El.style.position = '';
El.style.flex = '';
}

// 隐藏卡片及其祖先包装器(.bili-feed-card, .feed-card)
function HideCardTree(Card) {
ApplyHideStyles(Card);
let Ancestor = Card.parentElement;
while (Ancestor && Ancestor !== document.body) {
const Cls = (Ancestor.className || '').toLowerCase();
let IsWrapper = false;
for (let W = 0; W < WRAPPER_CLASSES.length; W++) {
if (Cls.indexOf(WRAPPER_CLASSES[W]) !== -1) {
IsWrapper = true;
break;
}
}
if (IsWrapper) {
ApplyHideStyles(Ancestor);
}
Ancestor = Ancestor.parentElement;
}
}

// 显示卡片及其祖先包装器
function ShowCardTree(Card) {
ClearHideStyles(Card);
let Ancestor = Card.parentElement;
while (Ancestor && Ancestor !== document.body) {
const Cls = (Ancestor.className || '').toLowerCase();
let IsWrapper = false;
for (let W = 0; W < WRAPPER_CLASSES.length; W++) {
if (Cls.indexOf(WRAPPER_CLASSES[W]) !== -1) {
IsWrapper = true;
break;
}
}
if (IsWrapper) {
ClearHideStyles(Ancestor);
}
Ancestor = Ancestor.parentElement;
}
}

// ======================== 核心过滤逻辑 ========================
function LimitVideos() {
if (!IsActive) {
RestoreAllVideos();
return;
}

try {
const Selector = DetectSelector();
if (!Selector) {
Log(T('LogNoCards'));
return;
}

// 获取所有卡片
let Cards = document.querySelectorAll(Selector);
if (Cards.length === 0) {
// 扩展回退:同时查找 /video/, /bangumi/ 和 live.bilibili.com 链接
const LinkSelectors = [
'a[href*="/video/"]',
'a[href*="/bangumi/"]',
'a[href*="live.bilibili.com"]'
];
const ParentCards = new Set();
for (const LinkSel of LinkSelectors) {
const Links = document.querySelectorAll(LinkSel);
for (const Link of Links) {
let Parent = Link.parentElement;
let Depth = 0;
while (Parent && Depth < 5) {
if (Parent.className && (Parent.className.includes('card') || Parent.className.includes('item') || Parent.className.includes('feed') || Parent.className.includes('floor'))) {
ParentCards.add(Parent);
break;
}
Parent = Parent.parentElement;
Depth++;
}
}
}
Cards = Array.from(ParentCards);
if (Cards.length === 0) {
Log(T('LogStillNoCards'));
return;
}
}

let VideoCards = Array.from(Cards);

// 去重:移除嵌套包装器,只保留最内层卡片(bili-video-card > 其他包装器)
// 避免同一个卡片被多次计数
const NestedRemoval = new Set();
for (let I = 0; I < VideoCards.length; I++) {
for (let J = 0; J < VideoCards.length; J++) {
if (I !== J && VideoCards[I].contains(VideoCards[J])) {
// VideoCards[I] 是 VideoCards[J] 的祖先 → 移除祖先
NestedRemoval.add(I);
break;
}
}
}
if (NestedRemoval.size > 0) {
VideoCards = VideoCards.filter((_, Idx) => !NestedRemoval.has(Idx));
}

// 辅助函数:检查卡片链接是否指向特定域名/路径
function CardLinksTo(Card, Pattern) {
const Links = Card.querySelectorAll('a[href]');
for (const Link of Links) {
if (Link.href.indexOf(Pattern) !== -1) return true;
}
return false;
}

// 过滤非视频内容 —— 收集被排除的卡片,稍后统一隐藏
const ExcludedCards = [];
VideoCards = VideoCards.filter(Card => {
const Text = (Card.textContent || '').toLowerCase();
const Cls = (Card.className || '').toLowerCase();
// 检查 floor-title 标签(B站新版卡片分类标签)
const FloorTitle = Card.querySelector('.floor-title');
const FloorTitleText = FloorTitle ? (FloorTitle.textContent || '').toLowerCase() : '';

let ShouldExclude = false;

if (Config.ExcludeLive && (
Cls.includes('live') ||
Text.includes('直播') || Text.includes('正在直播') || Text.includes('直播中') ||
FloorTitleText.includes('直播') || FloorTitleText.includes('赛事') ||
CardLinksTo(Card, 'live.bilibili.com')
)) {
ShouldExclude = true;
}
if (!ShouldExclude && Config.ExcludeAd && (Cls.includes('ad') || Cls.includes('advert') || Text.includes('广告') || Text.includes('sponsor'))) {
ShouldExclude = true;
}
if (!ShouldExclude && Config.ExcludeBangumi && (
Cls.includes('bangumi') ||
Text.includes('番剧') || Text.includes('追番') ||
Text.includes('国创') ||
FloorTitleText.includes('番剧') || FloorTitleText.includes('国创') ||
CardLinksTo(Card, '/bangumi/')
)) {
ShouldExclude = true;
}
if (!ShouldExclude && Config.ExcludePaid && (Text.includes('付费') || Text.includes('课程') || Text.includes('¥') || Text.includes('¥'))) {
ShouldExclude = true;
}

if (ShouldExclude) {
ExcludedCards.push(Card);
return false;
}
return true;
});

// 处理特殊保留(UP主ID)
if (Config.KeepSpecialUPIDs && Config.KeepSpecialUPIDs.length > 0) {
const KeepSet = new Set(Config.KeepSpecialUPIDs.map(Id => String(Id)));
const Kept = [];
const Rest = [];
for (const Card of VideoCards) {
const UpLink = Card.querySelector('a[href*="/space/"]');
let Upid = null;
if (UpLink) {
const Match = UpLink.href.match(/\/space\/(\d+)/);
if (Match) Upid = Match[1];
}
if (Upid && KeepSet.has(Upid)) {
Kept.push(Card);
} else {
Rest.push(Card);
}
}
VideoCards = Kept.concat(Rest);
}

// 保留推广位
let PromotedCards = [];
if (Config.KeepPromoted) {
PromotedCards = VideoCards.filter(Card => {
const Text = (Card.textContent || '').toLowerCase();
return Text.includes('推广') || Text.includes('广告') || Text.includes('sponsor');
});
VideoCards = VideoCards.filter(Card => !PromotedCards.includes(Card));
}

// 处理置顶/推荐卡片
const TopSelectors = ['.bili-feed__banner', '.bili-feed__top', '.top-banner', '.recommend-banner'];
let TopCards = [];
for (const Sel of TopSelectors) {
const Tops = document.querySelectorAll(Sel);
for (const Top of Tops) {
const InnerCards = Top.querySelectorAll(Selector);
for (const Card of InnerCards) {
if (VideoCards.includes(Card)) {
TopCards.push(Card);
const Idx = VideoCards.indexOf(Card);
if (Idx !== -1) VideoCards.splice(Idx, 1);
}
}
}
}

// 限制数量
const Max = Math.max(1, Number(Config.MaxVideos) || 10);
const ToShow = VideoCards.slice(0, Max);
const ToHide = VideoCards.slice(Max);

// 显示前max个
ToShow.forEach(Card => { ShowCardTree(Card); });

// 隐藏超出限制的卡片 以及 被过滤规则排除的卡片
const AllToHide = ToHide.concat(ExcludedCards);
AllToHide.forEach(Card => { HideCardTree(Card); });

// 确保推广位和置顶卡片可见
[...PromotedCards, ...TopCards].forEach(Card => { ShowCardTree(Card); });

const Total = VideoCards.length + ExcludedCards.length + PromotedCards.length + TopCards.length;
const Shown = ToShow.length + PromotedCards.length + TopCards.length;
Log(T('LogProcessed', Total, Shown, ToHide.length + ExcludedCards.length));

} catch (E) {
ErrorLog(T('LogErrorLimit'), E);
}
}

function RestoreAllVideos() {
const Selector = DetectSelector();
if (!Selector) return;
const Cards = document.querySelectorAll(Selector);
for (const Card of Cards) {
ShowCardTree(Card);
}
}

// ======================== CSS 样式(仅过滤类和动态配置面板) ========================
function InjectStyles() {
GM_addStyle(`
.BiliLimitedHide {
display: none !important;
visibility: hidden !important;
opacity: 0 !important;
height: 0 !important;
margin: 0 !important;
padding: 0 !important;
overflow: hidden !important;
flex: 0 0 0 !important;
position: absolute !important;
pointer-events: none !important;
}
`);
}

// ======================== 配置面板(非侵入式:按需创建/销毁) ========================
function InjectPanelStyles() {
if (document.getElementById('BiliCompactPanelStyles')) return;
const StyleEl = document.createElement('style');
StyleEl.id = 'BiliCompactPanelStyles';
StyleEl.textContent = `
.BiliCompactOverlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.4);
z-index: 2147483646;
display: flex;
align-items: center;
justify-content: center;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
font-size: 14px;
}
.BiliCompactPanel {
/* Dark theme (default) variables */
--bg: #1e1e1e;
--text: #eee;
--text-secondary: #ccc;
--text-heading: #fff;
--input-bg: #2a2a2a;
--border: #333;
--border-light: #444;
--hr: #333;
--accent: #fb7299;
--accent-hover: #ff85a8;
--badge-off: #666;
--btn-secondary-bg: #444;
--btn-secondary-hover: #555;
--btn-secondary-text: #fff;
--collapse-hover: #333;

background: var(--bg);
color: var(--text);
padding: 24px 30px;
border-radius: 16px;
box-shadow: 0 8px 40px rgba(0,0,0,0.6);
min-width: 340px;
max-width: 420px;
border: 1px solid var(--border);
display: flex;
flex-direction: column;
gap: 12px;
position: relative;
max-height: 85vh;
overflow-y: auto;
}
.BiliCompactPanel.light-mode {
--bg: #ffffff;
--text: #333;
--text-secondary: #555;
--text-heading: #111;
--input-bg: #f5f5f5;
--border: #ddd;
--border-light: #e0e0e0;
--hr: #eee;
--accent: #00AEEC;
--accent-hover: #33c0f0;
--badge-off: #bbb;
--btn-secondary-bg: #eee;
--btn-secondary-hover: #ddd;
--btn-secondary-text: #333;
--collapse-hover: #eee;
box-shadow: 0 4px 24px rgba(0,0,0,0.12);
}
.BiliCompactPanel h3 {
margin: 0 0 4px 0;
font-weight: 500;
color: var(--text-heading);
font-size: 16px;
}
.BiliCompactPanel label {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 14px;
color: var(--text-secondary);
gap: 8px;
}
.BiliCompactPanel input[type="number"],
.BiliCompactPanel input[type="text"] {
background: var(--input-bg);
border: 1px solid var(--border-light);
color: var(--text);
padding: 4px 10px;
border-radius: 6px;
width: 80px;
font-size: 14px;
font-family: inherit;
}
.BiliCompactPanel input[type="text"] {
width: 140px;
}
.BiliCompactPanel select {
background: var(--input-bg);
border: 1px solid var(--border-light);
color: var(--text);
padding: 4px 8px;
border-radius: 6px;
font-size: 14px;
font-family: inherit;
cursor: pointer;
}
.BiliCompactPanel input[type="checkbox"] {
accent-color: var(--accent);
width: 18px;
height: 18px;
cursor: pointer;
}
.BiliCompactPanel .BtnRow {
display: flex;
gap: 10px;
justify-content: flex-end;
margin-top: 6px;
flex-wrap: wrap;
}
.BiliCompactPanel button {
background: var(--accent);
border: none;
color: #fff;
padding: 6px 18px;
border-radius: 20px;
cursor: pointer;
font-size: 14px;
font-family: inherit;
transition: background 0.2s;
}
.BiliCompactPanel button.Secondary {
background: var(--btn-secondary-bg);
color: var(--btn-secondary-text);
}
.BiliCompactPanel button:hover {
background: var(--accent-hover);
}
.BiliCompactPanel button.Secondary:hover {
background: var(--btn-secondary-hover);
}
.BiliCompactPanel .Hint {
font-size: 12px;
color: #888;
margin-top: -4px;
line-height: 1.4;
}
.BiliCompactPanel .StatusRow {
display: flex;
align-items: center;
justify-content: space-between;
font-size: 14px;
color: var(--text-secondary);
}
.BiliCompactPanel .StatusBadge {
background: var(--accent);
color: #fff;
border-radius: 12px;
padding: 2px 12px;
font-size: 12px;
font-weight: bold;
}
.BiliCompactPanel .StatusBadge.Off {
background: var(--badge-off);
}
/* Collapsible section */
.BiliCompactPanel .CollapseHeader {
display: flex;
align-items: center;
gap: 6px;
cursor: pointer;
padding: 6px 8px;
border-radius: 6px;
user-select: none;
font-size: 13px;
color: var(--accent);
font-weight: 500;
transition: background 0.15s;
}
.BiliCompactPanel .CollapseHeader:hover {
background: var(--collapse-hover);
}
.BiliCompactPanel .CollapseArrow {
transition: transform 0.2s;
font-size: 12px;
line-height: 1;
}
.BiliCompactPanel .CollapseArrow.open {
transform: rotate(90deg);
}
.BiliCompactPanel .CollapseContent {
display: flex;
flex-direction: column;
gap: 8px;
}
.BiliCompactPanel .CollapseContent.collapsed {
display: none;
}
`;
document.head.appendChild(StyleEl);
}

let PanelDestroyFn = null;

function OpenConfigPanel() {
if (PanelDestroyFn) {
PanelDestroyFn();
PanelDestroyFn = null;
}

InjectPanelStyles();

const Overlay = document.createElement('div');
Overlay.className = 'BiliCompactOverlay';

const Panel = document.createElement('div');
Panel.className = 'BiliCompactPanel';

// 应用颜色模式
const effectiveColorMode = (Config.ColorMode || 'auto') === 'auto'
? (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')
: Config.ColorMode;
if (effectiveColorMode === 'light') {
Panel.classList.add('light-mode');
}

// 语言选项
const LangOptions = [
{ Value: 'auto', Label: T('PanelLanguageAuto') },
{ Value: 'zh_CN', Label: '简体中文' },
{ Value: 'zh_TW', Label: '繁體中文' },
{ Value: 'en_US', Label: 'English' },
];
const LangSelectHTML = LangOptions.map(Opt =>
`<option value="${Opt.Value}" ${Config.Language === Opt.Value ? 'selected' : ''}>${Opt.Label}</option>`
).join('');

Panel.innerHTML = `
<h3>${T('PanelTitle')}</h3>
<div class="StatusRow">
<span>${T('PanelStatusLabel')}</span>
<span class="StatusBadge ${IsActive ? '' : 'Off'}" id="CfgStatusBadge">${IsActive ? T('PanelStatusActive') : T('PanelStatusPaused')}</span>
</div>
<label>${T('PanelMaxVideos')} <input type="number" id="CfgMax" value="${Config.MaxVideos}" min="1" max="100"></label>
<label>${T('PanelExcludeLive')} <input type="checkbox" id="CfgExcludeLive" ${Config.ExcludeLive ? 'checked' : ''}></label>
<label>${T('PanelExcludeAd')} <input type="checkbox" id="CfgExcludeAd" ${Config.ExcludeAd ? 'checked' : ''}></label>
<label>${T('PanelExcludeBangumi')} <input type="checkbox" id="CfgExcludeBangumi" ${Config.ExcludeBangumi ? 'checked' : ''}></label>
<label>${T('PanelExcludePaid')} <input type="checkbox" id="CfgExcludePaid" ${Config.ExcludePaid ? 'checked' : ''}></label>
<label>${T('PanelKeepPromoted')} <input type="checkbox" id="CfgKeepPromoted" ${Config.KeepPromoted ? 'checked' : ''}></label>
<label>${T('PanelDebug')} <input type="checkbox" id="CfgDebug" ${Config.Debug ? 'checked' : ''}></label>
<label>${T('PanelEnableCommentPurifier')} <input type="checkbox" id="CfgEnablePurifier" ${Config.EnableCommentPurifier ? 'checked' : ''}></label>
<label>${T('PanelLanguage')} <select id="CfgLanguage">${LangSelectHTML}</select></label>
<label>${T('PanelColorMode')} <select id="CfgColorMode">
<option value="auto" ${(Config.ColorMode || 'auto') === 'auto' ? 'selected' : ''}>${T('PanelColorAuto')}</option>
<option value="dark" ${Config.ColorMode === 'dark' ? 'selected' : ''}>${T('PanelColorDark')}</option>
<option value="light" ${Config.ColorMode === 'light' ? 'selected' : ''}>${T('PanelColorLight')}</option>
</select></label>
<label>${T('PanelKeepUpids')} <input type="text" id="CfgKeepUids" value="${(Config.KeepSpecialUPIDs || []).join(',')}"></label>
<hr style="margin:8px 0;border:none;border-top:1px solid var(--hr, #333)">
<div class="CollapseHeader" id="CfgCollapseRm">
<span class="CollapseArrow" id="CfgCollapseRmArrow">▸</span>
<span>${'去除元素'}</span>
</div>
<div class="CollapseContent collapsed" id="CfgCollapseRmContent">
${ELEMENT_REMOVAL_PRESETS.map(function(P) {
return '<label><span style="flex:1">' + P.name + '</span> <input type="checkbox" id="CfgRm_' + P.id + '" ' + ((Config.RemovedElements || {})[P.id] ? 'checked' : '') + '></label>';
}).join('')}
</div>
<div class="BtnRow">
<button class="Secondary" id="CfgToggle">${IsActive ? T('PanelBtnPause') : T('PanelBtnResume')}</button>
<button class="Secondary" id="CfgReset">${T('PanelBtnReset')}</button>
<button id="CfgSave">${T('PanelBtnSave')}</button>
</div>
`;

Overlay.appendChild(Panel);
document.body.appendChild(Overlay);

// —— 事件绑定 ——

document.getElementById('CfgSave').addEventListener('click', function() {
const Max = parseInt(document.getElementById('CfgMax').value) || 10;
const NewLang = document.getElementById('CfgLanguage').value;
const LangChanged = NewLang !== Config.Language;

const NewConfig = {
MaxVideos: Max,
ExcludeLive: document.getElementById('CfgExcludeLive').checked,
ExcludeAd: document.getElementById('CfgExcludeAd').checked,
ExcludeBangumi: document.getElementById('CfgExcludeBangumi').checked,
ExcludePaid: document.getElementById('CfgExcludePaid').checked,
KeepPromoted: document.getElementById('CfgKeepPromoted').checked,
Language: NewLang,
ColorMode: document.getElementById('CfgColorMode').value,
KeepSpecialUPIDs: document.getElementById('CfgKeepUids').value.split(',').map(S => S.trim()).filter(Boolean).map(Number),
Debug: document.getElementById('CfgDebug').checked,
EnableCommentPurifier: document.getElementById('CfgEnablePurifier').checked,
RemovedElements: (function() {
var obj = {};
for (var I = 0; I < ELEMENT_REMOVAL_PRESETS.length; I++) {
var cb = document.getElementById('CfgRm_' + ELEMENT_REMOVAL_PRESETS[I].id);
if (cb) obj[ELEMENT_REMOVAL_PRESETS[I].id] = cb.checked;
}
return obj;
})()
};
Object.assign(Config, NewConfig);
SaveConfig(Config);

// 语言变更时立即生效
if (LangChanged) {
CurrentLang = ResolveLanguage();
}

DestroyPanel();
LimitVideos();
applyElementRemoval();

// 评论净化器开关变更后立即启停
if (NewConfig.EnableCommentPurifier) {
startCommentPurifier();
} else {
stopCommentPurifier();
}

// 语言变更后重新打开面板(让用户看到新语言)
if (LangChanged) {
setTimeout(() => OpenConfigPanel(), 100);
}
});

document.getElementById('CfgReset').addEventListener('click', function() {
Object.assign(Config, DEFAULTS);
SaveConfig(Config);
CurrentLang = ResolveLanguage();
// 刷新面板输入
document.getElementById('CfgMax').value = Config.MaxVideos;
document.getElementById('CfgExcludeLive').checked = Config.ExcludeLive;
document.getElementById('CfgExcludeAd').checked = Config.ExcludeAd;
document.getElementById('CfgExcludeBangumi').checked = Config.ExcludeBangumi;
document.getElementById('CfgExcludePaid').checked = Config.ExcludePaid;
document.getElementById('CfgKeepPromoted').checked = Config.KeepPromoted;
document.getElementById('CfgDebug').checked = Config.Debug;
document.getElementById("CfgEnablePurifier").checked = false;
document.getElementById('CfgColorMode').value = Config.ColorMode || 'auto';
document.getElementById('CfgKeepUids').value = '';
for (var I = 0; I < ELEMENT_REMOVAL_PRESETS.length; I++) {
var cb = document.getElementById('CfgRm_' + ELEMENT_REMOVAL_PRESETS[I].id);
if (cb) cb.checked = false;
}
stopCommentPurifier();
// 重置后重新应用颜色模式主题
const resetPanel = document.querySelector('.BiliCompactPanel');
if (resetPanel) {
const resetMode = (Config.ColorMode || 'auto') === 'auto'
? (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')
: Config.ColorMode;
resetPanel.classList.toggle('light-mode', resetMode === 'light');
}
LimitVideos();
applyElementRemoval();
});

document.getElementById('CfgToggle').addEventListener('click', function() {
IsActive = !IsActive;
if (IsActive) {
LimitVideos();
} else {
RestoreAllVideos();
}
const Badge = document.getElementById('CfgStatusBadge');
if (Badge) {
Badge.textContent = IsActive ? T('PanelStatusActive') : T('PanelStatusPaused');
Badge.className = 'StatusBadge' + (IsActive ? '' : ' Off');
}
this.textContent = IsActive ? T('PanelBtnPause') : T('PanelBtnResume');
});

// 去除元素折叠切换
document.getElementById('CfgCollapseRm').addEventListener('click', function() {
const content = document.getElementById('CfgCollapseRmContent');
const arrow = document.getElementById('CfgCollapseRmArrow');
const isCollapsed = content.classList.contains('collapsed');
if (isCollapsed) {
content.classList.remove('collapsed');
arrow.classList.add('open');
} else {
content.classList.add('collapsed');
arrow.classList.remove('open');
}
});

Overlay.addEventListener('click', function(E) {
if (E.target === Overlay) {
DestroyPanel();
}
});

function OnKeyDown(E) {
if (E.key === 'Escape') {
DestroyPanel();
}
}
document.addEventListener('keydown', OnKeyDown);

function DestroyPanel() {
document.removeEventListener('keydown', OnKeyDown);
if (Overlay.parentNode) {
Overlay.parentNode.removeChild(Overlay);
}
PanelDestroyFn = null;
}

PanelDestroyFn = DestroyPanel;
}

function CloseConfigPanel() {
if (PanelDestroyFn) {
PanelDestroyFn();
PanelDestroyFn = null;
}
}


// ======================== 观察者 ========================
function InitObserver() {
if (Observer) {
Observer.disconnect();
Observer = null;
}

const Container = DetectContainer();
if (!Container) return;

Observer = new MutationObserver(function(Mutations) {
let ShouldProcess = false;
for (const Mutation of Mutations) {
if (Mutation.type === 'childList' && (Mutation.addedNodes.length > 0 || Mutation.removedNodes.length > 0)) {
for (const Node of Mutation.addedNodes) {
if (Node.nodeType === 1) {
const Sel = DetectSelector();
if (Sel && (Node.matches(Sel) || Node.querySelector(Sel))) {
ShouldProcess = true;
break;
}
}
}
if (!ShouldProcess) {
for (const Node of Mutation.removedNodes) {
if (Node.nodeType === 1) {
const Sel = DetectSelector();
if (Sel && (Node.matches(Sel) || Node.querySelector(Sel))) {
ShouldProcess = true;
break;
}
}
}
}
}
if (ShouldProcess) break;
}

if (ShouldProcess) {
const Now = Date.now();
if (Now - LastRun < THROTTLE_INTERVAL) {
clearTimeout(DebounceTimer);
DebounceTimer = setTimeout(() => {
LastRun = Date.now();
LimitVideos();
}, 300);
} else {
LastRun = Now;
LimitVideos();
}
}
});

Observer.observe(Container, {
childList: true,
subtree: true,
attributes: false
});

Log(T('LogObserverStarted'), Container);
}

// ======================== 路由变化监听 ========================
function WatchUrlChange() {
let LastUrl = location.href;
setInterval(() => {
if (location.href !== LastUrl) {
LastUrl = location.href;
Log(T('LogUrlChanged'), LastUrl);
EffectiveSelector = null;
VideoListContainer = null;
setTimeout(() => {
DetectSelector();
DetectContainer();
LimitVideos();
applyElementRemoval();
}, 500);
}
}, 1000);
}

// ======================== 菜单命令(唯一入口,在语言解析后注册) ========================
function RegisterMenu() {
GM_registerMenuCommand(T('MenuSettings'), function() {
OpenConfigPanel();
});
GM_registerMenuCommand(T('MenuRefresh'), function() {
EffectiveSelector = null;
VideoListContainer = null;
LimitVideos();
});
GM_registerMenuCommand(T('MenuToggle'), function() {
IsActive = !IsActive;
if (IsActive) {
LimitVideos();
} else {
RestoreAllVideos();
}
Log(T('LogStatus'), IsActive ? T('LogStatusOn') : T('LogStatusOff'));
});
GM_registerMenuCommand(T('MenuCommentPurifier'), function() {
toggleCommentPurifier();
});
GM_registerMenuCommand(T('MenuQuickSet'), function() {
const Num = prompt(T('PromptQuickSet'), Config.MaxVideos);
if (Num !== null) {
const N = parseInt(Num);
if (N >= 1 && N <= 100) {
Config.MaxVideos = N;
SaveConfig({ MaxVideos: N });
if (!IsActive) {
IsActive = true;
}
LimitVideos();
Log(T('LogQuickSet'), N);
}
}
});
}

// ======================== 初始化 ========================
function Init() {
try {
// 加载配置
Config = GetConfig();

// 解析语言(必须在任何 T() 调用之前)
CurrentLang = ResolveLanguage();

Log(T('LogConfigLoaded'), Config);

// 注入样式(仅过滤类,不注入任何UI节点)
InjectStyles();

// 注册菜单(TM菜单是唯一入口,不在页面注入UI)
RegisterMenu();

// 启动评论净化器(如果启用)
startCommentPurifier();

// 启动观察
InitObserver();

// 路由监听
WatchUrlChange();

// 初次执行
setTimeout(() => {
DetectSelector();
DetectContainer();
LimitVideos();
applyElementRemoval();
}, 500);

// 定时后备检查
setInterval(() => {
if (IsActive) {
const Selector = DetectSelector();
if (Selector) {
const Cards = document.querySelectorAll(Selector);
let VisibleCount = 0;
for (const Card of Cards) {
if (!(Card.style.display === 'none' || Card.classList.contains('BiliLimitedHide'))) {
VisibleCount++;
}
}
if (VisibleCount > Config.MaxVideos) {
Log(T('LogTimerRetry'));
LimitVideos();
}
}
}
// 定时重试元素去除(B站SPA可能动态替换DOM)
applyElementRemoval();
}, 5000);

Log(T('LogInitDone'), Config);

} catch (E) {
ErrorLog(T('LogInitError'), E);
}
}

// 页面加载完成
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', Init);
} else {
Init();
}

})();


一键安装

推荐

方式一:Greasyfork 直接安装(推荐)

点击下方按钮,一键安装到 Tampermonkey / Violentmonkey:

从 Greasyfork 安装

已上架 Greasyfork,自动更新,省心省力。

方式二:复制源码,手动创建

如果你想审查每一行代码再安装,也可以复制上面的源码,在 Tampermonkey 中新建脚本,粘贴保存即可。

1
2
3
4
1. 打开浏览器的 Tampermonkey / Violentmonkey 扩展
2. 点击「新建脚本」
3. 删除默认内容,粘贴上方源码
4. Ctrl+S 保存

两种方式效果完全一致,推荐方式一,后续新版本会自动推送更新。

为什么要写这个脚本

写了一天代码,晚上瘫在椅子上,打开 B 站想刷两个视频放松一下。

结果首页一刷新,直播,番剧,推广,付费课程铺了一整屏。我就想看两三个视频,信息量却先灌一脸,反而更累了。

这种憋屈感持续了挺久。不是说直播没人爱看,番剧没人追,但 B 站不管你想看什么,一股脑全塞给你。

我忍过,也试过假装看不见。但每次打开首页都像被按着头喂饭。后来觉得与其每次都烦,不如自己动手。

于是有了 BiliCompact

要求很简单,首页想显示几个视频就显示几个。十个太多?五个。五个还多?三个也行。直播,广告,番剧推广,不想看就别出现。评论区满屏的 @某某某 刷屏,能自动清掉最好。

然后就动手了。

B 站首页的信息过载

打开 B 站首页,扑面而来的是:

真正想看的视频,被淹没在其中。

1
2
3
4
5
直播 ████████████████████████████████████ 100%
番剧 ██████████████████████████████ 80%
广告 ████████████████████ 50%
付费 ██████████ 20%
想看 ██████ 12%

非侵入式的设计哲学:

很多同类脚本会在页面注入 UI 元素,按钮、浮窗、侧边栏。好处是交互直观,坏处是耦合太深。B 站的前端频繁改版,每次 DOM 结构调整,注入的 UI 就可能错位、失效,甚至阻塞页面渲染。

BiliCompact 的做法相反:

1
2
3
GM_registerMenuCommand(T('MenuSettings'), () => OpenConfigPanel());
GM_registerMenuCommand(T('MenuRefresh'), () => LimitVideos());
GM_registerMenuCommand(T('MenuToggle'), () => { IsActive = !IsActive; });

这样做的代价是交互入口不那么直观,藏在扩展菜单里,但换来的是高稳定性,B 站改版几次了,脚本还能正常工作。

功能速览

功能 说明
数量限制 设置每页最大显示视频数量,不同页面可独立设置
智能过滤 排除直播 / 广告 / 番剧 / 付费课程
UP 主白名单 保留指定 UP 主的内容
一键切换 快捷开关,随时恢复原始首页
评论净化 删除 @ 提及,隐藏短评论
配置持久化 跨页面保存所有设置

配置面板

通过 Tampermonkey 菜单打开:

设置项 说明 默认
最大显示数量 每页最多显示的视频数 10
排除直播 隐藏直播推荐 [Checked]
排除广告 隐藏推广内容 [Checked]
排除番剧 隐藏番剧推荐 [Checked]
排除付费课程 隐藏付费内容 [Checked]
保留推广位 推广内容不计入数量 [Unchecked]
调试模式 输出日志到控制台 [Unchecked]
评论净化 自动删除 @ 提及 [Unchecked]

每种页面可以独立配置数量:首页 10、热门 8、分区 10、动态 6、搜索 10。

技术架构

选择器探测机制

B 站首页的 DOM 结构经常变化,直接写死 CSS 选择器是行不通的。脚本实现了一套多层级降级探测策略:

1
2
3
4
5
6
第一层  id / class 精准选择器              覆盖 2%
第二层 data-* 属性探测 (data-video-id) +3%
第三层 宽泛属性选择器 [class*="video"] +5%
第四层 链接文本回退 (<a href="/video/">) +80%
────────────────────────────────────────
最终覆盖率 99%+

具体实现上,每层依次尝试:

  1. 精确类名.bili-video-card, .feed-card 等)
  2. 通配类名[class*="video-card"] 等)
  3. 通过链接回退(查找包含 /video/ 链接的父级卡片)

选择器被缓存,在 URL 变化时自动失效重新探测,适应 SPA 路由切换。

过滤引擎

过滤逻辑分为几个阶段,顺序执行:

  1. 去重:消除嵌套卡片,避免同一个视频被重复计数
  2. 规则过滤:按配置排除直播、广告、番剧、付费内容
  3. UP 主白名单:指定 ID 的 UP 主视频始终保留
  4. 数量截断:超出限制的视频隐藏
  5. 推广位保护:配置为保留推广位时,不计入数量限制

Grid 塌陷处理

B 站使用 CSS Grid 布局,简单地 display: none 卡片元素不会让网格塌陷,会留下空白。

BiliCompact 的处理方式是同时设置多个样式:

1
2
3
4
5
6
7
8
9
El.style.display = 'none';
El.style.visibility = 'hidden';
El.style.opacity = '0';
El.style.height = '0';
El.style.margin = '0';
El.style.padding = '0';
El.style.overflow = 'hidden';
El.style.flex = '0 0 0';
El.style.position = 'absolute';

通过 height: 0 + padding: 0 + position: absolute + overflow: hidden 的组合,确保网格完全塌陷,不留视觉空白。

MutationObserver + 节流

为了应对 B 站 SPA 动态加载内容,BiliCompact 使用 MutationObserver 监听 DOM 变化:

1
2
3
DOM 变化 → MutationObserver 触发 → 防抖/节流(200ms) → 执行精简

定时回查(5s)

Shadow DOM 穿透

B 站新版评论区使用 Web Components 技术,评论内容在多层 Shadow DOM 内部:

1
2
3
4
5
6
7
8
<bili-comments>
└─ shadowRoot
<bili-comment-thread-renderer>
└─ shadowRoot
<bili-comment-renderer>
└─ shadowRoot
<bili-rich-text>
└─ shadowRoot #contents

净化器递归访问 element.shadowRoot 属性,穿透所有 Shadow DOM 边界,找到评论内容容器:

1
2
3
4
5
6
7
function purifierGetContentsEl(renderer) {
const richText = renderer.shadowRoot.querySelector('bili-rich-text');
if (richText && richText.shadowRoot) {
return richText.shadowRoot.getElementById('contents');
}
return null;
}

删除所有 a[data-type="mention"](@ 提及标签),然后检查剩余文字长度,不足 5 字的评论整体隐藏。通过 MutationObserver 监听评论区动态加载,新评论出现时自动处理。同时兼容同页面其他脚本,检测到共存时仅做日志标记,不冲突。

元素去除系统

除了核心的视频数量限制,脚本还提供了一组可选的 UI 元素去除预设,涵盖首页轮播图、频道导航、创作中心入口、直播入口等 13 个可去除元素。每个预设包含多版本选择器,适配 B 站不同的 DOM 版本。

多语言支持

内置完整的 i18n 系统,支持简体中文、繁体中文、英文,自动根据浏览器语言切换:

1
2
3
4
5
6
7
8
9
10
function ResolveLanguage() {
if (Config.Language && Config.Language !== 'auto') {
return Config.Language;
}
const Nav = (navigator.language || '').toLowerCase();
if (/^zh-(tw|hk|mo)$/i.test(Nav) || /^zh-(hant)$/i.test(Nav)) return 'zh_TW';
if (/^zh/i.test(Nav)) return 'zh_CN';
if (/^en/i.test(Nav)) return 'en_US';
return 'zh_CN';
}

所有用户可见文本通过 T(key, ...args) 函数统一翻译,键缺失时自动回退到中文,不会出现英文报错。语言可在配置面板中手动切换,切换后立即生效。

持久化与实时生效

所有配置通过 GM_setValue / GM_getValue 持久化,刷新页面不丢失。配置变更后立即生效,无需手动刷新。同时包含定时后备检查(每 5 秒),防止 B 站 SPA 的动态加载导致视频数量超出限制,这是一种防御性编程,确保极端情况下也能正常工作。

用户反馈

没想到会有人愿意用这东西。

一开始只是自己用,顺手传到了 GreasyFork,想着万一有人需要呢。后来真有人说不用被首页轰炸了,还有人问能不能加某某功能。

看到这些还挺高兴的。

现在的互联网产品越做越重,功能越堆越多。但用户想要的,有时候就是清静一点。在产品和用户之间,BiliCompact 尝试稍微往用户这边拉一拉。

如果你也装了它,觉得有用,那我很高兴。觉得哪里不好用,也欢迎提。

代码完全开源,MIT 协议,想改就改,想删就删,没什么藏着掖着的。

总结

BiliCompact 在功能上并不复杂,它的设计重心在稳定性非侵入性上。一些值得借鉴的点:

  1. 选择器多梯队探测,应对频繁的前端改版
  2. 非侵入式 UI,不污染页面 DOM,降低耦合
  3. Shadow DOM 穿透,处理现代前端框架的封装
  4. 防御性定时检查,SPA 动态加载的兜底策略

评论