@kangsicj/nexussync 1.1.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 +4 -5
- package/dist/NexusSync.user.js +211 -27
- 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
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
// ==UserScript==
|
|
2
|
-
// @name NexusSync
|
|
3
|
-
// @name:ko 넥서스싱크
|
|
4
|
-
// @name:nl-BE NexusSync Ultimate: Canvas & Media Synchronisatie
|
|
2
|
+
// @name NexusSync
|
|
3
|
+
// @name:ko NexusSync (넥서스싱크)
|
|
5
4
|
// @namespace https://github.com/kangsicj/NexusSync
|
|
6
5
|
// @author kangsicj
|
|
7
6
|
// @homepageURL https://github.com/kangsicj/NexusSync
|
|
@@ -9,7 +8,7 @@
|
|
|
9
8
|
// @icon https://cdn.jsdelivr.net/npm/@kangsicj/nexussync@latest/assets/icon.svg
|
|
10
9
|
// @updateURL https://cdn.jsdelivr.net/npm/@kangsicj/nexussync@latest/dist/NexusSync.meta.js
|
|
11
10
|
// @downloadURL https://cdn.jsdelivr.net/npm/@kangsicj/nexussync@latest/dist/NexusSync.user.js
|
|
12
|
-
// @version 1.1
|
|
11
|
+
// @version 1.2.1
|
|
13
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.
|
|
14
13
|
// @description:ko 어떤 웹 영상이든 완벽한 싱크로 함께 보기: 시네마 모드, 실시간 AI 자막·번역, 자동 번역 말풍선 채팅, 잠깐 그리기, 가리기, 다음 화도 함께.
|
|
15
14
|
// @match *://*/*
|
|
@@ -25,6 +24,7 @@
|
|
|
25
24
|
// @grant GM.getValue
|
|
26
25
|
// @grant GM.listValues
|
|
27
26
|
// @grant GM.xmlHttpRequest
|
|
27
|
+
// @grant unsafeWindow
|
|
28
28
|
// @connect translate.googleapis.com
|
|
29
29
|
// @connect nexussync-dd5b4-default-rtdb.asia-southeast1.firebasedatabase.app
|
|
30
30
|
// @connect api.openai.com
|
|
@@ -32,5 +32,4 @@
|
|
|
32
32
|
// @connect api.anthropic.com
|
|
33
33
|
// @connect api-free.deepl.com
|
|
34
34
|
// @connect api.deepl.com
|
|
35
|
-
// @require https://cdnjs.cloudflare.com/ajax/libs/lz-string/1.5.0/lz-string.min.js#sha256=95f4d1cbf099f57161b664bc048426ec3df92637801a4c79116e83315aa787e7
|
|
36
35
|
// ==/UserScript==
|
package/dist/NexusSync.user.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
// ==UserScript==
|
|
2
|
-
// @name NexusSync
|
|
3
|
-
// @name:ko 넥서스싱크
|
|
4
|
-
// @name:nl-BE NexusSync Ultimate: Canvas & Media Synchronisatie
|
|
2
|
+
// @name NexusSync
|
|
3
|
+
// @name:ko NexusSync (넥서스싱크)
|
|
5
4
|
// @namespace https://github.com/kangsicj/NexusSync
|
|
6
5
|
// @author kangsicj
|
|
7
6
|
// @homepageURL https://github.com/kangsicj/NexusSync
|
|
@@ -9,7 +8,7 @@
|
|
|
9
8
|
// @icon https://cdn.jsdelivr.net/npm/@kangsicj/nexussync@latest/assets/icon.svg
|
|
10
9
|
// @updateURL https://cdn.jsdelivr.net/npm/@kangsicj/nexussync@latest/dist/NexusSync.meta.js
|
|
11
10
|
// @downloadURL https://cdn.jsdelivr.net/npm/@kangsicj/nexussync@latest/dist/NexusSync.user.js
|
|
12
|
-
// @version 1.1
|
|
11
|
+
// @version 1.2.1
|
|
13
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.
|
|
14
13
|
// @description:ko 어떤 웹 영상이든 완벽한 싱크로 함께 보기: 시네마 모드, 실시간 AI 자막·번역, 자동 번역 말풍선 채팅, 잠깐 그리기, 가리기, 다음 화도 함께.
|
|
15
14
|
// @match *://*/*
|
|
@@ -25,6 +24,7 @@
|
|
|
25
24
|
// @grant GM.getValue
|
|
26
25
|
// @grant GM.listValues
|
|
27
26
|
// @grant GM.xmlHttpRequest
|
|
27
|
+
// @grant unsafeWindow
|
|
28
28
|
// @connect translate.googleapis.com
|
|
29
29
|
// @connect nexussync-dd5b4-default-rtdb.asia-southeast1.firebasedatabase.app
|
|
30
30
|
// @connect api.openai.com
|
|
@@ -32,20 +32,23 @@
|
|
|
32
32
|
// @connect api.anthropic.com
|
|
33
33
|
// @connect api-free.deepl.com
|
|
34
34
|
// @connect api.deepl.com
|
|
35
|
-
// @require https://cdnjs.cloudflare.com/ajax/libs/lz-string/1.5.0/lz-string.min.js#sha256=95f4d1cbf099f57161b664bc048426ec3df92637801a4c79116e83315aa787e7
|
|
36
35
|
// ==/UserScript==
|
|
37
36
|
|
|
38
37
|
(async function () {
|
|
39
38
|
'use strict';
|
|
40
39
|
|
|
40
|
+
// lz-string 1.5.0 — MIT License, © pieroxy (https://github.com/pieroxy/lz-string). Bundled so
|
|
41
|
+
// no extra file has to be fetched (and verified) by the userscript manager.
|
|
42
|
+
const LZString = (() => { var LZString=function(){var r=String.fromCharCode,o="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",n="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-$",e={};function t(r,o){if(!e[r]){e[r]={};for(var n=0;n<r.length;n++)e[r][r.charAt(n)]=n}return e[r][o]}var i={compressToBase64:function(r){if(null==r)return"";var n=i._compress(r,6,function(r){return o.charAt(r)});switch(n.length%4){default:case 0:return n;case 1:return n+"===";case 2:return n+"==";case 3:return n+"="}},decompressFromBase64:function(r){return null==r?"":""==r?null:i._decompress(r.length,32,function(n){return t(o,r.charAt(n))})},compressToUTF16:function(o){return null==o?"":i._compress(o,15,function(o){return r(o+32)})+" "},decompressFromUTF16:function(r){return null==r?"":""==r?null:i._decompress(r.length,16384,function(o){return r.charCodeAt(o)-32})},compressToUint8Array:function(r){for(var o=i.compress(r),n=new Uint8Array(2*o.length),e=0,t=o.length;e<t;e++){var s=o.charCodeAt(e);n[2*e]=s>>>8,n[2*e+1]=s%256}return n},decompressFromUint8Array:function(o){if(null==o)return i.decompress(o);for(var n=new Array(o.length/2),e=0,t=n.length;e<t;e++)n[e]=256*o[2*e]+o[2*e+1];var s=[];return n.forEach(function(o){s.push(r(o))}),i.decompress(s.join(""))},compressToEncodedURIComponent:function(r){return null==r?"":i._compress(r,6,function(r){return n.charAt(r)})},decompressFromEncodedURIComponent:function(r){return null==r?"":""==r?null:(r=r.replace(/ /g,"+"),i._decompress(r.length,32,function(o){return t(n,r.charAt(o))}))},compress:function(o){return i._compress(o,16,function(o){return r(o)})},_compress:function(r,o,n){if(null==r)return"";var e,t,i,s={},u={},a="",p="",c="",l=2,f=3,h=2,d=[],m=0,v=0;for(i=0;i<r.length;i+=1)if(a=r.charAt(i),Object.prototype.hasOwnProperty.call(s,a)||(s[a]=f++,u[a]=!0),p=c+a,Object.prototype.hasOwnProperty.call(s,p))c=p;else{if(Object.prototype.hasOwnProperty.call(u,c)){if(c.charCodeAt(0)<256){for(e=0;e<h;e++)m<<=1,v==o-1?(v=0,d.push(n(m)),m=0):v++;for(t=c.charCodeAt(0),e=0;e<8;e++)m=m<<1|1&t,v==o-1?(v=0,d.push(n(m)),m=0):v++,t>>=1}else{for(t=1,e=0;e<h;e++)m=m<<1|t,v==o-1?(v=0,d.push(n(m)),m=0):v++,t=0;for(t=c.charCodeAt(0),e=0;e<16;e++)m=m<<1|1&t,v==o-1?(v=0,d.push(n(m)),m=0):v++,t>>=1}0==--l&&(l=Math.pow(2,h),h++),delete u[c]}else for(t=s[c],e=0;e<h;e++)m=m<<1|1&t,v==o-1?(v=0,d.push(n(m)),m=0):v++,t>>=1;0==--l&&(l=Math.pow(2,h),h++),s[p]=f++,c=String(a)}if(""!==c){if(Object.prototype.hasOwnProperty.call(u,c)){if(c.charCodeAt(0)<256){for(e=0;e<h;e++)m<<=1,v==o-1?(v=0,d.push(n(m)),m=0):v++;for(t=c.charCodeAt(0),e=0;e<8;e++)m=m<<1|1&t,v==o-1?(v=0,d.push(n(m)),m=0):v++,t>>=1}else{for(t=1,e=0;e<h;e++)m=m<<1|t,v==o-1?(v=0,d.push(n(m)),m=0):v++,t=0;for(t=c.charCodeAt(0),e=0;e<16;e++)m=m<<1|1&t,v==o-1?(v=0,d.push(n(m)),m=0):v++,t>>=1}0==--l&&(l=Math.pow(2,h),h++),delete u[c]}else for(t=s[c],e=0;e<h;e++)m=m<<1|1&t,v==o-1?(v=0,d.push(n(m)),m=0):v++,t>>=1;0==--l&&(l=Math.pow(2,h),h++)}for(t=2,e=0;e<h;e++)m=m<<1|1&t,v==o-1?(v=0,d.push(n(m)),m=0):v++,t>>=1;for(;;){if(m<<=1,v==o-1){d.push(n(m));break}v++}return d.join("")},decompress:function(r){return null==r?"":""==r?null:i._decompress(r.length,32768,function(o){return r.charCodeAt(o)})},_decompress:function(o,n,e){var t,i,s,u,a,p,c,l=[],f=4,h=4,d=3,m="",v=[],g={val:e(0),position:n,index:1};for(t=0;t<3;t+=1)l[t]=t;for(s=0,a=Math.pow(2,2),p=1;p!=a;)u=g.val&g.position,g.position>>=1,0==g.position&&(g.position=n,g.val=e(g.index++)),s|=(u>0?1:0)*p,p<<=1;switch(s){case 0:for(s=0,a=Math.pow(2,8),p=1;p!=a;)u=g.val&g.position,g.position>>=1,0==g.position&&(g.position=n,g.val=e(g.index++)),s|=(u>0?1:0)*p,p<<=1;c=r(s);break;case 1:for(s=0,a=Math.pow(2,16),p=1;p!=a;)u=g.val&g.position,g.position>>=1,0==g.position&&(g.position=n,g.val=e(g.index++)),s|=(u>0?1:0)*p,p<<=1;c=r(s);break;case 2:return""}for(l[3]=c,i=c,v.push(c);;){if(g.index>o)return"";for(s=0,a=Math.pow(2,d),p=1;p!=a;)u=g.val&g.position,g.position>>=1,0==g.position&&(g.position=n,g.val=e(g.index++)),s|=(u>0?1:0)*p,p<<=1;switch(c=s){case 0:for(s=0,a=Math.pow(2,8),p=1;p!=a;)u=g.val&g.position,g.position>>=1,0==g.position&&(g.position=n,g.val=e(g.index++)),s|=(u>0?1:0)*p,p<<=1;l[h++]=r(s),c=h-1,f--;break;case 1:for(s=0,a=Math.pow(2,16),p=1;p!=a;)u=g.val&g.position,g.position>>=1,0==g.position&&(g.position=n,g.val=e(g.index++)),s|=(u>0?1:0)*p,p<<=1;l[h++]=r(s),c=h-1,f--;break;case 2:return v.join("")}if(0==f&&(f=Math.pow(2,d),d++),l[c])m=l[c];else{if(c!==h)return null;m=i+i.charAt(0)}v.push(m),l[h++]=i+m.charAt(0),i=m,0==--f&&(f=Math.pow(2,d),d++)}}};return i}(); return LZString; })();
|
|
43
|
+
|
|
41
44
|
// The version of the code that is actually running (the manager's metadata can lag
|
|
42
45
|
// behind when an old copy is still installed).
|
|
43
|
-
const NXU_VERSION = '1.1
|
|
46
|
+
const NXU_VERSION = '1.2.1';
|
|
44
47
|
// Rank used by the one-instance guard below and to tell newer builds apart in a room.
|
|
45
48
|
// It is independent of the public version (1.0.0 is the first public release, after
|
|
46
49
|
// private builds that reached ranks up to 10.2.0), so every older copy still installed
|
|
47
50
|
// steps aside instead of this release.
|
|
48
|
-
const NXU_RANK = '11.1
|
|
51
|
+
const NXU_RANK = '11.2.1';
|
|
49
52
|
const NXU_SESSION = Math.random().toString(36).slice(2, 10); // this page load // raise with every release
|
|
50
53
|
// App icon (same artwork as assets/icon.svg / @icon), inlined so it shows offline too.
|
|
51
54
|
const NXU_ICON = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiByb2xlPSJpbWciIGFyaWEtbGFiZWw9Ik5leHVzU3luYyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJueGJnIiB4MT0iMCIgeTE9IjAiIHgyPSIxIiB5Mj0iMSI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjNWIyZWZmIi8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjZmY0ZmEzIi8+PC9saW5lYXJHcmFkaWVudD48cmFkaWFsR3JhZGllbnQgaWQ9Im54Z2xvdyIgY3g9IjAuODIiIGN5PSIwLjE0IiByPSIwLjciPjxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iIzdjZjlmZiIgc3RvcC1vcGFjaXR5PSIwLjU1Ii8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjN2NmOWZmIiBzdG9wLW9wYWNpdHk9IjAiLz48L3JhZGlhbEdyYWRpZW50PjxsaW5lYXJHcmFkaWVudCBpZD0ibnhwbGF5IiB4MT0iMCIgeTE9IjAiIHgyPSIxIiB5Mj0iMSI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjZmZmZmZmIi8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjZmZlM2YxIi8+PC9saW5lYXJHcmFkaWVudD48Y2xpcFBhdGggaWQ9Im54ciI+PHJlY3Qgd2lkdGg9IjI1NiIgaGVpZ2h0PSIyNTYiIHJ4PSI1OCIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNueHIpIj48cmVjdCB3aWR0aD0iMjU2IiBoZWlnaHQ9IjI1NiIgZmlsbD0idXJsKCNueGJnKSIvPjxyZWN0IHdpZHRoPSIyNTYiIGhlaWdodD0iMjU2IiBmaWxsPSJ1cmwoI254Z2xvdykiLz48ZyBmaWxsPSJub25lIiBzdHJva2U9IiNmZmZmZmYiIHN0cm9rZS13aWR0aD0iMTUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCI+PHBhdGggZD0iTTYwIDExNiBBNzAgNzAgMCAwIDEgMTc4IDcyIi8+PHBhdGggZD0iTTE5NiAxNDAgQTcwIDcwIDAgMCAxIDc4IDE4NCIvPjwvZz48cGF0aCBkPSJNMTg2IDUwIEwyMDAgOTIgTDE1NyA4NiBaIiBmaWxsPSIjZmZmZmZmIi8+PHBhdGggZD0iTTcwIDIwNiBMNTYgMTY0IEw5OSAxNzAgWiIgZmlsbD0iI2ZmZmZmZiIvPjxwYXRoIGQ9Ik0xMTAgOTIgUTEwNCA4OCAxMDQgOTYgTDEwNCAxNjAgUTEwNCAxNjggMTEwIDE2NCBMMTYyIDEzMiBRMTY4IDEyOCAxNjIgMTI0IFoiIGZpbGw9InVybCgjbnhwbGF5KSIvPjwvZz48L3N2Zz4=';
|
|
@@ -171,7 +174,7 @@
|
|
|
171
174
|
'nxu_idle_timeout', 'nxu_sub_size', 'nxu_sub_opacity', 'nxu_sub_position', 'nxu_speaker_language',
|
|
172
175
|
'nxu_subtitle_language', 'nxu_audio_mode', 'nxu_translation_provider', 'nxu_stt_local_first',
|
|
173
176
|
'nxu_smart_speech_locale', 'nxu_fab_pos', 'nxu_asr_provider', 'nxu_key_openai', 'nxu_key_anthropic', 'nxu_key_groq', 'nxu_key_deepl',
|
|
174
|
-
'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
|
|
175
178
|
]);
|
|
176
179
|
|
|
177
180
|
// ───────────────────────── Cross-origin HTTP (GM xhr first, fetch fallback)
|
|
@@ -483,9 +486,31 @@
|
|
|
483
486
|
return w * h;
|
|
484
487
|
}
|
|
485
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;
|
|
486
492
|
function triggerHaptic(type = 'light') {
|
|
487
|
-
if (
|
|
488
|
-
|
|
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 (_) { }
|
|
489
514
|
}
|
|
490
515
|
|
|
491
516
|
function generateUserColor(id) {
|
|
@@ -622,7 +647,7 @@
|
|
|
622
647
|
travelBack: 'Back in the room', travelMoved: 'Moved to “{title}” · tap to go together', travelGoing: 'Going together…',
|
|
623
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}',
|
|
624
649
|
inkClosing: 'Closing soon — move or tap to keep drawing',
|
|
625
|
-
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',
|
|
626
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',
|
|
627
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',
|
|
628
653
|
you: 'You', roomBuffering: '⏳ {name} is loading… everyone waits', roomResume: '▶️ Everyone is ready — playing together', roomRate: '⏩ Speed {rate}x synced',
|
|
@@ -705,7 +730,7 @@
|
|
|
705
730
|
travelBack: '방에 다시 연결했어요', travelMoved: '「{title}」(으)로 이동했어요 · 눌러서 같이 가기', travelGoing: '같이 이동하는 중…',
|
|
706
731
|
chatTrOn: '자동 번역 켜짐', chatTrOff: '자동 번역 꺼짐', chatTrShowOrig: '눌러서 원문 보기', chatTrShowTr: '눌러서 번역 보기', chatTrFrom: '{lang}에서 번역됨',
|
|
707
732
|
inkClosing: '곧 닫혀요 — 계속 그리려면 움직이거나 눌러 주세요',
|
|
708
|
-
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: '저장',
|
|
709
734
|
talkTitle: '말풍선 채팅', talkPlaceholder: '메시지 보내기…', talkReplyTo: '↩ {name} 님에게 답장', talkClose: '닫기', talkSend: '보내기', talkFull: '지금까지의 대화 기록 전체 보기', talkHistory: '대화 기록',
|
|
710
735
|
inkTitle: '잠깐 그리기 (몇 초 뒤 사라짐)', inkHint: '화면에 그려 보세요 — 몇 초 뒤 사라져요', inkDone: '완료', inkBubble: '님이 화면에 그리고 있어요 — 탭해서 같이 그리기',
|
|
711
736
|
you: '나', roomBuffering: '⏳ {name} 기기 로딩 중… 모두 잠시 대기', roomResume: '▶️ 모두 준비됨 — 함께 재생합니다', roomRate: '⏩ {rate}배속 동기화',
|
|
@@ -3839,6 +3864,11 @@
|
|
|
3839
3864
|
|
|
3840
3865
|
function _writeVideoStateFields(state) {
|
|
3841
3866
|
yVideo.set('state', _stateForDoc(state));
|
|
3867
|
+
// Each device also keeps its latest decision under its own key. Two devices writing the
|
|
3868
|
+
// shared 'state' at the same moment are settled by Yjs in a fixed order — on a slow link
|
|
3869
|
+
// the same device always won, so the other one's pause could never get through. Keys
|
|
3870
|
+
// that only one device writes are never lost; readers pick the newest decision.
|
|
3871
|
+
yVideo.set(DEVICE_KEY_PREFIX + STATE.userId, _stateForDoc(state));
|
|
3842
3872
|
// Legacy mirror for clients that have not upgraded yet.
|
|
3843
3873
|
yVideo.set('eventId', state.eventId);
|
|
3844
3874
|
yVideo.set('sourceDeviceId', state.sourceDeviceId);
|
|
@@ -3885,8 +3915,12 @@
|
|
|
3885
3915
|
function _repairAcceptedVideoState() {
|
|
3886
3916
|
const accepted = STATE._acceptedVideoState;
|
|
3887
3917
|
if (!accepted || !ydoc || !yVideo) return;
|
|
3918
|
+
// Our own key carries what this device holds as the decision; once it shows the
|
|
3919
|
+
// accepted one, every reader can see it and there is nothing to rewrite.
|
|
3920
|
+
const own = _normalizeVideoState(yVideo.get(DEVICE_KEY_PREFIX + STATE.userId));
|
|
3921
|
+
if (own && own.eventId === accepted.eventId) return;
|
|
3888
3922
|
const current = _normalizeVideoState(yVideo.get('state'));
|
|
3889
|
-
if (current && current.eventId === accepted.eventId) return;
|
|
3923
|
+
if (current && current.eventId === accepted.eventId && own) return;
|
|
3890
3924
|
// Writing back is only for a decision a stale writer covered up, and at most every
|
|
3891
3925
|
// 3 s: two devices that disagree must not keep rewriting the doc at each other.
|
|
3892
3926
|
if (accepted.action === 'heartbeat') return;
|
|
@@ -3897,13 +3931,29 @@
|
|
|
3897
3931
|
logDebug(`[Authority] Restored accepted media state ${accepted.eventId}`, 'warn');
|
|
3898
3932
|
}
|
|
3899
3933
|
|
|
3934
|
+
const DEVICE_KEY_PREFIX = 's:';
|
|
3935
|
+
// The newest decision among the shared key and every device's own key.
|
|
3936
|
+
function _newestVideoState(keys) {
|
|
3937
|
+
let best = null;
|
|
3938
|
+
for (const key of keys) {
|
|
3939
|
+
if (key !== 'state' && !(key.startsWith(DEVICE_KEY_PREFIX) && key !== DEVICE_KEY_PREFIX + STATE.userId)) continue;
|
|
3940
|
+
const st = _normalizeVideoState(yVideo.get(key));
|
|
3941
|
+
if (st && (!best || _isVideoStateNewer(st, best))) best = st;
|
|
3942
|
+
}
|
|
3943
|
+
return best;
|
|
3944
|
+
}
|
|
3900
3945
|
function _readObservedVideoState(event) {
|
|
3901
3946
|
if (!yVideo) return null;
|
|
3902
|
-
if (event && event.keysChanged
|
|
3903
|
-
|
|
3947
|
+
if (event && event.keysChanged) {
|
|
3948
|
+
const keys = [...event.keysChanged].filter(k => k === 'state' || k.startsWith(DEVICE_KEY_PREFIX));
|
|
3949
|
+
if (keys.length) return _newestVideoState(keys);
|
|
3904
3950
|
}
|
|
3905
3951
|
return _readLegacyVideoState();
|
|
3906
3952
|
}
|
|
3953
|
+
function _currentDocVideoState() {
|
|
3954
|
+
if (!yVideo) return null;
|
|
3955
|
+
return _newestVideoState([...yVideo.keys()]) || _readLegacyVideoState();
|
|
3956
|
+
}
|
|
3907
3957
|
|
|
3908
3958
|
function _acceptRemoteVideoState(rawState) {
|
|
3909
3959
|
const state = _normalizeVideoState(rawState);
|
|
@@ -3927,7 +3977,7 @@
|
|
|
3927
3977
|
|
|
3928
3978
|
function _getVideoState() {
|
|
3929
3979
|
if (!yVideo) return STATE._acceptedVideoState;
|
|
3930
|
-
const current =
|
|
3980
|
+
const current = _currentDocVideoState();
|
|
3931
3981
|
if (!STATE._acceptedVideoState) return current;
|
|
3932
3982
|
return _isVideoStateNewer(current, STATE._acceptedVideoState) ? current : STATE._acceptedVideoState;
|
|
3933
3983
|
}
|
|
@@ -4920,7 +4970,7 @@
|
|
|
4920
4970
|
clearTimeout(STATE._deferredObserve);
|
|
4921
4971
|
STATE._deferredObserve = setTimeout(() => {
|
|
4922
4972
|
if (!yVideo) return;
|
|
4923
|
-
_onObservedVideoState(
|
|
4973
|
+
_onObservedVideoState(_currentDocVideoState(), true);
|
|
4924
4974
|
}, Math.max(50, STATE.localIntentUntil - Date.now() + 60));
|
|
4925
4975
|
return;
|
|
4926
4976
|
}
|
|
@@ -6579,6 +6629,8 @@
|
|
|
6579
6629
|
.sync-bubble { position:absolute; top:20px; left:50%; transform:translate(-50%,-20px); background:var(--glass-strong); color:#fff; padding:10px 22px; border-radius:24px; border-left:4px solid var(--accent); font-weight:600; opacity:0; transition:opacity 0.4s, transform 0.4s; z-index:1900; white-space:nowrap; }
|
|
6580
6630
|
.sync-bubble.visible { opacity:1; transform:translate(-50%,0); }
|
|
6581
6631
|
/* bubbles */
|
|
6632
|
+
.nexus-container.nx-composing .nx-bubbles { top:calc(70px + var(--nx-compose-h, 120px) + env(safe-area-inset-top, 0px)); }
|
|
6633
|
+
.nx-bubbles { transition:top 0.25s ease; }
|
|
6582
6634
|
.nx-bubbles { position:absolute; top:calc(64px + env(safe-area-inset-top, 0px)); left:calc(12px + env(safe-area-inset-left, 0px)); z-index:1950; display:flex; flex-direction:column; gap:8px; width:min(300px, calc(100% - 24px)); pointer-events:none; }
|
|
6583
6635
|
.nx-bubble { pointer-events:auto; display:block; width:100%; text-align:left; border:none; cursor:pointer; color:#fff; font:inherit; background:rgba(18,20,26,0.52); -webkit-backdrop-filter:blur(3px); backdrop-filter:blur(3px); text-shadow:0 1px 2px rgba(0,0,0,0.8);
|
|
6584
6636
|
border-radius:16px 16px 16px 6px; padding:9px 12px 10px; box-shadow:0 8px 24px rgba(0,0,0,0.45); border-left:4px solid var(--c, #69f0ae); opacity:0; transform:translateX(-24px) scale(0.96); transition:opacity 0.25s ease, transform 0.25s ease; }
|
|
@@ -6627,13 +6679,16 @@
|
|
|
6627
6679
|
.nx-next-k { font-size:11px; font-weight:800; opacity:0.75; text-transform:uppercase; letter-spacing:0.04em; }
|
|
6628
6680
|
.nx-next-l { font-size:14px; font-weight:800; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
|
6629
6681
|
.nx-next-x { flex:0 0 auto; width:28px; height:28px; border:none; border-radius:50%; background:rgba(255,255,255,0.12); color:#fff; cursor:pointer; font-size:12px; }
|
|
6630
|
-
|
|
6682
|
+
/* At the top: subtitles live at the bottom of the picture, and on phones the keyboard covers the bottom too. */
|
|
6683
|
+
.nx-compose { position:absolute; left:50%; top:calc(60px + env(safe-area-inset-top, 0px)); z-index:2060; width:min(520px, calc(100% - 24px)); transform:translate(-50%, -14px) scale(0.96); opacity:0; pointer-events:none;
|
|
6631
6684
|
background:rgba(16,18,24,0.58); -webkit-backdrop-filter:blur(4px); backdrop-filter:blur(4px); border-radius:18px; padding:8px 8px 9px; text-shadow:0 1px 2px rgba(0,0,0,0.75); box-shadow:0 14px 36px rgba(0,0,0,0.55), 0 0 0 1px color-mix(in srgb, var(--c, #69f0ae) 50%, transparent);
|
|
6632
6685
|
transition:opacity 0.22s ease, transform 0.25s cubic-bezier(.2,1.3,.4,1); display:flex; flex-direction:column; gap:6px; }
|
|
6633
6686
|
.nx-compose.open { opacity:1; transform:translate(-50%, 0) scale(1); pointer-events:auto; }
|
|
6634
6687
|
.nx-compose-head { display:flex; align-items:center; gap:6px; padding:0 2px 0 6px; }
|
|
6635
6688
|
.nx-compose-to { flex:1; min-width:0; font-size:12px; font-weight:800; color:var(--c, #69f0ae); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
6636
6689
|
.nx-compose-x { flex:0 0 auto; width:28px; height:28px; border:none; border-radius:50%; background:rgba(255,255,255,0.08); color:#fff; cursor:pointer; font-size:13px; font-family:inherit; }
|
|
6690
|
+
.nx-compose-sub { font-size:13px; line-height:1.35; padding:5px 10px; border-radius:10px; background:rgba(0,0,0,0.35); color:#fff; font-weight:600; overflow:hidden; display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; }
|
|
6691
|
+
.nx-compose-sub:empty { display:none; }
|
|
6637
6692
|
.nx-compose-chips { display:flex; gap:6px; overflow-x:auto; padding:0 2px; }
|
|
6638
6693
|
.nx-compose-row { display:flex; gap:6px; }
|
|
6639
6694
|
.nx-compose-input { flex:1; min-width:0; background:rgba(255,255,255,0.08); border:1px solid rgba(255,255,255,0.14); color:#fff; border-radius:999px; padding:9px 14px; font-size:16px; font-family:inherit; outline:none; }
|
|
@@ -6701,7 +6756,9 @@
|
|
|
6701
6756
|
/* together */
|
|
6702
6757
|
.react-sep { width:1px; align-self:stretch; background:rgba(255,255,255,0.18); margin:2px 2px; }
|
|
6703
6758
|
.nexus-container.nx-shake { animation:nxShake 0.5s ease; }
|
|
6704
|
-
|
|
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; } }
|
|
6705
6762
|
.nx-ping-catcher { position:absolute; inset:0; z-index:2100; pointer-events:auto; cursor:crosshair; background:rgba(0,0,0,0.12); }
|
|
6706
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; }
|
|
6707
6764
|
.nx-ping { position:absolute; z-index:2050; width:0; height:0; pointer-events:none; }
|
|
@@ -6924,6 +6981,9 @@
|
|
|
6924
6981
|
}
|
|
6925
6982
|
shadow = host.attachShadow({ mode: 'open' });
|
|
6926
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()));
|
|
6927
6987
|
if (!SUPPORTS_TOP_LAYER) {
|
|
6928
6988
|
// Without the top layer, paint order decides between equal z-indexes: stay last.
|
|
6929
6989
|
new MutationObserver(() => {
|
|
@@ -6973,6 +7033,11 @@
|
|
|
6973
7033
|
const btnCinema = mk('nxu-cinema', '🍿 ' + t('cinema'), t('cinemaTitle'));
|
|
6974
7034
|
const btnAutoCinema = mk('nxu-auto-cinema', '');
|
|
6975
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')); };
|
|
6976
7041
|
const btnPiP = mk('nxu-pip', '📺 PiP');
|
|
6977
7042
|
const btnRate = mk('nxu-rate', '⏩ 1x', t('rateTitle'));
|
|
6978
7043
|
// Talking happens in speech bubbles and quick ink now: the menu opens those directly
|
|
@@ -7086,7 +7151,7 @@
|
|
|
7086
7151
|
toast.setAttribute('role', 'status');
|
|
7087
7152
|
|
|
7088
7153
|
// Sketching and chat live in the reaction bar (🖍️ 💬); the menu does not repeat them.
|
|
7089
|
-
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);
|
|
7090
7155
|
uiWrapper.append(dock, reactPill, fab);
|
|
7091
7156
|
nexusContainer.append(drawCanvas, uiWrapper, chatPanel, subLogPanel, manualModal, transModal, toast, debugPanel);
|
|
7092
7157
|
shadow.appendChild(nexusContainer);
|
|
@@ -7812,6 +7877,8 @@
|
|
|
7812
7877
|
};
|
|
7813
7878
|
head.append(me, to, full, x);
|
|
7814
7879
|
const chips = createEl('div', 'nx-compose-chips');
|
|
7880
|
+
// The line being spoken right now, so writing a message never means missing it.
|
|
7881
|
+
const subEcho = createEl('div', 'nx-compose-sub');
|
|
7815
7882
|
const row = createEl('div', 'nx-compose-row');
|
|
7816
7883
|
const input = createEl('input', 'nx-compose-input', '', { type: 'text', maxlength: '500', placeholder: t('talkPlaceholder'), enterkeyhint: 'send', autocomplete: 'off' });
|
|
7817
7884
|
const send = createEl('button', 'nx-compose-send', '➤', { type: 'button', 'aria-label': t('talkSend') });
|
|
@@ -7823,7 +7890,7 @@
|
|
|
7823
7890
|
});
|
|
7824
7891
|
input.addEventListener('input', () => { SOCIAL.typing(); this._armClose(); });
|
|
7825
7892
|
row.append(input, send);
|
|
7826
|
-
wrap.append(head, chips, row);
|
|
7893
|
+
wrap.append(head, subEcho, chips, row);
|
|
7827
7894
|
['pointerdown', 'click', 'touchstart'].forEach(ev => wrap.addEventListener(ev, (e) => e.stopPropagation()));
|
|
7828
7895
|
container.appendChild(wrap);
|
|
7829
7896
|
this._composer = wrap;
|
|
@@ -7838,6 +7905,12 @@
|
|
|
7838
7905
|
const cnt = this._composer.querySelector('.nx-hist-count');
|
|
7839
7906
|
if (cnt) cnt.textContent = n ? (n > 99 ? '99+' : String(n)) : '';
|
|
7840
7907
|
this._composer.classList.add('open');
|
|
7908
|
+
clearInterval(this._subTimer);
|
|
7909
|
+
this._subTimer = setInterval(() => this._echoSubtitle(), 300);
|
|
7910
|
+
this._echoSubtitle();
|
|
7911
|
+
// The bubbles step down below the open box (both live at the top now).
|
|
7912
|
+
const host = getContainer();
|
|
7913
|
+
if (host) { host.classList.add('nx-composing'); requestAnimationFrame(() => host.style.setProperty('--nx-compose-h', (this._composer.offsetHeight || 120) + 'px')); }
|
|
7841
7914
|
this._fitKeyboard();
|
|
7842
7915
|
if (!this._vvBound && window.visualViewport) {
|
|
7843
7916
|
this._vvBound = true;
|
|
@@ -7869,9 +7942,23 @@
|
|
|
7869
7942
|
if (!this._composer) return;
|
|
7870
7943
|
if (this._composer.classList.contains('open')) UI_GUARD.shield();
|
|
7871
7944
|
this._composer.classList.remove('open');
|
|
7945
|
+
clearInterval(this._subTimer);
|
|
7946
|
+
getContainer()?.classList.remove('nx-composing');
|
|
7872
7947
|
try { this._input?.blur(); } catch (_) { }
|
|
7873
7948
|
},
|
|
7874
7949
|
isOpen() { return !!this._composer?.classList.contains('open'); },
|
|
7950
|
+
_echoSubtitle() {
|
|
7951
|
+
const box = this._composer?.querySelector('.nx-compose-sub');
|
|
7952
|
+
if (!box) return;
|
|
7953
|
+
if (!this.isOpen()) { clearInterval(this._subTimer); return; }
|
|
7954
|
+
let text = '';
|
|
7955
|
+
const finalSlot = shadow?.getElementById('nxu-final-slot');
|
|
7956
|
+
const last = finalSlot?.lastElementChild;
|
|
7957
|
+
if (last && TRANSLATOR.active) text = (last.querySelector('.sub-final-text')?.textContent || '' || '').trim();
|
|
7958
|
+
if (!text) { try { text = COVER._cueText(); } catch (_) { } }
|
|
7959
|
+
text = String(text || '').replace(/\s+/g, ' ').trim().slice(0, 160);
|
|
7960
|
+
if (box.textContent !== (text ? '💬 ' + text : '')) box.textContent = text ? '💬 ' + text : '';
|
|
7961
|
+
},
|
|
7875
7962
|
// Quick replies in the reader's language; ✏️ edits them (saved on this device).
|
|
7876
7963
|
_renderChips() {
|
|
7877
7964
|
const chips = this._composer?.querySelector('.nx-compose-chips');
|
|
@@ -8151,6 +8238,97 @@
|
|
|
8151
8238
|
}
|
|
8152
8239
|
};
|
|
8153
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
|
+
|
|
8154
8332
|
// ───────────────────────── Automatic cinema
|
|
8155
8333
|
// Opening a page that is all about one video (an episode, a movie — not a feed with many
|
|
8156
8334
|
// posts) switches to cinema mode as soon as the video is ready. It keeps the site's own
|
|
@@ -8285,9 +8463,13 @@
|
|
|
8285
8463
|
_cancelledFor: '',
|
|
8286
8464
|
_count: null,
|
|
8287
8465
|
enabled() { return STORE.get('nxu_auto_next', true) !== false; },
|
|
8288
|
-
|
|
8289
|
-
|
|
8290
|
-
|
|
8466
|
+
// Words that clearly mean "next episode" …
|
|
8467
|
+
RX_TEXT: /(다음\s*(화|회|편|회차|에피소드|episode|ep\.?)|next\s*(episode|ep\b|ep\.|part|chapter)|次の(話|エピソード)|下一[集话話]|siguiente\s*episodio|épisode\s*suivant|nächste\s*folge|próximo\s*episódio)/i,
|
|
8468
|
+
// … and ones that only might ("다음", "Next", ⏭): they need a matching class/id too.
|
|
8469
|
+
RX_WEAK: /(^\s*다음\s*[▶>»]?\s*$|^\s*next\s*[▶>»]?\s*$|^\s*⏭\s*$|次へ)/i,
|
|
8470
|
+
RX_ATTR: /(next[-_ ]?(ep|episode|eps|video|chapter)|episode[-_]?next|ep[-_]?next|btn[-_]?next[-_]?ep)/i,
|
|
8471
|
+
RX_ATTR_WEAK: /(btn[-_]?next|nav[-_]?next|\bnext\b)/i,
|
|
8472
|
+
RX_BAD: /(prev|이전|previous|前|上一|comment|댓글|page|페이지|article|post|글|게시물|기사|뉴스|story|reply)/i,
|
|
8291
8473
|
_visible(el) {
|
|
8292
8474
|
if (!el.isConnected || el.closest('#nxu-host')) return false;
|
|
8293
8475
|
const r = el.getBoundingClientRect();
|
|
@@ -8303,7 +8485,7 @@
|
|
|
8303
8485
|
const here = location.origin + location.pathname + location.search;
|
|
8304
8486
|
const out = [];
|
|
8305
8487
|
const rel = document.querySelector('a[rel~="next"][href], link[rel~="next"][href]');
|
|
8306
|
-
if (rel) out.push({ el: rel.tagName === 'A' ? rel : null, href: rel.href, label: rel.tagName === 'A' ? this._label(rel) : '', score:
|
|
8488
|
+
if (rel && !this.RX_BAD.test(rel.href)) out.push({ el: rel.tagName === 'A' ? rel : null, href: rel.href, label: rel.tagName === 'A' ? this._label(rel) : '', score: 30 });
|
|
8307
8489
|
const nodes = document.querySelectorAll('a[href], button, [role="button"], [onclick]');
|
|
8308
8490
|
let n = 0;
|
|
8309
8491
|
for (const el of nodes) {
|
|
@@ -8312,9 +8494,11 @@
|
|
|
8312
8494
|
const attrs = `${el.id} ${typeof el.className === 'string' ? el.className : ''} ${el.getAttribute('data-action') || ''}`;
|
|
8313
8495
|
let score = 0;
|
|
8314
8496
|
if (this.RX_TEXT.test(label)) score += 60;
|
|
8315
|
-
if (this.
|
|
8497
|
+
else if (this.RX_WEAK.test(label)) score += 20;
|
|
8498
|
+
if (this.RX_ATTR.test(attrs)) score += 40;
|
|
8499
|
+
else if (this.RX_ATTR_WEAK.test(attrs)) score += 15;
|
|
8316
8500
|
if (!score) continue;
|
|
8317
|
-
if (this.RX_BAD.test(label) || /prev/i.test(attrs)) score -= 70;
|
|
8501
|
+
if (this.RX_BAD.test(label) || /prev|page|post|article|comment/i.test(attrs)) score -= 70;
|
|
8318
8502
|
if (score <= 0 || !this._visible(el)) continue;
|
|
8319
8503
|
const href = el.tagName === 'A' ? el.href : '';
|
|
8320
8504
|
if (href) {
|
package/package.json
CHANGED