@kangsicj/nexussync 1.2.0 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/dist/NexusSync.meta.js +2 -1
- package/dist/NexusSync.user.js +134 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
<br>
|
|
6
6
|
|
|
7
|
-
[](./CHANGELOG.md)
|
|
8
8
|
[](https://www.tampermonkey.net/)
|
|
9
9
|
[](https://apps.apple.com/app/userscripts/id1463298887)
|
|
10
10
|
[](https://fairy.hada.io/@jinny)
|
|
@@ -74,6 +74,7 @@ No subtitles on that video? Turn on live speech recognition with translation int
|
|
|
74
74
|
- **Make it yours** — pick your name and colour; your bubbles and doodles wear it on every screen.
|
|
75
75
|
- **Calm, never jumpy** — phones going to sleep, slow Wi-Fi or a player restarting on its own never pause the whole room.
|
|
76
76
|
- **No accidental pauses** — tapping the chat, a doodle or a reaction never nudges the video for anyone.
|
|
77
|
+
- **No ad interruptions** — ad boxes and links laid over the video are hidden while you watch, and ad pop-ups opened by a tap on the player are stopped.
|
|
77
78
|
- **Invisible until needed** — the ⚙️ button appears only next to real videos, never on ordinary pages.
|
|
78
79
|
- **Private rooms** — everything you share in a room is end-to-end encrypted; only people with the PIN can join.
|
|
79
80
|
|
package/dist/NexusSync.meta.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// @icon https://cdn.jsdelivr.net/npm/@kangsicj/nexussync@latest/assets/icon.svg
|
|
9
9
|
// @updateURL https://cdn.jsdelivr.net/npm/@kangsicj/nexussync@latest/dist/NexusSync.meta.js
|
|
10
10
|
// @downloadURL https://cdn.jsdelivr.net/npm/@kangsicj/nexussync@latest/dist/NexusSync.user.js
|
|
11
|
-
// @version 1.2.
|
|
11
|
+
// @version 1.2.1
|
|
12
12
|
// @description Watch any web video together in perfect sync: cinema mode, live AI subtitles & translation, chat bubbles with auto-translation, quick ink, cover, go-together to the next episode.
|
|
13
13
|
// @description:ko 어떤 웹 영상이든 완벽한 싱크로 함께 보기: 시네마 모드, 실시간 AI 자막·번역, 자동 번역 말풍선 채팅, 잠깐 그리기, 가리기, 다음 화도 함께.
|
|
14
14
|
// @match *://*/*
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
// @grant GM.getValue
|
|
25
25
|
// @grant GM.listValues
|
|
26
26
|
// @grant GM.xmlHttpRequest
|
|
27
|
+
// @grant unsafeWindow
|
|
27
28
|
// @connect translate.googleapis.com
|
|
28
29
|
// @connect nexussync-dd5b4-default-rtdb.asia-southeast1.firebasedatabase.app
|
|
29
30
|
// @connect api.openai.com
|
package/dist/NexusSync.user.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// @icon https://cdn.jsdelivr.net/npm/@kangsicj/nexussync@latest/assets/icon.svg
|
|
9
9
|
// @updateURL https://cdn.jsdelivr.net/npm/@kangsicj/nexussync@latest/dist/NexusSync.meta.js
|
|
10
10
|
// @downloadURL https://cdn.jsdelivr.net/npm/@kangsicj/nexussync@latest/dist/NexusSync.user.js
|
|
11
|
-
// @version 1.2.
|
|
11
|
+
// @version 1.2.1
|
|
12
12
|
// @description Watch any web video together in perfect sync: cinema mode, live AI subtitles & translation, chat bubbles with auto-translation, quick ink, cover, go-together to the next episode.
|
|
13
13
|
// @description:ko 어떤 웹 영상이든 완벽한 싱크로 함께 보기: 시네마 모드, 실시간 AI 자막·번역, 자동 번역 말풍선 채팅, 잠깐 그리기, 가리기, 다음 화도 함께.
|
|
14
14
|
// @match *://*/*
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
// @grant GM.getValue
|
|
25
25
|
// @grant GM.listValues
|
|
26
26
|
// @grant GM.xmlHttpRequest
|
|
27
|
+
// @grant unsafeWindow
|
|
27
28
|
// @connect translate.googleapis.com
|
|
28
29
|
// @connect nexussync-dd5b4-default-rtdb.asia-southeast1.firebasedatabase.app
|
|
29
30
|
// @connect api.openai.com
|
|
@@ -42,12 +43,12 @@
|
|
|
42
43
|
|
|
43
44
|
// The version of the code that is actually running (the manager's metadata can lag
|
|
44
45
|
// behind when an old copy is still installed).
|
|
45
|
-
const NXU_VERSION = '1.2.
|
|
46
|
+
const NXU_VERSION = '1.2.1';
|
|
46
47
|
// Rank used by the one-instance guard below and to tell newer builds apart in a room.
|
|
47
48
|
// It is independent of the public version (1.0.0 is the first public release, after
|
|
48
49
|
// private builds that reached ranks up to 10.2.0), so every older copy still installed
|
|
49
50
|
// steps aside instead of this release.
|
|
50
|
-
const NXU_RANK = '11.2.
|
|
51
|
+
const NXU_RANK = '11.2.1';
|
|
51
52
|
const NXU_SESSION = Math.random().toString(36).slice(2, 10); // this page load // raise with every release
|
|
52
53
|
// App icon (same artwork as assets/icon.svg / @icon), inlined so it shows offline too.
|
|
53
54
|
const NXU_ICON = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiByb2xlPSJpbWciIGFyaWEtbGFiZWw9Ik5leHVzU3luYyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJueGJnIiB4MT0iMCIgeTE9IjAiIHgyPSIxIiB5Mj0iMSI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjNWIyZWZmIi8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjZmY0ZmEzIi8+PC9saW5lYXJHcmFkaWVudD48cmFkaWFsR3JhZGllbnQgaWQ9Im54Z2xvdyIgY3g9IjAuODIiIGN5PSIwLjE0IiByPSIwLjciPjxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iIzdjZjlmZiIgc3RvcC1vcGFjaXR5PSIwLjU1Ii8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjN2NmOWZmIiBzdG9wLW9wYWNpdHk9IjAiLz48L3JhZGlhbEdyYWRpZW50PjxsaW5lYXJHcmFkaWVudCBpZD0ibnhwbGF5IiB4MT0iMCIgeTE9IjAiIHgyPSIxIiB5Mj0iMSI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjZmZmZmZmIi8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjZmZlM2YxIi8+PC9saW5lYXJHcmFkaWVudD48Y2xpcFBhdGggaWQ9Im54ciI+PHJlY3Qgd2lkdGg9IjI1NiIgaGVpZ2h0PSIyNTYiIHJ4PSI1OCIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNueHIpIj48cmVjdCB3aWR0aD0iMjU2IiBoZWlnaHQ9IjI1NiIgZmlsbD0idXJsKCNueGJnKSIvPjxyZWN0IHdpZHRoPSIyNTYiIGhlaWdodD0iMjU2IiBmaWxsPSJ1cmwoI254Z2xvdykiLz48ZyBmaWxsPSJub25lIiBzdHJva2U9IiNmZmZmZmYiIHN0cm9rZS13aWR0aD0iMTUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCI+PHBhdGggZD0iTTYwIDExNiBBNzAgNzAgMCAwIDEgMTc4IDcyIi8+PHBhdGggZD0iTTE5NiAxNDAgQTcwIDcwIDAgMCAxIDc4IDE4NCIvPjwvZz48cGF0aCBkPSJNMTg2IDUwIEwyMDAgOTIgTDE1NyA4NiBaIiBmaWxsPSIjZmZmZmZmIi8+PHBhdGggZD0iTTcwIDIwNiBMNTYgMTY0IEw5OSAxNzAgWiIgZmlsbD0iI2ZmZmZmZiIvPjxwYXRoIGQ9Ik0xMTAgOTIgUTEwNCA4OCAxMDQgOTYgTDEwNCAxNjAgUTEwNCAxNjggMTEwIDE2NCBMMTYyIDEzMiBRMTY4IDEyOCAxNjIgMTI0IFoiIGZpbGw9InVybCgjbnhwbGF5KSIvPjwvZz48L3N2Zz4=';
|
|
@@ -173,7 +174,7 @@
|
|
|
173
174
|
'nxu_idle_timeout', 'nxu_sub_size', 'nxu_sub_opacity', 'nxu_sub_position', 'nxu_speaker_language',
|
|
174
175
|
'nxu_subtitle_language', 'nxu_audio_mode', 'nxu_translation_provider', 'nxu_stt_local_first',
|
|
175
176
|
'nxu_smart_speech_locale', 'nxu_fab_pos', 'nxu_asr_provider', 'nxu_key_openai', 'nxu_key_anthropic', 'nxu_key_groq', 'nxu_key_deepl',
|
|
176
|
-
'nxu_model_openai', 'nxu_model_anthropic', 'nxu_cinema_ambient', 'nxu_cinema_fit', 'nxu_cc_remaining', 'nxu_cinema_controls', 'nxu_nickname', 'nxu_color', 'nxu_quick_replies', 'nxu_chat_translate', 'nxu_sync_offset', 'nxu_cover_band', 'nxu_cover_marks', 'nxu_travel', 'nxu_auto_next', 'nxu_auto_cinema_sites', 'nxu_glossary', 'nxu_panel_size_chat-panel', 'nxu_panel_size_sub-log-panel', 'nxu_panel_size_debug-panel', 'nxu_panel_size_trans-modal', 'nxu_cinema_rotate', 'nxu_rotate_denied_' + HOST
|
|
177
|
+
'nxu_model_openai', 'nxu_model_anthropic', 'nxu_cinema_ambient', 'nxu_cinema_fit', 'nxu_cc_remaining', 'nxu_cinema_controls', 'nxu_nickname', 'nxu_color', 'nxu_quick_replies', 'nxu_chat_translate', 'nxu_sync_offset', 'nxu_cover_band', 'nxu_cover_marks', 'nxu_travel', 'nxu_auto_next', 'nxu_auto_cinema_sites', 'nxu_ad_guard', 'nxu_glossary', 'nxu_panel_size_chat-panel', 'nxu_panel_size_sub-log-panel', 'nxu_panel_size_debug-panel', 'nxu_panel_size_trans-modal', 'nxu_cinema_rotate', 'nxu_rotate_denied_' + HOST
|
|
177
178
|
]);
|
|
178
179
|
|
|
179
180
|
// ───────────────────────── Cross-origin HTTP (GM xhr first, fetch fallback)
|
|
@@ -485,9 +486,31 @@
|
|
|
485
486
|
return w * h;
|
|
486
487
|
}
|
|
487
488
|
|
|
489
|
+
// Vibration. Android: the Vibration API. iPhone/iPad have no Vibration API; Safari 18+
|
|
490
|
+
// gives a light tap when a switch control is toggled, which works inside a tap only.
|
|
491
|
+
let _hapticSwitch = null;
|
|
488
492
|
function triggerHaptic(type = 'light') {
|
|
489
|
-
if (
|
|
490
|
-
|
|
493
|
+
if (navigator.vibrate) {
|
|
494
|
+
try { navigator.vibrate(type === 'heavy' ? [100, 50, 100] : 15); } catch (_) { }
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
if (!IS_IOS) return;
|
|
498
|
+
try {
|
|
499
|
+
if (!_hapticSwitch || !_hapticSwitch.isConnected) {
|
|
500
|
+
const label = document.createElement('label');
|
|
501
|
+
label.setAttribute('aria-hidden', 'true');
|
|
502
|
+
label.style.cssText = 'position:fixed;left:-9999px;top:0;width:1px;height:1px;overflow:hidden;opacity:0;pointer-events:none;';
|
|
503
|
+
const input = document.createElement('input');
|
|
504
|
+
input.type = 'checkbox';
|
|
505
|
+
input.setAttribute('switch', '');
|
|
506
|
+
input.tabIndex = -1;
|
|
507
|
+
label.appendChild(input);
|
|
508
|
+
(document.body || document.documentElement).appendChild(label);
|
|
509
|
+
_hapticSwitch = label;
|
|
510
|
+
}
|
|
511
|
+
const pulses = type === 'heavy' ? 3 : 1;
|
|
512
|
+
for (let i = 0; i < pulses; i++) setTimeout(() => { try { _hapticSwitch.click(); } catch (_) { } }, i * 110);
|
|
513
|
+
} catch (_) { }
|
|
491
514
|
}
|
|
492
515
|
|
|
493
516
|
function generateUserColor(id) {
|
|
@@ -624,7 +647,7 @@
|
|
|
624
647
|
travelBack: 'Back in the room', travelMoved: 'Moved to “{title}” · tap to go together', travelGoing: 'Going together…',
|
|
625
648
|
chatTrOn: 'Auto-translate: on', chatTrOff: 'Auto-translate: off', chatTrShowOrig: 'Tap to see the original', chatTrShowTr: 'Tap to see the translation', chatTrFrom: 'Translated from {lang}',
|
|
626
649
|
inkClosing: 'Closing soon — move or tap to keep drawing',
|
|
627
|
-
modeSitePlayer: 'Site player in use', modeNexusPlayer: 'NexusSync player in use', modeSiteHint: 'Change subtitles or the server in the site’s player, then switch.', modeSiteHintAuto: 'Cinema started by itself. Pick subtitles or a server in the site’s player first if needed, then switch.', modeNexusHint: 'Everything is controlled by NexusSync. The site’s own settings are one tap away.', modeUseNexus: 'Use NexusSync player', modeUseSite: 'Use site player', autoCinemaStopSite: 'No auto-cinema on this site', autoCinemaOnSite: 'Auto-cinema on for {site}', autoCinemaOffSite: 'Auto-cinema off for {site}', autoCinemaBtnOn: 'Auto-cinema: on (this site)', autoCinemaBtnOff: 'Auto-cinema: off (this site)', ccPip: 'Picture-in-Picture (keep watching in a small window)', nextEpTitle: 'Up next', nextEpGo: 'Next episode', nextEpCancel: 'Stay on this episode', nextEpGoing: 'Next episode…', talkMenu: 'Chat', inkMenu: 'Quick sketch', quickEdit: 'Edit quick replies', quickEditHint: 'Separate with |', quickReset: 'Back to the default replies', save: 'Save',
|
|
650
|
+
modeSitePlayer: 'Site player in use', modeNexusPlayer: 'NexusSync player in use', modeSiteHint: 'Change subtitles or the server in the site’s player, then switch.', modeSiteHintAuto: 'Cinema started by itself. Pick subtitles or a server in the site’s player first if needed, then switch.', modeNexusHint: 'Everything is controlled by NexusSync. The site’s own settings are one tap away.', modeUseNexus: 'Use NexusSync player', modeUseSite: 'Use site player', autoCinemaStopSite: 'No auto-cinema on this site', autoCinemaOnSite: 'Auto-cinema on for {site}', autoCinemaOffSite: 'Auto-cinema off for {site}', autoCinemaBtnOn: 'Auto-cinema: on (this site)', autoCinemaBtnOff: 'Auto-cinema: off (this site)', adGuardOn: 'Ad guard: on', adGuardOff: 'Ad guard: off', adGuardHid: 'Hid an ad / link laid over the video', adGuardPopup: 'Stopped an ad pop-up', ccPip: 'Picture-in-Picture (keep watching in a small window)', nextEpTitle: 'Up next', nextEpGo: 'Next episode', nextEpCancel: 'Stay on this episode', nextEpGoing: 'Next episode…', talkMenu: 'Chat', inkMenu: 'Quick sketch', quickEdit: 'Edit quick replies', quickEditHint: 'Separate with |', quickReset: 'Back to the default replies', save: 'Save',
|
|
628
651
|
talkTitle: 'Chat bubble', talkPlaceholder: 'Say something…', talkReplyTo: '↩ Reply to {name}', talkClose: 'Close', talkSend: 'Send', talkFull: 'Open the chat history (every message so far)', talkHistory: 'History',
|
|
629
652
|
inkTitle: 'Quick sketch (fades away)', inkHint: 'Draw on the screen — it fades in a few seconds', inkDone: 'Done', inkBubble: 'is sketching on the screen — tap to draw back',
|
|
630
653
|
you: 'You', roomBuffering: '⏳ {name} is loading… everyone waits', roomResume: '▶️ Everyone is ready — playing together', roomRate: '⏩ Speed {rate}x synced',
|
|
@@ -707,7 +730,7 @@
|
|
|
707
730
|
travelBack: '방에 다시 연결했어요', travelMoved: '「{title}」(으)로 이동했어요 · 눌러서 같이 가기', travelGoing: '같이 이동하는 중…',
|
|
708
731
|
chatTrOn: '자동 번역 켜짐', chatTrOff: '자동 번역 꺼짐', chatTrShowOrig: '눌러서 원문 보기', chatTrShowTr: '눌러서 번역 보기', chatTrFrom: '{lang}에서 번역됨',
|
|
709
732
|
inkClosing: '곧 닫혀요 — 계속 그리려면 움직이거나 눌러 주세요',
|
|
710
|
-
modeSitePlayer: '사이트 플레이어 사용 중', modeNexusPlayer: 'NexusSync 플레이어 사용 중', modeSiteHint: '자막이나 서버는 사이트 플레이어에서 바꾼 뒤 전환하세요.', modeSiteHintAuto: '시네마 모드가 자동으로 켜졌어요. 필요하면 사이트 플레이어에서 자막·서버를 먼저 고른 뒤 전환하세요.', modeNexusHint: 'NexusSync가 모든 조작을 맡아요. 사이트 설정은 한 번 탭으로 돌아갈 수 있어요.', modeUseNexus: 'NexusSync 플레이어로', modeUseSite: '사이트 플레이어로', autoCinemaStopSite: '이 사이트는 자동 시네마 끄기', autoCinemaOnSite: '{site}에서 자동 시네마 켜짐', autoCinemaOffSite: '{site}에서 자동 시네마 꺼짐', autoCinemaBtnOn: '자동 시네마: 켬 (이 사이트)', autoCinemaBtnOff: '자동 시네마: 끔 (이 사이트)', ccPip: 'PiP (작은 창으로 계속 보기)', nextEpTitle: '다음 화', nextEpGo: '다음 화 보기', nextEpCancel: '이번 화에 머물기', nextEpGoing: '다음 화로 이동하는 중…', talkMenu: '말풍선 채팅', inkMenu: '잠깐 그리기', quickEdit: '빠른 답장 편집', quickEditHint: '| 로 구분', quickReset: '기본 답장으로', save: '저장',
|
|
733
|
+
modeSitePlayer: '사이트 플레이어 사용 중', modeNexusPlayer: 'NexusSync 플레이어 사용 중', modeSiteHint: '자막이나 서버는 사이트 플레이어에서 바꾼 뒤 전환하세요.', modeSiteHintAuto: '시네마 모드가 자동으로 켜졌어요. 필요하면 사이트 플레이어에서 자막·서버를 먼저 고른 뒤 전환하세요.', modeNexusHint: 'NexusSync가 모든 조작을 맡아요. 사이트 설정은 한 번 탭으로 돌아갈 수 있어요.', modeUseNexus: 'NexusSync 플레이어로', modeUseSite: '사이트 플레이어로', autoCinemaStopSite: '이 사이트는 자동 시네마 끄기', autoCinemaOnSite: '{site}에서 자동 시네마 켜짐', autoCinemaOffSite: '{site}에서 자동 시네마 꺼짐', autoCinemaBtnOn: '자동 시네마: 켬 (이 사이트)', autoCinemaBtnOff: '자동 시네마: 끔 (이 사이트)', adGuardOn: '광고 가리기: 켬', adGuardOff: '광고 가리기: 끔', adGuardHid: '영상 위에 덮인 광고·링크를 숨겼어요', adGuardPopup: '광고 팝업을 막았어요', ccPip: 'PiP (작은 창으로 계속 보기)', nextEpTitle: '다음 화', nextEpGo: '다음 화 보기', nextEpCancel: '이번 화에 머물기', nextEpGoing: '다음 화로 이동하는 중…', talkMenu: '말풍선 채팅', inkMenu: '잠깐 그리기', quickEdit: '빠른 답장 편집', quickEditHint: '| 로 구분', quickReset: '기본 답장으로', save: '저장',
|
|
711
734
|
talkTitle: '말풍선 채팅', talkPlaceholder: '메시지 보내기…', talkReplyTo: '↩ {name} 님에게 답장', talkClose: '닫기', talkSend: '보내기', talkFull: '지금까지의 대화 기록 전체 보기', talkHistory: '대화 기록',
|
|
712
735
|
inkTitle: '잠깐 그리기 (몇 초 뒤 사라짐)', inkHint: '화면에 그려 보세요 — 몇 초 뒤 사라져요', inkDone: '완료', inkBubble: '님이 화면에 그리고 있어요 — 탭해서 같이 그리기',
|
|
713
736
|
you: '나', roomBuffering: '⏳ {name} 기기 로딩 중… 모두 잠시 대기', roomResume: '▶️ 모두 준비됨 — 함께 재생합니다', roomRate: '⏩ {rate}배속 동기화',
|
|
@@ -6733,7 +6756,9 @@
|
|
|
6733
6756
|
/* together */
|
|
6734
6757
|
.react-sep { width:1px; align-self:stretch; background:rgba(255,255,255,0.18); margin:2px 2px; }
|
|
6735
6758
|
.nexus-container.nx-shake { animation:nxShake 0.5s ease; }
|
|
6736
|
-
|
|
6759
|
+
/* "translate", not "transform": the container is turned 90° with transform in iPhone
|
|
6760
|
+
landscape view, and animating transform briefly put it back upright. */
|
|
6761
|
+
@keyframes nxShake { 15% { translate:-6px 0; } 30% { translate:6px 0; } 45% { translate:-5px 0; } 60% { translate:4px 0; } 80% { translate:-2px 0; } 100% { translate:0 0; } }
|
|
6737
6762
|
.nx-ping-catcher { position:absolute; inset:0; z-index:2100; pointer-events:auto; cursor:crosshair; background:rgba(0,0,0,0.12); }
|
|
6738
6763
|
.nx-ping-hint { position:absolute; top:calc(16px + env(safe-area-inset-top, 0px)); left:50%; transform:translateX(-50%); background:rgba(0,0,0,0.75); color:#fff; padding:8px 16px; border-radius:999px; font-size:13px; font-weight:700; white-space:nowrap; }
|
|
6739
6764
|
.nx-ping { position:absolute; z-index:2050; width:0; height:0; pointer-events:none; }
|
|
@@ -6956,6 +6981,9 @@
|
|
|
6956
6981
|
}
|
|
6957
6982
|
shadow = host.attachShadow({ mode: 'open' });
|
|
6958
6983
|
UI_GUARD.install(host, shadow);
|
|
6984
|
+
// Taps on NexusSync's own controls stop here: many video sites open an ad tab
|
|
6985
|
+
// ("speed test", pop-unders) from a click listener on the whole page.
|
|
6986
|
+
['click', 'dblclick', 'auxclick', 'contextmenu', 'mousedown', 'pointerdown', 'touchstart'].forEach(type => host.addEventListener(type, (e) => e.stopPropagation()));
|
|
6959
6987
|
if (!SUPPORTS_TOP_LAYER) {
|
|
6960
6988
|
// Without the top layer, paint order decides between equal z-indexes: stay last.
|
|
6961
6989
|
new MutationObserver(() => {
|
|
@@ -7005,6 +7033,11 @@
|
|
|
7005
7033
|
const btnCinema = mk('nxu-cinema', '🍿 ' + t('cinema'), t('cinemaTitle'));
|
|
7006
7034
|
const btnAutoCinema = mk('nxu-auto-cinema', '');
|
|
7007
7035
|
btnAutoCinema.classList.add('subtle');
|
|
7036
|
+
const btnAdGuard = mk('nxu-ad-guard', '');
|
|
7037
|
+
btnAdGuard.classList.add('subtle');
|
|
7038
|
+
const paintAdGuard = () => { const on = AD_GUARD.enabled(); btnAdGuard.textContent = '🛡️ ' + t(on ? 'adGuardOn' : 'adGuardOff'); btnAdGuard.classList.toggle('active', on); };
|
|
7039
|
+
paintAdGuard();
|
|
7040
|
+
btnAdGuard.onclick = () => { STORE.set('nxu_ad_guard', !AD_GUARD.enabled()); paintAdGuard(); showToast('🛡️ ' + t(AD_GUARD.enabled() ? 'adGuardOn' : 'adGuardOff')); };
|
|
7008
7041
|
const btnPiP = mk('nxu-pip', '📺 PiP');
|
|
7009
7042
|
const btnRate = mk('nxu-rate', '⏩ 1x', t('rateTitle'));
|
|
7010
7043
|
// Talking happens in speech bubbles and quick ink now: the menu opens those directly
|
|
@@ -7118,7 +7151,7 @@
|
|
|
7118
7151
|
toast.setAttribute('role', 'status');
|
|
7119
7152
|
|
|
7120
7153
|
// Sketching and chat live in the reaction bar (🖍️ 💬); the menu does not repeat them.
|
|
7121
|
-
dock.append(btnFindMedia, btnLink, btnInvite, btnSyncStatus, btnCinema, btnAutoCinema, btnPiP, btnRate, btnBookmark, btnTranslate, btnSubLog, btnDebug, btnHideSite);
|
|
7154
|
+
dock.append(btnFindMedia, btnLink, btnInvite, btnSyncStatus, btnCinema, btnAutoCinema, btnAdGuard, btnPiP, btnRate, btnBookmark, btnTranslate, btnSubLog, btnDebug, btnHideSite);
|
|
7122
7155
|
uiWrapper.append(dock, reactPill, fab);
|
|
7123
7156
|
nexusContainer.append(drawCanvas, uiWrapper, chatPanel, subLogPanel, manualModal, transModal, toast, debugPanel);
|
|
7124
7157
|
shadow.appendChild(nexusContainer);
|
|
@@ -8205,6 +8238,97 @@
|
|
|
8205
8238
|
}
|
|
8206
8239
|
};
|
|
8207
8240
|
|
|
8241
|
+
// ───────────────────────── Ad guard
|
|
8242
|
+
// While a video is being watched, links and ad boxes that sites lay over the picture
|
|
8243
|
+
// ("speed test", "download", sponsor banners) are hidden, and new tabs a site tries to
|
|
8244
|
+
// open to another website right after a tap on the player are stopped.
|
|
8245
|
+
const AD_GUARD = {
|
|
8246
|
+
RX_AD: /(speed\s*test|speedtest|속도\s*(측정|테스트)|광고|advert|sponsor|click\s*here|download\s*now|install\s*now|casino|betting|vpn|지금\s*(설치|다운|확인)|무료\s*(다운|설치)|바로가기)/i,
|
|
8247
|
+
RX_CONTROL: /(control|vjs-|jw-|plyr|shaka|ytp-|caption|subtitle|cue|track|progress|seek|volume|menu|setting|spinner|loading|poster|time|title|nxu)/i,
|
|
8248
|
+
hidden: new WeakSet(),
|
|
8249
|
+
_count: 0,
|
|
8250
|
+
_toasted: false,
|
|
8251
|
+
_blockedPopups: 0,
|
|
8252
|
+
enabled() { return STORE.get('nxu_ad_guard', true) !== false; },
|
|
8253
|
+
_watching() { const v = STATE.video; return !!v && v.isConnected && !v.paused && !v.ended && v.readyState >= 2; },
|
|
8254
|
+
_otherOrigin(href) { try { const u = new URL(href, location.href); return /^https?:$/.test(u.protocol) && u.origin !== location.origin; } catch (_) { return false; } },
|
|
8255
|
+
_adLike(el) {
|
|
8256
|
+
if (el.tagName === 'IFRAME') {
|
|
8257
|
+
if (VIDEO_DETECTOR.frameReports.has(el)) return false; // the player itself
|
|
8258
|
+
return this._otherOrigin(el.src || '');
|
|
8259
|
+
}
|
|
8260
|
+
const link = el.matches('a[href]') ? el : el.querySelector?.('a[href]');
|
|
8261
|
+
const outLink = !!link && this._otherOrigin(link.href) && !/^(#|javascript:)/i.test(link.getAttribute('href') || '');
|
|
8262
|
+
const text = (el.textContent || '').trim().slice(0, 200);
|
|
8263
|
+
const clicky = outLink || el.matches('a, [onclick]') || getComputedStyle(el).cursor === 'pointer';
|
|
8264
|
+
if (outLink && (link.target === '_blank' || this.RX_AD.test(text) || el.querySelector('img, iframe'))) return true;
|
|
8265
|
+
return clicky && this.RX_AD.test(text);
|
|
8266
|
+
},
|
|
8267
|
+
scan() {
|
|
8268
|
+
if (!this.enabled() || !this._watching()) return;
|
|
8269
|
+
const v = STATE.video;
|
|
8270
|
+
const r = v.getBoundingClientRect();
|
|
8271
|
+
if (r.width < 240 || r.height < 135) return;
|
|
8272
|
+
const seen = new Set();
|
|
8273
|
+
for (const fx of [0.15, 0.5, 0.85]) for (const fy of [0.15, 0.5, 0.85]) {
|
|
8274
|
+
const x = r.left + r.width * fx, y = r.top + r.height * fy;
|
|
8275
|
+
if (x < 0 || y < 0 || x > innerWidth || y > innerHeight) continue;
|
|
8276
|
+
let stack;
|
|
8277
|
+
try { stack = document.elementsFromPoint(x, y); } catch (_) { continue; }
|
|
8278
|
+
for (const el of stack) {
|
|
8279
|
+
if (el === v || el.contains(v)) break; // reached the picture
|
|
8280
|
+
if (seen.has(el) || this.hidden.has(el) || el.closest('#nxu-host') || el.id === 'nxu-host') continue;
|
|
8281
|
+
seen.add(el);
|
|
8282
|
+
// Climb to the whole overlay box (but never to something holding the video).
|
|
8283
|
+
let box = el;
|
|
8284
|
+
while (box.parentElement && !box.parentElement.contains(v) && box.parentElement !== document.body) box = box.parentElement;
|
|
8285
|
+
if (this.hidden.has(box)) continue;
|
|
8286
|
+
const sig = `${box.id} ${typeof box.className === 'string' ? box.className : ''}`;
|
|
8287
|
+
if (this.RX_CONTROL.test(sig)) continue;
|
|
8288
|
+
const br = box.getBoundingClientRect();
|
|
8289
|
+
if (br.width * br.height > r.width * r.height * 0.92 && !box.querySelector?.('a[href], iframe')) continue; // likely the player's own click layer
|
|
8290
|
+
if (!this._adLike(box) && !this._adLike(el)) continue;
|
|
8291
|
+
this.hide(box);
|
|
8292
|
+
}
|
|
8293
|
+
}
|
|
8294
|
+
},
|
|
8295
|
+
hide(el) {
|
|
8296
|
+
this.hidden.add(el);
|
|
8297
|
+
el.setAttribute('data-nxu-adguard', '');
|
|
8298
|
+
el.style.setProperty('display', 'none', 'important');
|
|
8299
|
+
this._count++;
|
|
8300
|
+
logDebug('Ad guard: hid an overlay on the video (' + (el.tagName || '') + ')', 'info');
|
|
8301
|
+
if (!this._toasted && IS_TOP) { this._toasted = true; showToast('🛡️ ' + t('adGuardHid')); }
|
|
8302
|
+
if (!IS_TOP) FRAME.postToParent({ type: 'NXU_ACTIVITY' });
|
|
8303
|
+
},
|
|
8304
|
+
// window.open from the page while watching: a tab to another website opened right after
|
|
8305
|
+
// a tap on the player is almost always an ad (pop-under). Same-site windows still open.
|
|
8306
|
+
installPopupGuard() {
|
|
8307
|
+
const W = typeof unsafeWindow !== 'undefined' && unsafeWindow ? unsafeWindow : null;
|
|
8308
|
+
if (!W || typeof W.open !== 'function' || W.__nxuPopupGuard) return;
|
|
8309
|
+
const orig = W.open;
|
|
8310
|
+
const self = this;
|
|
8311
|
+
try {
|
|
8312
|
+
W.__nxuPopupGuard = true;
|
|
8313
|
+
W.open = function (url, ...rest) {
|
|
8314
|
+
try {
|
|
8315
|
+
const busy = self.enabled() && (self._watching() || STATE.isCinema || STATE.handshakeVerified);
|
|
8316
|
+
const recentTap = Date.now() - ((STATE._lastPlayerGesture && STATE._lastPlayerGesture.at) || 0) < 2000;
|
|
8317
|
+
if (busy && recentTap && url && self._otherOrigin(String(url))) {
|
|
8318
|
+
self._blockedPopups++;
|
|
8319
|
+
logDebug('Ad guard: stopped a pop-up to ' + String(url).slice(0, 80), 'warn');
|
|
8320
|
+
if (IS_TOP) showToast('🛡️ ' + t('adGuardPopup'));
|
|
8321
|
+
return null;
|
|
8322
|
+
}
|
|
8323
|
+
} catch (_) { }
|
|
8324
|
+
return orig.apply(this, [url, ...rest]);
|
|
8325
|
+
};
|
|
8326
|
+
} catch (_) { }
|
|
8327
|
+
}
|
|
8328
|
+
};
|
|
8329
|
+
AD_GUARD.installPopupGuard();
|
|
8330
|
+
setInterval(() => { try { AD_GUARD.scan(); } catch (_) { } }, 1500);
|
|
8331
|
+
|
|
8208
8332
|
// ───────────────────────── Automatic cinema
|
|
8209
8333
|
// Opening a page that is all about one video (an episode, a movie — not a feed with many
|
|
8210
8334
|
// posts) switches to cinema mode as soon as the video is ready. It keeps the site's own
|
package/package.json
CHANGED